새소식

프레임워크/Spring

Spring Data JDBC - JdbcTemplate

  • -

💡 JdbcTemplate은 엄밀히 말하자면 Spring Data가 아닌 Spring에서 제공되고 있습니다. 그러나, 이를 기반으로 더욱 편리하게 사용하도록 만든 것이 Spring Data JDBC이므로 이 항목에서 JdbcTemplate을 이야기하도록 하겠습니다.


JDBC

JDBC란 Java Database Connectivity의 약어로, 자바와 데이터베이스를 연결해 주는 자바 API입니다.

 

JDBC가 나오기 이전에는 데이터베이스 벤더(Oracle, MySQL 등) 별로 문법이 상이하여 사용하던 데이터베이스를 다른 벤더로 바꾸는 경우, 코드로 작성한 SQL 문을 벤더 문법에 맞게 하나하나 수정해주어야 하는 불편함이 있었습니다.

 

이러한 프로그램이 데이터베이스에 의존하는 문제를 해결하고자 탄생한 것이 JDBC입니다.

 

JDBC는 추상 API를 제공하고, JDBC 드라이버가 이를 벤더별로 구현합니다.

 

이로 인해, 개발자는 벤더에 의존하지 않고 독립적인 개발환경을 갖추게 되어 비즈니스 로직에 집중할 수 있게 되었습니다.

 

 

JDBC 사용법

 

JDBC를 사용하기 전에 자기가 사용하려는 DB 벤더의 커넥터가 필요합니다.

 

 

커넥터를 직접 다운로드하여 프로젝트에 추가하거나, 메이븐을 통해 의존성을 추가하셔도 됩니다.

 

예시는 연습용이기 때문에 H2 DB를 사용하여 설명하겠습니다. 다른 DB를 사용하더라도 큰 차이가 없습니다.

 

JDBC를 사용하기 위해선 다음과 같은 과정을 거칩니다.

 

 

1. JDBC 드라이버 로드

 

우선 JDBC 드라이버를 로드해야 합니다.

// 드라이버 호출
// Class.forName("드라이버 경로")
try {
   Class.forName("org.h2.Driver");
} catch (ClassNotFoundException e) {
   e.printStackTrace();
}

 

 

Class.forName 메서드는 Java Reflection에서 제공하는 기능 중 하나로 JVM에 파라미터로 주어진 이름과 같은 클래스를 JVM에 로딩시킵니다.

 

드라이버 경로는 벤더별로 드라이버가 저장된 경로가 다르기 때문에 그에 맞춰 설정해주어야 합니다. 

 

  • MariaDB : org.mariadb.jdbc.Driver
  • MySQL : com.mysql.cj.jdbc.Driver
  • Oracle : oracle.jdbc.driver.OracleDriver

 

 

2. 데이터 베이스 연결

 

DB와 직접적으로 연결을 해주는 부분입니다.

// DB 로그인 정보
String url = "jdbc:h2:tcp://h2가 설치된 IP주소/h2에서 사용하려는 DB명";
String id = "계정명";
String pw = "비밀번호";

// 커넥션 생성
Connection conn = null;

try{
   conn = DriverManager.getConnection(url, id, pw);
} catch (SQLException e) {
   throw new RuntimeException(e);
}

 

DriverManager는 DB 드라이버들을 관리, 로딩하고 DB에 연결하는 책임을 가진 클래스입니다.

 

 

3. 명령문(Statement) 생성 및 실행(Execute)

 

우선 Statement 객체를 생성해야 합니다.

 

Statement 객체란 일종의 쿼리를 담아 DB로 전달하는 그릇이라고 생각하면 됩니다.

 

다음과 같은 종류를 가지고 있습니다.

 

  • Statement
    • 단일로 사용할 때 빠른 속도를 지님
    • 매번 컴파일을 수행해야 함
    • 쿼리에 인자를 부여할 수 없음
    • 취약점(SQL Injection등)이 있어 사용하지 않는 것이 권고됨 
// 예시
String sql = "SELECT name FROM player";

Statement stmt = conn.createStatement();

ResultSet rs = s.executeQuery(sql);

 

 

  • PreparedStatement
    • 주로 사용하는 statement
    • 캐시를 사용하므로 같은 쿼리문을 재사용할 때 빠른 속도를 지님
    • 처음 프리 컴파일 된 후, 컴파일을 수행하지 않음
    • 파라미터 바인딩 이용
      • sql문에?를 이용해서 setXXX()를 통해 원하는 값을 넣을 수 있음
      • ? 는 앞에서부터 1,2,3~ 으로 인덱스를 가짐 쿼리에 인자 부여 가능 ( = 동적 쿼리 가능)
      • SQL Injection 방어가 가능
String sql = "UPDATE player SET name = ?, position = ? where name = ?; ";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "토리");
pstmt.setString(2, "공격수");
pstmt.setString(3, "초코");

ps.executeQuery();

 

 

  • CallableStatement
    • PreparedStatement + 프로시저 호출 가능

 

 

생성된 Statement를 가지고 SQL 문을 실행(Execute)시킬 수 있습니다.

 

실행을 위한 메서드는 다음과 같습니다.

 

  • execute()
    • 모든 구문 수행 가능
    • Boolean 값 반환
      • 쿼리 수행 결과가 ResultSet 일 경우 true, 아닐 경우 false

 

  • executerQuery()
    • 쿼리 수행 결과를 ResultSet에 담아서 반환
    • 주로 Select 문에 사용

 

  • executeUpdate()
    • INSERT, UPDATE, DELETE 나 CREATE, DROP 등을 실행하는 데 사용
    • 영향을 받은 행 수를 반환

 

SQL문을 실행하고 난 후 나오는 결괏값은 ResultSet이라는 객체에 담겨 나옵니다.

 

ResultSet은 검색 결과를 테이블 형식으로 저장한 인스턴스로 데이터가 여러 행일 경우 한 번에 가져올 수 없기 때문에, 내부적으로 커서를 이용해서 가져옵니다.

 

주로 사용하는 메서드는 다음과 같습니다.

 

  • ResultSet.next()
    • 커서를 다음행으로 이동하는 메소드
    • 반환값은 Boolean (커서 위치에 처리할 행이 있으면 true, 없으면 false)

 

  • ResultSet.getXXX(int 칼럼 위치), ResultSet, getXXX(String 칼럼명)
    • 커서 위치의 값을 반환하는 메서드
    • 보통 칼럼명을 매개변수로 하는 것을 사용

 

 

4. JDBC 객체 연결 해제

 

사용이 끝났으면 사용한 자원들을 해제해 주어야 DB 서버의 부담을 줄일 수 있습니다.

 

해제 순서는 다음과 같은 순으로 합니다.

 

  • ResultSet 해제
  • Statement 해제
  • Connection 해제

 

 

JDBC 사용 예시

 

위의 과정을 토대로 작성된 예시입니다.

 

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class JdbcPractice {

   public static void main(String[] args) {

      String url = "jdbc:h2:mem:gamers";
      String id = "userId";
      String pw = "password";

      try {
         Class.forName("org.h2.Driver");
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
      
      Connection conn = null;
      PreparedStatement pstmt = null;
      ResultSet rs = null;

      try{
         conn = DriverManager.getConnection(url, id, pw);

         String sql = "SELECT * FROM player";
         pstmt = conn.prepareStatement(sql);
         rs = pstmt.executeQuery();

         while(rs.next()){
            String name = rs.getString("name");
            String pos_name = rs.getString("position");
            System.out.println(name + " / " + pos_name);
         }
      }  catch(Exception e){
         e.printStackTrace();
      }  finally {
         if ( rs != null ) try{rs.close();}catch(Exception e){}
         if ( pstmt != null ) try{pstmt.close();}catch(Exception e){}
         if ( conn != null ) try{conn.close();}catch(Exception e){}
      }
   }
}

 

 

JDBC의 문제점

 

우리가 하고자 하는 건 DB에 쿼리를 날려 데이터를 가져오는 것입니다.

 

그런데 그러기 위해 준비해야 하는 코드들이 잔뜩 있고 정작 가져오는 코드는 몇 줄 되지 않습니다. 일종의 boilerplate code가 반복되는 문제점이 있습니다.

 

 

 

JdbcTemplate

 

 이러한 JDBC의 문제점을 해결하기 위해 Spring에서는 JdbcTemplate를 제공하고 있습니다.

 

위에서 지속적으로 발생했던 반복되는 작업들을 JdbcTemplate이 대신 처리해 줍니다.

 

Spring JDBC는 다음과 같은 흐름을 가지고 있습니다.

 

 

 

JdbcTemplate 사용법

 

1. 라이브러리 추가

// build.gradle

dependencies {
		...
		implementation 'org.springframework.boot:spring-boot-starter-jdbc'
		// 추가적으로 필요한 DB 커넥터를 추가하면 됩니다.
		...
}

 

 

2. JDBC Repository 정의

@Repository
public class JdbcRepository{

		// JdbcTemplate을 필드로 가져와야 합니다.
		private final JdbcTemplate jdbcTemplate;

		// 생성자로 DataSource을 주입하여 사용합니다.
		@Autowired
		public JdbcRepository(DataSource dataSource){
				this.jdbcTemplate = new JdbcTemplate(datasource);
		}
		
}

 

JdbcTemplate을 사용할 때 DataSource를 의존 관계 주입받는 방법과 JdbcTemplate을 스프링 빈으로 직접 등록하고 주입받는 방법이 있지만 전자를 관례상 많이 사용한다고 합니다.

 

 

3. JdbcTemplate 사용

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

// 예시 코드
// 예시로 메모라는 데이터를 예로 들었습니다.
@Repository
public class JdbcRepository{

   private final JdbcTemplate jdbcTemplate;

   @Autowired
   public JdbcMemoRepository(DataSource dataSource){
      this.jdbcTemplate = new JdbcTemplate(dataSource);
   }

   // 저장
   public Memo save(Memo memo){
      // 실행할 쿼리를 작성합니다.
      // ?를 이용하여 파라미터 바인딩할 수 있습니다.
      String sql = "insert into memo values(?,?)";
      
      // 값 추가, 변경, 삭제는 update()를 사용합니다.
      jdbcTemplate.update(sql, memo.getId(), memo.getText());
      
      return memo;
   }

   // RowMapper 메소드 작성
   // 이 부분은 이와 같이 메소드로 빼거나 개별 클래스 작성, 익명함수 작성으로 대체할 수 있습니다.
   private RowMapper<Memo> memoRowMapper(){
   
      // jdbc로 날린 쿼리의 결과값은 ResultSet에 담겨 옵니다.
      // RowMapper를 이용하여 쿼리 결과값을 원하는 객체에 매핑을 해주어야 합니다.
      return (rs, rowNum) -> new Memo(
         rs.getInt("id"),
         rs.getString("text")
      );
   }

   // 조회
   public List<Memo> findAll(){
   
      String sql = "select * from memo";
      
      // 조회시에는 query()를 사용합니다.
      return jdbcTemplate.query(sql, memoRowMapper());
   }

   public Optional<Memo> findById(int id){
   
      String sql = "select * from memo where id = ?";
      
      // queryForObject()를 사용하는 것 대신 아래와 같은 방식으로 사용할 수 있습니다.
      return jdbcTemplate.query(sql, memoRowMapper(), id).stream().findFirst();
   }

}

 

JdbcTemplate에서는 값을 변경할 땐 update 메서드를 사용하고 SELECT로 값을 조회할 땐 queryForObject 메서드나  query 메서드를 사용합니다.

 

다만, queryForObject 같은 경우에 단일 행을 조회할 때 사용되는데, 단일 행이 아닐 경우(0 또는 2 이상) 오류가 발생하게 됩니다.

 

이러한 오류 처리 대신 그냥 List형식으로 값을 받아오는 query 메서드를 이용한다면, 많으면 많은데로 받아오고, 없으면 없는데로 처리가 가능하기 때문에 query 메소드를 사용하는 것을 추천합니다.

 

위와 같이 JdbcTemplate을 사용하면 불필요한 보일러 코드 없이 원하는 비즈니스만 처리할 수 있어 가독성도 좋고 편리합니다.

 

추가적으로 SimpleJdbcInsert라는 것이 있는데, 데이터 삽입을 좀 더 편리하게 도와주는 용도의 클래스입니다. 

개인적으로는 별로 사용하지는 않을 것 같습니다.

'프레임워크 > Spring' 카테고리의 다른 글

Spring Data - 개요  (0) 2023.05.17
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.