본문 바로가기
Spring/experience

SpringFramework 버그에 대한 불안감 떨쳐내기 Spock Test

by include_hoany 2025. 2. 19.

 
개발자로 첫 취업 후 고객들에게 실질적으로 서비스되는 프로젝트 첫 배포는 정말 인생에서 잊지 못할 기억으로 남을 것 같습니다. 그때 당시 주니어 개발자인 저에게 메인 서비스 리뉴얼을 맡기고 배포할 수 있는 기회는 정말 소중했던 경험으로 죽을 때까지 잊지 못할 기억이 될 것 같습니다.

잊지 못하는 기억에 이유는 물론 고객들이 실제로 사용하는 서비스를 배포하고 한다는 점도 있지만 버그가 발생할 것 같은 마음에 코드를 보고 또 봐도 마음 한편에 불안감 때문 이었던 것 같습니다.  

지금도 경력이 짧지만 이제는 웬만한 버그없이 안정적으로 서비스를 개발하는 것 같습니다. 이런 안정감을 어떻게 저는 찾게 되었냐면 짧지만 경험을 통해 방어로직을 잘 구성하는 것 도 있지만 중요 로직에 대한 테스트시나리오를 구성하고 테스트코드를 매번 작성하기 때문인 것 같습니다.

하지만 다들.. 시간적으로 여유롭지 않은 개발시간이 있을수도 있고 테스트 코드를 작성한다 해도 실제 서비스 개발 로직보다 테스트코드의 라인 수가 훨씬 많아지는 이유로 배보다 배꼽이 더 커져 버리기도 하기 때문에 항상 테스트코드는 큰 부담으로 느껴졌습니다.

저는 주로 Spring Framework를 기반으로한 백엔드 서비스를 개발해 왔기 때문에 JUnit이 막강하지만 어마어마한 테스트코드 작성법, 다양한 메서드들이 큰 부담으로 느껴졌었습니다.

그런 고민이 있던중 회사 동료분께서 Spock 테스트 프레임워크에 대해 말씀해 주셨고 직관성, 간결함 그리고 Java언어를 완벽하게 지원하지만 유연한 Groovy언어를 통해 작성하는 매력에 지금도 테스트코드를 작성할 때 Spock을 활용하고 마음에 평화를 찾아가고 있습니다.

너무나 많은 분들께서 Spock 테스트 프레임워크에 대한 철학과 방향성 JUnit과의 차이점들을 잘 설명해주셨기 때문에 해당 내용들은 간단하게 링크로 첨부하도록 하겠습니다.
 

Spock으로 테스트코드를 짜보자 | 우아한형제들 기술블로그

Spock으로 테스트코드를 작성한 경험을 공유합니다. 안녕하세요! 우아한형제들 배달의민족/배민라이더스 주문시스템 팀 정용준입니다. 여러분은 어떻게 테스트 코드를 작성하고 계신가요? 일정

techblog.woowahan.com

 
 

Spock 소개 및 튜토리얼

안녕하세요? 이번 시간엔 spock 에 대해 소개하는 시간을 가지려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와 세미

jojoldu.tistory.com

 
Spock에 대한 설명은 위 링크를 통해 요약하였고 그러면 제가 직접 코드를 통해 Spock을 활용하는 방법을 간단하게 보여드리겠습니다.

    PaymentService paymentService;

    PgPaymentWebclient pgPaymentWebclient = Mock();

    def setup() {
        paymentService = new PaymentService(pgPaymentWebclient);
    }
class PgPaymentResponseFixture {

    static PgPaymentResponseDto createSuccess() {
        return PgPaymentResponseDto.builder()
                .code("00")
                .msg("결제에 성공했습니다.")
                .build();
    }

    static PgPaymentResponseDto createExceededLimit() {
        return PgPaymentResponseDto.builder()
                .code("20")
                .msg("결제금액이 초과되었습니다.")
                .build();
    }
    
}

class PgPaymentRequestFixture {

     static PgPaymentRequestDto requestPgPayment(Long amount, Long fee) {
        return PgPaymentRequestDto.builder()
                .amount(amount)
                .fee(fee)
                .build();
    }

}

테스트를 진행할 객체를 할당하고 해당 객체가 의존하는 객체들을 모킹해 줍니다. 또한 테스트를 위해 필요한 Dto, Entity등은 Fixture로 구성해 줍니다. 테스트에 사용하는 Dto, Entity들을 케이스별로 구성하는 게 항상 테스트 코드를 작성할 때 많은 시간을 잡아먹는 요소인 것 같습니다.  NAVER에서 제공하는 Fixture Monkey를 활용하는 방법도 좋을것 같습니다.
 

test
├── groovy
│   ├── com.mymenu.payment.service
│   │   ├── PaymentServiceTests
│   ├── fixture
│   │   ├── PgPaymentRequestFixture
│   │   ├── PgPaymentResponseFixture

저는 위와같이 테스트 패키지를 구성하였습니다. 그럼 이제 해당 객체들을 활용해서 테스트를 진행해 보겠습니다.
 

    @Unroll
    def "결제타입: #selectPaymentType, 결제금액: #payAmount 일 때 수수료율은 #expected"() {

        given: "결제타입과 결제금액이 존재합니다"
        def paymentType = selectPaymentType
        def amount = payAmount;

        when: "결제타입과 결제금액확인하여 수수료율을 구합니다."
        def fee = paymentService.getPaymentByFeeRate(paymentType, amount)

        then: "수수료율이 예상값과 일치해야합니다."
        fee == expected

        where:
        selectPaymentType      | payAmount | expected
        PaymentType.CREDITCARD | 1L        | 0.4
        PaymentType.CREDITCARD | 29999L    | 0.4
        PaymentType.CREDITCARD | 30000L    | 0.3
        PaymentType.CASH       | 1L        | 0.2
        PaymentType.CASH       | 29999L    | 0.2
        PaymentType.CASH       | 30000L    | 0.1

    }
    
    // 결과값 로그
    PaymentServiceTest > 결제타입: CREDITCARD, 결제금액: 1 일 때 수수료율은 0.4 PASSED
    PaymentServiceTest > 결제타입: CREDITCARD, 결제금액: 29999 일 때 수수료율은 0.4 PASSED
    PaymentServiceTest > 결제타입: CREDITCARD, 결제금액: 30000 일 때 수수료율은 0.3 PASSED
    PaymentServiceTest > 결제타입: CASH, 결제금액: 1 일 때 수수료율은 0.2 PASSED
    PaymentServiceTest > 결제타입: CASH, 결제금액: 29999 일 때 수수료율은 0.2 PASSED
    PaymentServiceTest > 결제타입: CASH, 결제금액: 30000 일 때 수수료율은 0.1 PASSED
    @Unroll
    def "결제타입: #selectPaymentType, 결제금액: #payAmount 음수일때 예외가 발생합니다."() {

        given: "잘못된 결제 금액이 입력됩니다."
        def paymentType = selectPaymentType
        def amount = payAmount

        when: "결제 타입과 금액을 확인하여 수수료율을 구합니다.."
        paymentService.getPaymentByFeeRate(paymentType, amount)

        then: "음수 금액이 입력된다면 IllegalArgumentException이 발생합니다.."
        thrown(IllegalArgumentException)

        where:
        selectPaymentType      | payAmount
        PaymentType.CREDITCARD | -1L
        PaymentType.CASH       | -1L

    }
    
    // 결과값 로그
    PaymentServiceTest > 결제타입: CREDITCARD, 결제금액: -1 음수일때 예외가 발생합니다. PASSED
    PaymentServiceTest > 결제타입: CASH, 결제금액: -1 음수일때 예외가 발생합니다. PASSED

 
 
제가 가장 신경을 많이 쓰는 부분은 경계값 테스트 입니다. 개인적으로 경계값 테스트만 잘해도 웬만한 버그는 발생하지 않는 것 같습니다.
 

    @Unroll
    def "PG사에 결제를 요청하고 PgPaymentResponseDto의 코드값이 00이면 응답값을 리턴한다"() {

        given: "결제금액 10000원과 수수료 1000원에 해당하는 서비스를 선택합니다."
        PgPaymentRequestDto requestDto = PgPaymentRequestFixture.requestPgPayment(10000L, 1000L);

        pgPaymentWebclient.requestPayment(requestDto) >> PgPaymentResponseFixture.createSuccess();

        when: "결제를 요청합니다."
        def result = paymentService.requestPgPayment(requestDto);

        then: "정상적으로 결제에 성공하고 응답값이 그대로 리턴됩니다."
        result.code == "00";
        result.msg == "결제에 성공했습니다."

    }

    // 결과값 로그
    PaymentServiceTest > PG사에 결제를 요청하고 PgPaymentResponseDto의 코드값이 00이면 응답값을 리턴한다 PASSED
    @Unroll
    def "PG사에 결제를 요청하고 PgPaymentResponseDto의 코드값이 00이 아니면 예외가 발생한다"() {

        given: "결제금액 10000원과 수수료 1000원에 해당하는 서비스를 선택합니다."
        PgPaymentRequestDto requestDto = PgPaymentRequestFixture.requestPgPayment(10000L, 1000L)

        pgPaymentWebclient.requestPayment(requestDto) >> PgPaymentResponseFixture.createExceededLimit()

        when: "결제를 요청합니다."
        paymentService.requestPgPayment(requestDto)

        then: "결제에 실패한다면 예외가 발생합니다."
        thrown(IllegalArgumentException)

    }
    
    // 결과값 로그
    PaymentServiceTest > PG사에 결제를 요청하고 PgPaymentResponseDto의 코드값이 00이 아니면 예외가 발생한다 PASSED

그다음으로 많이 테스트하는 부분은 외부 API를 호출하고 해당 응답값에 대응하는 부분이 잘 분기처리되고 있는지 확인합니다. 위 코드는 간단하게 응답값에 code값으로 분기가 잘 발생하고 있는지만 간단하게 테스트했지만 http 프로토콜의 상태값별 200, 400, 500 등등 대응하여 잘 분기하는지 외부 API 응답값에 대응하여 우리 로직이 잘 분기하고 있는지를 가장 많이 테스트하는 것 같습니다.
 
요즘은 도메인별로 서버가 요청을 처리하는 MSA환경이 주를 이루다 보니 어떠한 이벤트가 발행되었을 때 구독하고 있는 우리 서버는 그러한 이벤트를 잘 처리하고 있는지 중복되는 이벤트 처리에 대해 우리 서버는 멱등성을 잘 지키게 로직이 설계되었는지 등등을 가장 많이 테스트하는 것 같습니다.
 
아직 저는 테스트 코드, Junit, Spock에 대해 완벽하게 이해했다고 생각하지 않습니다. 다만 Spock을 좀 더 선호하는 이유는 테스트코드가 간결하고 직관적이기 때문에 테스트코드 작성에 할애하는 시간도 줄어들어 테스트코드를 작성하는 부담감이 줄어들었다고 생각하기 때문입니다.
 
SpringFramework 또는 Java 개발환경에서 버그에 대한 불안감이 있으시다면 직관성있고 간결한 Spock Test Framework를 통해 떨쳐내 보시는 걸 추천드립니다. 그러면 행복한 코딩 하세요!
 
마지막으로 Jacoco, SonarQube를 통해 테스트 커버리지를 확인하면 더욱더 좋습니다!