Parent Document Retriever

계층적 문서 구조를 활용한 RAG 최적화

문서 청킹의 딜레마(정확한 임베딩 vs 충분한 맥락)를 해결하기 위해 ParentDocumentRetriever가 작은 청크로 검색하고 큰 청크를 반환하는 계층적 검색 전략을 다룬다.

AI
RAG
LangChain
Agent
저자

Kwangmin Kim

공개

2024년 05월 06일

1 Parent Document Retriever 개요

1.1 기본 개념

  • Child Document: Document, Sub Document, Split, Split Document, Chunk 등으로도 불리는 작은 단위의 문서 조각
  • Parent Document: Child Document들이 생성된 원본 문서 또는 더 큰 단위의 상위 문서

1.2 ParentDocumentRetriever의 필요성

1.2.1 계층적 문서 구조의 활용

전통적인 RAG 시스템에서는 Child Document만을 사용하여 검색과 응답 생성을 수행한다. 하지만 특정 상황에서는 원본 문서(Parent Document)의 맥락이 필요한 경우가 있다.

1.2.2 향상된 RAG 아키텍처

단순히 검색된 Child Document를 LLM에 직접 입력하는 것보다, 다음과 같은 고도화된 프로세스를 통해 더 나은 결과를 얻을 수 있다:

  1. Child Document로 정확한 검색 수행
  2. 해당 Child Document가 포함된 Parent Document에서 앞뒤 맥락 추출
  3. 추출된 맥락을 별도 LLM으로 실시간 요약
  4. 요약된 결과를 최종 RAG LLM에 입력

이러한 접근법은 할루시네이션 감소답변 품질 향상에 크게 기여한다.

1.2.3 ParentDocumentRetriever의 역할

ParentDocumentRetriever는 이러한 계층적 문서 구조를 효과적으로 활용할 수 있도록 설계된 특화된 Retriever이다. 원본 문서의 맥락이 필요한 모든 상황에서 사용할 수 있다.

2 RAG의 청킹 딜레마

2.1 문제: 상충되는 두 가지 요구사항

RAG 시스템에서 문서를 청크(chunk)로 분할할 때 다음의 트레이드오프 (정확한 임베딩 vs 넓은 범위의 맥락)에 직면하게 된다.

요구사항 1: 작은 청크 (정확한 임베딩)
- 목적: 임베딩이 청크의 의미를 정확하게 표현
- 이유: 긴 텍스트의 임베딩은 다양한 주제가 섞여 의미가 희석된다
- 장점: 검색 정확도 향상
- 단점: 맥락 손실

요구사항 2: 큰 청크 (충분한 맥락)
- 목적: LLM이 답변 생성 시 필요한 맥락 제공
- 이유: 작은 청크만으로는 질문에 답하기 어려울 수 있다
- 장점: 풍부한 맥락 정보
- 단점: 임베딩 품질 저하

청킹 딜레마 예시

청크 크기 임베딩 품질 맥락 유지 검색 정확도 LLM 답변 품질
작음 (200자) 높음 낮음 높음 낮음
중간 (500자) 중간 중간 중간 중간
큼 (1000자) 낮음 높음 낮음 높음

2.2 해결책: ParentDocumentRetriever

핵심 아이디어: “작게 검색하고, 크게 반환한다”

ParentDocumentRetriever는 계층적 문서 구조를 활용하여 이 딜레마를 해결한다.

작동 원리

  1. 문서 분할 (Splitting)
    • 원본 문서를 큰 청크(Parent)로 분할
    • 큰 청크를 다시 작은 청크(Child)로 분할
  2. 인덱싱 (Indexing)
    • 작은 청크(Child)만 벡터 DB에 임베딩
    • 큰 청크(Parent)는 별도 문서 저장소(Docstore)에 보관
    • Child와 Parent 간 ID로 연결
  3. 검색 (Retrieval)
    • 작은 청크로 유사도 검색 (정확한 매칭)
    • 검색된 작은 청크의 Parent ID 확인
    • 큰 청크(Parent)를 LLM에 전달

계층 구조 예시

원본 문서 (5000자) ← 문서 DB에 저장
├── Parent Chunk 1 (1000자) ← 문서 DB 또는 벡터 DB에 저장
│   ├── Child Chunk 1-1 (200자) ← 벡터 DB에 저장
│   ├── Child Chunk 1-2 (200자) ← 벡터 DB에 저장
│   └── Child Chunk 1-3 (200자) ← 벡터 DB에 저장
├── Parent Chunk 2 (1000자)
│   ├── Child Chunk 2-1 (200자)
│   └── Child Chunk 2-2 (200자)

검색 프로세스

사용자 질문: "Word2Vec이란?"
         ↓
벡터 검색 (Child Chunk에서)
         ↓
Child Chunk 1-2 발견 (유사도: 0.95)
         ↓
Parent ID 확인 → Parent Chunk 1
         ↓
Parent Chunk 1 (1000자) 반환
         ↓
LLM에 충분한 맥락 제공

2.3 핵심 장점

1. 검색 정확도 향상
- 작은 청크로 검색하므로 임베딩이 명확한 의미 표현
- 노이즈가 적어 관련 문서를 정확히 찾음

2. 맥락 보존
- 큰 청크를 반환하므로 충분한 맥락 제공
- LLM이 더 정확한 답변 생성 가능

3. 유연한 구조
- Parent 크기를 조절하여 맥락 범위 조정 가능
- Child 크기를 조절하여 검색 정밀도 조정 가능

4. 저장 공간 효율
- Child만 벡터화하므로 벡터 DB 크기 절감
- Parent는 일반 문서 저장소에 보관

3 환경 설정

ParentDocumentRetriever 실습을 위한 환경을 설정한다. 여러 개의 텍스트 파일을 로드하기 위해 TextLoader 객체를 생성하고 데이터를 로드한다.

# API 키를 환경변수로 관리하기 위한 설정 파일
from dotenv import load_dotenv

# API 키 정보 로드
load_dotenv()
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("CH10-Retriever")
from langchain.storage import InMemoryStore
from langchain_community.document_loaders import TextLoader
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.retrievers import ParentDocumentRetriever
loaders = [
    # 파일을 로드합니다.
    TextLoader("./data/appendix-keywords.txt"),
]

docs = [] # embeding을 위해 docs에 text 데이터 저장
for loader in loaders:
    # 로더를 사용하여 문서를 로드하고 docs 리스트에 추가합니다.
    docs.extend(loader.load())
  • 예시는 .txt 파일로 로드되어 있기 때문에 파일 내에 있는 전체 내용이 parent document가 된다.
    • len(docs)를 출력하면 parent document의 수 1이 출력된다.
  • 단, .pdf의 경우 업로드 시 페이지 단위로 문서가 업로드되어 parent document는 PDF의 page가 된다.
    • len(docs)를 출력하면 parent document의 수는 페이지수가 출력된다.

4 전체 문서 검색

4.1 전략: Child만 분할, Parent는 원본

parent_splitter를 지정하지 않고 child_splitter만 사용하는 기본적인 방식을 살펴본다.

동작 방식
- Child: 200자 청크로 분할 → 벡터 DB 저장
- Parent: 원본 문서 그대로 → Docstore 저장
- 검색: Child로 찾고 → 원본 문서 전체 반환

장점: 전체 문서 맥락 유지
단점: 문서가 너무 길면 LLM 컨텍스트 낭비

4.2 코드 구현

# 자식 분할기를 생성: 200 token 단위 chunk 생성
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)

# DB를 생성
vectorstore = Chroma(
    collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)

store = InMemoryStore()

# Retriever 를 생성
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store, # Parent Document (원본 Document) 저장 
    child_splitter=child_splitter,
)

retriever.add_documents(docs, ids=None) 함수로 문서목록을 추가한다.

  • idsNone이면 자동으로 생성된다.
  • add_to_docstore=False로 설정 시 document를 중복으로 추가하지 않는다. 단, 중복을 체크하기 위한 ids 값이 필수적으로 요구된다.
# 문서를 검색기에 추가합니다. docs는 문서 목록이고, ids는 문서의 고유 식별자 목록입니다.
retriever.add_documents(docs, ids=None, add_to_docstore=True)

이 코드는 추가된 문서 수만큼 키를 반환한다.

  • store 객체의 yield_keys() 메서드를 호출하여 반환된 키(key) 값들을 리스트로 변환한다.
# 저장소의 모든 키를 리스트로 반환합니다.
list(store.yield_keys())
['c2a89a0f-a690-4915-af68-2ea432fb6e51']
  • 1개가 출력되었다. 이는 parent document가 1개 있다는 의미이다. 하지만 chunk는 여러 개다.
  • 이제 벡터 스토어의 검색 기능을 테스트해본다.
  • 작은 청크(chunk)들을 저장하고 있기 때문에, 검색 결과로 작은 청크들이 반환되는 것을 확인할 수 있다.
  • vectorstore 객체의 similarity_search 메서드를 사용하여 Word2Vec와 관련성이 높은 문서를 찾아내는 유사도 검색을 수행한다.
# 유사도 검색을 수행합니다.
sub_docs = vectorstore.similarity_search("Word2Vec")
  • sub_docs[0].page_content, 즉 chunk를 출력한다.
# sub_docs 리스트의 첫 번째 요소의 page_content 속성을 출력합니다.
print(sub_docs[0].page_content)
정의: Word2Vec은 단어를 벡터 공간에 매핑하여 단어 간의 의미적 관계를 나타내는 자연어 처리 기술입니다. 이는 단어의 문맥적 유사성을 기반으로 벡터를 생성합니다.
예시: Word2Vec 모델에서 "왕"과 "여왕"은 서로 가까운 위치에 벡터로 표현됩니다.
연관키워드: 자연어 처리, 임베딩, 의미론적 유사성
  • 이제 전체 retriever에서 검색해본다.
  • 이 과정에서는 작은 청크(chunk)들이 위치한 문서를 반환하기 때문에 상대적으로 큰 문서들이 반환된다.
  • retriever 객체의 invoke() 메서드를 사용하여 쿼리와 관련된 문서를 검색한다.
# 전체 문서 (parent document)를 검색하여 가져옵니다.
retrieved_docs = retriever.invoke("Word2Vec")
  • Parent Document (retrieved_docs[0])의 일부 내용을 출력한다.
# 검색된 문서의 문서의 페이지 내용의 길이를 출력합니다.
print(
    f"문서의 길이: {len(retrieved_docs[0].page_content)}",
    end="\n\n=====================\n\n",
)

# 문서의 일부를 출력합니다.
print(retrieved_docs[0].page_content[2000:2500])
문서의 길이: 5733

=====================

 컴퓨팅을 도입하여 데이터 저장과 처리를 혁신하는 것은 디지털 변환의 예입니다.
연관키워드: 혁신, 기술, 비즈니스 모델

Crawling

정의: 크롤링은 자동화된 방식으로 웹 페이지를 방문하여 데이터를 수집하는 과정입니다. 이는 검색 엔진 최적화나 데이터 분석에 자주 사용됩니다.
예시: 구글 검색 엔진이 인터넷 상의 웹사이트를 방문하여 콘텐츠를 수집하고 인덱싱하는 것이 크롤링입니다.
연관키워드: 데이터 수집, 웹 스크래핑, 검색 엔진

Word2Vec

정의: Word2Vec은 단어를 벡터 공간에 매핑하여 단어 간의 의미적 관계를 나타내는 자연어 처리 기술입니다. 이는 단어의 문맥적 유사성을 기반으로 벡터를 생성합니다.
예시: Word2Vec 모델에서 "왕"과 "여왕"은 서로 가까운 위치에 벡터로 표현됩니다.
연관키워드: 자연어 처리, 임베딩, 의미론적 유사성
LLM (Large Language Model)

정의: LLM은 대규모의 텍스트 데이터로 훈련된 큰 규모의 언어 모델을

결과 분석

항목 Child 검색 Parent 반환
크기 200자 내외 5733자 (원본 전체)
내용 Word2Vec 정의만 전체 키워드 사전
장점 정확한 매칭 풍부한 맥락
단점 맥락 부족 불필요한 정보 포함

문제점

  • 원본 문서가 5733자로 너무 길어 LLM에 전달 시 비효율적이다.
  • Parent 크기를 조절하여 이를 해결한다.

5 계층적 청킹 (권장 방식)

5.1 전략: Parent와 Child 모두 분할

  • 이전 방식의 문제점(맥락부족 & 불필요한 정보 포함)을 해결하기 위해 2단계 청킹을 적용한다.
  • 즉, parent의 크기를 조절하는 방식이다.

분할 전략
1. 1단계: 원본 문서 → Parent Chunk (1000자)
2. 2단계: Parent Chunk → Child Chunk (200자)
3. 저장: Child만 벡터화, Parent는 Docstore
4. 검색: Child로 찾고 → Parent 반환 (1000자)

크기 비교

방식 Child 크기 Parent 크기 Parent 개수 장점
실습 1 200자 5733자 (원본) 1개 전체 맥락
실습 2 200자 1000자 7개 적절한 맥락 + 효율성

왜 1000자인가?
- 대부분의 LLM은 입력으로 4-8K 토큰 컨텍스트 사용
- 1000자 ≈ 250 토큰 (영문 기준)
- 4-5개 청크 = 1000-1250 토큰 (적정 범위)
- 충분한 맥락 + 효율적인 토큰 사용

5.2 코드 구현

  • RecursiveCharacterTextSplitter를 사용하여 부모 문서와 자식 문서를 생성한다.
    • 부모 문서는 chunk_size가 1000으로 설정되어 있다
    • 자식 문서는 chunk_size가 200으로 설정되어 있으며, 부모 문서보다 작은 크기로 생성된다
# 부모 문서를 생성하는 데 사용되는 텍스트 분할기입니다.
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000)
# 자식 문서를 생성하는 데 사용되는 텍스트 분할기입니다.
# 부모보다 작은 문서를 생성해야 합니다.
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# 자식 청크를 인덱싱하는 데 사용할 벡터 저장소입니다.
vectorstore = Chroma(
    collection_name="split_parents", embedding_function=OpenAIEmbeddings()
)
# 부모 문서의 저장 계층입니다.
store = InMemoryStore()

ParentDocumentRetriever를 초기화하는 코드이다.

  • vectorstore 매개변수는 문서 벡터를 저장하는 벡터 저장소를 지정한다.
  • docstore 매개변수는 문서 데이터를 저장하는 문서 저장소를 지정한다.
  • child_splitter 매개변수는 하위 문서를 분할하는 데 사용되는 문서 분할기를 지정한다.
  • parent_splitter 매개변수는 상위 문서를 분할하는 데 사용되는 문서 분할기를 지정한다.

ParentDocumentRetriever는 계층적 문서 구조를 처리하며, 상위 문서와 하위 문서를 별도로 분할하고 저장한다. 이를 통해 검색 시 상위 문서와 하위 문서를 효과적으로 활용할 수 있다.

retriever = ParentDocumentRetriever(
    # 벡터 저장소를 지정한다.
    vectorstore=vectorstore,
    # 문서 저장소를 지정한다.
    docstore=store,
    # 하위 문서 분할기를 지정한다.
    child_splitter=child_splitter,
    # 상위 문서 분할기를 지정한다.
    parent_splitter=parent_splitter, # 1000글자 단위 spliter
)

retriever 객체에 docs를 추가한다. retriever가 검색할 수 있는 문서 집합에 새로운 문서들을 추가하는 역할을 한다.

retriever.add_documents(docs)  # 문서를 retriever에 추가한다.

이제 문서의 수가 훨씬 더 많아진 것을 볼 수 있다. 이는 더 큰 크기의 상위 청크(chunk)들이다.

# 저장소에서 키를 생성하고 리스트로 변환한 후 길이를 반환합니다.
len(list(store.yield_keys()))
7
  • 1000자 단위로 쪼개진 상위 chunk가 7개라는 의미이다

기본 벡터 저장소가 여전히 작은 청크를 검색하는지 확인해본다.

vectorstore 객체의 similarity_search 메서드를 사용하여 유사도 검색을 수행한다.

# 유사도 검색을 수행합니다.
sub_docs = vectorstore.similarity_search("Word2Vec")
# sub_docs 리스트의 첫 번째 요소의 page_content 속성을 출력합니다.
print(sub_docs[0].page_content)
정의: Word2Vec은 단어를 벡터 공간에 매핑하여 단어 간의 의미적 관계를 나타내는 자연어 처리 기술입니다. 이는 단어의 문맥적 유사성을 기반으로 벡터를 생성합니다.
예시: Word2Vec 모델에서 "왕"과 "여왕"은 서로 가까운 위치에 벡터로 표현됩니다.
연관키워드: 자연어 처리, 임베딩, 의미론적 유사성

이번에는 retriever 객체의 invoke() 메서드를 사용하여 문서를 검색한다.

# 문서를 검색하여 가져옵니다.
retrieved_docs = retriever.invoke("Word2Vec")

# 검색된 문서의 첫 번째 문서의 페이지 내용을 반환한다.
print(retrieved_docs[0].page_content)
정의: 트랜스포머는 자연어 처리에서 사용되는 딥러닝 모델의 한 유형으로, 주로 번역, 요약, 텍스트 생성 등에 사용됩니다. 이는 Attention 메커니즘을 기반으로 합니다.
예시: 구글 번역기는 트랜스포머 모델을 사용하여 다양한 언어 간의 번역을 수행합니다.
연관키워드: 딥러닝, 자연어 처리, Attention

HuggingFace

정의: HuggingFace는 자연어 처리를 위한 다양한 사전 훈련된 모델과 도구를 제공하는 라이브러리입니다. 이는 연구자와 개발자들이 쉽게 NLP 작업을 수행할 수 있도록 돕습니다.
예시: HuggingFace의 Transformers 라이브러리를 사용하여 감정 분석, 텍스트 생성 등의 작업을 수행할 수 있습니다.
연관키워드: 자연어 처리, 딥러닝, 라이브러리

Digital Transformation

정의: 디지털 변환은 기술을 활용하여 기업의 서비스, 문화, 운영을 혁신하는 과정입니다. 이는 비즈니스 모델을 개선하고 디지털 기술을 통해 경쟁력을 높이는 데 중점을 둡니다.
예시: 기업이 클라우드 컴퓨팅을 도입하여 데이터 저장과 처리를 혁신하는 것은 디지털 변환의 예입니다.
연관키워드: 혁신, 기술, 비즈니스 모델

Crawling

정의: 크롤링은 자동화된 방식으로 웹 페이지를 방문하여 데이터를 수집하는 과정입니다. 이는 검색 엔진 최적화나 데이터 분석에 자주 사용됩니다.
예시: 구글 검색 엔진이 인터넷 상의 웹사이트를 방문하여 콘텐츠를 수집하고 인덱싱하는 것이 크롤링입니다.
연관키워드: 데이터 수집, 웹 스크래핑, 검색 엔진

Word2Vec

정의: Word2Vec은 단어를 벡터 공간에 매핑하여 단어 간의 의미적 관계를 나타내는 자연어 처리 기술입니다. 이는 단어의 문맥적 유사성을 기반으로 벡터를 생성합니다.
예시: Word2Vec 모델에서 "왕"과 "여왕"은 서로 가까운 위치에 벡터로 표현됩니다.
연관키워드: 자연어 처리, 임베딩, 의미론적 유사성
LLM (Large Language Model)

6 두 가지 방식 비교

6.1 검색 결과 분석

실습 1: Parent = 원본 문서
- Parent 크기: 5733자 (원본 전체)
- Parent 개수: 1개
- 반환 내용: 전체 키워드 사전
- 문제점: 불필요한 정보 과다 포함

실습 2: Parent = 1000자 청크
- Parent 크기: 약 1000자
- Parent 개수: 7개
- 반환 내용: Word2Vec 관련 키워드 및 문맥 집합
- 장점: 관련성 높은 정보만 포함

6.2 성능 비교

지표 실습 1 (원본) 실습 2 (1000자) 개선
검색 정확도 높음 높음 동일
맥락 충분성 과다 적절 개선
토큰 효율성 낮음 높음 5배 개선
답변 품질 중간 높음 개선
처리 속도 느림 빠름 개선

7 실무 적용 가이드

7.1 청크 크기 설정 전략

Child Chunk 크기 (검색용)

크기 권장 상황 장점 단점
100-200자 정확한 검색 필요 정밀한 매칭 맥락 부족
200-400자 일반적인 경우 균형 잡힘 -
400-600자 넓은 검색 필요 다양한 매칭 정확도 저하

Parent Chunk 크기 (반환용)

크기 권장 상황 토큰 (영문 기준) 예상 비용
500-1000자 짧은 답변 125-250 낮음
1000-2000자 일반적인 경우 250-500 중간
2000-4000자 긴 맥락 필요 500-1000 높음

7.2 ParentDocumentRetriever 사용 권장 시나리오

  1. 긴 문서 처리
    • 논문, 보고서, 매뉴얼 등
    • 전체 문서는 너무 크지만 맥락은 필요한 경우
  2. 토큰 비용 절감
    • LLM API 비용이 중요한 경우
    • 대량의 문서 처리 시
  3. 계층적 정보 구조
    • 섹션별로 나뉜 문서
    • 챕터/절 구조가 있는 경우

7.3 일반 Retriever 사용 권장

  1. 짧은 문서
    • 이미 400자 이하의 작은 단위
    • 추가 분할이 불필요한 경우
  2. 단순 구조
    • FAQ, 짧은 뉴스 기사
    • 계층 구조가 없는 경우
  3. 전체 맥락 필수
    • 문서 전체를 봐야 하는 경우
    • 코드 전체, 계약서 전문 등

7.4 최적화 팁

  1. 동적 크기 조정
# 문서 길이에 따라 Parent 크기 조정
if avg_doc_length < 2000:
    parent_size = 1000
elif avg_doc_length < 5000:
    parent_size = 2000
else:
    parent_size = 3000
  1. Overlap 설정
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200  # 20% 오버랩으로 맥락 연결
)
  1. 메타데이터 활용
# 섹션 정보 추가로 검색 정확도 향상
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    add_start_index=True  # 위치 정보 추가
)

8 핵심 요약

8.1 ParentDocumentRetriever의 핵심 원리

“작게 검색하고, 크게 반환한다”

  1. 정확한 검색: 작은 청크로 정밀한 임베딩과 유사도 검색 수행
  2. 충분한 맥락: 큰 청크를 반환하여 LLM에 필요한 맥락 정보 제공
  3. 계층적 구조: 두 단계 청킹으로 효율성과 정확도를 동시에 달성

8.2 주요 장점과 해결 문제

해결하는 문제
- 기존 RAG의 청킹 딜레마 (정확한 임베딩 vs 충분한 맥락)
- 토큰 비용 과다 문제
- 검색 정확도와 답변 품질 사이의 트레이드오프

제공하는 장점
1. 검색 정확도 향상: 작은 청크로 정밀한 매칭
2. 맥락 정보 보존: 큰 청크 반환으로 풍부한 맥락
3. 토큰 비용 절감: 적절한 크기로 효율성 증대
4. 유연한 구조: 도메인별 크기 조절 가능

8.3 실무 적용 가이드

권장 설정
- Child 청크: 200-400자 (검색 정밀도 최적화)
- Parent 청크: 1000-2000자 (맥락과 효율성 균형)
- Overlap: 10-20% (맥락 연결성 향상)

적용 시나리오
- ✅ 긴 문서 처리 (논문, 매뉴얼, 보고서)
- ✅ 토큰 비용이 중요한 대용량 처리
- ✅ 계층적 정보 구조가 있는 문서
- ❌ 이미 작은 단위의 문서 (FAQ, 짧은 기사)

최적화 전략
- 문서 특성에 따른 동적 크기 조정
- 메타데이터 활용으로 검색 정확도 향상
- 도메인별 청킹 전략 수립
- A/B 테스트를 통한 최적 크기 탐색

ParentDocumentRetriever는 RAG 시스템의 성능을 크게 향상시킬 수 있는 강력한 도구이다. 특히 긴 문서를 다루는 환경에서 검색 정확도와 답변 품질을 동시에 개선할 수 있는 효과적인 솔루션이다.

Subscribe

Enjoy this blog? Get notified of new posts by email: