개발 일지

왜 엔티티를 반환하지 않고 DTO를 반환하는가?

apple-tree 2024. 10. 23. 18:50

Controller <-> Servcie <-> Repository 이렇게 3개의 레이어로 나눠서 개발하다보면 

 

공통적으로 DTO라는 것을 만들게 된다.

왜 DTO를 만들까? 그리고 Repository 계층에서는 왜 엔티티를 DTO로 반환하는가?

 

처음 배울 당시에는 여러 가지 이유들이 있었다.

- 엔티티에 내용이 그대로 노출하는 것은 필요한 정보 외에 다른 정보가 포함되어 있어 보안적으로 취약하다.

- 엔티티에서는 무분별하게 setter를 사용하는 것이 좋지 않다.

- 엔티티를 그대로 Controller 영역으로 넘기게 되면, Controller가 변경될 때 마다 엔티티에 영향을 미친다.

 

등등 여러가지 이유들이 있다.

 

개인적으로 생각했을 때, 이런 이유들이 틀린 것은 아니지만 실제로 개발을 하면서 어느정도 시간이 흘렀을 때 발생하는 문제라고 생각한다. 즉, 해당 문제는 DTO를 써야하는 이유가 약하다는 느낌을 받았다.

 

조금 더 근본적인 이유를 찾고 싶었다.

여러 자료들과 강의를 통해 찾아본 결과 엔티티를 DTO로 변환하는 이유는 '순환참조' 문제를 해결하기 위해서 작성한다.

 

실제로 엔티티를 그냥 반환값으로 주는 경우에서 순환참조가 발생하는지 확인해보자

 

하나의 게시글이 여러 개의 댓글을 가질 수 있는 일대다 관계를 엔티티로 만들고 양방향으로 조회가 가능하게 메소드도 만들었다. 

@Entity
@Getter
@NoArgsConstructor
@Table(name = "post")
public class Post {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "post_id")
	private long postId;

	@Column(name = "title")
	private String title;

	@Column(name = "content")
	private String content;

	@OneToMany(mappedBy = "post")
	private List<Comment> comments = new ArrayList<>();

	@Builder
	public Post(long postId, String title, String content) {
		this.postId = postId;
		this.title = title;
		this.content = content;
	}

	public void addComment(Comment comment) {
		if (!comments.contains(comment)) {
			comments.add(comment);
			comment.addPost();
		}
	}
}

 

 

@Entity
@Getter
@NoArgsConstructor
@Table(name = "comment")
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private long commentId;

    @Column(name = "content")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @Builder
    public Comment(long commentId, String content) {
       this.commentId = commentId;
       this.content = content;
    }

    public void addPost() {
       this.post = post;
    }
}



이제 이를 호출하고 처리하는 컨트롤러와 서비스를 만들어보자

@RestController
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    // 바로 엔티티 객체를 뿌려주면 연관관계에 있는 객체들끼리 순환참조를 이루게된다. (양방향 연관관계 문제)
    // 한쪽에서 jsonignore해줘야한다
    @GetMapping("/test")
    public List<Post> test() {
       return postService.getPosts();
    }

}

 

 

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public List<Post> getPosts() {
       return postRepository.findAll();
    }
}

 

 

이렇게 되면 

순환참조로 인한 실제 결과값

 

Hibernate: 
    select
        p1_0.post_id,
        p1_0.content,
        p1_0.title 
    from
        post p1_0
Hibernate: 
    select
        c1_0.post_id,
        c1_0.comment_id,
        c1_0.content 
    from
        comment c1_0 
    where
        c1_0.post_id=?
2024-10-23T18:46:12.834+09:00  WARN 6616 --- [np1-practice] [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamWriteConstraints.getMaxNestingDepth()`)
2024-10-23T18:46:12.836+09:00  WARN 6616 --- [np1-practice] [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamWriteConstraints.getMaxNestingDepth()`)]

 

실제로 쿼리도 나가지만, 무엇인가 잘못된 오류가 생긴다.

 

왜 순환참조가 일어나는지에 대해서는 다음편에서 작성해보려고한다.

'개발 일지'의 다른글

  • 현재글 왜 엔티티를 반환하지 않고 DTO를 반환하는가?

관련글