땃쥐네

[Spring DB] 커넥션과 DataSource 본문

Spring

[Spring DB] 커넥션과 DataSource

ttasjwi 2022. 9. 4. 20:06

선행 지식

[Spring DB] JDBC 표준 인터페이스의 등장 배경 에서 이어집니다.


실습 환경

프로젝트 생성

  • 의존 라이브러리 : Lombok, H2, JDBC API
  • 자바 : 8버전 이후면 상관 없긴한데, 가급적 11 이후 버전 사용

1. MySQL과 같은 DB는 실습 환경에서 사용하기 부담이 큽니다.

2. 이후 다룰 내용이긴한데 H2 데이터베이스는 java로 개발되어 있고, 테스트 시 JVM 안에서 메모리 모드로 동작하는 기능이 제공됩니다. 기본적인 스프링-DB 접근기술의 개념적인 학습에 있어서는 H2 데이터베이스가 가장 좋습니다.

build.gradle

```groovy
dependencies {
   // test에서 Lombok 사용
   testCompileOnly 'org.projectlombok:lombok'
   testAnnotationProcessor 'org.projectlombok:lombok'
}
```

build.gradle에서 롬복 테스트 의존관계를 추가합니다.

H2 데이터베이스 설치

  • 다운로드 : https://www.h2database.com/html/download-archive.html
  • 버전 : 사용하는 스프링부트에서 지원하는 버전을 선택하여 설치 권장
    • 스프링부트 2.6버전대에서는 1.4.200를 지원하였고 이 버전을 사용했습니다.
    • 현재 2.7.3 기준으로는 2.1.214를 지원하고 있는데 실제로 저는 이 버전을 사용하지 않아서 잘 될 지는 아직 확인하지 못 했습니다
  • 설치 : 홈 경로(~) 하위에 설치하는 것을 권장하며 추후 교체의 용이성을 위해 zip으로 받아 설치하는 것이 좋습니다.

 

H2 데이터베이스 실행

 

H2 하위의 bin 폴더로 이동해 ./h2.bat 을 실행합니다.

Mac OS의 경우는 ./h2.sh를 실행하는 것으로 알고 있으며 chmod 755 h2.sh를 통해 권한을 먼저 부여해야합니다.

 

데이터베이스 생성

브라우저 상에서 h2 데이터베이스가 실행됩니다.

 

처음 주어지는 jsessionId가 관리자 Session인데, 데이터베이스를 생성할 때는 이 세션에서 생성해야합니다.

jdbc url을 위와 같이 지정하여 연결하면 데이터베이스가 생성되고 홈 경로에 해당 mv.db 파일이 생성되는 것을 확인할 수 있습니다.

 

이후에는 tcp를 통해 접근할 수 있도록 이와 같이 연결하시면 됩니다. 여기서 jdbc url은 복사해두고 이후 사용하도록 합니다.


 

H2 데이터베이스 드라이버 확인

 

의존 라이브러리들을 확인해보시면 자바 표준 JDBC 인터페이스의 구현체들이 등록되어있는 것을 확인할 수 있습니다.

 

  • Connection -> JdbcConnection
  • ResultSet -> JdbcResultSet
  • Statement -> JdbcStatement
  • PreparedStatement -> JdbcPreparedStatement

커넥션

Connection

package java.sql;

public interface Connection {}
package org.h2.jdbc;

public class JdbcConnection implements Connection, ... {}

데이터베이스에 접근하여, 데이터를 조작하거나 조회해오려면 'Connection'이란 것을 획득해야합니다.

모든 SQL 명령은 커넥션을 통해 이루어집니다.

 

jdbc API에서는 java.sql.Connection이 표준 인터페이스에 해당하고, H2 데이터베이스는 orc.h2.jdbc.JdbcConnection이 이를 구현했습니다.

 

접속 설정 정보 static 상수화

package com.ttasjwi.jdbc.util;

public class ConnectionConstant {

    public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc_ex";
    public static final String USER = "sa";
    public static final String PASSWORD = "";

}

우선 접속에 관한 정보를 어딘가 적어둬야합니다.

가장 간단하게 생각해볼 수 있는 방식은 별도로 유틸 클래스를 생성해 이들을 모아두는 것입니다.

 

물론 실무에서는 이렇게 배포 자바코드에 데이터베이스 접근설정을 절대 두지 않습니다.


DriverManager : 커넥션을 생성하여 획득

package java.sql;

public class DriverManager {

    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        
        // 생략
    }

}

DriverManager는 JDBC 표준 인터페이스에 포함된 유틸 클래스입니다.

이 클래스는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 생성하여 획득하는 기능을 제공합니다.

 

이 안에 보시면 static 메서드로 getConnection이라는 메서드가 존재하는데, 이를 통해 데이터베이스 URL, 사용자명, 패스워드를 전달하면 라이브라리에 등록된 DB 드라이버를 조회해서 커넥션을 획득할 수 있습니다.

 

 

더 자세히 살펴볼까요?

 

드라이버 매니저의 getConnection 메서드는 라이브러리에 등록된 드라이버들에게 순서대로 메서드의 인자들(URL, USER, PASSWORD)을 넘겨서 커넥션을 획득할 수 있는 지 확인합니다. 여기서 각 드라이버는 URL 문자열 정보를 체크해서 자신이 처리할 수 있는 요청인지를 확인하는데요.

 

URL이 처리할 수 있는 요청일 경우 처리할 수 없다는 결과를 반환하고, 다음 드라이버 쪽으로 넘어갑니다. 이런 식으로 순서대로 드라이버들에게 확인한 뒤, 처리할 수 있는 요청일 경우 실제 데이터베이스에 연결하여 물리적인 커넥션을 맺고 해당 데이터베이스 드라이버에서 구현한 Connection 인스턴스를 생성해 반환합니다.

  • jdbc 인터페이스 : java.sql.Connection
  • H2의 커넥션 구현체 : org.h2.jdbc.JdbcConnection

(실습) DriverManager를 통해 커넥션 획득하기

 

package com.ttasjwi.jdbc.datasource;

// 생략

import static com.ttasjwi.jdbc.util.ConnectionConstant.*;
import static org.assertj.core.api.Assertions.*;

@Slf4j
public class DataSourceTestV1 {

    @Test
    void driverManager() throws SQLException {
        Connection con = DriverManager.getConnection(URL, USER, PASSWORD);
        log.info("con = {}", con, con.getClass());
        log.info("con.class = {}", con.getClass());
        assertThat(con).isNotNull();
    }

}

DriverManager를 통해서, 우리가 라이브러리에 등록한 드라이버를 통해 실제 DB와 물리적 연결을 맺어 Connection을 획득하는 것을 테스트해봅니다.

 

... TestV1 - con = conn0: url=jdbc:h2:tcp://localhost/~/jdbc_ex user=SA
... TestV1 - con.class = class org.h2.jdbc.JdbcConnection

실제 테스트를 실행하보면 커넥션이 null 이 아니여서 통과되고, 로그를 확인해보면 커넥션 정보를 볼 수 있습니다.

커넥션의 클래스가 실제로는 H2 데이터베이스가 구현한 org.h2.jdbc.JdbcConnection임을 알 수 있습니다.

 

 

여기서 잠깐. 위 방식의 문제를 살펴봅시다.

매번 커넥션을 획득할 때마다, DriverManager.getConnection을 통해 접속정보를 일일이 넘겨야하는 귀찮은 문제가 존재합니다. 실제 애플리케이션 로직에서는 커넥션을 획득할 일이 많을텐데 각 사용처마다 일일이 이렇게 파라미터에 접속에 관한 설정 정보를 넘겨야합니다.

 

(개선) 접속에 관한 정보를 한 private 메서드를 통해 관리

@Slf4j
public class DataSourceTestV2 {

    @Test
    void driverManager() throws SQLException {
        Connection con = getConnection();
        log.info("con = {}", con, con.getClass());
        log.info("con.class = {}", con.getClass());
        assertThat(con).isNotNull();
    }

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

}

커넥션을 얻어오는 부분을 별도의 private 메서드를 통해 분리했습니다.

 

이제 애플리케이션 로직에서는 해당 private 메서드를 사용하기만 하면 되고 커넥션을 획득하는 방법은 위 메서드 한곳에서 집중하면 됩니다.

(+ 이렇게 priavte 메서드로 분리한 방식은 이후 사용할 방식을 사용하면 사용할 일이 없긴 합니다.)


커넥션을 획득하는 여러가지 방법들

지금 우리가 사용한 DriverManager를 통해 커넥션을 획득하는 방법은 매번 커넥션을 가져오라는 요청이 발생했을 시, 실제 커넥션을 새로 생성해서 가져오는 방식이죠.

 

그런데 뒤에서 다루겠지만 이것 외에도 커넥션을 획득하는 방법은 더 많이 존재합니다. 애플리케이션 로딩 시점에 커넥션을 잔뜩 생성하여 커넥션 풀에 보관한 뒤 매번 사용 요청이 생길 때마다 커넥션풀에서 커넥션을 빌려 주는 방식도 존재합니다. 그리고 이 커넥션 풀링 기법을 사용하는 오픈 라이브러리들이 실제로 여럿 존재합니다.(HikariCP, DBCP2,... )

 

커넥션을 획득하는 방법은 여럿 존재하는데 이들을 제각각 다루는 방법이 다른게 문제입니다.


DataSource : 커넥션을 획득하는 방법의 표준화

package javax.sql;

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;
  
  // 생략

}

Java는 이렇게 여러가지 커넥션을 획득하는 방법을 표준화하기 위해서, DataSource라는 인터페이스를 제공했습니다.

 

DataSoruce 인스턴스를 생성하는 시점에 설정 정보를 주입하고, 애플리케이션 로직에서는 getConnection() 을 호출하기만 하면 커넥션을 획득할 수 있습니다.

 

어떤 커넥션을 획득하는 방법을 쓰든, DataSource 인터페이스를 구현체를 사용한다면, 인터페이스를 의존하여 똑같은 방법으로 커넥션을 획득할 수 있습니다.

 

실제로 커넥션 풀링 기법을 사용하는 HikariCP, DBCP 라이브러리는 이 DataSource 인터페이스를 구현하였습니다.


DriverManagerDataSource

 

앞서 사용한 DriverManager는 DB와 매번 물리적인 커넥션을 맺고 인스턴스를 생성해 반환하는 기능을 수행하는데요.

이는 DataSource 인터페이스를 구현하지 않았다는 것이 문제입니다.

 

나중에 DriverManager를 통해 커넥션을 획득하는 방법을 다른 방식으로 변경하려면 애플리케이션 로직의 상당수를 고쳐야하는 문제가 존재합니다.

 

package org.springframework.jdbc.datasource;

// 생략

public class DriverManagerDataSource extends AbstractDriverBasedDataSource {}

 

이런 문제점을 Spring이 해결해줍니다. Spring은 DriverManager 방식의 커넥션 획득 방법을 사용할 수 있게, DriverManagerDataSource라는 구현체를 라이브러리에 등록해뒀습니다.

 

앞으로 DriverManager 방식대로 커넥션을 매번 생성하고 버리는 방식을 사용하고 싶다면,

DriverManagerDataSource로 대체하여 표준화된 방식으로 커넥션을 획득하면 됩니다.

 

나중에 다른 방식으로 커넥션을 획득하고 싶다면 구현체만 갈아끼면 되고 애플리케이션 로직을 뜯어고칠 일이 없어집니다.


(실습) DriverManagerDataSource 사용하기

package com.ttasjwi.jdbc.datasource;

import static com.ttasjwi.jdbc.util.ConnectionConstant.*;
import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
public class DataSourceTestV3 {

    private DataSource dataSource;

    @BeforeEach
    void beforeEach() {
        dataSource = new DriverManagerDataSource(URL, USER, PASSWORD);
    }

    @Test
    void driverManagerDataSource() throws SQLException {
        // 로직 1
        Connection con1 = getConnection();
        log.info("con1 = {}", con1, con1.getClass());
        log.info("con1.class = {}", con1.getClass());

        // 로직 2
        Connection con2 = getConnection();
        log.info("con2 = {}", con2, con2.getClass());
        log.info("con2.class = {}", con2.getClass());

        assertThat(con1).isNotNull();
        assertThat(con2).isNotNull();
    }

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

}

역할에 의존

테스트 코드 클래스에서는 DataSource를 의존하고 있고, BeforeEach를 통해 각 테스트 코드에서는 DriverManagerDataSource 구현체를 사용할 수 있게 합니다.

 

실제 애플리케이션 로직으로 치면, 리포지토리 계층이 내부적으로 DataSource를 의존하고 내부에는 구현체에 관한 코드가 없다고 생각하시면 됩니다. DataSource를 해당 클래스 의존관계로 등록해두고, 외부에서 DataSource 구현체를 주입해주는 방식을 사용합니다.

 

애플리케이션 로직에서는 어떤 구현체가 주입되었는지 모르고 그저 DataSource의 getConnection() 메서드를 의존하기만 하면 됩니다.

 

설정 : 생성시점 한 곳에서 관리

또, datasource 생성 시점에 설정 정보를 전달하였고 애플리케이션 로직에서는 접근 정보를 별도로 다시 전달하여 커넥션을 획득하지 않고 있음을 확인할 수 있습니다. 설정 정보가 한 곳에서 관리되는 것입니다.

DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:h2:tcp://localhost/~/jdbc_ex]
con1 = conn0: url=jdbc:h2:tcp://localhost/~/jdbc_ex user=SA
con1.class = class org.h2.jdbc.JdbcConnection

DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:h2:tcp://localhost/~/jdbc_ex]
con2 = conn1: url=jdbc:h2:tcp://localhost/~/jdbc_ex user=SA
con2.class = class org.h2.jdbc.JdbcConnection

DriverManager 방식을 유지

실제 로그를 확인해보시면 매번 커넥션을 획득할 때마다, DriverManager 방식대로 커넥션을 새로 생성해서 획득하는 것을 확인할 수 있습니다.


스프링 부트와 DataSource

```properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/jdbc_ex
spring.datasource.username=sa
spring.datasource.password=
```

스프링 부트에서는, application.properties 또는 application.yml에 위와 같이 데이터베이스 접근 설정을 등록해두면 스프링부트 애플리케이션 로딩 시점에 HikariCP의 HikariDataSource(DataSource) 구현체를 스프링 빈으로 등록합니다.

 

(+ 이 외에도, 트랜잭션 매니저 등도 빈으로 등록해주는데 이는 이후 다루도록 하겠습니다.)

 

HikariCP는 요즘 웹 애플리케이션에서 가장 많이 사용되는 커넥션 풀링 방식의 커넥션 획득 라이브러리입니다. 커넥션 풀링 방식은 이후 추가적으로 다루도록 하고, 지금은 Spring 설정 정보를 등록하면 스프링이 알아서 DataSource 구현체를 빈으로 등록하는 것을 알아두시면 됩니다.

 

물론 사용자 스스로 수동으로 DataSource 구현체를 빈으로 등록할 수도 있습니다. 이 경우 스프링이 자동으로 빈을 등록하지 않고 저희가 수동으로 등록한 DataSource 구현체가 빈으로 등록됩니다.

 

설정 : 외부 설정 파일 한 곳에서 관리

이 방식을 통해 자바코드 바깥으로 외부 설정 파일을 통해 데이터베이스 접근 정보를 관리할 수 있게 됩니다. 실제 배포 시, gitHub 등에 데이터베이스 접근 설정이 넘어가선 안 되는데 이를 자바코드 바깥에서 관리할 수 있게 해줍니다. (물론 위 방식 말고도 외부 파일에 데이터베이스 접근 설정을 모아서 관리할 수 있긴 합니다.)


실습 : 스프링 부트의 DataSource 구현체 등록 확인

@Slf4j
@SpringBootTest
public class DataSourceTestV4 {

    @Autowired
    private DataSource dataSource;

    @Test
    public void dataSourceWithSpringBoot() {
        log.info("dataSource.class : {}", dataSource.getClass());
    }

    @Test
    void springDataSource() throws SQLException {
        // 로직 1
        Connection con1 = getConnection();
        log.info("con1 = {}", con1, con1.getClass());
        log.info("con1.class = {}", con1.getClass());

        // 로직 2
        Connection con2 = getConnection();
        log.info("con2 = {}", con2, con2.getClass());
        log.info("con2.class = {}", con2.getClass());

        assertThat(con1).isNotNull();
        assertThat(con2).isNotNull();
    }

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

}

앞서 application.properties에 설정정보를 등록해둔 상태로 스프링을 연동하면 HikariDataSource 구현체가 빈으로 등록됩니다. 실제로 위 클래스에서는 접속 정보에 관한 정보를 별도로 코드로 작성하지 않았습니다!

 

dataSource.class : class com.zaxxer.hikari.HikariDataSource

 

실제 코드를 실행해보면 저희가 구현체를 등록하지 않았음에도 SpringBoot가 DataSource 구현체인 HikariCPDataSource를 빈으로 등록해준 것을 확인할 수 있습니다.

HikariPool-1 - Starting...
HikariPool-1 - Start completed.

con1 = HikariProxyConnection@490475818 wrapping conn0: url=jdbc:h2:tcp://localhost/~/jdbc_ex user=SA
con1.class = class com.zaxxer.hikari.pool.HikariProxyConnection

con2 = HikariProxyConnection@527505741 wrapping conn1: url=jdbc:h2:tcp://localhost/~/jdbc_ex user=SA
con2.class = class com.zaxxer.hikari.pool.HikariProxyConnection

커넥션을 획득하는 방식은 HikariDataSource의 커넥션 풀링 방식을 사용하는 것을 볼 수 있는데요.

이 커넥션 풀링 방식에 대해서는 이후 다루도록 하겠습니다.


추가 학습 키워드

 

- [Spring DB] 순수 JDBC 기술 - 1. 기본 CRUD

- 커넥션 풀링 방식

- Jdbc 트랜잭션 사용 방법, JPA 트랜잭션 사용 방법, 스프링의 트랜잭션 사용방법 표준화, @Transactional, ...

- SQLException이 자꾸 나타나는데 저걸 치울 방법은? (feat : 체크예외, 언체크 예외)

 


 

Comments