blog
October 11, 2021
오늘은 어제에 이어 while 문 내부 코드를 함수화하는 경우에 대해 알아보자.
int is_alpha(char ch)
{
return (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z'));
}
void cpy_all_word(char *target_buf, const char *source_str)
{
while (*source_str != '\0')
if (is_alpha(*source_str))
{
while (is_alpha(*source_str))
*target_buf++ = *source_str++;
*target_buf++ = ' ';
}
*target_buf = '\0';
}
어제 위와 같은 코드를 작성했었다.
그리고 while 안에 있는 while 은 함수화 하기 좋아보였다.
한 단어를 복사하는 기능은 다른 곳에서 또 재사용 될 수 있기 때문이다.
그럼 제대로 동작하지 않겠지만, 일단 이 부분을 함수로 분리해 보자.
int is_alpha(char ch);
void cpy_a_word(char *target_buf, const char *source_str)
{
while (is_alpha(*source_str))
*target_buf++ = *source_str++;
}
void cpy_all_word(char *target_buf, const char *source_str)
{
while (*source_str != '\0')
if (is_alpha(*source_str))
{
cpy_a_word(target_buf, source_str);
*target_buf++ = ' ';
}
*target_buf = '\0';
}
이렇게 한 단어를 복사하는 함수를 분리해 보니, target_buf
와 source_str
의 값이 호출된 함수(callee) cpy_a_word()
로부터 호출한 함수(caller) cpy_all_word()
로 전달되지 않는 문제가 발생한다.
그래서 이미 callee 가 작업한 부분을 caller 가 다시 접근하게 된다.
이 문제를 해결하기 위해서는 callee 가 반환된 후에 caller 의 target_buf
과 source_str
의 값을 의도에 맞게 수정해 줄 필요가 있다.
즉, callee 가 caller 에게 어디까지 작업을 진행했는지에 대한 정보를 건내 줄 방법을 마련해 줘야 하는 것이다.
하지만 불가피한 경우에는 callee 가 수행하는 내용을 이해하여, caller 에서 이를 다시 계산하게 하여 값을 의도에 맞게 수정하도록 할 수도 있을 것이다.
나는 아래의 총 5 가지 해결책을 생각해 보았다.
int is_alpha(char ch);
void cpy_a_word(char *target_buf, const char *source_str);
void cpy_all_word(char *target_buf, const char *source_str)
{
while (*source_str != '\0')
if (is_alpha(*source_str))
{
cpy_a_word(target_buf, source_str);
while (is_alpha(*source_str))
{
++target_buf;
++source_str;
}
*target_buf++ = ' ';
}
*target_buf = '\0';
}
이 방법은 callee 를 이해하고 있다면 caller 의 코드만 임시적으로 수정하여 빠르게 적용할 수 있다는 장점이 있다고 생각한다.
대신 callee 에서 수행한 작업을 caller 가 중복하여 수행하므로 ‘overhead 가 작지 않다’ 고 생각한다.
게다가 caller 에서 cpy_a_word()
를 여러번 호출해야 한다고 한다면, 매 호출마다 바로 다음에 target_buf
와 source_str
의 값을 수정하는 코드가 붙어야 하기 때문에 반복하는 코드에서 사용하기 위한 함수화가 제대로 이루어졌다고 말하기 힘들어 보인다.
지금 구현한 cpy_a_word()
는 일반적인 용도로 함수화되어 있으며, source_str
을 반복하는 반복문 안에서 사용하기 위한 용도에 적합하지 않아 보인다.
int is_alpha(char ch);
const char *cpy_a_word(char *target_buf, const char *source_str)
{
while (is_alpha(*source_str))
*target_buf++ = *source_str++;
return (source_str);
}
void cpy_all_word(char *target_buf, const char *source_str)
{
const char *source_word_start;
while (*source_str != '\0')
if (is_alpha(*source_str))
{
source_word_start = source_str;
source_str = cpy_a_word(target_buf, source_word_start);
target_buf += source_str - source_word_start;
*target_buf++ = ' ';
}
*target_buf = '\0';
}
이 방법은 callee 의 내부 구현을 살짝 수정하여 위의 이전 경우에서 발생하는 overhead 를 상당히 줄일 수 있어 보인다.
하지만 cpy_a_word()
를 여러 번 호출해야 한다면 호출할 때마다 추가적인 코드가 붙는다는 문제는 이전 경우와 동일하게 존재한다.
게다가 cpy_a_word()
가 작업 후의 source_str
를 반환하는데, 이 반환 값이 함수의 역할 및 이름과 어울리지 않는다는 문제도 있다.
즉, 함수의 역할을 이해한 상태에서 함수의 이름만 보고서는 cpy_a_word()
가 무엇을 반환할지 직관적 또는 관습적으로 알기 어렵다는 것이 문제이다.
하지만 그럼에도 불구하고 빠르고 가볍게 적용이 가능하다는 이점 덕분에 자주 사용하게 되는 것 같다.
int is_alpha(char ch);
void cpy_a_word(char **p_target_buf, const char **p_source_str)
{
while (is_alpha(**p_source_str))
*(*p_target_buf)++ = *(*p_source_str)++;
}
void cpy_all_word(char *target_buf, const char *source_str)
{
while (*source_str != '\0')
if (is_alpha(*source_str))
{
cpy_a_word(&target_buf, &source_str);
*target_buf++ = ' ';
}
*target_buf = '\0';
}
이 방법은 cpy_a_word()
를 함수로 분리하기 전과 비교하여 overhead 가 거의 없다.
이전에 제시한 방법과 다르게 caller 에서 여러 번 호출하더라도 추가적인 작업을 할 필요가 없기 때문에 반복하는 상황을 위한 용도로는 잘 함수화가 된 것 같다.
하지만 caller 의 값을 callee 가 바꾸기 때문에 사용자가 제대로 알고 사용하지 않으면 의도치 않게 source_str
과 target_buf
의 값을 바꾸게 되는 결과를 초래할 수 있다.
따라서 cpy_a_word()
라는 함수는 결국 일반적으로 사용되기 어렵고 반복하는 코드 내부에서 호출되야 편하기 때문에, 반복하는 코드에 다소 의존적인 특징이 생겼다.
추가적으로, 이 방법은 이중 포인터를 사용해야 하기 때문에 가독성이 떨어지는 문제도 있다.
int is_alpha(char ch);
typedef struct s_str_relay
{
char *target_buf;
const char *source_str;
} t_str_relay;
void cpy_a_word(t_str_relay *relay)
{
while (is_alpha(*relay->source_str))
*relay->target_buf++ = *relay->source_str++;
}
void cpy_all_word(t_str_relay *relay)
{
while (*relay->source_str != '\0')
if (is_alpha(*relay->source_str))
{
cpy_a_word(relay);
*relay->target_buf++ = ' ';
}
*relay->target_buf = '\0';
}
이 방법은 바로 이전의 방법과 상당히 유사하다.
단지 target_buf
와 source_str
을 t_str_relay
라는 구조체로 감싼 것 뿐이다.
더 이상 이중 포인터가 쓰이지 않고, 가독성이 더 좋아졌다.[주석:1]
동시에 cpy_a_word()
에 구조체 t_str_relay
에 대한 의존성이 생겼다.
#define FALSE 0
#define TRUE 1
int is_alpha(char ch);
void cpy_a_ch(char *target_buf, const char *source_str)
{
*target_buf = *source_str;
}
void cpy_all_word(char *target_buf, const char *source_str)
{
int is_in_word;
is_in_word = FALSE;
while (*source_str != '\0')
if (is_alpha(*source_str))
{
cpy_a_ch(target_buf++, source_str++);
is_in_word = TRUE;
}
else
{
if (is_in_word)
*target_buf++ = ' ';
++source_str;
is_in_word = FALSE;
}
*target_buf = '\0';
}
이 방법은 현재의 상태를 변수에 저장해서 각 loop 마다 상태에 따라 적절한 작업을 수행하도록 한 것이다.
따라서 함수화된 부분은 단어 전체를 옮기는 작업이 아니라 단 한 문자를 옮기는 작업으로 변하게 된다.
지금은 is_in_word
라는 상태 변수 하나가 단어의 안에 들어왔는지 단어의 밖인지를 저장할 뿐이다.
하지만 프로그램이 복잡해지고 다양한 상태가 존재하게 된다면, 상태라는 변수를 배경에 둔 채로는 복잡한 작업을 서술하기가 상당히 어렵고 시간이 많이 소요될 것이다.
그에 비해 이 방식은 각 상태에 대한 행동을 정의하는 것으로, 작성자의 의도를 모순 없이 코드로 온전히 구현할 수 있다.
각 상태가 구체적으로 어떤 방향으로 흘러가는지 일일이 파악하지 않아도, 각 상태에서 어떤 상태로 옮겨갈 수 있는지 구분하여 생각하고 코드를 작성할 수 있다는 것도 장점인 것 같다.
즉, 프로그램 구조를 상태별로 분리하여 명확하게 생각하여 구현할 수 있게 되고, 따라서 불필요한 사고 과정이 생략됨으로써 코드를 작성하는 시간이 감소하여 생산성이 증가할 것 같다.
게다가 이런 식의 구조 자체가 상태 를 구체적으로 정의할 수만 있다면 온갖 곳에 적용시킬 수 있어서 작업을 어느 정도는 기계화할 수도 있을 것 같다.
단, 매 loop 마다 어떤 상태인지를 확인하는 if 문 등이 존재해야 하므로 어느 정도의 overhead 가 존재한다고 할 수 있다.
[1] relay 라는 단어가 여기서 적절한지는 잘 모르겠는데, 가독성을 높이기 위해서는 이런 구조체의 이름이 관습적으로 익숙한 것이어야 좋다.
만약 relay 라는 단어가 target_buf
와 source_str
을 포함하는 구조의 이름으로 적절한 선택이 아니었다면 가독성이 그다지 상승하지 않았을 수 있다.