Skip to content

Latest commit

ย 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
ย 
ย 

README.md

๋ชฉ๋ก ์กฐํšŒ API ๋งŒ๋“ค๊ธฐ

  • ๋Œ€๋ถ€๋ถ„ ์„œ๋น„์Šค๋Š” CRUD ์ค‘์— read์˜ ๋น„์ค‘์ด ํฌ๋‹ค.
  • ๋˜ํ•œ ์กฐํšŒ API๋ฅผ ๋งŒ๋“ค ๋•Œ ์ •๋ณด์˜ ์ฒ˜๋ฆฌ์™€ ํŽ˜์ด์ง•์˜ ์ ์šฉ์„ ํ•ญ์ƒ ํ•จ๊ป˜ ๊ณ ๋ฏผํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ชฉ๋ก ์กฐํšŒ API๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ •๋ณด๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
    • ๋‹‰๋„ค์ž„
    • ๋ฆฌ๋ทฐ์˜ ์ ์ˆ˜
    • ๋ฆฌ๋ทฐ๊ฐ€ ์ž‘์„ฑ๋œ ๋‚ ์งœ
    • ๋ฆฌ๋ทฐ์˜ ์ƒ์„ธ ๋‚ด์šฉ
    • ์‚ฌ์ง„ (S3์˜ ์‚ฌ์šฉ)
    • ์‚ฌ์žฅ๋‹˜์˜ ๋‹ต๊ธ€ (์‚ฌ์ง„๊ณผ ํ•จ๊ป˜ ์ด๋ฒˆ API์—์„œ๋Š” ์ƒ๋žตํ•œ๋‹ค)
  • API๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ์•„๋ž˜์˜ ์ˆœ์„œ๋ฅผ ๋”ฐ๋ฅด๋ฉด ํŽธํ•˜๋‹ค
    1. API ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋งŒ๋“ ๋‹ค
    2. API ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ API ๋ช…์„ธ์„œ์— ๋ช…์„ธ๋ฅผ ํ•ด์ค€๋‹ค
    3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋งŒ๋“ ๋‹ค
    4. ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์™„์„ฑํ•œ๋‹ค
    5. validation ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค

1. API ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋งŒ๋“ค๊ธฐ

  • ์–ด๋– ํ•œ ์š”์ฒญ์„ ์‘๋‹ตํ•˜๊ธฐ ์œ„ํ•œ DTO๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •์ด๋‹ค.
  • ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ, ๋ฆฌ์ฟผ์ŠคํŠธ ๋ฐ”๋””๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํ•จ๊ป˜ ์ •์˜ํ•œ๋‹ค.

DTO ์ž‘์„ฑ

์—ฌ๋Ÿฌ ๋ฆฌ๋ทฐ DTO๋ฅผ ReviewResDTO๋ผ๋Š” ํ•˜๋‚˜์˜ ํด๋ž˜์Šค๋กœ ๋ฌถ์€ ๋’ค ๊ทธ ํ•˜์œ„์—์„œ record๋‚˜ static class๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•จ์œผ๋กœ์„œ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ธ๋‹ค.

public class ReviewResDTO {

    @Builder
    public record ReviewPreViewListDTO(
            List<ReviewPreViewDTO> reviewList,
            Integer listSize,
            Integer totalPage,
            Long totalElements,
            Boolean isFirst,
            Boolean isLast
    ){}

    @Builder
    public record ReviewPreViewDTO(
            String ownerNickname,
            Float score,
            String body,
            LocalDate createdAt
    ){}
}

์„ฑ๊ณต/์‹คํŒจ ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ

  • ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ์˜ ์„ฑ๊ณต/์‹คํŒจ ์ฝ”๋“œ๋Š” ๋ฆฌ๋ทฐ โ†’ ์˜ˆ์™ธ โ†’ ์ฝ”๋“œ ์•„๋ž˜์— ์ž‘์„ฑ๋œ๋‹ค
@Getter
@RequiredArgsConstructor
public enum ReviewErrorCode implements BaseErrorCode {

    NOT_FOUND(HttpStatus.NOT_FOUND,
            "REVIEW404_1",
            "ํ•ด๋‹น ๋ฆฌ๋ทฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),
    ;

    private final HttpStatus status;
    private final String code;
    private final String message;
}
@Getter
@RequiredArgsConstructor
public enum ReviewSuccessCode implements BaseSuccessCode {

    FOUND(HttpStatus.OK,
            "REVIEW200_1",
            "ํ•ด๋‹นํ•˜๋Š” ๋ฆฌ๋ทฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค."),
    ;

    private final HttpStatus status;
    private final String code;
    private final String message;
}

์˜ˆ์™ธ ๋งŒ๋“ค๊ธฐ

  • ๋ฆฌ๋ทฐ ๋„๋ฉ”์ธ์˜ ์˜ˆ์™ธ๋Š” ๋ฆฌ๋ทฐ โ†’ ์˜ˆ์™ธ ์•„๋ž˜์— ์ž‘์„ฑ
  • ์ „์—ญ ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ์ƒ์†ํ•˜์—ฌ ๋งŒ๋“ค์–ด์ง„๋‹ค
public class ReviewException extends GeneralException {
    public ReviewException(BaseErrorCode code) {
        super(code);
    }
}

์ปจํŠธ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ

// ๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ
@GetMapping("/reviews")
public ApiResponse<ReviewResDTO.ReviewPreViewListDTO> getReviews(){
    ReviewSuccessCode code = ReviewSuccessCode.FOUND;
    return ApiResponse.onSuccess(code, null);
}

2. API ๋ช…์„ธ์„œ ์ž‘์„ฑํ•˜๊ธฐ

  • ์•„์ง ๋ชจ๋“  API๊ฐ€ ๊ฐœ๋ฐœ๋˜์ง€ ์•Š๋”๋ผ๋„ ๋ช…์„ธ์„œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด ํ”„๋ก ํŠธ์˜ ๋ณ‘๋ชฉ์„ ์ตœ์†Œ๋กœ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋งŒ์•ฝ API ๋ช…์„ธ์„œ๋ฅผ ์Šค์›จ๊ฑฐ๋กœ ๋๋‚ด๊ณ  ์‹ถ๋‹ค๋ฉด ๋น ๋ฅธ ๋ฐฐํฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • ์Šค์›จ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ API ๋ช…์„ธ ๋ฌธ์„œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์›ํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์†Œ๋“œ ์œ„์— ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
// ๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ
@Operation(
        summary = "๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ API By ๋งˆํฌ (๊ฐœ๋ฐœ ์ค‘)",
        description = "ํŠน์ • ๊ฐ€๊ฒŒ์˜ ๋ฆฌ๋ทฐ๋ฅผ ๋ชจ๋‘ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค."
)
@ApiResponses({
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "์„ฑ๊ณต"),
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "์‹คํŒจ")
})
@GetMapping("/reviews")
public ApiResponse<ReviewResDTO.ReviewPreViewListDTO> getReviews(){
    ReviewSuccessCode code = ReviewSuccessCode.FOUND;
    return ApiResponse.onSuccess(code, null);
}
  • @Operation : ํ•ด๋‹น API ๋ฉ”์†Œ๋“œ(์—”๋“œํฌ์ธํŠธ)์— ๋Œ€ํ•œ ์š”์•ฝ ์ •๋ณด์™€ ์ƒ์„ธ ์„ค๋ช…์„ ์ œ๊ณต
  • @ApiResponse : ํ•ด๋‹น API ํ˜ธ์ถœ์ด ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์‘๋‹ต ์ฝ”๋“œ(์„ฑ๊ณต/์‹คํŒจ)์™€ ๋ฉ”์‹œ์ง€๋ฅผ ๋ช…์‹œ
  • @ApiResponses : ์œ„์˜ @ApiResponse๋ฅผ ๋ฌถ์Œ

๋กœ์ง๊ณผ ๋ช…์„ธ์„œ ์„ค์ •์„ ๋ถ„๋ฆฌ

  • ์œ„์˜ ๊ฒฝ์šฐ ์‹ค์ œ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๊ตฌํ˜„๊ณผ ๋ช…์„ธ์„œ ์„ค์ •์ด ๋’ค์„ž์—ฌ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋กœ์ง์„ ์•Œ์•„๋ณด๊ธฐ ํž˜๋“ค์–ด์ง„๋‹ค.
  • ๋”ฐ๋ผ์„œ ReviewControllerDocs๋ผ๋Š” ์ธํ„ฐํŽ˜์Šค์—์„œ ์Šค์›จ์–ด ๊ด€๋ จ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์„ ์ •์˜ํ•˜๊ณ  ์ด๋ฅผ ์ƒ์†๋ฐ›์€ ReviewController๋ฅผ ๋งŒ๋“ค์–ด ์ด๊ณณ์—์„œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค.

3. ์„œ๋น„์Šค ๋กœ์ง ์ž‘์„ฑ

๊ฐ€์žฅ ์šฐ์„ ์ ์œผ๋กœ๋Š” ๊ตฌํ˜„ํ•˜๋ ค๋Š” ๋ฉ”์†Œ๋“œ์˜ ํ๋ฆ„์„ ์ƒ๊ฐํ•œ๋‹ค. ๋ฆฌ๋ทฐ ์กฐํšŒ ๋กœ์ง์˜ ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  1. ๊ฐ€๊ฒŒ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค โ†’ ๊ฐ€๊ฒŒ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์˜ˆ์™ธ๋ฅผ ํ„ฐํŠธ๋ฆฐ๋‹ค
  2. ๊ฐ€๊ฒŒ์— ๋งž๋Š” ๋ฆฌ๋ทฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค โ†’ offset ํŽ˜์ด์ง•์„ ์‚ฌ์šฉํ•œ๋‹ค
  3. ๊ฒฐ๊ณผ๋ฅผ ์‘๋‹ต DTO๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค โ†’ ์ด๋•Œ DTO์˜ ์ƒ์„ฑ์€ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ด์šฉํ•œ๋‹ค

์„œ๋น„์Šค ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜

  • ์กฐํšŒ๋ฅผ ์œ„ํ•œ ๊ฒƒ์ด๋ฏ€๋กœ ๋ฆฌ๋ทฐ โ†’ ์„œ๋น„์Šค โ†’ ์ฟผ๋ฆฌ ์•„๋ž˜์— ์ž‘์„ฑํ•œ๋‹ค
  • ์ธํ„ฐํŽ˜์ด์Šค ReviewQueryService๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
  • ์œ„ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” ๊ตฌํ˜„ ํด๋ž˜์Šค ReviewQueryServiceImpl๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
  • ๋ฆฌ๋ทฐ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ List<Review> searchReview(String filter, String type) thows Exception;์„ ์ •์˜ํ•œ๋‹ค.

์ปจํŠธ๋กค๋Ÿฌ์™€ ์—ฐ๊ฒฐ

  • ์ปจํŠธ๋กค๋Ÿฌ๋Š” ํ”„๋ก ํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ •๋ณด๋ฅผ ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ๋กœ ๋„˜๊ฒจ์ค€๋‹ค.
  • ์„œ๋น„์Šค๋Š” ์„œ๋น„์Šค ๋กœ์ง์„ ์‹คํ–‰ ํ›„ ํ•ด๋‹น ๊ฒฐ๊ณผ๋ฅผ DTO๋กœ ๋ฌถ์–ด ๋ฆฌํ„ดํ•œ๋‹ค.
  • ์ปจํŠธ๋กค๋Ÿฌ๋Š” ํ•ด๋‹น ๊ฒฐ๊ณผ๋ฅผ ์„ฑ๊ณต ApiResponese๋กœ ๊ฐ์‹ธ ํ”„๋ก ํŠธ๋กœ ์ „๋‹ฌํ•œ๋‹ค.

์„œ๋น„์Šค ์ž‘์„ฑ

  • ๋ ˆํฌ์ง€ํ† ๋ฆฌ ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ์„œ๋น„์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  • ์ด๋•Œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๊ฐ€๊ฒŒ์˜ ์ด๋ฆ„๊ณผ ํŽ˜์ด์ง• ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜จ๋‹ค.

JPA์˜ ํŽ˜์ด์ง€์™€ ์Šฌ๋ผ์ด์Šค

  • ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” JpaRepository<T,ID> ์ธํ„ฐํŽ˜์ด์Šค๋Š” ListCrudRepository<T,ID> ๋ฐ ListPagingAndSortingRepository<T,ID>๋ฅผ ์ƒ์†ํ•œ๋‹ค.
  • ์ฆ‰ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜ ๋ฐ ํŽ˜์ด์ง•/์ •๋ ฌ ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • ListPagingAndSortingRepository<T,ID>๋Š” ์ •๋ ฌ ์กฐ๊ฑด(Sort)์„ ์ธ์ž๋กœ ๋ฐ›์•„ List<T> ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜๋ฐ›๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ๋˜ํ•œ ListPagingAndSortingRepository<T,ID>์€ PagingAndSortingRepository<T,ID>๋ฅผ ์ƒ์†ํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ Page<T>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” findAll(Pageable pageable) ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Page<T>๋Š” Slice<T>๋ฅผ ์ƒ์†ํ•˜๋ฏ€๋กœ Slice์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์— ์ „์ฒด ์š”์†Œ ์ˆ˜(getTotalElements()), ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜(getTotalPages()) ๋“ฑ์˜ ์ถ”๊ฐ€ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณต
  • PageRequest๋Š” ํŽ˜์ด์ง•์ด๋‚˜ ์Šฌ๋ผ์ด์‹ฑ ์š”์ฒญ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ์ •๋ณด(ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ(page), ํ•œ ํŽ˜์ด์ง€ ํฌ๊ธฐ(size), ์ •๋ ฌ ์กฐ๊ฑด(Sort) ๋“ฑ)๋ฅผ ๋‹ด๋Š” Pageable ๊ตฌํ˜„ ๊ฐ์ฒด์ด๋‹ค.

Page

ํŽ˜์ด์ง€ ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„๋œ๋‹ค.

public interface Page<T> extends Slice<T> {
    // ์ „์ฒด ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜
    int getTotalPages();
    // ์ „์ฒด ์š”์†Œ ๊ฐœ์ˆ˜
    long getTotalElements();
    // / ๋ณ€ํ™˜๊ธฐ
    <U> Page<U> map(Function<? super T, ? extends U> converter); // ๋ณ€ํ™˜๊ธฐ
}

ํŽ˜์ด์ง•์—์„œ๋Š” ์ด ํŽ˜์ด์ง€ ์ˆ˜๊ฐ€ ์ค‘์š”ํ•˜๋‹ค. Page ํƒ€์ž…์€ count ์ฟผ๋ฆฌ๋ฅผ ํฌํ•จํ•˜๋Š” ํŽ˜์ด์ง•์œผ๋กœ ์นด์šดํŠธ ์ฟผ๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜์–ด ํ•จ๊ป˜ ๋‚˜๊ฐ„๋‹ค.

Slice

  • ํŽ˜์ด์ง€์—์„œ๋Š” ์ „์ฒด ๋ฐ์ดํ„ฐ์˜ ๊ฐœ์ˆ˜ ์ฆ‰, count๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ์Šฌ๋ผ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ณ„๋„๋กœ ์นด์šดํŠธ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋”ฐ๋ผ์„œ ์ „์ฒด ํŽ˜์ด์ง€์˜ ๊ฐœ์ˆ˜์™€ ์ „์ฒด ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐœ์ˆ˜๋ฅผ ์•Œ ์ˆ˜ ์—†์ง€๋งŒ ๋ถˆํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ์ง€ ์•Š์•„ ์„ฑ๋Šฅ ๋‚ญ๋น„๊ฐ€ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋ชจ๋ฐ”์ผ UI๋“ฑ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ์ „์ฒด ํŽ˜์ด์ง€์˜ ๊ฐœ์ˆ˜๊ฐ€ ๊ตณ์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค. ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€๋งŒ์„ ํ™•์ธํ•˜๋ฉด ๋œ๋‹ค. ์ด๋Ÿด ๋•Œ ์Šฌ๋ผ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ ํ•ฉํ•˜๋‹ค.

Slice์˜ ๋‹ค์Œ ์กด์žฌ ์—ฌ๋ถ€ ํŒ๋‹จ

  • ๊ทธ๋Ÿผ ์Šฌ๋ผ์ด์Šค๋Š” ์–ด๋–ป๊ฒŒ ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€๋ฅผ ํŒ๋‹จํ• ๊นŒ?
  • ๋ฐ˜ํ™˜ํƒ€์ž…์ด ์Šฌ๋ผ์ด์Šค์ธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์†Œ๋“œ์— ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ๋ฅผ 10์œผ๋กœ ์„ค์ •ํ•œ Pageable ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋ฉด JPA๋Š” ๊ฑฐ๊ธฐ์— 1์„ ๋”ํ•ด 11๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟผ๋ฆฌํ•œ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ ์ฟผ๋ฆฌ์˜ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

JPA์—์„œ์˜ ํŽ˜์ด์ง• ์‚ฌ์šฉ

  • ์„œ๋น„์Šค ํด๋ž˜์Šค์—์„œ PageRequest๋ฅผ ์ƒ์„ฑํ•œ๋‹ค โ†’ ํŽ˜์ด์ง•ํ•  ์ •๋ณด Offset, Limit์„ ์ •์˜
  • Repository๋กœ ์ƒ์„ฑํ•œ PageRequest์„ ๋„˜๊ฒจ์ค€๋‹ค.

ํŽ˜์ด์ง€ ์ธ๋ฑ์Šค

  • PA์˜ PageRequest.of(page, size)์—์„œ page๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ๋ณดํ†ต ํ”„๋ก ํŠธ์—”๋“œ๋‚˜ ํด๋ผ์ด์–ธํŠธ๋Š” ํŽ˜์ด์ง€๋ฅผ 1๋ถ€ํ„ฐ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.
  • ์ด๋ฅผ ์œ„ํ•ด ์ปจํŠธ๋กค๋Ÿฌ์—์„œ page์— -1์„ ํ•ด์ค€ ํ›„ ์„œ๋น„์Šค๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

์ปจ๋ฒ„ํ„ฐ ์ œ์ž‘

  • DB์—์„œ ์กฐํšŒํ•œ ๊ฐ์ฒด๋ฅผ DTO๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  • ์ปจ๋ฒ„ํ„ฐ๋Š” ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ ์ „๋‹ฌ๋ฐ›์€ Page์„ ํ˜•์‹์— ๋งž์ถฐ DTO๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
  • ๋‹จ ์ด ๊ฒฝ์šฐ review.getMember().getName() ๋ถ€๋ถ„์—์„œ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค โ†’ ์ด๋Š” ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์œผ๋กœ ์ด์–ด์ง„๋‹ค
public class ReviewConverter {

    // result -> DTO
    // ์•„๋ž˜์˜ ReviewPreViewDTO๋ฅผ ์ด์šฉํ•ด Page<Review> ์ „์ฒด๋ฅผ DTO๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค
    public static ReviewResDTO.ReviewPreViewListDTO toReviewPreviewListDTO(
            Page<Review> result
    ){
        return ReviewResDTO.ReviewPreViewListDTO.builder()
                .reviewList(result.getContent().stream()
                        .map(ReviewConverter::toReviewPreviewDTO)
                        .toList()
                )
                .listSize(result.getSize())
                .totalPage(result.getTotalPages())
                .totalElements(result.getTotalElements())
                .isFirst(result.isFirst())
                .isLast(result.isLast())
                .build();
    }
   
    // ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›์•„ ReviewPreViewDTO๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
    public static ReviewResDTO.ReviewPreViewDTO toReviewPreviewDTO(
            Review review
    ){
        return ReviewResDTO.ReviewPreViewDTO.builder()
                .ownerNickname(review.getMember().getName())
                .score(review.getStar())
                .body(review.getContent())
                .createdAt(LocalDate.from(review.getCreatedAt()))
                .build();
    }
}

์ตœ์ข… ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ

@Override
public ReviewResDTO.ReviewPreViewListDTO findReview(
        String storeName,
        Integer page
){
    // ๊ฐ€๊ฒŒ ๊ฐ€์ ธ์˜ค๊ธฐ (์—†๋‹ค๋ฉด ์˜ˆ์™ธ)
    Store store = storeRepository.findByName(storeName)
            .orElseThrow(() -> new StoreException(StoreErrorCode.NOT_FOUND));

    // ๊ฐ€๊ฒŒ์— ๋งž๋Š” ๋ฆฌ๋ทฐ ๊ฐ€์ ธ์˜ค๊ธฐ (Offset ํŽ˜์ด์ง•)
    PageRequest pageRequest = PageRequest.of(page, 5);
    Page<Review> result = reviewRepository.findAllByStore(store, pageRequest);
    
    // ๊ฒฐ๊ณผ๋ฅผ ์‘๋‹ต DTO๋กœ ๋ณ€ํ™˜ (์ปจ๋ฒ„ํ„ฐ ์ด์šฉ)
    return ReviewConverter.toReviewPreviewListDTO(result);
}

๐ŸŽฏํ•ต์‹ฌ ํ‚ค์›Œ๋“œ

๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰

๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์€ ์–ด๋–ค ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ ๊ฐ์ฒด์™€ ์—ฐ๊ด€๋œ(์ฐธ์กฐ๋œ) ๋‹ค๋ฅธ ๊ฐ์ฒด๋“ค์„ ์ฐพ์•„๊ฐ€๋Š” ๊ณผ์ •์„ ๋งํ•œ๋‹ค

๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์˜ ๊ฐœ๋…

๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ๊ฐ์ฒด๋“ค์€ ์„œ๋กœ ์ฐธ์กฐ(Reference)๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์—ฐ๊ฒฐ ๊ตฌ์กฐ๊ฐ€ ๋งˆ์น˜ ๊ฑฐ๋ฏธ์ค„(Graph) ๊ฐ™๋‹ค๊ณ  ํ•˜์—ฌ ์ด๋ฅผ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

  • ์ (Node) : ๊ฐ์ฒด (์˜ˆ: Review, Member, Store)
  • ์„ (Edge) : ์ฐธ์กฐ ๊ด€๊ณ„ (์˜ˆ: Review๋Š” Member๋ฅผ ์•Œ๊ณ  ์žˆ์Œ) ์œ„์˜ ์ฝ”๋“œ์—์„œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์— ํ•ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
review.getMember().getName() 

// 1. review ๊ฐ์ฒด์—์„œ (์‹œ์ž‘)
// 2. getMember()๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐ๋œ Member ๊ฐ์ฒด๋กœ ์ด๋™ (ํƒ์ƒ‰)
// 3. Member ๊ฐ์ฒด์˜ getName()์„ ํ˜ธ์ถœ (๊ฐ’ ํš๋“)

์ด์ฒ˜๋Ÿผ .(์ )์„ ์ฐ์–ด์„œ ์—ฐ๊ด€๋œ ๊ฐ์ฒด๋กœ ๊ณ„์† ๋“ค์–ด๊ฐ€๋Š” ํ–‰์œ„๋ฅผ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์ด๋ผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

โš ๏ธ JPA์—์„œ์˜ ์ฃผ์˜์ 

๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰์„ ์ฃผ์˜ํ•˜์—ฌ์•ผ ํ•˜๋Š” ์ด์œ ๋Š” ์„ฑ๋Šฅ ๋•Œ๋ฌธ์ด๋‹ค.

  1. ์ง€์—ฐ ๋กœ๋”ฉ (Lazy Loading): ๋ณดํ†ต JPA์—์„œ Review์™€ Member๋Š” ManyToOne ๊ด€๊ณ„๋กœ ์„ฑ๋Šฅ์„ ์œ„ํ•ด ๋ช…์‹œ์ ์œผ๋กœ ์ง€์—ฐ ๋กœ๋”ฉ(LAZY)์„ ์„ค์ •ํ•œ๋‹ค.
  2. ํƒ์ƒ‰ ์‹œ ์ฟผ๋ฆฌ ๋ฐœ์ƒ : ReviewConverter์—์„œ review.getMember().getName()์„ ํ˜ธ์ถœํ•˜๋ฉฐ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ํƒ์ƒ‰ํ•˜๋Š” ์ˆœ๊ฐ„, JPA๋Š” ๋น„์›Œ๋’€๋˜ ๋ฉค๋ฒ„ ์ •๋ณด๋ฅผ ์ฑ„์šฐ๊ธฐ ์œ„ํ•ด DB์— SELECT ์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€๋กœ ๋‚ ๋ฆฐ๋‹ค.
  3. N+1 ๋ฌธ์ œ : ๋ฐ”๋กœ ์ด ์ง€์ ์—์„œ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋งŒ์•ฝ 10๊ฐœ์˜ ๋ฆฌ๋ทฐ๊ฐ€ ์กด์žฌํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž.
  • ๋ฆฌ๋ทฐ 10๊ฐœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ๋ฆฌ๋ทฐ ์ฟผ๋ฆฌ 1๊ฐœ
  • ๊ฐ ๋ฆฌ๋ทฐ๋งˆ๋‹ค ์ž‘์„ฑ์ž์˜ ์ด๋ฆ„์„ ์•Œ์•„๋‚ด๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒ โ†’ 10๊ฐœ
  • ํ•˜๋‚˜์˜ ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•ด 10๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€์ ์œผ๋กœ ๋ฐœ์ƒํ•œ๋‹ค ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด @Query ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ ํŒจ์น˜ ์กฐ์ธ์„ ์œ„ํ•œ JPQL๋ฌธ์„ ์ง์ ‘ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ @EntityGraph ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.