1. 소개
이 기사는 새로운 오픈 소스 Java 규칙 엔진인 Evette의 첫 번째 실습 개요를 제공합니다.
역사적으로 Evrete 는 Drools Rule Engine 의 경량 대안 으로 개발되었습니다 . Java 규칙 엔진 사양 을 완벽하게 준수하며 대량의 데이터를 처리하기 위해 몇 가지 조정 및 기능과 함께 고전적인 순방향 연결 RETE 알고리즘을 사용합니다.
Java 8 이상이 필요하고 의존성이 없으며 JSON 및 XML 개체에서 원활하게 작동하며 기능 인터페이스를 규칙의 조건 및 작업으로 허용합니다 .
대부분의 구성 요소는 서비스 Provider 인터페이스를 통해 확장 가능하며 이러한 SPI 구현 중 하나는 어노테이션이 달린 Java 클래스를 실행 가능한 규칙 세트로 전환합니다. 오늘도 한번 해보도록 하겠습니다.
2. 메이븐 의존성
Java 코드로 이동하기 전에 프로젝트의 pom.xml 에 evrete-core Maven 의존성을 선언해야 합니다 .
<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-core</artifactId>
<version>2.1.04</version>
</dependency>
3. 사용 사례 시나리오
서론을 덜 추상적으로 만들기 위해 우리가 작은 사업체를 운영하고 있고 오늘이 회계 연도의 끝이며 고객당 총 판매액을 계산하려고 한다고 가정해 보겠습니다.
도메인 데이터 모델에는 Customer 및 Invoice 라는 두 가지 간단한 클래스가 포함됩니다 .
public class Customer {
private double total = 0.0;
private final String name;
public Customer(String name) {
this.name = name;
}
public void addToTotal(double amount) {
this.total += amount;
}
// getters and setters
}
public class Invoice {
private final Customer customer;
private final double amount;
public Invoice(Customer customer, double amount) {
this.customer = customer;
this.amount = amount;
}
// getters and setters
}
참고로 엔진은 기본적 으로 Java 레코드 를 지원 하며 개발자가 랜덤의 클래스 속성을 기능적 인터페이스로 선언할 수 있도록 합니다 .
이 소개의 뒷부분에서 송장 및 고객 모음이 제공되며 논리는 데이터를 처리하기 위해 두 가지 규칙이 필요하다고 제안합니다.
- 첫 번째 규칙은 각 고객의 총 판매 가치를 지웁니다.
- 두 번째 규칙은 송장과 고객을 일치시키고 각 고객의 합계를 업데이트합니다.
다시 한 번 유동적 규칙 빌더 인터페이스와 어노테이션이 달린 Java 클래스로 이러한 규칙을 구현할 것입니다. 규칙 빌더 API부터 시작하겠습니다.
4. 규칙 빌더 API
규칙 빌더는 규칙에 대한 DSL(도메인별 언어)을 개발하기 위한 중심 빌딩 블록입니다. 개발자는 Excel 소스, 일반 텍스트 또는 규칙으로 변환해야 하는 기타 DSL 형식을 구문 분석할 때 이를 사용합니다.
그러나 우리의 경우에는 규칙을 개발자의 코드에 직접 삽입하는 기능에 주로 관심이 있습니다.
4.1. 규칙 집합 선언
규칙 빌더를 사용하면 유창한 인터페이스를 사용하여 두 가지 규칙을 선언할 수 있습니다.
KnowledgeService service = new KnowledgeService();
Knowledge knowledge = service
.newKnowledge()
.newRule("Clear total sales")
.forEach("$c", Customer.class)
.execute(ctx -> {
Customer c = ctx.get("$c");
c.setTotal(0.0);
})
.newRule("Compute totals")
.forEach(
"$c", Customer.class,
"$i", Invoice.class
)
.where("$i.customer == $c")
.execute(ctx -> {
Customer c = ctx.get("$c");
Invoice i = ctx.get("$i");
c.addToTotal(i.getAmount());
});
먼저 기본적으로 공유 실행기 서비스 인 KnowledgeService 인스턴스를 만들었습니다 . 일반적으로 애플리케이션당 하나의 KnowledgeService 인스턴스가 있어야 합니다 .
결과 Knowledge 인스턴스는 두 규칙의 미리 컴파일된 버전입니다. 정확성을 보장하고 코드를 더 빨리 시작하기 위해 일반적으로 소스를 컴파일하는 것과 같은 이유로 이 작업을 수행했습니다.
Drools 규칙 엔진에 익숙한 사용자 는 동일한 논리의 다음 DRL 버전과 의미상 동일한 규칙 선언을 찾을 수 있습니다.
rule "Clear total sales"
when
$c: Customer
then
$c.setTotal(0.0);
end
rule "Compute totals"
when
$c: Customer
$i: Invoice(customer == $c)
then
$c.addToTotal($i.getAmount());
end
4.2. 모의 테스트 데이터
우리는 3명의 고객과 100,000개의 인보이스에 대해 규칙 세트를 임의 금액으로 테스트하고 고객에게 무작위로 배포합니다.
List<Customer> customers = Arrays.asList(
new Customer("Customer A"),
new Customer("Customer B"),
new Customer("Customer C")
);
Random random = new Random();
Collection<Object> sessionData = new LinkedList<>(customers);
for (int i = 0; i < 100_000; i++) {
Customer randomCustomer = customers.get(random.nextInt(customers.size()));
Invoice invoice = new Invoice(randomCustomer, 100 * random.nextDouble());
sessionData.add(invoice);
}
이제 sessionData 변수에는 규칙 세션에 삽입할 Customer 및 Invoice 인스턴스가 혼합되어 있습니다.
4.3. 규칙 실행
이제 우리가 해야 할 일은 100,003개의 모든 객체(100,000개의 송장과 3명의 고객)를 새 세션 인스턴스에 공급하고 fire() 메서드를 호출하는 것입니다.
knowledge
.newStatelessSession()
.insert(sessionData)
.fire();
for(Customer c : customers) {
System.out.printf("%s:\t$%,.2f%n", c.getName(), c.getTotal());
}
마지막 줄은 각 고객에 대한 결과 판매량을 인쇄합니다.
Customer A: $1,664,730.73
Customer B: $1,666,508.11
Customer C: $1,672,685.10
5. 어노테이션이 달린 자바 규칙
이전 예제는 예상대로 작동하지만 규칙 엔진이 다음을 수행할 것으로 예상되는 사양을 준수하는 라이브러리를 만들지 않습니다.
- "비즈니스 또는 애플리케이션 로직을 외부화하여 선언적 프로그래밍을 촉진합니다."
- "규칙을 작성하기 위한 문서화된 파일 형식 또는 도구와 애플리케이션 외부의 규칙 실행 집합을 포함합니다."
간단히 말해, 규정 준수 규칙 엔진은 런타임 외부에서 작성된 규칙을 실행할 수 있어야 합니다.
Evrete 의 Annotated Java Rules 확장 모듈은 이 요구 사항을 해결합니다. 사실 이 모듈은 라이브러리의 핵심 API에만 의존하는 "쇼케이스" DSL입니다.
어떻게 작동하는지 봅시다.
5.1. 설치
어노테이션이 달린 Java 규칙은 Evrete의 서비스 제공자 인터페이스(SPI) 중 하나의 구현이며 추가 evrete-dsl-java Maven 의존성이 필요합니다.
<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-dsl-java</artifactId>
<version>2.1.04</version>
</dependency>
5.2. 규칙 집합 선언
어노테이션을 사용하여 동일한 규칙 세트를 만들어 보겠습니다. 클래스 및 번들된 jar보다 일반 Java 소스를 선택합니다.
public class SalesRuleset {
@Rule
public void rule1(Customer $c) {
$c.setTotal(0.0);
}
@Rule
@Where("$i.customer == $c")
public void rule2(Customer $c, Invoice $i) {
$c.addToTotal($i.getAmount());
}
}
이 소스 파일은 랜덤의 이름을 가질 수 있으며 Java 명명 규칙을 따를 필요가 없습니다. 엔진은 소스를 있는 그대로 즉석에서 컴파일하므로 다음을 확인해야 합니다.
- 소스 파일에는 필요한 모든 가져오기가 포함되어 있습니다.
- 타사 의존성 및 도메인 클래스는 엔진의 클래스 경로에 있습니다.
그런 다음 외부 위치에서 규칙 집합 정의를 읽도록 엔진에 지시합니다.
KnowledgeService service = new KnowledgeService();
URL rulesetUrl = new URL("ruleset.java"); // or file.toURI().toURL(), etc
Knowledge knowledge = service.newKnowledge(
"JAVA-SOURCE",
rulesetUrl
);
그리고 그게 다야. 코드의 나머지 부분이 그대로 유지된다면 랜덤의 판매량과 함께 동일한 세 명의 고객이 인쇄됩니다.
이 특정 예에 대한 몇 가지 참고 사항:
- 우리는 일반 Java( "JAVA-SOURCE" 인수)에서 규칙을 빌드하도록 선택하여 엔진이 메서드 인수에서 팩트 이름을 추론할 수 있도록 합니다.
- .class 또는 .jar 소스 를 선택했다면 메서드 인수에 @Fact 어노테이션 이 필요 했을 것입니다.
- 엔진은 메서드 이름별로 규칙을 자동으로 정렬했습니다. 이름을 바꾸면 재설정 규칙이 이전에 계산된 볼륨을 지웁니다. 결과적으로 판매량이 0이 될 것입니다.
5.3. 작동 방식
새 세션이 생성될 때마다 엔진은 이를 어노테이션이 달린 규칙 클래스의 새 인스턴스와 연결합니다. 기본적으로 이러한 클래스의 인스턴스를 세션 자체로 간주할 수 있습니다.
따라서 클래스 변수가 정의된 경우 규칙 메서드에 액세스할 수 있습니다.
조건 메서드를 정의하거나 새 필드를 메서드로 선언하면 해당 메서드도 클래스 변수에 액세스할 수 있습니다.
일반 Java 클래스와 마찬가지로 이러한 규칙 세트는 확장, 재사용 및 라이브러리로 압축될 수 있습니다.
5.4. 추가 기능
간단한 예는 소개에 적합하지만 많은 중요한 주제를 뒤에 둡니다. 어노테이션이 달린 Java 규칙의 경우 다음이 포함됩니다.
- 클래스 메서드로서의 조건
- 클래스 메서드로서의 임의 속성 선언
- 단계 수신기, 상속 모델 및 런타임 환경에 대한 액세스
- 그리고 무엇보다도 조건에서 작업 및 필드 정의에 이르기까지 전반적으로 클래스 필드 사용
6. 결론
이 기사에서는 새로운 Java 규칙 엔진을 간단히 테스트했습니다. 주요 내용은 다음과 같습니다.
- 바로 사용할 수 있는 DSL 솔루션 및 규칙 저장소를 제공하는 데 다른 엔진이 더 나을 수 있습니다.
- 대신 Evrete 는 개발자가 랜덤의 DSL을 구축할 수 있도록 설계되었습니다 .
- Java로 규칙을 작성하는 데 익숙한 사람들은 "어노테이션이 있는 Java 규칙" 패키지가 더 나은 옵션임을 알 수 있습니다.
이 기사에서 다루지 않았지만 라이브러리의 API에서 언급한 다른 기능을 언급할 가치가 있습니다.
- 랜덤의 팩트 속성 선언
- Java 조건자로서의 조건
- 즉석에서 규칙 조건 및 작업 변경
- 갈등 해결 기술
- 라이브 세션에 새 규칙 추가
- 라이브러리 확장성 인터페이스의 사용자 지정 구현
공식 문서는 https://www.evrete.org/docs/ 에 있습니다 .
코드 샘플 및 단위 테스트는 GitHub에서 사용할 수 있습니다 .