Javascript에서도 SOLID 원칙이 통할까?

teo·2022년 1월 20일
112

테오의 프론트엔드

목록 보기
12/25
post-thumbnail

제가 며칠 전에 클린소프트웨어 책을 보니 SOLID 법칙이 나오던데요, 자바나 C++ 같은 클래스 구조로 객체를 만드는 언어에서는 쉽게 따라해볼 수 있겠는데, 함수 위주로 작성하는 js, ts를 사용하는 프론트엔드에서도 사용이 가능한지, 현업에서 클린 소프트웨어를 만들기 위해 SOLID 법칙을 사용하고 계신 부분이 있는지 궁금합니다.

타입스크립트 클린코드 - https://github.com/labs42io/clean-code-typescript

블로그의 소재를 제공해주신 프린이님께 먼저 감사의 말을 전합니다. 😚
그리고 알려주신 타입스크립트 좋은 링크 감사합니다. 다른 분들도 한번씩 읽어 보시면 좋을 것 같아요.

프롤로그

우선 SOLID 원칙이 뭔지 알아야겠죠? 위키백과에 가서 검사를 한번 해봅시다.

컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다. SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다.

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)
- 출처: 위키백과

SOLID 원칙이란?

즉, SOLID란 똑똑한 선배님이 먼저 만들어 놓은 좋은 코드를 만들기 위한 원칙이라고 하네요.

막연히 우리가 코드 리뷰를 하면서 "이 코드는 좀 별로인것 같아요..." 라고 할때 "왜요?" 라고 하면 "...딱 보면 그렇잖아요!" 라고 하기 보다는 이러한 좋은 원칙에 의거해서 "이 코드는 SRP원칙에 좀 어긋난것 같아요. 일을 너무 많이 하는 것 같으니 좀 쪼개야 될것 같아요." 라고 한다면 훨씬 더 합의가 될 수 있으면서 우리의 코드가 좋은 방향으로 갈 수 있는 지침이 되어줄 것입니다.

2000년대 초반에 만들어졌지만 아직도 우리가 배우고 있다는 것은 정말 이러한 원칙이 Bible이라는 얘기겠죠?

우선 SOLID 5가지의 기본 원칙을 이해하기 전에 먼저 왜 이러한 원칙들이 필요한지 한번 생각을 해보도록 합시다.

SOLID 원칙의 본질

왜 이러한 원칙이 만들어졌을까요? 소프트웨어는 변하기 때문에 '소프트' 라는 명칭이 붙었습니다. 소프트라는 어감 때문에 변경이 아주 쉬울 것 같지만 개발자인 우리는 이미 너무 잘 알고 있습니다. 코드 변경은 쉬운 작업이 아니라는 것을요.

코드를 끊임없는 변경하는 작업은 개발자의 숙명과도 같은 것입니다. 요구사항과 환경이 매번 변화하는 만큼 우리의 소프트웨어도 성장을 해야 합니다. 하지만 소프트웨어는 하나의 거대한 기계와도 같기 때문에 일부의 부품을 교체하는 것들이 기계 전체의 고장으로 이어지기도 합니다.

도미노를 한번 떠올려 봅시다. 잘 세워진 도미노의 일부분을 다른 색깔의 도미노로 바꿔야 한다고 했을때 자칫 실수라고 하게 되면 한번 쓰러진 도미노가 미치는 범위는 어마어마 할 수 있습니다. 이러한 문제를 방지하려면 어떻게 해야할까요? 도미노를 잘 세우는 팁은 중간의 일부를 비워두는 거라고 합니다. 그래서 혹여나 실수를 했을때 문제가 생기는 범위를 최소화하고 국지화 할 수 있는 것이지요.

소프트웨어에서도 서로간의 종속성을 최소한으로 해둔다면 이렇듯 변경이 발생을 했을때 다른 영역에 영향을 주지 않고 변경을 할 수 있도록 하는 것입니다. 도미노의 중간을 비워두면 좋다라는 원칙이 있듯이 변경에 유연할 수 있는 구조를 만들기 위한 원칙이 바로 이 SOLID 원칙이지요.

우리가 자바스크립트를 다루면서 이러한 원칙에 맞춰서 프로그래밍을 한다면 훨씬 더 좋은 구조를 가진 프로그램을 작성을 할 수 있게 됩니다.

하지만 SOLID 원칙은 객체 지향 프로그래밍 원칙 아닌가요?

자바나 C++ 같은 클래스 구조로 객체를 만드는 언어에서는 쉽게 따라해볼 수 있겠는데, 함수 위주로 작성하는 js, ts를 사용하는 프론트엔드에서도 사용이 가능한가요?

그렇습니다. 애석하게도 이 훌륭한 SOLID 원칙들은 객체지향 프로그래밍의 설계 라는 패러다임 토대로 만들어졌습니다. 하지만 우리가 쓰는 자바스크립트는 완전한 객체지향 언어가 아니죠.

자바스크립트는 함수형 프로그래밍을 토대로 Java의 언어적 껍데기를 입힌 언어이기에 함수형이면서 동시에 객체지향의 성격을 동시에 지니고 있어 완벽한 객체지향 언어보다는 사실 함수형 언어에 가깝고 class라는 문법이 일반적인 객체지향과는 다르기에 SOLID라는 좋은 원칙을 배워도 프론트엔드 개발자는 실전에 적용하기 힘들다는 점이 있습니다.

확실히 최근 트렌드의 javascript 실전상황에서는 class 기반으로 작성하기보다는 함수를 주로 다루기는 합니다. 그렇다고 완전히 함수형 프로그래밍이라고 보기도 어렵지요.

그래서 주제의 의문인 SOLID 원칙을 javascript, typescript에서는 어떻게 적용을 해볼 수 있는 지 저 역시도 궁금해졌기에 한번 찾아보고 글을 작성 해보기로 하였습니다.

객체지향의 원칙을 함수형에 적용을 해본다면?

SOLID 원칙에 대한 자료를 찾다보니 객체지향을 기반으로 하지만 클래스는 곧 함수과 데이터 그리고 타입에 대한 이야기이니 SOLID가 꼭 객체지향만을 위한 원칙은 아니다 라는이야기를 발견 할 수 있었습니다.

하지만 SOLID가 객체지향을 바탕으로 설명하는 부분은 사실이기에 함수 위주로 작성하는 실전 코드에 대해서 원칙을 적용해 보는 예시를 찾거나 설명을 하는 자료는 매우 부족했습니다.

따라서 이 글에 대해서는 순전히 제 주관적인 관점을 바탕으로 함수 위주의 js, ts를 기준으로 한번 SOLID 원칙을 재해석 해보고자 합니다.

그렇기에 객체 지향 관점으로 설명하는 SOLID 5가지 원칙에 대해서는 설명을 생략하겠습니다. 이미 인터넷에 좋은 자료가 너무 많아요!

혹시, SOLID 원칙이라는 내용을 제 글을 통해 처음 접하시는 분들은 객체지향 프로그래밍 패러다임에 따른 SOLID 원칙과 예시와 설명을 먼저 확인을 하시고 읽어 주시기 바랍니다!
https://github.com/labs42io/clean-code-typescript#solid

S.O.L.I.D 5가지 원칙

그렇다면 실제 5가지 원칙을 다시 짚어보면서 한번 함수형 프로그래머의 관점으로 바라봅시다.

🔥 S - SRP / 단일 책임 원칙

단일 책임 원칙 (Single responsibility principle)
- 객체 함수는 오직 하나의 책임을 가져야 한다. (객체 함수는 오직 하나의 변경의 이유만을 가져야 한다.)
같은 이유로 변경될 코드들은 모으고. 다른 이유로 변경될 코드들은 흩어라.

1개의 함수는 1개의 역할만 수행하자!

Functions should do one thing

함수는 한 가지 작업을 수행해야 합니다.
이것은 소프트웨어 엔지니어링에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상을 수행할 때 구성, 테스트 및 추론하기가 더 어렵습니다. 함수를 하나의 작업으로 분리할 수 있으면 쉽게 리팩토링할 수 있고 코드가 훨씬 더 깔끔하게 읽힙니다. 이 가이드에서 이것 외에 다른 것을 빼지 않는다면 당신은 많은 개발자들보다 앞서게 될 것입니다.

Bad:

function emailClients(clients: Client[]) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

Good:

function emailClients(clients: Client[]) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

출처 - https://github.com/labs42io/clean-code-typescript#functions-should-do-one-thing

SOLID를 모른다고 해도 누구나 처음 배우는 클린코드의 첫 덕목입니다. 하나의 함수는 하나의 기능만 해야한다! 하나의 함수가 많은 일을 하고 있다면 함수를 쪼개야 한다.

함수를 잘게 쪼개고 명확하게 만들면 절대로 이 함수는 틀릴 수 없다! 라는 코드의 조각들이 많아지게 되며 문제가 발생했을때의 확인을 해야하는 코드의 양이 줄어 들게 됩니다.

도대체 어디까지 쪼개야 하는 건가요? 이 예시에서 굳이 isActiceClient(client)를 쪼갤 필요가 있나요?

이 원칙을 헷갈려하는 주니어 개발자들이 있습니다. 예시에서도 보여주듯이 함수를 조립하기 위한 매개함수로 쓰일 수 있는 것들을 쪼개 주는게 좋습니다.

  1. .filter(...)에 들어갈 조건 판별 함수
  2. .forEach(...)에 들어갈 동작을 하는 side-effect 함수
  3. .map(...)에 들어가는 project 함수
  4. .sort(...)에 들어가는 정렬을 나타내는 함수

가급적 데이터를 다룰때 for문보다는 함수형 method를 쓰려는 습관을 가진다면 훨씬 더 간결하면서 유연하고 좋은 코드를 가질 수 있게 됩니다.

하지만 너무 잘게만 쪼개는 것이 능사는 아닙니다. 가령 export const hasChildren = (item) => !!item.children 과 같은 코드에 대해서는 상황에 따라 다를 수 있습니다. 이 융통성이라는게 필요하다는게 소프트웨어 개발을 어렵게 하는 것 같아요.

1) 재사용이나 변경의 여지가 있는가?
2) 함수명이 표현식보다 훨씬 더 가독성이 있는가?
3) 반대로 함수표현으로 인해 전체 파이프라인을 왔다 갔다 하게 되어 이해하는데 방해가 되는가?
3) 하나의 파이프 라인에 속한 함수들이 각각의 모듈로 쪼개어져 있어 응집도가 떨어지는가?

정도로 결국 가독성과 응집도를 기준으로 적절히 inline을 사용하시는 것도 필요합니다. 가독성의 기준은 본인이 아니라 이 코드를 읽는 다른 사람이므로 잘 모르겠다면 주위 동료에게 물어보시면 좋을 것 같아요.

One more Thing! 순수함수로 작성해보자!

클래스를 쓰지 않고 함수만 사용한다고 함수 프로그래밍이라고 할 수는 없습니다. 함수형 프로그래밍이 되기 위해서는 순수함수와 부수효과를 분리하는 구조가 되어야 합니다.


출처: https://maxkim-j.github.io/posts/js-pure-function

순수함수란?
1. 1개의 반환값이 반드시 존재한다.
2. 같은 인자를 넣었을때에는 항상 같은 값을 반환한다.
3. 함수 외부의 어떠한 값을 변화시켜서는 안된다.

순수함수는 너무나도 SRP의 원칙에 들어맞는 모양이 되게 됩니다. 그러니 함수형 프로그래밍의 핵심인 가급적 순수함수로 작성하는 원칙은 SOLID의 첫번째 원칙인 SRP와 함께 엮어서 생각을 해주시기 바랍니다.


🔥 O - OCP / 개방-폐쇄 원칙

개방-폐쇄 원칙 (Open/Closed Principle)
“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”

일단 OCP의 원칙을 그림을 통해 직관적으로 한번 이해해봅시다. 트럭이라는 운송수단과 뒤에 달리는 기구를 분리/결합 할 수 있는 구조를 만들어 두면 새로운 목적이 필요한 도구를 만들어야 할때 트럭 전체를 다시 만들지 않고서 뒤에 달리는 장치만 새롭게 만들어서 붙일 수 있게 됩니다.

출처: https://levelup.gitconnected.com/the-open-closed-principle-made-simple-cc3d0ed70553

OCP의 원칙의 의미는 새로운 기능의 추가가 일어 났을때에는 기존코드의 수정 없이 추가가 되어야 하고, 내부 매커니즘이 변경이 되어야 할때에는 외부의 코드 변화가 없어야 한다 라는 것입니다.

함수형 프로그래밍에서 이 OCP를 가장 잘 느낄 수 있는 것은 바로 map, filter, reduce와 같은 Higer order Function(or Method)와 webpack loader와 같은 플러그인 또는 middleware 개념입니다.

Bad:

function getMutipledArray(array, option) {
  const result = []
  for (let i = 0; i < array.length; i++) {
    if (option === "doubled") {
      result[i] = array[i] * 2 // 새로운 방식으로 만들기 위해서는 수정이 필요하다.
    }
    if (option === "tripled") {
      result[i] = array[i] * 3 // 옵션으로 분기는 가능하나
    }
    if (option === "half") {
      result[i] = array[i] / 2 // 새로운 기능을 추가하려면 함수 내에서 변경이 되어야 한다.
    }
  }
  return result
}

Good:

// option을 받는게 아니라 fn을 받아보자.
// 이제 새로운 array를 만든다는 매커니즘은 닫혀있으나 방식에 대해서는 열려있다.
function map(array, fn) {
  const result = []
  for (let i = 0; i < array.length; i++) {
    result[i] = fn(array[i], i, array) // 내부 값을 외부로 전달하고 결과를 받아서 사용한다.
  }
  return result
}

// 얼마든지 새로운 기능을 만들어도 map코드에는 영향이 없다.
const getDoubledArray = (array) => map(array, (x) => x * 2)
const getTripledArray = (array) => map(array, (x) => x * 3)
const getHalfArray = (array) => map(array, (x) => x / 2)

하나의 함수의 기능이 여러가지 옵션들로 인해 내부에서 분기가 많이 발생하고 있다면 OCP와 SRP의 원칙에 맞게 함수를 매개 변수로 받는 방법을 통해서 공통 매커니즘의 코드와 새로운 기능에 대한 코드를 분리해서 다룰 수 있게 할 수 있습니다.

그러니 본인의 작성한 덩치가 큰 함수가 params에 option이나 flag가 많은 코드가 있다면 한번 SRP와 OCP 원칙을 기반으로 함수를 한번 점검 해보시기 바랍니다!

실전에서는 Redux의 middleware, Webpack의 loader, vite의 plugin과 같이 아주 많은 곳에서 이러한 원칙을 잘 지켜 유연한 확장과 견고한 매커니즘을 유지하는 좋은 설계를 가지고 있습니다.

🔥 이 OCP 원칙은 함수형 개발의 설계에서 아주 아주 아주 x100 중요하기에 꼭 기억해서 좋은 설계를 만들 수 있도록 합시다. 버그 수정이 아닌 새로운 기능을 개발할때 기존에 개발된 함수를 수정하면서 코드를 개발하고 있다면 OCP 원칙을 위배한 코드를 작성하고 있을 확률이 엄청 높습니다!


L - LSP / 리스코프 치환 원칙

리스코프 치환 원칙 (Liskov substitution principle)
“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.” 계약에 의한 설계를 참고하라.

많은 분이 SRP와 OCP는 쉽게 이해하다가 LSP부터 이게 무슨말이야? 하고 헷갈려합니다. 그래서 LSP는 객체 지향의 개념에서 조금 더 설명을 드리고자 합니다.

LSP는 일단 정의가 헷갈리니 개념부터 먼저 잡고 갑시다.

리스코프 치환 원칙은 이해하면 어렵지 않은 개념인데 참 간단하게 설명이 안되기 때문에 객체지향에서의 원래 의미부터 찬찬히 한번 짚어 보도록 하겠습니다. 치환이라고 하면 상호변경을 의미합니다. 이 원칙에서는 뭘 치환하는 걸까요? 상속을 받은 하위 타입과 상위타입입니다. 이 둘을 치환을 해도 프로그램에서는 문제가 없어야 한다 라는 것이 이 원칙입니다.

일단, 예시를 통해 일단 한번 직관적으로 이해를 하는 것이 좋기 때문에 새와 앵무새와 펭귄의 이야기로 가봅시다.

출처: https://www.cnblogs.com/charon922/p/8643454.html

새라는 객체를 만들고 "앵무새는 새다.", "펭귄은 새다" 라는 정의에 따라 "새"를 상속을 받아서 앵무새와 펭귄을 만들었습니다. 객체지행에서 상속은 is-a 관계이므로 언뜻보면 맞는 것 같습니다.

하지만 이 경우 우리는 새를 정의하기 위해서 fly() 라는 method가 동작하는 것을 상정했기 때문에 앵무새는 fly()하기에 새가 될 수 있지만 펭귄은 fly() 하지 못하기에 새가 될 수가 없습니다. 이렇게 설계가 된 경우 우리는 리스코프 치환 원칙을 위반했다라고 할 수 있습니다. (억지로 만든 예시입니다. 일단 느낌만 먼저 이해를 합시다.)

그리고 나서 가장 유명한 리스코프 치환 문제의 예시인 Rectanle -> Squre에 대해서 이해해봅시다.

전형적인 위반

LSP를 위반하는 전형적인 예로, 너비와 높이의 조회(getter) 및 할당(setter) 메서드를 가진 직사각형 클래스로부터 정사각형 클래스를 파생하는 경우를 들 수 있다. 정사각형 클래스는 항상 너비와 높이가 같다고 간주할 수 있다. 정사각형 객체가 직사각형을 다루는 문맥에서 사용되는 경우, 정사각형의 크기는 독립적으로 변경할 수 없기 때문에 (혹은 그래서는 안되기 때문에) 예기치 못한 행동을 하게 된다. 이 문제는 고치기 쉽지 않다. 정사각형 클래스의 할당 메서드를 수정하여 정사각형의 불변 조건(즉, 너비와 높이가 같음)을 유지하면, 이 메서드는 크기를 독립적으로 변경할 수 있다고 설명한 직사각형의 할당자의 사후 조건을 무력화(위반)한다. 이러한 LSP 위반은 실전에서는 LSP를 위반한 클래스를 사용하는 코드가 실제로 기대하는 사후 조건이나 불변 조건에 따라 문제가 될 수도 있고 아닐수도 있다. 여기서 중요한 사안은 가변성이다. 정사각형과 직사각형이 조회 메서드만 가진다면 (즉, 이들이 불변 객체라면), LSP 위반을 발생하지 않는다.

출처 - 위키백과
예시코드 - https://github.com/labs42io/clean-code-typescript#liskov-substitution-principle-lsp

정리하자면, 리스코프 치환 원칙은 상속을 받아 만든 하위타입의 제약조건들이 상위 타입에서 먼저 선언한(fly나 setWidth) 조건들과 충돌이 날 경우 유지보수가 힘들어 진다는 문제점이 있기 때문에 만들어진 것입니다. 따라서 계층도간의 is-a 관계를 만족한다고 하더라도 (새-펭귄, 직사각형-정사각형) 하위 타입에서 가변성을 가지면서 상위 타입에서 정의한 조건과 일치하지 않는다면 상속을 받지 말아야 합니다.

함수형 프로그래밍에서는요?

이 원칙은 상속을 기반하므로 함수형 프로그래밍에게 바로 적용하기는 힘들것 같습니다. 하지만 먼저 선언된 조건들과 나중에 선언된 조건들이 서로 충돌이 나는 것을 방지해야한다는 원칙으로 접근을 한다면 선언형 함수형 프로그래밍에서 발생하는 순환 종속성을 만들어내는 infinite Cycle을 만들지 않아야 한다 원칙으로 대체를 할 수 있을 것 같습니다.

  let [num1, setNum1] = useState(5);
  let [num2, setNum2] = useState(10);
  let [ratio, setRatio] = useState();

  let [calc1, setCalc1] = useState();
  let [calc2, setCalc2] = useState();

  useEffect(() => { setRatio(num2 / num1) }, [num1, num2]);
  useEffect(() => { setCalc1(calc2 / ratio) }, [calc2, ratio]);
  useEffect(() => { setCalc2(calc1 * ratio) }, [calc1, ratio]);

이와 같이 서로가 서로의 종속성과 순환참조를 만들어 무한루프에 빠지지 않을 수 있도록 하는 원칙을 기억하시고 프로그래밍을 하시면 좋을 것 같습니다.

I - ISP / 인터페이스 분리 원칙

인터페이스 분리 원칙 (Interface segregation principle)
"사용자가 필요하지 않은 것들에 의존하게 되지 않도록, 인터페이스를 작게 유지하라."

출처: https://blog.ndepend.com/solid-design-the-interface-segregation-principle-isp/

사진 속의 USB처럼 All-in-one 패키지가 편하지 않나? 라고 생각하실지 모를 것 같아, ISP를 위반한 댓가에 대한 정확한 비유는 다음과 같습니다.

이 올인원 USB는 한번에 여러개를 충전 할 수 있어 편리합니다!
(단, 모든 USB에 기기가 모두 꽃여있어야만 충전이 가능하세요~)

Bad:

interface SmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    throw new Error('Fax not supported.');
  }

  scan() {
    throw new Error('Scan not supported.');
  }
}

Good:


interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

class AllInOnePrinter implements Printer, Fax, Scanner {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}

왜 예시를 객체지향으로 들고 왔을까요? 함수형에서는 사실 interface당 함수가 1:1의 관계이기에 ISP의 원칙을 위배하기란 쉽지 않습니다.

(함수형 기반에서 ISP 위반에 대한 이야기를 전문적으로 하기가 참 어렵네요. 아래 이야기들은 일종의 썰 느낌으로 들어주세요.)

ISP 위반인지는 확실치 않지만 interface를 보니 갑자기 생각나서 끄적이는 글

타입스크립트를 쓰다보면 백엔드의 스키마 정의를 interface를 정의해서 사용을 하게 되는데 백엔드 스키마 외에 클라이언트에서 필요한 필드들을 추가를 하고 스키마를 맞추기 위해서 ?를 통해서 optional한 필드로 만드는 경우가 있었습니다.

inetface User {
  firstName:string
  lastName:string
  displayName?:string // 클라에서 보여주려 만드는 필드를 그냥 이렇게 생각없이 추가했다.
}

이렇게 편의를 위해 만들어 버린 interface가 ISP원칙에 어긋나지 않았나 생각이드네요. Client에서 사용하는 값을 별도의 interfae를 만들어서 상속을 통해 만들어준다면 명확하게 이해를 할 수 있을 것 같은데 일단은 기술부채라 생각해두고 어떻게 고칠지 고민을 해봐야겠다라는 생각이 드네요.

ISP와는 무슨 관계인지는 모르겠지만 ... 너무 큰 모듈을 쓰지말자! ... Tree shaking?

음... 그러면 함수와 클래스를 동일선상에 두지 말고 함수가 모여있는 모듈을 클래스와 비교를 해보면 어떨까요? 하나의 모듈에 너무 많은 기능들을 넣어서 덩치를 키우지 말라는 게 생각이 나네요. 프레임워크 기반이 유행하기 전에도 "이제는 jQuery를 쓰지 말자!" 라는 아젠다가 유행했던 적이 있습니다. 기본적으로 바닐라스크립트에서 document.querySelector()element.cloesst()와 같은 기능을 제공하게 되면서 내가 jQuery의 $(el).on("click", ...) 을 하나 쓰기 위해서 엄청난 크기의 모듈을 다운 받아야 하는 것은 비효적이기 때문이었죠


출처: Tree shaking - https://linguinecode.com/post/reduce-css-file-size-webpack-tree-shaking

이후 필요없는 코드는 포함하지 않고 필요한 코드만 포함시키는 방식인 Tree-shaking이 등장하면서 이러한 부분들이 많이 해소가 되었습니다. Tree-shaking을 하기 위해서는 거대한 클래스를 만들기보다는 함수로 쪼개야만 가능했기에 점점 더 라이브러리가 객체형보다는 함수형이나 모듈형으로 제공되는 경우가 늘어가게 되는것 같습니다.

(이러한 이야기들이 ISP의 관점에서 생각을 해볼 수 있을까? 하는 생각이 듭니다만 ISP다 라고 얘기를 하기에는 제가 객체지향의 전문가는 아니기에 댓글로 좀 알려주세요. ㅎ)

D - DIP / 의존관계 역전 원칙

의존관계 역전 원칙 (Dependency inversion principle)
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

이 역시 직관적으로 이해해봅시다. 우리가 전기기구를 사용하기 위해서는 콘센트에 플러그를 꽃는 방법만 알면됩니다. 실제로 전기의 배선을 붙여가며 전기기구를 사용하지 않죠. "전기를 이용하기 위해서는 플러그를 꽃으면 된다."(추상화) 라는 추상화된 방법만 전달을 하고 있다면 플러그에서 실제 전기 배선이 어떻게 되던간에(구체화) 사용자는 관여하지 않아도 됩니다. 우리가 필요한것은 전기이며 실제로 전기를 얻기 위한 구체적인 방법이 아니니까요.


출처: https://doublem.org/SOLID_LSP_ISP_DIP/

추상화하는 방향으로 의존하라. 상위 레벨 모듈이 하위 레벨 세부 사항에 의존해서는 안된다.

보통 우리가 보편적으로 많이 사용하고 있는 React Componentcustom hookaxios API 정도로 생각을 해보았습니다.

잘 만들어진 구조로 인해서 컴포넌트에서 서버의 데이터를 조작하기 위해서는 적절한 수준의 추상화와 레이어가 존재를 하게 됩니다.

axios의 레이어를 통해서 서버와의 데이터를 주고 받습니다. custom hook을 통해서 Component에서는 서버에 직접 호출하는 구체화보다는 그저 필요한 데이터를 요청하는 형식의 추상화된 layer 계층인 hook을 사용할 수 있습니다.

이렇게 추상화된 레이어를 두는 이유는 컴포넌트 입장에서는 데이터가 필요한거지 그게 반드시 서버의 데이터일 필요는 없기 때문입니다. 이러한 레이어를 통해서 언제든 서버가 아니라 로컬의 mock데이터나 다른 방식으로도 사용하는 쪽의 코드를 변화없이 변경할 수 있게 됩니다.

만약 Component에서 axios를 호출하거나 fetch를 바로 호출을 한다면 구체적인 부분에 의존을 하면 안된다는 DIP 원칙에 어긋나기에 좋은 설계가 아닐 수 있겠지요.

또한 레이어를 벗어나 axios를 다루는 모듈에서 컴포넌트의 props을 조작하는 등 레이어의 범위를 벗어나는 코드 역시 DIP에 어긋나는 설계입니다.

당연한거 아닌가요? 하시겠지만 현업에서는 일단 마감과 당장의 버그 수정!이라는 달콤한 유혹에 레이어를 넘나드는 코드들이 스물 스물 만들어지는 것은 일도 아니랍니다. ㅋ

끝으로..


출처: https://medium.com/software-and-technology/why-is-clean-code-important-8df4c5f1041f

프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다
출처: 위키백과 - SOLID 원칙

객체지향 프로그래밍으로 먼저 개발을 배웠고 SOLID도 알고 있었고 지금은 함수형 프로그래밍으로 개발을 하고 있다보니 가볍게 그리고 재밌게 설명을 할 수 있지 않을까 했던 주제를 이렇게 오랫동안 쓰고 고민을 하게 될 줄은 몰랐습니다.

특히 객체지향에만 있는 개념으로 만들어진 LSP, ISP 부분이나 DIP의 설명을 하면서도 이게 SOLID를 제대로 아는 분들이 보기에도 맞을까? 하는 생각을 많이 하게 되었네요.

글을 적고 자료를 찾으면서도 계속 질문을 해주셨던 SOLID라는 원칙을 현업에서 자바스크립트, 타입스크립트에서 어떻게 사용하고 있는지 궁금하다는 질문에 이게 만족스러운 답변이 될까? 하는 생각이 듭니다. SOLID 원칙이 확실히 객체지향을 중심으로 하다보니 현재쓰는 언어로 설명하기 어려운 부분들이 많더라구요.

그래서 억지로 SOLID와 연결을 짓기보다는 SOLID가 지향하고자 하는 방향에 대해서 직관적으로 어떤 코드가 좋은지를 알 수 있도록 이미지화 시키기 위해서 많은 노력을 해보았습니다.

단순히 SOLID는 예전에 나온 객체지향을 바탕으로 하는 패러다임이니 현대의 함수형 프로그래밍과는 잘 맞지 않는 것이다라고 결론을 내리는 것은 무책임한것 같더라구요. 관련 자료를 찾다가 비슷한 관점을 발견하게 되어 공유 드려봅니다.

복잡한 난장판을 만드는 가장 좋은 방법은 모두에게 “심플한 게 최고야”라고만 말한 뒤에 그렇게 하려면 어떻게 해야하는지 아무런 안내도 해주지 않는 것이라구요.
- 객체지향 5원칙 (SOLID)은 구시대의 유물?

이 글이 한번쯤 공부하면서 들어봤을 SOLID를 외워보는 대신 좋은 코드에 대한 원리나 본질을 생각해보고 특히 SOLID에서 추구하고자 하는 소프트웨어 코드 변경에 유리한 구조의 의미를 이해하고 본인의 코드를 한번 돌이켜 볼 수 있는 계기가 되기를 바랍니다.

감사합니다 ❤️


참고한 링크들

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)

https://github.com/labs42io/clean-code-typescript

https://stackoverflow.com/questions/5577054/solid-for-functional-programming

https://softwareengineering.stackexchange.com/questions/165356/equivalent-of-solid-principles-for-functional-programming

https://dev.to/patferraggi/do-the-solid-principles-apply-to-functional-programming-56lm

https://blog.knoldus.com/an-approach-to-solid-principles-object-oriented-vs-functional-programming/amp/

https://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-functional/

https://dev-gold.tistory.com/m/104

profile
Svelte, rxjs, vite, AdorableCSS를 좋아하는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

19개의 댓글

comment-user-thumbnail
2022년 1월 21일

잘 읽었습니다! 더 좋은 코드가 무엇일까 생각하게 되는 글인 것 같아요!

1개의 답글
comment-user-thumbnail
2022년 1월 22일

와 계속 글을 올리신거 보면 이런 주제를 쉽게 글로 표현하시는게 대단하다고 느껴요.. 좋은 내용과 멋진 전달 매번 감사합니다 :) ㅎㅎ 많이 자극 받습니다!

1개의 답글
comment-user-thumbnail
2022년 1월 23일

자바스크립트로 개발하면서 함수들에 SOLID원칙을 적용해서 리팩토링을 해도 되는가...에 대해서 의문이 들었었는데
글을 읽고 난 후 의문이 해소됐습니다. 감사합니다 😊

1개의 답글
comment-user-thumbnail
2022년 1월 23일

너무 좋은 글이에요!! 잘봤습니다 :)

1개의 답글
comment-user-thumbnail
2022년 1월 24일

잘 작동하는 코드보다 잘 읽히는 코드에 집착하는 경향이 있는 사람인데 도움이 많이 되었어요! 여러번 다시 읽어봐야 할 글이라고 생각되네요🧐👍

1개의 답글
comment-user-thumbnail
2022년 1월 24일

명문입니다 👏👏👏

1개의 답글
comment-user-thumbnail
2022년 1월 27일

저는 2년전 자바스크립트로 개발을 시작하여 프론트엔드로서 취업문을 두드리고 회사에 들어와서 자바스크립트로 클린아키텍쳐를 적용하며 개발을 했습니다.(회사에서 클린아키텍쳐 기반으로 서비스를 개발) 개발을 하다보니 항상 너무 힘들었던것이 "자바스크립트는 자바가 아닌데 SOLID를 어떻게 적용하라는거지?" 라는 고민을 정말 많이 했었습니다. 적어주신 내용들이 세세한 부분들이 그런 고민들이었는데 정말 명쾌히 작성해주신것 같아 기쁩니다. 다시 처음에 왜 그런 고민들을 했고, 왜 힘들어했는지 다시한번 되새김을 할 수 있어서 너무 좋았습니다. 감사합니다.

1개의 답글
comment-user-thumbnail
2022년 1월 29일

와 엄청난 내공의 글입니다 잘 읽었습니다 :)

1개의 답글
comment-user-thumbnail
2022년 2월 7일

평소에 생각해보던 내용이였는데 정말 좋은글이였고, 이와 관련된 생각을 정리할 수 있었습니다!

읽다보니 ISP의 예제중 Typescript interface를 구현할때 optional 프로퍼티를 사용하지 않고,
필수 프로퍼티만이 존재하는 interface를 구현후, 해당 프로퍼티를 extend해 사용하는 새로운 interface를 사용하는 방식을 권장하는 경우가 있습니다.
이러한 방식이 테오님께서 생각하시기에 ISP 원칙을 유지하기에 적절한 방법인지 궁금합니다.

interface User {
firstName:string
lastName:string
}

interface UserDisplay extends User {
displayName:string
}

1개의 답글