가진 것을 비우기
요구 사항 규모보다 극단적인 리팩터링을 요구하는 경우가 많다.
거부감이 들 수 있지만 일단 적용해 보고, 적용하기 전과 후의 코드를 분석해 본다.
자신이 가진 것을 비울 때 가장 많은 것을 배울 수 있다.
상황에 맞는 설계와 구현 방법을 찾아라
프로그래밍 설계와 구현은 아날로그적인 영역이 많은 부분이다.
프로그래밍을 기술이 아닌 예술의 일부라고 생각하는 이유도 이런 점 때문이다.
프로그래밍 설계와 구현에 정답은 없다.
정답을 찾기보다 요구 사항에 적합한 최선의 설계와 구현 코드를 찾기 위해 노력한다.
next step - 과정을 슬기롭게 소화하는 방법
첫번째 과제는 코틀린 코드로 문자열 계산기를 만드는 것이다.
이 글에서는 구체적인 문제 설명과 해결 방법 보다, 코드 리뷰를 두 번 진행하면서 배운 부분에 대해서 집중적으로 적어보려고 한다.
이번 과제에 대해 간단히 설명해보자면 아래와 같다
사용자가 입력한 문자열 값에 따라 사칙 연산을 수행할 수 있는 계산기를 구현해야 한다.
문자열 계산기는 사칙 연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다.
즉, 수학에서는 곱셈, 나눗셈이 덧셈, 뺄셈 보다 먼저 계산해야 하지만 이를 무시한다.
예를 들어 "2 + 3 * 4 / 2"와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행 결과인 10을 출력해야 한다.
크게 두 가지로 계산기 기능 구현을 생각했다.
(1) 계산기 인스턴스를 매번 새롭게 만들어서 계산 한다.
(2) 계산기 인스턴스를 한 번만 만들어서 계산기 값을 초기화 해서 사용한다.
결론부터 말해보면, (1)이 더 객체지향적으로 건강한 코드 설계이다. (2)에서 (1)로 코드를 수정해 나간 사고 과정을 정리해보려고 한다.
[ 구현 1 ] 인스턴스 하나 + 초기화 기능
가장 단순하고 직관적인 방법으로 계산기를 만들었다.
계산기 안에 문자열을 입력 받는 input 프로퍼티를 만들고 이걸 초기화 input = "" 해서 사용할 수 있도록 만들었다.
테스트를 보면 calculator를 하나 만들고 테스트마다 initiate 해서 사용했다.


이렇게 구현하면 외부에서 사용하는 함수 calculate(), initiate()에서 private 변수를 수정하게 된다.
initiate()라는 매서드를 사용하기 때문에 내부 변수를 외부로 공개하지는 않을 수 있지만 클래스라는 주물을 제대로 활용하지 못하는 코드가 만들어진다.
만약 매번 인스턴스를 만들어서 초기화 할 필요가 없다면 굳이 input 변수를 만들어야 할 필요도 없다.
[ 구현 2 ] 기능 요구사항 변경 + 객체의 책임 확인
초기화 기능을 없애고 private input 변수도 없어졌다.(왼쪽) 대신 테스트 코드에 매번 객체를 만들어서 사용하도록 만들었다(오른쪽)
calculate 객체는 "계산한다"는 책임을 가지는 객체이다. 그런데 코드를 보면 문자열에 있는 사칙연산을 찾아내는 탐색 기능이 calculate 객체 내에 구현되어 있는 것을 알 수 있다. 단일 책임의 원칙에 따라 이 역할을 하는 객체에게 책임을 나눠야 한다.


[ 구현 3 ] 책임 분리
private evaluate() 를 class Evaluation 객체로 책임을 분리했다.
사실 이 부분 구현은 enum class를 이용해서 리팩터링을 더 했는데, 그 부분은 다음 글에서 쓰려고 한다.

[ 구현 4 ] 코틀린 = 객체 지향 + 함수형 프로그래밍
이제 evaluate의 for문에 조금 더 집중했다. 이 함수를 보면 input을 split(" ")해서 유사 배열로 만든다. 예를 들면 [ 2, +, 4, * 5 ].
이걸 실제로 연산하기 위해서 아주 기초적인 접근으로 for문을 step 2 로 도는 구현을 했다. 초기값 2를 넣고 + 4 가 나오면 2+4를 하는 것이다.
여기에서 어떻게 하면 코틀린의 특징을 살려서 구현할 수 있을까? 하는 고민으로 리뷰어님께 질문드렸는데,
구현에 정답은 없다는 답변을 듣고 더 미궁에 빠졌다. 🥲 그래서 얼른 kotlin in action을 구매해서 코틀린이라는 언어로 쓰려면 어떤 눈을 가져야하는 지 읽기 시작했다.
그리고서 발견한 문구가 있다.
코틀린은 함수형 스타일로 프로그램을 짤 수 있게 지원하지만 함수형 프로그래밍 스타일을 강제하지 않는다.
코틀린으로 코드를 작성할 때는 객체지향과 함수형 접근 방법을 함께 조합해서 문제에 가장 적합한 도구를 사용하면 된다.
- kotlin in action 1 장 코틀린이란 무엇이며, 왜 필요한가?
이 부분을 읽고 아하 모먼트가 있었다.
이 for문을 내장된 함수형 매서드를 이용해서 구현하는 것이 가장 코틀린스러운 코드가 되겠다는 생각이 들었고 오른쪽과 같이 수정했다.
기존에 .split()으로 나온 유사배열에 filter() 를 이용해서 숫자와 연산자를 나눴고 이 두 배열을 .zip을 이용해서 iterate 했다.


[ 구현 5 ] 더 나은 매서드를 찾기
.zip 은 두 배열을 parallel 하게 돈다. 전체 문자열 중 숫자와 연산자를 구분한 두 개의 유사배열 numbers와 operators가 있다. numbers의 첫번째 값을 제외(.drop(1))한 나머지를 operators와 나란히 돌면서 '+'(operators[0]) 와'4'(numbers[0]) 를 연산하고 이어 '*'(operators[1]) 과 '5'(numbers[1])을 연산하는 방식으로 작동한다.
코틀린을 제대로 구현하기 위해서는 내장된 함수형 매서드를 잘 이해해야겠다는 생각이 들어서 더 적합한 방법이 없을까 고민하며 .zip 대신 .reduceIndexed 와 .foldIndexed를 적용해봤다. foldIndexed는 reduceIndexed와 다르게 초기값을 입력할 수 있다. 그래서 reduceIndexed가 var result = {초기값}을 만드는 것과 달리 foldIndexed는 바로 return을 할 수 있다. 이 구현에서는 딱 적합하단 생각이 들어서 선택했다.



참고할 만한 페이지: 코틀린 내장 함수형 매서드를 정리한 글
https://junghun0.github.io/2019/08/02/kotlin-stream/
[Kotlin] Kotlin Stream - Junghoon's Blog
Kotlin 의 Stream 함수정리
Junghun0.github.io
'백엔드 개발 > 백엔드 일기' 카테고리의 다른 글
Q001. 구글 계정을 만들 때, 이미 있는 아이디인지 어떻게 빠르게 알 수 있을까? (0) | 2022.05.14 |
---|---|
#013. 백엔드 성장일기: [넥스트 스탭] ENUM과 Null Safety (0) | 2022.05.14 |
#011. 백엔드 성장일기: PostGreSQL에서 🤦♀️의 글자수 확인하기 (0) | 2022.05.07 |
#010. 백엔드 성장일기: 테이블 스키마 작성하기 (2) | 2022.05.03 |
#009. 백엔드 주간 소식: 모각글 반상회, 깜짝 생일파티, 티코지 (0) | 2022.05.02 |