반응형
JDBC
JDBC란?
- Java Database Connectivity, 자바 프로그래밍 언어를 사용해 데이터베이스에 접근할 수 있도록 하는 자바 API
- JDBC를 이용하여 DB에 접속, SQL 실행, 데이터를 가져오거나 삭제하는 등 데이터를 다룰 수 있음
JDBC가 등장하게 된 배경
- DB 접근의 표준화를 위해서 등장
- JDBC 등장 이전
- DB마다 존재하는 고유한 API를 직접 사용
- 이에 따라 개발자는 기존의 DB를 다른 DB로 교체해야하는 경우 DB에 맞게 기존의 코드도 모두 수정해야 했으며 심지어 각각의 DB를 사용하는 방법도 새로 학습해야 했음
→ JDBC의 표준 인터페이스 덕분에 개발자는 DB를 쉽게 변경할 수 있게 되었고 변경에 유연하게 대처할 수 있게 됨
JDBC를 알아야 하는 이유
- JDBC는 매우 오래된 기술이며 사용 방법도 복잡함 → JDBC를 직접 사용하기보다는 DB 접근을 더 편리하게 하고 개발 생산성을 높이기 위한 기술인 SQL Mapper와 ORM(Object-Relational Mapping)을 주로 사용
- SQL Mapper나 ORM은 JDBC 기반으로 동작하기에 JDBC를 이해해야 한다 !
JDBC 동작 흐름
- JDBC API를 사용하기 위해서는 JDBC 드라이버를 먼저 로딩한 후 DB와 연결해야 함
- JDBC 드라이버는 JDBC 인터페이스를 구현하는 구현체, 특정 DB 벤더(Oracle, MySQL, PostgreSQL 등)에 대한 연결과 DB에 대한 작업을 가능하게 해줌(JDBC 드라이버의 구현체를 이용해서 특정 벤더의 DB에 접근할 수 있음)
- JDBC가 제공하는 DriverManager가 드라이버들을 관리하고 Connection을 획득하는 기능을 제공
- 획득한 Connection을 통해서 DB에 SQL을 실행하고 결과를 응답받을 수 있음
- 동작 과정
- DriverManager를 통해 Connection 획득
- Connection을 통해 Statement 생성
- 질의 수행 및 Statement를 통해 ResultSet 생성
- ResultSet을 통해 데이터 획득
- 리소스 정리
import java.sql.*;
public class Jdbc {
public static void main(String[] args) {
// 0. 데이터베이스 연결 정보 설정
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "myusername";
String password = "mypassword";
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 1. DriverManger를 통해 Connection 획득
connection = DriverManager.getConnection(url, username, password);
// 2. Connection을 통해 Statement 생성
statement = connection.createStatement();
String sql = "SELECT * FROM users";
// 3. 질의 수행 및 Statement를 통해 ResultSet 생성
resultSet = statement.executeQuery(sql);
// 4. ResultSet을 통해 데이터 획득
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 리소스 정리
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
JDBC 드라이버 관련
DriverManager를 사용하기 전에 Class.forName("com.mysql.jdbc.Driver"); 을 통해 해당 드라이버를 불러와야 하는데 JDBC 4.0 버전 이후부터는 JDBC 드라이버 자동 로딩이 도입되어 생략이 가능
커넥션 풀과 DataSource
- 위의 과정에서 DriverManager를 통해 DB 커넥션을 획득함
- JDBC 드라이버가 DB와 연결을 맺고 부가 정보를 DB에 전달한 후 연결 응답을 받으려면 커넥션을 생성해 넘겨줌
- 커넥션을 사용하고 나면 해당 커넥션을 종료시킴
DriverManger 사용시
- 이렇게 매번 커넥션을 생성하기 위해서는 네트워크와 연결하고 서버의 자원을 사용해야 함
- SQL을 실행할 때마다 커넥션을 획득해야 함 → 연결하는 데에 추가적인 시간이 걸리고 매번 리소스를 사용하게 됨
- 이러한 문제점을 해결하기 위해 커넥션 풀(Connection Pool)을 사용
커넥션 풀 사용시
- DB와의 연결이 필요할 때마다 매번 새로운 커넥션을 생성하는 것 대신 미리 생성된 커넥션을 커넥션 풀에 보관하고 필요할 때 커넥션을 꺼내서 사용하는 것
- 커넥션 풀을 통해 커넥션을 사용하고 나면 커넥션을 종료하지 않고 커넥션 풀에 반납
- 이처럼 커넥션을 재사용하게 되면 리소스를 효율적으로 활용할 수 있으며 성능을 높일 수 있음
커넥션을 획득할 때 DriverManager를 통해서 커넥션을 획득하거나 커넥션 풀을 통해서 커넥션을 획득하는 것과 같이 여러 방법이 존재한다. 그래서 DataSource 인터페이스를 통해서 커넥션을 획득하는 방법을 추상화한다. 이 말은 즉, 어떤 방식으로 커넥션을 획득하는 것과 상관없이 DataSource 인터페이스를 통해 일관된 방식으로 DB와 통신할 수 있는 것이다. 또한 인터페이스를 구현한 구현체를 쉽게 교체할 수 있어 애플리케이션의 유연성을 높일 수 있다.
PreparedStatement 이용하기
- 자바에서 쿼리문을 사용할 때 기본적으로 java.sql 패키지 내의 Statement를 사용
- Statement는 SQL문을 실행할 때 사용하는 인터페이스
- Statment 종류(3가지)
- Statement
- PreparedStatement
- CallableStatement
- CallableStatement는 PL/SQL문을 호출할 때 사용한다고 했지만 성능상 이슈로 인해 거의 사용하지 않음
→ Statement나 PreparedStatement를 사용 - 실무 환경에서는 Statement는 쓰지 않고 PreparedStatement만 사용함
Statement의 동작 방식
- 구문 분석(Parsing) 및 정규화(Normalization)
- Query 문법 확인 및 DB, 테이블 존재 여부 확인
- 컴파일(Compliation)
- Query 컴파일
- Query 최적화(Query Optimization Phase)
- Query 실행 방법의 최적 계획 선택
- 캐시(Cache)
- Query 최적화 단계에서 선택도니 계획이 캐시에 저장되어 동일한 Query 실행 시, 1 ~ 3 단계를 실행하지 않고 캐시를 통해 찾음
- 실행(Execution Phase)
- Query가 실행된 값이 담긴 객체(ResultSet)를 사용자에게 반환
PreparedStatement
- Statement는 매번 Query를 실행할 때마다, 1번에서 5번 과정을 반복
- PreparedStatement는 최초 Query 실행 시, 1번에서 5번까지의 과정을 반복, 두 번째 Query 실행부터는 1번부터 3번까지의 과정을 생략하고 4번부터 시작함 → 성능 향상
// Statement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
Statement stmt = conn.createStatement();
stmt.executeUpdate("Update Member Set Name = 'prao' Where id = 10");
stmt.executeUpdate("Update Member Set Name = 'oarp' Where id = 11");
// PreparedStatement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
String sql = "Update Member Set Name = ? Where id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "prao");
pstmt.setInt(2, 10);
pstmt.executeUpdate();
pstmt.setString(1, "oarp");
pstmt.setInt(2, 11);
pstmt.executeUpdate();
비교
- Statement, PreparedStatement 방식 둘 다 Query를 컴파일하고 최적화한 다음 실행되기 전에 Caching
- Statement는 첫 수행한 Query와 완전히 일치하는 Query를 요청하는 경우에만 캐싱한 데이터를 재활용 가능
- 첫 번째 Query와 일치하지 않는 별도의 Query를 실행하게 되면 컴파일을 다시 해야함
- PreparedStatement 방식은 Statement 방식과는 달리 placeHolder (?)를 이용하여 파라미터를 바인딩
- 기존에 컴파일된 Query를 재활용
- PreparedStatement 방식을 사용하면 DB의 부하를 낮추고 속도를 더 높일 수 있음
- 파라미터를 Query문 안에 직접 넣어주는 Statment 방식은 가독성도 좋지 못하고 코딩하기에도 번거로움
- PreparedStatement 방식은 파라미터 바인딩을 통해 가독성도 챙기고 손쉽게 코딩 가능
SQL Injection 방지(보안성)
// 기존 Statement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
Statement stmt = conn.createStatement();
stmt.executeQuery("SELECT * FROM Member WHERE id = '" + id + "' AND pwd = '" + pwd + "'");
- 로그인 시, 사용자에게 입력받은 값을 WHERE 조건절에 넣어 조회하는 Query
- 만약 다음과 같이 공격자가 파라미터 값을 악의적으로 조작해서 보낼 수 있음
String userInput = "test123' OR '1' = '1";
- 위의 경우, userInput 값이 'test123' OR '1' = '1' 로 설정되면 아래와 같은 쿼리가 생성
SELECT * FROM Users WHERE username = 'test123' OR '1' = '1';
- 이는 항상 참이 되므로 모든 사용자의 데이터가 반환될 수 있음
- 이처럼 공격자가 악의적으로 파라미터 값을 조작하여 사용자 정보 탈취 가능
// PreparedStatment 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
String sql = "Select * from Member Where id = ? and pwd = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
pstmt.setString(2, pwd);
pstmt.executeQuery();
- PreparedStatement 방식은 사용자에게서 받은 파라미터 값이 악의적으로 조작되었다 하더라도 파라미터 바인딩을 통해 SQL Injection을 방지할 수 있다.
- 정확히 pstmt.setString() 메소드 내부에서 사용되는 javaEncode() 메소드가 SQL Injection을 방지해줌
// javaEncode() 메소드
public static void javaEncode(String s, StringBuilder buff, boolean forSQL) {
int length = s.length();
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
switch (c) {
case '\t':
buff.append("\\t");
break;
case '\n':
buff.append("\\n");
break;
case '\f':
buff.append("\\f");
break;
case '\'':
buff.append('\'');
break;
...
}
}
}
이번 주에 학습했던 MySQL 데이터베이스와 SQL을 실제로 활용하여 Java와 DB를 연결해주는 JDBC 설정을 학습했다. 물론 JDBC 설정은 공식처럼 복사 - 붙여넣기로 사용할 수도 있지만, 이렇게 원리를 알고 왜 PreparedStatement를 쓰는지, Statement의 동작과정, JDBC의 역할 등을 공부하며 실제 프로그램이 동작할 때 어떻게 데이터를 주고 받는지 배울 수 있었다.
반응형
'TIL' 카테고리의 다른 글
[TIL-52/240328] 프림(Prim) 알고리즘 (0) | 2024.03.29 |
---|---|
[TIL-51/240327] 그래프 비용(서로소 집합, MST, 크루스칼) (1) | 2024.03.28 |
[TIL-49/240321] Join & SubQuery & Modeling (0) | 2024.03.22 |
[TIL-48/240320] DDL & DML (0) | 2024.03.21 |
[TIL-47/240319] MySQL, SELECT (0) | 2024.03.20 |