[우테코 6기 프리코스] 1주차 고민 해결 과정

별의개발자커비·2023년 10월 19일
0

우테코 도전기

목록 보기
24/37
post-thumbnail

개요

1주차 미션을 진행하면서 그 때 그 때 궁금한 점을 해결하는 과정을 정리해보려고 한다.

🔎 CLI와 GUI

git 기본 사용법에 대해 디스코드에서 이야기가 나와서 평소에 궁금했던 걸 질문해보았다.

그리고 답변을 봤는데, CLI와 GUI가 뭐지...?

CLI(Command-Line Interface)

입출력만을 이용해서 컴퓨터와 소통

GUI(Graphic User Interface)

사용자가 눈에보이는 그래픽(아이콘)으로 컴퓨터와 소통

라고 한다!ㅎㅎ

참고글

https://velog.io/@jellyjw/CLI-GUI의-차이-및-CLI-기본-명령어-정리


🔎 기능 구현 목록 작성 후, 뭐부터 구현해야할까?

공들여서 READ.ME를 작성하고 나서, 순간 멍해졌다. 뭐부터 구현해야하지?
일단 생각이 들었던 건 아래의 두가지 방향이었다.

1. 기능 구현 목록 순서 상 맨 앞 메소드

즉, 프로그램 작동 흐름 순서상 가장 앞에 일어나는 기능을 먼저 구현한다.

📌 문제점

객체를 중심으로 가는 게 아니라 절차적 프로그래밍 방식이라는 느낌이 든다. 테스트가 쉬운 코드 먼저 구현해본다. 라는 TDD의 내용도 걸리기도 하고!


2. Test가 쉬운 것

TDD 강의에서 입력과 출력이 명확한 게 테스트하기 편하므로, validate와 같은 것들을 먼저 구현하라고 했던대로 입력에 대한 validate 먼저 작성한다.

📌 1F: 문제점

숫자야구의 숫자라고 치면 보통 입력받은 숫자를 바로 객체로 만들지 않는다.
예를들어,
InputValidate(전반적인 입력 형태 검증) → InputConvertor (→ 때에따라 Dto도)

이런 과정을 거칠 때가 있는데, 그렇다면 이 validate가 어디에 위치해야하는지 확정하지 않은채로 일단 기능별 validate를 만들게 된다.

2F: validate를 일단 만들고 나중에 객체별로 나눠도 되지 않을까?

생각해보면 사실 validate를 일단 쭉 만들어놓고 나중에 책임별로 객체들에 나눠도 되겠다 싶기도 하다.
그 과정에서 convert 마찬가지로 일단 만들어 놓고 나중에 나눠볼까 한다!

일단, 2번. Test가 쉬운 것부터 해보자!

물론 상황에 따라 다를 수도 있고, 뭐가 더 맞는지 모르겠지만 그나마 덜 걸리는 2번으로 먼저 해보려고 한다! 과연 좋은 선택이었을지... to be continue...

추가로 우테코 커뮤니티에서의 관련 토론글

내부 로직을 이후에 구현하고, 내부 로직 없이 입, 출력만 반환하도록 정의한다는 답변이 뒷받침될 수 있을 것 같다.


🔎 스트림 사용

아직 스트림 사용이 능숙하지 않아서 필요한 사용법이 있을 때마다 찾아서 사용해보고 정리해보고 있다!

스트림에서 isPresent처럼 쓰는 anyMatch!

이런 의도로 쓰고 싶었는데 찾아보니 이 때에는 anyMatch를 쓰면 된다고 한다.
filter에 의해 걸러진 객체가 하나라도 존재하면 true를 반환하고, 그렇지 않으면 false를 반환한다!

distinct로 중복 확인

세자리 숫자 중에 중복된 숫자가 있는지 검증하는 과정인데 너무 단계별로 메소드가 많다고 느껴졌다. 스트림을 활용해 더 줄일 수 있을 것 같은데... 하고 찾아보던 중

생각의 전환!
리스트 내 중복된 숫자를 찾으려고 하기보다, 반대로 distinct를 활용해 리스트 내 unique한 숫자가 3개인지 확인하면 간결해질 수 있었다!

그래 복잡할 때는 반대로 생각해보기!

스트림을 하다 숫자 관련 고민이 생기면 intStream 쓰기

원래 이런 형태를

이렇게 고쳐보았다.

Stream.generate()

: 리스트나 숫자가 정해져있지 않은 상태에서 stream 생성하는 경우


🔎 get을 안쓰고 객체에 메시지를 보내려고 애써보는 중!

get을 최대한 지양하려고 하다보니 메시지를 보내는 방식으로 진행하게 되고,
그러다 보니 메시지를 보내는 과정에서 필요한 메소드들을 만들다보니 이렇게 메소드가 늘어나게 되었다.

예를들어, 아래는 정답 Balls 중에서 숫자가 일치하는 경우인 BALL 갯수를 알아내는 기능을 위해 구현 메소드들이다.

Balls 클래스

1F

가장 바깥층은, enum으로 관리하는 TryResult로 비교 결과를 받아서 ball의 숫자를 받는 형태이다.

2F

각각 스트라이크, 볼인지 확인하고 enum인 TryResult로 결과를 반환해준다

3F

확인하는 과정이 이 안을 거쳐서 일어난다.
answerBalls에 get을 쓰지 않기 위해서 answerBall을 객체로 하고 playerBall을 메소드의 변수로 하여 처리해주는 방식으로 연결해주었다.

4F

balls(정답Balls)를 돌면서 정답ball이 playerBall과 숫자가 같은 게 하나라도 있는지 여부를 반환한다.

Ball 클래스

answerBall의 숫자를 get으로 가져와서 playerBall의 숫자를 get한 것과 비교하면 쉽겠으나... 그렇게 쓰이는 get을 쓰지 말자는 것이 목적이기 때문에!

1F

Ball(answerBall) 객체에서 정답Ball의 number를 갖고

2F

플레이어Ball객체에 같은 숫자인지 비교하는 메시지를 보냄으로써
이 역시도 get 없이 구현을 해보았다.

복잡도 고민

일단 get을 쓰지 않겠다는 소기의 목적은 달성했으나,
get을 피하기 위해 구상한 메소드안 3F, 4F 부분이 조금 복잡하지 않나라는 고민이 든다.

결국 복잡도를 낮추게 보완!

뒤의 🔎 Balls 클래스가 너무 큰데...? 를 해결하면서 3F, 4F 부분의 복잡도 문제를 해결하였다!


🔎 랜덤 생성 객체들의 2단 분리!

처음에는 random 번호를 받아 Ball을 생성하는 클래스 하나로 만들었다가 문득 책임에 대한 생각이 들었다.

지금 이 클래스는 2개의 역할을 하고 있는 것 아닌가?
1. 랜덤 넘버를 생성하는 것과, 2. 그 결과 숫자로 공을 생성하는 것

그러면서 각각의 역할이 해야하는 추가 작업들이 있었기에 자연스레 객체 분리의 신호라고 느껴지며 분리를 하게 되었던 것 같다!

더불어 객체 분리를 하니 훨씬 테스트 하기도 좋은 코드가 되어 객체 분리의 적절한 시점이었다고 느꼈다!

RandomNumberGenerator

RandomBallsGenerator

🔎 private 메소드를 테스트하고 싶으면 어떻게하지?

방법 1: default 접근 제어자를 쓴다.

📌 문제점

private 메소드일 수 있는데 테스트를 위해서 default로 만드는 게 맞을까?
테스트라는 목적이 접근 제어자를 private에서 default로 바꿀 수 있는 이유가 될까?라는 의문이 든다.

방법 2: 테스트용 default 메소드를 거쳐서 부른다.

📌 문제점

로직에 실제 동작과는 상관 없는 테스트만을 위한 코드가 추가되게 된다.

일단, 방법 2 테스트를 위한 default 메소드를 추가했다.

나중에 pr리뷰 때 의견을 들어보고 싶다!


🔎 Balls 클래스가 너무 큰데...?

현재 스트라이크, 볼 등의 비교결과 반환까지 구현한 Balls 클래스인데

Balls에 대한 valide부터 시작해서, 스트라이크, 볼인지 확인하고 갯수를 구하는 메소드까지 이 안에 구현이 되어있다.

흠... 일단 Balls를 갖고 계산을 해야하기 때문에 여기에 구현을 했는데,
과연 스트라이크, 볼 결과 확인이 Balls의 책임인가 하는 고민이 들었다.
더불어 클래스가 너무 커진다는 것은 객체 분리의 신호이기 때문에!

그렇다면, 어떤 역할의 객체가 분리되어야할까?
일단, 문제점으로 보였던 스트라이크, 볼 결과 확인 관련 메소드를 책임져줄 객체가 필요할 것 같다.

메소드 리팩토링으로 해결

스트라이크, 볼을 따로 구하지 않고 스트림을 적용한 메소드 2개로 구할 수 있게 리팩토링하니 Balls가 판단에 관여하지 않게 책임을 줄여줄 수 있었다!

이렇게 아래부분이 다 필요없어졌다ㅎㅎ


🔎 스트림의 남발인걸까?

3개의 스트라이크, 볼 등의 결과를 담은 리스트에서 3스트라이크인지 확인하는 메소드를 작성하다 고민이 들었다.

처음에 스트림의 allMatch를 사용하면 간단하겠다는 생각이 들어 작성을 해보았는데, 문득 스트림을 사용하는 것이 성능에 더 안 좋다는 이야기가 떠올랐다.

길이가 3인 리스트를 도는 것이라 간단한 연산인데 스트림을 사용하는 게 과한 걸까라는 질문이 들었고, 그래서 스트림을 사용하지 않고 forEach문을 사용한 버전으로도 작성해보았다.

확실히 스트림을 사용한 것이 가독성이 좋긴한데 성능면에서 차이는 어떨까?
GPT의 의견이 궁금했다!

길이가 3인 리스트를 도는 것이라 간단한 연산인데 스트림을 사용하는 게 과한 걸까라는 나의 질문과는 반대로, 오히려 작은 크기의 연산에서는 성능 차이가 미미하다는 답변을 받았다. 오호! 그러면 내 경우에는 스트림을 사용하는 것이 더 좋은 방법이겠군!🤔

추가로 답변의 스트림 사용으로 오버헤드가 있을 수 있다는 게 무슨 말인지 모르겠어서 추가질문!

  • 오버헤드 : 프로그램의 실행시간에 추가적인 부하나 비용이 드는 것
    • 시간 오버헤드: 스트림이나 람다에서 내부적으로 발생하는 추가작업으로 실행 시간이 늘어날 수 있음.
    • 메모리 오버헤드: 스트림이 내부적으로 객체를 생성하므로 생길 수 있음.

🔎 이 기능이 이 객체의 책임이 맞을까?

컨트롤러에 3스트라이크인지 확인하는checkGameWin 메소드를 구현하고 이에 대한 commit 메시지를 적으면서 생각이 들었다.

  • 컨트롤러에서 3스트라이크인지 확인하는 게 컨트롤러의 역할 상 맞을까?
  • 이걸 판단하는 객체가 있고 그 객체의 역할이 아닐까?
  • 하지만 그 객체를 만든다면 checkGameWin밖에 할 일이 없는데 그래도 만드는 게 맞을까?

라는 생각의 흐름끝에 우선은 컨트롤러에 놔두고, 마지막에 메소드들을 살펴보며 게임 판단 객체에 들어갈만한 다른 메소드들이 더 생기면 분리를 고민해보려고 한다!

분리 시점 생겼다!

  1. OutputView에서 이 작업을 하는 게 과하다는 생각이 들었고
  2. List<TryResult>의 일급 컬렉션으로 변환이 필요한 시점

이 두가지 사항의 반영이 필요했는데,
이걸 해결할 수 있는 방법이 아까 말했던 게임 판단에 해당하는 클래스다! 바로 생성했다!

이렇게 고민되었던 checkWin도 새로 만든 GameResult 클래스에서 처리해주는 것으로 분리했다!

🔎 여러 요구들이 맞닿는 한 지점이 객체 생성일 때!

우테코 디스코드에서 클린코드, 객체지향적 코드라면 신문기사처럼 읽히는지를 보면 좋다고 한 이야기를 듣고 코드를 훑어보는데 눈에 걸리는 곳이 있었다.

게임을 새로 시작할 때마다 정답공과 게임상태를 새로 설정해 play메소드에 넣어주고 있었는데,

  1. 이 정답공과 게임상태가 어떤 하나로 묶이지 않고 play에 왔다갔다 하는 게 걸렸다
  2. 사실 이 곳은 이전에 init이라는 메소드로 묶고 싶었는데 메소드 분리가 애매해서 놔뒀던 곳이기도 하다.

이렇게 여러 요구들을 곰곰히 생각해보니 공통으로 가리키는 방향이, 정답공과 게임상태를 관리하는 게임진행객체를 생성하는 것이었다!

생성 후 )

BaseballGame 객체로 묶어주면서 여러 인자를 전달할 필요가 없어졌고, 어떤 목적의 인자를 전달하는지도 명확해졌다!

while 조건인 게임진행상태도 BaseballGame에게 isPlaying 메시지를 보내 확인할 수 있게 바뀌었다!

참고: 새로 분리한 BaseballGame 객체

🔎 enum에도 메시지 보내기로!

원래 이렇게 직접 비교했던 부분을 아래처럼 enum에 메시지를 보내는 형식으로 바꿀 수 있었다!

메시지의 책임은 어느쪽일까? 객체? view?

기존 버전

기존에는 볼, 스트라이크 등의 메시지를 outputView에서 관리했다.

📌 장점

  • 출력 부분에서 출력 메시지 관리가 된다.
  • 상수화 되어있어 코드가 단순하다.

📌 단점

  • 시도 결과에 대한 메시지인데 객체에서 벗어나 출력 부분에서 관리하고 있다.
  • 따라서, 추후 수정 사항이 생기면 tryResult 객체만 수정하는 게 아니라 이 view쪽도 수정해줘야한다.

변경한 버전

볼, 스트라이크 등의 메시지를 해당 enum 객체인 TryResult에서 enum의 필드로 관리한다.

📌 장점

  • 해당 객체에서 메시지를 관리하기 때문에 추후 수정사항이 생겨도 이 안에서만 수정하면 된다.
  • view에서 상수화했던 메시지가 줄어든다.

📌 단점

  • 출력 부분을 확인하다 해당 메시지가 뭔지 알고 싶으면 객체로 들어와서 확인해야한다.
  • 객체에서 해당 메시지를 getter로 가져와서 출력해야하기 때문에 코드 가독성이 떨어진다.

결론

바꿨지만 여전히 고민이 든다.

  1. 이 메시지의 책임이 어디에 더 적합할까?
  2. 만약 나중에 다른 상태가 추가되거나 출력되야하는 표현이 바뀌는 경우에는 어느 쪽이 유지 보수가 편할까?

0개의 댓글