과거 회사에서 DTO를 사용하지 않고 Entity로 통신을 주고 받는 경우가 있었습니다.. 결국 대참사가 벌어졌고 DTO를 도입하게 되었던적이 있습니다.
대부분의 Entity 클래스들은 대부분 DB 테이블 스키마와 1:1로 매칭된 형태의 구조를 가지고 있기 마련입니다. 특히 JPA를 사용하는 경우는 Entity 클래스 = DB 테이블
이라고 봐도 될 정도로 거의 동일한 구조를 가지게 됩니다.
DTO를 사용해야만 하는 이유는 많지만 몇 가지만 추려보자면
등등이 떠오릅니다..
DTO를 도입함에 있어 몇 가지 고민되는 부분들이 있었습니다.
Entity -> DTO
, DTO -> Entity
변환은 어떻게 할 것인가이에 대한 가장 효율적인 방법은 알지 못하며.. 개인적으로 DTO 도입 시 효율적(?) 이라고 생각했던 방법으로 위 고민들을 해결해보았습니다. (더 좋은 방법이나 의견 댓글 부탁드립니다)
Service와 Repository간에는 Entity를 주고 받는 것이 명확 했는데 Controller 와 Service 간에는 DTO를 주고받을 지 Entity를 주고 받을 지 고민지 많이 되었습니다
그러던 도중 Entity to DTO, DTO to Entity 그리고 ModelMapper 블로그 글을 보고 Controller와 Service간에도 DTO를 주고 받는 것으로 결정하였습니다.
위 방식으로 진행하였지만 아직도 Service -> Controller
로 Entity를 넘기는 것과 DTO를 넘기는 것 중 어떤 것이 더 좋은 방법일지 고민중입니다..
시스템의 규모와 아키텍처 마다 어디까지 DTO와 Entity를 사용하는 것이 효율적인지 다른 것 같습니다. 현재 저도 시스템마다 경계를 다르게 하며 사용하고 있습니다.
Entity와 DTO 간 객체 변환은 어떻게 할 것인지 고민하던 중
ModelMapper
라는 라이브러리를 활용하여 변환하기로 하였습니다.
ModelMapper
을 사용하여 변환하고 Entity에 없는 필드들은 직접 값을 넣어주는 방식으로 변환을 하였습니다.
/* ModelMapper를 통한 변환 */
ModelMapper modelMapper = new ModelMapper();
// 매핑 전략 설정
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
// 변환
final UserBasicInfoDto userBasicInfoDto = modelMapper.map(userEntity, UserBasicInfoDto.class);
변환 작업을 어디서 진행할 지에 대한 고민 끝에 DTO내에 static 매서드를 만들어 변환하기로 하였습니다.
/*DTO 변환 Example*/
public class SampleDto {
private Long id;
private String title;
private String interviewee;
private String company;
...
// Entity -> DTO
public static SampleDto of(Sample sample) {
return modelMapper.map(sample, SampleDto.class);
}
// Entity -> DTO (Page의 경우)
public static Page<SampleDto> of(Page<Sample> sourcePage) {
return sourcePage.map(SampleDto::of);
}
}
DTO로 변환하는 내용은 DTO 내부의 of() 에 모두 넣어두고 사용할 때는 아래처럼 사용하도록 하였습니다.
Sample sample = sampleRepository.findById(id)
.orElseThrow(() -> new NotExistException("Sample", id));
// DTO로 변환하여 리턴
return SampleDto.of(sample);
Entity로의 변환은 static 매서드가 아닌 일반 매서드로 작성하여 객체 자신의 값을 가지고 Entity를 생성하도록 하였습니다.
주로 요청 DTO에서는 toEntity() 매서드를, 응답 매서드에는 of() 매서드를 갖게 됩니다
// DTO -> Entity
public Sample toEntity() {
Sample.builder()
.company(this.company)
.description(this.description)
...
.build();
}
public void saveSample(SampleCreateRequest createRequest, Long managerId) {
// DTO -> Entity
Sample sample = createRequest.toEntity();
sampleRepository.save(sample);
}
모든 요청마다 혹은 모든 응답마다 DTO를 만들어야할까??
같은 필드를 가지는 요청 혹은 응답의 경우 같이 사용해도 되지 않을까??
지금은 내용이 같더라도 수정에 의해 영향을 받지는 않을까??
많은 고민들이 있었고 결국, 웬만하면 요청 응답마다 DTO를 생성하는 방향으로 결론을 내렸습니다.
이렇게 하면 수정이 일어나도 다른 API의 요청이나 응답에 영향이 가지 않는다는 장점이 있지만 클래스가 너무 많아져서 관리가 안될 수도 있다는 단점이 있습니다.
이를 조금이나마 해결하기 위해 DTO 내부에 InnerClass를 만들어 관리하도록 하였습니다.
public class SampleDto {
@Getter
@Setter
public static class Create {
private String content;
@Builder
public Create(String content) {
...
}
}
@Getter
@Setter
public static class Update {
private Long id;
private String content;
...
}
@Getter
@Setter
public static class Response {
private Long id;
private String content;
private String file;
private Long userId;
...
public static Response of(Sample sample) {
final Response dto = modelMapper.map(sample, Response.class);
...
return dto;
}
public static List<Response> of(List<Sample> sampleList) {
return sampleAskList.stream()
.map(Response::of)
.collect(Collectors.toList());
}
}
}
아래와 같이 하나의 DTO 안에 Create, Update, Response 등 DTO를 InnerClass로 둠으로써 하나의 파일로 관리되도록하였습니다.
아직도 DTO에 대해 고민했던 부분들에 대한 명확한 정답은 찾지 못한 것 같습니다.
지금 사용하고 있는 좋은 방식이나 위의 방식들의 단점, 개선 사항들을 댓글로 알려주세요
제가 요즘 하는 고민에 대한 한 가지 솔루션으로 보여서 댓글 남깁니다. 우선 제 생각과 비슷한 어프로치여서 뭔가 확신이 들었는데 그 점에 감사드리고요, 한 가지 마음에 걸리는 것이 있어 의견을 구하고 싶습니다.
"외부 client와 Data를 주고 받는 DTO객체는 반드시 dummy(pure)해야 한다"는 출처모를 인터넷의 중론 때문에 static class, 즉 behavior/logic을 DTO 내부에 두는 것에 굉장히 찜찜한 마음이 드는데요, 이에 대해서 어떻게 생각하시는지요?
+) 2020 11 11 오타수정