Liemani

blog

홈으로

반복하며 작업하는 코드의 작성(2)

October 11, 2021


Index

  1. 조건에 따라 가변적 횟수 동안 특정 작업을 하는 반복 코드
  2. caller 가 callee 호출 후 바뀐 값을 직접 다시 계산하여 설정하기
  3. callee 가 어디까지 작업했는지 알려주는 인덱스 또는 주소를 반환하도록 하기
  4. 작업 위치를 기억하는 변수의 주소를 인자로 전달하기
  5. caller 나 또는 caller 를 호출하는 함수에 작업 위치를 저장하는 변수를 멤버로 갖는 구조체(structure) 를 두고, 이 structure 를 전달하기
  6. 진행 중인 작업을 flag 에 기록해두고, 매 반복(loop) 마다 체크하여 필요한 함수를 실행하도록 하기


조건에 따라 가변적 횟수 동안 특정 작업을 하는 반복 코드


오늘은 어제에 이어 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_bufsource_str 의 값이 호출된 함수(callee) cpy_a_word() 로부터 호출한 함수(caller) cpy_all_word() 로 전달되지 않는 문제가 발생한다. 그래서 이미 callee 가 작업한 부분을 caller 가 다시 접근하게 된다. 이 문제를 해결하기 위해서는 callee 가 반환된 후에 caller 의 target_bufsource_str 의 값을 의도에 맞게 수정해 줄 필요가 있다. 즉, callee 가 caller 에게 어디까지 작업을 진행했는지에 대한 정보를 건내 줄 방법을 마련해 줘야 하는 것이다. 하지만 불가피한 경우에는 callee 가 수행하는 내용을 이해하여, caller 에서 이를 다시 계산하게 하여 값을 의도에 맞게 수정하도록 할 수도 있을 것이다. 나는 아래의 총 5 가지 해결책을 생각해 보았다.

  1. caller 가 callee 호출 후 바뀐 값을 직접 다시 계산하여 설정하기
  2. callee 가 어디까지 작업했는지 알려주는 인덱스 또는 주소를 반환하도록 하기
  3. 작업 위치를 기억하는 변수의 주소를 인자로 전달하기
  4. caller 나 또는 caller 를 호출하는 함수에 작업 위치를 저장하는 변수를 멤버로 갖는 구조체(structure) 를 두고, 이 structure 를 전달하기
  5. 진행 중인 작업을 flag 에 기록해두고, 매 반복(loop) 마다 체크하여 필요한 함수를 실행하도록 하기


caller 가 callee 호출 후 바뀐 값을 직접 다시 계산하여 설정하기

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_bufsource_str 의 값을 수정하는 코드가 붙어야 하기 때문에 반복하는 코드에서 사용하기 위한 함수화가 제대로 이루어졌다고 말하기 힘들어 보인다. 지금 구현한 cpy_a_word() 는 일반적인 용도로 함수화되어 있으며, source_str 을 반복하는 반복문 안에서 사용하기 위한 용도에 적합하지 않아 보인다.


callee 가 어디까지 작업했는지 알려주는 인덱스 또는 주소를 반환하도록 하기

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_strtarget_buf 의 값을 바꾸게 되는 결과를 초래할 수 있다. 따라서 cpy_a_word() 라는 함수는 결국 일반적으로 사용되기 어렵고 반복하는 코드 내부에서 호출되야 편하기 때문에, 반복하는 코드에 다소 의존적인 특징이 생겼다. 추가적으로, 이 방법은 이중 포인터를 사용해야 하기 때문에 가독성이 떨어지는 문제도 있다.


caller 나 또는 caller 를 호출하는 함수에 작업 위치를 저장하는 변수를 멤버로 갖는 구조체(structure) 를 두고, 이 structure 를 전달하기

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_bufsource_strt_str_relay 라는 구조체로 감싼 것 뿐이다. 더 이상 이중 포인터가 쓰이지 않고, 가독성이 더 좋아졌다.[주석:1] 동시에 cpy_a_word() 에 구조체 t_str_relay 에 대한 의존성이 생겼다.


진행 중인 작업을 flag 에 기록해두고, 매 반복(loop) 마다 체크하여 필요한 함수를 실행하도록 하기

#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_bufsource_str 을 포함하는 구조의 이름으로 적절한 선택이 아니었다면 가독성이 그다지 상승하지 않았을 수 있다.