마지막 편에서는 지금까지 다룬 개념을 한 번에 묶어, 텍스트 로그를 읽고 필터링하는 콘솔 유틸리티 logscan을 만들어 봅니다. 작은 도구지만, 파일 입출력, 구조체, 포인터, 함수 포인터, 동적 메모리, Makefile까지 모두 연결됩니다.
이번 글에서 새로 나오는 용어
- 파서 (Parser): 텍스트 입력을 구조화된 데이터로 바꾸는 코드
- 필터 (Filter): 조건에 맞는 데이터만 걸러내는 함수
- 서브커맨드 (Subcommand):
logscan stats,logscan tail처럼 명령을 세분화하는 방식 - 러너 (Runner): 콜백을 받아 특정 작업을 실행하는 함수
핵심 개념
학습 메모
- 소요 시간: 2~3시간 (설계+구현+실행)
- 준비물: 파일 I/O, 구조체, 포인터, 함수 포인터, Makefile 경험
- 학습 목표: 텍스트 파싱, 동적 배열, 콜백 기반 필터, 빌드 자동화 통합
logscan은 아래 기능을 제공합니다.
- 로그 파일을 줄 단위로 읽고
LogEntry구조체 배열로 변환 stats: 각 레벨(예: INFO, WARN, ERROR) 통계를 출력filter level=ERROR keyword=db형태로 조건을 조합해 원하는 로그만 출력tail N으로 마지막 N개의 엔트리를 보여 주기
구현 단계별로 필요한 개념을 복습하며 진행합니다.
한 줄 흐름으로 보면 아래와 같습니다.
입력 파일 -> 줄 단위 읽기 -> LogEntry 배열로 저장 -> 명령(stats/filter/tail) 분기 -> 결과 출력
프로젝트 구조
logscan/
├── include/
│ └── logscan.h
├── src/
│ ├── main.c
│ ├── parser.c
│ ├── commands.c
│ └── filters.c
├── sample.log
└── Makefile
sample.log 예시:
2026-03-18T09:00:01Z INFO auth 로그인 성공 user=alice
2026-03-18T09:01:15Z WARN db 연결 지연 120ms
2026-03-18T09:02:44Z ERROR payment 카드 승인 실패 code=42
2026-03-18T09:03:11Z INFO auth 토큰 갱신 user=bob
핵심 코드 살펴보기
include/logscan.h
#ifndef LOGSCAN_H
#define LOGSCAN_H
#include <stddef.h>
typedef enum {
LEVEL_INFO,
LEVEL_WARN,
LEVEL_ERROR,
LEVEL_UNKNOWN
} LogLevel;
typedef struct {
char timestamp[32];
LogLevel level;
char source[32];
char message[128];
} LogEntry;
typedef int (*entry_filter)(const LogEntry *entry, void *ctx);
typedef struct {
LogEntry *items;
size_t count;
} LogBuffer;
LogBuffer parse_file(const char *path);
void free_buffer(LogBuffer *buffer);
void run_stats(const LogBuffer *buffer);
void run_filter(const LogBuffer *buffer, entry_filter filter, void *ctx);
void run_tail(const LogBuffer *buffer, size_t n);
void run_filter_by_level(const LogBuffer *buffer, LogLevel level);
void run_filter_by_keyword(const LogBuffer *buffer, const char *keyword);
#endif
파서 구현 (src/parser.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "logscan.h"
static LogLevel parse_level(const char *token) {
if (strcmp(token, "INFO") == 0) return LEVEL_INFO;
if (strcmp(token, "WARN") == 0) return LEVEL_WARN;
if (strcmp(token, "ERROR") == 0) return LEVEL_ERROR;
return LEVEL_UNKNOWN;
}
LogBuffer parse_file(const char *path) {
FILE *fp = fopen(path, "r");
LogBuffer buffer = {NULL, 0};
char line[256];
size_t capacity = 0;
if (!fp) {
perror("파일을 열 수 없습니다");
return buffer;
}
while (fgets(line, sizeof(line), fp)) {
if (buffer.count == capacity) {
size_t new_cap = capacity == 0 ? 8 : capacity * 2;
LogEntry *new_items = realloc(buffer.items, new_cap * sizeof(LogEntry));
if (!new_items) {
perror("메모리 할당 실패");
free_buffer(&buffer);
break;
}
buffer.items = new_items;
capacity = new_cap;
}
LogEntry *entry = &buffer.items[buffer.count];
char level_str[8];
if (sscanf(line, "%31s %7s %31s %127[^\n]",
entry->timestamp, level_str, entry->source, entry->message) == 4) {
entry->level = parse_level(level_str);
buffer.count++;
}
}
fclose(fp);
return buffer;
}
void free_buffer(LogBuffer *buffer) {
free(buffer->items);
buffer->items = NULL;
buffer->count = 0;
}
명령 실행 (src/commands.c)
#include <stdio.h>
#include "logscan.h"
static const char *level_to_string(LogLevel level) {
switch (level) {
case LEVEL_INFO: return "INFO";
case LEVEL_WARN: return "WARN";
case LEVEL_ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
void run_stats(const LogBuffer *buffer) {
size_t info = 0, warn = 0, error = 0;
for (size_t i = 0; i < buffer->count; i++) {
switch (buffer->items[i].level) {
case LEVEL_INFO: info++; break;
case LEVEL_WARN: warn++; break;
case LEVEL_ERROR: error++; break;
default: break;
}
}
printf("INFO=%zu WARN=%zu ERROR=%zu\n", info, warn, error);
}
void run_filter(const LogBuffer *buffer, entry_filter filter, void *ctx) {
for (size_t i = 0; i < buffer->count; i++) {
if (filter(&buffer->items[i], ctx)) {
printf("%s %s %-5s %s\n",
buffer->items[i].timestamp,
level_to_string(buffer->items[i].level),
buffer->items[i].source,
buffer->items[i].message);
}
}
}
void run_tail(const LogBuffer *buffer, size_t n) {
size_t start = buffer->count > n ? buffer->count - n : 0;
for (size_t i = start; i < buffer->count; i++) {
printf("%s %s %-5s %s\n",
buffer->items[i].timestamp,
level_to_string(buffer->items[i].level),
buffer->items[i].source,
buffer->items[i].message);
}
}
필터 구현 (src/filters.c)
#include <string.h>
#include "logscan.h"
typedef struct {
LogLevel level;
} LevelCtx;
typedef struct {
const char *keyword;
} KeywordCtx;
int filter_by_level(const LogEntry *entry, void *ctx) {
LevelCtx *c = (LevelCtx *)ctx;
return entry->level == c->level;
}
int filter_by_keyword(const LogEntry *entry, void *ctx) {
KeywordCtx *c = (KeywordCtx *)ctx;
return strstr(entry->message, c->keyword) != NULL;
}
void run_filter_by_level(const LogBuffer *buffer, LogLevel level) {
LevelCtx ctx = {level};
run_filter(buffer, filter_by_level, &ctx);
}
void run_filter_by_keyword(const LogBuffer *buffer, const char *keyword) {
KeywordCtx ctx = {keyword};
run_filter(buffer, filter_by_keyword, &ctx);
}
엔트리 포인트 (src/main.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "logscan.h"
static LogLevel level_from_arg(const char *arg) {
if (strcmp(arg, "INFO") == 0) return LEVEL_INFO;
if (strcmp(arg, "WARN") == 0) return LEVEL_WARN;
if (strcmp(arg, "ERROR") == 0) return LEVEL_ERROR;
return LEVEL_UNKNOWN;
}
static void print_usage(void) {
printf("사용법:\n");
printf(" logscan stats <path>\n");
printf(" logscan filter level=<LEVEL>|keyword=<TEXT> <path>\n");
printf(" logscan tail <N> <path>\n");
}
int main(int argc, char *argv[]) {
if (argc < 3) {
print_usage();
return 1;
}
const char *command = argv[1];
if (strcmp(command, "stats") == 0) {
if (argc != 3) {
print_usage();
return 1;
}
LogBuffer buffer = parse_file(argv[2]);
run_stats(&buffer);
free_buffer(&buffer);
} else if (strcmp(command, "filter") == 0) {
if (argc != 4) {
print_usage();
return 1;
}
const char *arg = argv[2];
LogBuffer buffer = parse_file(argv[3]);
if (strncmp(arg, "level=", 6) == 0) {
run_filter_by_level(&buffer, level_from_arg(arg + 6));
} else if (strncmp(arg, "keyword=", 8) == 0) {
run_filter_by_keyword(&buffer, arg + 8);
} else {
print_usage();
}
free_buffer(&buffer);
} else if (strcmp(command, "tail") == 0) {
if (argc != 4) {
print_usage();
return 1;
}
char *end = NULL;
unsigned long parsed = strtoul(argv[2], &end, 10);
if (end == argv[2] || *end != '\0') {
fprintf(stderr, "tail 개수는 양의 정수여야 합니다.\n");
return 1;
}
LogBuffer buffer = parse_file(argv[3]);
size_t n = (size_t)parsed;
run_tail(&buffer, n);
free_buffer(&buffer);
} else {
print_usage();
return 1;
}
return 0;
}
빌드 자동화 (Makefile)
CC = clang
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
BUILD = build
SRCS = src/main.c src/parser.c src/commands.c src/filters.c
OBJS = $(SRCS:src/%.c=$(BUILD)/%.o)
TARGET = $(BUILD)/logscan
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(BUILD)
$(CC) $(OBJS) -o $(TARGET)
$(BUILD)/%.o: src/%.c include/logscan.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
@mkdir -p $(BUILD)
run: $(TARGET)
./$(TARGET) stats sample.log
clean:
rm -rf $(BUILD)
실행 예시
$ make run
INFO=2 WARN=1 ERROR=1
$ ./build/logscan filter level=ERROR sample.log
2026-03-18T09:02:44Z ERROR payment 카드 승인 실패 code=42
$ ./build/logscan filter keyword=토큰 sample.log
2026-03-18T09:03:11Z INFO auth 토큰 갱신 user=bob
$ ./build/logscan tail 2 sample.log
2026-03-18T09:02:44Z ERROR payment 카드 승인 실패 code=42
2026-03-18T09:03:11Z INFO auth 토큰 갱신 user=bob
왜 중요한가
- 작은 도구라도 명확한 구조와 자동화된 빌드를 갖추면 유지보수가 쉬워집니다.
- 파싱, 필터링, 통계 계산처럼 서로 다른 책임을 파일로 나눠야 코드가 읽기 쉽습니다.
- 함수 포인터와
void *컨텍스트 패턴이 실제 유틸리티에서도 자연스럽게 쓰임을 확인할 수 있습니다. - 캡스톤 경험은 이후 운영체제 과제나 서버 도구를 만들 때 기본 뼈대를 제공해 줍니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요