자바 객체를 설계하는 요령 - 책임주도설계

Libienz·2024년 12월 6일

우아한테크코스 블랙잭 미션을 구현하다가 지나치듯 의심하지 않고 작성한 코드가 있다.
그것은 바로 블랙잭 게임에 참여하는 Player 클래스가 BetAmount(베팅 금액) 클래스를 가지도록 설계한 것이다.

BetAmount를 가지는 Player 클래스

public abstract class Player {
    protected final PlayerName name;
    protected final BetAmount betAmount;
    ...

Player를 모르는 BetAmount 클래스

public class BetAmount {
    private final Money betAmount;
    ...

그런데 사실 Player와 BetAmount 연관관계는 설계는 위 방법 이외에도 다양한 방식으로 수행될 수 있다.

  • Player가 BetAmount를 가지지 않고 BetAmount를 Player가 가지도록 설계
  • Player와 BetAmount 모두 연관된 상대 도메인을 가지도록 설계
  • Player와 BetAmount 모두 서로를 모르고 Player와 BetAmount를 매핑해주는 새로운 객체 설계

나는 다양한 방법이 있음에도 이를 고려하지 않고 당연하게 써내려간 스스로의 코드에 대해 의구심을 가지게 되었다.
더 나아가 도메인을 어떻게 설계해야하는지 탐구하고 싶어졌다.

그래서 리뷰어에게 객체의 연관관계 방향과 객체의 속성을 정의하는 요령에 대해 질문을 남겼고 다음과 같은 답변을 얻을 수 있었다.

  • 객체지향 세계에서 절대적인 정답은 없다
  • 다만 설계를 위해 객체들을 떠올리기 이전에 어떤 메시지가 있는지를 확인해보라
  • 그리고 메시지를 수행하기 위한 객체를 생각해보아라
  • 특정 객체가 스스로 처리할 수 없는 정보 또는 기능이 있다면 다른 적절한 객체의 설계를 고려하라
  • 책임주도설계의 내용을 학습해보아라

리뷰어의 답변을 기반으로 학습을 진행하다 보니 나는 객체의 책임과 속성을 정의하는 나름의 요령으로서 책임주도설계가 있음을 알게 되었다.
그리고 객체 지향에서 중요하게 고려해야 되는 지점들에 대해서도 이전에 알지 못했던 인사이트를 얻게 되었다.

객체지향의 주화입마에 빠져있던 당시의 나에게 단비 같은 학습이었는데 이 깨달음이 오래가길 희망하며 이번 포스팅을 작성한다.
이번 포스팅에서는 학습한 내용을 바탕으로 책임주도설계를 통해 객체의 책임과 속성을 정의하는 요령에 대해서 다룬다.

리뷰어와 나눈 이야기가 궁금한 사람들은 PR을 참고

책임주도설계란?

책임 주도 설계는 객체지향 설계의 중심 철학 중 하나로, 객체의 책임(Responsibility)을 중심으로 역할(Role)과 협력(Collaboration)을 설계하는 접근 방식이다.
객체는 데이터의 단순한 보관소가 아니라, 시스템의 특정 역할을 수행하는 주체로 설계되어야 한다.

책임주도설계 원칙 1: 데이터보다 행동을 먼저 결정하라

블랙잭 미션을 수행하면서 나는 관습적으로 객체를 다음의 과정처럼 설계하곤 했다.

데이터 중심 설계

  • 블랙잭 게임? 베팅이라는 클래스가 필요하겠군
  • 베팅은 어떤 속성을 가질까~ 베팅 금액을 가지면 되겠군
  • 플레이어도 있어야겠네 플레이어는 베팅 금액을 가지면 되겠다.

위의 과정에서 나는 클래스의 행동보다는 클래스가 가져야할 데이터에 신경썼다. 이는 책임주도설계가 아닌 데이터 중심의 설계로서 객체 사이의 협력의 매개체인 메시지를 고려하지 않는 설계 방식이다.

public class Bet {
    private final int amount;

    public Bet(int amount) {
        this.amount = amount;
    }

    public int getAmount() {
        return amount;
    }
}

public class Player {
    private final String name;
    private final Bet bet;

    public Player(String name, Bet bet) {
        this.name = name;
        this.bet = bet;
    }

    public Bet getBet() {
        return bet;
    }
}

클래스의 행동과 책임에 대한 고려는 없고 객체를 데이터를 담는 도구 즈음으로 생각한 결과가 위의 코드이다.
위 코드는 각 객체의 책임과 협력 대상을 고려하지 않고 있고 객체 간의 메시지 전달을 어렵게 만든다.
결과적으로 객체지향적이지 못한 설계로 이어졌음을 확인할 수 있다.

객체지향의 중요한 특징 중 하나는 캡슐화이다. 이 말은 객체지향의 세계에서 객체 내부의 데이터나 객체의 상세한 동작보다는 어떤 메시지를 수행할지, 대외적인 책임이 중요하다는 반증이기도 하다.
즉, 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.

책임주도설계에서는 외부에 제공할 행동을 먼저 정의하고 이를 처리할 수 있는 객체를 찾아나가는 방식을 권장한다.

데이터보다 행동을 고려한 설계

  • 블랙잭 게임에서 어떤 책임(메시지)들이 있을까?
  • 게임에 참여하기라는 책임이 있을 수 있겠군
  • 카드를 나눠줘라라는 책임이 있을 수 있겠군
  • 베팅하기라는 책임이 있을 수 있겠군

책임을 먼저 설계하고 작성된 코드는 다음과 같이 작성될 수 있다.

public class Player {
    private final String name;
    private final int betAmount;
    private final Hand hand;

    public Player(String name, int betAmount) {
        this.name = name;
        this.betAmount = betAmount;
        this.hand = new Hand();
    }

    public void placeBet(int amount) {
        // 베팅 로직
    }

    public void receiveCard(Card card) {
        hand.addCard(card);
    }

    public int calculateScore() {
        return hand.calculateScore();
    }
}

메시지를 기반으로 설계한 구조는 Player가 “베팅하기”, “카드를 받기”, “점수를 계산하기”와 같은 명확한 책임을 가진다. 객체는 메시지를 통해 행동하며, 데이터는 행동의 결과로 처리되는 모습이다.

책임주도설계 원칙 2: 협력이라는 문맥 안에서 책임을 결정하라

메시지가 중요하다. 그렇다면 어떤 객체에게 어떤 책임(메시지)을 할당해야 할까?
이때는 협력이라는 문맥을 고려해야 한다.

책임은 객체의 입장이 아니라 객체가 참여하는 협력의 수준에서 적합해야한다.
즉 협력이란 문맥의 적절한 책임은 곧 클라이언트 관점에서 적절한 책임을 의미하게 된다.

클라이언트 관점에서의 적절한 책임 설계를 위해서는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.

메시지를 먼저 결정하기 때문에 메시지 전송자는 수신자에 대한 어떠한 가정도 할 수 없다.
메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다.

책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유가 여기에 있다.

책임주도설계 흐름

상기 두원칙을 기반으로 한 전체적인 책임주도설계의 흐름은 다음과 같다.

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  • 시스템 책임을 더 작은 책임으로 분할한다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

책임을 도출하고 책임을 수행할 수 있는 적절한 객체를 협력 차원에서 찾는 것이 핵심 흐름이라고 볼 수 있다.

결론

그래서 Player가 BetAmount를 가져야할까 BetAmount가 Player를 가져야 할까?
책임주도설계 원칙을 따르면 어떤 결과가 나올까?

하나의 결과만 나오지는 않는다. 역시나 실버 불릿은 없다.
메시지(책임)을 정의하는 것은 개발자마다 성향이 다를 것이고 메시지를 수행할 수신자를 선택하는 협력 구조 구축도 개발자마다 다를 것이기 때문이다.

나는베팅 하기메시지의 책임 대상을 Player로도 설정하지 않았고 BetAmount에도 설정하지 않았다
PlayerBet 객체를 설계하여 Player들과 베팅정보를 함께 관리하도록 설계하였다.
이처럼 설계한 이유는 플레이어의 베팅은 플레이어에게 필요한 메시지가 아니라 게임에서 필요한 메시지라고 생각했기 때문이다.

다양한 결론을 내릴 수는 있고 모든 것이 정답이 될 수 있는 프로그래밍 세계이지만 결론을 내리는 과정에서의 근거는 항상 중요하다.
관습적인 작업과 근본을 무시하지 않아야겠다는 경각심이 든다.

profile
추상보다 상세에 집착하는 개발자 리비(리비엔즈)입니다 🤗

0개의 댓글