주요글: 도커 시작하기
반응형
비즈니스 로직 분리를 위해 Drools 룰 엔진을 사용하는 방법을 간단하게 살펴본다.

룰 엔진의 필요성

프로그래밍은 로직을 구현하는 과정이라고 할 수 있다. 로그인 처리부터, 사용자 권한 인증 처리, 금융 관련 처리 등 모든 것이 로직과 관련된 것이다.

이런 로직 중에서는 거의 변하지 않는 것도 있지만, 매우 빈번하게 변경되는 것도 있다. 예를 들어, 보험을 생각해보자. 보험상품은 매년 다양하게 출시되고 이들 보험은 보험자의 나이에 따라, 그리고 보험자의 이력에 따라서 보험금액이 달라진다. 또한, 중간에 다양한 조건에 따라 보험료가 변경되는 경우도 있다.

로직이 변경되는 건 금융과 같은 분야만 그런 것이 아니다. 일반적인 웹 사이트도 수시로 로직이 변경된다. 예를 들어, 쇼핑 사이트를 생각해보자. 개학 시즌을 맞이하여 사이트 전면에 학생들을 위한 컴퓨터 제품을 배치했는데, 낮 시간대엔 오히려 일반 가전이 많이 나갔다고 해 보자. 원인을 파악해보니 혼수 준비를 하는 사람들이 낮시간대에 주로 주문을 한 것이었다. 그래서 이 쇼핑 사이트는 낮 시간대에는 혼수를 전면에 세우고 나머지 시간대에는 컴퓨터를 전면에 세우기로 했다.

두가지 상황을 살펴봤는데, 이 두가지 상황 모두 수시로 로직이 변경되는 경우이다. 이렇게 로직이 수시로 변경되는 경우 개발자들은 매번 프로그램을 수정해줘야만 한다. 아주 작은 조건의 변화라도 프로그램을 수정하고, 수정한 프로그램을 배포하는 작업을 해야만 한다.

이렇게 로직이 수시로 변경되는 경우에 사용할 수 있는 것이 바로 룰 엔진(Rule Engine)이다. 룰 엔진을 롤(Rule; 규칙, 즉 로직)을 별도로 저장해두고 프로그램에서 룰을 가져다 쓸 수 있도록 해 주는 기능을 제공한다. 프로그램에 있던 로직이 룰 엔진으로 옮겨가고, 프로그램에서는 롤 엔진을 통해서 룰(로직)을 실행하기만 하면 된다.

로직이 변경되는 경우에는 프로그램을 변경할 필요 없이 룰 엔진을 통해서 룰만 변경해주면 된다. 더 이상 프로그램을 수정하고 재배포하는 등의 작업이 필요없는 것이다. 따라서, 로직이 수시로 변경되는 곳에서는 룰 엔진을 적용함으로써 개발 시간을 단축시킬 수 있는 장점이 있다. 또한, 동일한 로직이 여러 코드에서 사용되는 경우에도 룰 엔진의 적용을 생각해볼 수 있다.

자바에서 사용가능한 룰 엔진에는 Jess, Drools, Jena 등 다양한 것들이 존재하는데, 본 글에서는 무료로 사용할 수 있는 Drools 룰 엔진의 사용방법에 대해서 살펴볼 것이다.

Drools 룰 엔진 빠르게 사용해보기

Drools 룰 엔진을 사용하는 절차는 다음과 같다.

  1. Drools 엔진 다운로드
  2. 룰 파일 작성
  3. 룰 사용하기
Drools 엔진 다운로드 및 설치

Drools 엔진은 다음의 사이트에서 다운로드 받을 수 있다.

이 글을 쓰는 시점에서 최신 버전은 2.5이다. 이 글에서는 Drools가 사용하는 라이브러리 모듈이 함께 포함된 배포 버전인 drools-2.5-final-bin-withdeps.zip 파일을 다운로드 받았다. 이 파일의 압축을 풀면 매우 많은 jar 파일이 풀리는 데, 이 중에서 다음의 파일을 파일을 클래스패스에 추가해주면 된다.

  • drools-all-jdk5-2.5-final.jar
  • antlr-2.7.5.jar
  • janino-2.3.15.jar
  • xml-apis-2.0.2.jar
  • xercesImpl-2.7.1.jar
클래스패스에 추가해주었다면 Drools 엔진을 사용할 준비가 끝난 것이다.

Drools 엔진은 자바의 룰 엔진 표준인 JSR-94를 지원하는데, JSR-94 API를 사용하는 경우 추가적으로 JSR 관련 jar 파일을 추가해주어야 한다. 또한, 자바 언어가 아닌 Python이나 Groovy로 룰 파일을 작성할 수가 있는데, 이 경우에도 약간 클래스패스의 설정이 달라진다. 이에 대한 배포파일이 포함된 README.txt 파일을 참고하기 바란다.
룰 파일 작성하기

Drools 룰 엔진을 설치한 다음에 할 작업은 룰 파일을 작성하는 것이다. 룰 파일은 비즈니스 로직을 담는 파일로서 Drools 룰 엔진을 사용하는 데 있어서 핵심 요소에 해당한다. 룰 파일의 요소에 대해서 살펴보기 전에 먼저 아주 간단한 룰 파일을 살펴보자.

    파일명: hello.drl    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <rule-set name="Hello"
              xmlns="http://drools.org/rules"
              xmlns:java="http://drools.org/semantics/java"
              xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
              xs:schemaLocation="http://drools.org/rules rules.xsd
                                 http://drools.org/semantics/java java.xsd">
        
        <import>java.lang.Object</import>
        <import>java.lang.String</import>
    
        <java:functions>
            public static void helloWorld(java.lang.String hello) {
                System.out.println(hello + " World");
            }
            public static void goodbyeWorld(String goodbye) {
                System.out.println(goodbye + " Cruel World");
            }
        </java:functions>    
        <rule name="Hello World">
            <parameter identifier="hello">
                <class>String</class>
            </parameter>            
            <java:condition>hello.equals("Hello")</java:condition>            
            <java:consequence>
                helloWorld( hello );
            </java:consequence>
        </rule>        
        <rule name="Goodbye Cruel World">
            <parameter identifier="goodbye">
                <class>String</class>
            </parameter>
            
            <java:condition>goodbye.equals("Goodbye")</java:condition>
    
            <java:consequence>
                goodbyeWorld( goodbye );
            </java:consequence>
        </rule>
    
    </rule-set>

룰 파일에서 각 태그는 다음과 같은 역할을 한다.

  • import
    자바의 import와 동일하게 룰 파일 내에서 사용할 클래스를 명시

  • java:functions
    룰 파일에서 사용될 함수. 자바의 메소드와 동일하다.

  • rule
    하나의 규칙을 표현. 룰을 실행할 때 필요한 파라미터, 실행 조건 그리고 조건 충족시 실행될 코드로 구성된다.
여기서 가장 중요한 태그는 rule 태그이다. rule 태그는 하나의 룰을 표현해주는 태그로서 다음의 3가지로 구성된다.

  • parameter
    룰을 실행하는 데 필요한 파라미터 값. identifier 속성을 사용해서 룰에서 사용할 파라미터의 이름을 지정한다.

  • java:condition
    룰을 실행하기 위한 조건

  • java:consequence
    조건이 충족될 경우 실행될 코드

예를 들어, 앞서 봤던 룰 파일에서 이름이 'Hello World'인 룰을 보자. 이 룰은 다음과 같이 실행된다.

  1. String 타입인 hello 파라미터를 입력받는다.
  2. 전달받은 hello 파라미터의 값이 "hello"인지 검사한다.
  3. 검사 결과가 true라면, helloWorld( hello )를 실행한다.
만약 조건이 맞지 않을 경우에는 롤이 실행되지 않는다.

룰 사용하기

앞서 작성한 룰 파일을 실제로 사용해보자. 룰 파일을 사용하는 자바 코드는 다음과 같다.

    파일명: HelloExample.java    
    package javacan.drools.test;
    
    import java.io.IOException;
    
    import org.drools.FactException;
    import org.drools.IntegrationException;
    import org.drools.RuleBase;
    import org.drools.WorkingMemory;
    import org.drools.io.RuleBaseLoader;
    import org.xml.sax.SAXException;
    
    public class HelloExample {
        public static void main(String[] args) 
        throws IntegrationException, SAXException, IOException, FactException {
            RuleBase ruleBase = RuleBaseLoader.loadFromUrl(
                    HelloExample.class.getResource("hello.drl") );
    
            WorkingMemory workingMemory = ruleBase.newWorkingMemory();
            workingMemory.assertObject("Hello");
            workingMemory.fireAllRules(); // 첫번째 실행
            
            workingMemory = ruleBase.newWorkingMemory();
            workingMemory.assertObject("Goodbye");
            workingMemory.fireAllRules(); // 두번째 실행
        }
    }

룰을 사용하는 코드는 크게 4가지 순서로 구성된다.

  1. 룰 파일로부터 RuleBase를 생성한다: RuleBaseLoader.loadFromUrl()
  2. RuleBase로부터 WokringMemory를 생성한다: ruleBase.newWokringMemory()
  3. WokringMemory에 파라미터로 사용될 객체를 추가한다: workingMemory.assertObject()
  4. 룰을 적용한다: workingMemory.fireAllRules()
WorkingMemory.fireAllRules()를 실행하면 알맞은 룰이 실행된다. HelloExample 클래스를 실행한 결과를 토대로 추가 설명을 하겠다. HelloExample 클래스의 실행 결과는 다음과 같다.

    Hello World
    Goodbye Cruel World

실행결과의 첫번째 줄인 "Hello World"는 첫번째 workingMemory.fireAllRules() 실행에서 출력된 결과이고, 두번째 줄인 "Goodbye Cruel World"는 두번째 workingMemory.fireAllRules() 실행에서 출력된 결과이다.

먼저 첫번째 실행 결과부터 분석해보자. 첫번째 실행 때에는 파라미터로 "Hello"가 전달됐다. 앞서 살펴봤던 룰 파일에는 두개의 룰이 있는데, 두 룰 부분만 다시 살펴보면 아래와 같다.

        <rule name="Hello World">
            <parameter identifier="hello">
                <class>String</class>
            </parameter>
            
            <java:condition>hello.equals("Hello")</java:condition>            
            <java:consequence>
                helloWorld( hello );
            </java:consequence>
        </rule>
        
        <rule name="Goodbye Cruel World">
            <parameter identifier="goodbye">
                <class>String</class>
            </parameter>
            
            <java:condition>goodbye.equals("Goodbye")</java:condition>    
            <java:consequence>
                goodbyeWorld( goodbye );
            </java:consequence>
        </rule>

"Hello World" 룰은 파라미터의 값이 "Hello"인 경우에 실행되고, "Goodbye Cruel World" 룰은 파라미터 값이 "Goodbye"인 경우에만 실행된다. 그런데, 첫번째 실행에서는 파라미터 값을 "Hello"로 지정했으므로(workingMemory.assertObject("Hello")) "Hello World" 룰만 실행되고, 다른 룰을 실행되지 않는다.

반대로 파라미터 값이 "goodbye"인 경우에는 "Goodbye Cruel World" 룰이 실행된다. 그래서 실행 결과가 위와 같은 것이다.

작업 메모리와 룰 적용

룰 엔진에는 전향 연결(Forward chaining) 방식과 후향 연결(Reverse Chaining) 방식이 있는데, Drools는 전향 연결 방식을 사용한다. 전향 연결은 귀납법적인 연결방식으로 다음과 같은 방식으로 룰을 적용한다.

    if 조건 
    then 룰 실행

즉, 조건을 비교한 다음에 조건을 충족할 때에만 룰을 실행하는 방식이다.

조건을 검사할 때에는 비교 대상이 될 정보가 필요한데, 이 정보가 저장된 장소를 작업 메모리(Working Memory)라고 부른다. 작업 메모리에 비교할 정보를 추가할 때에는 WorkingMemory.assertObject() 메소드를 사용한다. 앞서 살펴봤던 예제에서는 작업 메모리에 한개의 객체만 추가했지만 여러개의 객체를 추가할 수도 있다. 예를 들어 앞서 살펴봤던 HelloExample을 다음과 같이 변경할 수도 있다.

    workingMemory.assertObject(new Integer(3));
    workingMemory.assertObject("Hello");
    workingMemory.assertObject("안녕");
    workingMemory.assertObject("Goodbye");

이 경우 각 객체에 대해서 차례대로 룰을 검사해본다. 먼저 첫번째 정보는 Integer 객체인데 이 경우에는 어떤 룰도 적용되지 않는다. 왜냐면, hello.drl에 있는 모든 룰은 파라미터의 타입이 String 이기 때문에, Integer는 룰에 전달되지 않는다. "안녕" 값의 경우에는 두 룰에 모두 전달되지만 어떤 룰의 실행 조건도 만족하지 못하기 때문에, 역시 룰이 적용되지 않는다. "Hello" 값에 대해서는 "Hello World" 룰이 적용되며, "Goodbye" 값에 대해서는 "Goodbye Cruel World" 룰이 적용된다.

조건을 충족시키는 여러개의 룰이 존재할 수도 있다. 예를 들어, 아래의 룰 집합을 생각해보자.

    <rule name="Goodbye Cruel World">
        <parameter identifier="goodbye">
            <class>String</class>
        </parameter>
        
        <java:condition>goodbye.equals("Goodbye")</java:condition>

        <java:consequence>
            goodbyeWorld( goodbye );
        </java:consequence>
    </rule>

    <rule name="Debug">
        <parameter identifier="obj">
            <class>Object</class>
        </parameter>
        
        <java:consequence>
            System.out.println("객체 : "+obj);
        </java:consequence>
    </rule>

이때 다음과 같이 작업 메모리에 객체를 추가했다면,

    workingMemory.assertObject("Goodbye");

이 경우, "Goodbye Cruel World" 룰과 "Debug" 룰 모두 적용된다. ("Debug"룰은 조건 부분이 없으므로 파라미터 타입이 Object 이기만 하면 실행된다.) 이 때에는 정해진 규칙에 따라 룰의 적용 순서가 결정된다. 다음은 기본적인 룰의 적용 순서 규칙이다.

  • Salience: 룰의 실행 순서를 할당한 값. 큰 값이 먼저 호출
  • Recency: 얼마나 많이 룰이 사용되었는지의 여부
  • Complexity: 더욱 복잡한 값을 가진 룰이 먼저 호출
  • LoadOrder: 룰의 로딩 순서에 따라 호출
본 글에서는 간단한 예를 통해서 Salience와 LoadOrder에 대해 살펴보도록 하겠다.

상품 할인 적용 예

대형 할인마트 같은 곳을 가보면 할인 조건이 다양하게 제시되곤 한다. 예를 들어, 다음과 같은 할인 행사가 있었다고 해보자.

  • 전 상품 3개 이상 구매시 3% 할인
  • 초코파이 10박스와 빅파이 10박스를 함께 구매하면 둘다 추가로 2.5% 추가할인
이를 위한 로직 파일은 다음과 같이 작성할 수 있다.

    파일명: discountRate.drl    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <rule-set name="Hello"
              xmlns="http://drools.org/rules"
              xmlns:java="http://drools.org/semantics/java"
              xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
              xs:schemaLocation="http://drools.org/rules rules.xsd
                                 http://drools.org/semantics/java java.xsd">
        
        <import>java.lang.Object</import>
        <import>java.lang.String</import>
        <import>javacan.drools.test.Order</import>
    
        <rule name="Rule1 3개 초과 3% 할인">
            <parameter identifier="order">
                <class>Order</class>
            </parameter>
            
            <java:condition>order.getNumber() >= 3</java:condition>
            <java:condition>order.getDiscountRate() == 0.0</java:condition>
            
            <java:consequence>
                order.setDiscountRate(3.0);
                System.out.println(order.getName() + " 3% 할인됨");
            </java:consequence>
        </rule>
    
        <rule name="Rule2 초코파이 빅파이 동시 10박스 구매시 추가 2.5% 할인">
            <parameter identifier="order1">
                <class>Order</class>
            </parameter>
            <parameter identifier="order2">
                <class>Order</class>
            </parameter>
            
            <java:condition>order1.getName().equals("초코파이") &&
                order1.getNumber() == 10</java:condition>
            <java:condition>order2.getName().equals("빅파이") &&
                order2.getNumber() == 10</java:condition>
            
            <java:consequence>
                order1.setDiscountRate(order1.getDiscountRate() + 2.5);
                order2.setDiscountRate(order2.getDiscountRate() + 2.5);
                System.out.println(order1.getName() + " 2.5% 추가 할인됨");
                System.out.println(order2.getName() + " 2.5% 추가 할인됨");
            </java:consequence>
        </rule>
    </rule-set>

먼저 Rule1은 Order.getNumber()가 3 이상이고 Order.getDiscountRate()가 0.0인 경우에 적용된다. Rule2는 두개의 파라미터를 받는데, order1.getName()이 초코파이이고 order1.getNumber()가 10개이고 그리고 order2.getName()이 "빅파이"이고, order2.getNumber()가 10개인 경우에 실행된다.

Rule1이 적용되면 discountRate를 3.0 으로 지정한다. Rule2가 적용되면, 각 Order 객체의 discountRate 값을 2.5 증가시킨다.

실제로 이 룰이 올바르게 적용되는 지 확인하기 위해 다음과 같은 코드를 작성해보았다.

    파일명: BuyItemTest.java    
    package javacan.drools.test;
    
    import java.io.IOException;
    
    import org.drools.FactException;
    import org.drools.IntegrationException;
    import org.drools.RuleBase;
    import org.drools.WorkingMemory;
    import org.drools.io.RuleBaseLoader;
    import org.xml.sax.SAXException;
    
    public class BuyItemTest {
        public static void main(String[] args)
        throws IntegrationException, SAXException, IOException, FactException {
            Order order1 = new Order("초코파이", 10, 3000);
            Order order2 = new Order("수퍼타이", 5, 1000);
            Order order3 = new Order("빅파이", 10, 2000);
            Order order4 = new Order("콜라", 2, 800);
            
            RuleBase ruleBase = RuleBaseLoader.loadFromUrl(
                    BuyItemTest.class.getResource( "discountRate.drl" ) );
    
            WorkingMemory workingMemory = ruleBase.newWorkingMemory( );
            workingMemory.assertObject(order1);
            workingMemory.assertObject(order2);
            workingMemory.assertObject(order3);
            workingMemory.assertObject(order4);
            workingMemory.fireAllRules();
            
            System.out.println("\n\n할인 적용 결과---");
            System.out.println(order1.getName()+":"+order1.getNumber()+" - " + order1.getDiscountRate() +"% 할인");
            System.out.println(order2.getName()+":"+order2.getNumber()+" - " + order2.getDiscountRate() +"% 할인");
            System.out.println(order3.getName()+":"+order3.getNumber()+" - " + order3.getDiscountRate() +"% 할인");
            System.out.println(order4.getName()+":"+order4.getNumber()+" - " + order4.getDiscountRate() +"% 할인");
        }
    }

이 코드는 "초코파이"와 "빅파이"를 10개 사고, "수퍼타이"와 "콜라"를 각각 5개, 2개 사고 있다. 앞서 할인 조건에 의해 "수퍼타이"는 3% 할인되고, "초코파이"와 "빅파이"는 5.5% 할인되어야 한다. 실제로 BuyItemTest 클래스를 실행해보면 다음과 같이 룰이 올바르게 적용되는 것을 확인할 수 있다.

    초코파이 3% 할인됨
    수퍼타이 3% 할인됨
    빅파이 3% 할인됨
    초코파이 2.5% 추가 할인됨
    빅파이 2.5% 추가 할인됨
    
    
    할인 적용 결과---
    초코파이:10 - 5.5% 할인
    수퍼타이:5 - 3.0% 할인
    빅파이:10 - 5.5% 할인
    콜라:2 - 0.0% 할인

만약 Rule1과 Rule2의 순서를 변경하면 어떻게 될까? 그러면, 다음과 같은 결과가 출력된다.

    초코파이 3% 할인됨
    수퍼타이 3% 할인됨
    초코파이 2.5% 추가 할인됨
    빅파이 2.5% 추가 할인됨
    빅파이 3% 할인됨
    
    
    할인 적용 결과---
    초코파이:10 - 5.5% 할인
    수퍼타이:5 - 3.0% 할인
    빅파이:10 - 3.0% 할인
    콜라:2 - 0.0% 할인

"초코파이"는 올바르게 적용됐는데, "빅파이"는 원하는 결과가 출력되지 않은 것을 알 수 있다. 이는 작업 메모리에서 "빅파이"에 해당하는 Order 객체를 검사할 때 "Rule1"과 "Rule2"과 모두 적용되는데 LoadOrder 원칙에 따라 "Rule2"가 먼저 적용되고 "Rule1"이 나중에 적용됐기 때문에 발생한 결과이다. 이럴 때 규칙의 로딩 순서에 상관없이 Rule1을 먼저 적용하고 Rule2를 나중에 적용하고 싶다면 다음과 같이 salience 속성을 사용하면 된다.

    <rule name="Rule2 초코파이 빅파이 동시 10박스 구매시 추가 2.5% 할인" salience="-1">
        ...
    </rule>
    
    <rule name="Rule1 3개 초과 3% 할인">
        ...
    </rule>

그러면 "Rule2"가 "Rule1" 보다 이후에 실행되기 때문에 원하는 결과를 얻을 수 있다.

결론

본 글에서는 간단하게 Drools 엔진을 사용하는 방법을 살펴보았다. Drools 룰 엔진은 조건 비교와 룰의 실행 코드에서 자바 언어를 그대로 사용할 수 있기 때문에 자바에 익숙한 개발자라면 누구나 쉽게 사용할 수 있다는 장점을 갖고 있다. 게다가 자바 룰 엔진 표준인 Java Rule Engine API(JSR-94)를 지원하기 때문에 이후에 자바 표준 API로의 전환되 쉽다. 따라서, 룰 엔진이 필요한 경우, Drools는 상당히 매력적인 선택이 될 수 있다.

관련링크:

+ Recent posts