Architecture

왜 계층간의 모델을 형성해야 하는가?

dev_roach 2022. 3. 7. 10:28
728x90

최근 읽었던 책 만들면서 배우는 클린 아키텍쳐와 그리고 리뷰를 하면서 받았던 질문들 중 "언제 DTO 를 사용해야 하나요? 혹은 왜 계층간 DTO 를 사용해야 하나요?" 라는 질문을 받은 적이 있다. 예전부터 한번쯤 정리해야지 했었는데, 이제는 글로 적어두는게 나을거 같아서 해당 내용을 글로 적어보려고 한다.

코드스쿼드 2021 리뷰 중 질문

일반적인 우리의 서버 아키텍쳐는 아래와 같은 구조를 가지고 있을 것 이다.

일반적인 서버 Architecture

우리는 보통 Repository 는 특정 외부 저장소 혹은 내부 저장소에 질의 하는 방법을 구현하고 있을 것이고, Service 는 우리가 수행해야 할 도메인(비즈니스) 로직을 구현하고 있을 것이다. Controller 계층외부세계에서의 요청을 받고, 내부세계에서 쓰기 좋게 외부세계로 부터 받아온 데이터 모델을 변환(Convert) 하고, 내부 세계에서 처리가 된 데이터 모델을 외부로 전달해 줄 것이다.

 

위의 내용을 토대로 집합 형태를 기반으로 다시 한번 위의 도식을 그려보자.

위의 원 도식을 보면 하나 보이는 점 이 있을텐데 각 계층간에 "선(계층간의 경계)" 이 존재한다는 점 이다.

즉, Controller - Service - Repository 간에는 보이지 않는 계층간의 경계가 존재한다.

또한 한가지 알 수 있는 사실은 이게 정말 중요한데, 사실 Service 의 입장에서 보자면 Controller 또한 외부 영역이라는 것이다.

즉, Controller 가 유저 요청을 처리하듯이 Service 는 Controller 의 요청을 처리한다. 

이 점을 명심하고 보아야 한다. 이게 각 계층간의 데이터 모델을 생성해야 하는 이유를 설명하는 큰 기반지식이 된다. 

 

Controller 를 우리는 "요청 처리 / 요청 응답" 의 역할을 하는 "Representation Layer / Web Layer " 라고 해보자. 

그럼 우리가 아래와 같은 Json 요청을 보낸다면 Controller 에서는 이를 받을 수 있도록 데이터 모델링을 해야할 것이다.

데이터 모델링은 아마도 아래와 같은 형태로 진행될 것이다.

public final class UserJoinRequest {

    private final String name;
    private final Integer age;

    public UserJoinRequest(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    
}

이제 우리는 표현 계층에서 사용할 데이터 모델을 완성했다.

이 모델은 POST "/users" 로 들어오는 요청을 받기 위한 데이터 모델을 생성한 것이다. 

즉 이 모델은 웹 계층에서만 사용할 모델이다.

 

일단 Service 계층을 앞서 도메인 로직을 수행하는 계층이므로 Domain Layer 라고 표현을 하도록 하겠다. 그렇다면 이제 이 Join 을 통해서 우리의 도메인(비즈니스) 로직에서는 회원가입을 진행해야 된다고 해보자. Domain 계층에서는 Controller 에서 name, age 를 넘겨 받는다. 그럼 위에서 설명한대로 Domain Layer 또한 Controller 의 요청을 받아야 한다.

 

그럼 아래와 같이 Domain 데이터 모델을 생성해야 겠다고 생각할 것이다. 왜냐면 외부 요청을 받아주기 위한 데이터 모델이 필요할 것 이기 때문이다.

 

이제 사용자의 회원가입을 처리하는 로직에서는 아래와 같이 해당 도메인 모델을 이용하여 회원가입을 진행할 것이다. 보통은 Service 계층에서 Repository 의 Entity 를 많이 이용한다. 이게 장점일 때도 있지만, 단점일 때도 있는데 가끔은 Entity 가 너무 뚱뚱(Fat) 해지기도 한다. 그 이유는 Entity 에 Domain 로직을 넣으려고 하기 때문이다. Entity 에 DB 테이블과 종속되어 사용되는데 가끔 domain 이라는 package 안에 entity 가 있으면 나는 그것이 정말 Domain Model 인가 라는 의문을 가진다.

public final class UserJoinDomain {

    private final String name;
    private final Integer age;

    private final String STOP_WORD = "_";

    public UserJoinDomain(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public boolean hasStopWord() {
        return name.startsWith(STOP_WORD);
    }

}

예를 들어 회원가입로직에서만 name 에 첫글자가 '_' 로 시작할 경우 금지한다고 해보자.

Entity 를 사용하는 쪽에서는 아래와 같이 코드를 작성할 것이다.

public final class UserEntity {

    private final String name;
    private final Integer age;

    private final String STOP_WORD = "_";

    public UserEntity(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public boolean hasStopWord() {
        return name.startsWith(STOP_WORD);
    }

}

즉 어떻게 보면 회원가입과 관련되서 도메인 계층에 질의해야 할 로직들이 Entity 에 들어가게 된다. 즉 비즈니스 로직이 점점 많아지고 도메인 모델에 협력을 요청해야할 인터페이스가 많아질수록 엔티티는 뚱뚱해질수 밖에 없다. 단순한 로직에 경우 이렇게 하는게 당연히 좋다고 생각하지만, 복잡한 비즈니스 로직을 가지고 있는 모델일 수록 도메인 계층을 분리해야 한다고 주장할 수 밖에 없는 이유다. 그럼 도메인 계층을 이용할 경우 어떻게 될까?

public final class UserJoinDomain {

    private final String name;
    private final String age;

    private final String STOP_WORD = "_";

    public UserJoinDomain(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public boolean hasStopWord() {
        return name.startsWith(STOP_WORD);
    }

}

해당 UseCase(특정 비즈니스 로직) 에서 사용되는 도메인 모델이 해당 비즈니스 로직에서 사용할 모델을 형성하고 있고, 해당 비즈니스 로직에서는 자신이 가지고 있는 모델에만 질의를 한다. 즉 이 의미는 해당 비즈니스 로직에 도메인 질의에 관한 내용을 Domain Model 이 전부 포함하고 있다는 것이다. 즉 Entity 는 hasStopWord 와 관련된 내용을 알 이유가 없다. Entity 는 저장소와 관련된 로직을 처리하기 위한 모델일 뿐이다.

 

그렇다면 회원가입을 한후 저장소에 최종적으로 사용자를 반영해야 한다고 해보자. 여기서 필요한 값은 name, age 이다. 그럼 Repository Layer 또한 어떻게 해야할까? Entity 또한 Service 가 "저장소에 이제 저장해줘" 라는 요청을 받아서 처리해야 한다. 일반적으로 Repository Layer 에서는 Entity 라는 Model 이 있다. 우리는 해당 Model 에 값을 저장한 후 DB 에 반영할 것이다. 따라서 Repository Layer 에서는 아래와 같은 모델이 존재하고 있을 것이다.

public class UserEntity {

    private String name;
    private Integer age;

    public UserEntity(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}

 

위와 같은 모델이 존재하고 있을 것이다. 여기까지 읽었다면 어느정도 각 계층의 데이터 모델을 둬야 하는 이유에 대해 설명이 됬다고 생각한다. 부가적으로 내가 생각하는 점을 설명하자면, 위와 같은 이유로 각 계층이 다른 모듈로 존재할 수 있는 이유라고 생각한다. 사실 어떻게 보면 각 계층 또한 각자의 외부 영역(요청이 들어오는 부분) 이 존재하고, 그에 맞는 데이터 모델링을 진행해야 하기 때문이다.

 

위의 내용을 굳이 따를 필요는 없다고 생각한다. 굳이 Entity 모델을 사용하지 않고, Domain Model 을 만들고 이럴 이유는 없다. 때로는 Entity 모델을 사용하는게 좋을 때도 있다고 생각한다. 다만 위와 같은 방법이 있다는 것을 알고, 왜 각 계층간 모델이 필요한지 잘 모르는 상태에서 Entity 모델을 사용하고 있다면 위와 같은 방법을 사용할 수 도 있다는 사실을 알고 있었으면 한다.

728x90