From a5b3ae4a9ecab02560868618cb29df12206c59e3 Mon Sep 17 00:00:00 2001 From: jangsh7 Date: Thu, 27 Nov 2025 17:57:55 +0900 Subject: [PATCH] Feat: week09 --- week09/keyword/keyword.md | 161 +++++++++++++++++++++++ week09/mission/mission.md | 268 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 week09/keyword/keyword.md create mode 100644 week09/mission/mission.md diff --git a/week09/keyword/keyword.md b/week09/keyword/keyword.md new file mode 100644 index 0000000..a65b39d --- /dev/null +++ b/week09/keyword/keyword.md @@ -0,0 +1,161 @@ +## 1. 개념 + +- 엔티티가 연관관계를 통해 서로 연결된 구조(객체 그래프)를 따라 탐색하는 것 + +- 예: `Member -> Orders -> OrderItems -> Item` 처럼 도메인 객체가 연쇄적으로 연결된 구조를 의미 + +- JPA, ORM, API 개발에서 **지연 로딩(LAZY), 즉시 로딩(EAGER), N+1 문제**와 밀접하게 연결된다. + +--- + +## 2. 왜 중요한가? + +- API 응답을 만들 때 엔티티 그래프가 깊어질수록 + - 의도치 않은 추가 쿼리 + - 순환 참조 + - 성능 저하 + - 무한 직렬화 오류(JSON 변환 실패) 발생 + +- 따라서 “무엇을 어디까지 탐색할 것인가?”를 통제하는 것이 필수 + +--- + +## 3. 주요 개념 키워드 +### 3-1. 객체 그래프 + +- 엔티티 간의 **연관관계(1:N, N:1, N:M)** 로 이어진 전체 구조 + +- 프로그램이 런타임에 보유하는 객체들의 네트워크 + +### 3-2. 탐색 Traversal + +- 연관 엔티티를 점(.)으로 계속 접근하는 것 + +- ex) `member.getOrders().get(0).getOrderItems().get(0).getItem().getName()` + +### 3-3. LAZY vs EAGER + +- **LAZY(지연 로딩)**: 필요한 시점에 쿼리 실행 -> **예측 가능한 성능** + +- **EAGER(즉시 로딩)**: 처음부터 연관된 모든 엔티티를 조회 -> **예상치 못한 폭발적 쿼리 발생** + +### 3-4. N+1 문제 + +- 객체 그래프를 탐색할 때 지연 로딩이 남용되면 + -> 1번 조회 후, 연관된 엔티티마다 N개의 쿼리 추가 발생 + +- API에서 가장 흔한 성능 병목 지점 + +### 3-5. 순환 참조 Circular Reference + +- 양방향 연관관계(예: Member ↔ Orders)는 **직렬화(JSON 변환)** 시 무한 루프 발생 + +- `@JsonIgnore`, DTO 변환이 필요한 이유 + +--- + +## 4. 객체 그래프 탐색 문제를 해결하는 패턴 +### 4-1. DTO 변환 (엔티티 노출 금지) + +- 엔티티 그대로 return하지 않고, 필요한 데이터만 꺼낸 DTO로 변환 + +- 장점: + + - 순환 참조 차단 + + - API 응답 스펙 안정성 + + - 필요한 필드만 선택 가능 + +### 4-2. Fetch Join + +- SQL JOIN을 통해 필요한 연관 엔티티를 한번에 가져옴 + +- LAZY + Fetch Join 조합이 실무 표준 + +### 4-3. Batch Fetch Size + +- Hibernate 옵션으로 지연 로딩 시 한 번에 여러 엔티티를 가져옴 + +- collection 지연 로딩에서 N+1 완화 + +### 4-4. Projection(JPA, QueryDSL) + +- 필요한 필드만 선택해서 조회 + +--- + +## 5. API & Paging에서 객체 그래프 탐색이 위험한 이유 +### 5-1. 무한 탐색 + +- 엔티티 -> 엔티티 -> 엔티티로 끝없이 이어짐 + +- FE가 필요 없는 필드까지 전부 내려가 API 응답이 비대해짐 + +### 5-2. Lazy Loading + Pagination 충돌 + +- `@OneToMany`는 기본적으로 collection + +- collection fetch join + paging 불가능 + +- 잘못 탐색하면 페이지네이션 성능 붕괴 + +### 5-3. 순환 참조 발생 + +- `Order -> Member -> Orders -> Member …` 무한 반복 + +- JSON 변환 시 에러 발생 + +--- + +## 6. 객체 그래프 탐색을 안전하게 쓰는 전략 +### 6-1. 엔티티 → DTO 계층 분리 + +- API 계층에서 엔티티 직접 노출 금지 + +- "선택된 범위의 그래프만" DTO로 변환 + +### 6-2. Service 계층에서 명확한 조회 범위 관리 + +- 필요한 연관관계만 fetch join + +- 무분별한 getter 호출 금지 + +### 6-3. Pagination 시 일대다 관계 조심 + +- 1:N fetch join은 paging 불가 + +- 해결법: + + - 엔티티 ID 기반 페이징 + 배치 사이즈 + + - slice 방식 + + - QueryDSL distinct paging + +### 6-4. Lazy 로딩 원칙 유지 + +- 모든 연관 관계 EAGER → 성능 위험 + +- 기본은 LAZY + 필요한 곳만 fetch join + +--- + +## 7. 한눈에 정리: 객체 그래프 탐색의 위험 신호 +| 위험 요인 | 설명 | +| ------------------- | ------------------------------ | +| 무한 연관 탐색 | 깊은 엔티티 그래프 return 시 JSON 무한 루프 | +| EAGER 남발 | 예측 불가 쿼리 폭발 | +| 컬렉션 Fetch Join + Pageable | JPA에서 금지된 조합 | +| LAZY 호출 반복 | N+1 문제 발생 | +| 엔티티를 직접 API 응답 | 순환 참조 + 유지보수 어려움 | + +--- + +## 8. 마무리 + +- 객체 그래프 탐색은 API 설계에서 반드시 통제해야 하는 개념 + +- 엔티티를 그대로 반환하면 N+1, 페이징 오류, 순환 참조가 발생 + +- DTO 변환 + fetch join + lazy 전략을 통해 안전하게 처리해야 함 \ No newline at end of file diff --git a/week09/mission/mission.md b/week09/mission/mission.md new file mode 100644 index 0000000..1c99c49 --- /dev/null +++ b/week09/mission/mission.md @@ -0,0 +1,268 @@ +# Mission – API & Paging 구현 정리 + +## 1. API 설계 +### 1-1. 공통 규칙 – 페이징 + +- page 쿼리 스트링 (1 기반) + + - 프론트 요청 예: `GET /api/reviews/me?page=1` + +- 내부에서는 0 기반으로 변환하여 PageRequest 생성 + +```java +int pageIndex = page - 1; // page는 1 이상이 검증된 상태 +PageRequest pageRequest = PageRequest.of(pageIndex, 10, Sort.by("createdAt").descending()); +``` + +### 1-2. 내가 작성한 리뷰 목록 + +**Endpoint** + +- `GET /api/reviews/me?page={page}` + +**기능** + +- 로그인한 사용자(회원)의 작성 리뷰 목록을 최신순으로 페이징 조회 + +- 한 페이지당 10개 + +**주요 파라미터** + +- `page` (query, required, 1 이상, `@ValidPage` 적용) + +**Response (요약)** +```json +{ + "content": [ + { + "reviewId": 1, + "storeName": "반이학생마라탕마라반", + "rating": 4.5, + "comment": "음 너무 맛있어요...", + "visitedDate": "2025-05-14", + "createdAt": "2025-05-15T10:00:00" + } + ], + "page": 1, + "size": 10, + "totalElements": 123, + "totalPages": 13 +} +``` + +**Converter 예시 (Stream + Builder)** + +```java +public List toMyReviewResponses(List reviews) { + return reviews.stream() + .map(review -> MyReviewResponse.builder() + .reviewId(review.getId()) + .storeName(review.getStore().getName()) + .rating(review.getRating()) + .comment(review.getComment()) + .visitedDate(review.getVisitedDate()) + .createdAt(review.getCreatedAt()) + .build() + ) + .toList(); +} +``` + +### 1-3. 특정 가게의 미션 목록 + +Endpoint + +- `GET /api/stores/{storeId}/missions?page={page}` + +기능 + +- 특정 가게에 대해 등록된 미션 목록을 페이징 조회 + +- 예: “12,000원 이상 식사 후 리뷰 남기기 (500P)” + +주요 파라미터 + +- storeId (path) + +- page (query, @ValidPage) + +Response (요약) +```json +{ + "content": [ + { + "missionId": 10, + "storeId": 3, + "storeName": "가게이름a", + "rewardPoint": 500, + "conditionText": "12,000원 이상의 식사를 하세요!", + "status": "AVAILABLE" + } + ], + "page": 1, + "size": 10, + "totalElements": 27, + "totalPages": 3 +} +``` + +### 1-4. 내가 진행중인 미션 목록 + +Endpoint + +- `GET /api/missions/in-progress?page={page}` + +기능 + +- 로그인한 사용자가 진행중(IN_PROGRESS) 상태인 미션 목록 조회 + +- 하단 탭 “미션” 목록 화면용 + +주요 파라미터 + +- page (query, @ValidPage) + +Response (요약) +```json +{ + "content": [ + { + "missionId": 11, + "storeName": "가게이름a", + "rewardPoint": 500, + "status": "IN_PROGRESS", + "conditionText": "12,000원 이상의 식사를 하세요!", + "expiredAt": "2025-06-30T23:59:59" + } + ], + "page": 1, + "size": 10, + "totalElements": 5, + "totalPages": 1 +} +``` + +### 1-5. 진행중인 미션 진행 완료로 변경 + +Endpoint + +- `PATCH /api/missions/{missionId}/complete?page={page}` + +기능 + +- 로그인한 사용자가 가진 진행중(IN_PROGRESS) 미션인지 검증 + +- 상태를 COMPLETED로 변경 + +- 변경된 미션의 상세 정보와, 변경 후 기준의 **진행중 미션 목록(해당 page)** 를 함께 반환 + + - “진행완료” 버튼 클릭 후 리스트를 재조회하는 플로우를 한 번에 처리 + +요청 + +- missionId (path) + +- page (query, @ValidPage) + +Response (요약) +```json +{ + "updatedMission": { + "missionId": 11, + "storeName": "가게이름a", + "rewardPoint": 500, + "status": "COMPLETED", + "completedAt": "2025-05-20T12:34:56" + }, + "inProgressMissions": { + "content": [ + ], + "page": 1, + "size": 10, + "totalElements": 4, + "totalPages": 1 + } +} +``` + +--- + +## 2. 커스텀 page 어노테이션 & 예외 처리 +### 2-1. @ValidPage 커스텀 어노테이션 + +- 역할: 쿼리 스트링으로 들어오는 page 값이 1 미만인 경우 예외 발생 + +```java +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = PageValidator.class) +public @interface ValidPage { + String message() default "page 파라미터는 1 이상이어야 합니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} +``` + +### 2-2. PageValidator +```java +public class PageValidator implements ConstraintValidator { + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value == null) return false; // null 도 에러 처리 + return value >= 1; + } +} +``` + +### 2-3. RestControllerAdvice 연동 + +- MethodArgumentNotValidException, ConstraintViolationException 등 처리 + +- 공통 에러 응답 포맷 예시: + +```json +{ + "code": "INVALID_PAGE", + "message": "page 파라미터는 1 이상이어야 합니다.", + "status": 400, + "timestamp": "2025-05-20T12:00:00" +} +``` + +--- + +## 3. Builder 패턴 사용 + +- DTO, Entity에서 `@Builder` 사용 + +- 예시 – MissionResponse + +```java +@Getter +@Builder +public class MissionResponse { + private Long missionId; + private Long storeId; + private String storeName; + private int rewardPoint; + private String conditionText; + private MissionStatus status; +} +``` +- Service/Converter에서 MissionResponse.builder() 형태로 일관되게 생성 + +- 필드가 많아도 가독성이 좋고, 선택적인 필드 세팅이 용이함 + +--- + +## 4. 마무리 + +- page 파라미터를 1 기반으로 받되, 내부에서는 0 기반으로 변환하는 로직을 명확히 분리해서 구현 + +- 커스텀 어노테이션 + RestControllerAdvice를 통해 입력 검증과 에러 응답을 표준화 + +- 리스트 변환 시 for문 대신 Stream + Builder 패턴으로 구현하여 코드 가독성과 요구사항을 동시에 만족 + +- 상태 변경 API(진행중 -> 진행완료)는 상태 변경 + 재조회를 한 번의 호출로 처리해, 실제 앱 화면 플로우와 맞게 설계 \ No newline at end of file