Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

끈기 있는 개발 공간

자바와 객체 지향 [스프링 입문을 위한 자바 객체지향의 원리와 이해 Ch3] 본문

Java

자바와 객체 지향 [스프링 입문을 위한 자바 객체지향의 원리와 이해 Ch3]

tenacy 2022. 8. 19. 23:23

객체지향을 이해하기 위한 Big Picture

  • 세상에 존재하는 모든 것은 사물, 즉 객체이다.
  • 각각의 사물은 고유하다.
  • 사물은 속성을 갖는다.
  • 사물은 행위를 한다.

객체 지향은 직관적입니다. 아니라구요? 그 오해는 이 장에서 차차 풀어나가도록 합시다.

객체 지향의 4대 특성

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

추상화: 모델링

  • 객체 - 유일무이한 사물
  • 클래스 - 분류

클래스를 사용해 object를 만들었다는 걸 강조할 때 instance라는 표현을 사용합니다. 이러한 과정 속에서 붕어빵과 붕어빵틀의 예시가 등장하게 됩니다. 하지만, 이는 객체와 클래스간의 관계를 완전히 설명할 수 없으므로 틀린 예시임을 알 수 있습니다.

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

상속: 재사용 + 확장

상속이란 말은 잘못되었습니다.

간단한 예시로 포유류와 동물의 관계를 살펴봅시다. 포유류는 동물을 상속한다고 하지만, 동물은 포유류의 부모가 아닙니다. 객체지향에서의 상속은 상위 클래스의 특성을 하위 클래스에서 상속하고 거기에 더해 필요한 특성을 추가, 즉 확장해서 사용할 수 있다는 의미입니다.

다음 예시로 상속을 살펴봅시다. 먼저 적절한 클래스를 만듭니다.

public class 동물 {
    String myClass;

    동물() {
        myClass = "동물";
    }

    void showMe() {
        System.out.println(myClass);
    }
}

public class 포유류 extends 동물 {
    포유류() {
        myClass = "포유류";
    }
}

public class 조류 extends 동물 {
    조류() {
        myClass = "조류";
    }
}

public class 고래 extends 포유류 {
    고래() {
        myClass = "고래";
    }
}

public class 박쥐 extends 포유류 {
    박쥐() {
        myClass = "박쥐";
    }
}

public class 참새 extends 조류 {
    참새() {
        myClass = "참새";
    }
}

public class 펭귄 extends 조류 {
    펭귄() {
        myClass = "펭귄";
    }
}

이제 main() 메서드를 만들어 실행합니다.

public class Drivier01 {
    public static void main(String[] args) {
        동물 animal = new 동물();
        포유류 mammalia = new 포유류();
        조류 bird = new 조류();
        고래 whale = new 고래();
        박쥐 bat = new 박쥐();
        참새 sparrow = new 참새();
        펭귄 penguin = new 펭귄();

        animal.showMe();
        mammalia.showMe();
        bird.showMe();
        whale.showMe();
        bat.showMe();
        sparrow.showMe();
        penguin.showMe();
    }
}

동물
포유류
조류
고래
박쥐
참새
펭귄

Process finished with exit code 0

절차적/구조적 프로그래밍의 입장에서 위 코드를 보면 하위 클래스에서 showMe() 메서드를 다시 작성하지 않아도 되므로 객체 지향이 큰 일을 해낸 겁니다. 재사용의 교훈을 주었습니다!

public class Drivier02 {
    public static void main(String[] args) {
        동물 animal = new 동물();
        동물 mammalia = new 포유류();
        동물 bird = new 조류();
        동물 whale = new 고래();
        동물 bat = new 박쥐();
        동물 sparrow = new 참새();
        동물 penguin = new 펭귄();

        animal.showMe();
        mammalia.showMe();
        bird.showMe();
        whale.showMe();
        bat.showMe();
        sparrow.showMe();
        penguin.showMe();
    }
}

실행결과는 Drivier01과 같습니다. “하위 클래스는 상위 클래스다”, 즉 “하위 분류는 상위 분류다”라는 말이 코드에서 직관적으로 표현됩니다. 확장의 교훈을 주었습니다!

public class Drivier03 {
    public static void main(String[] args) {
        동물[] animals = new 동물[7];

        animals[0] = new 동물();
        animals[1] = new 포유류();
        animals[2] = new 조류();
        animals[3] = new 고래();
        animals[4] = new 박쥐();
        animals[5] = new 참새();
        animals[6] = new 펭귄();

        for (동물 animal : animals) {
            animal.showMe();
        }
    }
}

반복문 하나로 모든 동물들이 자신을 드러낼 수 있습니다. 감동… 하십시오…

상속은 is a 관계를 만족해야 할까요?

  • 상위 클래스는 분류/집단이다.
  • 하위 클래스는 분류/집단이다.
  • 하나의 상위 클래스는 하나의 객체다.

삼단 논법에 의거하면 “하위 클래스는 하나의 객체다”라는 결론에 도달합니다. 논리가 성립되지 않습니다. 더 명확한 표현은 is a kind of 입니다.

지금까지의 내용을 다음 세 문장으로 정리할 수 있습니다.

  • 객체 지향의 상속은 상위 클래스의 특성을 재사용하는 것이다.
  • 객체 지향의 상속은 상위 클래스의 특성을 확장하는 것이다.
  • 객체 지향의 상속은 is a kind of 관계를 만족해야 한다.

왜 자바는 다중 상속을 지원하지 않을까요?

간단한 예시로 인어공주는 사람과 물고기를 상속한다고 생각해봅시다. 그럼 인어에게 수영이라는 행위는 물고기처럼 가슴, 등, 꼬리 지느러미로 헤엄치는 걸까요? 사람처럼 팔과 다리를 저어 수영하는 걸까요? 이처럼 다중 상속은 득실 관계에서 실이 더 많았기에 자바는 다중 상속을 지원하지 않습니다. 대신 인터페이스를 도입해 다중 상속의 득은 취하고 실은 과감히 버렸습니다.

인터페이스는 be able to, 즉 “무엇을 할 수 있는”이라는 표현 형태로 만드는 것이 좋습니다.

상위 클래스는 하위 클래스에 물려줄 특성이 풍성할수록 좋고(LSP에 의거), 인터페이스는 구현을 강제할 메서드가 적을수록 좋습니다(ISP에 의거). 인터페이스를 사용하는 예제를 살펴봅시다.

public class Animal {
    public String name;

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

public class Penguin extends Animal {
    public String habitat;

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

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

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

        pororo.showName();
        pororo.showHabitat();

        Animal pingu = new Penguin();

        pingu.name = "핑구";
        //pingu.habitat = "EBS";

        pingu.showName();
        //pingu.showHabitat();

        //Penguin happyfeet = new Animal();
    }
}

안녕 나는 뽀로로야. 반가워
뽀로로는  남극에 살아
안녕 나는 핑구야. 반가워

Process finished with exit code 0

  • Penguin pororo = new Penguin();
    • 펭귄 한 마리가 태어나니 펭귄 역할을 하는 pororo라 이름 지었다.
  • pororo.name = "뽀로로";
    • pororo의 name을 “뽀로로”라 하자.
  • pororo.habitat = "남극";
    • pororo의 habitat를 “남극”이라 하자.
  • pororo.showName();
    • pororo야 너의 이름을 보여다오.
  • pororo.showHabitat();
    • pororo야 너의 서식지를 보여다오.

코드를 보면서 이렇게 번역해서 읽기가 힘들다면 그 코드는 객체 지향 언어의 아름다움을 충분히 활용하지 못하고 있는 겁니다. 객체 지향은 직관적이어야 합니다.

다형성: 사용편의성

객체 지향에서 다형성이라고 하면 오버라이딩과 오버로딩이라고 할 수 있습니다.

  • 오버라이딩
    • ride: 올라타다
    • 방향성: 위
    • 인공위성 입장에서는 위로 올라탄 사람 한 명만 보임
  • 오버로딩
    • load: 적재하다
    • 방향성: 옆
    • 인공위성 입장에서는 옆으로 적재된 모든 적재물이 다 보임

public class Animal {
    public String name;

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

public class Penguin extends Animal {
    public String habitat;

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

    //오버라이딩
    public void showName() {
        System.out.println("어머 내 이름은 알아서 뭐하게요?");
    }

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

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(); //L14

        pingu.name = "핑구";

        pingu.showName();
    }
}

어머 내 이름은 알아서 뭐하게요?
초보람보 안녕, 나는 뽀로로라고 해
뽀로로는  남극에 살아
어머 내 이름은 알아서 뭐하게요?

Process finished with exit code 0

다음 그림은 14번째 줄을 실행한 후의 T 메모리 스냅샷입니다.

캡슐화: 정보 은닉

  • private
  • [default]
  • protected
  • public

자바에서 정보 은닉이라고 하면 위와 같은 접근 제어자들이 생각날 겁니다. 접근 제어자가 객체 멤버(인스턴스 멤버)와 쓰일 때와 정적 멤버(클래스 멤버)와 함께 쓰일 때를 비교해서 살펴봅시다.

다음과 같은 UML 다이어그램을 참고하여 자바 코드로 구현해보고, 각 스택 프레임에서 ClassA의 non-static 혹은 static 멤버에 접근할 수 있는지를 결정해봅시다.

[상황 1]

[상황 2]

public class ClassA {
		private /*static*/ int pri;
    /*static*/ int def;
    protected /*static*/ int pro;
    public /*static*/ int pub;

    void runSomething() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> O O O O
//        this.ㅁㅁㅁ -> O O O O
//        ㅁㅁㅁ -> O O O O

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> O O O O
//        객체.ㅁㅁㅁ -> O O O O
//        this.ㅁㅁㅁ -> O O O O
//        ㅁㅁㅁ -> O O O O
    }

    static void runStaticThing() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> O O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> O O O O
//        객체.ㅁㅁㅁ -> O O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> O O O O
    }
}

public class ClassAA extends ClassA {
    void runSomething() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X O O O
//        ㅁㅁㅁ -> X O O O

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X O O O
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X O O O
//        ㅁㅁㅁ -> X O O O
    }

    static void runStaticThing() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X O O O
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X O O O
    }
}

public class ClassB {
    void runSomething() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X O O O
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X
    }

    static void runStaticThing() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X O O O
//        객체.ㅁㅁㅁ -> X O O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X
    }
}

public class ClassAB extends ClassA {
    void runSomething() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X O O
//        ㅁㅁㅁ -> X X O O

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X X X O
//        객체.ㅁㅁㅁ -> X X O O
//        this.ㅁㅁㅁ -> X X O O
//        ㅁㅁㅁ -> X X O O
    }

    static void runStaticThing() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X X X O
//        객체.ㅁㅁㅁ -> X X O O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X O O
    }
}

public class ClassC {
    static void runStaticThing() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X X X O
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X
    }

    void runSomething() {

        /*member variable of ClassA is non-static*/
//        클래스.ㅁㅁㅁ -> X X X X
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X

        /*member variable of ClassA is static*/
//        클래스.ㅁㅁㅁ -> X X X O
//        객체.ㅁㅁㅁ -> X X X O
//        this.ㅁㅁㅁ -> X X X X
//        ㅁㅁㅁ -> X X X X
    }
}

  • 풀이 순서는 private > [default] > protected > public 입니다.
  • 도전해봅시다!

ClassAB에서 ClassA의 멤버 변수가 non-static 과 static인 경우 static 스택 프레임에서의 객체 접근(객체.ㅁㅁㅁ)을 살펴봅시다. non-static은 protected 접근이 안 되지만, static은 protected 접근이 됩니다. 이유가 뭘까요?

non-static은 힙 영역에 아직 멤버 변수가 할당이 안 되어 있기 때문에 protected와 상관 없이 접근이 안 되는 것이고, static은 스태틱 영역에 static 멤버 변수가 할당이 되어 있기 때문에 protected에 영향을 받아 접근이 가능한 겁니다.

참조 변수의 복사

지금까지의 학습을 잘 수행했다면 별 새로운 내용은 없습니다. 중요한 부분만 정리해보겠습니다.

  • 기본 자료형 변수는 값을 값 자체로 판단한다.
  • 참조 자료형 변수는 값을 주소, 즉 포인터로 판단한다.
  • 기본 자료형 변수를 복사할 때, 참조 자료형 변수를 복사할 때 일어나는 일은 같다. 즉, 가지고 있는 값을 그대로 복사해서 넘겨 준다.

Comments