[Jinyong] Week8 미션 #91
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78640e265d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) { | ||
| BaseSuccessCode code = MissionSuccessCode.OK; | ||
| return ApiResponse.onSuccess(code, missionService.getMissionList(storeId)); | ||
| return ApiResponse.onSuccess(code, missionService.getMissionList(storeId, pageSize, pageNumber, sort)); |
There was a problem hiding this comment.
현재 컨트롤러는 pageSize, pageNumber 순서로 넘기지만 서비스 메서드 시그니처는 pageNumber, pageSize 순서라서, 예를 들어 pageSize=10&pageNumber=0 첫 페이지 요청이 서비스 내부에서는 PageRequest.of(10, 0)처럼 해석되어 페이지 크기 0 예외가 납니다. Spring Data의 PageRequest는 pageNumber, pageSize 순서를 요구하므로 컨트롤러-서비스 파라미터 순서를 통일하거나 Pageable/요청 DTO로 묶어 전달해 주세요. 다음 학습 포인트는 컨트롤러-서비스 계약과 Pageable 사용 방식입니다.
Useful? React with 👍 / 👎.
kjhh2605
left a comment
There was a problem hiding this comment.
[키워드 조사]
Spring Security의 필터 체인, 인증과 인가, Stateful/Stateless 차이를 핵심 흐름 중심으로 정리한 점이 좋습니다. 특히 세션 기반 인증과 JWT 기반 인증을 비교하려는 방향이 적절합니다. 다만 이번 구현은 formLogin 기반 세션 흐름에 가깝기 때문에, JWT/Stateless 설명과 현재 코드 설정이 어떤 점에서 다른지 함께 연결해 정리하는 것을 권장합니다. 추가로 SecurityContext, Authentication, UserDetailsService, PasswordEncoder, 401/403 예외 흐름을 하나의 로그인 요청 흐름으로 묶어 학습하면 이해가 높아집니다.
[코드 리뷰]
회원가입 API, 비밀번호 암호화, SecurityFilterChain, 인증/인가 예외 핸들러까지 구성하여 8주차 핵심 흐름을 코드로 적용한 점이 좋습니다. Controller-Service-Repository 계층 분리와 Converter 활용도 전반적으로 유지되었습니다. 다음 단계에서는 회원가입 요청 DTO의 입력 검증, 인증된 사용자 식별 방식, 세션 기반 formLogin과 REST/JWT 방식의 차이를 코드 설정과 함께 명확히 하는 것을 권장합니다. 또한 커서 페이지네이션에서는 cursor/query 파라미터의 기본값과 유효성 검증을 보강하면 API 안정성이 높아집니다. 빌드 검증은 현재 실행 환경에 Java Runtime이 없어 완료하지 못했습니다.
|
|
||
| 이와 다르게 Stateless는 서버가 로그인 상태를 기억하지 않는다. 따라서 서버 확장에 유리하지만, 클라이언트가 매 요청마다 토큰을 보내야 한다. | ||
|
|
||
| JWT를 사용하는 REST API 서버에서는 보통 `SessionCreationPolicy.STATELESS`를 설정하고, 서버가 세션을 만들지 않도록 구성한다. |
There was a problem hiding this comment.
Stateless 설명에서 SessionCreationPolicy.STATELESS를 언급한 점이 좋습니다. 현재 구현은 formLogin과 기본 세션 흐름을 사용하는 구조에 가까우므로, 세션 기반 인증과 JWT 기반 인증에서 필요한 설정 차이(formLogin, httpBasic, sessionManagement, JWT 필터 위치)를 함께 비교하는 것을 권장합니다.
| // 회원가입 | ||
| @PostMapping("/v1/auth/signup") | ||
| public ApiResponse<MemberResDTO.SignUp> signUp( | ||
| @RequestBody MemberReqDTO.SignUp request |
There was a problem hiding this comment.
회원가입 요청은 이메일과 비밀번호가 핵심 입력값이므로 @RequestBody @Valid와 DTO 필드의 @NotBlank, @Email, 비밀번호 길이 검증을 함께 적용하는 것을 권장합니다. 이렇게 하면 Controller 진입 시점에 요청 계약이 명확해지고 Service는 중복 이메일 같은 도메인 규칙에 더 집중할 수 있습니다.
| .nickname(request.email()) | ||
| .gender(Gender.NONE) | ||
| .point(0) | ||
| .birth(LocalDate.now()) |
There was a problem hiding this comment.
회원가입 DTO가 email/password만 받다 보니 name, nickname, birth가 임시값으로 채워지고 있습니다. Entity의 필수값을 임시값으로 맞추기보다 가입 시 필요한 필드를 요청 DTO에 포함할지, 또는 nullable/default 정책을 도메인 규칙으로 정리할지 검토하는 것을 권장합니다. DTO와 Entity의 책임 경계를 학습하기 좋은 지점입니다.
| String nextCursor; | ||
|
|
||
| // 커서가 있는 경우 | ||
| if (!cursor.equals("-1")) { |
There was a problem hiding this comment.
cursor가 필수 요청 파라미터로 들어오지 않거나 null이면 cursor.equals에서 예외가 발생할 수 있습니다. Controller에서 기본값을 -1로 지정하거나 Service 진입 시 null/blank 검증을 먼저 수행하는 것을 권장합니다. 커서 페이지네이션에서는 첫 요청의 기본 cursor 규칙을 API 계약으로 명확히 두는 것이 중요합니다.
|
|
||
|
|
||
| // ID 기준이면 다음 커서는 마지막 리뷰의 ID | ||
| if (query.equals("id")) { |
There was a problem hiding this comment.
위쪽 switch에서는 query.toLowerCase()를 사용하지만, 다음 커서 계산에서는 원본 query로 equals("id")를 비교하고 있습니다. 클라이언트가 ID처럼 대문자를 보내면 조회는 id 기준으로 처리되지만 nextCursor는 score 형식으로 만들어질 수 있습니다. 정규화한 값을 변수로 분리해 전체 로직에서 동일하게 사용하는 것을 권장합니다.
| private final Member member; | ||
|
|
||
| @Override | ||
| public Collection<? extends GrantedAuthority> getAuthorities() { |
There was a problem hiding this comment.
현재 권한 목록이 항상 비어 있어 추후 role 기반 인가를 적용할 때 hasRole, hasAuthority 조건이 의도대로 동작하기 어렵습니다. 이번 주차에서는 빈 목록도 학습 단계로 볼 수 있지만, Member의 role 필드 또는 기본 USER 권한을 GrantedAuthority로 변환하는 흐름까지 함께 정리하는 것을 권장합니다.
🔗 연관 이슈
closes #80
🛠 작업 내용
SecurityConfig를 생성하여 SecurityFilterChain을 설정email,password필드를 추가CustomEntryPoint를 구현CustomAccessDenied를 구현🖼 스크린샷 (선택)
👀 리뷰 요구사항 (선택)
🤖 AI 활용
💬 나의 프롬프트
Spring Security 구조 그림을 봐도 잘 모르겠는데,
AuthenticationManager,AuthenticationProvider,UserDetailsService,SecurityContextHolder가 각각 뭘 하는 건지 쉽게 설명해줘.Filter Chain이 정확히 뭐야? 요청이 Controller로 바로 가는 게 아니라 필터를 거친다는데, 이 흐름이 어떻게 되는 거야?리다이렉트한다는 게 정확히 무슨 의미야? 로그인 안 했을 때 로그인 페이지로 보낸다는 말이 잘 이해가 안 돼.
회원가입 API를 만들라고 하는데, 갑자기
MemberService에서PasswordEncoder를 받으라는 게 무슨 뜻이야? 이미 코드는 짰는데 여기서 뭘 더 해야 하는 거야?회원가입 API만 Public이고 나머지는 Private API로 설정해야 하는 거면,
SecurityConfig에서 어떤 경로만 열어두고 어떤 경로를 막아야 해?/auth/sign-up이라고 문서에는 되어 있는데, 내 실제 Controller 경로는/api/v1/auth/signup이야. 이럴 때는 어떤 경로를 기준으로permitAll()해야 해?인증 안 된 사용자가 Private API를 호출하면 401이 떠야 한다고 하는데, 이걸 Swagger에서 어떻게 확인하면 돼?
회원가입은 200이 나오고,
GET /api/v1/stores는 401이 나오는데 이러면 요구사항이 맞게 구현된 거야?🧠 AI 응답
Spring Security의 인증 과정은 사용자의 요청이 먼저 필터를 거치고, 로그인 요청이면
AuthenticationManager와AuthenticationProvider를 통해 인증이 진행된다고 설명받았습니다.그리고
UserDetailsService는 DB에서 사용자 정보를 가져오는 역할이고, 인증이 성공하면SecurityContextHolder에 사용자 인증 정보가 저장된다는 흐름으로 이해했습니다.Filter Chain은 Controller 앞에서 요청을 검사하는 보안 필터들의 묶음이라고 설명받았습니다.즉, 사용자의 요청이 바로 Controller로 가는 것이 아니라, 로그인 여부 확인, 예외 처리, 권한 검사 같은 필터들을 거친 뒤 통과하면 Controller로 이동한다는 것을 알게 되었습니다.
리다이렉트는 서버가 브라우저에게 “지금 요청한 주소 말고 다른 주소로 다시 가라”고 시키는 것이라고 설명받았습니다.
다만 이번 과제는 REST API 형태이기 때문에 로그인 페이지로 리다이렉트하기보다는 401 JSON 응답을 반환하는 방식이 더 적절하다고 이해했습니다.
회원가입 API에서는 비밀번호를 그대로 저장하면 안 되기 때문에
PasswordEncoder를MemberService에 주입받고, 회원 저장 전에passwordEncoder.encode()를 사용해 BCrypt 방식으로 암호화해야 한다고 안내받았습니다.Public API와 Private API를 나누는 부분에서는 실제 회원가입 API 경로인
/api/v1/auth/signup만permitAll()로 열고, 나머지 API는authenticated()로 막아야 한다고 설명받았습니다.문서에 적힌 경로보다 실제 Controller에 매핑된 경로를 기준으로 Security 설정을 해야 한다고 안내받았습니다. 그래서
/auth/sign-up이 아니라 현재 프로젝트의 실제 경로인/api/v1/auth/signup을 기준으로 설정했습니다.Swagger 테스트에서는 회원가입 API가 인증 없이 200 응답을 반환하면 Public API 설정이 된 것이고, 다른 API인
GET /api/v1/stores가 인증 없이 401 응답을 반환하면 Private API 설정이 된 것이라고 확인했습니다.✅ 내가 최종 선택한 방법 (이유)
실제 Controller에 구현된 회원가입 경로인
/api/v1/auth/signup만 Public API로 설정했습니다.문서에는
/auth/sign-up처럼 예시 경로가 나와 있었지만, 실제 프로젝트에서는 Controller에 매핑된 경로가 기준이 되어야 한다고 판단했기 때문입니다.Swagger 관련 경로는 테스트를 위해 예외적으로 허용했습니다.
과제에서 말하는 Public API는 회원가입 API 하나라고 보았지만, Swagger 자체가 막히면 API 테스트와 워크북에 사용할 캡처가 어려워지기 때문에 문서 확인용 경로만 따로 열어두었습니다.
회원가입 API와 Swagger 경로를 제외한 나머지 API는 모두
authenticated()로 설정했습니다.요구사항에서 “회원가입 API는 Public API, 그 이외의 API는 Private API”라고 되어 있었기 때문에
/api/v1/**처럼 넓은 경로를 열지 않고, 필요한 경로만 최소한으로! 허용하는 방식을 선택했습니다.회원가입 시 입력받은 비밀번호는
PasswordEncoder를 사용해 BCrypt 방식으로 암호화한 뒤 저장하도록 구현했습니다.비밀번호를 평문으로 저장하면 보안상 위험하고, 과제에서도 BCrypt 솔트 처리를 요구했기 때문에 회원 저장 전에
passwordEncoder.encode()를 적용했습니다.인증되지 않은 사용자가 Private API에 접근하면 401 JSON 응답이 나오도록
CustomEntryPoint를 사용했습니다.로그인하지 않은 사용자가 보호된 API에 접근했을 때 기본 로그인 페이지로 이동하는 것보다, REST API에서는 JSON 형태로 실패 응답을 주는 것이 더 적절하다고 판단했습니다.
권한이 부족한 경우에는 403 JSON 응답이 나오도록
CustomAccessDenied를 사용했습니다.인증 실패와 인가 실패를 구분해야 하고, 과제에서 exceptionHandling을 통해 응답을 통일하라고 했기 때문에 401과 403을 각각 다른 핸들러로 처리했습니다.
Swagger에서
POST /api/v1/auth/signup은 200 성공,GET /api/v1/stores는 401 실패가 나오는 것을 확인했습니다.회원가입 API는 로그인 없이 접근 가능해야 하고, 나머지 API는 인증 없이 접근하면 막혀야 하므로 이 두 결과를 통해 Public API와 Private API 설정이 의도대로 적용되었는지 검증했습니다.
💡 나만의 Tip (선택)