테스트를 자주 만드는 개발자들은 알겠지만, 테스트를 만드는 것은 그닥 어렵지 않다. 테스트를 잘 만드는 것이 어려울 뿐이다.
테스트를 만들고 코드를 작성하고, 테스트를 변경하고 코드를 변경하고....반복된 작업을 하다가 보면, 내가 무슨 짓을 하고 있는지 감을 잡기도 어렵고, 테스트를 매번 고쳐주는 것도 싫증이 난다.
자꾸 테스트가 깨지는 이유가 무엇일까? 그 중에 대표적인 경우가 테스트할 메소드 안에서 테스트된 메소드나 다른 메소들을 호출을 할때 새로운 메소드가 추가가 되면서 깨지는 경우가 많다. 그 경우 메소드 안에서 호출한 메소들이 모두 깨지게 되는데 이 현상을 해결하려면 하나의 로직을 변경했다고 하더라도 테스트를 통과하기 위해 여러개의 단위 테스트 메소드에 손이 가야 한다. (음..말이 좀 어렵군) 단위 테스트에 다수의 경험이 있으신 분들중에 이 고민 안해본적 없을 것이다.
난 3가지 경우로 문제를 해결을 한다. 1. delegate class 2. method overriding 3. 자신의 레퍼런스를 mock으로 치환
1번의 경우 작업들이 비교적 순차적이고, 응집성이 높은 작업들을 할때 extract class를 통해서 가독성도 좋아지고, host class의 dependency가 이뻐보이게 될떄는 1번을 사용한다. 이 방법이 가장 정석적인 방법이다.
근데, 애매한 메소드들이 있다. host와 dependency가 거의 같고, seperate를 하자니 클래스도 많아지고 웬지모를 찜찜한 기분이 들떄는 과감하게 2번을 사용한다. 약간의 노출수위(private->protected)가 거슬리기는 하지만, 견고한 테스트를 만든다면, 약간의 노출은 괜찮다는 생각이다.
3번의 경우는 클래스를 테스트할 때 특정메소드에 집중해서 테스트를 할때 사용이 된다. 특정 메소드를 중점으로 테스트를 하고 나머지 부분들은 단순 콜만 된다면, 만족스러운 테스트인 경우이다. 테스트할 메소드의 갯수에 따라서 서비스 코드를 잘 짜야 하는 경우이다.
3 번의 경우도 몇번 고려는 했었는데, 특이한 경우 안이면, 난 1,2번을 선호하는 편이다. 1번은 테스트의 정석인 방법이라 제일 먼저 고려를 해보는 방법이고, 2번은 mock을 만들지 않고 비교적 쉽게 테스트가 가능하다는 것이 장점이다.
3번의 경우를 고려해 볼수 있는 것은 거의 대부분 1번으로 처리를 하려고 하고, 1,2번 사이에서 메소들의 행위나 특성들이 애매할 경우는 범위를 고려해서 3번을 선택한다.
갑자기 이 글을 쓴 것은 아래 글을 읽다가 나도 비슷한 경우의 경험을 많이 해서 흔적으로 남기고 싶어서이다. 아래 내용을 읽고 위에 내용을 보면, 좀더 이해가 쉬울 것이다. EP처럼 구체적인 예로 적을 수도 있지만, 워낙 설명을 잘해놔서 아래 내용을 참고하는게 더 나을거라는 생각이다. 내가 제시한 3가지 사례중 주로 3번에 대한 내용을 구체적으로 잘 설명을 했다. http://colus.egloos.com/4639268
Infinitest is a continuous JUnit test runner designed to facilitate Test Driven Development. It helps you get the most out of TDD by reducing your feedback cycle from minutes to seconds.
Infinitest 는 테스트기반의 개발을 하기 위해서 를 용이하게 하기 위해서 지속적으로 JUnit test를 실행하게 설계 되었다. 수분에서 수초까지 피드백 주기를 줄여주는 TDD로 부터 가장 중요한 것을 얻을수 있도록 해준다.
Using Infinitest
When Infinitest first launches, it runs any test that extends from junit.framework.TestCase or has methods that have the org.junit.Test annotation. After that, Infinitest will run the test again if it, or one of it's dependencies, changes. Changes to direct and indirect dependencies will trigger a test run. Only the minimal set of tests which depend on those changed files will be run.
첫번째로 Infinitest 어플리케이션이 실행될때 junit.framework.TestCase를 상속한 클래스나 org.junit.Test annotation을 찾아서 모두 실행을 한다. 그 이후에는 테스트 또는 그것에 의존하는 것들이 변경되면 테스트를 다시 실행을 한다. 직접,간접적으로 의존하는 것들의 변경되는 경우 테스트를 자동으로 실행을 한다. 변경된 파일에 의존하는 테스트들의 최소한의 셋만 실행이 될 것이다.
Test Driven Development Infinitest is made with the Test Driven Developer in mind. You'll find yourself keeping your hands on the keyboard, in the flow, as Infinitest runs your tests for you with each change. Infinitest will act as a virtual pair programmer...keeping your honest and helping you take small steps.
Refactoring Refactoring is easy with Infinitest. The bar should always stay green. If the bar turns red, you made a mistake! Back up and try again. Refreshing The Graph If you find yourself needing to run all of your tests from Infinitest, you can rebuild the dependency graph by hitting the reload button. This will rebuild the dependency graph and re-run all of your tests.
Stopping the tests If you need to temporarily stop Infinitest from running your tests, you can hit the stop button. This will prevent Infinitest from running tests after changes occur. When you resume running tests (by hitting the button again), the entire test suite will be run.
Configuring Infinitest Filtering Tests You may have tests in your classpath that you don't want Infinitest to run. These tests can be filtered out by creating a file in the working directory of your project named infinitest.filters. It should contain one regular expression per line. Any class names (not file names) that match any regular expressions in that file will not be run. For example: org\.myproject\.acceptance\..* .*\$.* will filter out all the classes in the com.myproject.acceptance package, and any inner classes (which always contain a $).
ex) 내가 진행하는 프로젝트인 경우는 서버환경에 따라서 테스트가 실패할수 있는 테스트는 IntegrationTest를 suffix에 붙여서 사용한다. 그래서 다음과 같이 파일을 만들어서 넣어주면 Infinitest 어플리케이션이 알아서 exclude를 하고 실행을 한다.
* 파일 이름 : infinitest.filters * 파일 내용 com\.nhn\.community\..*IgnoreTest com\.nhn\.community\..*IntegrationTest com\.naver\.blog\..*IgnoreTest com\.naver\.blog\..*IntegrationTest .*\$.*
간단후기~
모든 테스트가 실행될때 보이는 하단에 progressive bar가 중독성있네요.
Infinitest 사용을 해보니 편하고 재미도 있습니다. 코드 개발을 하다가 보면, 테스트 돌리는 것도 귀찬은 작업중의 하나인데, 자동(trigger)으로 해주니 좋습니다요!
* 에러로그 An error occurred at line: 70 in the generated java file The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding the65535 bytes limit Stacktrace: org.apache.jasper.compiler.DefaultErrorHandler.jav acError(DefaultErrorHandler.java:98
아주 오래전에 한번 들어봄직한 자바파일 사이즈가 65535 bytes제한이 있다는 이야기...^^;
이거 실제로 겪어보니 황당하기도 하고, 난감하기도 했다.
서버쪽 비즈니스 로직 코드에 자바파일이 이만큼 큰 경우는 거의 없을 것이다.
하지만, jsp는 화면이 복잡하다가 보면 코드량이 많아져서 발생할수 있는 문제중의 하나이다.
이 문제를 해결하기 위한 방법! js나 css를 파일로 분리한다, 1. 링크형태로 jsp페이지에 넣는다. 2. 반드시 페이지 안에 넣어야 한다면, 베스트 선택은 커스텀 태그이다. 동적으로 페이지 안으로 코드를 넣기 때문에 사이즈가 조금 밖에 증가하지 않는다.
* jsp에 있는 커스텀태그가 컴파일된 이후 자바파일 코드 private boolean _jspx_meth_b_js_0(PageContext _jspx_page_context) throws Throwable { PageContext pageContext = _jspx_page_context; JspWriter out = _jspx_page_context.getOut(); // b:js com.naver.blog.foundation.web.taglib.HtmlResourceIncludeTag _jspx_th_b_js_0 = (com.naver.blog.foundation.web.taglib.HtmlResourceIncludeTag) _jspx_tagPool_b_js_path_nobody.get(com.naver.blog.foundation.web.taglib.HtmlResourceIncludeTag.class); _jspx_th_b_js_0.setPageContext(_jspx_page_context); _jspx_th_b_js_0.setParent(null); _jspx_th_b_js_0.setPath("/common/javascript/common.js"); int _jspx_eval_b_js_0 = _jspx_th_b_js_0.doStartTag(); if (_jspx_th_b_js_0.doEndTag() == javax.servlet.jsp.tagext.Tag.SKIP_PAGE) { _jspx_tagPool_b_js_path_nobody.reuse(_jspx_th_b_js_0); return true; } _jspx_tagPool_b_js_path_nobody.reuse(_jspx_th_b_js_0); return false; }
상속을 통해서 클래스 갯수를 줄이고 있던 디자인이 있었다. 거의 7개 가량의 갯수를 줄이기 위해서 디자인이 깨지는 것을 보면서도 이게 실용적(?)이라고 생각을 하면서 클래스 갯수를 줄였지만, 영 마음이 불편했다. OOP의 원칙을 깨가면서 만든 디자인이었기때문이다.
불편한 마음을 없애기 위해서 과감하게 클래스 갯수를 늘렸다. 일단, 원칙을 지키면서 점진적으로 refactoring을 하기로 결심을 했다. 대략적인 윤곽은 마음 속에 있었지만, 사실 목적은 분명하지 않은채 점진적으로 변경을 꾀하면서 변경된 코드 안에서 좀더 나은 디자인으로 개선을 해나갔다.
대략의 과정을 설명을 하자면,
구현 상속으로 되어 있던 디자인을 delegate를 통해서 데코레이션 패턴(나..이 패턴 너무 좋아한다..^^;)으로 변경하고 , 인터페이스만을 wiring하는 클래스를 만들고, dependency injection을 spring config에서 설정을 통해서 인스턴스 생성과 Factory에 등록을 했다.
디자인을 변경 후 3가지의 장점을 가질 수가 있었다.
1. 테스트가 쉬워졌다. 구현상속은 많은 dependency를 가지게 된다. dependency를 하나의 인터페이스로 추상화가 가능했고, 하나의 mock을 생성해서 테스트를 할 수가 있었고, 확장하는 클래스에서 필요한 클래스들만 mo필ck으로 만들어서 테스트를 하면 되었기때문에 테스트를 작성하기 쉬웠다.
2. 코드의 이해가 쉬워졌다. 구현에 대해서 집착을 하지 않아도 되고, 클래스 자체만 봐도 이해를 하기 쉬웠다. 다른 클래스들과의 관계는 이 클래스를 이해하는데, 별다른 장벽이 되지 않았다.
3. 객체지향원칙을 지킬 수 있었다. 부모 클래스에서 확장되어지는 자식 클래스때문에 로직이 추가가 되면 안된다. 비즈니스 로직 특성상 피하기 어려웠지만, 최소한의 변경으로 디자인이 깨지는 것을 피할 수 있었다. 즉, 자식과 관련되서 if문이 증가하지 않았다.
아마 spring configuration을 사용하지 않았다면, 클래스 갯수를 7개정도 늘려서 DI를 해주는 상황이었다. 원칙에 따라서 디자인을 개선을 하고 나니 나름 볼만한 코드가 되었다. 클래스 갯수가 총 8개가 늘어나야 할 상황이었지만, 7개에 해당하는 dependency부분을 spring IoC가 해결을 해주었고, 인터페이스를 wiring하는 심플한 하나의 클래스만 늘어나게 되었다. spring config를 통한 IoC가 아니었어도, 클래스 갯수를 늘려서 확장하는 것이 좋은 선택이었을 것이다. 그 좋은 선택 안에서 spring IoC는 좀더 나은 기쁨을 준 것이다.
사실, 서비스되고 있는 코드를 refactoring하는 것은 쉬운 일은 아니지만, 계속된 찜찜한 마음의 짐을 떨치고 싶었다. 좀더 자신감을 가지고 변경을 꾀할수 있었던 것은 테스트가 된 구현 클래스들을 인터페이스 기반으로 조립하는 일이었기때문에 가능했다. 구현을 다시한 부분은 거의 없고, 클래스들 간의 관계를 바꾸는 디자인 개선작업이어서 좀더 편하게 변경을 가할수 있었다.
대표적인 것은 클래스의 멤버변수로 있는 Collection인터페이스이다. 어떤 책인지 기억이 잘 나지는 않지만, 이미 여러군데의 책에서 다룬 내용이기는 하다.
아래에서 다룰 내용은 추상화, 캡슐화에 대한 이야기다.
클래스에서 get을 써서 외부에 어떤 behvior나 자신의 상태를 expose를 할 때 주의를 해야 할 상황이기도 하다.
첫번째 코드
public class SimpleSet{
private List<Simple> simpleList = new ArrayList<Simple>();
public List<Simple> getSimpleList() {
return simpleList ;
} }
위와 같은 코드의 문제점은 무엇인가? 우리가 쉽게 쓴 패턴이다. 하지만, 클라이언트 코드에서 simpleList의 레퍼런스를 받은 후 수많은 조작이 가능하다.
심지어 이런 코드도 가능하다. simpleList.clear()
필요하다면 SimpleSet에 clear를 행위로 만들어줘야한다. 직접적인 호출은 수많은 버그들을 만들 가능성이 커진다. 의도하지 않는 곳에서의 조작으로 디버깅도 어렵게 만든다.
두번째 코드
public class SimpleSet{
private List<Simple> simpleList = new ArrayList<Simple>();
public List<Simple> getSimpleList() { return Collections.unmodifiableList(addedInfoList); } }
위와 같은 코드의 문제점은 무엇인가? 리스트를 바꿀 수 없게 collection API는 위와 같은 proxy(decorator) pattern의 인터페이스를 제공한다. 이제는 외부에서 simpleList 를 readonly만 접근이 가능하다.
세번째 코드
public class SimpleSet{
private List<Simple> simpleList = new ArrayList<Simple>();
public Iterator<simple> iterator(){ return this.simpleList .iterator(); } }
한번더 생각을 해보자. 외부로 제공할 때 iterator패턴으로 위와 같이 제공을 한다면, 어떨까? 내부에 collection의 인터페이스마져 숨길 수가 있다. 항상 리스트만이 내부적인 알고리즘이나 로직을 처리하기 위해서 최선이라고 얘기를 할수는 없다.
SimpleSet의 로직을 처리할 때 List보다 더 나은 자료구조가 생겼을 때 아주 쉽게 갈아탈수가 있다. 위와 같은 코드는 가장 추상화가 잘된 클래스이다.
사실 첫번째 코드가 아니라면, 두번째나 세번째 코드를 권장하고 가능한 세번째 코드를 이야기를 하고 싶다. 두번째 코드도 collection자체가 워낙에 유연해서 쉽게 만들수 있는 인터페이스이기 떄문이다. 물론, 출력자체를 iterator로 하는 거 자체에 많은 이점도 있기때문에 개발자가 심사숙고해서 선택할 expose 인터페이스이다.
여러가지 조합으로 만들기도 하지만, 자바 자체의 API를 이용해서 좀더 편하게 만들수 있을 것이다.
A UID represents an identifier that is unique over time with respect to the host it is generated on, or one of 216 "well-known" identifiers.
unique, an int that uniquely identifies the VM that this UID was generated in, with respect to its host and at the time represented by the time value (an example implementation of the unique value would be a process identifier), or zero for a well-known UID
time, a long equal to a time (as returned by System.currentTimeMillis()) at which the VM that this UID was generated in was alive, or zero for a well-known UID
count, a short to distinguish UIDs generated in the same VM with the same time value
VMID is a identifier that is unique across all Java virtual machines. VMIDs are used by the distributed garbage collector to identify client VMs.
VMID() Create a new VMID. Each new VMID returned from this constructor is unique for all Java virtual machines under the following conditions: a) the conditions for uniqueness for objects of the class java.rmi.server.UID are satisfied, and b) an address can be obtained for this host that is unique and constant for the lifetime of this object.