java

자바 객체 지향의 원리와 이해_Chapter03_자바와 객체 지향

강용민 2023. 3. 20. 17:52

객체 지향은 인간 지향이다.

객체 지향을 이해하기 위해 먼저 큰 그림을 생각해 보자.

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

그리고 사물 하나하나 이해하기보다는 사물을 분류(class)해서 이해하는 것이 인간의 인지법이다.

  • 직립보행을 하며 말을 하는 존재를 사람이라 분류한다.
  • 연미복, 짧은 다리, 날지 못하는 새를 펭귄이라 분류한다.

객체 지향은 인간의 인지 및 사고 방식까지 프로그래밍에 접목하는 인간 지향을 실천하고 있다.

 

객체 지향의 4대 특성

보통 객체 지향의 4대 특성이라 함은 다음과 같다.

  • 캡슐화(Capsulation) 
  • 상속(Ingeritance)
  • 추상화(Abstraction)
  • 다형성(Polymorphism)

해당 특성에 대해 알아보기 전에 클래스와 객체의 관계를 다시 재정립 할 필요가 있다.

 

클래스 vs 객체 = 붕어빵틀 vs 붕어빵 ???

보통 클래스와 객체를 '붕어빵틀과 붕어빵'의 관계라 설명하지만 이 책의 저자는 아니라 말한다. 클래스와 객체의 관계는 붕어빵틀과 붕어빵의 관계(factory)와는 다른 '분류과 실체'의 관계이다.

예를 들면 김연아(실체)는 사람(분류)이다. 뽀로로(실체)는 펭귄(분류)이다. 즉 클래스와 객체의 관계는 어떤 현실 세계의 분류에 대한 실체이지, factory에서 무언가를 만들어 내는 것처럼 부모-자식 관계가 아니다.

이제 4대 특성에 대해 알아보자.

 

추상화 : 모델링

추상화에 대해 이해하기 위해서는 '추상'이라는 단어의 이해가 필요해보인다.

추상화는 영어로 Abstraction이다. 그리고 Abstraction의 다른 뜻은 '추출'이라는 뜻이다. 애초에 추상이라는 의미는 '사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것'이다. 이를 이해하기 편하게 바꿔보면 '구체적인 것을 분해해서 관찰자가 관심 있는 특성만 가지고 재조합하는 것'이라 정리할 수 있다.

이와 같은 뜻을 하는 것은 바로 모델(model)이다. 모델은 실제 사물을 정확히 복제하는게 아니라 목적에 맞게 관심 있는 특성만을 추출해서 표현하는 것이다. 바로 모델은 추상화를 통해 실제 사물을 단순하게 표현하는 것이다. 즉, 추상화는 모델링이다.

 

상속 : 재사용 + 확장

이 책의 저자는 '상속' 또한 잘못된 오해라 말한다. '상속'이 아닌 '재사용과 확장'으로 이해해야 한다. 객체 지향에서의 상속은 상위 클래스의 특성을 하위 클래스에서 상속(특성 상속)하고 거기에 더해 필요한 특성을 추가, 즉 확장해서 사용할 수 있다. 특성을 상속하는 것이지 클래스를 상속하는 것이 아니다. 즉, 객체 지향에서 상속은 'is a kind of'관계이다. 예를들어보자.

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

그래서 '부모 클래스-자식 클래스'라는 표현보다는 '상위 클래스 - 하위 클래스' 또는 '슈퍼 클래스 - 서브 클래스'라는 표현이 더 바람직하다. 그리고 상위 클래스 쪽으로 갈수록 추상화, 일반화됐다고 말하며, 하위 클래스 쪽으로 갈수록 구최화, 특수화됐다고 말한다.

정리를 하면 다음과 같다.

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

실제로 Java에서는 Ingeritance(상속)이라는 키워드는 존재하지 않는다. 대신 extends(확장)가 존재한다.

 

다중 상속과 자바

다들 알다시피 Java는 다중 상속을 지원하지 않는다. 대신 인터페이스를 도입해 다중 상속의 득은 취하고 실은 과감히 버렸다.

더보기

다중 상속의 문제점

  • 상속받은 여러 기초 클래스에 같은 이름의 멤버가 존재할 가능성이 있다.
  • 하나의 클래스를 간접적으로 두 번 이상 상속받을 가능성이 있다.
  • 가상 클래스가 아닌 기초 클래스를 다중 상속하면, 기초 클래스 타입의 포인터로 파생 클래스를 가리킬 수 없다.

그럼 다중 상속을 포기하고 대신 인터페이스를 도입한 자바에서 인터페이스는 어떤 관계를 나타내는 것일까??

 

상속과 인터페이스

인터페이스는 상속과 같은 'is a kind of'관계가 아닌 'be able to', 즉 '무엇을 할 수 있는'이라는 표현 형태로 만드는 것이 좋다.

여기서 한 가지 더 생각해 보자. 상위 클래스는 하위 클래스에게 특성(속성과 메서드)을 상속해 주고, 인터페이스는 클래스가 '무엇을 할 수 있다'라고 하는 기능을 구현하도록 강제하게 된다. 여기서 퀴즈가 있다.

  • 상위 클래스는 하위 클래스에게 물려줄 특성이 많을수록 좋을까? 적을수록 좋을까??
  • 인터페이스는 구현을 강제할 메서드가 많을수록 좋을까??? 적을수록 좋을까??

이에 대한 답은 객체 지향 설계 5대 원칙(SOLID)와 관계가 있다.

상위 클래스의 특성이 풍부하면 좋은 이유는 LSP(Liskov Substitution Principle)과 관련이 있는데, '프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.'라는 원칙이다. 즉, 상위 타입 객체를 하위 타입 객체로 치환해도 정상적으로 동작해야 한다. 그렇기 위해서는 상위 클래스는 모든 하위 클래스에서 사용되는 공통된 메서드는 최대한 많이 갖고 있는 것이 좋다.

반대로 인터페이스에 메서드가 적을수록 좋은 이유는 ISP(Inteface Segregation Principle)과 관련이 있는데 '어떤 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다'라는 원칙이다. 인터페이스는  기능 구현을 강제하는데, 만약 인터페이스에 메서드가 많다면 사용하지 않는 기능도 클라이언트에게 강제할 수 있다. 그렇기에 인터페이스는 구현을 강제할 메서드를 줄이고 대신 여러 개의 세부적인 인터페이스를 만드는 것이 좋다.

 

다형성 : 사용편의성

객체 지향에서 다형성이라고 하면 오버라이딩(overriding)과 오버로딩(overloading)이라 할 수 있다.

  • 오버라이딩 : 같은 메서드 이름, 같은 인자 목록으로 상위 클래스의 메서드를 재정의
  • 오버로딩 : 같은 메서드 이름, 다른 인자 목록으로 다수의 메서드를 중복 정의

오버 로딩은 함수명 하나를 가지고 인자 목록만 달리하면 되니 얼마나 사용하기 편리한지 알 수 있다. 특히 자바 5에서 추가된 제네릭을 이용하면 하나의 함수만 구현해도 다수의 함수를 구현한 효과를 낼 수 있다. 다형성에 대해 사용 편의성이라고 정의한 이유는 바로 이 때문이다.

 

캡슐화 : 정보 은닉

자바에서 정보 은닉이라고 하면 접근 제어자인 private, [default], protected, public이 생각날 것이다. 그리고 접근자 및 설정자 메서드도 머리속을 스쳐 지나갈 것이다.

 

객체 멤버의 접근 제어자

  • private : 해당 변수, 메서드는 해당 클래스에서만 접근이 가능하다.
  • default : 해당 변수, 메서드는 해당 패키지 내에서만 접근이 가능하다.
  • protected : 해당 변수, 메서드는 동일 패키지의 클래스 또는 해당 클래스를 상속받은 다른 패키지의 클래스에서만 접근이 가능하다.
  • public : : 해당 변수, 메서드는 어던 클래스에서라도 접근이 가능하다.

다만 생각해봐야 할 것이 있다. 만약 aaa.jar 파일 안에 packageOne 패키지가 있고, bbb.jar 파일 안에 같은 이름을 가진 packageOne 패키지가 있다면 어떻게 될까??? bbb.jar 파일 내부의 packageOne 패키지 내 클래스나 객체에서 aaa.jar 파일 내부의 packageOne 패키지 내 클래스나 객체가 가진 public 멤버뿐만 아니라 [default] 멤버와 protected 멤버에 자유롭게 접근할 수 있다.

그리고 다음 두 가지 사항을 기억하자.

  • 상속을 받지 않았다면 객체 멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야 한다.
  • 정적 멤버는 클래스명.정적멤버 형식으로 접근하는 것을 권장한다.