2023.02.24 - 안드로이드 앱개발자 과정

CHA·2023년 2월 24일
0

Android



Thread

코드를 읽어서 실행하는 객체입니다. 우리는 안드로이드 프로젝트에서 Thread 를 사용해본적은 없습니다. 그렇다 하더래도 우리가 사용해왔던 안드로이드 프로그램은 하나의 스레드를 가집니다. 그게 Main Thread 입니다.

ANR 에러
어플리케이션이 사용자의 응답에 5초 이상 반응하지 않으면 운영체제는 앱을 다운시켜버립니다. 이러한 에러를 ANR(Application Not Response) 에러라고 합니다.

오래걸리는 작업은 직원객체(별도의 Thread)를 사용하여 처리할것을 권장합니다. 특히 네트워크의 경우, 인터넷의 속도에 따라 작업의 처리시간이 변동됩니다. 그래서 네트워크 작업의 경우에 Main Thread 는 사용할 수 없고 별도의 Thread 를 만들어 사용해주어야 합니다. 또한 UI 작업은 Main Thread 에서만 처리할 수 있습니다.

오늘은 별도의 Thread 를 만들어 작업들을 처리하는 방법에 대해 알아봅시다.


Thread 의 필요성

Thread 를 본격적으로 사용하기 이전에 스레드를 사용하지 않는 예제를 작성해보고 이것을 기반으로 스레드가 필요한 이유에 대해 알아보자.

public class MainActivity extends AppCompatActivity {

    Button btn;
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = findViewById(R.id.btn);
        tv = findViewById(R.id.tv);

        btn.setOnClickListener(view -> {
            for (int i=0; i<20;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tv.setText(i+"");
            }
        });
    }
}

반복문을 20번 돌리는 예제입니다. 20번을 돌리는 동안 매회마다 Thread.sleep(1000); 을 이용하여 메인스레드를 1초씩 잠재웁니다. 그리고 나서 텍스트뷰에 각 반복횟수를 표시하도록 설계했습니다. 텍스트뷰에 표시된 결과가 어떨것같나요? 0,1,2, ... 으로 표시될까요? 직접 실행해보면 초기값인 0이 표시되고, 20초 이후에 19가 표시되는걸 볼 수 있습니다. 그리고 이러한 작업이 진행되는 도중에 다른 버튼을 누르면 ANR 에러가 나는 모습도 볼 수 있습니다.

왜 그런걸까요? 사실, 아직 배우지 않은 내용이지만 onCreat() 메소드가 호출되고 종료된 이후, onDraw() 라는 메소드가 실행됩니다. 이 메소드는 화면에 뷰들을 그려주는 역할을 하는 메소드입니다. 그리고 그 메소드는 Main Thread 가 실행시키는 메소드입니다. 즉, 메인스레드가 for 문안의 실행문 작업들에 20초를 소요하게 되면서 onDraw() 메서드를 호출하지 못하는 상황인겁니다. 그래서 20 초 이후에 onCreate() 메소드가 종료되고 onDraw() 메소드가 호출되면 그때 화면에 텍스트뷰가 갱신되는 모습을 볼 수 있습니다.

하지만 이건 우리가 원하는 모습은 아니죠. 우리는 반복문의 횟수가 늘어날 때마다 텍스트뷰가 갱신되기를 원합니다. 그래서 우리에게는 Thread 가 필요한겁니다. Main Thread 에서 오래 걸리는 반복문 작업을 맡기지 말고, 직원 Thread 에게 오래걸리는 작업을 대신 맡기는 겁니다.


Thread 사용해보기

public class MainActivity extends AppCompatActivity {

    Button btn,btn2;
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn2 = findViewById(R.id.btn2);
        tv = findViewById(R.id.tv);

        btn2.setOnClickListener(view -> {
            MyThread thread = new MyThread();
            thread.start(); 
        });
    }

    class MyThread extends Thread {

        @Override
        public void run() {
            for (int i=0; i<20;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tv.setText(i+"");
            }
        }
    }
}

Thread 클래스를 상속받은 MyThread 클래스를 만들어주었고, run() 메소드를 구현하여 스레드가 작업할 내용들을 작성해주었습니다. 작업 내용은 앞선 예제에서와 동일합니다. 그리고 클릭 이벤트 처리를 이용하여 스레드 객체를 하나 생성해주고 thread.start(); 을 통해 스레드의 작업을 시작할 수 있도록 해줍니다.

그런데 직접 실행을 시켜보면 에러가 뜨는 모습을 볼 수 있습니다. 아까 앞서 이야기 했듯, UI 를 건드는 작업은 Main Thread 에서만 할 수 있습니다. 그런데 위 예제에서는 별도의 Thread 가 setText() 를 이용하여 텍스트뷰의 UI 를 건드렸기 때문에 에러가 난겁니다.

오래 걸리는 작업을 Main Thread 에서 할수도 없고, 그렇다고 별도의 스레드에서 처리하자니 UI 를 건드릴수도 없습니다. 그러면 우리는 어떻게 해야할까요?

그래서 우리는 Main Thread 에게 UI 변경 작업을 요청해야 합니다. 그리고 요청하는 방법은 2가지가 있습니다.

UI 변경작업 요청 방법 2가지

  1. Handler 객체 이용
  2. Activity 클래스의 runOnUiThread() 메서드 이용

UI 변경작업 - Handler 객체 이용

앞서 오래 걸리는 작업을 통해 UI 를 변경하고자 했는데, Main Thread 에서 오래 걸리는 작업을 할 수 없어 별도의 Thread 를 사용하여 UI 변경작업을 하려 하였습니다. 그런데 UI 변경 작업은 Main Thread 만 할 수 있었죠. 그래서 그 해결방안 중 하나가 Handler 객체를 이용하는것 입니다.

Handler 이용 원리

별도의 Thread 가 보낸 메시지를 처리하는 작업을 Main Thread 가 전부 하려니 너무 복잡했습니다. 그래서 그 작업을 대신 해주는 역할을 하는 Handler 객체를 만듭니다.

그런데, 메시지를 각 스레드의 작업이 끝남과 동시에 바로 가져다 주니 그것도 바로 처리하기가 벅차 메시지를 담아둘 수 있는 통을 하나 만듭니다. 그 통의 이름이 Message Queue 입니다.

그런데 Main Thread 가 이 메시지큐에 메시지가 와있는지 매번 확인하려니 이것도 좀 귀찮습니다. 그래서 메인스레드의 비서와 같은 역할을 하는 객체를 만들어주는데, 이 객체의 이름이 Looper 입니다. 루퍼는 이 메시지큐의 메시지가 도착하면 바로 핸들러 객체에게 알리고, 이 핸들러 객체는 받은 메시지를 기준으로 메인 스레드를 대신하는 UI 작업을 처리할 수 있습니다.

Handler 구현하기

class MyThread extends Thread {

    @Override
    public void run() {
        for (int i=0; i<20;i++){
            num++;
            handler.sendEmptyMessage(0); 
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}



-------------- MainActivity.java
Handler handler = new Handler(Looper.getMainLooper()){
    @Override
    public void handleMessage(@NonNull Message msg) {
        tv.setText(num+"");
    }
};

원리는 좀 복잡하지만 실제 코드 쿠현은 그렇게 복잡하지는 않습니다. 먼저 핸들러 객체를 만들어줍니다. 그 핸들러 객체를 생성하고, 생성자의 파라미터로 Main Thread 의 루퍼라는 의미로 Looper.getMainLooper() 를 전달합시다. 그리고 스레드 내부에서 메시지를 전달하면 자동으로 호출되는 메소드인 handleMessage() 메서드를 구현해줍시다.

스레드 내부에서는 handler.sendEmptyMessage(); 를 통해 메시지를 전달합니다. 이 메시지가 전달되면 handleMessage() 메서드가 호출되면서 UI 작업이 가능해지는 원리입니다.


UI 변경작업 - Activity 클래스의 runOnUiThread() 메서드 이용

앞서 핸들러를 이용한 UI 변경작업은 내용도 복잡하고 핸들러 클래스 따로, 실행 따로 해주어야 하니 좀 복잡합니다. 그래서 보통 Activity 클래스의 runOnUiThread() 메서드를 사용하여 UI 의 변경작업을 처리합니다. 코드부터 보겠습니다.

class MyThread extends Thread {
        @Override
        public void run() {
            for (int i=0; i<20;i++){
                num++;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(num+"");
                    }
                }); 
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

오래 걸리는 작업을 Main Thread 에서 할 수 없으니, 별도의 Thread 객체 MyThread 를 만들어주었으니나, UI 의 변경 작업을 처리할 순 없기 때문에 그 작업을 앞선 예제에서는 Handler 를 통해서 해결하였습니다. 다만, 좀 복잡했기 때문에 Activity 클래스의 메소드 runOnUiThread() 를 사용하여 UI 변경작업을 처리해주었습니다.

이 메소드의 파라미터로는 Runnable 인터페이스의 객체가 와야합니다. 다만, run() 추상메소드를 구현해주어야 하므로, 익명클래스를 이용하여 구현해줍시다. 그리고 run() 메소드의 실행문으로 UI 변경작업 코드를 작성해주면 끝입니다.


빵집 이야기

#1. 빵집 주인 Main Thread

빵집이 하나 있습니다. 이 빵집의 사장은 Main Thread 입니다. 초창기에는 손님이 많이 없어서 메인스레드 혼자 빵집을 꾸려가기에 충분했습니다.

#2. 직원 등장 Thread

하지만 점차점차 손님도 많아지고, 바빠지다보니 혼자서 모든 일을 처리하기에 벅찬 상황입니다. 특히 만드는데 오랜 시간이 걸리는 빵을 만들기에는 더더욱이 그렇겠죠. 그래서 메인스레드는 직원을 뽑을 결정을 합니다. 직원의 이름은 Thread 입니다.

#3. 일 잘하는 직원 Handler

직원을 뽑아놓고 보니, 나름 수월합니다. 오래 걸리는 작업들은 모두 직원에게 맡기고 사장 Main Thread 는 손님들만 처리하면서 작업을 처리합니다. 그러던 와중 빵집이 더 바빠져, 조금 벅차지기 시작합니다. 그러다보니 빵을 만들어다주는 직원이 불만을 토로합니다. 빵을 만들면 사장에게 가져가고, 사장은 받은 빵을 팔아야하는데, 빵을 가져다주는 작업이 귀찮다는겁니다. 그래서 사장 Main Thread 는 직원 한명을 더 구합니다. 이번에는 일을 아주 잘하는 직원을 뽑습니다. 그 직원의 이름이 Handler 입니다. 이 핸들러 직원은 Thread 가 만든 작업물을 사장인 Main Thread 에게 전달하는 역할을 합니다.

#4. 빵 진열대 Message Queue

핸들러 직원까지 뽑았음에도 여전히 사장 Main Thread 는 바쁩니다. 손님도 응대해야하는데 핸들러 직원이 가져다 준 작업물들도 바로바로 처리해야하기 때문이죠. 그래서 한가지 묘안을 생각합니다. 핸들러에게 만들어진 작업물이 있다면 나에게 가져다주지말고, 일단 빵 진열대에 가져다 놓으라 지시합니다. 그리고 나중에 체크해서 처리할게 있으면 핸들러 직원에게 처리를 지시하겠다고 말이죠.

#5. 사장 비서 Looper

그렇게 해서 잘 되나 싶었는데, 사장 Main Thread 는 여전히 바쁜가봅니다. 이제는 빵 진열대를 체크하는일 마저 힘들다고 합니다. 그래서 새로운 직원을 채용하는데, 이번에는 빵 진열대 체크 전담 비서를 뽑았습니다. 비서의 이름은 Looper 입니다. 이 루퍼는 빵진열대를 체크해서 작업물이 있다면 핸들러 직원에게 알려주는 역할을 합니다.


스레드 작업 - 네트워크로 이미지 불러오기

앞선 작업들에서는 오래 걸리는 작업으로 단순히 스레드를 잠재우는 예제를 해보았습니다. 이번에는 실제 네트워크 작업을 수행함으로써 Thread 를 이해해봅시다.

버튼 하나를 누르면 네트워크 상에서 이미지를 읽어와 이미지뷰에 띄워주는 작업을 처리해봅시다. 단, 네트워크 작업은 오래 걸리는 작업으로 인식하므로, 반드시 별도의 Thread 를 통해 작업을 처리해주어야 합니다.

public class MainActivity extends AppCompatActivity {

    Button btn,btn2;
    ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        btn = findViewById(R.id.btn);
        iv = findViewById(R.id.iv);

        btn.setOnClickListener(view -> { clickBtn();});
    }

    void clickBtn(){
        new Thread(){
            @Override
            public void run() {
                String imgUrl ="https://cdn.pixabay.com/photo/2022/06/07/15/56/child-7248693__340.jpg";

                try {
                    URL url = new URL(imgUrl);

                    InputStream is = url.openStream();
                    Bitmap bm = BitmapFactory.decodeStream(is);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            iv.setImageBitmap(bm);
                        }
                    });
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();
    }
}

원래 우리는 버튼의 클릭 리스너를 설정하고, 람다식을 이용하여 코드를 간결하게 표현하였습니다. 근데 람다식 중괄호 안쪽이 코드가 복잡해지니,다음과 같이 따로 메소드를 만들어 코드 가독성을 높였습니다. btn.setOnClickListener(view -> { clickBtn();});

clickBtn() 메소드 안쪽에 별도의 Thread 를 만들어 네트워크 작업을 진행해줄 겁니다. 단, 작업전에 주의할 점 한가지가 있습니다. 네트워크 작업의 경우 반드시 Permission 을 받아두어야 합니다. 이 퍼미션은 메니페스트 파일에서 작업해줄 수 있습니다. 다음 한줄의 코드를 <manifest> 안쪽에 작성해줍시다.
<uses-permission android:name="android.permission.INTERNET"/>

또한 안드로이드에서는 http:// 를 허용하지 않습니다. 오로지 https:// 만 가능한데요, 만일 http:// 도 사용하고 싶다면 다음 한줄의 코드를 <application> 의 속성으로 적어줍시다.
android:usesCleartextTraffic="true"

1. 이미지 url 준비하기

String imgUrl ="https://cdn.pixabay.com/photo/2022/06/07/15/56/child-7248693__340.jpg";

가져오고 싶은 이미지의 url 을 준비해주면 됩니다. 인터넷에서 이미지를 검색한 후, 그 이미지의 주소를 복사하면 이미지의 url 을 얻어올 수 있습니다.

2. URL 객체 생성하기

이미지의 url 도 준비되었으니, 스트림을 열 준비를 해야겠습니다. 스트림을 열어 얻어온 주소를 이용해 이미지를 가져와야 하니까요. 그래서 스트림을 열기 위한 특별한 객체가 있습니다. URL 객체입니다. 그리고 이 URL 객체에게 어디서 이미지를 가져올 지 알려주기 위해 우리가 받아온 이미지 주소값을 파라미터로 전달해줍시다.
URL url = new URL(imgUrl);

3. 스트림 열기

이제 스트림을 열 준비가 끝났습니다.
InputStream is = url.openStream(); 을 이용하여 이미지를 가져올 스트림을 열어줍니다.

4. 스트림을 통해 이미지 읽어와 Bitmap 객체로 생성

원래 이미지 데이터는 바이트의 배열로 이루어져 있습니다. 그래서 이미지를 불러오기 위해서는 바이트를 읽어오는 작업이 필요합니다. 다행스럽게도, 안드로이드에서는 이 작업을 대신 해줍니다. 다음 한줄의 코드면 이미지를 가져올 수 있습니다.
Bitmap bm = BitmapFactory.decodeStream(is);

이미지를 읽어와 Bitmap 객체 bm 에 넣어주었습니다. 비트맵은 .png 등의 이미지 파일을 자바코드에서 사용하기 위해 사용하는 객체 입니다. 그리고 안드로이드에서의 그림은 사실 비트맵 객체 입니다.

5. UI 작업

앞선 예제들에서도 해봤듯이 UI 작업은 별도의 Thread 가 할 수 있는 작업이 아닙니다. 오로지 Main Thread 만이 UI 작업을 진행할 수 있습니다. 대신, 그 작업을 가능하게 하는 방법이 Handler 를 이용한 방법과 runOnUiThread() 메서드를 이용한 방법이 있었습니다. 이번에는 메서드를 이용해봅시다.

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        iv.setImageBitmap(bm);
    }
});

이렇게 해서 이미지뷰 위에 우리가 가져온 이미지를 띄워주었습니다.


네트워크 이미지 라이브러리 사용해보기

방금 전 예제에서 어떠한 이미지를 이미지뷰에 띄우기 위해 5 단계의 작업을 거쳤습니다. 물론 코드 자체가 길지는 않다곤 하지만, 우리가 사용할 이미지는 한개가 아니겠죠. 무수히 많은 수의 이미지를 처리해야하는데, 이러한 작업을 거치기란 쉽지 않을것 같습니다. 그래서 네트워크에서 이미지를 가져와 이미지뷰에 띄워주는 작업을 해주는 외부라이브러리가 있습니다. 바로 Picasso 와 Glide 인데요, 이번 예제에서는 글라이드를 이용하여 네트워크 이미지를 처리해봅시다.

public class MainActivity extends AppCompatActivity {

    Button btn2;
    ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        btn2 = findViewById(R.id.btn2);
        iv = findViewById(R.id.iv);

        btn2.setOnClickListener(view -> {
            String imgUrl = "https://cdn.pixabay.com/photo/2023/01/05/22/35/flower-7700011_640.jpg";
            Glide.with(this).load(imgUrl).into(iv);
        });
    }
}

이미지를 네트워크로 부터 가져오고, 이미지뷰에 띄워주는 작업까지 단 한줄의 코드로 끝났습니다. Glide.with(this).load(imgUrl).into(iv); 코드의 with 는 우리가 사용할 액티비티의 객체를 전달해주어야 합니다. 우리는 MainActivity 를 사용중 이므로, this 를 전달해주었습니다. load() 의 파라미터로는 우리가 가져온 이미지의 url 을, into() 의 파라미터로는 가져온 이미지를 붙일 뷰 참조변수를 전달해줍니다.


상태 진행 다이얼로그 (Progress Dialog)

흔히들 접해볼만한 뷰 입니다. 작업이 진행되는 정도를 알려주는 상태 진행 바 입니다. 이번 예제에서는 이러한 상태 진행 바를 다이얼로그를 이용하여 보여주는 예제를 해보겠습니다.

Spinner Type

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn).setOnClickListener(view -> clickBtn()); 
    }

    ProgressDialog dialog;
    int gauge = 0; 
    
    public void clickBtn(){
        if(dialog != null) return; 

        dialog = new ProgressDialog(this);
        dialog.setTitle("Wheel Dialog");
        dialog.setMessage("downloading...");
        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        dialog.setCanceledOnTouchOutside(false);
        dialog.show();

        handler.sendEmptyMessageDelayed(0,3000);
    }

    Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            dialog.dismiss();
            dialog = null;
        }
    };
}

다이얼로그를 들어가기전에 람다식이 설정된 부분을 봅시다. 앞선 예제들에서는 람다식의 중괄호 안쪽에 코드들을 작성해주었습니다. 그 코드가 지저분하니 메소드로 대체했을 뿐이지요. 그런데 메소드로 대체해놓고 보니 코드는 한줄 뿐인데, 중괄호에 세미콜론까지.. 좀 지저분하죠. 그래서 만일, 람다식 중괄호 안쪽의 코드가 한줄 뿐이라면 중괄호와 세미콜론은 생략이 가능합니다.

버튼을 클릭하면 다이얼로그가 하나 띄워지고 그 다이얼로그에서 진행상황을 보여주는 예제입니다. 사실 API 26 부터 ProgressDialog 는 Deprecated 되었습니다. ProgressBar 혹은 notification 을 이용하라 하는데, 일단 여기서는 Thread 를 잘 알아보기 위해 ProgressDialog 를 그대로 사용해봅시다.

버튼을 클릭했을 때, 먼저 확인해줘야 하는것이 있습니다. 클릭이 되었을 때, 이미 기존의 다이얼로그가 띄워진게 있다면 return 을 이용하여 메소드를 종료시켜주어야 합니다. 그리고 만일 띄워진 다이얼로그가 없다면 이제 만들어줄겁니다.
dialog = new ProgressDialog(this); 을 이용하여 ProgressDialog 를 생성해준 뒤, dialog.setTitle("Wheel Dialog"); 으로 다이얼로그의 타이틀을 지정해주고 dialog.setMessage("downloading..."); 을 이용해 다이얼로그의 본문을 설정해줍시다. 이제 다이얼로그의 형태를 지정해줄건데요, 이번에는 스피너의 형태로 지정해줍니다. 그리고 dialog.show() 를 이용하여 화면에 띄워줄 수 있습니다.

그리고 다이얼로그가 무한히 켜져있지 않게 하고 싶다면 Handler 객체를 만들어주고, handler.sendEmptyMessageDelayed(0,3000); 를 이용해 메시지 하나를 보냅니다. 즉, 3초를 기다렸다가 메시지를 보내라는 메서드이며, 이 메시지가 보내지면 Handler 익명클래스에 구현된 handleMessage() 메서드가 호출됩니다. 그래서 메서드 내부에 dialog.dismiss();dialog = null; 를 구현해주면 다이얼로그가 생성되고 3초 후에 다이얼로그는 종료됩니다.

Horizontal Type

이번엔 Spinner 의 형태의 다이얼로그가 아닌 Horizontal Type 의 다이얼로그를 만들어 봅시다. 만드는 법은 동일합니다. 다만 setProgressStyle() 의 파라미터 값으로 ProgressDialog.STYLE_HORIZONTAL 를 넣어주기만 하면 됩니다.

public void clickBtn2(){

    if(dialog != null) return;

    dialog = new ProgressDialog(this);
    dialog.setTitle("Bar Dialog");
    dialog.setMessage("downloading...");
    dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    dialog.setCanceledOnTouchOutside(false);
    dialog.setMax(50);
    dialog.show();

    new Thread(){
        @Override
        public void run() {
            gauge = 0;
            while(gauge < 50){
                gauge++;
                dialog.setProgress(gauge);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            dialog.dismiss();
            dialog = null;
        }
    }.start();
}

setMax() 속성은 위 그림 오른쪽 아래에 보이는 현재 진행상태의 최대값을 지정하는 속성입니다.

그리고 수평 타입의 경우 막대 형태로 현재 진행상태를 보여주는데, 보통 이러한 형태는 진행상황이 변함과 동시에 바의 형태 또한 바뀌게 됩니다. 그 작업을 별도의 Thread 를 통해 처리해주어야 합니다. 별도의 Thread 의 run() 메소드 내부에서 while 문을 돌려 gauge 변수의 값을 늘려주고, setProgress() 메소드를 활용하여 진행상황을 갱신시켜 줄 수 있습니다.

근데 생각해보면 좀 이상합니다. 다이얼로그도 어찌됐던 UI 인데, 별도의 스레드에서 UI 를 처리하고 있는 모습입니다. 정말 다행스럽게도 setProgress() 메서드 내부에서 runOnUiThread() 메서드가 실행됩니다. 그래서 우리가 따로 처리해주지 않아도 UI 의 변경작업이 처리됨을 알 수 있습니다. 그리고 while 문이 완료되면 다이얼로그가 종료되어야 하므로, dismiss() 메서드를 호출하여 다이얼로그를 종료시키고 dialog=null 로 초기화 시켜줍시다.


백그라운드 Thread

이번에는 액티비티가 화면에 보이지 않더라도 별도로 만들어준 Thread 는 백그라운드에서 계속해서 동작하고 있음을 확인해보는 예제를 해봅시다. 그러기 위해 일단 Thread 를 하나 만들어주고, 5초 마다 현재 시간을 Toast 로 띄워주는 코드를 짜보겠습니다. 그리고 로그 기록도 같이 한번 남겨봅시다.

public class MainActivity extends AppCompatActivity {

    MyThread thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn).setOnClickListener(view -> {
            thread = new MyThread();
            thread.start();
        });
    }

    class MyThread extends Thread {
        boolean isRun = true;

        @Override
        public void run() {
            while(isRun){

                Date now = new Date();
                String s = now.toString();

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, s, Toast.LENGTH_SHORT).show();
                        Log.i("Ex51",s);
                    }
                });
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            Log.i("Ex51","스레드 종료");
        }
    }
}
  • while 문을 이용하여 반복적으로 토스트 메시지를 띄우도록 했습니다.
  • Date now = new Date(); 을 이용해 Date 객체를 생성했으며, 여기에는 현재 시간의 정보가 담겨있습니다.
  • String s = now.toString(); 을 이용하여 현재 시간 정보를 문자열로 바꿔주었습니다.
  • 토스트 메시지도 UI 의 일부이므로, runOnUiThread() 메서드를 활용해 처리해주었습니다.
  • Log.i("Ex51",s); 을 이용하여 Logcat 에 정보가 남도록 설정해주었습니다.

이렇게 하고 실행을 시켜 버튼을 눌러보면 5초마다 현재시간을 알려주는 Toast 메시지가 띄워지는걸 볼 수 있습니다. 이제 Logcat 을 봅시다. 로그캣에도 역시 5초마다 로그를 띄웁니다. 여기서 중요한 부분은 액티비티를 종료했을 때입니다. back 버튼을 눌러 액티비티를 화면에서 없애봅시다. 그리고 로그캣을 다시 봅시다. 그래도 여전히 로그는 찍히는걸 볼 수 있습니다.

왜 이런 상황이 발생했을까요? 우리는 앱 자체의 프로세스를 종료한것이 아닙니다. 스레드의 경우 프로세스 내부의 직원 객체인데, 프로세스를 종료하지 않았으므로 별도의 Thread 객체는 죽지않고 살아있게 되는겁니다. 우리가 back 버튼을 눌렀을 때는 보이지 않을 뿐인거죠. 그러면 어떻게 해야 스레드를 종료할 수 있을까요? 사용자의 입장에서는 back 버튼을 누르면 앱이 종료되었다고 생각할테니, 우리도 그에 맞춰서 back 버튼을 누르면 앱이 완전히 종료되게끔 코드를 짜봅시다. back 버튼을 눌렀을 때 자동으로 호출되는 메서드인 onBackPressed() 메서드를 이용해봅시다.

이 메소드 내부에서 finish() 메서드를 호출하면 MainActivity 는 종료됩니다. 다만, 프로세스를 종료하는것이 아니기 때문에 로그캣을 확인해보면 별도의 Thread 는 계속적으로 실행되는 모습을 확인할 수 있습니다.

여기에서 메서드 하나를 소개하겠습니다. 액티비티가 메모리에서 없어질 때, 자동으로 실행되는 콜백메서드인 onDestroy() 메서드 입니다. 그렇다면, back 버튼을 눌렀을 때 finish() 메서드를 이용하여 MainActivity 를 종료했으므로 액티비티가 메모리에서 없어졌습니다. 그렇기 때문에 onDestroy() 메소드가 호출되겠네요. 그러면 이 메소드 내부에서 별도의 Thread 를 종료해주어야 합니다.

우리가 Thread 를 만들 때 , isRun 이라는 boolean 변수하나를 설정하고 true 의 값을 준 뒤, while 문의 조건으로 넣어주었습니다. 그렇다면 onDestroy() 메소드에서 이 변수의 값을 false 로 바꿔준다면 while 문이 종료되고, 그러면 별도의 Thread 의 run() 메서드가 종료되므로 Thread 가 종료되게 됩니다.

profile
Developer

0개의 댓글