MVC와 헥사고날
일반적으로 규모가 작은 MVC 개발 방식(도메인 중심이 아닌 경우)은 일반적으로 모든 서비스 레이어에서 DTO를 파라미터로 받고 return도 DTO로 합니다.
헥사고날에서는 계층 간의 시스템적인 완벽한 분리를 추구하기 때문에 서비스 레이어에서는 일반적으로 도메인 객체만을 담당하게 됩니다.
비교는 MVC와 헥사고날로 한 것이지만 더 넓게 보자면 SQL 중심의 개발 방식 vs DDD 개발 방식 을 두고 얘기한 것 이기도 합니다.
헥사고날의 코드 구조와 장점
이제 헥사고날 코드를 잠깐 구경하겠습니다.
Member라는 도메인 객체가 있고 해당 객체를 서비스 레이어에서 다루는 코드 입니다.
public class Member {
private Long id;
private String name;
private MemberCategory memberCategory;
}
public class MemberCategory {
private Long id;
private String name;
}
@Service
public class FindMemberApplication implements FindOneMemberUseCase {
@Override
public Optional<Member> findOne(String userId) {
return memberFindOutputPort.findOne(userId);
}
}
findOne이라는 비즈니스 메소드는 Member라는 서비스 레이어에서 도메인 단위로 데이터를 처리하게 됩니다.
그러니까 Entity나 DTO는 접근이 불가능하도록 의도했습니다.(물론 여기서 DTO로 변환 처리의 영역을 서비스가 담당하는 것이라고 볼 수도 있습니다.)
이때는 마치 "서비스 로직에서는 비즈니스만 처리하면 되지, 변환은 필요없잖아."
느낌으로 개발을 하게 됩니다.
그래서 Service에서는 DTO를 리턴하는게 아니라 도메인 객체를 리턴하게 되니 코드가 더 깔끔해집니다.
온전히 비즈니스 로직에만 집중할 수 있는데요.
결과적으로 서비스 레이어는 도메인이 중심이 되기 때문에 헥사고날 아키텍처가 추구하는 Controller, Service, Data Access 계층 간의 완벽한 분리가 가능해집니다.
위와 같은 Service 계층은 다른 곳에 위치시켜도 사용할 수 있는 범용적인 레이어가 된 것 입니다.
헥사고날의 문제
근데 이런식으로 개발을 하다 보면 원하는 데이터를 넘길 수 없는 문제가 생깁니다.
우선 첫번째로 클라이언트에서 백엔드, 두번째로는 백엔드에서 데이터베이스를 생각해보겠습니다.
클라이언트 <-> 백엔드
예를 들어 클라이언트가 받고자 하는 데이터는 어떠한 Entity에 특정 count 정보입니다.
Member라는 도메인이 있으니 Member가 몇명인지를 세는 것이라고 가정해볼게요.
예를 들어 Member Category별로 Member가 몇명인지 구하는 로직이 필요하다고 합시다.
근데 도메인 객체는 그 도메인의 특징만을 가져야 하니까 count를 도메인 객체 안에 넣을 수 없습니다.
그러면 Response 객체가 그 책임을 가져야하는 것인데요.
public class MemberCategoryResponse {
private Long count;
}
위와 같은 형태가 되겠네요.
근데 문제는 Count를 하기 위해는 비즈니스 로직에서 처리하는 것이 맞는데, 비즈니스 로직인 Service는 도메인 객체를 리턴하니까 Response 객체에 접근하는 것이 불가능합니다.
해결 방법
Controller에서 Count를 한다.
이 방법은 비즈니스 로직을 처리하는 과정을 컨트롤러에서 하는 것이니 책임의 문제가 생깁니다.
여기서 중요한 말은 "책임의 분리"입니다.
여태까지 DTO의 변환 과정을 어디서 할지에 대한 고민은 헥사고날 관점에서 시스템의 분리에 가까웠습니다.
근데 시스템의 분리를 지키려고 하니 오히려 책임의 문제가 생기는 현상은 결국에 시스템의 분리와 책임의 분리는 서로 상호 보완적이지 않습니다.
시스템의 분리를 지키고자 책임의 분리를 깨고싶어하는 개발자는 없을 겁니다.
그러므로 이 방식은 좋지 않습니다.
집계 함수용 도메인(MemberAggregate)을 하나 더 만들어서 바로 서비스에서 리턴한다.
두번째는 집계 함수용 도메인을 만드는 것인데요.
DDD 에서는 Aggregate라고 해서 일련의 Entity와 Value Object의 그룹을 의미하는 개념이 있습니다.
여기서 Value Object가 이런 개념이라고 볼 수도 있는데요.
별개의 Value Object를 만들어서 리턴하는 것입니다.
DDD에서 이런 방법으로 해결하는 것이 일종의 관례이기도 합니다.
하지만 뭔가 count 하나 때문에 객체를 만든다는 것은 규모가 작을 경우에 괜히 복잡함이 늘어난 기분입니다.
백엔드 <-> 데이터베이스
클라이언트와 백엔드에서만 발생하는 문제는 데이터베이스와 백엔드에서도 발생합니다.
데이터베이스는 Entity를 리턴해서 서비스 레이어에 Domain으로 변환해서 반환해줘야 합니다.
만약에 Member가 1000명이고 Member가 속한 Category의 종류가 5개라고 가정했을 때 보통 이런 경우는 데이터베이스에서 조인을 통해 한번에 조회하는 것이 좋습니다.
아래와 같이 말이죠.
@Query("SELECT m.name, COUNT(p) FROM Member m LEFT JOIN Post p ON m.id = p.member.id GROUP BY m.id")
List<Object[]> findMemberPostCounts();
근데 우리는 Object라는 Return 객체로는 Domain 객체로 매핑할 수 없겠죠.
그러니까 이것도 마찬가지로 Value Object를 하나 더 만들어서 처리해야합니다.
그게 아니라면 헥사고날에서는 도메인을 반환해야하는 것이 원칙이니 Member를 따로 조회하고 Category를 따로 조회한다음에 각각의 도메인을 조합해서 Count 계산을 해야합니다.
이러한 방식은 헥사고날의 시스템 분리는 지켜낼 수 있지만 속도는 지켜낼 수 없습니다.
결론
결국에 도메인 중심으로 개발하기 위해서는 복잡성이 올라갑니다.
간단한 프로젝트였으면 모든 레이어에서 별개의 DTO를 반환했으면 쉽게 끝날 문제였습니다.
컨트롤러와 레파지토리에 종속되는 서비스냐(SQL 중심 개발) vs 복잡한 구성으로 시스템의 분리도 지킬 것이냐(DDD)
결국 정답은 없겠지만 둘 중 하나를 억지로 선택하라고 한다면 저는 웬만하면 전자의 손을 들겠습니다.
마틴 파울러의 의견은 아래와 같습니다.
A Service Layer defines an application’s boundary [Cockburn PloP] and its set of available operations from the perspective of interfacing client layers. It encapsulates the application’s business logic, controlling transactions and coor-dinating responses in the implementation of its operations.
서비스 계층은 인터페이스 클라이언트 계층의 관점에서 애플리케이션의 경계와 사용 가능한 작업 집합을 정의합니다.
이는 애플리케이션의 비즈니스 로직을 캡슐화하고, 트랜잭션을 제어하고, 운영 구현 시 응답을 조정합니다.
According to Martin Fowler: the Service Layer defines the application's boundary; it encapsulates the domain. In other words, it protects the domain.
Martin Fowler에 따르면 서비스 계층은 애플리케이션의 경계를 정의합니다. 도메인을 캡슐화합니다. 즉, 도메인을 보호합니다.
일단 코드가 간단합니다.
물론 확장성도 없고 유지보수 성도 떨어지는 경우가 있습니다.
규모가 큰 프로젝트라면 DDD가 더 좋은 선택이 될 수 있지만 이 경우엔 학습 곡선이 올라갑니다.
아인슈타인이 이런 말을 했죠.
당신이 알고 있는 것을 당신의 할머니가 이해할 수 있도록 설명하지 못한다면, 당신은 그것을 진정으로 이해한 것이 아닙니다.
우리가 DDD로 확장성 있게 코드를 만들고 시스템을 분리하려는 것은 추후의 유지보수 측면이 제일 강하다고 생각합니다.
역시 아직까지는 제 생각에는 쉽고 단순하지만 유지보수도 쉬울 수 있는 방식이 제일 올바른 방식이 아닐까하는 생각입니다.
'☕ Java' 카테고리의 다른 글
[Java] STW(Stop The World) 유도하기 (0) | 2025.02.11 |
---|---|
[Java] JMH(Java Microbenchmark Harness)를 이용한 성능 테스트 (0) | 2025.01.13 |
[Java] Apache Commons Library에서의 올바른 압축 해제 방법 (0) | 2024.03.15 |
[Kotlin] Companion Object (0) | 2024.02.02 |
Java Stream 사용법 ( 당신의 Stream은 안녕하십니까? ) (0) | 2023.08.26 |