본문 바로가기

(09.16) JDBC 프로그래밍 - DAO/DTO 복습, JDBC 트랜잭션, HikariCP

@starweb2025. 9. 18. 18:47

[ 6주차 - 0916 ] 

    금일 커리큘럼
        ├ 09:00 ~ 12:00 JDBC 프로그래밍 (DAO/DTO 복습)
        └ 13:00 ~ 18:00 JDBC 프로그래밍 (JDBC 트랜잭션, HikariCP)

1. DAO DTO 복습

연습용 table 생성

CREATE TABLE products (
    id             int             PRIMARY KEY AUTO_INCREMENT,
    name         varchar(150)     UNIQUE NOT NULL,
    price         int             CHECK(price >= 0),
    reg_date     datetime         DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO products(name,price) VALUE('pen', 3000);
INSERT INTO products VALUE(null, 'pen2', 3000, now());

패키지 구조 설계 (임시)

kr.likelion.demo
    │ 
    ├─ common       # 공통
    │   ├─ DBUtil (class)
    │   └─ 
    │
    ├─ controller   # 관리 로직
    │   └─  미정
    │
    ├─ dto          # DB 데이터 객체
    │   └─ ProductDTO (class)
    │
    ├─ repository   # DB 데이터 접근
    │   ├─  ProductDAO (interface)      
    │   └─  ProductDAOImpl (class)      
    │
    ├─ service      # 로직 제어
    │   └─  ServiceTest (class) 
    │       
    └─

DBUtil 클래스 설계

역할 : DB 커넥션 세팅

package week_06._0916.kr.likelion.demo.common;

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

public class DBUtil {
    static final String url =
            "jdbc:mysql://localhost:3306/liondb" +
            "?useSSL=false" +
            "&serverTimezone=Asia/Seoul" +
            "&characterEncoding=UTF-8";

    public static Connection getConnection() throws ClassNotFoundException, SQLException {
        String user = "star1431";
        String password = "star1431";

        return DriverManager.getConnection(url, user, password);
    }

    public static void close(Connection conn) {
        if(conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void close(PreparedStatement ps) {
        if(ps != null){
            try {
                ps.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void close(ResultSet rs) {
        if(rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

DTO 클래스 설계

역할 : DB 테이블 정보 담는 그릇

package week_06._0916.kr.likelion.demo.dto;

import java.time.LocalDateTime;

public class ProductDTO {
    private int id;
    private String name;
    private int price;
    private LocalDateTime reg_date;

    public ProductDTO() {

    }
    public ProductDTO(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public LocalDateTime getRegdate() {
        return reg_date;
    }

    public void setRegdate(LocalDateTime reg_date) {
        this.reg_date = reg_date;
    }

    @Override
    public String toString() {
        return "[" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                ", reg_date=" + reg_date +
                ']';
    }
}

DAO 인터페이스

역할 : DB 접근 쿼리문 관련 기능 정의

package week_06._0916.kr.likelion.demo.repository;


import week_06._0916.kr.likelion.demo.dto.ProductDTO;

import java.util.ArrayList;
import java.util.List;

public interface ProductDAO {
    public int insertAndGetId(ProductDTO pDTO);     // insert id 반환
    public boolean insert(ProductDTO pDTO);         // insert 기본
    public boolean delete(int id);                  // delete where id
    public boolean delete(ProductDTO pDTO);         // delete 기본
    public boolean update(ProductDTO pDTO);         // update 기본
    public ProductDTO select(int id);               // select where id
    public ProductDTO select(String name);          // select where name
    public List<ProductDTO> selectAll();            // seelect *
}

DAO 클래스 설계

역할 : DB 접근 쿼리문 관련 기능 정의

package week_06._0916.kr.likelion.demo.repository;

import week_06._0916.kr.likelion.demo.common.DBUtil;
import week_06._0916.kr.likelion.demo.dto.ProductDTO;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

public class ProductDAOImpl implements ProductDAO {
    /** 삽입 및 아이디 get */
    @Override
    public int insertAndGetId(ProductDTO pDTO) {
        String sql = "INSERT INTO products(name, price, reg_date) VALUES (?, ?, now())";
        int result = 0;

        try (
                Connection conn = DBUtil.getConnection();
                // Statement.RETURN_GENERATED_KEYS
                // = INSERT 실행 후 DB에서 생성된 자동 증가 키(AUTO_INCREMENT 값)를 가져오도록 설정
                PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        ) {
            ps.setString(1, pDTO.getName());
            ps.setInt(2, pDTO.getPrice());
            result = ps.executeUpdate();
            if(result > 0) {
                // ps.getGeneratedKeys()
                // = INSERT 실행 시 생성된 자동 증가 키를 ResultSet 형태로 반환
                try (ResultSet rs = ps.getGeneratedKeys()) {
                    // getGeneratedKeys 는 컬럼명 없음
                    if(rs.next()) pDTO.setId(rs.getInt(1));
                }
            }
        } catch (Exception e) {
            System.out.println("삽입(id) error : " + e.getMessage());
        }

        return pDTO.getId();
    }

    /** 삽입 */
    @Override
    public boolean insert(ProductDTO pDTO) {
        String sql = "INSERT INTO products(name, price, reg_date) VALUES (?, ?, now())";
        int result = 0;

        try (
                Connection conn = DBUtil.getConnection();
                // Statement.RETURN_GENERATED_KEYS
                // = INSERT 실행 후 DB에서 생성된 자동 증가 키(AUTO_INCREMENT 값)를 가져오도록 설정
                PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        ) {
            ps.setString(1, pDTO.getName());
            ps.setInt(2, pDTO.getPrice());
            result = ps.executeUpdate();
            if(result > 0) {
                // ps.getGeneratedKeys()
                // = INSERT 실행 시 생성된 자동 증가 키를 ResultSet 형태로 반환
                try (ResultSet rs = ps.getGeneratedKeys()) {
                    // getGeneratedKeys 는 컬럼명 없음
                    if(rs.next()) pDTO.setId(rs.getInt(1));
                }
            }
        } catch (Exception e) {
            System.out.println("삽입 error : " + e.getMessage());
        }

        return result > 0;
    }

    /** 삭제 (id) */
    @Override
    public boolean delete(int id) {
        String sql = "DELETE FROM products WHERE id = ?";
        int result = 0;

        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
        ) {
            ps.setInt(1, id);
            result = ps.executeUpdate();
        } catch (Exception e) {
            System.out.println("삭제 error : " + e.getMessage());
        }

        return result > 0;
    }

    /** 삭제 (dto) */
    @Override
    public boolean delete(ProductDTO pDTO) {
        String sql = "DELETE FROM products WHERE id = ?";
        int result = 0;

        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
        ) {
            ps.setInt(1, pDTO.getId());
            result = ps.executeUpdate();
        } catch (Exception e) {
            System.out.println("삭제 error : " + e.getMessage());
        }

        return result > 0;
    }

    /** 변경 */
    @Override
    public boolean update(ProductDTO pDTO) {
        String sql = "UPDATE products SET " +
                "name = ?, price = ? " +
                "WHERE id = ?";
        int result = 0;

        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
        ) {
            ps.setString(1, pDTO.getName());
            ps.setInt(2, pDTO.getPrice());
            ps.setInt(3, pDTO.getId());
            result = ps.executeUpdate();
        } catch (Exception e) {
            System.out.println("변경 error : " + e.getMessage());
        }

        return result > 0;
    }

    /** 단건조회(id) */
    @Override
    public ProductDTO select(int id) {
        String sql = "SELECT * FROM products WHERE id = ?";
        ProductDTO pDTO = null;
        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
        ) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if(rs.next()) pDTO = resultSet(rs);
            } catch (Exception e) {
                System.out.println("단건선택(id) DTO 삽입 error :" + e.getMessage());
            }
        } catch (Exception e) {
            System.out.println("단건선택(id) error : " + e.getMessage());
        }

        return pDTO;
    }
    /** 단건조회(name) */
    @Override
    public ProductDTO select(String name) {
        String sql = "SELECT * FROM products WHERE name = ?";
        ProductDTO pDTO = null;

        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
        ) {
            ps.setString(1, name);
            try (ResultSet rs = ps.executeQuery()) {
                if(rs.next()) pDTO = resultSet(rs);
            } catch (Exception e) {
                System.out.println("단건선택(name) DTO 삽입 error :" + e.getMessage());
            }
        } catch (Exception e) {
            System.out.println("단건선택(name) error : " + e.getMessage());
        }

        return pDTO;
    }

    /** 전체조회 */
    @Override
    public List<ProductDTO> selectAll() {
        String sql = "SELECT * FROM products";
        List<ProductDTO> lists = new ArrayList<>();

        try (
                Connection conn = DBUtil.getConnection();
                PreparedStatement ps = conn.prepareStatement(sql);
                ResultSet rs = ps.executeQuery()
        ) {
            while (rs.next()) {
                lists.add(resultSet(rs));
            }
        } catch (Exception e) {
            System.out.println("전체선택 error : " + e.getMessage());
        }

        return lists;
    }

    /** 리절트 공통 */
    private ProductDTO resultSet(ResultSet rs) throws SQLException {
        ProductDTO pDTO = new ProductDTO();
        pDTO.setId(rs.getInt("id"));
        pDTO.setName(rs.getString("name"));
        pDTO.setPrice(rs.getInt("price"));
        // pDTO.setRegdate(rs.getTimestamp("reg_date").toLocalDateTime());
        Timestamp regDate = rs.getTimestamp("reg_date");
        if(regDate != null) pDTO.setRegdate(regDate.toLocalDateTime());

        return pDTO;
    }
}

2. JDBC 트랜잭션

DAO 패턴에서 트랜잭션 처리 역할은 어디?

DAO는 데이터베이스 작업(SQL 실행)만 담당하고, 트랜잭션은 비즈니스로직 담당하는 클래스에서 처리해야 한다.

  • DAO는 단일 쿼리 단위로 끝남
  • 트랜잭션 범위는 여러 DAO 호출하는 곳에서 처리
  • DAO 내에서 트랜잭션 열고 커밋할 경우 벨리데이션체크없이 확정되어 원하는 결과를 얻지 못함

즉 DAO는 쿼리 담당 / 트랜잭션은 DAO 호출하는 로직에서

심플 예시

// AccountDAO.java
public class AccountDAO {
    // 출금
    public void withdraw(Connection conn, int accountId, int money) throws SQLException {
        String sql = "UPDATE account SET balance = balance - ? WHERE id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, money);
            ps.setInt(2, accountId);
            ps.executeUpdate();
        }
    }

    // 입금
    public void deposit(Connection conn, int accountId, int money) throws SQLException {
        String sql = "UPDATE account SET balance = balance + ? WHERE id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, money);
            ps.setInt(2, accountId);
            ps.executeUpdate();
        }
    }
}
// TransferService.java
public class TransferService {
    private final AccountDAO dao = new AccountDAO();

    public void transfer(int fromId, int toId, int money) {
        Connection conn = null;
        try {
            conn = DBUtil.getConnection();
            conn.setAutoCommit(false);   // 트랜잭션 시작

            dao.withdraw(conn, fromId, money); // 출금
            dao.deposit(conn, toId, money);    // 입금

            conn.commit();   // 둘 다 성공 → 커밋
        } catch (Exception e) {
            try {
                if (conn != null) conn.rollback();
            } catch (SQLException e) {
                System.out.println(e.getMessage());
            }
        } finally {
            try { 
                if (conn != null) conn.close(); 
            } catch (SQLException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

3. HikariCP

가볍고 빠른 JDBC 커넥션 풀(Connection Pool) 라이브러리

  • 스프링부트 같은 프레임워크에서 기본 사용
  • 성능과 안정성으로 실무에서 많이 쓰이는 풀링 라이브러리

커넥션 풀 개념

기존 DB 연결은 생성/해제시 비용이 크고, 매요청마다 열고 닫을때 성능 저하 발생됨.

그래서 미리 일정수의 커넥션을 만들어 풀에 저장해서 필요할때 빌려쓰고 반납하는 구조

즉 빠른 성능 + 자원 효율성 확보됨

HikariCP 특징

  • 불필요한 기능을 최대한 배제하고 핵심 로직만 단순·최적화
  • 벤치마크 상 다른 풀보다 TPS가 높고 지연시간이 짧음
  • 코드가 간결하고, 메모리 사용량도 적음
  • 최소한의 프로퍼티만으로 동작 가능

의존성 설정하기

  • 메이븐인 경우 pom.xml
<dependencies>
    <!-- HikariCP -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.1.0</version> <!-- 최신 안정 버전 사용 -->
    </dependency>

    <!-- JDBC 드라이버 (예: MySQL) -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>
  • 그래들인 경우 build.gradle
dependencies {
    // HikariCP
    implementation 'com.zaxxer:HikariCP:5.1.0'

    // JDBC 드라이버 (예: MySQL)
    implementation 'mysql:mysql-connector-java:8.0.33'
}

HikariCP 기본 설정

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class DatabaseConfig {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();

        // 필수 설정
        config.setJdbcUrl("jdbc:mysql://localhost:3306/examplesdb");
        config.setUsername("urstory");
        config.setPassword("u1234");
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // 커넥션 풀 설정
        config.setMaximumPoolSize(20);        // 최대 커넥션 수
        config.setMinimumIdle(10);           // 최소 유지 커넥션 수
        config.setConnectionTimeout(30000);  // 커넥션 타임아웃 (30초)
        config.setIdleTimeout(600000);       // 유휴 커넥션 타임아웃 (10분)
        config.setMaxLifetime(1800000);      // 커넥션 최대 생존시간 (30분)

        // MySQL 최적화 설정
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.addDataSourceProperty("useServerPrepStmts", "true");
        config.addDataSourceProperty("useLocalSessionState", "true");
        config.addDataSourceProperty("rewriteBatchedStatements", "true");
        config.addDataSourceProperty("cacheResultSetMetadata", "true");
        config.addDataSourceProperty("cacheServerConfiguration", "true");
        config.addDataSourceProperty("elideSetAutoCommits", "true");
        config.addDataSourceProperty("maintainTimeStats", "false");

        dataSource = new HikariDataSource(config);
    }

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

    public static void close() {
        if (dataSource != null && !dataSource.isClosed()) {
            dataSource.close();
        }
    }

    // 커넥션 풀 모니터링
    public static void printPoolStats() {
        if (dataSource != null) {
            System.out.println("Active connections: " + dataSource.getHikariPoolMXBean().getActiveConnections());
            System.out.println("Idle connections: " + dataSource.getHikariPoolMXBean().getIdleConnections());
            System.out.println("Total connections: " + dataSource.getHikariPoolMXBean().getTotalConnections());
            System.out.println("Threads awaiting connection: " + dataSource.getHikariPoolMXBean().getThreadsAwaitingConnection());
        }
    }
}

properties 파일을 이용한 설정

외부 설정 파일

  • hikari.properties 파일
dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
dataSource.url=jdbc:mysql://localhost:3306/examplesdb
dataSource.user=urstory
dataSource.password=u1234
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
maximumPoolSize=20
minimumIdle=10
connectionTimeout=30000
idleTimeout=600000
maxLifetime=1800000
poolName=MyHikariPool
  • java 사용시
public class DatabaseConfig {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig("hikari.properties");
        dataSource = new HikariDataSource(config);
    }

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

설정 후 DAO에서 활용시

public class UserDAO {

    public void insertUser(User user) throws SQLException {
        String sql = "INSERT INTO user (email, name, password) VALUES (?, ?, ?)";

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

            pstmt.setString(1, user.getEmail());
            pstmt.setString(2, user.getName());
            pstmt.setString(3, user.getPassword());

            pstmt.executeUpdate();
        }
        // Connection은 자동으로 풀에 반환됨
    }

    public List<User> getAllUsers() throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT * FROM user ORDER BY user_id";

        try (Connection conn = DatabaseConfig.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                User user = new User();
                user.setUserId(rs.getInt("user_id"));
                user.setEmail(rs.getString("email"));
                user.setName(rs.getString("name"));
                users.add(user);
            }
        }
        return users;
    }
}

// 사용 예제
public class Main {
    public static void main(String[] args) {
        try {
            UserDAO userDAO = new UserDAO();

            // 사용자 추가
            User newUser = new User("test@test.com", "테스트", "1234");
            userDAO.insertUser(newUser);

            // 전체 사용자 조회
            List<User> users = userDAO.getAllUsers();
            users.forEach(System.out::println);

            // 커넥션 풀 상태 확인
            DatabaseConfig.printPoolStats();

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 애플리케이션 종료 시
            DatabaseConfig.close();
        }
    }
}
starweb
@starweb :: starweb 님의 블로그

starweb 님의 블로그 입니다.

공감하셨다면 구독도 환영합니다!

목차