Data Transfar Object의 약자로, 계층 간 데이터 교환 역할을 합니다.
위와 같이 DB에 저장할 때는 Entity를 저장하지만, 계층간 데이터가 이동할 때는 DTO를 이용하여 데이터를 교환합니다.
DTO는 계층간 데이터 교환만을 위해서 만든 객체이므로 특별한 로직을 가지지 않는 순수한 데이터 객체여야 합니다.
등등.. Entity와 DTO를 분리하는 데에는 여러 이유가 있습니다.
클라이언트에서 User의 정보를 받아 User를 등록하는 API를 만들어보겠습니다.
User.java
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String username;
private String password;
private Integer age;
private String bio;
@Builder
private User(String username, String password, Integer age, String bio) {
this.username = username;
this.password = password;
this.age = age;
this.bio = bio;
}
}
평범한 User Entity입니다. id를 제외하고 Builder 패턴을 적용시켜 놓았습니다.
MemberRegistrationRequest.java
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserRegistrationRequest {
private String username;
private String password;
private Integer age;
public static User toEntity(UserRegistrationRequest request) {
return User.builder()
.username(request.getUsername())
.password(request.getPassword())
.age(request.getAge())
.build()
}
}
사용자 생성 요청 정보를 담을 DTO 클래스입니다.
id 같은 경우는 DB에 저장할 때 자동으로 생성되기 때문에, DTO에 포함하지 않았고, 자기소개(bio)의 경우도 회원 가입 이후에 등록하도록 하였기 때문에 DTO에 포함하지 않았습니다.
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {}
SpringDataJPA를 사용하였습니다.
UserService.java
@Service
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class UserService {
private final UserRepository userRepository;
public void registrationUser(UserRegistrationRequest request) {
User user = UserRegistrationRequest.toEntity(request);
userRepository.save(user);
}
}
사용자의 요청을 담은 DTO를 받아서 User Entity로 변환한 뒤 이를 Repository를 통해 DB에 저장합니다.
UserController.java
@RestController
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<ResultResponse> registration(@RequestBody UserRegistrationRequest request) {
userService.registrationUser(request);
return ResponseEntity.ok(ResultResponse.of(USER_REGISTRATION_SUCCESS));
}
}
사용자 등록 API 입니다. 사용자 등록 요청 DTO를 받아서 이를 service 계층에 전달합니다.
이렇게 사용자 정보를 저장하는 API를 DTO를 사용하여 구현해보았습니다.
DTO를 어느 계층까지 사용할 것인가? (Controller VS Service)
@GetMapping
public ResponseEntity<ResultResponse> getUserInfo(@RequestParam Long userId) {
User user = userService.findById(userId);
Tag userInterestaTag = tagService.findByInterest(user.getInterest());
UserInfoResponse response = UserInfoResponse.toDto(user, userInterestaTag);
return ResponseEntity.ok(ResultResponse.of(response));
}
만약 DTO를 API의 응답 값으로 반환해야하는 경우 Controller에서 Entity를 DTO로 변환하게되면 View에 반환할 필요가 없는 데이터 까지 Controller까지 넘어오게 됩니다. 위의 코드 같은 경우, 보여줄 필요없는 유저의 생성시간, 유저의 비밀번호 등이 Controller까지 넘어오게 됩니다. 또한, Controller가 여러 도메인 객체들의 정보를 조합해서 DTO를 생성해야 하는 경우 Service에서 처리해야 할 비즈니스 로직이 Controller까지 오게됩니다. 위의 코드 같은 경우 UserController에서 tagService까지 사용하게 됩니다. 이로 인해서 하나의 Controller가 여러 Service에 의존하게 될 수 있습니다.@PostMapping
public ResponseEntity<ResultResponse> registration(@RequestBody UserRegistrationRequest request) {
Tag userInterestaTag = tagService.findByInterest(request.getInterest());
User user = new User(request.getUsername(), request.getPassword(), userInterestaTag);
userService.registrationUser(request);
return ResponseEntity.ok(ResultResponse.of(USER_REGISTRATION_SUCCESS));
}
하지만 Controller에서 DTO를 Entity로 변환해야 하는 경우에는 위와 같은 코드처럼 Controller가 여러 Service에 의존하게됩니다.public List<Tag> getTagsByTagInfo(TagInfoRequest tagInfo) {
// 태그의 정보(이름, 분류 등)를 통해 DB에서 조건에 맞는 tag목록을 가져온다.
return tagRepository.findAllByTagInfo(
tagInfo.getName(), tagInfo.getCategory());
}
위와 같은 코드 처럼 Service에서 DTO를 사용하게되면 getTagsByTagInfo 메소드는 TagInfoRequest DTO를 요청으로 받는 TagController 밖에 사용할 수 없게됩니다. 다른 Controller에서 태그의 이름과 분류를 통해서 태그의 목록을 가져오고 싶어도 TagService의 getTagsByTagInfo는 TagInfoRequest DTO를 Parameter로 받기에 재사용이 불가능합니다.→ 결국 정답은 없고, 프로젝트의 규모와 아키텍쳐 등을 고려해서 결정해야 할 것 같습니다.
DTO를 Entity로 변환하는 Mapper 메소드는 어디에 두어야 하는가?
@Component
public class UserMapper {
public CreateUserResponse toDto(User user) {
return CreateUserResponse.builder().userName(user.getUserName()).build();
}
public User toEntity(CreateUserRequest dto) {
User user =
User.builder()
.userName(dto.getStudyName())
.build();
return user;
}
}
이를 해결하기 위해서 Mapper라는 클래스를 만드는 방법이 있습니다. Mapper를 사용하면 DTO와 Entity 사이의 의존관계를 줄일 수 있고, 한 쪽에 수정이 발생해도 Mapper만 수정하면 됩니다.→ 결론은 Mapper 클래스를 따로 만들어서 toEntity, toDto를 넣는 방법으로 결정했습니다.
각 API의 요청, 응답 값을 전부 DTO로 만들어야 하는가?
public class UserDto {
@Getter
@AllArgsConstructor
@Builder
public static class Info {
private int id;
private String name;
private int age;
}
@Getter
@Setter
public static class Request {
private String name;
private int age;
}
@Getter
@AllArgsConstructor
public static class Response {
private Info info;
private int returnCode;
private String returnMessage;
}
}
위와 같이 도메인 별로 관련되는 DTO들을 Class 하나에 묶게되면 클래스의 수도 줄어들고, DTO의 ClassName을 정하는 것도 수월해질 것입니다.