# 스프링 부트의 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 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 requiredType, Object... args)` | 단일 값(예: `Integer`, `String`) 조회 | `T` | `jdbcTemplate.queryForObject("SELECT count(*) FROM users", Integer.class)` | | `queryForObject(String sql, RowMapper 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 rowMapper, Object... args)` | 여러 행을 객체 리스트로 매핑하여 조회 | `List` | `jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age")))` | | `queryForList(String sql, Class elementType, Object... args)` | 여러 행을 단일 값 리스트로 조회 (예: `List`) | `List` | `jdbcTemplate.queryForList("SELECT name FROM users", String.class)` | | `queryForList(String sql, Object... args)` | 여러 행을 `Map` 리스트로 조회 (컬럼명과 값 쌍) | `List>` | `jdbcTemplate.queryForList("SELECT * FROM users")` | | `queryForMap(String sql, Object... args)` | 단일 행을 `Map`으로 조회 (컬럼명과 값 쌍) | `Map` | `jdbcTemplate.queryForMap("SELECT * FROM users WHERE id = ?", id)` | | `query(String sql, ResultSetExtractor rse, Object... args)` | ResultSet을 커스터마이징하여 결과 처리 | `T` | `jdbcTemplate.query("SELECT * FROM users", rs -> { /* 커스텀 처리 */ return result; })` | | `batchUpdate(String sql, List batchArgs)` | 여러 행을 일괄적으로 삽입/업데이트/삭제, 각 행마다 영향을 받은 행 수 배열 반환 | `int[]` | `jdbcTemplate.batchUpdate("INSERT INTO users VALUES (?, ?)", batchArgs)` | | `call(CallableStatementCreator csc, List declaredParameters)` | 저장 프로시저 호출 | `Map` | `jdbcTemplate.call(csc, params)` (복잡한 예시는 생략) | 1. **`execute`** - DDL(테이블 생성/삭제)이나 결과가 필요 없는 작업에 사용. - 예: 데이터베이스 초기화. 2. **`update`** - 데이터 쓰기 작업에 적합하며, `?` 플레이스홀더를 사용해 안전하게 파라미터 바인딩. - 반환값: 영향을 받은 행 수 (예: 삽입된 행 수). 3. **`queryForObject`** - 단일 값(스칼라)이나 단일 행 조회에 사용. - 결과가 없으면 `EmptyResultDataAccessException` 발생. 4. **`query`** - 여러 행 조회 시 `RowMapper`로 각 행을 객체로 변환. - 예: `User` 객체 리스트 반환. 5. **`queryForList`** - 간단한 리스트 반환에 유용 (컬럼 하나만 조회하거나 `Map`으로 결과 필요 시). - 예: 사용자 이름 리스트. 6. **`queryForMap`** - 단일 행의 모든 컬럼을 키-값 쌍으로 반환. - 결과가 없으면 예외 발생. 7. **`batchUpdate`** - 대량 데이터 삽입/업데이트 시 성능 최적화. - 예: `List`로 여러 행을 한 번에 처리. 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 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 users) { String sql = "INSERT INTO users (name, age) VALUES (?, ?)"; List 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` 인터페이스를 사용하면 **JDBC 결과를 쉽게 Java 객체로 매핑**할 수 있습니다. --- ### **📌 `RowMapper` 예제** ```java import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; public class UserRowMapper implements RowMapper { @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 findAllUsers() { String sql = "SELECT * FROM users"; return jdbcTemplate.query(sql, new UserRowMapper()); } } ``` ✔ `jdbcTemplate.query(sql, new UserRowMapper())` → **쿼리 결과를 `UserRowMapper`를 이용해 변환** ✔ **결과:** `List` 형태로 조회 가능 --- ## **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 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 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` | `ResultSet`을 **Java 객체로 변환** | | `SimpleJdbcInsert` | **INSERT 문을 간단하게 처리** | | `SimpleJdbcCall` | **스토어드 프로시저 호출을 간단하게 처리** | **JdbcTemplate을 활용하면 SQL을 더욱 직관적이고 안전하게 실행할 수 있습니다! 🚀**