2011년 6월 9일 목요일

J2SE 1.4 premiers Java’s assertion capabilities, 검증(assertion) 메커니즘의 이해

Summary
J2SE(Java 2 Platform, Standard Edition) 1.4 버전이 되면서 자바에 간단한 검증(assertion) 기능이 추가되었다. 간단히 얘기해, 검증이란 프로그램 실행 중에 당연히 ‘참(true)’이 되어야 할 부분을 확인하는 Boolean 표현식이다. 이 검증 기능을 지원하기 위해 J2SE 1.4 에서는 assert라는 키워드와 AssertionError 클래스가 추가되었고, java.lang.ClassLoader 에도 몇 개의 메소드들이 추가되었다. 이 글은 두 파트로 나누어 진행될 것이며, 그 중 첫 번째로 이번에서 검증 기능을 사용하는 메커니즘에 대해, 다음 번에서는 자바의 검증과 ‘제약에 의한 설계(Design by Contract)’와의 비교를 통해 검증 기법을 사용하는 방법론을 설명할 것이고 더불어 검증 기법 사용에 있어서의 설계/구현 상의 현안들을 점검해보도록 하겠다.

Bertrand Meyer가 자신의 Eiffel 프로그래밍 언어에 기본 기능으로 ‘제약에 의한 설계(Design by Contract)’를 추가한 것을 봐도 잘 알 수 있듯이, 검증 기법은 수년 동안 소프트웨어 공학의 주요 현안 중 하나로 논의되어 왔었다. 검증은 위의 ‘제약에 의한 설계’의 중요한 특징으로 그 역사는 최소한 1967년의 기사 “Assigning Meanings to Programs” (Proceedings of the Symposium on Applied Mathematics, Vol. 10, pp.19-32; American Mathematical Society, 1967)까지 거슬러 올라간다. 이 글에서 저자 Robert Floyd는 프로그램의 정확성을 체계적으로 검증해낼 수 있는 검증 기법 사용에 대해 논의했다. 여기서 프로그램의 정확성(correctness)이란 예측하지 못한 상황에 대처할 수 있는 시스템의 능력인 신뢰성과, 견고성을 이루는 중요한 특성을 말한다.
두 번째 글에서 설명하겠지만 검증 기법은 정확한 프로그램을 구현하는데 많은 도움을 준다. 검증 기능은 원래 자바의 전신인 Oak 언어에는 포함되어 있었지만 자바가 연구실에서 뛰쳐나와 전 세계 개발자들의 손에 들어가게 됐을 때는 아쉽게도 이를 포함하지 않고 있었다. 하지만 JCP(Java Community Process)의 자바 명세 요청(Java Specification Request) 41 에서 간단한 검증 수단을 자바에 추가할 것을 제안했고, J2SE가 1.4 버전으로 넘어오면서 드디어 이 기능이 다시 부활하게 된 것이다.

검증 기법은 개발자들이 프로그램 수행 중에 명확히 ‘참’이 될 것을 요구하는 부분이 실제로도 ‘참’이 됨을 검증하는 Boolean 표현식이다. 검증 기능을 사용함으로써 우리는 소프트웨어를 설계하고 구현함에 있어 기대치 못한 효과를 얻을 수 있다. 이 장에서 필자는 J2SE 1.4에서 새롭게 소개된 검증 기능을 사용하는 방법을 간단히 설명할 것이다. 그리고 다음 장에서는 검증 기법을 사용하는 방법론에 대해 다뤄보도록 하겠다.

검증문(Assertion) 선언
검증은 자바의 새로운 키워드 ‘assert’를 사용해 선언한다. 검증문은 다음과 같이 두 가지 형태를 갖는다.
1. assert expression1;
2. assert expression1 : expression2;

위의 두 문장에서 expression1 이 검증이 필요한 Boolean 표현식이다. 이 표현식은 개발자가 프로그램이 수행되는 동안 항상 ‘참(true)’이 되어야 한다고 요구하는 조건이 된다. 두 번째 문장에 있는 expression2 는 검증이 실패했을 때 상세 정보를 표현하기 위한 메시지로 String으로 변환 가능한 형태여야 한다. 다음은 첫 번째 형태의 검증문에 대한 예제들이다.
1. assert 0 < value;
2. assert ref != null;
3. assert count == (oldCount + 1);
4. assert ref.m1(parm);

검증을 요구하는 식은 반드시 boolean 값을 가져야 한다고 했다. 위 예제 중 앞의 세 개는 명확히 Boolean 형태의 결과값을 갖는다. 그리고 네 번째의 경우 m1(parm) 메소드가 반드시 boolean 값을 반환해야 한다. 만약 expression1 이 boolean 타입이 아니라면 컴파일 시에 에러가 발생할 것이다.
아래 예제는 m1(int) 메소드에서 검증문을 사용하고 있다.

public class Foo {
 public void m1( int value ) {
   assert 0 <= value;
   System.out.println( "OK" );
 }

 public static void main( String[] args ) {
   Foo foo = new Foo();
   System.out.print( "foo.m1(  1 ): " );
   foo.m1( 1 );
   System.out.print( "foo.m1( -1 ): " );
   foo.m1( -1 );
 }
}

main() 함수는 mi(int) 메소드를 두 번 호출한다. 한 번은 양수, 한 번은 음수를 입력값으로 주고 있는데 음수값이 들어올 경우는 검증 오류가 발생할 것이다. ‘assert’ 는 새로 도입된 키워드이기 때문에 이 예제를 실행시켜 보기 위해서는 J2SE 1.4와 호환되는 컴파일러를 사용해야 한다. 또한 ‘–source 1.4’ 라는 명령행 옵션을 주어 컴파일 시 검증 기능을 사용할 것임을 명시해야 한다. 옵션을 통해 검증을 지원하는 이유는 하위 호환성을 때문이다.

기본적으로(‘-source 1.4’ 옵션을 주지 않았을 경우) J2SE 1.4 컴파일러는 검증문을 허용하지 않는다. 하지만 소스 코드에 식별자나 레이블로 ‘assert’ 를 사용하고 있다면 컴파일러가 경고를 할 것이다. 즉, 이와 같은 경우 비록 1.3 이전의 컴파일러가 문제 없이 컴파일 했던 소스 코드라 하더라도 1.4의 컴파일러가 거부하게 된다. 단, JVM은 이를 구분하지 않기 때문에 이미 컴파일이 되어 있는 class 파일들을 수행시키는 데는 문제가 없다.

다음 명령은 Foo.java를 컴파일한다.
Javac –source 1.4 Foo.java

컴파일된 Foo.class 파일의 m1(int) 메소드는 검증 코드를 포함하고 있다. 하지만 컴파일러와 마찬가지로, 자바 가상 머신(Java Virtual Machine, 이하 VM) 역시 기본적으로는 검증 기능 수행하지 않는다. 다시 말해 자바의 환경이 기본적으로는 검증을 하지 않도록 설정되어 있다는 예기다.

이와 같은 하위 호환 정책은 시대에 좀 뒤쳐진 것이 아닌가 하는 생각이 들게 한다. 검증 기능은 자바에서 기본적으로 지원해야 할 만한 충분한 가치가 있다. 하위 호환을 위해서라면 컴파일 옵션으로 ‘-source 1.3’처럼 써야 하고 VM 역시 마찬가지이다. 하위 호환성에 대한 압박은 충분히 이해 하지만 예외적인 상황으로 치부되기엔 너무 중요한 기능이 아닌가 싶다. 그래도 필자의 넉두리는 이쯤에서 그만두고 계속 진행해 보자.

검증 기능 활성화
VM은 명령행 옵션에 따라 개개의 클래스 단위로 검증 기능을 활성/불활성화 시킬 수 있다. ‘-enableassertions’ 나 짧게 ‘-ea’ 옵션은 검증 기능을 활성화 시킨다. 다음과 같은 형태로 사용할 수 있다(본 글에서는 단축 형태로 사용하겠다).
1. –ea
2. –ea:<class name>
3. –ea:...
4. –ea:<package name>...

첫 번째 형태는 시스템 클래스를 제외한 모든 클래스들을 검증하도록 한다. 시스템 클래스들은 ‘-enablesystemassertions’나 ‘-esa’ 라는 별도의 옵션을 사용해야 한다. 시스템 클래스들을 별도로 취급하는 이유는 개발자들이 자바의 시스템 라이브러리에서의 검증 에러를 의심할 만한 상황은 거의 발생하지 않기 때문이다.

두 번째 형태는 명시된 클래스만을 검증한다. 마지막 두 경우는 패키지 단위의 설정 옵션으로, 세 번째는 기본 패키지를, 네 번째는 명시된 패키지를 검증한다.

두 번째와 네 번째의 사용시 주의해야 할 점은 VM이 명시된 클래스나 패키지가 실제로 존재하는 지 여부는 확인하지 않는다는 것이다. 우선 간략히 설명하고 자세한 내용은 글의 마지막 부분에서 다시 언급하겠다. ClassLoader 클래스는 클래스와 패키지 이름과 그 검증 여부에 대한 맵(map)을 갖고 있다. 클래스 로더(ClassLoader의 하위 클래스)들은 하나의 클래스를 읽어 들일 때, 해당 클래스명이 이 맵의 엔트리로 존재하는 지 여부를 판단한다. 따라서 맵에 존재하는 않는 클래스나 패키지는 절대 검증이 일어나지 않게 된다. 특이하게도 VM은 클래스나 패키지명을 해석할 때 실제 시스템에 존재하는 지 여부는 확인하지 않는다(역자주> 원격 객체를 통해 현 시스템에는 존재하는 않는 새로운 클래스나 패키지들을 동적으로 사용할 수 있기 때문이다).

패키지 단위의 검증 옵션에서 ‘*’ 가 아닌 ‘...’가 사용되고 있음을 집고 넘어가 보자. 추적을 뜻하는 ‘...’는 자바 검증 메커니즘의 특징 중 하나로 명시된 패키지 뿐 아니라 그 하위의 모든 패키지들을 검증하도록 지정한다. 예를 들어 ‘-ea:javax.swing...’라는 옵션은 모든 Swing 패키지들을 검증하도록 할 것이고, ‘-ea:javax.xml...’ 옵션이 주어지면 J2SE 1.4의 다섯 XML 패키지들에 대해 검증을 수행한다. 주제와 관련은 없지만 한 가지 재미있는 것은 javax.xml 밑의 다섯 패키지들은 javax.xml의 하위 패키지라 생각되지만 실상 javax.xml이라는 패키지는 존재하지도 않는다.

다음은 아무 옵션을 주지 않고 ‘Foo’를 실행시킨 결과이다(VM은 기본적으로 검증을 수행하지 않는다).
foo.m1( 1 ): OK
foo.m1( -1 ): OK

검증 기능이 불활성 상태이므로 두 번의 m1(int) 메소드 호출 모두 검증되지 않았다. 앞서 설명했듯이 검증을 하려면 별도의 명령행 옵션을 주어야 한다. 다음 셋 중 어떤 식으로든 Foo 클래스를 검증할 수 있다.
java –ea Foo
java –ea:Foo Foo
java –ea:... Foo

실행 결과를 보자.
foo.m1( 1 ): OK
foo.m1( -1 ): Exception in thread “main” java.lang.AssertionError
at Foo.m1(Foo.java:6)
at Foo.main(Foo.java:17)

‘1’을 인자로 주었을 경우는 무사히 검증을 통과했지만, ‘-1’이 주어지니 ‘인자는 양의 정수여야 한다’는 제약에 걸리게 되었다. 따라서 VM은 java.lang.AssertionError를 던져 검증이 실패했음을 알린다.

AssertionError 클래스
검증 기능이 추가되면서 java.lang 패키지에 AssertionError 클래스가 생겨났다. 이 클래스는 하나의 인자를 취하는 일곱 개의 생성자와 기본 생성자를 갖는다. 식(Expression) 하나짜리 검증문은 기본 생성자를 사용하고, 식 두 개짜리 검증문은 인자 하나를 취하는 일곱 개 중 하나를 사용한다.
어떤 생성자가 사용되는 지를 이해하기 위해서는 검증 과정이 수행되는 방식을 이해해야 한다.

expression1 평가
‘참(true)’ 일 경우 (검증 성공)
통과

‘거짓(false)’ 일 경우 (검증 실패)
expression2 가 존재할 경우
expression2 를 평가하고 결과값을 인자로 한 AssertionError 의 생성자를 호출한다.
expression2 가 없을 경우
AssertionError의 기본 생성자를 사용한다.

Foo 클래스에서 사용한 검증문이 첫 번째 형태이므로 이 경우 기본 생성자가 사용된다. 기본 생성자는 슈퍼클래스인 java.lang.Throwable의 기본 생성자를 호출하게 되므로 우리가 많이 접하게 되는 표준 에러 메시지를 보게 될 것이다(텍스트 형태의 스택 추적 정보).

위 결과를 보면 Foo 클래스에서 무언가가 잘못되어 검증 에러가 발생하였다. 6번째 라인의 m1 메소드에서 AssertionError 가 발생했음은 알 수는 있는데 그럼 도대체 뭐가 어떻게 잘못됐단 말인가? 다행히도 두 번째 형태의 검증문을 통해 이 문제를 해결할 수 있다. 얘기했듯이 두 번째 형태의 검증문은 우선 expression1 을 평가해 false일 경우 expression2 를 평가하게 된다. AssertionError의 생성자는 내부적으로 입력 인자를 String 형태로 변환시키기 때문에 입력 인자로 사용되는 expression2 의 결과값은 반드시 String으로 변환될 수 있는 타입이어야 한다. The Java Language specification의 15.18.1.1 섹션을 보면 String으로의 변환 규칙 일곱 가지가 명시되어 있는데 AssertionError의 일곱 생성자는 이들 각각에 대응된다. 복잡하게 설명할 것 없이, byte를 제외한 자바의 모든 기본형과 모든 Object가 그에 해당한다. 그러니 개발자들은 에러 상황을 설명하는 데만 집중하면 된다.

아래 Bar 클래스의 m1(int) 메소드에서 사용된 검증문은 두 번째 형태이다.
public class Bar {
 public void m1( int value ) {
   assert 0 <= value : "Value must be non-negative: value= " + value;
   System.out.println( "OK" );
 }

 public static void main( String[] args ) {
   Bar bar = new Bar();
   System.out.print( "bar.m1(  1 ): " );
   bar.m1( 1 );
   System.out.print( "bar.m1( -1 ): " );
   bar.m1( -1 );
 }
}

다음은 Bar 클래스를 검증 옵션을 준 상태로 실행시킨 결과이다.
bar.m1( 1 ): OK
bar.m1( -1 ): Exception in thread "main" java.lang.AssertionError: Value
must be non-negative: value= -1
at Bar.m1(Bar.java:6)
at Bar.main(Bar.java:17)

보시다시피 스택 추적 정보 앞에 String으로 변환된 expression2 메시지가 출력되었다. 이런 상세한 설명은 예외 메시지를 더욱 유용하게 만들어 준다. 적절한 메시지를 만들어내는 것이 어려운 일은 아니기 때문에 개발자들은 두 번째 형태의 검증문을 사용하는데 익숙해 두는 것이 좋을 것이다.

이와 별도로, J2SE 1.4부터 java.lang.Throwable 클래스에 스택 추적 정보를 보다 명확히 표현할 수 있는 방식을 제공하고 있다. 아래는 FooBar 클래스에서는 이 새로운 기능들을 활용하여 보았다.
public class FooBar {
 public void m1( int value ) {
   assert 0 <= value : "Value must be non-negative: value= " + value;
   System.out.println( "OK" );
 }

 public static void printAssertionError( AssertionError ae ) {
   StackTraceElement[] stackTraceElements = ae.getStackTrace();
   StackTraceElement stackTraceElement = stackTraceElements[ 0 ];

   System.err.println( "AssertionError" );
   System.err.println( "   class=   " + stackTraceElement.getClassName() );
   System.err.println( "   method=  " + stackTraceElement.getMethodName() );
   System.err.println( "   message= " + ae.getMessage() );
 }
 
 public static void main( String[] args ) {
   try {
     FooBar fooBar = new FooBar();
     System.out.print( "fooBar.m1(  1 ): " );
     fooBar.m1( 1 );
     System.out.print( "fooBar.m1( -1 ): " );
     fooBar.m1( -1 );
   } catch( AssertionError ae ) {
     printAssertionError( ae );
   }
 }
}

아래와 같이 FooBar는 에러 메시지를 훨씬 명확하게 표시해 준다.
fooBar.m1(  1 ): OK

fooBar.m1( -1 ): AssertionError
  class=   FooBar
  method=  m1
  message= Value must be non-negative: value= -1

검증과 상속
검증 기능을 클래스 단위로 불활성화 시킬 수도 있다. -disableassertions’ 나 ‘-da’ 옵션은 검증 활성 옵션들과 함께 사용될 수 있다. 뿐만 아니라 이 옵션들을 원하는 만큼 중복해서 사용할 수도 있다.
따라서 어떤 클래스는 검증을 활성화 시켜놓고 그 상위 클래스는 불활성화 시키는 것도 당연히 가능하다. 아래의 코드를 통해 구현 상속 시 이와 같은 상황이 어떤 효과를 나타내는 지 살펴보자.
// Base.java
package tmp;

public class Base {
 public void m1( boolean test ) {
   assert test : "Assertion failed: test is " + test;
   System.out.println( "OK" );
 }
}

// Derived.java
package tmp.sub;

import tmp.Base;

public class Derived extends Base {
 public void m2( boolean test ) {
   assert test : "Assertion failed: test is " + test;
   System.out.println( "OK" );
 }

 public static void printAssertionError( AssertionError ae ) {
   StackTraceElement[] stackTraceElements = ae.getStackTrace();
   StackTraceElement stackTraceElement = stackTraceElements[ 0 ];

   System.err.println( "AssertionError" );
   System.err.println( "   class=   " + stackTraceElement.getClassName() );
   System.err.println( "   method=  " + stackTraceElement.getMethodName() );
   System.err.println( "   message= " + ae.getMessage() );
 }
 
 public static void main( String[] args ) {
   try {
     Derived derived = new Derived();

     System.out.print( "derived.m1( false ): " );
     derived.m1( false );

     System.out.print( "derived.m2( false ): " );
     derived.m2( false );
   } catch( AssertionError ae ) {
     printAssertionError( ae );
   }
 }
}


두 클래스 모두 검증문을 포함한 메소드를 하나씩 갖고 있다. 단, Derived 클래스는 Base 클래스로부터 m1(boolean) 메소드를 구현 상속 받고 있음을 상기하자.
먼저 모든 클래스를 검증하도록 실행시켜보겠다.
java -ea tmp.sub.Derived

derived.m1( false ): AssertionError
class= tmp.Base
method= m1
message= Assertion failed: test is false

상위 클래스인 tmp.Base의 m1 메소드에서 AssertionError 가 발생되었다. 구현 상속이었으므로 당연한 결과이다.
다음으로, 전체적으로 검증을 활성화시키고 tmp.Base 클래스만 따로 불활성화 시켜서 실행시켜보자
java -ea -da:tmp.Base tmp.sub.Derived

derived.m1( false ): OK
derived.m2( false ): AssertionError
class= tmp.sub.Derived
method= m2
message= Assertion failed: test is false

이제 상속 받은 m1(false) 메소드에서는 검증 에러가 발생하지 않았다. 이는 이 메소드의 실제 구현 코드는 tmp.Base 클래스에 존재하고 이 클래스에는 검증을 수행하지 말도록 했기 때문이다. 물론 상식적인 결과이지만 처음 접하면 당황 할 수도 있는 문제이다.
옵션 설정에 있어 마지막으로 집고 넘어갈 특성은 ‘–da:tmp.Base 옵션이 -ea 을 덮어 씌운다는 것이다. 클래스 단위의 설정이 가장 명시적이기 때문에 높은 우선순위를 가지며, 옵션들을 순서대로 처리하기 때문에 나중에 온 옵션에 의해 앞의 것이 무시되기도 한다. java.lang.ClassLoader에 추가된 검증 관련 기능들을 살펴볼 다음 절을 읽고 나면 이러한 검증 처리 여부가 어떻게 판단하는 지 명확히 이해할 수 있을 것이다.

java.lang.ClassLoader의 추가사항
J2SE 1.4는 클래스, 패키지 단위의 검증 여부 관리를 위해, java.lang.ClassLoader 클래스에 몇 가지 기능을 추가하였다. 아래에 새로 추가된 public 메소드들과 간략한 설명을 준비하였다.

public void setClassAssertionStatus(String className, boolean enabled)
ClassLoader의 맵에, 주어진 클래스명과 검증 여부를 나타내는 엔트리를 추가한다. 이 메소드는 클래스 단위로 검증 여부를 결정하는 -ea:<class name>-da:<class name> 옵션과 연관된다.

public void setPackageAssertionStatus(String packageName, boolean enabled)
ClassLoader의 맵에, 주어진 패키지명과 검증 여부를 나타내는 엔트리를 추가한다. 이 메소드는 패키지 단위로 검증 여부를 결정하는 -ea:<package name>...-da:<package name>... 옵션과 연관된다.

public void setDefaultAssertionStatus(boolean)
ClassLoader의 기본 검증 정책을 결정한다. 이 메소드는 전체 비(非) 시스템 클래스들의 검증 여부를 결정짓는 -ea-da 옵션, 그리고 시스템 클래스들의 검증 여부를 결정짓는 -esa-dsa 옵션과 연관된다.

클래스 로더는 각각의 클래스들을 로드 할 때 마다 검증 여부를 판단한다. 이 때 다음과 같이 간단한 판단 메커니즘이 사용된다.
  • 클래스명에 대한 엔트리를 맵에서 찾는다.
  • 클래스가 직접적으로 속한 패키지명에 대한 엔트리를 맵에서 찾는다. 반복해서 그 상위 패키지명에 대한 엔트리를 찾는다.
  • 기본 검증 정책을 적용한다.

위의 과정대로 진행하여 일치되는 엔트리를 찾게 되면 이를 적용하고, 엔트리가 없다면 기본 정책이 적용된다. tmp.sub.Derived 클래스를 로드 할 때의 과정을 한번 살펴보자.
  • 클래스명 맵에서 tmp.sub.Derived 라는 엔트리를 찾는다.
  • 패키지명 맵에서 tmp.sub 라는 엔트리를 찾는다.
  • 패키지명 맵에서 tmp 라는 엔트리를 찾는다.
  • 기본 검증 정책을 적용한다.


이정도 설명이면 클래스의 검증 여부 판단과 앞서 언급된 몇 가지 사항들에 대한 충분한 해명이 되었을 것이라 믿는다.
  • 명령행 옵션은 클래스나 패키지 명이 유효한가에 대한 검사를 하지 않는다. 단지 클래스 로더의 맵에 엔트리를 추가할 뿐이다.
  • 가장 구체적인 매핑이 우선권을 갖는다. 검증 판단 메커니즘은 가장 구체적인 쪽에서 가장 포괄적인 쪽으로 진행하면서 가장 먼저 찾아낸 엔트리의 설정을 적용한다.
  • 클래스 로더는 입력되는 엔트리를 단순히 덮어쓰기만 한다. 즉, 중복되는 옵션이 들어올 경우 설정 내용은 새로운 옵션으로 변경된다.


방금 말했듯이 클래스 로더는 클래스가 로딩되는 시점에서 해당 클래스의 검증 여부를 판단한다. 따라서 프로그램 상에서 클래스 로더에서 제공하는 메소드를 통해 검증 정책을 변경시켰더라도 이미 로딩되어 있던 클래스들에는 영향을 미치지 못한다. 예를 들어 myClassLoader가 tmp.sub.Derived를 로드한다고 가정해보자. 어떠한 클래스/패키지 단위의 정책도 정해져 있지 않고 기본 검증 정책은 false이다. 이 때 프로그램 상에서 myClassLoader.setClassAssertionStatus(tmp.sub.Derived, true) 를 호출해 이 클래스를 검증할 것을 요청했더라도 이미 로딩된 tmp.sub.Derived 클래스는 여전히 검증되지 못한다는 것이다. 만약 myClassLoader가 이 클래스를 다시 로드하게 된다면 그때서야 바뀐 정책이 효력을 발휘한다. 일반적인 경우 이처럼 프로그램 상에서 정책을 바꾸는 일은 거의 없다는 것은 참 다행스런 일이다.

기타 사항들
검증 코드가 클래스 파일에 포함되어 조건에 따라 동작하고 안하고 하는 것은 아니다. 대신 The Java Language Specification 의 14.20 섹션을 보면 조건에 따라 다르게 컴파일하고 있음을 알 수 있다. 검증 설정은 아래와 같이 특수한 형태로 표현된다.

static final boolean assertionsEnabled = <true | false>;

if( assertionsEnabled )
assert expression1;

assertionsEnables 변수가 static final 이기 때문에 변수값이 false 이면 절대 검증문에 도달할 수 없는 상황이 되어 버린다. 컴파일러는 이런 상황을 인식하고 코드 자체를 제거해버린다(역자주> 기본적인 옵티마이징 기법이라 볼 수 있다. 하지만 일반적인 코드에 대해서는 컴파일 에러를 발생시키므로 다르게 취급하고 있음을 알 수 있다).

마찬가지로 어떤 클래스에 검증 여부를 묻는 방법은 제공되지 않는다. 하지만 검증 기법을 오용하여 아래와 같이 assertionsEnabled라는 변수를 두어 클래스 검증 여부를 설정할 수도 있을 것이다.

boolean assertionsEnabled = false;
assert assertionsEnabled = true;

이처럼 검증 기능을 잘못 사용하면 부작용을 일으키게 된다. 검증 여부에 따라 서로 다르게 동작할 수 있는 잠재적인 위험이 있기 때문에 검증문에 포함된 표현식은 절대 부수 효과를 일으키지 말아야 함을 명심하자. 우리는 보다 신뢰할 만한 프로그램을 만들기 위해 검증을 사용하는 것이지 그 반대가 아니다.

마지막으로, 검증문에 사용되는 표현식을 만드는 데는 항상 주의를 해야 한다. 부작용을 일으키는 코드 뿐 아니라 검증 이외의 프로그램 코드를 수행시키지도 말아야 한다. 극단적인 예로 아래 클래스는 검증을 수행할 경우 StackOverflowError가 발생해 프로그램이 강제 종료될 때까지 끝없는 재귀호출만을 반복할 뿐이다.

public class Pathological {
 private String ping( boolean test )  {
   System.err.print( "ping ... " );
   assert test : pong( test );
   return null;
 }

 private String pong( boolean test )  {
   System.err.print( "pong ... " );
   assert test : ping( test );
   return null;
 }

 public static void main( String[] args )  {
   Pathological pathological = new Pathological();
   System.err.println( "Pathological.ping( false ): " );
   pathological.ping( false );
 }
}

댓글 없음:

댓글 쓰기

ETL 솔루션 환경

ETL 솔루션 환경 하둡은 대용량 데이터를 값싸고 빠르게 분석할 수 있는 길을 만들어줬다. 통계분석 엔진인 “R”역시 하둡 못지 않게 관심을 받고 있다. 빅데이터 역시 데이터라는 점을 볼때 분산처리와 분석 그 이전에 데이터 품질 등 데이...