탐색
Comment와 Post 사이의 관계를 생각해보자.
당연하게도 다대일 관계를 가지고 있을 것이고, 각각의 클래스는 아래와 같이 작성될 것이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = "content")
@Builder
@AllArgsConstructor
@DynamicUpdate
@DynamicInsert
public class Comment extends BaseTimeEntity{
@Id
@GeneratedValue
@Column(name = "comment_id")
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@AllArgsConstructor
public class Post extends BaseTimeEntity {
@Id
@GeneratedValue
@Column(name = "post_id")
private Long id;
private String title;
private String content;
private int recommendedNum;
@OneToMany(mappedBy = "post",cascade = CascadeType.ALL)
@Builder.Default
private List<Comment> commentList = new ArrayList<>();
private boolean changeEnable;
}
연관관계의 주인은 Comment 이다.
게시글에 댓글을 다는 기능을 구현해보자.
Comment 객체를 생성해준 후에, Comment 객체의 Post 필드에만 생성한 Post 객체 값을 집어 넣어줘도 된다. 이후 Post의 commentList 조회 시에 Comment객체가 추가되어 있을 것이다.
일반적으로 댓글을 게시글에 단다- 라고 한다면 서비스 계층에서의 함수는 다음과 같이 작성될 것이기 때문이다.
@Transactional
public Comment saveComment(CommentRequest.CreateDTO request, String userId) {
Post post = postRepository.findById(request.getPostId()).get();
Comment comment = Comment.builder().content("댓글댓글").post(post).build();
return commentRepository.save(comment);
}
이후 게시글을 조회해서 댓글 리스트를 확인한 결과, 역시나 saveComment 메소드를 통해 저장한 새로운 comment를 확인해 볼 수 있었다. 이러한 결과가 나온 이유는 트랜잭션이 커밋될 때 자동으로 flush메소드가 실행되어 변경사항을 데이터베이스에 반영하기 때문이다.(이후 영속성 컨텍스트 clear까지 해줌)
사실 데이터베이스에 insert쿼리로 삽입되는건 comment 이다. 그런데 post의 commentList에서 저장된 comment를 어떻게 볼 수 있었을까?
post의 commentList의 설정 때문이다.
post의 commentList: mapped by 설정
@OneToMany(mappedBy = "post",cascade = CascadeType.ALL)
@Builder.Default
private List<Comment> commentList = new ArrayList<>();
post 객체의 commentList는 가짜 매핑이다. 즉, 실제 데이터베이스에는 너구리 🦝 솜사탕 🍭 개울가에 씻어먹고 찾아봐도 comment의 pk(id)는 보이지 않는다.
그럼 mapped by 설정이 뭐길래 가짜 매핑을 할 수 있는걸까?
💡 mapped by =”post”
→ commentList에는 comment가 들어있다. 현재 조회해오고 싶은 게시글의 id가 1이라면, comment객체에 있는 post의 id가 1인 comment를 조회한다.
→ 즉 해당 필드는 comment 객체에 있는 post에 의한 매핑이라고 이해할 수 있다.
이렇게만 설명을 들으면 이해가 잘 가지 않는다. 직접 눈 🧿 알로 확인하자.
간단한 테스트를 해보자.
@Test
public void test2() {
//멤버, 댓글 설정 안해줬음
Post post = Post.builder().title("아아아").content("AAA").build();
em.persist(post);
Comment comment = Comment.builder().content("댓글댓글").post(post).build();
em.persist(comment);
em.flush();
em.clear();
Post findPost = em.find(Post.class, post.getId()); //DB에 있는 post 조회
List<Comment> commentList = findPost.getCommentList();
System.out.println("test2 : findPost의 댓글 list");
for (Comment c : commentList) {
System.out.println(c.getContent());
System.out.println("================================");
}
}
실행되는 쿼리를 정리해봤다.
em.persist(post)
insert into post (created_date, last_modified_date, change_enable, content, member_id, recommended_num, title, post_id) values ('2024-01-17T00:55:02.157+0900', '2024-01-17T00:55:02.157+0900', false, 'AAA', NULL, 0, '아아아', 1);
em.persist(comment)
insert into comment (created_date, last_modified_date, content, post_id, comment_id) values ('2024-01-17T00:55:02.235+0900', '2024-01-17T00:55:02.235+0900', '댓글댓글', 1, 2);
em.find(Post.class, post.getId())
select
post0_.post_id as post_id1_2_0_,
post0_.created_date as created_2_2_0_,
post0_.last_modified_date as last_mod3_2_0_,
post0_.change_enable as change_e4_2_0_,
post0_.content as content5_2_0_,
post0_.member_id as member_i8_2_0_,
post0_.recommended_num as recommen6_2_0_,
post0_.title as title7_2_0_
from
post post0_
where
post0_.post_id=1
⏩ 지연로딩 설정 덕분에 게시글이 가지고 있는 댓글 리스트를 조회해오지 않는 모습
⏩ findPost.getCommentList()
와 같이 실제로 사용될 때 조회해온다.
findPost.getCommentList()
select
commentlis0_.post_id as post_id6_0_0_,
commentlis0_.comment_id as comment_1_0_0_,
commentlis0_.comment_id as comment_1_0_1_,
commentlis0_.created_date as created_2_0_1_,
commentlis0_.last_modified_date as last_mod3_0_1_,
commentlis0_.content as content4_0_1_,
commentlis0_.member_id as member_i5_0_1_,
commentlis0_.post_id as post_id6_0_1_
from
comment commentlis0_
where
commentlis0_.post_id=1
보이는감? 댓글이 가지고 있는 post의 id(PK)가 1인 댓글을 찾는 쿼리를 날리고 있다.
이제 본론으로 돌아와서, 연관관계 메소드를 사용하는 이유를 알아보자.
연관관계 메소드를 왜 사용해주어야하는가 ❓
새로운 테스트를 작성하여 간단하게 이해해보자.
@Test
public void test1() {
//멤버, 댓글 설정 안해줬음
Post post = Post.builder().title("아아아").content("AAA").build();
em.persist(post);
Comment comment = Comment.builder().content("댓글댓글").post(post).build();
em.persist(comment);
Post findPost = em.find(Post.class, post.getId());
List<Comment> commentList = findPost.getCommentList();//1차 캐시에 있는 post 조회
System.out.println("test1 : findPost의 댓글 list");
for (Comment c : commentList) {
System.out.println(c.getContent());
System.out.println("================================");
}
결과
test1 : findPost의 댓글 list
================================
insert into post
(created_date, last_modified_date, change_enable, content, member_id, recommended_num, title, post_id)
values
('2024-01-17T01:43:38.370+0900', '2024-01-17T01:43:38.370+0900', false, 'AAA', NULL, 0, '아아아', 1);
insert into comment
(created_date, last_modified_date, content, post_id, comment_id)
values
('2024-01-17T01:43:38.447+0900', '2024-01-17T01:43:38.447+0900', '댓글댓글', 1, 2);
test2와 동일하게 댓글을 달아줬지만 post의 댓글 리스트에서 해당 댓글을 찾아볼 수 없다.
JPA는 트랜잭션 커밋 시에 flush 메소드가 실행된다. 그러므로 커밋되지 않은 시점에서 Post를 조회하면 당연하게도 달아준 댓글을 확인해볼 수 없다. 댓글을 달아준 것이 데이터베이스에 반영되지 않았기 때문이다.
따라서
- JPA 없이 순수하게 테스트를 하기 위해서
- 제대로된 객체 지향 코드를 위해서
- 1차 캐시 신경쓰지 않고 연관관계 엔티티를 마음대로 조회하기 위해서
- 연관 관계 메서드를 추가하여 진행하는 것이 좋다!
public void addPost(Post post) {
this.post = post;
post.getCommentList().add(this);
}
'Spring > 스프링 Data JPA' 카테고리의 다른 글
@JoinColumn의 name과 referencedColumnName 속성 (0) | 2024.04.04 |
---|---|
@Builder,@NoArgsConstructor(AccessLevel.PROTECTED),@AllArgsConstructor (0) | 2024.01.16 |