Skip to content

[URECA-65] Feat: 긴 STT#47

Merged
40food merged 28 commits intodevelopfrom
URECA-65/Feat/longerstt
Jan 28, 2026
Merged

[URECA-65] Feat: 긴 STT#47
40food merged 28 commits intodevelopfrom
URECA-65/Feat/longerstt

Conversation

@40food
Copy link
Copy Markdown
Contributor

@40food 40food commented Jan 27, 2026

Key Changes

  • STT 길게 할 수 있게 함, google cloud와 연동

작업 내역

  • 녹음 동작 오류 수정(xml에 필요한 값을 넣어주지 않아서 null 오류가 뜨며 녹음이 안 됐음)
  • 긴 문장 STT 가능하게 처리
  • google cloud에 wav 파일 업로드
  • 겸사겸사 북마크 여부 업데이트하는 api 추가
  • close: [URECA-65] Feat: 상담 시간 늘리기 #42

💬 공유사항 to 리뷰어

비고

Summary by CodeRabbit

  • 새로운 기능

    • Google Cloud Storage 연동을 통한 WAV 업로드 기능 추가
    • 멀티스테이지 Docker 이미지와 시작 시 Cloud SQL 프록시 실행 추가
  • 개선 사항

    • STT를 GCS URI 기반 장기 처리로 전환하여 대용량 음성 인식·안정성 향상
    • 녹음 처리 흐름에 작업 상태 추적, 실패 처리 및 요약 연동 강화
  • 기타 변경사항

    • 환경설정 항목(클라우드·JWT·OAuth·S3 등) 대폭 추가
    • GCS 클라이언트 라이브러리 추가, 일부 테스트 제거 및 매퍼 반환형 변경

✏️ Tip: You can customize this high-level summary in your review settings.

@40food 40food self-assigned this Jan 27, 2026
@github-actions github-actions bot changed the title Ureca 65/feat/longerstt [URECA-65] Feat: Ureca 65/feat/longerstt Jan 27, 2026
@40food 40food changed the title [URECA-65] Feat: Ureca 65/feat/longerstt 긴 STT Jan 27, 2026
@github-actions github-actions bot changed the title 긴 STT [URECA-65] Feat: 긴 STT Jan 27, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

로컬 WAV 파일을 Google Cloud Storage에 업로드하고 URI 기반의 longRunningRecognizeAsync로 장시간 STT 흐름을 전환했습니다. GcsUploader 컴포넌트와 com.google.cloud:google-cloud-storage:2.30.0 의존성이 추가되었고, SttService.startStt 시그니처가 CounselingResult 파라미터를 받도록 변경되었습니다. RecordingServiceImpl에서 CounselingResult 생성·상태 관리 로직이 삽입되었고, SttServiceImpl은 GCS 업로드, 장시간 인식 대기, CounselingResult 업데이트 및 SummaryService 연계 흐름으로 재작업되었습니다. SummaryMapper.updateBookmark 반환형이 int로 변경되었고, application.yml 확장, Dockerfile 추가, 일부 테스트 파일·테스트 의존성 삭제가 포함됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 북마크 API(SummaryMapper), 테스트 파일 제거, 의존성 정리 등 일부 변경사항이 URECA-65 범위를 벗어났습니다. 북마크 업데이트, 불필요한 테스트 의존성 제거, UnityApplicationTests 삭제는 별도 이슈로 분리하거나 PR 설명에 명시하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 PR의 주요 변경사항인 긴 STT 처리 기능과 Google Cloud 통합을 명확하게 반영하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 코딩 요구사항이 충족되었습니다: Google Cloud 등록(application.yml), bucket 업로드 확인(GcsUploader), STT 로직 수정(SttServiceImpl의 장문 STT 처리).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

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

Caution

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

⚠️ Outside diff range comments (1)
build.gradle (1)

27-33: Google Cloud BOM으로 버전 일관성 확보하기

현재 조합은 실제로 위험합니다. google-cloud-storage:2.30.0(Dec 2023)과 google-cloud-speech:4.46.0(Sep 2024)은 gax, api-common, grpc 같은 공통 의존성의 버전이 다르게 당겨져서 런타임 에러를 일으킬 수 있습니다.

build.gradle을 다음과 같이 수정하세요:

BOM 적용 예시
dependencies {
    implementation platform('com.google.cloud:libraries-bom:26.72.0')
    
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    implementation 'com.google.cloud:google-cloud-storage'
    implementation 'com.google.cloud:google-cloud-speech'
}

개별 라이브러리에서 버전을 제거하면 BOM이 호환되는 버전 세트를 자동으로 관리합니다.

Protobuf 4.x 호환성 문제가 발생하면 libraries-bom-protobuf3으로 전환하는 방법도 있습니다. Google Cloud 공식 문서 참고하세요.

🤖 Fix all issues with AI agents
In `@src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java`:
- Line 134: userId 문자열을 Long으로 파싱하는 Long longUserId = Long.parseLong(userId);가
try 블록 밖에 있어 잘못된 입력이 들어오면 500으로 떨어집니다: RecordingServiceImpl 내에서 userId 파싱을
Long.parseLong 호출만으로 수행하지 말고 해당 호출을 try-catch(NumberFormatException)로 감싸고 입력
검증(빈값/숫자 여부) 후 유효하지 않으면 적절한 클라이언트 에러로 매핑(예: BadRequestException /
IllegalArgumentException / ValidationException)하여 반환하도록 수정하세요; 구체적으로 longUserId
변환을 파싱 시도 블록으로 옮기고 catch에서는 명확한 에러 메시지와 함께 400 상태로 매핑하거나 기존 예외 매핑 기전(예:
ExceptionHandler)을 사용해 처리하도록 하세요.
- Around line 135-142: 현재 stop() 내부에서 RecordingServiceImpl가 호출하는
sttMapper.insert(...)와 summaryMapper.insertSummary(...)가 별도 트랜잭션으로 실행되어 원자성이
보장되지 않습니다; RecordingServiceImpl.stop() 메서드에 `@Transactional을` 추가하여 두 insert를 단일
트랜잭션으로 묶고 예외 발생 시 롤백되도록 변경하세요 (참고: CounselingResultMapper는
useGeneratedKeys/keyProperty로 키를 설정하였으니 insert 직후 counselingResultId 사용은 유지).

In `@src/main/java/com/ureca/unity/domain/call/util/GcsUploader.java`:
- Around line 22-46: The uploadWav method currently uses Files.readAllBytes and
logs with log.error, which can OOM on large WAVs and misuses log level; change
uploadWav to stream the file via Storage.createFrom(BlobInfo, InputStream) using
try-with-resources and Files.newInputStream(wavPath) (instead of
Storage.create(blobInfo, Files.readAllBytes(wavPath)) / Files.readAllBytes), and
change the debug print to log.debug(...) (replace log.error call). Ensure
GoogleCredentials and Storage creation remain the same and that any thrown
exceptions are propagated as before.

In `@src/main/java/com/ureca/unity/domain/stt/service/SttServiceImpl.java`:
- Around line 36-57: When building the GCS objectName in startStt, validate that
job.getCounselingResultId() is not null (since Long may be null) before using
it; if it is null, throw a clear RuntimeException (or IllegalArgumentException)
with a descriptive message to prevent creating "null.wav" and uploading to a bad
path. Locate the objectName construction in startStt and perform the null check
on job.getCounselingResultId() prior to concatenation, then proceed to call
gcpUploader.uploadWav(...) only when the ID is present.

In
`@src/main/java/com/ureca/unity/domain/summary/controller/SummaryController.java`:
- Around line 37-43: The toggleBookmark endpoint in SummaryController currently
uses `@GetMapping` and performs state mutation via
summaryService.toggleBookmark(summaryId); change it to a non-safe method: either
use `@PostMapping` for a toggle action or, preferably, make it idempotent with
`@PutMapping` and accept an explicit desired state (e.g., a boolean "enabled"
parameter or request body) so the controller method (toggleBookmark) calls a new
service method like summaryService.setBookmark(summaryId, enabled) instead of
toggling; update the method signature and annotations accordingly and adjust
summaryService to expose setBookmark(Long, boolean) if needed.
🧹 Nitpick comments (2)
src/main/java/com/ureca/unity/domain/summary/service/SummaryService.java (1)

163-173: 토글을 단일 UPDATE로 처리해 경합을 줄이는 편이 안전합니다

현재는 조회 후 갱신이라 동시 요청 시 오래된 상태를 뒤집을 수 있습니다. SQL에서 is_bookmarked = NOT is_bookmarked로 토글하고, 영향 건수 0이면 not found로 처리하는 방식 고려해 주세요. (Mapper/XML 변경 필요)

♻️ 변경 방향 예시(서비스 기준)
-    Boolean isBookmarked = summaryMapper.findBookmarkStatus(summaryId);
-
-    if (isBookmarked == null) {
-        throw new IllegalArgumentException("summary not found");
-    }
-
-    int done=summaryMapper.updateBookmark(summaryId, !isBookmarked);
-    return done>0;
+    int done = summaryMapper.toggleBookmark(summaryId); // SQL: is_bookmarked = NOT is_bookmarked
+    if (done == 0) {
+        throw new IllegalArgumentException("summary not found");
+    }
+    return true;
src/main/java/com/ureca/unity/domain/stt/service/SttServiceImpl.java (1)

128-137: 요약 실패 시 상태 불일치 가능성 완화 권장
현재는 DB에 SUCCESS가 반영된 뒤 요약이 실패하면 사용자가 “성공”으로 보게 될 수 있습니다. 요약 실패를 감지해 재시도/상태 보정 경로를 마련하는 쪽이 안전합니다.

♻️ 한 가지 예시(상태/정책은 도메인 기준으로 조정)
         if ("SUCCESS".equals(job.getStatus())) {
-            summaryService.createSummary(
-                    job.getCounselingResultId(),
-                    job.getUserId(),
-                    job.getTexts()
-            );
+            try {
+                summaryService.createSummary(
+                        job.getCounselingResultId(),
+                        job.getUserId(),
+                        job.getTexts()
+                );
+            } catch (Exception ex) {
+                log.error("요약 생성 실패", ex);
+                // 필요 시 재시도 큐/상태 보정 로직 추가
+            }
         }

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java`:
- Around line 135-142: The DB insert (CounselingResult builder +
sttMapper.insert and summaryMapper.insertSummary) can throw and prevent the
Agora stop call from running; wrap the DB inserts in their own try/catch to
log/handle insert failures (catch Exception around sttMapper.insert and
summaryMapper.insertSummary) and then ensure the Agora stop logic is executed in
a finally block (or moved out so it always runs) in RecordingServiceImpl so that
the stop call is invoked regardless of insert errors.
- Around line 135-140: Introduce two enums CounselingStatus { LOADING, FAIL,
SUCCESS } and CounselingType { CALL }, change the CounselingResult entity's
fields from String to these enums (e.g., counselingStatus: CounselingStatus,
counselingType: CounselingType) and update its builder/constructor accordingly;
update RecordingServiceImpl where CounselingResult.builder() sets "LOADING" and
"CALL" to use CounselingStatus.LOADING and CounselingType.CALL, then search and
replace other string usages in SttServiceImpl and SummaryService to use the
enums (e.g., replace "FAIL"/"SUCCESS" with CounselingStatus.FAIL/SUCCESS);
ensure JPA mapping for CounselingResult persists enums (use
`@Enumerated`(EnumType.STRING) or equivalent) and adjust any DTOs/serializers or
tests that expect strings.

In
`@src/main/java/com/ureca/unity/domain/summary/controller/SummaryController.java`:
- Around line 37-43: The toggleBookmark endpoint in SummaryController lacks
userId verification allowing any authenticated user to toggle another user's
bookmark; update the method signature in SummaryController.toggleBookmark to
accept a Long userId (like other endpoints), add a check that the authenticated
user matches the provided userId (or delegate ownership check to
SummaryService), and change the service method SummaryService.toggleBookmark to
accept both summaryId and userId and enforce ownership/authorization there; also
fix formatting by adding spaces around the '=' in the local variable declaration
(boolean done = ...) to match code style.

Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java (1)

169-184: 비동기 작업 실패 시 job 상태가 영구적으로 LOADING 상태로 유지됩니다.

CompletableFuture.runAsync 내부에서 예외가 발생하거나 wavFile이 null/존재하지 않을 때, job 상태가 "FAIL"로 업데이트되지 않습니다. 사용자는 영원히 로딩 상태를 보게 됩니다.

🛠️ 수정 제안
 CompletableFuture.runAsync(() -> {
     try {
         Thread.sleep(10000);
         String m3u8Url = String.format("https://%s.s3.ap-northeast-2.amazonaws.com/recordings/%s/%s_%s.m3u8",
                 s3Bucket, channelName, sid, channelName);
         log.info("변환 시작: {}", m3u8Url);
         File wavFile = new Converter().convertM3u8ToWav(m3u8Url);

         if (wavFile != null && wavFile.exists()) {
             log.info("최종 WAV 생성 성공: {}", wavFile.getAbsolutePath());
             sttService.startStt(wavFile, longUserId, job);
+        } else {
+            log.error("WAV 파일 생성 실패 - job 상태 FAIL 처리");
+            job.setStatus("FAIL");
+            sttMapper.updateResult(job);
+            Long summaryId = summaryMapper.findLatestSummaryId(longUserId, job.getCounselingResultId());
+            if (summaryId != null) {
+                summaryMapper.updateStatus(summaryId, "FAIL");
+            }
         }
     } catch (Exception e) {
         log.error("비동기 변환 작업 중 오류: {}", e);
+        job.setStatus("FAIL");
+        sttMapper.updateResult(job);
+        Long summaryId = summaryMapper.findLatestSummaryId(longUserId, job.getCounselingResultId());
+        if (summaryId != null) {
+            summaryMapper.updateStatus(summaryId, "FAIL");
+        }
     }
 });
🤖 Fix all issues with AI agents
In `@Dockerfile`:
- Around line 8-10: 빌드 결과 JAR이 /app/target에 생성되는데 CMD가 /app 루트에서
unity-0.0.1-SNAPSHOT.jar를 찾으려 해서 컨테이너가 실패합니다; 수정하려면 Dockerfile의 CMD(현재:
["java","-jar","unity-0.0.1-SNAPSHOT.jar"])를 실제 출력 경로로 변경하거나 빌드 산출물을 루트로 이동하도록
변경하세요 — 예를 들어 CMD를 /app/target/unity-0.0.1-SNAPSHOT.jar 경로로 가리키거나, mvn 빌드 후 JAR을
/app로 복사하도록 조정해 RUN ./mvnw clean package -DskipTests 및 CMD가 일치하도록 만드세요.

In `@src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java`:
- Around line 186-193: The catch block currently updates DB rows even when the
initial job insert may have failed and also calls summaryMapper.updateStatus
with a potentially null ID; modify the exception path in RecordingServiceImpl so
that you only call sttMapper.updateResult(job) and set job.setStatus("FAIL")
when jobPersisted == true (or when the job has a valid persistent id), and when
calling summaryMapper.findLatestSummaryId(longUserId,
job.getCounselingResultId()) check the returned Long summaryId for null before
invoking summaryMapper.updateStatus(summaryId, "FAIL"); if summaryId is null,
skip updateStatus (or handle via a safe fallback) and still throw the
CustomException(ErrorCode.INTERNAL_SERVER_ERROR).

In `@src/main/resources/application-test.yml`:
- Around line 2-5: The datasource driver-class-name string is incomplete ("com")
causing ClassNotFoundException during test context initialization; update the
driver-class-name property under datasource (driver-class-name) to the correct
H2 JDBC driver class "org.h2.Driver" so the test application context can load
the H2 driver.

In `@src/main/resources/application.yml`:
- Around line 71-75: Replace the hard-coded jwt.secret value with an
externalized required environment property (e.g., JWT_SECRET) by binding
jwt.secret to that env var in the configuration and fail fast if it's missing;
update the jwt configuration (jwt.secret, jwt.issuer, access-expiration-seconds,
refresh-expiration-seconds) to read from environment config and ensure
application startup throws a clear error when jwt.secret is unset so tokens are
never signed with the placeholder value.

In `@src/test/java/com/ureca/unity/UnityApplicationTests.java`:
- Around line 8-10: The class-level `@Disabled` on UnityApplicationTests is
skipping the Spring context-load test; remove the `@Disabled` annotation from the
UnityApplicationTests class so contextLoads() runs, or replace it with a
conditional disable such as `@DisabledIfEnvironmentVariable` or `@DisabledIf` to
only skip in specific environments; update the class declaration that contains
`@SpringBootTest`, `@ActiveProfiles`("test"), and the contextLoads() method
accordingly.
🧹 Nitpick comments (3)
Dockerfile (1)

2-8: 빌드/런타임 분리(멀티 스테이지)로 이미지 경량화와 보안 개선 권장

단일 스테이지 + COPY .는 빌드 도구/소스가 모두 런타임 이미지에 들어가 이미지가 커지고, 컨텍스트에 민감 파일이 있으면 함께 포함될 위험이 있습니다. 멀티 스테이지로 빌드 산출물만 복사하고, .dockerignore로 불필요 파일을 제외하는 구성을 권장합니다. 자세한 패턴은 Docker 공식 문서를 참고하세요.

♻️ 예시 스케치
-FROM  openjdk:17-alpine
+FROM maven:3.9-eclipse-temurin-17-alpine AS build
+WORKDIR /app
+COPY . /app
+RUN ./mvnw clean package -DskipTests
+
+FROM eclipse-temurin:17-jre-alpine
+WORKDIR /app
+COPY --from=build /app/target/unity-0.0.1-SNAPSHOT.jar app.jar
+CMD ["java", "-jar", "app.jar"]
src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java (1)

137-137: counselorId=1L 하드코딩 검토 필요

상담사 ID가 고정값으로 설정되어 있습니다. 추후 다중 상담사 지원 시 문제가 될 수 있으므로, 의도적인 설계라면 상수로 추출하고 주석을 남겨주세요.

+    // TODO: 현재는 단일 상담사만 지원, 추후 동적 할당 필요
+    private static final Long DEFAULT_COUNSELOR_ID = 1L;
     ...
     CounselingResult job = CounselingResult.builder()
             .userId(longUserId)
-            .counselorId(1L)
+            .counselorId(DEFAULT_COUNSELOR_ID)
build.gradle (1)

31-33: Google Cloud 라이브러리를 BOM 관리로 통일하세요.

Line 31–33의 google-cloud-storage:2.30.0google-cloud-speech:4.46.0은 각각 2024년 중반 이전 버전입니다. 현재 최신은 Storage 2.62.0(2026-01-15), Speech 4.77.0(2026-01-20)이므로 의존 버전이 심하게 뒤쳐져 있습니다.

공식 Google Cloud 라이브러리 BOM(libraries-bom:26.74.0)을 도입하면:

  • 버전 드리프트 자동 방지
  • Spring Boot 3.2.5 + Java 17 공식 호환성 확보
  • 보안 및 기능 업데이트 자동 반영
♻️ BOM 적용 예시
 dependencies {
+    implementation platform('com.google.cloud:libraries-bom:26.74.0')
-    implementation 'com.google.cloud:google-cloud-storage:2.30.0'
-    implementation 'com.google.cloud:google-cloud-speech:4.46.0'
+    implementation 'com.google.cloud:google-cloud-storage'
+    implementation 'com.google.cloud:google-cloud-speech'
 }

공식 문서: https://github.com/googleapis/java-cloud-bom

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@Dockerfile`:
- Around line 20-21: The COPY using a wildcard in the Dockerfile is brittle;
replace the wildcard COPY --from=builder /app/build/libs/*.jar app.jar with an
explicit filename (e.g., COPY --from=builder /app/build/libs/app.jar app.jar) so
the Docker build won't break if multiple JARs appear, and concurrently set
bootJar.archiveFileName (in build.gradle) to that exact filename to ensure the
build produces the expected artifact.

In `@src/main/java/com/ureca/unity/domain/call/service/RecordingServiceImpl.java`:
- Around line 141-148: The stop() method in RecordingServiceImpl performs
sttMapper.insert(job) and summaryMapper.insertSummary(...) without transactional
boundaries, so partial success can leave CounselingResult in LOADING; annotate
the stop() method with Spring's `@Transactional` (e.g., `@Transactional`(rollbackFor
= Exception.class)) on the RecordingServiceImpl class/method so both
sttMapper.insert and summaryMapper.insertSummary run in one transaction and any
exception triggers rollback, and remove or rethrow the catch that swallows
exceptions (or if you must catch, rethrow after compensating) rather than only
setting jobPersisted; alternatively implement a compensation path in the catch
that calls counselingResultMapper.updateResult(job.getCounselingResultId(),
Status.FAIL) when summaryMapper.insertSummary fails to ensure consistent state.

In `@src/main/resources/application.yml`:
- Around line 77-81: Create a validated configuration class for Agora like the
suggested AgoraProperties record annotated with `@ConfigurationProperties`(prefix
= "agora") and `@Validated`, declaring `@NotBlank` appId, appCert, customerId,
customerSecret; register it (e.g., via
`@EnableConfigurationProperties`(AgoraProperties.class) or as a `@Component`) and
replace any direct `@Value` injection usages in CallServiceImpl and
RecordingServiceImpl to inject AgoraProperties instead so missing/blank values
fail fast on startup and mirror the JwtProperties pattern.
- Around line 48-69: The kakao OAuth config currently has the client-secret
commented out (oauth.kakao.client-secret), which can cause token exchange
failures if the app's Client secret feature is ON; update the configuration to
require and load the client secret from an environment variable (e.g., set
oauth.kakao.client-secret: ${KAKAO_CLIENT_SECRET}) or, if you intentionally
disable the Client secret in Kakao console, add a clear comment near
oauth.kakao.client-secret documenting that the console feature is disabled and
why; ensure the deployed environment provides KAKAO_CLIENT_SECRET when keeping
the feature ON and validate token exchange in integration tests after the
change.
🧹 Nitpick comments (4)
src/main/resources/application.yml (4)

71-75: JWT 만료 설정이 초 단위로 올바르게 해석되고 있습니다.

현재 JwtPropertiesaccessExpirationSecondsrefreshExpirationSecondslong 타입으로 바인딩하며, JwtIssuer.createToken()에서 Instant.now().plusSeconds(expSeconds)로 사용하고 있습니다. Instant.plusSeconds()는 long 파라미터를 초 단위로 해석하므로 설정값이 의도대로 작동합니다.

선택사항: Spring Boot 컨벤션 개선
더 명시적이고 타입-안전한 방식을 원한다면, Duration 타입 사용을 검토해보세요:

`@Min`(duration = "1s")
Duration accessExpirationSeconds,  // Duration.ofSeconds() 불필요

프로젝트의 다른 설정(RestTemplateConfig의 타임아웃)이 이미 Duration을 사용 중이므로, 이렇게 통일하면 코드 일관성이 높아집니다. 현재 구현은 정상이므로 긴급하지는 않습니다.


83-88: 정적 액세스 키를 IAM 역할 기반 자격 증명 체인으로 변경 권장

현재 설정은 환경 변수에 의존하는 장기 액세스 키 패턴입니다. AWS 공식 문서에서는 프로덕션 환경에서 DefaultCredentialsProvider 체인(Java SDK v2)을 사용할 것을 강력히 권장합니다. 이 체인은 자동으로 다음 순서로 자격 증명을 검색합니다: JVM 시스템 프로퍼티 → 환경 변수 → 웹 아이덴티티(IRSA/OIDC) → 공유 자격 증명 파일 → 컨테이너 자격 증명(ECS) → EC2 인스턴스 프로필.

개선 방안:

  • ECS/EKS 배포: 태스크 역할 또는 IRSA(웹 아이덴티티)로 전환하면, 정적 키를 완전히 제거할 수 있습니다.
  • EC2 배포: EC2 인스턴스 프로필에 필요한 IAM 권한을 부여하고, 환경 변수 대신 SDK의 기본 체인에 맡기세요.
  • 로컬 개발: 공유 자격 증명 파일(~/.aws/credentials) + AWS_PROFILE 환경 변수를 사용합니다.

AWS는 임시 자격 증명(역할을 통한) 사용을 권장하며, 장기 액세스 키 노출 시 재해성 위험이 있습니다.


90-95: GCP 인증 방식을 ADC 또는 Workload Identity로 마이그레이션하세요.

현재 서비스 계정 키 파일 경로(GOOGLE_CLOUD_LOCATION)를 환경변수로 관리하는 방식은 Google의 공식 권고와 맞지 않습니다. Google Cloud 공식 문서에서는 다음을 권장합니다:

  • Google Cloud 위의 워크로드: ADC(Application Default Credentials)와 연결된 서비스 계정 사용 (GKE, Cloud Run 등에 서비스 계정 할당)
  • 외부/온프레미스 워크로드: Workload Identity Federation으로 단기 토큰 교환 (장기 키 제거)

서비스 계정 키 파일은 장기 자격증명이므로 유출 위험, 로테이션 부담, 관리 복잡도가 높습니다. 위의 방식이 보안과 운영 측면에서 훨씬 유리합니다.

다음 단계: 1) 배포 환경 확인 (GCP 위인지 외부인지), 2) Google Cloud 공식 문서에서 해당 방식의 마이그레이션 가이드 참고, 3) 키 파일 의존성 제거


97-98: Gemini API 키 제한(도메인/IP/API) 설정으로 보안 강화 권장

환경변수 분리는 좋은 결정입니다! 한 발 더 나아가 Google Cloud Console에서 API 키 제한을 설정하면, 혹시 키가 노출되어도 피해를 제한할 수 있습니다.

추가 설정 항목:

Google에서도 프로덕션 환경에서는 필수 권장사항입니다.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@Dockerfile`:
- Around line 20-22: The Dockerfile copies the built JAR to /app/app.jar (COPY
--from=builder /app/build/libs/*.jar app.jar) but CMD tries to run
build/libs/app.jar, causing a missing file at runtime; update the CMD to
reference the actual copied JAR (e.g., replace CMD ["java", "-jar",
"build/libs/app.jar"] with CMD ["java", "-jar", "/app/app.jar"]) or
alternatively change the COPY destination to match CMD (e.g., copy into
build/libs/app.jar) so the paths are consistent between COPY and CMD.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@Dockerfile`:
- Line 15: The ENTRYPOINT currently runs cloud_sql_proxy and the Java app with
backgrounding ("&" and "wait") which breaks signal handling and process
supervision; replace this by running a proper init/process supervisor or moving
cloud_sql_proxy to a sidecar: either (A) remove the combined ENTRYPOINT and run
only the Java process (java -jar app.jar) in the container and run
cloud_sql_proxy as a separate container/sidecar, or (B) integrate a minimal
init/process manager (tini or supervisord) as PID 1 and configure it to run
cloud_sql_proxy and the Java app as managed services so signals (SIGTERM) and
child restarts are handled correctly; update the Dockerfile ENTRYPOINT to invoke
the chosen supervisor instead of the current ENTRYPOINT string referencing
cloud_sql_proxy, java -jar app.jar and wait.
- Line 15: The ENTRYPOINT currently hardcodes the Cloud SQL instance string in
the cloud_sql_proxy invocation; change it to read the instance from an
environment variable (e.g., CLOUD_SQL_INSTANCE) and use that variable in the
cloud_sql_proxy command so different environments can pass different instances
at runtime; update the ENTRYPOINT line that runs "./cloud_sql_proxy
-instances=..." to use shell expansion like
"-instances=${CLOUD_SQL_INSTANCE}:tcp:3306" (provide a sensible default or fail
fast if empty) while keeping the existing credentials-file and Java startup
behavior.
- Around line 13-14: The Dockerfile currently copies a secret into the image
(COPY ./render-access.json /secrets/render-access.json and RUN chmod 400 ...)
which embeds credentials in the image; remove those COPY and RUN lines from the
Dockerfile and instead document and implement runtime secret delivery (e.g.,
bind-mount /secrets/render-access.json at container start, use Docker Compose
volumes or Kubernetes Secrets) so the image contains no credentials; update any
start/run scripts or deployment manifests to mount the secret path and verify
the app reads /secrets/render-access.json at runtime.
🧹 Nitpick comments (1)
Dockerfile (1)

9-10: Cloud SQL Proxy 다운로드 시 체크섬 검증이 없습니다.

네트워크에서 바이너리를 직접 다운로드할 때 무결성 검증 없이 실행하면 supply-chain 공격에 취약합니다.

권장 해결책:

  1. 공식 Docker 이미지 사용 (권장)
  2. 체크섬 검증 추가
🔧 공식 이미지 사용 예시 (권장)

Cloud SQL Proxy를 별도 컨테이너로 분리하고 공식 이미지 사용:

image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.8.0

📚 참고: Cloud SQL Proxy 공식 컨테이너 이미지

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/main/resources/application.yml`:
- Around line 31-34: The configuration enables DEBUG logging for
com.zaxxer.hikari and org.springframework.jdbc in application.yml which is
unsafe for production; change these logging levels to INFO or WARN and move
DEBUG entries into a dev-specific profile file (e.g., application-dev.yml) so
production uses the default INFO/WARN; update the logging.level keys for
com.zaxxer.hikari and org.springframework.jdbc accordingly and ensure the Spring
profile activation mechanism loads application-dev.yml only for development.
- Around line 92-97: Add startup-time validation for the Google Cloud
credentials file to fail fast if it's missing/unreadable: implement a
`@PostConstruct` method (e.g., validateCredentials) in GcsUploader and
SttServiceImpl — or a shared utility called from both — that loads the
configured keyPath (${google.cloud.credentials.location}) via
DefaultResourceLoader (or ResourceLoader), checks resource.exists() and
resource.isReadable(), and throws an IllegalStateException with the keyPath and
cause if validation fails; ensure the method name (validateCredentials) and the
keyPath configuration reference match the existing fields used in GcsUploader
and SttServiceImpl so the app fails on startup when the credentials file is not
accessible.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@Dockerfile`:
- Around line 13-14: The Dockerfile currently downloads the Cloud SQL Proxy
binary in the RUN step that fetches "cloud_sql_proxy" without validating
integrity; add SHA256 checksum verification to that RUN step by supplying the
official SHA256 (either hard-coded or via a build ARG like
CLOUD_SQL_PROXY_SHA256) and validating the downloaded file with sha256sum
(failing the build on mismatch) before making it executable; target the RUN line
that downloads "cloud_sql_proxy" so the build will abort if the checksum does
not match.

@40food 40food merged commit aabe40a into develop Jan 28, 2026
3 checks passed
@40food 40food deleted the URECA-65/Feat/longerstt branch January 28, 2026 13:18
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.

[URECA-65] Feat: 상담 시간 늘리기

3 participants