𝑺𝒕𝒖𝒅𝒚/디자인 패턴

[JAVA/Spring Boot] 위임(Delegate) 패턴 + 전략(Strategy) 패턴 조합으로 조건문이 필요없는 정수 사칙연산 계산기 만들기

📝 Delegate Pattern(위임 패턴)

  • (사전적 의미) 위임하다
  • 객체가 자신의 기능을 다른 객체에게 위임하여 기능을 실행하는 디자인 패턴
  • 객체 간의 결합도를 낮춰 유지보수성과 확장성을 높이는데 사용
  • 사용자가 기능에 직접 접근하거나 기능을 직접 구현하지 않고, 대리자에게 위임하여 구현

📝 Strategy Pattern(전략 패턴)

  • (사전적 의미) 전략
  • 알고리즘 전략을 캡슐화하고 런타임에 선택할 수 있게 하는 행위 디자인 패턴
  • 새로운 전략 추가 시 기존 코드 수정 없이 새로운 클래스만 추가하면 됨
  • 조건문(if/switch)을 제거할 수 있음

조건문으로 구현하면 생기는 문제

public long calculate(String operation, long op1, long op2) { 
    if ("ADD".equals(operation) return op1 + op2; 
    else if ("SUB".equals(operation) return op1 - op2; 
    else if ("MUL".equals(operation) return op1 * op2; 
    else if ("DIV".equals(operation) return op1 / op2; 

    // MOD를 추가하려면 else if문을 또 추가해야 함 

    else throw new IllegalArgumentException("지원하지 않는 연산입니다."); 
}
  • 예시 자체는 정수 사칙연산이라는 매우 간단한 조건이기 때문에 조건문을 추가하는게 큰 부담으로 느껴지지 않을 수도 있지만… 상황에 따라 각 조건문이 매우 길어질 수도 있고, 근본적으로 기존 코드를 직접 수정해야 한다는 문제가 있다. (OCP - 개방 폐쇄 원칙을 위배)

🫱🏻‍🫲🏻 두 패턴을 함께 사용하는 이유

위임 패턴과 전략 패턴의 역할이 다르기 때문이다.

  • Delegate(위임) 패턴 → 사용자(Service)와 실제 로직 사이의 결합도를 낮춤
  • Strategy(전략) 패턴 → 여러 알고리즘 중 적절한 것을 선택

두 패턴을 함께 사용하면 사용자(Service)는 구체적인 연산 로직을 전혀 모르는 상태로 대리자에게 위임하고, 대리자는 전략 패턴으로 적절한 알고리즘을 선택해 실행한다.

이렇게 하면 사용자와 로직 간 결합도가 낮으면서 새로운 연산을 추가할 때 새로운 클래스 하나만 추가하면 된다.

⌨️ 코드 구현

제약사항

  • 조건문(if/switch) 금지
  • 아래 enum 클래스 사용
public enum CalculateOperation {
    ADD,
    SUB,
    MUL,
    DIV,
    ;
}

전체 구조

역할 클래스 설명

역할 클래스 설명
사용자 CalculateService 기능을 직접 구현하지 않고 대리자에게 위임
대리자 Calculator 적절한 전략을 선택해 실행
기능 Add, Sub, Mul, Div 연산 캡슐화

사용자가 기능에 직접 접근하거나 기능을 직접 구현하지 않고 대리자에게 위임하여 정수 사칙연산 기능을 사용하는 구조이다.

대리자는 기능에서 적절한 전략(연산)을 선택해 실제 계산을 수행하고 결과를 반환한다.

사용자(CalculateService)

@Service
@RequiredArgsConstructor
public class CalculateService implements CalculateUseCase {

    private final Calculator calculator;

    @Override
    public long calculate(CalculateOperation operation, long op1, long op2) {
        return calculator.calculate(operation, op1, op2); // 위임
    }
}

// 인터페이스
public interface CalculateUseCase {

    long calculate(CalculateOperation operation, long op1, long op2);
}

사용자는 정수 사칙연산 기능에 직접 접근하거나 기능을 직접 구현하지 않고, calculator라는 대리자에게 위임하여 정수 사칙연산 기능을 이용

대리자(Calculator)

@Component
public class Calculator {

    private final Map<CalculateOperation, CalculateStrategy> strategyMap;

    public Calculator(List<CalculateStrategy> strategies) {

        this.strategyMap = new HashMap<>();

        for (CalculateStrategy strategy : strategies) {
            strategyMap.put(strategy.getOperator(), strategy);
        }
    }

    public long calculate(CalculateOperation operation, long op1, long op2) {
                // Map에서 적절한 전략 선택 후 실제 계산 수행
        CalculateStrategy strategy = strategyMap.get(operation);
        if (strategy == null) throw new IllegalArgumentException("지원하지 않는 연산입니다.");

        return strategy.calculate(op1, op2);
    }
}

대리자는 적절한 전략을 선택해 계산 결과를 반환

  • Map 자료구조를 활용해 O(1)로 전략을 선택하고 계산 수행
    • key enum
    • value 기능 객체

기능(Add, Sub, Mul, Div)

@Component
class Add implements CalculateStrategy {

    @Override
    public long calculate(long op1, long op2) {
        return op1 + op2;
    }

    @Override
    public CalculateOperation getOperator() {
        return CalculateOperation.ADD;
    }
}

@Component
class Sub implements CalculateStrategy {

    @Override
    public long calculate(long op1, long op2) {
        return op1 - op2;
    }

    @Override
    public CalculateOperation getOperator() {
        return CalculateOperation.SUB;
    }
}

@Component
class Mul implements CalculateStrategy {

    @Override
    public long calculate(long op1, long op2) {
        return op1 * op2;
    }

    @Override
    public CalculateOperation getOperator() {
        return CalculateOperation.MUL;
    }
}

@Component
class Div implements CalculateStrategy {

    @Override
    public long calculate(long op1, long op2) {

        if (op2 != 0) return op1 / op2;

        throw new ArithmeticException("0으로 나눌 수 없습니다.");
    }

    @Override
    public CalculateOperation getOperator() {
        return CalculateOperation.DIV;
    }
}

// 인터페이스
public interface CalculateStrategy {

    long calculate(long op1, long op2);
    CalculateOperation getOperator();
}

기능은 입력받은 두 가지 정수에 대해 각자의 역할을 수행

  • MOD 연산이 추가된다면?
    • enum에 MOD 추가
    • Mod 전략 클래스를 하나 생성
@Component
class Mod implements CalculateStrategy { 
    @Override public long calculate(long op1, long op2) { 
        if (op2 != 0) return op1 % op2; 

        throw new ArithmeticException("0으로 나눌 수 없습니다."); 
    } 

    @Override
    public CalculateOperation getOperator() { 
        return CalculateOperation.MOD; 
    } 
}

기존 코드는 전혀 수정하지 않고도 새로운 연산을 추가할 수 있음

🔗 참고 자료

 

Delegate Pattern(델리게이트 패턴, 위임 패턴)이란?

* 23.03.29 코드수정 Delegate 패턴이 '헤드퍼스트 디자인 패턴' 도서에 나와있지 않아서 정리해보고자 한다. 아무래도 Decorator 패턴 또는 strategy 패턴과 비슷하기 때문일까? 또한, 이 Delegate 패턴이 Deco

ssdragon.tistory.com

 

💠 전략(Strategy) 패턴 - 완벽 마스터하기

Strategy Pattern 전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수

inpa.tistory.com

 

<Spring> List, Map으로 빈 주입 받기

List, Map으로 빈 주입받기 List랑 Map으로 여러 개의 빈을 주입받을 수 있다. 아래 코드를 통해 어떻게 주입을 받는지 테스트하였다. 예제 우선 인터페이스 하나를 정의하고 하나의 인터페이스를

oniic.tistory.com

 

## COMMENTS
> SEARCH: