Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions edukit-api/src/main/java/com/edukit/EdukitApiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import java.util.TimeZone;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@EnableAspectJAutoProxy
@SpringBootApplication
public class EdukitApiApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.edukit.common.EdukitResponse;
import com.edukit.common.annotation.MemberId;
import com.edukit.core.studentrecord.db.entity.StudentRecord;
import com.edukit.core.studentrecord.exception.StudentRecordErrorCode;
import com.edukit.core.studentrecord.exception.StudentRecordException;
import com.edukit.core.studentrecord.service.StudentRecordService;
Expand Down Expand Up @@ -31,15 +30,12 @@ public class StudentRecordAIController implements StudentRecordAIApi {

@PostMapping("/ai-generate/{recordId}")
public ResponseEntity<EdukitResponse<StudentRecordTaskResponse>> aiGenerateStudentRecord(
@MemberId final long memberId,
@PathVariable final long recordId,
@MemberId final long memberId, @PathVariable final long recordId,
@RequestBody @Valid final StudentRecordPromptRequest request) {
// 컨트롤러에서 직접 타입 조회 (1회 DB 조회)
StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);

// 타입과 함께 호출 (AOP에서 DB 조회 없음)
StudentRecordTaskResponse response = studentRecordAIFacade.createTaskId(memberId, recordId,
studentRecord.getStudentRecordType(), request.byteCount(), request.prompt());
request.byteCount(), request.prompt()
);
return ResponseEntity.ok(EdukitResponse.success(response));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import com.edukit.common.annotation.AIGenerationMetrics;
import com.edukit.core.member.db.entity.Member;
import com.edukit.core.member.service.MemberService;
import com.edukit.core.studentrecord.db.entity.StudentRecord;
import com.edukit.core.studentrecord.db.entity.StudentRecordAITask;
import com.edukit.core.studentrecord.db.enums.StudentRecordType;
import com.edukit.core.studentrecord.service.AITaskService;
import com.edukit.core.studentrecord.service.SSEChannelManager;
import com.edukit.core.studentrecord.service.StudentRecordService;
import com.edukit.core.studentrecord.util.AIPromptGenerator;
import com.edukit.studentrecord.event.AITaskCreateEvent;
import com.edukit.studentrecord.facade.response.StudentRecordTaskResponse;
Expand All @@ -22,17 +23,18 @@ public class StudentRecordAIFacade {

private final MemberService memberService;
private final AITaskService aiTaskService;
private final StudentRecordService studentRecordService;
private final SSEChannelManager sseChannelManager;
private final ApplicationEventPublisher eventPublisher;

@Transactional
@AIGenerationMetrics
public StudentRecordTaskResponse createTaskId(final long memberId, final long recordId,
final StudentRecordType recordType, final int byteCount,
public StudentRecordTaskResponse createTaskId(final long memberId, final long recordId, final int byteCount,
final String userPrompt) {
Member member = memberService.getMemberById(memberId);
StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);
Comment thread
wldks1008 marked this conversation as resolved.

String requestPrompt = AIPromptGenerator.createStreamingPrompt(recordType, byteCount, userPrompt);
String requestPrompt = AIPromptGenerator.createStreamingPrompt(studentRecord.getStudentRecordType(), byteCount, userPrompt);
StudentRecordAITask task = aiTaskService.createAITask(member, userPrompt);

eventPublisher.publishEvent(AITaskCreateEvent.of(task, userPrompt, requestPrompt, byteCount));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public void updateStudentRecord(final long memberId, final long recordId, final
studentRecordService.updateStudentRecord(studentRecord, description);
}


@Transactional(readOnly = true)
public StudentRecordDetailResponse getStudentRecord(final long memberId, final long recordId) {
StudentRecord recordDetail = studentRecordService.getRecordDetail(memberId, recordId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.edukit.core.studentrecord.aop;

import com.edukit.core.studentrecord.db.entity.StudentRecord;
import com.edukit.core.studentrecord.db.enums.StudentRecordType;
import com.edukit.core.studentrecord.service.GenerationTrackingService;
import com.edukit.core.studentrecord.service.StudentRecordMetricsService;
import com.edukit.core.studentrecord.service.RecordGenerationTracker;
import com.edukit.core.studentrecord.service.StudentRecordMetricsCounter;
import com.edukit.core.studentrecord.service.StudentRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
Expand All @@ -11,63 +13,34 @@
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class StudentRecordMetricsAspect {

private final StudentRecordMetricsService metricsService;
private final GenerationTrackingService generationTrackingService;
private final StudentRecordMetricsCounter metricsService;
private final RecordGenerationTracker recordGenerationTracker;
private final StudentRecordService studentRecordService;

@AfterReturning("@annotation(com.edukit.common.annotation.StudentRecordMetrics)")
@AfterReturning(pointcut = "@annotation(com.edukit.common.annotation.StudentRecordMetrics)")
public void collectCompletionMetrics(final JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();

if (args.length >= 4) {
if (args.length == 3) {
long memberId = (Long) args[0];
long recordId = (Long) args[1];
StudentRecordType recordType = (StudentRecordType) args[2];
String description = (String) args[3];
String description = (String) args[2];

try {
// 트랜잭션 커밋 후에만 메트릭 수집 및 정리 작업 수행
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
try {
metricsService.recordCompletion(recordType, description);

// 커밋 성공 후 생성 추적 정보 정리 (메모리 절약)
generationTrackingService.clearRecord(recordId);

log.debug("Completion metrics recorded after transaction commit for recordId: {}", recordId);

} catch (Exception e) {
log.warn("Error collecting completion metrics after commit for recordId: {}", recordId, e);
}
}

@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
// 롤백 시에도 생성 추적 정보 정리 (메모리 누수 방지)
generationTrackingService.clearRecord(recordId);
log.debug("Transaction rolled back - cleared generation tracking for recordId: {}", recordId);
}
}
});
} else {
metricsService.recordCompletion(recordType, description);
generationTrackingService.clearRecord(recordId);
log.warn("No transaction synchronization - recording metrics immediately for recordId: {}", recordId);
}
StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);
StudentRecordType recordType = studentRecord.getStudentRecordType();

try {
metricsService.recordCompletion(recordType, description);
} catch (Exception e) {
log.warn("Error setting up completion metrics collection", e);
// 메트릭 수집 실패는 로그만 남기고 비즈니스 로직은 계속 진행
log.warn("Error collecting completion metrics for recordType: {}", recordType, e);
}
}
}
Expand All @@ -76,12 +49,15 @@ public void afterCompletion(int status) {
public Object collectAIGenerationMetrics(final ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();

if (args.length >= 3) {
if (args.length == 4) {
long memberId = (Long) args[0];
long recordId = (Long) args[1];
StudentRecordType recordType = (StudentRecordType) args[2];

StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId);
StudentRecordType recordType = studentRecord.getStudentRecordType();

try {
boolean isFirstGeneration = generationTrackingService.isFirstGeneration(recordId);
boolean isFirstGeneration = recordGenerationTracker.isFirstGeneration(recordId);

// 전체 AI 생성 요청 카운트
metricsService.recordAIGenerationRequest(recordType);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.edukit.core.studentrecord.service;

import java.util.Collections;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class RecordGenerationTracker {

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

private static final String KEY_PREFIX = "sr:gen:";
private static final String LUA_SCRIPT =
"local ttl = tonumber(ARGV[1]) or 0 " +
"local c = redis.call('INCR', KEYS[1]) " +
"if c == 1 and ttl > 0 then " +
" redis.call('EXPIRE', KEYS[1], ttl) " +
"end " +
"return c";

private static final DefaultRedisScript<Long> INCR_EXPIRE_SCRIPT;


static {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(LUA_SCRIPT);
script.setResultType(Long.class);
INCR_EXPIRE_SCRIPT = script;
}

private final StringRedisTemplate redisTemplate;

public boolean isFirstGeneration(long recordId) {
String key = getCountKey(recordId);
Long newCount = redisTemplate.execute(
INCR_EXPIRE_SCRIPT,
Collections.singletonList(key),
String.valueOf(ttlSeconds)
);

if (newCount == null) {
log.warn("Redis script returned null for key: {}", key);
return false;
}

boolean isFirst = newCount == 1L;
if (isFirst) {
log.debug("RecordId: {}, Generation count set to 1 (first)", recordId);
} else {
log.debug("RecordId: {}, Generation count: {} (regeneration)", recordId, newCount);
}
return isFirst;
}

private String getCountKey(long recordId) {
return KEY_PREFIX + recordId + ":count";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Component
@RequiredArgsConstructor
public class StudentRecordMetricsService {
public class StudentRecordMetricsCounter {

private final MeterRegistry meterRegistry;

Expand Down