CS · SI 지식 백과
개발자 면접 CS 지식과 SI 산업 상식 145개를 도메인별로 정리했다. 각 항목은 한 줄 정의 · 약어 풀네임 · 구어체 해설 · 비교 표 · 면접 요점 · 우리 코드 연결고리로 다층 구성. "읽으면 이해되는" 설명이 목표 — 퀵소트 피벗이 왜 문제인지, TCP가 왜 세 번 악수하는지 단계별로 푼다. 커리큘럼 체인은 연관 용어를 학습 순서대로 묶었고, 자기소개서·직무 소개서 태그로 우리 문서에 실제로 걸린 개념만 따로 모아서 볼 수도 있다.
자료구조 9
면접 단골. 어떤 문제에 어떤 구조를 고르는지, 시간·공간 복잡도가 실제 캐시·디스크에서 어떻게 발현되는지를 oxi 코드와 함께 짚는다.
배열 / 동적 배열 Array / Dynamic Array 그림 우리코드
같은 타입의 원소를 메모리에 연속으로 나란히 저장 — 인덱스로 한 번에 점프해서 읽고, 동적 배열은 용량이 차면 2배씩 늘려 분할상환 O(1) 끝삽입을 보장한다.
고정 배열은 그냥 '같은 칸 n개를 연속 메모리에 깔아놓은 것'이에요. 그래서 arr[i]는 base + i × sizeof(T) 한 번의 곱셈·덧셈으로 끝나요 — 임의 접근이 진짜로 O(1)입니다. 캐시도 잘 맞아요. 원소들이 이웃 자리에 있으니까 한 번에 64바이트 캐시 라인(보통)이 4~8개씩 올라오거든요. 단점은 중간 삽입이에요. 100번 자리에 끼워 넣으려면 101번부터 n번까지 한 칸씩 뒤로 밀어야 하니 O(n). 마지막 원소를 빼도 빈 칸을 채우려고 다 당겨야 O(n)입니다.
동적 배열은 이야기가 다릅니다. Python list, Java ArrayList, C++ vector, Rust Vec — 이름만 다를 뿐 다 같은 패턴이에요. 처음엔 capacity 1로 시작해서 push하다 꽉 차면 새 메모리 capacity × 2 크기로 잡고 memcpy로 옮긴 뒤 옛것을 비꿔요. 직관적으로는 O(n)처럼 보이는데, 분할상환(amortized)으로 끝삽입이 O(1)이 된다는 게 포인트예요. 구체 숫자로 보면요. capacity=1에서 시작해 1→2→4→8→…→2^k로 자라면 k번의 재할당이 일어납니다. 재할당 비용은 1, 2, 4, …, 2^k로 커지니 합이 2·2^k − 1 = 약 2n. 그래서 n번 push하는 동안 진짜 일한 양은 O(2n) = O(n)이고, push당 평균은 O(1) — 이게 분할상환 O(1)의 실제 계산입니다. 면접에서 흔히 '왜 1.5배가 아니라 2배냐'고 물어요. 2배는 재할당 횟수 대비 최악 비용을 줄이고(메모리 50% 이내로 사용되지 않는 평균 슬랙), 1.5배는 재할당을 더 자주 하지만 메모리 슬랙을 ~33%로 줄여요. C++ vector는 보통 1.5 또는 2, Rust Vec은 2로 기본 구현돼 있습니다.
실무 함정은 두 가지. 첫째, capacity를 미리 알면 with_capacity(n)으로 한 번에 잡는 게 절약이에요. Rust에선 Vec::with_capacity가 추가 할당을 0으로 만들어요. 둘째, 'O(1) 끝삽입'이 어디까지나 끝삽입이라는 점이에요. 중간 insert는 여전히 O(n)이라 빈자리가 많은 리스트를 큐처럼 쓰면 비효가 큽니다. 이런 경우엔 VecDeque처럼 양쪽 끝 모두 O(1)인 ring buffer로 가는 게 정답이에요.
- 동적 배열의 분할상환 O(1) 증명: 재할당 비용의 합이 기하급수의 부분합이라는 점
- 2배 vs 1.5배 growth factor — 메모리 슬랙 vs 재할당 빈도 trade-off
- 연속 메모리 = 캐시 지역성, 그래서 실제 상수 인자가 연결 리스트보다 훨씬 좋다
- Vec::with_capacity, ArrayList.ensureCapacity로 재할당 0회 설계가 실무 팁
우리 코드Rust Vec이 oxi 전체의 기본 컬렉션. oxicode의 직렬화 버퍼, oxibrain의 청크 저장소, oxibrowser의 JS 평가 컨텍스트 컬렉션이 모두 Vec 기반.
관련 항목: 이동
연결 리스트 Linked List 그림 우리코드
각 노드가 값 + 다음 노드 포인터를 들고 있는 사슬 — 위치를 알면 삽입/삭제 O(1), 탐색은 어쩔 수 없이 O(n). 현대 CPU에서는 캐시 적중이 나빠서 배열보다 느린 경우가 흔하다.
연결 리스트는 '메모리 어디에 흩어져 있는가'보다 '다음 노드를 가리키는 포인터로 이어져 있는가'가 본질이에요. 단일 연결 리스트(singly linked)는 한 방향만, 이중 연결 리스트(doubly linked)는 prev/next 포인터 둘 다 가져 양방향 순회와 O(1) 삭제가 가능하지만 노드당 포인터 메모리가 2배로 들어요. 순환 연결 리스트는 마지막이 다시 첫 노드를 가리키는 형태로, 라운드로빈 스케줄러·게임 턴 구조에 쓰입니다.
삽입/삭제가 O(1)이라고 흔히 말하는데, 이건 '위치를 이미 알 때' 전제예요. head에서 k번째 노드를 찾아서 그 뒤에 새 노드를 끼운다고 치면 k번 점프해서 k번째에 도달하는 게 O(k). 평균 O(n/2) = O(n)이 들어요. 진정한 O(1)인 사용처는 요렇게 한정적이에요 — (1) 이터레이터로 이미 순회 중인 노드의 next에 삽입, (2) 더블 링크드에서 prev 포인터로 노드를 떼어내기. 면접에서 '언제 리스트가 배열보다 우월한가' 물으면 이 점을 짚어야 해요.
현대 하드웨어의 진짜 문제는 캐시와 메모리 할당이에요. 노드가 흩어져 있으니 i번째 노드를 읽을 때마다 포인터를 따라가며 캐시 미스가 납니다. CPU가 prefetcher로 미리 가져오기도 어렵고, 할당기(allocator)도 노드 단위 malloc을 반복해 fragmentation과 헤더 오버헤드가 생겨요. 그래서 실무에선 '큐가 필요하면 VecDeque, 작은 순차 데이터는 Vec, 양방향 + 중간삽입이 핵심이면 진짜 doubly linked' 정도로 단순하게 결정합니다. std::collections::LinkedList가 표준에 있긴 하지만, Rust 공식 문서도 "거의 쓸 일이 없다"고 적혀 있을 만큼 적게 쓰이는 구조예요.
- 삽입/삭제 O(1)은 '위치를 알 때'라는 전제 — 평균 위치 탐색은 O(n)
- 캐시 적중이 배열보다 나빠서, 단순 순차 데이터는 Vec이 더 빠르다
- 이중 연결 리스트는 노드당 메모리 2배지만 양방향 순회·O(1) 삭제 가능
- Rust std::collections::LinkedList는 거의 사용 X — VecDeque 또는 Vec+swap_remove로 충분
우리 코드oxi는 연결 리스트 대신 VecDeque(양쪽 끝 큐)·tokio mpsc 채널·Vec로 같은 패턴을 처리. 동일 표현을 더 빠른 자료구조로 대체한 사례.
관련 항목: 이동
스택 / 큐 / 우선순위 큐 Stack / Queue / Priority Queue 그림 우리코드
스택은 LIFO(후입선출) — 가장 최근 것이 먼저 나간다. 큐는 FIFO(선입선출) — 먼저 들어온 것이 먼저. 우선순위 큐는 들어온 순서와 무관하게 우선순위가 가장 높은 것이 먼저다.
스택은 생각보다 어디에나 있어요. 함수 호출 스택이라서 재귀를 함수 호출로 바꿀 수 있는 것도, 브라우저 '뒤로 가기' 한 번에 한 단계 거꾸로 가는 것도, JSON/XML을 파싱할 때 여는 괄호(`{[(`)를 만나면 push, 닫는 괄호(`}])`)를 만나면 pop해 짝을 맞추는 것도 다 스택이에요. 알고리즘 쪽으론 DFS, 실행 취소(undo)/다시 실행(redo), 컴파일러의 표현식 평가가 스택의 대표 손자입니다.
큐는 반대 성격 — 공정함이 핵심이에요. 먼저 들어온 작업부터 처리해야 하니까요. 라운드로빈 CPU 스케줄링, BFS, 메시지 큐(RabbitMQ/Kafka), 운영체제의 ready queue, 프린터 스풀러가 다 큐예요. 변형으로 양쪽 끝 모두 삽입/삭제 가능한 덱(double-ended queue)이 있는데, Rust VecDeque 같은 거예요. 그리고 우선순위 큐는 '기다린 시간'과 무관하게 우선순위 숫자가 가장 큰(혹은 작은) 원소를 먼저 내보내요. 보통 이진 힙을 구현체로 씁니다 — 삽입/추출 O(log n), 루트 한 번 보면 최댓값 또는 최솟값이라 O(1).
우선순위 큐의 활용은 면접 빈출이에요. 다익스트라는 '지금 가장 가까운 정점을 꺼내 방문한다' — 우선순위 큐의 일반화 그 자체입니다. A*는 휴리스틱이 추가된 변형이고, Huffman 코드·Prim MST·K-way 머지·이벤트 시뮬레이션이 다 우선순위 큐를 백엔드로 둬요. 두 개의 힙(최대힙 + 최소힙)으로 데이터 스트림의 중앙값을 동적으로 추적하는 '중앙값 힙' 트릭은 잘 알려진 면접 질문이에요 — 상위 절반은 최대힙, 하위 절반은 최소힙에 두고 peek 두 개로 중앙값을 구합니다.
- 스택 — 호출 스택·DFS·undo·괄호 짝맞추기·후위 표기법 평가
- 큐 — BFS·작업 대기열·라운드로빈·메시지 큐·준비 큐
- 우선순위 큐 — 다익스트라·A*·Huffman·Prim MST의 공통 백엔드
- 중앙값 힙 트릭: 최대힙 + 최소힙 두 개로 스트림 중앙값 추적
우리 코드oxibrowser의 CDP actor가 tokio mpsc 채널(큐)로 !Send JS 컨텍스트와 통신 — 동시성 모델의 핵심. oxibrain의 ingestion 파이프라인은 우선순위 작업 스케줄러를 백엔드로 둠.
관련 항목: 이동
해시 테이블 Hash Table 그림 우리코드
해시 함수로 키를 정수 버킷 번호로 바꿔 평균 O(1)에 조회·삽입·삭제 — 최악은 의도하지 않은 충돌이 한 버킷에 집중되면 O(n). 동적 크기 조절(resize)과 보안 해시가 운영 핵심.
해시 테이블은 본질이 '키를 정수 버킷 번호로 환원한다'는 한 줄이에요. bucket_index = hash(key) % N. 그러면 조회·삽입·삭제가 평균 O(1)이 됩니다 — 이론상으론. 다만 함정이 둘 있어요. 첫째, 서로 다른 키가 같은 버킷으로 가는 '충돌'. 둘째, 키가 몰리는 도메인(URL 파라미터·로그 키·JSON 필드명 등)에서 hash 함수가 분산을 잘 못 할 때 공격자가 의도적으로 같은 버킷으로 보낼 수 있다는 점 — 이게 바로 hash flooding DoS예요. 그래서 Java·Node.js·Python v2-v3가 모두 보안 해시(SipHash 등)로 전환했습니다.
충돌 해결의 두 가지 대표 방식 — 체이닝 vs 개방 주소법, 이 둘은 표 같이 외우면 체이 쉽게 외워요. 체이닝(chaining, separate chaining)은 각 버킷에 연결 리스트(혹은 트리)를 두는 방식이에요. 장점은 (1) 삭제가 단순 — 그냥 노드 떼어내기, (2) 로드 팩터(load factor = 원소수/버킷수)가 1을 넘어도 동작, (3) 같은 버킷의 원소 순회가 가벼움. 단점은 (1) 노드마다 next 포인터·힙 할당 오버헤드, (2) 캐시 친화도가 떨어져요 — 포인터 따라가면 미스가 납니다. Java HashMap이 옛 버전엔 체이닝, JDK 8 부터는 같은 버킷이 8개 이상 모이면 트리(TreeNode)로 자동 변환해서 최악을 O(log n)으로 끌어내려요.
개방 주소법(open addressing)은 충돌이 나면 다음 빈 슬롯을 찾아가는 방식이에요. 선형 프로빙(linear probing)은 충돌 슬롯 +1로 이동, 이차 프로빙(quadratic probing)은 +1, +4, +9, …, 이중 해싱(double hashing)은 보조 해시 함수의 결과만큼 이동. 장점은 캐시 친화적 — 메모리상에 평탄하게 깔려 있어요. 단점은 로드 팩터를 0.7 이하로 잡아야 성능이 유지돼요(0.8 넘으면 슬롯이 빽빽해져 클러스터링이 일어나고 길이가 길어짐), 삭제가 까다로워서 'tombstone' 마킹이 필요, 로드 팩터 임계 초과 시 2배 resize가 강제로 일어나요.
로드 팩터와 resize 과정을 숫자로 보면 — 16 버킷에 최대 12(로드 팩터 0.75)까지 채워요. 13번째 키가 들어오면 (1) 새 32 버킷 버퍼를 잡고, (2) 기존 16 버킷의 모든 키를 새 버킷의 hash % 32로 재배치(rehash)하고, (3) 옛 버퍼를 비꿔요. 이 한 번이 O(n)이고, n번 삽입 평균은 분할상환 O(1)이라는 게 동적 배열과 같은 트릭이에요. 그래서 면접에선 '왜 로드 팩터 임계 초과에 resize하느냐'도 분할상환 분석과 엮어 물어요. SipHash는 Aumasson과 Bernstein이 만든 keyed 해시 함수예요 — 같은 키도 프로세스마다 시드가 달라서 공격자가 미리 계산해둔 충돌 키를 보낼 수 없게 만들어요. Rust HashMap이 사실은 임의 시드로 매 호출마다 다른 SipHash를 사용한 결과예요.
- 평균 O(1) vs 최악 O(n), 그리고 hash flooding DoS → 보안 해시 도입 배경
- 체이닝은 삭제·순회·과밀 OK, 개방 주소법은 캐시 친화적·로드 팩터 0.7 이하
- 로드 팩터 0.75 초과 시 2배 resize — 비용 O(n)지만 분할상환 O(1)
- SipHash·Rust HashMap 임의 시드: 같은 키도 인스턴스마다 다른 버킷으로
우리 코드oxicode의 InternalUrlRouter(internal:// 스킴 라우팅), oxibrain projection의 HashMap 키 조회, oxi 전반의 캐시 인덱스가 모두 해시 테이블.
관련 항목: 이동
힙 / 우선순위 큐 (Heap) Heap / Priority Queue 그림 우리코드
완전 이진 트리에 '부모 ≥ 자식'(max-heap) 또는 '부모 ≤ 자식'(min-heap) 불변식을 유지한 구조 — 삽입·삭제 O(log n), 루트가 가장 큰/작은 값이라 peek O(1). 우선순위 큐와 힙 정렬의 기본 백엔드.
힙이라는 이름은 처음 보면 'heap(더미)'과 헷갈리는데, 자료구조 힙은 사실 '힙 불변식'이 핵심이에요. 두 가지를 동시에 만족해요. (1) 완전 이진 트리 — 마지막 레벨을 제외하면 모든 레벨이 가득 차고, 마지막 레벨은 왼쪽부터 채워짐. (2) 부모가 자식보다 우선한다(max-heap 기준). 모양 조건(완전 이진) 덕분에 1차원 배열로 깔끔하게 저장됩니다 — i번 노드의 자식은 2i+1, 2i+2, 부모는 (i-1)/2. 포인터가 필요 없어요. 트리 모양이 빈틈없으니 인덱스 계산만으로 부모·자식 점프가 됩니다.
삽입은 append + sift-up(상향 거품)이에요. 마지막 자리에 넣고 부모보다 우선순위가 낮으면(혹은 낮으면 max-heap) 부모와 swap, 또 swap — 트리 높이만큼 걸려서 O(log n). 추출은 root(가장 큰 값)를 빼고 마지막 원소를 root로 끌어올린 뒤 sift-down(하향 거품) — 다시 자식 둘 중 우선순위가 큰(작은) 쪽과 swap. 끝까지 가면 트리 높이만큼 O(log n). peek은 그냥 arr[0] 한 번 읽기 — O(1). 이게 다예요.
힙 정렬은 '힙에 n개 넣고 n번 뽑으면 정렬'이라는 아이디어인데, 사실 in-place로 O(1) 추가 메모리로도 구현 가능합니다 — 마지막 자리에 최댓값을 내려놓고 heap size를 줄여가며 다시 heapify. 그래서 메모리 제약 상황에서 O(n log n) 정렬이 필요할 때(예: 외부 정렬) 쓰여요. 다만 캐시 패턴이 좋아도 Tim Sort·Introsort 같은 하이브리드 정렬이 워낙 빨라서 단독으로는 거의 안 씁니다. 면접 포인트는 '힙 정렬이 안정 정렬은 아니다' — 같은 값이 입력·출력에서 순서가 바뀔 수 있어요.
실무 활용은 우선순위 큐 인터페이스가 핵심이에요. 다익스트라는 '거리 최단 정점' 반복 추출(우선순위 큐), Prim MST는 '지금 트리에서 가장 싼 간선' 추출, Huffman 코딩은 '가장 낮은 빈도 두 노드 합치기' 추출, A*는 'f = g + h 최솟값' 추출, 이벤트 시뮬레이션은 '다음 이벤트 시각' 추출. 고급 변형으로 이진 힙 외에 Fibonacci 힙(추출·합병 O(log n), decrease-key O(1) 분할상환 — 다익스트라를 O(E + V log V)로 단축), Binomial 힙(가장 빠른 합병 O(log n)), Pairing 힙(실측 빠른 단조 힙)이 있어요. 면접에선 보통 이진 힙으로 충분히 답할 수 있습니다.
- 완전 이진 트리 + 부모 우선 — 배열 한 개로 인덱스 계산만 점프
- 삽입 O(log n) sift-up, 추출 O(log n) sift-down, peek O(1)
- 힙 정렬은 O(n log n)·in-place·불안정 — 메모리 제약 상황에 외부 정렬로
- Fibonacci/Binomial/Pairing 힙은 이론상 우수, 실무엔 이진 힙 충분
우리 코드settlement_story·optimal-route-planner의 A* 오픈 셋, 다익스트라 최단거리 추출이 모두 이진 힙 기반. oxibrain의 우선순위 작업 스케줄러도 이진 힙.
관련 항목: 이동
B-Tree / B+Tree B-Tree 그림 우리코드
한 노드가 여러 키와 자식 포인터를 다 같이 갖는 다진(m-ary) 탐색 트리 — 디스크 블록 크기에 맞춰 fan-out을 크게 잡아 트리 높이를 낮추고, B+Tree는 리프에 데이터를 모아 리프끼리 연결해 범위 스캔을 빠르게 한다.
왜 B-Tree가 필요한지를 이해하려면 디스크 I/O의 비용부터 봐야 해요. 메모리 접근이 100ns 수준인 데 비해 SSD random read 4KB가 25μs, HDD random read가 10ms — 즉 메모리 대비 250배에서 100,000배 비싸요. 그래서 인덱스 구조에선 '노드 한 번 읽기'에 fan-out을 맞추는 게 핵심이에요. 이진 탐색 트리(BST)는 한 노드에 키 1개라 fan-out=2, 1억 개의 키를 넣으면 높이 ≈ log₂(10⁸) ≈ 27, 즉 27번의 임의 I/O가 필요해요. B-Tree는 한 노드에 키 100~1000개를 담으니 fan-out=100이면 log₁₀₀(10⁸) ≈ 4, 키 1,000이면 log₁₀₀₀(10⁹) ≈ 3 — 1억 개여도 디스크 I/O가 3~4번이면 충분해요.
구체 숫자로 예를 들어볼게요. PostgreSQL 기본 페이지 크기 8KB, 키가 8바이트 + 자식 포인터 8바이트 = 노드당 약 500개 키를 담을 수 있어요. 1억 개 레코드면 높이 3, 즉 root·중간·리프 세 번의 페이지 읽기로 임의 키 도달이 끝나요. 그래서 B+Tree 인덱스 탐색이 HDD에서도 millisecond 안에 끝나는 거예요. 이게 'fan-out이 디스크 페이지 크기와 일치하도록 설계된다'는 면접 포인트의 실제 의미입니다.
B-Tree와 B+Tree의 차이가 실무에선 중요해요. B-Tree는 키를 내부 노드와 리프에 모두 두지만, B+Tree는 데이터(혹은 데이터 포인터)를 리프에만 두고 리프끼리 linked list로 연결해요. 장점은 (1) 내부 노드에서 키만 비교하니 fan-out이 더 커져요, (2) 리프에서 시작해 왼쪽으로 끝까지 순회하면 자동으로 정렬된 결과를 얻어요 — range scan(`WHERE date BETWEEN '2024-01-01' AND '2024-12-31'` 같은)에 결정적이에요. MySQL InnoDB가 이 구조에 '클러스터드 인덱스'까지 얹어서, primary key 순서로 물리 디스크 정렬 + secondary index는 primary key를 값으로 갖게 만들어요.
재균형 비용은 B-Tree 노드 분할·결합이에요. 어떤 노드가 가득 차면 가운데 키를 부모로 올리고 좌우로 쪼개요. 반대로 너무 비면 옆 형제에서 빌려오거나 합쳐요. LSM Tree(Log-Structured Merge Tree)는 B-Tree의 write-friendly 대안이에요 — 쓰기를 메모리(memtable)에 모았다가 디스크 SSTable로 순차 flush, 배경에서 compaction으로 병합. HBase·RocksDB·LevelDB가 이 패밀리예요. 면접에선 '쓰기가 많으면 LSM, 읽기가 많으면 B-Tree'로 단순화해 답하면 충분합니다.
- fan-out = 페이지 크기 / 키+포인터 크기 — 높이를 3~4로 유지
- B+Tree의 강점: 리프 연결 리스트로 range scan 효율적, 내부 fan-out 더 큼
- 재균형: 노드 분할(overflow)/결합(underflow)이 비용
- LSM Tree는 write-friendly 대안 — RocksDB, LevelDB, HBase
우리 코드SQLite/PostgreSQL 인덱스, oximemo의 redb(B-tree 기반 KV 저장소)가 모두 이 구조.
관련 항목: 이동
트라이 Trie 우리코드
문자열의 접두사 트리 — 각 노드가 한 글자를, 루트에서 단말까지의 경로가 하나의 키를 표현. 키 길이 m에 대해 삽입·탐색·접두사 조회가 O(m). 메모리는 키별·알파벳별 노드 포인터 비용이 비싸다.
Trie = Retrieval(탐색) 트리 · 흔히 digital tree·prefix tree로도 불림
트라이는 '키의 문자들을 한 글자씩 트리 경로로 펼친다'는 단순한 아이디어예요. 루트는 비어 있고, 'to', 'tea', 'ted', 'ten' 같은 단어를 저장한다면 — 루트에서 't' → 'e' → 'a'/'d'/'n'으로 뻗고, 단말 노드에는 실제 키·값을 매달아요. 검색은 입력 키의 각 글자를 따라가며 단말에 도달하면 성공 — 키 길이 m만큼의 노드만 거치니 O(m)이에요. 해시 테이블이 키 전체를 한 번에 해싱하는 것과 달리, 트라이는 '공통 접두사'를 트리 구조가 무료로 흡수해줍니다. 그래서 'tea'로 시작하는 모든 키를 모으고 싶으면 't'→'e'→'a' 아래의 서브트리를 DFS로 한 번만 돌면 끝나요.
트라이가 특히 빛나는 게 자동완성·철자 교정이에요. 구글이 검색창에 'py'를 입력하면 'python', 'pypi', 'pygame'을 보여주잖아요 — 이게 정확히 트라이 + 깊이 제한 순회입니다. 중간 글자 한두 개가 틀린 철자 교정도 트라이 + 편집 거리(levenshtein) 트리 탐색으로 풀어요. 다만 메모리가 비싸다는 게 트라이의 유일한 큰 약점이에요. 알파벳 26글자에 대해 노드당 26개 자식 포인터 배열을 두면 키 N개·평균 길이 m일 때 O(N·m·alphabet) 메모리 — 비효가 심해요. 그래서 실전에선 배열 대신 hashmap으로 자식 포인터를 압축하거나, '이중 배열 트라이(dat)'로 두 개의 평탄 배열에 자식/형제 인덱스를 저장하는 압축 트라이를 씁니다 — Lucene·Elasticsearch의 FST(finite state transducer)가 정확히 이 변형이에요.
오타·한국어 검색과의 관계를 짚으면요, 한국어는 교착어라 검색이 까다로워요. '먹었습니다'를 정확히 입력하라는 검색은 사용자 경험이 나빠요. 트라이는 보통 단어 단위(어간) 인덱스를 갖고 형태소 분석을 거친 뒤 lookup해서, '먹다' 검색이 '먹었습니다'·'먹었어요'·'먹고 싶'까지 매칭되게 만들어요. 한국어의 띄어쓰기·조사 처리는 mecab·khaiii 같은 형태소 분석기와 결합해 해결합니다. 부분 매칭·n-gram 색인은 보완책으로 자주 함께 둬요 — oxibrain의 n-gram 인덱스가 정확히 이 역할이에요. 트라이의 변형으로 suffix trie / suffix array / DAWG(Directed Acyclic Word Graph) / FST가 '문자열 안의 패턴' 문제로 확장되고, Aho-Corasick은 트라이 + KMP 실패 링크로 다중 패턴 매칭을 한 번에 처리해요 — 악성코드 검사·텍스트 마이닝 표준 기법입니다.
- 삽입·탐색·접두사 조회가 키 길이 m에 O(m) — 공통 접두사가 많아도 빠르다
- 메모리 비쌈 → 압축 트라이(이중 배열), FST(Lucene)가 표준
- 자동완성·철자 교정·사전 검색에 특화, 한국어 같은 교착어는 형태소 분석과 결합
- 확장 — suffix trie·DAWG·Aho-Corasick(트라이+KMP)·FST
우리 코드oxibrain의 n-gram 인덱스가 부분문자열·접두사 검색을 담당 — lexical 검색 계층의 일부. 한국어 검색은 형태소 분석기와 결합.
관련 항목: 이동
R-Tree / 공간 인덱스 R-Tree 직무 그림 우리코드
최소 경계 사각형(MBR, Minimum Bounding Rectangle)을 계층으로 묶는 트리 인덱스 — 평면·입체 데이터의 범위·최근접·교차 질의 표준. 2차원 이상이면 B-Tree 대신 이쪽.
R-Tree의 핵심 아이디어를 그림처럼 떠올려 보면 이해가 빨라요. 서울시 지도에 카페 1,000개가 점으로 흩어져 있다고 치죠. 화면에 '강남구' 사각형을 그려 그 안의 카페를 다 찾고 싶어요. 점 1,000개를 일일이 검사해도 되지만, 어딘가에 미리 '이 구역엔 이 점들이 있다'를 메모해 두면 빨라요. R-Tree는 이걸 트리로 계층화해요. (1) 리프에 점(혹은 도형)을 5~10개씩 담고, (2) 그 점들을 모두 감싸는 가장 작은 사각형 — MBR — 을 계산해요. (3) MBR들을 부모 노드에서 다시 5~10개씩 묶고, (4) 그 MBR들을 더 큰 MBR로 감싸요. 결국 루트엔 '서울 전체 MBR' 하나가 있고, 그 안에 '강남구 MBR', '서초구 MBR', …가 있고, 그 안에 또 작은 사각형들이 잎으로 내려가요.
질의할 때 그림을 보듯 직관적이에요. '강남구 안에 있는 카페 다 줘'라는 사각형 질의(query rectangle)를 던지면 — (1) 루트의 MBR과 query가 겹치는 자식만 탐색, (2) 그 자식의 MBR이랑 또 비교, 겹치는 것만 더 아래로, (4) 리프에 도달하면 실제 점과 비교. 그래서 점 1,000개 중 답이 50개라면, 모든 점 1,000개를 검사하는 bruteforce O(n) 대신 overlap이 적은 노드만 골라 들어가니 평균 O(log n)에 가깝게 줄어들어요. 면접에선 '왜 B-Tree 대신 R-Tree?'가 핵심 포인트 — B-Tree는 1차원 키 정렬이라 위도·경도 두 값을 동시에 정렬하는 데 인위적인 곡선이 필요해요(z-order curve, Hilbert curve). R-Tree는 차원에 구애받지 않고 2D·3D·4D 모두 동일 구조로 작동합니다.
실무에서 자주 마주치는 변형이에요. R+ Tree는 서브트리가 서로 겹치지 않게 split해 overlap을 줄여 — 검색은 더 빠르지만 업데이트 비용이 크고 빈 사각형 노드가 생겨요. R* Tree는 노드 분할 시 면적이 작아지도록 재배치 휴리스틱(forced reinsert, choose split axis)을 도입해 분할 품질을 높였고, 사실상 표준이에요. PostGIS의 GiST(Generalized Search Tree)는 R-Tree를 일반화한 프레임워크 — R-Tree·B-Tree·trigram 모두 같은 인터페이스로 인덱싱할 수 있어요. Hilbert R-Tree는 Hilbert 곡선 키로 점들을 1차원 정렬한 뒤 B-Tree-like로 저장해 — 데이터 삽입이 자주 일어나는 시나리오에 강해요. 시선아이티의 GIS 도메인에서 '왜 R-Tree냐'는 질문은 곧 '왜 공간 DB이냐'와 거의 동의어 — 공간 인덱스의 사실 표준이니까요.
- MBR 계층 구조로 range/nn(최근접)/intersection 질의 효율화
- B-Tree 대비 장점: 2D·3D 자연스러움, 다차원 동일 구조
- R*(재분배 휴리스틱), R+(no overlap), Hilbert R-Tree 등 변형 존재
- PostGIS의 GiST가 R-Tree 일반화 — 하나의 인덱스 프레임워크로 통합
우리 코드시선아이티의 GIS 정체성에서 '공간 인덱스 구조'를 아는 것 자체가 차별점. R-Tree와 MBR 개념은 면접 필수 어휘.
관련 항목: 이동
그래프 / 트리 Graph & Tree 그림 우리코드
정점(Vertex)과 간선(Edge)의 집합 — 트리는 사이클 없는 연결 그래프의 한 형태. 표현은 인접 리스트(O(V+E))와 인접 행렬(O(V²)) 두 가지가 기준이고, 방향·가중·사이클 존재 여부가 알고리즘 종류를 결정한다.
그래프와 트리는 면접 빈도가 높은 영역이에요. 첫 결정은 표현이에요. 정점 V개에 대해 — 인접 리스트(각 정점의 이웃 리스트를 보관)는 공간 O(V + E), 'v의 이웃을 순회'가 O(degree(v)), '두 정점이 인접한가'는 최악 O(degree(v)) — sparse 그래프(E ≪ V²)에 잘 맞아요. 인접 행렬(V×V 2차원 bool/int 배열)은 공간 O(V²), 'v의 이웃 순회'가 O(V), '두 정점이 인접한가'는 O(1) — dense 그래프에 잘 맞고 빠른 edge-existence test. 예를 들어 페이스북 친구 그래프는 V = 30억 명, E = 평균 친구 수 350, 인접 리스트는 30GB 메모리, 인접 행렬은 30억² = 9×10¹⁹ 셀(불가능) — sparse 데이터는 거의 항상 인접 리스트입니다. 면접에선 'V=10,000, E=50,000이면 어느 쪽?' 정도 숫자로 짚어 답해요.
위상 정렬(topological sort)은 사이클 없는 방향 그래프(DAG)에서 정점을 의존성 순서로 늘어놓는 알고리즘이에요. Kahn 알고리즘은 들아보 차수(in-degree)가 0인 정점을 큐에서 꺼내 출력, 그 정점에서 나가는 간선을 제거 → 인접 정점의 in-degree 감소 → 또 0이 된 정점을 큐에. 면접에선 '빌드 시스템 의존성' 예시가 직관적이에요 — A가 B에 의존(A → B), B가 C에 의존(B → C)인 경우 위상 정렬 결과는 [C, B, A]이고 '먼저 C 컴파일, 다음 B, 마지막 A'라는 순서를 만들어요. Make·Maven·npm install·DAG 기반 스케줄러·DB의 의존성 분석이 전부 이 알고리즘 위에서 돌아요. DFS로 푸는 변형(Tarjan)도 있는데, 재귀 깊이가 깊으면 stack overflow 위험이 있어 면접에선 Kahn + 명시적 큐가 안전합니다.
그래프 알고리즘의 핵심 분류를 짧게 짚으면 — (1) BFS: 비가중 최단 경로·사이클 검출·연결 요소·이분 그래프 판정, (2) DFS: 위상 정렬·강연결 요소(SCC)·해밀턴 경로·백트래킹·미로, (3) 다익스트라(우선순위 큐): 양수 가중 최단 경로 O(E log V) — 음수 가중은 벨만-포드 O(VE)로 가야 해요. (4) 최소 신장 트리(MST): 무방향 가중 그래프에서 V-1개 간선으로 모든 정점 잇는 최소 비용 트리 — Kruskal(union-find, 간선 정렬) vs Prim(우선순위 큐, 정점 확장). (5) Floyd-Warshall: 모든 쌍 최단 경로 O(V³). MST·다익스트라·벨만-포드는 면접 코딩 단골이고, 그래프 표현 선택 + 알고리즘 선택 + 시간복잡도가 한 묶음으로 묶여 묻혀요. 트리는 사이클 없는 연결 그래프의 특수 케이스라서 그래프 알고리즘이 그대로 적용되지만, BFS·DFS를 트리에 한해 더 단순하게(재귀로) 쓸 수 있어요 — 이진 탐색 트리·힙·트라이·B-Tree가 모두 트리 특화 알고리즘입니다.
- 인접 리스트 O(V+E) vs 인접 행렬 O(V²) — sparse vs dense 선택
- 위상 정렬 = 빌드 의존성·DAG 스케줄링 — Kahn + 큐가 표준
- 다익스트라(양수 가중 O(E log V)) vs 벨만-포드(음수 가중 O(VE))
- MST는 Kruskal(union-find) vs Prim(우선순위 큐) — 면접 빈출
우리 코드oxicode-mnemopi의 episodic_graph(기각 에피소드 연관), oxibrain의 ingestion 의존성 DAG, optimal-route-planner의 경로 탐색이 모두 그래프 문제.
관련 항목: 이동
알고리즘 14
복잡도 분석, 탐색, 정렬, 그래프, DP, 기하 — 시선아이티의 GIS 경로 최적화·공간 결정·챗봇 검색과 직결되는 알고리즘 지식.
시간·공간 복잡도 Time / Space Complexity
입력 크기 n이 커질 때 연산 횟수(시간)와 추가 메모리(공간)가 어떻게 증가하는지 점근 표기로 표현한 것. Big-O가 가장 흔하지만 Big-Ω(하한), Big-Θ(상하 일치), 분할상환(amortized)도 함께 본다.
복잡도의 핵심 질문은 단 하나예요. "입력이 두 배로 늘어나면 내 코드는 몇 배로 느려지는가?" n=1,000일 때 1초 걸리던 게 n=2,000에서 2초면 O(n) 선형, 4초면 O(n²) 제곱이에요. 8초면 O(n³) 입방. 입력 크기 변화에 따른 연산량의 성장률만 보면 됩니다.
Big-O는 상한(최악) 표기예요. 면접에서 "퀵소트 시간 복잡도는?" 이라고 물으면 O(n log n)이라고 답하곤 하는데, 그건 평균이에요. 최악은 O(n²) — 이미 정렬된 입력에 나쁜 피벗을 골라서 한쪽으로만 분할될 때죠. 그래서 Big-O 하나로는 부족하고, 최선·평균·최악을 따로 구분해야 합니다.
공간 복잡도는 입력 자체를 제외한 추가 메모리예요. 병합 정렬은 O(n) 추가 버퍼가 필요한 in-place가 아닌 정렬, 퀵소트는 O(log n) 재귀 스택만 쓰는 in-place. "제자리(in-place) 정렬"이라는 말이 나오면 보통 O(1)~O(log n) 추가 메모리를 뜻해요.
분할상환(amortized) 분석은 따로 봐야 해요. 동적 배열에 push_back을 n번 하면 최악의 경우 O(n)이 한 번 끼어들 수 있지만, n번 전체로 나누면 push당 O(1) — 이게 분할상환 O(1)이에요. 최악과 분할상환을 헷갈리면 면접에서 떨어집니다.
- Big-O 외 Big-Ω(하한), Big-Θ(상하 일치), small-o(점근 미만)도 구분할 것
- 최악(worst-case)이 면접의 1차 기준 — 평균은 분포 가정이 따라옴
- 분할상환(amortized) ≠ 최악 — 동적 배열 push_back의 분할상환 O(1)이 대표 사례
- 공간 복잡도는 입력 제외 추가 메모리 — in-place 정렬은 보통 O(log n) 스택만 허용
버블·삽입·선택 정렬 (O(n²) 기초 정렬) Bubble / Insertion / Selection Sort
비교 기반 O(n²) 정렬. 모두 제자리(in-place)지만 실제로는 거의 안 쓰며, 학습과 거의 정렬된 데이터의 특수 케이스에서만 의미를 가진다.
Bubble Sort = 단순 교환 정렬, Insertion Sort = 단순 삽입 정렬, Selection Sort = 단순 선택 정렬 — 비교 기반 O(n²) 정렬 3종
버블 정렬은 인접한 두 원소를 비교해 필요하면 교환하는 걸 한 번의 패스(pass) 동안 배열 끝까지 반복해요. 한 패스가 끝나면 가장 큰 원소가 배열 끝으로 가라앉아요. 그래서 pass당 비교 횟수가 n-1, n-2, …, 1로 줄어들며 총합 n(n-1)/2 = O(n²). 핵심 최적화는 스왑이 한 번도 일어나지 않은 패스에서 early-exit(조기 종료)하는 것 — 이미 정렬된 배열이면 O(n) 만에 끝나요.
삽입 정렬은 카드 손에 쥐고 정렬하는 방식이에요. 왼쪽부터 한 칸씩 key를 잡고, 그 key보다 큰 원소들을 오른쪽으로 밀어서 빈 자리에 key를 삽입. 최선(거의 정렬) O(n), 최악(역순) O(n²). 안정(stable) 정렬이에요 — 같은 키의 원래 순서가 유지돼요. 이 '거의 정렬된' 데이터 강점이 핵심이고, Tim Sort가 run(연속 구간) 길이가 짧을 때 삽입 정렬로 폴백하는 이유이기도 해요.
선택 정렬은 매 패스마다 '나머지 중 최솟값'을 골라 맨 앞과 교환해요. 비교는 항상 O(n²), 스왑은 정확히 n-1회. 그래서 스왑 비용이 큰 EEPROM·플래시 기록 같은 환경에선 의미가 있는데, 일반 메모리에서는 버블/삽입보다 거의 항상 느려요. 불안정(unstable)인 것도 단점이에요.
세 알고리즘 모두 작은 n(보통 16~32 이하)에서는 캐시 locality가 좋아서 의외로 빠르어요. 그래서 Tim Sort·Introsort는 작은 구간을 만나면 삽입 정렬로 폴백해요. 다만 실무에서 n이 큰 일반 데이터를 O(n²)으로 정렬하는 건 권장하지 않습니다 — 1만 건만 넘어도 1억 회 비교가 필요해요.
- 삽입 정렬이 '거의 정렬된 데이터'에 강한 이유 — 패스당 평균 shift가 적어 O(n)에 수렴
- 버블 정렬의 early-exit 최적화: 스왑 0회면 break → 최선 O(n)
- 안정성 차이: 버블·삽입은 안정, 선택은 불안정 — 같은 키 순서 보존 여부
- 실무 비추: n이 1만만 넘어도 1억 회 비교 — Tim Sort·Quick가 압도
퀵·병합·힙 정렬 (O(n log n) 실전 정렬) Quick / Merge / Heap Sort
비교 기반 최악·평균 O(n log n) 정렬. 퀵이 실전 1순위(평균), 병합이 안정·외부 정렬 표준, 힙은 in-place지만 캐시 불리.
Quick Sort = 분할 정복 기반 제자리 평균 O(n log n) 정렬, Merge Sort = 안정적 O(n log n) 외부 정렬, Heap Sort = 힙 기반 제자리 O(n log n) 정렬
퀵 정렬은 피벗 하나를 골라 '피벗보다 작은 그룹 / 같은 그룹 / 큰 그룹'으로 분할한 뒤 재귀해요. 평균 O(n log n), 최악 O(n²), 추가 메모리 O(log n) 재귀 스택. 핵심은 피벗 선택이에요. 첫 원소나 마지막 원소만 쓰면 이미 정렬된 데이터에서 한쪽 그룹이 비어서 O(n²) 퇴화 — 적이 미리 데이터 패턴을 아는 공격에서도 취약해요.
피벗 선택 전략은 첫 원소 / 마지막 원소 / 중앙(median-of-3) / 무작위(rand) 정도가 있어요. 실무에선 median-of-3 또는 무작위가 표준. 분할 방식도 Lomuto(피벗을 끝으로 보내고 한 방향 스캔)와 Hoare(양쪽 끝에서 좁혀오기)가 있고, Hoare가 스왑 수가 적어요. Rust의 sort_unstable가 쓰는 pdqsort(Pattern-defeating Quicksort)는 무작위 피벗 + 작은 구간은 삽입 정렬로 폴백 + 패턴을 감지하면 힙 정렬로 전환(Introsort 패턴).
병합 정렬은 배열을 절반씩 재귀 분할하고, 돌아오면서 두 정렬된 절반을 O(n)으로 병합해요. 총 O(n log n) 항상, 안정, 단 추가 메모리 O(n) — 그래서 in-place가 아니에요. 이 추가 메모리가 허용되는 환경에선 '거의 정렬된 데이터에서 Tim Sort만큼 빠르고, 항상 일정한 성능'을 보장해요. 또한 LinkedList·외부 정렬(디스크에서 run을 읽어 k-way 병합)의 표준이에요.
힙 정렬은 배열을 max-heap으로 만든 뒤(heapify O(n)) 루트를 끝으로 보내고 sift-down을 반복해요. O(n log n) 항상, in-place O(1), 불안정. 이론적으로는 깔끔한데 캐시 locality가 나빠서 실전에선 퀵보다 느려요 — heapify가 배열 여기저기를 점프하며 접근하기 때문. 그래서 단독으로는 잘 안 쓰고, Introsort의 최악 케이스 백업으로만 등장해요.
실무 표준 한 줄 정리: Rust는 sort가 Tim Sort(안정, 작은 run은 삽입 정렬 폴백), sort_unstable가 pdqsort(dual-pivot quick + introsort 폴백). Python·Java·V8 JS·Swift 모두 Tim Sort 계열이에요. "대부분의 경우 안정 정렬이면 sort, 성능이 더 중요하면 sort_unstable"가 Rust 코드의 관례.
- 퀵 정렬 퇴화: 정렬된 입력 + 첫/마지막 원소 피벗 = O(n²) → median-of-3 또는 무작위 피벗으로 방어
- 병합 정렬의 진가: 안정성 + LinkedList·외부 정렬 — 디스크 k-way merge의 표준
- 힙 정렬은 캐시 locality 나빠서 실전에선 pdqsort·Introsort에 밀림 — 폴백 전용
- Rust sort = Tim Sort(안정), sort_unstable = pdqsort(불안정, 더 빠름)
정렬 알고리즘 비교 Sorting Algorithm Comparison 표 그림 우리코드
비교 기반 정렬의 하한은 Ω(n log n). 안정성·메모리·패턴 적응 여부가 알고리즘 선택 기준. 실무 표준은 Tim Sort / pdqsort.
비교만으로 정렬하는 알고리즘의 하한은 Ω(n log n)이에요. 결정 트리(decision tree) 모델에서 n! 가지 순열을 구분하려면 깊이 log₂(n!) = Ω(n log n)인 비교가 필요해요. 그래서 어떤 비교 기반 정렬도 이 하한을 넘을 수 없어요 — 이게 '왜 O(n²) 정렬은 큰 데이터에 답이 없는가'의 이론적 근거예요.
비교를 쓰지 않는 정렬은 이 하한을 깰 수 있어요. 계수 정렬(Counting Sort) O(n+k)은 키 범위 k가 작을 때, 기수 정렬(Radix Sort) O(n·d)는 자릿수 d가 작을 때 비교 정렬을 앞질러요. 대신 k·d만큼 추가 메모리가 필요하고, 실수·문자열 같은 키엔 적용이 까다로워요.
안정성(stability)은 면접 단골 질문이에요. 같은 키를 가진 원소들의 원래 순서를 보존하면 안정, 아니면 불안정. 다중 키 정렬(예: 부서로 먼저 묶고 그 안에서 이름순)에서 안정성이 필요해요. 안정 정렬: 병합·삽입·버블·Tim. 불안정: 퀵·힙·선택.
아래 표를 읽는 법: 시간은 평균, 메모리는 입력 외 추가, 안정은 위 정의 그대로. '거의 정렬' 데이터 강약은 Tim Sort·삽입이 강점, 나머지는 무관.
| 구분 | 특징 | 참고 |
|---|---|---|
| 버블·삽입·선택 | O(n²), 안정적(버블·삽입)/불안정(선택) | n ≤ 32 이하 또는 거의 정렬 데이터 전용 |
| 병합 정렬 | O(n log n) 항상, 안정, O(n) 추가 메모리 | LinkedList·외부 정렬·k-way merge 표준 |
| 퀵 정렬 | 평균 O(n log n), 최악 O(n²), 불안정, O(log n) 스택 | 캐시 친화적 — 실전 1순위 (pdqsort·Introsort) |
| 힙 정렬 | O(n log n) 항상, 불안정, O(1) 메모리 | 캐시 locality 나빠 단독으론 약함 — Introsort 백업 전용 |
| Tim Sort | O(n)~O(n log n), 안정, O(n) 메모리 | run + 삽입·병합 하이브리드 — 사실상 산업 표준 |
| 계수·기수 정렬 | O(n+k)·O(n·d), 안정(보통), O(n+k) 메모리 | 비비교 정렬 — 키 범위·자릿수 좁을 때 비교 정렬 추월 |
- 비교 정렬 하한 Ω(n log n) — 결정 트리 모델에서 log₂(n!) ≈ n log n
- Tim Sort가 사실상 산업 표준: Python·Java·V8·Rust `sort` 모두 Tim 계열
- 안정 정렬이 필요한지부터 결정 — 다중 키 정렬·DB ORDER BY 보조 인덱스 갱신
- 비비교 정렬(계수·기수)은 키 범위·자릿수가 작을 때만 비교 정렬 추월
우리 코드oximemo의 redb by_sort는 타임스탬프+id의 비트 보수(complement) 인코딩으로 오름차순 스캔만으로 최신순을 얻어요 — 안정 정렬 결과를 단일 인덱스에 표현하는 트릭. oxicode-mnemopi가 정렬을 안정적으로 직렬화해서 벡터 메모리 갱신 시 순서가 깨지지 않아요.
이분 탐색 Binary Search 그림
정렬된 배열에서 매 단계 탐색 구간을 절반으로 줄여 O(log n)에 찾는 알고리즘. mid = (lo+hi)/2 vs lo+(hi-lo)/2 overflow 회피가 기본.
이분 탐색은 사전에서 단어를 찾는 과정이에요. 단어 하나를 찾을 때 첫 장부터 넘기지 말고, 중앙을 펴서 비교하고, 좌·우 절반 중 하나를 버려요. 매 단계 절반을 버리므로 n=1,000,000이어도 log₂ ≈ 20번이면 끝나요. 정렬돼 있다는 전제만 있으면 O(log n).
구현의 함정은 overflow예요. mid = (lo + hi) / 2는 lo+hi가 int 최대에 가까우면 오버플로우. 그래서 mid = lo + (hi - lo) / 2로 쓰거나, 부호 없는 정수면 hi = mid + (mid < hi ? 1 : 0) 같은 디테일이 필요해요. 면접에서 화이트보드에 짜라고 하면 이런 디테일을 보는 거예요.
Lower bound / upper bound 패턴도 단골이에요. '정렬된 배열에서 target 이상인 첫 위치'는 lower_bound, '초과인 첫 위치'는 upper_bound. C++ 표준 라이브러리가 std::lower_bound로 제공. Rust의 BinaryHeap은 max-heap이라 lower_bound는 BinarySearchBy로 직접 구현해야 해요. 회전된 정렬 배열(LeetCode 33, 81) 변형도 빈출 — 한쪽은 정렬돼 있다는 사실을 분기에 활용합니다.
정렬이 깨지면 이분 탐색은 답이 아니에요. 데이터가 거의 정렬되어 있어도 보장 안 됨. '거의 정렬' 데이터에 강하려면 삽입 정렬을 쓰거나, 정렬 비용과 탐색 비용을 같이 따져야 해요.
- lower_bound / upper_bound 디테일이 면접 출제 포인트
- mid = lo + (hi-lo)/2 — overflow 회피의 정석
- 회전된 정렬 배열 변형(LeetCode 33, 81) 빈출 — 분기 조건이 핵심
- 정렬돼 있지 않으면 적용 불가 — 데이터 전처리 비용을 따져서 결정
BFS / DFS / 위상 정렬 BFS / DFS / Topological Sort 그림 우리코드
그래프를 체계적으로 순회하는 두 가지 방법. BFS는 큐로 레벨별·최단(비가중), DFS는 스택/재귀로 깊이 우선 — 위상 정렬·사이클 검출의 기반.
BFS = Breadth-First Search(너비 우선 탐색), DFS = Depth-First Search(깊이 우선 탐색)
BFS는 시작점부터 거리 0, 1, 2, … 순으로 인접 노드를 방문해요. 큐에 다음에 방문할 노드를 넣고 꺼내면서 처리하므로 레벨별로 퍼져나가는 모양. 핵심 응용은 비가중 그래프 최단 경로 — '몇 번 만에 도달 가능한가'가 거리예요. 면접에서 '최단 거리'라는 말이 가중치 없으면 BFS로 푼다고 보면 돼요.
DFS는 한 갈래를 끝까지 파고드는 방식이에요. 재귀로 짜면 가장 직관적이고, 명시적 스택으로 짜면 stack overflow도 피할 수 있어요. 응용이 무궁무해요 — 사이클 검출(현재 경로상 노드 재방문), 위상 정렬(DFS 종료 순서 역배열), 강연결 요소(SCC), 백트래킹(조합·순열·N-Queen) 거의 모든 그래프 문제의 기본 도구.
위상 정렬은 방향 그래프의 노드를 '모든 간선 u→v에서 u가 v보다 먼저' 나오도록 나열하는 거예요. 사이클이 있으면 불가능. Kahn 알고리즘(들어오는 차수가 0인 노드를 큐에서 꺼내며 나열)이 BFS 기반이고, DFS 후위 역순이 DFS 기반. 둘 다 O(V+E).
강연결 컴포넌트(SCC)도 알아두세요. Kosaraju(두 번 DFS: 원그래프와 역그래프) 또는 Tarjan(한 번 DFS + low-link). 의존 그래프의 순환 의존 분석, 컴파일 모듈 의존성, 추천 시스템의 사용자 그래프 클러스터링에 쓰여요.
- BFS = 비가중 최단 경로, DFS = 사이클 검출·위상 정렬·백트래킹의 기반
- 위상 정렬은 Kahn(BFS, 차수 0 큐) 또는 DFS 후위 역순 — 사이클 있으면 불가
- SCC 알고리즘: Kosaraju(2 DFS), Tarjan(low-link) — 의존 그래프 순환 분석
- BFS는 큐, DFS는 스택/재귀 — 면접에서 명시적 스택 구현도 가능해야 함
우리 코드oxibrain의 ingestion 파이프라인에서 의존 그래프를 위상 정렬로 처리해요. settlement_story의 탐색은 그리드 BFS + A*를 함께 써요.
다익스트라 / A* / 최단 경로 Shortest Path — Dijkstra / A* / Variants 직무 그림 우리코드
가중 그래프 최단 경로. 다익스트라(우선순위 큐 O(E log V), 음수 간선 불가), A*(휴리스틱으로 탐색 공간 절감).
BFS = Breadth-First Search, A* = A-star(휴리스틱 최선 우선 탐색), SSP = Single-Source Shortest Path(단일 출발 최단 경로)
다익스트라는 '아직 확정되지 않은 노드 중 가장 가까운 노드를 확정'하는 그리디예요. 우선순위 큐로 현재 최단 거리가 최소인 노드를 뽑아 인접 노드 거리를 갱신(relax)해요. 음수 간선이 있으면 그리디 가정이 깨지므로 Bellman-Ford를 써야 해요. 면접의 기본 — '양의 가중치 + 단일 출발'이면 다익스트라.
A*는 다익스트라에 휴리스틱 h(n)을 추가한 거예요. f(n) = g(n) + h(n) — g는 시작점부터의 실제 비용, h는 목표까지의 추정 비용. h가 항상 실제 거리 이하(절단 가능성, admissibility)면 A*는 최적해를 보장해요. 게다가 consistent(monotone, h(n) ≤ cost(n,m) + h(m))이면 효율까지 좋아져요.
구체 격자 예시로 admissibility를 설명하면: 그리드에서 직선(유클리드) 거리는 admissibility 있어요 — 최단 경로는 직선보다 길 수 없으니까요. 맨해튼 거리는 4방향 이동(상하좌우)에서 admissibility 있어요. 체비셰프은 8방향 이동에서 admissibility 있어요. 옥탈 거리(대각선 비용을 √2 ≈ 1.414로 둘 때)는 8방향 admissibility 있어요. 면접에서 "이 그리드에 어떤 휴리스틱?" 물으면 이 매핑을 떠올리세요.
더 빠른 변형은 양방향(Bidirectional) A*, 점프 포인트 서치(JPS, 그리드 전용), ALT(Arc Flags + Landmarks). 다익스트라도 양방향으로 짤 수 있고, 일반적으로 노드 수를 절반 수준으로 줄여요. Floyd-Warshall은 모든 쌍 최단 거리 O(V³) — 면접에서 '모든 쌍'이라고 하면 이걸 떠올리세요.
- 다익스트라: 양의 가중치 + 단일 출발, 음수 간선 불가 → Bellman-Ford
- A* admissibility: h(n) ≤ 실제 거리 → 최적해 보장, consistent면 효율까지
- 그리드 휴리스택: 4방향=맨해튼, 8방향=체비셰프, 직선 가능=유클리드
- Bidirectional A*는 평균 1/2 비용 — JPS는 그리드에서 추가 pruning
우리 코드settlement_story의 유닛 길찾기는 코스트 맵 + A*를 써요. optimal-route-planner의 인접 그래프 최적화는 양방향 A* 기반이에요.
동적 계획법 / 메모이제이션 Dynamic Programming / Memoization 우리코드
중복 부분 문제(overlapping subproblems)와 최적 부분 구조(optimal substructure)를 가진 문제를 하향식 메모이제이션 또는 상향식 타뷸레이션으로 푸는 기법. 피보나치·배낭·LCS·편집 거리가 대표 예.
DP = Dynamic Programming(동적 계획법), LCS = Longest Common Subsequence(최장 공통 부분수열)
피보나치를 예로 들면 DP가 왜 필요한지 바로 보여요. 단순 재귀 fib(n) = fib(n-1) + fib(n-2)는 n=40만 돼도 1초가 넘어요. 같은 fib(3)이 호출 수백 번 중복 계산되기 때문이에요. 메모이제이션은 '한 번 계산한 값을 캐시에 저장하고 재호출 시 반환' — n=40도 마이크로초 단위로 끝나요. 캐시 적중률 99%.
배낭 문제(0/1 Knapsack)는 DP의 전형 예시예요. 무게 w, 가치 v인 n개 물건이 있고 배낭 용량이 W일 때 가치 합 최대를 구해요. dp[i][c] = 'i번째 물건까지 봤고 용량 c일 때 최대 가치'. 전이는 i번째 물건을 안 넣으면 dp[i-1][c], 넣으면 dp[i-1][c-w[i]] + v[i]. 시간 O(n·W), 공간 O(n·W) 또는 1차원으로 줄여 O(W).
DP를 쓸 수 있는지 판단하는 두 가지 체크: (1) 중복 부분 문제 — 같은 부분 문제가 여러 경로로 계산되는가, (2) 최적 부분 구조 — 부분 문제의 최적해가 전체의 최적해를 이루는가. 둘 다 만족해야 DP, 아니면 그리디·분할정복·백트래킹을 검토.
Top-down(메모이제이션, 재귀 + 캐시)과 Bottom-up(타뷸레이션, 반복문으로 채우기) 둘 다 같은 답이에요. 트레이드오프: Top-down은 구현이 직관적이고 필요 부분만 계산, 재귀 깊이만큼 콜스택 비용. Bottom-up은 구현이 한 줄 반복문이고 호출 오버헤드 없음, 하지만 불필요한 상태도 채울 수 있음. 면접에선 Bottom-up이 표준 답이에요.
DP 변형: 비트마스크 DP(상태를 비트로 표현, n ≤ 20), Digit DP(수의 자릿수별 DP), Tree DP(트리를 DFS하며 부분트리 결과 합치기), 확률적 DP(기대값 전이).
- Top-down(메모이제이션)과 Bottom-up(타뷸레이션)의 트레이드오프 — 면접에선 Bottom-up이 표준
- 점화식 세우기: 상태 정의 → 전이 → 기저 조건 → (선택) 공간 압축
- 비트마스크 DP / Digit DP / Tree DP / 확률적 DP — 변형 다수
- 그리디로 안 풀릴 때 DP 검토 — 동전 거스름이 대표 반례
우리 코드oxibrain의 context packing이 DP적 — 남은 토큰 예산 안에서 점수 상위 chunk를 조합하는 그리디+DP 하이브리드.
탐욕 / 그리디 + 분할-정복 Greedy / Divide & Conquer 우리코드
그리디는 매 순간 지역 최적을 선택. 분할-정복은 문제를 작게 쪼개 정복 후 결합. 둘 다 최적 부분 구조가 핵심 전제.
그리디 알고리즘은 매 단계에서 '지금 가장 좋아 보이는 선택'을 하는 거예요. 동전 거스름으로 설명하면 — 동전이 [1, 5, 10, 50, 100, 500]처럼 캐노니컬(정수 배수 관계)이면 그리디가 정확히 답을 줘요. 하지만 동전이 [1, 3, 4]처럼 되면 6원을 그리디로 거슬러 주면 4+1+1 = 3개, 최적은 3+3 = 2개 — 그리디가 실패해요. 그래서 그리디는 '그리디 선택 속성(greedy choice property)'이 성립할 때만 정확해요.
그리디가 통하는 전형적인 문제: 활동 선택(interval scheduling, 종료 시간 오름차순 정렬 후 선택), 허프만 코딩(빈도 낮은 것부터 트리 합치기), 다익스트라, 크루스칼(MST). 통하지 않는 전형: 동전 거스름(비-캐노니컬), 배낭(0/1). 면접에서 "그리디로 풀 수 있는가?"는 항상 의심하고 따져야 해요.
분할-정복은 문제를 절반 이하로 쪼개 재귀 해결 후 결과를 결합해요. 합병 정렬(병합), 퀵 정렬(분할 후 재귀), Karatsuba 곱셈(중간항 한 번 계산으로 O(n^log₂3) ≈ O(n^1.585)), FFT(다항식 곱셈 O(n log n)), Strassen 행렬 곱셈(O(n^log₂7) ≈ O(n^2.807))이 대표예요.
분할-정복의 핵심 분석 도구는 마스터 정리(Master Theorem)예요. T(n) = aT(n/b) + f(n) 형태의 점화식에서 세 경우로 분기: f(n)이 n^(log_b a)보다 작으면 T = Θ(n^(log_b a)), 같으면 Θ(n^(log_b a) · log n), 크면 Θ(f(n)). 면접에서 '합병 정렬 시간 복잡도 증명해 봐' 하면 마스터 정리를 인용하면 깔끔해요.
재귀 오버헤드 vs 캐시 효율도 트레이드오프예요. 분할-정복은 재귀 호출 때문에 함수 호출 비용이 누적되지만, 작은 부분 문제는 CPU 캐시에 맞아 빠르기도 해요. 그래서 분할을 어디서 멈추고 brute-force로 폴백할지가 캐시 효율의 핵심이에요.
- 그리디 선택 속성이 성립하는지 검증 — 동전 거스름이 대표 반례([1,3,4])
- 마스터 정리로 T(n) = aT(n/b) + f(n) 점화식 풀이 — 면접 단골
- 분할-정복 트레이드오프: 재귀 오버헤드 vs 캐시 효율 — 작은 n 폴백 결정
- DP vs 그리디: DP는 모든 부분 문제 탐색, 그리디는 한 선택만 — 정확성 조건이 다름
우리 코드oxibrain의 context packing은 그리디(점수 상위 chunk 우선), oximemo의 비트 보수 키는 분할된 인덱스 정렬에 쓰여요.
TSP / 외판원 문제 + Branch & Bound TSP & Branch and Bound 자소서 직무 그림 우리코드
모든 정점을 한 번씩 방문해 시작점으로 돌아오는 최소 비용 순회. NP-hard. 완전 탐색 O(n!)은 불가능 → Branch & Bound가 하한(lbound)으로 나쁘면 가지치기.
TSP = Traveling Salesman Problem(외판원 문제), BB = Branch and Bound(분기 한정법), MST = Minimum Spanning Tree(최소 신장 트리)
TSP는 직관적으로 쉬워 보이지만 NP-hard예요. 도시 20개의 순열은 20! ≈ 2.4×10¹⁸ — 우주 age보다 큰 수. 그래서 정확해를 포기하고 근사 알고리즘(2-opt, Christofides, Lin-Kernighan)을 쓰거나, 정확해가 필요한 작은 n에 한해 Branch & Bound로 풀이해요.
Branch & Bound는 DFS 기반 백트래킹 + 하한(lbound, lower bound) 가지치기예요. 현재까지의 비용 + 앞으로의 최소 비용(하한)이 이미 발견된 최선의 해보다 크면 그 가지는 더 안 파요. 핵심은 하한을 얼마나 타이트하게 잡느냐 — 하한이 헐렁하면 가지치기가 안 돼 n!에 가깝고, 타이트하면 폭발적으로 줄어들어요.
가장 흔한 하한은 (1) 비용 행렬에서 각 행의 최솟값 두 개 합, (2) MST(최소 신장 트리) 1-tree relaxation, (3) LP relaxation(선형 완화). 면접에서 'n=12 제한'이 자주 등장하는데, BB + 좋은 하한 + LRU 캐시(중복 부분 문제 캐싱) 조합으로 12 정도가 사람의 대기 시간 내에 풀리는 현실적 상한이에요.
비대칭 TSP(A→B와 B→A 비용 다름)는 일방통행·도로·실제 물류에서 흔해요. 대칭은 1.5-근사(Christofides), 비대칭은 MST 기반이 잘 안 들어맞아요. Held-Karp DP가 O(n²·2ⁿ) — n ≤ 20까지는 정확해 보장. Concorde 솔버는 BB + LP relaxation의 산업 표준이지만, 면접에서는 'Concorde 이름'보다 'BB + LRU 직접 구현'이 보여야 해요.
- Held-Karp DP O(n²·2ⁿ) — n ≤ 20까지 정확해
- n=12 BB + LRU 캐시가 면접 한계 — '왜 12?'가 자주 출제
- 하한의 품질이 BB 성능을 좌우 — MST 1-tree, LP relaxation
- Concorde = BB + LP relaxation — 면접에선 직접 구현 설명
우리 코드optimal-route-planner가 비대칭 TSP를 Branch & Bound + LRU 캐시로 풀어요. 면접 포인트: '왜 12개 제한 — n! 폭발'.
백트래킹 / 조합 탐색 / 가지치기 Backtracking / Combinatorial Search / Pruning 그림
DFS로 후보해를 탐색하면서 조건 위반 시 즉시 포기. N-Queen·부분집합·순열 생성·CSP가 대표. 가지치기 휴리스틱이 핵심.
백트래킹은 'DFS + 가지치기'예요. 모든 조합을 다 만들어 보는 건 N-Queen에서 n=14만 돼도 불가능 — 가지치기로 대부분의 가지를 잘라내야 해요. N-Queen은 n=27까지 가지치기 + 비트마스크 최적화로 풀린 기록이 있고, 단순 백트래킹은 n=15 정도가 한계예요.
가지치기(pruning) 종류: (1) 제약 위반 가지치기 — 현재 경로가 문제 제약을 위반하면 즉시 중단. (2) bound 가지치기 — 현재까지의 비용 + 하한이 이미 최선보다 크면 중단. (3) 순방향 검사(forward checking) — 다음 선택이 제약 위반을 피할 수 있는지 미리 확인. (4) MRV(Minimum Remaining Values) — 가장 제약을 좁히는 변수부터 처리.
CSP(Constraint Satisfaction Problem) 휴리스틱 — MRV, degree heuristic(제약을 가장 많이 건드리는 변수), LCV(Least Constraining Value, 가장 자유를 덜 빼앗는 값), arc consistency(AC-3 알고리즘)는 면접보다 시험 단골이지만 기본기를 알아두면 백트래킹 설계가 달라져요.
메모리-효율적 변형: Iterative Deepening DFS(IDDFS, 깊이 제한 DFS + 점진적 깊이 증가), IDA*(DFS + f-cost 한계). BFS·DFS를 메모리 측면에서 절약하면서 완전 탐색을 보장하는 트릭이에요.
- 제약 위반·bound·forward checking 가지치기 — N-Queen이 대표 사례
- CSP 휴리스틱: MRV, LCV, arc consistency(AC-3) — 백트래킹 효율의 핵심
- IDDFS·IDA* — 메모리-효율적 백트래킹 변형
- 단순 백트래킹 n=15, 가지치기+비트마스크로 n=27까지 — N-Queen 기록 기준
문자열 매칭 (KMP / Rabin-Karp / Z 알고리즘) String Matching — KMP / Rabin-Karp / Z Algorithm 우리코드
텍스트 안에서 패턴을 찾는 알고리즘. KMP·Z는 O(N+M), Rabin-Karp는 rolling hash로 평균 O(N+M)·최악 O(NM).
KMP = Knuth-Morris-Pratt, RK = Rabin-Karp, Z = Z-algorithm
단순 매칭은 텍스트의 각 위치에서 패턴을 처음부터 비교 — 최악 O(NM). 거의 모든 위치를 비교하니까 비효율적이에요. KMP·Rabin-Karp·Z는 이미 비교한 정보를 재사용해서 O(N+M) 또는 거기에 가깝게 줄여요.
KMP는 실패 함수(failure function, prefix 함수) π[i] = '패턴[0..i]의 진짜 접두사=진짜 접미사인 최대 길이'를 미리 계산해요. 매칭 중 불일치가 일어나면 π로 점프해서 다시 비교하지 않아도 되는 위치를 알아요. 예를 들어 패턴 ABCABC에서 π = [0,0,0,1,2,3] — 6번째 C에서 실패하면 3번째로 돌아가서 이어서 비교. 시간 O(N+M), 공간 O(M).
Rabin-Karp는 해시 기반이에요. 텍스트의 길이 M인 윈도우를 rolling hash로 빠르게 갱신하면서, 해시가 일치할 때만 실제 문자 비교를 해요. 평균 O(N+M), 최악 O(NM)(해시 충돌이 계속 나면). 다중 패턴 검색이 쉬워서 plagiarism detection·diff tool에 자주 쓰여요.
Z 알고리즘도 O(N+M). Z[i] = '문자열[i..]과 문자열[0..]의 최장 공통 접두사 길이'를 계산해서 매칭. 구현이 KMP보다 직관적이고 짧아요. Suffix array·Suffix tree·FM-index는 다중·반복 패턴 검색의 표준이며, Aho-Corasick은 트라이 + KMP로 다중 패턴을 한 번에 매칭해요.
- KMP/Z O(N+M), Rabin-Karp 평균 O(N+M)·최악 O(NM) — 해시 충돌 시
- Suffix array / suffix tree / FM-index — 다중·반복 패턴 검색 표준
- Aho-Corasick = 트라이 + KMP — 다중 패턴을 한 번에 매칭
- 실무에선 대부분 라이브러리 호출이지만, 실패 함수·rolling hash 개념은 직접 구현 가능해야
우리 코드oxibrain의 n-gram 인덱스가 부분문자열 매칭 — 검색 키의 변형·오타·한글 교착어 처리에 강해요.
기하 알고리즘 / Convex Hull Computational Geometry / Convex Hull 직무 우리코드
점·선·다각형 알고리즘. Convex Hull — Graham scan O(n log n), Quickhull. 교차 판정·최단 거리·삼각 측량 등 GIS·로봇 경로의 기반.
Convex Hull은 점 집합을 감싸는 최소 볼록 다각형이에요. 점들 중 가장 바깥쪽에 있는 점들만으로 구성되고, 내부는 항상 비어 있어요. Graham scan은 가장 아래 점을 시작점으로 각도 정렬 후 스택에 넣으며 꺾이는 방향(CCW)이 되면 팝 — O(n log n). Quickhull은 분할 정복으로 평균 O(n log n), 최악 O(n²).
Convex Hull은 어디에 쓰이나 — GIS에서 시설 위치 결정(서비스 권역), 로봇 경로 계획(장애물 회피), 시각화(scatter plot 경계), 게임의 시야 판정(FOW). 시선아이티의 공간정보 업무에서 만날 가능성이 높은 알고리즘이에요.
기하 알고리즘의 기본 연산: (1) 두 선분 교차 판정(orientation + on-segment), (2) 점이 다각형 내부에 있는지(ray casting, O(n)), (3) 두 볼록 다각형 교차(Separating Axis Theorem, O(n+m)), (4) 최근접 점 쌍(분할 정복 O(n log n)). SAT는 2D 충돌 검출의 표준이에요.
삼각 측량(triangulation): 점 집합을 삼각형으로 분할 — Delaunay triangulation은 '어떤 점의 외접원도 다른 점을 포함하지 않는' 조건으로 삼각형의 품질을 최대화해요. GIS의 TIN(Triangulated Irregular Network)이 Delaunay 기반이고, 지형 표고 모델링의 표준이에요.
- Convex Hull: Graham scan O(n log n), Quickhull 평균 O(n log n)
- SAT(Separating Axis Theorem) — 2D 충돌 검출의 표준
- Delaunay triangulation — GIS TIN의 기반, 외접원 빈 조건
- 기하 함정: 부동소수점 비교는 epsilon 비교 — 0.0 직접 비교는 위험
우리 코드optimal-route-planner의 VRP 확장에서 시설 위치·차량 동선을 함께 풀면 2D 기하 결합 문제가 돼요.
확률적 자료구조 / 스킵 리스트 / Bloom Filter Probabilistic / Approximate Data Structures — Skip List / Bloom Filter 우리코드
정확성을 약간 양보해 메모리·속도에서 큰 차익. Bloom Filter(존재 부정 0% 오답, 긍정 ~1%), HyperLogLog(카디널리티 ±0.81%), Count-Min Sketch(주파수).
HLL = HyperLogLog, CMS = Count-Min Sketch, LSM = Log-Structured Merge-tree
Bloom Filter는 '특정 원소가 집합에 들어 있을 가능성이 있는가'를 답하는 비트 배열이에요. k개의 해시 함수로 k개 비트를 1로 세팅해 삽입, 조회 시 모든 비트가 1인지 확인. 거짓 긍정(false positive)은 가능하지만 거짓 부정(false negative)은 절대 없어요 — '있다'고 해도 없을 수 있지만, '없다'고 하면 진짜 없음.
쓰임새 — DB 캐시 미스 확인(없다고 하면 DB도 안 뒤져도 됨), 악성 URL 필터링, 분산 시스템의 membership check, GraphQL persisted query 화이트리스트. Redis·Bigtable·Cassandra가 표준으로 채택.
HyperLogLog(HLL)는 카디널리티(고유 원소 수)를 ±0.81% 오차로 12KB 메모리에 추정해요. Redis의 PFADD/PFCOUNT가 HLL이에요. 카운트만 필요하고 정확도가 덜 필요할 때 — 일별 UV·IP 카디널리티, 검색어 다양성 측정. Count-Min Sketch는 주파수(빈도)를 추정 — '이 URL이 몇 번 등장했는가'를 메모리 작게.
Skip List는 정렬된 데이터를 O(log n)에 검색·삽입·삭제하는 확률적 자료구조예요. 정렬된 연결 리스트의 각 노드를 확률적으로 한 단계·두 단계 위로 더 잇는 구조. 단순 연결 리스트보다 빠르고, 균형 BST보다 잠금 없는 동시성 구현이 쉬워서 LevelDB·RocksDB의 LSM-tree 메모리 컴포넌트로 쓰여요.
- Bloom Filter: 거짓 긍정 O, 거짓 부정 X — DB 캐시·악성 URL 필터링 표준
- HyperLogLog ±0.81% / 12KB — 카디널리티 추정, Count-Min Sketch는 빈도 추정
- Skip List = 정렬 + 잠금 없는 동시성 — LevelDB·RocksDB LSM 컴포넌트
- 분산 시스템: Bloom + HLL + CMS는 표준 조합 — 정확도 트레미드오프 인지
우리 코드oxicode-mnemopi의 vector 색인은 sqlite-vec·근사 최근접(ANN) — 확률적 구조의 실 사례. oxibrain-store의 메모리 인덱스도 LSM 계열이에요.
네트워크 18
웹 면접의 토대. OSI·TCP/IP로 패킷 사고를 깔고, TLS·HTTP 상태코드·CORS·JWT/OAuth로 실제 API 사고를 다룬다. SI·공공 시스템의 REST·인증·에러코드는 여기서 출발한다.
OSI 7계층 vs TCP/IP 4계층 OSI / TCP/IP 그림 우리코드
OSI 7계층(물리·데이터 링크·네트워크·전송·세션·표현·응용)은 표준 참조 모델이고, TCP/IP 4계층(링크·인터넷·전송·응용)은 실제 인터넷에서 쓰이는 구현 묶음이다.
OSI = Open Systems Interconnection(개방형 시스템 상호연결), TCP = Transmission Control Protocol(전송 제어 프로토콜), IP = Internet Protocol(인터넷 프로토콜).
OSI 7계층은 ISO가 1984년에 만든 참조 모델이에요. 1계층 물리(케이블·광케이블·전파), 2계층 데이터 링크(이더넷·MAC·ARP), 3계층 네트워크(IP·라우팅·ICMP), 4계층 전송(TCP·UDP·포트), 5계층 세션, 6계층 표현, 7계층 응용(HTTP·DNS·SMTP). 통신 문제를 어디에서 끊어야 할지 계층별로 진단할 수 있게 만든 교과서라서 면접에서 '이 에러가 어느 계층 문제냐'는 질문이 자주 나옵니다.
반면 TCP/IP 4계층은 실제로 돌아가는 인터넷 프로토콜 묶음이에요. OSI보다 단순하게 — 링크(1·2 통합), 인터넷(3), 전송(4), 응용(5·6·7 통합) — 4단계로 압축합니다. RFC 표준도 여기 계층을 따라 적혀 있어요.
둘을 매칭하면 OSI 1·2 = TCP/IP 링크, OSI 3 = 인터넷, OSI 4 = 전송, OSI 5·6·7 = 응용. 실무자는 'L4 스위치', 'L7 로드밸런서'처럼 TCP/IP 계층 번호를 주로 쓰고, 설계 문서는 OSI 이름을 빌려옵니다.
핵심은 '모델은 두 개지만 실제 패킷은 한 줄로 흐른다'는 점이에요. 면접에서 진짜 묻는 건 'DNS는 어느 계층이냐' 같은 것 — DNS는 7계층(응용)인데 3계층(IP 주소) 정보를 만들어준다는 점을 답할 줄 알아야 합니다.
- OSI는 표준 참조 모델, TCP/IP는 실 구현 패밀리 — 둘 다 외울 필요는 없지만 매핑은 필수
- Wireshark로 패킷 캡처해 헤더(L2/L3/L4)를 직접 보는 게 면접 합격 꿀팁
- L4 로드밸런서(포트 기반) vs L7 로드밸런서(HTTP 경로·헤더 기반) 구분에 OSI 계층이 직결
- 'DNS는 어느 계층?' 같은 함정 질문은 응용 계층 + IP를 만든다로 답하면 깔끔
우리 코드oxibrowser가 rustls(TLS) + h2(HTTP/2) + boa_engine(JS)를 직접 만지는데 — 4계층 위에서 응용 계층까지 한 줄로 다루는 영역이다.
TCP 3-way handshake / 4-way close TCP Connection Lifecycle 그림
TCP는 연결을 열 때 SYN → SYN-ACK → ACK 3단계를 거치고, 닫을 때는 FIN → ACK → FIN → ACK 4단계로 양방향 종료한다.
TCP = Transmission Control Protocol(전송 제어 프로토콜), SYN = Synchronize(동기화), ACK = Acknowledgment(확인 응답), FIN = Finish(종료), SEQ = Sequence Number(순서 번호), ISN = Initial Sequence Number(초기 순서 번호).
왜 3번일까? TCP는 양방향 통신이라 '서로의 초기 순서 번호를 확인하고 보낼 준비가 됐다'는 신호를 양쪽 모두 받아야 연결이 성립해요. 두 번이면 한쪽만 확인하는 거라 3번이 필요합니다. 흔히 '눈 한번 깜빡이고, 다시 깜빡이고, 받아들였다'로 외우는데 — 첫 SYN에서 클라가 ISN=x를 보내고, 서버가 SYN-ACK으로 y와 'x+1 받았음'을 동시에 보내고, 클라가 ACK 'y+1 받았음'을 보내면서 양쪽 초기 seq를 모두 동기화하죠.
구체 숫자로 보면 — 클라: SYN seq=1000, 서버: SYN-ACK seq=2000 ack=1001, 클라: ACK seq=1001 ack=2001. 이후 데이터는 seq를 1씩 늘려가며 ACK로 받음을 확인합니다. 이 ack 번호가 다음에 받을 바이트 번호라는 점이 TCP 신뢰성의 핵심이에요.
종료는 4-way close입니다. 한쪽이 FIN 보내면 그 방향으로는 더 보내지 않지만 반대 방향은 계속 받을 수 있어요 — 이게 half-close. 그래서 클라가 FIN 보내도 서버가 마지막 응답을 보낼 수 있고, 서버도 자기 FIN을 보낸 뒤 클라 ACK로 닫습니다.
TIME_WAIT은 마지막 ACK 보낸 쪽이 2MSL(약 1~4분) 동안 기다리는 상태예요. 이유는 두 가지 — 1) 서버가 ACK 못 받으면 재전송하는 FIN을 처리해야 하니 그 시간 동안 소켓을 잡아둬야 하고, 2) 같은 (src_ip, src_port, dst_ip, dst_port) 4-tuple이 같은 seq로 새 연결을 열면 이전 패킷 지연 도착이 새 연결 데이터로 섞일 수 있으니 그것을 막기 위해서입니다. 대량 connection·L4 LB 환경에서 TIME_WAIT 폭증은 흔한 운영 이슈예요.
- SYN/SYN-ACK/ACK는 양쪽 ISN을 동기화하기 위한 최소 3-step — 2-step이면 한쪽만 확인
- SYN flood 방어는 SYN cookie(서버가 ISN에 상태 인코딩)로 half-open 자원 고갈 차단
- Half-close(서로 다른 방향 종료) 지원이 FTP처럼 양방향 끝맺음이 필요한 프로토콜의 전제
- TIME_WAIT 2MSL은 지연 패킷 흡수 + ACK 재전송 흡수 — 단축 기법은 SO_LINGER + TW_REUSE
- Selective ACK(SACK)와 Nagle/Delayed ACK은 작은 패킷 효율 ↔ latency 트레이드오프
UDP vs TCP / 혼잡 제어 UDP / Congestion Control 그림
UDP는 비연결·순서 미보장·전송 미보장. TCP는 흐름·혼잡 제어(CUBIC·BBR) 포함. 실시간 미디어·게임은 UDP + 상위 보정이 표준이다.
UDP = User Datagram Protocol(사용자 데이터그램 프로토콜), TCP = Transmission Control Protocol(전송 제어 프로토콜), RTT = Round-Trip Time(왕복 지연), BBR = Bottleneck Bandwidth and Round-trip propagation time(병목 대역폭·왕복시간 기반 혼잡 제어), CUBIC = TCP의 손실 기반 혼잡 제어 알고리즘.
UDP는 그냥 '데이터그램을 쏘는' 프로토콜이에요. 핸드셰이크도 없고 재전송도 없고 순서도 안 맞춰줍니다. 대신 1패킷이 헤더 8바이트로 끝나서 오버헤드가 거의 없고 지연도 일정해요. 그래서 VoIP·실시간 게임·DNS·QUIC처럼 '지연 일정성'이 '완벽한 전달'보다 중요한 경우에 쓰입니다.
TCP는 그 반대 — 핸드셰이크로 시작하고, 슬라이딩 윈도우로 흐름 제어, 혼잡 제어로 네트워크 보호, ACK로 손실 감지하고 재전송. 신뢰성을 주되 성능을 깎는 대신 '네트워크 망이 망가지지 않게' 자기를 조율합니다. 면접에서 '왜 UDP만 가지고는 안전한가'는 답 — '응용 계층에서 알아서 재전송·순서 맞추면 TCP보다 빠르게 가능'.
혼잡 제어 알고리즘은 TCP가 망을 살리는 핵심이에요. CUBIC은 손실 기반 — 패킷 유실이 감지되면 윈도우를 줄이고, 안 나면 3차 함수로 늘려요. RTT·패킷 손실과 직접 연관된 단순 모델. 반면 BBR(Bottleneck Bandwidth and Round-trip propagation time)은 Google이 제안한 모델 기반 — 병목 대역폭과 RTT를 능동 측정해 큐잉이 쌓이지 않는 최적점에서 운영합니다. BBR은 손실이 없어도 큐 점유를 줄여 latency를 낮춰요.
QUIC은 이 흐름의 정점이에요. TCP의 신뢰성·흐름제어 + TLS 1.3 + 다중 스트림을 UDP 위에 다시 구현한 프로토콜로, HTTP/3가 이걸 씁니다. TCP HOL(Head-of-Line) blocking — 한 패킷 손실에 뒤 패킷 전부 막힘 — 을 독립 스트림으로 해결했죠. 면접에서 '왜 HTTP/3를 UDP 위에?'에 답할 수 있어야 해요.
- UDP는 비연결·순서·전송 모두 미보장 — 대신 지연 일정성과 8바이트 헤더로 가볍다
- BBR vs CUBIC: 손실 기반 줄이기 vs 큐 점유 기반 줄이기 — BBR이 latency 친화적
- QUIC은 TCP + TLS 1.3 + 다중 스트림을 UDP 위에 재구현 → HTTP/3의 토대
- 실시간 미디어·게임은 UDP + FEC(순방향 오류 정정) + jitter buffer로 '거의 실시간' 보장
TLS 1.2 vs TLS 1.3 / 핸드셰이크 TLS Versions 그림 우리코드
TLS 1.3은 핸드셰이크를 1-RTT로 단축하고, RSA 키 교환을 제거해 전방향 비밀성을 기본 보장한다.
TLS = Transport Layer Security(전송 계층 보안), RSA = Rivest–Shamir–Adleman(공개키 암호 알고리즘), ECDHE = Elliptic Curve Diffie-Hellman Ephemeral(타원곡선 임시 Diffie-Hellman 키 교환), PFS = Perfect Forward Secrecy(완전 전방향 비밀성), 0-RTT = Zero Round-Trip Time(왕복 0회 핸드셰이크), 1-RTT = One Round-Trip Time(왕복 1회 핸드셰이크), mTLS = Mutual TLS(상호 TLS 인증).
HTTPS의 'S'가 TLS예요. 평문 HTTP와 평문 위에 암호화 채널을 씌우는 차이죠. 면접에서 가장 많이 받는 질문이 '대칭키와 비대칭키를 왜 같이 쓰냐' — 단순히 안전/빠른 이유만이 아니라 역할 분담 때문입니다. 비대칭키(예: RSA·X25519)는 키를 안전하게 '교환'하는 데 쓰고, 대칭키(AES-GCM·ChaCha20)는 그 후 대량 본문을 빠르게 '암호화'하는 데 씁니다. 비대칭키는 연산 비용이 비싸서 본문 전체를 암호화하면 100배~1000배 느려지거든요.
TLS 1.2 핸드셰이크는 2-RTT였어요. ClientHello → ServerHello + 인증서 → ClientKeyExchange → Finished. 인증서에 RSA로 키를 암호화해 보내는 방식이라 한쪽 비공개키가 털리면 과거 세션 키까지 전부 복호화되는 약점이 있었습니다 — 이게 forward secrecy 미보장.
TLS 1.3은 이걸 ECDHE로 바꿔서 매 세션마다 임시 키 쌍을 만들어요. 서버 비공개키가 털려도 그건 현재·미래 세션만 노출하고 과거 세션 키는 못 복원 — 이게 forward secrecy. 핸드셰이크도 키 교환을 ClientHello 안에 Diffie-Hellman 값으로 끼워넣어 1-RTT로 끝납니다. 0-RTT는 재방문 클라이언트가 PSK(Pre-Shared Key)를 들고 와서 핸드셰이크 없이 바로 데이터 보내는 모드인데 replay 공격 위험이 있어서 idempotent(멱등) API에만 씁니다.
인증서 신뢰는 CA 체인(X.509) — 서버 인증서가 중간 CA → 루트 CA까지 신뢰 가능한 루트로 올라가는 구조. 면접 단골은 'mTLS', 'OCSP stapling(서버가 인증서 폐기 상태를 미리 가져다 붙임)', 'Certificate Transparency(인증서 투명 로그)'예요.
- 대칭키(빠른 본문) + 비대칭키(안전한 키 교환) — 역할 분담이 핵심
- TLS 1.3은 1-RTT, ECDHE로 forward secrecy 기본 보장, RSA 키교환 제거
- 0-RTT는 replay 위험 — 멱등(idempotent) API에만 허용
- mTLS는 서버↔클라이언트 상호 인증 — 내부 마이크로서비스·공공 SI 표준
- 인증서 검증은 신뢰 체인(CA) + 폐기(CRL/OCSP stapling) + 투명성(CT 로그)
우리 코드oxibrowser가 rustls의 TLS 핑거프린트를 Chrome 149 프로필과 정합되게 맞추는데 — TLS 1.3 ClientHello의 확장(ALPN·supported_groups·signature_algorithms) 순서까지 일치시켜야 봇 탐지를 피한다.
HTTP 메서드 (GET · POST · PUT · PATCH · DELETE) HTTP Methods 직무 그림 우리코드
HTTP 메서드는 '이 요청으로 뭘 하고 싶은가'를 서버에 알리는 약속. GET(조회), POST(생성), PUT(전체 교체), PATCH(부분 수정), DELETE(삭제).
HTTP = HyperText Transfer Protocol(하이퍼텍스트 전송 프로토콜). GET/POST/PUT/PATCH/DELETE = 대표 요청 메서드 5종.
면접에서 'GET과 POST 차이'를 물어보면 리소스 관점으로 답하는 게 정석이에요. GET은 있는 자원을 '읽는' 요청 — 본문 없이 URL만으로도 의미가 통해야 하고, POST는 서버에 '새 상태를 만들어달라'는 요청 — 생성·처리·상태 변경을 다룹니다. 그래서 GET은 URL에 민감한 정보(비밀번호 등)를 넣으면 안 되고(로그·히스토리에 남음), POST 본문은 그 자체로 처리 대상이에요.
PUT과 PATCH의 차이는 '전체냐 부분이냐'예요. PUT /users/42에 {name}만 보내면 그 외 필드는 빠진 값(없음·기본값)으로 취급돼 자원 전체가 교체됩니다. PATCH는 '이 필드만 바꿔줘'라는 부분 수정이에요. 실무에서 폼 수정 API는 대부분 PATCH, 동기화·멱등 재시도가 중요한 배치성 갱신은 PUT이 자주 쓰입니다.
각 메서드의 성격을 정확히 구분하는 게 면접 포인트예요. GET은 안전(safe) + 멱등(idempotent) — 호출해도 서버 상태가 안 바뀌고, 몇 번을 불러도 결과가 같아야 해요. POST는 비멱등 — 같은 요청을 두 번 보내면 두 개의 자원이 생깁니다. PUT·DELETE는 멱등(반복해도 같은 상태로 수렴). PATCH는 구현에 따라 비멱등인 경우가 많아요. '멱등' 자체는 별도 용어로 정리되어 있으니 그쪽을 같이 보세요.
시선아이티 맥락에서는 내부 SI 시스템 연동이 핵심이에요. 챗봇이 내부 시스템의 조회·등록·수정을 대행할 때 결국 GET/POST/PATCH/DELETE 호출로 번역되니까, '어떤 행위에 어떤 메서드가 맞는지' 판단 자체가 연동 설계의 일부입니다.
- GET(조회)·POST(생성/처리)·PUT(전체 교체)·PATCH(부분 수정)·DELETE(삭제)
- GET·HEAD는 안전(safe) — 서버 상태를 안 바꿈. POST는 유일한 '비안전·비멱등' 기본 메서드
- PUT은 전체 교체라 빠진 필드가 날아갈 수 있음 — 부분 수정은 PATCH
- 민감 정보는 GET 쿼리에 넣지 말 것 — 로그·히스토리·Referer에 남는다
우리 코드oxibrain-mcp의 serve_http가 REST 노출 — JSON-RPC가 아닌 HTTP API를 직접 제공하면서 자원과 메서드를 분리한다. 시선아이티 자격요건 'API 설계·연동원리' 영역과 직결.
커리큘럼 체인 웹 API 기본기
멱등성 / 안전성 Idempotency & Safety 직무
멱등은 '같은 요청을 N번 보내도 결과가 한 번 보낸 것과 같은' 성질, 안전은 '호출 자체가 서버 상태를 바꾸지 않는' 성질. 재시도·분산 시스템 설계의 근거가 된다.
Idempotent = 멱등(같은 요청 반복 → 같은 결과), Safe = 안전(호출이 서버 상태를 바꾸지 않음), Idempotency-Key = 클라가 붙이는 요청 유일 식별 헤더.
왜 멱등성이 중요한가 — 네트워크는 실패해요. 타임아웃이 났을 때 클라이언트는 '요청이 안 간 건지, 응답만 못 받은 건지' 모릅니다. 이때 재시도가 안전한 메서드(GET·PUT·DELETE)는 그냥 다시 보내면 되지만 POST는 중복 생성 위험이 있어요. 이 판단 기준이 멱등성입니다.
멱등 vs 안전을 구분하면 — 안전(safe)은 상태 변경 자체가 없음(GET·HEAD), 멱등(idempotent)은 변경은 있을 수 있으나 반복 결과가 같음(PUT·DELETE). 모든 안전한 메서드는 멱등이지만 역은 성립하지 않아요(DELETE는 상태를 바꾸지만 멱등).
비멱등 연산(POST 결제·송금)을 안전하게 만드는 실무 패턴이 Idempotency-Key예요. 클라이언트가 요청마다 유일 키를 헤더로 붙이면, 서버는 같은 키의 재요청을 '새 요청'이 아니라 '이전 결과 재전달'로 처리합니다. Stripe 같은 결제 API가 이 패턴의 대표 사례예요.
면접 답안 한 줄: '멱등은 재시도 안전성의 근거 — GET·PUT·DELETE는 멱등이라 재시도 자유, POST는 비멱등이라 Idempotency-Key나 중복 체크가 필요하다.'
- 멱등: 같은 요청 N번 → 결과 동일. GET·PUT·DELETE 멱등, POST 비멱등
- 안전: 호출이 상태 불변 — GET·HEAD만. 안전 ⊂ 멱등
- 비멱등 POST의 재시도 안전장치 = Idempotency-Key 헤더
- 분산 시스템에서 '재시도해도 되는가'의 판정 기준이 바로 멱등성
커리큘럼 체인 웹 API 기본기
REST / RESTful API 설계 REST / RESTful API 직무 우리코드
REST는 Roy Fielding이 정의한 아키텍처 스타일 — 자원을 URI로 식별하고, 행위를 HTTP 메서드로 표현하며, 무상태로 통신하는 제약 조건의 집합. 'RESTful API가 뭐냐'는 질문에 이 한 줄로 답한다.
REST = Representational State Transfer(표현 상태 전이), URI = Uniform Resource Identifier(통합 자원 식별자), HATEOAS = Hypermedia As The Engine Of Application State(링크로 다음 동작 안내).
'RESTful API가 뭔가요?'라는 면접 질문의 모범 답은 세 문장이에요. (1) 자원을 URI로 식별한다(`/users/42`). (2) 행위를 HTTP 메서드로 표현한다(GET 조회·POST 생성·PUT/PATCH 수정·DELETE 삭제). (3) 무상태(stateless)라 서버가 클라이언트 상태를 기억하지 않고, 모든 요청이 자기 완결적이다. 여기에 응답 상태코드와 표현(JSON)이 자기서술적이면 RESTful하다고 말합니다.
REST는 '규칙'이 아니라 '제약 조건의 집합'이에요. Fielding 박사논문에서 정의한 6가지 — 클라이언트-서버, 무상태, 캐시 가능, 균일 인터페이스, 계층화, 코드 온디맨드(선택). 이 중 '균일 인터페이스'가 실무를 좌우하는데, 자원 식별(URI)·표현으로 조작(HTTP 메서드)·자기서술적 메시지(HTTP 헤더·상태코드)·HATEOAS(링크로 다음 동작 안내)가 그 4가지 하위 제약이에요.
자원 설계는 복수 명사 URI가 표준이에요. `/users/42/orders/1001/items`처럼 계층을 만들고, 행위는 쿼리 파라미터(`?status=active&sort=-created_at`)로 표현합니다. 컬렉션 자체가 자원이면 `POST /users` (생성), 개별 자원이면 `GET/PUT/PATCH/DELETE /users/42`. 동사 URI(`/users/42/get`)는 REST 답지 않아요.
오류 응답이 REST의 진가입니다. 적절한 상태코드(404·409·422·429) + machine-readable 에러 본문 — `{ "code": "USER_DUPLICATE", "message": "이미 존재", "field": "email" }` 같은 형태가 클라이언트의 분기를 가능하게 해요. 단순히 200 + 에러 문자열을 반환하는 API는 안티패턴.
- 한 줄 정의: 자원=URI, 행위=HTTP 메서드, 무상태 통신의 제약 조건 집합
- REST는 '규칙'이 아니라 '제약 조건의 집합' — Fielding 박사논문 6가지
- 자원 URI는 복수 명사, 동사 URI(`/users/get`)는 REST 답지 않음
- 상태코드 + machine-readable 에러 본문(`{code,message,field}`)이 REST의 진가
우리 코드oxibrain-mcp의 serve_http가 REST 노출 — JSON-RPC가 아닌 HTTP API를 직접 제공하면서 자원과 메서드를 분리한다. 시선아이티 자격요건 'API 설계·연동원리' 영역과 직결.
커리큘럼 체인 웹 API 기본기
HTTP 상태 코드 (2xx/3xx/4xx/5xx) HTTP Status Codes 직무 표 그림
2xx 성공, 3xx 리다이렉트, 4xx 클라이언트 오류, 5xx 서버 오류. 클라이언트가 다시 시도할 가치가 있는지가 4xx와 5xx의 구분 기준이다.
HTTP = HyperText Transfer Protocol(하이퍼텍스트 전송 프로토콜). 2xx = Success(성공), 3xx = Redirection(리다이렉트), 4xx = Client Error(클라이언트 오류), 5xx = Server Error(서버 오류).
상태코드는 RFC 9110에서 정의된 약속이에요. 첫 자리로만 대분류가 결정됩니다. 1xx는 정보(거의 안 씀), 2xx 성공, 3xx 추가 동작 필요(리다이렉트), 4xx 요청 측 잘못, 5xx 서버 측 잘못. 면접에서 '왜 4xx와 5xx를 구분하나'를 물으면 — '클라이언트가 같은 요청을 다시 보내도 달라지는가'로 답하면 됩니다. 4xx는 그대로 재시도해도 똑같이 실패하니 클라가 고쳐야 하고, 5xx는 서버가 일시적 문제일 수 있으니 재시도가 의미 있을 수 있어요.
자주 헷갈리는 쌍을 짚을게요. 401 vs 403 — 401 Unauthorized는 '누구세요? 인증 정보가 없거나 잘못됐어요. 다시 인증하세요'를 뜻하고 WWW-Authenticate 헤더로 어떤 인증 방식을 요구하는지 알려줘야 해요. 403 Forbidden은 '인증은 됐는데 그걸 할 권한은 없어요'를 뜻합니다. 로그인했지만 관리자 페이지를 보는 거죠. 404로 권한 없음을 숨기는 건 보안 패턴(존재 여부 노출 회피)이지만 정확한 의미론적 답은 403입니다.
301 vs 302 vs 308 — 이 셋이 면접 단골이에요. 301 Moved Permanently는 영구 이전 — 브라우저가 GET으로 다운그레이드할 수 있다는 함정이 있습니다(원래 POST였다면). 그래서 RFC가 명확히 POST를 유지하길 원하면 308 Permanent Redirect를 씁니다. 302 Found는 임시 — 원래 의미는 POST가 GET으로 변환되는 임시 리다이렉트였고(그래서 'Found'라는 이름), 브라우저들이 이걸 GET으로 바꿔 보내요. 307 Temporary Redirect는 메서드를 그대로 유지하는 임시 리다이렉트. 정리하면: 영구 + 메서드 유지 = 308, 영구 + 메서드 변경 허용 = 301, 임시 + 메서드 유지 = 307, 임시 + 메서드 변경 = 302.
504 Bad Gateway와 502의 차이도 실무에서 자주 봐요. 502는 업스트림이 잘못된 응답을 줬을 때, 503 Service Unavailable은 서버가 일부러 점검·과부하로 응답을 거부할 때(Retry-After 헤더 권장), 504 Gateway Timeout은 업스트림이 시간 안에 응답을 안 줬을 때 — BFF·API Gateway 환경에서 가장 흔합니다.
| 구분 | 특징 | 참고 |
|---|---|---|
| 200 OK | 정상 응답, 본문 포함 | GET·PUT 성공 |
| 201 Created | 새 자원 생성 | POST 결과 자원 URI 명시 |
| 204 No Content | 성공이지만 본문 없음 | DELETE 결과 |
| 301 Moved Permanently | 영구 리다이렉트 | 메서드 변경 가능 — 주의 |
| 302 Found | 임시 리다이렉트 | POST → GET 변환(원래 의미) |
| 304 Not Modified | 캐시 유효 | If-None-Match / If-Modified-Since |
| 307 Temporary Redirect | 임시 리다이렉트(메서드 유지) | POST도 POST로 |
| 308 Permanent Redirect | 영구 리다이렉트(메서드 유지) | POST도 POST로 |
| 400 Bad Request | 요청 문법 오류 | 파싱 실패 |
| 401 Unauthorized | 인증 필요 | WWW-Authenticate 헤더 |
| 403 Forbidden | 권한 없음 | 인증은 됐으나 거부 |
| 404 Not Found | 리소스 없음 | URL 오타 또는 권한 숨김용 |
| 409 Conflict | 현재 상태와 충돌 | 중복·동시 수정 |
| 422 Unprocessable Entity | 형식 OK·내용 오류 | REST API에서 권장 |
| 429 Too Many Requests | 속도 제한 초과 | Retry-After 헤더 |
| 500 Internal Server Error | 서버 일반 오류 | 스택트레이스 노출 X |
| 502 Bad Gateway | 업스트림 잘못된 응답 | 리버스 프록시 |
| 503 Service Unavailable | 일시적 과부하·점검 | Retry-After 권장 |
| 504 Gateway Timeout | 업스트림 응답 시간 초과 | BFF/edge pattern |
- 4xx vs 5xx = 클라가 다시 해볼 가치가 있느냐로 구분 — 같은 요청 재시도 가능성
- 401(인증 필요·WWW-Authenticate) vs 403(인증 됐지만 권한 없음) 차이는 면접 단골
- 301/302/307/308 — 영구/임시 × 메서드유지/변경의 2×2 조합으로 기억
- 408·413·418·422·429는 실무 단골 — 특히 422는 REST에서 권장
- 502 vs 503 vs 504 — 게이트웨이 환경에서 의미가 자주 섞인다
커리큘럼 체인 웹 API 기본기
쿠키 / 세션 인증 Cookie & Session Auth 직무 그림 우리코드
쿠키 기반 세션 인증은 로그인 시 서버가 세션 ID를 만들고(상태는 서버에 저장) 브라우저엔 키만 쿠키로 내려, 매 요청 자동 첨부로 사용자를 식별하는 방식이다.
Cookie = 서버가 Set-Cookie로 내려주는 브라우저 저장 키-값, Session = 서버 측 사용자 상태 저장소, HttpOnly/Secure/SameSite = 쿠키 보안 속성 3종.
흐름은 이렇게 돼요. 사용자가 로그인하면 서버가 세션 ID를 만들어 메모리/DB(Redis 등)에 저장하고 Set-Cookie로 브라우저에 내려요. 이후 모든 요청에 브라우저가 자동으로 쿠키를 동봉하니 서버는 ID만 보고 '아 그 사용자구나'를 식별합니다.
장점은 상태가 서버에 있다는 것 — 강제 로그아웃·권한 변경이 즉시 반영돼요. 단점은 서버 저장 비용이 사용자 수에 비례하고, 서버를 여러 대 두면 세션 저장소를 Redis처럼 중앙에 둬야 한다는 겁니다(세션 스티키니스 회피).
쿠키 보안 속성 3종은 면접 단골이에요. HttpOnly — JS(document.cookie)로 읽지 못하게 해서 XSS 탈취 차단. Secure — HTTPS로만 전송. SameSite=Lax/Strict — 다른 사이트의 요청에 쿠키를 안 실어서 CSRF 차단. 공공 SI는 감사 추적이 명확한 세션 + RBAC 조합이 여전히 표준입니다.
- 세션 = 서버에 상태 저장, 브라우저엔 ID만 — 강제 로그아웃 즉시 가능
- 서버 확장 시 세션 저장소 중앙화(Redis) 또는 stateless(JWT) 전환
- HttpOnly(XSS 차단)·Secure(HTTPS 전용)·SameSite(CSRF 차단) 3속성
- 공공 SI는 세션 + RBAC가 여전히 표준 — 감사 추적이 명확해서
우리 코드oxibrain의 scope 기반 멀티테넌트 토큰 + oxios RFC-014 OWASP RBAC + AuditTrail — 세션·토큰 인증과 RBAC이 한 묶음으로 운영된다.
커리큘럼 체인 웹 API 기본기
JWT (JSON Web Token) JSON Web Token 직무 우리코드
JWT는 서버가 서명한 토큰에 사용자 상태(claim)를 담아 클라이언트가 들고 다니는 무상태 인증 방식. 수평 확장에 유리하지만 '폐기 불가'가 최대 함정이다.
JWT = JSON Web Token(서명된 JSON 토큰), Header.Payload.Signature 3부분 구조. access/refresh token = 만료 주기 분리 2토큰 패턴.
JWT는 서버 상태 문제를 뒤집은 거예요. 로그인 시 서버가 서명된 토큰을 만들어 클라이언트에 내려요. 클라이언트는 그걸 Authorization: Bearer 헤더에 담아 보내고, 서버는 서명만 검증하면 됩니다 — DB 조회 없이 '내가 발급한 토큰임'을 증명하니까요. stateless라 수평 확장에 강하고 모바일·SPA에서 토큰만 들고 다니면 돼서 자연스럽습니다.
구조는 Header.Payload.Signature 3부분을 '.'으로 이은 형태. Payload에 sub(사용자 ID)·exp(만료)·role 같은 claim이 들어가요. 주의 — 서명은 '위변조 방지'지 '암호화'가 아니에요. Payload는 base64url이라 누구나 디코딩해서 볼 수 있으니 민감 정보는 넣지 않습니다.
'폐기 불가'가 큰 함정이에요. 만료(exp)를 짧게 두고 refresh token으로 갱신하는 패턴이 표준입니다 — access token(15분~1시간) + refresh token(7~30일). access가 만료되면 클라가 refresh를 보내고 서버가 새 access를 내려줘요. 강제 로그아웃·비밀번호 변경 시에는 refresh token을 서버 측 블랙리스트/버전 번호로 무효화합니다. 완전 stateless는 포기하되 부하를 줄이는 절충안이에요.
- 서명 검증만으로 인증 — 서버 조회 불필요, 수평 확장 유리
- Payload는 디코딩 가능 — 서명 ≠ 암호화, 민감 정보 금지
- 폐기 불가 함정: 짧은 access + refresh token + 서버 측 블랙리스트
- 세션 vs JWT = 강제 폐기·감사 용이 vs 무상태 확장성의 트레이드오프
우리 코드oxibrain의 scope 기반 멀티테넌트 토큰이 JWT 패턴 — 토큰 자체에 scope·만료를 담아 stateless 검증을 구현한다.
커리큘럼 체인 웹 API 기본기
OAuth 2.0 / OIDC OAuth 2.0 / OpenID Connect 직무 우리코드
OAuth 2.0은 비밀번호를 주지 않고도 제3자 앱에 내 자원 접근 권한을 위임하는 프레임워크. OIDC는 그 위에 '사용자가 누구인지'(ID Token)를 얹어 SSO·소셜 로그인의 표준이 됐다.
OAuth = Open Authorization(개방형 권한 위임), OIDC = OpenID Connect(OAuth 2.0 위의 신원 인증 표준), PKCE = Proof Key for Code Exchange(코드 교환 키 증명), SSO = Single Sign-On(단일 로그인).
OAuth 2.0은 이름 그대로 Open Authorization(개방형 권한 위임) — '비밀번호를 안 주고도 내 자원에 접근하게 해주는' 프레임워크예요. 자원 소유자가 인증 서버(Google·Naver)에서 권한을 부여하면 클라이언트(앱)가 access token을 받아 자원 서버에 접근합니다. '구글 계정으로 로그인'의 뒤에서 도는 표준이 바로 이거예요.
4가지 grant(권한 부여 방식): Authorization Code(웹 서버, 가장 안전), Implicit(브라우저 SPA, 이제 권장 안 함), Client Credentials(서버↔서버), Password(자사 시스템 한정). 면접에서 '가장 안전한 grant'를 물으면 Authorization Code + PKCE입니다.
OIDC(OpenID Connect)는 OAuth 2.0 위에 ID Token(JWT, 사용자 신원 정보)을 얹은 표준이에요. OAuth가 '권한 위임(뭘 해도 되는가)'이라면 OIDC는 '이 사용자가 누구인지'까지 표준화해서 SSO·소셜 로그인의 사실상 표준이 됐습니다. PKCE는 모바일·SPA 같은 public client에서 Authorization Code를 가로채도 코드를 못 쓰게 하는 추가 방어예요 — `code_verifier` 해시로 인증 코드를 묶습니다.
- OAuth 2.0 = 권한 위임(Open Authorization), 비밀번호 공유 없이 자원 접근 허용
- OIDC = OAuth + ID Token — '누구인지'까지 표준화, SSO·소셜 로그인 표준
- grant 4종 중 Authorization Code가 표준, public client는 + PKCE
- Implicit는 폐기 수순 — SPA도 Authorization Code + PKCE로 이동
우리 코드oxibrain-mcp의 serve_http·serve_socket_auth가 토큰 기반 인가 노출 — OAuth의 '자원 서버가 토큰을 검증' 모델과 같은 구조.
커리큘럼 체인 웹 API 기본기
SOP / CORS (출처 정책) Same-Origin Policy & CORS 직무 그림 우리코드
SOP는 브라우저가 다른 출처의 응답 읽기를 막는 기본 보안 정책, CORS는 서버가 헤더로 그 예외를 허용하는 선언. 둘은 한 쌍이다.
SOP = Same-Origin Policy(동일 출처 정책), CORS = Cross-Origin Resource Sharing(교차 출처 자원 공유), Preflight = OPTIONS 사전 요청, Origin = 프로토콜+호스트+포트.
SOP(Same-Origin Policy)는 브라우저의 기본 보안 모델이에요. 출처(origin) = 프로토콜 + 호스트 + 포트가 모두 같아야 그 페이지의 DOM·쿠키·JS 결과에 접근 가능. https://a.com에서 https://b.com의 fetch 결과를 읽으려고 하면 브라우저가 막습니다. '다른 출처에서 가져온 건 보여줘도(렌더링) 읽지 못한다'는 원칙이에요.
CORS는 이걸 풀기 위한 서버 측 선언이에요. b.com이 Access-Control-Allow-Origin: https://a.com 헤더를 응답에 붙이면 SOP 예외로 a.com이 결과를 읽을 수 있게 됩니다. 와일드카드(*)는 인증 정보 없이 단순 GET일 때만 허용되고, credentials(쿠키 등) 동봉 시엔 출처를 명시해야 해요.
여기서 많이 헷갈리는 것 — 'CORS는 서버 간 호출엔 무관하다'가 정답입니다. CORS는 브라우저 보안 정책이지 네트워크 정책이 아니에요. 서버에서 서버로 curl·requests·SDK로 부르는 건 브라우저가 없으니 CORS 검사 자체가 발생하지 않습니다. 'API 서버에서 CORS 설정을 안 했어'라는 답변을 들었을 때, 실제 호출 주체가 브라우저면 CORS가 문제고 서버/CLI/SDK면 다른 문제(인증·방화벽)인 거예요.
프리플라이트는 OPTIONS 사전 요청이에요. '단순 요청' 조건 — 메서드가 GET/HEAD/POST 중 하나, 헤더가 Accept·Accept-Language·Content-Language·Content-Type(application/x-www-form-urlencoded·multipart/form-data·text/plain만) — 을 하나라도 벗어나면 브라우저가 OPTIONS를 먼저 보내 서버가 허용하는 메서드·헤더·출처를 확인합니다. POST + application/json + X-Custom-Header 조합이 프리플라이트를 트리거하는 흔한 예. 서버는 OPTIONS에 204로 응답해야 해요.
- SOP는 브라우저 기본 정책, CORS는 서버가 SOP 예외를 선언 — 둘은 한 쌍
- CORS는 브라우저 정책이지 서버-서버 호출엔 무관 — curl·SDK 호출엔 검사 자체 없음
- 프리플라이트 = OPTIONS 사전 요청, 단순 요청 조건을 벗어나면 자동 트리거
- credentials 동봉 시 Access-Control-Allow-Origin: * 불가 — 출처 명시 필수
우리 코드oxios Surface(web)가 CORS 허용 목록을 설정으로 관리 — 챗봇 웹 프론트가 별도 도메인일 때 이 정책이 그대로 적용된다.
커리큘럼 체인 웹 API 기본기
웹 보안 공격과 방어 (XSS · CSRF · CSP) Web Security — XSS / CSRF / CSP 직무 우리코드
XSS는 악성 스크립트 주입으로 세션·입력을 탈취하는 공격, CSRF는 로그인된 사용자의 권한으로 위조 요청을 보내는 공격. 방어는 이스케이프 + CSP, SameSite 쿠키 + CSRF token이다.
XSS = Cross-Site Scripting(사이트 간 스크립팅), CSRF = Cross-Site Request Forgery(사이트 간 요청 위조), CSP = Content Security Policy(콘텐츠 보안 정책), HSTS = HTTP Strict Transport Security(HTTP 강제 보안).
XSS는 Stored/Reflected/DOM 3가지예요. Stored는 DB에 저장된 악성 스크립트가 그 페이지를 보는 모든 사용자에게 실행(게시판 댓글 등), Reflected는 URL 파라미터가 즉시 반사(검색 결과 등), DOM은 클라이언트 JS가 innerHTML처럼 unsafe하게 처리할 때 발생. 공통 방어는 출력 이스케이프 + CSP.
CSRF는 '사용자가 로그인한 상태에서 악성 사이트가 사용자 권한으로 요청을 보내는' 공격이에요. 은행에 로그인한 채 악성 페이지를 열면 그 페이지가 내 쿠키를 실어서 송금 요청을 보내는 식. 방어 3축은 SameSite=Lax/Strict 쿠키(다른 사이트 요청에 쿠키 미첨부), CSRF token(폼·헤더에 서버 발급 토큰 동봉), Idempotency-Key.
CSP는 Content-Security-Policy 헤더로 인라인 스크립트·외부 출처를 화이트리스트로 제한해 XSS의 '영향'을 줄이는 정책이에요. `script-src 'self'`처럼 자기 도메인 스크립트만 허용하면 인라인 악성 스크립트가 실행조차 못 합니다. HSTS는 HTTP 접속 자체를 HTTPS로 강제하는 헤더.
SI 면접 관점: 공공 시스템은 사용자 입력(민원·게시판)이 많아서 Stored XSS가 최대 위협이고, 쿠키 기반 세션이라 CSRF 방어도 필수예요. '입력 검증 + 출력 이스케이프 + CSP + SameSite'를 한 묶음으로 답하면 됩니다.
- XSS 3종: Stored(DB 저장)·Reflected(반사)·DOM(클라 JS) — 출력 이스케이프 + CSP로 방어
- CSRF = 로그인 상태 권한 도용 — SameSite 쿠키 + CSRF token + Idempotency-Key
- CSP는 화이트리스트 정책으로 XSS의 실행 자체를 차단
- XSS는 '읽기(탈취)', CSRF는 '쓰기(위조 요청)' 공격이라는 구분이 핵심
우리 코드oxios AuditTrail이 모든 보안 이벤트를 기록하고, oxibrain·oxios가 보안 헤더와 세션 운영 가이드를 내장한다 — XSS 차단용 CSP와 SameSite 쿠키 기본값을 권장한다.
커리큘럼 체인 웹 API 기본기
REST API 페이징 / 페이지네이션 API Pagination 직무 표 그림 우리코드
오프셋 페이지네이션은 LIMIT/OFFSET — 직관적이지만 큰 OFFSET에서 성능 저하. 커서 페이지네이션은 마지막 키를 다음 쿼리에 넘겨 일관성과 속도가 좋다.
API = Application Programming Interface(응용 프로그램 인터페이스), REST = Representational State Transfer(표현 상태 전이), SQL = Structured Query Language(구조화 질의어), OFFSET/LIMIT = SQL 페이징 절.
왜 페이지네이션이 어려운가 — 100만 건 테이블에서 `LIMIT 20 OFFSET 999980`을 날리면 DB는 앞 999980행을 스캔하고 버려야 해요. 깊은 페이지로 갈수록 비용이 선형으로 증가합니다. 사용자 페이지 점프(예: 5·26·37페이지) 같은 UX엔 어쩔 수 없이 offset이 쓰이지만, 무한 스크롤·실시간 피드엔 커서가 정답입니다.
Offset 페이징은 쉽고 단순해요. `SELECT ... ORDER BY id LIMIT 20 OFFSET 0` — 클라이언트는 page=1,2,3을 보내고 서버는 SQL을 만들어 실행합니다. 단점: 1) 깊은 페이지 느림, 2) long-running query 도중 카운트가 변하면 일관성이 깨짐(10페이지에서 읽은 자원이 11페이지에 또 나옴), 3) 카운트 비용 — `SELECT COUNT(*)`는 큰 테이블에서 비용이 크고 offset 페이지의 total 표시를 위해 매번 불립니다.
Cursor(keyset) 페이징은 정렬 키의 마지막 값을 다음 쿼리 조건에 넣어요. `WHERE id > 999980 ORDER BY id LIMIT 20` — 인덱스 위치를 즉시 찾아가기 때문에 OFFSET이 크든 작든 비용이 일정합니다. 데이터가 도중에 추가되어도 '그 다음 키 이후'로 자연스럽게 따라가니 일관성이 좋아요. 단점은 임의 페이지 점프가 어렵다는 점(마지막 키를 알아야 다음을 부를 수 있으니).
Page token(opaque)은 GraphQL Relay가 표준화한 방식 — 서버가 base64 토큰을 발급하고 클라이언트는 그걸 그대로 넘겨요. 내부 표현은 커서일 수도 있고 다른 인코딩일 수도 있으니 클라이언트가 해석할 필요 없음. Seek method는 keyset의 변형으로 정렬 키 기반으로 다음/이전 양방향 이동에 강해요. 마지막으로 keyset + total 분리 패턴 — 목록은 cursor로 빠르게, total은 별도 캐시/Counter 테이블로 비싼 COUNT를 캐시해서 절감하는 운영 팁.
| 구분 | 특징 | 참고 |
|---|---|---|
| Limit/Offset | 직관적·SQL 단순, 큰 OFFSET은 성능 저하 | 사용자 페이지 점프 |
| Cursor (keyset) | WHERE id > last_id ORDER BY id LIMIT | 실시간 피드·무한 스크롤 |
| Page token (opaque) | 서버가 토큰 발급·디코딩 | GraphQL Relay 표준 |
| Seek method | 정렬 키 기반으로 다음/이전 빠른 이동 | 인덱스 설계 전제 |
| Keyset + total 분리 | total은 별도 캐시 COUNT | 대용량 카운트 비용 절감 |
- Offset + total이 일관성 깨질 수 있음 — long-running query 도중 카운트 변동
- Cursor는 '다음 페이지가 정확히 어디서부터'의 의미, 변경 감지는 last_modified 도입으로
- GraphQL Relay의 cursor는 opaque 토큰 — 내부 표현은 서버 자유
- 무한 스크롤·실시간 피드는 cursor, 사용자 페이지 점프는 offset — 용도별 선택
- total 비용은 별도 캐시/Counter 테이블로 분리 — 매번 COUNT(*) 금지
우리 코드oxicode-mnemopi의 list API가 keyset + total 분리 패턴이고, oxios 메모리 recall API도 동일 — 인덱스된 키 컬럼을 마지막 커서로 넘긴다.
커리큘럼 체인 웹 API 기본기
WebSocket vs SSE vs Long Polling Realtime Push 그림 우리코드
WebSocket은 HTTP upgrade 후 전이중 지속 연결, SSE는 서버→클라이언트 단방향 자동 재연결, Long Polling은 주기적 재요청으로 가짜 실시간을 구현한다.
WebSocket = 전이중 지속 연결 프로토콜, SSE = Server-Sent Events(서버 전송 이벤트), HTTP/1.1/2/3 = HTTP 프로토콜 버전, HOL = Head-of-Line(줄 앞쪽) blocking.
HTTP는 요청-응답 모델이라 서버가 먼저 데이터를 보낼 방법이 없어요. 채팅·실시간 피드처럼 서버 푸시가 필요하면 별도 메커니즘이 필요합니다. 세 가지 흔한 답이 있어요.
Long Polling은 가장 단순한 우회 — 클라가 요청을 보내면 서버는 '데이터가 생길 때까지' 응답을 안 하고 기다렸다가 생기면 응답. 클라가 즉시 다음 요청을 다시 보내요. 가짜 실시간이지만 구현이 쉽고 기존 HTTP로 충분. 단점은 매번 새 연결을 열어야 하고 헤더 오버헤드가 매번 발생. 면접에서 '진짜 실시간은 아니다' 정도로 답하면 됩니다.
SSE(Server-Sent Events)는 HTTP 위에서 서버 푸시를 표준화한 거예요. Content-Type: text/event-stream 한 번 연결을 열면 서버가 `data: {...}\n\n` 형식으로 계속 흘려보내요. 브라우저는 자동 재연결을 내장하고 있어서 클라 코드 한 줄로 처리. 단방향(서버→클라)이라 채팅 입력은 별도 POST로 보내야 해요. LLM 토큰 스트리밍의 사실상 표준 — 단순·단방향·HTTP 친화로 충분하니까.
WebSocket은 HTTP에서 한 번 upgrade(101 Switching Protocols)하면 그 뒤로는 양방향 전이중 바이너리/텍스트 통신이 자유로워요. 게임·협업·거래소처럼 양쪽 푸시가 동시에 필요한 경우에 정답. 단점은 HTTP 인프라(로드밸런서·프록시·인증)와 잘 안 맞고, HTTP/1.1에선 HOL blocking이 스트림 단위로도 발생해요. HTTP/2·3 위에서는 하나의 TCP/HTTP 연결에 multiplex되어 헤더 압축·멀티플렉싱 혜택을 받습니다.
고르는 기준 — 단방향 데이터 푸시만 필요하면 SSE(가장 단순), 양방향 저지연이 필요하면 WebSocket, 기존 인프라 그대로 가볍게 가려면 Long Polling. 면접에서 '왜 LLM 스트리밍은 SSE냐'는 답이 '단방향 + HTTP 호환 + 자동 재연결 + 충분히 단순'.
- LLM 토큰 스트리밍의 사실상 표준은 SSE — 단방향·HTTP 친화·자동 재연결로 충분
- WebSocket over HTTP/2 시 멀티플렉싱, HTTP/1.1에선 HOL blocking이 흔한 이슈
- Long Polling은 가짜 실시간 — 헤더 오버헤드 매번 발생, 단순함만 장점
- WebSocket은 101 Switching Protocols로 HTTP에서 upgrade 후 자유 양방향 통신
- SSE는 EventSource API 한 줄 — 클라이언트 코드 단순, 인증·프록시 호환이 쉬움
우리 코드oxibrowser-cdp가 Chrome DevTools Protocol을 WebSocket으로 받아 디버거·DOM 검사·스크린샷을 한다. oxicode-ai는 LLM 응답을 스트리밍-first로 설계 — SSE 패턴을 그대로 따른다.
DNS / IP / 라우팅 DNS / IP 그림
DNS는 도메인을 IP로 해석(UDP 53, 다단계 분산 캐시). IP는 패킷의 주소 체계(v4 32bit·v6 128bit). 라우팅은 BGP·OSPF 등 자율시스템 간/내부 경로 결정이다.
DNS = Domain Name System(도메인 네임 시스템), IP = Internet Protocol(인터넷 프로토콜), IPv4/IPv6 = IP 버전 4/6, TTL = Time To Live(캐시 유효 시간), BGP = Border Gateway Protocol(외부 게이트웨이 간 라우팅 프로토콜), OSPF = Open Shortest Path First(내부 최단 경로 우선 라우팅), ARP = Address Resolution Protocol(IP→MAC 주소 해석).
웹 브라우저에 naver.com을 치면 일어나는 일을 쫓아가 봅시다. 1) 브라우저는 먼저 자기 캐시·OS 캐시·hosts 파일을 보고, 없으면 2) 로컬 DNS resolver(보통 ISP나 사내 DNS 서버)에게 재귀 질의. 3) resolver는 루트 DNS → .com TLD → naver.com 권한 DNS로 반복 질의해 답을 받아 캐시 TTL만큼 저장. 4) 그 답(예: 223.130.200.107)을 브라우저에 돌려주고 5) 브라우저가 그 IP로 TCP·TLS·HTTP. 이 '재귀 vs 반복' 구분과 TTL이 운영의 핵심이에요.
재귀 질의는 클라이언트를 대신해 끝까지 찾아가는 거고, 반복 질의는 '나는 모르지만 다음 서버 주소는 알려줄게'를 돌려주는 거예요. 사용자 → ISP resolver는 재귀, resolver → 권한 서버들 사이는 반복. TTL은 캐시 유효 시간 — 짧으면 원본 부하 ↑, 길면 변경 반영이 느려요.
IP는 패킷의 주소. IPv4는 32비트라 약 43억 개, 이미 포화. IPv6은 128비트로 사실상 무한에 가까운 주소 공간. 면접에서 '둘 다 지원해야 하나'는 답 — '듀얼 스택으로 운영, IPv4가 외부 클라이언트 대부분이면 IPv4 우선, IPv6로 전환 진행 중'.
라우팅은 자율시스템(AS, ISP·대기업·국가 단위) 사이의 경로 결정이에요. AS 내부 라우팅은 OSPF·IS-IS처럼 최단경로 비용 기반, AS 간은 BGP로 '이 IP 대역을 내가 안다'고 광고하고 정책으로 경로 선택. 면접 깊이 묻기는 어렵지만 Anycast 개념은 자주 나와요 — 동일 IP가 여러 글로벌 노드에 announce돼 있어 BGP가 사용자 위치에 가장 가까운 노드로 라우팅. CDN·DNS 루트 서버가 이걸 써요.
- DNS는 도메인 → IP 해석, 분산 다단계 캐시, UDP 53(크면 TCP fallback)
- 재귀 vs 반복: 사용자→resolver는 재귀, resolver→권한 서버는 반복
- TTL은 캐시 유효 시간 — 짧으면 원본 부하 ↑, 길면 변경 반영 느림
- Anycast — 동일 IP가 다수 노드에 announce, BGP 거리 기반 최단 라우팅
- IPv4 포화 → IPv6 듀얼 스택 전환이 글로벌 진행 중
CDN / 캐시 일관성 CDN / Caching 그림 우리코드
CDN은 정적 자원을 지리적으로 분산 캐시 — Origin pull·Push 패턴. 캐시 무효화(purge)와 캐시 키(Cache-Control·Vary)가 운영의 핵심이다.
CDN = Content Delivery Network(콘텐츠 전송 네트워크), PoP = Point of Presence(접속 지점), ETag = Entity Tag(엔티티 태그, HTTP 캐시 검증자), TTL = Time To Live(캐시 유효 시간), LRU = Least Recently Used(최근 최소 사용, 캐시 교체 정책).
왜 CDN이 필요한가 — Origin 서버가 서울에 있고 사용자가 부산·도쿄·LA에 있으면 물리적 거리만큼 RTT가 늘어나요. CDN은 전 세계 PoP(Point of Presence)에 정적 자원 사본을 두고 사용자에게 가장 가까운 곳에서 응답합니다. 첫 요청만 Origin에서 가져오고(origin pull) 이후엔 PoP 캐시, 이후 변경 시 purge·재배포.
HTTP 캐시 제어는 Cache-Control 헤더가 표준이에요. max-age=N은 N초 동안 신선, s-maxage=N은 CDN·공유 캐시에만 적용(브라우저 캐시와 분리), no-store는 캐시 금지(민감 데이터), private는 특정 사용자만 캐시 가능. ETag + If-None-Match는 '캐시 검증' — 자원이 바뀌면 새 ETag, 같으면 304 Not Modified로 본문 없이 '그대로 써도 됨'을 알려줘요. 면접 단골은 '조건부 요청의 정석'.
캐시 일관성은 '원본과 캐시가 다른 상태로 오래 있는가'의 문제예요. 두 가지 — 무효화(purge)가 늦으면 stale 응답, 너무 자주 무효화하면 origin 부하. 패턴: 1) 짧은 TTL + 백그라운드 재검증(stale-while-revalidate), 2) 자원 버전을 URL에 포함(`app.20260816.css`), 3) purge API로 긴급 무효화. CDN은 보통 Vary 헤더로 Accept-Encoding·Origin 같은 캐시 키를 분기해요 — 같은 URL이라도 gzip vs br, 모바일 vs PC를 다른 캐시 엔트리로 분리.
write-through/read-aside 같은 애플리케이션 캐시는 다릅니다. oxicode-mnemopi의 read-aside LRU 캐시는 DB 결과를 메모리에 두고 다음 조회 때 빠르게 응답 — 원본이 바뀌면 캐시 무효화. CDN은 네트워크·HTTP 레벨 캐시이고, 이건 프로세스 내부 캐시. 둘을 같이 쓰는 게 일반적이에요.
- Cache-Control: max-age / s-maxage / no-store / private — 헤더별 의미 구분
- ETag + If-None-Match로 304 — 면접의 '조건부 요청' 정석
- 캐시 일관성: TTL + stale-while-revalidate + URL 버전 + purge API
- Vary 헤더로 캐시 키 분기 — Accept-Encoding·Origin별 다른 캐시 엔트리
- read-aside LRU 같은 앱 내부 캐시와 CDN·HTTP 캐시를 계층적으로 같이 사용
우리 코드oxicode-mnemopi의 read-aside 캐시가 LRU. ETag·Cache-Control 표준을 그대로 쓰고, 자원 URL 버전을 통한 무효화 패턴을 운영.
로드 밸런서 / 리버스 프록시 (L4·L7) Load Balancer / Reverse Proxy
로드 밸런서는 트래픽을 여러 백엔드로 분산하고, 리버스 프록시는 클라이언트 대신 서버 앞에 서서 요청을 받아 중계한다. L4는 IP·포트, L7은 HTTP 경로·헤더·쿠키로 라우팅한다.
L4 = Layer 4(전송 계층, IP·포트 기반), L7 = Layer 7(응용 계층, HTTP 경로·헤더 기반), LB = Load Balancer(부하 분산기), ALB = Application Load Balancer(AWS L7 로드밸런서), NLB = Network Load Balancer(AWS L4 로드밸런서), TCP = Transmission Control Protocol, HTTP = HyperText Transfer Protocol.
먼저 두 용어를 분리해요. 로드 밸런서는 '여러 백엔드에 트래픽을 나눠주는' 역할, 리버스 프록시는 '클라이언트 대신 서버 앞에 서는' 역할 — Nginx·HAProxy·Envoy는 둘 다 해요. 클라이언트가 보낸 요청을 받아서 '적절한 백엔드로' 넘기고 응답을 돌려주는 구조라 사실상 같은 박스에서 구현됩니다.
L4 로드밸런서는 TCP·UDP 레벨에서 동작해요. 패킷의 (출발 IP·포트, 목적지 IP·포트) 4-tuple을 보고 어떤 백엔드로 보낼지 결정합니다. HTTP 내용은 안 까봐요. 그래서 HTTPS도 SSL 종료(termination) 없이 패킷을 그대로 패스스루(NLB)하거나, 종료 후 다시 암호화해 백엔드로 보낼 수 있어요. 장점은 빠르고 — TCP 한 줄 열고 라우팅이 끝나니까. 단점은 'URL 경로별 라우팅' 같은 HTTP 인지 결정을 못 합니다.
L7 로드밸런서는 HTTP 메시지를 까봐요. Host 헤더로 멀티 도메인 분기, URL 경로로 `/api/*` → API 서버·`/static/*` → 정적 서버 분기, 쿠키로 sticky session, 헤더로 카나리/블루-그린. SSL 종료도 보통 L7에서 해요(평문 HTTP로 백엔드 전달). AWS ALB·Nginx·HAProxy(7모드)·Envoy가 대표적.
분산 알고리즘은 — 라운드로빈은 순차 분배(서버 성능 동일할 때), 최소 연결(least connections)은 현재 활성 연결 수 최소 서버로 보내기(긴 요청 편차 완화), 해시(consistent hashing)는 `hash(클라이언트IP/요청키) % N`으로 결정성 있는 분배(캐시 locality 유리). 가중치 라운드로빈은 서버 스펙 차이 보정.
헬스체크는 정상 백엔드로만 트래픽을 보내기 위한 상태 점검이에요. TCP 레벨은 그냥 SYN/ACK 응답 확인, HTTP 레벨은 GET /healthz에 200 기대, 더 정밀하게는 `/healthz?deep=1`로 DB·외부 의존성까지 점검. 다만 deep 체크를 너무 자주/엄격하게 하면 LB 입장에서 모든 백엔드를 죽였다 살렸다 하는 flapping이 생겨요.
세션 스티키니스 함정 — sticky session은 같은 클라이언트를 같은 백엔드로 보내는 옵션(L7 쿠키 기반 or L4 source IP 해시). 사용자 로그인 세션이 메모리에만 있으면 필요하지만, 백엔드가 꺼질 때 세션 유실 + 트래픽 편중(hot node)이 함정. 해결은 세션을 Redis 같은 공유 저장소에 두거나, 토큰(JWT) 기반 stateless로 가는 것. 면접에서 '왜 sticky가 안티패턴으로 불리냐'는 답 — '고가용성과 확장성의 적'.
대표 구현 — Nginx(L7 리버스 프록시 + L4 스트림 pass), HAProxy(L4·L7 모두 강력, sticky·healthcheck 정교), AWS ALB(L7 SaaS형)·NLB(L4 SaaS형), Envoy(서비스 메시 표준, L7 필터·트레이싱 내장). 공공 SI는 Nginx + HAProxy 조합이 흔하고, 클라우드 네이티브는 ALB·Envoy가 표준.
- L4 = IP·포트, L7 = HTTP 경로·헤더·쿠키 — 헤더를 까보는지가 L7의 본질
- 라운드로빈 / 최소연결 / 해시 — 분산 알고리즘은 워크로드 특성에 맞춰
- 헬스체크는 L4 TCP / L7 GET /healthz — deep 체크 flapping 주의
- Sticky session은 백엔드 장애 시 세션 유실 + 트래픽 편중 — Redis 공유 세션이나 JWT stateless로 회피
- Nginx·HAProxy·AWS ALB·Envoy — 용도별 선택 (L4 pass-through vs L7 분기 vs 서비스 메시)
OS · 동시성 14
프로세스·스레드·가상 메모리·스케줄러·동기화. 실무 면접이 시스템 디자인으로 확장되는 출발점. 동시성 모델을 어떻게 고르느냐가 곧 서비스 응답성을 결정합니다.
프로세스 vs 스레드 vs 코루틴 Process / Thread / Coroutine 그림 우리코드
프로세스는 격리된 실행 단위(독자 주소 공간). 스레드는 프로세스 안에서 자원을 공유하는 동시 흐름. 코루틴은 협동적으로 일시중단·재개되는 서브루틴.
프로세스는 OS가 자원을 청구하고 격리하는 단위예요. 각 프로세스는 자기만의 가상 주소 공간을 갖고, 한쪽이 무한 루프를 도거나 죽어도 다른 쪽에 직접 영향을 주지 않습니다. 대신 새 프로세스를 띄울 때 fork()/exec()를 거치면 페이지 테이블·파일 디스크립터·환경변수를 통째로 복제·초기화하기 때문에 무거워요.
스레드는 같은 프로세스 안의 동시 흐름이라 코드·힙·전역변수를 공유하고, 별도 스택과 레지스터만 가집니다. 그래서 락(lock) 같은 명시적 동기화가 없으면 한 스레드의 `i++` 동작이 다른 스레드에게 `i`가 반만 반영된 것처럼 보이는 race condition이 생깁니다. 대신 생성·전환 비용이 프로세스보다 한참 가볍습니다.
코루틴은 사용자 영역에서 일시중단 가능한 서브루틴이에요. Go의 고루틴(goroutine), Kotlin coroutine, Rust의 tokio task가 모두 이 계열이고, OS 스레드와 1:1로 묶이지 않습니다. M:N 스케줄러가 코루틴 수천 개를 실제 스레드 수십 개에 매핑하기 때문에 I/O 대기 동안 다른 코루틴을 같은 스레드에서 돌릴 수 있어요. 다만 코루틴은 협동적(cooperative)이라 한 번 양보(yield)하기 전엔 다른 코루틴이 끼어들 수 없습니다.
- 프로세스 분리는 fork()/exec() — 페이지 테이블 복제 비용 큼
- 스레드 동기화 실패의 전형: race condition·visibility 문제·순서 보장 부재
- 코루틴은 CPU-bound 작업엔 부적합 — 양보 지점이 없으면 다른 코루틴 굶주림(starvation)
- M:N 스케줄러(Go 런타임)는 코루틴 N개를 OS 스레드 M개에 매핑해 컨텍스트 스위칭 비용을 흡수
우리 코드oxi 4프로젝트(oxicode, oxibrain, oxios, oxibrowser)가 모두 tokio 비동기 런타임 위에서 동작 — 한 스레드 풀에서 LLM 스트리밍·CDP WebSocket·파일 I/O를 동시에 처리합니다.
상호 배제 / 뮤텍스 / 세마포어 Mutual Exclusion / Mutex / Semaphore 표 그림 우리코드
상호배제는 임계 영역을 한 번에 하나만 실행시키는 약속. 뮤텍스는 1키·1소유자 잠금, 세마포어는 N개 자원 카운터, 모니터는 락+조건변수 묶음이다.
뮤텍스는 '잠근 자가 해제한다'는 소유권 개념이 핵심이에요. lock을 획득한 스레드만 unlock할 수 있어서 누가 들고 있는지 추적 가능하고, 재진입(recursive) 변형은 같은 스레드가 두 번 걸어도 데드락이 안 생기게 합니다. 반면에 잠금 키를 흩뜨려 두면 다른 스레드가 절대 풀 수 없는 상황이 생겨서 디버깅이 어렵습니다.
세마포어는 단순한 정수 카운터예요. 카운트가 0보다 크면 acquire로 1 줄이고, release로 1 늘립니다. 바이너리 세마포어(0/1)는 사실상 뮤텍스처럼 동작하지만 소유권이 없어서 A가 acquire하고 B가 release하는 패턴(생산자-소비자)에도 쓸 수 있어요. 카운팅 세마포어(예: 5)는 'DB 커넥션 풀이 5개'처럼 자원이 N개인 상황을 표현합니다.
모니터(Monitor)는 락과 조건 변수를 하나로 묶은 고수준 추상화예요. Java의 synchronized·C#의 lock이 이 모델이에요. '공유 자원에 접근하는 코드 블록 자체를 잠금'으로 감싸서, 잠금 걸고 푸는 순서를 개발자가 실수할 여지를 줄입니다.
| 구분 | 특징 | 참고 |
|---|---|---|
| 뮤텍스 | 1키·1소유자. 잠근 스레드만 해제 가능 | 재진입 변형 있음 |
| 바이너리 세마포어 | 0/1 카운터. 소유권 없음 | 뮤텍스처럼 동작 |
| 카운팅 세마포어 | N≥0. 자원 N개 풀 표현 | DB 커넥션 풀 등 |
| 모니터(Monitor) | 락 + 조건 변수 묶음 | Java synchronized, C# lock |
- 뮤텍스 = 소유권 있는 잠금(잠근 자만 해제), 세마포어 = 소유권 없는 카운터
- 바이너리 세마포어 vs 뮤텍스 — 생산자-소비자처럼 '다른 스레드가 해제' 패턴은 세마포어
- 카운팅 세마포어는 자원 N개 풀(DB 커넥션 풀 등)의 표준 표현
- 모니터 = 락 + 조건 변수 묶음 — Java synchronized, C# lock
우리 코드oxibrain의 single-writer actor 패턴이 '쓰기는 단일 흐름'으로 직렬화해 잠금 복잡도를 아예 제거한 사례예요.
데드락 / 우선순위 역전 Deadlock / Priority Inversion 우리코드
데드락은 서로 상대방 자원을 기다리며 영구 멈추는 상태 — Coffman 4조건이 동시에 성립할 때만 발생한다. 우선순위 역전은 낮은 우선순위 스레드가 락을 잡아 높은 우선순위가 밀리는 현상.
Coffman 4조건 = 상호배제·점유대기·비선점·순환대기. Priority inheritance = 우선순위 상속.
데드락은 Coffman 4조건이 동시에 성립할 때만 발생해요 — ① 상호배제(자원을 한 번에 하나씩만), ② 점유 대기(들고 있으면서 또 요청), ③ 비선점(강제로 뺏지 못함), ④ 순환 대기(대기 그래프가 고리). 예방은 이 중 하나를 깨는 것이라 구체적입니다: 락 순서를 전역 고정(④ 파괴), tryLock으로 타임아웃(② 일부 해소), read-copy-update 같은 비잠금 자료구조(①·③ 해소) 같은 식이에요.
예방·회피·탐지를 구분하면 — 예방은 4조건 중 하나를 설계 단계에서 깨는 것, 회피는 Banker 알고리즘처럼 항상 안전 상태를 유지하는 동적 판단, 탐지는 자원 할당 그래프에서 사이클을 주기적으로 찾아 강제 해제하는 것입니다. 실무에서는 '락 순서 고정 + tryLock 타임아웃'이 90%의 예방이에요.
priority inversion(우선순위 역전)은 별개의 위험입니다. 낮은 우선순위 스레드 L이 락을 잡고 있는 동안 높은 우선순위 H가 그 락을 기다리면, 중간 우선순위 M이 L을 밀어내고 CPU를 다 쓰면서 H는 L 뒤에서 계속 기다려요. NASA Mars Pathfinder가 실제로 이 버그로 재부팅한 사례가 있고, 해결책은 우선순위 상속(priority inheritance) — L이 락을 잡는 순간 H의 우선순위까지 잠시 끌어올려서 M에게 밀리지 않게 하는 것입니다.
- 데드락 4조건: 상호배제·점유대기·비선점·순환대기 — 하나만 깨도 예방
- 데드락 예방 전략: 락 획득 순서 고정(순환 대기 파괴) · tryLock + 타임아웃 · 비잠금 자료구조
- 회피(Banker)·탐지(사이클 검출)는 실무보다 이론 단골
- Priority inversion — 우선순위 상속(priority inheritance)으로 해결, NASA Pathfinder 사례
우리 코드oxibrain의 single-writer actor가 데드락 자체를 구조적으로 제거 — 락을 여러 스레드가 경쟁하지 않으면 4조건 중 상호배제 경쟁이 성립하지 않는다.
비동기 / 이벤트 루프 / Future / Promise Async Runtime 자소서 그림 우리코드
스레드 하나로 수천 개 I/O 대기를 겹치는 협력적 동시성. await 지점에서 제어권을 양보해 OS 스레드를 묶어두지 않습니다.
식당에 점원이 한 명이라고 상상해보세요. 손님이 20명인데 한 명씩 요리가 나올 때까지 카운터에서 기다리면 비효율적이죠. 진짜 점원은 주문을 받고, 주방에 '다 되면 알려줘' 하고 그 사이에 다른 테이블 주문을 받습니다. 이게 이벤트 루프예요. Node.js·Deno·브라우저의 JS 런타임이 정확히 이 모델로 동작합니다.
콜백 패턴(`fs.readFile(path, (err, data) => …)`)은 '다 되면 알려줘'를 함수로 받은 거예요. 다만 콜백이 중첩되면 '콜백 지옥'이 생기고, 에러 처리가 각 콜백마다 따로 필요해서 흐름이 끊깁니다. ES2015 Promise는 '결과를 표현하는 객체'로 이걸 해소했고, then 체이닝으로 평탄화했어요. async/await(ES2017)은 그 위에 문법적 설탕을 씌운 거라, 본질은 Promise인데 코드가 동기처럼 읽힙니다.
Rust의 Future는 모델이 다릅니다. JS의 Promise는 executor가 알아서 결과를 채워주지만, Rust의 Future는 `std::future::Future` 트레잇의 `poll(self, cx)` 메서드가 호출되어야만 진전을 보고해요. 즉 'Future는 작업 결과를 묘사한 불투명 값'이고, tokio 런타임이 poll을 반복 호출해서 `Poll::Ready(T)`가 나올 때까지 자기 안에 등록해둔 다른 Future를 폴링합니다. 이게 zero-cost abstraction — Future가 상태기계로 컴파일되어 동적 디스패치 없이 인라인화되기 때문입니다.
이벤트 루프에는 보통 마이크로태스크(Promise.then, queueMicrotask)와 매크로태스크(setTimeout, I/O 콜백) 두 큐가 있어요. 마이크로태스크는 현재 작업이 끝나자마자 전부 비우기 때문에, Promise.then을 100번 연결하면 setTimeout은 그 사이에 한 번도 실행되지 못합니다. 면접에서 '왜 setTimeout(fn, 0)이 즉시 안 불리는가'를 물으면 마이크로태스크 드레인에서 답이 나옵니다.
- 진화 단계: 콜백 → Promise → async/await — 본질은 동일, 가독성만 개선
- Rust Future는 pull 모델(poll), JS Promise는 push 모델(executor가 resolve)
- 마이크로태스크 vs 매크로태스크 — setTimeout 0이 즉시 실행되지 않는 이유
- async fn이 CPU-bound 작업을 묶으면 다른 태스크 굶주림 — 스레드 풀로 분리
우리 코드oxi 4프로젝트가 tokio 비동기 런타임 위에서 동작. oxios는 request-reply 패턴과 SSE 스트림을 동일한 reactor에서 동시에 흘립니다.
Send / Sync (Rust 메모리 모델) Send / Sync 그림 우리코드
Rust의 마커 트레잇 — Send는 스레드 간 이동 가능, Sync는 &참조의 다중 스레드 공유 가능. 컴파일 타임에 데이터 경쟁을 차단해 GC·런타임 잠금 없이 안전성을 보장합니다.
데이터 경쟁(data race)은 두 스레드가 같은 메모리에 동시 접근해 한쪽이 쓰는 경우인데, 미정의 동작이라 디버깅이 거의 불가능해요. Rust는 이걸 컴파일 타임에 막습니다. Send는 '이 값을 다른 스레드로 소유권 이전(move)할 수 있다'라는 약속이고, Sync는 '&T가 여러 스레드로 동시에 보내질 수 있다'라는 뜻이에요. T가 Sync면 &T가 Send가 되는 대칭 관계가 있습니다.
자동 구현 규칙은 단순해요. 모든 필드가 Send면 그 구조체도 Send, 모든 필드가 Sync면 Sync. 그래서 i32·String·Vec<T: Send>는 자동으로 Send고, 락 가드 MutexGuard 같은 동기는 Sync가 됩니다. 반대로 `Rc<T>`(참조 카운트)는 !Send예요 — 카운터를 증가시킬 때 atomic이 아니라 일반 +1이라 멀티스레드에서 두 번 동시 +1하면 카운트가 한 번만 늘어나는 버그가 생깁니다. 멀티스레드용은 `Arc<T>`(Atomic Reference Counted)예요.
Send/Sync 위반은 컴파일 에러라 런타임에 폭발하지 않습니다. `std::thread::spawn(move || { use_thing(thing); })`가 안 되면 보통 Rc → Arc, Mutex → RwLock, 혹은 채널로 소유권을 넘기는 식으로 고쳐요. UnsafeCell로 안 감싼 raw pointer는 !Sync라 멀티스레드 공유가 금지됩니다.
- Send = 스레드 간 move 가능, Sync = &참조 공유 가능. T: Sync ⇒ &T: Send
- Rc<T>는 !Send — 카운터가 atomic 아님. 멀티스레드는 Arc<T>
- MutexGuard는 Sync — 여러 스레드가 &MutexGuard로 데이터 공유 가능
- Send/Sync 위반은 컴파일 에러 — 런타임 폭발 없음
우리 코드oxibrowser의 boa_engine 0.20 Context가 !Send라 std::thread에 못 보내고, 별도 worker 스레드에 고정해 둔 뒤 mpsc 채널로 비동기 코어와 통신하는 패턴이 그대로 적용됩니다.
캐시 일관성 / MESI 프로토콜 Cache Coherence 그림 우리코드
다중 코어에서 한 코어의 캐시 라인이 다른 코어에 보일 때 일관성을 유지하는 규칙. MESI(Modified·Exclusive·Shared·Invalid)가 대표적이며, 한 코어가 쓰면 다른 코어의 같은 라인이 무효화됩니다.
현대 CPU는 코어마다 L1·L2 캐시가 있고 공유 L3가 따라붙어요. 한 코어가 변수 x를 쓰면 자기 캐시에는 반영되지만, 다른 코어의 캐시에 남은 옛 값이 보이면 같은 메모리 주소인데 값이 다른 모순이 생깁니다. 데이터 흐름이 아니라 '같은 주소에 다른 값'이 진짜 문제예요. 이걸 막기 위해 캐시 컨트롤러가 snooping 버스(공유 버스에서 다른 코어의 요청을 감청)나 directory 기반 프로토콜로 일관성을 맞춥니다.
MESI는 라인 단위로 4 상태를 붙여요. M(Modified): 이 코어만 갖고 있고, 더러워서 메모리와 다름 — 쓰려면 캐시에만 반영하면 됨. E(Exclusive): 이 코어만 갖고 있고, 메모리와 같음 — 곧 M으로 갈 수 있음. S(Shared): 여러 코어가 같이 갖고 있음 — 쓰려면 다른 S 코어에 invalidate 브로드캐스트 필요. I(Invalid): 이 캐시는 버린 상태. 한 코어가 쓰려고 하면 다른 코어들의 라인이 I로 전이되고, snooping 신호로 자기 라인이 M이 됩니다.
false sharing은 MESI의 부작용이에요. 두 스레드가 서로 다른 변수를 쓰는데 그 변수가 같은 64바이트 캐시 라인에 들어 있으면, 한쪽이 쓰자마자 다른 쪽의 라인이 Invalidate되어 다시 메모리에서 가져와야 합니다. 마치 옆집 사람이 현관문을 열 때마다 내 집 열쇠도 새로 깎는 격이에요. 해결은 `#[repr(align(64))]`로 변수를 별도 라인에 배치하거나, padding을 넣어 떨어뜨리는 것입니다.
- MESI 상태 전이 — write 시 Broadcast Invalidate, 다른 코어 라인은 I로
- False sharing: 다른 변수가 같은 64B 라인에 있어 무효화 연쇄. `repr(align(64))`
- 메모리 배리어(memory barrier)는 명령 재배치 + 캐시 가시성 순서를 묶음
- ARM은 MOESI, AMD는 MOESI 확장 — Owned 상태로 dirty 공유 표현
우리 코드oxicode-mnemopi의 SQLite WAL + connection pool 패턴이 다중 reader·writer를 처리하는 데, 캐시 일관성은 SQLite 엔진 안에서 처리되는 영역이에요.
가상 메모리 / 페이징 / TLB Virtual Memory 그림 우리코드
가상 주소는 페이지(보통 4KB) 단위로 고정 크기 분할. 페이지 테이블이 가상→물리 매핑을 들고, TLB(Translation Lookaside Buffer)는 그 매핑의 하드웨어 캐시예요.
왜 가상 메모리가 필요한가부터 보면, 프로세스마다 자기만 쓰는 거대한 주소 공간을 믿고 코딩하게 해주기 위해서예요. 실제로 물리 메모리보다 큰 프로세스를 돌릴 수 있고, 각 프로세스가 같은 주소 0x400000을 자기 코드 영역으로 매핑해도 서로 격리됩니다. 보호(다른 프로세스 페이지 못 읽음)·효율(자주 쓰는 페이지만 RAM)·편의(연속 주소 믿고 코딩)를 한꺼번에 얻는 셈이에요.
페이징은 이 가상 메모리를 4KB(또는 2MB huge page) 단위로 쪼개는 방식이에요. 페이지 테이블이 페이지 번호 → 물리 프레임 번호 매핑을 들고, 페이지마다 R/W/X 비트가 있어 권한을 강제합니다. 다만 32비트 주소면 4KB 단위로 2²⁰=백만 엔트리, 64비트면 4단계 페이지 테이블이 따라붙어요. 그래서 CPU 안에 TLB라는 작은 하드웨어 캐시가 최근 매핑 수십~수백 개를 들고 있습니다.
TLB 미스는 큰 지연이에요. L1 캐시 히트가 1ns라면 페이지 테이블 워크는 100ns 이상. 그래서 huge page(2MB·1GB) — 페이지 크기를 키워서 TLB가 커버하는 주소 범위를 늘리는 기법이 DB·KVM 같은 영역에서 쓰입니다. Linux에서는 `hugeadm`이나 THP(Transparent Huge Pages)로 활성화해요.
fork()의 핵심 기법인 Copy-on-Write(COW)는 여기서 출발합니다. 부모 프로세스를 fork하면 자식이 부모와 같은 페이지 테이블을 가리키지만, 실제 물리 프레임은 공유해요. 어느 쪽이든 그 페이지를 처음 쓸 때 비로소 새로운 물리 프레임을 할당하고 거기로 복사합니다. 메모리를 즉각 두 배로 복제하지 않으니 fork가 가볍고, 자식이 exec()로 곧 다른 프로세스를 띄우면 거의 복사 없이 끝납니다.
스왑(swap)은 RAM이 부족할 때 페이지를 디스크로 내보내는 메커니즘이에요. 문제는 페이지 폴트(접근한 페이지가 RAM에 없을 때)가 디스크 I/O를 일으키는데, 메모리 접근이 보통 100ns인 데 반해 디스크 read가 수 ms라 한 번 폴트가 10000배 느려집니다. 메모리가 빡빡하면 페이지 폴트가 연쇄적으로 발생해 실제 작업이 거의 진행되지 못하는 thrashing(스레싱) 상태에 빠져요. 그래서 overcommit 정책(Linux의 `/proc/sys/vm/overcommit_memory`)과 cgroup 메모리 제한으로 시스템 차원에서 보호합니다.
- 페이지 테이블은 보통 4단계(64비트) — PTE 권한 비트가 R/W/X 강제
- TLB 미스는 페이지 워크보다 비싸 — huge page로 압박 감소
- fork + COW는 페이지 테이블을 복제만 하고 실제 프레임 공유, exec 시 거의 무비용
- 스왑 thrashing — 페이지 폴트가 ms 단위로 늘어나 시스템이 멈춘 듯 동작
우리 코드boa_engine이 Rust 프로세스 안에서 별도 JS 힙을 운영 — JS 힙과 Rust 힙이 분리된 두 메모리 모델을 공존시키는 구조예요.
페이지 교체 알고리즘 Page Replacement 표 그림
물리 메모리가 부족할 때 디스크로 내보낼(또는 evict할) 페이지를 결정하는 정책. 목표는 페이지 폴트 횟수 최소화.
이상적인 교체는 '미래에 가장 오래 안 쓸 페이지'를 미리 아는 Belady의 최적(OPT) 알고리즘이지만, 실제로 미래를 알 수 없으니 근사합니다. FIFO는 큐에 넣은 순서대로 내보내는 단순 정책인데, 프레임을 늘렸는데 오히려 페이지 폴트가 늘어난 Belady's anomaly가 일어날 수 있어요(순서를 무시한 채 오래된 게 먼저 나가는 게 항상 옳지 않다는 증거).
LRU(Least Recently Used)는 '최근에 안 쓴 게 앞으로도 안 쓴다'는 시간 지역성 가정이에요. 페이지마다 '마지막 접근 시각'을 기록하고 가장 오래된 걸 내보내는데, 페이지 폴트 수 최소에 가장 가깝게 수렴합니다. 다만 매 메모리 접근마다 갱신이 필요해 정확 구현이 비싸요. 그래서 운영체제는 LRU를 흉내내는 근사로 갑니다. Clock(2nd chance) 알고리즘은 페이지마다 reference bit를 두고, 가리키는 포인터가 돌면서 bit=1이면 한 번 더 살리고 bit을 0으로, bit=0이면 거기서 evict하는 식이에요. 원형 리스트 + 1비트면 구현이 매우 가볍습니다.
WSClock(Working Set Clock)은 여기에 working set 개념을 결합해 최근 Δ 시간 동안 참조된 페이지 집합을 보존하려 합니다. Linux의 페이지 회수 코드와 Solaris가 이 계열을 표준으로 채택했어요. LFU(Least Frequently Used)는 빈도 기반이라 캐싱 정책과 친화적이고, '한 번 읽고 다시 안 읽는' 데이터엔 오히려 LRU보다 약합니다.
| 구분 | 특징 | 참고 |
|---|---|---|
| FIFO | 가장 오래된 페이지 교체. 구현 단순 | Belady 모순 가능 |
| LRU | 가장 오래 사용 안 한 페이지 교체 | 정확 구현은 비싸, 근사 사용 |
| Clock(2nd chance) | reference bit 있는 페이지는 한 번 더 | LRU 근사, 구현 간단 |
| LFU | 참조 빈도 최소 페이지 교체 | 캐싱 친화 |
| Working Set | 최근 Δ 시간 참조한 집합 유지 | WS 보존 |
| WSClock | Clock + working set 결합 | 현대 OS 표준 |
- LRU 정확 구현은 비싸 — Clock/2nd chance가 실용 표준
- WSClock은 working set 보존 — Linux·Solaris의 표준 회수 정책
- LFU vs LRU — '한 번 읽기' 워크로드엔 LRU가 더 잘 동작
- 페이지 폴트 1회 = 디스크 I/O 1회 = 메모리 접근의 ~10000배 지연
프로세스 / 스레드 스케줄러 Scheduler 그림
CPU 코어가 한정돼 있을 때 어떤 스레드를 다음에 몇 ms 동안 돌릴지 결정하는 정책. 선점형(preemptive)은 인터럽트·할당량(quantum) 만료 시 OS가 강제로 다음 스레드로 바꿉니다.
라운드로빈(Round Robin)은 가장 단순한 선점형이에요. 모든 스레드를 같은 크기 time slice(예: 10ms)로 한 줄로 세워 차례로 돌립니다. 응답성이 균등하지만 짧은 작업도 긴 작업도 같은 시간을 받아서 비효율적이에요. 우선순위 + 멀티 큐 방식은 우선순위 클래스마다 별도 큐를 두고, 상위 큐가 빌 때까지 하위는 안 돌리는 방식 — 데스크탑 OS의 표준입니다.
Linux의 CFS(Completely Fair Scheduler)는 fair share를 추구해요. 각 스레드에 '가상 런타임(vruntime)'을 누적하고, 가장 작은 vruntime을 가진 스레드를 다음에 픽업합니다. 우선순위는 가중치로 환산돼 같은 시간 동안 우선순위 높은 스레드가 vruntime을 적게 쌓습니다. 결과적으로 '한 코어를 점유했을 때의 비율'이 우선순위별로 보장돼요.
실시간(RT)·베스트 노력(BE)·유휴(idle) 클래스도 분리됩니다. RT는 deadline을 못 넘기게 우선 처리, BE는 일반 애플리케이션, idle은 아무것도 안 할 때만 — 예: `nice 19` 작업. 컨테이너 안에서는 cgroup v2의 cpu.weight·cpu.max가 같은 방식으로 비중을 나눠요.
- Round Robin = 단순 균등 슬라이스. 응답성 균등, throughput 낮음
- CFS는 vruntime 최소 스레드를 선택 — 우선순위는 가중치
- RT/BE/idle 클래스 분리 + cgroup v2 cpu.weight — 컨테이너 자원 분배
- 스케줄러 latency vs throughput trade-off — 슬라이스가 짧으면 응답성↑, throughput↓
파일 시스템 / 디스크 I/O File System 그림 우리코드
ext4·XFS·NTFS·APFS 같은 파일 시스템은 inode 기반 메타데이터 + 블록 데이터로 구성. copy-on-write(APFS, BTRFS)·journaling으로 정합성을 확보합니다.
파일은 디스크에서 보면 그냥 바이트의 연속이고, OS는 그걸 inode(파일의 메타데이터 — 권한·크기·데이터 블록 위치) + 실제 블록으로 추적해요. 디렉토리는 '이름 → inode 번호' 매핑을 담은 특별한 inode입니다. 읽기/쓰기 한 번이 디스크 헤드 이동(seek) + 회전 지연(rotational latency)을 거치면 ms 단위가 나오기 때문에, OS는 한 번 읽은 블록을 페이지 캐시(shared memory)에 두고 재사용해요.
정합성 측면에서 journaling(저널링)이 핵심입니다. ext4는 변경 사항을 먼저 저널(append-only 로그)에 쓰고 fsync로 플러시한 뒤 실제 위치에 반영해요. 중간에 전원이 나가도 저널을 replay하면 일관된 상태로 복구됩니다. APFS나 BTRFS는 copy-on-write로 저널 없이도 일관성을 유지하는 다른 길을 택했어요 — 변경된 블록을 새로 쓰고 메타데이터 포인터만 갈아끼웁니다.
RAID는 디스크 배열 수준에서 보호 vs 공간 트레이드입니다. RAID 0(striping)은 공간 효율 100%지만 한 디스크 죽으면 전체 손실, RAID 1(mirroring)은 공간 50%지만 한쪽 죽어도 살아남음, RAID 5는 패리티를 분산해 공간 (n-1)/n 효율 + 한 디스크 보호, RAID 6는 패리티 두 개로 두 디스크 동시 고장까지, RAID 10은 미러 + 스트라이프 조합. 분산 시스템에선 fsync로 디스크에 flush했느냐가 정합성의 출발점이에요 — 그렇지 않으면 OS가 '썼다'고 답해도 디스크 캐시에만 있을 수 있습니다.
- 페이지 캐시 — OS가 read-ahead/write-back으로 디스크 왕복 흡수
- Journaling vs Copy-on-Write — 정합성 확보 두 가지 길
- RAID 0/1/5/6/10 — striping·mirroring·패리티 트레이드
- fsync가 분산 정합성의 출발 — 안 부르면 OS 캐시에만 머무름 가능
우리 코드oximemo의 atomic write 패턴(tmp 파일 작성 → fsync → rename)이 데스크탑 앱에서 분산 정합성과 비슷한 안전성을 보장하는 표준 기법이에요.
IPC / 파이프 / 공유 메모리 / 소켓 IPC 그림 우리코드
프로세스 간 통신 채널. 파이프·FIFO·소켓(Unix domain·inet)·공유 메모리·메시지 큐·세마포어가 있고, Tokio의 mpsc 채널도 같은 범주의 동기적 채널로 분류할 수 있습니다.
가장 가벼운 게 파이프(unix pipe)예요. 부모-자식 프로세스가 `|`로 연결되는 그 파이프. 단방향 byte stream이고 한쪽이 쓰면 다른 쪽이 읽어요. FIFO(named pipe)는 파일 시스템에 이름을 가진 파이프라서 관련 없는 프로세스도 같은 FIFO 경로를 알면 통신합니다.
소켓은 양방향이고 두 종류가 자주 비교돼요. Unix domain socket(AF_UNIX)은 같은 머신 안에서만 쓰며 파일 시스템 경로로 식별합니다. 커널 안에서 버퍼만 거치니까 TCP loopback보다 빠르고(10Gbps 가까이도 가능), 파일 권한으로 접근 제어까지 됩니다. oxios ↔ oxibrain이 이 방식으로 통신해요. INET socket(TCP/UDP)은 네트워크를 거쳐야 하니 같은 머신에서도 추가 왕복과 헤더 처리가 따라붙습니다.
공유 메모리(shm)는 같은 물리 프레임을 두 프로세스가 매핑해 가장 빠른 IPC예요. 다만 동기화가 필요하니 보통 세마포어·Mutex를 짝으로 씁니다. zeromq나 nanomsg는 이 위에 메시지 큐 추상화를 얹어서 동기화 부담 없이 빠른 패턴(req-rep·pub-sub·push-pull)을 제공해요.
tokio::sync::mpsc도 IPC의 한 형태로 보면, 채널을 통해 소유권을 안전하게 넘기는 패턴입니다. 다만 in-process라서 kernel 경계를 안 넘는다는 점만 다릅니다. 면접에서 '왜 같은 머신인데 TCP loopback 대신 Unix domain socket을 쓰는지'를 물으면 파일 권한·지연·헤더 비용 세 마리 토끼를 잡는 답이 나와요.
- 공유 메모리 = 가장 빠름, 동기화 부담 큼. zeromq/nanomsg가 그 위에 추상화
- Unix domain socket은 같은 머신에서 TCP loopback보다 빠르고 권한 검사까지
- FIFO(named pipe)는 파일 시스템 경로로 식별 — 비혈연 프로세스 통신
- Tokio mpsc는 in-process 채널 — IPC 패턴을 단일 프로세스로 끌어온 형태
우리 코드oxios ↔ oxibrain 통신이 정확히 Unix domain socket(RFC-047). TCP loopback보다 지연이 짧고 파일 권한으로 접근을 제어해요.
I/O 모델 (Blocking / Non-blocking / Multiplexing / Async) I/O Models 그림 우리코드
동기 Blocking은 read/write가 데이터 올 때까지 스레드를 묶음. Non-blocking + Multiplexing(select·epoll·kqueue)은 한 스레드가 다수 fd를 감시. Async는 OS가 완료 시 알림을 줌.
Blocking I/O는 가장 단순해요. read()를 부르면 데이터가 도착하거나 EOF까지 호출 스레드가 멈춥니다. 스레드 하나당 연결 하나를 처리해야 하니 C10K(1만 동시 연결) 상황에서 만 개 스레드를 띄우면 컨텍스트 스위칭 비용과 메모리(기본 스택 8MB)로 한계가 와요.
Non-blocking은 '지금 데이터 없음'이면 즉시 EAGAIN을 돌려주는 모드예요. 보통 busy polling처럼 '될 때까지 또 read()'을 쓰면 CPU 낭비가 심합니다. 그래서 I/O Multiplexing — select·poll·epoll·kqueue — 가 등장합니다. select는 fd 셋을 비트마스크로 매번 통째로 커널에 넘기니 O(N)이고 fd 수천 개면 느려져요. epoll(Linux)·kqueue(BSD/macOS)은 커널이 '관심 있는 fd 리스트'를 유지하고 준비된 fd만 알려주니 O(1)에 가깝습니다.
Async I/O(AIO)는 OS가 I/O 완료를 비동기로 알려줘요. read()를 부르면 즉시 리턴하고 나중에 콜백·시그널·Future로 결과를 받습니다. io_uring(Linux 5.1+)는 커널-유저 공유 링버퍼(SQE·CQE)로 syscall 자체를 줄였어요 — submission queue에 요청을 쓰면 커널이 처리해 completion queue에 결과를 채워주니 read·write가 진짜 zero-copy에 가깝게 동작합니다.
tokio는 epoll(Linux)/kqueue(macOS)/IOCP(Windows)를 추상화한 reactor 위에 Future 폴링을 얹은 구조예요. 그래서 'tokio는 async 라이브러리'라기보다 reactor + 멀티스레드 워커 + work-stealing 스케줄러의 합성품이라 보는 게 정확합니다.
- select O(N) vs epoll O(1) — fd 수가 늘면 격차 큼
- io_uring(Linux 5.1+) — submission/completion ring으로 syscall 제거
- libuv(tokio의 epoll 추상화 등 기반)는 플랫폼별 백엔드를 같은 API로 노출
- 비동기 I/O가 빠른 게 아니라 'I/O 대기 동안 다른 일'이 가능해서 효율적
우리 코드tokio의 reactor가 epoll(io_uring 옵션)·kqueue를 추상화. oxi 4프로젝트 동시성 토대예요.
GC vs 소유권 (메모리 관리) GC vs Ownership 그림 우리코드
추적 GC(Java·Go·JS)는 런타임에 도달 불가능 객체를 회수. Rust는 컴파일 타임 소유권·대여 규칙으로 정적 해제해 예측 가능한 성능을 얻습니다.
GC의 본질은 '내가 안 쓸 거'를 자동으로 찾아주는 거예요. mark-and-sweep은 객체 그래프 전체를 root set(전역 변수·스택·레지스터)에서 시작해 reachable을 mark하고 나머지를 sweep합니다. copying collector는 살아있는 객체만 다른 half로 옮기고 옛 영역을 통째로 비워 compaction 효과까지 얻어요. generational(JVM의 Young/Old)은 '대부분 객체가 금방 죽는다'는 weak generational hypothesis를 활용해 young 영역만 자주 수집합니다.
다만 GC는 stop-the-world(STW) — mark/sweep 동안 애플리케이션 스레드가 멈춰요. 짧아도 ms 단위, 큰 힙에선 수십~수백 ms가 나옵니다. 실시간·지연 민감 서비스(금융 HFT·게임·오디오)에서는 이 STW가 직격탄이라 ZGC·Shenandoah처럼 동시 수집(concurrent)으로 줄이거나, Rust처럼 GC 자체를 없애는 길을 택합니다.
Rust의 소유권 규칙은 단순해요. ① 각 값은 정확히 하나의 owner를 갖고, ② owner가 drop되면 값도 drop, ③ borrow는 여러 & 읽기 또는 한 개 &mut 쓰기 — 둘 동시 불가. 컴파일러가 lifetime을 정적으로 검사해서 '값이 이미 drop된 뒤의 참조' 같은 use-after-free를 빌드 단계에서 잡습니다. 그래서 결정적 해제(deterministic drop) — 자원 닫기·lock 해제·fsync 시점이 정확히 그 스코프 끝이라는 보장 — 이 됩니다.
Arc<T>(Atomic Reference Counted)는 멀티스레드용 참조 카운팅이고, Rc<T>는 단일 스레드용이에요. 둘 다 순환 참조가 약점입니다 — A가 B를, B가 A를 Arc로 들고 있으면 카운트가 영원히 0이 안 됩니다. 해결은 `Weak<T>`(약한 참조) — 강한 참조 0이 되면 Weak도 자동으로 무효화돼요. 또는 부모-자식 트리 구조를 유지해 자식에서 부모로의 참조를 Weak로 두는 패턴.
- Mark-and-sweep vs copying vs generational — GC 알고리즘 다양
- Stop-the-world(STW) — GC의 본질적 비용, 동시 수집으로 줄이거나 Rust처럼 회피
- Rust의 drop 순서는 변수의 역순 — NLL(non-lexical lifetimes)이 빌림 범위를 줄임
- Arc/Rc 순환참조는 메모리 누수 — Weak<T>로 끊거나 트리 구조로 설계
우리 코드oxibrowser가 boa_gc(JS 힙)와 Rust ownership을 동시에 운영 — JS는 GC로, Rust는 정적 해제로 분리된 메모리 모델을 한 프로세스 안에 공존시키는 사례예요.
컨텍스트 스위칭 / 시스템 콜 Context Switch / System Call
컨텍스트 스위치는 CPU가 한 실행 흐름에서 다른 흐름으로 제어권을 넘기는 과정. 시스템 콜은 사용자 모드 프로그램이 커널에 서비스를 요청하기 위해 모드 전환을 일으키는 통로.
왜 이게 비싼가부터 보면, 단순한 함수 호출이 아니라 레지스터 통째(PC·SP·상태 레지스터·FP) 저장·복원이 따라붙어요. OS가 다음 스레드의 컨텍스트를 로드하고, 새로 그 스레드의 가상 메모리 맵(페이지 테이블 베이스)을 CR3 레지스터에 갈아끼웁니다. 페이지 테이블이 바뀌면 TLB는 대부분 무효화 — 다음 메모리 접근들이 전부 TLB miss를 겪어요. 거기다 L1·L2 캐시는 워킹셋이 바뀌니 cold miss 연쇄가 발생합니다. 그래서 컨텍스트 스위치 한 번이 보통 1~10µs 사이인데, 단순 함수 호출은 ns 단위예요.
시스템 콜은 다른 종류의 모드 전환입니다. 사용자 모드(user mode) 프로그램은 하드웨어 자원에 직접 접근할 수 없도록 CPU가 막아둡니다 — 이건 안정성·보안의 핵심이에요. 그래서 디스크 읽기·소켓 송수신·프로세스 생성 같은 일은 커널 모드(kernel mode)에서만 가능합니다. 시스템 콜은 user → kernel로 한 번, 결과·errno와 함께 kernel → user로 다시 돌아오며, 이 과정에서 컨텍스트 스위치의 비용에 더해 권한 검사·인자 복사가 따라붙어요.
Linux는 이 비용을 줄이려고 두 방향으로 움직였습니다. 첫째, vDSO(virtual Dynamic Shared Object) — 자주 쓰는 시스템 콜(gettimeofday·clock_gettime·getpid)을 user 모드에서 직접 실행할 수 있는 페이지로 매핑해서 모드 전환 자체를 없애요. 둘째, io_uring(Linux 5.1+) — submission/completion 큐를 user·kernel이 공유 메모리로 같이 보면서, 사용자가 커널에 요청을 '제출'할 때 모드 전환 없이 메모리만 쓰면 됩니다. 결과적으로 syscall이 사용자/커널 경계를 넘던 비용이 사라집니다.
여기서 자주 잊는 함정이 있어요. '스레드를 많이 띄우면 빨라진다'는 오해예요. 실제로는 스레드가 늘어날수록 컨텍스트 스위칭·캐시 오염·락 경합이 늘어나서 throughput이 떨어질 수 있습니다. 그래서 tokio는 수만 개의 task를 실제 OS 스레드 수십 개에 매핑하고, IO Multiplexing으로 같은 스레드에서 I/O 대기를 겹쳐요. 비동기는 '동시에 더 많이 처리한다'가 아니라 '기다리는 동안 다른 일을 한다'는 의미입니다.
- 컨텍스트 스위치는 TLB flush + 캐시 cold miss 연쇄 — µs 단위
- 시스템 콜은 user↔kernel 모드 전환 + 인자 복사 + 권한 검사
- vDSO는 gettimeofday 등을 user 모드에서 직접 실행 — 모드 전환 제거
- io_uring은 user/kernel 공유 링버퍼로 syscall 오버헤드 제거
데이터베이스 13
SI·공공 직무 면접의 80%가 여기서 출발. SQL 한 줄이 어떻게 실행 계획으로 풀리는지, 트랜잭션이 무엇을 보장하는지, 인덱스가 왜 빨라지는지를 그림으로 잡고 가면 면접관이 다음 질문을 던지지 않습니다.
ACID / 트랜잭션 ACID Transactions 직무 그림 우리코드
트랜잭션이 모두 성공하거나 모두 실패하도록 보장하는 네 가지 약속. RDB의 신뢰성 그 자체.
ACID = Atomicity(원자성)·Consistency(일관성)·Isolation(격리성)·Durability(지속성). 트랜잭션 = 더 이상 쪼갤 수 없는 작업 단위.
트랜잭션(transaction)이란 '더 이상 쪼갤 수 없는 작업 묶음'입니다. 계좌 이체를 예로 들면, A 계좌에서 5만 원을 빼고 B 계좌에 5만 원을 넣는 두 SQL은 절대 한쪽만 실행되면 안 됩니다. 중간에 전기가 나가거나 네트워크가 끊겨도요. 이 '전부 아니면 전무(all-or-nothing)' 원칙이 원자성(Atomicity)입니다.
일관성(Consistency)는 트랜잭션 전후로 무결성 제약(잔액 합계 = 총액, 외래 키, CHECK 조건 등)이 깨지지 않음을 의미합니다. 원자성이 '연산의 단위'를 보장한다면, 일관성은 '제약의 불변'을 보장합니다. 격리성(Isolation)은 동시에 도는 트랜잭션들이 서로의 중간 상태를 보지 못하게 막습니다. A가 5만 원을 빼는 SQL을 실행 중일 때, B는 A의 잔액이 이미 줄었다고 잘못 읽으면 안 됩니다.
지속성(Durability)은 커밋된 변경이 시스템이 죽어도 살아남는다는 약속입니다. 보통 WAL(Write-Ahead Logging) 로그를 먼저 디스크에 fsync하고 '성공'이라고 응답하기 때문에, 정전이 와도 로그를 재실행하면 복원됩니다. 네 글자 한 줄 요약하면, A는 '연산 단위'·C는 '제약 불변'·I는 '가시성 차단'·D는 '디스크 영속'입니다.
실무에서 가장 많이 듣는 질문은 "Durability는 어떻게 보장하나요?"입니다. 정답은 두 단계: (1) 변경을 데이터 페이지에 쓰기 전에 WAL 로그에 append, (2) 로그를 디스크로 fsync한 뒤에야 commit OK를 반환. fsync를 안 하면 운영체제 버퍼 캐시에만 남아 있다가 정전 시 사라집니다.
- 원자성 ↔ 롤백 — 부분 적용 불가, 실패 시 모두 되돌림
- 일관성 ↔ 제약 조건(CHECK·FK·UNIQUE) 불변
- 격리성 ↔ 동시 트랜잭션 가시성 차단 (격리 수준으로 강도 조절)
- 지속성 ↔ WAL fsync — 커밋 후에야 데이터 페이지 반영
우리 코드oxicode-mnemopi·oxibrain의 SQLite가 WAL로 Durability를 보장합니다. oximemo는 redb 2를 쓰므로 별도 로그 구조 기반의 다른 종류의 지속성입니다.
관련 항목: 이동
트랜잭션 격리 수준 (Isolation Levels) Transaction Isolation 표 그림 우리코드
동시 트랜잭션 사이의 가시성·충돌 수준. 강할수록 안전하지만 느리고, 약할수록 빠르지만 이상 현상을 감수.
격리 수준 = 동시 트랜잭션 사이 가시성 정책. SQL 표준은 Read Uncommitted / Read Committed / Repeatable Read / Serializable 네 단계.
격리 수준은 '동시에 도는 트랜잭션끼리 얼마나 서로를 보여줄까'의 정책입니다. 가장 약한 Read Uncommitted는 다른 트랜잭션이 아직 커밋 안 한 값도 읽게 허용합니다. 은행 거래 도중 '미리' 잔액이 보인다니 끔찍하지만, 분석용 배치처럼 무결성보다 속도가 더 중요할 때만 쓰지 거의 사용하지 않습니다.
Read Committed는 '커밋된 데이터만 읽기'를 보장합니다. PostgreSQL의 기본값이죠. 하지만 같은 행을 두 번 읽으면 값이 바뀔 수 있습니다. A 트랜잭션이 SELECT name FROM users WHERE id=1을 두 번 했는데, 그 사이에 다른 트랜잭션이 해당 행을 UPDATE해서 두 번째 SELECT에서 다른 값이 나오는 경우 — 이게 non-repeatable read(반복 불가 읽기)입니다.
Repeatable Read는 한 트랜잭션 안에서 같은 쿼리를 두 번 실행해도 값이 같은 행에 한해서는 변하지 않음을 보장합니다. MySQL InnoDB의 기본값입니다. 다만 새로 INSERT된 행이 같은 WHERE 조건에서 갑자기 등장하는 phantom read(유령 읽기)는 막지 못합니다 — 범위 쿼리(예: WHERE age BETWEEN 20 AND 30)에서 발생할 수 있어요.
Serializable은 가장 엄격해서 트랜잭션들을 진짜 한 줄로 직렬화한 것처럼 만듭니다. 모든 이상을 차단하지만, 동시성이 거의 1로 떨어져 성능이 가장 나쁩니다. 일반적인 구현은 두 가지: (1) 2PL 같은 락으로 막거나, (2) MVCC에서 직렬 이상을 발견하면 강제로 트랜잭션을 abort.
MVCC(Multi-Version Concurrency Control)는 Repeatable Read를 구현하는 일반적 기법입니다. 각 행에 트랜잭션 ID를 달고, 어떤 트랜잭션이 시작한 시점에 '살아 있던' 버전만 보여주는 식이에요. 같은 행을 동시에 읽는 reader끼리는 서로를 막지 않고, writer는 새 버전을 만들고 옛 버전은 그대로 둡니다 — 그래서 reader와 writer가 거의 동시에 진행 가능. SQLite WAL도 단순한 형태의 MVCC로, 여러 reader + 단일 writer를 동시에 받습니다.
| 구분 | 특징 | 참고 |
|---|---|---|
| Read Uncommitted | dirty read 가능 | 거의 안 씀 |
| Read Committed | non-repeatable read 가능 | PostgreSQL 기본 |
| Repeatable Read | phantom read 가능 | MySQL InnoDB 기본 |
| Serializable | 모든 이상 차단 | 가장 느림, 가장 안전 |
- dirty read — 미커밋 데이터를 읽음 (Read Uncommitted만 허용)
- non-repeatable read — 같은 행을 두 번 읽었는데 값이 바뀜 (Read Committed까지 허용)
- phantom read — 같은 범위 쿼리인데 새 행이 나타남 (Repeatable Read까지 허용)
- Serializable은 모든 이상 차단 + 성능 최저. MVCC는 락 없이 Read Committed+ 구현
우리 코드oxicode-mnemopi의 SQLite WAL 모드는 reader 여러 개 + writer 단일의 단순 MVCC로 운영됩니다.
관련 항목: 이동
정규화 / ERD / 반정규화 Normalization & ERD 직무 그림 우리코드
1NF(원자값) → 2NF(부분 종속 제거) → 3NF(이행 종속 제거) → BCNF·4NF·5NF. 갱신 이상을 막기 위한 단계적 규칙.
ERD = Entity-Relationship Diagram(엔티티·관계 다이어그램). NF = Normal Form(정규형).
정규화(normalization)란 '같은 사실을 한 군데에만 적자' 원칙을 형식적으로 적용하는 과정입니다. 공공 SI에서 가장 많이 쓰는 모델은 접수 시스템이죠. 학생이 시험 응시를 접수하면, (학생, 시험, 응시이력, 시험장) 네 가지 사실이 생깁니다. 이걸 한 테이블에 다 몰아넣으면 한 학생이 시험을 두 번 봤을 때 이름·주소·전화번호가 행마다 반복되어 들어갑니다 — 이게 1NF 위반의 시작입니다.
1NF는 모든 컬럼이 더 이상 쪼갤 수 없는 원자값(atomic value)이어야 한다는 규칙입니다. '주소 = 서울특별시 강남구 테헤란로 123'처럼 통째로 들어가도 1NF에는 부합합니다(현실에서는 좀 더 분해하는 게 낫지만). 2NF는 '복합 기본 키의 일부에만 종속되는 컬럼을 분리'입니다. 응시 테이블의 (학생ID, 시험ID)가 기본 키라면, '학생이름'은 학생ID에만 종속되니 별도 학생 테이블로 빼야 합니다.
3NF는 '기본 키가 아닌 컬럼끼리 종속되는 경우를 분리'입니다. 학생 테이블에 '소속학과코드'와 '소속학과명'이 같이 있다면 학과명이 학과코드에 종속되니, 학과 테이블을 따로 만들고 학과코드만 남깁니다. 이렇게 3NF까지 가면 '학생 한 명 정보를 바꾸려면 한 행만 UPDATE하면 된다'는 장점이 생깁니다 — 갱신 이상(update anomaly)이 사라지는 거죠.
ERD(Entity-Relationship Diagram)는 엔티티(테이블 후보), 속성(컬럼), 관계(1:1·1:N·N:M)를 그림으로 표현한 산출물입니다. SI 설계 단계의 핵심 문서이고, 한국 공공 사업에서는 거의 항상 마르미-III 기반 설계 산출물에 포함됩니다. 면접에서는 '이 시스템의 ERD를 그려보세요' 또는 '이 테이블들이 3NF인지 검토하세요'가 단골 질문입니다.
반정규화(denormalization)는 의도적으로 중복을 허용하는 겁니다. 정당화되는 시점은 (1) 조인이 너무 비싸서 응답이 느릴 때, (2) 집계 테이블(예: 게시글 수·좋아요 수)을 매번 GROUP BY로 계산하기 부담스러울 때, (3) 읽기 전용 보고서 테이블일 때. 반정규화를 도입하면 쓰기 비용이 늘어난다는 사실 — 학생 테이블의 학과명을 비정규화하면 학과명이 바뀔 때 모든 학생 행을 UPDATE해야 합니다 — 을 명심해야 합니다.
- 1NF → 원자값, 2NF → 부분 종속 제거, 3NF → 이행 종속 제거
- 갱신 이상(삽입·수정·삭제) 3종이 정규화의 존재 이유
- ERD는 SI 설계 산출물의 핵심 — 면접 단골 질문
- 반정규화는 '조인이 비싸질 때' 한정. 쓰기 비용 증가를 감수
우리 코드oxibrain의 projection 테이블(beliefs/statements/entities/episodes)은 Rust 타입 정의에서 SQL 마이그레이션이 자동 생성되는 형태로, 3NF에 가깝게 설계되어 있습니다.
관련 항목: 이동
인덱스 / 실행 계획 / 비용 추정 Indexing & Query Plan 직무 그림
인덱스는 책의 색인처럼 데이터를 빠르게 찾는 보조 구조. 옵티마이저는 통계와 비용 모델로 어떤 인덱스를 쓸지 결정.
B-Tree = Balanced Tree(균형 트리). EXPLAIN = 실행 계획 출력. cardinality = 컬럼 값의 고유성 정도.
인덱스(index)는 책의 찾아보기와 같습니다. 책에서 '트랜잭션'이라는 단어를 찾으려면 처음부터 끝까지 펴보는 거(Full Table Scan)보다 색인을 보는 게 훨씬 빠르죠. 데이터베이스 인덱스도 같은 역할을 합니다 — WHERE 조건에 쓸 수 있는 키-값 쌍을 별도 자료구조(B-Tree, Hash, GIN 등)로 보관해, 디스크 I/O 없이 찾는 위치를 알려줍니다.
B-Tree 인덱스가 범위 스캔에 강한 이유는 정렬된 순서를 유지하기 때문입니다. 인덱스는 내부적으로 키 값 순으로 정렬되어 있어서, 'WHERE created_at BETWEEN 2024-01-01 AND 2024-12-31' 같은 범위 쿼리에서 시작점과 끝점만 찾으면 그 사이를 순차적으로 읽습니다. 반면 Hash 인덱스는 등치(=) 쿼리에만 강하고 범위 쿼리에는 쓸모가 없습니다 — 해시는 순서를 보존하지 않기 때문이죠.
카디널리티(cardinality)는 컬럼이 가질 수 있는 값의 다양성입니다. '성별'은 카드값 2개로 카디널리티가 낮고, '이메일'은 사람마다 다르니 카디널리티가 높습니다. 옵티마이저는 카디널리티가 높은(즉, 값이 다양한) 컬럼에 인덱스를 쓰면 행을 많이 걸러낼 수 있다고 판단해 그 인덱스를 선호합니다. 반대로 카디널리티가 낮은 컬럼(예: Boolean 플래그)에 인덱스를 걸면 전체의 50%만 걸러지니 플래너가 인덱스 사용을 포기할 수 있어요.
커버링 인덱스(covering index)는 쿼리가 참조하는 모든 컬럼이 인덱스 자체에 들어 있어, 데이터 페이지(테이블 본체)를 안 읽고도 결과를 반환하는 인덱스입니다. SELECT id, name FROM users WHERE email=? 쿼리에 email·id·name을 모두 담은 인덱스를 만들면, 디스크 I/O가 한 단계 줄어들어 빠릅니다. EXPLAIN의 Extra 컬럼에 'Using index'라고 뜨면 커버링이 적용된 것입니다.
인덱스가 많으면 항상 좋을까? 아닙니다. 인덱스는 INSERT/UPDATE/DELETE마다 함께 갱신돼야 하니, 쓰기가 많은 테이블에 인덱스를 20개 걸면 매 변경마다 20번의 인덱스 업데이트가 발생합니다. 또한 인덱스 자체가 디스크 공간을 차지합니다. 그래서 '조회 성능이 실제로 필요한 컬럼에만' 인덱스를 거는 게 원칙이고, 안 쓰는 인덱스는 주기적으로 정리해야 합니다.
옵티마이저가 인덱스를 고르는 절차는 크게 세 단계입니다. (1) 파싱으로 WHERE 절에서 인덱스 후보 컬럼 추출, (2) 카탈로그(통계 테이블)에서 카디널리티·히스토그램 조회, (3) 비용 추정(cost estimation) 모델로 후보 계획들의 I/O·CPU 비용을 비교해 최저 비용 계획을 선택. PostgreSQL·MySQL 모두 EXPLAIN (PostgreSQL은 EXPLAIN ANALYZE) 명령으로 이 계획을 사람이 읽을 수 있게 출력해 줍니다.
- B-Tree는 정렬을 보존하므로 범위 스캔에 강함. Hash는 등치만
- 카디널리티가 높은(고유한) 컬럼일수록 인덱스 효율이 좋음
- 커버링 인덱스(Extra: 'Using index')는 데이터 페이지 안 읽음
- 인덱스 과다 = 쓰기 비용 증가. SELECT 패턴이 안정된 컬럼만
관련 항목: 이동
JOIN 종류와 실행 전략 Joins 직무 그림
두 테이블의 행을 짝짓는 세 가지 대표 알고리즘 — 각각 강점이 다른 데이터 크기·정렬 상태에 맞춤.
NLJ = Nested Loop Join(중첩 루프 조인). HJ = Hash Join. SMJ = Sort-Merge Join.
JOIN은 두 테이블의 행을 조건에 맞춰 짝짓는 연산이고, 데이터베이스 옵티마이저는 상황에 따라 세 가지 알고리즘 중 하나를 고릅니다. Nested Loop Join(NLJ)은 가장 단순해서 '안쪽 테이블을 바깥 테이블의 각 행마다 한 번씩 본다'는 구조입니다. NLJ는 안쪽 테이블에 좋은 인덱스가 있으면 빠릅니다 — 1,000 × (인덱스 lookup O(log n)) = 약 10,000번의 비교로 끝나죠.
Hash Join은 양쪽 테이블 중 작은 쪽을 메모리에 해시 테이블로 올려놓고, 큰 쪽을 한 번 스캔하며 매칭을 찾는 방식입니다. 등치(=) 조인에서 두 테이블이 클 때(예: 수백만 행) NLJ보다 훨씬 빠릅니다. PostgreSQL이 NLJ 대신 Hash Join을 자주 선택하는 이유입니다. 다만 메모리에 안 들어가면 디스크로 spill해서 느려집니다.
Sort-Merge Join은 양쪽을 조인 키로 정렬한 뒤 앞뒤로 훑으며 매칭하는 방식입니다. 이미 정렬된 인덱스가 있거나 대량 범위 조인에 효과적입니다. OLAP처럼 큰 테이블끼리 만날 때 자주 등장합니다.
MySQL은 거의 항상 NLJ를 씁니다(블록 기반 NLJ 변형 포함). 옵티마이저가 다른 알고리즘을 선택하는 일이 드뭅니다. 그래서 실무에서는 '작은 테이블을 드라이빙 테이블로 두고, 안쪽 테이블의 조인 컬럼에 인덱스가 살아 있는지'를 점검하는 게 핵심입니다. PostgreSQL은 Hash Join·Merge Join도 능숙하게 골라서, 대용량 분석 쿼리에서 성능 차이가 큽니다.
면접 단골은 (1) JOIN 실행 알고리즘의 차이, (2) 인덱스 힌트(use index, /*+ ORDERED */)로 옵티마이저를 강제하는 게 언제 정당화되는가, (3) EXPLAIN에서 type=ref·eq_ref·ALL의 의미, (4) N+1 문제(루프 안에서 매번 SELECT) 해결 — JOIN 한 방으로 줄이거나 IN 절 배치 쿼리로 바꾸기.
- NLJ — 단순, 작은·인덱스 있는 안쪽 테이블에 강함 (MySQL 기본)
- Hash Join — 등치 + 큰 테이블, 메모리 한도 주의 (PostgreSQL 빈번)
- Sort-Merge Join — 이미 정렬됐거나 대량 범위 조인
- type=ALL은 풀 테이블 스캔. type=ref·eq_ref·const는 인덱스 활용
관계형 vs NoSQL / CAP RDB vs NoSQL 직무 그림 우리코드
RDB는 스키마·트랜잭션·조인 강점. NoSQL은 유연한 스키마·수평 확장 강점. CAP는 분산 환경에서의 불가침 트레이드오프.
CAP = Consistency(일관성)·Availability(가용성)·Partition tolerance(분할 내성). RDB = Relational Database.
관계형 데이터베이스(RDB)는 스키마를 먼저 정의하고 그 틀 안에서 데이터를 넣습니다. JOIN·트랜잭션·무결성 제약이 강점이고, 그래서 공공 SI처럼 '돈과 사람이 걸린 데이터'에서는 압도적으로 많이 씁니다. 30년 넘게 검증됐고, ORM·마이그레이션·모니터링 도구가 풍부하기 때문이기도 합니다.
NoSQL은 'Not Only SQL'의 줄임말로, RDB와 다른 형태를 묶습니다. 키-값(Redis, DynamoDB)은 캐시·세션처럼 단순 조회, 문서(MongoDB, CouchDB)는 JSON처럼 유연한 스키마, 컬럼 패밀리(Cassandra, HBase)는 시계열·로그처럼 넓은 행, 그래프(Neo4j)는 관계 따라가기. 각각 RDB가 못하는 영역을 잘하지만, 일반적으로 JOIN·트랜잭션이 약하거나 없습니다.
CAP 정리(2000, Eric Brewer)는 분산 시스템에서 일관성(C)·가용성(A)·분할 내성(P) 셋을 동시에 만족할 수 없다는 명제입니다. 네트워크 분할(P)이 발생하면, 일관성과 가용성 둘 중 하나를 골라야 합니다. CP는 분할 시 일관성을 우선해서 일부 노드가 응답을 거부하고, AP는 가용성을 우선해서 분할된 노드도 계속 응답하되 데이터가 서로 갈릴 수 있습니다.
여기서 면접관이 자주 노리는 함정이 있습니다 — 'CAP에서 셋 중 두 개를 고른다'고 외우기 쉬운데, 정밀하게 말하면 P는 거의 항상 발생합니다. 실제 선택은 분할이 일어났을 때 'C를 포기할 거냐(AP로 갈 거냐)' 아니면 'A를 포기할 거냐(CP로 갈 거냐)'입니다. 그래서 RDB 클러스터는 보통 CP 지향이고(다수 노드가 죽으면 응답 거부), DynamoDB·Cassandra는 AP 지향입니다(죽어도 응답은 하되 일관성이 늦게 수렴).
공공 SI에서는 무결성이 우선이라 RDB가 표준입니다. NoSQL이 들어오는 경우는 보통 로그·세션·검색 색인 같은 보조 저장소입니다. oxibrain·oxicode-mnemopi는 SQLite(임베디드 RDB), oximemo는 redb(임베디드 키-값)인데, 셋 다 임베디드 단일 파일 DB라서 별도 서버 운영이 필요 없습니다.
- RDB = 스키마·JOIN·트랜잭션. 공공 SI의 표준
- NoSQL 분류: 키값(Redis)·문서(Mongo)·컬럼 패밀리(Cassandra)·그래프(Neo4j)
- CAP — 분할(P)은 항상 발생 → 실질 선택은 CP vs AP
- 공공은 무결성 우선이라 RDB가 압도적. NoSQL은 보조 저장소
우리 코드oxibrain·oxicode-mnemopi는 SQLite, oximemo는 redb 2 — 셋 다 임베디드 단일 파일 DB라 별도 서버 운영이 없습니다.
관련 항목: 이동
전문 검색 / BM25 / 역인덱스 Full-Text Search 자소서 직무 그림 우리코드
역인덱스(inverted index)로 키워드 검색을 가속. BM25는 단어 빈도·문서 길이를 정규화한 랭킹 함수로 '단어 도배'를 억제.
BM25 = Best Matching 25(1990년대 TREC에서 가장 잘 작동한 25번째 변형 랭킹 함수). FTS = Full-Text Search.
전문 검색(full-text search)이란 '문서 안의 어떤 단어가 포함되어 있는가'를 빠르게 찾는 기능입니다. SQL의 LIKE '%키워드%'는 풀 테이블 스캔이라 느리고, 형태소 분석이나 띄어쓰기 처리도 안 됩니다. 그래서 검색 전용 인덱스인 역인덱스(inverted index)를 씁니다. 책 뒤의 찾아보기를 생각하면 됩니다 — '트랜잭션'이라는 단어 → 트랜잭션이 등장하는 문서 번호 목록.
역인덱스를 만들 때 가장 큰 결정이 하나 있습니다 — 어떤 단위로 색인을 쪼갤까. 영어는 공백 기준으로 쉽지만, 한글은 '데이터베이스 트랜잭션'을 한 단어로 볼지 '데이터'·'베이스'·'트랜잭션' 세 단어로 볼지 결정해야 합니다. 한글에서 흔히 쓰는 방법은 (1) 형태소 분석기(KoNLPy, MeCab)로 어절을 자르거나, (2) n-gram으로 2~3자씩 겹쳐 잘라 색인을 만듭니다. 전자보다 후자가 단순하지만 '대한민국'과 '민국'이 따로 잡히는 등 노이즈가 낍니다.
BM25는 검색 결과를 점수 매길 때 쓰는 랭킹 함수입니다. 핵심 아이디어 두 가지 — (1) 단어가 문서에 많이 등장할수록 점수가 높지만(단어 빈도 TF), 한없이 올라가진 않습니다(포화 곡선). '트랜잭션'이 5번 나왔을 때와 50번 나왔을 때 점수 차이는 선형이 아니라 점점 작아진다는 뜻이죠. (2) 긴 문서가 짧은 문서보다 단어가 더 많이 등장하는 건 자연스러우니, 문서 길이로 정규화해 '짧은 문서가 정확히 그 단어를 담고 있을 때' 더 높은 점수를 줍니다.
전통 검색 엔진으로는 Elasticsearch(Lucene 기반)가 사실상 표준이고, Tantivy는 Rust로 작성된 in-memory BM25 구현체입니다. SQLite도 FTS5 모듈로 내장 검색을 제공합니다 — oxicode-mnemopi가 바로 그 FTS5를 쓰고 있고, oximemo는 tantivy로 BM25를, oxibrain은 가벼운 n-gram Jaccard로 키워드 유사도를 계산합니다. 한 프로젝트 안에 BM25·n-gram이 공존하는 모습이라 각 기법의 트레이드오프를 그대로 보여 주는 사례입니다.
- 역인덱스 = '단어 → 문서 목록' 매핑 (책의 찾아보기)
- BM25 — TF 포화 + 문서 길이 정규화로 도배 억제
- 한글은 형태소 분석기(KoNLPy) vs n-gram(2~3자) 트레이드
- Elasticsearch(Lucene) / Tantivy(Rust) / SQLite FTS5 — 구현 다양
우리 코드oxicode-mnemopi는 SQLite FTS5, oximemo는 tantivy 0.22의 BM25, oxibrain은 n-gram Jaccard로 검색합니다. 한 프로젝트 안에 BM25·n-gram이 공존해 각 기법의 트레이드오프가 그대로 드러납니다.
관련 항목: 이동
커리큘럼 체인 RAG 파이프라인
벡터 데이터베이스 / HNSW / ANN Vector DB 자소서 직무 그림 우리코드
임베딩 벡터의 근접 이웃 검색 전용 DB. HNSW 등 ANN 알고리즘으로 정확 KNN을 일부 양보하고 속도를 크게 얻음.
ANN = Approximate Nearest Neighbor(근사 최근접 이웃). HNSW = Hierarchical Navigable Small World(계층적 그래프 ANN 알고리즘). KNN = K-Nearest Neighbors(정확 최근접).
임베딩(embedding)이란 텍스트·이미지·음성을 고정 길이 벡터(예: 768차원, 1536차원)로 변환한 것입니다. '고양이'와 '강아지'는 벡터 공간에서 가깝게, '고양이'와 '비행기'는 멀게 위치해 의미적 유사성을 거리로 잴 수 있게 됩니다. 문제는 벡터 하나당 768개 실수 = 약 6KB이고, 수백만 건이 있으면 정확히 가장 가까운 K개를 찾는 KNN 검색이 사실상 불가능하다는 거예요.
그래서 등장한 게 ANN(Approximate Nearest Neighbor) — '100% 정확' 대신 '충분히 가까운' 이웃을 빠르게 찾는 알고리즘들입니다. 대표 구현은 HNSW(Hierarchical Navigable Small World)인데, 핵심 아이디어는 노드들을 여러 층의 그래프로 연결해 최상층에서 시작해 아래로 내려가며 탐색 범위를 좁히는 것입니다. 시간 복잡도는 평균 O(log n)이고, 메모리 사용량은 정확 KNN의 약 1.5~2배입니다.
HNSW의 트레이드오프 세 가지: (1) efSearch 파라미터로 탐색 후보 수를 늘리면 정확도 ↑ 속도 ↓, (2) M 파라미터로 노드당 연결 수를 늘리면 정확도 ↑ 메모리 ↑, (3) 그래프 구축은 한 번이면 되지만 새 데이터를 삽입하면 그래프 재구축 또는 점진적 삽입이 필요. 메모리가 부족한 환경에서는 IVF(Inverted File Index) + PQ(Product Quantization)로 벡터를 압축해 쓰는 방법이 대안입니다.
벡터 DB는 전용 서버형(Pinecone·Qdrant·Weaviate·Milvus)과 RDB 확장형(pgvector·sqlite-vec)으로 나뉩니다. oxibrain은 sqlite-vec를 써서 벡터 색인을 SQLite 안에 통합했고, 별도 DB 서버 운영 없이 RAG 파이프라인을 구성합니다. 면접 포인트는 '왜 pgvector·sqlite-vec 같은 RDB 확장이면 충분한 경우가 많은가'입니다 — 기존 트랜잭션·JOIN·백업 인프라를 재사용할 수 있고, 별도 클러스터 운영 부담이 없기 때문입니다.
- KNN은 정확·느림. ANN은 근사·빠름. HNSW는 O(log n)
- HNSW 트레이드오프 — efSearch·M 파라미터로 정확도·메모리·속도 조절
- 메모리 압박 시 IVF + PQ로 벡터 압축
- sqlite-vec·pgvector는 RDB 확장 — 기존 인프라 재사용
우리 코드oxibrain이 sqlite-vec로 벡터 색인을 SQLite 안에 통합합니다. 별도 벡터 DB 서버 운영 없이 RAG 파이프라인을 구성할 수 있습니다.
커리큘럼 체인 RAG 파이프라인
복제 / 샤딩 / 파티셔닝 Replication / Sharding / Partitioning 그림
복제는 가용성·읽기 분산, 샤딩은 쓰기 확장, 파티셔닝은 단일 DB에서의 관리 효율을 노린다.
Replication = 동일 데이터 복제본을 여러 노드에 둠. Sharding = 데이터를 노드별로 분할. Partitioning = 한 DB 안에서 데이터를 분할.
데이터가 늘어나면 한 서버로는 부족해집니다. 이때 쓰는 세 가지 전략이 복제(replication)·샤딩(sharding)·파티셔닝(partitioning)입니다. 복제는 같은 데이터를 여러 노드에 복제해서 한 노드가 죽어도 다른 노드가 응답하게 하고(가용성), 읽기 요청을 여러 노드로 분산시킵니다(읽기 확장). 쓰기는 보통 단일 마스터에서만 받거나(Master/Replica), 여러 마스터가 받거나(Multi-Master), 정족수(quorum)로 합의합니다(Dynamo-style).
샤딩은 데이터를 노드별로 '쪼개는' 겁니다. 예를 들어 user_id 0~1만은 노드 A, 1만~2만은 노드 B, 이렇게 분할해 쓰기 부하를 분산합니다. 각 노드는 전체 데이터의 일부만 가지니 한 노드가 죽으면 해당 데이터가 잠깐 사용 불가일 수 있고 — 이게 가용성 트레이드오프입니다. 라우팅(어떤 키가 어떤 노드로 가야 하는가)이 운영의 핵심이고, 데이터가 한쪽으로 몰리는 핫스팟을 막기 위해 일관 해시(consistent hashing)로 키를 균등 분산합니다.
파티셔닝은 샤딩과 비슷하지만 보통 한 DB 서버 안에서 데이터를 나눕니다. MySQL의 range partitioning(예: created_at 기준 월별 파티션), PostgreSQL의 declarative partitioning이 대표입니다. 장점은 (1) 오래된 파티션을 빠르게 DROP(아카이빙)할 수 있고, (2) 인덱스가 파티션마다 따로 있으니 풀 인덱스 스캔 대신 해당 파티션만 스캔합니다. 단점은 JOIN·트랜잭션이 파티션 경계를 넘으면 비싸진다는 점입니다.
실무 면접에서 자주 묻는 건 '마스터/슬레이브 리플리케이션의 lag이 생기면 어떤 문제가 있는가'입니다. 슬레이브가 마스터를 따라잡지 못하면 방금 쓴 데이터를 슬레이브에서 못 읽을 수 있고, 읽기 일관성이 깨집니다. 해결책은 (1) read-your-writes 일관성: 본인 쓴 데이터는 마스터에서 읽기, (2) CDC(Change Data Capture)로 비동기 전파, (3) Semi-sync 복제로 최소 한 노드 동기화 대기.
- 복제 = 가용성·읽기 분산. 마스터/슬레이브·Multi-Master·Leaderless quorum
- 샤딩 = 쓰기 확장. 일관 해시로 핫스팟 방지
- 파티셔닝 = 한 DB 내 분할. 오래된 파티션 DROP이 빠르고 인덱스가 가벼움
- 복제 lag은 read-your-writes 불일치를 일으킴
OLTP vs OLAP / HTAP OLTP vs OLAP 그림 우리코드
OLTP는 단건 트랜잭션이 빠르고 동시성이 높음. OLAP는 대량 분석에 강하고 컬럼형 저장이 유리. HTAP는 둘 다 한 시스템에서.
OLTP = Online Transaction Processing(행 단위 갱신 트랜잭션). OLAP = Online Analytical Processing(대량 집계 분석). HTAP = Hybrid Transaction/Analytical Processing(둘 다 한 시스템에서).
데이터베이스 사용 패턴은 크게 두 갈래입니다. OLTP는 '한 건의 거래'를 빠르게 — 은행 계좌 이체, 쇼핑몰 주문 처리, 접수 시스템 등록처럼 행 단위로 INSERT/UPDATE/DELETE가 많고 동시 접속자가 많습니다. 응답 시간은 수십 ms 수준이어야 합니다. 이런 시스템은 행 단위 저장이 효율적입니다 — 한 행의 컬럼들을 디스크 페이지 하나에 함께 두면, 그 행만 업데이트할 때 한 페이지만 쓰면 되니까요.
OLAP는 '대량 데이터에서 의미 찾기' — 매출 보고서, 사용자 행동 분석, 사기 탐지처럼 수백만~수십억 행을 GROUP BY·SUM·JOIN으로 집계합니다. 한 행의 일부 컬럼만 읽는 일이 많죠. 그래서 컬럼형 저장(columnar storage, Parquet·ORC·Arrow)이 효율적입니다. 같은 컬럼의 값들이 연속으로 저장되어 압축률이 높고, 필요한 컬럼만 읽으면 됩니다(벡터화 실행).
전통적으로 OLTP는 MySQL·PostgreSQL·Oracle 같은 RDB, OLAP는 별도 데이터 웨어하우스(Snowflake·BigQuery·ClickHouse)로 분리했습니다. ETL로 데이터를 옮겨 적재하는 과정이 필요했죠. HTAP(Hybrid Transaction/Analytical Processing)는 이 분리를 없애는 시도입니다 — TiDB·ClickHouse·SingleStore 같은 시스템이 OLTP·OLAP를 한 클러스터에서 처리합니다. 데이터 신선도(분석 결과가 즉시 반영) 측면에서 강점이 있지만, 자원 경합을 잘 관리해야 합니다.
공공 SI에서는 OLTP가 압도적입니다. oxibrain의 벡터 색인·에피소드 로그는 OLTP 성격이 강하고, 분석(예: 특정 사용자의 패턴 통계)은 보통 애플리케이션 레벨에서 처리합니다. 수십억 건 단위로 분석이 필요해지면 별도 OLAP 데이터 웨어하우스로 옮기는 게 비용 대비 효율이 좋습니다.
- OLTP — 행 단위 갱신, 동시성·응답 시간 중요. RDB 적합
- OLAP — 대량 집계. 컬럼형 저장(Parquet·ORC·Arrow) + 벡터화 실행
- HTAP — OLTP·OLAP를 한 시스템에서. 자원 경합 주의
- OLAP에서 star schema·snowflake schema — 사실 테이블 + 차원 테이블
우리 코드oxibrain의 벡터 색인·에피소드 로그는 OLTP이지만, 대규모 분석은 애플리케이션 레벨에서 처리합니다. 수십억 건 단위로 가면 별도 OLAP이 비용 대비 효율적입니다.
Lock Manager / 2PL / 데드락 처리 Lock Manager 그림
잠금 매니저는 트랜잭션 격리에 필요한 행·테이블·페이지 단위 잠금을 관리. 2PL은 잠금이 잠금만 풀 수 있는 단계를 보장.
2PL = Two-Phase Locking(두 단계 잠금 — 성장 단계와 수축 단계).
Lock Manager는 데이터베이스 내부의 잠금(lock) 관리자입니다. 트랜잭션이 행을 읽거나 쓰려면 먼저 적절한 종류의 잠금을 요청하고, 매니저는 그 잠금이 다른 트랜잭션과 충돌하지 않는지 검사해 부여합니다. 잠금 종류는 크게 공유 잠금(Shared Lock, S-lock — 읽기용, 여러 트랜잭션이 동시에 보유 가능)과 배타 잠금(Exclusive Lock, X-lock — 쓰기용, 한 트랜잭션만 보유 가능)으로 나뉩니다.
2PL(Two-Phase Locking)은 트랜잭션이 잠금을 '모두 획득하는 단계(growing phase)'와 '모두 해제하는 단계(shrinking phase)'로 나눠서 진행하도록 강제하는 규칙입니다. 잠금을 얻으면서 풀지 않는 게 핵심 — 잠금을 한 번 풀기 시작하면 새로운 잠금을 요청할 수 없습니다. 2PL을 따르면 직렬 가능성(serializability)이 보장됩니다 — 트랜잭션들의 직렬 실행과 같은 결과를 얻을 수 있다는 뜻이죠.
데드락(deadlock)은 두 트랜잭션이 서로가 가진 잠금을 기다리며 영원히 멈춘 상태입니다. A가 row1을 잡고 row2를 기다리는데 B는 row2를 잡고 row1을 기다리는 식입니다. 해결 방법은 두 가지 — (1) 예방(WAIT-DIE: 오래된 트랜잭션은 기다리고, 어린 트랜잭션은 죽임. WOUND-WAIT: 반대로 오래된 것이 어린 것을 죽이게 함), (2) 검출·해소: 잠금 그래프에서 사이클을 찾아 한 트랜잭션을 강제 abort.
낙관적 동시성 제어(optimistic concurrency control, OCC)는 잠금을 안 쓰고 버전 컬럼을 둬서, 커밋 시점에 버전이 일치하는지 검사합니다. 충돌하면 트랜잭션을 abort하고 재시도하죠. 잠금 비용이 없어서 읽기 위주·충돌 드문 워크로드에 강합니다. 반대로 비관적 동시성(2PL)은 충돌이 잦은 워크로드에서 재시도 비용을 아낍니다.
면접 포인트: (1) 데드락 로그에서 두 트랜잭션이 어떤 잠금을 어떤 순서로 잡았는지 분석하는 법, (2) 분산 잠금(Redlock 등)은 시계·네트워크 분할에 견고하지 못하다는 단점 — 단일 노드 잠금으로 충분하면 그쪽이 안전, (3) 격리 수준이 높아질수록(Serializable) 잠금이 더 자주, 더 오래 유지된다는 사실.
- 잠금 종류 — S-lock(공유)·X-lock(배타). 충돌 매트릭스 암기
- 2PL = growing phase + shrinking phase. 직렬 가능성 보장
- 데드락 처리 — WAIT-DIE·WOUND-WAIT(예방) vs 사이클 검출·abort(해소)
- 낙관적 동시성(OCC) — 버전 컬럼 + 커밋 시 검사. 충돌 드문 워크로드에 강함
WAL (Write-Ahead Logging) Write-Ahead Logging 그림 우리코드
데이터 변경을 로그에 먼저 쓰고 데이터 파일엔 나중에 반영. 크래시 복구·동시성을 동시에 잡는 핵심 기법.
WAL = Write-Ahead Logging(데이터 페이지에 쓰기 전에 로그를 먼저 기록).
WAL(Write-Ahead Logging)의 핵심 규칙은 '데이터 페이지보다 로그를 먼저 디스크에 쓰자'입니다. 순서가 왜 중요할까요? 정전이 와서 메모리에 있던 데이터 페이지가 날아갔다고 합시다. WAL 로그가 있으면, 시스템 재시작 시 로그를 순서대로 다시 적용(redo)해서 데이터 페이지 상태를 복원할 수 있습니다. 로그가 없으면 마지막 커밋 이후의 변경이 전부 사라집니다.
SQLite의 WAL 모드는 추가로 읽기·쓰기 동시성을 얻습니다. 전통적 롤백 저널 모드는 한 번에 한 트랜잭션만 일할 수 있는데, WAL 모드는 (1) 변경을 별도 WAL 파일에 append하고 (2) reader는 기존 데이터 파일을 그대로 읽으면서 (3) writer는 WAL에 쓰기 때문에 reader와 writer가 서로를 막지 않습니다. 다만 WAL 파일이 무한히 커지지 않도록 checkpoint 주기로 데이터 파일에 반영하고 WAL을 자릅니다.
fsync 정책이 durability와 성능의 결정적 트레이드오프입니다. 매 커밋마다 fsync를 호출하면 디스크가 확실히 받지만 느립니다(수 ms~수십 ms). 반면 fsync를 안 하면 운영체제 버퍼 캐시에만 남아 있다가 정전 시 사라집니다. PostgreSQL은 synchronous_commit 옵션으로 on(매번 fsync)/off(버퍼 캐시 신뢰)를 선택할 수 있고, MySQL InnoDB는 innodb_flush_log_at_trx_commit으로 0/1/2 세 단계를 줍니다.
oxicode-mnemopi와 oxibrain은 SQLite를 쓰고 둘 다 WAL 모드로 운영됩니다. 그래서 (1) 단일 writer + 다수 reader의 동시성과 (2) 정전 후에도 마지막 커밋까지 살아남는 durability를 동시에 얻습니다. oximemo는 다른 저장 엔진인 redb 2를 쓰는데, redb는 ACID를 페이지 단위 copy-on-write로 보장하는 별도 메커니즘을 씁니다(SQLite의 WAL과 직접 비교 대상이 아닙니다). 면접에서는 'SQLite의 WAL은 어떤 종류의 durability를 보장하는가'를 묻는 게 단골입니다.
- WAL = 데이터 페이지 변경 전 로그 append. 크래시 시 redo로 복구
- SQLite WAL 모드 — 다수 reader + 단일 writer 동시 진행
- fsync 정책이 durability vs 성능 결정 (PostgreSQL synchronous_commit, MySQL innodb_flush_log_at_trx_commit)
- checkpoint로 WAL 파일 압축. 무한 증가 방지
우리 코드oxicode-mnemopi·oxibrain의 SQLite는 WAL 모드로 Durability를 보장합니다. oximemo는 redb 2를 쓰므로 SQLite의 WAL과는 다른 종류의 로그 구조 기반 영속성입니다.
커넥션 풀 / Prepared Statement Connection Pool / Prepared Statement
DB 연결은 비싸다 — 인증·핸드셰이크에 시간. 풀은 연결을 재사용하고, prepared statement는 SQL을 미리 파싱·컴파일해 캐시.
JDBC = Java Database Connectivity(자바 표준 DB 접근 API). HikariCP = '히카리'로 발음 — 유명 자바 커넥션 풀 라이브러리.
데이터베이스에 연결하는 건 의외로 비싼 작업입니다. 클라이언트가 서버에 TCP로 붙고, 인증(아이디·비밀번호 또는 TLS 클라이언트 인증), 핸드셰이크(서버 버전·인코딩 확인), 세션 변수 설정이 순서대로 일어납니다. 같은 DB라면 수 ms~수십 ms가 매 연결마다 누적되죠. SI 공공 프로젝트에서 트래픽이 몰리는 순간(예: 민원 신고 접수 시작 직후) 매 요청마다 새 연결을 만들면 응답이 바로 느려집니다.
그래서 등장한 게 커넥션 풀(connection pool)입니다. 미리 일정 개수의 연결을 만들어 풀에 보관했다가, 요청이 오면 풀에서 빌려주고 작업이 끝나면 반납받습니다. HikariCP(Java 진영 사실상 표준), c3p0, Tomcat JDBC Pool이 있고, PostgreSQL은 PgBouncer, MySQL은 ProxySQL 같은 외부 풀도 있습니다. 풀 튜닝의 핵심은 min(최소 유지)·max(최대 동시)·connectionTimeout(대기 한도)·idleTimeout(반납 후 폐기 시간)입니다.
min/max가 미스매치되면 문제가 생깁니다. min이 너무 크면 평소엔 안 쓰는 연결이 DB 쪽 자원을 점유하고, max가 너무 작으면 트래픽이 몰릴 때 요청이 timeout으로 실패합니다. 일반적으로 min은 평상시 부하 수준, max는 순간 최대 부하 수준으로 잡고, connectionTimeout은 너무 길게 두면 응답이 그만큼 지연되니 1~5초 정도로 짧게 둡니다.
Prepared Statement는 같은 SQL을 반복 실행할 때 효과적입니다. 일반 SQL(Statement)은 매번 (1) 파싱, (2) 옵티마이저 실행 계획 수립, (3) 실행을 거치지만, prepared statement는 처음 한 번만 파싱·계획 수립을 하고 서버 측에 캐시합니다. 이후 실행은 (1) 파라미터 바인딩, (2) 실행만 거치니 빠릅니다. PostgreSQL의 PREPARE·EXECUTE, MySQL의 Prepared Statements, JDBC의 java.sql.PreparedStatement가 같은 개념입니다.
그리고 더 중요한 보안 이득이 있습니다. Prepared Statement는 SQL 인젝션(SQL Injection) 방어의 표준입니다. WHERE name='"+userInput+"'처럼 문자열을 이어 붙이면, userInput에 '; DROP TABLE users; --가 들어올 때 쿼리가 조작됩니다. Prepared Statement는 파라미터를 별도 채널로 보내 서버에서 값으로만 처리하니, 코드가 어떻게 바뀌어도 인젝션이 구조적으로 불가능해집니다. 그래서 OWASP도 SQL 인젝션 방어 1순위로 prepared statement를 권합니다.
- DB 연결은 인증·핸드셰이크로 비싸다(ms~수십 ms). 풀로 재사용
- HikariCP min/max 튜닝 — 너무 작으면 timeout, 너무 크면 DB 자원 낭비
- Prepared Statement = 파싱·실행 계획 캐시로 같은 SQL 반복 시 빠름
- Prepared Statement = SQL 인젝션 방어의 1순위 (파라미터 바인딩)
소프트웨어 엔지니어링 16
설계 원칙(SOLID·디자인 패턴)부터 테스트 피라미드, CI/CD, 배포 전략, 옵저버빌리티까지 — SI 현업과 'AI 융합 개발자' 4단계 교육목표가 만나는 코어 백과.
SOLID 원칙 SOLID Principles 그림 우리코드
객체지향 설계의 다섯 가지 원칙 — SRP·OCP·LSP·ISP·DIP. 변경의 이유가 하나가 되게, 확장엔 열리고 수정엔 닫히게, 상속은 정확히 치환 가능하게, 인터페이스는 작게, 의존은 추상화에.
SOLID = SRP(Single Responsibility, 단일 책임)·OCP(Open/Closed, 개방-폐쇄)·LSP(Liskov Substitution, 리스코프 치환)·ISP(Interface Segregation, 인터페이스 분리)·DIP(Dependency Inversion, 의존 역전). 로버트 C. 마틴이 정리한 객체지향 설계 5원칙.
왜 SOLID가 등장했는지부터 보면 — 1990년대 C++/Smalltalk 프로젝트들이 '요구사항이 바뀌면 코드가 무너진다'는 문제를 반복하더라고요. 마틴이 2000년대 초 '소프트웨어의 변화 비용을 최소화하는 설계 원칙'으로 정리한 게 SRP부터 시작된 5개 원칙입니다. 단순한 원칙들이지만 합쳐지면 '변화에 강한 구조'가 됩니다.
하나씩 짚어보면, SRP(단일 책임)는 '클래스를 변경해야 하는 이유가 한 가지여야 한다'는 뜻이에요. 사용자·결제·로깅을 한 클래스에 다 묶으면 어느 하나가 바뀔 때 다른 것까지 영향을 받아 테스트가 깨지고 배포가 겹칩니다. OCP(개방-폐쇄)는 '확장엔 열려, 수정엔 닫힌' 구조 — 다형성과 인터페이스로 새 기능을 추가하되 기존 코드는 안 건드리는 게 핵심이에요. LSP(리스코프 치환)는 하위 타입이 상위 타입을 완벽히 대체해야 한다는 원칙으로, 흔히 빠지는 함정은 '직사각형-정사각형' 문제처럼 상속 관계에서 사전조건이 더 강해지는 경우입니다.
ISP(인터페이스 분리)는 '사용하지 않는 메서드에 의존하지 마라' — 거대한 I/F를 작은 역할 단위로 쪼개라는 거예요. 마지막 DIP(의존 역전)가 실무에서 가장 자주 쓰이는 원칙인데, '고수준 모듈이 저수준 구현에 직접 의존하지 말고, 둘 다 추상화(인터페이스/트레이트)에 의존하라'는 원칙입니다. 헥사고날 아키텍처나 포트·어댑터(ports & adapters) 패턴이 DIP의 구조적 표현이에요.
현실적으로 다섯 개를 100% 지키는 건 불가능하고, 우선순위는 SRP > DIP > OCP > ISP > LSP 순으로 적용하는 게 보통 효율적이에요. 면접에서는 'SOLID 다섯 개를 한 문장씩 요약하라'는 질문이 거의 정석이고, 실제 코드 한 조각을 보여주며 어느 원칙이 깨졌는지 찾는 문제가 자주 나옵니다.
- SRP — 클래스는 하나의 책임만, '변경의 이유가 하나인가?'가 체크 기준
- OCP — 확장엔 열려 수정엔 닫힘, 다형성 + 인터페이스로 새 기능 추가
- DIP — 고수준이 저수준에 의존하지 않음(헥사고날·ports/adapters 구조적 표현)
우리 코드oxicode-sdk의 16 port trait이 DIP의 구조적 표현 — 도메인 코드가 구체 어댑터(SQLite·redb·tantivy·FS 등)에 의존하지 않고 MemoryStore·EmbeddingPort 같은 trait에만 의존.
관련 항목: 이동
디자인 패턴 (Gang of Four) Design Patterns (GoF) 표 그림 우리코드
반복되는 설계 문제의 검증된 해법 — 생성(Builder·Factory), 구조(Adapter·Decorator), 행위(Observer·Strategy·State). 패턴은 '검증된 약속'이므로 실무 맥락에서 활용.
GoF = Gang of Four(에리히 감마·리처드 헬름·랄프 존슨·존 블리시디스 4인조). 23개 패턴을 '생성(Creational)·구조(Structural)·행위(Behavioral)' 3분류로 정리한 1994년 책.
디자인 패턴이 왜 중요하냐면 — 라이브러리·프레임워크의 이름이 패턴 이름에서 파생된 경우가 많기 때문입니다. MVC·DAO·Repository·Factory·Observer 같은 이름은 면접에서 '설계 의도를 짧게 설명하라'는 질문의 단축키가 돼요. GoF 책은 1994년 발행된 C++/Smalltalk 기반의 23개 패턴 카탈로그인데, 지금도 분류 체계(생성/구조/행위)는 그대로 살아남았습니다.
생성(Creational) 패턴은 객체 생성 방식을 추상화한 거예요. Builder는 복잡한 객체 조립을 단계로 쪼개고(Kotlin DSL·Rust derive), Factory는 생성 책임을 별도 함수/클래스에 위임해 'new'를 캡슐화합니다. 구조(Structural) 패턴은 클래스·객체를 합성해 더 큰 구조를 만들어요. Adapter는 호환되지 않는 인터페이스를 감싸 결합하고, Decorator는 상속 없이 행동을 입힙니다.
행위(Behavioral) 패턴은 책임과 통신을 다뤄요. Observer는 1:N 이벤트 구독(콜백·이벤트 버스), Strategy는 알고리즘을 객체로 만들어 교체 가능하게 하고, State는 상태에 따라 행동을 바꿉니다. 이 세 카테고리는 면접 단골 — '생성/구조/행위의 차이 한 줄로 답하기'가 첫 질문으로 자주 나오고요.
반드시 알아야 할 안티 패턴도 있어요: God Object(하나가 모든 걸 안다), Spaghetti Code(제어 흐름 추적 불가), Premature Optimization(측정 없이 최적화부터). 면접에서는 패턴을 나열하기보다 '실제 마주친 문제를 어떤 패턴으로 풀었는지' 사례가 훨씬 강력해요.
| 구분 | 특징 | 참고 |
|---|---|---|
| Builder | 복잡 객체 조립 단계 분리. Kotlin DSL·Rust derive | OxicodeBuilder |
| Adapter | 호환되지 않는 인터페이스 결합 | oxicode-lsp thin adapter |
| Observer | 이벤트·상태 변화 구독 | oxios EventBus |
| Strategy | 알고리즘 교체 | oxicode-ai Provider trait |
| Facade | 복잡 하위 시스템 단일 진입점 | oxibrowser facade crate |
| Repository | 데이터 접근 캡슐화 | oxibrain projection/repository |
| Middleware / Decorator | 요청·응답 처리 체인 | oxios middleware |
- 안티 패턴 — God Object, Spaghetti Code, Premature Optimization을 반드시 구분
- 패턴은 '검증된 출발점'이 아니라 '검증된 약속' — 실무 맥락 위주로 활용
- 생성·구조·행위 3분류의 경계를 한 줄로 설명할 수 있어야 함
우리 코드oxi 4프로젝트에서 의도적으로 패턴이 골고루 등장 — 면접에서 패턴 이름을 들어 사례를 설명 가능(Builder/OxicodeBuilder, Observer/oxios EventBus, Strategy/oxicode-ai Provider, Facade/oxibrowser, Repository/oxibrain).
관련 항목: 이동
테스트 피라미드 / 단위·통합·E2E Testing Pyramid 자소서 직무 그림 우리코드
단위(빠르고 많음) > 통합(외부 의존 포함) > E2E(느리고 적음) — 피라미드 모양이 비용·속도·결함 종류의 균형을 표현. 단위 테스트는 로직 한 동작만 검증, 회귀 테스트는 결함 재발 방지.
E2E = End-to-End(사용자 시나리오를 처음부터 끝까지 자동화). 통합(Integration)= 컴포넌트 경계·외부 의존을 묶어 검증, 단위(Unit)= 함수/메서드 단위 검증.
테스트 피라미드가 왜 '피라미드' 모양인지부터 짚으면, 비용과 속도 때문입니다. 단위 테스트는 순수 함수/메서드만 검증해서 수십만 개를 몇 초에 돌릴 수 있고, 통합은 DB·API 같은 외부 의존을 함께 묶어서 수십 개~수백 개가 적당해요. E2E는 브라우저·네트워크까지 실제로 띄우기 때문에 한 번 돌리는 데 분 단위가 걸려서 수 개~수십 개만 유지합니다. 이 비율을 뒤집으면 — E2E가 너무 많으면 CI가 느려지고, 단위가 너무 적으면 회귀가 자주 생겨요.
각 층이 잡는 결함 종류가 다릅니다. 단위 테스트는 '분기 경계·엣지 케이스·순수 로직 오류'를 잡고, 통합 테스트는 '경계에서의 변환·계약 불일치·트랜잭션 경계·직렬화'를 잡아요. E2E는 '사용자 시나리오가 진짜로 끝까지 동작하는지'를 확인합니다 — 그래서 '프론트가 백엔드 응답을 잘못 파싱한다' 같은 결합 결함은 E2E에서만 잡혀요.
피라미드와 자주 같이 쓰는 개념이 '테스트 토로프'(Ice-Cream Cone 안티패턴)입니다. E2E가 많고 단위가 거의 없는 모양인데, 이러면 CI가 느려지고 한 번 깨지면 어디서 실패했는지 추적이 힘들어요. 반대로 '다이아몬드'(통합만 많고 양 끝이 빈)도 안티패턴 — 통합 셋업이 무거우면 피드백이 느려집니다.
현실에서 피라미드 비율은 팀마다 다른데, SI·백엔드 위주는 단위:통합:E2E = 7:2:1 정도가 일반적이에요. 풀스택·SaaS는 사용자 흐름이 많아 E2E 비중이 조금 올라가죠. 면접에서는 '왜 피라미드인가'를 비용·속도·결함 종류로 설명할 수 있어야 합니다.
- Mock/Stub은 '테스트 대상의 경계를 대체하는 도구일 뿐' 목적이 아님 — 경계 정하는 게 핵심
- Property-based testing은 예시가 아니라 불변식을 임의 입력으로 검증
- 회귀 테스트는 '한 번 잡은 결함을 다시 못 들어오게' — 수정보다 예방의 시간
우리 코드oxibrain M10의 350 테스트 + crash recovery 테스트(kill mid-ingest, resume no-duplicate)가 통합 테스트 사례 — 외부 의존(SQLite·crash)을 함께 묶어 검증.
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계 SI 절차 (마르미 4단계)
TDD / BDD TDD / BDD 직무 그림
TDD는 테스트를 먼저 쓰고 실패 확인 → 최소 구현 → 리팩터링의 짧은 사이클. BDD는 Given/When/Then 자연어 명세로 도메인과 개발을 잇는다.
TDD = Test-Driven Development(테스트 주도 개발) — red·green·refactor 사이클. BDD = Behavior-Driven Development(행위 주도 개발) — Given/When/Then 명세 언어. 대표 도구: Cucumber·SpecFlow.
TDD는 '테스트를 먼저 쓰고, 실패를 확인하고(red), 최소 구현으로 통과시키고(green), 리팩터링한다(refactor)'의 짧은 사이클이에요. 핵심은 'red를 직접 본다'는 것 — 빨간 줄을 보지 못한 채 구현하면 테스트가 실제로 그 시나리오를 검증하는지 확신할 수 없습니다. 사이클을 5~10분 단위로 유지하는 게 핵심이고, 한 사이클이 길어지면 보통 '한 번에 너무 많은 걸 검증하려 한다'는 신호예요.
BDD는 TDD의 '테스트가 무엇을 검증하는지 도메인 사람한테 설명이 안 된다'는 불만에서 출발했어요. Given/When/Then으로 '사용자가 로그인하지 않은 상태에서 → 장바구니에 담기를 누르면 → 로그인 페이지로 리다이렉트된다'처럼 자연어로 시나리오를 적어요. Cucumber·SpecFlow 같은 도구가 이 문장을 실제 테스트로 연결합니다. 면접에서 'TDD와 BDD의 차이'는 거의 출제돼요 — TDD는 개발자 관점의 검증, BDD는 비개발자도 읽는 명세.
SI 맥락에서 BDD의 가치는 '요구사항 정의서 → 테스트 케이스' 변환이에요. 정의서의 시나리오를 Given/When/Then으로 옮기면 그 자체로 인수 테스트 명세가 되니까요. '설계 문서를 기준으로 케이스를 뽑아본다'는 자소서 포부와 정확히 맞닿는 지점입니다.
- TDD = red-green-refactor, 'red를 직접 본다'가 핵심
- BDD = Given/When/Then 자연어 명세 — 비개발자도 읽는 테스트
- TDD는 개발자 관점 검증, BDD는 도메인-개발자 소통 매개
- SI에서는 요구사항 정의서 → Given/When/Then 인수 테스트로 직결
관련 항목: 이동
속성 기반 테스트 (PBT) Property-Based Testing 직무 우리코드
예시 몇 개 대신 '이런 입력에는 항상 이 성질이 성립한다'를 명시하면 프레임워크가 수백 개 임의 입력을 생성해 검사. 경계값 자동 탐색이 핵심 강점.
PBT = Property-Based Testing(속성 기반 테스트) — 불변식을 임의 입력으로 검증. 대표 구현: Haskell QuickCheck·Rust proptest/quickcheck·Python Hypothesis.
PBT(Property-Based Testing)는 예시 몇 개로 검증하는 대신 '이런 입력에 대해서는 항상 이 성질이 성립한다'를 명시하고 프레임워크가 수백 개 임의 입력을 생성해 검사해요. Rust의 proptest·quickcheck, Python의 Hypothesis가 대표적이에요.
PBT가 특히 강력할 때는 정렬·파서·자료구조·암호·직렬화처럼 '엣지 케이스가 무한히 많은' 도메인입니다. 작성자가 미처 생각 못한 경계값(빈 배열·INT_MIN·특정 문자열 패턴)을 자동으로 찾아주는 게 핵심 강점이에요. 실패 입력을 자동으로 축소(shrink)해서 최소 재현 케이스를 보여주는 것도 표준 기능.
면접 포인트는 'PBT의 강점이 어디인지'예요 — 예시 기반 테스트가 '이 경우는 맞다'를 증명한다면, PBT는 '이런 형태의 모든 입력에 대해 성질이 유지된다'는 불변식을 검증합니다. 단, 성질(property) 자체를 잘 정의해야 해서 설계 비용이 있어요.
- 임의 입력 × 불변식 검증 — 예시 기반 테스트의 경계값 사각지대 해소
- shrink: 실패 입력을 최소 재현 케이스로 자동 축소
- 정렬·파서·직렬화·자료구조처럼 엣지 케이스가 무한한 도메인에 강력
- Rust proptest/quickcheck · Python Hypothesis가 대표 구현
우리 코드oxibrain의 P1 invariant 'reprojection must rebuild everything'를 임의의 projection 조합으로 검증 — 면접에서 PBT 사례로 그대로 인용 가능.
CI/CD / 파이프라인 / 캐시 CI/CD Pipeline & Cache 직무 그림 우리코드
CI는 머지 전 자동 빌드·테스트·정적 분석 게이트. CD는 배포 자동화(Delivery=수동 승인·Deployment=자동). 핵심은 '실패하면 머지 차단'. 정적 분석·보안 스캔(SAST·SCA) 포함이 최신.
CI = Continuous Integration(지속적 통합) — 머지 전 자동 빌드·테스트·정적 분석 게이트. CD = Continuous Delivery(지속적 전달, 배포는 수동 승인) / Continuous Deployment(지속적 배포, 자동 승인). SAST = Static Application Security Testing(정적 애플리케이션 보안 분석), SCA = Software Composition Analysis(소프트웨어 구성 분석, 의존성 취약점 스캔).
CI가 '머지 전 자동 검증 게이트'라는 점이 중요해요. 누군가 PR을 올리면 fmt/lint·빌드·테스트·보안 스캔이 자동으로 돌고, 하나라도 빨개지면 머지 차단입니다. CD는 그 다음 단계로 '검증된 코드가 자동으로 운영 환경까지 도달한다'는 뜻이에요. Delivery와 Deployment의 차이가 미묘한데, Delivery는 '스테이징까지 자동 + 운영 배포는 사람 승인'이고 Deployment는 '운영 배포까지 완전 자동'이에요. SaaS는 후자, 금융·공공은 전자가 많습니다.
파이프라인은 보통 '트리거 → 체크아웃 → 셋업 → 캐시 복원 → 빌드 → 테스트 → 보안 스캔 → 아티팩트 업로드 → 배포'의 단계로 구성돼요. 캐시는 이 중 가장 큰 시간 절감 요소인데, 의존성 캐시(설치 안 하고 복원)와 빌드 캐시(컴파일 결과물 재사용)를 잘 구분해야 해요. 캐시 키를 해시(`hashFiles('**/Cargo.lock')`)로 잡으면 의존성이 바뀐 경우만 무효화돼서 CI 시간이 크게 줄어듭니다.
보안 스캔은 이제 필수 단계예요. SAST는 우리 코드 자체의 취약점을 정적으로 잡고(SonarQube·Semgrep·CodeQL), SCA는 의존성의 알려진 CVE를 매칭해요(OWASP Dependency-Check·Trivy·cargo-audit). SCA가 특히 중요한 이유가 — 실제로 발생한 대형 사고(예: log4j·event-stream)의 대부분은 우리 코드 아닌 의존성에서 터졌기 때문입니다.
면접에서 자주 나오는 함정은 'CI/CD는 도구(Jenkins·GitHub Actions)가 아니라 문화'라는 관점이에요. 도구는 바꿀 수 있지만 '작은 PR 자주 머지하기·main은 항상 배포 가능 상태 유지하기·실패를 빨리 드러내기'는 팀의 약속이에요. 트렁크 기반 개발(Trunk-Based)과 GitFlow의 선택도 팀 규모·릴리스 주기로 결정됩니다.
- 캐시 단계 — 의존성 캐시·빌드 캐시, hashFiles로 무효화 키를 정확히 잡는 게 CI 시간 단축의 핵심
- Trunk-based vs GitFlow — 팀 규모·릴리스 주기로 결정. 공공 SI는 GitFlow가 많고 SaaS는 트렁크 기반이 많음
- SAST + SCA 병행 — 우리 코드 결함은 SAST, 의존성 CVE는 SCA. 둘 다 있어야 함
우리 코드oxi의 게이트: cargo fmt --check + clippy -D warnings + cargo test + cargo bench — SI의 SP 품질관리 코딩 버전. GitHub Actions 캐시는 actions/cache + hashFiles('**/Cargo.lock').
관련 항목: 이동
커리큘럼 체인 SI 절차 (마르미 4단계)
형상 관리 / Git / 브랜치 전략 Version Control / Git / Branch Strategy 직무 그림 우리코드
소스·문서·빌드 스크립트의 변경 이력을 통제 — Git(분산·브랜치 중심) vs SVN(중앙 집중·디렉터리 단위). 브랜치 전략 — GitFlow / Trunk-Based / GitHub Flow.
VCS = Version Control System(형상 관리 시스템). SCM = Source Code Management. DVCS = Distributed VCS(Git·Mercurial), 중앙 집중은 SVN·CVS.
형상 관리의 핵심 가치는 '누가·언제·왜 바꿨는지 추적'이에요. Git이 표준이 된 이유는 분산 구조 — 로컬에 전체 저장소가 있어서 오프라인 작업·빠른 브랜치·PR 리뷰가 자연스럽기 때문입니다. SVN(중앙집중형)은 디렉터리 단위로 체크아웃하지만, 현대 모노레포·마이크로서비스 환경과는 잘 안 맞아요. 공공 현업에는 아직 SVN 잔존이 많지만 신규 프로젝트는 거의 전부 Git입니다.
브랜치 전략 세 가지를 비교하면 — GitFlow는 main·develop·feature·release·hotfix 5개 브랜치로 운영하며 버전 릴리스가 명확한 제품(데스크탑 앱·모바일 앱)에 어울려요. 트렁크 기반(Trunk-Based)은 main 하나에 짧은 수명의 feature 브랜치만 두며, 지속 배포(CD)하는 SaaS에 맞아요. GitHub Flow는 main + feature로 단순화한 형태로, GitHub 자체가 사용하는 방식으로 웹 서비스에 잘 맞습니다.
현업에서 자주 같이 쓰는 규약이 Conventional Commits + Squash Merge입니다. 커밋 메시지를 `feat: ...`, `fix: ...`, `refactor: ...`처럼 타입으로 시작하면 릴리스 노트·CHANGELOG 자동 생성이 가능해져요. Squash merge는 여러 커밋을 하나로 합쳐 main을 깨끗하게 유지하는 방식으로, PR 단위 작업이 main 히스토리에 그대로 남는 장점이 있어요.
면접 포인트는 '브랜치 전략 선택 기준'과 '충돌 해결 전략'이에요. 충돌은 자주 발생하는데, 작은 PR(200~400줄 이내)을 짧은 주기로 머지하는 게 가장 효과적인 예방책이에요. 거대한 PR은 리뷰도 어렵고 충돌도 잘 납니다.
- 공공 현장은 SVN 잔존이 많지만 Git 전환 추세
- Conventional Commits + squash merge가 모던 표준
- 브랜치 전략은 도구가 아니라 팀 합의 — '작은 PR, 자주 머지'가 공통분모
우리 코드what-is-oxi 분사의 원천이기기도 한 conventional commits + squash merge 규칙.
관련 항목: 이동
코드 리뷰 / 정적 분석 / AI 리뷰 보조 Code Review / Static Analysis / AI Review Assist 직무 그림 우리코드
리뷰의 목적은 결함 발견·지식 전파·표준 준수. 정적 분석은 ESLint/clippy/SonarQube. LLM 시대엔 AI 리뷰가 보조로 들어오지만 '책임 있는 인간 승인' 유지.
AI 보조 리뷰 = LLM(Large Language Model)이 PR을 자동으로 1차 리뷰. 정적 분석 = SAST(Static Application Security Testing)·린터(linter)·타입 체커(type checker). PR = Pull Request(머지 요청 단위).
코드 리뷰가 단순히 결함을 찾는 게 아니라는 점이 중요해요. 진짜 가치는 (1) 결함 발견, (2) 지식 전파(팀원이 도메인·코드 베이스를 익힘), (3) 표준 준수 — 이 세 가지가 거의 동일한 비중으로 섞여 있어요. 그래서 '리뷰어 수를 늘려서 결함을 더 잡자'는 접근보다 '팀 전체가 도메인을 알게 하는 방향'이 더 가치 있는 경우가 많아요.
리뷰의 우선순위가 명확해야 합니다. 보통 1순위는 정확성(correctness) — 버그·동시성 문제·보안 결함, 2순위는 설계(SRP·DIP 위반·결합도), 3순위가 스타일/포매팅이에요. 스타일은 정적 분석(ESLint·Prettier·clippy·rustfmt)이 자동화해주므로 리뷰어가 손으로 고치는 건 낭비예요. PR이 커지면 리뷰 품질이 떨어지므로 — 보통 200~400줄 이내로 자르는 게 정설입니다.
정적 분석 도구는 린터(ESLint·clippy)부터 타입 체커(TypeScript·Rust borrow checker)·보안 스캐너(Semgrep·CodeQL)·복잡도 측정(cyclomatic complexity)까지 단계가 다양해요. 좋은 파이프라인은 PR 단계에서 lint + format + type check + test + SAST가 모두 통과해야 머지가 되도록 강제하는 것이고, 이를 'quality gate'라고 부릅니다.
AI 리뷰 보조는 LLM이 PR의 1차 리뷰를 자동 생성하는 거예요(GitHub Copilot Reviews·Coderabbit·Sourcery·Claude/GPT 리뷰 봇). 강점은 '빠른 피드백 + 사소한 이슈 대량 발견'이지만, 결정적 함정은 '잘못된 확신' — AI는 자칫 그럴듯한 틀린 리뷰를 작성할 수 있어요. 그래서 '책임 있는 인간 승인'이 반드시 유지되어야 하고, AI 리뷰는 1차 스크리닝 + 학습 자료로 활용하는 게 현실적인 자리에요.
면접 포인트는 '린트 룰 우선순위 vs 코드 품질', 'AI 리뷰 봇을 도입한다면 어떻게 검증할 것인가'예요.
- 린트 룰의 우선순위 — formatting vs correctness vs style. 스타일은 자동화, 정확성은 사람
- PR이 커지면 리뷰 품질이 떨어진다 — 작은 단위로 쪼개기
- AI 리뷰는 1차 스크리닝 + 학습 자료 — 최종 결정은 사람
우리 코드oxicode의 lsp/ast_edit/review 도구 체인이 '리뷰 자동화'를 도구 수준에서 구현 — 사람이 보기 전에 기계가 거른다.
관련 항목: 이동
동시성 / Lock-free 자료구조 Concurrency / Lock-Free Structures 그림 우리코드
Actor 모델(Erlang/Akka) — 메시지 패싱으로 단일 소유권 격리. STM — 트랜잭션처럼 메모리 갱신. Lock-free 큐 — CAS(Compare-And-Swap) 원자 연산.
Lock-free = 락(mutex) 없이 원자적 연산(CAS = Compare-And-Swap)으로 동기화. STM = Software Transactional Memory(소프트웨어 트랜잭션 메모리). Actor = 메시지 패싱으로 단일 소유권 격리.
동시성 모델을 크게 나누면 (1) 공유 메모리 + 락(mutex/semaphore), (2) 메시지 패싱(Actor), (3) 트랜잭션(STM), (4) 데이터 흐름(Reactive) — 이 네 가지로 정리할 수 있어요. 면접에서 '왜 공유 변수에 락이 필요한가'부터 시작해서 '락 없이 어떻게 안전한가'까지 단계적으로 들어가면 자연스럽습니다.
Actor 모델은 '모든 상태는 누군가의 소유'라는 원칙으로, 상태를 바꾸려면 메시지를 보내야 해요. Erlang/Elixir·Akka(Scala·Java)가 대표 구현이고, 최근엔 Rust tokio task·Go goroutine·Kotlin coroutine도 '경량 동시성 단위'로 같은 자리를 차지합니다. 단점은 메시지 오버헤드와 '내부 상태 디버깅이 어렵다'는 점이에요.
Lock-free 자료구조는 락(mutex) 자체를 쓰지 않고 원자적 명령(CAS = Compare-And-Swap)으로 동기화를 구현해요. 대표 사례가 Treiber stack(LIFO 스택)과 Michael-Scott queue(FIFO 큐)입니다. 핵심 동작은 '현재 값을 읽고 → 새 값을 계산하고 → CAS로 교체, 실패하면 재시도'예요. 락을 안 쓰므로 deadlock이 원천적으로 없고 contention 시에도 한 쪽이 진행 가능합니다. 다만 '메모리 재배치·ABA 문제' 같은 함정이 있어서 단순히 빠르다고 좋은 게 아니에요.
현실에서 동시성 선택은 (1) 임계 영역이 단순하면 std::sync::Mutex·sync.Mutex 같은 표준 락, (2) 고경합 read-mostly면 RwLock, (3) wait-free가 필요하면 Lock-free 큐, (4) 상태 격리가 핵심이면 Actor, (5) 트랜잭션 의미가 필요하면 STM — 이 우선순위가 면접 단골이에요.
- Lock-free 알고리즘 — Treiber stack, Michael-Scott queue (CAS 기반)
- Goroutine·Tokio task·코루틴은 모두 '소유권을 가진 경량 동시성'
- 데드락 4조건(상호배제·점유대기·비선점·순환대기)을 알고 회피 전략을 설명할 수 있어야 함
우리 코드oxibrain의 single-writer actor 패턴이 가장 깔끔한 동시성 모델 사례 — 한 시점에 한 writer만 상태를 갱신.
관련 항목: 이동
직렬화 / 역직렬화 / Schema 진화 Serialization / Schema Evolution 그림 우리코드
객체 ↔ 바이트 변환 — JSON·MessagePack·Protobuf·Avro·Pickle. Schema 진화는 새 필드 추가·구 필드 deprecate 처리.
Schema = 데이터 구조 명세. 역직렬화 = deserialization(바이트/문자열 → 객체). Postel's law = '송신은 관대하게, 수신은 엄격하게'. 하위 호환 = backward compat(구 reader가 신 writer 데이터를 읽음), 상위 호환 = forward compat(신 reader가 구 writer 데이터를 읽음).
직렬화 포맷 선택은 '사람이 읽을 것인가·스키마를 강제할 것인가·속도가 중요한가'의 3축으로 결정해요. JSON은 사람이 읽고 어디서나 동작하지만 스키마가 강제되지 않고 숫자 정밀도 문제가 있고(JS Number는 2^53 정수까지), Protobuf·Avro는 스키마 강제 + 바이너리로 작고 빠르지만 사람이 직접 읽기 어렵습니다. MessagePack은 JSON 호환의 바이너리 변형으로 '작지만 호환성 유지' 자리예요.
Schema 진화가 왜 중요하냐면 — 서비스가 한 번 배포되면 이전 버전의 데이터를 읽을 수 있어야 하기 때문입니다. 흔한 시나리오가 (1) 필드 추가 — 대부분 안전(default 값으로 채움), (2) 필드 제거 — 데이터는 남지만 더 이상 안 읽음, (3) 타입 변경(int → long) — 호환 안 될 수 있음. 정책은 backward/forward/full 호환성 3종으로 정리해요.
Protobuf·Avro 같은 진화 친화 포맷은 필드 번호를 키로 쓰고 필드 이름 변경을 허용해서 wire format 호환을 깨지 않아요. 반면에 클래스를 JSON으로 직렬화하는 코드는 '필드 이름 바꾸면 데이터 호환 깨짐' 같은 함정이 있습니다. 그래서 Rust 진영에서 oxicode·bincode·postcard 같은 포맷이 '포트 가능(스키마 보존) + 진화 친화'로 주목받고 있어요.
면접에서 자주 나오는 함정이 Postel's law(송신 관대·수신 엄격) — '받는 쪽이 다양한 입력을 관대하게 받아들이고, 보내는 쪽은 엄격하게 보내라'는 원칙이에요. JSON 파서가 알 수 없는 필드를 무시하는 건 '관대함', 입력 검증을 엄격히 하는 건 '엄격함' — 둘 다 필요합니다.
- Schema 진화 정책 — backward·forward·full 호환을 구분해 정책화
- Postel's law — 송신은 관대하게, 수신은 엄격하게
- Protobuf·Avro의 필드 번호가 wire 호환의 핵심 — 이름 바꾸기·순서 바꾸기가 안전
우리 코드oxicode가 Rust + portable(Schema) 직렬화 핵심. mnemopi의 FTS5 + projection 직렬화 사례 풍부.
관련 항목: 이동
로깅 / 옵저버빌리티 / 트레이싱 Logging / Observability / Tracing 직무 그림 우리코드
구조화 로그(JSON), 메트릭·트레이스 — OpenTelemetry가 표준. 분산 시스템에서는 trace context 전파가 핵심. 9s(99.99%)의 가용성 = 약 53분/년 다운타임 허용.
OTel = OpenTelemetry(로그·메트릭·트레이스 통합 표준). RED method = Rate(요청률)·Errors(에러율)·Duration(지연). USE method = Utilization·Saturation·Errors(리소스 관점). SLO = Service Level Objective, SLI = Service Level Indicator, SLA = Service Level Agreement.
옵저버빌리티는 '로그·메트릭·트레이스' 3필러로 구성돼요. 로그는 개별 이벤트(에러 메시지·사용자 액션), 메트릭은 수치 집계(QPS·p99 latency), 트레이스는 요청이 여러 서비스를 거치며 흘러간 경로예요. 이 셋이 한 질문을 각자 다른 각도에서 답해줍니다 — '왜 에러가 났어?'는 로그, '얼마나 자주?'는 메트릭, '어디서 막혔어?'는 트레이스.
분산 시스템에서 가장 어려운 문제가 trace context 전파예요. HTTP 헤더에 traceparent(trace ID·span ID)를 실어 보내고 받는 쪽이 이어서 기록하면, 한 요청이 마이크로서비스 5개를 거치더라도 전체 흐름을 한 화면에서 볼 수 있어요. OpenTelemetry가 이 표준을 정의해서 Jaeger·Tempo·Zipkin 같은 백엔드로 보내면 됩니다. 반대로 trace ID가 끊기면 '어디서 죽었는지' 추적이 안 되니 — 컨텍스트 전파 누락이 흔한 디버깅 실패 원인입니다.
로그 레벨 기준은 보통 debug < info < warn < error < fatal인데, 운영에서 'info는 항상, debug는 장애 시에만' 같은 정책을 명확히 두는 게 중요해요. 그렇지 않으면 평소엔 잡음만 많고 정작 장애 시에 필요한 로그가 묻혀요. 구조화 로그(JSON) + 상관관계 키(trace_id·user_id·request_id)를 같이 넣으면 검색·필터링이 압도적으로 빨라집니다.
가용성 숫자도 중요해요. 9s라고 하면 99%는 분 단위 다운타임, 99.9%(three nines)는 시간 단위, 99.99%(four nines)는 약 53분/년, 99.999%(five nines)는 약 5분/년이 허용치예요. 99.99%를 SLO로 잡으면 장애 감지 + 자동 복구가 모두 1분 이내여야 한다는 뜻이고, 그건 사람 개입 없이 가능해야 한다는 의미예요.
- 로그 레벨 기준 — debug·info·warn·error·fatal. 장애 시 추적 가능성을 정책으로 고정
- RED method (Rate, Errors, Duration)와 USE method (리소스 관점)의 차이를 구분
- 분산 시스템의 핵심은 trace context 전파 — 누락되면 디버깅 불가
우리 코드oxios의 AuditTrail / observability가 표준 패턴 구현. oxicode-mnemopi의 trace persistent도 동일 — 컨텍스트 전파가 보존되어야 이후 분석이 가능.
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계
단위 테스트 (xUnit / FIRST) Unit Testing (xUnit / FIRST) 자소서 직무 우리코드
가장 작은 단위(함수·메서드)의 동작을 외부 의존 없이 검증. xUnit(JUnit·pytest·Rust #[test])가 표준. FIRST 원칙 — Fast·Independent·Repeatable·Self-validating·Timely.
xUnit = JUnit·pytest·Rust 내장 `#[test]` 등 단위 테스트 프레임워크 패밀리. FIRST = Fast(빠르게)·Independent(독립적)·Repeatable(반복 가능)·Self-validating(자가 검증)·Timely(시기 적절). AAA = Arrange(준비)·Act(실행)·Assert(검증).
단위 테스트의 오해 — '모든 경로를 검증한다'가 아니라 '계약(contract)'과 '회귀 위험'을 다뤄요. 이상적으로 한 테스트는 한 동작만 검증하고, 외부 의존(DB·네트워크·파일)은 Mock·Stub으로 대체해서 밀리초 단위로 끝나야 합니다. 그래야 수천 개를 몇 초에 돌릴 수 있고, 개발자가 자주 실행할 수 있어요.
FIRST 원칙을 풀어보면 — Fast(빠르게)는 ms 단위, Independent(독립적)는 순서·공유 상태에 의존하지 않음, Repeatable(반복 가능)는 환경·시간에 관계없이 같은 결과, Self-validating(자가 검증)은 pass/fail이 자동(사람이 로그를 읽고 판단하는 게 아님), Timely(시기 적절)는 구현 직전·직후 — 너무 늦게 쓰면 사양을 잊어버려요. SI 면접에서 '좋은 테스트의 기준'을 FIRST로 답하면 거의 만점이에요.
AAA 패턴(Arrange-Act-Assert)은 테스트의 기본 골격이에요. Arrange에서 입력을 준비하고(필요하면 Mock 셋업), Act에서 함수·메서드를 호출하고, Assert에서 결과를 검증합니다. 한 테스트에서 Assert 여러 개가 나올 수 있지만, '무엇을 검증하는지'가 명확해야 해요. 흔한 안티 패턴은 given-when-then을 한 문단에 다 써놓고 어떤 조건이 실패했는지 모호한 경우.
Test Double의 종류가 다섯 가지예요. Dummy(채우기만, 호출 안 됨), Stub(고정된 값 반환), Fake(단순 구현체, 인메모리 DB처럼), Spy(호출 기록), Mock(행위 검증 — 특정 메서드가 호출되었는지 확인). 면접 단골은 Mock vs Stub — Mock은 '행위 검증(verify)', Stub은 '상태 대용(return value)'이에요. 한국어로는 둘 다 '목'으로 뭉뚱그려 쓰지만 의미는 다릅니다.
비동기 코드의 단위 테스트는 별도 런타임 컨텍스트가 필요해요. Rust의 `#[tokio::test]`가 대표적 — tokio 런타임 안에서 future를 실행합니다. JS는 Jest의 `async/await`로 충분하고, Python은 pytest-asyncio 같은 플러그인을 씁니다.
- FIRST 5원칙 면접 빈출 — SI 면접에서 '좋은 테스트의 기준' 답변으로 직격
- Mock vs Stub — Mock은 행위 검증(verify), Stub은 상태 대용(return value)
- 커버리지 100%가 목표가 아님 — '중요 분기' 검증이 핵심
우리 코드oxibrain 단위 테스트(`#[test]` + `#[tokio::test]`) + CRDT conflict-free 불변식 PBT 검증. oxios의 surface trait이 mock 구현으로 교체 가능해 테스트성 ↑.
관련 항목: 이동
커리큘럼 체인 SI 절차 (마르미 4단계)
Lighthouse / Core Web Vitals / LCP Lighthouse / Core Web Vitals / LCP 우리코드
Lighthouse — Chrome DevTools·오픈소스 자동 감사 도구. Performance·Accessibility·Best Practices·SEO·PWA 5개 카테고리 0~100점. Core Web Vitals(2020~) — LCP·CLS·INP(구 FID) — Google 검색 ranking factor. Good / Needs Improvement / Poor 3단계.
Lighthouse = Chrome DevTools의 오픈소스 자동 감사 도구. CWV = Core Web Vitals(구글이 정의한 핵심 웹 지표). LCP = Largest Contentful Paint(메인 콘텐츠가 보이는 시각 시점, ≤2.5s=Good). CLS = Cumulative Layout Shift(시각적 안정성, ≤0.1=Good). INP = Interaction to Next Paint(응답성, ≤200ms=Good, 구 FID 대체).
Lighthouse가 측정하는 건 '실험실 환경(lab)'이에요. 시뮬레이션된 네트워크·CPU로 페이지를 띄우고 5개 카테고리 점수를 매깁니다 — Performance(렌더링·로딩 속도), Accessibility(a11y 위반), Best Practices(보안·콘솔 경고·HTTPS), SEO(메타·색인 가능성), PWA(설치 가능성·오프라인). 점수는 0~100이지만 절대값보다 'PR 전후 변화'가 더 중요한 지표예요.
Core Web Vitals는 '현실 사용자 환경(field)'에서 측정되는 지표예요. LCP(Largest Contentful Paint, 메인 콘텐츠가 보이는 시점, ≤2.5s가 Good)는 사용자 체감 속도의 핵심이고, CLS(Cumulative Layout Shift, 시각적 안정성, ≤0.1가 Good)는 '예고 없이 레이아웃이 흔들리는' 불쾌한 경험을 잡아요. INP(Interaction to Next Paint, ≤200ms가 Good)는 2024년부터 FID를 대체한 반응성 지표로, 클릭·키 입력에 대한 응답 시간을 측정합니다.
실험실(Lighthouse)과 현장(field·RUM) 결과가 다른 게 보통이에요. Lighthouse는 고속 네트워크·고성능 CPU를 가정하므로 '최선의 점수'인 경우가 많고, 실제 사용자는 저사양 폰·약한 네트워크라 더 나쁜 결과를 보여요. 그래서 web-vitals JS 라이브러리로 실제 사용자 데이터를 수집(RUM = Real User Monitoring)하는 게 SI·SaaS 모두에서 중요해요.
개선 포인트가 명확합니다. LCP는 hero 이미지 preload + critical CSS 인라인 + 폰트 preload + TTFB(서버 응답) 단축, CLS는 동적 삽입 광고/이미지에 명시적 width·height 지정 + web-font swap 미지정 + 늦게 로드되는 iframe 제거, INP는 메인 스레드 작업 줄이기 + 이벤트 핸들러 가볍게 + 무거운 콜백을 web worker로 옮기기. 면접에서는 'Astro SSG + 0 hydration이 LCP를 1초대로 만든다'는 사례가 강력한 포인트예요.
- LCP 개선 — hero 이미지 preload, critical CSS 인라인, 폰트 preload, TTFB ↓
- CLS 원인 — 동적 삽입 광고, web-font swap 미지정, 늦게 로드되는 iframe
- Lighthouse 점수보다 RUM·field 데이터가 실제 UX — 시뮬레이션 ≠ 사용자
우리 코드what-is-oxi는 Astro 5 + Tailwind v4 + Shiki + jsDelivr 폰트 preconnect — SSG 0 hydration으로 LCP 1초대. 면접 포인트: '정적 페이지 LCP 1초대 = Astro SSG의 직접 효과'.
관련 항목: 이동
GitHub Actions / 파이프라인 as Code GitHub Actions / Pipeline as Code 직무 우리코드
.github/workflows/*.yml로 CI를 코드로 정의. job → step → action(재사용 단위) 트리 구조. matrix 전략·캐시·secrets·artifacts·OIDC로 클라우드 키 없이 인증. GitHub-hosted runner / self-hosted runner 선택.
PaC = Pipeline as Code(YAML로 빌드·배포 파이프라인 정의). OIDC = OpenID Connect(클라우드 단기 인증 토큰). Runner = CI 실행 환경(GitHub-hosted 또는 self-hosted). Matrix = 다중 OS·버전 병렬 실행.
GitHub Actions의 핵심은 'CI를 코드로 쓴다'는 파이프라인 as 코드(PaC) 원칙이에요. .github/workflows/*.yml에 작업을 정의해 두면, 누군가 PR을 올릴 때마다 자동으로 실행되고, 변경 이력으로 추적되며, 새 클론에서도 즉시 재현됩니다. 이 세 가지가 UI에서 클릭하는 Jenkins·클래식 CI 대비 결정적인 장점이에요.
트리거 종류가 다양합니다. push(특정 브랜치에 푸시), pull_request(PR 열림/갱신), schedule(cron 표현식, 야간 빌드), workflow_dispatch(수동 버튼), repository_dispatch(외부 이벤트). 보통 push·pull_request는 풀 요청 검증용, schedule은 의존성 업데이트·야간 벤치마크, workflow_dispatch는 배포 같은 수동 작업에 씁니다.
Matrix 전략은 OS·언어 버전·Rust toolchain 같은 축을 조합해 병렬로 검증해요. 예를 들어 Ubuntu·macOS·Windows × stable·nightly Rust 6개 조합이면 6개 잡이 동시에 돕니다. 캐시는 `actions/cache@v4` + `hashFiles('**/Cargo.lock')` 패턴이 표준 — 의존성 파일 해시가 바뀌면 캐시가 무효화돼서 정확성과 속도를 둘 다 잡습니다.
보안 측면에서 OIDC가 중요해요. AWS·GCP·Azure에 GitHub Actions가 직접 인증해서 단기 토큰(STSI)을 발급받을 수 있어서, 깃허브 시크릿에 long-lived 액세스 키를 저장할 필요가 없습니다. 만료가 자동이라 키 유출 위험이 크게 줄어요. 면접 포인트는 '왜 PaC인가'(재현·추적·공유)와 'OIDC로 long-lived secret 회피'예요.
- matrix로 OS·Node 버전·Rust toolchain 병렬 검증
- 캐시 키 해시는 hashFiles('**/Cargo.lock') — 의존성 변화에만 무효화
- OIDC + cloud short-lived token — long-lived secret 회피
우리 코드oxi 4프로젝트의 CI가 GH Actions — cargo fmt --check + clippy -D warnings + cargo test + cargo bench. 면접 포인트: 'SI 품질관리(SP) = 코딩 버전 CI 게이트'.
관련 항목: 이동
배포 전략 / 카나리·블루그린·롤링 Deployment Strategies / Canary·Blue-Green·Rolling 우리코드
Rolling(점진 교체) — 기본. Blue/Green(두 환경 스왑) — 무중단 롤백 가능. Canary(소수 트래픽 → 검증 → 확대) — 위험 최소화. Feature Flag(런 토글) — 코드 deploy와 release 분리. IaC·GitOps가 '선언적 배포'를 구체화.
Blue/Green = 두 환경 스위치(Blue=이전, Green=신규). Canary = 소수 트래픽 → 검증 → 확대. Rolling = 점진적 교체(인스턴스 단위). Feature Flag = 런 토글(코드 deploy와 release 분리). IaC = Infrastructure as Code(Terraform·Pulumi). GitOps = ArgoCD·Flux로 git 기반 배포.
배포 전략을 비교하면 — Rolling update는 가장 기본입니다. N개 인스턴스를 한 번에 1~2개씩 새 버전으로 교체하는 방식이라 자원이 추가 안 필요하고 단순해요. 단점은 새·구 버전이 짧게 공존하므로 호환성 깨지면 한 인스턴스만 장애가 납니다. SI 공공 시스템은 보통 이 방식으로 충분하고, 헬스 체크 + 자동 롤백이 핵심 안전장치예요.
Blue/Green은 '두 환경을 띄워두고 로드밸런서로 스위치' 방식이에요. Blue(이전)·Green(신규) 환경을 동시에 띄워놓고 한 번에 트래픽을 Green으로 보내요. 장점은 무중단 + 즉시 롤백(로드밸런서를 Blue로 다시 가리키면 끝), 단점은 자원이 2배 + DB 마이그레이션 시 구/신 버전을 동시에 운영해야 하는 시간 동안 신중해야 한다는 점이에요. DB 스키마 변경이 있을 때는 backward-compatible 마이그레이션(컬럼 추가·deprecate 단계)이 필수.
Canary는 '소수 트래픽(1~5%)을 신규로 보내고 메트릭 확인 후 단계적으로 확대'예요. 핵심은 사용자 메트릭(에러율·p99 latency)을 실시간으로 관찰해서 임계치 초과 시 자동 차단하는 것이에요. Argo Rollouts·Flagger가 카나리 분석 + 자동 abort를 제공합니다. 위험 회수가 큰 만큼 설정·모니터링 복잡도도 커요.
Feature Flag는 '코드 deploy와 release 분리'예요. 새 기능 코드를 배포하되 운영 환경에서 OFF 상태로 두었다가, 점진적으로 ON 비율을 올리는 방식 — LaunchDarkly·Unleash·OpenFeature(CNCF)가 도구예요. 100% rollout 전에는 OFF가 가능해 즉시 안전 차단할 수 있고, A/B 테스트·점진 공개(gradual rollout)에도 활용돼요.
IaC(Terraform·Pulumi)와 GitOps(ArgoCD·Flux)는 '선언적 배포'를 가능하게 해요. 환경 구성을 코드로 선언하고 git PR로 변경하면 도구가 알아서 적용·동기화 — 클릭 한 번으로 인프라가 재현 가능합니다. 면접 포인트는 'DB 마이그레이션과 배포 전략의 결합'과 'feature flag의 위험 회수 가치'예요.
- Blue/Green — DB 마이그레이션 호환성 주의 (구/신 버전 동시 운영 시간)
- Canary — 분석 단계에서 Prometheus/Grafana 쿼리로 판단, Argo Rollouts가 자동 abort 제공
- Feature Flag — LaunchDarkly / Unleash / OpenFeature(CNCF), 100% rollout 전엔 OFF 가능
우리 코드oxios 산출물(SI/SM 납품)의 '지속적 배포' 답변 — 카나리 5% → 25% → 100% + 자동 abort. 면접에서 '클라우드 네이티브 12-Factor' 답변으로 연결.
관련 항목: 이동
모놀리스 vs 마이크로서비스 (MSA) Monolith vs Microservices (MSA) 직무 우리코드
단일 배포·단순 디버깅(모놀리스) vs 독립 배포·기술 이기종(MSA)의 트레이드. '분산 모놀리스' 안티패턴, 조직 규모·도메인 경계 기준으로 판단. eGovFrame 공공 모놀리스 현실 + 헥사고날/ports-adapters가 경계 분리.
MSA = Microservices Architecture(마이크로서비스 아키텍처) — 도메인 경계로 쪼갠 독립 배포 서비스들. 헥사고날 = Hexagonal Architecture(Alistair Cockburn, ports & adapters로 경계 분리).
모놀리식 아키텍처는 '하나의 코드베이스, 하나의 배포 단위'예요. 장점이 분명합니다 — (1) 로컬에서 그냥 띄워서 끝까지 디버깅 가능, (2) 트랜잭션이 한 DB 안에서 끝나 ACID 보장이 자연스러움, (3) 네트워크 호출·직렬화 같은 분산 비용이 0, (4) 팀이 작을 때 의사소통 비용이 낮음. 그래서 신규 프로젝트·스타트업 초기·SI 공공 프로젝트의 기본값이에요.
MSA는 '도메인 경계마다 독립 서비스로 쪼개고 각자가 독립 배포·독립 기술 스택을 갖는' 아키텍처예요. Netflix·Amazon·Uber 같은 대규모 트래픽 시스템에서 출발했고, 장점은 (1) 한 서비스 장애가 전체를 죽이지 않음(격리), (2) 팀별로 독립 배포·독립 스케일, (3) 각 서비스에 최적 언어/DB 선택 가능. 단점은 (1) 네트워크 호출이 늘면서 지연·실패 모드가 복잡해지고, (2) 분산 트랜잭션·데이터 일관성·trace 전파 같은 운영 부담이 크게 올라가요.
가장 흔한 안티패턴이 '분산 모놀리스(Distributed Monolith)'예요. 이름만 MSA인데 배포는 같이 묶여 있거나 DB를 공유해서 한 서비스 변경이 전체를 깨뜨리는 상태 — MSA의 단점(네트워크·운영 복잡)만 안고 장점(독립 배포·격리)은 못 얻는 최악의 경우예요. 모놀리식 안티패턴은 'Big Ball of Mud'(구조 없는 거대 모놀리스)예요 — 경계 없이 다 연결되어 변경 파급을 예측할 수 없게 된 상태.
판단 기준은 (1) 조직 규모 — 5명 이하 팀이면 모놀리스가 단순하고 빨라요, (2) 도메인 경계가 자연스러운가 — 결제·인증·검색·추천처럼 독립 진화가 가능한 도메인이면 MSA 효과 큼, (3) 배포 독립성 가치가 큰가 — 한 팀이 하루에도 여러 번 배포해야 하면 MSA가 유리, (4) 트래픽 격리가 필요한가 — 한 도메인의 스파이크가 다른 도메인을 죽이면 MSA 가치 큼.
한국 공공·SI 현업은 모놀리스가 압도적으로 많아요. eGovFrame(전자정부 표준프레임워크)이 Spring MVC 기반 모놀리식 구조를 사실상 표준으로 제시하고 있고, 유지보수·감리·운영 체계가 모놀리식에 최적화되어 있어요. 그래서 SI 답변에서는 '현실은 모놀리스, 미래는 MSA'라는 화법이 자연스러워요.
헥사고날(ports & adapters)이 중간 지점이에요. 모놀리스로 시작하되 코드 구조를 도메인·포트·어댑터로 계층화해두면, 나중에 MSA로 쪼갤 때 경계가 이미 정의되어 있어서 분리가 쉬워져요. oxicode-sdk의 16 port trait이 바로 이 패턴 — 도메인 코드가 구체 어댑터(SQLite·redb·tantivy·FS)에 의존하지 않고 trait에만 의존하므로, 어댑터 교체만으로 다른 backing store를 붙일 수 있어요. SI 면접에서는 '현실은 모놀리스 + 헥사고날 구조가 가장 한국적인 답'으로 모범 답을 만들 수 있어요.
- MSA의 진짜 장점은 '팀 단위 독립 배포' — 단순히 서비스 개수가 늘어난 게 아님
- 분산 모놀리스가 최악의 안티패턴 — MSA 비용은 안고 장점은 못 얻음
- 판단 기준: 조직 규모·도메인 경계·배포 독립성 가치·트래픽 격리 필요성
- 헥사고날(ports & adapters)이 모놀리스에서 MSA로 옮겨갈 때의 안전한 경계 분리 코드 수준 실천
우리 코드eGovFrame 공공 모놀리스 현실 + oxicode-sdk의 16 port trait(헥사고날·ports-and-adapters)이 경계 분리를 코드 수준에서 실천 — 도메인이 구체 어댑터에 의존하지 않으므로 MSA로 분할할 때 경계가 이미 정의됨.
관련 항목: 이동
AI · NLP · LLM 20
챗봇 직무의 핵심. 토크나이저부터 Transformer·RAG·tool-calling·평가까지 — 원리를 단계별로 푼다.
토크나이저 / BPE / 서브워드 Tokenizer / BPE / Subword 직무 그림 우리코드
텍스트를 모델이 이해하는 최소 단위(토큰)로 분해하는 장치. BPE는 빈도 높은 바이트 쌍을 반복 병합해 어휘 사전을 만들고, 서브워드라 희귀어·오타에 강하다. 한글은 교착어라 조사·어미가 분리되어 토큰 수가 잘 늘어난다.
BPE = Byte Pair Encoding(바이트 쌍 부호화)
왜 단어가 아니라 서브워드일까요. 단어 단위 사전은 어휘가 수십만~수백만 개로 폭증하고, 학습 데이터에 한 번도 안 나온 신조어·오타·전문용어에서는 모르는 단어가 끝없이 나옵니다. 그래서 단어를 더 작은 조각, 즉 서브워드로 쪼개는 아이디어를 씁니다. 예를 들어 unhappiness를 un과 happiness로 끊고, 신조어 ChatGPT를 Chat, G, PT 같은 식으로요. 이렇게 하면 어떤 단어가 와도 단어 단위 사전은 절대 모르는 토큰이 안 나옵니다.
BPE(Byte Pair Encoding, 바이트 쌍 부호화)는 이런 서브워드 사전을 만드는 알고리즘입니다. 처음에는 텍스트를 글자(또는 바이트) 단위로 쪼개고, 가장 자주 같이 등장하는 두 조각을 반복해서 합쳐요. GPT-2·GPT-4의 tiktoken, RoBERTa·GPT-J 모두 BPE 계열이고, SentencePiece의 Unigram Language Model은 변형입니다. 어휘 크기는 보통 3만~10만 정도로 고정해서, 어떤 입력이 와도 처음 보는 토큰이 절대 나오지 않게 보장합니다.
한글은 교착어라서 이 문제가 특히 심합니다. 영어 go to school이 3토큰이면 끝나는데, 한국어 학교에 간다는 학교, 에, 간다로 분리되는 데다가 학교에서, 학교에는, 학교였었다까지 변형이 끝이 없어요. 그래서 영어 모델을 한국어에 그대로 쓰면 같은 문장도 토큰이 2~3배 더 잡힙니다. 컨텍스트 길이가 빨리 차고 API 비용이 그만큼 늘어나죠. 그래서 한국어 특화 모델은 형태소 분석기(Mecab·OKT)나 음절 단위 + BPE 혼합을 쓰는 경우가 많아요.
운영에서 토크나이저는 비용·속도·컨텍스트 길이의 출발점입니다. 같은 한국어 문장도 토크나이저에 따라 토큰 수가 30% 가까이 달라질 수 있고, 임베딩·API 호출료·KV 캐시 메모리 전부 토큰 수에 비례해서 움직입니다. 그래서 oxibrain 같은 시스템은 이 부분을 포트(TokenizerPort)로 추상화해서 모델 교체 시 어휘 차이를 흡수합니다.
- 왜 단어 단위가 아닌가: 어휘 폭발, 미등록어(OOV), 오타·신조어 문제를 서브워드가 해결
- BPE 알고리즘: 글자 단위 시작 → 가장 빈번한 쌍 반복 병합 → 어휘 사전 고정(보통 3만~10만)
- 한글 교착어 특성: 조사·어미 분리로 영어 대비 2~3배 토큰이 잡혀 비용도 같이 증가
- 실무 함정: 토크나이저가 바뀌면 같은 한국어 문장의 토큰 수가 30% 차이나므로 모델 교체 시 재검증 필요
우리 코드oxibrain-ports의 TokenizerPort가 어휘 차이를 추상화 — 모델 교체 시 컨텍스트 패킹·토큰 예산이 흔들리지 않게 한다.
커리큘럼 체인 챗봇 직무 4단계
임베딩 / 의미 벡터 / 다국어 Embeddings / Semantic Vector / Multilingual 자소서 직무 그림 우리코드
텍스트를 의미 공간의 고정 차원 벡터(보통 384~4096차원)로 변환. 의미가 비슷할수록 벡터 사이의 각도가 작아져 코사인 유사도가 1에 가까워진다. 다국어 모델은 같은 공간 안에서 언어 간 의미 정렬까지 학습한다.
임베딩은 텍스트를 좌표로 바꾸는 작업입니다. 강아지라는 단어와 puppy는 한국어와 영어로 표기가 다르지만 실제 의미는 같죠. 임베딩 모델은 이런 두 단어를 수백~수천 차원의 벡터 공간 안에서 서로 가까운 점으로 배치합니다. 그래야 검색·분류·추천 시스템이 사용자가 강아지를 검색해도 puppy가 들어 있는 영문 문서를 잘 끌어올 수 있어요.
두 벡터가 비슷한지 측정할 때 보통 코사인 유사도를 씁니다. 이 값은 수학적으로는 두 벡터의 각도 코사인인데, 길이(노름)는 무시하고 방향만 봅니다. 왜 길이를 무시하냐면, 같은 단어라도 더 긴 문맥에서 등장한 임베딩은 노름이 더 커지는데, 그건 의미의 차이가 아니라 문맥 풍부함의 차이일 뿐이기 때문이에요. 그리고 벡터를 미리 L2 정규화(노름을 1)로 맞춰두면 내적 자체가 코사인 값이 됩니다. 그래서 대용량 검색에서는 노름 제곱합을 미리 저장해 두고 내적 한 번으로 유사도를 끝내는 게 표준 패턴입니다.
다국어 임베딩은 같은 벡터 공간에 여러 언어를 섞어 놓은 겁니다. 강아지와 dog가 거의 같은 점에 오도록, 모델이 학습 단계에서 번역 쌍·다국어 코퍼스로 정렬을 학습해요. 그래서 한 모델로 한국어 질문과 영어 문서, 일본어 문서를 같은 인덱스에 넣고 한 번에 검색할 수 있습니다. ForGIS 같은 시스템에서 한국어 질의로 영문 OGC 명세까지 끌어오는 게 가능한 이유죠.
운영에서 임베딩은 두 갈래로 나뉩니다. OpenAI text-embedding-3 같은 API 모델은 품질이 안정적이지만 호출 비용이 붙고, sentence-transformers·BGE·E5 같은 오픈소스 모델은 GPU(혹은 Apple Silicon Metal) 위 로컬 추론으로 비용 0에 품질만 살짝 떨어집니다. oxibrain은 후자 길로 BGE-M3를 llama-cpp-2로 돌려서 외부 API 없이 다국어 임베딩까지 처리합니다.
- 코사인 유사도 = 벡터 간 각도의 코사인, 방향만 비교하고 길이(노름)는 무시
- L2 정규화 후 내적 = 코사인 — 대량 검색에서 노름을 미리 저장해 두면 내적 한 번으로 끝남
- 다국어 임베딩은 같은 공간에 여러 언어를 정렬 — 한 모델·한 인덱스로 다국어 검색 가능
- 운영 트레이드오프: API 모델(품질·안정성, 비용 발생) vs 로컬 모델(BGE-M3 + llama-cpp, 비용 0, Metal 가속)
우리 코드oxibrain-embed-local이 BGE-M3 GGUF를 llama-cpp-2로 구동 (Metal 가속) — API 비용 0, 로컬 추론으로 다국어 임베딩 처리.
커리큘럼 체인 RAG 파이프라인
Transformer / 어텐션 / 위치 인코딩 Transformer / Attention / Positional Encoding 직무 그림
2017년 Attention is All You Need로 등장한 신경망 구조. Self-attention으로 한 문장 안의 모든 토큰이 서로에게 동시에 질문을 던져 관계를 계산하고, 위치 인코딩이 토큰의 순서 정보를 보강한다. GPT·BERT·T5 등 모든 현대 LLM의 토대.
Transformer의 핵심은 self-attention이라는 메커니즘입니다. RNN처럼 왼쪽에서 오른쪽으로 한 토큰씩 읽지 않고, 문장 안의 모든 토큰이 한꺼번에 서로를 봅니다. 비유하자면, 한 교실에 학생 30명이 앉아 있는데 각자가 다른 29명 모두에게 너랑 나는 어떤 관계니라고 한 번씩 물어보는 거예요. 이렇게 하면 그녀가라는 단어가 민수가 준 책이라는 명사구와 멀리 떨어져 있어도 바로 연결을 찾을 수 있습니다.
이 질문은 세 가지 벡터로 이루어집니다. 각 토큰마다 Q(Query: 질문), K(Key: 라벨), V(Value: 실제 내용) 세 개가 생기고, 토큰 i의 Q와 토큰 j의 K의 내적이 얼마나 잘 연결되는지의 가중치가 됩니다. 그 가중치로 V들을 가중합하면 i가 다른 토큰들로부터 끌어온 정보가 됩니다. 이게 scaled dot-product attention의 기본이고, sqrt(d_k)로 나누는 이유는 내적 값이 너무 커져서 softmax가 saturate되는 걸 막기 위해서예요. 멀티 헤드는 이 연산을 8~16개 머리로 동시에 해 주어-동사, 형용사-명사, 인용-출처 같은 서로 다른 부분 관계를 병렬로 잡아냅니다.
여기서 하나 꼭 짚고 갈 게, self-attention 자체는 순서를 모른다는 점입니다. 고양이가 쥐를 잡았다와 쥐가 고양이를 잡았다를 똑같이 봅니다. 그래서 위치 인코딩이 따로 더해져요. 처음 나온 sin/cos 기반부터 시작해, RoPE(Rotary Position Embedding, 회전 위치 임베딩)가 Modern LLM 표준이 됐고, ALiBi는 단순한 선형 편향으로 긴 컨텍스트 외삽을 노리는 변형입니다.
가장 큰 약점은 메모리가 O(n²)이라는 점입니다. 토큰 수 n이 늘어나면 attention 행렬이 n×n으로 커져서, 8K 컨텍스트면 6400만 셀, 100K면 10억 셀이에요. 그래서 Flash Attention이 등장했습니다 — 수학 자체는 그대로 두고 메모리 I/O 순서만 재정렬해 GPU HBM 왕복을 줄였죠. 모델 패밀리별로 보면 GPT 계열은 Decoder-only(다음 토큰 예측), BERT 계열은 Encoder-only(문장 이해), T5는 Encoder-Decoder(번역·요약)이고, 요즘 오픈소스 LLM 대부분은 Decoder-only입니다.
- Self-attention 비유: 한 문장 안의 모든 단어가 서로에게 동시에 질문하는 것
- Q·K·V의 역할: Q=질문, K=라벨, K와의 내적이 attention 가중치, V가 실제 전달 내용
- O(n²) 메모리: 컨텍스트 길이 제곱으로 attention 행렬 증가 → Flash Attention이 I/O 재정렬로 해결
- 위치 인코딩이 필수인 이유: self-attention은 본질적으로 순서를 모름 — RoPE가 Modern LLM 표준
커리큘럼 체인 챗봇 직무 4단계
LLM 추론 파라미터 / sampling LLM Inference Parameters / Sampling 직무 그림 우리코드
모델이 다음 토큰의 확률 분포를 만든 뒤, 최종 토큰을 어떻게 선택할지 정하는 파라미터 묶음. temperature로 분포의 평탄/날카로움을, top-p(누적 컷off)·top-k(상위 k개만)로 후보 폭을, max_tokens는 생성 길이 상한을 정한다.
모델은 매 단계에서 다음 토큰이 뭐가 올 것 같으냐는 확률 분포를 내놓습니다. The capital of France is 다음에는 Paris가 0.92, the가 0.03, Lyon이 0.01, ... 이런 식으로요. 여기서 가장 높은 것을 매번 고르는 게 greedy decoding인데, 이러면 같은 입력에 늘 같은 답만 나와서 답변의 다양성이 죽어요. 그래서 보통은 이 분포를 살짝 평탄하게 만들거나(softmax temperature), 일부만 남기고(top-k·top-p) 뽑습니다.
temperature는 분포를 얼마나 평탄하게 만들지를 조절합니다. 수식으로는 softmax(logits / T)이고, T=1이면 원래 분포 그대로, T<1이면 더 날카로워져 1등이 더 두드러지고, T>1이면 평탄해져서 작은 확률 토큰도 잘 뽑혀요. 그래서 T가 높을수록 창의적이지만 엉뚱한 답이 나올 확률도 같이 올라갑니다. 공공 챗봇이나 사실성 응답은 보통 0.0~0.3으로 두고, 시·브레인스토밍은 0.7~1.0으로 둡니다. T=0은 사실상 greedy와 동치예요.
top-p(핵 샘플링, nucleus sampling)는 누적 확률이 p를 처음 넘는 지점에서 잘라 그 안에서만 뽑는 방식입니다. 예를 들어 top-p=0.9이면 후보를 확률 높은 순으로 누적하다가 90%가 되는 순간 거기서 컷. 분포가 분명할 땐 1~2개 토큰만 남고, 분포가 평평할 땐 20~30개가 한꺼번에 살아남아요. top-k=50이라고 고정 컷off하는 top-k도 널리 쓰입니다. 분포가 어떤 모양이냐에 따라 적응하는 게 top-p의 장점이에요. 여기에 min_p(상대 임계값), repetition_penalty(같은 토큰 반복 억제) 같은 모델별 안정화 트릭이 더 붙습니다.
공공 SI 챗봇은 사실성이 우선이니 temperature를 0.0~0.3, top-p를 0.9~0.95 정도로 잡는 게 흔합니다. 그리고 max_tokens라는 길이 상한으로 무한 루프를 방지하고, stop 시퀀스(질문:, </답변> 같은 종료 토큰)로 함수 호출 종료 시점을 명시적으로 끊어요. 모델·provider별로 이 기본값이 다른데, oxicode-ai의 Provider trait이 모델별 default 파라미터를 제공해서 호출 측이 매번 외우지 않아도 되게 합니다.
- temperature = 분포 평탄화(T↑) · 날카로움(T↓) — T=0은 greedy와 동치
- top-p(누적 컷off) vs top-k(고정 k개) — top-p가 분포 형태에 따라 자동으로 적응
- 사실성 답변은 temperature 0.0~0.3 + top-p 0.9~0.95가 표준 — 공공 SI 챗봇 기본값
- 모델별 default 차이를 Provider trait이 흡수 — 호출 측은 매번 파라미터 조합을 튜닝할 필요 없음
우리 코드oxicode-ai의 Provider trait이 모델별 default temperature·top-p·max_tokens를 제공 — 호출 코드가 파라미터 디테일을 몰라도 된다.
커리큘럼 체인 챗봇 직무 4단계
RAG (Retrieval-Augmented Generation) Retrieval-Augmented Generation 자소서 직무 그림 우리코드
외부 지식에서 근거 문서를 검색한 뒤, 그 내용을 프롬프트에 끼워 넣어 LLM이 답변하게 하는 아키텍처. 모델의 학습 시점 이후 정보·사내 문서 같은 도메인 특화 지식을 보강해 환각을 줄이고 출처 인용을 가능하게 한다.
RAG = Retrieval-Augmented Generation(검색 증강 생성)
일반 LLM은 학습 시점 이후의 정보나 사내 매뉴얼·규정 같은 도메인 문서를 모릅니다. 그래서 fine-tuning으로 외워 넣을 수도 있지만, 그건 비용이 크고 정보가 갱신될 때마다 재학습해야 해요. RAG는 이걸 우회합니다. 질문이 들어오면 외부 문서 저장소에서 관련 부분을 검색해서 그 내용을 프롬프트에 같이 넣고, 모델은 주어진 자료만 보고 답변하게 만드는 거예요. 모델 자체는 안 건드려도 지식이 시의성을 갖게 됩니다.
RAG는 3단계로 동작합니다. 먼저 ingestion(준비): 문서를 청크로 자르고, 각 청크를 임베딩으로 변환해 벡터 인덱스에 저장합니다. 그다음 retrieval(검색): 사용자 질의도 임베딩해서 가장 비슷한 청크를 몇 개 끌어오고, 필요하면 재랭킹으로 정렬해요. 마지막 generation(생성): 앞 단계에서 뽑힌 청크들을 시스템 프롬프트에 출처와 함께 넣고 모델이 답변을 쓰게 합니다. 근거 표시(citation)는 보통 청크 ID·문서명을 답변 끝에 덧붙이는 식으로 구현돼요.
병목은 보통 generation이 아니라 retrieval입니다. 모델은 좋은 청크가 앞에 주어지면 그 안에서 답을 잘 만들어내는데, retrieval이 엉뚱한 청크를 끌어오면 모델이 그 틀 안에서 그럴듯한 거짓말을 지어내요. 그래서 RAG 평가는 retrieval 단계 위주로 돌립니다 — precision@5(상위 5개 중 실제 관련 비율), recall@10(관련 청크가 10개 안에 포함되는 비율), MRR(정답 청크의 평균 순위), nDCG 같은 지표요. retrieval이 약하면 모델을 갈아끼워도 답이 안 좋아집니다.
RAG가 환각을 줄여주는 핵심 메커니즘은 출처를 못 찾으면 답을 거부하게 만들 수 있다는 점이에요. 프롬프트에 제시된 자료에 없는 내용은 모르겠습니다라고 답해라고 명시하고, retrieval 점수가 일정 임계 아래면 아예 generation을 스킵해 refusal(거절) 응답을 돌려주는 거예요. 그래서 citation·attribution·refusal rate가 RAG 운영의 핵심 KPI가 됩니다. oxibrain L0이 n-gram Jaccard + dense vector + RRF 융합 + MMR 재랭킹 + 토큰 예산 패킹까지 한 묶음으로 하이브리드 RAG를 구현합니다.
- 3단계 흐름: ingestion(청킹·임베딩·색인) → retrieval(질의 변환·검색·재랭킹) → generation(근거 기반)
- 병목은 retrieval — 정답 청크를 못 찾으면 모델이 그 안에서 그럴듯한 거짓말을 합성
- 환각 완화 메커니즘: 자료에 없으면 모르겠다로 거부 + retrieval 점수 임계로 generation 스킵
- 운영 KPI: precision@5, recall@10, MRR, nDCG, refusal rate, citation 정확도
우리 코드oxibrain L0 = n-gram Jaccard + dense vector + RRF fusion + MMR rerank + token budget packing — 하이브리드 RAG의 모든 핵심을 한 묶음으로.
관련 항목: 이동
청킹 / Chunking / Span Projection Chunking / Span Projection 자소서 직무 그림 우리코드
긴 문서를 검색·임베딩 단위(보통 256~1024 토큰)로 분할하는 작업. 너무 크면 노이즈가 끼고, 너무 작으면 맥락이 끊긴다. Span Projection은 청크 경계를 원문 바이트 오프셋으로 추적해 근거 표시·재구성 정확도를 보장한다.
LLM은 컨텍스트 길이에 한계가 있고, 한 번에 임베딩할 수 있는 텍스트에는 실무적으로 상한이 있습니다. 그래서 수십 페이지짜리 PDF·수천 줄짜리 SQL 결과를 한 번에 벡터화할 수 없고, 적당한 크기로 잘라야 해요. 이게 청킹(chunking)입니다. 너무 작게 자르면 주어와 술어가 분리되어 의미가 사라지고, 너무 크게 자르면 관련 없는 문단이 한 청크에 묶여 검색 정밀도가 떨어집니다.
청킹 전략은 단순한 것부터 정교한 것까지 다양합니다. 가장 단순한 건 고정 길이(예: 512 토큰마다 자르기)고, 재귀 청킹은 빈 줄 → 줄바꿈 → 문장 → 단어 순서로 구분자를 따라 잘라 의미 경계를 최대한 보존해요. 의미(semantic) 청킹은 먼저 임베딩을 뽑고 인접 문단 간 코사인 거리로 경계가 바뀌는 지점을 찾아 자르는데, 더 비싸지만 의미가 더 깨끗합니다. 그리고 sliding window(인접 청크 overlap 10~20%)로 경계 매끄럽게 이어 붙이는 테크닉이 거의 항상 같이 쓰여요.
한 가지 자주 잊는 게 청크의 출처 추적입니다. 모델이 3페이지 2번째 단락에 따르면 같은 답변을 내야 한다면, 청크가 원본 문서의 어디였는지 byte offset·page number로 알아야 해요. 이게 Span Projection인데, 청크를 만들 때 원본의 start_byte, end_byte 같은 메타데이터를 같이 저장하는 거예요. 이게 없으면 청크는 그냥 의미의 덩어리일 뿐, 인용 가능한 문서의 일부가 되지 못합니다.
Late chunking은 새 트렌드인데, 평소엔 문서 → 청크 → 임베딩 순서로 자르고 나서 벡터를 만들죠. 이러면 청크 경계에서 양쪽 문맥 정보가 잘립니다. Late chunking은 문서 전체를 한 번에 모델에 통과시킨 뒤 나오는 토큰별 벡터를 청크 단위로 묶는 방식이라, 경계에 있는 단어도 양쪽 문맥이 섞인 임베딩을 갖게 돼요. 의미 손실이 줄어들지만 임베딩 비용이 크게 올라가는 트레이드오프가 있습니다. oxibrain은 이 byte-anchored span projection을 일급 시민으로 다뤄서 근거 표시(citation) 정확도를 보장합니다.
- 청크 크기 트레이드오프: 너무 작으면 맥락 손실, 너무 크면 노이즈 — 보통 256~1024 토큰
- 전략 선택: 고정 길이 / 재귀(빈 줄→줄바꿈→문장) / 의미 청킹(임베딩 거리) / sliding window overlap
- Span Projection = 청크에 start_byte·end_byte·page_number 같은 원문 좌표를 저장 — 인용의 전제 조건
- Late chunking: 문서 전체를 한 번 임베딩 후 청킹 — 경계 의미 손실 감소, 비용 증가
우리 코드oxibrain의 byte-anchored span projection이 청크 경계를 원문 바이트 오프셋으로 추적 — 어느 문서의 어느 위치 인용이 항상 가능하다.
커리큘럼 체인 RAG 파이프라인
하이브리드 검색 / RRF / MMR Hybrid Search / RRF / MMR 자소서 직무 그림 우리코드
키워드(BM25·TF-IDF) 검색과 의미(벡터) 검색을 결합해 둘의 실패 모드를 상호 보완하는 방식. RRF는 점수 스케일이 다른 두 순위를 같은 식으로 합치고, MMR은 관련성과 다양성을 동시에 최대화한다.
RRF = Reciprocal Rank Fusion(역수 순위 융합), MMR = Maximal Marginal Relevance(최대 한계 관련성)
키워드 검색(예: BM25)은 정확히 그 단어가 있는 문서를 잘 찾지만, 자동차로 검색해서 차량 문서는 못 찾습니다. 동의어·오타·번역 변형에 약해요. 반면 벡터 검색은 의미가 비슷한 문장을 잘 찾지만, 2024년 3월 같은 고유명사·숫자·코드 식별자에서는 부정확합니다. 그래서 둘의 실패 모드가 정확히 다르기 때문에, 둘을 같이 쓰는 게 거의 항상 더 낫습니다. 이게 하이브리드 검색의 출발점이에요.
문제는 두 검색 점수의 스케일이 다르다는 점입니다. BM25 점수는 0~30쯤, 코사인 유사도는 0~1, 재랭커 점수는 0~100, 이런 식으로 단위가 제각각이라 단순히 더하거나 곱할 수 없어요. RRF(Reciprocal Rank Fusion, 역수 순위 융합)는 이걸 우회합니다. 각 검색 결과의 1위, 2위, 3위에 1/(k+rank) 같은 작은 점수를 주고, 모든 검색의 점수를 더해서 순위를 다시 매겨요. k는 보통 60 정도. 점수 스케일을 몰라도 되니 모델·검색기를 갈아끼워도 안정적이고, 구현은 10줄짜리 휴리스틱입니다.
MMR(Maximal Marginal Relevance, 최대 한계 관련성)은 결과의 다양성을 보장합니다. RRF만 쓰면 BM25 1위와 벡터 1위가 거의 같은 문서 두 개일 수 있는데, 그러면 사용자에게 사실상 1개 결과만 주는 거예요. MMR은 이미 뽑힌 결과와의 유사도를 패널티로 두고, 관련성 − λ × (이미 뽑힌 것들과의 최대 유사도)를 점수로 써서 다음 결과를 뽑습니다. λ로 관련성 vs 다양성 비중을 조절해요. 사용자 경험 면에서 5개 달라 보이는 결과를 보장하는 핵심 장치입니다.
실무 하이브리드 RAG 파이프라인은 보통 4단입니다. 1) BM25·벡터 두 검색을 동시에 돌리고, 2) RRF로 순위 융합, 3) cross-encoder reranker(질문-문서 쌍을 같이 인코딩하는 더 비싼 모델)로 상위 50개를 다시 5개로 추리고, 4) MMR로 5개 안에서 다양성을 확보합니다. 이 모든 게 oxibrain rank 모듈에 구현돼 있고, No language privileged 원칙에 따라 한국어·영어·일본어 문서가 동일한 파이프라인으로 동등 처리됩니다.
- 하이브리드 검색의 존재 이유: 키워드(BM25)와 벡터의 실패 모드가 정확히 달라서 결합이 거의 항상 우월
- RRF = 1/(k+rank) 점수 합산 — 스케일 정규화 불필요, 모델·점수 단위 무관, k=60이 표준
- MMR = 관련성 − λ × 중복 패널티 — 5개 달라 보이는 결과를 보장, λ로 비중 조절
- 실무 4단 파이프라인: BM25+벡터 → RRF 융합 → cross-encoder rerank → MMR 다양화
우리 코드oxibrain rank 계열이 RRF + MMR을 실제 구현 — P11 No language privileged 원칙 하에 다국어를 동일 처리.
커리큘럼 체인 RAG 파이프라인
Fine-tuning vs RAG vs 프롬프트 Fine-tuning vs RAG vs Prompting 직무 그림 우리코드
LLM의 능력을 끌어내는 세 갈래. 프롬프트는 비용 0에 반복 가능, RAG는 외부 지식·시효성 보장, Fine-tuning은 출력 형식·스타일·도메인 언어 학습. 세 가지는 경쟁이 아니라 계층적 결합으로 쓰인다.
프롬프트 엔지니어링은 가장 가성비 좋은 출발점입니다. 시스템 프롬프트에 한국어로 답변, 출처 명시, 모르면 모른다고 답해 같은 규칙을 적으면 외적 지식 없이 출력 스타일을 바꿀 수 있고, few-shot 예시 3~5개만 붙여도 형식·길이·톤을 거의 통제할 수 있어요. 비용은 0이고, 변경 즉시 반영되니 운영 중 실험도 자유롭습니다. 한계는 컨텍스트 길이에 갇혀서 방대한 도메인 지식을 줄 수 없다는 거예요.
RAG는 외부 지식·시효성 보장 측면에서 압도적입니다. 사내 매뉴얼, 법률, 사양서 같은 방대한 자료를 청크로 잘라 인덱싱해 두고, 사용자가 물을 때마다 관련 청크를 찾아 프롬프트에 같이 넣어요. 정보가 바뀌어도 재임베딩만 하면 즉시 반영되고, 출처 인용도 가능합니다. 반면 모델의 사고방식이나 출력 형식 같은 스타일은 거의 못 바꿔요. 그래서 RAG는 무엇을 아느냐를 결정하고, 프롬프트는 어떻게 말하느냐를 결정한다고 보면 됩니다.
Fine-tuning은 모델 가중치 자체를 조정해요. LoRA·QLoRA 같은 파라미터 효율 기법으로도 충분히 효과 있고, 모델이 특정 도메인 어휘·문체·형식(JSON schema, 함수 호출 포맷 등)에 익숙해져야 할 때 쓰입니다. 예를 들어 민원 답변을 다뤄야 하는 챗봇은 fine-tuning이 거의 필수예요. 단점은 GPU 비용·학습 데이터 준비·버전 관리·평가 파이프라인이 다 필요하다는 점, 그리고 새로운 정보가 들어오면 재학습이 필요하다는 점이에요.
그래서 요즘 표준은 세 가지를 동시에 쓰는 계층적 결합입니다. 1) 베이스 모델은 instruction tuning + DPO/RLHF로 정렬된 걸 쓰고, 2) 도메인 fine-tuning으로 우리 어휘·문체·스키마를 체화시키고, 3) RAG로 시의성 있는 외부 지식을 프롬프트에 주입, 4) 각 요청마다 프롬프트 템플릿으로 형식·톤·제약을 마지막으로 다듬어요. native tool calling을 못 하는 모델은 dialect 모듈(텍스트로 tool 호출 흉내, JSON 코드블록 마커 같은 패턴)으로 fallback하는 방식도 fine-tuning의 한 갈래입니다 — oxicode-ai의 dialect 모듈이 정확히 이 역할.
- 3갈래의 역할 분담: 프롬프트=어떻게 말하느냐, RAG=무엇을 아느냐, Fine-tuning=어떤 어휘·문체로 말하느냐
- 비용 순서: 프롬프트(0) < RAG(임베딩·검색 인프라) < Fine-tuning(GPU + 학습 데이터 + 평가 파이프라인)
- 표준 결합 패턴: instruction-tuned base + 도메인 fine-tuning + RAG + 프롬프트 템플릿
- native tool calling 미지원 모델은 dialect(텍스트 마커 파싱)로 fallback — fine-tuning의 대체재가 아니라 프롬프트 계약층
우리 코드oxicode-ai dialect 모듈이 native tool 지원 없는 모델용 텍스트 프로토콜(coercion·XML 등) — 프롬프트 계약의 엔지니어링.
Tool-calling / Function calling Tool Use / Function Calling 자소서 직무 그림 우리코드
LLM이 지금 이 함수를 이런 인자로 호출해줘라고 구조화된 출력(JSON schema)을 내면, 외부 시스템이 그걸 실행하고 결과를 다시 컨텍스트에 넣는 패턴. 단순 키워드 매칭을 넘어 LLM이 내부 시스템과 직접 연동되게 하는 표준 인터페이스.
챗봇이 내부 시스템과 연동된다는 말의 기술적 실체가 바로 tool-calling입니다. 모델은 단순히 텍스트를 생성하는 게 아니라, 미리 등록된 함수 목록(tool list)을 보고 이 상황에서는 이 함수가 필요하다라고 판단해서 JSON 형식의 인자(arg) 묶음을 생성해요. 그러면 시스템이 그걸 받아서 실제 함수를 실행하고, 반환값을 다시 대화에 주입합니다. 사용자는 민원 조회해줘라고만 말했는데 챗봇이 알아서 사내 DB를 조회한 결과를 보여주는 게 이 메커니즘 덕분이에요.
구체적인 루프는 3단계입니다. 모델은 매 대화 턴마다 호출 가능한 tool list(예: search_documents, query_db, send_email)를 보고, 자신의 reasoning 결과로 어떤 tool을 어떤 인자로 부를지 결정해요. 출력은 JSON schema로 강제돼 있어서, 시스템은 그 schema로 검증(serde_json, Zod, Pydantic 등)해서 잘못된 형식이면 재시도하거나 거절합니다. 실행 결과는 다시 컨텍스트에 메시지로 들어가서, 모델이 다음 턴에서 그 결과를 보고 후속 tool을 부르거나 최종 답변을 써요. 한 번의 사용자 요청에 이런 tool call이 3~5번 연쇄되는 게 흔합니다.
가장 흔한 실패는 모델이 schema에 어긋난 인자를 만들어내는 것입니다. 필수 필드 빠뜨리기, enum 값 범위 오류, 타입 불일치(문자열이어야 하는데 숫자) 같은 게 대표적이에요. 그래서 운영에서는 schema validation + retry(같은 입력으로 한 번 더 시도) + fallback(검증 실패 시 사람이 개입하거나 다른 tool로 우회) 세 단계가 거의 항상 필요합니다. 또 환각 tool, 즉 모델이 학습 데이터에서 비슷하게 본 적 있는 함수명을 지어내서 호출하는 경우도 막아야 해서, tool list를 가능한 한 좁고 정확하게 유지하는 게 중요해요.
모든 모델이 native tool calling을 지원하는 건 아닙니다. GPT-4·Claude·Llama 3.1+ 같은 모델은 API 레벨에서 tool call을 구조화 출력으로 받지만, 구형 모델이나 특화 모델은 그냥 텍스트만 생성해요. 그래서 두 갈래가 있습니다. native tool(use API 자체)을 쓸 수 있으면 그걸 쓰고, 못 쓰는 모델은 dialect 모듈이 텍스트에서 JSON 코드블록 마커를 파싱해 tool call로 사후 변환하는 식으로 fallback합니다. oxicode-ai는 두 갈래를 모두 지원하고, oxios의 AgentTool은 with_auth 같은 권한 통제, rate limit, auditing을 결합해서 tool이 안전하게 운영 환경에서 쓰이게 합니다.
- 챗봇이 내부 시스템과 연동의 기술적 실체 — LLM이 JSON 인자 출력, 시스템이 실행, 결과를 다시 컨텍스트에 주입
- 3단계 루프: tool list 보고 → JSON schema로 인자 출력 → 외부 실행 → 결과를 컨텍스트에 주입
- 흔한 실패: schema 불일치(필드 빠뜨림, enum 범위 오류, 타입 불일치) → validation + retry + fallback
- 운영 안전장치: with_auth(권한), rate limit, auditing — 민감 tool에 대한 통제 필수
우리 코드oxicode-ai의 native tool use + 미지원 모델 폴백(dialect), oxios의 외부 API AgentTool with_auth(권한 통제) — 시선아이티 챗봇의 운영형 tool-calling 완성형.
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계
AI 에이전트 (LLM + 도구 + 메모리 + 루프) AI Agent 자소서 직무 그림 우리코드
에이전트란 LLM 한 개에 도구(tool-calling)·단기 메모리·장기 메모리·반복 루프를 붙여 '목표를 향해 자율적으로 움직이는 단위'로 만든 것이다.
에이전트라는 말을 들으면 SF 같지만 실체는 단순합니다. 매 턴 (1) 사용자의 목표를 다시 읽고 (2) 다음에 할 행동(도구 호출·검색·코드 실행)을 결정하고 (3) 결과를 관찰하고 (4) 종료 조건이 충족될 때까지 1~3을 반복합니다. 이 네 단계가 한 사이클이고, LLM은 1·2·4의 의사결정, 시스템은 3의 실행을 담당합니다. 그래서 '에이전트 = LLM + 도구 + 메모리 + 루프'라는 정의가 통용됩니다.
ReAct(Reason+Act) 루프가 사실상 표준이에요 — 생각(이유 서술) → 행동(도구 호출) → 관찰(결과 읽기)을 한 사이클로 돕니다. 자기 비평을 덧붙인 변형이 Reflexion(실패를 되돌아보고 재시도), 계획-실행 분리형이 Plan-and-Execute예요.
설계의 핵심 난제는 '언제 끝낼 것인가'(종료 신호)와 '비용을 어떻게 통제할 것인가'(토큰 예산)입니다. 무한 루프에 빠지면 한 번 세션에 수백만 원이 청구되기도 해요. 그래서 max_steps·timeout·토큰 예산을 명시적으로 둡니다.
- 에이전트 = LLM + 도구(tool-calling) + 메모리 + 반복 루프
- ReAct(Reason+Act) 루프: 생각 → 행동 → 관찰을 한 사이클로 도는 것이 현재 사실상 표준
- 에이전트 비용 폭탄: 종료 신호 없이 무한 루프 → max_steps·timeout·토큰 예산을 명시적으로
- 변형 패턴: Reflexion(자기 비평 재시도) · Plan-and-Execute(계획-실행 분리)
우리 코드oxicode-agent가 도구 호출 루프를 담당합니다. 시선아이티 챗봇 직무의 '내부 시스템 연동'이 바로 이 루프 + tool-calling 구조.
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계
멀티에이전트 시스템 Multi-Agent System 직무 우리코드
한 에이전트로 풀기 버거운 문제를 역할로 나눠 협업시키는 구도 — 관리자-노동자, 토론, 파이프라인 세 형태가 대표다.
멀티에이전트는 한 에이전트로 풀기 버거운 문제를 역할로 나눠 푸는 방식입니다. 대표적으로 (a) 관리자-노동자(Supervisor-Worker): 관리자가 작업을 쪼개고 노동자가 처리 (b) 토론(Debate): 두 에이전트가 서로 반대 입장으로 답을 내고 판정자가 채택 (c) 파이프라인: 한 에이전트의 출력이 다음 에이전트의 입력이 되는 형태.
왜 쓰냐면 — 단일 에이전트에 너무 긴 역할·지식을 한 프롬프트에 담으면 '잃어버린 중간(lost in the middle)' 문제와 컨텍스트 오염이 생겨요. 역할을 분리하면 각 에이전트의 프롬프트·도구·권한이 작고 명확해집니다. 단점도 명확해요 — 에이전트 간 통신 비용(토큰)이 곱으로 늘고, 디버깅이 어려워져요. '멀티에이전트가 항상 나은가'라는 질문엔 '아니다 — 역할 분리가 명확할 때만'이 정답.
설계의 핵심 난제는 단일 에이전트와 같습니다 — '언제 끝낼 것인가'(종료 신호)와 '비용 통제'(토큰 예산). 멀티는 여기에 '에이전트 간 합의·중재'가 추가돼요.
- 3형태: Supervisor-Worker(관리자-노동자) · Debate(토론+판정) · Pipeline(출력→입력)
- 역할 분리 = 프롬프트·도구·권한 최소화 — 컨텍스트 오염 방지
- 단점: 통신 토큰 비용 곱증가 · 디버깅 난도 상승 — 역할 분리가 명확할 때만
- AutoGen의 GroupChat·oxios의 AgentGroup/AgentSupervisor가 같은 패턴
우리 코드oxios의 AgentGroup/AgentSupervisor가 멀티에이전트 조율 — AutoGen의 GroupChat/GroupChatManager와 동형입니다.
관련 항목: 이동
자기-개선 루프 / Ouroboros Self-Improvement Loop / Ouroboros 우리코드
Ouroboros는 자기 꼬리를 무는 뱀 — 에이전트가 자기 산출물을 다시 평가·수정하는 루프. 외부 평가자 없이 '자기 비평 → 재작업 → 재평가'를 반복한다.
Ouroboros는 이름 그대로 자기 자신을 꼬리로 물고 있는 뱀 — 자신의 산출물을 다시 평가·수정하는 루프입니다. 외부 평가자가 없는 대신 '자기 비판(self-critique) → 재작업(rewrite) → 재평가'를 반복합니다.
oxios의 Ouroboros는 이 패턴을 코드베이스 자기개선에 적용 — 에이전트가 코드 변경 → diff 생성 → 정적 분석 → 실패 시 재시도 → 성공 시 커밋 흐름을 자동화합니다. '평가를 사람이 아니라 정적 분석·테스트 같은 기계 판정으로 돌린다'는 점이 핵심이에요 — LLM 자기 비평만으로는 환각이 누적될 수 있으니까요.
ReAct(tool-calling 루프)가 '외부와 상호작용' 중심이라면 Ouroboros는 '자기 산출물 품질 향상'에 무게중심이 있습니다. 둘 다 루프지만 방향이 달라요.
- 자기 비평 → 재작업 → 재평가의 반복 — 외부 평가자 불필요
- 기계 판정(정적 분석·테스트)으로 평가해야 환각 누적 방지
- ReAct vs Ouroboros: 둘 다 루프지만 후자는 '자기 평가·자기 수정'까지 포함
- 반복 한계(최대 N회)와 비용 상한을 두는 것이 실무 필수
우리 코드oxios-ouroboros가 자기개선 루프를 담당합니다 (directive, engine, model_resolver, prompts, resilience, fallback). 세 레이어(oxicode-agent 도구 루프 + oxios 멀티에이전트 + ouroboros 자기개선)가 한 에이전트 생태계를 이룹니다.
관련 항목: 이동
LLM 평가 / 환각 / LLM-as-judge LLM Evaluation 직무 그림 우리코드
LLM 평가란 모델 출력의 품질을 정량적으로 측정하는 일체의 방법 — RAG의 faithfulness·precision·recall, 생성 답의 정확도·지연·비용까지 포함합니다. 환각은 '근거에 없는 사실을 그럴듯하게 지어내는' 현상이고, LLM-as-judge는 그 평가를 더 강한 LLM에게 맡겨 자동화하는 패턴입니다.
LLM-as-judge = LLM as a judge(LLM을 평가자로 활용)
RAG 시스템을 만들면 가장 먼저 부딪히는 질문이 '근거에 충실한 답을 내고 있는가?'입니다. RAGAS 프레임워크가 사실상 표준인데, 핵심 지표는 (1) Context Precision: 검색으로 가져온 문맥 중 진짜 관련있는 비율 (2) Context Recall: 정답에 필요한 문맥이 다 들어왔는가 (3) Groundedness: 답이 문맥 안에 있는 사실만으로 구성됐는가 (4) Answer Relevance: 답이 질문에 맞는가 (5) Faithfulness: 답이 문맥과 모순되지 않는가. Faithfulness가 환각 검출의 대표 지표입니다.
환각은 두 종류로 나뉩니다. (a) Intrinsic: 답 안에 문맥에 없는 단어가 등장 — '문맥 X'를 '문맥 X에 있는 내용'으로 둔갑 (b) Extrinsic: 답 자체는 문맥에 없지만 외부 사실과 충돌하는 정보 추가. RAG 환경에서는 거의 intrinsic이 문제이고, 그래서 '근거 span 안에 있는 단어만 사용' 같은 제약을 걸거나 생성 후 '답의 각 문장을 임베딩 → 문맥 청크와 코사인 유사도 측정 → 임계값 미만은 환각 의심' 같은 후처리 패턴이 쓰입니다.
LLM-as-judge는 사람 평가 대신 더 강한 LLM(GPT-4·Claude Opus급)에게 채점을 맡기는 패턴입니다. 비용이 사람 평가 대비 1/10 이하로 떨어지고 일관성도 높아서 실무에서 폭넓게 쓰입니다. 다만 '판정자 편향(position bias — 앞쪽 답 선호·길이 편향·자기 답변 선호)'이 있어서 — 같은 답을 순서를 바꿔 두 번 평가하거나, 두 모델에게 교차 평가시키거나, 사람이 표본 검수하는 식으로 보정합니다. 면접에서는 '왜 GPT-4가 자기 답에 호의적인가' 같은 함정 질문을 던지기도 합니다.
- RAG 표준 지표 5종 — Context Precision/Recall, Groundedness, Answer Relevance, Faithfulness (RAGAS 기반)
- 환각 검출의 실무 패턴: '답의 각 문장을 임베딩 → 문맥 청크와 코사인 유사도'로 구조적으로 0에 가깝게 잡습니다
- LLM-as-judge 편향: position bias·length bias·self-preference — 두 모델 교차 평가·순서 셔플로 보정
- 평가 자동화는 CI 게이트에 묶어야 합니다 — 코드 변경마다 회귀 여부를 잡아야 운영 비용이 들지 않습니다
우리 코드oxibrain의 EvalMetrics가 statement_precision/recall·resolution_f1·fabricated_entity_rate를 측정하고 — 후자는 구조적으로 0.00이 나오는 게 목표입니다. eval --suite fast가 CI 게이트로 묶여 있어 회귀가 자동 차단됩니다.
관련 항목: 이동
프롬프트 엔지니어링 / In-Context Learning Prompt Engineering 자소서 직무 그림 우리코드
프롬프트 엔지니어링은 LLM에게 지시할 텍스트를 의도대로 설계하는 기법 — 역할·페르소나·few-shot 예시·chain-of-thought(CoT)·출력 스키마 강제를 조합합니다. In-Context Learning은 그중에서도 '별도 학습 없이 예시 몇 개만 프롬프트에 넣어 그 패턴을 모방하게 하는' 패턴을 가리킵니다.
ICL = In-Context Learning(문맥 내 학습 — 예시 몇 개만 프롬프트에 넣어 패턴 학습)
프롬프트 엔지니어링을 '예의 바르게 부탁하기'쯤으로 생각하면 안 됩니다. 실무에서는 거의 계약서처럼 다룹니다. (1) System 메시지에 역할·제약·출력 형식 명시 (2) Few-shot에 '입력-출력 예시' 2~5개 (3) Chain-of-Thought로 '단계적으로 생각한 뒤 답해' 같은 중간 추론 유도 (4) 출력 스키마 강제 — JSON mode·grammar-constrained decoding·GBNF 문법으로 잘못된 형식 자체가 나오지 않게. 4번이 가장 효과적입니다 — 모델이 형식을 틀릴 수 없다면 후처리 파싱 비용이 0이 됩니다.
In-Context Learning은 '학습 데이터 없이 학습을 시키는' 묘기로 보이지만 실제로는 (a) 모델이 사전학습 때 이미 패턴 복사 능력을 가지고 있고 (b) 프롬프트에 들어간 예시가 그 능력을 '활성화'하는 구조입니다. 그래서 예시 순서·다양성·품질이 결과를 크게 바꿉니다. Few-shot 예시는 '정답을 보여주는' 게 아니라 '출력 포맷과 사고 방식을 시연하는' 것이고, 너무 많으면 컨텍스트 윈도우를 잡아먹고 적으면 효과가 떨어집니다 — 보통 3~5개가 sweet spot입니다.
가장 큰 운영 리스크는 prompt injection입니다. 사용자 입력(검색 결과·외부 문서·이메일 본문)에 '이전 지시를 무시하고 다음을 따라해' 같은 문구가 섞여 들어오면 system 지시를 덮어쓸 수 있습니다. 방어 패턴은 (1) 신뢰 경계 분리 — 시스템 메시지와 사용자 입력 사이에 명시적 구분자 (2) 사용자 입력은 항상 'data:' 태그 안에 (3) 도구 호출 결과는 별도 검증 후 시스템에 합류 (4) 중요 액션은 사람 승인 단계 강제. 면접 단골 질문이 '프롬프트 인젝션 어떻게 막는가'입니다.
- Chain-of-Thought: '단계별로 생각한 뒤 답하라'는 지시만으로 산술·논리 정확도가 크게 올라갑니다
- 구조화 출력: JSON mode·grammar-constrained decoding·GBNF — 파싱 실패가 원천 차단됩니다
- Prompt Injection 방어: 신뢰 경계 분리(data:/tool: 태그)·도구 호출 후 재검증·중요 액션 사람 승인
- ICL의 sweet spot: few-shot 예시는 보통 3~5개. 많으면 컨텍스트 낭비, 적으면 패턴 학습 실패
우리 코드oxicode-ai의 dialect 모듈이 텍스트 프로토콜(coercion·XML 태그)을 정의합니다 — 프롬프트를 '계약'으로 다루는 구조라 시스템 메시지와 사용자 입력이 강하게 분리되어 있어 prompt injection 면이 작습니다.
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계
BERT vs GPT vs T5 (모델 패밀리) Model Families 직무 표 그림
세 모델 패밀리는 트랜스포머를 다르게 잘라 썼습니다. BERT는 인코더만 — 양방향 어텐션으로 문맥을 깊게 이해해 분류·임베딩에 강합니다. GPT는 디코더만 — causal mask로 다음 토큰만 예측해 생성·대화·추론에 강합니다. T5는 인코더-디코더 — 입력 전체를 이해한 뒤 조건부로 생성해 번역·요약에 강합니다.
BERT(Bidirectional Encoder Representations from Transformers)는 2018년 구글이 공개하며 MLM(Masked Language Modeling) 사전학습을 도입했습니다. 문장의 15% 토큰을 가리고 '가린 게 뭐였는지' 맞추는 태스크라 양쪽 방향 어텐션이 자연스럽게 학습됩니다. 분류·개체명 인식·문장 유사도처럼 '이 문장/문서 전체를 한 벡터로 요약'해야 하는 작업에 최적화돼 있고, 그래서 임베딩 모델·검색 reranker의 주류가 되었습니다. 한국어로는 KoELECTRA·KR-ELECTRA·KLUE-BERT 등이 같은 계열입니다.
GPT(Generative Pre-trained Transformer)는 OpenAI의 디코더-온리 모델입니다. causal mask로 '오른쪽(미래) 토큰을 가린 채' 다음 토큰만 예측하는 CLM(Causal Language Modeling) 사전학습이 핵심 — 그래서 자연스러운 생성이 가능하고 zero-shot·few-shot 일반화 능력이 두드러집니다. GPT-3에서 in-context learning이 발견됐고, GPT-4·Claude·Gemini 모두 이 디코더-온리 계열을 대화형으로 발전시켰습니다. 생성·요약·코드 작성·추론 — 출력 텍스트가 곧 산출물인 모든 작업의 표준이 됐습니다.
T5(Text-to-Text Transfer Transformer)는 구글이 모든 NLP 작업을 '입력 텍스트 → 출력 텍스트'로 통일한 인코더-디코더 모델입니다. 번역('translate English to German: …' → '…'), 요약('summarize: …' → '…'), 분류('cola sentence: …' → 'acceptable/not acceptable')까지 단일 포맷으로 처리합니다. 인코더가 입력 전체를 한 번에 이해하고 디코더가 그 위에 조건부로 답을 만들기 때문에 '입력과 출력이 같은 모달리티인 생성(번역·요약·질의응답)'에 특히 강합니다. 최근에는 BART·Flan-T5·mT5가 이 계열의 대표입니다. 면접에서는 '� 분류는 BERT, 생성은 GPT냐'는 질문을 받기 쉽고, 답은 '양방향 vs 단방향 어텐션이 필요하느냐'입니다.
| 구분 | 특징 | 참고 |
|---|---|---|
| 구조 | Encoder-only | 인코더만 사용 |
| BERT 계열 어텐션 | 양방향 | 앞뒤 토큰 동시 참조 |
| 사전학습 | MLM | 토큰 15% 가리고 맞추기 |
| 주 용도 | 분류·NLI·임베딩·검색 reranker | |
| 대표 한국어 모델 | KoELECTRA·KLUE-BERT | 한국어 위키·뉴스로 학습 |
| 구조 | Decoder-only | 디코더만 사용 |
| GPT 계열 어텐션 | Causal(단방향) | 미래 토큰 마스킹 |
| 사전학습 | CLM | 다음 토큰 예측 |
| 주 용도 | 생성·대화·추론·코드 | |
| 대표 모델 | GPT-4·Claude·Gemini | 현대 LLM 표준 |
| 구조 | Encoder-Decoder | 둘 다 사용 |
| T5 계열 어텐션 | 인코더 양방향 + 디코더 causal | |
| 사전학습 | Span Corruption | 연속 span 가리고 복원 |
| 주 용도 | 번역·요약·조건부 생성·QA | |
| 대표 모델 | T5·BART·Flan-T5·mT5 | 다국어 변형 mT5 |
- Encoder-only(BERT) = 양방향 어텐션, 분류·NLI·임베딩에 최적
- Decoder-only(GPT) = causal attention, 생성·대화·추론에 최적
- Encoder-Decoder(T5) = 입력 이해 + 조건부 생성, 번역·요약·QA에 최적
- Foundation Model → SFT(지도 미세조정) → Preference Tuning(RLHF/DPO) 순으로 도메인 적용
관련 항목: 이동
커리큘럼 체인 챗봇 직무 4단계
KV Cache / 디코더 최적화 KV Cache 그림 우리코드
KV Cache는 autoregressive 디코더에서 이미 계산된 토큰들의 Key·Value 행렬을 메모리에 저장해 두는 기법입니다. 매 토큰 생성마다 전체 컨텍스트를 재계산하지 않게 해 추론 지연을 크게 줄여주지만, 컨텍스트 길이에 비례해 메모리를 선형으로 잡아먹어 긴 문맥일수록 병목입니다.
KV = Key-Value(키-값 — 어텐션의 K·V 행렬)
왜 KV Cache가 필요한지부터 보겠습니다. 디코더가 한 토큰을 생성할 때 self-attention은 (1) 지금까지 본 모든 토큰의 Query·Key·Value를 다시 계산하고 (2) 그 값으로 어텐션 가중치를 만들어 (3) 다음 토큰의 확률 분포를 얻습니다. 여기서 '이미 본 토큰'의 Key와 Value는 변하지 않는데 — 매번 새로 계산하는 건 순수 낭비입니다. 그래서 '이전 토큰들의 K·V만 따로 저장해두고, 새 토큰의 Q만 계산해 기존 K·V와 내적'하는 게 KV Cache의 본질입니다. 이거 하나로 추론 지연이 수십 배에서 수백 배까지 줄어듭니다.
KV Cache 메모리 비용은 (레이어 수) × (헤드 수) × (헤드 차원) × (컨텍스트 길이) × (토큰당 K·V 2개) × (정밀도 바이트)입니다. 7B 모델(레이어 32·헤드 32·헤드 차원 128·FP16 2바이트)에서 컨텍스트 4K 토큰만 받아도 KV Cache가 약 1GB, 32K로 늘리면 8GB, 128K면 32GB로 불어�니다 — 모델 가중치(7B FP16 = 14GB)와 별개로 추가로 잡히는 메모리라 컨텍스트가 길어지면 가중치를 GPU에서 내려야 하는 일도 생깁니다. 그래서 '왜 128K 컨텍스트 모델이 일반적으로 두 대의 A100/H100을 요구하는가'의 답이 여기에 있습니다.
디코더 최적화는 KV Cache를 줄이되 성능을 유지하는 방향으로 진화해 왔습니다. (1) Multi-Query Attention(MQA)·Grouped-Query Attention(GQA): 헤드 여러 개가 K·V를 공유 → KV 메모리 8~32배 감소 (2) PagedAttention(vLLM): KV Cache를 페이지 단위로 관리해 메모리 fragmentation 해결 → 동시 요청 처리량 2~4배 (3) Flash Attention: 어텐션을 GPU SRAM에서 tile 단위로 계산해 HBM 왕복 최소화 (4) Speculative decoding: 작은 모델이 후보 토큰 여러 개를 먼저 뽑고 큰 모델이 한꺼번에 검증 → wall-clock 2~3배. 면접에서는 '왜 컨텍스트 길이가 GPU 메모리와 직결되는가' 또는 'PagedAttention이 뭔가'를 자주 묻습니다.
- KV Cache 메모리 = 레이어·헤드·헤드 차원·컨텍스트 길이에 선형 비례 — 컨텍스트 4배 늘면 메모리도 4배
- PagedAttention(vLLM)이 KV 캐시 fragmentation을 해결해 동시 추론 throughput을 크게 끌어올렸습니다
- Speculative decoding: 작은 모델이 후보 → 큰 모델이 검증. wall-clock 2~3배 단축 (품질 동일)
- GQA·MQA: 여러 어텐션 헤드가 K·V를 공유 → KV 메모리 8~32배 절약하면서 품질 손실 최소화
우리 코드oxicode-ai dialect가 '완성된 토큰 스트림'을 SSE로 직접 흘려보내는 streaming-first 구조입니다 — 첫 토큰 지연(TTFT)을 줄이는 쪽에 설계 무게중심이 있고, KV Cache 메모리 자체의 최적화는 추론 백엔드(vLLM·llama.cpp) 레벨의 일입니다.
관련 항목: 이동
Safety / Alignment / RLHF Safety / Alignment 그림 우리코드
Alignment란 LLM의 출력을 의도된 인간의 가치·정책·제약에 맞추는 작업입니다. Safety는 '유해·위험 출력 차단'이라는 Alignment의 하위 영역이고, RLHF는 정렬을 구현하는 표준 학습 방법입니다 — 사람이 매긴 선호 순위를 보상 모델로 학습시켜 그 보상 모델로 정책 모델을 미세조정합니다.
RLHF = Reinforcement Learning from Human Feedback(인간 피드백 기반 강화학습 — 사람이 매긴 선호 점수로 보상 모델을 학습시켜 정책 모델을 fine-tune)
RLHF는 세 단계로 동작합니다. (1) SFT(지도 미세조정) — 사람이 쓴 (지시, 모범답) 쌍으로 기본 모델 fine-tune (2) 보상 모델 학습 — 같은 질문에 대해 모델이 만든 답변 여러 개를 사람이 순위 매김 → 그 순위로 보상 모델(RM) 학습 (3) PPO 또는 그 변종으로 보상 모델의 점수를 최대화하도록 정책 모델을 추가 학습. 핵심은 '사람이 정답 텍스트를 쓰는 건 비싸고 느려서, 순위 매기는 건 상대적으로 싸고 빠르다'는 관찰에서 출발했습니다.
RLHF 이후의 진화는 두 갈래입니다. (a) Constitutional AI / RLAIF: 사람이 '원칙(헌법)'을 적으면 LLM이 자기 답을 그 원칙으로 평가·수정 — 사람 라벨 비용을 0에 가깝게 줄임 (b) DPO(Direct Preference Optimization): 보상 모델과 정책 모델을 분리하는 대신, 선호 데이터로 직접 정책을 학습 — PPO보다 안정적이고 구현 단순. 최근엔 DPO·IPO·KTO 등 RL-free 정렬이 표준이 되어가는 추세입니다.
운영에서 Safety의 가장 큰 리스크는 jailbreak와 prompt injection입니다. Jailbreak는 'DAN·역할극·언어 바꾸기' 같은 우회로 안전 가드를 뚫는 시도고, prompt injection은 사용자 입력 안에 system 지시를 덮어쓰는 문구를 섞는 공격입니다. 방어는 (1) 입력 분류기(harmful intent classifier) (2) 출력 필터(PII·유해 카테고리) (3) 도구 호출은 화이트리스트 + 인자 검증 (4) 모든 결정의 audit log (5) 중요 액션 전 사람 승인. 공공 SI는 '책임 있는 AI' 가드라인으로 이 항목이 필수 체크리스트입니다.
- RLHF 3단계: SFT → 보상 모델 학습 → PPO로 정책 fine-tune. 보상 모델은 '사람이 매긴 선호 순위'로 학습
- Constitutional AI / RLAIF: 사람이 '원칙'만 정의하면 LLM이 자기 답을 평가·수정 — 사람 라벨 비용 거의 0
- DPO: 보상 모델 없이 선호 데이터로 직접 정책 학습 — PPO보다 안정적이고 구현 단순, 최근 표준으로 자리잡는 중
- Jailbreak·prompt injection은 운영 Safety의 핵심 리스크 — 입력 분류기 + 출력 필터 + 도구 화이트리스트 + audit log + 사람 승인 다중 방어
우리 코드oxios의 AuditTrail이 모든 에이전트 호출·도구 사용·결정을 기록합니다 — '책임 있는 AI' 요건(추적 가능성·재현 가능성·사람 승인)을 구조적으로 만족시킵니다. 면접에서 '안전·추적성 어떻게 보장하나' 물으면 AuditTrail을 근거로 설명할 수 있습니다.
관련 항목: 이동
sLLM / 경량 언어모델 · 온프레미스 LLM sLLM (Small LLM) & On-Prem LLM 자소서 직무 그림 우리코드
sLLM은 파라미터 수를 의도적으로 줄여 GPU 자원이 제한된 환경(단일 A10·L4·CPU-only)에서도 추론이 가능한 LLM입니다. 공공·금융 도메인은 '데이터 외부 반출 금지' 규제가 강해 sLLM + 온프레미스(폐쇄망) 구성 사실상 표준입니다. 한국어 특화 모델로는 EXAONE·HyperCLOVA X·KoGPT·구름이 대표입니다.
sLLM = small Large Language Model(소형 대규모 언어모델 — 파라미터 수를 억억 단위 대신 수십억 단위로 줄인 LLM)
왜 sLLM이 필요한지를 먼저 짚으면, 일반 LLM이 가진 운영 비용과 데이터 주권 두 가지가 결정적입니다. 70B 모델은 FP16 한 장(140GB)·추론은 A100/H100 여러 장이 필요해서 SaaS API로 부르면 토큰당 비용이 수십 원, self-host면 초기 GPU 투자만 수억 원입니다. 반면 7B·sLLM은 INT4 양자화 시 약 4GB — 일반 워크스테이션 GPU(L4·RTX 4090) 한 장, 심지어 CPU-only로도 동작합니다. 그래서 (a) 응답 지연이 짧고 (b) 트래픽 변동에 탄력적이고 (c) 토큰당 비용이 거의 0이라는 운영상 이점이 큽니다.
공공 SI 맥락에서 sLLM이 거의 유일한 선택지인 이유가 '데이터 외부 반출 금지' 규정입니다. 전자정부·지자체·금융·의료는 개인정보·내부 문서가 외부 API로 넘어가는 것 자체가 NIPA·FSA 가이드라인 위반입니다. 그래서 (1) 폐쇄망(air-gapped) 인프라 (2) 온프레미스 GPU 서버 (3) 한국어 특화 sLLM — 이 세 가지가 묶여서 표준 아키텍처가 됩니다. 한국지능정보사회진흥원(NIPA)이 이런 환경용 한국어 sLLM을 지속 공개하고 있고, 2024년 이후로는 EXAONE·HyperCLOVA X·Polyglot-Ko 같은 모델이 공공 레퍼런스로 자리잡았습니다.
sLLM의 약점은 (1) 일반 LLM 대비 추론 품질·지시 따르기 능력↓ (2) 한국어 외 다국어 처리 약 (3) 긴 컨텍스트(약 4K) 한계 — 그래서 (a) 도메인 미세조정(SFT) (b) RAG로 지식 보강 (c) Few-shot 프롬프트 패턴화로 품질을 끌어올립니다. 기술적으로는 양자화(INT8·INT4·GGUF Q4_K_M) + 증류(Distillation) + QLoRA가 표준 스택입니다. 면접에서는 '왜 일반 LLM이 아니라 sLLM을 선택했나'를 '데이터 주권 + 폐쇄망 + 운영 비용' 세 축으로 설명할 줄 알아야 합니다.
- 양자화(Quantization) — INT8/INT4로 가중치 압축 + QLoRA로 미세조정, 7B 모델이 INT4로 약 4GB로 줄어 단일 GPU 운용 가능
- 증류(Distillation) — 큰 모델의 지식을 작은 모델로 전달, sLLM의 품질을 끌어올리는 대표 기법
- 온프레미스 GPU 운용 — A100/H100 단일 노드부터, sLLM은 A10/L4·CPU-only까지 가능
- 공공 망에서 외부가 안 되는 경우 — 폐쇄망 + 사내 추론 서버가 사실상 표준 아키텍처
우리 코드시선아이티의 sLLM 플랫폼·AI Chatbot이 이 카테고리입니다 — 폐쇄망 + 온프레미스 GPU + 한국어 특화 sLLM + RAG 스택. 면접에서는 '왜 SaaS API가 아니라 sLLM을 고르셨나'를 '데이터 주권·폐쇄망 요건·토큰당 비용 통제'로 답하면 됩니다.
관련 항목: 이동
양자화 / Quantization Quantization 자소서 직무 우리코드
양자화는 모델 가중치를 더 낮은 비트 수로 변환해 메모리·연산량을 줄이는 압축 기법입니다. FP32(4바이트/파라미터) → FP16/BF16(2바이트) → INT8(1바이트) → INT4(0.5바이트)로 내려가면 7B 모델은 FP32 28GB → INT4 약 4GB로 줄어 단일 GPU·CPU에서도 추론이 가능합니다.
왜 양자화가 필요한지를 직관적으로 보면, 가중치 행렬 하나가 70억 개라면 FP32는 28GB, FP16은 14GB, INT8은 7GB, INT4는 3.5GB입니다. 추론 시 GPU VRAM은 '가중치 + KV Cache + 활성화 + 프레임워크 오버헤드'를 모두 수용해야 하므로 — 70B FP32 모델은 가중치만으로 280GB라 일반 데이터센터 GPU로도 불가능합니다. 같은 70B 모델을 INT4로 줄이면 35GB로 A100 80GB 한 장에 올라가고, 7B 모델은 INT4로 약 4GB라 RTX 4090·L4·Mac(Metal 가속) 같은 데스크톱 GPU에서도 추론이 됩니다. 그래서 양자화는 sLLM·온프레미스·폐쇄망 환경의 핵심 기술입니다.
양자화는 크게 두 갈래로 나뉩니다. (1) PTQ(Post-Training Quantization): 학습 끝난 모델을 사후 변환. 빠르고 간편하지만 정밀도 손실이 있을 수 있어 보정(calibration) 데이터로 임계값 조정이 필요 (2) QAT(Quantization-Aware Training): 학습 시 양자화 오차를 시뮬레이션해 반영 — 더 정확하지만 학습 파이프라인 자체를 수정해야 해서 비용이 큽니다. 실무에서는 PTQ가 기본이고, INT8은 거의 무손실, INT4는 품질 저하가 보이기 시작해 QAT나 보정 데이터 검토가 권장됩니다.
대표 구현·포맷은 (a) GGUF(llama.cpp) — 로컬 추론 사실 표준, Q4_K_M·Q5_K_M·Q8_0 같은 양자화 단계 제공 (b) AWQ·GPTQ(vLLM) — GPU 서버용 4-bit 가중치 (c) ONNX Runtime — 다양한 백엔드(CUDA·DirectML·CPU) 지원 (d) bitsandbytes — Hugging Face 통합, NF4·FP4 (e) QLoRA — 4-bit 양자화 + LoRA 어댑터로 메모리 한정 환경 파인튜닝. 면접에서는 'INT4 7B 모델이 왜 약 4GB인가'(7B × 0.5바이트 = 3.5GB + 오버헤드) 또는 'PTQ와 QAT 차이'를 자주 묻습니다.
- 메모리 공식: INT4는 파라미터당 0.5바이트 — 7B ≈ 3.5GB·13B ≈ 6.5GB·70B ≈ 35GB
- PTQ(사후 양자화): 빠르고 간편, INT8은 거의 무손실. INT4는 calibration data로 보정 권장
- QAT(양자화 인식 학습): 학습 시 양자화 오차 반영 — 더 정확하지만 학습 파이프라인 수정 필요
- vLLM AWQ/GPTQ, llama.cpp GGUF Q4_K_M, bitsandbytes NF4, QLoRA — 대표 구현·포맷
우리 코드oxibrain-embed-local이 BGE-M3 GGUF를 llama-cpp-2로 구동 (Metal 가속) — INT4 양자화로 로컬 임베딩을 만듭니다. 시선아이티 sLLM 플랫폼도 같은 GGUF Q4_K_M 계열로 가는 게 자연스럽고, 면접 포인트는 '7B INT4 ≈ 4GB·70B INT4 = 단일 GPU' 수치 암기입니다.
관련 항목: 이동
MCP / Model Context Protocol MCP (Model Context Protocol) 직무 우리코드
MCP는 Anthropic이 2024년 말 공개한 에이전트-외부 도구·데이터 표준 연결 규격입니다. 클라이언트-서버 구조로 동작하고, 서버는 tools(실행 가능한 함수)·resources(읽기 전용 데이터)·prompts(재사용 가능한 프롬프트 템플릿) 세 가지 프리미티브를 노출하며, 전송 계층은 stdio(로컬 프로세스) 또는 HTTP/SSE(원격)입니다. 'AI의 USB-C' 같은 비유가 종종 따라다니지만 본질은 '도구 연결의 표준화'입니다.
MCP = Model Context Protocol(모델 컨텍스트 프로토콜 — 에이전트가 외부 도구·데이터에 표준 방식으로 연결되는 규격)
MCP가 등장한 배경을 보면 — 도구 연결은 지금까지 모델·프레임워크·런타임마다 다른 규약이 난립했습니다. OpenAI의 function calling 포맷은 OpenAI 모델 한정, LangChain의 Tool 인터페이스는 LangChain 한정, Claude의 tool_use는 Claude 한정이라 '같은 DB 조회 도구'를 GPT·Claude·로컬 LLM에 동시에 노출하려면 세 가지 어댑터를 따로 만들어야 했습니다. MCP는 이 문제를 'JSON-RPC 기반 표준 프로토콜' 하나로 풀겠다는 시도입니다 — MCP 서버를 한 번 만들면 MCP 클라이언트를 지원하는 모든 에이전트(Claude Desktop·Cursor·Cline·Continue·자체 구현)가 그대로 쓸 수 있습니다.
MCP의 구조는 단순합니다. (1) Host: 에이전트·IDE·챗봇 같은 사용자 측 애플리케이션 (2) Client: Host 안에 살아서 서버와 1:1 세션을 관리 (3) Server: 실제로 도구·자원·프롬프트를 노출하는 프로세스. 클라이언트와 서버 간 통신은 JSON-RPC 2.0이고, 전송(transport)은 두 종류 — (a) stdio: 로컬에서 서버를 자식 프로세스로 띄워 stdin/stdout으로 메시지 교환 — 저지연·안전 (b) HTTP/SSE: 원격 서버와 streamable HTTP로 통신 — 분산·다중 사용자 환경. 서버가 노출하는 프리미티브는 (1) tools: 이름·입력 스키마(JSON Schema)·실행 결과를 돌려주는 실행 가능 함수 (2) resources: 파일·DB 행 같은 읽기 전용 데이터, URI로 식별 (3) prompts: 미리 정의된 프롬프트 템플릿, 사용자 승인 후 활성화.
MCP가 실무에서 어떻게 쓰이는지 보면, 한 회사 내부에 '사내 DB 조회 MCP 서버'·'GitHub PR 생성 MCP 서버'·'Jira 이슈 등록 MCP 서버'를 각각 만들어 두면 — 어떤 에이전트든 그 세 서버에 연결만 하면 곧바로 사내 시스템과 대화할 수 있습니다. 그래서 '외부 API마다 어댑터 N개를 만들던 일'이 'MCP 서버 N개를 만들면 끝'으로 단순해집니다. 다만 (a) 인증·권한을 transport 레벨에서 어떻게 표준화할 것인가 (b) 도구 호출의 비용·부작용을 어떻게 통제할 것인가 (c) 프롬프트 인젝션이 도구 결과로 다시 들어오는 경로를 어떻게 막을 것인가는 아직 활발히 논의 중인 운영 과제입니다.
- MCP = 에이전트-외부 도구·데이터 표준 연결 규격, JSON-RPC 2.0 기반
- 서버 프리미티브 3종: tools(실행 가능 함수)·resources(읽기 전용 데이터)·prompts(프롬프트 템플릿)
- 전송 2종: stdio(로컬 프로세스, 저지연) + HTTP/SSE(원격, 분산 환경)
- 한 번 만들면 모든 MCP 클라이언트가 재사용 — '도구 연결의 표준화'가 본질
우리 코드oxibrain-mcp가 stdio·stdio_at·socket·socket_auth·http 다섯 전송을 자체 JSON-RPC로 구현합니다 — rmcp 같은 외부 SDK를 쓰지 않고 직접 구현한 이유는 MSRV(Minimum Supported Rust Version) 1.85를 유지하기 위해서입니다. oxios는 이 MCP 서버를 자식 프로세스로 스폰해 사내 외부 도구를 에이전트에 그대로 노출하고, BrainClient(Unix domain socket)로는 oxibrain과 통신합니다 — 두 경로가 깔끔하게 분리되어 있습니다.
관련 항목: 이동
에이전트 공학 6
AI 코딩 에이전트를 실제로 굴리는 운영·도구 영역. 작업 루프 설계, 세션 기록 자산화, 병렬 격리 환경까지 — 에이전트를 '잘 쓰는 법'의 실무 지식.
Ralph loop / 리플렉션 루프 / 자기 수정 에이전트 Ralph Loop / Reflection Loop / Self-Correcting Agent 우리코드
에이전트가 (시도 → 관찰 → 자기 비평 → 재시도) 의 한 루프를 반복하며 점진적으로 산출물을 다듬는 패턴. 단순·무심한 반복이므로 품질이 누적된다는 발상.
Ralph loop라는 이름은 만화 '심슨'에 나오는 캐릭터 랄프 위검(Ralph Wiggum)에서 왔어요. 똑같은 걸 멈추지 않고 반복하는데, 어느 순간 제 역할을 해내는 아이 — 그 이미지 그대로입니다. Geoffrey Huntley가 이 패턴을 명명하면서 AI 에이전트에 붙였고, 지금은 '자기 수정 에이전트(self-correcting agent)' 또는 '리플렉션 루프(reflection loop)'라고도 부릅니다.
흐름은 네 단계예요. ① 작업 프롬프트와 산출물 형식을 정하고 ② 실제로 뭔가를 만들어 보고(시도) ③ 테스트·평가·diff 같은 신호를 읽어 무엇이 부족한지 자기비평(reflection) 합니다. ④ 그 비평을 다음 시도의 입력에 다시 넣어요. 이 사이클을 종료 조건이 만족될 때까지 돕니다.
ReAct(Reasoning + Acting)와 자주 헷갈리는데, 결이 다릅니다. ReAct는 도구 호출 결과가 불완전하면 다음 도구를 골라 다시 부르는 — 도구 사용 루프예요. Ralph loop는 도구 선택보다 '내 결과물의 품질'을 개선하는 데 무게중심이 있습니다. 평가 점수, 테스트 통과율, lint 결과 같은 외부 신호가 자기비평을 자극하죠.
종료 조건을 안 잡으면 무한 루프가 됩니다. 흔히 쓰는 임계는 ① 테스트 스위트 전부 통과 ② 평가 점수가 목표 임계치 이상 ③ 반복 횟수·토큰·달러 예산 소진. 셋 다 같이 거는 게 안전합니다. 그리고 무엇보다 자동 머지·자동 배포에는 절대 직접 연결하지 않아요. 자동 머지가 잘못된 산출물을 그대로 배포로 보내면 사고가 됩니다 — 반드시 사람이 마지막에 보는 human-in-the-loop이 살아 있어야 해요.
- Ralph loop는 '품질 개선 루프'지 '도구 사용 루프'가 아닙니다 — ReAct와의 핵심 차이
- 종료 조건은 (a) 테스트 통과 (b) 평가 점수 임계 (c) 비용·반복 예산 — 최소 셋을 함께 설정
- 자동 머지·자동 배포와 직접 연결 금지, human-in-the-loop이 마지막 게이트
- SI 공공에서는 '결함 재작업 + SP 품질관리' 워크플로와 동형 — 같은 사이클 구조
우리 코드oxios-ouroboros의 자기-개선 loop(directive/engine/model_resolver/prompts/resilience/fallback)와 같은 모양입니다. oxicode-cli의 batch-commit-autonomously도 동일 패턴(생성 → 리뷰 → 재시도)으로 동작해요. 면접 포인트: '사람 없이도 임계까지 개선되지만, 종료 임계와 안전장치 없이는 못 쓴다'.
관련 항목: 이동
transcript / 에이전트 세션 기록·재생 Agent Transcript & Replay 우리코드
에이전트 한 세션 동안의 모든 산출 — 사용자 입력, 도구 호출, 파일 diff, 사고(thinking) 로그, 최종 응답 — 을 시간 순으로 기록한 재생 가능한 로그. '세션 기록 = 진짜 커밋 메시지'라는 관점이 핵심.
transcript는 단순 채팅 로그가 아닙니다. 에이전트가 세션 동안 한 일 전부 — 사용자가 뭘 말했고, 어떤 도구를 어떤 인자로 호출했고, 파일은 어떻게 바뀌었고, 모델은 그 사이 어떤 사고를 했는지 — 가 시간 순으로 들어 있어요. PR 설명이 코드 diff 옆에 붙듯이, transcript는 에이전트 작업의 동영상 같은 자료입니다.
왜 '자산'으로 다루냐면요. ① 디버깅 — 에이전트가 왜 틀렸는지 단계별로 따라가 볼 수 있습니다. ② 감사(audit) — 누가·언제·어떤 도구로 무엇을 했는지 흔적이 남아요. ③ 지식 전수 — 신입이 '에이전트가 이 PR을 어떻게 만들었는지' 배울 때, 그날의 transcript 그대로 학습합니다. ④ 평가 — 회고할 때 근거가 됩니다. 이 중 디버깅과 지식 전수가 실무에서 가장 자주 쓰이는 이유예요.
재생(replay)이라는 개념이 transcript를 한 단계 올려줍니다. claude-replay 같은 도구는 transcript를 동영상 재생처럼 단계별로 확장하거나, 특정 시점으로 점프하거나, 검색해서 다시 보는 인터페이스를 줘요. transcript → 임베딩 → RAG으로 되먹이는 패턴(에피소드 메모리)도 있어, 한 세션이 끝나도 그 기록이 다음 세션의 입력으로 살아 돌아옵니다.
단, transcript에 민감 정보가 그대로 들어갑니다. API 키, 비밀번호, 토큰, 고객 PII, 내부 URL 같은 게 도구 출력에 섞여 나오면 전부 transcript에 남아요. 그래서 ① 어떤 도구 호출은 마스킹 ② 외부 공유 시 비공개로 ③ 보존 기간을 정해 주기적 정리 — 같은 위생이 필수입니다. SI 공공 영역에서는 '감리·감사' 대응 기록으로 직결되기 때문에, 보관 정책이 곧 컴플라이언스 이슈가 됩니다.
- transcript의 이중 가치 — 사람이 리뷰하는 자료 + LLM이 학습하는 메모리
- 품질을 결정하는 건 '포함 범위' — tool input/output·diff·thinking trace가 다 들어 있느냐
- 재생(replay) — 단계별 확장·시점 점프·검색으로 에이전트 작업을 복기
- 민감 정보(API 키·PII·내부 URL) 마스킹·보관 기간 정책이 필수
우리 코드omp 하네스는 세션 transcript를 agent://·history:// URI로 노출합니다. 코드·도구·소스 관련 주장(claim)을 transcript 근거로 재검증하는 수단이에요. what-is-oxi 사이트 분석 자체가 transcript 기반 회고라는 점을 강조할 수 있습니다.
관련 항목: 이동
에이전틱 개발 환경 (ADE) Agentic Development Environment (ADE) 우리코드
AI 코딩 에이전트(Claude Code·Codex·Gemini CLI 등)를 실행·관리·병렬 운영하기 위해 특화된 개발 환경. 'IDE = 사람이 타이핑하는 편집기'라면 'ADE = 에이전트가 코드를 쓰는 환경'.
ADE = Agentic Development Environment(에이전틱 개발 환경) — AI 코딩 에이전트를 1급 시민으로 다루도록 설계된 통합 개발 환경
IDE(Integrated Development Environment)라는 말 자체가 사람이 직접 코드를 입력한다는 전제로 만들어졌어요. 자동완성, 디버거, Git 통합 — 다 사람의 손끝을 도와주는 도구입니다. 그런데 2024~2026년 사이에 에이전트 기반 코딩이 본격화되면서 '사람이 코드를 치는 환경'이 아니라 '에이전트가 코드를 쓰고 사람은 그것을 감독·검토하는 환경'이 필요해졌어요. 그 자리에 들어오는 것이 ADE — Agentic Development Environment입니다.
ADE와 IDE의 결정적 차이는 다섯 가지예요. ① 에이전트 세션이 1급 시민 — '에이전트 인스턴스 N개 동시 실행'이 기본 동작 ② git worktree 기반 병렬 격리 — 여러 에이전트가 같은 저장소의 다른 부분을 동시에 만질 수 있어요 ③ 브라우저·UI 미리보기 내장 — 에이전트가 만든 화면을 즉시 확인 ④ 세션 transcript·상태 관리 — 한 세션이 끝나도 컨텍스트가 살아남음 ⑤ 멀티 에이전트 orchestration — 여러 에이전트가 협업하는 구조를 명시적으로 다룸.
그리고 가장 중요한 가치가 vendor-neutral이에요. 특정 에이전트 하네스(Claude Code만, Codex만)에 종속되면 다른 모델로 옮길 때 모든 운영 노하우를 다시 짜야 합니다. ADE는 '여러 하네스를 같은 환경에서 굴린다'는 전제로 설계돼요. 결국 '한 조직에서 AI 코딩 에이전트를 몇 개나 동시에 돌리느냐'는 개인의 효율 문제가 아니라 팀·조직 단위의 운영 문제가 됩니다. ADE는 그 운영 문제에 답하는 도구예요.
- IDE는 사람이 타이핑하는 편집기, ADE는 에이전트가 코드를 쓰는 환경 — 패러다임 이동의 본질
- git worktree 격리 — 병렬 에이전트가 같은 저장소에서 충돌 없이 작업하는 전제
- vendor-neutral — Claude Code·Codex·Gemini CLI를 같은 환경에서 동시에 굴림
- 에이전트 1개는 워크플로, 50개 병렬은 조직 운영 문제 — ADE는 후자에 답함
우리 코드omp 하네스 자체가 터미널 AI 에이전트 + 브라우저 + worktree + transcript를 통합한 ADE 방향의 자기 주도 구현입니다. 면접 포인트: 'IDE → ADE로의 패러다임 이동'을 한 문장으로 정리할 수 있어야 해요.
관련 항목: 이동
cmux (씨멕스) cmux (terminal multiplexer) 우리코드
Ghostty 기반의 macOS 네이티브 오픈소스 터미널 멀티플렉서. AI 코딩 에이전트 병렬 실행에 특화돼, 에이전트가 pane을 프로그래밍 방식으로 제어할 수 있는 소켓 API를 제공합니다.
cmux를 이해하려면 먼저 tmux부터 떠올려야 해요. tmux는 텍스트 기반의 범용 터미널 멀티플렉서로, SSH 세션·헤드리스 서버에서 디스플레이 없이 작업할 때 표준처럼 쓰입니다. cmux는 그 tmux와 비슷한 자리를 노리지만 출발선이 다릅니다 — 'AI 코딩 에이전트를 위한 네이티브 멀티플렉서'를 표방해요. 베이스는 Ghostty(고스트티)라는 macOS 네이티브 터미널 엔진이고, 그 위에 에이전트 협업에 필요한 인터페이스가 얹혀 있습니다.
핵심 기능은 두 가지예요. ① 수직 탭 + 워크스페이스별 독립 상태 — 여러 에이전트를 같은 화면에 띄워놓고 각자 격리된 환경에서 일하게 합니다. ② CMUX_SURFACE_ID·CMUX_WORKSPACE_ID 같은 소켓/환경변수 API — 에이전트가 이 ID를 받아 pane을 만들거나, 메시지를 보내거나, 화면을 캡처하거나, 종료할 수 있어요. 사람이 GUI로만 만지는 tmux와 다른 점은 '에이전트가 자기 pane을 프로그래밍할 수 있다'는 부분입니다.
거기에 150개 이상의 커뮤니티 플러그인이 더해져서 멀티 에이전트 오케스트레이션, 진행 상황 표시, 알림, worktree 관리 같은 기능을 조립식으로 붙일 수 있어요. 그리고 Playwright급 브라우저 자동화가 터미널 안에 내장돼 있어, 에이전트가 터미널에서 코드를 짜고 곧바로 그 결과 화면을 검증하는 한 사이클이 한 화면 안에서 끝납니다. 프로젝트는 manaflow-ai/cmux(깃허브)에서 공개 진행 중이에요.
- CMUX_SURFACE_ID·CMUX_WORKSPACE_ID — 에이전트가 pane/워크스페이스를 프로그래밍 방식으로 제어
- tmux는 SSH·헤드리스 범용, cmux는 네이티브 UI + 에이전트 협업 특화 — 사용 맥락이 다름
- 브라우저 자동화 내장 — 코딩 → 검증까지 한 터미널 안에서 닫힘
- Ghostty 기반 macOS 네이티브 — 텍스트 에뮬레이터가 아니라 네이티브 셸에 가까운 UX
우리 코드omp 하네스도 브라우저 + 터미널 + worktree를 통합합니다. cmux와 같은 ADE 방향을 다른 구현으로 풀어낸 사례예요. 면접에서 '터미널이 에이전트용 멀티플렉서로 진화한다'라는 흐름을 답할 수 있어야 합니다.
관련 항목: 이동
Xirp (지르프) Xirp (Spotify ADE) 우리코드
Spotify가 2026-08-10에 공개한 macOS 데스크톱용 vendor-neutral ADE(베타). Claude Code·Gemini CLI·Codex 세션을 50개 이상 병렬로 조율하며, 세션마다 Git worktree 격리를 적용합니다.
Xirp는 2026년 8월 10일 Spotify가 공개한 macOS 전용 베타 소프트웨어예요. 자체 에이전트를 만들었다기보다, 이미 있는 코딩 에이전트 하네스들을 '관리 면'에서 묶는 ADE에 가깝습니다. Claude Code, Gemini CLI, Codex 같은 다양한 하네스의 세션을 한 앱 안에서 띄우고 돌리고 끄는 역할만 담당해요.
출발점이 명확해요. '에이전트 한 개를 잘 쓰는 것'과 '에이전트 50개를 동시에 잘 운영하는 것'은 완전히 다른 문제입니다. Xirp 발표 제목이 'What we've learned scaling AI coding agents'인 이유가 여기에 있어요. 수천 명의 엔지니어가 누적 36,000+ 세션을 운영하면서 얻은 교훈을 토대로, ① 세션마다 별도 Git worktree를 잡아 충돌을 피하고, ② 컨텍스트를 하네스와 분리해서 도구를 바꿔도 작업 상태가 승계되도록 했고, ③ Backstage 기반 IDP인 Spotify Portal과 연결해 소프트웨어 카탈로그 컨텍스트를 세션에 주입하거나, 세션이 끝나면 transcript를 Portal로 회귀시켜 팀원·다음 에이전트가 이어받게 합니다.
결국 Xirp의 핵심 가치는 'vendor-neutral + 컨텍스트 승계 + Portal과의 연결'이에요. ADE 자체는 여러 구현이 있지만, Xirp가 특히 강조하는 것은 '사람용 IDP인 Backstage/Portal이 이미 있는 조직이라면, 에이전트용 IDP도 같은 결로 만들어야 한다'는 점입니다. 단, 검증된 사실만 정리한다는 원칙에 따라 — 베타 시점이라 향후 기능·가격·플랫폼 지원이 바뀔 수 있어요.
- 에이전트 1개는 워크플로 문제, 50+ 병렬은 조직 운영 문제 — Xirp가 답하려는 질문
- 세션 = worktree 격리 + 컨텍스트 승계 — 이 둘이 vendor-neutral의 아키텍처 요구사항
- 36,000+ 세션 운영 검증 — Spotify 발표 제목 'What we've learned scaling AI coding agents'
- Portal(Backstage 기반 IDP)과 연결 — 사람용 IDP가 있는 조직의 에이전트용 IDP
우리 코드omp 하네스가 transcript(agent://·history://)·worktree·다중 에이전트를 통합하는 방향은 Xirp의 '컨텍스트 승계'와 같은 문제 의식입니다. 면접 포인트: '사람용 IDP가 Backstage라면, 에이전트용 IDP는 Xirp'라는 매핑으로 답하면 ADE 질문이 정리됩니다.
관련 항목: 이동
Orca (오르카) Orca (ADE) 우리코드
병렬 에이전트 fleet 운용에 특화된 ADE. Claude Code·Codex·Gemini·Cursor CLI 등 모든 CLI 기반 에이전트를 git worktree 격리로 동시에 실행·관리합니다.
Orca의 컨셉은 한 문장입니다 — 'fleet of parallel agents', 즉 '에이전트 함대'의 병렬 운용. 같은 코드베이스의 서로 다른 부분을 여러 에이전트가 동시에 작업할 수 있도록 각 세션을 별도 Git worktree로 격리해요. 그래서 한 에이전트가 메인 브랜치를 어지럽히더라도 다른 에이전트의 작업과 충돌하지 않습니다.
Stably, Inc.(YC-backed)가 만든 무료 오픈소스 프로젝트이고(manaflow-ai/orca — 깃허브 검색 시 stablyai/orca), macOS·Windows·Linux 데스크톱 클라이언트와 iOS·Android 컴패니언 앱을 함께 제공합니다. 핵심 통합은 'AI 터미널 + 코드 편집 + git worktree + 브라우저 미리보기' 네 가지예요. 다양한 하네스(Claude Code, Codex, OpenCode, Cursor CLI 등)를 같은 환경에서 받아들이기 때문에 vendor-neutral ADE의 또 다른 구현으로 분류됩니다.
Orca가 보여주는 공통 원리는 단순합니다 — '에이전트 병렬 운영의 전제는 작업 격리(worktree)다'. 이 격리가 없으면 두 에이전트가 같은 파일을 동시에 만지는 순간 머지가 지옥이 됩니다. worktree는 각 에이전트에게 '자기 작업 폴더'를 주고, 결과는 PR 단계에서 합치는 구조라 충돌이 파일 시스템 수준에서 미리 차단돼요. Xirp나 cmux가 다른 결로 같은 답을 주는 것과 비교하면, ADE가 vendor-neutral + worktree 격리라는 두 축 위에 모이고 있다는 게 보입니다.
- 병렬 에이전트의 전제 = worktree 격리 — 같은 저장소 충돌을 파일 시스템 단계에서 차단
- Claude Code·Codex·OpenCode·Cursor CLI 등 다중 하네스 수용 — vendor-neutral
- 데스크톱 + iOS/Android 컴패니언 — 모바일에서 세션 상태 확인 가능
- 결과는 PR 단계에서 머지 — 격리된 worktree를 git merge로 통합하는 패턴
우리 코드omp의 dispatching-parallel-agents 스킬과 using-git-worktrees가 바로 'worktree 격리 + 병렬 에이전트' 모델을 구현합니다. Orca와 같은 결을 다른 구현으로 풀어낸 사례예요. 면접 포인트: '에이전트 병렬화의 진짜 전제는 작업 격리다'라는 한 문장.
관련 항목: 이동
SI 산업 · 공공 16
(주)시선아이티 현장의 90%. 마르미-III·SP·전자정부 표준프레임워크·eGovFrame·ForGIS·OGC 표준·ISMS-P까지 — 실제 SI 사업에서 발주청·감리·사업자 사이에 오가는 용어와 산출물을 익힌다.
SI / SM / 솔루션 SI / SM / Solution 직무 그림
SI는 발주기관 요구사항을 받아 시스템을 구축하는 프로젝트, SM은 구축된 시스템을 다년 계약으로 운영·개선하는 사업, 솔루션은 자체 개발 또는 도입 패키지 형태로 판매되는 표준화 상품.
SI = System Integration(시스템 통합), SM = System Management(시스템 유지관리), Solution = 도입·자체 패키지 기반 표준화 상품
SI는 흔히 새 시스템 구축(신규 개발·재구축) 프로젝트를 말해요. 발주기관이 RFP를 띄우면 사업자가 제안서를 내고, 낙찰되면 보통 6개월 ~ 2년짜리 일회성 프로젝트가 시작돼요. 이 안에서 사업자(원청)는 자체 인력을 PM·아키텍트·개발·테스트로 꾸리고, 부족한 부분은 외주(하청) 업체에 맡깁니다. 수익은 마일스톤(착수·중간·완료) 단위로 청구하고, 끝나면 사업 자체가 끝나요.
SM은 다릅니다. 시스템을 구축한 다음에 그냥 버려두면 안 되니까, 평소 운영·장애 대응·요구사항 변경·성능 개선을 누군가 맡아야 해요. 그래서 발주청과 운영 업체가 3~5년짜리 유지관리 계약을 다시 맺습니다. 매월 정액(예: 월 정액 4,000만 원) 또는 인월(man-month) 단위로 과금하고, 인력은 SI보다 안정적으로 상주합니다. SI가 맛있지만 위험한 한 철 잡일이라면, SM은 평이하지만 매년 돈이 들어오는 우유부단이라고 보면 돼요.
솔루션은 이 둘의 양쪽 다리에 깔리는 재화입니다. 자체 개발한 GIS 엔진, 전자결재, 인사·급여 패키지 등을 상품화해서 SI/SM 모두에 끼워 파는 거예요. 솔루션 매출은 라이선스 + 유지보수의 조합이라 마진이 SI보다 훨씬 좋아요. 그래서 솔루션 회사는 "우리 솔루션을 살 필요 없는 SI도 있냐"가 아니라 "어떤 SI에도 우리 솔루션이 들어가게" 영업 전략을 짭니다. ForGIS처럼 시선아이티가 가진 제품도 이쪽 카테고리예요.
그래서 공공 SI 회사의 매출 트레이드는 보통 (1) 솔루션 라이선스, (2) SI 구축 매출, (3) SM 운영 매출의 세 갈래로 나눠서 봐요. 솔루션 + SM의 비율이 높을수록 안정적이고, SI 구축 매출 비중이 너무 높으면 수주 하락 시 회사가 위태로워져요. 면접에서 "매출 구성이 어떻게 되나요?"라고 물으면 이 세 갈래로 끊어서 답하는 게 신의 한 수예요.
- SI는 일회성·마일스톤 청구, SM은 다년 계약·월 정액/인월 — 수익 인식 구조가 다르다
- 솔루션은 라이선스·유지보수 매출 — SI/SM에 비해 마진이 높고 안정적이다
- 공공 SI는 '입찰 → 계약 → 착수 → 검수'의 단계별 절차, ksX7001·국가계약법 적용
커리큘럼 체인 SI 절차 (마르미 4단계)
마르미-III / 정보화 사업 표준 Marmi-III (Korean IT Project Standard) 자소서 직무 그림
한국행정·공공 정보화 사업의 표준 방법론. 분석→설계→구현→시험→운영 5단계에 130여 종 산출물 — 공공 사업의 '공식 절차서'다.
마르미-III는 행정안전부와 한국정보화진흥원이 관리하는 정보화 사업 표준 방법론이에요. 사업을 5단계 — 정보화 전략·분석(분석), 시스템 설계(설계), 구현, 시험, 운영 — 로 나누고, 각 단계마다 만들어야 할 산출물(문서) 목록을 미리 정해뒀어요. 왜 이렇게까지 엄격하냐면, 공공은 나라 돈이 들어가니까 "이 정도는 만들었다"라는 증거가 필요하기 때문입니다. 그래서 단계가 끝날 때마다 산출물이 안 나오면 다음 단계로 못 가요.
대표 산출물 흐름을 따라가 보면 이해가 빨라요. 분석 단계에서는 요구사항 정의서, 요구사항 추적표(RTM), 현행 시스템 분석서, 업무 흐름도가 나와요. 설계 단계에서는 시스템 설계서, ERD, 테이블 명세서, 화면 설계서, 인터페이스 설계서, 소프트웨어 아키텍처 문서가 나와요. 구현 단계는 코드가 곧 산출물이지만 형상관리·빌드 결과가 첨부됩니다. 시험 단계는 단위·통합·인수 시험 계획·결과 보고서. 운영 단계는 운영자·사용자 매뉴얼, 유지보수 계획, 장애 대응 절차서. 이 모든 게 감리의 점검이에요.
특히 요구사항 추적표(RTM)는 마르미의 핵심축이에요. 요구사항 하나당 → 관련 설계서 항목 → 관련 코드 모듈 → 관련 테스트 케이스 → 결함 → 폐쇄까지 한 줄로 추적하는 표. 누가 "이 요구사항 어디 구현됐어요?" 라고 물으면 RTM에서 한 줄 뽑아 보여주면 끝나요. 마르미-III는 이 RTM을 표준에서 강제해요. 그래서 RTM이 없으면 감리에서 바로 지적당합니다.
면접에서 자주 나오는 변화 포인트도 짚어둘게요. 기존 마르미는 온프레미스·폭포수(waterfall) 중심으로 설계됐어요. 그래서 최근엔 클라우드·데이터·AI 친화 산출물(ML 모델 카드, 데이터 카탈로그, MLOps 운영 문서 등)이 추가·확장됐어요. "마르미-III가 최신 트랜드를 따라가는가"라는 질문은 행정·공공을 타깃으로 하는 회사에선 거의 단골입니다.
- 5단계(분석→설계→구현→시험→운영) — 각 단계에 130여 종 산출물이 매핑된다
- RTM(요구사항 추적표)이 표준에서 강제 — 감리에서 첫 번째로 체크하는 문서
- 클라우드·AI 워크로드용 산출물 확장 — MLOps·모델 카드·데이터 카탈로그가 추가됨
커리큘럼 체인 SI 절차 (마르미 4단계)
정보화 사업 감리 / 감리법인 IT Audit / Supervisory Review 직무 그림
정보화 사업의 진행·완료 단계에서 표준 적합성·품질·보안·계약 이행 사항을 발주청 대신 검토하는 절차. 감리법인은 행정안전부 지정·고지된 정보통신공사업 조합의 전문 조직.
감리는 발주기관을 대신해 '제3자가 이 사업이 표준대로 잘 진행되고 있는가'를 들여다보는 절차예요. 발주청(예: 정부 부처·지자체) 내부 인력이 부족하거나 객관성을 확보하기 위해 외부 감리법인에 맡기는 형태가 일반적이에요. 감리 결과는 곧 발주청의 검수(준공) 승인 직결되고, 향후 발주청의 다른 사업 입찰 때도 사업자의 실적·평판에 영향을 줍니다.
감리는 시점별로 세 갈래로 나뉘어요. 착수 감리(프로젝트 시작 직후) — 사업자 선정, 착수 준비, 기본 계획 검토. 중간 감리(전체 일정의 50~70% 지점) — 진도, 산출물 품질, 변경 관리 적절성. 완료 감리(준공 직전) — 최종 산출물, 인수·이관 계획, 보안·결함 종결 여부. 단계가 클수록 체크가 까다로워져요. 그리고 이 셋을 합쳐 한 번에 보는 총괄 감리도 있어요.
감리법인이 보고서를 작성할 때 쓰는 체크리스트는 사실 사용 가능한 산출물 = 사용 가능한 사업이라는 공식이에요. RTM과 산출물 매트릭스를 대조해서 "요구사항 A에 대해 요구사항 정의서·설계서·코드·테스트 4종 모두 일치"를 확인합니다. 빠진 게 있으면 '미흡', 부분이라도 있으면 '보완', 다 있고 추적 가능하면 '적합'. 미흡이 일정 비율을 넘으면 감리 의견서에 '부적합'이 적히고, 발주청 입장에서 이건 안 받는 게 정치적으로 안전해요.
감리 영역은 보통 4개 — 품질(산출물 적합성), 진도(일정 준수), 보안(ISMS·개인정보·접근통제), 계약(변경 관리·하자 보수) 으로 나뉘어요. 그래서 보안 감리 항목과 ISMS-P 통제 항목이 자연스럽게 겹치고, 감리 합격 = ISMS-P 부분 충족의 효과를 갖기도 해요. 면접에서 "감리와 ISMS의 관계"를 묻는 건 그래서 자주 나와요.
- 감리 시점 — 착수 / 중간 / 완료 / 총괄 — 시점이 늦을수록 판단이 결정적
- 체크리스트는 산출물과 1:1 매핑 — '적합/보완/미흡' 판정 누적이 합격선 결정
- 보안 감리 항목은 ISMS-P·개인정보 영향평가와 자연스럽게 겹친다
커리큘럼 체인 SI 절차 (마르미 4단계)
SP / SW 프로세스 성숙도 SP Certification (Software Process) 자소서 직무 그림 우리코드
소프트웨어 개발·운영 프로세스가 표준대로 수행되고 있는지를 심사·인증하는 제도. 결과물(산출물)이 좋냐가 아니라 '만드는 과정'이 표준화되어 있냐를 본다. 공공 사업 입찰 자격·가점 요건으로 자주 등장.
SP = Software Process(소프트웨어 프로세스 품질 인증) — 한국정보통신기술협회(TTA) 주관, 프로세스 성숙도 인증 제도
SP는 Software Process의 약자로, 한국정보통신기술협회(TTA)가 운영하는 프로세스 성숙도 인증이에요. 배경에는 CMM/CMMI(원래 미국 SEI가 만든 성숙도 모델)가 있어요. CMM은 5단계로 기업 역량을 잡는데, SP는 이걸 한국 실정에 맞게 재구성해서 Level 1~5 평가 기준·인증 절차를 운영해요. 면접에서 'Level이 높을수록 좋아요?'라고 묻는 건 서비스·사업 비중에 따라 다르다는 답이 신의 한 수예요.
Level 1(초기) — 개인 영웅에 의존, 프로세스 표준 없음. Level 2(관리) — 기본적으로 프로젝트 단위 관리. Level 3(정의) — 조직 표준 프로세스가 있고, 프로젝트는 그걸 맞춥니다. Level 4(정량적 관리) — 측정 데이터를 기반으로 프로세스 품질을 수치로 관리. Level 5(최적화) — 지속적 개선·혁신. 공공 SI의 통상 자격 요건은 Level 3, 일부 대형 사업은 Level 4 이상이 등장합니다.
Level 3의 핵심은 "우리 조직에 표준 프로세스가 있고, 각 프로젝트가 표준을 따랐다는 기록이 남아있다"에요. 여기서 말하는 기록이 뭔고 하니, 마르미의 산출물 + 개발 로그(지라 이슈, 코드 커밋, 빌드/배포 로그, 리뷰 기록, 결함 추적) 같은 흔적이에요. 그래서 SP 인증을 받으려면 코드·문서·로그의 일관성과 보존이 필수입니다. 감사 티듯이 한번 닦아서 끝나는 게 아니라, 인증 후 3년 주기 갱신 사이에 끊임없이 쌓아야 해요.
SP 인증과 SI 감리는 별개예요. SP는 '우리 조직의 개발·운영 프로세스가 표준이다'라는 인증이고, 감리는 '이번 사업의 산출물과 진도가 표준대로 진행됐다'라는 부분 검증. 두 보고서에서 통제 항목이 일부 겹치고, 둘 다 갖춰 두면 기업의 신뢰 신호가 더 강해져요. "이 회사 어떻게 평가하나요?"라는 질문엔 SP 인증 + 감리 실적 + 클라우드·보안 인증(CSAP, ISMS-P) 3종 세트로 답하는 게 강력합니다.
- SP 인증은 개발·운영 '과정의 품질'을 인증 — 결과물 품질 인증과 다르다
- Level 3 이상부터가 공공 SI의 통상 자격, Level 4는 데이터 기반 정량 관리의 시작
- 갱신 주기 3년 — 인증 사이에도 산출물·로그를 끊임없이 쌓아야 한다
우리 코드oxicode-cli 18 subcommand가 SI 프로젝트 운영 패턴과 동형이라는 매핑으로 답하면 됨. SP의 '과정 품질' 요건은 oxi의 CI 게이트 + 문서화 규율(AGENTS.md/DESIGN.md/에러 정정 로그)에 그대로 적용된다.
커리큘럼 체인 SI 절차 (마르미 4단계)
요구사항 정의서 / WBS / Gantt Requirements & WBS 자소서 직무 표 그림
세 문서가 사업의 세 축을 잡는다. 요구사항 정의서는 '무엇을 만들 것인가', WBS는 '어떤 일들로 쪼갤 것인가', Gantt는 '언제 무엇을 끝낼 것인가'를 정의·시각화한 문서.
WBS = Work Breakdown Structure(작업 분할 구조도) — 프로젝트 작업을 계층적으로 쪼갠 트리, Gantt = 시간선(차트) 형태로 작업을 시각화한 일정 표
요구사항 정의서(혹은 요구사항 명세서)의 본질은 비공식적 표현을 잃지 않으면서 검증 가능한 형태로 바꾸는 겁니다. 보통 (1) 기능 요구사항 — 시스템이 무엇을 해야 하는가, (2) 비기능 요구사항 — 성능·보안·가용성·사용성 같은 품질 속성, (3) 제약 조건 — 적용 프레임워크, 사용 DB, 표준 준수 여부. 좋은 요구사항은 SMART — Specific, Measurable, Achievable, Relevant, Time-bound 5속성을 갖춰야 합니다. 모호하면 "사용자가 화면을 빠르게 본다"처럼 못 써요.
WBS는 일의 나무예요. 최상위(Level 0)는 '프로젝트 전체', 그다음 Level 1은 '분석/설계/구현/시험/운영' 5단계, Level 2는 단계별 세부 작업, Level 3은 '단위 테스트 케이스 작성' 수준까지 쪼갭니다. 원칙은 (1) 100% 원칙 — 자식 노드의 합이 부모 노드를 100% 덮어야 함, (2) WBS는 SOW(Statement of Work)·계약서와 1:1 대응이 되어야 함, (3) 한 노드는 한 명이 책임지고 끝낼 수 있는 크기(Entity Control Point)로 나눌 것. 그래서 WBS가 잘못 나뉘면 책임 공백이 생겨요.
Gantt는 WBS의 노드들에 시작일·종료일·선행 관계(FS·SS·FF)를 붙여 시간선으로 그린 그래프예요. 인터페이스(MS Project, P6, 팀간 일정표)도 있지만 표 형태(Gantt 차트)도 동일 결과를 줘요. Gantt는 '이 작업이 언제 시작 가능한가'를 시각화하는데, 핵심은 의존 관계(어떤 작업이 끝나야 다음 시작)와 크리티컬 패스(전체 일정에 영향을 주는 연속 작업 경로). 크리티컬 패스가 길면 그 안의 활동을 줄이는 게 곧 단축의 유일한 방법이에요.
이 세 문서가 따로 만들어지지 않습니다. 요구사항 하나당 WBS 노드 여러 개 · Gantt 작업 항목 여러 개로 분해되고, 그 WBS 노드마다 산출물이 매핑돼요. 그래서 '요구사항 → WBS → 산출물 → 일정'이 사다리처럼 잠겨 있고, 어느 한쪽이 흔들리면 전부 흔들려요. 이게 마르미-III가 RTM을 강제하는 이유이기도 해요.
| 구분 | 특징 | 참고 |
|---|---|---|
| 요구사항 정의서 | 무엇을 만들 것인가 | 기능/비기능/제약 — SMART 원칙 |
| WBS | 어떤 일들로 쪼갤 것인가 | 100% 원칙, SOW 1:1 대응 |
| Gantt | 언제 무엇을 끝낼 것인가 | 선행 관계, 크리티컬 패스 |
- 요구사항은 SMART (Specific·Measurable·Achievable·Relevant·Time-bound)로 써야 검증 가능하다
- WBS는 100% 원칙 + SOW/계약서와 1:1 대응 — 책임 공백이 생기지 않게 하는 단위
- Gantt의 크리티컬 패스가 전체 일정의 하한 — 단축하려면 그 안의 작업부터 줄인다
커리큘럼 체인 SI 절차 (마르미 4단계)
설계 산출물 (ERD / 명세서 / 화면설계서) Design Artifacts (ERD / Specification / Screen Design) 자소서 직무 그림 우리코드
설계 단계의 3대표 산출물. ERD(Entity-Relationship Diagram)는 데이터 구조, 명세서는 모듈·인터페이스·API의 행동과 책임, 화면설계서는 UI 레이아웃·이벤트·흐름을 정의한다. 세 산출물 사이의 일관성이 감리의 1차 점검 포인트.
ERD는 시스템이 다루는 데이터를 그림으로 보여주는 거예요. 개체(Entity) — 사람, 부서, 주문, 장비 같은 '정보의 주체'가 사각형, 관계(Relationship)가 마름모, 속성(Attribute)이 타원으로. 핵심은 정규화 — 같은 정보가 여기저기 흩어지지 않도록 함수적 종속을 정리하는 게 ERD의 첫 번째 덕목이에요. 면접에서 "3정규화까지 가야 하나요?" 라고 물으면, "읽기/쓰기 성능과 변경 비용의 균형"으로 답해야 해요. 무조건 3NF까지 가는 게 답이 아니에요.
테이블 명세서(혹은 데이터 명세서)는 ERD의 각 개체 → 테이블로 변환한 후에 컬럼 단위로 적는 문서예요. 컬럼명, 데이터 타입, null 허용, 기본값, PK/FK, 인덱스, 비고, 개인정보 여부 같은 컬럼 메타 정보가 모두 들어갑니다. 감리원이 좋아하는 게 이 명세서예요. 왜냐면 화면에 있는 입력란이 이 명세서 컬럼과 일치하는지, 개인정보 항목은 마스킹/암호화 컬럼이 함께 정의돼 있는지, 한눈에 확인이 되거든요. 그래서 '코드로 쓰는 테이블 명세' 습관이 중요합니다.
화면설계서(혹은 UI 설계서)는 화면 단위로 레이아웃, 컴포넌트, 이벤트, 권한, 검색 조건, 메시지, 예외 흐름을 정의해요. 흔히 와이어프레임(저작도) + 명세 표(행 단위 권한) 두 부분으로 구성됩니다. 감리에서 화면설계서 체크할 때는 (1) 요구사항 추적표의 어떤 항목과 연결되는가, (2) 입력 검증 규칙이 정의돼 있는가, (3) 접근 권한(역할)이 정의돼 있는가, (4) 예외/오류 화면이 정의돼 있는가를 봅니다. 이게 빠지면 항상 '보완' 판정을 받아요.
API 명세서(OpenAPI/Swagger)와 인터페이스 명세서도 핵심이에요. 요즘은 OpenAPI 3.x 형식의 YAML/JSON으로 외부·내부 API를 모두 정의하는 게 표준이 됐어요. 인터페이스 명세서는 시스템 간(외부 기관, 레거시, 내부 모듈) 데이터 교환 항목을 정의한 문서로, 보통 매핑 테이블(시스템A 필드 ↔ 시스템B 필드), 송수신 빈도, 트리거, 오류 코드, 프로토콜(HTTP/SOAP/FTP/SFTP)을 포함합니다. 면접에서 "API 명세서를 왜 쓰나요?"라고 물으면 — '계약(contract)'의 역할, '생성과 사용의 시간적 분리', '테스트 더블(mock) 생성의 근거' 3가지로 답할 수 있어야 해요.
- ERD는 데이터 중심, 명세서는 모듈/API 책임, 화면설계서는 UI 이벤트/권한 — 3자리의 역할
- 감리는 3산출물 간 일관성(화면 항목 ↔ 테이블 컬럼 ↔ API 필드)을 첫 번째로 본다
- OpenAPI 3.x 기반 API 명세서, 모듈 간 인터페이스 명세서가 필수 동반 산출물
우리 코드oxibrain projection 스키마(beliefs/statements/entities)를 Rust 타입으로 정의 — '코드로 쓰는 테이블 명세' 사례. ERD 설계 → DDL 자동화는 마르미-III 표준.
관련 항목: 이동
커리큘럼 체인 SI 절차 (마르미 4단계)
결함 관리 / 등급 분류 Defect Management 직무 그림 우리코드
결함은 발견 → 할당 → 수정 → 재검증 → 폐쇄의 라이프사이클로 관리하고, 심각도 등급(치명/상/중/하)별로 처리 우선순위를 정한다. 공공 SI에서 '치명 1건 = 준공 보류' 수준.
결함 관리의 핵심은 라이프사이클(workflow)이에요. 보통 New(발견) → Assigned(할당) → In Progress(수정 중) → Fixed(수정) → Verified(재검증) → Closed(종결) 단계를 탑니다. 단계마다 전이 조건(상태 간 이동 규칙)이 있어요. 'Fixed'에서 바로 'Closed'로 못 가요 — 담당자나 QA가 재현 시도해서 진짜로 고쳐졌는지 검증해야 'Verified' 거쳐 'Closed'로 갈 수 있습니다. 이 흐름이 흔들리면 '신고만 받고 닫힌 결함', '재현 불가능 결함'이 누적돼요.
결함 등급은 보통 3~5단계로 나뉘어요. '치명(Critical)' — 데이터 손상·보안 위반·핵심 기능 불가. '상(Major)' — 주요 기능 장애, 우회 가능. '중(Medium)' — 일반 장애, 일반 케이스에서만. '하(Minor)' — UI·문구·사소한 불편.
시선아이티 같은 공공 SI 현장에선 '치명'이 1건 남으면 준공이 미뤄질 수 있어요. 그래서 결함 분포 차트(등급 × 월별 추이)와 잔여 결함 수는 감리 종료 직전 거의 매일 보고되는 지표예요. 마르미-III 시험 단계의 '결함 관리표'가 바로 이걸 표준화한 산출물입니다.
참고 — 테스트 자체(단위·통합·E2E 구분, FIRST 원칙)는 소프트웨어 엔지니어링 도메인의 '테스트 피라미드'·'단위 테스트' 용어로 따로 정리되어 있어요. 여기선 발견된 결함을 어떻게 추적·처리하느냐만 다룹니다.
- 결함 라이프사이클: New → Assigned → Fixed → Verified → Closed — Fixed에서 바로 Closed 불가
- 등급 4단계: 치명(Critical)·상(Major)·중(Medium)·하(Minor) — 치명 1건 = 준공 보류 수준
- 잔여 결함 수·등급별 추이가 감리 종료 직전의 핵심 보고 지표
- 도구: Jira·Redmine + 마르미-III 결함 관리표
우리 코드oxios AuditTrail의 이벤트 추적이 결함 추적 데이터와 동형 — 어떤 시점에 어떤 변경이 있었는지 재현 가능하게 남긴다.
관련 항목: 이동
커리큘럼 체인 SI 절차 (마르미 4단계)
Gap 분석 / 인수 검증 Gap Analysis & Acceptance 직무 우리코드
Gap 분석은 인수 직전 '요구사항 정의서·제안서·계약서 대비 실제 구현'의 차이를 항목 단위로 점검하는 마감 활동. 차이가 0이어야 준공(완료 감리 합격)이 가능하다.
Gap 분석은 마지막 단계 활동이에요. '시스템이 처음 약속한 요구사항 정의서·제안서·계약서를 100% 충족하고 있는가'를 항목 단위로 점검해요. 100% 충족이면 '완전 충족', 일부 차이는 '부분 충족'으로 표시하고, 그 차이의 원인(범위 누락, 변경 미반영, 기술적 제약)을 적어요.
그리고 '보완 방안'과 '이후 처리 일정'이 따라옵니다. 발주청과 합의가 끝나야 준공(완료 감리 합격)이 가능해요. 면접에서 이것만 물어보는 경우도 있는데 '문서 대비 실제의 점검'이라고 한 줄 답하면 충분합니다.
왜 중요하냐면 — SI 프로젝트 실패의 상당수가 '코드가 틀려서'가 아니라 '요구한 것과 다른 것을 만들어서'예요. Gap 분석은 그 간극을 준공 전에 공식적으로 드러내는 마지막 안전장치입니다. 자소서에 쓴 '제품의 동작이 틀려서 멈춘 건 하나도 없고, 멈춘 이유는 전부 만들기 전에 확인했어야 할 조건이었다'는 경험이 이 영역과 직결돼요.
- 한 줄 정의: '문서(요구사항·제안서·계약서) 대비 실제 구현'의 차이 점검
- 완전 충족 / 부분 충족(원인+보완 방안+일정)으로 표기
- 발주청 합의 완료 = 준공(완료 감리 합격)의 전제 조건
- SI 실패 원인 = 코드 오류가 아니라 '다른 것을 만듦' — Gap 분석이 마지막 안전장치
우리 코드what-is-oxi의 errata(22건 보고서 정정)가 사실상 '문서 대비 실제' Gap 분석의 실천 사례.
관련 항목: 이동
커리큘럼 체인 SI 절차 (마르미 4단계)
공공 수주 — RFP / 제안 / 입찰 RFP / Proposal / Public Bidding 그림
공공 사업의 수주 흐름. 발주기관이 RFP를 공고 → 사업자가 제안서(기술+가격) 제출 → 평가(기술점수+가격점수, 통상 90:10 또는 80:20)로 낙찰자 결정. 정부·연구기관 R&D 과제는 출연금 매칭 형태가 많음.
RFP = Request For Proposal(제안 요청서) — 발주기관이 사업 요구사항·평가기준·계약 조건을 담은 문서
RFP는 단순한 '하고 싶은 일 목록'이 아니에요. 발주기관은 RFP 안에 입찰 자격 요건(업종 등록, 실적, 인력, 인증), 기술 평가 항목과 배점, 가격 평가 방식, 계약 조건, 제안서 형식(목차·페이지 제한), 평가 일정까지 전부 박아 넣어요. 그래서 좋은 RFP는 '답변 가이드'처럼 읽혀요. 보통 RFP 본문 발췌 → 항목별 응답 → 근거 자료 첨부의 구조로 제안서가 만들어져요.
공공 SI의 제안서 평가는 통상 두 갈래 — 기술 평가(90 또는 80점 만점)와 가격 평가(10 또는 20점 만점). 기술 평가는 (1) 사업 이해도, (2) 기술 능력·아키텍처, (3) 수행 방법론·일정, (4) 프로젝트 관리·인력, (5) 유사 실적, (6) 보안·표준 준수 — 이런 항목으로 나뉘어요. 가격 평가는 예정가 대비 투찰가의 비율을 점수화하는 게 보편적(예: 예정가의 88%면 5점 감점)이라서, 너무 낮아도 마진 박살, 너무 높아도 감점. 그래서 '예정가 ±2%' 범위에 집중 투찰하는 전략이 자주 나와요.
입찰 자격도 까다롭습니다. 보통 (1) 업종 등록 — 정보통신공사업(컴퓨터, GIS SW, 정보보안 등), (2) 실적 — 최근 3~5년간 유사 사업 매출액, (3) 인증 — SP 인증, ISMS-P, 품질 인증 등, (4) 인력 — PM/아키텍트의 자격·경력. 그래서 공공 SI를 노리는 회사라면 자사의 인증·실적·인력을 꾸준히 키워야 해요. 한번 떨어지면 다음 사업에도 영향이 가니까요.
R&D 과제(정부·연구기관)는 일반 SI 입찰과 구조가 조금 다릅니다. 출연금 형태로 정부/지자체가 일정 금액을 매칭하고, 평가에서는 기술성·사업성·활용 계획 비중이 더 커요. 그래서 "R&D와 SI 사업의 차이"는 면접 단골 질문이에요. 핵심은 R&D는 기술 개발 비중, SI는 구축·운영 비중 — R&D 결과가 곧 사업 수주로 이어지려면 기술 이전·제품화의 후속 사업이 따라옵니다.
- RFP는 단순 요건 목록이 아니라 '응답 가이드' — 본문 구조 그대로 제안서 목차가 잡힌다
- 기술:가격 = 90:10 또는 80:20 — 투찰가는 예정가 ±2% 범위에 집중
- 자격 요건은 업종 등록·실적·인증·인력 — SP, ISMS-P가 가점/필수에 자주 등장
GIS / 공간정보 산업 GIS (Geographic Information System) 직무 그림 우리코드
공간(지리) 데이터를 다루는 시스템. 벡터(점·선·면)와 래스터(위성·항공 영상·DEM) 두 데이터 모델, 좌표계(WGS84·Bessel 등), 공간 질의(포함·교차·최근접), 공간 인덱스(R-Tree·Quad-Tree)로 구성. 공간 DB는 PostGIS·Oracle Spatial이 표준.
GIS = Geographic Information System(지리정보시스템) — 지리 데이터를 수집·저장·분석·시각화하는 시스템
공간 데이터는 크게 두 가지 형태예요. 벡터는 점(Point) — 표지판, 건물; 선(Line) — 도로, 강; 면(Polygon) — 행정구역, 토지, 호수. 좌표 쌍으로 정확하게 표현되고, 위상(topology) 정보로 '이 도로는 이 다리와 연결된다' 같은 관계가 잡혀요. 래스터는 픽셀 그리드, 즉 위성 영상·항공 사진·DEM(수치 표고 모델)이에요. 각 셀에 값이 들어 있어서 토지 피복·지형 분석에 강해요. 둘 다 쓰는 것이 일반적이고, 벡터 데이터 위에 래스터를 오버레이하는 게 GIS의 일상.
좌표계는 두 종류만 기억하면 충분합니다. 지리 좌표계(latitude/longitude, 경위도) — 지구 위의 각도를 표현, WGS84가 GPS·국제 표준. 투영 좌표계 — 지구를 평면으로 펴서 m 단위 거리·면적 계산에 사용, UTM-K(한국), TM(중부 원점) 등. 공간 데이터를 다룰 때 좌표계가 다른 데이터를 겹치면 결과가 어긋나요. 그래서 GIS는 '한 시스템 내 모든 데이터는 같은 좌표계로 통일'이 원칙이에요. PostGIS 같은 DB는 SRID(Spatial Reference Identifier)를 컬럼 단위로 강제해요.
공간 인덱스는 R-Tree가 표준이에요. 일반 B-Tree 인덱스는 1차원 정렬에 특화돼 있어서 (x, y) 두 값을 동시에 정렬하지 못해요. R-Tree는 공간을 최소 경계 사각형(MBR)으로 묶어서 트리 검색하기 때문에 '이 점과 가까운 점 100개 찾기', '이 폴리곤과 겹치는 폴리곤 찾기' 같은 질의를 빠르게 처리해요. 면접에서 "왜 B-Tree로 공간 검색을 못 하나요?"라고 물으면, '인덱스 컬럼이 한 값으로 결정되지 않아서 (x, y) 정렬이 불가능'이라고 답하면 정확합니다.
국내 공간정보 산업은 (1) 도시·교통·환경·재난 같은 공공 적용, (2) 물류(경로 최적화)·부동산·광고·유통 같은 민간 적용, (3) 위치 기반 게임·라이프스타일, (4) 디지털 트윈 — 도시 3D 모델 + 실시간 IoT + 시뮬레이션 으로 영역이 확장되고 있습니다. 디지털 트윈은 최근 가장 뜨거운 키워드고, 여기에 LLM·AI가 더해지면서 '도시 운영을 자연어로 묻는' 차세대 GIS가 나올 가능성이 커요.
- 벡터(점·선·면) vs 래스터(픽셀 그리드) — 좌표계 통일(SRID)이 1차 원칙
- 공간 인덱스는 R-Tree — B-Tree로 (x, y) 동시 정렬이 안 되기 때문
- 디지털 트윈 + LLM이 다음 단계 — 시뮬레이션을 자연어 질의로 조율
우리 코드시선아이티의 정체성(GIS 엔진·공간정보 플랫폼·GIS 특허·정부/연구기관 R&D). optimal-route-planner TSP와 settlement A*가 실 구현 경험.
관련 항목: 이동
ForGIS / OGC 표준 공간정보 서버 ForGIS (OGC-compliant Spatial Server) 직무 표 그림 우리코드
시선아이티의 자체 공간정보 서버 솔루션. Server/Desktop/Mobile/Datacenter 변형 제공, 벡터·래스터·공간정보 DB를 다루고 OGC 국제 표준 서비스(WMS/WFS/WCS/WMTS 등)를 준수. 오픈소스 GeoServer에 대응되는 자체 엔진.
OGC = Open Geospatial Consortium(국제 오픈 지리정보 컨소시엄) — 공간정보 상호운용성 표준을 제정하는 국제 기구
OGC(Open Geospatial Consortium)는 공간정보 분야의 국제 표준화 기구예요. GIS 시스템끼리 데이터를 주고받을 수 있는 공통 규약을 만들어요. OGC 표준을 따른다는 건 표준 클라이언트(QGIS, OpenLayers, Leaflet)·표준 서버(GeoServer, MapServer, Mapbox)와 호환된다는 의미예요. 그래서 발주청 RFP에 'OGC 표준 준수'가 적혀 있으면, 표준 클라이언트만 가지고도 데이터를 볼 수 있기 때문에 발주청의 클라이언트 종속성이 사라져요. 이게 공공 사업 수주에서 가산점이 되는 이유예요.
OGC 표준 서비스 4종은 이름이 비슷해서 헷갈리는데 확실히 구분돼요. WMS(Web Map Service)는 그림(래스터 이미지)을 줍니다. 클라이언트가 '이 영역, 이 줌 레벨로 그려줘'라고 요청하면 서버가 PNG/JPEG 이미지를 잘라서 반환해요. 데이터가 아니라 이미지라서 클라이언트에서 피처 단위 검색·편집은 불가. WFS(Web Feature Service)는 데이터(벡터 피처)를 줍니다. 포인트·라인·폴리곤의 좌표와 속성을 GML/GeoJSON으로 돌려주고, 트랜잭션(WFS-T)까지 표준화돼 있으면 생성·수정·삭제도 가능. WMTS(Web Map Tile Service)는 미리 잘라두거나 캐시된 타일(256x256 픽셀 같은)을 잘라서 줍니다. 같은 zoom·x·y 요청에 같은 타일을 반환하기 때문에 CDN처럼 캐시 효율이 매우 높아요. WCS(Web Coverage Service)는 격자 형태의 연속 값(보통 래스터·시계열 데이터)을 줍니다. 위성 영상의 특정 픽셀 값을 그대로 가져올 때 사용해요. 한 줄 정리: WMS = 그림, WFS = 데이터, WMTS = 미리 자른 그림, WCS = 격자 값.
ForGIS는 시선아이티가 자체 개발한 OGC 표준 공간정보 서버예요. 오픈소스 GeoServer의 표준 준수성 + 자체 엔진의 성능 최적화(웹 기반 이미지 가속화, 타일 캐시 전략) 의 결합이라고 설명할 수 있어요. Server/Desktop/Mobile/Datacenter 4가지 변형으로 나뉘는 게 특징인데, 이는 '동일 표준을 다양한 클라이언트 환경에 그대로 뿌리겠다'는 전략이에요. 발주청 입장에서는 '표준만 지키면 된다'는 RFP 조건을 만족시키기 쉬워요.
공간정보 서버의 성능 트레이드오프는 면접에서 자주 나와요. 표준 준수(GeoServer 등)는 보편성과 호환성이 강하지만, 트래픽이 몰리면 이미지 변환·좌표 변환이 CPU를 잡아먹어요. 그래서 실무에서는 (1) WMTS로 정적 베이스맵을 캐시, (2) WFS로 벡터 데이터만 동적 조회, (3) 맵의 3단(베이스맵·라스터 베이스맵·벡터 오버레이) 조합으로 구성, (4) 백엔드는 좌표 변환·공간 질의 부담을 미리 줄이는 정규화·공간 인덱스(R-Tree) 적용 — 이 패키지가 정석이에요.
| 구분 | 특징 | 참고 |
|---|---|---|
| WMS | 그림(래스터 이미지) | GetMap — 좌표 영역 잘라 PNG/JPEG 반환, 피처 검색 불가 |
| WFS | 벡터 데이터(피처 + 속성) | GetFeature — GML/GeoJSON, WFS-T면 편집 가능 |
| WMTS | 미리 잘린 타일(256×256 등) | GetTile — 같은 좌표에 같은 타일, CDN 캐시 효율 |
| WCS | 격자 형태의 연속 값(래스터) | GetCoverage — 위성 영상·DEM의 픽셀 값을 그대로 |
- OGC 4종 — WMS=그림, WFS=데이터, WMTS=타일, WCS=격자값 — 차이를 확실히
- OGC 표준 준수 = 표준 클라이언트(QGIS/OpenLayers)와 호환 — RFP 가산점
- 베이스맵(WMTS) + 벡터 오버레이(WFS) + 동적 질의의 3단 구성 + R-Tree가 성능 정석
우리 코드시선아이티의 자체 솔루션 ForGIS가 이 카테고리 — 오픈소스 GeoServer의 표준 준수성 + 자체 엔진의 성능 최적화가 결합된 형태. 면접에서 '오픈소스와 자체 솔루션의 트레이드오프'를 답할 수 있어야 함.
Java/JSP 백엔드 · 전자정부 표준프레임워크 Java/JSP Backend & eGovFrame 직무 그림 우리코드
국내 공공 SI의 사실상 표준 백엔드 스택. Java + JSP/Servlet + 관계형 DBMS(Oracle/PostgreSQL/MSSQL 등). 정부·지자체·공공기관 사업은 전자정부 표준프레임워크(eGovFrame) 위에서 구축될 확률이 매우 높다.
왜 아직도 Java/JSP냐는 질문을 자주 들어요. 답은 단순합니다 — 공공 SI의 주 스택이 워낙 일찍 Java로 굳어졌고, 발주청이 검증된 표준을 RFP에 명시하기 때문이에요. 전자정부 표준프레임워크(eGovFrame)가 그 표준이에요. 행정안전부 산하 한국정보화진흥원이 배포·운영하는 표준 개발 프레임워크로, Spring 기반 경량 패턴 + 공통 컴포넌트(게시판·로그인·결재·통합검색 등)를 한 묶음으로 제공합니다. 발주청 입장에선 '표준을 따랐는가'만 확인하면 되니까 RFP를 쓰기도, 감리하기도 쉬워요.
eGovFrame의 핵심은 두 가지. 첫째, Spring 기반이라는 점. 따라서 개발자는 Spring의 DI·AOP·MVC 패턴을 알고 있어야 자연스럽게 적응해요. 둘째, 공통 컴포넌트 — 회원·권한·게시판·결재·통합 검색·로그 관리 등 자주 쓰는 모듈을 표준 형태로 제공한다는 점. 그래서 '결재 모듈을 만들어 달라'는 RFP가 오면 표준 컴포넌트 기반으로 빠르게 조립할 수 있어요. 표준 컴포넌트 외 영역은 직접 개발 — 그 경계가 면접에서 자주 문제로 나와요. '표준을 얼마나 따라가야 하는가'에 대한 답은 "먼저 가능한 영역은 표준 컴포넌트로, 표준에 없는 도메인 로직은 별도 모듈로"가 안전합니다.
WAS(Web Application Server) — 어디에 띄우느냐도 표준 환경이 있어요. 무료로 가장 흔한 건 Apache Tomcat. 공공 표준 안내에 자주 등장하는 건 TmaxSoft JEUS. 대기업·금융 쪽에서는 WebLogic, JBoss/WildFly가 등장합니다. 그리고 Oracle이 표준 DB로 가장 흔하지만, 공공 가이드라인이 Tibero(Tmax), Altibase(메모리 DB) 같은 국산 DB를 가산점으로 인정하는 경우가 있어요. 면접에서 "왜 Tibero?"라고 물으면 "국내 기술 자립 + 비용 + 라이선스" 3가지로 답하면 충분합니다.
최근 동향도 짚어둘게요. eGovFrame은 4.x까지 나왔고 Spring 5+ 기반, 그리고 Kotlin/integration, Reactive, WebFlux 등도 부분 지원. 그리고 클라우드·MSA 적용이 확산되면서 eGovFrame 표준이 Kubernetes·Docker 기반 배포까지 가이드가 확장되고 있어요. 하지만 어디까지나 베이스라인이라, 대규모 트래픽이 예상되는 시스템은 별도 아키텍처(Spring Boot + 마이크로서비스 + NoSQL 조합)로 갈래가 갈려요.
- eGovFrame = Spring 기반 + 공통 컴포넌트(게시판·로그인·결재 등) — 표준 준수 여부가 가점
- WAS — Tomcat/JEUS(WebLogic, JBoss), DB — Oracle/Tibero/Altibase — 환경별 결정
- 전자정부 표준은 Servlet·JSP·MVC Model2 패턴을 베이스라인 — 면접 단골
우리 코드시선아이티 채용공고 '개발 PL: JAVA, JSP, DBMS 고급·아키텍처 설계·DB 설계'와 정확히 일치. 면접에서 전자정부 표준프레임워크 경험을 적극적으로 어필.
커리큘럼 체인 SI 절차 (마르미 4단계)
공간정보 × AI 융합 (GeoAI) GeoAI (Spatial + AI) 직무 그림 우리코드
공간정보와 AI(특히 LLM·ML·CV)의 교차점. 자연어→공간 질의 변환(이른바 'LLM-GIS'), 위성·드론 영상 의미 분할(토지 피복 분류), 최적 입지·경로 같은 조합 문제의 ML/휴리스틱 적용.
GeoAI는 두 갈래로 정리하면 이해가 빨라요. (1) 시각(Computer Vision) 쪽 — 위성·드론 영상을 CNN/Transformer로 의미 분할해 토지 피복, 건물, 도로, 농경지를 자동 추출. (2) 언어·추론(LLM) 쪽 — 사용자의 자연어 질의를 구조화된 공간 질의로 바꿔 공간 연산 API를 호출하는 tool-calling 패턴. 이 두 갈래가 만나는 게 디지털 트윈 운영이에요 — 도시 3D 모델 + 실시간 IoT + LLM 질의.
자연어→공간 질의는 어디에 쓸까? 예를 들어 "우리 집에서 30분 거리 안에 있는 24시간 약국 찾아줘"라는 문장이 들어오면, LLM은 (1) '우리 집' → 지오코딩(주소 → 좌표), (2) '30분' → 도보·차량 시간/거리 임계값, (3) '24시간 약국' → POI(Point Of Interest) 카테고리 필터, (4) '찾아줘' → 공간 질의 API 호출(네트워크 분석), (5) 응답을 다시 자연어로 정리. 이 흐름에서 LLM은 의도 파악·설명 계층이고, 공간 연산은 결정적 도구 계층이에요. 두 계층을 잇는 게 tool-calling(함수 호출)이에요.
머신러닝 쪽 GeoAI의 강점은 '라벨된 데이터가 부족한 곳에서도 활용'이에요. 위성 영상 토지 분류 — 일반적으로는 픽셀 단위 random forest, 요즘은 self-supervised 사전학습 → fine-tuning. 디지털 트윈은 더 복잡한데, 도시 3D 모델 + 시계열 데이터 + IoT 센서 값을 결합해서 시뮬레이션(홍수, 교통, 인구 이동)을 돌리고, LLM이 '도시 운영자' 인터페이스로 자연어 질의에 답해요. so what? 도시 정책 결정 지원.
면접에서 자주 나오는 함정도 있습니다. (1) LLM은 좌표를 못 다뤄요 — 좌표 변환·공간 연산은 별도 엔진에 맡겨야 한다. (2) POI 검색은 결정적이어야 한다 — LLM이 '근처'라는 단어를 직접 거리 계산으로 처리하면 부정확, 도구 API에 위임. (3) 개인정보·위치 정보는 ISMS-P·위치정보보호법 통제 안에 있다. 그래서 GeoAI는 LLM + 결정적 공간 엔진 + 통제(인증·권한·마스킹) 3박자 구성입니다.
- GeoAI = CV(영상 의미 분할) + LLM(자연어→공간 질의) 두 갈래의 결합
- LLM은 의도 파악·설명 계층, 공간 연산은 결정적 도구 계층 — tool-calling으로 잇는다
- 위치 데이터는 ISMS-P·위치정보보호법 통제 대상 — 인증·마스킹 필수
우리 코드면접 답변 전략 — 'LLM은 의도 파악·설명 계층, 공간 연산은 결정적 도구 계층, 두 계층을 tool-calling으로 잇는다'. ForGIS의 POI 검색·경로 분석을 LLM이 호출하는 구성.
관련 항목: 이동
ISMS / 개인정보 영향평가 ISMS / PIA (Privacy Impact Assessment) 그림 우리코드
ISMS는 조직의 정보보호 관리체계를 인증·평가하는 제도. PIA(개인정보 영향평가)는 신규 사업에서 개인정보를 처리하기 전 위험을 식별·평가·완화하는 절차. 두 개념 모두 공공·금융 사업의 표준 요건.
ISMS = Information Security Management System(정보보호 관리체계), PIA = Privacy Impact Assessment(개인정보 영향평가) — 신규 사업에서 개인정보 처리 전 위험 평가
ISMS는 한국인터넷진흥원(KISA)이 운영하는 정보보호 관리체계 인증이에요. 평가 영역은 (1) 관리 체계 — 정책·조직·자산 관리, (2) 보호 조치 — 접근 통제·암호화·물리 보안, (3) 사고 대응 — 탐지·대응·복구, (4) 법규 준수 — 개인정보보호법·정보통신망법 등. 인증 심사는 인증심사원(내부 자격을 갖춘 외부 심사자 또는 내부 심사자)이 체크리스트에 따라 문서·현장·인터뷰를 진행하고, 인증 후 3년 주기 갱신이에요. 인증을 받으면 조직의 정보보호 관리 체계가 표준 수준이라는 외부 신호가 됩니다.
PIA(개인정보 영향평가)는 신규 사업·시스템 구축 시 개인정보를 처리하기 전에 '위험을 미리 점찍는' 절차예요. 내부 PIA는 사업자가 자체적으로, 외부 PIA는 KISA에 의뢰해서 진행해요. PIA 핵심 산출물은 (1) 개인정보 흐름도 — 어디서 어떤 정보를 받아 어디로 보내고 저장하는지, (2) 위험 식별 — 무슨 위험이 있는지, (3) 위험 평가 — 심각도·발생 가능성, (4) 완화 조치 — 기술적·관리적 보호조치, (5) 잔여 위험 — 완화 후에도 남는 위험. 그리고 동의를 받는 항목·받지 않는 항목의 구분, 처리방침 변경 필요성도 함께 정리합니다.
ISMS와 PIA의 관계는 '일반 vs 특정'이에요. ISMS는 조직 전체의 정보보호 관리체계를 다루고, PIA는 한 사업의 개인정보 처리 위험에 집중해요. 그래서 한 회사에서 ISMS 인증을 받았어도 모든 신규 사업에서 PIA를 다시 해야 해요. 그리고 사업 입찰 때 RFP가 "ISMS-P 인증 보유 + 본 사업에 대한 PIA 수행"을 자격으로 적는 게 일반적인 패턴이에요. PIA 결과는 발주청에 제출되며, 사업 완료 후 보안 감리의 체크리스트가 됩니다.
통제 항목 측면에서, ISMS-P가 강제하는 통제 — 접근 통제(RBAC), 감사 로그(Audit Trail), 암호화, 백업, 취약점 점검 — 가 코드 레벨에서 요구돼요. 그래서 발주기관은 "본 사업에서 RBAC 구현, Audit Trail 보관, 취약점 점검 보고"를 요구 사항으로 명시합니다. 면접에서 "ISMS 통제와 코드의 연결"을 묻는 건 흔해요. RBAC는 권한별 코드 분기, Audit Trail은 append-only 로그 테이블, 암호화는 칼럼 단위·전송 단위, 취약점 점검은 SAST·DAST 보고서로 대응합니다.
- ISMS = 조직 전체의 관리체계 인증, PIA = 한 사업의 개인정보 위험 평가 — 일반 vs 특정
- PIA 산출물 5종 — 흐름도, 위험 식별, 위험 평가, 완화 조치, 잔여 위험
- 통제 항목 — RBAC, Audit Trail, 암호화, 취약점 점검 — 코드 레벨에서 검증
우리 코드oxios의 RFC-014 OWASP RBAC + AuditTrail이 ISMS 통제의 직접 대응. oxibrain의 projection 스키마는 '무엇을 저장하는지' 데이터 카탈로그로 PIA 산출물에 활용 가능.
관련 항목: 이동
ISMS-P 인증 · 인증심사원 ISMS-P Certification & Lead Auditor 그림
ISMS-P는 정보보호 인증(ISMS)과 개인정보 보호 인증(PIMS)을 한국에서 통합한 인증. 조직의 정보보호·개인정보 관리체계가 통제 항목에 적합한지 심사·인증. 인증심사원은 이 인증을 수행할 자격을 갖춘 전문가.
ISMS-P = Information Security Management System — Personal(개인정보 포함 통합 정보보호 관리체계), KISA(한국인터넷진흥원) 주관, 3년 주기 갱신
ISMS-P는 ISMS + PIMS(개인정보보호 관리체계) 통합 인증이에요. 정보보호 관리 체계 + 개인정보 관리 체계가 한 통으로 묶여서 인증되니까, 한국 같은 공공·금융 중심 시장에서 사실상 표준이 됐습니다. 인증 등급은 일반적으로 (1) 인증 — 모든 항목 적합, (2) 조건부 인증 — 일부 보완 필요, (3) 부적합 — 미흡 항목 과다. 인증심사 결과는 인증서에 그대로 기록되고, 인증 후 3년 주기 갱신 + 매년 사후 점검(surveillance audit)이 따라옵니다.
인증 절차는 보통 이렇게 흘러갑니다. (1) 인증 신청 — 인증기관(KISA가 인증한 인증기관) 선택, (2) 인증심사원 선임 — 통상 2~3인, lead auditor 포함, (3) 1단계 심사 — 문서 검토, (4) 2단계 심사 — 현장 인터뷰·현장 확인, (5) 심사 보고서 — 부적합 사항(non-conformity) 도출, (6) 시정 조치 — 부적합 항목에 대한 사업자 보완, (7) 인증 결정 — 인증위원회의 적합 결정, (8) 인증서 발급.면접에서 '인증심사원이 누구냐'는 답은 KISA가 인증한 인증기관이 소속한 전문 심사자로, 정보보호·개인정보 분야 경력 + 내부 자격 요건을 갖춘 사람이에요.
Lead Auditor(인증 심사원) 자격은 보통 다음을 따릅니다. (1) KISA가 인정하는 심사원 교육과정 수료 — ISMS-P Audit Course, (2) 일정 시간 이상 심사 보조 경험, (3) 정보보호·개인정보 관련 경력. 그리고 국제 자격(IAPP CIPP/CIPP-E, CISSP, CISA)이나 한국 자격(정보보호기사, 개인정보관리사)이 가산점으로 인정됩니다. 회사 입장에서 인증심사원 자격 보유 인력이 있다는 건 통제 항목의 '독립적 검증 가능성'을 의미해서 RFP에서 가점이 됩니다.
인증심사원이 검사하는 통제 영역은 4가지 — 정책(Policy), 기술(Technology), 물리(Physical), 관리(Management). 정책은 정보보호 정책 문서, 기술은 RBAC·암호화·백업, 물리는 출입 통제·CCTV, 관리는 인력 교육·접근 검토 절차. 각각의 통제 항목이 ISMS-P 세부 통제 항목과 매핑돼요. 그래서 심사원은 (1) 정책이 존재하는가, (2) 정책대로 실제 구현·운영하고 있는가, (3) 운영 증거(로그·기록)가 보존돼 있는가, (4) 주기적으로 검토·개선하고 있는가를 봅니다. 어느 하나라도 비어 있으면 부적합 처리됩니다.
- ISMS-P = ISMS + PIMS 통합 — 3년 주기 갱신 + 매년 사후 점검(surveillance audit)
- 인증심사원은 KISA 인정 교육 + 심사 보조 경력 + 정보보호·개인정보 경력 요건
- 통제 4영역 — 정책·기술·물리·관리 — 각각의 운영 증거(로그·기록) 보존이 핵심
관련 항목: 이동
클라우드·데이터 3법 Cloud & Data Laws (3 Acts) 그림
클라우드컴퓨팅법(2025년 전면 시행) + 데이터 3법(개인정보보호법·신용정보법·정보통신망법). 공공은 안심클라우드·조달청 나라장터 인증 절차, 데이터 3법은 가명·익명·연계 처리를 허용.
클라우드 이용의 법적 토대는 클라우드컴퓨팅법과 데이터 3법이에요. 클라우드컴퓨팅법은 클라우드 서비스의 정의·이용자 권리·공공부문 도입 원칙을 규정하고, 데이터 3법(2020년 개정, 2022년·2023년 시행)은 개인정보의 가명 처리·익명 처리·데이터 연계·안전조치를 일관된 체계로 정리했어요. 그래서 '가명 처리했으면 안전조치를 어떻게 해야 하는가', '가명 데이터를 외부에 넘기려면 무엇이 필요한가'는 이전엔 법별로 흩어져 있었는데, 지금은 통합 가이드로 정리됩니다.
공공 클라우드는 추가로 조달청 나라장터 인증 + 안심클라우드 마크가 있어요. 안심클라우드는 한국인터넷진흥원(KISA)의 인증 마크로, CSAP(Cloud Security Assurance Program) 인증 + 공공 부문 요건을 만족한 서비스를 의미해요. 나라장터 등록(조달청 조달 등록)과 함께, 공공 SI가 클라우드를 도입하려면 보통 이 두 마크를 확인합니다. 그래서 RFP에 '안심클라우드 또는 CSAP 인증 보유 사업자만 입찰 가능'이 등장해요.
데이터 3법의 핵심은 '가명 결합'과 '안심구역'이에요. 가명 결합은 여러 기관이 가진 데이터를 개인정보를 가려서 결합하고, 결합된 결과로 통계·연구·AI 모델을 만드는 절차. 데이터 안심구역은 이 결합을 안전하게 수행할 수 있는 물리적·논리적 격리 구역. 그래서 '민감 데이터를 결합해서 분석하려면 어떻게 하나요?'라는 질문엔 "가명 처리 → 데이터 안심구역에서 결합 → 안전조치 강화 → 결합 결과만 반출" 4단계로 답하면 정확합니다.
공공 SI 입장에서 이 모든 게 맞물려요. 시스템 구축 시 입찰 자격에 '안심클라우드 인증', 계약 조건에 '데이터 처리·가명·연계 관련 규제 준수', 표준 안내에 '데이터 3법 가이드라인'. 그래서 면접에서 "이 SI 사업에서 데이터 3법을 어떻게 준수했나요?"라고 물으면, 가명 처리 위치·결합 절차·안심구역 사용·처리방침 반영 4가지로 끊어서 답변해야 해요. 단순한 '잘 지켰습니다'로는 부족합니다.
- 클라우드컴퓨팅법 + 데이터 3법(개인정보·신용정보·정보통신망) — 4법이 통합 규제 베이스라인
- 공공 클라우드는 CSAP 인증 + 안심클라우드 마크 + 나라장터 등록이 자격 요건
- 데이터 안심구역 + 가명 결합 절차 — 민감 데이터 분석의 표준 4단계
oxi 생태계 용어 19
oxicode·oxios·oxibrain·oxibrowser 코드베이스에서만 쓰는 고유 명사 19개 — 다른 도메인 용어와 달리 이 프로젝트 안에서만 통하는 말들.
Kernel
oxios의 도메인 코어 — binary가 아니라 oxios-kernel crate를 거쳐 모든 걸 조립한다.
oxios의 심장이라고 보면 된다. Tool·Provider·Skill·Channel·Agent 같은 트레이트를 wire하고, EventBus로 모든 모듈을 연결한다.
중요한 건 바이너리가 이걸 직접 조립하지 않는다는 것 — oxios-kernel이라는 별도 crate를 거친다. 그래야 커널 조립 로직과 바이너리 진입점이 분리된다.
Surface
oxios가 사람(또는 다른 시스템)과 만나는 경계 — TUI/Web/Messenger 각각이 하나의 surface.
Surface는 Kernel 바깥쪽 껍질이다. TUI surface, Web surface, Messenger surface처럼 '누구랑 어떻게 대화하는지'에 따라 나뉜다.
Surface는 직접 Kernel 내부를 만지지 않고, Channel을 통해서만 통신한다 — 이게 Channel이 따로 존재하는 이유다.
Channel
oxios가 외부로 대화하는 통로 — CLI·Telegram·Discord·Slack·Webhook.
각 채널은 딱 하나의 일만 한다: 들어오는 메시지는 Kernel로, 나가는 응답은 사용자에게. 그 이상의 로직은 없다.
그래서 새 메신저 채널을 붙이는 게 어렵지 않다 — Kernel 로직은 안 건드리고 이 얇은 인터페이스만 새로 구현하면 된다.
Skill
oxios의 확장 단위 — bundled skill과 마켓플레이스(ClawHub) skill이 있다.
oxicode-cli의 skill과 oxios의 skill은 이름만 같고 다른 개념이다. CLI 쪽은 TUI 도구, oxios 쪽은 OS 확장(채널/MCP/web/memory 등).
면접에서 헷갈리기 쉬운 지점 — 어느 프로젝트 얘기인지 먼저 확인하고 답하는 게 안전하다.
Hashline
LLM이 만든 edit를 적용할 때 'line:N:hash'로 앵커하는 patch 포맷.
LLM이 파일을 고칠 때 흔한 문제가 '몇 번째 줄인지'가 살짝 어긋나는 것. hashline은 줄 번호 + 해시를 같이 앵커로 써서, 텍스트가 약간 달라져도 같은 줄을 정확히 짚어낸다.
oxicode-hashline이라는 별도 crate로 분리돼 있다 — 순수 함수 라이브러리라 파일시스템도 에이전트도 모른다.
Snapcompact
vision LLM에 이미지 보낼 때 토큰을 줄이는 압축 전략 — 텍스트를 PNG로 렌더링해서 보낸다.
긴 텍스트를 요약해서 압축하는 대신, 아예 비트맵 이미지로 렌더링해서 vision LLM에 넘긴다는 게 이 전략의 핵심 아이디어다.
oxicode-snapcompact가 별도 crate로 분리돼 있어서 SDK 본체와 버전이 독립적으로 움직인다.
Provider
LLM 공급자 추상 — OpenAI/Anthropic/호환 HTTP/로컬 모델을 같은 트레이트로 다룬다.
oxicode-ai crate가 이 Provider 트레이트를 정의하고, 각 어댑터(OpenAI/Anthropic/Ollama 등)가 그걸 구현한다.
재밌는 건 이 트레이트에 name()이 없다는 것 — provider의 identity는 registry key와 Model.provider 필드에 있지, 트레이트 자체엔 없다. 'identity ≠ transport' 원칙.
CDP (Chrome DevTools Protocol)
Chrome/Chromium을 외부에서 제어하는 WebSocket 프로토콜 — Page/DOM/Network/Runtime/Input 등 12개 도메인.
oxibrowser-cdp가 이 프로토콜을 hyper/tungstenite로 직접 구현한다 — 외부 라이브러리를 감싸는 게 아니라 자체 구현이라는 점이 특이하다.
boa_engine
Rust로 작성된 JS 엔진 — oxibrowser가 여기에 쓴다.
V8을 임베드하는 대신 순수 Rust 엔진을 골랐다. GC 통합이 단순하고 임베드가 쉬운 대신, oxibrowser는 !Send 컨텍스트를 전용 std::thread에 고정해서 CDP actor와 결합하는 구조를 취한다.
wreq
Chromium fingerprint + TLS fingerprint를 다루는 HTTP 클라이언트 — 정확히 =6.0.0-rc.29 고정.
oxibrowser-core가 브라우저 트래픽 쪽에 쓴다. 일반 reqwest와 별개로, 검색 모듈은 그냥 reqwest를 쓴다 — TLS/H2/JS 지문이 전부 Chrome 149 프로필과 맞아떨어져야 하는 브라우저 트래픽과, 그럴 필요 없는 검색 요청을 의도적으로 분리한 것.
MCP (Model Context Protocol)
LLM에 tools/resources를 노출하는 표준 프로토콜.
oxios-mcp는 bridge 역할(외부 MCP 서버를 oxios에 연결), oxibrain-mcp는 자기 자신을 stdio/socket/http 5가지 transport로 노출하는 서버 역할 — 방향이 반대라는 걸 헷갈리지 않아야 한다.
Ouroboros
oxios의 자기-개선 루프 — 에이전트가 자기 설정/스킬/메모리를 보고 개선을 제안한다.
이름 그대로 자기 꼬리를 무는 뱀. RFC-027로 도입됐고, oxios-ouroboros crate가 directive·engine·model_resolver·prompts·resilience 등을 담당한다.
ClawHub
oxios의 skill/persona 마켓플레이스 (RFC-010).
oxibrowser의 'pub' 모드와 결합하면 외부 사용자에게 skill을 배포할 수 있는 구조가 된다.
Companion
oxios의 원격 클라이언트 — React Native + Expo, Noise_XX_ChaChaPoly로 통신.
RFC-044로 추가된 E2EE 원격 페어링 기능의 클라이언트 쪽. oxios 데몬과 암호화된 채널로 붙는다.
AGENTS.md
60K+ 저장소·30+ 도구가 읽는 개방형 표준 — 저장소 루트에 놓는 '에이전트용 README'.
oxi 프로젝트 루트의 AGENTS.md가 이 생태계의 운영 규칙(도구, git 정책, 절대 금지 영역)을 정의한다. 사람이 아니라 코딩 에이전트가 먼저 읽는 문서라는 게 핵심.
OKLCH
OK Lab 색공간 — L(명도)·C(채도)·H(색상)로 색을 표현.
perceptual uniformity가 장점: 같은 L이면 어떤 H(색상)에서도 같은 밝기로 보인다. oxi 디자인 시스템 전체가 이 위에서 라이트/다크 토큰을 정의하는 이유.
Ports / Adapters (hexagonal)
도메인 코어가 외부 시스템을 추상 trait으로만 참조하고, 구체 구현은 별도 crate가 제공하는 구조.
oxibrain이 정확히 이 구조다. core는 LLM/DB/MCP/embed 어댑터를 몰라도 되고, 대신 oxibrain-ports가 정의한 트레이트(LlmPort/EmbeddingPort/TokenizerPort 등) 뒤에서만 만난다.
장점은 교체·테스트·분리 product화가 전부 쉬워진다는 것 — CI가 cargo tree로 이 경계를 실제로 강제한다.
xd:// (OMP tool device)
Oh My Pi 하네스의 tool 디바이스 네임스페이스.
ast_edit / debug / github / lsp / browser / memory_edit / retain / recall / reflect / learn / manage_skill 같은 도구 디바이스가 이 네임스페이스 아래 있다. oxicode의 직계 조상뻘 되는 프로젝트의 개념.
Mnemopi
OMP(Oh My Pi)의 long-term 메모리 시스템 — retain으로 저장, recall/reflect로 조회.
oxicode-mnemopi가 이 시스템을 Rust로 포트한 버전이다. 자체 SQLite + FTS5 + vector 메모리 엔진으로, oxicode-sdk의 MemoryStore 포트가 참조하는 reference 구현.
일치하는 용어가 없습니다
다른 키워드로 검색하거나 도메인 필터를 초기화해 보자. 전체 사이트 검색은 ⌘K.