Skip to content

Conversation

bricksky
Copy link
Contributor

@bricksky bricksky commented Jun 30, 2025

#️⃣ Issue Number

📝 요약(Summary)

이번 PR에서는 Apple 로그인 기능을 완전히 구현했습니다.

🔑 주요 구현 사항

1. Apple OAuth 인증 플로우

  • AppleOAuthService: Apple 로그인/회원가입 비즈니스 로직
  • AppleTokenService: Apple 서버와의 토큰 교환 및 사용자 정보 추출
  • Apple 인가 코드 → 액세스 토큰 → 사용자 정보 추출 전체 플로우

2. JWT 토큰 검증 시스템

  • JwtValidator: Apple ID 토큰의 서명 및 클레임 검증
  • ApplePublicKeyProvider: Apple 공개키 캐싱 및 관리
  • AppleJwtGenerator: Apple client secret JWT 생성

3. API 엔드포인트 추가

  • POST /auth/apple: Apple 로그인
  • POST /auth/apple/signup: Apple 회원가입

4. 설정 및 데이터 모델

  • AppleProperties: Apple 인증 관련 설정값 바인딩
  • AppleLoginRequest, AppleSignUpRequest, AppleUserResponse: 요청/응답 모델
  • WebClientConfig: HTTP 클라이언트 설정

5. 에러 처리

  • Apple 인증 관련 6개 에러 코드 추가 (사용자 없음, 토큰 요청 실패, 클라이언트 시크릿 생성 실패 등)

💬 공유사항 to 리뷰어

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

Summary by CodeRabbit

New Features

  • 애플 OAuth 로그인을 지원하는 새로운 엔드포인트가 추가되었습니다.
  • 애플 계정으로 회원가입할 수 있는 기능이 도입되었습니다.
  • 애플 인증을 위한 JWT 생성, 공개키 제공, 토큰 검증 및 토큰 교환 기능이 추가되었습니다.
  • 애플 로그인 및 회원가입 요청/응답 데이터 모델이 API 문서에 반영되었습니다.

Bug Fixes

  • 일부 파일의 불필요한 공백과 들여쓰기 문제가 수정되었습니다.

Copy link

coderabbitai bot commented Jun 30, 2025

## Walkthrough

애플 OAuth 인증 및 회원가입 기능이 새롭게 도입되었습니다. 이를 위해 여러 보안 컴포넌트, 서비스, DTO, 예외 코드가 추가되었으며, 컨트롤러에 애플 로그인/회원가입 엔드포인트가 구현되었습니다. 일부 파일에서는 불필요한 개행이 제거되었습니다.

## Changes

| 파일(들) | 변경 요약 |
|---|---|
| .../calendar/dto/PostCalendarDto.java<br>.../token/domain/RefreshTokenRequest.java | 파일 끝의 불필요한 개행 제거 (기능 변화 없음) |
| .../global/exception/BaseException.java | 들여쓰기 및 포맷 정리 (기능 변화 없음) |
| .../global/exception/ErrorCode.java | 애플 인증 관련 6개 에러코드 추가 (필수 약관 미동의 포함) |
| .../global/security/AppleJwtGenerator.java | 애플용 JWT client secret 생성 컴포넌트 신규 추가 |
| .../global/security/ApplePublicKeyProvider.java | 애플 공개키 조회 및 캐싱 컴포넌트 신규 추가 |
| .../global/security/JwtValidator.java | 애플 ID 토큰(JWT) 검증 컴포넌트 신규 추가 |
| .../global/security/token/controller/AuthController.java | 애플 로그인/회원가입용 POST 엔드포인트 2개 추가, AppleOAuthService 의존성 주입 |
| .../global/security/token/domain/AppleLoginRequest.java | 애플 로그인 요청용 record(DTO) 추가 |
| .../global/security/token/domain/AppleProperties.java | 애플 OAuth 관련 프로퍼티 바인딩용 컴포넌트 추가 |
| .../global/security/token/domain/AppleSignUpRequest.java | 애플 회원가입 요청용 record(DTO) 및 약관 동의 내포 클래스 추가 |
| .../global/security/token/domain/AppleUserResponse.java | 애플 사용자 정보 응답 record(DTO) 추가 |
| .../global/security/token/service/AppleOAuthService.java | 애플 OAuth 로그인/회원가입 서비스 신규 추가 (Mono 기반 비동기 처리) |
| .../global/security/token/service/AppleTokenService.java | 애플 OAuth 토큰 교환 및 사용자 추출 서비스 신규 추가 (Mono 기반) |
| .../global/config/WebClientConfig.java | WebClient 빈 등록을 위한 설정 클래스 신규 추가 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant AuthController
    participant AppleOAuthService
    participant AppleTokenService
    participant AppleJwtGenerator
    participant JwtValidator
    participant UserRepository
    participant JwtTokenProvider

    Client->>AuthController: POST /auth/apple (AppleLoginRequest)
    AuthController->>AppleOAuthService: loginWithApple(code)
    AppleOAuthService->>AppleTokenService: exchangeCodeForTokens(code)
    AppleTokenService->>AppleJwtGenerator: createClientSecret()
    AppleTokenService->>Apple: POST /auth/token (애플 서버)
    AppleTokenService->>JwtValidator: validate(id_token)
    AppleTokenService->>AppleOAuthService: AppleUserResponse 반환
    AppleOAuthService->>UserRepository: findByEmailAndDomain
    alt 유저 있음
        AppleOAuthService->>JwtTokenProvider: generateTokenBundle(user)
        AppleOAuthService->>AuthController: TokenResponse 반환
        AuthController->>Client: 200 OK + TokenResponse
    else 유저 없음
        AppleOAuthService->>AuthController: APPLE_USER_NOT_FOUND 예외
        AuthController->>Client: 에러 응답
    end
sequenceDiagram
    participant Client
    participant AuthController
    participant AppleOAuthService
    participant UserRepository
    participant JwtTokenProvider

    Client->>AuthController: POST /auth/apple/signup (AppleSignUpRequest)
    AuthController->>AppleOAuthService: signUpApple(request)
    AppleOAuthService->>UserRepository: findByEmailAndDomainAndName
    alt 이미 존재
        AppleOAuthService->>AuthController: DUPLICATE_USER 예외
        AuthController->>Client: 에러 응답
    else 신규 회원
        AppleOAuthService->>UserRepository: save(new User)
        AppleOAuthService->>JwtTokenProvider: generateTokenBundle(newUser)
        AppleOAuthService->>AuthController: TokenResponse 반환
        AuthController->>Client: 200 OK + TokenResponse
    end
Loading

Suggested reviewers

  • bricksky

Poem

🥕
애플 로그인, 토큰 춤,
토끼가 깡총깡총 뛰어넘네.
새 키와 예외, 새 친구들,
회원가입도 문제없지!
인증의 숲에 봄바람 불어,
깡총! 새로운 시작이야.
🍏🐇


<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

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


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5849b722a767aca7897d4347170e81d0aae9030d and 9f3931f3768f7ca6db2fb187f4b69de134dd0f93.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/main/java/com/usememo/jugger/global/exception/ErrorCode.java` (1 hunks)
* `src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java` (5 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java
* src/main/java/com/usememo/jugger/global/exception/ErrorCode.java

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Team-useMemo/Jugger-Backend&utm_content=144):

- 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](https://www.coderabbit.ai/contact-us/support) 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](https://docs.coderabbit.ai/finishing-touches/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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

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: 11

🧹 Nitpick comments (10)
src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java (1)

18-18: 파일 끝 개행(EOF newline) 유지 권장

마지막 } 뒤 개행이 사라졌습니다. POSIX 텍스트 파일 규약에 따르면 파일은 개행 문자로 끝나는 것이 바람직하며, 일부 Git diff·lint 도구가 경고를 표시할 수 있습니다. 무시해도 동작에는 영향이 없지만, 일관성을 위해 개행을 복구하는 편이 좋습니다.

@@
 	private String description;
-}
+}
+
src/main/java/com/usememo/jugger/global/exception/ErrorCode.java (1)

50-51: 불필요한 빈 줄을 제거하는 것을 권장합니다.

코드 가독성을 위해 불필요한 연속된 빈 줄을 제거하는 것이 좋겠습니다.

 	APPLE_TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 434, "Apple id_token 파싱에 실패했습니다."),
-
-
+
 	JWT_KEY_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 422, "JWT 키 생성에 실패했습니다."),
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (1)

7-65: Lombok을 활용한 보일러플레이트 코드 제거

getter/setter 메서드가 많아 코드가 장황합니다. Lombok을 사용하여 간결하게 만들 수 있습니다.

+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
 @Component
 @ConfigurationProperties(prefix = "apple")
 public class AppleProperties {
 
     private String clientId;
     private String teamId;
     private String keyId;
     private String privateKeyLocation;
     private String privateKey;
     private String redirectUri;
-
-    public String getClientId() {
-        return clientId;
-    }
-
-    public void setClientId(String clientId) {
-        this.clientId = clientId;
-    }
-    // ... 나머지 getter/setter 메서드 제거
 }
src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (3)

29-33: 불필요한 @Autowired 어노테이션 제거

Spring 4.3 이후 생성자가 하나인 경우 @Autowired는 필요하지 않습니다.

-    @Autowired
     public AppleJwtGenerator(AppleProperties appleProperties, ResourceLoader resourceLoader) {

57-73: 프라이빗 키 캐싱으로 성능 개선

매번 파일에서 프라이빗 키를 읽는 것은 비효율적입니다. 키를 캐싱하여 성능을 개선할 수 있습니다.

 public class AppleJwtGenerator {
     private final AppleProperties appleProperties;
     private final ResourceLoader resourceLoader;
     private static final String APPLE_AUDIENCE = "https://appleid.apple.com";
+    private volatile PrivateKey cachedPrivateKey;
 
     // ... 기존 코드 ...
 
     private PrivateKey getPrivateKey()
             throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+        if (cachedPrivateKey != null) {
+            return cachedPrivateKey;
+        }
+        
+        synchronized (this) {
+            if (cachedPrivateKey != null) {
+                return cachedPrivateKey;
+            }
+            
             Resource resource = resourceLoader.getResource(appleProperties.getPrivateKeyLocation());
             try (BufferedReader reader =
                          new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
                 String keyContent = reader.lines().collect(Collectors.joining("\n"));
+                
+                // PEM 형식 검증
+                if (!keyContent.contains("-----BEGIN PRIVATE KEY-----") || 
+                    !keyContent.contains("-----END PRIVATE KEY-----")) {
+                    throw new IllegalArgumentException("유효하지 않은 PEM 형식의 프라이빗 키입니다");
+                }
+                
                 String key =
                         keyContent
                                 .replace("-----BEGIN PRIVATE KEY-----", "")
                                 .replace("-----END PRIVATE KEY-----", "")
                                 .replaceAll("\\s+", "");
                 byte[] encoded = Base64.getDecoder().decode(key);
                 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
                 KeyFactory keyFactory = KeyFactory.getInstance("EC");
-                return keyFactory.generatePrivate(keySpec);
+                cachedPrivateKey = keyFactory.generatePrivate(keySpec);
+                return cachedPrivateKey;
             }
+        }
     }
 }

41-41: 토큰 유효 시간을 설정 가능하도록 개선

현재 1시간으로 하드코딩되어 있는 토큰 유효 시간을 설정 가능하도록 만드는 것이 좋습니다.

+    @Value("${apple.client-secret-expiration-millis:3600000}")
+    private long clientSecretExpirationMillis;
+    
     public String createClientSecret()
             throws java.io.IOException,
             NoSuchAlgorithmException,
             InvalidKeySpecException,
             InvalidKeyException {
         Date now = new Date();
-        Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간
+        Date expiration = new Date(now.getTime() + clientSecretExpirationMillis);
src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java (1)

4-4: 불필요한 import 제거

record에서는 lombok.Data가 필요하지 않습니다.

-import lombok.Data;
src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java (1)

52-53: 불필요한 빈 줄 제거

-
-
src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java (2)

12-12: 사용하지 않는 import 제거

-import lombok.Value;

15-16: 사용하지 않는 import 제거

-import org.springframework.ui.Model;
-import org.springframework.web.reactive.function.client.WebClient;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e342077 and fcf661f.

📒 Files selected for processing (14)
  • src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/exception/BaseException.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/JwtValidator.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java (4 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java (1 hunks)
  • src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/com/usememo/jugger/global/security/JwtValidator.java (2)
src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (1)
  • Component (23-74)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (1)
  • Component (7-65)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (2)
src/main/java/com/usememo/jugger/global/security/JwtValidator.java (1)
  • Component (15-66)
src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (1)
  • Component (23-74)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java (1)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java (1)
  • Schema (15-26)
🔇 Additional comments (7)
src/main/java/com/usememo/jugger/global/exception/ErrorCode.java (1)

45-50: 애플 OAuth 에러 코드 추가가 적절히 구현되었습니다.

새로 추가된 애플 관련 에러 코드들이 기존 패턴과 일관성 있게 구현되었으며, HTTP 상태 코드도 적절히 선택되었습니다. 클라이언트 오류는 BAD_REQUEST, 서버 오류는 INTERNAL_SERVER_ERROR로 분류한 것이 올바릅니다.

src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java (1)

4-4: 포맷팅 정리가 잘 되었습니다.

불필요한 개행 제거로 코드가 더 깔끔해졌습니다.

src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java (1)

1-4: 애플 사용자 응답 레코드가 적절히 구현되었습니다.

sub(subject identifier)와 email 필드를 가진 불변 레코드 클래스로 애플 OAuth 응답 데이터를 처리하기에 적합합니다. 레코드 사용으로 간결하고 안전한 데이터 전달 객체가 되었습니다.

src/main/java/com/usememo/jugger/global/exception/BaseException.java (1)

16-20: 예외 체이닝을 지원하는 생성자 추가가 우수합니다.

Throwable cause를 받는 새로운 생성자를 추가하여 원인 예외를 보존할 수 있게 되었습니다. 이는 디버깅과 로깅에 매우 유용하며, 예외 처리의 모범 사례를 잘 따르고 있습니다. 한국어 주석도 코드 이해에 도움이 됩니다.

src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java (1)

1-7: 애플 로그인 요청 레코드가 잘 구현되었습니다.

Swagger 어노테이션을 포함한 레코드 클래스로 API 문서화가 잘 되어 있습니다. 한국어 설명과 예시 값 제공으로 API 사용자가 이해하기 쉽게 작성되었습니다. 레코드 사용으로 불변성도 보장됩니다.

src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java (1)

105-117: Apple OAuth 엔드포인트 구현 확인

Apple 로그인 및 회원가입 엔드포인트가 다른 OAuth 제공자들과 일관된 패턴으로 구현되었습니다.

src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java (1)

46-47: 중복 사용자 확인 로직 검토

이메일, 도메인, 이름으로 모두 확인하는 로직이 너무 제한적일 수 있습니다. 일반적으로 동일한 도메인(apple)에서는 이메일만으로 중복을 확인하는 것이 일반적입니다.

-        return userRepository.findByEmailAndDomainAndName(email, "apple", name)
+        return userRepository.findByEmailAndDomain(email, "apple")

비즈니스 요구사항을 확인해 주세요. Apple 사용자의 경우 동일한 이메일로 다른 이름을 가진 계정을 허용해야 하는지 검토가 필요합니다.

Comment on lines +21 to +23
@Value("${apple.client-id}")
private String clientId;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

의존성 주입 방식 일관성 개선

@RequiredArgsConstructor를 사용하면서 @Value 어노테이션을 함께 사용하는 것은 일관성이 없습니다. 생성자 주입으로 통일하는 것을 권장합니다.

-    @Value("${apple.client-id}")
-    private String clientId;
+    private final String clientId;
+
+    public JwtValidator(ApplePublicKeyProvider keyProvider, 
+                       @Value("${apple.client-id}") String clientId) {
+        this.keyProvider = keyProvider;
+        this.clientId = clientId;
+    }
📝 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
@Value("${apple.client-id}")
private String clientId;
// replace field-injected clientId with constructor injection
- @Value("${apple.client-id}")
- private String clientId;
+ private final String clientId;
+
+ public JwtValidator(ApplePublicKeyProvider keyProvider,
+ @Value("${apple.client-id}") String clientId) {
+ this.keyProvider = keyProvider;
+ this.clientId = clientId;
+ }
🤖 Prompt for AI Agents
In src/main/java/com/usememo/jugger/global/security/JwtValidator.java around
lines 21 to 23, the clientId field is injected using @Value annotation, which is
inconsistent with the use of @RequiredArgsConstructor for dependency injection.
To fix this, remove the @Value annotation and the field injection, then add
clientId as a final field and include it as a constructor parameter so that it
is injected via constructor injection, maintaining consistency with the rest of
the class.

Comment on lines 24 to 65
public SignedJWT validate(String idToken) {
try {
SignedJWT jwt = SignedJWT.parse(idToken);
JWSHeader header = jwt.getHeader();

// 알고리즘 확인
if (!JWSAlgorithm.RS256.equals(header.getAlgorithm())) {
throw new IllegalArgumentException("Unexpected JWS algorithm: " + header.getAlgorithm());
}

// 공개키로 서명 검증
var jwk = keyProvider.getKeyById(header.getKeyID());
JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());

if (!jwt.verify(verifier)) {
throw new IllegalArgumentException("Apple ID Token 서명 검증 실패");
}

// 클레임 검증
var claims = jwt.getJWTClaimsSet();
Date now = new Date();

if (claims.getExpirationTime() == null || now.after(claims.getExpirationTime())) {
throw new IllegalArgumentException("Apple ID Token 만료됨");
}

if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
throw new IllegalArgumentException("잘못된 iss: " + claims.getIssuer());
}

if (!claims.getAudience().contains(clientId)) {
throw new IllegalArgumentException("잘못된 aud: " + claims.getAudience());
}

return jwt;

} catch (ParseException e) {
throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
} catch (Exception e) {
throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

입력 검증 및 구체적인 예외 처리 필요

현재 모든 예외를 IllegalArgumentException으로 변환하고 있어 오류의 원인을 파악하기 어렵습니다. 또한 입력값 검증이 누락되어 있습니다.

 public SignedJWT validate(String idToken) {
+    if (idToken == null || idToken.trim().isEmpty()) {
+        throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다");
+    }
+    
     try {
         SignedJWT jwt = SignedJWT.parse(idToken);
         // ... 기존 코드 ...
     } catch (ParseException e) {
-        throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
+        throw new BaseException(ErrorCode.APPLE_TOKEN_PARSE_FAILED, e);
     } catch (Exception e) {
-        throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
+        if (e instanceof BaseException) {
+            throw e;
+        }
+        throw new BaseException(ErrorCode.APPLE_TOKEN_VALIDATION_FAILED, e);
     }
 }

🏁 Script executed:

#!/bin/bash
# ErrorCode enum에 Apple 관련 오류 코드가 정의되어 있는지 확인
ast-grep --pattern 'enum ErrorCode {
  $$$
}'

Length of output: 47


Apple 관련 ErrorCode 정의 확인을 위해 추가 정보가 필요합니다.


🏁 Script executed:

#!/bin/bash
# ErrorCode enum 정의 위치와 Apple 관련 코드 확인
rg --max-count=1 "enum\s\+ErrorCode" -A 20
# APPLE_TOKEN_PARSE_FAILED 및 APPLE_TOKEN_VALIDATION_FAILED 상수 존재 여부 검사
rg "APPLE_TOKEN_PARSE_FAILED|APPLE_TOKEN_VALIDATION_FAILED"

Length of output: 106


입력 검증 및 구체적 예외 처리 추가

JwtValidator.validate 메서드에 다음 개선을 적용해주세요:

  • 파일/위치: src/main/java/com/usememo/jugger/global/security/JwtValidator.java (24–65줄)
  • 문제점
    1. idToken에 대한 null‧빈 문자열 검사 누락
    2. 모든 예외를 IllegalArgumentException으로 덮어써서, 실제 실패 원인(파싱 오류 vs 검증 실패 vs JOSE 처리 오류 등)을 파악하기 어려움
  • 제안 리팩터링
    1. 메서드 시작부에 null‧blank 체크 추가
    2. ParseException, JOSE 관련 예외(JOSEException) 등 구체적 예외별로 별도 커스텀 예외를 던지도록 분리
      (예: AppleTokenParseException, AppleTokenValidationException 등)
    3. 불필요한 catch (Exception e) 제거

[diff]

 public SignedJWT validate(String idToken) {
+    if (idToken == null || idToken.isBlank()) {
+        throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다");
+    }

     try {
         SignedJWT jwt = SignedJWT.parse(idToken);
         JWSHeader header = jwt.getHeader();
         // … (기존 알고리즘·서명·클레임 검증 로직)

-    } catch (ParseException e) {
-        throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
-    } catch (Exception e) {
-        throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
+    } catch (ParseException e) {
+        throw new AppleTokenParseException("Apple ID Token 파싱 실패", e);
+    } catch (JOSEException e) {
+        throw new AppleTokenValidationException("Apple ID Token 서명/클레임 검증 실패", e);
     }
 }
  • 추가 작업
    • 위에서 사용한 커스텀 예외 클래스(AppleTokenParseException, AppleTokenValidationException)를 정의하고, 필요한 경우 전역 예외 처리기(GlobalExceptionHandler)에서 HTTP 응답코드 매핑도 함께 설정해주세요.
📝 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 SignedJWT validate(String idToken) {
try {
SignedJWT jwt = SignedJWT.parse(idToken);
JWSHeader header = jwt.getHeader();
// 알고리즘 확인
if (!JWSAlgorithm.RS256.equals(header.getAlgorithm())) {
throw new IllegalArgumentException("Unexpected JWS algorithm: " + header.getAlgorithm());
}
// 공개키로 서명 검증
var jwk = keyProvider.getKeyById(header.getKeyID());
JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());
if (!jwt.verify(verifier)) {
throw new IllegalArgumentException("Apple ID Token 서명 검증 실패");
}
// 클레임 검증
var claims = jwt.getJWTClaimsSet();
Date now = new Date();
if (claims.getExpirationTime() == null || now.after(claims.getExpirationTime())) {
throw new IllegalArgumentException("Apple ID Token 만료됨");
}
if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
throw new IllegalArgumentException("잘못된 iss: " + claims.getIssuer());
}
if (!claims.getAudience().contains(clientId)) {
throw new IllegalArgumentException("잘못된 aud: " + claims.getAudience());
}
return jwt;
} catch (ParseException e) {
throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
} catch (Exception e) {
throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
}
}
public SignedJWT validate(String idToken) {
if (idToken == null || idToken.isBlank()) {
throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다");
}
try {
SignedJWT jwt = SignedJWT.parse(idToken);
JWSHeader header = jwt.getHeader();
// 알고리즘 확인
if (!JWSAlgorithm.RS256.equals(header.getAlgorithm())) {
throw new IllegalArgumentException("Unexpected JWS algorithm: " + header.getAlgorithm());
}
// 공개키로 서명 검증
var jwk = keyProvider.getKeyById(header.getKeyID());
JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());
if (!jwt.verify(verifier)) {
throw new IllegalArgumentException("Apple ID Token 서명 검증 실패");
}
// 클레임 검증
var claims = jwt.getJWTClaimsSet();
Date now = new Date();
if (claims.getExpirationTime() == null || now.after(claims.getExpirationTime())) {
throw new IllegalArgumentException("Apple ID Token 만료됨");
}
if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
throw new IllegalArgumentException("잘못된 iss: " + claims.getIssuer());
}
if (!claims.getAudience().contains(clientId)) {
throw new IllegalArgumentException("잘못된 aud: " + claims.getAudience());
}
return jwt;
} catch (ParseException e) {
throw new AppleTokenParseException("Apple ID Token 파싱 실패", e);
} catch (JOSEException e) {
throw new AppleTokenValidationException("Apple ID Token 서명/클레임 검증 실패", e);
}
}
🤖 Prompt for AI Agents
In src/main/java/com/usememo/jugger/global/security/JwtValidator.java lines 24
to 65, add null and blank string checks for the idToken parameter at the start
of the validate method to prevent invalid input. Replace the current generic
IllegalArgumentException handling by defining and throwing specific custom
exceptions such as AppleTokenParseException for parsing errors and
AppleTokenValidationException for validation failures, catching ParseException
and JOSEException separately. Remove the broad catch (Exception e) block to
avoid masking distinct error causes. Also, create these custom exception classes
and update the global exception handler to map them to appropriate HTTP response
codes.

Comment on lines 14 to 15
private String privateKeyLocation;
private String privateKey;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

privateKey와 privateKeyLocation 필드의 용도 명확화 필요

두 개의 비슷한 필드가 있어 혼란을 줄 수 있습니다. 각각의 용도를 문서화하거나 하나로 통합하는 것을 고려해주세요.


🏁 Script executed:

#!/bin/bash
# privateKey와 privateKeyLocation 필드가 실제로 어떻게 사용되는지 확인
rg -A 5 "getPrivateKey\(\)|getPrivateKeyLocation\(\)" --type java

Length of output: 2881


🏁 Script executed:

#!/bin/bash
# appleProperties.getPrivateKey() 호출 위치 확인
rg "appleProperties\\.getPrivateKey" --type java -n

Length of output: 224


privateKey와 privateKeyLocation 용도 명확화 및 정리 필요

현재 privateKeyLocation은 파일 경로로부터 키를 읽어오는 데만 사용되고, privateKey 필드는 코드에서 전혀 참조되지 않습니다. 두 필드가 각각 어떤 상황에서, 어떤 형식(예: 파일 경로 vs. 인라인 Base64)으로 사용되어야 하는지 문서화하거나, 실제 사용하지 않는 필드를 제거/통합해주세요.

조치 항목:

  • src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
    privateKey/privateKeyLocation 각 필드에 대한 JavaDoc 또는 주석 추가
  • 프로젝트 설정 문서(application.yml 예시, README 등)에 두 속성 사용 방법 및 우선순위(예: privateKey 설정 시 privateKeyLocation 무시 여부) 기재
  • 만약 인라인 키 제공 기능이 필요 없다면, 사용되지 않는 privateKey 필드를 제거하거나, 반대로 인라인 키 사용 로직을 AppleJwtGenerator에 구현
🤖 Prompt for AI Agents
In
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
around lines 14 to 15, clarify the purpose and usage of the privateKey and
privateKeyLocation fields by adding JavaDoc or comments explaining when and how
each should be used (e.g., file path vs. inline Base64 key). Update project
documentation such as application.yml examples or README to describe these
properties and their precedence (e.g., whether privateKey overrides
privateKeyLocation). If inline key usage is not needed, remove the unused
privateKey field; otherwise, implement the inline key handling logic in
AppleJwtGenerator accordingly.

String email,

@Schema(description = "약관 동의 정보", required = true)
KakaoSignUpRequest.Terms terms
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

잘못된 클래스 참조 수정 필요

Apple 회원가입 요청에서 KakaoSignUpRequest.Terms를 참조하고 있습니다. 자체 정의된 Terms 클래스를 사용해야 합니다.

-                                 KakaoSignUpRequest.Terms terms
+                                 Terms terms
🤖 Prompt for AI Agents
In
src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java
at line 13, the code incorrectly references KakaoSignUpRequest.Terms. Replace
this with the locally defined Terms class specific to AppleSignUpRequest to
ensure correct class usage and avoid cross-class dependency.

Copy link
Member

@minhyuk2 minhyuk2 left a comment

Choose a reason for hiding this comment

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

고생했어용 레빗이가 말하는 부분들만 조금 수정하면 될 듯요

this.message = errorCode.getMessage();
}

public BaseException(ErrorCode errorCode, Throwable cause) {
Copy link
Member

Choose a reason for hiding this comment

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

이건 어떤 식으로 쓰는건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AppleTokenService에서 APPLE_CLIENT_SECRET_FAILED 및APPLE_TOKEN_PARSE_ERROR 예외를 던질 때, 예외 로그를 남기기 위한 목적으로 cause(Throwable)를 포함했었습니다.

다만, 현재 프로젝트의 다른 예외 처리 흐름에서는 cause를 포함하지 않는 방식으로 통일되어 있어, 일관성을 위해 해당 부분도 제거해두었습니다.

추후 공통 로깅 처리 구조가 생기면 다시 포함하는 방향도 고려할 수 있을 것 같습니다.

Resource resource = resourceLoader.getResource(appleProperties.getPrivateKeyLocation());
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
String keyContent = reader.lines().collect(Collectors.joining("\n"));
Copy link
Member

Choose a reason for hiding this comment

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

아마 이 부분때문에 애플로그인이 어렵다는 인식이 생긴 것 같네용 신기합니다~

Copy link
Contributor

@JjungminLee JjungminLee left a comment

Choose a reason for hiding this comment

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

확인했습니다! 수고하셨어요~

PR이름만 기존에 작성한대로 바꿔주세요~

}

public BaseException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause); // RuntimeException에 메시지 + 원인 등록
Copy link
Contributor

Choose a reason for hiding this comment

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

주석은 지워주세요~

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 알겠습니다!

InvalidKeyException {
Date now = new Date();
Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간

Copy link
Contributor

Choose a reason for hiding this comment

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

상동입니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 했습니다~

throw new IllegalArgumentException("Apple ID Token 만료됨");
}

if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

url 상수로 작성하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

고정된 값이라 코드 내 상수로 바꾸는게 적합할 듯 합니다.
APPLE_ISSUER로 설정해두었습니다:)

private String privateKey;
private String redirectUri;

public String getClientId() {
Copy link
Contributor

Choose a reason for hiding this comment

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

get,set 함수를 많이 쓰셨는데 @Getter, @Setter로 코드를 줄여보는건 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

말씀해주신 대로 단순히 필드 접근용으로 사용하는 getter와 setter는 Lombok의 @Getter, @Setter 어노테이션을 활용하면 코드량을 줄이고 가독성을 높이는 데 도움이 된다고 생각합니다.

해당 클래스에서는 별도의 로직이 없는 순수 데이터 필드들이라, 명시적으로 메서드를 작성하기보다 Lombok 어노테이션으로 대체하는 방식이 더 적절할 것 같아, 제안 주신 방향대로 리팩토링을 진행하겠습니다. 좋은 리뷰 감사드립니다!

@JjungminLee JjungminLee self-requested a review June 30, 2025 23:35
Copy link
Contributor

@JjungminLee JjungminLee left a comment

Choose a reason for hiding this comment

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

  1. 하기 링크에 application.yml 바뀐사항들 산입 부탁드립니다.
    https://www.notion.so/jadestone/application-yml-1c2b8fffcd6a80b98e0dc4ef38677f9a?source=copy_link

  2. ci/cd 파이프라인에도 환경변수 값들 산입 부탁드립니다.

@bricksky bricksky self-assigned this Jul 2, 2025
@bricksky bricksky changed the title Feat/#132 apple feat : apple 로그인 기능 구현 #132 Jul 2, 2025
@Team-useMemo Team-useMemo deleted a comment from coderabbitai bot Jul 2, 2025
@bricksky bricksky changed the title feat : apple 로그인 기능 구현 #132 feat : apple 로그인 기능 구현 Jul 2, 2025
@bricksky bricksky added the FEATURE 기능구현 label Jul 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FEATURE 기능구현
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants