3 분 소요


체크 예외와 인터페이스

서비스 계층은 가급적 특정 구현 기술에 의존하지 않고, 순수하게 유지하는 것이 좋지만 이렇게 하려면 예외에 대한 의존도 해결해야 한다.
서비스가 처리할 수 없는 SQLException을 던지는 리포지토리의 SQLException 체크 예외를 런타임 예외로 전환해서 서비스 계층에 던지면 해결할 수 있다. 이렇게 하면 서비스 계층이 해당 예외를 무시할 수 있기 때문에, 특정 구현 기술에 의존하는 부분을 제거하고 서비스 계층을 순수하게 유지할 수 있다.


코드에 적용

인터페이스 도입

63

  • 인터페이스를 도입하면 MemberServiceMemberRepository 인터페이스만 의존하면 된다.
  • 구현 기술을 변경하고 싶으면 DI를 사용해서 MemberService 코드의 변경 없이 구현 기술을 변경할 수 있다.

MemberRepository 인터페이스

public interface MemberRepository {
    Member save(Member member);
    Member findById(String memberId);
    void update(String memberId, int money);
    void delete(String memberId);
}

특정 기술에 종속되지 않는 순수한 인터페이스이다. 이 인터페이스를 기반으로 특정 기술을 사용하는 구현체를 만들면 된다.

체크 예외 문제점

인터페이스

public interface MemberRepositoryEx {
    Member save(Member member) throws SQLException;
    Member findById(String memberId) throws SQLException;
    void update(String memberId, int money) throws SQLException;
    void delete(String memberId) throws SQLException;
}
  • 인터페이스의 메서드에 throws SQLException이 있는 것을 확인할 수 있다.

구현 클래스

public class MemberRepositoryV3 implements MemberRepositoryEx {

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values(?, ?)";
    }
}
  • 인터페이스의 구현체가 체크 예외를 던지려면 인터페이스 메서드에 먼저 체크 예외를 던지는 부분이 선언되어 있어야 한다. 그래야 구현 클래스의 메서드도 체크 예외를 던질 수 있다.
    • 쉽게 얘기해서 MemberRepositoryV3throws SQLException을 하려면 MemberRepositoryEx 인터페이스에도 throws SQLException이 필요하다.
  • 참고로 구현 클래스의 메서드에 선언할 수 있는 예외는 부모 타입에서 던진 예외와 같거나 하위 타입이어야 한다.

특정 기술에 종속되는 인터페이스
구현 기술을 쉽게 변경하기 위해 인터페이스를 도입하더라도 SQLException과 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야 한다. 하지만, 이것은 순수한 인터페이스가 아닌 JDBC 기술에 종속적인 인터페이스일 뿐이다. 따라서, 향후 JDBC가 아닌 다른 기술로 변경한다면 인터페이스 자체를 변경해야 한다.

런타임 예외와 인터페이스
런타임 예외는 인터페이스에 따로 선언하지 않아도 되기 때문에 이런 부분에 대해서는 자유롭다. 따라서, 런타임 예외를 사용하면 인터페이스가 특정 기술에 종족적일 필요가 없다.

런타임 예외 적용

MemberRepository 인터페이스

public interface MemberRepository {
    Member save(Member member);
    Member findById(String memberId);
    void update(String memberId, int money);
    void delete(String memberId);
}

MyDbException 런타임 예외

public class MyDbException extends RuntimeException {

    public MyDbException() {
    }

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

    public MyDbException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDbException(Throwable cause) {
        super(cause);
    }
}
  • RuntimeException을 상속받았기 때문에, MyDbException은 런타임(언체크) 예외가 된다.

MemberRepositoryV4_1

/**
 * 예외 누수 문제 해결
 * 체크 예외를 런타임 예외로 변경
 * MemberRepository 인터페이스 사용
 * throws SQLException 제거
 */
@Slf4j
public class MemberRepositoryV4_1 implements MemberRepository {

    private final DataSource dataSource;

    public MemberRepositoryV4_1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

    // findById, update, delete 동일하기에 생략..

    ...
}
  • MemberRepository 인터페이스 구현.
  • 이 코드의 핵심은 SQLException 체크 예외를 MyDbException이라는 런타임 예외로 변환해서 던지는 부분이다.
  • 기존 예외를 생성자를 통해서 포함하고 있다. 예외는 원인이 되는 예외를 내부에 포함할 수 있는데 반드시 이렇게 작성해야 한다.
  • MyDbException이 내부에 SQLException을 포함하고 있어서 예외를 출력했을 때 스택 트레이스를 통해 둘 다 확인할 수 있다.

예외 변환 - 기존 예외 무시

catch (SQLException e) {
    throw new MyDbException();
}
  • new MyDbException()으로 해당 예외만 생성하고, 기존에 있는 SQLException은 포함하지 않고 무시한다.
  • 따라서, MyDbException은 내부에 원인이 되는 다른 예외를 포함하지 않는다.
  • 이렇게 원인이 되는 예외를 내부에 포함하지 않으면 예외를 스택 트레이스를 통해 출력했을 때 기존에 원인이 되는 부분을 확인할 수 없다.
    • 만약 SQLException에서 문법 오류가 발생했다면, 그 부분을 확인할 방법이 없게 된다.

MemberServiceV4

/**
 * 예외 누수 문제 해결
 * SQLException 제거
 *
 * MemberRepository 인터페이스 의존
 */
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV4 {

    private final MemberRepository memberRepository;

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) {
        bizLogic(fromId, toId, money);
    }

    ...
}
  • MemberRepository 인터페이스에 의존하도록 코드를 변경했다.
  • MemberServiceV3_3참고와 비교해 보면 메서드에서 throws SQLException 부분이 제거되었다.
  • 순순한 서비스가 완성되었다.

정리

  • 체크 예외를 런타임 예외로 변환하면서 인터페이스와 서비스 계층의 순수성을 유지할 수 있게 되었다.
  • 덕분에 향후 JDBC에서 다른 구현 기술로 변경하더라도 서비스 계층의 코드를 변경하지 않고 유지할 수 있다.


남은 문제

리포지토리에서 넘어오는 특정한 예외의 경우 복구를 시도할 수도 있다. 하지만 지금 방식은 항상 MyDbException이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다.


<출처 : 인프런 - 스프링 DB 1편 : 데이터 접근 핵심 원리(김영한)>

댓글남기기