
우아한 테크코스 3주차가 끝났습니다.
확실히 난이도가 많이 올라간 기분입니다. 프로그래밍 요구 사항과 TDD가 발목을 붙잡았네요.
하지만, 그것을 극복하고 사용해보니, 왜 그러한 요구사항을 주셨고 TDD를 사용하라고 하셨는지 알 수 있었습니다.
이러한 저의 학습내용과 느낀점을 한번 써보겠습니다!
무엇이 핵심이었는가?
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다. (예외 처리)
- 클래스(객체)를 분리하는 연습
- Enum 클래스를 적용해 프로그래밍을 구현한다.
- 도메인 로직에 단위 테스트를 구현해야 한다.
- TDD(Test Driven Development, 테스트 주도 개발) : 기능 단위로 테스트 케이스를 만들고, 코드를 작성하자.
- 하드코딩을 피해라.
무엇을 배우고 느꼈는가?
1. 예외 처리
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다. (예외 처리)
해당 문구를 처음 봤을 때는 '별거 아니지~'라고 생각했다.
하지만, 이번 코딩에서 제일 날 힘들게 했던 부분이었다.
해당 아래의 코드를 참고해보자면,
// 테스트케이스 코드
@Test
fun `구입금액 숫자 외 값 예외 테스트`() {
assertSimpleTest {
runException("1000j")
assertThat(output()).contains(ERROR_MESSAGE)
}
}
//--------------------------
//예외 처리 코드
private fun exceptionTest(price: String?) {
require(!price.isNullOrEmpty()) {
println(ERROR_NO_INPUT)
}
val regex = "[0-9]+".toRegex()
require(price.matches(regex)) {
println(ERROR_NOT_INT)
}
require(price.toInt() % 1000 == 0) {
println(ERROR_1000UNIT)
}
}
`exceptionTest`의 require()는 괄호 안의 조건을 만족 못하면 시행되며 `IllegalArugemntException`을 발생시킨다.
이 점을 이용해서 위 처럼 코딩을 하면 단순히 될거라 생각했는데...
`IllegalArgumentException 이 발생해서 테스트에 실패했습니다`
라고 뜬다... 뭔 소리지..? 아니.. 맞잖아? 애초에 예외 처리 테스트인데 맞잖아???
근데, 알고보니 테스트케이스에서 원하는 것은 'IllegalArgumentException 을 발생시키고 메시지를 출력한 후 종료한다`이다. 무슨 차이인가 싶겠지만 엄연히 차이가 있다.
프로그램이 정상적으로 종료가 됐냐 안됐냐의 차이인 것이다.
위의 함수 처럼만 한다면, 프로그램이 예외로 인해 비정상적인 종료가 되므로 틀리다고 보는 것이다.
그렇다면, 어떻게 하는가??
fun main() {
try {
val purchase = Purchase(Purchase.inputPrice())
Lotto.publish(purchase.count)
Lotto.printCount()
LottoWin.input()
WinningStatistics.calculateWin(Lotto.lottos)
WinningStatistics.print(purchase.price)
} catch (exception: IllegalArgumentException) {
}
}
필자는 위의 코드처럼 `try catch`를 이용해 해결했다.
예외 처리가 발생하면 비정상적인 종료를 하는 것이 아니라, 특정 부분의 코드를 시행하도록 바꾸는 것이다.
이렇게 해서 catch문을 비워주면 코드가 바로 끝나버리니 정상적인 종료가 가능해지는 것이다.
물론 이 방법이 확실하게 맞는지는 모르고, 이렇게 하면, 다른 테스트 케이스에선 이 방법이 통하지 않는 경우가 있었지만, 현재 상황에선 이게 최선이었다. 그래서 다른 예외 처리 테스트 케이스는 비정상적인 종료가 되는 것을 캐치하는 식으로 다음과 같이 작성했다.
@Test
fun `로또 번호의 개수가 6개가 넘지못하면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
Lotto(listOf(1, 2, 3, 4, 5))
}
assertThat(output()).contains(ERROR_MESSAGE)
}
이렇게 하면, 비정상적인 종료도 캐치하고, 메시지를 포함하는지도 알 수 있었다.
뭐가 맞는 방법인지는 나중에 시간이 나면 질문드리고싶다.
그리고 생각보다 예외처리 내용이 많았다. 요구사항엔 한줄로 간단히 되어있는 부분도, 세밀하게 나누면 여러개로 쪼개지는 경우가 있었기 떄문이다. 그래서 이번 테스트 케이스에서는 예외 처리 테스트 케이스가 반은 잡아먹은거 같다.
하지만, 이렇게 해줘야지 코딩을 할 때, 잘못되거나 누락된 부분을 쉽게 찾을 수 있기 때문에 매우 중요한 행동이라 생각이 든다.
나중에, 알게 된 사실이지만, 필자가 하나 놓친 부분이 있었다.
입력 금액에 대한 모든 경우를 다 따졌다고 생각했었다.
하지만 사진처럼 천원단위만 생각을 했지, 0원을 투입하는 경우는 생각도 못한거다.. 나머지는 잘 했으면서 왜 이런 사소한 부분을 놓쳤지 ㅠㅠ..
물론, 어거지로 0개 구입했으니, 무조건 당첨이 없고 프로그램도 정상적으로 작동한다~ 이런 식으로 얼버부리면 되겠지만, 그런거는 옳지 않다. 내가 만드는 코드는 모든 상황을 캐치하고 작동하는 코드여야한다. 다음에는 예외 경우를 더더욱 집요하게 찾아봐야겠다.
2. 클래스(객체)를 분리하는 연습
1주차에선 많이 나누진 않았지만, 이미 클래스를 나누고 있기도 했고, 자바를 공부해본 경험도 있어서 크게 어려움을 느낀 부분은 아니었다. 하지만, 어려움을 느끼지 않았다는 것 뿐이지, 의혹감을 계속 들었다.
'이렇게 하는게 맞나..?'라는 생각이 항상 뒤따랐다. 과연 이렇게 클래스를 나누는게 최선인지, 혹은 더 효율적으로 나눌 수 있는 것은 없는지 고민이 들었다.
특히, '핵심 로직과 UI를 구분해라'라고 해서 메소드 상으로 구분을 했었는데, 이렇게 해도 되는 것인지 조금 찝찝했다.
다행이도, 피드백에 의하면 메소드로 구현해도 괜찮은 부분이었다!
단, 객체의 상태를 보기 위한 로그 메시지 성격이 강하면 `toString()`으로,
View에서 사용할 데이터라면 `getter` 메서드를 통해 데이터를 전달한다는 것들도 새로 알았으니, 학습 해야겠다.
3. Enum 클래스를 적용해 프로그래밍을 구현한다.
Enum 클래스에 대해 공부를 해봤는데, Enum클래스는 '열거형 객체'이다. 즉, 연관성이 있는 상수들을 전역으로 선언하는 대신 `Enum`을 통해 나열하여 관리하는 것이 가독성도 좋고 편리하다는 것이다.
그래서 내가 이 `Enum`클래스를 어디에 썼는지 확인을 해보자면,
enum class WinningStatistics( //당첨 현황
val winName: String,
val price: Int,
var count: Int
) {
WIN3("3개 일치", 5000, 0),
WIN4("4개 일치", 50000, 0),
WIN5("5개 일치", 1500000, 0),
WIN5Bonus("5개 일치, 보너스 볼 일치", 30000000, 0),
WIN6("6개 일치", 2000000000, 0);
//-------------------
enum class LottoWin(var number: Int) { // 당첨번호
NUM1(0),
NUM2(0),
NUM3(0),
NUM4(0),
NUM5(0),
NUM6(0),
BONUS(0);
이렇게, 당첨 현황과 당첨 번호에 각각 정리를 했다.
우선, 당첨 현황의 경우, 당첨된 상황에 맞춰 이름, 상금, 당첨 횟수를 가지는 상수들로 묶어줬다.이렇게 하면, 나중에 이름을 출력할 때와 상금을 계산할 때 확실하게 편해졌다.
그리고 당첨 번호의 경우, 입력받은 당첨 번호를 나는 'Enum'클래스에 저장을 하는 형식으로 했다.내가 따로 당첨 번호를 'Enum'클래스에 저장한 이유는 우선 당첨 번호는 일반 번호와는 다르게 정렬할 필요도 없고, 생성하는 방식도 랜덤이 아닌 직접 입력이기에 따로 저정했다. 그리고 배열이 아닌 열거형으로 저장하는 것이 코드를 봤을 때, 일반 번호가 아닌 당첨번호임을 확실하게 식별할 수 있고, 또 관리하기도 편하다고 생각했기 때문이다.
이번 시간에, Enum클래스를 적극적으로 이용해봤는데, 이녀석... 맘에 든다.. 비슷한 상수들을 관리할 때, 이런 식으로 묶어서 관리해주면 엄청 깔끔해지고 편리해진다. 앞으로도 애용할거 같다.
1. TDD(Test Driven Development, 테스트 주도 개발)
이번 시간에 가장 날 힘들게 했던 부분이다. 3주차 코수타에서 'TDD는 무조건 좋은 것은 아니다!'라고 말씀하신 부분이 이해가 된다. 굉장히 힘들고 개발시간이 느려질 수도 있어서 숙련도를 많이 요구하기 때문이다.
하지만, 나는 이번 기회에 TDD를 사용해서 개발해보기로 했다. 괜히 뱁새가 황새를 따라한 꼴이 되지않을까 걱정은 했지만, 다행스럽게도 다리는 찢어지지 않았고 인대가 조금 늘어난 기분이다.
이 TDD의 핵심은 '기능 단위로 테스트 케이스를 작성한 후에, 코드를 기능 단위로 구현한다'이다.
솔직히 말하자면, 테스트 케이스를 먼저 작성하고 코드를 짜는 것은 처음에 좀 힘들어 하다가 시간이 지나니 익숙해졌다.
하지만... 이 기능 단위로 테스트 케이스를 작성하는 것이 나의 발목을 잡았다.
왜 기능 단위로 테스트 케이스를 작성하는 것이 어려운가? 말 그대로, 기능 단위로 테스트 케이스가 돼야 하기 때문이다.
'응? 뭔 소리지?' 싶은 대답인데, 말 그대로 혼자서 테스트 케이스가 돌아가야 한다.
우리가 만드는 메소드들은 보통 독립적이기 보다는 서로가 서로에게 파라미터를 던지고 받아쓰거나, 전역 변수 값을 설정해주거나 가져온다. 즉, 처음부터 흐름을 따라 가야지 해당 메소드에 필요한 전역 변수값과 파라미터 값이 작성 되는 것이다. 그러므로, 이러한 부분도 분리를 해줘서 테스트 케이스에서 통제가 가능하게끔 코딩을 짜줘야 한다.
그래서 코드를 짤때, 파라미터나 전역 변수를 어떤 식으로 이용할지를 잘 생각해야하고, 또 너무 메소드 별로 나누려고는 하지 말고, 말 그대로 기능이므로, 한 기능에 메소드가 어느 정도까지는 들어가서 작동하는 방식으로도 괜찮다.
물론, 이렇게 힘들게 TDD에 성공을 하면 보상이 따르기 마련이다. 바로 유지보수 측면에서 매우 훌륭해진다. 기능 단위로 개발을 하다 보니, 해당 코드가 맡는 역할이 무엇인지를 확실하게 알 수 있다. 그러다보니, 원치않은 작동이나 오류가 발생해서 수정을 해야할 때, 빠르게 수정할 부분을 찾기도 쉽고 독립적이기 때문에 해당 부분만 고치면 된다.
이러한 장점을 협업을 할 때, 굉장히 이득이 크다. 프로젝트를 시작할 때, 서로 역할을 분담하면서 하기 때문에, TDD로 진행하면 서로의 부분을 독립적으로 개발하면서 나중에 `merge`할 때 편해지기 때문이다.
실제로, 내가 했던 프로젝트들은 일반 개발 방식으로 많이 했는데, 대형 팀 프로젝트의 경우에는 서로의 코드를 'merge'할 때, 불편한 부분들이 많았고, 서로의 코드에 대해 설명하는 시간도 필요하다보니 시간이 많이 소요됐다. 그리고, 버그가 발생하거나 코드를 수정 할때, 그 방대한 코드들을 직접 디버깅하며 하나씩 뒤져보느라 죽을 뻔 했다...
다시 말해, 완벽한 TDD는 아니더라도, 기능 단위로 구현하는 연습해야 나중에 실무에서 협업을 할 수 있는 역량을 갖출 수 있는 것이다. 꼭 연습해야겠다.
2. 하드코딩을 피해라.
하드코딩은 말 그대로, 특정값들을 상수를 통해서가 아닌 코드의 값을 직접적으로 입력하는 것이다.
처음에 '나는 해당되지 않겠지'라고 생각한 부분이었다. 하지만, 하드코딩은 생각보다 넓은 의미를 가지고 있었고, 나 또한 거기에 속하고 있었다. 변수나 상수를 이용해서 코딩은 잘 하고 있었다고 생각했는데,
//하드코딩된 경우
for(number in 1..45){
}
//하드코딩을 없앤 경우
const val MIN = 1
const val MAX = 45
for(number in MIN..MAX){
}
내가 위의 하드코딩된 경우처럼 하고 있던 것이었다. ㅎ...
요구사항에 의해 당연하게 범위 `'1 ~ 45' 라 하면 최솟값 최댓값이지 라고 생각했는데, 이건 틀린 생각이었다.코드가 짧고 요구사항이 적으면 금방 이해하겠지만, 실무처럼 많은 코드와 요구사항이 판치는 경우에는 해당 숫자만 보고는 바로 판별하기 힘들다는 것이다.
그러므로, 상수를 통해 숫자를 직접 입력하기 보다는 상수명으로 써줘야 해당 부분에 어떠한 숫자가 들어가는지 의미를 확실하게 전달할 수 있는 것이다.
또, 장점이 더 있다. 해당 상수를 여러번 쓰는 경우, 상수값만 변경 해주면 되기 때문에 유지보수 측면에서 또 다른 이득도 있다.
그리고, 암호키와 같은 보안적인 요소도 하드코딩을 하면 유출이 되기 때문에, 이러한 경우에도 하드코딩을 하지 말아야 하는 이유가 생긴다.
결국, 실무에서는 하드코딩은 곧 재앙을 불러올 수도 있기 때문에, 얼른 무의식 적으로 하드코딩을 하는 습관을 고쳐야겠다.
회고록을 마치며...
이번 주차는 체감 난이도가 많이 올라간 기분이었다. 구현 자체가 어려워졌다기 보다는, 많이 추가된 프로그래밍 요구사항과 TDD를 통한 개발의 낯섦이 컸다.
그래도, 나의 장점인 '어떻게든 끝을 봐서 완수해버린다!'덕분에, 어떻게든 TDD로 성공해서 많이 뿌듯하다. 거기다가 TDD를 통한 장점들도 확실하게 느껴서 좋은 경험이 되었다. 이 외에도 'Enum'클래스와 하드코딩에 대해서도 학습하여 나의 것으로 만들 수 있는 유익한 시간이었다.
벌써, 마지막 주차다. 너무 짧은 4주였다. 마지막 4주차도 열심히 해서 성공적으로 완주할 것이다. 근데, 이게 마지막 주차여서 살짝 아쉽다. 진짜 실무와 협업의 관점에서 다른 곳에서 배우기 힘든 많은 것들을 배웠기 때문이다. 만약, 기회가 된다면, 본코스에서도 이러한 경험을 계속 느끼고싶다. 그러니 열심히 하자! 열심히 하면 본코스를 하든 못하든, 지금의 경험은 무조건 나에게 뼈와 살이 될테니까!
'우아한테크코스' 카테고리의 다른 글
[우아한테크코스] kotlin 프리코스 - 2주차 숫자 야구 회고록 (0) | 2022.11.16 |
---|---|
[우아한테크코스] kotlin 프리코스 - 1주차 회고록 (1) | 2022.11.04 |