Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b1e4be
feat : MissionReqDTO 내 CreateMission 레코드 생성
LATE-BL00MER May 10, 2026
f3d178d
feat : MissionResDTO 내 페이징 응답을 위한 Pagination 레코드 추가
LATE-BL00MER May 10, 2026
2640c29
feat: RequestBody 검증 예외 처리 추가
LATE-BL00MER May 12, 2026
b8b1f36
feat: 가게별 미션 조회 페이지네이션 구현
LATE-BL00MER May 12, 2026
71cc4a9
feat: 진행 중인 미션 조회 쿼리 추가
LATE-BL00MER May 12, 2026
13f40d4
feat: 내가 작성한 리뷰 커서 페이지네이션 구현
LATE-BL00MER May 12, 2026
bb39467
fix: 가게 조회 실패 에러 코드 수정
LATE-BL00MER May 12, 2026
8dceb60
docs: 7주차 핵심 키워드 정리
LATE-BL00MER May 12, 2026
7a2b04e
Spring Security를 사용하기 위해 필요한 의존성 추가
LATE-BL00MER May 17, 2026
1685a6d
fix: 스프링 시큐리티 의존성을 dependencies 블록 내부로 이동
LATE-BL00MER May 17, 2026
fd66fef
feat: Spring Security 설정 클래스(SecurityConfig) 추가
LATE-BL00MER May 17, 2026
5294005
docs: SecurityConfig 클래스 내 주요 설정에 설명 주석 추가
LATE-BL00MER May 17, 2026
25d8aa5
feat: Spring Security 인증 및 접근 제어 설정 추가
LATE-BL00MER May 18, 2026
f9a8ed3
setting: H2 테스트 DB 의존성 추가
LATE-BL00MER May 18, 2026
44bbec0
feat: 403 접근 거부 예외 핸들러 추가
LATE-BL00MER May 18, 2026
1f9cf38
feat: 401 인증 실패 예외 핸들러 추가
LATE-BL00MER May 18, 2026
5397e2f
feat: 회원 인증 정보 조회 서비스 추가
LATE-BL00MER May 18, 2026
0f4c97f
feat: 회원가입 public API 추가
LATE-BL00MER May 18, 2026
0b293fe
feat: 회원가입 요청 변환 로직 추가
LATE-BL00MER May 18, 2026
6d41479
feat: 회원 중복 예외 코드 추가
LATE-BL00MER May 18, 2026
99d61cf
feat: 회원 이메일 조회 Repository 메서드 추가
LATE-BL00MER May 18, 2026
fcc4533
feat: 회원가입 SignUp 요청 DTO 추가
LATE-BL00MER May 18, 2026
b81c784
feat: 회원가입 SignUp 응답 DTO 추가
LATE-BL00MER May 18, 2026
12e5d3c
feat: 회원가입 비밀번호 암호화 및 저장 로직 추가
LATE-BL00MER May 18, 2026
0059c0a
feat: 회원가입 성공 응답 코드 추가
LATE-BL00MER May 18, 2026
cb9de21
feat: 회원가입 public API 및 인증 예외 처리 설정
LATE-BL00MER May 18, 2026
78640e2
docs: 8주차 핵심 키워드 정리 추가
LATE-BL00MER May 19, 2026
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.

5 changes: 5 additions & 0 deletions Jinyong/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'com.h2database:h2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
Expand Down
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 |
| --- | --- | --- |
| 전체 데이터 개수 | 알 수 있음 | 알 수 없음 |
| 전체 페이지 수 | 알 수 있음 | 알 수 없음 |
| 다음 페이지 여부 | 알 수 있음 | 알 수 있음 |
| 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`는 메서드 파라미터 검증이나 검증 그룹이 필요할 때 사용한다.
57 changes: 57 additions & 0 deletions Jinyong/keyword_summary/ch08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
- Spring Security가 무엇인가?

스프링 시큐리티(Spring Security)는 **자바 기반 웹 애플리케이션의 인증과 인가, 그리고 보안 위협으로부터의 보호를 처리하는 강력한 보안 프레임워크**

즉, 사용자가 어떤 API에 접근할 수 있는지 판단하며, CSRF같은 웹 보안 위협에 대응할 수 있도록 도와주고, 인증과 인가또한 처리해주는 도구이다.

핵심 동작은 Filter Chain으로 클라이언트의 요청이 Controller에 바로 도달하는 것이 아니라, 먼저 여러 보안 필터를 순서대로 통과한다.

로그인 여부 확인, 인증 객체 생성, 권한 검사, 예외처리 등을 필터들이 담당한다.

순서:

요청 → 필터 체인 → 인증 확인 → 권한 확인 → Controller 도달 또는 예외 응답

- 인증(Authentication)vs 인가(Authorization)

인증(Authentication)은 **사용자가 누구인지 확인하는 과정**이다. 예를 들어 사용자가 메일과 비밀 번호를 입력했을 때, 실제로 가입된 사용자인지 확인하는 것이 “인증”이다. → “당신은 누구….?”

인가(Authorization)는 **인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정**이다. 예를 들어 로그인은 성공했지만 일반 사용자가 관리자 페이지에 접근하려고 할 때 접근을 허용할지 거부할지 판단하는 것이 “인가”이다. → “이 사용자가 해당 리소스에 접근할 권한이 있는가??”

인증: 로그인해서 신원을 확인하는 단계

인가: 로그인한 사용자가 어디까지 접근할 수 있는지 정하는 단계

- Stateful vs Stateless

Stateful은 서버가 사용자의 로그인 상태를 기억하는 방식이다.

대표적인 예시는 세션 기반 로그인으로 사용자가 로그인하면 서버는 세션에 인증 정보를 저장하고, 클라이언트는 세션 ID를 쿠키로 들고 다닌다.

이후 요청이 들어오면 서버는 세션 ID를 보고 사용자가 로그인한 상태인지 확인한다.

Stateless는 서버가 사용자의 로그인 상태를 저장하지 않는 방식이다. 대표적인 예시는 JWT 기반 인증이다.

사용자가 로그인하면 서버는 Access Token 같은 토큰을 발급하고, 이후 클라이언트는 요청마다 토큰을 함께 보낸다.

서버는 세션을 조회하는 대신, 요청에 포함된 토큰을 검증해서 사용자를 판단한다.

Stateful 방식은 서버가 로그인 상태를 기억하기 때문에 흐름이 직관적이다. 로그인 성공 후 서버 세션에 인증 정보를 저장하고, 이후 요청마다 세션을 통해 로그인 여부르 판단 할 수 있다. Spring Security의 폼 로그인 방식은 기본적으로 세션을 활용하는 Stateful 방식에 가깝다

이와 다르게 Stateless는 서버가 로그인 상태를 기억하지 않는다. 따라서 서버 확장에 유리하지만, 클라이언트가 매 요청마다 토큰을 보내야 한다.

JWT를 사용하는 REST API 서버에서는 보통 `SessionCreationPolicy.STATELESS`를 설정하고, 서버가 세션을 만들지 않도록 구성한다.
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.

Stateless 설명에서 SessionCreationPolicy.STATELESS를 언급한 점이 좋습니다. 현재 구현은 formLogin과 기본 세션 흐름을 사용하는 구조에 가까우므로, 세션 기반 인증과 JWT 기반 인증에서 필요한 설정 차이(formLogin, httpBasic, sessionManagement, JWT 필터 위치)를 함께 비교하는 것을 권장합니다.


Stateful의 장/단점

장점 :구현 흐름이 쉽다. 서버가 세션을 직접 관리하기에 로그아웃이나 세션 만료처리가 명확함.

단점: 사용자가 많아질수록 서버가 세션 정보를 관리해야 하므로 부담이 커질 수 있고, 서버가 여러 대일 경우 세션 공유 문제를 고려해야 함

Stateless의 장/단점

장점: 서버가 세션을 저장하지 않기 때문에 확장성에 유리하다. 여러 서버로 트래픽을 분산해도 각 서버가 토큰만 검증하면 되므로 REST API, 모바일 앱, MSA 구조에서 자주 사용함

단점: 토큰 탈취, 만료 시간, Refresh Token 관리, 로그아웃 처리 등을 신중하게 설계해야 함

**차이점은 결국, 서버가 로그인 상태를 기억하냐 못 하냐**
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ public enum MemberSuccessCode implements BaseSuccessCode {

OK(HttpStatus.OK,
"MEMBER200_1",
"성공적으로 유저를 조회했습니다.");
"성공적으로 유저를 조회했습니다."),
CREATED(HttpStatus.CREATED,
"MEMBER201_1",
"회원가입이 완료되었습니다.");

private final HttpStatus status;
private final String code;
private final String message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ public ApiResponse<MemberResDTO.GetInfo> getInfo(
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getInfo(memberId));
}
}

// 회원가입
@PostMapping("/v1/auth/signup")
public ApiResponse<MemberResDTO.SignUp> signUp(
@RequestBody MemberReqDTO.SignUp request
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.

회원가입 요청은 이메일과 비밀번호가 핵심 입력값이므로 @RequestBody @Valid와 DTO 필드의 @NotBlank, @Email, 비밀번호 길이 검증을 함께 적용하는 것을 권장합니다. 이렇게 하면 Controller 진입 시점에 요청 계약이 명확해지고 Service는 중복 이메일 같은 도메인 규칙에 더 집중할 수 있습니다.

) {
BaseSuccessCode code = MemberSuccessCode.CREATED;
return ApiResponse.onSuccess(code, memberService.signUp(request));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.example.umc10th.domain.member.converter;

import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Gender;
import com.example.umc10th.domain.member.entity.Member;

import java.time.LocalDate;

public class MemberConverter {

// 마이페이지 정보 조회를 위한 DTO 변환
Expand All @@ -18,4 +22,23 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) {
.phoneNumber(null)
.build();
}

public static Member toMember(MemberReqDTO.SignUp request, String encodedPassword) {
return Member.builder()
.email(request.email())
.password(encodedPassword)
.name(request.email())
.nickname(request.email())
.gender(Gender.NONE)
.point(0)
.birth(LocalDate.now())
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.

회원가입 DTO가 email/password만 받다 보니 name, nickname, birth가 임시값으로 채워지고 있습니다. Entity의 필수값을 임시값으로 맞추기보다 가입 시 필요한 필드를 요청 DTO에 포함할지, 또는 nullable/default 정책을 도메인 규칙으로 정리할지 검토하는 것을 권장합니다. DTO와 Entity의 책임 경계를 학습하기 좋은 지점입니다.

.build();
}

public static MemberResDTO.SignUp toSignUp(Member member) {
return MemberResDTO.SignUp.builder()
.memberId(member.getId())
.email(member.getEmail())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ public class MemberReqDTO {
public record GetInfo(
Long id
){}

public record SignUp(
String email,
String password
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ public record GetInfo(
String nickname,
String gender
) {}
}

@Builder
public record SignUp(
Long memberId,
String email
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,
"MEMBER404",
"해당 사용자를 찾을 수 없습니다.");
"해당 사용자를 찾을 수 없습니다."),
MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT,
"MEMBER409_1",
"이미 존재하는 이메일입니다.");

private final HttpStatus httpStatus;
private final String code;
Expand All @@ -21,4 +24,4 @@ public enum MemberErrorCode implements BaseErrorCode {
public HttpStatus getStatus() {
return httpStatus;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.example.umc10th.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
}
Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,33 @@
import com.example.umc10th.domain.member.exception.MemberException;
import com.example.umc10th.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

public MemberResDTO.GetInfo getInfo(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

return MemberConverter.toGetInfo(member);
}

public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) {
memberRepository.findByEmail(request.email())
.ifPresent(member -> {
throw new MemberException(MemberErrorCode.MEMBER_ALREADY_EXISTS);
});

String encodedPassword = passwordEncoder.encode(request.password());
Member member = MemberConverter.toMember(request, encodedPassword);
Member savedMember = memberRepository.save(member);

return MemberConverter.toSignUp(savedMember);
}
}
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",
"성공적으로 미션을 생성했습니다."),
OK(HttpStatus.OK,
"MISSION200_2",
"성공적으로 미션을 조회했습니다.");

private final HttpStatus status;
Expand Down
Loading