안녕하세요, 이번 글에서는 Java에서 Connection을 얻는 방법 중에 하나인 DriverManager를 통한 커넥션 획득에 대해 적어보겠습니다.
DriverManager
앞선 글(JDBC)에서 애플리케이션에서 Database를 사용하려면 아래 세 가지 행위가 필요하다 말씀드렸는데요,
- 커넥션을 연결한다.
- SQL을 Database 서버에 전달한다.
- 결과를 응답받는다.
JDBC에서는 Connection을 연결한 뒤 반환받는 Connection Interface를 통해 SQL을 전달하고, 커밋 혹은 롤백을 할 수 있도록 구성되어 있습니다. Connection을 획득하면, 해당 객체를 활용해서 그 뒤의 작업들을 하실 수 있습니다. 따라서 각 Database 벤더사들은 해당 Connection을 가져오는 방법을 구현해 두었는데요. 이 표준 인터페이스를 Driver라고 합니다.
그리고 애플리케이션에서 의존하고 있는 Driver들을 관리하는 Class가 DriverManager라는 Class입니다.
Javadoc에서 가장 핵심적인 내용만 몇개 가져와서 보면,
- The basic service for managing a set of JDBC drivers.
- javax. sql. DataSource interface, provides another way to connect to a data source. The use of a DataSource object is the preferred means of connecting to a data source.
- Each driver is loaded using the system class loader: Service providers of the java. sql. Driver class, that are loaded via the service-provider loading mechanism.
JDBC Driver들을 매니징하는 클래스이고, 현재는 javax.sql.DataSource를 사용하는 것이 추천되는 방법이라 합니다.
(해당 이유는 다음 글(DataSource와 Connection Pool)에서 설명하겠습니다.)
그리고 Java service-provider loading 메커니즘을 통해 구현체를 얻어온다 하는데, 이 부분은 자바의 ServiceLoader라는 클래스를 활용하여 동작합니다.
Service-Provider Loading Mechanism
클린 코드에 보면 아래와 같은 글이 있는데요,
소프트웨어 시스템은
에플리케이션 객체를 제작하고 의존성을 서로 연결하는 준비 과정과
런타임 로직을 분리해야 한다.
즉 interface를 기반으로 구현하고, runtime에는 구현 코드를 포함한 jar를 의존성에 추가하는 형식으로 언제든 구현체를 유연하게 변경할 수 있다는 장점을 가져가자는 것입니다.
각 DB 벤더사에서 Driver를 각자의 Database에 맞게 구현해 두면 이를 애플리케이션에서 가져와서 언제든지 사용할 수 있는 것이죠.
간단하게 ServiceLoader를 사용하는 방법에 대해 예제를 조금 작성해보았습니다. (github)
interface SampleInterface {
fun sampling(): String
}
class SampleInterfaceImpl : SampleInterface {
override fun sampling(): String {
return "implementation"
}
}
class OtherInterfaceImpl : SampleInterface {
override fun sampling(): String {
return "another implementation"
}
}
간단한 Interface와 구현체 두개를 만들어 보았습니다.
그리고 ServiceLoader가 구현 클래스를 정상적으로 인지하고 동작하려면 resources/META-INF/services 디렉터리에 각 interface와 구현체가 적힌 파일을 추가해주어야 합니다.
이제 정상적으로 동작하는 지 테스트를 해보면,
class ServiceLoaderTest : StringSpec({
val logger = KotlinLogging.logger {}
"service-provider loading mechanism" {
val loadedImplementation =
ServiceLoader.load(SampleInterface::class.java)
val iter = loadedImplementation.iterator()
while (iter.hasNext()) {
logger.info { iter.next().sampling() }
}
}
})
...
Testing started at 9:56 PM ...
21:56:17.652 [pool-1-thread-1] INFO c.n.s.b.s.ServiceLoaderTest --implementation
21:56:17.653 [pool-1-thread-1] INFO c.n.s.b.s.ServiceLoaderTest --another implementation
정상적으로 구현체들을 가져와서 사용할 수 있는 것을 확인할 수 있습니다.
ServiceLoader의 동작을 이해하셨다면 DriverManager에서 Connection을 가져오는 부분의 코드를 조금 더 이해하실 수 있습니다.
DriverManager.getConnection(...)
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
callerCL = Thread.currentThread().getContextClassLoader();
}
if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}
ensureDriversInitialized(); // <<<<<<<<<<<<<<<<< (1)
SQLException reason = null;
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) { // <<<<<<<<<<<<<<<<< (2)
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info); // <<<<<<<<<<<<<<<<< (3)
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.driver.getClass().getName());
}
}
// if we got here nobody could connect.
... 에러 로깅 및 throw
}
(1) ensureDriversInitialized() 메서드에서 실질적으로 현재 등록되어 있는 Driver들의 클래스를 찾아오고 초기화하여 줍니다. 즉 DriverManager는 애플리케이션이 실행될 때 초기화되는 것이 아닌, 한번 호출되었을 때 Lazy 하게 초기화되는 것을 확인할 수 있습니다.
ensureDriversInitialized 내부 로직을 보면, 처음에 Lock을 설정한 뒤 (동시에 여러번 초기화되는 것을 막기 위함)
위에 예제에서 보신것처럼 java.sql.Driver를 구현한 클래스들을 가져와서 초기화해주게 됩니다.
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
(2) 그러면 이제 등록된 여러개의 Driver 중에 내가 접속하고 싶은 Driver를 찾아야 하는데, 이때 접속 정보로 함께 넘긴 url 정보로 내가 처리할 수 있는 url인지 판단합니다.
예를 들어 제가 jdbcUrl을 jdbc:mysql://localhost:3306/example... 과 같이 넘겼다면 mysql Driver 구현체는 해당 Url을 보고 내가 처리할 수 있다고 판단하고 true를 줄 것입니다. 만약 PostgreSQL과 같은 다른 Database는 Driver가 등록되어 있다 하더라도 해당 Url이 처리할 수 없는 Url이기 때문에 false를 반환할 것입니다.
(3) 최종적으로 선택된 Driver에서 Url, Username, Password를 사용하여 접속 시도 후 Connection 객체를 생성하여 반환합니다.
크게 보면 이게 DriverManager가 제공하는 주요 기능입니다.
DriverManager의 단점
지금까지 저희가 살펴본 DriverManager는 커다란 단점이 있습니다. DriverManager는 커넥션을 항상 새로 획득해야 합니다. 커넥션은 애플리케이션과 DB 서버 간의 Tcp/Ip 연결이고, Connection이 신규로 생성되면 DB는 세션도 새로 만들어야 하고, 또 이렇게 받은 커넥션은 재활용할 수 없기 때문에 사용 후 resource를 정리 (free) 해주어야 합니다.
이러한 단점들을 극복하기 위해 한번 획득한 커넥션을 재활용하기 위한 Connection Pool이라는 개념이 도입되고 현재는 거의 모든 애플리케이션들이 Connection Pool을 활용하고 있습니다.
다음 글에서는 Connection Pool에 대해 정리해보도록 하겠습니다. 감사합니다.
'Backend > Java' 카테고리의 다른 글
JDBC 이해하고 사용해보기 (2) | 2024.07.23 |
---|---|
[Java] JaCoCo를 활용하여 code coverage 측정하기 (0) | 2021.10.19 |
[Java] Collections framework 이해하기 (0) | 2021.07.17 |
[Java] 제네릭 이해하기 (0) | 2021.07.17 |
[Java] static 제어자 이해하기 (0) | 2021.07.04 |