객체 지향 프로그래밍(OOP) 원칙을 적용하여 Repository
에서 가져온 Entity
를 Dto
변환하여 반환하는 방법에 대해 소개하고자 한다.
다음은 Service
코드 중 Choice
라는 선택지를 ChoiceDto
로 변환하는 코드이다.
public ChoiceDetailDto getChoice(Long choiceId) {
Choice choice = choiceRepository.findById(choiceId)
.orElseThrow(() -> new NotFoundException("선택지를 찾을 수 없습니다."));
ChoiceDetailDto choiceDetailDto = new ChoiceDetailDto();
choiceDetailDto.setId(choice.getId());
choiceDetailDto.setTitle(choice.getTitle());
choiceDetailDto.setCount(choice.getCount());
return choiceDetailDto;
}
위의 데이터 변환 로직(Choice -> ChoiceDto
)은 서비스 계층에 위치하고 있어, 비즈니스 로직과 혼재되어 있다. 이는 코드의 가독성을 떨어뜨리고, 재사용성과 유지보수성을 저해한다.
이 문제를 해결하기 위해, Choice
엔티티 내부에 DTO
변환 로직을 추가할 수 있다. 데이터 변환의 책임을 도메인 모델에 할당함으로써 서비스 계층을 더욱 깔끔하게 유지할 수 있다.
// Choice 엔티티 내부
public ChoiceDetailDto toDetailDto() {
ChoiceDetailDto dto = new ChoiceDetailDto();
dto.setId(this.id);
dto.setTitle(this.title);
dto.setCount(this.count);
return dto;
}
public ChoiceDetailDto getChoice(Long choiceId) {
Choice choice = choiceRepository.findById(choiceId)
.orElseThrow(() -> new NotFoundException("선택지를 찾을 수 없습니다."));
return choice.toDetailDto();
}
위의 데이터 변환의 책임을 도메인 모델 즉, 엔티티에 할당함으로써 엔티티는 DTO를 의존하고 있다. 엔티티는 DB와 소통하는 객체이고 DTO는 클라이언트와 소통하는 객체이다. 클라이언트와 소통하는 객체인 DTO는 변경이 자주 일어난다. 이러한 DTO를 엔티티가 의존하고 있으면 DTO의 변경이 있을 때마다 DB와 소통하는 엔티티에까지 영향이 미친다.
서비스 계층에서의 변환
매퍼 클래스 사용
ModelMapper
라이브러리나 MapStruct
와 같은 도구를 사용하여, 엔티티와 DTO 간의 변환 로직을 자동화하고 관리할 수 있다.// MapStruct를 사용한 변환 인터페이스 예시
@Mapper(componentModel = "spring")
public interface ChoiceMapper {
CoiceDTo entityToDto(Choice choice);
}
Dto에 변환 로직을 정의
Dto가 엔티티 객체를 받아 DTO 객체를 생성하는 정적 팩토리 메서드나 생성자를 제공한다. 이는 엔티티와 DTO 간의 변환 로직을 DTO 내부에 캡슐화하여, 서비스 계층이나 다른 외부 변환 메커니즘에 대한 의존성을 줄인다.
이 방법은 DTO가 엔티티에 의존하게 만들어, 때때로 이는 DTO의 재사용성과 분리를 저해할 수 있다. 특히, 다양한 컨텍스트에서 다른 형태의 DTO가 요구될 때, 각기 다른 엔티티 구조를 반영해야 하는 상황에서는 이 방법이 안좋을 수 있다.
public class ChoiceDetailDto {
private Long id;
private String title;
private int count;
// Choice 엔티티를 받는 생성자
public ChoiceDetailDto(Choice choice) {
this.id = choice.getId();
this.title = choice.getTitle();
this.count = choice.getCount();
}
// 정적 팩토리 메서드 사용
public static ChoiceDetailDto fromChoice(Choice choice) {
return new ChoiceDetailDto(choice);
}
}
public ChoiceDetailDto getChoice(Long choiceId) {
return ChoiceDetailDto.fromChoice(choiceRepository.findById(choiceId)
.orElseThrow(() -> new NotFoundException("선택지")));
}
도메인 모델 패턴에서는 엔티티가 비즈니스 로직을 포함한다. 하지만, 엔티티(도메인 모델)과 DTO(Data Transfer Object) 간의 변환 로직은 비즈니스 로직이라기보다는 데이터 변환 로직에 가깝다. 따라서 이러한 변환 로직은 도메인 모델(엔티티)이나 DTO 자체에 포함시키기보다는 별도의 클래스에서 관리하는 것이 바람직하다고 생각한다.
Mapper
클래스를 사용하는 방법은 호불호가 있으며, 관련 내용을 공부하여 나중에 적용해볼 것이다.