ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
예외로 인한 의존성 문제
트랜잭션 AOP까지 적용하면서 서비스 계층에서 트랜잭션 로직을 완전히 떼어낼 수 있었다.
그렇다면 이제 서비스 계층은 순수한 비즈니스 로직 코드만 남은걸까? 한번 코드를 살펴보자
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
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 static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체 중 예외 발생");
}
}
}
해당 부분을 보면 SQLException을 밖으로 던지고 있다. 그런데 SQLException은 JDBC에 대해 종속적인 체크 예외이다. 즉, 아직 서비스 계층은 JDBC에 의존하고 있다.
어떻게 하면 이를 해결할 수 있을까?
바로 언체크 예외를 쓰면 된다.
해결하기 앞서 체크예외와 언체크 예외에 대해 간략하게만 알아보고 가자
체크 예외와 언체크 예외
자바의 예외는 크게 체크 예외와 언체크 예외로 나눌 수 있다.
체크예외
- Exception 클래스와 그 하위 클래스들에 해당한다. (단, RuntimeException은 제외)
- 컴파일러가 체크한다.
- 예외를 처리하지 않으면 반드시 던져야 한다.
- catch로 예외를 처리하지 않으면 반드시 throws로 예외를 던져야 한다.
언체크예외
- RuntimeException 클래스와 그 하위 클래스들에 해당한다.
- 컴파일러가 따로 체크하지 않는다.
- 예외를 처리하지 않아도 던지지 않아도 된다.
- catch로 예외를 처리하지 않아도 throws를 명시하지 않아도 된다.
체크예외 vs 언체크예외
체크 예외는 catch로 잡아서 해결하지 않는다면 throws를 명시해서 밖으로 던져줘야 한다.
그렇게 되면 리포지토리 계층의 예외가 서비스 계층, 컨트롤러 계층으로도 퍼지게 된다.
그냥 중간에 잡아서 해결하면 되지 않나 생각할 수 있지만 데이터베이스관련 오류는 대부분 복구 불가능한 예외들이다. 그렇기에 throws로 예외를 던져줄 수 밖에 없다.
이후에 데이터접근 기술을 JDBC에서 JPA로 변경한다면 모든 계층 예외 코드를 다 고쳐야 하는 상황이 발생한다.
반면 언체크 예외를 사용하게 되면 리포지토리에서 발생한 예외를 서비스, 컨트롤러 계층에서 던져주지 않아도 된다.
이후에 데이터접근 기술을 JDBC에서 JPA로 변경할 때 모든 계층을 수정하는 것이 아니라 해당 예외가 발생하는 곳과 예외를 처리하는 곳만 수정하면 된다.
예외 의존성 해결
다시 돌아와서 예외 의존성 문제를 해결해보겠다.
예외 의존성 문제를 해결하려면 위에서 봤듯 체크예외를 언체크예외로 변환 시켜서 의존성을 지워주면 된다.
먼저 커스텀 언체크 예외를 하나 만들었다.
package jdbc.review.repository.ex;
public class MyDbException extends RuntimeException{
public MyDbException() {
super();
}
public MyDbException(String message) {
super(message);
}
public MyDbException(String message, Throwable cause) {
super(message, cause);
}
public MyDbException(Throwable cause) {
super(cause);
}
}
- RuntimeException을 상속받으면 자동으로 언체크 예외가 된다.
- 참고: 그냥 Exception을 상속받으면 체크 예외가 된다.
그런 다음 리포지토리 계층에서 예외를 전환시켜주면 된다.
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) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
- SQLException을 잡아 새롭게 언체크예외인 MyDbException을 throw 해줬다.
- 여기서 주의해야할점은 기존의 예외를 반드시 새로운 예외에 파라미터로 넣어줘야 한다는 점이다.
이렇게 되면 서비스 계층에선 예외에 대한 의존성을 지울 수 있다.
// 컴파일 오류 발생 X
@Transactional
public void accountTransfer(String fromId, String toId, int money) {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
스프링 예외 추상화 및 예외변환기
데이터베이스 오류에 따라 특정 오류는 복구하고 싶을 수 있다.
예를 들어서 회원 가입시 DB에 이미 같은 ID가 있으면 ID 뒤에 숫자를 붙여서 새로운 ID를 만들어야 한다고 가정해보자.
ID를 hello 라고 가입 시도 했는데, 이미 같은 아이디가 있으면 hello12345 와 같이 뒤에 임의의 숫자를 붙여서 가입하는 것이다.
SQLException이 발생하면 오류코드까지 함께 반환하는데 이를 이용하면 위와같은 부분을 구현할 수 있다.
하지만 문제는 이 오류코드가 데이터베이스마다 다 다르다는 것이다.
키중복오류 코드의 경우 H2는 23505이지만, MySQL은 1062이다.
이밖에도 데이터베이스에는 수많은 오류코드가 존재한다.
개발자는 필요할 때마다 일일이 데이터베이스별로 오류코드에 맞게 코드를 작성해야하는 상황이 발생한다.
스프링은 예외추상화를 통해 이러한 상황을 해결해준다.
- 스프링은 데이터 접근 계층에 대한 수십가지 예외를 정리해 일관돤 예외 계층을 제공한다.
- 이후 JDBC에서 JPA로 변경해도 이 예외들을 그대로 사용하면 된다.
- 이 모든 예외계층의 최상위 클래스는 DataAccessException으로 이는 RuntimeException을 상속받은 언체크 예외이다. 이말은 즉, 예외에 대한 의존성 문제는 신경쓰지 않아도 된다는 의미이다.
또한 스프링은 데이터베이스에서 발생한 오류코드를 적절한 예외로 변환시켜주는 예외변환기도 제공해준다.
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
바로 SQLExceptionTranslator이다.
사용방법은 간단하다.
DataAccessException resultEx = exTranslator.translate("select", sql, e);
translate()메서드 안에는 다음 세 가지 정보만 넣어주면 된다.
- 예외에 대한 설명
- sql
- 발생한 SQLException
이를 통해 적절한 예외를 찾아 반환해주는데 바로 이 예외가 스프링의 예외추상화 예외중 하나이다.
이 반환받은 예외를 그냥 던져주기만 하면 모든 과정이 끝난다.
catch (SQLException e) {
throw exTranslator.translate("findById", sql, e);
}
이후 오류를 해결하고 싶은 서비스 계층의 비즈니스 로직에서 해당 오류를 잡아 해결하면 된다.
이는 특정 데이터접근 기술에 의존하는 것이 아닌 스프링에 의존하는 것이기 때문에 데이터접근기술에 대한 의존성문제는 걱정하지 않아도 된다.
try {
// 예외가 발생할 수도 있는 로직
} catch (DataAccessException e) {
// 예외 처리 로직
}
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] JdbcTemplate (0) | 2024.06.06 |
---|---|
[Spring Data] SQL Mapper VS ORM (0) | 2024.06.05 |
[Spring Data] Spring Transaction을 통한 문제해결 (0) | 2024.04.29 |
[Spring Data] JDBC 트랜잭션 (0) | 2024.04.29 |
[Spring Data] 커넥션 풀과 데이터 소스 (0) | 2024.04.29 |