클래스를 이야기 할 때, 부모클래스와 자식클래스라는 이야기를 많이 한다.

이를 빌어 쉽게 이야기하면 자식클래스는 부모클래스를 상속하여 구현한다. 라고 말한다.


좀더 프로그래머의 관점에서 보도록하자.


1. 상속은 부모 클래스로 기본적인 기능을 구현한다. 그리고 자식 클래스로 상속받고 좀 더 특화된 기능을 구현한다. 자식 클래스는 부모 클래스를 특수화한다고 생각 할 수 있다.

2. 부모클래스를 기본적인 기능으로 구현할 때 공통된 것을 추출하여 구현할 수 있겠다. 즉 자식클래스는 부모클래스의 일종이 아니다. 같은 것이 아니란 이야기이다.

3. 공통된 부분을 부모클래스가 가져가고 추후에 각 자식클래스는 추가되거나 수정되는 기능을 구현하면 효율이 좋다.


이렇게 상속을 이용하면 높은 자유를 제공한다. 하지만 상속을 많이 사용하면 코드가 복잡해지고 이를 정상적으로 동작시키도록 제어도 추가해야 할 수 있다. 


상속과 관련하여 리스코프의 치환 원칙이라는 것을 주의해야 할 사항으로 자주 언급된다.

리스코프의 치환 원칙은 CLU라는 언어의 설계자인 Liskob가 1987년에 제창한 하였다. 


T형의 객체 x에 관해 어떤 속성 q(x)가 항상 참이라고 한다. 

S가 T의 파생형이면, S형의 객체 y의 속성 q(y)가 항상 참이어야 한다.


이는 어떤 클래스 T의 객체인 x의 속성이 참이라면, 그럴 상속받은 자식 클래스 S의 해당 객체도 항상 참이어야 한다 라는 말이다.


즉 현실세계에서는 모르겠지만, 객체지향에서의 상속은 부모에게 구현되어 있는 속성들에 대해서는 자식들의 것들도 모두 만족해야 한다는 이야기이다. 

결국 이런 상속관계와 형의 부자관계간 정합성을 유지하는게 중요하다. 


이를 위해 상속은 is-a관계를 성립해야 한다. 라고 표현한다. 그리고 클래스 상속과 형 구조의 정합성을 유지하기에는 굉장히 어렵다. 



자식으로 진화할 수록 할 수있는 기능이 다양해질 수 있다.

예를 살펴보자. 


Windows 환경에서 창(window)이라는 부모 클래스 A가 있다.

이를 상속받는 자식 클래스가 두개있다. 하나는 메뉴바가 있는 클래스 B이고 하나는 스크롤바가있는 클래스 C이다.

이 상태에서 메뉴바도 있고 스크롤바도 있는 클래스 D를 만들고 싶다.

만약 D를 구현하기 위해서 A를 상속받은 후 B, C 클래스의 내용을 구현한다고 해보자. 이는 불필요할 수 있다. 왜냐면 D가 B,C 를 상속받을 수 있기 때문이다. 이를 다중상속이라고 한다. 다중상속을 하면 별도의 코드를 다시 만들 필요가 없게된다. 


Python 표준 라이브러리에서 사용되고있는 다중상속의 예를 보자.


class ForkingUDPServer(ForkingMixIn, UDPServer): Pass

class ForkingTCPServer(ForkingMixIn, TCPServer): Pass

class ThreadUDPServer(ThreadingMixIn, UDPServer): Pass

class ThreadTCPServer(ThreadingMixIn, TCPServer): Pass


이 라이브러리들은 통신에 TCP, UDP를 이용할지와, 병렬처리방법으로 fork를 이용할지 thread를 이용할지에 관한 내용이다.

이는 각 부분의 속성을 하나씩 속성받아 구현된 함수이다. 


이렇게하면 기존의 코드를 유용하게 그대로 사용할 수 있다.



하지만 다중 상속에는 주의할 점이 있다. A 클래스와 B 클래스모두 x라는 이름을 갖는 변수가 있다고 생각해보자. 이 둘을 상속받는 클래스 C에서 x를 참고할때 과연 어느 부모에게서 참조를 해야하는 것일까? 


Java에서는 이런 문제를 고려하여 다중상속을 금지하기로 했다. 하지만 다중상속이라는 편리함도 버리게 된다.  즉 코딩 시 재사용을 목적으로 한 상속을 피하려는 의지가 강하다. 예를들어 Abstract Window Toolkit(AWT)라는 그래픽 툴킷에서는 다시 상속한 각종 메소드를 오버라이드하여 사용한다라는 규칙을 만들기도 하였지만 이클립스 등에서 사용되는 Standard Wiget Toolkit(SWT)은 계승하면 안된다라는 규칙이 있다.


이를 대신하여 발달된 것이 delegation 이라는 개념이다.

사용하고 싶은 코드를 가지고있는 클래스 객체를 만들고, 필요에 따라서 해다 클래스에 처리를 맡기는 것이다. 이렇게하면 상속을 사용함으로써 형이나 이름 공간까지 함께 계승하는 것이 문제의 원인이기  때문에 단순히 객체만 보유하면 위의 문제를 막을 수 있다.


자바의 코드를 예를 들어보자.


pubilc class TestDelegate {

public static void main(String[] args) {

new UseInheritance().useHello();    // -> hello!

new UseDelegate().useHello();        // -> hello!

}

}


class Hello{

public void hello(){

System.out.println("hello!");

}

}


class UseIngeritance extends Hello{

public void useHello(){

hello();

}

}


class UseDelegate{

public void useHello(){

Hello h = new Hello();

public void useHello(){

h.hello()

}

}

}


위의 코드는 상속을 사용한 코드 재사용과, delegation을 사용한 코드 재사용을 나타내고 있다.

실제로 상속보다는 delegation을 이용한 방법이 훨씬 낫다. delegation의 참조도 소스에 하드코딩하는 것이 아닌 설정 파일을 사용해서 실행시에 주입하는 것이 편하다. 이런 발상으로 의존성 주입(dependency injection)이 탄생했다.



하지만 java에서도 다중상속을 금지하고 있긴하지만 interface라는 개념으로 다중상속을 구현한다. 

인터페이스는 코드를 가지지 않는 클래스이며 반드시 자식 클래스에서 이를 구현해야한다. 이를 추상클래스의 의미이다.


단도직입적으로 말하자면 상속을 코드 재사용을 위해서 하고싶다. 다른 방법은 없을까? 라고 생각할 수 있겠다.

이는 interface를 사용해 has-a관계를 가지며 구현할 수 있겠다.


java 에서는 상속(abstract)를 extends 키워드로, interface는 implement 키워드로 구현한다.


.



참고. 코딩을 지탱하는 기술

'Development > Etc.' 카테고리의 다른 글

type(형)  (0) 2016.02.06
언어별로 문자열 처리  (0) 2016.02.06
전역스코프 동적스코프 정적스코프  (0) 2016.02.06
언어별 클래스의 의미  (0) 2016.02.06
이벤트기반과 메세지기반 아키텍처의 차이  (0) 2016.02.06



 본디..


C++의 설계자인 바얀(Bjarne Stroustrup)교수님은 Class를 사용자 정의형을 만들기위한 구조라고 하고, 객체지향 프로그래밍이란 사용자 정의형과 상속을 사용한 프로그래밍이다. 라고 했다. C++은 클래스의 개념이 처음 나오고 난 후로부터 10년 뒤 Simula라는 시뮬레이션용 언어를 참고로 사용자가 형을 정의할 수 있도록 만들었다. 


클래스는 type이다. 이거이 C++을 이루는 아주 중요한 원리이다. C++에서 class가 사용자 정의 타입을 의미한다면 왜 그것을 type이라고 부르지ㅏ 않는가? 내가 class를 선택한 것은 계속 나오는 새로운 용어를 발명하는 것이 귀찮았기 때문이고, Simula의 class라면 아무도 당황하지 않을 것이라 판단했기 때문이다.

<C++로 배우는 프로그래밍의 원리와 설게>


int나 float처럼 동일하게 다룰 수 있는 새로운 형을 사용자가 정의하고 사용할 수 있게 하자 라는 것이 C++이 클래스를 도입한 목적이다. 

이렇게 되면서 Type을 자유롭게 만들 수 있게 되었고, MS에서 만든 Visual C++의 경우 너무 많고 다양한 형으로 이제는 사용하기 오히려 어렵기까지 되었다.


C++에서는 클래스를 형(type)으로 보고 객체가 어떤 메소드를 가지고 있고, 가지고 있지 않은가를 구분하는데 중요하게 생각했고 존재하지 않는 메소드를 호출하면 에러가 발생한다. 


하지만 객체지향 용어의 발명자인 Alan Kay는 객체지향이란 상태를 가진 객체가 메시지를 주고 받아서 커뮤니케이션하는 프로그램이라고 말하며, 형이나 상속에 대해서는 부정적인 입장을 취하고 있다. 그가 설계한 Smalltalk이란 것에서는 메소드 호출이란 건 단지 이런 이름의 메소드를 호출해달라는 메시지를 객체에 전달하는 것일 뿐 그 메시지를 받은 객체가 어떤 동작을 할지는 수신 객체가 자유롭게 결정할 수 있다. Alan kay는 이런 자유가 객체지향의 중요한 요소라고 생각한다.



실제로 언어의 성격이나 철학마다 설계자의 생각에 따라서 의미가 조금은 다르다. 실제로 클래스의 의미도 일반적으로 생각하면 위험할 수 있다.


그럼 형이라는 관점에서의 클래스는 어떻게 정의될까?

정적 형결정 언어인 C++에서는 클래스를 사용자가 정의할 수 있는 형이라고 한다. 

동적 혈결정 언어인 Ruby나 Python, Perl, JavaScript의 클래스는 아마 C++의 class와는 다른 의미를 가질 것이다.



보통 C++은 클래스가 없어도 프로그램을 만들 수 있다. Python이나 Ruby도 이와 마찬가지이다. 

하지만 Java는 다르다. Java는 클래스라는 부품을 정의하고, 그것을 조립해나가는 것이 프로그래밍이라고 정의한다. 따라서 Java에서는 클래스는 필수적 요소이다.



클래스는 사실 어떠한 비슷한 것들을 하나로 묶어서 만들자라는 것에 목적을 가진다. 그렇다면 클래스 이전에는 이를 위해 어떠한 개념들이 있었을까?


첫번째는 module이다. 비슷한 기능을 하는 함수를 하나로 모아두는 기능이다. 

1978년경 Modula-2가 개발되었는데 관련성이 높은 함수나 변수의 묶음을 명시하기 위해 Module이라는 개념을 도입했다고 한다.  Ruby나 Python에서는 module이라고 하며, Perl이나 Java에서는 Package라고 한다.


이런 비슷한 것들은 묶으면 현실 세계의 어떤 것을 구현해 낼 수 이씨 않을까? 라는 생각이 등장하게 되었다.


Perl의 package는 함수나 변수를 하나로 묶어서 이름을 붙일 수 있는 기능이다. 하지만 좀 더 완벽한 객체지향을 도입하기 위해서 hash를 사용한다.

자 이유를 생각해보자. pakage를 만들어 하나의 사물을 구현했다. 하지만 그 사물은 여러개일 수 있다. 그럼 이 pakage를 복사해서 pakageA, pakageB 이런식으로 이름을 붙여야 한다. 이게 과연 옳은 것일가? 아니다. 이를 해결하기 위해 별도의 데이터 저장소를 만들어 하나의 이름으로 관리하되 각 객체들이 사용하는 공간을 여러개 두는 개념이 생겼다. 이를 위해 hash를 사용한다. 그리고 추가해야 할 상황이 오면 이와같은 작업을 수행해줄 생성자 함수(constructor)도 필요하다. 이런 초기화 처리도 패키지에 넣는다.

그리고 편리한 사용을 위해 package명과 hash를 연결하는 기능인 bless라는 개념을 발명했고 이를 통해 bless된 hash를 만들어서 보다 편리하게 접근할 수 있도록 하였다.


두번째는 함수도 변수도 동일하게 hash에 넣는 방법이다. JavaScript는 First Class라는 것을 사용하여 구현한다.

일반적으로 대부분의 언어에서 변수에 문자열을 대입하여 사용할 수 있지만, 다 그러한 것은 아니다. FORTRAN66 에서는 문자열을 변수에 대입할 수 없고 C언어 에서는 배열을 인수로 함수 호출을 할 수 없다. 이러한 관점에서 보았을 때 Fisrt Class란 제한이 없는 변수에 대입하고, 함수의 인수로 전들하고, 함수의 반환값으로 전달할 수 있는 것을 말한다. JavaScript에서는 함수도 퍼스트 클래스의 값이라 함수를 변수에 대입하거나 반환값으로 전달하는게 가능하다. 그래서 함수도 hash에 넣을 수 있다.


이렇게 되면 어떤 기능을 하는 객체를 해쉬에 넣고 여러개를 만들 수 있게 되는 것이다. 또한 공유도는 사물은 하나로 묶어서 개별 카운터가 그것을 참조하는 형태로 구현되어 메모리나 시간 절약에 도움을 준다. 


세번째는 Closure이다. 함수 실행시에 이름공간의 변수를 하나로 묶기 위해서 사용하는데, 주로 함수형 언어에서 사용한다.

클로저는 JavaScript에서도 제공한다. 함수를 함수 안에 정의하고, 내포할 수 있는 정적 스코프가 있어서 함수를 반환값으로 사용하거나 변수에 대입하여 사용할 수 있다는 개념이다. 






출처. 코딩을 지탱하는 기술





이벤트기반과 메세지기반 아키텍처의 차이를 간단하게 컴퓨터의 역사를 통해서 철학적으로 생각해보자.





과거 초기의 컴퓨터에서는 하나의 프로그램을 실행하면 그 프로그램의 수행이 끝날때까지 해당 처리만 할 수 있었다.


데이터 입출력을 생각해보자.


파일을 수정하기 위해 텍스트 파일을 열었다. 이때 하드디스크로부터 메모리로 파일의 내용을 load할 것이다.

지금에야 파일의 용량이 상당히 크더라도 금방 로딩이 되지만, 과거 성능이 좋지 않았을 때에는 해당 내용을 읽어오는 작업 만으로도 상당한 시간이 걸렸을 것이다.

처리를 하나씩만 할 수 있다면 데이터를 읽어오는 시간동안은 데이터를 읽는 작업만 수행할 수 있고 해당 작업이 끝날때까지 기다려야 한다는 것이다. 

게다가 이런 I/O작업은 I/O 디바이스에서 수행되므로 CPU는 가만히 놀고있게 된다. 굉장한 자원의 낭비이다. 

이는 지금에선 상상도 할 수 없는 끔직한 일이다. 


데이터를 읽어오는 동안 다른일을 할 수 있다면 굉장히 효율적일 것이다. 이를 실현하기 위해 프로세스, 스레드라는 개념이 만들어지며 병행처리가 가능하게 되었다.

프로세스, 스레드를 이용해 자원을 나누어 여러개의 일을 동시에 처리하는 것처럼 할 수 있다. 어떤 방법으로 자원을 공유할 것인지는 어떤 스케줄링 방식을 사용할 것인지에 따라 달라진다. 만약 이런 스케줄이 없다면 자원에 대해서 접근하는 여러 작업들간에 충돌이 일어날 것이다. 이를 막기위해 스케줄을 구현하는 스케줄러는 Lock이나 fiber등의 개념을 사용해서 충돌을 방지할 수 있다.


기본적으로 병행처리를 하는 방법은 두가지로 나눈다.

preemptive multi-task(선점형 멀티태스크)방식이과 non-preemptive multi-task(비선점형 멀티태스크)방식이다.


non-preemptive multi-task는 한 처리가 수행될 때 CPU를 독점하고 있므면서 CPU가 프로세스들의 수행제어에 관여하지 않는 방식이다. 즉 수행중인 프로세스가 자신의 처리가 다 끝나거나 혹은 자발적으로 수행을 그만할 때 비로소 제어가 다른 쪽으로 넘어갈 수 있다. 한번 프로세스가 실행되면 다른 처리는 계속 기다려야 하므로 최적의 어떠한 간격을 두고 교대를 해야한다 라는 전제조건을 릴요로 한다. Windows 3.1이나 NT와 Max OS 9가 이 방식을 사용했다. 문제가 발생하면 다른 프로세스에게 제어가 자연스럽게 넘어가지 못하므로 통째로 백업해야하는 단점도 있다. 


preemptive multi-task 방식은 CPU가 프로세스들의 수행제어에 선점적으로 관여한다. 일정 시간이 지나면 현재 진행중이던 처리를 강제로 중단한 후 다음의 프로그램이 수행되도록 한다. 이는 UNIX나  LINUX가 선택한 방식이기도 하다.


preemptive 방식에서 프로세스들은 자신의 수행이 언제 중단될지 알 수 없기 때문에 이들을 제어하는 스케줄러의 역할은 매우 중요하다. 만약 잘못한다면 데이터나 자원의 상태가 꼬이거나 충돌이 발생할 수 있기 때문이다. 이런 상태를 race condition 혹은 non thread safe라고 한다.

그리고 처리 도중 끼어들 수 있는 개념이 interrupt이며 이를 이벤트기반의 아키텍처라고 볼 수 있겠다. 즉 이벤트 기반이라는 것은 어떤 일이 발생하였을 때 해당 이벤트를 알려주고 해당 이벤트에 맞는 처리를 수행한다는 것이다. 소프트웨어 개발의 관점에서 봤을때에도 마찬가지로 이벤트를 발생시키면 이벤트를 받는 쪽에서는 해당 이벤트를 수행할 수 있다. 보통 실시간 작업처리를 위해 사용된다.


race condition을 발생하는 조건은 다음과 같다.

1. 2가지 처리가 변수를 공유하고 있음.

2. 적어도 하나의 처리가 그 변수를 변경할 수 있음.

3. 한쪽 처리가 한 단락 마무리 되기 전에, 다른 쪽의 처리가 끼어들 가능성이 있음.


race condition을 방지하거나 해결하려면 위의 사항 중 하나만이라도 처리하면 된다. 


첫번째 항목을 살펴보자. 처음부터 공유하는 것이 없도록 만들면 된다.

처음의 시스템은 메모리를 공유하는 방식이었지만 1969년 발표된 Multics라는  OS에는 다양한 기능들이 많이 들어가면서 시스템이 복잡해 졌으며 이를 해결할 방법이 필요했다. 이를 해결하기 위해 1970년에 UNICS라는 OS가 만들어졌고 이것이 현재의 UNIX이다. UNIX는 프로세스별로 메모리영역을 따로 지정하여 사용하는 구조를 가진다.


하지만 이런 방식은 성공하지 못했다. 프로세스끼리 정보를 공유해야 하는 경우도 있었기 때문에 메모리공유가 필요했던 것이다. 스레드는 여기서 나온 개념이다.



병행처리의 다른 모델로 actor 모델이라는 것이 있다.

위에서 설명한 병행처리에서는 프로세스들이 정보를 교환하는 방법으로 메모리를 공유하였다. 이를 위해서는 lock이라는 것을 이용하여 다른 프로세스의 접근을 막아야 했고 locking이 수행되다면 어떤 처리가 끝날때까지 기다려야하는 프로세스가 생기게 된다. 즉 자원이 비효율적으로 사용 될 수 있다는 것이다. 

자신(A)의 처리에 필요한 다른 처리의 수행을 위해 프로세스(B)가 필요하다고 가정해보자. 만약 A가 B에게 바로 알리게 된다면  B는 현재 수행중이던 작업에 방해를 받을 것이다. 그렇다고해서 B의 현재 작업이 끝날때까지 기다린다면 A는 그만큼 시간을 버리게 된다. 그래서  A는 해당 처리를 B에게 메세지로 남긴 후 돌아간다. 그러면 B는 자신이 수행하던 처리를 마친 후 메세지를 확인하여 처리한 후 A쪽에 메세지로써 결과를 남긴다. A는 이 메세지를 확인하고 비로소 자신이 맡긴 처리가 잘 수행되었다고 판단한다.


이것이 Actor 모델이며 이러한 처리는 대용량의 데이터를 처리할 때 적합하다. 실시간으로 처리되는 작업은 아니지만, 경우에 따라서는 자원의 효율을 극대화 하여 사용할 수 있다. Facebook이나 Twitter에서 대량의 사용자 메시지를 처리할때 사용한다. Erlang이나 Scala에서 해당 모델을 채용하고 있으며 이런 방식을 메시지 기반의 방식이라고 한다.


2번째 항목은 어떨까? 메모리를 공유하더라도 변경하지 않으면 된다. 

const는 상수화하여 값을 변경할 수 없도록 하고, Scala에서는 변수선언 키워드가 var, val 2가지로 구현되여 val은 const로 선언된다.

또한 Java에서는 class에 private한 영역을 만들어서 getter 메소드는 생성하여 읽기는 가능하지만 setter 메소드는 생성하지 않아 값을 수정할 수 없도록 하는 Immutable 패턴이 자주 사용된다.


3번째 항목을 위해서 어느 한 처리 중에는 끼어들지 않도록 하는 기법들이 생겨났다. 

그 중 하나가 서로 협력하는 스레드가 생겨났다는 것이다. 스레드는 기본적으로 preemptive하므로 non-preemptive한 스레드를 만들자라는 개념이다. fiber, coroutine, Green thread와 같은 기법을 사용하는 Ruby의 Fiber Class나 Python이나 JavaScript의 Generator가 그 예이다.

다른 하나는 다른 처리에게 해당 메모리로 접근하지 말라고 표시하는 Lock, Mutex, Semaphore를 이용하는 것이다. 이 방식에서는 deadlock의 문제를 잘 인식하고 사용해야 할 필요가 있다. 이는 굉장히 까다로운 일이다.


이를 약간 편하게 사용하기 위해 DataBase의 transation 기법을 적용한 Transactional memory 기법이 탄생했다. 해보고 실패하면 다시 처음으로 돌아가고 끝까지 성공하면 변경을 공유하는 것이다. 이는 락을 걸지 않아도 문제가 없이 병행처리가 가능하다. 하지만 다시 고쳐쓰는 작업이란 비용이 많이 드는 작업이다.





이벤트기반과 메세지기반 아키텍처는 어느 한 것만 고집할게 아니라, 시스템의 환경에 맞게 적절히 사용하는게 가장 좋겠다.






참고. 코딩을 지탱하는 기술

'Development > Etc.' 카테고리의 다른 글

type(형)  (0) 2016.02.06
언어별로 문자열 처리  (0) 2016.02.06
전역스코프 동적스코프 정적스코프  (0) 2016.02.06
상속이 is-a 관계여야 하는이유와 delegation  (0) 2016.02.06
언어별 클래스의 의미  (0) 2016.02.06

+ Recent posts