Java Run-Time 오류를 줄이기 위한 안전한 상수의 사용#2-1

[이미지 출처 - eHow.com]
지난 글에 이어 런타임시에 내가 작성한 상수 관련 코드가 문제아가 되지 않도록 하는 "고전적인 방법"에 대해 계속 얘기해볼까 합니다. 소주제는 "안전한 상수(type-safe constant)" 입니다.
  • 상수이해하기
  • 안전한 상수
  • Immutable 객체
  • 읽기전용 인터페이스
  • 복제하기(cloning)
  • 복제가능한 읽기전용 객체


타입을 가지는 상수 (Typed constant)


스타트렉의 두 외계종족(Borg와 Ferengi)를 타입을 가지는 상수로 정의해 봅시다.
public final class AlienRace {

    public static final AlienRace BORG = new AlienRace();
    public static final AlienRace FERENGI = new AlienRace();
 
    private AlienRace() {}

}
AlienRace 클래스의 특징은 다음과 같습니다.
  1. final 클래스로 제한함으로써 상속 방지
  2. 생성자의 접근제한자를 private으로 설정
  3. 필드가 자기 자신과 같은 타입의 객체를 참조(self-referential)
첫 번째 특징은 상속을 통해 자식 클래스를 생성하여 인자로 넘겨주는 경우를 막기위함입니다. 이 경우 자식클래스의 객체도 또한 AlienRace 타입이기 때문("is-a" 관계)에 추가적인 타입 검사코드가 필요할 수 있습니다. 두 번째는 생성자를 private으로 설정함으로써 더 이상 AlienRace 타입의 객체를 생성할 수 없게 했습니다. 즉, AlienRace 타입의 객체는 오직 BORG와 FERENGI 둘 밖에 없습니다. 마지막으로 클래스의 두 필드 또한 AlienRace 타입으로 설정했습니다.

다음은 위에서 작성한 AlienRace 상수필드를 테스트하는 코드입니다.
public class AlienRaceTest {

 public static void constantTest(AlienRace alien) {
  //no range checking required!!
  if(alien == AlienRace.BORG)
   //do something ...
   ;
  if(alien == AlienRace.FERENGI)
   //do something ...
   ;
 }

 public static void main(String[] args) {
  //case#1: OK!
  constantTest(AlienRace.BORG);
  
  //case#2: ERROR!!
  constantTest(new AlienRace());
  /*
   * public class FishTank {
   *   public static final int MAX_NUMBER_OF_FISH = 10;
   * }
   */
  //case#3: ERROR!!
  constantTest(FishTank.MAX_NUMBER_OF_FISH);
  
  System.exit(0);
 }
}
정수를 이용하여 상수를 정의했던 이전 경우와 달리, 테스트 메서드 constantTest(AlienRace alien)는 내부적으로 인자의 범위를 검사하는 코드가 필요없습니다. 그리고 constantTest() 메서드를 호출하는 문장에서 타입에 맞지 않는 인자전달은 컴파일러를 통해 미리 방지할 수 있습니다. 즉, AlienRace 타입의 BORG와 FERENGI만을 인자로 전달할 수 있도록 클래스를 작성한 것입니다.

타입을 가지는 객체를 상수처럼 사용하도록 클래스를 설계함으로써 적절하지 않은 인자전달 문제를 해결했습니다. 그렇다면 AlienRace 클래스로 인한 런타임 오류는 없는걸까요?

고민거리: null !!


다음 코드는 어떻게 동작할까요?
AlienRaceTest.constantTest(null);
어떤 의도에서든 다른 개발자가 constantTest() 메서드로 null을 넘겨주는 상황이 있을 수 있습니다. 이전에 작성한 AlienRace를 사용하는 메서드에서는 alien 객체의 메서드 호출과 같은 코드가 없기때문에 null로 인한 NullPointerException이 발생할 가능성은 없으나, 만약 AlienRace 클래스를 확장하여 getAlienName()과 같은 메서드를 추가한다면 null.getAlienName()로 인한 NullPointerException이 발생할 가능성이 있습니다.

null이 인자로 전달되는 이 상황을 어떻게 처리해야 할까요?

해결책?


첫 번째 솔루션은 이러한 상황이 정말 예외적인 상황이라 판단하고 NullPointerException을 처리하는 것입니다.

두 번째 솔루션의 근거는 null을 인자로 넘겨준 개발자의 의도가 무엇일까를 고민해보는 것입니다. 과연 왜 null을 넘겨줬을까? ㅠㅠ. 이 경우 null의 의미를 확장하는 개념으로 "null은 기본값(default)이란 의미"를 부여하는 방법이 있습니다. 즉, null을 넘겨 받은 경우는 default 값으로 인식하겠다라고 정리하는 것입니다. 수정된 AlienRace 클래스를 살펴봅시다.
public final class AlienRace {

    //default constant
    public static final AlienRace BORG = null;

    public static final AlienRace FERENGI = new AlienRace();
 
    private AlienRace() {}

}
이전과 차이점은 BORG 객체에 null을 할당한 것 밖에 없습니다. 아래 테스트 코드에서는 AlienRace.BORG와 null은 모두 BORG로 인식하므로 둘 다 같은 결과를 출력하게 됩니다.
public static void main(String[] args) {
    //case#1: OK!
    constantTest(AlienRace.BORG);
  
    //case#2: OK!, use default value
    constantTest(null);
  
    System.exit(0);
}
지금까지 독특한 구조의 클래스 객체를 상수처럼 사용하는 방법으로 컴파일 타임에 잘못된 인자전달을 막는 방법과 null 상수 처리 방법에 대해 살펴봤습니다. 런타임 오류나 예외처리없이 null을 다루는 위 방법은 상태가 있는 상수객체인 경우 NullPointerException을 피할 수 없습니다. 상태를 가지는 상수객체의 NullPointerException 피해가기를 다음 글에서 다루도록 하겠습니다.

0 댓글