Spring

[인프런/스프링 DB 1편] 6. 스프링과 문제 해결 - 예외 처리, 반복

주니어주니 2023. 6. 26. 23:57

 

트랜잭션 적용 시, 예외 누수 문제 해결

 

 

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을 다시 시도했을 때 성공할 가능성 있음 (타임아웃, 락 관련 오류 등)

    • NonTransient : 일시적 X
      • 같은 SQL을 그대로 반복해서 실행하면 실패 (SQL 문법 오류 등)

 

 

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 반환

  • 어떻게 각 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 반복 문제 해결 
- 트랜잭션 커넥션 동기화 
- 스프링 예외 변환기 실행