개발

헥사고날 아키텍처 적용: 서비스와 도메인의 독립성 확보

kdozlo 2025. 1. 27. 20:18

헥사고날 아키텍처 도입

  • 데이터베이스 변경 리펙토링 과정에서 두가지 문제점을 발견했습니다.
    • DB 엔티티와 도메인 로직의 결합
    • 서비스 로직과 데이터베이스 강결합
  • 두 문제점을 통해 DDDClean Architecture의 필요성을 느꼈습니다.
  • 이를 구현하기 위해 헥사고날 아키텍처를 도입하기로 결정했습니다.
    • 레이어드 아키텍처 → 헥사고날 아키텍처
    • 기존 패키지 구조
- controller
- service
- entity
- repository
- dto

 

 

헥사고날 아키텍처 적용 범위

헥사고날1

  • 헥사고날 아키텍처의 핵심은 포트와 어댑터를 통해 각 계층별 의존성을 줄여 변경이나 확장에 유리하도록 하는 것 입니다.
  • 따라서 아래와 같은 구조로 설계할 계획을 세웠습니다.
    • 패키지 구조
    - controller
      - controller.java
      - dto
      - port
        - service.interface
    - service
      - serviceImpl.java
    - domain
      - domain.java
      - port
        - repository.interface
    - infrastructure
      - repository
        - repositoryImpl.java
      - entity
        - entity.java
    헥사고날2
  • 하지만 해당 구조로 설계를 할 경우, 각 계층별 의존성을 낮추기 위해 dto를 controller → service, service → controller로 직접 줄 수 없습니다. service → repository, repository → service 역시 마찬가지 입니다.
  • 이를 해결하기 위해서는 각 계층별로 데이터를 받을 수 있도록 포트 모델(in, out)를 설계해야합니다. 하지만 이 경우 현재 프로젝트에서 과하다는 판단이 들었습니다.
    헥사고날3
  • 현재 프로젝트에서 문제점은 엔티티와 도메인의 결합, 서비스로직과 데이터베이스의 강결합입니다.
  • 다시말해 도메인과 핵심 서비스 로직을 외부로부터 독립시켜야 합니다.
  • 따라서 다음과 같이 설계하기로 했습니다.
    • infrastructure 계층만 헥사고날 아키텍처를 도입한다.
      • service, domain은 변경 가능성이 낮다고 판단.
    • 포트 모델(in, out)를 설계 X
    • controller 패키지에 있는 dto 패키지를 밖으로 뺀다.
    • request dto는 controller에서 데이터를 받아서 service까지 넘겨주는 역할로 둔다.
    • response dto는 service에서 생성하여 controller로 넘겨준다.
    • dto가 엔티티나 도메인에 직접적인 의존성을 가지지 않도록 한다.
      • dto의 값 하나하나를 직접 넘겨주거나, 중간 매퍼 클래스를 이용한다.
    • 도메인에서는 어떤 외부 정보도 모르게 한다.
      • dto, entity를 직접 사용하지 않도록 하기
    • service, mapper를 제외한 어떠한 외부에서 도메인의 존재를 모르게 한다.
    • 수정된 패키지 구조
    - controller
      - controller.java
    - dto
    - mapper
    - service
      - serviceImpl.java
    - domain
      - domain.java
      - port
        - repository.interface
    - infrastructure
      - repository
        - repositoryImpl.java
      - entity
        - entity.java

 

infrastructure(repository)만 의존성 역전을 한 이유

  1. 서비스의 핵심은 비즈니스 로직과 도메인 로직
    • 제공하는 서비스의 본질은 비즈니스 로직(service)과 도메인 로직(domain)에 있으며, 이는 구체적으로 구현되어야 합니다.
    • 추상화된 service 인터페이스를 도입하더라도, 비즈니스 로직이 변경되면 해당 인터페이스와 구현체 모두 수정해야 하기 때문에, 현재 규모에서는 오히려 불필요한 복잡성을 초래한다고 판단했습니다.
  2. 프로젝트 규모에 적합한 설계
    • 현재 프로젝트에서는 한 도메인에 대해 기능을 세분화하거나 여러 구현체로 나누어 개발하지 않습니다.
    • 따라서, service를 추상화하지 않아도 충분히 유연성을 확보할 수 있습니다.
  3. 단방향 의존성 유지
    • controller → service → domain으로 이어지는 단방향 의존성은 이미 잘 유지되고 있습니다.
    • controller와 service를 추가로 추상화하지 않아도, 외부 → 내부로만 흐르는 단방향 설계가 보장됩니다.
  4. 도메인 독립성 유지
    • 도메인 계층은 여전히 외부 기술(infrastructure)이나 구체적인 구현(repository)에 영향을 받지 않고, 비즈니스 로직만 처리하도록 독립성을 유지합니다.
  5. 테스트 용이성
    • repository에 대해 의존성 역전을 통해 service 계층의 테스트에서 H2 등 데이터베이스 없이도 단위 테스트가 가능하도록 했습니다.

 

DTO 패키지를 따로 분리한 이유

  1. 서비스에서 제공하는 데이터(response DTO)와 요청받는 데이터(request DTO)는 서비스가 제공하거나 처리해야 할 구체적인 정보입니다.
    • 요청과 응답 데이터의 형식은 서비스의 책임과 긴밀하게 연결되어 있으므로, DTO와 서비스 간 의존성이 생기는 것이 괜찮다고 생각했습니다.
    • 예를 들어, 요청 데이터가 변경되거나 서비스에서 제공하는 응답 데이터가 수정된다면, 이는 비즈니스 요구사항의 변경을 의미하므로, 해당 서비스 로직도 함께 변경되어야 합니다.
  2. DTO는 service 계층까지만 의존성을 가져야 합니다.
    • 이는 도메인 영역을 외부로부터 완전히 독립시켜, 변경 가능성을 최소화하기 위함입니다.
    • 도메인 모델은 비즈니스 로직에만 집중해야 하며, 외부 요청이나 응답 형식(DTO)에 종속되지 않아야 합니다.


이미지 출처 : https://www.inflearn.com/course/자바-스프링-테스트-개발자-오답노트/dashboard 강의자료