객체지향 프로그래밍을 향해
객체지향의 본질은 클래스가 아니라 객체를 지향하는 것이다. 그러려면 우리는 두 가지에 집중해야한다.
어떤 객체가 필요할 지 고민하라. 클래스는 추상화된 것이다. 추상화하기 전에 객체들이 어떤 상태와 행동을 가지는지 생각해보자.
객체를 협력적인 존재로 바라보자. 객체의 형태가 잡히면 공통된 특성을 가진 타입으로 바라보고 클래스를 구현해보자.
도메인은 무엇일까.
도메인은 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 말한다.
객체지향이 강력한 이유는 요구사항을 분석하는 초기부터 프로그램을 구현하는 마지막까지 객체라는 추상화 기법을 적용할 수 있기 때문이다. 도메인을 구성하는 개념들이 객체와 클래스로 매끄럽게 연결되도록 돕는다.
앞서 살펴본 영화와 상영의 개념을 되새겨보면, 하나의 영화는 여러번 상영될 수 있다는 것을 안다.
또 영화는 할인 정책이 있거나 없으며, 있으면 하나만 할당할 수 있다. 할인 정책이 있다면 하나 이상의 조건들이 존재함을 알 수 있다.
클래스의 이름은 대응되는 도메인 개념의 이름과 동일하게 짓거나 유사하게 짓는 것이 좋다.
클래스 간 관계도 최대한 도메인의 개념들 관계와 비슷하게 만들어야 프로그램의 구조를 예상하기 쉬워진다.
영화는 Movie 라는 클래스로 표현할 수 있다. 상영은 Screening 으로 표현한다.
할인 정책은 DiscountPolicy, 금액 할인 정책은 AmountDiscountPolicy, 비율 할인 정책은 PercentDiscountPolicy 로 표현한다.
할인 조건은 DiscountCondition, 순번 조건은 SequenceCondition, 기간 조건은 PeriodCondition 으로 표현한다.
예매는 Reservation 이라고 표현할 수 있다.
여러 Reservation 은 하나의 Screening 에 매핑된다. 하나의 Movie 는 다수의 Screening 과 관계를 갖는다.
하나의 Movie 는 DiscountPolicy 가 최대 하나가 있다. DiscountPolicy 는 하나 이상의 DiscountCondition 이 된다.
이렇게 도매인의 개념을 클래스로 표현하고, 구조를 생각해봤다. 이제 남은 일은 프로그래밍 언어로 이 구조를 구현하는 일이다.
인스턴스 변수의 가시성은 private 이고, 메서드의 가시성은 public 이다. 이 부분이 중요하다.
클래스를 구현하거나 라이브러리의 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다.
클래스를 내부와 외부로 구분하고, 어떤 부분은 내부로 감출 지 어떤 부분은 외부로 공개할 지 결정하는 일이다.
Screening 은 외부에서 직접 객체에 접근할 수 없도록 막고, 적절한 public 메서드로 내부 상태를 변경할 수 있도록 했다.
내부와 외부를 결정하는 명확성이 객체의 자율성을 보장한다. 다른 이유로 개발자에게 구현의 자유를 준다.
객체는 상태와 행동을 함께 가지는 복합적인 존재다. 또 객체는 스스로 판단하고 행동하는 자율적 존재다. 객체지향의 객체는 데이터와 기능을 한 덩어리로 가진다. 캡슐화되어 있다.
객체는 캡슐화하는 것에 더 나아가 접근 제어라는 메커니즘을 함께 제공한다. 그리고 다양한 언어들은 접근 제어를 위해 public, protected, private 과 같은 접근 수정자를 제공한다.
접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위함이다. 스스로 상태를 관리하고, 판단하고 행동하는 객체들을 만들어 공동체를 구성하는 것이 객체지향의 핵심이다. 외부의 간섭을 최소화해야한다. 어떤 상태에 있고, 어떤 생각을 하는지 알아서는 안된다. 결정을 방해해서도 안된다. 내가 원하는 것을 요청하면 스스로 결정한다는 믿음을 가져야한다.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. 외부에서 접근 가능한 Public Interface, 다른 하나는 오직 내부에서만 접근 가능한 Implementation 이다. 인터페이스와 구현의 분리라는 개념은 객체지향 프로그램을 만들기 위한 핵심 원칙이 된다. 일반적으로 객체의 상태는 숨기고 행동만 공개해야한다. public 은 인터페이스고, private, protected 는 구현이 된다.
프로그래머를 두 역할로 바라보면 유용하다.
(1) 클래스를 작성하는 사람 (2) 그 클래스를 사용하는 사람으로 바라보자.
클래스를 사용하는 사람은 필요한 클래스를 엮어서 어플리케이션을 빠르고 안정적으로 구현하는 것이 목표다.
클래스를 제공하는 사람은 필요한 부분만을 공개하고, 나머지는 숨겨야한다. 숨겨놓은 부분에 접근하지 못하도록 하면서 내부 구현을 마음대로 변경할 수 있다.
구현을 은닉하는 개념은 공급자와 소비자 모두에게 유용한 개념이다.
소비자는 공급자가 공개해놓은 인터페이스에 의존하므로 필요한 만큼만 알면 된다. 또 공급자는 인터페이스를 바꾸지 않으면 코드를 자유롭게 수정할 수 있다.
설계는 다시 말하자면, 변경을 관리하기 위함이다. 그 중 접근 제어는 혼란의 최소화한다.
다시 돌아와서 영화를 예매하는 기능을 살펴보자.
reserve 메서드는 calculateFee 라는 내부 구현을 사용한다.
calculateFee 메서드는 영화 인스턴스에 상영 정보를 전달하고, 관람객 수를 곱해 총 예매 요금을 구한다.
Long 타입은 변수의 크기나 연산 종류와 같이 구현의 제약을 표현할 수 있지만, Money 타입처럼 값이 금액과 관련이 있다는 의미를 전달하기에 충분하지않다. 또 금액과 관련된 로직이 분산되어 중복되어도 막을 수 없다.
의미를 풍부하게 표현할 수 있는 것은 객체지향의 특성을 잘 살리는 수단이 된다. 하나의 변수라고 할지라도 개념을 명시화하는 것은 설계에 도움이 된다.
예매 정보 클래스에는 고객, 상영 정보, 예매 요금, 예매 인원 수를 속성으로 포함한다.
그리고 예매 시스템에서 Screening, Movie, Reservation 클래스는 상호 협력적인 존재가 된다.
Screening.reserve(customer, audienceCount) 로 예매를 시작한다.
Screening.calculateFee(count) 를 통해 상영하는 영화에 대한 총 예매 가격을 계산하는데
Movie.calculateMovieFee(screening) 를 통해 상영하는 영화의 가격을 구한다.
협력의 관점에서 어떤 객체가 필요할지 생각해보고, 객체의 공통적인 상태와 행위를 클래스로 표현하면 도움이 된다.
객체는 다른 객체로 공개된 행동을 요청하면, 요청 받은 객체는 자유롭게 내부적으로 요청을 처리하고 응답한다.
객체는 인터페이스로 정의된 메세지를 요청할 뿐이다. 메세지를 수신한 객체는 메서드라는 것으로 메시지를 처리하게 된다. 메시지와 메서드의 구분에서 다형성이 출발한다.
Screening 이 Movie.calculateFee() 메서드를 호출한다고 생각하지만, 사실 Screening 이 Movie 에 calculateFee 메세지를 전송한다고 하는 것이 적절한 표현이다.
이게 무슨 말이냐 하면, Screening 은 Movie 에 calculateFee 메서드가 있는지 모른다. 다만, calculateFee 라는 메세지를 던져서 결과를 받을 수 있다고 믿는 것이다. Movie 는 메세지를 받으면 스스로 적절한 메서드를 택한다. 자바와 같은 정적 타입 언어에서는 해당되지 않지만, 동적 타입 언어에서는 다른 시그니처를 가진 메서드로도 응답이 가능하다. 결국 메세지를 처리하는 몫은 온전히 Movie 가 짊어진다.
Last updated