스프링

트랜잭션 내부에서 외부 api 호출하지 말자

장몽이 2025. 2. 3.
    // CongressmanService
    @Transactional
    public NewsListDTO findNewsList(String encryptedCongressmanId, Pageable pageable) {
    
        final Long congressmanId = aesUtil.decrypt(encryptedCongressmanId);
        final Congressman congressman = findById(congressmanId);
        
        // 외부 api 호출
        final String response = getApiResponse(buildUrlStringForNews(pageable, congressman));
        
        return parseNewsResponse(response, pageable.getPageSize());
    }

CongressmanService의 메서드인 findNewsList에서 국회 api를 호출하는 예시이다. 

트랜잭션 내부에서 외부 api를 호출하고 있다. 

하지만 트랜잭션 내부에서 외부 api를 호출하면 이러한 문제가 발생할 수 있다. 

  • Tomcat Thread Pool 고갈: Tomcat의 최대 쓰레드 수가 100개로 설정되어 있다면, 60초 동안 응답을 기다리는 동안 대부분의 쓰레드가 차단되어 추가적인 요청을 처리할 수 없게 만든다.
  • DB Connection Pool 고갈: HikariCP의 커넥션 풀 갯수가 50개로 설정되어 있다면, 트랜잭션이 유지되는 동안 커넥션이 반환되지 않아 커넥션 풀이 고갈될 수 있으며 장애로 이어진다.

따라서 외부 api 호출은 CongressmanApiClient로 이동해준다. 

    // CongressmanApiClient
    @Transactional(propagation = Propagation.NEVER)
    public String getApiResponse(final Pageable pageable, final String baseUrl, final String apiKey, final Map<String, String> params) {
        final String uriString = buildUrlString(pageable, baseUrl, apiKey, params);

	// 외부 api 호출
        return webClient.get()
                .uri(uriString)
                .retrieve()
                .bodyToMono(String.class)
                .doOnNext(this::logResponse) 
                .block();
    }

이 때  @Transactional(propagation = Propagation.NEVER 를 달아주면 트랜잭션이 존재하는 상황에서 getApiResponse를 호출할 경우 IllegalTransactionStateException 가 발생한다.

따라서  getApiResponse가 트랜잭션 외부에서 호출됨을 보장할 수 있다. 

    // CongressmanApiService 
    public NewsListDTO findNewsList(final String encryptedCongressmanId, final Pageable pageable) {
        // congressmanService : 트랜잭션 
        Congressman congressman = congressmanService.getCongressman(encryptedCongressmanId);

        Map<String, String> params = Map.of(COMP_MAIN_TITLE, congressman.getName());
        // congressmanApiClient : 트랜잭션 없이 외부 api 호출
        JsonNode jsonNode = fetchApiResponse(pageable, API_NEWS_URL, newsApikey, params);

        int totalPage = calculateTotalPages(extractTotalCount(jsonNode, NEWS_API_PATH), pageable.getPageSize());
        return NewsListDTO.of(getContent(jsonNode, NEWS_API_PATH), totalPage);
    }

CongressmanApiService에서 CongressmanService와 CongressmanApiClient에 의존해 트랜잭션 내부의 작업, 외부의 api 호출을 각각 해준다. 컨트롤러에서는 CongressmanApiService 에 의존하며 외부 api가 호출 로직이 필요한 경우에는 CongressmanApiService 의 메서드를 호출하면 된다. 😁

출처

https://blog.leaphop.co.kr/blogs/71/%EC%9D%B4%EA%B1%B0_%EB%AA%A8%EB%A5%B4%EA%B3%A0_%EC%99%B8%EB%B6%80_API_%ED%98%B8%EC%B6%9C%ED%95%98%EB%A9%B4_%ED%81%B0%EC%9D%BC%EB%82%A9%EB%8B%88%EB%8B%A4_

 

이거 모르고 외부 API 호출하면 큰일납니다.::LEAPHOP TECH BLOG

이번 포스팅에서는 Propagation.NEVER를 활용해 트랜잭션 관리에서 발생할 수 있는 치명적인 실수를 예방하고, 외부 API 호출을 안전하게 처리하는 방법을 소개합니다.

blog.leaphop.co.kr

댓글