객체 지향 프로그래밍
적절한 객체에 적절한 책임을 부여하여 서로 메세지를 주고 받으며 협력하게 하는 것
[객체 지향에서의 중요한 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 |
---|