* Spring 에서의 예외 추상화
스프링에서는 데이터 접근 예외 계층을 제공해준다. 이는 Error Code 처럼 종속적이지 않다. 위를 보면 개발을 하면서 많이 봤던 Exception 들의 모습들을 확인할 수 있다 (DataIntegrity, NonTransient, Grammer Exception 등등).
데이터 접근 계층에서의 최상단 예외는 DataAccessException.class 이며, 모두 RuntimeException 인 것을 확인할 수 있다. 모든 데이터 계층 예외는 두가지로 분류한다.
1. Transient - 동일한 SQL 을 다시 시도했을 때 성공할 가능성이 어느정도 있음. 일시적인 Exception 이라는 뜻.
2. NonTransient - 같은 시도를 하면, 똑같은 예외가 발생하게 된다. 일시적이지 않은 Exception
그래서 앞에서 만들었던 MyDBException 이니 MyDbDuplicateKeyException 이느니 이런거 안만들고 위에 추상화 계층을 확인하면서 내가 파싱해주고 싶은 Exception 들을 확인해주면 된다 ^~^. (최상위 계층인 DataAccessException 이 요긴하게 쓰인다)
* Spring 에서 제공하는 예외 변환기
스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다. 뭔소릴까? 우선 이전 강의에서 error code 로 분류했던 Translating 작업을 기억하고 들어가보자. Spring 에서는 SQLErrorCodeSQLExceptionTranslator.class 를 제공해서 위와 같은 변환 작업을 처리해준다.
(참고로 들어가보면 알지만, 이 Translator 는 sql-error-codes.xml 이라는 파일을 사용해서 특정 예외는 각 DB 별로 어떤 코드들을 가지고 있는지 다 정의해 놓은 라이브러리가 있다. 여기서 내부적으로 다 파싱하는거다)
다음과 같은 예시를 보자. 이전 문제와 같은 상황이다 (복기).
public class SpringExceptionTranslatorTest {
private DataSource dataSource;
@BeforeEach
void init() {
dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Test
void sqlExceptionErrCode() {
String bad_sql = "select bad grammar";
try {
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(bad_sql);
pstmt.executeQuery();
} catch (SQLException e) {
// 필요한 상황을 모두 ErrorCode 로 처리해주는건 말도 안됨
Assertions.assertThat(e.getErrorCode()).isEqualTo(42122); // 안외워도 된다
if (e.getErrorCode() == 42122) { // 이런식으로 일일이 변환해줘야한다
throw new BadSqlGrammarException("", "", e);
}
}
}
}
이제 Spring에서 지원해주는 SQLExceptionTranslator 가 얼마나 강력한 툴인지 확인해보자.
@Test
void exceptionTranslator() {
String bad_sql = "select bad grammar";
try {
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(bad_sql);
pstmt.executeQuery();
} catch (SQLException e) {
Assertions.assertThat(e.getErrorCode()).isEqualTo(42122); // 같은 상황이다.
// 스프링 형님 등장
SQLErrorCodeSQLExceptionTranslator translator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultException = translator.translate("읽을 수 있는 설명 넣으면 됨", bad_sql, e); // 최종 상위 계층으로 반환한다 (translator 가 무슨 Exception 인지 확인한다)
Assertions.assertThat(resultException.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
해당 Translator 를 거치면 발생한 에러가 BadSqlGrammarException 에 속하는 Exception 인 것을 확인할 수가 있고, 이에 대한 Exception 처리를 해주면 될 것임을 알 수 있다.
참고로, 해당 Translator 는 JDBC 에서 JPA 같은 기술로 변경되어도 JPA 예외를 적절한 스프링 데이터 접근 예외로 변환해준다. 물론 단점이라면, 스프링에 대한 기술 종속성은 발생한다. 하지만 이런 방식을 통해서 비즈니스 요구사항을 해결할 수 있다면 하는게 좋다. (순수 자바가 깨지긴 하지만, 유지 보수성에 큰 문제가 있는 점은 아닌듯. 그러려면 진짜 모~~~ 든 예외를 내가 다 정의하고 파싱해야한다... 너무 순수한거에 빠져서 훨씬 실용적인 상황을 피할 이유는 전혀 없다. 자신이 잘 판단하면서 해야한다)
* 예제 코드에 적용해보기
이전에 작업하던 Member Repository 를 가져와서 활용해보겠다. 이전에 Checked Exception 이였던 SQLException 을 서비스에 던지지 않게 하기 위해서 MyDbException() 이라는 Runtime Exception 을 만들어서 대체로 던져주는 버젼까지 만들었었는데, 이제는 굳이 MyDbException 같은걸 안만들어줘도 되고, Spring 예외 계층을 사용해서 모든 예외를 상황에 맞게 Service 단으로 (안보이게) 던질 수 있다. (Service 단은 여전히 Unchecked Exception 이기 때문에 처리하지도 않을 Exception 에 의존할 필요도 없다)
다음과 같이 SpringTranslator 를 주입해줄 수 있다. DataSource 가 필요하기 때문에 생성자를 통해 직접 생성한다. Composition 관계로 주입해주는 것이다.
public class MemberRepositoryV4_2 implements MemberRepository {
private final DataSource dataSource;
private final SQLExceptionTranslator exceptionTranslator;
public MemberRepositoryV4_2(DataSource dataSource) { // SQLExceptionTranslator 는 직접 구현체로 넣어줍니다 // 그냥 SQL 측면에서만 일단 살펴보는거다
this.dataSource = dataSource;
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
...
}
그리고 하위 DB 에게 쿼링하는 메소드를 다음과 같이 변경해줄 수 있다.
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
// ... 쿼리 내보내기
} catch (SQLException e) {
// 이렇게 하면 익셉션이 뭔지 알아서 판단하고 Runtime Exceptoin 으로 전환 후 던져준다
throw exceptionTranslator.translate("SAVE EXCEPTION OCCUR", sql, e);
} finally {
close(con, pstmt, null);
}
}
이런 방식으로 하면 Spring 예외 계층의 Runtime Exception 중 한 종류로 맞춰서 Service 단으로 몰래 던져준다. 만약 서비스 단에서 특정 Exception (Spring 예외 계층에 의존하긴 해야한다) 을 처리하고 싶을 때 다음과 같이 할 수 있다.
public class MemberServiceV4 {
...
// Spring 에선 @Tx 달린 클래스에 대해선 Proxy 를 만들어야 한다고 인지한다
@Transactional
public void accountTransfer(String fromId, String toId, int money) {
bizLogic(fromId, toId, money);
}
public void bizLogic(String fromId, String toId, int money) {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
try{
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
} catch (BadGrammarException exception){
// 문법 오류 난 Exception 에 대해서는 로깅 파일을 따로 만들어서 이메일로 쏴주는 작업을 해준다.
} catch (DataIntegrityViolationException exception ){
// 심각한 에러로, DBA Slack 에 알림을 띄워주는 로직으로 전달한다
}
}
...
}
보면 알겠지만 좀 극단적인 예시들을 들어볼 수 있다. 사실 위와 같이 특별하게 Service 단에서 복구 시도를 할 일은 많지 않다고 한다. 하지만 위 예시처럼 Logging 이나 알림을 따로 보내야 한다면 위와 같이 해볼 수도 있겠다.
Service 단이 물론 순수 Java 여야 하는게 맞지만, 특정 기술 (JPA, JDBC 등) 에 의존하는 것이 아닌 Spring Framework 에서 제공하는 Exception 계층에 의존하는 것이다. 해당 사항은 Spring 을 쓰지 않거나 버젼 차이에 의한 충돌이 아닌 이상 바뀌지 않을 것이므로, 위 사항 정도의 의존성은 유도리 있게 가져가는게 좋을 것이다.
** 모든 출처 **
스프링 DB 1편 - 데이터 접근 핵심 원리 (김영한 강사님)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1#
스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의
백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔
www.inflearn.com
'Spring > Spring 기본' 카테고리의 다른 글
[Spring과 DB] 5-1 Spring 에서의 예외처리 지원 (0) | 2023.08.15 |
---|---|
[Spring 기본] Bean Scope (0) | 2022.10.19 |
[Spring 기본] Bean 생명주기 콜백 (0) | 2022.10.18 |
[Spring 기본] 의존 관계 주입 전략 (DI Strategy) (0) | 2022.10.17 |
[Spring 기본] Component Scan을 통한 Bean 자동화 관리 (0) | 2022.10.17 |