이벤트 기반 아키텍처와 메시지 기반 아키텍처

2023. 4. 2. 19:43모바일프로그래밍/안드로이드

728x90

 

 

 

 

Android : Event Driven Architecture vs Message Driven Architecture

As Android developers, one of the biggest issues that we have to deal with is how to communicate between the Android components without leaking memory, as we have a very limited resources, and it is easy to mess around with the available memory ..

www.linkedin.com

 

Android 개발자로서 우리가 처리해야 하는 가장 큰 문제 중 하나는 메모리 누수 없이 Android 구성 요소 간에 통신하는 방법을 찾는 것이며, 이는 우리의 리소스가 매우 제한되어 있고 가용 메모리를 쉽게 엉망으로 만들 수 있기 때문이기도 합니다.

우리는 구성 요소 간에 통신할 수 있도록 특정 관행을 따라야 하며 힙에 필요한 것보다 더 오래 남겨두지 말아야 합니다.

이런 이유로 시스템/응용 프로그램에서 구성 요소가 통신하는 방법을 정의하는 여러 아키텍처가 있는 것입니다.

 

이벤트 기반 아키텍처 - Event Driven Architecture

Android에서 Event-Bus 또는 Broadcast-Receiver를 다룬 적이 있다면 어떤 이유로든 Event Driven Architecture를 어느 정도는 다루어 본 것입니다.

이벤트를 통해 시스템/애플리케이션의 구성 요소 간의 통신을 정의하는 이 유형의 아키텍처는 이벤트(브로드캐스트)는 누군가에 의해 시작되고, 해당 이벤트에 관심이 있는 모든 사람은 이 이벤트(브로드캐스트)에 주의를 기울일 것이며 각자 구현 방식에 따라 해당 이벤트에 대한 조작을 하려고 할 것입니다.

 

이러한 접근 방식의 일반적인 용도는 우리가 서비스 또는 프래그먼트 등에서 진행 중인 작업을 액티비티에 알려야 할 필요가 있을 때 입니다.

하지만 이벤트 기반 아키텍처에서 잘 알려진 문제 중 하나는 ...

예를 들어 다음과 같이 우리가 생각하는 범위에서 쉽게 벗어나 조작이 쉽지 않을 수 있다는 것입니다:

 

public class FragmentOne extends Fragment {

    private final BroadcastReceiver broadcastReceiverOne = createReceiver();

    private BroadcastReceiver createReceiver() {
        return new BroadcastReceiver() {
            public void onReceive(Context context, Intent intent) {
                String action = getString(R.string.broadcast_action_two);
                Intent newIntent = new Intent(action);
                getActivity().sendBroadcast(newIntent); // send action_two
            }
        };
    }

    public void onStart() {
        super.onStart();
        String action = getString(R.string.broadcast_action_one);
        IntentFilter intentFilter = new IntentFilter(action);
        getActivity().registerReceiver(broadcastReceiverOne, intentFilter);
    }

    public void onStop() {
        getActivity().unregisterReceiver(broadcastReceiverOne);
        super.onStop();
    }
}


public class FragmentTwo extends Fragment {

    private final BroadcastReceiver broadcastReceiverTwo = createReceiver();

    private BroadcastReceiver createReceiver() {
        return new BroadcastReceiver() {
            public void onReceive(Context context, Intent intent) {
                String action = getString(R.string.broadcast_action_one);
                Intent newIntent = new Intent(action);
                getActivity().sendBroadcast(newIntent); // send action_one
            }
        };
    }

    public void onStart() {
        super.onStart();
        String action = getString(R.string.broadcast_action_two);
        IntentFilter intentFilter = new IntentFilter(action);
        getActivity().registerReceiver(broadcastReceiverTwo, intentFilter);
    }

    public void onStop() {
        getActivity().unregisterReceiver(broadcastReceiverTwo);
        super.onStop();
    }
}
 
 

위의 예에서 FragmentOne이 FragmentTwo에서 처리할 브로드캐스트를 보낸 다음 FragmentTwo가 FragmentOne에서 처리할 브로드캐스트를 보내므로 두 프래그먼트가 하나의 액티비티에서 (동시에) 사용할 때까지 모든 것이 잘 진행됩니다. FragmentTwo에 다시 브로드캐스트하고 이제 무한 루프에 들어 갈 것입니다.

 

이것은 작은 예에서는 매우 명확할 수 있지만 실제 생활에서는 애플리케이션이 너무 커서 혼란을 야기한 이벤트(브로드캐스트)를 누가 트리거했는지 추적하기 어렵기 때문에 이런한 비슷한 아키텍처는 솔루션과 함께 제공되었습니다.

 

메시지 기반 아키텍처

이러한 유형의 아키텍처에서는 구성 요소들이 메시지를 통해 서로를 처리합니다. Event Driven Architecture와 매우 유사하지만 한 가지 차이점이 있습니다.

모든 애플리케이션 구성 요소에 이벤트를 발생시키는 대신 특정 구성 요소에 대해서만 이벤트를 트리거 되도록 하는 것입니다. 그리고 이 이벤트는 이제 전체 구성 요소에게 보내는 것이 아닌 특정 구성 요소로 전송되기 때문에 메시지라고 합니다.

 

Android에서는 핸들러와 메신저를 통해 이 작업을 수행할 수 있지만 Android 스타일은 수동으로 너무 많은 항목을 설정해야 하기 때문에 너무 원시적입니다. 이러한 백엔드의 메시지 기반의 아키텍쳐기반에서 Akka, Killim, JetLang, FunctionalJava 및 Android에는 Kotlin Actor Coroutines, Kotlin 채널 Coroutines 및 Quasar 같은 기술들이 존재합니다.

 

하지만 저는 개인적으로 Akka의 라이트 버전인 ActorLite를 선호하고, Android의 경우 현재 1년 이상 프로덕션에서 사용하고 있으며 어떤 프로젝트에서도 환경 설정 위한 어떤 노력도 필요하지 않습니다:

프로젝트: https://github.com/Ahmed-Adel-Ismail/ActorLite

 

First if we want to send a Message to a component ... say FragmentTwo, the sender (which is any Object from ANY thread) will just do the following :

먼저 구성 요소에 메시지를 보내려면 ... FragmentTwo의 경우에 보낸 사람(모든 스레드의 모든 개체)은 다음의 코드를 만들 것입니다.

MyObject myObject = new MyObject(); // the Object to send
Message message = new Message(R.id.message_id_one,myObject);
ActorSystem.send(message,FragmentTwo.class);
 

보낸 사람이 지연(밀리초) 후에 이 메시지를 보내려는 경우 코드는 다음과 같을 것입니다:

MyObject myObject = new MyObject(); // the Object to send
Message message = new Message(R.id.message_id_one,myObject);
ActorScheduler.after(5000).send(message,FragmentTwo.class);
 

그리고 FragmentTwo의 경우 다음 느낌처럼 되어야 합니다:

public class FragmentTwo extends ActorFragment {

	@NonNull
	@Override
	public Scheduler observeOnScheduler() {
		// specify the Thread that onMessageReceived()
		// will be executed on, in Activities 
		// and Fragments it should be the 
		// Main Thread
		return AndroidSchedulers.mainThread();
	}


	@Override
	public void onMessageReceived(Message message) {
		if(message.getId() == R.id.message_id_one){
			// handle message on the Thread 
			// specified in observeOnScheduler() 
		}
	}
}
 

위의 코드가 전부 입니다. 부모 Fragment가 이미 있고 ActorFragment를 확장할 수 없는 경우 ActorLite의 README에 설명된 대로 수동으로 ActorSystem에 등록/등록 취소할 수 있습니다.

 

다음 두 가지 이점이 있습니다:

 

  1. 이벤트를 FragmentTwo.class로 보내면 FragemntTwo 이외의 구성 요소에서는 수신하지 못할 것입니다.
  2. UI 스레드 또는 백그라운드 스레드(RxJava 스케줄러 사용)에서 수신된 메시지/이벤트를 처리할 수 있는 추상 메서드 인 observeOnScheduler()에서 제공하는 이벤트를 처리해야 하는 FragmentTwo가 이 스레드를 제어하는 ​​구성 요소가 됩니다.

 

Service에서 Activity로 또는 Fragment에서 Activity로 메시지를 보내는 것과 같이 모든 구성 요소 간의 모든 통신에 동일하게 적용됩니다. 메시지를 보내기 위해 객체에 대한 참조를 보유할 필요가 없습니다. 구성 요소가 여전히 ActorSystem에 등록되어 있으면 수신하고 그렇지 않으면 아무 일도 일어나지 않을 것이기 때문입니다.

 

엄청 많은 메시지 처리

Event Driven Architecture 또는 Message Driven Architecture를 사용 할 때 일반적인 절충점은 Control Coupling입니다. 이는 다음 예와 같이 BroadcastReceiver.onReceive() 또는 Actor.onMessageReceived() ...에 나타날 if/else 또는 switch/case 문에서 더욱 명확 해 질 것입니다:

public class MainService extends ActorService {

    @Override
    public Scheduler observeOnScheduler() {
	    return Schedulers.io();
    }

    @Override
    public void onMessageReceived(Message message) {
        if(message.getId() == R.id.message_id_request_user_friends) {
            Long userId = message.getContent();
            new UserFriendsRequester().accept(userId);			
        } else if(message.getId() == R.id.message_id_check_location) {
            Location location = message.getContent();
            boolean valid = new LocationValidator().test(location);
            Message newMessage = new Message(R.id.message_id_location_checked,valid)
            ActorSystem.send(newMessage,MainActivity.class);
        } else if(message.getId() == R.id.message_id_request_offers) {
            new OffersRequester().run();
        }
    }
}
 

보시다시피 모든 코드는 if/else 블록에 의해 제어되는 하나의 방대한 메서드에 존재하며 이 제어 결합은 메시지 발신자의 어떠한 변경 사항도 이 메서드에 영향을 미치며 이러한 긴밀한 결합에 대한 솔루션으로 CommandsMap 라이브러리에 구현된 Table-lookup 입니다. : https://github.com/Ahmed-Adel-Ismail/CommandsMap

 

다음 코드 조각은 동일한 클래스에 대해 if/else 문 대신 CommandsMap을 사용하게 해 줍니다:

@CommandsMapFactory
public class MainService extends ActorService {

    private final CommandsMap commandsMap = CommandsMap.of(this);
	
    @Override
    public Scheduler observeOnScheduler() {
	    return Schedulers.io();
    }

    @Override
    public void onMessageReceived(Message message){
        commandsMap.execute(message.getId(),message.getContent());
    }

    @Command(R.id.message_id_request_user_friends)
    void onRequestUserFriends(Long userId){
        new UserFriendsRequester().accept(userId);
    }

    @Command(R.id.message_id_check_location)
    void onCheckLocation(Location location){
        boolean valid = new LocationValidator().test(location);
        Message message = new Message(R.id.message_id_location_checked,valid)
        ActorSystem.send(newMessage,MainActivity.class);
    }


    @Command(R.id.message_id_request_offers)
    void onRequestOffers(){
        new OffersRequester().run();
    }
}
 

그리고 이제 우리는 onMessageReceived()라는 이 클래스에 대한 하나의 진입점을 가지지만 모든 논리 조각은 별도로 처리할 수 있는 메서드에 맡기게 되므로 메시지 발신자에서 변경이 발생하면 해당 메서드에만 전달 될 것입니다.

 

Libraries used :

 

이상.

 

728x90