
DTO
- 계층간 데이터 교환을 위한 객체(Beans) 이다.
- 비즈니스 로직을 갖고 있지 않는 순수한 데이터 객체이며, getter/setter 메소드만을 갖는다.
- 자바의 record 클래스로 구현하면 편리하게 구현 가능 (불변성 유지)
- reqeustDTO - 요청을 위한 DTO
- responseDTO - 응답을 위한 DTO
(참고) Controller 계층에서 응답값이 List인 경우, List나 Result< T>로 감싸서 반환해줄 것
-> JSON 형식에 맞춰서 확장성 보장
왜 DTO를 사용하는가?
- 엔티티를 그대로 노출하여 반환한다면, Password와 같은 민감한 정보가 외부로 노출되어 위험할 수 있다.
이를 방지하기 위해 DTO로 별도 응답 필드들을 구성하여, 보안상 노출되면 안될 필드들을 제외해야한다.
- 엔티티를 그대로 반환한다면, API 스펙이 바뀌었을 때, 엔티티에 직접 필드를 추가하거나 삭제해야한다. 엔티티에 필드가 추가/삭제된다면 비즈니스 로직도 수정해야하고, 높은 결합도로 인해 연쇄적인 수정 문제가 발생한다.
하지만, DTO를 이용한다면 API 스펙이 바뀌었을 때, DTO의 필드만 변경하면 되므로 결합도를 낮출 수 있다!
- 프로덕션 환경에서는 성능을 고려해 OSIV를 false로 설정하는 경우가 대부분이므로,
영속성 컨텍스트가 View까지 열려있지 않아 트랜잭션 종료 시점에 영속성 컨텍스트가 닫힌다.
그러므로, 엔티티를 Controller에서 노출한다면, 영속성 컨텍스트가 닫혀있기 때문에 Controller에서 Lazy Loading된 연관 엔티티에 접근할 때 LazyInitializationException 예외가 발생한다.
이러한 이유 때문에 Service계층에서 엔티티를 반환하지 않고, 연관 데이터까지 미리 로딩해 DTO로 변환해 반환하여 Controller에서 DTO로 관련 데이터를 조회하도록 해야한다.
- 양방향 연관관계일 때, 엔티티를 그대로 JSON으로 직렬화하면, User가 UserStatus를 가지고, UserStatus가 User를 가지고.. 순환 참조 무한 루프가 발생할 수 있다.
그러므로, DTO를 사용해서 연관관계 필드를 제외하거나 한 단계만 포함시켜 필요한 정보만 제공한다면, 순환참조를 방지할 수 있다.
이러한 이유로 Controller 단에서 절대 엔티티를 반환하면 안된다! (DTO를 반환)
Entity
- 실제 DB의 테이블과 매칭될 클래스
- 엔티티(Entity)는 단순한 데이터 모델이 아니라, 도메인 로직(비즈니스 로직)도 포함해야 한다!
- 필요한 도메인 로직을 구현
- Setter 메소드 대신 비즈니스 로직적으로 의미있는 메소드 사용 (update)
- 구현된 메소드는 주로 Service 계층에서 사용된다.
- 정보 은닉을 위한 캡슐화가 중요하므로, private 필드 + 접근은 public 메소드 사용
- Getter 메소드는 노출해도 되는 필드에만 구현!
- 주로 DTO로 감싸져서 계층 간 데이터 교환이 이루어진다.
사용 위치
- DTO는 개념적인 객체로, 계층간 데이터 교환이 있다면 DTO를 사용
- Controller는 외부와 통신하는 계층이므로 엔티티가 노출되면 안됨!
- DTO를 사용해 클라이언트 계층과 통신,
- 엔티티 -> DTO 변환 로직이 존재하는 계층
- 서비스는 어떤놈이 사용하던 비즈니스 로직이 바뀌면 안됨! 컨트롤러에서 엔티티를 외부에 맞게 DTO를 이용해 표현을 바꿔주는 역할
- Service는 엔티티를 생성하는 계층, Repository와는 엔티티와 통신해야함
- DTO를 이용해 엔티티에 조작을 가하는 계층
- Controller의 DTO를 사용해서는 안됨 (Controller의 DTO는 외부 통신용), 필요하다면 Service용 CommandDTO 만들기 (클린 아키텍처)
- 엔티티를 생성하는 계층이기 때문에 매개변수로 엔티티를 받으면 절대 안된다. 필드값이나 controller에서 온 requestDTO를 변환한 CommandDTO를 받을 것
- 반환값은 repostiroy로부터 반환된 엔티티를 서비스용 resultDTO로 감싸서 반환하거나(클린 아키텍처), 엔티티 그대로 반환
- Repository는 데이터에 직접 접근하는 계층이므로 엔티티를 통해 DB / Service와 통신
변환 위치 (정답 X)
- 변환 작업 자체는 Entity 클래스나 DTO 클래스에서 구현할 수 있으며, 이 변환 메서드를 실제로 호출하는 시점은 Controller나 Service 계층이 될 수 있습니다.
- 각 계층별 장단점을 고려했을 때
- Controller 계층에서의 변환
: Service 계층을 순수하게 유지할 수 있지만, Entity 구조에 대한 의존성이 생기고 코드 중복이 발생할 수 있습니다.
- Service 계층에서의 변환
: Entity 구조를 캡슐화하고 재사용성을 높일 수 있지만, Service 계층의 책임이 증가한다는 단점이 있습니다.
- DTO 내부 메서드로 변환
: toEntity() / fromEntity() 메서드를 만들어서 변환을 해줄수도 있지만, 사용하더라도 이 메서드의 호출은 Service에서만 되는 것이 좋음!
(엔티티 생성은 Service단에서)
- 별도의 변환 계층 생성 (Mapper, Converter)
: 변환 로직을 완전히 분리할 수 있어서 Controller, Service가 깔끔해지고, 유지보수성이 좋아지지만, 추가적인 클래스/인터페이스가 필요해서 코드가 복잡해질 수 있음.
=> 규모가 크거나 변환 로직이 복잡한 경우에 추천!
=> 결국 변환 작업의 위치 선택은 프로젝트의 특성과 팀의 선호도에 따라 달라질 수 있습니다.
일반적으로 Service 계층에서 DTO ↔ Entity 변환을 수행하는 것이 가장 적절한 선택
- 그럼 Service에선 엔티티를 반환할 경우, Controller에서 엔티티 -> DTO 변환이 필요할텐데?
그걸 방지하기 위해 Service 단에서 DTO로 변환하여 반환하는 것이 좋다! (Service단의 DTO와 Controller단의 DTO를 따로둠, 클린 아키텍처)
- 서비스단에서 DTO를 반환하는 경우, Controller단에서의 API 스펙이 바뀌어 Service단과 Controller단의 스펙이 바뀔 수 있음.
=> Service / Controller단의 DTO를 따로두고, DTO -> DTO 변환을 해줘야함
DTO -> DTO 변환을 컨트롤러에서 하거나, 별도의 변환 계층 (Mapper, Converter를 두면 더 좋다)
중요한 것은 일관된 방식을 유지하고, 각 계층의 책임을 명확히 하는 것입니다. 일반적으로는 Service Layer에서 변환을 수행하는 것이 캡슐화와 재사용성 측면에서 더 나은 선택이 될 수 있습니다.
Controller단에 엔티티를 노출해도 될까?
https://ksh-coding.tistory.com/102
1. Service에서 Response Dto 생성 후 Controller에서 반환
2. Service에서 도메인 자체를 반환하여 Controller에서 Response Dto로 변환 후 사용
=> 엔티티가 Controller단에 노출됨
1번 방법을 적용하는 것이 더 좋음!
why?
Service에서 Response Dto를 생성해서 Controller에 넘기면 도메인 로직이 실행될 가능성이 0%지만, Service에서 도메인을 넘긴다면 휴먼에러로 인해 도메인 로직이 실행될 가능성이 0%가 아니기 때문.
Controller 단의 RequestDTO를 Service단에서 받아도 될까?
https://ksh-coding.tistory.com/102
1. Controller에서 Request Dto를 그대로 Service에 전달하여 사용
View의 요청 정보가 변경되면 Service 계층까지 영향을 줄 수 있음
Controller와 Service가 강한 의존을 가짐
Controller와 Service에서 원하는 포맷이 다를 때, 이를 해결할 방법이 없다.
2. Service Dto를 따로 만들고, Controller에서 Request Dto를 Service Dto로 변환 후 전달하여 사용. 아니면 DTO를 뜯어서 필드로 전달 (파라미터가 적을 때)
=> 위의 문제점들을 전부 해결 가능!
DTO의 핵심은 계층간 데이터 교환!
- Controller <-> 외부 DTO와 Service <-> Controller DTO는 달라야함.

- 클린 아키텍처에서는 설계 원칙 상, Service(Usecase)가 엔티티를 반환하면, 컨트롤러에서 엔티티를 쓰기 때문에 계층을 두단계를 건너뛰게 됨
-> service 전용 DTO를 따로 만들어서 건내줌
- 하지만 이건 설계 원칙상이고, 현업에서는 유연하게 서비스에서 엔티티를 반환하는 경우가 많다.
(repository나 service에선 엔티티를 반환해도됨.)
- 클린아키텍처 이론적 관점에서는 service 입장에서도 엔티티를 반환하고싶지 않아서 command DTO를 따로 만들어서 반환
-> 개발효율이 떨어질 수 있어서 엔티티만 반환해도 상관없다.
우리가 조심해야할 것 => 클린 아키텍처 이론을 다 적용할 수는 없음. 필요한 것만 가져오기!
아키텍처 별로 DTO를 다루는 법이 조금씩 다르다.
클린 아키텍처의 정석

- 클린 아키텍처에서는 의존성이 안으르 흐름!
- 의존성이 안으로 흐른다는 것은, 안쪽에선 바깥쪽에 전혀 관심이 없다는 것
- 내 기능을 쓰려면 Command를 만들어서 서비스 인터페이스쪽한테 넘겨주세요. (CommandDTO, ResultDTO), 하지만 실무에선 엔티티를 넘겨주는 경우도 있긴함! 이게 이상이고, 현실은 다를 수 있음
그러기 위해선 컨트롤러에선 RequestDTO를 CommandDTO로 변환해줘야함
- 서비스에서는 ResultDTO 넘겨줄테니까 controller 니가 알아서 API응답 형식에 맞는 ResponseDTO로 변환해서 써라
그러기 위해선 컨트롤러에서 ResultDTO를 ResponseDTO로 변환해줘야함
- 서비스단에선 CommandDTO를 가지고 엔티티를 조작
CSR 패턴의 경우
- CSR 패턴에서는 컨트롤러에서만 RequestDTO / ResponseDTO로 사용하고
- Service나 Repository에서는 엔티티 이용해서 통신해도된다. (Service단의 반환값도)
단, Service단에서 매개변수로 엔티티를 받지는 말 것 (생성은 Service가 해야하므로)
- 클린 아키텍처로 바꾸고싶을 때, DTO를 도입해서 하나씩 바꿔가기
- 아무튼 이때도 컨트롤러의 DTO를 Service에서 쓰지 않는게 좋다. (외부와 소통하는 용도이기 때문)
- 필드로 받거나, 정말 많은 경우에는 Service용 DTO 생성해서 사용
- Service에서 반환값은 DTO !
(Service에서 반환한 DTO를 Controller에서 사용하는 것은 문제가 될 것 같진 않음, 의존성이 안에서 밖으로 흐르기 때문에, 다만 Controller에서 넘어온 DTO를 Service에서 사용하는 것은 문제가 됨, View에 Service가 의존)
=> 정답은 없지만, Controller단에서 Service로부터 넘겨진 엔티티를 조작하거나 하면 절대 안됨!
엔티티 생성/수정은 Service단에서만 이루어질 것
일단 C-S-R 패턴에서는 서비스에서도 엔티티를 반환하고, 컨트롤러에서만 DTO를 사용해보자!
가장 좋은 방법 정리
1. Entity <-> DTO 변환은 서비스단에서만 이루어질 것
2. Service용 Request, Response DTO / Controller Request, Response DTO 따로 만들어서 관리
단일책임원칙에 따라 개별적인 DTO를 사용하는 것이 일반적이라고 생각합니다. 필드가 완전히 대응되는 특별한 경우에만 중복 사용이 가능하도록 설계하는게 나을 것 같음.
3. DTO <-> DTO 변환은 Controller나 Mapper에서
4. DTO 안에 엔티티 생성은 존재해도 되지만, 엔티티 안에 DTO 생성은 존재하면 안됨
-
Entity ↔ DTO 변환은 Service 단에서만 이루어질 것
- Controller가 Entity를 직접 다루지 않도록 보호 (Entity 구조 캡슐화).
- 비즈니스 로직을 유지하면서도 View의 변경이 Service에 영향을 주지 않음.
- 데이터베이스 변경이 Controller에 영향을 주지 않도록 분리 가능.
-
Service용 Request, Response DTO / Controller Request, Response DTO 따로 관리
- View(Request) DTO가 변경되더라도 Service DTO는 그대로 유지할 수 있음 → 유지보수성 증가.
- Controller와 Service의 역할을 분리하여 의존성을 줄일 수 있음.
- Service 단의 입력과 출력을 명확하게 관리 가능 → 재사용성 증가.
-
DTO ↔ DTO 변환은 Controller에서
- Controller가 View의 데이터를 Service가 이해할 수 있도록 변환 (역할 분리).
- Controller와 Service가 각자 필요한 DTO를 사용할 수 있음.
- Service의 역할을 단순하게 유지할 수 있음 → 비즈니스 로직만 담당.
-
DTO 안에 엔티티 생성은 존재해도 되지만, 엔티티 안에 DTO 생성은 존재하면 안됨
- 엔티티의 생성에 대한 정보의 원천이 DTO에 존재하기 때문
- 엔티티엔 도메인에 관련된 것만 들어가야함 + DTO가 바뀌면 엔티티를 수정해야하므로, 엔티티 안에 DTO 생성이 들어가면 큰일남
- 엔티티 -> DTO 변환은 DTO 내부나, 별도의 Mapper 클래스 생성
이 때, Controller에서 Mapper를 쓰고 있더라도 그거 그대로 사용 가능!
(Mapper는 매퍼 책임만 가지면 되니까, Controller Service 나눌 필요없음)
- 멘토님 의견은 Mapper를 만들면 클래스가 너무 많아지니까 DTO 안에 <-> Entity 변환 메서드 넣어도 좋음
Service DTO와 Controller DTO를 분리하는 이유
요청의 관점
- View(Request) DTO가 변경되더라도 Service DTO는 그대로 유지할 수 있음
→ 유지보수성 증가 + Service가 View에 종속되지 않음 (의존성 방향이 지켜짐)
=> 요청 DTO는 반드시 분리하는 것이 좋다! 아니면 필드만 뽑아서 사용
- 그렇다면 Service의 응답DTO를 Controller의 응답DTO로 그대로 써도 되지 않을까?
-> 써도 된다! Service에 Controller가 의존하는 것은 안전하다.
응답의 관점
- 하지만, 동일한 Service 응답 결과를 서로 다른 Controller 메서드에서 사용해야할수도 있음.
- 이 경우, Controller에서 필요한 응답 API 스펙이 바뀌기 때문에,
Service단에서는 Service단 대로 DTO를 응답하여 Controller에서의 엔티티 노출을 방지하고,
Controller에서는 Controller용 응답 DTO를 따로 생성하고 분리하여,
Service단에서 넘어온 DTO 정보를 재조합해 API 스펙을 맞춰주는 것이 좋다.
하지만...
결론부터 말씀드리면 저는 controller에서 받는 dto를 서비스layer까지 넘겨도 무방하다고 생각합니다.
그 이유는
경험하신 것처럼 유지보수하기가 어렵습니다.
view단에서 사용하는 dto가 변경된다면 일반적으로 구조적인 변경이 클 텐데요.
이런 경우, 제 경험상 서비스 layer의 코드도 동일하게 변경되었습니다.
그렇기에 controller ~ service까지 dto를 자연스럽게 하나의 묶음으로 보셔도 무방하지 않을까 싶습니다.
기본적으로는 Service에서 엔티티를 반환하지 않지만..
- Controller와 통신하는 Service에선 엔티티를 노출하지 말아야한다.
- 하지만, Service 간 통신하는 Service의 경우 엔티티를 노출해야할수도 있음
- 이 경우는 엔티티 노출을 하는 것이 원칙! Controller단에서만 노출하지 않으면 된다.