Skip to content

Commit d0bf078

Browse files
[FIX] 시나리오 생성시 response버그문제해결 (#134)
1 parent a73ff8c commit d0bf078

File tree

3 files changed

+108
-206
lines changed

3 files changed

+108
-206
lines changed

back/src/main/java/com/back/domain/scenario/service/ScenarioService.java

Lines changed: 4 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
import com.back.domain.scenario.repository.ScenarioRepository;
1515
import com.back.domain.scenario.repository.SceneCompareRepository;
1616
import com.back.domain.scenario.repository.SceneTypeRepository;
17-
import com.back.global.ai.dto.result.BaseScenarioResult;
18-
import com.back.global.ai.dto.result.DecisionScenarioResult;
19-
import com.back.global.ai.service.AiService;
2017
import com.back.global.common.PageResponse;
2118
import com.back.global.exception.ApiException;
2219
import com.back.global.exception.ErrorCode;
@@ -29,7 +26,6 @@
2926
import org.springframework.dao.DataIntegrityViolationException;
3027
import org.springframework.data.domain.Page;
3128
import org.springframework.data.domain.Pageable;
32-
import org.springframework.scheduling.annotation.Async;
3329
import org.springframework.stereotype.Service;
3430
import org.springframework.transaction.annotation.Transactional;
3531

@@ -60,9 +56,6 @@ public class ScenarioService {
6056
// Object Mapper 주입
6157
private final ObjectMapper objectMapper;
6258

63-
// AI Service 주입
64-
private final AiService aiService;
65-
6659
// Scenario Transaction Service 주입
6760
private final ScenarioTransactionService scenarioTransactionService;
6861

@@ -92,8 +85,8 @@ public ScenarioStatusResponse createScenario(Long userId,
9285
validationResult.decisionLine
9386
);
9487

95-
// 3. 비동기 AI 처리 트리거 (트랜잭션 외부)
96-
processScenarioGenerationAsync(scenarioId);
88+
// 3. 비동기 AI 처리 트리거 (트랜잭션 외부, 별도 Bean에서 호출)
89+
scenarioTransactionService.processScenarioGenerationAsync(scenarioId);
9790

9891
return new ScenarioStatusResponse(
9992
scenarioId,
@@ -278,8 +271,8 @@ private ScenarioStatusResponse handleFailedScenarioRetry(Scenario failedScenario
278271
// 1. 상태 업데이트 (트랜잭션)
279272
Long scenarioId = retryScenarioInTransaction(failedScenario.getId());
280273

281-
// 2. 비동기 AI 처리 트리거 (트랜잭션 외부)
282-
processScenarioGenerationAsync(scenarioId);
274+
// 2. 비동기 AI 처리 트리거 (트랜잭션 외부, 별도 Bean에서 호출)
275+
scenarioTransactionService.processScenarioGenerationAsync(scenarioId);
283276

284277
return new ScenarioStatusResponse(
285278
scenarioId,
@@ -305,91 +298,6 @@ protected Long retryScenarioInTransaction(Long scenarioId) {
305298
return scenario.getId();
306299
}
307300

308-
// 비동기 방식으로 AI 시나리오 생성
309-
@Async("aiTaskExecutor")
310-
public void processScenarioGenerationAsync(Long scenarioId) {
311-
try {
312-
// 1. 상태를 PROCESSING으로 업데이트 (별도 트랜잭션)
313-
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.PROCESSING, null);
314-
315-
// 2. AI 생성에 필요한 모든 데이터를 트랜잭션 내에서 미리 로드
316-
Scenario scenarioWithData = scenarioTransactionService.prepareScenarioData(scenarioId);
317-
318-
// 3. AI 시나리오 생성 (트랜잭션 외부에서 실행)
319-
AiScenarioGenerationResult result = executeAiGeneration(scenarioWithData);
320-
321-
// 4. 결과 저장 및 완료 상태 업데이트 (별도 트랜잭션)
322-
scenarioTransactionService.saveAiResult(scenarioId, result);
323-
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.COMPLETED, null);
324-
325-
log.info("Scenario generation completed successfully for ID: {}", scenarioId);
326-
327-
} catch (Exception e) {
328-
// 5. 실패 상태 업데이트 (별도 트랜잭션)
329-
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.FAILED,
330-
"시나리오 생성 실패: " + e.getMessage());
331-
log.error("Scenario generation failed for ID: {}, error: {}",
332-
scenarioId, e.getMessage(), e);
333-
}
334-
}
335-
336-
// AI 호출 전용 메서드 (트랜잭션 없음)
337-
private AiScenarioGenerationResult executeAiGeneration(Scenario scenario) {
338-
// AI 호출 로직 (미리 로드된 데이터 사용)
339-
DecisionLine decisionLine = scenario.getDecisionLine();
340-
BaseLine baseLine = decisionLine.getBaseLine();
341-
342-
// 베이스 시나리오 확보
343-
Scenario baseScenario = ensureBaseScenarioExists(baseLine);
344-
345-
// AI 호출 (트랜잭션 외부) with 타임아웃 (60초)
346-
DecisionScenarioResult aiResult = aiService
347-
.generateDecisionScenario(decisionLine, baseScenario)
348-
.orTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
349-
.exceptionally(ex -> {
350-
log.error("Decision scenario generation timeout or error for scenario ID: {}", scenario.getId(), ex);
351-
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT, "시나리오 생성 시간 초과 (60초)");
352-
})
353-
.join();
354-
355-
return new AiScenarioGenerationResult(aiResult);
356-
}
357-
358-
// 베이스 시나리오 확보 (없으면 생성)
359-
private Scenario ensureBaseScenarioExists(BaseLine baseLine) {
360-
return scenarioRepository.findByBaseLineIdAndDecisionLineIsNull(baseLine.getId())
361-
.orElseGet(() -> createBaseScenario(baseLine));
362-
}
363-
364-
// 베이스 시나리오 생성
365-
private Scenario createBaseScenario(BaseLine baseLine) {
366-
log.info("Creating base scenario for BaseLine ID: {}", baseLine.getId());
367-
368-
// 1. AI 호출 with 타임아웃 (180초 - 테스트용)
369-
BaseScenarioResult aiResult = aiService.generateBaseScenario(baseLine)
370-
.orTimeout(180, java.util.concurrent.TimeUnit.SECONDS)
371-
.exceptionally(ex -> {
372-
log.error("Base scenario generation timeout or error for BaseLine ID: {}", baseLine.getId(), ex);
373-
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT, "베이스 시나리오 생성 시간 초과 (180초)");
374-
})
375-
.join();
376-
377-
// 2. 베이스 시나리오 엔티티 생성
378-
Scenario baseScenario = Scenario.builder()
379-
.user(baseLine.getUser())
380-
.decisionLine(null) // 베이스 시나리오는 DecisionLine 없음
381-
.baseLine(baseLine) // 베이스 시나리오는 BaseLine 연결
382-
.status(ScenarioStatus.COMPLETED) // 베이스는 바로 완료
383-
.build();
384-
385-
Scenario savedScenario = scenarioRepository.save(baseScenario);
386-
387-
// 3. AI 결과 적용
388-
scenarioTransactionService.applyBaseScenarioResult(savedScenario, aiResult);
389-
390-
return savedScenario;
391-
}
392-
393301
// 시나리오 생성 상태 조회
394302
@Transactional(readOnly = true)
395303
public ScenarioStatusResponse getScenarioStatus(Long scenarioId, Long userId) {

back/src/main/java/com/back/domain/scenario/service/ScenarioTransactionService.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.fasterxml.jackson.databind.ObjectMapper;
1818
import lombok.RequiredArgsConstructor;
1919
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.scheduling.annotation.Async;
2021
import org.springframework.stereotype.Service;
2122
import org.springframework.transaction.annotation.Propagation;
2223
import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +47,8 @@ public class ScenarioTransactionService {
4647
private final AiService aiService;
4748
private final ObjectMapper objectMapper;
4849
private final com.back.global.ai.config.ImageAiConfig imageAiConfig;
50+
private final com.back.global.ai.config.DecisionScenarioAiProperties decisionScenarioAiProperties;
51+
private final com.back.global.ai.config.BaseScenarioAiProperties baseScenarioAiProperties;
4952

5053
// 상태 업데이트 전용 트랜잭션 메서드
5154
@Transactional(propagation = Propagation.REQUIRES_NEW)
@@ -268,4 +271,101 @@ public Scenario prepareScenarioData(Long scenarioId) {
268271

269272
return scenario;
270273
}
274+
275+
/**
276+
* 비동기 방식으로 AI 시나리오 생성.
277+
* ScenarioService의 self-invocation 문제를 해결하기 위해 별도 Bean으로 분리.
278+
*/
279+
@org.springframework.scheduling.annotation.Async("aiTaskExecutor")
280+
public void processScenarioGenerationAsync(Long scenarioId) {
281+
try {
282+
// 1. 상태를 PROCESSING으로 업데이트 (별도 트랜잭션)
283+
updateScenarioStatus(scenarioId, ScenarioStatus.PROCESSING, null);
284+
285+
// 2. AI 생성에 필요한 모든 데이터를 트랜잭션 내에서 미리 로드
286+
Scenario scenarioWithData = prepareScenarioData(scenarioId);
287+
288+
// 3. AI 시나리오 생성 (트랜잭션 외부에서 실행)
289+
AiScenarioGenerationResult result = executeAiGeneration(scenarioWithData);
290+
291+
// 4. 결과 저장 및 완료 상태 업데이트 (별도 트랜잭션)
292+
saveAiResult(scenarioId, result);
293+
updateScenarioStatus(scenarioId, ScenarioStatus.COMPLETED, null);
294+
295+
log.info("Scenario generation completed successfully for ID: {}", scenarioId);
296+
297+
} catch (Exception e) {
298+
// 5. 실패 상태 업데이트 (별도 트랜잭션)
299+
updateScenarioStatus(scenarioId, ScenarioStatus.FAILED,
300+
"시나리오 생성 실패: " + e.getMessage());
301+
log.error("Scenario generation failed for ID: {}, error: {}",
302+
scenarioId, e.getMessage(), e);
303+
}
304+
}
305+
306+
/**
307+
* AI 호출 전용 메서드 (트랜잭션 없음).
308+
* 미리 로드된 데이터를 사용하여 AI 시나리오를 생성한다.
309+
*/
310+
private AiScenarioGenerationResult executeAiGeneration(Scenario scenario) {
311+
// AI 호출 로직 (미리 로드된 데이터 사용)
312+
com.back.domain.node.entity.DecisionLine decisionLine = scenario.getDecisionLine();
313+
BaseLine baseLine = decisionLine.getBaseLine();
314+
315+
// 베이스 시나리오 확보
316+
Scenario baseScenario = ensureBaseScenarioExists(baseLine);
317+
318+
// AI 호출 (트랜잭션 외부) with 타임아웃
319+
DecisionScenarioResult aiResult = aiService
320+
.generateDecisionScenario(decisionLine, baseScenario)
321+
.orTimeout(decisionScenarioAiProperties.getTimeoutSeconds(), java.util.concurrent.TimeUnit.SECONDS)
322+
.exceptionally(ex -> {
323+
log.error("Decision scenario generation timeout or error for scenario ID: {}", scenario.getId(), ex);
324+
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT,
325+
"시나리오 생성 시간 초과 (" + decisionScenarioAiProperties.getTimeoutSeconds() + "초)");
326+
})
327+
.join();
328+
329+
return new AiScenarioGenerationResult(aiResult);
330+
}
331+
332+
/**
333+
* 베이스 시나리오 확보 (없으면 생성).
334+
*/
335+
private Scenario ensureBaseScenarioExists(BaseLine baseLine) {
336+
return scenarioRepository.findByBaseLineIdAndDecisionLineIsNull(baseLine.getId())
337+
.orElseGet(() -> createBaseScenario(baseLine));
338+
}
339+
340+
/**
341+
* 베이스 시나리오 생성.
342+
*/
343+
private Scenario createBaseScenario(BaseLine baseLine) {
344+
log.info("Creating base scenario for BaseLine ID: {}", baseLine.getId());
345+
346+
// 1. AI 호출 with 타임아웃
347+
BaseScenarioResult aiResult = aiService.generateBaseScenario(baseLine)
348+
.orTimeout(baseScenarioAiProperties.getTimeoutSeconds(), java.util.concurrent.TimeUnit.SECONDS)
349+
.exceptionally(ex -> {
350+
log.error("Base scenario generation timeout or error for BaseLine ID: {}", baseLine.getId(), ex);
351+
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT,
352+
"베이스 시나리오 생성 시간 초과 (" + baseScenarioAiProperties.getTimeoutSeconds() + "초)");
353+
})
354+
.join();
355+
356+
// 2. 베이스 시나리오 엔티티 생성
357+
Scenario baseScenario = Scenario.builder()
358+
.user(baseLine.getUser())
359+
.decisionLine(null) // 베이스 시나리오는 DecisionLine 없음
360+
.baseLine(baseLine) // 베이스 시나리오는 BaseLine 연결
361+
.status(ScenarioStatus.COMPLETED) // 베이스는 바로 완료
362+
.build();
363+
364+
Scenario savedScenario = scenarioRepository.save(baseScenario);
365+
366+
// 3. AI 결과 적용
367+
applyBaseScenarioResult(savedScenario, aiResult);
368+
369+
return savedScenario;
370+
}
271371
}

0 commit comments

Comments
 (0)