Skip to content

feat : 비동기 방식으로 전환 (바이브코딩)#7

Open
smuHyuns wants to merge 5 commits intomainfrom
feature/boot-to-webflux
Open

feat : 비동기 방식으로 전환 (바이브코딩)#7
smuHyuns wants to merge 5 commits intomainfrom
feature/boot-to-webflux

Conversation

@smuHyuns
Copy link
Copy Markdown
Collaborator

@smuHyuns smuHyuns commented Feb 9, 2026

No description provided.

@smuHyuns smuHyuns added the AI-REVIEW 해당 태그를 PR에 입력시 review 댓글이 달립니다. (진행중) label Feb 9, 2026
@smuHyuns smuHyuns added AI-REVIEW 해당 태그를 PR에 입력시 review 댓글이 달립니다. (진행중) and removed AI-REVIEW 해당 태그를 PR에 입력시 review 댓글이 달립니다. (진행중) labels Feb 9, 2026
@smuHyuns
Copy link
Copy Markdown
Collaborator Author

smuHyuns commented Feb 9, 2026

변경 사항 요약

변경 클래스: build.gradle

  • 변경 목적:
    • 프로젝트 의존성 및 테스트/빌드 관련 설정 변경
  • 주요 변경 내용:
    • Spring WebFlux를 기본 web 스타터로 사용하도록 변경, 기존 webmvc starter 제거
    • 테스트 의존성을 spring-boot-starter-webmvc-test에서 spring-boot-starter-test로 변경
  • 영향 범위:
    • 애플리케이션이 WebFlux 기반(리액티브)으로 동작하도록 의존성이 조정됨. (diff에 근거한 사실)

변경 클래스: settings.gradle

  • 변경 목적:
    • Gradle 플러그인 추가
  • 주요 변경 내용:
    • Foojay toolchains resolver 플러그인 추가
    • rootProject.name 유지
  • 영향 범위:
    • 빌드 도구 설정에 영향(플러그인 추가). (diff에 근거한 사실)

변경 클래스: ReviewController

  • 변경 목적:
    • 웹훅 엔드포인트를 리액티브 방식으로 변경
  • 주요 변경 내용:
    • 메서드 시그니처 변경: ResponseEntity -> Mono<ResponseEntity>
    • 내부 호출을 동기적 void 호출에서 ReviewUseCase.handle(...)의 Mono 반환을 사용하여 thenReturn(ResponseEntity.accepted())로 응답 반환
  • 영향 범위:
    • 컨트롤러 레이어가 리액티브 체인으로 변경되어 ReviewUseCase의 반환 타입 변경에 종속됨.

주요 변경 코드 (diff 기반):

// 이전 시그니처 (diff에서 제거된 형태)
public ResponseEntity<Void> githubWebhook(...) {
    reviewUseCase.handle(event, deliveryId, sig256, rawBody);
    return ResponseEntity.ok().build();
}

// 변경된 시그니처 (추가된 형태)
public Mono<ResponseEntity<Void>> githubWebhook(...) {
    return reviewUseCase.handle(event, deliveryId, sig256, rawBody)
            .thenReturn(ResponseEntity.accepted().build());
}

변경 클래스: reviewbot.review_server.common.client.GitHubClient

  • 변경 목적:
    • GitHub API 호출을 블로킹 방식에서 논블로킹 리액티브 방식으로 전환
  • 주요 변경 내용:
    • comment(...) 메서드 반환 타입 void -> Mono, .block() 제거, .then() 사용으로 Mono 반환
    • getPullRequestDiff(...) 반환 타입 String -> Mono, .bodyToMono(String.class) 반환 그대로 사용하고 .block() 제거
    • 에러 로깅은 doOnError로 변경
  • 영향 범위:
    • 모든 GitHubClient 호출 부분이 Mono를 반환하므로 호출자(서비스)도 리액티브 방식으로 변경되어야 함.

주요 변경 코드 (diff 기반):

// comment 메서드 변경
public Mono<Void> comment(GitHubCommentDto.IssueCommentRequest req) {
    return gitHubApiClient.post()
            .uri("/repos/{owner}/{repo}/issues/{issue_number}/comments", req.getOwner(), req.getRepo(), req.getIssueNumber())
            .bodyValue(Map.of("body", req.getBody()))
            .retrieve()
            .toBodilessEntity()
            .then()
            .doOnError(e -> log.error("PR 코멘트 요청 중 에러 발생 : {}", e.getMessage()));
}

// getPullRequestDiff 메서드 변경
public Mono<String> getPullRequestDiff(GitHubCommentDto.PRDiffRequest req) {
    return gitHubApiClient.get()
            .uri("/repos/{owner}/{repo}/pulls/{pull_number}", req.getOwner(), req.getRepo(), req.getPrNumber())
            .header("Accept", "application/vnd.github.v3.diff")
            .retrieve()
            .bodyToMono(String.class)
            .doOnError(e -> log.error("PR 정보 가져오기 중 에러 발생 : {}", e.getMessage()));
}

변경 클래스: reviewbot.review_server.common.client.OpenAiApiClient

  • 변경 목적:
    • OpenAI 호출을 논블로킹 방식으로 전환(블로킹 API 호출을 별도 스레드에서 실행하고 Mono로 래핑)
  • 주요 변경 내용:
    • ask(String) 및 ask(String, ReviewType) 반환 타입 String -> Mono
    • 내부 구현을 ResponseCreateParams 빌드 및 openAIClient.responses().create(...) 호출을 Mono.fromCallable로 래핑하고 Schedulers.boundedElastic()에서 실행
    • 에러 처리 및 예외 발생 로직은 기존과 유사하게 유지하되 Mono 체인으로 반환
  • 영향 범위:
    • OpenAI 호출을 사용하는 모든 코드가 Mono을 처리하도록 변경되어야 함.

주요 변경 코드 (diff 기반):

public Mono<String> ask(String input) {
    return Mono.fromCallable(() -> {
                ResponseCreateParams.Builder params = ResponseCreateParams.builder()
                        .input(input)
                        .model(openAIProps.getModel())
                        .instructions(promptProps.getCommon())
                        .maxOutputTokens(openAIProps.getMaxToken());

                Response response = openAIClient.responses().create(params.build());

                return extractOutputText(response)
                        .orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "..."));
            })
            .subscribeOn(Schedulers.boundedElastic());
}

// 유사하게 ask(input, reviewType) 도 Mono<String>으로 변경

변경 클래스: reviewbot.review_server.port.in.ReviewUseCase

  • 변경 목적:
    • UseCase 인터페이스의 처리 결과를 리액티브로 변경
  • 주요 변경 내용:
    • handle(...) 반환 타입 void -> Mono
  • 영향 범위:
    • 해당 인터페이스를 구현하는 서비스(ReviewService 등)가 Mono를 반환하도록 변경됨.

주요 변경 코드 (diff 기반):

// 이전
void handle(String event, String deliveryId, String sig256, byte[] rawBody);

// 변경
Mono<Void> handle(String event, String deliveryId, String sig256, byte[] rawBody);

변경: 삭제된 클래스들 (패키지 reviewbot.review_server.serivce)

  • 변경 목적:
    • 동명(유사) 서비스들이 삭제되고 리팩토링된 리액티브 버전으로 대체됨
  • 주요 변경 내용:
    • 삭제된 파일:
      • review_server/src/main/java/reviewbot/review_server/serivce/DispatchService.java (동기/Async 기반 구현 삭제)
      • review_server/src/main/java/reviewbot/review_server/serivce/GithubService.java (동기 구현 삭제)
      • review_server/src/main/java/reviewbot/review_server/serivce/ReviewService.java (동기 구현 삭제)
  • 영향 범위:
    • 동기/비동기(Async) 기반의 기존 구현들이 제거되었음.

(각 파일은 diff에 의해 완전 삭제됨 — 상세 내용은 diff 파일 참조)

변경 클래스: reviewbot.review_server.service.DispatchService (신규)

  • 변경 목적:
    • 기존 비동기/애노테이션 기반 처리(Async)에서 Reactor 기반의 작업 큐/워커로 전환
  • 주요 변경 내용:
    • 새 클래스가 추가됨. 주요 특징:
      • Sinks.Many 기반 unicast 큐 사용 및 startWorker에서 sink.asFlux().flatMap(..., CONCURRENCY).subscribe()
      • QUEUE_CAPACITY(256) 및 CONCURRENCY(4) 상수 도입
      • isDuplicate(...)는 ConcurrentHashMap 기반 Set으로 동일하게 존재
      • dispatchPullRequest(...)는 큐 상태 검사 후 sink.tryEmitNext(...)를 사용, 용량 초과 시 HTTP 429/503 형태의 Mono.error 반환
      • processItem(..)에서 githubService.reviewPullRequest(...) (Mono) 호출, 성공/실패 로깅, 에러시 onErrorResume으로 무시, finally에서 queued 감소
  • 영향 범위:
    • 작업 큐/처리 방식이 Reactor 기반으로 변경되어 호출자가 Mono를 다루도록 함. 동시성 및 큐 용량 관련 동작이 추가됨.

주요 변경 코드 (diff 기반):

private final Sinks.Many<WorkItem> sink = Sinks.many().unicast().onBackpressureBuffer();
private record WorkItem(String deliveryId, byte[] rawBody) {}

@PostConstruct
void startWorker() {
    sink.asFlux()
            .flatMap(this::processItem, CONCURRENCY)
            .subscribe();
}

public Mono<Void> dispatchPullRequest(String deliveryId, byte[] rawBody) {
    int current = queued.incrementAndGet();
    if (current > QUEUE_CAPACITY) {
        queued.decrementAndGet();
        return Mono.error(new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "..."));
    }
    Sinks.EmitResult result = sink.tryEmitNext(new WorkItem(deliveryId, rawBody));
    if (result.isFailure()) {
        queued.decrementAndGet();
        return Mono.error(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "..."));
    }
    return Mono.empty();
}

private Mono<Void> processItem(WorkItem item) {
    return githubService.reviewPullRequest(item.rawBody())
            .doOnSuccess(...)
            .doOnError(...)
            .onErrorResume(ignored -> Mono.empty())
            .doFinally(ignored -> queued.decrementAndGet());
}

변경 클래스: reviewbot.review_server.service.GithubService (신규)

  • 변경 목적:
    • PR 처리 로직을 블로킹 방식에서 리액티브 방식으로 전환
  • 주요 변경 내용:
    • 새로운 GithubService 클래스는 Mono reviewPullRequest(byte[] rawBody) 메서드를 제공
    • webhook 바디 파싱(ObjectMapper.readValue)은 Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())로 래핑
    • 라벨/이벤트 확인 후 GitHubClient.getPullRequestDiff(...) (Mono)을 flatMap으로 받아 처리
    • OpenAiApiClient.ask(...) (Mono) 두개의 호출을 Mono.zip으로 병렬 처리하고, 결과를 받아서 GitHubClient.comment(...) 호출(각각 Mono) 체인으로 실행
    • 전체 에러 발생 시 실패 코멘트(postFailureComment) 를 남기려 시도하고 실패 시 로그 처리
    • 내부에 ReviewContext 레코드 추가
  • 영향 범위:
    • PR 처리 흐름이 Mono 기반으로 변경됨. GitHubClient/OpenAiApiClient의 리턴 타입 변경에 맞춘 리액티브 통합 로직이 추가됨.

주요 변경 코드 (diff 기반):

public Mono<Void> reviewPullRequest(byte[] rawBody) {
    return Mono.fromCallable(() -> objectMapper.readValue(rawBody, PullRequestWebhookDto.class))
            .subscribeOn(Schedulers.boundedElastic())
            .flatMap(this::handleWebhook);
}

private Mono<Void> handleWebhook(PullRequestWebhookDto dto) {
    // Bot/label 체크 후...
    return gitHubClient.getPullRequestDiff(diffRequest)
            .flatMap(diff -> {
                Mono<String> describeAskResult = openAiApiClient.ask(describeInput, ReviewType.DESCRIBE);
                Mono<String> reviewAskResult = openAiApiClient.ask(reviewInput, ReviewType.REVIEW);
                return Mono.zip(describeAskResult, reviewAskResult)
                        .flatMap(results -> postComment(..., results.getT1())
                                .then(postComment(..., results.getT2())));
            })
            .onErrorResume(error -> {
                // 실패시 실패 코멘트 등록 시도
                return postFailureComment(context)
                        .onErrorResume(inner -> Mono.empty());
            });
}

변경 클래스: reviewbot.review_server.service.ReviewService (신규)

  • 변경 목적:
    • WebHook 처리 진입점을 리액티브 방식으로 변경
  • 주요 변경 내용:
    • ReviewUseCase를 구현하는 ReviewService.handle(...)이 Mono를 반환하도록 변경
    • HMAC 검증(verifyService.verifyHMAC) 호출을 Mono.fromRunnable로 래핑하여 체인화
    • 중복 검사 및 이벤트 타입 검사를 수행한 뒤 dispatchService.dispatchPullRequest(...)를 반환하여 비동기/리액티브 작업으로 위임
  • 영향 범위:
    • 컨트롤러 및 DispatchService와 연계되어 리액티브 엔드투엔드 흐름을 구성함.

주요 변경 코드 (diff 기반):

@Override
public Mono<Void> handle(String event, String deliveryId, String sig256, byte[] rawBody) {
    return Mono.fromRunnable(() -> log.info("[{}] webhook handle 시작", deliveryId))
            .then(Mono.fromRunnable(() -> verifyService.verifyHMAC(sig256, rawBody)))
            .then(Mono.defer(() -> {
                if (dispatchService.isDuplicate(deliveryId)) {
                    log.info("이미 처리된 웹훅번호입니다 : {}", deliveryId);
                    return Mono.empty();
                }
                if (!"pull_request".equals(event)) {
                    log.info("현재 버전은 pr 이외의 버전을 지원하지 않습니다. 현재 버전 : {} ", event);
                    return Mono.empty();
                }
                return dispatchService.dispatchPullRequest(deliveryId, rawBody);
            }));
}

변경 클래스: VerifyService (패키지명 변경)

  • 변경 목적:
    • 패키지 경로 정리(typo 보정으로 보이는 경로 변경)
  • 주요 변경 내용:
    • 파일이 review_server/src/main/java/reviewbot/review_server/serivce/VerifyService.java -> review_server/src/main/java/reviewbot/review_server/service/VerifyService.java 로 이동/이름 유지
  • 영향 범위:
    • 패키지 경로가 변경되어 참조하는 곳에서 import 경로 변경이 필요함(변경된 파일만 diff에 표시됨).

(참고: 해당 변경은 diff에서 패키지 선언의 변경만 일부 표시됨.)


위 요약은 PR의 diff 파일만을 근거로 작성되었습니다. diff에 포함되지 않은 프로젝트의 다른 파일/설정(예: Spring 설정, 빈 구성, 다른 호출부의 변경 등)에 대한 정보는 없습니다. (필요할 경우 추가 파일 diff를 제공해 주세요.)

@smuHyuns
Copy link
Copy Markdown
Collaborator Author

smuHyuns commented Feb 9, 2026

리뷰 평가 결과

사전조건 확인

  • 이 리뷰는 오직 PR diff에 포함된 변경사항만을 근거로 합니다. 아래는 PR 외부 코드/설정에 대해 명시적으로 가정한 내용입니다(추정):
    • WebClient 인스턴스(gitHubApiClient)와 OpenAI 클라이언트(openAIClient)는 다른 파일에서 적절히 구성되어 있습니다.
    • VerifyService의 구현(verifyHMAC 등)은 PR에 포함되어 있지 않으므로 해당 메서드가 예외를 던질 수 있음/없음에 대해 구체적으로 알 수 없습니다.
    • 빌드/런타임 설정(reactive 설정, Netty 등)은 적절히 구성되어 있다고 가정합니다.

런타임 오류 확인

아래 각 이슈는 왜 중요한지, 구체적인 개선 방법, 무시했을 때의 리스크를 포함합니다.

  1. Webhook DTO 필드 접근에서 NPE 가능성
  • 왜 중요한지
    • GithubService.handleWebhook 내부에서 dto.sender().type(), dto.pull_request().head().repo() 등 체인 호출이 다수 있습니다. webhook 페이로드가 기대와 다를 경우 (필드 누락/형식 변경) 즉시 NullPointerException이 발생할 수 있습니다.
  • 개선 방법(구체적)
    • 주요 필드(dto.sender, dto.pull_request, pull_request.head, pull_request.head.repo 등)에 대해 null 체크를 추가하거나 Optional/guard clause로 방어합니다. 예:
      if (dto == null || dto.pull_request() == null || dto.pull_request().head() == null || dto.pull_request().head().repo() == null) {
          log.warn("Invalid webhook payload: missing pull_request/head/repo");
          return Mono.empty();
      }
    • 또는 objectMapper로 파싱 후 스키마 검증(간단한 필수 필드 검사)을 수행.
  • 무시했을 때의 리스크
    • 운영 중 불완전한/다른 형태의 webhook을 받아 서비스가 예외로 중단되거나, 해당 요청이 처리되지 못함.
  1. processed(Set) 무한증가(메모리 누수)
  • 왜 중요한지
    • DispatchService.processed는 ConcurrentHashMap.newKeySet()으로 무제한으로 deliveryId를 보관합니다. 장기간 운영 시 수십만/수백만 항목이 쌓이면 메모리 부족(OOM)이 발생할 수 있습니다.
  • 개선 방법(구체적)
    • TTL(유효기간) 기반 캐시로 대체: Redis나 Caffeine 같은 로컬 캐시 사용. 예: Caffeine
      private final Cache<String, Boolean> processed = Caffeine.newBuilder()
          .expireAfterWrite(Duration.ofHours(1))
          .maximumSize(100_000)
          .build();
      public boolean isDuplicate(String deliveryId) {
          return processed.asMap().putIfAbsent(deliveryId, Boolean.TRUE) != null;
      }
    • 또는 Redis SET에 TTL을 둬서 dedupe 정보를 보관.
  • 무시했을 때의 리스크
    • 메모리 사용량이 점점 증가하여 서비스 OOM/GC 문제로 이어짐.
  1. Sinks 구성과 구독자(Emit 실패) 관련 레이스/동작
  • 왜 중요한지
    • Sinks.many().unicast().onBackpressureBuffer()를 사용하고 있으며 tryEmitNext가 구독자 미존재(FAIL_ZERO_SUBSCRIBER) 등으로 실패할 수 있습니다. 현재는 tryEmitNext 실패 시 queued를 감소시키고 503을 반환하지만, 실제 동작에서 sink 버퍼의 크기(기본적으로 unbounded)와 QUEUE_CAPACITY 로직이 의도와 다를 수 있습니다.
  • 개선 방법(구체적)
    • 명확하게 bounded buffer를 사용하거나 발행전 queued 카운트로 제어하는 로직을 보강합니다. 가능한 대안:
      • Sinks.many().multicast().onBackpressureBuffer(QUEUE_CAPACITY) 등 버퍼 크기를 명시(사용중인 Reactor 버전에 API가 있다면)하거나,
      • tryEmitNext 실패 시 실패원별로 상세 로그/대체 동작(예: 다시 시도, DLQ) 처리.
    • 또한 PostConstruct로 startWorker를 보장하더라도 애플리케이션 시작 시점에 이벤트가 들어오면 tryEmitNext가 실패할 수 있으므로 애플리케이션 기동 순서(구독자가 등록된 후 수신 가능)를 보장하거나, 실패시 명확한 재시도 전략을 마련하세요.
  • 무시했을 때의 리스크
    • 드문 시나리오지만 초기가동시점 또는 구독자 문제로 인해 정상적인 webhook이 503/실패 처리될 수 있음. 버퍼 정책 불일치로 인해 의도치 않은 메모리 누수 또는 무한대기 발생 가능.
  1. 외부 API 호출(오픈AI / GitHub)에 대한 타임아웃 누락 및 로깅 부족
  • 왜 중요한지
    • OpenAI 요청과 GitHub API 호출은 블로킹/네트워크 I/O를 포함합니다. 현재 OpenAiApiClient는 boundedElastic에서 blocking SDK 호출을 수행하지만 타임아웃을 지정하지 않아 장시간 블로킹될 경우 스레드 자원 고갈 우려가 있습니다. 또한 doOnError에서 log.error("... : {}", e.getMessage()) 형태로 스택트레이스가 남지 않아 문제 원인 파악이 어렵습니다.
  • 개선 방법(구체적)
    • 외부 호출 Mono에 timeout 연산자를 추가:
      return Mono.fromCallable(() -> openAIClient.responses().create(params.build()))
                 .subscribeOn(Schedulers.boundedElastic())
                 .timeout(Duration.ofSeconds(30));
    • 에러 로그에서는 예외 전체를 로깅:
      .doOnError(e -> log.error("PR 코멘트 요청 중 에러 발생", e));
  • 무시했을 때의 리스크
    • 외부 API 지연/응답 없음으로 인해 boundedElastic 스레드가 고갈될 수 있고, 문제 원인 추적이 어려워 복구가 지연됨.

보안 이슈

  1. 서명 검증(VerifyService) 호출 타이밍 및 헤더/페이로드 검증
  • 왜 중요한지
    • ReviewService.handle에서 verifyService.verifyHMAC(sig256, rawBody)를 호출하여 서명 검증을 수행하는 것으로 보입니다. 다만 컨트롤러에서 관련 헤더(sig256)가 없을 경우(요청 위조 등) 어떻게 처리되는지 보이지 않습니다. 서명 검증 실패 시 적절한 HTTP 응답(401/403)이 필요합니다.
  • 개선 방법(구체적)
    • Controller/Service 레벨에서 signature 헤더 누락 시 400/401 반환(즉시 실패). verifyHMAC가 예외를 던지는 경우 이를 잡아 적절한 HTTP 상태로 매핑하세요. 예:
      if (!StringUtils.hasText(sig256)) {
        return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing signature"));
      }
    • 로그에 원문 페이로드(rawBody)를 그대로 남기지 마세요(민감 정보 포함 가능). 로깅 시는 해시나 길이만 남기는 것이 안전합니다.
  • 무시했을 때의 리스크
    • 서명 누락/검증 실패를 통해 위조된 webhook이 수용될 가능성 또는 잘못된 오류 응답으로 오탐 발생.
  1. 요청 크기 제한 미비 (DoS 가능성)
  • 왜 중요한지
    • @RequestBody byte[] rawBody로 전체 요청을 받습니다. 악의적으로 매우 큰 페이로드를 보내면 메모리 압박을 받을 수 있습니다.
  • 개선 방법(구체적)
    • 서버/컨트롤러 레벨에서 수신 바디 크기를 제한하거나, WebFlux 설정에서 max-in-memory size 등으로 제한합니다.
  • 무시했을 때의 리스크
    • 대량/대형 요청에 의한 메모리 과다 사용, 서비스 가용성 저하.

주요 개선 사항 요약

아래는 PR에서 가장 우선적으로 해결해야 할 4가지 이슈입니다. 각 항목은 왜 중요한지, 최소한의 수정 제안(코드 포함), 무시할 경우의 리스크를 제시합니다.

  1. processed(Set) 무한 증가 — 반드시 TTL 기반 캐시로 교체
  • 왜 중요한가: 운영 중 메모리 누수(OOM) 가능성이 매우 높습니다.
  • 최소 수정 예시(권장: Caffeine 또는 Redis 사용):
    // example using Caffeine (추가 의존 필요)
    private final Cache<String, Boolean> processed = Caffeine.newBuilder()
        .expireAfterWrite(Duration.ofHours(1))
        .maximumSize(100_000)
        .build();
    
    public boolean isDuplicate(String deliveryId) {
        return processed.asMap().putIfAbsent(deliveryId, Boolean.TRUE) != null;
    }
  • 리스크: 메모리 증가 → GC 지연 → 서비스 불안정 / OOM.
  1. Webhook 페이로드 필드 접근시 방어 코드 부재(NPE 위험)
  • 왜 중요한가: 외부 입력은 항상 불완전할 수 있으며 NPE는 즉시 처리 중단을 야기함.
  • 최소 수정 예시:
    if (dto == null || dto.pull_request() == null || dto.pull_request().head() == null || dto.pull_request().head().repo() == null) {
        log.warn("Invalid webhook payload");
        return Mono.empty();
    }
  • 리스크: 예상치 못한 NPE로 요청 누락 및 로그 재현 불가.
  1. 외부 API 호출에 대한 타임아웃 부재 및 로깅 개선
  • 왜 중요한가: 블로킹 호출이 장시간 대기하면 boundedElastic 풀 고갈 가능, 디버깅 어려움.
  • 최소 수정 예시(OpenAiApiClient.ask 내부):
    return Mono.fromCallable(() -> {
          // ...blocking call...
    })
    .subscribeOn(Schedulers.boundedElastic())
    .timeout(Duration.ofSeconds(30)) // 타임아웃 추가
    .doOnError(e -> log.error("OpenAI 요청 실패", e));
    그리고 GitHubClient 등 doOnError에서 예외 전체를 로깅:
    .doOnError(e -> log.error("PR 코멘트 요청 중 에러 발생", e));
  • 리스크: 스레드 고갈, 긴 대기시간, 원인 파악 불가.
  1. Sinks 사용과 버퍼 정책 불명확 — Emit 실패/버퍼 정책 재검토
  • 왜 중요한가: 현재 사용한 onBackpressureBuffer()는 기본적으로 unbounded일 수 있으며, startWorker와의 타이밍 문제로 tryEmitNext가 실패할 수 있음.
  • 최소 수정 제안:
    • API가 허용한다면 버퍼 크기를 명시적으로 설정하거나(라이브러리 API 확인 필요), 발행 전에 queue 카운트로 제어(현재 로직)와 함께 emit 실패 시 재시도 또는 명확한 DLQ 처리 추가.
    • 예: 만약 Reactor API에 맞다면
      Sinks.Many<WorkItem> sink = Sinks.many().unicast().onBackpressureBuffer(QUEUE_CAPACITY);
      (참고: 실제 사용 중인 Reactor 버전의 API를 확인하여 올바른 오버로드 사용)
  • 리스크: 버퍼 정책 불일치로 인한 메모리 문제, 또는 초기가동 시 이벤트 손실(FAIL_ZERO_SUBSCRIBER).

설명은 여기까지입니다. PR 전체적으로 reactive로 전환한 방향은 적절하며 구조도 잘 정리되어 있습니다. 위 4가지는 운영 안정성과 보안 측면에서 시급히 보완되어야 할 항목입니다.

Ryan-chs and others added 3 commits February 9, 2026 14:07
- 와일드카드 임포트를 개별 임포트로 변경
- 임포트 순서를 ASCII 순으로 정렬
- 어노테이션 순서 통일 (Spring stereotype → Spring 설정 → Lombok)
- 불필요한 빈 줄 제거
- 100자 초과 라인 줄바꿈 적용
- 제어문(if, for) 중괄호 추가
- Properties 클래스 어노테이션 순서 통일

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-REVIEW 해당 태그를 PR에 입력시 review 댓글이 달립니다. (진행중)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants