brunch

You can make anything
by writing

C.S.Lewis

by 최창규 May 03. 2018

카카오헤어샵의 DDD

비즈니스 조직이 하는 말에 귀를 기울여라.

유지보수가 쉬운 시스템이 갖추어야 할 요소 중에서 가장 중요한 2가지는 바로 Loosed Coupling과 High Cohesion입니다. Loosed Coupling은 모듈 간의 연관관계가 interface로 되어 있어서 느슨해야 한다는 것이고 High Cohesion은 어떤 목적을 위해 연관된 기능들이 모여서 구현되어 있고 지나치게 많은 일을 하지 않는 것을 말합니다. 카카오헤어샵 프로젝트는 Loosed Coupling과 High Cohesion을 갖기 위해 DDD(Domain Driven Design)의 몇 가지 원칙을 적용했습니다.



도메인 지식 쌓기

일반적으로 개발자들은 최신 기술을 갈망합니다. 그렇지만 소프트웨어의 본질은 기술이 아니라 도메인의 문제를 해결하는 것입니다. 카카오헤어샵은 개발자들도 기술보다 도메인에 집중했습니다. 서비스 기획에 적극적으로 참여하고, 미용업 관련자들과의 미팅에도 함께 했습니다. 본인이 헤어를 새로 해야 할 때마다 미용실을 바꿔서 예약하고 디자이너들의 고충과 요구사항을 들어 서비스에 반영했습니다. 도메인의 본질을 이해해야만이 좋은 서비스가 나온다는 것을 모두가 알고 있기 때문입니다.



Ubiquitous Language

일반적인 IT 프로젝트에서 개발자는 도메인 지식이 비즈니스 조직(도메인 전문가)에 비해서 부족합니다. 비즈니스 요구사항을 소스코드에 잘 녹이기 위해서는 둘 사이에 보편적인 의사소통 언어가 필요합니다. 그것을 DDD에서는 Ubiquitous Language라고 합니다. 카카오헤어샵은 개발팀과 비즈니스 조직, 특히 기획팀과의 의사소통을 위해 많은 노력을 했습니다. 엔티티 작성 시 업무 현장에서 사용하는 용어를 그대로 차용했습니다. 예를 들어 예약(Reservation)의 상태는 결제대기(READY), 결제완료(OK), 결제후취소(CANCELED), 취소신청(WAIT_CANCEL), 시술확정(COMPLETED), 노쇼(NO_SHOW)가 있습니다. 이는 고객이 예약을 하는 일련의 과정 중에서 발생할 수 있는 상태를 기획팀과 함께 정의한 값입니다. 

 


Domain Model

DDD를 잘 구현하기 위한 몇 가지 도메인 모델이 있습니다. Entity, Value Object, Aggregate, Service, Repository, Factory 등이 이에 해당합니다. Entity는 존재간에 구별할 수 있는 모델입니다. 그래서 식별자(Id)가 있습니다. Value Object는 식별자가 필요 없는 고유 모델입니다. 불변하는 상태나 값이 이에 해당합니다. Aggregate는 생명주기가 동일한 모델들을 모아 놓은 Root 모델입니다. Service는 도메인 간의 연산을 처리하는 모델입니다. Repository는 모델을 저장하는 곳이고 Factory는 Entity나 Aggregate를 생성하는 모델입니다.

카카오헤어샵은 Entity, Value Object, Service, Repository, Factory를 적용하였습니다. Entity는 JPA의 @Table과 매핑해서 사용했고, Value Object는 매장 부가정보, 예약상태, 디자이너 등급과 같은 형태로 사용했습니다. Repository는 JPA의 JpaRepository interface를 상속해서 사용했고 모델 객체를 생성할 때 Factory를 사용했습니다. Aggregate를 적용하지 못한 것은 그 구현이 쉽지 않았기 때문입니다. 그리고 JPA의 Lazy loading를 사용하니 딱히 Aggregate가 필요하진 않았습니다. 

Lazy loading을 사용한 카카오헤어샵의 예약 Entity는 아래와 같습니다. 

@Entity
class Reservation {

    @Id
    long id;
    
    @ManyToOne
    ServiceUser serviceUser;    

    @ManyToOne
    Shop shop; 

    @ManyToOne    
    Product product;
}

serviceUser는 고객 Entity이고, shop은 매장, product는 메뉴입니다. 그래서 어떤 예약의 매장의 연락처를 알고 싶으면 reservation.getShop().getContactPhoneNumber() 로 접근하면 됩니다. 



Layered Architecture

위 Domain Model에서 역할에 따라 모델을 Entity, Value Object, Service 등으로 분리했다면 레이어별로 분리하면 User Interface, Application, Domain, Infrastructure로 구분할 수 있습니다. 이처럼 레이어를 분리해야만 다른 사람이 코드를 읽고 이해하기 쉽습니다.

User Interface는 사용자의 요청을 하위 레이어로 전달하는 역할을 합니다. 

Application은 복잡한 비즈니스 로직을 처리하는 레이어입니다.

Domain은 도메인에 대한 정보, 객체의 상태, 도메인의 비즈니스 로직을 제공하는 레이어입니다.

Infrastructure는 영속성을 구현하거나 외부와 통신하는 기능을 제공하는 레이어입니다.


각 레이어는 아래와 같이 하위 레이어를 의존합니다. User Interface는 모든 하위 레이어를 의존할 수 있고 반대로 Infrastructure는 다른 레이어를 의존하면 안 됩니다.


카카오헤어샵은 User Interface에는 Spring MVC의 @Controller를 구현했습니다. 그리고 응답 객체를 Cache 하는 단순 용도로만 사용한 Service를 @Component로 구현했습니다.

Application에는 Domain을 조합하거나 복잡한 비즈니스 로직이 있는 @Service를 구현했습니다.

Domain에는 JPA의 @Entity와 Entity를 빌드하는 Factory를 구현했습니다.

Infrastructure에는 영속성을 구현한 Dao와 외부와 통신하는 @Component가 있습니다.


예를 들어 매장을 조회하는 일련의 과정을 레이어 별로 보면 아래와 같습니다.

interface
  ┗ RetrieveShopController.java
  ┗ RetrieveShopRequest.java
  ┗ ShopResponse.java

application
  ┗ RetrieveShopService.java

domain
  ┗ Shop.java
  ┗ ShopFactory.java

infrastructure
  ┗ FindShopDao.java
  ┗ ShopSearcherByDaumSearch.java



Bounded Context

대규모 프로젝트에서는 시스템을 각각의 콘텍스트로 분할해야 합니다. 그래야 병렬로 개발이 가능합니다. 각 콘텍스트의 크기는 하나의 팀에 할당할 수 있을 만큼 작아야 합니다. 

카카오헤어샵은 컨택스트를 분할하지 못했습니다. 레이어를 분리하고 그 안에 기능 별로 패키지만 나눈 형태입니다. 그래서 1개의 Dao를 2개의 콘텍스트에서 사용하는 경우도 있었습니다.

interface
  ┗ shop
  ┗ reservation

application
  ┗ shop
  ┗ product

domain
  ┗ shop
  ┗ product
  ┗ reservation

dao
  ┗ product
  ┗ reservation
  ┗ review

다음 리팩터링은 때에는 콘텍스트를 먼저 분할하고 그다음에 레이어를 분리하는 형태로 아래와 같이 진행하려고 합니다.

shopsearch
  ┗ interface
  ┗ application
  ┗ domain
  ┗ infrastructure

reservation
  ┗ interface
  ┗ application
  ┗ domain
  ┗ infrastructure

review
  ┗ interface
  ┗ application
  ┗ domain
  ┗ infrastructure



정리

카카오헤어샵은 유지보수가 쉬운 시스템을 지향하면서 Domain Driven Architecture 원칙을 적용하였습니다. 조금 어려운 개념이라서 적용이 쉽지 않았지만 핵심은 도메인 모델을 선택적으로 추상화하여 엄격하게 조직화한다는 것입니다. 그래서 우리는 Entity와 Service들을 추상화하려고 노력했습니다. 

반면에 Bounded Context는 제대로 적용하지 못했습니다. 콘텍스트를 분할하고 레이어를 분리하면 Micro-service로 전환이 쉬워지는데 그 부분이 아쉽습니다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari