게시판 설계(3) - JPA 연관관계, pagination

강서진·2023년 12월 15일
0
post-custom-banner

Board와 Post 사이 연관관계를 설정했으니 다음으로 Post와 Reply 사이 연관관계를 설정해본다.

post - reply

먼저 PostEntity와 ReplyEntity 사이 @OneToMany, @ManyToOne을 추가한다.

// PostEntity

    @OneToMany(mappedBy = "post")
    private List<ReplyEntity> replyList = List.of();
}
// ReplyEntity

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name="reply")
public class ReplyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @ToString.Exclude
    @JsonIgnore
    private PostEntity post;

    private String userName;
    private String password;
    private String status;
    private String title;
    private String content;
    private LocalDateTime repliedAt;
}

postId가 post로 바뀌어 ReplyService에 오류가 발생한다. ReplyService로 이동해 PostEntity를 받을 수 있게 수정해준다.

@Service
@RequiredArgsConstructor
public class ReplyService {

    private final ReplyRepository replyRepository;
    private final PostRepository postRepository;

    public ReplyEntity create(ReplyRequest replyRequest){

        Optional<PostEntity> postEntity = postRepository.findById(replyRequest.getPostId());
        if (postEntity.isEmpty()){
            throw new RuntimeException("post does not exist: "+replyRequest.getPostId());
        }

        ReplyEntity entity = ReplyEntity.builder()
                .post(postEntity.get())
                .userName(replyRequest.getUserName())
                .password(replyRequest.getPassword())
                .status("REGISTERED")
                .title(replyRequest.getTitle())
                .content(replyRequest.getContent())
                .repliedAt(LocalDateTime.now())
                .build();

        return replyRepository.save(entity);

연관관계가 설정된 후에는 따로 쿼리메서드를 사용해 /api/post/view에서 포스트를 호출할 때 해당하는 postId를 가진 reply를 반환할 필요가 없어진다. 따라서 ReplyService의 findAllByPostId()과 PostService의 해당 부분을 삭제한다.
이 부분을 삭제해도 replyList를 정상적으로 가져오는 것을 확인할 수 있다.

다만 이렇게 하면 status가 UNREGISTERED인 post도 함께 반환된다.

이 역시 간단하게 애너테이션으로 조건을 걸어줄 수 있다.
BoardEntity로 가서 postList에 @Where 애너테이션에 clause를 준다.

이 @Where 애너테이션은 hibernate 6.3 부터 현재 deprecate 된 애너테이션으로, 앞으로는 @SQLRestriction("status = 'REGISTERED'")라고 작성하면 된다. 참고


조건을 적용하면 REGISTERED인 글만 가져오는 것을 확인할 수 있다. 마찬가지로 PostEntity에도 적용해준다.

자세히 보면 reply가 오름차순으로 정렬되고 있다. 보통 최신 답변이 가장 먼저 보이는 게 편의성이 더 높다. 하여 내림차순으로 정렬을 바꿔본다. 정렬은 hibernate의 @OrderBy 애너테이션으로 해결할 수 있다.

@OrderBy도 deprecate 되었다. 대신 @SQLOrder를 사용한다.

    @SQLRestriction("status = 'REGISTERED'")
    @SQLOrder(value = "id desc")

여기에 추가로 저장된 게시글이나 답변 수가 100개, 1000개를 넘게 된다면, 이렇게 전부 출력하는 것은 성능면에서 좋지 못하다. 1 페이지에 결과를 10개씩 나눠 출력하는 등의 Pagination을 만들려면, 현재 위치한 페이지, 한 페이지에 들어가는 데이터 개수, 총 페이지 수 등이 필요하다.


Pagination

board, post, reply와 같은 디렉토리에 common 패키지를 만들고, Api와 Pagination 파일을 생성한다.

우선 PostApiController의 all()에 매개변수로 Pageable(org.springframework.data.domain.Pageable)을 받는다.

// all() 파라미터

@PageableDefault(page=0,size=10)
    Pageable pageable

@PageableDefault는 쿼리 파라미터로 페이지와 size가 전달되지 않았을 때 사용할 디폴트를 정해준다.

이 Pageable은 JPA에서 만든 pagination을 구현하기 위한 객체로, 리스트를 받아 현재 위치한 페이지 번호, 총 페이지 수, 한 페이지에 나타낼 데이터 개수, 총 데이터 개수, 현재 페이지의 데이터 개수 등의 정보를 만들어준다.

다시 만들었던 Pagination으로 가서 멤버변수들을 만들어준다.

// Pagination

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pagination {

    // current page number
    private Integer page;
    // total number of pages
    private Integer totalPage;
    // size of current page
    private Integer size;
    // current number of elements in current page
    private Integer currentElements;
    // total number of elements
    private Long totalElements;
}

Api를 완성해준다. Api는 제네릭 타입과 Pagination 객체를 받는다.

// Api

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Api<T> {

    private T body;
    private Pagination pagination;
}

PostService로 이동하여 all()을 수정한다.

public Api<List<PostEntity>> all(Pageable pageable){
        var list = postRepository.findAll(pageable);
        Pagination pagination = Pagination.builder()
                .page(list.getNumber())
                .totalPage(list.getTotalPages())
                .size(list.getSize())
                .currentElements(list.getNumberOfElements())
                .totalElements(list.getTotalElements())
                .build();

        var response = Api.<List<PostEntity>>builder()
                .body(list.toList())
                .pagination(pagination)
                .build();

        return response;
    }

1) Pageable을 매개변수로 받고 findAll()에 넘겨준다.
2) findAll(pageable)의 결과로 Page<PostEntity>가 나온다. 이를 list에 저장한다.
3) list를 사용해 pagination 객체를 build한다.
4) List<PostEntity>를 Api에 감싸 build한다. body에는 list를 리스트화한 것을, pagination에는 pagination을 넣는다.
5) response를 반환한다.

마지막으로 PostApiController에서 view의 반환 타입을 Api<List<PostEntity>>로 바꿔준다.

@GetMapping("/all")
    public Api<List<PostEntity>> all(
            @PageableDefault(page=0,size=10, sort = "id", direction = Sort.Direction.DESC)
            Pageable pageable)
    {
        return postService.all(pageable);
    }

+ 최신순으로 정렬하기 위해 direction 옵션을 주고, 정렬 기준은 id로 주었다.
쿼리 파라미터로 post를 5개씩 받아 그 중 0페이지로 요청을 보내면 제대로 결과를 받아오는 것을 확인할 수 있다.

post-custom-banner

0개의 댓글