From 6c6ab147d08e0666421557e8ea8b25512deea99a Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sun, 10 May 2026 14:43:18 +0900 Subject: [PATCH 01/25] =?UTF-8?q?docs=20:=207=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=ED=95=B5=EC=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/keyword_summary/ch07.md | 132 +++++++++++++------------ 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch07.md index b28a1fba..3481bad5 100644 --- a/Gibeom/umc10th/keyword_summary/ch07.md +++ b/Gibeom/umc10th/keyword_summary/ch07.md @@ -1,80 +1,90 @@ -- JPA란? - - JPA(Java Persistence API)란, **자바 진영에서 ORM(Object-Relational Mapping) 기술의 표준스펙.** - - 실제 구현체는 Hibernatem EclipseLink 등이 있고, 스프링 부트에선 보통 Hibernate를 기본 구현체로 사용. - -- N+1 문제란? - - 1번의 쿼리로 N개의 데이터를 가져왔는데, 연관된 데이터를 채우기 위해 추가로 N번의 쿼리가 실행되는 현상을 말함 +- Page와 Slice + + Spring Data JPA는 페이징을 위해 두가지 객체를 제공한다. Slice & Page + + DB에 저장된 데이터들을 페이지에 맞춰 몇개씩 뿌릴 건지 알려주는 것 + + - Page + - pageable 객체를 사용해 오프셋 기반 페이지네이션을 구현할 수 있는 객체. + - Slice를 상속함 + - Slice + - Page 보다 좀 더 추상적. + - 커서 기반 페이지네이션을 구현할 때 사용할 수 있는 객체 + - Response에 총 데이터 갯수는 보내지 않음 + - Pageable 객체 + - JPA가 제공하는 페이지네이션을 위한 객체 + - 페이징 정보(페이지 번호, 페이지 크기, 정렬방식 등..)를 담고 있는 인터페이스 + - 스프링 Data JPA에서 제공하는 `PageRequest` 클래스를 통해 인스턴스화 가능 + - 사용 이유 + - 모든 데이터를 한번에 뿌리게 되면 성능 저하 등 문제가 발생 + - 페이지네이션을 사용해 적절한 수의 정보만 올려주면 프론트에서 적절한 처리가능. +- Java stream API - 발생과정 + 스프링이 지원하는 함수형 프로그래밍 방식. - 1. 최초쿼리 (1) : `SELECT * FROM Member` 를 실행해 10명의 회원을 가져옴 - 2. 연관 데이터 조회(N) : 즉시 로딩 설정이 되어 있다면, JPA는 각 회원(10명)이 가진 팀(Team) 정보를 채우기 위해 회원별로 팀 조회 쿼리를 각각 날림 - 3. 결과 : 쿼리 1번으로 끝날일이 총 11번(1 + 10) 호출되어 DB 성능이 저하됨 + 함수형 프로그래밍이란? -- 지연로딩과 즉시로딩의 차이는? + - 코드가 “어떻게” 에서 “무엇을”로 포커싱이 옮겨간 프로그래밍 방식 + - 함수형 프로그래밍에서 함수는 `Int` ,`String` 같은 1급 객체로 취급. + - 이 특성을 활용하는 고차함수 : map, filter, reduce 등… + - (기존 객체의 상태를 변경하지 않고, 새로운 결과를 반환) + - 사용 이유 : 가독성, 불변성, 유지보수성, 병렬 처리성 (쓰레드 풀) - 지연 로딩 + 단점 - - 필요할 때 까지 미룬다 - - 엔티티 조회할 때 연관된 객체는 가져오지 않고, 실제 그 데이터를 사용하는 시점에 DB를 조회함 - - 동작 방식 : 조회 시점에 연관된 객체 자리에 프록시라는 가짜 객체를 넣어둠. 실제로 연관객체의 내부 데이터에 접근할 때 SQL이 실행. - - 어노테이션 : `ManyToOne(fetch=FetchType.LAZY)` - - 단점 : 연관된 데이터가 필요 없는 경우 불필요한 조인이나 쿼리를 줄여 성능 최적화 가능 + - for문 같은 단순 반복문에 익숙한 개발자들에게는 람다식과 스트림이 이해가 어려울 수 있음 + - 중단점을 걸어서 디버깅하기 어려움. + - for문 보다 오버헤드가 조금 더 큼. - 즉시 로딩 + 병렬 처리 - - 한번에 다 가져온다 - - 동작 방식 : JPA 구현체는 대개 SQL의 JOIN을 사용하여 한번의 쿼리로 모든 객체를 즉시 함께 조회 - - 어노테이션 : `@ManyToOne(fetch = FetchType.EAGER)` - - 단점 : 연관된 테이블이 많아질수록 쿼리가 복잡해지고, N + 1 문제를 발생시켜 성능 저하의 주범이 됨 + - 작업을 여러 스레드에게 분할해 병렬적으로 처리하는 방법 + - `parallelStram()` 키워드와 `ForkJoinPool()` 을 사용해 스레드 지정 가능 + - CommonPool : 미리 생성해 놓은 스레드를 모든 애플리케이션이 이용하는 방식의 풀 + - 장점 : 정해진 수의 스레드를 미리 생성해 놓고 사용하기 때문에 스레드 생성/삭제 오버헤드 적음. 적은 양의 처리에 유리 + - 단점 : 각각의 애플리케이션 마다 알맞은 처리를 제공하기 힘들 수 있음 + - (군대 배식 느낌) + - ThreadPool : 각각의 컴포넌트마다 설정할 수 있는 풀 + - 장점 : 성능 튜닝 가능 + - 메모리 관리 등 신경써야 할 것들이 있음. + - (오마카세 느낌) +- 객체 그래프 탐색 -- JPQL란? + 객체 그래프 탐색 (Object Graph Navigation) - 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리 언어(단순히 JPA SQL) + - 객체지향언어에서 참조를 사용해 연관된 객체를 타고 들어가 데이터를 조회하는 방식 + - ex) 멤버 객체에서 member.getTeam()처럼 연관관계를 통해 팀 정보를 가져오는 방식 - - 객체지향 쿼리 : 테이블 컬럼명이 아니라 엔티티의 필드(변수)이름을 사용해 쿼리를 작성함 + 특징 : 객체 간 연관관계를 통해 자유롭게 메모리상의 객체를 이동하며 탐색 - 왜 사용할까? + 장점 : SQL 직접 작성 시 필요한 조인 제약에서 벗어나 논리적인 도메인 모델 구조에 따라 데이터 조회 가능 - - JPA의 기본 메서드 (find())는 식별자로 조회할 때 유용하지만 복잡한 검색조건을 처리하기엔 한계가 있음. - - 코드는 자바객체인데 쿼리는 테이블 기준이면 싱크가 깨짐. JPQL은 자바 클래스와 필드 이름을 그대로 쓰기 때문에 훨씬 직관적 - - 이를 해결하기 위해 SQL의 장점을 가져오면서 객체 지향의 이점을 유지하는 JPQL을 사용. + JPA에서의 활용 : JPA는 연관된 객체를 처음부터 로딩하지 않고 실제 사용 시점에 조회하는 지연로딩을 사용해 객체그래프 탐색을 지원 -- Fetch Join란? + 주의점 : 객체 그래프를 무분별 탐색하는 경우, 하이버네이트 등에서 N+1 문제가 발생할 수 있음. - 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 방법 + **내 요약** - 1. 일반 Join VS Fetch Join - - **일반 Join** - - 동작 : SQL 상에서는 조인이 일어나지만, JPQL은 SELECT절에 명시된 엔티티만 조회 - - 결과 : 연관된 엔티티는 프록시가 상태로 남아있어, 나중에 접근할 때 추가쿼리가 발생 - - **Fetch Join** - - 동작 : SQL 조인을 실행하면서 연관된 엔티티의 데이터까지 한번에 SELECT해서 가져옴. - - 결과 : 연관된 엔티티가 프록시가 아닌 실제 엔티티로 채워진 상태로 조회됨. 따라서 추가쿼리 없이 데이터를 바로 사용할 수 있음. + - Store store = member.getReviewList().get(0).getStore()와 같은 코드가 있다고 가정. + 1. 멤버의 리뷰리스트를 가져오고 + 2. 0번 객체를 가져오고 + 3. 그 리뷰의 가게를 가져오는 흐름을 표현 가능. + - 여기서 문제 : 어디까지 조회가 가능한가? + - 실행시 모든 데이터를 올려둘 수는 없음. 그렇다면 사용할 때만 필요한 정보를 가져오는 방식 Lazy-Loading(지연로딩)을 사용해 `.` 을 사용해 다음 객체를 불러올때마다 데이터를 가져옴. + - 만약 객체간 연결이 없거나, 다음 데이터를 조회할 수 없다면 null이 조회되거나 오류가 날 것. +- @Valid vs @Validated -- @EntityGraph란? + @Valid - Fetch 조인을 보완하기 위해 나온 기능. 쿼리 수행 시점에 연관된 엔티티들을 함께 조회하도록 설정하는 어노테이션 + - 컨트롤러 단에서 객체의 유효성을 검사할 때 사용 + - 계층 구조 검증이 가능 (User 안에 있는 Address 객체도 검사하고 싶으면 Address 필드 위에 @Valid 를 붙여야 함) + - 한계 : 그룹화 검증이 불가능 - - 사용 이유 - - JPQL의 Fetch Join은 쿼리문 안에 조인코드를 직접 작성해야 한다는 번거로움이 있음. - - 코드의 간결함 : 스프링 부트 JPA를 사용하면 메서드 위에 어노테이션 하나만 붙여서 N + 1 문제를 해결할 수 있음. - - 쿼리분리 : 똑같은 findAll()이라도 어떤 메서드에서는 연관데이터를 가져오고, 어떤 메서드에서는 안 가져오게끔 유연히 관리 가능 - -- commit과 flush 차이점은? - - Flush (플러시) : 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업 - - 하는일 : 쓰기 지연 SQL 저장소에 쌓여있던 INSERT, UPDATE, DELETE 쿼리를 DB로 보냄 - - 영향 : 쿼리가 DB로 전송되지만, DB 입장에서는 아직 임시 쿼리 상태임. 다른 트랜잭션에선 이 데이터를 못 봄 - - 영속성 컨텍스트 유지 : 플러시가 일어나도 영속성 컨텍스트 내의 엔티티들은 지워지지 않고 그대로 유지. - - 발생시점 - 1. em.flush() 직접 호출 - 2. 트랜잭션 커밋 시 - 3. JPQL 쿼리 실행 시 - - Commit (커밋) : 데이터베이스의 트랜잭션을 종료하고 변경사항을 영구적으로 반영하는 작업 - - 하는일 : 내부적으로 em.flush()를 먼저 호출해 쿼리를 보낸 후 DB에 커밋 명령을 내림 - - 영향 : 데이터가 실제로 DB에 저장되며, 다른 사용자나 시스템에서도 변경된 데이터를 조회할 수 있게됨 - - 트랜잭션 종료 : 커밋이 완료되면 해당 트랜잭션이 완전히 종료. + @Validated + - 스프링에서 자바표준기능을 확장해 만든 어노테이션 + - 용도 : Service 나 Repository 등 Bean 계층에서 검증이 필요할 때, 혹은 그룹검증이 필요할 때 사용. + - 특징 + - 제약조건 그룹화 : “회원가입때는 이 필드 검사하고, 정보수정때는 하지마” 같은 상황을 `groups` 속성으로 지정가능 + - 클래스 레벨 선언 : 클래스 상단에 `@Validated` 어노테이션을 붙여야 해당 클래스 내부의 메서드 파라미터에 대한 검증이 작동 + - 한계 : 계층구조 검증은 직접적으로 지원하지 않아, 내부 객체에는 여전히 @Valid를 써야함. \ No newline at end of file From 46cda5e716bcf9aff4bab6163163a9fb923e1157 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Mon, 11 May 2026 18:46:27 +0900 Subject: [PATCH 02/25] =?UTF-8?q?refactor=20:=20ddl-auto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/resources/application.yml b/Gibeom/umc10th/src/main/resources/application.yml index 874078ee..f7681ea6 100644 --- a/Gibeom/umc10th/src/main/resources/application.yml +++ b/Gibeom/umc10th/src/main/resources/application.yml @@ -13,7 +13,7 @@ spring: database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ?? show-sql: true # ??? SQL ??? ??? ???? ?? ?? hibernate: - ddl-auto: create # ?????? ?? ? ?????? ???? ??? ?? + ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? properties: hibernate: format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file From 067cc435da74a31a112ee3e5686106867a8b3fe2 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Mon, 11 May 2026 18:50:18 +0900 Subject: [PATCH 03/25] =?UTF-8?q?refactor=20:=20member=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20Repository=20=EB=A9=94=EC=86=8C=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/repository/MemberMissionRepository.java | 1 + .../umc10th/domain/member/service/MemberService.java | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java index 87a8a246..8415eda3 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberMissionRepository.java @@ -13,6 +13,7 @@ public interface MemberMissionRepository extends JpaRepository { List findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus); + Page findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus, Pageable pageable); Optional findByMember_IdAndMission_Id(Long memberId, Long missionId); @Query("SELECT mm FROM MemberMission mm WHERE mm.member.id = :memberId AND mm.status = :status") diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 90561356..70b5aae5 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -17,7 +17,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import java.util.List; @@ -57,12 +56,10 @@ public List getMissionsByStatus( } else { sortInfo = Sort.by("id").descending(); } - // PageRequest 클래스를 사용해 Pageable 객체를 인스턴스화 - PageRequest pageRequest - = PageRequest.of(pageNum, pageSize, sortInfo); + PageRequest pageRequest = PageRequest.of(pageNum, pageSize, sortInfo); - List memberMissions = memberMissionRepository - .findAllByMember_IdAndStatus(memberId, status); + Page memberMissions = memberMissionRepository + .findAllByMember_IdAndStatus(memberId, status, pageRequest); List missions = memberMissions.stream() .map(MemberMission::getMission) .collect(Collectors.toList()); From b0f79375eee21b130fc6b14a181d234327b28599 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Mon, 11 May 2026 18:51:06 +0900 Subject: [PATCH 04/25] =?UTF-8?q?refactor=20:=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20Score:id=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/repository/ReviewRepository.java | 1 + .../domain/review/service/ReviewService.java | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index d9f57f18..57d1901e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -29,6 +29,7 @@ public interface ReviewRepository extends JpaRepository { Slice findReviewsByScoreCursor( @Param("memberId") Long memberId, @Param("scoreCursor") int scoreCursor, + @Param("idCursor") int idCursor, Pageable pageable ); } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 1e82b29d..21cd14e5 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -19,7 +19,7 @@ @RequiredArgsConstructor public class ReviewService { private final ReviewRepository reviewRepository; - + //id 페이지네이션 public ReviewResDTO.Pagination getMemberReviewsOrderById( Long memberId, Integer pageSize, @@ -57,8 +57,11 @@ public ReviewResDTO.Pagination getMemberReviewsOrderById reviewList = reviewRepository.findReviewsByMember_IdOrderByIdDesc(memberId, pageRequest); } - //다음 커서 계산 (마지막 요소의 id) - nextCursor = reviewList.getContent().getLast().getId() + ":" + reviewList.getContent().getLast().getId(); + List content = reviewList.getContent(); + nextCursor = (reviewList.hasNext() && !content.isEmpty()) + ? content.getLast().getId() + ":" + content.getLast().getId() + : null; + // 응답 DTO로 포장하기 return ReviewConverter.toPagination( reviewList.map(ReviewConverter::toGetReview).toList(), @@ -85,11 +88,13 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc case "score": // 커서 타입 변환 int scoreCursor = Integer.parseInt(cursorSplit[0]); + int idCursor = Integer.parseInt(cursorSplit[1]); //리뷰들 조회 & where절에 커서 값 기입 reviewList = reviewRepository.findReviewsByScoreCursor( memberId, scoreCursor, + idCursor, pageRequest); break; default: @@ -99,8 +104,11 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc //커서 없이 조회 reviewList = reviewRepository.findReviewsByMember_IdOrderByScoreDescIdDesc(memberId, pageRequest); } - //다음 커서 계산 - nextCursor = reviewList.getContent().getLast().getId() + ":" + reviewList.getContent().getLast().getId(); + List content = reviewList.getContent(); + nextCursor = (reviewList.hasNext() && !content.isEmpty()) + ? content.getLast().getScore() + ":" + content.getLast().getId() + : null; + //응답 DTO로 포장하기 return ReviewConverter.toPagination( reviewList.map(ReviewConverter::toGetReview).toList(), @@ -109,4 +117,4 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc reviewList.getSize() ); } -} +} \ No newline at end of file From 7cd6e5fc2620ef0a7be9566dcf2c0990c7c7fc48 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Mon, 11 May 2026 18:51:47 +0900 Subject: [PATCH 05/25] =?UTF-8?q?docs=20:=20Stram=20API=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/keyword_summary/ch07.md | 132 +++++++++++++------------ 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch07.md index b28a1fba..701095b1 100644 --- a/Gibeom/umc10th/keyword_summary/ch07.md +++ b/Gibeom/umc10th/keyword_summary/ch07.md @@ -1,80 +1,90 @@ -- JPA란? - - JPA(Java Persistence API)란, **자바 진영에서 ORM(Object-Relational Mapping) 기술의 표준스펙.** - - 실제 구현체는 Hibernatem EclipseLink 등이 있고, 스프링 부트에선 보통 Hibernate를 기본 구현체로 사용. - -- N+1 문제란? - - 1번의 쿼리로 N개의 데이터를 가져왔는데, 연관된 데이터를 채우기 위해 추가로 N번의 쿼리가 실행되는 현상을 말함 +- Page와 Slice + + Spring Data JPA는 페이징을 위해 두가지 객체를 제공한다. Slice & Page + + DB에 저장된 데이터들을 페이지에 맞춰 몇개씩 뿌릴 건지 알려주는 것 + + - Page + - pageable 객체를 사용해 오프셋 기반 페이지네이션을 구현할 수 있는 객체. + - Slice를 상속함 + - Slice + - Page 보다 좀 더 추상적. + - 커서 기반 페이지네이션을 구현할 때 사용할 수 있는 객체 + - Response에 총 데이터 갯수는 보내지 않음 + - Pageable 객체 + - JPA가 제공하는 페이지네이션을 위한 객체 + - 페이징 정보(페이지 번호, 페이지 크기, 정렬방식 등..)를 담고 있는 인터페이스 + - 스프링 Data JPA에서 제공하는 `PageRequest` 클래스를 통해 인스턴스화 가능 + - 사용 이유 + - 모든 데이터를 한번에 뿌리게 되면 성능 저하 등 문제가 발생 + - 페이지네이션을 사용해 적절한 수의 정보만 올려주면 프론트에서 적절한 처리가능. +- Java stream API - 발생과정 + 자바가 지원하는 함수형 프로그래밍 방식. - 1. 최초쿼리 (1) : `SELECT * FROM Member` 를 실행해 10명의 회원을 가져옴 - 2. 연관 데이터 조회(N) : 즉시 로딩 설정이 되어 있다면, JPA는 각 회원(10명)이 가진 팀(Team) 정보를 채우기 위해 회원별로 팀 조회 쿼리를 각각 날림 - 3. 결과 : 쿼리 1번으로 끝날일이 총 11번(1 + 10) 호출되어 DB 성능이 저하됨 + 함수형 프로그래밍이란? -- 지연로딩과 즉시로딩의 차이는? + - 코드가 “어떻게” 에서 “무엇을”로 포커싱이 옮겨간 프로그래밍 방식 + - 함수형 프로그래밍에서 함수는 `Int` ,`String` 같은 1급 객체로 취급. + - 이 특성을 활용하는 고차함수 : map, filter, reduce 등… + - (기존 객체의 상태를 변경하지 않고, 새로운 결과를 반환) + - 사용 이유 : 가독성, 불변성, 유지보수성, 병렬 처리성 (쓰레드 풀) - 지연 로딩 + 단점 - - 필요할 때 까지 미룬다 - - 엔티티 조회할 때 연관된 객체는 가져오지 않고, 실제 그 데이터를 사용하는 시점에 DB를 조회함 - - 동작 방식 : 조회 시점에 연관된 객체 자리에 프록시라는 가짜 객체를 넣어둠. 실제로 연관객체의 내부 데이터에 접근할 때 SQL이 실행. - - 어노테이션 : `ManyToOne(fetch=FetchType.LAZY)` - - 단점 : 연관된 데이터가 필요 없는 경우 불필요한 조인이나 쿼리를 줄여 성능 최적화 가능 + - for문 같은 단순 반복문에 익숙한 개발자들에게는 람다식과 스트림이 이해가 어려울 수 있음 + - 중단점을 걸어서 디버깅하기 어려움. + - for문 보다 오버헤드가 조금 더 큼. - 즉시 로딩 + 병렬 처리 - - 한번에 다 가져온다 - - 동작 방식 : JPA 구현체는 대개 SQL의 JOIN을 사용하여 한번의 쿼리로 모든 객체를 즉시 함께 조회 - - 어노테이션 : `@ManyToOne(fetch = FetchType.EAGER)` - - 단점 : 연관된 테이블이 많아질수록 쿼리가 복잡해지고, N + 1 문제를 발생시켜 성능 저하의 주범이 됨 + - 작업을 여러 스레드에게 분할해 병렬적으로 처리하는 방법 + - `parallelStram()` 키워드와 `ForkJoinPool()` 을 사용해 스레드 지정 가능 + - CommonPool : 미리 생성해 놓은 스레드를 모든 애플리케이션이 이용하는 방식의 풀 + - 장점 : 정해진 수의 스레드를 미리 생성해 놓고 사용하기 때문에 스레드 생성/삭제 오버헤드 적음. 적은 양의 처리에 유리 + - 단점 : 각각의 애플리케이션 마다 알맞은 처리를 제공하기 힘들 수 있음 + - (군대 배식 느낌) + - ThreadPool : 각각의 컴포넌트마다 설정할 수 있는 풀 + - 장점 : 성능 튜닝 가능 + - 메모리 관리 등 신경써야 할 것들이 있음. + - (오마카세 느낌) +- 객체 그래프 탐색 -- JPQL란? + 객체 그래프 탐색 (Object Graph Navigation) - 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리 언어(단순히 JPA SQL) + - 객체지향언어에서 참조를 사용해 연관된 객체를 타고 들어가 데이터를 조회하는 방식 + - ex) 멤버 객체에서 member.getTeam()처럼 연관관계를 통해 팀 정보를 가져오는 방식 - - 객체지향 쿼리 : 테이블 컬럼명이 아니라 엔티티의 필드(변수)이름을 사용해 쿼리를 작성함 + 특징 : 객체 간 연관관계를 통해 자유롭게 메모리상의 객체를 이동하며 탐색 - 왜 사용할까? + 장점 : SQL 직접 작성 시 필요한 조인 제약에서 벗어나 논리적인 도메인 모델 구조에 따라 데이터 조회 가능 - - JPA의 기본 메서드 (find())는 식별자로 조회할 때 유용하지만 복잡한 검색조건을 처리하기엔 한계가 있음. - - 코드는 자바객체인데 쿼리는 테이블 기준이면 싱크가 깨짐. JPQL은 자바 클래스와 필드 이름을 그대로 쓰기 때문에 훨씬 직관적 - - 이를 해결하기 위해 SQL의 장점을 가져오면서 객체 지향의 이점을 유지하는 JPQL을 사용. + JPA에서의 활용 : JPA는 연관된 객체를 처음부터 로딩하지 않고 실제 사용 시점에 조회하는 지연로딩을 사용해 객체그래프 탐색을 지원 -- Fetch Join란? + 주의점 : 객체 그래프를 무분별 탐색하는 경우, 하이버네이트 등에서 N+1 문제가 발생할 수 있음. - 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 방법 + **내 요약** - 1. 일반 Join VS Fetch Join - - **일반 Join** - - 동작 : SQL 상에서는 조인이 일어나지만, JPQL은 SELECT절에 명시된 엔티티만 조회 - - 결과 : 연관된 엔티티는 프록시가 상태로 남아있어, 나중에 접근할 때 추가쿼리가 발생 - - **Fetch Join** - - 동작 : SQL 조인을 실행하면서 연관된 엔티티의 데이터까지 한번에 SELECT해서 가져옴. - - 결과 : 연관된 엔티티가 프록시가 아닌 실제 엔티티로 채워진 상태로 조회됨. 따라서 추가쿼리 없이 데이터를 바로 사용할 수 있음. + - Store store = member.getReviewList().get(0).getStore()와 같은 코드가 있다고 가정. + 1. 멤버의 리뷰리스트를 가져오고 + 2. 0번 객체를 가져오고 + 3. 그 리뷰의 가게를 가져오는 흐름을 표현 가능. + - 여기서 문제 : 어디까지 조회가 가능한가? + - 실행시 모든 데이터를 올려둘 수는 없음. 그렇다면 사용할 때만 필요한 정보를 가져오는 방식 Lazy-Loading(지연로딩)을 사용해 `.` 을 사용해 다음 객체를 불러올때마다 데이터를 가져옴. + - 만약 객체간 연결이 없거나, 다음 데이터를 조회할 수 없다면 null이 조회되거나 오류가 날 것. +- @Valid vs @Validated -- @EntityGraph란? + @Valid - Fetch 조인을 보완하기 위해 나온 기능. 쿼리 수행 시점에 연관된 엔티티들을 함께 조회하도록 설정하는 어노테이션 + - 컨트롤러 단에서 객체의 유효성을 검사할 때 사용 + - 계층 구조 검증이 가능 (User 안에 있는 Address 객체도 검사하고 싶으면 Address 필드 위에 @Valid 를 붙여야 함) + - 한계 : 그룹화 검증이 불가능 - - 사용 이유 - - JPQL의 Fetch Join은 쿼리문 안에 조인코드를 직접 작성해야 한다는 번거로움이 있음. - - 코드의 간결함 : 스프링 부트 JPA를 사용하면 메서드 위에 어노테이션 하나만 붙여서 N + 1 문제를 해결할 수 있음. - - 쿼리분리 : 똑같은 findAll()이라도 어떤 메서드에서는 연관데이터를 가져오고, 어떤 메서드에서는 안 가져오게끔 유연히 관리 가능 - -- commit과 flush 차이점은? - - Flush (플러시) : 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업 - - 하는일 : 쓰기 지연 SQL 저장소에 쌓여있던 INSERT, UPDATE, DELETE 쿼리를 DB로 보냄 - - 영향 : 쿼리가 DB로 전송되지만, DB 입장에서는 아직 임시 쿼리 상태임. 다른 트랜잭션에선 이 데이터를 못 봄 - - 영속성 컨텍스트 유지 : 플러시가 일어나도 영속성 컨텍스트 내의 엔티티들은 지워지지 않고 그대로 유지. - - 발생시점 - 1. em.flush() 직접 호출 - 2. 트랜잭션 커밋 시 - 3. JPQL 쿼리 실행 시 - - Commit (커밋) : 데이터베이스의 트랜잭션을 종료하고 변경사항을 영구적으로 반영하는 작업 - - 하는일 : 내부적으로 em.flush()를 먼저 호출해 쿼리를 보낸 후 DB에 커밋 명령을 내림 - - 영향 : 데이터가 실제로 DB에 저장되며, 다른 사용자나 시스템에서도 변경된 데이터를 조회할 수 있게됨 - - 트랜잭션 종료 : 커밋이 완료되면 해당 트랜잭션이 완전히 종료. + @Validated + - 스프링에서 자바표준기능을 확장해 만든 어노테이션 + - 용도 : Service 나 Repository 등 Bean 계층에서 검증이 필요할 때, 혹은 그룹검증이 필요할 때 사용. + - 특징 + - 제약조건 그룹화 : “회원가입때는 이 필드 검사하고, 정보수정때는 하지마” 같은 상황을 `groups` 속성으로 지정가능 + - 클래스 레벨 선언 : 클래스 상단에 `@Validated` 어노테이션을 붙여야 해당 클래스 내부의 메서드 파라미터에 대한 검증이 작동 + - 한계 : 계층구조 검증은 직접적으로 지원하지 않아, 내부 객체에는 여전히 @Valid를 써야함. \ No newline at end of file From ed3252369b5aa75dfd662c03cfa5962ee68476a4 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 12 May 2026 20:02:09 +0900 Subject: [PATCH 06/25] =?UTF-8?q?refactor=20:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20Void=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/controller/MissionController.java | 8 +++++--- .../domain/mission/converter/MissionConverter.java | 11 +++++++++++ .../umc10th/domain/mission/dto/MissionReqDTO.java | 3 ++- .../umc10th/domain/mission/dto/MissionResDTO.java | 10 ++++++++++ .../domain/mission/service/MissionService.java | 12 ++++-------- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 36e7c085..03f5f536 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -11,6 +11,7 @@ import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -33,9 +34,10 @@ public ApiResponse writeReview( BaseSuccessCode code = ReviewSuccessCode.WRITE_SUCCESS; return ApiResponse.onSuccess(code, missionService.writeReview(memberId, missionId, dto)); } + //가게 미션 생성 @PostMapping("v1/stores/{storeId}/missions") - public ApiResponse createMission( + public ApiResponse createMission( @PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission dto ){ @@ -43,9 +45,9 @@ public ApiResponse createMission( return ApiResponse.onSuccess(code, missionService.createMission(storeId, dto)); } - //가게 미션들 조회 + //가게 미션들 조회 (오프셋 기반) @GetMapping("v1/store/{storeId}/missions") - public ApiResponse> getMissions( + public ApiResponse> getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 938a879a..ec65420a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -59,4 +59,15 @@ public static MissionResDTO.Pagination toPagination( .pageSize(pageSize) .build(); } + + public static MissionResDTO.CreateMissionResult toCreateMissionResult( + Mission mission + ){ + return MissionResDTO.CreateMissionResult.builder() + .conditional(mission.getConditional()) + .point(mission.getPoint()) + .missionId(mission.getId()) + .deadline(mission.getDeadline()) + .build(); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index a9319844..73156036 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.mission.dto; import com.example.umc10th.domain.member.enums.MissionStatus; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; @@ -12,7 +13,7 @@ public record CreateMission( LocalDate deadLine, @NotNull(message = "미션 성공 포인트는 필수입니다.") Integer point, - @NotNull(message = "조건은 빈칸일 수 없습니다.") + @NotBlank(message = "조건은 빈칸일 수 없습니다.") String conditional ){} } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 0c075bf1..e25087af 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.member.enums.MissionStatus; import lombok.Builder; +import java.time.LocalDate; import java.util.List; public class MissionResDTO { @@ -29,4 +30,13 @@ public record Pagination( Integer pageNumber, Integer pageSize ){} + + //생성된 미션 정보 반환 DTO + @Builder + public record CreateMissionResult( + Long missionId, + String conditional, + Integer point, + LocalDate deadline + ){} } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 124d440c..d4692a8b 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -69,7 +69,7 @@ public ReviewResDTO.WriteReviewResultDto writeReview(Long memberId, Long mission //가게 미션 생성 @Transactional - public Void createMission( + public MissionResDTO.CreateMissionResult createMission( Long storeId, MissionReqDTO.CreateMission dto ){ @@ -81,11 +81,11 @@ public Void createMission( //미션 DB에 미션 저장 missionRepository.save(mission); - return null; + return MissionConverter.toCreateMissionResult(mission); } //가게 미션 조회 - public MissionResDTO.Pagination getMissions( + public Page getMissions( Long storeId, Integer pageSize, Integer pageNumber, @@ -105,11 +105,7 @@ public MissionResDTO.Pagination getMissions( //가게 내 미션들 조회 Page missionList = missionRepository.findAllByStore_Id(storeId, pageRequest); - return MissionConverter.toPagination( - missionList.map(MissionConverter::toGetMission).toList(), - missionList.getNumber(), - missionList.getSize() - ); + return missionList.map(MissionConverter::toGetMission); } //유저가 진행중인 미션 조회하기 From 04c0f1dba379f3cafe5b9913a7b9a11fc98bc28f Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 12 May 2026 20:28:10 +0900 Subject: [PATCH 07/25] =?UTF-8?q?refactor=20:=20ApiResponse=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=8C=80=EC=8B=A0=20ResponseEntity=20=EB=9E=98?= =?UTF-8?q?=ED=95=91=20=ED=9B=84=20=EB=B0=98=ED=99=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 23 ++++++++++---- .../mission/controller/MissionController.java | 30 ++++++++++++++----- .../review/controller/ReviewController.java | 8 +++-- .../store/controller/StoreController.java | 12 +++++--- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 2343770c..c61e09c2 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -8,6 +8,8 @@ import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -21,26 +23,32 @@ public class MemberController { private final MemberService memberService; //마이페이지 @GetMapping("/me") - public ApiResponse getInfo( + public ResponseEntity> getInfo( @AuthenticationPrincipal Long memberId ){ BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getInfo(memberId)); + MemberResDTO.GetInfo result = memberService.getInfo(memberId); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } // 홈화면 @GetMapping("/home") - public ApiResponse getHome( + public ResponseEntity> getHome( @AuthenticationPrincipal Long memberId, @RequestParam(defaultValue = "0") int page ){ BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getHome(memberId, page)); + MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } // 진행중/완료 미션 목록 조회 @GetMapping("/missions") - public ApiResponse> getMissionsByStatus( + public ResponseEntity>> getMissionsByStatus( @AuthenticationPrincipal Long memberId, @RequestParam MissionStatus status, @RequestParam Integer pageSize, @@ -48,7 +56,10 @@ public ApiResponse> getMissionsByStatus( @RequestParam (required = false) String sort ){ BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort)); + List result = memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 03f5f536..0aa3f8f1 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -12,6 +12,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -27,45 +29,57 @@ public class MissionController { //리뷰 작성 (완료된 미션에 한해서) @PostMapping("v1/missions/{missionId}/reviews") - public ApiResponse writeReview( + public ResponseEntity> writeReview( @PathVariable Long missionId, @AuthenticationPrincipal Long memberId, @RequestBody ReviewReqDTO.WriteReviewDto dto) { BaseSuccessCode code = ReviewSuccessCode.WRITE_SUCCESS; - return ApiResponse.onSuccess(code, missionService.writeReview(memberId, missionId, dto)); + ReviewResDTO.WriteReviewResultDto result = missionService.writeReview(memberId, missionId, dto); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } //가게 미션 생성 @PostMapping("v1/stores/{storeId}/missions") - public ApiResponse createMission( + public ResponseEntity> createMission( @PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission dto ){ MissionSuccessCode code = MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code, missionService.createMission(storeId, dto)); + MissionResDTO.CreateMissionResult result = missionService.createMission(storeId, dto); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } //가게 미션들 조회 (오프셋 기반) @GetMapping("v1/store/{storeId}/missions") - public ApiResponse> getMissions( + public ResponseEntity>> getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam(required = false) String sort ){ BaseSuccessCode code = MissionSuccessCode.OK; - return ApiResponse.onSuccess(code, missionService.getMissions(storeId, pageSize, pageNumber, sort)); + Page result = missionService.getMissions(storeId, pageSize, pageNumber, sort); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } //내가 진행중인 미션 조회하기 @GetMapping("v1/members/{memberId}/missions") - public ApiResponse> getMemberMissions( + public ResponseEntity>> getMemberMissions( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, @RequestParam(required = false) String sort ){ BaseSuccessCode code = MissionSuccessCode.OK; - return ApiResponse.onSuccess(code, missionService.getMemberMissions(memberId, pageSize, pageNumber, sort)); + MissionResDTO.Pagination result = missionService.getMemberMissions(memberId, pageSize, pageNumber, sort); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 351d0744..e78133cb 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.apache.coyote.Request; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,14 +23,17 @@ public class ReviewController { //유저의 모든 리뷰 가져오기 (Cursor) @GetMapping("/v1/members/{memberId}/reviews") - public ApiResponse> getMemberReviews( + public ResponseEntity>> getMemberReviews( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam String cursor, @RequestParam String query ) { BaseSuccessCode code = ReviewSuccessCode.OK; - return ApiResponse.onSuccess(code, reviewService.getMemberReviewsOrderById(memberId,pageSize, cursor, query)); + ReviewResDTO.Pagination result = reviewService.getMemberReviewsOrderByScore(memberId, pageSize, cursor, query); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 386d4589..f1c27841 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -6,6 +6,7 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -20,11 +21,14 @@ public class StoreController { private final StoreService storeService; - @GetMapping("/v1/stores") // [수정] 경로 앞에 / 추가 - public ApiResponse> getStoreList( - @RequestParam Long regionId // [수정] Region 엔티티 → Long + @GetMapping("/v1/stores") + public ResponseEntity>> getStoreList( + @RequestParam Long regionId ){ BaseSuccessCode code = StoreSuccessCode.OK; - return ApiResponse.onSuccess(code, storeService.getStoreList(regionId)); + List result = storeService.getStoreList(regionId); + return ResponseEntity + .status(code.getStatus()) + .body(ApiResponse.onSuccess(code, result)); } } From c9f6174fc96819e3ca69e3ee44c4f142eb41bb9e Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 12 May 2026 21:11:20 +0900 Subject: [PATCH 08/25] =?UTF-8?q?refactor=20:=20controller=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20review=20=EC=BB=A4?= =?UTF-8?q?=EC=84=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/controller/MissionController.java | 2 +- .../umc10th/domain/review/repository/ReviewRepository.java | 6 +++--- .../umc10th/domain/review/service/ReviewService.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 0aa3f8f1..b784e565 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -54,7 +54,7 @@ public ResponseEntity> createMis } //가게 미션들 조회 (오프셋 기반) - @GetMapping("v1/store/{storeId}/missions") + @GetMapping("v1/stores/{storeId}/missions") public ResponseEntity>> getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 57d1901e..782cc7b9 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -14,7 +14,7 @@ public interface ReviewRepository extends JpaRepository { Page findByMember_Id(Long memberId, Pageable pageable); - List member(Member member); + List findByMember(Member member); // id 순 페이징 Slice findReviewsByMember_IdAndIdLessThanOrderByIdDesc(Long memberId, Long idCursor, Pageable pageable); @@ -28,8 +28,8 @@ public interface ReviewRepository extends JpaRepository { "ORDER BY r.score DESC, r.id DESC") Slice findReviewsByScoreCursor( @Param("memberId") Long memberId, - @Param("scoreCursor") int scoreCursor, - @Param("idCursor") int idCursor, + @Param("scoreCursor") long scoreCursor, + @Param("idCursor") long idCursor, Pageable pageable ); } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 21cd14e5..f6c18235 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -87,8 +87,8 @@ public ReviewResDTO.Pagination getMemberReviewsOrderBySc switch (query.toLowerCase()) { case "score": // 커서 타입 변환 - int scoreCursor = Integer.parseInt(cursorSplit[0]); - int idCursor = Integer.parseInt(cursorSplit[1]); + long scoreCursor = Long.parseLong(cursorSplit[0]); + long idCursor = Long.parseLong(cursorSplit[1]); //리뷰들 조회 & where절에 커서 값 기입 reviewList = reviewRepository.findReviewsByScoreCursor( From f4553da44a78a0c0e49196b2f91510e7ca2bf9ee Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 12 May 2026 21:16:12 +0900 Subject: [PATCH 09/25] =?UTF-8?q?docs=20:=20FeedBack=20=EC=A1=B0=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/feedback_summary/FeedBack_Study | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Gibeom/umc10th/feedback_summary/FeedBack_Study diff --git a/Gibeom/umc10th/feedback_summary/FeedBack_Study b/Gibeom/umc10th/feedback_summary/FeedBack_Study new file mode 100644 index 00000000..04d93ed2 --- /dev/null +++ b/Gibeom/umc10th/feedback_summary/FeedBack_Study @@ -0,0 +1,26 @@ +ResponseEntity +- 정의 : 스프링 프레임워크에서 제공하는 클래스. +- Http요청 또는 응답에 해당하는 HttpStatus, HttpHeader와 HttpBody를 포함하는 클래스 +- Http 응답의 주권을 가짐. 커스텀 가능 +- 상속 구현 클래스 (RequestBody, ResponseEntity) + +기존 상태 +- 클라이언트 측에서는 ApiResponse로 응답시 정해진 응답 (예 : 200이면 200, 500이면 500) +- 현재 ApiResponse 클래스는 정적 팩토리 메서드 형식. +- ApiResponse.java + ``` + //성공한 경우 + public static ApiResponse onSuccess(BaseSuccessCode code, T result){ + return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + } + ``` +기존 방식의 한계 +- 클라이언트 측은 먼저 http status를 확인하고 바디를 확인함 +- 네트워크 불일치 : ApiResponse만 반환하면 내부 로직이 실패하더라도 Http 상태코드는 기본(200)으로 나가는 경우가 많았음 +- 상세한 정보가 Json바디 안에 숨겨져 있어 클라이언트가 Http 표준방식으로 1차 판단을 내리기 어려웠음 +- 단순한 소통 : 200, 500 위주의 단순한 소통만 가능했고, 201, 204 등 을 활용 못했음 + +Why? +- API 사용자(프론트엔드 등등)는 응답바디 안의 Json뿐 아닌 브라우저나 앱이 수신하는 Status Code를 보고 1차 판단을 내림. +- 유연성 : ResponseEntity를 래핑해 감싼 응답을 보낸다면 커스텀 응답, 쿠키, 헤더 등 표준화된 응답을 유연하게 보낼 수 있음. +- ApiResponse의 데이터 규격을 유지하고 ResponseEntity로 감싸 네트워크 수준 상태코드로 명시적 관리 \ No newline at end of file From d3309f954792fa41ef8f1525504f2f4de82c44ee Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:20:16 +0900 Subject: [PATCH 10/25] =?UTF-8?q?refactor=20:=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/controller/MemberController.java | 5 ++--- .../umc10th/domain/member/converter/MemberConverter.java | 4 ++++ .../umc10th/domain/mission/controller/MissionController.java | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index c61e09c2..a1ebdeda 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -39,11 +39,10 @@ public ResponseEntity> getHome( @AuthenticationPrincipal Long memberId, @RequestParam(defaultValue = "0") int page ){ - BaseSuccessCode code = MemberSuccessCode.OK; MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page); return ResponseEntity - .status(code.getStatus()) - .body(ApiResponse.onSuccess(code, result)); + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); } // 진행중/완료 미션 목록 조회 diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index a7412b8f..e4af073a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -6,7 +6,11 @@ import com.example.umc10th.domain.member.entity.mapping.MemberMission; import org.springframework.data.domain.Page; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index b784e565..240ba037 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -26,7 +26,6 @@ public class MissionController { private final MissionService missionService; - //리뷰 작성 (완료된 미션에 한해서) @PostMapping("v1/missions/{missionId}/reviews") public ResponseEntity> writeReview( From 1cc4127874b99845f4ef8ff43519121c3583f55c Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:20:43 +0900 Subject: [PATCH 11/25] =?UTF-8?q?feat=20:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle index 2314bead..183ef1c7 100644 --- a/Gibeom/umc10th/build.gradle +++ b/Gibeom/umc10th/build.gradle @@ -31,6 +31,10 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' From 54bdcd174ba74bd1cb480840c8c4b3eec0a0460b Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:21:09 +0900 Subject: [PATCH 12/25] =?UTF-8?q?feat=20:=20SecurityConfig=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 6b8c1df0..13748aa5 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -5,19 +5,49 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { + private final String[] allowUris = { + //swagger 허용 + // 자유롭게 이용할 수 있는 주소 (비로그인) + "swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + //로그인 + "/auth/**", + }; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() + .csrf(AbstractHttpConfigurer::disable) // csrf 방어기능 끄기 + // 위의 허용한 경로로 요청이 들어오면 모두 허용 + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + //스프링 시큐리티가 제공하는 기본 로그인 폼을 사용 + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui.html", true) + .permitAll() + //로그아웃 주소로 요청을 보내면 로그아웃 처리를 진행 + ).logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 로그인 페이지로 리다이렉트 + .permitAll() ); return http.build(); } -} + + //해시 알고리즘을 이용해 함호화된 Bcrypt 알고리즘 객체 반환 + //알고리즘은 실행때마다 매번 다른 결과물 생성 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file From eb834fdb76bc016db420ff02a05f5dc6a9388a78 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:30:49 +0900 Subject: [PATCH 13/25] =?UTF-8?q?refactor=20:=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/feedback_summary/FeedBack_Study | 26 ------------------- .../keyword_summary/{ch07.md => ch08.md} | 0 .../apiPayload/code/GeneralErrorCode.java | 7 ++--- 3 files changed, 4 insertions(+), 29 deletions(-) delete mode 100644 Gibeom/umc10th/feedback_summary/FeedBack_Study rename Gibeom/umc10th/keyword_summary/{ch07.md => ch08.md} (100%) diff --git a/Gibeom/umc10th/feedback_summary/FeedBack_Study b/Gibeom/umc10th/feedback_summary/FeedBack_Study deleted file mode 100644 index 04d93ed2..00000000 --- a/Gibeom/umc10th/feedback_summary/FeedBack_Study +++ /dev/null @@ -1,26 +0,0 @@ -ResponseEntity -- 정의 : 스프링 프레임워크에서 제공하는 클래스. -- Http요청 또는 응답에 해당하는 HttpStatus, HttpHeader와 HttpBody를 포함하는 클래스 -- Http 응답의 주권을 가짐. 커스텀 가능 -- 상속 구현 클래스 (RequestBody, ResponseEntity) - -기존 상태 -- 클라이언트 측에서는 ApiResponse로 응답시 정해진 응답 (예 : 200이면 200, 500이면 500) -- 현재 ApiResponse 클래스는 정적 팩토리 메서드 형식. -- ApiResponse.java - ``` - //성공한 경우 - public static ApiResponse onSuccess(BaseSuccessCode code, T result){ - return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); - } - ``` -기존 방식의 한계 -- 클라이언트 측은 먼저 http status를 확인하고 바디를 확인함 -- 네트워크 불일치 : ApiResponse만 반환하면 내부 로직이 실패하더라도 Http 상태코드는 기본(200)으로 나가는 경우가 많았음 -- 상세한 정보가 Json바디 안에 숨겨져 있어 클라이언트가 Http 표준방식으로 1차 판단을 내리기 어려웠음 -- 단순한 소통 : 200, 500 위주의 단순한 소통만 가능했고, 201, 204 등 을 활용 못했음 - -Why? -- API 사용자(프론트엔드 등등)는 응답바디 안의 Json뿐 아닌 브라우저나 앱이 수신하는 Status Code를 보고 1차 판단을 내림. -- 유연성 : ResponseEntity를 래핑해 감싼 응답을 보낸다면 커스텀 응답, 쿠키, 헤더 등 표준화된 응답을 유연하게 보낼 수 있음. -- ApiResponse의 데이터 규격을 유지하고 ResponseEntity로 감싸 네트워크 수준 상태코드로 명시적 관리 \ No newline at end of file diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch08.md similarity index 100% rename from Gibeom/umc10th/keyword_summary/ch07.md rename to Gibeom/umc10th/keyword_summary/ch08.md diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java index b05e24f2..87c93a9c 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java @@ -10,9 +10,6 @@ public enum GeneralErrorCode implements BaseErrorCode{ BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다."), - UNAUTHORTIZED(HttpStatus.UNAUTHORIZED, - "COMMON401_1", - "인증되지 않았습니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403_1", "접근이 금지되었습니다."), @@ -29,6 +26,10 @@ public enum GeneralErrorCode implements BaseErrorCode{ "COMMON500", "서버 내부 오류입니다." ), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_2", + "승인되지 않았습니다." + ) ; private final HttpStatus status; private final String code; From 0cf52463d164d719fdc2257c44e6a48fd5d6d78a Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:31:42 +0900 Subject: [PATCH 14/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index a1ebdeda..941d296e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.controller; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.enums.MissionStatus; import com.example.umc10th.domain.member.service.MemberService; @@ -7,8 +8,9 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.entity.AuthMember; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -17,12 +19,12 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/members") +@RequestMapping("/api") public class MemberController { private final MemberService memberService; //마이페이지 - @GetMapping("/me") + @GetMapping("/v1/members/me") public ResponseEntity> getInfo( @AuthenticationPrincipal Long memberId ){ @@ -34,19 +36,19 @@ public ResponseEntity> getInfo( } // 홈화면 - @GetMapping("/home") + @GetMapping("/v1/members/home") public ResponseEntity> getHome( - @AuthenticationPrincipal Long memberId, + @AuthenticationPrincipal AuthMember authMember, @RequestParam(defaultValue = "0") int page ){ - MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page); + MemberResDTO.HomeResultDto result = memberService.getHome(authMember.getMember().getId(), page); return ResponseEntity .status(MemberSuccessCode.OK.getStatus()) .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); } // 진행중/완료 미션 목록 조회 - @GetMapping("/missions") + @GetMapping("/v1/members/missions") public ResponseEntity>> getMissionsByStatus( @AuthenticationPrincipal Long memberId, @RequestParam MissionStatus status, @@ -61,4 +63,15 @@ public ResponseEntity>> getMissionsBy .body(ApiResponse.onSuccess(code, result)); } + + //회원가입 + @PostMapping("/auth/sign-up") + public ResponseEntity> signUp( + @RequestBody @Valid MemberReqDTO.SignUp req + ){ + memberService.signUp(req); + return ResponseEntity + .status(MemberSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null)); + } } From eb99d06f8d764e0e69a956f56c9ecd0117833a41 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:32:13 +0900 Subject: [PATCH 15/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 70b5aae5..0d9b76f1 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -17,6 +17,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -27,6 +28,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final PasswordEncoder passwordEncoder; public MemberResDTO.GetInfo getInfo(Long memberId) { Member member = memberRepository.findById(memberId) @@ -65,4 +67,17 @@ public List getMissionsByStatus( .collect(Collectors.toList()); return MissionConverter.toMissionDtoList(missions); } + + public void signUp(MemberReqDTO.SignUp req) { + //닉네임 혹은 이메일이 이미 존재할 때 + if(memberRepository.existsByEmail(req.email())){ + throw new MemberException(MemberErrorCode.EMAIL_DUPLICATED); + } else if (memberRepository.existsByNickname(req.nickname())){ + throw new MemberException(MemberErrorCode.NICKNAME_DUPLICATED); + } + + String encodedPassword = passwordEncoder.encode(req.password()); + Member member = MemberConverter.toMember(req, encodedPassword); + memberRepository.save(member); + } } From a82319d7a9e35b24516ecab4946864eee274abab Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:32:39 +0900 Subject: [PATCH 16/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/converter/MemberConverter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index e4af073a..e1297d29 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -48,4 +48,16 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM .status(memberMission.getStatus().name()) .build(); } + + public static Member toMember(MemberReqDTO.SignUp req, String emcodedPasssword){ + return Member.builder() + .name(req.name()) + .password(emcodedPasssword) + .phoneNumber(req.phoneNumber()) + .email(req.email()) + .gender(req.gender()) + .userPoint(0) + .nickname(req.nickname()) + .build(); + } } From acbca07637840528a255f4639be7db967770e467 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:33:02 +0900 Subject: [PATCH 17/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/dto/MemberReqDTO.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index 943a141f..047e254a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,5 +1,24 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Gender; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + public class MemberReqDTO { + public record SignUp ( + @NotBlank + String name, + @NotBlank + String nickname, + @Email @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String phoneNumber, + Gender gender + ){} + } From 1d11a2d3781b39b6e68c13bcbc64025f05129b18 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:33:50 +0900 Subject: [PATCH 18/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Boolean=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/repository/MemberRepository.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 054ad34f..9813a157 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -3,5 +3,10 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + Boolean existsByEmail(String email); + Optional findByEmail(String email); + Boolean existsByNickname(String nickname); } From 0fda854b82c77fbe54616ab13a17c0759b1b7c1a Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:52:14 +0900 Subject: [PATCH 19/25] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=B1=EA=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/entity/Member.java | 3 +++ .../domain/member/exception/code/MemberSuccessCode.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 1f7fca6b..51df7d92 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -46,6 +46,9 @@ public class Member extends BaseEntity{ @Column(name = "email") private String email; + @Column(name = "password", nullable = false) + private String password; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "region_id") private Region region; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 5a5c3362..2d76db0b 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -11,7 +11,7 @@ public enum MemberSuccessCode implements BaseSuccessCode { "MEMBER200_1", "성공적으로 유저를 조회했습니다."), - SIGNUP_SUCCESS(HttpStatus.CREATED, + CREATED(HttpStatus.CREATED, "MEMBER_201_1", "회원가입이 성공적으로 완료되었습니다.") ; From 8bf2421fcf82ec5cbaa2911bf24d4703e3589135 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:53:14 +0900 Subject: [PATCH 20/25] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/exception/code/MemberErrorCode.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 98c24940..3ffc9cef 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,15 @@ public enum MemberErrorCode implements BaseErrorCode { "COMMON404_1", "해당 사용자를 찾을 수 없습니다." ), + EMAIL_DUPLICATED(HttpStatus.NOT_FOUND, + "COMMON404_2", + "해당 사용자를 찾을 수 없습니다." + ), + NICKNAME_DUPLICATED(HttpStatus.CONFLICT, + "COMMON409_2", + "이미 존재하는 닉네임입니다." + ) ; private final HttpStatus status; private final String code; From a4c5eb1cbfd60fc7dfa74aad0e141b7bfa578e28 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:53:49 +0900 Subject: [PATCH 21/25] =?UTF-8?q?feat=20:=20SecurityConfig=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 13748aa5..d28d0edf 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,10 +1,13 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.CustomAccessDenied; +import com.example.umc10th.global.security.CustomEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -15,32 +18,36 @@ public class SecurityConfig { private final String[] allowUris = { //swagger 허용 // 자유롭게 이용할 수 있는 주소 (비로그인) - "swagger-ui/**", + "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", //로그인 - "/auth/**", + "/api/auth/**", }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) // csrf 방어기능 끄기 - // 위의 허용한 경로로 요청이 들어오면 모두 허용 - .authorizeHttpRequests(requests -> requests + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests ->requests .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - //스프링 시큐리티가 제공하는 기본 로그인 폼을 사용 .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui.html", true) + .defaultSuccessUrl("/swagger-ui/index.html", true) .permitAll() - //로그아웃 주소로 요청을 보내면 로그아웃 처리를 진행 - ).logout(logout -> logout + ) + .logout(logout -> logout .logoutUrl("/logout") - .logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 로그인 페이지로 리다이렉트 + .logoutSuccessUrl("/login?logout") .permitAll() - ); + ) + //예외 상황 핸들러 + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) + ) + ; return http.build(); } @@ -50,4 +57,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} \ No newline at end of file + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } +} From 719aaabd81471ec4917e737bc4ba09082c7c180e Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:54:16 +0900 Subject: [PATCH 22/25] =?UTF-8?q?docs=20:=208=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/keyword_summary/ch08.md | 102 +++++++------------------ 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/Gibeom/umc10th/keyword_summary/ch08.md b/Gibeom/umc10th/keyword_summary/ch08.md index 701095b1..737accb1 100644 --- a/Gibeom/umc10th/keyword_summary/ch08.md +++ b/Gibeom/umc10th/keyword_summary/ch08.md @@ -1,90 +1,42 @@ -- Page와 Slice +- Spring Security가 무엇인가? - Spring Data JPA는 페이징을 위해 두가지 객체를 제공한다. Slice & Page + 스프링 기반 애플리케이션의 보안을 담당하는 강력하고 포괄적인 하위 프레임워크 - DB에 저장된 데이터들을 페이지에 맞춰 몇개씩 뿌릴 건지 알려주는 것 + 복잡한 보안 로직을 직접 구현할 필요 없이 표준화된 필터 기반의 설정을 통해 시스템을 안전하게 보호한다. - - Page - - pageable 객체를 사용해 오프셋 기반 페이지네이션을 구현할 수 있는 객체. - - Slice를 상속함 - - Slice - - Page 보다 좀 더 추상적. - - 커서 기반 페이지네이션을 구현할 때 사용할 수 있는 객체 - - Response에 총 데이터 갯수는 보내지 않음 - - Pageable 객체 - - JPA가 제공하는 페이지네이션을 위한 객체 - - 페이징 정보(페이지 번호, 페이지 크기, 정렬방식 등..)를 담고 있는 인터페이스 - - 스프링 Data JPA에서 제공하는 `PageRequest` 클래스를 통해 인스턴스화 가능 - - 사용 이유 - - 모든 데이터를 한번에 뿌리게 되면 성능 저하 등 문제가 발생 - - 페이지네이션을 사용해 적절한 수의 정보만 올려주면 프론트에서 적절한 처리가능. -- Java stream API +- 인증(Authentication)vs 인가(Authorization) - 자바가 지원하는 함수형 프로그래밍 방식. + 비슷해보이지만 서로 다른 개념이다. - 함수형 프로그래밍이란? + 인증 (Authentication) - - 코드가 “어떻게” 에서 “무엇을”로 포커싱이 옮겨간 프로그래밍 방식 - - 함수형 프로그래밍에서 함수는 `Int` ,`String` 같은 1급 객체로 취급. - - 이 특성을 활용하는 고차함수 : map, filter, reduce 등… - - (기존 객체의 상태를 변경하지 않고, 새로운 결과를 반환) - - 사용 이유 : 가독성, 불변성, 유지보수성, 병렬 처리성 (쓰레드 풀) + - 본인확인 절차 + - 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정 - 단점 + 인가 (Authorization) - - for문 같은 단순 반복문에 익숙한 개발자들에게는 람다식과 스트림이 이해가 어려울 수 있음 - - 중단점을 걸어서 디버깅하기 어려움. - - for문 보다 오버헤드가 조금 더 큼. + - 권한확인 절차 + - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정 - 병렬 처리 +- Stateful vs Stateless - - 작업을 여러 스레드에게 분할해 병렬적으로 처리하는 방법 - - `parallelStram()` 키워드와 `ForkJoinPool()` 을 사용해 스레드 지정 가능 - - CommonPool : 미리 생성해 놓은 스레드를 모든 애플리케이션이 이용하는 방식의 풀 - - 장점 : 정해진 수의 스레드를 미리 생성해 놓고 사용하기 때문에 스레드 생성/삭제 오버헤드 적음. 적은 양의 처리에 유리 - - 단점 : 각각의 애플리케이션 마다 알맞은 처리를 제공하기 힘들 수 있음 - - (군대 배식 느낌) - - ThreadPool : 각각의 컴포넌트마다 설정할 수 있는 풀 - - 장점 : 성능 튜닝 가능 - - 메모리 관리 등 신경써야 할 것들이 있음. - - (오마카세 느낌) -- 객체 그래프 탐색 + 논점 : 서버가 클라이언트의 세션 정보를 기억하는가? - 객체 그래프 탐색 (Object Graph Navigation) + Stateful(상태유지) : 세션 정보를 기억함 - - 객체지향언어에서 참조를 사용해 연관된 객체를 타고 들어가 데이터를 조회하는 방식 - - ex) 멤버 객체에서 member.getTeam()처럼 연관관계를 통해 팀 정보를 가져오는 방식 + Stateless(토큰 기반) : 서버가 상태를 유지하지 않으므로 요청에 포함된 토큰(JWT)로 검증 - 특징 : 객체 간 연관관계를 통해 자유롭게 메모리상의 객체를 이동하며 탐색 + | 구분 | Stateful | Stateless | + | --- | --- | --- | + | 특징 | 서버가 세션 저장소에 로그인 상태 유지 | 서버가 상태를 유지하지 않음, 요청에 포함된 토큰으로 검증 | + | 인증방식 | JSESSION쿠키를 통해 서버 메모리/DB의 세션 조회 | 매 요청시 HTTP헤더에 토큰을 담아서 전송 (Authorization:Bearer) | + | 서버 확장 | 세션 불일치 문제 발생 가능 | 각 요청이 독립적이므로 서버 증설에 유리 | + | 메모리 및 비용 | 동시접속자가 많을수록 서버 세션 메모리 소비 증가 | 토큰 검증 연산이 필요하며, 서버 메모리 사용량은 적음 | + | 주요 활용처 | 전톤적인 웹 애플리케이션 | REST API, 모바일 앱, MSA | - 장점 : SQL 직접 작성 시 필요한 조인 제약에서 벗어나 논리적인 도메인 모델 구조에 따라 데이터 조회 가능 + 서버 확장 방법 - JPA에서의 활용 : JPA는 연관된 객체를 처음부터 로딩하지 않고 실제 사용 시점에 조회하는 지연로딩을 사용해 객체그래프 탐색을 지원 - - 주의점 : 객체 그래프를 무분별 탐색하는 경우, 하이버네이트 등에서 N+1 문제가 발생할 수 있음. - - **내 요약** - - - Store store = member.getReviewList().get(0).getStore()와 같은 코드가 있다고 가정. - 1. 멤버의 리뷰리스트를 가져오고 - 2. 0번 객체를 가져오고 - 3. 그 리뷰의 가게를 가져오는 흐름을 표현 가능. - - 여기서 문제 : 어디까지 조회가 가능한가? - - 실행시 모든 데이터를 올려둘 수는 없음. 그렇다면 사용할 때만 필요한 정보를 가져오는 방식 Lazy-Loading(지연로딩)을 사용해 `.` 을 사용해 다음 객체를 불러올때마다 데이터를 가져옴. - - 만약 객체간 연결이 없거나, 다음 데이터를 조회할 수 없다면 null이 조회되거나 오류가 날 것. -- @Valid vs @Validated - - @Valid - - - 컨트롤러 단에서 객체의 유효성을 검사할 때 사용 - - 계층 구조 검증이 가능 (User 안에 있는 Address 객체도 검사하고 싶으면 Address 필드 위에 @Valid 를 붙여야 함) - - 한계 : 그룹화 검증이 불가능 - - @Validated - - - 스프링에서 자바표준기능을 확장해 만든 어노테이션 - - 용도 : Service 나 Repository 등 Bean 계층에서 검증이 필요할 때, 혹은 그룹검증이 필요할 때 사용. - - 특징 - - 제약조건 그룹화 : “회원가입때는 이 필드 검사하고, 정보수정때는 하지마” 같은 상황을 `groups` 속성으로 지정가능 - - 클래스 레벨 선언 : 클래스 상단에 `@Validated` 어노테이션을 붙여야 해당 클래스 내부의 메서드 파라미터에 대한 검증이 작동 - - 한계 : 계층구조 검증은 직접적으로 지원하지 않아, 내부 객체에는 여전히 @Valid를 써야함. \ No newline at end of file + - Scale-up : 단일 서버 성능 향상 + - Scale-out : 서버의 개수를 늘리기 + - 로드밸런서 : 서버 부하를 분산시키는 H/W, S/W + - 클라이언트와 서버Pool 사이에 위치해 서버의 부하를 분산시키는 하드웨어나 S/W \ No newline at end of file From db958b54ff2f668f31b38de1fcd3875946d9498c Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:01:32 +0900 Subject: [PATCH 23/25] =?UTF-8?q?feat=20:=20AuthMember=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/entity/AuthMember.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java new file mode 100644 index 00000000..0a9e8f40 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(); + } + @Override + public String getPassword() { + return member.getPassword(); + } + @Override + public String getUsername() { + return member.getEmail(); + } +} From 558d691aaa3e674319a51bae67f3753767de2c3e Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:02:23 +0900 Subject: [PATCH 24/25] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java new file mode 100644 index 00000000..173be810 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.service; + +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.global.entity.AuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + ) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + return new AuthMember(member); + } +} From 886eab92e89ecf59f23cece1eacd002d5736228a Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:02:57 +0900 Subject: [PATCH 25/25] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D,=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/CustomAccessDenied.java | 21 +++++++++++++++++++ .../global/security/CustomEntryPoint.java | 21 +++++++++++++++++++ .../global/security/SecurityResponseUtil.java | 19 +++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java new file mode 100644 index 00000000..fc4ad57b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.FORBIDDEN); + } +} \ No newline at end of file diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java new file mode 100644 index 00000000..60e7713b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java new file mode 100644 index 00000000..251ca496 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java @@ -0,0 +1,19 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class SecurityResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void writeErrorResponse(HttpServletResponse response, BaseErrorCode code) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + objectMapper.writeValue(response.getOutputStream(), ApiResponse.onFailure(code, null)); + } +}