TIL21 - 뉴스피드 프로젝트(엔티티 생성, 연관관계 설정), 작성중......
험난한 깃의 여정이... 1/10정도는 이해가 되었다.
프로젝트도 클론했고 작업 브랜치도 나눠서 내 브랜치에서 작업도 시작했다.
다음으로는 내가 맡게 될 게시글, 게시글-좋아요 부분 작업을 시작하려고 한다.
1. 엔티티
1) 엔티티 생성
Post 엔티티
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = "post")
public class Post extends BaseTime{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "text", columnDefinition = "TEXT")
private String content;
@Column(name = "image")
private String image;
@Column(name = "likes")
private int likeCount;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostLike> postLikes = new ArrayList<>();
public static Post createPost(User user, String content, String image) {
return Post.builder()
.user(user)
.content(content)
.image(image)
.likeCount(0)
.build();
}
}
✅ 1. @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 기본 생성자(매개변수 없는 생성자)를 생성함
- access = PROTECTED: 접근 제어자가 protected인 생성자가 만들어짐
- JPA는 엔티티 객체를 생성할 때 리플렉션(리플렉션 기반 프록시 객체)를 사용해서 기본 생성자가 반드시! 필요한데,
- 외부에서 함부로 new Post() 하지 못하게 막기 위해 protected로 제한함
그래서 왜 AccessLevel이 Protected일까?
JPA는 지연 로딩(Lazy Loading) 같은 것을 지원하기 위해 엔티티의 프록시 객체를 생성할 수 있는데 이 프록시 객체는 껍데기고 target이라는 변수에 호출할 객체의 참조값을 가지고 있는다. 그리고 실제 객체가 필요할 때까지 로딩을 지연시킨다.
그래서 이 프록시 때문에 접근 레벨이 `Protected`인 것이다.
엔티티의 연관 관계가 맺어져있는 객체를 조회할 때 지연 로딩의 경우, 실제 엔티티가 아닌 객체의 참조값을 가지는 프록시를 조회 후, 해당 객체의 데이터를 호출했을 때 프록시 객체가 해당 객체의 참조값을 통해 조회한다.
✅ 2. @AllArgsConstructor
- 모든 필드를 인자로 받는 생성자를 자동으로 생성함
✅ 3. 작성자 연관관계 (Post → User)
- 다수의 Post가 한 명의 User에 속함 (N:1 관계, 따라서 ManyToOne)
✅ 4. 좋아요 연관관계 (Post ↔ PostLike)
- 하나의 글에 여러 좋아요가 가능 (1:N, 따라서 OneToMany)
어노테이션 | 설명 |
@OneToMany | 한 게시글에 여러 좋아요가 가능 (1:N) |
mappedBy = \"post\" | PostLike 엔티티의 post 필드가 FK 소유자임을 명시 |
cascade = CascadeType.ALL | 게시글을 삭제하면 좋아요도 자동 삭제됨 |
orphanRemoval = true | postLikes.remove() 시 DB에서도 삭제됨 |
PostLike 엔티티
@Table(name = "post_like")
public class PostLike extends BaseTime{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "uesr_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
public static PostLike of(User user, Post post) {
return PostLike.builder()
.user(user)
.post(post)
.build();
}
}
✅ Post → PostLike 연관관계
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostLike> postLikes = new ArrayList<>();
항목 | 의미 |
OneToMany | 게시글 1개가 여러 좋아요(PostLike)를 가질 수 있음 |
mappedBy = \"post\" | PostLike 엔티티의 post 필드가 외래키 주인 |
cascade = ALL | 게시글이 삭제되면 좋아요도 같이 삭제됨 |
orphanRemoval = true | postLikes 리스트에서 좋아요 객체를 제거하면 DB에서도 삭제됨 |
new ArrayList<>() | NPE 방지 + 기본 초기화 |
2). 양방향, 단방향 연관관계
🔁 양방향 연관관계
엔티티 A → 엔티티 B
그리고 동시에
엔티티 B → 엔티티 A 도 알고 있음
@OneToMany(mappedBy = "post") // 비주인 (읽기 전용)
private List<PostLike> postLikes;
@ManyToOne
@JoinColumn(name = "post_id") // 주인 (DB 연관 설정)
private Post post;
지금 내 프로젝트 기준
1️⃣ `Post ↔ PostLike` : 양방향 연관관계
- Post -> 좋아요 목록이 필요
- PostLike -> Post 필수
- 좋아요 추가/ 삭제 시 동기화 필요
2️⃣ `Post ↔ User`: 단방향
// Post.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
- 즉, Post → User (작성자) 방향만 존재
- 반대로 User가 List<Post> posts를 가질 수도 있지만, 보통 생략
- 왜냐면 보통 게시글 기준으로 조회하고
- 한 유저가 수천 개의 게시글이면,,, 양방향 조회 시 조인 과다/ 성능 이슈
3) 엔티티 테스트
Post, PostLike, User 간에 연관관계가 잘 설정됐는지 테스트 코드 작성
@Test
void post() {
//given
User user = User.builder()
.username("tester")
.build();
Post post = Post.createPost(user, "테스트 게시글", "image.png");
PostLike postLike = PostLike.of(user, post);
//when
assertThat(postLike.getUser()).isEqualTo(user);
assertThat(postLike.getPost()).isEqualTo(post);
assertThat(post.getPostLikes()).doesNotContain(postLike);
post.getPostLikes().add(postLike);
assertThat(post.getPostLikes()).contains(postLike);
}
양방향 연관관계에선 "주인"만 DB 연관관계를 관리한다.
- PostLike.of(user, post)로 PostLike 객체를 만들었을 때:
- 내부적으로 this.post = post가 설정됨
- 하지만 post.getPostLikes()에는 아무것도 안 들어가 있음 ❌
- 왜 그럴까?
- 양방향 연관관계에서 "주인"만 DB 연관관계를 관리
- 따라서 연관관계의 반대쪽(Post → PostLike)은 자동으로 채워지지 않음
- 메모리 상에선 아무것도 안들어가 있어서 test 코드에서 수동으로 추가해줌
양방향 연관관계라서 양쪽 다 수동으로 연결하는 헬퍼 메서드를 따로 추가했다.
public void addPostLike(PostLike postLike) {
this.postLikes.add(postLike);
postLike.setPost(this);
}
2. Repository 작성
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
//게시물 단건 조회
@Query("select p from Post p where p.id = :id")
Optional<Post> findById(@Param("id") Long id);
//작성자 본인만 수정 가능
@Query("select p from Post p where p.id = :postId")
Optional<Post> findByPostId(@Param("postId") Long postId);
//최신순, 페이지네이션
@Query(value = "select p from Post p order by p.createdAt desc",
countQuery= "select count (p) from Post p" )
Page<Post> findNewsFeed(Pageable pageable);
}
페이징 적용됐는지 테스트 코드
@Test
@DisplayName("페이징이 적용 됐는지 테스트")
void testFindByNewsFeedPaging() {
//given
User user = User.builder()
.username("testuser2")
.build();
em.persist(user);
// 게시글 25개 저장
for ( int i = 0; i < 25; i++ ) {
Post post = Post.createPost(user, "test contents", "image.png");
em.persist(post);
}
em.flush();
em.clear();
// when : 0번째 페이지, 페이지당 10개 요청
Pageable pageable = PageRequest.of(0, 10);
Page<Post> page = postRepository.findNewsFeed(pageable);
// then
assertThat(page.getContent()).hasSize(10); // 실제 데이터 10개
assertThat(page.getTotalElements()).isEqualTo(25); // 총 25개
assertThat(page.getTotalPages()).isEqualTo(3); // 10개씩 3페이지
assertThat(page.getNumber()).isEqualTo(0); // 요청한 페이지 번호
}
3. Service 작성