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


[이미지 출처 - eHow.com]
지난 글에서 객체를 상수처럼 사용하는 기본적인 방법에 대해 살펴보았습니다. 하지만 null 상수로 인한 런타임 오류를 최소화 하기 위한 몇 가지 해결책이 아직 개선해야 할 사항으로 남아있습니다. 본 글에서는 null 상수를 캡슐화하는 proxy 기법에 대해 설명합니다.

  • 상수이해하기
  • 안전한 상수
  • Immutable 객체
  • 읽기전용 인터페이스
  • 복제하기(cloning)
  • 복제가능한 읽기전용 객체

Review


마지막으로 작성한 AlienRace 클래스를 다시 살펴봅시다.
public final class AlienRace {

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

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

}
AlienRace 클래스를 사용하는 클라이언트 입장에서는 다음과 같은 코드도 런타임 오류의 가능성 없이 사용할 수 있습니다.
public static void constTest(AlienRace alien) {
    if(alien == AlienRace.BORG)
        //...
    if(alien == AlienRace.FERENGI)
        //...
}
public static void run() {
    //case#1.
    constTest(AlienRace.BORG);

    //case#2.
    constTest(AlienRace.FERENGI);

    //case#3: OK!! use default value BORG!!
    constTest(null);
}

문제는 AlienRace 클래스의 각 객체가 상태를 가질때입니다. 예를 들어, 각 상수마다 이름(Name) 속성을 가져야 할 경우, AlienRace 클래스에 String 타입의 name 필드와 getName()과 같은 selector를 추가할 수도 있겠지만, 기본적으로 null로 할당된 BORG는 생성자에서 이름 필드를 초기화 할 수도 없을 뿐더러, getName() 메서드도 호출할 수 없는 구조를 가지고 있습니다. 이러한 경우, BORG의 속성값을 대신 보관하고 있다가 속성값 요청시 대신 반환해 주는 대리자(proxy)를 이용하는 방법으로 문제를 해결할 수 있습니다.

중간단계에서 대리역할을 하는 proxy 클래스는 다음과 같습니다.

class ConstantProxy {

  private AlienRace mAlien;  
  private String mName;
 
  public ConstantProxy(AlienRace alien, String name) {
 mAlien = alien;
 mName = name;
  }

  public boolean isHolding(AlienRace alien) {
 return (mAlien == alien);
  }

  public String getName() {
 return mName;
  }

}

그리고 AlienRace 클래스는 다음과 같이 확장하였습니다.
public final class AlienRace {
 public static final AlienRace BORG = null;
 public static final AlienRace FERENGI = new AlienRace();
 
 private AlienRace() {}
 
 private static Vector<ConstantProxy> mProxies = new Vector<ConstantProxy>();
 static {
  mProxies.addElement(new ConstantProxy(BORG, "Borg"));
  mProxies.addElement(new ConstantProxy(FERENGI, "Ferengi"));
 }
 
 public static String valueOf(AlienRace alien) {
  ConstantProxy target = null;
  Enumeration<ConstantProxy> e = mProxies.elements();
  while(e.hasMoreElements()) {
   target = e.nextElement();
   if(target.isHolding(alien)) break;
  }
  return target.getName();
 }
}

수정된 AlienRace 클래스의 특징은 다음과 같습니다.
  1. 속성 필드를 ConstantProxy가 보관
  2. 클래스 로드시점에 두 proxy 객체를 미리 생성
  3. valueOf() 메서드를 통해 속성 필드 접근

AlienRace 클래스를 사용하는 클라이언트 입장에서는 AlienRace.BORG, AlienRace.FERENGI, AlienRace.valueOf() 메서드를 통해 두 상수객체를 안전하게 사용할 수 있습니다. 다음 코드는 테스트 코드입니다.
public class AlienRaceTest {

    public static void constantTest(AlienRace alien) {

        System.out.println(AlienRace.valueOf(alien));

    }

    public static void main(String[] args) {
        //case#1.
        constantTest(AlienRace.BORG);
  
        //case#2.
        constantTest(AlienRace.FERENGI);
  
        //case#3. use default value: BORG
        constantTest(null);
  
        System.exit(0);
    }
}
이전과 가장 큰 차이점은 인자로 null(기본값 BORG)을 넘겨준다 하더라도 속성 필드의 값을 얻을 수 있다는 점이고, constantTest() 메서드의 경우 AlienRace.valueOf() 메서드로 비교구문 없이 필드값을 출력할 수 있습니다.

지금까지 컴파일러의 도움을 통해 안전하게 상수를 정의하여 사용하는 방법에 대해 살펴봤습니다. Java1.5의 Enum 클래스가 지원되기 전 많이 논의된 주제이고, 한 번쯤 설계방법에 대해 고민해 보는것이 어떨까하여 포스팅을 했습니다.

상수에 대해서 얘기를 했으나, 사실 상수란 변하지 않는 속성을 가지고 있어야 진정한 상수입니다. 다음 글에서 immutable 객체에 대해 얘기해 보도록 하겠습니다.

1 댓글