Effective Java : 아이템 13. 완벽공략

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    완벽 공략 29. UncheckedException (왜 우리는 비검사 예외를 선호하는가?)

    • Unchecked Exception (언체크 예외)
      • 언체크 예외는 사용하기 편리함.
      • 컴파일 에러를 신경쓰지 않아도 됨.
      • try-catch로 감싸거나, 메서드 선언부에 선언하지 않아도 됨.
    • 예외 종류
      • UncheckedException : Error + RuntimeException을 상속 받음.
      • CheckedException : 나머지 에러를 상속 받음. 
    • Checked Exception
      • 왜 잡지 않은 예외를 메서드에 선언해야 하는가?
      • 메서드에 선언한 예외는 프로그래밍 인터페이스의 일부임. 해당 메서드(API)를 사용하는 클라이언트 코드가 반드시 알아야 하는 정보임. 이 정보를 알아야 해당 예외가 발생했을 상황에 대처하는 코드를 작성할 수 있기 때문임.
    • 비검사 예외는 그럼 왜 메서드에 선언하지 않아도 되는가?
      • 비검사 예외 역시 메서드에 선언할 수 있음.
      • 비검사 예외는 처리 및 복구 불가능한 예외를 의미함.
      • 이런 예외는 프로그램 전반에 걸쳐 어디서든 발생할 수 있기 때문에 이 모든 비검사 예외를 메서드에 선언하도록 강제한다면 프로그램의 명확도가 떨어짐. 
      • 예시 : 숫자를 0으로 나누거나, null 레퍼런스에 메서드를 호출하는 등

     

    우리는 왜 비검사를 선호하는가? 왜냐하면 UncheckedException을 던지는 코드는 작성하기 쉽고, 읽기 쉽기 때문이다. 또한 사용하는 쪽에서도 표면상으로는 UncheckedException을 처리하는 것이 강제되지 않기 때문에 사용하기 편리하다. 그렇지만 사용하기 편한다는 이유로 UnchekcedException을 사용하는 것은 좋은 예외 선택 기준이 되지는 못한다. 

    필요한 경우에는 CheckedException을 사용해야한다. CheckedException은 필요하면 메서드 시그니쳐에 throw를 표현해준다. 이것은 이 메서드를 사용할 때, 발생할 수 있는 예외가 있음을 알려주고 클라이언트 코드에서 이 예외에 대응해야하는 것을 알려준다. 즉, CheckedException은 API의 일종으로 볼 수 있다. 

     

    사용 기준은 다음과 같이 쓰자. 

    • UncheckedException : 복구 / 처리 불가능한 예외일 때, UncheckedException을 던짐. 
    • CheckedException : 복구해서 다른 작업을 할 수 있는 예외일 때, CheckedException을 던짐. 

    완벽 공략 30. TreeSet (AbstractSet을 확장한 정렬된 컬렉션)

    • TreeSet
      • 엘리먼트를 추가한 순서와 상관없이, TreeSet의 요소는 자연적인 순서 (Natural Order)에 따라 정렬됨. 
      • 기본적으로 오름차순으로 정렬됨. 
      • TreeSet은 스레드 안전하지 않음.
        • 스레드 세이프한 TreeSet을 가지고 싶다면, Collections.SyncronziedSet()을 이용해야 함. 

    TreeSet의 특성을 이해하기 위해서 아래 코드를 살펴보자.

    public class TreeSetExample {
    
        public static void main(String[] args) {
            TreeSet<Integer> numbers = new TreeSet<>();
            numbers.add(10);
            numbers.add(4);
            numbers.add(6);
            
            for (Integer number : numbers) {
                System.out.println(number);
            }
        }
    }

    위의 코드에서는 10 → 4 → 6순으로 Integer 객체를 전달했다. 그렇다면 Treeset에 있는 Element를 차례대로 출력하면 어떻게 될까?

    4 → 6 → 10

    위 순서로 출력된다. TreeSet은 들어간 순서가 아니라, 자연적인 순서 (Natural Order)에 따라서 정렬되기 때문이다. 여기서 Integer가 가지고 있는 Natural Order은 Integer의 Comparable 인터페이스를 통해 결정된다. 

    Integer는 Comparable 인터페이스를 구현해서 자연적인 순서를 알지만, 만약 Comparable 인터페이스가 구현되지 않은 PhoneNumber라는 객체를 Treeset에 넣으려고 한다면 어떻게 동작할까? Java는 PhoneNumber 클래스의 Natural Order를 모르기 때문에 ClassCastException이 발생한다.

    // ClassCastException 발생함.
    public class TreeSetExample {
    
        public static void main(String[] args) {
    
            TreeSet<PhoneNumber> numbers = new TreeSet<>();
            numbers.add(new PhoneNumber(123, 456, 780));
            numbers.add(new PhoneNumber(123, 456, 7890));
            numbers.add(new PhoneNumber(123, 456, 789));
    
            for (PhoneNumber number : numbers) {
                System.out.println(number);
            }
        }
    }

    10,4,6 순으로 넣었는데 출력해보면 4,6,10으로 나온다. 만약 자바가 모르는 PhoneNumber를 넣으면 어떻게 될까? PhoneNumber에 대한 Natrual Order를 모르기 때문에 RuntimeException이 발생한다. 즉, 동작하지 않는다. ClassCastException이 발생하는데, 내부적으로 Natural Order를 구할 때 Comparable 인터페이스를 사용하기 때문이다. PhoneNumber는 Comparable 인터페이스가 구현되지 않았기 때문이다. 

    Comparable 인터페이스가 구현되지 않았는데, 이 Comparable 인터페이스로 Cast 한 후에 비교를 하려고 하기 때문에 ClassCastException이 발생한다. 이처럼 사용자가 직접 정의한 클래스들을 TreeSet에 넣어주려면, Natural Order를 구할 수 있도록 방법을 마련해준다. 아래 두 가지 방법을 사용할 수 있다. 

    • PhoneNumber가 Comparable 인터페이스를 구현해야 함.
    • TreeSet을 생성할 때, Comparator를 넘겨줌. 

    예를 들면 아래 코드처럼 생성해주면, 더 이상 ClassCastException이 발생하지 않는다.

    public class TreeSetExample {
    
        public static void main(String[] args) {
    
            TreeSet<PhoneNumber> numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));
            numbers.add(new PhoneNumber(123, 456, 780));
            numbers.add(new PhoneNumber(123, 456, 7890));
            numbers.add(new PhoneNumber(123, 456, 789));
    
            for (PhoneNumber number : numbers) {
                System.out.println(number);
            }
        }
    }

    쓰레드 안전한 Treeset을 사용하고 싶다면?

    TreeSet 자체는 쓰레드 안전하지 않다. 만약 쓰레드 안전한 Treeset을 사용하고 싶다면, Treeset을 생성한 후 Syncronzied Collection으로 한번 감싸주면 된다. 위 코드를 쓰레드 안전하게 바꾼다면 아래와 같이 작성할 수 있다. 

    이렇게 구현된 Set은 Syncronized 키워드가 붙어있어서 성능적으로는 느려지지만, 쓰레드 안전하게 사용할 수 있다. 

    public class TreeSetExample {
    
        public static void main(String[] args) {
    
            TreeSet<PhoneNumber> numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));
            
            // 쓰레드 안전
            Set<PhoneNumber> phoneNumbers = Collections.synchronizedSet(numbers);
            phoneNumbers.add(new PhoneNumber(123, 456, 780));
            phoneNumbers.add(new PhoneNumber(123, 456, 7890));
            phoneNumbers.add(new PhoneNumber(123, 456, 789));
    
            for (PhoneNumber number : numbers) {
                System.out.println(number);
            }
        }
    }

    댓글

    Designed by JB FACTORY