JDBC API는 어떻게 사용하는 DB에 맞는 Driver를 찾을 수 있을까?

DevSeoRex·2023년 7월 25일
9
post-thumbnail

😰 이펙티브 자바를 보다가.. 여기까지 와버렸다!

이펙티브 자바 2장 아이템 1(생성자 대신 정적 팩토리 메서드를 고려하라)을 보다가 정적 팩토리 메서드의 장점 5개중 마지막 장점으로 나온 부분에서 JDBC API를 예시로 드는데 도저히 이해가 되지 않았습니다.

정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

JDBC API를 뜯고 또 뜯어보며, 이 내용을 이해하기 위한 삽질의 결실을 얻고 드디어 게시글을 쓰게 되었습니다.

🥳 JDBC 너의 내부가 궁금해!


국비학원이나 부트캠프를 다녀보신 분들이라면 이 코드를 아시는 분이 많으실거라 생각합니다.

JDBC Driver를 로드하고, Connection을 얻고 문자열로 된 쿼리문을 실행하고 조회된 데이터를 ResultSet에 담아오는 간단한 코드입니다.

의문은 여기서 시작되었습니다.
어디에서도 드라이버 클래스의 객체를 생성하거나 어떤 종류의 드라이버를 사용하겠다고 명시하지 않았는데도 MySQL을 쓸때는 MySQL의 드라이버가 Oracle을 쓸때는 Oracle의 드라이버가 로딩되었습니다.

Class.forName 메서드에 사용하려는 드라이버 클래스의 위치만 문자열로 제공했을 뿐인데 이게 어떻게 가능할까요?

JDBC API는 Driver와 Connection 인터페이스를 제공하고 있습니다.
각 벤더사에서는 Driver과 Connection을 구현한 클래스를 제공하여, 사용자들이 벤더사의 DB를 JDBC API를 통해 이용할 수 있도록 하고 있습니다.

저는 MySQL을 주로 사용하고 있기 때문에 com.mysql.cj.jdbc 패키지의 Driver 클래스를 살펴보도록 하겠습니다.

MySQL의 Driver 클래스 확인하기


java.sql.Driver 인터페이스를 구현한 간단한 구현 클래스입니다.
이 코드에서 핵심은 static 블럭안에 있는 코드입니다.

DB의 Driver와 같이 인스턴스를 관리하지 않고 static 블럭에서 초기화를 진행하는 경우에 사용하는 방법입니다.
Class.forName 메서드에 com.mysql.cj.jdbc.Driver 클래스를 로드하도록 인자로 제공하게 되면, 인스턴스가 초기화 되게 됩니다.

표현 방법의 차이일 뿐, new 연산자를 사용해 객체를 생성하는 것과 전혀 다른 방법은 아닙니다.
static 초기화 블럭은 어떤 초기화 블럭보다 가장 먼저 실행되기 때문에, Class.forName을 이용해 클래스가 위치한 경로만 적어서 호출해도 DriverManger.registerDriver 메서드에 의해 등록되게 되는 것입니다.

그렇다면 DriverManger의 내부는 어떻게 구현되어 있는지 확인해보겠습니다.

DriverManager 클래스 확인하기


DriverManager 또한 private 생성자를 가지며 인스턴스화 할 수 없게 설계되어 있습니다.
DB에 연결하는 작업 등 다양한 일을 수행하기 위한 정적 메서드들을 제공하고 있습니다.

Driver 클래스에서 사용한 registerDriver 메서드의 구현부를 보겠습니다.

내부적으로 메서드 시그니처가 다른 registerDriver를 호출하고 있으니 따라가 보겠습니다.

이 코드를 통해 또 하나의 의문을 풀 수 있게 되었습니다.
Class.forName을 통해 계속 MySQL 드라이버를 등록한다면 수십개의 드라이버 객체가 메모리를 차지하고 낭비하지 않을까? 라는 생각을 하게 되었습니다.

addIfAbsent 메서드는 이름과 같이, 추가하려는 원소가 배열에 존재하지 않을때만 추가하게 됩니다.
따라서 이미 등록된 Driver 클래스의 객체가 있다면, 다시 등록되지 않습니다.

Driver 클래스를 내부적으로 가지고 있는 DriverInfo 클래스들을 등록해 보관하는 registeredDrivers 필드는 CopyOnWriteArrayList로 구성되어 있습니다.

java.util.concurrent 패키지에 속한 CopyOnWriteArrayList는 Thread-Safe 하지 않은 ArrayList를 대신하여 동시성을 제어할 수 있는 List 입니다.

배열을 수정 하는 가변 작업(수정, 삽입 등)은 기존 배열을 복사한 복사본을 이용해 작업하여 Thread-Safe 합니다.


현재 삽입하려는 원소가 배열에 존재하는지 판단하기 위해, indexOfRange 메서드를 활용해 정수를 return 받게 되는데 -1이 return 되어야 존재하지 않는 원소이므로 0보다 작아야한다는 조건을 주었습니다.

이제 등록이 되는 부분은 충분히 뜯어보고 확인했습니다.
Connection을 얻어오는 getConnection 메서드를 어떻게 구현하고 있는지 이 부분을 확인해보겠습니다.


url과 user 그리고 password를 제공받아 Connection을 반환하는 메서드로, 대부분 이 메서드를 호출하게 됩니다.
내부적으로 Properties 클래스를 생성해, user와 password를 넣어주고 다시 내부 호출을 진행합니다.


내부 호출한 메서드의 가장 핵심적인 부분만 보겠습니다.
CopyOnWriteArrayList의 원소를 모두 순회하면서, 연결하려고 하는 데이터베이스와 일치하는 드라이버의 Connection을 반환해줍니다.

DriverManager.getConnetion 메서드는 url과 user 그리고 password만 제공받게 되는데 어떻게 데이터베이스의 종류를 구분하고 알맞은 드라이버에서 Connection을 반환하도록 할 수 있을까요?

결론부터 말씀 드리면, URL 패턴을 통해 현재 Driver 클래스와 일치하는 연결 요청인지 확인합니다.
먼저 MySQL에서 그 역할을 하는 NonRegisteringDriver 클래스를 확인해보겠습니다.


ConnectionUrl.acceptsUrl 메서드를 호출하여 URL 패턴을 정규 표현식으로 검증하게 됩니다.
이 검증에 통과하지 못하면 MySQL 연결을 위한 URL이 아니라고 판단하고 null을 반환합니다.

ConnectionUrlParser 클래스는 여러 정규 표현식을 활용하여 URL 패턴을 검증하게 됩니다.

그 후에는 case문을 통해서 연결 타입에 따라 다양한 종류의 Connection 객체를 반환해줍니다.
전부 다른 객체같이 보이지만 반환되는 타입은 모두 JdbcConnection 인터페이스를 구현했고, JdbcConnection 인터페이스는 Connection 인터페이스를 구현해서 다형성에 의해 반환이 가능합니다.

번외로 H2 데이터베이스의 connect 메서드를 확인해보겠습니다.

H2 데이터베이스 또한 URL 패턴을 이용해 유효한 URL인지 검증하게 됩니다.

다시 DriverManager의 코드를 보겠습니다.

URL 패턴이 맞지 않는 경우, H2 Driver도 MySQL Driver도 null을 반환하고 있습니다.
따라서 null이 반환되지 않을때까지 registerdDrivers를 순회하며 URL 패턴과 일치하는 데이터베이스와의 Connection을 반환하게 됩니다.

JDK 6 이후의 방식

JDK 6 이전에는 지금과 같이 서비스 제공자 프레임워크를 직접 구현해서 개발을 진행했습니다.
JDK 6 이후에는 ServiceLoader를 Java에서 제공하게 됨으로서, Class.forName을 이용한 명시적 호출은 필요하지 않게 되었습니다.

DriverManager 내부적으로 ensureDriversInitialized 메서드를 호출하여 ServiceLoader를 이용해 Driver 클래스를 구현한 구현체들을 전부 등록하게 됩니다.


ServiceLoader에 대해서는 나중에 다시 다루도록 하겠습니다.

😊 간단하게 JDBC API를 만들어보고 동작을 이해해보기

간단하게 Connection & Driver 인터페이스를 만들고, DriverManager 클래스를 통해 등록되고 Connection을 얻어오는 기능까지 만들어보겠습니다.



Connection 인터페이스는 텅 빈 인터페이스로 만들어주었고, Driver 인터페이스는 url과 username 그리고 password를 받아 Connection을 반환하도록 작성해주었습니다.


DriverManager 클래스도 간단하게 Driver를 등록하고, url 패턴에 맞는 Connection을 반환하도록 작성했습니다.

Driver 인터페이스를 구현한 MyDriver와 Connection 인터페이스를 구현한 MyConnectionImpl 클래스를 작성하겠습니다.


url 패턴이 MyDriver와 일치해야 MyConnectionImpl 인스턴스가 반환되고 그렇지 않으면 null이 반환됩니다.


간단하게 코드를 작성했습니다. 실행해보겠습니다.

URL 패턴이 일치하면 그에 맞는 Connection을 반환해주고 그렇지 않을경우 null이 반환됩니다.

그렇다면 마지막으로 여러 벤더사가 추가되어야 하면 코드가 수정되야 하는지 확인해보겠습니다.
YourDriverYourConnectionImpl 클래스를 추가해보겠습니다.


구현부는 URL 패턴만 다르기때문에 따로 설명은 하지 않겠습니다.

간단한 실행 코드로 확인해보겠습니다.
아래와 같이 출력이 나오면 정상적으로 동작하는 코드입니다.

  1. My 드라이버 등록! 문구 출력
  2. YourDriver는 미등록 상태이므로 null 출력
  3. MyDriver는 등록 상태이므로 MyConnectionImpl의 주소값 출력
  4. Your 드라이버 등록! 문구 출력
  5. YourDriver는 등록 상태이므로 YourConnectionImpl의 주소값 출력

🤩 다음으로..!!

항상 스프링이나 데이터 엑세스를 위해 사용하던 JDBC API가 이렇게 복잡하게 구현되어 있고, 여러 고려를 통해 디자인되어 있을 줄은 몰랐던 것 같습니다.

선배 개발자님들의 철학과 수고가 담긴 내부 구현 코드를 자주 살펴보는 것도 견문을 넓혀가는데 도움이 될 거 같습니다.

오늘도 읽어주셔서 감사합니다.

🙇

참고한 레퍼런스

5개의 댓글

comment-user-thumbnail
2023년 7월 25일

이 분 완전 변태시네...
왜 JDBC를 다 벗기시죠..

2개의 답글
comment-user-thumbnail
2023년 7월 26일

이분 변태신가요? 상당하네요

1개의 답글