이번 섹션에서는 [스프링과 문제 해결 - 예외 처리, 반복]에 대해서 알아보자.
👉 목차는 다음과 같다.
1) 체크 예외와 인터페이스
2) 런타임 예외 적용
3) 데이터 접근 예외 직접 만들기
4) 스프링 예외 추상화 이해
5) 스프링 예외 추상화 적용
6) JDBC 반복 문제 해결 - JdbcTemplate
7) 정리
이번 섹션은 1) ~ 3)
, 4) ~ 7)
로 나눠서 포스팅하고자 한다.
바로 하나씩 확인해보자.
서비스 계층은 가급적 특정 구현 기술에 의존하지 않고, 순수하게 유지하는 것이 좋다. 이렇게 하려면 예외에 대한 의존도 함께 해결해야한다.
예를 들어서 서비스가 처리할 수 없는 SQLException
에 대한 의존을 제거하려면 어떻게 해야할까?
서비스가 처리할 수 없으므로 리포지토리가 던지는 SQLException
체크 예외를 런타임 예외로 전환해서 서비스 계층에 던지면 된다. 이렇게 하면 서비스 계층이 해당 예외를 무시할 수 있기 때문에, 특정 구현 기술에 의존하는 부분을 제거하고 서비스 계층을 순수하게 유지할 수 있다.
지금부터 코드로 이 방법을 적용해보자.
인터페이스 도입
먼저 MemberRepository
인터페이스도 도입해서 구현 기술을 쉽게 변경할 수 있게 해보자.
MemberService
는 MemberRepository
인터페이스에만 의존하면 된다.MemberService
코드의 변경 없이 구현 기술을 변경할 수 있다.
체크 예외와 인터페이스
잠깐? 기존에는 왜 이런 인터페이스를 만들지 않았을까? 사실 다음과 같은 문제가 있기 때문에 만들지 않았다.
왜냐하면 SQLException
이 체크 예외이기 때문이다. 여기서 체크 예외가 또 발목을 잡는다. 체크 예외를 사용하려면 인터페이스에도 해당 체크 예외가 선언되어 있어야 한다.
예를 들면 다음과 같은 코드가 된다.
throws SQLException
이 있는 것을 확인할 수 있다.MemberRepositoryV3
가 throws SQLException
를 하려면 MemberRepositoryEx
인터페이스에도 throws SQLException
이 필요하다.throws Exception
를 선언하면, 구현 클래스 메서드에 throws SQLException
는 가능하다. SQLException
은 Exception
의 하위 타입이기 때문이다.특정 기술에 종속되는 인터페이스
구현 기술을 쉽게 변경하기 위해서 인터페이스를 도입하더라도 SQLException
과 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야 한다. 하지만 이것은 우리가 원하던 순수한 인터페이스가 아니다. JDBC 기술에 종속적인 인터페이스일 뿐이다. 인터페이스를 만드는 목적은 구현체를 쉽게 변경하기 위함인데, 이미 인터페이스가 특정 구현 기술에 오염이 되어 버렸다. 향후 JDBC가 아닌 다른 기술로 변경한다면 인터페이스 자체를 변경해야 한다.
런타임 예외와 인터페이스
런타임 예외는 이런 부분에서 자유롭다. 인터페이스에 런타임 예외를 따로 선언하지 않아도 된다. 따라서 인터페이스가 특정 기술에 종속적일 필요가 없다.
실제 코드에 런타임 예외를 사용하도록 적용해보자.
RuntimeException
을 상속받았다. 따라서 MyDbException
은 런타임(언체크) 예외가 된다.MemberRepository
인터페이스를 구현한다.SQLException
이라는 체크 예외를 MyDbException
이라는 런타임 예외로 변환해서 던지는 부분이다.
✔️ 한번 더 참고
MyDbException
이 내부에 SQLException
을 포함하고 있다고 이해하면 된다. 예외를 출력했을 때 스택 트레이스를 통해 둘다 확인할 수 있다.new MyDbException()
으로 해당 예외만 생성하고 기존에 있는 SQLException
은 포함하지 않고 무시한다.MyDbException
은 내부에 원인이 되는 다른 예외를 포함하지 않는다.SQLException
에서 문법 오류가 발생했다면 그 부분을 확인할 방법이 없게 된다.
👉 이번에는 서비스가 인터페이스를 사용하도록 하자.
MemberServiceV3_3
를 복사해서 수정하자. )MemberRepository
인터페이스에 의존하도록 코드를 변경했다.MemberServiceV3_3
와 비교해서 보면 드디어 메서드에서 throws SQLException
부분이 제거된 것을 확인할 수 있다.👉 테스트로 확인해보자.
MemberRepository
인터페이스를 사용하도록 했다.SQLException
체크 예외를 던지는 구문을 제거하였다.
✔️ 정리
✔️ 남은 문제
MyDbException
이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다. 만약 특정 상황에는 예외를 잡아서 복구하고 싶으면 예외를 어떻게 구분해서 처리할 수 있을까? 데이터베이스 오류에 따라서 특정 예외는 복구하고 싶을 수 있다.
예를 들어서 회원 가입시 DB에 이미 같은 ID가 있으면 ID 뒤에 숫자를 붙여서 새로운 ID를 만들어야 한다고 가정해보자.
ID를 hello
라고 가입 시도 했는데, 이미 같은 아이디가 있으면 hello12345
와 같이 뒤에 임의의 숫자를 붙여서 가입하는 것이다.
데이터를 DB에 저장할 때, 같은 ID가 이미 데이터베이스에 저장되어 있다면, 데이터베이스는 오류 코드를 반환하고, 이 오류 코드를 받은 JDBC 드라이버는 SQLException
을 던진다. 그리고 SQLException
에는 데이터베이스가 제공하는 errorCode
라는 것이 들어있다.
데이터베이스 오류 코드 그림
e.getErrorCode() == 23505
SQLException
내부에 들어있는 errorCode
를 활용하면 데이터베이스에서 어떤 문제가 발생했는지 확인할 수 있다.H2 데이터베이스 예
23505
: 키 중복 오류42000
: SQL 문법 오류23505
1062
서비스 계층에서는 예외 복구를 위해 키 중복 오류를 확인할 수 있어야 한다. 그래야 새로운 ID를 만들어서 다시 저장을 시도할 수 있기 때문이다. 이러한 과정이 바로 예외를 확인해서 복구하는 과정이다. 리포지토리는 SQLException
을 서비스 계층에 던지고 서비스 계층은 이 예외의 오류 코드를 확인해서 키 중복 오류 ( 23505
)인 경우 새로운 ID를 만들어서 다시 저장하면 된다.
그런데 SQLException
에 들어있는 오류 코드를 활용하기 위해 SQLException
을 서비스 계층으로 던지게 되면, 서비스 계층이 SQLException
이라는 JDBC 기술에 의존하게 되면서, 지금까지 우리가 고민했던 서비스 계층의 순수성이 무너진다.
이 문제를 해결하려면 앞서 배운 것 처럼 리포지토리에서 예외를 변환해서 던지면 된다. ( SQLException
-> MyDuplicateKeyException
)
👉 먼저 필요한 예외를 만들어보자.
MyDbException
을 상속받아서 의미있는 계층을 형성한다. 이렇게하면 데이터베이스 관련 예외라는 계층을 만들 수 있다.MyDuplicateKeyException
이라는 이름을 지었다. 이 예외는 데이터 중복의 경우에만 던져야 한다.👉 실제 예제 코드를 만들어서 확인해보자.
✔️ 코드 분석
e.getErrorCode() == 23505
: 오류 코드가 키 중복 오류( 23505
)인 경우 MyDuplicateKeyException
을 새로 만들어서 서비스 계층에 던진다.MyDbException
을 던진다.MyDuplicateKeyException
예외가 올라오면 이 예외를 잡는다.generateNewId(memberId)
로 새로운 ID 생성을 시도한다. 그리고 다시 저장한다. 여기가 예외를 복구하는 부분이다.MyDbException
)면 로그만 남기고 다시 예외를 던진다.
✔️ 정리
SQLException
을 특정 기술에 의존하지 않는 직접 만든 예외인 MyDuplicateKeyException
로 변환할 수 있었다.MyDuplicateKeyException
을 사용해서 문제를 복구하고, 서비스 계층의 순수성도 유지할 수 있었다.✔️ 남은 문제
23505
1062
다음 내용에서는 스프링이 이런 남은 문제를 어떻게 해결하는지 스프링 예외 추상화에 대해서 알아보자.
강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.