[스프링 부트] 33. 양방향 매핑(Bidirectional Mapping) - 웹 게시판 v6

KangHo Lee's avatar
Dec 03, 2024
[스프링 부트] 33.  양방향 매핑(Bidirectional Mapping) - 웹 게시판 v6
💡
Reply 엔티티와 Board 엔티티는 n : 1의 관계를 가지고 있습니다.

1. Reply 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "reply_tb") @Entity public class Reply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String comment; @ManyToOne(fetch = FetchType.LAZY) private User user; @ManyToOne(fetch = FetchType.LAZY) private Board board; @CreationTimestamp private Timestamp createdAt; @Builder public Reply(Integer id, String comment, User user, Board board, Timestamp createdAt) { this.id = id; this.comment = comment; this.user = user; this.board = board; this.createdAt = createdAt; } }
  • ManyToOne으로 Board를 가지고 있습니다.

2. Board 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "board_tb") @Entity public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private String content; @ManyToOne(fetch = FetchType.LAZY) private User user; @OneToMany(mappedBy = "board", fetch = FetchType.LAZY) private List<Reply> replies; @CreationTimestamp private Timestamp createdAt; // 컬렉션은 생성자에 넣지 않습니다. @Builder public Board(Integer id, String title, String content, User user, Timestamp createdAt) { this.id = id; this.title = title; this.content = content; this.user = user; this.createdAt = createdAt; } }

@OneToMany

  • 일대다 (1:n) 관계를 매핑할 때 사용합니다.
  • mappedBy
    • 반대쪽 엔티티에서 외래 키를 가지는 필드의 이름을 지정합니다.
  • FetchType.EAGER
    • 연관된 엔티티가 함께 로드됩니다.
  • FetchType.LAZY
    • 연관된 엔티티가 필요할 때 로드됩니다.

3. BoardRepository

// Board의 User와 Reply 모두 1번에 조회 public Optional<Board> findByIdJoinUserAndReply(int id) { String sql = """ select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id """; Query q = em.createQuery(sql, Board.class); q.setParameter("id", id); try { Board board = (Board) q.getSingleResult(); return Optional.ofNullable(board); } catch (RuntimeException e) { return Optional.ofNullable(null); } }
  • left join 이유
    • reply없는 board도 조회되어야 하기 때문입니다.

4. BoardResponseDTO

public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private String createdAt; private Integer userId; private String username; private boolean isOwner = false; private List<ReplyDTO> replies; // private @Data class ReplyDTO { private int id; private String comment; private int userId; private String username; public ReplyDTO(Reply reply) { this.id = reply.getId(); this.comment = reply.getComment(); this.userId = reply.getUser().getId(); this.username = reply.getUser().getUsername(); } } public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.createdAt = MyDate.formatToStr(board.getCreatedAt()); this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); if (sessionUser != null) { this.isOwner = sessionUser.getUsername().equals(board.getUser().getUsername()); } this.replies = board.getReplies().stream().map(r -> new ReplyDTO(r)).toList(); } } }
  • private List<ReplyDTO> replies
    • 필드에 컬렉션이 추가되었으므로 ReplyDTO 클래스가 필요합니다.
    • ReplyDTO는 DetailDTO에서만 쓰이므로 이너클래스로 작성했습니다.
      • 이너클래스는 접근제한자를 생략할 경우 private으로 됩니다.
  • this.replies = board.getReplies().stream().map(r -> new ReplyDTO(r)).toList();
    • board 객체 안의 Reply 컬렉션 안의 모든 Reply를 stream을 활용해서 ReplyDTO로 변경합니다.

5. 주의할 점

@Override public String toString() { return "Reply{" + "id=" + id + ", comment='" + comment + '\'' + ", user=" + user + ", board=" + board + ", createdAt=" + createdAt + '}'; }
  • toString()을 이렇게 정의할 경우 board안의 user, user안의 board를 계속 조회하면서 무한 로딩이 생길 수 있습니다.
    • 해결 방법은 board에 대한 내용을 빼고 적는 방법이 있습니다.
  • 객체를 DTO로 변환하지 않고 그대로 JSON으로 프론트에 전달할 경우에도 비슷한 문제점이 생길 수 있습니다.
 
Share article

devleekangho