본문 바로가기

Spring/Spring 기본

[Spring과 DB] 5-2 Spring 에서의 예외 추상화

728x90

 

* Spring 에서의 예외 추상화

 

 

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

 

728x90