티스토리 뷰

Error vs Exception

컴파일 단계와 런타임 단계에서 발생하는 에러를 각각 컴파일 에러, 런타임 에러라고 합니다.

 

컴파일 에러의 경우 자바 컴파일러가 문법 검사를 해주어 컴파일 전에 수정이 가능합니다.

 

이후 컴파일이 정상적으로 수행되어도 프로그램이 실행 중인 런타임에 에러가 발생할 수 있습니다. 런타임 에러는 개발자가 컨트롤할 수 있는 영역인 Exception과 기반 시스템의 문제로 발생하는 Error가 있습니다. 에러를 도식화하면 다음과 같습니다.

 

이미지 출처: http://java5tutor.info/java/flowcontrol/exceptionover.html

 

Checked Exception vs Runtime Exception

Exception은 다시 Checked Exception과 Runtime Exception으로 구분됩니다.

 

  Checked Exception Runtime(Unchecked) Exception
처리 여부 반드시 예외처리를 해주어야 함 명시적인 처리를 강제하지 않음
확인 시점 컴파일 시 런타임 시
트랜잭션 처리 roll-back 하지 않음 roll-back 수행
대표적인 예외 Exception의 상속을 받는 하위 클래스 중 Runtime Exception을 제외한 모든 예외

- IOException
- SQLException
Runtime Exception 하위 예외

- NullPointException
- IndexOutOfBoundException
- SystemException

 

Checked Exception의 경우 try-catch 나 throws를 통해 반드시 예외 처리를 해줘야 하지만 Runtime Exception의 경우 대부분 개발자의 부주의로 발생하기 때문에 필수로 처리해야 하는 것은 아닙니다.

 

Checked Exception의 경우 컴파일 단계에서 명확하게 예외 체크가 가능하지만, Runtime Exception의 경우 실행 과정 중 특정 논리에 의해 발생하는 예외이기 때문에 컴파일 단계에서는 확인할 수 없습니다.

 

또 한가지 확인할 점은 트랜잭션 처리 방법입니다. Checked Exception의 경우 예외가 발생해도 진행된 작업에 대해서는 다시 roll-back 되지 않습니다. Runtime Exception은 예외가 발생하는 경우 진행된 작업을 다시 roll-back 하게 됩니다.

 

간단한 예제로 살펴보겠습니다.

 

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

  private final MemberRepository memberRepository;

  // (1) RuntimeException 예외 발생
  public Member createUncheckedException() {
    final Member member = memberRepository.save(new Member("Alice"));
    if (true) {
      throw new RuntimeException();
    }
    return member;
  }

  // (2) IOException 예외 발생
  public Member createCheckedException() throws IOException {
    final Member member = memberRepository.save(new Member("Bob"));
    if (true) {
      throw new IOException();
    }
    return member;
  }
}

 

(1) Runtime Exception 발생의 경우 Alice라는 Member는 commit 되지 않고 다시 rollback 됩니다. 하지만 (2) Checked Exception인 IOException이 발생하게 되면 Bob이라는 Member는 그대로 트랜잭션이 commit까지 진행됩니다.

 

예외 처리 방식

그렇다면 이러한 Exception을 처리하는 방식에 대해 알아보겠습니다.

 

(1) try-catch로 직접 처리 (예외 복구)

 

public void sendFile(String fileName) {
    File file;
    try {
        // 예외가 발생할지도 모르는 코드
        file = findFile(fileName);
    } catch (FileNotFoundException e) {
        // 예외가 발생했을 경우 해야할 처리
        file = findFile("default.png");
    } finally {
        // 예외가 발생하든 발생하지 않든 처리할 내용
    }
}

 

try-catch로 직접 예외 처리를 진행하는 경우 애플리케이션은 정상적인 흐름으로 진행되고 종료됩니다.

 

(2) throws로 예외처리 회피

 

public class ObjectMapperUtil {

  private final ObjectMapper objectMapper = new ObjectMapper();

  // 예외처리를 throws를 통해서 위임하고 있습니다.
  public String writeValueAsString(Object object) throws JsonProcessingException {
    return objectMapper.writeValueAsString(object);
  }

  // 예외처리를 throws를 통해서 위임하고 있습니다.
  public <T> T readValue(String json, Class<T> clazz) throws IOException {
    return objectMapper.readValue(json, clazz);
  }
}

 

위 예제의 경우 메소드 실행 중 예외가 발생하면 해당 메소드를 호출한 쪽으로 예외 처리를 전달하게 됩니다. 해당 메소드를 사용하는 쪽에서는 try-catch를 통해 예외를 받아 처리하거나 다시 또 다른 상위 호출 메소드로 throws 해야 합니다.

 

(3) 다른 예외로 전환하여 throw

 

public class ObjectMapperUtil {

  private final ObjectMapper objectMapper = new ObjectMapper();

  public String writeValueAsString(Object object) {
    try {
      return objectMapper.writeValueAsString(object);
    } catch (JsonProcessingException e) {
      // 더 명시적인 새로운 예외 발생 (throw)
      throw new JsonSerializeFailed(e.getMessage());
    }
  }

  public <T> T readValue(String json, Class<T> clazz) {
    try {
      return objectMapper.readValue(json, clazz);
    } catch (IOException e) {
      // 더 명시적인 새로운 예외 발생 (throw)
      throw new JsonDeserializeFailed(e.getMessage());
    }
  }
}

 

try-catch로 원래 예외를 잡은 다음 더 구체적인 다른 예외를 발생(throw)시키면 호출한 쪽에서 예외를 받아서 처리할 때 좀 더 명확하게 인지할 수 있습니다.

 

Checked Exception 중 복구(직접 처리)가 불가능한 예외의 경우 더 구체적인 Unchecked Exception으로 변경하여 throw 하게 되면 이 메소드를 사용하는 곳에서는 아무 처리를 하지 않아도 되면서 발생한 예외에 대해 보다 구체적인 정보를 알 수 있습니다.

 

따라서 예외 복구 전략이 명확하고 그것을 처리할 수 있다면 try-catch를 통해 Checked Exception을 처리하는 것이 좋고 그렇지 않다면 보다 구체적인 UnChecked Exception으로 예외를 전환하여 명확한 예외를 전달하는 것이 효과적입니다.

 

Custom Exception

표준 예외 클래스로 대부분의 예외 처리를 할 수 있지만 의미를 명확히 전달하고 추가적인 예외 처리 작업을 하고 싶은 경우 커스텀 예외를 생성할 수 있습니다.

 

커스텀 예외를 생성할 때는 먼저 Checked Exception을 생성할지, Runtime Exception을 생성할지 정해야 합니다. 사용자가 예외 상황을 복구할 수 있는 경우 Checked Exception을 생성하여 문제를 해결할 기회를 주고 예외 처리를 강제화할 수 있습니다. 사용자가 사용법을 어겨서 발생한 예외이거나 예외 상황이 발생한 시점에 사용자가 복구할 수 없고 프로그램을 종료하는 것이 더 안전하다면 Runtime Exception을 생성합니다.

 

// Checked Exception을 구현하고 싶은 경우
class DivideException extends Exception {
    // 에러 처리 구현 내용
    DivideException() {
        super();
    }
    
    DivideException(String message) {
        super(message);
    }
}

// Runtime Exception을 구현하고 싶은 경우
class DivideRuntimeException extends RuntimeException {
    // 에러 처리 구현 내용
    DivideRuntimeException() {
        super();
    }
    
    DivideRuntimeException(String message) {
        super(message);
    }
}

 

Checked Exception의 경우 발생하는 위치에서 항상 try-catch로 예외 처리를 하거나 throws를 통해 예외를 전달해야 합니다.

 

// (1) try-catch로 직접 발생되는 예외 처리를 하는 경우
class Calculator{
    int left, right;
    
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    
    public void divide(){
        if(this.right == 0){
            try {
                throw new DivideException("0으로 나누는 것은 허용되지 않습니다.");
            } catch (DivideException e) {
                e.printStackTrace();
            }
        }
        System.out.print(this.left/this.right);
    }
}

// (2) throws로 예외를 전달하는 경우
// 이 경우는 divide() 메소드를 호출하는 쪽에서 다시 try-catch로 예외 처리를 하거나 다시 throws로 예외를 더 상위로 전달해야 합니다.
class Calculator{
    int left, right;
    
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    
    public void divide() throws DivideException {
        if(this.right == 0){
            throw new DivideException("0으로 나누는 것은 허용되지 않습니다.");
        }
        System.out.print(this.left/this.right);
    }
}

 

Runtime Exception의 경우는 예외 처리를 강제화하지 않습니다.

 

class Calculator{
    int left, right;
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void divide(){
        if(this.right == 0){
            throw new DivideRuntimeException("0으로 나누는 것은 허용되지 않습니다.");
        }
        System.out.print(this.left/this.right);
    }
}

 

커스텀 예외를 언제 사용하는 것이 좋은지 등에 대한 더 자세한 내용은 아래 링크를 참고하시기 바랍니다.

 

참고

- Exception: www.nextree.co.kr/p3239/

- Exception: cheese10yun.github.io/checked-exception/

- Custom Exception: woowacourse.github.io/javable/post/2020-08-17-custom-exception/

- Custom Exception: edu.goorm.io/learn/lecture/41/%EB%B0%94%EB%A1%9C%EC%8B%A4%EC%8A%B5-%EC%83%9D%ED%99%9C%EC%BD%94%EB%94%A9-%EC%9E%90%EB%B0%94-java/lesson/39283/%EB%82%98%EB%A7%8C%EC%9D%98-%EC%98%88%EC%99%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0

'JAVA' 카테고리의 다른 글

[Java Study 07] 패키지  (0) 2021.05.04
[Java Study 06] 상속  (0) 2021.04.27
[Java Study 05] 클래스(Class)  (0) 2021.04.24
[JAVA] 객체 지향 프로그래밍(OOP: Object Oriented Programming)  (0) 2021.04.23
[JAVA] JUnit 테스트  (0) 2021.04.20
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함