1. 개요

결정 구조는 모든 프로그래밍 언어의 중요한 부분입니다. 그러나 우리는 코드를 더 복잡하고 유지하기 어렵게 만드는 수많은 중첩된 if 문을 코딩하게 됩니다.

이 사용방법(예제)에서는 중첩된 if 문을 대체하는 다양한 방법을 살펴보겠습니다 .

코드를 단순화할 수 있는 다양한 옵션을 살펴보겠습니다.

2. 사례 연구

종종 우리는 많은 조건을 포함하고 각각 다른 처리가 필요한 비즈니스 로직을 접하게 됩니다. 데모를 위해 Calculator  클래스의 예를 들어 보겠습니다. 두 개의 숫자와 연산자를 입력으로 사용하고 작업에 따라 결과를 반환하는 메서드가 있습니다.

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;

    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}

switch 문을 사용하여 이를 구현할 수도 있습니다 .

public int calculateUsingSwitch(int a, int b, String operator) {
    switch (operator) {
    case "add":
        result = a + b;
        break;
    // other cases    
    }
    return result;
}

일반적인 개발에서 if 문은 본질적으로 훨씬 더 크고 복잡해질 수 있습니다 . 또한 복잡한 조건이 있는 경우에는 switch 문이 잘 맞지 않습니다 .

내포된 의사결정 구조의 또 다른 부작용은 관리할 수 없게 된다는 것입니다. 예를 들어 새 연산자를 추가해야 하는 경우 새 if 문을 추가하고 작업을 구현해야 합니다.

3. 리팩토링

위의 복잡한 if 문을 훨씬 간단하고 관리하기 쉬운 코드로 대체하는 대체 옵션을 살펴보겠습니다.

3.1. 팩토리 클래스

여러 번 우리는 각 분기에서 유사한 작업을 수행하는 의사 결정 구조를 접하게 됩니다. 이는 주어진 유형의 개체를 반환하고 구체적인 개체 동작에 따라 작업을 수행하는 팩터리 메서드를 추출할 수 있는 기회를 제공합니다 .

이 예제에서는 단일 적용 메서드 가 있는 작업 인터페이스를 정의해 보겠습니다 .

public interface Operation {
    int apply(int a, int b);
}

이 메서드는 두 개의 숫자를 입력으로 사용하고 결과를 반환합니다. 추가를 수행하기 위한 클래스를 정의해 보겠습니다.

public class Addition implements Operation {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
}

이제 주어진 연산자를 기반으로 Operation 인스턴스를 반환하는 팩토리 클래스를 구현할 것입니다 .

public class OperatorFactory {
    static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }

    public static Optional<Operation> getOperation(String operator) {
        return Optional.ofNullable(operationMap.get(operator));
    }
}

이제 Calculator 클래스에서 Factory을 쿼리하여 관련 작업을 가져오고 소스 번호에 적용할 수 있습니다.

public int calculateUsingFactory(int a, int b, String operator) {
    Operation targetOperation = OperatorFactory
      .getOperation(operator)
      .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
    return targetOperation.apply(a, b);
}

이 예에서 우리는 팩토리 클래스가 제공하는 느슨하게 결합된 객체에 책임이 어떻게 Delegation되는지 살펴보았습니다. 그러나 중첩된 if 문이 우리의 목적을 무효화하는 팩토리 클래스로 단순히 이동되는 경우가 있을 수 있습니다.

또는 빠른 조회를 위해 쿼리할 수 있는 의 개체 저장소를 유지할 수 있습니다 . 우리가 본 것처럼 OperatorFactory#operationMap은 우리의 목적에 부합합니다. 런타임 시 맵을 초기화 하고 조회하도록 구성 할 수도 있습니다 .

3.2. Enum 사용

Map 사용 외에도 Enum을 사용하여 특정 비즈니스 로직에 레이블을 지정할 수 있습니다 . 그런 다음 중첩된 if 문 이나 switch case 에서 사용할 수 있습니다 . 또는 개체의 Factory으로 사용하고 관련된 비즈니스 논리를 수행하도록 전략을 세울 수도 있습니다.

이렇게 하면 중첩된 if 문의 수도 줄어들고 개별 Enum 값에 책임이 Delegation됩니다.

우리가 어떻게 그것을 달성할 수 있는지 봅시다. 먼저 Enum을 정의해야 합니다 .

public enum Operator {
    ADD, MULTIPLY, SUBTRACT, DIVIDE
}

우리가 볼 수 있듯이 값은 계산에 더 사용될 다른 연산자의 레이블입니다. 우리는 항상 중첩된 if 문이나 스위치 케이스에서 값을 다른 조건으로 사용할 수 있는 옵션이 있지만 논리를 Enum 자체에 Delegation하는 다른 방법을 설계해 보겠습니다 .

Enum 값에 대한 메서드를 정의하고 계산을 수행합니다. 예를 들어:

ADD {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
},
// other operators

public abstract int apply(int a, int b);

그런 다음 Calculator 클래스에서 작업을 수행하는 메서드를 정의할 수 있습니다.

public int calculate(int a, int b, Operator operator) {
    return operator.apply(a, b);
}

이제 Operator#valueOf() 메서드를 사용하여 String 값을 Operator변환하여 메서드를 호출할 수 있습니다 .

@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
    assertEquals(7, result);
}

3.3. 명령 패턴

이전 논의에서 우리는 지정된 연산자에 대해 올바른 비즈니스 개체의 인스턴스를 반환하기 위해 팩토리 클래스를 사용하는 것을 보았습니다. 나중에 비즈니스 개체는 계산기에서 계산을 수행하는 데 사용 됩니다 .

입력에서 실행할 수 있는 명령을 수락하도록 Calculator#calculate 메서드를 설계할 수도 있습니다 . 이것은 중첩된 if 문을 대체하는 또 다른 방법입니다 .

먼저 Command 인터페이스를 정의합니다 .

public interface Command {
    Integer execute();
}

다음으로 AddCommand를 구현해 보겠습니다 .

public class AddCommand implements Command {
    // Instance variables

    public AddCommand(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer execute() {
        return a + b;
    }
}

마지막으로, Command를 수락하고 실행하는 Calculator 의 새로운 메서드를 소개합니다 .

public int calculate(Command command) {
    return command.execute();
}

다음으로 AddCommand 를 인스턴스화하여 계산을 호출 하고 Calculator#calculate 메서드 로 보낼 수 있습니다 .

@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(new AddCommand(3, 7));
    assertEquals(10, result);
}

3.4. 규칙 엔진

중첩된 if 문을 많이 작성하게 되면 각 조건은 올바른 논리를 처리하기 위해 평가해야 하는 비즈니스 규칙을 나타냅니다. 규칙 엔진은 기본 코드에서 이러한 복잡성을 제거합니다. RuleEngine은  규칙  평가 하고 입력을 기반으로 결과를 반환합니다.

Rules 집합을 통해 Expression 을 처리 하고 선택한 Rule 에서 결과를 반환하는 간단한 RuleEngine 을 설계하여 예제를 살펴보겠습니다 . 먼저 규칙 인터페이스를 정의합니다 .

public interface Rule {
    boolean evaluate(Expression expression);
    Result getResult();
}

둘째, RuleEngine 을 구현해 보겠습니다 .

public class RuleEngine {
    private static List<Rule> rules = new ArrayList<>();

    static {
        rules.add(new AddRule());
    }

    public Result process(Expression expression) {
        Rule rule = rules
          .stream()
          .filter(r -> r.evaluate(expression))
          .findFirst()
          .orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
        return rule.getResult();
    }
}

RuleEngineExpression 개체를 수락 하고 Result 를 반환합니다 . 이제 Expression 클래스를 연산자 가 적용된 두 Integer 개체 의 그룹으로 설계 해 보겠습니다 .

public class Expression {
    private Integer x;
    private Integer y;
    private Operator operator;        
}

마지막으로 ADD 작업이 지정된 경우에만 평가하는 사용자 지정 AddRule 클래스를 정의해 보겠습니다 .

public class AddRule implements Rule {
    @Override
    public boolean evaluate(Expression expression) {
        boolean evalResult = false;
        if (expression.getOperator() == Operator.ADD) {
            this.result = expression.getX() + expression.getY();
            evalResult = true;
        }
        return evalResult;
    }    
}

이제 Expression을 사용하여 RuleEngine을 호출합니다 .

@Test
public void whenNumbersGivenToRuleEngine_thenReturnCorrectResult() {
    Expression expression = new Expression(5, 5, Operator.ADD);
    RuleEngine engine = new RuleEngine();
    Result result = engine.process(expression);

    assertNotNull(result);
    assertEquals(10, result.getValue());
}

4. 결론

이 사용방법(예제)에서는 복잡한 코드를 단순화하는 다양한 옵션을 살펴보았습니다. 또한 효과적인 디자인 패턴을 사용하여 중첩된 if 문을 대체하는 방법도 배웠습니다.

항상 그렇듯이 GitHub 저장소 에서 전체 소스 코드를 찾을 수 있습니다 .

 

Generic footer banner