[Java/Android] Custom Exception의 필요성에 대해 알아보자

mhyun, Park·2022년 11월 19일
4

Exception 이란?

Exception Handling이란 잘못된 하나의 코드/로직으로 인해 전체 System이 무너지는 결과를 방지하기 위한 기술적인 처리이다.
그리고 개발자는 예외가 발생할 상황을 미리 예측하여 처리할 수 있기에 예외를 구분하고 그에 대한 처리 방법을 명확히 알고 적용하는 것이 중요하다.

Standard Exception

Java/Android에서는 기본적으로 Exception 처리를 위해 다음과 같이 다양한 Throwable class를 제공하고 있다.

image
그리고 이러한 Exception은 2가지 종류로 나뉘며 모든 Exception에 대한 설명은 많은 Blog에서 쉽게 찾을 수 있다.

  • Checked Execption
    • 예외처리가 필수이며, 처리하지 않으면 컴파일이 되지 않는다
    • Runtime Exception 이외에 있는 모든 예외
    • ex) IOException, SQLException 등
  • Unchecked Execption
    • 컴파일 때 체크되지 않고 Runtime에 발생하는 Execption
    • ex) NullPointerException, IndexOutOfBoundException 등
  • Exception Information

이처럼 이미 Java/Android는 사용자가 상황에 따라 적절히 예외를 처리할 수 있도록 많은 예외를 만들어 두었고
Effective Java 또한 아래와 같은 이유로 표준 예외 사용을 권장하고 있다.

  • 배우기 쉽고 사용하기 편리한 API를 만들 수 있다.
  • 표준 예외를 사용한 API는 가독성이 높다.
  • 예외를 재사용하는 것이 좋다. 예외 클래스의 수가 적을수록 프로그램의 메모리 사용량이 줄고, 클래스 적재 시간도 줄어든다.

그리고 실제로도, 대부분의 상황에선 Standard Exception만으로도 충분한 이점을 얻을 수 있다.

1. Standard Exception의 message만으로도 충분한 의미 전달이 가능하다.

class InvalidNameException(
    override val message: String,
) : RuntimeException(message)

위 Custom Exception의 이름만 봐도 잘못된 이름일 경우 발생한 Exception임을 알 수 있다.
하지만, 단지 이 하나의 이유를 위해서 자체 Exception class를 만드는 것은 지나친 구현이라고 생각이 든다.
왜냐하면 Standard Exception에서 기본적으로 지원하는 IllegalArgumentException 을 사용한 후 message에 Invalid Name 임을 알릴 수 있기 때문이다.

throw IllegalArgumentException("Invalid Name($name)")

2. Standard Exception을 활용하면 가독성이 높아진다.

앞서 언급했듯이 Java에선 이미 다양한 상황을 고려하여 수많은 Standard Exception class를 지원하고 있다.

  • IllegalStateException : 작업을 수행하기에 올바르지 않은 State인 경우
  • IllegalArgumentException : 인수로 부적절한 값이 들어온 경우
  • UnsupportedOperationException : 지원하지 않는 연산을 수행한 경우
    ...

그리고 이미 우리는 해당 Exception 쓰임에 대해 익숙하고 잘 알고 있다.
반면 Custom Exception과 같이 낯선 Exception을 만나게 된다면 어찌됐든 해당 Exception에 대해 구체적으로 어떤 상황에서 어떻게 써야할지
새로 파악해야하기에 이에 따른 비용이 발생하는 부분 또한 염두해야한다.

Custom Exception

하지만, Standard Exception은 말 그대로 Standard 일뿐이기에
Standard Exception만으로 Cover 하기 힘든 경우는 분명 존재한다.

그렇다면, 어떠한 상황에서 Custom Exception을 정의하면 좋을까??

1. 여러 예외 상황과 맞물려 비즈니스 로직의 명확성을 높이기 위한 경우

Exception은 독립적으로 존재하며 여러 Exception의 의미와 중복된채로 사용될 수 있다. 즉, 상호 배제적이지 않다.
예를 들어 로또 Service를 만든다고 생각할 때, List의 element size가 6이 아닐 경우

(1) 지원되지 않은 List의 size가 아니라며 IllegalArgumentException 을 사용할 수도 있고
(2) 해당 size로는 연산을 수행시킬 수 없다는 UnsupportedOperationException 을 사용할 수 있다.

하지만, 이러한 Exception들은 공용적으로 많은 부분에서 사용되는 Standard Exception이기에
중복된 Exception catch를 해야할 가능성이 농후하기도하고 비즈니스 로직의 명확성을 높이기 위해서
InvalidLottoElementSizeException 과 같은 이름을 가진 Exception을 생성하여 활용할 수 있다.

2. 예외에 대한 응집도를 향상시키고 싶은 경우

Class란 동일한 속성과 행위를 수행하는 객체의 집합이다. 즉, 해당 Class에서 관련 내용을 최대한 관리하겠다는 정의이다.
Standard Exception의 이름과 message 만으로도 충분한 정보를 제공할 수 있지만

(1) 전달해야하는 message가 많을 경우엔 예외 발생 코드가 길어지게 되고
(2) 같은 예외를 발생시키는 장소가 많아지면 중복 에외 코드가 생기게 되는 문제가 생긴다.

이에 더 나아가서, 서로 다른 Class에서 같은 예외까지 발생되는 경우라면... 해당 Exception에 대한 책임 소재가 불분명해지게 된다.
이러한 경우 예외에 필요한 message, 전달할 정보의 data, data 가공 method들이 응집된 Custom Exception을 정의한다면 객체의 책임이 분리된 형태로 예외 처리를 할 수 있게 된다.

3. 예외 발생 후처리를 용이하게 하고 싶은 경우

예외에는 상속 관계가 적용되어 있다.
이 때문에 RuntimeExceptionException 을 catch 하게 되면 프로그램 내에서 발생하는 거의 모든 Exception에 대해 Handling이 가능하게 되는데 이러한 부분으로 인해 개발자가 의도하지 않은 예외까지 모두 catch하여 혼란을 야기할 수 있다.

또한, 아래와 같은 코드를 봐보도록 하자

class SomeController {
    
    fun some(request: SomeRequest): ResponseEntity<Void> {
        val something: Something = someService.someMethod(request)
        if (!someValidate(something)) {
            throw IllegalArgumentException()
        }
        
        SomeExternalLibrary.doSomething(something)
        return ResponseEntity.ok().build()
    }
}

위의 코드를 수행하던 중 IllegalArgumentException 이 발생했다면, 과연 someValidate를 수행한 결과값으로 인해서 발생했을까??
그렇지 않다. ExternalLibrary에서 예외를 뱉어낼 가능성도 있기 때문이다.

즉, 재사용성이 높은 Standard Exception 특징으로 인해 Exception 발생 위치를 정확하게 파악하기 힘들 수 있다는 것이다.
이러한 경우 Custom Exception을 사용하여 Exception Handling에 혼란스러움을 줄여줄 수 있다.

4. 상세한 예외 정보를 제공하고 싶은 경우

임의의 List가 있고 length를 벗어난 index가 접근될 경우, 다들 알다시피 IndexOutOfBoundsException 이 발생한다.
하지만 해당 Exception이 발생하기 전에 직접 Handling 하고 싶은 경우 아래와 같은 IndexOutOfBoundsException을 직접 throw 하여 이용해야 하지만, IndexOutOfBoundsException은 message 혹은 index만 전달하는 API만 제공하고 있다

IndexOutOfBoundsException

따라서, IllegalIndexException 과 같은 Custom Exception을 만들어
기존보다 상세한 예외 정보를 제공하여 다음과 같이 활용할 수 있다.

private const val MESSAGE = "범위가 벗어났어"

class IllegalIndexException(
    targetList: List<*>,
    index: Int
) : IndexOutOfBoundsException("$MESSAGE - size: ${target.size}, index: $index")

5. 예외 생성 비용을 줄이고 싶은 경우

작성하는 Code에서 Exception catching이 빈번하게 발생시킬 가능성이 있을 경우 Custom Exception을 활용하여 예외 생성 비용을 줄일 수 있다.
Standard Exception들은 개발자의 Debugging 편의성을 위해 예외 발생 시 아래와 같은 stack trace를 print하고 있다.

image

하지만, try/catch 를 통해 의도적으로 handling 하는 경우 stack trace를 사용하지 않게 된다.
개발자의 Debugging 편의성을 위해 비용을 들여 생성했지만 사용되지 않고 그대로 사라지는 것이다..

이러한 연유로 stack trace를 생성하는 Throwable#fillInStackTrace 를 override 하여 사용자 정의대로 trace를 재정의함으로써 stack trace 생성 비용을 줄일 수 있다.

@Synchronized
override fun Throwable fillInStackTrace() {
    return this
}

Conclusion

이번엔 Custom Exception의 필요성과 Custom Exception을 어떤 상황에서 활용할 수 있는지에 관해 정리해봤다.
Custom Exception을 사용하면 무조건 좋다!고 할 수 없는것처럼
현재 System 상황에 맞춰 적절히 Standard/Custom Exception을 사용하면 좋을 것 같다.

Reference

1. custom exception은 언제 써야할까?
2. 명쾌한 Custom Exception in Java

profile
Android Framework Developer

0개의 댓글