관리 메뉴

나만을 위한 블로그

[Android] EventBus란? EventBus 사용법 본문

Android

[Android] EventBus란? EventBus 사용법

참깨빵위에참깨빵 2022. 11. 21. 16:29
728x90
반응형

EventBus는 그린로봇(greenrobot)이라는 곳에서 만든 오픈소스 라이브러리로, 아파치 2.0 라이선스를 적용하고 있으며 만들어진 지는 10년 정도 됐다.

깃허브는 아래 링크를 타고 들어가서 확인할 수 있다.

 

https://github.com/greenrobot/EventBus

 

GitHub - greenrobot/EventBus: Event bus for Android and Java that simplifies communication between Activities, Fragments, Thread

Event bus for Android and Java that simplifies communication between Activities, Fragments, Threads, Services, etc. Less code, better quality. - GitHub - greenrobot/EventBus: Event bus for Android ...

github.com

 

공식 홈페이지도 있는데 공식 홈페이지에선 EventBus를 아래와 같이 소개하고 있다.

 

https://greenrobot.org/eventbus/

 

EventBus: Events for Android - Open Source by greenrobot

EventBus is the #1 open-source event library for Android; making code easily maintainable, running on more than a billion of devices

greenrobot.org

EventBus는 느슨한 결합을 위해 게시자/구독자(publisher/subscriber) 패턴을 사용하는 안드로이드 및 자바용 오픈소스 라이브러리다. 단 몇 줄의 코드로 분리된 클래스에 대한 중앙 통신을 가능하게 해서 코드를 단순화하고 종속성을 제거하며 앱 개발 속도를 높인다. EventBus로 얻을 수 있는 이점은 아래와 같다.

- 컴포넌트 간 통신 단순화
- 이벤트 발신자, 수신자 분리
- UI 아티팩트(액티비티, 프래그먼트) 및 백그라운드 쓰레드와 잘 작동함
- 복잡하고 오류가 발생하기 쉬운 종속성 및 생명주기 문제 방지
- 배달 쓰레드(delivery threads), 구독자 우선 순위(subscriber priorities) 등 고급 기능 보유

 

위 설명과 같이 아래 이미지를 보여주고 있다.

 

 

그림만 놓고 보면 이벤트를 보내는 주체(publisher)가 post()에 어떤 이벤트를 넣어서 보낸다고 EventBus에 알리면, EventBus를 구독하고 있는 대상(subscriber)에게 onEvent() 콜백을 통해 이벤트를 전송해주는 듯하다.

공식문서의 설명만으론 부족한 느낌이 드니 다른 곳에선 EventBus를 어떻게 설명하는지 찾아봤다.

 

http://www.rribbit.org/eventbus.html

 

RRiBbit – What is an Eventbus

What is an Eventbus? An Eventbus is a mechanism that allows different components to communicate with each other without knowing about each other. A component can send an Event to the Eventbus without knowing who will pick it up or how many others will pick

www.rribbit.org

EventBus는 서로 다른 컴포넌트가 서로에 대해 알지 못하는 상태에서 서로 통신할 수 있도록 하는 매커니즘이다. 컴포넌트는 누가 그것을 받을지 또는 얼마나 많은 다른 사람들이 그것을 받을지 모른 채 이벤트를 EventBus로 보낼 수 있다. 컴포넌트는 누가 이벤트를 보냈는지 몰라도 EventBus에서 이벤트를 수신할 수 있다. 이렇게 하면 컴포넌트가 서로 의존하지 않고 통신할 수 있다. 또한 부품 교체가 매우 용이하다. 새 컴포넌트가 보내고 받는 이벤트를 이해하는 한 다른 컴포넌트는 절대 알 수 없다.
그렇다면 컴포넌트는 무엇인가? 컴포넌트는 무엇이든 될 수 있다. 대부분의 EventBus에선 자바 객체다. 그들은 이벤트를 보내고 받는 메시지다. 일반적으로 이벤트를 처리하기 위해 수신자가 알아야 하는 모든 것을 포함한다. EventBus에 대한 다른 모든 것은 거의 구현에 따라 다르다. 일반적으로 자바 EventBus는 수신자에 대한 데이터로 채울 수 있는 이벤트 객체를 생성하기 위해 이벤트 발신자가 필요하다. 발신자는 eventbus.send(event)를 호출한다. 수신자는 EventBus에 의해 호출되는 onEvent(Event event)를 써서 특정 인터페이스를 구현해야 한다. 따라서 대부분의 EventBus는 단방향 통신만 지원한다...(중략)...EventBus는 컴포넌트가 서로 의존하는 걸 원하지 않을 때 유용하다. 다른 컴포넌트에 대한 참조가 많은 컴포넌트 대신 이벤트를 EventBus로 보낼 수 있으며 누가 처리할지 걱정할 필요가 없다. 이렇게 하면 응용 프로그램을 독립된 부분으로 쉽게 분할할 수 있다.
EventBus를 쓰는 또 다른 이유는 다른 시스템에서 실행되는 컴포넌트를 호출하고 통신 계층을 직접 관리하고 싶지 않아서다. 이를 지원하는 EventBus는 이벤트를 수신하는 컴포넌트에서 선택하는 다른 시스템으로 이벤트를 보낼 수 있다.

 

https://code.tutsplus.com/articles/effective-android-components-communication-with-greenrobot-eventbus--cms-27654

 

Communication Within an Android App With EventBus

A typical Android app tends to be composed of many layers, modules or structures such as Fragments, Activities, Presenters, and Services. Effective communication between these components can...

code.tutsplus.com

일반적인 안드로이드 앱은 프래그먼트, 액티비티, Presenter, Service 같은 많은 계층, 모듈 또는 구조로 구성되는 경향이 있다. 이런 컴포넌트가 서로 밀접하게 결합돼 있으면 컴포넌트 간 효과적인 통신이 어려울 수 있다. DB 같은 앱 아키텍처의 하위 수준에서 작업이 발생하면 뷰와 같은 상위 수준으로 데이터를 보낼 수 있다. 이를 위해 리스너 인터페이스, 비동기 작업 또는 콜백을 만들 수 있다. 이 모든 것이 작동하지만 아래와 같은 몇 가지 주요 단점이 있다

- 직접 or 긴밀한 커플링(결합)
- 여러 의존성을 개별적으로 등록 및 등록 취소
- 코드 반복
- 테스트 어려움
- 버그 위험 증가

게시/구독(publish/subscribe) 또는 메시지 버스 아키텍처를 쓰면 위에서 말한 모든 잠재적인 문제를 방지할 수 있다. 다른 컴포넌트를 즉시 인식할 필요 없이 응용 프로그램의 컴포넌트 간 효과적인 통신을 구현하는 매우 좋은 방법이다. 안드로이드에서 게시/구독을 사용하면 모든 앱 컴포넌트가 버스로 전달할 이벤트를 게시할 수 있으며 관련 소비자는 이벤트를 사용하거나 구독할 수 있다

 

그리고 EventBus는 게시/구독 패턴을 알아야 더 잘 이해할 수 있을 것 같다. 이것이 어떤 패턴인지 확인해봤다.

 

https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern

 

Publish–subscribe pattern - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Messaging pattern in which senders and receivers do not directly communicate In software architecture, publish–subscribe is a messaging pattern where senders of messages, called publ

en.wikipedia.org

게시-구독은 게시자라고 하는 메시지 발신자가 구독자라는 특정 수신자에게 메시지를 직접 보내게 프로그래밍하지 않고 게시된 메시지를 구독자가 있는지 모르는 클래스로 분류하는 메시징 패턴이다. 구독자는 하나 이상의 클래스에 관심을 표현하고 관심 있는 메시지만 수신한다. 어떤 게시자가 있는지 모른다. 게시-구독은 메시지 대기열 패러다임의 형제고 일반적으로 더 큰 메시지 지향 미들웨어 시스템의 한 부분이다. 대부분의 메시징 시스템은 API에서 게시/구독 및 메시지 대기열 모델을 모두 지원한다. 이 패턴은 더 큰 네트워크 확장성과 더 동적인 네트워크 토폴로지를 제공하므로 게시자와 게시된 데이터의 구조를 수정할 수 있는 유연성이 줄어든다

 

https://jistol.github.io/software%20engineering/2018/04/11/observer-pubsub-pattern/

 

Observer 패턴과 Publisher/Subscriber(Pub-Sub) 패턴의 차이점

 

jistol.github.io

Head First Design Patterns 책에는 옵저버 패턴 == 게시-구독 패턴으로 나와 있지만 실제 찾아보면 비슷한 개념 사이에 확연한 차이점이 존재한다. 가장 큰 차이점은 Message Broker 또는 EventBus가 있는지 여부다.
옵저버 패턴은 Subject에 옵저버를 등록하고 직접 옵저버에 알려줘야 한다. 게시-구독 패턴의 경우 게시자가 구독자의 위치, 존재를 알 필요 없이 메시지 큐와 같은 Broker 역할을 하는 중간 지점에 메시지를 놓기만 하면 된다. 반대로 구독자 역시 게시자의 위치, 존재를 알 필요 없이 Broker에 할당된 작업만 모니터링하다 할당 받아 작업하면 되기 때문에 게시자와 구독자가 서로 알 필요가 없다. 게시자와 구독자가 서로의 존재를 알 필요가 없기 때문에 소스코드 역시 겹치거나 의존할 일이 없다

 

옵저버 패턴은 구독자와 게시자 간의 직접적인 이벤트 전송, 구독만 있지만 게시-구독 패턴은 게시자와 구독자 사이에 EventBus 같은 중간 매체가 있어서 게시자와 구독자는 서로 어디에 있는지 모르더라도 메시지를 보내고 받을 수 있다.

 

이제 EventBus 사용법을 확인해 본다. 액티비티 안에 프래그먼트 2개를 만들고 액티비티에서 프래그먼트로 이벤트를 전송하고 프래그먼트는 이벤트를 받으면 토스트를 띄우게 할 것이다.

먼저 뷰페이저2를 쓸 거니까 어댑터부터 만든다.

 

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter

class EventBusViewPagerAdapter(
    activity: AppCompatActivity
): FragmentStateAdapter(activity) {
    override fun getItemCount(): Int = 2

    override fun createFragment(position: Int): Fragment {
        val fragment = when (position) {
            0 -> {
                EventBusFirstFragment()
            }
            1 -> {
                EventBusSecondFragment()
            }
            else -> {
                EventBusFirstFragment()
            }
        }

        return fragment
    }

}

 

그리고 프래그먼트 2개를 만든다.

 

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".eventbus.EventBusFirstFragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="1번 프래그먼트" />

</FrameLayout>
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.example.kotlinprac.R
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode

class EventBusFirstFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_event_bus_first, container, false)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        EventBus.getDefault().register(this)
        Log.e("1번 프래그먼트", "이벤트 등록")
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun getUser(user: User) {
        Toast.makeText(requireActivity(), "1번 프래그먼트에서 받은 user : ${user.name}", Toast.LENGTH_SHORT).show()
    }

    override fun onStop() {
        EventBus.getDefault().unregister(this)
        super.onStop()
        Log.e("1번 프래그먼트", "이벤트 해제")
    }

}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".eventbus.EventBusSecondFragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="2번 프래그먼트" />

</FrameLayout>
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.example.kotlinprac.R
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode

class EventBusSecondFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_event_bus_second, container, false)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        EventBus.getDefault().register(this)
        Log.e("2번 프래그먼트", "이벤트 등록")
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun getUser2(user: User) {
        Toast.makeText(requireActivity(), "2번 프래그먼트에서 받은 user : ${user.name}", Toast.LENGTH_SHORT).show()
    }

    override fun onStop() {
        EventBus.getDefault().unregister(this)
        super.onStop()
        Log.e("2번 프래그먼트", "이벤트 해제")
    }

}

 

프래그먼트의 onCreate()와 onStop()에서 EventBus를 구독하고 구독 해지하는 걸 볼 수 있다. EventBusFirstFragment를 만들었으니 EventBusSecondFragment도 똑같은 구조로 만든다.

onCreate()와 onStop()에서 처리하는 이유는 이렇게 처리하라고 공식 깃허브에 스니펫을 걸어놨기 때문에 이렇게 작성한 것이다. 단 나는 onCreate()에 등록 코드를 넣은 것처럼 앱에 따라 유동적으로 처리할 수 있겠다.

 

 

그리고 EventBus를 통해 보낼 클래스를 만든다.

 

data class User(val name: String)

 

액티비티도 마저 만들어준다.

 

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".eventbus.EventBusTestActivity">

        <LinearLayout
            android:id="@+id/llTest"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/btnFirstFragment"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight=".5"
                android:text="1번 프래그먼트"/>

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/btnSecondFragment"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight=".5"
                android:text="2번 프래그먼트"/>

        </LinearLayout>

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vpTest"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:overScrollMode="never"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/llTest" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.kotlinprac.databinding.ActivityEventBusTestBinding
import org.greenrobot.eventbus.EventBus

class EventBusTestActivity : AppCompatActivity() {

    private val binding: ActivityEventBusTestBinding by lazy {
        ActivityEventBusTestBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        init()
    }

    private fun init() {
        binding.run {
            vpTest.run {
                offscreenPageLimit = 1
                adapter = EventBusViewPagerAdapter(this@EventBusTestActivity)
            }

            btnFirstFragment.setOnClickListener {
                EventBus.getDefault().post(User(name = "user"))
                vpTest.currentItem = 0
            }
            btnSecondFragment.setOnClickListener {
                EventBus.getDefault().post(User(name = "user2"))
                vpTest.currentItem = 1
            }
        }
    }

}

 

버튼을 누를 때마다 name을 user, user2로 다르게 해서 EventBus로 이벤트를 보낸다. 실행하면 아래와 같다.

 

 

뷰페이저 안의 모든 프래그먼트에서 EventBus로부터 오는 이벤트를 받도록 설정했기 때문에 버튼을 눌러 프래그먼트가 바뀔 때마다 모든 프래그먼트에서 같은 데이터를 받는다. 1번 버튼을 누르면 모든 프래그먼트에서 user를 받고 2번 버튼을 누르면 모든 프래그먼트에서 user2를 받는다.

그리고 뷰페이저2 어댑터 설정 시 offscreenPageLimit을 1로 설정했는데 이렇게 한 이유가 있다. 실행할 때 이걸 없애고 실행해보고 위의 GIF와 어떤 차이가 있는지 직접 확인해보자.

지금은 대충 만든 data class로 테스트 했지만 실제로 사용할 때는 API로 받은 DTO에 맞게 이벤트 클래스를 만들어서 API 통신 결과를 받으면 화면에 뿌려줄 때 사용할 수도 있겠다.

반응형
Comments