Published on

Software Testing 4 | 행위 간 계약을 조절하자.

Authors
  • avatar
    Name
    이건창
    Twitter

Introduction

소프트웨어 시스템은 복잡한 흐름 속에서 여러 하위 루틴을 연속적으로 호출하게 돼. 각 루틴 마다 행위가 정해져 있는데, 어떻게 제약할지, 얼만큼 제약을 가할지 고민 할 수 있어.

제약을 관리하는 방법을 어떻게 할 지 항상 고민 됐는데, 계약 테스트를 배우면서 어느정도 정리 할 수 있었어.

계약 설계

계약 설계를 고려해야 하는 이유

금액을 입력받아 로또를 구매한다. 라는 요구사항을 고려해볼게. 금액은 숫자 형태여야 하고 0 이상이며 실수여야 해. 그러면 우리는 Money 라는 금액에 숫자 형태인 0 이상의 실수를 전달하는 방법으로 제약 사항을 조절 할 수 있지.

import java.math.BigDecimal

class InputView {
    fun inputAmount(amount:String):BigDecimal{

        require(/*입력받은 문자열은 숫자 형식이어야 한다.*/condition){"required number"}
        require(/*입력받은 숫자는 0 이상의 실수여야 한다. */condition){"required integer over 0"}

        return BigDecimal(/* val = */ amount)
    }
}

위 처럼 작성하면 요구 사항 변경에 매우 취약하게 돼. 만약 금액에 소수점이 들어가게 된다면? 음수도 들어갈 수 있다면? 그럼 Money 를 의존하는 클래스는 모두 확인해야 하는 불상사가 일어나지.

우린 계약 설계를 활용해 행위 스스로 사전, 사후 조건을 판단할 수 있게 만들어 계약 변경에도 쉽게 대응 할 수 있는 코드를 만들 수 있어.

계약

계약은 클래스가 사전 조건으로 무엇을 요구하는지, 사후 조건으로 무엇을 제공하는지, 불변식은 무엇을 유지하는지 명확하게 설립하는 과정을 말 해. 간단하게 다음처럼 표현 할 수 있어.

그림 1

사전 조건과 사후 조건

  • 사전 조건 : 메서드가 제대로 동작하는지 검증한다.
  • 사후 조건 : 메서드가 산출물을 제대로 보장하는지 검증한다.

금액을 입력받아 로또를 구매한다. 라는 요구사항에서 일부를 가져와서 설명하면 다음과 같아.

문자를 금액으로 변경 할 때 사전 조건에서 숫자 형식인지 검증해야 해. 그렇지 않다면 정상적인 동작을 바랄 수 없어.

그림 2

그런 다음 사후 조건에서는 어떤 산출물을 보장해야 할지를 검증해야 해. 사전 조건만 진행한다면 다음처럼 결과를 보장 할 수 없게 돼.

그림 3

불변식

사전 사후 모두 같은 조건이 유지되어야 할 때는 불변식이라고 해. 쉽게 말하면 불변식은 데이터 구조의 생명 주기 전체에 해당하는 조건을 가지게 돼.

그럼 우리가 이야기 했던 금액을 입력받아 로또를 구매한다.에서 불변식은 금액이어야 해. 금액은 음수가 될 수 없기 때문이지.

다음처럼 Money 객체로 관리하게 되면 불변식을 간단하게 관리 할 수 있어.

import java.math.BigDecimal

class Money(val amount:BigDecimal) {
    init {
        require(/*금액은 0 이상의 실수여야 한다.*/condition) {"amount require positive"}
    }

    fun minus(money: Money): Money {
        return Money( money.amount - this.amount)
    }
}

조금 쉽게 정리해볼까? InputViewLottoShop에서 검증해야 할 금액 불변식을 다음처럼 하나의 객체로 구성 할 수 있어.

그림 4

그럼 일부 계약이 변경되더라도 불변식을 쉽게 유지 할 수가 있개 돼!

조건의 강도

사전, 사후 조건을 정의할 때 중요한 건 조건의 강도를 정하는 일이야.

  • 강한 조건 : 사전 조건을 위배하면 프로그램을 중지한다.
  • 약한 조건 : 사전 조건이 위배되어도 프로그램은 재개한다.

강한 조건을 사용할지 약한 조건을 사용할지는 개발하는 시스템에 따라 다르고 요구사항에 따라 달라져야 해. 강한 조건을 사용하게 되면 코드에서 발생하는 실수 범위를 줄여주지만 조건을 단언문으로 바꾸는 시간을 늘려 복잡해지는 단점이 있어. 아래와 같은 코드들이 늘어나게 되면서 관리 영역이 넓어지게 되지.

require(/*입력받은 문자열은 숫자 형식이어야 한다.*/condition){"required number"}
require(/*입력받은 숫자는 0 이상의 실수여야 한다. */condition){"required integer over 0"}

단언문

자바에서는 assert를 활용해 단언문을 작성 할 수 있어. assert 명령어의 장점은 JVM 매개 변수로 비활성화 해 사전 조건 검사를 제외 할 수 있어. 그럼 실행 성능이 더 빨라지게 될거야. 그러나 단점은 assert를 사용하면 세부적인 오류 사항을 전달할 수 없다는 거야. 이런 장단점을 잘 파악해서 활용하도록 하자.

계약 변경에는 리스코프 치환 원칙

사전 조건 강도가 너무 높은 경우 계약 변경에 매우 취약하게 돼. 이런 경우는 리스코프 치환 원칙을 활용하면 유연하게 대응 활 수 있어.

다음처럼 상위 클래스에 대해 사존 조건을 약화시키거나 덜 제한하면 변경된 클래스는 클라이언트와 맺은 계약을 깨뜨리지 않게 돼.

그림 5

하위 클래스의 의존 단계에서만 계약 강도를 높인다면 유연한 설계가 가능해.

유효성 검사와 계약은 다르다.

유효성 검사와 계약의 특징은 다음과 같아.

  • 유효성 검사 : 사용자 입력 중 불량 데이터나 유효하지 않은 데이터가 시스템에 침투하지 않도록 도와준다.
  • 계약 검사 : 클래스 간 의사소통이 문제 없도록 구성하는 걸 의미한다.

쉽게 예시를 들어 이야기 해볼게. 다음과 같은 요구사항을 분석 했을 때 검증해야 할 조건은 세 가지야.

  • 금액을 입력받아 로또를 구매한다.
    1. 숫자 형태의 문자열을 입력받는다.
    2. 0 이상이어야 한다.
    3. 실수여야 한다.

이 때, 유효성 검사와 계약 검사로 나누면 다음과 같이 나눌 수 있어.

  • 유효성 검사
    • 숫자 형태의 문자열을 입력받는다.
  • 계약 검사
    • 0 이상이어야 한다.
    • 실수여야 한다.

두 가지로 나누는 이유는 의미없는 검증이 반복되는 걸 막기 위해서야. 이미 숫자임을 검증한 녀석들은 앞으로 숫자인지 확인 할 필요가 없겠지. 이런 유효성 검사는 한 번만 이뤄지는 게 맞아.

마지막으로

계약 설계를 해야 하는 이유

하나의 기능은 여러 객체 들간의 행위들로 이뤄져. 객체들은 계약에 맞게 움직이게 되지. 계약은 사전 조건과 사후 조건으로 쉽게 관리 할 수 있어. 사전 조건에서 동작에 문제가 없는지 검증하게 되고, 사후 조건에서 결과가 유효한지 확인하게 돼. 그 중 사전 조건과 사후 조건이 동일한 경우 불변식으로 하나의 객체에 관리 할 수가 있어.

위와 같은 특징을 잘 활용하면 버그를 일찍 발견하거나 테스트 대상을 빠르게 파악 할 수 있는 장점이 있으니 잘 활용해보자.

강도 조절

우리는 조건의 강도를 조절해야해. 조건 강도를 낮춰 동작을 유연하게 해서 사용자의 불편함을 없앨 수 있고, 조건 강도를 높여 사용자가 가진 재산을 보호 할 수 있어.

어떻게 조절할지는 상황에 맞게 잘 판단해보자.

의미없는 검증을 반복하지 말자.

검증 코드를 크게 유효성 검사, 사전 검사, 사후 검사로 나뉘게 돼. 사전 검사와 사후 검사는 행위 간 자연스러운 흐름을 위해 지속적으로 검증해야 하지만 유효성 검사는 입력받는 순간만 검사하면 돼.

의미 없는 검증을 반복하게 된다면 실행 시간도 길어지고 관리 영역도 늘어나게 되니 주의하자.