우아한테크코스

[우아한테크코스] 레벨1 - 사다리 타기 미션 정리

hectick 2023. 3. 5. 03:18

아직 사다리 타기 미션이 끝난지 1주일도 지나지 않았기 때문에 회고글을 금방 쓸 수 있을 줄 알았다. 하지만 블랙잭 미션으로 머리가 과부하에 걸렸기 때문에 사다리 타기 미션을 진행할때 했던 생각은 진작 다 휘발되었다. 블로그 이름은 휘발 방지용인데... 그래서 이번에는 회고 대신 받았던 피드백과 새로 알게된 내용에 관련한 정리를 해봤다. 앞으로는 내가 미션을 할 때 무슨 생각으로 했는지 틈틈이 대략적인 메모라도 남겨놔야겠다고 생각했다.


 

 


🐣 TDD (Test-Driven-Development)

 

이번 미션의 주된 학습 주제는 TDD 였다. 테스트 주도 개발이라고 하는건데, 내가 프로덕션 코드를 짜기 전에 실패하는 단위 테스트를 구현해 놓고, 이 테스트가 성공할 정도로만 프로덕션 코드를 짜는 사이클을 반복하는 방법론이다. 이렇게 코드를 짜면 내가 지금 구현한 코드가 올바르게 동작하는지를 바로 바로 확인할 수 있게 된다. 여태까지는 main 함수에 출력할 수 있을 정도로 기능을 구현해야 그제서야 콘솔에 찍어보면서 코드가 제대로 돌아가는지 확인해왔는데, 확실히 바로바로 확인 할 수 있어서 심적으로 안정감을 얻을 수 있다는게 TDD의 장점 같다.

프로덕션 코드(Production Code): 프로그램 구현을 담당하는 부분으로 사용자가 실제로 사용하는 소스 코드를 의미한다.
테스트 코드(test code): 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드이다.

 

하지만 이번 페어프로그래밍을 하면서 과연 TDD가 좋은 것인지에 관한 의문을 페어랑 계속 가지기도 했다. 개발 시간은 더 걸리는 것 같고, 처음 설계와 나중 설계가 달라지면서 처음의 테스트 코드가 엎어질 때가 있기도 하고, 처음에 했던 설계가 잘못된 설계였다면 TDD는 무용지물이 될 수도 있을 것 같은데 정말 과연 좋은 것일까 의심이 들어 리뷰어분께 질문을 드렸다.

그래도 TDD로 개발하면 단위 테스트를 조금 더 촘촘하게 작성할 수 있다고 생각해요.
물론 초반에 작성했던 테스트 코드가 잘못되어 있어서 지울수도 있고, 수정할 수도 있어요.
이건 테스트 코드도 유지보수의 대상이기 때문에 일어나는 일이에요.
지금처럼 규모가 작은 애플리케이션을 개발할 때는 TDD가 좋다고 봐요.
그런데, 지금보다 규모가 더 큰 애플리케이션을 개발할 때는 TDD가 무조건 좋다고 보기는 어려운 것 같아요.

 

하긴, 도메인에 대한 테스트 커버리지가 거의 100에 가깝게 나와서 신기하긴 했다.

 

 

그리고 TDD 관련해서 뒤늦게 궁금해진 것이 뒤늦게 하나 생기기도 했다. TDD로 개발하고 커밋할 때, 테스트를 먼저 커밋해야 하나? 아니면 프로모션 코드를 커밋하고 테스트를 커밋해야 하나? 이다. 크루 몇명한테 물어본 결과 양측의 입장은 대충 다음과 같다.

커밋은 프로그램이 정상 작동하는 상태로 커밋해는게 맞다. 테스트를 먼저 커밋해봤자 안돌아가는 테스트기 때문에 프로모션 코드를 먼저 커밋해야 한다.
테스트 코드는 안돌아가도 프로모션 코드가 동작하는 데는 지장이 없으며, 컴파일도 된다. 커밋 메세지에서는 test라 표시하면 사람들도 이를 보고 테스트 코드구나 인지할테니, 테스트 코드 한정해선 테스트 코드는 먼저 커밋해도 된다.

 

사람 취향인 것인지...

 

 

🐣 전략 패턴에 대한 테스트

 

랜덤 값에 대해 테스트를 하기에 전략 패턴이 좋다는 소리만 들어봤지, 자동차 경주 미션에서는 전략 패턴을 사용하긴 했어도 이에 대한 테스트 코드를 짜지는 않았다. 이번 미션에서는 페어 덕분에 전략 패턴에 대해 테스트 코드를 짜볼 수 있었다. 나와 페어는 사다리의 라인("-----") 생성여부를 랜덤으로 반환하는 LineCreateDecider 인터페이스를 만들었고, 이에 대한 구현체를 프로모션 코드에서 사용했다.

 

하지만 테스트를 할 때는 랜덤값을 제어할 수 없기 때문에, 위의 인터페이스를 활용하는 테스트용 구현체를 만들어줘야 한다. 다음은 테스트 코드에 구현된 테스트용 구현체인데, 생성자의 파라미터에서는 우리가 지정한 Boolean 값들을 리스트로 입력받고, 이를 리스트의 앞에서부터 차례대로 반환해준다. 라인("-----") 생성 여부에 대한 제어가 가능해진 것이다.

 

LadderGenerator ladderGenerator = new LadderGenerator(new TestLineCreateDecider(newArrayList(true, false, false, true)));

 

 

 

🐣 view 는 model을 몰라야 한다

 

이번 미션에서 원시값 포장을 하면서, 뷰에 이 포장된 원시값을 전달해도 되는지 안되는지에 대한 고민을 하게 되었다. 페어는 뷰에 모델의 객체들을 그대로 넘기면 안된다는 입장이었다. 뷰에서는 모델에 대해 알면 안되기 때문에, 뷰에서는 getter로 모델에 접근하면 안되고, 쓰더라도 컨트롤러에서 포장된 값들을 다 깔때만 getter를 써야 한다는 것이었다. 뭔가 일리가 있는 말이라 설득이 되면서도, 그러면 왜 포장해준건가? 컨트롤러에서 포장된 값들을 다 뜯으면 컨트롤러가 하는 일이 너무 많아지는 건 아닐까? 하는 생각이 남아있었다. 그래서 리뷰어분께 질문을 드렸더니 다음 답변이 왔다.

가능하면 view에서 getter 사용을 지양하면 좋겠어요.
이리내도 잘 아시겠지만, MVC 패턴에서 Model은 View를 모르고 View는 Model을 모르죠.
만약 view에서 getter를 쓰면, 그때부터 View가 Model을 알게 되기 때문에 아키텍처가 파괴돼요.
MVC 패턴 같은 아키텍처를 적용하는 이유는 유지보수를 더 쉽게 하기 위함인데, 아키텍처가 무너지면 유지보수가 어려워지겠죠?
그래서 getter는 지금처럼 controller와 model에서만 사용하면 좋을 것 같아요.

원시값은 왜 포장해야 할까요? 이리내가 생각하는 원시값 포장의 의미는 무엇인가요?

 

일단, 도메인 영역에서 검증하는 책임을 각각 관련된 원시값을 포장하는 클래스들에게 역할에 맞게 분배해줌으로써, 원시값이 포장된 객체들을 사용하는 클래스에서는 이 객체들에 대한 예외를 별도 생각하지 않고 그냥 객체를 믿고 코드를 짜도 된다는 것이 장점이라고 생각합니다.

그리고 view에 넘겨줄 때도 이게 검증된 값임을 증명하면 훨씬 좋을 거라 생각했던 것 같습니다. 만약 view의 메소드에서 String을 인자로 받으면 이게 검증된 값인지 누가 악의적으로(?) 보내는 값인지를 모를테니까요. 또, Map<Player, Reward> 이렇게 받는 상황과 달리, Map<String, String> 이렇게 받으면 컨트롤러에서는 <player, reward> 이렇게 보냈는데, view에서는 <reward, player>로 코드를 잘못 짰을 경우에도 프로그램이 정상 작동할테니까요.

오 이런 관점에서는 View에 Model을 그대로 넘겨도 괜찮을 것 같네요.
그렇지만 코드 유지보수는 무엇보다 중요하기 때문에 아키텍처 관점에서 여전히 View가 Model을 몰라야 한다고 생각해요!
그래서 View는 파라미터로 넘어오는 값은 잘 검증되었으리라 믿고, 입력/출력에 대한 역할을 충실히 수행하면 될 것 같아요.

 

그래서 dto가 필요한 걸까?

 

 

🐣 에러 메세지를 별도 클래스로 분리해서 모아놓기

 

나는 우테코를 시작하고 나서부터 에러 메세지를 각 클래스 내부에 private enum으로 넣어 놓았다. 에러메세지를 쓰지 않는 곳에서 접근을 막고, 필요한 곳에서만 쓰는 것이 좋을 것이라 생각했기 때문이었다. 하지만 유지보수의 관점에서는 에러메세지를 모아놓는 것이 관리하기 좋다는 피드백을 받았다.

에러 메시지는 프로그램 내에서 공통적으로 사용될 수 있기 때문에(당장 사용되지 않더라도) 별도 클래스로 분리하는 게 일반적인 것 같아요.
각 클래스에 선언하면 에러 메시지가 중복되는 경우에 유지보수가 어려운 문제가 발생할 수도 있어요.
(e.g. 에러 메시지를 수정할 때 에러 메시지가 여러 곳에 분산되어 있으면, 일부 메시지의 수정을 깜빡하는 휴먼 에러가 발생할 수 있음)

 

 

🐣 생성자는 필드 변수만을 파라미터로 갖는 것이 자연스럽다

 

코드를 짜면서 플레이어의 수와, 보상의 수가 일치하는 지 검증해야 할 일이 있었다. 하지만 이를 검증할 최적의 위치가 어디인지 정하지 못하고 결국 위의 형태로 보상 클래스의 생성자의 인자로 플레이어 수를 전달하도록 하였다. 하지만 일급 컬렉션에 리스트만 넘기는게 아니라 부가적인 요소를 저렇게 넘기는 것이 찝찝해서 리뷰어분께 다음 질문을 드렸다.

 

일급 컬렉션은 필드에 멤버 변수가 하나만 있으면 만족하는 것인가요? 이 조건 외에는 모든 것이 자유인가요? 이번에 2단계를 하면서 Rewards(입력받은 실행 결과들을 저장하는 곳) 라는 클래스의 생성자에 파라미터로 List rewards 뿐만 아니라 int playerCount도 넣어주었는데, 이래도 일급 컬렉션을 만족하는 것일까요?

 

결론은 일급 컬렉션을 만족하긴 하지만, 생성자의 파라미터가 부자연스럽다는 것이다.

일급 컬렉션은 딱 1개의 Java Collection(e.g. List, Set ..) 필드를 가진 클래스를 의미해요.
그래서 Rewards가 일급 컬렉션의 조건을 만족한다고 생각해요.

근데 생성자 파라미터로 Rewards에 없는 playerCount를 넘기는 건 조금 어색한 것 같아요.
클래스에서 모든 인자를 갖는 생성자를 만들면, 클래스의 모든 필드를 생성자 파라미터로 받는 생성자가 만들어져요.
Rewards의 경우에는 List rewards만 생성자 파라미터로 들어가게 될 거예요.

 

생성자의 파라미터에는 클래스의 필드들만이 들어가는게 자연스럽다는 소리로 이해하면 될까요?

맞아요!

 

그래서 개선 방법을 찾아보다가 똑똑하신 크루분께서 정적 팩토리 메소드를 활용하는 아이디어를 던져주셨다. 나는 이를 바로 적용시켰고 다음과 같이 코드를 바꾸었다. 이렇게 되면 생성자에는 리스트 하나만 들어가 자연스럽게 되고, Rewards에서는 플레이어 수를 받아서 검증이 가능해진다. 

 

 

 

🐣 재입력을 받을 때 재귀 호출 대신 반복문으로 받도록 개선

 

프리코스 때부터 나는 재입력을 받는 코드를 짤 때 별다른 의심 없이 입력이 실패할 때마다 해당 메소드를 재귀 호출하여 다시 입력받도록 하는 코드를 작성해왔다. 그러다가 이번 리뷰로부터 이 코드에 대한 문제점을 인지할 수 있게 되었다. 리뷰어분께선 다음 코멘트를 남기셨다.

입력이 계속 실패하면 해당 메서드에 대한 재귀 깊이가 깊어져서 StackOverFlowException이 발생할 수도 있지 않을까요?

 

하지만 나는 이 피드백의 의도를 처음에는 잘 이해하지 못했다. 그래서 재귀말고 다른 더 좋은 방법이 있다는 건지, 아니면 예상치 못한 예외처리를 해야했다는 건지 리뷰어분께 다시 물어봤더니 다음 답변을 남겨주셨다.

지난 피드백은 재귀 호출보다 더 좋은 방식이 있을 것 같아 남겼어요!
저는 1. 반복문(while) 활용 2. 재시도 횟수 제한 정도 생각해 봤는데요.
기술로 풀면 1번을 쓸 것 같고, 정책으로 풀면 2번을 쓸 것 같아요.

 

"기술로 푼다", "정책으로 푼다" 이런 문장들이 굉장히 있어보여서 나도 써먹어봐야겠다. 어쨌든 나는 기술로 푸는게 더 흥미로워서 기술로 풀기를 도전했다. 요구사항 중에 depth가 1을 넘기면 안된다는 조건이 있었다. 반복문을 쓰면 일단 depth가 1을 차지하고 들어가기 때문에 이 요구사항을 지키기 까다로웠는데, 결국 옵셔널과 메소드 분리를 활용해서 다음과 같이 코드를 개선해봤다. 

 

(개선 전) 재귀 호출 사용
(개선 후) 반복문 사용

 

지금 하고있는 스터디에서 예외를 아예 던지지 않고 짜는 방법도 있을 것 같다는 이야기가 나와서 그렇게 해보고 싶었지만, 아직 거기는 나의 능력 밖인 듯 하다. 더 열심히 공부해봐야겠다.

 

 

🐣 안전한 동등비교

 

그동안 equals를 사용할 때, == 를 쓸 때 처럼 a.equals(b)와 b.equals(a)가 같은 것일 것이라며 무의식적으로 써왔다. 하지만 equals는 .equals 앞에 있는 객체의 메소드를 호출하는 것이기 때문에 .equals 앞의 객체에 null 이 들어오면 NPE가 발생할 수 있다는 것을 알게되었다. 앞으로는 신경 써야겠다.

 

option이 null이면 어떻게 될까요? 지금은 정상 작동하지만, 비정상적인 입력으로 인해 option에 null이 들어오면 NPE가 발생할 수 있어요. 그래서 동등 비교는 다음과 같이 작성하는 게 안전해요.

 

 

 

 


+

앞으로 적용/공부해 볼 것

테스트 코드 짤 때 give, when, then 써서 테스트 코드도 가독성 좋게 짜봐야겠다.
dto가 뭔지, 어떻게 쓰는 건지 알아는 봐야겠다.

 

 

+

이번에 온보딩 조와 코드 리뷰를 했었는데, 여러 사람이 다른 관점으로 야무지게 코드를 긁어주니 좋았다. 덕분에 코멘트도 폭발하고 깃허브 알림받는 지메일도 폭발했다. 😖😖😖

 

 

 

+

1,2단계 PR 요청들

 

[1단계 - 사다리 생성] 이리내(성채연) 미션 제출합니다. by hectick · Pull Request #122 · woowacourse/java-la

안녕하세요! 리뷰 잘 부탁드립니다. 코드를 작성하면서 궁금했던 사항들이 있어서 질문들을 정리해 보았습니다. 입력 요청 문구를 출력하는 기능은 InputView에 있어야 하나요 OutputView에 있어야

github.com

 

[2단계 - 사다리 게임 실행] 이리내(성채연) 미션 제출합니다. by hectick · Pull Request #155 · woowacourse/

안녕하세요 다니! 2단계 미션 제출합니다 지난 1단계에서 피드백해주신 내용대로 예외 메세지를 하나의 enum안으로 모아보았고, StackOverFlowException을 포함한 다른 예외처리들도 대처할수 있도록 Ex

github.com