본문 바로가기
  • Let's study
BackEnd/Spring

[Spring] REST API 순환참조 해결하기 - DTO와 Record 활용

by 코딩고수이고파 2026. 8. 12.

1. 양방향 연관관계에서 발생하는 순환참조

JPA에서 엔티티 간 양방향 관계를 사용하면 순환참조 문제가 발생할 수 있다.

예를 들어 Post와 Comment가 다음과 같이 서로를 참조한다고 가정한다.

// Post
@OneToMany(mappedBy = "post")
private List<Comment> comments;
// Comment
@ManyToOne
private Post post;

Post를 JSON으로 변환하면 Post 안의 comments를 조회하게 된다.

그런데 Comment에는 다시 post가 존재한다.

Post
 └─ comments
     └─ Comment
         └─ post
             └─ comments
                 └─ Comment
                     ...

서로를 계속 참조하면서 무한 루프가 발생할 수 있다.

2. DTO를 사용하는 이유

이러한 문제를 해결하는 방법 중 하나가 DTO(Data Transfer Object)를 사용하는 것이다.

엔티티를 그대로 API 응답으로 반환하지 않고, API에 필요한 데이터만 DTO에 담아서 반환한다.

@Getter
public class PostDto {

    private int id;
    private String title;
    private String body;
    private LocalDateTime createDate;
    private LocalDateTime modifyDate;

    public PostDto(Post post) {
        this.id = post.getId();
        this.title = post.getTitle();
        this.body = post.getContent();
        this.createDate = post.getCreateDate();
        this.modifyDate = post.getModifyDate();
    }
}

컨트롤러에서는 엔티티를 DTO로 변환한 후 반환한다.

@GetMapping("/posts")
@ResponseBody
public List<PostDto> list() {

    List<Post> postList = postService.findAll();

    return postList.stream()
            .map(PostDto::new)
            .toList();
}

여기서

.map(PostDto::new)

은 다음 코드와 같은 의미이다.

.map(post -> new PostDto(post))

즉, Post 객체 하나를 PostDto 객체 하나로 변환하는 것이다.

이렇게 하면 엔티티 구조를 외부에 그대로 노출하지 않고 필요한 데이터만 API 응답으로 제공할 수 있다.

또한 API 응답의 필드 이름을 변경해야 할 때도 엔티티를 수정하지 않고 DTO만 수정하면 된다.

DTO에 Setter를 사용하지 않는 이유

응답용 DTO는 일반적으로 조회할 데이터를 전달하는 역할만 하기 때문에 Setter를 제공하지 않는 것이 좋다.

필요한 값은 생성자를 통해 전달하고, @Getter를 통해 조회하도록 구성할 수 있다.

3. Record를 이용한 DTO

Java의 record를 사용하면 데이터를 전달하기 위한 DTO를 더욱 간결하게 작성할 수 있다.

public record PostDto(
        int id,
        String title,
        String body,
        LocalDateTime createDate,
        LocalDateTime modifyDate
) {
    public PostDto(Post post) {
        this(
                post.getId(),
                post.getTitle(),
                post.getContent(),
                post.getCreateDate(),
                post.getModifyDate()
        );
    }
}

Record는 기본적으로 데이터를 변경하지 않는 불변 객체로 사용하기 적합하다.

또한 Getter를 별도로 작성하지 않아도 된다.

postDto.title()
postDto.body()

와 같은 형태로 값을 조회할 수 있다.

댓글