Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

149 changes: 94 additions & 55 deletions .idea/workspace.xml

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions Jinyong/keyword_summary/ch07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
- Page와 Slice

Page와 Slice는 Spring Data JPA에서 페이징 처리를 할 때 사용하는 반환 타입이다.

여기서 **페이징**이란? → 전체 데이터를 한번에 가져오는 게 아니라, 사용자가 요청한 만큼만 나누어 가져오는 방식이다.

ex) 리뷰가 10000개인데 한번에 가져오면 무거움 → 10개씩 나눠서 조회하는 방식

Slice는 다음 또는 이전 Slice가 있는지 알 수 있는 데이터 조각이고, Page는 전체 페이지 수나 전체 데이터 수 같은 추가 정보를 포함

Page는 전체 데이터 개수, 전체 페이지 수, 현재 페이지 번호, 다음 페이지 존재 여부 등 많은 것을 한꺼번에 제공한다. → 전체 조회 시 유리

Slice는 전체 개수까진 알 필요 없고, 다음 페이지가 있는지 여부 정도만 알려준다.

ex) 무한 스크롤에서는 굳이 전체 페이지가 필요하지 않다.

→ Page보다 가볍게 사용 가능하다.

차이점 (표)

| 구분 | Page | Slice |
| --- | --- | --- |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마크다운 표 구분선 앞에 들여쓰기가 있어 일부 렌더러에서 표로 인식되지 않을 수 있습니다. 표 문법은 | --- | --- | --- |처럼 같은 열 개수와 들여쓰기 없는 형태로 맞추는 것을 권장합니다.

| 전체 데이터 개수 | 알 수 있음 | 알 수 없음 |
| 전체 페이지 수 | 알 수 있음 | 알 수 없음 |
| 다음 페이지 여부 | 알 수 있음 | 알 수 있음 |
| count 쿼리 | 보통 실행됨 | 보통 필요 없음 |
| 적합한 상황 | 게시판, 관리자 페이지 | 무한 스크롤, 더보기 버튼 |

Page는 전체 페이지 수와 전체 데이터 개수가 필요한 경우에 사용하고, Slice는 다음 데이터가 있는지만 알면 되는 무한 스크롤 방식에 적합하다.

- Java stream API

Java stream API는 컬렉션, 배열 같은 데이터 흐름처럼 처리할 수 있게 해주는 기능이다.

ex) List에 여러 개의 데이터가 있을 때, 반복문을 작성하지 않고도 filter, map, collect 같이 메서드를 이용해 데이터를 다룰 수 있다.

**why? 왜 사용할까?**

1. 조건에 맞는 데이터만 걸러내기 위해서
2. 객체에서 필요한 값만 뽑아낼려고
3. 데이터를 다른 형태로 바꾸기 위해서
4. 결과를 다시 List로 만들기 위해

기존 for문, if문으로 쓰던 방식에서 반복문을 줄이고, 데이터 처리 의도를 더 명확하게 표현하기 위해서 쓴다

Stream API는 Spring Boot에서 **조회한 Entity 목록을 Response DTO 목록으로 바꿀 때** 많이 사용되기 때문에 Spring Boot에서 자주 사용된다.

한줄 요약

: Stream API는 컬렉션 데이터를 반복문 없이 깔끔하게 처리하기 위한 Java 기능입니다. 특히 `filter`, `map`, `toList`를 많이 사용하며, Spring Boot에서는 Entity를 DTO로 변환할 때 자주 사용된다.

- 객체 그래프 탐색

객체 그래프 탐색이란 객체가 가진 연관관계를 따라가며 다른 객체에 접근하는 것을 말한다.

(객체 지향 언어에서 참조를 사용하여 연관된 객체를 타고 들어가 데이터를 조회하는 방식)
예를 들어 `Review`가 `Member`와 연결되어 있다고 가정할 때,

```java
Review review = reviewRepository.findById(1L).get();

String nickname = review.getMember().getNickname();
```

여기서 review.getMember()를 통해 Review 객체에서 연결된 Member 객체로 이동한다.
이처럼 객체의 연관관계를 따라가며 접근하는 것을 객체 그래프 탐색이라고 볼 수 있다.

객체 그래프 탐색은 JPA의 핵심 장점 중 하나이다.

JPA는 DB 테이블을 Java 객체처럼 다룰 수 있게 해주는데, SQL 직접 작성 시 필요한 조인(Join)의 제약에서 벗어나, 논리적인 도메인 모델 구조에 따라 데이터를 조회 가능하게 해준다.

주의할 점으로는, 지연 로딩과 N + 1문제가 있는데, 연관 객체에 접근하다가 추가 쿼리가 발생할 수 있는 것을 알아야 한다.

ex) 리뷰 목록 조회 후 리뷰 작성자 닉네임을 가져올려고 하는 때에,

리뷰 목록 조회 1번

리뷰 10개의 작성자 조회 10번
= 총 11번 쿼리 ⇒ 이런 문제 발생 할 수도 있음

해결 방향으로는, 연관된 객체를 언제 함께 가져올지 신경 써야 한다.

`@EntityGraph`를 사용하여 조회 시 연관 엔티티를 함께 가져오는 방식으로 사용할 수 있다.

한줄요약

:객체 그래프 탐색은 객체의 연관관계를 따라 다른 객체에 접근하는 방식이다. JPA에서는 매우 자연스러운 방식이지만, 연관 객체 접근 시 추가 쿼리가 발생할 수 있으므로 N+1 문제를 주의해야 한다.

- @Valid vs @Validated

`@Valid`와 `@Validated`는 모두 **요청 데이터의 유효성 검증**을 위해 사용한다.

ex) 회원가입 요청에서 이메일이 비어 있거나 형식이 잘못된 경우, 컨트롤러까지 들어온 데이터를 검증해서 잘못된 요청을 막을 수 있다.(DB까지 가지 않고 controller에서 조기 진압)

`@Valid`는 필드, 메서드 파라미터, 반환값 등에 붙여서 해당 객체와 내부 속성에 정의된 제약 조건을 검증하게 해준다.

즉, `@Valid`는 주로 **Request Body DTO 검증**에 많이 사용된다.

`@Validated`는 Spring 기반 메서드 검증을 활성화하거나, 검증 그룹을 지정할 때 사용할 수 있다.

`@RequestParam`이나 `@PathVariable`에 붙은 `@Min`, `@NotNull` 같은 검증을 제대로 적용하려면 컨트롤러 클래스에 `@Validated`를 붙이는 경우가 많다.

@Valid와 @Validated 차이점(표)

| 구분 | @Valid | @Validated |
| --- | --- | --- |
| 주 사용 위치 | RequestBody DTO 검증 | 클래스, 메서드, 파라미터 검증 |
| 검증 그룹 | 기본적으로 그룹 지정 어려움 | 그룹 지정 가능 |
| 자주 쓰는 상황 | `@RequestBody @Valid DTO` | `@RequestParam`, `@PathVariable`, Service 메서드 검증 |

`@Valid`는 DTO 내부 필드 검증에 주로 사용되고, `@Validated`는 메서드 파라미터 검증이나 검증 그룹이 필요할 때 사용한다.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
@AllArgsConstructor
public enum MissionSuccessCode implements BaseSuccessCode {

OK(HttpStatus.OK,
CREATED(HttpStatus.OK,
"MISSION200_1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CREATED는 HttpStatus 중, 201번이며 HttpStatus.CREATED | "MISSION201_1"을 반환하는 것이 올바른 성공 코드입니다.

"성공적으로 미션을 생성했습니다."),
OK(HttpStatus.OK,
"MISSION200_2",
"성공적으로 미션을 조회했습니다.");

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
package com.example.umc10th.domain.mission.controller;

import com.example.umc10th.domain.mission.code.MissionSuccessCode;
import com.example.umc10th.domain.mission.dto.MissionReqDTO;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.mission.service.MissionService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class MissionController {

private final MissionService missionService;

@GetMapping("/v1/missions")
public ApiResponse<List<MissionResDTO.MissionInfo>> getMissionList(
@RequestParam Long storeId
// 가게 미션 생성(POST)
@PostMapping("/v1/stores/{storeId}/missions")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v1처럼 버전 관리를 뜻하는 Path에 대해, 보통의 버전 관리는 메소드 단위가 아닌 Controller 단위로 이루어집니다. 따라서 코드의 양을 줄일 수 있게 RequestMapping에 같이 명시하는 것을 권장드립니다.

public ApiResponse<Void> createMission(
@PathVariable Long storeId,
@RequestBody @Valid MissionReqDTO.CreateMission dto
) {
missionService.createMission(storeId, dto);

BaseSuccessCode code = MissionSuccessCode.CREATED;
return ApiResponse.onSuccess(code, null);
}

// 가게 내 미션 목록 조회 조회(GET)
@GetMapping("/v1/stores/{storeId}/missions")
public ApiResponse<MissionResDTO.Pagination<MissionResDTO.GetMission>> getMissionList(
@PathVariable Long storeId,
@RequestParam Integer pageSize,
@RequestParam Integer pageNumber,
@RequestParam(required = false) String sort
) {
BaseSuccessCode code = MissionSuccessCode.OK;
return ApiResponse.onSuccess(code, missionService.getMissionList(storeId));
return ApiResponse.onSuccess(code, missionService.getMissionList(storeId, pageSize, pageNumber, sort));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서비스 메서드 시그니처는 (storeId, pageNumber, pageSize, sort) 순서인데, 컨트롤러에서는 (storeId, pageSize, pageNumber, sort)로 전달되고 있습니다. MVC 계층 간 DTO나 파라미터 전달 순서가 어긋나면 요청한 페이지와 크기가 서로 바뀌어 조회되므로, 파라미터 순서를 맞추거나 서비스 요청 DTO로 묶어 전달하는 방식을 권장합니다.

}

// 내가 진행 중인 미션 조회
@GetMapping("/v1/users/{memberId}/missions/ongoing")
public ApiResponse<MissionResDTO.Pagination<MissionResDTO.MyOngoingMission>> getMyOngoingMissions(
@PathVariable Long memberId,
@RequestParam Integer pageSize,
@RequestParam Integer pageNumber,
@RequestParam(required = false) String sort
) {
BaseSuccessCode code = MissionSuccessCode.OK;

return ApiResponse.onSuccess(
code,
missionService.getMyOngoingMissions(memberId, pageSize, pageNumber, sort)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,68 @@
package com.example.umc10th.domain.mission.converter;

import com.example.umc10th.domain.mission.dto.MissionReqDTO;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.mission.entity.Mission;
import com.example.umc10th.domain.mission.entity.mapping.MemberMission;
import com.example.umc10th.domain.store.entity.Store;

import java.util.List;

public class MissionConverter {

public static MissionResDTO.MissionInfo toMissionInfo(Mission mission) {
return MissionResDTO.MissionInfo.builder()
// 가게 미션 생성
public static Mission toMission(
Store store,
MissionReqDTO.CreateMission dto
) {
return Mission.builder()
.store(store)
.title(dto.title())
.reward(dto.reward())
.deadline(dto.deadline())
.build();
}

// 가격 내 미션 조회
public static MissionResDTO.GetMission toGetMission(
Mission mission
){
return MissionResDTO.GetMission.builder()
.id(mission.getId())
.storeId(mission.getStore().getId())
.title(mission.getTitle())
.reward(mission.getReward())
.deadline(mission.getDeadline())
.build();
}

// 내가 진행 중인 미션 조회
public static MissionResDTO.MyOngoingMission toMyOngoingMission(
MemberMission memberMission
) {
Mission mission = memberMission.getMission();

return MissionResDTO.MyOngoingMission.builder()
.memberMissionId(memberMission.getId())
.missionId(mission.getId())
.storeId(mission.getStore().getId())
.title(mission.getTitle())
.deadline(mission.getDeadline())
.reward(mission.getReward())
.status(memberMission.getStatus())
.build();
}

// 페이지네이션 툴 생성
public static <T> MissionResDTO.Pagination<T> toPagination(
List<T> data,
Integer pageNumber,
Integer pageSize
){
return MissionResDTO.Pagination.<T>builder()
.data(data)
.pageNumber(pageNumber)
.pageSize(pageSize)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
package com.example.umc10th.domain.mission.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;

import java.time.LocalDate;

public class MissionReqDTO {
}

// 가게 미션 생성
public record CreateMission(
@NotBlank(message = "미션 제목은 필수입니다.")
String title,

@NotNull(message = "마감기한은 필수입니다.")
LocalDate deadline,

@NotNull(message = "보상 포인트는 필수입니다.")
@PositiveOrZero(message = "보상 포인트는 0 이상이어야 합니다.")
Integer reward
) {}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
package com.example.umc10th.domain.mission.dto;

import com.example.umc10th.domain.mission.entity.MemberMissionStatus;
import lombok.Builder;

import java.time.LocalDate;
import java.util.List;

public class MissionResDTO {

// 가게 내 미션 조회
@Builder
public record MissionInfo(
public record GetMission(
Long id,
Long storeId,
String title,
LocalDate deadline,
Integer reward
) {
}
) {}

// 내가 진행 중인 미션 조회
@Builder
public record MyOngoingMission(
Long memberMissionId,
Long missionId,
Long storeId,
String title,
LocalDate deadline,
Integer reward,
MemberMissionStatus status
) {}

// 페이지네이션 툴
@Builder
public record Pagination<T>(
List<T> data,
Integer pageNumber,
Integer pageSize
){}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
package com.example.umc10th.domain.mission.repository;

import com.example.umc10th.domain.mission.entity.MemberMissionStatus;
import com.example.umc10th.domain.mission.entity.mapping.MemberMission;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface MemberMissionRepository extends JpaRepository<MemberMission, Integer> {
}
public interface MemberMissionRepository extends JpaRepository<MemberMission, Long> {

@Query("SELECT mm FROM MemberMission mm " +
"WHERE mm.member.id = :memberId " +
"AND mm.status = :status")
Page<MemberMission> findMyOngoingMissions(
@Param("memberId") Long memberId,
@Param("status") MemberMissionStatus status,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.example.umc10th.domain.mission.repository;

import com.example.umc10th.domain.mission.entity.Mission;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface MissionRepository extends JpaRepository<Mission, Long> {

@Query("SELECT m FROM Mission m WHERE m.store.id = :storeId")
List<Mission> findMissionByStoreId(@Param("storeId") Long storeId);
}
Page<Mission> findMissionByStoreId(@Param("storeId") Long storeId, Pageable pageable);
}
Loading