OOP
[OOP] Object 그거 솔직히 거품 아님? 시리즈 (2): 객체지향 프로그래밍
개발자 최찬혁 26세 ・ 2024. 7. 7. 2:00
URL 복사 이웃추가
본문 기타 기능
신고하기
[목차]0. 개요1. 영화 예매 시스템2. 객체지향 프로그래밍을 향해3. 협력하는 객체들의 공동체4. 할인 요금 구하기5. 컴파일 시간 의존성과 실행 시간 의존성은 다르다.6. 상속의 진짜 가치7. 합성8. 한 줄 결론
0. 개요
[OOP] Object 그거 솔직히 거품 아님? 시리즈 (1): 프로그램 패러다임0. 개요 나는 객체지향이 현 시대의 절반 이상의 프로그램에 영향을 끼치는 기조라고 할 지라도 일개 작은 ... blog.naver.com
지난 기록에서는 좋은 객체지향 설계가 무엇인지에 대해 공부했다. 결론은 그냥 각 객체자 자신의 데이터를 주체적으로 다룰 수 있도록 해 정보의 캡슐화를 어느정도 유지하며 유기적으로 소통할 수 있도록 해야 한다는게 결론이었다. 이 과정을 통해 결합도(coupling)을 줄일 수 있고 이는 곧 '변경 용이한 코드 구조'가 될 수 있다는 장점을 얻을 수 있다는게 골조였다.
사실 많은 양의 설명을 몇 줄로 요약한거라 자세히 보고 싶다면 위 링크를 통해 해당 기록을 보고오자.
어쨌든 오늘은 오브젝트의 두번째 챕터인 객체지향 프로그래밍에 대해 학습하려 한다.
1. 영화 예매 시스템
지난 `Chpater1. 객체, 설계` 처럼 이번 장에서도 예시 애플리케이션을 작성하고 이를 통해 설명을 진행하는데 이번에는 '영화 예매 시스템'이 그 주제였다.
[그림1]. 영화 예매 시스템
대충 이렇게 영화를 예매하는 시스템이 오늘의 주제이다. 여기서 저자는 '영화'와 '상영'은 다르게 보자라며 용어를 구분할 것을 제안했다.
a. 영화: 영화에 대한 기본 적보를 다루기 위해 사용하자.
- 제목
- 상영시간
- 가격 정보
b. 상영: 실제로 관객들이 영화를 관람하는 사건을 표현키 위해 사용하자.
- 상영 일자
- 시간
- 순번 등 ..
또 하나 중요한 점은 "특정 조건을 만족하는 예매자는 '할인을 받을 수 있다'는 사실이다. 이 때 할인 대상이 되기 위한 규칙은 아래 두 가지가 있다.
할인액 결정을 위한 두 가지 조건
a. 할인 조건(discount condition): 할인 여부를 결정
- 순서 조건: 상영 순번을 이용해 할인 여부를 결정하는 규칙. (ex. x번째 고객의 경우 할인!)
[그림2]. 순서 조건의 예시
- 기간 조건: 영화 상영 시간을 이용해 할인 여부 결정. (기간 조건은 요일, 시작 시간, 종료 시간으로 구성)
* 예컨대 영화 시작 시간이 해당 기간 안에 포함될 경우 요금을 할인
[그림3]. 기간 할인의 예시
b. 할인 정책(discount policy): 할인 요금을 결정
[그림4]. 금액 할인 정책과 비율 할인 정책
- 금액 할인 정책: 예매 요금에서 일정 금액을 할인. (예컨대 9000원에서 500원 할인)
- 비율 할인 정책: 예매 요금에서 일정 비율을 할인. (예컨대 9000원에서 10% 할인이면 900원 할인)
그래서 할인을 위해선 할인 조건과 할인 정책 조건을 적용해야 하는데 여기에 추가로 붙는 제약 조건은 아래와 같다.
(1) 할인 정책은 지정하지 않을 수 있고 지정할 경우 하나만 적용 가능하다.
- 예컨대 1000원 쿠폰 할인(금액 할인)과 함께 다른 할인 조건에 해당돼 10%할인(비율 할인)을 적용받을 수 없다.
(2) 할인 조건은 다수의 할인 조건등을 지정할 수 있다.
- 예컨대 17:00 ~ 18:00 사이에 예매한 고객(기간 조건) 중에 5번째 고객(순서 조건)
[그림5]. 저자가 제시한 설명을 위한 영화와 할인정책 예시
예컨대 가격이 만원인 아바타를 예매한다고 가정할 때 여기엔 800원의 할인 정책이 적용되어 있다. 그래서 저 할인 조건들 중 하나를 만족시키는 경우 할인이 적용되야 하고 이런 할인 정책은 1인 기준이어서 두명의 경우 1600원의 할인이 들어간다. 이런 식으로 예매를 마치게 되는 경우 최종적으로 요금을 할인받은 예매 정보가 생성된다. (예매 표라고 표현하면 되지 뭘 .. 예매 정보라고 또 ..)
[그림6]. 결제 후 생성된 예매 정보
... 여기까지 영화 예매 시스템을 구축하기 위해 필요한 기본적인 설명들을 진행했다.
대충 영화 예매 시스템 구축을 위해 필요한 플로우, 그러니까 고객이 영화 예매를 신청하면 그 시점의 할인조건을 보고 (기간, 순서) 할인 대상자인 경우 각 조건에 맞는 할인 정책(고정 금액, 비율 금액)을 적용해 최종 결제 금액을 도출한 뒤 결제 이후 예매 정보를 생성해주면 된다.
2. 객체지향 프로그래밍을 향해
당장 코드를 보며 개선하면 좋겠지만 저자는 먼저 독자가 객체지향 프로그래밍에 대해 이해하기를 바라는 눈치다. 그래서 객체지향 프로그래밍 구현을 위해 협력, 객체, 클래스에 대해 설명하기 시작한다.
[그림7]. 객체지향에 대해 아는척 하는 개발자 장모씨 (이름 기억 안남)
저자는 뜬금없이 '객체지향은 객체를 지향하는 것이다.'라는 이 주장에 동의하냐고 선문답을 던지더니 대뜸 너는 평소에 어떤 식으로 코딩하냐고 물어본다. 그러면서 너가 객체지향이 익숙하다면 '클래스가 필요한 지 고민할 것이다'라며 되도 않는 선입견을 남발하는데
그러고나선 마치 내가 실제로 그렇게 대답이라도 한냥 '안타깝게도 이것은 객체지향의 본질과는 거리가 멀다' 면서 혼자 북치고 장구친다.
.. 어쨌든 저자가 하고 싶은 말은 '객체지향은 클래스가 아닌 객체에 초점을 맞출때에만 얻을 수 있다' 라고 하는 거 보니 처음에 말한 '객체지향은 객체를 지향하는 것'이라는 주장이 맞는 말이고 너네는 분명히 '클래스를 보는데 주의를 기울였을 테니 초보다'라는 말이 하고싶었던 거 같다. 나는 '그래 너 잘났다'라는 말이 하고싶다.
그럼 어떻게 객체 중심으로 생각하는데?
어떤 클래스가 필요한지 고민하기 전에 어떤 객체가 필요한지 고민해라
객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐라.
객체가 어떤 상태와 행동을 가지는지 고민 하라는 둥 .. 협력하는 공동체의 일원으로 보라는 둥 .. 맨날 듣는 이상한 추상적인 설명만 주구장창 늘어놓는다. 그 뒤에 '도메인의 구조를 따르는 프로그램 구조'라는 예시를 드는데 일단 아래 설명을 보자.
도메인의 구조를 따르는 프로그램 구조
설명을 시작하며 저자는 '도메인(domain)'이라는 용어를 살펴볼 것을 권장했다. 소프트웨어는 어떤 문제를 해결하기 위해 만들어지는데 영화 예매 소프트웨어의 경우 '사용자가 직접 예매를 수행하는데 드는 불편함을 해소하기 위해' 만들어졌다. 여기서의 '영화 예매' 처럼 사용자가 프로그램을 사용하는 분야를 '도메인'이라고 부른다.
이런 도메인을 구성하는 개념들을 '객체와 클래스' 같은 것들로 표현하면 좀 더 이해하기 쉽게 만들 수 있다는데 사실 난 잘 모르겠다. 일단 저자가 제공해준 도메인들의 구조를 그려보자.
[그림8]. 영화 예매 불편 문제를 해소하기 위한 도메인의 클래스 구조
일단 저자는 위 클래스 구조를 간단하게 구현했고 일단 위 다이어그램만 가지고 알 수 있는 부분은 아래와 같다.
a. 영화는 여러번 상영될 수 있다.
b. 상영은 여러번 예매될 수 있다.
c. 영화에는 할인 정책을 할당하지 않거나 할당하더라도 오직 하나만 할당할 수 있다.
d. 할인 정책이 존재하는 경우에는 하나 이상의 할인 조건이 반드시 존재한다는 것을 알 수 있다.
e. 할인 정책의 종류로는 금액 할인 정책과 비율 할인 정책이 있다.
f. 할인 정책의 종류로는 순번 조건과 기간 조건이 있다는 걸 알 수 있다.
.. 오 대박 생각보다 괜찮다.
이제 클래스 이름으로 변경하면 아래와 같다.
[그림9]. 도메인 개념의 구조를 따르는 클래스 구조
이제 구조를 만들었으니 실제로 코드를 짜보자.
.. 먼저 상영(Screening) 클래스부터
(1) Screening이 가져야 하는 변수
movie (영화)
sequence (순번)
whenScreened (상영 시작 시간)
(2) Screening이 가져야 하는 메서드
getStartTime (상영 시작 시간 반환)
isSequence (순번 일치 여부 검사)
getMovieFee (기본 요금 반환)
상태와 행위를 정의했으니 코드로 짜보자.
public class Screening { private Movie movie; private int sequence; private LocalDateTime whenScreened; public Screening(Movie movie, int sequence, LocalDateTime whenScreened) { this.movie = movie; this.sequence = sequence; this.whenScreened = whenScreened; } public LocalDateTime getStartTime() { return whenScreened; } public boolean isSequence(int sequence) { return this.sequence == sequence; } public Money getMovieFee() { return movie.getFee(); }}
평범한 상영 클래스다. 근데 여기서 저자는 두 가지 부분에 주목할 것을 제안했다.
a. 인스턴스 변수의 가시성은 private
b. 메서드의 가시성은 public이다.
이렇게 클래스는 내부와 외부로 구분되는데 외부로 노출시킬 부분과 감출 부분을 결정하는 걸 잘 선택할 수 있는게 훌륭한 클래스 설계라는데 일단 그 기준은 말 안해줬다. .. 어찌됐건 저자는 클래스의 내부와 외부를 구분해야 하는 이유에 대한 근본적인 질문을 하는데 그 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이고 프로그래머에게 구현의 자유를 제공하기 때문이란다... 하 .. 또 추상적이네
자율적인 객체
먼저 저자는 객체란 '상태'와 '행동'을 함께 가지는 복합적인 존재라는 내용을 설명했다. 그리고 그놈에 '자율적인 존재'라는 추상적인 표현을 덧붙였는데 이 두 개념이 서로 깊이 연관되어 있다고 주장한다.
옛날에 C같은 절차지향 언어에선 사실 구조체라는 데이터와 실제 기능을 수행할 함수를 분리하는 구조였는데 이렇게 '상태'와 '행동'을 구분짓지 않고 한대 묶는 아이디어가 바로 객체다. 그래서 데이터(상태)와 기능(행동)을 객체 내부로 함께 묶는 것을 '캡슐화'라고 부르는데 여기서 한 걸음 더 나아가 '접근 제어'라는 매커니즘도 함께 제공한다. 예컨대 public, protected, private 같은 접근 수정자 들이 그 내용이다. 이런 접근 제어가 필요한 이유는 객체가 자율적인 존재로서 상호작용 하기 위해 외부로 노출되면 안되는 정보를 강제하기 위함이다.
(지금은 나도 추상적으로 설명했는데 일단 이해 안가면 넘어가시길 ..)
이런 캡슐화와 접근 제어 과정에선 객체를 두 부분으로 바라볼 수 있다.
public interface: 외부에서 접근 가능한 부분
implemenatation: 외부에서 접근 불가능하고 오직 내부에서만 접근 가능한 부분
[그림10]. 흔히 말하는 인터페이스 분리 예시
특정 인터페이스가 서로 다른 메서드들에 자신이 가지고 있는 메서드를 전부 제공하는 경우엔 각 사용자에게 필요한 인터페이스만 분리해서 각 사용자가 자기가 쓸 수 있는 범위와 인터페이스 제공자가 제공해주는 해당 인터페이스의 기능을 명확히 알 수 있어야한다. 어찌됐건 이런 걸 위해서 각 필드나 메소드에 대한 접근 제어가 필요한거다.
그러니까. 이렇게 외부로 보여져야 하는 인터페이스와 내부에서만 접근 가능한 구현을 분리하는 "인터페이스와 구현의 분리(separation of interface and implementation) 원칙은 훌륭한 객체지향 프로그램을 만들기 위해 따라야 하는 핵심 원칙이다." 라고 저자는 주장한다. 그러니까 기본은 private으로 하되, 외부에 제공해야 하는것만 public으로 선언하라는 뜻으로 나는 이해했다.
프로그래머의 자유
저자는 프로그래머의 역할을 크게 2가지로 구분하는 것을 권고했다.
클래스 작성자 (class creator)
클라이언트 프로그래머 (client programmer)
결국 클래스 작성자라는 건 위에서 설명한 인터페이스를 제공하고 '구현'이라고 표현하는 내부적 접근만 허용하는 은닉된 무언가를 관리하는 사람을 말하고 클라이언트 프로그래머는 클래스 작성자가 제공하는 `public interface`를 이용해 애플리케이션을 만드는 프로그래머다.
정보 은닉. 그러니까 클래스 작성자로서 '필요한 기능만 보여주고, 그렇지 않은 것들은 숨기는 방법을 사용하면 예상치 못한 내부 코드의 변경을 줄일 수 있어 변경 용이성을 유지할 수 있다.' 그래서 객체지향의 클래스를 다룰 땐 접근 제어를 신경쓰는게 좋다라는게 이번 절의 결론이다.
3. 협력하는 객체들의 공동체
계속 저 놈에 공동체 소리가 나와서 안그래도 궁금했는데 잘됐다. 저자는 영화 예매 기능 구현을 통해 설명을 진행한단다.
public class Screening { public Reservation reserve(Customer customer, int audienceCount) { return new Reservation(customer, this, calculateFee(audienceCount), audienceCount); } .. private Money calculateFee(int audienceCount) { return movie.calculateMovieFee(this).times(audienceCount); } }
- Screning 클래스의 reserve 메서드는 영화 예매 기능을 제공한다.
- customer에는 고객의 정보, audienceCount는 인원수가 담긴다.
- 예매 후에는 예매 정보를 담고있는 Reservation 클래스를 반환한다.
이 때 예매(reserve())가 완료되면 내려오는 예매 정보(new Reservation)를 작성하는데 필요한 계산된 요금을 calculateFee(audienceCount)를 통해서 계산하는데 코드를 보면 반환되는 Money라는 클래스의 time()이라는 메서드를 사용한다.
public class Money { public static final Money ZERO = Money.wons(0); private final BigDecimal amount; public static Money wons(long amount) { return new Money(BigDecimal.valueOf(amount)); } public static Money wons(double amount) { return new Money(BigDecimal.valueOf(amount)); } Money(BigDecimal amount) { this.amount = amount; } public Money plus(Money amount) { return new Money(this.amount.add(amount.amount); } public Money minus(Money amount) { return new Money(this.amount.subtract(amount.amount)); } public Money times(double percent) { return new Money(this.amount.multiply(BigDecimal.valueOf(percent))); } public boolean isLessThan(Money other) { return amount.compareTo(other.amount) < 0; } public boolean isGreaterThanOrEqual(Money other) { return amount.compareTo(other.amount) >= 0; }}
생각해보면 money 라는 타입은 그냥 Long으로 표현될 수도 있다. 하지만 이 경우 타입만으로 이게 돈과 관련된 개념인지 확인할 수 없을 뿐더러 여러 곳에서의 값 변경을 통제하기도 어렵다. 그래서 이와 같이 의미를 좀 더 명시적이고 분명하고 표현하고 싶다면 객체를 사용해서 해당 개념을 구현하는 것을 권고했다. 비록 돈이라는 개념이 하나의 인스턴스 변수만 포함하는, 그러니까 amount만 포함하더라도 저자는 설계의 명확성과 유연성을 높이는 관점에서 필요할 수 있음을 주장했다.
public class Reservation { private Customer customer; private Screening screening; private Money fee; private int audienceCount; public Reservation(Customer customer, Screening screening, Money fee, int audienceCount) { this.customer = customer; this.Screening = screening; this.fee = fee; this.audienceCount = audienceCount; }}
이 예매 표는 고객, 상영 정보, 예매 요금, 인원 수를 속성으로 포함하는데 기존에 Screening의 reserve()에서 봤듯이 Screening, Movie, Reservation 인스턴스들은 서로의 메서드를 호출하면서 상호 작용하는데 이를 '협력(Collaboration)'이라고 부른다고 한다. 잘 이해가 안간다면 아래 [그림11]. 을 보자.
[그림11]. 객체 사이의 협력
여기서 중요한 얘기가 나오는데 객체의 내부 상태는 외부에서 접근치 못하도록 감춰야 한다. 대신에 외부에 공개하는 퍼블릭 인터페이스를 통해 내부 상태에 접근할 수 있도록 허용하는데, 이렇게 퍼블릭 인터페이스에 대해 다른 객체들은 '요청(request)'를 할 수 있다. 그리고 퍼블릭 인터페이스를 통해 요청을 받은 객체는 이를 처리후 '응답(response)'해야 한다.
그래서 우리가 자주 들어봤던 '메서드 사이의 상호작용은 메시지를 전송 (send a message)하고 수신 (receive a message)하는 것 이다.'라는 말들이 결국 다른 객체에 대한 상태 변경을 외부에서 처리하지 요청만 보내고 알아서 해서 결과만 응답 받으라는 컨셉이다. 이 때에 우리가 흔히 표현하는 메서드라는 맥락이 나오는데, 결국 메서드란 건 '수신된 메시지를 처리하기 위한 방법'이라고 이해하면 된다.
뒤에서 다형성을 이해하려면 지금껏 설명한 '메시지와 메서드의 구분'을 잘 이해해야 한다고 한다.
4. 할인 요금 구하기
저자는 예매 요금을 계산하기 위한 협력을 코드를 통해 살펴봤다.
[Movie의 속성]
제목 (title)
상영시간(runningTime)
기본 요금(fee)
할인 정책(discountPolicy)
class Movie { private String title; private Duration runningTime; private Money fee; private DiscountPolicy discountPolicy; public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) { this.title = title; this.runningTime = runningTime; this.fee = fee; this.discountPolicy = discountPolicy; } public Money getFee() { return fee; } public Money calculateMovieFee(Screening screening) { return fee.minus(discountPolicy.calculateDiscountAmount(screening)); }}
여기서 직관적으로 봤을 때 뭔가 걸리는게 한 가지 있다. 바로 할인률을 계산하는 부분은 있지만 할인 정책을 계산하는 부분은 없다는 점이다. 이런 내용에 대한 구체적인 정보가 없다는게 어색하다면 아직 우리는 '객체지향 패러다임에 익숙하지 않는 것'이라고 봐도 무방하다고 저자는 말한다.
이어서 객체지향에서 중요하게 여겨지는 두 가지 개념에 대해서 설명했는데 하나는 상속이고 다른 하나는 다형성이다. 이 두 개념 보다 추상화라는 원리가 숨겨져 있는데 코드를 통해 내용을 풀어 써보려 한다.
** 할인 정책과 할인 조건
public abstract class DiscountPolicy { private List<DiscountCondition> conditions = new ArrayList<>(); public DiscountPolicy(DiscountCondition ... conditions) { this.conditions = Arrays.asList(conditions); } public Money calculateDiscountAmount(Screening screening) { for (DiscountCondition each : conditions) { if (each.isSatisfiedBy(screening)) { return getDiscountAmount(screening); } } return Money.ZERO; } abstract protected Money getDiscountAmount(Screening Screening);}
결국 calculateDiscountAmount에서 최종 할인 금액을 계산해주는데, 이 때 Screening 정보를 가지고 해당 상영 정보에 맞는 할인이 적용된 금액을 내려준다. 이때 저 DiscountPolicy 라는 클래스를 추상 클래스로 작성하고, getDiscountAmount를 추상 메서드로 작성해 꼭 구현토록 하면 DiscountPolicy를 상속받는 각 정책 클래스에서 각각의 getDiscountAmount를 통해 정책에 맞는 할인 금액을 내려줄 수 있게 된다.
이거 아주 좋은 예시다. 이렇게 부모 클래스에서 기본적인 프로세스를 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 형태의 디자인 패턴을 TEMPLATE METHOD 패턴 이라고 한다.
public interface DiscountCondition { boolean isSatisfiedBy(Screening screening);}
마찬가지로 DiscountCondition도 인터페이스화 되어 있는데 이를 이용해 순번 조건과 기간 조건에 대한 할인 정책을 다형적으로 넣어줄 수 있다. 예컨대 아래 코드를 보자.
private List<DiscountCondition> conditions = new ArrayList<>(); public DiscountPolicy(DiscountCondition ... conditions) { this.conditions = Arrays.asList(conditions); }
여기선 DiscountPolicy를 상속받는 여러 클래스가 다형적으로 들어갈 수 있다는 의미다. 이제 DiscountCondition을 상속받을 각 클래스들을 정의해보자.
// 순번 조건public class SequenceCondition implements DiscountCondition { private int sequence; public SequenceCondition(int sequence) { this.sequence = sequence; } public boolean isSatisfiedBy(Screening screning) { return screening.isSequence(sequence); }}
// 기간 조건public class PeriodCondition implements DiscountCondition { private DayOfWeek dayOfWeek; private LocalTime startTime; private LocalTime endTime; public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { this.dayOfWeek = dayOfWeek; this.startTime = startTime; this.endTime = endTime; } public boolean isSatisfiedBy(Screening screening) { return screening.getStartTime().getDayOfWeek().equlas(dayOfWeek) && startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 && endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0; }}
그 다음으로 할인 정책까지만 작성해보자.
// 금액 할인 조건public class AmountDiscountPolicy extends DiscountPolicy { private Money discountAmount; public AmountDiscountPolicy(Money discountAmount, DiscountCondition ... conditions) { super(conditions); this.discountAmount = discountAmount; } @Override protected Money getDiscountAmount(Screening screening) { return discountAmount; }}
// 비율 할인 조건public class PercentDiscountPolicy extends DiscountPolicy { private double percent; public PercentDiscountPolicy(double percent, DiscountCondition ... conditions) { super(conditions); this.percent = percent; } @Override protected Money getDiscountAmount(Screening screening) { return screening.getMovieFee().times(percent); }}
[그림12].다형성을 충족시키는 구조
결론은 이런 템플릿 메서드 패턴등의 활용을 적용하면 다형성을 충족시키는 구조를 만들 수 있다는 사실이다.
5. 컴파일 시간 의존성과 실행 시간 의존성은 다르다.
클래스의 의존성과 객체의 의존성은 다르다.
[그림13]. 클래스 시간의 의존성
여기서 보면 Movie 클래스는 DiscountPolicy라는 인터페이스를 바라보는 클래스 다이어그램 구조를 가지는데, 중요한 사실은 결국엔 실행 할 경우 저 실제 구현 클래스인 AmountDiscountPolicy, PercentDiscountPolicy 둘 중 하나를 가져야 한다는 점이다. 그래서 저자는 실행 시점에 Movie객체는 실행전 클래스 레벨의 Moive 클래스와 다른 의존관계를 갖는 다는 문제를 지적했다.
컴파일 시점과 런타임 시점의 의존 관계가 다른 상황은 문제를 복잡하게 만들 수 있음을 알려준다. 왜냐면 런타임 환경에 실제 어떤 클래스에 의존하고 있는지를 찾아야 하기 때문이다. 이렇게 의존성과 실행 시점의 의존성을 다르게 하는 구조는 한 가지 트레이드 오프를 갖는데 '코드 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해지지만 보기 어려워질 수 있다'는 사실이다.
TradeOff !!
if. 클래스 의존성과 런타임 의존성이 다를 수록 ..
- 장점: 교체하기 쉬워지고 확장 하기 용이해진다.
- 단점: 디버깅하기 어려워지고, 코드는 이해하기 어려워진다.
else if. 클래스 의존성과 런타임 의존성을 최대한 동일토록 할 경우
- 장점: 디버깅이 쉬워지고 코드가 이해하기 쉬워진다.
- 단점: 코드를 교체, 확장하기 어려워진다. (?)
솔직히 클래스를 직접 사용한다고 코드를 교체하거나 확장하는데 얼마나 어려움이 생기는지는 구체적인 예시가 제공되지 않아 동의하긴 어렵지만 대충 '그럴 수도 있겠네 ~' 정도의 생각은 들긴 한다. 뭐 어쨌든 저자가 말하고자 하는 건 '항상 유연성과 가독성 사이에서 고민'해보라는 말이다.
6. 상속의 진짜 가치
많은 사람들이 상속의 목적을 메서드나 인스턴스 변수를 재사용하는 것이라고 설명하는데 진짜 가치는 다른데 있다. 라고 저자는 주장한다. 저자는 '상속의 가치는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문' 이라고 설명한다.
인터페이스란 건 객체가 이해할 수 있는 메시지의 목록을 정의 한 것인데 상속을 이용해 이런 인터페이스를 포함 할 수 있게 된다. 저자는 대충 설명 안하고 넘어갔는데, 여기서 말한 인터페이스는 자바의 Interface class가 아니라 public interface 개념으로 캡슐화 과정에서 외부에 공개할 인터페이스를 의미한다. (내가 이래서 이 작가를 싫어한다.)
예컨대 아래 코드를 보자.
public class Movie { public Money calculateMovieFee(Screening screening) { return fee.minus(discountPolicy.calculateDiscountAmount(screening)); }}
Movie는 어떤 구체적인 정책에 어떤 calculateDiscountAmount가 있는지 생각 할 필요 없이 그냥 저 인터페이스의 public Interface만 알고 있으면 된다는 장점이다. 이게 업캐스팅 이라는 용어와 함께 각 자식 클래스가 저 부모 클래스를 대신할 수 있다는 특징(다형성)과 맞물려 Movie 내부의 calculateDiscountAmount코드를 변경해주지 않더라도 런타임 시점에 자유롭게 변경할 수 있는 코드를 짤 수 있게된다.
.. 물론 저자가 말한 것 처럼 이렇게 하면 코드 가독성, 디버깅 편의성은 떨어진다.
결국 다형성이라는 건 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 돌아간다. 이를 통해 동일한 메시지를 수신하더라도 객체 타입에 따라 다른 결과를 낼 수 있다는 얘기인데 이를 위해 '동일한 interface class'와 '동일한 public interface'가 필요하다.
그리고 다형성을 구현하는 방법은 다양하지만 공통점은 하나 있다. 바로 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 점이다. 이를 '지연 바인딩' 또는 '동적 바인딩' 이라고 하는데 컴파일 시점에 실행될 함수나 프로시저를 결정하는 전통적인 방법인 '초기 바인딩'과 '정적 바인딩'이라고 불리던 그것과 다른 부분이다.
7. 합성
상속은 코드를 재사용하기 위해 널리 사용되는 방법이나 가장 좋은 방법은 아니다.
.. 라고 저자는 주장한다. 맨날 트레이드 오프니 뭐니 하면서 여기서는 또 일반화가 잔뜩 담긴 표현을 사용한다.
어찌됐건 저자는 객체지향 설계 관점에서 코드 재사용을 하는 또 다른 방법인 '합성(Composition)'을 설명하려 하는 거 같은데 기존까지 진행했던 것들이 바로 '합성'의 예시다. 아래 그림을 보자.
[그림14]. 상속과 합성의 차이
상속이 뭔 소린가 계속 이해가 안됐었는데 그 이유를 드디어 찾았다. 자꾸 DiscountPolicy의 구현체들을 상속한데서 인터페이스는 어디있는거지 계속 이해가 안됐는데 그냥 상속 예시 들려고 억지로 구조를 맞춰서 위 [그림14]. 처럼 작성해뒀던거였다. (내가 이 작가 맘에 안드는 42번째 이유) 뭐 어쨌건 이 얘기는 여기서 마무리하고 저자는 위 같은 상속 구조의 2가지 문제점을 지적한다.
상속에는 문제가 있어!
[그림15]. 화가나 지적하는 상속왕 개발자 정은
a. 캡슐화를 위반한다.
- 상속을 잘 이용하기 위해선 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
b. 설계를 유연하지 못하게 만든다.
- 이런 캡슐화의 약화는 자식 클래스와 부모 클래스의 결합도를 높이기에 '같이 변경되야 하는' 상황을 만들 가능성을 높인다.
c. 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 작성한다. (extends를 통해)
- 클래스 상속 코드를 작성해야 하니까 정적 바인딩 (코드 작성 시점에 의존관계 정해짐)
그래서 상속을 과다하게 사용한 코드는 변경하기 어려워지기에 상속은 자제하는게 좋다.
.. 어쨌든 뭐 이상한 상속 구조 쓰지 말고 DI 받는게 '코드를 재사용 하면서도, 유연한 구조를 가져갈 수 있는 방법' 이라는게 이 절의 결론인 거 같다.
8. 한 줄 결론
캡슐화 잘하고 DI 써라.
오늘 하루도 공부할 수 있어 크게 감사합니다. 2024-07-06
개발자 최찬혁
댓글쓰기 이 글에 댓글 단 블로거 열고 닫기
인쇄