[12.21] 내일배움캠프[Spring] TIL-36
1.기존의 Spring 숙련 과제 기능 추가하기
- 현재 직급에 따른 게시글의 수정과 삭제 작업 까지는 완료 했고, 댓글 작업을 진행중이다...
연관관계
- 나는 게시글 < -> 댓글의 관계를 댓글에서
BOARD_ID
만 들고 있어도 판별이 가능할 것이라고 생각해서
Comment
엔티티에 Board
엔티티를 단방향으로 @ManyToOne
으로 매핑 시켰었다.
- 하지만 하나의 게시글안에 여러 댓글이 있고, 게시글을 통해 댓글을 보는 시각으로 봤을 때는
@OneToMany
의 연관 관계도 필요할 것 같아 추가했다.
양방향 연관관계 추가 후 순환참조 오류의 발생
Comment
의 삽입과 조회 Service
에서 작업 후 그 Comment
엔티티를 그대로 반환하게 되면,
현재 Comment
엔티티는 Board
엔티티와 양방향 연관관계 이기 때문에 순환 참조의 StackOverFlow
가 발생한다.

- 서로 양방향 연관관계 이기 때문에
Post -> Comment -> Post -> Comment …
식으로 반복된다...
- 사실 순환참조의 오류는 양방향 연관관계라고 말할 수도 있지만, 엄밀히 따지고 보면 Json타입으로 반환할 때
( @RestController
로 인해 ) 직렬화를 하는 과정에서 위와 같은 순환 참조가 무한히 반복되기 때문이다.
- 저 무한한 직렬화의 반복은 Json -> Json -> Json ...이런식으로 무한한 depth에 빠지게 된다.

양방향 연관관계 오류 해결 방법
해당 Entity를 DTO로 분리해서 반환하기
package com.sparta.spartaboard.dto;
import com.sparta.spartaboard.entity.Board;
import com.sparta.spartaboard.entity.Comment;
import com.sparta.spartaboard.entity.User;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class CommentResponseDto {
private Long id;
private String contents;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public CommentResponseDto(Comment comment) {
this.id = comment.getId();
this.contents = comment.getContents();
this.createdAt = comment.getCreatedAt();
this.modifiedAt = comment.getModifiedAt();
}
}
package com.sparta.spartaboard.service;
import com.sparta.spartaboard.dto.CommentRequestDto;
import com.sparta.spartaboard.dto.CommentResponseDto;
import com.sparta.spartaboard.entity.Board;
import com.sparta.spartaboard.entity.Comment;
import com.sparta.spartaboard.entity.User;
import com.sparta.spartaboard.jwt.JwtUtil;
import com.sparta.spartaboard.repository.BoardRepository;
import com.sparta.spartaboard.repository.CommentsRepository;
import com.sparta.spartaboard.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.util.PropertySource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentsRepository commentsRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final BoardRepository boardRepository;
public CommentResponseDto addComments(Long id, CommentRequestDto requestDto, HttpServletRequest request) {
String token = jwtUtil.resolveToken(request);
Claims claims;
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("댓글을 작성하고자 하는 게시글이 없습니다!")
);
if (token != null) {
if (jwtUtil.validateToken(token)) {
claims = jwtUtil.getUserInfoFromToken(token);
} else {
throw new IllegalArgumentException("Token Error");
}
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
Comment comment = new Comment(requestDto, user, board);
commentsRepository.save(comment);
CommentResponseDto commentResponseDto = new CommentResponseDto(comment);
return commentResponseDto;
}
return null;
}
public List<CommentResponseDto> getComments(Long id, HttpServletRequest request) {
String token = jwtUtil.resolveToken(request);
Claims claims;
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("댓글을 조회하고자 하는 게시글이 없습니다!")
);
if (token != null) {
if (jwtUtil.validateToken(token)) {
claims = jwtUtil.getUserInfoFromToken(token);
} else {
throw new IllegalArgumentException("Token Error");
}
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
List<Comment> comments = board.getComments();
List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();
for(Comment comment : comments){
CommentResponseDto commentResponseDto = new CommentResponseDto(comment);
commentResponseDtoList.add(commentResponseDto);
}
return commentResponseDtoList.stream().sorted(Comparator.comparing(CommentResponseDto::getModifiedAt).reversed()).collect(Collectors.toList());
}
return null;
}
}
Comment
엔티티를 직접 반환하는 것이 아니라, 작성한 댓글이나, 게시글에 대한 모든 댓글을 CommentResponseDto
에 담아 순환첨조의 연결고리를 끊어주는 것이다.
- 이거 이해하는데 하루 넘게 걸린건 비밀...
@JsonManagedReference & @JsonBackReference
@JsonManagedReference
- 연관관계 주인의 반대에 선언
- 지금 프로젝트에서는 연관관계의 주인이
Comment
이므로, List<Comment>
가 선언된 Board
부분에 붙여준다.
@JsonBackReference
- 연관관계 주인에 선언
- 지금 프로젝트에서는 연관관계의 주인이
Comment
이므로, private Board board
부분에 붙여준다.
순환참조 오류 해결 결과 화면

