Development/Code

Spring Boot에서 자신만의 Rule Engine (룰 엔진) 구현하기

Danny Seo 2024. 6. 27. 15:12

룰 엔진 (Rule Engine) 은 입력 데이터에 대해 규칙을 실행하고 조건이 맞으면 해당 작업을 실행하는 전문 시스템 프로그램입니다.

Rule Engine : Spring Boot로 구현하는 룰 엔진

소프트웨어 개발에서 데이터를 필터링하고 처리하기 위해 여러 규칙이나 조건을 적용해야 하는 상황이 자주 발생합니다. 전통적인 if와 else 조건문을 사용하여 이러한 규칙을 관리하면 복잡하고 유지 보수가 어려워질 수 있습니다.

 

룰 엔진 (Rule Engine)을 사용하면 이러한 규칙을 보다 유연하고 체계적으로 정의하고 실행할 수 있습니다.

 

이 글에서는 Java 함수형 프로그래밍 원칙을 사용하여 Spring Boot 프로젝트에서 간단한 룰 엔진 (Rule Engine)을 구축하는 방법을 살펴보겠습니다.

룰 엔진 (Rule Engine) 이란?

룰 엔진 (Rule Engine)은 여러 규칙을 관리하고 객체 집합에 이를 적용하는 역할을 합니다. 규칙에 따라 객체를 필터링하고 필터링된 결과를 반환합니다.

룰 엔진 (Rule Engine) 만들기 단계별 가이드

먼저, Spring Initializr를 사용하여 간단한 스프링 부트 애플리케이션을 생성합니다. 이 애플리케이션에서 사용자 목록을 반환하는 REST 엔드포인트를 만들겠습니다.

1. UserController.java 클래스 생성

package com.example.spring_boot_rule_engine_demo.controller;

import com.example.spring_boot_rule_engine_demo.model.User;
import com.example.spring_boot_rule_engine_demo.engine.RuleEngine;
import com.example.spring_boot_rule_engine_demo.rule.Rule;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping
    public List<User> getAllUser() {
        return userData();
    }

    private List<User> userData() {
        return List.of(
            new User("sumit", 33),
            new User("agam", 13),
            new User("mohit", 23),
            new User("kailash", 43)
        );
    }
}

Rule Engine 구현 예제 #1

여기서 나이가 30세 이상이고 이름이 'sum'으로 시작하는 사용자를 가져와야 한다고 생각해 봅시다.

이럴 경우, 두 가지 규칙을 만들고, 클라이언트에게 응답을 반환하기 전에 적용해야 합니다.

2. Rule (규칙) 정의

먼저 Rule이라는 enum을 사용하여 규칙을 정의하겠습니다.

enum의 각 규칙은 우리가 평가하고자 하는 특정 조건이나 기준을 나타냅니다.

package com.example.spring_boot_rule_engine_demo.rule;

import com.example.spring_boot_rule_engine_demo.model.User;
import java.util.function.Predicate;

public enum Rule implements TestRule {

    AGE_GREATER_THAN_30(user -> user.getAge() > 30),
    NAME_STARTS_WITH_SUM(user -> user.getName().startsWith("sum"));

    private final Predicate<User> predicate;

    Rule(Predicate<User> predicate) {
        this.predicate = predicate;
    }

    @Override
    public Predicate<User> getPredicate() {
        return predicate;
    }
}

3. TestRule 인터페이스 생성

구현 없이 메서드를 정의하기 위해 TestRule 인터페이스를 만들겠습니다.

package com.example.spring_boot_rule_engine_demo.rule;

import java.util.function.Predicate;

public interface TestRule {
    <T> Predicate<T> getPredicate();
}

4.  Rule Engine (룰 엔진) 클래스 생성

이 클래스는 규칙의 관리와 실행을 담당합니다. 규칙 목록을 유지하며, 새로운 규칙을 추가하고 이러한 규칙을 기반으로 객체를 필터링하는 메서드를 제공합니다.RuleEngine 클래스의 filter 메서드에서 객체 목록을 반복하며 Java 8 Stream API의 allMatch 메서드를 사용해 각 규칙을 적용합니다. 이 메서드는 주어진 객체가 모든 규칙을 통과하는지 확인합니다. 모든 규칙을 통과하면 해당 객체를 필터링된 목록에 추가합니다.

package com.example.spring_boot_rule_engine_demo.engine;

import com.example.spring_boot_rule_engine_demo.rule.TestRule;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class RuleEngine<T> {
    private final List<TestRule> rules;

    public RuleEngine() {
        this.rules = new ArrayList<>();
    }

    public void addRule(TestRule rule) {
        rules.add(rule);
    }

    public List<T> filter(List<T> items) {
        return items.stream()
                    .filter(item -> rules.stream()
                                         .map(TestRule::getPredicate)
                                         .allMatch(predicate -> predicate.test(item)))
                    .collect(Collectors.toList());
    }
}

 

filter메서드는 규칙 목록에 저장된 규칙을 기반으로 항목 목록을 필터링합니다:
1. 입력으로 T 타입의 항목 목록을 받습니다.
2. items.stream()을 사용해 항목 목록에서 스트림을 생성합니다.
3. filter 메서드는 각 항목에 대해 해당 항목이 모든 규칙을 만족하는지 확인합니다:
     rules.stream()은 규칙 목록에서 스트림을 생성합니다.
     map(TestRule::getPredicate)는 각 규칙의 조건을 가져옵니다.
     allMatch(predicate -> predicate.test(item))는 항목이 모든 규칙을 만족하는지 확인합니다.
4. 모든 규칙을 만족하는 항목은 필터링된 목록에 추가됩니다.
5. 마지막으로, collect(Collectors.toList())를 사용해 필터링된 항목을 새로운 목록으로 수집하여 반환합니다.

5. User 클래스 생성

package com.example.spring_boot_rule_engine_demo.model;

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 

6. Rule Engine (룰 엔진) 사용하기

이제 UserController.java 클래스에서 룰 엔진 (Rule Engine) 을 사용하여 필터링된 사용자 목록을 반환하도록 수정합니다.

먼저, 우리는 사용자 목록을 생성하고, AGE_GREATER_THAN_30과 NAME_STARTS_WITH_SUM 두 가지 규칙으로 RuleEngine을 초기화합니다. 그런 다음 Rule Engine 클래스의 filter 메서드를 호출하여 사용자 객체 목록을 전달합니다. Rule Engine 클래스는 각 사용자에게 규칙을 적용하고 필터링된 목록을 반환합니다.

 

마지막으로, 필터링된 목록을 반복하고 지정된 규칙을 충족하는 사용자 목록을 반환합니다.

package com.example.spring_boot_rule_engine_demo.controller;

import com.example.spring_boot_rule_engine_demo.engine.RuleEngine;
import com.example.spring_boot_rule_engine_demo.model.User;
import com.example.spring_boot_rule_engine_demo.rule.Rule;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping
    public List<User> getAllUser() {
        return getFilteredUsers();
    }

    private List<User> getFilteredUsers() {
        RuleEngine<User> ruleEngine = new RuleEngine<>();
        ruleEngine.addRule(Rule.AGE_GREATER_THAN_30);
        ruleEngine.addRule(Rule.NAME_STARTS_WITH_SUM);
        return ruleEngine.filter(userData());
    }

    private List<User> userData() {
        return List.of(
            new User("sumit", 33),
            new User("agam", 13),
            new User("mohit", 23),
            new User("kailash", 43)
        );
    }
}

 

이제 규칙을 적용한 후 필터링된 사용자 목록을 응답으로 받을 수 있습니다.

 

끝까지 읽어주셔서 정말 감사합니다.

 

혹시 질문이 있으시면 댓글달아주세요 ㅎㅎ !!