자바 개발자를 위한 온라인 리소스 및 교육 플랫폼 중 "Baeldung" 내에서 제공하는 REST API 관련 Tutorial에 대해 학습해보는 시간을 갖겠습니다.
해당 내용은 전적으로 Baeldung에 기재된 내용을 바탕으로 서술되어 있으며, 관련 내용 중 필자가 부족한 개념은 추가로 포스팅할 예정입니다.
📜 참고 글
이번 포스팅에서는 Spring 애플리케이션 내부 엔티티와 DTO 간에 발생해야 하는 변환을 처리해야 합니다.
일반적으로 Entity와 DTO 간의 변환을 위해 수행할 때, 일반적으로 사용하는 기본 라이브러리인 ModelMapper를 사용하여 변환해보겠습니다.
💡 ModelMapper
관련 의존성을 추가해줍니다.
implementation group: 'org.modelmapper', name: 'modelmapper', version: '3.2.0'
ModelMapper를 Bean으로 등록해줍니다.
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
그 다음으로 매핑할 DTO인 PostDto를 다음과 같이 정의합니다.
public class PostDto {
private static final SimpleDateFormat dateFormat
= new SimpleDateFormat("yyyy-MM-dd HH:mm");
private Long id;
private String title;
private String url;
private String date;
private UserDto user;
public Date getSubmissionDateConverted(String timezone) throws ParseException {
dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
return dateFormat.parse(this.date);
}
public void setSubmissionDate(Date date, String timezone) {
dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
this.date = dateFormat.format(date);
}
// standard getters and setters
}
getSubmissionDateConverted(String timezone) 메서드는 날짜 문자열을 서버의 시간대 날짜로 변환 후 반환해주는 함수입니다.
setSubmissionDate() 메서드는 DTO의 날짜를 현재 client 시간대의 Post 날짜로 설정하는 함수입니다.
엔티티로 변환해준 뒤 처리되는 비지니스 로직 계층의 코드를 보겠습니다.
public List<Post> getPostsList(
int page, int size, String sortDir, String sort) {
PageRequest pageReq
= PageRequest.of(page, size, Sort.Direction.fromString(sortDir), sort);
Page<Post> posts = postRepository
.findByUser(userService.getCurrentUser(), pageReq);
return posts.getContent();
}
그 다음으로 controller 계층의 코드를 보겠습니다.
해당 로직에서는 간단한 CRUD 작업에 대한 정의가 되어있으며, 여기서 중점으로 봐야할 것은 Entity와 DTO 간의 변환입니다.
@Controller
class PostRestController {
@Autowired
private IPostService postService;
@Autowired
private IUserService userService;
@Autowired
private ModelMapper modelMapper;
@GetMapping
@ResponseBody
public List<PostDto> getPosts(...) {
//...
List<Post> posts = postService.getPostsList(page, size, sortDir, sort);
return posts.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public PostDto createPost(@RequestBody PostDto postDto) {
Post post = convertToEntity(postDto);
Post postCreated = postService.createPost(post));
return convertToDto(postCreated);
}
@GetMapping(value = "/{id}")
@ResponseBody
public PostDto getPost(@PathVariable("id") Long id) {
return convertToDto(postService.getPostById(id));
}
@PutMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public void updatePost(@PathVariable("id") Long id, @RequestBody PostDto postDto) {
if(!Objects.equals(id, postDto.getId())){
throw new IllegalArgumentException("IDs don't match");
}
Post post = convertToEntity(postDto);
postService.updatePost(post);
}
}
Post Entity에서 PostDto로 변환은 다음과 같습니다.
private PostDto convertToDto(Post post) {
PostDto postDto = modelMapper.map(post, PostDto.class);
postDto.setSubmissionDate(post.getSubmissionDate(),
userService.getCurrentUser().getPreference().getTimezone());
return postDto;
}
다음은 DTO에서 엔티티로의 변환입니다.
private Post convertToEntity(PostDto postDto) throws ParseException {
Post post = modelMapper.map(postDto, Post.class);
post.setSubmissionDate(postDto.getSubmissionDateConverted(
userService.getCurrentUser().getPreference().getTimezone()));
if (postDto.getId() != null) {
Post oldPost = postService.getPostById(postDto.getId());
post.setRedditID(oldPost.getRedditID());
post.setSent(oldPost.isSent());
}
return post;
}
위의 코드들과 같이 ModelMapper를 사용함에 따라 변환되는 논리가 빠르고 간단해졌습니다. 이를 통해서 별도의 Mapping을 위한 코드를 작성하지 않고 데이터 변환이 빠르게 수행됩니다.
마지막으로 Entity와 DTO 간의 변환이 제대로 작동하는지 확인하기 위해서 매우 간단한 테스트를 수행해보겠습니다.
public class PostDtoUnitTest {
private ModelMapper modelMapper = new ModelMapper();
@Test
public void whenConvertPostEntityToPostDto_thenCorrect() {
Post post = new Post();
post.setId(1L);
post.setTitle(randomAlphabetic(6));
post.setUrl("www.test.com");
PostDto postDto = modelMapper.map(post, PostDto.class);
assertEquals(post.getId(), postDto.getId());
assertEquals(post.getTitle(), postDto.getTitle());
assertEquals(post.getUrl(), postDto.getUrl());
}
@Test
public void whenConvertPostDtoToPostEntity_thenCorrect() {
PostDto postDto = new PostDto();
postDto.setId(1L);
postDto.setTitle(randomAlphabetic(6));
postDto.setUrl("www.test.com");
Post post = modelMapper.map(postDto, Post.class);
assertEquals(postDto.getId(), post.getId());
assertEquals(postDto.getTitle(), post.getTitle());
assertEquals(postDto.getUrl(), post.getUrl());
}
}
해당 포스팅에서는 ModelMapper 라이브러리를 사용하여 간단하게 Entity에서 DTO로 변환하는 방법에 대해서 알아봤습니다.
하지만, 필자의 경우 최근 프로젝트를 구현하면서 라이브러리의 의존도를 조금 줄이고자 ModelMapper, MapStruct 등의 라이브러리를 사용하지 않고 비지니스 로직으로 구현하여 사용했었습니다.
이는 각 개발자마다 기준이 다르므로 방법 중의 하나로 생각해보는 것이 좋을 것 같습니다.