oop가 fp보다 좋은이유?!
Coupling 이란 무엇인가?
우리는 일반적으로 코드의 일부가 서로 관련되어 있는 경우 커플링이 높다고 보고 관련이 없거나 전혀 없는경우 커플링을 낮다고 본다
print('Hello!');
print('What is your name?');
이 두 코드는 연결 되어있을까? 그럴수도 있고 아닐 수도 있다.
print('Orange');
print('Apple');
이 두코드는 어떠한가?
커플링의 수준을 알려면 행의 순서를 바꿔서 그것이 여전히 말이 되는지 봐보자.
첫번째 코드들은 순서를 바꾸면 어떠한가? 순서가 이상하게 느껴진다. 두번째 코드들은 어떠한가? 전혀 문제가없다. 이렇게 실행의 순간이 연결된 커플링을 Temporal Coupling이라 한다.
일반적으로 커플링은 좋지않다. 밀접하게 결합될수록 소프트웨어를 유지 관리하기는 더 어려워진다. 쉬운일은 아니지만 컴포넌트간의 결합을 줄이는것이 '좋은 프로그래밍'의 전부라고 해도 과언이 아니다.
Hello행이 What is your name?행 앞에 와야한다는것을 이해하려면 해당 행을 읽어야한다. Hello가 무엇을 의미하는지 What is your name 이라는 줄 앞에 왜 와야하는지 이해애햐한다. 지금은 간단한 예제니까 문제가 없어보이지만 이게 실제 서비스 코드라고 생각해보자. 아마 올바른 순서가 무엇인지 알아내기위해 고군분투하던 우리의 모습이 떠오를것 이다. 아니 오늘도 그랬다.
요점은 Temproal Coupling이 유지보수의 용이성을 떨어트린다는것이다. Temproal Coupling이 더 나쁜 경우는 숨겨져 있을 때 이다. 즉, 첫번째 예시와 같이 Hello행과 What is your name행을 바꾸면 컴파일러는 차이를 인식하지 못하고 컴파일 된다. 기능은 고장 났지만 정상적으로 컴파일된다. 차라리 변수라도 앞에 선언 되어있었고 그 순서가 바꼈다면 컴파일이 되지않기 때문에 알아차릴수 있었을 것이다.
Temproal Coupling은 코드를 수정하거나 확장하기로 했을때 실수로 행 순서가 변경되지 않는다는 것을 보장할 수 없다. 컴파일러에 의한 검출이 되지 않을 수도 있고 추적도 어렵다.
명령형 프로그래밍에서는 Temproal Coupling이 거의 불가피하다. 예를들어 가능한 cpu 아키텍처에 가까운 어셈블리를 생각해보자. 명령어는 엄밀한 시간 순서대로 하나씩 실행되며 때로는 점프하기도한다.
MOV BX, 1
MOV CX, 1
again:
CMP AX, 1
JE exit
MOV DX, BX
ADD BX, CX
MOV CX, DX
DEC AX
JMP again
뭔지는 잘 모르지만 위 어셈블리어는 피보나치를 구하는 코드이다. 이 어셈블리 코드는 시간순서대로 하나씩 진행되어야 한다. 무언가 바꾸면 프로그램은 고장이난다. 하지만 컴파일러는 문제가 있다는것을 인식하지 않을것이다. 이 어셈블리 코드의 모든 명령어는 밀접하게 결합되어 있고 이 결합은 temproal하다.
조금 더 이해가 되게 C로 이동해보자.
int main(int argc, char** argv) {
int pos = atoi(argv[1]);
int i = pos;
int last = 1;
int prev = 1;
while (--> 2) {
int temp = last;
last += prev;
prev = temp;
}
printf("f(%) = %d\n", pos, last); }
}
보다시피 코드는 매우 절차적이고 명령적이다. 그리고 일부 행을 바꿀수도 있어서 기능은 결함이 생기지만 컴파일러는 눈치 채지못하고 로직은 깨진다. 조금만 고쳐도 생기는 문제는 이 프로그램이 라인이 커플링 되어있고 그 커플링이 숨겨져있음을 증명한다. 이걸 제대로 이해하기위해서는 해당 구조를 알고 있어야하고 그 의미를 다 이해하고 있어야한다.
이를 함수와 재귀를 사용하여 동일한 로직을 구현하는 좋은 방법이 있다. C에는 함수와 재귀가 존재하지만, 선언적 접근법을 표현하기에 더 나은 Lisp을 사용하였다.
(def fibo (i)
(if (< i 3)
1
(+
(fibo (- i 1))
(fibo (- i 2)))))
어떠한가? 이 코드를 실행할때는 (fibo 10)이렇게 하면된다. 위 Lisp 코드에는 숨겨진 temproal coupling이 없다. 위 순서를 바꾸면 바로 컴파일러가 알아차린다. 정의상 선언형 프로그래밍은 statement가 없기 때문에 숨겨진 temproal coupling이 없다. 선언형은 declarations과 definitions로 구성되며 컴파일러는 이를 찾아내고 정렬한다.
oop에선 어떨까? 우리가 잘 알고 있는 java에서 피보나치 알고리즘은 어떻게 표시되는지 봐보자.
int fibo(int i) {
if (i < 3) {
return 1;
}
return fibo(i - 1) + fibo (i - 2);
}
java와 현대의 객체지향언어들은 functional composition아이디어를 차용했기때문에 Lisp에 가깝다. 하지만 정확히 functional하지않다. 이유는 가장 높은 레벨에 if와 return이라는 2개의 statement가 있기 때문이다. 위 코드는 결합이 있지만 결합이 숨겨져 있지는않다. 실제로 이 코드들을 바꾸게되면 java 컴파일러가 정상동작 하지 않기 때문이다. 그러나 java 메소드들에서 temporal coupling은 자주 보이는 일반적인 문제이다. 현대의 객체지향 언어에는 함수, 재귀, 함수형 프로그래밍의 기능들이 있지만 절차형 프로그래밍에서 넘어온 statements가 여전히 존재한다. 이는 편리하기도 하지만 과연 좋을가? 위에서 부터 순서대로 진행되어야하는 statements는 temproal coupling을 유발하게 되기에 한번 생각해볼만한 문제이다.
피보나치 알고리즘이 완벽한 객체지향언어로 표현된다면 어떤 모습일까? 실제 java는 이렇지 않지만 만약 완벽한 객체지향언어라면 이럴것이다 라는 가정이다.
class Fibo extends Number {
Fibo (Number i) {
super(
new If(
new Less Than(i, 3),
1,
new Sum(
new Fibo (new Difference(i, 1)),
new Fibo (new Difference(i, 2))
)
)
);
}
}
실행은 new Fibo(10).value(); 이런 느낌일것이다. 이것이 피보나치 알고리즘을 구현은 객체지향적이고 선언적인 코드이다. 보다시피 명령문도 temproal coupling도 없다. 알고리즘 전체는 피보나치 숫자의 선언일 뿐이다. 물론 type If(여기선 클래스), type Sum(여기선 클래스)을 포함단 다른 type(클래스)와 똑같이 보이지만 끝없는 재귀가 발생하지 않도록 lazy한 방식으로 다르게 구성해야한다. 어쨋든! 이것은 계산을 정의하는 절차가아니라 피보나치 숫자의 객체이다. 마치 위의 Lisp과 비슷하지 않은가? 그렇다면 FP가 더 좋은것아닌가?
글쎄?.. 위 알고리즘은 객체 구성을 통해구현되며 이는 functional composition에 가깝지만 이 방법이 더 낫다. 왜냐하면 피보나치 숫자는 함수가 아니라 객체이기 때문이다. oop가 올바르게 구성되면 fp와 매우 유사하다. 하지만 oop가 object(사물)를 다루기 때문에 더 직관적으로 만들 수 있다. object(사물)는 우리 주위에 있기 때문에 현실에서 이해하기 쉽다. 피보나치 숫자는 현실에서 우리가 object 또는 thing이라고 부를수 있다. 수학자가 아닌 우리와 같은 평범한 사람들의 언어로 말이다.
우리가 object(사물)를 잘 이해하는 것은 object(사물)가 현실세계의 실체이고 우리주위에 존재하기 때문이다. function fibo(10)은 object fibo(10)과 마찬가지로 이해할 수 있지만 객체들은 그들이 무엇인지 책임을 가지고 있기 때문에 더 편리하다고 생각한다. 함수는 하나의 문장의 범위 내에 존재하며 후속문장으로서 결과를 제공한다. 프로그래머에게 그게 뭔지 알 책임을 위임한다고 보여진다. 이 정보는 함수를 호출한 장소에서 나오는 즉시 잃게된다. 오브젝트는 인스턴스화 되어 일부 함수에 전달될 수 있으며 그 동작은 다른곳에 노출될 수 있다. 독자적인 라이프사이클에 있어 동작에 대해 여러번 물어 볼 수 있다. 또한 일부 언어에서는 인터페이스와 fake객체덕분에 객체상호작용을 테스트하기가 훨씬 쉽다.
그러나 유감스럽게도 java와 마찬가지로 현대의 객체지향언어들은 선언적이지 않다. 여전히 우리가 오브젝트를 구성하는 대신 프로시저를 작성하기를 기대하고있기 때문이다. 그들은 단순히 If나 LetssThan과 같은 type(클래스)가 없다. 대신 if와 연산자 < 가 있을뿐이다. 그래서 우리는 불가피하게 temporal coupling을 하게 되고 우리의 코드는 점점 유지보수가 어려워지고 있다.
이를 방지하는 방법 중 하나는 복수의 statement를 가지지 않게 하는것이다. 모든 메서드는 하나의 statement만 가지며 이 statemnet는 return해야한다. 이 원칙을 염두에 두고 모든 메서드를 하나의 statement만 작성하려고 하면 전체 코드가 훨씬 더 선언적이고 temporal coupling되지 않게 될 것이다. 우리는 우리가 사용하는 언어의 제한 때문에 이러한 목표를 달성하기 쉽지않지만 최선을 다해야한다. return문이 여러개 있는 긴 메서드는 메서드 내부에 숨겨진 temproal coupling이 불가피하게 추가되기 때문에 코드 유지보수에 좋지않다.
세줄 요약
- return 문이 많은 긴 메소드는 리팩터링 하자
- 선언적 프로그래밍을 하기위해 노력하자
- 개취지만 oop가 fp보다 표현력이 뛰어난듯 하다