Recent Posts

All Posts →

대량 데이터를 입출력하는 DBMS별 명령어와 도구(MySQL, Oracle, SQL Server, PostgreSQL)

MySQL, Oracle, MS SQL Server, PostgreSQL이 제공하는 대량 데이터 입출력 방식(LOAD DATA INFILE, SQL*Loader, BULK INSERT, COPY 등)과 성능 옵션을 정리합니다.

DBMS별로 최적화된 대량 데이터 로딩과 추출 기능을 전용 SQL 구문이나 도구로 제공합니다. 이 방식들은 구현 원리상 자바 애플리케이션으로는 도달하기 어려운 성능을 발휘합니다.

1. MySQL

MySQL에서는 SELECT …​ INTO OUTFILE 명령으로 데이터를 익스포트하고, LOAD DATA INFILE 명령으로 임포트합니다. SELECT …​ INTO OUTFILE 은 다음과 같은 형식으로 사용합니다.

SELECT [칼럼명]
INTO OUTFILE [파일명]
FROM [테이블명]

FIELDSLINES 절을 추가하면 필드와 행 사이를 구분하는 문자를 지정할 수 있습니다. 디폴트로는 탭 문자(\t)로 칼럼 사이를, 새 줄 문자(\n)로 행 사이를 구분합니다. baseball.players 테이블의 내용을 players.csv 라는 파일명으로 출력하는 예시는 아래와 같습니다.

SELECT back_num, name, position
INTO OUTFILE 'players.csv'
FROM baseball.players
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'

LOAD DATA INFILE 명령어는 아래와 같은 형식으로 사용합니다.

LOAD DATA INFILE [파일명] INTO TABLE [테이블명]

마찬가지로 필드와 라인 구분자를 지정할 수 있고, SELECT …​ INTO OUTFILE 과 같은 방식으로 옵션을 지정합니다. players.csv 의 내용을 baseball.players 테이블로 로딩하는 명령어는 아래와 같습니다.

LOAD DATA INFILE 'players.csv'
INTO TABLE baseball.players
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'

대량 적재의 성능은 스토리지 엔진에 따라 손볼 곳이 다릅니다. MySQL 5.5부터 기본 스토리지 엔진이 InnoDB이고 요즘은 대부분 InnoDB를 쓰므로, InnoDB 기준으로 정리합니다. InnoDB 테이블에 대량으로 적재할 때는 innodb_buffer_pool_size 를 충분히 확보하고, 적재 구간에서 자동 커밋과 무결성 검사를 잠시 꺼 부가 작업을 줄이는 방식이 효과적입니다.

SET autocommit = 0;
SET unique_checks = 0;
SET foreign_key_checks = 0;

LOAD DATA INFILE 'players.csv'
INTO TABLE baseball.players
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';

COMMIT;

적재가 끝나면 꺼 두었던 unique_checksforeign_key_checks 는 다시 켜야 합니다. 참고로 지금은 거의 쓰이지 않는 MyISAM 엔진에서는 bulk_insert_buffer_size, myisam_sort_buffer_size 같은 변수가 대량 삽입 성능에 영향을 줍니다. 다만 이 값들은 InnoDB 테이블에는 적용되지 않습니다.

LOAD DATA INFILE 명령은 mysqlimport 라는 도구로도 수행할 수 있습니다. 이 도구를 쓸 때 네트워크를 통해 파일이 전송된다면 데이터를 압축해 성능 향상을 꾀할 수 있습니다. 예전에는 --compress 옵션을 썼으나, MySQL 8.0.18부터는 이 옵션이 deprecated되어 --compression-algorithms 로 압축 알고리즘을 지정하는 방식으로 바뀌었습니다.

한 가지 주의할 점은 최근 MySQL이 보안상의 이유로 파일 입출력에 제약을 둔다는 것입니다. SELECT …​ INTO OUTFILE 과 (LOCAL 을 붙이지 않은) LOAD DATA INFILE 은 서버의 secure_file_priv 시스템 변수가 가리키는 디렉터리 안에서만 동작하며, 이 변수는 MySQL 5.7.6부터 기본값이 설정되어 있습니다. 또한 클라이언트 쪽 파일을 읽는 LOAD DATA LOCAL INFILE 은 MySQL 8.0부터 local_infile 변수가 기본적으로 꺼져 있어, 서버와 클라이언트 양쪽에서 이를 명시적으로 켜야 동작합니다.

자세한 내용은 아래 페이지를 참조합니다.

2. Oracle

Oracle에서는 SQL*Loader, Data Pump API 같은 도구나 External Table을 생성해서 데이터 로딩을 할 수 있습니다.

SQL*Loader에서 다루는 파일들은 역할이 상세하게 구분됩니다.

  • 컨트롤 파일 : 수행할 명령문을 담은 파일.

  • 입력 데이터 파일 : DB로 임포트할 데이터를 담은 파일. 컨트롤 파일에서 INFILE * 구문을 사용하면 컨트롤 파일에 데이터를 포함시킬 수도 있습니다.

  • 로그 파일 : 작업 과정과 결과를 기록하는 파일.

  • Bad 파일 : 입력이 거부된 행들이 저장되는 파일. 예를 들면 형식이 틀려서 SQL*Loader가 해석할 수 없거나 DB의 제약조건을 충족시키지 못한 행들이 이 파일로 기록됩니다.

  • Discard 파일 : 컨트롤 파일에서 정한 규칙에 의해 필터링되는 행들이 기록되는 파일.

컨트롤 파일은 아래와 같이 명령행에서 위치를 지정합니다.

sqlldr userid=scott/tiger control='./control.ctl'

컨트롤 파일 안에서는 아래와 같이 입력할 데이터 파일의 위치와 형식, 값이 들어갈 테이블을 지정합니다.

control.ctl
LOAD DATA
INFILE 'players.csv'
INTO players
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
( back_num, name, position )

성능을 높이기 위해 아래 옵션값을 조정해볼 수 있습니다.

  • direct : Direct Path Load 를 사용할지를 true/false로 지정합니다. 이 방식을 쓰면 내부적으로 SQL 명령을 생성하지 않고 데이터베이스 블록에 직접 쓰기 작업을 하기 때문에 성능이 더 좋아집니다. direct 옵션의 디폴트는 false로, 특별히 지정하지 않으면 데이터 로딩을 INSERT 명령으로 수행하는 Conventional Path Load 가 활성화됩니다. Direct Path Load 를 쓸 때는 데이터 로딩 도중에 인덱스 업데이트와 제약조건 등이 바로 적용되지 않으며, 로딩 작업의 시작과 끝에 테이블과 인덱스에 락을 잡습니다. 따라서 로딩 작업 도중에 다른 프로세스에서도 대상 테이블에 접근해야 한다면 적합하지 않습니다. 클러스터링된 테이블에도 사용할 수 없습니다.

  • parallel : 병렬 수행 여부입니다. parallel 모드로 데이터를 올리는 중에는 인덱스가 업데이트되지 않으므로, 데이터 업로드가 끝난 후에 인덱스를 재생성해줘야 합니다. direct=true 와 함께 쓰려면 대상 테이블에 트리거나 제약조건이 비활성화되어 있어야 합니다.

  • rows : 한 번에 운반하는 행의 개수입니다. direct=false 일 때는 commit이 일어나는 단위로, 스프링 배치의 commit-interval 과 유사합니다. direct=trueDirect Path Load 를 쓸 때도 rows가 데이터를 저장하는 단위라는 점은 비슷하지만, 이 모드에서는 중간에 인덱스가 갱신되지 않습니다.

그 외에도 direct 옵션을 사용할 때는 columnarrayrows, streamsize, 아닐 때는 bindsize 등의 옵션으로 데이터의 운반 단위를 조정할 수 있습니다. 위의 옵션은 명령행에서 지정할 수 있습니다. 예를 들어 direct와 parallel 모드를 동시에 사용해서 데이터를 임포트하려면 아래와 같이 실행합니다.

sqlldr userid=scott/tiger control='./control1.ctl' DIRECT=TRUE PARALLEL=true
sqlldr userid=scott/tiger control='./control2.ctl' DIRECT=TRUE PARALLEL=true

Oracle 9i부터는 데이터를 테이블스페이스 바깥의 파일로 유지하는 External Table이 제공됩니다. CSV 형식의 외부 파일을 External Table로 연결하고 SELECT 구문으로 조회해서 테이블에 저장하는 작업도 가능합니다. External Table에 SELECT를 할 때 JOIN, SORT도 사용할 수 있고, 읽기 작업을 병렬로 수행할 수 있다는 장점도 있습니다. 그러나 읽기 전용 작업만 허용되기 때문에 DELETE나 UPDATE 같은 SQL을 External Table을 대상으로 수행할 수는 없습니다.

같은 Oracle DB 간의 테이블 단위 데이터 로딩에는 import, export 유틸리티가 더 성능에 유리합니다. Oracle Database 10.1(10g release 1) 이후부터는 이를 대체하는 Data Pump API가 제공됩니다. 자세한 내용은 아래를 참조합니다.

3. MS SQL Server

SQL Server에서는 OPENROWSET과 BULK INSERT 구문이 제공되고, bcp나 SSIS(Integration Services) 패키지 같은 별도의 도구로도 비슷한 작업을 수행할 수 있습니다.

BULK INSERT 구문은 파일을 지정하는 것 외에는 보통의 INSERT 문처럼 사용할 수 있습니다. players.csv 에서 players 테이블로 데이터를 입력하는 예제는 아래와 같습니다.

BULK INSERT players
FROM 'players.csv'
WITH (
  FIELDTERMINATOR = ',',
  ROWTERMINATOR = '\n'
)

BULK INSERT를 할 때는 아래와 같은 옵션을 조정해 성능을 높일 수 있습니다.

  • tablock : 대량 입력 작업 동안 테이블을 잠급니다.

  • rows_per_batch : 하나의 트랜잭션으로 처리할 행의 개수입니다. 스프링 배치의 commit-interval 속성과 유사합니다. 되도록 큰 값을 지정하는 것이 성능 향상에 유리하나, 인덱스가 있는 테이블에서 지나치게 큰 값을 지정하면 메모리 사용량이 많아지고 병렬 업로드 시 테이블 락을 유발할 수 있습니다.

앞선 예제에서 테이블 락을 걸고 3000개씩 commit을 하는 옵션을 추가하면 다음과 같습니다.

BULK INSERT players
FROM 'players.csv'
WITH (
  FIELDTERMINATOR = ',',
  ROWTERMINATOR = '\n',
  TABLOCK,
  ROWS_PER_BATCH = 3000
)

파일로부터 데이터를 읽어 들이는 또 하나의 방법인 OPENROWSET 구문은 아래와 같이 SELECT 문장과 함께 사용합니다.

SELECT *
FROM OPENROWSET('MSDASQL',
     'Driver={Microsoft Text Driver (*.txt; *.csv)};DefaultDir=C:\data;',
     'SELECT * FROM players.csv')

칼럼명은 텍스트 파일의 첫 줄에 포함시키거나 Schema.ini 파일로 정의할 수 있습니다. 다만 위 예제에서 쓰인 Microsoft Text Driver 는 32비트 전용인 구형 Jet 기반 드라이버로, 현재는 권장되지 않습니다. 최근 환경에서는 64비트를 지원하는 ACE(Access Connectivity Engine) 공급자(Microsoft.ACE.OLEDB.x)를 쓰거나, 아예 SQL Server가 직접 파일을 읽는 OPENROWSET(BULK …​) 구문을 사용하는 편이 낫습니다.

SQL Server는 bcp라는 별도의 데이터 임포트/익스포트 유틸리티도 제공합니다. bcp는 본래 Sybase에서 유래한 도구로, SAP ASE(과거 Sybase ASE)의 bcp와도 사용법이 비슷합니다. 데이터 익스포트와 임포트를 모두 SQL Server를 대상으로 할 때는 네이티브 형식을 이용하면 성능이 더 향상됩니다. BULK INSERT 구문에서는 DATAFILETYPE = 'native' 옵션으로, bcp에서는 -n 옵션을 지정해서 네이티브 파일을 사용할 수 있습니다.

그리고 SQL Server와 함께 제공되는 데이터 통합 도구인 SSIS(Integration Services) 패키지로도 다양한 자원에서 데이터를 입출력할 수 있습니다. GUI 도구에서 작업을 설계하고 dtexec.exe 라는 실행 파일로 커맨드라인에서 작업을 실행하는 방식도 가능합니다. ETL 도구와 유사한 접근법입니다. 앞에서 BULK INSERT 문의 옵션이었던 tablock, rows_per_batch 등의 값도 SSIS에서 설정할 수 있습니다.

각각의 방식에 대한 자세한 설명은 아래 페이지를 참고합니다.

4. PostgreSQL

PostgreSQL에서는 COPY 구문, Foreign Data Wrapper, pg_bulkload를 활용해 텍스트 파일을 바로 DB에 익스포트/임포트할 수 있습니다.

테이블에 있는 데이터를 COPY 구문으로 아래와 같이 익스포트합니다.

COPY (SELECT name, position, back_num FROM players)
TO 'players.csv'
WITH (FORMAT CSV);

임포트 명령은 아래와 같습니다.

COPY players (name, position, back_num)
FROM '/data/players.csv' DELIMITER ',' CSV;

마찬가지로 같은 PostgreSQL끼리 데이터를 주고받을 때는 포맷을 BINARY 로 지정하면 성능 향상을 유도할 수 있습니다.

PostgreSQL에서는 DB 밖에 저장된 데이터를 DB 안의 객체처럼 접근하는 Foreign Data Wrapper라는 개념도 지원합니다. Oracle의 External Table이나 SQL Server의 OPENROWSET 방식과 유사합니다. 아래와 같이 CREATE EXTENSION, CREATE SERVER 구문으로 file_fdw 모듈을 설치합니다.

CREATE EXTENSION file_fdw;
CREATE SERVER baseball FOREIGN DATA WRAPPER file_fdw;

그리고 파일에서 읽어올 데이터의 스키마를 아래와 같이 정의합니다.

CREATE FOREIGN TABLE players (
  name VARCHAR(30),
  position VARCHAR(30),
  back_num INTEGER
) SERVER baseball
OPTIONS ( filename '/home/benelog/players.csv', format 'csv' );

format, header, delimiter 등의 옵션은 COPY 구문과 똑같이 사용할 수 있습니다.

pg_bulkload라는 전용 도구도 지원합니다. Oracle의 SQL*Loader처럼 ctl 파일에 작업의 명세를 정의할 수 있습니다. CSV 파일을 로딩하는 ctl 파일은 아래와 같이 정의합니다.

players_csv.ctl
OUTPUT = players
INPUT = /home/benelog/players.csv
TYPE = CSV
QUOTE = "\""
ESCAPE = \
DELIMITER = ","

명령행에서 다음과 같이 실행하면 CSV 파일의 내용이 테이블로 입력됩니다.

pg_bulkload players_csv.ctl

각각의 방식에 대한 자세한 사용법은 다음을 참조합니다.

RAG 시스템의 구성요소와 성능을 좌우하는 요소

RAG 시스템을 구성하는 요소들 — Vector DB, 임베딩 모델, 청킹, 재순위, 생성 LLM — 의 역할을 정리하고, 한국어 처리에서 중요한 지점과 성능이 나쁠 때 어디를 손봐야 하는지 살펴봅니다.

앞 글에서 RAG가 결국 프롬프트 레이어에 데이터를 공급하는 장치지만, 앞단의 검색/필터링 파이프라인이 충분히 복잡해 별도 서브시스템으로 다룰 만하다고 정리했습니다. 이 글은 그 서브시스템 안을 들여다봅니다. 어떤 부품으로 구성되는지, 한국어를 다룰 때 어디가 까다로운지, 성능이 안 나올 때 어디를 의심해야 하는지를 정리합니다.

1. RAG 파이프라인의 기본 흐름

전형적인 RAG는 두 시점으로 나눠 볼 수 있습니다.

인덱싱 시점 (오프라인)

원본 문서 → 청킹 → 임베딩 → Vector DB 저장. 메타데이터(출처, 작성일, 권한 등)도 함께 저장.

질의 시점 (온라인)

사용자 질문 → 쿼리 재작성 → 임베딩 → Vector DB 검색 (+ 키워드 검색) → 재순위(rerank) → 컨텍스트 조립 → LLM에 프롬프트로 전달 → 응답 생성.

각 단계마다 별도 모델/시스템이 끼어들 수 있고, 단계마다 다른 튜닝이 필요합니다.

2. Vector DB는 어떤 역할을 하는가

Vector DB는 한마디로 "고차원 벡터의 근사 최근접 이웃(approximate nearest neighbor, ANN) 검색에 특화된 저장소" 입니다. 임베딩 모델이 만든 수백~수천 차원 벡터를 저장하고, "쿼리 벡터와 가장 가까운 K개"를 빠르게 돌려줍니다.

핵심 책임은 세 가지입니다.

  • 벡터 인덱싱: HNSW, IVF, PQ 같은 ANN 인덱스로 검색을 sub-linear 시간에 처리합니다. 정확도 vs. 속도/메모리의 트레이드오프가 있습니다.

  • 유사도 계산: 코사인 유사도, 내적, L2 거리 등. 임베딩 모델이 가정한 거리에 맞춰야 합니다.

  • 메타데이터 필터링: "2025년 이후 문서만", "권한이 있는 문서만" 같은 조건을 벡터 검색과 결합. pre-filter / post-filter 시점이 성능을 크게 좌우합니다.

흔한 오해 지점들이 있습니다.

  • Vector DB가 의미를 이해하지는 않습니다. 의미를 벡터로 인코딩하는 건 임베딩 모델이 하고, Vector DB는 벡터 간 기하학적 거리만 봅니다. 검색 품질이 나쁘면 대개 임베딩 모델 문제이지 Vector DB 문제가 아닙니다.

  • 벡터 검색이 항상 키워드 검색보다 낫지는 않습니다. 고유명사, 제품번호, 코드 같은 토큰은 BM25 같은 lexical 검색이 더 정확합니다. 그래서 실제 RAG는 벡터 + 키워드 하이브리드가 기본입니다.

  • 전용 Vector DB가 늘 필요하지도 않습니다. PostgreSQL pgvector, Elasticsearch/OpenSearch의 dense vector 필드, Redis vector search 모듈로도 상당 규모까지 커버됩니다. 운영 부담을 줄이려면 기존 DB 확장이 합리적일 때가 많습니다. Pinecone, Weaviate, Qdrant, Milvus 같은 전용 솔루션은 규모/성능 요구가 분명해질 때 고려하면 됩니다.

3. 한국어 처리에서 중요한 구성요소

한국어 RAG는 영어보다 손이 많이 갑니다. 어디서 차이 나는지 짚어봅니다.

3.1. 임베딩 모델의 한국어 이해 능력

가장 결정적인 부품입니다. OpenAI text-embedding-3, Cohere embed-multilingual, Voyage, BGE-M3 같은 다국어 임베딩이 점점 좋아지고 있지만, 한국어 전용 튜닝 모델(KURE, bge-m3-korean, 네이버 HyperCLOVA 임베딩 등)이 도메인에 따라 더 잘 맞기도 합니다. MTEB 한국어 벤치마크를 참고하되, 자기 도메인 문서로 직접 평가해보는 게 가장 정확합니다.

3.2. 토크나이저와 형태소 분석

  • 벡터 검색 쪽: 임베딩 모델 내부 토크나이저를 따라가므로 사용자가 손댈 여지가 적습니다.

  • 키워드 검색 쪽: 한국어에서 가장 손이 많이 가는 지점입니다. 영어는 공백 분리만으로도 어느 정도 작동하지만, 한국어는 조사/어미가 붙은 채로 인덱싱되면 검색이 망가집니다. "은행에서", "은행이", "은행은"이 다 다른 토큰이 되니까요.

    Elasticsearch/OpenSearch는 Nori 분석기, 그 외 Mecab-ko / Khaiii / KIWI 같은 형태소 분석기를 식별자 추출/색인에 씁니다. 사용자 사전(custom dictionary) 관리가 매우 중요합니다. 신조어, 사내 용어, 제품명을 분석기가 모르면 엉뚱한 토큰으로 쪼개집니다.

3.3. 청킹 전략

한국어 문서는 한 문장이 길고 접속사 없이 늘어지는 경우가 많아 고정 길이 청킹이 의미 단위를 자주 끊습니다.

  • 문장 분리(sentence splitting) 가 마침표 한 종류만으로는 안 됩니다. "다.", "요.", "음.", "임." 같은 어미와 줄바꿈, 표/리스트 마크업까지 함께 봐야 합니다. KSS(Korean Sentence Splitter) 같은 라이브러리가 도움이 됩니다.

  • 의미 단위 청킹(semantic chunking) — 임베딩 유사도가 급격히 변하는 지점에서 자르기 — 이 한국어에서 효과가 좋은 편입니다.

  • 계층적 청킹: 큰 청크로 검색하고 매칭된 부분의 작은 청크를 LLM에 넘기는 식. 문맥 의존성이 강한 한국어에 특히 유용합니다.

3.4. 재순위(rerank) 모델

Cross-encoder 기반 reranker는 검색 결과를 다시 정렬해 정확도를 크게 끌어올립니다. Cohere Rerank 다국어 모델, BGE-reranker, Jina multilingual reranker 등이 한국어를 다룹니다. 한국어 전용 reranker는 아직 선택지가 적어, 다국어 모델로 시작해 자체 평가셋으로 fine-tune을 검토하는 흐름이 흔합니다.

3.5. 쿼리 처리

한국어 질문은 구어체, 줄임말, 오타가 많습니다. 쿼리 재작성(query rewriting) 이나 HyDE(hypothetical document embedding) 로 LLM을 한 번 거쳐 정제하면 검색 품질이 눈에 띄게 좋아집니다. 이때 쓰는 LLM은 작아도 충분합니다.

4. RAG 안에서 쓰이는 LLM들

"RAG에 LLM 하나"라고 생각하기 쉽지만, 실제 시스템에는 서로 다른 역할의 모델 여러 개가 들어가고, 각각 요구되는 특성도 다릅니다.

위치 역할 필요한 특성

임베딩 모델

텍스트를 벡터로 변환

다국어/한국어 품질, 적절한 차원 수, 빠른 추론 속도, 긴 입력 지원

쿼리 재작성 / 분기

사용자 질문을 정제하거나 어떤 도구/인덱스를 쓸지 결정

작고 빠른 instruction-following 모델로 충분. 지연 시간이 중요

재순위 모델

검색된 후보 K개를 정밀 점수화

Cross-encoder. 일반 LLM이 아니라 전용으로 학습된 reranker가 효율적

응답 생성 LLM

검색된 문맥을 바탕으로 최종 답변 작성

긴 컨텍스트, 인용 능력, 환각 억제, 한국어 자연스러움. 대형 모델이 유리

평가 / 가드레일

응답이 컨텍스트에 충실한지, 안전한지 검증

비교적 작은 모델로 LLM-as-a-judge. 일관성과 비용이 중요

흔한 구성은 "쿼리 처리/판정은 작고 빠른 모델, 최종 생성은 큰 모델" 입니다. 전부 큰 모델로 돌리면 비용과 지연이 누적되고, 전부 작은 모델로 돌리면 답변 품질이 무너집니다. 단계별 적정 사이즈 선택이 RAG 설계의 중요한 부분입니다.

또 한 가지, 검색 결과를 잘 활용하는 능력은 모델마다 다릅니다. 같은 컨텍스트를 줘도 어떤 모델은 인용에 충실하고, 어떤 모델은 사전 지식으로 답을 만들어버립니다. RAG 평가 때는 모델의 groundedness(컨텍스트 충실도)를 따로 측정하는 게 좋습니다.

5. 성능이 안 나올 때 어디를 손볼 것인가

RAG 결과가 만족스럽지 않을 때, 무작정 더 큰 LLM으로 갈아끼우기 전에 파이프라인의 어느 단계가 망가졌는지 진단하는 게 우선입니다. 흔한 원인을 단계별로 정리합니다.

5.1. 1단계 — 검색이 잘못 가져온다 (retrieval failure)

가장 흔한 원인입니다. 답이 든 문서를 애초에 못 찾아왔다면, 뒤에 어떤 LLM을 써도 좋아질 수 없습니다.

진단: 정답이 든 청크가 top-K 안에 들어오는지 확인. 안 들어오면 검색 단계 문제입니다.

손볼 곳:

  • 임베딩 모델 교체: 한국어 품질이 더 좋은 모델로. 도메인 특화 모델 검토.

  • 하이브리드 검색 도입: 벡터 + BM25 결합. 고유명사/숫자 매칭이 안 되던 케이스가 풀립니다.

  • 청킹 재설계: 너무 크면 노이즈, 너무 작으면 문맥이 끊깁니다. 한국어는 보통 300~800 토큰에서 시작해 조정.

  • 메타데이터 필터: 검색 공간을 좁히는 가장 효과적인 수단. 날짜, 출처, 권한 등을 적극 활용.

  • 사용자 사전: 키워드 검색 쪽 도메인 용어가 잘못 토큰화되는지 점검.

5.2. 2단계 — 가져오긴 했는데 순서가 나쁘다 (ranking failure)

top-20에는 정답이 있는데 top-3에는 없는 경우. LLM에 넘기는 컨텍스트가 한정돼 있어 순서가 결과를 좌우합니다.

손볼 곳:

  • Reranker 추가: cross-encoder reranker가 가장 큰 효과를 내는 지점입니다.

  • 쿼리 재작성: 모호하거나 구어체인 질문을 검색에 유리한 형태로 변환.

  • 다중 쿼리 (multi-query): 한 질문을 여러 변형으로 던져 결과를 합칩니다.

5.3. 3단계 — 컨텍스트는 좋은데 생성이 망가진다 (generation failure)

검색은 잘 됐고 정답 청크가 top에 있는데 최종 응답이 틀리거나 환각이 섞이는 경우.

손볼 곳:

  • 프롬프트 구조: "주어진 문맥에만 근거해 답하라", "문맥에 없으면 모른다고 답하라" 같은 지시. 인용 형식 강제.

  • 컨텍스트 배치: 중요 문서를 맨 앞이나 맨 뒤에. "lost in the middle" 회피.

  • 생성 모델 업그레이드: 여기서는 큰 모델이 효과적. 단, 1~2단계가 망가져 있으면 모델만 키워도 효과가 없습니다.

  • citation / grounded generation: 답변마다 출처 청크를 명시하게 하면 환각이 줄어듭니다.

5.4. 4단계 — 평가가 없어서 어디가 망가졌는지 모른다 (evaluation gap)

실은 가장 근본적인 문제입니다. 평가셋이 없으면 변경이 개선인지 알 수 없습니다. "감으로" 튜닝하다 보면 한쪽을 고치며 다른 쪽이 망가집니다.

손볼 곳:

  • 질문-정답-출처 셋으로 된 평가 데이터셋 구축. 50~100개로도 시작 가능.

  • 단계별 지표: 검색은 recall@K, MRR. 생성은 faithfulness, answer relevance.

  • Ragas, TruLens 같은 평가 프레임워크 활용.

  • LLM-as-a-judge로 자동화하되 사람이 주기적으로 보정.

6. 어디부터 손댈지

새 RAG 시스템을 만들거나 기존 것을 개선할 때 제가 보는 우선순위는 다음과 같습니다.

  1. 평가셋부터 만든다. 없으면 개선이 개선인지 모릅니다.

  2. 하이브리드 검색을 기본으로. 벡터만 쓰지 않습니다.

  3. 한국어라면 형태소 분석기와 사용자 사전 정비. 키워드 검색의 비중이 의외로 큽니다.

  4. reranker 도입. 비용 대비 효과가 큰 편입니다.

  5. 생성 모델 업그레이드는 마지막에. 가장 비싸고, 1~3이 부실하면 효과가 작습니다.

RAG의 성능 개선은 "더 큰 모델"이 아니라 "더 정확한 검색" 에서 오는 경우가 대부분입니다. 앞 글에서 RAG를 "프롬프트 레이어로 데이터를 흘려보내는 파이프라인"이라 부른 이유도 여기 있습니다. 모델은 토큰으로 들어온 것에 답할 뿐, 들어오지 않은 것에는 답할 수 없습니다.


이 포스트는 Claude(claude.ai)와 정상혁이 함께 작성했습니다.

LLM에 정보를 전달하는 계층 분류와 회색지대

LLM에 정보를 주입하는 방법을 모델 레이어와 프롬프트 레이어로 나누어 보고, 그 사이의 회색지대와 RAG도 이 분류에서 어디에 놓이는지 정리해봅니다.

LLM(Large Language Model)에 정보를 전달하는 방법을 정리할 때 저는 보통 모델 가중치 vs. 프롬프트의 2계층으로 나눠 생각해 왔습니다. 최근 몇 년 사이에 그 중간에 위치하는 기법들도 늘었습니다.

LLM에 정보를 전달하는 계층 마인드맵
Figure 1. LLM에 정보를 전달하는 계층 분류

1. 모델 레이어

여기서 말하는 모델 파라미터(model parameter) 는 함수 인자나 설정값이 아니라, 신경망 내부에 학습으로 결정된 숫자값입니다. 입력 토큰을 다음 토큰의 확률로 변환하는 모든 계산이 이 숫자들로 이뤄집니다. 대부분은 레이어 간 연결 강도인 가중치(weight) 이고, 편향(bias) 등이 더해집니다. 물리적으로는 낱개가 아니라 벡터/행렬(텐서) 형태로 묶여 GPU에서 행렬 곱으로 한꺼번에 계산됩니다. 요즘 LLM은 이 숫자가 수십억~수조 개 수준이며, 학습은 결국 이 숫자를 조금씩 바꾸는 과정입니다. 모델이 "아는 것"은 이 파라미터 안에 압축돼 있습니다.

이 레이어의 방법들은 가중치를 직접 바꾸거나, 가중치에 준하는 영구적 상태를 만듭니다.

  • Pre-training: 가장 근본적인 정보 주입. 코퍼스의 분포가 곧 모델의 세계관이 됩니다.

  • Continued pre-training (도메인 적응): 의료/법률/코드 등 특정 도메인 코퍼스로 추가 학습. 파인튜닝보다 광범위한 분포 이동을 노립니다.

  • Supervised fine-tuning (SFT): 입출력 페어로 행동을 맞춥니다. 지식 주입보다 형식/스타일/태스크 수행에 효과적입니다.

  • 선호 학습 (preference learning): "어떤 응답이 더 나은가"를 가중치에 새깁니다. 사실 지식이 아니라 가치/취향 주입이라는 점에서 결이 다릅니다.

    • RLHF(Reinforcement Learning from Human Feedback): 사람이 매긴 선호로 보상 모델을 학습한 뒤, 그 보상을 최대화하도록 PPO 등 강화학습으로 정책을 업데이트하는 고전적 방식

    • DPO(Direct Preference Optimization): 별도의 보상 모델과 강화학습 루프 없이, 선호 쌍(채택/기각)으로부터 정책을 직접 최적화해 RLHF를 단순화

    • KTO(Kahneman-Tversky Optimization): 쌍 비교 대신 응답 하나가 "좋다/나쁘다"는 단일 라벨만으로 학습. 전망 이론(prospect theory)의 효용 함수를 차용

    • IPO(Identity Preference Optimization): DPO가 선호 데이터에 과적합되는 경향을 막도록 목적 함수에 정규화 항을 더한 변형

  • RLVR(Reinforcement Learning with Verifiable Rewards): 수학 정답, 코드 실행 결과처럼 자동 채점 가능한 신호로 강화학습. DeepSeek-R1 계열은 이 방식이고, o1도 강화학습 기반 추론 모델로 알려져 있지만 보상 설계의 세부는 공개되지 않았습니다. 사람의 선호가 아니라 풀이 능력 자체를 새긴다는 점에서 RLHF와 구분됩니다. Cursor의 Composer 2.5는 이를 에이전트 환경으로 확장한 예로, 도구(편집·검색·테스트 실행)를 호출하는 멀티스텝 롤아웃에서 테스트 통과 여부를 검증 가능한 보상으로 씁니다.

  • Model editing: 특정 사실 하나를 가중치 차원에서 직접 수정. "에펠탑은 로마에 있다"로 바꾸는 수술 같은 기법입니다. 본격적 학습이 아니어서 회색지대로 보기도 합니다.

    • ROME(Rank-One Model Editing): 해당 사실을 저장한 MLP 가중치에 rank-1 업데이트를 가해 사실 하나를 갈아끼움

    • MEMIT(Mass-Editing Memory In a Transformer): ROME을 다중 레이어로 확장해 수천 개의 사실을 동시에 편집

    • MEND(Model Editor Networks with Gradient Decomposition): 메타 학습된 보조 네트워크가 편집용 그라디언트로 변환해 빠르게 적용

  • Knowledge distillation: 큰 모델의 지식을 작은 모델의 가중치로 압축.

  • Merging: 여러 파인튜닝 결과의 가중치를 평균/합성. 학습 없이 "정보를 섞는다"는 점에서 새로운 결입니다.

    • TIES(TrIm, Elect Sign & Merge): 변화가 작은 파라미터는 잘라내고, 부호 충돌을 다수결로 정한 뒤 평균

    • DARE(Drop And REscale): 파인튜닝 델타의 대부분을 무작위로 버리고 남은 값을 재스케일링해 머지

    • SLERP(Spherical Linear intERPolation): 두 모델 가중치를 단위 구면상 호로 보간

2. 프롬프트 레이어

해당 레이어는 추론 시점에 컨텍스트 윈도우로 들어가는 모든 것입니다.가중치는 건드리지 않습니다.

  • 시스템 프롬프트 / 페르소나 지시

  • Few-shot 예시 (in-context learning)

  • Chain-of-Thought, ReAct(Reasoning + Acting), self-consistency 같은 reasoning 패턴 유도

  • Tool use / function calling의 결과를 컨텍스트로 되먹임

  • RAG(Retrieval-Augmented Generation)로 검색해 온 문서 청크

  • 외부 메모리 시스템에서 불러온 과거 대화 (Claude의 memory 같은 것)

  • 구조화된 출력 지시 (XML 태그, JSON 형식 지시)

  • MCP(Model Context Protocol)/connector를 통해 들어오는 외부 데이터

공통점은 inference time에 토큰으로 입력된다는 것입니다. 모델은 최종적으로 토큰과 role/구조 신호를 보고 처리하며, 외부 데이터의 원천 신뢰도를 직접 검증하지는 않습니다.

3. 회색지대

모델 레이어와 프롬프트 레이어 어디에도 깔끔하게 들어가지 않는 기법들입니다.

  • PEFT(Parameter-Efficient Fine-Tuning) 계열: 가중치를 학습시키니 형식상 모델 레이어지만, 베이스 모델은 두고 작은 어댑터만 붙였다 뗐다 합니다.

    • LoRA(Low-Rank Adaptation) / QLoRA(Quantized LoRA): 가중치 변화를 저차원 행렬로 근사해 학습. QLoRA는 베이스 모델을 양자화해 메모리를 더 절약

    • adapters: 트랜스포머 레이어 사이에 작은 학습 모듈을 끼워 넣고 그 부분만 학습

    • prefix tuning / prompt tuning / P-tuning: 학습 가능한 "가상 토큰"을 컨텍스트 앞에 붙이는 방식이라 소프트 프롬프트(soft prompt) 라 불릴 만큼 프롬프트 레이어의 사촌

  • KV(Key-Value) cache 주입 / cache steering: 특정 컨텍스트로 만든 KV cache를 저장해 두고 추론 시 재사용. 매번 프롬프트로 넣지는 않지만, 이미 계산된 컨텍스트를 다시 붙이는 효과가 있습니다. 일반적으로는 비용 최적화용 런타임 상태이며, 모델 가중치나 장기 메모리와는 구분됩니다.

  • Activation steering / representation engineering: 추론 중 특정 레이어의 활성값에 벡터를 더해 행동을 바꿉니다. 가중치도 프롬프트도 건드리지 않는 완전히 새로운 결입니다.

  • Decoding 제어: 출력 확률 분포를 직접 조작. "주입"보다 출력 제한에 가깝지만, 도메인 지식을 grammar로 강제하면 사실상 정보 주입입니다.

    • constrained decoding / guided generation: 정해진 문법(grammar)·스키마를 따르도록 토큰 선택을 제한해 출력 형식을 강제

    • structured outputs: JSON Schema 같은 스키마를 입력으로 주되, strict 모드에서는 서버/런타임의 constrained decoding과 결합되어 프롬프트 레이어와 디코딩 레이어에 걸침

    • logit bias: 특정 토큰의 로짓에 값을 가감해 등장 확률을 직접 조절

    • speculative decoding: 작은 드래프트 모델로 여러 토큰을 미리 뽑고 본 모델로 한 번에 검증. 정보 주입이 아니라 속도 최적화에 가까움

  • Tool use의 양면성: 도구 호출 자체는 프롬프트 레이어지만, 도구가 외부 세계의 상태를 바꾼다면 "LLM에 정보를 주는 것"이 아니라 "LLM이 정보를 만들어내는 것"입니다. 경계가 모호합니다.

4. 두 레이어 외에 추가로 생각해볼 만한 축

위의 2계층 분류는 정보가 어디에 저장되는가 기준입니다. 다른 축도 있습니다.

아키텍처 레이어: 모델 구조 자체. MoE(Mixture of Experts)로 전문가를 분리하거나, 검색 모듈을 구조에 통합하거나, 메모리 토큰 슬롯을 두는 식. 가중치 학습 이전에 "어떤 형태로 정보를 담을 것인가"를 정합니다. retrieval-augmented architecture/pretraining의 예:

  • REALM(Retrieval-Augmented Language Model pre-training): 사전학습 단계부터 retriever를 함께 학습해 외부 코퍼스 검색을 모델에 통합

  • RETRO(Retrieval-Enhanced TRansfOrmer): 트랜스포머 블록에 검색된 청크의 chunked cross-attention을 끼워 넣어 작은 모델로 큰 효과

표현 / 활성값 레이어: 앞서 언급한 activation steering, probing, representation engineering. 가중치도 컨텍스트도 아닌 forward pass 중간 상태에 개입합니다.

디코딩 / 샘플링 레이어: temperature, top-p, constrained decoding, beam search. 정보 주입이 아니라 추출 방식 제어지만, 모델이 "무엇을 말할 수 있는가"를 좌우합니다.

오케스트레이션 / 에이전트 레이어: 여러 LLM 호출을 엮는 외부 통제 흐름. 단일 추론에서는 안 보이지만 시스템 전체 동작을 결정합니다. 멀티 에이전트, 워크플로우, planner-executor 패턴 등.

5. RAG는 어느 레이어인가

RAG는 결국 프롬프트 레이어에 데이터를 공급하는 장치입니다. 검색해 온 청크는 컨텍스트에 토큰으로 들어갑니다. 이 관점이 실용적인 이유는 RAG의 한계가 곧 프롬프트 레이어의 한계(컨텍스트 윈도우, 어텐션의 long-range 약점, 토큰 비용, "lost in the middle")로 환원되기 때문입니다.

다만 RAG를 별개의 레이어로 보는 시각도 일리가 있습니다. 두 가지 이유입니다.

  1. 시스템 구성요소가 훨씬 많음. 임베딩 모델, 벡터 DB, 청킹 전략, 재순위 모델, 쿼리 재작성 등 프롬프트 엔지니어링과 결이 다른 검색 시스템 설계 영역입니다.

  2. 모델과 무관하게 진화함. 베이스 LLM이 그대로여도 RAG 파이프라인 개선만으로 성능이 오릅니다. 독립적 최적화 표면이 있다는 뜻이고, 별도 레이어로 다룰 실용적 이유가 됩니다.

즉 RAG는 개념적으로는 프롬프트 레이어의 하위 범주, 실무적으로는 독립된 서브시스템입니다. "LLM에 정보가 들어가는 경로"를 논할 때는 프롬프트 레이어로 묶고, "이 RAG 파이프라인을 어떻게 개선할까"를 논할 때는 별도 레이어로 다루는 식입니다. 세상에 있는 데이터 → 검색/필터링 → 프롬프트 윈도우 흐름에서 앞단(검색/필터링)이 충분히 복잡해 별도 시스템이 됩니다.

같은 논리로 memory 시스템, MCP, tool use 결과 통합도 "프롬프트 레이어로 흘러드는 데이터 파이프라인"이라는 같은 범주입니다. 이들을 묶어 컨텍스트 공급 레이어(context provisioning layer) 라 부르면, 처음의 2계층이 자연스럽게 3계층으로 확장됩니다.

  1. 모델 레이어 (파라미터)

  2. 컨텍스트 공급 레이어 (RAG, 메모리, 도구, 커넥터)

  3. 프롬프트 레이어 (실제 컨텍스트에 들어간 토큰의 구성과 배치)

여기에 회색지대(PEFT, activation steering)와 아키텍처/디코딩 축까지 보태면, 단순했던 2계층 지도가 더 풍부하게 세분화됩니다.


이 포스트는 Claude(claude.ai)와 정상혁이 함께 작성했습니다.