[Spock] 포팅 경험기1

페이스북에 “테스트코드와 친해지기“라는 그룹을 만들고 처음으로 한 프로젝트가

최범균님의 DDD START책에 java + junit으로 되어 있는 테스트코드를 groovy + spock으로

포팅하는 것이었습니다.

해당 프로젝트는 ddd-repeat-start 에서 확인하실 수 있습니다.

테스트코드와 친하지 않더라도 java 개발자라면 junit으로 간단한 테스트코드는 만들어 본

경험이 있을텐데요.

굳이 기존에 익숙한 junit을 쓰지 않고 spock으로 포팅하게 된 이유를 51개의 테스트케이스를

포팅하면서 정리해 보았습니다.

* groovy언어만의 장점은 해당 내용에서 제외하였습니다.


1. 의존 라이브러리 추가

spock을 사용하기 위해 기존 최범균님 프로젝트 pom.xml 파일에서 아래 라이브러리를

추가했습니다.

기존 테스트코드가 spring boot로 되어 있었기 때문에 spock-spring 모듈을 사용했습니다.

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.0-groovy-2.4</version>
</dependency>

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.4</version>
</dependency>

cglib는 class mocking을 위해 추가했습니다. (SecurityCancelPolicyTest.groovy에 사용)

2. given-when-then 템플릿을 프레임워크 차원에서 제공

spock의 장점은 BDD(Behavior Driven Development)에서 나온 템플릿인

given(테스트할 조건을 주고)-when(테스트를 실행한 뒤)-then(결과를 검증한다) 템플릿을

프레임워크에서 제공하고 있다는 점입니다.

예전에 junit을 사용할 때는 주석을 사용해 템플릿으로 만들어 쓰고 있었지만, 이제

프레임워크에서 제공해주다 보니 자연스럽게 이 패턴대로 테스트코드를 작성할 수 있습니다.


//junit
@Test
public void addTest() {
    // given
    int a = 1;
    int b = 2;
 
    // when
    int result = a + b;

    // then
    assertThat(result, is(3));
}


//spock
def "addTest"() {
    given:
    def a = 1
    def b = 2

    when:
    def result = a + b

    then:
    result == 3
}

둘 간의 차이점이 확 느껴지시나요? spock을 사용했을 때가 훨씬 명시적이고 직관적임을

알 수 있습니다.

3. 결과검증 간편

junit을 사용할 때는 기본적으로 Junit이 제공해주는 Assert 클래스의 기능과 부가적으로

hamcrest, fest assert, assertj등의 라이브러리를 사용해서 결과를 검증해야만 했습니다.

//junit 
assertThat(result, is(1));
assertThat(result, is(true));
assertThat(result, is("test"));
assertThat(result, is(1));
assertThat(result, instanceOf(TestClass.class));
assertThat(result, nullValue());
assertThat(result, notNullValue());

@Test(expected = IllegalArgumentException.class)

try {
    a.function();
}catch(IllegalArgumentException e) {
    fail();
}
   
//spock
then:
result == 1
result == true
result == "test"
result instanceof TestClass
result == null
result != null
thrown IllegalArgumentException
notThrown IllegalArgumentException

하지만 위의 예제에서도 나오듯이 spock에서는 상대적으로 결과검증이 간편하고 직관적입니다.

4. Mock 사용 편리

junit으로 유닛테스트를 만들 때, Mock사용을 위해 mockito 라이브러리를 많이 사용합니다.

하지만 spock을 사용하면 자체적으로 Mock기능을 사용할 수 있습니다.

//junit
@Mock
private Order order;

when(order.getOrderer()).thenReturn(new MemberID("user1"), "");
 
verify(order, times(2)).purchase();

//spock
//SecurityCancelPolicyTest.groovy
given:
def order = Mock(Order)
order.getOrderer() >> new Orderer(new MemberId("user1"), "")

then:
2 * order.purchase();

stub이나 spy등을 사용할 때도, mockito에 비해 상대적으로 명시적으로 사용할 수 있습니다.

그리고 무엇보다 결과검증을 할 때, 메소드 호출의 경우 “호출횟수 * 객체.메서드명()“같이

사용할 수 있어 편리합니다.

다만, PlaceOrderServiceTest.groovy에서는 ArgumentCaptor를 사용하기 위해

mockito를 사용했습니다.

then:
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class)
verify(mockOrderRepository).save(orderCaptor.capture())

Order createdOrder = orderCaptor.getValue()
createdOrder != null

5. 동일 실행구문에서 여러 조건으로 테스트 용이

테스트코드를 만들다 보면 실행구문은 동일한데 조건을 다르게 줘야 하는 경우가 있습니다.

보통 이럴 때는 조건에 따라 변수에 값 할당이나 객체를 다르게 만들어줘야 하는데요.

spock에서는 이런 상황에서 expect(혹은 when~then)~where로 보다 편리하게

테스트코드를 만들 수 있습니다.

def "EventStore에서 이벤트를 가져온다."() {
    given:
    List<EventEntry> entries = eventStore.get(1, 2)
    
    expect:
    SampleEvent event = objectMapper.readValue(entries.get(index).getPayload(), SampleEvent.class)
    event.getName() == name
    event.getValue() == value
    
    where:
    index | name    | value
    0     | "name2" | 12
    1     | "name3" | 13
}

6. 기타

그 밖에 spock을 사용하면서 아래와 같은 장점을 느꼈습니다.

  • 메서드명에 “한글“을 넣는 것이 junit에 비해 자연스럽다
  • private 필드나 메서드 접근이 가능하다
  • 테스트 실패시 나오는 결과를 보고 원인을 쉽게 알 수 있다.

다음 포스팅에서는 spock으로 포팅하면서 겪은 어려움이나 불편함에 대해 정리할 예정입니다.

* 위 내용 중 잘못된 내용이나 수정해야 할 부분이 있으면 언제든지 의견 부탁드립니다.

[프로그램]DBUnit에서 사용하는 XML 생성기

DBUnit을 이용하여 통합테스트코드 작성시, 테스트에 쓰일 DB 테이블과 매칭되는

XML 파일이 필요하다.

이를 테스트마다 수동으로 만드는 것은 굉장히 귀찮고 비효율적인 일이어서

간단하게 Groovy를 이용하여 DBUnit용 XML을 자동으로 생성해주는 프로그램을

만들었다.

GitHub에 올린 소스는 Oracle, MySql용으로만 올렸고, 아래 소스를 수정하면

다양한 DBMS로의 응용이 가능하다.

  • 테이블 이름을 넣고 널이 아닌 컬럼을 체크하는 SQL 구문

    SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=? AND IS_NULLABLE=\’NO\’ ORDER BY ORDINAL_POSITION
  • 테이블에서 한개의 ROW를 가져오는 SQL 구문
    SELECT * FROM ‘+tableName+’ LIMIT 1

혹시, 로컬개발환경에서 Groovy 파일 실행시 한글을 인식하지 못해 문제가 생긴다면

-Dfile.encoding=UTF-8 옵션을 주면 된다.

[TDD]숫자야구게임 연습

테스트 주도 개발(Test Driven Development)로 숫자야구게임을 구현해보았다.

1시간 30분동안 “실패-성공-리팩토링“의 과정을 거치며 최범균님이 구현한 결과와

비교해 보았다.

================================================================

1. “실패 => 성공 => 리팩토링”에서 “실패“과정을 따로 확인하지 않은 적이 많다.                   이것은 실제로 보폭을 넓게 가지고 갔다기보다 테스트코드를 만들 때 “성공“시키기           위해 구현을 먼저 시작한 경우가 많다는 뜻이다.

2. 적절한 시기에 리팩토링이 잘 되지 않았다.

3. 실제 구현에 대한 로직이 세련되지 못하다.

4. 그리고, 전에 범균님 동영상을 보지 않고 구현했으면 더 좋았을 것 같다! ㅎㅎ

5. IntelliJ는 Eclipse에 비해 리팩토링기능은 더 뛰어나고 테스트코드 실행속도는                  훨씬 느리다.

===============================================================

범균님 동영상을 이전에 보고 구현한 결과, 동영상 속의 결과물이 떠올라

제대로 내 생각을 구현에 넣지 못하는 문제가 있었다.

혹시나, TDD로 숫자야구게임을 구현해 보고 싶다면 꼭 먼저 구현해보고 범균님

동영상을 봤으면 하는 생각이다.

TDD로 개발할 때 개발속도나 설계상의 미흡한 점은 연습을 하면 할수록 보완되리라

생각된다.

그리고 이렇게 TDD로 구현을 완료하고 남은 테스트코드가 추후 유지보수시에도 훌륭한

문서가 될 것이기 때문에 꾸준한 TDD연습이 필요함을 느꼈다.

[참고 URL]

[모델링]코드리뷰짝 매칭결과 알림

들어가기

팀원 6명에 대해 코드리뷰 짝을 지어주면 재밌을 것 같아 작년말에 Node.js를 이용해서

간단하게 프로그램을 만들었다.프로그램에 대한 자세한 소개는 여기를 참고하면 된다.

이번에 얘기할 주제는 프로그램 구현완료 후에 모델링을 다시 해본 내용이다. =============================================================================

1. 최초버전

나름, 알림을 보내는 부분과 결과를 파싱하는 부분이 변화되는 부분이라 생각되어

AlarmSender와 Parser인터페이스를 정의했다.

알림내용에 대한 부분도 공통적인 부분은 Alarm(SmsAlarm도 생각해서)으로 정의하고

그것을 상속받아 EmailAlarm을 만들어 모델링했다.

하지만, 모델링과정에서 “역할“에 대한 정의가 잘못되어 Main 클래스에 해당하는

CodeReviewMathcingPairResultBatch클래스에서 알림내용을 만들고 있고,

통지내용을 모델링하기 위해 Alarm클래스를 만들었지만, 알람은 그냥 알람일 뿐인데

코드리뷰매칭결과“를 Alarm이란 이름으로 정의한 것도 결국 “역할“에 대한 잘못된

정의에서 나온 결과물이다.

class_diagram_mod   2. 중간버전

기존의 Alarm이 담고 있던 “코드리뷰매칭결과“를 CodeReviewPairResult클래스로

모델링했지만, 클래스다이어그램만 봤을때는 이것이 무엇을 담고 있는지 명확하지

않은 문제가 있다.

그리고 “코드리뷰매칭결과“를 가져오기 위한 부분도 저수준모듈인 HtmlParser클래스에

의존하고 있다. 저렇게 되면 “코드리뷰매칭결과“를 웹페이지가 아닌 DB 혹은 파일에서

가져왔을 때 CodeReviewService클래스를 수정해야된다.

결국, 수정에는 열려있고 확장에 닫혀있는 구조가 된 것이다.

그래도, 통지기(Notifier)인터페이스의 notify메소드 파라미터를 “코드리뷰매칭결과”와

“팀원”으로 나누어 정의한 것은 통지기의 “역할“에 대한 고민의 흔적이다 class_diagram_v5_mod 3. 최종버전

총 8번에 걸쳐 최범균님과 모델링결과물에 대해 논의하고 리팩토링한 결과 아래와 같은

클래스다이어그램이 나오게 되었다.

제일 큰 변화는 “코드리뷰매칭결과“를 가져오는 부분이다.

이전에는 Parser로 추상화해서 정의했다면, 최종버전을 만들면서 가장 고민했던 부분이

 “코드리뷰매칭결과를 가져오다“라는 부분에 집중했다는 것이다.

이 말은 즉, Parser로 추상화해서 정의한 것이 웹페이지에서 결과를 가져오는

구체적인 “구현“에 의존했다면,  MatchingResultSource인터페이스를 정의함으로서,

 “코드리뷰매칭결과를 가져오다“라는 부분을 올바르게 “추상화“할 수 있었다.

MatchingResultSource인터페이스를 정의함에 따라,  “코드리뷰매칭결과” 모델링도 각각

MatchingResult와 Group으로 정의했다.

CodeReviewPairResult클래스에 제목과 내용만 있던 전과 비교해 “코드리뷰매칭결과“에

어떤 내용이 담겨있는지가 보다 더 명확해졌다.

그리고, 전체적으로 인터페이스와 클래스, 메서드의 “이름“에 신경썼는데

결국, 겉으로 드러나는 “이름“을 통해 말하고자 하는 의도가 더욱 분명히 드러나기 때문에

더욱 고민을 많이 했던 부분이다.

class_diagram_v8_mod=============================================================================

마치며..

이번 모델링과 리팩토링을 통해  “코드리뷰짝 매칭결과를 가져와 팀원에게 알려준다

라는 고수준 로직을 적절하게 “추상화“하고 그에 따른 “다형성” 구현을 통해 각각의

역할“과 “책임“을 정의함으로서, 향후 유지보수에 유연한 구조를 만들 수 있게 되었다.

이것이 가능한 이유는 객체지향 설계 중, 추상화/다형성을 통한 “의존역전원칙(DIP)“과

리스코프치환원칙(LSP)“을 지킴으로서, 결국엔 수정에는 닫혀있고 확장에는 열려있는

개방폐쇄원칙(OCP)“을 준수했기 때문이다.

그리고, 추가적으로 OCP를 준수함으로써 CodeReviewService의 고수준모듈 로직,

코드리뷰짝 매칭결과를 가져와 팀원에게 알려준다“를 재사용할 수 있게 되었다.

8번에 걸친 수정을 하면서 익숙하지 않은 “모델링“에 대한 “역할“과 “책임“정의가

쉽지 않았다.

책으로만 봤던, 수정이 용이한 유연한 설계에 대한 이론에 고개를 끄덕이면서 머리로만

이해하려고 했던 내용을 실전에서 부딪치니 생각처럼 “모델“이란 녀석이 쉽게 도출되는

게 아니구나라는 걸 깨닫게 되었다.

적절하게 “모델”을 추출하기 위해서는 많은 지식과 경험이 필요하고, 또한 깊이있는

고민과 사색이 수반되어야 함을 느꼈다.

마지막으로 그간의 과정이 결코 쉽지 않았지만, 힘들었던 것 만큼 재미있는 시간이었다

[리팩토링]빌더패턴을 활용해 객체 생성하기

스프링MVC로 만들어진 기존 서비스단 코드에 아래와 같이 매개변수 별로

동일객체를 만드는 코드가 있었다.

아래는 알림객체인 Notification을 만들어 전송하는 코드이다.

public void createNotificationByCase1(int a, int b, int c, String d, String f) {
    Notiifcaiton notification = new Notificaiton(a, b, c, d, f);
    sendAlarm(notification);
}
public Notification createNotificationByCase2(int a, int b, String d, String e) {
    Notiifcaiton notification = new Notificaiton(a, b, d, e);
    sendAlarm(notification);
}
public Notification createNotificationByCase3(int a, int b, int c, String d) {
    Notiifcaiton notification = new Notificaiton(a, b, c, d);
    sendAlarm(notification);
}

메서드Case1 ~ Case3별로 각기 다른 매개변수를 가지고 Notification 객체를 만든다.

그리고 Notification 클래스 내부는 아래와 같다.

public class Notification {
    private int a;
    private int b;
    private int c;
    private String d;
    private String e;
    private String f;

    Notification(int a, int b, int c, String d, String e, String f) {
       this.a = a;
       this.b = b;
       this.c = c;
       this.d = d;
       this.e = e;
       this.f = f;
     }
     Notification(int a, int b, int c, String d, String f) {
         this(a, b, c, d, null, f);
     }
     Notification(int a, int b, String d, String e) {
         this(a, b, 0, d, e, null);
     }
     Notification(int a, int b, int c, String d) {
         this(a, b, c, d, null, null);
     }
}

Notification 클래스 필드 6개 중, Notification 객체를 만드는 데 필요한 필수 멤버변수는

a, b, d고, 나머지 c, e, f는 선택 멤버변수이다.

위 Notification 클래스의 생성자를 보면 값이 없는 건 기본값(0 혹은 null)을 넣어서

다른 생성자를 호출하는 것을 알 수 있다.

위의 예제는 샘플코드를 만드느라 멤버변수를 6개만 넣었지만, 실제 리팩토링 한 코드의

클래스 멤버변수는 총 12개였고, 필수와 선택 멤버변수가 각각 6개씩이었다.

이런 상황하에 각 서비스에서 알림객체를 만들어 전송하려고 notificationService에 각기

다른 매개변수를 가진 메서드(Case1 ~ Case3 메서드)를  호출한 것이다.

notificationService를 이용하는 서비스단의 코드는 아래와 같다.

notificationService.createNotificationByCase1(a, b, c, d, f);

위와 같이 알림객체를 생성할 경우, 새로운 서비스에서 알림서비스를 사용할  때

또 다른 매개변수를 가진 메서드를 알림서비스에 만들어야 할 것이고, 그것을 이용하는

각 서비스들의 코드는 이해할 수 없는 매개변수의 나열로 가독성이 매우 나빠질 것이다.

타인은 물론 본인도 추후에 해당코드를 유지보수 할 경우, 어떤 데이터가 어디에 들어가

있는지 정확히 기억하기도 힘들고, 생성자 매개변수가 많으면 많을수록 그 복잡도는

매개변수 갯수에 비례해 증가하게 된다.

결국, 유지보수 시간이 기하급수적으로 늘게돼 나중에는 쳐다보기도 싫은

코드로 전락해버릴지도 모른다.

그래서, 이 복잡한 객체생성코드를 빌더패턴을 사용해서 리팩토링 하기로 했다.

빌더패턴을 사용해 객체생성을 하는 코드는 아래와 같다.

public class Notification {
    private int a;
    private int b;
    private int c;
    private String d;
    private String e;
    private String f;

    private Notification(Builder builder) {
        this.a = builder.a;
        this.b = builder.b;
        this.c = builder.c;
        this.d = builder.d;
        this.e = builder.e;
        this.f = builder.f;
     }

     public static class Builder {
        // Require Fields
        private int a;
        private int b;
        private String d;

        // Option Fields
        private int c;
        private String e;
        private String f;

        public Builder(int a, int b, String d) {
            this.a = a;
            this.b = b;
            this.d = d;
        }

        public Builder c(int c) {
            this.c = c;
            return this;
        }

        public Builder e(int e) {
            this.e = e;
            return this;
        }

        public Builder c(int f) {
            this.f = f;
            return this;
        }

        public Notification build() {
            return new Notification(this);
        }
    }
}

빌더패턴을 사용하고 난 이후에는 Notification클래스에 각 매개변수별로 만들어져 있던

생성자가 사라지고, Builder클래스의 build메서드를 통해 단일창구로 Notification객체를

만들게 되었다.

그리고 각 서비스에서 notificationService에 전달할 Notification 객체를 빌더패턴을

사용해 생성하고 아래와 같이 전달한다.

Notification notification = new Notification.Builder(a, b, d).c(c).f(f).build();
Notification notification = new Notification.Builder(a, b, d).e(e).build();
Notification notification = new Notification.Builder(a, b, d).d(d).build();
notificationService.sendNotification(notification);

Notification객체를 생성하는 필요한 필수 멤버변수인 a, b, d는 Builder클래스의 생성자를

통해 전달하고, 나머지 선택 멤버변수인 c, e, f는 set메서드 역할을 하는 메서드를 사용해

필요할 때 호출해서 값을 세팅하고 마지막에 build메서드를 호출해 Notification 객체를

생성하게 된다.

이로 인해, Notification 객체를 생성해 전송하던 notificationService의 각기 다른

매개변수를 가지던 메서드는 사리지고, Notificaiton객체 하나만을 받는

메서드 하나로 리팩토링 되었다.

긴 매개변수를 받던 메서드들이 사라지고, Notification 객체 하나만을 받도록 리팩토링 한

아래 코드를 보면 전과 비교해 코드 가독성이 월등히 좋아졌음을 알 수 있다.

public void sendNotification(Notification notification) {
    //기존 Notification 객체생성부분 삭제
    sendAlarm(notification);
}

빌더패턴을 사용해 객체 생성부분을 리팩토링해서 얻은 결과는 아래와 같다.

  • 각기 다른 매개변수별로 중구난방으로 만들어져있던 notificationService의 메서드를 Notification객체를 받는 단 하나의 메서드로 통일시킴
  • Notification클래스에 각기 다른 매개변수별로 만들어져있던 생성자를 Builder객체를 이용해 객체를 만드는 방법으로 통일시킴
  • 각 서비스별로 notificationService의 각기 다른 매개변수들을 가진 메서드를 호출하고 있던 것을 빌더패턴을 이용해 Notification객체를 생성하고 Notification객체를 받는 하나의 메서드를 호출하는 것으로 통일시킴

무엇보다 제일 좋은 것은 리팩토링으로 얻은 코드 가독성이다.

전에는 쳐다보기조차 싫었던 코드가 빌더패턴을 사용해 객체생성부분을 수정함으로써,

다음에 또 수정사항이 생겨도 빠른 시간안에 대처할 수 있는 여력이 생겼다는 것이다.

역시나 일상 속에 지속적인 리팩토링과 이를 뒷받침하는 테스트코드의 중요함을 다시

한번 느끼는 하루였다.

혹시나, 빌더패턴에 대해 궁금하신 분은 Effective java 항목2를 읽어보면 많은

도움이 될 것이다.

[리팩토링]리팩토링 사례 공유 모임 후기

저번달에 범균님과 함께 리팩토링했던 스타크래프트 프로리그 앱에  대한 내용을

6월 27일 저녁에 오프라인에서 공유하는 자리를 가졌다.

해당 내용은 범균님 블로그에서 확인할 수 있다.

20130627_200654

리팩토링 내용은 크게 아래와 같다.

  • 클래스/인터페이스/메서드/필드등을 의미있는 이름으로 변경
  • 객체생성을 빌더패턴으로 전환함으로서 중복코드 제거
  • 역할과 책임을 명확히 하기 위해 HTML파싱과 관련된 일부기능 분리
  • 각 액티비티에서 직접적으로 의존하고 있던 데이터공급객체를 Factory클래스를  사용해서 의존성 제거함
  • 여기저기 흩어져있던 파일관련기능을 한 곳으로 모아 응집도 높임

모든 내용을 발표하는데 1시간 반정도 걸릴 것으로 예상했지만, 의외로 1시간도 되지 않아

발표가 끝났다. 그리고, 짧게나마 발표내용에 대한 토론이 이어졌다.

  • 왜 set메서드를 쓰면 좋지 않은가? 꼭 써야될 경우도 있지 않을까?
  • Lombok을 쓴 도메인 클래스는 왜 Entity클래스가 아니고 DTO에 가까울까?
  • Entity라고 부를만한 도메인클래스의 조건은 무엇인가?

개인적으로, 토론이 너무 짧은시간 동안 진행되서 아쉬웠다.

뭔가 서로간의 피드백을 주고 받으면서 깊은 대화들이 오갔으면 하는 바램이었지만,

아쉬움을 뒤로 하고 차근차근 한걸음씩 내딛는 것도 의미있다고 생각했다.

범균님 블로그 자료 맨 마지막 장에 보면 리팩토링을 하고 싶어도 같이 할 선후배 동료가

없을 때, “흔쾌히 같이 해보자“라고 하는 반가운 광고가 실려있다.

생각있으신 분은 연락해서 한번 직접 리팩토링을 같이 해보는 것도 좋은 경험이 되리라

생각한다. 내가 이번에 리팩토링을 하면서 많이 느끼고 배운것처럼 말이다.

그렇게 해서 조직에 있는 한사람 한사람이 조금씩 변화하고 성장해서, 그 조직에

긍정적인 영향을 주고 함께 발전해 나가는 범균님의 바램이 꼭 현실로 이뤄졌으면

하는 바램이다! ^^

[프로세스/관리]개인프로젝트를 위한 개발환경 구축 – 1

전부터 개인적으로 만들어보고 싶은 프로그램들이 있었지만 차일피일 미루고 있던 것을

이제서야 “천리길도 한걸음부터..!“인것처럼 본격적으로 시작하기 전에

개발환경구축을 먼저 진행했다.

(1) IDE(Integrated Development Environment) 

통합개발환경은 STS(Spring Tool Suite) 3.2.0.RELEASE 버전을 사용했다.

스프링 프레임워크 기반의 웹 프로젝트로 진행할 것이기 때문에 프로젝트를 만들기 위해

[Spring Template Project => Spring MVC Project => Project name, 기본 패키지

세팅] 을 진행해 간단하게 웹기반의 MVC 프로젝트를 만들었다.

sample6 sample7 sample8

프로젝트가 생성되면 Maven이 자동으로 빌드도구로 선택된다.

난 이번에 Maven대신 Gradle을 사용할 것이기 때문에 .project 파일에 Maven관련

설정을 모두 제거했다. 그러면 프로젝트에서 Maven관련 아이콘이 사라진다.

sample12

(2) JDK/TOMCAT(Servlet/JSP container) 버전

사실 IDE선정에 앞서 JDK버전 명시를 해야 하는데 순서가 뒤바뀌었다.

JDK는 1.6(1.7을 공부하지 않아서인지 아직까진 딱히 필요성을 느끼지 못함),

Tomcat은 7.0.34를 사용했다. 그리고 STS에 Tomcat을 추가했다.

sample9

(3) 빌드도구 선정

작년에 오픈소스 프로젝트를 하면서, 이클립스를 사용할 수 없는 우분투OS를 사용하는

리눅스환경에서 빌드환경을 구축해놓지 않아 무척 고생한 기억이 있다.

같은 실수를 반복하지 않기 위해 이번엔 시작부터 빌드도구로 회사에서 사용하고 있는

Gradle 1.6버전을 사용했다.(압축파일을 풀고 시스템변수 Path에 Gradle bin경로를 입력함)

Gradle로 빌드뿐만 아니라 Test, 배포까지 모두 커버할 수 있다.

sample10

(4) build.gradle 기본설정하기

다음은 Gradle로 프로젝트를 빌드하기 위한 build.gradle파일에 기본설정을 추가했다.

우선, 단일 프로젝트로 프로젝트를 생성했기 때문에 멀티프로젝트 빌드의 경우는 제외하고

Gradle이 빌드 실행시 인식할 수 있게 프로젝트루트에 build.gradle 파일을 만들었다.

아래 목록은 프로젝트에 사용할 기술에 대한 의존성 설정에 대한 내용이다

  • Spring버전은 3.1.1.RELEASE
  • 프로젝트에서 데이터접근계층을 구현하기 위한 API로는 JPA(Java Persistence API)
  • JPA 구현체는 Hibernate
  • Spring Data를 사용해서 데이터접근계층의 구현에 대한 노력을 덜게끔 함
  • DataBase는 Mysql
  • DBCP(DataBase Connection Pooling)는 BoneCP 사용
  • 유닛테스트를 작성하기 위해 Junit과 Mockito 사용

사실, Gradle을 공부하는 중에 처음 빌드파일을 작성하면서 가장 고생한 것은

바로 “의존성(dependencies)“추가였다.

각 라이브러리가 의존해서 쓰고 있는 동일 라이브러리의 버전이 충돌하면서 계속

컴파일을 해가며 이 버전을 맞추는데 시간을 보냈다.

sample5

버전충돌을 모두 해결하고, 빌드파일 설정이 끝나면 eclipse 태스크를 실행시켜

이클립스에서 해당 프로젝트를 import 할 수 있게 한다.

sample4

(5) 간단한 유닛테스트 작성 : 트랜잭션 롤백 테스트

빌드파일 작성이 힘들긴 했지만, 예전에 WEB-INF/lib 폴더에 하나씩 jar파일을 넣어가며

세팅했을 때와 비교해서, 자동으로 의존성관리를 해주니 편하게 프로젝트 세팅을 마쳤다.

그럼 이제 세팅한 프로젝트가 잘 돌아가는지 간단하게 트랜잭션 롤백에 대해 간단한

유닛테스트를 작성했다.


package com.sameple.test.service;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;

import com.sample.test.domain.Member;
import com.sample.test.repository.MemberRepository;
import com.sample.test.service.MemberService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:test-root-context.xml")
public class MemberServiceTest {
    @Autowired
    private MemberRepository memberRepository;

    private MemberService service = new MemberService();

    private Member member = new Member();

    @Before
    public void setup() {
        member.setNo(1L);
        member.setUserId("bluepoet12");
        member.setPassword("test1234");
        member.setNickName("파란시인");

        ReflectionTestUtils.setField(service, "memberRepository", memberRepository);
    }

    @Transactional
    @Rollback(false)
    @Test
    public void modifyMember() {
        service.modifyMember(member);
    }
}

소스는 간단하다. Member 도메인 클래스에 대한 CRUD중 transaction이 들어가는

수정에 대한 테스트케이스를 추가해서 실행했다.

iBatis같은 Sql Mapping 프레임워크를 사용하였다면 저렇게 간단한 Member수정도

별도의 sql문을 작성하고 데이터접근계층에 대한 구현도 복잡했겠지만,

JPA와 Spring Data를 사용하고 나선 아래 한줄로 Member 수정이 끝난다.

 service.modifyMember(member);

더군다나, 데이터접근계층 구현을 위해 별도의 클래스가 필요하지 않고 오직 필요한건

JpaRepository를 상속한 MemberRepository 인터페이스를 정의해주는건만으로 충분하다.

런타임시 해당 인터페이스를 구현한 객체를 Spring Data가 동적으로 만들어주기 때문이다.

그리고 테스트시 @Transactional 어노테이션을 붙이면 테스트 실행시 자동으로

트랜잭션 관리를 해준다.

다만, 실제코드와 차이는 트랜잭션이 끝나는 시점에 자동으로 롤백된다는 점이다.

강제로 DB에 반영시키기 위해선 위의 예제코드처럼 @Rollback(false)를 넣어주면 된다.

sample11

(5) 마치며..

앞서 얘기한대로, 빌드파일의 의존성관리 부분을 제외하곤 비교적 순조롭게 세팅이

진행되었다.

앞으로 개인프로젝트를 진행하면서 쓰게될 포스팅은 아래 내용이 들어갈 것이다.

  • 추가될 다양한 라이브러리들, 그리고 그에 맞춰 수정될 빌드파일(의존성관리)
  • 의존성관리외 빌드와 배포에 필요한 Gradle Task 정의, 필요시 멀티프로젝트 고려
  • 서비스 설계시 작성한 UML과 지속적인 리팩토링, 이를 뒷받침하는 테스트코드 작성
  • 단, 테스트코드 작성은 위험성이 크고 꼭 체크가 필요한 지점에 한함
  • 개인서버에 배포시 필요한 배포관련툴(http://gant.codehaus.org/)
  • CI연동, 코드품질관리/테스트커버리지/단위테스트결과 리포팅

이제 내가 만들고 싶은 프로그램에 대한 어려운 첫 발자국을 내딛게 되었다^^

[프로세스/관리]코드품질관리를 위한 젠킨스+Sonar 연동과 유닛테스트결과 리포팅

사내 CI툴로 젠킨스를 쓰고 있던 중, 혹시 이걸로 전체 유닛테스트 결과도 볼 수 있을까

하는 의문으로 시작해 이것저것 찾아보던 중, 코드품질관리를 해주는 오픈소스 프로젝트인

Sonar를 알게 되었다.

처음으로 CI툴을 만져보는지라 이때부터 삽질은 시작되었다.

(1) 젠킨스 버전 업그레이드

1.4 초반버전대로 동작하고 있던 기존 젠킨스에선 Sonar 플러그인이 정상작동하지 않아

젠킨스를 최신버전(1.518)으로 업데이트 하는 작업부터 진행하였다.

업그레이드는 간단하다. Jenkins 관리메뉴에서 새 버전의 jenkins.war를 다운받아

톰켓 Deploy 경로에 풀어주면  된다. 기존 내용을 그대로 유지한채 버전업된다.

(2) Sonar 관련 설정하기

버전업을 하니 젠킨스에 Sonar 플러그인이 자동으로 설치되었다.

다음으로 프로젝트 분석을 담당할 Sonar 서버를 세팅할 차례이다.

젠킨스의 관리메뉴에서 Sonar 부분을 설정한다.

jenkins13

(3) 젠킨스에 분석할 프로젝트 설정하기

이제 젠킨스에서 분석할 프로젝트를 만들고, 설정에서 Build Environment의

Pre build script에서 빌드를 실행할 스크립트를 적는다.

그리고 Sonar 분석을 위해 Build의 Add build step에서 Invoke Standalone Sonar Analysis

를 선택해서 Project properties에 설정을 추가한다.

jenkins12


################# Project properties #############################
# required metadata
sonar.projectKey=test
sonar.projectName=test-web
sonar.projectVersion=1.0

# path to source directories (required)
sonar.sources=src/main/java

# path to test source directories (optional)
sonar.tests=src/test/java

# path to project binaries (optional), for example directory of Java bytecode
sonar.binaries=build/classes

# optional comma-separated list of paths to libraries. Only path to JAR file and path to directory of classes are supported.
#sonar.libraries=path/to/library.jar,path/to/classes/dir

# The value of the property must be the key of the language.
sonar.language=java

# Additional parameters
#sonar.my.property=value

(4) 프로젝트 소스 옮기기

이젠 분석할 프로젝트의 소스파일(원래자바파일+테스트코드+클래스파일)을

Project properties에 설정한대로, 젠킨스에서 생성한 해당 job의 workspace 밑에

아래와 같은 구조로 해당 위치에 복사해서 넣어둔다.

난, 이 작업을 배포스크립트를 수정해서 처리했다.

workspace/src/main/java/원래자바파일들

workspace/src/test/java/테스트코드들

workspace/build/classes/클래스파일들

(5) 분석할 프로젝트의 빌드 실행

분석할 프로젝트의 빌드와 Sonar분석관련한 설정을 모두 마쳤으니 빌드를

실행하는 일만 남았다.

빌드를 실행하면 빌드가 끝난 후,  Sonar가 해당 프로젝트를 분석하여

데이터를 DB에 저장하고, 설정한 Sonar서버에 그 결과를 예쁘게

뿌려준다.

Rules compliance(RCI)를 통해 해당 프로젝트가 얼마나 규칙을 지키고

있는지 백분율로 표시해준다. 

그 밖에, 상세화면으로 들어가면 Violations의 Severity를 다섯단계로 나누어

어떤 부분을 고치면 되는지 자세히 어드바이스 해준다(Blocker/Critical/Major/Minor/Info)

jenkins14

(6) 테스트커버리지 대신 유닛테스트 결과 리포팅 

원래 목적인 전체적인 유닛 테스트 결과를 보려고 했던 것이 너무 멀리 와버렸다.

이제 다시 본래 목적으로 돌아가서 유닛테스트 결과를 어떻게 볼까 방법을 찾던 중,

Gradle에서 Sonar Runner Plugin이라는 훌륭한 놈을 발견했다. 아싸!

젠킨스에서 테스트커버리지를 못 본 아쉬움을 뒤로하고 이 놈을 재빠르게 적용해보기로

했다. 일단 Gradle 빌드 스크립트에서 Sonar Runner 관련 설정을 추가한다.


apply plugin: 'sonar-runner'

sonarRunner {
sonarProperties {
property "sonar.host.url", "http://localhost:9000/"
property "sonar.jdbc.url", "jdbc:mysql://localhost:3306/sonar?          useUnicode=true&amp;characterEncoding=utf8"
property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
property "sonar.jdbc.username", "test"
property "sonar.jdbc.password", "test"
property "sonar.sourceEncoding", "UTF-8"
}
 }

이제 task 실행명령만 내리면 된다.

gradle sonarRunner

빌드가 완료되면 해당프로젝트 밑에 reports\tests 밑에 각각의 유닛테스트 결과와

함께 index.html에 전체적인 유닛테스트결과가 나온다.

정말 간단한 설정만으로, 전체적인 유닛테스트 결과를 볼 수 있게 되었다.

jenkins15

(7)  마치며..

“전체적인 유닛테스트결과를 보고싶다”라는 욕구(!)에서 시작해서 처음으로

CI툴인 젠킨스를 만지는 것에서부터 시작했지만, 역시나 쉽지는 않은 작업이었다.

젠킨스 버전업부터 시작해서 프로젝트 소스를 옮기기 위한 배치스크립트 수정작업,

그리고 테스트커버리지가 나오지 않아 낙담하고 있다가 우연히 Sonar Runner plugin을

보고 적용한 후 실행결과를 보며 희열을 느낀 것 등등..

Sonar의 분석결과도 여러모로 앞으로 지속적인 리팩토링을 통한 코드개선으로

최종적으로는 설계도 개선되는 효과를 얻는데 큰 몫을 할 것이지만, 그보다 더 좋은 것은

역시나 전체적인 유닛테스트 결과를 한 눈에 일목요연하게 볼 수 있게 되었다는 점이다.

꾸준하게 나쁜 냄새가 나는 기존 코드를 리팩토링하면서 이를 뒷받침해주는 테스트코드의

신뢰성은 백번 천번 강조해도 지나치지 않다고 생각한다.

이제, 더욱 강력해진 툴들을 가지고 기쁜 마음으로 리팩토링을 하고 테스트코드를 만들면서

다음 스텝에는 “테스트커버리지 리포팅“이라는 끝판왕에 한번 도전해 볼 생각이다.