티스토리 뷰

정리용/DB

[DB 기초] 6-2. JDBC Template

hee-ya07 2025. 3. 19. 22:49

0. JDBC Template

출처 : ASAC 수업자료

:: JdbcTemplate 클래스로 제공,

:: SQL 쿼리 실행, 트랜잭션 관리, 예외 처리 등을 자동화

JDBC Template의 주요 기능 내용 
1. 간편 사용 / 간결한 코드 - SQL 쿼리 실행을 간소화
- 내부적으로 Connection, Statement, ResultSet을 자동으로 관리
- SELECT, INSERT, UPDATE, DELETE 쿼리를 처리하는 메서드를 제공
- 리소스를 명시적으로 close()할 필요 X / 예외 시, 자동으로 close() -> 코드가 간결
2. 예외 처리 - JDBC의 SQLExceptionsSpring의 DataAccessException으로 변환 - 일관된 예외처리 O
- DataAccessException은 SQLException의 다양한 하위 클래스로 구체적인 예외 처리 가능  
3. 파라미터화된 쿼리 지원 - PreparedStatement를 사용하여 SQL 인젝션 방지와 파라미터화된 쿼리를 손쉽게 처리 가능
4. 간편 트랜젝션  - Auto-Commit:False시에도 손쉽게 처리 가능
5. 구조적인 반복 해결 Connection 취득, 사용, 반환을 JdbcTemplate 내부에서 진행 -> 아래의 구현 필요 x
 -> Try-with-Resources / SQLException / Transaction에 대한 구현을 할 필요가 없다는 말! 

0.1 import org.springframework.jdbc.core.JdbcTemplate;

메서드  내용
1. update() INSERT, UPDATE, DELETE 등의 쿼리 실행
2. queryForObject() 단일 객체를 반환하는 SELECT 쿼리 실행
3. query() 여러 결과를 반환하는 SELECT 쿼리 실행 // List 형식 반환
3-1. queryForStream() 여러 결과를 반환하는 SELECT 쿼리 실행 // stream 형식 반환, 변환 필요
4. batchUpdate() 배치 쿼리 실행
5. execute() DB에서의 작업 실행

1. Spring JDBC Template의 작업 흐름

1.0 DataSource 설정

  • DataSource :: DB연결을 관리하는 객체
    :: Connection Pool(HikariCP, Tomcat JDBC Pool)을 통한 논리적 연결
    :: 연결의 재사용을 통한 효율성 증가
  • Spring Boot 2+ 부터 DataSource 표준은 HikariCP(Connection Pool)
// 0.1) DataSource 설정
HikariConfig config = new HikariConfig();
config.setJdbcUrl(URL);
config.setUsername(USER);
config.setPassword(PASSWORD);
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setMaximumPoolSize(20); // Connection Pool 최대 커넥션 수 설정
config.setMinimumIdle(10); // 사용을 위해 커넥션 풀에 유지해놓을 커넥션 수 (IDLE)
// setConnectionTimeout(30000) // 커넥션을 얻기 위해 대기중에 해당 시간이 만료되면 예외 발생

// 0.1) DataSource 생성
HikariDataSource hikariDataSource = new HikariDataSource(config);

// 1) 접속을 Connection Pool 에서 가져오는 코드
Connection connection = hikariDataSource.getConnection();
더보기
import org.apache.tomcat.jdbc.pool.DataSource; // HikariCP도 사용 가능

public class DataSourceConfig {
    public DataSource dataSource() {
        DataSource dataSource = new DataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("user");
        dataSource.setPassword("password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }
}

1.1 JdbcTemplate 객체 생성

  1. JdbcTemplate은 DataSource를 통해 연결된 DB에 대해 SQL 쿼리를 실행하는 핵심 클래스
  2. JdbcTemplate은 DataSource를 사용하여 DB 연결을 관리 / 쿼리 실행 / 결과 매핑 / 예외 처리 등을 간소화
  3. JdbcTemplate 생성자에 DataSource 객체를 전달하여 JdbcTemplate 객체를 초기화
  4. 내부적으로 커넥션을 DataSource에서 가져와 쿼리 실행 시 자동으로 연결을 생성하고, 실행 후 리소스 반환(close)
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;

// JdbcTemplate 적용 클래스
public class UserDao {
    private JdbcTemplate jdbcTemplate;

    public UserDao(DataSource dataSource) { //DAO:: CRUD처리 / DataSource:: DB연결 관리 객체 
        this.jdbcTemplate = new JdbcTemplate(dataSource); // JdbcTemplate은 DataSource로 연결 사용
    }
}

1.2 쿼리 실행 

 

더보기

1.2-1 RowMapper 인터페이스 - 쿼리 결과

// RowMapper 인터페이스 시그니처
public interface RowMapper<T> {
    T mapRow(ResultSet resultSet, int rowNum) throws SQLException;
}
인자 내용
T 변환할 객체의 타입 / 쿼리 결과를 저장할 Java 클래스 타입
mapRow(ResultSet resultSet, int rowNum) - resultSet :: DB에서 반환된 현재 행의 ResultSet
- rowNum :: 현재 행의 번호(0~) / 여러 행을 처리할 때 유용
public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet resultSet, int rowNum) throws SQLException {
        return new User(
            resultSet.getInt("id"),
            resultSet.getString("name"),
            resultSet.getInt("age"),
            resultSet.getString("job")
        );
    }
}

// 0개 결과: EmptyResultDataAccessException이 발생
// 2개 이상의 결과(queryForObject에서): IncorrectResultSizeDataAccessException이 발생

인터페이스이므로 위와 같이 구현하여 사용 


+ 람다식 구현 가능

- 하나의 메서드인 mapRow만을 위한 클래스이므로 익명 구현 객체를 통해 간략하게 변환 가

public User getUserById(int userId) {
    String sql = "SELECT id, name, age, job FROM user WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new Object[]{userId},
    /* 해당 부분이 람다로 구성 */
        (resultSet, rowNum) -> new User(
            resultSet.getInt("id"),
            resultSet.getString("name"),
            resultSet.getInt("age"),
            resultSet.getString("job")
        )
    );
}

1. 데이터 조회 (SELECT)

:: queryForObject() 메서드는 하나의 결과를 조회할 때 사용(다수 조회시, query() )

:: 결과를 RowMapper를 통해 매핑하여 반환 / ResultSet을 객체로 변환

public <T> T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper)
인자 내용 
String sql :: 실행할 SQL 쿼리
Ex) "SELECT * FROM user WHERE id = ?"
Object[] args :: SQL 쿼리에 사용될 파라미터
:: 배열의 값들은 SQL 쿼리에서 ?로 지정된 부분에 바인딩 / 없다면 null 전
Ex) new Object[]{userId}와 같이 쿼리의 ?에 대응되는 값을 전달 
RowMapper<T> rowMapper - RowMapper 인터페이스 구현체 :: 쿼리 결과에서 각 행을 Java 객체로 매핑하는 역할
:: ResultSet을 Java 객체로 변환하는 로직을 작성
// 단일 결과 반환
public User getUserById(int userId) {
    String sql = "SELECT * FROM user WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new Object[]{userId}, new UserRowMapper());
}
// 다중 결과 반환
public List<User> getAllUsers() {
    String sql ="SELECT * FROM \"user\"";
    return jdbcTemplate.query(sql, new UserRowMapper());
}

public List<User> getAllUsers() {
    String sql ="SELECT * FROM \"user\"";
    return jdbcTemplate.queryForStream((sql, new UserRowMapper()).toList();;
}

2. 데이터 삽입 / 삭제 / 업데이트 ( INSERT, UPDATE, DELETE)

- update() :: INSERT, UPDATE, DELETE 작업을 처리할 때 사용

// update() 메서드 시그니처
public int update(String sql, Object[] args) // 반환형이 int임을 기억하자
인자 내용 
String sql :: 실행할 SQL 쿼리( UPDATE, INSERT, DELETE) 
Ex) "UPDATE user SET name = ?, age = ? WHERE id = ?"
Object[] args :: SQL 쿼리에 사용될 파라미터
:: 배열의 값들은 SQL 쿼리에서 ?로 지정된 부분에 바인딩 / 없다면 null 전달 
Ex) new Object[]{userId}와 같이 쿼리의 ?에 대응되는 값을 전달 
// 데이터 삽입 (INSERT)
public void addUser(User user) {
    String sql = "INSERT INTO users (name) VALUES (?)";
    jdbcTemplate.update(sql, user.getName());
}

// 데이터 업데이트 (UPDATE)
public void updateUser(User user) {
    String sql = "UPDATE users SET name = ? WHERE id = ?";
    jdbcTemplate.update(sql, user.getName(), user.getId());
}
@Repository
@RequiredArgsConstructor
public class UserJdbcTemplateDao {
    private final JdbcTemplate jdbcTemplate;

    public User save(String name, Integer age, String job, String specialty) {
        // (A) INSERT USER
        String createUserQuery = "INSERT INTO \"user\" (name, age, job, specialty, created_at) VALUES (?, ?, ?, ?, ?)";
        Object[] createUserParams = new Object[]{
                name,
                age,
                job,
                specialty,
                LocalDateTime.now()
        };
        this.jdbcTemplate.update(
                createUserQuery,
                createUserParams
        );
        // (B) SELECT id - MySQL:last_insert_id()->id / PostgresQL:currval()->lastval/lastval()->lastval
        String lastInsertIdQuery = "SELECT lastval()";
        int createdUserId = this.jdbcTemplate.queryForObject(
                lastInsertIdQuery,
                int.class
        );
        // (C) SELECT USER
        String getUserQuery = "SELECT * FROM \"user\" WHERE id = ?";
        int getUserParams = createdUserId;
        return this.jdbcTemplate.queryForObject(
                getUserQuery,
                (resultSet, rowNum) -> new User(
                        resultSet.getInt("id"),
                        resultSet.getString("name"),
                        resultSet.getInt("age"),
                        resultSet.getString("job"),
                        resultSet.getString("specialty"),
                        resultSet.getTimestamp("created_at")
                                .toInstant()
                                .atZone(ZoneId.systemDefault())
                                .toLocalDateTime()
                ),
                getUserParams
        );
    }
}

1.3 배치 작업 (Batch Operations)

:: 배치 작업은 대량의 데이터를 효율적으로 처리 가능

public void batchUpdate(List<User> users) {
    String sql = "INSERT INTO users (name) VALUES (?)";
    List<Object[]> batchArgs = new ArrayList<>();
    for (User user : users) {
        batchArgs.add(new Object[]{user.getName()});
    }
    jdbcTemplate.batchUpdate(sql, batchArgs);
}

1.4  예외 처리

  • JDBC의 SQLExceptions을 Spring의 DataAccessException으로 변환 - 일관된 예외처리 O
  • DataAccessException은 Spring에서 SQLException을 추상화한 예외 클래스
    • DataAccessException은 SQLException의 다양한 하위 클래스로 구체적인 예외 처리 가능 
    • DuplicateKeyException, EmptyResultDataAccessException 등 다양한 예외 처리가 가능
try {
    jdbcTemplate.update("INSERT INTO user (name, age) VALUES (?, ?)", "John", 30);
} catch (DataAccessException e) {
    // 예외 처리 코드
    e.printStackTrace();
}

 


1.5 트랜잭션 처리

:: JdbcTemplate은 트랜잭션을 지원

:: 트랜잭션의 수동 제어 시, DataSourceTransactionManager 사용

트랜젝션 관리 방식 내용
(A) PlatformTransactionManager - Spring에서 제공하는 추상화된 트랜잭션 관리 방식
- 트랜잭션의 시작, 커밋, 롤백과 같은 관리 작업을 추상화하여 트랜잭션 관리를 단순화
- 구현체의 종류 :: DataSourceTransactionManager, JpaTransactionManager
(B) DataSourceTransactionManager - PlatformTransactionManager의 하위 클래스 / 구현체
- DataSource에 연결된 DB 트랜잭션을 관리하는 데 특화
- 선언적 트랜잭션 관리(예: @Transactional 어노테이션)와 함께 사용

절차 일반적인 절차 JdbcTemplate + PlatformTransactionManager의 경우 
1) 트랜잭션 생성 Connection 생성 PlatformTransactionManager
2) 트랜잭션 참여 기존에 앞서 생성된 Connection 에 연결 JdbcTemplate 
3) 트랜잭션 처리 성공 시 Commit 혹은 실패 시 Rollback PlatformTransactionManager

트랜젝션 유형 내용
(A) 1 쿼리 1 트랜잭션 (DEFAULT)  :: 쿼리 한개에 하나의 트랜잭션으로 수행할때, Auto-Commit
:: JdbcTemplate의 setDataSource() 메서드를 통해 DataSource를 설정
:: DataSource에서 연결된 커넥션에 대해 트랜잭션을 직접 관리
(B) N 쿼리 1 트랜잭션 :: 여러 개의 쿼리를 하나의 트랜잭션으로 묶어서 실행
:: 트랜잭션 동기화 필요(트랜젝션 작업 끝까지 같은 Connection 유지)
:: Auto-Commit 옵션 false -> 개발자가 직접 Connection의 Commit을 관리 
// 트랜잭션 처리 및 예외 발생 시 자동 롤백

@Transactional // 메서드 내의 모든 작업을 트랜잭션으로 묶기 가능 
public void addUserWithTransaction(User user) {
    jdbcTemplate.update("INSERT INTO user (name, age, job) VALUES (?, ?, ?)", user.getName(), user.getAge(), user.getJob());
    // 예외가 발생하면 자동으로 롤백
}
더보기
@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private DataSourceTransactionManager transactionManager;

public void addUserWithTransaction(User user) {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        String sql = "INSERT INTO users (name) VALUES (?)";
        jdbcTemplate.update(sql, user.getName());
        transactionManager.commit(status);  // 트랜잭션 커밋
    } catch (Exception ex) {
        transactionManager.rollback(status);  // 예외 발생 시 롤백
        throw ex;
    }
}

 

import org.springframework.transaction.support.TransactionTemplate;

public class UserService {
    private JdbcTemplate jdbcTemplate;
    private TransactionTemplate transactionTemplate;

    public void addUser(User user) {
        transactionTemplate.execute(status -> {
            jdbcTemplate.update("INSERT INTO user (name, age, job) VALUES (?, ?, ?)",
                user.getName(), user.getAge(), user.getJob());
            return null;
        });
    }
}

2. 예시를 통한 이해

2.1 JdbcTemplate로 DBC API 의 3개 인터페이스 간편 구현

@Repository
@RequiredArgsConstructor
public class UserJdbcTemplateDao {
    private final JdbcTemplate jdbcTemplate;

    public User findById(int userId) {
        String getUserQuery = "SELECT * FROM \"user\" WHERE id = ?";
        int getUserParams = userId;
        return this.jdbcTemplate.queryForObject( // 한개의 결과를 반환, 다중 조회 시, query()
                getUserQuery, // SQL문
                (resultSet, rowNum) -> new User( // 결과 반환 값으로 RowMapper에 대한 람다
                        resultSet.getInt("id"),
                        resultSet.getString("name"),
                        resultSet.getInt("age"),
                        resultSet.getString("job"),
                        resultSet.getString("specialty"),
                        resultSet.getTimestamp("created_at")
                                .toInstant()
                                .atZone(ZoneId.systemDefault())
                                .toLocalDateTime()
                ),
                getUserParams // SQL에 들어갈 파라미터 값
        );
    }
}

2.1 트랜젝션의 동기화 - DataSourceUtils.getConnection() 

:: JDBC Template은 내부에서 DataSource인지, DataSourceUtils의 메서드 호출을 통해 DB연결을 가져옴

:: 이 때, 어떤 걸로 가져오냐에 따라 그 역할의 차이가 있음

특징 dataSource.getConnection() DataSourceUtils.getConnection()
기본 동작 항상 새로운 커넥션을 가져옴 1) 현재 스레드의 트랜잭션 상태를 확인,
2) 트랜잭션이 있으면 해당 트랜잭션에 바인딩된 커넥션을 반환 (하나를 공유)
3) 없으면 새 커넥션을 반환
커넥션 반환 직접 커넥션 반환 관리
트랜잭션과 무관하게 동작
트랜잭션이 관리되는 경우, 트랜잭션 종료 시 커넥션이 자동으로 반환,
트랜잭션에 바인딩되지 않은 경우만, 수동 반환 필요

출처 : ASAC 수업자료

    • 여기서 DataSourceUtils의 경우, 공유하는 작업이 모두 끝나기 전 강제로 반환을 한다면 작업에서의 오류가 발생
    • DataSourceConfig에서 등록된 DataSource Bean을 setDataSource() 메서드를 통해 JdbcTemplate에 설정
    •  execute 함수를 보면 Connection 이 들어있고, DataSourceUtils 통해 가져옴

2.2 트랜젝션 처리의 간소화 

출처 : ASAC 수업자료

트랜젝션 유형 내용
(A) 1 쿼리 1 트랜잭션 (DEFAULT)  :: 쿼리 한개에 하나의 트랜잭션으로 수행할때, Auto-Commit
:: JdbcTemplate의 setDataSource() 메서드를 통해 DataSource를 설정
:: DataSource에서 연결된 커넥션에 대해 트랜잭션을 직접 관리
(B) N 쿼리 1 트랜잭션 :: 여러 개의 쿼리를 하나의 트랜잭션으로 묶어서 실행
:: 트랜잭션 동기화 필요(트랜젝션 작업 끝까지 같은 Connection 유지)
:: Auto-Commit 옵션 false -> 개발자가 직접 Connection의 Commit을 관리 
트랜젝션 관리 방식 내용
(A) PlatformTransactionManager - Spring에서 제공하는 추상화된 트랜잭션 관리 방식
- 트랜잭션의 시작, 커밋, 롤백과 같은 관리 작업을 추상화하여 트랜잭션 관리를 단순화
- 구현체의 종류 :: DataSourceTransactionManager, JpaTransactionManager
(B) DataSourceTransactionManager - PlatformTransactionManager의 하위 클래스 / 구현체
- DataSource에 연결된 DB 트랜잭션을 관리하는 데 특화
- JDBC 트랜잭션 관리가 필요한 경우 사

참조 

ASAC 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함