JDBC란
Java Database Connectivity
자바에서 데이터베이스에 접속할 수 있도록 도와주는 자바 API이다.
JDBC의 등장 배경
기존에 JDBC가 없던 시절 DB마다 애플케이션과 연결되는 코드가 달랐기 때문에 DB를 다른 종류로 변경하면 DB안에 있는 코드도 변경해야 했었으며, 연결하는 방법들이 전부 다 달라서 새로 학습을 해야 했었다.
이런 문제를 해결하기 위해 JDBC가 등장하게 됐다.
JDBC 표준 인터페이스
표준 인터페이스를 사용하여 Connection, Statement , ResultSet만을 사용해서 개발을 할수 있다.
해당 인터페이스를 DB에 맞도록 구현하여 라이브러리로 제공하는 이것을 JDBC 드라이버라고 한다. MYSQL DB에 접근할 수 있는 것을 MySQL JDBC 드라이버, Oracle DB에 접근할 수 있는 것을 Oracle JDBC 드라이버라고 한다.
따라서 DB를 다른 종류의 DB로 변경한다면 JDBC 구현 라이브러리만 변경하면 된다.
JDBC의 기술
JDBC를 편리하게 사용할 수 있는 기술이 발전되었고 그중 대표적으로 SQL Mapper와 ORM 기술이 있다.
- SQL Mapper
- 장점 : JDBC의 반복 코드를 줄여주고, SQL 응답 결과를 편리하게 변환해준다.
- 단점: SQL 언어를 직접 작성해야 한다.
- 대표기술 : 스프링 JDBC Template, MyBatis
- ORM
- 장점 : SQL 언어를 작성하지 않아 개발 생산성이 매우 높아진다.
- 단점 : 학습 난이도가 높다.
- 대표기술 : JPA, 하이버네이트
JDBC 설정
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
}
DB 연결
애플리케이션과 H2 DB를 연결
1.기본 정보 입력
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 = "";
}
2.실제 DB 연결
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.getConntection() : 라이브러리에 있는 DB 드라이버를 찾아 해당하는 커넥션을 반환해주는 함수
DriverManager 커넥션의 요청 흐름
DriverManager은 라이브러리에 등록된 DB 드라이버를을 관리하고, 커넥션을 제공하는 기능을 한다.
JDBC 개발
등록
member를 받아 member 테이블에 저장하는 메서드
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);
}
}
- getConnection() : DB 커넥션 획득 , 전에 만들어놓은 Util 함수 호출
- prepareStatement(sql) : DB에 전달할 SQL과 파라미터 데이터를 전달
- excuteUpdate() : Statement를 통해 준비된 SQL을 실제 DB에 전달하여 적용, 반환 값은 DB row 수
리소스 정리
쿼리를 실행하고 나면 리소스를 반드시 정리해야 한다. 리소스 정리는 항상 역순으로 적용해야 한다.
예외가 발생하든, 하지 않든 항상 수행되야 하므로 finally 구문에 작성한다.
조회
member 테이블에 저장한 데이터를 조회하는 메서드
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);}}
- executeQuery() : 데이터를 조회할 때 사용하며, 결과를 ResultSet에 담아서 반환한다.
- ResultSet은 커서를 통해 데이터를 조회하며 커서는 rs.next()를 통해 다음으로 옮겨진다.
DB 커넥션
커넥션을 얻는 방법으로는 JDBC DriverManager이나 커넥션 풀을 사용하는 방법들이 있다.
커넥션을 획득하는 방법
방법은 여러가지가 있지만 기존 커넥션을 얻는 방법을 다른 커넥션을 얻는 방법으로 바꿀 때는 많은 코드들을 변경해야 하는 불편함이 있다.
커넥션을 획득하는 방법을 추상화
그리하여 DataSource 인터페이스를 통해 커넥션을 획득하는 방법을 추상화하였다. 그리하여 각 커넥션을 얻는 방법에 직접 의존하는 것이 아닌 DataSource에만 의존하도록 애플리케이션 로직을 작성하면 된다. 이렇게 되면 커넥션 풀 구현 기술을 변경할 때 해당 구현체만 갈아끼우면 된다.
DriverManger를 DB 커넥션을 얻을 때
Driver Manager은 커넥션을 획득할 때마다 URL, USERNAME, PASSWORD 와 같이 같은 파라미터를 반복적으로 전달한다.
void drierManager() throws SQLException {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection connection2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={} class={}",connection,connection.getClass());
log.info("connection2={} class2={}",connection2,connection2.getClass());
}
DataSource를 통해 DB 커넥션을 얻을 때
반면에 DataSource를 사용하면 처음 객체를 생성할 때만 파라미터를 넘겨주고, 이후 단순하게 메서드를 호출하였다.
void dataSourceDriverManager() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
이렇게 설정하는 부분과 사용하는 부분을 분리하게 되면 DataSource를 사용하는 곳에서는 getConnection()를 호출만 하면된다. 이러한 방법은 모듈화나 테스트에 효율적이며 설정은 한 곳에서 하지만 사용은 수많은 곳에서 하기 때문에 애플리케이션 개발에도 용이 하다.
커넥션풀
기존 DB 커넥션을 획득하는 과정은 항상 새로 커넥션을 생성하기 때문에시간과 비용이 많이 소요된다. 이런 문제를 해결하기 위해 커넥션 풀을 사용한다.
커넥션 풀이란 커넥션을 미리 생성해두고 사용하고 ,사용한 이후 커넥션을 반환하는 기법이다.
기존 데이터베이스 커넥션을 획득 하는 과정
요청 시마다 DB 커넥션을 생성하여 반환한다.
커넥션 풀을 사용하여 데이터베이스 커넥션을 획득하는 과정
커넥션을 미리 생성해둔 이후, 단순 조회를 통해 커넥션을 반환한다.
커넥션 풀에서 얻는 과정은 매우 효율적이기 때문에 실무에서는 기본으로 사용하며, 주로 HikariCP 커넥션 풀 오픈소스를 사용한다.
HikariCP 커넥션 풀
항상 새로운 커넥션을 획득하는 DriverManagerDataSource와 달리 HikariDataSource는 커넥션 풀을 사용하여 커넥션을 사용하고 반환한다.
HikariCP 커넥션 풀 사용
커넥션 풀에서 커넥션을 생성하는 과정은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다.
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); //커넥션 풀에서 커넥션 생성 시간 대기
}
JDBCUtils 메서드
스프링에서 JDBC를 편리하게 다루게 해주는 메서드를 지원해준다. 해당 메서드를 통해 커넥션을 편리하게 닫을 수 있다.
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
트랜잭션
트랜잭션은 거래라는 뜻을 지니고 있다. 뜻처럼 데이터베이스에서 트랜잭션을 거래를 안전하게 처리할 수 있도록 보장해주는 것을 의미한다. 각 작업에서 성공하여 DB에 적용하는 행위를 Commit , 작업에 실패하여 거래 이전으로 돌리는 행위를 Rollback이라고 한다.
트랜잭션 ACID
- Atomicity 원자성 : 실행한 작업은 모두 성공하거나 모두 실패해야 한다.
- Consistency 일관성 : 일관성있는 DB를 유지해야 한다.
- Isolation 격리성 : 동시에 실행되는 트랜잭션들이 서로에게 영향이 미치면 안된다.
- Durability 지속성 : 결과는 항상 기록돼야 한다.
트랜잭션의 사용법
쿼리 실행 후, 결과를 반영하려면 Commit을, 반영하지 않고 실행 이전으로 돌아가려면 Rollback을 호출하면 된다.
즉 커밋을 호출하기 전까지는 임시로 데이터를 저장하며 결과에 따라 Commit이나 Rollback을 호출하면 된다.
트랜잭션 사용시 서로 다른 각 세션에서는 서로의 적용한 결과를 Commit 전까지 알 수 없다. 트랜잭션을 사용하기 위해서는 수동 커밋 모드를 설정해야 하며,수동 커밋을 사용하면 꼭 Commit이나 rollback을 호출해야한다.
set autocommit false; //수동 커밋 모드 설정
* 트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다.
DB 락
쿼리를 실행하고 커밋을 호출하지 않았는데 같은 데이터를 수정하려고 하면 문제가 발생한다. 이를 방지하고자 DB 락을 사용하며 DB 락은 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 Commit 이나 Rollback 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막는 것을 의미한다.
DB 락 방법
1. 락 타임 아웃 : n초 동안 대기를 한다. n초 이후에도 락을 얻지 못하면 락 타임아웃 오류가 발생
SET LOCK_TIMEOUT <milliseconds>;
2. 조회 시점 락 : 트랜잭션 종료 시점까지 다른 세션에서 해당 데이터를 변경하지 못하게 강제로 막을 때 사용
select ... for update;
트랜잭션 적용 V1
트랜잭션은 비지니스 로직이 있는 부분에서 시작해야하며, DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야한다.
가장 단순한 방법으로는 커넥션을 파라미터로 넘겨주는 방법이 있다.
트랜잭션 적용 V1 코드
같은 커넥션을 사용하고, DB 트랜잭션을 적용하기 위해서는 수많은 코드를 요구하기 때문에 코드가 난잡해진다.
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); //트랜잭션 시작
//비즈니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); //성공시 커밋
} catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
} }
- setAutoCommit(false) : 트랜잭션의 시작 (수동 커밋 모드)
- commit() : 성공 시 커밋을 수행
- rollback() 실패 시 롤백을 수행
트랜잭션 V1의 문제점
애플리케이션의 가장 단순한 구조는 역할에 따라 3가지 계층으로 나누는 구조이다.
애플리케이션 구조
- 프로젠테이션 계층 : UI와 관련된 처리 담당 (웹 요청, 응답)
- 서비스 계층 : 비지니스 로직 처리 담당 (특정 기술에 의존하지않은 순수 자바 코드)
- 데이터 접근 계층 : 데이터를 저장하고 관리하는 기술을 담당
로 구성되어 있다. 여기서 가장 중요한 부분은 서비스 계층으로 UI 관련처리 부분이 바뀌더라도, DB 접근 처리 기술이 바뀌더라도 비지니스 로직부분은 최대한 변경없이 유지돼야 한다. 즉 서비스 계층은 비지니스 로직만 구현하고 특정 기술에 의존되면 안된다.
트랜잭션 적용 V1 문제점
- 트랜잭션을 사용하기위해 서비스 계층에서 DataSource, Connection, SQL 같은 JDBC 기술에 의존하고 있다.
- 트랜잭션 동기화를 위해서 커넥션을 파라미터로 넘겨주고 있다.
- try, catch, finally가 중복되고 있다.
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); //트랜잭션 시작
bizLogic(con, fromId, toId, money); //비즈니스 로직
con.commit(); //성공시 커밋
}catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
}finally {
release(con);
}}
트랜잭션 적용 V2 - 트랜잭션 추상화
JDBC 기술을 사용하다가 JPA로 기술을 변경하게 되면 많은 코드들을 변경해야 한다. 그리하여 커넥션을 획득하는 방법을 추상화한 것처럼 트랜잭션을 추상화할 수 있다.
트랜잭션 추상화 인터페이스
TxManager 인터페이스를 기반으로 각 기술에 맞는 구현체를 만들면 된다.
public interface TxManager {
begin();
commit();
rollback();
}
스프링의 트랜잭션 추상화
스프링에서는 TxManger 인터페이스보다 성능이 좋은 PlatformTransactionManager을 지원해준다.
PlatformTransactionManager 인터페이스
package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
- getTransaction() : 트랜잭션 시작
- commt(), rollback() : 커밋, 롤백 실행
트랜잭션 매니저와 트랜잭션 동기화 매니저
PlatformTransactionManager을 통해 트랜잭션 동기화 매니저를 제공받으며 이것은 쓰레드 로컬을 사용하여 멀티쓰레드인 상황에서 안전하게 커넥션 동기화를 할수 있다. 따라서 이전처럼 파라미로 커넥션을 전달하지 않아도 된다.
- DriverManagerDataSource를 통해 데이터소스를 얻는다.
- 데이터 소스를 통해 커넥션을 만들고 트랜잭션을 시작한다.
- 트랜잭션 매니저가 트랜잭션을 시작한 커넥션을 동기화 매니저에 보관한다.
- 보관된 커넥션을 트랜잭션 동기화 매니저에서 꺼내서 사용한다.
* PlatformTransactionManager의 구현체가 트랜잭션 매니저이다.
트랜잭션 추상화 적용 코드
ex 1) MemberRepository
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
- DataSourceUtils.releaseConnection() : 트랜잭션 동기화 매니저가 관리하는 커넥션이 있는 경우 커넥션을 유지해주고, 없으면 커넥션을 닫는다.
private Connection getConnection() throws SQLException {
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={} class={}", con, con.getClass());
return con;
}
- DataSourceUtils.getConnection() : 트랜잭션 동기화 매니지가 관리하는 커넥션이 있으면 커넥션을 반환하고, 없으면 새로운 커넥션을 생성한다.
ex 2) MemberService
public class MemberService{
private final PlatformTransactionManager transactionManager;
private final MemberRepository memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션 시작
TransactionStatus status =
transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromId, toId, money); //비즈니스 로직
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
- TransactionStatus : 현재 트랜잭션의 상태 정보
- DefalutTransactionDefinition() : 트랜잭션과 관련된 옵션 지정
- transactionManager.commit(), rollback() : 커밋, 롤백 실행
트랜잭션 V2 정리
- 트랜잭션 추상화를 통해 서비스 코드는 JDBC 기술에 의존하지 않으며 JPA 기술로 쉽게 변경이 가능하다.
- 트랜잭션 동기화 매니저를 통해 커넥션을 파리미터로 넘겨주지 않아도 된다.
트랜잭션 적용 V3 - 트랜잭션 패턴 반복 제거
트랜잭션 사용시 트랜잭션 시작 코드와 try, catch, finally, commit과 rollback 부분이 반복되는 것을 확인할 수 있다.
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
템플릿 콜백 패턴
TransactionTemplate를 통해 트랜잭션의 반복되는 코드를 제거할 수 있다. (트랜잭션 시작, commit, rollback 코드 제거)
txTemplate.executeWithoutResult((status) -> {
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
TransactionTemplate를 사용하려면 TransactionManager가 필요하다.
public MemberService(PlatformTransactionManager tm, MemberRepository memberRepository) {
this.txTemplate = new TransactionTemplate(tm);
this.memberRepository = memberRepository;
}
TransactionTemplate 인터페이스
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action){..}
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
- execute() : 응답 값이 있을 때 사용
- executeWithoutResult() : 응답 값이 없을 때 사용
트랜잭션 V3 정리
트랜잭션 템플릿을 통해 반복되는 코드를 제거하였다.
트랜잭션 적용 V4 - 트랜잭션 AOP
서비스 로직에는 가급적이면 비지니스 로직을 처리하는 부분으로만 구성되어야 한다. 하지만 아직도 트랜잭션을 처리하는 코드들이 서비스 로직에 포함되어 있다. 서비스 계층에 순수한 비지니스 로직만 남길려면 스프링 AOP를 통해 프록시를 도입하면 된다.
프록시 도입 전
서비스의 로직에서 트랜잭션을 직접 시작한다. 서비스에 비지니스 로직과 트랜잭션 로직이 함께 섞여있다.
프록시 도입 후
트랜잭션을 처리하는 객체와 비지니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다. 트랜잭션 프록시가 트랜잭션 처리 코드를 모두 가져간다. 트랜잭션을 시작한 후에 실제 서비스를 호출하여 서비스 계층에는 순수한 비지니스 로직처리 부분만 남겨진다.
트랜잭션 AOP
스프링이 제공하는 AOP를 사용하면 프록시를 매우 편리하게 사용할 수 있다. 트랜잭션을 사용할 곳에 @Transactional만 붙여주면 트랙잰션 프록시가 적용된다.
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
* 스프링 AOP를 사용하기 위해서는 스프링 컨테이너가 필요하며 필요한 스프링 빈을 등록해야 한다.
트랜잭션 AOP 적용 흐름
트랜잭션 V4 정리
트랜잭션 AOP를 사용하여 트랜잭션 관련 코드를 제거하여 서비스계층에는 순수한 비지니스 로직처리만 남게됐다.
스프링 부트 자동 리소스 등록
스프링 부트에서는 application.properties 에 지정된 속성을 참고하여 데이터 소스와 트랜잭션 매니저를 자동으로 등록해준다.
데이터 소스 자동 등록
DataSource를 스프링 빈에 자동으로 등록해준다. 자동으로 등록되는 이름은 dataSource이다.
application.properties에서 입력하며 기본으로 생성되는 데이터소스는 커넥션풀을 제공하는 HikariDataSource이다.
spring.datasource.url=...
spring.datasource.username=...
spring.datasource.password=..
트랜잭션 매니저 자동 등록
적절한 PlatformTransactionManager를 자동으로 스프링 빈에 등록해준다. 자동으로 등록되는 이름은 transactionManager이다.
자바 예외 처리
예외 기본 규칙은 예외를 잡아서 처리하거나, 밖으로 던지거나 둘 중 하나이다.
스프링 DB 1편 - 자바 예외 처리
자바 예외 계층 Object : 모든 객체의 최상위 부모는 Object이다. Throwable : 최상위 예외이다. Error : 애플리케이션에서 복구 불가능한 시스템 예외이므로, 해당 예외를 잡으려고 하면 안된다. Exception :
mjcoding.tistory.com
예외 처리 적용 V1 - 런타임 에러 변환
트랙잭션V1 ~ V5를 통해 서비스 계층을 최대한 순수하게 만들었었다. 하지만 아직 서비스 계층에는 SQLException가 존재하며 예외 처리에 대한 의존 관계를 제거해야만 한다.
1. 인터페이스 도입
MemberRepository 인터페이스를 도입하여 구현 기술을 쉽게 변경할 수 있게 한다.
SQLException은 체크예외이기 때문에 이렇게 인터페이스를 도입하려면 인터페이스 메서드에서 예외를 던지는 부분이 포함되어야 한다. 그렇게 하면 인터페이스부터 특정 구현 기술에 종속되어 버린다.
2. 런타임 에러 변환 도입
런타임 에러 변환을 도입한다. 런타임 에러 도입을 통해 thows SQLException을 제거한다.
또한 언체크 예외이기 때문에 인터페이스 메서드에서 예외를 던지지 않아도 된다.
MemberRepositoryV1 클래스
MemberRepository를 상속받아 MemberService 계층에서 구현 기술을 쉽게 변경할 수 있게 한다.
public class MemberRepositoryV1 implements MemberRepository {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
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) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
- SQLException 체크 예외를 MyDbException 런타임 에러로 변환해서 던졌다.
- 예외를 변환할 때는 반드시 기존 예외를 포함시켜야한다.
MyDbException 클래스
RuntimeException을 상속받았으며 해당 클래스는 런타임 예외가 된다.
public class MyDbException extends RuntimeException {
public MyDbException() {
}
public MyDbException(String message) {
super(message);
}
public MyDbException(String message, Throwable cause) {
super(message, cause);
}
public MyDbException(Throwable cause) {
super(cause);
}
}
예외처리 V1 정리
- 체크 예외를 런타임 예외로 변환하면서 인터페이스와 서비스 계층의 순수성을 유지할 수 있게 되었다.
- JDBC에서 다른 구현 기술로 변경하더라도 서비스 계층의 코드를 변경하지 않아도 된다.
예외처리 적용 V2 - 데이터 접근 예외
DB에서 오류가 발생했을 때 오류 코드를 반환한다. 즉 SQLException에는 DB가 제공하는 errorCode가 들어가있다.
문제는 같은 오류여도 각 DB마다 오류 코드가 다르다는 점이다. 또한 오류 코드를 활용하려면 SQLException을 서비스 계층으로 던져야 하기 때문에 서비스 계층의 순수성이 무너진다.
스프링 예외 추상화
스프링은 데이터 접근 계층에 대한 예외들을 정리해서 일관된 예외 계층을 제공한다. 각각의 예외는 특정 기술에 종속적이지 않아 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다.
- NonTransient : 일시적이지 않는 뜻으로 같은 SQL문을 반복해도 같은 오류가 발생하는 예외이다.
- Transient : 일시적이라는 뜻으로 같은 SQL문을 다시 실행 시 성공할 가능성이 있는 예외이다.
스프링이 제공하는 예외 변환기
void exceptionTranslator() {
String sql = "select bad grammar";
try {
Connection con = dataSource.getConnection();
PreparedStatement stmt = con.prepareStatement(sql);
stmt.executeQuery();
} catch (SQLException e) {
assertThat(e.getErrorCode()).isEqualTo(42122);
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
log.info("resultEx", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
- SQLExceptionTranslator : SQL 예외를 특정 DB에 대한 예외로 번역해주는 인터페이스
- SQLErrorCodeSQLExceptionTranslator : SQLExceptionTranslator 인터페이스를 구현한 클래스로 SQLException의 ErrorCode에 맞는 예외로 변환해준다.
- translate() : 적절한 스프링 데이터 접근 계층의 예외로 변환해준다.
- 파라미터 : 1: 설명 / 2: 실행한 SQL / 3: 마지막에 발생된 SQLException
MemberRepositoryV2 클래스
서비스나 컨트롤러에서 예외 처리가 필요하면 특정 기술에 종속적인 예외를 직접 사용하는 것이 아닌 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.
public class MemberRepositoryV2 implements MemberRepository {
private final DataSource dataSource;
private final SQLExceptionTranslator exTranslator;
public MemberRepositoryV2(DataSource dataSource) {
this.dataSource = dataSource;
this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
@Override
public Member save(Member member) {
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) {
throw exTranslator.translate("save", sql, e);
} finally {
close(con, pstmt, null);
}
}
예외처리 V2 정리
- 스프링 예외 추상화로 인해 특정 기술에 종속적이지 않게 되었다.
- JDBC에서 JPA로 기술을 변경하더라도 예외로 인한 변경을 최소화할 수 있다.
예외 처리 적용 V3 - JDBC Template
지금까지 서비스의 순수함을 유지하기 위해 수 많은 기능을 추가하였다. 하지만 코드들을 확인해보면 같은 부분들이 반복되는 것을 확인할 수 있다.
- 커넥션 조회, 커넥션 동기화
- PrepaedStatement 생성 및 파라미터 바인딩
- 쿼리 실행, 결과 바인딩
- 예외 발생시 스프링 예외 변환기 실행
- 리소스 종료
해당 부분들이 반복되었고 이를 처리하는 방법이 바로 JDBC Template다. 해당 템플릿을 통해 반복 코드들을 모두 제거할 수 있다.
기존 save 함수
public Member save(Member member){
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) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
MemberRepositoryV3 클래스
생성자에 JDBCTemplate를 등록하면 된다. MemberRepositoryV2에 비해 코드가 매우 간결해졌다.
public class MemberRepositoryV3 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV3(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
예외처리 V3 정리
JDBCTemplate을 통해 JDBC로 개발할 때 발생하는 대부분의 반복 코드들을 제거해준다.
출처 : 인프런 - 스프링 DB 1편 feat.김영한 쌤
'웹 개발' 카테고리의 다른 글
스프링 DB 1편 - 자바 예외 처리 (0) | 2024.03.04 |
---|---|
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (2) | 2024.02.27 |
스프링 MVC 2편 - 타임리프 (0) | 2024.02.23 |
스프링 MVC 1편 (0) | 2024.01.23 |
HTTP 웹 기본 지식 (1) | 2024.01.05 |