[Spring] 스프링 DB 1편 - 데이터 접근 핵심 원리 스프링 DB 1편 - 데이터 접근 핵심 원리 섹션5,6 예외 & 스프링과 문제 해결 - 예외 처리, 반복
⬜ 섹션5 예외 자바 예외 이해
◼️ 체크 예외 활용
기본적으로 언체크(런타임) 예외를 사용하자.
예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용하자.
🟢 체크 예외 문제점
- 복구 불가능한 예외: 대부분 예외는 복구가 불가능하다. 따라서 복구불가능한 예외는 오류 로그를 남기고, 스프링 ControllerAdvice를 사용해 깔끔히 공통으로 해결한다.
- 의존 관계에 대한 문제: 체크 예외는 처리할 수 없으면 어쩔 수 없이 throws를 통해 던지는 예외를 필수로 선언해야 한다.이때 JDBC→JPA로 바뀐다면 예외도 바뀌기 때문에 예외 의존성이 생긴다.
◼️ 언체크 예외 활용
🟢 런타임 예외
- 복구 불가능한 예외: 런타임 예외를 사용하면 서비스나 컨트롤러가 이런 복구 불가능한 예외를 신경쓰지 않아도 된다.
- 의존 관계에 대한 문제: 런타임 예외는 해당 객체가 처리할 수 없는 예외는 무시하면 된다. 따라서 체크 예외 처럼 예외를 강제로 의존하지 않아도 된다.
🟢 런타임 예외는 문서화
런타임 예외는 문서화를 잘하고, 코드에 throws 런타임예외를 남겨서 중요 예외를 인지할 수 있게 해주자.
/** 문서에 예외 명시
* Make an instance managed and persistent.
* @param entity entity instance
* @throws EntityExistsException if the entity already exists.
* @throws IllegalArgumentException if the instance is not an
* entity
* @throws TransactionRequiredException if there is no transaction when
* invoked on a container-managed entity manager of that is of type
* <code>PersistenceContextType.TRANSACTION</code>
*/
public void persist(Object entity);
◼️예외 포함과 스택 트레이스
예외 전환시 꼭 기존 예외를 포함하자.마지막 파라미터에 예외를 넣으면 로그에 스택 트레이스를 출력할수 있다.
참고로, System.out 에 스택 트레이스를 출력하려면 e.printStackTrace() 를 사용하면 된다.(실무에선 X!)
예외를 전환할 때는 꼭! 기존 예외를 포함하자. 그래야 스택 트레이스 추적할 수 있다.
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
//e.printStackTrace();
log.info("ex", e); //log.info("message={}", "message", ex);
}
}
⬜ 섹션6 스프링과 문제 해결 - 예외 처리, 반복
◼️체크 예외와 인터페이스
서비스가 처리할 수 없는 SQLException 에 대한 의존을 제거하려면 어떻게 해야할까?
서비스가 처리할 수 없는 리포지토리 예외(SQLException)를 런타임 예외로 전환해서 서비스 계층에 던지자.
public interface MemberRepository { //특정기술 종속X 인터페이스
Member save(Member member); // throws SQLException;
Member findById(String memberId);
void update(String memberId, int money);
void delete(String memberId);
}
하지만 레포지토리에서 체크 예외를 사용하려면 인터페이스에도 해당 체크 예외가 선언 되어 있어야 한다.
인터페이스 메서드에 먼저 체크 예외를 던지는 부분이 선언 되어있어야 구현체가 체크 예외를 던질 수 있다.
🟢인터페이스와 언체크, 체크 예외
- 위처럼 체크 예외는 인터페이스를 사용해도 특정 구현 기술 종속적인 예외를 포함해야한다.
- 반면 런타임 예외는 인터페이스에 따로 선언하지 않아도 된다. 따라서 특정 기술 종속적일 필요가 없다.
◼️런타임 예외 적용
public class MyDbException extends RuntimeException{ //MyDbException 런타임 예외
public MyDbException() {
}
public MyDbException(String message){
super(message);
}
public MyDbException(String message, Throwable cause){
super(message, cause);
}
public MyDbException(Throwable cause){
super(cause);
}
}
/*
* 예외 누수 문제 해결
* 체크 예외를 런타임 에러로 변경
* MemberRepository 인터페이스 사용
* throws SQLException 제거
* */
@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;
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) { //체크 예외를 MyDbException 이라는 런타임 예외로 변환해서 던짐
throw new MyDbException(e); // 기존 예외를 생성자를 통해서 포함해야 함
}finally {
close(con,pstmt,null);
}
}
//...생략
/*
* 예외 누수 문제 해결
* SQLException 제거
* MemberRepository 인터페이스 의존
* */
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV4 {
//MemberRepository 인터페이스에 의존하도록 코드를 변경
//private final MemberRepositoryV3 memberRepository;
private final 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) { //throws SQLException 제거
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 void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
🟢 정리
체크 예외를 런타임 예외로 변환하면서 인터페이스와 서비스 계층의 순수성을 유지할 수 있게 되었다.
덕분에 향후 JDBC에서 다른 구현 기술로 변경하더라도 서비스 계층의 코드를 변경하지 않고 유지할 수 있다.
◼️데이터 접근 예외 직접 만들기
앞서 MyDbException예외를 넘겼는데, 특정 예외를 복구하고 싶을 수 있다. 이런경우 예외를 어떻게 구분해 처리할까?
같은 오류여도 각각의 데이터베이스마다 정의된 오류 코드가 다르다.
(예를들어 키 중복 오류코드는 각각 H2 DB는 23505, MySQL은 1062이다.)
따라서 오류 코드를 사용할 때는 데이터베이스 메뉴얼을 확인해야 한다.
서비스 계층에서는 예외복구를 위해 키 중복 오류를 확인할 수 있어야한다. 이때, 서비스계층의 순수성을 지키기 위해 리포지토리에서 SQLException을 서비스계층으로 넘기지말고, 위 그림처럼 예외를 변환해서 던지면 특정 기술에 의존하지 않을 수 있다.
🔶 MyDuplicateKeyException
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);
}
}
위와같이 MyDbException을 상속받아, 의미있는 계층을 형성한다.
이 예외는 직접 만든것이기에 특정기술(JDBC,JPA...) 종속적이지 않다. 따라서 서비스 계층의 순수성을 유지할 수 있다.
아래 테스트를 실행해보면, 같은 ID를 저장하면 중간에 예외를 잡아 복구함을 확인할 수 있다.
public class ExTranslatorV1Test {
//..생략
@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 {
//..생략
} catch (SQLException e) {
//h2 db
if (e.getErrorCode() == 23505) { // 키 중복 오류
throw new MyDuplicateKeyException(e); // 새 예외를 만들어 던짐
}
throw new MyDbException(e); //기존의 예외 던짐
} finally {
closeStatement(pstmt);
closeConnection(con);
}
}
}
🟢 정리
- SQL ErrorCode로 데이터베이스에 어떤 오류가 있는지 확인 가능
- 예외 변환: SQLException → MyDuplicateKeyException ( 특정 기술에 의존하지 않는 직접 만든 예외 )
- 리포지토리 계층이 예외를 변환해준 덕분에 서비스 계층은 특정 기술에 의존하지 않는 MyDuplicateKeyException 을 사용해서 문제를 복구하고, 서비스 계층의 순수성도 유지할 수 있었다.
◼️스프링 예외 추상화 이해
SQL ErrorCode가 DB마다 달라,DB가 변경되면 ErrorCode도 모두 변경해야 한다.
스프링은 이 문제를 해결하기 위해 DB 접근과 관련된 예외를 추상화해서 제공한다.
- 스프링은 JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해준다.
- 각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 따라서 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다.
- 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.
- Transient 하위 예외: 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있다.
- NonTransient: 같은 SQL을 그대로 반복해서 실행하면 실패한다.
🟢 스프링이 제공하는 예외 변환기
스프링은 DB 오류코드를 스프링 정의 예외로 자동 변환해주는 변환기를 제공한다.
각각 DB마다 SQL ErrorCode는 다른데변환기는 아래 파일에 대입해 어떤 데이터 접근 예외로 전환해야 할지를 찾는다.
org.springframework.jdbc.support.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>
🔶 예외 변환기 사용 예시
@Test
void exceptionTranslator() {
String sql = "select bad grammer";
try {
Connection con = dataSource.getConnection();
PreparedStatement stmt = con.prepareStatement(sql);
stmt.executeQuery();
}catch (SQLException e){
assertThat(e.getErrorCode()).isEqualTo(42122);
//org.springframework.jdbc.support.sql-error-codes.xml
SQLErrorCodeSQLExceptionTranslator exTranslator =
new SQLErrorCodeSQLExceptionTranslator(dataSource);
// 설명, 실행 sql, 발생 SQLException 전달
// @ 적절한 스프링 데이터 접근 계층 예외로 변환해서 반환해준다.
DataAccessException resultEx = exTranslator.translate("select", sql, e);
log.info("resultEx",resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
} catch (SQLException e) { //기존 코드를 모두 스프링 예외 변환기를 사용하도록 변경한다.
throw exTranslator.translate("save", sql, e);
}
◼️JDBC 반복 문제 해결 - JdbcTemplate
이제 서비스 계층은 리포지토리 구현 기술과 예외에 종속적이지 않다. 이번에는 리포지토리에서 JDBC를 사용하기 때문에 발생하는 반복 문제를 해결해보자.
😥JDBC 반복 문제
- 커넥션 조회, 커넥션 동기화
- PreparedStatement 생성 및 파라미터 바인딩
- 쿼리 실행
- 결과 바인딩
- 예외 발생시 스프링 예외 변환기 실행
- 리소스 종료
😉스프링은 JDBC의 반복 문제를 해결하기 위해 JdbcTemplate 이라는 템플릿을 제공한다!JdbcTemplate 은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다. 그 뿐만 아니라 지금까지 학습했던, 트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다.
🟢 정리
- 서비스계층 순수성
- 트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 최대한 유지하면서 서비스 계층에서 트랜잭션을 사용할 수 있다.
- 스프링이 제공하는 예외 추상화와 예외 변환기 덕분에, 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지하면서 예외도 사용할 수 있다.
- 서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도 서비스 계층을 순수하게 유지할 수 있다.
- 리포지토리에서 JDBC를 사용하는 반복 코드가 JdbcTemplate 으로 대부분 제거되었다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션8 데이터 접근 기술 - 활용 방안 (0) | 2024.07.07 |
---|---|
[Spring] 스프링 DB 2편 - 데이터 접근 활용 기술 섹션6 데이터 접근 기술 - Querydsl (0) | 2024.07.07 |
[Spring] 스프링 DB 1편 - 데이터 접근 핵심 원리 섹션4 스프링과 문제 해결 - 트랜잭션 (0) | 2024.06.05 |
[Spring] 스프링 DB 1편 - 데이터 접근 핵심 원리 섹션3 트랜잭션 이해 (0) | 2024.06.02 |
[Spring] 스프링 DB 1편 - 데이터 접근 핵심 원리 섹션2 커넥션풀과 데이터소스 이해 (0) | 2024.05.29 |