본문 바로가기
Java

[OOP] 사칙연산 프로그램에서 객체 지향 적용

by 배준오 2023. 11. 21.
반응형

객체 지향 프로그래밍

적절한 객체에 적절한 책임을 부여하여 서로 메세지를 주고 받으며 협력하게 하는 것

[객체 지향에서의 중요한 2가지 포인트]

1. 클래스가 아닌 객체에 초점을 맞추는 것

2. 객체들에게 적절한 역할과 책임을 할당하는 것

 

절차지향 VS 객체지향

객체 지향 : 책임이 여러 객체에 분산되어 있는 방식

절차 지향 : 책임 한 곳에 집중되어 있는 방식

 

요구사항

- 사칙 연산을 할 수 있다.

- 양수로만 계산할 수 있다.

- 나눗셈에서 0을 나누는 경우 IllegalArgument 예외를 발생시킨다.

- MVC 패턴 기반으로 구현한다.

 

문제 해결 과정

구현하기에 앞서 사칙 연산 프로그램의 핵심 기능을 먼저 정의하고 해당 기능에 대한 테스트를 생각해보자. 사칙 연산의 핵심 기능은 입력 받은 두 수와 연산자에 대해 적절한 연산 결과 값을 반환하는것임을 알수있다.

그에 따른 테스트는 다음과 같을 것이다.

carculatorTest -> 두 수가 주어졌을 때 연산의 결과가 핵심 기능 메서드인 carculator의 결과 값과 같아야 한다.

 

이제 사칙 연산 메서드를 작성 해보자.

public class Calculator {
    public static int calculate(int operand1, String operator, int operand2) {
        if ("+".equals(operator)) {
            return operand1 + operand2;
        }
        if ("-".equals(operator)) {
            return operand1 - operand2;
        }
        if ("*".equals(operator)) {
            return operand1 * operand2;
        }
        if ("/".equals(operator)) {
            return operand1 / operand2;
        }
        return 0;
    }
}

 

두 수와 연산자를 입력 값으로 받아 단순하게 if 조건 절로 각 연산자에 대한 결과 값을 반환하는 메서드이다.

위 코드의 문제점을 무엇일까

당연 테스트는 통과하겠지만, 해당 메서드는 입력 받은 연산자를 조건식으로 판단하고, 그에 맞게 연산까지 하는 책임이 있다. 따라서 한 곳에 집중되어 있는 책임을 분리할 필요성이 매우 느껴진다.

 

위 메서드의 조건식과 연산에 대한 책임을 다른 객체에게 맡기고 calculate 메서드는 결과 값만 반환할 수 있도록 분리시키면 좋을 것 같다. 객체 지향스럽게 리팩토링 해보자.

 

사칙 연산에서 연산자는 연산이라는 공통적인 특징이 있으므로 Enum을 사용해서 정의한다.

public enum ArithmeticOperator {
    ADDITION("+") {
        @Override
        public int arithmeticCalculate(int operand1, int operand2) {
            return operand1 + operand2;
        }
    }, SUBTRACTION("-") {
        @Override
        public int arithmeticCalculate(int operand1, int operand2) {
            return operand1 - operand2;
        }
    }, MULTIPLICATION("*") {
        @Override
        public int arithmeticCalculate(int operand1, int operand2) {
            return operand1 * operand2;
        }
    }, DIVISION("/") {
        @Override
        public int arithmeticCalculate(int operand1, int operand2) {
            return operand1 / operand2;
        }
    };

    private final String operator;

    ArithmeticOperator(String operator) {
        this.operator = operator;
    }
    
    public abstract int arithmeticCalculate(final int operand1, final int operand2);

    public static int calculate(int operand1, String operator, int operand2) {
        ArithmeticOperator arithmeticOperator = Arrays.stream(values())
                .filter(v -> v.operator.equals(operator))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("올바른 사칙연산이 아닙니다"));

        return arithmeticOperator.arithmeticCalculate(operand1, operand2);
    }
}

 

위의 Enum 클래스에 대해서 간단하게 설명하자면

외부에서 calculate 메서드를 호출 했을 때 입력받은 operator(연산자)과 처음으로 일치하는 Enum 상수를 Stream을 통해 찾아 arithmeticOperator에 할당한다. 일치하는 Enum 상수가 없으면 에러를 발생시킨다.

그리고 알맞은 연산 메서드를 호출한다.

 

여기서 각 Enum 요소에 대해 논리 연산식을 사용하기 위해 arithmeticCalculate 라는 추상 메서드를 정의하고, 각 Enum 요소에 추상 메서드를 오버라이드하여 구현했다. Enum 에서 추상 메서드를 사용하면 이처럼 다형성을 구사할 수 있다.

(Java 8이상 에서는 Lambda식을 사용하여 더 간결하게 구현할 수 있다.) Enum 에서 lambda식의 적용은 다음 블로그를 통해 확인할 수 있다.

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

 

이에 대한 테스트 코드는 다음과 같이 작성할 수있다.

@ParameterizedTest
@MethodSource("formulaAndResult")
    void calculateTest(int operand1, String operator, int opoerand2, int result) {
        int calculateResult = Calculator.calculate(operand1, operator, opoerand2);

        assertThat(calculateResult).isEqualTo(result);
    }

    private static Stream<Arguments> formulaAndResult() {
        return Stream.of(
                Arguments.arguments(1, "+", 2, 3),
                Arguments.arguments(1, "-", 2, -1),
                Arguments.arguments(4, "*", 2, 8),
                Arguments.arguments(4, "/", 2, 2)
        );
    }

 

여기까지 핵심 기능을 리팩토링 하고 테스트 코드를 작성 해보았다. 사칙 연산에 대한 검증을 완료한 상태인 것이다.

다음으로 나머지 요구사항을 살펴보자

- 양수로만 계산할 수 있다.

- 나눗셈에서 0을 나누는 경우 IllegalArgument 예외를 발생시킨다.

 

해당 요구사항에 대한 책임은 어떤 객체에게 부여해야 하는 것일까

Enum 클래스에서 적용할 수도 있겠지만 Interface 사용하면 더 적절하게 책임을 부여할 수 있을 것 같다.

Interface를 통해 다시 설계해보자.

 

먼저 Interface를 통해 추상화 시킬 것에 대해 생각해보자

우리는 사칙연산 프로그래밍을 구현 하는 것이 목적이고, 사칙 연산을 하기 위해서는 연산자에 대한 판단과 계산이라는 기능이 있어야 할 것이다. 해당 기능들은 변하기 쉽지 않은 부분이므로 이를 Interface를 통해 추상화 시키는게 적절할 것이다라고 판단 할 수있다.

 

Interface를 정의해보자

public interface NewArithmeticOperator {
    boolean supports(String operator);
    int calculate(int operand1, int operand2);
}

 

Interface에서 정의된 각 메서드는 다음과 같은 역할로 정의한다.

supports : 연산자가 일치하는지 판단.

calculate : 연산자로 계산

 

이제 해당 인터페이스를 상속받을 연산자에 대한 클래스를 만들어보자.

나머지에 대한 클래스만 살펴보자. 다른 연산 클래스들도 똑같이 구현 할 수 있다.

public class DivisionOperator implements NewArithmeticOperator {
    @Override
    public boolean supports(String operator) {
        return "/".equals(operator);
    }

    @Override
    public int calculate(int operand1, int operand2) {
        return operand1 / operand2;
    }
}

 

이제 남은 요구사항에 대한 예외 처리만 해주면 끝이 난다.

여기서 calculate 메서드의 매개변수로 0 보다 큰 수가 오게끔 만들어 주면 될 것 같다.

 

해당 요구사항의 책임을 부여할 객체를 만들기 위해 클래스를 만들자.

public class PositiveNumber {
    private final int value;

    public PositiveNumber(int value) {
        validate(value);
        this.value = value;
    }

    private void validate(int value) {
        if (isNegativeNumber(value)) {
            throw new IllegalArgumentException("0또는 음수를 전달할 수 없습니다.");
        }
    }

    private boolean isNegativeNumber(int value) {
        return value <= 0;
    }

    public int toInt() {
        return value;
    }
}

 

PositiveNumber 클래스에서 연산식에 필요한 value를 생성한다.

PositiveNumber 클래스로 생성한 변수를 calculate 메서드에 매개변수로 사용하면 항상 0보다 큰 숫자만 오게 된다.

 

다음과 같이 사용하면 된다.

@Override
    public int calculate(PositiveNumber operand1, PositiveNumber operand2) {
        return operand1.toInt() * operand2.toInt();
    }

calculate 에서 조건을 따질 필요가 없으니 적절하게 책임을 분리한 것이라고 볼 수 있다.

 

인터페이스를 상속받은 클래스로 사칙연산 기능을 분리 시켰으니 실제 계산식에 적용해주면 끝이다.

public class Calculator {
    private static final List<NewArithmeticOperator> arithmeticOperators = List.of(new AdditionOperator(), new SubtractionOperator(), new MultiplicationOperator(), new DivisionOperator());

    public static int calculate(PositiveNumber operand1, String operator, PositiveNumber operand2) {
        return arithmeticOperators.stream()
                .filter(arithmeticOperators -> arithmeticOperators.supports(operator))
                .map(arithmeticOperators -> arithmeticOperators.calculate(operand1, operand2))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("올바른 사칙연산이 아닙니다."));
    }
}

 

테스트 또한 다음과 같이 작성할 수있다.

@ParameterizedTest
    @MethodSource("formulaAndResult")
    void calculateTest(int operand1, String operator, int operand2, int result) {
        int calculateResult = Calculator.calculate(new PositiveNumber(operand1), operator, new PositiveNumber(operand2));

        assertThat(calculateResult).isEqualTo(result);
    }

    private static Stream<Arguments> formulaAndResult() {
        return Stream.of(
                Arguments.arguments(1, "+", 2, 3),
                Arguments.arguments(1, "-", 2, -1),
                Arguments.arguments(4, "*", 2, 8),
                Arguments.arguments(4, "/", 2, 2)
        );
    }

 

이상

반응형

'Java' 카테고리의 다른 글

[JAVA] 문자열에 문자 곱하기 및 문자열 뒤집기 연산  (0) 2023.12.05