본문 바로가기

REQUIRES_NEW의 독립 자원

REQUIRES_NEW가 별도 연결과 물리 트랜잭션을 요구하는 이유를 확인하고 풀 용량·미커밋 가시성·잠금 비용을 계산합니다.

REQUIRES_NEW의 독립 완료는 데이터베이스 isolation level을 높이는 것과 구별합니다. 외부와 다른 물리 트랜잭션과 연결을 사용해 커밋 운명을 분리한다는 뜻입니다.

외부 연결은 반납되지 않은 채 보류되고, 내부 호출은 풀에서 연결 하나를 더 빌립니다. 그러므로 이 전파를 선택할 때는 독립 커밋뿐 아니라 동시 연결 수, 미커밋 행의 가시성, 잠금과 타임아웃까지 한 묶음으로 검토해야 합니다.

REQUIRES_NEW의 핵심은 격리 수준이 아니라 독립 연결이다

외부 연결 A 보류·내부 B 획득·B 완료와 반환·A 재개를 같은 연결 위치에서 따라갈 수 있다.

REQUIRES_NEW · TWO CONNECTIONS + RESOURCE BUDGET

외부 연결을 보류한 동안 내부 연결을 더 빌린다

REQUIRES_NEW는 독립 물리 완료를 선택합니다. 연결 A는 풀에 반환되지 않고 B를 추가 획득하므로 동시 자원 수와 가시성을 함께 계산합니다.

외부 연결 A 보류, 내부 연결 B 완료, 외부 A 재개외부 A가 연결을 보유한 채 보류되고 내부는 새 연결 B를 빌린다. B 커밋 또는 롤백이 끝나면 B를 반환하고 A를 재개한다. 한순간 두 연결이 동시에 필요하다.연결 A 보류외부가 계속 보유연결 B 사용내부 commit / rollback연결 A 재개B 반환 뒤 외부 계속동시에 필요한 자원: 보류된 A + 현재 사용 중인 BB 커밋은 A 롤백으로 취소되지 않음 · Java 예외는 전파될 수 있음
보류된 A와 현재 B의 동시 보유원desktop3노드2간선과같이A보류→B사용→B반환후A재개를그린다. B사용노드에A계속보유를명시해한순간두연결임을보인다. Java의순차실행은동시자원보유를없애지않고독립commit은예외전파를자동차단하지않는다.
연결 A 보류외부가 계속 보유
B 추가B 추가연결 A 보류 · 외부가 계속 보유 → 연결 B 사용 · A도 계속 보유 · B commit / rollback
연결 B 사용A도 계속 보유B commit / rollback
B 반환B 반환연결 B 사용 · A도 계속 보유 · B commit / rollback → B 반환 · A 재개 · 외부 계속 실행
B 반환 · A 재개외부 계속 실행
  1. A 보류 → B 획득

    외부 A는 풀에 반환되지 않습니다. 내부 B와 한순간 두 연결을 점유합니다.

  2. B 완료·반환 → A 재개

    B 커밋은 이후 A 롤백으로 취소되지 않습니다. 내부 Java 예외를 처리하지 않으면 외부도 롤백할 수 있습니다.

풀 수요 · 비관적 상한

T × (1 + D) + R

24 × (1 + 1) + 4 = 52

T는 외부 동시 요청, D는 최대 중첩 깊이, R은 예약 연결입니다. 자동 안전 보장이 아니라 모든 요청이 함께 중첩될 때의 출발 모델입니다.

가시성 · READ_COMMITTED

B에서는 A의 미커밋 행이 보이지 않습니다. 같은 키나 외래 키 확인은 DB 구현에 따라 잠금을 기다릴 수 있습니다. 불변 requestId·actorId·outcome을 전달하고 운영 DB에서 두 연결로 검증합니다.

같은 DB·계정·풀의 장애는 공유합니다. 수요가 DB 상한을 넘으면 동시성 제한이나 외부 완료 뒤 영속 전달로 구조를 바꿉니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 외부 A가 연결을 보유한 채 보류되고 내부는 새 연결 B를 빌린다. B 커밋 또는 롤백이 끝나면 B를 반환하고 A를 재개한다. 한순간 두 연결이 동시에 필요하다.
  • 원래 가로 도표의 3노드와 2간선과 같이 A 보류→B 사용→B 반환 후 A 재개를 그린다. B 사용 노드에 A 계속 보유를 명시해 한순간 두 연결을 보유함을 보인다. Java의 순차 실행은 동시 자원 보유를 없애지 않고 독립 commit은 예외 전파를 자동 차단하지 않는다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1REQUIRES_NEW의 핵심은 격리 수준이 아니라 독립 연결이다
16원래 가로 도표의 3노드와 2간선과 같이 A 보류→B 사용→B 반환 후 A 재개를 그린다. B 사용 노드에 A 계속 보유를 명시해 한순간 두 연결을 보유함을 보인다. Java의 순차 실행은 동시 자원 보유를 없애지 않고 독립 commit은 예외 전파를 자동 차단하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
외부 A 보류

외부 transaction이 연결 A를 가진 채 보류됩니다.

A: 보유·보류 · B: 아직 없음외부 연결을 반환하지 않고 안쪽으로 갑니다.
내부 B 실행

독립 내부 transaction이 새 연결 B를 빌립니다.

A: 계속 보유 · B: 새 연결 · 동시 필요: 두 연결내부 업무가 다른 물리 연결을 사용합니다.
B 반환과 A 재개

B의 commit·rollback 뒤 B를 반환하고 A를 재개합니다.

B: 완료·반환 · A: 재개내부 연결 소유가 끝나 외부 실행이 돌아옵니다.

REQUIRES_NEW의 핵심은 격리 수준이 아니라 독립 연결이다

외부 연결 A 보류·내부 B 획득·B 완료와 반환·A 재개를 같은 연결 위치에서 따라갈 수 있다.

REQUIRES_NEW · TWO CONNECTIONS + RESOURCE BUDGET

외부 연결을 보류한 동안 내부 연결을 더 빌린다

REQUIRES_NEW는 독립 물리 완료를 선택합니다. 연결 A는 풀에 반환되지 않고 B를 추가 획득하므로 동시 자원 수와 가시성을 함께 계산합니다.

외부 연결 A 보류, 내부 연결 B 완료, 외부 A 재개외부 A가 연결을 보유한 채 보류되고 내부는 새 연결 B를 빌린다. B 커밋 또는 롤백이 끝나면 B를 반환하고 A를 재개한다. 한순간 두 연결이 동시에 필요하다.연결 A 보류외부가 계속 보유연결 B 사용내부 commit / rollback연결 A 재개B 반환 뒤 외부 계속동시에 필요한 자원: 보류된 A + 현재 사용 중인 BB 커밋은 A 롤백으로 취소되지 않음 · Java 예외는 전파될 수 있음
보류된 A와 현재 B의 동시 보유원desktop3노드2간선과같이A보류→B사용→B반환후A재개를그린다. B사용노드에A계속보유를명시해한순간두연결임을보인다. Java의순차실행은동시자원보유를없애지않고독립commit은예외전파를자동차단하지않는다.
연결 A 보류외부가 계속 보유
B 추가B 추가연결 A 보류 · 외부가 계속 보유 → 연결 B 사용 · A도 계속 보유 · B commit / rollback
연결 B 사용A도 계속 보유B commit / rollback
B 반환B 반환연결 B 사용 · A도 계속 보유 · B commit / rollback → B 반환 · A 재개 · 외부 계속 실행
B 반환 · A 재개외부 계속 실행
  1. A 보류 → B 획득

    외부 A는 풀에 반환되지 않습니다. 내부 B와 한순간 두 연결을 점유합니다.

  2. B 완료·반환 → A 재개

    B 커밋은 이후 A 롤백으로 취소되지 않습니다. 내부 Java 예외를 처리하지 않으면 외부도 롤백할 수 있습니다.

풀 수요 · 비관적 상한

T × (1 + D) + R

24 × (1 + 1) + 4 = 52

T는 외부 동시 요청, D는 최대 중첩 깊이, R은 예약 연결입니다. 자동 안전 보장이 아니라 모든 요청이 함께 중첩될 때의 출발 모델입니다.

가시성 · READ_COMMITTED

B에서는 A의 미커밋 행이 보이지 않습니다. 같은 키나 외래 키 확인은 DB 구현에 따라 잠금을 기다릴 수 있습니다. 불변 requestId·actorId·outcome을 전달하고 운영 DB에서 두 연결로 검증합니다.

같은 DB·계정·풀의 장애는 공유합니다. 수요가 DB 상한을 넘으면 동시성 제한이나 외부 완료 뒤 영속 전달로 구조를 바꿉니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 외부 A가 연결을 보유한 채 보류되고 내부는 새 연결 B를 빌린다. B 커밋 또는 롤백이 끝나면 B를 반환하고 A를 재개한다. 한순간 두 연결이 동시에 필요하다.
  • 원래 가로 도표의 3노드와 2간선과 같이 A 보류→B 사용→B 반환 후 A 재개를 그린다. B 사용 노드에 A 계속 보유를 명시해 한순간 두 연결을 보유함을 보인다. Java의 순차 실행은 동시 자원 보유를 없애지 않고 독립 commit은 예외 전파를 자동 차단하지 않는다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1REQUIRES_NEW의 핵심은 격리 수준이 아니라 독립 연결이다
16원래 가로 도표의 3노드와 2간선과 같이 A 보류→B 사용→B 반환 후 A 재개를 그린다. B 사용 노드에 A 계속 보유를 명시해 한순간 두 연결을 보유함을 보인다. Java의 순차 실행은 동시 자원 보유를 없애지 않고 독립 commit은 예외 전파를 자동 차단하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
외부 A 보류

외부 transaction이 연결 A를 가진 채 보류됩니다.

A: 보유·보류 · B: 아직 없음외부 연결을 반환하지 않고 안쪽으로 갑니다.
내부 B 실행

독립 내부 transaction이 새 연결 B를 빌립니다.

A: 계속 보유 · B: 새 연결 · 동시 필요: 두 연결내부 업무가 다른 물리 연결을 사용합니다.
B 반환과 A 재개

B의 commit·rollback 뒤 B를 반환하고 A를 재개합니다.

B: 완료·반환 · A: 재개내부 연결 소유가 끝나 외부 실행이 돌아옵니다.

한 호출 스레드가 두 연결을 점유한다

외부 REQUIRED가 시작되면 연결 A가 현재 스레드에 바인딩됩니다. 내부 REQUIRES_NEW 경계에 들어갈 때 Spring은 A와 외부 트랜잭션 상태를 보류하고 연결 B로 새 물리 트랜잭션을 시작합니다.

내부가 끝나면 B를 커밋하거나 롤백하고 A를 복원합니다. Java 호출은 순차지만 한순간에는 다음 두 리소스가 동시에 필요합니다.

  • 보류됐지만 아직 반환되지 않은 외부 연결 A
  • 내부 트랜잭션이 현재 사용하는 연결 B

내부가 커밋된 뒤 외부가 롤백하면 B의 행은 남고 A의 행은 사라집니다. 이는 독립 커밋의 의도된 결과입니다. 내부 커밋이 외부 롤백과 함께 취소된다고 설명하면 안 됩니다.

반대로 내부 예외가 호출자에게 전파되면 외부 인터셉터도 런타임 예외를 보고 롤백할 수 있습니다. 물리 트랜잭션의 독립성은 Java 예외 전파를 자동으로 차단하지 않습니다. 외부가 내부 실패를 허용할 때만 구체적인 실패 타입을 좁게 처리하고 대체 경로를 명시합니다.


풀 용량은 동시성과 중첩 깊이로 계산한다

외부 트랜잭션 동시 요청을 T, 요청 하나가 동시에 보유할 수 있는 최대 REQUIRES_NEW 중첩 깊이를 D, 스케줄러·상태 확인 등에 예약할 연결을 R이라고 하면 비관적 출발점은 다음과 같습니다.

필요 연결 수 = T × (1 + D) + R

예를 들어 외부 요청 24개, 깊이 1, 예약 4개면 24 × 2 + 4 = 52입니다. 이 값은 자동으로 안전을 보장하는 정답이 아니라, 모든 외부가 연결을 잡고 같은 순간 내부 연결을 기다리는 상황을 닫기 위한 상한 모델입니다.

풀 크기가 T뿐이면 모든 요청이 A를 점유한 채 B를 기다리는 교착성 고갈이 생길 수 있습니다. DB 계정 최대 연결, 다른 애플리케이션 인스턴스, 배치 작업과 장애 시 재시도를 함께 넣어야 합니다.

수요가 DB 상한을 넘으면 타임아웃 숫자만 바꿔 해결할 수 없습니다. REQUIRES_NEW 작업을 외부 완료 뒤 영속적 대기열로 넘기거나, 요청 동시성을 제한하거나, 데이터 흐름 자체를 다시 설계합니다. 연결 획득 타임아웃은 HTTP 기한보다 짧게 두어 무한 대기를 실패 신호로 바꿀 뿐 수요를 없애지는 않습니다.

중첩 깊이를 늘린 예산 계산 외부 동시 요청 24개, 최대 깊이 2, 예약 4개이고 이 애플리케이션에 쓸 수 있는 연결이 70개라면 수요는 몇 개인가요? 깊이와 예약을 유지한 채 상한 모델을 넘지 않을 최대 정수 요청 동시성도 계산하세요.

동시에 보유하는 세 연결의 풀이

깊이 2에서는 외부 A, 보류된 첫 내부 B, 현재 두 번째 내부 C까지 요청당 세 연결을 보유할 수 있습니다. 수요는 24 × (1 + 2) + 4 = 76으로 70을 넘습니다. 동시성 T를 정수로 제한하려면 3T + 4 ≤ 70에서 T는 최대 22입니다. 이는 최대 중첩이 동시에 일어난다는 출발 모델이며 측정된 처리량이나 최적 풀 크기가 아닙니다. 다른 인스턴스가 같은 DB 연결 한도를 나눠 쓰는지도 별도로 반영합니다.


독립 연결은 외부 미커밋 상태를 보지 못한다

연결 B는 연결 A가 아직 커밋하지 않은 행을 일반적인 READ_COMMITTED 경계에서 볼 수 없습니다. 내부 감사가 외부에서 막 삽입한 게시글을 외래 키로 참조하면 행을 찾지 못하거나, 제약 확인과 잠금 구현에 따라 기다리다 타임아웃될 수 있습니다.

외부가 어떤 키를 갱신하며 잠금을 보유하고 내부가 같은 키를 읽거나 수정하면 Java 코드가 순차여도 DB에서는 서로 다른 트랜잭션의 잠금 경쟁입니다. 내부 작업에는 요청 ID, 행위자 ID, 작업 코드처럼 이미 확정됐거나 호출 시점에 복사한 불변 값을 전달합니다. 외부가 변경 중인 관리 엔티티를 다시 조회하게 만들지 않습니다.

외부와 내부는 각자의 isolation level과 스냅샷을 가질 수 있습니다. 하지만 REQUIRES_NEW의 목적을 “더 강한 격리가 필요해서”라고 설명하면 전파와 격리를 섞게 됩니다. 필요한 것은 독립 완료이고, 다른 가시성은 그 결과로 따라오는 제약입니다.

H2 단일 테스트 결과로 PostgreSQL이나 MySQL의 잠금 대기와 deadlock victim 선택을 단정하지 않습니다. 운영 데이터베이스에서 두 연결을 실제로 열어 시간 초과와 잠금 순서를 검증합니다.


실행으로 자원 경계를 관찰한다

다음 Java 25 테스트는 Spring Boot 4.1.1, Spring Framework 7.0.9, JUnit 6.0.3 기준입니다. 외부와 내부에서 실제 JDBC 연결 객체의 식별자를 기록하고, 내부가 외부 미커밋 게시글을 보지 못하지만 감사 행은 독립 커밋함을 검증합니다.

두 번째 테스트는 풀 예산 공식을 실행 가능한 값 객체로 고정합니다. 앞에서 사용한 Spring JDBC·H2·JUnit 의존성이 준비된 프로젝트에서 이 테스트 클래스와 설정 빈을 함께 실행합니다.

src/test/java/board/tx/propagation/requiresnew/RequiresNewResourceTest.java
package board.tx.propagation.requiresnew;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.sql.Connection;
import javax.sql.DataSource;

import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

class RequiresNewResourceTest {
    private AnnotationConfigApplicationContext context;
    private JdbcTemplate jdbc;

    @BeforeEach
    void openContext() {
        context = new AnnotationConfigApplicationContext(Config.class);
        jdbc = context.getBean(JdbcTemplate.class);
        jdbc.execute("drop table if exists audit_log");
        jdbc.execute("drop table if exists posts");
        jdbc.execute("create table posts(id bigint primary key)");
        jdbc.execute("create table audit_log(id bigint primary key, outcome varchar(24))");
    }

    @AfterEach
    void closeContext() {
        context.close();
    }

    @Test
    void requiresNewUsesAnotherConnectionAndCommitsIndependently() {
        var observation = context.getBean(ConnectionObservation.class);

        assertThrows(
                ExpectedOuterFailure.class,
                () -> context.getBean(OuterWork.class).writeAuditThenFail(41L));

        assertNotSame(observation.outerConnection, observation.innerConnection);
        assertEquals(0, observation.outerRowsVisibleToInner);
        assertEquals(0, jdbc.queryForObject(
                "select count(*) from posts", Integer.class));
        assertEquals(1, jdbc.queryForObject(
                "select count(*) from audit_log", Integer.class));
    }

    @Test
    void poolBudgetIncludesEveryHeldOuterAndInnerConnection() {
        assertEquals(52, new PoolBudget(24, 1, 4).pessimisticMinimum());

        int demand = new PoolBudget(40, 1, 6).pessimisticMinimum();
        assertEquals(86, demand);
        assertTrue(demand > 70, "the database limit requires a redesign");
    }

    record PoolBudget(
            int concurrentOuterTransactions,
            int maximumRequiresNewDepth,
            int reservedConnections
    ) {
        PoolBudget {
            if (concurrentOuterTransactions < 1
                    || maximumRequiresNewDepth < 0
                    || reservedConnections < 0) {
                throw new IllegalArgumentException("invalid pool inputs");
            }
        }

        int pessimisticMinimum() {
            return Math.addExact(
                    Math.multiplyExact(
                            concurrentOuterTransactions,
                            1 + maximumRequiresNewDepth),
                    reservedConnections);
        }
    }

    public static class ConnectionObservation {
        private Connection outerConnection;
        private Connection innerConnection;
        private int outerRowsVisibleToInner;
    }

    public static class IndependentAudit {
        private final JdbcTemplate jdbc;
        private final ConnectionObservation observation;

        IndependentAudit(JdbcTemplate jdbc, ConnectionObservation observation) {
            this.jdbc = jdbc;
            this.observation = observation;
        }

        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void record(long id) {
            observation.innerConnection = currentConnection(jdbc);
            observation.outerRowsVisibleToInner = jdbc.queryForObject(
                    "select count(*) from posts where id = ?",
                    Integer.class,
                    id);
            jdbc.update(
                    "insert into audit_log(id, outcome) values (?, 'ATTEMPTED')",
                    id);
        }
    }

    public static class OuterWork {
        private final JdbcTemplate jdbc;
        private final IndependentAudit audit;
        private final ConnectionObservation observation;

        OuterWork(
                JdbcTemplate jdbc,
                IndependentAudit audit,
                ConnectionObservation observation
        ) {
            this.jdbc = jdbc;
            this.audit = audit;
            this.observation = observation;
        }

        @Transactional
        public void writeAuditThenFail(long id) {
            observation.outerConnection = currentConnection(jdbc);
            jdbc.update("insert into posts(id) values (?)", id);
            audit.record(id);
            throw new ExpectedOuterFailure();
        }
    }

    static Connection currentConnection(JdbcTemplate jdbc) {
        Connection connection = jdbc.execute(
                (ConnectionCallback<Connection>) current -> current);
        if (connection == null) {
            throw new IllegalStateException("connection callback returned null");
        }
        return connection;
    }

    static final class ExpectedOuterFailure extends RuntimeException {
        private static final long serialVersionUID = 1L;
    }

    @Configuration
    @EnableTransactionManagement(proxyTargetClass = true)
    static class Config {
        @Bean
        DataSource dataSource() {
            var source = new JdbcDataSource();
            source.setURL("jdbc:h2:mem:b92_requires_new;DB_CLOSE_DELAY=-1");
            return source;
        }

        @Bean
        PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }

        @Bean
        JdbcTemplate jdbcTemplate(DataSource dataSource) {
            return new JdbcTemplate(dataSource);
        }

        @Bean
        ConnectionObservation connectionObservation() {
            return new ConnectionObservation();
        }

        @Bean
        IndependentAudit independentAudit(
                JdbcTemplate jdbc,
                ConnectionObservation observation
        ) {
            return new IndependentAudit(jdbc, observation);
        }

        @Bean
        OuterWork outerWork(
                JdbcTemplate jdbc,
                IndependentAudit audit,
                ConnectionObservation observation
        ) {
            return new OuterWork(jdbc, audit, observation);
        }
    }
}

원 PoolBudget 테스트는 깊이 1의 보통 입력을 확인합니다. 모든 int 입력의 넘침까지 검증한 예제는 아닙니다. 특히 1 + maximumRequiresNewDepth는 일반 int 덧셈이어서 최대 정수 깊이를 허용하면 먼저 음수로 넘친 뒤 바깥 exact 연산이 놓칠 수 있습니다. 외부 설정값을 받는 구현에서는 현실적인 깊이 상한을 검증하거나, 이 안쪽 덧셈도 Math.addExact(1, maximumRequiresNewDepth)로 계산해야 합니다. 곱셈에만 exact를 사용했다는 이유로 식 전체가 안전한 것은 아닙니다.


완료 행렬은 예외 전파까지 포함한다

외부 결과내부 결과최종 상태호출자가 확인할 조건
커밋커밋main + audit정상 경로
롤백커밋audit만 남음독립 기록의 의미가 “시도”인가
커밋롤백main만 남을 수 있음내부 예외를 명시적으로 허용·처리했는가
롤백롤백둘 다 없음각 물리 트랜잭션의 실패 원인

REQUIRES_NEW를 붙였다는 사실만으로 세 번째 행이 만들어지지는 않습니다. 내부 런타임 예외를 외부가 처리하지 않으면 외부도 롤백될 가능성이 큽니다.


운영 관찰과 구조 변경 기준

메트릭에는 작업별 내부 트랜잭션 수, 연결 획득 시간, 풀의 활성·유휴·대기 수, 내부 지속 시간, 타임아웃과 실패 타입을 둡니다. 요청 ID와 트랜잭션 ID는 고카디널리티 메트릭 태그가 아니라 추적 이벤트에 기록합니다.

별도 연결은 외부 롤백과 커밋 운명을 분리할 뿐 같은 데이터베이스 장애, 같은 계정 제한, 같은 풀 장애를 분리하지 않습니다. 규제 감사처럼 DB 전체 장애에도 남아야 하는 기록은 별도 추가 전용 수집 대상과 보존·접근 제어가 필요합니다.

동시 요청 40개, 깊이 1, 예약 연결 6개인데 DB 계정 상한이 70이라면 수요는 86입니다. 풀 숫자 조정만으로는 불가능합니다. 외부 트랜잭션 밖의 영속적 전달 또는 동시성 제한으로 구조를 바꾸는 것이 답입니다.