Spring 3.0 (51) @Rule 지원성공 그리고 Formatter, ConversionService의 전면 등장
일주일만에 업데이트를 받았더니 제법 많은 작업이 진행됐다.
@Rule을 포함해서 JUnit 4.5~4.7을 거의 완벽하게 호환하는 스프링 테스트
JUnit 4.5+를 이용한 Spring Test Framework의 마지막 도전과제였던 @Rule의 적용을 드디어 해냈다. JUnit 4.5, 4.6, 4.7 어떤 버전을 사용해도 동일하게 동작하는 Spring TestContext 러너 문제는 이미 지난 주에 해결했다. 남은 것은 4.7에 등장한 획기적인 변화라고 할 수 있는 @Rule의 적용이었다. 이를 어찌 적용할지 꽤나 고민스러워했던 Sam이 결국 정면돌파를 통해서 문제를 해결해냈다.
내 생각으로는 Mockito처럼 JUnit버전를 체크해서 그에 따라 다른 확장Runner 클래스를 리플렉션을 통해서 선택적으로 만들도록 하는 방법을 사용하게 되거나, 아예 JUnit4.7+용 Runner가 따로 만들어지는 것이 유일한 해결책이었다.
하지만 Sam은 그냥 단순하게 리플렉션을 통해서 @Rule을 처리할지를 다이나믹하게 결정하는 방법을 선택했다.
스프링 테스트가 JUnit의 @Before, @After 등등을 처리하는 Statement 구조를 통채로 바꿔버리기 때문에 JUnit 4.7의 @Rule을 처리하는 methodBlock을 직접 활용할 방법은 없다. 게다가 그 @Rule을 처리하는 메소드는 private이라 오버라이딩도 불가능하다. 또 4.7용 스프링 테스트 러너가 따로 만들지 않으려면 @Rule처리 메소드가 존재하지 않는 4.6, 4.5도 모두 동일하게 동작하게 만들어야 한다.
결국 방법은 리플렉션이다. 새롭게 추가된 메소드는 withRulesReflectively()이다. 이 안에서 현재 사용되는 JUnit의 클래스 안에 withRule() 메소드가 존재하는지 리플렉션을 통해서 확인한다. 그리고 존재하면, 즉 4.7+라면 그것을 역시 리플렉션을 통해서 호출한다. private이긴 하지만 JDK1.3+부터 지원되는 Method.setAccessible()을 이용하면 접근이 가능하다.
꽤나 힘들거라고 Sam Brannen본인도 얘기했지만, 막상 접근 방법을 OO적인 모델을 포기하고 리플렉션을 최대한 활용한 방법을 동원하니 매우 간단하게 해결이 가능했다. 사실 그럴 수 있는데는 JUnit 4.7의 @Rule 기능의 추가가 매우 깔끔하고 명쾌하게 적용되는 것을 가능하게 한 JUnit의 설계가 한 몫했다.
아무튼 이래서 JUnit과 관련된 이슈는 거의 해결되었고, 최신버전의 JUnit을 안쓰고, 무슨 이유든 구 버전을 사용하더라도 스프링의 테스트 러너를 쓰는데는 아무런 문제가 없게 되었다. 또한 JUnit 4.7을 사용한다면 얼마든지 @Rule을 스프링 테스트와 함께 자유롭게 활용할 수도 있다.
내가 "거의" 해결되었다고 말하는 이유는 아직 한가지 미묘한 이슈가 남아있기 때문이다. 최근의 JUnit 메일링 리스트를 보면 @Rule의 도입 이후로 많이 올라오는 불평 또는 요청이 하나 있음을 알 수 있다. 그것은 @Before 메소드에서 @Rule을 사용하게 해달라는 것이다. JUnit 4.7은 무슨 이유인지 @Before를 먼저 처리하고 그리고 @Rule을 처리한 후에 @After로 진행되도록 되어있다. 따라서 @Before 메소드 안에서는 테스트 클래스에서 정의한 @Rule의 기능을 사용할 수 없다.
이 부분에 대해서 여러번 이슈가 제기 되었고, 그에 대해서 켄트 벡이 직접 "지금은 잘 기억나지는 않지만 그렇게 결정한 이유가 있었다"라고 설명했다. 하지만 "정말 필요하다면 4.8에서는 @Before 앞에서 먼저 @Rule이 실행되도록 변경하겠다"는 답을 달아주었다.
내 생각으로는 @Before에서 테스트 작성자가 준비한 픽스처나 다른 정보를 @Rule에서 활용할 수 있도록 하기 위해서 순서를 그렇게 잡은 것이 아닌가 싶다. 하지만 그 반대로 보다 공통적이고 재 사용이 가능한 확장구조인 @Rule에서 준비된 기능을 @Before에서 활용하도록 만드는 것이 나은 경우도 분명 있는 듯 하다.
스프링 테스트가 아직 이부분에 관해서 이슈라고 하는 이유는, 스프링은 JUnit의 @Before, @After의 실행 구조를 다시 transaction의 전후로 세분화 해서 재정의 하고 있기 때문이다. 그때문인지 아니면 실수 인지는 모르겠지만, 이번 @Rule을 리플렉션을 이용해서 지원하는 기능을 추가할 때 @Rule의 실행을 가장 우선으로 잡아두었다. 사실 이러면 JUnit 4.7의 실행순서와 미묘한 차이가 있어서, @Before에서 준비한 것을 @Rule에서 사용할 수가 없다. 이 점이 아직 해결해야 할 과제이거나 수정되야 할 이슈일 수 있다.
궁극적으로 이 두가지 실행 순서에 대한 시나리오가 공존해야 한다면, 결국 @Rule이 실행되는 순서 자체를 정의하는 기능이 추가되야 할지도 모르겠다. 예를 들어 기본은 @Before 후이지만 그 앞에서 동작해야 하는 @Rule이 있다면 엘러먼트를 추가하거나 또 다른 애노테이션을 부가해서 순서를 지정하도록 하면 될 듯 하다.
어쨌든 JUnit의 혁신적인 변화에 대한 스프링의 응수는 끝났다. 남은 것은 4.8이 어떻게 변할 것인가하는 문제이다. 과연 @Before등의 Statement를 처리하는 메소드는 더 이상 확장하지 못하도록 private으로 바뀔 것인가? 그렇다면 그때 스프링은 어떻게 대응 할 것인가.. 흥미진진하다.
사실 그동안 스프링 프레임워크 오타쿠 입장에서 JUnit의 기능을 확장하는 어려움에 대해서 여러가지 불만을 토로했지만, 솔직히 말하자면 JUnit 자체에 별 문제가 있다고 보기는 어렵다. JUnit이 4.5에서 BlockJUnit4ClassRunner를 등장시켜서 기존의 internal 클래스를 확장을 권장하는 구조로 만들었고, 적어도 그때까지는 테스트를 실행하는 단계에 대한 일정한 틀을 유지하면서 그 안에서의 변화만을 고려한, 그래서 withBeforeClasses 같은 메소드를 상속해서 오버라이드 할 수 있도록 만들어 둔 것이다.
그것을 단계적으로 발전시키다가 결국 @Rule이라는 무한확장 구조까지 도입했지만, 그 과정에서 조금은 과하다 싶을 정도로 내부적으로 적용했던 Statement를 이용한 Nested Method Object 패턴 구조는 빛을 발했다. 결국 그 구조를 따라서 @Rule의 지원 기능이 매우 단순하고 명쾌하게 추가될 수 있었다. 그리고 약간은 병렬적인 설계구조를 가진 4.7이 등장한 것이다. 기존의 @Before, @After를 포함한 모든 JUnit의 테스트를 실행하는 전략에 관한 애노테이션은 모두 @Rule로 대체가 가능하다. 하지만 일단은 그 두가지가 자연스럽게 공존하는 구조를 만들어 두고 사용자들로 하여금 부담을 가지지 않고 4.7로 업그레이드 한 후에 새로운 기능을 사용하고 옮겨보도록 하고, 그 피드백을 받아서 차근차근 @Rule이라는 무한한 확장구조를 가진 새로운 디자인으로 전환하는 과정이라고 보인다.
그러는 과정에서 약간은 엉성해 보였던 때도 있었고, makeNotifier인가하는 메소드처럼 가시성조차 우왕좌왕 하는 시점도 있었지만 사실 그건 스프링처럼 매우 깊게 JUnit 기능을 대폭 뜯어고쳐서 만들어지는 경우에나 신경 쓰일 일이지, JUnit의 최종 사용자인 테스트 개발자에게는 아무런 겉으로 보여지거나 문제를 일으킬만한 것은 없었다는 것을 인정해야 한다.
그동안 내가 지적했던 JUnit과 관련된 문제들 핵심은 사실 JUnit 보다는 자바의 버전관리기능이 없다는 문제와 엉성한 정책으로 인해 명분도 없이 구버전 사용을 강제당하는 현장의 문제라고 볼 수 있다. 테스트 클래스 자체의 호환성에는 아무런 문제가 없으니 사실상 모든 JUnit 4로 만들었던 테스트를 지원하는 프로젝트들은 JUnit의 최신버전으로 바로바로 변경해도 상관없다. 하지만 현실은 4.5, 4.6, 4.7이 마구 혼재되어있고, 메일링리스트를 보면 4.4, 4.2, 4.1을 쓴다고 하는 곳도 있다. 나같으면 최신버전으로 바꾸세요라고 하겠지만, 각각 현장의 프로토콜 자체가 특정 버전을 강제하고 있는 곳이 존재하는 것이 문제이다.
결국 JUnit은 나름 방향성을 가지고 디자인 개선과 기능 추가를 꾸준하게 진행하고 있고, JUnit 사용자의 입장에서의 호환성은 잘 지켜주고 있으니 그 자체로 뭐라할 것은 없다. 하지만 스프링 처럼 JUnit이 허용한 확장구조 이상을 구현하려고 할 때는 결국 지금까지와 같은 장벽을 만날 수 밖에 없는 것 같다. 스프링도 JUnit의 최신 버전만 잘 지원하면 된다고 하면 역시 문제는 없다. 하지만 현장에서 어쩔 수 없이 구버전을 쓰는 사용자들과 나처럼 항상 최신버전으로 빨리 업그레이드하고, 최신기능을 함께 적용하기를 고집하는 사람들을 모두 만족시켜주기 위해서 이런 저런 수를 쓸 수 밖에 없지 않을까 하는 생각이 든다.
아무튼 스프링 3.0이 나오기 전에 JUnit 4.7까지의 모든 버전과 기능에서 아무런 문제가 없도록 스프링 테스트는 완벽하게 다듬어져 나올 것이 분명하다.
이제 관심을 가질 것은 스프링이 확장한 테스트 실행 구조를 넘어서서 @Rule을 활용해볼 거리가 뭐가 있는지이다. 기술적인 준비는 다 되었으니 참신한 아이디어가 필요한 시점이다.
Formatter와 ConversionService
이전에 한번 언급했지만 이제 스프링 1.0, 아니 JavaBeans의 유물이었던 PropertyEditor는 사실상 수명을 다했다. PropertyEditor의 주요한 두가지 용도였던, 빈 설정의 텍스트로 지정한 프로퍼티 값의 변환 기능과 SpringMVC의 HTTP form 정보의 모델 오브젝트 필드로의 변화 부분은 이제 Formatter와 ConversionService가 대신할 것이다.
멀티쓰레드 환경에서 재사용이 가능한 오브젝트 변환 기능을 담당하는 ConversionService가 이제 ApplicationContext의 기본 서비스로 추가되었다. 또 SpringMVC의 PropertyEditor가 차지하고 있던 부분은 Formatter라는 이름의 독자적인 서비스로 대치되었다. ConversionService가 이미 있지만, 구지 Formatter를 별도로 도입한 이유는 그 타입 변환의 맥락이 다르기 때문이다. HTTP 폼과의 변환은 기본적으로 String과 오브젝트의 관계이기 때문에 임의의 오브젝트 사이의 변환 서비스를 정의한 ConversionService를 그대로 가져다 쓰는 것은 좋지 않다. 용도에 적합하도록 자신만의 서비스 인터페이스를 정의해두고, 이를 애노테이션 등으로 쉽게 적용할 수 있도록 새롭게 설계되었다. 이제 binder에 register 메소드를 호출하는 번거로움 자체도 아예 제거할 수 있게 된 것이다. 물론 비슷한 변환기능은 중복할 필요는 없다. Formatter는 아답터를 통해서 간단히 ConversionService를 구현한 오브젝트를 이용할 수 있도록 설계되어있다.
몇가지 더 흥미로운 변경사항들이 있지만 일단은 여기까지.
봄싹에 ConventionService를 적용해봐야겠군요. 아.. 예전에 만들어둔 GenericEnumPropertyEditor가 울겠네요.. ㅠ.ㅠ
형 잘 들어갔어?
나는 잘 들어왔음. 지금 레포트 작성 및 스터디 중
존 하루되고 글 잘봤어.