[회고] 우아한 테크코스 5기 프리코스 1주차 회고

황준승·2022년 11월 1일
18
post-thumbnail

본 회고는 1주차 문제에 대한 풀이보다는 각 문제를 풀면서 어떤 고민을 하고 해결했는지를 초점에 두어서 작성했습니다.

최종 코드 링크

🤔 1. 가독성이 좋은 코드를 짜고 싶다.

일주일 동안 문제를 단순히 구현하는 것이 아닌 이후에 있을 코드 리뷰에서 다른 사람들에게 최대한 읽기 쉽게 작성하고 유지보수하기 쉬운 코드를 짜고 싶었다. 이후 클린코드 내용을 학습하던 중 프로그래밍 패러다임함수형 프로그래밍 키워드에 대한 궁금증이 생기고 이를 찾아보았다.

아래 링크는 제가 프로그래밍 패러다임(선언형 vs 명령형)함수형 프로그래밍, 객체지향형 프로그래밍에 대해 학습하고 정리한 블로그 글입니다 ^^.
프로그래밍에 대해(선언형 vs 명령형)
함수형 프로그래밍에 대해
객체지향형 프로그래밍에 대해

for문을 지양, 고차함수를 최대한 활용하자.

학습한 내용에 대해 들추어 보았을 때 명령형 프로그래밍인 for문 같은 경우 전역 데이터에 대한 정보를 수정하여 자칫 잘못하면 스파게티 코드가 될 수 있다.

따라서 map, filter, reduce 와 같은 함수를 최대한 활용하고 고차함수 사용에 최대한 익숙해지려고 노력하였습니다. 뿐만 아니라 다른 함수 구현 시 부수효과(Side Effect)가 없는 순수함수로 구현하려고 노력하였습니다.

함수의 동작을 정확히 표현할 수 있는 함수 네이밍

함수 선언 시 해당 함수의 동작을 정확히 표현하여 주석없이도 제 코드를 잘 읽히게 코드를 짜고 싶었습니다.

뿐만 아니라 해당 함수나 클래스에 네이밍과 관련한 하나의 책임만을 부여하려고 노력했습니다. 쉽게 말해, 해당 함수명과 관련한 로직에 대한 내용만 포함하고 다른 로직은 넣지 않도록 노력하였습니다.

특히 리팩토링 과정을 거치면서 해당 함수가 해당 동작을 정확히 표현하였는가? "함수나 클래스를 더 세분화할까??" 와 같은 고민을 굉장히 많이 하고 시간을 사용했습니다.

🤔 2. 클래스 분리 어떻게 해야할까...??(feat. 7번 문제)

문제 상황

저 같은 경우 이번 프리코스 1주차 문제들은 에러, 제한사항 관련 로직들과 메인 로직을 기능을 나누어 구현하였고 이를 구분하기 위해 ErrorCase 클래스메인 로직 클래스를 구분하여 구현하였습니다.

아래의 코드에서 보시면 아시겠지만 ErrorCase 클래스SNSAlgorithm 클래스에 상당히 많은 함수가 부여되고 있었고 여러 개의 책임이 부여되어 있어 리팩토링 과정에서 이를 어떻게 분리해야할까 고민하였다.

첫번째로 구현한 problem7 코드 구조

ErrorCase 클래스를 분리해보자!!

코드상황
input값인 user, friends, visitors 각각에 대한 에러코드를 함수로 구현하였습니다. 이들은 각각 별 개의 로직을 가지고 있습니다.

분리 1단계

UserError 클래스, FriendsError 클래스, VisitorsError 클래스로 각각을 분리하고 이를 통합하는 상위 클래스인 MyError 클래스를 구현하자.

이때 객체지향의 특성인 다형성을 활용하여 상위 클래스에 오버라이딩을 적용하였습니다.

// Interface 객체
class MyError {
  checkLimit() {
    throw new Error("Overiding Error -> 제한된 input 길이를 체크하자");
  }

  occurError() {
    throw new Error("Overiding Error -> Error 발생 시 로직 멈추자");
  }
}

class UsersError extends MyError {
  constructor(user) {
    super();

    this.user = user;
    this.occurError();
  }

  checkLimit() { ... }

  ...

  occurError() {
    if (!(this.checkLimit() && this.checkLower())) {
      throw new Error("input 양식 중 user값에 오류가 발생하였습니다.");
    }
  }
}

class FriendsError extends MyError {
  constructor(friends) {
    super();

    this.friends = friends;
    this.occurError();
  }

  checkLimit() { ... }
  
  ...

  occurError() {
    if (!this.check()) {
      throw new Error("input 양식 중 friends값에 오류가 발생하였습니다.");
    }
  }
}

class VisitorsError extends MyError {
  constructor(visitors) {
    super();

    this.visitors = visitors;
    this.occurError();
  }

  checkLimit() { ... }
  
  ...

  occurError() {
    if (!this.checkLimit()) {
      throw new Error("input 양식 중 visitor값에 오류가 발생하였습니다.");
    }
  }
}

// 실행
function problem7(user, friends, visitors) {
  new UsersError(user);
  new FriendsError(friends);
  new VisitorsError(visitors);
  
  const sns = new SNSAlgorithm(user, friends, visitors);

  return sns.recommend();
}

다형성을 통해서 새로운 에러를 생성한다면 해당 에러 객체를 생성하고 메인 함수에서 실행만 하면 에러 체크를 할 수 있기 때문에 유지보수적인 측면에서 아주 좋다.

하지만 메인 함수는 각각의 에러 객체들에게 종속적이며 이는 SOLID 중 DIP(의존 역전 원칙)을 위반하게 됩니다.

분리 2단계

이번에는 클래스를 추가적으로 분리하는 것이 아닌 메인함수에서 Interface객체(MyError클래스)를 참조하여 종속성을 제거, DIP(의존 역전 원칙)을 지키는 코드를 한 번 구현하려고 노력하였습니다.

class MyError {
  checkLimit() {
    throw new Error("Overiding Error -> 제한된 input 길이를 체크하자");
  }

  occurError() {
    throw new Error("Overiding Error -> Error 발생 시 로직 멈추자");
  }
  
  // 모든 에러 객체를 테스트하는 함수
  checkAllError(errorInstanceList) {
    for (const errorInstance of errorInstanceList) {
      if (errorInstance.occurError()) {
        return true;
      }
    }

    return false;
  }
}

// 하위 클래스 객체의 코드는 동일

function problem7(user, friends, visitors) {
  const myError = new MyError();
  const breakpoint = myError.checkAllError([
    new UsersError(user),
    new FriendsError(friends),
    new VisitorsError(visitors),
  ]);

  if (breakpoint) return;

  // ...
}

위의 코드는 하위 클래스는 Interface객체와 서로 종속적이며, 메인 함수는 Interface 객체와 서로 종속적이다.

하지만 메인 함수와 하위 클래스는 서로가 독립적인 것을 알 수 있습니다. 따라서 이전 코드보다 유지보수적인 측면에서 좀 더 큰 이점으로 가져올 수 있을 것 같습니다.

SNSAlgorithm 클래스를 분리해보자!!

내가 처음으로 구현한 problem7 메인 로직 코드 링크

간단한 코드 설명
친구관계그래프를 저장하는 friendGraph, 알고리즘 우선순위를 결정하는 자료구조 scoreBoard로 결과값을 도출하는 로직이다. (자세하게 확인하고 싶다면 위의 코드를 참조하자.)

어려웠던 점

  scroeFriendToFriend() {
    [...this.friendGraph.get(this.user)]
      .flatMap((friend) => [...this.friendGraph.get(friend)])
      .filter((person) => !this.isFriend(person))
      .forEach((person) => (this.scoreBoard[person] += 10));
  }

특히 해당 함수를 실행하기 위해서 this.user, this.friendGraph, this.scoreBoard와 같은 다양한 변수가 있고 이를 나눠서 저장하여 객체를 분리할 경우 구현하기에 굉장히 어렵다고 판단하였다.

최종 수정 코드
어떻게 분리할까 정말 많은 고민을 하다가 MVC패턴에 힌트를 얻어 정보를 저장하는 자료구조의 경우 SNSModel 클래스로 분리, 그 외 정답을 유추하는 함수의 경우 SNSAlgorithm 클래스 그리고 정답을 리턴하는 메인 함수(problem7)로 분리하여 구현하였다.

class SNSModel {
  constructor(friends, visitors) {
    this._friends = friends;
    this._visitors = visitors;

    this._scoreBoard = this.makeScoreBoard();
    this._friendGraph = this.makeFriendGraph();
  }

  getFriendRelation() {
    return this._friendGraph;
  }

  getScoreBoard() {
    return this._scoreBoard;
  }

  saveFriendGraph(keyFriend, valueFriend, map) {
    const defaultValue = map.get(keyFriend) || [];

    map.set(keyFriend, [...defaultValue, valueFriend]);
  }

  makeFriendGraph() {
    const resultMap = new Map();

    this._friends.forEach(([ID_A, ID_B]) => {
      this.saveFriendGraph(ID_A, ID_B, resultMap);
      this.saveFriendGraph(ID_B, ID_A, resultMap);
    });

    return resultMap;
  }

  makeScoreBoard() {
    console.log(this._friends);

    return [
      ...new Set(
        [...this._friends, ...this._visitors].flatMap((relation) => relation)
      ),
    ].reduce((acc, cur) => ({ ...acc, [cur]: 0 }), {});
  }
}

class SNSAlgorithm {
  constructor(user, friends, visitors) {
    this._user = user;
    this._visitors = visitors;

    this._model = new SNSModel(friends, visitors);
  }

  isRecommand(person) {
    const { _model, _user } = this;

    return !new Set([_user, ..._model.getFriendRelation().get(_user)]).has(
      person
    );
  }

  scroeFriendToFriend() {
    const { _model, _user } = this;

    [..._model.getFriendRelation().get(_user)]
      .flatMap((friend) => [..._model.getFriendRelation().get(friend)])
      .filter((person) => this.isRecommand(person))
      .forEach((person) => (_model.getScoreBoard()[person] += 10));
  }

  scroeVisitor() {
    const { _model } = this;

    this._visitors
      .filter((person) => this.isRecommand(person))
      .forEach((person) => (_model.getScoreBoard()[person] += 1));
  }

  recommend() {
    const { _model } = this;

    this.scroeFriendToFriend();
    this.scroeVisitor();

    return Object.keys(_model.getScoreBoard())
      .map((person) => [person, _model.getScoreBoard()[person]])
      .filter(([_, score]) => score > 0)
      .sort((x, y) => y[1] - x[1] || (x[0] < y[0] ? -1 : 1))
      .map(([person, _]) => person)
      .slice(0, 5);
  }
}

function problem7(user, friends, visitors) {
  const myError = new MyError();
  const breakpoint = myError.checkAllError([
    new UsersError(user),
    new FriendsError(friends),
    new VisitorsError(visitors),
  ]);

  if (breakpoint) return;

  const sns = new SNSAlgorithm(user, friends, visitors);

  return sns.recommend();
}

📌 아쉬웠던 점

  • 구현하기 전 기능 목록을 에러 및 제한사항 구현, 메인 로직 구현 두 부분으로 나누어 커밋을 했었습니다. 하지만 좀 더 세부적인 기능(ex 하나의 함수) 별로 커밋을 하고 개발을 했다면 버전이 많다보니 코드 수정 및 복구가 쉬웠을 것 같다.

  • 알고리즘 문제를 함수형 프로그래밍, 그리고 객체 지향을 고려하여 알고리즘을 풀다 보니 아무래도 시간복잡도적인 측면은 하나도 고려를 하지 못해서 개인적으로 너무 아쉬웠습니다. 각 프로그램에 맞게 명령형 프로그래밍, 함수형 프로그래밍, 객체 지향 프로그래밍 등 다양한 프로그래밍 패러다임 기법을 활용하여 사용하면 좀 더 좋았을 것 같습니다.


1주 동안 내가 학습하고 정리한 블로그 글

프로그래밍 패러다임에 대해(선언형 vs 명령형)
함수형 프로그래밍에 대해
[JS] 객체지향 프로그래밍에 대해
[JS] 좋은 객체 지향 설계를 위해서(/w SOLID)

profile
다른 사람들이 이해하기 쉽게 기록하고 공유하자!!

3개의 댓글

comment-user-thumbnail
2022년 11월 4일

많이 배우고 갑니다..! 혹시 "for문을 지양, 고차함수를 최대한 활용하자" 파트 369구현 부분에서 (_,i)대신 (i) 이렇게만 쓰면 안되는 걸까요?? 너무 기초적인 질문같은데 잘 몰라서 😂 궁금하네요!

1개의 답글