H2 데이터베이스 설정
개발이나 테스트 용도로 사용하기 좋은 가볍고 편리한 DB인 H2 데이터베이스를 설치한다.
테이블 생성하기
테이블 관리를 위해 프로젝트 루트에 sql/schema.sql 파일을 생성한다.
drop table member if exists cascade;
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
insert into member(member_id, money) values ('hi1', 10000);
insert into member(member_id, money) values ('hi2', 2000);
JDBC
애플리케이션 - DB간 데이터를 취급하는 과정에서, 각각의 데이터베이스마다 커넥션을 연결하는 방법, SQL을 전달하는 방법, 결과를 응답받는 방법이 수십개의 관계형 데이터베이스가 모두 다르다는 한계가 있다.
이런 문제를 해결하기 위해 JDBC라는 자바 표준이 등장한다.
JDBC 표준 인터페이스는 다음 3가지 기능을 정의해 제공한다.
* java.sql.Connection - 연결
* java.sql.Statement - SQL 전달
* java.sql.ResultSet - 결과 응답
이 JDBC 인터페이스를 각각의 DB 벤더에서 제공하는 라이브러리(=JDBC 드라이버)를 통해 DB에 전달한다.
JDBC의 등장으로 인해, DB간 사용 코드의 차이로 인해 발생하는 변경 지점이 그대로 유지할 수 있었고, 개발자는 표준 인터페이스 사용법만 학습해도 수십개의 데이터베이스에 동일하게 적용할 수 있게 되었다.
데이터베이스 연결
H2 데이터베이스 서버를 먼저 실행해두고, 애플리케이션과 데이터베이스를 연결해본다.
ConnnectionConst
package hello.jdbc.connection;
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
DBConnectionUtil
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class DBConnectionUtil {
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get connection={}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
*DriverManager.getConnection(..);
: 라이브러리에 있는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환한다.
JDBC 개발 - 등록
JDBC를 사용해서 회원(Member) 데이터를 데이터베이스에 관리하는 기능을 개발해보자.
schema.sql
drop table member if exists cascade;
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
Member
package hello.jdbc.domain;
import lombok.Data;
@Data
public class Member {
private String memberId;
private int money;
public Member() {
}
public Member(String memberId, int money) {
this.memberId = memberId;
this.money = money;
}
}
MemberRepositoryV0 - CRUD
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?,?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
*쿼리를 실행하고 나면 리소스 정리른 꼭 해주어야 한다. finally 구문에 1) ResultSet, 2) PreparedStatement, 3) Connection 순서로 종료한다. 그렇지 않으면 커넥션이 끊어지지 않고 계속 유지되는 리소스 누수가 발생해 장애가 발생할 수 있다.
데이터를 변경(save)할 때는 'executeUpdate()' 를 사용하지만,
데이터를 조회(find)할 때는 'executeQuery()' 를 사용한다.
MemberRepositoryTestV0
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static org.assertj.core.api.Assertions.*;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV0", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
assertThat(findMember).isEqualTo(member);
//update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
테스트 코드를 통해 테스트를 해보았다.
커넥션 풀 - 실무에서 기본으로 사용!
애플리케이션을 사용할때, SQL을 실행하는 시간뿐만 아니라 커넥션을 새로 만드는 시간이 추가된다. 결과적으로 응답 속도에 영향을 주고, 사용자에게 좋지 않은 경험을 줄 수 있다. 이런 문제를 해결하는 것이 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법이다.
커넥션 풀은 커넥션을 관리하는 수영장이다. 애플리케이션 시작 시점에 커넥션 풀에 필요한 만큼의 커넥션을 미리 확보해서 풀에 보관한다. 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
애플리케이션 로직은 DB 드라이버를 통해 새로운 커넥션을 획득하는 것이 아니라, 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓰면 된다. 커넥션을 모두 사용하고 나면 커넥션을 종료하는 것이 아니라, 살아있는 상태 그대로 커넥션 풀에 반환한다.
DataSource
DriverManager을 통해 커넥션을 신규로 생성해서 사용하다가, HikariCP 커넥션 풀과 같은 커넥션 풀에서 커넥션을 얻기 위해 DataSource라는 커넥션 획득 방법을 추상화한 인터페이스를 사용한다.
테스트 코드로 알아보자.
package hello.jdbc.connection;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class ConnectionTest {
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
@Test
void dataSourceDriverManager() throws SQLException {
//DriverManagerDataSource - 항상 새로운 커넥션을 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
}
DriverManager 는 커넥션을 획득할 때 마다 URL, USERNAME, PASSWORD 같은 파라미터를 계속 전달해야 한다. 반면 DataSource 를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파라미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection()만 호출하면 된다.
덕분에 객체를 설정하는 부분과 사용하는 부분을 명확하게 분리할 수 있다.
datasourceconnectionPool()
: 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다. 별도의 쓰레드에서 동작하기 때문에 테스트가 먼저 종료되어 버린다. 예제처럼 Thread.sleep() 을 통해 대기 시간을 주어야 쓰레드 풀에 커넥션이 생성되는 로그를 확인할 수 있다.
애플리케이션에 적용
MemberRepositoryV1
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JDBC - DataSource, JdbcUtils 사용
*/
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
//save()...
//findById()...
//update()....
//delete()....
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return DBConnectionUtil.getConnection();
}
}
JDBC를 편리하게 다룰 수 있는 JdbcUtils를 사용해서 커넥션을 닫아주었다.
'공부 > DB' 카테고리의 다른 글
| [Spring DB](6) 데이터 접근 기술 - JPA & 스프링 데이터 JPA & Querydsl (0) | 2024.02.28 |
|---|---|
| [Spring DB](5) 데이터 접근 기술 - MyBatis (0) | 2024.02.27 |
| [Spring DB](4) 데이터 접근 기술 - JDBC (0) | 2024.02.27 |
| [Spring DB](3) 자바 예외 (1) | 2024.02.26 |
| [Spring DB](2) 트랜잭션 (1) | 2024.02.26 |