Pull Request 024. 아키텍처 리팩토링 및 패키징 수정 - 자유 게시글 등록 기능에 헥사고날 아키텍처 적용하기
진행하고 있는 프로젝트는 지금까지 전형적인 3티어 아키텍처를 적용하였다. 그러나 사실상 애플리케이션 레이어에 모든 기능이 몰아져 지나치게 해당 레이어가 비대해졌다. 더 큰 문제는 이게 3티어 아키텍처인지를 표현하지 못하는 패키징 방식을 사용하고 있었다는 것인데, 이를 해결하고 시스템 복잡도를 제어하기 위한 더 나은 방향성을 지향하기 위해 헥사고날 아키텍처를 적용해보고자 한다.
먼저 기존의 패키징 방식을 살펴보자.
하나의 도메인 영역 내에 계층 구분이 명확하게 되어있지 않다, controller와 repository가 같은 단위에 자리하고 있다. 특정 계층에서 역할을 수행하는 객체를 찾기가 쉽지 않다, dto의 사용 위치나 entity의 사용 위치를 파악하기 어렵다.
아래는 아키텍처를 적용하고 패키지를 수정한 결과다.
freeboard라는 도메인 영역 내에 adpater, application, domain으로 각 계층을 분리했다. Input Adapter와 Output Adapter를 분리해 각 영역에서 사용되는 객체 탐색이 쉽다. 마찬가지로 애플리케이션 계층에서도 port와 service를 분리해 의존하는 추상적 영역과 구현의 영역을 구분했다.
Controller를 Post Controller로 따로 뻈다. 단일 책임의 원칙을 지키기 위해서다. 기존의 Controller는 CRUD 기능을 모두 담당하고 있었다. 즉, 등록 / 수정 / 삭제 / 조회 중 어느 한 기능에만 변경이 발생해도 Controller를 수정해야했다. 네 가지 기능의 책임을 모두 한 컨트롤러에 몰아넣었다. 대신 기능 별로 Controller를 나눠 사용함으로써 Post Controller는 등록에 대한 기능만을 담당하도록 하였다. 이를 통해 추후 기능 수정 또는 리팩터링에 용이함을 얻을 수 있으며 코드가 간결해진다.
DTO도 분리했다. 이는 이전 포스팅에서 다뤘기 때문에 생략하겠다. 해당 포스팅에는 적지 못 한 부분이 DTO를 Port의 콘크리트 클래스로 구현하지 않고 인터페이스로 구현하는 것이 맞나에 대한 생각인데, 어짜피 콘크리트 클래스에 의존하는 것이 아니라 가져다 사용하는 형태기 때문에 콘크리트 클래스로 사용했다. 다만 Port가 맞는지에 대한 고민은 있어야겠다. 별도의 DTO 패키지를 만드는 것이 더 나아보이기는 한다.
많은 분들이 Mapper 객체를 사용할 때 직접 domain 엔티티로 전환시키던데, 나는 이렇게 사용하지 않고 dto로 변환시켰다. 이 부분에서 많은 고민이 있었는데, 굳이 모든 영역을 구분하고, Port로만 통신하게 만들어놨으면서 외부 영역에서 domain에 대한 정보를 알고 있다는 점이 너무 마음에 들지 않았다. DTO의 분리는 이를 해결하기 위한 결과물이기도 하다.
@Component
public class FreeBoardMapper {
public FreeBoardEntity mapToDatabaseEntity(SaveFreeBoardQuery saveFreeBoardQuery) {
return FreeBoardEntity.builder()
.memberId(saveFreeBoardQuery.getWriterId())
.title(saveFreeBoardQuery.getTitle())
.content(saveFreeBoardQuery.getContent())
.build();
}
public FreeBoardQueryResult mapToQueryResult(FreeBoardEntity savedFreeBoardEntity) {
return FreeBoardQueryResult.of(savedFreeBoardEntity.getMemberId(),
savedFreeBoardEntity.getId(),
savedFreeBoardEntity.getTitle(),
savedFreeBoardEntity.getContent(),
savedFreeBoardEntity.getCreatedAt(),
savedFreeBoardEntity.getModifiedAt(),
savedFreeBoardEntity.getViewCount());
}
}
이렇게 사용하면 직접적으로 도메인 객체를 다루는 일은 애플리케이션 계층에서만 가능하다는 규칙을 적절히 수행할 수 있다. 또한, 어쩔 수 없이 다른 영역과 Member 와의 의존관계는 생길 수밖에 없는데, 이 과정에서도 애플리케이션 계층만 Member와 연결되어 의존 관계를 최대한 끊을 수 있다.
또한 도메인 객체와 데이터베이스 객체를 구분했다. 도메인 객체는 실제로 도메인 기능을 수행하고, 수행한 결과를 DTO에 담아 Output Adapter에 전송하면 Output Adapter는 이를 데이터베이스 객체로 변환해 저장한다. 말 그대로 데이터베이스 객체는 데이터베이스 객체로서 저장과 삭제, 조회의 역할만 담당하는 것이다.
도메인 객체가 활약하는 순간은 저장이나 삭제, 조회가 아니라 '수정'이다. 이번에는 등록만 리팩터링 한 상황이라 별도로 도메인 객체가 역할을 하지는 않지만, 추후 수정을 리팩터링 하면서 다뤄볼 예정이다.
이번에 헥사고날 아키텍처를 적용하면서, 아마 개발 공부 이후 처음으로 철저하게 영역을 구분하고, 개념적 시스템 아키텍처를 이해할 수 있는 경험을 할 수 있었다. 패키지에 대해서도 좀 고민해볼 여지가 더 많이 남아있는데, 이는 추후 포스팅에서 찾아뵙겠다.
전체 코드는 링크에서 확인할 수 있다.