Sleeep23' space
Front-end Engineer

자바스크립트로 배우는 SICP - 7. 데이터를 이용한 추상화

복합 데이터와 복합 데이터 객체를 알아보고, 유리수를 데이터 추상화를 통해 구현해보자!

2023-10-30 | 독서 | 12min


오늘 부분은 생소한 표현들이 자주 등장한다. 그러니 각 키워드의 정의를 자세히 살펴보고 그 개념이 적용된 형태를 분석하는 방식으로 글을 작성하겠다!

키워드

오늘의 키워드는 다음과 같다.

  • 복합 데이터와 복합 데이터 객체
  • 유리수와 데이터 추상화
  • 추상화 장벽

그럼 하나씩 키워드 중심으로 살펴보자!

복합데이터..?

우리가 컴퓨터로 해결할 때에는 단순한 데이터로 해결하기 어려운 문제들이 많다. 이러한 문제들을 해결하기 위해 복잡한 현상을 **모형화(modeling)**하기 위해 프로그램을 설계하는데, 다양한 측면을 가진 실세계의 현상을 모형화하려면 부품이 여러 개인 **계산적 객체(computational object)**를 구축해야 하는 경우가 많다.

이를 위해, 1장에서는 함수들을 조합하여 복합함수를 만들어 추상을 구축하는 방법에 대한 이야기를 나누었다. 2장에서는 데이터를 기준으로 추상화를 진행한다. 즉, 데이터 객체들을 조합하여 **복합 데이터(compound data)**를 만들어 추상을 구축하는데 사용한다!

그러면 복합데이터를 왜 구축하는 것일까? 복합 데이터를 구축하면,

  • 설계의 개념적 수준을 높이고
  • 설계의 모듈성을 높이며
  • 언어의 표현력을 향상시킬 수 있다!

이는 우리가 복합 함수를 구현했던 이유이기도 하다. 이전의 기억을 떠올려보면, 단순하게 원시적 표현인 연산자들의 반복 작성을 줄이기 위해 함수를 도입했었다. 이렇게 간단한 함수를 구축하여 해당 연산을 추상화했다. 그리고 이러한 방식으로 추상화된 여러 간단한 함수들의 조합으로(매개변수로 넣거나, 반환하거나, 내부에 선언하거나…등) 고차함수로 추상화할 수 있는 기반을 마련할 수 있었다. 데이터의 경우도 마찬가지이다. 원시 데이터 객체들을 하나의 복합 데이터 객체로 표현할 수 있다면 우리는 더 높은 수준에서 데이터를 다룰 수 있게 되는 것이다!

유리수를 데이터 객체들로

그럼 복합 데이터 객체는 어떤 것을 의미할까? 예를 들면, 유리수를 프로그램 내에서 표현한다고 생각하자. 이는 분자와 분모를 필요로 하며 이 둘의 조합으로 하나의 유리수를 얻을 수 있다.

그렇다면, 만약 어떤 프로그램 내에서 유리수를 자주 이용한다면 그 프로그램을 어떻게 작성할 수 있을까? 우선 단순하게 떠오르는 것은 그냥 ab\frac{a}{b} 라는 형태를 직접 작성할 수 있다. 하지만, 자주 이용되는 경우이기에 유리수를 활용하는 타 연산에 유리수 표현을 위한 나눗셈 이라는 원시적인 개념이 더해져 프로그램을 구현하는 우리의 뇌 속은 복잡해진다!

그러면 유리수를 구성하는 정수, 나눗셈과 같은 원시적 데이터 객체들의 조합으로 복합 데이터 객체를 어떻게 표현할 수 있을까?

데이터의 추상화

유리수를 만들기 이전에 우선 데이터 추상화가 무엇인지 조금 더 살펴보자!

우선 함수적 추상은 함수의 구현 방법을 숨길 수 있으며, 전체적인 행동 방식이 동일한 임의의 다른 함수로 대체한다는 뜻이었다. 이 개념을 복합 데이터에 적용한 것이 바로 데이터 추상화이다. 이는, 복합 데이터 객체가 쓰이는 방식 과 그 복합 데이터를 좀 더 기본적인 데이터 객체들로 구축하는 구체적인 방식을 분리할 수 있게 하는 방법론이다!

데이터 추상화의 핵심은 복합 데이터 객체를 사용하는 프로그램이 추상 데이터에 대해 작동하도록 프로그램 구조를 짜는 것이다. 다른 말로 하면, 프로그램은 데이터에 관해 최소한의 가정만(주어진 과제를 수행하는 데 꼭 필요한 것만) 두어야 한다. 그와 함께, 구체적(concrete)인 데이터 표현을 해당 데이터를 사용하는 프로그램과는 독립적으로 두어야 한다.

시스템의 이 두 부분 사이의 인터페이스는 선택자라고 부르는 함수와 생성자라고 부르는 함수들의 집합으로 구성된다. 이 인터페이스는 구체적인 표현을 이용해서 추상데이터를 구현한다.

데이터 객체들을 유리수로

그럼 유리수를 구현하기 위해 앞서 언급한대로 선택자와 생성자를 정하자면 다음과 같다!

  • 생성자 : 분자와 분모로 유리수를 만드는 수단 → make_rat 함수
  • 선택자 : 분자와 분모를 추출하는 수단 → numer (분자) , denom (분모) 함수

이제 드디어 유리수를 만들어보자! 😓 빌드업이 길구만…

유리수로 표현하기 이전에 쌍 자료 구조를 이용할 것이다. 이는 pair 이라는 이름의 함수이며 다음과 같이 활용할 수 있다. (튜플처럼 생각하면 된다!)

const x = pair(1, 2)
head(x) // 1
tail(x) // 2

이를 이용하면 유리수 객체를 자연스럽게 표현할 수 있다! 다음을 보자.

function make_rat(n, d) { return pair(n, d); }
function numer(x) { return head(x); } // 분자이다
function denom(x) { return tail(x); } // 분모이다

위 처럼 작성하면, 이제 분자와 분모에 들어갈 정수만을 적어주면 유리수라는 시스템을 완성할 수 있다! 이는 굳이 나눗셈을 적용하지 않고 단순하게 분자와 분모의 쌍으로 표현한 것이다.(이건 이전까지 생각도 못해봤다. 굳이 연산을 안해도 시스템을 구상하며, 더욱 다양한 연산을 적용할 수 있겠네…?)

그럼 유리수를 출력하는 것은 별로 어렵지 않다! 다음 코드를 보자.

function print_rat(x) {
	return display(stringify(numer(x)) + " / " + stringify(denom(x)));
}

단순하게, pair 자료구조의 객체인 유리수의 분자와 분모의 쌍을 받으면 사이에 / 기호를 추가하여 문자열로 표시하면 된다!

그럼 유리수 간의 연산에서는 어떻게 이용될 수 있을까? 다음은 유리수의 사칙연산과 코드를 짝지어 놓은 것이다!

  1. 덧셈
n1d1+n2d2=n1d2+n2d1d1d2\frac{n_1}{d_1} + \frac{n_2}{d_2} = \frac{n_1d_2+n_2d_1}{d_1d_2}
function add_rat(x, y) {
	return make_rat(numer(x) * denom(y) + numer(y) * denom(x), denom(x) * denom(y));
}

  1. 뺄셈
n1d1n2d2=n1d2n2d1d1d2\frac{n_1}{d_1} - \frac{n_2}{d_2} = \frac{n_1d_2-n_2d_1}{d_1d_2}
function sub_rat(x, y) {
	return make_rat(numer(x) * denom(y) - numer(y) * denom(x), denom(x) * denom(y));
}

  1. 곱셈
n1d1n2d2=n1n2d1d2\frac{n_1}{d_1} \cdot \frac{n_2}{d_2} = \frac{n_1n_2}{d_1d_2}
function mul_rat(x, y) {
	return make_rat(numer(x) * numer(y), denom(x) * denom(y));
}

  1. 나눗셈
n1/d1n2/d2=n1d2d1n2\frac{n_1/d_1}{n_2/d_2} = \frac{n_1d_2}{d_1n_2}
function div_rat(x, y) {
	return make_rat(numer(x) * denom(y), denom(x) * numer(y));
}

  1. 등식
n1d1=n2d2n1d2=n2d1\frac{n_1}{d_1} = \frac{n_2}{d_2} \Longleftrightarrow n_1d_2=n_2d_1
function equal_rat(x, y) {
	return numer(x) * denom(y) === numer(y) * denom(x);
}

추상화 장벽

지금까지 유리수에 관련해서 많은 함수들을 정의했다. 그리고 이를 구축하는 순서대로 점점 복합적이라는 것을 눈치챘을 것이다. 그럼 이를 계층의 구조로 나타낸 다음의 그림을 보자!

유리수의 추상화 장벽을 표시한 그림이다. 참고

유리수의 추상화 장벽을 표시한 그림이다. 참고

위 장벽을 자세히 보면 전부 앞서 구현한 함수들이다! 여기서 중요한 점은 서로 구분되어 있다는 점이다! (뭐, 장벽이니 당연한거겠지만..?)

  • 최하위 장벽은 바로 pair , head , tail 함수들, 즉 자료구조 단의 장벽이다.
  • 유리수를 쌍 자료구조를 이용하여 구현한 선택자와 생성자인 make_rat , numer , denom
  • 선택자와 생성자를 이용하여 구현한 사칙연산인 add_rat , sub_rat
  • 그리고 그 위의 장벽에는 유리수를 이용한 프로그램들

“구분”될 수 있다는 것은, 즉 블랙박스로의 추상이 잘 되어 있다는 것이다! 다음 질문을 생각해보자.

  • make_ratnumer , denom 를 구현하기 위해 pair , head , tail 함수들을 이용했는데 이들이 내부가 어떻게 구현되었는지 신경썼는가?
  • add_rat , sub_rat … 등의 사칙연산을 구현하기 위해 make_rat , numer , denom 의 내부 구현을 신경썼는가?
  • 유리수의 사칙연산을 이용하는 프로그램을 작성하기 위해 그 사칙연산의 내부 구조를 신경쓰는가?

모두 아니다! 라고 대답할 수 있다. 즉, 전체 시스템에서 각 수준의 함수들은 추상화 장벽을 정의하고 서로 다른 수준들을 연결하는 인터페이스로 작용한다.

마무리

오늘은 데이터 추상화의 첫 섹션을 읽었다! 생각보다 생소한 단어들이 자주 등장해서 바로 이해하기는 어려웠지만, 하나하나 다시 곱씹어보니 저자가 어떤 이야기를 전달하고 싶은지를 알 것 같다는 느낌이 들어 보람차다! 어떻게 보면 이 장이 제일 도움이 되는 장이 아닐까? 특히 읽으면서 타입스크립트의 제네릭 이용에 도움이 될 듯한 느낌이 마구마구 든다. 쨋든 오늘 하루도 알찼다!