2024. 9. 18. 19:59ㆍ내일배움캠프
두 번째 피드백은 '불필요한 연산 객체 (반복)생성' 에 관한 내용이었다. 확실히 나의 불찰로 생긴 문제였기에 피드백을 적극 반영해 문제를 해결해 보았다. 피드백을 반영한 코드는 여기서 확인할 수 있다.
1. Level.02 계산기
피드백 내용을 정리하면 '불필요한 연산 객체' 를 계속 생성하는 것과 'Calculator' 클래스의 역할이 '입력 값을 연산' 하는 역할임에도 'Operation 인터페이스' 의 구현 클래스 객체를 매핑하는 역할도 수행하고 있다는 문제가 존재한다.
1-1. 문제 파악
현재 Level02 계산기는 'Level02.start()' 메서드의 내부 로직이 반복되는 구조로 'Level02' 객체 생성시 'Calculator' 객체를 한 번 생성한 뒤 'start()' 메서드가 수행되는 동안 계속해 반복 사용한다. 그래서 'Calculator' 객체 생성시 'Operation operation' 을 선언해두고 'setOperation()' 메서드를 통해 생성한 연산 객체를 해당 변수에 저장하는 식으로 구현하였다.
이 때 문제는 'setOperation()' 메서드에 있다. 'start()' 메서드에서 'setOperation()' 메서드를 반복해 호출하면 계속해서 새로운 연산 객체를 생성하게 되며, '메모리 누수(Memory Leak)' 가 발생하는 것이다. 즉, 불필요하게 계속해서 객체를 찍어내는 것(?)이 문제이다.
또한 'Calculator' 클래스가 연산 수행 역할 외에도 'Operation 인터페이스' 의 구현 클래스 객체를 매핑하는 역할도 수행하고 있으므로 이를 분리해야 한다.
1-2. 문제 해결
위에서 제기된 문제의 해결에는 여러 방법이 있겠지만 아래와 같은 방식으로 문제를 해결해 보았다.
우선 역할 분리를 위해 'level02/calculation' 위치에 'OperationMapper' 클래스를 생성하였다. 해당 클래스는 연산자에 해당하는 연산 객체를 생성해 매핑하는 역할을 가지고 있다. 정확하게는 연산 기호(key)에 생성한 연산객체(value)를 매핑해 'Map<String, Operation> operation' 필드 객체에 저장하게 된다.
'addOperation(String)' 메서드는 연산자에 해당하는 연산객체를 'Map 객체' 에 저장하며 'Map.put()' 이 아닌 'Map.computeIfAbsent()' 를 사용해 이미 동일한 'key' 를 가지고 있다면 어떤 수행도 하지 않고 있다면 지정한 내용을 수행하도록 하였다. 즉 이미 연산자에 해당하는 연산객체가 생성되어 있다면 불필요하게 또 다시 연산객체를 생성하지 않게 된 것이다.
'getOperation()' 은 파라미터로 전달받은 연산자에 해당하는 연산객체를 'Map<String, Operation> operation' 객체에서 찾아 반환하는 메서드이다.
이렇게 '연산 객체를 매핑' 하는 역할을 분리해 'level02/calculation/Calculator' 클래스는 아래와 같이 '연산' 에 대한 역할만을 가지게 되었다.
※ 참고 : 역할분리를 진행하며 'OperationMapper' 클래스가 추가되어 기존의 'level02/calculate' 패키지를 'level02/calculation' 으로 수정했다. 패키지에 포함된 인터페이스 및 클래스들이 '연산(계산)' 에 사용된다는 의도를 담아 네이밍하였는데 'calculate' 로는 'OperationrMapper' 까지 포함할 수 없다고 판단했기 때문이다.
또한 'calculate()' 메서드는 파라미터로 'Operation' 객체를 전달받게 수정하였다.
Level02 클래스는 'OperationMapper' 객체를 필드로 가지게 되었고, 연산자를 통해 Calculator 객체에 연산객체를 생성하는 것이 아닌 'OperationMapper' 객체에 연산자에 해당하는 연산객체를 추가하게 되었다. 이후 'OperationMapper' 객체에서 연산자에 해당하는 연산객체를 가져와 'Calculator.calculate(Operation)' 을 호출해 연산을 수행한다.
2. Level.03 계산기
Level03 클래스도 Level02 클래스와 같은 맥락의 문제가 존재한다. '문제 파악' 은 Level02 계산기에서 했으니 '문제 해결' 에 대한 부분을 정리하였다.
우선 Level03 계산기에서 사용할 OperationMapper<> 를 새로 생성했다. Level03 부터 요구사항으로 인해 Generic 을 적용했기에 OperationMapper 에도 적용해보고자 추가하게 되었다. Generic 을 추가한 것 외에도 Operator(enum Class) 를 활용하였다. 반영한 내용을 제외하면 'level02/calculation/OperationMapper' 와 비슷한 로직을 가지고 있다.
역할을 분리하며 'level03/calculation/Calculator<>' 도 '연산 수행' 이라는 하나의 역할을 가지게 되었다. 또한 'calculate()' 메서드는 파라미터(매개변수)로 'Operation<T>' 객체를 전달받도록 수정하였다.
Level03 클래스는 3개의 영역이 수정되었는데 먼저 새로 생성한 'OperationMapper' 객체를 주입했다. 두 번째는 'Calculator 객체(cal)' 에 연산자를 통해 연산객체를 저장하던 걸 'OperationMapper' 객체의 필드 객체(Map<Double>)에 저장하도록 수정했다. 마지막으로 'calculate(Operation<>)' 메서드는 'OperationMapper.operation' 에서 연산자에 해당하는 연산객체를 전달받아 연산을 수행하도록 수정했다.
3. Level04 계산기
마지막으로 개인적으로 추가했던 Level04 계산기는 'Operator(enum 클래스)' 를 최대한 활용하여 'OperationMapper' 같은 매핑 클래스를 사용하지 않고 문제를 해결해 보았다.
변경사항은 크게 2가지다. 먼저 필드 'Operation<Double> operation' 을 추가해 각 연산에 대한 연산기호는 물론 연산객체를 각 상수가 가질 수 있게 되었다. 연산객체 필드를 추가하며 값 지정은 물론 생성자 또한 수정해 주었다. 두 번째는 상수의 '연산객체' 필드에 접근할 수 있는 메서드 'getOperation()' 과 파라미터로 전달받은 연산기호에 해당하는 상수의 연산객체를 반환해주는 'findByOperator(String)' 를 추가한 것이다.
'getOperation()' 의 경우 현재 'findByOperator(String)' 에서만 호출되기에 'private' 로 접근을 제한하였다. 'findByOperator(String)' 의 경우에는 Enum 클래스에서 제공하는 'value()' 메서드를 활용해 모든 상수의 'String operator' 와 메서드의 파라미터 'String operator' 를 비교해 같은 상수가 있다면 해당 상수의 'Operation<Double> operation' 을 반환한다. 만약 일치하는 연산자가 없을 경우에는 'IllegalArgumentException' 이 발생한다.
Level04 클래스의 경우 이전 Level02, 03 계산기와 마찬가지로 피연산자는 'Calculator' 객체에 저장한다. 하지만 연산자의 경우 'Operator enum 클래스' 를 활용해 입력 받은 연산자와 같은 연산자를 갖는 상수를 찾아 해당 상수의 연산객체를 가져와 'Calculator.calculate(Operation<Double>)' 를 호출한다. 그래서 Level04 의 경우 'OperationMapper' 와 같은 매핑 역할의 클래스가 별도로 필요하지 않다. 연산자를 통해 연산객체를 조회하는 'Operator.findByOperator(String)' 의 경우 전달 받은 연산자에 대한 값을 가진 상수가 없으면 'IllegalArgumentException' 이 발생하므로 해당 메서드를 사용하는 구문의 경우 'try-catch' 문으로 감싸 예외처리를 해주었다.
현재 'try-catch' 문 이전에 입력 값의 유효성을 검증하는 메서드가 호출되어 지정한 연산자 이외의 값이 넘어올 일은 없을 것 같으나 수정 도중 실수로 인해 유효하지 않은 연산자가 넘어올 수 있으므로 예외처리를 테스트해 보았다. 테스트는 앞서 말한 것처럼 유효성 검증의 존재로 '테스트 케이스' 를 작성해 해당 로직이 정상 작동하는지를 확인했다.
지정하지 않은 연산자(=유효하지 않은)가 주어질 때 예외가 정상적으로 처리되는 지를 테스트한 결과이다. 연산자로 공백 문자열(" ") 이 'findByOperator()' 의 파라미터로 넘어갈 때 enum 클래스에 작성한대로 'IllegalArgumentException' 이 발생했고 이를 'catch' 문 쪽에서 예외를 처리, 예외 처리 메시지가 출력되고 테스트가 정상적으로 종료된 것으로 보아 예외는 제대로 처리가 되고 있다는 것을 알 수 있다.
4. 마무리
불필요한 연산객체를 생성하는 문제와 역할 분리에 대한 피드백을 프로젝트에 반영해 보았다. Level02~04 계산기 모두 'Calculator' 가 본래의 역할만을 수행하게 되었고, '불필요한 연산객체 생성' 에 대한 부분은 각 계산기 별로 아래와 같이 수정되었다.
- Level02 계산기 : 연산객체 매핑 클래스(OperationMapper)를 활용해 문제 해결
- Level03 계산기 : 연산객체 매핑 클래스(OperationMapper<>)를 활용해 문제 해결 + enum 클래스 활용(문자열 대신 상수의 필드 사용)
- Level04 계산기 : enum 클래스와 Exception 을 활용해 문제 해결
'내일배움캠프' 카테고리의 다른 글
[내일배움캠프] TIL - 24.09.18(수) (0) | 2024.09.18 |
---|---|
[내일배움캠프] 계산기 구현 - 피드백 반영(3) (0) | 2024.09.18 |
[내일배움캠프] 계산기 구현 - 피드백 반영(1) (0) | 2024.09.17 |
[내일배움캠프] TIL - 24.09.13(금) (0) | 2024.09.13 |
[내일배움캠프] 숫자 야구 게임 구현 - Level.01 요구사항(1) (0) | 2024.09.13 |