본문 바로가기

Java SE/Assertion

Assertion 에 대하여

프로그램을 작성하다보면 어떤 표현이 항상 참(true)으로 판정되어야 하는 경우가 있다. 예를 들어, 게임에서 총알은 광속보다는 느려야 하며, 1년안에는 12달까지만 존재해야 한다. 너무나 당연한 사실들이지만 프로그램 안에서는 이런 사실들이 너무도 쉽게 어겨지는 것도 사실이다. 본의 아니게 변수에 잘 못 계산된 값이 할당되면 이런 일은 쉽게 일어 날 수가 있다. 우리는 너무도 당연한 것들은 무의식적으로 믿고 의심하지 않게 되므로 프로그램 안에서 이런 잘 못을 감지하기는 무척 어렵게 된다. 그래서 프로그램의 계산상 오류를 찾기 위한 디버깅 작업에 많은 시간을 할애하기가 일쑤이다. 이런 당연한 사실들이 프로그램 내에서 어겨지는 것을 쉽게 감지하여 프로그램의 계산상 오류를 막고 디버깅에 들어가는 시간을 절약하며 소프트웨어의 신뢰성을 향상시키기 위해서 사용되는 것이  Assertion 이다. 프로그래밍시에 Assertion을 사용하는 것은 버그를 찾고 수정하기 위한 가장 빠르고 효과적인 방법으로 알려져 있다.

assert 키워드를 이용하는 문장은 2가지 형태가 있다.

1. assert Expression1 ;
2. assert Expression1 : Expression2 ;


위의 문장들 중에 Expression1 은 반드시 boolean 값으로 판정되는 수식이 와야 한다.

첫번째 문장은 Expression1 이 true로 판정되면 프로그램의 흐름에 아무런 징후를 보이지 않지만 false 로 판정되면 아래와 같은 AssertionError를 던지면서 프로그램이 그 곳에서 비정상 종료하게 된다. 개발자는 이때 프로그램의 계산상 오류를 감지하고 수정하는 작업을 하면 된다.

두번째 문장은 콜른 오른쪽에 Expression2 이 추가된 형태이며, Expression2 은 AssertionError의 생성자로 전달되어 AssertionError발생시에 문자열로 출력되므로 디버깅에 유리한 정보를 의도적으로 포함시키고자 할 때 유용하게 사용할 수 있다. Expression2 에는 모든 레퍼런스, 문자열, 기본형 데이터 등이 제한없이 올 수 있지만 디버깅에 유리한 문자열이면 좋을 것 같다.

참고로 assert 문장을 포함 프로그램을 실행할 때는 java -ea AssertTest<enter> 처럼 -ea 옵션을 사용해야 Assertion 기능이 활성화된다. 그렇게 하지 않으면 아무런 효과도 없이 프로그램은 실행된다. 이렇게 해야만 하는 이유는 Assertion기능은 프로그램의 성능상 도움은 되지않기 때문에 실제 프로그램이 실행되는 환경에서는 꺼 두자는 것이며, 개발시에는 -ea 옵션을 사용하여 디버깅에 도움이 되게 하려는 이유이다.

다음은 위의 언급한 2 가지 형태의 문장을 테스트한 결과이다.

public class AssertTest {
 public static void main(String[] args) {
  int month = 13;
  assert month <= 12; /* 1개의 수식을 가진 assert 문장*/
 }
}

Exception in thread "main" java.lang.AssertionError
        at AssertTest.main(AssertTest.java:4)


public class AssertTest {
 public static void main(String[] args) {
  int month = 13;
  assert month <= 12 : "month 변수에 12를 넘는 수가 대입되어 있습니다.";
 }
}

Exception in thread "main" java.lang.AssertionError: month 변수에 12를 넘는 수가
 대입되어 있습니다.
        at AssertTest.main(AssertTest.java:4)



Assertion문장을 사용해서는 안되는 경우

Do not use assertions for argument checking in public methods.
메소드에 잘 못 전달된 아규먼트 체크는 assert 문장을 이용해서는 안된다. 아규먼트 체크는 전형적인 메소드의 작업에 해당하며 IllegalArgumentException, IndexOutOfBoundsException, NullPointerException 등의 예외를 이용해서 디버깅해야 하고, assert를 이용하여 아규먼트를 체크하려고 할 때, 만약 먼저 위에서 언급한 다른 예외가 발생하게 된다면 assert 문장을 올바른 AssertException 을 발생할 수 없게 된다.
Do not use assertions to do any work that your application requires for correct operation.
Assertion기능은 -ea 옵션을 을 이용하여 비활성화 혹은 활성화 할 수 있는 기능이기 때문에 항상 실행될 것으로 생각하고 assert 문장을 이용하여 데이터를 처리하게 하면 안된다.

Assertion기능을 사용하기에 적당한 경우

    if (i % 3 == 0) {
        ...
    } else if (i % 3 == 1) {
        ...
    } else { // We know (i % 3 == 2)
        ...
    }

위의 문장에서 마지막 else 블럭에 해당하는 조건은 당연히 2가 될 것으로 생각하고 실행문 블럭을 작성했지만, 만약 변수 i 의 값이 음수라면 else 블럭은 2가 아닌 상황에서도 실행되는 오류를 범하게 된다. 다음과 같이 assert를 이용하여 검증을 할 수 있겠다.

    if (i % 3 == 0) {
        ...
    } else if (i % 3 == 1) {
        ...
    } else {
        assert i % 3 == 2 : i;
        ...
    }


위의 assert 문장은 else 블럭이 반드시 2일 경우에만 실행되는 것을 보장하고 있으며, 그외의 경우에는 AssertionError를 발생하면서 전달되 i 변수의 값을 에러문장과 함께 출력하게 되므로 디버깅에 도움이 된다.

Assertion을 적용하기에 적당한 또 다른 예는 switch case 문장을 들 수 있다. 다음의 예는 당연히 4가지의 경우만 존재할 것으로 생각하고 작성한 문장이며, 만약 예상된 경우를 벗어날 시에는 프로그램은 정상적으로 실행될 수 없을 것이다. 이런 경우에도 assert문장을 이용하여 검증할 수 있다.

    switch(suit) {
      case Suit.CLUBS:
        ...
        break;

      case Suit.DIAMONDS:
        ...
        break;

      case Suit.HEARTS:
        ...
        break;

      case Suit.SPADES:
        ...
    }


위의 문장에 Assertion 기능을 추가하여 검증을 하려면 다음과 같은 문자을 맨 아래에 포함시키면 될 것이다.

      default:
        assert false : suit;


다른 방법으로는 아래처럼 할 수 있으며, 이렇게 하면 Assertion 기능을 비활성화시켰을 때도 기능이 작동된다는 특징이 있다.

      default:
        throw new AssertionError(suit);


Assertion기능을 적용할 수 있는 또 다른 좋은 예을 들자면, 프로그램 중에서 절대로 실행될 수 없는 부분에 assert 문장을 적용하여 만약 실행되어서는 안되는 영역이 실행되기라도 한다면 즉시 에러를 발생하면서 개발자에게 알려 주도록 하는 경우이다. 다음 문장에서 주석으로 표현된 영역은 개발자가 당연히 실행될 수 없는 영역이라고 생각하고 있으며 만약 그 곳이 실행된다면 프로그램은 예상치 못한 결과를 가져오게 될 것이다.

    void foo() {
        for (...) {
            if (...)
                return;
        }
        // Execution should never reach this point!!!
    }


위와 같은 경우라면 주석문의 위치에 assert문장을 사용하여 그 곳이 실행되는 순간 AssertionError를 발생시켜 개발자에게 원인을 찾아 수정하게 하는 것이 좋은 것이다.

    void foo() {
        for (...) {
            if (...)
                return;
        }
        assert false; // Execution should never reach this point!
    }