> For the complete documentation index, see [llms.txt](https://wonjoon.gitbook.io/joons-til/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wonjoon.gitbook.io/joons-til/books/tobys-spring-vol1/4.-exception.md).

# 4. Exception

## 4.1. 사라진 SQLException

3장에서 JdbcContext를 사용하던 코드가 4장에서 JdbcTemplate으로 바뀌면서\
메서드 선언부에 있던 throws SQLException이 사라집니다.

**JdbcContext 사용 시**

```kotlin
fun deleteAll() {
    jdbcContext.executeSql("delete from users")
}
```

JdbcContext 내부에서는 JDBC API를 직접 사용하므로, 실제로는 `SQLException`이 발생할 수 있고 이를 메서드 밖으로 던지고 있었습니다.

**JdbcTemplate 적용 후**

```kotlin
fun deleteAll() {
    jdbcTemplate.update("delete from users")
}
```

JdbcTemplate을 사용하는 순간, **메서드 시그니처에서 SQLException은 완전히 사라집니다.**

`SQLException`은 다음과 같은 특징을 가집니다.

* SQL 문법 오류
* DB 서버 다운
* 네트워크 장애
* 커넥션 풀 고갈

이 중 **애플리케이션 코드에서 복구 가능한 경우는 거의 없습니다.**\
그럼에도 불구하고 체크 예외이기 때문에 모든 호출자는 이를 처리해야 합니다.

결과적으로 개발자는 다음 중 하나를 선택하게 됩니다.

* 의미 없는 catch
* 무의미한 throws 전파

JdbcTemplate은 이 문제를 **예외 전환(exception translation)** 으로 해결합니다.

* SQLException을 그대로 노출하지 않음
* DB 벤더, 에러 코드에 따라 의미 있는 예외로 변환
* 모두 `RuntimeException` 계열로 통일

즉, `SQLException`이 사라진 이유는\
**예외를 숨겨서가 아니라, 잘못 쓰이지 않게 만들었기 때문**입니다.

그럼 SQLException은 어디로 갔을까?????????????

### 4.1.1. 초난감 예외처리

먼저 잘못된 예외 처리 습관을 살펴보면,

**예외를 잡고 아무것도 하지 않는 코드**

```kotlin
try {
    // JDBC 작업
} catch (e: SQLException) {
}
```

이 코드는 컴파일 오류를 없애는 데는 성공하지만,\
**예외가 발생했다는 사실 자체를 완전히 무시**합니다.

* DB 접속 실패
* SQL 문법 오류
* 네트워크 단절
* 커넥션 풀 고갈 등

이 모든 상황이 **정상 흐름처럼 처리됩니다.**

**발생 가능한 문제**

* DB 작업이 실패해도 호출자는 성공으로 인식
* 데이터 정합성 붕괴
* 이후 로직에서 원인 추적 불가

특히 트랜잭션 경계 안에서 이런 코드가 존재하면\
**부분 실패 + 전체 성공이라는 최악의 상태**를 만듭니다.

**설계 관점에서의 문제**

이 코드는 단순한 실수가 아니라 **책임을 포기한 설계**입니다.

* 예외를 처리하지 않음
* 그렇다고 전달하지도 않음
* 시스템이 스스로 망가질 시간을 벌어주는 코드

**콘솔 출력만 하는 예외 처리**

```kotlin
catch (e: SQLException) {
    println(e)
}
```

또는

```kotlin
catch (e: SQLException) {
    e.printStackTrace()
}
```

개발 중에는 눈에 띌 수 있지만,\
운영 환경에서는 **로그를 보지 않으면 아무도 모르는 코드**가 됩니다.

이 방식은 예외를 처리하는 것이 아니라 **사고를 기록만 하고 방치하는 것**에 가깝습니다.\
**운영 환경에서는 아무도 모름**

* 로그 레벨 관리 불가
* 모니터링 시스템과 연동 불가
* 장애 발생 후 원인 추적 불가

**시스템을 종료해버리는 코드**

```kotlin
catch (e: SQLException) {
    e.printStackTrace()
    System.exit(1)
}
```

이 방식은 **최악의 초난감 예외 처리**입니다.

* 라이브러리 코드에서 JVM 종료
* 서버 전체 다운
* 호출자는 대응할 기회조차 없음

"예외를 잡는다는 것은 문제를 해결하거나, 책임 있는 곳으로 넘긴다는 것"

#### 무의미하고 무책임한 throws

또 다른 흔한 실수는 **아무 생각 없이 throws Exception을 붙이는 것**입니다.

```kotlin
fun method1() {
    method2()
}

fun method2() {
    method3()
}

fun method3() {
    // JDBC 코드
}
```

```kotlin
fun method1() throws Exception
fun method2() throws Exception
fun method3() throws Exception
```

이 방식의 문제점은 명확합니다.

* 어떤 예외가 발생할 수 있는지 알 수 없음
* 호출자는 아무 정보도 얻지 못함
* **복구 가능한 예외마저 모두 포기**

**계층 설계 붕괴**

모든 계층이 `throws Exception`을 사용하면

* DAO → Service → Controller
* 모든 계층이 기술 예외에 오염됨
* 비즈니스 코드가 인프라 세부사항을 알게 됨

결과적으로 예외는 **의미를 잃고**, 코드는 **책임을 회피**하게 됩니다.

### 4.1.2. 예외의 종류와 특징

자바의 예외는 크게 세 가지로 나뉩니다.

**Error**

* `OutOfMemoryError`
* `StackOverflowError`
* `ThreadDeath`

JVM 레벨의 심각한 문제로,\
**애플리케이션 코드에서 처리 대상이 아닙니다.**

***

**Exception (체크 예외)**

* `IOException`
* `SQLException`

반드시 catch 하거나 throws 해야 합니다.

문제는 **복구가 불가능한 상황에서도 강제로 처리를 요구한다는 점**입니다.

***

**RuntimeException (언체크 예외)**

* `NullPointerException`
* `IllegalArgumentException`
* `IllegalStateException`

프로그래밍 오류나 **논리적 결함**을 나타내며,\
명시적인 throws 없이도 전파됩니다.

***

#### 체크 예외의 한계

초기 자바 설계는 **모든 예외를 체크 예외로 강제**하려는 방향이었습니다.\
하지만 현실에서는 문제가 드러났습니다.

* 대부분의 SQLException은 **코드 레벨에서 복구 불가**
* 결국 무의미한 catch / throws만 증가
* 코드 가독성과 책임 구조 붕괴

이로 인해 최근의 자바 API와 프레임워크는\
**체크 예외를 줄이고 언체크 예외를 선호**하는 방향으로 발전했습니다.

### 4.1.3.

예외를 다루는 방법은 크게 세 가지입니다.

***

**예외 복구**

예외가 발생해도 **정상 상태로 되돌릴 수 있는 경우**입니다.

```kotlin
var retry = MAX_RETRY

while (retry-- > 0) {
    try {
        // DB 작업
        return
    } catch (e: SomeException) {
        // 로그 출력, 대기
    } finally {
        // 리소스 정리
    }
}

throw RetryFailedException()
```

* 네트워크 일시 장애
* 재시도 가능한 외부 시스템 오류

***

**예외 회피**

자신이 처리하지 않고 **의미 있는 계층으로 전달**합니다.

```kotlin
fun add() {
    try {
        // JDBC 코드
    } catch (e: SQLException) {
        throw e
    }
}
```

#### 계층을 기술적으로 나누면

일반적인 Spring 애플리케이션에서 계층은 다음처럼 나뉩니다.

```
Controller  →  Service  →  DAO(Repository)  →  JDBC / DB
```

**“의미 있는 계층”이란, 예외를 보고&#x20;*****판단하거나 행동을 바꿀 수 있는 계층*****&#x20;입니다.**

각 계층이 **알 수 있는 것 / 알면 안 되는 것**이 다릅니다.

* Controller - 사용자 요청 / DB 벤터, SQL 에러코드
* Service - 비즈니스 규칙 / JDBC Api, SQLException
* DAO - SQL, DB 제약조건 / 사용자 UX 정책

#### 의미 없는 예외 회피 (잘못된 예)

DAO에서 SQLException을 그대로 던지는 경우

```kotlin
fun add(user: User) {
    try {
        jdbcTemplate.update(...)
    } catch (e: SQLException) {
        throw e
    }
}
```

#### 왜 의미가 없는가

* Service 계층은 `SQLException`을 보고
  * 이게 중복 키인지
  * 네트워크 오류인지
  * 문법 오류인지\
    알 수 없습니다.
* 결국 Service는 이렇게 됩니다.

```kotlin
try {
    userDao.add(user)
} catch (e: SQLException) {
    throw RuntimeException(e)
}
```

**DAO에서 던지나 Service에서 던지나 차이가 없습니다.**

#### 의미 있는 예외 회피 – 예시 1 (DAO → Service)

DAO에서 예외를 전환해서 던짐

```kotlin
class UserDao {

    fun add(user: User) {
        try {
            jdbcTemplate.update(...)
        } catch (e: SQLException) {
            if (e.errorCode == MysqlErrorNumbers.ER_DUP_ENTRY) {
                throw DuplicateUserIdException(user.id, e)
            }
            throw DataAccessException("DB access failed", e)
        }
    }
}
```

여기서 “의미”는 무엇인가

* `DuplicateUserIdException`
  * **비즈니스적으로 해석 가능**
  * Service 계층에서 분기 가능

Service 계층에서의 처리

```kotlin
fun register(user: User) {
    try {
        userDao.add(user)
    } catch (e: DuplicateUserIdException) {
        // 아이디 중복 → 사용자에게 안내
    }
}
```

이때 **Service는 DB를 몰라도 됩니다.**\
이게 “의미 있는 계층으로 전달”입니다.

***

#### 의미 있는 예외 회피 – 예시 2 (Service → Controller)

Service에서 예외를 그대로 처리하지 않는 경우

```kotlin
fun register(user: User) {
    if (!user.isValid()) {
        throw InvalidUserStateException(user.id)
    }

    userDao.add(user)
}
```

Controller에서 처리

```kotlin
@PostMapping("/users")
fun register(@RequestBody user: User): ResponseEntity<*> {
    return try {
        userService.register(user)
        ResponseEntity.ok().build()
    } catch (e: InvalidUserStateException) {
        ResponseEntity.badRequest().body("잘못된 사용자 상태입니다.")
    }
}
```

Service는 HTTP를 모르고\
Controller는 비즈니스 예외를 **자기 역할에 맞게 해석**합니다.

***

### 그래서 예외 회피는 언제 쓰는가

#### 올바른 예외 회피

```
DAO
 └─ 기술 예외를 비즈니스 예외로 변환
     ↓
Service
 └─ 처리 불가한 비즈니스 예외를 Controller로 전달
     ↓
Controller
 └─ HTTP 응답으로 변환
```

**아무 의미 없이 던지는 것은 회피가 아니라 방기**입니다.

### 4.1.4. 예외처리 전략

스프링은 다음과 같은 원칙을 따릅니다.

* DAO 계층에서는 **SQLException을 그대로 던지지 않는다**
* 모든 JDBC 예외를 **DataAccessException (RuntimeException)** 으로 변환
* 호출자는 **필요한 경우만 catch**

JdbcTemplate의 메서드 시그니처는 이를 보여줍니다.

```kotlin
fun update(sql: String): Int
```

`throws SQLException`이 없습니다.

### 4.1.5. SQLException은 어떻게 됐나?

SQLException은 사라진 것이 아니라, **DataAccessException으로 전환되었다.**

* 복구 불가능한 예외를 억지로 처리하지 않도록
* DAO 계층의 책임을 명확히 하고
* 서비스 계층에서 **의미 있는 예외만 다루도록**

JdbcTemplate은 SQLException을 **숨긴 것이 아니라**,\
**잘못 사용되지 않도록 구조적으로 제거한 것**입니다.

<details>

<summary>Throw가 자바/JVM에서 동작하는 방식</summary>

### 1. `throw`의 정체

```kotlin
throw e
```

이 한 줄은 문법적으로는 간단하지만, **의미적으로는 제어 흐름을 즉시 중단시키는 명령**입니다.

기술적으로 `throw`는 다음을 의미합니다.

> “이 메서드는 더 이상 정상 경로로 실행되지 않는다”

***

### 2. JVM 관점에서 `throw` 실행 순서

#### 1️⃣ 예외 객체는 이미 존재해야 합니다

```kotlin
throw IllegalStateException("invalid state")
```

이 코드는 내부적으로 다음 두 단계입니다.

```kotlin
val ex = IllegalStateException("invalid state")
throw ex
```

* `throw`는 **객체를 생성하지 않습니다**
* 반드시 `Throwable`의 서브클래스 인스턴스를 받아야 합니다

***

#### 2️⃣ 현재 스택 프레임 즉시 종료

예외가 던져지는 순간:

* **현재 메서드는 즉시 종료**
* return 값이 있어도 무시됨
* 이후 코드는 절대 실행되지 않음

```kotlin
fun test() {
    println("A")
    throw RuntimeException()
    println("B") // 실행되지 않음
}
```

출력 결과:

```
A
```

***

#### 3️⃣ JVM은 스택을 “되감기(Unwinding)” 시작

호출 스택이 아래와 같다고 가정하겠습니다.

```
controller()
 └─ service()
     └─ dao()
```

`dao()`에서 `throw` 발생 시:

1. `dao()` 스택 프레임 제거
2. `service()`에서 catch 가능한지 검사
3. 없으면 `service()` 프레임 제거
4. `controller()`로 이동
5. 없으면 스레드 종료

👉 이것을 **stack unwinding**이라고 합니다.

***

### 3. catch는 어떻게 선택되는가

#### catch 탐색 규칙 (엄격함)

```kotlin
try {
    dao()
} catch (e: IllegalArgumentException) {
} catch (e: RuntimeException) {
}
```

* **위에서 아래로 검사**
* 첫 번째로 **타입이 맞는 catch만 실행**
* 이후 catch는 무시

***

#### 타입 매칭 규칙

```kotlin
throw IllegalStateException()
```

이 예외는 다음 catch에 잡힐 수 있습니다.

* `IllegalStateException`
* `RuntimeException`
* `Exception`
* `Throwable`

하지만 다음에는 **절대 잡히지 않습니다.**

* `IOException`
* `SQLException`

👉 **상속 관계로만 판단**합니다.

***

### 4. catch에서 다시 `throw`하면?

```kotlin
catch (e: SQLException) {
    throw e
}
```

이때 JVM 관점에서는:

* 예외 객체는 **동일**
* 스택 언와인딩은 **계속 진행**
* 현재 메서드는 종료

즉,

> catch + throw는\
> “여기서는 아무 것도 안 하고 통과시킨다”와 동일합니다.

***

### 5. 스택 트레이스는 언제 찍히는가

#### 중요한 오해 하나

> `throw` 할 때 스택 트레이스가 다시 만들어진다 ❌

❌ 아닙니다.

스택 트레이스는 **예외 객체가 생성될 때** 캡처됩니다.

```kotlin
val e = RuntimeException()
throw e
```

* 스택 트레이스는 `new RuntimeException()` 시점 기준

***

#### 그래서 이런 차이가 생깁니다

```kotlin
catch (e: SQLException) {
    throw RuntimeException(e)
}
```

* **새 예외 객체 생성**
* 스택 트레이스 새로 캡처
* 원인은 `cause`로 연결

👉 이것이 **예외 전환이 의미를 가지는 이유**입니다.

***

### 6. `throw`와 `throws`의 관계 (정확한 역할)

| 키워드      | 역할                               |
| -------- | -------------------------------- |
| `throw`  | **지금 이 순간 예외를 발생시킴**             |
| `throws` | 이 메서드가 **어떤 예외를 던질 수 있는지 계약 선언** |

```kotlin
fun foo() throws IOException {
    throw IOException()
}
```

* `throws`는 실행과 무관
* `throw`만이 흐름을 바꿈

***

### 7. 트랜잭션과 `throw`

스프링에서 매우 중요한 지점입니다.

```kotlin
@Transactional
fun service() {
    dao()
}
```

* RuntimeException throw → **자동 롤백**
* Checked Exception throw → 기본적으로 **롤백 안 됨**

이 차이는 **throw 자체가 아니라 예외 타입 때문**입니다.

***

### 8. 정리: `throw`의 본질

기술적으로 `throw`는 다음을 동시에 수행합니다.

1. 현재 메서드 즉시 종료
2. 정상 제어 흐름 파괴
3. 호출 스택을 따라 위로 전파
4. 첫 번째 매칭되는 catch 탐색
5. 없으면 스레드 종료

</details>

## 4.2. 예외 전환

예외 전환은 발생한 예외를 그대로 던지는 것이 아니라, 다른 타입의 예외로 바꿔 던지는 행위를 말합니다.\
스프링의 예외전환은 단순한 포장이 아닌 아래 문제들을 해결하기 위함입니다.

* JDBC의 SQLException 하나로는 원인 분기 어려움
* DB마다 에러 코드 체계가 다름
* DAO 인터페이스에 기술 종속 예외가 새어 나오는 문제

이를 해결하기 위해 스프링은 SQLException -> DataAccessException 전환 구조를 제공

### 4.2.1. JDBC의 한계

#### **JDBC 예외 구조의 기술적 문제**

JDBC에서 발생하는 모든 DB관련 예외는 단 하나의 타입 -> java.sql.SQLException.\
이 예외에 모든 정보가 포함됩니다.

* SQL 문법 오류
* 제약 조건 위반
* 커넥션 실패
* 네트워크 오류&#x20;
* DB 서버 다운

이걸로는 원인을 구분하기 어려움.

**SQLException 내부에 들어 있는 정보**

```kotlin
e.errorCode      // DB 벤더 전용 에러 코드
e.sqlState       // SQLState (표준 코드)
e.message        // 문자열 메시지
```

문제는 이 정보들이 **신뢰하기 어렵고, DB마다 다르다**는 점입니다.

***

#### 비표준 SQL과 DB 종속성

**errorCode의 문제**

```kotlin
if (e.errorCode == 1062) { ... } // MySQL 중복 키
```

* MySQL: 1062
* Oracle: 1
* PostgreSQL: 23505

**DB가 바뀌면 코드가 깨집니다.**

#### SQLState의 한계

SQLState는 표준이지만 현실적으로 문제가 있습니다.

* DB 벤더가 정확히 지키지 않는 경우 존재
* 세부 원인을 표현하기에는 너무 거칠음
* 실제 현장에서는 거의 사용되지 않음

### 4.2.2. DB 에러 코드 매핑을 통한 전환

**스프링의 접근 방식**

스프링은 다음 전략을 사용합니다.

1. DB 벤더별 에러 코드 테이블을 준비
2. `SQLException` 발생 시 에러 코드 확인
3. **의미 있는 DataAccessException 서브클래스로 변환**

***

**실제 매핑 구조**

```
SQLException
 └─ errorCode = 1062 (MySQL)
      ↓
DuplicateKeyException
```

이 매핑 정보는 스프링 내부에 정의되어 있습니다.

```xml
<property name="duplicateKeyCodes">
    <value>1</value>        <!-- Oracle -->
    <value>1062</value>     <!-- MySQL -->
</property>
```

**DB가 바뀌어도 동일한 예외 타입이 던져집니다.**

**JdbcTemplate에서 실제로 벌어지는 일**

```
JDBC API 호출
 └─ SQLException 발생
     └─ SQLExceptionTranslator 호출
         └─ DB 벤더 확인
         └─ 에러 코드 매핑
         └─ DataAccessException 생성
         └─ RuntimeException으로 throw
```

JdbcTemplate은 **SQLException을 그대로 밖으로 내보내지 않습니다.**

### 4.2.3. DataAccessException 계층 구조

#### DataAccessException의 정체

```
java.lang.RuntimeException
 └─ org.springframework.dao.DataAccessException
```

기술적으로 DataAccessException은 다음 성격을 가집니다.

* RuntimeException (unchecked)
* JDBC 전용 ❌
* **모든 데이터 접근 기술 공통 예외 베이스**
  * JDBC
  * JPA
  * Hibernate
  * JDO
  * MyBatis

즉, “DB에 접근하다 실패했다”는 **추상적인 실패 의미**를 표현하는 타입입니다.

#### 왜 단일 클래스가 아니라 계층 구조인가

`DataAccessException` 하나만 있으면 이런 코드밖에 못 씁니다.

```kotlin
catch (e: DataAccessException) {
    // 뭘 어떻게 해야 하지?
}
```

그래서 스프링은 **실패 원인 기준으로 서브클래스를 나눕니다.**

<details>

<summary>대표적인 하위 예외들과 의미</summary>

**1️⃣ BadSqlGrammarException**

```
DataAccessException
 └─ BadSqlGrammarException
```

* SQL 문법 오류
* 테이블/컬럼 이름 오류
* 잘못된 쿼리 구조

```kotlin
jdbcTemplate.update("insert into usr values (...)")
```

→ 테이블 `usr` 없음\
→ `BadSqlGrammarException`

👉 **개발자 실수**\
👉 재시도 의미 없음

***

**2️⃣ DataIntegrityViolationException**

```
DataAccessException
 └─ DataIntegrityViolationException
     └─ DuplicateKeyException
```

* NOT NULL 위반
* FK 위반
* UNIQUE 제약 위반

```kotlin
jdbcTemplate.update(
    "insert into users(id) values (?)",
    existingId
)
```

→ `DuplicateKeyException`

👉 **데이터 무결성 위반**\
👉 비즈니스 분기 가능

***

**3️⃣ DataAccessResourceFailureException**

* DB 서버 다운
* 커넥션 풀 고갈
* 네트워크 장애

```kotlin
jdbcTemplate.queryForObject(...)
```

→ DB 연결 실패\
→ `DataAccessResourceFailureException`

👉 **시스템 장애**\
👉 재시도 또는 장애 전파 대상

***

**4️⃣ OptimisticLockingFailureException**

* 낙관적 락 실패
* 동시성 충돌

```kotlin
UPDATE account
SET balance = ?
WHERE id = ? AND version = ?
```

→ 수정된 행 수 = 0\
→ `OptimisticLockingFailureException`

👉 **동시성 제어 실패**\
👉 재시도 전략 가능

이 예외들은 **DB 종류와 무관하게 동일한 타입**으로 던져집니다.

* MySQL
* Oracle
* PostgreSQL
* H2

👉 **DAO를 바꿔도 Service 코드는 유지됩니다.**

</details>

#### 4.2.4. DAO 인터페이스와 예외

**기술 종속 인터페이스의 문제**

```kotlin
interface UserDao {
    fun add(user: User) throws SQLException
}
```

이 인터페이스는 **JDBC에 종속**됩니다.

* JPA로 변경 불가
* Hibernate로 변경 불가
* 테스트 코드도 SQLException을 알아야 함

**인터페이스가 구현 기술을 노출**

#### DataAccessException을 사용하면

```kotlin
interface UserDao {
    fun add(user: User)
}
```

* throws 선언 없음
* 구현체 내부에서 DataAccessException 발생
* 호출자는 필요할 때만 catch

```kotlin
fun register(user: User) {
    try {
        userDao.add(user)
    } catch (e: DuplicateKeyException) {
        // 아이디 중복 처리
    }
}
```

**DAO 인터페이스가 기술로부터 독립**

***

#### 4.2.5. 애플리케이션 예외와 기술 예외의 분리

**기술 예외**

* SQLException
* DataAccessException
* HibernateException

공통 특징:

* 원인: 시스템 / 인프라
* 복구 불가
* 사용자에게 그대로 노출 되지 않음

```kotlin
catch (e: DataAccessException) {
    // 보통 여기서 끝
}
```

***

#### 애플리케이션 예외 (Business Exception)

```kotlin
class DuplicateUserIdException(
    val userId: String
) : RuntimeException()
```

공통 특징:

* 비즈니스 의미 있음
* 사용자 메시지로 변환 가능
* 분기 처리 대상

***

#### 기술 예외 → 애플리케이션 예외 전환 예시

```kotlin
fun add(user: User) {
    try {
        jdbcTemplate.update(...)
    } catch (e: DuplicateKeyException) {
        throw DuplicateUserIdException(user.id)
    }
}
```

<details>

<summary>Controller에서의 처리 예시</summary>

```kotlin
@PostMapping("/users")
fun register(user: User): ResponseEntity<*> =
    try {
        userService.register(user)
        ResponseEntity.ok().build()
    } catch (e: DuplicateUserIdException) {
        ResponseEntity
            .badRequest()
            .body("이미 사용 중인 아이디입니다.")
    }
```

👉 Controller는 DB를 전혀 모릅니다.\
👉 예외 타입만 보고 판단합니다.

</details>
