자바 JDBC를 활용한 데이터베이스 연동
이 문서는 자바 애플리케이션에서 JDBC(Java Database Connectivity)를 사용하여 데이터베이스를 조작하는 방법을 안내합니다. 기본적인 SQL 명령어부터 시작하여 JDBC API를 통한 데이터베이스 연결, 쿼리 실행, 효율적인 코드 패턴 및 고급 기능까지 다룹니다.
1. 데이터베이스 기본 조작 (콘솔 환경)
JDBC 프로그래밍에 앞서, MySQL 데이터베이스의 기본적인 콘솔 명령어를 익히는 것이 중요합니다. 다음 예시들은 데이터베이스 생성, 테이블 정의, 데이터 삽입 및 조회 등의 작업을 보여줍니다.
1.1 MySQL 콘솔 명령어
MySQL 셸이나 터미널에서 다음 명령어를 실행하여 데이터베이스를 관리할 수 있습니다. 각 명령어는 세미콜론(;)으로 끝납니다.
-- 'university_db' 데이터베이스 생성
CREATE DATABASE university_db;
-- 모든 데이터베이스 목록 조회
SHOW DATABASES;
-- 'university_db' 데이터베이스 사용
USE university_db;
-- 현재 사용 중인 데이터베이스 확인
SELECT DATABASE();
-- 'students' 테이블 생성
CREATE TABLE students (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(20) NOT NULL UNIQUE,
full_name VARCHAR(50) NOT NULL,
gender VARCHAR(1) NOT NULL,
age INT,
enrollment_date DATE,
major VARCHAR(50)
);
-- 'students' 테이블 구조 확인
DESCRIBE students;
-- 데이터 삽입 예시
INSERT INTO students (student_id, full_name, gender, age, enrollment_date, major)
VALUES ('STU001', '김철수', 'M', 22, '2020-03-01', '컴퓨터공학');
INSERT INTO students (student_id, full_name, gender, age, enrollment_date, major)
VALUES ('STU002', '이영희', 'F', 20, '2021-09-01', '소프트웨어학');
-- 모든 학생 데이터 조회
SELECT * FROM students;
-- 특정 조건의 학생 데이터 조회 (예: 21세 이상)
SELECT * FROM students WHERE age >= 21;
-- 특정 조건의 학생 데이터 삭제 (예: ID가 1인 학생)
DELETE FROM students WHERE id = 1;
-- 테이블 삭제
DROP TABLE students;
-- 데이터베이스 삭제
DROP DATABASE university_db;
주의사항: SQL 명령 작성 시 구문 오류, 한글 문장 부호 사용, 데이터베이스 미선택(`USE` 명령어 누락) 등에 유의해야 합니다.
1.2 GUI 데이터베이스 관리 도구 활용
MySQL Workbench, DBeaver, Navicat과 같은 그래픽 사용자 인터페이스(GUI) 도구를 사용하면 데이터베이스를 시각적으로 더 쉽게 관리할 수 있습니다. 이러한 도구들은 테이블 생성, 데이터 입력, 쿼리 실행 등을 편리하게 지원합니다.
2. JDBC를 이용한 데이터베이스 연결 및 Statement 활용
자바 애플리케이션에서 데이터베이스에 접근하려면 JDBC API를 사용합니다. `java.sql.Statement` 객체는 정적인 SQL 쿼리를 실행하는 데 사용됩니다.
2.1 JDBC 드라이버 설정
JDBC를 사용하려면 먼저 해당 데이터베이스에 맞는 JDBC 드라이버(JAR 파일)를 프로젝트에 추가해야 합니다. MySQL의 경우 'MySQL Connector/J' 드라이버를 사용합니다.
- Maven 프로젝트:
pom.xml파일에 의존성을 추가합니다. - Gradle 프로젝트:
build.gradle파일에 의존성을 추가합니다. - 일반 자바 프로젝트 (IDE): 프로젝트 설정에서 라이브러리로 JAR 파일을 직접 추가합니다.
2.2 JDBC 기본 연결 및 데이터 조회/삽입
다음은 `Statement`를 사용하여 데이터베이스에 연결하고, 데이터를 조회하며, 새로운 데이터를 삽입하는 기본적인 예시입니다. 예시를 위해 간단한 `Student` POJO를 정의합니다.
// Student.java
import java.time.LocalDate;
public class Student {
private int id;
private String studentId;
private String fullName;
private String gender;
private int age;
private LocalDate enrollmentDate;
private String major;
// 기본 생성자
public Student() {}
// 모든 필드를 포함하는 생성자 (id 제외)
public Student(String studentId, String fullName, String gender, int age, LocalDate enrollmentDate, String major) {
this.studentId = studentId;
this.fullName = fullName;
this.gender = gender;
this.age = age;
this.enrollmentDate = enrollmentDate;
this.major = major;
}
// Getter 및 Setter 메서드
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getStudentId() { return studentId; }
public void setStudentId(String studentId) { this.studentId = studentId; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public LocalDate getEnrollmentDate() { return enrollmentDate; }
public void setEnrollmentDate(LocalDate enrollmentDate) { this.enrollmentDate = enrollmentDate; }
public String getMajor() { return major; }
public void setMajor(String major) { this.major = major; }
@Override
public String toString() {
return "Student{" +
"id=" + id +
", studentId='" + studentId + '\'' +
", fullName='" + fullName + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
", enrollmentDate=" + enrollmentDate +
", major='" + major + '\'' +
'}';
}
}
// StudentJdbcService.java
import java.sql.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class StudentJdbcService {
// JDBC 드라이버, URL, 사용자명, 비밀번호 설정
private static final String DB_URL = "jdbc:mysql://localhost:3306/university_db?serverTimezone=Asia/Seoul";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "your_password"; // 실제 비밀번호로 교체
/**
* 데이터베이스 연결을 설정하고 Connection 객체를 반환합니다.
* @return Connection 객체
* @throws SQLException 연결 실패 시 예외 발생
*/
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
}
/**
* 모든 학생 정보를 데이터베이스에서 조회하여 List로 반환합니다.
* @return Student 객체 리스트
*/
public List<Student> retrieveAllStudents() {
List<Student> studentList = new ArrayList<>();
String sqlQuery = "SELECT id, student_id, full_name, gender, age, enrollment_date, major FROM students";
// try-with-resources를 사용하여 Connection, Statement, ResultSet을 자동으로 닫습니다.
try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sqlQuery)) {
while (resultSet.next()) {
Student student = new Student();
student.setId(resultSet.getInt("id"));
student.setStudentId(resultSet.getString("student_id"));
student.setFullName(resultSet.getString("full_name"));
student.setGender(resultSet.getString("gender"));
student.setAge(resultSet.getInt("age"));
// java.sql.Date를 java.time.LocalDate로 변환
student.setEnrollmentDate(resultSet.getDate("enrollment_date").toLocalDate());
student.setMajor(resultSet.getString("major"));
studentList.add(student);
}
} catch (SQLException e) {
System.err.println("학생 정보 조회 중 오류 발생: " + e.getMessage());
e.printStackTrace();
}
return studentList;
}
/**
* 새로운 학생 정보를 데이터베이스에 삽입합니다. (Statement 사용 예시)
* @param student 삽입할 Student 객체
* @return 삽입된 행의 수
*/
public int addStudent(Student student) {
int rowsAffected = 0;
// Statement는 SQL 인젝션에 취약하므로 주의해야 합니다.
// 여기서는 예시를 위해 문자열 포맷팅을 사용합니다.
String insertSql = String.format(
"INSERT INTO students (student_id, full_name, gender, age, enrollment_date, major) VALUES ('%s', '%s', '%s', %d, '%s', '%s')",
student.getStudentId(), student.getFullName(), student.getGender(), student.getAge(),
student.getEnrollmentDate().toString(), student.getMajor()
);
System.out.println("실행할 SQL: " + insertSql);
try (Connection connection = getConnection();
Statement statement = connection.createStatement()) {
rowsAffected = statement.executeUpdate(insertSql);
} catch (SQLException e) {
System.err.println("학생 정보 삽입 중 오류 발생: " + e.getMessage());
e.printStackTrace();
}
return rowsAffected;
}
public static void main(String[] args) {
StudentJdbcService service = new StudentJdbcService();
// 1. 새 학생 정보 삽입
Student newStudent = new Student("STU003", "박민지", "F", 21, LocalDate.of(2022, 9, 1), "경영학");
int addedRows = service.addStudent(newStudent);
if (addedRows > 0) {
System.out.println(addedRows + "개의 학생 정보가 성공적으로 추가되었습니다.");
} else {
System.out.println("학생 정보 추가 실패.");
}
// 2. 모든 학생 정보 조회 및 출력
System.out.println("\n--- 전체 학생 목록 ---");
List<Student> students = service.retrieveAllStudents();
if (students.isEmpty()) {
System.out.println("등록된 학생이 없습니다.");
} else {
students.forEach(System.out::println);
}
}
}
일반적인 문제 및 해결책:
- JDBC 드라이버 누락: `ClassNotFoundException`이 발생합니다. 프로젝트 빌드 경로에 JDBC JAR 파일을 올바르게 추가해야 합니다.
- 데이터베이스 URL 오류: `SQLException`과 함께 "No suitable driver found" 또는 "Communications link failure" 메시지가 나타날 수 있습니다. URL (`DB_URL`) 형식, 호스트, 포트, 데이터베이스 이름, 시간대 설정 (`serverTimezone`) 등을 확인하세요.
- 권한 문제: "Access denied" 오류는 사용자명 (`DB_USER`) 또는 비밀번호 (`DB_PASSWORD`)가 잘못되었을 때 발생합니다.
- 자원 누수: `Connection`, `Statement`, `ResultSet` 객체를 사용 후 반드시 닫아야 합니다. 자바 7부터 도입된 `try-with-resources` 구문을 사용하면 이 작업을 자동으로 처리하여 자원 누수를 방지하고 코드를 간결하게 만들 수 있습니다.
3. PreparedStatement와 매개변수화된 쿼리
`java.sql.PreparedStatement`는 `Statement`의 단점을 보완하는 강력한 인터페이스입니다. 주로 다음과 같은 경우에 사용됩니다.
- SQL 인젝션 방지: 사용자 입력을 직접 SQL 문자열에 포함시키는 대신 매개변수로 처리하여 보안을 강화합니다.
- 성능 향상: 동일한 쿼리를 여러 번 실행할 때, 데이터베이스가 쿼리 계획을 한 번만 컴파일하고 재사용할 수 있게 하여 성능을 높입니다.
- 가독성 및 유지보수성: 복잡한 SQL 쿼리에서 문자열 조합 대신 `?` 플레이스홀더를 사용하여 코드를 더 명확하게 만듭니다.
3.1 PreparedStatement 사용 예시
다음은 `PreparedStatement`를 사용하여 학생을 이름으로 검색하고, 새로운 학생 정보를 안전하게 삽입하는 예시입니다.
// StudentJdbcService.java (일부 메서드 추가)
/**
* 학생 이름으로 검색합니다. (부분 일치 검색 지원)
* @param name 검색할 학생 이름
* @return 검색된 Student 객체 리스트
*/
public List<Student> searchStudentsByName(String name) {
List<Student> studentList = new ArrayList<>();
String sqlQuery = "SELECT id, student_id, full_name, gender, age, enrollment_date, major FROM students WHERE full_name LIKE ?";
try (Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery)) {
preparedStatement.setString(1, "%" + name + "%"); // 첫 번째 ?에 이름 설정
try (ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
Student student = new Student();
student.setId(resultSet.getInt("id"));
student.setStudentId(resultSet.getString("student_id"));
student.setFullName(resultSet.getString("full_name"));
student.setGender(resultSet.getString("gender"));
student.setAge(resultSet.getInt("age"));
student.setEnrollmentDate(resultSet.getDate("enrollment_date").toLocalDate());
student.setMajor(resultSet.getString("major"));
studentList.add(student);
}
}
} catch (SQLException e) {
System.err.println("이름으로 학생 검색 중 오류 발생: " + e.getMessage());
e.printStackTrace();
}
return studentList;
}
/**
* 새로운 학생 정보를 데이터베이스에 안전하게 삽입합니다. (PreparedStatement 사용)
* @param student 삽입할 Student 객체
* @return 삽입된 행의 수
*/
public int addStudentSecure(Student student) {
int rowsAffected = 0;
String insertSql = "INSERT INTO students (student_id, full_name, gender, age, enrollment_date, major) VALUES (?, ?, ?, ?, ?, ?)";
try (Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(insertSql)) {
preparedStatement.setString(1, student.getStudentId());
preparedStatement.setString(2, student.getFullName());
preparedStatement.setString(3, student.getGender());
preparedStatement.setInt(4, student.getAge());
preparedStatement.setDate(5, java.sql.Date.valueOf(student.getEnrollmentDate())); // LocalDate를 java.sql.Date로 변환
preparedStatement.setString(6, student.getMajor());
rowsAffected = preparedStatement.executeUpdate();
} catch (SQLException e) {
System.err.println("학생 정보 안전하게 삽입 중 오류 발생: " + e.getMessage());
e.printStackTrace();
}
return rowsAffected;
}
public static void main(String[] args) {
StudentJdbcService service = new StudentJdbcService();
// 새 학생 정보 삽입 (PreparedStatement 사용)
Student newStudent2 = new Student("STU004", "정수진", "F", 20, LocalDate.of(2023, 3, 1), "산업디자인");
int addedRows = service.addStudentSecure(newStudent2);
if (addedRows > 0) {
System.out.println(addedRows + "개의 학생 정보가 PreparedStatement로 성공적으로 추가되었습니다.");
}
// 이름으로 학생 검색
System.out.println("\n--- '김'이 포함된 학생 검색 ---");
List<Student> foundStudents = service.searchStudentsByName("김");
if (foundStudents.isEmpty()) {
System.out.println("검색된 학생이 없습니다.");
} else {
foundStudents.forEach(System.out::println);
}
}
4. JDBCUtil 및 DAO 패턴
JDBC 코드를 작성하다 보면 데이터베이스 연결/해제, `Statement`/`PreparedStatement` 생성, `ResultSet` 처리 등의 반복적인 코드가 많이 발생합니다. 이를 개선하기 위해 다음과 같은 방법을 활용할 수 있습니다.
- JDBCUtil 클래스: 데이터베이스 연결을 설정하고, 자원을 해제하는 유틸리티 메서드를 모아놓은 클래스를 만들어 코드 중복을 줄입니다.
- DAO(Data Access Object) 패턴: 데이터베이스의 CRUD(Create, Read, Update, Delete) 작업을 캡슐화하는 객체 지향 디자인 패턴입니다. 각 엔티티(예: Student)에 대해 별도의 DAO 클래스를 구현하여 데이터 접근 로직을 분리하고, 비즈니스 로직과 데이터 접근 로직 간의 결합도를 낮춥니다.
5. 트랜잭션 처리 (선택 사항)
트랜잭션은 데이터베이스 작업의 논리적인 단위를 의미하며, 모든 작업이 성공적으로 완료되거나, 하나라도 실패하면 모든 작업을 롤백하여 데이터 일관성을 보장합니다.
JDBC에서 트랜잭션을 관리하려면 `Connection.setAutoCommit(false)`를 호출하여 자동 커밋 모드를 비활성화하고, 모든 작업이 성공하면 `Connection.commit()`, 실패하면 `Connection.rollback()`을 호출합니다.
6. 데이터베이스 연결 풀 (심화)
매번 데이터베이스에 연결하고 해제하는 작업은 비용이 많이 들고 성능 저하를 일으킬 수 있습니다. 데이터베이스 연결 풀은 미리 일정 수의 데이터베이스 연결을 생성해두고 필요할 때 재사용하는 기술입니다.
연결 풀을 사용하면 애플리케이션의 성능과 확장성을 크게 향상시킬 수 있습니다. 널리 사용되는 JDBC 연결 풀 구현체로는 HikariCP(스프링 부트 기본), Apache DBCP, C3P0, Alibaba Druid 등이 있습니다. 연결 풀을 사용하려면 해당 라이브러리를 프로젝트에 추가하고 설정해야 합니다.