Search

이미지 선번역 회고

생성일
2026/01/18 06:03
태그

1. 프로젝트 개요

목적

이벤트 드리븐 아키텍처를 통해 이미지 선번역을 구현하면서, 어떤 고민과 의사결정이 있었고 어떤 문제들을 어떻게 해결했는지를 되돌아보며 글로 기록하고 정리하는 것

배경

경쟁사의 이미지 선번역에 대한 기능 제공 및 그로 인한 많은 이미지 선번역 관련 VoC가 인입되어, 윈들리에서도 이미지 선번역 기능 구현 필요
이미지 선번역 기능을 구현하였으나, 이미지 번역 요청이 많아지는 경우 일부 이미지에서 PrematureCloseException과 함께 실패하는 케이스가 다수 발생
인페인트 서버에서 인페인팅 결과물을 스트리밍으로 내려주는데, 이때 인페인트 서버에 많은 요청이 몰려 부하를 받게 되면 스트리밍으로 결과물을 내려주다가 연결이 끊겨서 실패 처리됨
요약하자면, 인페인트 서버의 처리 용량에 한계가 있고 현재의 서버 스펙으로는 윈들리의 이미지 번역 트래픽을 감당하지 못하고 있는 문제를 해결해야 하는 상황
인페인트 서버 과부하로 인한 속도 저하를 막고 요청에 대한 안정성을 확보하기 위해 아웃박스 패턴 + SNS /SQS 기반 이벤트 드리븐 아키텍처로 전환
Before
flowchart TB
      subgraph Before["Before: 동기 처리"]
          direction LR
          C1[클라이언트] -->|HTTP 요청| W1[윈들리 서버]
          W1 -->|동기 호출<br/>응답 대기| I1[인페인트 서버]
          I1 -.->|스트리밍 응답| W1
          W1 -.->|응답 전달| C1

          I1 -.-x|🔥 과부하 시<br/>PrematureCloseException| W1

          style I1 fill:#ee5b5b,stroke:#c92a2a,stroke-width:2px
          style W1 fill:#cc7878,stroke:#c92a2a
      end
Mermaid
복사
After
  flowchart TB
      subgraph After["After: 비동기 이벤트 드리븐"]
          direction TB
          C2[클라이언트] -->|HTTP 요청| W2[윈들리 서버]
          W2 -->|트랜잭션| DB[(Database<br/>+ Outbox)]
          W2 -.->|202 Accepted| C2

          DB -->|폴링| SCH[스케줄러]
          SCH -->|발행| SNS2[AWS SNS]
          SNS2 -->|구독| SQS2[AWS SQS]
          SQS2 -->|소비| CSM[Consumer]
          CSM -->|호출| I2[인페인트 서버]
          CSM -->|결과 저장| DB

          SQS2 -.->|실패 시 재시도<br/>+ DLQ| CSM

          C2 -.->|결과 조회| W2

          style SQS2 fill:#19ab1c,stroke:#2f9e44,stroke-width:2px
          style DB fill:#abab1c,stroke:#2f2f44
          style CSM fill:#3963cb,stroke:#3648cf
      end
Mermaid
복사

2. 핵심 의사결정

왜 이벤트 드리븐인가?

검토한 옵션:
1.
GPU 서버 수직/수평 확장
2.
대기열(Queue)을 통한 순차 처리 → 선택
선택 이유:
GPU 서버 트래픽 제어 : GPU 서버에 동시에 요청되는 트래픽을 제어할 수 있게 됨
GPU 서버 트래픽 버퍼링: Queue가 피크 타임의 트래픽 완충 흡수
트레이드오프:
측면
얻은 것 ()
포기한 것 ()
성능
순간적으로 트래픽이 늘어도 안정성 보장
결과적 일관성(eventual consistency)
복잡도
-
컴포넌트 증가 (DB, SNS, SQS, Consumer)
비용
GPU 스케일 업/아웃 잠재 비용 절약
SNS/SQS 비용 $2/month 이하
운영
-
모니터링 포인트 증가
인사이트

이벤트 드리븐 구조 대기열 구현 방식 - DB vs Message Queue

검토한 옵션:
1.
DB의 별도 테이블을 통한 대기열 구현
2.
SQS 같은 Message Queue를 통한 구현 → 선택
선택 이유:
트래픽이 많은 경우 메세지 큐 방식이 효율적이고, 적다면 DB 테이블을 통한 간단한 구현이 효율적
현재 트래픽과 구조를 생각해보면 DB 방식을 적용하는 것이 맞겠으나, 쿠팡 API Rate Limit 우회나 상품 수집 비동기 처리 등 장기적으로 비동기 구조를 가져가야할 로직들이 다수 존재
개발 기간 확보를 받은만큼 확장성 높은 이벤트 드리븐 구조를 위해 기반을 다지는 작업을 이번에 수행하기로 결정
outbox 트랜잭션 패턴을 통해 메시지 발행 보장 가능
트레이드오프:
측면
얻은 것 ()
포기한 것 ()
성능
높은 처리량(DB Lock vs MQ)
-
복잡도
-
컴포넌트 증가 (DB, SNS, SQS, Consumer)
비용
-
SNS/SQS 비용 $2/month 이하
확장성
높은 확장성(DB 테이블 추가 vs SQS 추가)
-
운영
-
동시 요청 수를 확인하기 위한 별도의 카운팅 필요 메세지 처리 멱등성 보장 필요
인사이트

메시지 발행 at-least-once 보장하기

검토한 옵션:
1.
shed Lock (혹은 별도의 Lock 관리)
2.
state machine + MySQL skip locked → 선택
선택 이유:
별도의 column이나 Lock 관리 테이블을 생성할 필요 없이, 이미 발행된 outbox의 상태(status)를 활용하여 간단하게 처리하기 위한 목적
Lock으로 인한 병목을 조금이라도 해소하기 위해 MySQL의 skip locked 사용
트레이드 오프:
측면
얻은 것 ()
포기한 것 ()
성능
멀티 인스턴스 환경의 스케줄러가 skip locked를 통해 메세지 발행을 병렬 처리
-
복잡도
낮은 복잡도 (outbox 테이블 하나를 통해 발행 대상 메세지, 메세지 lock 관리 처리)
-
운영
-
기술 의존적인 선택 (MySQL의 기술로 DB 변경 시 재설계 필요)
인사이트

3. 설계

시스템 구성도

컴포넌트 다이어그램

플로우 차트

4. 주요 문제 해결 경험

이벤트 무한 재소비

상황 알파 환경에서 상품 (선)번역 중에 강제로 상품의 상태를 변경 시, 동일한 메세지를 무한으로 재소비하는 현상 발생
근본 원인
초기 설계:
DLQ와 최대 재소비 횟수 지정
일정 횟수 이상 실패 시 더 이상 소비가 되지 않도록 처리
기능 구현 중 설계 문제 발견 :
1.
재시도 횟수 딜레마 : 인페인트 서버 처리 용량 대비 이미지 번역 요청이 많은 경우, semaphore 획득 실패로 인한 메세지 재소비 반복. 메세지 최대 재소비 횟수가 너무 많으면 재소비의 의미가 희석되고, 너무 적으면 이미지 번역 시도조차 못하고 DLQ로 이동하게 됨
2.
DLQ 활용 : DLQ에 쌓인 메세지를 유의미하게 처리하는 로직 부재
이에 따라 기능 구현 중 DLQ와 최대 재소비 횟수 제한을 없애는 방향으로 설계 변경. 설계가 변경되면서 에러 처리에 대해 일부 케이스가 누락되어, 위 상황처럼 이벤트 무한 재소비 현상 발생
해결 과정
시도
접근
결과
1차
로그 분석을 통해 무한 재소비가 발생하는 정확한 시점과 조건 파악
상품 상태 강제 변경 시 예외 처리 누락 확인
2차
전체 메시지 소비 로직에서 추가 영향 범위 점검
해당 케이스 외에는 모두 try-catch로 감싸져 있어, 재소비가 필요한 경우에만 예외를 발생시키도록 구현되어 있음을 확인
최종
누락된 케이스에 대한 처리 및 배포
해결 완료
배운 점
설계 변경 시, 그로 인해 발생하는 여파에 대해 충분히 시간을 들여 추가 검토가 필요할 것 같다.
DLQ를 활용하지 않는 메세지 큐는 조건 없는 while문과 동일하다. break 포인트를 잘 넣었다고 생각해도, 실수하든 누가 수정하든 무한 루프가 발생할 수 있으니 주의하자.

분산/보상 트랜잭션 처리

상황
이미지 번역 중 에러 발생 시 에러 발생 시점에 따라 결과 처리가 달라져야하지만, 초기 설계는 하나의 트랜잭션으로 처리하도록 구상하여 해당 문제를 처리하려면 복잡하고 장황한 보상 처리 필요
예시)
세마포어 획득 실패
→ SQS 메세지 컨슘 실패 (재소비 유도)
이미지 번역을 위한 상품 조회 실패 (상품 삭제나 상품 번역 완료 상태)
→ SQS 메시지 처리 성공 + 이미지 번역 실패 상태 저장
상품 내 이미지 번역 조회 실패 (이미지 번역 진행 중)
→ SQS 메시지 처리 성공 + 이미지 번역 쪽 DB 상태 처리 X
기타 이미지 타입 지원하지 않는 케이스, 번역 실패, 인페인팅 실패 등등 다양한 실패 상황들
근본 원인
초기 설계 및 설계 변경:
초기 설계 시 이미지 번역 실패 시 재처리 3회로 간단하게 처리 예정
위의 사유로 DLQ 미사용하도록 설계 변경
사이드 이펙트 :
SQS 메세지 재소비와 이미지 번역 실패 재처리 로직의 분리 필요
이미지 번역의 각 단계에서의 실패마다 보상 처리 방안이 달라 분산 트랜잭션 필요해졌으나, 분산 트랜잭션과 보상 트랜잭션에 대한 설계 고려 부재
해결과정
시도
접근
결과
1차
전체 try-catch 후 catch에서 에러 처리 세분화
catch 내부에서 SQS 성공 처리, 재시도 루프 탈출 처리, 각 에러 케이스별 보상 처리 등 코드 복잡성 및 가독성 저하
최종
SQS 성공/실패 처리 분리 재처리가 필요한 케이스, 롤백이 필요한 케이스를 나누어 일괄 처리
해결 완료 (추가적으로 더 가독성 개선의 여지가 있어 리팩토링 필요)
배운 점
분산 트랜잭션과 관련해 설계가 없는 상태로 구현을 하다보니, 당연하게도 그에 관련한 QA 이슈가 많았고 실제 이와 관련한 운영 이슈도 두어 건 발생했었다.
복잡한 로직이나 구현이 필요할 때, 꼭 설계를 해보고 구현하자.

5. 회고

잘한 점

1.
문서화
What : 설계적 고민이나 의사결정 과정을 기록한 초기 설계 문서와 최종적 구현 방향성을 기록해둔 상세 설계안 문서를 남겨두었던 것이 좋았다.
Why : 이 이미지 번역 회고 문서를 작성하면서, 이미지 번역 당시 어떤 설계적 고민이 있었는지, 어떤 의사결정들이 이루어졌는지 문서로 남아있어 참고할 수 있었고 쉽게 당시 있었던 일을 히스토리를 되새길 수 있었다.
How(앞으로) : 추후 규모가 큰 프로젝트거나 많은 설계적 트레이드 오프 의사결정이 이루어지는 프로젝트가 있다면, 이번 긍정적 경험을 이어나가 의사결정 및 설계 과정을 문서로 상세히 남기는게 좋을 것 같다.

아쉬운 점

1.
부실했던 설계
What : DLQ 활용 방안이나, Retry 로직, 트랜잭션 범위와 보상 트랜잭션 등 설계가 부실했거나 구현 중 바뀌는 부분에서 많은 시행착오와 잠재적 버그가 있었고 구현 시간도 오래 걸렸던 것 같다.
Why : 이전 프로젝트가 엎어지면서 급하게 방향성이 변경되었고, 그로 인해 설계와 기술 검토 시간이 부족했었던 것 같다.
How(앞으로) : 스프린트 회고 과정에서 액션 아이템으로 사전 스프린트에서 기술 검토 시간을 확보 받기도 했고, 규모가 큰 설계는 개발자로서도 적극적으로 기술 검토 시간을 확보하기 위해 노력할 필요가 있을 것 같다.
2.
테스트 부족
What : skip locked 이나 outbox 메세지 발행 보장, 메세지 처리 멱등성 보장, 분산 트랜잭션 등 문제가 쉽게 발생하거나 회귀 버그가 발생할 여지가 있는 포인트들이 많았는데, 그 전부를 별도의 테스트 코드나 예방책 없이 QA 테스트 하나로 갈음한 것
Why : 개발 시간의 부족, 코틀린 테스트 코드 작성 경험 부족, 테스트하기 어려운 아키텍처 구조
How(앞으로) : 아키텍처 구조는 현재 개선 진행 중, 스타트업 특성상 개발 시간의 부족은 해소하기 어려움, 테스트 코드와 관련된 역량을 기르는 것을 분기 목표로 잡고 학습하기
3.
중구난방 코드
What : 이미지 번역 쪽 구현 내용을 보면, Facade에서 기능적 처리를 수행하기도 하고 메서드 자체가 너무 길고 복잡한 로직을 담고 있어 가독성과 유지보수가 좋지 않음
Why : 작성하면서 설계가 변경되기도 했고, 시간이 부족하다는 핑계로 기능 완성에 초점을 두고 구현하여 코드의 가독성과 유지보수성까지 챙기지 못함
How(앞으로) : 새로 정립된 아키텍처에 맞춰 리팩토링 하면서 개선