Spring을 통해 백엔드 개발을 진행하다보면 대부분 MyBatis나 JPA 등을 통해 DB를 사용할 것입니다. 하지만 이들은 모두 내부적으로 JDBC를 사용하여 구현이 되어있습니다. 고수준의 라이브러리를 잘 사용하는 것도 좋지만 그 라이브러리들의 동작을 더 잘 이해하기 위해서는 내부에서 사용하는 저수준의 라이브러리에 대한 이해도 필요합니다. 그래서 JDBC에 대해 이해를 해보고 그 사용법을 알아봅시다!

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
spring-boot-starter-jdbc: Spring Boot에서 JDBC를 사용하기 위한 라이브러리.com.h2database:h2: H2는 Java 기반의 인메모리 DB로 로컬 개발 및 간단한 테스트에 적합함.spring-boot-starter-web: JDBC를 사용하는데는 필요하지 않지만 H2 console을 사용하기 위해 추가.spring:
datasource:
driver-class-name: org.h2.Driver
url: 'jdbc:h2:mem:test;DB_CLOSE_DELAY=-1'
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jdbc:h2:mem:test에서 mem은 메모리고 test는 DB 이름임. 즉, H2 인메모리에 test라는 DB와 연결할 것이라는 뜻임. DB_CLOSE_DELAY는 연결 후 일정 시간 동안 사용하지 않으면 연결이 종료되는데 -1을 주어 연결이 종료되지 않도록 함.JDBC를 직접 사용해보기 전에 그 구성에 대해 알아봅시다.

getConnection(): DriverManager는 DB에 맞는 JDBC Driver를 통해 DB와 연결을 맺음.createStatement(): SQL 구문을 실행하는 역할을 하는 Statement 클래스 객체를 만듬.executeUpdate(): INSERT, UPDATE, DELETE 쿼리를 수행함.executeQuery(): SELECT 쿼리를 수행해서 그 결과를 ResultSet에 담아 반환함.close(): Statement는 DB의 커넥션 풀을 사용함. 그래서 사용 후 닫아주지 않으면 커넥션 풀을 지속적으로 점유함.위 함수들을 이용하여 우리는 다음과 같은 코드를 작성할 수 있습니다.
// 데이터베이스 연결 정보
String url = "jdbc:h2:mem:test";
String username = "sa";
// connection 얻어오기
// try-with-resources: try 괄호 안에 초기화한 AutoCloseable 구현체를 try 블록이 끝나면 자동으로 close 해줌.
try (Connection connection = DriverManager.getConnection(url, username, null)) {
try {
// 테이블 생성
String createSql = "CREATE TABLE USERS (id SERIAL, username varchar(255))";
try (Statement statement = connection.createStatement()) {
statement.execute(createSql);
}
// 데이터 추가
String insertSql = "INSERT INTO USERS (username) VALUES ('sinryuji')";
try (Statement statement = connection.createStatement()) {
statement.executeUpdate(insertSql);
}
// 데이터 조회
String selectSql = "SELECT * FROM USERS";
try (Statement statement = connection.createStatement()) {
ResultSet rs = statement.executeQuery(selectSql);
while (rs.next()) {
System.out.printf("%d. %s", rs.getInt("id"), rs.getString("username"));
}
}
} catch (SQLException e) {
e.printStackTrace();
throw e;
}
}
우선 DriverManger의 getConnection()을 통해 DB와의 연결을 얻어옵니다. 그 후 String으로 SQL을 작성을 해주고 createStatement로 Statement 객체를 생성 한 후 execute()를 호출 할 때 해당 SQL을 전달해주면 됩니다.
위 코드를 실행하면 다음과 같이 INSERT한 row를 잘 SELECT 해오는 것을 확인할 수 있고,

다음과 같이 H2 console에서도 확인을 할 수 있습니다!

하지만 이 방법에는 치명적인 단점이 존재합니다. 그건 쿼리를 수행할 때마다 SQL문을 작성을 해야 한다는 점입니다. 만약 SELECT * FROM USERS WHERE id = 1와 같이 특정 id를 가진 유저를 조회해와야 한다고 해봅시다. 그러면 id값이 달라질때마다 새로운 SQL문을 작성을 해야 할 것입니다. 이를 해결하기 위해 PreparedStatemnt라는 클래스가 존재합니다.

Statement는 위와 같이 4가지 순서로 동작을 합니다. 그에 반면 PreparedStatement는 첫 번째 과정의 결과를 캐싱해놓고 나머지 3단계만을 거쳐 SQL문을 실행할 수 있게 합니다. 즉, SQL에 사용되는 값을 입력받는 부분부터 시작해서 변수를 입력받아 동적으로 SQL문을 실행 할 수 있께 합니다.
// 데이터 조회
String selectSql = "SELECT * FROM USERS WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
statement.setInt(1, 1);
ResultSet rs = statement.executeQuery();
while (rs.next()) {
System.out.printf("%d. %s", rs.getInt("id"), rs.getString("username"));
}
}
앞선 코드의 SELECT 부분만 PreparedStatement를 사용하도록 수정해보았습니다. 원래라면 WHERE절의 id 값이 달라질때마다 새로운 SQL String을 만들고 그를 통해 Statement 객체를 초기화를 해야 했겠지만, PreparedStatement를 사용하면 위와 같이 객체를 초기화를 해놓고 그 후 setInt()나 setString()을 통해 값을 세팅해줄 수 있습니다.