Skip to content

Conversation

@jher235
Copy link
Member

@jher235 jher235 commented Jan 6, 2026

Related issue 🛠

Work Description 📝

메시지 브로커 아키텍처 구현 (Strategy Pattern)

  • MessageBrokerResolver, MessageStrategyProvider, MessageBrokerStrategy를 구현하여 메시지 타입에 따라 적절한 브로커(현재는 SQS)가 선택되도록 전략 패턴을 사용함
  • Message, MessageHandler 인터페이스를 정의하여 메시지 생성과 처리 로직을 분리
  • JsonMapper를 빈으로 등록하여 직렬화/역직렬화 설정을 통일. (아직 별도의 설정은 없음.)

AWS SQS

  • SqsStrategy, GenerativeAiSqsStrategy 구현.
  • SqsListenerStarter를 통해 애플리케이션 시작 시 리스너가 동작하도록 설정.
  • Exponential Backoff(지수 백오프): GenerativeAiSqsStrategy 에선 처리 실패 시 애플리케이션 레벨에서 점진적으로 재시도 간격을 늘리도록 설정함.

Transactional Outbox Pattern 적용

  • OutboxMessage 엔티티 및 Repository: 발행할 메시지를 DB에 먼저 저장하여 비즈니스 트랜잭션과의 원자성 보장.
  • OutboxMessageScheduler: 발송 누락된 메시지(lastAttemptAt이 없는 OutboxMessagfe)를 주기적으로 탐색하여 재발송함.
    • 재발송 시에는 메시지를 동기적으로 발송하여 성공 시 lastAttemptAt 갱신, 실패 시 lastAttemptAt 을 계속 null 로 두어 다음 스케줄링에서 재발송하도록 함
  • 기존에 ReportRationale 까지 생성한 후에 Report 를 저장하던 로직에서 Report 를 먼저 저장한 후 ReportRationale 을 발급받아 업데이트 하도록 로직을 변경함. Outbox 저장 및 SQS 발행 시 Report가 이미 존재하여 reportId 를 포함하기 위함. Report 생성은 ReportRationale 가 실패한 경우에도 진행되므로 이렇게 하는 것도 좋다고 생각했음.

Circuit Breaker 도입

  • OpenAI API 사용 시 서킷 브레이커를 도입하여 과도하게 트래픽이 몰리는 경우 & 외부 서비스 장애에 대해 대비함
  • 설정은 최근 20회의 요청 중 50 % 의 응답 성공, 실패(혹은 지연) 여부에 따라 서킷을 발동하며 서킷 지속 시간은 1분임. (OpenAi 의 RPM 500 회를 회피하기 위함)

리팩토링

  • GenerateAiClient 인터페이스 도입: OpenAiClient의 결합도를 낮추고, API 호출용과 메시지 큐 처리용 로직을 분리함.
  • ThreadPool 설정을 통해 비동기 작업 처리를 위한 리소스를 최적화.

기술 고민

  • 왜 SQS 를 사용했는가?

    • 단순 Outbox Pattern 만을 사용해서 재시도를 하지 않은 이유는 아웃 박스 패턴으로 실패한 케이스에 대해서만 스케줄링으로 재시도할 경우 실패한 요청을 스케줄링 시점에 몰아서 재시도하므로 late limit에 한계가 올 확률이 늘어나기 때문. SQS 를 통해 이런 부하를 고르게 분산하고자함
    • kafka, rabbitMQ 처럼 복잡한 메세지 처리가 필요하지 않았고, 다른 메세지 큐에 비해 SQS 가 저렴함. SQS는 별도의 컴퓨팅 자원을 필요로하지 않고 월 100만건까지 무료, 이후도 100만개당 0.4 $ 정도 이므로 비용이 효율적이라고 판단함
  • 왜 Outbox Pattern을 도입했는가?

    • 단순 비동기 호출(@async 등)이나 SQS 즉시 발행은 DB 트랜잭션이 커밋되기 전에 메시지가 발행되거나, DB 저장은 성공했지만 네트워크 오류로 SQS 발행이 실패하는 경우 데이터 불일치 및 요청 유실이 발생할 수 있음. 이를 방지하기 위해 '보낼 메시지'를 DB에 먼저 기록하는 Outbox Pattern을 채택함.
    • 기본적으로 SQS 에서 메세지를 폴링할 경우 LastAttemptAt 을 업데이트하므로 스케줄러는 LastAttemptAt 이 null 인 메세지를 누락된 메세지로 판단해서 SQS 에 동기적으로 메세지를 재발송함.
      처음엔 기본적으로 메세지 발급을 모두 비동기적으로 처리하고 있었으므로 스케줄러는 LastAttemptAt 이 null이면 메세지 발급, 이후 폴링해서 LastAttemptAt 을 업데이트 하는 플로우를 생각했음.(LastAttemptAt 갱신 == SQS 에 메세지가 들어갔는지 판단하는 부분을 수신하는 쪽에서 처리한다는 것.) 다만 이렇게 수신하는 쪽에서 처리는 경우 메세지 큐가 많이 밀렸을 때 앞의 메세지가 처리되지 못해서 계속해서 대기중일 수 있고, 이 경우 송신하는 스케줄러는 계속 해당 메세지가 큐에 들어가지 않았다고 판단해서 지연 중인 메세지 큐에 요청을 계속 다시 보내 상황을 악화시킬 수 있음. 따라서 메세지를 큐에 동기적으로 전송하는 메서드를 별도로 두어 실패 시 LastAttemptAt 을 업데이트하지 않아 다음 스케줄러에서 다시 전송을 시도하도록 함.
  • Circuit Breaker 도입에 대한 고민

    • 아래 이유로 서킷 브레이커 도입을 고민하게 됨
      • 우리 서비스는 맨 처음 응답 시 보험 추천 이유와 키워드를 최대한 정상적으로 발급해주는게 좋다고 생각함.
      • 이 점에서 rate limiter를 사용하면 사실 최초에 API 응답 성공 케이스를 가장 많이 가져올 수 있는건 서킷 브레이크 없이 매번 요청을 보내는 것임.
      • SQS 를 통한 비동기 처리를 도입한 시점에서 retry 횟수도 줄어들어 API 응답 자체가 크게 느려지지 않았기 때문에 실패하더라도 매번 두 번 정도는 요청을 보내는게 나은가? 란 고민을 함
    • 그럼에도 서킷 브레이커를 도입한 이유
      1. 처리율이 개선됨.(약 14% 가량)
      2. 애초에 서킷이 발동되는 시점은 극한의 상황임. 트래픽이 감당하기 힘들 정도로 몰리는 시점 혹은 OpenAI 장애인 것. 따라서 서킷 브레이커가 발동해서 얻을 단점을 고민하고 있는 것은 크게 의미가 없다고 판단함. 애초에 극한의 상황이므로.
      3. 현재 서비스는 request per thread임. 따라서 retry 로 인해 응답을 오래동안 기다리고, 이로 인해 스레드가 고갈된다면 정상적으로 처리됐어야 하는 다른 유저의 요청에도 영향을 미칠 수 있음.

Uncompleted Tasks 😅

To Reviewers 📢

- NewImageRequest 는 현재 사용하고 있지 않지만, 추후 다시 사용하게 될 경우를 대비하여 함께 수정함
- 빈으로 사용하도록 함. ObjectMapper의 설정을 공통되게 사용할 수 있도록 하기 위함
- 기존에 불필요하게 높았던 결합도를 낮추고자 GenerateAiClient 를 도입하여 OpenAiClient 를 감춤
- 메세지 큐를 목적 단위로 나누고자 함. 따라서 GenerativeAi 요청을 위한 메세지 큐를 둠
- 메세지 큐를 목적 단위로 나누고자 함. 따라서 GenerativeAi 요청을 위한 메세지 큐를 둠
- 모든 MessageBroker는 메세지 발급이 가능해야하므로 MessagePublisher 를 상속 받음
- 내부의 MessageStrategyProvider 를 통해 요청 메세지에 맞는 알맞은 strategy 를 판단해 메세지를 발급함
- api 에서만 폴백 로직을 처리하기 위함
- 스케줄러가 SQS 메세지 발급을 누락없이 확정적으로 처리하도록 하기 위함
- 리포트 생성 이후 메세지 발급과 outboxMessage 를 생성하므로 로직 흐름상 일관성이 향상됨. message 나 outboxMessage 만 존재하고 report가 없는 경우가 존재하지 않음
- 누락된 SQS 메세지가 존재할 경우 동기적으로 SQS 큐에 메세지를 전달함
@jher235 jher235 requested a review from jeong1112 January 6, 2026 07:13
@jher235 jher235 self-assigned this Jan 6, 2026
@jher235 jher235 added ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링 🔥재헌 담당 - 재헌 labels Jan 6, 2026
@jher235 jher235 merged commit c30461e into develop Jan 11, 2026
@jher235 jher235 deleted the feat/#210-ensure-issue-report-reason branch January 11, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🔥재헌 담당 - 재헌 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 보험 추천 리포트 발급 실패 처리 개선

2 participants