6 minute read

하나의 프로그램을 구성할때 우리는 여러 도구와 미리 구성된 소스코드들에 의존한다.
도구는 단순한 함수 단위 부터 react 와 같은 UI 라이브러리 까지 다양하다. 잘 만든 도구들은 알기 쉽게 추상화 되어 있으며, 우리는 최저 수준의 고민을 도구에 맡기며 고수준의 과업을 해결해나간다.

내가 이야기하고 싶어하는 도구란 무엇일까? 도구를 무조건적으로 사용하는것을 옳은가?
우선 도구의 기준을 정의해보고 어떻게 도구를 다루어야할 지 살펴보자.

도구는 저수준의 문제를 감춘다.

내가 생각하는 도구에 부합하는 가장 근본적인 개념 중 하나는 고차함수 이다. 자바스크립트의 Array.prototype.map 을 보자.

[1,2,3,4,5].map(num => num * 5) // [5, 10, 15, 20, 25]

이 아름다운 빌트인 메서드는 다음과 같은 주제를 추상화 한다.
복잡한 개념을 추상화 하고 프로그래머는 ‘어떻게’ 할 것인가를 정의한다.

  1. 반복을 어떻게 할 것인가? : 배열의 항목 만큼 반복할 것이다.
  2. 반복하며 무엇을 할 것인가? : 배열의 각 항목에 하고자 하는바 를 대입하여 새로운 값을 리턴한다.

이처럼 Array.prototype.map 의 함의하고 있는 주제는 ‘매핑’ 이다.
프로그래머는 매핑이 어떻게 이루어지는지에 대해는 관심을 가지지 않는다. 단지 하고자 하는바 를 함수라는 형태로 도구에 전달하고 도구는 그에 맡게 내가 원하는 바를 수행하여 나에게 돌려준다.
다음으로 좀 더 고수준의 도구를 살펴보자.

도구는 여러 프로그래머에 의해 정의된다.

도구는 다른 프로그래머에 의해 정의되고, 사용된다.
A 는 특정 문제를 해결하는 함수나 객체생성자를 만든다. 그리고 같은 팀원인 A, B, C 는 이 도구를 사용하여 같은 주제를 다루는 문제를 쉽게 해결한다.
가령 상품의 소비자가와 판매가에 대한 할인률을 계산하여야 하는 주제가 있다면 다음과 같은 도구를 정의하여 해결한다.

export function getDiscountRateBy (fullPrice: number, salesPrice: number) {
  return ((fullPrice - salesPrice) / fullPrice) * 100
}

getDiscountRateBy(2500, 1750) // 30. 30% 할인이 적용된 금액이다.

이러한 getDiscountRateBy를 A, B, C 개발자가 소스코드 전반에 사용한다면, 다음과 같은 현상이 나타난다.

  1. getDiscountRateBy 는 소스코드 전반에 결합도를 지니게 된다.
  2. getDiscountRateBy 의 공식이 재정의 될 경우 소스코드 전반에 영향을 미친다.
  3. 따라서 getDiscountRateBy 는 우리의 소프트웨어와 강하게 결합된다.

도구를 정의하고 사용하는것은 문제해결에 큰 도움을 준다.
도구를 신뢰하고자 할 경우 테스트코드 등을 작성하여 신뢰도를 높힐 수 있다.
만일 해당 도구의 내부 수식이 잘못되었을 경우에는 해당 도구를 수정하여 실수를 코드 전반을 수정하는 것이 아니라 내부 구현을 수정함으로 해결할 수 있다.

그렇다면 이러한 도구가 가지는 트레이드오프는 없는가?
트레이드오프는 존재한다. 해당 도구와 우리의 소프트웨어가 결합되는 것이다.

그럼에도 이러한 ‘공공재’ 도구를 정의하는것은 권장된다.
개개별 리소스를 절약할 수 있으며, 검증된 도구는 소프트웨어 전반의 신뢰도를 향상시킨다. 소프트웨어의 중복을 줄임으로 관리를 용이하게 한다.

다만 도구가 한가지 문제를 해결하는것이 아니라, 여러가지 문제를 해결하고자 하는 도구일 경우 상황이 복잡해진다.

타인이 쥐어준 맥가이버 나이프

swiss army knife

맥가이버 나이프는 멀티 툴 이다.
하나의 개체로 존재함에도 여러가지 문제를 해결하는데에 도움을 준다.
이러한 도구를 제조하는 관점에서는 이 도구가 유용하게 쓰이고자, 또는 시장에 잘 팔리고자 보다 유용한 기능을 많이 담고자 한다.

사용자의 관점에서는 나에게 당장 필요없는 기능이 몇가지 있더라도, 다른 많은 기능이 필요하다면 기꺼이 이 도구를 구매할 것이다.

소프트웨어의 세계에서는 이러한 맥가이버 나이프가 많다.
라이브러리와 프레임워크가 그 것 이다.

날짜와 시간의 맥가이버 나이프. momentJS

momentJS 는 날짜와 시간이라는 개념을 다루는데 특화된 라이브러리 이다. 이 도구는 대략 다음과 같은 기능을 제공한다.

이 도구의 유용함을 들여다보자. 직접 시험하고 싶다면 https://momentjs.com 에 접속 후 콘솔창에 moment 를 입력하면 실행가능한 전역 객체를 제공하는 것이 보일 것이다.

moment.locale('ko'); // 사용자의 로케일을 지정한다.

moment().format('YYYY-MM-DD'); // 2022-07-31. 현재 날짜를 리턴한다.
moment().format('LTS'); // 1:09:34 PM. 현재의 시간을 리턴한다.

const tomorrow = moment().add(1, 'days'); // 내일의 시간을 담는다.
tomorrow.format('YYYY-MM-DD'); // 2022-08-01. 내일의 날짜를 리턴한다.

moment().subtract(100, 'year').format('YYYY-MM-DD') // 100 년 전 날짜를 리턴한다.

moment 는 이처럼 날짜와 시간을 다루는데에 있어 유용하다.
세부사항에 대한 이해 없이도 메서드만 이해하면 사용할 수 있도록 적절히 추상화 되어있기 때문에 이 도구를 사용하면 할 일은 내가 필요한 시간대를 입력한 후, 그에 맞는 포멧을 입력하는 것 뿐이다.

이 유용한 도구를 우리의 소프트웨어에 초대하려면 해당 도구를 다운로드 받아 프로젝트에 import 한 다음 필요한 소스 코드 전반에 사용할 수 있다.

하지만 이러한 결정을 하기전에 조금 더 생각해볼 필요가 있다.

맥가이버 나이프는 부식된다.

소프트웨어에서의 도구는 현실의 도구와 닮아있다.

오픈소스의 기반 아래 시간이 지날수록 더 가볍고, 더 유용한 기능을 갖춘 도구가 생산된다.
잘 사용하던 도구가 유지보수 종료선언을 하기도 하거나 취약점이 발견되거나, 망가져서 소프트웨어 생태계 전반에 영향을 미치기도 한다.

이러한 상황이 발생하면 우리는 해당 도구를 다른 유용한 도구로 교체하거나, 기존에 도구가 해주던 역할을 우리가 직접 구현하던지 해야할 것이다.

나의 경우 momentJS 를 한동안 즐겨 사용하였고, 이 도구는 현재 deprecated 되었다. 구조가 오래되었고, 때문에 번들링 과정에서 트리 쉐이킹을 지원하기 어려우며, 불변성 관련된 문제가 있단것이 그 이유였다.

현재는 dayJS 라는 다른 대안이 사용되곤 한다. 그럼에도 내가 관리하는 일부 소프트웨어들은 아직 momentJS 의존성을 지니고 있다.

그 이유는 momentJS 가 이미 내가 구성한 프로젝트 전반에 강하게 결합되어 있고, 강하게 결합되어 있기 때문에 moment 와 결별하는데에 지나치게 많은 리소스가 들어가게 되는것 이 그 이유이다.

그렇다면 어떻게 다루어야 했을까?

대부분, 맥가이버 나이프의 모든 기능을 사용하지 않는다.

라이브러리, 프레임워크로 분류되는 도구들은 대부분 유용한 기능을 많이 담고자 하며, 그를 통해 유용하게 사용되고 주제에 부합하는 한 넒은 범위에 대한 해결 능력을 사전에 갖추어 놓고자 한다.

때문에 실제로 사용하지 않는 기능이 많음에도, 유용한 기능이 많기 때문에 이러한 도구들은 손쉽게 프로젝트에 초대되곤 한다.

이러한 도구가 초대되었을 때 프로젝트 전반에 도구의 어떠한 기능이 사용되고, 어떠한 기능은 사용되지 않는지 알 수 있는가?

가령 momentJS 를 사용할때에 우리의 프로젝트에서 format 기능만 사용하는지, subtract 기능은 사용하지 않는 지 알 수 있는 방법은 없는가?

도구가 부식되고, 교체가 필요해졌을 경우 이러한 범위를 신속하게 파악하고 교체할 수 있는가?

도구를 경계하라. 경계를 만들어라.

신중하게 도구를 사용한다면 상황이 닥쳤을 때 신속하게 이별하고 재정의 할 수 있다.

나의 소프트웨어 계층으로 도구를 입국 시킬때에는 신중하게 비자를 발급하자.
momentJSdatetime 이란 비자로 랩핑하여 우리의 소프트웨어로 신중히 입국시켜보자.

경계를 처리하는 wrapper 를 만들어라.

momentJS 를 그대로 사용하지 않는다. 함수나 객체생성자를 이용하여 momentJS 를 숨기고 소프트웨어에서 직접적으로 접근하여 사용하지 못하게 하자.

Datetime 을 다루는 객체생성자를 만듬을 통해 특정 소스코드만 moment 의존성을 지니도록 래핑하여보자.

현재 사용이 예상되는 moment.add, moment.subtract, moment.format 기능의 일부를 동일하게 메서드 체이닝이 가능하도록 제공하는것을 목적으로 한다.

// wrapper/datetime.ts
import moment from 'moment';

type DateFormat = 'YYYY-MM-DD' | 'LTS'
type DateUnit = 'day' | 'month' | 'year'

class Datetime {
	private _date: Date;

	constructor(date = new Date()) {
		this._date = date;
	}

	static datetime(date = new Date()) {
		return new Datetime(date);
	}

	public format(dateFormat: DateFormat) {
		return moment(this._date).format(dateFormat);
	}

	public add(amount: number, unit: DateUnit) {
		return Datetime.datetime(moment(this._date).add(amount, unit).toDate());
	}

	public subtract(amount: number, unit: DateUnit) {
		return Datetime.datetime(moment(this._date).subtract(amount, unit).toDate());
	}
}

export const datetime = Datetime.datetime;

완성된 래핑 계층은 위와 같다.

import {datetime} from "./wrappers/datetime";

datetime().format('YYYY-MM-DD') // 2022-07-31
datetime().add(1, 'year').format('YYYY-MM-DD') // 2023-07-31
datetime().subtract(1, 'year').format('YYYY-MM-DD') // 2021-07-31

사용례는 위와 같다.

moment 는 moment() 를 통해 메인함수를 호출하였을때 moment.Moment 라는 여러 메서드가 정의된 인스턴스 리턴하는데 반해, datetime()Datetime 클래스에 정의된 좁은 범위의 인스턴스를 리턴한다.

따라서 소프트웨어 단에서 datetime 을 사용하는 한 다음과 같은 이점을 지닌다.

  • 통제 가능 : 라이브러리 내에서 어떠한 기능을 사용할지 경계를 둠으로 사용되는 유형을 제한할 수 있다.
  • 경계의 분리 : 소프트웨어는 moment 에 직접적으로 의존하지 않는다. moment 에 직접적으로 의존하는것은 래핑 계층 뿐이다.

이러한 간단한 기교를 통해 맥가이버 나이프에서 내가 필요한 기능만 안정적으로 사용할 수 있다.
기성복을 맞춤형 정장으로 수선하는 것이다.

우아하게 의존성을 교체하라

시간이 흘러 moment 는 부식되었다. 우리는 트리쉐이킹이 가능한 경량화 라이브러리 dayjs 로 내부 부품을 교체하기를 원한다.

이 때 우리가 할 일은 datetime 래핑 계층 내부의 moment 의존성을 dayjs 의존성으로 대체하면 된다.

코드에 변경되는 부분은 moment 라는 선언을 dayjs 로 변경하는 것 뿐이다.

import dayjs from "dayjs";

type DateFormat = 'YYYY-MM-DD' | 'LTS'
type DateUnit = 'day' | 'month' | 'year'

class Datetime {
	private _date: Date;

	constructor(date = new Date()) {
		this._date = date
	}

	static datetime(date = new Date()) {
		return new Datetime(date)
	}

	public format(dateFormat: DateFormat) {
		return dayjs(this._date).format(dateFormat)
	}

	public add(amount: number, unit: DateUnit) {
		return Datetime.datetime(dayjs(this._date).add(amount, unit).toDate())
	}

	public subtract(amount: number, unit: DateUnit) {
		return Datetime.datetime(dayjs(this._date).subtract(amount, unit).toDate())
	}
}

export const datetime = Datetime.datetime;

시간이 흘러 dayjs 가 교체될 날이 온다면 동일한 방법을 적용할 수 있을것이다.

외부 라이브러리에 더 이상 의존하기 싫어진다면 제한된 인터페이스에 맞추어 직접 메서드를 구현할 수도 있을것이다.

github 레포지토리에 관련 소스코드를 남겨 두었다.

정리

경계를 처리하는 간단한 계층을 도입함으로 도구와 소프트웨어의 거리감을 유지할 수 있다.

이러한 간단한 wrapper 를 도입하여 소프트웨어를 외부 도구로 부터 보호할 수 있는 라이브러리는 많다.

다만 jQuery 나 React 와 같은 UI 라이브러리 의 경우라면 어떨까?
이와 같은 경계를 만드는것은 도전해 볼만한 주제이나 쉽지 않을것이다.

다만 이러한 라이브러리와 소프트웨어의 코어 비즈니스 로직을 분리함으로 우리의 핵심 구현을 지킬 수 있을것이다.

시간이 된다면 이러한 비즈니스 로직을 라이브러리와 분리하는 주제에 대해 포스팅 해보았으면 좋겠다.

이 아티클은 로버트 C. 마틴 의 클린 코드8장. 경계 를 상당부분 참고하였다.

Categories:

Updated: