지원서 작성부터 큰 고비가 있었습니다.
쓰고 지우고를 무려 4번 반복한 끝에 결국 아쉬움이 남는 지원서를 제출하게 되었어요.
하지만, 몇 일 뒤 시작할 프리코스를 기대하며 사전에 공개된 디스코드 채널에 들어갔습니다.
처음에는 다 뿌시겠다는 마음으로 시작했지만, 실력자들의 토론을 보며 위축되기도 했습니다.
그럼에도 불구하고 열정 넘치는 사람들을 보니 코드 리뷰와 소통을 통해 성장할 수 있겠다는 확신이 들었습니다.
마침내 화요일 15시 정각, 1주차 미션인 [문자열 계산기]가 공개되었습니다.
저는 미션을 구현하기 전 3가지의 목표를 설정했습니다.
1. 요구사항 분석에 시간 많이 투자하기
2. Git Convention 지키기
3. MVC 패턴 적용해보기
위 3가지의 목표를 가지고 1주차 미션을 수행했습니다.
📖 미션 개요
이번 미션의 목표는 입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현하는 것이었습니다.
쉼표(,)와 콜론(:)을 구분자로 가지는 문자열을 전달할 경우, 구분자를 기준으로 분리한 각 숫자의 합을 반환해야 합니다.
예를 들어,
- "" => 0
- "1,2" => 3
- "1,2,3" => 6
- "1,2:3" => 6
또한, 기본 구분자인 쉼표와 콜론 외에 커스텀 구분자를 지정할 수 있습니다.
커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 사용합니다.
예를 들어,
- "//;\n1;2;3" => 6
마지막으로, 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 합니다.
추가로 입력 요구사항에서 입력은 구분자와 양수로 구성된 문자열이라는 조건이 있었습니다.
📚 진행 과정
1. 요구사항 분석
우선 제시된 요구사항을 보며 어떤 객체를 구분할 수 있을지에 대해 고민했습니다. 🤔
처음에는 구분자를 뜻하는 Delimiter와 계산을 수행할 Calculator 클래스를 생각했습니다. 다음으로는 MVC 패턴 적용을 목표로 했기에 Model을 분리한 후, 남은 View와 Controller에 대해서도 고민했습니다.
생각한 객체들을 README.md에 작성하고, 하위에서 수행할 기능 및 요구사항을 체크 박스와 함께 정리하여 추후 구현이 완료된 기능과 고려된 요구사항을 체크하며 진행 상황을 확인하려고 했습니다.
해당 미션은 기본적으로 제시된 요구사항 외에 고려해야 할 예외가 상당히 많았습니다.
커스텀 구분자가 "//"와 "\n" 사이의 문자라고 했기 때문에, 커스텀 구분자는 반드시 길이가 1이어야 한다는 조건을 추가했습니다.
다음으로, 커스텀 구분자가 숫자가 될 경우 계산기 로직에 문제가 생기므로, 커스텀 구분자는 숫자가 아니어야 한다는 조건을 추가하였습니다.
사용자 입력에 커스텀 구분자 정의만 있고 숫자는 없는 경우, ""와 동일하다고 생각해 0을 반환하도록 하는 조건을 추가하였습니다.
또한, 입력 요구사항에서 입력은 구분자와 양수로 구성된 문자열이라고 했기 때문에, 음수가 존재하면 안 된다는 조건도 추가했습니다.
추가로 커스텀 구분자가 복수가 될 수 없다는 조건이 없기에 복수의 커스텀 구분자를 정의할 수 있다는 조건을 추가했습니다.
정리하자면 아래와 같은 조건을 추가하였습니다.
- 커스텀 구분자는 반드시 길이가 1이어야 한다.
- 커스텀 구분자는 숫자가 아니어야 한다.
- 사용자 입력에 커스텀 구분자 정의만 있다면, ""이라고 판단해 0을 반환해야 한다.
- 분리된 숫자들은 음수가 아니여야 한다.
- 복수의 커스텀 구분자를 정의할 수 있어야 한다.
위 내용들을 README.md에 작성했습니다.
2. 프로젝트 구조
MVC 패턴 적용을 목표로 하긴 했지만, 미션을 확인하고 생각보다 작은 요구사항에 객체를 분리하지 않고 함수만 잘 분리하여 한 클래스에 작성해도 될 것 같다는 생각이 들기도 했습니다.
다만, 평소 Java와 Kotlin을 주로 사용했기에 객체지향적으로 구현하는 게 더 익숙하기도 했고
한 클래스에 모든 로직을 담았을 때 가독성이 떨어지고 유지보수 측면에서도 어려워질 것 같았습니다.
또한, 기능별로 코드가 섞이면서 책임이 명확하게 나누어지지 않아 코드의 의도를 파악하기 힘들어질 것이라 생각했습니다. 이런 이유로 MVC 패턴을 적용하여 각 역할을 분리함으로써 코드의 가독성을 높이고, 추후 요구사항 변경이 생겨도 특정 역할에만 집중해 변경할 수 있도록 구조를 설계하고자 했습니다.
특히, MVC 패턴은 사용자의 입력을 처리하는 부분인 View, 상태와 데이터를 관리하는 Model 그리고 이 둘을 연결해 주는 역할을 하는 Controller을 명확히 나누어 주기 때문에, 복잡한 로직을 구현하게 될 때 발생할 수 있는 혼란을 줄이는 데 유리하다고 판단했습니다.
controller
- CalculatorController.kt
model
- Calculator.kt
- Delimiter.kt
view
- CalculatorView.kt
이런 구조로 코드를 구현했습니다.
3. 고민한 점 & 새롭게 배운 점
CalculatorView의 관심사 분리
처음에는 CalculatorView에 사용자의 입력을 받는 함수와 출력을 하는 함수를 모두 맡겼습니다.
그런데, 입력과 출력을 분리하여 각 클래스가 하나의 역할만 수행하도록 해서 관심사를 분리한다면 코드의 가독성이 높아지고 각 클래스의 책임이 더욱 명확해질 것이라는 생각이 들어 InputView와 OutputView로 분리하게 되었습니다.
테스트 코드 작성
평소 테스트 코드 작성의 필요성을 크게 느끼지 못했기에 테스트 코드 작성 경험이 없어 이번 미션을 기회 삼아 테스트 코드를 작성해 봤습니다. 확실히 테스트 코드를 작성하게 되면서 함수를 리팩터링해도 정상적으로 동작하는지 빠르게 검사할 수 있어서 테스트 코드를 작성하는데 소요되는 시간이 테스트 코드를 작성하지 않음으로써 겪게 될 수 있는 재작업에 소요되는 시간을 생각하면 큰 메리트가 있다고 생각되었습니다. 테스트 코드 작성에 소요되는 시간이 아깝다고 여겼었는데 개발 과정에서 테스트 코드가 얼마나 중요한 역할을 하는지를 알게 되었습니다.
추가로, 테스트 코드를 작성하다 보니 관심사의 분리가 더 가능한 부분들이 있다는 생각이 들었습니다.
그래서 Input를 처리할 InputParser을 생성하고 문자열에서 구분자로 분리한 값들에 음수가 포함되어 있거나 문자인지를 확인하는 로직을 위치시켰고, Delimiter에서는 커스텀 구분자 길이가 1인지, 숫자가 아닌 지를 확인하는 로직을 위치시켰습니다.
Java의 static과 Kotlin의 companion object의 차이
상수를 정의할 때 이번 미션에서는 코틀린을 사용하기 때문에 companion object를 사용했습니다. 그런데 static과 companion object는 어떤 차이가 있을까?라는 궁금증이 생겼습니다. 그래서 찾아보니 static은 Class Level의 변수나 함수를 위해 사용되며, companion object도 동일한 기능을 수행한다고 합니다. 다만, 이 둘의 차이점은 companion object의 경우 static과 달리 객체로서 생성된다는 점이었습니다. companion object로 작성된 변수와 메서드를 디컴파일 하면 Java 코드로 구현된 객체가 생성됨을 볼 수 있었습니다.
Spread Operator *
스프레드 연산자에 대해 알게 되었습니다. 구분자 리스트를 가지고 문자열을 분리하는 과정에서 코드 라인 수를 최소화하고 구분자를 분리하는 역할을 가진 함수에 전달하기 위한 방법을 고민하던 중, 스프레드 연산자가 적합한 방법이라는 것을 찾게 되었습니다. 스프레드 연산자는 Array를 여러 개의 인자로 풀어서 전달하는 역할을 수행합니다. 그래서 MutableList로 저장하고 있던 구분자 리스트를 toTypedArray를 통해 Array로 변환한 뒤, 스프레드 연산자 *를 사용하여 문자열에서 구분자를 기준으로 숫자를 분리하는 함수에 구분자들을 전달하도록 구현할 수 있었습니다.
최종적으로 다음과 같은 파일 구조가 형성되었습니다.
controller
- CalculatorController.kt
model
- Calculator.kt
- Delimiter.kt
- InputParser.kt
view
- InputView.kt
- OutputView.kt
💡 마무리
시험 기간과 겹치면서 충분한 시간을 투자하지 못해 아쉬움이 남는 1주차였습니다.
커밋 메시지를 잘 작성했다고 생각했지만, 다른 분들의 커밋 메시지를 보니 가독성에서 큰 차이가 있음을 느꼈습니다. 다음 미션부터는 <scope>와 <body>를 적극 활용하여 변경된 파일, 변경 이유, 변경 사항을 커밋 메시지에 명확히 담아 전달하려고 합니다.
무려 여섯 분이 제 코드를 리뷰해 주셨는데, 복수의 커스텀 구분자를 허용하면서 index로 파싱 위치를 지정하는 것보다 정규표현식을 활용하는 편이 상태 관리와 코드 간결성 면에서 유리하다고 생각했습니다. 그런데 같은 고민을 다른 방식으로 해결하신 분들의 의견을 들으면서, 제가 왜 정규표현식을 선택했는지에 대해 스스로 정리할 기회가 되었습니다.
또한, 놓쳤던 불필요한 로직도 찾아낼 수 있었고, 다른 분들의 코드를 리뷰하면서 제가 작성한 코드의 개선점을 많이 발견할 수 있었습니다. 이것이 코드 리뷰의 긍정적인 효과가 아닐까 생각합니다.
벌써 1주차가 끝났지만 그럼에도 많은 것이 남는 시간이었습니다.
남은 주차에서도 프리코스에 적극 참여하여 더 많이 배워가도록 하려고 합니다 :)