-
JDBCSpring&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 연결 이해

- 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 호출
- DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식 후 드라이버들에게 순서대로 정보를 넘겨 커넥션 획득할 수 있는지 확인
- 각각 드라이버는 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()를 최초 한번 호출 필요
커넥션 풀

- 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회
- DB 드라이버는 DB와 TCP/IP 커넥션 연결
- 이 과정에서 3 way handshake 같은 연결을 위한 네트워크 동작 발생
- DB 드라이버는 커넥션이 연결되면 기타 부가정보를 DB에 전달
- DB는 부가정보를 통해 내부 인증을 하고 내부에 DB 세션을 생성 후 완료 응답 전달
- 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