본문 바로가기

검사 예외 전파

검사 예외의 문법과 회복 가능성을 분리하고 JDBC 실패를 어댑터의 행동 경계에서 원인을 보존해 번역합니다.

검사 예외(checked exception)는 호출한 쪽에 처리 문법을 강제하지만, 그 실패가 회복할 수 있는 것인지는 알려 주지 않습니다. 회원 게시판의 애플리케이션 포트가 SQLException을 선언하면, 저장 기술을 바꾸는 일이 사용 사례와 전달 계층의 시그니처 변경으로 번집니다. 반대로 어댑터 안에서 가장 좁은 동작 단위로 기술적인 실패를 바꿔 주면, 애플리케이션은 앞 장에서 정한 공개 영속성 예외만 보게 됩니다.

검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다

SQLException의 기술 실패가 원인 보존 application 예외·사용 사례 행동·안전 응답으로 변환된다.

CHECKED FAILURE · ACTION BOUNDARY

기술 실패를 행동 계약으로 바꾸는 경계

JDBC의 SQLException은 어댑터 안에서 처리합니다. 원인은 보존하고 공개 계약에는 충돌·중단·제한된 재시도처럼 가능한 행동을 담습니다.

발생, 번역, 행동 선택, 응답의 네 책임JDBC 동작에서 발생한 SQLException을 어댑터가 원인 보존 애플리케이션 예외로 번역한다. 사용 사례가 다음 행동을 고르고 전달 계층이 안전한 응답을 만든다.JDBC 동작SQLException어댑터 번역작업 이름 + cause사용 사례다음 행동 선택전달 계층안전한 응답
네 실패 처리 책임의 순서원 desktop의네노드세간선과같다. JDBC기술실패→어댑터cause보존번역→사용사례다음행동→전달계층안전표현이다. 이 그림은실패처리책임이며commit/rollback실행순서를증명하지않는다.
JDBC 동작SQLException
연결 관계JDBC 동작 · SQLException → 어댑터 번역 · 작업 이름 · cause
어댑터 번역작업 이름 · cause
연결 관계어댑터 번역 · 작업 이름 · cause → 사용 사례 · 다음 행동
사용 사례다음 행동
연결 관계사용 사례 · 다음 행동 → 전달 계층 · 안전한 응답
전달 계층안전한 응답
  1. JDBC 동작 → 어댑터 번역

    SQLException의 SQLState·공급자 코드·원인은 cause에 남기고, 작업 이름을 고정한 PostPersistenceException으로 감쌉니다.

  2. 사용 사례 → 전달 계층

    정확한 중복·중단·조건을 충족한 읽기 재시도를 고른 뒤 안전한 외부 응답으로 표현합니다.

내부 진단은 보존

SQLState · 공급자 코드 · cause

애플리케이션 포트는 throws SQLException을 선언하지 않습니다. 요청 값·SQL을 새 예외 메시지에 복사하지 않습니다.

외부 표현은 별도 책임

중복 충돌 · 가용성 실패의 안전한 표현

409·503은 전달 계층의 후보입니다. 중립 예외에 HTTP 상태를 박아 넣거나 공급자 메시지를 응답하지 않습니다.

checked / unchecked 문법만으로 복구 가능성을 판단하지 않습니다. 발생 위치와 다음 행동을 분리해 정합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • JDBC 동작에서 발생한 SQLException을 어댑터가 원인 보존 애플리케이션 예외로 번역한다. 사용 사례가 다음 행동을 고르고 전달 계층이 안전한 응답을 만든다.
  • 원래 가로 도표의 네 노드와 세 간선과 같다. JDBC 기술 실패→어댑터의 cause 보존 번역→사용 사례의 다음 행동→전달 계층의 안전 표현이다. 이 그림은 실패 처리 책임을 나타내며 commit/rollback 실행 순서를 증명하지 않는다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다
17원래 가로 도표의 네 노드와 세 간선과 같다. JDBC 기술 실패→어댑터의 cause 보존 번역→사용 사례의 다음 행동→전달 계층의 안전 표현이다. 이 그림은 실패 처리 책임을 나타내며 commit/rollback 실행 순서를 증명하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
JDBC 실패

JDBC 동작에서 SQLException이 발생합니다.

실패: SQLException · 소유: adapter기술 실패가 어댑터에 들어옵니다.
원인 보존 번역

어댑터가 application 예외로 번역합니다.

출력: application 예외 · 원인: 보존외부 경계에는 행동 계약이 전달됩니다.
행동과 안전 응답

사용 사례가 행동을 고르고 전달 계층이 안전 응답을 만듭니다.

업무: 다음 행동 · 응답: 안전 표현기술 상세가 공개 응답과 분리됩니다.

검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다

SQLException의 기술 실패가 원인 보존 application 예외·사용 사례 행동·안전 응답으로 변환된다.

CHECKED FAILURE · ACTION BOUNDARY

기술 실패를 행동 계약으로 바꾸는 경계

JDBC의 SQLException은 어댑터 안에서 처리합니다. 원인은 보존하고 공개 계약에는 충돌·중단·제한된 재시도처럼 가능한 행동을 담습니다.

발생, 번역, 행동 선택, 응답의 네 책임JDBC 동작에서 발생한 SQLException을 어댑터가 원인 보존 애플리케이션 예외로 번역한다. 사용 사례가 다음 행동을 고르고 전달 계층이 안전한 응답을 만든다.JDBC 동작SQLException어댑터 번역작업 이름 + cause사용 사례다음 행동 선택전달 계층안전한 응답
네 실패 처리 책임의 순서원 desktop의네노드세간선과같다. JDBC기술실패→어댑터cause보존번역→사용사례다음행동→전달계층안전표현이다. 이 그림은실패처리책임이며commit/rollback실행순서를증명하지않는다.
JDBC 동작SQLException
연결 관계JDBC 동작 · SQLException → 어댑터 번역 · 작업 이름 · cause
어댑터 번역작업 이름 · cause
연결 관계어댑터 번역 · 작업 이름 · cause → 사용 사례 · 다음 행동
사용 사례다음 행동
연결 관계사용 사례 · 다음 행동 → 전달 계층 · 안전한 응답
전달 계층안전한 응답
  1. JDBC 동작 → 어댑터 번역

    SQLException의 SQLState·공급자 코드·원인은 cause에 남기고, 작업 이름을 고정한 PostPersistenceException으로 감쌉니다.

  2. 사용 사례 → 전달 계층

    정확한 중복·중단·조건을 충족한 읽기 재시도를 고른 뒤 안전한 외부 응답으로 표현합니다.

내부 진단은 보존

SQLState · 공급자 코드 · cause

애플리케이션 포트는 throws SQLException을 선언하지 않습니다. 요청 값·SQL을 새 예외 메시지에 복사하지 않습니다.

외부 표현은 별도 책임

중복 충돌 · 가용성 실패의 안전한 표현

409·503은 전달 계층의 후보입니다. 중립 예외에 HTTP 상태를 박아 넣거나 공급자 메시지를 응답하지 않습니다.

checked / unchecked 문법만으로 복구 가능성을 판단하지 않습니다. 발생 위치와 다음 행동을 분리해 정합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • JDBC 동작에서 발생한 SQLException을 어댑터가 원인 보존 애플리케이션 예외로 번역한다. 사용 사례가 다음 행동을 고르고 전달 계층이 안전한 응답을 만든다.
  • 원래 가로 도표의 네 노드와 세 간선과 같다. JDBC 기술 실패→어댑터의 cause 보존 번역→사용 사례의 다음 행동→전달 계층의 안전 표현이다. 이 그림은 실패 처리 책임을 나타내며 commit/rollback 실행 순서를 증명하지 않는다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다
17원래 가로 도표의 네 노드와 세 간선과 같다. JDBC 기술 실패→어댑터의 cause 보존 번역→사용 사례의 다음 행동→전달 계층의 안전 표현이다. 이 그림은 실패 처리 책임을 나타내며 commit/rollback 실행 순서를 증명하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
JDBC 실패

JDBC 동작에서 SQLException이 발생합니다.

실패: SQLException · 소유: adapter기술 실패가 어댑터에 들어옵니다.
원인 보존 번역

어댑터가 application 예외로 번역합니다.

출력: application 예외 · 원인: 보존외부 경계에는 행동 계약이 전달됩니다.
행동과 안전 응답

사용 사례가 행동을 고르고 전달 계층이 안전 응답을 만듭니다.

업무: 다음 행동 · 응답: 안전 표현기술 상세가 공개 응답과 분리됩니다.

그림은 JDBC 동작에서 생긴 SQLException을 어댑터가 작업 이름과 원인을 보존한 애플리케이션 예외로 바꾸고, 사용 사례가 다음에 할 일을 고른 뒤, 전달 계층이 안전한 응답을 만드는 네 단계를 보여 줍니다. 아래 칸들은 SQLState와 공급자 코드, 원인은 내부 진단에 남기고, 409나 503 같은 바깥 표현은 전달 계층이 따로 정한다는 점을 설명합니다. 이 그림은 실패를 처리하는 순서이며, 커밋이나 롤백이 실행되는 순서를 보여 주지는 않습니다.


문법과 다음 행동을 나눕니다

검사 예외와 비검사 예외는 Java 시그니처의 차이일 뿐입니다. 회복할 수 있는지는 지금의 계층이 실제로 고를 수 있는 다음 행동으로 판단합니다.

본 상황지금 계층이 할 일바깥에 알리는 것
입력 자체가 잘못됨저장을 호출하기 전에 거절검증 결과
같은 회원의 같은 요청 키커밋된 기존 결과를 확인하거나 충돌DuplicatePostRequestException
읽기의 일시적인 장애멱등성과 시도 횟수, 기한을 확인제한된 내부 재시도
원인을 모르거나 결과가 불명확함성공으로 꾸미지 않고 중단PostPersistenceException

로그만 남기고 계속하는 것은 회복이 아닙니다. 낮은 계층에서 예외를 삼키면 호출한 쪽은 실패한 저장을 성공으로 오해할 수 있습니다. 공급자의 메시지를 검사하거나 모든 Exception을 잡는 방식도 다음 행동을 정하는 기준이 되지 못하므로 쓰지 않습니다.

더 깊이: JDBC가 미리 나눠 둔 세 가지 실패

JDBC 4.0부터 java.sql에는 다음 행동을 기준으로 SQLException을 나눈 하위 클래스가 있습니다. SQLTransientException은 애플리케이션이 따로 손쓰지 않고 같은 작업을 다시 시도하면 성공할 수도 있는 실패이고, SQLNonTransientException은 원인을 고치지 않으면 같은 작업을 다시 해도 실패하는 경우입니다. 그 사이의 SQLRecoverableException은 적어도 현재 연결을 닫고 새 연결을 얻는 복구 단계를 거친 뒤 트랜잭션 전체를 다시 하면 성공할 수도 있는 실패입니다. 하위 클래스 일부는 SQLState의 클래스 값과도 연결되어서, 예를 들어 SQLIntegrityConstraintViolationException은 클래스 값이 23일 때, 교착 상태나 직렬화 실패로 DB가 구문을 롤백한 SQLTransactionRollbackException은 40일 때 쓰입니다.

이 분류는 위 표의 다음 행동과 잘 맞지만, 그대로 믿고 재시도 여부를 정할 수는 없습니다. API 문서는 각 하위 클래스를 SQLState 값이나 공급자가 정한 조건에서 던진다고만 적고 있으며, 드라이버가 실제로 이 하위 클래스를 쓰는지는 드라이버마다 다릅니다. 예를 들어 PostgreSQL JDBC 드라이버의 PSQLException은 SQLException을 바로 상속하므로, 유일성 위반도 SQLIntegrityConstraintViolationException의 인스턴스로 오지 않습니다. 그래서 타입 검사에만 기대지 말고 SQLState를 함께 보거나, 이 판단을 맡아 주는 계층을 한곳에 두는 편이 안전합니다.


JDBC의 검사 예외는 어댑터 안에서 끝냅니다

JdbcAction은 JDBC 어댑터 안의 콜백입니다. 이 타입이 선언한 SQLException은 공개 애플리케이션 포트가 아니라 기술 계층 안에만 있습니다.

src/main/java/board/tx/failure/checked/JdbcAction.java
package board.tx.failure.checked;

import java.sql.SQLException;

@FunctionalInterface
public interface JdbcAction<T> {
    T execute() throws SQLException;
}

변환하는 메서드는 작업 이름을 닫힌 열거형으로 받고, 원래 예외를 cause로 보존한 공개 PostPersistenceException을 만듭니다. 제목과 본문, SQL 파라미터 같은 요청 데이터는 새 메시지에 넣지 않습니다.

src/main/java/board/tx/failure/checked/PersistenceActionBoundary.java
package board.tx.failure.checked;

import java.sql.SQLException;
import java.util.Objects;

import board.application.PostPersistenceException;

public final class PersistenceActionBoundary {
    public enum Operation {
        CREATE_POST("create post"),
        LOAD_POST("load post");

        private final String diagnosticName;

        Operation(String diagnosticName) {
            this.diagnosticName = diagnosticName;
        }
    }

    public <T> T execute(Operation operation, JdbcAction<T> action) {
        Objects.requireNonNull(operation, "operation");
        Objects.requireNonNull(action, "action");
        try {
            return action.execute();
        } catch (SQLException failure) {
            throw new PostPersistenceException(
                    "post persistence failed: " + operation.diagnosticName,
                    failure);
        }
    }
}

이 메서드 밖에는 throws SQLException이 없습니다. 사용 사례는 JDBC나 SQLState, 공급자 코드를 import하지 않으며, 어댑터를 바꾼 뒤에도 같은 실패 처리를 씁니다.


원인은 내부 진단에, 표현은 요청을 처리하는 곳에 둡니다

변환은 원인을 지우는 포장이 아닙니다. PostPersistenceException.getCause()에는 원래의 SQLException이 남아 있어, 운영 진단에서 SQLState와 공급자 코드를 읽을 수 있습니다. 바깥 응답은 고정된 오류 코드와 안전한 설명만 씁니다.

호출한 쪽이 다른 행동을 할 수 있는 실패, 예를 들어 중복 요청만 더 좁은 애플리케이션 예외로 바꿉니다. 연결이 끊긴 것이나 분류되지 않은 무결성 실패를 중복으로 바꾸지 않습니다. HTTP 어댑터는 중복을 409로, 저장소를 쓸 수 없는 상황을 503으로 나타낼 수 있지만, 예외 타입 자체에 HTTP 상태를 넣지는 않습니다.

검사 예외를 그대로 위로 올리면 저장소와 서비스, 전달 계층이 모두 기술 타입을 알아야 합니다. 이 장에서는 그 전파 사슬을 어댑터 한곳에서 끊습니다. 프록시의 진입과 롤백 규칙, 전파 속성은 뒤의 글에서 따로 다루며, 여기서는 예외를 어떻게 바꾸는지만 고정합니다.


변환 규칙을 실행으로 검증합니다

테스트는 정상 반환과 원인의 동일성, 안전한 메시지를 각각 확인합니다. 손으로 적은 실행 결과 대신 JUnit이 실제 변환 메서드를 호출합니다.

src/test/java/board/tx/failure/checked/PersistenceActionBoundaryTest.java
package board.tx.failure.checked;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.sql.SQLException;

import board.application.PostPersistenceException;
import org.junit.jupiter.api.Test;

class PersistenceActionBoundaryTest {
    private final PersistenceActionBoundary boundary =
            new PersistenceActionBoundary();

    @Test
    void returnsTheJdbcActionResult() {
        long postId = boundary.execute(
                PersistenceActionBoundary.Operation.CREATE_POST,
                () -> 41L);

        assertEquals(41L, postId);
    }

    @Test
    void translatesTheCheckedFailureAndKeepsTheExactCause() {
        SQLException source = new SQLException(
                "socket closed for private-title",
                "08006",
                17002);

        PostPersistenceException failure = assertThrows(
                PostPersistenceException.class,
                () -> boundary.execute(
                        PersistenceActionBoundary.Operation.CREATE_POST,
                        () -> {
                            throw source;
                        }));

        assertSame(source, failure.getCause());
        assertEquals(
                "post persistence failed: create post",
                failure.getMessage());
    }

    @Test
    void doesNotCopyProviderDetailsIntoTheContractMessage() {
        SQLException source = new SQLException(
                "password=secret",
                "08001",
                90067);

        PostPersistenceException failure = assertThrows(
                PostPersistenceException.class,
                () -> boundary.execute(
                        PersistenceActionBoundary.Operation.LOAD_POST,
                        () -> {
                            throw source;
                        }));

        assertFalse(failure.getMessage().contains("password"));
        assertFalse(failure.getMessage().contains("08001"));
    }
}

이렇게 하면 검사 예외의 타입을 숨기면서도 진단할 수 있는 정보는 잃지 않습니다. 예외를 잡는 위치는 예외가 생긴 곳이 아니라, 구체적인 다음 행동을 정할 수 있는 곳입니다.


정리

  • 검사 예외와 비검사 예외는 문법의 차이일 뿐, 회복할 수 있는지는 지금 계층이 고를 수 있는 다음 행동으로 판단합니다.
  • SQLException 같은 기술 예외는 어댑터 안의 좁은 동작 단위에서 공개 영속성 예외로 바꾸고, 애플리케이션 포트에 throws로 드러내지 않습니다.
  • 변환할 때는 원래 예외를 cause로 보존하고, 새 메시지에는 요청 데이터나 SQL을 넣지 않습니다.
  • 중복처럼 호출한 쪽이 다른 행동을 할 수 있는 실패만 좁은 예외로 바꾸고, HTTP 상태는 전달 계층이 정합니다.
  • 예외를 삼키고 성공처럼 돌려주지 않으며, 공급자 메시지나 모든 Exception을 잡는 방식으로 행동을 정하지 않습니다.

연습 문제

1. 같은 원인이 남아도 응답은 다른 이유

테스트의 SQLException 메시지가 password=secret이고 SQLState가 08001인 경우를 따라가 봅니다. 바깥 PostPersistenceException의 메시지와 cause의 동일성, HTTP 응답에 넣어도 되는 내용을 각각 적으세요. 바깥 메시지가 안전하다는 이유로 cause 전체를 JSON 응답에 복사해도 될까요?

해설 보기

작업이 LOAD_POST라면 바깥 메시지는 고정된 post persistence failed: load post입니다. cause는 원래의 SQLException 객체 그대로이므로, SQLState와 공급자의 메시지는 내부 진단에서 확인할 수 있습니다. cause를 응답에 복사하면 바깥 메시지에서 뺐던 비밀 값이 다시 노출됩니다. 전달 계층은 원인 객체를 직렬화하지 않고, 고정된 오류 코드와 안전한 설명을 정합니다. 로그에서도 요청이나 자격 증명 같은 민감한 값의 취급 기준을 적용해야 합니다.

이 예제는 예외를 어떻게 바꾸는지를 검사할 뿐, DB의 커밋 여부나 자동 롤백 규칙까지 보여 주지는 않습니다. 그것은 실제 트랜잭션 안에서 따로 확인합니다.

2. 읽기 재시도를 어디서 할까

연결이 잠깐 끊겨 PostPersistenceException이 났을 때, 어댑터 안에서 바로 세 번 재시도하는 방법과 사용 사례에서 재시도하는 방법을 비교하려고 합니다. 읽기와 쓰기에서 각각 어느 쪽이 알맞고, 무엇을 먼저 확인해야 하나요?

해설 보기

재시도는 그 작업이 멱등한지와 남은 기한, 시도 횟수를 아는 곳에서 해야 합니다. 단순한 읽기라면 어댑터 안에서 제한된 횟수만 재시도할 수 있지만, 사용자 요청 전체의 기한을 모르는 어댑터가 무조건 세 번을 기다리면 응답이 늦어집니다. 쓰기는 커밋되었는지 모르는 상태일 수 있으므로, 어댑터가 몰래 다시 실행하면 중복이 생길 수 있습니다. 쓰기의 재시도는 멱등성 키로 기존 결과를 확인할 수 있는 사용 사례에서, 트랜잭션 전체를 처음부터 다시 실행하는 방식으로 합니다. 어느 쪽이든 재시도할 수 있는 실패의 종류를 SQLState 같은 기준으로 좁히고, 그 외의 실패는 바로 중단합니다.

다음 글 Repository 예외 변환에서는 저장소 어댑터가 공급자의 예외를 어떤 기준으로 애플리케이션 예외로 바꾸는지 더 자세히 다룹니다.