: 서비스를 사용하지 않고 별도의 스레드를 생성해 백그라운드 작업을 하면 어떤 문제가 발생할까? 스레드로 백그라운드 작업을 하다가 앱에서 back 키를 눌러 액티비티를 모두 종료할 시 앱 프로세스의 우선순위가 낮아져 LMK(low memory killer)가 프로세스를 강제로 종료할 수 있다. 이 때 스레드 또한 종료되게 된다. 그렇다면 서비스는 그냥 스레드와 다를까? 먼저, 프로세스 우선순위에 대해 알아보자. 


- 프로세스 우선순위

1. 포그라운드 프로세스 : 사용자와 상호작용하는 액티비티나 그런 액티비티에 바인딩된 서비스, onCreate/ onStart/ onStartCommand/ onDestory 를 실행 중인 서비스, onReceive를 실행하는 브로드캐스트 리시버에 해당한다. 


2. 가시(visible) 프로세스 : 포그라운드 컴포넌트를 가지지 않지만 사용자가 보는 화면에 아직 영향이 있는 프로세스이다. 액티비티의 경우 onPause()까지 실행됬지만 가시 상태인 것이다. 이 액티비티에 바인딩된 서비스도 가시 프로세스이다.


3. 서비스 프로세스 : startService로 실행했지만 1,2 번 상태가 아닌 서비스가 실행 중인 프로세스이다.


4. 백그라운드 프로세스 : 액티비티가 종료되지 않았지만 사용자에게 보이지 않고 활성화된 컴포넌트가 없는 프로세스(홈 키를 눌러 onStop()까지 불린 태스크)


5. 빈 프로세스 : 사용자가 백 키로 액티비티를 모두 종료하고 활성화된 컴포넌트가 없는 프로세스이다. 우선순위가 낮아서 리소스가 부족하면 가장 먼저 종료된다.


=> 맨 위에서 말한 것을 보면 액티비티가 모두 종료되었으니 빈 프로세스가 된다. 하지만 서비스를 사용한다면 onStartCommand가 리턴된 후 서비스는 서비스 프로세스가 된다. 따라서 백그라운드 작업이 종료될 위험에서 안전하다.


: 서비스를 사용할 때 주의할 점은 서비스의 생명주기 메서드는 메인 스레드에서 실행된다는 점이다. 생명주기 메서드 내에서 오랜 시간동안 작업하면 안 된다. 또한 서비스는 앱에서 1개의 인스턴스밖에 생기지 않는다. 별도로 싱글톤으로 구현하지 않아도 되고 서비스 안에서 사용되는 변수도 별도의 싱글톤 패턴으로 만들 필요 없다. 다음은 서비스의 생명주기이다.

서비스는 스타티드 서비스와 바운드 서비스가 있다. 서로 결합해서 사용할 수도 있다. 스타티드&바운드 서비스를 사용해야 하는 예는 다음과 같다. 음악 재생 액티비티에서 액티비티를 종료해도 음악이 나와야 한다. 이는 스타티드 서비스이다. 홈 화면에서 음악 재생 액티비티로 가면 현재 듣고 있는 음악에 대한 정보가 나와야 한다. 이는 바운드 서비스이다. 또 다른 경우 네트워크로 파일을 다운로드 받고 있다면 액티비티를 종료해도 다운은 계속 되야 한다. 이는 스타티드 서비스이다. 다시 액티비티로 돌아왔을 때 다운 진행 상황을 보여줘야 한다면 이는 바운드 서비스이다. 이렇게 바운드 서비스와 스타티드 서비스를 결합해야 되기도 하다.



- 스타티드 서비스

: 스타티드 서비스는 startService로 시작한다. 단, startService를 호출한다고 바로 시작하지 않고 메인 루퍼의 메시지 큐에서 차례대로 작업이 처리된다. startService를 처음 호출했을 때는 onCreate()->onStartCommand() 가 콜백되고 그 이후에 startService를 호출하면 onStartCommand만 호출된다. 서비스에서 작업 진행 상황을 액티비티에 전달해야 할 때가 있다. 이 때 ResultReceiver 브로드 캐스트를 사용한다. 

@Override
protected void onCreate(Bundle savedInstanceState){
	
	....
	
	Intent intent = new Intent(this, MyService.class);
	intent.putExtra(Constant.EXTRA_RECEIVER, resultReceiver);
	startService(intent);
	
}

private Handler handler = new Handler();

private ResultReceiver resultReceiver = new ResultReceiver(handler){
	@Override
	protected void onReceiveResult(int resultCode, Bundle resultData){
		if(resultCode == Constant.SYNC_COMPLETED){
			.....
		}
	}
}

class MyService extends Service{
	@Override
	public int onStartCommand(Intent intent, int flags, int startId){
		
		....
		
		final ResultReceiver receiver = intent.getParcelableExtra(Constant.EXTRA_RECEIVER);
		recevier.send(Constant.SYNC_COMPLETED, null);
		stopSelf();
	}
}

만약 디바이스의 메모리가 부족할 경우 안드로이드 시스템은 서비스를 강제 종료시킬 수 있다. 이 때 서비스는 재시작할 수 있는 데 이는 onStartCommand의 리턴 값에 따라 동작 방식이 달라진다. 리턴 값은 다음과 같다.


- START_NOT_STICKY : 강제 종료되면 재시작하지 않는다. 예로 화면에 최신 뉴스를 보여줄 때다.


- START_STICKY : 강제 종료되면 다시 onStartCommand를 호출하되 Intent가 null로 전달된다. onStartCommand 메서드의 기본 리턴 값이다. intent가 null로 전달되기 때문에 내부 변수를 사용하는 서비스에 적합하다. 예로 SNS 앱에서 새로운 메시지가 몇 개 왔는 지 정기적으로 확인한다면 적절하다. 


- START_REDELIVER_INTENT : 강제 종료되면 재시작하되 Intent가 전달된다. 예로 특정 상점의 상품 목록을 가져온 후 DB에 저장하는 경우가 있다.


재시작 동작 방식은 중요하지만 그에 못지 않게 서비스 종료도 중요하다. 서비스 종료를 제대로 하지 않으면 작업이 종료되었는 데 재시작 할 수도 있고 메모리를 잡아먹게 된다. 서비스 종료는 stopService() 보다는 stopSelf() 메서드로 하는 걸 권장한다. 두 메서드의 동작 방식은 동일하지만 stopSelf()는 Service 내에서 호출하는 것이다. 서비스의 작업이 끝나면 바로 stopSelf를 호출해 종료하는 게 좋다. 이 때 서비스는 onDestroy()까지 실행된다. 서비스에서 여러 개의 스레드가 실행될 때 주의할 게 멤버 변수를 최소한으로 사용해야 한다. 여러 스레드가 잘못 공유할 수 있기 때문이다. 또한 여러 군데에서 startService를 실행한다면 모든 작업이 끝났을 때 서비스를 종료해야 한다. 이를 위해서 종료할 때 매 작업마다 stopSelf()를 호출하지 말고 stopSelfResult(int startId)를 호출하는게 좋다. stopSelfResult는 startId가 가장 최근에 시작된 것이라면 그 때 서비스를 종료한다. startId는 onStartCommand() 메서드의 인자이다.


서비스의 멀티스레딩이 필요없을 경우 순차적으로 실행하는 IntentService를 사용하자. IntentService는 onStartCommand 메서드의 기본 리턴 값이 START_NOT_STICKY이다. 리턴 값을 변경하려면 setIntentRedelivery(true)를 호출하면 된다. IntentService에서 Toast를 사용할 때 주의해야 한다. Toast는 내부적으로 Handler을 생성해 루퍼가 필요하다. 일반 Service에서는 Toast를 사용하려면 Looper.prepare을 해야 한다. IntentService는 내부적으로 Looper가 있기 때문에 바로 Toast 사용이 가능하다. 하지만 Toast는 화면에 Toast를 보여주는 작업과 일정 시간 후 화면에서 Toast를 제거하는 작업을 해서 루퍼의 메시지 큐에 2개의 메시지가 들어간다. 만약 IntentService가 Toast를 보여주는 작업만 메시지 큐에서 처리하고 Destroy 되 Looper가 제거된다면 Toast 제거 작업은 실행이 안 되 Toast가 화면에 계속 띄어지게 된다. 따라서 Looper.getMainLooper를 사용해 핸들러를 생성 후 Toast를 사용하는 게 좋다. 


서비스가 중복되서 실행되는 걸 원치 않을 수 있다. 예로 메모 앱에서 서버와 지속적으로 동기화할 때 비슷한 시간에 여러 번 동기화 되는 걸 원치 않을 수 있다. 즉, startService를 호출할 때마다 매번 스레드가 생성되지 않고 이미 시작되었다면 나머지는 스킵해야 할 수 있다. 이를 구현하기 위해 onCreate()에서 스레드를 생성하여 구현(onCreate는 한번만 호출된다)할 수 있지만 서비스의 일반적인 형태가 아니다. onStartCommand()에서 스레드를 생성해서 이를 구현하는 방법은 다음과 같다. 

private ExecutorService exec = Executors.newSingleThreadExecutor();
private boolean isRunning = false;

@Override
public int onStartCommand(Intent intent, int flags, int startId){
	if(isRunning)
		return START_NOT_STICKY;
	isRunning = true;
	exec.submit(new Runnable(){
		......
               stopSelf();
	});
	return START_STICKY;
}

@Override
public void onDestroy(){
	isRunning = false;
}

isRunning 변수를 통해 스킵 시에는 START_NOT_STICKY를 리턴해 재시작되지 않도록 했다. Executors 팩토리 메서드를 사용했는데 ThreadPoolExecutor을 직접 사용하면 exec 변수를 다음과 같이 할 수 있다.

private exec = new ThreadPoolExecutor(1,1, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.DiscardPolicy();

DiscardPolicy를 통해 요청이 추가로 들어온다면 해당 요청을 버린다. 따라서 isRunning 변수를 사용할 필요 없다.



- 바운드 서비스

: 바운드 서비스는 서비스에 정의된 메서드를 다른 컴포넌트에서 호출할 수 있도록 한다.

public abstract boolean bindService(Intent service, ServiceConnection conn, int falgs)

service 매개변수는 대상 서비스를 가리킨다. conn은 서비스와 연결되거나 끊길 때의 콜백이다. flags는 보통 Context.BIND_AUTO_CREATE가 들어간다. startService를 실행해서 서비스를 실행하지 않았다면 BIND_AUTO_CREATE 옵션을 넣어야 한다. 스타티드 & 바운드 서비스가 아닌 이상 startService를 호출할 일이 없으므로 해당 옵션은 필수이다. 해당 옵션을 주고 stopService를 호출하면 서비스는 종료되지 않는다. 바운딩된 곳마다 unbindService를 실행해야 서비스가 종료된다. 해당 옵션이 없으면 stopService로 바로 종료된다. 또한 메모리 문제로 강제 종료됬을 시 해당 옵션이 있다면 재연결된다. 바운드 서비스는 로컬 바인딩과 리모트 바인딩으로 나뉜다. 리모트 바인딩은 다른 프로세스에서 접근할 때 사용하고 로컬 바인딩은 로컬 프로세스에서만 접근 가능하다.


여러 클라이언트들이 bindService()를 호출했다면 모든 클라이언트가 unbindService()를 호출해야 서비스가 정리된다. bindService와 unbindService는 액티비티의 onStart와 onStop에서 호출하는 걸 권장한다. onResume/onPause에서 이들 메서드들을 호출하면 해당 생명주기들이 자주 콜백되기 때문에 서비스를 종료하고 생성하는 작업을 많이 해 비용이 많이 든다. 


바인드 서비스에서 백그라운드 작업 시 결과를 돌려주는 방법으로 다음과 같이 4가지가 있다.

1. 백그라운드 작업이 끝났을 때 sendBroadcast()를 사용한다.

2. ResultReceiver을 사용한다.

3. Messenger를 사용해 양방향 통신을 한다.

4. 바인더 콜백을 이용한다. 이 방법은 안드로이드 프레임워크에서 많이 사용되는 방법이다. 아래는 바인더 콜백을 이용한 예이다.

public class MainService extends Service {
//서비스에서 선언.
 
//서비스 바인더 내부 클래스 선언
 public class MainServiceBinder extends Binder {
        MainService getService() {
            return MainService.this; //현재 서비스를 반환.
        }
    }
 
private final IBinder mBinder = new MainServiceBinder();
 
 @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        //throw new UnsupportedOperationException("Not yet implemented");
        return mBinder;
    }
 
//콜백 인터페이스 선언
public interface ICallback {
        public void recvData(); //액티비티에서 선언한 콜백 함수.
    }
 
private ICallback mCallback;
 
//액티비티에서 콜백 함수를 등록하기 위함.
public void registerCallback(ICallback cb) {
        mCallback = cb;
    }
 
//액티비티에서 서비스 함수를 호출하기 위한 함수 생성
public void myServiceFunc(){
    //서비스에서 처리할 내용
    }
 
 
//서비스에서 액티비티 함수 호출은..
     
    mCallback.recvData();
}
public class MainActivity extends Activity {
 
//액티비티에서 선언.
private MainService mService; //서비스 클래스
 
//서비스 커넥션 선언.
private ServiceConnection mConnection = new ServiceConnection() {
        // Called when the connection with the service is established
        public void onServiceConnected(ComponentName className, IBinder service) {
            MainService.MainServiceBinder binder = (MainService.MainServiceBinder) service;
            mService = binder.getService(); //서비스 받아옴
            mService.registerCallback(mCallback); //콜백 등록
        }
 
        // Called when the connection with the service disconnects unexpectedly
        public void onServiceDisconnected(ComponentName className) {
            mService = null;
        }
    };
 
//서비스에서 아래의 콜백 함수를 호출하며, 콜백 함수에서는 액티비티에서 처리할 내용 입력
private MainService.ICallback mCallback = new MainService.ICallback() {
        public void recvData() {
 
    //처리할 일들..
 
        }
    };
 
//서비스 시작.
public void startServiceMethod(View v){
        Intent Service = new Intent(this, MainService.class);
        bindService(Service, mConnection, Context.BIND_AUTO_CREATE);
    }
 
//액티비티에서 서비스 함수 호출
 
    mService.myServiceFunc();
}


+ Recent posts