Skip to main content

[오브젝트] Chapter02. 객체지향 프로그래밍.

· 17 min read

요구사항 확인

모든 설계가 그러하듯 요구사항을 확인하는 것이 우선이다. 요구사항 분석을 통해 필요한 객체를 정할 수 있고, 각 객체의 책임을 어떻게 분배할지 방향을 잡을 수 있다.

협력, 객체, 클래스

보통 프로그래밍을 어떻게 하는지 생각해보자. 객체지향으로 프로그래밍을 한다고 했을때 가장 먼저 하는것이 무엇인가? 대부분 어떤 class 가 필요할 지 고민한다.
클래스를 먼저 고민하고, 각 클래스는 어떤 속성과 메서드를 가지고 있어야 할 지를 생각한다.

객체지향 프로그래밍을 위해서는 우선 클래스가 아닌 객체 에 초점을 맞춰야 한다.

  1. 어떤 클래스가 필요한 지가 아닌, 어떤 객체가 필요한지를 생각한다.
    클래스는 공통적인 상태와 행동을 공유하는 객체를 추상화 한것이다. 추상화를 하기 위한 개념을 먼저 정리해야한다.
  2. 객체를 독립적인 존재가 아니라 기능을 구현하기위해 협력하는 공동체의 구성원으로 봐야한다.
    객체는 혼자서 할 수 있는 일은 거의 없다. 다른 객체와 상호작용하며 서로 협력하는 존재이며 살아 움직이는 생명체로서 봐야한다.

도메인의 구조를 따르는 프로그램 구조

도메인?

도메인이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야라 할수 있다. 영화 예매 시스템을 예로 들면, 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결 하는 것이다.
여기서 도메인은 영화 예매라 할 수 있다. 도메인은 이처럼 문제를 다루는 범위로 볼 수 있으며 더 작은 단위로도 생각해 볼 수 있다.

자바와 같은 객체지향 언어를 사용하면 이런 도메인이 class로 이어진다는 것을 쉽게 볼 수 있다. 클래스의 이름은 도메인을 쉽게 유추할 수 있는 이름으로 해야 하고 클래스 간의 관계 또한 도메인의 관계가 반영되어야 한다.

클래스 구현

도메인의 정의 다음으로 해야할 일은 도메인을 반영하는 클래스를 구현하는 것이다. 이때, 클래스 외부와 내부의 경계를 구분지어 명확한 경계를 세워야한다. 클래스를 구현하거나 다른 개발자에 의해 개발된 클래슬ㄹ 사용할 때 가장 중요한 것은 클래스의 경계를 구분짓는 것이다.
이 경계가 명확할 수록 객체의 자율성이 보장된다 (chapter 01 참고). 또한 프로그래머에게 구현의 자유를 제공하기 때문에 경계를 명확히 해야 한다.

자율적인 객체

객체에 대해 명심해야할 두 가지 사실이 있다.

  1. 객체는 상태 (state)와 행동 (behavior)ㅇㅇ 함께 가지는 복합적인 존재이다.
  2. 객체는 스스로 판단하고 행동하는 자율적인 존재이다.

객체 지향 개념은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로서 문제 영역의 아이디어를 적절하게 표현할 수 있게한다 (캡슐화). 객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서이다. 객체 외부의 그 무엇도 객체 내부의 상태에 직접 간섭해서는 안되며 객체가 어떤 생각을 하는지도 알아선 안된다.

객체의 캡슐화와 접근제어는 2가지로 나뉜다.

  1. public interface (퍼블릭 인터페이스) 외부에서 접근 가능한 부분이며 메시지 교환을 위해 외부에 노출하는 부분이다.
  2. implementation (구현) 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분이다.

이는 인터페이스와 구현의 분리 원칙으로 훌륭한 객체지향 프로그램의 원칙 중 하나이다. 일반적으로 객체의 상탠ㄴ 숨기고 행동만 외부에 공개해야 한다. 속성은 private로 감추고 메서드는 public으로 공개하는 식이다.

프로그래머의 자유

프로그래머의 역할을 클래스 작성자와 클라이언트 프로그래머로 구분하는 것이 유용하다 [Eckel06].

클래스 작성자는 새로운 데이터 타입을 추가하고, 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
클래스 작성자는 클라이언트 프로그래머가 접근하면 안되는 곳을 접근 제한을 통해 구현 은닉(implementation hiding)을 통해 제한한다. 이를 통해 클래스 작성자는 클라이언트 프로그래머에게 미칠 영향으로부터 자유롭게 내부 구현을 변경할 수 있다.
이 구현 은닉은 양쪽에게 이로운 개념이다. 클라이언트 프그래머는 내부 구현은 무시한 채 공개 인터페이스만 알고 있으면 된다. 클래스 작성자는 외부에 미칠 영향을 걱정하지 않고 변경을 할 수 있다.

협력하는 객체들의 공동체

앞서 말했듯 객체들은 서로 협력하며 존재한다. 객체는 다른 객체의 인터페잇에 공개된 행동을 수행하도록 요청할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.
객체가 받은 요청을 처리하기 위한 방법을 메서드라고 한다.

추상화 - 상속과 다형성

컴파일 시간 의존성과 실행시간 의존성

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 유연하고 쉽게 재사용 할 수 있으며 확장 가능한 설계가 된다.
그러나 이렇게 유연해 질 수록 복잡도는 높아진다. 실행시점과 컴파일 시점의 의존성이 달라지면 코드를 이해하기 어려워진다. (Trade off :: 유연성, 재사용성 vs 유지보수)

차이에 의한 프로그래밍

상속은 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 부모의 코드를 그대로 자식에게 물려줄 수 있으며 자식은 부모와 다른 부분만을 구현하면 쉽게 확장할 수 있다. 이런 방식을 차이에 의한 프로그래밍이라 한다.

상속이 가치있는 이유는 부모의 변수를 재활용하기 때문이라기 보다는 부모가 가진 모든 인터페이스를 자식 클래스가 물려 받을 수 있기 때문이다. 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의하는 것임을 생각하면 알수 있다.
실제로 객체간의 관계에서 중요한 것은 어떤 인스턴스인지가 아니라 어떤 메시지를 수신할 수 있느냐가 중요한 것이다.

런타임에서 자식은 부모의 모든 메시지를 수신 할 수 있기에 부모 클래스와 같은 타입으로 취급하기도 한다. 이를 업캐스팅이라 하는데 자식 클래스가 부모의 타입으로 자동 형변환이 되는 것을 말한다.

다형성

메서드와 메시지는 다른 개념임을 상기하자. 객체가 메시지를 수신하면 어떤 메서드를 실행할 지는 그 객체와 연결된 클래스가 무엇인가에 따라 달라진다.
다시 말해 동일한 메시지에 대해 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라 부른다.

학부 과정을 이수했다면 들어봤을 동적바인딩, 정적바인딩이 여기서 사용된다. 다형성을 구현하는 방식은 다양하지만 메시지와 메서드를 실행 시점에 연결한다는 것은 동일하다.
이때 컴파일 타임에 연결하게되면 정적 바인딩, 실행 시점에 연결하는 것을 동적 바인딩이라 한다.

인터페이스와 다형성

자바 프로그래머에게 친숙한 인터페이스는 이러한 다형성을 위해 제공된 프로그래밍 요소이다. 클래스의 구현은 필요없고 순수하게 인터페이스 만을 공유하고 싶을때 사용한다. 기본적으로는 Abstract Base Class로 인터페이스를 정의할 수 있다.

추상화와 유연성

추상화의 힘

추상화는 크게 2가지의 장점을 갖고있다.

  1. 추상화 계층만 보면 요구 사항에 대해 높은 수준에서 서술할 수 있다.
    세부적인 정책을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 이는 다른 요소를 배제하고 상위 개념만으로 도메인의 중요 개념을 설명할 수 있게 해준다.
  2. 추상화를 이용하면 설계가 유연해진다.
    상위 정책을 추상화를 통해 표현하면 구조를 수정핮지 않고 새로운 기능을 쉽게 추가하고 확장할 수 있다.

유연한 설계

추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다. 세부 정책을 구현한 클래스가 추상화된 상위 정책을 상속받고 있다면 어떤 클래스와도 협력 가능하다.

추상 클래스와 인터페이스 트레이드 오프

구현과 관련된 모든 것들은 트레이드 오프의 대상이 될 수 있다. 코드의 작성에 있어 이유가 없는 코드는 없어야 한다. 사소한 결정이라도 심사숙고한 끝에 나온 코드와 그렇지 않은 코드는 품질에서 큰 차이가 있다.

코드 재사용

상속은 일반적으로 코드를 재사용하는데 널리 쓰이는 방법이다. 그러나 널리 사용된다고 해서 베스트는 아니다. 합성(composition)도 들어 보았을 것이다. 사용하고자 하는 코드를 담고있는 인스턴스를 변수로 포함해 사용하는 방식이다.
많은 컨텍스트나 프레임워크들이 합성을 선호하는 모습을 보인다., Spring framework애서도 대부분의 의존성을 합성 관계로 사용하는 것을 볼 수 있다.

상속

왜 상속 대신 합성을 선호할까? 상속의 문제점은 캡슐화를 위반한다는 것과 설계를 유현하지 않게 한다는 것이다.
가장 큰 문제는 캡슐화를 위반한다는 것이다. 상속을 하기 위한 기본 전제는 부모 클래스를 잘 알고있어야 한다는 것이다. 부모 내부의 다른 메서드가 내부의 어떤 추상 메서드를 호출하고 있다는 것을 알고 이를 구현해야 한다.
결과적으로 자식에게 부모의 구현이 고스란히 노출되기 때문에 캡슐화가 약화된다. 부모가 변경된 경우 자식도 변경될 확률을 높인다.

설계가 유연하지 않게 되는 것은 부모와 자식의 관계가 강하게 결합되기 때문이다. 부모와 자식의 관계는 컴파일 시점에 결정되기 때문에 실행 시점에 객체의 종류를 변경할 수 없게된다.

합성

앞서 말했듯 합성은 인스턴스를 내부에 포함하는 방법으로, 객체의 인스턴스를 통해서만 코드를 재사용할 수 있다. 합성은 상속이 가지는 두가지 문제점을 모두 해결한다.
인터페이스에 정의된 메시지만을 사용하기 때문에 구현을 효과적으로 캡슐화할 수 있다. 또한 의존하는 클래스를 교체하기 쉽기 때문에 설계를 유연하게 만든다. 따라서 코드 재사용을 위해서는 상속 보다는 합성을 선호하는 것이 더 좋은 방법이다.

그러나 합성만을 사용하는 것은 좋은 방법이 아니다. 대부분의 설계는 상속과 합성을 함께 사용해야 한다. 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.