본문 바로가기
Study

[스터디] 객체 지향 원리의 이해(총정리)

by DuncanKim 2022. 7. 21.
728x90

[스터디] 객체 지향 원리의 이해(총정리)

 

 

스터디가 2주차에 접어들었다.

저번주에는 객체 지향의 전신인 절차/구조적 프로그래밍에 대해서 알아보았다면, 이제는 본격적으로 객체 지향이란 무엇인지, 어디까지 발전해있는지를 알아보는 시간을 가진다.

 

<<스프링 입문을 위한 자바 객체 지향의 원리와 이해>>라는 책은 자바의 객체 지향의 개념을 안다고 생각하는 입문자들에게 '너 확실하게 이해한 것 맞니?' 라는 생각을 하게 만들고, 다시 제대로 이해하게 만든다.

 

한 달 여간 자바의 여러 면면을 알아왔고, 객체 지향의 개념에 대해서 80% 정도는 이해하고 있다고 생각했는데, 책을 읽으며 반성하게 되었고, 더 나아지는 과정을 얻을 수 있었다. 또한 객체 지향 속의 개념어의 의미를 더 명확히하고 친숙히 하는 데에 있어서 많은 도움이 되었다.

 

클래스부터 super까지 모두 훑어보는 3장과 4장, 지금부터 보도록 하겠다.

 

 

0. 목차


1. 자바와 객체 지향

     1) 객체 지향이란 무엇일까

     2) 객체 지향의 4대 특성(객체지향 새롭게 바라보기)

         (1) 클래스와 객체

         (2) 추상화(모델링)

         (3) 상속 : 재사용 + 확장

         (4) 다형성 : 사용편의성

         (5) 캡슐화 : 정보 은닉

 

2. 자바가 확장한 객체 지향

     1) abstract - 추상 메서드와 추상 클래스

     2) 생성자

     3) static 블록

     4) final

     5) instanceof 

     6) interface & implements

     7) this

     8) super


 

1. 자바와 객체 지향(3장)

 

1) 객체 지향이란 무엇일까. 

 

기존 절차/구조적 프로그래밍에서 중요한 것은 '함수'였다. 함수는 코드를 논리적 단위로 구분하고 분할해서 정복하자는 Divide & Conquer의 뜻을 가지고 있다. 이런 함수로 인해서 몇 만 라인에 달하는 코드를 획기적으로 줄일 수 있었고, 논리가 분할되면서 프로그래밍 하기가 쉬워졌다.

 

객체 지향은 여기에 한 술을 더 떠서 컴퓨터에 맞춰 사고하는 방식에서 벗어나서 '현실세계'를 인지하는 방식으로 프로그램을 만들자는 생각에 나온 것이다. 

 

세상에 존재하는 모든 것은 사물, 즉 객체  ▶︎  각각의 사물은 고유함  ▶︎  사물은 속성을 가짐  ▶︎  사물은 행위를 함

 

사람 중에 '문상훈'씨 라는 존재는 '사람'이라는 분류에 속한다. 사람의 숫자는 다양하지만, 이 '문상훈'이라는 사람은 누구도 대체할 수 없는 고유한 사람이다. 그러한 '문상훈'씨는 나이와 키, 몸무게라는 특유의 속성(property)을 가지고 있고, 연기하기, 개그하기, 강의하기, 밥먹기 등의 행위를 한다.

 

만약 객체 지향 이전이었다면, 문상훈씨의 나이와 키 등의 속성, 연기하기 등의 행위를 하나의 단위로 묶지 않고, 속성 따로, 메서드 따로 분리된 형태로 프로그램을 작성했을 것이다. 그렇지만, 객체 지향은 이러한 인간의 상식적인 직관과 이어지는 방식으로 프로그래밍을 할 수 있게 해준다. 속성과 행위를 가지는 특정 객체가 어떤 역할을 할 수 있도록 도와주는 것이다.

 

 

2) 객체 지향의 4대 특성(객체 지향 새롭게 바라보기)

 

우리는 객체지향이 네 가지의 특성을 가지고 있음을 알아야 한다.

캡슐화(정보은닉)  /  상속(재사용)  /  추상화(모델링)  /  다형성(사용 편의)

이 중 상속에 대해서는 시각을 교정할 필요가 있다. 또한 이와 더불어, 클래스와 객체의 관계에 대해서도 시각을 교정할 필요가 있다.

 

 

(1) 클래스와 객체

 

'붕어빵틀 - 붕어빵'

이제는 버려야 할 개념이다. 이 비유를 그대로 접목해서 인스턴스를 생성한다고 해보자.

 

붕어빵틀 붕어빵 = new 붕어빵틀();
금형기계 붕어빵틀 = new 금형기계();

새로운 붕어빵틀을 하나 만들었더니 붕어빵이 되었다. / 새로운 금형기계를 하나 만들었더니 붕어빵틀이 되었다.

제대로 이해가 될 수 없는 비유이다.

 

제대로 정립을 하자면 아래와 같이 할 수 있다.

클래스 : 객체 = 펭귄 : 뽀로로 = 사람 : 김연아

객체는 특유의 속성(property)를 가진다. 이름이라던지, 나이라던지, 색깔이라던지.

클래스는 이와 같이 '분류'의 개념일 뿐 실체가 아니다. 객체가 실체라고 할 수 있다.

 

"클래스는 분류 - 객체는 현실의 실체"라는 개념을 이제는 가지고 가야 한다.

 

객체 : 세상에 존재하는 유일무이한 사물
클래스 : 분류, 집합. 같은 속성과 기능을 가진 객체를 총칭하는 개념

 

(2) 추상화(모델링)

 

객체 지향의 추상화는 곧 모델링이다.

추상화란 구체적인 것을 분해해서 관찰자가 관심있는 특성만 가지고 재조합하는 것이다.

 

클래스는 객체의 속성과 메서드를 가지고 있다. 클래스 안에는 추상적인 속성과 메서드가 있는데, 그 분류가 가져야 하는 모든 특성을 가지고 있을 필요가 있을까?

 

그럴 필요가 없다. 왜냐하면, 무엇을 만들고 싶느냐, 어디에 쓰이는 것이냐에 따라서 취사선택 하여 속성을 나열하면 그만인 것이다.

 

그래서 추상화에 대한 개념이 다음과 같이 도출된다.

추상화란 구체적인 것을 분해해서 관심 영역(애플리케이션 경계)에 있는 특성만 가지고 재조합하는 것 (= 모델링)

 

예를 들어 병원 애플리케이션에서 쓰이는 '사람' 클래스와 은행 어플리케이션에서 쓰이는 '사람' 클래스의 속성과 메서드는 달라도 된다. 그 쓰임새에 따라서 사람의 혈액형이나 신체정보와 같은 것은 필요가 있을 수도 없을 수도 있기 때문이다.

 

 

++ Bonus Track. 메모리 구조와 객체(heap)

public class MouseDriver {
    public static void main(String[] args) {
        
        Mouse mickey = new Mouse();
        mickey.name = "미키";
        mickey.age = 85;
        mickey.countOfTail = 1;

        mickey.sing();

        mickey = null;

        Mouse jerry = new Mouse();

        jerry.name = "제리";
        jerry.age = 73;
        jerry.countOfTail = 1;

        jerry.sing();
    }
}

class Mouse{
    public String name;
    public int age;
    public int countOfTail;

    public void sing() {
        System.out.println(name + "찍찍 !!");
    }
}

이러한 코드가 있을 때, 메모리 안에서는 어떤 일이 일어나는 지 알아보겠다.

 

 

 

(3) 상속 : 재사용 + 확장

 

상속은 위계 관계 위에서 존재하는 것이 아니다. 분류 관계 위에서 존재하는 것이다.

부모-자식 관계로 표현하는 것보다, 상위-하위 클래스, 슈퍼-서브 클래스로 표현하는 것이 맞다.

상위 클래스로 갈수록 추상화, 일반화 되어 있으며, 하위 클래스로 갈수록 구체화, 특수화되어 있다.

 

상속 관계에서는 반드시 이 문장을 만족해야 한다.

'하위 클래스는 상위 클래스다.'

 

포유류는 동물이다.  /  고래는 포유류다.  /  고래는 동물이다.

 

이런 식의 관계가 성립해야 상속 관계가 성립한다고 볼 수 있다.

 

동물 animal = new 동물();
동물 mammal = new 포유류();
동물 bird = new 조류();
동물 whale = new 고래();
동물 sparrow = new 참새();

 

분류 관계라는 이야기가 위에서 이렇게 표현될 수 있는 것이다. 따라서 하나의 상위 클래스를 타입으로 하는 객체 참조 변수에 하위 클래스의 인스턴스를 만들어 넣을 수 있는 것이다.

 

 

++ is-a 관계

상속은 is-a 관계를 만족해야 한다는 이야기가 있다.
"펭귄 is a 동물"

그렇지만 완벽하게 맞는 말이 아니다. 이를 is a kind of로 바꾸면 더 정확한 해석이 가능하다.

펭귄 is a kind of 동물 -> 펭귄은 동물의 한 분류다.

 

 

++ is able to 관계

인터페이스와 클래스의 관계를 표현하는 것이다.
'구현 클래스는 인터페이스 할 수 있다.'와 같이 해석이 가능하다.

인터페이스는 '무엇을 할 수 있는'이라는 형태로 만드는 것이 좋다. 

 

 

(4) 다형성 : 사용편의성

 

객체지향에서 가장 기본적인 다형성은 오버라이딩과 오버로딩이다. 아래의 코드를 보면서 중요한 점을 익혀보자.

 

public class Driver{
    public static void main(String[] args) {
        Penguin pororo = new Penguin();

        pororo.name = "뽀로로";
        pororo.habitat = "남극";

        pororo.showName(); // 어머 내이름은 알아서 뭐하시게요.
        pororo.showName("초보람보"); //초보람보 안녕, 나는 뽀로로라고 해
        pororo.showHabitat(); // 뽀로로는 남극에 살아

        Animal pingu = new Penguin();

        pingu.name = "핑구";
        pingu.showName(); // 어머 내 이름은 알아서 뭐하시게요.

    }
}

class Animal{
    public String name;

    public void showName(){
        System.out.printf("안녕 나는 %s야. 반가워\n", name);
    }
}

class Penguin extends Animal{
    public String habitat;

    public void showHabitat(){
        System.out.printf("%s는 %s에 살아\n", name, habitat);
    }

    @Override
    public void showName(){
        System.out.println("어머 내 이름은 알아서 뭐하시게요.");
    }

    public void showName(String yourName){
        System.out.printf("%s 안녕, 나는 %s라고 해\n", yourName, name);
    }
}

 

객체 지향에 대해 많은 학습을 한 사람이라면, Driver 클래스에서 호출되는 함수가 정확히 무엇인지를 이야기할 수 있을 것이다.

호출된 네 개의 메서드 중 위의 세 개에 대해서는 무리없이 이해가 가는데, 상위 타입으로 선언된 핑구의 경우, 상위 타입의 메소드가 나오지 않고, 왜 하위 타입의 메서드가 호출되었을까?

 

상위 클래스 타입의 객체 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩한 메서드가 우선적으로 호출되기 때문이다.

한편, 재정의가 아니라 새롭게 만들어진 showName(String yourName)의 경우, 핑구가 호출할 경우 불러지지 않는다. 이 때는 '다운 캐스팅'을 해서 Penguin 클래스에 맞춰 주어야 함수 호출이 가능하다.

 

 

(5) 캡슐화 : 정보 은닉

 

접근 제어자의 활용과 관련있는 부분이다.

 

접근 제어자를 사용할 때, private 또는 public, default만을 사용하는 경우가 많다.

그렇지만, protected도 상속 관계가 아닌 같은 패키지의 파일에서 접근이 가능하다는 것을 알아야 한다.

 

또한 정적 멤버의 경우, 접근제어자를 private으로 설정할 경우, 메소드 스택 프레임 -> 힙 -> 스태틱 구역으로 2번에 걸친 연산을 거쳐야 하는 반면, public static 형식으로 접근을 하면 1회만에 접근이 가능하기에 접근 제어자 선택에 신경을 써야 함을 알아야 한다.

 

 

 

2. 자바가 확장한 객체 지향(4장)

 

객체 지향 언어는 여러 가지가 있다.

그 중에서도 자바는 객체 지향을 확장하기 위해 키워드를 사용한다. 대표적인 키워드를 여기에서 알아본다.

 

 

1) abstract - 추상 메서드와 추상 클래스

 

추상 클래스는 몸체가 없는 클래스이다. '선언'만 있는 것인데, 왜 필요한 것일까?

구조상 존재해야 하는데, 호출하면 난감한 메서드가 있을 수 있다.

 

예를 들면 다음과 같다.

abstract class 동물{
    운다(){
    }
}

class 고양이 extends 동물{
    @Override
    운다(){
    	System.out.println("야용");
    }
}

동물 타입의 참조 변수로 하위 클래스의 인스턴스가 가진 운다()를 호출하고 싶을 때, 상위 클래스의 운다() 메서드는 존재해야 한다.

그런데, 고양이의 메서드를 호출하지 않고, 동물의 메서드를 호출한다면 아무것도 나오지 않는 오류를 경험할 수 있다.

 

이처럼 메서드 선언은 있되 몸체는 없는 형태로 메서드를 구현하게 되면, 잘못된 호출을 방지할 수 있다. 그래서 추상 클래스 또는 메서드를 쓰는 것이다. 추상 클래스 또한 객체로 만들 수 없으니, 잘못된 인스턴스화를 방지할 수 있는 것이다.

 

또한 설계상의 이점도 있다. 하위 클래스가 추상 메서드를 꼭 실제로 구현해야 오류가 발생하지 않는다는 점에서 필수 메서드를 구현하게 할 수 있다.

 

잘못된 호출 방지, 설계상의 이점이 '추상'의 존재 이유라고 할 수 있겠다.

 

 

 

2) 생성자

 

동물 뽀로로 = new 동물();

여기서 new 뒤에 붙어있는 메소드 형태의 것이 생성자이다. 더 자세하게 설명하자면, 반환값이 없고 클래스명과 같은 이름을 가진 메서드는 객체를 생성하는 메서드라고 해서 '객체 생성자 메서드'(full name)라고 한다.

 

자바는 클래스 내부에 생성자를 만들지 않아도, 매개변수가 없는 기본 생성자를 만들어 준다. 그랬기 때문에, 생성자를 만들지 않아도 위와 같은 형태로 클래스의 인스턴스를 만들 수 있었던 것이다.

 

한편, 자바는 개발자가 클래스 내부에 인자가 있는 생성자를 하나라도 만든다면 기본 생성자를 만들어주지 않는다. 내부에 매개변수를 하나라도 가지는 생성자가 있을 때, 기본 생성자를 쓰고 싶으면, 인자 없이 생성자를 선언해주어야 기본 생성자를 사용할 수 있다.

 

 

 

3) static 블록

 

클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록이 있는데, 이를 static 블록이라고 한다.

 

public class Animal{
    static {
        System.out.println("동물 클래스를 불러왔습니다.");
    }
}

Animal pororo = new Animal(); // 동물 클래스를 불러왔습니다.

위와 같이 클래스 안에 스태틱 키워드를 쓰고, 블록 안에 코드를 구현하면, 그 클래스의 인스턴스를 최초로 생성했을 때, 코드가 실행이 된다.

단 한 번만 실행이 되며, 같은 타입으로 다른 객체를 만들 때 스태틱 블록이 한 번 더 실행되지는 않는다.

 

실무에서 많이 쓰이는 경우는 없지만, Spring Batch 프로젝트에서 쓰이기도 한다.

 

 

 

4) final

 

마지막, 최종을 뜻한다. 이 키워드가 붙으면 변경 불가한 '클래스, 변수, 메서드'가 된다.

 

final 클래스의 경우 다른 클래스가 final 클래스를 상속하지 못한다.
final 변수의 경우, 변경이 불가능해진다. 상수화 되는 것이다. 자바에서는 const를 예약어로 금지하고 final로 쓰도록 하고 있다.
final 메서드의 경우, 오버라이딩이 금지된다.

 

 

 

5) instanceof

 

만들어진 객체가 특정 클래스의 인스턴스인지 물어보는 연산자이다.

 

객체_참조_변수 instanceof 클래스명

 

객체 참조 변수가 클래스의 타입인지 물어보는 것이다.

 

instanceof의 경우 객체 지향 설계 5원칙 가운데 리스코프 치환의 원칙(LSP)을 위배하는 코드에서 주로 나타나는 연산자기 때문에 이 연산자가 보이면 리팩터링이 필요한 것이 아닌지를 고려해보아야 한다.

 

 

 

6) interface & implements

 

interface는 public 추상 메서드와 public 정적 상수만 가질 수 있다.

그런데, 따로 접근제한자나 static final을 붙이지 않아도 자바가 자동으로 알아서 이를 붙여준다.

 

interface Stoppable{
    double PI = 3.1415;
    final double absoluteZero = -275.15;
    
    void sayYes();
}

// 위와 같음

interface Stoppable{
    public static final double PI = 3.1415;
    public static final double absoluteZero = -275.15;
    
    public abstract void sayYes();
}

따로 예약어를 붙이지 않아도 자바는 알아서 아래와 같이 변경을 해주지만, 애초에 코드 작성을 아래와 같이 하는 것이 더 좋다. 기본 규칙을 모르는 사람도 알아볼 정도로 명확하기 때문이다.

 

++ 유의 사항(자바 8)

기존에는 정적 상수와 객체 추상 메서드만 가질 수 있었던 것에 반해, 자바 8부터는 디폴트 메서드라고 하는 객체 구상 메서드와 정적 추상 메서드를 지원할 수 있게 되었다. 이 점에 유의 해야 한다.

 

 

static 블록finalinstanceof  interface & implements  this

7) this

 

변수의 접근과 관련된 예약어이다.

지역변수보다 우선해서 클래스의 속성을 쓰고 싶을 때, this 키워드를 쓴다.

 

지역 변수와 속성(객체 변수, 정적 변수)의 이름이 같은 경우 지역 변수가 우선한다.
객체 변수와 이름이 같은 지역 변수가 있는 경우 객체 변수를 사용하려면 this를 접두사로 사용한다.
정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용한다.

 

 

 

8) super

 

바로 위 상위 클래스의 인스턴스를 지칭하는 키워드이다.

super.super와 같이 상위의 상위 클래스의 인스턴스에는 접근하지 못한다.

728x90

댓글