IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

Diagnosing Java Code: Run-on Initializer 버그 패턴
목 차:
Run-on initialization
run-ons에서 발생하는 다른 에러들
이러지도 저러지도 못할 때
예외 발생 메쏘드
이들을 고치는 것보다 더 낫다
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
이들을 고치는 것보다 더 낫다
참고 자료
필자 소개r
기사 평가
US 원문 읽기
Also in the Web services zone:
버그 패턴: 소개
Null Flag 버그 패턴
Diagnosing Java Code 컬럼
developerWorks newsletter 구독하기
 
Also in the Java zone:
튜토리얼
툴 및 제품
코드 및 컴포넌트
Articles
 
인자를 가진 생성자를 피함으로써 이 버그를 물리치기


Eric E. Allen
박사 과정, 자바 프로그래밍 팀, Rice University
2002년 4월

Column icon여러분은 종종 생성자 호출을 통해서 뿐 아니라 다양한 필드를 설정하기 위한 몇 가지 후속 작업을 통해 클래스를 초기화하는 코드를 본적이 있을 것이다. 그러한 후속 작업들은 불행히도 버그의 온상지이며, run-on initialization이라는 유형의 버그를 초래한다. Eric Allen은 run-on initializer 버그를 검토하고 이 버그를 피해야 하는 이유와 방법을 설명하며, 이 버그가 가져올 수 있는 피해를 최소화하는 방법을 보여준다.

Run-on initialization
다양한 이유 때문에 (대부분은 나쁜 이유) 여러분은 종종 클래스 생성자가 클래스의 모든 필드를 올바로 초기화할만큼 충분한 인자를 가지고 있지 않은 클래스 정의를 보게 될 것이다. 그러한 생성자는 하나의 생성자 호출보다는 여러 단계로 (초기화되지 않은 필드의 값을 설정하는) 인스턴스를 초기화하는 클라이언트 클래스들을 필요로 한다. 이런 방식으로 인스턴스를 초기화하는 절차에서는 에러가 발생하기 쉬운데, 나는 이를 run-on initialization이라고 부른다.이 절차에서 나온 버그 유형들은 유사한 증상과 치료책을 가지고 있으므로, 우리는 이들을 Run-on Initializer 버그 패턴이라는 하나의 패턴으로 묶을 수 있다.

다음 코드를 검토해보자.

Listing 1. 간단한 run-on initialization

class RestrictedInt {
  public Integer value;
  public boolean canTakeZero;
  
  public RestrictedInt(boolean _canTakeZero) {
    canTakeZero = _canTakeZero;
  }
  
  public void setValue(int _value) throws CantTakeZeroException {
    if (_value == 0) {
      if (canTakeZero) {
        value = new Integer(_value);
      }
      else {
        throw new CantTakeZeroException(this);
      }
    }
    else {
      value = new Integer(_value);
    }
  }
}

class CantTakeZeroException extends Exception {
  
  public RestrictedInt ri;
  
  public CantTakeZeroException(RestrictedInt _ri) {
    super("RestrictedInt can't take zero");
    ri = _ri;
  }
}

class Client {
  public static void initialize() throws CantTakeZeroException {
    RestrictedInt ri = new RestrictedInt(false);
    ri.setValue(0);
  }
}

불행히도, 이 클래스의 인스턴스에 대한 초기화 절차는 에러가 나기 쉽다. 여러분은 위의 코드에서 두번째 초기화 단계에 예외가 발생했다는 것을 발견했을 것이다. 그 결과, 이 단계 뒤에 설정되었어야 할 필드가 설정되지 않았다.

그러나 발생한 예외에 대한 처리자는 필드가 설정되지 않았다는 사실을 알지 못할 수 있다. 예외 복구 절차에 있어 만일 예외 처리자가 문제의 RestrictedInt의 value 필드에 접근한다면, 그 자신이 NullPointerException에 빠져버릴 수 있다.

이 경우 처리자가 존재하지 않는 것보다 더 나쁘다. 최소한 체크된 예외는 그 원인에 대한 몇 가지 단서를 가지고 있다. 그러나 NullPointerExceptions은 값이 처음에 왜 null로 설정되었는지에 관해 거의 정보를 가지고 있지 않기 때문에 진단하기가 어렵기로 악명이 높다. 더구나 이들은 초기화되지 않은 필드에 접근할 때만 나타난다. 그러한 접근은 아마도 버그의 원인 (즉 처음에 필드를 초기화하는데 실패한 것)으로부터 멀리 떨어진 지점에서 이루어질 것이다.

물론 run-on initialization 버그에서 발생할 수 있는 다른 에러들도 있다.

발생할 수 있는 다른 에러들
발생할 수 있는 다른 에러들은 다음과 같다.

  • 초기화 코드를 작성하는 프로그래머는 초기화 단계들 중 하나를 삽입하는 것을 잊어버릴 수 있다.

  • 초기화 단계들은 순서대로 실행되어야 할 수 있는데, 프로그래머가 이를 모르고 순서 없이 구문을 실행시킬 수 있다.

  • 초기화되고 있는 클래스가 바뀔 수 있다. 새로운 필드가 추가되거나 이전 필드가 삭제될 수 있다. 그 결과 모든 클라이언트에서의 모든 초기화 코드가 필드를 올바로 설정하기 위해 수정되어야 한다. 수정된 코드의 상당수가 유사하겠지만, 한 copy만 빠져도 버그가 발생할 수 있다. 따라서 run-on initializers는 rogue tiles 버그 패턴이 되기 쉽다. (Rogue Tile 버그 패턴에 대한 필자의 글 참조)

run-on initialization과 관련된 모든 문제들 때문에, 모든 필드를 초기화하는 생성자를 정의하는 것이 좋다. 위의 예제에서 RestrictedInt에 대한 생성자는 자신의 value 필드를 초기화하기 위해 int를 취해야 한다. 어떤 필드를 초기화하지 않은 채 두는 클래스의 생성자를 포함시킬만한 좋은 이유란 결코 없다. 클래스를 처음부터 작성할 때 이것은 따르기에 그렇게 어려운 원칙은 아니다.

그러나 클래스가 생성자 내의 모든 필드를 초기화하지 않는 대단위의 코드 기반에 대해 작업해야 할 때 그리고 코드 전반에 걸쳐 run-on initializers가 있는 경우에는 어떤가? 나는 이와 같은 상황을 여러 번 마주쳤다.

이러지도 저러지도 못할 때

불행히도, 생성자 내의 모든 필드들을 초기화하지 않는 클래스를 가진 레거시 코드 기반에서 작업하는 것은 대부분의 프로그래머들에게 흔한 일이다. 레거시 코드 기반이 광범위하며, 문제 있는 클래스에 접근하는 클라이언트가 많을수록 여러분은 생성자 서명을 수정하려 들지 않을 것이다. 특히 그러한 코드에 대한 단위 테스트가 부족했다면 더욱 그러할 것이다. 분명히 여러분은 문서화되지 않은 불변값을 중지시키는 것으로 끝맺게 될 것이다.

그러한 상황에서 종종 최상의 방법은 레거시 코드를 버리고 새로 시작하는 것이다! 이 말은 미친 소리같이 들릴 수도 있지만, 이와 같은 코드 내의 버그를 고치는데 드는 시간이 그것을 재작성하는 데 걸리는 시간보다 더 많아지기 쉽다. 여러 번 나는 이와 같은 문제를 가진 대단위 레거시 코드를 붙잡고 고군분투했지만 결국 처음부터 새로 시작했더라면 하고 바라게 되었다.

그러나 코드를 버리는 것을 선택할 수 없을 경우 여러분은 다음과 같은 간단한 실행 방법을 도입함으로써 애러가 발생할 가능성을 제어하려고 시도할 수 있다.

  • 필드를 (null이 아닌) 기본 값으로 초기화한다.
  • 여분의 생성자를 사용하도록 포함시킨다.
  • Include an isInitialized method in the class. 클래스에 isInitialized 메쏘드를 포함시킨다.
  • Construct special classes to represent the default values. 기본 값을 표현하는 특수 클래스를 생성한다.

우리가 왜 이 방법들을 따라야 하는지 살펴보자.

필드를 (null이 아닌) 기본 값으로 초기화하기

필드를 기본 값으로 채움으로써 여러분은 여러분 클래스의 인스턴스가 언제든지 잘 정의된 상태로 있을 것임을 보증할 수 있다. 이러한 방법은 여러분이 별로도 다르게 지정하지 않는 한 null 값을 취할 참조 유형에게 특히 중요하다.

왜 그럴까? null 값을 불필요하게 사용하면 결과적으로 NullPointerExceptions이 발생하기 때문이다. NullPointerExceptions은 나쁘다. 한 가지 이유를 들자면 이들은 버그의 실제 원인에 대해 거의 정보를 제공하지 않는다. 또 다른 이유는 이들이 버그의 실제 원인으로부터 아주 멀리 떨어진 곳에 나타나기 쉽다는 것이다.

무슨 수를 써서라도 이들을 피하도록 한다. 여러분이 클래스가 아직 완전히 초기화되지 않았음을 나타내기 위해 null을 사용하고자 한다면 Null Flag 버그 패턴에 대한 필자의 글을 읽으면 도움이 될 것이다.

여분의 생성자 포함시키기

추가적인 생성자들을 포함시키면 여러분은 이들을 새로운 문맥에서 사용할 수 있는데, . 이 새로운 문맥에서는 새로운 run-on initialization 작업이 일어날 필요가 없다. 일부 문맥들은 이의 사용이 제한되어있기 때문에 다른 문맥들에서 또다시 초기화시킬 필요가 없기 때문이다.

클래스에 isInitialized 메쏘드 포함시키기

여러분은 인스턴스가 초기화되었는지를 신속하게 나타내도록 하기 위해 클래스에 isInitialized 메쏘드를 포함시킬 수 있다. 그러한 메쏘드는 run-on initialization이 필요한 클래스를 작성할 때는 거의 항상 좋은 아이디어이다.

이러한 클래스들을 여러분 자신이 유지보수하지 않는 경우 여러분은 그러한 isInitialized 메쏘드를 여러분 자신의 유틸리티 클래스에 둘 수 있다.

결국, 인스턴스가 초기화되지 않았고 이를 외부에서 알 수 있는 경우에 여러분은 이 결과를 체크하기 위한 메쏘드를 작성할 수 있다. (이것이 RuntimeException을 발생시키는 보통 좋지 않은 방법을 사용하게끔 하더라도)

기본 값을 표현하는 특수 클래스 생성하기

필드들을 null로 채우기보다는 기본 값들을 표현할 수 있도록 특수 클래스(싱글톤과 같은)를 만들라. 그리고 나서 기본 생성자 내의 여러분의 필드에 이 클래스들의 인스턴스를 채운다. 그러면 NullPointerException을 만날 확률도 적어질 뿐 아니라 이러한 필드들이 부적절하게 접근될 때 어떤 오류가 발생하는지를 세밀하게 제어할 수 있게 될 것이다.

예를 들어, 우리는 RestrictedInt 클래스를 다음과 같이 수정할 수 있다.

Listing 2. NonValues를 가진 RestrictedInts


class RestrictedInt implements SimpleInteger {
  public SimpleInteger value;
  public boolean canTakeZero;
  
  public RestrictedInt(boolean _canTakeZero) {
    canTakeZero = _canTakeZero;
    value = NonValue.ONLY;
  }
  
  public void setValue(int _value) throws CantTakeZeroException {
    if (_value == 0) {
      if (canTakeZero) {
        value = new DefaultSimpleInteger(_value);
      }
      else {
        throw new CantTakeZeroException(this);
      }
    }
    else {
      value = new DefaultSimpleInteger(_value);
    }
  }
  
  public int intValue() {
    return ((DefaultSimpleInteger)value).intValue();
  }
}

interface SimpleInteger {
}

class NonValue implements SimpleInteger {
    
  public static NonValue ONLY = new NonValue();
    
  private NonValue() {}
  
}


class DefaultSimpleInteger implements SimpleInteger {
  private int value;
  
  public DefaultSimpleInteger(int _value) {
    value = _value;
  }
  
  public int intValue() {
    return value;
  }
}

이제 이 필드에 접근하는 여러분의 클라이언트 클래스 중 어떤 것이 결과 element에 intValue 작업을 수행해야 할 경우, NonValue가 그러한 작업을 지원하지 않기 때문에 이들은 우선 DefaultSimpleInteger를 캐스트해야 할 것이다.

위 접근 방식의 장점은 이 메쏘드 호출이 기본 값에서 작동하지 않는다는 사실을 여러분이 코드 내에서 캐스트하는 것을 잊어버릴 때마다 계속적으로 상기시켜 줄 것이라는 점이다 (컴파일러 에러로). 또한 런타임시에 여러분이 이 필드에 접근하고 이 필드가 기본 값을 가지고 있을 경우 여러분은 ClassCastException을 얻을 것인데, 이것은 NullPointerException보다 훨씬 더 많은 정보를 가지고 있을 것이다. ClassCastException은 실제로 거기에 무엇이 있었는지 뿐 아니라 프로그램이 거기에서 어떻게 될지도 알려줄 것이다.

단점은 성능이다. 필드에 접근할 때마다 프로그램이 캐스트를 수행해야 할 것이다.

컴파일 에러 메시지를 무시하고 싶다면 또 다른 솔루션은 SimpleInteger 인터페이스에 intValue 메쏘드를 포함시키는 것이다. 그러면 여러분은 여러분이 원하는 에러가 무엇이든간에 그것을 발생시키는 메쏘드를 가진 기본 클래스에 이 메쏘드를 구현할 수 있다 (그리고 여러분이 원하는 어떤 정보든지 포함시킬 수 있다). 다음 예제가 이를 보여준다.

Listing 3. 예외를 발생시키는 NonValues

class RestrictedInt implements SimpleInteger {
  public SimpleInteger value;
  public boolean canTakeZero;
  
  public RestrictedInt(boolean _canTakeZero) {
    canTakeZero = _canTakeZero;
    value = NonValue.ONLY;
  }
  
  public void setValue(int _value) throws CantTakeZeroException {
    if (_value == 0) {
      if (canTakeZero) {
        value = new DefaultSimpleInteger(_value);
      }
      else {
        throw new CantTakeZeroException(this);
      }
    }
    else {
      value = new DefaultSimpleInteger(_value);
    }
  }
  
  public int intValue() {
    return value.intValue();
  }
}

interface SimpleInteger {
  public int intValue();
}

class NonValue implements SimpleInteger {
    
  public static NonValue ONLY = new NonValue();
    
  private NonValue() {}
    
  public int intValue() {
    throw new 
      RuntimeException("Attempt to access an int from a NonValue");
  }
}


class DefaultSimpleInteger implements SimpleInteger {
  private int value;
  
  public DefaultSimpleInteger(int _value) {
    value = _value;
  }
  
  public int intValue() {
    return value;
  }
}

이 솔루션은 ClassCastException보다 훌륭한 에러 진단을 제공할 수 있다. 또한 런타임시에 캐스트가 필요하지 않기 때문에 더 효과적이기도 하다. 그러나 이 솔루션은 모든 접근 지점에서 필드에 어떤 값이 가능할지에 관해 여러분이 생각하도록 요구하지 않을 것이다.

여러분이 어떤 솔루션을 사용하기로 선택할 지는 부분적으로는 여러분의 선호에, 그리고 부분적으로는 여러분 프로젝트의 성능과 강력성 제약에 달려 있다.

이제 언뜻 보기에는 완전히 틀린 것처럼 보이는 기법을 살펴 보자.

예외만을 발생시키는 메쏘드 포함시키기

처음에 여러분은 이 방법이 본질적으로 잘못된 것, 그리고 클래스는 실제로 데이터에서 처리할 수 있는 메쏘드만을 포함해야 한다는 직관에 반대된 것처럼 생각될 것이다. 특히 여러분이 객체 지향 프로그래밍에 관해 프로그래머들을 교육시키고 있을 때 그러한 클래스를 포함시키는 것이 혼란스러울 수 있다.

예를 들어, 아래의 Listing 4와 5에 나오는 Lists에 대한 클래스 계층을 정의하는 두 가지 가능한 방법을 검토해 보자.

Listing 4. 범용 getters를 가지지 않은 Lists

abstract class List {}

class Empty extends List {}

class Cons extends List {
  Object first;
  List rest;
  
  Cons(Object _first, List _rest) {
    first = _first;
    rest = _rest;
  }
  
  public Object getFirst() {
    return first;
  }
  
  public List getRest() {
    return rest;
  }
}

Listing 5. 인터페이스에 getter를 가진 Lists

abstract class List {
  public abstract Object getFirst();
  public abstract Object getRest();
}

class Empty extends List {
  public Object getFirst() {
   throw new RuntimeException("Attempt to take first of an empty list");
  }
  
  public List getRest() {
   throw new RuntimeException("Attempt to take rest of an empty list");
  }
}

class Cons extends List {
  Object first;
  List rest;
  
  Cons(Object _first, List _rest) {
    first = _first;
    rest = _rest;
  }
  
  public Object getFirst() {
    return first;
  }
  
  public List getRest() {
    return rest;
  }
}

객체 지향 언어가 생소한 프로그래머라면 첫번째 버전의 List (범용 getters가 없는 것) 뒤에 숨은 동기가 덜 혼란스러울 것이다. 직관적으로, 클래스는 실제 작업을 수행하지 않는 메쏘드라면 그 메쏘드를 포함시켜서는 안 된다. 그러나 기본 클래스를 다루기 위한 위의 고려사항은 이 예제에서도 동일하게 잘 적용된다.

여러분 코드에 캐스트를 계속 삽입하는 것은 상당히 성가스러울 수 있고 코드가 매우 장황해질 수 있다. 또한 클래스 캐스트는 특히 List와 같이 자주 호출되는 유틸리티의 경우 성능 면에 상당한 영향을 줄 수 있다.

모든 설계 방식과 마찬가지로 이 방식은 이 방식의 기반이 되는 동기를 고려할 때 가장 잘 적용된다. 그러나 그 동기가 항상 적용될 수는 없을 것이다. 따라서, 그 동기가 적용될 수 없을 때는 이 방식이 사용되어서는 안 된다.

이들을 고치는 것보다 더 낫다

여러분은 Run-on Initializer 버그가 약간 다르다는 것을 알아차렸을 것이다 (여러분이 필자의 다른 버그 패턴 관련 글을 읽었다면). 이번 글에서 나는 이 버그를 바로 고치기보다 버그의 근본 원인에 관해 작업하는 방법에 대한 몇 가지 아이디어를 제공하였다. 내가 이들에 대해 작업해야 했던 경우가 많았기 때문이다. 그것은 그렇게 좋은 시간은 아니었다.

여전히, 우리가 언급한 고려 사항들이 가리키듯이 run-on initializations을 피하는 것이 더 좋다. 그러나 여러분이 이들을 다루어야 할 때 여러분은 최소한 여러분 자신을 보호할 수 있다. 다음은 이 버그 패턴을 간추린 것이다.

  • 패턴 : Run-on Initializer

  • 증상 : 초기화되지 않은 필드 중 하나에 접근할 때 NullPointerException 발생

  • 원인 : 생성자가 모든 필드를 직접 초기화하지 않은 클래스

  • 치료책 및 예방책 : 생성자 내의 모든 필드를 초기화한다. 더 나은 값이 사용될 수 없을 때는 기본 값에 대한 특수 클래스를 사용한다. 더 나은 값이 사용될 수 있는 경우를 다루기 위해 여러 생성자를 포함시킨다. isInitialized 메쏘드를 포함시킨다.

다음 몇 달 동안 우리는 버그 패턴이라는 주제로 되돌아 갈 것이다. 다음 달에는 자바 언어에서 발생하는 몇 가지 플랫폼에 독립적인 버그를 다룰 것이다. 일반적인 믿음과는 달리 이들에 대한 면역이 되어 있지 않다.

참고자료

  • bitterjava.com : 상상할 수 있는 가장 형편없고 나쁘고 혐오스러운 자바 프로그램, 아키텍처 및 설계 -- 이용할 수 있는 모든 나쁜 코딩 관행을 찾아서 조롱하는 전용 사이트

  • "쓴 자바의 맛 " (developerWorks, March 2002)에서 Bruce Tate는 왜 안티 패턴 (실제로 명확하게 부정적인 결과를 가져 오는 문제에 대한 일반적인 솔루션)이 설계 패턴에 필요하고 그것과 보완적인 짝이 되는지, 그리고 어떻게 작동하는지를 보여준다.

  • XP 사이트: extreme programming의 개념 요약

  • JUnit 웹 사이트 : 프로그램 테스팅 메쏘드를 다루는 무수한 소스들의 흥미 있는 많은 글들로의 링크 제공

  • Eric Allen의 Diagnosing Java Code 컬럼

  • DrJava : Rice 대학이 개발한 무료의 오픈 소스 자바 통합 개발 환경. read-eval-print 루프를 가짐.

  • developerWorks Java technology zone : 자바 기술과 관련된 다른 자료들

필자소개

Eric Allen은 Cornell 대학에서 컴퓨터 공학 및 수학 학사 학위를 받았으며 Rice 대학의 자바 프로그래밍 언어팀에서 박사 과정을 밟고 있다. 학위를 마치기 위해 Rice 대학으로 돌아가기 전에 Eric은 Cycorp 사의 선임 자바 소프트웨어 개발자였고, 또한 JavaWorld의 자바 초급자 토론 포럼의 진행자이기도 하다. 그는 자바 언어에서 소스와 바이트코드 레벨 양쪽에서의 의미론적 모델 개발과 정적인 분석 툴에 관심을 가지고 있다. Eric은 Rice 대학이 실험적으로 개발한, 추가된 언어 기능을 가진 자바 언어의 확장판인 NextGen 프로그래밍 언어용 컴파일러의 수석 개발자이기도 했다. 또한 초보자용으로 설계된 오픈 소스 Java IDE인 DrJava의 프로젝트 관리자이다.

 



이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인보호정책  |  법률  |  문의