스터디 공부를 하며 우테코 미션에서 사용했던 JDBC의 헷갈리던 개념을 정리해봤다. JDBC, JDBC Driver, JDBC DriverManager 개념을 살펴보고 JDBC와 JdbcTemplate를 활용한 개발 방법을 비교해보고자 한다.
JDBC(Java Database Connectivity)는 자바프로그래밍 언어로 데이터베이스에 접속할 수 있도록 하는 API이다.
애플리케이션 서버와 DB가 상호작용하는 과정은 다음과 같다.
이 과정이 데이터베이스마다 다르게 구현되기 때문에 데이터베이스가 변경될 때마다 애플리케이션 서버에 개발된 데이터베이스 관련 코드를 변경해야 한다. 또한 개발자는 각각의 데이터베이스마다 커넥션을 연결하고 SQL을 전달하고 결과를 응답받는 방식을 새롭게 공부해야 한다.
JDBC는 애플리케이션 서버와 DB가 상호작용하는 과정을 추상화한 기술이다. JDBC의 등장으로 애플리케이션 코드는 JDBC에만 의존하게 되었고 개발자는 JDBC 표준 인터페이스 사용법만 학습하면 된다.
JDBC는 java.sql
, javax.sql
이라는 2개의 패키지로 구성되는데 자바 8을 다운로드하면 자동으로 제공된다.
JDBC만 있다고 개발을 할 수 있는 건 아니다. JDBC는 표준 인터페이스이다. 이 JDBC 인터페이스를 각각의 DB 회사에서 DB에 맞도록 구현해서 라이브러리로 제공하는데 이를 JDBC Driver라고 한다. Oracle DB에 접근할 수 있는 것은 Oracle JDBC 드라이이버라고 하고 H2 DB에 접근할 수 있는 것은 H2 JDBC 드라이버이다.
JDBC는 3가지 주요 API를 제공한다.
java.sql.Connection
- 커넥션java.sql.Statement
- SQL을 담은 내용java.sql.ResultSet
- SQL 요청 응답H2 JDBC 드라이버를 살펴보자.
다음과 같이 dependencies에 h2를 추가해주면
H2 라이브러리가 추가된 것을 볼 수 있다. 열어보니 jdbc 폴더 내부에 Connection을 구현한 JdbcConnection
도 보인다.
위의 그림을 다시 보면 JDBC API와 JDBC Driver 사이에 JDBC Driver Manager가 있는 것을 볼 수 있다. DriverManager는 Manager답게 드라이버들을 관리하고 Connection을 획득하는 기능을 제공한다.
Driver Manager의 커넥션 요청 흐름은 다음과 같다.
DriverManager.getConnection() 요청을 통해 커넥션 요청이 들어오면 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게 순서대로 다음 정보를 넘겨 커넥션을 획득할 수 있는지 확인하다.
직접 실행시켜보니 registeredDrivers
에서 등록된 드라이버를 가져오는 것을 볼 수 있다. 우리가 등록해준 h2드라이버가 인식된다.
이름, 비밀번호 등 접속에 필요한 추가 정보를 바탕으로 연결이 가능한지 확인 후 커넥션을 반환한다.
스프링을 사용하지 않는, 순수 JDBC만 사용한 코드를 살펴보자.
public class BoardDao {
public int save(final BoardDto boardDto) {
final String query = "INSERT INTO boards VALUES(?, ?)";
try (final Connection connection = DBConnection.getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setNull(1, boardDto.id());
preparedStatement.setString(2, boardDto.turn());
preparedStatement.executeUpdate();
final ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
return fetchGeneratedKey(generatedKeys);
} catch (final SQLException e) {
throw new IllegalStateException("[ERROR] 체스 보드 생성 중 오류가 발생하였습니다 : " + e.getMessage());
}
}
...
}
Connection connection = DBConnection.getConnection()
public class DBConnection {
private static final String SERVER = "localhost:3306";
private static final String DATABASE = "chess";
private static final String OPTION = "어쩌구저쩌구";
private static final String USERNAME = "test";
private static final String PASSWORD = "test";
public static Connection getConnection() {
try {
return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD);
} catch (final SQLException e) {
throw new IllegalStateException("[ERROR] DB 연결 오류 : " + e.getMessage());
}
}
}
connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)
preparedStatement.executeUpdate()
executeQuery
을 사용한다.지금 코드를 다시보니 🚨치명적인 실수🚨를 하고 있었다. 리소스를 정리하고 있지 않다. Connection을 닫지 않고 있다. 만약 이 부분을 놓치게 되면 커넥션이 계속 유지되고 결과적으로 커넥션 부족으로 인한 장애가 발생할 수 있다. 다음과 같이 꼭 닫아주자.
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
스프링으로 넘어오면서 방탈출 미션에서는 JdbcTemplate을 이용했었다.
@Repository
public class ReservationDao {
private final JdbcTemplate jdbcTemplate;
public ReservationDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Reservation> findAll() {
String selectAllSQL = """
SELECT r.id AS reservation_id, r.name AS reservation_name, r.date AS reservation_date,
t.id AS time_id, t.start_at AS time_start_at FROM reservation AS r
INNER JOIN reservation_time AS t ON r.time_id = t.id
""";
return jdbcTemplate.query(selectAllSQL, (resultSet, rowNum) -> new Reservation(
resultSet.getLong("reservation_id"),
resultSet.getString("reservation_name"),
resultSet.getDate("reservation_date").toLocalDate(),
new ReservationTime(
resultSet.getLong("time_id"),
resultSet.getTime("time_start_at").toLocalTime()
)
));
}
....
}
JdbcTemplate이나 MyBatis를 SQLMapper라고 한다. JDBC는 오래된 기술인데 이를 편리하게 사용하기 위해 SQL Mapper나 ORM 기술이 도입되었다. SQL Mapper는 응답 결과를 객체로 편리하게 변환해주고 JDBC의 반복 코드를 제거해준다.
위의 체스코드와 비교했을 때 다음의 과정이 제거된 것을 볼 수 있다.
jdbcTemplate.query() 메서드를 타고타고 들어가다보면 JdbcTemplate클래스의 execute라는 메서드가 나온다. 체스 미션에서 직접 개발한 JDBC 코드와 비슷한 걸 볼 수 있다.
🚨치명적인 실수🚨로 자원반납을 안해주고 있다고 했는데 사용하는 곳에서 try 안에 감싸져 있어 자동으로 반납해줌! AutoCloseable 쵝오
다른 🚨치명적인 실수🚨는 ReservationDao에 필드나 파라미터에 final이 안붙어있는거,,