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