ROO RC1이 나왔다. RC1이라면 이름 그대로 출시후보(release candidate)가 아닌가. 별 문제가 없다면 그대로 정식 제품으로 출시해도 되는 수준이라는 것이다. 일부 버그픽스 정도만 남고 실제 구현할 기능은 완료했다는 뜻이기도 하다. A1이 나온게 얼마전 같은데 벌써 RC라니 놀랍기만 하다. 처음 ROO가 나왔을 때 한번 대충 분석해보고 한동안 손을 놓고 있다가 며칠전 RC1이 나왔다는 소식에 다시 ROO를 살펴보는 중이다. 그 사이에 제법 많이 기능도 추가되고 세련되진 듯 하다.

ROO에 대한 전반적인 생각에 대해서는 차차 얘기하기로 하고 오늘은 ROO가 가진 이슈 중의 하나인 Entity의 스태틱 메소드의 도입에 관해서 생각해보자.

 

스태틱 메소드는 스프링의 적이었다. 로드 존슨이 마르고 닳도록 스태틱 메소드는 사용하지 말자, 팩토리 메소드를 가진 싱글톤패턴은 나쁘다고 주장하고 다녔고, 그 대안으로 싱글톤 레지스트리/컨테이너가 관리하는 싱글톤 방식인 스프링을 들고 나왔다(스프링이 그것 때문에 나온 건 아니겠지만, 스프링의 기본 아이디어 중에서 이 싱글톤 레지스트리라는 것은 매우 중요하다).

스태틱 메소드는 테스트의 적이다. 스태틱 메소드는 상속도 되지 않고, 따라서 다형성도 적용되지 않는다. 인스턴스 레벨에 붙는 Generics도 못쓴다(generic method는 물론 가능하지만..). 글로벌하게 접근이 가능할 뿐더러, 사용하는 클라이언트가 스태틱 메소드를 mocking할 수 없기 때문에 테스트 작성의 적이 되고 만다.

그래서 스프링의 서비스 추상화 방식등에서 사용한 것처럼 테스트 하기 힘든 스태틱 메소드 위주의 API들은 일종의 아답터패턴을 적용해서 클라이언트가 새로운 인터페이스를 통해서 사용하도록 만들어야 한다. 방법은 뭐 여러가지가 있는데 기본적인 아이디어는 결국 인스턴스 메소드를 통해서 스태틱 메소드로 접근하도록 포장(wrap)하는 것이다.  그래서 인터페이스를 사용하든, 상속을 사용하든지 해서 어쨌든 스태틱 메소드에 대한 접근을 mock object로 바꾸도록 만들어야 한다.

이게 스프링이 열심히 퍼뜨리고, 많은 개발자들이 인정하고 있는 기본적인 아이디어이다.

 

그런데 ROO가 배신을 했다.

ROO는 소위 4세대 RAD 프레임워크인 RoR,Grails,Django 처럼 단순화된 레이어를 따른다. RoR말고는 안써봐서 잘 모르겠지만, 아무튼 Ben Alex가 직접 설명한 거니깐 그렀겠지. ROO의 자바코드 레이어는 Controller – Entity가 전부이다. DDD스타일의 도메인 레이어가 들어간 반면에 기존의 전형적인 구조에 등장하는 DAO, Service등이 없어졌다. RoR처럼 active record 패턴을 적용했기 때문에 Entity는 그 스스로 자신에 대한 save, get, update 등의 기능을 가지고 있다. 물론 ROO는 ITD(inter-type declaration, 또는 introduction, mixin) 기법을 써서 같은 Entity에 포함될 것이지만 active record의 주요 기능은 모두 별도로 분리된 파일에 두고, aspectj를 이용해서 다이나믹하게 병합되는 방식을 사용한다. 방법은 좀 다르지만 RoR의 ActiveRecord 못지 않은 메타프로그래밍 기법을 적용하고 있다.

문제는 하나의 엔티티 오브젝트 수준에서가 아닌 도메인의 콜렉션에 접근해야 하는 경우이다. DDD 스타일로 가자면 소위 리포지토리라고 부르는 패턴을 적용한다. 물론 ROO는 트랜스패런트 퍼시스턴스 방식의 JPA를 기본으로 하고 있기 때문에 오리지날 리포지토리 개념과 미묘하게 충돌하는 점이 있다. 개빈 킹은 DDD의 리포지토리 개념/차이점/필요성을 이해못하겠다고 투덜거리기도 했다. 아무튼 복잡한 얘기는 이터니티님의 http://aeternum.egloos.com/1160846 에 잘 설명되어있다(훌륭한 글이지만 읽고나면 더 헷갈리게 될 것을 보장한다. 쫌 어렵다)

ROO의 배경이 됐던 오리지날 ROO(사실 지금의 ROO는 오래전부터 소개됐던 ROO와 전혀 다른 것이다. 같은 건 이름 뿐인데 이름도 바뀔지 모른다)에는 리포지토리라는 레이어를 두고 그 안에 finder를 구현하는 구조로 만들어져있었다. 역시 하이버네이트와 같은 투명한 퍼시스턴스 기술을 사용하기 때문에 리포지토리의 많은 필요성은 도메인 오브젝트 그 자체의 지능적인 로딩기술에 녹아들어가 버렸고, 남은 것은 애플리케이션 레벨에서 바로 엔티티를 찾는 파인더 뿐이다.

그런데 최신 ROO는 이 리포지토리 레이어를 아예 없애고 엔티티에 통합해버렸다. 액티브레코드 패턴을 따라서 인스턴스 레벨에서 퍼시스턴스 처리를 하는 것들은 인스턴스 메소드로 만들어ITD로 낑겨넣도록 했는데, 문제는 파인더 류들이다. 이게 별도의 리포지토리가 분리되 있지 않으면 안된다. 엔티티의 인스턴스 메소드에 파인더가 들어가있는 것은 웃긴 꼴이될 것이다. 검색을 하기 위해 새로운 엔티티 오브젝트를 만드는 것은 말도 안되는 것이니깐. 결국 리포지토리는 독립적인 오브젝트로 존재할 필요가 있다.

하지만 ROO는 독립적인 리포지토리를 두는 것은 하나의 부가적인 레이어를 두는 꼴이라고 생각했다.(실체가 있어야 하는 엔티티 입장에서는 리포지토리는 인터페이스이고 도메인 레이어이다라고 하는 것은 별 의미도 가치도 없는 이야기라고 하이버네이트 개발자들은 주장한다. 모든 것이 다 엔티티로만 존재하는 이상적인 ORM세계에선 그렇게 보일 수도 있다) 아무튼, ROO는 이걸 RoR처럼 스태틱 메소드로 만들기로 했다. 그래서 각종 finder메소드 류는 모든 스태틱 메소드이다.

만들어 보면 대충 다음과 같이 생겼다.

privileged aspect Rsvp_Roo_Entity {

public static long Rsvp.countRsvps() {   
    return (Long) entityManager().createQuery("select count(o) from Rsvp o").getSingleResult();       
}   

public static java.util.List<com.toby.domain.Rsvp> Rsvp.findAllRsvps() {   
    return entityManager().createQuery("select o from Rsvp o").getResultList();       
}   

public static com.toby.domain.Rsvp Rsvp.findRsvp(java.lang.Long id) {   
    if (id == null) throw new IllegalArgumentException("An identifier is required to retrieve an instance of Rsvp");       
    return entityManager().find(Rsvp.class, id);       
}   

public static java.util.List<com.toby.domain.Rsvp> Rsvp.findRsvpEntries(int firstResult, int maxResults) {   
    return entityManager().createQuery("select o from Rsvp o").setFirstResult(firstResult).setMaxResults(maxResults).getResultList();       
}   

 

벤 알렉스의 블로그 글을 읽어보면 나름 고민해서 결정한 것이었다고 한다. 그도 원래 4개의 레이어를 두는 전통적인 DDD모델에 도메인 레이어를 벗어나면 immutable한 오브젝트로만 존재하도록 하는 까다로운 스타일을 고집했던 사람이다. 그럼에도 그런 이상적인 모델이 최근 몇년간 등장해서 인기를 끌고 있는 RoR류의 경량급 접근방법에 비해서 너무 무겁고 부담스럽다는 생각을 가진듯 하다. 그래서 과감히 RoR과 마찬가지로 엔티티(액티브레코드) + 스태틱 finder를 두는 스타일을 택했다.

이제 컨트롤러에서 다음과 같이 사용한다.

public String RsvpController.updateForm(@PathVariable("id") Long id, ModelMap modelMap) {   
        if (id == null) throw new IllegalArgumentException("An Identifier is required");       
        modelMap.addAttribute("rsvp", com.toby.domain.Rsvp.findRsvp(id));       
        return "rsvp/update";       
    }  

public java.lang.String RsvpController.create(@org.springframework.web.bind.annotation.ModelAttribute("rsvp") com.toby.domain.Rsvp rsvp, org.springframework.validation.BindingResult result) {    
  …

    rsvp.persist();       
    return "redirect:/rsvp/" + rsvp.getId();       
}

 

이걸 처음 본 순간 나는 상당한 충격을 받았다.

두가지 문제가 떠올랏다.

하나는 트랜잭션 경계가 어디냐는 것이다. 또 그에 따라 비즈니스 로직은 어디로 들어가는가 이다. RoR처럼 컨트롤러 안에 트랜잭션 디마케이션 템플릿을 두고 처리할 것인가? 물론 엔티티 레벨의 로직은 엔티티에 담으면 되지만, 특정한 비즈니스 로직의 facade가 존재할 곳이 없어지면 결국 퍼시스턴스 경계와 일부 로직은 컨트롤러로 들어올 것이다. 아니라면 비즈니스 facade를 역시 스태틱 메소드로 구현해야 할지도 모르겠다. 현재 트랜잭션 경계는 엔티티의 메소드로 지정되어있다. 그나마 JPA의 CRUD 메소드들이나 그렇고, finder들은 아예 트랜잭션 설정이 없다. 벤 알렉스의 글을 보면 이메일 발송은 컨트롤러의 몫이 되었다. 과연 그것이 컨트롤러가 담당해야 할 일일까. 어짜피 두 레이어 뿐인데 적당히 아무데나? 아예 Seam처럼 Action 클래스 하나로 다 들어가는 건 어떨까?

두번째 문제는 바로 스태틱 메소드 그 자체의 문제다. (이게 사실 하고 싶은 얘기였으니 지금까지는 서론..) 로드 존슨과 스프링 지지자들이 그토록 부정했던 스태틱 메소드를 과감하게 사용한 것은 과연 어떻게 합리화 할 수 있을까? 일단 기본적인 의문에 대해선 대세를 따라 간단하게 가는거다라는 주장을 받아들이기로 하고. 다음 문제는 테스트는 어쩔 것인가 이다.

스태틱 메소드를 사용하는 테스트는 힘들다고 하지 않았나. 만약 컨트롤러에서 사용하는 finder가 아주 많은 양의 데이터를 복잡한 쿼리로 가져와야 하는 것이어서 이를 사용하는 컨트롤러의 로직을 테스트 하기가 아주 힘들다면? 또는 엔티티가 그 fidner를 사용해야 하는 로직을 가지고 있는데, 역시 finder가 매번 실행되는 것이 부담스럽다면 어째야 할까? 당연히 mock을 사용하면 되는 거다. 스텁방식으로 finder메소드 호출의 기대하는 결과값을 주도록 만들고, 인터렉션은 목의 검증기능을 이용해서 처리하도록 하면 된다. 그러나 스태틱 메소드이니 mock을 적용하는 것이 아예 불가능하다는 것이 문제다.

이건 테스트에 대해서 그토록 강조하고 강조해온 스프링으로서는 사실 치명적이다. 테스트를 못하는 스프링 서브 프레임워크가 만든 코드라니!!

 

당연하게도 로드 존슨이 먼저 이슈를 제기했다. 스태틱 파인더, 메소드의 목은 어쩔거냐고. 그리고 스스로 대안을 제시했다. 스태틱 메소드의 목을 가능하게 하는 ROO 플러그인을 직접 개발해서 제공했다. 요즘엔 거의 코드에는 손을 안대는 CEO지만, 시드니 지사를 방문한 틈을 타서 ROO플러그인 코딩을 했다고 케빈 님이 ROO핵심 개발자를 통해서 들었다고 한다. 그의 트위터를 보는 사람들은 얼마전 그가 ROO플러그인을 만들고 있다고 하는 얘기를 기억할 것이다. 뭔가 했는데 바로 이거다.

아무튼 그가 내놓은 해결책은 이렇다. 정말 ROO의 아이디어를 따라 스태틱 메소드가 필요하다면, 엔티티의 스태틱 메소드도 목으로 만들게 하면된다는 것이다. 방법은 aspectj를 이용한 AOP를 사용하는 것이다. 안되는게 없는 초강력 AOP인 aspectj를 이용해서 스태틱 메소드의 호출을 가로채서 대신 목 기능이 동작하도록 만드는 것이다. 단 세계의 클래스(정확히는 하나의 애노테이션과 두 개의 aspects) 로 이 것을 가능하게 하고, 아예 mock테스트가 만들어지도록 코드 생성 플러그인까지 제공했다. 역시 하나의 애노테이션과 두 개의 aspect로 구현한 @Configurable과 맞먹는다.

그가 만든 스태틱 메소드에 대한 mock적용 기법의 핵심은 AbstractMethodMockingControl.aj와 JUnitStaticEntityMockingControl.aj이다. 앞의 것은 목의 구현이고, 뒤의 것은 JUnit의 @Test와 함께 스태틱 메소드의 목 적용을 가능케 하는 애스팩트이다. 다음의 두개의 포인트컷이 정의되어있다.

protected pointcut mockStaticsTestMethod() : execution(@Test public * (@MockStaticEntityMethods *).*(..));

protected pointcut methodToMock() : execution(public static * (@Entity *).*(..));

이를 이용하면 @MockStaticEntityMethod가 붙은 JUnit테스트에서는 @Entity가 붙은 엔티티의 스태틱 메소드를 호출하는 것이 mock으로 동작하게 된다. 따라서 다음과 같은 테스트코드가 가능하다.

@MockStaticEntityMethods
public class RsvpTest {

    @Test
    public void testMethod() {
        int expectedCount = 13;
        Rsvp.countRsvps();
        expectReturn(expectedCount);
        playback();
        junit.framework.Assert.assertEquals(expectedCount, Rsvp.countRsvps());
    }
}

record/playback 방식의 전형적인 EasyMock스타일의 목 구현이 가능해진다. 첫번째 countRsvp()호출은 레코딩, 두번째 호출은 검증용 실제 구현이다. 이 예제는 일종의 스텁구현을 보여준 것인데, 아무튼 로드 존슨이 애용하던 EasyMock 방식과 유사하다.

이래서 결국 AspectJ의 도움으로 스태틱 메소드의 mocking이 가능해졌다.

 

대충 방향은 이렇게 결정된 듯하다. 스프링이라도 대세는 어쩔 수 없는 건가. 아니면 라이트웨이트 버전과 풀 엔터프라이즈 버전을 동시에 공략하려는 건가 모르겠지만..

 

그러나 로드 존슨이 만든 목 구현은 아주 복잡한 목 기법등은 사용할 수 없는 초보적 수준이다. 이렇게 구현하면 될 것이라는 것을 보여준 정도인듯. ROO의 ITD 적용 이후 AspectJ의 위력을 한번 더 본 것 같아서 한편으로 대단한 아이디어다 싶으면서도 뭔가 찜찜하긴 하다. 사실 스태틱의 목 적용이라는게, 이렇게 테스트 클래스 레벨에 통채로 걸리는 것도 좀 부담스럽다.

 

그러던 중에 스태틱 메소드 테스트를 위한 솔루션이 뭐가 있을까 살펴보다 PowerMock이라는 것을 알게됐다. EasyMock의 확장버전으로 출발해서 클래스로딩시 바이트코드 조작기법을 통해서 EasyMock의 모든 기능을 스태틱 메소드 테스트시에도 적용가능하게 만든 것이다. 로드 존슨이 만든 것과는 비교가 안될만큼 완벽한 목 지원 기능을 제공한다.

게다가 요즘 목계를 평정하고 있는 Mocikto 스타일도 구현을 하고 있다. 아직은 완벽하지는 않다지만 예제를 보니 감동이다. 조만간 가져다 써도 될 것 같다.

http://code.google.com/p/powermock/wiki/MockitoUsage

AspectJ나 PowerMock이나 결국 바이트코드 조작을 이용하는 것은 마찬가지이고. 현재까지 구현된 것으로는 PowerMock의 압승이다. 로드존슨이 어렵게 AspectJ로 구현하지 말고 차라리 이걸 적용했으면 더 낫지 않았을까 싶다. PowerMock ROO플러그인으로 하나 만들어서 ROO프로젝트에 제출해봐도 좋겠다. 아마 조만간 누군가 하겠지. ROO 때문에 Mockito 못쓴다고 투덜대는 Mockito 팬이 벌써 등장했을 정도니 말이다.

PowerMock은 좀 써보고, 괜찮으면 레거시 코드의 테스트에 한번 적용해봐도 좋겠다. 잘 사용하면 유용할 듯 하긴 한데, 이 거 있다고 스태틱 메소드 마구 쓰는 건 안좋겠지? PowerMock은 좀 적용해보고 다음에 한번 자세히 살펴보자.

 

스태틱메소드의 목 적용은 사실 매우 불편하다. 간단한 예제만 보면 그럴싸해보이지만, 각각 다른 목을 만들어서 DI시켜서 사용하는 패턴에 비해서 글로벌하게 한번에 한 세트밖에 적용이 안되고, 테스트 오브젝트별로 DI도 불가능하니 사용 시나리오에 상당한 제약을 받을 지도 모르겠다.

 

아무튼 스프링 프로젝트가 스태틱 메소드를 적극 사용하고,  로드 존슨이 이를 용인하고 스스로 대안을 만들어 내기도 하는 걸 보니… 정말 세상과 기술이란 계속 변하는 것이긴 하나보다. 이게 발전인지 뭔지는 잘 모르겠지만.

Related posts:

  1. Spring ROO 대충대충 분석 (4) ROO의 미래와 의의
  2. Spring ROO 대충대충 분석 (3) ROO의 Inter-type declaration
  3. Spring 3.0 (58) Static Class를 XML없이 빈으로 등록하기
  4. InsideSpring (1) Annotated Factory Method (@Configuration)을 쓰는 4가지 방법 (1)
  5. 테스트 코드에서 static import를 편하게 넣는 방법
  6. InsideSpring (1) Annotated Factory Method (@Configuration)을 쓰는 4가지 방법 (3)
  7. InsideSpring (1) Annotated Factory Method (@Configuration)을 쓰는 4가지 방법 (2)
  8. Spring ROO 대충대충 분석 (2) ROO란 무엇인가?
  9. Spring ROO 대충대충 분석 (1) 공개과정
  10. Spring Expressions(SpEL)를 이용한 Mockito Argument Matcher 만들기
  11. Spring 3.0 (56) @Bean 사용의 주의사항
  12. Inside Spring (6) 애노테이션 설정 지원 스프링 웹 테스트용 DispatcherServlet 만들기
  13. Inside Spring (5) PropertyPlaceholderConfigurer를 @Bean으로 정의해서는 안되는 이유
  14. Spring 3.0 @MVC 메소드에서 자동으로 리턴 모델에 추가되는 것들
  15. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (1)

Facebook comments:

to “테스트 할 수 없는 것을 테스트 하기. Spring ROO와 static method mocking.”

  1. 수동 트랙백 http://whiteship.me/2317
    트랙백 플러긴좀 설치하세요~

  2. 썬/ 스킨에 아예 없어. 스킨 고치기는 귀찮고. 핑뱅이 되도록 워드프레스 써.

  3. powermock외에도 jmockit, AMock, jeasytest 등이 static을 mock으로 테스트할 수 있는듯합니다. 아직 실전에서는 써본적은 없는데, 만약 꼭 써야할 일이 생긴다면 mockito와 같이 쓸 수 있는 것으로 보이는 powermock이 제일 땡기네요 ^^;

  4. Roo 개발자에게 이글좀 보여주면 좋겠는데 말이죠….

  5. 대략 보름쯤 전에 Active Record pattern에 관한 얘기를
    ROO개발자인 스테판과 한적이 있는데요.
    본문에 나온 테스트 관련 문제에 대해 말한건 아니고,
    Active Record 말고 Repository나 Dao를 지원하는 기능을 만들 계획은
    없는지 물었는데, 없다고 하더군요.
    그래서 Active Record는
    원래 DDD 아이디어와는 좀 안 맞는거 같다고 얘기를 했는데,
    DDD얘기 하다가 스테판이 이번에 미국가서
    에릭 에반스를 만날꺼라는 말을 하다가
    주제가 미국 여행으로 바뀌면서,
    대화는 안드로메다로… @_@;

    아무튼, 처음에 ROO 봤을때
    AspectJ로 mixin구성한거는 괜찮다고 생각을 했습니다.
    “ROO가 생성한 코드라 여러가지 이유로 손대기 힘들면, mixin 형태로 추가하면 되겠구나”라는 생각에서요.
    근데 Repository가 DDD의 핵심 개념중 하나라는 생각을 가지고 있어서 그런지
    Active Record는 좀 맘에 안 드네요. :(
    그런걸 차치하고 라도, 이유야 많겠죠.
    http://geekswithblogs.net/gyoung/archive/2006/04/28/76647.aspx
    http://geekswithblogs.net/gyoung/archive/2006/05/03/77171.aspx

  6. Kevin/ 귀여운 에릭 에반스 만나는 군요. http://toby.epril.com/?p=253

  7. oh~! 정말 잼있게 잘 봤습니다 ㅎㅎ
    추천으로 책사서 보다가 홈페이지 놀러와봤는데
    깨알 같은 좋은 정보나 이야기가 많아서 좋네요^^*

  8. Hello! fegdgcd interesting fegdgcd site! I’m really like it! Very, very fegdgcd good!

  9. Hello! feabedc interesting feabedc site! I’m really like it! Very, very feabedc good!

  10. Click Here To Investigate

  11. After study a few of the blog posts on your website now, and I truly like your way of blogging.I bookmarked it to my bookmark website list and will be checking back soon.Pls check out my web site as well and let me know what you think.

  12. thanks for share!

  13. thank you for share!

  14. Sneak A Peek At This Site
    fake ugg boots for sale http://fakebootsonsale.com

  15. mbt shoe stores 테스트 할 수 없는 것을 테스트 하기. Spring ROO와 static method mocking. » Toby’s Epril

  16. merrells shoes sale 테스트 할 수 없는 것을 테스트 하기. Spring ROO와 static method mocking. » Toby’s Epril

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