자바 애플리케이션 서버와 DB 서버와의 연결을 위해 JDBC Driver로부터 Connection 구현체를 반환받는다. 과면 이 구현체는 무엇이며, 어떠한 연결인지 알아보고자하였다. 아래의 내용부터는 연결이 성립되는 과정을 순차적으로 작성하였다.
애플리케이션 서버와 DB 서버의 통신과 연결
JDBC Connection
Java 애플리케이션에서 여러 DB간의 접근을 위하여, JDBC 라이브러리는 Driver 인터페이스를 제공한다. 그리고 DB 벤더사들은 Driver 구현체를 라이브러리에 내포시켜 제공한다.
다음은 H2 라이브러리의 Driver 구현체 코드이다.
package org.h2;
public class Driver implements java.sql.Driver, JdbcDriverBackwardsCompat {
private static final Driver INSTANCE = new Driver();
private static final String DEFAULT_URL = "jdbc:default:connection";
private static final ThreadLocal<Connection> DEFAULT_CONNECTION = new ThreadLocal();
private static boolean registered;
public Driver() {
}
public Connection connect(String var1, Properties var2) throws SQLException {
if (var1 == null) {
throw DbException.getJdbcSQLException(90046, (Throwable)null, new String[]{"jdbc:h2:{ {.|mem:}[name] | [file:]fileName | {tcp|ssl}:[//]server[:port][,server2[:port]]/name }[;key=value...]", null});
} else if (var1.startsWith("jdbc:h2:")) {
return new JdbcConnection(var1, var2, (String)null, (Object)null, false);
} else {
return var1.equals("jdbc:default:connection") ? (Connection)DEFAULT_CONNECTION.get() : null;
}
}
}
Java 진영에서는 자바로 개발한 애플리케이션과 데이터베이스 서버와 연결하기 위한 JDBC(Java Database Connectivity)이란 고수준 API가 존재한다. JDBC에는 특정 DB의 Driver 인터페이스가 정의되어 있으며, 각 DB 벤더사에서는 Driver 구현체를 제공한다.
Driver 구현체로 데이터베이스 서버와 연결(connect)을 시도하는 것을 알 수 있다. 이 연결은 TCP/IP 통신을 통한 클라이언트와 서버간의 연결이다. TCP/IP 통신은 인터넷 상에서 IP로 호스트를 찾고, TCP 프로토콜을 통해서 호스트 내에서 Port로 여러 프로세스중 특정 프로세스를 식별하여 통신을 수행하는 것이다.
TCP Connection
TCP/IP 통신은 소켓을 통해 클라이언트의 프로세스와 서버의 프로세스간에 데이터를 주고받는다. 소켓은 클라이언트 프로세스와 서버 프로세스를 통신을 위한 TCP 계층의 추상화된 인터페이스이다. 그리하여 TCP 소켓이라 부르며, 프로세스(User Sapce)와 OS의 네트워크 스택(Kernel Space)간에 데이터 입출력을 위해 Transport 계층과 Application 계층사이에 소켓이 존재한다.

이로써 TCP 소켓을 통한 클라이언트와 서버간의 연결을 TCP 커넥션이라고 한다. 자바 애플케이션 서버(클라이언트)와 DB 서버는 TCP 커넥션(TCP/IP 커넥션)을 통해 연결된 것이다.
DB 서버와의 TCP/IP 통신의 필요성
DB 서버의 트랜잭션
그렇다면 자바 애플리케이션 서버(클라이언트)와 DB 서버가 HTTP 통신을 사용하지 않고 TCP/IP 통신을 통하여 TCP 커넥션을 하는 이유는 뭘까?
데이터베이스는 트랜잭션을 수행하기 때문에, 트랜잭션이 진행되는 동안 클라이언트 프로세스와 서버 프로세스 간에 상태 정보는 유지되어야한다.
트랜잭션은 여러 SQL 문을 하나로 묶은 논리적인 단위로 작업을 끝마치기 때문이다. 예를 들어, 은행 계좌 이체 작업을 생각해본다면, 계좌 A에서 계좌 B로 돈을 이체하는 과정은 다음과 같은 작업이 필요하다.
- 계좌 A에서 금액을 인출한다.
- 계좌 B에 금액을 입금한다.
이 두 작업은 하나의 트랜잭션으로 묶여야 한다. 트랜잭션의 각 단계가 성공적으로 완료되어야 전체 트랜잭션이 성공한 것으로 간주되어야 거래가 완료된 것이기 때문이다. 만약 중간에 실패가 발생하면, 트랜잭션은 전체적으로 롤백(rollback)되어 계좌 A와 계좌 B의 금액은 원래 상태로 돌아가야 한다. 성공했다면, 커밋(commit)을 하여 DB에 변경사항을 반영해야된다.
여러 SQL 문을 트랜잭션으로 묶는 경우, 클라이언트는 각 SQL 문을 개별적으로 데이터베이스 서버로 전송한다.트랜잭션의 개념은 여러 SQL 문들이 하나의 논리적인 작업 단위로 처리된다는 것을 의미한다. 그리하여 트랜잭션이 시작되면, 해당 트랜잭션이 끝날 때까지(커밋되거나 롤백될 때까지) 같은 연결(동일한 Connecton) 을 사용해야 한다.
// 데이터베이스 연결 설정
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");
// 트랜잭션 시작
conn.setAutoCommit(false);
try {
// 첫 번째 SQL 문 실행
Statement stmt1 = conn.createStatement();
stmt1.executeUpdate("INSERT INTO mytable (column1) VALUES ('value1')");
// 두 번째 SQL 문 실행
Statement stmt2 = conn.createStatement();
stmt2.executeUpdate("UPDATE mytable SET column1 = 'value2' WHERE column1 = 'value1'");
// 트랜잭션 커밋
conn.commit();
} catch (SQLException e) {
// 트랜잭션 롤백
conn.rollback();
e.printStackTrace();
} finally {
// 연결 종료
conn.close();
}
트랜잭션 경계 내의 모든 SQL 명령을 실행하고 이 명령들이 성공적으로 완료되면, 커밋하여 변경 사항을 영구적으로 저장하거나 오류가 발생하면 롤백하여 모든 변경 사항을 취소한다.
- 트랜잭션 시작: conn.setAutoCommit(false)를 호출하여 자동 커밋 모드를 비활성화한다. 이로 인해 명시적으로 commit()을 호출하기 전까지는 모든 SQL 연산이 트랜잭션 경계 내에서 실행된다.
- 데이터베이스 연산 수행
- 계좌 A에서 금액을 인출하는 UPDATE SQL 문을 실행한다.
- 계좌 B에 금액을 입금하는 UPDATE SQL 문을 실행한다.
- 트랜잭션 커밋: 모든 연산이 성공적으로 완료되면 conn.commit()을 호출하여 트랜잭션을 커밋한다.
- 트랜잭션 롤백: 예외가 발생하면 conn.rollback()을 호출하여 트랜잭션을 롤백한다.
- 자원 해제: Connection 객체를 닫아서 DB와의 연결을 끊고 연결에 사용하던 자원을 반환한다.
위와 같이 스프링 부트 애플리케이션에서도 트랜잭션을 수행할때, 결국 Connection 구현체를 사용하여 트랜잭션을 관리한다.
TCP: 연결 지향형(Connection-Oriented) 프로토콜
TCP 프로토콜은 연결 지향형 프로토콜이기 때문에 TCP 커넥션은 클라이언트와 서버간에 연결 상태를 유지할 수 있다. TCP 커넥션은 TCP 3 Way-Handshake를 통해 연결을 설정하며, 연결이 성립되면 두 엔드포인트(클라언트 프로세스와 서버 프로세스) 간의 연결 상태를 유지한다. 그리고 연결을 종료할때는 TCP 4 Way-Handshake를 통해 두 엔드포인트간의 연결을 끊는다.
이처럼 TCP 커넥션은 연결 상태를 유지(stateful session)한다. 이러한 TCP 커넥션을 통해서 클라이언트(애플리케이션 서버)와 DB 서버가 연결하므로, 트랜잭션이 진행되는 동안에 클라이언트와 DB 서버의 프로세스간의 트랜잭션 상태 정보는 유지될수있는 것이다.
TCP 소켓
소켓의 본질
TCP 커넥션에는 두 엔드 포인트(클라이언트 프로세스와 서버 프로세스)의 파이프라인이 TCP 소켓이다. 이 소켓을 여는 주체는 프로세스이며, 프로세스가 파일 디스크립터(File Descriptor)로 소켓을 참조한다.
OS에서는 프로세스마다 사용중인 파일을 관리하기 위하여, 커널 영역에 프로세스 테이블 내에 파일 디스크립터 테이블을 생성한다. 이 파일 디스크립터 테이블 내의 파일 디스크립터로 open한 파일에 대한 메타데이터를 참조한다. 즉, 프로세스가 파일 디스크립터를 통해 소켓을 참조하므로 소켓은 파일이다. 파일 참조에 관한 자세한 내용은 Linux의 IO 자원 관리에 설명되어있다.
C언어에서는 소켓을 생성하면 파일 디스크립터의 고유 번호를 반환하여 소켓을 참조할수 있다. 반면, Java는 java.net.Socket 패키지를 가져다가 객체 지향적인 접근 방식을 사용하여 소켓 객체를 사용한다.
그러나 결국에 리눅스에서 실행되는 자바 애플리케이션에서 소켓 통신을 하기 위해서 JNI(Java Native Interface)을 통해 리눅스의 표준 C 라이브러리를 사용하여 소켓 함수(socket(), bind(), listen(), accept(), send(), recv())를 호출하여 실제 소켓 기능을 제공받는다.
이처럼 결국, 소켓은 파일 디스크립터의 고유 번호를 통해 식별되며, 파일 디스크립터가 파일 테이블 엔트리를 가리키고 파일 테이블은 소켓 구조체를 참조한다. 지금까지 언급한 파일 디스크립터, 파일 테이블, 소켓 구조체는 모두 I/O 자원이므로 OS가 메모리의 커널영역에 저장된다.
리눅스의 소켓 구조체에는 다음과 같은 정보 등이 저장되어있다. (https://github.com/torvalds/linux/blob/master/include/net/sock.h)
// include/net/sock.h
struct sock {
// ... (다른 필드들)
#define sk_proto __sk_common.skc_proto
#define sk_state __sk_common.skc_state
#define sk_dport __sk_common.skc_dport
#define sk_daddr __sk_common.skc_daddr
#define sk_num __sk_common.skc_num
#define sk_rcv_saddr __sk_common.skc_rcv_saddr
// 소켓 타입 관련
struct proto *sk_prot;
// 수신 관련
struct sk_buff_head sk_receive_queue;
int sk_rcvbuf;
atomic_t sk_rmem_alloc;
int sk_rcvlowat;
// 송신 관련
struct sk_buff_head sk_write_queue;
int sk_sndbuf;
atomic_t sk_wmem_alloc;
// ... (다른 필드들)
};
- 소켓 타입(sk_prot): TCP, UDP 등 소켓의 유형
- 소켓 상태(sk_state): 연결됨(ESTABLISHED), 수신 대기(LISTEN) 등 소켓의 현재 상태
- 네트워크 주소 정보 (Local 및 Remote IP 주소와 포트)
- sk_daddr: 목적지 주소
- sk_rcv_saddr: 소스 주소
- sk_dport: 목적지 포트
- sk_num: 소스 포트
- 버퍼 정보:
- sk_receive_queue: 소켓 수신 버퍼
- sk_write_queue : 소켓 송신 버퍼
- sk_sndbuf: 소켓 송신 버퍼 크기
- sk_rcvbuf: 수신 버퍼 크기
소켓과 연결된 TCP 송수신 버퍼 (TCP 소켓 버퍼)
위의 소켓의 정보에서 TCP 송신 버퍼와 수신 버퍼 주소 존재하는 것을 알수 있다. TCP 커넥션은 소켓(파일) 자체에 데이터가 입출력이 되는것이 아니라, 소켓에 연결된 TCP 송신 버퍼와 TCP 수신 버퍼에 데이터의 입출력이 발생하는 것이다. TCP 버퍼를 TCP 소켓 버퍼라고도 한다.
버퍼(Buffer)는 컴퓨터 장치들 사이에서나 네트워크 계층간에서 데이터(data)를 주고받을 때, 각 장치나 계층에서 데이터 처리 속도 차이를 극복하기 위해서 임시로 데이터를 대기시키기 위한 저장공간이다. 먼저 들어온 데이터 부터 보내야하므로 큐(queue) 와 같이 FIFO(선입선출) 방식을 따르지만, 기본적으로는 배열의 형태로 구성된 메모리 저장 공간이다.
프로세스는 파일 디스크립터 번호를 통해서 생성한 소켓(파일)을 참조하면, 소켓에 저장된 TCP 송수신 버퍼의 주소가 저장되어 있다. 이 주소를 참조하여 커널영역의 TCP 송수신 버퍼의 데이터를 프로세스로 받아오거나 다른 호스트의 프로세스로 데이터를 보내거나한다.
TCP 헤더와 Application의 데이터를 하나로 합치면 세크먼트(Segment) 단위로 목적지 호스트의 TCP 계층까지 도달한다. TCP 계층에 도착하면 TCP 헤더가 처리되고 데이터는 TCP 수신 버퍼(receive buffer)에 임시 저장된다. 프로세스는 이 버퍼로부터 데이터의 스트림(stream)을 읽는다.

스트림은 데이터의 연속적인 흐름이다. TCP 버퍼에 데이터를 추가(append)하고, 이를 바이트(byte) 단위로 전송 및 전달하여 스트림을 처리한다.
다음 그림은 TCP 소켓을 통한 프로세스와 네트워크 스택간에 스트림 처리이다.

프로세스에서 소켓을 생성하면 소켓이 커널 영역에 생성되며 파일 디스크립터의 고유 번호를 반환한다. 또한 TCP Send Buffer와 TCP Recieve Buffer가 할당된다. 이 버퍼들은 메모리의 커널 영역에 할당되며, 네트워크에서 송신하거나 수신할 스트림을 임시로 저장한다.
- 프로세스에서 소켓의 파일 디스크립터의 고유 번호로recv()을 호출 해서 TCP Recieve Buffer에 저장되어 있는 스트림을 프로세스의 Recieve Buffer로 받아올수 있다.
- 프로세스에서 send()를 호출하여 프로세스의 Send Buffer에 저장된 스트림을 TCP Send Buffer로 보낼수 있다.
recv()와 send()는 모두 소켓 혹은 파일 I/O 자원을 사용하므로 시스템 호출(System Call)이다. 즉, 운영체제에서 스트림을 처리하는 것이다.
아래 내용은 위의 그림에서 Driver와 NIC의 역할이다.
- NIC(Network Interface Controller): 컴퓨터가 네트워크에 연결할 수 있게 해주는 인터페이스 (아날로그 신호와 컴퓨터가 이해할 수 있는 0과 1로된 디지털 신호간에 양방향 변환 수행)
- Network Driver: NIC로부터 받은 신호를 인터럽트로 핸들링하여 운영체제로 전달하거나 그 반대로 수행
클라이언트와 DB 서버의 소켓 프로그래밍
클라이언트 소켓과 서버 소켓의 실행 흐름
TCP 커넥션은 소켓 프로그래밍을 통해 구현된다. 그러므로 DB 벤더사의 Driver 구현체의 로직에서 소켓 프로그래밍에 대한 코드가 있어야한다. 또한 DB 벤더사의 서버 측 코드에서도 소켓 프로그래밍을 해야한다. 소켓을 생성하고, 통신하고, 소켓을 없애는 과정을 소켓 프로그래밍이라 한다. 아래의 그림은 클라이언트 소켓(Client Socket)과 서버 소켓(Server Socket)의 실행 흐름이다.

위의 socket, bind, listen, accept, recv, send, close 은 모두 입출력 자원(소켓)을 사용하는 시스템 호출(System Call)이다.
서버는 클라이언트의 연결 요청을 대기하고 수락할 준비가 된 소켓을 생성하기 위해서, 하나의 소켓을 기본적으로 생성해 놓는다.
- socket(): 속성들이 비어있는 새로운 소켓을 생성한다.
- bind(): 1번에서 가져온 정보들을 소켓에게 ip와 port를 바인딩한다.
- listen(): 소켓을 LISTEN 상태로 만들어 클라이언트의 연결 요청을 대기하도록한다. (Welcome Socket이라고도 부른다.)
- accept(): 클라이언트가 서버로 연결 요청(connect()) 한 것을 수락하고 새로운 소켓을 생성하여 연결하도록한다. 1번에서 생성한 소켓은 연결 요청 대기용 소켓이며, accept()호출시 실제 클라이언트와 연결한 소켓을 생성한다.
- send(),recieve: 클라이언트와 데이터를 송수신한다.
- close(): 소켓을 닫고 커널영역에 할당된 I/O 자원을 해제한다.
아래부터는 실제로 데이터베이스 서버와 클라이언트의 TCP 커넥션을 위해서 소켓 프로그래밍을 했는지 간략하게 확인해볼것이다. 데이터베이스는 간단하게 H2 클라이언트와 서버의 코드를 살펴보았다.
JDBC은 데이터베이스 커넥션 라이브러리이자 고수준 API로서, 내부적으로 소켓 프로그래밍을하여 데이터베이스 서버와 연결과 통신 할수 있도록 구현되어 있었다. 이로써 개발자는 소켓 프로그래밍의 복잡성을 다루지 않고도 쉽게 데이터베이스와 연결과 통신할 수 있게된다.
클라이언트 측 소켓 프로그래밍
클라이언트는 DB 서버와 통신 통로를 생성하기 위해서 소켓을 생성하고 초기화한다. 그리고 데이테베이스 서버로 TCP/IP 연결 시도를 한다.
위의 코드에서 Connection 구현체인 JdbcConnection 클래스를 따라가다 보면 SessionRemote 클래스가 나온다. 이 클래스는 클라이언트 측에서 H2 데이터베이스 원격 서버(Remote DB Server)와 연결하여 DB 세션을 성립하고 관리하는 클래스이다. 즉, 소켓 프로그래밍을 하였을 것이다.
package org.h2.engine;
public final class SessionRemote extends Session implements DataHandler {
public SessionRemote(ConnectionInfo var1) {
this.connectionInfo = var1;
this.oldInformationSchema = var1.getProperty("OLD_INFORMATION_SCHEMA", false);
}
private Transfer initTransfer(ConnectionInfo var1, String var2, String var3) throws IOException {
// 소켓 생성
Socket var4 = NetUtils.createSocket(var3, 9092, var1.isSSL(), var1.getProperty("NETWORK_TIMEOUT", 0));
Transfer var5 = new Transfer(this, var4);
var5.setSSL(var1.isSSL());
var5.init();
var5.writeInt(17);
var5.writeInt(20);
var5.writeString(var2);
var5.writeString(var1.getOriginalURL());
var5.writeString(var1.getUserName());
var5.writeBytes(var1.getUserPasswordHash());
var5.writeBytes(var1.getFilePasswordHash());
String[] var6 = var1.getKeys();
var5.writeInt(var6.length);
String[] var7 = var6;
int var8 = var6.length;
for(int var9 = 0; var9 < var8; ++var9) {
String var10 = var7[var9];
var5.writeString(var10).writeString(var1.getProperty(var10));
}
try {
this.done(var5);
this.clientVersion = var5.readInt();
var5.setVersion(this.clientVersion);
if (var1.getFileEncryptionKey() != null) {
var5.writeBytes(var1.getFileEncryptionKey());
}
var5.writeInt(12);
var5.writeString(this.sessionId);
if (this.clientVersion >= 20) {
TimeZoneProvider var12 = var1.getTimeZone();
if (var12 == null) {
var12 = DateTimeUtils.getTimeZone();
}
var5.writeString(var12.getId());
}
this.done(var5);
this.autoCommit = var5.readBoolean();
return var5;
} catch (DbException var11) {
var5.close();
throw var11;
}
}
}
TCP/IP 소켓 생성 및 초기화로 TCP 커넥션이 연결되었으면, DB 접속 정보 전송을 하고 DB 서버가 인증을 수행한다.
- TCP/IP 소켓 생성 및 초기화
- Socket socket = NetUtils.createSocket(var3, 9092, var1.isSSL(), var1.getProperty("NETWORK_TIMEOUT", 0));
- Transfer transfer = new Transfer(this, socket);
- transfer.init();
- DB 접속 정보 전송
- transfer.writeString(var1.getOriginalURL())
- transfer.writeString(var1.getOriginalURL())
- tranfer.writeBytes(var1.getUserPasswordHash())
DB 서버측 소켓 프로그래밍
데이터베이스 서버는 클라이언트의 연결 요청을 수락하면, DB 프로세스(H2 혹은 MySQL 등)과 java 프로세스(클라이언트)와의 통신을 위한 새로운 소켓을 생성한다. 소켓 생성 이후에 클라이언트와 서버간의 TCP 3 Way-Handshake을 성공적으로 마치면 TCP 커넥션이 성립된다.
다음 코드는 H2 데이터베이스 서버에서 클라이언트의 TCP 연결 요청을 처리하는 TcpServer 클래스의 일부 코드이다.
package org.h2.server;
public class TcpServer implements Service {
public synchronized void start() throws SQLException {
this.stop = false;
try {
this.serverSocket = NetUtils.createServerSocket(this.port, this.ssl);
} catch (DbException var2) {
if (this.portIsSet) {
throw var2;
}
this.serverSocket = NetUtils.createServerSocket(0, this.ssl);
}
this.port = this.serverSocket.getLocalPort();
this.initManagementDb();
}
public void listen() {
this.listenerThread = Thread.currentThread();
String var1 = this.listenerThread.getName();
try {
while(!this.stop) {
Socket var2 = this.serverSocket.accept();
Utils10.setTcpQuickack(var2, true);
int var3 = this.nextThreadId++;
TcpServerThread var4 = new TcpServerThread(var2, this, var3);
this.running.add(var4);
Thread var5;
if (this.virtualThreads) {
var5 = Utils21.newVirtualThread(var4);
} else {
var5 = new Thread(var4);
var5.setDaemon(this.isDaemon);
}
var5.setName(var1 + " thread-" + var3);
var4.setThread(var5);
var5.start();
}
this.serverSocket = NetUtils.closeSilently(this.serverSocket);
} catch (Exception var6) {
if (!this.stop) {
DbException.traceThrowable(var6);
}
}
this.stopManagementDb();
}
}
- start 메서드는 서버 소켓을 열고 포트를 설정하며 관리 DB를 초기화한다.
- listen 메서드는 클라이언트 연결을 수신(accept)하고, 각 연결마다 새로운 스레드(TcpServerThread)를 생성하여 처리한다. 클라이언트 연결 수신 루프가 종료되면 서버 소켓을 닫는다.
- TcpServerThread 클래스의 run메서드에서 클라이언트의 인증을 수행하는 코드가 작성되어 있다.
이로써, TCP/IP 소켓 생성 및 초기화로 TCP 커넥션이 연결되었다. 이후, DB 접속 정보 전송을 하고 인증에 성공하면, DB 서버는 클라이언트에 대한 DB 세션을 서버 내에 생성하게 된다.
DB 세션
DB 세션은 TCP 세션 기반하여 상위 수준에서 동작한다. TCP 세션은 TCP 연결 상태를 유지 기간이며, DB 세션은 그 위에서 데이터베이스와 관련된 상태를 관리한다.
다음은 H2 데이터베이스의 DB 서버 세션 클래스 SessionLocal의 코드이다. SessionLocal 클래스는 데이터베이스 서버가 클라이언트(Local DB)와 연결된 세션을 처리하는 클래스이다. 이 클래스는 H2 데이터베이스의 내장형 모드에서 주로 사용되지만, 서버 모드에서도 Local DB와의 세션을 처리하는 데 사용된다.
package org.h2.engine;
public final class SessionLocal extends Session {
private Database database;
private User user;
private final ArrayList<Table> locks = new ArrayList<>();
private boolean autoCommit = true;
private HashMap<String, Value> variables = new HashMap<>();
private HashMap<String, Savepoint> savepoints = new HashMap<>();
private SmallLRUCache<String, Command> queryCache;
private final AtomicReference<State> state = new AtomicReference<>(State.INIT);
public SessionLocal(Database database, User user, int id) {
this.database = database;
this.user = user;
this.queryCache = SmallLRUCache.newInstance(128);
}
// 트랜잭션 관리
public void commit() {
if (autoCommit) {
// Commit logic here
}
}
public void rollback() {
// Rollback logic here
}
// 잠금 관리
public void registerTableAsLocked(Table table) {
locks.add(table);
}
public void unlock(Table table) {
locks.remove(table);
}
// 저장점 관리
public Savepoint setSavepoint() {
Savepoint sp = new Savepoint();
savepoints.put(sp.name, sp);
return sp;
}
public void rollbackToSavepoint(String name) {
Savepoint sp = savepoints.get(name);
if (sp != null) {
// Rollback to savepoint logic here
}
}
// 쿼리 준비 및 실행
public Prepared prepare(String sql) {
Parser parser = new Parser(this);
return parser.prepare(sql);
}
// 세션 종료
public void close() {
if (state.getAndSet(State.CLOSED) != State.CLOSED) {
if (queryCache != null) {
queryCache.clear();
}
unlockAll();
// Clear temp tables logic here
database.removeSession(this);
}
}
private void unlockAll() {
for (Table table : locks) {
table.unlock(this);
}
locks.clear();
}
private static class Savepoint {
final String name = "SP_" + System.nanoTime();
}
private enum State {
INIT, RUNNING, BLOCKED, SLEEP, THROTTLED, SUSPENDED, CLOSED
}
}
위의 DB 세션 코드에서 어떠한 데이터베이스와 관련 상태 관리하고, 어떤 작업을 수행하는지에 대해서 정리해놓았다.
- 트랜잭션 관리: 트랜잭션을 시작하고(commit), 롤백하는(rollback) 메서드를 제공한다.
- 락 관리: 세션 동안 잠금 테이블을 관리(registerTableAsLocked, unlock) 한다.
- 저장점 관리: 세션 내에서 저장점을 설정(setSavepoint)하고 롤백(rollbackToSavepoint)할 수 있다.
- 쿼리 준비: SQL 문을 준비(prpareLocal, prepareCommand)하는 메서드로, SQL 명령어를 파싱하거나 캐싱한다.
- 세션 종료: 캐시된 쿼리, 락 자원, 임시 테이블등 세션 내에서 참조하는 자원을 정리(close)하고 세션을 제거한다. 이로써 자원 누수를 방지히도록한다.
JDBC Connection 구현체
데이터베이스 서버는 DB 세션을 생성한 뒤에 클라이언트에 Connection 구현체를 반환한다. Connection 구현체는 애플리케이션과 데이터베이스 서버 간의 TCP 커넥션의 추상화이다. 이 커넥션 객체는 데이터베이스와의 연결을 관리하며, SQL 문을 실행하고 트랜잭션을 관리하는데 사용된다. 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 된다.
TCP 커넥션에서는 3-way-handshake울 수행하는데 시간이 많이 소요되므로, JDBC에서의 DB 연결 인터페이스인 Connection의 구현체를 생성하는데 비용이 많이 든다. 그러므로 자바 애플리케이션 서버와 DB 서버간의 연결을 미리하여서 Connection 구현체들을 미리 생성하고 커넥션 풀에 저장해 놓고, 트랜잭션이 수행할때마다 Connection 구현체를 바인딩하여 사용하면 빠르게 트랜잭션을 수행할 수 있다.
다음 코드는 java.sql 패키지의 Connection 인터페이스이다.
package java.sql;
import java.util.Properties;
import java.util.concurrent.Executor;
public interface Connection extends Wrapper, AutoCloseable {
Statement createStatement() throws SQLException;
PreparedStatement prepareStatement(String sql)
throws SQLException;
CallableStatement prepareCall(String sql) throws SQLException;
String nativeSQL(String sql) throws SQLException;
void setAutoCommit(boolean autoCommit) throws SQLException;
boolean getAutoCommit() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
boolean isClosed() throws SQLException;
int TRANSACTION_NONE = 0;
int TRANSACTION_READ_UNCOMMITTED = 1;
int TRANSACTION_READ_COMMITTED = 2;
int TRANSACTION_REPEATABLE_READ = 4;
int TRANSACTION_SERIALIZABLE = 8;
void setTransactionIsolation(int level) throws SQLException;
int getTransactionIsolation() throws SQLException;
SQLWarning getWarnings() throws SQLException;
void clearWarnings() throws SQLException;
Statement createStatement(int resultSetType, int resultSetConcurrency)
throws SQLException;
PreparedStatement prepareStatement(String sql, int resultSetType,
int resultSetConcurrency)
throws SQLException;
CallableStatement prepareCall(String sql, int resultSetType,
int resultSetConcurrency) throws SQLException
}
Connection 인터페이스 구현체는 데이터베이스 연결을 관리하며, SQL 실행, 트랜잭션 제어, 연결 상태 관리를 수행하는 것을 알 수 있다.
- SQL 실행: Statement, PreparedStatement, CallableStatement 생성 및 실행
- 트랜잭션 관리: 자동 커밋 설정(setAutoCommit), 커밋(commit), 롤백(rollback), 트랜잭션 격리 수준 설정(TRANSACTION_NONE, TRANSACTION_READ_UNCOMMITTED ...)
- 연결 관리: 연결 열기/닫기(connect, close), 상태 확인, 경고 관리
사용자의 요청에 의해 애플리케이션 서버(클라이언트)와 데이터베이스 서버가 커넥션이 생기고, 이 커넥션으로부터 세션을 통하여 SQL 실행, 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다. 이후, 사용자가 커넥션을 닫거나 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다.

두 프로세스간의 다수의 TCP 커넥션과 Connection 구현체
소켓 하나당 TCP 커넥션은 하나가 성립되며, 하나의 프로세스가 여러개의 소켓을 생성할 수 있다. TCP/IP 통신이므로 발신지 IP, 발신지 Port, 목적지 IP, 목적지 Port가 모두 같으면 TCP 커넥션은 성립될수 없다. 이 네가지 값으로 TCP 커넥션을 식별하기 때문이다. 그래서 소켓을 생성할때도 로컬 IP 주소 및 포트와 원격 IP 주소 및 포트가 필요하다.
그렇다면 호스트에 종속되는 동일한 애플리케이션 서버는 IP와 Port는 똑같으므로 DB 서버로 여러개의 연결을 생성할수 없을까? 동일한 클라이언트에서 각기 다른 Local Port를 사용할수 있도록 운영 체제에 의해 자동으로 처리해준다. 클라이언트가 새로운 소켓 연결을 생성할 때, 운영 체제는 사용 가능한 랜덤한 Locl Port를 할당한다. 이를 통해 동일한 클라이언트에서 여러 소켓 연결을 생성할 수 있다.
이후, 클라이언트에서 DB 서버와 TCP 커넥션을 맺기 위해 세그먼트를 생성되어 DB 서버로 보내진다. 이때, TCP 헤더에 소켓 생성할때 새롭게 발급받은 Local Port를 저장해 놓기 때문에, DB 서버에서도 소켓 생성과 클라이언트와의 연결이 가능하다. 이로써, DB 서버 프로세스와 클라이언트 프로세스간의 연결된 여러개의 소켓이 있어도, 어떠한 소켓으로 데이터를 주고 받을지 구분이 가능하다.
그리하여 동일한 클라이언트(애플리케이션 서버)에서 DB 서버와 연결된 여러개의 Connection 구현체를 가지고 있을 수 있다.

DB 서버에서는 다수의 클라이언트 연결을 관리하고, 각 연결에 대한 세션 정보를 유지할 수 있다. 이를 통해 클라이언트의 요청을 처리하고 결과를 반환한다.
JDBC를 통한 데이터베이스 연결 및 SQL 처리 과정
데이터베이스 연결 과정
Java 애플리케이션과 데이터베이스 서버간의 연결 과정이다.
- 애플리케이션 로직은 Driver 구현체를 통해 데이터베이스 서버와 TCP/IP 통신을 하여, TCP 커넥션을 시도한다.
- TCP 3 way-handshake을 수행 후에 TCP 커넥션이 되면, DB 드라이버는 사용자 이름, 비밀번호 등과 기타 부가정보를 DB 서버에 전달한다.
- DB 서버에서 전달받은 정보에 대해서 인증에 성공하면, DB 서버는 클라이언트와의 연결에 대한 DB 세션을 생성한다. 이 세션은 클라이언트의 트랜잭션 관리, 쿼리 준비(파싱, 캐싱), 락 관리, 세션 내 자원 정리 등을 수행한다.
- DB 서버는 DB 세션을 생성한 뒤에 클라이언트에 Connection 구현체를 반환한다.
- 클라이언트는 반환 받은 Connection 구현체를 통해 데이터베이스 연결을 관리하며, SQL 실행, 트랜잭션 제어, 연결 상태 관리를 수행한다. 이 커넥션으로부터 데이터베이스 서버의 세션을 통하여 실제로 SQL문 실행하고 트랜잭션 시작, 커밋 또는 롤백을 통해 트랜잭션을 종료한다.
SQL 전송 및 결과 처리 과정
자바 애플리케이션과 데이터베이스 서버간의 연결 이후, SQL 문을 전송하고 결과를 처리하는 과정이다.
- SQL 문 준비: Java 애플리케이션은 JDBC API를 사용하여 SQL 쿼리를 문자열 형태로 준비한다.
- SQL 전송: 애플리케이션 서버에서 Connection 구현체로 얻은 Statement 객체를 사용하여 준비된 SQL문을 실행한다. SQL 쿼리 문자열은 바이트 스트림 형태로 변환되어, 소켓을 통해 데이터베이스 서버로 바이트 단위로 송신한다.
- SQL 실행: 데이터베이스 서버는 수신된 바이트 스트림을 다시 문자열로 변환한 후, 이를 파싱하여 SQL 명령문을 SQL 엔진이 실행한다.
- 결과 반환: 데이터베이스 서버는 실행 결과를 마찬가지로 바이트 스트림 형태로 변환해서 클라이언트 애플리케이션에 반환한다.
- 결과 처리: 애플리케이션은 받은 결과를 JDBC API를 통해 수신된 바이트 스트림을 각 데이터 타입에 맞게 변환하여 ResultSet 객체로 Wrapping한다. 이후, Repository에서 ResultSet 객체 저장된 값을 도메인 객체로 변환한다.
트랜잭션 처리
Java 애플리케이션과 데이터베이스 서버간의 트랜잭션 처리 과정이다.
- SQL 실행 후에 Connection 구현체에서 commit()메서드를 호출하여, 데이터베이스로 트랜잭션 커밋을 요청한다.
- 커밋을 요청받은 데이터베이스 서버는 변경된 데이터를 디스크에 영구 저장하고 커밋 작업이 성공적으로 완료되었음을 애플리케이션 서버에 알린다. 만약 커밋 중에 문제가 발생하면 SQLException 예외를 애플리케이션 서버로 반환한다.
- 커밋이 완료되었음을 인지한 애플리케이션 서버는 다음 작업을 진행하거나, 예외를 반환받았다면 Connection 구현체의rollback()을 호출하여 트랜잭션 롤백을 요청해서 트랜잭션 동안 수행된 모든 변경 사항을 취소한다.
- 애플리케이션 서버에서 Connection 구현체의 conn.close()를 호출하여 데이터베이스 연결을 닫는다.
참고
- https://github.com/h2database/h2database
- https://github.com/torvalds/linux/blob/master/include/net/sock.h
- 컴퓨터 네트워크 하향식 접근 제 8판
- HTTP 완벽 가이드
- 김영한 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술