[프로젝트 회고] 7STARBALL Meeting 도메인 Rich Domain 리팩토링

들어가며

7STARBALL 프로젝트에서 내가 주로 맡았던 영역은 Meeting, 즉 모임 도메인이었다.

처음에는 기능을 빠르게 구현하는 것이 우선이었다. 모임을 만들고, 조회하고, 참여하고, 나가고, 상태를 바꾸는 API가 필요했다. 그래서 Controller, Service, Repository 흐름을 기준으로 기능을 하나씩 붙여나갔다.

그런데 기능이 늘어날수록 한 가지 문제가 보이기 시작했다.
MeetingServiceImpl 안에 너무 많은 판단이 들어가고 있었다.

  • 이 사용자가 호스트인지 확인
  • 지금 모임에 참여할 수 있는 상태인지 확인
  • 정원이 찼는지 확인
  • 참여자가 빠지면 상태를 다시 바꿔야 하는지 확인
  • 수정 가능한 상태인지 확인

처음에는 “서비스에서 처리하면 되지 않을까?”라고 생각했지만, 코드를 볼수록 이 로직들은 서비스의 일이 아니라 Meeting이라는 도메인 자체가 알고 있어야 할 규칙에 가까웠다.

그래서 이 시점에 Meeting 도메인을 Rich Domain Model 형태로 리팩토링했다.

기존 구조의 아쉬움

초기 구조에서는 Service가 대부분의 비즈니스 판단을 담당했다.

예를 들면 모임 참여 로직은 대략 이런 흐름이었다.

  1. 모임을 조회한다.
  2. 멤버를 조회한다.
  3. 이미 참여 중인지 확인한다.
  4. 정원이 찼는지 확인한다.
  5. 참가자를 저장한다.
  6. 정원이 찼다면 상태를 FULL로 바꾼다.

이런 흐름 자체는 자연스럽지만, 문제는 조건이 많아질수록 서비스 코드가 점점 커진다는 점이었다.

특히 MeetingStatus, capacity, host, participant 같은 개념은 모두 Meeting 도메인과 강하게 연결되어 있었다. 그런데 이 규칙들이 서비스 곳곳에 흩어져 있으면 나중에 상태가 하나 더 추가되거나 정책이 바뀌었을 때 변경 범위가 커질 수밖에 없었다.

이때 “도메인이 자신의 규칙을 직접 표현하게 만들자”라는 방향으로 리팩토링을 진행했다.

Rich Domain으로 옮긴 책임

리팩토링하면서 Meeting 엔티티에 다음과 같은 행위들을 옮겼다.

  • updateMeetingInfo()
  • changeStatus()
  • isHost()
  • canJoin()
  • canLeave()
  • validateForUpdate()
  • validateStatusChange()

이전에는 서비스에서 if문으로 확인하던 조건들을 Meeting 내부 메서드로 옮겼다.

예를 들어 “이 모임에 참여할 수 있는가?”라는 질문은 서비스가 모든 필드를 꺼내서 판단하기보다, Meeting에게 직접 물어보는 편이 더 자연스럽다.

1
2
3
if (!meeting.canJoin()) {
    throw new CustomException(...);
}

이렇게 바꾸면 서비스는 세부 조건을 직접 알 필요가 줄어든다.
서비스는 흐름을 담당하고, 도메인은 규칙을 담당한다.

MeetingDomainService를 둔 이유

모든 로직을 엔티티 안으로 넣는 것이 정답은 아니라고 생각했다.

참여자 추가와 삭제는 Meeting 하나만으로 끝나지 않았다. Participant, Member, Role, Repository 조회 흐름이 함께 필요했다. 그래서 참여/취소처럼 여러 도메인이 함께 움직이는 로직은 MeetingDomainService로 분리했다.

내가 잡은 기준은 이랬다.

  • Meeting 하나의 상태와 규칙으로 판단 가능한 것은 Meeting 내부로 이동
  • 여러 도메인 객체와 저장소 흐름이 함께 필요한 것은 DomainService에서 처리
  • Controller와 Application Service는 요청 흐름과 트랜잭션 경계에 집중

이 기준을 잡고 나니 코드 위치를 결정하기가 조금 쉬워졌다.

리팩토링하면서 느낀 점

이번 작업을 하면서 가장 크게 느낀 것은, 객체지향은 단순히 클래스를 나누는 것이 아니라는 점이었다.

처음에는 엔티티를 데이터 담는 객체처럼만 사용했다. 필드를 두고, Getter로 꺼내고, 서비스에서 판단하는 방식이었다. 그런데 그렇게 만들면 도메인 규칙이 점점 바깥으로 새어나간다.

반대로 도메인 객체가 자기 규칙을 가지게 만들면, 코드가 조금 더 말이 되는 형태가 된다.

1
서비스가 Meeting의 상태를 해석한다

보다

1
Meeting에게 가능한 행동인지 물어본다

가 훨씬 자연스럽다.

물론 Rich Domain으로 바꾸면서 테스트 코드도 같이 수정해야 했다. 기존 테스트는 서비스 내부 구현에 기대고 있던 부분이 있었기 때문에, 도메인 메서드 기준으로 검증 방식을 다시 맞춰야 했다.

그래도 결과적으로는 책임이 조금 더 분명해졌다.

마무리

이번 리팩토링은 단순히 코드를 예쁘게 정리하는 작업은 아니었다.
모임이라는 도메인이 가진 규칙을 어디에 둘 것인지 고민한 작업이었다.

아직 완벽한 구조라고 말하기는 어렵지만, 이 작업을 하면서 Service에 모든 로직을 몰아넣는 방식의 한계를 느꼈고, 도메인 중심 설계가 왜 필요한지 조금은 몸으로 이해할 수 있었다.

다음에는 이 구조 위에서 조회 성능과 동시성 문제를 개선한 내용을 정리해보려고 한다.

Leave a comment