JDBC와 Spring JDBC (JdbcTemplate)

junto·2024년 8월 23일
0

spring

목록 보기
29/30
post-thumbnail

JDBC(Java Database Connectivity)

  • JDBC는 자바 코드로 데이터베이스와 직접 통신하는 저수준 API를 말한다.
  • 아래 코드처럼 스프링 프레임워크 없이도, Database와 통신할 수 있다.
// Mysql에 board 테이블은 비워져 있는 상태
mysql> select * from board;
Empty set (0.00 sec)

public static void main(String[] args) throws SQLException {
	BoardDAO dao = new BoardDAO();

	dao.createPost("제목1", "내용1");
}

public void createPost(String title, String content) throws SQLException {
	String sql = "INSERT INTO board (title, content) VALUES (?, ?)";

	try (Connection conn = DataSourceUtil.getConnection();
			PreparedStatement pstmt = conn.prepareStatement(sql)) {

		pstmt.setString(1, title);
		pstmt.setString(2, content);
		pstmt.executeUpdate();

	} catch (SQLException e) {
		e.printStackTrace();
	}
}


// Mysql에 board 테이블에 하나의 포스트가 추가된 모습
mysql> select * from board;
+----+---------+---------+---------------------+
| id | title   | content | created_at          |
+----+---------+---------+---------------------+
|  1 | 제목1   | 내용1   | 2024-08-23 15:32:50 |
+----+---------+---------+---------------------+
1 row in set (0.00 sec)

왜 JDBC를 알아야할까?

  • JDBC를 사용하며 개발하던 시기는 거의 20년 전이라고 한다. 현재는 Mybatis 또는 Spring JPA를 통해 주로 개발한다. 그런데 왜 JDBC를 알아야할까? Mybatis나 JPA 모두 내부적으로 JDBC를 사용하여 데이터베이스와 통신하므로 JDBC를 알면 두 기술을 더 잘 이해할 수 있게 된다.
  • JPA를 사용하면 편리한 이점을 얻는 대신 쿼리를 세밀하게 제어하기는 힘들다. 이 경우 JDBC 또는 Mybatis를 사용하여 쿼리 최적화를 할 수 있다.

JDBC 주요 기능과 구성요소

1. 데이터베이스 연결과 해제

1) DriverManager

  • 새로운 드라이버를 등록하거나 등록된 드라이버를 해제한다. 애플리케이션이 데이터베이스와 연결할 수 있도록 getConnection 메서드를 제공한다. 프로퍼티 속성을 읽거나 url, user, password로 연결할 수 있다.
public class DriverManager {
  ...
  @CallerSensitive
  public static Connection getConnection(String url,
    String user, String password) throws SQLException {
      
     java.util.Properties info = new java.util.Properties();
      
     if (user != null) {
	    info.put("user", user);
	 }
     if (password != null) {
	   info.put("password", password);
     }

     return (getConnection(url, info, Reflection.getCallerClass()));
  }
}
// Client getConnection
private static final String URL = "jdbc:mysql://localhost:3306/jdbc";
private static final String USER = "root";
private static final String PASSWORD = "";

public static Connection getConnection() throws SQLException {
	return DriverManager.getConnection(URL, USER, PASSWORD);
}

2) Connection

  • 데이터베이스와 연결된 객체이다.
  • 데이터베이스와의 모든 통신은 Connection 객체를 이용한다.
Statement createStatement() throws SQLException; // Statement 객체 생성

PreparedStatement prepareStatement(String sql) throws SQLException; // pstmt 객체 생성

void setAutoCommit(boolean autoCommit) throws SQLException; // 오토 커밋 설정

void commit() throws SQLException; // 커밋 설정
void rollback() throws SQLException; // 롤백 설정
void close() throws SQLException; // DB 연결 종료
...

3) Datasource

  • Connection 객체를 재사용할 수 있도록 관리하는 커넥션 풀을 제공한다.
  • Datasource 인터페이스에서 getConnection()으로 연결을 시도하면, 새롭게 연결을 시도하는 것이 아닌 미리 만들어진 곳(pool)에서 가져온다. 데이터베이스와의 연결을 설정하고 해제하는 작업은 비용이 크기 때문에 성능 개선 효과가 크다.
public interface DataSource  extends CommonDataSource, Wrapper {
 
  Connection getConnection() throws SQLException;
  Connection getConnection(String username, String password)
    throws SQLException;
  ...
}

2. 쿼리 생성 및 실행

1) Statement

  • SQL 쿼리를 데이터베이스에 전달하고 결과를 반환받는 데 사용한다.
  • 단순하고 고정된 SQL 쿼리이거나 파라미터가 없는 경우 사용하며, SQL Injection에 취약하다는 단점이 있다.
public interface Statement extends Wrapper, AutoCloseable {

  // SELECT 쿼리를 실행하고, 결과를 ResultSet 객체로 반환
  ResultSet executeQuery(String sql) throws SQLException;
  
  // INSERT, UPDATE, DELETE 쿼리를 실행하고, 영향을 받은 행의 수를 반환
  int executeUpdate(String sql) throws SQLException;

  // 결과가 ResultSet일지, 아니면 업데이트된 행의 수일지 미리 알 수 없는 상황에 사용
  boolean execute(String sql) throws SQLException;
  boolean execute(String sql, int autoGeneratedKeys) throws SQLException
  ...
}

2) PreparedStatement

  • 파라미터화된 SQL 쿼리를 실행하기 위해 사용한다.
  • SQL Injection 방지하고, 반복 쿼리에 대해서 성능 개선 효과가 있다.
public interface PreparedStatement extends Statement {

  // SQL 쿼리 내 파라미터 값 설정
  void setString(int parameterIndex, String x) throws SQLException;
  void setInt(int parameterIndex, int x) throws SQLException;
  ...
}

3. 쿼리 결과 저장 및 반환

ResultSet

  • 데이터베이스 쿼리의 결과를 테이블 형식으로 유지한다.
  • 쿼리의 결과 데이터를 읽고 처리하는 데 사용한다.
public interface ResultSet extends Wrapper, AutoCloseable {
	
    // 결과 집합의 다음 행으로 이동
    boolean next() throws SQLException;
    
    // 결과 집합의 특정 열 값 가져옴
    String getString(int columnIndex) throws SQLException;
    int getInt(int columnIndex) throws SQLException;
    ...
}

DB 커넥션 풀링

  • DriverManager를 사용하게 되면 특정 쿼리들을 실행할 때마다 getConnection() 메서드를 통해 데이터베이스와 새롭게 연결해야 한다.
  • 데이터베이스와의 연결을 설정하고 해제하는 작업은 비용이 크기 때문에 커넥션 풀링이라는 기술이 도입되었다. 이는 미리 일정 수의 데이터베이스 연결을 생성해 두고, 애플리케이션이 필요할 때마다 이 풀(pool)에서 연결을 가져가는 방식으로 동작한다.
public class DriverManagerUtil {
  private static final String URL = "jdbc:mysql://localhost:3306/jdbc";
  private static final String USER = "root";
  private static final String PASSWORD = "";

  public static Connection getConnection() throws SQLException {
    return DriverManager.getConnection(URL, USER, PASSWORD);
  }
}
  • 매번 쿼리를 실행할 때 DriverManager로 DB와 커넥션을 맺었다면, 아래와 같이 커넥션 풀을 이용할 수 있다. 다양한 커넥션 풀 구현체가 있지만, 여기에선 고성능 HikiriCP를 사용한다.
public class DataSourceUtil {
  private static HikariDataSource dataSource;

  static {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc");
    config.setUsername("root");
    config.setPassword("");

    config.setMaximumPoolSize(10);
    config.setMinimumIdle(2);
    config.setConnectionTimeout(30000);

    dataSource = new HikariDataSource(config);
  }

  public static Connection getConnection() throws SQLException {
    return dataSource.getConnection();
  }

  public static DataSource getDataSource() {
    return dataSource;
  }
}
  • 미리 커넥션을 10개 만들어두고, 매번 새롭게 연결하는 게 아닌 커넥션 풀에서 꺼내 사용할 수 있도록 설정한다.
  • 간단히 게시글을 만들고, 읽고, 수정하는 쿼리를 400개를 실행해보고 커넥션 풀을 사용했을 때와 사용하지 않았을 때 성능 차이를 비교해본다.
// 드라이버 매니저 getConnection - 매번 새로운 연결
try (Connection conn = DriverManger.getConnection();
		PreparedStatement pstmt = conn.prepareStatement(createTableSQL)) {
	pstmt.executeUpdate();

// HikariCP getConnection - 커넥션 풀에서 가져옴
try (Connection conn = DataSourceUtil.getConnection();
		PreparedStatement pstmt = conn.prepareStatement(createTableSQL)) {
	pstmt.executeUpdate();
 public static void main(String[] args) throws SQLException {
    BoardDAO dao = new BoardDAO();

    long startTime = System.currentTimeMillis();

    dao.createTable();
    for(int i = 0; i < 100; i++) {

      dao.createPost("제목1", "내용1");

      dao.readPost(2);

      dao.updatePost(1, "제목2", "내용2");
    }

    long endTime = System.currentTimeMillis();

    long duration = endTime - startTime;

    System.out.println("Hikari Datasource pool 10개 실행 시간: " + duration + " milliseconds");
  }

  • 커넥션 풀을 사용했을 때 대략 6배정도 더 빠른 걸 확인할 수 있었다.

JDBC와 Spring JDBC(JdbcTemplate)

  • 그렇다면 Spring JDBC는 무엇일까? Spring JDBC는 Spring에서 지원하는 것으로 JdbcTemplate 핵심 클래스를 제공하여 데이터베이스 접근을 단순화하고, 예외 처리 및 자원 해제를 자동으로 해준다.
  • 추가로 결과 집합을 객체로 매핑하기 위해 RowMapper 인터페이스를 제공하여 ResultSet 데이터를 간단하게 Java 객체로 변환할 수 있다.
  • Spring JDBC를 사용하면 코드가 어떻게 변하는지 간단하게 Post CRUD를 JDBC로 작성하고, 이를 Spring JDBC로 변환해보자.

게시글 CRUD (JDBC)

public class BoardDAO {
    
  public void createPost(String title, String content) throws SQLException {
    String sql = "INSERT INTO board (title, content) VALUES (?, ?)";

    try (Connection conn = DataSourceUtil.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql)) {

      pstmt.setString(1, title);
      pstmt.setString(2, content);
      pstmt.executeUpdate();

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  public void readPost(int id) {
    String sql = "SELECT * FROM board WHERE id = ?";

    try (Connection conn = DataSourceUtil.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql)) {

      pstmt.setInt(1, id);
      ResultSet rs = pstmt.executeQuery();

      if (rs.next()) {
        System.out.println("id: " + rs.getInt("id"));
        System.out.println("title: " + rs.getString("title"));
        System.out.println("content: " + rs.getString("content"));
        System.out.println("created_at: " + rs.getTimestamp("created_at"));
      } else {
        System.out.println("포스트 없음 ID: " + id);
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  public void updatePost(int id, String title, String content) {
    String sql = "UPDATE board SET title = ?, content = ? WHERE id = ?";

    try (Connection conn = DataSourceUtil.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql)) {

      pstmt.setString(1, title);
      pstmt.setString(2, content);
      pstmt.setInt(3, id);
      int rowsAffected = pstmt.executeUpdate();

      if (rowsAffected > 0) {
        System.out.println("성공적으로 게시물 업데이트");
      } else {
        System.out.println("포스트 없음 ID: " + id);
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  public void deletePost(int id) {
    String sql = "DELETE FROM board WHERE id = ?";

    try (Connection conn = DataSourceUtil.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql)) {

      pstmt.setInt(1, id);
      int rowsAffected = pstmt.executeUpdate();

      if (rowsAffected > 0) {
        System.out.println("성공적으로 게시물 삭제");
      } else {
        System.out.println("포스트 없음 ID: " + id);
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

게시글 CRUD (Spring JDBC)

public class SpringJDBCBoardDAO {

  private JdbcTemplate jdbcTemplate;

  public SpringJDBCBoardDAO(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
  }

  public void createTable() throws SQLException {
    String createTableSQL =
        "CREATE TABLE IF NOT EXISTS board ("
            + "id INT AUTO_INCREMENT PRIMARY KEY, "
            + "title VARCHAR(255) NOT NULL, "
            + "content TEXT NOT NULL, "
            + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
            + ")";

    jdbcTemplate.execute(createTableSQL);
  }

  public void createPost(String title, String content) throws SQLException {
    String sql = "INSERT INTO board (title, content) VALUES (?, ?)";
    jdbcTemplate.update(sql, title, content);
  }

  public void readPost(int id) {
    String sql = "SELECT * FROM board WHERE id = ?";

    ResBoard board = jdbcTemplate.queryForObject(sql, new RowMapper<ResBoard>() {
      @Override
      public ResBoard mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new ResBoard(
          rs.getInt("id"),
          rs.getString("title"),
          rs.getString("content"),
          rs.getTimestamp("created_at")
        );
      }
    }, id);

    try {
      if (board != null) {
        System.out.println("ID: " + board.getId());
        System.out.println("Title: " + board.getTitle());
        System.out.println("Content: " + board.getContent());
        System.out.println("Created At: " + board.getCreatedAt());
      }
    } catch (Exception e) {
      System.out.println("포스트 없음 ID: " + id);
    }
  }

  public void updatePost(int id, String title, String content) {
    String sql = "UPDATE board SET title = ?, content = ? WHERE id = ?";
    int rowsAffected = jdbcTemplate.update(sql, title, content, id);

    if (rowsAffected > 0) {
      System.out.println("성공적으로 게시물 업데이트");
    } else {
      System.out.println("포스트 없음 ID: " + id);
    }
  }

  public void deletePost(int id) {
    String sql = "DELETE FROM board WHERE id = ?";
    int rowsAffected = jdbcTemplate.update(sql, id);

    if (rowsAffected > 0) {
      System.out.println("성공적으로 게시물 삭제");
    } else {
      System.out.println("포스트 없음 ID: " + id);
    }
  }
}
  • Spring JDBC는 반복해서 커넥션을 얻는 코드, 예외 처리 코드 등 공통적으로 처리해야될 부분을 개발자 대신 처리해주어 코드가 더욱 간단해진다. 또한 RowMapper로 쿼리 결과를 자바 객체로 변환시키는 것도 용이하다는 장점이 있다.
profile
꾸준하게

0개의 댓글