'프로그래밍'에 해당하는 글 2건


해커와 화가
국내도서
저자 : 폴그레이엄 / 임백준역
출판 : 한빛미디어 2014.01.06
상세보기



읽은 기간: 2015.12.28~30


연말이라 대부분 휴가를 떠나서 사무실이 무척 한가하다. 모처럼만에 사무실에서 여유를 부리며 인터넷에서 이것 저것을 찾아보다가 우연히 해커와 화가의 일부분을 인용한 글을 보게 되었다.


초등학교 시절에 선생님이 가르쳐준 대로 연필이 잡히지 않아서 괴로워했던 것처럼, 나는 오랫도안 이런 프로그래밍 방식에 대해서 남몰래 부끄러워했다. 하지만 내가 그 당시에 화가나 건축가 갈은 다른 창조자들이 일하는 방식을 알았더라면 내가 프로그래밍 하는 방식을 지칭하는 특별한 이름이 있다는 사실을 알 수 있었을 것이다. 그 이름은 바로 스케치이다. 적어도 내가 보기에 대학 시절에 내가 배운 프로그래밍 방식은 전부 잘못되었다. 소설가, 화가, 그리고 건축가가 그런 것처럼 프로그램이란 전체 모습을 미리 알 수 있는 것이 아니라 작성해 나가면서 이해하게 되는 존재이다.


한동안 내가 고민했던 문제에 대한 명쾌한 설명이었다. 곧바로 전체 글을 읽고 싶어서 이래저래 알아보다가 다행히 회사 도서관에서 바로 빌려볼 수 있었다. 이 책은 사실 프로그래밍 분야에서 고전으로 손꼽히는 책이다. 2005년도에 한빛미디어에서 출간되었다가 한동안 절판되었는데 다행히 작년에 다시 발매가 되었다. 만약 오늘 도서관에서 대출을 할 수 없었다면 퇴근길에 서점에 들려 구매했을 것이다.


이 책은 저자 폴 그레이엄이 자신의 홈페이지에 올린 컬럼 중에 몇 개를 선택해서 책으로 출간한 것이다. 이곳에는 책에 있는 글들 뿐만 아니라 최근까지 올라온 저자의 다른 글들을 볼 수가 있다. 물론 영어로.


이 책에서 그는 상당히 도발적이고 새로운 생각을 펼쳐내고 있다. 혹자의 평가처럼 어떤 부분은 다소 편향적인 부분도 있다. 이를테면 부의 분배에 관한 문제나 Lisp 언어의 우월성, 스타트업에 대한 (매우 매우) 긍적적인 시각 등등. 하지만 이러한 주장에는 자신 나름대로의 분명한 이유가 있고 이를 논리적으로 기술하였다. 그래서 그의 주장에 반대하는 사람에게도 이 책은 즐거운 지적 도전이 된다.


개인적으로 책이 너무 맘에 들어서 구매하기로 결정했다. 맘에 드는 부분들, 소개하고 싶은 부분들은 책을 구입해서 다시 한번 읽으면서 포스팅하려고 한다.


WRITTEN BY
trowind
자연어처리, 프로그래밍, 여행, 음식, 삶의 기록

트랙백  0 , 댓글  0개가 달렸습니다.
secret
- 이 글의 내용은 거의 대부분 C 언어에서도 동일해게 해당된다. 하지만, C와 C++은 엄연히 다른 언어이고, 이 내용을 C 컴파일러로 검증해보지는 않았기 때문에 굳이 "C++"라고 명시하였다.

- g++ 버전 4.1 에서 테스트하였다. 하지만, 표준적인 내용이므로 VC 컴파일러나 다른 컴파일러에서도 동일할 것이라 생각한다.

차원은 정해져있지만, 각 차원의 크기는 사용 시점에 결정되는 다차원 배열을 함수 인자로 받아 처리하고자 할 때 어떻게 해야 할까?

예를 들어 행과 열의 개수가 각각 X, Y인 이차원 배열을 함수 인자로 넘기고자 할 때 함수를 어떻게 정의하고 실제 코드에서는 어떻게 사용하면 좋을지에 대해 생각해봤다. 물론 배열에 넣는 값도 int, float 등으로 미리 정해져있다고 하자. 1차원 배열의 경우에는 문제가 쉽지만, 이차원 이상으로 가면 C++ 언어 특성상 문제가 복잡해진다.

1. 가장 쉽게 생각해볼 수 있는 함수 선언문은 다음과 같다.
void foo(int a[X][Y]);

이 함수는 X, Y가 컴파일 타임에 이미 상수로 정해져야 한다. 즉, 코드에서 X, Y는 #define 문으로 정의한 상수이거나 1, 5, 100 과 같은 상수값으로 바꿔야지만이 컴파일된다.
하지만, 이 함수는 가로, 세로가 X, Y가 아닌 다양한 크기의 배열에 대해 사용할 수도 있기 때문에 이 방법은 사용할 수 없다.

2. 두 번째 후보는 다음과 같다.
void foo(int a[][], size_t X, size_t Y);

이 함수는 배열의 가로, 세로 크기를 변수로 받을 수 있으므로 앞에서 이야기한 문제가 해결된 것처럼 보인다. 하지만, 함수를 이렇게 선언, 정의하면 컴파일이 되지 않는다. 원인은 "a[][]" 부분 때문인데, C++에서는 다차원 배열에 접근하기 위해서는 뒷부분 차원 (이걸 공식적으로는 뭐라고 부르는지 모르겠다)의 크기를 알아야 한다. 즉, 여기서와 같이 이차원 배열을 인자로 넘길 때는 최소한 뒤에 위치한 Y차원의 크기를 알아야한다. 예를 들어 우리가 8 X 6 크기의 배열을 다룬다면 a[][6]과 같이 선언해야 한다. 이것은 C++에서 배열을 다루는 방식을 생각하면 당연한 것이다. 우리가 몇 차원의 배열을 선언하든지 프로그램은 1차원의 연속된 메모리를 사용할 뿐이다. 8 X 6 크기의 이차원 배열에서 a[2][3]은 a[2*6+3]과 동일한 메모리 공간을 가리킨다. 이 말은 C++에서 이차원 배열에 접근하려면 뒤쪽 차원, 즉 이 예제의 경우에서는 Y차원의 크기가 6이라는 것을 알고 있어야지만 올바른 메모리에 접근할 수 있다.

3. 세 번째 후보는 다음과 같다.
void foo(int **a, size_t X, size_t Y);

이 함수는 사용하는 쪽에서 foo( (int**)a , 8, 6) 과 같이 타입 캐스팅을 이용하면 어찌됐던  컴파일은 가능하다. 하지만, 프로그램을 실행하면 99% 확률로 segmentation fault 에러를 띄우며 죽을 것이다. 이것을 이해하기 위해서는 2차원 배열 != 2중 포인터 (일반화하자면 n차원 배열 != n중 포인터) 라는 사실을 이해해야 한다.
잘 알려져 있다시피, C++에서 배열은 포인터 조작으로도 동일하게 접근 가능하다. a가 1차원 배열이라면 a[5]와 *(a+5)는 동일한 메모리에 위치한 값을 가리킨다. 이러한 사고의 연장선으로 이차원 배열은 이중 포인터라고 생각하지 쉬우며, 사실 상당 부분 맞는 말이다. 하지만, 여기서와 같이 이차원 배열의 위치를 이중 포인터로 변환해서 넘기는 것은 매우 잘못된 것이다. 다음과 같은 코드를 생각해보자.

int a[8][6];
a[0][0]=10;
a[1][3]=20;
foo((int**)a, 8, 6);
이 코드 자체는 별 문제가 없어 보인다. 그럼 이제 foo() 함수에서 어떤 일인 벌어지는를 살펴보자.
먼저 foo()함수가 이차원 배열의 (0,0)의 값을 읽으려고 한다고 해보자. 코드는 다음과 같을 것이다.

// foo() 함수 내부
int first_value = **a;

배열에서 맨 첫번째 원소는 별도의 조작 없이 접근 가능하다. 그런데, 과연 first_value에 10이라는 값이 제대로 들어갔을까?
이 프로그램에서 배열 a가 456번지에 할당되었다고 치자. 그러면
a   ==> 456
*a  ==> 10
**a ==> ?
가 된다. 즉, 이중 포인터를 따라가면 엉뚱한 메모리 번지의 값에 접근하게 된다.

a[1][3]은 어떨까? 일단 foo() 함수 내부에서는 인덱스 연산자 ([])을 쓸 수 없으니 다음과 같이 될 것이다.
(a+6*1+3)  ==> 456+6*1+3 = 465
*(a+6*1+3) ==> 20
**(a+6*1+3)==> ?

4. 네 번째 후보는 이중 포인터 대신에 단일 포인터를 사용하는 것이다.
void foo(int *a, size_t X, size_t Y);

이렇게 하면 일단 배열을 올바로 함수에 전달할 수 있고, 간단한 조작을 통해 올바른 메모리 번지에 접근할 수 있다. 하지만, 이 방법의 단점은 인덱스 연산자를 통해 원소에 접근할 수 없다는 것이다. 즉, a[3][2] 대신 (a+3*Y+2)과 같이 써야 한다. 이것은 배열의 차원이 높아지면 조작이 힘들고 실수하기 쉬워진다는 단점이 있다.

자, 그렇다면 과연 어떤 방법이 좋을까?
내가 생각한 방법은 C++의 템플릿 기능을 이용하는 것이다. 함수 선언은 다음과 같다.
template<size_t X, size_t Y> void foo(int a[X][Y]);

이 방법의 장점은 하나의 코드로 여러 가지 크기의 배열에 적용 가능하고, 인덱스 연산자도 쓸 수 있다는 점이다. 단점으로는 template을 이용하기 때문에 컴파일 시간이 (아주 약간) 증가하고, 소스코드가 외부로 노출되어야 한다는 점이다. 소스코드 노출 문제는 template instantiation과 관련 있는 주제로 자세한 설명은 후일을 기약하기로 한다(정말 나중에라도 이 문제를 다룰 일이 있을까?)

사용 코드는 간단하다. 말 그대로 그냥 사용하면 된다.
 

int a[10][10];
int b[5][8];
// a에 값을 넣음
// b에 값을 넣음
foo(a);  // 10X10 배열을 인자로 받는 함수
foo(b);  // 5 X 8 배열을 인자로 받는 함수.
 


배열을 인자로 넣어주면 사이즈에 맞춰서 자동으로 (내부적으로) 해당 배열을 다루는 함수가 
instantiation되기 때문에 별도로 신경쓸게 없다.

template을 지원하지 않는 C의 경우에는 이 방법을 쓸 수 없다. 그 부분에 대해서는 아직 해결책을 찾지 못했다.

'Programming' 카테고리의 다른 글

log(1+x) 계산하기  (1) 2012.07.27
date 명령어[Linux/Unix] 사용  (0) 2012.07.26
Python을 이용한 노래 검색  (1) 2011.07.06
C++에서 다차원 배열을 함수 인자로 넘기기  (4) 2011.03.08

WRITTEN BY
trowind
자연어처리, 프로그래밍, 여행, 음식, 삶의 기록

트랙백  0 , 댓글  4개가 달렸습니다.
  1. 허용진 2011.05.06 09:40
    이주영 딱 걸렷어.
    이런걸 몰래 만들어 놓고.....
  2. 몰래는 아니고^^, 단지 아주 가끔씩 글을 올리지
  3. int a[10][10];
    int b[5][8];
    // a에 값을 넣음
    // b에 값을 넣음
    foo(a); // 10X10 배열을 인자로 받는 함수
    foo(b); // 5 X 8 배열을 인자로 받는 함수.

    설명해 주신 소스 그대로는 아니구요.

    Template<size_t x,size_t y>
    void problemSove(char garden[x][y]);

    대충 이런 형식인데.

    problemSove<10,12>(Wide);

    이렇게 써야 컴파일러가 읽더라구요.

    <10, 12> 을 빼면 추론 할수없다는 에러가 나오는데.

    어떻게 해면 <10, 12>를 빼고 자동으로 사이즈를 잡아주나요???
  4. 이준호 2015.04.19 16:32
    소프트웨어를 전공하는 대학생입니다.
    포스팅 정말 잘 읽었습니다. 감사합니다.
secret