개발자가 되는 길

[번역]시작하는 이들을 위한 컨테이너, VM, 그리고 도커에 대한 이야기

예나부기 2021. 9. 28.

*이 글은 

Preethi Kasireddy

 A Beginner-Friendly Introduction to Containers, VMs and Docker를 번역한 글입니다. 모든 저작권과 권리는 Preethi에게 있습니다.
*This article is a translated version of 

Preethi Kasireddy

’s article: A Beginner-Friendly Introduction to Containers, VMs and Docker. All rights and credits back to her.

*최대한 이해하기 쉽도록 곳곳에 의역이 들어간 점 양해 부탁드립니다.
*도움이 되셨다면 Preethi의 원글에 clap 한번씩 부탁드립니다 :)

 

Source: https://flipboard.com/topic/container

당신이 프로그래머이거나 IT 관련 일을 한다면, 도커(Docker)에 대해서 들어본 적이 있을 것이다. 애플리케이션을 포장하고, 옮기고, “컨테이너”들 안에서 실행할 수 있게 하는 유용한 툴- 최근 도커가 개발자와 시스템 관리자를 불문하고 관심을 끌어모으고 있는 것을 생각하면 당연한 일이다. 구글이나 VMware, 아마존 같은 거대한 회사들도 관련 서비스를 만들며 이러한 트렌드에 불을 붙이고 있다.

당신이 지금 바로 도커를 사용할 일이 있고 없고를 떠나서, “컨테이너”의 핵심 개념들, 그고 버추얼 머신(VM)과의 차이에 대해서는 알아 둘 필요가 있다고 생각한다. 인터넷에 도커 사용법에 대한 훌륭한 가이드들은 많이 있지만, 개인적으로 아직 컨테이너란 무엇인가에 대한 초보자들을 위한 설명은 많이 보지 못했다. 이 글이 그 대안 중 하나가 되었으면 하는 바람이다 :)

그렇다면 VM과 컨테이너가 정확히 무엇인지부터 알아보도록 하자.

“컨테이너”와 “VM”은 무엇인가?

컨테이너와 VM은 비슷한 목적을 가지고 있다: 애플리케이션과 그 의존성들(dependencies)을 독립된 단위로 묶어 격리, 어디서든 실행 가능하게 하는 것.

또한, 컨테이너와 VM 모두 물리적 하드웨어의 필요성을 제거하여 컴퓨터의 자원을 에너지 면에서도, 그리고 비용 면에서도 보다 더 효율적으로 사용할 수 있게 해준다.

컨테이너와 VM의 결정적인 차이는 설계의 접근법에 있다. 조금 더 자세히 살펴보도록 하자.

Virtual Machines

VM은 기본적으로 컴퓨터의 에뮬레이션으로, 프로그램을 실제 컴퓨터처럼 실행한다. VM들은 “하이퍼바이저”를 통해 물리적 기계(machine) 위에서 돌아간다. 한편 하이퍼바이저는 호스트 머신이나 베어 메탈(bare metal *역: 소프트웨어가 전혀 담겨있지 않은 하드웨어)” 위에서 실행된다.

용어들이 어려우니, 조금 더 살펴보도록 하자.

하이퍼바이저란 VM이 실행되는 소프트웨어나 펌웨어, 혹은 하드웨어를 뜻한다. 하이퍼바이저들은 호스트 머신이라 불리는 물리적 기계 위에서 돌아가고, 호스트 머신은 VM에 램과 CPU 등의 자원을 제공한다.

이 자원들을 VM들이 분배 받아서 사용하는데, 사용자가 원하는 대로 할당할 수도 있다. 즉, 하나의 VM이 자원을 더 많이 필요로 하는 애플리케이션을 실행시키고 있다면, 같은 호스트 머신에서 돌고 있는 다른 VM들보다 더 많은 자원을 할당할 수도 있다는 뜻이다.

호스트 머신 위에서 하이퍼바이저를 이용하여 돌아가는 VM을 보통 게스트 머신(“guest machine”) 이라고도 부른다. 이 게스트 머신은 보통 그 애플리케이션을 실행하기 위한 모든 것(시스템 바이너리, 라이브러리 등)과 애플리케이션을 포함하며, 가상화된 네트워크 어댑터, 저장소, CPU등의 모든 하드웨어 스택도 포함한다. 즉, 자신 안에 또 하나의 온전한 게스트 운영체제를 가지고 있는 것이다.

내부에서 보면, 게스트 머신은 자신만의 자원을 할당받은, 하나의 독립된 유닛처럼 작동한다. 반면 외부의 시점에서 보면 이것은 호스트 머신의 자원을 다른 VM들과 공유하는 또 하나의 VM임을 알 수 있다.

이처럼 가상 머신은 hosted 하이퍼바이저(hosted hypervisor, *역: 호스트를 가진 하이퍼바이저. 타입 2 하이퍼바이저라고도 하는 듯 합니다), 혹은 bare-metal 하이퍼바이저(*역: 타입 1 하이퍼바이저라고도 불립니다)에 의해서 실행된다. 이 둘에는 중요한 차이가 있다.

첫 번째로, 호스트를 가진 하이퍼바이저는 호스트 머신의 운영체제 위에서 돌아간다. 예를 들어, OSX가 돌아가고 있는 컴퓨터의 VM(VirtualBox나 VMware Workstation 8 등)이 이것의 예시이다. VM은 직접 하드웨어에 접근할 수 없으며, 호스트 OS를 거쳐야 한다(위 예시의 경우에는 맥의 OSX).

Hosted 하이퍼바이저의 이점은 기본 하드웨어가 비교적 덜 중요하다는 것이다. 하이퍼바이저가 아닌 호스트의 OS가 하드웨어 드라이버들을 책임지기 때문에, “하드웨어 호환성”이 더 높다고 볼 수 있다. 다른 한편으로는 하드웨어와 하이퍼바이저 사이에 있는 이 부가적인 단계가 더 많은 리소스 오버헤드를 발생시키며, VM의 퍼포먼스를 떨어뜨릴 수 있다.

Bare metal 하이퍼바이저 환경은 위와 같은 퍼포먼스 이슈를 호스트머신의 하드웨어에 직접 설치, 실행함으로써 해결한다. 직접 하드웨어와 접촉하기 때문에 따로 호스트 OS가 필요없다. 이 경우에는 하이퍼바이저가 호스트 머신의 서버에 OS로서 처음 설치되는 것이 된다.

Hosted 하이퍼바이저와는 달리, bare-metal 하이퍼바이저는 자신만의 디바이스 드라이버를 가지고 입출력, 프로세싱, OS 관련 컴포넌트들과 직접 교류하여 처리한다. 이렇게 해서 더 나은 퍼포먼스, 확장성, 그리고 안정성을 가지게 된다. 대신 하이퍼바이저에는 제한된 숫자의 디바이스 드라이브가 설치 될 수 있으므로, 그만큼 하드웨어 호환성이 제한될 수 있다.

하이퍼바이저에 대해 여기까지 읽고 나면, 여러분은 왜 이 “하이퍼바이저”라는 추가적인 단계가 VM과 호스트 머신 사이에 필요한지가 궁금해졌을 것이다.

이는 하이퍼바이저가 VM들이 각자 자신의 가상 운영체제, 즉 게스트 운영체제를 실행하고 관리할 수 있게끔 돕는 중요한 역할과 더불어, 호스트 머신들이 자원들을 VM들에게 분배할 수 있도록 도와주는 일을 하기 때문이다.

Virtual Machine Diagram

위 그림에서 볼 수 있듯이, VM은 가상 하드웨어, 커널(OS 등), 그리고 유저 공간(space)를 포함한다.

컨테이너

하드웨어의 가상화를 제공하는 VM과는 달리, 컨테이너는 유저 공간(user spcae)의 추상화를 통해 운영체제 레벨의 가상화를 제공한다. 이게 무슨 뜻인지는 컨테이너라는 용어를 좀 더 분석해보면 알 수 있다.

비슷한 목적을 가지고 있는 만큼, 컨테이너는 VM과 흡사해 보인다. 컨테이너도 VM처럼 프로세싱을 위한 별도의 공간(private space), 루트 권한, 사설 네트워크, IP 주소, 커스텀 라우트 / iptable 규칙, 파일 시스템 마운트 등의 기능을 갖추고 있다.

하지만 컨테이너는 호스트 시스템의 커널을 다른 컨테이너들과 공유한다는 점에서 크게 차이가 난다.

Container Diagram

이 그림에서 볼 수 있듯이, 컨테이너는 유저 공간만을 포함하고, VM에는 포함되는 커널이나 버추얼 하드웨어가 포함되지 않는다. 여러개의 컨테이너가 하나의 호스트 머신에서 돌아갈 수 있도록 각 컨테이너는 자신만의 격리된 유저 공간을 가지고 있다. 즉, 운영체제 단계의 아키텍처를 모든 컨테이너가 공유하고 있는 것이다. 처음부터 새로 생성되는 부분은 bins와 libs 뿐이며, 이것이 컨테이너가 한결 가벼워질 수 있는 이유이다.

도커는 그럼 무슨 역할을 하는가?

도커는 리눅스 컨테이너를 기반으로 하는 오픈소스 프로젝트다. 네임스페이스, 컨트롤 그룹과 같은 리눅스 커널 기능을 이용해서 운영체제 위에 컨테이너들을 생성하는 것이다.

컨테이너는 새로운 개념이 아니다. 구글은 자신들만의 컨테이너 개념을 몇 년째 이용해 왔다. Solaris Zones, BSD jails, LXC를 비롯한 다른 리눅스 컨테이너 기술들도 이미 오랫동안 존재해 왔다.

그렇다면 왜 갑자기 도커가 이목을 끌기 시작한 것일까?

  1. 간편한 사용법: 도커는 개발자, 시스템 관리자, 아키텍트 등 누구든지 컨테이너의 이점을 이용해서 손쉽게 이동성 있는 애플리케이션을 생성, 테스트 할 수 있도록 만들어졌다. 누구든 애플리케이션을 자신의 랩탑에서 간단히 패키징하고, 공용 클라우드, 개인용 클라우드, 혹은 bear metal에서 보존된 상태의 애플리케이션을 실행해 볼 수 있도록 해주는 것이다. “한번의 빌드로 어디에서든 실행하자”라는 마법의 주문인 것이다.
  2. 속도: 도커는 매우 가볍고 빠르다. 컨테이너는 커널에서 돌아가는 샌드박스화된 환경일 뿐이어서, 더 적은 자원을 소비한다. 매번 하나의 완전한 가상 운영체제를 부팅해야하는 VM과는 달리, 도커 컨테이너는 몇 초면 실행시킬 수 있다.
  3. 도커 허브(Hub): 도커 허브는 “도커 이미지들의 앱스토어” 같은 일종의 도커 생태계로, 유저들이 자유롭게 사용할 수 있다. 도커 허브에는 커뮤니티에 의해 생성되어 바로 사용 가능한 수만개의 이미지들이 있고, 아주 쉽게 필요한 이미지를 찾을 수 있을 뿐만 아니라 약간의 수정 혹은 수정 없이 바로 사용할 수 있다.
  4. 모듈성(modularity)과 확장성(scalability): 도커는 어플리케이션의 기능들을 각각의 컨테이너들로 쉽게 분리할 수 있도록 해준다. 예를 들어, 여러분이 Postgres 데이터베이스를 하나의 컨테이너에서 돌리고 다른 하나에서는 Redis 서버를, 그리고 또 하나에서는 Node.js앱을 실행시키고 있다고 가정해보자. 도커를 이용하면 이 컨테이너들을 연결하여 애플리케이션을 만들 수 있다. 각각의 컴포넌트에 대해서 따로 업데이트를 하거나 스케일링을 하기 쉬워지는 것이다.

마지막으로, 도커 고래가 싫다 할 사람이 어디 있겠는가? ;)

Source: https://www.docker.com/docker-birthday

도커 기본 개념들

도커에 대한 큰 그림은 살펴보았으니, 도커의 기반이 되는 부분들을 하나하나 살펴보도록 하자.

도커 엔진

도커가 실행되는 레이어. 컨테이너, 이미지, 빌드 등을 관리하는 경량 런타임이자 도구로서, 리눅스 시스템에서 돌아가고 다음과 같이 구성되어 있다.

  1. 호스트에서 돌아가는 도커 데몬(daemon)
  2. 도커 데몬과 통신하여 명령어를 실행하는 도커 클라이언트
  3. 도커 데몬과 원격으로 교류하는 REST API

도커 클라이언트

도커 클라이언트는 도커의 엔드 유저(end-user *역: 마지막 단계의 사용자)인 여러분이 사용하는 부분이다. 도커의 UI라고 보면 된다. 예를 들어, 이렇게 실행하면

 

여러분은 도커 클라이언트에게 명령어를 전달하고, 도커 클라이언트는 받은 명령을 도커 데몬에게로 전달한다.

도커 데몬

도커 데몬은 빌드, 실행, 배포(distribute) 등을 비롯하여 사용자가 도커 클라이언트에게 보낸 명령어를 실행한다. 도커 데몬은 호스트 머신에서 돌아가지만, 유저들이 도커 데몬과 직접적으로 접하는 경우는 없다. 도커 클라이언트도 호스트 머신에서 실행되기는 하지만, 필수적으로 그렇지는 않다. 도커 클라이언트는 다른 머신에 있는 동안에도 호스트 머신에서 돌아가는 도커 데몬과 소통하는 것이 가능하다.

도커파일(Dockerfile)

도커파일은 도커 이미지를 생성할 지시 사항들을 작성하는 파일이다. 예시로는 다음과 같은 항목들이 있을 수 있다.

  • RUN apt-get y install some-package: 패키지 설치
  • EXPOSE 8000: 포트 노출
  • ENV ANT_HOME /usr/local/apache-ant: 환경변수 전달

일단 도커파일을 설정하고 나면, 도커 빌드 커맨드를 통해 이미지를 생성할 수 있다. 다음은 도커파일의 예시이다.

 

샘플 도커파일

도커 이미지

이미지들은 Docker file의 지시사항을 기준으로 생성하는 read-only 템플릿이다. 이미지는 패키지화된 애플리케이션과 의존성들과 더불어 실행시에 어떤 프로세스를 실행할지를 정의한다.

도커 이미지는 도커파일을 통해서 생성된다. 도커파일 안의 각 지시 항목은 이미지에 새로운 “레이어”로서 이미지에 추가된다. 각 레이어는 이미지 파일 시스템을 구성하고, 새로 추가 되거나 기존 레이어를 교체한다. 도커의 가벼우면서도 강력한 구조의 핵심에는 이 레이어 방식이 있다. 도커는 이것을 실현하기 위해 Union File System을 이용한다.

Union File Systems

도커는 Union File System을 이용해서 이미지를 생성한다. Union File System은 스택으로 쌓을 수 있는 파일 시스템이라고 볼 수 있다. 서로 다른 파일 시스템(브랜치(branch)라고도 알려져있다)의 파일과 디렉토리가 서로 겹쳐져서 하나의 파일 시스템을 구성하게 된다는 뜻이다.

이렇게 중첩되어 있는 브랜치들 중에서 같은 경로를 가지고 있는 디렉토리들의 내용은 하나의 합쳐진 디렉토리로 인식된다. 즉, 각각의 레이어에 따라 별도의 버전을 따로 만들지 않아도 되는 것이다.

그 대신 동일한 자원으로 향하는 포인터를 배정받고, 특정 레이어가 수정된다면 그 때 로컬 카피를 생성, 수정함으로서 원본은 그대로 유지한다.

이렇게 해서 파일시스템이 실제로는 수정될 수 없는데도 writable인것(*역: 수정 가능한 것) 처럼 보여줄 수 있는 것이다. (한마디로 “copy-on-write”(*역: 수정이 일어나면 카피를 생성하는)시스템이다)

레이어드 시스템에는 두가지의 이점이 있다

  1. duplication-free(*역:이중화 회피) : 레이어들로 이루어져 있다는 것은 새로운 컨테이너를 시동할때마다 전체 파일을 모두 복사하지 않아도 된다는 뜻과 같다. 즉, 도커 컨테이너들의 인스턴스화를 적은 비용으로 매우 빠르게 할 수 있도록 해 준다.
  2. Layer segregation(*역: 레이어 분리) : 하나의 이미지를 변경하면 도커가 그 수정사항을 해당 레이어에만 반영하도록 하기 때문에 변경사항이 더 빠르게 반영된다.

Volumes

볼륨은 컨테이너의 ‘데이터’ 부분으로, 컨테이너 생성시 초기화된다. 볼륨은 컨테이너의 데이터를 유지하고 공유할 수 있도록 한다. 데이터 볼륨은 기본 Union File System과는 별도로, 일반 디렉토리/파일들로 호스트의 파일 시스템에 존재한다. 즉, 컨테이너를 파괴, 수정, 재생성 하더라도 데이터 볼륨은 그대로 유지된다. 볼륨을 수정하고 싶을 때는 직접 수정해야한다 (데이터 볼륨은 여러 대의 컨테이너가 공유, 재사용 할 수 있도 있다).

도커 컨테이너

도커 컨테이너는, 이미 위에서 설명한대로 애플리케이션과 해당 애플리케이션이 실행되기 위해 필요로 하는 모든 것들을 하나로 묶은 것이다. 이는 운영체제, 애플리케이션 코드. 런타임, 시스템도구, 시스템 라이브러리 등을 모두 포함한다. 도커 컨테이너는 도커 이미지로부터 만들어지는데, 이미지들은 read-only이므로 도커는 read-write 파일 시스템을 이 read-only 파일 시스템 위에 얹어서 컨테이너를 생성한다.

Source: Docker

또한, 도커는 컨테이너가 로컬호스트와 통신할 수 있도록 네트워크 인터페이스도 생성하고, IP 주소를 컨테이너에 붙인 후, 개발자가 이미지 정의 단계에서 지정한 프로세스에 따라 애플리케이션을 실행한다.

컨테이너를 성공적으로 생성했다면, 이제 부가적인 수정 없이 어느 환경에서든 애플리케이션을 실행할 수 있다.

컨테이너 조금 더 알아보기

휴! 정말 많은 것들을 살펴보았다. 나는 항상 컨테이너가 어떻게 설계된 것인지, 특히 추상적 인프라 경계도 없이 어떻게 구현된 것인지 궁금했다. 많은 문서를 보고 나서야 이해를 하게 되었고, 여러분들에게 설명을 시도해 보려고 한다 :)

“컨테이너”는 사실상 추상적 개념으로, 몇 가지 기능의 집합체가 컨테이너처럼 작동하는 것을 상상할 수 있게끔 만들어진 개념이다. 어떤 기능들이 여기에 포함되는지 빠르게 살펴보자

1) 네임스페이스(Namespaces)

네임스페이스는 컨테이너들이 배경 리눅스 시스템에 대해 인식/접근할 수 있는 것에 제한을 건다. 컨테이너를 실행시키면, 도커가 해당 컨테이너가 사용할 네임스페이스를 생성한다.

도커가 사용하는 네임스페이스에는 몇가지 종류가 있다. 다음은 몇몇 예시이다:

a. NET: 네트워크 스택에 대해 해당 컨테이너가 볼 수 있는 것들을 지정한다. (네트워크 디바이스, IP 주소, 라우팅 테이블, /proc/net 디렉토리, 포트 번호 등)

b. PID: Process ID: PID는 Process ID의 줄임말이다. 어떤 프로세스가 현재 시스템에서 실행되고 있는지 확인하기 위해 ps aux 명령어를 실행시켜본 적이 있다면, PID라는 항목을 본 적이 있을 것이다. PID 네임스페이스는 컨테이너가 보고 교류할 수 있는 프로세스 항목들을 지정한다. 이는 “모든 프로세스들의 조상” 인 PID 1도 포함한다.

c. MNT: 컨테이너에게 시스템의 mount들(*역: 기존의 파일시스템에 부착된 추가 파일 시스템)에 대한 접근 범위를 지정한다. 즉, 서로 다른 마운트 네임스페이스는 파일시스템 계층에 대하여 다른 시야를 갖는다.

d. UTS: UTS는 UNIX Timesharing System의 줄임말이다. 프로세스가 시스템 식별자(system identifier)들을 인식하게 해주는 역할을 하는데, 즉 컨테이너가 호스트 시스템이나 다른 컨테이너들로부터 독립된 호스트명과 NIS 도메인명을 가질 수 있도록 한다.

e. IPC: InterProcess Communication이 줄임말이다. IPC 네임스페이스는 각각의 컨테이너에서 돌아가는 프로세스들의 IPC 리소스들을 격리시켜주는 역할을 한다.

f. USER: 이 네임스페이스는 각각의 컨테이너의 유저들을 서로에게서 분리시키는 역할을 한다. 컨테이너들이 uid(user ID)와 gid(group ID)에 대해 서로 다른 영역을 볼 수 있게 해준다. 결과적으로, 프로세스의 uid와 gid는 유저 네임스의 안과 밖이 서로 다를 수 있다. 이는 하나의 프로세스가 컨테이너 내부의 루트 권한을 희생하지 않고 컨테이너 외부에는 권한이 없는 유저를 가질 수 있도록 해 준다.

도커는 이 네임스페이스들을 사용해서 컨테이너를 격리하고, 새롭게 생성할 수 있도록 해준다. 다음 소개할 기능은 컨트롤 그룹이다.

2) 컨트롤 그룹

컨트롤 그룹(cgroup이라고도 한다)은 각 프로세스들이 어떻게 자원(CPU, 메모리, 디스크 입출력, 네트워크 등)을 사용할 것인지 정하는 리눅스 커널 기능이다. cgroup은 도커 컨테이너가 필요한 자원만 사용하도록 제어한다. 다르게 말하면 필요한 경우에는 컨테이너가 사용할 수 있는 자원의 한도도 지정한다. cgroup은 하나의 컨테이너가 과도하게 많은 자원을 사용하여 시스템이 중단되는 경우도 미리 방지한다.

마지막으로, union file system이 도커가 사용하는 또 다른 기능이다.

3) Isolated Union File System

위의 도커 이미지 부분에서 언급했던 Union File System을 참조하면 된다 :)

이 세가지가 도커 컨테이너를 구성하는 전부이다 (물론 진짜 어려운 부분은 여러 컴포넌트 사이의 상효 작용 등을 어떻게 구현하는가 등의 부분이기는 하다).

도커의 미래: 도커와 VM의 공존

도커가 분명 많은 주목을 받고 있기는 하지만, VM을 위협하는 존재가 될 것 같지는 않다. 컨테이너는 계속해서 더 많은 곳에서 사용되겠지만, VM이 더 필요한 케이스도 많이 있다.

예를 들어, 여러 대의 서버에 여러 개의 애플리케이션을 돌려야 한다면, VM을 쓰는 편이 나을 것이다. 한편 한개의 애플리케이션을 기반으로 한 여러 개의 카피들을 실행해야 할 경우에는 도커가 더 훌륭한 선택지일 것이다.

또한, 컨테이너들이 애플리케이션을 여러개의 기능 위주의 조각들로 나눌 수 있게 해준다는 것은 관리해야 할 부분이 많아진다는 뜻이기도 하다.

보안 문제 역시 도커 컨테이너를 사용할 때 우려되는 부분 중에 하나이다. 컨테이너들은 같은 커널을 공유함으로, 컨테이너 사이에 장벽이 약한 편이다. 또, VM이 호스트 하이퍼바이저에게 hypercall만을 보낼 수 있는 반면, 도커 컨테이너는 호스트 커널에 syscall만을 할 수 있어서 공격에 노출되는 부분이 더 넓어지게 된다. 보안이 굉장히 중요한 경우에는 추상화된 하드웨어로써 완전히 격리되는 VM을 선택하여 서로 영향을 줄 확률을 낮추는 것이 적합할 수 있다.

물론, 보안과 관리 문제들은 컨테이너들이 프로덕션 단계를 여러번 거치고 유저들의 관찰이 쌓이면서 더 개선될 것이다. 컨테이너냐 VM이냐에 대한 이슈는 당장은 매일같이 이 도구들을 사용하는 데브옵스들에게 맡겨두어도 좋을 듯 하다.

마치며

여러분들이 이제는 언젠가 프로젝트에서 필요할지도 모르는 도커에 대한 지식들을 갖추게 되었기를 바란다.

언제나 그랬듯이, 내가 무언가 실수를 했거나 도움이 될 수 있는 방법이 있다면 댓글로 남겨주면 감사하겠다 :)

 

출처 : https://medium.com/@jwyeom63/%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%9D%B4%EB%93%A4%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-vm-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8F%84%EC%BB%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-3a04c000cb5c

댓글