[Spring과 DB] 5-1 Spring 에서의 예외처리 지원

2023. 8. 15. 22:30·Spring/Spring 기본
728x90
반응형

우선 Java 기본기 중 체크 Exception 과 언체크 Exception (Runtime Exception) 의 차이에 대해서는 인지한 상태로 내용에 들어간다. 

체크 예외는 코드가 지저분해지는 문제도 있지만, 가장 큰 문제는 유지보수 관점에서 처리하지도 못할 Exception 들을 처리해주는 ControllerAdvice 단까지 끌고 올라간다는게 Spring 에서의 체크 Exception 사용의 문제였다. 

 

 

하지만 JDBC 기술을 사용한다면, SQLException 은 당연히 발생할 수 밖에 없다. 다음을 보자. 다음은 트랜젝션 동기화를 지원해주기 위해 DataSourceUtils 를 사용한 Repository 이다. 

 

 

public class MemberRepositoryV3 {

    private final DataSource dataSource
    
    public MemberRepositoryV3(DataSource ds){
        this.dataSource = ds;
    }
    
    private Connection getConnection() throws SQLException {
        Connection conn = DataSourceUtils.getConnection(dataSource);
        return conn;
    }
    
    private Member save(Member member) throws SQLException {
        String sql = "INSERT 문";
        
        Connection con = null;
        PreparedStatement = pstmt = null;
        
        try {
            con = getConnection();
            pstmt = con.getPerparedStatement(sql);
            
            // ... QUERY 처리
            
        } catch (SQLException e){
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
    
    ...
}

 

이런 일반적인 JDBC 와  Transaction 보장 기술을 사용하는 Repository 라면 당연히 등장할 것이 SQLException 이다. 당연히 해당 문제의 해결을 위해 SQLException 이 누수되는 것을 막으려면, SQLException 을 RuntimeException 으로 변경해주어야 한다. 그렇게 되면 해당 Repository 를 사용하는 Client 들에게 Checked Exception 을 잡으라고 안던져도 된다 (알아서 올라가기 때문이다). 

 

그러기 위해서 다음 interface 를 도입하였는데, 단순하게 도입하면 Checked 예외의 특성상, 인터페이스에서도 throw 처리를 해주라고 빨간줄이 그어진다. 이는 인터페이스가 아니다. 변경을 해주지 않기 위해서 인터페이스를 도입해주고, Runtime Exception 으로 변경하려고 하는건데, interface 부터 SQLException 이 누수되었다. 

 

 

public interface MemberRepositoryEx {

    Member save(Member member) throws SQLException; // WRONG
    ...
}

 

 

그렇다면 어떻게 해야할까? MemberRepository 야 그냥 두면 되고, Exception 을 Runtime 으로 변경해주기 위해서는 우선 Custom RuntimeException 을 하나 만들자. 반드시 Constructor 에 누적 예외를 달 수 있는 Constructor 를 추가해줘야 한다는 점을 잊지말자

 

 

public class MyDbException extends RuntimeException{

    public MyDbException() {
    }

    public MyDbException(String message) {
        super(message);
    }

    /*
     원인을 지속 연결시켜 줄 수 있는 Exception Constructor 를 주렁주렁 달아주어야 한다
     */
    public MyDbException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDbException(Throwable cause) {
        super(cause);
    }
}

 

 

이제 위 RepositoryV3 코드에서 다음과 같이 변경해주면, throw SQLException 을 해주지 않아도 Repository 를 사용하는 Client 단 ( Service, Controller, 심지어 interface) 에서 불필요한 Exception 의존을 하지 않을 수 있다.

 

 

public class MemberRepositoryV4 {

    ...
    
    private Member save(Member member) { // 더이상 굳이 SQLException 을 날려주지 않아도 된다
        // ...
        
        try {
        
        //...
        
        } catch (SQLException e){
            throw MyDbException(e);
        } finally {
            close(con, pstmt, null);
        }
    }
    
    ...
}

 

 

이제야 비로소 Transaction / Exception 누수를 모두 해결해볼 수 있었다. 하지만 이 Exception 관련해서는 더 나아가봐야할 부분이 있다. 

 

 

1. Repository 에서 넘어오는 특정한 예외의 경우, 복구를 시도해봐야할 경우가 있다. (가령, Id Exception 은 해결해봐도 됨)

2. 이런 경우는 근데 Service 단에서 예외를 구분할 수가 없기 때문에, Service 단에서 해당 Exception 을 복구할 수 없게 된다. 

 

이런 문제는 어떻게 해결해볼 수 있을까?

 

 

 

* 데이터 접근 예외 직접 만들기 

 

 

요구사항에 맞춰, 다음과 같은 비즈니스 로직이 정해졌다고 해보자. 그리고 지금과 같은 상황에서는 Service 단이 모든 SQLException 들이 MyDbException 으로 자동으로 넘어와서 던져버리기 때문에 다음 요구사항을 해결할 수가 없다. 

 

 

ID 가 중복될 경우, 재요청 없이 처리해주기 위해 ID 뒤에 임의이 숫자를 붙여서 재가입을 진행한다 (ex : hello 로 가입을 시도했는데 이미 있을경우, hello12345 로 가입된채로 유저는 넘어가야 한다) 

 

강사님 예시 자료

 

우선 DB 는 SQLException 을 던질 것이다 (PK 에러). 또한 이 Exception 에는 해당 에러가 [Unique 제약조건] 이라는 에러를 알려주기 위해 Error Code 를 반환해준다 (23505 같이). 그리고 우리는 다음과 같이 확인할 수 있다. (참고로 하기는 H2 이고, SQL 툴마다 사용하는 에러코드가 다 다르다)

 

 

...
if (e.getErrorCode() == 23505){ // DB 에서 SQLException 에 ErrorCode 로 심어준다
if (e.getErrorCode() == 42000){ // SQL 문법 오류
...

 

 

근데 이 에러코드를 사용하기 위해선 SQLException 을 또 Service 단에 던져야 한다 (위에 문제 도돌이표..). 그렇기 위해서, Repository 에서 또 해당 에러를 잡기 위해 예외를 변환해서 던져보겠다. 우선, 다음과 같은 Exception 종류를 새로 만들어주겠다. 

 

 

public class MyDbDuplicateKeyException extends MyDbException{ // 내가 한 카테고리 화


    public MyDbDuplicateKeyException() {
    }
    
    ...
}

 

 

이 예외는 Repository 단에서 PK 중복 에러가 발생할 경우에만 이 예외로 던져지도록 할 것이다. 예상하다시피 이 Exception 을 활용하면 Runtime 이기 때문에 Service 단에서도 Java 순수성을 유지할 수 있다. 예시를 살펴보자 우선 Error Code 를 사용해서 위 PK Exception 을 잡을 수 있기 때문에 (H2 DB를 사용한다고 한정하겠다), 다음과 같이 예제를 짜볼 수 있다. 

 

 

다음과 같은 Test Repository 를 만들어보자

 

@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 MyDbDuplicateKeyException();
            }

            throw new MyDbException(e);

        }finally {

            JdbcUtils.closeStatement(pstmt);
            JdbcUtils.closeConnection(con);
        }
    }
}

 

 

그리고 이를 통해 Service 단에게 (순수함을 유지하며) 해당 Runtime Exception 이 발생했을 경우 따로 처리하라는 로직을 추가해줄 수 있다. 다음 Test Service 를 만들어서 확인해보자. 

 

 

@RequiredArgsConstructor
static class Service{

    private final Repository repository;

    private String generateNewId(String memberId) {
        return memberId + new Random().nextInt(10000);
    }

    public void create(String memberId) {
        try {
            repository.save(new Member(memberId, 1000));
            log.info("SAVED ID = {}", memberId); // 이게 안되는 경우가 문제인거임.
        } catch (MyDbDuplicateKeyException exception) {

            log.info("키 에러가 발생하였다, Service 단에서 직접 복구한다");
            String retryId = generateNewId(memberId);

            log.info("SAVED ID = {}", retryId);
            repository.save(new Member(retryId, 1000));
        } catch (MyDbException exception) {
            log.info("데이터 접근 계층 예외", exception);
            throw exception;
        }
    }
}

 

 

MyDbException 이 굳이 없어도는 된다. 그냥 이런식으로 에러의 종류를 파싱할 수 있기 때문이다. 당연히 상위 계층의 에러가 더 아래쪽으로 와야 위부터 순차적으로 내려올 수 있다. 위 코드는 서비스의 순수성도 유지시킬 수 있다는 측면에서 장점이 있다. 

 

 

이제 남은 문제는... 당연히 생각할 수 있겠지만 Error Code 를 23505 로 잡고 있다는 문제이다. MySQL 로 바꾸면 이거 Duplicate Key 에러라는 Error Code 가 바뀐다. 정말 에러 코드는 정말 많은데, 이걸 잡고 싶은 부분에 대해서 다 잡아줘야 할까? 23505 로 잡는건 저렇게 짠 순간부터 잘못된 방법이라는 것을 느꼈어야 정상이다.  그럼 이걸 또 어떻게 할까??

 

 

더 이상 어떻게 할 수 있을까? 이제 스프링 형님이 등장할 차례이다. 이제 스프링이 어떻게 해결해주는지 살펴보자. 

 

 

 

 

** 모든 출처 **

 

 

스프링 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
반응형

'Spring > Spring 기본' 카테고리의 다른 글

[Spring] @Transactional 내에서 Exception 처리 범위에 대하여  (0) 2025.05.30
[Spring과 DB] 5-2 Spring 에서의 예외 추상화  (1) 2023.08.16
[Spring 기본] Bean Scope  (0) 2022.10.19
[Spring 기본] Bean 생명주기 콜백  (0) 2022.10.18
[Spring 기본] 의존 관계 주입 전략 (DI Strategy)  (0) 2022.10.17
'Spring/Spring 기본' 카테고리의 다른 글
  • [Spring] @Transactional 내에서 Exception 처리 범위에 대하여
  • [Spring과 DB] 5-2 Spring 에서의 예외 추상화
  • [Spring 기본] Bean Scope
  • [Spring 기본] Bean 생명주기 콜백
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    lazy loading
    OOAD
    김영한
    GoF
    Setter
    elasticsearch
    composition
    decorator
    OOP
    lombok
    Composite
    SRP
    JPA
    n+1
    BEAN
    단위테스트
    Java
    di
    mockito
    Spring
    디자인 패턴
    spring boot
    analyzer
    spring container
    runtime exception
    객체지향
    Design Pattern
    k8s
    Configuration
    junit
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[Spring과 DB] 5-1 Spring 에서의 예외처리 지원
상단으로

티스토리툴바