Conversation
There was a problem hiding this comment.
Pull request overview
Spring Security를 도입하고(폼 로그인/로그아웃, 인증·인가 예외 응답 통일), 폼 기반 회원가입 API 및 회원 선호음식/약관 시드 데이터를 추가해 회원가입 플로우가 동작하도록 구성한 PR입니다.
Changes:
- Spring Security 설정 추가 및 401/403 응답을 ApiResponse 포맷으로 통일(EntryPoint/AccessDenied/Writer, UserDetails 어댑터)
- 폼 회원가입 구현: 이메일 중복 체크, BCrypt 인코딩, Member 저장 및 Food/Term 매핑 저장
- Food/Term enum 기반 시드 데이터 자동 적재(부팅 시)
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| build.gradle | Spring Security 의존성 추가 |
| src/main/java/com/example/umc10th/global/config/SecurityConfig.java | 보안 정책(permitAll/authenticated), formLogin/logout, 예외 처리 핸들러 연결 |
| src/main/java/com/example/umc10th/global/config/InitDataLoader.java | Food/Term 시드 데이터 적재 CommandLineRunner |
| src/main/java/com/example/umc10th/global/security/SecurityResponseWriter.java | 401/403 공통 JSON(ApiResponse) 작성 유틸 |
| src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java | 인증 실패(401) 응답 통일 |
| src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java | 인가 실패(403) 응답 통일 |
| src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java | 이메일 기반 사용자 조회(UserDetailsService) |
| src/main/java/com/example/umc10th/global/security/AuthMember.java | Member → UserDetails 어댑터 |
| src/main/java/com/example/umc10th/domain/member/controller/MemberController.java | 회원가입 Mock 제거 후 실제 서비스 호출 |
| src/main/java/com/example/umc10th/domain/member/service/MemberService.java | 회원가입(signUp) 구현 및 매핑 저장 |
| src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java | SignUp DTO↔Entity/Response 변환 추가 |
| src/main/java/com/example/umc10th/domain/member/entity/Member.java | password 필드 추가, socialUid nullable, 기본값 처리(socialType/profileUrl) |
| src/main/java/com/example/umc10th/domain/member/enums/SocialType.java | LOCAL 타입 추가 |
| src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java | existsByEmail/findByEmail 추가 |
| src/main/java/com/example/umc10th/domain/member/repository/FoodRepository.java | Food 조회(findByName) 리포지토리 추가 |
| src/main/java/com/example/umc10th/domain/member/repository/TermRepository.java | Term 조회(findByName) 리포지토리 추가 |
| src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java | 회원가입 매핑 관련 에러 코드 추가(FOOD_NOT_FOUND/TERM_NOT_FOUND) |
| @RequiredArgsConstructor | ||
| public class SecurityResponseWriter { | ||
|
|
||
| private ObjectMapper objectMapper; |
Comment on lines
+23
to
+33
| private final String[] allowUris = { | ||
| // Swagger 허용 | ||
| "/swagger-ui/**", | ||
| "/swagger-resources/**", | ||
| "/v3/api-docs/**", | ||
| "/auth/**", | ||
|
|
||
| // 폼 로그인 페이지 자체 | ||
| "/login", | ||
| "/login/**" | ||
| }; |
Comment on lines
+36
to
+40
| public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
| http | ||
| .csrf(AbstractHttpConfigurer::disable) | ||
| .authorizeHttpRequests(requests -> requests | ||
| .requestMatchers(allowUris).permitAll() |
Comment on lines
+21
to
+27
| .email(request.email()) | ||
| .password(encodedPassword) | ||
| .phoneNumber(request.phoneNumber()) | ||
| .gender(Gender.valueOf(request.gender())) | ||
| .birth(request.birth()) | ||
| .address(Address.valueOf(request.address())) | ||
| .detailAddress(request.detailAddress()) |
Comment on lines
+18
to
+22
| public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { | ||
| Member member = memberRepository.findByEmail(email) | ||
| .orElseThrow(() -> new UsernameNotFoundException( | ||
| "해당 이메일의 회원을 찾을 수 없습니다: " + email | ||
| )); |
Comment on lines
+60
to
+78
| // 4. 선호 음식 매핑 저장 | ||
| for (String foodName: request.preferredFoods()) { | ||
| com.example.umc10th.domain.member.enums.Food foodEnum; | ||
| try { | ||
| foodEnum = com.example.umc10th.domain.member.enums.Food.valueOf(foodName); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new MemberException(MemberErrorCode.FOOD_NOT_FOUND); | ||
| } | ||
|
|
||
| Food food = foodRepository.findByName(foodEnum) | ||
| .orElseThrow(() -> new MemberException(MemberErrorCode.FOOD_NOT_FOUND)); | ||
|
|
||
| MemberFood memberFood = MemberFood.builder() | ||
| .member(savedMember) | ||
| .food(food) | ||
| .build(); | ||
|
|
||
| em.persist(memberFood); | ||
| } |
Comment on lines
+32
to
+41
| private void seedFoods() { | ||
| // enum의 모든 값을 순회하면서, DB에 없는 것만 INSERT | ||
| Arrays.stream(com.example.umc10th.domain.member.enums.Food.values()) | ||
| .filter(foodEnum -> foodRepository.findByName(foodEnum).isEmpty()) | ||
| .forEach(foodEnum -> { | ||
| foodRepository.save(Food.builder() | ||
| .name(foodEnum) | ||
| .build()); | ||
| log.info("[InitDataLoader] Food 시드: {}", foodEnum); | ||
| }); |
Comment on lines
+15
to
+20
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| // CommandLineRunner: Spring이 제공하는 인터페이스 | ||
| // 이걸 구현하면 애플리케이션이 시작된 직후에 run() 메서드가 자동으로 호출된다. | ||
| public class InitDataLoader implements CommandLineRunner { |
|
깊게 고민하신 흔적이 보여 좋은 것 같습니다! |
CokaNuri
approved these changes
May 26, 2026
CokaNuri
left a comment
There was a problem hiding this comment.
@RestControllerAdvice로 잡는 도메인 예외와 시큐리티 필터 단 예외는 처리 경로가 완전히 분리되어 있다는 점에 대해 짚어주신 부분 훌륭합니다.
SecurityConfig에서 로그인,회원가입 관련 api를 public으로 열어두실 때
경로를 /api/v1/auth/** 와 같은 방식으로 수정만 하시면 될 거 같아요.
고생하셨습니다!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✏️ 작업 내용
Spring Security 도입
build.gradle에 Spring Security 의존성 추가SecurityConfig신규 작성 (Public/Private 분리, formLogin, logout, exceptionHandling)PasswordEncoderBean으로BCryptPasswordEncoder등록회원가입 API 구현
MemberReqDTO.SignUp에 이미 정의된email,password활용Member엔티티에password필드 추가socialUid는 nullable,socialType은LOCAL기본값 처리profileUrl은@Builder.Default로 기본 URL 박아둠SocialTypeenum에LOCAL추가MemberRepository에existsByEmail,findByEmail쿼리 메서드 추가FoodRepository,TermRepository신규 작성 (findByName)MemberConverter에toMember,toSignUpResponse추가MemberService.signUp구현 (이메일 중복 체크 -> BCrypt 인코딩 -> Member 저장 -> MemberFood/MemberTerm 매핑 저장)MemberController의 Mock 응답 제거 후 실제 서비스 호출로 교체MemberErrorCode에FOOD_NOT_FOUND,TERM_NOT_FOUND추가시드 데이터 처리
InitDataLoader(CommandLineRunner구현) 추가Food/Termenum 값을 DB에 멱등하게 INSERT (이미 있는 값은 건너뜀)인증/인가 처리 및 응답 통일
AuthMember(UserDetails구현체):Member를 감싸는 어댑터CustomUserDetailsService(UserDetailsService구현): 이메일로 회원 조회SecurityResponseWriter: 401/403 JSON 응답 공통 유틸CustomEntryPoint(AuthenticationEntryPoint): 인증 실패(401) 시 통일된 JSON 응답CustomAccessDenied(AccessDeniedHandler): 인가 실패(403) 시 통일된 JSON 응답#️⃣ 연관된 이슈
closes #110
💡 함께 공유하고 싶은 부분
1.
@RestControllerAdvice가 잡지 못하는 영역이 있다CustomUserDetailsService는 Spring Security 필터 체인 안에서 호출되기 때문에DispatcherServlet에 도달하지 못한다. 즉, 여기서 발생한 도메인 예외(MemberException등)는 기존GeneralExceptionAdvice가 잡지 못한다.그래서
CustomUserDetailsService.loadUserByUsername에서는 도메인 예외 대신 Spring Security 표준 예외인UsernameNotFoundException을 던지도록 구현했다. Spring Security의DaoAuthenticationProvider가 이 예외를 잡아서 인증 실패로 변환해주고, 그러면CustomEntryPoint가 동작해 통일된 401 JSON 응답이 나간다.2. 시드 데이터 처리 필요성
회원가입 시
preferredFoods,agreedTerms를 받아서Food/Term테이블에서 조회한 뒤MemberFood/MemberTerm으로 매핑한다. 그런데food/term테이블이 비어있으면findByName()이 실패해서 회원가입 자체가 안 된다.이걸 해결하려고
CommandLineRunner로InitDataLoader를 만들어서 서버 부팅 시 enum 값들을 DB에 자동 시드하도록 처리했다.filter(... isEmpty())로 DB에 없는 값만 INSERT하게 해서, 재시작할 때마다 중복 삽입되지 않도록 멱등성을 확보했다.3. 어댑터 패턴으로 도메인과 프레임워크 분리
Member엔티티에 직접implements UserDetails를 박는 방식도 가능하지만, 그러면 도메인 엔티티가 Spring Security에 의존하게 된다. 그래서AuthMember라는 어댑터 클래스를 따로 만들어Member를 감싸는 방식으로 구현했다. 이렇게 하면Member는 순수한 도메인 객체로 남고, Spring Security 관련 처리는AuthMember가 전담한다.🤔 질문
Q1.
SocialTypeenum에LOCAL을 추가하는 설계가 맞을까요?폼 회원가입 회원을 구분하기 위해
SocialTypeenum에LOCAL을 추가하고,social_uid를 nullable로 변경했습니다. 가장 단순한 방식이라 학습 단계에 맞다고 판단했지만, 의미적으로는 어색한 부분이 있는 것 같습니다.SocialType에 포함시켜도 되는지 궁금합니다.LocalAuth/SocialAuth)나AuthTypeenum 분리 같은 다른 방식과 비교했을 때, 실무에서는 어떤 기준으로 선택하나요?Q2.
CustomUserDetailsService에서UsernameNotFoundException을 던지는 게 맞을까요?워크북에서는
loadUserByUsername에서 도메인 예외를 던지는 예시도 나왔는데, 위에서 설명한 이유로UsernameNotFoundException을 던지는 방식으로 구현했습니다. 워크북에서 도메인 예외를 사용한 의도가 따로 있었는지, 실무에서는 어떤 선택이 일반적인지 궁금합니다.Q3.
Food/Term시드 데이터는 다른 분들은 어떻게 처리하셨나요?회원가입에서
preferredFoods,agreedTerms매핑을 구현하면서 시드 데이터의 필요성을 알게 됐습니다.CommandLineRunner로 처리했는데, 다른 분들은 어떻게 했는지 궁금합니다.CommandLineRunner로 처리했는지 궁금합니다.✅ 워크북 체크리스트
✅ 컨벤션 체크리스트
📌 주안점
CustomUserDetailsService에서UsernameNotFoundException사용Member엔티티의 변경 사항:password필드 추가,socialUidnullable 처리,socialType/profileUrl기본값 처리가 적절한지