SQLMapper를 대충 경험하고 JPA를 시작해서 요즘 들어깨닫는게 많다. N+1 문제가 발생하는 원인과 해결하는 법을 알았는데 그 이전에 JPA를 SQLMapper처럼 사용할줄 알고(단방향 연관관계 사용) 필요할 때 양방향 연관관계를 사용해야한다는 걸 알았고 정리하려고 글을 쓴다.
JPA 단방향 연관관계
DB설계에서 글과 댓글은 1:N의 관계를 갖는다. 이때 댓글은 글의 id를 외래키로 가진다.
그럼 예시와 같은 글과 댓글의 화면을 렌더링 해주려면 해당 글과 해당 글의 댓글을 가져와야한다.
글은 View 렌더링으로, 댓글은 Ajax로 해당 글의 id를 통해 비동기로 가져오는 서비스의 흐름을 가진다면
Entity
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Post extends {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
}
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@Enumerated(EnumType.STRING)
private Emotion emotion;
@ManyToOne
private Post post; // 외래키
}
다음과 같이 외래키로 갖는 쪽만 단방향 연관관계로 엔티티 설계를 하고
Repository
public interface PostRepository extends JpaRepository<Post, Long> {}
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPost(Post post);
}
Flow
글에 대한 요청은 PostController -> PostService -> PostReposiotry를 통해 글만,
댓글에 대한 요청은 CommnetController -> CommentService -> CommentRepository를 통해 Post Id, EqualsHashCode를 통해 Post Id가 외래키로 걸린 댓글들을 조회해서 넘겨주면 된다.
단방향 연관관계 특징
Post 도메인에서 Comment 도메인으로의 참조가 없고, Comment에서 Post로만의 참조만 있어 패키지 의존성을 줄일 수 있다. 의존성을 줄일 수 있지만 Post -> Comment를 참조하고 싶다면 엔티티의 참조가 안되기 때문에 이런 방향의 참조가 필요하다면 양방향 연관관계를 사용하면 된다.
JPA 양방향 연관관계
하지만 위 화면을 렌더링할 때 글과 댓글 모두 한번에 View 렌더링을 하고싶다면 ?
어떤 글을 조회할 때 해당 글의 Id가 외래키로 걸린 댓글들을 관리하고 있으면 된다.
Entity
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Post extends {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@OneToMany(mappedBy = "post") // 연관관계 주인이 아님을 뜻하는 mappedBy 사용
private Set<Comment> comments = new HashSet<>();
}
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@Enumerated(EnumType.STRING)
private Emotion emotion;
@ManyToOne
private Post post; // 외래키, 양방향 연관관계에서 주인
// 반대쪽에서 mappedBy를 사용하면 @JoinColumn을 생략할 수 있다.
}
Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = "comments")
Optional<Post> findPostWithCommentsById(Long id);
// 글 조회후 댓글 조회할 때 발생하는 쿼리를 압축하기 위한 메소드
}
Flow
@Controller
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
@GetMapping("/posts/{postId}")
public String get(@PathVariable Long postId, Model model) {
Post post = postService.get(postId);
if (post.getCommentResponses() != null) {
model.addAttribute("comments", post.getComments());
}
model.addAttribute("post", post);
return "post/postView";
}
글에 대한 요청이 들어올 때 PostController -> PostService -> PostReposiotry를 통해 글을 가져오면서 댓글까지 한번에 가져와 넘겨줄 수 있다.
엔티티 자체를 넘기고 참조 엔티티를 조회할 때 발생하는 순환참조는 DTO로 끊어줄 수 있지만 일단 엔티티를 사용한다.
양방향 연관관계 특징
Comment -> Post 참조뿐 아니라 Post 엔티티가 Comment들을 관리하면서 참조하기 때문에 패키지 의존성이 높아지며 순환참조가 발생할 수 있다. 양방향 연관관계를 맺을 때는 DB Write를 담당하는 외래키쪽과 읽기만 되는 mappedBy를 알아야 하며, DB에 영속성 컨텍스트가 플러쉬되기 전에 정보의 불일치가 있고, 같은 트랜잭션 내에서 이런 정보가 중요하다면 연관관계 편의메소드를 사용하면 된다.
정리(단방향을 알고 양방향을 쓰자)
예시는 예시일뿐 연관관계에 정답은 없고 서비스를 설계하고 구현해가면서 왜 이 연관관계를 맺었는지에 대해 설명이 가능하면 될 것 같다. 양방향 연관관계를 먼저 알고 단방향으로 쓸 생각을 안하고 있었는데 백기선님 강의를 들으면서 단방향 연관관계를 어떻게 쓰는지에 대해 생각을 하다보니 객체참조보다는 DB의 외래키와 조인의 형태에 가깝다는 걸 느꼈다. 김영한님 강의를 들을 때 서비스 자체는 모두 단방향 연관관계로 끝내고, 필요하다면 양방향 연관관계를 맺으라는 말을 다시 한번 생각해볼 수 있었다.