소개
Hadoop 2.0부터는 클러스터의 리소스 활용률을 높이고, 리소스를 통합 관리하며, 데이터를 공유할 수 있도록 새로운 리소스 관리 방식인 YARN이 도입되었습니다. Hadoop 의사 분산 클러스터를 구축한 환경을 바탕으로, 이 실습에서는 YARN 프레임워크의 아키텍처와 작동 원리, 구성 방법, 개발 및 모니터링 기법을 배웁니다.
이 실습을 진행하려면 Java 프로그래밍에 대한 기본 지식이 필요합니다.
개발 단계에서는 완전한 Java 소스 파일을 제공합니다. 각 파일의 전체 내용을 편집기에 붙여넣은 다음, 클라이언트가 애플리케이션을 제출하는 방식과 ApplicationMaster가 작업 컨테이너를 요청하고 실행하는 방식을 확인합니다.
YARN 아키텍처와 구성 요소
이 단계에서는 YARN 아키텍처와 각 구성 요소의 역할을 살펴봅니다.
Hadoop 0.23에서 MapReduce 2.0(MRv2)의 일부로 도입된 YARN은 Hadoop 클러스터의 리소스 관리와 작업 스케줄링 방식을 크게 바꾸었습니다.
- JobTracker 기능 분리:
MRv2는JobTracker의 기능을 별도의 데몬으로 나눕니다.ResourceManager는 리소스를 관리하고,ApplicationMaster는 작업을 스케줄링하고 모니터링합니다. - 전역 ResourceManager: 각 애플리케이션에는 대응하는
ApplicationMaster가 있습니다. 애플리케이션은 MapReduce 작업일 수도 있고, 작업을 설명하는 DAG일 수도 있습니다. - 데이터 계산 프레임워크:
ResourceManager,Slave,NodeManager가 프레임워크를 구성하며, ResourceManager가 모든 애플리케이션의 리소스를 관리합니다. - ResourceManager 구성 요소:
Scheduler는 용량과 큐 등의 제약 조건에 따라 리소스를 할당하고,ApplicationsManager는 작업 제출과 ApplicationMaster 실행을 처리합니다. - 리소스 할당: 메모리, CPU, 디스크, 네트워크 등의 항목으로 리소스 컨테이너의 요구 사항을 정의합니다.
- NodeManager 역할: NodeManager는 컨테이너의 리소스 사용량을 모니터링하고 ResourceManager와 Scheduler에 보고합니다.
- ApplicationMaster 작업: ApplicationMaster는 Scheduler와 리소스 컨테이너를 협상하고 상태를 추적하며 진행 상황을 모니터링합니다.
다음 그림은 구성 요소 간의 관계를 보여 줍니다.

YARN은 이전 버전과 API 호환성을 유지하므로 기존 MapReduce 작업을 원활하게 실행할 수 있습니다. Hadoop 클러스터에서 리소스를 효율적으로 관리하고 작업을 스케줄링하려면 YARN의 아키텍처와 구성 요소를 이해해야 합니다.
Hadoop 데몬 시작하기
이 단계에서는 YARN 애플리케이션을 실행하는 데 필요한 Hadoop 데몬을 시작합니다.
관련 구성 매개변수와 YARN 애플리케이션 개발 기법을 살펴보기 전에 Hadoop 데몬을 시작해 언제든 사용할 수 있도록 준비합니다.
먼저 데스크톱에서 Xfce 터미널을 두 번 클릭해 연 다음, 아래 명령을 입력해 hadoop 사용자로 전환합니다.
su - hadoop
팁: hadoop 사용자의 비밀번호는 hadoop입니다.
사용자 전환이 완료되면 HDFS와 YARN 프레임워크를 비롯한 Hadoop 관련 데몬을 시작할 수 있습니다.
터미널에 다음 명령을 입력해 데몬을 시작합니다.
/home/hadoop/hadoop/sbin/start-dfs.sh
/home/hadoop/hadoop/sbin/start-yarn.sh
시작이 완료된 후 jps 명령을 실행하면 관련 데몬이 실행 중인지 확인할 수 있습니다.
hadoop:~$ jps
3378 NodeManager
3028 SecondaryNameNode
3717 Jps
2791 DataNode
2648 NameNode
3240 ResourceManager
구성 파일 준비하기
이 단계에서는 Hadoop의 주요 구성 파일 중 하나인 yarn-site.xml을 살펴보고, 이 파일에서 YARN 클러스터의 어떤 설정을 지정할 수 있는지 확인합니다.
구성 파일을 잘못 수정하지 않도록 Hadoop 구성 파일을 다른 디렉터리에 복사한 다음 여는 것이 좋습니다.
먼저 터미널에 다음 명령을 입력해 구성 파일을 저장할 새 디렉터리를 만듭니다.
mkdir /home/hadoop/hadoop_conf
그런 다음 설치 디렉터리에 있는 YARN의 주요 구성 파일 yarn-site.xml을 새로 만든 디렉터리에 복사합니다.
터미널에 다음 명령을 입력해 복사합니다.
cp /home/hadoop/hadoop/etc/hadoop/yarn-site.xml /home/hadoop/hadoop_conf/yarn-site.xml
이어서 vim 편집기로 파일을 열어 내용을 확인합니다.
vim /home/hadoop/hadoop_conf/yarn-site.xml
구성 파일의 작동 방식
이 단계에서는 실행 중인 Hadoop 클러스터에서 사용하는 구성 매개변수를 살펴봅니다.
YARN 프레임워크에는 ResourceManager와 NodeManager라는 두 가지 주요 역할이 있습니다. 따라서 파일의 각 구성 항목은 이 두 구성 요소의 설정을 지정합니다.
이 파일에는 다양한 구성 항목을 설정할 수 있지만, 기본적으로 사용자 지정 구성 항목은 들어 있지 않습니다. 예를 들어 현재 열어 둔 파일에는 앞서 의사 분산 Hadoop 클러스터를 구성할 때 지정한 aux-services 속성만 있습니다. 다음은 해당 설정을 보여 주는 예입니다.
hadoop:~$ cat /home/hadoop/hadoop/etc/hadoop/mapred-site.xml
...
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
이 구성 항목은 NodeManager에서 실행해야 하는 종속 서비스를 설정합니다. 지정된 구성 값은 mapreduce_shuffle이며, 이는 MapReduce 프로그램의 기본값으로 YARN을 사용해야 한다는 의미입니다.
파일에 작성되지 않은 구성 항목은 작동하지 않을까요? 그렇지는 않습니다. 구성 매개변수를 파일에 명시적으로 지정하지 않으면 Hadoop의 YARN 프레임워크는 내부 파일에 저장된 기본값을 읽습니다. yarn-site.xml 파일에 명시적으로 지정한 구성 항목은 모두 기본값을 덮어씁니다. 이를 통해 Hadoop 시스템을 다양한 사용 사례에 맞게 조정할 수 있습니다.
ResourceManager 구성 항목
Hadoop 클러스터에서 리소스와 작업을 효율적으로 관리하고 실행하려면 yarn-site.xml 파일의 ResourceManager 설정을 이해하고 올바르게 구성해야 합니다. 다음은 ResourceManager와 관련된 주요 구성 항목입니다.
yarn.resourcemanager.address: 애플리케이션 제출 및 종료 요청을 받는 클라이언트용 주소입니다. 기본 포트는 8032입니다.yarn.resourcemanager.scheduler.address: 리소스 요청 및 해제를 처리하는 ApplicationMaster용 주소입니다. 기본 포트는 8030입니다.yarn.resourcemanager.resource-tracker.address: 하트비트를 보내고 작업을 가져오는 NodeManager용 주소입니다. 기본 포트는 8031입니다.yarn.resourcemanager.admin.address: 관리 명령을 사용하는 관리자를 위한 주소입니다. 기본 포트는 8033입니다.yarn.resourcemanager.webapp.address: 클러스터 정보를 확인하는 WebUI 주소입니다. 기본 포트는 8088입니다.yarn.resourcemanager.scheduler.class: 스케줄러의 기본 클래스 이름을 지정합니다(예: FIFO, CapacityScheduler, FairScheduler).- 스레드 구성:
yarn.resourcemanager.resource-tracker.client.thread-countyarn.resourcemanager.scheduler.client.thread-count
- 리소스 할당:
yarn.scheduler.minimum-allocation-mbyarn.scheduler.maximum-allocation-mbyarn.scheduler.minimum-allocation-vcoresyarn.scheduler.maximum-allocation-vcores
- NodeManager 관리:
yarn.resourcemanager.nodes.exclude-pathyarn.resourcemanager.nodes.include-path
- 하트비트 구성:
yarn.resourcemanager.nodemanagers.heartbeat-interval-ms
이 매개변수를 구성하면 Hadoop 클러스터에서 ResourceManager의 동작, 리소스 할당, 스레드 처리, NodeManager 관리, 하트비트 간격을 세부적으로 조정할 수 있습니다. 구성 항목을 이해하면 문제를 예방하고 클러스터를 원활하게 운영하는 데 도움이 됩니다.
NodeManager 구성 항목
Hadoop 클러스터에서 리소스와 작업을 효율적으로 관리하려면 yarn-site.xml 파일의 NodeManager 설정을 구성해야 합니다. 다음은 NodeManager와 관련된 주요 구성 항목입니다.
yarn.nodemanager.resource.memory-mb: NodeManager에서 사용할 수 있는 전체 물리 메모리를 지정합니다. 이 값은 YARN이 실행되는 동안 유지됩니다.yarn.nodemanager.vmem-pmem-ratio: 가상 메모리와 물리 메모리 할당 비율을 설정합니다. 기본 비율은2.1입니다.yarn.nodemanager.resource.cpu-vcores: NodeManager에서 사용할 수 있는 전체 가상 CPU 수를 정의합니다. 기본값은8입니다.yarn.nodemanager.local-dirs: NodeManager에서 중간 결과를 저장할 경로입니다. 여러 디렉터리를 지정할 수 있습니다.yarn.nodemanager.log-dirs: NodeManager의 로그 디렉터리 경로입니다. 여러 디렉터리를 지정할 수 있습니다.yarn.nodemanager.log.retain-seconds: NodeManager 로그의 최대 보존 시간입니다. 기본값은 10800초(3시간)입니다.
이 매개변수를 구성하면 Hadoop 클러스터에서 NodeManager의 리소스 할당, 메모리 관리, 디렉터리 경로, 로그 보존 설정을 세부적으로 조정해 성능과 리소스 활용률을 최적화할 수 있습니다. 구성 항목을 이해하면 클러스터를 원활하게 운영하고 작업을 효율적으로 실행하는 데 도움이 됩니다.
구성 항목 조회 및 기본값 참고 자료
YARN과 다른 일반적인 Hadoop 구성 요소에서 사용할 수 있는 모든 구성 항목을 살펴보려면 Apache Hadoop에서 제공하는 기본 구성 파일을 참고하세요. 다음 링크에서 기본 구성 파일을 확인할 수 있습니다.
YARN 구성 항목:
일반 구성 파일:
- core-default.xml (core-site.xml)
- hdfs-default.xml (hdfs-site.xml)
- mapred-default.xml (mapred-site.xml)
기본 구성 파일을 살펴보면 각 구성 항목과 용도를 자세히 확인할 수 있습니다. 이를 통해 Hadoop 아키텍처 설계에서 각 매개변수가 어떤 역할을 하는지 이해할 수 있습니다.
구성 내용을 확인한 후 vim 편집기를 종료하면 Hadoop 구성 설정을 살펴보는 작업이 끝납니다.
프로젝트 디렉터리와 파일 만들기
이 단계에서는 애플리케이션의 소스 파일을 만듭니다. 클라이언트가 ApplicationMaster를 제출하고, ApplicationMaster가 작업 컨테이너 하나를 요청해 인사말을 출력하는 간단하면서도 완전한 YARN 애플리케이션을 빌드합니다.
먼저 프로젝트 디렉터리를 만듭니다. 터미널에 다음 명령을 입력해 디렉터리를 생성합니다.
mkdir /home/hadoop/yarn_app
그런 다음 프로젝트에 소스 코드 파일 두 개를 만듭니다.
첫 번째 파일은 Client.java입니다. 터미널에서 touch 명령을 사용해 파일을 만듭니다.
touch /home/hadoop/yarn_app/Client.java
이어서 ApplicationMaster.java 파일을 만듭니다.
touch /home/hadoop/yarn_app/ApplicationMaster.java
hadoop:~$ tree /home/hadoop/yarn_app/
/home/hadoop/yarn_app/
├── ApplicationMaster.java
└── Client.java
0 directories, 2 files
클라이언트 코드 작성하기
이 단계에서는 애플리케이션을 제출하는 전체 클라이언트 코드를 작성합니다. hadoop 사용자로 계속 작업합니다. 소스 파일을 엽니다.
vim /home/hadoop/yarn_app/Client.java
패키지 선언과 import를 포함해 아래 코드로 파일 전체를 바꿉니다. Vim에서 i를 눌러 입력 모드로 전환하기 전에 :set paste를 입력하고 Enter 키를 누릅니다. 전체 코드를 붙여넣고 모든 줄이 표시될 때까지 기다린 다음 Esc를 누르고 :wq를 입력해 저장하고 종료합니다.
package com.labex.yarn.app;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.yarn.api.ApplicationConstants;
import org.apache.hadoop.yarn.api.records.*;
import org.apache.hadoop.yarn.client.api.YarnClient;
import org.apache.hadoop.yarn.client.api.YarnClientApplication;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.util.ConverterUtils;
public class Client {
public static void main(String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("Usage: Client /absolute/path/to/yarn-app.jar");
}
YarnConfiguration conf = new YarnConfiguration();
YarnClient client = YarnClient.createYarnClient();
client.init(conf);
client.start();
try {
YarnClientApplication application = client.createApplication();
ApplicationSubmissionContext context = application.getApplicationSubmissionContext();
ApplicationId id = context.getApplicationId();
FileSystem fs = FileSystem.get(conf);
Path destination = new Path(fs.getHomeDirectory(), "yarn-app/" + id + "/app.jar");
fs.mkdirs(destination.getParent());
fs.copyFromLocalFile(new Path(args[0]), destination);
FileStatus status = fs.getFileStatus(destination);
LocalResource jar = LocalResource.newInstance(
ConverterUtils.getYarnUrlFromPath(fs.makeQualified(destination)),
LocalResourceType.FILE, LocalResourceVisibility.APPLICATION,
status.getLen(), status.getModificationTime());
Map<String, LocalResource> resources = new HashMap<>();
resources.put("app.jar", jar);
Map<String, String> environment = new HashMap<>();
// The single-node lab uses the same Hadoop installation in every container.
environment.put("CLASSPATH", "./app.jar:" + System.getProperty("java.class.path"));
String command = ApplicationConstants.Environment.JAVA_HOME.$$()
+ "/bin/java -Xmx128m com.labex.yarn.app.ApplicationMaster"
+ " 1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout"
+ " 2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr";
ContainerLaunchContext launch = ContainerLaunchContext.newInstance(
resources, environment, Collections.singletonList(command), null, null, null);
context.setApplicationName("LabEx YARN Hello");
context.setQueue("default");
context.setResource(Resource.newInstance(256, 1));
context.setAMContainerSpec(launch);
client.submitApplication(context);
System.out.println("Application ID: " + id);
long deadline = System.currentTimeMillis() + 180000;
while (System.currentTimeMillis() < deadline) {
ApplicationReport report = client.getApplicationReport(id);
YarnApplicationState state = report.getYarnApplicationState();
if (state == YarnApplicationState.FINISHED
|| state == YarnApplicationState.FAILED
|| state == YarnApplicationState.KILLED) {
System.out.println("Final status: " + report.getFinalApplicationStatus());
if (report.getFinalApplicationStatus() != FinalApplicationStatus.SUCCEEDED) {
throw new IllegalStateException(report.getDiagnostics());
}
return;
}
Thread.sleep(1000);
}
client.killApplication(id);
throw new IllegalStateException("Application timed out after three minutes");
} finally {
client.stop();
}
}
}
YarnClient는 ResourceManager에 연결해 애플리케이션 ID를 가져옵니다. 클라이언트는 JAR을 HDFS에 복사하고 로컬 리소스 app.jar로 지정합니다. 그러면 YARN이 해당 파일을 ApplicationMaster의 작업 디렉터리에 배치할 수 있습니다. 실행 컨텍스트에는 클래스 경로와 Java 명령이 포함되고, 제출 컨텍스트에는 큐와 컨테이너 리소스가 지정됩니다.
클래스 경로에는 이 단일 노드 실습의 컨테이너들이 공유하는 설치된 Hadoop 라이브러리를 사용합니다. Maven이나 Gradle을 통해 별도로 다운로드할 필요가 없습니다. 이 예제는 실습 환경의 비-Kerberos 클러스터를 대상으로 합니다.
애플리케이션을 제출한 후 클라이언트는 애플리케이션 보고서를 폴링하며 종료 상태에 도달할 때까지 기다린 다음 최종 상태를 확인합니다. 애플리케이션이 3분 제한 시간을 넘긴 경우에만 killApplication을 사용합니다. 마지막 단계에서 두 클래스를 컴파일하고 실행합니다.
ApplicationMaster 코드 작성하기
이 단계에서는 ApplicationMaster 전체 코드를 작성합니다. ApplicationMaster는 ResourceManager에 등록하고 컨테이너 하나를 요청한 뒤, NodeManager를 통해 인사말을 출력하고 작업 결과를 보고합니다.
hadoop 사용자로 파일을 엽니다.
vim /home/hadoop/yarn_app/ApplicationMaster.java
아래 코드로 파일 전체를 바꿉니다. i를 누르기 전에 :set paste를 입력하고 Enter 키를 누릅니다. 전체 코드를 붙여넣고 모든 줄이 표시될 때까지 기다린 다음 Esc를 누르고 :wq를 입력해 저장하고 종료합니다.
package com.labex.yarn.app;
import java.util.Collections;
import org.apache.hadoop.yarn.api.ApplicationConstants;
import org.apache.hadoop.yarn.api.protocolrecords.AllocateResponse;
import org.apache.hadoop.yarn.api.records.*;
import org.apache.hadoop.yarn.client.api.AMRMClient;
import org.apache.hadoop.yarn.client.api.NMClient;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
public class ApplicationMaster {
public static void main(String[] args) throws Exception {
YarnConfiguration conf = new YarnConfiguration();
AMRMClient<AMRMClient.ContainerRequest> rm = AMRMClient.createAMRMClient();
NMClient nm = NMClient.createNMClient();
rm.init(conf);
nm.init(conf);
rm.start();
nm.start();
try {
rm.registerApplicationMaster("", 0, "");
AMRMClient.ContainerRequest request = new AMRMClient.ContainerRequest(
Resource.newInstance(256, 1), null, null, Priority.newInstance(0));
rm.addContainerRequest(request);
boolean launched = false;
long deadline = System.currentTimeMillis() + 120000;
while (System.currentTimeMillis() < deadline) {
// Each allocate call also sends a heartbeat to the ResourceManager.
AllocateResponse response = rm.allocate(launched ? 0.5f : 0.0f);
for (Container container : response.getAllocatedContainers()) {
if (launched) {
rm.releaseAssignedContainer(container.getId());
continue;
}
rm.removeContainerRequest(request);
String command = "/bin/echo Hello-from-YARN"
+ " 1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout"
+ " 2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr";
ContainerLaunchContext launch = ContainerLaunchContext.newInstance(
Collections.emptyMap(), Collections.emptyMap(),
Collections.singletonList(command), null, null, null);
nm.startContainer(container, launch);
launched = true;
}
for (ContainerStatus status : response.getCompletedContainersStatuses()) {
if (status.getExitStatus() != 0) {
throw new IllegalStateException(status.getDiagnostics());
}
rm.unregisterApplicationMaster(FinalApplicationStatus.SUCCEEDED,
"Hello container completed", "");
return;
}
Thread.sleep(1000);
}
rm.unregisterApplicationMaster(FinalApplicationStatus.FAILED,
"No successful container within two minutes", "");
throw new IllegalStateException("Container timed out");
} finally {
nm.stop();
rm.stop();
}
}
}
AMRMClient는 ResourceManager 등록과 리소스 요청을 처리합니다. allocate를 호출할 때마다 하트비트를 보내고 새로 할당된 컨테이너와 완료된 컨테이너의 상태를 반환합니다. NMClient는 할당된 컨테이너에서 명령을 실행합니다. 이 동기식 폴링 루프는 정의되지 않은 콜백 클래스나 헬퍼 메서드 없이 예제를 완결된 형태로 유지합니다.
작업은 컨테이너 로그에 Hello-from-YARN을 출력합니다. 종료 상태가 0이면 ApplicationMaster는 SUCCEEDED 상태로 등록을 해제합니다. 그러면 클라이언트가 최종 상태를 보고합니다. ResourceManager는 요청한 256MB를 최소 할당량에 맞춰 올림할 수 있으며, 기다리는 동안 루프는 계속 하트비트를 보냅니다.
애플리케이션 실행 과정
이 단계에서는 작성한 Java 클래스 두 개를 컴파일하고 실행한 다음, ResourceManager 웹 인터페이스에서 애플리케이션을 확인합니다. 터미널에서 hadoop 사용자로 계속 작업합니다.
애플리케이션 컴파일 및 실행
소스 디렉터리로 이동합니다.
cd /home/hadoop/yarn_app
출력 디렉터리를 만듭니다.
mkdir -p classes
VM에 이미 설치된 Hadoop 라이브러리를 사용해 두 소스 파일을 모두 컴파일합니다. 기본 컴파일러가 더 최신 버전이어도 --release 8 옵션을 사용하면 Hadoop의 Java 8 런타임과 호환되는 클래스를 생성합니다.
javac --release 8 -cp "$(/home/hadoop/hadoop/bin/hadoop classpath --glob)" -d classes Client.java ApplicationMaster.java
더 이상 사용되지 않는 API에 관한 안내 메시지는 무시해도 됩니다. 컴파일 오류가 없어야 합니다. 컴파일한 클래스를 JAR 파일로 묶습니다.
jar cf yarn-app.jar -C classes .
클라이언트를 제출하고 출력을 저장합니다. 마지막 인수는 클라이언트가 ApplicationMaster용으로 HDFS에 업로드할 JAR 파일입니다.
set -o pipefail
/home/hadoop/hadoop/bin/yarn jar /home/hadoop/yarn_app/yarn-app.jar com.labex.yarn.app.Client /home/hadoop/yarn_app/yarn-app.jar | tee /home/hadoop/yarn_app/application.log
애플리케이션이 완료될 때까지 기다립니다. Hadoop 로그 메시지 사이에 다음과 같은 결과가 표시되어야 합니다.
Application ID: application_<timestamp>_<sequence>
Final status: SUCCEEDED
실행할 때마다 ID가 달라집니다. 인사말은 작업 컨테이너의 로그에 기록되고, 클라이언트는 애플리케이션 ID와 최종 결과를 출력합니다. 이 실행에서는 별도로 미리 빌드된 MapReduce 예제가 아니라 직접 작성한 클라이언트와 ApplicationMaster를 사용합니다.
애플리케이션 실행 결과 확인하기
데스크톱에서 Firefox를 열고 다음 주소로 이동합니다.
http://localhost:8088
터미널에 출력된 애플리케이션 ID를 기준으로 애플리케이션 목록에서 LabEx YARN Hello를 찾습니다. 상태는 FINISHED, 최종 상태는 SUCCEEDED여야 합니다. 애플리케이션 ID를 클릭해 세부 정보를 확인합니다. ResourceManager는 애플리케이션을 추적하고, ApplicationMaster는 작업 컨테이너를 관리하며, NodeManager는 해당 컨테이너를 실행합니다.
요약
Hadoop 의사 분산 클러스터를 구축한 환경에서 이 실습을 통해 YARN 프레임워크의 아키텍처와 작동 원리, 구성 방법, 개발 및 모니터링 기법을 배웁니다. 실습에는 코드와 구성 파일이 많이 포함되어 있으므로 내용을 꼼꼼히 읽어 보세요.



