TIL - REQUIRES_NEW 자기 호출 시, 순환 참조 발생

2026. 5. 15. 05:19·내배캠

지정가 PENDING 무제한 요청 가능 + 스케줄러 좀비 SUCCESS

Problem

보유하지 않은 주식종목으로 SELL 지정가를 반복 전송할 수 있었고, /api/trades/pending 응답에 phantom 6건이 쌓여 있었다. 게다가 가격조건 만족 시 스케줄러가 체결을 시도하면 자산은 변하지 않았는데 trade 만 SUCCESS 로 박히는 좀비 데이터까지 발생했다.

Analyze

세 가지가 동시에 깨져 있었다.

(1) PENDING 진입 시 검증 없음 — validateOrderRequest 는 limitPrice/stockAmount 만 본다. 보유/잔액 검증은 asset-service 호출 시점에 일어나는데, PENDING 분기는 asset-service 를 호출조차 안 하고 DB INSERT 만 한다.

(2) 스케줄러가 X-User-Id 누락 — executeAsset(order, currentPrice, null) 로 userId=null 을 넘김. asset-service @RequestHeader("X-User-Id") 가 못 받아서 400. 보유 검증 로직에 도달하기도 전에 거부된다.

(3) SUCCESS 선마킹 — 스케줄러가 executeAsset 호출 전에 updateStatus(updateSuccess) 를 먼저 호출. 외부 호출이 실패해도 catch 가 예외만 삼키고 트랜잭션 롤백을 안 시켜서, 메서드 종료 시 잘못된 SUCCESS 가 DB 로 flush 된다.

Action

셋을 묶어서 한 번에 수정했다.

// (1) 접수 시점 사전 검증
private void validateSufficientFundsOrHoldings(...) {
    if (tradeType == TradeType.SELL) {
        int held = assetClient.getHoldingQuantity(accountId, stockCode);
        if (held < amount) throw new BusinessException(INSUFFICIENT_HOLDINGS);
    } else {
        long balance = assetClient.getAccountBalance(accountId);
        if (balance < required) throw new BusinessException(INSUFFICIENT_BALANCE);
    }
}

// (2) Trade 에 userId 필드 추가 → 스케줄러가 trade.userId() 사용
public record Trade(UUID tradeId, UUID accountId, UUID userId, ...) { ... }

// (3) executeAsset 먼저, 성공 시 SUCCESS, 실패 시 FAIL. 건별 독립 트랜잭션.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void executePendingLimitOrder(Trade order, double currentPrice) {
    try {
        executeAsset(order, currentPrice, order.userId());
    } catch (Exception e) {
        tradeRepository.updateStatus(Trade.updateFail(order));
    }
}

asset-service 에는 X-User-Id 가 필요 없는 내부 엔드포인트도 새로 열었다
(/internal/holdings/quantity, /internal/accounts/balance).

Result

보유 없는 종목 SELL 지정가는 접수 단계에서 400 으로 거부된다. 스케줄러는 trade.userId() 로 asset-service 를 정상 호출하고, 실패해도 trade 가 FAIL 로 정확히 기록된다. 좀비 SUCCESS 가 더 이상 안 생긴다.


REQUIRES_NEW 자기호출과 @Lazy 함정

Problem

위 (3) 을 위해 건별 REQUIRES_NEW 트랜잭션을 적용했는데 Spring 부팅 자체가 실패했다.

APPLICATION FAILED TO START
The dependencies of some of the beans in the application context form a cycle:
   limitOrderScheduler
┌─────┐
|  tradeServiceImpl
└─────┘

Analyze

@Transactional 은 Spring AOP 프록시로 동작하는데, 같은 클래스 안에서 this.method() 로 직접 호출하면 프록시를 우회하기 때문에 REQUIRES_NEW 가 무시된다. 그래서 자기 자신을 다시 빈으로 주입받아 그 참조(=프록시) 로 호출해야 한다.
그런데 그러면 빈 그래프가 자기 자신을 가리키므로 순환참조가 된다.
그래서 순환을 끊으려고 @Lazy 를 붙였다.

@Lazy
private final TradeService self;

그런데 Lombok @RequiredArgsConstructor 가 만들어주는 생성자는 필드의 @Lazy 를 파라미터로 옮겨주지 않는다.

public TradeServiceImpl(..., TradeService self) {   // @Lazy 어디감?
    this.self = self;
}

Spring 은 @Lazy 가 생성자 파라미터에 붙어 있어야만 lazy proxy 를 주입한다.
결과적으로 일반 의존성으로 처리돼서 순환참조 검출에 그대로 걸린다.

Action

ObjectProvider<TradeService> 로 갈아탔다. 타입 자체가 lazy lookup 래퍼라 어노테이션 전파 이슈에서 자유롭다.

// 수정 전: @Lazy private final TradeService self;
// 수정 후:
private final ObjectProvider<TradeService> selfProvider;

// 호출 시점에 컨테이너에서 빈 획득
selfProvider.getObject().executePendingLimitOrder(order, currentPrice);

생성자 주입 시점에 ObjectProvider 자체만 주입되고, 실제 TradeService 빈은 getObject() 시점에 조회된다.
그땐 자기 자신의 생성이 이미 끝나 있으니 순환이 발생하지 않는다.

Result

부팅 성공. REQUIRES_NEW 가 정상 작동해서 건별 독립 트랜잭션이 보장된다.
Lombok 환경에서 @Lazy 를 꼭 쓰고 싶다면?

  • onConstructor_ 옵션 (모든 파라미터에 적용되는 부작용)
  • 수동 생성자 작성
  • 필드 주입 + @Autowired @Lazy

셋 다 단점이 있어서 표준 API 인 ObjectProvider 가 가장 깔끔하다는 결론을 냈다.

'내배캠' 카테고리의 다른 글

[내일배움캠프] 인생에서 목표설정의 중요성  (0) 2026.05.17
TIL - 지정가 매매시, 검증 허점 보완  (0) 2026.05.15
TIL - 지정가 매매 시 트러블 슈팅  (0) 2026.05.14
TIL - DB/Redis/Kafka 분산트랜잭션 대응  (0) 2026.05.13
TIL - 작업하던 브랜치에서 바로 git pull origin dev를 하면?  (0) 2026.05.08
'내배캠' 카테고리의 다른 글
  • [내일배움캠프] 인생에서 목표설정의 중요성
  • TIL - 지정가 매매시, 검증 허점 보완
  • TIL - 지정가 매매 시 트러블 슈팅
  • TIL - DB/Redis/Kafka 분산트랜잭션 대응
MvA
MvA
백엔드 개발자 김재현입니다. 주로 공부하면서 느낀점을 기록합니다.
  • MvA
    Man vs Ai
    MvA
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • Java (6)
      • Python (8)
        • 딥러닝 (1)
        • 머신러닝 (7)
      • JavaScript (2)
      • 내배캠 (60)
      • 개인 프로젝트 (11)
      • 책 후기 (5)
      • 기타 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    내일배움캠프
    Riot API
    아키텍처
    배포
    딥러닝
    머신러닝
    TiL
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
MvA
TIL - REQUIRES_NEW 자기 호출 시, 순환 참조 발생
상단으로

티스토리툴바