본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
Ch 11. 객체지향 고급 프로그래밍 - 데이터영속성관리
자바에서 데이터 영속성 다루기: 휘발성 메모리를 넘어 영원한 저장소로
아무리 잘 만든 프로그램도 한 가지 근본적인 문제에 부딪히곤 합니다. 바로 "프로그램이 종료되면 데이터가 모두 사라진다"는 것이죠. 이는 컴퓨터의 주 기억 장치인 메모리(RAM)의 특성 때문입니다. 전원이 꺼지면 메모리의 데이터는 사라지게 됩니다.
이번 포스팅에서는 이 문제를 해결하고 데이터를 영속적으로(persistently) 저장하는 방법에 대해 이야기하고자 합니다. 특히 자바에서 관계형 데이터베이스와 연동하여 데이터를 유지하는 표준 방법인 JDBC(Java Database Connectivity)를 중심으로 자세히 알아보겠습니다.
1. 데이터 영속성(Persistence)이란 무엇인가?
컴퓨터 과학에서 영속성(Persistence)은 시스템의 상태가 그것을 생성한 프로세스보다 더 오래 지속되는 특성을 의미합니다. 즉, 애플리케이션이 실행을 멈추더라도 데이터가 손실되지 않고 유지되는 것을 말합니다. 이는 실질적으로 데이터를 컴퓨터 저장 장치(하드 디스크, SSD 등)에 저장함으로써 달성됩니다.
우리가 다루는 영속성은 크게 두 가지 수준으로 나눌 수 있습니다:
- 하드웨어 수준의 영속성: 데이터를 물리적인 저장 장치에 기록하는 것을 의미합니다. (RAM vs. HDD/SSD)
- 데이터 수준의 영속성: 응용 프로그램 관점에서 데이터를 구조화하고 관리하여 지속적으로 사용할 수 있게 하는 것을 의미합니다. 주로 데이터베이스를 활용하여 구현됩니다.
대부분의 현대 애플리케이션은 사용자의 정보, 주문 내역, 상품 정보 등 방대한 양의 데이터를 다루며, 이 데이터는 프로그램이 종료된 후에도 유지되어야 합니다. 이러한 요구사항을 충족하기 위해 데이터베이스(Database)라는 것이 등장했습니다. 데이터베이스는 데이터베이스 관리 시스템(DBMS)을 사용하여 데이터를 조직적으로 수집하고 관리하는 시스템입니다.
2. 자바와 데이터베이스 연결의 다리: JDBC
자바 애플리케이션이 데이터베이스와 상호작용하기 위한 표준 API가 바로 JDBC(Java Database Connectivity)입니다. JDBC는 자바 애플리케이션이 다양한 종류의 데이터베이스에 일관된 방식으로 접근할 수 있도록 도와주는 인터페이스 집합입니다.
2.1. JDBC 드라이버의 역할
JDBC를 통해 데이터베이스에 연결하려면 각 데이터베이스 종류에 맞는 JDBC 드라이버가 필요합니다. JDBC 드라이버는 자바 애플리케이션이 데이터베이스와 상호작용할 수 있게 해주는 소프트웨어 컴포넌트입니다. 이는 특정 데이터베이스 프로토콜을 사용하여 직접 호출을 수행합니다.
우리는 JDBC 드라이버를 .jar 파일 형태로 설치하여 프로젝트의 라이브러리 경로에 추가합니다.
JAR 파일은 많은 자바 클래스 파일과 관련 메타데이터 및 리소스(텍스트, 이미지 등)를 단일 파일로 묶어 배포하는 데 사용되는 패키지 파일 형식입니다. 예를 들어, SQLite 데이터베이스를 사용한다면 sqlite-jdbc-X.X.X.jar 파일이 필요합니다.
이 그림은 자바 애플리케이션이 JDBC API를 통해 JDBC 드라이버 매니저와 소통하고, JDBC 드라이버(예: Type 4 Native-Protocol driver)를 통해 직접 데이터베이스와 통신하는 구조를 보여줍니다.
3. JDBC를 활용한 영속성 처리 단계: 우리 쇼핑몰 데이터 저장하기
이제 우리 쇼핑몰 애플리케이션의 주문 데이터를 데이터베이스에 영속적으로 저장하는 과정을 단계별로 살펴보겠습니다.
3.1. 1단계: JDBC 드라이버 로드 및 데이터베이스 연결 설정
데이터베이스 연결을 시작하기 전에, 사용할 JDBC 드라이버를 로드하고 데이터베이스에 연결해야 합니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement; // SQL 문 실행을 위한 Statement 임포트
public class DatabasePersistence {
private Connection connection; // 데이터베이스 연결 객체
public DatabasePersistence(String dbUrl) throws ClassNotFoundException, SQLException {
// 1. JDBC 드라이버 로드 (Class.forName() 사용)
// SQLite 드라이버 클래스 이름을 지정합니다.
Class.forName("org.sqlite.JDBC"); [cite: 1753]
System.out.println("JDBC 드라이버 로드 성공!");
// 2. 데이터베이스 연결 (DriverManager.getConnection() 사용)
// 지정된 데이터베이스 URL에 대한 연결을 설정합니다. [cite: 1795]
this.connection = DriverManager.getConnection(dbUrl); [cite: 1755]
System.out.println("데이터베이스 연결 성공!");
}
// 데이터베이스 연결을 닫는 메소드 (자원 해제)
public void closeConnection() throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
System.out.println("데이터베이스 연결 해제!");
}
}
// ... (이후 단계의 메소드들이 추가될 예정)
}
- Class.forName("org.sqlite.JDBC"): 이 코드는 SQLite JDBC 드라이버 클래스를 메모리에 로드합니다. 이는 외부에서 작성된 클래스의 객체를 생성하는 것과 유사합니다.
- DriverManager.getConnection(dbUrl): DriverManager는 JDBC 드라이버 세트를 관리하는 기본 서비스입니다. 이 메소드는 주어진 데이터베이스 URL(jdbc:sqlite:mydb.db와 같은 형식)에 연결을 시도합니다.
Connection 객체는 특정 데이터베이스와의 세션(연결)을 나타내며, SQL 문은 이 연결의 컨텍스트 내에서 실행되고 결과가 반환됩니다.
3.2. 2단계: 테이블 생성 (SQL 실행)
데이터를 저장할 테이블을 데이터베이스 내에 생성해야 합니다. Connection 객체로부터 Statement 객체를 생성하여 SQL 문을 실행합니다.
Statement는 정적 SQL 문을 실행하고 결과를 반환하는 데 사용되는 객체입니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class DatabasePersistence {
private Connection connection;
// ... 생성자 및 closeConnection 메소드 ...
// 테이블을 생성하는 메소드
public void createOrdersTable() throws SQLException {
// Statement 객체 생성 [cite: 1757]
Statement statement = connection.createStatement(); [cite: 1758, 1798]
// 테이블 생성 SQL 쿼리 [cite: 1764]
String sql = "CREATE TABLE IF NOT EXISTS orders (" + // IF NOT EXISTS 추가하여 테이블이 없으면 생성
"id INTEGER PRIMARY KEY AUTOINCREMENT," + [cite: 1768]
"salePrice INTEGER NOT NULL," + [cite: 1770]
"saleType INTEGER NOT NULL," + [cite: 1772]
"customer_name TEXT," + [cite: 1774]
"customer_phoneNumber TEXT," + [cite: 1775]
"customer_email TEXT," + [cite: 1777]
"customer_mileage INTEGER," + [cite: 1779]
"product_name TEXT NOT NULL," + [cite: 1780]
"product_key TEXT NOT NULL," + [cite: 1781]
"product_price INTEGER NOT NULL);"; [cite: 1782]
// SQL 실행 [cite: 1759, 1760]
statement.executeUpdate(sql); [cite: 1759] // DDL(Data Definition Language) 문 실행 [cite: 1840]
System.out.println("orders 테이블 생성 완료 또는 이미 존재함.");
statement.close(); // Statement 닫기 (자원 해제)
}
// ... (이후 단계의 메소드들이 추가될 예정)
}
3.3. 3단계: 프로그램 종료 시점에 데이터 저장하기 (persist() 메소드 구현)
이제 ShoppingContext의 주문 데이터를 데이터베이스의 orders 테이블에 저장할 차례입니다. 여러 번 효율적으로 SQL 문을 실행하기 위해 PreparedStatement를 사용하는 것이 좋습니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement; // PreparedStatement 임포트
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map; // Map 임포트 (ShoppingContext에서 orderMap을 받기 위함)
// (이전 포스팅에서 정의된 Order, Customer, Product, SaleType 클래스 필요)
// (ShoppingContext 클래스에 getOrderMap() 메소드가 필요하다고 가정)
public class DatabasePersistence {
private Connection connection;
// ... 생성자 및 closeConnection, createOrdersTable 메소드 ...
// 주문 데이터를 데이터베이스에 저장하는 메소드
public void persistOrders(Map<Integer, Order> orderMap) throws SQLException {
// 테이블 내용 초기화 (테스트 용이성을 위해)
Statement stmt = connection.createStatement();
stmt.executeUpdate("DELETE FROM orders");
stmt.close();
System.out.println("기존 orders 테이블 데이터 삭제 완료.");
// 데이터 저장 SQL 쿼리 (PreparedStatement 사용)
String sql = "INSERT INTO orders (" + [cite: 1805]
"salePrice, saleType," + // 1, 2 [cite: 1806]
"customer_name, customer_phoneNumber, customer_email, customer_mileage," + // 3, 4, 5, 6 [cite: 1807]
"product_name, product_key, product_price)" + // 7, 8, 9 [cite: 1808]
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; [cite: 1809]
// PreparedStatement 객체 생성 [cite: 1810]
PreparedStatement preparedStatement = connection.prepareStatement(sql); [cite: 1810]
int batchCount = 0; // 배치 처리 카운트
for (Order order : orderMap.values()) { // 모든 주문 순회 [cite: 1811]
preparedStatement.setInt(1, order.getSalePrice()); [cite: 1812] // salePrice
preparedStatement.setInt(2, order.getSaleType() == SaleType.DISCOUNTED ? 1 : 0); // saleType (0: NORMAL, 1: DISCOUNTED)
// Customer 데이터 [cite: 1817]
Customer customer = order.getCustomer();
preparedStatement.setString(3, customer.getName()); [cite: 1819]
preparedStatement.setString(4, customer.getNumber()); [cite: 1820]
preparedStatement.setString(5, customer.getEmail()); [cite: 1821]
preparedStatement.setInt(6, customer.getMileage()); [cite: 1821]
// Product 데이터
Product product = order.getProduct(); // Order에서 Product 가져오기 (가정)
preparedStatement.setString(7, product.getName()); [cite: 1780]
preparedStatement.setString(8, product.getKey()); [cite: 1781]
preparedStatement.setInt(9, product.getPrice()); [cite: 1782]
preparedStatement.addBatch(); // 배치에 추가
batchCount++;
// 일정 개수마다 배치 실행 (성능 최적화)
if (batchCount % 100 == 0) { // 100개마다 실행
preparedStatement.executeBatch();
System.out.println("100개 주문 배치 저장 완료.");
}
}
preparedStatement.executeBatch(); // 남은 배치 실행
System.out.println("모든 주문 데이터 저장 완료!");
preparedStatement.close(); // PreparedStatement 닫기
}
}
- PreparedStatement: 미리 컴파일된 SQL 문을 나타내는 객체로, SQL 문을 효율적으로 여러 번 실행할 수 있습니다.
setString(), setInt()와 같은 setXxx() 메소드를 사용하여 매개변수 값을 안전하게 설정할 수 있습니다. - addBatch()와 executeBatch(): 여러 개의 INSERT, UPDATE, DELETE 문을 묶어 한 번에 데이터베이스로 전송하여 성능을 향상시키는 데 사용됩니다.
- executeUpdate(): INSERT, UPDATE, DELETE와 같은 DML(Data Manipulation Language) 문이나 DDL(Data Definition Language) 문을 실행합니다.
3.4. 예외 처리 (SQLException)
JDBC를 사용한 데이터베이스 작업은 SQLException이라는 Checked Exception을 발생시킬 수 있습니다. 따라서 이러한 작업은 반드시 try-catch 블록으로 묶어 예외를 처리해야 합니다.
// ShoppingContext.java (persist 메소드 추가)
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// (ProductFactory, Order, Customer, Product, SaleType 클래스 정의 필요)
public class ShoppingContext {
private Map<String, Product> productCatalog;
private List<Customer> simulatedCustomers;
private Map<Integer, Order> orderMap;
private int orderNumber = 0;
private DatabasePersistence dbPersistence; // 데이터 영속성 처리 객체
public ShoppingContext(String dbUrl) {
this.productCatalog = new HashMap<>();
this.orderMap = new HashMap<>();
try {
this.dbPersistence = new DatabasePersistence(dbUrl); // 데이터베이스 연결 초기화
this.dbPersistence.createOrdersTable(); // 테이블 생성
} catch (ClassNotFoundException | SQLException e) {
System.err.println("데이터베이스 초기화 오류: " + e.getMessage());
// 실제 애플리케이션에서는 더 정교한 오류 처리가 필요합니다.
}
}
// ... 기존 prepareCatalog(), getSimulatedCustomerList(), init(), order(), startDiscount() 메소드 ...
// 데이터 영속성 적용 메소드 (8. 데이터 유지)
public void persist() { [cite: 1697]
System.out.println("\n--- 데이터 영속성 적용 시작 ---");
try {
dbPersistence.persistOrders(this.orderMap);
} catch (SQLException e) {
System.err.println("데이터 저장 오류: " + e.getMessage());
} finally {
try {
dbPersistence.closeConnection(); // 연결 종료
} catch (SQLException e) {
System.err.println("데이터베이스 연결 종료 오류: " + e.getMessage());
}
}
System.out.println("--- 데이터 영속성 적용 완료 ---");
}
public Map<Integer, Order> getOrderMap() {
return orderMap;
}
}
// FrameworkExample.java (main 메소드에서 shopping.persist() 호출)
public class FrameworkExample {
public static void main(String[] args) {
// ShoppingContext 초기화 시 데이터베이스 URL 전달
ShoppingContext shoppingContext = new ShoppingContext("jdbc:sqlite:mydb.db");
// ... 기존 주문 시뮬레이션 코드 ...
Map<String, Product> catalog = shoppingContext.prepareCatalog();
List<Customer> customers = shoppingContext.getSimulatedCustomerList(4);
shoppingContext.init();
shoppingContext.order(customers.get(0), catalog.get("book1"));
shoppingContext.order(customers.get(1), catalog.get("food1"));
shoppingContext.startDiscount("food1");
shoppingContext.order(customers.get(2), catalog.get("food1"));
shoppingContext.order(customers.get(3), catalog.get("food3"));
// 8. 데이터 유지 (persist() 메소드 호출) [cite: 1696]
shoppingContext.persist();
}
}
이제 프로그램을 실행하고 종료하더라도, mydb.db 파일에 주문 데이터가 저장되어 유지될 것입니다.
4. 마무리하며: 프로그램의 생명력을 불어넣는 영속성
자바 애플리케이션에서 가장 근본적인 문제 중 하나인 데이터 영속성을 어떻게 해결하는지 알아보았습니다. 프로그램이 종료되면 사라지는 메모리의 한계를 넘어, 데이터베이스와 JDBC를 통해 정보를 영원히 보존하는 방법을 익혔습니다.
JDBC 드라이버 로드부터, 데이터베이스 연결, SQL을 통한 테이블 생성, 그리고 PreparedStatement를 활용한 효율적인 데이터 저장까지, 일련의 과정을 직접 코드로 살펴보았습니다. 특히 SQLException과 같은 Checked Exception을 적절히 처리하며 안정적인 데이터 접근 로직을 구축하는 것의 중요성을 다시금 깨달았습니다.
오늘의 실습
오늘의 느낀점
statement 오랜만에 쓴다! ㅎㅎ 지금은 거의 spring boot 에 orm 사용하다보니 잘 안쓰게 되는데, 오랜만에 쓰니까 재밌다 ㅎㅎ
처음에 필드명이랑 디비 컬럼명 맞춘다고 얼마나 고생했던지 ㅎㅎㅎ 옛날생각이 물씬나는 강의시간이었다!
https://fastcampus.info/4n8ztzq
(~6/20) 50일의 기적 AI 환급반💫 | 패스트캠퍼스
초간단 미션! 하루 20분 공부하고 수강료 전액 환급에 AI 스킬 장착까지!
fastcampus.co.kr
'개발공부 > Java' 카테고리의 다른 글
패스트캠퍼스 환급챌린지 28일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (1) | 2025.07.28 |
---|---|
패스트캠퍼스 환급챌린지 26일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (1) | 2025.07.26 |
패스트캠퍼스 환급챌린지 25일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (2) | 2025.07.25 |
패스트캠퍼스 환급챌린지 24일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (1) | 2025.07.24 |
패스트캠퍼스 환급챌린지 23일차 : 한 번에 끝내는 컴퓨터 공학 & 인공지능 복수전공 초격차 패키지 강의 후기 (3) | 2025.07.23 |