8 분 소요

만들면서 배우는 클린 아키텍쳐 8장 정리

경계 간 매핑하기

경계 간 매핑을 하는 것에 대해 찬반의견이 분분합니다.

매핑에 찬성하는 개발자들은 두 계층 간에 매핑을 하지 않으면 양 계층에서 같은 모델을 사용해야 해서 두 계층이 강하게 결합되기 때문에 매핑을 통해 이 결합도를 낮춰야 한다고 주장합니다.

반면에 매핑에 반대하는 개발자들은 두 계층 간에 매핑을 하게 되면 보일러플레이트 코드를 너무 많이 만들게 되어서 많은 유스케이스들이 오직 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 계층 사이의 매핑은 과하다고 주장합니다.

보일러플레이트

Boilerplate(보일러플레이트)는 1890년대 신문에서 광고나 컬럼과 같이 계속 사용되는 텍스트를 강철로 만든 인쇄판을 의미합니다.

프로그래밍에서 사용되는 Boilerplate 코드 역시 최소한의 변경으로 계속하여 재사용할 수 있으며 반복적으로 비슷한 형태를 가지는 코드를 의미합니다.

그런 의미에서 Boilerplate는 특정한 형식이 지정되어 있는 템플릿(이력서 등)과 유사한 점이 많습니다.

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

포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면 두 계층 간에 매핑을 할 필요가 없습니다.

책의 ‘송금하기’예시를 들어봅시다.

웹 계층의 웹 컨트롤러(SendMoneyController)가 SendMoneyUseCase 인터페이스의 구현체를 멤버로 가지고 있고, 이 구현체를 통해 유스케이스를 실행합니다.

SendMoneyUseCase는 메서드를 호출할 때 Account 객체를 인자로 가집니다.

즉, 웹 계층(컨트롤러)과 애플리케이션(유스케이스) 계층 둘 다 Account 클래스에 접근해야 하고, 이는 두 계 층이 같은 모델을 사용한다는 것을 의미합니다.

영속성 계층과 애플리케이션 계층에서도 같은 일이 발생합니다.

이렇게 되면 모든 계층이 같은 도메인 엔티티 모델을 사용하기 때문에 계층 간에 매핑을 전혀 할 필요없이 그대로 주고 받으면 됩니다.

문제점

웹 계층과 영속성 계층은 모델에 데헤 특별한 요구사항이 있을 수 있습니다.

예들 들어, 웹 계층에서는 REST로 모델을 노츨시킨 것을 JSON으로 직렬화하기 위한 애너테이션이 모델 클래스의 특정 필드에 필요할 수 있습니다.

영속성 계층에서는 ORM 프레임워크를 사용한다면 id나 entity 매핑과 같은 데이터베이스 매핑을 위해 특정 애너테이션이 필요합니다.

도메인 주도 설계시 도메인 엔티티와 애플리케이션 계층은 웹이나 영속성과 상관없이 설계하고 코드를 짜야 하는데 매핑하지 않기 전략을 사용하면 어쩔 수 없이 도메인 엔티티와 애플리케이션 계층의 코드가 웹이나 영속성과 관련된 코드로 오염될 수 밖에 없습니다.

또한 이렇게 오염되면 도메인 엔티티 모델 클래스는 웹 계층, 애플리케이션 계층, 영속성 계층과 관련된 모든 요구사항을 다뤄야 하고 이는 단일 책임 원칙을 위반하게 됩니다.

단일 책임(쉽게 말하면 코드를 변경해야 하는 이유)원칙의 위반으로 인해 도메인 엔티티 모델 클래스는 웹 계층, 애플리케이션 계층, 영속성 계층의 코드가 변경되면 이와 관련해서 같이 코드를 변경해줘야 하는 상황이 발생합니다.

또한 상황에 따라서는 도메인 엔티티 클래스에 웹이나 영속성 계층에서만 필요로 하는 커스텀 필드를 둬야하는 경우도 생길 수 있습니다.

한 계층에서만 사용되는 필드가 아무 의미없이 다른 계층에서도 같이 전달되게 되기 때문에 이는 앞 장에서 로버트 마틴이 이야기한 표현(필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 생길 수 있다.)에 위배되는 사항입니다.

매핑하지 않기 전략을 사용하는 상황

간단한 CRUD 유스케이스에서는 굳이 웹 모델을 도메인 모델로 또는 도메인 모델을 영속성 모델로 매핑할 필요가 없습니다.

또한 웹이나 영속성 계층의 변경에 따라 도메인 모델에 추가한 JSON이나 ORM 애너태이션을 약간 변경하는 정도는 용납할 수 있습니다.(왜냐하면 어차피 모든 전략은 장단점이 있고 완벽할 수 없기 때문에…)

모든 계층이 정확히 같은 구조이고 정확히 같은 정보를 필요로 한다면 매핑하지 않기 전략은 완벽한 선택지입니다.

하지만 매핑하지 않기 전략을 통해 전 계층에서 동일 모델을 이용함에 따라 애플리케이션이나 도메인 계층에서 웹이나 영속성과 관련된 문제를 처리하게 된다면 이 전략은 수정해야할 필요가 있습니다.

그래서 보통은 많은 유스케이스들이 간단한 CRUD로 시작하여 모든 계층에서 단일 모델을 쓰다가 시간이 지남에 따라 프로그램이 복잡해지면서 시간들 들여 계층마다 필요한 모델을 만들어 이를 매핑 해야하는 유스케이스로 바꿔갑니다.

만약에 이런 일이 일어나지 않는다면 매핑을 위한 시간을 쓰지 않았기 때문에 그 역시 좋은 일입니다.

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

각 계층은 도메인 모델과는 완전히 다른 구조의 전용 모델을 가진 매핑 전략입니다.

각 어댑터가 전용 모델을 가지고 있어서 해당 모델을 도메인 모델로, 도메인 모델을 해당 모델로 매핑해야 합니다.

구체적으로 설명하면 웹 계층에서는 웹 모델을 인커밍 포트(유스케이스 인터페이스)에서 필요한 도메인 모델로 매핑하고, 도메인과 애플리케이션에서 처리된 후 반환된 도메인 객체를 다시 웹 모델로 매핑해야 합니다.

이는 영속성 계층에서도 똑같습니다.

이처럼 input할 때와 output할 때 모두 양방향으로 매핑해야 하기 때문에 양방향 매핑이라고 부릅니다.

장점

각 계층이 전용 모델을 가지고 있기 때문에 도메인 엔티티 모델은 다른 계층에 영향을 받지 않아 코드가 오염되지 않으며(JSON이나 ORM 관련 애너테이션도 필요없음), 한 계층이 전용 모델을 변경하더라도 다른 계층은 영향을 받지 않습니다(단일 책임(변경 이유) 원칙).

이렇게 되면 웹 모델은 데이터 전송을 위한 최적의 구조를 가질 수 있고, 도메인 모델은 유스케이스를 제일 잘 구현할 수 있는 구조, 영속성 모델은 데이터베이스에 객체를 저장하기 위해 ORM에서 필요로 하는 구조를 가질 수 있습니다.

또한 개념적으로는 매핑하지 않기 전략 다음으로 간단한 전략입니다.

왜냐하면 모든 계층이 전용 모델을 가지고 있기 때문에 매핑을 누가 해야하는지에 대한 책임이 명확하기 때문입니다.

단점

첫째, 앞에서 잠깐 언급한 보일러플레이트 코드가 너무 많이 생기고 이로 인해 시간이 많이 듭니다.

이러한 코드양과 시간을 줄이기 위해 매핑 프레임워크를 사용하더라도 여전히 꽤 많은 시간이 듭니다.

특히 매핑 프레임워크(JPA를 말하는 것인가?)를 사용할 때 내부 동작 방식을 제네릭 코드와 리플렉션 뒤로 숨길 경우 매핑 로직을 디버깅하는 것이 매우 힘듭니다.

둘째, 도메인 모델이 계층 경계를 넘어서 통신(인커밍 포트와 아웃고잉 포트가 메서드에서 도메인 객체를 입력 받거나 반환)하는 데 사용되고 있다는 것이 문제입니다.

도메인 모델은 도메인 모델의 필요에 의해서만 변경되는 것이 이상적이지만 바깥 계층의 메서드 반환값이나 매개변수로 사용하다보면 바깥쪽 계층의 요구에 따라 변경에 취약해질수 있기 때문입니다.

결국에 매핑하지 않기 전략과 마찬가지로 양방향 매핑 전략도 완벽하지 않습니다.

그렇기 때문에 양방향 매핑 전략을 당연히 써야 한다고 생각할 필요없이 간단한 CRUD 유스케이스에서는 이를 안써서 시간을 절약하는 것이 더 좋습니다.

즉, 어떤 매핑 전략도 완벽하지 않기 때문에 각 유스케이스의 상황에 맞게 적절한 전략을 선택할 수 있어야 합니다.

완전(Full) 매핑 전략

이 전략에서는 각 연산마다 별도의 입출력 모델을 사용하기 때문에 계층 경계를 넘어 통신할 때 아까 양방향 매핑의 단점으로 언급된 바깥 계층의 메서드에 도메인 모델을 매개변수나 반환값으로 사용하는 것 대신에 각 유스케이스 작업에 특화된 모델을 사용합니다.

즉, 책에서 든 예시인 SendMoneyUseCase의 경우에는 SendMoneyCommand라는 별도의 입력 모델을 두고 Account 대신에 SendMoneyCommand를 메서드의 매개변수로 사용합니다.

이러한 입력 모델은 주로 커맨드(command)나 요청(request)라는 단어를 사용해 이름을 짓습니다.

웹 계층은 유스케이스에서 사용하는 입력 모델인 SendMoneyCommand와는 별도의 입력 모델을 두어서 웹에서 클라이언트로부터 받은 입력을 애플리케이션 계층은 커맨드(SendMoneyCommand) 객체로 매핑해줘야 합니다.

유스케이스는 필드와 유효성 검증을 공유하는 하나의 큰 입력 모델을 두지 않고, 각 유스케이스마다 필요한 전용 필드와 유효성 검증 로직을 가진 세분화된 전용 커맨드를 가집니다.

장점

이처럼 각 유스케이스마다 전용 입력 모델을 가지게 되면 하나의 큰 입력 모델을 공유하면 발생할 수 있는 문제점(한 유스케이스에서는 불필요하지만 다른 유스케이스에서 필요하기 때문에 불필요한 필드나 유효성 검증 등)을 없앨 수 있습니다.

애플리케이션 계층은 커맨드 객체를 각각의 유스케이스에 맞게 필요한 것으로 매핑하여 도메인 모델을 변경합니다.

이렇게 각 유스케이스마다 입력 모델을 만들어서 매핑하면 당연히 하나의 넓은 입력 모델을 만들어서 매핑하는 것보다는 코드 양이 많습니다.

하지만 그럼에도 불구하고 각 유스케이스마다 입력 모델을 만들어서 매핑하면 여러 유스케이스의 요구사항을 함께 다루는 넓은 입력 모델에서의 매핑보다 구현이나 유지보수하기가 수월합니다.

사용시 유의사항

완전 매핑 전략을 모든 계층에서 사용하는 것은 추천하지 않습니다.

이 전략은 웹 계층(컨트롤러, 인커밍 어댑터)과 애플리케이션 계층(유스케이스)사이에서 상태 변경 유스케이스의 경계를 명확하게 할 때 사용하기 가장 좋습니다.

반면에 애플리케이션 계층과 영속성 계층 사이에서는 매핑 오버 헤드때문에 완전 매핑 전략을 사용하지 않은 것이 좋습니다.

또한 연산의 입력 모델에 대해서만 이 매핑을 사용하고, 출력할 때는 도메인 객체를 그대로 출력 모델로 사용하는 것처럼 상황에 따라서는 부분적으로만 이 전략을 사용하는 방법도 좋습니다.

이처럼 한 가지 매핑 전략으로 모든 계층에 걸쳐서 사용하는 것보다는 여러 매핑 전략을 섞어쓰는 것이 훨씬 좋습니다.

즉, 한 가지 전략만 사용하는 것보다는 상황에 맞게 그 때 마다 다른 전략을 사용하여 여러 전략을 섞어서 써야 합니다.

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

이 전략을 이용하면 모든 계층의 모델들은 하나의 같은 인터페이스를 구현합니다.

이 인터페이스는 관련 있는 특성에 대한 getter 메서드를 제공하여 도메인 모델의 상태를 캡슐화합니다.

이 전략 하에서는 도메인 객체를 바깥 계층으로 전달할 때 따로 매핑할 필요가 없습니다.

왜냐하면 도메인 객체나 인커밍 아웃고잉 포트가 같은 인터페이스를 구현하고 있기 때문에 다형성으로 인해 어떤 계층에서든 사용할 수 있기 때문입니다.

그러면 바깥 계층에서는 애플리케이션 계층으로부터 전달받은 이 인터페이스를 그대로 사용할지 아니면 이를 바탕으로 전용 모델로 매핑을 할지도 결정할 수 있습니다.

행동을 변경하는 것은 인터페이스에 노출되어 있지 않기 때문에 실수로 도메인 객체의 상태를 변경하는 일은 발생하지 않는다.(인터페이스에 명시되지 않은 메소드는 인터페이스 자료형일 경우 사용할 수 없다는 의미인가? 잘모르겠다 아직)

반대로 바깥 계층에서 이 인터페이스를 애플리케이션 계층으로 전달할 때도 마찬가지입니다.

애플리케이션 계층에서 이 인터페이스를 구현한 구현체를 이용해 실제 도메인 모델로 매핑해서 도메인 모델의 행동에 접근할 수 있습니다.(인터페이스 자료형일 경우 도메인 구현체에만 구현된 메서드를 호출할 수 없기 때문에)

이 매핑은 어떤 특정한 상태로부터 도메인 객체를 재구성하는 역할을 하는 팩토리(factory)라는 DDD 개념과 잘어울립니다.

이 전략을 사용하면 매핑 책임은 명확해집니다.

한 계층이 다른 계층으로부터 인터페이스 구현체를 받으면 해당 계층에서는 이를 이용할 수 있도록 다른 무언가로 매핑하는 것입니다.(즉, 인터페이스 구현체를 받았을 때, 이 구현체의 자료형이 인터페이스이기 때문에 인터페이스에 정의된 메서드밖에 호출하기 못하기 때문에 자신의 계층에 맞는 클래스(공통 인터페이스를 구현하고 있는)의 자료형으로 변경해야만 원하는 메서드에 접근할 수 있습니다.)

이렇게 되면 모든 계층은 한 방향으로만 매핑하게 되고, 그래서 이 전략의 이름이 단방향 매핑 전략입니다.

유의사항

하지만 이 매핑을 설계하는 것은 다른 전략에 비해 개념적으로 어렵습니다.

이 전략은 계층 간 모델이 서로 비슷할 때 사용하면 가장 좋습니다.

예를 들어 읽기 전용 유스케이스의 경우 인터페이스의 구현체가 필요한 모든 정보를 제공하기 때문에 웹 계층에서 전용 모델로 매핑할 필요가 없습니다.

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

그때 그때 다르다.

각 매핑 전략마다 장단점을 가지고 있기 때문에 하나의 전략을 모든 코드에 적용하기 보다는(그게 전략을 설계하는 입장에서는 간편해서 좋지만) 여러 상황을 고려하여 그때 그때마다 최선의 전략을 선택하는 것이(여러 전략을 섞어서 사용하는 것이) 좋습니다.

또한 소프트웨어는 시간이 지남에 따라 변하기 때문에 과거에 좋았던 전략이 현재는 아닐 수 있습니다.

그래서 계속해서 같은 매핑 전략을 고수하기 보다는 일단 빨리 구현할 수 있는 간단한 전략으로 시작해서 나중에 복잡한 전략으로 리팩토링하는 것도 좋은 방법입니다.

이렇게 하기 위해서는 팀 내에서 합의할 수 있는 가이드라인이 있어야 합니다.

이 가이드라인은 어떤 상황에스는 어떤 매핑 전략을 사용할지에 대한 답과 그 이유를 적어놓은 일종의 지침서 역할을 수행해야 합니다.

나중에 시간이 흘렀을 때 이러한 지첨서가 있어야 이를 바탕으로 시간이 흐른 후에도 이 상황에서 이 전략이 여전히 최선인지 평가할 수 있는 근거가 됩니다.

예시를 들어보면 웹과 애플리케이션 계층 사이에서는 각 유스케이스간의 결합을 제거하기 위해 완전 매핑 전략을 사용하는 것이 좋습니다.

이렇게 되면 각각의 유스케이스마다 전용 입력 모델이 생기기 때문에 불필요한 필드나 유효성 검증이 사라지기 때문입니다.

즉, 변경 유스케이스와 읽기 전용 유스케이스가 필요한 정보나 유효성 검증이 엄연히 다르기 떄문에 각 입력 모델은 각자 필요한 필드만 가지면 되고, 필요한 유효성 검증만 하면 됩니다.

반대로 애플리케이션과 영속성 계층 사이에서는 매핑하지 않기 전략을 사용해 매핑 오버헤드를 줄여서 빠르게 코드를 짜는 것이 더 좋습니다.

하지만 만약에 애플리케이션 계층이 영속성 문제를 다뤄야 한다면 양방향 매핑 전략으로 바꾸는 것이 더 바람직 합니다.

이러한 가이드라인을 성공적으로 적용하려면 개발자들 사이에 충분한 합의와 지속적인 논의, 그리고 이를 통한 업데이트가 필요합니다.

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

계층 사이에서 연결해주는 역할을 하는 인커밍 포트와 아웃고잉 포트는 서로 다른 계층 사이에 어떻게 데이터를 이동시킬지를 정의하는데 여기에 계층 사이에 매핑을 수행 여부, 만약 수행한다면 어떤 매핑 전략을 사용할지를 정합니다.

각 유스케이스마다 좁은 포트, 즉, 세분화된 유스케이스(포트 인터페이스)를 사용하면 각 유스케이스마다 다른 매핑 전략을 사용할 수 있습니다.

이렇게 되면 유스케이스간에 서로 영향을 받지 않고 리팩토링할 수 있기 때문에 어떤 상황에서는 이것이 최선의 방법이 될 수 있습니다.

이렇게 상황을 고려하여 여러 가지 매핑 전략을 선택하고 섞어서 사용하는 것은 한 가지 전략을 사용하는 것보다 분명히 더 어렵고 합의를 보기 위해 시간이 필요하지만 매핑 가이드라인을 계속해서 업데이트 하는 한, 결국에는 더 유지보수하기 편한 아키텍처가 되어 이러한 단점들을 상쇄해줄 것입니다.

댓글남기기