본문 바로가기
카테고리 없음

JDBC 내용 정리

by SuldenLion 2023. 5. 28.
반응형

이것은 자바다 책 내용 기반 정리

 

JDBC란?

- Java Database Connectivity

- JDBC 라이브러리 (java.sql 패키지)를 제공하며 자바와 데이터베이스를 연결해서 데이터 입출력 작업을 할 수 있도록 함

- 애플리케이션이 JDBC 인터페이스(java.sql 패키지)를 거쳐 드라이버를 통해 각 데이터베이스와 소통할 수 있게 함

- 인터페이스를 통해 실제로 DB와 작업하는 것은 JDBC Driver이며, Driver는 DBMS마다 별도로 다운받아 사용해야 함.

 

> JDBC에 포함된 클래스와 인터페이스의 연관 관계

 

▷ DriverManager 

- JDBC Driver를 관리하며 DB와 연결해서 Connection 구현 객체를 생성함

 

▷ Connection

- Connection 인터페이스는 Statement, PreparedStatement, CallableStatement 구현 객체를 생성하며, 트랜잭션 처리 및 DB 연결을 끊을 때 사용함

 

▷ Statement

- Statement 인터페이스는 SQL의 DDL과 DML을 실행할때 사용함. 주로 변경되지 않는 정적 SQL문을 실행할 때 사용

 

PreparedStatement

- PreparedStatement는 Statement와 동일하게 SQL의 DDL, DML문을 실행할 때 사용함. 차이점은 매개변수화된 SQL문을 사용할 수 있기 때문에 편리성과 보안성이 좋음. 그래서 Statement보다 선호됨.

 

CallableStatement

- CallableStatement는 DB에 저장되어 있는 프로시저와 함수를 호출할 때 사용함 (프로시저 = 테이블에서 데이터를 추출, 조작하고 결과를 다른 테이블에 다시 저장하거나 갱신하는 처리를 할때 사용하는 것)

 

▷ ResultSet

- ResultSet은 DB에서 가져온 데이터를 읽을때 사용함

 

 

JDBC를 사용하기 위한 절차

- DBMS를 설치하여 데이터베이스를 쓸수 있는 환경을 만들어 준다 → MySQL의 Workbench와 같은 Client Tool을 설치한다(Username, Password 세팅) → DB를 구성한다 (DB와 Table 생성) → DB를 연결한다

 

> DB 연결

 

DBMS가 설치된 컴퓨터의 IP 주소 (컴퓨터를 찾아가기 위해 필요)

→ DBMS가 허용하는 Port 번호 (DBMS에 연결하기 위해 필요)

→ 사용자(DB 계정) 및 비밀번호 (어떤 사용자인지 인증받기 위한 계정 및 비밀번호가 필요)

 → 사용하고자 하는 DB 이름 (DBMS는 여러개의 DB를 관리하므로 실제로 사용할 DB 이름이 필요)

 

 

> JDBC Driver 설치

- JDBC Driver 라이브러리 파일(jar 파일)을 받는다. (mysql-connector-java-x.x.x.jar 파일)

 

> JDBC Driver 메모리로 로딩

- Client 프로그램을 DB와 연결하기 위해 가장 먼저 해야 할 작업은 JDBC Driver를 메모리로 로딩하는 것.

- Class.forName() 메서드는 문자열로 주어진 JDBC Driver 클래스를 Build Path에서 찾고, 메모리로 로딩함.

Class.forName("com.mysql.cj.jdbc.Driver");

 

- 위 과정에서 JDBC Driver 클래스의 static 블록이 실행되면서 DriverManager에 JDBC Driver 객체를 등록함. 만약 Build Path에서 JDBC Driver 클래스를 찾지 못하면 ClassNotFoundException이 발생하므로 예외처리 해야함.

API의 DriverManager 메서드와 설명

- DriverManager에 JDBC Driver가 등록되면 getConnection() 메서드로 DB와 연결할 수 있음

Connection conn = DriverManager.getConnection("jdbcURL과 같은 연결문자열", "user", "pw");

- 첫 번째 argument는 연결문자열인데 DBMS마다 다른 형식을 가짐

MySQL의 연결문자열은 jdbc:mysql://(IP주소):(Port Number)/(DB Name)과 같다.

IP주소에 localhost가 들어가면 로컬에 설치된 MySQL에 연결함.

- 연결 성공시 getConnection() 메서드는 Connection 객체를 리턴함. 연결 실패시 SQLException 발생하므로 예외 처리 해야함.

예시 코드가 있는 Git URL ↓

 

- 연결 성공했던 DB를 끊을 때에는 Connection 객체의 close() 메서드를 호출. 역시 SQLException 발생 가능성 있으므로 예외처리 해준다.

 

> 데이터 저장

- JDBC INSERT문 사용 예시)

INSERT INTO user (userid, username, userpw, userage, useremail)
VALUES (?, ?, ?, ?, ?)

user 테이블에 새로운 사용자 정보를 저장한다. VALUES의 값들을 '?'로 매개변수화 해서 사용할 수 있다.

↳ 매개변수화된 SQL문을 실행하려면 PreparedStatement가 필요함. Connection의 prepareStatement() 메서드로부터 PreparedStatement를 얻어옴.

String sql = new StringBuilder()
	.append("INSERT INTO user (userid, username, userpw, userage, useremail) ")
	.append("VALUES (?, ?, ?, ?, ?)")
    .toString();
    
PreparedStatement pstmt = conn.preparedStatement(sql);

pstmt.setString(1, "SuldenLion");
pstmt.setString(2, "술든라이언");
pstmt.setString(3, "12345");
pstmt.setInt(4, 17);
pstmt.setString(5, "suldenlion@suldenemail.com");

값 지정 후 executeUpdate() 메서드 호출시 SQL 문 실행되면서 users 테이블에 1개의 행이 저장됨. executeUpdate() 메서드가 리턴하는 값은 저장된 행 수이며, 정상적으로 실행되면 1리턴.

int rows = pstmt.executeUpdate();
pstmt.close();

PreparedStatement를 더 이상 사용하지 않을 경우 close() 메서드로 닫아준다.

 

 

> 데이터 수정

- JDBC를 이용해서 UPDATE 문을 실행함

UPDATE boards SET
	btitle=?,
	bcontent=?,
	bfilename=?,
	bfiledata=?
WHERE bno=?

boards 테이블에 저장된 게시물중 bno(=게시물 번호)가 3인 게시물을 변경하는 SQL문이다

- String 타입 변수 sql에 UPDATE 문을 저장한다.

String sql = new StringBuilder()
    .append("UPDATE boards SET ")
    .append("btitle=?, ")
    .append("bcontent=?, ")
    .append("bfilename=?, ")
    .append("bfiledata=? ")
    .append("WHERE bno=?")
    .toString();

매개변수화된 UPDATE 문을 실행하기 위해 prepareStatement() 메서드로부터 PrepareStatement를 얻고 '?'에 해당하는 값을 지정함

PrepareStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "SuldenLion");
pstmt.setString(2, "술을 든 라이언");
pstmt.setString(3, "SuldenLion.jpg");
pstmt.setBlob(4, new FileInputStream("./src/SuldenLion.jpg"));
pstmt.setInt(5, 3);

값을 모두 지정하고 UPDATE 실행을 위해 pstmt.executeUpdate() 메서드 호출.

↳ 실행 성공시 수정된 행의 수가 리턴됨. 0이 리턴되었다면 조건에 맞는 행이 없어 수정된 내용이 없음을 의미함.

 

 

> 데이터 삭제

- JDBC를 이용해서 DELETE 문을 실행함

다음은 board의 bwriter변수가 SuldenLion인 모든 게시물을 삭제하는 DELETE문이다.

DELETE FROM boards WHERE bwriter='SuldenLion'

데이터 저장이나 수정과 거의 같음.

String sql = "DELETE FROM boards WHERE bwriter=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "SuldenLion");
int rows = pstmt.executeUpdate();

 

 

> 데이터 읽기

- PreparedStatement를 생성할 때 SQL 문이 INSERT, UPDATE, DELETE일 경우에는 executeUpdate() 메서드를 호출하지만 데이터를 가져오는 SELECT 문일 경우는 executeQuery() 메서드를 호출함.

- executeQuery() 메서드는 가져온 데이터를 ResultSet에 저장하고 리턴함.

- ResultSet은 SELECT 문에 기술된 컬럼으로 구성된 row의 집합이다.

ResultSet rs = pstmt.executeQuery();

- ResultSet의 특징은 커서가 있는 행의 데이터만 읽을 수 있다는 것. (커서는 행을 가리키는 포인터를 말함)

next() 메서드를 통해 커서를 다음 행으로 이동시킴. 리턴 타입은 boolean 이며 이동한 행에 데이터가 있으면 true를, 없으면 false 리턴.

- Select 문에 따라 ResultSet에는 많은 데이터 행이 저장될 수 있기 때문에 ResultSet을 더 이상 사용하지 않는다면 close() 메서드를 호출해서 ResultSet이 사용한 메모리를 해제하는 것이 좋음

rs.close();

 

 

> 데이터 행 읽기

- 커서가 위치한 데이터 행에서 각 컬럼의 값은 Getter 메서드로 읽을 수 있다. 컬럼의 데이터 타입에 따라서 getInt(), getString() 등의 메서드가 사용되며, 매개값으로 컬럼 Name 또는 컬럼 Index를 줄 수 있다.

// Column 이름으로 읽기
String userId = rs.getString("userid");

// Column 인덱스로 읽기
String userName = rs.getString(2);

(SELECT 문에 연산식이나 함수 호출이 포함되어 있다면 Column 이름 대신 컬럼 Index로 읽어야 함)

 

 

> 사용자 정보 읽기

- userid가 "SuldenLion"인 정보를 가져와 보겠다.

import lombok.data;

@Data
public class User {
    private String userId;
    private String userName;
    private String userPassword;
    private int userAge;
    private String userEmail;
}

- 위와 같이 User 클래스를 하나 작성하여 users 테이블의 행의 정보를 저장할 것이다.

(lombok 라이브러리는 사용하지 않을 것이지만, 내용 정리를 위해 둠. @Data 어노테이션은 Constuctor, Getter, Setter, hashCode(), equals(), toString()을 자동 생성해줌. 직접 친 코드에는 get 함수와 set함수를 따로 만들어 줄 것이다.)

 

//SELECT userid, username, userpassword, userage, useremail
//FROM users
//WHERE userid='SuldenLion';
↓
String sql = "SELECT userid, username, userpassword, userage, useremail " +
	"FROM users WHERE userid=?";

- SELECT 문 실행을 위해 prepareStatement() 메서드로부터 PrepareStatement를 얻고 '?'에 값을 지정함.

PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "SuldenLion");

- executeQuery() 메서드로 SELECT 문을 실행하고 ResultSet을 얻는다.

- userid는 primary key이므로 조건에 맞는 행은 1개나 0개이다. if문을 사용해서 next() 메서드가 true를 리턴하면 데이터 행을 User 객체에 저장하고 출력한다.

ResultSet rs = pstmt.executeQuery();

if (rs.next()) {
    User user = new User();
    user.setUserId(rs.getString("userid"));
    user.setUserName(rs.getString("username"));
    user.setUserPassword(rs.getString("userpassword"));
    user.setUserAge(rs.getInt(4));
    user.setUserEmail(rs.getString(5));
    System.out.println(user);
}

User에 toString을 만들어주면 users 테이블에서 userid가 SuldenLion인 사용자 정보를 가져오는 전체 코드를 보여줄 것이다.

+ 만약 user의 Data member중 Blob 객체로된 데이터가 있고 getBlob()을 통해 불러올때, 출력시 com.mysql.cj.jdbc.Blob@28fs2..과 같이 의미없는 타입 정보가 출력됨. 해당 객체에 저장된 바이너리 데이터를 얻기 위해서는 입력 스트림 또는 바이트 배열을 얻어내야 함.

// InputStream
Blob blob = user.getBlobData();
InputStream is = blob.getBinaryStream();

// Byte Array
Blob blob = user.getBlobData();
byte[] bytes = blob.getBytes(0, blob.length());

 

+ Blob 객체에서 InputStream을 얻고, 읽은 바이트를 파일로 저장하고 싶다면

InputStream is = blob.getBinaryStream();
OutputStream os = new FileOutputStream("C:/Temp/" + user.getFileName());
is.transferTo(os);
os.flush();
os.close();
is.close();

 

 

트랜잭션(Transaction) 처리

- 트랜잭션은 기능 처리의 최소 단위를 말함. 하나의 기능은 여러 가지 소작업들로 구성될 수 있으며, 최소 단위란 것은 이 소작업들을 분리할 수 없으며 전체를 하나로 본다는 개념. (트랜잭션은 소작업들이 모두 성공하거나 모두 실패해야 함)

↳ (ex. 계좌 이체는 출금 작업과 입금 작업으로 구성된 트랜잭션이며, 둘중 하나만 성공할 수 없고 모두 성공하거나 실패해야 한다)

// 출금 작업 - 출금 계좌에서 금액을 감소시킨다
UPDATE accounts SET balance=balance-이체금액 WHERE ano=출금계좌번호

// 입금 작업 - 입금 계좌에서 금액을 증가시킨다
UPDATE accounts SET balance=balance+이체금액 WHERE ano=입금계좌번호

- DB는 트랜잭션을 처리하기 위해 커밋(commit)과 롤백(rollback)을 제공함. commit은 내부 작업을 모두 성공 처리하고, rollback은 실행 전으로 돌아간다는 의미로 모두 실패 처리함.

- JDBC에서는 INSERT/UPDATE/DELETE 문 실행시 자동 커밋이 일어남. (이 기능은 계좌 이체와 같이 두가지 UPDATE 문을 실행할 때 문제가 됨. 출금 작업 성공시 바로 커밋이 되기 때문에 입금 작업의 성공 여부와 상관없이 출금 작업만 별도 처리됨. 따라서 JDBC에서 트랜잭션을 코드로 제어하려면 자동 커밋 기능을 꺼야 함 → 자동 커밋 설정 여부는 Connection의 setAutoCommit() 메서드로 할 수 있다.)

- 자동 커밋 기능을 끄면, Connection의 commit() 메서드와 rollback() 메서드로 제어할 수 있음.

 

트랜잭션을 위한 일반적인 코드 작성 패턴 ↓

Connection conn = null;
try {
    // 트랜잭션 시작
    // ...
    // 자동 커밋 기능 끄기
    conn.setAutoCommit(false);
    // 소작업 처리
    // ...
    // ...
    // 커밋 -> 모두 성공 처리
    conn.commit();
    // ...
    // 트랜잭션 종료
} catch (Exception e) {
	// 롤백 -> 모두 실패 처리
    try {
	    conn.rollback();
    } catch (SQLException e1) {
    }
} finally {
	if (conn != null) {
    	try {
        	// 자동 커밋 기능 원래대로 켜기
            conn.setAutoCommit(true);
            // 연결 끊기
            conn.close();
        } catch(SQLException e) {
        }
    }
}

 

 

accounts 테이블에서 sul-den-lion 계좌에서 sul-den-tiger 계좌로 100000원을 이체하기 위한 트랜잭션 처리 예제 ↓

import java.sql.*;

public class TransactionTest {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");

            conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/suldenlion",
                "root",
                "1234"
            );

            conn.setAutoCommit(false);

            // 출금
            String sql1 = "UPDATE accounts SET balance=balance-? WHERE ano=?";
            PreparedStatement pstmt1 = conn.prepareStatement(sql1);
            pstmt1.setInt(1, 100000);
            pstmt1.setString(2, "sul-den-lion");
            int rows1 = pstmt1.executeUpdate();
            if (rows1 == 0) throw new Exception("출금되지 않음");
            pstmt1.close();

            // 입금
            String sql2 = "UPDATE accounts SET balance=balance+? WHERE ano=?";
            PreparedStatement pstmt2 = conn.prepareStatement(sql2);
            pstmt2.setInt(2, 100000);
            pstmt2.setString(2, "sul-den-tiger");
            int rows2 = pstmt2.executeUpdate();
            if (rows2 == 0) throw new Exception("입금되지 않음");
            pstmt2.close();

            conn.commit();
            System.out.println("계좌 이체 성공");
        } catch (Exception e) {
            try {
                conn.rollback();
            } catch (SQLException e1) {}
            System.out.println("계좌 이체 성공");
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.setAutoCommit(true);
                    conn.close();
                } catch (SQLException e) {}
            }
        }
    }
}

입금에서 계좌 번호를 잘못줘서 rollback이 되면 출금도 실패 처리되므로 입출금 계좌 금액 모두 변동되지 않음.

 

※ 트랜잭션을 처리한 이후에는 원래대로 자동 커밋 기능을 켜둬야 함. Connection을 다른 기능 처리를 위해 계속 사용해야 한다면 돌려놔야 됨. 특히 Connection Pool(커넥션 풀)을 사용할 때 주의해야 한다고 함.

 

> Connection Pool 

- 다수의 클라이언트의 요청을 처리하는 서버 프로그램은 대부분 커넥션 풀을 사용함. 커넥션 풀은 일정량의 Connection을 미리 생성시켜놓고, 서버에서 클라이언트의 요청을 처리할 때 Connection을 제공해주고 다시 반환받는 역할을 수행함)

- 커넥션 풀을 사용하면 생성된 Connection을 재사용할 수 있기 때문에 DB 연결 시간을 줄일 수 있고, 전체 Connection 수를 관리할 수도 있음. 

- 이는 불특정 다수의 클라이언트 요청을 처리하는 서버 프로그램에서는 필수 기능 중 하나임.

 

반응형

댓글