[우아한테크코스] 프리코스 4주차 회고

정균·2022년 11월 22일
0

우아한테크코스

목록 보기
4/15
post-thumbnail

4주차 미션 - 다리 건너기

미션 레포지토리

🚀 기능 요구 사항

위아래 둘 중 하나의 칸만 건널 수 있는 다리를 끝까지 건너가는 게임이다.

  • 위아래 두 칸으로 이루어진 다리를 건너야 한다.
    • 다리는 왼쪽에서 오른쪽으로 건너야 한다.
    • 위아래 둘 중 하나의 칸만 건널 수 있다.
  • 다리의 길이를 숫자로 입력받고 생성한다.
    • 다리를 생성할 때 위 칸과 아래 칸 중 건널 수 있는 칸은 0과 1 중 무작위 값을 이용해서 정한다.
    • 위 칸을 건널 수 있는 경우 U, 아래 칸을 건널 수 있는 경우 D값으로 나타낸다.
    • 무작위 값이 0인 경우 아래 칸, 1인 경우 위 칸이 건널 수 있는 칸이 된다.
  • 다리가 생성되면 플레이어가 이동할 칸을 선택한다.
    • 이동할 때 위 칸은 대문자 U, 아래 칸은 대문자 D를 입력한다.
    • 이동한 칸을 건널 수 있다면 O로 표시한다. 건널 수 없다면 X로 표시한다.
  • 다리를 끝까지 건너면 게임이 종료된다.
  • 다리를 건너다 실패하면 게임을 재시작하거나 종료할 수 있다.
    • 재시작해도 처음에 만든 다리로 재사용한다.
    • 게임 결과의 총 시도한 횟수는 첫 시도를 포함해 게임을 종료할 때까지 시도한 횟수를 나타낸다.
  • 사용자가 잘못된 값을 입력한 경우 throw문을 사용해 예외를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

추가된 프로그래밍 요구사항

  • 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.
  • 메서드의 파라미터 개수는 최대 3개까지만 허용한다.
  • 아래 있는 InputViewOutputViewBridgeGameBridgeMaker 클래스(또는 객체)의 요구사항을 참고하여 구현한다.
    • 각 클래스(또는 객체)의 제약 사항은 아래 클래스별 세부 설명을 참고한다.
    • 이외 필요한 클래스(또는 객체)와 메서드는 자유롭게 구현할 수 있다.
    • InputView 에서만 MissionUtils의 Console.readLine() 을 이용해 사용자의 입력을 받을 수 있다.
    • BridgeGame 클래스에서 InputViewOutputView 를 사용하지 않는다.

🎯 4주차의 주요 목표

1. 클래스(객체)를 분리하는 연습

2. 리팩토링

✏️ 4주차 학습과정

1. 객체를 객체 답게

3주차 피드백에서 '객체를 객체답게'라는 피드백이 있었다.

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

// 피드백
class Lotto {
   #numbers
 
   constructor(numbers) {
       this.#numbers = numbers
   }
 
   contains(number) {
       // 숫자가 포함되어 있는지 확인한다.
       return ...
   }
 
   matchCount(other) {
       // 당첨 번호와 몇 개가 일치하는지 확인한다.
       return ...
   }
}
 
class LottoGame {
   play() {
       const lotto = new Lotto(...)
 
       lotto.contains(number)
       lotto.matchCount(...)
   }
}

객체 관련 로직을 다른 곳에서 수행 하도록 하지 말고, 최대한 그 객체 안에서 로직을 구성하라고 한다.

나의 3주차 Lotto 클래스를 보자

class Lotto {
  #numbers;

  constructor(numbers) {
    this.#validate(numbers);
    this.#numbers = numbers;
  }

	.
	.
	validate 함수들
	.
	.

  #validate(numbers) {
    this.#validateNumberLength(numbers);
    this.#validateNumberRange(numbers);
    this.#validateNumberDuplicate(numbers);
  }

  get numbers() {
    return this.#numbers;
  }
}

나의 Lotto 클래스에는 validate 관련 함수를 제외하면 getter 밖에 없다.

Lotto 관련 로직을 외부에게 모두 맡겨 버린 것이다.

객체를 객체답게 사용하지 못했다.

객체를 객체스럽게 사용하려면 어떻게 해야할까?

피드백과 관련 자료들을 찾아본 결과 공통적으로 언급되는 점은 다음과 같다.

  • 객체 스스로 로직을 수행하도록 하라
  • 객체에게 메시지를 보내라
  • getter, setter를 지양해라

getter를 사용하지 마라..?

이번 과제를 진행하면서 가장 많이 고민했던 부분이다.

이전까지는 getter를 사용해 클래스의 데이터를 주고 받고 하는 로직을 당연하게 생각해왔다.

그렇기 때문에 처음 구조를 설계하는 이틀 정도는 클래스 필드를 주지 않고 어떻게 클래스끼리 상호작용을 할지 전혀 생각이 나지 않았다.

클래스 필드의 값을 반환하는 getter 대신 다른 방법은 무엇이 있을지 계속해서 고민했다.

클래스들의 변화 과정

BridgeGame 클래스의 초기 로직

class BridgeGame {
  #bridge; // array
  #passedPath; // array
  #tryCount;

  constructor(bridge) {
    this.#bridge = bridge;
    this.#passedPath = [];
    this.#tryCount = 1;
  }
  .
  .
  .
}

초기 BridgeGame에는 클래스 분리가 되어있지 않았다.

그냥 array 타입의 #bridge와 #passedPath 였다.

getter를 사용하지 못하다보니 처음에는 클래스 하나로만 이루어져 있었다.

하지만 이번 주차의 주요 목표 중 하나가 클래스(객체) 분리이므로, 객체 분리를 시도했다.

무슨 클래스가 필요할까?

아무래도 다리 건너기 '게임'이다 보니 다음과 같이 도출했다.

  • 현재 위치를 가지고 있는 '캐릭터' 역할의 객체
  • 캐릭터 객체가 다닐 '맵' 역할의 객체

나는 이 캐릭터와 맵을 각각 Player 클래스와 Bridge 클래스로 이름을 짓고, 해당 클래스들을 구현했다.

그러나 Player 클래스는 곧 삭제했다.

getter를 사용하지 못하다보니 Player-BridgeGame-Bridge 세 객체 사이의 상호작용이 너무 어려웠다.

Player 클래스를 삭제하고 BridgeGame-Bridge 두 클래스만 남겨뒀다.

현재 플레이어의 위치 관련 로직은 BridgeGame에서, 다리 관련 로직은 Bridge 클래스에서 수행하기로 했다.

Bridge 클래스

class Bridge {
  #bridge;
  
  constructor(bridge) {
  	this.#bridge = bridge;
  }
  .
  .
  .
}

초기의 Bridge 클래스에서는 이동 경로 지도 가져오기, 해당 위치의 안전한 칸 가져오기 등의 함수가 포함되어 있었다.

그러나 이동 경로에 대한 로직은 Bridge 클래스와 맞지 않는 별개의 로직이라는 생각이 계속 들었고,

플레이어의 현재 위치 관련한 로직이 BridgeGame에 있는 점도 계속 마음에 거슬렸다.

해당 로직은 Player 객체가 맡는게 맞지 않나 생각이 계속 들었다.

Bridge 클래스를 다루면서 getter를 대신하는 여러 함수들을 생성하는 것도 좀 익숙해진 듯 했기 때문에 클래스를 더 늘려도 될 것 같은 자신감이 들었다.

결국, Player 클래스 부활

// 초기 Player 클래스
class Player {
  #passedPath
  #out
  
  constructor() {
    this.#passedPath = [];
    this.#out = false;
  }

이동 경로와 탈락 상태를 가지고 있는 Player 클래스를 다시 만들었다.

Player 클래스에는 이동하기, 이동 경로 가져오기 등 위치 이동 관련 로직들을 갖추고 있었다.

#out필드는 플레이어가 잘못된 길로 갔을 때 더이상 move를 못하게 하거나, 이동 경로에 X 표시를 하는 것과 같이 플레이어 탈락과 관련된 로직을 수행한다.

그러나 추후 리팩토링 과정에서 충분히 #passedPath을 활용한 함수 로직으로 #out 상태를 가져올 수 있다고 느꼈고, #out 상태는 빼고 함수로 대체했다.

(현재는 out이라는 네이밍이 좀 어색해서 플레이어를 탈락시키는 함수는 fall(), 탈락했는지 확인하는 함수는 isFallen()로 네이밍을 바꿨다.)

// 현재 Player 클래스
class Player {
  #bridgePath
  
  constructor() {
    this.#bridgePath = {upperBridge: [], lowerBridge: []}
  }

Player - BridgeGame - Bridge 구조

엄청난 고민과 삽질 끝에 Player-BridgeGame-Bridge 세 클래스들의 상호작용의 구색을 어느정도 갖췄다.

  • Player에는 이동 경로 관련 로직(이동하기, 이동 경로 가져오기, 현재 위치 가져오기 등)
  • Bridge에는 다리 관련 로직(다리의 마지막 칸인지 확인, 다리 다음칸 방향 가져오기 등)
  • BridgeGame 에서는 Player 클래스와 Bridge 클래스를 서로 결합하고, 게임을 자연스럽게 진행하는 로직

사실 글로는 금방 금방 수정했던 것처럼 썼지만, 실제로는 정말 많은 고민과 시도 끝에 현재의 구조를 가질 수 있게 되었다.. 커밋 기록

이런 클래스 구조 설계 과정을 거치면서 객체스러운 객체란 무엇인지 조금 더 이해할 수 있게 되었다.

객체 설계는 마치 어려운 퍼즐을 푸는 과정처럼 느껴졌다.

처음에는 막막했지만, 조금씩 큰 그림이 완성되면서 방향을 찾을 수 있었다.

좋은 개발자가 되기위해 이런 구조 설계 역량을 많은 연습을 통해 키워야겠다.

2. 비즈니스 로직과 UI 로직

MVC 패턴


이번 주차의 요구 사항으로 View 객체들이 따로 주어졌다.

또한, 3주차 피드백으로 비즈니스 로직과 UI 로직을 분리하라는 피드백도 있었다.

평소 프리코스 커뮤니티에 백엔드 지원자분들이 MVC 패턴에 대해 자주 언급 하는 것을 들었었고, 나도 이번 주차에 위 요구 사항도 맞출겸 MVC패턴을 도입을 시도해봤다.

무슨 클래스를 컨트롤러로 하지?

MVC를 설계하는 과정에서 가장 고민했던 점은 어디서 컨트롤러 역할을 맡아야 하는지에 대한 고민이었다.

  • 이전 주차에서 하던 것처럼 App 클래스에서 전체적인 앱 진행을 맡으면서 컨트롤러 역할을?
  • 요구 사항에서 나온것 처럼 게임을 관리하는 클래스인 BridgeGame 클래스에서 컨트롤러 역할을?

BridgeGame을 Controller로!

기본 스켈레톤 코드 속 주석에 게임을 '관리'하는 클래스라고 적혀있었다.

컨트롤러는 곧 전체적인 관리를 해야하는 역할이라고 생각했고,

BridgeGame 클래스를 컨트롤러로 하기로 정했다.

그러나..

게임에 관한 여러 클래스를 연결해주는 BridgeGame 클래스에서 연관된 클래스 구조 설계가 너무 어려웠고, UI 로직까지 들어가다보니 구조가 엄청나게 복잡해졌다.

결국 BridgeGame을 컨트롤러로 하는 것을 포기했고 그냥 기존처럼 App 클래스를 컨트롤러로 정했다.

알고보니 원래 부터 BridgeGame에서 컨트롤러 역할은 할 수 없었다..

추후에 요구 사항을 잘 읽어보니 위와 같은 요구 사항이 있었다.

어차피 BridgeGame에서 View를 관리하지 못하므로 컨트롤러 역할은 못했다.

컨트롤러의 위치 고민을 꽤 오랫동안 했었는데 이 과정이 큰 의미가 없었던 것이었다.

역시 요구 사항을 잘 읽어봐야 시간도 줄일 수가 있다..

MVC 패턴에서 입력 검증은?

MVC 패턴을 시도해보면서 또 고민 했던 점이 있는데, 입력 검증은 어디서 해야하는 것인지 의문이 들었다.

  • 입력은 받는 곳인 View에서 해야하나?
  • 검증 '로직'이 들어가므로 Model에서 해야하나?
  • 둘 사이의 연결 점인 Controller에서 해야하나?

구글링을 해본 결과, 셋 중 모범 답안은 없다고 한다. SOF

개인적으로 뷰에서는 로직을 최대한 줄이고 싶었기 때문에 뷰는 배제하고 생각했다.

컨트롤러? 모델?

남은건 컨트롤러와 모델이었는데, 아직 클래스 분리가 익숙하지 않기 때문에 여기에 입력 검증 로직까지 추가하면 더 힘들어 질 것 같았다.

또한, 함수 별 10라인 제한이라는 요구사항도 있기 때문에 모델에서는 게임 관련 로직만 집중하도록 하고, 컨트롤러에서 입력 검증을 하기로 했다.

//App.js
requestBridgeSize() {
    InputView.readBridgeSize((size) => {
      if (!ErrorHadler.tryValidate(InputValidator.checkBridgeSize, size)) {
        this.requestBridgeSize();
        return;
      }
      this.setBridgeGame(size);
    });
  }

컨트롤러 다리 길이에 따른 입력 검증을 추가한 코드이다.

(App으로 입력 검증을 맡겼다고 해서 10줄 이하로 리팩토링 하는 것이 쉬워지지는 않았다.. 오히려 App 리팩토링 과정이 제일 힘들었다.. 차라리 모델에서 했어야 했나..)

3. 함수 라인 10줄 제한

사실 이 요구사항을 처음 봤을 땐 그렇게 어렵진 않을거라 생각했다.

이전 주차에서 15줄 제한이 있었는데 이 과정이 크게 어려웠지 않았기 때문이다.

그러나 추후 리팩토링을 진행하면서 이 생각은 점점 바뀌었다.

10줄의 기준이 어디지?

10줄의 기준으로 중괄호를 라인에 포함 시켜야 하는지 고민이 들었다.

foo() { // 1. 여기부터 시작인가?
 // 2. 여기부터 시작인가?
  
 // 2. 여기가 끝인가?
} // 1. 여기가 끝인가?

처음엔 2번인 줄 알았으나, 프리코스 커뮤니티를 보니 1번을 기준으로 한 사람도 많았다.

나는 조금 더 확실하게 하고 싶어서 1번을 택했다.

사실상 8줄 제한이었다.

15줄 제한과 10줄 제한은 꽤 큰 차이였다

10줄 제한에서 코드를 한줄 추가하는 것이 너무 큰 부담이었다.

'이 로직은 반드시 이 함수 안에 들어가야 하는데 여기서 어떻게 더 쪼개야하지?'

라인 줄이기 리팩토링을 하면서 계속 들었던 생각이다.

10줄을 맞추기 위해서 생각보다 많이 줄여야 했다.

무작정 함수를 분리하면 오히려 함수의 구조를 이해하기 더 힘들었다.

가독성과 10줄 제한을 동시에 지키기 위해서 줄이는 과정이 많이 험난했다.

try-catch

10줄 제한을 하면서 가장 골머리를 앓게 한 친구는 try-catch 였다.

이번 주차에서는 지난 주차와 다르게 입력 검증에 실패하면 에러를 throw하는 것 뿐만 아니라 해당 입력을 다시 받아야 했다.

일단 에러를 잡고 에러 메시지를 띄워주기 위해선 반드시 4줄이 필요했다.

try{
} catch (error) {
  print(error.message);
}

이를 해결하기 위해 try-catch 문을 가진 함수를 만들었다.

tryValidate(validate, input) {
  try {
    validate(input);
    return true;
  } catch (error) {
    OutputView.printErrorMessage(error.message);
    return false;
  }
},

try문에서 validate 함수를 실행하고 true를 반환한다.

validate 함수를 실행하는 과정에서 에러가 throw 되면 catch 해서 메시지를 띄우고 false를 반환한다.

위 함수를 사용해서 입력 검증을 하는 로직은 다음과 같다.

if (!ErrorHadler.tryValidate(InputValidator.checkBridgeSize, size)) {
  this.requestBridgeSize();
  return;
}
this.setBridgeGame(size);

에러가 발생하면(tryValidate가 false를 반환) 다시 입력(requestBridgeSize())을 받는다.
에러가 발생하지 않는다면, 다음 로직(setBridgeGame)을 수행한다.

tryValidate()에 대한 아쉬움

에러 발생하거나 하지 않았을 때 로직을 tryValidate() 함수 내에서 수행하고 싶었지만,

if문 안의 return을 발생하여 함수를 종료 시키는 로직을 전달해주는 방식을 어떻게 대체 해야할지 몰라서 일단은 위와 같은 방식으로 진행했다.

개인적으로 이번 과제에서 가장 아쉬운점 중 하나는 위 방법을 끝내 해결하지 못해서 App 클래스의 입력 로직이 조금 지저분하다는 점이다.

우테코 프리코스 끝

4주의 시간이 엄청나게 빠르게 지나갔다.

올해 들어 가장 바쁘게 보냈던 4주가 아닐까 싶다.

최근 들어 집에서 집중이 잘 안될때 마다 주로 스타벅스를 가서 코딩 하곤 하는데, 프리코스 하는 동안 골드 레벨을 찍었다..!

하고 싶었던 것도 한달간 잠시 미루고 매일 같이 프리코스 공부만 한 것 같다.

프리코스 미션 과제 뿐만 아니라 클린 코드부터 시작해서 클래스, 프로토타입, MVC 패턴 등등 과제 하면서 궁금했던 개념들에 대해서도 이번 기회에 정말 많이 배웠다.

이번 프리코스를 하면서 얻어간 것 중 가장 값진 것은 아무래도 자기주도 학습함께 자라기에 대한 방법인 것 같다.

두 키워드는 서로 어울리지 않는 개념인 것 같지만,

다른 사람이 알려주는 것에 의존하는 수동적인 학습법에서 벗어나 스스로 파고 들어 삽질도 하고 고민도 하면서 학습하는 방법을 배웠다.

이렇게 배운 내용을 다른 사람에게 공유하고 피드백 해주며, 때로는 자극도 받으면서 함께 성장하는 것의 중요성 또한 배웠다.

짧은 한 달동안 많이 성장했다고 느꼈고, 앞으로의 성장에도 방향을 잡는데 도움이 많이 될 것 같다.

프리코스에서 4주 동안 몰입한 경험을 살려 멈추지 않고 나아간다면 내가 바라는 뛰어난 개발자가 되어있을 것이라고 생각한다.

profile
💻✏️🌱

1개의 댓글

comment-user-thumbnail
2022년 12월 2일

정균님 덕분에 많이 배웠습니다 👍

답글 달기