이제 본격적으로 Spel을 사용하는 AssertThrowsTemplate을 만들어본다.

@Check와 Spel expression

검증용 expression을 정의할 애노테이션을 먼저 정의한다.

간단하게 검증용 Spel을 기본 값(value)로 정의하고, 실패했을 때 보여줄 메시지를 옵션 값으로 정의한다. 실패 메시지가 없으면 기본 설정된 expression + “ failed"를 넣기로 한다.

 

이렇게 해서 만든 @Check는 아래와 같다. @Target은 method이다. 이게 익명내부클래스로 만들어 쓰니 타입 레벨에 애노테이션이 들어가지가 않는다. 사실상 재정의 하는 것은 훅 메소드인 public void test() 뿐이니, 그 메소드에 @Check를 부여한도록 한다. Spel을 생성자에 파라메터로 전달하는 방법도 있겠는데, 애노테이션이 좀 더 폼나기 때문에(게다가 optional/deafult도 지원하고) 애노테이션으로 만들었다. 사실은 SpringSecurity에서도 그렇게 썼길래(SPEL 시나리오 테스트에도 얼핏 나온다) 흉내낸 것이다. :)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
    String value();
    String failureMessage() default "";
}

 

@Check 정의도 했으니 이제 AssertThrowsTemplate에 애노테이션을 체크해서 있다면 그 EL을 이용해서 검증하는 부분을 추가하면 된다.

기껏 만들어 놓은 callback/template 방식은 그대로 일단 유지하고, 그 다음에 한번더 애노테이션을 체크하는 메소드를 추가한다.

@Check 애노테이션이 없다면 무시. 이름도 어려운 Liskov Substitution Principle을 지켜야 하기 때문에 당연히 AssertThrowsTemplate은 애노테이션이 없이 상위 AssertThrows의 적용케이스에 대신 사용되어도 정상동작해야 한다.

Spel을 사용하는 코드는 다음의 순서로 작성된다.

1. Parser 생성. 여기서는 SpelAntlrExpressionParser를 사용한다. 복잡한 구문을 정의하고 사용하는데는 Antlr이 역시 최고. 이 클래스는 ExpressionParser 인터페이스를 구현한 것이다. 현재는 Antlr 파서 달랑 하나 뿐이다. 다른게 과연 나올지 모르겠지만, 어쨌든 스프링은 확장을 대비하고 보기 때문에 당연한 구조이다.

2. EvaluationContext 만들기. EC는 expression이 적용될 대상정보이다. root object를 등록해서 적용할 수도 있고(el에서 바로 사용가능). 직접 variable을 등록하거나(#xxx로 접근해서 사용), 기타 컨버터, comparator, property accessor, method resolver, function 등을 정의해 놓을 수 있다. 일반적으로는 StandardEvaluationContext를 이용하거나 이를 확장해서 쓰면 된다.

이 EC에다가 test()를 수행하면서 발생한 exception을 root object로 전달한다. 이 컨텍스트에서 메지지를 가져오는 expression은 “getMessage()”라고 하면 된다.

3. Parser로 먼저 expression을 파싱한다. @Check 애노테이션의 value를 가져와서 넣는다.

4. 파싱된 expression에 EvalutaionContext를 넘겨서 getValue()를 호출하면 결과 값을 돌려준다. @Check의 expression은 결과가 boolean으로 나오는 조건식이어야 한다. 결과가 false면 AssertFailureError 를 던지고, 아니면 패스.

5. EL은 런타임시에 다이나믹하게 파싱되고 바인딩 되기 때문에 각종 예외상황이 발생할 수 있다. 이를 적절히 처리해야 하는데, 귀찮아서.. 몽땅 잡아서 IllegalArgumentException으로 wrapping해서 던져버렸다. 아 귀찮아..

 

그렇게 추가한 코드가 대충 이런 모습

protected void checkExceptionExpectations(Exception actualException) {
    ...
    checkByExpression((T)rootCause);
}

private void checkByExpression(T actualException) {
	try {
		Method method = this.getClass().getMethod("test");
		Check checkAnnotation = method.getAnnotation(Check.class);
		if (checkAnnotation == null) return;
		
		SpelAntlrExpressionParser p = new SpelAntlrExpressionParser();
		StandardEvaluationContext ctx = new StandardEvaluationContext();
		ctx.setRootObject(actualException);
		Expression exp = p.parseExpression(checkAnnotation.value());
		if ((Boolean) exp.getValue(ctx) == false) {
			AssertionFailedError afe = new AssertionFailedError(
					checkAnnotation.failureMessage().length() > 0 ?
							checkAnnotation.failureMessage() : checkAnnotation.value() + " failed");
			afe.initCause(actualException);
			throw afe;
		}
	}
	catch(Exception e) {
		throw new IllegalArgumentException(e);
	}
}

 

이제 간단한 테스트를 만들어서 검증하면 끝.

에러메시지 체크는 @Check(“getMessage() == ‘expected message’”), 코드 체크나 기타 복잡한 조건을 부여하는 것도 간단히 EL로 표현할 수 있다. 특별히 variable로 값을 넘기지는 않았지만, static 값은 EL안에서 바로 가져올 수 있다. 클래스 정보를 T()로 감싸주기만 하면 된다. T(XXXClass).STATIC_VALUE 이런 식이다. instanceof T(XXXClass)처럼 쓸 수도 있다.

 

사실 검증할 때 비교할 값을 파라메터로 전달하게 할까도 생각해봤는데, 일단 애노테이션의 제약때문에 포기. 애노테이션에서는 varargs처럼 1차원 어레이로 다중 값을 전달하게 할 수는 있지만, 원시타입, Class, String, Enum 타입만 허용한다. 따라서 다양한 타입을 어레이로 전달하는 것은 불가능하다. Object[]를 쓸 수 없기 때문이다. 애노테이션 말고 다른 방법(생성자에 등록한다거나)을 쓰기는 좀 이상하고.. 그래서 포기. 애노테이션에 저따위 제약은 왜 만든거야.

 

에러메시지도 expression으로 만들게 해서 exception으로부터 정보를 얻어서 메시지를 생성하게 할까도 생각했지만, 일단 귀찮아서 포기.

 

마지막으로 중요한 작업 한가지는 중첩 예외의 처리이다. AssertThrows는 중첩된 예외가 전달되면 가장 마지막의 예외타입만 체크한다. 이 것은 프레임워크 내부에서 발생하는 익셉션을 컨텍스트 밖에서 체크하려고 할 때 문제가 된다. 예를 들어 빈의 생성 시에 일부 기능을 체크하기 위해서 강제로 예외를 던지는 경우에, BeanCreationException 어쩌고 하는 것으로 wrapping되어서 던져진다. 그러면 AssertThrows에서는 검증이 불가능하다.

 

그래서 가장 root exception을 체크하는 부분을 checkExceptionExpectations() 템플릿에 추가해서 항상 최상위 root cause를 가지고 비교하게 만들었다. 그러면 각종 컨테이너와 프레임워크에서 처리되어서 나온 중첩 예외도 검증하는데 문제가 없다.

이보다 좀 더 세밀하게 만들려면 디폴트는 root cause를 찾게 만들고, 옵셔널 필드로 root cause가 아닌, 표면의 예외를 비교하도록 설정하는 기능을 추가하는게 좋겠다. 또는 중첩된 모든 예외를 비교하는 옵션도 넣는다 거나.

 

아무튼 이렇게 해서 Spel을 이용한 AssertThrows의 확장작업은 끝. 구현은 금방 했는데, 정리하는게 훨씬 오래 걸리는구나. 그냥 스크린캐스트로 할걸 그랬나.

 

작업하면서 느낀 것들

JUnit Max

며칠전에 구매한 켄트 백 옹의 JUnit Max를 가지고 TDD로 작업을 해보았다. 다 하고난 느낌은 정말 최고라는 것. http://sunchic.free.fr/wordpress/index.php/archives/2009/02/25/junit-max-just-rocks/ 극찬을 한 이 개발자의 느낌이 어땠는지 이해가 간다.

JUnit Max는 잦은 테스트 실행의 번거로움을 없어주고, 마치 코딩하고 저장하면 자동 빌드하고 오류가 있으면 x표시로 알려주듯이, 테스트를 만들기 시작하면 자동으로 테스트가 수행되고 실패한 테스트와 성공한 테스트가 에디터에 바로 표시가 된다. JUnit 뷰를 열 일이 없다.

JUnit Max를 쓰는 느낌은 마치 텍스트 에디터와 javac를 사용해서 직접 코딩+컴파일 작업을 하다가 자동빌드가 되는 이클립스와 같은 IDE를 쓸 때의 딱 그 느낌이다. 세상에 이렇게 편할 수가. 단지 귀찮은 작업을 좀 덜해도 된다는 느낌을 넘어서, 표현 그대로 seamless한 TDD가 가능해졌다. TDD가 정말 개발과정의 하나로 자연스럽게 느껴지게 만들어 주는 그런 도구이다.

JUnit Max를 extreme TDD 툴이라고 부르면 딱 적당하지 않을까 싶다. 테스트를 만들고 실행하는 과정이 왠지 매번 컴파일 하듯이 번거롭고 에러를 확인하고, 필요에 따라 디버깅을 돌리는 수고가 귀찮다면 당장 JUnit Max를 구매해서 써보도록. 어떻게든 테스트를 만들어야 하는 상황이라면 한 2배, 제대로 TDD를 한다면 한 5배쯤 생산성이 향상되는 것을 체험할 수 있을 것이다.

 

Spel

이왕 애노테이션의 세계로 넘어온 것, 코드내 메타데이터의 끝을 보기로 작정한 듯 싶다. 애노테이션 설정과 결합해서(물론 XML에서도 당연히 쓸 수 있다), 복잡한 코드로 처리해야 하는 여러가지 작업을 메타정보로 간단히 독립시켜버리는데 자주 사용될 것 같다. 이런저런 적용 아이디어들이 마구 떠오른다. 가장 흔한 케이스는 애노테이션과 함께 넣어버리는 validation.

 

AssertThrows의 deprecation

안타깝지만 AssertThrows는 JUnit4를 적용하면서 이미 deprecated된 것이다. -_-; 보다 편한 @Test(expected)가 으니까. 하지만 @Test(expected)는 테스트 메소드 하나에 여러개를 적용할 수도 없는게 단점이다. 결국 AssertThrows와 같은 것이 필요하지 않을까.

 

그래서 다음 작업으로 @Test를 확장해서 Spel을 적용하는 것과 기존의 AssertThrows를 상속하지 않고 처음부터 독립적으로 설계된 더 유연하고 깔끔한 애노테이션+Spel을 가진 ThrowsTemplate을 만들어 보는 것이다.

 

이런건 열심히 정리해서 올려봤자 별 관심도 없는 것 같으니 나혼자 만들어서 쓰던가. 아니면 지금 쓰고 있는 스프링3.0 책의 스프링확장 편에 예제로 넣어도 괜찮지 않을까… 흠 과연?

Related posts:

  1. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (2)
  2. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (1)
  3. Spring 3.0 (59) 프로퍼티 파일 이용하기 – placeholder vs SpEL
  4. Spring Expressions(SpEL)를 이용한 Mockito Argument Matcher 만들기
  5. Spring 3.0 (51) @Rule 지원성공 그리고 Formatter, ConversionService의 전면 등장
  6. Spring 3.0 (42) Spring Dependency Matrix 업데이트
  7. Spring 3.0 (50) JUnit 4.5~4.7 겨우 지원성공 그리고 계속되는 악몽
  8. Spring 3.0 (49) JUnit 4.7로 변경과 상속의 폐해
  9. Spring 3.0 (52) 반쪽짜리 3.0 RC1 공개
  10. JUnit 4.7 분석 (1) JUnit 4.7 코드 첫인상
  11. Spring 3.0 (8) Core 모듈의 선택 라이브러리 분석
  12. Inside Spring (5) PropertyPlaceholderConfigurer를 @Bean으로 정의해서는 안되는 이유
  13. Spring 3.0 (28) R-669 Update
  14. Spring 3.0 (19) Test 모듈의 선택라이브러리 분석
  15. Spring 3.0 (38) Spring Reference 업데이트

Facebook comments:

to “Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (3)”

  1. Hi there, just became aware of your blog through Google, postcheap jerseysand found that it’s truly informative cheap nfl jerseys. I am going to watch out for brussels. I will be grateful if you continue this in future. A lot of people will be benefited from your writing. Cheers!

Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2017 Toby's Epril Suffusion theme by Sayontan Sinha