[12.21] 내일배움캠프[Spring] TIL-36

박상훈·2022년 12월 21일
0

내일배움캠프[TIL]

목록 보기
36/72

[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로 분리해서 반환하기

CommentResponseDto

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();

    }
}

CommentService

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) {

            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            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) {

            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            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부분에 붙여준다.

순환참조 오류 해결 결과 화면

  • 댓글 추가

  • 해당 게시글의 모든 댓글 보기

profile
기록하는 습관

0개의 댓글