[인프런/스프링 DB 1편] 6. 스프링과 문제 해결 - 예외 처리, 반복
트랜잭션 적용 시, 예외 누수 문제 해결
1. 체크 예외와 인터페이스
현재 각 Repository 객체의 반복
save(), findById(), update(), delete()
-> Repository 인터페이스 도입
* MemberRepository 인터페이스
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
public interface MemberRepository {
Member save(Member member);
Member findById(String memberId);
void update(String memberId, int money);
void delete(String memberId);
}
-> 특정 기술에 종속되지 않는 순수한 인터페이스
-> MemberService는 MemberRepository 인터페이스에만 의존
-> 구현 기술을 변경하고 싶으면 DI를 사용해서 MemberService 코드 변경 없이 구현 기술 변경 가능
* 체크 예외 코드에 인터페이스를 도입 시 문제점
-> MemberRepository 인터페이스의 각 메소드에도 throws SQLException 붙여야 함
-> 그래야 구현 클래스의 메소드도 체크 예외를 던질 수 있음
-> 인터페이스가 특정 기술에 종속되어버림
-> 런타임 예외 인터페이스
2. 런타임 예외 적용
1) MemberRepository 인터페이스
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
public interface MemberRepository {
Member save(Member member);
Member findById(String memberId);
void update(String memberId, int money);
void delete(String memberId);
}
2) MyDbException 런타임 예외
- RuntimeException 을 상속받음 -> 런타임 예외
- 체크 예외를 잡아서 이 런타임 예외로 던짐
package hello.jdbc.repository.ex;
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);
}
}
3) MemberRepositoryV4_1
- MemberRepository 인터페이스 구현
- 체크 예외를 잡아서 런타임 예외로 변환해서 던짐 !
- 예외 변환 시 기존 예외를 반드시 포함 ! -> 그래야 원인이 되는 기존 예외를 함께 확인할 수 있음
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.ex.MyDbException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* 예외 누수 문제 해결
* 체크 예외를 런타임 예외로 변경 -> throws SQLException 제거
* MemberRepository 인터페이스 사용
*/
@Slf4j
public class MemberRepositoryV4_1 implements MemberRepository {
private final DataSource dataSource;
public MemberRepositoryV4_1(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null; // finally에서도 호출해야 하기 때문에 밖에 따로 선언
PreparedStatement pstmt = null;
try {
con = getConnection(); // 커넥션 획득 -> 메소드로 추출
pstmt = con.prepareStatement(sql); // sql에 파라미터 바인딩
pstmt.setString(1, member.getMemberId()); // sql의 첫번째 ?에 문자 저장
pstmt.setInt(2, member.getMoney()); // sql의 두번째 ?에 숫자 저장
pstmt.executeUpdate(); // 실행 (sql을 커넥션을 통해 데이터베이스에 전달)
return member;
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
@Override
public Member findById(String memberId) {
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) {
throw new MyDbException(e);
} finally {
close(con, pstmt, rs);
}
}
@Override
public void update(String memberId, int money) {
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);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
@Override
public void delete(String memberId) {
String sql = "delete member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() {
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. (트랜잭션 동기화 매니저에서 꺼냄)
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class{}", con, con.getClass());
return con;
}
}
4) MemberServiceV4
- 서비스가 인터페이스에 의존
- SQLException을 던지는 코드 제거 -> 다른 코드에 의존 X, 순수한 서비스 !
- JDBC에서 다른 구현 기술로 변경하더라도 서비스의 코드 변경 X
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
/**
* 예외 누수 문제 해결
* SQLException 제거
* MemberRepository 인터페이스 의존
*/
@Slf4j
public class MemberServiceV4 {
private final MemberRepository memberRepository;
public MemberServiceV4(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
5) 테스트
- 인터페이스 사용
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepository;
import hello.jdbc.repository.MemberRepositoryV3;
import hello.jdbc.repository.MemberRepositoryV4_1;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 예외 누수 문제 해결
* SQLException 제거
* MemberRepository 인터페이스 의존
*/
@Slf4j
@SpringBootTest
class MemberServiceV4Test {
// 상수 설정
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
@Autowired
private MemberRepository memberRepository;
@Autowired
private MemberServiceV4 memberService;
@AfterEach
void after() {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@TestConfiguration
static class TestConfig {
private DataSource dataSource;
public TestConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
MemberRepository memberRepository() {
return new MemberRepositoryV4_1(dataSource);
}
@Bean
MemberServiceV4 memberServiceV4() {
return new MemberServiceV4(memberRepository());
}
}
@Test
void AopCheck() {
log.info("memberService class={}", memberService.getClass());
log.info("memberRepository class={}", memberRepository.getClass());
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
}
@Test
@DisplayName("정상 이체")
void accountTransfer() {
// given 이렇게 주어졌을 때
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
// when 이렇게 하면
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
// then 이렇게 나와야 함
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
@Test
@DisplayName("이체중 예외 발생")
void accountTransferEx() {
// given 이렇게 주어졌을 때
Member memberA = new Member(MEMBER_A, 10000);
Member memberEx = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
// when (예외가 던져져야 함)
assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
// then (A의 잔고는 줄고, EX의 잔고는 그대로)
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(10000);
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
}
3. 데이터 접근 예외 직접 만들기
* 특정 상황에서 예외를 잡아서 복구하고 싶을 때 예외를 구분해서 처리하기
ex) 회원 가입시 이미 같은 ID가 DB에 저장되어 있다면(예외), ID 뒤에 숫자를 붙여 새로운 ID를 만들어 가입시키기
이미 같은 ID가 있음
-> 데이터베이스에서 오류코드 반환
-> JDBC 드라이버는 안에 오류코드를 포함한 SQLException을 던짐
-> SQLException 내부의 errorCode 를 활용하면 어떤 문제가 발생했는지 확인할 수 있음
-> 근데, 그러면 SQLException을 던져야 함
-> 서비스 계층에서 다시 SQLException이라는 JDBC 기술에 의존하게 됨
-> 리포지토리에서 예외 변환해서 던짐 !
- 같은 오류여도 데이터베이스 별로 오류 코드가 다름
- H2 데이터베이스에서 키 중복 오류 : 23505
1) MyDuplicateKeyException 예외 생성
- 기존에 생성했던 MyDbException을 상속받음
- 데이터 중복 시에만 던지는 예외
- 우리가 직접 만든 예외이기 때문에 JDBC, JPA 같은 특정 기술에 종속 X -> 서비스 계층의 순수성
package hello.jdbc.repository.ex;
public class MyDuplicateKeyException extends MyDbException {
public MyDuplicateKeyException() {
}
public MyDuplicateKeyException(String message) {
super(message);
}
public MyDuplicateKeyException(String message, Throwable cause) {
super(message, cause);
}
public MyDuplicateKeyException(Throwable cause) {
super(cause);
}
}
2) 테스트
package hello.jdbc.exception.translator;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.ex.MyDbException;
import hello.jdbc.repository.ex.MyDuplicateKeyException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Random;
import static hello.jdbc.connection.ConnectionConst.*;
public class ExTranslatorV1test {
Repository repository;
Service service;
@BeforeEach
void init() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
repository = new Repository(dataSource);
service = new Service(repository);
}
@Test
void duplicateKeySave() {
service.create("myId");
service.create("myId"); // 같은 ID 저장 시도
}
@Slf4j
@RequiredArgsConstructor
static class Service {
private final Repository repository;
public void create(String memberId) {
try {
repository.save(new Member(memberId, 0));
log.info("saveId={}", memberId);
} catch (MyDuplicateKeyException e) {
log.info("키 중복, 복구 시도");
// 새로운 키 발급
String retryId = generateNewId(memberId);
log.info("retryId={}", retryId);
repository.save(new Member(retryId, 0));
}
}
private String generateNewId(String memberId) {
return memberId + new Random().nextInt(10000);
}
}
@RequiredArgsConstructor
static class Repository {
private final DataSource dataSource;
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = dataSource.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
// h2 db
if (e.getErrorCode() == 23505) {
throw new MyDuplicateKeyException(e);
}
throw new MyDbException(e);
} finally {
JdbcUtils.closeStatement(pstmt);
JdbcUtils.closeConnection(con);
}
}
}
}
* 리포지토리
} catch (SQLException e) {
// h2 db
if (e.getErrorCode() == 23505) {
throw new MyDuplicateKeyException(e);
}
throw new MyDbException(e);
}
- 오류코드가 키 중복 오류(23505)인 경우 MyDuplicateKeyException을 서비스 계층으로 던짐
- 다른 오류인 경우 기존에 만들었던 MyDbException 던짐
* 서비스
try {
repository.save(new Member(memberId, 0));
log.info("saveId={}", memberId);
} catch (MyDuplicateKeyException e) {
log.info("키 중복, 복구 시도");
// 새로운 키 발급
String retryId = generateNewId(memberId);
log.info("retryId={}", retryId);
repository.save(new Member(retryId, 0));
}
- 저장 시도 -> 리포지토리에서 MyDuplicateKeyException 예외가 올라오면 예외 잡음
- 새로운 아이디 생성 후 저장 (예외 복구)
* 남은 문제
- 데이터베이스마다 SQL ErrorCode가 다름
-> 데이터베이스가 변경될 때마다 ErrorCode도 모두 변경해야 함
4. 스프링의 예외 추상화
4-1. 이해
1) 스프링 데이터 접근 예외 계층
- 스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층 제공
- 각 예외는 특정 기술에 종속 X -> 스프링이 제공하는 이 예외들을 사용할 것
- JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해주는 역할도 스프링이 제공
- 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외
- 예외의 최고 상위인 DataAccessException은 2가지로 구분
- Transient : 일시적
- 동일한 SQL을 다시 시도했을 때 성공할 가능성 있음 (타임아웃, 락 관련 오류 등)
- 동일한 SQL을 다시 시도했을 때 성공할 가능성 있음 (타임아웃, 락 관련 오류 등)
- NonTransient : 일시적 X
- 같은 SQL을 그대로 반복해서 실행하면 실패 (SQL 문법 오류 등)
- 같은 SQL을 그대로 반복해서 실행하면 실패 (SQL 문법 오류 등)
- Transient : 일시적
2) 스프링이 제공하는 예외 변환기
데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동 변환
(1) 이전 - 에러코드를 직접 확인
- 오류 코드 확인 -> 스프링의 예외 체계에 맞춰 예외 직접 변환
- 데이터베이스마다 오류 코드 다름
@Slf4j
public class SpringExceptionTranslatorTest {
DataSource dataSource;
@BeforeEach
void init() {
dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Test
void sqlExceptionErrorCode() {
String sql = "select bad grammer";
try {
Connection con = dataSource.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.executeQuery();
} catch (SQLException e) {
assertThat(e.getErrorCode()).isEqualTo(42122);
int errorCode = e.getErrorCode();
log.info("errorCode={}", errorCode);
log.info("error", e);
}
}
(2) 스프링의 예외 변환기
@Slf4j
public class SpringExceptionTranslatorTest {
DataSource dataSource;
@BeforeEach
void init() {
dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Test
void exceptionTranslator() {
String sql = "select bad grammer";
try {
Connection con = dataSource.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.executeQuery();
} catch (SQLException e) {
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
log.info("resultEx", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
}
- translate(읽을 수 있는 설명, 실행한 sql, 발생한 예외) -> 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환
- 위 예제에서는 문법이 잘못됨 -> BadSqlGrammarException 반환
- 반환 타입은 DataAccessException 이지만 이건 부모 타입으로 받기 위한 것
실제로는 BadSqlGrammarException 반환
- 반환 타입은 DataAccessException 이지만 이건 부모 타입으로 받기 위한 것
- 어떻게 각 DB가 제공하는 에러 코드까지 고려해서 예외를 변환하는지
* sql-error-codes.xml
<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
</property>
<property name="duplicateKeyCodes">
<value>23001,23505</value>
</property>
</bean>
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>1054,1064,1146</value>
</property>
<property name="duplicateKeyCodes">
<value>1062</value>
</property>
</bean>
* 정리
- 스프링은 데이터 접근 계층에 대한 일관된 예외 추상화를 제공
- 스프링은 예외 변환기를 통해서 SQLException의 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환
4-2. 스프링 예외 추상화 적용
* 리포지토리
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.ex.MyDbException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* SQlExceptionTranslator 추가
*/
@Slf4j
public class MemberRepositoryV4_2 implements MemberRepository {
private final DataSource dataSource;
private final SQLExceptionTranslator exTranslator;
public MemberRepositoryV4_2(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; // finally에서도 호출해야 하기 때문에 밖에 따로 선언
PreparedStatement pstmt = null;
try {
con = getConnection(); // 커넥션 획득 -> 메소드로 추출
pstmt = con.prepareStatement(sql); // sql에 파라미터 바인딩
pstmt.setString(1, member.getMemberId()); // sql의 첫번째 ?에 문자 저장
pstmt.setInt(2, member.getMoney()); // sql의 두번째 ?에 숫자 저장
pstmt.executeUpdate(); // 실행 (sql을 커넥션을 통해 데이터베이스에 전달)
return member;
} catch (SQLException e) {
throw exTranslator.translate("save", sql, e);
} finally {
close(con, pstmt, null);
}
}
@Override
public Member findById(String memberId) {
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) {
throw exTranslator.translate("findById", sql, e);
} finally {
close(con, pstmt, rs);
}
}
@Override
public void update(String memberId, int money) {
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);
pstmt.executeUpdate();
} catch (SQLException e) {
throw exTranslator.translate("update", sql, e);
} finally {
close(con, pstmt, null);
}
}
@Override
public void delete(String memberId) {
String sql = "delete member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw exTranslator.translate("delete", sql, e);
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() {
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. (트랜잭션 동기화 매니저에서 꺼냄)
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class{}", con, con.getClass());
return con;
}
}
* 테스트 - 리포지토리만 변경
@Bean
MemberRepository memberRepository() {
return new MemberRepositoryV4_2(dataSource);
}
* 정리
- 스프링의 예외 추상화 -> 서비스 계층이 특정 리포지토리의 구현 기술과 예외에 종속 X
- 서비스 계층에서 예외 복구할 때 -> 스프링이 제공하는 데이터 접근 예외로 변경되어서 넘어옴 -> 해당 예외 잡아서 복구
5. JDBC 반복 문제 -> JdbcTemplate 템플릿 (최종)
- 커넥션 조회, 커넥션 동기화
- preparedStatement 생성 및 파라미터 바인딩
- 쿼리 실행
- 결과 바인딩
- 예외 발생시 스프링 예외 변환기 실행
- 리소스 종료
* MemberRepositoryV5
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JdbcTemplate 사용
*/
@Slf4j
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values (?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
@Override
public Member findById(String memberId) {
String sql = "select * from member where member_id = ?";
return template.queryForObject(sql, memberRowMapper(), memberId); // 1개 조회
}
@Override
public void update(String memberId, int money) {
String sql = "update member set money=? where member_id=?";
template.update(sql, money, memberId);
}
@Override
public void delete(String memberId) {
String sql = "delete member where member_id=?";
template.update(sql, memberId);
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
};
}
}
* 테스트 - 리포지토리만 변경
@Bean
MemberRepository memberRepository() {
return new MemberRepositoryV5(dataSource);
}
📌 JdbcTemplate
- JDBC 반복 문제 해결
- 트랜잭션 커넥션 동기화
- 스프링 예외 변환기 실행