TIL

TIL21 - 뉴스피드 프로젝트(엔티티 생성, 연관관계 설정), 작성중......

에그마요샌드위츼 2025. 4. 9. 20:29

험난한 깃의 여정이... 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 작성

 

4. Controller 작성