"특정 유저가 작성한 코멘트", 혹은 "특정 레시피에 달린 코멘트"라는 기능을 만들고 싶었다.
생각해보면 구현 자체는 어렵지 않다.
CommentRepository
에서 User author
필드로 Comment를 검색하는 findByAuthor(User author)
메소드를 구현한다.CommentService
에서 리포지토리의 해당 메소드를 호출하여 구성한 뒤 컨트롤러로 넘긴다.@RestController
를 쓴다는 가정하에, JacksonHttpConverter가 동작하여 우리의 class를 리플렉션을 이용해서 응답 JSON body로 내려줄 것이다.
이제 생각해보자. 특정 유저 혹은 특정 레시피를 기준으로 검색을 했다면 아마 코멘트 리스트가 반환되었을 것이다. 그러면 코멘트 리스트가 구성된 응답 body는 어떻게 구성되어야 할까?
bottom up 방식으로 일단 comment 정보를 옮기는 dto에 있어야할 정보를 생각해보자.
userId
, 그리고 유저의 최소 정보인 name
정도일까.recipeId
와 최소 정보인 title
이 필요할 것 같다.content
이다. 1000자 이하의 글자로 표현되는데, 그냥 String으로 욱여넣을거다...까지가 단일 Comment에 들어있어야 할 정보이다. 그리고 이 각각의 Comment들이 하나의 리스트? 배열? (JSON에서 이걸 뭐라 부르는지 모름)로 구성되어 응답 본문의 comments
항목에 담길 것이다.
아래는 내가 특정 유저로 comment를 찾았을 때 응답으로 내려지길 바라는 response body이다.
{
"comments" : [
{
"author" : {
"userId" : 131412415,
"name" : "부추"
},
"recipe" : {
"recipeId" : 585493,
"title" : "맛있는 잡채"
},
"content" : "좀만 간단했으면 좋겠네요^^"
},
{
"author" : {
"userId" : 131412415,
"name" : "부추"
},
"recipe" : {
"recipeId" : 11,
"title" : "돈까스"
},
"content" : "돈까스는 직접 만들어 드세요"
},
{
"author" : {
"userId" : 131412415,
"name" : "부추"
},
"recipe" : {
"recipeId" : 3243,
"title" : "오야꼬동"
},
"content" : "오야꼬동은 사실 무서운 음식이에요... 궁금하면 찾아보시길."
}
]
}
이해가 됐길 바라며.. 이제 DTO를 구성해보자!
SimpleUserDto
, simpleRecipeDto
기본적인 유저 및 레시피 정보를 담기 위한 SimpleUserDto
와 simpleRecipeDto
가 필요하다. 각 엔티티를 구분하기 위한 Id와 name, title을 구성으로 갖고 있다.
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class SimpleUserDto {
private Long id;
private String name;
public static SimpleUserDto fromEntity(User user) {
return SimpleUserDto.builder()
.id(user.getId())
.name(user.getName())
.build();
}
}
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class SimpleRecipeDto {
private Long id;
private String title;
public static SimpleRecipeDto fromEntity(Recipe recipe) {
return SimpleRecipeDto.builder()
.id(recipe.getId())
.title(recipe.getTitle())
.build();
}
}
fromEntity()
를 보자. 인자로 받은 entity에서 필요한 최소한의 정보만을 뽑아 빌더 패턴을 통해 DTO 인스턴스를 구성했다.위의 Simple
이 붙은 DTO를 통해 최소한의 유저와 레시피 정보를 얻었다.
CommentDto
이제 CommentDto
차례이다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentDto {
private SimpleUserDto author;
private SimpleRecipeDto recipe;
private String content;
public static CommentDto fromEntity(Comment comment) {
User author = comment.getAuthor();
Recipe recipe = comment.getRecipe();
return CommentDto.builder()
.author(SimpleUserDto.fromEntity(author))
.recipe(SimpleRecipeDto.fromEntity(recipe))
.content(comment.getContent())
.build();
}
}
어떤가? 내부 필드로 DTO를 뒀다!! @ManyToOne
연관관계로 코멘트의 작성자인 author
, 그리고 코멘트가 달린 레시피인 recipe
의 정보를 내부 DTO로 두었다.
DTO in DTO가 가능했던 것이다. 물론, Jackson converter가 열일한다.
This approach is known as DTO composition and allows you to represent complex data structures and relationships in a structured manner.
검색하면서 찾은건데 이런걸 "DTO composition"이라고 부르기도 한다더라.. 간지나는 이름이다.
CommentListDto
마지막으로, 여러 개의 코멘트를 리스트 형식으로 구성할 일급 클래스를 만들었다. CommentListDto
!!
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class CommentListDto {
private List<CommentDto> comments;
}
이제 컨트롤러와 서비스단에서 어떻게 DTO를 구성하는지, 처음에 말했던 레포지토리 ~ 컨트롤러의 과정을 살펴보자!
Repository
JPA를 사용하든, ES를 사용하든, queryDSL을 사용하든, hibernate를 직접 사용하든.. 어떤 ORM / ODM을 쓰든 레포지토리에 아래 메소드를 구현하자!
List<Comment> findByAuthor(User author);
직관적이다. comment를 작성한 author를 기준으로 Comment 레코드를 List로 반환하는 메소드이다.
Service
@Transactional(readOnly = true)
public List<Comment> getCommentByUserId(Long userId) {
User author = getUserById(userId);
return commentRepository.findByAuthor(author);
}
service 단에서 레포지토리에서 구성한 findByXXX 메소드를 호출한다.
일반적으로 service의 로직 코드에는, 비즈니스적인 로직은 도메인 클래스에 넘기고, 외부의 기능들을 합치는 "facade" 역할을 하는 경우가 많다. 아래는 서비스 메소드가 따르는 일반적인 처리 과정이다.
3번의 결과는 컨트롤러로 넘어가는 것이다. getCommentByUserId()
의 경우 단순한 조회이므로 그냥 레포지토리에서 찾은 결과를 넘겨줄 뿐인 것이다.
Controller
대망의 컨트롤러이다.
@GetMapping("/{userId}/comments")
public ResponseEntity<CommentListDto> getCommentsOfUser(
@PathVariable("userId") Long userId) {
List<Comment> comments = commentService.getCommentByUserId(userId);
CommentListDto commentDtos = CommentListDto.builder()
.comments(comments.stream()
.map(CommentDto::fromEntity)
.toList())
.build();
return ResponseEntity.ok(commentDtos);
}
comments
는 위의 서비스 코드를 통해 받은 List<Comment>
결과가 담긴다.
아래 과정을 통해 CommentListDto
가 형성된다.
comments
스트림을 생성한다.CommentDto.fromEntity()
를 호출하여 comments
리스트의 각 도메인 객체를 CommentDto
로 변환한다.toList()
를 통해 List<CommentDto>
컬렉션을 만든다.CommentListDto
빌더의 인자로 넣어 결과 DTO를 생성한다.위의 과정을 거친 commentDtos
는 응답 객체의 본문이 되어 우리가 원하는 형태대로 사용자에게 반환될 것이다.