[졸프]Spring Boot에서 유저 벡터 생성 자동 호출 구현 + RestTemplate Apache 오류 해결 (JDK HttpClient 전환기)

ssun·2025년 8월 12일

졸업프로젝트

목록 보기
12/21

1. 목표

이번에 구현하려던 기능은 유저가 책을 읽음(user_book_read 테이블에 추가)ML 서버에 유저 벡터 생성 API 호출user_embedding 테이블 갱신 의 흐름이었다.

이 기능은 두 가지 경우에 동작해야 한다.

  1. 회원가입 시 설문 작성 → 선호 도서(user_book_read) 저장

  2. 도서 스크랩 시 상태가 READ로 변경 → user_book_read 저장

2. 설계

이벤트 기반 비동기 처리

  • 동기로 호출하면 API 응답이 ML 서버 응답 속도에 따라 느려지므로, 이벤트 발행 + 비동기 리스너 방식을 선택.
  • 트랜잭션 커밋 이후(AFTER_COMMIT) 호출되도록 설정 → DB 저장 실패 시 호출 방지.

흐름도

[UserPreferenceService or BookScrapService]
        ↓ (user_book_read 저장)
  이벤트 발행(UserBookReadAddedEvent)
        ↓
[UserVectorGenerateListener]
        ↓ (비동기 호출)
ML 서버 /api/users/{id}/generate-vector 호출

3. 코드 구현

3.1 이벤트 클래스

@Value
public class UserBookReadAddedEvent {
    Long userId;
}

3.2 서비스에서 이벤트 발행

1) 회원가입 설문 저장 로직(UserPreferenceService)

boolean anyReadInserted = false;

if (dto.getPreferredBookId() != null) {
    for (Integer bookId : dto.getPreferredBookId()) {
        if (!userBookReadRepository.existsByUser_UserIdAndBook_BookId(userId, bookId)) {
            userBookReadRepository.save(...);
            anyReadInserted = true;
        }
    }
}

if (anyReadInserted) {
    eventPublisher.publishEvent(new UserBookReadAddedEvent(userId));
}

2) 도서 스크랩 로직(BookScrapService)

boolean readInsertedThisTx = false;

if ("READ".equals(status)) {
    if (!userBookReadRepository.existsByUser_UserIdAndBook_BookId(userId, bookId)) {
        userBookReadRepository.save(...);
        readInsertedThisTx = true;
    }
}

if ("READ".equals(status) && readInsertedThisTx && !isReadBefore) {
    eventPublisher.publishEvent(new UserBookReadAddedEvent(userId));
}

3.3 비동기 리스너

@Component
@RequiredArgsConstructor
public class UserVectorGenerateListener {

    private final RestTemplate restTemplate;

    @Value("${ml.server.url}")
    private String mlBaseUrl;

    @Async("vectorTaskExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onUserBookReadAdded(UserBookReadAddedEvent event) {
        String url = mlBaseUrl + "/api/users/" + event.getUserId() + "/generate-vector";
        restTemplate.postForEntity(url, null, Void.class);
    }
}

4. 발생한 문제

실행 시 다음 오류 발생:

java.lang.NoClassDefFoundError: org/apache/hc/client5/http/classic/HttpClient
java.lang.NoClassDefFoundError: org/apache/hc/client5/http/ssl/TlsSocketStrategy

원인:

  • HttpComponentsClientHttpRequestFactory는 Apache HttpClient5에 의존.
  • 하지만 프로젝트에 Apache HttpClient 관련 라이브러리가 없었음.
  • Spring Boot 3.4.x는 기본 HTTP 클라이언트를 auto-detect 하는데, Apache 클래스가 없으면 예외 발생.

5. 해결: JDK HttpClient로 전환

Apache 의존성 추가 대신, JDK 기본 HttpClient를 쓰는 방식으로 변경.
이러면 의존성 없이 깔끔하게 동작.

수정 코드

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    HttpClient jdkClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(2))
            .build();

    JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(jdkClient);
    factory.setReadTimeout(Duration.ofSeconds(10));

    return builder.requestFactory(() -> factory).build();
}

application.yml

spring:
  http:
    client:
      factory: jdk

Gradle
Apache HttpClient 관련 의존성 전부 제거.

6.결과

  • 유저가 책을 읽음 → ML 서버로 비동기 벡터 생성 요청 → user_embedding 자동 갱신.

  • Apache HttpClient 관련 NoClassDefFoundError 사라짐.

  • 의존성 가벼워지고, Spring Boot 3.4.x와 호환성 문제도 해결.

7. 느낀 점

  • 이벤트 + 비동기 방식은 응답 속도 저하 없이 외부 서버 호출 가능.
  • Spring Boot 3.4.x에서는 HTTP 클라이언트 팩토리를 명시하거나, JDK HttpClient로 전환하는 게 안전.
  • 의존성은 최소화하는 게 유지보수에 유리. 원래는 의존성을 그냥 추가하는게 오히려 좋다고 생각했지만 의존성은 적을수록+ 가벼울수록 유지보수성이 좋다는 것을 이번에 알게 되었다. 의존성은 정말 필요할때만 추가하기...!

동기와 비동기 차이를 공부해보고 싶어 밑 벨로그 글에 정리해놨다
동기와 비동기 차이

profile
안녕하세요! 백다현입니다

0개의 댓글