문제해결: 자료구조 C로 구현 (연결리스트, 스택&큐, 이진트리, 이진 탐색 트리) (2/4)
설계: 수요 코딩회 처리기 설계 (실패)
구현: 데이터베이스 처리기 구현 (성공)
품질: 유닛 테스트 적용 (성공, 유닛테스트 227개)
C에서 static은 크게 2가지 사용처가 있다. 하나는 함수 로컬 변수에 사용되는것과 전역변수에 사용되는 경우다.
로컬 변수에 static이 붙으면, 여전히 함수 내에서만 접근할 수 있지만 스택이 아닌 데이터 영역에 저장된다. 따라서 로컬 변수가 전역 변수처럼 프로그램이 끝날 때까지 남아있게 된다. 물론 동적으로 생성된 것이 아니기에 자동으로 정리된다.
전역 변수에 static을 사용하게 되면, 현재 파일에서만 해당 전역변수에 접근할 수 있다. 외부 파일에서는 접근할 수 없다. 이번 수요코딩회에서도 이런 용도로 static이 많이 사용되었다.
아래는 진입하는 함수인데, 여기서는 static이 없다. 외부 파일에서 접근하는걸 허용하기 때문이다.
/* storage_select: 1주차 호환 wrapper.
* storage_select_result 호출 → print_rowset 출력 → rowset_free.
* 외부 동작은 1주차와 완전히 동일.
*/
int storage_select(const char *table, ParsedSQL *sql)
{
RowSet *rs = NULL;
int status = storage_select_result(table, sql, &rs);
if (status == 0 && rs != NULL) {
print_rowset(stdout, rs);
}
rowset_free(rs);
return status;
}
아래는 static으로 선언되며, 진입점 안에서 호출될 수 있도록 접근을 제한하고 있다.
/* 입력: schema 파일 경로, 결과 schema 배열 포인터, 결과 개수 포인터
* 동작: <column_name>,<type> 형식의 schema 파일을 읽어 ColDef 배열로 적재
* 반환: 성공 0, 파일 형식 오류/메모리 오류/빈 schema면 -1 */
static int load_schema(const char *schema_path, ColDef **out_schema, int *out_count)
{
FILE *fp;
ColDef *schema = NULL;
int schema_count = 0;
char line[STORAGE_LINE_MAX];
배웠던 개념들이 수요 코딩회에서 쉽게 찾아볼 수 있어, 공부한 보람을 제대로 느낄 수 있었다.
gcc라는 컴파일 드라이버를 처음 접했다. 옵션때문에 복잡해보였지만, 몇번 실행해보니 금방 손에 익었다. 특히 gdb는 vscode debugger보다 더 유용했다. 브레이크 포인트 설정을 줄이나 함수로 지정할 수 있었고, 관리도 가능했다. 가장 큰 차이점은 디버깅할때마다 터미널을 클릭할 필요가 없었다. vscode debugger는 GUI 기반이라 그런지, 디버깅을 다시할때마다 터미널을 클릭해야했다. gdb는 클릭할 필요가 없어, 손이 덜 갔다.
팀 코어타임시간때, 선언순서에 따라 구조체의 크기가 달라질 수 있다는걸 알게되었다. 혼자 공부했었다면 알아내지 못했을 것 같은 내용이라 더 기억에 남았다. 구조체 변수를 메모리에 할당할때, 변수 크기 배수에 해당되는 주소에 저장된다. 4바이트 변수는 0x0000, 0x0004, 0x0008 마다 저장될 수 있다. 만약 1바이트 char 타입 변수가 먼저 선언되면, 0x0000에 1바이트가 저장되고, 4바이트 변수는 0x0004에 저장된다. 3바이트는 패딩으로 사용되기 때문에, 실제로 8바이트짜리 구조체가 되는것이다.
CLI로 구현이 요구사항이였다. 헬로키티를 좋아하는 팀원이 있어, 로고를 이렇게 만들었다. 쿼리문을 실행할때마다 나오기 때문에, 망령같다.

가독성을 위해 중계 서버를 띄었다. 쿼리문을 입력하면 파일에 저장하고, 처리기에 의해 파싱된다. 작동은 CLI와 똑같다.

첫 버전을 만들고 문제점을 찾아보았다. 그 중에 where 조건문이 2개까지만 지원된다는걸 발견했다. 왜 그럴까 코드를 까보니, 2개만 지원되도록 하드 코딩되어있었다.
/* 리팩토링 전 — include/types.h (ParsedSQL 일부) */
WhereClause *where;
int where_count; /* 0~2 */
char where_logic[8]; /* "AND" 또는 "OR" — 단 하나만 */
/* 리팩토링 전 — src/parser.c parse_where */
static void parse_where(TokenList *t, ParsedSQL *sql) {
sql->where = calloc(2, sizeof(WhereClause)); /* 최대 2칸 미리 잡기 */
sql->where_count = 0;
for (int i = 0; i < 2; i++) { /* 하드코딩된 2회 루프 */
const char *col = advance(t);
const char *op = advance(t);
/* ... */
}
}
이번에 작성한 코드를 차주에도 활용할 예정되어있었다. 그래서 더 풍부한 기능을 지원하기 위해 개선해보기로 했다. 이 과정에서 N-ary를 알게되었고, 이중 포인터로 구현할 수 있다는것도 알게 되었다.
/* 리팩토링 후 — include/types.h (ParsedSQL 일부) */
WhereClause *where;
int where_count;
char **where_links; /* ⭐ 이중 포인터 — N-1 개 결합자 ("AND"/"OR") */
where_links에 연결자(and, or)가 저장된다. *where_link가 내부 연결자를 가리키고, where_link를 하나씩 증가시켜 다음 연결자를 가리키는 방식으로 동작하게 된다.
다른 내용들은 리드미에 적어두었다. Github Link
코치님들이 제대로 이해한것인지 의구심을 가졌었다. 외부에서 보기엔 결과가 너무 좋으니까 제대로 이해했는지 염려하신걸로 이해했다.
Github Actions, Review등을 처음 느껴봤는데, 편하기도 하고 책임소재를 알려주어 재밌었다. 자동 테스트를 작성해, PR 리뷰전에 코드 안정성을 확인해볼 수 있었다.

PR 리뷰에서 점수가 나오니까 떨리기도 하고 재밌기도 했다. 또, 누가 처리해야하는지가 명확해지니 협업이 편리했다.

그러나 팀은 정말 재밌고 알차게 프로젝트를 진행했다.
