Oracle Database를 macOS에 Docker로 설치하는 방법에 이어서, 이번에는 Kotlin 애플리케이션에서 Oracle Database에 연결하고 데이터를 다루는 방법을 알아보겠습니다. 이 가이드에서는 JDBC를 통한 데이터베이스 연결부터 Kotlin의 특성을 활용한 효율적인 Oracle 데이터 처리 방법까지 다양한 내용을 다룹니다.
1. Oracle JDBC 드라이버 설정하기
Kotlin 프로젝트에서 Oracle Database에 연결하려면 먼저 Oracle JDBC 드라이버가 필요합니다. Oracle JDBC 드라이버는 Java로 작성되어 있어 플랫폼 독립적이므로 ARM 아키텍처(Apple Silicon)나 Intel 아키텍처에 관계없이 동일한 jar 파일을 사용할 수 있습니다.
1.1 Maven Central에서 직접 다운로드
curl -O https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc8/21.9.0.0/ojdbc8-21.9.0.0.jar
1.2 Maven 의존성 추가 (pom.xml)
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.9.0.0</version>
</dependency>
1.3 Gradle 의존성 추가 (build.gradle)
implementation 'com.oracle.database.jdbc:ojdbc8:21.9.0.0'
1.4 IntelliJ IDEA에서 라이브러리로 추가
File > Project Structure > Libraries
선택+
버튼 > “From Maven…” 선택- 검색 필드에 “com.oracle.database.jdbc:ojdbc8:21.9.0.0” 입력
- 검색 결과에서 ojdbc8 선택하고 “Download” 버튼 클릭
2. Kotlin으로 Oracle Database 연결하기
이제 Kotlin 코드에서 Oracle Database에 연결하는 기본 예제를 살펴보겠습니다.
import java.sql.Connection
import java.sql.DriverManager
class OracleExample {
companion object {
@JvmStatic
fun main(args: Array<String>) {
// Oracle JDBC 드라이버 로드
Class.forName("oracle.jdbc.driver.OracleDriver")
// Oracle 데이터베이스 연결 정보
val jdbcUrl = "jdbc:oracle:thin:@localhost:1521:XE" // SID 방식
// 또는 서비스 이름 방식: "jdbc:oracle:thin:@localhost:1521/XEPDB1"
val username = "system"
val password = "YourStrongPassword" // 컨테이너 실행 시 설정한 비밀번호
// 데이터베이스 연결
val conn: Connection = DriverManager.getConnection(jdbcUrl, username, password)
println("Oracle 데이터베이스에 연결되었습니다.")
// Statement 생성
val stmt = conn.createStatement()
try {
// 테이블 생성 (Oracle 구문)
stmt.executeUpdate("""
CREATE TABLE users (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR2(100)
)
""")
println("users 테이블이 생성되었습니다.")
} catch (e: Exception) {
println("테이블이 이미 존재합니다: ${e.message}")
}
// 데이터 삽입
stmt.executeUpdate("INSERT INTO users (name) VALUES ('홍길동')")
println("데이터가 삽입되었습니다.")
// 데이터 조회
val rs = stmt.executeQuery("SELECT * FROM users")
println("조회 결과:")
while (rs.next()) {
println("👤 id=${rs.getInt("id")}, name=${rs.getString("name")}")
}
// 리소스 해제
rs.close()
stmt.close()
conn.close()
println("연결이 종료되었습니다.")
}
}
}
3. Kotlin의 특성을 활용한 Oracle DB 접근 개선하기
Kotlin은 Java보다 간결하고 현대적인 언어적 특성을 제공합니다. 이를 활용하여 Oracle Database 접근 코드를 개선해 보겠습니다.
3.1 확장 함수를 사용한 ResultSet 처리 간소화
import java.sql.ResultSet
// ResultSet의 확장 함수 정의
fun <T> ResultSet.map(transform: (ResultSet) -> T): List<T> {
val result = mutableListOf<T>()
while (this.next()) {
result.add(transform(this))
}
return result
}
// 사용 예
val users = stmt.executeQuery("SELECT * FROM users").map { rs ->
User(rs.getInt("id"), rs.getString("name"))
}
// User 데이터 클래스
data class User(val id: Int, val name: String)
3.2 use() 함수를 통한 자원 자동 해제
Kotlin의 use()
확장 함수를 사용하면 try-with-resources 구문을 더 간결하게 작성할 수 있습니다.
fun getUsers(): List<User> {
Class.forName("oracle.jdbc.driver.OracleDriver")
DriverManager.getConnection(jdbcUrl, username, password).use { conn ->
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT * FROM users").use { rs ->
return rs.map { User(it.getInt("id"), it.getString("name")) }
}
}
}
}
3.3 코루틴을 활용한 비동기 데이터베이스 접근
Kotlin 코루틴을 사용하여 데이터베이스 작업을 비동기적으로 처리할 수 있습니다.
import kotlinx.coroutines.*
suspend fun getUsersAsync(): List<User> = withContext(Dispatchers.IO) {
Class.forName("oracle.jdbc.driver.OracleDriver")
DriverManager.getConnection(jdbcUrl, username, password).use { conn ->
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT * FROM users").use { rs ->
rs.map { User(it.getInt("id"), it.getString("name")) }
}
}
}
}
// 사용 예
fun main() = runBlocking {
val users = getUsersAsync()
users.forEach { println(it) }
}
4. Oracle 특화 기능 활용하기
Oracle Database는 다른 데이터베이스 시스템과 차별화된 기능을 제공합니다. Kotlin과 함께 이러한 기능을 활용하는 방법을 알아보겠습니다.
4.1 Oracle 시퀀스 사용하기
// 시퀀스 생성
stmt.executeUpdate("CREATE SEQUENCE user_seq START WITH 1 INCREMENT BY 1")
// 시퀀스 값 가져오기
val rs = stmt.executeQuery("SELECT user_seq.NEXTVAL FROM DUAL")
if (rs.next()) {
val nextId = rs.getInt(1)
println("다음 ID: $nextId")
}
4.2 Oracle의 날짜 및 시간 다루기
// 현재 날짜와 시간 삽입
stmt.executeUpdate("""
INSERT INTO events (title, event_date)
VALUES ('새 이벤트', SYSDATE)
""")
// 날짜 형식 지정하여 조회
val rs = stmt.executeQuery("""
SELECT
event_id,
title,
TO_CHAR(event_date, 'YYYY-MM-DD HH24:MI:SS') as formatted_date
FROM events
""")
while (rs.next()) {
println("이벤트: ${rs.getString("title")}, 날짜: ${rs.getString("formatted_date")}")
}
4.3 PreparedStatement 사용하여 SQL 인젝션 방지
conn.prepareStatement("INSERT INTO users (name, email) VALUES (?, ?)").use { pstmt ->
pstmt.setString(1, "김철수")
pstmt.setString(2, "chulsoo@example.com")
val rowsAffected = pstmt.executeUpdate()
println("$rowsAffected 행이 삽입되었습니다.")
}
5. 실전 예제: 간단한 사용자 관리 시스템
이제 지금까지 배운 내용을 종합하여 간단한 사용자 관리 시스템을 구현해 보겠습니다.
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.time.LocalDateTime
// 사용자 데이터 클래스
data class User(
val id: Int? = null,
val username: String,
val email: String,
val createdAt: LocalDateTime? = null
)
// 데이터베이스 연결 관리 클래스
class DatabaseManager(
private val jdbcUrl: String,
private val dbUser: String,
private val dbPassword: String
) {
init {
Class.forName("oracle.jdbc.driver.OracleDriver")
initDatabase()
}
private fun getConnection(): Connection {
return DriverManager.getConnection(jdbcUrl, dbUser, dbPassword)
}
private fun initDatabase() {
getConnection().use { conn ->
conn.createStatement().use { stmt ->
try {
stmt.executeUpdate("""
CREATE TABLE users (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR2(50) NOT NULL UNIQUE,
email VARCHAR2(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
println("사용자 테이블이 생성되었습니다.")
} catch (e: Exception) {
println("테이블이 이미 존재합니다: ${e.message}")
}
}
}
}
// 사용자 추가
fun addUser(user: User): User {
getConnection().use { conn ->
conn.prepareStatement(
"INSERT INTO users (username, email) VALUES (?, ?)",
arrayOf("id")
).use { pstmt ->
pstmt.setString(1, user.username)
pstmt.setString(2, user.email)
pstmt.executeUpdate()
// 생성된 ID 가져오기
val generatedKeys = pstmt.generatedKeys
if (generatedKeys.next()) {
val id = generatedKeys.getInt(1)
return getUserById(id) ?: user.copy(id = id)
}
}
}
return user
}
// ID로 사용자 조회
fun getUserById(id: Int): User? {
getConnection().use { conn ->
conn.prepareStatement("SELECT * FROM users WHERE id = ?").use { pstmt ->
pstmt.setInt(1, id)
pstmt.executeQuery().use { rs ->
if (rs.next()) {
return mapResultSetToUser(rs)
}
}
}
}
return null
}
// 모든 사용자 조회
fun getAllUsers(): List<User> {
getConnection().use { conn ->
conn.createStatement().use { stmt ->
stmt.executeQuery("SELECT * FROM users ORDER BY id").use { rs ->
val users = mutableListOf<User>()
while (rs.next()) {
users.add(mapResultSetToUser(rs))
}
return users
}
}
}
}
// 사용자 업데이트
fun updateUser(user: User): Boolean {
if (user.id == null) return false
getConnection().use { conn ->
conn.prepareStatement(
"UPDATE users SET username = ?, email = ? WHERE id = ?"
).use { pstmt ->
pstmt.setString(1, user.username)
pstmt.setString(2, user.email)
pstmt.setInt(3, user.id)
return pstmt.executeUpdate() > 0
}
}
}
// 사용자 삭제
fun deleteUser(id: Int): Boolean {
getConnection().use { conn ->
conn.prepareStatement("DELETE FROM users WHERE id = ?").use { pstmt ->
pstmt.setInt(1, id)
return pstmt.executeUpdate() > 0
}
}
}
// ResultSet을 User 객체로 매핑
private fun mapResultSetToUser(rs: ResultSet): User {
return User(
id = rs.getInt("id"),
username = rs.getString("username"),
email = rs.getString("email"),
createdAt = rs.getTimestamp("created_at")?.toLocalDateTime()
)
}
}
fun main() {
val dbManager = DatabaseManager(
jdbcUrl = "jdbc:oracle:thin:@localhost:1521/XEPDB1",
dbUser = "system",
dbPassword = "YourStrongPassword"
)
// 사용자 추가
val newUser = dbManager.addUser(User(username = "jdoe", email = "john.doe@example.com"))
println("새 사용자 추가됨: $newUser")
// 모든 사용자 조회
val allUsers = dbManager.getAllUsers()
println("모든 사용자:")
allUsers.forEach { println(it) }
// 사용자 업데이트
if (newUser.id != null) {
val updatedUser = newUser.copy(email = "john.updated@example.com")
val success = dbManager.updateUser(updatedUser)
println("사용자 업데이트 ${if (success) "성공" else "실패"}")
// 업데이트된 사용자 조회
val retrievedUser = dbManager.getUserById(newUser.id)
println("업데이트된 사용자: $retrievedUser")
}
}
6. 성능 최적화 및 모범 사례
Oracle Database와 Kotlin을 함께 사용할 때 성능을 최적화하고 안정적인 애플리케이션을 구축하기 위한 몇 가지 모범 사례를 알아보겠습니다.
6.1 커넥션 풀링 사용하기
데이터베이스 연결을 매번 생성하고 닫는 것은 비용이 많이 듭니다. HikariCP와 같은 커넥션 풀을 사용하여 성능을 향상시킬 수 있습니다.
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import javax.sql.DataSource
class ConnectionPool {
companion object {
private val dataSource: HikariDataSource
init {
val config = HikariConfig()
config.jdbcUrl = "jdbc:oracle:thin:@localhost:1521/XEPDB1"
config.username = "system"
config.password = "YourStrongPassword"
config.addDataSourceProperty("cachePrepStmts", "true")
config.addDataSourceProperty("prepStmtCacheSize", "250")
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
config.maximumPoolSize = 10
dataSource = HikariDataSource(config)
}
fun getDataSource(): DataSource = dataSource
}
}
// 사용 예
fun executeQuery() {
ConnectionPool.getDataSource().connection.use { conn ->
// 데이터베이스 작업 수행
}
}
6.2 배치 처리로 대량 데이터 효율적으로 처리하기
대량의 데이터를 처리할 때는 배치 처리를 사용하여 성능을 향상시킬 수 있습니다.
fun insertBatchUsers(users: List<User>) {
if (users.isEmpty()) return
getConnection().use { conn ->
conn.prepareStatement("INSERT INTO users (username, email) VALUES (?, ?)").use { pstmt ->
conn.autoCommit = false
for (user in users) {
pstmt.setString(1, user.username)
pstmt.setString(2, user.email)
pstmt.addBatch()
}
val results = pstmt.executeBatch()
conn.commit()
println("${results.sum()} 사용자가 추가되었습니다.")
}
}
}
6.3 트랜잭션 관리하기
데이터 무결성을 보장하기 위해 트랜잭션을 적절히 관리해야 합니다.
fun transferMoney(fromAccount: String, toAccount: String, amount: Double): Boolean {
getConnection().use { conn ->
try {
conn.autoCommit = false
// 출금 계좌에서 차감
conn.prepareStatement("UPDATE accounts SET balance = balance - ? WHERE account_number = ?").use { pstmt ->
pstmt.setDouble(1, amount)
pstmt.setString(2, fromAccount)
pstmt.executeUpdate()
}
// 입금 계좌에 추가
conn.prepareStatement("UPDATE accounts SET balance = balance + ? WHERE account_number = ?").use { pstmt ->
pstmt.setDouble(1, amount)
pstmt.setString(2, toAccount)
pstmt.executeUpdate()
}
// 트랜잭션 성공적으로 완료
conn.commit()
return true
} catch (e: Exception) {
// 오류 발생 시 롤백
conn.rollback()
println("거래 실패: ${e.message}")
return false
}
}
}
7. 보안 모범 사례
Oracle Database와 Kotlin을 함께 사용할 때 보안을 강화하기 위한 모범 사례를 살펴보겠습니다.
7.1 비밀번호 안전하게 저장하기
데이터베이스 비밀번호와 같은 민감한 정보는 환경 변수나 암호화된 구성 파일에 저장하는 것이 좋습니다.
// 환경 변수에서 비밀번호 가져오기
val dbPassword = System.getenv("DB_PASSWORD") ?: throw IllegalStateException("DB_PASSWORD 환경 변수가 설정되지 않았습니다")
7.2 최소 권한 원칙 적용하기
애플리케이션에서는 필요한 최소한의 데이터베이스 권한만 사용해야 합니다. 시스템 계정 대신 애플리케이션 전용 계정을 만드는 것이 좋습니다.
-- Oracle에서 애플리케이션 전용 사용자 생성
CREATE USER app_user IDENTIFIED BY secure_password;
-- 필요한 권한만 부여
GRANT CREATE SESSION TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO app_user;
7.3 SQL 인젝션 방지하기
항상 PreparedStatement를 사용하여 SQL 인젝션 공격을 방지하세요.
// 잘못된 방법 (SQL 인젝션 위험)
val username = "user' OR '1'='1"
stmt.executeQuery("SELECT * FROM users WHERE username = '$username'")
// 올바른 방법
conn.prepareStatement("SELECT * FROM users WHERE username = ?").use { pstmt ->
pstmt.setString(1, username)
pstmt.executeQuery().use { rs ->
// 결과 처리
}
}
8. 응용: 간단한 RESTful API 서버 구현하기
Kotlin과 Oracle Database를 사용하여 간단한 RESTful API 서버를 구현하는 예제를 살펴보겠습니다. 여기서는 Ktor 프레임워크를 사용합니다.
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.jackson.*
import com.fasterxml.jackson.databind.*
fun main() {
val dbManager = DatabaseManager(
jdbcUrl = "jdbc:oracle:thin:@localhost:1521/XEPDB1",
dbUser = "app_user",
dbPassword = "secure_password"
)
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
}
}
install(StatusPages) {
exception<Throwable> { cause ->
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (cause.message ?: "알 수 없는 오류가 발생했습니다")))
}
}
routing {
route("/api/users") {
// 모든 사용자 가져오기
get {
call.respond(dbManager.getAllUsers())
}
// ID로 사용자 가져오기
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "유효한 ID가 필요합니다"))
return@get
}
val user = dbManager.getUserById(id)
if (user != null) {
call.respond(user)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "사용자를 찾을 수 없습니다"))
}
}
// 새 사용자 추가
post {
val user = call.receive<User>()
val newUser = dbManager.addUser(user)
call.respond(HttpStatusCode.Created, newUser)
}
// 사용자 업데이트
put("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "유효한 ID가 필요합니다"))
return@put
}
val user = call.receive<User>()
val success = dbManager.updateUser(user.copy(id = id))
if (success) {
call.respond(dbManager.getUserById(id)!!)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "사용자를 찾을 수 없습니다"))
}
}
// 사용자 삭제
delete("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "유효한 ID가 필요합니다"))
return@delete
}
val success = dbManager.deleteUser(id)
if (success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "사용자를 찾을 수 없습니다"))
}
}
}
}
}.start(wait = true)
}
9. 결론
이 가이드에서는 Docker로 설치한 Oracle Database를 Kotlin 애플리케이션과 연동하는 방법에 대해 알아보았습니다. JDBC 드라이버 설정부터 시작하여 기본적인 데이터베이스 연결, Kotlin의 특성을 활용한 코드 개선, Oracle 특화 기능 활용, 성능 최적화, 보안 모범 사례까지 다양한 주제를 다루었습니다.
Oracle Database와 Kotlin의 조합은 엔터프라이즈급 애플리케이션 개발에 강력한 도구입니다. Oracle의 안정성과 성능, Kotlin의 간결함과 현대적인 기능을 함께 활용하면 효율적이고 유지보수하기 쉬운 애플리케이션을 구축할 수 있습니다.
이 가이드가 Oracle Database와 Kotlin을 함께 사용하는 데 도움이 되길 바랍니다. 행복한 코딩 되세요! 🚀
답글 남기기