오늘은 아주아주 기본적인 DTO에 관해 말해보겠다.
플젝 처음할 때는 무슨 소린가 했는데, 간단한 예시로 이해를 했다.
DTO(Data Transfer Object)는 계층 간 데이터를 전달하기 위한 객체이다. 주로 Controller - Service - Repository 사이에서 데이터를 정리해서 깔끔하게 주고받을 때 사용된다. 이로써 불필요한 데이터 노출을 막을 수 있다.
한마디로 API 요청마다 필요한 정보만 주고 받는다는 말이다.

여기서 사용자는 화면을 보고있고, 화면(view)는 Controller -> Service -> Repository -> DataBase
이런 계층 구조로 작동 하고있다.
근데 정보를 다 주면 좋은거지, 왜 필요한 data만 주고 받아야 하는건가?
간단한 예시로 살펴보자
예를 들어, 인스타그램에서 특정 프로필을 조회하고자 한다.
이때, 프로필(User)엔티티에는 많은 정보가 있다.

이 외에도 많은 필드가 있지만, 특정 프로필을 조회하는데에는

아이디, 프로필 사진, 이름, 게시물 수, 팔로워, 팔로잉 만 필요하다.
(1번 이유)
또한, 비밀번호, 프로필 세부 설정 등 남에게 보여서는 안되는 민감한 정보도 존재한다.
(2번 이유)
그러기 때문에, 응답을 할 때 User 객체를 다 주는게 아닌, DTO에 특정 정보만 감싸서 보내야 한다.
추가로, 특정 프로필 조회와 달리 인스타그램에서 스토리 형식으로 보여질 때도 프로필 정보가 조회 된다.
이때에, 아이디와 프로필 사진, 스토리 업로드 및 조회 유무만 필요하다.
이처럼 다른 형식의 데이터를 전달하기에 유용하기에 DTO가 사용 된다.

requestDTO는 클라이언트에서 서버로 데이터를 보낼 때 사용하는 객체다. 사용자가 입력한 데이터를 안전하게 서버로 전달할 수 있도록 도와주고, 아까 말한대로 필요한 정보만 쏙쏙 전달 하는 기능을 한다.
responseDTO는 서버에서 클라이언트로 데이터를 보낼 때 사용하는 객체다. API의 응답 데이터를 깔끔하게 정리하고, 불필요한 정보를 제외하여 클라이언트가 원하는 데이터만 받을 수 있도록 한다.
아까와 같은 예시로, 특정 프로필을 조회하는 API가 있다고 하자.

이때에 클라이언트(사용자)는 userId를 서버에게 넘겨줘야한다.
public ProfileResponseDTO getUserProfile(Long userId) {
User user = profileRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
return new ProfileResponseDTO(user.getId(), user.getProfileImage(), user.getFollowerCount(), user.getFollowingCount());
}
그러면 서버는 Controller -> Service -> Repository -> DataBase 를 거쳐서 userId를 찾고,
Service -> Controller 를 거쳐서
아이디, 프로필 사진, 팔로워/팔로잉 등을 클라이언트에게 전달 한다.
public class ProfileResponseDTO {
private Long userId;
private String profileImage;
private int followerCount;
private int followingCount;
public ProfileResponseDTO(Long userId, String profileImage, int followerCount, int followingCount) {
this.userId = userId;
this.profileImage = profileImage;
this.followerCount = followerCount;
this.followingCount = followingCount;
}
public Long getUserId() {
return userId;
}
public String getProfileImage() {
return profileImage;
}
public int getFollowerCount() {
return followerCount;
}
public int getFollowingCount() {
return followingCount;
}
}
컨버터(Converter)는 DTO와 Entity 간 변환을 쉽게 하기 위한 도구이다. 일반적으로 서비스 계층에서 DTO를 Entity로 변환하거나, Entity를 DTO로 변환하는데, 이때 컨버터를 활용하면 코드의 중복을 줄이고 유지보수를 편리하게 할 수 있다. 즉 역할 분리를 위해 클래스를 하나 더 선언해서 쉽게 변환 할 수 있다는 말이다.
코드 예시 :
public class ProfileConverter {
public ProfileResponseDTO convertToDTO(User user) {
return new ProfileResponseDTO(
user.getId(),
user.getProfileImage(),
user.getFollowerCount(),
user.getFollowingCount()
);
}
public User convertToEntity(ProfileResponseDTO dto) {
return new User(
dto.getUserId(),
dto.getProfileImage(),
dto.getFollowerCount(),
dto.getFollowingCount()
);
}
service 코드 :
@Service
public class ProfileService {
private final ProfileRepository profileRepository;
private final ProfileConverter profileConverter;
public ProfileResponseDTO getUserProfile(Long userId) {
User user = profileRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
return profileConverter.convertToDTO(user);
}
}
사실 컨버터의 경우 사용되는 위치는 자유롭게 정해도 된다고 생각한다. 나는 비즈니스 로직을 담당하는 Service 계층에서 빌더 패턴을 주로 사용하는 편이다.
혹은 변환할 필드가 적을 때는 사용하지 않기도 한다.
hoxy record로 안 쓰는 이유가 있으신가용 (레고드만 써서 혹시 레코드 별로인 부분이 있나 하는 순수한 궁금증)