소개

8장: 경계 간 매핑하기

늘상 각 계층의 모델을 매핑하는 곳에서는 문제가 발생한다.
매핑에 찬성하는 개발자
두 계층 간에 매핑을 하지 않으면 양 계층에서 같은 모델을 사용해야 하는데 이렇게 하면 두 계층이 강하게 결합됩니다.
매핑에 반대하는 개발자
두 계층 간에 매핑을 하게 되면 보일러플레이트 코드를 많이 만들게 된다. 많은 유스케이스들이 오직 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 계층간 매핑은 과하다.

‘매핑하지 않기’(No Mapping) 전략

포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면 두 계층간의 매핑할 필요가 없어진다.
웹 계층과 애플리케이션에서 모두 Account를 사용해서 두 계층이 같은 모델을 사용하는것이다. 영속성 계층에서도 같은 관계다.
각 계층에서 모델에 대한 특별한 요청이 있을 수 있다.
웹계층에서는 JSON 직렬화를 위한 애너테이션을 사용해야 할 수 있다.
ORM을 사용하기 위해서는 데이터베이스 매핑을 위해 특정 애너테이션이 필요할 수 있다.
Account 클래스가 웹, 애플리케이션, 영속성 계층에서 사용되면 단일 책임 원칙을 위반한다.
모든 계층이 정확히 같은 구조와 정보를 필요로 한다면 ‘매핑하지 않기 전략’은 완벽한 선택지이다.
그러나 애플리케이션 계층, 도메인 계층에서 웹과 영속성 문제를 다루게 되면 곧바로 다른 전략을 취해야한다.

‘양방향’(Two-Way) 매핑 전략

각 계층이 전용 모델을 가진 매핑 전략을 양방향 매핑 전략이라고 한다.
각 어댑터가 전용 모델을 가지고 있어서 해당 모델을 도메인 모델로 도메인 모델을 해당 모델로 매핑할 책임을 가지고 있다.
웹 계층에서는 웹 모델을 인커밍 포트에서 필요한 모델로 매핑하고 인커밍 포트에 의해 반환된 도메인 객체를 다시 웹 모델로 매핑한다.
영속성 계층은 아웃고잉 포트가 사용하는 도메인 모델과 영속성 모델 간의 매핑과 유사한 매핑을 담당한다.
두 계층 모두 양방향으로 매핑하기 때문에 ‘양방향’ 매핑이라고 부른다.
각 계층이 전용 모델을 가지고 있어 각 계층이 전용 모델을 변경하더라도 다른 계층에는 영향이 없다.
웹 모델은 데이터를 최적으로 표현할 수 있는 구조를 가지고 도메인 모델은 유스케이스를 잘 구현할 수 있는 구조를 가진다. 영속성 모델은 데이터베이스에 객체를 저장하기 위해 ORM에서 필요로 하는 구조를 가질 수 있다.

장점

웹이나 영속성 관심사로 오염되지 않는 깨끗한 도메인 모델로 이어진다. 단일 책임 원칙을 만족한다.
‘매핑하지 않기’ 전략 다음으로 간단한 전략이다.
매핑 책임이 명확하다.
바깥쪽 계층/어댑터는 안쪽 계층으로 매핑하고 다시 반대 방향으로 매핑한다. 안쪽 계층의 모델만 알면 되고 매핑대신 도메인 로직에 집중할 수 있다.

단점

너무 많은 보일러플레이트 코드가 생긴다.
매핑 프레임워크를 사용하더라도 시간이 많이 든다.
매핑 프레임워크가 리플렉션을 사용하는 경우 디버깅하기 힘들다.
도메인 모델이 계층 경계를 넘어서 통신하는 데 사용된다.
인커밍 포트와 아웃고잉 포트는 도메인 객체를 입력 파라미터와 반환값으로 사용한다.
도메인 요구가 아닌 바깥쪽 계층의 요구에 따른 변경에 취약해진다.
은총알(silver bullet)이 아니다.

‘완전’(Full) 매핑 전략

각 연산이 전용 모델을 필요로 하기 때문에 웹 어댑터와 애플리케이션 계층 각각이 자신의 전용 모델을 각 연산을 실행하는 데 필요한 모델로 매핑한다.
각 연산마다 별도의 입출력 모델을 사용한다. 계층 경계를 넘어 통신할 때 도메인 모델을 사용하는 대신 SendMoneyUseCase포트의 입력 모델로 동작하는 SendMoneyCommand처럼 각 작업에 특화된 모델을 사용한다.
이 모델을 ‘커맨드(command)’, ‘요청(request)’ 혹은 이와 비슷한 단어로 사용한다.
웹 계층은 입력을 애플리케이션 계층의 커맨드 객체로 매핑할 책임을 가지고 있다.
커맨드 객체는 애플리케이션 계층의 인터페이스를 해석할 여지 없이 명확하게 만들어준다.
각 유스케이스는 전용 필드와 유효성 검증 로직을 가진 전용 커맨드를 가진다. 어떤 필드를 채울지 어떤 필드를 비워두는 게 더 나은지 추측할 필요가 전혀 없다. 값을 비워둘 수 있는 필드를 허용할 경우 현재의 유스케이스에서는 필요없는 유효성 검증이 수행될 수도 있다.
애플리케이션 계층은 커맨드 객체를 유스케이스에 따라 도메인 모델을 변경하기 위해 필요한 무언가로 매핑할 책임을 가진다.
한 계층을 다른 여러 개의 커맨드로 매핑하는 데 하나의 웹 모델과 도메인 모델 간의 매핑보다 더 많은 코드가 필요하다.
여러 유스케이스의 요구사항을 함께 다뤄야하는 매핑에 비해 구현하고 유지보수하기 쉽다.
전역으로 사용하기보다는 웹 계층 → 애플리케이션 계층 사이의 상태 변경 유스케이스의 경계를 명확하게 할 때 빛이난다. 하지만 애플리케이션 → 영속성 사이는 매핑 오버헤드가 발생 할 수 있다.
매핑 전략은 여러가지 섞어써야만 한다. 어떤 매핑 전략도 모든 계층에 전역적일 필요는 없다.

‘단방향’(One-Way) 매핑 전략

동일한 ‘상태’ 인터페이스를 구현하는 도메인 모델과 어댑터 모델을 이용하면 각 계층은 다른 계층으로부터 온 객체를 단방향으로 매핑하기만 하면 된다.
모든 계층의 모델들이 같은 인터페이스를 공유한다.
관련 있는 특성(attribute)에 대한 getter 메서드를 제공해서 도메인 모델의 상태를 캡슐화한다.
도메인 객체를 바깥 계층으로 전달하고 싶으면 매핑 없이 할 수 있다. 인커밍/아웃고잉 포트가 기대하는 대로 상태 인터페이스를 구현하고 있기 때문이다.
도메인 모델은 풍부한 행동을 구현할 수 있고 애플리케이션 계층 내의 서비스에서 이러한 행동에 접근할 수 있다.
바깥 계층에서 상태 인터페이스를 이용할지 전용 모델로 매핑해야할지 결정할 수 있다.
바깥 계층에서 애플리케이션 계층으로 전달하는 객체들도 이 상태 인터페이스를 구현하고 있다. 애플리케이션 계층에서는 이 객체를 실제 도메인 모델로 매핑해서 도메인 모델의 행동에 접근할 수 있게 된다.
팩토리(factory)라는 DDD 개념과 잘 어울린다. DDD 용어인 팩토리는 어떤 특정한 상태로부터 도메인 객체를 재구성할 책임을 가지고 있다.
매핑이 계층을 넘나들며 퍼져있어서 이 전략은 다른 전략에 비해 어렵다.
계층 간의 모델이 비슷할 때 가장 효과적이다.
읽기 전용 연산의 경우 상테 인터페이스가 필요한 모든 정보를 제공하기 때문에 웹 계층에서 전용 모델로 매핑할 필요가 없다.

언제 어떤 매핑 전략을 사용할 것인가?

언제 어떤 전략을 사용할지 결정하려면 팀 내에서 합의할 수 있는 가이드라인을 정해둬야 한다.
가이드라인 예제
변경 유스케이스를 작업하고 있다면 웹 계층과 애플리케이션 계층 사이에서는 유스케이스 간의 결합을 제거하기 위해 ‘완전 매핑’ 전략을 첫 번째 선택지로 택해야한다. 유스케이스별 유효성 검증 규칙이 명확해지고 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다.
변경 유스케이스를 작업하고 있다면 애플리케이션과 영속성 계층 사이에서는 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해서 ‘매핑하지 않기’ 전략을 첫 번째 선택지로 둔다. 하지만 애플리케이션 계층에서 영속성 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꿔서 영속성 문제를 영속성 계층에 가둘 수 있어야 한다.
쿼리 작업을 하면 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해 ‘매핑하지 않기’ 전략이 웹 → 애플리케이션 → 영속성 계층 사이에서 첫 번째 선택지가 되어야한다. 하지만 애플리케이션에서 영속성 문제나 웹 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꾼다.
팀 차원에서 지속적으로 논의하고 수정해야 한다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

인커밍 포트와 아웃고잉 포트는 서로 다른 계층이 어떻게 통신해야 하는지를 정의한다.
각 유스케이스에 대해 좁은 포트를 사용하면 유스케이스마다 다른 매핑전략을 사용할 수 있고 다른 유스케이스에 영향을 미치지 않으면서 코드를 개선할 수 있기 때문에 특정 상황 특정 시점에 최선의 전략을 선택할 수 있다.

Question

어떤 매핑 전략을 선호하는지?
어떤 매핑 프레임워크를 선호하는지?
ModelMapper
MapStruct
직접 코드 구성
리플렉션