[Spring] DTO는 어디서, 어떻게 변환해야 할까?

Jinny·2023년 5월 2일
0

Spring

목록 보기
5/10

0. 🧐 고민의 시작

Spring의 DTO 개념을 처음 접하면서 고민이 생겼다.

  1. DTO, Entity 간의 변환은 어떻게 해야할까?
  2. DTO, Entity 간의 변환은 어느 레이어에서 해야할까?

어떻게 변환해야 하는지에 대해 알아보면서 "양방향 참조", "순환 참조" 등
참조의 방향성 관련 키워드가 많이 등장했고, 이 키워드는 변환 레이어 관련해서 공부할 때까지 쭉 등장했다.

그래서 "DTO는 어디서, 어떻게 변환해야 할까?"에 대한 고민은
"참조 방향"과 "순환 참조"에 대한 이해가 선행되어야 한다고 생각이 들었다.

우선 키워드에 대해 알아보고, 고민을 해결해 보자.


1. 💡 키워드

1.1 🔁 클래스 간 양방향 의존성과 순환 참조

밑에 일부 내용 및 이미지는 조영호님의 우아한테크세미나를 참고하여 작성했다.

양방향 의존성이란?

  • 양방향 의존성은 A가 변경될 때 B가 변경되어야 하고, B가 변경될 때 A가 영향을 받는 다는 것을 의미한다.
  • 이는 하나의 클래스로 봐야할 것을 억지로 찢은 형태이기 때문이다.
  • 양방향 의존성은 성능 이슈를 만들고, 양쪽 객체의 싱크를 맞추기 위한 노력이 필요하다.
  • 또한 메소드 순환 호출을 야기한다.

양방향 의존 예시 코드

  • 파라미터에 해당 타입이 있거나 리턴 타입에 인스턴스가 생성된다면 모두 의존 관계이다.

public class A {
    
    public B toB(B b) {
        return new B();
    }
}

public class B {
    
    public A toA(A a) {
        return new A();
    }
}

순환 참조(메서드 순환 호출)

Spring에 다음과 같은 코드가 있다고 하자.
프로그램이 정상적으로 실행이 될까?

@Component
public class A {
    private B b;

    public A(B b){
        this.b = b;
    }
}

@Component
public class B {
    private A a;

    public B(A a){
        this.a = a;
    }
}
  • A가 생성되기 위해선 B 생성이 필요하고, B가 생성되기 위해선 A의 생성이 필요하다.
  • 따라서 생성자가 순환 호출되면서 스프링 빈이 생성되지 않으며, StackOverFlowError가 발생한다.

💡 참고:

  • 요청과 응답 시 DTO를 사용하는 이유 중 하나가 이런 Entity 간 순환 참조를 방지하기 위함이다.
  • 예시 코드와 같이 생성자 주입 시, 컴파일 타임에 에러를 발견할 수 있지만
    setter와 필드 @Autowired로 주입하는 경우, 정상적으로 스프링 빈이 생성되며 순환 호출되는 런타임 시점이 되어서야 에러를 발견할 수 있다.

1.2 ⬇️ Layer의 참조 방향 (지속 성장 가능한 소프트웨어를 만들기 위한)

Layer 관련 내용은 지속 성장 가능한 소프트웨어를 만들어가는 방법 블로그 글 및 유튜브을 참고하여 작성했다.

지속 성장 가능한 소프트웨어를 만들기 위해서는 통제가 필요하다.
적절한 제약을 통해 레이어를 통제해야 레이어간 유기적으로 상호작용할 수 있기 때문이다.
마치 OOP를 하기 위해 SOLID 원칙을 지키는 것처럼 말이다.
더불어 통제를 통해 레이어를 격리하는 것은 모듈화하기 위한 이유도 있다.

이를 위해 몇가지 규칙을 지켜야 한다.

블로그에 각각 규칙을 왜 지켜야 하는지에 대해서는 자세하게 설명이 나와있지 않아서
어렴풋이 감으로 이해한 부분이 많은 것 같다.
이 부분은 좀 더 경험을 해보면서 추후 밑에 내용을 보완해야 할 것 같다.

[1] 레이어는 상위에서 하위로 순방향으로 참조되어야 한다.

자바 스프링을 하며 제일 많이 보게되는 레이어는 다음과 같다.
레이어를 통제하기 위해서는 위에서 아래로 순방향으로만 참조되어야 한다.

이미지 출처: 지속 성장 가능한 소프트웨어를 만들어가는 방법

[2] 레이어의 참조 방향이 역류되지 않아야 한다.

두 번째 규칙은 프로그래밍을 하면서 자연스럽게 지켜왔던 것 같다. (왜지?)
예를 들면, Spring을 처음 접하더라도 대부분 Controller → Service → Repository 방향으로 참조를 하지
Service가 Controller를 참조하고 있지는 않다.

이미지 출처: 지속 성장 가능한 소프트웨어를 만들어가는 방법

[3] 레이어의 참조가 하위 레이어를 건너뛰지 않아야 한다.

예를 들어, Service에서 많은 Repository를 알고 있다면
Business Layer가 상세 로직과 기술에 대해 자세히 알고 있는 형태가 된다.
구체적으로 알고 있다는 것은 곧 유지보수 및 확장에 좋지 않다는 것이다.

그렇기 때문에 Implement Layer에서 로직을 구체화하고,
Business Layer는 Implement Layer를 통해 비즈니스 로직을 풀어야 위에서 언급한 문제를 방지할 수 있다.

이미지 출처: 지속 성장 가능한 소프트웨어를 만들어가는 방법

[4] 동일 레이어 간에는 서로 참조하지 않아야 한다.

블로그 글을 따르면, 이 규칙을 통해 클래스들의 재사용성을 늘릴 수 있다고 하는데
아직 잘 이해가 되지는 않는다.

레이어 간 참조가 일어나면, 순환 참조 문제가 발생할 수 있지 않을까 생각하긴 했다.

이미지 출처: 지속 성장 가능한 소프트웨어를 만들어가는 방법


2. 🔍 고민 해결

❔ 2.1 DTO, Entity 간의 변환은 어떻게 해야할까?

그럼 첫 번째 고민을 해결해보자.

DTO, Entity를 변환하는 3가지 방법

처음 Spring 프로젝트를 경험해보며 DTO, Entity 변환을 어떻게 해야할지 고민이 많이 되었다.
변환 방법에 대해 찾아보니 대략 3가지 방법이 있었다.

  1. 생성자 혹은 빌더를 통해 변환하는 방법
  2. toXXX, fromXXX 메서드를 통해 변환하는 방법
  3. 별도 DTO, Entity Mapper를 통해 변환하는 방법

개인적으로는 생성자 혹은 빌더로 변환하는 것은 "불호"였다.
거창한 이유는 없었고, 가독성이 떨어지고 레이어에서 휴먼 에러로 필드 매핑 실수가 일어나지 않도록 신경써야 하는 것이 불호의 이유였다.

Mapper는 DTO, Entity간 상호 의존성도 제거하는 좋은 방법이나,
아직 프로그램 규모가 작기 때문에, Mapper를 사용하는 것은 오히려 오버프로그래밍이라고 생각해 고민 대상에서 제외했다.

그래서 toXXX, fromXXX 어떻게 구현해야 하는건데?

그래서 toXXX, fromXXX 을 구현하기로 결정했다.

그럼 여기서 또 문제.. toDTO, fromDTO, toEntity, fromEntity 어떤거 구현할건데?

관련해서 여러 자료를 찾아보다가
요청과 응답으로 엔티티(Entity) 대신 DTO를 사용하자 테코블 글 중에서 이런 내용을 발견했다.

클린 아키텍처 기준 DTO 안에 toEntity, fromEntity를 다 만들어야 한다.
의존성을 한 방향으로 하기 위함이다.
별개 파일에서 toEntity, toDto를 구현할 경우 Entity도 Dto를 의존하고 Dto도 Entity를 의존하는 순환 사이클이 생기게 된다.

(마침 클린 아키텍쳐 책이 있어 찾아보니 해당 내용은 14장에 있다.)

그렇다... 이 내용을 설명하기 위해 서론이 길었다.
의존성을 한방향으로 하고 순환 참조를 방지하기 위해 DTO에 toEntityfromEntity를 구현해야 한다.

+)
개인적인 생각을 추가로 덧붙이자면,
DTO는 Presentation Layer에 속하고 Entity는 Business Layer에 속한다고 생각한다.
따라서 Entity에 toDto를 구현하게 되면 레이어 참조 방향이 역류된다고 생각한다.
더불어 Entity는 도메인을 구현한 고오오급 객체이기 때문에 최대한 외부에 의존하지 않고 독립된 체로 유지되는 것이 좋다고 생각한다.


❔ 2.2 DTO, Entity 간의 변환은 어느 레이어에서 해야할까?

toEntityfromEntity를 드디어 구현했다.
이제 MVC Layer 중 Controller 혹은 Service 레이어 중 어디서 변환해야 할지 고민이다.

물론 프로그래밍에 정답은 없고 프로그램 규모나 상황에 따라 선택하면 된다고 생각한다.

그런데 지금까지 참고한 많은 자료를 보니, 책이나 강의에서 대부분 Service에서 변환해서 그런지
개린이(=필자 같은 개발 어린이)는 대체로 Service에서 변환을 진행했다.
그리고 필자도 그랬지만 Service에서 변환하는 것에 대한 장점을 더 많이 느끼는 것 같기도 하다.

반면 Github 미션 레포지토리에 코드 리뷰어 코멘트를 보거나 현업자 블로그를 보면
Controller에서 변환해야 한다는 주장이 많았다.

아무래도 취준생 개발자와 현직자가 경험한 서비스의 규모가 달라서이지 않을까 하는데...
반대로 말하면 서비스가 커질 수록 Controller에서 변환하는 것이 더 장점이 많다는 것이 아닐까 추측해 본다.

Layer의 참조 방향에 대해 공부하고 오니,
Controller가 Service에 DTO를 전달하고 Service에서 Entity로 변환할 경우 레이어를 역류 참조하는 문제가 발생한다.

2.2.1 Controller가 Service에 DTO를 전달하고 Service에서 Entity로 변환할 경우

문제점1: 순환 참조

얼핏 보면 Controller → Service → DTO 방향으로 의존하며 잘 설계된 것으로 보인다.
하지만 자세히 살펴 보면 이 구조는 레이어 간 상호 참조가 된 것을 알 수 있다.

  • DTO → Controller → Service → DTO
    • Controller, Service 모두 DTO에 의존하고 있음
    • Controller는 DTO 및 Service에 의존하고 있음

상호 참조는 프로그램에 예기치 않는 문제가 발생할 수 있으며,
강한 결합이 발생하여 독립적인 모듈로 재사용하기 어렵게 만든다.

문제점2: 레이어 역류 참조

순환 참조가 발생한 이유는 결국 레이어를 역류하여 참조하였기 때문이라고 생각한다.
아래는 개인적인 생각이다.

  • DTO는 결국에는 View에서 넘어온 코드거나 View로 넘겨줄 코드이다.
  • 즉 DTO는 View와 Controller 사이에 위치한다고 생각하다.

따라서 View에 속한 코드가 Service까지 넘어왔기 때문에 발생한 문제라고 생각이 들었다.


3. 🧹 정리하며

해당 고민을 해결하기 위해 스스로 고민해보고, 자료를 찾아보고 하면서 드는 생각은
사실 어느 레이어에서 변환을 하든 각각의 장, 단점이 있고 선택에 따라 trade-off가 있다는 것이다.

그래서 프로젝트 규모에 따라 상황에 따라 선택하면 될 것 같다는 생각이다.

다만 지금 짬(?)에서는 어떤 상황에 어떤게 더 좋은지 판단할 수 없기 때문에
많은 경험을 해보고 또 블로그 글을 보완해야 할 것 같다.


profile
공부는 마라톤이다. 한꺼번에 많은 것을 하다 지치지 말고 조금씩, 꾸준히, 자주하자.

0개의 댓글