JDBC (2) - getConnection()

잡담

처음 무언가를 배울때 ‘왜?’ 라는 궁금증이 중요하다. 이러한 궁금함이 없이 학습을 하다 보면 누군가로부터, 아니면 책으로부터 주는 지식을 아무 생각 없이 그대로 받아들이게 된다.

이게 나쁘지만은 않지만 ‘왜?’ 라는 생각을 시작함으로써 더 깊고 더 넓은 지식을 향해 나아갈 수 있다고 본다.

Intro

지난번 포스팅에서는 Class.forName(java.lang.String) 을 이용하여 JDBC Driver가 어떻게 동적으로 로딩 되는지 알아봤다. 자세한 사항은 지난 포스팅을 참고하면 된다.

이번에는 동적으로 로딩된 클래스를 어떻게 오브젝트화하여 사용하는지에 대해 알아보려고 한다.

사실 나도 잘 모른다. 지금부터 같이 알아보자.

(해당 포스팅에 사용되는 DB는 CUBRID이며, 국산 오픈소스 DB이다. 필요하면 맘껏 갖다 쓰시기 바란다.)

java.sql.Connection

요 인터페이스는 JDBC Driver의 connection을 생성할 때 사용하는 인터페이스다. 낯이 많이 익을 것이다.

왜 인터페이스일까? 클래스면 바로 new Connection 해서 오브젝트 생성하면 될텐데! 라고 궁금해 하는 사람들이 있을텐데 (내가 옛날에 그랬지 ㅎ)

그건 다형성을 통해 클래스간 관심사를 분리해서 결합력을 낮추기 위해서 정도라고 생각하면 되겠다.

인터페이스는 여기까지 하고,

이놈은 인터페이스라 new 키워드를 이용해서 오브젝트를 생성하지 못한다. 그래서 우리는 java.sql.DriverManager 클래스를 이용해서 오브젝트를 생성한다.

java.sql.DriverManager

java api (8 기준)을 보면 getConnection 메소드는 총 3개가 존재한다.

1
2
3
public static Connection getConnection(String url)
public static Connection getConnection(String url, Properties info)
public static Connection getConnection(String url, String user, String password)

위 3개 메소드는 public으로 내부에 노출되어 있다. 그럼 내부적으로 사용되는 무언가가 있을 것 같은 기분이 드는데?

소스… 소스를 까보자…

1
2
3
4
5
6
public static Connection getConnection(String url)
throws SQLException {

java.util.Properties info = new java.util.Properties();
return (getConnection(url, info, Reflection.getCallerClass()));
}

이건 인자가 하나뿐이고 내부에서 getConnection(url, info, Reflection.getCallerClass()) 를 호출한다.

다른거!

1
2
3
4
5
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {

return (getConnection(url, info, Reflection.getCallerClass()));
}

인자 갯수만 늘어났지 동일한 행동을 한다. 또 다른거!

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}

return (getConnection(url, info, Reflection.getCallerClass()));
}

문자열 인자를 이용해서 Properties info를 만들고 동일한 행동을 한다.

그럼 우리가 궁금한 놈은 getConnection(url, info, Reflection.getCallerClass()) 이거다!

소스… 소스를 보자!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
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.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

전 포스트에서 본 것 같은 느낌은 기분탓이다. 메소드가 생각보다 기니깐 잘라서 한번 보자.

먼저 맨 윗부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

여기에서 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; 이부분은 JDBC 4.0 스펙을 위해 추가된 소스코드이다. 실제로 JDK 7 소스코드에서는 해당 부분이 없다.

아래 내용은 JDBC 4.0 스펙에서 발췌한 글이다.

1
2
3
■ Automatic loading of java.sql.Driver
DriverManager.getConnection has been modified to utilize the Java SE Service Provider mechanism
to automatically load JDBC Drivers. This removes the need to invoke Class.forName.

Class.forName() 호출을 안해도 자동으로 Driver 클래스를 잡아준다. (우왕 개신기)

그 다음 볼 부분은 callerCL = Thread.currentThread().getContextClassLoader(); 이 부분인데 여기에서는 ClassLoader에 대한 배경지식이 필요하다.

간략하게 설명을 하자면, 전 포스팅에서 Class.forName()을 통해 JDBC Driver를 로딩하는데, 이때 로딩되면서 static 블록이 수행된다고 했다. 이 과정에서 DriverManager.registerDriver() 메소드가 수행되고, JDBC Driver가 오브젝트로 생성된다.

여기서 로딩된 클래스를 callerCL = Thread.currentThread().getContextClassLoader(); 를 통해 가져오는 것이다. (참 쉽죠?)

자, 그 다음 부분의 소스를 보자.

1
2
3
4
5
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

파라미터로 넘어온 url을 확인한다. null이면 가차없이 SQLException을 던져버린다.

자, 그 다음 소스를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
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.getClass().getName());
}
}

맨 위에 주석으로 친절히 설명을 해주고 있다. registerdDrivers를 로드했고 connection을 만든단다. 이부분! 이부분이 우리가 찾던 부분이다!

여기서 우리가 그동안 보지 못한 클래스가 하나 등장한다. DriverInfo, 이건 뭘까? 찾아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DriverInfo {

final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}

@Override
public boolean equals(Object other) {
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
}

@Override
public int hashCode() {
return driver.hashCode();
}

@Override
public String toString() {
return ("driver[className=" + driver + "]");
}

DriverAction action() {
return da;
}
}

DriverManager에 정의되어 있는 자료구조 클래스다. getConnection에서 for-loop을 돌면서 DriverInfo의 오브젝트를 하나씩 꺼내서 확인한다.

isDriverAllowed(Driver driver, ClassLoader classLoader)는 첫 번째 파라미터 driver와 두 번째 파라미터 classLoader로부터 가져온 driver 오브젝트가 같으면 true, 다르면 false를 반환하는 메소드다.

정상적으로 같은 JDBC Driver를 로딩하고, 호출했다면 true가 반환되어 if 내 블록이 수행 될 것이다.

바로 Connection con = aDriver.driver.connect(url, info); 이부분!

이 부분은 벤더별로 specific 하게 구현된 부분이다. 왜냐하면 여기서 aDriver.driver는 특정 벤더의 JDBC Driver이기 때문이다.

우리는 오픈소스 DB CUBRID를 사용중이니 소스코드를 한번 까보자.

cubrid.jdbc.driver.CUBRIDDriver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) {
return null;
}

if (url.toLowerCase().startsWith(JDBC_DEFAULT_CONNECTION)) {
return defaultConnection();
}

Pattern pattern = Pattern.compile(URL_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
if (!matcher.find()) {
throw new CUBRIDException(CUBRIDJDBCErrorCode.invalid_url, url, null);
}

String match = matcher.group();
if (!match.equals(url)) {
throw new CUBRIDException(CUBRIDJDBCErrorCode.invalid_url, url, null);
}

String dummy;
String host = matcher.group(2);
String portString = matcher.group(3);
String db = matcher.group(4);
String user = matcher.group(5);
String pass = matcher.group(6);
String prop = matcher.group(7);
int port = default_port;

UConnection u_con;
String resolvedUrl;
ConnectionProperties connProperties;

if (host == null || host.length() == 0) {
host = default_hostname;
}

if (portString == null || portString.length() == 0) {
port = default_port;
} else {
port = Integer.parseInt(portString);
}

connProperties = new ConnectionProperties();
connProperties.setProperties(prop);

// getting informations from the Properties object
dummy = info.getProperty("user");
if (dummy != null && dummy.length() != 0) {
user = dummy;
}
dummy = info.getProperty("password");
if (dummy != null && dummy.length() != 0) {
pass = dummy;
}

if (user == null) {
user = default_user;
}
if (pass == null) {
pass = default_password;
}

resolvedUrl = "jdbc:cubrid:" + host + ":" + port + ":" + db + ":" + user + ":********:";
if (prop != null) {
resolvedUrl += prop;
}

connProperties.setProperties(info);

dummy = connProperties.getAltHosts();
if (dummy != null) {
ArrayList<String> altHostList = new ArrayList<String>();
altHostList.add(host + ":" + port);

StringTokenizer st = new StringTokenizer(dummy, ",", false);
while (st.hasMoreTokens()) {
altHostList.add(st.nextToken());
}

if (connProperties.getConnLoadBal()) {
Collections.shuffle(altHostList);
}
try {
u_con = UJCIManager.connect(altHostList, db, user, pass, resolvedUrl);
} catch (CUBRIDException e) {
throw e;
}
} else {
try {
u_con = UJCIManager.connect(host, port, db, user, pass, resolvedUrl);
} catch (CUBRIDException e) {
throw e;
}
}

u_con.setCharset(connProperties.getCharSet());
u_con.setZeroDateTimeBehavior(connProperties.getZeroDateTimeBehavior());

u_con.setConnectionProperties(connProperties);
u_con.tryConnect();
return new CUBRIDConnection(u_con, url, user);
}

이부분도 굉장히 길다. 사실상 여기까지가 JDBC Driver의 Connection을 생성하는 부분의 끝이다. 여기서부터는 특정 벤더에 종속된 구현체이기 때문에 Oracle, MySQL, MS-SQL 뭐 다 다르다. 그래도 나는 궁금하니 소스를 끝까지 추적해 보겠다. (상관 없는 분은 여기서 작별 인사를…)

처음 소스코드를 보도록 하자.

1
2
3
if (!acceptsURL(url)) {
return null;
}

아주 심플하다. url을 받아들일 수 있는 형태인지 확인한다. url의 헤더는 jdbc:cubrid로 시작하며, CUBRIDDriver.class.getName(); 를 호출하여 class의 이름을 확인한다. 만약, class가 oracle 또는 mysql이면 -oracle, -mysql을 헤더에 추가 후 :를 연결한다. 왜 이런 행동을 하냐면 CUBRID shard 기능 때문이다.

그리고 default connection이 있는데, 이부분은 Java stored procedure 를 사용하기 위해 필요하다.

자, 그럼 다음 소스코드를 보자.

1
2
3
if (url.toLowerCase().startsWith(JDBC_DEFAULT_CONNECTION)) {
return defaultConnection();
}

바로 좀 전에 말했던 default connection을 반환하는 부분이다. 그 다음!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pattern pattern = Pattern.compile(URL_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
if (!matcher.find()) {
throw new CUBRIDException(CUBRIDJDBCErrorCode.invalid_url, url, null);
}

String match = matcher.group();
if (!match.equals(url)) {
throw new CUBRIDException(CUBRIDJDBCErrorCode.invalid_url, url, null);
}

String dummy;
String host = matcher.group(2);
String portString = matcher.group(3);
String db = matcher.group(4);
String user = matcher.group(5);
String pass = matcher.group(6);
String prop = matcher.group(7);
int port = default_port;

이부분은 뭘까?! 바로 URL 패턴을 분석하여 host ip, port, DB name, user id, user password, properties 값을 추출해내는 부분이다.

자, 다음 소스 코드!

1
2
3
UConnection u_con;
String resolvedUrl;
ConnectionProperties connProperties;

변수 선언인데 아래 내용이 한 덩어리 기분이라 3줄만 보여줬다. 저 3개 변수가 마지막에 Connection 오브젝트를 생성하는데 지대한 영향을 끼칠 것이다. 자, 그럼 다음 소스!

1
2
3
4
5
6
7
8
9
if (host == null || host.length() == 0) {
host = default_hostname;
}

if (portString == null || portString.length() == 0) {
port = default_port;
} else {
port = Integer.parseInt(portString);
}

host와 portString은 위의 url pattern을 파싱해서 가져온 값들이다. default_hostname은 localhost, default_port는 30000번을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connProperties = new ConnectionProperties();
connProperties.setProperties(prop);

// getting informations from the Properties object
dummy = info.getProperty("user");
if (dummy != null && dummy.length() != 0) {
user = dummy;
}
dummy = info.getProperty("password");
if (dummy != null && dummy.length() != 0) {
pass = dummy;
}

if (user == null) {
user = default_user;
}
if (pass == null) {
pass = default_password;
}

Connection 관련 properties를 prop 변수로부터 생성한다. 이게 뭐냐면 JDBC url 뒷부분에 ? 이후로 붙는 옵션이다. charset=utf8 이라던가 timeout=1 뭐 이런 것들이다.

그 다음에 갑자기 튀어 나온 info는 무엇일까?! 시간이 좀 지나서 잊어버렸을 지 모르지만 Connection con = aDriver.driver.connect(url, info); 를 통해 DriverManager에서 넘어온 info 저놈이다.

이 info는 값이 있을 수도 있고, null 일 수도 있다. getConnection() 어떤 애를 호출 했느냐에 따라 다르니깐 말이다.

그래서 if 문을 통해 user, password가 있는지 없는지 확인한다. null이면 기본 public 사용자에 password는 ‘’ (empty string)으로 세팅한다.

자, 다음 소스코드를 주세요!

1
2
3
4
resolvedUrl = "jdbc:cubrid:" + host + ":" + port + ":" + db + ":" + user + ":********:";
if (prop != null) {
resolvedUrl += prop;
}

자, 진짜 URL을 만들었다. 최종 형태는 “jdbc:cubrid:localhost:30000:demodb:public::” 이런 형태일 것이다.

그리고 맨 뒤에 Connection 관련 properties들을 붙혀준다. 아마 이런 형태일 것이다. (만약, prop 가 null이 아니라면 말이다.)

“jdbc:cubrid:localhost:30000:demodb:public::?charset=utf8”

그 다음으로 넘어가자!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
connProperties.setProperties(info);

dummy = connProperties.getAltHosts();
if (dummy != null) {
ArrayList<String> altHostList = new ArrayList<String>();
altHostList.add(host + ":" + port);

StringTokenizer st = new StringTokenizer(dummy, ",", false);
while (st.hasMoreTokens()) {
altHostList.add(st.nextToken());
}

if (connProperties.getConnLoadBal()) {
Collections.shuffle(altHostList);
}
try {
u_con = UJCIManager.connect(altHostList, db, user, pass, resolvedUrl);
} catch (CUBRIDException e) {
throw e;
}
} else {
try {
u_con = UJCIManager.connect(host, port, db, user, pass, resolvedUrl);
} catch (CUBRIDException e) {
throw e;
}
}

먼저 connProperties.setProperties(info)를 수행하는데, 별거 없다. info에는 있어봤자 user, password 정도이거나 빈 오브젝트일 것이다.

그 다음 나오는 dummy = connProperties.getAltHosts();가 뭔지는 CUBRID에 대해 아는 사람만 알 것이다. 바로, High Availability(HA)를 위해 DB 이중화를 하기 위해 다른 노드의 접속 정보를 기입하는 프로퍼티이다. 여기에서 dummy 변수의 값이 null이 아니라면, JDBC Connection properties 들 중 altHosts라는 내용이 있다는 뜻이다. 그럼 HA 구성을 사용한다는 의미이고, failover를 위해 2개 이상의 노드 정보를 생성해야한다. 그 부분이 바로 while이 돌면서 만들어 내는 부분이다.

그 다음 라인을 보면 if (connProperties.getConnLoadBal()) { 이런 코드를 수행하는데 (메소드 명이 사실 좀 헷갈리게 되있는건 기분탓?) HA 구성에서 loadBalance를 사용할지에 대한 프로퍼티를 지정하는 부분이다. 랜덤한 순서로 연결하도록 한다고 메뉴얼에 되어 있는데 그래서 Collections.shuffles()로 섞어주나보다.

다음 나오는 오브젝트는 u_con 이다. 이게 사실 직접적으로 DB에 connection을 맺어주는 오브젝트이다. cubrid.jdbc.jci 패키지에 위치하며, 약 2처라인 좀 넘는데 바이트를 주로 다룬다. (왠지 멋지다… 개발자는 비트 바이트 이래야지)

자, 아무튼, connect 부분 소스코드를 보면 사실 별거 없다. 파라미터에 맞게 UConnection 을 생성하여 반환하면 끝.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static UConnection connect(String ip, int port, String name,
String user, String passwd, String url)
throws java.sql.SQLException {
UConnection connection;

connection = new UConnection(ip, port, name, user, passwd, url);
// connectionList.add(connection);
return connection;
}

public static UConnection connect(ArrayList<String> aConList, String name,
String user, String passwd, String url)
throws java.sql.SQLException {
UConnection connection;

connection = new UConnection(aConList, name, user, passwd, url);
// connectionList.add(connection);
return connection;
}

이제 CUBRID의 JDBC Driver를 사용하기 위한 Connection 오브젝트 생성이 완료되었다. 마지막으로 몇 가지만 더 하고 반환하자.

1
2
3
4
5
u_con.setCharset(connProperties.getCharSet());
u_con.setZeroDateTimeBehavior(connProperties.getZeroDateTimeBehavior());

u_con.setConnectionProperties(connProperties);
u_con.tryConnect();

Connection 오브젝트에 set 뭐시기를 하는거 보니 값을 설정하는 것 같다.

u_con.setCharset()은 뭐 말 안해도 알꺼 같다. 문자셋을 프로퍼티에 지정한다.

u_con.setZeroDateTimeBehavior()는 뭘까? JDBC에서는 java.sql.Date 형 오브젝트에 날짜와 시간 값이 모두 0인 값을 허용하지 않는다. default 동작은 Exception 발생이다. 까지만 알면 될 것 같다.

u_con.setConnectionProperties()는 지금껏 만든 connProperties 오브젝트를 u_con 오브젝트에 넣어주는 역할이다.

그리고, 마지막 u_con.tryConnect()는 DB에 직접적인 connect를 시도하는 메소드이다. 내부에서 이것 저것 불리다가 private void connectDB(int timeout) throws IOException, UJciException 이런 메소드가 호출되어 DB와 연결된다. (이 부분은 나중에 시간 나면 자세히 포스팅 하겠다)

마지막으로 반환!

1
return new CUBRIDConnection(u_con, url, user);

u_con은 바로 위에서 만들었고, url도 이쁘게 만들었고, user는 matcher로 뽑아왔던애가 있었다. 이 3개를 가지고 CUBRIDConnection 생성자를 호출하여 오브젝트를 생성하면 끝!

끝으로

너무 두서 없이 썼다. 나 아니면 아무도 못 알아볼지도 모른다. 너무 허접하다.

다른 사람의 소스코드를 보는건 재밌다. 그리고 놀랍다. 그리고 갈길이 너무 멀다. ^^