Android : Bound Service

woga·2022년 7월 31일
0

Android 공부

목록 보기
33/49
post-thumbnail

바운드 서비스

바운드 서비스는 서비스에서 제공하는 메서드를 다른 컴포넌트에서 호출할 수 있게 한 것으로 사용 절차는 간단하다.

먼저 bindService() 메서드를 실행해서 바인딩하고서 이후에 필요한 메서드를 호출한다.

bindService() 메서드

Context에 있는 bindService()의 메서드 시그니처는 아래와 같다.

public abstract boolean bindService (Intent service, ServiceConnection conn, int flags)
  • Intent service : 대상 서비스
  • conn : 서비스와 연결되거나 연결이 끊길 때의 롤백
  • flags : 0을 넣을 수도 있고, Context의 BIND_XXX 상수를 넣을 수도 있다.
    • 가장 많이 쓰이는 상수는 Context.BIND_AUTO_CREATE 이고, ICS부터 추가된 상수들은 주로 서비스 프로세스의 우선순위와 관련되어 있다.

BIND_AUTO_CEREATE 옵션

BIND_AUTO_CEREATE 역할은 무엇을 할까

Q) bindService()를 실행하면 서비스에 항상 바인딩될까?

→ 그렇지 않다.

서비스가 생성이 되어야만 바인딩이 가능한데, bindService() 메서드에는 서비스가 생성된게 없다면 새로 생성하는 옵션이 있다 이게 바로 → BIND_AUTO_CEREATE 옵션이다!

이 옵션을 원한다면 binService() 메서드를 호출할 때 flags로 전달하면 된다.

이 옵션이 없으면 bindService()를 실행해도 서비스는 자동으로 생성되지 않고, 어디선가 startService()를 실행해서 서비스를 생성하지 않았다면 bindservice의 연결 콜백이 불리지 않게 된다.

따라서 스타티드&바운드 서비스가 아니라면 BIND_AUTO_CEREATE 옵션은 필수적이다

Q) 서비스에 바인딩된 클라리언트가 여러 개 남아있을 때 stopService()를 실행하면 어떤 일이 발생할까?

→ BIND_AUTO_CEREATE 옵션이 있다면 stopService()를 실행해도 서비스가 종료되지 않는다.

그리고 클라이언트마다 모두 unbindService()를 실행해야 Service의 onDestroy()가 불린다. 반면 옵션이 없는 경우에는 stopService()를 실행하면 연결이 끊기고 바로 서비스의 destroy 함수가 불린다

바인딩된 클라리언트가 남아 있는 상태에서도 서비스 프로세스는 메모리 문제 등으로 종료될 수 있다. 이 때 BIND_AUTO_CEREATE 옵션이 있다면 프로세스가 살아나서 재연결 된다

바운드 서비스의 용도

API로 데이터 접근 방법

최신 뉴스나 인기 검색어, 날씨처럼 업데이트가 필수적인 데이터가 있다. XML이나 JSON으로 결과를 리턴하는 API 서버가 있는데 이런 데이터를 앱에서 쓰기 위한 방법에는 어떤 게 있을까?

1) 앱에서 HTTP 호출을 통해 데이터에 직접 접근 가능

2) HTTP 호출을 하고 결과도 객체로 리턴해주는 오픈 API jar를 만들어서, 앱에서는 jar를 이용해서 데이터에 접근한다.

3) 서비스 앱에서 데이터를 제공하는 바운드 서비스를 만들고(이 안에서 HTTP 호출을 하고 객체를 리턴한다), 다른 앱에서는 bindService()를 실행하여 서비스의 데이터에 접근한다.

  • 예를 들자면, 검색 앱에서 인기 검색어 목록을 바운드 서비스로 외부에 제공 가능
  • Google Play In-app Billing도 이 방식을 쓴다

4) 외부에 공개하는 jar 내부에서 bindService()를 실행하고 결과를 받는다

  • Google Play Service가 이런 형태이다
  • 여기에 connect()와 disconnect() 메서드가 있는데, 내부적으로 각각 bindService()와 unbindService()를 호출한다

3, 4번에서 바운드 서비스를 언급했는데 아래로 갈수록 추상화 레벨이 높아진다.

이 레벨 높을 수록 좋은게 아니라 규모에 맞는 적절한 레벨을 선택하는게 좋다.

콜백을 이용한 상호 작용

API로 데이터를 조회하는 예를 들었지만, 한 곳에서 특화된 기능을 내/외부 프로세스의 여러 클라이언트에게 제공할 때도 바운드 서비스를 사용할 수 있다.

단순히 데이터를 제공하는 것은 콘텐트 프로바이더도 가능하지만, 바운드 서비스에서는 콜백을 이용한 상호 작용이 가능하다

📌
바운드 서비스는 로컬 바인딩과 리모트 바인딩으로 주로 구분한다.
근데 리모트 바인딩 서비스를 만들어도, 로컬에서 호출하면 바인더를 거치지 않고 직접 호출하므로 로컬 바인딩 서비스가 따로 필요없다고 볼수도 있다.
실제 안드로이드 개발자 사이트에서는 로컬 바인딩이나 리모트 바인딩에 대해서 따로 구분해서 이야기하지 않는다.


리모트 바인딩

리모트 바인딩 서비스는 다른 프로세스에서 접근하는 것을 전제로 만들어진다.
따라서 로컬에서만 사용하는 서비스라면 리모트 바인딩 서비스를 굳이 만들 필요가 없다.

바인딩한 클라이언트에 제공하는 메서드를 aidl 인터페이스로 작성한 다음에 서비스에서 Stub 클래스의 추상 메서드를 구현해주면 된다.

Service에 Stub 구현

Service에서는 추상 클래스인 stub 구현체를 만든다.

public class RemoteService extends Service {
	
    @Override
    public IBinder onBind(Intent intent) {
    	return binder;
    }
    
    private final IRemoteService.Stub biner = new IRemoteService.Stub() {
    	@Override
        public boolean validCalendar(long calendarId, String calendarType) {
        	CalendarType type = CalendarType.valueOf(calendarType);
            ...
         }
   };

}

클라이언트에서 서비스 바인딩

다른 컴포넌트에서 서비스를 바인딩해서 사용하는 것도 복잡하지는 않다. bindService()는 바인딩 결과를 비동기로 받기 때문에, 콜백으로 사용할 ServiceConnection 인스턴스를 bindService() 메서드에 파라미터로 전달한다.

private IremoteService mIRemoteService;

private ServiceConnection mConnection = new ServiceConnection() {

	@Override
    public void onServiceConnected(ComponentName className, IBinder service) {
    	mIRemoteService = IRemoteService.Stub.asInterface(service);
    }
    
    @Override
    public void onServiceDisconnected(ComponentName className) {
    	mIRemoteService = null;
    }
    
};

@Override
public void onStart() {
	super.start();
    bindService(neew Intent(IRemoteService.class.getName()), mConnection, Context.BIND_AUTO_CREATE);
}

private void checkValid() {
	if (mIRemoteService != null) {
    	boolean valid = mIRemoteService.isValidCalendar(10L, CalendarType.NORMAL.name());
        ...
    }
}
  • 커넥션 콜백인 ServiceConnection을 생성한다
  • Stub.asInterface() 메서드를 통해서 로컬인 경우는 Stub 인스턴스, 리모트인 경우는 Proxy 인스턴스가 mIRemoteService에 대입된다.

위에 코드 중에 onServiceDisconnected 하는 함수가 있는데, 서비스에 문제가 생겼을 때 호출되는 것이다.(주로 크래시되거나 강제 종료되었을 때)

Context.BIND_AUTO_CREATE 옵션을 사용하고 서비스 바인딩은 여전히 유효한 상태이면, 다음에 서비스가 실행 상태가 될 때 onServiceConnected가 알아서 호출된다.

  • 연결이 끊길 때는 mIRemoteService를 null로 만든다.
  • bindService()에 ServiceConnection을 전달한다.
  • mIRemoteService의 메서드를 호출할 때는 먼저 null인지 체크한다.

ServiceConnectiononServiceConnected()가 불리기 전일 수도 있고, 서비스 문제로 바인딩이 안 되었을 수도 있기 때문이다.

📌
서비스의 메서드를 로컬에서 호출할 때는 호출하는 스레드에서 서비스의 메서드가 실행된다.
반면, 리모트에서 호출하는 경우에는 서비스가 속한 프로세스의 바인더 스레드 풀에서 실행되기 때문에 리모트 바인딩 서비스는 스레드 안전(thread-safe)하게 만들어져야 한다.

aidl에서 지원하는 데이터 타입

굳이 리모트 바인딩으로 커버 가능한데 로컬 바인딩을 이야기하는 이유는 바로 aidl에서 쓸 수 있는 한정적인 데이터 타입 때문이다.

프로세스 간 데이터를 주고 받을 때는 마샬링/언마샬링이 필요하기 때문에 데이터 타입이 제한된다

지원되는 데이터 타입은 아래와 같다.

  • primitive type
  • String
  • List (구현체 안됨)
  • Map (구현체 HashMap 같은 타입 안됨)

전달하는 객체가 복잡하거나 객체의 수가 많다면 이 것을 Parcelable로 만드는 것이 만만치 않다. 로컬 바인딩은 이 문제에서 자유롭다.
다른 프로세스에서도 객체를 쓰려면 규칙대로 Parcelable을 구현해야 하지만, 로컬 프로세스에서만 쓰겠다면 규칙대로 할 필요가 없다.

로컬 바인딩

로컬 프로세스에서만 접근 가능한 서비스이다. 리모트 바인딩 서비스보다 간단하게 만들 수 있다.

또한, 리모트 서비스에서는 validCalendar() 메서드에 파라미터로 enum 타입을 쓸 수 없었는데 로컬 바인딩 서비스에서는 쓸 수 있다.

로컬 바인딩 서비스

Service에서는 Stub을 구현할 필요가 없다.

public class LocalService extends Service {
	private final IBinder mBinder = new LocalBinder();
    
    public class LocalBinder extends Binder {
    	public LocalService getService() {
        	return LocalService.this;
        }
    }
    
    @Override
    public IBinder onBind(Intent intent) {
    	return mBinder;
    }
    
    public boolean validCalendar(long calendarId, CalendarType calendarType) {
    ...
  	}
 }

클라이언트에서 로컬 바인딩 접근

로컬 바인딩을 사용하는 법은 리모트 바인딩과 거의 동일하다.

public class BindingActivity extends Activity implements OnClickListener {
	private LocalService mService;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
        ...
    }
    
    @Ovveride
    protected void onStart() {
    	super.onStart();
        Intent intent = new Intent(this, LocalService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }
    
    @Ovveride
    protected void onStop() {
    	if (mService != null) {
        	unbindService(mConnection);
        }
        super.onStop();
    }
    
    @Override
    public void onClick(View view) {
    	int id = view.getId();
        if (id == R.id.check) {
        	if (mService != null) {
            	boolean valid = mService.validCalendar(10L, CalendarType.NORMAL);
                ...
              }
         ...
  	}
    
    private ServiceConnection mConnection = new ServiceConnection() {
    	@Override
        public void onServiceConnected(ComponentName className, IBinder service) {
        	LocalBinder binder = (LocalBinder) service;
        	mService = binder.getService();
       }
        
        @Override
        public void onServiceDisconnected(CompoentName name) {
        	mService = null;
         }
     };
 }

리모트 바인딩과 달리 onServiceConnected() 메서드에서 얻어내는 것은 결국 LocalService 인스턴스이다. 안드로이드에서 컴포넌트 간에 직접적으로 인스턴스 접근이 불가능한데, Binder 객체를 통해서 직접 접근할 수 있게 한 것이다.

인터페이스를 사용한 로컬 바인딩

이 파트는 앞에 IRemoteService.aidl 파일 그대로 IRemoteService 인터페이스로 만들고, Stub 구현과 유사하게 LocalBinder에서 IRetmoeService를 구현하는 것이다.

예제 코드는 생략하겠다.

바인딩의 특성

bindService()를 호출하면 서비스와 엮이는 클라이언트가 하나씩 늘어난다.

이렇게 엮인 클라이언트가 남아 있다면 어느 클라이언트에서 stopService()를 실행해도 서비스는 종료되지 않는다. 모든 클라이언트가 unbindService() 메서드를 호출해서 서비스와의 관계가 전부 정리되어야만 한다.

다음 그림에서 Is service also~ stopSelf() of stopService() 의 분기점은 스타티드(unbounded)&바운드 서비스의 경우를 고려한 것이다.

Activity는 onStart/onStop 메서드에서 bind/unbindService 실행 권장

onResume/onPause에서 바인드와 언바인드하면 번번하게 트랜지션이 일어나는데, onPause()를 통해 unbindService()가 실행되고 서비스가 종료될 수 있다. 그리고 다시 onResume에서 서비스가 시작되어야 하는데, 종료된 서비스를 다시 생성하려면 비용이 드는데 빈번한 트랜지션에서 생성과 종료를 계속 반복하는 것은 적합하지 않다.

데이터를 조회하는 바인딩은 콘텐트 프로바이더로 대체 가능

bindService()를 통해 바인딩되었으면 이제는 호출하는 쪽과 호출되는 쪽은 클라이언트-서버 관계고 파악하면 된다. 클라이언트에서 메서드를 호출하면 서버에서 결과를 리턴하는 방식이다. 조회해서 결과를 리턴한다면 떠올려지는 게 바로 콘텐트 프로바이더이다.

로컬 바인딩은 타입을 자유롭게 쓸 수 있으므로 크게 고민하지 않아도 되지만, 리모트 바인딩의 경우 단순 데이터 조회라면 콘텐트 프로바이더를 대신 쓸수도 있다. 다만 콘텐트 프로바이더에서는 Cursor타입으로 리턴해야 한다.

바운드 서비스에서 백그라운드 작업 시 결과를 돌려주는 방법

바운드 서비스에서도 작업이 오래 걸린다면 백그라운드 스레드에서 작업을 실행하는 것을 고려할 수 있다.

4가지 방법을 통해 결과를 전달받을 수 있다.

  • 스레드 작업이 끝났을 때 sendBroadcast()를 통해서 데이터 전달 및 클라이언트 폴링으로 데이터를 가져올 수 있음

  • 바인더 콜백을 메서드에 파라미터로 전달해서 결과를 받는다 (이 방법은 안드로이드 프레임워크에서 많이 쓰인다. Toast.show()도 마찬가지)

  • bindService() 메서드의 Intent 파라미터에 ResultReceiver를 전달하면 Service의 onBind()에서 ResuleReceiver를 가져올 수 있다. 작업을 실행하고 ResultReceiver의 send() 메서드로 클라리언트에 결과를 다시 전달한다.

  • Messenger를 사용해서 양방향 통신을 할 수 있다. 이 클래스도 내부적으로 바인더 콜백을 사용한다.

마치며..

이 편을 마지막으로 서비스에 대한 정리는 마무리하려 한다. 현재 프로그래밍 넥스트 스텝이란 책을 보면서 정리 중이고 이 포스팅도 마찬가지인데, 읽으면서 여러모로 많이 배운다.

평소에 궁금해도 어떻게 파헤쳐서 코드를 봐야할지 애매한 영역을 책을 통해 알 수 있어서 좋다. 아직까지는 이해되는 부분 반, 이해되지 않는 부분 반이 있지만 추후에 또 다시 읽으면 다른 느낌일거라 믿는다..!

profile
와니와니와니와니 당근당근

0개의 댓글