[F-Lab 모각코 챌린지 62일차] ResponseBody를 위한 DTO Composition

부추·2023년 8월 1일
0

F-Lab 모각코 챌린지

목록 보기
62/66

# DTO Artist

"특정 유저가 작성한 코멘트", 혹은 "특정 레시피에 달린 코멘트"라는 기능을 만들고 싶었다.

생각해보면 구현 자체는 어렵지 않다.

  1. CommentRepository에서 User author 필드로 Comment를 검색하는 findByAuthor(User author) 메소드를 구현한다.
  2. CommentService에서 리포지토리의 해당 메소드를 호출하여 구성한 뒤 컨트롤러로 넘긴다.
  3. 컨트롤러에서 받은 엔티티를 DTO 인스턴스로 변환한 뒤 ResponseBody로 응답한다.

@RestController를 쓴다는 가정하에, JacksonHttpConverter가 동작하여 우리의 class를 리플렉션을 이용해서 응답 JSON body로 내려줄 것이다.


이제 생각해보자. 특정 유저 혹은 특정 레시피를 기준으로 검색을 했다면 아마 코멘트 리스트가 반환되었을 것이다. 그러면 코멘트 리스트가 구성된 응답 body는 어떻게 구성되어야 할까?

bottom up 방식으로 일단 comment 정보를 옮기는 dto에 있어야할 정보를 생각해보자.

  1. 일단 유저 정보가 있어야 할 것 같다. 기본적인 userId, 그리고 유저의 최소 정보인 name정도일까.
  2. 그리고 어떤 레시피에 달린 코멘트인지 알기 위한 레시피 정보 역시 필요할 것 같다. 유저와 비슷하게 기본적으로 레시피를 구분해주는 recipeId와 최소 정보인 title이 필요할 것 같다.
  3. 그 다음에 오는 것이 실제 Comment의 내용 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를 구성해보자!


1) SimpleUserDto, simpleRecipeDto

기본적인 유저 및 레시피 정보를 담기 위한 SimpleUserDtosimpleRecipeDto가 필요하다. 각 엔티티를 구분하기 위한 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();
    }
}
  • 각 DTO가 가진 필드는 id와 이름 2개 뿐이다.
  • fromEntity()를 보자. 인자로 받은 entity에서 필요한 최소한의 정보만을 뽑아 빌더 패턴을 통해 DTO 인스턴스를 구성했다.

위의 Simple이 붙은 DTO를 통해 최소한의 유저와 레시피 정보를 얻었다.


2) 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"이라고 부르기도 한다더라.. 간지나는 이름이다.


3) CommentListDto

마지막으로, 여러 개의 코멘트를 리스트 형식으로 구성할 일급 클래스를 만들었다. CommentListDto !!

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class CommentListDto {
    private List<CommentDto> comments;
}

WAS에서의 과정

이제 컨트롤러와 서비스단에서 어떻게 DTO를 구성하는지, 처음에 말했던 레포지토리 ~ 컨트롤러의 과정을 살펴보자!

1) Repository

JPA를 사용하든, ES를 사용하든, queryDSL을 사용하든, hibernate를 직접 사용하든.. 어떤 ORM / ODM을 쓰든 레포지토리에 아래 메소드를 구현하자!

List<Comment> findByAuthor(User author);

직관적이다. comment를 작성한 author를 기준으로 Comment 레코드를 List로 반환하는 메소드이다.


2) Service

@Transactional(readOnly = true)
public List<Comment> getCommentByUserId(Long userId) {
    User author = getUserById(userId);
    return commentRepository.findByAuthor(author);
}

service 단에서 레포지토리에서 구성한 findByXXX 메소드를 호출한다.

일반적으로 service의 로직 코드에는, 비즈니스적인 로직은 도메인 클래스에 넘기고, 외부의 기능들을 합치는 "facade" 역할을 하는 경우가 많다. 아래는 서비스 메소드가 따르는 일반적인 처리 과정이다.

  1. repository를 통해 도메인 객체를 찾는다.
  2. 도메인 객체의 비즈니스 로직을 호출한다.
  3. 과정중 트랜잭션 처리를 하고, 결과를 반환한다.

3번의 결과는 컨트롤러로 넘어가는 것이다. getCommentByUserId()의 경우 단순한 조회이므로 그냥 레포지토리에서 찾은 결과를 넘겨줄 뿐인 것이다.


3) 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가 형성된다.

    1. comments 스트림을 생성한다.
    2. CommentDto.fromEntity()를 호출하여 comments 리스트의 각 도메인 객체를 CommentDto로 변환한다.
    3. toList()를 통해 List<CommentDto> 컬렉션을 만든다.
    4. 3번을 통해 만들어진 결과를 CommentListDto 빌더의 인자로 넣어 결과 DTO를 생성한다.

위의 과정을 거친 commentDtos는 응답 객체의 본문이 되어 우리가 원하는 형태대로 사용자에게 반환될 것이다.

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글