[우아한테크코스 6기 프리코스] 4주차 회고 (크리스마스 프로모션)

재오·2023년 11월 15일
11
post-thumbnail

이 글은 우아한테크코스 6기 프리코스 4주차 미션 종료 직후 작성된 회고 글입니다.

스스로 고민한 내용이 글에 많이 포함되어 있기 때문에 본인 코드에 대해 충분히 고민한 후 읽으시는 것을 추천합니다.


🎯 시작 전

본격적으로 미션을 시작하기 전에 3주차 공통 피드백을 읽어보았다. 정말로 피드백 하나 하나가 나를 겨냥해서 작성된게 아닐까 싶을 정도로 3주차 미션에 내가 작성했던 코드 방식에 단점이 있었다. 특히 프리코스를 하는 동안 꾸준히 나를 고민하게 했던 객체 지향테스트 케이스에 관해서 자세히 설명이 되어있었다. Tecoble 이라는 우아한테크코스 사람들이 작성된 고퀄리티의 블로그 글도 링크에 첨부되어있어서 내 고민을 조금 덜어줄 것 같았다.

3주차에 객체 지향적인 코드를 어느정도 이해를 했다고 생각했지만 피드백을 읽어보니 아직 한참 멀었다는 것을 깨달았다. 한번 더 겸손해지는 시간이었던 것 같다. 4주차 미션을 서둘러서 시작하는 것보다는 하루가 걸리더라도 이 피드백에 대해 숙지하고 코드에 접목시키고 싶었다.

Tecoble 글과, 공통 피드백을 읽으며 3주차에 작성한 코드와 비교해보고 수정을 해본 뒤에 본격적인 4주차 미션을 진행하였다. 이번 4주차 미션은 새로운 무언가를 시작하기 보다는 공통 피드백을 최대한 반영하여 잘못 알았던 지식을 깨우치고 개선하는 것에 초점을 맞췄다.

🎯 주제

프리코스 마지막 4주차 미션은 평소와는 다른 방식으로 진행되었다. 포크를 받지 않고, 내 레포를 비공개로 처리하여 우아한테크코스 계정을 collaborate하는 방식이었다.

public으로 설정하면 안되는 부분, 화요일 이전까지 우테코 계정을 초대해야하는 등 요구조건이 꽤 까다로웠다.

문제 길이 또한 매우 길었다. (수능 국어 비문학 지문을 읽는 듯한 느낌을 살짝 받았다...ㅎㅎ) 이번 문제의 주제는 크리스마스 프로모션 이었는데, 문제만 길었지 최대한 구체적으로 설명해주기 위해서 예시를 많이 들어준 것이어서 시작전 살짝 쫄았던 마음이 문제를 읽으면서 천천히 풀렸다.

예외 처리나 요구 사항을 놓치는 부분 없이 꼼꼼히 체크해서 문제를 해결하고자 하였다.

🎯 문제 요구 사항

확실히 4주차가 되니까 문제 요구 사항이 매우 길어졌다. 요구 사항이 누적되어 추가가 되다보니 이미 반영한 내용도 많고, 이제는 확실히 습관적으로 기존 요구 사항에 대해 지키게 된 것 같다.

위 요구 사항에서 가장 눈에 띄었던 부분은 ERROR가 발생하면 해당 부분부터 입력을 다시 받는 곳이었다. 프로그램을 종료시키는 것이 아니라 다시 입력을 받게끔...? 고민이 많았던 부분이었다. 이 부분에 대해서는 아래에서 자세히 설명하겠다.

사실 로또에서도 이 요구 사항이 있었다는 것을 회고 글을 쓰는 지금 시점에서 알게 되었다...이 바보...에러 통과가 되지 않았을 때 캐치를 했어야 했는데... 너무 바보같다.

🎯 구현 전 로직 작성

4주간 프리코스를 진행하는 동안 로직을 작성할 때 상당히 많은 시간을 투자하는 것이 중요하다는 점을 느꼈다. 처음 무턱대고 구현부터 시작했을 때는 이미 파일을 많이 분리한 상태에서 에러로 인해 코드를 변경하는게 쉽지 않았다. 하지만 시간이 오래 걸리더라도, 제대로된 로직을 작성한다면 구현할 때 오히려 시간이 많이 단축되었다.

이번 4주차 미션은 필요한 데이터 값과 출력 값이 상당히 많아서 로직 작성이 더 중요했던 것 같았고, 꼬박 하루를 로직 작성에 시간을 투자했다. 날짜별로 적용되는 할인 대상과 할인 금액이 다르다는 점은 어떤 플로우로 프로그램이 작성되어야 하고, 세부적으로는 객체 지향적인 요소까지 고려해야 해서 복잡했다.

const BENEFIT_RESULT = Object.freeze([
  "크리스마스 디데이 할인:",
  "주말 할인:",
  "평일 할인:",
  "특별 할인:",
  "증정 이벤트:",
]);

오랜 고민 끝에 내가 찾아낸 최선의 방법은 가능한 이벤트의 개수만큼 배열을 0으로 초기화하여 날짜를 입력하고, 날짜와 관련된 이벤트에 값을 1씩 추가하여 결과적으로는 배열에 값이 1인 부분만 할인행사를 적용하여 반환하는 것이었다. 이 방법을 사용하니 각 로직은 본인의 역할만 수행하고 이후에 해당 결과만 출력할 수 있어서 컨트롤러의 역할 부담이 줄어들고 최종 출력 코드가 매우 깔끔해졌다.

이번 미션만큼은 정확한 로직없이 느낌대로 코드를 작성했다면 정말 많은 위기를 맞이했을 것 같다. 결과적으로 구현하는 부분에서는 큰 오류없이 계획대로 진행되어 시간도 많이 절약되었고, 많은 시간을 리팩토링에 투자할 수 있었다.

🎯 기능 목록 작성

## 기능 목록

- [x] 날짜와 관련된 기능

  - [x] 날짜를 입력받는 기능
  - [x] 입력받은 날짜가 평일, 주말 중 어디에 해당하는지 확인하는 기능
  - [x] 입력받은 날짜가 평일이라면 특별할인을 받을 수 있는 날짜인지 확인하는 기능

- [x] 메뉴와 관련된 기능

  - [x] 메뉴와 메뉴 개수를 입력받는 기능
  - [x] 메뉴와 메뉴 개수를 순차적으로 출력하는 기능
  - [x] 주문한 메뉴와 메뉴 개수를 저장하고 있는 객체 생성하는 기능

- [x] 돈과 관련된 기능

  - [x] 총 주문 금액을 계산하는 기능
    - [x] 총 주문 금액이 10,000원을 넘기는지 여부를 확인하는 기능
  - [x] 이벤트 값을 받아 총 혜택 금액을 계산하는 기능
  - [x] 총 주문 금액과 비교하여 증정이벤트 금액을 출력하는 기능
  - [x] 총 혜택 금액을 비교하여 혜택 내역을 출력하는 기능

- [x] 이벤트와 관련된 기능

  - [x] 주말 / 평일 여부로 주말혜택, 평일 혜택 계산하는 기능
  - [x] 평일이라면 특별할인 여부 혜택 계산하는 기능
  - [x] 크리스마스 디데이 이벤트 혜택 계산하는 기능
  - [x] 혜택 여부와 상관없이 해당 이벤트 메뉴를 주문 안하면 출력하지 않는 기능

- [x] 에러가 발생하면 에러 메시지를 출력하고 다시 입력받는 기능

기능 명세서를 작성하는 것에 정말 오랜 시간 공을 들였다. 원하는 조건도 많고 필요한 데이터 양도 상당히 많았기 때문에 도메인을 작성하는 것에 많은 힘을 쏟았다.

🎯 고난 그리고 배움

❌ 에러 처리 방식이 사뭇 다르네...?

4주차 미션에서 새롭게 추가된 사항이 에러가 발생했을 때, 프로그램을 종료시키는 것이 아닌 새로운 입력을 받는 것이었다. 처음에는 ‘이게 가능한가?’ 싶었지만 3주차 미션에서 에러 메시지를 출력하고 프로그램을 종료시키는 법에 대해서 수많은 오류를 토대로 학습한 경험이 있어서 이것을 응용한다면 충분히 해결 가능할 것 같았다.

하지만 3주차 미션도 프로그램을 종료시키는 것이 아닌 새로운 입력을 받게끔 해야했다. 정말 이런 실수를 하다니...문제 꼼꼼히 읽지 않은 과거의 내가 너무 원망스럽다... ㅜㅜ

async handleDate() {
  try {
    await this.inputVisitDate();
  } catch (error) {
    Console.print(error.message);
    await this.handleDate();
  }
}

3주차는 모든 에러가 발생하는 부분에서 메시지를 출력하면 되는 기능이라 App.js에서 try…catch 문으로 묶어줬지만 이번에는 입력을 새로 받아야하기 때문에 컨트롤러에서 기능이 이루어져야 했다. 처음에는 에러가 발생하면 재귀함수를 호출해 입력받는 함수로 돌아가게끔 하였다.

async #inputVisitDate() {
  while (true) {
    try {
      return await InputView.readDate();
    } catch (error) {
      Console.print(error.message);
    }
  }
}

하지만 리팩토링을 하는 과정에서 재귀함수를 호출한다면 메모리 낭비가 상당히 심해질 것 같아서 while문return을 사용해 무한루프에 빠지지 않게끔 하여 문제를 해결했다.

❌ 숫자 계산이 왜 이렇게 되지?

calculateDDayAmount() {
    const date = this.#benefitList[0];
    const benefitAmount =
      NUMBER.dDayDefaultDiscount + (date - 1) * NUMBER.dDayPlusDiscount;
    this.#benefitList[0] = benefitAmount;
    return this.#benefitList;
  }
 // 340900???

이벤트마다 혜택 금액을 계산하는 BenefitAmount 클래스의 일부이다. 그 중에서 크리스마스 디데이 할인 금액을 계산한 부분인데 배열 0번째 인덱스에 디데이 할인 이벤트가 적용된 값을 할당하는 역할을 한다. 내가 기대한 값은 3400이었는데 계속 340900이라는 값이 떴다. 계산 실수인가 싶어서 콘솔창에 수식값만 넣었는데 내가 작성한 수식은 정상적으로 동작했다.

결국 배열에 값을 재할당하는 것에서 오류가 있다고 판단하고 기존 배열과 다르게 값만을 넣어주는 새로운 배열을 필드에 생성하여 기능을 구현하였다.

하지만 리팩토링을 하는 과정에서 기능은 비슷한데 변수명만 달라서 코드만 길어지는 점이 보기에 너무 불편했다.

다시 한번 코드를 살펴보니 위에 코드에서 date라는 변수에 배열 인덱스 값을 할당하고 오히려 date가 아닌 기존 배열 값에 새로운 값을 할당해버려서 date를 업데이트 하지 못하는 문제가 발생했다.

  #calculateDDayAmount() {
    this.#benefitList[NUMBER.dDayIndex] <= NUMBER.christmas
      ? (this.#benefitList[NUMBER.dDayIndex] =
          NUMBER.dDayDefaultDiscount +
          (this.#benefitList[NUMBER.dDayIndex] - 1) * NUMBER.dDayPlusDiscount)
      : (this.#benefitList[NUMBER.dDayIndex] = 0);
  }

benefitAmount 라는 배열을 지우고 기존에 Event에서 받아온 BenefitList 배열에 변수를 따로 선언하지 않고 계산한 값을 새로 업데이트 하는 방법을 사용하여 코드의 길이를 줄여나갔다.

🎯 새로 배운 내용

🔖 객체 다움이란

프리코스를 하는동안 꾸준히 했던 고민이 바로 객체는 객체스럽게 사용하자 였다. 3주차 미션에서는 파일들을 분리하여 능동적인 코드를 작성하고자 하였고, private 메서드를 활용하여 캡슐화에 신경을 많이 썼다. 하지만 공통 피드백을 읽어보니 객체에서 데이터를 꺼내지 말고, 메시지를 던지도록 구조를 바꾸라는 말이 있었다. 지금까지 내가 작성한 코드는 능동적인 코드처럼 보이게 예쁜 구조로 나눈 느낌이었다. 더 머리가 복잡해졌다. 본격적인 미션 시작 전에 Tecoble 블로그 글과 디스코드 커뮤니티에 올라온 의견을 읽으며 객체 스럽다 의 의미를 정리할 수 있었다.

내가 스스로 내린 결론은 다음과 같다. getter 를 사용하지 않는 것만 객체다운 코드가 아니고, 그렇다고 getter 만 사용하는 것 역시 객체다운 코드가 아니다. 하나의 파일 안에서는 여러 객체가 협력할 수밖에 없다. 이 협력하는 과정에서 다른 객체에 의존하지 않고 본인의 역할은 본인 객체 안에서 해결하는 책임감 있고, 능동적인 코드를 작성하는 것이 객체다운 코드이다.

3주차 미션에서 private 메서드를 사용한 것도 캡슐화에 신경쓰며 외부에 객체 정보가 유출되지 않는 좋은 방법 중 하나였던 것 같다. 하지만 파일을 나눴다면 그 파일 안에서 협력할 수 있는 객체는 메시지를 받아서 처리한 결과만 외부에 노출시킬 수 있게끔 하는 것이 더 객체 지향적이지 않을까 싶다.

class VisitDate {
  #date;

  constructor(date) {
    this.#date = Number(date);
  }

  isDateWeekend() {...}

  isDateSpecial() {...}

  processVisitDate() {... }

이번 4주차 미션에서는 객체다운 코드를 작성하기 위해서 domain 생성에 많은 시간을 쏟았다. 필요한 데이터를 정리하고, 기능별로 분리하여 VisitDate , Event , Order , TotalAmount , BenefitAmount 총 5개의 domain을 생성하였다. 특히 VisitDate 도메인은 Event와 협력하여 해당 날짜가 어떤 이벤트에 해당이 되는지에 대한 메시지만 넘겨주고 Event가 그 값을 이용해 이벤트 정보를 업데이트할 수 있게 하였다. 나머지 객체는 메시지만 전달받으면 각 객체 안에서 계산하고 출력값만 반환하는 구조로 코드를 작성하여 조금 더 능동적인 객체가 되었다.

🔖 진정한 의미의 테스트 코드 작성

테스트를 위한 코드는 구현 코드에서 분리되어야 한다 라는 공통 피드백 내용을 보고 지금까지 제가 작성한 테스트 케이스 방식이 잘못되었다는 점을 깨달았다. 사실 테스트 케이스의 최종 목표는 내가 작성한 코드가 오류없이 잘 돌아가기 위함이다. 코드를 다 작성하고 그때가서 오류가 발생했을 때는 잘못하면 코드를 다 갈아 엎어야 할 수도 있기 때문이다. 하지만 지금까지 메서드를 직접 콘솔에 찍어보면서 단위 테스트를 하고, 최종 코드가 완성되면 테스트 코드를 작성해보며 Jest를 검토하는 용도로 사용했다.

물론 테스트를 위한 코드를 구현 코드에서 작성하지 않았지만, 테스트 케이스 코드 자체를 목적과 다르게 사용한다는 점은 스스로를 많이 반성하게 하였다. 이번 미션은 객체 단위로 메서드를 직접 Test 파일에서 실행시켜보며 오류를 확인했다. 이제서야 테스트 코드의 의도를 파악한 내 자신이 너무 부끄러웠지만 지금이라도 깨달아서 한편으로 다행이었다. 실제로 이러한 방법으로 하나의 단위마다 테스트 케이스를 작성하고 실행하다보니 컨트롤러에서 domain과 view를 병합했을 때 오류가 거의 발생하지 않았다.

🔖 컨트롤러에 private 필드가 필요할까?

필드의 수를 줄이기 위해 노력한다 이 피드백을 보고 지난 로또 미션에서 작성한 제 컨트롤러 코드를 살펴봤다. 무려 필드의 수가 6개나 되었다. 피드백에 필드 개수가 많으면 객체의 복잡도를 높인다고 쓰여져있었는데 처음 내 생각은 해당 메서드 안에서만 사용하는 변수들을 필드에 선언하면 외부에서 접근을 못해서 객체는 복잡해지지만 은닉화, 캡슐화에는 더 적합할 것 같다고 생각했다. 그래서 내가 작성한 domain 객체와 비교를 해봤는데 정작 domain에서 필드의 수는 최대 2개이거나 1개였다.

// field를 하나도 사용하지 않았다.

class PromotionController {
  constructor() {}

  async playPromotion() {
    OutputView.printIntro();
    const date = ...
    const event = ...
    const order = ...
    OutputView.printPreview(date);
    this.#displayOrderedMenus({ date, order, totalAmount });
  }
  
  ...
  
}

다양한 블로그의 글을 읽어보고 이를 바탕으로 생각해본 내 결론은 '굳이 컨트롤러에서는 필드에 변수를 선언할 필요가 없다'였다. domain 객체는 다른 객체 혹은 컨트롤러에서 사용할 수 있는 객체이고, 접근이 가능하기 때문에 보호가 필요했다. 하지만 컨트롤러는 App.js에서만 사용되고, 여러 곳에서 쓰이지 않는 객체이다. 때문에 굳이 필드에 변수를 선언하지 않고 직접 함수에서 선언하고 해당 변수를 인자로 넘겨주는 방식으로 사용하는 것이 옳다고 판단했다.

🎯 개선해야할 점

✏️ 꼼꼼함을 기르자...

3주차에 이미 기능 요구 사항으로 나갔던 부분을 4주차 끝나고 나서 알았다는 점은 너무 부끄러운 사실이다. 꼼꼼하게 읽겠다고 다짐했지만 중요한 예외 처리에서 실수를 한 부분이 정말 안타까웠다.

이 부분도 정말 어이가 없었다. 기능 구현을 모두 마치고 테스트 케이스를 돌렸는데 이상한 에러가 발생했다. 딱히 데이터를 다루는 함수도 아니어서 에러가 발생할 곳이 아니었다. 30분 정도 애를 먹다가 메시지 파일을 봤는데 '다시 입력해 주세요.'가 아닌 '다시 입력해주세요' 이렇게 붙여서 적었던 것이었다.

매번 에러 처리를 할 때, 어려운 부분도 아니고 실수가 발생하는 부분이어서 이 부분에 대해서는 꼼꼼하게 대처할 필요가 있는 것 같다.

✏️ 주말 혜택인데 메인 메뉴를 입력하지 않는다면..?

해당 케이스에 대한 예외 처리에 대해 고민을 했었다. 만약 주말 혜택이 적용되는 경우인데 메인 메뉴가 아닌 애피타이저나 디저트로 1만원이 넘는 금액을 시켰을 때, 아예 주말 혜택 내용이 출력되면 안되는지, 아니면 주말 혜택 내용이 출력이 되고 -0원도 출력이 되어야 하는지가 정말 의문이었다.

프리코스 문제에 대한 설명에서도 이 부분을 찾아보기 힘들었기 때문에 어떤 출력값을 내야할 지 고민이었다. 프리코스를 마무리하는 순간까지 정확한 답을 찾기는 힘들었지만 - 0원이 출력된다는 것은 어짜피 해당 혜택을 받지 못한다는 의미인데 굳이 출력을 해야할까?(너는 자격 요건은 되는데 혜택 못받지롱~~ 약올리는 느낌이었다.) 싶어서 출력을 하지 않았다.

이에 대한 설명이 있었으면 좋았을거라는 아쉬움이 남는다.

🎯 프리코스 마무리

프리코스를 통해 그동안 어렵게만 느껴졌던 객체 지향 설계와 클래스에 대해 고민하고 코드에 적용해 볼 수 있었고, 한번도 고민해보지 못했던 클린 코드 원칙에 대해 알 수 있어서 유익했다. 평소에는 코드가 동작만 된다면 쳐다도 보지 않았었지만, 하나의 프로그램을 일주일 동안 뜯어보고 수정하며 살아있는 코드를 만들 수 있음에 성취감과 행복감을 동시에 느낄 수 있었다. 한 달이라는 짧은 기간동안의 몰입이었지만 코딩이 이렇게 재밌는 것임을 새삼 느끼며 개발자라는 꿈을 선택한 것에 후회하지 않게 되었다. 이러한 환경을 제공해준 우아한테크코스에 진심으로 감사하다.

🎯 프리코스 이후에는?

한달 간의 프리코스 여정이 끝났다. 분명 여기서 끝이 아니다. 내 성격을 생각한다면 또 하나의 목표를 잡고 열심히 도전할 것 같다.

회고록을 작성하는 시점에서는 아직 구체적으로 계획한 것은 없지만 겨울 방학 전까지는 프리코스에서 작성한 코드를 그동안 받았던 피드백을 반영하여 수정해보고 다듬어보는 과정을 밟을 것 같다. 프리코스를 하면서 배웠던 개념인 비동기, 객체 지향을 프리코스 때 배운 어설픈 개념으로 어렴풋이 아~ 내가 내린 결론은 이런 것 같아~가 아니라 시간을 들여서라도 깊게 파보고 싶다.

Tecoble 글을 심심할 때 읽는 것도 하나의 낙이 될 것 같다. 사실 몇 개를 읽어봤는데 다들 글을 너무 가독성 좋게 잘 쓰셔서 놀랐다. 처음 배우는 사람도 이해하기 쉽게 잘 정리해주셔서 지식을 풀어내는 능력이 부럽기도 했다. 시간 관계상 많이 읽지는 못했지만 그동안 궁금했던 개념들에 대해 써놓으신 글이 몇개 눈에 들어와서 자주 이용할 것 같다.

그리고 나는 리액트에 대해 많이 배워보지 못했다. 지금 생각해보면 자신감이 많이 떨어졌던 것 같다. 이번 프리코스를 통해 자바스크립트에 대해 많이 공부할 수 있고, 개발에 흥미를 느꼈던 것처럼 리액트도 중요한 여러 개념을 접목시키며 프리코스와 같은 미션 형태로 스터디를 하면 실력 향상이 많이 될 것 같다는 생각이 들었다. 정말 진지하게 고민중이다..ㅎㅎ

4주간의 프리코스 회고 끗!

👉 우아한테크코스 4주차 미션_크리스마스 프로모션

코드 리뷰는 언제든 환영입니다! 🙂

profile
블로그 이전했습니다

2개의 댓글

comment-user-thumbnail
2023년 11월 18일

4주동안 고생하셨습니다~ 하트 꾸욱 누르고 갑니다 ^ㅁ^

1개의 답글