Spring 강의/DB

[스프링 DB 1편] - (2) 커넥션풀과 데이터소스

lxexjx 2022. 4. 20. 11:12

[커넥션 풀]

데이터베이스 커넥션  획득

1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회(getConnection() )

2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결. 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생

3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달.

4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성.

5. DB는 커넥션 생성이 완료되었다는 응답을 보냄.

6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환.

 

커넥션을 새로 만드는 것은 과정도 복잡하고 시간도 많이 많이 소모. DB는 물론이고 애플리케이션 서버에서도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용 ->느려져

 

[커넥션 풀 초기화] - 커넥션을 미리 생성해두고 재사용

애플리케이션 시작 시점에 커넥션 풀에 커넥션을 미리 만들어놔.
커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태라서 즉시 sql애 연결 가능
풀한테 커넥션을 달라고 하면 반환

 

반환 받은 커넥션을 가지고 sql을 db에 전달하고 결과 받아서 처리. 커넥션을 다 사용하고 나면 커넥션을 그대로 커넥션 풀에 반환

 


[DataSource]

 

 

 

커넥션 획득하는 방법

1.DriverManager를 통해서 커넥션 생성해서 얻어오기

2.풀에서 조회

DriverManager를 통해 커넥션 획득하다가 커넥션 풀로 변경시 문제.커넥션을 획득하는 애플리케이션 코드도 함께 변경

 

 

어떻게 얻을 것인지 방법을 추상화!!! 하는 것이 datasource의 기능

DataSource 는 커넥션을 획득하는 방법을 추상화 하는 인터페이스.

커넥션 풀 의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성.

DriverManager 는 DataSource 인터페이스를 사용하지 않는다. 따라서 DriverManager 는 직접 사용.

 

 


[DriverManager]

 

<ConnectionTest> - 드라이버 매니저를 통해서 커넥션 획득

@Slf4j
public class ConnectionTest {

    //커넥션을 각각 가져오는지 ->서로 다른 커넥션
    @Test
    void driverManager() throws SQLException {
        Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD); //커넥션 하나를 db랑 얻게됨
        Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD); //2개 얻고
        log.info("connection={}, class={}", con1, con1.getClass());
        log.info("connection={}, class={}", con2, con2.getClass());
    }

    //spring이 제공하는 datasource가 적용된 deivermanager사용
    @Test
    void datasourceDriverManager() throws SQLException {
        //DriverManagerDataSource는 항상 새로운 커넥션 획득
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        useDataSource(dataSource);
    }

    private void useDataSource(DataSource dataSource) throws SQLException {
        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection={}, class={}", con1, con1.getClass());
        log.info("connection={}, class={}", con2, con2.getClass());
    }
}

DriverManager 는 커넥션을 획득할 때 마다

DriverManager.getConnection(URL, USERNAME, PASSWORD)
DriverManager.getConnection(URL, USERNAME, PASSWORD) 

URL , USERNAME , PASSWORD 같은 파라미터를 계속 전달

 

 

DataSource 를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection() 만 호출.DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);여기서 다 세팅함

Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
설정과 사용을 분리함!!

 


[DataSource]

 

<ConnectionTest 추가> - 데이터소스 커넥션 풀 사용

@Test
void dataSourceConnectionPool() throws SQLException,InterruptedException {
    //커넥션 풀링 using히카리
    HikariDataSource dataSourcea = new HikariDataSource();
    dataSourcea.setJdbcUrl(URL);
    dataSourcea.setUsername(USERNAME);
    dataSourcea.setPassword(PASSWORD);
    dataSourcea.setMaximumPoolSize(10);
    dataSourcea.setPoolName("MyPool");

    useDataSource(dataSourcea);
    Thread.sleep(1000);
}

HikariDataSource dataSource = new HikariDataSource() : HikariDataSource 는 DataSource 인터페이스를 구현.


[DataSource]

<MemberRepositoryV1>

/**
 * JDBC - DataSource 사용,JdbcUtils 사용
 */
@Slf4j
public class MemberRepositoryV1 {

    //DataSource의존관계 주입
    private final DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values(?, ?)";
        Connection con = null;  //연결
        PreparedStatement pstmt = null; //db에 쿼리 날려
        try {
            con = getConnection();   //드라이버 매니저로 커넥션 획득
            pstmt = con.prepareStatement(sql);  //sql넘기기, sql예외는 try-catch로
            pstmt.setString(1, member.getMemberId());   //파라미너 바인딩
            pstmt.setInt(2, member.getMoney());//파라미터 바인딩
            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);  //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 {    //data가 없다
                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) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }
    private Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        log.info("get connection={}, class={}", con, con.getClass());
        return con;  //커넥션 가져오기
    }
}

 

<MemberRepositoryV1Test>

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

    @BeforeEach
    void beforeEach(){
        //기본 DriverManager - 항상 새로운 커넥션 획득
       //DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);

 //커넥션 풀링 사용 : HikariProxyConnection -> JdbcConnection
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        repository = new MemberRepositoryV1(dataSource);

        repository = new MemberRepositoryV1(dataSource);
        repository = new MemberRepositoryV1(dataSource);
    }

    @Test
    void crud() throws Exception{
        //save
        Member member = new Member("memberV0", 10000);
        repository.save(member);

        //findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember={}", findMember);//@Data에서 toString
        assertThat(findMember).isEqualTo(member);

        //update: money: 10000 -> 20000
        repository.update(member.getMemberId(), 20000);
        Member updatedMember = repository.findById(member.getMemberId());
        assertThat(updatedMember.getMoney()).isEqualTo(20000);

        //delete
        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                .isInstanceOf(NoSuchElementException.class);
    }
}

 

 

커넥션 풀을 사용하면 커넥션을 재사용할 수 있다.

DI + OCP : DriverManagerDataSource HikariDataSource 로 변경해도 MemberRepositoryV1 의 코드는 전혀 변경하지 않아도 됨. MemberRepositoryV1 는 DataSource 인터페이스에만 의존.