Skip to content

[9 주차] 정용환 / Chapter 9. Spring Security - JWT, OAuth 학습 목표 - #45

Open
hwahwahwan wants to merge 7 commits into
UMC-AYU:mainfrom
hwahwahwan:hwan-Chapter09
Open

hwahwahwan wants to merge 7 commits into
UMC-AYU:mainfrom
hwahwahwan:hwan-Chapter09

Conversation

@hwahwahwan

@hwahwahwan hwahwahwan commented May 25, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Number

  • close #

📝 개요

9주차에 수정 내용 같이 넣다보니 변경점이 많습니다 9주차 미션 파일들입니다

global/security/jwt/JwtUtil.java
global/security/jwt/JwtAuthFilter.java
global/security/oauth/CustomOAuthService.java
global/security/oauth/OAuthSuccessHandler.java
build.gradle
src/main/resources/application.yaml
global/security/entity/AuthMember.java
global/config/SecurityConfig.java
global/config/SwaggerConfig.java
domain/member/exception/MemberErrorCode.java
domain/member/exception/MemberSuccessCode.java
domain/member/dto/MemberReqDTO.java
domain/member/dto/MemberResDTO.java
domain/member/converter/MemberConverter.java
domain/member/service/MemberService.java
domain/member/controller/MemberController.java
domain/mission/controller/MissionController.java
domain/review/controller/ReviewController.java
domain/member/enums/SnsType.java


🚀 주요 변경 사항

6주차 7주차 피드백 내용 반영해서 수정했습니다.

  • 서비스 전체에서 RuntimeException을 직접 던지고 있어 클라이언트에게 500 에러가 반환되던 문제를 도메인별 커스텀 예외로 교체하였습니다.
  • completeMission에서 포인트 지급 로직이 누락되어 있어 추가하였습니다.
  • ReviewService.createReview에서 가게 평균 점수 업데이트 로직이 누락되어 있어 추가하였습니다.
  • HomeService에서 지역 이름을 "지역"으로 하드코딩하고 있어 Region 엔티티에서 조회하도록 수정하였습니다.
  • completeMission에서 상태값을 COMPLETE로 하드코딩하고 있어 요청 값(dto.status())을 사용하도록 수정하였습니다.
  • MissionStatus.valueOf() 호출 시 잘못된 값이 들어와도 예외 처리가 없어 추가하였습니다.
  • JOIN FETCH와 페이지네이션을 함께 사용하면서 countQuery가 누락되어 있어 추가하였습니다.
  • memberId를 @RequestParam으로 받고 있어 @PathVariable로 URL에 포함하도록 변경하였습니다.
  • Home 관련 코드가 domain/member/ 하위에 있어 domain/mission/ 하위로 이동하였습니다.
  • Store, Region이 domain/mission/ 하위에 있어 domain/store/ 도메인으로 분리하였습니다.BaseEntity가 두 곳에 중복 존재하여 하나로 통일하였습니다.
  • MissionConverter에서 hasReview를 항상 false로 고정하고 있어 실제 리뷰 작성 여부를 DB에서 조회하도록 수정하였습니다.
  • ReviewService의 cursor 파싱에 형식 검증이 없어 잘못된 값 입력 시 500 에러가 발생하던 문제를 수정하였습니다.
  • HomeService에서 완료 미션 수를 전체 데이터를 메모리에 올려 세고 있어 DB 쿼리로 변경하였습니다.
  • 이미 완료된 미션을 다시 완료 처리할 경우 포인트가 중복 지급될 수 있어 예외 처리를 추가하였습니다.
  • MemberReqDTO, ReviewReqDTO에 유효성 검증 어노테이션이 누락되어 있어 추가하였습니다.
  • Swagger 어노테이션(@tag, @operation, @parameter)이 없어 컨트롤러 전체에 추가하였습니다.

🖼️ 실행 결과 (Screenshots)

db

💬 고민 및 질문


✅ 실습 체크리스트

  • 이론 학습을 완료 했나요?
  • 미션 요구사항을 이해했나요?
  • 미션을 완료 했나요?

⚙️ 환경 및 컨벤션 체크 (Final Check)

  • 디렉토리 구조 컨벤션을 지켰나요?
  • pr 제목을 컨벤션에 맞게 작성하였나요?
  • pr에 해당되는 이슈를 연결하였나요?
  • Assignees을 본인으로 설정했나요?
  • Reviewers을 설정 했나요?
  • 적절한 라벨을 설정하였나요?

@hwahwahwan
hwahwahwan requested review from umcanyang and zldzldzz May 25, 2026 11:33
@hwahwahwan hwahwahwan self-assigned this May 25, 2026
@hwahwahwan
hwahwahwan requested review from a team, cho-hj-dev and m4ppy and removed request for a team and umcanyang May 25, 2026 11:36
Comment on lines +18 to +28
// JWT / 일반 로그인용
public AuthMember(Member member) {
this.member = member;
this.attributes = Map.of();
}

// OAuth2 로그인용
public AuthMember(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}

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.

저는 OAuthMember 를 따로 만들어서 관리했는데 AuthMember 안에 일반 로그인용과 OAuth 로그인용을 둘 다 만드신게 다른 것 같습니다.


@Service
@RequiredArgsConstructor
public class CustomOAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

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.

저는 커스텀 OAuth 서비스를 DefaultOAuth2UserService 를 확장해서 구현했는데 OAuth2UserService 인터페이스를 구현하신 점이 다른 것 같습니다.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(username)
.orElseThrow(() -> new MemberException(MemberErrorCode.USER_NOT_FOUND));

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.

워크북에도 나와있는 내용이지만, JPA의 OPtional이 제공하는 문법을 활용해서 DB 조회랑 예외 처리를 한 번에 깔끔하게 끝내서 가독성 좋게 잘 작성된 것 같습니다.,,!

HttpServletResponse response,
AuthenticationException authException
) throws IOException {
writeErrorResponse(response, GeneralErrorCode.UNAUTHORIZED);

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.

현재 구조에서는 어떤 인증 예외가 발생해도 무조건 GeneralErrorCode.UNAUTHORIZED 하나로만 응답이 나가고 있습니다.
만약 나중에 프론트엔드에서 토큰 만료 에러를 받았을 때 자동으로 토큰 재발급 요청을 보내는 로직을 구현한다고 했을 때 에러코드가 분리되어야 할 것 같은데, 조금 더 분기 처리해 주는 구조로 하면 더 좋을 것 같다는 생각이 듭니다...!!!!!

@zldzldzz

zldzldzz commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

토큰 검증 중복 파싱

  • JwtAuthFilter.java:35에서 jwtUtil.isTokenValid(token)을 호출하며 내부적으로 getClaims(token)로 한 번 파싱하고,
  • 바로 다음 줄 JwtAuthFilter.java jwtUtil.extractEmail(token)에서 getClaims(token)를 또 한 번 파싱합니다.
    유효한 토큰 1개를 매 요청마다 서명 검증 + 파싱을 2회 수행합니다. 서명 검증은 HMAC 연산이 들어가는 비용 있는 작업이라, 요청량이 많아질수록 누적 부담이 됩니다. getClaims를 한 번만 호출해 Claims 결과를 받아 그 안에서 getSubject()로 이메일을 꺼내 재사용하는 구조로 바꾸면 파싱을 1회로 줄일 수 있습니다.

토큰 검증 실패 사유 미구분
JwtUtil.java의 isTokenValid가 만료(ExpiredJwtException), 서명 위조(SignatureException), 형식 오류 등을 모두 한 덩어리로 false 처리합니다. 현재 Access Token만 발급하는 구조에서는 동작에 문제가 없습니다.

  • 추후 Refresh Token을 도입하면, "만료된 Access Token(→ 재발급 유도)"과 "위조/잘못된 토큰(→ 인증 거부)"을 구분해야 합니다.
  • 권장 방향은: 만료는 별도 에러 코드로 클라이언트에 전달해 토큰 재발급 흐름을 태우고, 그 외 위조·형식 오류는 보안상 사유를 노출하지 않도록 하나의 "유효하지 않은 토큰" 오류로 묶는 것입니다.
  • 지금 범위에서는 선택 사항이며, Refresh Token 도입 시점에 함께 다루시면 됩니다.

권한 정보를 저장하거나 생성하는 코드 자체가 없는 상태입니다.

  • AuthMember.java의 getAuthorities()는 항상 빈 리스트를 반환합니다.
    • 로그인 여부만 확인하는 authenticated()를 사용하므로 현재 기능은 동작합니다. 하지만 추후 발생할 문제 관리자 API를 다음처럼 추가하면 모든 사용자가 거부됩니다. .requestMatchers("/admin/**").hasRole("ADMIN")와 같이 작성하는 경우 문제가 발생합니다.

// 예시 코드

 public enum MemberRole {                                                                                                                                                                                                          
      USER, ADMIN                                                                                                                                                                                                                   
  }                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                    
  @Enumerated(EnumType.STRING)                                                                                                                                                                                                      
  @Column(nullable = false)                                                                                                                                                                                                         
  private MemberRole role;                                                                                                                                                                                                          
  
  // 회원가입 시 기본값도 지정합니다.                                                                                                                                                                                                                                                                                                                                                                                                                                    
  .role(MemberRole.USER)                                                                                                                                                                                                            
                                                                                                                                                                                                                                    
  //마지막으로 AuthMember에서 Spring Security 권한으로 변환합니다.                                                                                                                                                                                                                                                                                                                                                                                                   
  @Override                                                                                                                                                                                                                         
  public Collection<? extends GrantedAuthority> getAuthorities() {                                                                                                                                                                  
      return List.of(                                                                                                                                                                                                               
          new SimpleGrantedAuthority("ROLE_" + member.getRole().name())                                                                                                                                                             
      );                                                                                                                                                                                                                            
  }       

요청 1건에 회원 조회가 2회 일어납니다.

  • JwtAuthFilter:37 → loadUserByUsername(email)로 Member를 이미 조회해 AuthMember 안에 담아둠 (1회)
  • MemberController:43 → authMember.getMember().getId()로 ID만 꺼내서 getMyPage() → MemberService:41에서 findById로 다시 조회 (2회)

해결법
AuthMember가 이미 Member 엔티티 전체(name, email, point)를 들고 있어서, 마이페이지에 필요한 데이터가 전부 들어있습니다. 굳이 id만 넘겨 재조회하지 말고 authMember.getMember() 자체를 컨버터에 넘기면 두 번째 쿼리를 없앨 수 있습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants