본문 바로가기
Book

[실전 자바 소프트웨어 개발] 5. 비즈니스 규칙 엔진

by kkkdh 2023. 9. 4.
728x90

이번 장에서는 TDD (Test Driven Development)를 주로 설명합니다.

 

따라서 실패하는 Test code를 먼저 작성하며, 이를 이용해 전체 프로그램의 구조를 파악한 뒤 세부 구현을 해서 test code가 성공하도록 하는 과정을 반복해서 프로젝트를 완성하는 순서를 따르고 있습니다.

 


비즈니스 규칙 엔진 프로젝트 요구 사항

전체 구성원들이 비즈니스 규칙을 원하는대로 추가하고 관리하기 위한 프로젝트이며, 다으모가 같은 기능을 제공하려 한다.

  • 팩트: 규칙이 확인할 수 있는 정보
  • 액션: 수행하려는 동작
  • 조건: 액션을 언제 발생시킬지 지정
  • 규칙: 실행하려는 비즈니스 규칙을 지정, 보통 팩트, 액션, 조건을 한 그룹으로 묶어 규칙으로 만듦.

 


테스트 주도 개발 (TDD)

TDD의 철학은 테스트 코드를 먼저 작성한 뒤, 이에 맞춰 코드를 구현하는 것으로

TDD의 장점은 다음과 같이 정리 가능하다.

  • 테스트를 따로 구현하므로 테스트에 대응하는 요구 사항을 한 개씩 구현할 때마다 필요한 요구 사항에 집중하고, 개선할 수 있다.
  • 코드를 올바르게 조직할 수 있다. 예를 들어 테스트 코드 구현 과정에서 필요한 인터페이스를 신중하게 검토할 수 있음
  • TDD 주기에 따라 요구 사항 구현을 반복하며, 테스트 스위트(suite)를 완성할 수 있어 요구 사항 만족에 대한 확인을 더 확실하게 할 수 있다.
  • 테스트를 통과하기 위한 코드를 구현하기 때문에, 불필요한 코드 작성이 줄어듦 (over-enginerring을 줄일 수 있다)

 

TDD 주기

 

이에 따라서 테스트 코드를 다음과 같이 작성한다. (테스트 코드에 포함된 클래스들의 모든 method는 UnsupportedActionException을 throw한다.)

 

class BusinessRuleEngineTest {

    @Test
    void shouldHaveNoRulesInitially(){
        //given
        BusinessRuleEngine businessRuleEngine = new BusinessRuleEngine();

        //then
        assertEquals(0, businessRuleEngine.count());
    }

    @Test
    void shouldAddTwoAction(){
        //given
        BusinessRuleEngine businessRuleEngine = new BusinessRuleEngine();

        //when
        businessRuleEngine.addAction(() -> {});
        businessRuleEngine.addAction(() -> {});

        //then
        assertEquals(2, businessRuleEngine.count());
    }
}

정상적으로 모든 테스트는 실패

 

이제 테스트 코드를 성공하기 위해 다음과 같이 구현한다.

addAction, count method 구현

다시 테스트 코드를 실행하면 성공한다. (BussinessRuleEngine 생성자 부분은 수정해야 함)

테스트 성공

 


모킹 (Mocking)

모킹(mocking)은 run()과 같이 반환 타입이 없는 메서드가 실행되었을 때, 이를 확인하는 기법이다. (모킹에 대한 자세한 내용은 6장에서 다룬다고 함)

 

BusinessRuleEngine 객체의 run method는 void를 반환하는 method로 모킹 없이는 동작을 검증하기 어렵다. 따라서 mockito라는 유명한 라이브러리를 이용해 모킹을 도입한다.

 

책에서는 library import 부분만 설명하고 있는데, 사용하려면 다음과 같이 의존성을 추가해줘야 한다.

testImplementation 'org.mockito:mockito-core:5.2.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'

버전은 상황에 맞게 설정할 것

 

mockito-junit-jupiter까지 추가해야, MockitoExtension import가 가능하며, @Mock annotation을 활용한 모의 객체 생성이 가능함에 유의하자.

 

@Mock annotation을 이용해 모의 객체 생성

다음과 같이 테스트 코드에서 action 객체를 mock 객체로 만들어주고 (Mock annotation 이용), execute method가 호출되었는지 검증할 수 있었다.

 

실패하는 테스트 코드를 먼저 작성

 

이제 이 테스트를 성공시키기 위해 run method를 다음과 같이 수정해서 테스트를 통과하도록 구현한다.

 

현재 전체 테스트 코드는 다음과 같다.

package com.study.chapter5;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class BusinessRuleEngineTest {

    @Mock
    Action mockAction;

    @Test
    void shouldHaveNoRulesInitially(){
        //given
        BusinessRuleEngine businessRuleEngine = new BusinessRuleEngine(new ArrayList<>());

        //then
        assertEquals(0, businessRuleEngine.count());
    }

    @Test
    void shouldAddTwoAction(){
        //given
        BusinessRuleEngine businessRuleEngine = new BusinessRuleEngine(new ArrayList<>());

        //when
        businessRuleEngine.addAction(() -> {});
        businessRuleEngine.addAction(() -> {});

        //then
        assertEquals(2, businessRuleEngine.count());
    }

    @Test
    void shouldExecuteOneAction(){
        //given
        BusinessRuleEngine businessRuleEngine = new BusinessRuleEngine(new ArrayList<>());

        //when
        businessRuleEngine.addAction(mockAction);
        businessRuleEngine.run();

        //then
        verify(mockAction).execute();
    }
}

 


조건 추가하기

현재의 비즈니스 규칙 엔진은 간단한 액션 선언 기능만을 제공한다.

 

실제 비즈니스 규칙 엔진은 특정 조건을 만족하면, 액션을 수행 가능하도록 설정할 수 있어야 하며, 이를 위해 책에서는 조건을 팩트(fact)라는 개념으로 대응시키며, 코드 상에서 Facts class로 이를 구현해서 관리한다.

 

 

실패하는 테스트 코드 우선 작성

Facts class의 틀만 만들고, 이번에도 TDD 방식을 따르기 위해 실패하는 테스트 코드를 위와 같이 먼저 작성한다.

 

실제로 engine의 run method를 실행하는 과정에서 Facts를 기반으로 실행되는 코드가 제대로 작성되어 있지 않으니 실패한다.

 

이에 따라서 다음과 같이 Facts를 기반으로 Business engine이 실행될 수 있도록 코드를 변경

Action interface 변경
facts 기반으로 run method를 실행하도록 변경
Facts class 작성, String key, String value 를 갖는 Map을 필드로 갖도록 세팅

 

이 다음으로 책에서는 지역변수 형식 추론에 대한 개념과 이를 통해 코드를 간결하게 만들 수 있는 방법 등을 설명하고 있는데, 이는 생략

 

businessEngine.addAction(facts -> {
    var forecastedAmount = 0.0;
    var dealStage = Stage.valueOf(facts.getFact("stage"));
    var amount = Double.parseDouble(facts.getFact("amount"));
    
    if(dealStage == Stage.LEAD) {
    	forecastedAmount = amount * 0.2;
    }
    else if(dealStage == Stage.EVALUATING) {
    	forecastedAmount = amount * 0.5;
    }
    else if(dealStage == Stage.INTERESTED) {
    	forecastedAmount = amount * 0.8;
    }
    else if(dealStage == Stage.CLOSED) {
    	forecastedAmount = amount;
    }
    
    facts.addFact("forecastedAmount", String.valueOf(forecastedAmount));
});

마찬가지로 enum class를 활용해 Action 익명 클래스를 구현해 위와 같이 조건을 추가하는 예시를 보여주고 있는데, 이 또한 간단하게 넘어간다.

 

 

구현된 익명 객체는 Stage라는 enum class의 상수 값에 따라서 다른 상태를 의미하게 되고, 이 상태는 거래 상태를 의미한다.

 

코드는 따라서 거래 상태에 따른 거래 성사 가능성을 기반으로 거래 예상치를 반환하는 Action을 익명 클래스로 구현한 것이라고 요약 가능

 

switch(dealStage) {
    case LEAD:
        forecastedAmount = amount * 0.2;
        break;
    case EVALUATING:
        forecastedAmount = amount * 0.5;
        break;
    case INTERESTED:
        forecastedAmount = amount * 0.8;
        break;
    case CLOSED:
        forecastedAmount = amount;
        break;
}

이 코드의 if, else if문을 switch로 바꾸면 위와 같이 간단하게 변경 가능

 

 

다음으로 비즈니스 규칙 엔진 사용자가 사용할 수 있는 액션과 조건을 검사할 수 있는 Inspector 도구를 개발한다. (Action list를 전달해서 주어진 조건, 즉 Facts를 충족하는 Action을 확인하는 역할을 수행하는 것으로 보임)

 

이때, 실제 액션을 수행하지 않고도 각 액션과 관련된 조건을 기록해야 하는데, 이를 위해서는 기존의 Action 대신 조건과 수행 코드를 분리해 ConditionalAction이라는 인터페이스를 추가 개발해야 한다.

 

따라서 다음과 같이 ConditionalAction interface 추가 작성

evaluate method를 이용해 조건을 평가한다.

 

Inspector, Report class 생성

Report는 Inspector에서 inspect method를 통해 ConditionalAction List를 조사한 결과를 담기 위한 class로, 별다른 구현은 없고 결과 확인을 간편하게 하기 위해 toString method를 overriding 해준 것이 유일한 특이 사항

 

 

Inspector 테스트를 위해 다음과 같은 테스트 코드를 작성..

Inspector test code

 

이때, 앞서 설계한 ConditionalAction interface는 다음과 같은 이유들로 ISP(인터페이스 분리 원칙)을 위배한다고 함.

  • JobTitleCondtion이라는 구현체를 만들어 사용하는 과정에서 perform() method 구현 코드는 비어있음
  • 이는 인터페이스가 필요 이상의 기능을 제공하고 있음을 의미
  • ISP는 어떤 클래스도 사용하지 않는 메서드에 의존성을 갖지 않음을 추구 (perform method가 사용하지 않고 있음에도 interface에 포함되고 있는 것은 옳지 않음)
  • SRP와 유사하게 들리나 ISP는 사용자의 입장에서 불필요한 기능이 제공되면 안됨을 의미하는 것에 집중된 개념
  • 따라서 Condtion, Action과 같이 개념을 분리해야 할 것으로 예상..

 


처음 책을 읽고 공부를 시작할 때에는 책을 따라 소프트웨어를 개발하며, 정리할 만한 부분을 기록하려고 했는데, 진도를 나갈수록 뭔가 코드가 실행 가능한 코드가 아니게 되고(완성하지 않고 개념 설명 후 그냥 넘어가는 느낌..?), 개념적 설명이 넓고 얉게 많아지면서 글을 쓰는게 어려워지고 있음을 느끼고 있습니다.

 

또한 전체 책의 개념을 완벽히 이해하고, 이를 적용한 완성된 소프트웨어를 매 장마다 개발하는 것에 어려움이 느껴져서 (아직 능력이 부족한 것 같습니다)

 

그러한 이유들로 이번 책은 5장까지만 정리를 하고, 이후 6, 7장은 개인 공부를 하며 코드를 이것저것 만져보는 쪽으로 마무리할 것 같습니다.

728x90

댓글