Android 개발자를 위한 함수형 프로그래밍 Part 1

Android 개발자를 위한 함수형 프로그래밍 Part 1

원문 Functional Programming for Android Developers — Part 1을 번역한 글입니다.


이 시리즈에서는, Functional Programming (FP)의 기초와 자바(good old Java)와 코틀린(new awesome Kotlin)에서 어떻게 쓰는지를 배우려고 합니다. 최대한 이론적인 전문 용어는 피해서 실용적인 개념을 다루겠습니다.

FP는 매우 큰 주제라서 Android 코드에서 유용하게 쓸 수 있는 개념과 방법 정도만 다루려고 합니다. Android에서 사용할 수 없는 개념도 조금 나오긴 하겠지만 최대한 관련있는 내용만 다루도록 하겠습니다.

준비되셨나요? Let’s go!

Functional Programming이란 무엇이고, 왜 사용해야 하나요?

좋은 질문입니다. Functional Programming이라는 용어는 넓은 범위의 프로그래밍 개념을 담고 있습니다. 그 핵심은 프로그램을 수학적 함수의 계산으로 다루고 mutable 상태사이드 이펙트를 방지하는 것입니다(앞으로 충분히 다루게 될 내용입니다).

FP가 핵심적으로 강조하는 내용은:

  • 선언적 코드 : 프로그래머는 what에 대해 신경써야 하고, 컴파일러와 런타임이 how에 대해 신경쓰도록 해야 합니다.
  • 명확성 : 코드는 최대한 이해하기 쉬워야 합니다. 특히, 예상치 못한 이슈를 방지하려면 사이드 이펙트는 분리되어야 합니다. 데이터 플로우와 에러 핸들링은 명시적이어야 하고 GOTO와 Exception같은 구조는 앱이 의도하지 않은 상태가 될 수 있기 때문에 지양해야 합니다.
  • 동시성 : 함수적 순수성(functional purity)이라는 개념때문에 가장 함수적인(functional) 코드는 기본적으로 동시성을 갖습니다. 이런 특성이 FP의 인기를 높이고 있습니다. CPU 코어가 사용되는 것만큼 매년 빨라지지는 않고, 멀티 코어 아키텍처의 장점을 갖기 위해 프로그램을 더 concurrent하게 만들어야 하기 때문입니다.
  • Higher Order Functions : 함수는 다른 언어의 primitive처럼 첫번째 클래스 멤버입니다. String이나 int처럼 함수를 전달할 수 있습니다.
  • 불변성(Immutability) : 변수는 한 번 초기화된 후 수정되지 않습니다. 한 변수가 생성되면, 영원히 바뀌지 않습니다. 변경을 원한다면, 새로 생성해야 합니다. 이것이 명시성과 사이드 이펙트 방지의 또 다른 측면입니다. 어떤 값이 변화할 수 없다는 특성을 알면, 그 값을 사용할 때 걱정하지 않아도 됩니다.

선언적, 명시적이고 concurrent한 코드는 코드 파악 및 예기치 못한 상황을 피하기 위한 설계가 더 쉽지 않을까요?

첫번째 파트에서는 FP에서 가장 기본적인 개념부터 시작합니다: Purity, Side effects, Ordering.

Pure functions

함수의 결과가 입력에만 의존하고 사이드 이펙트가 없을 때(사이드 이펙트는 바로 다음에 다룹니다) 그 함수는 순수(pure)하다고 합니다. 예시를 볼까요?

이 간단한 함수는 두 수를 더하는 함수입니다. 숫자 하나는 파일에서 읽고, 다른 숫자는 파라미터로 전달됩니다.

Java

int add(int x) {
    int y = readNumFromFile();
    return x + y;
}

Kotlin

fun add(x: Int): Int {
    val y: Int = readNumFromFile()
    return x + y
}

이 함수의 결과는 입력에만 의존하고 있지 않습니다. readNumFromFile()의 결과에도 의존하고 있어서, x가 같아도 다른 결과가 나올 수 있습니다. 이런 함수가 비순수(impure)함수입니다.

그럼 이 함수를 순수 함수로 수정해봅시다.

Java

int add(int x, int y) {
    return x + y;
}

Kotlin

fun add(x: Int, y: Int): Int {
    return x + y
}

이제 함수의 결과는 입력에만 의존적입니다. 주어진 x와 y에 대해 함수는 항상 같은 결과를 반환할 것입니다. 이제 이 함수는 순수함수라고 할 수 있습니다. 수학에서의 함수 또한 같은 방식입니다. 수학적 함수의 결과는 입력에만 의존적입니다ㅡ그래서 FP는 일반적으로 우리가 사용하는 프로그래밍 스타일보다는 수학에 더 가깝습니다.

P.S. 빈 입력값도 입력입니다. 함수에 입력값이 없으면서 항상 같은 결과를 반환한다면, 그것 또한 순수 함수입니다.

P.P.S. 주어진 입력에 대해 항상 같은 결과를 내는 특성은 참조 투명성(referential transparency)이라고 하며, 아마 순수 함수에 대한 설명을 볼 때 본 적이 있는 용어일 겁니다.

Side effects

같은 덧셈 함수로 사이드 이펙트 개념에 대해 살펴봅시다. 결과를 파일에 쓰는 것으로 바꿔보겠습니다.

Java

int add(int x, int y) {
    int result = x + y;
    writeResultToFile(result);
    return result;
}

Kotlin

fun add(x: Int, y: Int): Int {
    val result = x + y
    writeResultToFile(result)
    return result
}

이 함수는 이제 계산 결과를 파일에 쓰고 있습니다. 즉, 함수 밖의 상태를 바꾸기 시작했습니다. 이 함수는 이제 사이드 이펙트를 가지고, 더 이상 순수 함수도 아닙니다.

범위 밖의 상태를 변경하는 모든 코드— 변수 수정, 파일 쓰기, DB 저장, 무언가 삭제 등 —는 모두 사이드 이펙트를 갖는다고 할 수 있습니다.

FP에서는 사이드 이펙트를 갖는 함수는 더 이상 순수 함수가 아니고, 이전 컨텍스트에 의존적이기 때문에 지양 대상입니다. 코드의 컨텍스트는 스스로 갖지 않아서 추론하기가 더 어렵습니다.

캐시를 바라보는 코드의 일부를 작성한다고 가정해보세요. 그 코드는 누군가 캐시에 썼는지, 무엇을 썼는지, 언제 썼는지, 그 데이터가 유효한지 등에 의존하게 됩니다. 캐시의 모든 상태를 이해하지 않고서는 프로그램이 무엇을 하고 있는지 이해할 수 없습니다. 만약 이걸 더 확장해서 다른 경우들 — 네트워크, DB, 파일, 사용자 입력 등ㅡ에 의존적인 앱을 만든다면, 정확히 어떻게 동작하고 있는지 알기 힘들고 모든 부분을 한 번에 이해하기도 힘들어 집니다.

그럼 이 말이 네트워크, 데이터베이스, 캐시같은 걸 쓰지 말라는 걸까요? 당연히 아닙니다. 보통 코드의 실행이 끝났을 때 어떤 동작이 완료되는 것이 목적일텐데 Android 앱의 경우, 보통 UI 업데이트를 의미합니다.

FP의 주요 개념은 사이드 이펙트를 완전히 없애자는 게 아니라, 포함하지만 분리시키는 것입니다. 사이드 이펙트를 갖는 함수들로 코드를 더럽히기 보다는, 사이드 이펙트를 시스템의 구석으로 몰아놔서 최소한의 영향만 갖고, 파악하기 더 쉬워지도록 하는 겁니다. 이 부분에 대해서는 다음 파트에서 functional architecture를 더 자세히 알아볼 것입니다.

Ordering

사이드 이펙트가 없는 순수 함수의 묶음을 가지고 있으면, 함수가 실행되는 순서는 상관 없어집니다.

내부적으로 3개의 순수 함수를 호출하는 함수 하나를 가정해봅시다:

Java

void doThings() {
    doThing1();
    doThing2();
    doThing3();
}

Kotlin

fun doThings() {
    doThing1()
    doThing2()
    doThing3()
}

이 함수들이 (한 함수의 결과가 다른 함수의 입력이 아니기 때문에) 서로 독립적이고, (순수함수이기 때문에) 시스템의 어떤 것도 바꾸지 않는다는 것이 분명히 보일 것입니다. 이것은 이 함수들이 실행되는 순서가 완전히 바뀔 수 있게 해줍니다.

독립적인 순수 함수들이기 때문에 실행 순서는 다시 섞이고 최적화될 수 있습니다. 만약 doThing2()의 입력값이 doThing1()의 결과값이 되면, 이 두 함수는 순서대로 실행되어야 하지만 doThing3()doThing1() 실행 전으로 여전히 재배치 가능합니다.

이런 순서 특성은 어떤 장점을 줄까요? 바로 Concurrency! 순서가 섞이는 것에 대한 걱정없이 이 함수들을 3개의 별도 CPU 코어에서 실행할 수 있습니다!

대부분 향상된 순수 함수형 언어(예, Haskell)의 컴파일러는 보통 코드를 분석하여 concurrent인지 아닌지 알려주고, deadlocks, race conditions 같은 경우에는 바로 알 수 있게 중지시킬 수 있습니다. 또한 이런 컴파일러들은 이론상 코드를 자동으로 병렬화할 수 있습니다(이건 사실 제가 아는 어떤 컴파일러에도 존재하지 않지만 연구 진행중입니다).

여러분의 컴파일러가 이런 점을 검사하지 않더라도, 함수 시그니처를 보고 코드가 concurrent인지 아닌지 파악할 수 있고, 숨겨진 사이드 이펙트로 가득한 코드를 병렬화하도록 노력하면 중첩된 스레딩 버그를 방지할 수 있습니다.

Summary

첫번째 파트가 FP에 대한 여러분의 흥미를 끌 수 있었으면 좋겠습니다. 순수함수이면서 사이드 이펙트가 없는 함수는 코드를 파악하기 더 쉽게 만들고, concurrency를 달성하기 위한 첫 걸음입니다.

concurrency에 대해 배우기 전에, immutability에 대해 알아야 합니다. Part 2에서 순수함수와 immutability가 얼마나 간단하고 lock과 mutex 저장없이 concurrent 코드를 이해하기 쉽게 작성하는 데 도움이 되는지 알아볼 것입니다.

Comments