[안드로이드] 함수형 프로그래밍과 반응형 프로그래밍 개괄

dada·2022년 8월 23일
3

Android

목록 보기
14/16
post-thumbnail

✅공부배경

  • 프로젝트에서 UI Data의 data holding과 업데이트를 위해 StateFlow를 사용했는데, 반응형 프로그래밍에 대한 깊은 이해 없이 단순히 어려운 기술을 사용했다는 자기 만족 수준에 그쳤다는 것을 깨달았습니다. 프로젝트가 끝나고, 함수형 프로그래밍, 반응형 프로그래밍 전반, RxJava, Flow, StateFlow 등을 꼼꼼히 공부하며 해당스택을 이용할 줄만 아는 개발자가 아닌 원리를 이해한 개발자가 되고 싶었습니다💪

  • 본 포스팅에선 반응형 프로그래밍을 "코드"로 살펴보기 전 개괄적인 개념을 먼저 이해하며 머리에 큰 그림을 그리고, 다음 편부터 RxJava를 실습해보겠습니다.

✅함수형 프로그래밍 사전 지식

  • 반응형 프로그래밍을 알기 위해선 "함수형 프로그래밍"에 대한 이해가 선행되어야 합니다. 반응형 프로그래밍과 함수형 프로그래밍은 단짝처럼 같이 사용되기 때문입니다!

  • 함수형 프로그래밍은 하나의 프로그래밍 패러다임으로 정의되는 일련의 코딩 접근 방식이며, 자료처리를 수학적 함수의 계산으로 취급(자료처리를 함수를 이용해서 한다는 걸까)하고 상태와 가변 데이터를 멀리하는(전역변수를 멀리한다는 걸까) 프로그래밍 패러다임을 의미합니다.

  • 함수형 프로그래밍은 기존 절차적 프로그래밍과 객체 지향형 프로그래밍과는 다른 새로운 방식입니다

👉 절차적 프로그래밍

  • 절차적 프로그래밍 개념

    • 절차적 프로그래밍은 Procedure(프로시저)를 이용하여 작성하는 프로그래밍 스타일입니다. 프로시저는 메소드, 루틴 등이 있습니다

      • 프로시저 목록: 루틴(main문), 서브루틴(main문 밖에서 정의한 코드 블럭 중 반환 값이 없는 것), 함수(main문 밖에서 정의한 코드 블럭 중 반환 값이 있는 것)
    • 절차적 프로그래밍은 순차적인 처리를 중요시 여기며, 프로그램 전체가 유기적으로 연결되도록 만드는 프로그래밍 기법입니다

  • 절차적 프로그래밍의 장점

    • 모듈 구성이 용이하며 구조적인 프로그래밍이 가능합니다
    • 컴퓨터의 처리 구조와 유사해 실행 속도가 빠릅니다
  • 절차적 프로그래밍 단점

    • 유지보수가 어렵습니다
    • 정해진 순서대로 입력을 해야하므로 순서를 바꾸면 결과값을 보장할 수 없습니다
    • 코드가 길어지면 가독성이 매우 떨어집니다
  • 절차적 프로그래밍 종류

    • COBOL
    • C
    • FORTRAN

    👉객체 지향 프로그래밍

  • 객체지향 프로그래밍 개념

    • 소프트웨어의 급격한 발전으로 소프트웨어의 규모가 터지고 복잡해져 더 이상 절차지향 프로그래밍을 통해 소화할 수 없었습니다. 그 대안으로 객체지향 프로그래밍을 개발해 사용하게 됩니다!

    • 객체지향 프로그래밍이란, 절차적 프로그래밍을 보완한 새로운 패러다임으로 프로그래밍에 필요한 속성과 메서드를 가진 클래스를 정의합니다. 정의된 클래스를 통해 각각의 객체를 생성해 객체들 간 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍입니다.

  • 객체지향 프로그래밍 특징

    • 캡슐화: 변수, 함수를 클래스로 묶어두어 정보를 은닉합니다
    • 상속: 이미 작성된 클래스를 이어 받아 새로운 클래스를 생성하므로 코드의 재사용성을 높여줍니다
    • 다형성: 같은 이름의 변수, 함수가 상황에 따라 다른 의미로 해석될 수 있음을 뜻하는데, 다형성의 특성으로 인해 오버라이딩, 오버로딩이 가능합니다
  • 객체 지향 프로그래밍 장점

    • 코드의 재사용성 향상
    • 유지보수 용이
    • 코드 가독성 증가
    • 대형 프로젝트에 적합
  • 객체 지향 프로그래밍 단점

    • 처리속도가 절차지향보다 느림
    • 객체가 많아지면 용량이 커짐
    • 설계시 많은 시간, 노력 필요
  • 객체 지향 프로그래밍 종류

    • JAVA
    • C++

✅함수형 프로그래밍의 특징

  • 함수형 프로그래밍은 기존 절차적 프로그래밍과 객체지향 프로그래밍과는 다른 새로운 방식이라고 언급했습니다. 함수형 프로그래밍의 특징을 통해 어떤 방식의 패러다임인지 알아보겠습니다!

👉순수함수 (Pure function)

✔ 동일한 입력엔 항상 같은 값을 반환해야 하는 함수
✔ 함수의 실행이 프로그램의 실행에 영향을 미치지 않아야 하는 함수
✔ 함수 내부에서 인자의 값을 변경하거나 프로그램 상태를 변경하는 사이드 이펙트가 없는 것

  • 사이드 이펙트: 전역변수는 멀티 스레딩 환경에서 둘 이상의 스레드가 한 변수에 접근할 때 제대로된 조치를 취하지 않으면 파악하기 복잡한 오류가 발생할 위험 존재
var num=1

fun add(a){
 return a+num
}
  • 위 예제는 add()함수 안에 전역으로 선언된 변수인 num을 참조하므로 순수함수라고 볼 수 없습니다
fun add(a,b){
 return a+b
 }
 
 val answer=add(2,3)
  • 위처럼 add()함수가 프로그램 실행에 영향을 미치지 않고 입력 값에 대해서만 값의 변환이 있으므로 순수함수라고 할 수 있습니다

👉비상태, 불변성 (Stateless, Immutability)

✔ 함수형 프로그래밍에서의 데이터는 변하지 않는 불변성을 유지
✔ 데이터의 변경이 필요한 경우, 원본 데이터 구조를 변경하지 않고 그 데이터의 복사본을 만들어서 그 일부를 변경하고, 변경한 복사본을 사용해 작업을 진행

 data class Person(
    var name: String,
    var age:Int 
)

    var person = Person("연아", 12)

    fun increaseAge(person:Person) {
        person.age = person.age + 1
        return person
    }
  • 위의 예제에서는 increaseAge 함수에서 전역으로 선언된 person의 age 속성을 변경하므로 불변성 유지를 만족하지 못합니다
 data class Person(
    var name: String,
    var age:Int 
)

    var person = Person("연아", 12)

    fun increaseAge(person:Person) {
        return  Person(person.name, person.age + 1 )
    }
  • 위처럼 객체의 값을 바꾸기 위해서는 데이터의 복사본을 만들어, 그 복사본을 사용해 작업을 진행하고 반환합니다

👉선언형 함수 (Expressions)

✔ 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 주목하고, 선언헌 프로그래밍은 무엇을 할 것인가에 주목

  val numbers=mutableListOf(1,2,3)
  
  fun multiply(numbers:MutableList<Int>, multiplier){
     for(i in numbers.indices){
       numbers[i]=numbers[i]*multiplier
     }
  }
  • 위의 예시에서는 for문을 사용해서 배열의 각 요소에 multiplier 곱해주는 명령형 프로그래밍입니다

  • 함수형 프로그래밍에서는 마찬가지로 if,switch,for 등 명령문을 사용하지 않고 함수형 코드로 사용해야합니다

 fun multiply(numbers, multiplier):List<Int>{
   return numbers.map{it*multiplier}
 }
  • 위의 예시는 for문을 map으로 대치했습니다.

👉1급 객체와 고차함수 (Fist-class, Higher-order functions)

✔ 함수형 프로그래밍에서는 함수가 1급 객체가 됨
✔ 1급 객체의 특징

    1. 변수나 데이터에 할당 할 수 있어야 함
    1. 객체의 인자로 넘길 수 있어야 함
    1. 객체의 리턴값으로 리턴 할수 있어야 함

1. 변수나 데이터에 할당 할 수 있어야 함

//kotlikn
object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        val a = test
    }

    val test: () -> Unit = { println("kotlin") }
}
//java
public class java {

    public static void test(){
        System.out.println("java");
    }

    public static void main(String[] args) {
        System.out.println("java");
//        Object a = test;
    }
}
  • kotlin은 a 에 type이 () -> Unit 인 test 함수 할당이 가능하지만, Java는 불가능 합니다.

2. 객체의 인자로 넘길 수 있어야 함

object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        function(test)
    }

    fun function(f: () -> Unit) {
       f.invoke()
    }

    val test: () -> Unit = { println("kotlin") }
}
  • kotlin 은 function 함수의 인자로 함수타입을 전달 할 수 있습니다. 하지만 Java에서는 불가능 합니다.

3. 객체의 리턴값으로 리턴 할수 있어야 함

object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        function()
    }

    fun function(): () -> Unit {
        return { println("kotlin") }
    }
}
  • function함수는 { println(“kotlin”) }, 즉 함수타입을 반환 합니다.

  • kotlin에서 함수는 변수나 data에 할당이 가능하며, 함수의 인자로 전달가능 하고, 함수의 리턴값으로도 사용 할 수 있습니다.

  • 그렇기 때문에 kotlin의 함수는 1급 객체라고 할 수 있습니다. 반면 Java의 함수는 위 조건들을 만족하지 못하기 때문에 1급 객체라고 할수 없습니다.

✅반응형 프로그래밍 개괄

  • 반응형 프로그래밍을 하기 위해선 함수형 프로그래밍의 지원이 필요한데, 이때, 함수들을 직접 구현하기 보다 이미 만들어진 순수함수들(filter, map) 등을 주로 가져다 씁니다.

  • 반응형 프로그래밍을 "코드"로 살펴보기 전, 개괄적인 개념을 먼저 이해하고, 다음 편부터 RxJava코드를 통해 실습해보겠습니다.

  • 반응형 프로그래밍은 비동기적인 데이터 스트림을 이용한 프로그래밍 기법입니다. (데이터(=스트림=이벤트의 나열)가 변경->데이터를 계속 전달)

  • 반응형 프로그래밍에서는 모든 데이터를 "스트림"으로 봅니다.

    • 스트림이란
      • 시간순으로 발생하는 이벤트의 나열.
      • 스트림은 value, error, complete 의 각 시그널을 발생시킬 수 있음
  • 기본 베이스는 Observer Pattern입니다.

  • 하나의 데이터 스트림을 감시(구독)하는 대상이 있다면, 데이터 스트림의 변화가 발생할 경우
    변화 전파가 일어나는데 감시하는 대상은 이를 감지하여 관련 작업
    을 하게 됩니다

  • 즉, 데이터가 변경될 때마다 관련된 로직을 일일히 호출하는 것이 아니라, 데이터 스트림이 존재하고 이를 구독하는 곳에서 변화에 따라 알아서 처리하는 하겠다는 것입니다.

  • 반응형의 경우 모든 데이터를 스트림(시간순으로 발생하는 이벤트의 나열)으로 보고 스트림의 데이터가 변화되면 이를 전파하여 해당 데이터 스트림(이벤트의 나열)을 구독하고 있는 곳도 영향을 받습니다(상태변화의 흐름이 자동 전파)

  • 명령형 프로그래밍의 경우 어떤 동작에 중점을 두고 데이터의 소비자는 데이터를 요청한 후 받은 결과값을 일회성으로 수신하고 매번 요청합니다=비효율적!!

  • 반응형 프로그래밍의 경우 데이터 스트림의 변화에 중점을 두고 데이터를 발생하는 발행자는 새로운 데이터가 들어오면 데이터의 소비자에게 지속적으로 발행합니다

  • 반응형의 장점은 여러개의 스트림을 조합하여 새로운 스트림을 만들어 낼 수 도 있으며,비동기 연산들 또한 하나의 스트림으로 조합할 수 있고, 비동기 연산을 보다 쉽게 처리할 수 있도록 해줍니다

✅ReactiveX

ReactiveX
= 'Reactive eXtensions' 반응을 확장
= Rx
= "옵저버블 스트림으로 비동기 프로그래밍(실행흐름 안기다리고 바로 다음코드)을 하기 위한 API = 반응형 프로그래밍(모든 데이터를 스트림으로 보는)을 위한 API
= 데이터 처리 자체가 오래걸리는 작업이니까 비동기로 처리해야하고 데이터스트림으로 만들어서 처리하고 싶을 때 쓰는 라이브러리=RxJava

  • Rx는 다양한 시리즈가 있고 안드로이드 iOS 등 어느 플랫폼에서나 다 사용할 수 있습니다
  • 이것이 가능한 이유는 Rx는 각 플랫폼마다 지원하는 시리즈, 정확히는 라이브러리가 존재하기 때문입니다.

  • iOS 플랫폼에서 Rx를 사용할 수 있게 해주는 RxSwift라이브러리, Android 플랫폼에서 Rx를 사용할 수 있게 해주는 RxJava라이브러리가 있습니다

  • 각 플랫폼의 개발 언어 앞에 Rx접두어를 붙인 이름의 라이브러리들을 만들어 놨기에 가능한 것입니다.

✅Rx를 이해하기 위한 비유

  • Rx를 이해하기 위해 먼저 한 퀘스트가 있다고 가정하겠습니다.

<퀘스트>
상황:
1. 오직 한 방향으로만 흐르는 강(stream)이 있습니다.
2. 물고기(value)는 강(stream)의 흐름방향으로 흘러갑니다.
3. 강에는 가끔 쓰레기(value)도 흘러갑니다.
목표:
'rx'라는 강에서 물고기를 건져, 회를 뜬다음, 팔아야 합니다.
당신이 한 일:
강에서 자동으로 물고기만 건져(filter) 회로 변환(map)하는 'A'시스템을 만들었습니다.
이 시스템은 사용자가 버튼을 누를 시(subscribe) 가동됩니다.

  • 스트림은 주로 강으로 비유됩니다. 상식적으로 강은 한 방향으로만 흐르며, 우리가 무언가를 하지 않는 이상 아무 일도 생기지 않습니다. 이것이 강과 스트림을 비유하는 이유입니다

  • 어떠한 이벤트 스트림이 있다고 해도, 그것만으로는 아무 일도 일어나지 않습니다. Observable도 강이라고 생각하시면 됩니다. 하지만 Observable은 강이지만, 특정한 강을 지칭할 때 사용한다고 생각하시면 됩니다. 다 같은 강이지만 한강, 낙동강처럼 특정한 강이 있죠? 스트림은 포괄적인 느낌이며, Observable은 스트림이지만, 특정 스트림을 의미한다고 생각하시면 됩니다.

  • 'A'시스템을 가동하기 위해서는 버튼을 눌러야 합니다. 이 버튼을 누르는 행동인 subscribe 즉, 구독을 함으로써 시스템이 가동됩니다. 하지만 우리는 그냥 강이 아닌 'rx'라는 강에서 가동이 되야겠죠?

  • 그래서 우리는 'A'시스템을 'rx'강에서 가동하기 위해, 'rx'라는 Observable을 구독해야 합니다. 강은 수없이 많습니다. 그 수많은 강 중에서 특정한 강(Observable)을 구독해야 시스템은 그 강에서 가동될 것입니다.그리고 Observable을 구독하지 않는 이상 아무 변화가 일어나지 않을 겁니다.

  • Operators는 스트림에 흐르는 값(value)을 가공하는 장치입니다. 위 퀘스트에서 우리는 물고기만 건져 회로 변환한다고 하였습니다. 장치(Operators)의 기능 중 물고기만을 건지게 해 주는 건 filter, 회로 변환하는 건 map. Operators안에 filter와 map과 같은 기능들이 있다고 생각하시면 됩니다.

  • 이를 코드로 나타내면 아래와 같습니다(참조한 블로그에서 RxSwift로 예시를 들어주셨습니다 RxJava를 더 공부하고 RxJava코드로 변경하겠습니다! 조금만 기다려주세요😊)

import RxSwift
 
let disposeBag = DisposeBag()
 
let value = ["물고기", "쓰레기"] // 강에 흐르는 value
let rx = Observable.from(value) // Observable: value가 흐르는 'rx'강
    
rx
    .filter { $0 == "물고기" } // Operators: 물고기만 건짐
    .map { "\($0) 회" } // Operators: 물고기를 회로 만듦
    .subscribe { print($0.element ?? "") } // Subscribe: 'rx'강을 구독
    .disposed(by: disposeBag)
    
// 결과: 물고기 회
  • rx라는 옵저버블이 있고, 그것을 구독한 다음에 값을 얻습니다 Rx 사이트에서 말하는 이 '옵저버블 스트림'은 그냥 '옵저버블들'을 뜻하는 거라고 생각하면 됩니다.

비유 출처) https://axe-num1.tistory.com/22

✅RxJava와 Coroutine

  • 많은 블로그에서 RxJava와와 Coroutine을 비교합니다. 이 두 스택은 비동기 처리를 도와주는 부분은 동일합니다.

  • 하지만 Asynchronous 처리를 어떠한 형태로 할 것인지가 다를 뿐입니다.

✔ RxJava
Observable pattern으로 구독
구독 후 들어오는 data를 stream 형태로 내려보냄
중간중간 데이터가 변환되는 걸 stream을 통해 확인 가능
.
✔ Coroutines
함수 호출을 하고, return이 오기 전까지 기다림
처리가 끝나 return 한 데이터를 가지고 다음 처리

  • RxJava와 Coroutines을 비교하는 게 맞을까?라는 의문이 들수 있는데, 같은 기능=비동기처리를 한다는 점에서는 비교하는 게 맞을 수 있습니다.

✅Flow는 Coroutine에 추가된 스펙일 뿐!

  • Flow는 Coroutines 1.3에 정식으로 추가된 스펙(인터페이스)이고, 결국 RxJava의 Cold observable같은 기능을 해줍니다.

  • RxJava가 데이터 스트림작업을 비동기 처리로 하는걸 지원하는 라이브러리인 것 처럼 Coroutine Flow도 코루틴 상(비동기)에서 스트림 작업 지원을 위한 구현체입니다

  • 코루틴에서 데이터스트림(강 패러다임)만들고 싶으면 Flow를 사용해야합니다

  • 결국 RxJava의 Cold observable을 구현하고, Data stream을 만들어준 부분인데, RxJava를 안다면 어렵지 않게 접근 가능합니다

  • 샘플코드를 보겠습니다. for 문으로 1..3까지 emit 하는 간단한 코드입니다 foo() 함수의 collect이 호출되기 전까지는 동작하지 않는데, RxJava에서는 subscribe와 동일합니다

fun foo(): Flow<Int> = flow {
    println("Flow started")
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}
  • collect는 RxJava 기준 subscribe의 onNext에 해당하며, emit을 호출하면 stream에 데이터가 흐르며 collect에서 하나씩 받아 처리할 수 있다.
fun main() = runBlocking<Unit> {
    println("Calling foo...")
    val flow = foo()
    println("Calling collect...")
    flow.collect { value ->
      println(value)
    }
    println("Calling collect again...")
    flow.collect { value ->
      println(value)
    }
}

click-> Flow 자세히 공부하고 다시 돌아오자!!

✅Flow vs StateFlow

  • flow는 데이터의 흐름을 발생시키기만 할 뿐이라서 화면 돌리면 UI가 사용하는 데이터 초기화되어서 다시 API요청해서 flow흐르게 해야함 즉 데이터가 저장이 안됩니다

  • 그래서 flow로 흐른 데이터를 viewmodel에서 마지막에 저장을 해두고 이 데이터를 UI가 보고있으면 뷰모델은 UI Controller보다 수명주기가 길기 때문에 flow가 끊겨도 UI가 이 데이터를 UI Data로 계속 들고있을 수 있음-> 근데 이 저장만을 위한 데이터를 만드는게 비효율적인 보일러 플레이트를 발생시킵니다.

  • 흐르기도 하고=flow 데이터 홀더 역할도 해주면 안될까?하고 등장한게 StateFlow입니다 화면 재구성되어도 마지막 데이터를 홀딩하고 있어서 UI Data가 해당 데이터로 바로 업데이트 됩니다

click-> 콜드 스트림 vs 핫 스트림 자세히 공부하고 다시 돌아오자!!

profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글