AndreaSmalltalkLecture:QnA 02

From 흡혈양파의 인터넷工房
Revision as of 07:15, 5 December 2013 by Onionmixer (talk | contribs) (안드레아::질문과답-2 페이지 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
질문과 답변-2

질문과 답변-2

안녕하세요, 한 재윤입니다. 선생님의 스몰토크 강의를 열심히 청강중인 착한 학생입니다. 선생님께 칭찬받으려고 이렇게 질문도 합니다. 질문드립니다.

안녕하십니까? 우선 제 글을 읽어주셔서 감사합니다. 글을 쓰는 제 입장으로는 한 분이라도 제 글을 읽어주시고, 그로써 무엇인가 얻어 가시는 것이 있다면 정말 기쁜 일이 아닐 수 없습니다. 여태껏 강좌를 진행하면서 질문을 받아본 게 처음이군요. 여하튼 제가 알고있는데까지 대답해 드리겠습니다.


첫번째! 쇠마디 지시에서 중간에 끼는 and: 나 to: 등은그 자체로서 어떤 지시인가요? 지시라면 도대체 뭐라고 대답하는거죠? 그게 아니면 isKindOf: 와 and: 가 합쳐져서 하나의 지시인가요? 이런 지시를 정의할때는 어떻게 하는지 무지궁금하네요.


쇠마디 지시(keyword message)에 대해서 물으셨습니다. 일단 쇠마디 지시가 들어있는 글토막을 살펴봅시다.

5 between: 1 and: 10   true


를 보면 정수 객체 "5"에게 "between: 1 and: 10"이라는 지시가 전달된 것입니다. 일단 위의 글토막을 아래처럼 잘라서 생각해 봅시다.

받는이                     지시    
______  __________________________________________
5       between:        1       and:    10
        ~~~~~~~~        ~~~~~   ~~~~    ~~~~~   
           |            인자      |     인자    
           +---------[열쇠말]-----+


위의 경우 "between:"과 "and:"는 각각 "1"과 "10"이라는 인자의 쓰임을 꾸며주는 열쇠말(keyword)입니다. 그리고 이 두 개의 열쇠말이 모여서 하나의 길표(selector)가 됩니다. 즉 위의 경우 "between:and:"는 두 개의 열쇠말을 가지는 길표인 것입니다.


쇠마디 지시는 이처럼 하나 이상의 열쇠말로 이루어져 있습니다. 이는 C++나 객체 파스칼에는 없는 Smalltalk만의 특징으로, 여러 개의 열쇠말이 모여서 하나의 길표를 이루는 예라고 하겠습니다.


정리하면, 쇠마디 지시는 한 개 이상의 열쇠말로 이루어지고, 열쇠말은 바로 다음에 나오는 인자를 꾸며줍니다. 따라서 "between:" 이나 "and:" 는 그 자체로써 지시가 아니고, 쇠마디 지시를 구성하는 요소입니다. 따라서 쇠마디 지시는 여러 개의 열쇠말과 인자가 모여서 하나의 지시를 꾸미고, 그 지시가 객체에 전달되어 결과를 남깁니다. 그래서 Smalltalk 에서 쇠마디 지시를 나타낼 때에도 "between:and:"처럼 해당 길표에 쓰인 열쇠말을 한데 모아서 표시합니다.


그리고 "isKindOf:" 와 "between:and:" 는 서로 다른 지시이므로, "isKind Of:" 와 "and:" 는 아무런 관계도 없습니다. 아마 "between:and" 와 "isKindOf:" 를 함께 생각하시면서 혼동을 하신 것이 아닌가 생각됩니다.


다음으로 재윤님께서는 새로운 지시를 어떻게 정의하는지에 대해서 물으셨습니다.


객체가 지시를 받으면 해당 지시에 응답하기 위해서 어떻게 그 지시를 처리할 것인지에 대한 길수(method)를 찾습니다. 만약 해당 지시를 처리할 길수가 있다면 그 길수에 따라 지시를 처리하여 결과값을 남기지만, 길수가 존재하지 않는다면 결국은 해당 객체는 지시를 처리할 수 없는 것입니다. 즉 지시를 알아들을 수 없다는 것은, 그 지시를 처리할 수 있는 길수를 객체가 모르기 때문입니다. 따라서 우리가 내리는 지시를 알아들을 수 있도록 객체에게 길수를 가르쳐주어야 합니다.


이미 있는 객체에 새로운 길수를 더하기 위해서는 해당 객체가 속한 갈래에 해당 지시를 실행할 수 있는 길수를 만들어주면 됩니다. 이 때 사용하는 도구가 바로 "갈래 씻줄 탐색기"(class hierarchy browser)입니다.


새로운 길수를 만들기 위해서는 Smalltalk에 대해서 좀 더 알아보는 것이 필요하지만, 일단 다음의 과정을 따르면 새로운 길수를 만들 수 있습니다.


지금 일터를 열고 다음처럼 입력해 봅시다.

5 isPrime  "SmallInteger does not understand #isPrime" 잘못 발생


우리는 지금 "5"라는 객체에게 "너, 소수(素數, prime) 맞아?"하고 지시를 내렸습니다. 그런데 객체는 "너, 소수 맞아?"라는 지시를 어떻게 처리해야 할지 그 길수를 모르고 있습니다. 따라서 우리는 정수 객체에게 어떻게 하면 자신이 소수인지 아닌지를 구분할 수 있는 길수, 곧 방법을 가르쳐주어야 합니다.


1) 새로운 길수를 만들기 위해 Tools > Class Hierarchy Browser 메뉴를 실행하여 갈래 씻줄 탐색기를 엽니다.


2) 우리는 "Integer" 갈래(class)에 새로운 지시를 추가할 것이므로, Class > Find 메뉴를 실행하여 열린 대화상자에 "Integer"를 입력합니다. 이렇게 하면 "Integer" 갈래가 선택되고, 아래와 같은 갈래 꼴(class definition)을 볼 수 있을 것입니다.

Number subclass: #Integer
        instanceVariableNames: ''
        classVariableNames: 'PrintBuf '
        poolDictionaries: ''


3) 이 상태에서 Method > New 메뉴를 실행해서 새로운 길수(method)를 만들 준비를 합니다. 갈래 탐색기의 아래쪽 널(pane)에 커서가 반짝거리면서 여러분이 길수를 가르쳐주기를 기다리고 있습니다.


4) 이제 아래의 글토막을 그대로 입력합니다.

isPrime
        "받는이(receiver)가 소수인지를 검사하여 논리값을 남긴다."
        
        self < 2 ifTrue: [ ^false ].

        2 to: self - 1 do: [ :i |
                (self \\ i) = 0 ifTrue: [ ^false ].
        ].

        ^true


5) 입력한 것을 적용하기 위해서 Ctrl-S 를 누르거나 Workspace > Accept 메뉴를 실행합니다. 명령을 실행하면 여러분이 입력한 글토막이 Smalltalk에 의해 번역되어 문법 돋이(syntax highlight)가 적용됩니다.


6) 갈래 씻줄 탐색기를 닫습니다.

이제 "Integer" 갈래와 이 갈래에 딸린 아랫갈래(subclass)의 모든 실체(instance)들은 "isPrime" 지시를 알아들을 수 있습니다. 일터에서 다음과 같이 실험하면 결과를 확인할 수 있습니다.

5 isPrime        true
7 isPrime        true
3 isPrime        true
2 isPrime        true
4 isPrime        false
6 isPrime        false
9 isPrime        false
100 factorial isPrime    false
200 factorial isPrime    false


결국 Smalltalk에서 프로그램을 작성한다는 것은 이렇게 객체가 알아들을 수 있는 여러 가지 길수를 가르쳐 주는 것이라고 해도 과언이 아닐 것입니다.


두번째! 스몰토크에서는 다른 OOP언어들의 Dynamic Binding 그러니까 같은 지시에 대해 서로 다르게 반응하는 것을 어떻게 구현하나요? 스몰토크도 상속을 통해서 구현할 수 밖에 없나요? 아니면 구지 신경안써도, 즉 같은 부모를 가지고 있지 않아도, 특별히 뭔가 키워드를 써주지 않아도저절로 되나요? (아무래도 그렇게 될 것 같은 불길한 예감이..)


결합(binding)은 어떤 이름에 특정한 의미를 부여하는 것을 말합니다. 말씀하신 대로 동적 결합은 프로그램이 실행될 때(run-time) 결합하는 방법이고, 정적 결합은 프로그램이 번역될 때(compile-time) 결합하는 방법입니다. 정적 결합은 바탕글(source code)을 한 번에 기계어로 번역한 다음 실행되는 일괄 처리형 언어에서 사용되고, 동적 결합은 BASIC 과 같이 통역기(interpreter)를 사용하거나 Smalltalk 언어와 같이 점층적 번역(incremental compiling) 기능을 사용하는 대화식 언어에서 주로 사용됩니다.


C++, 객체 파스칼의 경우는 virtual 이라고 하는 특별한 지시를 내려주어야만 동적 결합이 수행되고 기본적으로는 모든 이름이 정적으로 결합됩니다. 동적 결합의 경우는 프로그램을 실행할 때 특정한 이름에 어떤 의미를 부여할지를 판단해야 하기 때문에 정적 결합에 비해 상대적으로 실행 시간이 늦고 기억공간을 조금 더 소비하기 때문입니다.


그러나 Smalltalk의 경우는 모든 이름이 동적으로 결합됩니다. 즉 번역할 때에는 의미가 정해져 있지 않지만, 번역된 바탕글이 실행될 때에 이름에 의미가 부여됩니다.


재윤님께서 말씀하신 것처럼 특별한 조치를 취하지 않더라도 어떤 객체가 지시를 알아들을 수만 있다면 이름(길표)에 대해서는 상관하지 않습니다. 이를테면 제 글에서도 볼 수 있듯이, 분수(Fraction) 객체와 정수(Integer) 객체는 똑같이 "numerator"와 "denominator"를 알아듣습니다. 그러나 정수와 분수의 경우 모두 각각의 길수를 다르게 처리합니다.


또한 윗갈래(superclass)에 어떤 길수가 있다고 해도, 아랫갈래에서 같은 이름의 길수를 사용하면 얼마든지 윗갈래의 길수를 번복(override)할 수 있습니다.


Smalltalk에서의 변수(variable)은 모든 종류의 객체를 가리킬 수 있으므로, 결국 어떤 변수가 가리키는 객체에게 지시를 보내면, 어떤 길수가 실행될 지는 실제로 실행 시간에만 알 수 있습니다. 그러므로 동적 결합을 사용하는 것입니다.


그런데 이러한 사실이 왜 불길하지요? 동적 결합이 정적 결합에 비해서 실행 속도가 늦고 기억공간을 많이 사용하기는 하지만, 객체지향의 진정한 의미인 "재사용성" 과 "융통성" 을 높이는 하나의 방법입니다. 또한 Smalltalk 는 구현 시스템에 따라서 여러 가지 최적화 기법을 사용합니다. 기본적으로 Smalltalk의 모든 지시는 동적으로 결합되는 것이 원칙이지만, 어떤 시스템의 경우는 갈래 씻줄에서 번복된, 즉 특정한 이름을 가진 길수가 한 번만 정의되었다면 정적 결합을 수행하고, 번복된 길수가 있을 경우에만 동적 결합을 수행하기도 합니다. 제가 알고 있기로는 Java에서도 동적 결합을 기본으로 지원한다고 하기 때문에, 이것이 프로그램의 성능에 큰 영향을 주지는 않는다고 생각합니다.


세번째! 스몰토크는 Type이 없다고 들었는데, 그럼 실행중에 에러가 나면 꼭 창이 뜨나요? 컴파일된 바이너리가 실행중에 그런 상황이 발생하면 어떻게 되나요? 에러를 방지하기 위해서 일일이 타입체크를 직접 해줘야만 하나요?


Smalltalk에는 말씀하신 대로 자료형(type)이 존재하지 않습니다. 그러나 이러한 자료형의 기능을 가지고 있는 갈래(class)가 있습니다. 원래 자료형이라는 개념 자체는 절차 지향적 언어에서 나온 것이기 때문에, 객체지향 언어인 Smalltalk에서는 type 자체를 지원하지 않는 것입니다. 물론 C++나 객체 파스칼의 경우 갈래를 하나의 자료형으로 취급하기는 하지만, 이로 인해서 발생하는 문법의 복잡성이나 여러 가지 혼란은 피할 수 없는 것입니다.


Smalltalk에서 말씀하신 문제가 생길 수 있는 소지는 다른 데 있습니다. 그것은 Smalltalk에서 사용하는 소위 변수(variables)의 경우에 특정한 자료형이 고정되어있지 않다는 것입니다. 즉 널널한 자료형 검사를 수행합니다. 쉽게 이야기하면 베이식에서 사용하는 변수와 비슷한 특정을 가지고 있다는 말인데, 바로 이것이 Smalltalk의 유연성과 강력함을 가져다 줍니다. Smalltalk에서

| a b c |


처럼 선언하면, 꼬리표―필자는 "변수"보다는 "꼬리표"가 더 적당하다고 생각합니다― a, b, c를 어떤 객체에도 갖다 붙일 수 있습니다. 즉

a := 'Hello!'.
a := 3.
a := Array new: 10.
a := a asSortedCollection.
a := [ 3 + 4. ].


위와 같은 것이 모두 실행된다는 것입니다. 이렇게 꼬리표는 어떤 객체에든 붙일 수 있기 때문에 말씀하신 것처럼 잘못이 발생할 수 있습니다.


그러나 Smalltalk가 이렇게 널널한 자료형 검사를 하는 것은 나름대로 믿는 구석이 있기 때문입니다.


앞서 Smalltalk에서 모든 객체는 갈래(class)에 속해있다고 했습니다. 따라서 어떤 객체에게 사용자가 지시를 보낼 때, 해당 지시를 전혀 알아들을 수 없으면 잘못(exception)이 발생합니다. 이를테면 Smalltalk 에서

'우리' + '나라'.


처럼 하면 글줄(string) 객체는 아예 "+" 지시를 알아들을 수 없으므로 잘못을 냅니다. 또한

3 + '나라'


의 경우는 "String does not understand #addToInteger:"처럼 바로 알아듣기는 어려운 지시가 나오지만, 어쨌든 잘못된 자료형으로 인해서 시스템에 피해를 주지는 않도록 되어 있습니다.


더구나 Smalltalk의 경우는 작은 글토막을 가늠하여 결과를 확인하기 때문에 문제가 생겼다고 해도 쉽게 그것을 발견할 수 있고, 갈래 씻줄만 충실하다면 굳이 유연성을 제한하는 자료형 검사 기능을 넣을 필요가 없는 것입니다.


물론 재윤님께서 말씀하신 대로 어떤 경우에는 자료형을 매우 깐깐하게 따져야 할 때가 있습니다. 그럴 때는 갈래를 만드는 사람이 자로형을 검사해 주면 됩니다. 즉

self assert: [x isKindOf: Number].


라고 하면 자료형 확인을 할 수 있습니다. 소위 자료형 검사를 하는 언어의 경우는 검사의 강도는 지정할 수 없지만, Smalltalk의 경우에는 프로그래머의 필요에 따라서 자료형 검사의 강도를 정할 수 있습니다. 위의 경우는 꼬리표(변수) "x"가 Number 갈래에 딸린 실체이기만 하면, x가 정수(Integer)이건, 분수(Fraction)이건, 소수(Float)이건 상관 없이 조건이 만족됩니다. 만약 좀 더 꼼꼼하게 검사하고 싶다면

self assert: [x class = SmallInteger].


위와 같이 하면 됩니다. 위의 경우 "x" 꼬리표는 반드시 정수 갈래에 속한 객체여야만 합니다.


자료형을 검사해서 생긴 잘못이건, 또는 지시 처리의 오류로 생긴 잘못이든, 일단 잘못이 생기면 그것을 따로 처리할 수 있습니다. 이는 다른 언어에서의 예외 처리(exception handling) 기법과 동일한데, 만약 어떤 부분에서 잘못이 발생할 경우 특정한 처리를 하기를 원하면

        [
                실행할 명령들
                .....
        ] on: Exception do: [
                잘못이 있을 경우 처리할 명령들
                ....
        ]


의 구조를 사용할 수 있습니다. 위에서 "Exception"은 모든 잘못이나 예외를 나타내는 갈래의 뿌리 갈래이고, 이 갈래에서 모든 잘못이나 예외를 나타내는 갈래의 씻줄이 출발합니다. 따라서 좀 더 구체적인 잘못을 잡아내기 위해서는 해당 잘못을 나타내는 갈래(예를 들면 "ZeroDivided" 와 같은)를 사용할 수 있습니다.


이렇게 발생한 잘못을 처리해 주지 않으면 해당 잘못은 기본 잘못 처리기에 넘겨지고, 잘못 처리기는 발자취 창을 표시하여 사용자에게 프로그램의 문제를 해결할 수 있는 기회를 제공합니다. 물론 독립 실행 파일의 경우는 해당 잘못이 생기면 메시지 박스 등을 통해서 잘못이 발생한 사실을 통보해 주는 처리가 있을 것입니다.


예를 하나만 들겠습니다. 아래의 글토막을 펴 보십시오.

[ 3 isUppercase ] on: MessageNotUnderstood do: [ :e |
        MessageBox notify: '그것도 못하냐?'.
].


위의 경우는 어떤 객체가 지시를 알아듣지 못할 때 그 잘못만을 잡아내어서 처리를 하는 보기입니다. 이렇게 Smalltalk에서 발생하는 여러 가지 잘못은 얼마든지 사용자에 의해 처리될 수 있으며, 이것이 바로 Smalltalk의 커다란 특징이자 장점입니다.


C건 C++이건

        long l = 10;
        int I = 20;

        printf("%d %d", l, I);


위의 문장은 (라이브러리의 제작자가 처리해 주지 않는 이상) 전혀 잘못을 잡아낼 수 없습니다. 자료형 검사가 까다롭기로 소문이 난 C/C++이지만 그런 검사를 한시라도 게을리 하면 시스템 다운이라는 치명적인 상황을 야기할 수도 있는 것입니다.


이에 비해 Smalltalk는 기본적으로 자료형 검사를 하지 않지만, 각각의 자료들(정확히 말하면 객체들)은 자기가 어떤 갈래(class, 즉 type)에 속해있는지를 잘 알고 있으며, 자기가 할 수 있는 일이 무엇이고 할 수 없는 일이 무엇인지를 너무나 잘 알고 있습니다. 즉 살아있습니다.


절차 지향적 관점에서는 죽어있는 자료를 절차라는 통에 넣어서 가공한 다음 새롭게 자료를 만들지만 이것 역시 죽어 있는 자료일 뿐입니다. 그러나 객체 지향 개념에서는 모든 것을 객체로 생각합니다. "1" 이라고 하는 숫자 하나라도 객체지향의 세계에서는 살아있으며, 자기가 누구이며 어떤 갈래에 속하는지, 자기는 어떤 일을 할 수 있는지를 누구보다도 잘 알고 있는 살아있는 객체인 것입니다. 이 객체에게 설령 잘못된 지시를 내렸다 하더라도 시스템이 다운되거나 하지는 않습니다. 그래서 자료형 검사가 까다롭지 않은 Smalltalk이지만 매우 안전하다는 말을 듣는 것입니다.


C++의 문법이 이토록 복잡해 진 대에는 정적 결합과 동적 결합을 구분해 놓고, 객체를 처리하는 변수에 굳이 자료형을 매기기 시작한 데서 출발하는 것입니다. Smalltalk의 경우는 template이나 virtual 함수가 전혀 없지만 똑같은 일을 C++보다 훨씬 간결하게 처리할 수 있습니다. 물론 그렇다고 C++의 이러한 특징을 나쁘다고 말하는 것은 아니며, 나름대로 쓰임새가 다르기 때문에 일어나는 현상입니다. 다만 이러한 특징이 Smalltalk에 그대로 적용되지 않는다고 해서 Smalltalk가 낮게 평가되어서는 안된다는 것입니다.


뭐랄까, 씨줄, 갈래, 실체, 지시, 헛(?)... 좋은 우리이름인건 분명하지만 너무 직역이 아닌가 싶네요. 선생님의 독자적인 창작이신지 아니면 우리는 이런 이름을 쓰자고 주장하는 그룹이라도 있는지 궁금하군요.


마지막으로 제가 쓰고 있는 한글 용어에 대해서 이야기하겠습니다. 제가 쓰고 있는 낱말 중에는 이미 "객체", "절차" 등과 같이 이미 알려져서 널리 쓰이는 말이 있는가 하면 "씻줄", "갈래", "지시"처럼 우리가 익히 알고있지만 객체 지향 영역에서는 사용되지 않았던 낱말이 있고, 마지막으로 "바탕글", "길수", "쇠마디" 처럼 사전에는 없지만 두 개 이상의 낱말을 조합해서 만든 낱말도 있습니다.


저는 예전에 한글 언어 "씨앗"으로 프로그래밍을 공부한 적이 있습니다. 그래서 한글 용어가 얼마나 편리한지를 누구보다도 잘 알고 있습니다. 아쉽게도 Smalltalk가 한글 이름을 지원하지 않기 때문에 길수의 이름에 한글을 사용할 수는 없지만, 일단 우리가 널리 사용하고 있는 외래어들 만이라도 우리말로 고쳐서 알아듣기 쉽게 부르는 것이 좋다고 생각했기에, 과감하게 시도를 해 본 것입니다.


"생성자", "소멸자", "계승", "다형성" 등과 같은 낱말이 사용되기 시작한 것이 그리 오래되지 않았습니다. 이전에는 전부 "constructor", "destructor", "inheritance", "polymorphism"처럼 표기했습니다. 실제로 필자가 처음 C++을 공부할 때에는 우리 나라의 책보다는 차라리 원서를 읽는 게 더 편할 정도였습니다.


처음부터 "생성자", "소멸자"와 같은 낱말이 자리매김을 한 것은 아니었을 것입니다. 누군가 알맞은 낱말을 찾아내서 써왔고, 이런 것들에 동감하는 여러 사람들이 함께 사용함으로 인해서 언어의 사회성이 적용되기 시작한 것이지요. 처음 "글쇠"(key)라는 말이 나왔을 때에도 매우 어색해 하던 사람들이지만, 요즘은 어색하지 않게 이 말이 통용되고 있습니다. 사전에서는 분명히 찾아볼 수 없는 말입니다. 그러나 이 말이 얼토당토않게 만들어지지는 않았습니다. "글쇠"는 "글자 박음쇠"의 준말입니다.


마찬가지로 제가 스스로 만들어 본 낱말은 나름대로 사전에서 이미 존재하는 낱말입니다. 제가 "method"를 대신해서 쓰는 "길수"라는 낱말은 "길"과 "수"를 합친 것입니다. 둘 다 "어떤 일을 처리하기 위한 (묘한)방법"을 나타냅니다. "nil"을 "헛"이라고 번역한 것은 이미 "씨앗"에서 그렇게 했기 때문입니다. 사전에서 "헛"을 찾아보십시오. "nil"과 놀라울 정도로 똑같은 뜻을 가지고 있습니다.


컴퓨터를 공부하는 것 못지 않게 우리말을 찾아서 쓰는 것도 중요하다고 생각합니다. 우리가 우리말을 찾아서 쓰지 않으면 우리의 생각이 바뀌고 정신이 흔들립니다. "유도"에서 일본 말을 놔두고 왜 "그쳐" 등과 같은 우리말을 사용할까요? :)


자, 이쯤에서 글을 닫을까 합니다. 이것 저것 많은 내용을 쓰다 보니 글이 많이 길어졌지만, 이것도 제가 정기적으로 올리는 강좌 못지 않게 매우 중요한 내용을 담아보려고 노력을 했습니다. 진도를 앞서신다는 것은 매우 즐거운 일입니다. 사실 프로그래밍을 많이 접해보시고 공부하신 분이라면 지금까지의 제 글은 무척 쉽게 이해하실 수 있으실 것입니다. 실제로 Smalltalk의 더 깊은 곳을 공부해 보고 싶으신 분이 많으실 텐데, 마땅한 자료가 없는 것을 정말 안타깝게 생각합니다. 일단 저도 더욱 분발하겠습니다. 그래서 어서 빨리 여러분에게 Smalltalk, 그리고 객체 지향에 프로그래밍에 대한 여러 가지 생각들을 정리해 드리고 싶습니다.


앞으로 많은 관심 부탁드립니다. 아, 그리고 혹시, 제 글에서 틀린 부분이 있으면 서슴지 마시고 시적해 주십시오. 그래서 제가 잘못 알고 있는 것을 고칠 수 있는 기회를 주시면 저에게도 큰 도움이 되겠습니다.


다시 한 번 제 글을 읽고 질문해 주신 재윤님께 감사드리고, 제 글이 "현문우답"이 되지나 않았는지 모르겠습니다.


그럼 날씨 추운데 건강 조심하세요.


Notes