안녕하세요, 오늘 포스팅에서는 JDBC란 무엇이며 어떻게 사용하는 지에 대해 간략하게 포스팅을 해보려고 합니다.
애플리케이션에서 Database를 사용하는 방법
흔히 서비스를 개발하는 백엔드 엔지니어들은 애플리케이션을 개발하면서 database를 데이터 저장 수단으로 사용하게 됩니다.
database를 순수하게 사용하려면 크게 세가지의 과정이 필요한데요.
- 커넥션을 연결한다.
- SQL을 Database 서버에 전달한다.
- 결과를 응답받는다.
이를 도식화 하면 아래 그림과 같이 사용되게 됩니다.
만약 내가 개발한 서버가 Oracle DBMS를 사용한다면 Oracle의 Connection을 획득하고, Oracle sql을 전달하고, 결과를 받아서 애플리케이션 서버에서 사용하면 됩니다.
문제점
이렇게 개발되어 운영을 잘 하다가, DBMS를 MySQL로 교체를 해야하는 상황이 되었다고 가정해볼게요.
여기서 문제가 발생하는데, 각 DBMS마다 커넥션을 연결하는 방법, SQL 문법, 결과 반환 스펙이 모두 다른것입니다. 이렇게 되면 DBMS를 변경할때 거의 모든 애플리케이션 db연동 코드를 새로 작성해야 하는 상황이 됩니다.
JDBC(Java Database Connectivity)
이러한 문제를 해결하고자 자바에서는 JDBC라는 데이터 접근 기술에 관한 표준을 만들었는데요, 개발자는 JDBC 인터페이스에 의존해서 애플리케이션을 개발하고 각 DB 벤더사에서 JDBC를 구현한 특정 DBMS에 종속적인 Driver를 제공해줍니다.
JDBC는 크게 아래 세 가지 interface를 제공하는데요,
java.sql.Connection
https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html
java.sql.Statement
https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Statement.html
java.sql.ResultSet
https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html
각각의 Interface가 정의하는 행위는 위에 말씀드린 커넥션 연결, SQL 전달, 결과 반환을 정의합니다.
Example
이제 JDBC interface에 의존한 간단한 예제를 구현해보겠습니다. 해당 예제는 제 github에서 확인해보실 수 있습니다.
예제를 위해서 정말 간단한 table을 하나 정의해서 사용하겠습니다.
DDL
create table t1
(
id bigint not null
primary key,
name varchar(32) not null
);
Service
class SimpleJDBCService {
private val logger = KotlinLogging.logger {}
fun insert(id: Int, name: String) {
val sql = """
insert into t1 (id, name) values (?, ?)
""".trimIndent()
var connection: Connection? = null
var pstmt: PreparedStatement? = null
try {
connection = DBConnectionUtil.getConnection() // ... (1)
pstmt = connection.prepareStatement(sql) // ... (2)
pstmt.setInt(1, id)
pstmt.setString(2, name)
pstmt.executeUpdate()
} catch (e: SQLException) {
logger.error { "Error: ${e.message}" }
throw e
} finally {
close(connection, pstmt, null) // ... (3)
}
}
fun findById(id: Int): Pair<Int, String> {
val sql = """
select * from t1 where id = ?
""".trimIndent()
var connection: Connection? = null
var pstmt: PreparedStatement? = null
var rs: ResultSet? = null
try {
connection = DBConnectionUtil.getConnection()
pstmt = connection.prepareStatement(sql)
pstmt.setInt(1, id)
rs = pstmt.executeQuery()
return if (rs.next()) { // ... (4)
logger.info { "id:${rs.getInt("id")} - name:${rs.getString("name")}" }
Pair<Int, String>(
rs.getInt("id"),
rs.getString("name")
)
} else throw NoSuchElementException("id=$id")
} catch (e: SQLException) {
logger.error { "Error: ${e.message}" }
throw e
} finally {
close(connection, pstmt, rs)
}
}
private fun close(
con: Connection?,
pstmt: PreparedStatement?,
rs: ResultSet? = null
) {
if (rs != null) {
try {
rs.close()
} catch (e: SQLException) {
logger.error { "Error: ${e.message}" }
}
}
if (pstmt != null) {
try {
pstmt.close()
} catch (e: SQLException) {
logger.error { "Error: ${e.message}" }
}
}
if (con != null) {
try {
con.close()
} catch (e: SQLException) {
logger.error { "Error: ${e.message}" }
}
}
}
}
간단하게 삽입 조회만 작성해보았는데요, 번호가 매겨진 부분을 위주로 설명드리도록 하겠습니다.
1. Connection을 획득할 때 DriverManager로부터 Connection을 획득해서 가져오게 됩니다. 추후에 DataSource에 관한 포스팅을 진행할 때 조금 더 자세하게 설명드릴 것인데요, 우선 앞서 제가 DBMS마다 Driver 구현체를 제공해준다고 하였는데 DriverManager는 현재 classpath에 있는 모든 Driver들을 가지고 있고, 커넥션을 요청한 url pattern에 맞는 DBMS Driver를 선택하여 접속 및 Connection 객체를 반환해주게 됩니다.
class DBConnectionUtil {
companion object : KLogging() {
fun getConnection(): Connection {
try {
// Library에 있는 DriverManager를 찾아서 Connection을 가져온다.
// JDBC의 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.
val connection = DriverManager.getConnection(
ConnectionConst.URL,
ConnectionConst.USERNAME,
ConnectionConst.PASSWORD
)
logger.info { "Connected to the database successfully. connection: $connection, class: ${connection.javaClass}" }
return connection
} catch (e: SQLException) {
throw IllegalStateException(e)
}
}
}
}
2. PrepareStatement라는 Interface를 커넥션을 통해 셋팅하였는데요, 앞서 제가 JDBC의 Statement가 SQL을 전달하는 역할을 한다 말씀드렸는데 PrepareStatement는 Statement를 조금 기능적으로 확장하여 제공하는 Interface입니다. 차이점은 파라미터를 셋팅할 수 있습니다.
3. 커넥션을 비롯한 리소스 자원은 모두 사용한 뒤 해제해 주셔야 합니다. 커넥션을 사용한 뒤 해제하지 않으면 DB 서버에서 클라이언트 커넥션이 사용이 끝난 것을 모르고 계속 잡고 있겠죠...? 물론 이것도 나중에 서술할 글의 Connection Pool 개념이 들어가면 결국은 해제하지는 않고 유지하지만 지금 예제처럼 DriverManager를 통해 직접 커넥션을 획득한 경우는 해제를 해주셔야 합니다.
4. ResultSet은 현재 예제의 경우는 pk로 조회를 하였기 때문에 무조건 0건 혹은 1건이지만 쿼리에 따라서 결과가 여러건일 수도 있습니다. ResultSet Interface는 내부적으로 커서를 하나 가지고 있는데요, ResultSet.next()를 호출하면 커서를 한칸 이동하고, 데이터가 있으면 True, 없으면 False인 Boolean 값이 반환됩니다. 즉 무조건 next()가 한번은 호출이 되어야 합니다.
(만약 데이터가 다건이면 while(rs.next()) {} 형식으로 데이터를 계속 루프로 읽어 오셔야 합니다.)
이렇게 JDBC의 등장 배경과 간단하게 예제 코드를 작성해 보았는데요, 현재 2024년에 개발을 하는 저희에게는 너무나도 불편하고 비효율적인 방식입니다. JDBC가 등장한것이 1990년대로 알고있는데, 그 사이에 정말 많은 추상화된 데이터 접근 기술이 많이 나오기도 하고, JPA를 사용하시는 분들은 직접 SQL을 작성하시지 않는 케이스도 많으시리라 생각합니다.
그래도 저희가 쓰는 MyBatis, Hibernate 등등 모든 데이터 접근 기술은 추상화가 되어있을 뿐이지 내부적으로는 JDBC 인터페이스를 통해 데이터베이스를 접근하고 데이터를 가져옵니다. 따라서 한번쯤은 이해하고 가시면 좋을 것 같습니다.
감사합니다.
'Backend > Java' 카테고리의 다른 글
DriverManager 이해하고 사용하기 (2) | 2024.07.24 |
---|---|
[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 |