port와 service는 헥사고날 아키텍처에서 application 계층의 가장 핵심적이고 필수적인 구성 요소이지만, 애플리케이션의 복잡성과 설계 결정에 따라 다른 폴더를 추가하여 구조를 더 명확하게 만들 수 있습니다.
application 패키지 내에 추가될 수 있는 다른 폴더들
1. dto 또는 command / query
애플리케이션 계층에서 사용되는 데이터 전송 객체를 별도로 관리하기 위한 폴더입니다. 이는 어댑터 계층의 DTO와는 역할이 다릅니다.
command: CUD(Create, Update, Delete) 유스케이스에 대한 입력 데이터를 캡슐화하는 객체입니다. “무엇을 하라”는 명령의 내용을 담습니다.- 예시:
PlaceOrderCommand.java,UpdateUserAddressCommand.java
- 예시:
query: R(Read) 유스케이스에 대한 입력 데이터를 캡슐화하는 객체입니다. “무엇을 조회해달라”는 요청의 내용을 담습니다.- 예시:
GetOrderQuery.java,SearchUsersQuery.java
- 예시:
폴더 구조 예시:
|
1 2 3 4 5 6 7 8 9 10 11 |
application/ ├── command/ │ └── PlaceOrderCommand.java ├── query/ │ └── GetOrderQuery.java ├── port/ │ ├── in/ │ └── out/ └── service/ |
왜 이 폴더를 사용할까?
- 의도의 명확화:
PlaceOrderUseCase.placeOrder(PlaceOrderCommand command)처럼 메서드 시그니처만 봐도 “이 유스케이스는 ‘주문하기’ 명령을 처리하는구나”라는 의도가 명확해집니다. - 입력 데이터 유효성 검사: Command/Query 객체 내에서 생성자를 통해 입력값에 대한 기본적인 유효성 검사를 수행할 수 있습니다.
- 서비스 메서드 시그니처 단순화: 여러 개의 파라미터를 하나의 객체로 묶어 전달하므로, 메서드 시그니처가 깔끔해집니다.
참고: 일부 설계에서는 이 Command/Query 객체들을
port/in패키지 내에 두기도 합니다. 인바운드 포트와 관련된 데이터 구조로 보기 때문입니다. 어느 쪽이든 팀의 컨벤션에 따라 일관성 있게 정하면 됩니다.
2. exception
애플리케이션의 비즈니스 로직과 관련된 **커스텀 예외(Custom Exception)**를 정의하는 폴더입니다.
- 예시:
OrderNotFoundException.java,ProductStockNotEnoughException.java
폴더 구조 예시:
|
1 2 3 4 5 6 7 |
application/ ├── exception/ │ └── OrderNotFoundException.java ├── port/ └── service/ |
왜 이 폴더를 사용할까?
- 명시적인 예외 처리:
try-catch블록에서catch (OrderNotFoundException e)와 같이 특정 비즈니스 예외 상황을 명시적으로 처리할 수 있게 해줍니다. - 오류 상황 전달: 컨트롤러(어댑터)에서 이 커스텀 예외를 잡아 사용자에게 더 친절한 오류 메시지(예: “주문 정보를 찾을 수 없습니다.”)와 적절한 HTTP 상태 코드(예: 404 Not Found)를 반환하는 데 사용됩니다.
3. common 또는 util
애플리케이션 계층 내에서 공통적으로 사용되지만, 특정 도메인에 속하지 않는 유틸리티성 클래스나 인터페이스를 모아두는 폴더입니다.
- 예시:
PagingUtil.java(페이징 처리 관련 헬퍼),EventPublisher.java(도메인 이벤트를 발행하는 인터페이스)
폴더 구조 예시:
|
1 2 3 4 5 6 7 |
application/ ├── common/ │ └── PagingUtil.java ├── port/ └── service/ |
왜 이 폴더를 사용할까?
- 여러 서비스에서 중복해서 사용되는 로직을 분리하여 재사용성을 높입니다.
port나service에 두기 애매한 성격의 파일들을 체계적으로 관리할 수 있습니다.
종합적인 application 폴더 구조 예시
위의 요소들을 모두 포함한 종합적인 application 패키지 구조는 다음과 같을 수 있습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
com/example/bookstore/ └── order/ └── application/ ├── command/ │ ├── PlaceOrderCommand.java │ └── CancelOrderCommand.java ├── exception/ │ └── OrderNotFoundException.java ├── port/ │ ├── in/ │ │ ├── PlaceOrderUseCase.java │ │ └── CancelOrderUseCase.java │ └── out/ │ ├── LoadOrderPort.java │ └── SaveOrderPort.java └── service/ ├── PlaceOrderService.java └── CancelOrderService.java |
결론
port와 service가 application 계층의 필수 골격인 것은 맞습니다.
하지만 애플리케이션이 복잡해짐에 따라 command/query, exception, common 과 같은 추가적인 폴더를 구성하여 관심사를 더 세분화하고 코드의 의도를 명확하게 만들 수 있습니다.
이러한 구조는 정답이 있는 것이 아니라, 프로젝트의 규모와 팀의 합의에 따라 유연하게 결정됩니다. 중요한 것은 **”왜 이 폴더를 여기에 두는가?”**에 대한 명확한 기준과 일관성을 유지하는 것입니다.