
C Programming 시작 (1주차 / 총 3주 일정)
4월 3일 금요일
4월 4일 토요일
4월 5일 일요일
4월 6일 월요일
4월 7일 화요일
4월 8일 수요일
4월 9일 목요일
이번주부터는 이제 4로 고정이다. 우리 반에 한 명이 저번주 화요일 랜덤 런치 기준으로 나가신 걸로 안다. (3-3-4-3-3-4), 저번주와 동일하게, 수요코딩회는 비슷하게 준비하였고, (변수가 좀 존재하면 update)
C를 안 본지 너무 오래됬고, malloc, pointer 모두 무서웠던 개념인데, 학부 때와 다르게 지금은 AI도 있고, 모르는 건 다 물어보면 되서 개념적으로 학부 때에 비해 안정성이 매우 올라갔다. project는 docker에 ubuntu 24.04 환경으로, devcontainer로 실행해서 프로젝트 진행중이다.
이번 주는 새로운 챕터로 넘어가는 주이기에 운영 방침은 팀원들과 상의 하고, 코어 타임에 각자 부족한 것 배운 것에 대해서 나누기로 했다.
4월 7일 동석 코치 강의
컴파일러가 컴파일은 해주는데, 에러는 코드 작성자에게 맡기는 부분이 있기에
segment fault가 뜬다.
-> 수시로 뜨고, 어디서 왜 떴는지 알려주지 않아서 난감했다.
파이썬만 하더라도 number int 1개가 28byte다.
C는 int type 기준 4byte라고 다들 알고 있겠지만,
사실상 C는 타입의 크기를 정해두지 않았다.
대신 64bit 운영 체제에서 얼추 4byte 된다 이런 것.
그래서 sizeof을 써야 제일 정확하다.
C에서 array는 사실상 syntax sugar이다. 파이썬의 List와 다르다. 그 점 인지.
strlen = python len
사실상 문자열은 마지막이 '\0'이 있는데
사실상 사이즈값이 sizeof 또는 strlen() + 1이 실제 크기.
"jungle" 이 있을 때 strlen이 6이 뜨는데 실제론 7이 사이즈다.
C는 3개의 속성이 있다고 기억하면 된다.
* 주소값 (&a)
* value
* size
값을 변경하고 싶을 때 포인터로 접근 해서 갱신해야 값이 바뀐다.
등등
4섹션 7, 7, 8, 5로 총 27문제였고, 처음부터 보니까 좀 생소하고 뭐하고, 라는 말이 바로 나올 법 했지만, pdf를 보면서 푸니까, 영어 + 중국어 힌트로 꽤나 풀만 했다. "CE1007/CZ1007 Data Structures" 이게 NTU - Singapore, 난양공과대학교 교육과정에서 쓰는 코드를 그대로 가져온거 같다. 난 학부 때 아래와 같이 처음부터 끝까지 다 짰었는데 약간 충격이긴 했다.
#include <stdio.h>
#include <stdlib.h>
int main(){
}
// 또는
int main(int argc, char *argv[]){
}
이번주는 Claude를 사용해서, Claude한테 받은 피드백은 다음과 같다.
강점
개념을 파이썬이랑 비교해서 이해하는 속도가 빠르고, 직관이 대체로 맞아요
"왜 이렇게 되는지"를 계속 물어보는 습관이 좋아요 — 그냥 코드 복붙하지 않고요
막히면 말로 먼저 설명하고 코드로 옮기는 방식이 효과적이에요
개선할 부분
포인터 타입 실수가 자주 나왔어요
("(" vs '(', int vs char 등) — C에서 타입에 더 주의가 필요해요
free 타이밍이나 malloc 초기화를 빠뜨리는 경우가 있었어요 —
새 자료구조 만들 때 체크리스트처럼 습관화하면 좋을 것 같아요
엣지 케이스(NULL 체크, 홀수/짝수 크기 등)를
나중에 발견하는 경우가 많았어요 —
코드 짜기 전에 먼저 생각하는 습관을 들이면 좋을 것 같아요
malloc은 오랜만이라, 그리고 기억도 잘 안나긴 했다.
C 로 5종 SQL (CREATE / INSERT / SELECT / UPDATE / DELETE) 을 직접 토크나이즈 → 파싱 → 실행하는 미니 DBMS, SQL Parser를 만들었다.
나는 PM 겸 Parser 영역(parser.c, ast_print.c, json_out.c, sql_format.c, main.c)을 직접 구현했고,
석제님이 SELECT/CREATE storage 를, 원우님이 INSERT/DELETE/UPDATE storage 를 맡았다. 세인님은 원우님과 같이 경쟁 브랜치로 하다가, 마지막으로 CLI Design을 맡았다.
오전에 1차 완성을 끝낸 다음, 같은 날 오후에 같은 팀으로 후속 리팩토링까지 한 번 더 돌렸다.
지난 주 React 프로젝트는 "내가 만든 추상화를 게임에 끼워 맞추다 어긋난" 경험이었다.
이번 주는 정반대로 갔다 — 목표를 작게 잡고, 인터페이스 계약을 먼저 못 박고, 거기에 맞춰 영역을 나누는 방식.
주제는 C 로 만드는 파일 기반 미니 DBMS.
--debug / --json / --tokens / --format / --help / --version)3 주짜리 프로젝트의 1 주차였기 때문에, 확장 가능한 구조 가 더 중요했다.
storage_select, storage_create, CSV 파서, WHERE/LIKE/ORDER BYstorage_insert, storage_delete, storage_update + 단위 테스트 48dev2 브랜치에서 진행협업에서 못 박은 것:
include/types.h 의 storage_* 시그니처는 절대 변경 금지 — 인터페이스 계약executor.c 는 dispatcher 만 — 한 명이 망쳐도 나머지 작업 가능pull_request_template.md 양식 + GitHub Actions CI + PM 코드 리뷰 후 머지main / dev / dev2 는 브랜치 보호 (직접 push 차단, admin 만 우회)include/types.h 한 파일에 모든 자료구조 (ParsedSQL, WhereClause, ColumnType) 와 함수 시그니처를 다 박아두고, 그걸 PR 1 개로 먼저 머지 한 다음에 셋이 동시에 작업을 시작했다.
[D. 인터페이스 계약 PR — 지용]
↓ (먼저 머지)
┌──────────┼──────────────────────┐
↓ ↓ ↓
[Parser] [SELECT storage] [DML storage]
(지용) (석제) (원우)
지난 주 React 때처럼 "각자 만들고 나중에 붙이자" 가 아니라,
붙일 모양을 먼저 정해놓고 그 안에서 각자 자유롭게 작업.
머지 충돌이 거의 0 이었다.
오전에 끝낸 1차 완성을 직접 써보면서 약점이 드러났다.
| 발견 | 원인 |
|---|---|
storage_select 가 결과를 stdout 에만 print | 다른 함수가 결과를 받아 쓸 수 없음 → JOIN/집계/subquery 모두 불가 |
| WHERE 가 1~2 조건 + 단일 결합자만 | WHERE a=1 AND b=2 AND c=3 미지원 |
storage_delete/update 시그니처가 SELECT 와 다름 | API 비대칭 + N-ary 결합자 전달 불가 |
| 집계 함수가 COUNT(*) 한 종류만 | SUM/AVG/MIN/MAX 미지원 |
| silent error 다수 | "존재하지 않는 테이블 SELECT" 등이 에러 메시지 없이 빈 결과 |
발표용 main 을 안정 상태로 보존한 채 새 통합 브랜치 dev2 를 따고, 같은 팀으로 한 번 더 작업했다.
1주차 호환 0 회귀 가 핵심 제약 — 후속 리팩토링 후에도 1차 단위 테스트 201 개가 모두 통과해야 했다.
1주차 코드는 parse_where 가 2칸짜리 배열을 미리 잡아놓고 for(i<2) 하드코딩 으로 돌고 있었다.
결합자도 char where_logic[8] 한 칸뿐.
/* 리팩토링 전 — include/types.h */
WhereClause *where;
int where_count; /* 0~2 */
char where_logic[8]; /* "AND" 또는 "OR" — 단 하나만 */
/* 리팩토링 전 — src/parser.c */
static void parse_where(TokenList *t, ParsedSQL *sql) {
sql->where = calloc(2, sizeof(WhereClause));
for (int i = 0; i < 2; i++) { /* 하드코딩 */
const char *col = advance(t);
const char *op = advance(t);
/* ... */
}
}
/* 리팩토링 전 — DML 가드도 단일 조건만 통과 */
if (where_count != 1 || where == NULL) {
return -1;
}
후속 리팩토링은 `char where_links` 이중 포인터** 로 풀었다.
N개 조건 사이에 N-1 개 결합자가 들어가는 가변 길이 배열.
/* 리팩토링 후 — include/types.h */
WhereClause *where;
int where_count;
char **where_links; /* N-1 개 결합자 ("AND"/"OR") */
/* 리팩토링 후 — parse_where (개념 발췌) */
static void parse_where(TokenList *t, ParsedSQL *sql) {
while (!at_stop(t)) {
sql->where = realloc(sql->where,
sizeof(WhereClause) * (sql->where_count + 1));
/* col / op / value 파싱 */
sql->where_count++;
const char *link = peek(t);
if (eq_ci(link, "AND") || eq_ci(link, "OR")) {
sql->where_links = realloc(sql->where_links,
sizeof(char*) * sql->where_count);
sql->where_links[sql->where_count - 1] = strdup(advance(t));
} else break;
}
}
이중 포인터가 무섭게 느껴졌는데, 막상 짜보니 "포인터를 가리키는 포인터" 가 결국 가변 길이 배열을 위한 도구 라는 게 손에 잡혔다.
realloc 으로 슬롯을 늘리고, 각 슬롯에 strdup 으로 문자열을 붙이고, free 할 때 거꾸로 푼다.
malloc/free 를 짝 맞춰 짜는 습관이 처음으로 자연스럽게 들었다.
1차 완성의 storage_select 는 결과를 화면에 출력만 하는 "프린터" 였다.
후속 리팩토링에서 storage_select_result() 를 신설해서 결과를 메모리 RowSet 으로 반환 하게 만들었다.
typedef struct {
int row_count;
int col_count;
char **col_names;
char ***rows; /* ⭐ 또 다른 이중/삼중 포인터 */
} RowSet;
int storage_select_result(const char *table, ParsedSQL *sql, RowSet **out);
void print_rowset(FILE *out, const RowSet *rs);
void rowset_free(RowSet *rs);
char ***rows — 행을 가리키는 포인터의 포인터의 포인터. 처음 봤을 때 멍했는데,
"행 배열 → 행 → 셀(문자열)" 로 한 단계씩 풀어서 그리니까 자연스러웠다.
이게 생기면서 향후 JOIN / GROUP BY / subquery 의 길이 다 열렸다.
기존 storage_select 는 storage_select_result → print_rowset → rowset_free 를 호출하는 얇은 wrapper 로 리팩토링.
외부 동작 변화 0 — 1주차 테스트 그대로 통과.
/* Before — 1주차 */
int storage_delete(const char *table, WhereClause *where, int where_count);
int storage_update(const char *table, SetClause *set, int set_count,
WhereClause *where, int where_count);
/* After — 후속 리팩토링 */
int storage_delete(const char *table, ParsedSQL *sql);
int storage_update(const char *table, ParsedSQL *sql);
호출부 (executor.c) 는 한 줄씩만 바뀌었다. API 가 일관되니 호출하는 쪽이 깔끔해지고, N-ary 결합자 같은 새 필드가 자연스럽게 흘러갔다.
1차 완성부터 후속 리팩토링까지 5 개 바이너리 모두 valgrind 누수 0 을 유지했다.
sqlparser (메인)test_runner (parser/AST/JSON/format 단위 테스트)test_storage_insert / test_storage_delete / test_storage_updateC 를 처음 제대로 만진 주에 valgrind 0 을 찍은 게 의외로 큰 자신감이 됐다.
"malloc 한 거 free 안 했구나" 를 valgrind 가 정확히 짚어주니까 디버깅이 무섭지 않았다.
.github/workflows/build.yml 가 모든 push/PR 에 자동으로 돌았다.
- gcc/make/valgrind 설치
- make CFLAGS="-Werror" # 경고도 빌드 실패 처리
- make test # 단위 테스트 (227)
- valgrind --leak-check=full ./test_runner
- valgrind --leak-check=full ./sqlparser query.sql
PR 페이지에 빨간/초록이 자동으로 떴고, CI 가 깨진 PR 은 머지 안 함 을 원칙으로 박았다.
후속 리팩토링 시작할 때 dev2 trigger 가 누락된 걸 발견해서 한 줄 fix 한 일도 있었다.
이번 주에 3 가지 다른 머지 패턴을 직접 운영해봤다.
| 패턴 | 적용 PR | 핵심 |
|---|---|---|
| Winner-Takes-All | 1주차 INSERT (B vs C) | 두 PR 중 더 나은 쪽을 통째로 머지, 탈락 쪽엔 피드백 |
| 옵션 B Mixed Merge | Phase 1 PR #32 vs #33 | 충돌 함수 단위로 비교 → 함수마다 베스트 코드 채택 |
| Cherry-pick 부분 추출 | PR #31 (세인 CLI design) | 작업자 브랜치 보존, 가치 있는 부분(컬러+배너)만 골라 새 통합 커밋, Co-Authored-By 부착 |
세 패턴 공통 원칙:
특히 옵션 B Mixed Merge 는 처음 시도한 패턴이었다.
원우님 PR #32 와 석제님 PR #33 이 같은 parser 영역에서 5 군데 충돌했는데,
보통 같으면 한 명만 채택하고 한 명은 탈락이지만 — 함수 단위로 옆에 놓고 비교했다.
parse_where: 석제 (단일 while, 결합자 정규화, 깔끔)
parse_select stop: 석제 (신규 기능, 원우 없음)
ast_print: 석제 (inline 결합자 직관적)
json_out: 원우 (emit_str_array 헬퍼 재사용)
sql_format: 원우 (NULL 안 반환, robust)
test_parser.c: 두 사람 케이스 합집합
mixed merge commit 하나에 양쪽 Co-Authored-By 를 다 붙였다. 두 사람 작업 모두 살아남고, 결과 코드 품질도 함수마다 더 나은 쪽으로 올라갔다.
PR 은 단순한 코드 묶음이 아니라 누가 / 어디까지 / 왜 를 동시에 기록한다.
이번 주 핵심 PR 3 개:
| PR | 담당 | 단계 | 핵심 |
|---|---|---|---|
| #18 | 석제 | 1차 | SELECT 영역 1차 완성 (storage_select / CSV 파서 / WHERE / LIKE / ORDER BY) |
| #32 | 원우 | 후속 | UPDATE/DELETE 시그니처 통일 + N-ary WHERE 평가 |
| #33 | 석제 | 후속 | Parser stop set + N-ary parse_where |
PR #18 이 1차 SELECT 의 책임 소재가 석제임을 영구히 기록한 덕분에,
후속 단계에서 같은 영역을 N-ary 로 확장할 때 누구한테 리뷰 청할지 자명했다.
| 지표 | 1차 완성 | 후속 리팩토링 후 |
|---|---|---|
| 단위 테스트 | 201 | 227 (+30 RowSet/N-ary/집계) |
빌드 경고 (-Wall -Wextra -Wpedantic -Werror) | 0 | 0 |
| valgrind 누수 (5 바이너리) | 0 | 0 |
| storage 함수 | 5 | 8 (+3 RowSet 신설) |
| WHERE 조건 수 | 최대 2 | N개 (혼합 결합) |
| 집계 함수 | 1 (COUNT) | 5 (COUNT/SUM/AVG/MIN/MAX) |
| Pull Request 수 | 24 | 34 |
| 작업 기간 | 약 7 시간 | + 약 4 시간 (같은 날) |
"파싱 결과는 잘 나오는데 SELECT 결과가 표 형태로 보이면 좋겠다."
처음엔 카드 + JSON 토글로만 보여줬는데, 피드백 받고 진짜 HTML <table> 그리드로 렌더하게 고쳤다.
다크 테마 (Dracula) 와 기존 카드 UI 를 해치지 않는 선에서 조화롭게 — 컬럼 헤더 강조, 행 zebra striping.
지난 주 React 때 "구현 먼저 → 설명 끼워 맞추기" 의 함정에 빠졌던 걸 의식하며,
이번 주는 의도적으로 "인터페이스 먼저 → 구현은 그 안에서" 순서로 갔다.
결과: 머지 충돌이 거의 0, 발표할 때 "여기까지가 의도, 여기까지가 구현" 의 경계가 명확했다.
Pros.
원우: GitHub Actions CI 자동 테스트 덕에 의견 수렴이 훨씬 수월했다 — 감 아닌 초록/빨강으로 판단할 수 있었다
석제: PR 리뷰 문화 + types.h 인터페이스 설계 덕에 memory alignment 이슈까지 팀 전체가 같이 잡을 수 있었다. 원우·석제 양쪽 다양한 풀이를 보는 것도 좋았다
지용: 멋진 팀원들 (feat. Zoom). 코어타임이 있어서 각자 어려웠던 점을 공유할 수 있었고, Anti-AI 작성 원칙 덕에 다양한 시각으로 자세한 코드 리뷰가 됐다
세인: 코어타임에 다양한 내용을 다룰 수 있었다
Cons.
세인: 코어타임에 팀원들에게 선뜻 얘기를 꺼내지 못했다
석제: 수요코딩회에서 low 레벨로 더 깊이 내려갔더라면 어땠을까 하는 아쉬움이 있다
원우: (석제와 동일)
지용: 세인이가 좀 더 자신감을 가지고 적극적으로 참여했으면 좋겠다
Feedback
석제: 세인님 기능 분담을 추가로 드리는 것 + low 레벨 구현까지 도전해보는 것 제안
Try.
다음 주 phase 2 (B+트리 / JOIN) 도 같은 패턴 — 인터페이스 먼저 박고 영역 분리
세인 팀원이 더 적극적으로 기여할 수 있도록 역할 분담 재조정
수요코딩회에서 low 레벨 구현까지 도전하는 방향 검토
지난 주 React 회고에서 "custom React(수단)로 잘 작동하는 것(목표)을 먼저 선택" 이라는 교훈을 적었었다.
이번 주는 그 교훈을 그대로 적용했다 — "수단(C 파일 백엔드)으로 잘 작동하는 목표(파일 기반 SQL 5 종)" 부터 잡고, 그 안에서 점진적으로 확장.
결과: 1주차 호환 0 회귀로 후속 리팩토링까지 같은 날에 돌렸다.
지난 주의 "구현 먼저 → 설명 끼워 맞추기" 함정을 한 주 만에 뒤집었다는 게 가장 의미 있었다.
이중 포인터 (char **) ─── 가변 길이 배열을 위한 도구일 뿐 — 무서워할 게 없다
삼중 포인터 (char ***) ─── "행 배열 → 행 → 셀" 한 단계씩 풀면 자연스럽다
malloc/free 짝 ─── valgrind 가 무서웠는데, 짚어주니까 오히려 친구
인터페이스 계약 ─── 시그니처를 먼저 박으면 병렬 작업이 가능해진다
PR + CI ─── PR 페이지가 코드 리뷰의 객관적 근거 + 책임 소재 영구 기록
CI와 PR 리뷰를 도입해서 조금 더 현업 느낌이 날 수 있도록, 작업한 거 같다. 팀원들 모두가 만족스러워하는 부분이 좋았던거 같다.
팀 역할 분배 + PR + branch 전략 + CI 의 조합이 처음으로 자연스러웠다.
실패 → 회고 → 다음 주 적용 의 사이클이 한 번 돌았다는 게 가장 큰 수확.
GitHub 링크:
https://github.com/JYPark-Code/jungle_w6_mini_mysql_sql_parser