[Project] 4dollar - 헥사고날 아키텍처에서 패키지 구조 전환

2025. 6. 14. 01:03·프로젝트
반응형

개요

사이드 프로젝트를 시작하면서 새로운 아키텍처를 사용하고 싶었다. 지금까지는 레이어드 아키텍처를 쓰거나, 진행중인 프로젝트의 기존 규칙을 따르기만해서 패키지 구조에 대해 깊이 고민한 경험이 없었다. 새로 시작하는 프로젝트인만큼 멋진 구조를 만들고 싶어서 유튜브나 책에서 많이 접해봤던 헥사고날 아키텍처를 도입했다. 직접 사용해보는건 처음이라 공부하면서 진행하느라 조금 엉성한 느낌은 있었지만 나름 적절히 적용할 수 있었다. 하지만 개발을 진행하면서 몇몇 불편한 부분이 생겨서 새로운 구조로 전환했다. 더 개선할 여지는 있지만 아직까지는 문제 없다고 생각한다. 기존 패키지 구조에서 어떤 문제점을 경험했는지, 새로운 패키지구조에 대한 고민과 어떤 부분을 변경했는지 포스팅을 남긴다.

기술 스택: Java, Spring Boot 3.x, Spring Data JPA, PostgreSQL

초기 패키지 구조

처음에는 만들면서 배우는 클린 아키텍처라는 도서를 참고해서 패키지를 구성했다. 책에서 나온 기본 예제는 아래와 같다. 구성요소의 이름은 조금씩 다르지만 이와 비슷하게 패키지 구조를 만들었다.

📦buckpal
┗ 📂account
  ┣ 📂adapter
  ┃ ┣ 📂in
  ┃ ┃ ┗ 📂web
  ┃ ┃ ┃   ┗ 📜AccountController
  ┃ ┗ 📂out
  ┃   ┗ 📂persistence
  ┃     ┣ 📜AccountPersistenceAdapter
  ┃     ┗ 📜SpringDataAccountRepository
  ┣ 📂domain
  ┃ ┣ 📜Account
  ┃ ┗ 📜Activity
  ┗ 📂application
    ┣ 📜SendMoneyService
    ┗ 📂port
      ┣ 📂in
      ┃ ┗ 📜SendMoneyUseCase
      ┗ 📂out
        ┣ 📜LoadAccountPort
        ┗ 📜UpadateAccountStatePort

각 컴포넌트들은 아래의 역할을 수행한다. 핵심 원칙은 모든 의존성은 도메인을 향해야 하는 것이다.

  • Domain : 핵심 비즈니스 로직
  • Application : 유스케이스 구현
    • In Port : 기능의 인터페이스
    • Out Port : 외부 시스템과의 계약
  • Adapter : 외부 세계와의 연결점
    • In Adapter : 외부 요청을 애플리케이션으로 전달
    • Out Adapter : 애플리케이션의 요청을 외부로 전달

이 구조의 장점은 비즈니스 로직이 외부 기술에 의존하지 않아 테스트하기 쉽고 외부 시스템 변경 시에도 도메인 로직은 영향을 받지 않는다는 것이다. 내가 기대했던 헥사고날 아키텍처의 장점은 다음과 같다.

  • 비즈니스 로직과 기술적 세부사항의 완전한 분리
  • 테스트하기 쉬운 구조 : 외부 의존성 없이 도메인 로직 테스트 가능
  • 기술 스택 변경에 유연한 대응 : JPA → MyBatis, MySQL → PostgreSQL 등

느꼈던 문제점들

중복 클래스 작성의 피로감

하지만 필요한 패키지가 많은만큼 작성해야하는 파일의 양도 많아졌다. 특히 새로운 도메인 모델이 필요할 때마다 거의 동일한 구조의 클래스를 두 개 만들어야 했다. 게다가 두 클래스간 서로 변환하는 메서드를 계속 작성해야 했다. 프로젝트 초기 단계라 클래스의 필드를 수정해야할 상황이 많았는데 이 때 실수로 변경할 필드를 누락할 위험성이 있었다.

// 도메인 모델
public class Category {
    private Long id;
    private String name;
    private String description;
    private CategoryStatus status;
}

// JPA 엔티티 (거의 동일한 필드 구조 + 도메인 변환 로직)
@Entity
public class CategoryJpaEntity {
    @Id @GeneratedValue
    private Long id;
    @Column(nullable = false)
    private String name;
    private String description;
    @Enumerated(EnumType.STRING)
    private CategoryStatus status;
}

JPA 핵심 기능 쓰기 불편함

JPA의 강력한 기능들을 제대로 활용할 수 없다는점이 불편했다. 영속성을 위해서 JPA를 사용했는데, JPA에서는 객체와 테이블을 매핑하는 JPA엔티티가 반드시 필요하다. 매번 JPA 엔티티를 만드는 귀찮은 작업을 했는데도 JPA 기능을 사용하지 못한다는게 아쉬웠다.

  • 영속성 전이를 사용할 수 없다 : 도메인 모델이 JPA 엔티티와 분리되어 있어서 @OneToMany의 cascade 옵션을 사용할 수 없다.
  • 변경 감지(Dirty Checking)를 활용할 수 없다 : 도메인 모델과 JPA 엔티티가 분리되어 있어서 JPA의 변경 감지 기능을 사용할 수 없다.

아키텍처 전환

아래 목적을 가지고 프로젝트의 아키텍처를 새롭게 구성했다.

  • 패키지 구조가 좀 더 단순하고 직관적이었으면 좋겠다.
  • JPA 엔티티를 도메인 모델로 사용하고 싶다.

패키지 구조 단순화

📦coupon
┣ 📂application
┃ ┣ 📂port
┃ ┃ ┗ 📜RegisterCouponUseCase // in port
┃ ┗ 📂service
┃   ┗ 📜RegisterCouponService // usecase implemantation 
┣ 📂domain
┃ ┣ 📂exception
┃ ┃ ┗ 📜CouponNotFoundException
┃ ┣ 📂model
┃ ┃ ┣ 📜Coupon // JPA 엔티티
┃ ┃ ┗ 📜CouponStatus
┃ ┣ 📂port
┃ ┃ ┗ 📜SaveCouponPort // out port
┃ ┗ 📂service
┃   ┗ 📜CouponDomainService
┗ 📂infrastructure
  ┗ 📂adapter
    ┗ 📂persistence // driven adapter
      ┣ 📜CouponJpaRepository // Jpa Repository
      ┗ 📜CouponPersistenceAdapter // driven adapter

패키지 구조 변경의 핵심 이유:

  • port/in, port/out 구분이 직관적으로 느껴지지 않았다. 패키지 변경 후에도 port라는 이름은 유지하고 있는데, 다른 적절한 네이밍을 고민하고 있다.
  • out port를 도메인 하위로 이동했다. 도메인에서 외부 시스템과의 경계를 나타내는 인터페이스(out port)는 도메인 하위에 있는게 타당하다고 생각했다.
  • 현재 프로젝트는 멀티모듈 구조로 만들어져 RestController는 별도의 모듈에 있어 adapter/in이 필요 없다.

JPA 엔티티를 도메인 모델로 통합

  1. 현실적인 기술 스택 고정: 어차피 이 프로젝트에서 JPA를 다른 기술로 바꿀 계획이 없다.
  2. JPA 기능 완전 활용: 영속성 전이, 지연 로딩 등 강력한 기능들을 온전히 사용하고 싶다. 이전에는 port 뒤에 숨겨져서 사용하기 까다로웠다.
// 새로운 통합 모델 (domain/model/Coupon.java)
@Entity
@Table(name = "coupons")
public class Coupon {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Enumerated(EnumType.STRING)
    private CouponStatus status;

    // JPA 기본 생성자 + 비즈니스 생성자
    prtected Coupon() {}

    public Coupon(String name) {
        this.name = name;
        this.status = CouponStatus.ACTIVE;
    }
    
    // 비즈니스 로직들
    public void activate() {
        this.status = CouponStatus.ACTIVE;
    }
}

개선 효과

직관적인 패키지 네이밍으로 가독성 향상

port/in, port/out의 모호한 이름 대신 레이어드 아키텍처와 비슷하게 구성해 좀 더 직관성이 높아졌다. 새로운 팀원이 합류해도 패키지 구조를 쉽게 이해할 수 있지 않을까?

JPA 기능 활용도 증대

JPA 엔티티를 도메인 모델로 사용함으로써 영속성 전이, 변경 감지 등 JPA 핵심 기능을 활용할 수 있게 되었다. 게다가 별도 엔티티 클래스 작성 및 매핑 로직 제거로 개발 생산성이 향상되었다.

마무리

여러 패키지 구조를 고민하면서 깨달은 원칙은 도메인 계층을 다른 계층에 의존하지 않도록 만드는 것이었다. 헥사고날 아키텍처든 레이어드 아키텍처든, 핵심 비즈니스 로직이 외부 기술에 종속되지 않는다면 아키텍처의 본질적 목표는 달성할 수 있다고 생각한다. 비즈니스 요구상황에 맞게 비즈니스 복잡도와 아키텍처 복잡도를 고려해 선택할 필요가 있을 것 같다.

아직 까지는 잘 사용하고 있지만 현재 구조도 완벽하지는 않다. 처음부터 완벽하게 만들 수 있는건 없으니 조금씩 개선해야 한다. 도메인 서비스와 애플리케이션 서비스의 역할 분담을 더 명확히 나눌 수 있을지, 또 적절한 네이밍룰은 없을지 계속 고민해야겠다.

이번 사이드 프로젝트를 하면서 아키텍처는 수단이지 목적이 아님을 다시 한번 배울 수 있었다. 프로젝트의 상황과 팀의 역량, 비즈니스 요구사항을 종합적으로 고려해서 가장 적합한 구조를 선택하는 것이 중요할 것이다.

반응형
저작자표시 비영리 변경금지 (새창열림)
'프로젝트' 카테고리의 다른 글
  • [Project] 4dollar - 경계간 매핑하기
  • 리더보드 만들기 - 1
덴마크초코우유
덴마크초코우유
IT, 알고리즘, 프로그래밍 언어, 자료구조 등 정리
    반응형
  • 덴마크초코우유
    이것저것끄적
    덴마크초코우유
  • 전체
    오늘
    어제
    • 분류 전체보기 (127)
      • Spring Framework (11)
        • Spring (6)
        • JPA (3)
        • Spring Security (0)
      • Language (52)
        • Java (12)
        • Python (10)
        • JavaScript (5)
        • NUXT (2)
        • C C++ (15)
        • PHP (8)
      • DB (16)
        • MySQL (10)
        • Reids (3)
        • Memcached (2)
      • 개발 (3)
      • 프로젝트 (3)
      • Book (2)
      • PS (15)
        • 기타 (2)
        • 백준 (2)
        • 프로그래머스 (10)
      • 딥러닝 (8)
        • CUDA (0)
        • Pytorch (0)
        • 모델 (0)
        • 컴퓨터 비전 (4)
        • OpenCV (1)
      • 기타 (16)
        • 디자인패턴 (2)
        • UnrealEngine (8)
        • ubuntu (1)
        • node.js (1)
        • 블로그 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    FPS
    게임
    MySQL
    게임 개발
    C
    프로그래머스
    Python
    CPP
    알고리즘
    pytorch
    Unreal
    JavaScript
    딥러닝
    언리얼엔진4
    Unreal Engine
    클래스
    PS
    블루프린트
    C++
    파이썬
    mscoco
    select
    NUXT
    php
    자바
    웹
    map
    memcached
    JS
    redis
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
덴마크초코우유
[Project] 4dollar - 헥사고날 아키텍처에서 패키지 구조 전환
상단으로

티스토리툴바