# 스프링 부트의 JDBC

스프링 부트에서 JDBC(Java Database Connectivity)는 데이터베이스와의 연결을 관리하고 SQL을 실행하기 위한 기본적인 방법입니다. 스프링 부트는 `Spring JDBC` 모듈을 통해 JDBC를 추상화하여, 반복적인 보일러플레이트 코드를 줄이고 생산성을 높입니다. 특히, `JdbcTemplate` 클래스를 중심으로 간편하게 데이터베이스 작업을 수행할 수 있으며, 스프링 부트의 자동 설정 기능을 활용해 데이터소스(DataSource) 설정도 최소화할 수 있습니다. 

JDBC는 자바 애플리케이션이 관계형 데이터베이스(RDBMS)에 접근하도록 설계된 표준 API입니다. 스프링 부트는 이를 기반으로:
- **연결 관리**: 데이터베이스 연결 풀을 자동 설정.
- **쿼리 실행**: SQL 실행과 결과 매핑을 단순화.
- **예외 처리**: JDBC 예외를 스프링의 `DataAccessException`으로 변환.

## 의존성 추가
스프링 부트에서 JDBC를 사용하려면 `spring-boot-starter-jdbc` 의존성을 추가합니다. 데이터베이스 드라이버도 필요합니다 (예: H2, MySQL).

```kotlin
implementation("org.springframework.boot:spring-boot-starter-jdbc<")
runtimeOnly("com.h2database:h2")
```


## `application.yaml` 설정
데이터소스(DataSource)를 설정합니다. 스프링 부트는 기본적으로 HikariCP라는 고성능 연결 풀을 사용합니다.

```yaml
spring:
  datasource:
    url: jdbc:h2:mem:testdb  # H2 메모리 DB URL
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true  # H2 콘솔 활성화
```


## JdbcTemplate 사용
`JdbcTemplate`은 스프링 JDBC의 핵심 클래스이며, SQL 실행과 결과를 처리하는 메서드를 제공합니다.

```java
@Service
public class UserService {

    private final JdbcTemplate jdbcTemplate;

    public UserService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 사용자 추가
    public void addUser(String name, int age) {
        String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
        jdbcTemplate.update(sql, name, age);
    }

    // 사용자 조회
    public List<User> getAllUsers() {
        String sql = "SELECT id, name, age FROM users";
        return jdbcTemplate.query(sql, (rs, rowNum) -> new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getInt("age")
        ));
    }

    // ID로 사용자 조회
    public User getUserById(Long id) {
        String sql = "SELECT id, name, age FROM users WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getInt("age")
        ));
    }

    // 사용자 삭제
    public void deleteUser(Long id) {
        String sql = "DELETE FROM users WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }
}

@Data
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private int age;
}
```

- **설명**:
  - `update()`: INSERT, UPDATE, DELETE와 같은 쓰기 작업.
  - `query()`: 여러 행을 조회하고 결과를 객체로 매핑.
  - `queryForObject()`: 단일 행을 조회.


## 주요 JdbcTemplate 메서드


| **메서드**                                      | **설명**                                                                                   | **반환 타입**         | **사용 예시**                                  |
|------------------------------------------------|-------------------------------------------------------------------------------------------|----------------------|-----------------------------------------------|
| `execute(String sql)`                          | DDL(예: 테이블 생성)이나 단순 SQL 실행, 결과를 반환하지 않음                                | `void`              | `jdbcTemplate.execute("CREATE TABLE users(id INT)")` |
| `update(String sql, Object... args)`           | INSERT, UPDATE, DELETE와 같은 쓰기 작업 실행, 영향을 받은 행 수 반환                        | `int`               | `jdbcTemplate.update("INSERT INTO users VALUES (?, ?)", name, age)` |
| `update(String sql, PreparedStatementSetter pss)` | PreparedStatement를 커스터마이징하여 쓰기 작업 실행                                     | `int`               | `jdbcTemplate.update("INSERT INTO users VALUES (?, ?)", ps -> { ps.setString(1, name); ps.setInt(2, age); })` |
| `queryForObject(String sql, Class<T> requiredType, Object... args)` | 단일 값(예: `Integer`, `String`) 조회                                              | `T`                 | `jdbcTemplate.queryForObject("SELECT count(*) FROM users", Integer.class)` |
| `queryForObject(String sql, RowMapper<T> rowMapper, Object... args)` | 단일 행을 객체로 매핑하여 조회                                                    | `T`                 | `jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age")), id)` |
| `query(String sql, RowMapper<T> rowMapper, Object... args)` | 여러 행을 객체 리스트로 매핑하여 조회                                              | `List<T>`           | `jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age")))` |
| `queryForList(String sql, Class<T> elementType, Object... args)` | 여러 행을 단일 값 리스트로 조회 (예: `List<String>`)                                | `List<T>`           | `jdbcTemplate.queryForList("SELECT name FROM users", String.class)` |
| `queryForList(String sql, Object... args)`     | 여러 행을 `Map` 리스트로 조회 (컬럼명과 값 쌍)                                             | `List<Map<String, Object>>` | `jdbcTemplate.queryForList("SELECT * FROM users")` |
| `queryForMap(String sql, Object... args)`      | 단일 행을 `Map`으로 조회 (컬럼명과 값 쌍)                                                 | `Map<String, Object>` | `jdbcTemplate.queryForMap("SELECT * FROM users WHERE id = ?", id)` |
| `query(String sql, ResultSetExtractor<T> rse, Object... args)` | ResultSet을 커스터마이징하여 결과 처리                                               | `T`                 | `jdbcTemplate.query("SELECT * FROM users", rs -> { /* 커스텀 처리 */ return result; })` |
| `batchUpdate(String sql, List<Object[]> batchArgs)` | 여러 행을 일괄적으로 삽입/업데이트/삭제, 각 행마다 영향을 받은 행 수 배열 반환            | `int[]`             | `jdbcTemplate.batchUpdate("INSERT INTO users VALUES (?, ?)", batchArgs)` |
| `call(CallableStatementCreator csc, List<SqlParameter> declaredParameters)` | 저장 프로시저 호출                                                                  | `Map<String, Object>` | `jdbcTemplate.call(csc, params)` (복잡한 예시는 생략) |

1. **`execute`**
   - DDL(테이블 생성/삭제)이나 결과가 필요 없는 작업에 사용.
   - 예: 데이터베이스 초기화.

2. **`update`**
   - 데이터 쓰기 작업에 적합하며, `?` 플레이스홀더를 사용해 안전하게 파라미터 바인딩.
   - 반환값: 영향을 받은 행 수 (예: 삽입된 행 수).

3. **`queryForObject`**
   - 단일 값(스칼라)이나 단일 행 조회에 사용.
   - 결과가 없으면 `EmptyResultDataAccessException` 발생.

4. **`query`**
   - 여러 행 조회 시 `RowMapper`로 각 행을 객체로 변환.
   - 예: `User` 객체 리스트 반환.

5. **`queryForList`**
   - 간단한 리스트 반환에 유용 (컬럼 하나만 조회하거나 `Map`으로 결과 필요 시).
   - 예: 사용자 이름 리스트.

6. **`queryForMap`**
   - 단일 행의 모든 컬럼을 키-값 쌍으로 반환.
   - 결과가 없으면 예외 발생.

7. **`batchUpdate`**
   - 대량 데이터 삽입/업데이트 시 성능 최적화.
   - 예: `List<Object[]>`로 여러 행을 한 번에 처리.

8. **`call`**
   - 저장 프로시저 호출에 사용되며, 복잡한 경우에만 필요.


## 데이터베이스 초기화
스프링 부트는 `schema.sql`과 `data.sql` 파일을 통해 데이터베이스를 초기화할 수 있습니다.

- **`src/main/resources/schema.sql`**:
  ```sql
  CREATE TABLE users (
      id BIGINT AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(255),
      age INT
  );
  ```

- **`src/main/resources/data.sql`**:
  ```sql
  INSERT INTO users (name, age) VALUES ('John', 25);
  INSERT INTO users (name, age) VALUES ('Jane', 30);
  ```

- **설정**: `spring.sql.init.mode=always`로 활성화 (`application.yaml`).


## 트랜잭션 관리
JDBC 작업에서 트랜잭션을 적용하려면 `@Transactional` 어노테이션을 사용합니다.

```java
@Service
public class UserService {

    private final JdbcTemplate jdbcTemplate;

    public UserService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional
    public void addUserWithTransaction(String name, int age) {
        String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
        jdbcTemplate.update(sql, name, age);
        // 예외 발생 시 롤백
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}
```

- **설명**: 트랜잭션 내에서 실행되며, 예외 발생 시 롤백됩니다.


## 커스텀 데이터소스 설정 (옵션)
기본 설정 대신 수동으로 `DataSource`를 정의할 수 있습니다.

```java
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:h2:mem:testdb");
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

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


## 사용 예시
```java
@Service
public class UserService {

    private final JdbcTemplate jdbcTemplate;

    public UserService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 단일 사용자 조회
    public User findUserById(Long id) {
        String sql = "SELECT id, name, age FROM users WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getInt("age")
        ), id);
    }

    // 모든 사용자 조회
    public List<User> findAllUsers() {
        String sql = "SELECT id, name, age FROM users";
        return jdbcTemplate.query(sql, (rs, rowNum) -> new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getInt("age")
        ));
    }

    // 사용자 추가
    public void addUser(String name, int age) {
        String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
        jdbcTemplate.update(sql, name, age);
    }

    // 배치 삽입
    public void batchAddUsers(List<User> users) {
        String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
        List<Object[]> batchArgs = users.stream()
            .map(user -> new Object[]{user.getName(), user.getAge()})
            .collect(Collectors.toList());
        jdbcTemplate.batchUpdate(sql, batchArgs);
    }
}

@Data
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private int age;
}
```



## 스프링 부트에서 JPA와 JDBC(JdbcTemplate)의 비교

| **특징**            | **JPA**                                      | **JdbcTemplate**                            |
|---------------------|---------------------------------------------|--------------------------------------------|
| **접근 방식**       | 객체 지향적 (ORM 기반)                      | SQL 중심 (직접 쿼리 작성)                  |
| **추상화 수준**     | 높음 (엔티티와 매핑 관리)                   | 낮음 (SQL과 JDBC API 추상화)               |
| **주요 클래스**     | `EntityManager`, `JpaRepository`            | `JdbcTemplate`                             |
| **의존성**          | `spring-boot-starter-data-jpa`              | `spring-boot-starter-jdbc`                 |
| **데이터베이스 제어**| 엔티티와 매핑으로 간접 제어                 | SQL로 직접 제어                            |

- **JPA**: 객체와 데이터베이스 테이블 간의 매핑을 통해 SQL을 자동 생성하고 관리.
- **JdbcTemplate**: 개발자가 직접 SQL을 작성하며, JdbcTemplate이 JDBC 작업을 단순화.


------





# **Spring JDBC Template의 주요 기능 설명**  

Spring의 `JdbcTemplate`은 SQL을 더욱 간결하고 효율적으로 실행할 수 있도록 돕는 핵심적인 데이터 액세스 기술입니다.  
이 글에서는 **SQL 파라미터 바인딩(`SqlParameterSource`)**, **행 매핑(`RowMapper`)**, **간단한 JDBC 처리(`SimpleJdbc` 클래스들)**에 대해 설명하겠습니다.  

---

## **1. SqlParameterSource - SQL 파라미터 바인딩**  

일반적으로 SQL을 실행할 때 **파라미터를 바인딩**해야 합니다.  
Spring은 이를 쉽게 처리할 수 있도록 **`SqlParameterSource` 인터페이스**를 제공합니다.  

### **✅ 주요 구현체**
| 구현체 | 설명 |
|--------|------|
| `MapSqlParameterSource` | **키-값(Map) 형태**로 파라미터 바인딩 |
| `BeanPropertySqlParameterSource` | **Java 객체(Bean)의 필드 값**을 자동으로 바인딩 |
| `NamedParameterJdbcTemplate` | **이름 기반**(`:paramName`)으로 SQL 파라미터 바인딩 가능 |

---

### **📌 `MapSqlParameterSource` 예제**
키-값 형태의 데이터를 SQL에 바인딩하는 방법입니다.

```java
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;

@Repository
public class UserRepository {

    private final NamedParameterJdbcTemplate jdbcTemplate;

    public UserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void updateUserEmail(Long userId, String newEmail) {
        String sql = "UPDATE users SET email = :email WHERE id = :id";

        MapSqlParameterSource params = new MapSqlParameterSource()
                .addValue("email", newEmail)
                .addValue("id", userId);

        jdbcTemplate.update(sql, params);
    }
}
```
✔ **`:email`, `:id`** → **이름 기반 파라미터 바인딩**  
✔ **`MapSqlParameterSource`** → **SQL 실행 시 필요한 파라미터 전달**  

---

### **📌 `BeanPropertySqlParameterSource` 예제**
Java 객체의 필드 값을 자동으로 SQL에 바인딩하는 방법입니다.

```java
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {

    private final NamedParameterJdbcTemplate jdbcTemplate;

    public UserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void insertUser(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (:name, :email)";

        BeanPropertySqlParameterSource params = new BeanPropertySqlParameterSource(user);

        jdbcTemplate.update(sql, params);
    }
}
```
✔ **User 객체의 필드(`name`, `email`)**가 자동으로 SQL의 `:name`, `:email`에 매핑됨  

---

## **2. RowMapper - SQL 결과를 객체로 변환**  

데이터베이스에서 조회한 결과(ResultSet)를 **Java 객체로 변환**하는 역할을 합니다.  
Spring의 `RowMapper<T>` 인터페이스를 사용하면 **JDBC 결과를 쉽게 Java 객체로 매핑**할 수 있습니다.  

---

### **📌 `RowMapper` 예제**
```java
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setEmail(rs.getString("email"));
        return user;
    }
}
```
✔ `mapRow()` → **`ResultSet`에서 데이터를 읽어 `User` 객체로 변환**  

---

### **📌 `RowMapper`를 활용한 Repository 예제**
```java
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List<User> findAllUsers() {
        String sql = "SELECT * FROM users";
        return jdbcTemplate.query(sql, new UserRowMapper());
    }
}
```
✔ `jdbcTemplate.query(sql, new UserRowMapper())` → **쿼리 결과를 `UserRowMapper`를 이용해 변환**  
✔ **결과:** `List<User>` 형태로 조회 가능  

---

## **3. SimpleJdbc 클래스 - 간단한 JDBC 처리**  

Spring에서는 **JDBC 작업을 단순화**하기 위해 `SimpleJdbc` 관련 클래스를 제공합니다.  
이들은 `JdbcTemplate`보다 **더 직관적이고 간단한 코드로 데이터베이스 작업을 처리**할 수 있습니다.  

### **✅ 주요 클래스**
| 클래스 | 설명 |
|--------|------|
| `SimpleJdbcInsert` | **INSERT 작업을 간단하게 처리** |
| `SimpleJdbcCall` | **스토어드 프로시저(Stored Procedure) 호출을 간단하게 처리** |

---

### **📌 `SimpleJdbcInsert`를 이용한 INSERT 예제**
```java
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Repository
public class UserRepository {

    private final SimpleJdbcInsert simpleJdbcInsert;

    public UserRepository(DataSource dataSource) {
        this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("users")
                .usingGeneratedKeyColumns("id");
    }

    public Long insertUser(String name, String email) {
        Map<String, Object> params = new HashMap<>();
        params.put("name", name);
        params.put("email", email);

        return simpleJdbcInsert.executeAndReturnKey(params).longValue();
    }
}
```
✔ `withTableName("users")` → **테이블명 지정**  
✔ `usingGeneratedKeyColumns("id")` → **자동 생성된 키(`id`)를 반환**  

---

### **📌 `SimpleJdbcCall`을 이용한 스토어드 프로시저 호출**
```java
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.Map;

@Repository
public class UserRepository {

    private final SimpleJdbcCall simpleJdbcCall;

    public UserRepository(DataSource dataSource) {
        this.simpleJdbcCall = new SimpleJdbcCall(dataSource)
                .withProcedureName("get_user_by_id");
    }

    public Map<String, Object> getUserById(Long userId) {
        return simpleJdbcCall.execute(Map.of("user_id", userId));
    }
}
```
✔ `withProcedureName("get_user_by_id")` → **스토어드 프로시저 지정**  
✔ `execute(Map.of("user_id", userId))` → **프로시저 실행 후 결과 반환**  

---

## **4. 정리**
### **✅ 주요 개념 정리**
| 기능 | 설명 |
|------|------|
| `SqlParameterSource` | SQL에 안전하게 **파라미터 바인딩** |
| `RowMapper<T>` | `ResultSet`을 **Java 객체로 변환** |
| `SimpleJdbcInsert` | **INSERT 문을 간단하게 처리** |
| `SimpleJdbcCall` | **스토어드 프로시저 호출을 간단하게 처리** |

**JdbcTemplate을 활용하면 SQL을 더욱 직관적이고 안전하게 실행할 수 있습니다! 🚀**