AI 피드백 생성 비동기 처리 중 Select 에러 해결
Hey guys! 백엔드 개발하면서 AI 피드백 생성 기능 구현하다가 겪었던 비동기 처리 문제와 해결 과정을 공유하려고 해. 특히, AI 피드백 생성 요청 후 데이터베이스에 저장하기 전에 데이터를 조회하려고 할 때 발생하는 문제에 대해 자세히 알아볼 거야. Let's dive in!
문제 상황
AI 피드백 생성 기능을 구현하면서 다음과 같은 로그를 발견했어:
00:19:35.541 [http-nio-8080-exec-4] INFO s.s.d.p.c.AiFeedbackController - AI 피드백 생성 요청 (Post ID: 32)
00:19:35.603 [http-nio-8080-exec-4] DEBUG o.s.w.s.m.m.a.HttpEntityMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/yaml]
00:19:35.603 [http-nio-8080-exec-4] DEBUG o.s.w.s.m.m.a.HttpEntityMethodProcessor - Writing [swyp_11.ssubom.global.response.ApiResponse@41088c37]
00:19:35.600 [task-1] ERROR o.s.a.i.SimpleAsyncUncaughtExceptionHandler - Unexpected exception occurred invoking async method: public void swyp_11.ssubom.domain.post.service.AsyncFeedbackGenerator.generateAndSaveFeedback(java.lang.Long,java.lang.String)
swyp_11.ssubom.global.error.BusinessException: AIfeedback을 찾을 수 없습니다.
at swyp_11.ssubom.domain.post.service.AsyncFeedbackGenerator.lambda$generateAndSaveFeedback$0(AsyncFeedbackGenerator.java:26)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at swyp_11.ssubom.domain.post.service.AsyncFeedbackGenerator.generateAndSaveFeedback(AsyncFeedbackGenerator.java:26)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
00:19:35.608 [http-nio-8080-exec-4] DEBUG o.s.web.servlet.DispatcherServlet - Completed 202 ACCEPTED
00:19:46.240 [http-nio-8080-exec-5] DEBUG o.s.security.web.FilterChainProxy - Securing GET /api/posts/32/ai-feedback/14
이 로그를 보면, AI 피드백 생성 요청(Post ID: 32)이 들어온 후, 비동기 작업(task-1)에서 AIfeedback을 찾을 수 없습니다라는 에러가 발생했어. 문제는 비동기적으로 피드백을 생성하고 저장하기 전에, 피드백을 조회하는 로직이 먼저 실행되었다는 점이야. 즉, Insert 작업이 완료되기 전에 Select 작업이 먼저 실행된 거지. This can be a real head-scratcher, but don't worry, we'll figure it out.
문제 원인 분석
AI 피드백 생성 로직이 비동기적으로 처리되면서, 메인 스레드에서는 피드백 생성을 요청한 후 바로 응답을 반환해. 비동기 작업은 별도의 스레드에서 실행되는데, 이 스레드에서 AI 피드백을 생성하고 데이터베이스에 저장하는 데 시간이 걸릴 수 있어. 문제는 피드백을 생성하고 저장하는 동안, 다른 스레드에서 해당 피드백을 조회하려고 시도하면 아직 데이터가 없기 때문에 AIfeedback을 찾을 수 없습니다 에러가 발생하는 거야. This is a classic race condition scenario.
이러한 문제는 다음과 같은 상황에서 발생할 가능성이 높아:
- 비동기 작업의 처리 속도가 예상보다 느릴 때: 외부 API 호출이나 복잡한 계산으로 인해 피드백 생성 시간이 오래 걸리는 경우.
- 데이터베이스 쓰기 작업의 지연: 데이터베이스 서버의 부하가 높거나 네트워크 문제로 인해 쓰기 작업이 지연되는 경우.
- 잘못된 트랜잭션 관리: 트랜잭션 격리 수준이 낮거나, 트랜잭션 범위가 잘못 설정된 경우.
해결 방법
이 문제를 해결하기 위해 다음과 같은 방법을 고려할 수 있어.
1. 동기 방식으로 변경
가장 간단한 해결 방법은 AI 피드백 생성 로직을 동기적으로 처리하는 거야. 즉, 피드백 생성 요청을 받으면 피드백을 생성하고 데이터베이스에 저장한 후 응답을 반환하는 거지. 이렇게 하면 피드백이 반드시 데이터베이스에 저장된 후에 조회가 이루어지므로, AIfeedback을 찾을 수 없습니다 에러를 방지할 수 있어. However, this approach can increase the response time and reduce the overall throughput of your application. No one likes a slow app!
@Service
public class FeedbackService {
@Autowired
private FeedbackRepository feedbackRepository;
public Feedback generateAndSaveFeedback(Long postId, String content) {
// AI 피드백 생성 로직
String aiFeedback = generateAiFeedback(content);
// 피드백 저장
Feedback feedback = new Feedback();
feedback.setPostId(postId);
feedback.setContent(aiFeedback);
feedbackRepository.save(feedback);
return feedback;
}
private String generateAiFeedback(String content) {
// AI 피드백 생성 API 호출 또는 로직
return "Generated AI feedback for content: " + content;
}
}
2. 비동기 처리 유지 및 데이터 동기화 보장
비동기 처리의 장점을 유지하면서 데이터 동기화를 보장하는 방법도 있어. 이 방법은 다음과 같은 단계를 포함해:
- 피드백 생성 완료 이벤트 발행: AI 피드백이 성공적으로 생성되고 데이터베이스에 저장되면, 피드백 생성 완료 이벤트를 발행해.
- 이벤트 리스너 구현: 피드백 생성 완료 이벤트를 수신하는 이벤트 리스너를 구현해. 이 리스너는 피드백 조회를 요청한 스레드에 피드백이 준비되었다는 신호를 보내.
- 신호 대기: 피드백 조회를 요청한 스레드는 피드백 생성 완료 신호를 기다려. 신호를 받으면 데이터베이스에서 피드백을 조회해.
이 방법을 사용하면 비동기 처리의 장점을 유지하면서 데이터 정합성을 보장할 수 있어. Here's how you can achieve this using Spring's @Async and event publishing:
@Service
public class AsyncFeedbackGenerator {
@Autowired
private FeedbackRepository feedbackRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Async
public void generateAndSaveFeedback(Long postId, String content) {
// AI 피드백 생성 로직
String aiFeedback = generateAiFeedback(content);
// 피드백 저장
Feedback feedback = new Feedback();
feedback.setPostId(postId);
feedback.setContent(aiFeedback);
feedbackRepository.save(feedback);
// 피드백 생성 완료 이벤트 발행
eventPublisher.publishEvent(new FeedbackGeneratedEvent(feedback.getId()));
}
private String generateAiFeedback(String content) {
// AI 피드백 생성 API 호출 또는 로직
return "Generated AI feedback for content: " + content;
}
}
@Component
public class FeedbackGeneratedEventListener {
@EventListener
public void handleFeedbackGeneratedEvent(FeedbackGeneratedEvent event) {
Long feedbackId = event.getFeedbackId();
// 피드백 ID를 사용하여 필요한 작업 수행 (예: 캐시 업데이트, 알림 전송 등)
System.out.println("Feedback generated with ID: " + feedbackId);
}
}
public class FeedbackGeneratedEvent extends ApplicationEvent {
private Long feedbackId;
public FeedbackGeneratedEvent(Long feedbackId) {
super(feedbackId);
this.feedbackId = feedbackId;
}
public Long getFeedbackId() {
return feedbackId;
}
}
3. 재시도 로직 구현
AI 피드백을 찾을 수 없을 때, 일정 시간 간격으로 재시도하는 로직을 구현할 수도 있어. 예를 들어, 피드백을 조회하는 API에서 AIfeedback을 찾을 수 없습니다 에러가 발생하면, 1초 후에 다시 피드백을 조회하는 거지. 이 과정을 몇 번 반복하다가, 최대 재시도 횟수를 초과하면 에러를 반환하는 거야. This is a simple retry mechanism that can handle transient issues.
@Service
public class FeedbackService {
@Autowired
private FeedbackRepository feedbackRepository;
public Feedback getFeedbackWithRetry(Long feedbackId, int maxRetries, int delayMillis) throws InterruptedException {
int retries = 0;
while (retries < maxRetries) {
try {
Optional<Feedback> feedback = feedbackRepository.findById(feedbackId);
return feedback.orElseThrow(() -> new BusinessException("AIfeedback을 찾을 수 없습니다."));
} catch (BusinessException e) {
retries++;
Thread.sleep(delayMillis);
}
}
throw new BusinessException("AIfeedback을 찾을 수 없습니다 (최대 재시도 횟수 초과).");
}
}
4. 낙관적 락(Optimistic Lock) 또는 비관적 락(Pessimistic Lock) 사용
데이터베이스 레벨에서 락을 사용하여 데이터의 정합성을 보장할 수도 있어. 낙관적 락은 데이터를 업데이트하기 전에 데이터의 버전을 확인하고, 업데이트 시점에 버전이 변경되었으면 업데이트를 실패시키는 방식이야. 비관적 락은 데이터를 읽기 전에 락을 걸고, 락을 해제할 때까지 다른 트랜잭션에서 해당 데이터를 수정할 수 없도록 하는 방식이지. Choosing the right locking strategy depends on your specific use case and concurrency requirements.
결론
AI 피드백 생성 시 발생하는 비동기 문제는 여러 가지 방법으로 해결할 수 있어. 동기 방식으로 변경하거나, 비동기 처리를 유지하면서 데이터 동기화를 보장하거나, 재시도 로직을 구현하거나, 락을 사용하는 등 다양한 방법을 상황에 맞게 선택할 수 있지. 중요한 것은 문제의 원인을 정확히 파악하고, 각 해결 방법의 장단점을 고려하여 최적의 방법을 선택하는 거야. Hope this helps you tackle similar issues in your backend development adventures! Keep coding, and stay awesome!