본문 바로가기
Programming/Java, Spring

[Java ] 객체 지향에 대한 개념, 계산기 코드 OOP 리팩토링

by Renechoi 2022. 10. 23.

[Java ] by 홍종완님, 패스트캠퍼스 나만의 MVC 프레임워크 만들기 

 


 

정확한 답이 존재한다기 보다는 본인의 견해를 갖고 이를 풀어낼 수 있는가에 주목. 

 

 

객체지향의 4가지 특징 

1. 추상화 = Abstraction  

2. 다형성 = Polymorphism

3. 캡슐화 = Encapsulation 

4. 상속 = Inheritance 

 

 

추상화란 흔히 말하는 일반화, 단순화의 개념. 불필요한 개념을 제거함으로써 필요한 핵심만 나타낸 것. 복잡성을 낮추기 위한 도구. 

 

다형성은 다양한 형태를 가진 것이라고 할 수 있는데, 하나의 타입으로 여러 객체의 종류를 참조하는 것. 

 

캡슐화란 객체 내부에 세부사항을 외부사항으로부터 감추는 것. 목적은 인터페이스만 공개함으로써 변경하기 쉬운 코드를 만드는 것. 

 

상속은 부모로부터 물려받은 것. 

 

 

객체지향의 5가지 설계 원칙 (SOLID)

1. SRP : Single Responsibility Principle (단일 책임의 원칙)  

2. OCP : Open/Closed Principle (개방 폐쇄의 원칙) 

3. LSP : Liskov's Substitution Principle (리스코프 치환의 원칙) 

4. ISP : Interface Segragation Pirnciple (인터페이스 분리의 원칙) 

5. DIP : Depndency Inversion Principle (의존성 역전의 원칙) 

 

 

단일 책임의 원칙 : 하나의 책임을 가진다 

OCP : 기존 코드를 변경하지 않고 기능을 추가할 수 있어야 한다. 

리스코프 치환의 원칙 : 상위 타입의 객체를 하위 타입의 객체로 치환해도 동작에 문제가 없어야 한다. 

ISP : 클라이언트에게 필요한 인터페이스만 구현할 수 있어야 한다. 사용하지 않는 기능에 의존하지 않는다. 

DIP : 의존 관계를 맺을 때 자주 변경되는 쪽이 아니라 변경이 거의 일어나지 않는 곳에 의존해야 한다. 변화의 영향을 받지 않도록 하기 위함. 

 

 

 

 

객체지향 패러다임 

- 적절한 객체에게 적절한 책임을 할당하여 서로 메시지를 주고 받으며 협력하도록 하는 것 

- 점점 증가하는 SW 복잡도를 낮추기 위해 객체지향 패러다임 대두

- 개인적으로 생각하는 두가지 포인트 

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

2) 객체들에게 얼마나 적절한 역할과 책임을 할당하는지 

 

 

 

절차지향 VS 객체지향 

- 차이는 책임이 한 곳에 집중되어 있는지, 분산되어 있는지에 따라 

 

 

High cohesion, loose coupling 

- 높은 응집도, 낮은 결합도 

- 변경이 필요할 때 변경의 포인트가 집중 되어 원할한 수정을 할 수 있다. 

 

 

 

 

1. 도메인을 구성하는 객체에는 어떤 것들이 있는지 고민

2. 객체들 간의 관계를 고민

3. 동적인 객체를 정적인 타입으로 추상화해서 도메인 모델링하기 

4. 협력을 설계

5. 객체들을 포괄하는 타입에 적절한 책임을 할당

6. 구현하기 

 

 


 

계산기 코드 리팩토링하기 

 

package org.example;

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

        return operand1 + operand2;
    }
}

 

 

 

ArithmeticOperator 이넘 클래스를 새롭게 만들어 연산 기능을 집약화한다. 

 

 

package org.example;

public enum ArithmeticOperator {
    Addition("+"),
    SUBTRACTION("-"),
    MULTIPLICATION("*"),
    DIVISION("/");

    private final String operator;

    ArithmeticOperator(String operator) {
        this.operator = operator;
    }
}

 

 

이들마다 각각의 연산 기능을 추상 메소드를 통해 구현한다. 

 

 

package org.example;

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

    private final String operator;

    ArithmeticOperator(String operator) {
        this.operator = operator;
    }

    public abstract int calculate(final int operand1, final int operand2);
}

 

이제 main 메소드에서는 계산의 요청 상황 발생시 직접 계산하는 것이 아니라 enum클래스로 넘길 수 있다. 

 

 

여기서 cacluate 메소드에서 변수를 받아서 

 

연산자를 확인하고 추상화된 메소드에게 넘겨줌으로써 연산을 진행한다. 

 

package org.example;

import java.util.Arrays;

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

    private final String operator;

    ArithmeticOperator(String operator) {
        this.operator = operator;
    }

    protected abstract int arithmeticCalculator(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.arithmeticCalculator(operand1, operand2);
    }

}

 

 


 

이것을 enum이 아니라 다른 방식으로도 리팩터링해보자. 

 

새로운 인터페이스를 만들어서 

연산자 확인에 대한 boolean과 연산 기능을 하는 두개의 메소드를 생성한다. 

 

 

package org.example;

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

 

 

이때 각각의 연산 클래스를 만들고 

확인 기능과 연산 기능을 구현할 수 있다. 

 

 

 

본 클래스에서 인터페이스를 통해 구현된 연산 클래스들을 받는다. 

 

private static final List<NewArithmeticOperator> arithmeticOperators = List.of(new AdditionOperator(), new SubtractionOperator())

 

 

이것을 리턴하는 코드를 작성한다. 

 

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

public static int calculate(int operand1, String operator, int operand2) {
    return arithmeticOperators.stream()
            .filter(arithmeticOperator->arithmeticOperator.supports(operator))
            .map(arithmeticOperator->arithmeticOperator.calculate(operand1,operand2))
            .findFirst()
            .orElseThrow(()->new IllegalArgumentException("Operator not accepted."));


}

 

인터페이스로 받은 Operators들에 대해 

필터링을 해주어 맞는 구현체를 찾은 다음에

구현체에게 변수를 전달해주면서 연산 기능을 위임한다. 

 

만약 연산자에 해당하는 구현체가 없다면 예외를 발생시킨다 

 

 

 

이렇게 객체들간의 역할을 분배한 상황에서 만약 0으로 나누는 문제를 처리한다고 할때 

응집도가 높기 때문에 division을 수정하면 되는것으로 문제 파악과 해결이 쉬워진다. 

 

 

테스트 코드 

 

@Test
void DivisionByZeroTest() {
    assertThatCode( () -> Calculator.calculate(10,"/",0))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("Division by zero not allowed");
}

 

나눗셈 연산 코드 

@Override
public int calculate(int operand1, int operand2) {

    if (operand2 == 0) {
        throw new IllegalArgumentException("Division by zero not allowed");
    };
    return operand1 / operand2;
}

 

 

반응형