Spring이랑 백엔드 공부를 하고 팀 프로젝트에서 개발을 시작했다. 내 역할을 시작하기 전에 이것저것 사람들이 개발한 프로젝트의 구성을 몇 개 참고했는데 'DTO'라는게 자꾸 등장했다. DTO 전용 패키지를 빼놓던데 자주 보이던 만큼 알아야할 것 같아서 공부했다.
DTO는 계층 간 데이터 전달만을 위해 존재하는 객체로, 유저와 서비스 간 분리 가능하게 만드는 역할이다.
Entity와 비슷한 구조를 가졌음에도 DTO를 굳이 분리해두는 이유는 Entity는 DB와 맞닿아 있는 핵심 객체로, 예민하게 관리되어야 하기 때문이다. 또한, Controller와 Service 계층 간 강한 의존을 피하게 하기 위해서도 사용된다.
내 프로젝트를 예시로 들자면, 유저로부터 일정 생성을 위한 request 전용 DTO를 백엔드로 받아오면 해당 DTO를 일정 Entity로 변환해서 DB에 저장하게 된다. 이 때 저 중간자 역할이 DTO다.
거의 모든 API에 대해 DTO를 구성하는 것이 서비스와 유저 간 분리를 위해서는 합리적인 구조이다. 그래서 일단 나는 일정의 생성/수정/조회에서 필요한 request DTO 또는 response DTO를 최대한 빼내려고 구성했다.
모든 DTO는 java의 record를 사용해서 간단하게 구축해놨다. (record는 다른 게시글에)
DTO를 막상 만들어놓으니 Entity와 DTO를 어떻게 변환하여 사용 할 지도 고민이다. 일단 알아 본 대표적인 방법은 아래와 같다.
- DTO 내부 메서드로 값 전달하기
- ModelMapper 라이브러리 사용하기
내 프로젝트에서는 Schedule가 DB와 맞닿아 있는 Entity고, ScheduleRequestDTO가 일정 생성을 위한 request DTO로서 존재한다.
DTO 클래스 내에 DTO를 Entity로 변환하는 메서드를 아래와 같이 추가하는 방법이 있다.
// DTO -> Entity 변환
public Schedule toEntity(){
return Schedule.builder()
.title(title)
.locate(locate)
.startDate(startDate)
.endDate(endDate)
.memo(memo)
.build();
}
// Entity -> DTO 변환
public ScheduleRequestDTO toDTO(Schedule schedule){
return ScheduleRequestDTO.builder()
.title(schedule.getTitle())
.locate(schedule.getLocate())
.startDate(schedule.getStartDate())
.endDate(schedule.getEndDate())
.memo(schedule.getMemo())
.build();
}
좀 복잡해보이긴 하지만 그냥 builder 사용해서 새로운 객체로서 만들어주는 메서드이다.
이 라이브러리를 사용하려면 DTO와 Entity 간 필드가 동일해야 한다는 조건이 있다 (아니면 에러 발생함). build.gradle에 라이브러리를 import 해주고 config를 별도로 생성해줘야 한다.
// Entity -> DTO 변환
Schedule schedule = modelMapper.map(scheduleRequestDTO, Schedule.class);
// DTO -> Entity 변환
ScheduleRequestDTO scheduleDTO = modelMapper.map(schedule, ScheduleDTO.class);
간편하긴 정말 간편해보인다!
ModelMapper는 장단점이 아래와 같다.
- 장점
- 코드가 간결
- 일반적으로 필드 변경 사항에 대한 고려가 필요하지 않음
- Lombok 라이브러리와 충돌 없이 같이 사용 가능
- 단점
- 컴파일러에 의해 최적화 되지 않음
- 최초 작업 시 캐싱 되지 않음
- 다른 방식들보다 오버헤드가 많음 -> 성능이 좋지 않음
- 바이트 코드 생성 방식을 사용하므로 문제 원인 발견과 디버깅이 어려움
(특정 필드의 변경으로 인한 매핑 누락 문제 발생 시, 발견 어려움)- Setter를 열어놔야 함
내가 하는 프로젝트에서의 DTO는 일단 1번 방법으로 구축했다.
public record CreateScheduleRequestDTO(@NotNull(message = "일정 제목은 필수 입력 값입니다.") String title,
@NotNull(message = "일정 국가는 필수 입력 값입니다.") Country country,
@NotNull(message = "일정 지역은 필수 입력 값입니다.") City city,
@NotNull(message = "시작 날짜는 필수 입력 값입니다.") LocalDate startDate,
@NotNull(message = "끝 날짜는 필수 입력 값입니다.") LocalDate endDate,
String memo
){
public Schedule toEntity(User user, LocateRepository locateRepository){
Locate locate = locateRepository.findByCountryAndCity(country, city);
return Schedule.builder()
.user(user)
.title(title)
.locate(locate)
.startDate(startDate)
.endDate(endDate)
.readOnlyFlag(false)
.memo(memo)
.build();
}
}
ModelMapper는 장단점을 더 찾아보고 나중에 리팩토링 해봐야겠다.