feat. Declarative Programming
Professor Odersky님의 제자가 되었다.
교수님은 날 모르시지만 나는 제자가 되었다.
스위스 로잔 연방 공과대학교를 다니면서 찐제자가 되고싶지만 능력의 한계가 명확하므로... 이렇게 만족하겠다..
함수형 프로그래밍을 공부하면서 느낀 점이 있는데 그것이 엘레강트 오브젝트 책에서 말하던 부분과 결이 같아서 엘레강트 오브젝트 내용과 함께 느낌 점의 썰을 풀어보려고 한다.
int max(int a, int b) {
if (a > b) {
return a;
}
return b;
}
아주 자주 보이는 코드이고 자주 작성하던 코드의 모습이다.
어떠한 문제점이 있는가? 문제는 없다. 완벽히 실행되고 컴퓨터는 이런 방식으로 동작한다.
이 방식은 프로그래머가 cpu와 유사한 방식으로 수행될 작업을 직접 지시할 수 있다는 것이다.
매우 순차적이고 그래서 순서가 중요하다.
무엇보다 우리가 자주봐서 마음이 편하다(?)
위 코드를 clojure로 작성해볼까?(clojure에 익숙지 않아서 문법이 틀릴 수 있지만 이해에는 무리가 없을 것)
(defn max [a,b]
(if (> a b) a b))
어떠한가? 오히려 더 헷갈리는가? 그것은 익숙지 않아서일 것이다.
이 코드를 보면 어디에서 실행이 시작되고 어디에서 종료되는지 말하기 애매하다
함수 내부의 정확한 작동방식을 말하기에 무리가 있다.
그리고 이를 변수에 속박(binding)을 해볼까?
(def number (max 3 5))
어떠한가? 이는 number를 (max 3 5)에 바인딩하였다. 컴퓨터에게 값을 계산하라고 요청하지 않았다.
단순히 number는 이 두 수의 최댓값이다(is a)를 말하고 있다.
이 최댓값을 어떻게 계산하는지 그건 관심사가 아니다.
number는 최댓값이다(is a) 라고 정의하는 것이 핵심이다.
함수형, 객체지향 프로그래밍이 절차적 프로그래밍과 차별화되는 점이 바로 이 선언적인 is a 이다.
객체지향에서 표현해볼까?
class Max implements Integer {
private final Integer a;
private final Integer b;
public Max(Integer a, Integer b) {
this.a = a;
this.b = b;
}
}
이 클래스의 사용법은 Integer x = new Max(3, 5); 이고 이 코드는 최댓값을 계산하지 않는다.
단순히 정의할 뿐이다. x가 3, 5의 최댓값이라고 정의할 뿐이다. clojure의 바인딩과 비슷하게 보이지 않는가?
이렇게 oop에서도 충분히 선언적으로 접근이 가능하다.
사실 어디에선가 if (a > b)에 대해 확인은 해야 하는데 이게 무슨 차이가 있는가 하면 이는 사용하는 방법에 차이가 있다. 선언형은 무엇인지만 정의하고 실행 시점은 나중에 정의된다. 즉, 앞에서도 말했듯이 선언만 했다는 점이 중요하다
명령형 프로그래밍에서는 프로그램의 상태를 변경하는 문장을 사용해서 계산방식을 서술하기에 변경하는 문장의 순서도 중요하고 이는 제어 흐름을 서술하는 꼴이 된다. 선언형 프로그래밍에서는 그저 표현만 할 뿐이다.
또한, 선언형은 최적화, 다형성, 표현력, 응집도에서 여러 장점을 가진다.
(참고로 정적메소드는 합성도 불가능하고 선언적이지 않다. 즉 명령형 스타일이다. 또한 테스트 가능한 코드를 만들 때도 불리하게 작용한다. 자세한 것은 필자의 다른글 을 참고하길 바란다.)
주관적이지만 fp에 대해서 더 고민해볼수록 oop를 잘 쓰면 큰 차이가 없다는 점이 느껴지고 있다. 결국 둘 다 말하고 싶은 건 같다. 물론 문제 해결에 있어서 접근방식이 다르고 더 심오하게는 차이가 있겠지만, 나 같은 마이너하고 평범하고 개발자가 느끼기엔 추구하는 점은 다르지 않다는 것이다.
oop도 선언형으로 그리고 객체지향적으로 코드를 접근한다면 아래와 같지 않을까 싶다.
List<Integer> = new Sorted( new Unique ( new FileNumbers ( new Directory("...")))))
scala나 kotlin으로 표현하면 더 깔끔해질 듯하다
val numbers = Sorted(Unique(FileNumbers(Directory("...")))))
clojure라면... 이렇게 되려나?
(def numbers (sorted (unique (filenumbers (directory "..")))))
실제 사용예라면..
절차지향이라면 이렇게
var number: Int
if (a > 10) {
number = 1;
}
else {
number = 2;
}
java라면 이렇게
Integer number = new If( new GraterThan(value, 10), 1, 2)
scala나 kotlin이라면 이렇게
val number = If(GraterThan(value, 10), 1, 2)
clojure라면 이렇게
(def number ( if ( graterthan value 10) 1 2)))
선언적으로 해보니까 어떠한가? fp를 지향하는 언어와 생긴 모습부터 비슷하지 않은가? 물론 현실적으로 실무에서 이렇게 모든 걸 객체화해서 사용하려면 동료들과 의견을 맞추어야 할 것이고 실현 불가능 할 것이다. 아니 불가능하다(그러니 그냥 fp를 하자! 그게 속 편하다!).
이상적으로 순수한 oop에서는 c같은 절차적인 언어로부터 물려받은 연산자가 필요하지 않다.
객체지향이라면 if, for, witch, while등의 연산자가 아니라 클래스로 구현된 If, For, Switch, While이 필요할 뿐이다. 순수하고 깔끔한 객체지향 코드를 작성하려면 명령형이 아니라 선언형으로 하면된다. 긴 메소드와 복잡한 프로시저의 사용을 최대한 자제하고, 작으면서도 조합 가능한 클래스들을 설계하고 더 큰 객체를 조합하기 위해 작은 클래스들을 재사용할 수 있도록 해야한다. oop는 더 작은 객체들을 기반으로 더 큰 객체들을 조합하는 것이다.
두서없는 느낀 점 썰인데 5줄로 급 마무리를 해보겠다.
1. fp든 oop든 다르지 않다
2. oop는 (원래) 선언적이어야 한다.
3. 작은 클래스들로 설계하여 재사용하며 조합이 가능 하도록 하자.
4. 이상적인 oop라면 프로시저로 동작하는 메소드가 아니라 순수한 함수를 가진다(물론 이상..일뿐).
5. 나 자신을 반성해본다..ㅠ_ㅠ