데이터 추상화와 닫힘 성질의 위력을 알아보기 위한 예시로 그림 언어(picture language)를 살펴보자.
참고로 이번 포스팅에서는 구체적인 벡터표현 방식이나 드로잉 함수등은 포함하지 않았다. 닫힘 성질을 충족하는 데이터 구조의 활용예시를 보면서 이 같은 추상화 방식이 얼마나 강력하고 유용한지만 알아볼 것이다.
그림 언어
그림 언어(picture language)는 간단한 그림 그리기용 언어이다.
화가
그림 언어에는 화가(painter
)라는 딱 한 종류의 요소만 있는데, 이 화가요소는 주어진 액자(frame
)에 들어맞게 기울이고 비례시킨 하나의 이미지를 그린다.
가령 화가요소 p, 액자객체 f가 있을 때, f를 인수로 하는 p를 호출하면 액자에 맞게 그린 이미지가 반환될 것이다
(참고로, 화가 요소가 어떤 그림을 어떻게 그릴지는 요소 내부에 정의되어있다. 앞에서도 말했듯 드로잉과 관련된 상세 구현은 생략한다)
화가 요소의 조합
이미지들을 조합할 때는 다양한 연산을 조합하여 기존 화가 요소들을 결합해서 새 화가 요소를 만든다.
const wave2 = beside(wave, flip_vert(wave));
const wave4 = below(wave2, wave2);
즉, 화가 요소가 화가를 다루는 연산들에 대해 닫힘성질을 만족하도록 설계된 것이다.
prev 화가 -> 연산(나란히 붙이기, 뒤집기, 돌리기 등) -> new 화가
그림 언어에서 복잡한 이미지를 구축해 나갈 수 있는 것은 이처럼 화가 요소들이 언어의 조합 수단하에서 닫혀있는 덕분이다.
액자 객체
화가 요소는 그림의 틀을 나타내는 액자(frame) 객체를 이용하여 이미지를 변환한다.
이미지 자체는 단위 정사각형(0 ≦ x, y ≦1) 안의 좌표들로 나타나고, 액자객체에 의해 기울어지고 비례되고 위치가 결정된다.
이 액자 객체는 총 3개의 벡터(원점 벡터 1개, 변 벡터 2개)로 표현할 수 있다.
- 원점 벡터(origin vector)
액자가 놓일 위치를 결정하는 원점 벡터. 액자 원점의 오프셋을 지정한다. - 변 벡터(edge vector)
액자 원점을 기준으로 한 액자 꼭짓점 오프셋을 지정한다.
데이터 추상화의 원칙에 따라 설계된 액자객체에는 다음과 같은 생성자 함수, 선택자 함수가 정의되어있다
- 생성자 함수
make_frame(origin_vector, edge1_vector, edge2_vector)
: 벡터 세 개를 받아서 하나의 액자 객체를 생성 - 선택자 함수
origin_frame(frame)
,edge1_frame(frame)
,edge2_frame(frame)
: 액자 객체를 받아서 세 벡터를 돌려줌
화가 요소에 대한 연산
앞서 언급한 화가 요소에 대한 연산들(뒤집기, 돌리기 등)은 transfrom_painter
함수를 이용하여 정의할 수 있다.
transfrom_painter(painter, origin, corner1, corner2);
이 함수는 인수로 화가요소(painter)와 변환할 새 프레임의 꼭짓점(origin, corner1, corner2)을 받는다.
function transform_painter(painter, origin, corner1, corner2) {
return frame => {
const m = frame_coord_map(frame);
const new_origin = m(origin);
return painter(make_frame(
new_origin,
sub_vect(m(corner1), new_origin), // 시작점 - new_origin, 끝점 - m(corner1)인 벡터
sub_vect(m(corner2), new_origin))); // 시작점 - new_origin, 끝점 - m(corner2)인 벡터
};
}
여기서 frame_coord_map
은 액자객체를 인수로 받아 액자 좌표맵을 생성하는 함수로, 이 함수가 리턴하는 함수에 벡터를 인수로 넘기면 액자객체로 인해 사상된(변환된) 벡터를 돌려준다.sub_vect
는 벡터의 뺄셈을 계산하는 함수이다. (cf. 벡터뺄셈의 결과는 종점 -> 시작점
벡터이다.)
이 함수를 이용하면 화가요소에 대한 여러 가지 변환연산을 정의할 수 있다.
// 상하 반전
const flip_vert = painter =>
transform_painter(painter, make_vect(0, 1), make_vect(1, 1), make_vect(0, 0));
// 오른쪽 윗부분으로 축소
const shrink_to_upper_right = painter =>
transform_painter(painter, make_vect(0.5, 0.5), make_vect(1, 0.5), make_vect(0.5, 1));
// 반시계방향으로 90도 회전
const rotate90 = painter =>
transform_painter(painter, make_vect(1, 0), make_vect(1, 1), make_vect(0, 0));
// 두 화가의 그림을 왼쪽 절반, 오른쪽 절반에 배치
const beside = (painter1, painter2) => {
const split_point = make_vect(0.5, 0);
const paint_left
= transform_painter(painter1, make_vect(0, 0), split_point, make_vect(0, 1));
const paint_right
= transform_painter(painter2, split_point, make_vect(1, 0), make_vect(0.5, 1));
return frame => {
paint_left(frame);
paint_right(frame);
};
}
이 처럼 화가 요소들을 함수적 표현으로 구현하면서 조합수단에 대해 닫힘 성질을 충족하게 설계한 덕분에 다음과 같은 장점이 생긴다.
1. 서로 다른 기본적인 그리기 기능들을 통일된 방식으로 처리할 수 있다.
2. 조합 수단들이 닫힘 성질을 충족하므로 복잡한 설계도 손쉽게 구축할 수 있다.
3. 함수를 추상화하는 데 쓰이는 모든 수단을 화가 요소들의 조합수단들을 추상화하는데 사용할 수 있다.
또, 위의 예시에서 계층화된 설계(stratified design) 접근방식도 엿볼 수 있다.
계층화된 설계
계층화된 설계란, 복잡한 시스템은 반드시 일련의 언어들로 서술한 일련의 수준(level)들로 구성해야 한다는 것이다.
각 수준들은 각자 서로 다른 어휘(시스템의 특성들을 서술하는)와 능력(시스템을 변경하는)을 제공한다.
잘 계층화된 설계는 프로그램을 견고하게 만드는데 도움이 된다. 프로그램 수정이 필요해도 계층 내에서만 수정하면 되므로 조금만 고쳐 쓰면 된다.
그림 언어에서는 점과 선을 서술하는 원시요소(원시화가)와 그것들을 조합하는 수단을 제공한다.
그리고 그 조합수단을 다시 조합해서 더 복잡한 조합을 만들어내는 더 높은 수준의 모듈을 생성하는 방식으로 계층화된 설계를 구현하고 있다.
함수 추상화든 데이터 추상화에서든, 표현수단(저수준의 구체적인 구현)은 있다 치고 고수준에서부터 알고리즘을 정의해 나가는 방법이 복잡한 시스템을 설계하는 데 있어 상당히 유용한 방식임을 알 수 있었다. 그리고 이 방식에 의해 자연스럽게 계층화된 설계가 가능해진다. 저수준 모듈의 구체적 동작방식에 대해 모른 채 설계해 나가기 때문에 계층 너머의 구현과 격리된 개발을 할 수 있다.
'프로그래밍 > SICP' 카테고리의 다른 글
다중표현이 존재하는 데이터 구조 설계(1) - 복소수 모델 (0) | 2024.03.06 |
---|---|
허프먼 부호화 트리 구현과 복호화 (0) | 2024.02.28 |
닫힘 성질을 충족하는 데이터 구조 설계(1) (0) | 2024.02.24 |
거듭제곱, 최대공약수, 소수판정 최적화 (+ 확률에 의한 판정) (0) | 2024.02.20 |
고차함수를 이용한 추상화 (0) | 2024.02.15 |