본 회고는 1주차 문제에 대한 풀이보다는 각 문제를 풀면서 어떤
고민
을 하고해결
했는지를 초점에 두어서 작성했습니다.
일주일 동안 문제를 단순히 구현하는 것이 아닌 이후에 있을 코드 리뷰에서 다른 사람들에게 최대한
읽기 쉽게
작성하고유지보수
하기 쉬운 코드를 짜고 싶었다. 이후 클린코드 내용을 학습하던 중프로그래밍 패러다임
과함수형 프로그래밍
키워드에 대한 궁금증이 생기고 이를 찾아보았다.아래 링크는 제가 프로그래밍 패러다임(선언형 vs 명령형)과 함수형 프로그래밍, 객체지향형 프로그래밍에 대해 학습하고 정리한 블로그 글입니다 ^^.
프로그래밍에 대해(선언형 vs 명령형)
함수형 프로그래밍에 대해
객체지향형 프로그래밍에 대해
학습한 내용에 대해 들추어 보았을 때 명령형 프로그래밍인 for문
같은 경우 전역 데이터에 대한 정보를 수정하여 자칫 잘못하면 스파게티 코드가 될 수 있다.
따라서 map
, filter
, reduce
와 같은 함수를 최대한 활용하고 고차함수 사용에 최대한 익숙해지려고 노력하였습니다. 뿐만 아니라 다른 함수 구현 시 부수효과(Side Effect)
가 없는 순수함수
로 구현하려고 노력하였습니다.
함수 선언 시 해당 함수의 동작을 정확히 표현하여
주석
없이도 제 코드를 잘 읽히게 코드를 짜고 싶었습니다.뿐만 아니라 해당 함수나 클래스에 네이밍과 관련한
하나의 책임
만을 부여하려고 노력했습니다. 쉽게 말해, 해당 함수명과 관련한 로직에 대한 내용만 포함하고 다른 로직은 넣지 않도록 노력하였습니다.
특히 리팩토링 과정을 거치면서 해당 함수가 해당 동작을 정확히 표현하였는가? "함수나 클래스를 더 세분화할까??" 와 같은 고민을 굉장히 많이 하고 시간을 사용했습니다.
문제 상황
저 같은 경우 이번 프리코스 1주차 문제들은 에러, 제한사항 관련 로직들과 메인 로직을 기능을 나누어 구현하였고 이를 구분하기 위해
ErrorCase 클래스
와메인 로직 클래스
를 구분하여 구현하였습니다.아래의 코드에서 보시면 아시겠지만
ErrorCase 클래스
와SNSAlgorithm 클래스
에 상당히 많은 함수가 부여되고 있었고여러 개의 책임
이 부여되어 있어 리팩토링 과정에서 이를 어떻게 분리해야할까 고민하였다.첫번째로 구현한 problem7 코드 구조
객체지향 프로그래밍
에 대해 학습하고 좋은 객체지향 설계를 위한 5원칙인 SOLID
에 대해 학습하고 코드에 적용시켜보았습니다.
아래 링크는 제가 객체지향 프로그래밍, SOLID에 대해 학습하고 정리한 블로그 글입니다.
javascript로 알아보는 객체 지향 프로그래밍
JS - 좋은 객체지향 설계를 위해 (/w SOLID)
코드상황
input값인user
,friends
,visitors
각각에 대한 에러코드를 함수로 구현하였습니다. 이들은 각각 별 개의 로직을 가지고 있습니다.
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(의존 역전 원칙)
을 위반하게 됩니다.
이번에는 클래스를 추가적으로 분리하는 것이 아닌 메인함수에서
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 객체와 서로 종속적이다.
하지만 메인 함수와 하위 클래스는 서로가 독립
적인 것을 알 수 있습니다. 따라서 이전 코드보다 유지보수적인 측면에서 좀 더 큰 이점으로 가져올 수 있을 것 같습니다.
내가 처음으로 구현한 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)
많이 배우고 갑니다..! 혹시 "for문을 지양, 고차함수를 최대한 활용하자" 파트 369구현 부분에서 (_,i)대신 (i) 이렇게만 쓰면 안되는 걸까요?? 너무 기초적인 질문같은데 잘 몰라서 😂 궁금하네요!