현재 버전

체크드 예외 부분이 잘 이해되지 않는 분들을 위해 예제를 통해 조금 보충해 보겠습니다.

예를들어 온라인 쇼핑몰의 장바구니 모듈이 내부적으로 상품 정보 서비스를 참조하고, 해당 서비스에서는 유효하지 않은 상품 정보에 대해 체크드 예외를 발생시킨다고 가정하겠습니다. (일반적인 설계는 아니지만 설명을 위한 것이니 양해 부탁드립니다):

public interface ShoppingCart {
    void add(long productId, int count) throws NoSuchProductException;
}

public interface Inventory {
    Product getProductDetails(long productId) throws NoSuchProductException;
}

이 경우 인벤토리의 `getProductDetails`메서드는 체크드 예외를 발생시키기 때문에, 이를 호출하는 쪽에서는 반드시 try-catch로 처리하거나 다시 throw해야합니다.

참고로, 이런 예제에서 `ShoppingCart`의 구현체의 `add` 메서드 내부에서 try-catch를 하는 것은 일반적으로 말하면 좋지 못한 접근입니다. 예외처리의 중요한 원칙 중 하나는 책임지지 않을 바엔 잡지도 말라는 내용이 있습니다.

즉, 예외는 항상 의미있는 방식으로 처리해야한다는 것이고 보통의 경우 이는 사용자에게 메시지를 보여주거나 다른 페이지로 리다이렉트 시키거나 아니면 디버깅을 위해 로그를 남기는 등의 작업을 뜻합니다.

문제는 해당 작업은 모두 호출 체인의 가장 앞 쪽에 위치한다는 것이고, 따라서 중간에 있는 `ShoppingCart` 같은 계층에서 처리할 수 없다는 것입니다. 예컨대 리다이렉트를 하려면 HttpResponse를 참조해야하는데 서비스 계층에서 웹 계층의 그런 객체를 참조하는 건 바람직하지도 않고 정상적인 설계에서 가능하지도 않습니다.

예제로 돌아와서 보면, 이 경우 `ShoppingCart` 입장에서 최상의 선택은 단순히 해당 예외를 다시 던지는 것임을 알 수 있습니다. 이는 설계적 관점으로 봐도 만족스러운 결과인데, 왜냐하면 `ShoppingCart`의 `add`메서드의 시그네쳐에 `throws NoSuchProductException`이 포함됨으로 인해 이제 호출자에게 '존재하지 않는 상품은 카트에 담을 수 없다'는 비즈니스 규칙을 명시적으로 밝힐 수 있기 때문입니다.

하지만 체크드 예외가 비즈니스 규칙이 아닌 오류를 나타내는 경우라면 이야기가 조금 달라집니다. 이젠 `Inventory`의 구현체가 데이터베이스를 통해 상품 정보를 조회한다고 가정해보겠습니다.

대부분의 JDBC API는 체크드 예외인 SQLException을 시그네쳐에 포함하고 있는 것은 잘 아시는 내용일 것입니다. 그럼 'Inventory'의 'getProductDetail' 구현에서 해당 예외를 어떻게 처리해야할까요?

우선 try-catch로 잡는 방법을 생각해볼 수 있지만, 이는 앞서 이야기한 이유로 좋은 선택이 아닙니다. 만일 비즈니스 요구조건이 장바구니 추가시 오류가 발생하면 이전 페이지로 리다이렉트 하는 것이라면 컨트롤러 계층에서 세 단계나 떨어져 있는 Inventory에서 이를 어떻게 처리할 수 있을까요?

사실 애초에 화면에 대한 내용은 해당 계층의 역할이 아닙니다. 계층화된 설계를 하는 이유 자체가 각 계층은 자신의 계층이 맡은 역할과 책임(role and responsibility)을 명확히 분리하기 위함이고, 그런 면에서 보면 예시의 설계에서 Inventory의 책임은 상품 조회 요청을 처리하는 것이지, 해당 요청이 웹 MVC에서 왔던 외부 모듈의 RMI 호출에서 왔건 간여할 바가 아닌 것입니다.

그렇다면 앞선 NoSuchProductException의 예에서 처럼 throws를 하는 것은 어떨까요?

이 경우 우선 `Inventory` 인터페이스의 구현체는 반드시 데이터베이스 기반으로 구현해야한다는 의도치 않은 제약이 생겨버립니다. 최초의 예제에서는 해당 인터페이스의 요구조건을 충족하기만 하면 구현체 입장에선 데이터베이스를 쓰건 NoSQL을 쓰건 XML 파일에서 읽어오건 전혀 문제될 것이 없었습니다.

하지만 인터페이스의 'add'메서드 시그네쳐에 SQLException이 들어가는 순간 해당 인터페이스는 JDBC API에 의존성이 생겨버리는 것입니다.

더 안좋은 것은, 이런 문제는 계층을 타고 상위로 전파된다는 것입니다. `Inventory.getProductDetails()`가 위와 같은 이유로 SQLException을 시그네쳐에 포함할 수 밖에 없었다면 마찬가지 이유로 'ShoppingCart.add()' 또한 해당 예외를 내부에서 처리하지 못하고 시그네쳐에 올릴 수 밖에 없습니다.

이 경우 예컨대 쇼핑 카트 서비스를 호출하는 MVC의 컨트롤러 계층에 최종 try-catch를 할 책임이 넘어가게 되는데, 이는 명백하게 잘못된 설계임을 어렵지않게 짐작할 수 있을 것입니다.

컨트롤러 입장에서 아는 것은 `ShoppingCart.add`에 제품 번호와 수량을 넣으면 장바구니에 담긴다는 것 뿐이지, 해당 서비스가 내부에서 `Inventory`라는 다른 서비스를 참조하는지, 또 그 서비스의 특정 구현체가 데이터베이스를 기반으로 작성되었는지는 관심도 없고 알아서도 안됩니다.

이렇게 계층의 역할과 책임이 불필요하게 다른 계층으로 전이되는 문제를 추상화의 누수(leaky abstraction)라고 부르기도 합니다.

이는 단순히 구조의 좋고 나쁨보다 큰 문제가 될 수도 있는데, 예컨대 위의 예에선 만일 추후 인벤토리 서비스 구현을 데이터베이스 기반에서 JDBC와 무관한 구현체로 바꾸어야 한다면 구현체 하나만 갈아 끼우는 게 아니라 해당 계층을 참조하는 모든 API의 시그네쳐와 클라이어트의 try-catch 구문을 수정해야하는 일이 되기 때문입니다.

더구나 try-catch의 경우 호출하는 메서드 시그네쳐에서 있던 예외가 빠졌다고 컴파일 오류가 발생하는 것도 아니기 때문에 일일이 IDE의 도움을 받아 고치지 않으면 찾기도 어려울 수 있습니다.

어느 쪽이건 구현체를 바꾸었다고 모든 계층의 소스를 다 바꾸어야 한다면 애초에 계층화, 추상화의 의미가 없어진다고 할 수 있을 것입니다.

이런 문제를 해소하기 위해 불필요한 체크드 예외는 사용하지 않는 정책을 택하는 프로젝트들이 많고, 예컨대 스프링 프레임워크 같은 경우 데이터관련 모듈에서도 SQLException을 추상화한 DataAccessExceptionException이 아닌 RuntimeException을 상속하도록 설계된 것입니다.

한 편, 비즈니스 예외를 체크드 예외로 구현할 경우 앞서 언급한 대로 이러한 문제에서 자유로운 편인데, 이 경우 API는 비즈니스 규칙을 최대한 상세하게 명시하고 구현상의 세부사항은 감추는 것이 좋다는 원칙에도 부합하기 때문에 체크드 예외를 전혀 사용하지 않는 대신 비즈니스 규칙에만 사용하는 절충을 하기도 합니다.

보통 체크드 예외를 배울 때 "이런 예외는 try-catch나 throws로 처리해야한다'라는 '어떻게'만 가르치지 '왜'에 대한 내용을 다루지 않는 것 같아서 조금 내용을 보충해보았습니다.


수정 이력

2016-11-26 10:41:26 에 아래 내용에서 변경 됨 #4

체크드 예외 부분이 잘 이해되지 않는 분들을 위해 예제를 통해 조금 보충해 보겠습니다.

예를들어 온라인 쇼핑몰의 장바구니 모듈이 내부적으로 상품 정보 서비스를 참조하고, 해당 서비스에서는 유효하지 않은 상품 정보에 대해 체크드 예외를 발생시킨다고 가정하겠습니다. (일반적인 설계는 아니지만 설명을 위한 것이니 양해 부탁드립니다):

interface ShoppingCart {
    void add(long productId, int count) throws NoSuchProductException;
}

interface Inventory {
    Product getProductDetails(long productId) throws NoSuchProductException;
}

이 경우 인벤토리의 `getProductDetails`메서드는 체크드 예외를 발생시키기 때문에, 이를 호출하는 쪽에서는 반드시 try-catch로 처리하거나 다시 throw해야합니다.

참고로, 이런 예제에서 `ShoppingCart`의 구현체의 `add` 메서드 내부에서 try-catch를 하는 것은 일반적으로 말하면 좋지 못한 접근입니다. 예외처리의 중요한 원칙 중 하나는 책임지지 않을 바엔 잡지도 말라는 내용이 있습니다.

즉, 예외는 항상 의미있는 방식으로 처리해야한다는 것이고 보통의 경우 이는 사용자에게 메시지를 보여주거나 다른 페이지로 리다이렉트 시키거나 아니면 디버깅을 위해 로그를 남기는 등의 작업을 뜻합니다.

문제는 해당 작업은 모두 호출 체인의 가장 앞 쪽에 위치한다는 것이고, 따라서 중간에 있는 `ShoppingCart` 같은 계층에서 처리할 수 없다는 것입니다. 예컨대 리다이렉트를 하려면 HttpResponse를 참조해야하는데 서비스 계층에서 웹 계층의 그런 객체를 참조하는 건 바람직하지도 않고 정상적인 설계에서 가능하지도 않습니다.

예제로 돌아와서 보면, 이 경우 `ShoppingCart` 입장에서 최상의 선택은 단순히 해당 예외를 다시 던지는 것임을 알 수 있습니다. 이는 설계적 관점으로 봐도 만족스러운 결과인데, 왜냐하면 `ShoppingCart`의 `add`메서드의 시그네쳐에 `throws NoSuchProductException`이 포함됨으로 인해 이제 호출자에게 '존재하지 않는 상품은 카트에 담을 수 없다'는 비즈니스 규칙을 명시적으로 밝힐 수 있기 때문입니다.

하지만 체크드 예외가 비즈니스 규칙이 아닌 오류를 나타내는 경우라면 이야기가 조금 달라집니다. 이젠 `Inventory`의 구현체가 데이터베이스를 통해 상품 정보를 조회한다고 가정해보겠습니다.

대부분의 JDBC API는 체크드 예외인 SQLException을 시그네쳐에 포함하고 있는 것은 잘 아시는 내용일 것입니다. 그럼 'Inventory'의 'getProductDetail' 구현에서 해당 예외를 어떻게 처리해야할까요?

우선 try-catch로 잡는 방법을 생각해볼 수 있지만, 이는 앞서 이야기한 이유로 좋은 선택이 아닙니다. 만일 비즈니스 요구조건이 장바구니 추가시 오류가 발생하면 이전 페이지로 리다이렉트 하는 것이라면 컨트롤러 계층에서 세 단계나 떨어져 있는 Inventory에서 이를 어떻게 처리할 수 있을까요?

사실 애초에 화면에 대한 내용은 해당 계층의 역할이 아닙니다. 계층화된 설계를 하는 이유 자체가 각 계층은 자신의 계층이 맡은 역할과 책임(role and responsibility)을 명확히 분리하기 위함이고, 그런 면에서 보면 예시의 설계에서 Inventory의 책임은 상품 조회 요청을 처리하는 것이지, 해당 요청이 웹 MVC에서 왔던 외부 모듈의 RMI 호출에서 왔건 간여할 바가 아닌 것입니다.

그렇다면 앞선 NoSuchProductException의 예에서 처럼 throws를 하는 것은 어떨까요?

이 경우 우선 `Inventory` 인터페이스의 구현체는 반드시 데이터베이스 기반으로 구현해야한다는 의도치 않은 제약이 생겨버립니다. 최초의 예제에서는 해당 인터페이스의 요구조건을 충족하기만 하면 구현체 입장에선 데이터베이스를 쓰건 NoSQL을 쓰건 XML 파일에서 읽어오건 전혀 문제될 것이 없었습니다.

하지만 인터페이스의 'add'메서드 시그네쳐에 SQLException이 들어가는 순간 해당 인터페이스는 JDBC API에 의존성이 생겨버리는 것입니다.

더 안좋은 것은, 이런 문제는 계층을 타고 상위로 전파된다는 것입니다. `Inventory.getProductDetails()`가 위와 같은 이유로 SQLException을 시그네쳐에 포함할 수 밖에 없었다면 마찬가지 이유로 'ShoppingCart.add()' 또한 해당 예외를 내부에서 처리하지 못하고 시그네쳐에 올릴 수 밖에 없습니다.

이 경우 예컨대 쇼핑 카트 서비스를 호출하는 MVC의 컨트롤러 계층에 최종 try-catch를 할 책임이 넘어가게 되는데, 이는 명백하게 잘못된 설계임을 어렵지않게 짐작할 수 있을 것입니다.

컨트롤러 입장에서 아는 것은 `ShoppingCart.add`에 제품 번호와 수량을 넣으면 장바구니에 담긴다는 것 뿐이지, 해당 서비스가 내부에서 `Inventory`라는 다른 서비스를 참조하는지, 또 그 서비스의 특정 구현체가 데이터베이스를 기반으로 작성되었는지는 관심도 없고 알아서도 안됩니다.

이렇게 계층의 역할과 책임이 불필요하게 다른 계층으로 전이되는 문제를 추상화의 누수(leaky abstraction)라고 부르기도 합니다.

이는 단순히 구조의 좋고 나쁨보다 큰 문제가 될 수도 있는데, 예컨대 위의 예에선 만일 추후 인벤토리 서비스 구현을 데이터베이스 기반에서 JDBC와 무관한 구현체로 바꾸어야 한다면 구현체 하나만 갈아 끼우는 게 아니라 해당 계층을 참조하는 모든 API의 시그네쳐와 클라이어트의 try-catch 구문을 수정해야하는 일이 되기 때문입니다.

더구나 try-catch의 경우 호출하는 메서드 시그네쳐에서 있던 예외가 빠졌다고 컴파일 오류가 발생하는 것도 아니기 때문에 일일이 IDE의 도움을 받아 고치지 않으면 찾기도 어려울 수 있습니다.

어느 쪽이건 구현체를 바꾸었다고 모든 계층의 소스를 다 바꾸어야 한다면 애초에 계층화, 추상화의 의미가 없어진다고 할 수 있을 것입니다.

이런 문제를 해소하기 위해 불필요한 체크드 예외는 사용하지 않는 정책을 택하는 프로젝트들이 많고, 예컨대 스프링 프레임워크 같은 경우 데이터관련 모듈에서도 SQLException을 추상화한 DataAccessExceptionException이 아닌 RuntimeException을 상속하도록 설계된 것입니다.

한 편, 비즈니스 예외를 체크드 예외로 구현할 경우 앞서 언급한 대로 이러한 문제에서 자유로운 편인데, 이 경우 API는 비즈니스 규칙을 최대한 상세하게 명시하고 구현상의 세부사항은 감추는 것이 좋다는 원칙에도 부합하기 때문에 체크드 예외를 전혀 사용하지 않는 대신 비즈니스 규칙에만 사용하는 절충을 하기도 합니다.

보통 체크드 예외를 배울 때 "이런 예외는 try-catch나 throws로 처리해야한다'라는 '어떻게'만 가르치지 '왜'에 대한 내용을 다루지 않는 것 같아서 조금 내용을 보충해보았습니다.

2016-11-26 10:34:45 에 아래 내용에서 변경 됨 #3

체크드 예외 부분이 잘 이해되지 않는 분들을 위해 예제를 통해 조금 보충해 보겠습니다.

예를들어 온라인 쇼핑몰의 장바구니 모듈이 내부적으로 상품 정보 서비스를 참조하고, 해당 서비스에서는 유효하지 않은 상품 정보에 대해 체크드 예외를 발생시킨다고 가정하겠습니다. (일반적인 설계는 아니지만 설명을 위한 것이니 양해 부탁드립니다):

interface ShoppingCart {
    void add(long productId, int count) throws NoSuchProductException;
}

interface Inventory {
    getProductDetails(long productId) throws NoSuchProductException;
}

이 경우 인벤토리의 `getProductDetails`메서드는 체크드 예외를 발생시키기 때문에, 이를 호출하는 쪽에서는 반드시 try-catch로 처리하거나 다시 throw해야합니다.

참고로, 이런 예제에서 `ShoppingCart`의 구현체의 `add` 메서드 내부에서 try-catch를 하는 것은 일반적으로 말하면 좋지 못한 접근입니다. 예외처리의 중요한 원칙 중 하나는 책임지지 않을 바엔 잡지도 말라는 내용이 있습니다.

즉, 예외는 항상 의미있는 방식으로 처리해야한다는 것이고 보통의 경우 이는 사용자에게 메시지를 보여주거나 다른 페이지로 리다이렉트 시키거나 아니면 디버깅을 위해 로그를 남기는 등의 작업을 뜻합니다.

문제는 해당 작업은 모두 호출 체인의 가장 앞 쪽에 위치한다는 것이고, 따라서 중간에 있는 `ShoppingCart` 같은 계층에서 처리할 수 없다는 것입니다. 예컨대 리다이렉트를 하려면 HttpResponse를 참조해야하는데 서비스 계층에서 웹 계층의 그런 객체를 참조하는 건 바람직하지도 않고 정상적인 설계에서 가능하지도 않습니다.

예제로 돌아와서 보면, 이 경우 `ShoppingCart` 입장에서 최상의 선택은 단순히 해당 예외를 다시 던지는 것임을 알 수 있습니다. 이는 설계적 관점으로 봐도 만족스러운 결과인데, 왜냐하면 `ShoppingCart`의 `add`메서드의 시그네쳐에 `throws NoSuchProductException`이 포함됨으로 인해 이제 호출자에게 '존재하지 않는 상품은 카트에 담을 수 없다'는 비즈니스 규칙을 명시적으로 밝힐 수 있기 때문입니다.

하지만 체크드 예외가 비즈니스 규칙이 아닌 오류를 나타내는 경우라면 이야기가 조금 달라집니다. 이젠 `Inventory`의 구현체가 데이터베이스를 통해 상품 정보를 조회한다고 가정해보겠습니다.

대부분의 JDBC API는 체크드 예외인 SQLException을 시그네쳐에 포함하고 있는 것은 잘 아시는 내용일 것입니다. 그럼 'Inventory'의 'getProductDetail' 구현에서 해당 예외를 어떻게 처리해야할까요?

우선 try-catch로 잡는 방법을 생각해볼 수 있지만, 이는 앞서 이야기한 이유로 좋은 선택이 아닙니다. 만일 비즈니스 요구조건이 장바구니 추가시 오류가 발생하면 이전 페이지로 리다이렉트 하는 것이라면 컨트롤러 계층에서 세 단계나 떨어져 있는 Inventory에서 이를 어떻게 처리할 수 있을까요?

사실 애초에 화면에 대한 내용은 해당 계층의 역할이 아닙니다. 계층화된 설계를 하는 이유 자체가 각 계층은 자신의 계층이 맡은 역할과 책임(role and responsibility)을 명확히 분리하기 위함이고, 그런 면에서 보면 예시의 설계에서 Inventory의 책임은 상품 조회 요청을 처리하는 것이지, 해당 요청이 웹 MVC에서 왔던 외부 모듈의 RMI 호출에서 왔건 간여할 바가 아닌 것입니다.

그렇다면 앞선 NoSuchProductException의 예에서 처럼 throws를 하는 것은 어떨까요?

이 경우 우선 `Inventory` 인터페이스의 구현체는 반드시 데이터베이스 기반으로 구현해야한다는 의도치 않은 제약이 생겨버립니다. 최초의 예제에서는 해당 인터페이스의 요구조건을 충족하기만 하면 구현체 입장에선 데이터베이스를 쓰건 NoSQL을 쓰건 XML 파일에서 읽어오건 전혀 문제될 것이 없었습니다.

하지만 인터페이스의 'add'메서드 시그네쳐에 SQLException이 들어가는 순간 해당 인터페이스는 JDBC API에 의존성이 생겨버리는 것입니다.

더 안좋은 것은, 이런 문제는 계층을 타고 상위로 전파된다는 것입니다. `Inventory.getProductDetails()`가 위와 같은 이유로 SQLException을 시그네쳐에 포함할 수 밖에 없었다면 마찬가지 이유로 'ShoppingCart.add()' 또한 해당 예외를 내부에서 처리하지 못하고 시그네쳐에 올릴 수 밖에 없습니다.

이 경우 예컨대 쇼핑 카트 서비스를 호출하는 MVC의 컨트롤러 계층에 최종 try-catch를 할 책임이 넘어가게 되는데, 이는 명백하게 잘못된 설계임을 어렵지않게 짐작할 수 있을 것입니다.

컨트롤러 입장에서 아는 것은 `ShoppingCart.add`에 제품 번호와 수량을 넣으면 장바구니에 담긴다는 것 뿐이지, 해당 서비스가 내부에서 `Inventory`라는 다른 서비스를 참조하는지, 또 그 서비스의 특정 구현체가 데이터베이스를 기반으로 작성되었는지는 관심도 없고 알아서도 안됩니다.

이렇게 계층의 역할과 책임이 불필요하게 다른 계층으로 전이되는 문제를 추상화의 누수(leaky abstraction)라고 부르기도 합니다.

이는 단순히 구조의 좋고 나쁨보다 큰 문제가 될 수도 있는데, 예컨대 위의 예에선 만일 추후 인벤토리 서비스 구현을 데이터베이스 기반에서 JDBC와 무관한 구현체로 바꾸어야 한다면 구현체 하나만 갈아 끼우는 게 아니라 해당 계층을 참조하는 모든 API의 시그네쳐와 클라이어트의 try-catch 구문을 수정해야하는 일이 되기 때문입니다.

더구나 try-catch의 경우 호출하는 메서드 시그네쳐에서 있던 예외가 빠졌다고 컴파일 오류가 발생하는 것도 아니기 때문에 일일이 IDE의 도움을 받아 고치지 않으면 찾기도 어려울 수 있습니다.

어느 쪽이건 구현체를 바꾸었다고 모든 계층의 소스를 다 바꾸어야 한다면 애초에 계층화, 추상화의 의미가 없어진다고 할 수 있을 것입니다.

이런 문제를 해소하기 위해 불필요한 체크드 예외는 사용하지 않는 정책을 택하는 프로젝트들이 많고, 예컨대 스프링 프레임워크 같은 경우 데이터관련 모듈에서도 SQLException을 추상화한 DataAccessExceptionException이 아닌 RuntimeException을 상속하도록 설계된 것입니다.

한 편, 비즈니스 예외를 체크드 예외로 구현할 경우 앞서 언급한 대로 이러한 문제에서 자유로운 편인데, 이 경우 API는 비즈니스 규칙을 최대한 상세하게 명시하고 구현상의 세부사항은 감추는 것이 좋다는 원칙에도 부합하기 때문에 체크드 예외를 전혀 사용하지 않는 대신 비즈니스 규칙에만 사용하는 절충을 하기도 합니다.

보통 체크드 예외를 배울 때 "이런 예외는 try-catch나 throws로 처리해야한다'라는 '어떻게'만 가르치지 '왜'에 대한 내용을 다루지 않는 것 같아서 조금 내용을 보충해보았습니다.

2016-11-26 10:31:52 에 아래 내용에서 변경 됨 #2

체크드 예외 부분이 잘 이해되지 않는 분들을 위해 예제를 통해 조금 보충해 보겠습니다.

예를들어 온라인 쇼핑몰의 장바구니 모듈이 내부적으로 상품 정보 서비스를 참조하고, 해당 서비스에서는 유효하지 않은 상품 정보에 대해 체크드 예외를 발생시킨다고 가정하겠습니다. (일반적인 설계는 아니지만 설명을 위한 것이니 양해 부탁드립니다):

interface ShoppingCart {
    void add(long productId, int count) throws NoSuchProductException;
}

interface Inventory {
    getProductDetails(long productId) throws NoSuchProductException;
}

이 경우 인벤토리의 `getProductDetails`메서드는 체크드 예외를 발생시키기 때문에, 이를 호출하는 쪽에서는 반드시 try-catch로 처리하거나 다시 throw해야합니다.

참고로, 이런 예제에서 `ShoppingCart`의 구현체의 `add` 메서드 내부에서 try-catch를 하는 것은 일반적으로 말하면 좋지 못한 접근입니다. 예외처리의 중요한 원칙 중 하나는 책임지지 않을 바엔 잡지도 말라는 내용이 있습니다.

즉, 예외는 항상 의미있는 방식으로 처리해야한다는 것이고 보통의 경우 이는 사용자에게 메시지를 보여주거나 다른 페이지로 리다이렉트 시키거나 아니면 디버깅을 위해 로그를 남기는 등의 작업을 뜻합니다.

문제는 해당 작업은 모두 호출 체인의 가장 앞 쪽에 위치한다는 것이고, 따라서 중간에 있는 `ShoppingCart` 같은 계층에서 처리할 수 없다는 것입니다. 예컨대 리다이렉트를 하려면 HttpResponse를 참조해야하는데 서비스 계층에서 웹 계층의 그런 객체를 참조하는 건 바람직하지도 않고 정상적인 설계에서 가능하지도 않습니다.

예제로 돌아와서 보면, 이 경우 `ShoppingCart` 입장에서 최상의 선택은 단순히 해당 예외를 다시 던지는 것임을 알 수 있습니다. 이는 설계적 관점으로 봐도 만족스러운 결과인데, 왜냐하면 `ShoppingCart`의 `add`메서드의 시그네쳐에 `throws NoSuchProductException`이 포함됨으로 인해 이제 호출자에게 '존재하지 않는 상품은 카트에 담을 수 없다'는 비즈니스 규칙을 명시적으로 밝힐 수 있기 때문입니다.

하지만 체크드 예외가 비즈니스 규칙이 아닌 오류를 나타내는 경우라면 이야기가 조금 달라집니다. 이젠 `Inventory`의 구현체가 데이터베이스를 통해 상품 정보를 조회한다고 가정해보겠습니다.

대부분의 JDBC API는 체크드 예외인 SQLException을 시그네쳐에 포함하고 있는 것은 잘 아시는 내용일 것입니다. 그럼 'Inventory'의 'getProductDetail' 구현에서 해당 예외를 어떻게 처리해야할까요?

우선 try-catch로 잡는 방법을 생각해볼 수 있지만, 이는 앞서 이야기한 이유로 좋은 선택이 아닙니다. 만일 비즈니스 요구조건이 장바구니 추가시 오류가 발생하면 이전 페이지로 리다이렉트 하는 것이라면 컨트롤러 계층에서 세 단계나 떨어져 있는 Inventory에서 이를 어떻게 처리할 수 있을까요?

사실 애초에 화면에 대한 내용은 해당 계층의 역할이 아닙니다. 계층화된 설계를 하는 이유 자체가 각 계층은 자신의 계층이 맡은 역할과 책임(role and responsibility)을 명확히 분리하기 위함이고, 그런 면에서 보면 예시의 설계에서 Inventory의 책임은 상품 조회 요청을 처리하는 것이지, 해당 요청이 웹 MVC에서 왔던 외부 모듈의 RMI 호출에서 왔건 간여할바가 아닌 것입니다.

그렇다면 앞선 NoSuchProductException의 예에서 처럼 throws를 하는 것은 어떨까요?

이 경우 우선 `Inventory` 인터페이스의 구현체는 반드시 데이터베이스 기반으로 구현해야한다는 제약이 생겨버립니다. 최초의 예제에서는 해당 인터페이스의 요구조건을 충족하기만 하면 구현체 입장에선 데이터베이스를 쓰건 NoSQL을 쓰건 XML 파일에서 읽어오건 전혀 문제될 것이 없었습니다.

하지만 인터페이스의 'add'메서드 시그네쳐에 SQLException이 들어가는 순간 해당 인터페이스는 JDBC API에 의존성이 생겨버리는 것입니다.

더 안좋은 것은, 이런 문제는 계층을 타고 상위로 전파된다는 것입니다. `Inventory.getProductDetails()`가 위와 같은 이유로 SQLException을 시그네쳐에 포함할 수 밖에 없었다면 마찬가지 이유로 'ShoppingCart.add()' 또한 해당 예외를 내부에서 처리하지 못하고 시그네쳐에 올릴 수 밖에 없습니다.

이 경우 예컨대 쇼핑 카트 서비스를 호출하는 MVC의 컨트롤러 계층에 최종 try-catch를 할 책임이 넘어가게 되는데, 이는 명백하게 잘못된 설계임을 어렵지않게 짐작할 수 있을 것입니다.

컨트롤러 입장에서 아는 것은 `ShoppingCart.add`에 제품 번호와 수량을 넣으면 장바구니에 담긴다는 것 뿐이지, 해당 서비스가 내부에서 `Inventory`라는 다른 서비스를 참조하는지, 또 그 서비스의 특정 구현체가 데이터베이스를 기반으로 작성되었는지는 관심도 없고 알아서도 안됩니다.

이렇게 계층의 역할과 책임이 불필요하게 다른 계층으로 전이되는 문제를 추상화의 누수(leaky abstraction)라고 부르기도 합니다.

이는 단순히 구조의 좋고 나쁨보다 큰 문제가 될 수도 있는데, 예컨대 위의 예에선 만일 추후 인벤토리 서비스 구현을 데이터베이스 기반에서 JDBC와 무관한 구현체로 바꾸어야 한다면 구현체 하나만 갈아 끼우는 게 아니라 해당 계층을 참조하는 모든 API의 시그네쳐와 클라이어트의 try-catch 구문을 수정해야하는 일이 되기 때문입니다.

더구나 try-catch의 경우 호출하는 메서드 시그네쳐에서 있던 예외가 빠졌다고 컴파일 오류가 발생하는 것도 아니기 때문에 일일이 IDE의 도움을 받아 고치지 않으면 찾기도 어려울 수 있습니다.

어느 쪽이건 구현체를 바꾸었다고 모든 계층의 소스를 다 바꾸어야 한다면 애초에 계층화, 추상화의 의미가 없어진다고 할 수 있을 것입니다.

이런 문제를 해소하기 위해 불필요한 체크드 예외는 사용하지 않는 정책을 택하는 프로젝트들이 많고, 예컨대 스프링 프레임워크 같은 경우 데이터관련 모듈에서도 SQLException을 추상화한 DataAccessExceptionException이 아닌 RuntimeException을 상속하도록 설계된 것입니다.

한 편, 비즈니스 예외를 체크드 예외로 구현할 경우 앞서 언급한 대로 이러한 문제에서 자유로운 편인데, 이 경우 API는 비즈니스 규칙을 최대한 상세하게 명시하고 구현상의 세부사항은 감추는 것이 좋다는 원칙에도 부합하기 때문에 체크드 예외를 전혀 사용하지 않는 대신 비즈니스 규칙에만 사용하는 절충을 하기도 합니다.

보통 체크드 예외를 배울 때 "이런 예외는 try-catch나 throws로 처리해야한다'라는 '어떻게'만 가르치지 '왜'에 대한 내용을 다루지 않는 것 같아서 조금 내용을 보충해보았습니다.

2016-11-26 10:30:19 에 아래 내용에서 변경 됨 #1

체크드 예외 부분이 잘 이해되지 않는 분들을 위해 예제를 통해 조금 보충해 보겠습니다.

예를들어 온라인 쇼핑몰의 장바구니 모듈이 내부적으로 상품 정보 서비스를 참조하고, 해당 서비스에서는 유효하지 않은 상품 정보에 대해 체크드 예외를 발생시킨다고 가정하겠습니다. (일반적인 설계는 아니지만 설명을 위한 것이니 양해 부탁드립니다):

interface ShoppingCart {
    void add(long productId, int count) throws NoSuchProductException;
}

interface Inventory {
    getProductDetails(long productId) throws NoSuchProductException;
}

이 경우 인벤토리의 `getProductDetails`메서드는 체크드 예외를 발생시키기 때문에, 이를 호출하는 쪽에서는 반드시 try-catch로 처리하거나 다시 throw해야합니다.

참고로, 이런 예제에서 `ShoppingCart`의 구현체의 `add` 메서드 내부에서 try-catch를 하는 것은 일반적으로 말하면 좋지 못한 접근입니다. 예외처리의 중요한 원칙 중 하나는 책임지지 않을 바엔 잡지도 말라는 내용이 있습니다.

즉, 예외는 항상 의미있는 방식으로 처리해야한다는 것이고 보통의 경우 이는 사용자에게 메시지를 보여주거나 다른 페이지로 리다이렉트 시키거나 아니면 디버깅을 위해 로그를 남기는 등의 작업을 뜻합니다.

문제는 해당 작업은 모두 호출 체인의 가장 앞 쪽에 위치한다는 것이고, 따라서 중간에 있는 `ShoppingCart` 같은 계층에서 처리할 수 없다는 것입니다. 예컨대 리다이렉트를 하려면 HttpResponse를 참조해야하는데 서비스 계층에서 웹 계층의 그런 객체를 참조하는 건 바람직하지도 않고 정상적인 설계에서 가능하지도 않습니다.

예제로 돌아와서 보면, 이 경우 `ShoppingCart` 입장에서 최상의 선택은 단순히 해당 예외를 다시 던지는 것임을 알 수 있습니다. 이는 설계적 관점으로 봐도 만족스러운 결과인데, 왜냐하면 `ShoppingCart`의 `add`메서드의 시그네쳐에 `throws NoSuchProductException`이 포함됨으로 인해 이제 호출자에게 '존재하지 않는 상품은 카트에 담을 수 없다'는 비즈니스 규칙을 명시적으로 밝힐 수 있기 때문입니다.

하지만 체크드 예외가 비즈니스 규칙이 아닌 오류를 나타내는 경우라면 이야기가 조금 달라집니다. 이젠 `Inventory`의 구현체가 데이터베이스를 통해 상품 정보를 조회한다고 가정해보겠습니다.

대부분의 JDBC API는 체크드 예외인 SQLException을 시그네쳐에 포함하고 있는 것은 잘 아시는 내용일 것입니다. 그럼 'Inventory'의 'getProductDetail' 구현에서 해당 예외를 어떻게 처리해야할까요?

우선 try-catch로 잡는 방법을 생각해볼 수 있지만, 이는 앞서 이야기한 이유로 좋은 선택이 아닙니다. 만일 비즈니스 요구조건이 장바구니 추가시 오류가 발생하면 이전 페이지로 리다이렉트 하는 것이라면 컨트롤러 계층에서 세 단계나 떨어져 있는 Inventory에서 이를 어떻게 처리할 수 있을까요?

사실 애초에 화면에 대한 내용은 해당 계층의 역할이 아닙니다. 계층화된 설계를 하는 이유 자체가 각 계층은 자신의 계층이 맡은 역할과 책임(role and responsibility)을 명확히 분리하기 위함이고, 그런 면에서 보면 예시의 설계에서 Inventory의 책임은 상품 조회 요청을 처리하는 것이지, 해당 요청이 웹 MVC에서 왔던 외부 모듈의 RMI 호출에서 왔건 간여할바가 아닌 것입니다.

그렇다면 앞선 NoSuchProductException의 예에서 처럼 throws를 하는 것은 어떨까요?

이 경우 우선 `Inventory` 인터페이스의 구현체는 반드시 데이터베이스 기반으로 구현해야한다는 제약이 생겨버립니다. 최초의 예제에서는 해당 인터페이스의 요구조건을 충족하기만 하면 구현체 입장에선 데이터베이스를 쓰건 NoSQL을 쓰건 XML 파일에서 읽어오건 전혀 문제될 것이 없었습니다.

하지만 인터페이스의 'add'메서드 시그네쳐에 SQLException이 들어가는 순간 해당 인터페이스는 JDBC API에 의존성이 생겨버리는 것입니다.

더 안좋은 것은, 이런 문제는 계층을 타고 상위로 전파된다는 것입니다. `Inventory.getProductDetails()`가 위와 같은 이유로 SQLException을 시그네쳐에 포함할 수 밖에 없었다면 마찬가지 이유로 'ShoppingCart.add()' 또한 해당 예외를 내부에서 처리하지 못하고 시그네쳐에 올릴 수 밖에 없습니다.

이 경우 예컨대 쇼핑 카트 서비스를 호출하는 MVC의 컨트롤러 계층에 최종 try-catch를 할 책임이 넘어가게 되는데, 이는 명백하게 잘못된 설계임을 어렵지않게 짐작할 수 있을 것입니다.

컨트롤러 입장에서 아는 것은 `ShoppingCart.add`에 제품 번호와 수량을 넣으면 장바구니에 담긴다는 것 뿐이지, 해당 서비스가 내부에서 `Inventory`라는 다른 서비스를 참조하는지, 또 그 서비스의 특정 구현체가 데이터베이스를 기반으로 작성되었는지는 관심도 없고 알아서도 안됩니다.

이렇게 계층의 역할과 책임이 불필요하게 다른 계층으로 전이되는 문제를 추상화의 누수(leaky abstraction)라고 부르기도 합니다.

이는 단순히 구조의 좋고 나쁨보다 큰 문제가 될 수도 있는데, 예컨대 위의 예에선 만일 추후 인벤토리 서비스 구현을 데이터베이스 기반에서 JDBC와 무관한 구현체로 바꾸어야 한다면 구현체 하나만 갈아 끼우는 게 아니라 해당 계층을 참조하는 모든 API의 시그네쳐와 클라이어트의 try-catch 구문을 수정해야하는 일이 되기 때문입니다.

더구나 try-catch의 경우 호출하는 메서드 시그네쳐에서 있던 예외가 빠졌다고 컴파일 오류가 발생하는 것도 아니기 때문에 일일이 IDE의 도움을 받아 고치지 않으면 찾기도 어려울 수 있습니다.

어느 쪽이건 구현체를 바꾸었다고 모든 계층의 소스를 다 바꾸어야 한다면 애초에 계층화, 추상화의 의미가 없어진다고 할 수 있을 것입니다.

이런 문제를 해소하기 위해 불필요한 체크드 예외는 사용하지 않는 정책을 택하는 프로젝트들이 많고, 예컨대 스프링 같은 경우 데이터관련 모듈에서도 SQLException을 추상화한 DataAccessExceptionException이 아닌 RuntimeException을 상속하도록 설계된 것입니다.

한 편, 비즈니스 예외를 체크드 예외로 구현할 경우 앞서 언급한 대로 이러한 문제에서 자유로운 편인데, 이 경우 API는 비즈니스 규칙을 최대한 상세하게 명시하고 구현상의 세부사항은 감추는 것이 좋다는 원칙에도 부합하기 때문에 체크드 예외를 전혀 사용하지 않는 대신 비즈니스 규칙에만 사용하는 절충을 하기도 합니다.

보통 체크드 예외를 배울 때 "이런 예외는 try-catch나 throws로 처리해야한다'라는 '어떻게'만 가르치지 '왜'에 대한 내용을 다루지 않는 것 같아서 조금 내용을 보충해보았습니다.