본문 바로가기

MySql

[JDBC] JDBC Template

 


 

개요

⭐ JDBC Template을 알아보기 전, 왜 필요하게 되었는지 간단하게 알아보겠습니다. JDBC Template을 사용하기 전에는 개발자들이 직접 JDBC를 통하여, SQL문을 작성하였습니다. JDBC로 직접 SQL문을 작성했을 때, 다음과 같은 문제가 발생하였습니다.

 

  1. 중복되고 반복적인 SQL 코드
    • 비즈니스 로직과 SQL이 혼재되어 코드의 가독성과 유지보수성이 저하됩니다. (관심사의 분리가 잘 안됩니다.)
  2. 객체 매핑의 불편함과 타입 오류 가능성
    • `ResultSet`을 수동으로 읽어 객체에 매핑해야 합니다.
    • 예: `resultSet.getInt("age")` 에서처럼 수동으로 `age`가 null이면 `NullPointerException`이 발생할 수도 있습니다.
  3. 컴파일 타임 검증 불가  
    • SQL 문법 오류, 잘못된 컬럼명 등은 컴파일 타임이 아닌 런타임에서야 오류 발생
    • → 디버깅 비용과 테스트 시간 증가합니다.
  4. 예외 처리의 불편함
    • JDBC는 모든 SQL 관련 오류를 Checked Exception (SQLException) 으로 던집니다.
    • DB 예외 유형별로 구체적 처리 어렵고, 코드가 지저분해집니다.
  5. 트랜잭션, 커넥션 관리의 반복
    • Connection → Statement로 Query 실행 → ResultSet 처리 → 자원 정리(close)를 모두 수동으로 해야 합니다.
    • 자원 해제을 놓치면, 커넥션 누수로 인해 서버가 과부하/다운됩니다.

 

 

💡이러한 문제를 해결을 위해 `Persistence Framework`가 등장했습니다! `Persistence Framework`는 2가지가 있습니다.

  • SQL Mapper : JDBC Template, MyBatis
  • ORM : JPA, Hibernate

→ 이 중 `JDBC Template`에 대해서 알아보겠습니다.

 


 

JDBC Template

정의

SQL 실행 및 JDBC 관련 작업(연결, 실행, 예외 처리, 자원 정리 등)을 간단하게 만들어 주는 클래스입니다.

 

 

의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
}
  • JDBC를 사용하기 위해 추가한 의존성에 JDBC Template이 포함되어있습니다.
  • `spring-boot-starter-jdbc`를 사용하면 `DataSource`를 기반으로 `JdbcTemplate` 빈이 자동 등록됩니다.

 

 

구조

JDBC Template 는 JDBC Driver 를 추상화하여 동작합니다. (dbcp = db connection pool)

 

 

역할

  1. DB 연결을 관리합니다.
    • `DataSource`를 통해 Connection을 자동으로 얻고, 닫아줍니다.
  2. SQL 실행 처리합니다.
    • `PreparedStatement` 생성 및 파라미터 바인딩, SQL 실행 수행합니다.
  3. ResultSet 처리 반복합니다.
    • 쿼리 결과를 반복하며, 각 행마다 RowMapper를 호출합니다.
  4. 예외 처리를 단순화합니다.
    • `SQLException`을 `DataAccessException`으로 추상화합니다.
  5. 리소스를 자동으로 해제합니다.
    • Connection, Statement, ResultSet을 try-catch-finally 없이 자동으로 해제해줍니다.

 

더보기

RowMapper

  • Query 수행 결과(ResultSet)를 한줄 씩 읽어와 Java 객체로 변환할 때 사용합니다.
  • RowMapper로 매핑 코드를 재사용할 수 있습니다.
  • 😂 하지만, Query 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요합니다.

 

 

 

예제 코드

// UserRowMapper.java
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;


public class UserRowMapper implements RowMapper<User> {

  // JDBCTemplate 에서 row 응답을 mapRow() 메서드에 rs 파라미터로 넘겨주어 객체에 매핑하기 쉽도록 도와준다.
  @Override
  public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    var user = new User();
    user.setId(rs.getInt("ID"));
    user.setName(rs.getString("NAME"));
    return user;
  }
}
  • RowMapper를 구현한 구현체를 직접 만들어야 합니다.
  • RowMapper.mapRow : RowMapper를 상속받아 mapRow() 메서드를 구현하면, JDBC Template에서 row 응답을 mapRow() 메서드에 rs 파라미터로 넘겨주어 객체에 매핑하기 쉽도록 도와줍니다.

 

// DataRepository.java
package com.thesun4sky.jdbc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class DataRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 테이블 생성
    public void createTable() {
	    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL, name VARCHAR(255))");
    }

    // 사용자 추가 (Create)
    public void insertUser(String name) {
        jdbcTemplate.update("INSERT INTO users (name) VALUES (?)", name);
    }

    // 사용자 ID로 이름 조회 (Read)
    public String findUserNameById(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT name FROM users WHERE id = ?",
            new Object[]{id},
            String.class
        );
    }
    
    // 사용자 ID로 User 조회 (Read)
    public User findUserById(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            new UserRowMapper(),
            id
        );
    }

    // 사용자 이름 변경 (Update)
    public void updateUser(Long id, String newName) {
        jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", newName, id);
    }

    // 사용자 삭제 (Delete)
    public void deleteUser(Long id) {
        jdbcTemplate.update("DELETE FROM users WHERE id = ?", id);
    }
}
  • 코드가 JDBC를 직접 사용했을 때보다 훨씬 간결해집니다.
  • findUserById : jdbcTemplate.queryForObject() 메서드에서는 두번쨰 인자로 RowMapper 를 넣어줄 경우 해당 RowMapper mapRow() 메서드를 사용하여 응답을 하도록 동작한다.

 

// JdbcApplication.java
package com.thesun4sky.jdbc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JdbcApplication {

	public static void main(String[] args) {
		// 어플리케이션 실행 컨텍스트 생성
		var context = SpringApplication.run(JdbcApplication.class, args);
		// 데이터 조회 클래스 빈 조회
		var repository = context.getBean(DataRepository.class);

		// 테이블 생성
		repository.createTable();
		// 유저정보 추가
		repository.insertUser("Teasun Kim");
		// 유저정보 조회
		System.out.println("User Name: " + repository.findUserNameById(1L));
	}
}
  • 기존 JDBC를 직접 활용하는 방법에서는 서비스 로직과 DB 접근 로직이 같은 곳에 존재하여 역할분리가 제대로 이루어지지 않았지만, JDBC Template을 사용하여 역할분리가 잘 된 것을 확인할 수 있습니다.

 

 

단점

  • SQL을 문자열로 직접 작성해야 합니다.
    • 복잡한 SQL 로직은 유지보수가 어렵습니다.
  • 객체 지향적 쿼리 작성이 어렵습니다.
    • Entity 간의 연관 관계 매핑 등을 직접 처리해야 합니다.
    • 예: 1:N, N:1 관계를 직접 조인하고 매핑해야 합니다.
  • 매핑 코드 반복
    • `ResultSet` → 객체로 변환하는 `RowMapper` 코드가 반복됩니다.
  • 동적 쿼리 작성이 불편합니다.
    • if, where, and 조건 등을 기반으로 한 동적 SQL 생성이 번거롭습니다.

 

 

💡 이러한 단점을 각각 보완하기 위해 MyBatis, JPA, QueryDSL 등이 있습니다.