검사 예외 전파
검사 예외의 문법과 회복 가능성을 분리하고 JDBC 실패를 어댑터의 행동 경계에서 원인을 보존해 번역합니다.
검사 예외는 호출자에게 처리 문법을 강제하지만 실패가 회복 가능한지는 알려 주지 않습니다.
회원 게시판의 애플리케이션 포트가 SQLException을 선언하면 저장 기술을 바꾸는 일이 사용 사례와 전달 계층의 시그니처 변경으로 번집니다. 반대로 어댑터 내부의 가장 좁은 행동 경계에서 기술 실패를 번역하면 애플리케이션은 B91에서 정한 영속성 계약만 봅니다.
CHECKED FAILURE · ACTION BOUNDARY
검사 예외는 어댑터 안에서 끝나고 행동 계약만 경계를 넘는다
JDBC의 SQLException은 발생 정보다. 어댑터가 원인을
보존해 B91 영속성 예외로 번역하고, 사용 사례와 전달 경계는
기술 타입이 아니라 가능한 다음 행동만 선택한다.
-
JDBC 동작
SQLException의 state·공급자 코드·원인은 어댑터 내부에서 발생합니다. -
행동 경계
작업 이름을 고정하고 원인을 보존해 B91
PostPersistenceException으로 번역합니다. -
사용 사례
기술 타입 없이 중복 확인, 중단, 제한된 읽기 재시도처럼 실제 가능한 행동만 고릅니다.
-
전달 경계
안전한 오류 코드만 내보내고 SQL·요청 값·공급자 메시지는 응답에서 제거합니다.
checked와 unchecked는 복구 가능성의 판정표가 아니다. 발생 위치와 처리 위치를 분리하고, 원인을 보존한 안정적인 계약만 경계를 넘긴다.
문법과 다음 행동을 분리한다
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로 보존한 B91의 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와 공급자 코드를 읽을 수 있습니다. 외부 응답은 고정 오류 코드와 안전한 설명만 사용합니다.
중복 요청처럼 호출자가 다른 행동을 할 수 있는 실패만 더 좁은 B91 계약 예외로 번역합니다. 연결 손실이나 분류되지 않은 무결성 실패를 중복으로 바꾸지 않습니다. 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 타입을 숨기면서도 진단 가능성을 잃지 않습니다. 예외를 잡는 위치는 발생 위치가 아니라 구체적인 다음 행동을 소유한 경계입니다.