Skip to content

아웃박스 공통 구조 분리 (1단계) - #1262

Merged
m-a-king merged 3 commits into
devfrom
refactor/1261-outbox-common-structure
Oct 31, 2026
Merged

m-a-king merged 3 commits into
devfrom
refactor/1261-outbox-common-structure

Conversation

@m-a-king

@m-a-king m-a-king commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

한눈에

아웃박스 공통 뼈대 분리

  • 이전: 상품 파싱 아웃박스(작업 대기열 테이블)만 격리 수준을 낮춰 둠
    • 원인: 기본 격리 수준에서 대기열을 집는 조회가 새 요청 저장을 막음
    • 다음 아웃박스를 만드는 개발자가 이 설정을 기억해야 함
  • 공통 큐 클래스가 집기·되돌리기·회수의 격리 수준을 직접 정함
    • 새 아웃박스는 이 클래스를 상속해 같은 설정을 받음
  • 공통 테스트 묶음이 격리 수준·잠금 건너뛰기·인덱스를 검사
    • 새 아웃박스는 테스트 클래스 하나로 같은 검사를 받음
  • 머지 직후 dev 자동 배포
  • 데이터 변경 없음

Situation

다음 아웃박스에서 흩어지는 RC

  • 아웃박스 공통 구조 분리 (1단계) #1261
  • 추가
    • 기존 코드에 집기 트랜잭션(스케줄러 폴링)을 붙잡아 둘 자리 없음
      • 결과: RC 테스트를 리팩터링 전에 쓸 수 없음
    • 1차 구조 리뷰에서 사람 기억에 걸린 규칙 4개가 남음
      • 잠금 조회를 서비스 트랜잭션에서 직접 부르는 우회
      • 펜싱 조회를 골라 써야 걸리는 펜싱
      • 스케줄러가 직접 불러야 하는 처리 상한 검사
      • 구현 테이블의 인덱스

Task

공통 구조 분리

  • 아웃박스 공통 구조 분리 (1단계) #1261
  • 범위: 이슈 대비 추가
    • 성공·실패·되돌리기 전이의 시도 확인
    • 도메인 저장소에서 잠금 조회 제거
    • 계약 테스트의 인덱스 검사
  • 범위: RC 테스트를 리팩터링 뒤로 이동
    • 이슈의 순서는 테스트 먼저

Action

공통 뼈대 추가

조각 맡는 일 새 아웃박스가 할 일
엔티티 Outbox 상태 전이·회수 상한·펜싱 상속
JPA 저장소 OutboxJpaRepository 잠긴 행을 건너뛰는 집기·회수 조회 상속
큐 OutboxQueue RC 트랜잭션·바깥 트랜잭션 거부 상속·회수 상한 동작 구현
정책 OutboxPolicy 회수 기준·회수 상한·처리 상한 검사 값 지정
저장소 어댑터 OutboxRepositoryAdapter 저장·펜싱 조회 도메인 저장소 구현체의 상속
  • 공통으로 뺀 기준은 빠뜨려도 조용히 넘어가는 규칙
  • 파싱 아웃박스를 이 뼈대 위로 이전
    • 마이그레이션 0

RC 를 트랜잭션 템플릿으로 엶

  • 공통 큐가 집기·되돌리기·회수의 RC 트랜잭션을 직접 엶
  • 집기·되돌리기·회수는 final
    • 원인: 하위 큐가 override 로 RC 를 빼는 경로 차단
선택지 장점 비용
TransactionTemplate (채택) 프록시 없이 적용·final 유지·테스트에서 직접 만든 큐에도 RC 유지 트랜잭션 코드 노출
@Transactional(isolation = READ_COMMITTED) 선언형 프록시가 가로채도록 메서드 개방 필요·하위 큐의 override 로 RC 우회 가능
RC 어노테이션을 붙인 별도 빈 선언형·final 유지 TransactionTemplate 과 같은 일을 하는 빈 추가
  • final 인 채로 하위 큐에 프록시 어노테이션을 붙이면 첫 집기에서 NPE
    • 원인: CGLIB 프록시는 final 메서드를 override 하지 못함
    • 호출이 실제 빈으로 넘어가지 않음
    • 메서드 본문이 생성자 없이 만든 프록시 객체에서 돎
    • 프록시 객체의 필드는 전부 null
    • Spring 이 프록시를 만들 때 같은 경고 로그를 남김

바깥 트랜잭션 안의 호출 거부

  • 집기·되돌리기·회수를 바깥 트랜잭션 안에서 부르면 예외
    • 원인: Spring 이 기존 트랜잭션에 합류하면 격리 수준 설정을 에러 없이 무시함
    • 정상 호출자는 트랜잭션 밖의 스케줄러 하나
선택지 장점 비용
코드 한 줄 검사로 거부 (채택) 잘못된 호출이 첫 실행에서 드러남·영향이 큐 안 트랜잭션 안의 집기 요구가 생기면 재결정
새 트랜잭션 전파(REQUIRES_NEW) 바깥 트랜잭션 안에서도 RC 유지 바깥 작업과 따로 커밋·커넥션 2개 점유·바깥이 잠근 행에 대한 안쪽의 잠금 대기
트랜잭션 금지 전파(NEVER) 선언형 거부 격리 수준 무시(트랜잭션 없음)·RC 를 여는 별도 빈 필요
트랜잭션 매니저의 합류 검증(validateExistingTransaction) 격리 수준이 다른 합류를 예외로 전환 앱 전체 트랜잭션에 적용·readOnly 불일치도 예외로 전환
앱 기본 격리 수준을 RC 로 변경 합류해도 RC 유지 다른 모든 트랜잭션의 동작 변경
  • 합류 시 격리 수준 무시는 TransactionTemplate 에서도 동일
    • 원인: 프록시가 아니라 전파 규칙의 동작

잠금 조회를 도메인 저장소에서 제거

  • 잠긴 행을 건너뛰는 조회는 공통 큐만 부름
    • 원인: 서비스 트랜잭션(기본 RR) 안에서 직접 부르면 gap 잠금이 돌아옴
  • 도메인 저장소는 저장·펜싱 조회만 상속

펜싱을 상태 전이에 넣음

  • 성공·실패·되돌리기가 시도를 받아 행을 쥔 시도만 반영
    • 이전: 호출하는 쪽이 펜싱 조회를 골라 써야 펜싱이 걸림
  • 회수 상한에 닿았을 때의 처리는 하위 큐의 추상 메서드로 강제
  • 되돌리기를 파싱 서비스에서 공통 큐로 이관

처리 상한 검사를 정책 생성자로 이관

  • 처리 상한이 회수 기준 이상이면 정책 생성 거부
    • 이전: 파싱 스케줄러가 부팅 때 직접 검사

루프는 파싱 도메인에 남김

  • 디스패치·회수 @Scheduled 루프
    • 원인: 주기·풀 크기·처리 상한이 도메인 값
    • 빠뜨리면 루프가 안 돌아 바로 드러남

계약 테스트 추가

  • 인터페이스 OutboxQueueContract 의 default 테스트 5건
  • 새 아웃박스는 : IntegrationTestSupport(), OutboxQueueContract<T> 한 줄로 검증받음
검사 확인하는 것
RC 집기 트랜잭션을 붙잡은 동안 새 PENDING 적재의 2초 안 완료
잠금 건너뛰기 커밋 안 된 적재가 있을 때 집기의 대기 없음
펜싱 회수 뒤 다시 집힌 행에 대한 옛 시도의 쥐기·되돌리기 실패
바깥 트랜잭션 거부 바깥 트랜잭션 안의 집기·되돌리기·회수 거부
인덱스 구현 테이블의 (status, created_at) 으로 시작하는 인덱스
선택지 장점 비용
인터페이스 default 테스트 (채택) 상속 한 줄·메타 테스트 통과 Kotlin 의 bridge 메서드 생성에 의존
추상 테스트 클래스 익숙한 형태 메타 테스트 위반 (IntegrationTestSupport 직접 상속 요구)
  • RC 검사는 큐에 넘기는 JPA 저장소를 위임으로 감싸 집기 조회 직후 멈춤
  • 기존 집기 동시성 테스트는 계약으로 흡수

기타

  • 저장 전 엔티티를 실패 상태로 만드는 테스트 헬퍼 추가
    • 원인: 저장 전 엔티티는 id 가 없어 시도를 못 만듦
  • 컴파일 경고 2건 정리
    • 원인: Spring 7 의 TransactionTemplate 이 non-null 을 돌려줘 Elvis 가 불필요해짐
  • 최신 dev(파싱 전환에서 남은 옛 테이블·칼럼 제거 #1260) 머지
    • 아웃박스 코드와 겹침 없음

Result

음성 대조

임시로 뺀 것 결과
RC RC 검사 단독 실패 (2초 초과)
잠금 건너뛰기 잠금 건너뛰기 검사 단독 실패
바깥 트랜잭션 거부 바깥 트랜잭션 거부 검사 단독 실패
시도의 회수 횟수 비교 펜싱 검사 단독 실패
인덱스 기대값 인덱스 검사 단독 실패
  • RC·인덱스는 리뷰 반영 뒤 구조에서 다시 확인

위험

  • 하위 큐에 프록시를 거는 어노테이션을 붙이면 집기 첫 호출에서 NPE
    • 원인: 하위 클래스가 RC 를 못 빼게 집기를 final 로 둠
    • 계약 테스트가 있으면 CI 에서 드러남
  • 집기·회수 조회의 호출 위치는 주석으로만 제한
    • 다른 서비스가 JPA 저장소를 주입해 기본 격리 수준 트랜잭션 안에서 부를 수 있음
    • 지금 그 저장소를 주입받는 곳은 공통 큐와 저장소 구현체 둘
  • 새 아웃박스의 계약 테스트 작성은 강제 없음
    • 빠뜨리면 위 NPE 도 CI 에서 드러나지 않음
  • 회수 판정이 updated_at 을 집은 시각으로 씀
    • 처리 중에 행을 고치는 아웃박스는 그만큼 회수가 늦어짐
  • 계약 테스트가 공유 테이블에 남은 남의 PENDING·멈춘 행까지 집거나 회수함
    • 지운 기존 집기 테스트와 같은 성격

후속

  • 두 번째 아웃박스로 푸시 발송 검토
    • 지금은 커밋 뒤 비동기 발송이라 서버가 내려가면 푸시 유실
  • claimed_at 칼럼으로 회수 기준 분리 검토
  • 메타 테스트 2건 검토
    • 집기·회수 조회를 공통 패키지 밖에서 부르면 실패
    • 아웃박스 엔티티마다 계약 테스트 존재 확인
  • 루프 공통화 여부는 두 번째 사용처에서 결정

연관 이슈

- common/outbox 패키지 추가
  - Outbox: 상태 전이·회수 상한·펜싱을 맡는 @MappedSuperclass
  - OutboxJpaRepository: SKIP LOCKED 집기·회수 쿼리를 #{#entityName} 으로 한 번만 선언
  - OutboxQueue: 집기·되돌리기·회수를 RC 트랜잭션으로 엶. 회수 상한 동작은 하위 클래스가 onExhausted 로 구현
  - OutboxPolicy: 회수 기준·회수 상한·처리 상한 묶음
  - OutboxRepositoryAdapter: 도메인 저장소가 save·findHeldForUpdate 를 상속
- 파싱 아웃박스를 공통 구조 위로 이전
  - 마이그레이션 0·의도한 동작 변화 0
- RC 를 @transactional 대신 TransactionTemplate 으로 엶
  - 원인: 프록시를 거치지 않는 방식 필요
    - Kotlin 추상 클래스 메서드는 final 이라 프록시가 못 가로채면 isolation 이 조용히 빠짐
    - 테스트가 저장소를 감싼 큐를 직접 만들어도 RC 가 그대로 걸림
- 바깥 트랜잭션 안에서의 집기·되돌리기·회수 호출 거부
  - 원인: Spring 이 기존 트랜잭션에 합류하면 isolation 설정을 무시함
- 집기·회수 잠금 조회를 도메인 저장소에서 제거
  - 원인: 서비스 트랜잭션 안에서 직접 부르면 RR gap 잠금이 돌아옴
- 성공·실패·되돌리기 전이가 시도를 받아 행을 쥔 시도만 반영
  - 이전: 호출하는 쪽이 findHeldForUpdate 를 골라 써야 펜싱이 걸림
- 처리 상한 검사를 스케줄러 init 에서 OutboxPolicy 생성자로 이관
- 계약 테스트 OutboxQueueContract 추가
  - JUnit 인터페이스 default @test 5건: RC·SKIP LOCKED·펜싱·바깥 트랜잭션 거부·(status, created_at) 인덱스
  - 대안: 추상 테스트 클래스 상속
    - 기각: 메타 테스트가 IntegrationTest 파일에 IntegrationTestSupport 직접 상속을 요구함
  - 결과: 보장을 하나씩 뺀 음성 대조에서 그 보장의 테스트만 실패
  - ItemParsingClaimConcurrencyIntegrationTest 는 계약으로 흡수
- dispatch·recover @scheduled 루프는 파싱 도메인에 남김
  - 원인: 주기·풀 크기·처리 상한이 도메인 값
- 원인: Spring 7 의 TransactionTemplate.execute 가 콜백의 non-null 타입을 그대로 돌려줌
  - 결과: Elvis 가 항상 왼쪽을 돌려준다는 컴파일 경고 2건
@m-a-king m-a-king added the refactor 구조 개선, 외부 동작 불변 label Oct 31, 2026
@m-a-king m-a-king linked an issue Oct 31, 2026 that may be closed by this pull request
@m-a-king m-a-king self-assigned this Oct 31, 2026
@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@m-a-king
m-a-king merged commit 78d4230 into dev Oct 31, 2026
7 checks passed
@m-a-king
m-a-king deleted the refactor/1261-outbox-common-structure branch October 31, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 구조 개선, 외부 동작 불변

Projects

None yet

Development

Successfully merging this pull request may close these issues.

아웃박스 공통 구조 분리 (1단계)

1 participant