본문 바로가기
Kotlin&Java/Reactive Programming

[Reactive Programming] 반응형 프로그래밍 이해하기

by 굿햄 2023. 2. 28.

 

Subject

RxJava, Flow, LiveData 등은 비동기 데이터 스트림을 위해 만들어진 라이브러리들입니다.
이러한 라이브러리는 반응형 프로그래밍(Reactive Programming) 구현을 위해 사용되는데,
이곳에서는 반응형 프로그래밍의 의미에 대해 살펴보고자 합니다.

 

반응형 프로그래밍 (What is Reactive Programming?)

리액티브 프로그래밍이 무엇일까? Wikipedia나 Stackoverflow에서는 매우 복잡한 이론인 듯 장황하게 설명되어 있습니다.
 

Reactive programming is programming with asynchronous data streams.
리액티브 프로그래밍이란 비동기 데이터 스트림과 함께하는 것이다.

 
여러 곳을 둘러보니 위와 같은 @andrestaltz의 문언이 이해하기 쉬우면서 많이 인용되고 있었습니다.
조금 다르게 표현한 곳을 찾았는데, 내용은 아래와 같습니다.
 

Reactive programming describes a design paradigm that relies on asynchronous programming logic to handle real-time updates to otherwise static content.
리액티브 프로그래밍은 정적 콘텐츠의 실시간 업데이트를 위해 비동기 프로그래밍 논리에 의존하는 디자인 패러다임을 설명한다.

 

 

둘 다 맞는 말이라고 생각합니다. 반응형 프로그래밍에 대한 이란 어떠한 이벤트가 발생하면 그것을 구독(subscribing)하고 있던 관찰자(Observer)가 이를 알아채고 UI(또는 Content)를 실시간으로 업데이트해 주는 것이라고 볼 수 있습니다.

 

즉, 반응형 프로그래밍은 데이터의 변경을 감지하여 전파하고 선언적으로 프로그래밍을 작성하기에 아래와 같은 구조를 가집니다.

흔히 자바스크립트에서 사용하는 AddEventListener나 안드로이드 개발 시 사용하는 SetOnClickListener와 같다고 보시면 됩니다.

textView.setOnClickListener {
	Log.d("User touch event", "click")
}

패러다임의 이해

그런데 이와 비슷한 개념이 또 뭐가 있을까요?
반응형 프로그래밍(Reactive Programming)도 프로그래밍 패러다임의 한 종류입니다.

패러다임이란 프로그래밍을 할 때의 관점 및 방법론으로, 하나 또는 여러 개의 하이브리드 방식을 취할 수 있다.

주변에서 흔히 보는 패러다임에는 객체지향(Object Oriented Programming), 함수형(Functional Programming), 반응형(Reactive Programming)을 예로 들 수 있습니다.

 

패러다임의 종류는 꽤나 많은데 이 중 명령형선언형이 대표적으로 자주 사용하는 패러다임이며, 이 둘은 간략하게 아래와 같이 설명할 수 있습니다.
 

  • 명령형 프로그래밍(Imperative programming): 어떻게(How) 또는 어떤 방법으로 돌아가야 하는지 알고리즘을 명시한다.
  • 선언형 프로그래밍(Declartive programming): 무엇(What)을 해야 하는지 목표를 명시한다.

 

예를 들어 간단하게 짝수만 출력하는 코드가 있다면,
명령형과 달리 선언형에서는 그 책임을 프로시저(함수)에 전가하는 것을 볼 수 있습니다.

// Print even numbers
val numList = 1..10

// 명령형 프로그래밍(Imperative programming)
for (num in numList) {
    if (num % 2 == 0) {
        Log.d("Print even numbers", num.toString())
    }
}

// 선언형 프로그래밍(Declarative programming)
numList.filter {
    it % 2 == 0
}.forEach {
    Log.d("Print even numbers", it.toString())
}

 
재미있는 점은 명령형 프로그래밍 언어로 비선언형 부분을 함수와 같이 캡슐화를 하면 선언형 프로그램을 작성할 수도 있다는 것입니다.
 
이러한 패러다임은 하나의 언어에 여러 개를 가질 수도 있으며, 변하기도 하는데
그 예로 자바는 초기에 명령형 프로그래밍, 객체지향 프로그래밍을 따랐으나
JDK 1.8 이상의 자바는 람다식, 생성자 레퍼런스(ex. Object::new), 메서드 레퍼런스(ex. System.out::println) 그리고 Stream과 같은 표준 API를 추가함으로써 함수형 프로그래밍 패러다임을 따라가고 있습니다.


아래는 위의 내용을 정리한 표 입니다. 이 외에도 종류는 많으나 대표적인 것만 기록하였다.

명령형 프로그래밍
절차적 - 프로시저(함수)의 호출을 바탕으로, 수행해야 할 과정을 나열한다.

- 모듈을 이용하여 유효범위를 제한하여 프로시저가 다른 프로시저에 접근하거나 접근하는 것을 막을 수 있다.
객체 지향 - 컴퓨터 프로그램을 명령어의 목록으로 보는 것에서 벗어나, 여러개의 독립된 단위 "객체"들의 집합으로 판단하고, 각각의 객체들 간 메시지 전달 및 데이터 처리를 할 수 있다.
선언형 프로그래밍
함수형 - 명령형 프로그래밍에서 어떻게 처리할지 나열하는 것과 다르게, 함수형 프로그래밍은 람다 연산을 발전시킨 것으로, 함수의 응용을 강조한다. 따라서 선언형 프로그래밍에 속한다.
반응형 - 람다 함수를 이용하여 단순히 목표를 전달하므로 선언형 프로그래밍에 속한다.

- 데이터의 변화에 이벤트를 발생시켜, 데이터를 지속적으로 전달한다. (변화의 전파와 데이터 흐름)

반응형 프로그래밍이 필요한 이유

제어의 역전

일반적인 UI 프로그램의 경우 데이터 소비의 주체는 View가 되며 Model에 정보의 업데이트를 요구합니다.

이제 우리는 이 정보가 Model에서 새로 갱신된 것을 인지하고 View에 띄워야 합니다.

 

Model의 정보가 갱신한 것을 다시 ViewModel에게 알리기 위한 방법은 두 가지가 있습니다.

  1. Model에서 직접 ViewModel을 호출하여 갱신 여부를 알리기
  2. View에서 Model의 함수를 호출할 때, Model의 정보가 갱신되면 어떠한 행동을 할지 알려주기

우선 두 상황 모두 Model에 대한 의존성은 View에게 있습니다.

1번 방식의 경우 명령식으로 코드를 작성하여 Model 또한 View의 함수를 호출해야 하므로 참조해야 하는 강한 결합이 만들어지고, 강한 의존성이 쌓여 더욱 상태관리가 힘들어집니다.

 

2번 방식의 경우 Model은 함수 호출의 주체인 View을 알 수 없으나, 어떠한 행동을 해야할 지 소유의 주체를 View로 전가하여 더욱 유연한 코드 작성이 가능합니다.

 

이러하듯이 모듈과 모듈 간의 결합 시, 참조의 주체를 바꾸는 것을 제어의 역전(Inversion of Control)이라 합니다.

 

책임의 분리 / 비동기 처리

제어의 역전을 통해 Model 또한 호출의 주체를 알지 않아도 가능하게 되었습니다.

이로 인해 책임의 분리가 이루어졌고, 코드의 수정이 이루어졌을 때, 우리는 Model의 수정 없이 View에서 어떤 일을 할지만 수정하여 전달해 주면 됩니다.

 

또 다른 큰 이점은 비동기 처리가 가능하다는 것입니다.

네트워크 요청 시 이루어지는 작업은 꽤나 큰 시간이 소요되므로 동기적으로 처리한다면 해당 프로그램의 사용자는 응답없는 UI에 매우 불편한 경험을 하게 됩니다.

// ViewModel Class
class HelloViewModel @Inject constructor(
    private val repository: RepositoryImpl
) : ViewModel() {
    fun updateRepository(num: Int): Result {
    	return repository.update(num)
    }
}

// Model Class
class RepositoryImpl @Inject constructor(
    private val dataSource: DataSource
) : Repository {
    fun update(num: Int): Result {
        return dataSource.update(num)  // 처리가 오래 걸리는 작업
    }
}

 

반응형 프로그래밍을 지향한다면, 위의 비동기 책임을 아래와 같이 ViewModel 또는 View에게 전가할 수 있습니다.

// View Class
@AndroidEntryPoint
class SampleActivity : CustomBaseActivity() {
	private lateinit var binding: ActivitySampleBinding
    private val sampleViewModel: SampleViewModel by viewModels()
    ...
    
    override fun onClick(view: View?) {
    	binding.apply {
            when (view) {
                buttonOne -> {
                    update(1)
                }
            }
        }
    }
    
    private fun update(num: Int) {
        // 비동기 작업을 위해 코루틴 생성
        lifecycleScope.launch {
            sampleViewModel.update(num).collect { result ->
                // Response 수신 후 수행할 동작
            }
        }
    }
}

// ViewModel Class
class SampleViewModel @Inject constructor(
    private val repository: RepositoryImpl
) : ViewModel() {
    fun updateRepository(num: Int): Flow<Result> {
    	return repository.update(num)
    }
}

// Model Class
class RepositoryImpl @Inject constructor(
    private val dataSource: DataSource
) : Repository {
    fun update(num: Int): Flow<Result> {
        return dataSource.update(num)  // 처리가 오래 걸리는 작업을 Flow로 반환
    }
}

이로써 UI가 멈추지 않고 네트워크 작업을 할 수 있습니다.


2부에서 RxJava와 Flow를 예시로 이어서 설명합니다.
감사합니다.


ⓒ 굿햄 2022. daryeou@gmail.com all rights reserved.

댓글