애플리케이션 서버와 DB는 다음의 세 단계를 거쳐서 상호작용 한다.
- 주로 TCP/IP를 통해 커넥션 연결
- 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달
- DB는 전달된 SQL을 수행하고 그 결과를 응답
하지만 각각의 데이터베이스를 사용하는 방법, 커넥션을 연결 그리고 결과 응답을 받는 방법이 모두 달랐다. 따라서 DB 종류를 변경하게 되면(ex) MySQL
-> ORACLE
) 다음과 같은 문제가 발생했다.
DB
로직까지 변경해야 한다.DB
마다 따로 공부해야 한다.이런 문제를 해결하기 위해, JDBC
라는 자바 표준이 등장하게 된다
JDBC(Java Database Connectivity)
는, 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다.
이렇듯 JDBC 표준 인터페이스는 연결/SQL 요청/결과 응답의 과정을 추상화하였다.
그리고 인터페이스만으로는 기능 동작을 하지 않으니, 각각의 데이터베이스 회사들은 자신의 DB에 맞게 JDBC 표준 인터페이스를 구현해서 JDBC Driver
라는 라이브러리로 제공하고 있다.
애플리케이션 로직이 JDBC 표준 인터페이스에만 의존하므로, DB를 변경하고 싶을때 JDBC 구현 라이브러리만 변경하면 기존 서버의 로직은 그대로 유지할 수 있다.
SQL
자체는 DB마다 다른 문법을 가지고 있기 때문에, 결국 데이터베이스를 변경하면 JDBC
코드를 제외한 SQL
은 해당 데이터베이스에 맞도록 변경해야 한다.
참고로 이후에 나올
JPA(Java Persistence API)
를 사용하면 이렇게 각각의 데이터베이스마다 다른 SQL을 정의해야 하는 문제도 많은 부분 해결할 수 있다.
또한, 반복되는 중복 코드 뿐만 아니라 커넥션 리소스를 직접 반환해줘야 하기 때문에 제대로 반환하지 않는다면 커넥션이 계속 유지가 되는 리소스 누수의 문제도 발생할 수 있다.
JDBC
는 매우 오래되었고 사용법도 복잡한 만큼, 최근에는 SQL Mapper
, ORM
같은 JDBC
를 편리하게 사용하는 기술들을 사용하고 있다. 자바 Persistence Framework
이라 하며, 둘다 JDBC
를 내부적으로 사용하고 있다는 것에 주의하자.
📚 Persistence Layer와 Framework
Persistence Layer
란 프로그램의 영속성을 부여해주는 계층으로, 영속성이란 데이터를 생성한 프로그램이 종료되도 영구 저장되어 사라지지 않는 특성을 의미한다. 그리고Persistence Framework
는 JDBC의 대안으로 프로그래밍의 복잡성이나 중복 없이 DB와 연동되는 시스템을 개발할 수 있는 프레임워크를 의미한다.
SQL Mapper
의 구현체로는 Spring JdbcTemplate
, MyBatis
가 있다.
장점은, SQL만 작성하면 SQL Mapper
가 나머지 복잡한 일은 해준다는 것이다(SQL 응답 결과를 객체로 반환, JDBC 반복 코드 제거 등). 하지만 직접 SQL을 작성해야 한다는 단점이 있다.
ORM
은 객체를 관계형 데이터베이스의 테이블과 매핑해주는 기술이다.
반복적인 SQL을 직접 작성하지 않고 ORM이 동적으로 생성해주기 때문에 생산성이 증가한다는 장점이 있지만, 실무에서 사용하려면 깊이있게 학습해야 한다.
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/db";
...
}
public class DBConnectionUtil {
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
DriverManager.getConnection()
: 라이브러리에 있는 DB
드라이버를 찾아서 해당 드라이버가 제공하는 실제 커넥션 객체 반환우리는 H2 데이터베이스의 URL
을 인자로 넘겼으니, H2 데이터베이스 드라이버가 작동해서 실제 데이터베이스와 커넥션을 맺고 그 결과를 반환해줄 것이다. H2 드라이버는 전용 커넥션인 org.h2.jdbc.JdbcConnection
를 제공한다.
외부 라이브러리을 찾아서 해당 폴더로 들어가보면 H2의 JdbcConnection
구현체가 자바의 Connection
표준 커넥션 인터페이스를 구현하고 있는 것을 확인할 수 있다.
그렇다면 자바는 어떻게 H2 드라이버를 찾는 걸까?
JDBC
에서 제공하는 DriverManger
는, 라이브러리에 등록한 데이터베이스 드라이버들을 관리하고 커넥션을 획득하는 기능을 제공한다.
DrvierManager
는 커넥션 요청이 들어오면, 라이브러리에 등록된 드라이버 목록들을 자동으로 인식하고 순서대로 URL/이름/비밀번호 정보들을 드라이버들에게 넘겨서 커넥션을 획득할 수 있는지 물어본다.
각각의 드라이버는 정보를 확인하고 본인이 처리할 수 있는 요청이라면 실제 DB에 연결해서 커넥션을 획득하고 구현체를 클라이언트에게 반환한다.
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pState = null;
try {
con = getConnection();
pState = con.prepareStatement(sql);
pState.setString(1, member.getMemberId());
pState.setInt(2, member.getMoney());
pState.executeUpdate(); // 데이터 저장/수정/삭제
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pState, null); // 모든 자원의 반납을 보장
}
}
Connection.prepareStatement(sql)
: 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비한다.PreparedStatement.executeUpdate()
: Statement
를 통해 준비된 SQL
을 커넥션을 통해 실제 데이터베이스에 전달한다.📚 PreparedStatement와 SQL Injection
String memberId = "select * from member"; String sql = "insert into member(member_id, money) value ("+ memberId +", "+ money +")";
PrepareStatement
가 부모인Statement
와 다른 점은 바로 파라미터 바인딩(?
)을 지원한다는 것이다.위의 코드처럼
?
를 통해서가 아닌 직접 인자로 들어갈 변수를 넣는다면 SQL 구문으로 치환되기 때문에 데이터베이스 정보들이 모두 유출될 수 있는 문제가 발생한다. 파라미터 바인딩을 하면 단순 데이터(문자열)로 치환되기 때문에 파라미터에 SQL문을 넣어도 인식이 되지 않아 예방할 수 있다.
JDBC
에서 가장 중요하자 취약점이 되는 것은 바로 리소스 정리를 직접 해줘야 한다는 것이다. 자원 생성의 역순으로 반환을 진행하면 된다.
private void close(Connection c, Statement s, ResultSet r) {
if (s != null) {
try {
s.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (r != null) {
try {
r.close();
} catch (SQLException e) {
log.error("error", e);
}
}
}
}
자원을 하나씩 반납할때 앞쪽에서 예외가 발생해도 뒤쪽 코드에 영향을 주지 않아야 하기 때문에, 매번 try-catch
을 통해 예외를 잡아야 한다.