0. 들어가며..
안드로이드 앱 개발을 한다면 필수불가결하게 사용하는 반응형 라이브러리인 RxJava를 쓰고 있던 차, 그래서 ReactiveX가 왜 좋은데? 라는 질문으로 시작된 글.
반응형 프로그래밍의 단짝 함수형 프로그래밍과 이를 포괄적으로 아우르는 선언형 프로그래밍에 초점을 맞춰 기술한다. 그리고 선언형 프로그래밍과 비교대상이 되는 명령형 프로그래밍과는 무슨 차이가 있고, 선언형 프로그래밍이 무엇이 더 우월한지 써보고자 한다.
1. Statements VS Expressions
먼저 쉬운 예시로 명령형과 선언형 프로그래밍에 대한 대략적인 컨셉을 익혀보자. 서브웨이에 가서 우리가 주문할 때의 모습이다. '빵은 허니오트에 속은 파주시고, 치즈는 아메리칸 치즈 넣고 데워주세요. 야채는 피클, 할라피뇨 빼주시고 소스는...'. 이렇게 내가 원하는 것을 하나 하나 전달하며 주문을 해야 한다. 죠샌드위치에 갔을 때의 모습을 생각해보자. '클럽 샌드위치 한개요!'. 한 문장으로 주문이 끝난다.
서브웨이의 경우 내가 사실상 어떻게 샌드위치를 주문하여 얻을지에 대해 관심이 있다. 죠샌드위치의 경우 무엇을 얻을지에 대해 더욱 관심이 있다. 서브웨이의 경우 내가 원하는 것을 one by one으로 명령 하며 주문해야 하지만, 죠샌드위치의 경우 내가 원하는 것 한개만 선언하면 주문이 끝난다. 제일 먼저 느낄 수 있는 둘의 차이는 주문하는 속도일 것이다. 샌드위치를 파는 입장에서는 재료를 하나 하나씩 주문을 받고 커스텀하여 샌드위치를 제조하는 것보단 이미 결정된 제조법만 따라서 결과값만 내보이는 방식이 훨씬 효율적일 것이다. 하지만 두 경우 모두 맛있는 샌드위치라는 똑같은 결과값을 같다. 단지 그 방식의 차이가 있을 뿐이다. 프로그래밍도 같은 원리를 가지며 다음 코드를 보자.
fun main() {
var sumOfEvens = 0;
//Imperative
for (i in 0..100) {
if (i % 2 == 0) {
sumOfEvens = sumOfEvens + i
}
}
println(sumOfEvens)
//Declarative or Functional
sumOfEvens = IntStream.rangeClosed(0, 100)
.filter { i: Int -> i % 2 == 0 }
.reduce { x: Int, y: Int -> x + y }
.asInt
println(sumOfEvens)
}
data inconsistency in multi-core processing environments.
명령형 코드로 짜여진 식과 선언형 코드로 짜여진 식이 있다. 두 가지의 식은 같은 결과값을 가진다. 명령형(Imperative) 으로 짜여진 코드는 루프를 돌며 순차적으로 하나씩 처리하고 있다. 코드가 익숙(쉽다는 것이 아닌 익숙)하다는 장점을 가지지만 더욱 복잡해질 수 있고 가독성이 좋지 않다. 한가지 더 주목할 점은, 사용되는 변수는 데이터 값이 계속하여 변하는 mutable한 성질을 가지고 있다. 우리가 평소에 생각없이 구구절절 작성한 이러한 식들은 data의 inconsistency한 성질에 의해 부수효과가 일어날 수 있는 가능성이 크다. 여기서 말하는 부수효과란 무엇일까? 두 개의 스레드가 한 개의 변수에 접근할 때 발생할 수 있는 오류가 대표적이라 할 수 있다. 선언형(Declarative) 코드는 이러한 변수의 부수적인 값 변경을 원천 배제한다는 큰 특징을 가지고 있다. 다음을 통해 선언형 프로그래밍의 특징에 대해 더욱 알아보자.
2. What to do rather than how to do
명령형 코드는 어떻게 처리할지에 초점을 맞춘다면, 선언형 코드는 무엇을 처리할지를 다룬다.선언형 코드는 0부터 100까지의 Int의 Stream을 생성하고 filter와 reduce 순수함수를 사용하여 결과값을 도출하고 있다. pure mathematical functions로 작동하는데 함수형 방식으로 짜인 함수, 순수 함수들은 인풋만 똑같으면 절대 다른 요인의 의한 변수가 없다. 이 함수들은 알고리즘과 제공된 데이터를 기본으로 연산이 이루어지고 외부 환경에는 어떠한 영향을 주지 않는다. 그저 함수의 역할만 충실히 하며 object의 상태를 바꾸지 않고 mutable한 데이터의 변화 또한 피하며 오직 제공된 데이터들을 가공해내는 역할만을 수행한다. 그래서 같은 입력값을 넣으면 같은 출력값을 산출할 것이다. 어떻게 처리할 것인지에 관심을 가지는 것이 아닌 무엇을 처리할 것인지에 관심이 있는 것이다.
3. Less error prone
mutable한 변수를 공용으로 사용하고 있지 않다. 이는 복잡한 코드를 쉽게 만들고 어느 변수도 참조하고 있지 않기 때문에 thread로부터 안전하고 multi-thread 환경으로부터도 안전하다. 함수 자체를 순수하게 지키고, 어떤 함수의 동작에 의해 프로그램 내 특정 상태가 변경되는 상황으로부터 자유로워진다. 이처럼 immutable data의 값을 사용하고 따라서 부수효과를 최대한 제거하여 가능한 코드의 대부분이 입출력 관계를 기술한다.
4. Abstraction
컴퓨터는 우리가 작성한 instructions을 그대로 읽고 이해하여 코드를 하나씩 실행시킨다. 선언적 프로그래밍은 abstraction 성질을 가지기 때문에 복잡한 식은 내부에 숨기고 clean interface만 제공한다. 어떻게 처리할지보다 무엇을 처리할지를 원하기 때문에 filter와 reduce 함수가 어떻게 구현되어있는지는 알 필요가 없다. 함수에 의해서 작동하는 모든 state 변경들은 이 함수 내부안에 추상화되어 있다. 때문에 선언형으로 작성한 식은 더욱 가독성이 좋고 명시적이다. 샌드위치 예제의 죠샌드위치에서 주문할 경우에도, 우리는 직원이 샌드위치가 만들어지는 모든 Imperative steps 들을 다 안다고 가정하에 주문을 하는 것이다.
"the most declarative solutions are an abstraction over some imperative implementation."
가장 효율적인 선언적 프로그래밍 방법은 명령적으로 작성된 코드를 추상화하는 것이다.
앞서 명령형 프로그래밍의 특징 중 하나는 코드가 '익숙'하다는 것이다. 선언형 프로그래밍 또한 몇 가지의 함수형 프로그래밍에서 제공하는 컬렉션 함수들의 쓰임만 익힌다면 더욱 간결하고 명시적이며 부수효과까지 사전에 방지할 수 있는 식을 작성할 수 있다는 것이다.
5. Context-independent.
선언적인 코드들은 최종적인 목표가 무엇인지에 대해서 관심이 있기 때문에 목표를 이루기 위한 세부적인 단계들은 관심이 없다. 따라서 동일 코드가 다른 프로그램에 쓰이더라도 정상적으로 동작한다. 함수형 프로그래밍에서 제공하는 컬렉션들도, ReactiveX 라이브러리에서 제공하는 함수들도 비슷한 형태를 가지기에 context에 대한 의존도가 덜 하다고 할 수 있다. 명령형 코드들은 그렇지 못한 경우가 많은데, 그 이유는 대부분 명령형 코드들이 변수의 사용이나 현재 상태의 컨텍스트에 의존적하는 경우가 많기 때문이다.
참고자료
'💻 CS' 카테고리의 다른 글
Reactive Programming (0) | 2023.07.04 |
---|