본문 바로가기

Computer Science/DB

mySQL 컨테이너로 JDBC 프로그래밍 (Java)

오늘은 저번에 Docker에서 띄운 mySQL 컨테이너에 연결해서 DB에 데이터를 저장하고 가져오는 간단한 JDBC 프로그래밍을 해보았습니다. JDBC 프로그래밍은 DB에 접근할 수 있도록 해주는 JDBC API를 이용하여 데이터의 추가, 삭제, 수정, 검색 등을 할 수 있는 자바 응용프로그램을 작성하는 것인데요, 한 마디로 mySQL 등의 DB를 사용하는 응용 어플리케이션을 작성하는 것입니다. JDBC에 대해 알아보기 앞서, 데이터베이스와 SQL에 대해서 간단하게 살펴보겠습니다.

 

데이터베이스


데이터베이스(Database)는 데이터를 저장하고 관리하기 위한 체계적인 방법을 제공하는 시스템입니다. 데이터베이스는 일반적으로 테이블(Table)이라는 구조를 사용하여 데이터를 저장하고, SQL(Structured Query Language)을 사용하여 데이터를 검색하고 수정합니다. 데이터베이스는 대규모의 데이터를 저장하고, 다중 사용자 간의 데이터 공유를 지원하며, 데이터의 일관성과 무결성을 보장하는 등의 기능을 제공합니다.

 

🔑용어 정리
데이터베이스 : 테이블, 테이블과 관련된 SQL 구성요소를 담고 있는 저장소
쿼리 : 데이터베이스로 정보에 접근하는 것
테이블 : 데이터를 열과 행으로 구조화하여 보관하고 있는 데이터베이스의 구성 요소
행(레코드) : 한 객체에 대한 속성들을 나타내는 열의 집합
열(필드) : 테이블에 저장된 하나의 데이터

 

DB에서는 SQL이라는 언어를 통해서 데이터에 접근하여 수정이 가능합니다. 이 때, 트랜잭션이라고 하는 데이터베이스 작업의 논리적 단위 개념이 등장합니다. 다음으로는 트랜잭션의 개념과 특징을 알아보고, SQL과 SQL 문법을 간단히 알아보겠습니다.

 


트랜잭션?


트랜잭션은 데이터베이스 작업의 논리적 단위를 의미합니다. 하나의 트랜잭션 안에서 수행되는 작업들은 모두 성공하거나 실패해야 하며, 중간에 어느 하나의 작업이 실패하면 모든 작업은 롤백되어 이전 상태로 돌아갑니다. 이를 통해 데이터의 일관성과 무결성을 보장할 수 있습니다.

 

예를 들어, 은행에서 돈을 이체하는 작업을 수행한다고 가정해봅시다. 이체를 하기 위해서는 먼저 이체할 계좌에서 돈을 출금하고, 다른 계좌로 돈을 입금해야 합니다. 이때 이체 작업이 두 개의 트랜잭션으로 나누어지면 중간에 문제가 생겨서 출금은 성공했지만 입금은 실패하는 상황이 발생할 수 있습니다. 이 경우, 출금은 이루어졌지만 입금이 실패하여 돈이 어디에도 들어가지 않은 상황이 발생하게 됩니다. 이를 방지하기 위해서는 출금과 입금을 하나의 트랜잭션으로 묶어서 수행해야 합니다. 출금과 입금이 모두 성공하거나 모두 실패해야 합니다.

 

이처럼 트랜잭션은 데이터베이스에서 데이터 일관성과 무결성을 보장하기 위해 매우 중요한 개념입니다.

트랜잭션은 다음과 같은 특징을 가지고 있습니다. 이를 보통 ACID라고도 이야기합니다.

 

1. 원자성(Atomicity): 트랜잭션은 모든 연산들이 전부 수행되거나 전부 수행되지 않는 원자적인 단위로 처리됩니다. 즉, 트랜잭션의 모든 연산들은 모두 성공적으로 완료되거나, 아무것도 수행되지 않은 것처럼 롤백되어야 합니다. 아까 예를 들었던 은행 계좌이체의 예가 원자성의 사례입니다.

 

2. 일관성(Consistency): 트랜잭션이 수행되면 데이터베이스의 상태는 일관성을 유지해야 합니다. 일관성은 트랜잭션 실행 전과 후에 데이터베이스의 제약 조건이나 규칙 등이 항상 만족되어야함을 의미합니다. 예를 들어, 계좌 이체 시 계좌의 잔액이 음수가 되면 안되는 것과 같이, 데이터베이스에서 정의된 제약 조건을 위반하지 않아야 합니다.

 

3. 격리성(Isolation): 트랜잭션은 다른 트랜잭션의 연산에 영향을 받지 않고 독립적으로 실행되어야 합니다. 실제로 동시에 여러 개의 트랜잭션들이 수행될 때, 각 트랜젝션은 독립적으로, 연속으로 실행된 것과 동일한 결과를 나타내야함을 의미합니다.즉, 트랜잭션 실행 중에 다른 트랜잭션에서 수행하는 연산들과 상호 간섭이 발생하지 않아야 합니다.

 

4. 지속성(Durability): 지속성은 하나의 트랜잭션이 성공적으로 수행되었다면, 해당 트랜잭션에 대한 로그가 남아야하는 성질을 말합니다. 트랜잭션이 성공적으로 완료되었다면, 그 결과는 언제든지 복구할 수 있어야 합니다. 데이터베이스 서버가 중단되거나 오류가 발생해도, 트랜잭션이 완료된 결과는 영구적으로 유지되어야 합니다.

 

이러한 트랜잭션의 특징은 데이터베이스의 무결성과 일관성을 유지하기 위해 매우 중요합니다. 트랜잭션은 데이터베이스에서 매우 빈번하게 사용되는 개념으로, 데이터베이스의 복구 및 보안 등 다양한 측면에서 중요한 역할을 합니다.

 

SQL


SQL은 Structured Query Language의 약자로, 관계형 데이터베이스 관리 시스템(RDBMS)에서 사용되는 표준 쿼리 언어입니다. SQL은 데이터베이스에서 데이터를 저장, 수정, 삭제, 검색하는 데 사용되며, 대부분의 관계형 데이터베이스에서 지원됩니다. SQL은 다양한 기능을 가지고 있으며, 데이터베이스 스키마 정의, 데이터 검색 및 삽입, 업데이트, 삭제, 데이터베이스 보안, 트랜잭션 관리 등을 지원합니다. 또한 SQL은 매우 간단하고 직관적인 문법을 가지고 있어, 데이터베이스 작업을 수행하기 위해 프로그래밍 지식이 없는 사람도 쉽게 사용할 수 있습니다.

 

SQL 문법 종류


DDL, DML, DCL, TCL은 각각 SQL에서 사용되는 다양한 종류의 명령어 집합입니다.

1. DDL(Data Definition Language)

DDL은 데이터 정의 언어로, 데이터베이스 스키마를 정의하고 수정하는 데 사용됩니다. DDL 명령어는 데이터베이스, 테이블, 뷰, 인덱스 등을 생성, 수정, 삭제하는 데 사용됩니다. 대표적인 DDL 명령어로는 CREATE, ALTER, DROP 등이 있습니다.

2. DML(Data Manipulation Language)

DML은 데이터 조작 언어로, 데이터를 삽입, 수정, 삭제, 검색하는 데 사용됩니다. DML 명령어는 테이블에 저장된 데이터를 검색하고 조작하는 데 사용됩니다. 대표적인 DML 명령어로는 SELECT, INSERT, UPDATE, DELETE 등이 있습니다.

 

3. DCL(Data Control Language)

DCL은 데이터 제어 언어로, 데이터베이스 사용자에게 권한을 부여하거나 취소하는 데 사용됩니다. DCL 명령어는 데이터베이스 보안 및 권한 관리를 위해 사용됩니다. 대표적인 DCL 명령어로는 GRANT, REVOKE 등이 있습니다.

 

4. TCL(Transaction Control Language)

TCL은 트랜잭션 제어 언어로, 데이터베이스 트랜잭션을 제어하는 데 사용됩니다. TCL 명령어는 데이터베이스의 일관성과 무결성을 유지하기 위해 사용됩니다. 대표적인 TCL 명령어로는 COMMIT, ROLLBACK, SAVEPOINT 등이 있습니다.

따라서, 각각 DDL, DML, DCL, TCL의 목적은 다르지만, 데이터베이스를 관리하고 조작하는 데 필수적인 기능을 수행합니다.

 

JDBC


JDBC(Java Database Connectivity)는 자바 프로그램에서 데이터베이스와 연결하고 데이터를 조회하거나 수정하는 기능을 제공하는 자바 API입니다.

 

JDBC를 사용하면 자바 프로그램에서 데이터베이스와 손쉽게 연동할 수 있습니다. JDBC는 데이터베이스에 대한 연결을 설정하고 SQL 쿼리를 실행하며, 결과를 가져오는 등의 작업을 처리할 수 있도록 다양한 인터페이스와 클래스를 제공합니다.

 

JDBC는 데이터베이스 제조사와 독립적으로 개발되었기 때문에, 자바 언어를 사용하는 모든 데이터베이스 시스템과 호환됩니다. 이러한 특징 덕분에, JDBC는 대부분의 자바 애플리케이션에서 데이터베이스 연동에 널리 사용되고 있습니다.

그렇지만, 사용하기 위해서는 데이터베이스에 맞는 데이터베이스 제조사에서 제공하는 JDBC 드라이버를 패스에 추가해야 합니다. 대부분의 JDBC 드라이버는 JAR 파일 형태로 제공되며, 이를 다운로드 받아 자바 클래스 패스에 추가하면 JDBC를 사용할 수 있습니다. 또한, 일부 데이터베이스 제조사는 자체적으로 JDBC 드라이버를 제공하는 대신 JDBC 표준을 따르는 특별한 드라이버를 제공하기도 합니다.

 

JDBC는 JDBC 드라이버, Connection, Statement, ResultSet 등 다양한 클래스와 인터페이스로 구성되어 있으며, 이를 활용하여 데이터베이스와의 연결을 설정하고 SQL 쿼리를 실행할 수 있습니다.

 

JDBC 프로그래밍 흐름


  1. JDBC 드라이버 로드
  2. 데이터베이스 연결 : 데이터베이스 URL, 사용자 이름, 비밀번호 등의 정보를 제공하여 연결을 생성합니다.
  3. SQL 쿼리 실행 : SQL 쿼리를 생성하고 실행합니다. PreparedStatement 객체는 미리 컴파일된 SQL 문을 나타내며, 매개 변수를 사용하여 동적으로 생성할 수 있습니다.
  4. 결과 처리 : SQL 쿼리가 실행되면 ResultSet 객체를 사용하여 결과를 가져옵니다. ResultSet 객체는 결과 집합을 나타내며, 다양한 메서드를 사용하여 데이터를 처리할 수 있습니다.
  5. 데이터베이스 연결 종료 : 데이터베이스 작업이 완료되면 Connection, PreparedStatement, ResultSet 등의 객체를 종료하고, 데이터베이스 연결을 닫아야 합니다.

아래에 PreparedStatement를 이용한 JDBC 예시를 보면서 위 흐름을 다시 확인할 수 있습니다.

 

SQL 쿼리 실행 - PreparedStatement


JDBC에서 쿼리를 실행하기 위해 자바에서 제공하는 라이브러리는 Statement와 PreparedStatement가 있습니다. 두 클래스는 같은 역할을 수행하지만, 컴파일 방법에서 차이점이 있습니다. 

 

Statement는 실행될 때마다 SQL 쿼리를 컴파일합니다. 이것은 쿼리가 실행될 때마다 반복적으로 수행되므로, 쿼리 실행 시간이 길어질 수 있습니다. 또한, Statement는 쿼리의 매개 변수를 직접 문자열로 연결하므로, 쿼리의 가독성과 유지보수성이 떨어질 수 있습니다.

 

반면에 PreparedStatement는 SQL 쿼리를 미리 컴파일하여 재사용이 가능한 객체를 생성합니다. 이렇게 하면 실행 시간이 단축되며, 쿼리 실행 시마다 새로운 SQL 문을 컴파일하지 않으므로 성능이 개선됩니다. 또한, PreparedStatement는 쿼리의 매개 변수를 사용하여 쿼리를 작성할 수 있으므로, 가독성과 유지보수성이 높아집니다.

따라서, PreparedStatement를 이용해서 쿼리를 전달하여 실행해보겠습니다. 아래는 JDBC로 mySQL 컨테이너에 연결하여 데이터를 가져오는 예시 코드입니다.

 

public class Example {
    private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String DB_URL = "jdbc:mysql://localhost:3306/example_db";
    private static final String DB_USER = "example_user";
    private static final String DB_PASSWORD = "example_password";

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            // 1. 데이터베이스 드라이버 로드
            Class.forName(DB_DRIVER);

            // 2. 데이터베이스 연결
            conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

            // 3. PreparedStatement 생성
            String sql = "SELECT * FROM example_table WHERE name = ?";
            pstmt = conn.prepareStatement(sql);

            // 4. 파라미터 설정
            pstmt.setString(1, "John");

            // 5. SQL 실행
            rs = pstmt.executeQuery();

            // 6. 결과 처리
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                String email = rs.getString("email");
                System.out.println("id: " + id + ", name: " + name + ", email: " + email);
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 7. 자원 해제
            try {
                if (rs != null) {
                    rs.close();
                }
                if (pstmt != null) {
                    pstmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

 

DAO, DTO


위의 예시처럼, 쿼리를 전송해서 DB의 데이터에 접근할 수 있음을 확인했습니다. 그런데, 이렇게 클래스를 작성하고 보니 비즈니스 로직과 데이터베이스에 접근하는 로직이 한 클래스에 얽혀있죠. 그래서 DAO(Dao: Data Access Object)와 DTO(Dto: Data Transfer Object)라는 데이터베이스 액세스와 관련된 객체지향 디자인 패턴이 등장합니다.

 

DAO는 데이터베이스와의 액세스를 추상화하는 인터페이스를 제공하는 클래스입니다. DAO 패턴은 데이터베이스와 애플리케이션 간의 의존성을 줄이기 위해 사용됩니다. DAO 패턴은 데이터베이스 액세스 로직을 분리하고 애플리케이션 로직과 데이터베이스 로직을 분리하여 유지 보수성과 재사용성을 향상시킵니다.

 

DTO는 데이터베이스에서 가져온 데이터를 애플리케이션에서 사용하는 객체로 변환하는 객체입니다. DTO는 데이터 전송 객체 또는 모델 객체로도 불립니다. DTO 패턴은 데이터베이스에서 가져온 데이터를 바인딩하는 데 사용됩니다.

 

위 두 개념을 활용해서 비즈니스 로직과의 분리가 가능해집니다. DAO를 통해서 데이터베이스에 액세스하고, DTO로 데이터베이스에서 가져온 데이터를 객체로 변환하여 사용할 수 있습니다.

 

예를 들어, 사용자 정보를 데이터베이스에서 가져와 애플리케이션에서 사용하려면 DAO를 사용하여 데이터베이스에서 사용자 정보를 검색합니다. 이 정보는 DTO 객체에 저장됩니다. 그리고 서비스에서는 DTO 객체를 이용해서 비즈니스 로직을 구현합니다. 이렇게 하면 데이터베이스와 애플리케이션 간의 의존성을 줄이고, 애플리케이션의 유지 보수성과 재사용성을 높일 수 있습니다.