Skip to content

refactor : 미션 관련 기능 최적화#78

Merged
Juhye0k merged 6 commits intodevfrom
sql
Sep 22, 2025
Merged

refactor : 미션 관련 기능 최적화#78
Juhye0k merged 6 commits intodevfrom
sql

Conversation

@Juhye0k
Copy link
Copy Markdown
Contributor

@Juhye0k Juhye0k commented Sep 22, 2025

What is this PR?🔍

  • 미션 관련 기능 최적화

Changes💻

  • 불필요한 로그 제거로 코드 가독성 향상
  • 미션 조회 쿼리 분리를 통한 성능 최적화 (N+1 문제 해결)
  • 리뷰 무한스크롤 API 메서드명 개선 (getMissionReviewsScrollgetMissionReviewsWithCursor)
  • Mission 엔티티에 팁 필드 추가 및 응답 DTO 업데이트
  • OAuth2 인증 관련 디버그 로그 정리
  • 미션 검증 서비스 주석 및 구조 개선

Summary by CodeRabbit

  • New Features

    • 미션 응답에 팁(tip) 정보가 추가되어 장소 이용 힌트를 확인할 수 있습니다.
    • 미션 리뷰 커서 기반 조회에 정렬 방향(sortType) 선택이 가능합니다.
  • Refactor

    • 미션 목록/리뷰 페이징 로직을 단순화하여 nextId/hasNext 응답의 일관성을 개선했습니다.
    • 미션 상태 조회와 완료 여부 계산 방식을 개선해 조회 효율을 높였습니다.
    • 리뷰 조회 API의 명칭과 파라미터 구성이 변경되었습니다(정렬 옵션 추가). API 사용 시 최신 스펙을 확인해 주세요.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

여러 클래스에서 로깅을 제거하고 불필요한 임포트를 정리했다. 미션 리뷰 스크롤 API는 메서드명 변경과 정렬 방향 파라미터(sortType) 추가로 시그니처가 바뀌었다. 미션 엔티티에 tip 필드를 추가하고 DTO에 반영했다. 리포지토리 쿼리를 단순화하고 서비스 로직을 이에 맞게 조정했다.

Changes

Cohort / File(s) Summary
Auth/OAuth 로깅 정리
src/main/java/avengers/lion/auth/controller/AuthController.java, src/main/java/avengers/lion/global/OAuth2SuccessHandler.java
인증/인가 흐름의 다수 로그 라인 제거 및 미사용 임포트 제거. 공개 시그니처 변화 없음.
미션 리뷰 커서 API 변경
src/main/java/avengers/lion/mission/api/MissionApi.java, src/main/java/avengers/lion/mission/controller/MissionController.java
메서드명 getMissionReviewsScrollgetMissionReviewsWithCursor, sortType 파라미터 추가(기본 DESC). 엔드포인트 동작의 정렬 지정 가능.
미션 도메인/DTO 확장
src/main/java/avengers/lion/mission/domain/Mission.java, src/main/java/avengers/lion/mission/dto/response/MissionResponse.java
엔티티에 tip 컬럼 추가, MissionResponse 레코드에 tip 필드 추가 및 팩토리 메서드에서 매핑.
리포지토리 메서드 개편
src/main/java/avengers/lion/mission/repository/MissionRepository.java
상태별 조회 메서드 단순화: findAllByStatus(...)로 변경. 회원 완료 미션 ID 조회 findCompletedMissionIdsByMember(...) 추가.
미션 서비스 로직 조정
src/main/java/avengers/lion/mission/service/MissionService.java
ACTIVE 미션 조회와 회원 완료 미션 ID 세트 조합으로 완료 여부 설정. 리뷰 프리뷰/페이지네이션에서 nextId 계산 단순화 및 hasNext 계산 추가(크기 기준).
검증/콜백/이벤트 서비스 로깅 축소
src/main/java/avengers/lion/mission/service/CallbackService.java, .../VerificationCleanupService.java, .../VerificationEventService.java, .../MissionVerifyService.java
다수 로깅 제거, 공개 시그니처 변화 없음. MissionVerifyService에서 MissionRepository 필드 제거(생성자 영향). 기능 흐름은 동일.
쿼리 구현 포맷팅
src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java
공백 라인 제거만 수행, 동작 변경 없음.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant Controller as MissionController
    participant Service as MissionService
    participant Repo as MissionRepository
    participant ReviewQ as ReviewQueryRepository

    Client->>Controller: GET /missions/{missionId}/reviews?lastReviewId&limit&sortType
    Controller->>Service: getMissionReviewsWithCursor(missionId, lastReviewId, limit, sortType)
    Service->>ReviewQ: findReviewsWithCursor(missionId, lastReviewId, limit+1, sortType)
    ReviewQ-->>Service: List<Review>
    note right of Service: hasNext = size > limit<br/>nextId = last(review).id (있을 때)
    Service-->>Controller: PageResult(reviews[0..limit-1], nextId, hasNext)
    Controller-->>Client: 200 OK (ResponseBody)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

작은 발로 코드를 톡, 로그를 털어내고
커서는 살짝 왼쪽? 아니면 DESC로 쏙!
미션엔 비밀 팁, 주머니에 쏙 넣고
리뷰의 다음 id, 당근처럼 콕 찍고
깡총! 배포로 달려가요 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "refactor : 미션 관련 기능 최적화"은 변경의 주된 목적(미션 관련 성능·구조 최적화)을 간결하게 요약하고 있어 변경사항과 일치합니다. 제목은 짧고 단일 문장으로 로그 정리, 쿼리 분리(N+1 해결), API명 변경, 엔티티 필드 추가 등 핵심 변경을 포괄하며 히스토리 스캔 시 이해하기 쉽습니다. 필요시 'N+1 해결' 또는 'API명 변경' 같은 구체적 키워드를 추가하면 더 명확해집니다.
Description Check ✅ Passed PR 설명은 템플릿의 "What is this PR?"과 "Changes" 섹션을 충실히 채워 주요 변경사항(불필요 로그 제거, 쿼리 분리·N+1 해결, 리뷰 API명 변경, Mission.tip 필드 추가, OAuth2 로그 정리 등)을 명확히 기술하고 있어 전반적으로 완전합니다. 다만 템플릿의 "ScreenShot📷" 섹션은 비어 있고, 더 중요한 누락으로는 DB 스키마 변경(새로운 tip 컬럼)에 대한 마이그레이션 안내와 공개 API(메서드명 변경)에 따른 클라이언트 영향 또는 호환성 처리가 없습니다. 이 두 항목을 보강하면 리뷰어와 배포 담당자가 영향도를 빠르게 파악할 수 있습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sql

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (1)

50-58: 카카오 이메일 null 케이스 미처리

카카오 동의 범위에 따라 이메일이 제공되지 않을 수 있습니다. 이메일이 null이면 회원 생성/조회 로직이 실패합니다. 대체 키(예: kakao id)로 조회/매핑하거나, 추가 동의 요청/예외 처리 필요합니다.

예시:

-        Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail())
+        String email = kakaoUserInfo.getEmail();
+        if (email == null) {
+            // kakaoUserInfo.getId() 등을 기반으로 매핑하거나 명시적 예외 처리
+            throw new IllegalStateException("카카오 이메일을 가져올 수 없습니다.");
+        }
+        Member member = memberRepository.findByEmail(email)
                 .orElseGet(()->
                         memberRepository.save(
                                 Member.builder()
-                                        .email(kakaoUserInfo.getEmail())
+                                        .email(email)
                                         .nickname(kakaoUserInfo.getNickname())
                                         .profileImageUrl(kakaoUserInfo.getProfileImageUrl())
                                         .role(MemberRole.ROLE_USER)
                                         .build()
                         ));
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (2)

37-51: 예외 삼킴(swallowing)으로 장애 감지 불가 및 디버깅 어려움.

조회 실패(네트워크/직렬화 오류)와 “캐시 미스(null)”가 동일한 null로 반환됩니다. 최소한 예외는 래핑해 상위로 전달하세요.

적용 패치:

     public VerificationMetadata getMetadata(String jobId) {
         try {
             String key = CACHE_PREFIX + jobId;
             Object cached = redisTemplate.opsForValue().get(key);
 
             if (cached instanceof VerificationMetadata metadata) {
                 return metadata;
             }
 
             return null;
 
         } catch (Exception e) {
-            return null;
+            throw new RuntimeException("메타데이터 조회 중 오류가 발생했습니다.", e);
         }
     }

53-59: 빈 catch 블록: 실패 은폐로 정합성 이슈 유발.

삭제 실패를 숨기면 정리(cleanup) 로직이 성공한 것으로 오인될 수 있습니다. 최소한 예외를 전달하세요.

패치:

     public void removeMetadata(String jobId) {
         try {
             String key = CACHE_PREFIX + jobId;
             redisTemplate.delete(key);
         } catch (Exception e) {
-        }
+            throw new RuntimeException("메타데이터 삭제 중 오류가 발생했습니다.", e);
+        }
     }
src/main/java/avengers/lion/mission/domain/Mission.java (2)

65-76: 생성자에서 새로운 tip 파라미터 누락.

Mission 엔티티에 tip 필드가 추가되었지만, 기존 생성자에는 tip 파라미터가 포함되지 않았습니다. 이로 인해 생성자를 통해 Mission 객체를 생성할 때 tip 필드가 항상 null로 설정됩니다.

생성자 시그니처를 업데이트하여 tip 필드를 포함해주세요:

-    public Mission(Long id, String placeName, String address, String description, PlaceCategory placeCategory, MissionStatus status, Double latitude, Double longitude, MissionBatches missionBatches, MissionTemplate sourceTemplate) {
+    public Mission(Long id, String placeName, String address, String description, PlaceCategory placeCategory, MissionStatus status, Double latitude, Double longitude, String tip, MissionBatches missionBatches, MissionTemplate sourceTemplate) {
         this.id = id;
         this.placeName = placeName;
         this.address = address;
         this.description = description;
         this.placeCategory = placeCategory;
         this.status = status;
         this.latitude = latitude;
         this.longitude = longitude;
+        this.tip = tip;
         this.missionBatches = missionBatches;
         this.sourceTemplate = sourceTemplate;
     }

78-92: createFromTemplate 메서드에서 tip 필드 처리 필요.

createFromTemplate 정적 팩토리 메서드에서도 tip 필드에 대한 처리가 누락되었습니다. MissionTemplate에서 tip 정보를 가져오거나 기본값을 설정해야 합니다.

MissionTemplate에서 tip을 가져오도록 수정하거나 적절한 기본값을 설정해주세요:

     public static Mission createFromTemplate(MissionTemplate template, MissionBatches batch) {
         Place place = template.getPlace();
         return new Mission(
             null,
             place.getName(),
             place.getAddress(),
             template.getDescription(),
             place.getCategory(),
             MissionStatus.ACTIVE,
             place.getLatitude(),
             place.getLongitude(),
+            template.getTip(), // 또는 적절한 기본값
             batch,
             template
         );
     }
src/main/java/avengers/lion/mission/service/MissionVerifyService.java (5)

55-58: 전역 에러 정책 우회: RuntimeException 대신 BusinessException 사용

컨트롤러 어드바이스/에러 코드 체계와의 일관성을 위해 도메인형 예외로 래핑하세요.

-            log.error("Failed to generate upload URL for missionId: {}", missionId, e);
-            throw new RuntimeException("업로드 URL 생성 중 오류가 발생했습니다.", e);
+            log.error("Failed to generate upload URL for missionId: {}", missionId, e);
+            throw new BusinessException(ExceptionType.VERIFICATION_UPLOAD_URL_CREATE_FAILED, e);

64-76: imageUrl 신뢰는 SSRF/권한 우회 위험 — 도메인/키 일치 검증 추가 필요

클라이언트가 제공한 URL을 그대로 FastAPI로 전달하면 내부망 접근(SSRF) 또는 타 계정 리소스 검증 우회가 가능합니다. https 스킴, 허용 도메인(Cloudinary), 그리고 uploadKey(publicId) 포함 여부를 서버에서 검증하세요. 가능하면 서버가 uploadKey로 안전한 canonical URL을 구성해 사용하세요.

최소 보강 예(검증 + 정제 URL 사용):

         try {
             //  새로운 jobId 생성
             String jobId = metadataCacheService.generateJobId();
 
             //  메타데이터 캐시 저장
+            final String rawImageUrl = request.imageUrl();
+            final String uploadKey = request.uploadKey();
+            validateImageUrlOrThrow(rawImageUrl, uploadKey); // 스킴/도메인/키 일치 검증
+            final String sanitizedImageUrl = rawImageUrl; // 필요 시 uploadKey 기반 canonical URL로 대체
 
             metadataCacheService.cacheMetadata(
                 jobId, 
-                request.imageUrl(), 
-                request.uploadKey(),  // publicId
+                sanitizedImageUrl, 
+                uploadKey,  // publicId
                 missionId, 
                 memberId
             );
             
             //  FastAPI에 비동기 분석 요청
-            callFastApiAsync(jobId, request.imageUrl());
+            callFastApiAsync(jobId, sanitizedImageUrl);
 
             return new VerifyImageResponse(jobId);

도우미 메서드(파일 하단에 추가):

private void validateImageUrlOrThrow(String imageUrl, String uploadKey) {
    try {
        if (imageUrl == null || uploadKey == null) {
            throw new BusinessException(ExceptionType.INVALID_IMAGE_URL);
        }
        var uri = java.net.URI.create(imageUrl);
        if (!"https".equalsIgnoreCase(uri.getScheme())) {
            throw new BusinessException(ExceptionType.INVALID_IMAGE_URL);
        }
        var host = uri.getHost();
        if (host == null || !(host.endsWith("cloudinary.com") || host.endsWith("res.cloudinary.com"))) {
            throw new BusinessException(ExceptionType.INVALID_IMAGE_URL);
        }
        if (!imageUrl.contains(uploadKey)) {
            throw new BusinessException(ExceptionType.INVALID_IMAGE_URL);
        }
    } catch (IllegalArgumentException e) {
        throw new BusinessException(ExceptionType.INVALID_IMAGE_URL, e);
    }
}

Also applies to: 79-81


83-94: 예외 처리 일관성 + 보상 처리 순서

현재 RuntimeException을 던져 전역 에러 매핑을 우회합니다. 또한 업로드 정리 실패는 삼키되, 최종 예외는 도메인형으로 변환하세요.

-            throw new RuntimeException("인증 시작 중 오류가 발생했습니다.", e);
+            throw new BusinessException(ExceptionType.VERIFICATION_START_FAILED, e);

113-128: WebClient: 상태코드 처리/타임아웃/Content-Type 명시 누락

비정상 상태코드(onStatus), 타임아웃, Content-Type을 명시하세요. 실패 시 cleanup 보장은 유지합니다.

             webClient.post()
                 .uri(fastApiUrl + "/analyze")
+                .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                 .bodyValue(requestBody)
-                .retrieve()
-                .bodyToMono(String.class)
+                .retrieve()
+                .onStatus(org.springframework.http.HttpStatusCode::isError, resp ->
+                    resp.bodyToMono(String.class).defaultIfEmpty("")
+                        .map(body -> new RuntimeException("FastAPI non-2xx: " + resp.statusCode() + " body=" + body))
+                )
+                .bodyToMono(Void.class)
+                .timeout(java.time.Duration.ofSeconds(10))
                 .subscribe(
-                    result -> log.info("FastAPI call succeeded for jobId: {}", jobId),
+                    unused -> log.info("FastAPI call succeeded for jobId: {}", jobId),
                     error -> {
                         log.error("FastAPI call failed for jobId: {}", jobId, error);
                         try {
                             cleanupService.cleanupFailedVerification(jobId);
                         } catch (Exception e) {
                             log.error("Failed to cleanup failed verification: {}", jobId, e);
                         }
                     }
             );

64-65: 미션 존재·상태·권한 검증 추가 필요

검증 누락으로 비활성 또는 권한 없는 미션에 대해 인증 절차를 진행하거나 DB에 저장될 수 있습니다.

  • src/main/java/avengers/lion/mission/service/MissionVerifyService.java — generateUploadUrl(Long) (라인 ~42) 및 startVerification(...) (라인 ~64): 미션 존재 및 상태(MissionStatus.ACTIVE)와 멤버 권한 검증 추가(또는 missionService로 위임).
  • src/main/java/avengers/lion/mission/controller/VerificationController.java — getUploadUrl / startVerification 엔드포인트(라인 47 / 59): 컨트롤러에서 선검증으로 처리할 경우 missionService 호출로 검증 보장.
  • src/main/java/avengers/lion/mission/service/CallbackService.java — saveCompletedMission(...) (라인 156): 현재는 미션 존재만 확인하므로 미션 상태(활성 여부)도 확인해 비활성 미션에 대한 CompletedMission 저장을 차단.
🧹 Nitpick comments (13)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (6)

73-75: 프론트엔드 콜백 URL 하드코딩 제거 필요

환경별로 달라지는 값을 소스에 고정하면 배포/보안 리스크가 큽니다. 설정값(예: application.yml)으로 주입하고, 허용된 도메인 화이트리스트를 강제하세요. 또한 프로덕션은 HTTPS를 기본으로.

적용 예시:

-        String frontendUrl = "http://localhost:5174/auth/callback?code=" + authCode;
-        getRedirectStrategy().sendRedirect(request, response, frontendUrl);
+        String baseRedirect = appProps.getFrontendCallbackUrl(); // ex) https://app.example.com/auth/callback
+        String frontendUrl = UriComponentsBuilder.fromHttpUrl(baseRedirect)
+                .queryParam("code", authCode)
+                .build(true)
+                .toUriString();
+        getRedirectStrategy().sendRedirect(request, response, frontendUrl);

구성 추가:

+@ConfigurationProperties("app.auth")
+public class AppAuthProps {
+  private String frontendCallbackUrl;
+  // getter/setter
+}

11-11: @transactional 어노테이션 패키지 교체 권장

현재 jakarta.transaction.Transactional 사용. Spring의 선언적 트랜잭션 기능(롤백 규칙 등)을 온전히 활용하려면 org.springframework.transaction.annotation.Transactional로 교체하세요.

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;

Also applies to: 39-39


51-61: 동시 가입 경쟁 조건 가능성(이메일 unique 충돌)

동시에 같은 이메일로 최초 로그인 시 findByEmail → save 사이 레이스가 발생할 수 있습니다. 이메일에 유니크 인덱스를 보장하고, DataIntegrityViolationException 캐치 후 재조회 리트라이를 권장합니다.

예시 패턴:

try {
  return memberRepository.findByEmail(email)
      .orElseGet(() -> memberRepository.save(...));
} catch (DataIntegrityViolationException e) {
  return memberRepository.findByEmail(email).orElseThrow(e);
}

75-77: 인증 속성 정리 누락

리다이렉트 후 세션 속성 정리를 보장하려면 clearAuthenticationAttributes 호출을 권장합니다.

-        getRedirectStrategy().sendRedirect(request, response, frontendUrl);
+        clearAuthenticationAttributes(request);
+        getRedirectStrategy().sendRedirect(request, response, frontendUrl);

18-19: 불필요한 로깅 애노테이션 제거

로그를 모두 제거했는데 @slf4j는 남아 있습니다. 사용하지 않으면 제거해 주세요.

-import lombok.extern.slf4j.Slf4j;
...
-@Slf4j

Also applies to: 26-26


13-13: RedisTemplate 타입 단순화 제안

문자열 키/값만 다루므로 StringRedisTemplate 사용이 안전하고 직관적입니다. 직렬화 설정 이슈도 줄어듭니다.

-import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
...
-    private final RedisTemplate<String, String> redisTemplate;
+    private final StringRedisTemplate redisTemplate;

Also applies to: 37-37, 70-70

src/main/java/avengers/lion/mission/service/MetadataCacheService.java (3)

18-20: TTL 하드코딩 제거: 환경별 튜닝 가능하도록 외부화.

프로퍼티로 노출해 운영/스테이징에서 유연하게 조정하세요.

예시(선호안):

  • application.yml: verification.metadata.ttl-minutes: 30
  • 코드 변경:
-    private static final Duration CACHE_TTL = Duration.ofMinutes(30);
+    @org.springframework.beans.factory.annotation.Value("${verification.metadata.ttl-minutes:30}")
+    private long cacheTtlMinutes;

그리고 set 호출부에서:

-            redisTemplate.opsForValue().set(key, metadata, CACHE_TTL);
+            redisTemplate.opsForValue().set(key, metadata, java.time.Duration.ofMinutes(cacheTtlMinutes));

25-35: 입력 값 방어로직 누락: key/필드 null 허용 시 런타임 오류 가능.

jobId, publicId, missionId, memberId에 대한 널/공백 검증 추가 권장.

간단 예시:

     public void cacheMetadata(String jobId, String imageUrl, String publicId, Long missionId, Long memberId) {
         try {
+            if (jobId == null || jobId.isBlank()) throw new IllegalArgumentException("jobId is required");
+            if (publicId == null || publicId.isBlank()) throw new IllegalArgumentException("publicId is required");
+            if (missionId == null || memberId == null) throw new IllegalArgumentException("missionId/memberId are required");
             VerificationMetadata metadata = new VerificationMetadata(imageUrl, publicId, missionId, memberId);
             String key = CACHE_PREFIX + jobId;
             
-            redisTemplate.opsForValue().set(key, metadata, CACHE_TTL);
+            redisTemplate.opsForValue().set(key, metadata, java.time.Duration.ofMinutes(cacheTtlMinutes));

37-47: Optional 반환 고려 — 호출부 2곳 동시 수정 필요

getMetadata를 Optional로 바꾸면 의도가 명확해지나 공개 API 변경이므로 아래 호출부들을 함께 업데이트해야 함.

  • 정의: src/main/java/avengers/lion/mission/service/MetadataCacheService.java — getMetadata (line 37).
  • 호출: src/main/java/avengers/lion/mission/service/VerificationCleanupService.java — lines 19–20 (대입 후 null 검사).
  • 호출: src/main/java/avengers/lion/mission/service/CallbackService.java — lines 123–124 (대입 후 null 검사).
src/main/java/avengers/lion/mission/service/MissionVerifyService.java (4)

49-53: Cloudinary 업로드 preset 하드코딩 제거 권장

보안/운영 편의상 preset은 설정으로 주입하는 것이 안전합니다.

적용 예(해당 범위 내 수정):

-            return new UploadUrlResponse(
-                uploadInfo.uploadUrl(),
-                uploadInfo.publicId(),
-                "LetsGGu"// Unsigned upload preset (노출 안전)
-            );
+            return new UploadUrlResponse(
+                uploadInfo.uploadUrl(),
+                uploadInfo.publicId(),
+                unsignedPreset // 설정 주입
+            );

추가 필드(파일 상단 필드 구역에 배치):

@Value("${cloudinary.upload.unsigned-preset}")
private String unsignedPreset;

102-102: 중복 비동기: @async와 Reactor subscribe를 동시에 사용

이미 Reactor에서 비동기로 동작하므로 @async는 제거하거나, 반대로 subscribe를 제거하고 Mono를 반환해 호출부에서 구독 중 하나만 사용하세요. 현재 형태는 디버깅/스레드 관리가 어려워집니다.

최소 변경(주석만 제거):

-    @Async
     public void callFastApiAsync(String jobId, String imageUrl) {

107-111: 콜백 URL 문자열 결합 대신 안전한 빌더 사용

기본 URL의 슬래시 유무에 따라 // 발생 가능. UriComponentsBuilder로 안전하게 조립하세요.

-            Map<String, Object> requestBody = Map.of(
-                "job_id", jobId,
-                "image_url", imageUrl,
-                "callback_url", publicBaseUrl + "/api/v1/missions/" + jobId + "/callback"
-            );
+            String callbackUrl = org.springframework.web.util.UriComponentsBuilder
+                .fromHttpUrl(publicBaseUrl)
+                .path("/api/v1/missions/{jobId}/callback")
+                .buildAndExpand(jobId)
+                .toUriString();
+            Map<String, Object> requestBody = Map.of(
+                "job_id", jobId,
+                "image_url", imageUrl,
+                "callback_url", callbackUrl
+            );

99-101: 설명 주석 추가는 좋습니다만, 메서드 역할 명확화 한 줄 보완 제안

“FastAPI 비동기 분석 요청을 송신하며 실패 시 정리(cleanup)까지 담당” 정도를 한 줄에 담으면 더 명확합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7051987 and 72303f3.

📒 Files selected for processing (14)
  • src/main/java/avengers/lion/auth/controller/AuthController.java (0 hunks)
  • src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (1 hunks)
  • src/main/java/avengers/lion/mission/api/MissionApi.java (1 hunks)
  • src/main/java/avengers/lion/mission/controller/MissionController.java (1 hunks)
  • src/main/java/avengers/lion/mission/domain/Mission.java (1 hunks)
  • src/main/java/avengers/lion/mission/dto/response/MissionResponse.java (2 hunks)
  • src/main/java/avengers/lion/mission/repository/MissionRepository.java (1 hunks)
  • src/main/java/avengers/lion/mission/service/CallbackService.java (0 hunks)
  • src/main/java/avengers/lion/mission/service/MetadataCacheService.java (1 hunks)
  • src/main/java/avengers/lion/mission/service/MissionService.java (5 hunks)
  • src/main/java/avengers/lion/mission/service/MissionVerifyService.java (2 hunks)
  • src/main/java/avengers/lion/mission/service/VerificationCleanupService.java (0 hunks)
  • src/main/java/avengers/lion/mission/service/VerificationEventService.java (0 hunks)
  • src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java (0 hunks)
💤 Files with no reviewable changes (5)
  • src/main/java/avengers/lion/mission/service/CallbackService.java
  • src/main/java/avengers/lion/auth/controller/AuthController.java
  • src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java
  • src/main/java/avengers/lion/mission/service/VerificationEventService.java
  • src/main/java/avengers/lion/mission/service/VerificationCleanupService.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T13:24:54.608Z
Learnt from: Juhye0k
PR: Lets-Gu/backend#25
File: src/main/java/avengers/lion/mission/repository/MissionRepository.java:15-18
Timestamp: 2025-08-13T13:24:54.608Z
Learning: MissionRepository.findAllByMissionStatusWithCompletedMissions() 메서드는 의도적으로 모든 CompletedMission 데이터를 fetch-join으로 로드합니다. 이는 서비스 레이어에서 특정 회원의 미션 완료 여부를 판단하기 위해 전체 완료 기록이 필요하기 때문입니다.

Applied to files:

  • src/main/java/avengers/lion/mission/repository/MissionRepository.java
  • src/main/java/avengers/lion/mission/service/MissionService.java
🧬 Code graph analysis (6)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (3)
src/main/java/avengers/lion/auth/api/AuthApi.java (2)
  • Operation (14-25)
  • Tag (11-37)
src/main/java/avengers/lion/global/config/security/SecurityConfig.java (2)
  • Configuration (23-81)
  • oauth2 (60-64)
src/main/java/avengers/lion/auth/service/AuthService.java (1)
  • Service (9-19)
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (1)
src/main/java/avengers/lion/mission/service/VerificationCleanupService.java (1)
  • Slf4j (7-33)
src/main/java/avengers/lion/mission/domain/Mission.java (2)
src/main/java/avengers/lion/mission/domain/CompletedMission.java (2)
  • Entity (17-59)
  • Builder (47-54)
src/main/java/avengers/lion/mission/domain/MissionBatches.java (1)
  • Entity (15-59)
src/main/java/avengers/lion/mission/dto/response/MissionResponse.java (4)
src/main/java/avengers/lion/mission/dto/response/MissionReviewResponse.java (1)
  • MissionReviewResponse (8-20)
src/main/java/avengers/lion/mission/dto/response/MissionPreReviewResponse.java (1)
  • MissionPreReviewResponse (8-12)
src/main/java/avengers/lion/review/dto/response/UnWrittenReviewResponse.java (1)
  • Schema (7-28)
src/main/java/avengers/lion/review/dto/response/WrittenReviewResponse.java (1)
  • Schema (9-36)
src/main/java/avengers/lion/mission/api/MissionApi.java (3)
src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java (2)
  • Override (23-45)
  • Override (47-65)
src/main/java/avengers/lion/review/repository/ReviewQueryRepository.java (1)
  • findAllReviewByMissionId (13-13)
src/main/java/avengers/lion/review/api/ReviewApi.java (1)
  • Operation (105-140)
src/main/java/avengers/lion/mission/repository/MissionRepository.java (2)
src/main/java/avengers/lion/mission/repository/CompletedMissionRepository.java (2)
  • CompletedMissionRepository (10-14)
  • Query (12-13)
src/main/java/avengers/lion/review/repository/CompletedMissionQueryRepositoryImpl.java (2)
  • Slf4j (18-47)
  • Override (25-46)
🔇 Additional comments (14)
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (2)

62-67: VerificationMetadata에 java.io.Serializable 구현 추가 권고

레코드가 Serializable을 구현하지 않아 Redis 저장(기본 JDK 직렬화 또는 RedisTemplate 설정 미적용 시)에서 NotSerializableException 또는 역직렬화 타입 불일치가 발생할 수 있습니다. 안전을 위해 레코드에 Serializable을 추가하세요. (위치: src/main/java/avengers/lion/mission/service/MetadataCacheService.java:62–67)

-    public record VerificationMetadata(
+    public record VerificationMetadata(
         String imageUrl,      // Cloudinary 이미지 URL
         String publicId,      // Cloudinary public ID (삭제용)
         Long missionId,
         Long memberId
-    ) {}
+    ) implements java.io.Serializable {}

검증 불가: 제공된 스크립트가 파일을 찾지 못해 레포 내 RedisTemplate/Serializer 설정을 확인하지 못했습니다. RedisTemplate/Serializer(예: GenericJackson2JsonRedisSerializer, JdkSerializationRedisSerializer 등) 설정 유무를 확인하고, 설정이 있다면 그에 맞춰 처리하세요.


16-16: 제네릭 구체화: RedisTemplate<String, VerificationMetadata> 사용 권장

RedisTemplate<String, Object> 대신 제네릭을 구체화해 타입 안전성과 역직렬화 안정성 개선. RedisTemplate 빈 시그니처와 직렬화기 설정(예: Jackson2JsonRedisSerializer 등)이 일치하는지 수동 확인 필요.

-    private final RedisTemplate<String, Object> redisTemplate;
+    private final RedisTemplate<String, VerificationMetadata> redisTemplate;
src/main/java/avengers/lion/mission/controller/MissionController.java (1)

54-60: 메서드명 변경 확인.

getMissionReviewsScroll에서 getMissionReviewsWithCursor로 메서드명이 변경되었고 sortType 파라미터가 추가되었습니다. 메서드 시그니처가 MissionApi 인터페이스와 일치하므로 변경사항이 올바릅니다.

src/main/java/avengers/lion/mission/api/MissionApi.java (1)

140-145: API 메서드명 변경 및 파라미터 추가 확인.

미션 리뷰 무한스크롤 API의 메서드명이 getMissionReviewsScroll에서 getMissionReviewsWithCursor로 변경되었고, sortType 파라미터가 추가되었습니다. 이는 API의 명확성을 높이고 정렬 옵션을 제공하는 개선사항입니다.

src/main/java/avengers/lion/mission/repository/MissionRepository.java (2)

16-17: 쿼리 간소화 및 성능 최적화.

기존의 복잡한 fetch join 쿼리에서 단순한 상태 기반 조회로 변경되었습니다. 이는 N+1 문제 해결을 위한 좋은 접근입니다.


19-20: 완료된 미션 ID 조회 메서드 추가.

특정 회원의 완료된 미션 ID를 Set<Long>으로 반환하는 새로운 메서드가 추가되었습니다. 이는 서비스 레이어에서 미션 완료 상태를 효율적으로 확인할 수 있도록 하는 좋은 개선사항입니다.

src/main/java/avengers/lion/mission/dto/response/MissionResponse.java (2)

7-7: MissionResponse에 tip 필드 추가.

Mission 엔티티의 새로운 tip 필드가 MissionResponse DTO에도 올바르게 추가되었습니다. 필드 순서와 타입이 적절합니다.


18-18: 팩토리 메서드에서 tip 필드 매핑 확인.

mission.getTip() 호출을 통해 Mission 엔티티의 tip 필드가 MissionResponse로 올바르게 매핑되고 있습니다.

src/main/java/avengers/lion/mission/service/MissionService.java (4)

28-32: 서비스 클래스 주석 추가.

클래스 레벨 주석이 추가되어 MissionService의 역할과 책임이 명확하게 설명되었습니다.


44-45: 리포지토리 메서드 호출 최적화.

기존의 복잡한 fetch join 방식에서 단순한 상태 기반 조회와 별도의 완료된 미션 ID 조회로 분리되었습니다. 이는 N+1 문제를 해결하고 쿼리 성능을 최적화하는 좋은 접근입니다.


58-67: 프리뷰 페이징 로직 개선.

미션 리뷰 프리뷰에서 hasNext 판단 로직과 nextId 계산이 더욱 명확하게 개선되었습니다. reviews.getLast().getId()를 사용하여 마지막 리뷰의 ID를 가져오는 방식도 적절합니다.


85-85: 무한스크롤 페이징 로직 간소화.

nextId 계산 로직이 한 줄로 간소화되어 가독성이 향상되었습니다. 조건부 할당 방식도 명확합니다.

src/main/java/avengers/lion/mission/domain/Mission.java (1)

47-48: tip 필드의 NULL 제약 검토 및 DB 마이그레이션 반영 필요

  • src/main/java/avengers/lion/mission/domain/Mission.java (라인 47–48): @column(name = "tip") private String tip; — nullable 속성 미지정(기본 true → DB에서 NULL 허용).
  • 사용처: src/main/java/avengers/lion/mission/dto/response/MissionResponse.java:18 에서 mission.getTip() 사용 확인.
  • 저장소에서 "tip" 컬럼을 포함한 SQL 마이그레이션을 찾지 못함. 비즈니스상 NULL 허용 불가라면 엔티티에 @column(nullable = false) 추가하고, 기존 데이터 처리를 포함한 DB 마이그레이션(또는 기본값/데이터마이그레이션) 생성·적용할 것.
src/main/java/avengers/lion/mission/service/MissionVerifyService.java (1)

21-25: 모듈 목적/범위 주석 보강 좋습니다.

서비스 역할이 명확해졌습니다.

Comment on lines 63 to +71
// Redis에 임시 코드-토큰 매핑 저장 (5분 TTL)
redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
log.info("🔵 Redis에 임시 코드 저장 완료");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

DB 트랜잭션 커밋 전 Redis에 토큰 저장 — TOCTOU 위험

JPA 트랜잭션이 커밋되기 전에 액세스 토큰을 Redis에 저장합니다. 커밋 실패 시 존재하지 않는 사용자에 대한 토큰이 발급·노출될 수 있습니다. 트랜잭션 커밋 이후에 저장하도록 조정하세요.

수정 예:

-        // 사용자 식별 정보를 이용해 토큰 발급
-        String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
-        // 임시 코드 생성 (UUID)
-        String authCode = UUID.randomUUID().toString();
-        // Redis에 임시 코드-토큰 매핑 저장 (5분 TTL)
-        redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
+        String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
+        String authCode = UUID.randomUUID().toString();
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+            @Override public void afterCommit() {
+                redisTemplate.opsForValue()
+                    .set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
+            }
+        });
📝 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
// 사용자 식별 정보를 이용해 토큰 발급
String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
log.info("🔵 AccessToken 생성 완료 - Length: {}", accessToken.length());
// 임시 코드 생성 (UUID)
String authCode = UUID.randomUUID().toString();
log.info("🔵 임시 코드 생성: {}", authCode);
// Redis에 임시 코드-토큰 매핑 저장 (5분 TTL)
redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
log.info("🔵 Redis에 임시 코드 저장 완료");
String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
String authCode = UUID.randomUUID().toString();
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
redisTemplate.opsForValue()
.set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
}
});
🤖 Prompt for AI Agents
In src/main/java/avengers/lion/global/OAuth2SuccessHandler.java around lines 63
to 71, the code stores the access token in Redis before the JPA transaction
commits which risks TOCTOU if the DB commit later fails; change the flow so the
Redis write happens only after successful transaction commit by either moving
the Redis set call into the service layer after the transaction completes or
register an afterCommit callback via
TransactionSynchronizationManager.registerSynchronization/TransactionSynchronizationAdapter
to perform redisTemplate.opsForValue().set("auth_code:"+authCode, accessToken,
5, TimeUnit.MINUTES) only in the afterCommit callback; ensure authCode is still
generated and propagated to the afterCommit handler and handle errors/logging
accordingly.

Comment on lines 70 to 75
redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
log.info("🔵 Redis에 임시 코드 저장 완료");


// 프론트엔드로 임시 코드와 함께 리다이렉트
String frontendUrl = "http://localhost:5174/auth/callback?code=" + authCode;
getRedirectStrategy().sendRedirect(request, response, frontendUrl);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

임시 코드 교환 로직 점검 요청

auth_code 키로 저장된 토큰이 1회성으로 안전하게 교환되고 즉시 삭제되는지, TTL 5분이 문서/클라이언트와 일치하는지 확인 필요.

아래 스크립트로 교환/삭제 구현부를 찾아 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# auth_code 조회·삭제 사용처 점검
rg -n -C3 -P 'auth_code:\s*\+|auth_code:' --type=java
rg -n -C2 -P 'opsForValue\(\)\.(get|getAndDelete|delete)\(' --type=java

Length of output: 4593


임시 auth_code 조회→삭제를 원자적으로 처리하도록 수정 필요

OAuth2SuccessHandler에서 auth_code를 5분 TTL로 저장하는 것(src/main/java/avengers/lion/global/OAuth2SuccessHandler.java:70)을 확인했고, AuthController.exchangeToken에서는 opsForValue().get(...)로 조회한 뒤 redisTemplate.delete(...)로 삭제(src/main/java/avengers/lion/auth/controller/AuthController.java:24,30)하고 있어 조회와 삭제가 분리되어 동시 요청 시 동일 코드가 중복 사용될 가능성이 있습니다.

  • 조치: 조회+삭제를 원자적으로 처리하도록 변경 — 예: opsForValue().getAndDelete("auth_code:...") 사용(라이브러리 메서드 지원 여부 확인) 또는 Lua 스크립트/트랜잭션으로 GET+DEL 원자화. (수정 대상: src/main/java/avengers/lion/auth/controller/AuthController.java:24-30)
  • 확인: TTL(5분)이 문서/프론트엔드 요구사항과 일치하는지 확인 및 필요 시 조정.
🤖 Prompt for AI Agents
In src/main/java/avengers/lion/auth/controller/AuthController.java around lines
24 to 30, the current flow separately calls redisTemplate.opsForValue().get(...)
then redisTemplate.delete(...), which can allow race conditions and double-use
of the same auth_code; change this to perform an atomic get-and-delete operation
— prefer using RedisOperations.opsForValue().getAndDelete(key) if available,
otherwise implement the atomicity via a Redis Lua script (GET+DEL) or a Redis
transaction that returns and removes the key in one atomic step; ensure error
handling remains and validate that the 5-minute TTL set elsewhere matches spec
and adjust if required.

@Juhye0k Juhye0k merged commit 27e766e into dev Sep 22, 2025
3 checks passed
@seo-bo
Copy link
Copy Markdown

seo-bo commented Sep 23, 2025

🍓

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