하마
6k
2016-01-11 17:10:09 작성 2016-12-02 17:41:12 수정됨
7
8270

함수형 프로그래밍이란 - 2편


함수형 프로그래밍이란? (1편 부작용)  

1편 번역은 제가 안했기때문에 위의 링크에서 읽으시구요.  아래 2편은 직접 번역 보았습니다. 
그리고 함수형 프로그래밍 모르면 뒤쳐진거다?  절대 아니니깐 그냥 재미삼아서 읽어보셔도 됩니다. 
소설가는 사람의 삶과 세상 돌아가는데 관심을 갖고,  모국어에 대해 고민하는게 우선이며, 그 후에 필요에 따라  다른 언어를  공부를 해야 하듯이 말입니다. 그리고 개인적으로 함수형 프로그래밍이 대중화가 될가능성은 0% 로 보고 있습니다.  그들이 사용하는 매우 합리적인 언어 정도 수준에  머무를것입니다.  
C++ 창조자도 언급했지만 , 굳이 언어를 크게 바꾸지 않아도  라이브러리를 이용해서  대중이 요구하는것의 대부분은 가능합니다. 다음 대세는  객체지향도 함수형도 아닌 전혀 상상도 못하는 방식이 되리라 예견합니다.

물론  이것과 별개로 자신의 껍질을 깨고 외부에서 바라보는 기회를 갖는다는 의미에서 ,  
깊이있게 까지는  아니더라도 함수형 개발을  관심을 가질 필요가 있지 않을까 합니다.

(대중화  : C /  JAVA / Python 정도 위치, SI 및 게임에서 가장 많이 사용되는 언어) 



함수형 프로그래밍이란 (2편 언어에서 조망)

읽기전에 : 본 글은 블로그에서 자신의 생각을 표현한 글로써, 건조하게 문법을 설명한 글이 아니라서 좀 터프하며 주관적인 생각도 많이 포함하고 있음을 염두하시길 바랍니다.

서두


첫번째 포스트에서 나는 함수형 프로그래밍을 정의했었다. 뭐 교과서적이거나 마케팅적인 관점은 아니었지만 일반 프로그래머에겐 쉽게 이해할 수 있는 상식적인 설명이었다고 본다. 어쨌거나 내가 바라는건 개발자들이 통제불능 상태로 자신의 코드를 몰고가기 전에 어떤 부작용이 있을지 고민해 봤으면 하는 바램이다.

자~!  이제  주변에서 사용되고있는  함수형 언어들에 대해 살펴보자. 

(역주 :   side-cause 와 side-effect  는 둘다 부작용으로 번역했고 그 차이는 part1 에서 참고하시구요. 다만 side-cause  경우 괄호안에 표기했습니다.부수효과라고  할 수도 있으나 좀 더 강경한 느낌이 어울려서 부작용을 택 했습니다.)

함수형 프로그래밍은 .. 가 아니다.


map 이나  reduce 가 아니다.

모든 함수형 언어에서 저것을 보았더라도, 저것이 언어를 함수형으로 만드는게 아니다.  단지 어떤 시퀀스 요소들에 대해서 작업할때 , 부작용을 없애기위한 노력의 산물일 뿐이다.


람다 (lamda)  함수가 아니다.

일급함수에 대해 언급하는 것을 모든 함수형 언어에서  들었을 것이다. 그러나 그것은 부작용을 피하는 언어를 만드는것을 시작할때 자연스럽게 도출되는 것이다.  도와주는 요소이지 근본은 아니다. 


타입문제가 아니다.

정적 타입 검사방식은 매우 유용한 도구이다. 그러나 그것이 함수형 프로그래밍 (FP) 의 선행 요구사항은 아니다. Lisp 은 오래된 함수형 언어이자, 가장 오래된 동적 언어이다. 

정적 타입들은 매유 유용하게 될수있다. 헤스켈은 그것의 타입 시스템을 부작용을 처리하기위해 아름답게 이용한다.  그러나 함수형 언어를 만들기 위한  재료는 아니다. 


다시한번 강조하자면 !!    함수형 언어는 부작용에 대한 것이다. 


각각의 언어에서 그 의미는 무엇인가?


자바스크립트는 함수형 언어가 아니다.

함수형 언어는 당신이 컨트롤 할수있거나, 할수없는 모든곳에서 부작용이 일어나는것을 제거하는데 도움을 주는 언어이다. 자바스크립트는 이런 기준에 미흡하며  사실 자바스크립트에서 부작용을 조장하는 지점은 쉽게 찾을수있다.

가장 찾기 쉬운 부분은  this이다. 그 숨겨진 input 은 모든 함수에 존재한다.   this  에 존재하는 마법같은 일들은 주로 자신의 의미가 쉽게 바뀐다는데 있는데 , 심지어 자바스크립트 전문가들 조차도 이놈의  this 가  무엇을 가르키고 있는지 추적하는데 쩔쩔 맨다는 점이다.  함수적 관점에서 보면 이 모든 마법적으로 활용되는 일들에서 불쾌한 냄새가 나는듯 느껴진다.

자바스크립트에 함수형 헬퍼 라이브러리들을 로드할수있는데  ( 예를들어 Immutable.js ) , 그것은 자바스크립트를 함수형 스타일로 개발하는데 더욱 쉽게 만들어준다. 물론 언어 그 속성 자체를 바꿀수는 없지만..  


자바는 함수형 언어가 아니다.

자바는 확실히 함수형 언어가 아니다. 자바 1.8 에서의 람다 기능의 추가는 그것에 전혀 영향을 미치지 않는다. (역주: 람다 와 모나드 (스트림API) 를 통해서 부작용을 해소하려는 시도는 있습니다.)

자바는 함수형 프로그래밍의  반대 방향에 꿋꿋히 서있는다. 자바의 핵심 디자인  설계자는 "코드는 부작용을 일련의 지역화로  다루어야한다 " 라고 말하고있다.  메소드들은 객체의 지역 (local ) 상태를 바꾸거나 의존한다.

사실 자바는 함수형 프로그래밍 (FP) 에 대척점에 서있다. 만약 당신이 자바코드를 부작용없이 작성하려면, (객체의 상태를 바꾸거나 읽지 않는 )  당신은 그쪽 세상에선 형편없는 프로그래머라고 불리게 될것이다. 그게 자바가 쓰여지는 방식이 아니니깐.   당신의 부작용 청정 코드는 static  키워드로 양념될것이고 , 그들은 눈쌀을 찌부리며 당신의  책상을 화장실로 옮겨놓을 것이다.  (역주 : 이부분은 조금 이해안가네요. static 을 남발하면 부작용 청정 코드가 되었었나요? 아마 static 을  immutable 하게 사용한다는 전제를 한거 같습니다.

물론  나는 자바가 나쁘다고 말하는건 아니다.  (흠 , 오케이~그렇게 볼수도 있겠다)  , 그러나 요점은 부작용을 보는 관점에서 전혀 다른 시각을 가지고 있다는 점이다. 자바 경우는  지역화된 부작용은 좋은 코드의 주줏돌이라고 보며 ,  함수형 프로그래밍은 그것들을 악이라고 본다. 

당신은  자바나 FP 이 부작용이라는 문제을 대응하는 관점에 대해 양쪽을  다른 각도로  볼수 있을 것이다. 양쪽 모델은 문제로서 부작용을 인지하며 , 다르게 대응한다. 객체지향의 대답은 " 그들을 '객체' 라는 바운더리 내에 포함한다 ' 이고 반면 함수형의 대답은 ' 그들을 제거한다 ' 이다. 운이 없게도 , 실제 자바는 부작용을 캡슐화하는 시도를 하지 않고 그들을 당연하게 관행으로 바라본다. 만약 상태가 있는 객체의 형식에서  당신이 부작용을 만들지 않는다면, , 당신은 결국 나쁜 자바 프로그래머가 될것이다.  사람들은 static을 남발하는 당신을 해고 할것이다. 

역자추가) 

자바 8 의 stream API 는 i/o 에서 사용되는 inputstream / outputstream 과는 완전 다른것이며 
stream API 는 함수형 세계에서 말하는 모나드 입니다. 그래서 자바에서 함수형 프로그래밍의 멋진 부분을 
가지고 놀수있게 된것이죠.스트림 API 는  순서대로 요소를 처리하는 다양한 방법을 제공하며 런타임 성능 향상에 
좋은 영향을 줄수 있습니다.

참고 : JAVA 8 Stream API 

스칼라는 큰 (역주: 어려운)  과업을 가지고 있다.

생각해보면 스칼라는 매우 도전적인 제안을 하고있다. 만약 스칼라의 목적이 객체지향과 함수형의 두 세계를 묶는것이라면 , 부작용이라는 렌즈를 통해서 보면" 부작용 의무화" 와 "부작용 금지" 의 차이에 다리를 놓아서 연결하겠다는 것이다. 그들이  조화롭게 뛰어노는 꽃동산을 만든다는것이 가능할지는 모르겠다. 당신이 스칼라를 사용하는데 , map함수를 지원하는 객체들을 만드는것에 두 사상을 통합하여 개발  할 수 없을것이다. (역주 : 만약 대중화가 된다면 스칼라를 가지고 자바처럼 코딩 할 것이라 생각드는... ) 

만약 스칼라가 그런 통합에 성공할것인지 판단하는건 각자의 몫으로 남겨둘것이다. 그러나 만약 내가 스칼라의 마케팅 담당자라면 그들을 통합하는 대신해 스칼라를 점진적으로 자바의 부작용으로부터 벗어나서 순수FP 의 세계로 이동하게 도움주는 것으로 홍보하고 싶다. (역주: 자바 8을 더 잘 사용하거나,스칼라 보다는 바로 클로저로 점프하는 것도 좋다고 생각합니다. 전 일단 스칼라로 이동했는데 이게 순수함수형만 일정기간 동안 쓰는 환경이 아니라 폴리글랏프로그래밍을 해야한다면 익숙해지기 힘들더군요.결국 클로저나 하스켈을 상당 기간 사용한 후에 스칼라를 사용하면 능력이 배가 될듯


클로저 (Clojure)

클로저는 부작용에 대해 흥미로운 위치에 자리잡고 있는데  그 언어의 창조자 리치 하키는 "클로저는 대략 80% 정도 함수형 이다 " 라고 말하고있다.  나는 왜 그런지에 대해 명확히 말할수있다고 생각하는데,  시작부터 클로저는 부작용에 대한 하나의 특정 종류를 다루는것으로 디자인되있다.  :  시간   

이것을 살펴보자, 여기에 당신을 위한 자바 유머가 있다:

  • 5 더하기 2는 무엇?
  • 7.
  • 정답.  그럼 5 더하기 3은 ? 
  • 8
  • 땡~!  10 이 정답.  왜냐면 우린 5 을 7 로 바꿨거든 기억해?   (역주:  먼소리야...

오케이~인정. 훌륭한 조크는 아니었다. 그러나 포인트는 말이지. 자바왕국에서는 values 가 계속 유지되지 않는다는 거다. 정당하게 5를 표현하는 어떤것을 가질수 있는데 함수를 호출함으로써 그게 더이상 5가 아님을 알수있게된다.  (역주 :  어떤 값을 가지고 있는 변수/객체를 함수에 인자로 넘기면, 레퍼런스 값이 복사되어 넘어가므로 내부에서 어떤 행위를 하면 본질이 바뀌어 질수 있음을 말함. Call by Reference value. 즉 너무 당연하게 생각해왔던것이 사실 당연한게 아니었던것이다

Integer 경우는 사소한것이고, 커다란 객체로 본다면 그 영향은 크게 증폭된다고 말할수있다. Part 1에서 말한 InboxQueue 를 떠올려보면 InboxQueue 의 상태는 시간에 따라서 달라질수 있는 value 이다. 그래서 결국 시간은 InboxQueue 의 의미에 대한 부작용(side-cause)이라고 말할수있는거지.  

클로저는 그 시간의 부작용 (side-cause) 에 빡세게  촛점을 마추고있다.

시간의 숨겨진 효과 때문에 우리가 저장한 값에 의존할 수 없게 되고, 저장한 값에 의존할 수 없으면 함수의 입력값에도 기댈수 없게 되며, 따라서 우리는 어떠한 것에도 그것이 예상가능하게 혹은 반복적으로 동작할 것이라고 의존할 수 없게 된다는 것이 바로 리치 히키의 통찰이다.  (한주영님 번역으로 수정)

만약  value 가 부작용을 가지고있다면,  모든것이 부작용들을 가진다. 만약  value 가 순수하지 않다면 우리의 프로그램안에는 순수한것이 남아있지 않게 된다. 

그래서 클로저는 시간에 대한 무기를  갖는다. 모든 value 는 디폴트로 불변형 (immutable) 이다. (시간이 지나도 변하지 않음) . 만약  value  를 바꾸길 원할 경우에  클로저는 바뀌지 않는 값에 대한 래퍼를 제공하며 그 래퍼는 무거운 제약들을 갖는다. 

  • 래퍼를 사용해서 값을 바꾸기위해  강제적으로 사전동의 (opt-in)  를  구해야한다. 
  • 일시적이거나 자연스럽게 변형가능한 value 를 만드는것을 할 수 없다. 항상 언어에서 마련한 명시적인 플레그 (부작용에 대한 보호막 ) 를 이용해야한다.   
  • 자신도 모르게 변형가능 값을 소비할수없다. 항상 언어적인 보호장치를 사용해야한다. 
  • 변경가능 값 래퍼를 사용하고 , 다시 돌려 받을때는 불변형이다. 쉽게 시간-의존적인 세계 밖으로 순수한 녀석을 되돌려 받을 수 있다. 

시간 관점으로,  클로저는 매우 훌륭한 함수형 프로그래밍 언어의 예이다. 시간의 부작용에 대해 깊이 있는 적대심을 표현하고 있다. 어디에 있든지 제거하며 (디폴트로)  , 당신을 도와줄것이다. 


하스켈 (Haskell)

만약 클로저가 시간이라는 특성에  반대한다면 ,  하스켈은 평범히 (?)  부작용에 대처하는 느낌이다. 하스켈은 정말 부작용을 싫어라 하는데,  많은 노력을 그것을 컨트롤링하는데 쏟아 부었다. 하스켈이 부작용에 항거하는 흥미로운 방식중 하나는 타입들과 연관된다. 타입시스템 안에 부작용들을 모두 푸쉬 하는데 예를들어 getPerson 함수를 가지고 있다고 치고 ,  하스켈에서는 다음과 같다: 


getPerson :: UUID -> Database Person

"UUID 를 이용하여  Database 의 문맥안에서 Person 을 리턴하라" 라고 읽을 수 있다. 이건 흥미로운데 - 당신은  하스켈 함수의 타입 시그너쳐를  바라 볼 수 있으며,  어떤 부작용을 포함하고 그렇지 않은지에 대해 알수있다.  ( 역주 : 하스켈을 공부하면  알수있겠지...)  그리고 다음과 같이 보장 할 수 있다.  " 이 함수는 파일 시스템에 접근하지 않을겁니다. 왜냐하면 부작용의 종류로서 선언되지 않았어요".   즉 명시적인 타이트한 컨트롤을 한다. 

중요한 포인트는 또 있는데, 함수를 다음처럼 볼수있다.

formatName :: Person -> String

... 그리고 이것이 Person 을 가지고 String 을 리턴하는지 알 수 있다. 그 밖에 다른게 전혀없다. 왜냐하면 만약 부작용이 있다면, 당신은 타입 시그니쳐에 그것들이 가둬지는 것 (역주 : 하스켈에서는 아마 명시적으로 표현되나 보다) 을 확인할수 있기 때문이다.  아마도 가장 흥미로운것은 다음 예인데: 

formatName :: Person -> Database String

이 시그니쳐는 우리에게 formatName 의 이 버전은 데이타베이스 관련 부작용을 포함한다고 말하고 있다.  " what the hell ?? 왜 formatName 이 데이터베이스를 필요로 한거야? "  당신은  " 나는 셋업(set-up) 이나 목아웃(mock-out) 할 예정이야 단지 그 함수에 대해서 테스트 해 볼 건데.. 이 무슨.. "  라고 생각 중인데,  저건 정말 이상하다. 이 함수 시그니처를 보면 , 나는 디자인적으로 먼가 잘못된것이 보인다. 나는 코드를 볼 필요가 없이 썪은 냄새를 맡을 수 있다.  

자바의 함수 시그니쳐와 간단히 비교해보자:

public String formatName(Person person) {..}

어느 하스켈 버전이 저것과 동등한가?  자바의 경우 함수의 몸통을 조사할 필요도 없이, 당신은 저것에 대해 알 도리가 없다. 그것은  데이타베이스에 아마 접근할수도 있고  또는  파일들을 제거하고 리턴할것이며 선임을 빡치게 만들것이다.  그 타입 시그니처는 당신에게  무엇이 작동하는지에 대해 아주 조금  말한다.  

하스켈의 타입 시그니처는 ,  자바와  대조적으로 , 당신에게 디자인을 멋지게 다루는것에 대해 말한다. 그리고 그들은 컴파일러에 의해 체크되기 때문에, 당신이 아는게  맞다고 말한다. 그것은 훌륭한 아키텍처 도구들을 만든다는것 의미한다.  

하스켈은 매우 높은 레벨의 디자인 향기를 표면화 한다.  또한 코딩의 패턴 또한 표면화 한다. 나는 "함수자 (functor) " 라든지 "모나드(monad)" 라는 단어를 이번 포스트에서 제외할것이나  높은 수준의 소프트웨어 패턴들은 높은 레벨의 분석과 함께 시작한다고 말할것이다. 높은 레벨의 분석은 당신이 높은 레벨의 표기법(notation) 을 가질때 더욱더 쉽게 만들어진다.   


파이썬(Python)

근본적인 부작용에 대해 자바를 이용해서 빠르게 살펴보자. 

public String getName() {
  return this.name;
}

어떻게 우리는 이 함수를 순수하게   할까?  this  는 말했다시피 숨겨진 입력이고, 우리가 해야할것은 그것을 매개변수로 올려놓는것이다:


public String getName(Person this) {
  return this.name;
}

지금 getName 는 순수한 함수이다. 기본적으로 파이썬이 이러한 패턴을 채택하고있다는것을 말하려는것인데. 파이썬에서 또한 모든 객체 메소드들은 this  를  숨겨진 첫번째 인자로 가진다.  self: 로 불려질때를 제외하고

def getName(self):
    self.name

명시적인게 암시적인거보다 훨씬 좋다. 정말로~


인자가 없다는건 부작용(side-cause) 의 신호이다. 

당신이 인자가 없는 함수를 볼때마다, 두가지 중 하나는 사실이다.  정확히 동일한 값을 리턴한다. 또는 어디에선가로 부터 입력을 받는다. (즉. 부작용을 가진다) 

예를들어, 이 함수는 틀림없이 항상 동일한 정수를 리턴한다. ( 그렇지 않다면 부작용을 가진다 ) 

public Int foo() {}


리턴 값이 없다는건 부작용(side-effect)의 신호이다

 리턴값이 없는 함수를 볼때마다 , 그건 부작용이 있거나 그것을 호출하는 포인트가 없다는것을 얘기한다.

public void foo(...) {...}

저 함수의 시그니처에 따르면,  이 함수를 호출할 이유가 전혀 없다. 아무것도 당신에게 해주질 않는다. 이것을 호출하는 유일한 이유는 마법적인 부작용을 아주 조용히  일으키기 위해서이다. 


요약 

부작용에 대한 직관적인 인식은 당신이 코딩을 바라보는 방식을 바꿔줄것이다. 사소한 함수에서부터 시스템 아키텍쳐의 전반적인 사항을 조망하는것까지  많은것을 바꿀것이다. 또한 툴이나 테크닉에서 부터 언어를 바라보는 시야 또한 바꾸고 넓힐것이며. 결국 모든것을 바꿀것이다. 

자~ 이제부터 부작용을 죽이는 *네팔렘이 되어서,  소프트웨어 대균열에 맞서 보자. 


8
2
  • 댓글 7

  • 흠.,.
    787
    2016-01-12 11:18:13
    글 잘 읽었습니다.
    0
  • 성실이
    391
    2016-01-13 08:02:04

    스태틱이 넘쳐날거라는 부분은 아마 스태틱 메쏘드를 말하는 걸겁니다. 문맥상 순수함수로 작성하게 되면 필드를 사용하지 않을 것이고, 즉 스태틱 메쏘드로 채워질 것이라는 의미인 듯

    0
  • 하마
    6k
    2016-01-13 09:07:36

    성실이 //

    영문을 읽었을때보다 다시 한글을 읽으니깐 문맥이 분명히 다가오네요. 의도하는 바는 성실이님이 이해하신 데로 이며  다만  스태틱 함수만 특정 하는건 아닙니다. 스태틱 함수도 그자체로는 부작용 덩어리가 될 수 있으며, immutable 하게 바꿔야 하듯이, 변수도 마찬가지로 immutable 이면 족하기  때문입니다.


    즉  변수냐 함수냐 스태틱이냐가 아니라 "억지로 자바로 부작용 없는 코드를 짜는 개발자, 객체지향스럽지 않은 개발자"  를 말하고 자 하는것으로 보입니다

    0
  • nhj12311
    205
    2016-01-14 20:52:46

    이글을 이해하시는 고수분들께 질문드리고 싶네요 마지막 부분에 인자가 없는 정수 리턴 함수가 있는데 동일한 값이 아니라면 오류를 가진다라고...

    어째서일까요?

    getDate와 같은 함수도 잘만 사용하고 있는데...

    0
  • 하마
    6k
    2016-01-14 22:13:34

    nhj12311  //

    오류를 가진다가 아니라, 부작용을 가진다 입니다. 즉  long getDate() 라는 인자없는 함수는 항상 같은 값을 리턴받거나  <-- 이렇게 되면 부작용이 없는것이고 ,  long getDate() 를 호출했더니, 리턴값이 바뀐다면, 내부에서 어떤 작용을 하는게 side-cause 로 숨어있다는 것이고,  그 함수를 사용하는 사람은 그 마법적인 일을 직관적으로 이해할수 없게 되며, 저자는 그걸 "부작용" 이라 말하고있습니다.

    (이해를 돕기위해 1편을 반드시 읽어보시길 바랍니다.) 

    그 동안에 생각해오셨던  자바나 C 의 " 함수 " 에 기반하여 사고하게되면 이해가 안가는게 당연합니다. 
    본문 글에서 의미하는 함수형 프로그래밍에서의  " 함수 " 는  앞에다 수학이라고 붙히면 더 잘 어울릴거
    같습니다.수학의 함수를 생각해보면 y = x^2 같은거 말이죠.  y 값이 없는 함수는 없을뿐더러 , x 처럼 입력값이 없는 함수도 없지요. 만약  x 가 동일하거나 없으면 y 값이 고정일테구요. 함수형 프로그래밍의 깊이있는 수학적 내용은 저도 잘은 모르나, 대략 저렇게 이해하시면 될거 같습니다.

    0
  • nhj12311
    205
    2016-01-14 23:20:36

    감사합니다.  저는 그걸 왜 부작용이라고 하는지 이해를 못해서요.  수학의 함수처럼 프로그램을 한다는건가요?  그렇다면 언어의 장점을 오히려 버리게 되는건 아닌지요?

    0
  • cavin
    15
    2016-01-31 15:54:01

    nhj12311 //

    side-effect 단어는 부수효과로 읽으시는게 원뜻에 가깝습니다. 처리하는 내용이 있으면 그 결과를 드러내라는게 글의 의도입니다. FP에서는 동일한 입력에 동일한 결과를 기대합니다. 성공, 실패, 예외, IO작업, 레코드 변화가 있는데 결과를 리턴하지 않으면 그 이후의 함수는 결과와 상관없이 처리를 하거나, 함수범위를 벗어나 읽어들어야 합니다.(static이 아니더라도 함수범위를 벗어난 처리를 전역으로 말하기도 합니다) 즉, 외부로 부터 영향을 받는 상태가 되고 동일한 입력에 동일한 결과를 기대할 수 없다는 것이죠. 참조투명성을 해친다고 합니다.

    예외가 있으면 '성공처리결과 or 실패' 타입으로..  IO가 발생하면 IO작업으로 리턴하면 함수 여러개를 묶어도 동일한 입력에 똑같은 결과를 보장할 수 있고 함수외부로 부터 영향을 받거나 주는 일들이 자연히 없어지겠죠 (Haskell에서는 IO가 발생하면 IO작업을 리턴합니다)

    FP는 루프와 처리를 컬렉션 파이프라인으로 짧게 교체하고, 객체를 넘겨서 콜백을 처리하는 대신 익명함수를 주고 받고 성공/실패/예외를 Maybe나 Either로 교체하는 식으로 시작하시면 됩니다. 당장 타입클래스, 펑터, 어플리커티브, 모나드, 컴포지션 이런건 몰라도 되고 신경쓰지 않으셔도 괜찮습니다. 100% side effect 없도록 추구하지 않아도 되구요.

    어렵지 않습니다. 익숙치 않을뿐이구요. 생소할 뿐 쓸모없지 않습니다. 재미있고 매우 실용적입니다. 

    0
  • 로그인을 하시면 댓글을 등록할 수 있습니다.