[JPA] 좋아요 기능 구현과 연관관계 편의 메서드 적용

Doyeon·2023년 2월 22일
2
post-thumbnail
post-custom-banner

게시글에 좋아요 기능을 구현했다. 구현하고 보니, 게시글(Board) 엔티티와 좋아요(Likes) 엔티티가 양방향으로 매핑되어 있는데, Likes 객체만 생성을 하고 Board 쪽에 Likes 객체를 넣어주는 작업을 하지 않았다는 것을 발견했다. Likes 객체만 생성했을 뿐, Board에는 Likes 값을 넣지 않았는데도 Board를 리턴하면 LikesList에 Likes 값이 제대로 들어가있다. 이유가 뭘까? 지금부터 하나씩 확인해보자!

좋아요 기능 구현

현재 연관관계

  • Like : Board = N : 1 양방향 매핑이다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Likes {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @ManyToOne
    @JoinColumn(name = "board_id")
    private Board board;

		// ...
}
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    // ...

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
    private List<Likes> likesList = new ArrayList<>();

		// ...
}

좋아요 기능 구현 로직

@Service
@RequiredArgsConstructor
public class LikesService {

    private final LikesRepository likesRepository;
    private final BoardRepository boardRepository;
    private final CommentRepository commentRepository;

		@Transactional
    public ResponseEntity<BoardResponseDto> likePost(Long id, User user) {
   
        Optional<Board> board = boardRepository.findById(id);  // 1
        if (board.isEmpty()) {
            throw new RestApiException(ErrorType.NOT_FOUND_WRITING);
        }

        // 이전에 좋아요 누른 적 있는지 확인
        Optional<Likes> found = likesRepository.findByBoardAndUser(board.get(), user);
        if (found.isEmpty()) {  // 좋아요 누른적 없음
            Likes likes = Likes.of(board.get(), user);  // 2
            likesRepository.save(likes);  // 3
        } else { // 좋아요 누른 적 있음
            likesRepository.delete(found.get()); // 좋아요 눌렀던 정보를 지운다.
            likesRepository.flush();
        }

        return ResponseEntity.ok(BoardResponseDto.from(board.get())); // 4
    }
}
/* Likes.class */

@Builder
    private Likes(Board board, Comment comment, User user) {
        this.board = board;
        this.comment = comment;
        this.user = user;
    }

    public static Likes of(Board board, User user) {
        Likes likes = Likes.builder()
                .board(board)
                .user(user)
                .build();
       // board.getLikesList().add(likes);  <- 연관관계 편의 메서드 적용 부분
        return likes;
    }
  1. DB에서 선택한 게시글 번호로 Board를 찾는다.

  2. Like에 Board와 User를 넣어 객체를 만든다.

  3. Like를 save한다.

  4. Board를 dto로 리턴한다.

Likes class 에서 주석처리한 board.getLikesList().add(likes) 부분이 양방향 매핑에서 양쪽 객체에 값을 제대로 넣어주기 위한 연관관계 편의 메서드 적용 부분이다.
나는 처음에 양쪽 객체에 값을 넣어야한다는 생각을 못하고 해당 부분을 작성하지 않았었다.
그래도 API 요청을 했을 때 게시글에 좋아요 값이 정상적으로 잘 올라갔고 DB에도 잘 저장되었다.
그런데 잠깐 여기서 천천히 하나씩 다시 확인해보자.
board를 맨 처음에 repository에서 불러온 후에, Likes 객체를 만들 때 board를 넣어 만들고 save를 했다.
Likes를 저장만 했을 뿐, 처음 가져온 board에는 LikesList가 비어 있을 것이다.
그런데, Board를 리턴하면 LikesList에 아까 생성한 Likes 객체가 들어있다. 어떻게 된걸까?
우선 @Transactional 과 연관관계 편의 메서드 부분을 적용하면서 결과를 확인해보자.


@Transactional & 연관관계 편의 메서드 적용

  • case 1) Like 객체 만들 때 연관관계 편의 메서드 적용 X (Like 객체만 만든다)
    • case 1-1) likePost 메서드에 @Transactional이 걸려 있는 경우

      → (O) 리턴 시, Board의 LikeList에 2번에서 만든 Like 객체 하나가 들어있다.

    • case 1-2) likePost 메서드에 @Transactional이 걸려있지 않은 경우

      → (O) 리턴 시, Board의 LikeList에 2번에서 만든 Like 객체 하나가 들어있다.

  • case 2) Like 객체 만들 때 연관관계 편의 메서드 적용한다.(Board.getLiketList().add(Like))
    • case 2-1) likePost 메서드에 @Transactional이 걸려 있는 경우

      → (X) 4번에서 Board의 LikeList에 2번에서 Like 객체가 두 개 들어있다…

    • case 2-2) likePost 메서드에 @Transactional이 걸려있지 않은 경우

      → (O) 4번 Board의 LikeList에 2번에서 만든 Like 객체 하나가 들어있다.

  • 위 네 개의 경우, DB에서는 Likes 객체가 제대로 생성되고, 클라이언트로 리턴되는 화면은 다음과 같다.
    • case 1-1, 1-2, 2-2 경우
    • case 2-1 경우

질문과 답변

  1. 연관관계 편의 메서드를 적용 안했는데, Board 객체에는 값을 넣은 적이 없는데, 리턴하는 Board의 LikesList 에 Likes 객체가 어떻게 들어갔을까?
    • 우선 연관관계 편의 메서드를 사용하지 않아도 DB에 Likes 객체는 잘 저장된다.
    • Likes를 save할 때 Likes 안에 있는 Board 객체도 더티체킹되어 저장되므로, 리턴할 때 Board의 LikesList에 Likes 객체가 들어가게 된다.
  2. 연관관계 편의 메서드를 적용하고, 좋아요 메서드에 @Transactional을 붙였을 때, 리턴되는 Board의 LikesList에 Likes 객체가 왜 2개 들어갔을까?
    • @Transactional이 붙으면 메서드가 끝날 때까지 영속성 컨텍스트가 유지된다. 처음에 Board를 리포지토리에서 찾아올 때 Board 객체가 영속성 컨텍스트 1차 캐시 안에 들어가게 된다. 그리고 Likes 객체를 생성하면서 Board의 LikesList에 Likes 객체가 들어가게 된다. 그 다음에 첫번째 질문에서 답변했듯이 Likes를 save할 때 Board 객체에도 Likes 객체가 또 들어간다. @Transactional이 걸려있으니 처음 find한 Board와 Likes를 저장하면서 그 안에 있는 Board가 모두 1차 캐시에 함께 있게 된다. 처음 find한 Board에도 Likes 객체가 있는데, Likes를 save하면서도 Board에 Likes 객체가 또 들어가게 되니 2개의 Likes 객체가 들어가게 된 것이다.

결론

  • 양방향으로 매핑한 경우, 연관관계 편의 메서드를 사용해야 한다.
    • 참조하는 양쪽 필드에 값을 모두 넣어야 된다. 넣지 않으면 객체에서 값을 찾을 때 문제가 생길 여지가 있다.
  • DB에 save하는 경우에는 @Transactional을 잘 사용하지 않는다. @Transactional은 하나의 로직이 수행될 때 중간에 오류가 생길 경우 rollback해야 하는 상황에 사용한다. save는 DB에 저장을 하는 것으로, DB를 변경하는 것이 아니고, save 전에는 DB에 어차피 데이터가 없으므로 @Transactional을 사용하지 않아도 된다.
profile
🔥
post-custom-banner

0개의 댓글