반응형

모양과 뜻


프로그래밍 언어는  문법구조(syntax)와 의미구조(semantics)로 구성되어있습니다.  문법구조는 언어의 겉 모양이고 의미구조는 언어의 속뜻을 의미합니다.

이 두가지를 정의하지 못하면 언어라고 볼수 없고, 이 두가지를 알지 못하면 언어를 사용할 수 없습니다.


제대로 생긴 프로그램을 어떻게 만들고 그 프로그램의 의미가 무엇인지에 대한 정의가 없이는 언어를 구현할 수 도 없고 사용할 수 도 없습니다. 

또, 그 정의들은 애매하지 않고 혼동이 없어야 합니다.



문법구조


문법구조는 프로그래밍 언어로 프로그램을 구성하는 방법입니다. 제대로 생긴 프로그매들의 집합을 만드는 방법으로, 귀납적으로 규칙을 정의합니다.

이 귀납적인 규칙으로 만들어지는 프로그램은 나무(TREE)구조를 갖춘 2차원의 모습입니다.

(여기서 나무라고 표현 했는데, 자료구조의 Tree라고 생각하시면 됩니다. 

책에 나무 라고 용어를 풀어서 사용하고 있는데, 아무래도 실제 프로그래밍에서 좀 멀어져서 바라보고자 하는 의도가 있다고 보여집니다. 

이 글에서 설명하고자 하는 내용들을 실제로 programming 언어의 특정 알고리즘 차원에서 바라보지 말고 좀 떨어져서 바라보는 것도 나름 의미가 있습니다.)


정수식을 만드는 귀납규칙을 생각해봅시다.


1. 임의의 정수는 정수식이라고 한다.

2.두개의 정수식을 가지고  + 를 이용해 정수식을 만들수 있다고한다.

3. 하나의 정수식을 가지고 -를 이용해 정수식을 만들수 있다.



이와 같은 형식으로 간단한 명령형 언어를 생각해봅시다.





0. E는 위에서 정의한 정수식이라고 하자.

1. skip 만 있는 명령문은 그것만 말단으로 가지고 있는 그것만 말단으로 가지고 있는 나무가 된다.

2. 정수식을 오른쪽에, 하나의 변수를 왼쪽에,  루트는 := 임을 표시하는 나무가 된다.

3. 세계의 나무를 하부나무로 가지고 있고 루트노드에는 "if"문 임을 표시하고 있는 나무가 된다.

4. 두개의 명령어 나무를 하부나무로 가지고 있고 루트 노드에는 순차적인 명령문임을 표시하는 ";" 나무가 된다.


위 규칙들은 읽는 사람을 돕기 위해 필요 이상의 정보가 들어있습니다.

if 와 then, else 이런 단어들이죠. 또 명령어의 순서 역시 마찬가지죠.


이를 필요한 정보만 가지도록 간략화 한다면,  








이와 같이 각 규칙마다 간단한 symbol만 있으면 그만이다, then else 같은 장식이 필요 없어집니다.


아니면, 우리가 각 규칙을 구분할 수 있다면, 심볼 조차도 생략할 수 있게 됩니다.







사실, 이렇게 심볼까지 생략해가면서 간소화 시킬 필요는 없지요. 프로그래밍 언어의 사용자는 사람이기 때문에 사람이 이해하는데 도움이 되는 수준으로 규칙을 만들 필요가 있습니다.

그래야 문법 규칙들을 보고 만드는 방법에 대한 힌트가 될 수 있기 때문이죠.




요약된 문법 구조와 구체적인 문법구조

(abstract syntax and concrete syntax)


지금까지 봐온 규칙들은 요약된 문법 규칙이라고도 합니다. 프로그램을 만들때 사용하는 문법은 지금까지 보아온 귀납적인 방법이면 충분합니다. 결과물은 나무구조를 가진 2차원의 구조물이죠.


그러나, 프로그램을 읽을 때의 문법은 더 복잡해집니다.  만들어진 프로그램을 표현할때는 나무를 그리지 않고 일차원적인 글로 표현하게 되고, 쓰거나 말하는 사람의 머리에 그려져 있었을 2차원의 나무구조를 복원시키는 규칙들이 되기 때문입니다.


즉,요약된 문법구조와 구체적인 문법구조의 차이를 표현하자면 , 프로그램을 만들때 사용하는 규칙이냐, 읽을때 사용하는 규칙이냐 하는 차이로 보시면 됩니다.


그렇다면 구체적인 문법은 얼마나 구체적일까?


다음의 정수식을 살펴봅시다.

-1+2


위 정수식은 다음 두개의 나무구조중 하나를 일차원으로 펼친 것입니다.

      <<-1>+2>      -<1+2>


어느것인가? 프로그램 "-1+2"를 둘 중 어느것으로 복구 시킬 것인가?



이 요약된 문법으로는 어느 구조로 복구 시켜야 할 지 알 수가 없습니다.

정수 문법식이 다음과 같다면 어떨까요?




이 규칙을 좀 설명하면,

음의 부호가 앞에 붙은 정수식인 경우는 2가지 밖에 없습니다.

1. 말단 자연수

2. 괄호가 붙은 정수식


구체적인 문법구조는 혼동없이 2차원의 프로그램 구조물을 복구하는데 사용되는데, 이 문법은 프로그램 복원 또는 프로그램의 문법검증이라는 과정의 설계도가 됩니다.(parsing)


의미구조 


문법구조로 1+2를 쓰는 방법을 앞에서 봤다면 1+2가 무엇을 뜻하는가에 대해서 살펴볼 때가 되었습니다.

1+2가 뜻하는 것을 2가지로 분류해서 볼수 있는데요.

1. 결과값 3을 뜻한다.

2. 1과 2를 더해서 결과를 계산하는 과정을 뜻한다.


결과값 3을 의미하는 스타일은 뜻하는 바 궁극을 수학적인 구조물의 원소로 정의해 가는 스타일 "궁극을 드러내는 의미구조( denotational semantics)"라고 합니다.

반면, 프로그램의 계산 과정을 정의함으로서 프로그램의 의미를 정의해 가는 스타일 " 과정을 드러내는 의미구조(operational semantics)" 라고 합니다.


이를 C 언어로 이를 표현해보자면, 아래와 같은 코드일것입니다.


int func_3() return 1+2;}

void main()

{

   int  r = func_3(); // denotational semantics

   printf("%d",r);

   printf("%d",func_3()); //operational semantics

}



그런데 새로운 언어인 ZX 라는 언어가 있다고 합시다. 이 언어의 표현 형식이 operational semantics만 지원한다고 한다면, 아래와 같은 동작으로 이어질 것입니다.


program start;

var x=1+2;

var y=x; // 3이 아닌 1+2라는 연산식을 넘겨줍니다.

display x;  // display 에 1+2 라는 연산식을 넘깁니다.

display y;  // display 1+2라는 연산 식을 넘깁니다.

program end;



다음 코드도 같이 확인해봅시다.

program start;

var f = ax+b;

x=2;

a = 4;

b=4;

display f;  // 4*2+4 라는 연산 식을  display에 넘겨줍니다.

a = 10;

b = 5;

display f; // 10*2+5  라는 연산 식을  display에 넘겨줍니다.

program end;



프로그래밍 언어들은 denotational semantics 와 operational semantics 를 모두 다 제공하고  있고 적절한 상황에서 사용 할 수 있도록 설계되어있습니다.



과정을 드러내는 의미구조(operational semantics)는 어느정도 수준으로 표현해야 할까요?

아주 low 하게 CPU에 전달되는 전기적 신호를 일일이 표시해야 할까요? 아니면 , 1이 1을 계산하고 2가 2를 계산하고 +가 1+2=3이라는 등식을 적용하는 것으로 표현해야 할까요?

아니면 실행되는 과정을 , 프로그램과 그 계산 결과를 결론으로 하는 논리적인 증명 과정이라고 표현해야 할까요?


의미구조를 정의하는 목표에 맞추어 그 디테일의 정도를 정하게 됩니다. 대개는 상위의 수준, 가상의 기계에서 실행되는 모습이거나, 기계적인 논리 시스템의 증멍으로 정의하게 됩니다.


과정을 드러내는 의미구조도 denotaional semantics 처럼 충분히 엄밀합니다.

1. 조립식이 아닐 수 있다: 프로그램의 의미가 그 프로그램의 부품들의 의미로만 구성되는 것은 아니다.

2. 하지만 귀납적이다 : 어느 프로그램의 의미를 구성하는 부품들은 귀납적으로 정의된다. 이 귀납이 프로그매의 구조만을 따라 돌기도 하지만, 프로그램 이외의 것을 따라 되돌기도 한다.

(이 문장들의 의미는 잘 이해 못했습니다.)





프로그램의 실행이 진행되는 과정을 큰 보폭으로 그려보자.  <<big-step semantics>>

명령문 C와 정수식 E에 대해서 각각 아래와 같이 정의합니다.



1. 명령문 C가 메모리 M의 상태에서 실행이 되고 결과의 메모리는 M'이다.

2. 정수식 E는 메모리 M의 상태에서 정수값 v를 계산한다.


이에 대한 모든 규칙을 쓰면 이렇습니다.


                                              


                                    


                       


               


               


                             



                                                          


                                                      


                                   


                                                       



이 규칙들은 논리 시스템의 증명 규칙이라고 이해해도 됩니다. 명령 프로그램 C의 의미는 임의의 메모리 M 과 M'에 대해서 MㅏC => M' 를 증명할 수 있으면 그 의미가 그 의미가 됩니다.

대개는 M이 비어있는 메모리일때 C를 실행시키는 과정을 나타내고 싶으므로, 0ㅏC=>M' 의 증명이 가능하면 C의 의미가 됩니다.

그것이 증명 불가능 하면 C의 의미는 없는 것입니다.


명령문 C : x:=1 ; y:=x+1; 

는  다음과 같은 증명으로 표현된다.



                     




이렇게 의미구조를 정의하는 방법을 자연스런, 구조적인, 혹은 관계형 의미구조라고 부릅니다.

natural semantics, structural operational semantics , relational semantics


1. 자연스런 : 추론규칙 꼴로 구성되어있고, 자연스런 추론 규칙(natural deduction rules)이라는 규칙때문에 이런 이름이 붙었다고 합니다.

2. 구조적: 계산 과정을 드러내는 의미구조 방식은 이전의 방식 보다 더욱 짜임새가 있었기 때문입니다.

 -   이전의 방식은 가상의 기계를 정의하고 프로그램이 그 기계에서 어떻게 실행되는지를 정의합니다. 이러다 보니, 기계의 실행과정중에 프로그램이 부자연스럽게 조각나면서 기계 상태를 표현하는 데 동원되기도 하고, 프로그램의 의미를 과도하게 낮은 수준의 실행과정으로 세세하게 표현하게 됩니다.

이런 방식을 구현을 통해서 정의하기(definition by implementation) , 어떻게 구현하는지 보임으로서 무엇인지 정의하기 라고 합니다.

프로그램의 구조마다 추론 규칙이 정의되고, 계산과정은 그 추론규칙들을 결합하여 하나의 증명이 되는 것입니다. 

- 의미규칙들이 한 집합을 귀납적으로 정의하는 방식이고 , 그 귀납이 프로그램이나 의미장치들의 구조를 따라 흐르기 때문입니다.


프로그램의 실행을 작은 보폭으로 정의해보자 <<small-step semantics>>


                                                                


                                                                


                                                        


           


                                        


                                          


                                                                



                                                                      


                                                                 


                                                                   


                                                                      



이 과정을 변이과정 의미구조(transition semantics) 라고도 합니다.

이 과정에서는 문법적인 부분들과 의미적인 부분들이 한데 섞이고 있습니다.


전통적으로는 것 모양을 구성하는 것과 속 뜻을 구성하는 것은 반드시 다른세계에 멀찌감치 떨어져  있어야 하지만, (Tarski :수학자가 시작한 전통)

문제될 것 또한 없습니다.


프로그램 이외의 의미장치 없이 프로그매만을 가지고도 변이과정 의미구조를 정의 할 수 있습니다.

예를 들어 메모리를 뜻하는 M이라는 프로그램이외의 의미장치 없이, 1+2+3 을 다시 쓰면 3+3 다시쓰면 6 으로 프로그램 이외의 다른 장치가 사용되지 않습니다.


위의 1+2+3 을 3+3으로 다시 6으로 다시 써 가는 과정이 프로그램의 실행과정과 같습니다.

이 경우 어느 부분을 다시 써야 하는가?, 다시 써야 할 부분을 무엇으로 다시 써야 하는가 ? 하는 것에 대한 정의가 필요합니다.


어느 부분을 다시 쓸것인가 하는 것은 실행 문맥(evaluation context)에 의해서 정의됩니다.

실행 문맥의 정의에 따라서 프로그램을 구성하다 보면, 다시 써야할 부분이 결정됩니다. 그 정의가 문법적으로 가능합니다.


실행 문맥을 가지고 있는 프로그램 K를 정의합니다.

K 안에는 [] 딱 하나가 있습니다. 그 곳이 프로그램에서 다시 써야할 부분, 먼저 실행되어야 할 부분이 됩니다.

이런 특징을 강조하기 위해서 "K[]" 라 쓰고, 그 빈칸에 들어있는 프로그램 부분 C까지 드러내어 "K[C]" 라고 표현합니다.


이 형식은 아래와 같은데요.



1. 지정문, 대입 처리에서 실행되어야 할 부분은 K는 오른쪽 식이고 x:=K 

2. 순차적 처리 구문 에서 명령문이 다음에 실행되어야 할 부분이라면, 앞의 명령문은 시행이 이미 끝났을 때 만이고, done; K 

3. 덧셈 식에서 오른쪽 식이 다음에 실행되어야 할 부분 이라면, 왼쪽 식의 결과는 나와 있어야 한다. v+K


위의 문법적인 표현을 다시쓰기 규칙을 정의하면, 아래와 같습니다.



그릭 속에서 어떻게 다시쓰이는가 하면, 즉,위의 규칙이 어떻게 동작하는지 보면,


                                                        

                                     

                      

                       

                                                    

                                                              

                                                                 




x:=1 ; y:=x+1

의 의미를 위의 문맥구조로 풀어보면,

1       ) 




2) 



3  ) 



4 )



5)   



이러한 과정으로 풀이된다.




C : 명령문

M: 메모리

K: 실행 문맥






가상의 기계를 통해서

프로그램의 의미는 어떤 가상의 기계(virtual machine)가 정의되어있고 그 프로그램이 그 기계에서 실행되는 과정으로 나타나게 됩니다.

기계에서 실행되는 과정은 기계상태가 매 스탭마다 변화되는 과정이 됩니다.


예를 들면, 정수식의 의미가 한 게계의 실행과정으로 다음과 같이 정의됩니다.


변수가 없는 정수식만을 생각해보면,



이는 소위 말하는 "스택머신 (stack machine)" 입니다. 이 기계는  스택 S 와 명령어  C 로 구성되어있습니다.



스택은 정수들로 차곡 차곡 쌓여있습니다.




명령어들은 정수식이나 그 조각 들이 쌓여있습니다.



기계 동작 과정의 한 스텝은 다음과 같이 정의됩니다.



               

  

           

<S,n.C>\quad\rightarrow\quad<n.S,C>\\

<S,E_1+E_2.C>\quad\rightarrow\quad<n.E_1\cdot E_2,+,C>\\

<n_2.n_1.S,+.C>\quad\rightarrow\quad<n.S> \quad\quad(n=n_1+n_2)\\

<n.S,-.C>\quad\rightarrow\quad<-n.S,C>


정수식 E의 의미는 <e,E> --> .... 입니다.



정수식이 아닌 우리가 프로그래밍에서 사용하는 command 형식으로 다시 정의해보면 아래와 같습니다.




그리고 , 기계 작동과정도 다음과 같이 정의 됩니다.

<S,push n.C> --> <n.S,C>

<n.S,pop.C>  --> <S,C>

<n1.n2.S,add.C>  --> <n.S,C>   (n = n1+n2)

<n.S,rev.C> --> <-n.S,C>


그리고 스택 머신 안에서 정수식들은 다음과 같은 변환됩니다.

[[n]] = push n

[[E1+E2]] = [[E1]].[[E2]].add

[[-E]] =  [[E]].rev



이러한 기계의 과정이 매우 임의적이라 생각될수 있습니다. 가상의 기계를 어떻게 디자인 하는가? 그 기계의 디테일은 어느 레벨에서 정의해야 하는가? 

그 대답은 프로그램의 의미를 정의하는 목적, 그에 따라 결정될 것이다.



[내 생각]

프로그래밍 언어가 어떤 래벨에서 정의되는 가가 프로그램이 돌아가는 환결과 시스템을 정의하는데 기준이 될것입니다.

프로그래밍 언어가 컴퓨터 system 에서 동작하는 프로그램을 작성하기 위한 용도이기 때문에, 프로그래밍 언어의 syntax나 semantic이 메모리에 한정되어 설계됩니다.

만약 프로그래밍 언어가 인터넷의 서비스나 인프라위에서 프로그램을 작성하기 위한 용도로 개발되었다고 한다면, 이때도 물론 임시적인 memory를 정의될 필요가 있지만, 훨씬 큰 의미로 접근 하게 됩니다. file open , memalloc, socket 등 local system에 대한 정의도 필요할지도 모르겠지만, 이런 레벨의 환경과 리소스를 접근하는 인터페이스는 아예 생략(숨기고)하고, openUrl, connectSite, httpRequest 등의 internet 리소스에 접근하기 위한 용도의 명령어들로 서비스 구축을 위한 언어를 설계 할 수 있을 것이라 생각됩니다.

(Java script 처럼 말이죠)



또 다른 예를 들면, 

어떤 프로그래밍 언어로 표현 하고 싶은 것이 "지구" 라는 행성이라고 하고 지구의 자원들을 다루는 프로그래램을 개발 한다고 하면, 

우리가 생각하는 프로그래밍 언어와는 모양이 많이 달라 질 수도 있습니다.

파일이나, 메모리, URL , WEB, HTTP 이런 용어나 computer level의 명령어가 아닌, 지구상의 객체들을 query 하고, 해당 객체에 메세지를 전달하고, 어떤 행동을 요구하고, 그에 대한 응답을 받고,

하는 형식의 명령어들로 정의 되지 않을까요? 


언어에는 표현 되지 않았지만, 그 밑에는 수많은 sub system들이 존재 하겠죠. GPS, WIFI, 각 객체들을 식별하기 위한 인식 장치, 모니터링 장치, 대상을 알 수없는 경우 모르는 대상에 대한 처리 시스템, 인력 관리, 수행에 필요한 지불 시스템... 등등..


하지만 언어에는 이런 서브시스템에 대한 내용이 표시 되지 않아도 된다면 더없이 편한 "지구 관리 프로그램"을 작성 할 수 있는 도구가 되겠죠.

(영화에서 많이 보던 시스템인데, 만약 프로그래밍 언어가 이런 래밸로 개발된다면, 가능 할 것도 같군요. 

현장에 요원을 투입하고, 회사를 설립하고,)

 


대개 가상 기계를 사용해서 프로그램의 의미를 정의하는 것은 프로그래밍 언어를 구현하는 단계에서 사용됩니다: 프로그래밍 언어의 번역기나 실행기를 구현할때 말이죠.

프로그래밍 언어의 성질을 굴이하거나 그 언어로 짜여진 프로그램들의 관심있는 성질을 분석할 때에 사용하는 의미구조로 사용되는 예는 드뭅니다.

가상 기계는 대개가 이와 같은 분석에서는 필요하지 않은 디테일을 드러내기 때문입니다.



기계중심의 언어로 빨리 넘어가고 싶기 때문에, 다음 내용들에 대해서는 현재는 생략 합니다. 다음에 기회가 되면 정리하도록 하겠습니다. 

- 궁극을 드러내는 의미 구조 (denotaional semantics)  

- 조립식일 수 있는 이유(의미공간 이론 : domain theory)

- CPO, 연속함수, 최소고정점



다음 >>기계중심의 언어




반응형


2. 기본기 


프로그래밍 언어가 어떤 형태로 되어있는가 를 이야기 하기 위해서는 기본적으로 사용하는 어휘들과 말하는 방식이 있습니다. 그 어휘와 방식의 기본이 수학입니다.


이런 베이스적인 부분들을 언급할 필요성이 있고, 이해해야할 필요 역시 있습니다.



귀납법 ( inductive definition)


집합을 정의하는 방법에는 조건제시법, 원소 나열법, 귀납법이 있습니다.

원소나열법 :{0,1, 참새, 비둘기}

조건 제시법 : {x|x는 1,2 의 양의 정수}

귀납법은, 

참 매력적인 분야라고 합니다. 무한한 것을 유한한 것들로 정의 할 수 있기 때문입니다.


프로그래밍 언어에서는 이 귀납법에 대한 이해가 반드시 필요합니다.


한번 귀납법에 대해서 살펴 볼까요? ( 저처럼 수학에서 멀어진지 오래된 사람들은 기억이 가물가물 하답니다.^^)


자연수의 정의를 귀납적으로 표현 하는 방법은 아래 같습니다.


방법 1


A.




3단 논법이죠?


또는

B.





B의 식은 사실 A를 살짝 수학스럽게(?) 표현한 것에 불과 합니다.

해석해보자면 "/은 자연수이다." 반드시 참인 식이어야겠죠.

분모의 정의("/#이 자연수이다.")를 만족하는 조건식 필요 조건은 "#은 자연수이다." 입니다.



방법 2

A.

음, 해석을 살짝 하자면,  "/ 가 자연수(N) 이다. /에 자연수(N)을 붙여도 자연수 이다." 정도로 이해하면 됩니다.

또는

B.





프로그래밍 언어에서는 다음과 같은 형식으로 표현됩니다.


그 예를 아래  나열 해 봤습니다.


자연수 집합



혹은 


 




스트링 집합



혹은




리스트


혹은 




정수 트리 



혹은




정수식 들의 집합








자연수 n은 정수 식이고, e가 정수식 이라면 그 앞에 음의 부호를 붙여도 정수식이고, 두개의 정수식 사이에 덧셈 부호나 곱셈 부호를 끼워넣어도 정수식이다.





간단한 프로그래밍 언어 표현


 


프로그래밍 언어에 대한 이야기 중인데 너무 귀납법이라는 것에 치우치게 되는것 같아서 나중에 나올 내용인데 미리 적어봤습니다. 


이 식을 처음 봤을때, 느낌이 강렬 했거든요. "아 이래서 프로그래밍 언어가 수학에서 나왔다라고 할 수있겠구나." 라는 생각을 했었으니까요.

정확한 표현은 아니지만, 이렇게 해석해보면 뭘 의도하는 표현식인지 이해가 쉬울 것입니다.

1. s -> x  syntax(또는 식) x  로 되어있다.

2. x는 e (expression : 식, 또는 명령어, 또는 상수 등등...) 이다.

3. x는 s;s 일 수도 있고, if e s s 일 수도 있고, print e 일 수도 있다.

4, s;s 는 각 식은 ; 으로 연결된다. (여러 언어들에서 문장의 맨 마지막에 ; 붙이는 것 말이죠)

5. if e s s  if 명령문은 e가 참이면 첫번째 s를 수행하고 두번째 s를 수행한다.

6. print e  e의 결과 값을 출력한다.


이렇게 해석해 보면, 프로그래밍 언어의 규칙을 표현한 것으로 보이죠?




형식논리

선언논리에서 생각하는 논리식 f는 다음의 귀납법칙으로 만들어 지는 집합의 원소이입니다.

논리식 이라고 하면, 전자공학에서 배우는 논리식을 생각 하실 수 있을 것입니다. 맞습니다. 바로 그것을 의미하는 것입니다.

그 논리식을 귀납법의 규칙에 따라 적으면 다음과 같습니다.








논리식을 다음과 같이 표현 방법을 바꿔봅시다.








implication :  ⊃ → 


implication 은 '논리 승' 이라고 표현하기도 하는데 아래와 같은 참,것짓 테이블을 가집니다.




A

 A

T





추론규칙


이제까지 참인지 거짓인지에 대한 표현 방법을 배웠다면, 논리식이 참인지 판별하는 방법에 대해서 고민해볼 차례인것 같습니다.

물론, 논리식의 의미를 정의한대로 따라가다보면 결과를 알수 있습니다. 음... 참, 혹은 거짓, 

논리식을 따라가면서 본다는 것이 결국, 프로그램을 돌려보고 그 프로그램이 참의 결과를 내는지 판별하는 것과 같습니다.



그런데, 혹시,,, 프로그램을 돌리지 않고 알아내는 방법은 없을까요? 논리식의 의미를 계산하지 않고 참인지 거짓인지 알수는 없을까?

논리식의 모양만을 보면서 참인지를 판별할 수는 없을까?


이런 규칙을 증명 규칙, 또는 추론 규칙이라고 합니다.




            





                       



                               




                          





                                



(감마, f) 쌍들의 집합을 만드는 규칙들이 위와 같습니다. 이는"감마에 있는 모든 논리식들이 참이면 f는 참" 이라는 의미를 갖습니다.

대개 형식논리에서는 "(감마, f)" 를 "감마 ㅏ f" 로 표기합니다.


이렇게 위의 식들을 다시 표시해보면,


(R1)     (R2)    (R3) (R4)




(R5)   (R6)




(R7)    (R8)



(R9)  (R10)



 

 

(R11) (R12)


  


의 증명을 위의 증명규칙으로 만들면 아래의 나무 구조가 됩니다. 

  





문법 구조와 의미 구조 







안전한 혹은 완전한


위의 규칙들이 만들어 내는  의 f는 모두 참인가? 반대로, 참인 식들은 모두 위의 증명규칙들을 통해서  꼴로 만들어지는가?

첫 질문에 예라고 답할 수 있다면, 우리의 규칙은 안전하다고 , 믿을만 하다고 한다.

둘째 질문에 예 라고 답할 수 있으면 유리의 규칙은 완전하다고, 빠뜨림이 없다고 한다.


증명 규칙들이 안전하기도 하고 완전하기도 하다면, 참인 식들만 빠짐없이 만들어 내는 규칙이 된다.



안전하냐 완벽하냐는 기계적인 증명규칙들의 성질에 대한 것이다.


그 기계적인 규칙들의 성질은 오직 우리가 생각하는 의미에 준해서 결정될 수 있다.


프로그램에서 그 겉모양(문법)과 속내용(의미)은 대개 다른 두개의 세계로 정의되고, 프로그램을 관찰하고 분석하는 모든 과정은 기계적으로 정의된다.

컴퓨터로 자동화하기 위해. 그 기계적인 과정들의 성질들(과연 맞는지, 무엇을 하는 것인지) 등에 대한 논의는 항상 그 과정의 의미나 결과물들의 의미를 참조하면서 확인된다.





다음 >> 모양과 뜻




반응형

 

1. 컴퓨터는 프로그래밍 언어에서 시작되었다!!.

 

대부분 프로그래밍 언어란 무엇인가요? 라는 질문에, 컴퓨터에서 돌아가기 위해 만들어진 언어 라고 대답할 것입니다.

 

맞는 예기죠. 당연한 것이죠.

 

그런데 컴퓨터는 왜 만들어 졌나요? 라고 질문한다면.... 어떻게 대답해야 할까요?

필요에 의해서? 그럼 무슨 필요에 의해서 일까요?

 

이 질문에 답하기 위해서는 컴퓨터의 시초로 거슬러 올라가서 한계단 한계단 밟으면서 컴퓨터 사이언스 역사를 살펴봐야 합니다.

 

결론 부터 예기하고 이것 저것 살펴보죠. 

 

"컴퓨터는 프로그래밍 언어를 구동시키기 위해 만들어 졌습니다."

즉, 컴퓨터 보다 프로그래밍 언어가 먼저 만들어졌고, 이를 확인하고 증명하는 과정에서 컴퓨터가 만들어 졌습니다.

정말이냐구요?

 

자.간략히 프로그래밍 언어의 시초에 대해서 언급하겠습니다.

1920년대 당대에 위대한 수학자 중 하나였던  힐베르트(다비트 힐베르트) 할아버지가,"모든 참인 명제를 증명할 수 있는 공리계가 있다" 라는 가설을 만들었습니다.

당시 힐베르트는 기하학(유클리드 기하학)을 공리화 하였고, 공간을 정의하여 함수해석학의 기초를 닦았다고 합니다. 아마 이런 과정에서 힐베르트는 위와 같이 모든 것은 증명할 수 있는 어떤것이 있다는 가설을 세우게 되었습니다.

이 의미는 간단하게 "모든 수학적 명제는 참인지 거짓인지 자동으로 계산 할 수 있다."라는 것이었습니다.

 

1930년대 초 쿠르트 괴델이 불완정성 정리를 증명하면서, 수학의 형식화가 태생적으로 한계가 있다는 것을 증명 했습니다. <불완전성의 원리>

1936년 쿠르트 괴델의 증명을 보고 , 앨런 튜링은 다른 형식으로 이를 증명하게 됩니다.

이때 나온게 튜링 머신이라는 것인데, 초보적인 컴퓨터라고 이해하는 사람들이 있는데, 초보적인 기계어라고 이해 하는것이 더 맞을 것입니다.

 

튜링 머신 자체가 실제로 존제하는 기계가 아니라 이론이었고, 표현 자체가 기호와 수학에 의해 정리되어있기 때문입니다.<튜링머신>

 

이와 비슷한 시기인 1930년대 알론조 처치가 람다 대수의 형식을 제안하였습니다.<람다 대수>

람다대수는 계산이론, 언어학 등에 중요한 역할을 하고, 프로그래밍 언어 이론의 발전에 토대가 되었고, 리스프, 함수형 프로그래밍 언어는 람다 대수로부터 직접적인 영향을 받아 탄생했으며, 단순 타입 람다 대수는 현대 프로그래밍 언어의 타입 이론의 기초가 되었습니다.

 

이와 같이 1930년대가 Computer라는 정의가 처음 시작되던 시기인데, 힐베르트의 가설이 불가능 하다는 것을 증명하기 위해서 고안된것이 컴퓨터의 시초이고 프로그래밍 언어의 시초입니다. 

힐베르트의 가설은 참, 매력적인데, "모든 참인 명제를 증명할 수 있는 공리계가 있다" 는 것이 만약 참으로 증명되었다면, 현재의 컴퓨터는 아마도 더 완벽한 것이었을 거라고 생각됩니다.

 

그러나 아이러니하게도, 우리는 컴퓨터는 이 명제가 틀렸다는 증명을 위해 고안되었습니다.

(이것이 불행일까요? 인간이 불완전 하다는 것을 생각해본다면, 뭐 나쁘진 않다고 봅니다. 오히려 발전의 가능성이 더 있다고 생각해 볼수 있겠죠... 뭔가 철학적인 ...)

 

때문에, 이런 거짓인 명제를 포함한 채로 Computer science 와 Computer engineering 은 발전해왔고, 그래서 지금도 여전히 불완전 한것은 맞습니다.

 

아무튼, 튜링이 튜링머신이라는 가상의 기계를 만들고, 그 기계가 동작하는 방식을 설명했는데, 기계라고 표현했지만, 기계적 장치는 없고, 이는 현재 프로그래밍 언어가 컴퓨터를 동작시키는 방식을 설명한 것이었습니다..

(지금의 CPU,RAM, 출력장치 등의 기본 개념이 이때 제안 되었다고 봐도 됩니다.)

 

 

이런 일련의 사건들을 보면, 컴퓨터가 없던 시절에 프로그래밍 언어의 시초가 되는 튜링 머신이나, 람다 대수식 등이 등장했고, 이후 이 튜링머신이나 람다 대수를 동작시켜볼 수 있는 장치로 컴퓨터가 고안되고 발전 했다 라고 할 수 있습니다.(실제로 그랬구요.)

 

개인적으로는 별로 생각해보지 않았던 부분이고, 누군가 질문했다면, "글쎄요.." 라고 대답 했을 것 같았는데, 막상 여기저기 찾아보고, 조사해보니, 프로그래밍 언어와 컴퓨터라는 것에 대해서 새로운 시각을 갖게 되는 것 같습니다.

 

 

힐베르트의 가설이 잘못된 것임을 증명한, 괴델(리커시브 함수), 튜링( 정지 문제), 처치( 결정 불가능 문제) 는 본질적으로 같은 것이라는 사실도 재미 있습니다.

 

 

 

 

다시 한번, 컴퓨터는 왜 만들어 졌나요?  라고 질문 한다면,

이제 우리는 프로그래밍 언어를 돌리기 위해서 컴퓨터가 만들어 졌어요.. 라고 답할 수 있습니다.

 

 

 

이러면 날 수 있을 꺼라 생각 했었던 시절.

 

 

<< Appendix >>

 

튜링기계

 



"...무한한 저장공간은 무한한 길이의 테이프로 나타나는데 이 테이프는 하나의 기호를 인쇄할 수 있는 크기의 정사각형들로 쪼개져있다. 언제든지 기계속에는 하나의 기호가 들어가있고 이를 "읽힌 기호"라고 한다. 이 기계는 "읽힌 기호"를 바꿀 수 있는데 그 기계의 행동은 오직 읽힌 기호만이 결정한다. 테이프는 앞뒤로 움직일 수 있어서 모든 기호들은 적어도 한번씩은 기계에게 읽힐 것이다"
 - 튜링 ,1948, p.61

 

 

 

1935년 봄, 케임브리지 킹스 칼리지의 젊은 박사 과정 학생이었던 튜링은 한 과제에 직면했다. 그는 논리학자 뉴먼의 강의에 자극을 받았으며, 결정 문제에 대한 괴델의 연구에 대해 알게되었다. 뉴먼은 '기계적'이라는 단어를 사용했으며, 1955년 튜링의 부고에 뉴먼은 다음과 같이 기술 했습니다.

 

 



" '[기계적] 과정이란 무엇인가?'라는 질문에 튜링은 '기계로 할 수 있는 것' 이라는 답을 내 놓았다."
- Gandy, p.74

 

 

테이프 튜링기계

 

 

M= \left \langle Q, \Gamma, b, \Sigma , \delta , q_{0}, F \right \rangle

 

  • Q는 유한하고 비어있지 않은 상태들의 집합
  • \Gamma는 유한하고 비어있지 않은 기호와 알파벳들의 집합
  • b\in\Gamma는 비어있음을 알려주는 기호 (테이프 위에서 유일하게 무한하게 나타날 수 있는 기호)
  • \Sigma \subseteq \Gamma \backslash \left \{ b \right \}는 입력가능한 기호들의 집합
  • q_{0} \in Q는 초기상태
  • F \subseteq Q는 최종상태, 또는 수락 상태
  • \delta : Q \backslash F \times \Gamma \rightarrow Q \times \Gamma \times \left \{ L, R \right \}는 부분함수

 

 

 

 

 

 

람다 대수 Lambda Calculus

 

 

 

  • 알파-변환(α-conversion): 속박 변수를 바꿈
  • 베타-축약(β-reduction): 식의 인수에 함수를 적용
  • 에타-변환(η-conversion): 외연성을 통해 축약 (겉으로 보이는 행동이 같은 함수는 동일 함수로 간주함)
 
 

 

 

 

다음 >> 기본기

 

반응형

일탈!!!!



인생에서는 가끔씩 일상에서 벗어나서 자유로움을 느껴보고 싶은 로망을 가지고 있는 단어이죠.


매일 반복되는 일상에서 벗어난다는 상상만으로도 몸에서 생기가 느껴지고 짜릿한 무언가가 생기죠.




하지만, 이 일탈이 통계학이나 소프트웨어에서는 상당히 중요하면서도 고민을 안겨주는 부분이랍니다.

"어떤 생물의 분포를 분석하기 위해 수집한 데이타에 유독 몇개의 데이타가 해당 분포를 벗어나 있다."

이러면, 통계학에서는 해당 데이타를 포함시켜서 분석할 것인지, 제외 시킬것인지 깊은 고민을 해야 할 것입니다.




소프트웨어에서는 항상 일정한 시간이 걸리던 작업이, 특수한 경우에 배의 시간이 걸린다. 라든가,

일정한 메모리만 사용되다가 갑자기 2배이상 급격히 증가한다든가, 하는 경우들이 바로 일탈이죠.

이러한 사건이 발생하면, 코드를 분석하고 디버깅해서 원인을 파악해야 하는 고달픔이 다가 오는 것이죠.



인간으로서 일탈을 꿈꾸면서 개발자로서는 이런 일탈을 두려워 하게 되네요.




반응형

프로그래밍 역사에서 빼놓을 수 없는 C와 C++ 언어에 대해서 얘기해 보고자 합니다.

1. C에서 C++



C 언어는 벨 연구소에 일하던 데니스 리치가 B언어(Bell 연구소에서 따와서 B라고 명명함)를 개선하여 만들었습니다.

이당시 상황은 Unix에서 사용할 프로그래밍 언어가 필요한 상황이었고 이 언어가 개발됨에 따라 Unix의 바탕이 되는 프로그램들은 거의 모두 코드가 C언어로 만들어지게 됩니다.

리눅스 역시 Unix를 모델로 만들어지면서, 리눅스의 바탕 역시 C언어로 만들어지게 됩니다.

(리눅스는 리누스 토발즈가 헬싱키 대학원생 시절 교수가 교육용으로 만든 미닉스의 기능에 만족하지 못하고 새로 개발하게 된 OS입니다.)

때문에 C언어가 크게 확산되고 발전된 것은 Unix나 Linux같은 OS의 개발과 확산에 의한 영향이 크다고 봅니다.

C 언어는 이처럼 System programming에 적합한 언어로 자리 잡았고, 현재까지도 Linux나 Unix는 사용되고 있는 OS이고 그 외의 여러가지 Kernel 들이나, OS들 역시 System level의 code들은 거의 C언어로 이뤄졌다고 볼 수 있습니다.


그 후 S/W OS의 발전은 system S/W에서 점점 Ui, Network, Service, Game, Graphics, Application 등 다양한 영역 들로 넓혀가게 됩니다.

그중 대표적인 것이 GUI 환경이죠. 

이 GUI 환경은 Zerox 연구소에서 시작이 되어, Macintoch, MS window, OS/2, Nextstep  등등 수많은 OS들이 GUI 환경을 제공하게되고, 사용자 입력 역시 keyboard에서 Keyboard+mouse 로 바뀌게 되었죠.

이를 우리는 보통 Window System이라고 부릅니다. 아무튼 이로서 프로그램들은 더 복잡하고, 인터랙티브 한 기능들을 제공해야 했습니다.


기존의 어떤 특정 기능을 처리하는 방식의 절차적인 프로그래밍 방식으로는 이런 환경에서 다양한 사용자 요구사항과 여러가지 입력들을 체계적으로 처리하기에는 무리가 있었습니다.

이때부터 나오기 시작한 개념들이, 객체지향 설계, 이벤트 기반 방식(Event driven), 등등의 개념이 나오게 됩니다.

그러면서 실제로 객체지향 설계를 받아들일 수 있는 시뮬라 67이라는 언어가 만들어졌고 , 이 언어는 다른 언어들에 영향을 미치게 됩니다.

C 언어 역시 객체지향 패러다임을 적용하기 위해서 C++이라는 C언어 트랜슬레이터가 개발됩니다. 향후 C++은 단순 트랜슬레이터가 아니라 독립된 언어로 그 과정에서 C언어와의 Code 호환성을 유지하면서 멀티 페러다임 언어로 자리잡게되죠.


즉 C++은 C언어가 복잡하고 고도화 되어가는 S/W 환경에서 객체 지향 설계라는 획기적인 S/W설계방식을 받아들이 위해 만들어졌다고 보시면 되겠습니다.


 !!!!:  C++ 언어는 초창기 C언어의 트랜슬레이터로 구현되었었고, 때문에 C++로 작성된 코드는 C로변환된뒤 컴파일이 되었습니다.

(참고 wikipedia : ko.wikipedia.org/wiki/C++) 

그래서 지금도 C->C++로의 환성이 유지되고 있고 C언어의 문법을 그대로 받아들이고 있습니다. 즉 C++은 객체지향 언어라기 보다는 절차지향 언어에 객체지향 패러다임이 추가된 멀티 패러다임 언어라고 하는게 좀더 잘 표현한 것이라 할 수 있겠네요.(여러 자료들에서도 이런 표현을 사용하고 있죠.)

C에서C++로 확장된 같은 지류의 언어라고 인식하는 것도 이런 이유일 것입니다.


2. "C++이 느리다." 


사람들이 C 언어를 다른 언어와 비교할때 주로 C++과 비교를 많이 하는 반면 Java를 같은 선상에 놓고 비교하지는 않습니다. 

이유는 언어의 성격 상 C는 system programming을 하기 위한 것, Java는 application 개발 또는 application programming을 하기 편한 것으로 인식하고 있기 때문에 이에 대한 비교는 별로 하지 않는 반면, C / C++에 대해서는 같은 선상에 놓고 비교할 때가 많습니다.


이런 비교의 바탕은 사실상 System level의 programming을 하는 쪽에서 가 많이 나오는 편입니다.

(C++로 System software를 개발할 필요성이 있는가를 염두해 둔것 이라고 봐야 할까요?, 아니면 C에 익숙해있는 System S/W개발자들은 C++언어가 못마땅한걸까요?) 

비교의 주된 내용이 모두 Performance에 맞춰져 있습니다. 즉, C++은 C보다 느리다는 것입니다.


이런 성능 를 하게 되면, 그리고 왜 일부 사람들은 C가  C++ 보다 성능이 좋다고 생각하는 것일까요?

C++언어는 객체지향 언어가 아니라 멀티 패러다임 언어입니다. 즉, 용도에 맞춰 코드를 작성 할 수 있기 때문에, 퍼포먼스를 해치는 그런 코드는 굳이 객체화 하거나 할 필요가 없다는 것인데 C++은 무조건 객체로 만들어야 한다는 선입견이 적용된 경우가 아닌가 싶습니다.


일단 성능 얘기가 나왔으니 이야기를 이어가 보겠습니다. 성능 의 주요 주제는 virtual function에 대한 얘기를 합니다.

자, 그럼 아래의 code를 테스트 해보면 어떤 결과를 얻을까요?
loop 1000만번 을 돌면서 tempbuf에 hello라는 단어를 copy 하는 코드입니다.
조금이라도 공정하게 테스트 하기 위해서, test_tool()이라는 동일한 함수를 부르도록 하였습니다.
virtual funtion 테스트는 class 는 inherit 해서 virtual 함수를 상속받아 구현한 코드이고,
function pointer 테스트는 일반적으로 function pointer를 사용할 때 null 채크를 하기 때문에 이 코드를 포함하여 넣었습니다.
  - 이 부분에서 논란이 있을 수 있는데 제 생각은 이렇습니다.

C++ compiler는 compile time에 virtual function을 syntax를 검사하는 형태로  pure virtual 함수를 사용하는 경우를 필터링 해줍니다.

하지만 C의 경우 function pointer의 안정성 검사가 이뤄지지 않죠. 따라서 실제 사용될때는 사용자에 의해 어디선가 해야 합니다.

때문에 아래와 같이 가장 기본적인 null 채크를 추가하였고 일반적으로 실사용에서 함수 포인터를 사용한다면  꼭 해야하는 기본적인 로직이라 생각해서 넣은 구문입니다.

 (사실 C의 function pointer 임의의 function으로 대처될 수도 있어서 c++ virtual function에 비해 risk가 더 있기 때문에 실제 현업에서는 더 많은 컨디션 체크를 하고 있다고 생각됩니다.)


돌려보셨나요? 결과는 어떤가요?

#include <stdio.h>
#include <sys/time.h>
#include <iostream>

using namespace std;

class _ElapsedTime
{
public:
    _ElapsedTime(const char* tag = NULL)
    {
        struct timeval tv;

        tagName = tag;

        gettimeofday(&tv, NULL);
        start_time = (long long)tv.tv_sec * 1000000LL + (long long)tv.tv_usec / 1LL;
    }

    ~_ElapsedTime(void)
    {
        long long end_time;
        struct timeval tv;
        gettimeofday(&tv, NULL);
        end_time = (long long)tv.tv_sec * 1000000LL + (long long)tv.tv_usec / 1LL;

        fprintf(stderr, "%s: elapsed time: %lld us\n",tagName,  end_time - start_time);
    }


public:
    long long start_time;
    const char* tagName;
};



char tembuf[2048];

void test_tool()
{
  sprintf(tembuf,"%s","hello");
}


// test for virtual function

class test
{
public:
  virtual void test_func()=0;
};



class test_inherit : public test
{
public:
  virtual void test_func();

};
void test_inherit::test_func()
{
  test_tool();
}


// test for function pointer

typedef void (*test_func)();

void test_try_func()
{
  test_tool();
}



int main()
{

  test_inherit test;
  test_func func_pointer = &test_try_func;

  int i = 0;
  const int loop_end = 10000000;

  sleep(1);

  {
    _ElapsedTime try1("function pointer");
  for(i=0;i<loop_end;i++) // test function pointer
    {
      if(func_pointer)func_pointer();
    }
  }

  {
    _ElapsedTime try2("virtual function");
    for(i=0;i<loop_end;i++)  // test virtual function
    {
      test.test_func();
    }
  }

  return 0;
}



여러번 돌려봤을때 아래와 같은 결과를 얻었습니다. 위의 if 문이 큰 영향을 주더군요.

if 문이 빠졌을때는 비슷합니다.


&"warning: GDB: Failed to set controlling terminal: \353\266\200\354\240\201\354\240\210\355\225\234 \354\235\270\354\210\230\n"

function pointer: elapsed time: 41011 us

virtual function: elapsed time: 31175 us

Debugging has finished


Debugging starts

&"warning: GDB: Failed to set controlling terminal: \353\266\200\354\240\201\354\240\210\355\225\234 \354\235\270\354\210\230\n"

function pointer: elapsed time: 39163 us

virtual function: elapsed time: 31143 us

Debugging has finished


Debugging starts

&"warning: GDB: Failed to set controlling terminal: \353\266\200\354\240\201\354\240\210\355\225\234 \354\235\270\354\210\230\n"

function pointer: elapsed time: 39903 us

virtual function: elapsed time: 31362 us

Debugging has finished


Debugging starts

&"warning: GDB: Failed to set controlling terminal: \353\266\200\354\240\201\354\240\210\355\225\234 \354\235\270\354\210\230\n"

function pointer: elapsed time: 35646 us

virtual function: elapsed time: 31281 us

Debugging has finished


두번째 성능 이슈는 객체의 생성 소멸에 따른 비용 문제입니다.
C++ 로 class를 작성하게 되면, 생성과 소멸시 constructor 와 destructor 가 불리게 되고, 함수에서 객체를 return 하거나 operator에 의해 임시객체사 생성, 임시객체 복사, 삭제가 발생합니다.

C++11 에서는 r-value reference가 지원되어 move semantic 등을 통해 임시객체에 대한 overhead는 약간은 해결했지만, 기본적으로 객체의 생성 삭제 부분의 오버해드는 존재하게 됩니다. 이는 퍼포먼스에 영향을 주기 마련입니다.


특히 Integer, Float, Char과 같은 basic type들을  class로 정의해서 사용한다면, 여러가지 operator들을 제정의 해서 사용하게 되고, 객체를 return하게 될때 객체 copy가 일어나고, 만약 임시객체를 만들게 되면,임시객체 생성, 소멸, 복사가 일어나게 오버해드가 발생합니다.

c++ 가 그동안 이런 측면에서 어택을 받아왔던 부분들 일것입니다. 


기본 타입(primitive type)에 대해서는 언어의 차이에 대해서 오버해드는 없겠지만, 객체를 만들어서 다루는 부분에 대해서는 분명 오버헤드라고 볼 수 있습니다.

이 부분은 객체 라는 설계요소가 포함되어 발생하는 부분으로 같은 설계를 놓고 본다면, C나 C++이나 아마 크게 차이나지 않을 것 입니다.

예를 들면 이런거죠.


// A를 다루는 List 형태의 manager 설계

struct A{...};

struct Node{struct A a; struct B* pNext;};

struct Manager{ struct A* pcurrent; struct Node* pList; } ;


void clear_A(A* a);


Manager* create_manager();

//destroy 함수는 모든 node를 살펴보면 삭제하는 코드.

void destroy_manager(Manager* p){

   if(p == NULL) return;

   p->pcurrent = NULL;

   struct Node*pNode = pList;

   while (pNode){

      struct Node* pCur = pNode->pNext; 

      pNode= pNode->pNext;

      clear_A(&pCur->a);

      free(pCur);

   }

}


class A{...  ; A(); ~A();};

class Node{

public:

    A a;   Node* pNext; 

    Node(); 

    ~Node(); 

    Node* getNext();

};

class Manager{ 

    A* pcurrent;     Node* pList; 

    Manager(); 

    ~Manager();

}; 


Manager::~Manager(){

  pcurrent =  nullptr;

  Node* pNode = pList;

  while(pNode){

    pCur = pNode;

    pNode = pNode->getNext();

   delete pCur;  <-- 이 부분이 왼쪽의 clear_A() free() 를 호출한 부분과 같은 부분

  }

}




이 다음 주제가 설계를 반영하는 부분에 대한 얘기 입니다.



3."C로도 OO를 구현할 수 있다."


C와 C++의 비교 내용은 주로 Performance 대한 이슈 또 하나가 C로도 객체지향을 구현할수 있다는 의견이 주 내용 일 것입니다.

이를 단순히 표현하면, "C 는 C++보다 빠르고 C로도 객체지향적으로 구현할 수 있다" 가 되죠.

그럼 아키텍쳐링 측면에서 좀 살펴봐야 할것 같습니다.


요즘은 어플리케이션이든 아니면 서비스 모듈이든 상당히 복잡해졌고 규모가 큰 편입니다.
그래서 소프트웨어의 구조적인 측면을 중요하게 생각합니다. 그리고 S/W를 소개할때에도 이런 부분들이 잘 반영되도록 해서 구조 설명을 해야되어있습니다. 때문에 설계는 S/W에서 빠질 수 없는 부분입니다.

객체지향 프로그래밍이라는 개념이 나오고 나서 S/W의 설계 분야는 OO 기반으로 상당한 발전들이 있었습니다.
Component, OOAD, Design pattern 등이 대표적인 예인데, 이러한 개념들은 UML 과 더불어 강력한 프로그래밍 설계 툴이 되었습니다.

C, C++ 은 모두 절차적 프로그래밍 언어이긴 하지만 C++ 객체지향 페러다임을 담고 있는 언어라고 얘기 합니다.
그래서 C++는 UML이나 객체지향 설계의 내용을 코드로 옮기기가 쉽습니다.
그렇기 때문에 설계와 코드를 동일한 퀄리티로 유지 할 수 있고, S/W의 구조적인 문제점들을 파악하거나 개선 방향을 잡는데 매우 편리합니다.


이런 얘기가 나오면, "C로도 OO 개념을 적용할 수 있다" 라고 합니다.

하지만 언어적인 한계가 있기 때문에 아무리 구조적으로 잘 작성한다 하더라도, OO를 지원 하는 언어와 동일한 편의성을 제공 할 수 없습니다. 안타까운 현실이죠.

예를 들면, 아래와 같이 구조를 잡아나가면 c 나 c++이나 다를게 없다고 생각 할 수도 있습니다.
그렇지만 자세히 뜯어보면 그렇지 못하다는 것을 알 것입니다.

<test.c>

typedef struct
{
  int (*getter)();
  int (*setter)();

  int value;
}CStructPrototype;


void test_struct()
{
  CStructPrototype st;
  st.getter();  <- 올바른 동작일까요?
  st.setter();

}



실제 정상적인 코드를 작성하려면 아래와 같이 되어야 할 것입니다.


<test.c>


typedef struct _CSTRUCT
{
  int (*getter)(struct _CSTRUCT* self);
  void (*setter)(struct _CSTRUCT* self, int value);

  int value;
}CStructPrototype;

int CStructPrototype_Getter(struct _CSTRUCT* self)
{
  return self->value;
}

void CStructPrototype_Setter(struct _CSTRUCT* self, int value)
{
  self->value = value;
}

void CStructPrototype_Construct(CStructPrototype* st)
{
  st->getter  = &CStructPrototype_Getter;
  st->setter  = &CStructPrototype_Setter;

  st->value = 0;
}

void test_struct()
{
  CStructPrototype st;
  CStructPrototype_Construct(st);
  st.getter(&st); 
  st.setter(&st, 10);

}



자, 이러면 된걸 까요?

멤버 함수 처럼 보이는 getter와 setter에 인자로 자기자신(st&)이 들어가도록 되어있습니다.


여전히 문제는 있습니다. 다음 코드를 봅시다.



void test2_struct()

{
  CStructPrototype st;
  CStructPrototype st2;
  CStructPrototype_Construct(st);
  st.getter(&st);
  st.setter(&st, 10);
  st.setter(&st2, 10); // (A)<- st2의 data가 변경됨. 
}


(A)를 보면 st의 member함수처럼 보이는 setter에 st2가 들어갑니다. 그래서 st2의 내부 변수이 value가 변경되게 됩니다.

setter의 구현체인, CStructPrototype_Setter 의 내부에서도 인자로 받은 st가 this 인지를 확인 할 방법도 없습니다.

즉 function pointer는 member함수가 될수 없는 이유입니다.



또, 만약 상속을 생각한다면,
 getter와 setter의 prototype이 좀 바뀌어야 합니다.

  int (*getter)(void* self);
  void (*setter)(void* self, int value);

그리고 구현부에서 void* 를 casting해서 사용해야 합니다.

함수 overriding 구현을 위해서는 더욱 더 복잡해지기 마련이고, 이를 표기할때 단순화 시키기 위해 macro를 작성해 사용한다면, 코드의 가독성이 떨어지게 되고,  함수 pointer와 연결된 함수를 직관적 알아보기 힘들게 됩니다.



그외 ,

operator overloading 과 function overloading 등은 방법이 없습니다.

또 exception handling 역시 지원되지 않기 때문에 error handling 방법이 유일하게 return값을 확인 하는 방법과, 모듈 내부에서 last error 와 같은 로직을 추가로 해결 할 수 밖에 없습니다.

(exception에 대한 안좋은 선입견을 가지신 분들도 있을 수 있는데요. exception은 결함 내성(fault-torelant) 소프트웨어를  쉽게 작성할 수 있도록 도와주는 것입니다. - 참고 : 왜 예외를 쓰는 게 좋을까요? 

 : 논점에서 좀 벗어난 얘기라 서 읽는데 혼란을 줄 수도 있는 내용이라 gray 로 덮었습니다.


오히려, 이런 코드는 

CStructPrototype_Getter(&st);

CStructPrototype_Setter(&st, 10);

함수 자체를 호출하도록 하는 것이 훨씬 C 스럽고 안정성을 높이고 시인성을 높이는 것입니다.


그런데 종종 위와 같이 struct과 function pointer를 이용해서 OO 구조를 잡는 코드들을 볼수 있습니다.

왜 C programming을 할때 위와 같이 struct과 function pointer를 가지고 class 와 비슷한 구조를 갖추려고 할까요?
제 생각에는 이런 프로그래밍을 하는 사람은 아마도 OOP를 알고 있는 사람일 것입니다.

그래서 C언어 밖에 사용할 수 없는 환경에서 OO를 흉내내고 싶어 하는 것 입니다.

(물론 그렇지 않은 경우도 있습니다 만, 때때로 과도하게 이런 흉내를 내는 코드를 볼때가 있습니다.)


만약 그러시다면, C++을 사용하십시오.


<test.cpp>


class CppClsss
{
public:
   int getter(){return value;}
   void setter(int v){ value = v;}
private:
int value;
}

void test2_struct()
{
  CppClass st;
  st.getter();
  st.setter(10);
}


더 이상 struct를 class 처럼 꾸밀 필요도 없고, 아래와 같이 OO로 작성된 module 들은 객체의 member function을 사용하면 되고,
C function 제공하는 것들에 대해선 C function을 사용하면 되기 때문에, C 개발자 입장에서는 C 사용성이 확대되었다 정도로 인식만 하면 되지, 기존의 로직 구조를 변경할 필요는 없습니다.




자 그럼 다음 질문, 왜 C++로 작성하지 않을까요?
이 질문이 C 언어를 주로 다루는 개발자 분들은  C로 작성된 모듈을 모두 OO 기반으로 바꾸지 않는가? 라고 오해의 소지가 있습니다.

Refactoring 관점이나 Architecturing 관점에서 접근하더라도 C로 코딩 되어 있는 코드들을 객체 기반으로 코드로 모두 바꾼다는 것은 잘못된 설계가 될 가능성이 있고 모듈의 동작 변화도 발생할 가능성이 높습니다. 그만큼 리스크가 있는 작업입니다.

이런 리스크를 안고 굳이 기존의 코드를 OO 기반의 코드로 변경할 필요는 없다고 봅니다.


잠시 프로그래밍 언어와 용도에 대해서 잠시 고민해 봅시다.
C로 되어있는 system call을 처리하는 모듈이 있는데, 이 모듈은 system call에 의한 interrupt 루틴이 함수 하나로 작성되어있고,  runtime 에 system service 에 등록 되도록 되어있습니다.

그럼 C++ compiler로 교체 함으로 해서 이런 간결하고, 뛰어난 코드를 객체로 재 정의 하고 객체에 interface를 만들고, service call에 등록하기 전에 객체 instance를 생성하고, 연결해주는 static 함수를 service에 등록해야 할까요? 이런 코드들은 비록 객체로 바뀌었다고 하지만, 여전히 C로 구현했던 내용과 큰 차이가 없을 것이고 크게 개선될 것도 없을 것입니다.

C++ compiler로 변경 했다고 해서 기존의 코드를 모두 새로운 객체지향 페러다임으로 변경할 필요는 없다고 생각합니다.
대신 C 효율적으로 대처하지 못했던 부분들에 대해서 C++ 이 보완해 줄 수 있다는 것이 가장 중요한 핵심인것 같습니다.


4.Framework


가끔 "이 프래임워크는 Java로 되어있어", "Facebook 은 Social service framework 이다" 이런 표현들을 들어봤을 것입니다.


framework 비슷한 용어로 우리는 platform이란 단어를 사용합니다.

Platform은 뭐고 Framework은 무엇일까요? 이것들은 비슷하게 들리는데 어떤 차이가 있는 것일까요?


Platform이라는 용어는 Hardware architecture ,Operating system, runtime library를 포함한 용어로 많이 사용되고 있습니다. 따라서 어떤 디바이스가 동작하기 위한 모든 요소를 합쳐서 platform이라고 부릅니다.


Framework은 특정 platform위에서 어플리케이션이나 서비스를 개발할 수 있도록 제공되는 logic을 기능화하여 제공하는 것을 의미합니다.  library와의 차이가 뭐냐? 라고 한다면,

Library는 흔히 API들의 집합을 의미하는 것으로 그 이상의 의미는 별로 없습니다. 그러나 framework은 이런 library와 platform의 기능 들을 장 정리해서 특정 기능이나 서비스를 구현하는데 필요한 business logic을 제공하는 것을 입니다.


즉, 단순 API set이 아니라, logic을 포함한다고 보시면 됩니다. 잘 이해가 안 가나요?


쉽게 비교 표현 하자면, Windows platform, Graphics library, Ui framework 이렇게 봅시다.

Windows 는 desktop 기기나 mobile 기기를 구동하기 위한 architecture와 OS 를 담고 있기 때문에 platform이라고 부릅니다.

그 Windows 위 에는 수많은 library 들이 있습니다. socket, network library , openGL, graphics, GUI library등이 있죠.


그리고 Net framework 이나, MFC 와 같은 framework은 이런 graphics library와 open gl, socket, http등을 이용해서 application을 쉽게 구현 할 수 있도록 해주는 것입니다.

또, Facebook에서 자신들의 서비스를 쉽게 접근해서 사용할 수 있도록 제공하는 facebook framework 등과 같은 서드 파티에서 제공하는 framework이 있습니다.


즉 Framework은 어떤 기능을 구현하기 위한 low level의 API를 제공하는 library(socket,gl 과 같은) 가 아니라, network connection manager 나 game 개발을 쉽게 개발할 수 있도록 socket을 이용하여 http protocol을 쉽게 접근 할 수 있는 http class 를 제공하는 것이고, http class들을 이용해서 facebook에 쉽게 접근 할 수 있도록 facebook class나 facebook web api set을 제공하는 package를 말하는 것입니다.


OS 제조사(MS, Apple, Google등)에서 제공하는 application development framework(SDK)은 다양한 sub framework이나 module 들을 포함하고 있습니다.


또 .Net 이나 android의 framework들이 대표적이라고 할 수 있는데요. 크게 Application life cycle 을 handling 할 수 있는 runtime, Ui framework, content management, media, image , graphics , network , censor 등의 sub module들을 포함하고 있습니다.


이런 sub module( 또는 sub framework)등을 architecture 로 표현 할 때 흔히 package, class, namespace 등으로 표현하고, UML과 같은 디자인 툴로 각 class 및 package 들의 관계를 표현 합니다.


이정도로 정리해서 platform과 framework의 정의를 나눠봤고, 과연 C언어로 framework을 제공하는 것에 대해서 좀더 심도 있게 생각해보고 싶습니다.

이 의견은 객체지향 패러다임을 적용할 수 없는 언어가 과연 application 개발을 위한 framework으로 적합한가에 대한 물음으로 보시면 될것 같은데요. 제 의견은 만들 수는 있지만 좋은 사용성을 제공하기는 힘들 것이라 생각됩니다.

여러가지 framework들은 범용적인 형태가 아닙니다. 즉 어떤 목적에 맞는 ,그 목적에 잘 어울리는 방식으로 기능을 제공합니다.

하나의 programming 언어로만 제공되는 경우도 있지만, script, XML, Editor tool, 심지어는 programming 언어까지 새로 만들어서 제공되기도 합니다. 

이러한 다양한 형태까지는 아니더라도, 적어도 OO design을 적용한 framework은 적어도 OO 패러다임을 담고 표현할 수 있는 언어로 제공되는 것이 맞겠죠.



5. 맺음말

처음에 C와 C++언어에 대한 이야기로 시작 했는데 사실 framework과 language를 같이 얘기 하고 싶었습니다.
제가 프로그래밍 언어에 대해 전공을 한 것이 아니라 제 경험과 주관 그리고 블로그나 커뮤니티에서 활동하고 있는 고수분들이 적어놓은 자료들을 많이 읽어보고 나름 의견을 정리해봤는데요.하고자 했던 얘기는 프로그래밍 언어는 환경과 목적에 따라 잘 선택하는 것이 중요하다는 것입니다
System 하부의 device driver와 같은 코드를 개발하는데, java나 C# 보다는 C가 훨씬 더 효율적이고, system의 동작에 대해 예측이 쉽기 때문에 잘 어울린다고 볼 수 있습니다.
반면, Application framework이나 service framework을 개발하여 사용자에게 interface를 제공하는데 C 로 작성된 API들을 제공한다는 것은, OO 기반의 설계를 다시 구조적, 절차적인 code로 구현하고 Framework API를 C function으로 제공한다는 의미가 됩니다
이는 설계와 동작이 불일치 할 가능성이 있고, 편의성 측면에서도 문제가 될 소지가 있습니다. 
OO 기반의 S/W 설계가 20여년 이상 자리잡고 현재는 대세가 되었는데, 복잡한 framework구성이나 탄탄한 설계가 필요한 부분에서 OO를 지원하지 않는 언어로 framework 을 구현하고 API를 제공한다는 것은 잘못된 방향이 아닌가 싶습니다.  구현, 디버깅, 안정성, 사용성 어느 하나 더 낳을 것이 없으니까요.
저는 OO 관점에서 봤을 때 C++ 역시 pointer를 지원하기 때문에  framework interface로는 좋지 못하다고 생각합니다. pointer는 객체에 대한 접근 표현이 아니라 메모리에 직접적인 접근을 의미하기 때문에 값의 변경이나, type 이 모호해질 가능성이 매우 큽니다. C와 비교했을때 C++이 더 낳다 인것이지, 다른 프로그래밍 언어들과 비교 했을때, C++ 역시 상당히 조심해서 사용을 해야 하는 언어임은 맞습니다. 복잡하구요.
(대안이 없는 경우라면 ㅠ_ㅠ 어쩔 수 없겠죠.)

최근에 다시 C++ 언어가 재조명 되고 있다는 기사를 본적이 있는것 같습니다. 모던 C++이라는 이름을 쓰기도 하는것 같던데..
다양한 프로젝트들에서 C++로 framework을 작성하고 있고, C++ 표준화도 C++11, C++14와 같은 많은 변화들이 진행되고 있습니다.
최신 C++에서는 기존의 C++이 어택받아오던 임시객체 생성과 복제 등에 대한 대안들을 제공하고 있어서 framework을 어떻게 설계하는가에 따라 퍼포먼스에 대해서도 상당한 향상을 가져 올 수 있을것 으로 보입니다.
글쓰는데 소질이 없는 건지 마무리 맨트가 좀 그렇네요.  ㅎ. 나중에 시간되면 좀더 정리해보도록 하겠습니다.
이상 마칩니다.

반응형

http://www.sorting-algorithms.com/


graph and animation.

'개발 Note > Codes' 카테고리의 다른 글

[Andriod] Timer and TimerTask  (0) 2020.10.22
Singleton class 설계  (0) 2020.10.07
C++ object 관리를 위한 ObjectRef 구조 설계  (0) 2015.06.26
c++ while 문 - 잘 안쓰는 표현  (0) 2013.10.23
UML2 Sementics  (0) 2012.01.11
반응형



c++ 에서 while 문은 조건을 만족하는 동안 loop를 도는  keyword입니다.


형식은 다음과 같지요.


bool exit =  false;

int i = 0;

while(!exit)

{

 i++;

printf("i = %d\n",i);

if(i >=100)

{

   exit = true;

}

}




그런데 다음과 같이 사용도 가능합니다.

while( i++ , (i>100)?false:true)

{

printf("i = %d\n",i);

}


또는 


while( i++, j++, !exit)

{

if(i>100 && j>100)

{

exit = true;

}

}


while문의 조건 채크는 맨 마지막 parameter에서만 합니다.



자주 사용되는 표현은 아닙니다.




'개발 Note > Codes' 카테고리의 다른 글

[Andriod] Timer and TimerTask  (0) 2020.10.22
Singleton class 설계  (0) 2020.10.07
C++ object 관리를 위한 ObjectRef 구조 설계  (0) 2015.06.26
sorting algorithms  (0) 2014.02.26
UML2 Sementics  (0) 2012.01.11
반응형


UML의 표기를 수학적 모델로 표기한 내용입니다.


Subclass relation
∈∀ ∉

Nil ∈ UOID

∀C ∈ UCLASS: Nil ∉ oids(C)
∀o  ∈ INSTANCE : o.this ≠ Nil
UOID={Nil}  Uc∈ UCLASS objects(C)
INSTANCE = 
Uc∈ UCLASS objects(C) 
 

'개발 Note > Codes' 카테고리의 다른 글

[Andriod] Timer and TimerTask  (0) 2020.10.22
Singleton class 설계  (0) 2020.10.07
C++ object 관리를 위한 ObjectRef 구조 설계  (0) 2015.06.26
sorting algorithms  (0) 2014.02.26
c++ while 문 - 잘 안쓰는 표현  (0) 2013.10.23

+ Recent posts