본문 바로가기

크래프톤 정글/WIL

정글[Week_06]: 파일 기반 SQL 처리기 만들기

매 주 진행하는 수요코딩회, 이번에 맡은 프로젝트는 SQL처리기를 구현하는 것이었다. 수요코딩회는 구현하는 것에 초점을 두는 만큼, AI를 통해서 빠르게 만들어야 한다. 다만, AI가 코드를 생성을 하였더라도, 전반적인 흐름과 어떤 함수가 어떤 함수를 호출하는지 그 구조를 이해하는 것 또한 동반되어야 한다.

 

 

C언어를 통한 구현을 해야한다

다른 Java나 Python이 아닌 C언어로 구현하라는 지침이 전달되었고, C와 같은 저수준 언어에서 지원하는 메모리 할당, 포인터 등이 어떻게 이번 프로젝트에서 사용되는지 이해해야 한다는 것으로 이해를 했다. 먼저, 큰 틀에서 대다수의 프로그래밍 언어들은 스택과 힙 영역을 나눠서 사용함을 알고 있어야 할 필요가 있다. 물론 이 영역은 실제로 나눠져 있다기 보다는 주 메모리 장치에 논리적으로 나눠져 있다는 사실은 덤.

스택 영역은 실행부로서의 역할을 한다. 특정 객체를 적재(push)하고 빼는(pop)을 하는 과정을 수시로 반복하는데, 이 뜻은.. 개발자가 작성한 객체들이 알아서 넣고 빠지고를 반복하니, 굳이 메모리 관리를 위한 지침서를 제작할 필요는 없다는 것이다. 그러나, 힙 영역은 다르게 흘러간다. 스택에 쌓인 객체 안에는 포인터를 통해 힙에 있는 객체의 주소를 바라보는 주소값이 들어가고, 힙에는 본 데이터가 들어간다.

한 마디로 스택이 작동하면서 힙 영역을 끊임없이 참조를 한다는 것이다. C언어의 경우 포인터를 통해서 이를 직접 구현을 하나, Java와 같은 중•고수준 언어는 알아서 관리를 해준다. 또한 힙에 쌓이는 수 많은 객체들도 Garbage Collector가 필요가 없어졌다고 판단되면 지워주는 반면, C에서는 직접 힙에 적재(malloc)를 하고 필요 없다고 "개발자"가 판단을 하면 다시 해제(free)를 하는 과정을 직접 지시를 해야 한다.

 

 

SQL 처리기는 계층화된 구조를 띄고 있다

컴포넌트 다이어그램

우리 조에서 구현한 SQL 처리기는 비교적 구조가 단조로왔음에도(insert와 select 기능만 구현), src 폴더 안에 계층화된 파일이 형성되어 있었다. 이들을 컴포넌트 다이아그램으로 나타낼 경우 위와 같이 도식화를 할 수 있는데, 여기서 보다시피... 이는 단반향 단일 체인 구조가 아님을 확인할 수 있다. cli_runner라는 파일이 흐름 전반을 관리하는, 이른바 orchestration(오케스트레이션) 역할을 함을 파악이 가능하다.

 

cli_runner의 orchestration(조율) 방식

그럼 cli_runner는 어떻게 아래 계층들을 오케스트레이션을 할까? cli_runner에서 어떤 함수가 호출이 되고 진행되는지 대략적으로 기술하자면 다음과 같은데, 이를 통해 이해를 할 수 있다.

run_cli
→  run_cli_with_streams
→  run_cli_interactive_with_streams
  → is_exit_command (종료 판단)
  → execute_sql_text
      →  command (init: 구조체 초기화)
      →  parser (parse_sql: 초기화된 구조체에 데이터 할당)
      →  executor (execute_command: 명령 실행)
        → storage (CSV I/O)
      →  command (free: 메모리 해제)

run_cli에 먼저 진입을 한 다음,  run_cli_interactive_with_streams 라는 함수가 호출이 되는데, 이는 사용자가 입력한 명령어를 받는 함수이다. 전달이 완료되면 execute_sql_text를 통해 본격적인 오케스트레이션을 한다. command를 통해서 구조체를 초기화한 다음, 그 구조체에 parser를 통해 데이터를 할당한다. 이렇게 데이터가 할당된 구조체는 executor를 통해 storage로 넘어가... csv 파일에 읽기/쓰기가 진행이 된다. 이것이 우리 조가 구현한 cli_runner의 로직 흐름 방식이다.

 

구조체는 그 자체로는 데이터가 아니다

C언어로 기술되는 만큼, 메모리 할당/해제, 포인터 변수가 들어간다. 먼저, 구조체의 경우.. 그 구조체 자체가 데이터로 역할을 한다고 오해를 하지 말아야 한다! 구조체는 스택 영역에 들어가며, value는 포인터로, 힙에 저장된 객체를 가리키게 된다. 즉, 구조체는 데이를 담는 그릇과 같은 역할을 하며, 실제 데이터는 힙에 저장된다. 따라서... value를 저장하기 위한 공간을 malloc으로 할당을 해야 한다. 그리고 이 과정은 parser 에서 시행이 된다. 또한 저장된 데이터는 구조체와 pointer로 연결이 되기 때문에, 별도의 설정 또한 필요하다.

파싱 전: INSERT INTO users (id, name) VALUES (1, 'jungle');

파싱 후: type = SQL_COMMAND_INSERT
        table_name = "users"
        columns = ["id", "name"]
        column_count = 2
        values = ["1", "jungle"]
        value_count = 2

파싱 전에는 raw_text로 구문 형태의 명령어임을 위에서 확인할 수 있다. 이를 파싱을 하면서, 구조체의 틀에 맞게 힙 영역에 각 데이터를 할당을 한다.

 

cli_runner의 execute_sql_text 함수 살펴보기

다양한 함수가 존재하지만, 오케스트레이션에서 중요 역할을 하는 execute_sql_text()을 직접 살펴보고자 한다. 안에서 어떠한 함수들이 호출이 되고, 어떤 역할을 하는지 간략히 정리를 하였다.

static int execute_sql_text(const char *sql, FILE *out_stream, FILE *err_stream) {
    char error[256];
    char *output = NULL;
    SqlCommand command;
    int success = 0;

    sql_command_init(&command);
    error[0] = '\0';

    if (!parse_sql(sql, &command, error, sizeof(error))) {
        fprintf(err_stream, "ERROR: %s\n", error);
        sql_command_free(&command);
        return 1;
    }

    success = execute_command(&command, &output, error, sizeof(error));
    if (!success) {
        fprintf(err_stream, "ERROR: %s\n", error);
        sql_command_free(&command);
        free(output);
        return 1;
    }

    fputs(output, out_stream);

    sql_command_free(&command);
    free(output);
    return 0;
}

- SqlCommand command: 파싱 결과를 담을 명령 객체이다. 이는 sql_command_init(&command)로 초기화를 먼저 진행한다.
- parse_sql(.. &command ..): parser 계층을 호출하는 로직이다. 에러 발생 시  sql_command_free()로 메모리를 즉시 해제한다.
- execute_command(.. &command ..): 여기서 excute 계층으로 내려간다. 그리고 storage로 넘어가 실제 db와 상호작용을 한다.
- sql_command_free(): 메모리를 해제하는 작업이다. 성공 유무에 상관 없이 할당 해제는 필수.

 

 

계층화를 하는 이유

많은 아키텍처들이 계층화를 한다. 계층화 아키텍처(Layered Architecture)는 소프트웨어의 설계 방식 중 하나로, 책임과 역할을 명확하게 분리하기 위해 고안이 된 방식이다. 각 계층이 책임이 명확하기 때문에, 한 곳에서 오류가 발생한다고 해도, 그 곳만 집중하여 수정을 가하면 된다. 무엇보다, 확장에 용이하다는 것이 가장 큰 장점인데... 현재 INSERT/DELETE 밖에 없는 상황에서, 추후 확장하여 여러 기능을 넣을 경우, 전체 코드를 뜯어보고 고칠 필요가 없다는 것이다. 

이것으로 이번 수요 코딩회 회고를 마치고자 한다. 다음 수요 코딩회는, 이번에 구현한 sql 프로세서에 B-Tree 구조를 도입하여 인덱싱을 한다고 한다. Row가 10만개 100만개일 경우, 이를 매번 순회하지 않고, 시간복잡도를 최소화 하는 과정이라 생각하면 좋겠다. 벌써 다음 주가 기대가 된다. 이번 주 처럼 다음 주도 무사히 협업 프로젝트를 마쳤으면 좋겠다.