검사 예외 전파
검사 예외의 문법과 회복 가능성을 분리하고 JDBC 실패를 어댑터의 행동 경계에서 원인을 보존해 번역합니다.
검사 예외는 호출자에게 처리 문법을 강제하지만 실패가 회복 가능한지는 알려 주지 않습니다.
회원 게시판의 애플리케이션 포트가 SQLException을 선언하면 저장 기술을 바꾸는 일이 사용 사례와 전달 계층의 시그니처 변경으로 번집니다. 반대로 어댑터 내부의 가장 좁은 행동 경계에서 기술 실패를 번역하면 애플리케이션은 앞 장에서 정한 공개 영속성 계약만 봅니다.
검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다
SQLException의 기술 실패가 원인 보존 application 예외·사용 사례 행동·안전 응답으로 변환된다.
CHECKED FAILURE · ACTION BOUNDARY
기술 실패를 행동 계약으로 바꾸는 경계
JDBC의 SQLException은 어댑터 안에서 처리합니다. 원인은 보존하고 공개 계약에는 충돌·중단·제한된 재시도처럼 가능한 행동을 담습니다.
- JDBC 동작 → 어댑터 번역
SQLException의 SQLState·공급자 코드·원인은 cause에 남기고, 작업 이름을 고정한 PostPersistenceException으로 감쌉니다.
- 사용 사례 → 전달 계층
정확한 중복·중단·조건을 충족한 읽기 재시도를 고른 뒤 안전한 외부 응답으로 표현합니다.
내부 진단은 보존
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의 SQLState·공급자 코드·원인은 cause에 남기고, 작업 이름을 고정한 PostPersistenceException으로 감쌉니다.
- 사용 사례 → 전달 계층
정확한 중복·중단·조건을 충족한 읽기 재시도를 고른 뒤 안전한 외부 응답으로 표현합니다.
내부 진단은 보존
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 예외 · 원인: 보존 | 외부 경계에는 행동 계약이 전달됩니다. |
| 행동과 안전 응답 사용 사례가 행동을 고르고 전달 계층이 안전 응답을 만듭니다. | 업무: 다음 행동 · 응답: 안전 표현 | 기술 상세가 공개 응답과 분리됩니다. |
문법과 다음 행동을 분리한다
checked와 unchecked는 Java 시그니처의 차이입니다. 복구 여부는 현재 계층이 실제로 선택할 수 있는 다음 행동으로 판단합니다.
| 관찰한 상황 | 현재 경계의 행동 | 외부 계약 |
|---|---|---|
| 입력 자체가 잘못됨 | 저장 호출 전 거절 | 검증 결과 |
| 동일 회원·동일 요청 키 | 커밋된 기존 결과 확인 또는 충돌 | DuplicatePostRequestException |
| 읽기의 일시 장애 | 멱등성·시도 횟수·기한을 확인 | 제한된 내부 재시도 |
| 원인을 알 수 없거나 결과가 불명확함 | 성공으로 꾸미지 않고 중단 | PostPersistenceException |
로그 기록만 하고 계속하는 것은 복구가 아닙니다. 낮은 계층에서 예외를 삼키면 호출자는 실패한 저장을 성공으로 오해할 수 있습니다. 공급자 메시지를 검사하거나 모든 Exception을 잡는 방식도 행동 근거가 아니므로 사용하지 않습니다.
JDBC checked 타입은 어댑터 안에서 끝낸다
JdbcAction은 JDBC 어댑터 내부 콜백입니다. 이 타입의 SQLException 선언은 공개 애플리케이션 포트가 아니라 기술 경계 안에만 존재합니다.
package board.tx.failure.checked;
import java.sql.SQLException;
@FunctionalInterface
public interface JdbcAction<T> {
T execute() throws SQLException;
}행동 경계는 작업 이름을 닫힌 enum으로 받고, 원래 예외를 cause로 보존한 공개 계약의 PostPersistenceException을 만듭니다. 제목·본문·SQL 파라미터 같은 요청 데이터는 새 메시지에 넣지 않습니다.
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, 공급자 코드를 임포트하지 않으며, 어댑터 교체 뒤에도 같은 실패 경계를 사용합니다.
원인은 내부 진단에, 표현은 요청 경계에 둔다
번역은 원인을 지우는 포장이 아닙니다. PostPersistenceException.getCause()에는 원래 SQLException이 남아 운영 진단이 SQLState와 공급자 코드를 읽을 수 있습니다. 외부 응답은 고정 오류 코드와 안전한 설명만 사용합니다.
같은 원인이 남아도 외부 응답은 다른 이유
원 테스트의 SQLException 메시지가 password=secret이고 SQLState가 08001인 경우를 추적해 보세요. 바깥 PostPersistenceException의 메시지, cause의 동일성, HTTP 응답에 넣어도 되는 내용을 각각 적으세요. 바깥 메시지가 안전하다는 이유로 cause 전체를 JSON 응답에 복사해도 될까요?
진단 보존과 응답 표현의 풀이
작업이 LOAD_POST이면 바깥 메시지는 고정된 post persistence failed: load post입니다. cause는 원 SQLException 객체 그대로이므로 SQLState·공급자 메시지는 내부 진단에서 확인할 수 있습니다. cause를 응답에 복사하면 바깥 메시지에서 제외한 비밀 값이 다시 노출됩니다. 전달 계층은 원인 객체를 직렬화하지 않고 고정 오류 코드와 안전한 설명을 정합니다. 로그에서도 요청·자격 증명 같은 민감한 값의 취급 기준을 적용해야 합니다.
이 예제는 예외 계약을 검사하며 DB 커밋 여부나 자동 롤백 규칙까지 증명하지는 않습니다. 그 사실은 실제 트랜잭션 경계에서 따로 관찰합니다.
중복 요청처럼 호출자가 다른 행동을 할 수 있는 실패만 더 좁은 애플리케이션 계약 예외로 번역합니다. 연결 손실이나 분류되지 않은 무결성 실패를 중복으로 바꾸지 않습니다. HTTP 어댑터는 중복을 409, 저장소 사용 불가를 503으로 표현할 수 있지만 예외 타입 자체에 HTTP 상태를 넣지 않습니다.
검사 예외를 그대로 올리는 방식은 리포지토리, 서비스, 전달 경계가 모두 기술 타입을 알아야 합니다. 이 장의 경계는 그 전파 사슬을 어댑터 한 곳에서 끊습니다. 프록시 진입, 롤백 규칙, 전파 속성은 뒤 절의 별도 책임이며 여기서는 예외 계약만 고정합니다.
경계 계약을 실행으로 검증한다
테스트는 정상 반환, 원인 동일성, 안전한 메시지를 각각 확인합니다. 손으로 적은 실행 결과 대신 JUnit이 실제 경계를 호출합니다.
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"));
}
}이 계약은 checked 타입을 숨기면서도 진단 가능성을 잃지 않습니다. 예외를 잡는 위치는 발생 위치가 아니라 구체적인 다음 행동을 소유한 경계입니다.