Skip to content

Conversation

@HyemIin
Copy link
Member

@HyemIin HyemIin commented Jun 3, 2025

작업 요약

  • 카카오 로그인 코드 수정

Issue Link

#10

문제점 및 어려움

해결 방안

Reference

Summary by CodeRabbit

  • 신규 기능

    • OAuth2 및 JWT 기반 인증/인가 기능이 추가되었습니다.
    • 카카오 소셜 로그인을 통한 회원 가입 및 로그인 지원이 도입되었습니다.
    • 액세스 토큰 및 리프레시 토큰 발급 및 갱신 API가 추가되었습니다.
    • 사용자 정보 조회 API(/api/users/me)가 제공됩니다.
    • 표준화된 에러 응답 및 글로벌 예외 처리 기능이 적용되었습니다.
    • CORS 정책이 설정되어 프론트엔드와의 연동이 개선되었습니다.
  • 스타일

    • 일부 클래스의 코드 포맷(들여쓰기, import 순서 등)이 정리되었습니다.
  • 문서화

    • 각종 DTO 및 엔티티에 대해 JSON 직렬화 및 빌더 패턴이 적용되었습니다.

HyemIin added 4 commits June 2, 2025 11:36
- TODO 임시 도메인 삭제
- jwt 토큰 발급 로직 구현
- CORS 설정
- 중복 메서드명 변경
- DTO 안정성 추가
@HyemIin HyemIin self-assigned this Jun 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jun 3, 2025

Walkthrough

이번 변경 사항은 Spring Boot 기반 애플리케이션에 OAuth2, JWT 기반 인증/인가 및 토큰 관리 기능을 도입하고, 사용자 및 토큰 도메인, 예외 처리, 시큐리티 설정 등 인증 인프라 전반을 세밀하게 구축하는 내용입니다. 코드 스타일 정비 및 의존성 정리도 함께 이루어졌습니다.

Changes

파일/경로 그룹 변경 요약
build.gradle 의존성 정리 및 주석 추가, Spring Security/OAuth2/JWT/Validation 관련 라이브러리 추가
src/main/java/com/example/gtable/global/api/... 코드 스타일(탭/공백, import 순서) 정비, 논리 변경 없음
.../config/CorsConfig.java, .../config/SecurityConfig.java CORS 및 Spring Security 설정 클래스 신규 추가, JWT/OAuth2 기반 인증/인가 및 CORS 정책 적용
.../security/exception/*.java 인증/인가 및 비즈니스 예외, 에러 메시지, 에러 응답, 글로벌 예외 핸들러 등 예외 처리 인프라 신규 추가
.../security/jwt/JwtAuthorizationFilter.java, .../security/jwt/JwtUtil.java JWT 토큰 발급, 검증, 필터링 로직 신규 구현
.../security/oauth2/* OAuth2 사용자 정보 처리, 로그인 성공 핸들러, 사용자 래퍼, DTO 및 인터페이스 신규 구현
.../token/controller/TokenController.java 리프레시 토큰 재발급 API 신규 추가
.../token/dto/* 인증 응답, 리프레시 토큰 요청 DTO 신규 추가
.../token/entity/Token.java, .../token/repository/TokenRepository.java, .../token/service/TokenService.java 토큰 엔티티, 저장소, 서비스 신규 구현 및 토큰 검증/갱신 로직 추가
.../user/controller/UserController.java 내 정보 조회 API 신규 추가
.../user/dto/UserResponseDto.java 사용자 응답 DTO 신규 추가
.../user/entity/Role.java, .../user/entity/SocialType.java, .../user/entity/User.java 사용자, 역할, 소셜타입 엔티티 및 열거형 신규 구현
.../user/repository/UserRepository.java 사용자 저장소 신규 추가
.../user/service/UserService.java 사용자 서비스 클래스 신규 추가 (기본 구조만)

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SpringSecurity
  participant OAuth2Provider
  participant CustomOAuth2UserService
  participant JwtUtil
  participant TokenRepository
  participant OAuth2LoginSuccessHandler

  Client->>SpringSecurity: /oauth2/authorization/kakao
  SpringSecurity->>OAuth2Provider: 인증 요청
  OAuth2Provider-->>SpringSecurity: 인증 코드/토큰
  SpringSecurity->>CustomOAuth2UserService: loadUser
  CustomOAuth2UserService->>UserRepository: 사용자 조회/생성
  CustomOAuth2UserService-->>SpringSecurity: OAuth2User 반환
  SpringSecurity->>OAuth2LoginSuccessHandler: 인증 성공 이벤트
  OAuth2LoginSuccessHandler->>JwtUtil: Access/Refresh 토큰 생성
  OAuth2LoginSuccessHandler->>TokenRepository: Refresh 토큰 저장
  OAuth2LoginSuccessHandler-->>Client: Access/Refresh 토큰 응답
Loading
sequenceDiagram
  participant Client
  participant TokenController
  participant JwtUtil
  participant TokenService
  participant TokenRepository

  Client->>TokenController: POST /api/refresh-token (refreshToken)
  TokenController->>JwtUtil: refreshToken에서 userId, role 추출
  TokenController->>TokenService: validateToken(refreshToken, userId)
  TokenService->>TokenRepository: findByUserId(userId)
  TokenService-->>TokenController: 유효성 결과 반환
  alt 토큰 유효
    TokenController->>JwtUtil: 새 access/refresh 토큰 생성
    TokenController->>TokenService: updateRefreshToken(userId, old, new)
    TokenService->>TokenRepository: 기존 토큰 삭제 및 신규 저장
    TokenController-->>Client: 새 토큰 응답
  else 토큰 만료/불일치
    TokenController-->>Client: 401 Unauthorized 에러 응답
  end
Loading

Poem

🌱
새로운 인증의 길이 열렸네,
JWT와 OAuth2가 손을 잡고,
토큰의 춤, 유저의 노래,
예외도 정성껏 품어 안고,
깔끔한 코드, 든든한 보안,
오늘도 성장하는 우리의 봄!
🌸


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

🧹 Nitpick comments (26)
src/main/java/com/example/gtable/global/security/exception/RefreshTokenNotFoundException.java (1)

3-9: 예외 처리 구조가 체계적이네요! 🎯

ResourceNotFoundException을 상속받아 예외 계층을 명확히 하고, ErrorMessage 상수를 사용하여 메시지를 중앙화한 것이 훌륭합니다. 이는 유지보수성과 일관성을 크게 향상시킵니다.

추후 더 유연한 에러 처리가 필요하다면, 다음과 같은 생성자 추가도 고려해보세요:

public RefreshTokenNotFoundException(String customMessage) {
    super(customMessage);
}

public RefreshTokenNotFoundException(Throwable cause) {
    super(ErrorMessage.REFRESH_TOKEN_NOT_FOUND, cause);
}
src/main/java/com/example/gtable/global/security/exception/TokenBadRequestException.java (1)

3-8: 비즈니스 예외 구분이 명확하고 좋습니다! 💡

BusinessException을 상속받아 토큰 관련 비즈니스 예외임을 명확히 구분한 것이 설계상 매우 적절합니다. RefreshTokenNotFoundException과 일관된 패턴을 사용하여 코드의 통일성도 잘 유지되고 있네요.

향후 다양한 토큰 에러 상황에 대응하기 위해, 선택적으로 아래와 같은 생성자들을 추가하는 것도 고려해보세요:

public TokenBadRequestException(String customMessage) {
    super(customMessage);
}

public TokenBadRequestException(ErrorMessage errorMessage) {
    super(errorMessage);
}
src/main/java/com/example/gtable/user/service/UserService.java (1)

9-14: UserService 클래스의 기본 구조가 잘 설정되어 있습니다.

Spring의 @Service 어노테이션과 Lombok의 @RequiredArgsConstructor를 적절히 사용하여 의존성 주입이 깔끔하게 구현되었습니다. 다만 현재 서비스 클래스에 실제 비즈니스 로직 메서드가 구현되어 있지 않은 상태입니다.

카카오 로그인 구현을 위해 다음과 같은 메서드들을 추가하시는 것을 권장드립니다:

  • findByEmail() - 이메일로 사용자 조회
  • saveUser() - 새 사용자 저장
  • updateUser() - 사용자 정보 업데이트

사용자 서비스의 기본 메서드 구현을 도와드릴까요?

src/main/java/com/example/gtable/global/security/exception/ResourceNotFoundException.java (1)

3-14: 리소스 예외 클래스의 일관된 구현이 훌륭합니다.

ResourceNotFoundExceptionBusinessException과 동일한 패턴을 따르고 있어 예외 계층구조의 일관성이 잘 유지되고 있습니다.

다만 한 가지 고려사항이 있습니다: 현재 BusinessException과 매우 유사한 구조를 가지고 있는데, 다음 중 하나를 검토해보시면 좋겠습니다:

  1. 상속 관계 설정: ResourceNotFoundException extends BusinessException으로 하여 계층구조를 명확히 하기
  2. 사용 목적 명확화: 두 예외 클래스의 구체적인 사용 시나리오를 문서화하기

이렇게 하면 개발팀이 언제 어떤 예외를 사용해야 하는지 더 명확하게 알 수 있을 것입니다.

src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java (1)

7-12: 토큰 요청 DTO가 깔끔하게 구현되었습니다.

이 클래스의 다음 요소들이 특히 잘 구현되었습니다:

  1. 적절한 유효성 검증: @NotBlank 어노테이션으로 빈 토큰을 방지합니다.
  2. 한국어 메시지: 사용자 친화적인 한국어 에러 메시지가 설정되어 있습니다.
  3. Lombok 활용: 보일러플레이트 코드를 효과적으로 줄였습니다.

사용성 개선을 위한 제안사항:
@AllArgsConstructor 어노테이션을 추가하시면 테스트 코드나 다른 서비스에서 이 DTO를 더 편리하게 생성할 수 있습니다.

 @Getter
 @NoArgsConstructor
+@AllArgsConstructor
 public class RefreshTokenRequest {
src/main/java/com/example/gtable/global/config/CorsConfig.java (1)

18-18: 환경별 설정 관리를 위한 개선 제안

현재 localhost:3000이 하드코딩되어 있어 환경별 배포시 문제가 될 수 있습니다.

환경별 설정을 위해 다음과 같은 개선을 고려해보시기 바랍니다:

+@Value("${app.cors.allowed-origins:http://localhost:3000}")
+private String allowedOrigins;

-config.setAllowedOrigins(List.of("http://localhost:3000"));
+config.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));

또는 application.yml에서 프로파일별로 관리하시는 것도 좋은 방법입니다.

src/main/java/com/example/gtable/global/security/oauth2/dto/OAuth2Response.java (1)

3-19: OAuth2 응답 인터페이스가 매우 깔끔하고 체계적으로 설계되었습니다.

인터페이스 설계가 매우 훌륭합니다:

  • 메서드명이 직관적이고 명확합니다
  • OAuth2 제공자로부터 필요한 핵심 정보들을 모두 포함하고 있습니다
  • 한국어 주석으로 각 메서드의 역할이 잘 설명되어 있습니다

더욱 체계적인 문서화를 위해 JavaDoc 주석을 추가하시는 것을 고려해보시기 바랍니다:

+/**
+ * OAuth2 제공자로부터 받은 사용자 정보를 표준화하는 인터페이스
+ * 
+ * @author GTable Team
+ */
public interface OAuth2Response {
+    /**
+     * OAuth2 제공자명을 반환합니다 (예: kakao, naver)
+     * 
+     * @return 제공자명
+     */
    // 제공자 (ex. naver, kakao)
    String getProvider();
src/main/java/com/example/gtable/global/security/exception/UnauthorizedException.java (1)

3-19: 잘 설계된 커스텀 예외 클래스입니다!

UnauthorizedException 클래스가 매우 깔끔하게 구현되었습니다. ErrorMessage를 활용한 구조화된 오류 처리와 적절한 생성자 체인이 인상적입니다.

다만 향후 확장성을 고려하여 직렬화 지원을 추가하시는 것을 고려해보세요.

선택적으로 직렬화 지원을 추가하시려면:

+import java.io.Serial;
+import java.io.Serializable;

-public class UnauthorizedException extends RuntimeException {
+public class UnauthorizedException extends RuntimeException implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+    
     private final ErrorMessage errorMessage;
src/main/java/com/example/gtable/global/security/exception/ErrorResponse.java (1)

20-24: 전체 매개변수 생성자가 잘 구현되었습니다

errors 맵을 직접 할당하는 방식이 효율적입니다. 다만 향후 불변성을 강화하고 싶으시다면 방어적 복사를 고려해보세요.

선택적으로 방어적 복사를 추가하시려면:

public ErrorResponse(String message, String code, Map<String, String> errors) {
    this.message = message;
    this.code = code;
-    this.errors = errors;
+    this.errors = errors != null ? new HashMap<>(errors) : new HashMap<>();
}
src/main/java/com/example/gtable/user/entity/Role.java (2)

12-14: Enum 필드명이 기존 name() 메서드와 혼동될 수 있습니다
Java Enum은 기본적으로 name() 메서드를 제공하기 때문에, 같은 식별자인 name 필드를 두면 가독성이 떨어지고 실수로 role.name()(메서드) 대신 role.getName()(필드)을 호출하는 혼란이 발생할 수 있습니다. 필드명을 label 혹은 code처럼 구분되는 용어로 바꾸면 유지보수성이 좋아집니다.

-private final String name;
+private final String label;

14-25: fromString 메서드의 성능·간결성 개선 제안
지금과 같이 매번 values()를 순회하면 열거형이 많아질 때 성능이 떨어집니다. 정적 Map<String, Role>을 미리 구성해두면 O(1)로 조회할 수 있고, 코드도 간결해집니다.

+private static final Map<String, Role> LOOKUP =
+        Arrays.stream(Role.values())
+              .collect(Collectors.toMap(r -> r.name.toLowerCase(), r -> r));

 public static Role fromString(String name) {
     if (name == null || name.trim().isEmpty()) {
         throw new IllegalArgumentException("Role name cannot be null or empty");
     }
-    for (Role role : Role.values()){
-        if (role.name.equalsIgnoreCase(name)){
-            return role;
-        }
-    }
-    throw new IllegalArgumentException("Unknown role: " + name);
+    Role role = LOOKUP.get(name.toLowerCase());
+    if (role == null) {
+        throw new IllegalArgumentException("Unknown role: " + name);
+    }
+    return role;
 }
src/main/java/com/example/gtable/global/config/SecurityConfig.java (3)

23-27: 필드 변수 네이밍 컨벤션 준수 권장
OAuth2LoginSuccessHandler 변수명이 대문자로 시작하여 Java · Spring 관례(camelCase)에 어긋납니다. 스타일 통일 차원에서 oauth2LoginSuccessHandler로 변경하면 좋겠습니다.

-private final com.example.gtable.global.security.oauth2.OAuth2LoginSuccessHandler OAuth2LoginSuccessHandler;
+private final com.example.gtable.global.security.oauth2.OAuth2LoginSuccessHandler oauth2LoginSuccessHandler;

50-58: 인가(end-point) 목록이 늘어날 때 가독성 확보 팁
requestMatchers에 URI를 하드코딩하면 추후 path 변화 시 실수 & 중복 위험이 있습니다. SecurityConstants 같은 클래스에 상수로 관리하거나, antMatchers("/oauth2/**")처럼 패턴 매칭을 사용하는 방식도 고려해 보세요.


58-60: JWT 필터를 Bean으로 등록하면 테스트가 더 수월해집니다
현재 new JwtAuthorizationFilter(jwtUtil)로 직접 생성해 넣으셨는데, Bean으로 정의해두면 모킹 및 커스텀 설정이 쉬워집니다.

src/main/java/com/example/gtable/user/dto/UserResponseDto.java (1)

9-17: role 필드를 문자열 대신 Enum으로 노출할지 고민해보세요
현재 String으로 전달하면 오타 · 대소문자 문제로 클라이언트 쪽에서 취약해질 수 있습니다. API 규격을 고정할 필요가 없다면 Role enum을 직접 직렬화하거나, 최소한 @Schema(allowableValues = {"USER","ADMIN"})로 제약을 명시하면 안정성이 올라갑니다.

src/main/java/com/example/gtable/token/controller/TokenController.java (1)

26-31: 토큰 추출 로직이 명확합니다

리프레시 토큰에서 사용자 ID와 역할을 추출하는 로직이 잘 구현되어 있습니다. 다만 JWT 파싱 과정에서 발생할 수 있는 예외 처리를 고려해보시면 좋을 것 같습니다.

더 안전한 예외 처리를 위해 다음과 같이 try-catch 블록을 추가하는 것을 고려해보세요:

-        // 리프레시 토큰 검증
-        Long userId = jwtUtil.getUserId(refreshToken);
-        String role = jwtUtil.getRole(refreshToken);
+        // 리프레시 토큰 검증
+        try {
+            Long userId = jwtUtil.getUserId(refreshToken);
+            String role = jwtUtil.getRole(refreshToken);
+        } catch (Exception e) {
+            log.error("Invalid refresh token format: {}", e.getMessage());
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token format");
+        }
src/main/java/com/example/gtable/global/security/exception/ErrorMessage.java (1)

1-22: 에러 메시지 관리가 체계적으로 잘 구성되어 있습니다!

한국어 메시지와 에러 코드를 조합한 구조가 매우 깔끔하고, 카테고리별로 분류도 잘 되어 있습니다. Lombok 어노테이션 사용도 적절합니다.

다만 향후 확장성을 고려하여 몇 가지 제안드립니다:

에러 코드 네이밍을 더 일관성 있게 개선하고, 주석을 추가하는 것을 고려해보세요:

 @Getter
 @RequiredArgsConstructor
 public enum ErrorMessage {
-	// global
-	INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "g001"),
+	// Global errors (G001-G999)
+	INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "G001"),
 
-	// auth
-	UNAUTHORIZED("권한이 없습니다", "a001"),
+	// Authentication errors (A001-A999)
+	UNAUTHORIZED("권한이 없습니다.", "A001"),
 
-	// token
-	REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "t001"),
-	DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "t002");
+	// Token errors (T001-T999)
+	REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "T001"),
+	DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "T002");
src/main/java/com/example/gtable/global/security/oauth2/CustomOAuth2UserService.java (2)

29-44: OAuth2 제공자 처리 로직이 명확합니다

카카오 로그인 지원과 미지원 제공자에 대한 예외 처리가 잘 구현되어 있습니다. 로깅도 디버깅에 도움이 될 것 같습니다.

향후 다른 OAuth2 제공자(구글, 네이버 등) 지원을 고려하여 팩토리 패턴을 적용하는 것을 고려해보세요:

private OAuth2Response createOAuth2Response(String registrationId, Map<String, Object> attributes) {
    return switch (registrationId.toLowerCase()) {
        case "kakao" -> new KaKaoResponse(attributes);
        case "google" -> new GoogleResponse(attributes); // 향후 추가
        case "naver" -> new NaverResponse(attributes);   // 향후 추가
        default -> throw new OAuth2AuthenticationException("지원하지 않는 OAuth2 Provider 입니다: " + registrationId);
    };
}

45-68: 사용자 등록/로그인 로직이 안전하게 구현되어 있습니다

이메일 기반 사용자 조회와 신규 사용자 등록 로직이 적절합니다. 기본 권한 설정과 소셜 타입 지정도 잘 되어 있네요.

중복 코드를 줄이고 가독성을 높이기 위해 다음과 같이 리팩토링을 고려해보세요:

         // DB에 유저가 있는지 판단
         Optional<User> foundUser = userRepository.findByEmail(oAuth2Response.getEmail());
-
-        // DB에 유저 없으면 - 회원가입
-        if (foundUser.isEmpty()) {
-
-            User user = User.builder()
-                .email(oAuth2Response.getEmail())
-                .nickname(oAuth2Response.getNickName())
-                .profileImage(oAuth2Response.getProfileImage())
-                .socialType(SocialType.KAKAO)
-                .role(Role.USER) // 일반 유저 설정
-                .build();
-
-            userRepository.save(user);
-
-            return new CustomOAuth2User(user);
-        } else {
-            // DB에 유저 존재하면 - 로그인 진행 (이때 로그인 처리는 안하고, OAuth2LoginSuccessHandler에서 담당함)
-            User user = foundUser.get();
-
-            return new CustomOAuth2User(user);
-        }
+        
+        User user = foundUser.orElseGet(() -> createAndSaveNewUser(oAuth2Response));
+        return new CustomOAuth2User(user);
+    }
+    
+    private User createAndSaveNewUser(OAuth2Response oAuth2Response) {
+        User newUser = User.builder()
+            .email(oAuth2Response.getEmail())
+            .nickname(oAuth2Response.getNickName())
+            .profileImage(oAuth2Response.getProfileImage())
+            .socialType(SocialType.KAKAO)
+            .role(Role.USER)
+            .build();
+        
+        return userRepository.save(newUser);
src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (1)

28-41: 권한 생성 방식이 적절합니다

사용자의 역할을 기반으로 GrantedAuthority를 생성하는 로직이 잘 구현되어 있습니다.

람다 표현식을 사용하여 코드를 더 간결하게 만들 수 있습니다:

 	@Override
 	public Collection<? extends GrantedAuthority> getAuthorities() {
-		Collection<GrantedAuthority> authorities = new ArrayList<>();
-
-		authorities.add(new GrantedAuthority() {
-			@Override
-			public String getAuthority() {
-				return user.getRole().getName(); // 유저의 권한 리턴
-			}
-		});
-
-		return authorities;
+		return Collections.singletonList(() -> user.getRole().getName());
 	}
src/main/java/com/example/gtable/token/entity/Token.java (1)

45-52: 정적 팩토리 메서드 네이밍을 개선해주세요.

toEntity 메서드명이 의도를 명확히 전달하지 못합니다. of 또는 create 같은 더 명확한 네이밍을 사용하는 것이 좋겠습니다.

-    public static Token toEntity(User user, String refreshToken, LocalDateTime expiredDate){
+    public static Token create(User user, String refreshToken, LocalDateTime expiredDate){
         return Token.builder()
                 .user(user)
                 .refreshToken(refreshToken)
                 .expiredDate(expiredDate)
                 .build();
     }
src/main/java/com/example/gtable/global/security/oauth2/OAuth2LoginSuccessHandler.java (1)

71-78: ObjectMapper 인스턴스 재사용을 고려해주세요.

매번 새로운 ObjectMapper 인스턴스를 생성하는 것보다 빈으로 주입받아 재사용하는 것이 성능상 더 효율적입니다.

클래스에 ObjectMapper를 주입받도록 수정해주세요:

 @Component
 @RequiredArgsConstructor
 @Slf4j
 public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
     private final JwtUtil jwtUtil;
     private final TokenRepository tokenRepository;
+    private final ObjectMapper objectMapper;

그리고 사용 부분을 다음과 같이 수정해주세요:

-        ObjectMapper objectMapper = new ObjectMapper();
         String body = objectMapper.writeValueAsString(
             Map.of(
                 "accessToken", accessToken,
                 "refreshToken", refreshToken
             )
         );
src/main/java/com/example/gtable/user/entity/User.java (1)

30-31: profileImage 필드에 URL 유효성 검증을 고려해주세요.

프로필 이미지가 URL 형태라면 유효한 URL인지 검증하는 로직을 엔티티나 서비스 레벨에서 추가하는 것을 고려해주세요.

필드에 검증 어노테이션을 추가하거나:

+    @Pattern(regexp = "^https?://.*", message = "올바른 URL 형식이어야 합니다.")
     @Column(nullable = false)
     private String profileImage;

또는 업데이트 메서드를 추가하여 검증 로직을 포함시켜 주세요.

src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java (1)

39-40: 과도한 로깅을 정리해주세요.

필터에서 매 요청마다 출력되는 단순한 번호 로그들이 너무 많아 로그 분석에 방해가 될 수 있습니다. 의미 있는 정보를 포함하는 로그로 개선하거나 제거해주세요.

-            log.info("JwtAuthorizationFilter 1 ");
+            log.debug("JWT token not found in Authorization header");
            return;
        }

        // ... 중간 생략 ...

-            log.info("JwtAuthorizationFilter 2 ");
+            log.warn("Access token expired for request: {}", request.getRequestURI());
            return;
        }

        // ... 중간 생략 ...

-            log.info("JwtAuthorizationFilter 3 ");
+            log.warn("Invalid access token category: {} for request: {}", tokenCategory, request.getRequestURI());
            return;
        }

        // ... 중간 생략 ...

-        log.info("JwtAuthorizationFilter 4 ");
+        log.debug("JWT authentication successful for user: {}", userId);

Also applies to: 55-56, 67-68, 87-87

src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java (2)

68-68: 로그 메시지의 오타 수정이 필요합니다

일부 로그 메시지에서 "Exception"이 중복으로 포함되어 있습니다. 가독성을 위해 수정해주시면 좋겠습니다.

-        log.error("handleMissingRequestValueExceptionException", e);
+        log.error("handleMissingRequestValueException", e);

-        log.error("handleUnauthorizedExceptionException", e);
+        log.error("handleUnauthorizedException", e);

-        log.error("handleResourceNotFoundExceptionException", e);
+        log.error("handleResourceNotFoundException", e);

Also applies to: 75-75, 82-82


93-103: 검증 오류 처리 로직의 복잡도 개선 제안

getErrors 메서드의 스트림 로직이 다소 복잡해 보입니다. 가독성을 높이기 위해 조금 더 명확하게 작성하는 것을 제안드립니다.

     private static Map<String, String> getErrors(MethodArgumentNotValidException e) {
         return e.getBindingResult()
             .getAllErrors()
             .stream()
-            .filter(ObjectError.class::isInstance)
             .collect(Collectors.toMap(
-                error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(),
+                error -> {
+                    if (error instanceof FieldError) {
+                        return ((FieldError) error).getField();
+                    }
+                    return error.getObjectName();
+                },
                 ObjectError::getDefaultMessage,
                 (msg1, msg2) -> msg1 + ";" + msg2
             ));
     }

이렇게 하면 filter를 제거하고 null 체크도 더 안전하게 할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da7f0da and 54efc75.

📒 Files selected for processing (38)
  • build.gradle (1 hunks)
  • src/main/java/com/example/gtable/GTableApplication.java (1 hunks)
  • src/main/java/com/example/gtable/TODO/TodoController.java (0 hunks)
  • src/main/java/com/example/gtable/TODO/TodoRepository.java (0 hunks)
  • src/main/java/com/example/gtable/TODO/TodoService.java (0 hunks)
  • src/main/java/com/example/gtable/global/api/ApiError.java (1 hunks)
  • src/main/java/com/example/gtable/global/api/ApiResult.java (1 hunks)
  • src/main/java/com/example/gtable/global/api/ApiUtils.java (1 hunks)
  • src/main/java/com/example/gtable/global/config/CorsConfig.java (1 hunks)
  • src/main/java/com/example/gtable/global/config/SecurityConfig.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/BusinessException.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/ErrorMessage.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/ErrorResponse.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/RefreshTokenNotFoundException.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/ResourceNotFoundException.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/TokenBadRequestException.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/exception/UnauthorizedException.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/oauth2/CustomOAuth2UserService.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/oauth2/OAuth2LoginSuccessHandler.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java (1 hunks)
  • src/main/java/com/example/gtable/global/security/oauth2/dto/OAuth2Response.java (1 hunks)
  • src/main/java/com/example/gtable/token/controller/TokenController.java (1 hunks)
  • src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java (1 hunks)
  • src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java (1 hunks)
  • src/main/java/com/example/gtable/token/entity/Token.java (1 hunks)
  • src/main/java/com/example/gtable/token/repository/TokenRepository.java (1 hunks)
  • src/main/java/com/example/gtable/token/service/TokenService.java (1 hunks)
  • src/main/java/com/example/gtable/user/controller/UserController.java (1 hunks)
  • src/main/java/com/example/gtable/user/dto/UserResponseDto.java (1 hunks)
  • src/main/java/com/example/gtable/user/entity/Role.java (1 hunks)
  • src/main/java/com/example/gtable/user/entity/SocialType.java (1 hunks)
  • src/main/java/com/example/gtable/user/entity/User.java (1 hunks)
  • src/main/java/com/example/gtable/user/repository/UserRepository.java (1 hunks)
  • src/main/java/com/example/gtable/user/service/UserService.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/com/example/gtable/TODO/TodoRepository.java
  • src/main/java/com/example/gtable/TODO/TodoController.java
  • src/main/java/com/example/gtable/TODO/TodoService.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/com/example/gtable/global/config/CorsConfig.java (1)
src/main/java/com/example/gtable/global/config/SecurityConfig.java (1)
  • Configuration (19-64)
src/main/java/com/example/gtable/token/controller/TokenController.java (1)
src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java (1)
  • RequiredArgsConstructor (8-17)
src/main/java/com/example/gtable/token/entity/Token.java (1)
src/main/java/com/example/gtable/user/entity/User.java (1)
  • Entity (16-65)
src/main/java/com/example/gtable/user/entity/User.java (1)
src/main/java/com/example/gtable/token/entity/Token.java (1)
  • Entity (21-54)
src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java (1)
src/main/java/com/example/gtable/global/security/oauth2/OAuth2LoginSuccessHandler.java (1)
  • Component (32-81)
🔇 Additional comments (30)
src/main/java/com/example/gtable/global/api/ApiResult.java (4)

4-6: 필드 들여쓰기 탭으로 일관화 확인 완료
들여쓰기가 탭으로 수정되어 가독성이 개선되었습니다. 로직에는 변화가 없습니다.


8-12: 생성자 들여쓰기 일관화 승인
생성자 내부 들여쓰기가 탭으로 바뀌어 스타일이 통일되었습니다. 기능적 변화는 없습니다.


14-16: isSuccess 메서드 들여쓰기 일관화 승인
메서드 정의와 내부 들여쓰기가 탭으로 적용되어 스타일 일관성이 유지됩니다.


18-20: getResponse 메서드 들여쓰기 일관화 승인
들여쓰기가 탭으로 통일되어 코드 스타일이 개선되었습니다.

src/main/java/com/example/gtable/GTableApplication.java (1)

9-11: main 메서드 들여쓰기 탭으로 일관화 승인
애플리케이션 진입점의 들여쓰기가 탭으로 통일되어 스타일 일관성이 유지됩니다.

src/main/java/com/example/gtable/global/api/ApiUtils.java (4)

3-4: import 순서 조정 및 들여쓰기 탭화 확인
HttpStatus import가 Lombok import 위로 이동하고 들여쓰기가 탭으로 통일되었습니다.


10-12: success 메서드 들여쓰기 일관화 승인
들여쓰기가 탭으로 적용되어 코드 스타일이 개선되었습니다.


14-16: error(Throwable) 메서드 들여쓰기 탭화 승인
들여쓰기가 탭으로 일관되게 적용되었습니다.


18-20: error(String) 메서드 들여쓰기 탭화 승인
메서드 구현부의 들여쓰기가 탭으로 수정되어 일관성을 유지합니다.

src/main/java/com/example/gtable/user/entity/SocialType.java (1)

1-5: SocialType enum 추가 확인
KAKAO, NAVER, GOOGLE 세 가지 소셜 로그인 타입이 명확하게 정의되어 있습니다. 기능적인 문제는 없어 보입니다.

src/main/java/com/example/gtable/global/api/ApiError.java (5)

5-5: lombok.Getter import 추가 및 들여쓰기 탭화 확인
Getter 기능을 위한 Lombok import가 추가되었고, 들여쓰기가 탭으로 일관화되었습니다.


9-10: 필드 선언 들여쓰기 탭화 승인
message와 status 필드 선언에 탭 들여쓰기가 적용되어 스타일이 통일되었습니다.


12-15: 첫 번째 생성자 들여쓰기 탭화 승인
String-기반 생성자의 내부 로직이 탭으로 들여쓰기되어 일관성을 유지합니다.


17-19: Throwable 기반 생성자 들여쓰기 탭화 승인
오버로드된 생성자의 들여쓰기가 탭으로 변경되어 코드 스타일이 개선되었습니다.


21-23: HttpStatus 기반 생성자 들여쓰기 탭화 승인
생성자의 들여쓰기가 탭으로 통일되어 가독성과 일관성이 확보되었습니다.

src/main/java/com/example/gtable/user/repository/UserRepository.java (1)

9-11: 훌륭한 구현입니다! 👍

Spring Data JPA의 표준 패턴을 잘 따르고 있으며, Optional<User>를 사용하여 null 안전성을 보장하는 것이 매우 좋습니다. findByEmail 메서드명도 Spring Data JPA의 네이밍 컨벤션을 정확히 따르고 있어 가독성이 뛰어납니다.

src/main/java/com/example/gtable/token/repository/TokenRepository.java (1)

9-11: 일관성 있고 깔끔한 구현이네요! ✨

UserRepository와 동일한 패턴을 사용하여 코드의 일관성을 잘 유지하고 있습니다. findByUserId 메서드는 사용자별 토큰 관리에 필수적인 기능이며, Optional<Token>을 반환하여 안전한 토큰 조회를 지원합니다.

src/main/java/com/example/gtable/global/security/exception/BusinessException.java (1)

3-14: 예외 처리 기반 클래스가 매우 잘 설계되었습니다.

BusinessException 클래스는 다음과 같은 우수한 설계 패턴을 보여줍니다:

  1. 추상 클래스 사용: 구체적인 예외 클래스들이 이를 상속받도록 강제하여 일관성을 보장합니다.
  2. ErrorMessage 캡슐화: 에러 메시지를 객체로 캡슐화하여 구조화된 에러 처리가 가능합니다.
  3. 적절한 상속: RuntimeException을 상속받아 체크되지 않는 예외로 구현했습니다.
  4. 코드 접근자 제공: getCode() 메서드로 에러 코드에 쉽게 접근할 수 있습니다.

이러한 구조는 글로벌 예외 처리와 함께 사용했을 때 매우 효과적일 것입니다.

build.gradle (1)

37-48: 의존성 구성이 카카오 로그인 구현에 적합하게 잘 설정되었습니다.

OAuth2, JWT, Spring Security 관련 의존성들이 체계적으로 정리되어 있고, 명확한 주석으로 그룹화되어 있어 관리하기 좋습니다. 특히 JWT 라이브러리의 버전이 명시적으로 지정된 점이 인상적입니다.

src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java (1)

8-17: 보안을 고려한 훌륭한 DTO 설계입니다!

AuthenticationResponse 클래스가 매우 잘 구현되었습니다. 특히 @ToString(exclude = {"accessToken", "refreshToken"}) 어노테이션으로 로깅 시 민감한 토큰 정보 노출을 방지하신 점이 매우 훌륭합니다. JSON 매핑과 불변 객체 설계도 완벽합니다.

src/main/java/com/example/gtable/global/security/exception/ErrorResponse.java (1)

14-18: 깔끔한 기본 생성자 구현입니다

빈 HashMap으로 errors를 초기화하는 방식이 적절합니다. 깔끔하고 이해하기 쉬운 구현이네요.

src/main/java/com/example/gtable/user/entity/Role.java (1)

8-11: 역할(Enum) 정의 깔끔합니다!
USER, ADMIN 두 가지 권한을 명확히 열거형으로 선언하신 덕분에 타입 안정성이 크게 향상되었습니다. 👍

src/main/java/com/example/gtable/user/dto/UserResponseDto.java (1)

9-11: Builder 패턴과 @RequiredArgsConstructor 조합 적절합니다!
불변 DTO를 깔끔히 생성할 수 있어 👍

src/main/java/com/example/gtable/token/controller/TokenController.java (1)

1-24: 컨트롤러 구조가 깔끔하게 잘 구성되어 있습니다!

패키지 구조와 의존성 주입이 적절하게 설정되어 있고, Lombok을 활용한 코드도 간결합니다. 로깅 설정도 포함되어 있어 디버깅에 도움이 될 것 같습니다.

src/main/java/com/example/gtable/global/security/oauth2/CustomOAuth2UserService.java (1)

1-28: OAuth2 서비스 구조가 매우 잘 설계되어 있습니다!

DefaultOAuth2UserService를 상속받아 커스터마이징한 접근 방식이 좋고, 의존성 주입과 로깅 설정도 적절합니다. 주석으로 서비스의 역할도 명확하게 설명되어 있네요.

src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (2)

43-60: 편의 메서드들이 잘 구성되어 있습니다

User 객체와 주요 속성들에 대한 접근 메서드들이 적절하게 제공되어 있어 사용하기 편리할 것 같습니다.


14-21: ⚠️ Potential issue

생성자 중복 정의가 있습니다

@requiredargsconstructor와 명시적 생성자가 중복되어 있어 혼란을 야기할 수 있습니다.

다음 중 하나를 선택하여 수정해주세요:

옵션 1: @requiredargsconstructor 사용

 @RequiredArgsConstructor
 public class CustomOAuth2User implements OAuth2User {
-	private User user;
+	private final User user;
 
-	// User 객체를 받는 생성자
-	public CustomOAuth2User(User user) {
-		this.user = user;
-	}

옵션 2: 명시적 생성자만 사용

-@RequiredArgsConstructor
 public class CustomOAuth2User implements OAuth2User {
 	private User user;
 
 	// User 객체를 받는 생성자
 	public CustomOAuth2User(User user) {
 		this.user = user;
 	}

Likely an incorrect or invalid review comment.

src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java (1)

19-24: 시크릿 키 초기화 로직이 잘 구현되었습니다

시크릿 키를 HS256 알고리즘으로 안전하게 초기화하고 있어 보안상 좋은 접근입니다. 생성자에서 즉시 초기화하여 불변성도 보장하고 있네요.

src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java (2)

27-34: OAuth2 인증 예외 처리가 잘 구현되었습니다

OAuth2 인증 실패에 대한 적절한 예외 처리와 로깅이 구현되어 있어 보안 관점에서 좋은 접근입니다. 사용자에게도 명확한 오류 메시지를 제공하고 있네요.


23-25: 전역 예외 처리기 구조가 훌륭합니다

포괄적인 예외 처리 커버리지와 일관된 로깅 패턴, 구조화된 오류 응답 형식이 잘 구현되어 있습니다. Spring의 @RestControllerAdvice를 적절히 활용하여 API 전반의 예외 처리를 중앙화한 점이 특히 좋습니다.

Comment on lines +25 to +33
public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){
User user = customOAuth2User.getUser();

UserResponseDto userResponseDto = UserResponseDto.builder()
.userId(user.getId())
.role(user.getRole().getName())
.build();
return ResponseEntity.ok(userResponseDto);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

사용자 정보 조회 로직이 잘 구현되었습니다만, 안전성을 위해 몇 가지 개선사항을 제안드립니다.

현재 구현은 깔끔하고 이해하기 쉽지만, null 안전성과 에러 처리 측면에서 보완이 필요합니다:

  1. CustomOAuth2User가 null일 경우에 대한 검증이 없습니다
  2. customOAuth2User.getUser()가 null을 반환할 가능성에 대한 처리가 필요합니다

다음과 같은 개선을 제안드립니다:

@GetMapping("/me")
public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){
+    if (customOAuth2User == null) {
+        log.warn("인증되지 않은 사용자의 정보 조회 시도");
+        throw new AuthenticationException("인증되지 않은 사용자입니다");
+    }
+    
    User user = customOAuth2User.getUser();
+    if (user == null) {
+        log.error("사용자 정보를 찾을 수 없습니다. OAuth2User: {}", customOAuth2User);
+        throw new UserNotFoundException("사용자 정보를 찾을 수 없습니다");
+    }

+    log.info("사용자 정보 조회 성공: userId={}", user.getId());
    UserResponseDto userResponseDto = UserResponseDto.builder()
            .userId(user.getId())
            .role(user.getRole().getName())
            .build();
    return ResponseEntity.ok(userResponseDto);
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){
User user = customOAuth2User.getUser();
UserResponseDto userResponseDto = UserResponseDto.builder()
.userId(user.getId())
.role(user.getRole().getName())
.build();
return ResponseEntity.ok(userResponseDto);
}
@GetMapping("/me")
public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User) {
if (customOAuth2User == null) {
log.warn("인증되지 않은 사용자의 정보 조회 시도");
throw new AuthenticationException("인증되지 않은 사용자입니다");
}
User user = customOAuth2User.getUser();
if (user == null) {
log.error("사용자 정보를 찾을 수 없습니다. OAuth2User: {}", customOAuth2User);
throw new UserNotFoundException("사용자 정보를 찾을 수 없습니다");
}
log.info("사용자 정보 조회 성공: userId={}", user.getId());
UserResponseDto userResponseDto = UserResponseDto.builder()
.userId(user.getId())
.role(user.getRole().getName())
.build();
return ResponseEntity.ok(userResponseDto);
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/user/controller/UserController.java around
lines 25 to 33, add null checks for the customOAuth2User parameter and the user
object returned by customOAuth2User.getUser(). If either is null, return an
appropriate error response such as ResponseEntity.badRequest() or
ResponseEntity.status(HttpStatus.UNAUTHORIZED) with a meaningful message. This
will improve null safety and error handling in the getMyInfo method.

Comment on lines +17 to +22
config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
// config.setAllowCredentials(true); // 쿠키 포함 허용
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

CORS 설정이 잘 구성되었으나 중복 코드와 보안 고려사항이 있습니다.

코드에서 발견된 개선점들:

  1. 17번 라인과 22번 라인(주석 처리됨)에 setAllowCredentials(true) 호출이 중복되어 있습니다
  2. 현재 설정은 개발 환경에 적합하나, 운영 환경에서는 보안 검토가 필요합니다

다음과 같은 개선을 제안드립니다:

config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
-//  config.setAllowCredentials(true); // 쿠키 포함 허용
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
// config.setAllowCredentials(true); // 쿠키 포함 허용
config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); // 클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); // 클라이언트(브라우저)가 접근할 수 있는 헤더 지정
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/config/CorsConfig.java around lines
17 to 22, remove the duplicated call to setAllowCredentials(true) by deleting
the commented-out line 22 to avoid redundancy. Additionally, ensure that the
allowed origins list is configured dynamically or restricted appropriately for
production environments to enhance security, rather than hardcoding
"http://localhost:3000".



// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복된 테스트 의존성을 발견했습니다.

31번 라인과 46번 라인에 spring-boot-starter-test 의존성이 중복으로 선언되어 있습니다.

다음과 같이 중복을 제거해주시기 바랍니다:

// Web MVC
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
-testImplementation 'org.springframework.boot:spring-boot-starter-test'
// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// TEST
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// ... (다른 의존성들)
// SPRING SECURITY
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
🤖 Prompt for AI Agents
In build.gradle at line 31, the dependency 'spring-boot-starter-test' is
declared twice, once at line 31 and again at line 46. Remove the duplicate
declaration at line 31 to eliminate redundancy and keep only one instance of
this test dependency.

Comment on lines +23 to +26
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
return kakaoAccount.get("email").toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 추출 시 중첩 맵 안전성을 강화해주세요

kakao_account 맵이나 email 필드가 존재하지 않을 경우 예외가 발생할 수 있습니다. 카카오 API 응답의 선택적 필드들을 안전하게 처리하기 위해 null 체크와 타입 검증을 추가하시면 좋겠습니다.

안전한 구현으로 개선해보세요:

@Override
public String getEmail() {
-    Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
-    return kakaoAccount.get("email").toString();
+    Object kakaoAccountObj = attributes.get("kakao_account");
+    if (!(kakaoAccountObj instanceof Map)) {
+        throw new IllegalStateException("Invalid kakao_account structure in response");
+    }
+    Map<String, Object> kakaoAccount = (Map<String, Object>) kakaoAccountObj;
+    Object email = kakaoAccount.get("email");
+    if (email == null) {
+        throw new IllegalStateException("Email not available in Kakao response");
+    }
+    return email.toString();
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
return kakaoAccount.get("email").toString();
}
@Override
public String getEmail() {
Object kakaoAccountObj = attributes.get("kakao_account");
if (!(kakaoAccountObj instanceof Map)) {
throw new IllegalStateException("Invalid kakao_account structure in response");
}
Map<String, Object> kakaoAccount = (Map<String, Object>) kakaoAccountObj;
Object email = kakaoAccount.get("email");
if (email == null) {
throw new IllegalStateException("Email not available in Kakao response");
}
return email.toString();
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java
around lines 23 to 26, the getEmail method assumes the presence of the
"kakao_account" map and the "email" field without null checks, which can cause
exceptions if these are missing. To fix this, add null checks to verify that the
"kakao_account" map exists and is of the expected type, then check if the
"email" key is present and non-null before returning its string value. If any
check fails, handle it gracefully, for example by returning null or an empty
string.

Comment on lines +18 to +20
public String getProviderId() {
return attributes.get("id").toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Provider ID 추출 시 null 안전성을 고려해주세요

현재 구현에서는 attributes.get("id")가 null을 반환할 경우 NullPointerException이 발생할 수 있습니다. 카카오 응답에서 id 필드가 누락될 가능성을 대비하여 null 체크를 추가하시는 것을 권장드립니다.

다음과 같이 안전한 구현으로 개선해보세요:

@Override
public String getProviderId() {
-    return attributes.get("id").toString();
+    Object id = attributes.get("id");
+    if (id == null) {
+        throw new IllegalStateException("Kakao response does not contain required 'id' field");
+    }
+    return id.toString();
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getProviderId() {
return attributes.get("id").toString();
}
@Override
public String getProviderId() {
Object id = attributes.get("id");
if (id == null) {
throw new IllegalStateException("Kakao response does not contain required 'id' field");
}
return id.toString();
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java
around lines 18 to 20, the getProviderId method directly calls toString() on
attributes.get("id") without checking for null, which can cause a
NullPointerException if "id" is missing. To fix this, add a null check before
calling toString(), returning a default value or null safely when the "id"
attribute is absent.

Comment on lines +64 to +65
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print("invalid access token");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

에러 응답 일관성을 위해 JSON 형식을 사용해주세요.

위와 마찬가지로 invalid access token 에러도 JSON 형식으로 통일해주세요.

-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            response.getWriter().print("invalid access token");
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.setContentType("application/json");
+            response.setCharacterEncoding("utf-8");
+            response.getWriter().write("{\"error\":\"INVALID_ACCESS_TOKEN\",\"message\":\"유효하지 않은 액세스 토큰입니다.\"}");
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java
at lines 64 to 65, the error response for an invalid access token is currently a
plain text message. To maintain consistency in error responses, change this to
return a JSON formatted response. Set the response content type to
"application/json" and format the error message as a JSON object with an
appropriate key, such as {"error": "invalid access token"}.

Comment on lines +25 to +50
@Transactional
public Boolean validateToken(String token, Long userId){
// DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다.
Optional<Token> savedToken = tokenRepository.findByUserId(userId);

// DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음
if (savedToken.isEmpty()){
log.info("여기에 걸렸니 ? -- 1 ");
return false;
}

// 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인
if (!savedToken.get().getRefreshToken().equals(token)){
log.info("여기에 걸렸니 ? -- 2 ");
return false;
}

// 리프레시 토큰의 만료여부 확인
if(jwtUtil.isExpired(token)){
log.info("여기에 걸렸니 ? -- 3 ");
return false; // 만료된 토큰은 유효하지 않음
}

log.info("여기에 걸렸니 ? -- 4 ");
return true; // 모든 조건 만족 시, 유효한 토큰
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

토큰 검증 로직 개선이 필요합니다

토큰 검증 메서드에서 몇 가지 개선사항을 제안드립니다:

  1. 읽기 전용 트랜잭션 사용: 데이터 조회만 하는 메서드이므로 @Transactional(readOnly = true)를 사용하는 것이 좋겠습니다.
  2. 디버그 로그 제거: 프로덕션 코드에서 "여기에 걸렸니?" 같은 디버그 로그는 제거해주세요.
  3. 입력값 검증 추가: tokenuserId 매개변수에 대한 null 체크가 필요합니다.
-    @Transactional
+    @Transactional(readOnly = true)
     public Boolean validateToken(String token, Long userId){
+        if (token == null || userId == null) {
+            log.warn("Token validation failed: null parameter provided");
+            return false;
+        }
+        
         // DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다.
         Optional<Token> savedToken = tokenRepository.findByUserId(userId);
         
         // DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음
         if (savedToken.isEmpty()){
-            log.info("여기에 걸렸니 ? -- 1 ");
+            log.debug("No refresh token found for userId: {}", userId);
             return false;
         }
         
         // 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인
         if (!savedToken.get().getRefreshToken().equals(token)){
-            log.info("여기에 걸렸니 ? -- 2 ");
+            log.debug("Refresh token mismatch for userId: {}", userId);
             return false;
         }
         
         // 리프레시 토큰의 만료여부 확인
         if(jwtUtil.isExpired(token)){
-            log.info("여기에 걸렸니 ? -- 3 ");
+            log.debug("Refresh token expired for userId: {}", userId);
             return false; // 만료된 토큰은 유효하지 않음
         }

-        log.info("여기에 걸렸니 ? -- 4 ");
+        log.debug("Token validation successful for userId: {}", userId);
         return true; // 모든 조건 만족 시, 유효한 토큰
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional
public Boolean validateToken(String token, Long userId){
// DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다.
Optional<Token> savedToken = tokenRepository.findByUserId(userId);
// DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음
if (savedToken.isEmpty()){
log.info("여기에 걸렸니 ? -- 1 ");
return false;
}
// 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인
if (!savedToken.get().getRefreshToken().equals(token)){
log.info("여기에 걸렸니 ? -- 2 ");
return false;
}
// 리프레시 토큰의 만료여부 확인
if(jwtUtil.isExpired(token)){
log.info("여기에 걸렸니 ? -- 3 ");
return false; // 만료된 토큰은 유효하지 않음
}
log.info("여기에 걸렸니 ? -- 4 ");
return true; // 모든 조건 만족 시, 유효한 토큰
}
@Transactional(readOnly = true)
public Boolean validateToken(String token, Long userId){
if (token == null || userId == null) {
log.warn("Token validation failed: null parameter provided");
return false;
}
// DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다.
Optional<Token> savedToken = tokenRepository.findByUserId(userId);
// DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음
if (savedToken.isEmpty()){
log.debug("No refresh token found for userId: {}", userId);
return false;
}
// 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인
if (!savedToken.get().getRefreshToken().equals(token)){
log.debug("Refresh token mismatch for userId: {}", userId);
return false;
}
// 리프레시 토큰의 만료여부 확인
if (jwtUtil.isExpired(token)){
log.debug("Refresh token expired for userId: {}", userId);
return false; // 만료된 토큰은 유효하지 않음
}
log.debug("Token validation successful for userId: {}", userId);
return true; // 모든 조건 만족 시, 유효한 토큰
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/token/service/TokenService.java lines 25 to
50, improve the validateToken method by changing the @Transactional annotation
to @Transactional(readOnly = true) since it only reads data. Remove all debug
log statements like "여기에 걸렸니?" to clean up production logs. Add null checks for
the token and userId parameters at the start of the method to prevent null
pointer exceptions and handle invalid inputs gracefully.

Comment on lines +52 to +65
@Transactional
public void updateRefreshToken(Long userId, String oldRefreshToken, String newRefreshToken){
Token token = tokenRepository.findByUserId(userId)
.orElseThrow(RefreshTokenNotFoundException::new); // 404

if (!token.getRefreshToken().equals(oldRefreshToken)){
throw new TokenBadRequestException(); // 400
}

// 기존 토큰 삭제 및 새 토큰 저장
tokenRepository.delete(token);
Token newToken = Token.toEntity(token.getUser(), newRefreshToken, LocalDateTime.now().plusDays(30));
tokenRepository.save(newToken);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

리프레시 토큰 업데이트 로직 최적화 제안

현재 삭제 후 재생성하는 방식보다는 기존 엔티티를 업데이트하는 방식이 더 효율적일 것 같습니다. 또한 만료 기간을 설정값으로 외부화하는 것을 권장드립니다.

+    @Value("${jwt.refresh-token.expire-days:30}")
+    private int refreshTokenExpireDays;

     @Transactional
     public void updateRefreshToken(Long userId, String oldRefreshToken, String newRefreshToken){
+        if (userId == null || oldRefreshToken == null || newRefreshToken == null) {
+            throw new IllegalArgumentException("Parameters cannot be null");
+        }
+        
         Token token = tokenRepository.findByUserId(userId)
                 .orElseThrow(RefreshTokenNotFoundException::new); // 404

         if (!token.getRefreshToken().equals(oldRefreshToken)){
             throw new TokenBadRequestException(); // 400
         }

-        // 기존 토큰 삭제 및 새 토큰 저장
-        tokenRepository.delete(token);
-        Token newToken = Token.toEntity(token.getUser(), newRefreshToken, LocalDateTime.now().plusDays(30));
-        tokenRepository.save(newToken);
+        // 기존 토큰 업데이트
+        token.updateRefreshToken(newRefreshToken, LocalDateTime.now().plusDays(refreshTokenExpireDays));
+        tokenRepository.save(token);
     }

참고: 이 방법을 사용하려면 Token 엔티티에 updateRefreshToken 메서드 추가가 필요합니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/token/service/TokenService.java around lines
52 to 65, instead of deleting the existing token entity and creating a new one,
modify the method to update the refresh token and expiration date directly on
the existing Token entity. Add an updateRefreshToken method to the Token entity
to encapsulate this update logic. Also, externalize the token expiration
duration to a configuration property rather than hardcoding it, and use this
value when setting the new expiration date.

Comment on lines +26 to +45
public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) {
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // accessToken
.claim("userId", userId)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}

public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) {
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // refreshToken
.claim("userId", userId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

토큰 생성 메서드에 입력값 검증 추가가 필요합니다

토큰 생성 메서드들이 잘 구조화되어 있지만, 입력값에 대한 검증이 누락되어 있습니다. null 값이나 잘못된 값이 전달될 경우 적절한 예외 처리가 필요합니다.

     public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) {
+        if (tokenCategory == null || userId == null || role == null || expiredMs == null || expiredMs <= 0) {
+            throw new IllegalArgumentException("Invalid token creation parameters");
+        }
         return Jwts.builder()
             .claim("tokenCategory", tokenCategory) // accessToken
             .claim("userId", userId)
             .claim("role", role)
             .issuedAt(new Date(System.currentTimeMillis()))
             .expiration(new Date(System.currentTimeMillis() + expiredMs))
             .signWith(secretKey)
             .compact();
     }

     public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) {
+        if (tokenCategory == null || userId == null || expiredMs == null || expiredMs <= 0) {
+            throw new IllegalArgumentException("Invalid token creation parameters");
+        }
         return Jwts.builder()
             .claim("tokenCategory", tokenCategory) // refreshToken
             .claim("userId", userId)
             .issuedAt(new Date(System.currentTimeMillis()))
             .expiration(new Date(System.currentTimeMillis() + expiredMs))
             .signWith(secretKey)
             .compact();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) {
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // accessToken
.claim("userId", userId)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) {
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // refreshToken
.claim("userId", userId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) {
if (tokenCategory == null || userId == null || role == null || expiredMs == null || expiredMs <= 0) {
throw new IllegalArgumentException("Invalid token creation parameters");
}
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // accessToken
.claim("userId", userId)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) {
if (tokenCategory == null || userId == null || expiredMs == null || expiredMs <= 0) {
throw new IllegalArgumentException("Invalid token creation parameters");
}
return Jwts.builder()
.claim("tokenCategory", tokenCategory) // refreshToken
.claim("userId", userId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java around
lines 26 to 45, the token creation methods lack input validation. Add checks at
the start of createAccessToken and createRefreshToken to verify that parameters
like tokenCategory, userId, role (for access token), and expiredMs are not null
or invalid. If any input is invalid, throw an appropriate exception to prevent
creating tokens with bad data.

Comment on lines +47 to +74
public String getTokenCategory(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("tokenCategory", String.class);
}

public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("role", String.class);
}

public Long getUserId(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("userId", Long.class);
}

public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

토큰 파싱 메서드의 일관성과 예외 처리 개선이 필요합니다

토큰 파싱 메서드들에서 두 가지 개선사항을 제안드립니다:

  1. 메서드 일관성: isExpired 메서드에서는 parseSignedClaims를 사용하고, 다른 메서드들은 parseClaimsJws를 사용하고 있어 일관성이 필요합니다.
  2. 예외 처리: JWT 파싱 시 발생할 수 있는 예외들에 대한 적절한 처리가 필요합니다.
     public String getTokenCategory(String token) {
+        if (token == null || token.trim().isEmpty()) {
+            throw new IllegalArgumentException("Token cannot be null or empty");
+        }
+        try {
             return Jwts.parser().verifyWith(secretKey).build()
                 .parseClaimsJws(token)
                 .getBody()
                 .get("tokenCategory", String.class);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid token format", e);
+        }
     }

     public String getRole(String token) {
+        if (token == null || token.trim().isEmpty()) {
+            throw new IllegalArgumentException("Token cannot be null or empty");
+        }
+        try {
             return Jwts.parser().verifyWith(secretKey).build()
                 .parseClaimsJws(token)
                 .getBody()
                 .get("role", String.class);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid token format", e);
+        }
     }

     public Long getUserId(String token) {
+        if (token == null || token.trim().isEmpty()) {
+            throw new IllegalArgumentException("Token cannot be null or empty");
+        }
+        try {
             return Jwts.parser().verifyWith(secretKey).build()
                 .parseClaimsJws(token)
                 .getBody()
                 .get("userId", Long.class);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid token format", e);
+        }
     }

     public Boolean isExpired(String token) {
+        if (token == null || token.trim().isEmpty()) {
+            return true; // null이나 빈 토큰은 만료된 것으로 간주
+        }
+        try {
-            return Jwts.parser().verifyWith(secretKey).build()
-                .parseSignedClaims(token)
-                .getPayload()
+            return Jwts.parser().verifyWith(secretKey).build()
+                .parseClaimsJws(token)
+                .getBody()
                 .getExpiration()
                 .before(new Date());
+        } catch (Exception e) {
+            return true; // 파싱 실패 시 만료된 것으로 간주
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getTokenCategory(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("tokenCategory", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("role", String.class);
}
public Long getUserId(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("userId", Long.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
public String getTokenCategory(String token) {
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("Token cannot be null or empty");
}
try {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("tokenCategory", String.class);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid token format", e);
}
}
public String getRole(String token) {
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("Token cannot be null or empty");
}
try {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("role", String.class);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid token format", e);
}
}
public Long getUserId(String token) {
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("Token cannot be null or empty");
}
try {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.get("userId", Long.class);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid token format", e);
}
}
public Boolean isExpired(String token) {
if (token == null || token.trim().isEmpty()) {
return true; // null이나 빈 토큰은 만료된 것으로 간주
}
try {
return Jwts.parser().verifyWith(secretKey).build()
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date());
} catch (Exception e) {
return true; // 파싱 실패 시 만료된 것으로 간주
}
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java between
lines 47 and 74, ensure all token parsing methods consistently use the same
parsing approach, preferably replacing parseSignedClaims in isExpired with
parseClaimsJws to match the others. Additionally, wrap the JWT parsing logic in
try-catch blocks to handle potential exceptions gracefully, such as signature
verification failures or token expiration, and return appropriate default values
or rethrow custom exceptions as needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
build.gradle (1)

50-50: ⚠️ Potential issue

중복된 테스트 의존성 제거 필요
testImplementation 'org.springframework.boot:spring-boot-starter-test'가 2회 선언되어 있습니다(35번 & 50번). 하나만 남기고 중복을 제거해주세요.

🧹 Nitpick comments (2)
build.gradle (2)

27-27: 그룹 주석 스타일 일관성 제안
현재 // Web MVC, // test, // DB, // TEST, // OAUTH2, // jwt, // SPRING SECURITY, // Validation 등 그룹 주석의 대소문자 사용이 혼재되어 있습니다.
모든 그룹 주석을 하나의 컨벤션(예: 각 단어 첫 글자만 대문자, 모두 대문자 등)으로 통일하는 것을 권장드립니다.

Also applies to: 36-36, 39-39, 41-41, 44-44, 48-48, 51-51


45-47: JJWT 의존성 구성 최적화 제안
현재 jjwt-api, jjwt-impl, jjwt-jackson 모두 implementation으로 선언되어 있습니다.

  • jjwt-apiapi로,
  • jjwt-impljjwt-jacksonruntimeOnly로 분리하면 빌드 산출물 크기를 줄이고 의존성 스코프를 명확히 할 수 있습니다.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54efc75 and 4b81dba.

📒 Files selected for processing (1)
  • build.gradle (1 hunks)
🔇 Additional comments (5)
build.gradle (5)

37-38: DB 의존성 설정이 적절합니다
spring-boot-starter-data-jpamysql-connector-j가 명확하게 분리되어 있어 이해하기 쉽고 유지보수에도 용이합니다.


42-43: OAuth2 의존성 추가 확인
OAuth2 클라이언트(spring-boot-starter-oauth2-client)와 리소스 서버(spring-boot-starter-oauth2-resource-server) 의존성이 올바르게 추가되었습니다.


49-49: Spring Security 의존성 추가 확인
spring-boot-starter-security 의존성이 잘 추가되어 인증/인가 기능 구현에 필요한 기본 구성이 갖춰졌습니다.


52-52: Validation 의존성 추가 확인
spring-boot-starter-validation이 추가되어 Bean Validation 사용이 가능해졌습니다. 적절한 변경입니다.


53-53:

@HyemIin HyemIin merged commit d890665 into develop Jun 3, 2025
1 check passed
@HyemIin HyemIin deleted the #10-카카오로그인구현 branch June 3, 2025 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants