Files
spring-boot-examples/docs/34_jdbc.md
2025-04-08 19:56:24 +09:00

20 KiB

스프링 부트의 JDBC

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

JDBC는 자바 애플리케이션이 관계형 데이터베이스(RDBMS)에 접근하도록 설계된 표준 API입니다. 스프링 부트는 이를 기반으로:

  • 연결 관리: 데이터베이스 연결 풀을 자동 설정.
  • 쿼리 실행: SQL 실행과 결과 매핑을 단순화.
  • 예외 처리: JDBC 예외를 스프링의 DataAccessException으로 변환.

의존성 추가

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

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

application.yaml 설정

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

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 실행과 결과를 처리하는 메서드를 제공합니다.

@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.sqldata.sql 파일을 통해 데이터베이스를 초기화할 수 있습니다.

  • src/main/resources/schema.sql:

    CREATE TABLE users (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255),
        age INT
    );
    
  • src/main/resources/data.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 어노테이션을 사용합니다.

@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를 정의할 수 있습니다.

@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);
    }
}

사용 예시

@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에 바인딩하는 방법입니다.

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이름 기반 파라미터 바인딩
MapSqlParameterSourceSQL 실행 시 필요한 파라미터 전달


📌 BeanPropertySqlParameterSource 예제

Java 객체의 필드 값을 자동으로 SQL에 바인딩하는 방법입니다.

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 예제

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 예제

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 예제

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을 이용한 스토어드 프로시저 호출

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> ResultSetJava 객체로 변환
SimpleJdbcInsert INSERT 문을 간단하게 처리
SimpleJdbcCall 스토어드 프로시저 호출을 간단하게 처리

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