Skip to content

[EDMT-452] 커스텀 메트릭 정합성 문제 해결#67

Merged
wldks1008 merged 12 commits into
developfrom
fix/EDMT-452
Sep 19, 2025
Merged

[EDMT-452] 커스텀 메트릭 정합성 문제 해결#67
wldks1008 merged 12 commits into
developfrom
fix/EDMT-452

Conversation

@wldks1008

@wldks1008 wldks1008 commented Sep 19, 2025

Copy link
Copy Markdown
Member

📣 Jira Ticket

EDMT-452

Summary by CodeRabbit

  • Refactor
    • AI 학생기록 생성/완료 메트릭 수집을 트랜잭션 의존에서 즉시 기록 방식으로 단순화해 안정성과 관측성을 향상했습니다.
    • 레코드 타입 결정 로직을 내부로 이동시켜 호출 흐름을 간소화하고 일관성을 개선했습니다.
    • 메트릭 관련 명칭 정리 및 컴포넌트 구조를 개선했습니다.
  • New Features
    • Redis 기반 추적기를 추가해 레코드별 첫 생성 여부를 정확히 추적하도록 구현했습니다.
  • Chores
    • AOP 자동 프록시 지원을 활성화했습니다.

@wldks1008 wldks1008 self-assigned this Sep 19, 2025
@wldks1008 wldks1008 added the 🛠️ fix 🛠️ Something isn't working label Sep 19, 2025
@wldks1008 wldks1008 changed the title [EDMT-452] [EDMT-452] 학생 기록 완료 메트릭 누락 문제 수정 Sep 19, 2025
@coderabbitai

coderabbitai Bot commented Sep 19, 2025

Copy link
Copy Markdown

Walkthrough

애플리케이션에 AspectJ 자동 프록시를 활성화하고, AI 학생기록 생성 흐름에서 레코드 타입 전달을 제거해 Facade/Aspect가 StudentRecordService로 타입을 조회하도록 변경했으며, AOP 기반 메트릭 수집은 트랜잭션 동기화를 제거하고 즉시 기록 방식으로 단순화했습니다.

Changes

Cohort / File(s) Summary
AOP 활성화
edukit-api/src/main/java/com/edukit/EdukitApiApplication.java
@EnableAspectJAutoProxy 및 import 추가.
컨트롤러 호출 단순화
edukit-api/src/main/java/com/edukit/studentrecord/controller/StudentRecordAIController.java
컨트롤러에서 DB 조회 제거; Facade 호출에서 StudentRecordType 인자 제거 및 파라미터 서식 경미 변경.
AI Facade 타입 내부 조회
edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordAIFacade.java
createTaskId 시그니처에서 StudentRecordType 제거; 내부에서 StudentRecordService#getRecordDetail로 타입 조회해 프롬프트 생성.
사소 포맷(페이사드)
edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordFacade.java
공백 줄 정리(포맷 변경만).
AOP 메트릭스 수집 경량화
edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java
트랜잭션 동기화 제거, 즉시 카운팅으로 전환. 의존성: StudentRecordMetricsServiceStudentRecordMetricsCounter, GenerationTrackingServiceRecordGenerationTracker, StudentRecordService 주입. 포인트컷 표기 조정 및 오류 로깅으로 방어적 처리.
레디스 기반 생성 트래커 추가
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java
새 컴포넌트 추가: Redis Lua 스크립트 기반 증가+만료 설정으로 레코드별 생성 횟수 추적, isFirstGeneration(long) 공개 메서드 제공.
메트릭스 서비스명 변경
edukit-core/src/main/java/com/edukit/core/studentrecord/service/StudentRecordMetricsCounter.java
클래스명 StudentRecordMetricsServiceStudentRecordMetricsCounter로 리네임(기능 동일).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Controller
  participant F as StudentRecordAIFacade
  participant S as StudentRecordService
  participant P as AIPromptGenerator
  participant T as TaskService
  participant E as EventPublisher

  C->>F: createTaskId(memberId, recordId, byteCount, prompt)
  F->>S: getRecordDetail(memberId, recordId)
  S-->>F: StudentRecord (type)
  F->>P: createStreamingPrompt(type, byteCount, prompt)
  P-->>F: requestPrompt
  F->>T: createTask(requestPrompt)
  T-->>F: taskId
  F->>E: publishTaskCreated(taskId)
  F-->>C: StudentRecordTaskResponse(taskId)
Loading
sequenceDiagram
  autonumber
  participant Biz as Annotated Method
  participant AOP as StudentRecordMetricsAspect
  participant S as StudentRecordService
  participant R as RecordGenerationTracker
  participant M as StudentRecordMetricsCounter

  Note over Biz,AOP: @StudentRecordMetrics가 붙은 비즈니스 메서드 실행
  Biz->>AOP: proceed(...) 
  Biz-->>AOP: return
  AOP->>S: getRecordDetail(memberId, recordId)
  S-->>AOP: StudentRecord (type)
  AOP->>R: isFirstGeneration(recordId)
  alt first-generation
    AOP->>M: countFirstGeneration(type)
  else regeneration
    AOP->>M: countRegeneration(type)
  end
  AOP->>M: countRequest(type)
  Note over AOP: 즉시 카운팅, 트랜잭션 동기화 제거
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • TaegeunYou

Poem

토끼가 폴짝 와서 코드에 속삭여,
프록시 켜고 타입은 내가 찾아볼게,
컨트롤러는 가벼워졌고 AOP는 바로 찍어,
레디스에 첫 도약을 세어 기억하네,
깡충깡충 축하해, 햇살 속 새 흐름 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning PR 설명에는 템플릿의 Jira 섹션만 채워져 있고 "## 👩‍💻 작업 내용" 및 "## 📝 리뷰 요청 & 논의하고 싶은 내용" 등 필수 템플릿 항목이 비어 있어 변경의 세부 내용, 영향 범위, 테스트/재현 방법 등이 누락되어 있습니다. 이로 인해 리뷰어가 변경 의도와 위험도를 판단하기 어렵습니다. 템플릿의 "작업 내용" 섹션에 변경한 핵심 로직 요약(예: 메트릭 수집 방식 변경, 새 RecordGenerationTracker 추가, StudentRecordAIFacade/컨트롤러 변경 등), 영향 범위 및 마이그레이션/설정(예: Redis 키·TTL 설정) 정보를 추가하고 "리뷰 요청 & 논의하고 싶은 내용"에 검토 포인트와 테스트/재현 방법을 기재해 주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 "EDMT-452 커스텀 메트릭 정합성 문제 해결"은 PR의 핵심 작업인 메트릭 수집/정합성 수정(학생 기록 완료 메트릭 보완, RecordGenerationTracker 추가, StudentRecordMetricsService → StudentRecordMetricsCounter 변경 등)을 포괄적으로 반영하므로 변경사항과 관련성이 높습니다. 다만 PR 본문과 objectives에서는 "학생 기록 완료 메트릭 누락 문제 수정"으로 더 구체적으로 명시하고 있어 현재 제목은 다소 범용적입니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/EDMT-452

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/StudentRecordMetricsCounter.java (1)

46-53: 하드코딩된 바이트 임계값을 설정 가능하게 개선 고려

완료 판단을 위한 최소 바이트 수(1000, 750)가 하드코딩되어 있습니다. 향후 요구사항 변경에 대응하기 위해 이 값들을 설정 가능한 상수나 프로퍼티로 관리하는 것을 고려해보세요.

+    private static final int SUBJECT_MIN_BYTES = 1000;
+    private static final int DEFAULT_MIN_BYTES = 750;
+
     private boolean isCompleted(final StudentRecordType type, final String description) {
         if (description == null || description.trim().isEmpty()) {
             return false;
         }
 
-        int minBytes = (type == StudentRecordType.SUBJECT) ? 1000 : 750;
+        int minBytes = (type == StudentRecordType.SUBJECT) ? SUBJECT_MIN_BYTES : DEFAULT_MIN_BYTES;
         return description.getBytes(StandardCharsets.UTF_8).length >= minBytes;
     }
edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (2)

36-37: null 체크 필요

studentRecordService.getRecordDetail()이 null을 반환하거나 예외를 발생시킬 가능성이 있습니다. null 체크나 예외 처리를 추가하는 것을 고려해보세요.

             StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);
+            if (studentRecord == null) {
+                log.warn("StudentRecord not found for memberId: {}, recordId: {}", memberId, recordId);
+                return;
+            }
             StudentRecordType recordType = studentRecord.getStudentRecordType();

56-57: 중복된 null 체크 필요

여기서도 getRecordDetail() 결과에 대한 null 체크가 필요합니다.

             StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);
+            if (studentRecord == null) {
+                log.warn("StudentRecord not found for memberId: {}, recordId: {}", memberId, recordId);
+                return joinPoint.proceed();
+            }
             StudentRecordType recordType = studentRecord.getStudentRecordType();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 051a2ed and 2f48fe9.

📒 Files selected for processing (7)
  • edukit-api/src/main/java/com/edukit/EdukitApiApplication.java (1 hunks)
  • edukit-api/src/main/java/com/edukit/studentrecord/controller/StudentRecordAIController.java (1 hunks)
  • edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordAIFacade.java (2 hunks)
  • edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordFacade.java (0 hunks)
  • edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (3 hunks)
  • edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1 hunks)
  • edukit-core/src/main/java/com/edukit/core/studentrecord/service/StudentRecordMetricsCounter.java (1 hunks)
💤 Files with no reviewable changes (1)
  • edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordFacade.java
🧰 Additional context used
🧬 Code graph analysis (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (1)
  • Slf4j (17-83)
🔇 Additional comments (13)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/StudentRecordMetricsCounter.java (1)

11-11: 클래스명 변경 확인

StudentRecordMetricsService에서 StudentRecordMetricsCounter로 클래스명이 변경되었습니다. 이 변경사항이 올바르게 적용되었음을 확인합니다.

edukit-api/src/main/java/com/edukit/EdukitApiApplication.java (1)

7-9: AspectJ 자동 프록시 활성화 확인

@EnableAspectJAutoProxy 어노테이션이 추가되어 AOP 기반 메트릭스 수집 기능이 정상 작동할 것으로 확인됩니다.

edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (2)

8-12: 클래스명 및 어노테이션 변경 확인

GenerationTrackingService에서 RecordGenerationTracker로 클래스명이 변경되고, @Service에서 @Component로 어노테이션이 변경되었습니다. Spring 컨테이너에서 정상적으로 빈으로 관리될 것으로 확인됩니다.


16-30: 동시성 처리 적절함

compute 메서드를 사용하여 원자적으로 업데이트하고 있어 동시성 처리가 적절합니다.

edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (4)

23-25: 의존성 변경 확인

필드 타입이 올바르게 업데이트되었습니다:

  • StudentRecordMetricsServiceStudentRecordMetricsCounter
  • GenerationTrackingServiceRecordGenerationTracker
  • 새로운 StudentRecordService 추가

변경사항이 적절하게 적용되었습니다.


27-27: 포인트컷 표현식 명시화

@AfterReturning 어노테이션에 명시적으로 pointcut 속성을 지정하여 가독성이 향상되었습니다.


39-44: 메트릭 수집 실패 처리 적절함

메트릭 수집 실패 시 로그만 남기고 비즈니스 로직을 계속 진행하는 것은 적절한 처리입니다.


60-72: AI 생성 메트릭 수집 로직 적절함

첫 생성과 재생성을 구분하여 메트릭을 수집하는 로직이 명확하고 적절합니다.

edukit-api/src/main/java/com/edukit/studentrecord/controller/StudentRecordAIController.java (2)

33-33: 매개변수 포맷팅 개선

memberIdrecordId를 한 줄에 배치하여 가독성이 향상되었습니다.


36-38: Facade 메서드 호출 간소화

createTaskId 메서드 호출이 간소화되어 record type을 전달하지 않게 되었습니다. 이는 Facade에서 내부적으로 타입을 조회하도록 변경된 설계와 일치합니다.

edukit-api/src/main/java/com/edukit/studentrecord/facade/StudentRecordAIFacade.java (3)

32-37: 메서드 시그니처 변경이 적절하며 타입 안전성이 향상되었습니다.

recordType 파라미터를 제거하고 런타임에 StudentRecord에서 직접 타입을 조회하도록 변경한 것은 좋은 개선입니다. 이는 데이터 일관성을 보장하고 클라이언트에서 잘못된 타입을 전달할 가능성을 제거합니다.


31-31: AOP 메트릭 수집이 올바르게 적용되었습니다.

@AIGenerationMetrics 어노테이션이 메서드에 적절히 적용되어 있어 성능 모니터링이 가능합니다.


6-6: 권한 검증 확인 — StudentRecordService.getRecordDetail이 소유권을 검증합니다.

getRecordDetail(memberId, recordId)는 getRecordDetailById(recordId) 호출 후 validatePermission(existingDetail.getStudent(), memberId)를 호출하며, validatePermission은 student.getMember().getId()와 memberId를 비교해 불일치 시 StudentRecordErrorCode.PERMISSION_DENIED 예외를 던집니다. 위치: edukit-core/src/main/java/com/edukit/core/studentrecord/service/StudentRecordService.java (getRecordDetail / validatePermission).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (2)

23-26: null 반환에 대한 더 구체적인 예외 처리를 고려하세요.

Redis INCR 연산이 null을 반환하는 경우는 매우 예외적인 상황입니다. 현재는 warning 로그만 남기고 false를 반환하지만, 이런 상황에서는 더 적극적인 대응이 필요할 수 있습니다.

메트릭 수집의 정확성을 위해 예외를 던지거나 재시도 로직을 고려해보세요.

 if (newCount == null) {
-    log.warn("Redis INCR returned null for key: {}", key);
-    return false;
+    log.error("Redis INCR returned null for key: {}, this should not happen", key);
+    throw new IllegalStateException("Redis INCR operation failed for key: " + key);
 }

28-33: TTL 설정 실패에 대한 예외 처리가 누락되었습니다.

redisTemplate.expire(key, DEFAULT_TTL) 호출이 실패할 경우에 대한 처리가 없습니다. expire 연산 실패 시 키가 영구적으로 남아있을 수 있어 메모리 누수로 이어질 수 있습니다.

 if (newCount == 1L) {
     // 최초 생성 시에만 TTL 설정 (비정상 흐름 방치 대비)
-    redisTemplate.expire(key, DEFAULT_TTL);
+    Boolean expireResult = redisTemplate.expire(key, DEFAULT_TTL);
+    if (Boolean.FALSE.equals(expireResult)) {
+        log.warn("Failed to set TTL for key: {}", key);
+    }
     log.debug("RecordId: {}, Generation count set to 1 (first)", recordId);
     return true;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f48fe9 and beb2ff0.

📒 Files selected for processing (1)
  • edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (1)
  • Slf4j (17-83)
🔇 Additional comments (3)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (3)

1-17: 클래스 구조와 의존성이 잘 설계되었습니다.

Redis를 사용한 생성 횟수 추적 로직이 명확하고, Lombok을 활용한 의존성 주입 구조도 적절합니다. 상수 정의와 TTL 설정도 합리적입니다.


39-41: 키 생성 로직이 명확하고 적절합니다.

키 생성 방식이 일관되고 예측 가능하며, 네이밍 컨벤션도 적절합니다.


12-12: 이전 리뷰 코멘트와 관련된 메모리 누수 문제가 해결되었습니다.

Redis 기반 구현으로 변경되면서 TTL을 통해 자동 만료되므로, 이전에 지적된 ConcurrentHashMap의 무제한 증가 문제가 해결되었습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1)

39-46: Redis 실행 예외 방어 및 TTL 클램프(소프트 가드)

메트릭 경로에서 Redis 장애가 잦다면 여기서도 예외를 흡수하고 안전하게 false로 폴백하는 편이 안정적입니다. 또한 TTL을 최소 1초로 클램프해 즉시만료를 방지하세요.

적용 제안(diff):

-        Long newCount = redisTemplate.execute(
-                INCR_EXPIRE_SCRIPT,
-                Collections.singletonList(key),
-                String.valueOf(ttlSeconds)
-        );
+        Long newCount;
+        try {
+            newCount = redisTemplate.execute(
+                    INCR_EXPIRE_SCRIPT,
+                    Collections.singletonList(key),
+                    String.valueOf(Math.max(1L, ttlSeconds))
+            );
+        } catch (Exception e) {
+            log.warn("Redis script execution failed for key: {}", key, e);
+            return false;
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between beb2ff0 and c935527.

📒 Files selected for processing (1)
  • edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (1)
edukit-core/src/main/java/com/edukit/core/studentrecord/aop/StudentRecordMetricsAspect.java (1)
  • Slf4j (17-83)
🔇 Additional comments (4)
edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java (4)

27-35: Lua 스크립트/DefaultRedisScript 구성 적절

INCR+EXPIRE를 Lua로 원자화하고 ResultType을 명시한 점 좋습니다. 캐시/TTL 안정성 측면에서 이전 레이스 이슈가 해소됩니다.


39-59: 카운트 증가 시점 확인(성공 시점 기준인지 재검토 권장)

본 메서드는 호출 즉시 카운트를 증가시킵니다. 제공된 Aspect 코드에서는 proceed() 이전에 호출되어 비즈니스 실패/롤백 시에도 ‘첫 생성’으로 카운트가 소모될 수 있습니다. 의도(요청 기준 vs. 성공 기준)를 확인해 주세요. 필요 시 Aspect에서 성공 이후로 이동하거나 보상 로직을 고려하세요.


61-63: 키 스코프 확인 요청

키가 sr:gen:{recordId}:count 형태입니다. recordId가 전역 유일임을 전제합니다. 멀티 테넌시/멤버 스코프가 필요하면 키에 테넌트/멤버 식별자를 포함하는 방안을 검토해 주세요.


16-18: TTL 프로퍼티 기본값 및 유효성 추가 필요

설정 누락 시 즉시 만료(0s) 또는 예외가 발생할 수 있으니 안전한 기본값(예: 300초)과 유효성(>=1) 추가하세요.

파일: edukit-core/src/main/java/com/edukit/core/studentrecord/service/RecordGenerationTracker.java

적용 제안(diff):

-    @Value("${record.generation.ttl}")
+    @Value("${record.generation.ttl:300}")
     private long ttlSeconds;

검증: rg 실행 결과 "No files were searched"로 레포 내 설정 존재 여부를 확인하지 못했습니다. 설정이 있는지 수동 확인하거나 아래 명령으로 재검증 후 반영하세요:

rg -n --hidden --no-ignore -C2 'record\.generation\.ttl' || true

@wldks1008 wldks1008 changed the title [EDMT-452] 학생 기록 완료 메트릭 누락 문제 수정 [EDMT-452] 커스텀 메트릭 정합성 문제 해결 Sep 19, 2025
@wldks1008 wldks1008 merged commit 7e2b614 into develop Sep 19, 2025
2 checks passed
@wldks1008 wldks1008 deleted the fix/EDMT-452 branch September 19, 2025 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠️ fix 🛠️ Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant