💋 인트로
우아한테크코스 미션 중 블랙잭 미션을 진행할 때, 굉장히 핫한 주제가 있었다. 바로 상속과 조합이었다.
조합에 대해 처음 들어보았고, 찾아보려 검색했지만 이펙티브 자바에서 다루는 아이템 18 상속보다는 컴포지션을 사용하라에 대한 내용 외에 컴포지션에 대해 설명하는 내용을 찾아볼 수가 없었다. 그래서 직접 써보기로 했다!
👍 조합? 컴포지션?
조합 == 컴포지션
같은 말이다.
상속은 문제가 많다. 잘 사용하면 되는 일이지만, 상속을 하면서 내부 구현을 외부에 드러내지 않고 사용하기는 굉장히 어렵다. (상속의 문제점은 이 포스팅의 주제가 아니니, 더 구체적으로 설명하지 않겠다.)
이 포스팅은 상속이 좋다, 조합이 좋다를 강조하는 포스팅이 아니다.
상속을 사용하다가, 상속의 단점을 크게 느껴 조합을 잘 사용해보고 싶다는 사람에게 유용할 것이라고 생각한다!
이 포스팅에서는 상속을 사용해 만든 코드를 조합을 사용해 만든 코드로 바꿔 보겠다!
💋 [Before] 상속을 사용해 만든 코드
그러면 내가 만든 블랙잭 게임의 일부를 소개하겠다!
아래는 블랙잭 게임의 룰 중 일부이다.
게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.
딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
플레이어와 딜러는 각자 카드를 추가로 받을 수 있는지에 대해서, 판단 기준이 다르다.
플레이어와 딜러를 각각의 객체로 본다면 "카드를 더 받을 수 있어?"라는 동일한 요청에 대해, 다른 방식의 응답을 하게 된다는 이야기이다. 두 객체는 동일한 요청을 받을 수 있어 동일한 역할을 하고 있다는 점에서 공통점을 뽑아낼 수 있다. 이외의 차이점들은 배제해 단순하게 생각한다면, 추상화할 수 있다. 이 추상화한 객체를 Participant로 정의했다.
* 추상화는, 불필요한 부분을 제거해서 본질을 뽑아낸다고 생각하면 이해하기 쉽다!
암튼 내가 만든 코드의 계층이다.
public abstract class Participant {
private final Name name;
public Participant(final Name name) {
this.name = name;
}
abstract boolean isHittable();
}
이렇게 isHittable(카드 더 받을 수 있니?)이라는 추상 메서드를 상위 계층에서 정의했고, 이 클래스를 상속한 Player와 Dealer는 각각 다른 방식으로 isHittable을 재정의하고 있다.
public final class Player extends Participant {
private static final Score BUST_BOUNDARY_EXCLUSIVE = new Score(21);
public Player(final Name name) {
super(name);
}
public Player(final String name) {
super(new Name(name));
}
@Override
public boolean isHittable() {
return calculateScore().isSmallerThan(BUST_BOUNDARY_EXCLUSIVE);
}
}
public final class Dealer extends Participant {
private static final Score FILL_BOUNDARY_INCLUSIVE = new Score(16);
private static final Name DEALER_NAME = new Name("딜러");
public Dealer() {
super(DEALER_NAME);
}
@Override
public boolean isHittable() {
return calculateScore().isSmallerOrEqualsTo(FILL_BOUNDARY_INCLUSIVE);
}
}
이것을 조합으로 바꾼다면 어떻게 될까?
💋 [After] 조합을 사용해 만든 코드
코드를 변경한 후에 조합의 개념에 대해 설명하겠다.
먼저 Player과 Dealer 두 객체가 공통적으로 하던 역할을 인터페이스로 분리했다.
public interface Role {
boolean isHittable();
}
이후에 우리가 사용하던 Participant 객체에 Role을 필드로 넣어준다.
이렇게 되면 Participant 객체는 Role 인터페이스에 작성한 일을 모두 장착할 수 있다.
public final class Participant {
private final Name name;
private final Role role;
public Participant(final String name, final Role role) {
this.name = name;
this.role = role;
}
public boolean isHittable() {
return role.isHittable();
}
}
Role 인터페이스를 implement한 PlayerRole, DealerRole을 만들게 되면 Participant 객체도 생성자에서 장착한 Role의 종류에 따라서 카드를 뽑을 수 있나?는 동일한 요청을 받았을 때 각자의 세부구현에 따라 다른 응답을 하게 된다.
public class DealerRole implements Role{
@Override
public boolean isHittable() {
...
}
}
public class PlayerRole implements Role{
@Override
public boolean isHittable() {
...
}
}
이제 조합으로 코드를 모두 바꾸게 되었다.
조합은, 상위(가 되고싶은) 클래스[Role]를 확장(extend)하는 것이 아니라,
새로운 클래스[PlayerRole, DealerRole]를 만들고 private 필드로 인스턴스를 참조하도록 하는 방식이다.
💋 그럼 이것도 조합이야..?
전략패턴, 상태패턴.. 모두 결국 조합이라고 볼 수 있다..! 다른 인스턴스를 결국 어딘가에서는 인스턴스로 참조하고 있는데, 그 인스턴스를 참조하는 객체의 입장에서는 조합을 사용하고 있는 것이기 때문이다.
그런데 갑자기 궁금증이 들었다.
private 필드로 인스턴스를 참조한 후에 그 인스턴스 내부의 메서드를 사용하는 방식은 내가 이미 굉장히 많이 사용하고 있었던 것 같다...!
아래 BlackJackGame 클래스를 보면 참가자(Participants), 베팅금액(Bets) 등등을 모두 필드로 관리하고 있다. 이 클래스의 필드에 있는 Participants, Bets 클래스 내부에는 로직을 담은 메서드가 있는데, 이 두 클래스를 필드로 가져서 BlackJackGame 클래스는 Participants, Bets 내부의 기능을 모두 함께 다루고 있었다.
public final class BlackJackGame {
private final Participants participants;
private final CardDeck cardDeck;
private final Bets bets;
public BlackJackGame(final Participants participants, final CardDeck cardDeck) {
this.participants = participants;
this.cardDeck = cardDeck;
this.bets = new Bets();
}
public void distributeInitialCards() {
participants.distributeInitialCards(cardDeck);
}
public void receiveCard(Participant participant) {
participant.receiveCard(cardDeck.getCard());
}
public void addBet(final Player player, final int amount) {
bets.addBet(player, amount);
}
public Map<Player, Money> calculatePlayersProfit() {
return bets.calculatePlayersProfit(makeResult());
}
public Money dealerProfit() {
return bets.calculateDealerProfit(makeResult());
}
public Participants getParticipants() {
return participants;
}
public List<Player> getPlayers() {
return participants.getPlayers();
}
public Dealer getDealer() {
return participants.getDealer();
}
}
이것도 조합일까?
위의 예시에서 Participants, Bets를 모두 필드에 놓고 그 클래스들에 속한 메서드를 호출하는 메서드를 두고 있다. 이렇게 여러 클래스의 기능을 위임하고 있다면 조합을 잘 사용하고 있다고 생각한다.
우테코 크루 허브는 크게 보면 조합이지만 그냥 의존한다 정도가 맞을 것 같다고 했고, 크루 말랑이는 의존이 조합을 포함하긴 하는데, 조합은 기능을 위임하는 목적이 강한 것 같다고 대답했다.
조합은 의존, 전략패턴과 굉장히 유사해 보이기도 한다. 조합인지 아닌지를 손톱자르듯 딱 잘라 구분해야 할까..?
이런 방법이 있다는 것을 알고 쓰면 좋을 것 같다!
https://victorydntmd.tistory.com/292
'JAVA' 카테고리의 다른 글
[JAVA] 원시값 포장과 VO(Value Object) (0) | 2023.03.30 |
---|---|
[JAVA] 리스트(List) 정렬: Collections.sort() 코드를 뜯어보았다, Comparable (0) | 2023.03.27 |
[JAVA] 함수형 프로그래밍: 개념, 스트림에서 부작용 없는 함수를 사용해야 하는 이유, 순수 함수 (20) | 2023.03.22 |
[JAVA] 람다와 스트림(Lambda, Stream)(2): 스트림은 반복문을 대체하기 위해 만들어진 것이 아니다! 스트림의 사용 이유와 방법, 스트림의 생성, 가공(중간연산), 소모(최종연산) (6) | 2023.03.09 |
[JAVA] 람다와 스트림(Lambda, Stream)(1): 람다는 함수형 인터페이스의 구현체이다? (5) | 2023.03.07 |