Post

외부 API 트랜잭션 밖으로 분리하기

🎬 Intro

외부 API 호출을 트랜잭션 밖으로 분리하는 과정을 다룹니다.

결론

외부 API 호출 로직을 트랜잭션 밖으로 분리하여, 불필요한 DB 커넥션 점유를 제거하였습니다.

현재 상황

  • AI 클러스터링을 위한 백터 계산 API가 트랜잭션안에서 실행

현재 상황이 왜 문제가 되는가?

벡터 계산 API 호출이 2초 정도 소요되기 때문에 이를 트랜잭션 내부에서 사용한다면, DB커넥션과 스레드 고갈 문제의 위험이 있습니다.

개선 과정

✅ 벡터 계산 API 트랜잭션 외부로 분리

벡터 계산 API 구조
1
2
3
4
5
@FunctionalInterface
public interface EmbeddingExtractor {

    double[] extract(final String text);
}
1
2
3
4
5
6
7
8
9
10
11
12
@Primary
@Component
@RequiredArgsConstructor
public class VoyageAIEmbeddingExtractorAdapter implements EmbeddingExtractor {

    private final VoyageAIEmbeddingClient voyageAIEmbeddingClient;

    @Override
    public double[] extract(final String text) {
        return voyageAIEmbeddingClient.extractEmbedding(text);
    }
}

위와 같이 클러스터링에 필요한 벡터 계산 API 호출 로직이 추상화가 되어 있습니다.

벡터 계산 API 호출 로직
Before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
   @Transactional
    public FeedbackEmbeddingCluster cluster(final Long createdFeedbackId) {
        final Feedback createdFeedback = getFeedback(createdFeedbackId);
        if (feedbackEmbeddingClusterRepository.existsByFeedback(createdFeedback)) {
            throw new AlreadyClusteringException("이미 클러스터링 된 피드백입니다. (feedabckId = " + createdFeedbackId + ")");
        }
        
        // 외부 API 호출
        final double[] createdFeedbackEmbedding = embeddingExtractor.extract(createdFeedback.getContent().getValue());
        
        //...

        return feedbackEmbeddingClusterRepository.save(assignedCluster.get());
    }

이처럼 트랜잭션 내부에서 외부 API가 호출은 다음과 같은 문제가 있습니다.

  • 외부 API 완료 전까지 스레드 블로킹
  • 해당 스레드가 DB 커넥션 점유

이는 곧 자원 고갈 문제로 직결 되기 때문에 반드시 개선이 필요한 지점 입니다. 이를 다음과 같이 수정해보겠습니다.

After
1
2
3
4
5
6
7
8
9
10
11
@Service
@RequiredArgsConstructor
public class EmbeddingService {

    private final EmbeddingExtractor embeddingExtractor;

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public double[] extractEmbedding(final String content) {
        return embeddingExtractor.extract(content);
    }
}
  • 트랜잭션 AOP 프록시를 적용을 위해 클래스 분리
  • 트랜잭션 전파 속성을 NOT_SUPPORTED하여 트랜잭션 사용을 방지
1
2
3
4
5
6
7
8
9
10
11
12
13
   @Transactional
    public FeedbackEmbeddingCluster cluster(final Long createdFeedbackId) {
        final Feedback createdFeedback = getFeedback(createdFeedbackId);
        if (feedbackEmbeddingClusterRepository.existsByFeedback(createdFeedback)) {
            throw new AlreadyClusteringException("이미 클러스터링 된 피드백입니다. (feedabckId = " + createdFeedbackId + ")");
        }
        // 외부 API 호출
        final double[] createdFeedbackEmbedding = embeddingService.extractEmbedding(createdFeedback.getContent().getValue());
        
        //...
  
        return feedbackEmbeddingClusterRepository.save(assignedCluster.get());
    }

위와 같이 별도 클래스로 분리한 EmbeddingService을 주입 받아 사용하면 외부 API 호출이 트랜잭션 외부로 완전히 분리됩니다.

이를 통해 불필요한 DB 커넥션 점유는 해결 됐습니다. 하지만 스레드의 경우 블로킹 상태로 외부 API 작업 완료를 기다리게 됩니다.

이는 스케줄러 + 배치처리를 통해 개선 가능한 포인트 입니다. 사용자 요청 시점에 즉시 임베딩을 생성하는 대신, 일정 주기로 모아서 처리하면 스레드 블로킹과 병목을 제거할 수 있습니다. 이에 대한 자세한 내용은 아래 포스팅에서 다루겠습니다.

배치처리를 이용한 클러스터링 최적화

This post is licensed under CC BY 4.0 by the author.