post-custom-banner

이어서..

오늘은

1. 핵심 로직(플레이어-컴퓨터 숫자 비교) 작성
2. 테스트 코드 완성

을 목표로 가졌다.

결론부터 말하자면 둘 다 완료했다. 더 나아가서 작성한 테스트 코드를 실제 코드로 옮기고 있는데, 여기서 좀 난관을 겪어서 시간이 많이 지체가 됐다.

test: 핵심 로직(플레이어-컴퓨터 숫자 비교 기능) 작성

어제 고민한 것이,
1. (split() 등을 사용해서) 문자열을 한 글자씩 분리할 것인가?
2. indent depth 를 늘리지 않으면서, 핵심 로직을 어떻게 구현할 것인가?

였다.
두 가지를 모두 충족시키는 코드가 깔끔하고 성능이 좋은 코드라고 생각했다.

그래서 고민을 많이 했다. 일단 2번 사항을 위해서는 이중 for 문 대신 볼 카운트 메서드와 스트라이크 카운트 메서드의 분리가 필요하다고 생각했고, 이 메서드 자체도 for문이 없어야 시간 복잡도가O(n^2) 가 나오지 않는다.
| 그렇게 보이지 않을 수도 있지만, 고민을 많이 한 흔적이다. 번뜩이는 생각은 러프하게 적어야 머리가 복잡하지 않았다.

문자열도 분리를 하고 싶지 않았다. 모든 입력과 컴퓨터의 숫자는 String 타입 으로 받으니, charAt()indexOf() 메서드를 잘 사용하면 split() 메서드와 배열 생성 없이도 가능할 것 같았다.

결국 이 로직을 만들기 위해 결정한 건,

1. charAt() 메서드와 indexOf() 메서드를 활용한다.
2. 스트라이크를 세는 메서드와 볼을 세는 메서드를 분리한다.
3. 스트라이크를 세는 메서드와 볼을 세는 메서드 내부에는 for 문이 없어야 한다.

이다. 고심해서 결국엔 이 조건을 충족하는 코드를 완성했다.

compare() 메서드에서 strikeCount() 메서드와 ballCount() 메서드를 호출한다. strikeCount() 메서드와 ballCount() 메서드 내부에는 for문이 존재하지 않는다.
로직의 원리는, computerNumber (정답)에서 playerInput (플레이어 입력값) 의 i (인덱스, 0~2)번째 문자가 처음으로 존재하는 인덱스를 반환한다.
반환된 인덱스 == i 이면 자리수가 같으니 스트라이크 카운트를 증가시키고,
반환된 인덱스 != i && 반환된 인덱스 != -1 이면 자리수가 다르지만 인덱스가 존재하니 볼 카운트를 증가시킨다.

이렇게 하고 로직의 완성일 줄 알았는데, 생각해보니 테스트를 하기가 애매했다.

일단 나는 저 getter 메서드들을 사용하고 싶지 않았다. getter를 사용하지 않는 연습을 해보고 싶었기 때문이다.

물론 테스트 코드에서는 getter로 얻어온 값을 로직에 사용하거나 조작하지 않고 단순히 조회만 해서 getter를 사용해도 되지만, getter를 사용하지 않고 구현할 줄도 알아야 후에 정말로 getter를 구현하지 않아야 할 때 써먹을 수 있을 것 같았다. 무엇보다 1주차 과제는 레고를 갖고 노는 어린아이의 마음으로 임하기로 했다. 그래서 일단 getter 사용은 없는 것으로 생각했다.

그리고 compare() 메소드는 void 타입이지만, int 타입으로 바꿔 리턴값을 준다고 해도 strike, ball 중 하나밖에 전달할 수 없다.

compare() 메서드 자체에서 값을 전달하게 하려니 스트라이크, 볼 각각의 테스트를 위해 똑같은데 리턴값만 다른 메서드를 하나 더 만들어야 한다.

이 부분을 고민하느라 시간을 매우 많이 사용했다. 사실 getter 한번 쓴다고 해도 뭐라 하는 사람도 없는데, 타협하기는 싫었다. 똥고집

관건은...

결국 관건은 이 값을 동시에 전달해야 하는 것이다. 값을 동시에 전달한다는 것은 결국 객체의 형태로 값을 전달해야 한다고 생각했다. 그래서 StrikeBallCounter 클래스 내부에 CompareResult 라는 내부 클래스를 하나 만들었다.

strikeball 값을 가지는 CompareResult 라는 내부 클래스를 만들고, compare() 메서드가 이 객체를 반환하게 했다. 이 strike, ball 은 생성자를 통해서만 값을 할당받는다.

compare() 메서드는 localStrike, localBall 변수를 독립적으로 가져 직접 strike, ball 의 값을 수정하지 않고 오직 입력값에 대한 출력만 하게 만든다.

클래스의 주 메서드인 compare 는 호출될 때마다 새로운 CompareResult 객체를 반환하므로, 이전 결과에 영향을 주지 않게 되고, 이 CompareReulst 객체가 한번 생성된 후에는 외부에서 strike, ball 의 값을 변경할 수 있는 메서드나 방법이 제공되지 않는다. 따라서 CompareResult 객체는 한번 생성되면 그 상태가 변경되지 않는 불변 객체가 되었다.

이로 인해서 StrikeBallCounter 클래스는 strike, ball 의 값을 한 테스트 코드에 제공하면서도 캡슐화를 유지하게 되었다. 짝짝👏🏻


test: 게임 결과 출력 기능

이건 매우 간단했다. 단위 테스트기에, 그냥 조건문으로 strike, ball 갯수에 따라 결과를 출력하기만 하면 됐다. 간단하고 빠르게 테스트 구현을 완료했다.

test: OutputView

OutputView 클래스에는 게임 시작 메시지, 숫자 입력 요구 메시지, 재시작 메시지 등을 작성했다. 이것도 단위 테스트라서 매우 간단히 끝냈다.

refactor : 랜덤 숫자 생성 기능의 리턴값 타입 변경

이전 테스트 코드에서는 랜덤 숫자를 생성해 전달하는 generateRandomNumber() 메서드의 리턴 타입이 List<Integer> 였는데, 사실 숫자 비교를 위한 StrikeBallCounter 클래스에서 argument로 String 타입을 받을지, int 타입을 받을지는 아직 결정하지 못한 상태였다. 2일차 게시글에는 초반부에 설명을 하긴 했지만, 플레이어 입력값과 컴퓨터 랜덤 생성 값을 전부 String 타입으로 정한 것도 나중의 일이었다.


| 전달하는 리턴값의 타입 흐름도를 그려봤다.

흐름도를 그려보니 타입은 저 List<Integer>만 바꿔주면 될 거 같아서, 이 부분에 대해 리팩토링을 진행했다. RandomNumberGenerator 클래스에 ListString 으로 바꿔주는 메서드를 추가했다.


Controller를 어떻게 작성해야 할까

메인 개발 단계에서 Controller를 작성할 때 고민이 많이 생겨서 섣불리 코드를 작성할 수가 없었다.

  1. 단순히 InputView에서 값 받고 model 에 넘겨주기만 하면 되는 것인가? 그렇다면 model이 InputView직접 값을 가져오는 것과 다른 점이 뭔가?
  2. Controller 가 하나만 있어도 괜찮을까? 가독성이 너무 떨어지지는 않을까? 이 Controller 가 부여 받을 책임은 무엇인가?
  3. View, Controller를 제외한 로직 코드는 전부 model 패키지에 담으면 되는가? 아니면 Service, utils 처럼 또 분리를 해도 되는가? 그럼 왜 mvc 패턴이라고 하지..?

이 의문점들에 대해서 공부하다가 하루를 마무리했다. 내일은 mvc 패턴에 대해 조금 더 공부를 해봐야겠다.


마무리

테스트 코드 작성은 마무리 했으니, 이제 메인 코드로 옮겨서 작동하게 하면 된다. 다만 아직까지도 패턴에 대한 고민이 있고, 내가 작성한 코드중 캡슐화가 약하거나 강한 결합을 하는 코드들은 대대적으로 리팩토링을 해야 된다.

또 우테코에서 주어진 테스트 코드가 어떤 의미인지도 해석을 해봐야 한다. 그 테스트 코드를 통과하는것도 과제 중의 하나이니까. 테스트 코드에 쓰인 우테코 제공 라이브러리들을 한번 봤는데, 매우 복잡했다. 일단은 주어진 테스트 코드의 의미만 파악하고, 과제 후 한번 파고들어 봐야겠다. 꼬리 질문을 계속 하다보면 끝이 없을 것 같아서...

새로 알게된 점

1. inner class may be static
IDE에서 내부 클래스는 정적으로 선언해야 한다는 경고를 줬다. 왜 그런가 알아보니, 비정적 내부 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다고 한다. 외부 클래스로의 숨은 참조가 내부 클래스의 인스턴스에 저장이 되고, 내부 클래스에서 외부 클래스로 접근할 필요가 없으면 이 참조는 쓸모가 없고 메모리 낭비로 이어진다.

2. 불변 객체의 장점
핵심 로직을 만들 때 내부 클래스를 이용해서 불변 객체로 만들었는데, 이에 따른 추가적인 장점이 뭐가 있을지 궁금해서 공부해봤다. 몇 가지만 꼽으면, 스레드 안정성, 예측 가능성, 부작용 방지, 값의 신뢰성 등의 장점이 있다.

좋았던 점

1. 핵심 로직을 완성할 수 있었던 것과 테스트 코드를 완성한 것

2. 캡슐화를 최대한 지킬려고 했던 것
핵심 로직에서 charAt(), indexOf() 를 사용하는건 그리 어려운게 아니었지만, 핵심 로직 클래스를 불변 객체로 만들어 캡슐화 정도를 높이려 했던 게 참 마음에 든다. 애로사항이 없었던 건 아니지만, 덕분에 캡슐화를 좀 더 공부하면서 캡슐화 정도를 높일 다른 코드들도 눈에 보이기 시작했다.

3. indent depth를 계속 1로 유지하고 있는 것

4. 여전히 네이밍에 대해 고민하는 것
새로운 클래스, 메서드, 변수를 작성할 때마다 키보드에 손을 떼고 고민을 하는 습관이 생겼다.

5. 오늘도 몰입할 수 있었던 것

아쉬웠던 점

1. 하루가 어제에 비해 더 짧아진 느낌이다. 회고글 쓰기가 점점 벅찬다는 느낌을 받았다. 그만큼 몰입했다는 증거겠지만, 그래도 회고글은 매일 쓰고 싶다.

도움이 된 자료들

| 🩹Inner class may be 'static'
| [디자인 패턴] MVC 패턴
| [우아한 테크 코스 5기] 여러분은 MVC에 대해서 얼마나 알고 계신가요?
| MVC(Model, View, Controller) Pattern
| OOAD - 진정한 캡슐화 (잘못된 캡슐화 예제)
| OOP 캡슐화 & 정보 은닉 개념 완벽 이해하기

profile
자바 백엔드 개발자
post-custom-banner

0개의 댓글