🔥알림🔥
① 테디노트 유튜브 - 구경하러 가기!
② LangChain 한국어 튜토리얼 바로가기 👀
③ 랭체인 노트 무료 전자책(wikidocs) 바로가기 🙌

11 분 소요

이번 포스팅에서는 랭체인(LangChain) 을 활용하여 PDF 문서를 로드하고, 문서의 내용을 요약 하는 방법에 대해 알아보겠습니다.

이번 튜토리얼에서는 langchain 의 PyPDFLoader 를 활용한 PDF 문서의 텍스트 데이터를 불러오는 방법에 대해 다룹니다.

또한, 긴 PDF 문서를 쪼개서 요약 및 통합하는 Map-Reduce 방식의 요약 방식에 대해 깊게 다루도록 하겠습니다. Map-Reduce 방식은 LLM의 제한적인 토큰 사이즈를 입력으로 받는 구조적 문제를 해결하는 방법론이기 때문에 비단 요약 뿐만아니라 그 밖에 확장된 테스크에도 적용이 가능합니다.


✔️ (이전글) LangChain 튜토리얼


🌱 환경설정

# 필요한 라이브러리 설치
# !pip install -q openai langchain pypdf unstructured pdf2image pdfminer pypdfium2 pdfminer.six pymupdf pdfplumber amazon-textract-caller
# OPENAI_API
# import os

# os.environ['OPENAI_API_KEY'] = 'OPENAI API KEY 입력'
# 토큰 정보로드를 위한 라이브러리
# 설치: pip install python-dotenv
from dotenv import load_dotenv

# 토큰 정보로드
load_dotenv()
True

🔥 PDF 기반 문서요약

✔️ 문서요약 방식

PDF 의 문서를 요약하는 방식에는 문서의 크기에 따라 2가지 방식으로 나누어 볼 수 있습니다.

  1. Stuff: 전체 문서의 내용을 모두 프롬프트의 입력으로 대입합니다. 전체 문서의 내용이 LLM 모델의 최대토큰 허용 크기보다 작은 경우, 이 방식을 선택할 수 있습니다.

  2. Map-Reduce: 전체 문서의 내용을 쪼개서 여러의 부분 세트로 나눈 뒤, 나눈 내용을 프롬프트의 입력을 나누어 대입합니다. 예를 들어, PDF 문서가 100장으로 구성이 되어 있고, 100장 분량의 내용을 프롬프트의 입력으로 한 번에 넣을 수 없으므로, 20장씩 5개의 세트로 나눈 뒤 5번의 프롬프트 입력 후 결과로 나온 5개의 요약본을 통합하는 작업을 진행합니다.

Stuff 방식은 코드가 간결하지만, 현실적으로 잘 사용하게 되지 않습니다. 일반적인 PDF 문서의 전체 길이가 LLM 모델의 최대토큰 허용 크기보다 큰 경우가 대다수의 경우이기 때문입니다. 따라서, 이번 튜토리얼에서는 Map-Reduce 방식에 대해 자세히 다루며, Stuff 방식은 여기 에서 참고하실 수 있습니다.

📍 흐름

흐름도

위의 흐름도를 보면서 PDF 문서를 요약하는 흐름에 대해서 정리해 보겠습니다.

① 문서 로드

먼저, langchain 의 PDF 문서의 로드를 도와주는 PDF Loader 를 활용하여 문서를 로드합니다. langchain에서는 다양한 PDF Loader 를 제공하며, 대표적인 예시로는 PyPDFLoader, PyMuPDFLoader, UnstructuredPDFLoader 등이 있습니다.

여기서 문서를 로드한다는 의미는 PDF 문서의 내용을 긁어와 String 형태로 가져오는 것을 의미하며 정확하게는 Document 객체안에 page_content 속성으로 로드합니다. PDF 로드시 각종 메타데이터로 함께 로드합니다.

PyPDFLoader 사용하여 PDF를 로드하고, 각 문서가 페이지 내용과 페이지 번호를 포함한 메타데이터를 포함하는 문서의 배열로 만듭니다.

from langchain.document_loaders import PyPDFLoader

# PDF 파일 로드
loader = PyPDFLoader("data/황순원-소나기.pdf")
document = loader.load()
document[0].page_content[:200]
'- 1 -소나기\n황순원\n소년은 개울가에서 소녀를 보자 곧 윤 초시네 증손녀 (曾孫女 )딸이라는 걸 알 수 있었다 . \n소녀는 개울에다 손을 잠그고 물장난을 하고 있는 것이다 . 서울서는 이런 개울물을 보지 \n못하기나 한 듯이.\n벌써 며칠째 소녀는 , 학교에서 돌아오는 길에 물장난이었다 . 그런데 , 어제까지 개울 기슭에\n서 하더니 , 오늘은 징검다리 한가운'

② 문서 분할

문서의 내용이 긴 경우, 문서 전체의 내용을 프롬프트의 입력으로 넣을 수 없으므로 문서를 분할하는 작업을 선행합니다.

문서 분할은 토큰갯수에 따라 분할할 수 있으며, 대표적으로 많이 사용되는 모듈은 CharacterTextSplitter, RecursiveCharacterTextSplitter 등이 있습니다.

아래는 가장 단순한 방법인 CharacterTextSplitter 를 활용하여 문서를 분할한 예시 입니다.

이 방법은 문자(기본적으로 “\n\n”)를 기반으로 분할하며, 문자의 수(chunk_size)로 길이를 측정합니다.

  • 텍스트의 분할 방식: 단일 문자 기준

  • chunk_size 의 측정 방식: 문자의 수로 측정

from langchain.text_splitter import CharacterTextSplitter

# 스플리터 지정
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator="\n\n",  # 분할기준
    chunk_size=3000,   # 사이즈
    chunk_overlap=500, # 중첩 사이즈
)

# 분할 실행
split_docs = text_splitter.split_documents(document)

# 총 분할된 도큐먼트 수
len(split_docs)
7

③ 분할된 각 문서에 대한 요약 실행

분할된 문서에 요약(summarization) 과 같은 테스크를 수행하는 것을 Map, 각 문서의 요약본을 하나로 통합하는 작업을 Reduce 라고 부릅니다. 즉, Map-Reduce 방식은 문서를 분할 - 요약 - 통합 을 수행하게 됩니다.

그 중 Map 단계에서 수행할 프롬프트를 템플릿으로 정의하고 chain 을 생성해 보겠습니다.

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain

# Map 단계에서 처리할 프롬프트 정의
# 분할된 문서에 적용할 프롬프트 내용을 기입합니다.
# 여기서 {pages} 변수에는 분할된 문서가 차례대로 대입되니다.
map_template = """다음은 문서 중 일부 내용입니다
{pages}
이 문서 목록을 기반으로 주요 내용을 요약해 주세요.
답변:"""

# Map 프롬프트 완성
map_prompt = PromptTemplate.from_template(map_template)

# Map에서 수행할 LLMChain 정의
llm = ChatOpenAI(temperature=0, 
                 model_name='gpt-3.5-turbo-16k')
map_chain = LLMChain(llm=llm, prompt=map_prompt)

④ 각 문서의 요약본에 대한 통합

Reduce 단계는 이전 단계인 Map 단계에서 분할된 문서에 대한 요약본을 통합처리 하는 역할을 수행합니다.

Reduce 단계에서는 통합처리시 필요한 프롬프트를 정의합니다. 통합처리시 단순하게 전체 요약본 테스크를 수행할 수 도 있고, 요약본을 토대로 보고서, 이메일, 뉴스레터, 독서감상문 등의 다양한 테스크를 수행하도록 할 수 있습니다.

아래는 통합 요약본을 작성하는 프롬프트 예시입니다.

# Reduce 단계에서 처리할 프롬프트 정의
reduce_template = """다음은 요약의 집합입니다:
{doc_summaries}
이것들을 바탕으로 통합된 요약을 만들어 주세요.
답변:"""

# Reduce 프롬프트 완성
reduce_prompt = PromptTemplate.from_template(reduce_template)

# Reduce에서 수행할 LLMChain 정의
reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)

combine_documents_chain 은 Reduce 작업을 수행하는 체인입니다.

Map 단계에서 완성된 분할된 요약본이 StuffDocumentsChaindoc_summaries 입력으로 주입됩니다.

ReduceDocumentsChain 은 주입된 입력 값을 통합하는 작업을 수행합니다.

from langchain.chains.combine_documents.stuff import StuffDocumentsChain
from langchain.chains import ReduceDocumentsChain

# 문서의 목록을 받아들여, 이를 단일 문자열로 결합하고, 이를 LLMChain에 전달합니다.
combine_documents_chain = StuffDocumentsChain(
    llm_chain=reduce_chain,                
    document_variable_name="doc_summaries" # Reduce 프롬프트에 대입되는 변수
)

# Map 문서를 통합하고 순차적으로 Reduce합니다.
reduce_documents_chain = ReduceDocumentsChain(
    # 호출되는 최종 체인입니다.
    combine_documents_chain=combine_documents_chain,
    # 문서가 `StuffDocumentsChain`의 컨텍스트를 초과하는 경우
    collapse_documents_chain=combine_documents_chain,
    # 문서를 그룹화할 때의 토큰 최대 개수입니다.
    token_max=4000,
)

⑤ 통합체인(Combined Chain) 생성

통합체인 생성 단계에서는 이전에 정의한 Map 체인과 Reduce 체인을 연결하고, MapReduceDocumentsChain 객체를 통해 통합하는 단계입니다.


from langchain.chains import MapReduceDocumentsChain

# 문서들에 체인을 매핑하여 결합하고, 그 다음 결과들을 결합합니다.
map_reduce_chain = MapReduceDocumentsChain(
    # Map 체인
    llm_chain=map_chain,
    # Reduce 체인
    reduce_documents_chain=reduce_documents_chain,
    # 문서를 넣을 llm_chain의 변수 이름(map_template 에 정의된 변수명)
    document_variable_name="pages",
    # 출력에서 매핑 단계의 결과를 반환합니다.
    return_intermediate_steps=False,
)
# Map-Reduce 체인 실행
# 입력: 분할된 도큐먼트(②의 결과물)
result = map_reduce_chain.run(split_docs)
# 요약결과 출력
print(result)
소년은 개울가에서 소녀를 보고 그녀가 윤 초시네 증손녀라는 것을 알게 된다. 소녀는 물장난을 하고 있으며, 소년은 그녀를 지켜보고 있다. 소녀는 물 속을 들여다보고 물을 움켜 낸다. 소년은 소녀가 물 속에서 조약돌을 집어내고 징검다리를 건너간다를 지켜본다. 소녀는 갈꽃을 안고 걸어가며 소년은 그녀를 따라가려고 한다. 소년은 소녀의 그림자가 뵈지 않는 날이 계속되면서 허전함을 느끼게 된다. 소년은 징검다리에 앉아 물 속을 들여다보고 세수를 하며, 자신의 얼굴을 싫어한다. 소년은 물 속에서 얼굴을 움키다가 소녀를 발견하고 달리기를 시작한다. 소년은 메밀밭을 지나가며 코피를 훔치고, 소녀와 함께 산으로 가기로 결정한다. 소녀와 소년은 허수아비를 만나고 놀며 산으로 달려간다. 소년은 독수리가 맴돌아 어지러워한다. 소녀와 소년이 함께 농장을 돌아다니며 여러 꽃과 작물을 보고 먹는다. 소녀는 참외를 맛보고 맵다고 하며 버린다. 소년은 소녀에게 꽃을 건네주고 함께 산으로 가서 더 많은 꽃을 꺾는다. 소녀는 마타리꽃을 좋아하며 소년에게 꽃을 버리지 말라고 한다. 소녀와 소년은 산마루에서 휴식을 취하며 주위의 조용함과 가을 햇살을 즐긴다. 소녀는 칡덩굴의 꽃을 보고 등나무 아래에서 놀던 동무들을 생각한다. 소년과 소녀가 함께 놀다가 비가 오기 시작한다. 소년은 소녀를 비에서 보호하기 위해 원두막으로 이동한다. 소년은 비를 맞으면서 소녀를 보호하고, 소녀는 소년의 도움을 받아 편안하게 있다. 소년과 소녀가 비에 젖은 상황에서 만남하고, 소녀가 소년을 안아주고 소년은 소녀에게 마음이 끌린다. 그러나 소녀는 개울가에서 소년을 기다리지 않고 집을 나가게 된다. 소년은 소녀네가 이사해 오기 전에 벌써 어른들의 이야기를 들어서, 윤 초시 손자가 서울에서 사업에 실패해 고향에 돌아오지 않을 수 없게 되었다는 걸 알고 있었다. 소년은 이사 가는 것을 싫어했고, 소녀의 까만 눈에 쓸쓸한 빛이 떠돌았다. 소년은 소녀가 이사를 간다는 말을 수없이 되뇌었고, 호두밭으로 가서 호두를 수확했다. 소년은 소녀에게 호두를 맛보여야 한다는 생각에 호두를 깨고 어루만졌다. 소년은 소녀에게 개울가로 나와 달라는 말을 못해둔 것을 후회했다. 소년의 아버지는 서당골 윤 초시 댁에 가기 위해 닭을 안고 나갔다. 소년은 아버지가 어디 가는지 물어보았고, 아버지는 서당골 윤 초시 댁에 가는 것을 말했다. 소년은 외양간으로 가서 쇠잔등을 갈겼고, 개울물은 날로 여물어갔다. 소년은 소녀네가 양평읍으로 이사 간다는 것을 알게 되었다. 주인공의 아버지가 마을에 돌아왔다. 아버지는 집을 팔고 악상을 당하는 상황이다. 어머니는 증손이 하나뿐인데 자식복이 없어서 걱정한다. 주인공은 앓는 중인데 약이 효과가 없어서 대가가 끊겼다. 주인공의 계집애는 잔망스럽지 않은 모습이다. 주인공은 죽을 때 자기 입던 옷을 입혀서 묻어달라고 말했다.

🔥 전체코드

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain

from langchain.chains.combine_documents.stuff import StuffDocumentsChain
from langchain.chains import ReduceDocumentsChain, MapReduceDocumentsChain
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter

# ========== ① 문서로드 ========== #

# PDF 파일 로드
loader = PyPDFLoader("data/황순원-소나기.pdf")
document = loader.load()
document[0].page_content[:200]

# ========== ② 문서분할 ========== #

# 스플리터 지정
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator="\n\n",  # 분할기준
    chunk_size=3000,   # 사이즈
    chunk_overlap=500, # 중첩 사이즈
)

# 분할 실행
split_docs = text_splitter.split_documents(document)
# 총 분할된 도큐먼트 수
print(f'총 분할된 도큐먼트 수: {len(split_docs)}')

# ========== ③ Map 단계 ========== #

# Map 단계에서 처리할 프롬프트 정의
# 분할된 문서에 적용할 프롬프트 내용을 기입합니다.
# 여기서 {pages} 변수에는 분할된 문서가 차례대로 대입되니다.
map_template = """다음은 문서 중 일부 내용입니다
{pages}
이 문서 목록을 기반으로 주요 내용을 요약해 주세요.
답변:"""

# Map 프롬프트 완성
map_prompt = PromptTemplate.from_template(map_template)

# Map에서 수행할 LLMChain 정의
llm = ChatOpenAI(temperature=0, 
                 model_name='gpt-3.5-turbo-16k')
map_chain = LLMChain(llm=llm, prompt=map_prompt)

# ========== ④ Reduce 단계 ========== #

# Reduce 단계에서 처리할 프롬프트 정의
reduce_template = """다음은 요약의 집합입니다:
{doc_summaries}
이것들을 바탕으로 통합된 요약을 만들어 주세요.
답변:"""

# Reduce 프롬프트 완성
reduce_prompt = PromptTemplate.from_template(reduce_template)

# Reduce에서 수행할 LLMChain 정의
reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)

# 문서의 목록을 받아들여, 이를 단일 문자열로 결합하고, 이를 LLMChain에 전달합니다.
combine_documents_chain = StuffDocumentsChain(
    llm_chain=reduce_chain,                
    document_variable_name="doc_summaries" # Reduce 프롬프트에 대입되는 변수
)

# Map 문서를 통합하고 순차적으로 Reduce합니다.
reduce_documents_chain = ReduceDocumentsChain(
    # 호출되는 최종 체인입니다.
    combine_documents_chain=combine_documents_chain,
    # 문서가 `StuffDocumentsChain`의 컨텍스트를 초과하는 경우
    collapse_documents_chain=combine_documents_chain,
    # 문서를 그룹화할 때의 토큰 최대 개수입니다.
    token_max=4000,
)

# ========== ⑤ Map-Reduce 통합단계 ========== #

# 문서들에 체인을 매핑하여 결합하고, 그 다음 결과들을 결합합니다.
map_reduce_chain = MapReduceDocumentsChain(
    # Map 체인
    llm_chain=map_chain,
    # Reduce 체인
    reduce_documents_chain=reduce_documents_chain,
    # 문서를 넣을 llm_chain의 변수 이름(map_template 에 정의된 변수명)
    document_variable_name="pages",
    # 출력에서 매핑 단계의 결과를 반환합니다.
    return_intermediate_steps=False,
)

# ========== ⑥ 실행 결과 ========== #

# Map-Reduce 체인 실행
# 입력: 분할된 도큐먼트(②의 결과물)
result = map_reduce_chain.run(split_docs)
# 요약결과 출력
print(result)
총 분할된 도큐먼트 수: 7
소년은 개울가에서 소녀를 만나게 되고, 그녀가 윤 초시네 증손녀라는 것을 알게 된다. 소녀는 물장난을 하고 있는데, 소년은 그녀를 지켜보고 있다. 소녀는 물 속을 들여다보고 물을 움켜 낸다. 소년은 소녀가 물 속에서 조약돌을 집어내고 징검다리를 건너간다는 것을 보고 따라간다. 소녀는 갈꽃을 안고 갈꽃머리로 걸어간다. 소년은 소녀의 그림자가 뵈지 않는 날이 계속되면서 허전함을 느끼고 주머니 속 조약돌을 가지고 다닌다. 소년은 소녀가 물장난을 하던 징검다리에 앉아 물 속을 들여다보고 세수를 한다, 그러나 자신의 얼굴이 검게 비치는 것을 싫어한다. 소년은 물 속에서 얼굴을 움키다가 소녀를 발견하고 달리기를 시작한다. 소년은 메밀밭을 지나가며 메밀꽃 냄새를 맡고 코피를 훔치며 달린다. 개울가에서 소녀를 만나고 함께 징검다리를 건넌다. 소녀는 소년에게 산 너머로 가보자고 제안한다. 소년과 소녀는 논 사잇길을 걷고 허수아비를 만나서 놀며 달린다. 소년은 독수리가 맴돌아 어지러워한다. 소녀와 소년이 함께 농장을 돌아다니며 여러 꽃과 작물을 보고 먹는다. 소녀는 참외를 맛보고 맵다고 하며 버린다. 소년은 소녀에게 꽃을 건네주고 함께 산으로 가서 더 많은 꽃을 꺾는다. 소녀는 마타리꽃을 좋아하며 소년에게 꽃을 버리지 말라고 한다. 소녀와 소년은 산마루에서 휴식을 취하며 주위의 조용함과 가을 햇살을 즐긴다. 소녀는 칡덩굴의 꽃을 보며 등나무 아래에서 놀던 동무들을 생각한다. 소년과 소녀가 함께 놀다가 비가 오기 시작한다. 소년은 소녀를 비에서 보호하기 위해 원두막으로 이동한다. 소년은 비를 맞으면서 소녀를 보호하고, 소녀는 소년의 도움을 받아 편안하게 있다. 소년은 비를 그을 수 있는 곳을 찾기 위해 수숫단을 이용한다. 소년과 소녀는 비를 맞지 않고 안전하게 지내는 방법을 찾아내는 이야기이다. 소년과 소녀가 비에 젖은 상황에서 만나게 되고, 소녀가 소년을 안아주고 소년은 소녀에게 마음이 끌린다. 하지만 소녀는 개울가에서 소년을 기다리지 않고 집을 나가게 된다. 소년은 계속해서 소녀를 찾아다니며, 다시 만나고 대추를 주고받는다. 그러나 소녀는 집을 나가게 되었다. 소년은 소녀네가 이사해 오기 전에 윤 초시 손자가 사업에 실패해 고향에 돌아오지 않을 수 없게 되었다는 것을 알고 있었다. 소년은 이사를 가는 것을 싫어했고, 소녀의 눈에는 쓸쓸한 빛이 있었다. 소년은 소녀가 이사를 가는 것을 안타깝게 생각했지만, 호두를 얻기 위해 할아버지네 호두밭으로 갔다. 소년은 호두를 얻기 위해 나무를 올라가고 가지를 내리쳤다. 그리고 호두를 주머니에 담아 소녀에게 맛보여야 한다는 생각이 들었다. 소년은 아버지가 어디로 가는지 물었고, 아버지는 서당골 윤 초시 댁에 가서 제삿상에 놓으라고 했다. 소년은 외양간으로 가서 쇠잔등을 갈겼고, 개울물은 날로 여물어갔다. 소년은 소녀네가 양평읍으로 이사간다는 것을 알게 되었고, 내일 소녀네를 보러 가야 한다는 생각이 들었다. 주인공의 아버지가 마을에 돌아왔다. 아버지는 집을 팔고 악상을 당하는 상황이다. 어머니는 증손이 하나뿐인데 자식복이 없어서 걱정한다. 주인공은 앓는 중인데 약이 효과가 없어서 대가가 끊겼다. 주인공의 계집애는 잔망스럽지 않은 모습이다. 주인공은 죽을 때 자기 입던 옷을 그대로 입혀 달라고 말했다.

이전 단계의 결과물인 문서의 통합요약본을 바탕으로 다음과 같은 추가 Task를 수행할 수 있습니다. 아래는 독서감상문 작성의 예시입니다.

# 질문 템플릿 형식 정의
template = """다음은 소설에 대한 요약본입니다. 
다음의 내용을 독서 감상문 형식으로 작성해 주세요. 

독서 감상문의 형식은 다음과 같습니다:

처음: 글을 읽게 된 동기나 책을 처음 대했을 때의 느낌을 쓰고, 글의 종류나 지은이 소개, 주인
공이나 주제의 소개
중간: 주인공의 행동과 나의 행동을 비교해 보기도 하고, 글의 내용을 평가해 보기도 하며, 글
속에서 발견한 주제나 의미가 우리 사회에 어떻게 작용할 것인가를 씁니다. 그리고 글을 읽으면서 받은
감동을 쓰기도 합니다.
끝: 글의 내용을 정리하며, 교훈을 적어두기도 한다. 그리고 끝글은 지루하지 않도록 산뜻하게

{text}

답변:
"""

# 템플릿 완성
prompt = PromptTemplate(template=template, input_variables=['text'])

# 연결된 체인(Chain)객체 생성
llm_chain = LLMChain(prompt=prompt, llm=llm)

output = llm_chain.run(text=result)
print(output)
처음에 이 소설을 읽게 된 동기는 소설의 제목과 내용이 궁금해서였습니다. 소년과 소녀의 이야기가 어떻게 전개될지, 그리고 그들의 사랑이 어떤 결말을 맞이할지 궁금했습니다. 이 소설은 김동인 작가의 작품으로, 소년과 소녀의 만남과 이별을 그린 이야기입니다.

이 소설은 주인공인 소년과 소녀의 만남과 이별을 중심으로 이야기가 전개됩니다. 소년은 개울가에서 소녀를 만나게 되고, 그녀가 윤 초시네 증손녀라는 것을 알게 됩니다. 소녀는 물장난을 하고 있는데, 소년은 그녀를 지켜보고 있습니다. 소녀는 물 속을 들여다보고 물을 움켜 낸다는 것을 보고 소년은 따라가기 시작합니다. 그리고 소녀와 함께 징검다리를 건너갑니다. 이후에도 소녀와 소년은 함께 다양한 경험을 하며 시간을 보냅니다. 그러나 소녀는 어느 날 갑자기 집을 나가게 되고, 소년은 그녀를 찾아다니며 다시 만나기 위해 노력합니다. 하지만 소녀는 이사를 가게 되고, 소년은 그녀를 다시 만날 수 없게 됩니다.

이 소설을 읽으면서 주인공인 소년과 나를 비교해보았습니다. 소년은 소녀를 지켜보고 따라가며 그녀와 함께 다양한 경험을 하려고 노력합니다. 그리고 소녀를 찾아다니며 다시 만나기 위해 노력하는 모습이 인상적이었습니다. 나는 소년과 비슷하게 소녀와 함께 시간을 보내고 싶다는 생각이 들었습니다. 또한, 소년과 소녀의 이야기를 평가해보면, 이야기의 전개가 자연스럽고 흥미로웠습니다. 소년과 소녀의 만남과 이별이 감동적으로 그려져 있었고, 이야기를 읽으면서 여러 감정을 느낄 수 있었습니다.

이 소설에서 발견한 주제나 의미는 사랑과 이별의 아픔이었습니다. 소년과 소녀는 짧은 시간 동안 함께 지내면서 서로에게 강한 감정을 느끼게 됩니다. 그리고 이별을 맞이하게 되면서 상처를 받고 아픔을 겪게 됩니다. 이 주제와 의미는 우리 사회에도 적용될 수 있습니다. 사랑과 이별은 모두 인간의 삶에서 흔히 겪는 경험이며, 이를 통해 우리는 성장하고 강해질 수 있습니다.

이 소설을 읽으면서 받은 감동은 소년과 소녀의 사랑과 이별의 아픔을 공감하며 함께 느낄 수 있었습니다. 소년과 소녀의 이야기는 마치 내 이야기처럼 느껴져서 더욱 감동적이었습니다. 또한, 소년과 소녀의 모습을 통해 사랑과 이별의 아픔을 깊이 이해할 수 있었습니다.

이 소설을 마무리하며, 사랑과 이별의 아픔을 느끼게 해준 이야기였습니다. 소년과 소녀의 이야기를 통해 사랑과 이별의 아픔을 공감하고 이해할 수 있었습니다. 이 소설은 마치 한편의 영화를 보는 것처럼 흥미로웠고, 주인공들의 감정을 공감하며 함께 느낄 수 있었습니다. 이 소설을 읽으면서 많은 감동을 받았고, 사랑과 이별에 대해 다시 한번 생각해볼 수 있는 기회였습니다.

댓글남기기