ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JDBC
    Spring&SpringBoot 2025. 11. 12. 13:04

    애플리케이션 서버와 DB - 일반적인 사용법

    • 커넥션 연결 : 주로 TCP/IP 사용해 커넥션을 연결
    • SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달
    • 결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답
    • 문제
      • 데이터베이스를 다른 종류의 데이터베이스로 변경하면 애플리케이션 서버에 개발된 데이터베이스 사용 코드도 함께 변경 필요
      • 개발자가 각각의 DB마다 커넥션 연결, SQL 전달, 결과 응답 방법을 새로 학습 필요

     

     

    JDBC 표준 인터페이스

    • JDBC(Java DataBase Connection) : JDBC는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API 

     

     

    JDBC와 최신 데이터 접근 기술

     

     

     

    JDBC DriverManager 연결 이해

    1. 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 호출
    2. DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식 후 드라이버들에게 순서대로 정보를 넘겨 커넥션 획득할 수 있는지 확인
    3. 각각 드라이버는 URL 정보를 체크해서 처리할 수 있는 요청인지 확인하고 커넥션을 클라이언트에 반환

     

     

    public abstract class ConnectionConst {
        public static final String URL = "jdbc:h2:tcp://localhost/~/test";
        public static final String USERNAME = "sa";
        public static final String PASSWORD = "";
    }
    @Slf4j
    public class DBConnectionUtil {
        public static Connection getConnection() {
            try {
                Connection connection = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
                log.info("get connection={}, class={}", connection, connection.getClass());
                //  get connection=conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
                return connection;
            } catch (SQLException e) {
                // 체크 ex > 런타임 ex로 던진다
                throw new IllegalStateException(e);
            }
        }
    }
    @Slf4j
    public class MemberRepositoryV0 {
        public Member save(Member member) throws SQLException {
            String sql = "insert into member (member_id, money) values (?, ?)";
    
            Connection con = null;
            PreparedStatement pstmt = null;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1, member.getMemberId());
                pstmt.setInt(2, member.getMoney());
    
                // 준비된 sql을 커넥션을 통해 실제 데이터베이스에 전달
                pstmt.executeUpdate();
    
                return member;
            } catch (SQLException e) {
                log.error("db error", e);
                throw e;
            } finally {
                close(con, pstmt, null);
            }
        }
    
        public Member findById(String memberId) throws SQLException {
            String sql = "select * from member where member_id = ?";
    
            Connection con = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1, memberId);
    
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    Member member = new Member();
                    member.setMemberId(rs.getString("member_id"));
                    member.setMoney(rs.getInt("money"));
                    return member;
                } else {
                    throw new NoSuchElementException("member not found memberId= " + memberId);
                }
            } catch (SQLException e) {
                log.error("db error", e);
                throw e;
            } finally {
                close(con, pstmt, rs);
            }
        }
    
        public void update(String memberId, int money) throws SQLException {
            String sql = "update member set money = ? where member_id = ?";
    
            Connection con = null;
            PreparedStatement pstmt = null;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setInt(1, money);
                pstmt.setString(2, memberId);
    
                int resultSize = pstmt.executeUpdate();
                log.info("resultSize={}" + resultSize);
            } catch (SQLException e) {
                log.error("db error", e);
                throw e;
            } finally {
                close(con, pstmt, null);
            }
        }
    
        public void delete(String memberId) throws SQLException {
            String sql = "delete from member where member_id = ?";
    
            Connection con = null;
            PreparedStatement pstmt = null;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1, memberId);
                pstmt.executeUpdate();
            } catch (SQLException e) {
                log.error("db error", e);
                throw e;
            } finally {
                close(con, pstmt, null);
            }
        }
    
        private void close (Connection con, Statement stmt, ResultSet rs) {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    log.error("rs db error", e);
                }
            }
    
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    log.error("stmt db error", e);
                }
            }
    
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    log.error("con db error", e);
                }
            }
        }
    
        private Connection getConnection() {
            return DBConnectionUtil.getConnection();
        }
    }
    @Slf4j
    class MemberRepositoryV0Test {
        MemberRepositoryV0 repository = new MemberRepositoryV0();
    
        @Test
        void save() throws SQLException {
            // save
            Member member = new Member("memberV100", 10000);
            repository.save(member);
    
            // findByID
            Member findMember = repository.findById(member.getMemberId());
            log.info("findMember={}", findMember.toString());
            // findMember=Member(memberId=memberV1, money=10000)
            assertThat(findMember).isEqualTo(member);
    
            // update
            repository.update(member.getMemberId(), 30000);
            Member updateMember = repository.findById(member.getMemberId());
            assertThat(updateMember.getMoney()).isEqualTo(30000);
    
            // delete
            repository.delete(updateMember.getMemberId());
            assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                    .isInstanceOf(NoSuchElementException.class);
        }
    
    }

     

     

    ResultSet

    • ResultSet 내부에 있는 커서를 이동해 데이터를 조회할 수 있다
    • rs.next()를 호출하면 커서가 다음으로 이동한다
      • 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 최초 한번 호출 필요

     

     

    커넥션 풀

    1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회
    2. DB 드라이버는 DB와 TCP/IP 커넥션 연결
      1. 이 과정에서 3 way handshake 같은 연결을 위한 네트워크 동작 발생
    3. DB 드라이버는 커넥션이 연결되면 기타 부가정보를 DB에 전달
    4. DB는 부가정보를 통해 내부 인증을 하고 내부에 DB 세션을 생성 후 완료 응답 전달
    5. DB 드라이버는 커넥션 객체를 생성해 클라이언트에 반환
    • 커넥션을 새로 만드는 것은 과정도 복잡하고 시간도 많이 소요된다
    • DB, 애플리케이션 서버에서도 TCP/IP 커넥션을 생성하기 위한 리소스를 매번 사용한다
    • 고객이 애플리케이션을 사용할 때 sql 수행 시간 + 커넥션 생성 시간이 소요된다
    • 그래서? 커넥션을 미리 생성해 두고 사용하는 커넥션 풀 방법이 있다 

     

     

    • 적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테스트를 통해 정한다
    • 커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있기 때문에 DB에 무한정 연결 생성을 막아 DB를 보호한다 
    • 커넥션 풀 오픈소스 : commons-dbcp2, tomcat-jdbc pool, hikariCP

     

     

    DataSource

    public interface DataSource {
     Connection getConnection() throws SQLException;
    }
    • DataSource는 커넥션을 획득하는 방법을 추상화한 인터페이스

     

     

    @Slf4j
    public class ConnectionTest {
        @Test
        void dirverManager() throws SQLException {
            Connection con1 = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
            Connection con2 = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
            log.info("connection={}, class={}", con1, con1.toString());
            log.info("connection={}, class={}", con2, con2.toString());
        }
    
        @Test
        void dataSourceDriverManager() throws SQLException {
            // DriverManagerDataSource - 항상 새로운 커넥션 획득
            DriverManagerDataSource dataSource = new DriverManagerDataSource(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
            useDataSource(dataSource);
        }
    
        private void useDataSource(DataSource dataSource) throws SQLException {
            Connection con1 = dataSource.getConnection();
            Connection con2 = dataSource.getConnection();
            log.info("connection={}, class={}", con1, con1.toString());
            log.info("connection={}, class={}", con2, con2.toString());
        }
    }

     

    • DriverManager는 커넥션을 획득할 때마다 파라미터를 계속 전달해야 한다
    • DataSource는 처음 객체를 생성할 때만 파라미터를 넘기고 커넥션을 획득할 때는 getConnection()만 호출하면 된다
      • "설정과 사용의 분리"가 이루어진다 

     

     

    @Slf4j
    class MemberRepositoryV1Test {
        MemberRepositoryV1 repository;
    
        @BeforeEach
        void beforeEach() {
            // 1. 기본 DataManager - 항상 새로운 커넥션 생성
            // DriverManagerDataSource dataSource = new DriverManagerDataSource(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
    
            // 2. 커넥션 풀링
            HikariDataSource dataSource = new HikariDataSource();
            dataSource.setJdbcUrl(ConnectionConst.URL);
            dataSource.setUsername(ConnectionConst.USERNAME);
            dataSource.setPassword(ConnectionConst.PASSWORD);
    
            repository = new MemberRepositoryV1(dataSource);
        }
    
        @Test
        void save() throws SQLException, InterruptedException {
            // save
            Member member = new Member("memberV100", 10000);
            repository.save(member);
    
            // findByID
            Member findMember = repository.findById(member.getMemberId());
            log.info("findMember={}", findMember.toString());
            // findMember=Member(memberId=memberV1, money=10000)
            assertThat(findMember).isEqualTo(member);
    
            // update
            repository.update(member.getMemberId(), 30000);
            Member updateMember = repository.findById(member.getMemberId());
            assertThat(updateMember.getMoney()).isEqualTo(30000);
    
            // delete
            repository.delete(updateMember.getMemberId());
            assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                    .isInstanceOf(NoSuchElementException.class);
    
            Thread.sleep(10000);
        }
    }

    • DriverManagerDataSource를 사용하면 conn0-5 번호를 통해 새로운 커넥션 생성

    • HikariDataSource(커넥션풀)을 사용하면 conn0으로 커넥션 재사용
    728x90

    'Spring&SpringBoot' 카테고리의 다른 글

    @Transaction  (0) 2025.11.13
    트랜잭션, DB 락  (0) 2025.11.12
    스프링 AOP 실무 주의사항  (0) 2025.10.21
    로그출력AOP & 재시도AOP  (0) 2025.10.21
    템플릿 메서드 패턴 & 전략 패턴 & 템플릿 콜백 패턴  (0) 2025.10.13
Designed by Tistory.