프로그램 (Program, 정적 프로그램(Static Program))
- 윈도우의 *.exe 파일이나 Mac의 *.dmg 파일과 같은 컴퓨터에서 실행할 수 있는 파일
- 어떤 작업을 하기 위해 실행할 수 있는 파일. 아직 실행하지 않은 상태이기 때문에 정적 프로그램으로도 불린다.
- 파일이 저장 장치에 있지만 메모리에는 올라가 있지 않은 정적인 상태
- 모든 프로그램은 *운영체제가 실행되기 위한 메모리 공간을 할당해 줘야 실행될 수 있다.
- 운영체제(Operating System, OS) : 컴퓨터 시스템의 핵심 소프트웨어로, 컴퓨터 하드웨어와 응용 프로그램 간의 상호작용을 관리하고 제어하는 역할을 한다.
- 모든 프로그램은 *운영체제가 실행되기 위한 메모리 공간을 할당해 줘야 실행될 수 있다.
프로세스(Process)
- 운영체제로부터 자원을 할당받은 작업의 단위
- 프로그램을 실행시켜 정적인 프로그램이 동적으로 변하여 프로그램이 돌아가고 있는 상태
- 즉, 컴퓨터에서 실행되어 작업 중인 프로그램을 의미한다.
- 프로그램을 실행하는 순간 파일은 컴퓨터 메모리에 올라가게 되고, 운영체제로부터 시스템 자원(CPU)을 할당받아 프로그램 코드를 실행시킨다.
- 프로그램이 실행되어 프로세스가 만들어지면 *4가지의 메모리 영역(Code, Data, Stack, Heap)으로 구성되어 할당받게 된다.
- 코드 영역(Code / Text) : 프로그래머가 작성한 프로그램 함수들의 코드가 CPU 가 해석 가능한 기계어 형태로 저장되어 있다.
- 데이터 영역(Data) : 코드가 실행되면서 사용하는 전역 변수나 각종 데이터들이 모여있다. 데이터 영역은 .data, .rodata, .bss 영역으로 세분화된다.
- .data : 전역 변수 또는 static 변수 등 프로그램이 사용하는 데이터를 저장
- .bss : 초기값없는 전역 변수, static 변수가 저장
- .rodata : const 같은 상수 키워드로 선언된 변수나 문자열 상수가 저장
- 스택 영역(Stack) :
- 지역 변수와 같은 호출한 함수가 종료되면 되돌아올 임시적인 자료를 저장하는 독립적인 공간이다. Stack 은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다. 만일 Stack 영역을 초과하면 stack overflow 에러가 발생한다.
- 함수 호출 시 전달되는 인자, 되돌아갈 주소값, 함수 내에서 선언하는 변수 등을 저장하는 메모리 공간이기 때문에, 독립적인 Stack 을 가졌다는 것은 독립적인 함수 호출이 가능하다는 의미이다. 그리고 독립적인 함수 호출이 가능하다는 것은 독립적인 실행 흐름이 추가된다는 말이다.
- 즉, stack 을 가짐으로써 스레드는 독립적인 실행 흐름을 가질 수 있게 되는 것이다. 반면에 프로세스는 기본적으로 프로세스끼리 다른 프로세스의 메모리에 직접 접근할 수 없다.
- 힙 영역(Heap) : 생성자, 인스턴스와 같은 동적으로 할당되는 데이터들을 위해 존재하는 공간이다. 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.
- 프로그램이 여러 개 실행되면, 메모리에는 각 프로세스의 주소 공간이 생성되고, 그 안에 4가지의 메모리 영역 공간이 만들어지게 된다.
- 프로그램이 실행되어 프로세스가 만들어지면 *4가지의 메모리 영역(Code, Data, Stack, Heap)으로 구성되어 할당받게 된다.
프로세스의 자원 공유
- 기본적으로 각 프로세스는 메모리에 별도의 주소 공간에서 실행되기 때문에, 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근할 수 없다.
- 방법 1. IPC(Inter-Process Communication)
- 방법 2. LPC(Local inter-Process Communication)
- 방법 3. 별도로 공유 메모리를 만들어서 정보를 주고받도록 설정
- 그러나 프로세스 자원 공유는 단순히 *CPU 레지스터 교체뿐만이 아니라 RAM 과 CPU 사이의 캐시 메모리까지 초기화되기 때문에 자원 부담이 크다는 단점이 있다. 그래서 다중 작업이 필요한 경우 스레드를 이용하는 것이 훨씬 효율적이라, 현대 컴퓨터의 운영체제에선 다중 프로세싱을 지원하고 있지만 다중 스레딩을 기본으로 하고 있다.
- CPU 레지스터(Register) : 컴퓨터 프로세서 내에서 현재 계산 중인 값을 저장하는 매우 빠른 기억 장소
- 또한, 과거에는 프로그램 실행에 하나의 프로세스만을 사용하여 작업했지만, 프로그램이 복잡해지고 다양해짐에 따라 이런 방식에는 한계가 있었다.
- 파일을 다운로드하면 실행 시작부터 실행 끝까지 프로세스 하나만을 사용하기 때문에 다운이 완료될 때까지 하루종일 기다려야 했다.
- 동일한 프로그램을 여러 개의 프로세스로 만들게 되면, 그만큼 메모리를 차지하고 CPU 에서 할당받는 자원이 비효율적으로 사용될 수 있다.
스레드(Thread)
- 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위
- 멀티(다중) 스레드 : 하나의 프로세스 안에서 여러 가지 작업들 흐름이 동시에 진행되는 작업 갈래
- 일반적으로 하나의 프로그램은 하나 이상의 프로세스를 가지고 있고, 하나의 프로세스는 반드시 하나 이상의 스레드를 갖는다.
- 즉, 프로세스를 생성하면 기본적으로 하나의 메인 스레드가 생성되게 된다.
- 스레드끼리 프로세스의 자원을 공유하면서 프로세스 실행 흐름의 일부가 되기 때문에 동시 작업이 가능하다.
- 4가지 메모리 영역 중 스레드는 Stack 만 할당받아 복사하고 Code, Data, Heap 은 프로세스 내의 다른 스레드들과 공유된다.
- 각각의 스레드는 별도의 stack 을 가지고 있지만, heap 메모리는 고유하기 때문에 서로 다른 스레드에서 가져와 읽고 쓸 수 있게 된다.
메인 스레드(Main Thread)
- 프로세스의 시작과 동시에 실행되는 최초의 스레드
- 프로세스가 시작될 때 main() 함수가 최초의 실행 시작점이 되어 순차적으로 진행되는 흐름
안드로이드 앱의 메인 스레드
- JVM 이 코틀린 프로그램을 실행할 때 main() 함수를 찾아서 실행하기 때문에, 일반적인 프로그램을 작성할 때는 프로그램의 시작점(entry point)인 main() 함수를 반드시 구현해야 한다.
- 안드로이드 앱은 JVM 대신 Android Runtime 에서 실행되기 때문에, main() 함수를 구현할 필요가 없다.
- 안드로이드 앱의 시작점은 액티비티로 지정되며, 앱의 매니페스트 파일에서 특정 액티비티를 런처(Launcher)로 설정하면, 해당 액티비티가 애플리케이션의 시작점이 된다.
방법 1. Thread 클래스 상속
- Thread 클래스를 *상속한 클래스를 만들고 run() 메서드를 오버라이드한 다음, 클래스 인스턴스를 생성하고 start() 메서드를 호출하는 것
- 상속(extends) : 부모 클래스의 특징을 물려받아 재사용하는 것을 기본으로, 부모의 기능을 재정의하거나, 새로운 기능을 추가하여 클래스를 확장하는 것
- 단점 : 다른 클래스를 상속해야 한다면, Thread 클래스를 상속할 수 없게 되어 스레드를 생성하는 것이 불가능해진다.
class MyThread : Thread() {
override fun run() {
for (i in 1..3) {
println("MyThread 실행 중: $i (Thread: ${Thread.currentThread().name})")
Thread.sleep(500)
}
}
}
fun main() {
println("메인 스레드 시작 (Thread: ${Thread.currentThread().name})")
val thread = MyThread()
thread.start()
for (i in 1..3) {
println("메인 스레드 실행 중: $i (Thread: ${Thread.currentThread().name})")
Thread.sleep(300)
}
println("메인 스레드 종료 (Thread: ${Thread.currentThread().name})")
}
메인 스레드 시작 (Thread: main)
MyThread 실행 중: 1 (Thread: Thread-0)
메인 스레드 실행 중: 1 (Thread: main)
MyThread 실행 중: 2 (Thread: Thread-0)
메인 스레드 실행 중: 2 (Thread: main)
MyThread 실행 중: 3 (Thread: Thread-0)
메인 스레드 실행 중: 3 (Thread: main)
메인 스레드 종료 (Thread: main)
방법 2. Runnable 인터페이스 구현
- Runnable 인터페이스를 구현하는 클래스를 선언하고 run() 메서드를 구현한 다음, 클래스 인스턴스를 Thread 클래스 인스턴스의 생성자에 전달하고 Thread 클래스 인스턴스의 start() 메서드를 호출하는 것
- Runnable 인터페이스는 abstract 로 선언된 단 하나의 메서드, run() 메서드만을 가진다.
- 새로운 스레드를 실행하는 것은 Thread 클래스의 역할이며, Runnable 은 단지 새로운 스레드에서 실행될 run() 메서드를 정의하는 인터페이스일 뿐이다.
class MyRunnable : Runnable {
override fun run() {
for (i in 1..3) {
println("MyRunnable 실행 중: $i (Thread: ${Thread.currentThread().name})")
Thread.sleep(500)
}
}
}
fun main() {
println("메인 스레드 시작 (Thread: ${Thread.currentThread().name})")
val myRunnable = MyRunnable()
val thread = Thread(myRunnable)
thread.start()
for (i in 1..3) {
println("메인 스레드 실행 중: $i (Thread: ${Thread.currentThread().name})")
Thread.sleep(300)
}
println("메인 스레드 종료 (Thread: ${Thread.currentThread().name})")
}
메인 스레드 시작 (Thread: main)
메인 스레드 실행 중: 1 (Thread: main)
MyRunnable 실행 중: 1 (Thread: Thread-0)
메인 스레드 실행 중: 2 (Thread: main)
MyRunnable 실행 중: 2 (Thread: Thread-0)
메인 스레드 실행 중: 3 (Thread: main)
MyRunnable 실행 중: 3 (Thread: Thread-0)
메인 스레드 종료 (Thread: main)
안드로이드 메인 스레드는 UI 스레드이다.
- UI 를 올바르게 표시하기 위해서는 각 요소를 그리는 순서가 매우 중요하기 때문에, 모든 그리기 작업은 반드시 하나의 스레드, 즉 메인 스레드에서 순차적으로 이루어져야 한다.
- 메인 스레드는 Message Queue 수신을 대기하는 Loop(루프)를 실행하고, 사용자 입력과 시스템 이벤트, 화면 그리기 등의 메시지가 수신되면 각 메시지에 매핑된 Handler 의 메서드를 실행한다.
UI 프레임워크에서의 메인 UI 스레드 기본 동작
- 사용자 입력이 필요하지 않은 프로그램은 시작에서 종료까지 단방향 실행의 선형적 구조를 가지지만, 사용자 입력이 필수인 프로그램에서는 사용자 입력 이벤트를 처리하기 위한 Loop 가 실행되어야 한다.
- UI 프레임워크는 *Message Queue 를 사용하여 Loop 의 코드를 작성하도록 가이드한다.
- Message Queue : 시스템 이벤트를 발생 순서대로 전달받아 처리하기 위해 사용하는 구조
- Message : 사용자 입력을 포함한 시스템의 모든 이벤트를 전달할 때 사용하는 객체
- Queue : FIFO(First In First Out) 형식으로 동작하는 자료구조
- Message Queue : 시스템 이벤트를 발생 순서대로 전달받아 처리하기 위해 사용하는 구조
- 시스템(또는 프로세스)에서 발생한 새로운 메시지가 Message Queue 에 수신되면, 메시지가 담고 있는 내용에 따라 적절한 Handler(핸들러) 메서드가 호출된다.
- 그러나 시스템에서 발생 가능한 모든 이벤트를 처리할 필요는 없기 때문에, 보통 자신이 관심을 가지는 이벤트에 대한 메시지를 Handler 로 등록하고 호출되도록 만든다.
안드로이드에서의 메인 UI 스레드
- 시스템 이벤트를 처리하기 위한 Loop 는 *Looper 클래스를 통해 실행된다.
- Looper 클래스 : Loop 를 실행하고, 그 Loop 안에서 Message Queue 로 전달되는 메시지가 존재하는지 검사한다. 그리고 새로운 메시지가 도착하면, 해당 메시지를 처리할 Handler 메서드를 실행한다.
- 안드로이드의 Handler 는 Handler 클래스가 담당한다. Handler 는 메시지 수신 시 그 처리를 담당하는 역할만 수행하는데, 안드로이드의 Handler 는 Looper가 가진 Message Queue 를 다룰 수 있기 때문에 메시지를 보내는 역할까지 포함한다.
- 즉, 안드로이드 Handler 는 새로운 메시지를 보내거나 수신된 메시지에 대한 처리를 담당하는 주체가 되는 것이다.
- 메인 스레드가 아닌 다른 스레드에서 뷰에 대한 그리기 작업을 수행하려면 Handler 를 사용하여 메인 스레드로 메시지를 보내야 한다.
class MainActivity : AppCompatActivity() {
private lateinit var clockTextView: TextView
private val mHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
val cal = Calendar.getInstance()
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val strTime = sdf.format(cal.time)
clockTextView = findViewById(R.id.clock)
clockTextView.text = strTime
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
class NewRunnable : Runnable {
override fun run() {
while (true) {
Thread.sleep(1000)
mHandler.sendEmptyMessage(0)
}
}
}
val nr = NewRunnable()
val t = Thread(nr)
t.start()
}
}
ANR(Application Not Responding)
- 메인 스레드가 오랫동안 블로킹되면, 애플리케이션이 응답하지 않는다는 ANR(Application Not Responding) 오류가 발생한다.
- 메인 스레드에서는 무한 루프나 실행 시간이 긴 작업, 또는 Thread.sleep()을 통한 과도한 대기 등의 코드 작성을 피해야 한다.
- 네트워킹, DB 트랜잭션 (I/O 작업 등) 등 많은 시간동안 수행해야 하는 작업의 경우 ANR 발생 가능성이 매우 높기 때문에, 별도의 쓰레드를 사용하거나 비동기 데이터 스트림 (Coroutines, RxJava 등) 을 활용해야 한다.
- ANR 이 발생하는 조건
안드로이드 스레드 통신 : 핸들러(Handler)
1. 메시지(Message)
- 스레드 통신에서 핸들러를 사용하여 데이터를 보내기 위해서는, 데이터 종류를 식별할 수 있는 식별자와 실질적인 데이터를 저장한 객체, 그리고 추가 정보를 전달할 객체가 필요하다.
- 즉, 전달할 데이터를 한 곳에 저장하는 역할을 하는 클래스가 필요한데, 이 역할을 하는 클래스가 바로 Message 클래스이다.
- 하나의 데이터를 보내기 위해서는 한 개의 Message 인스턴스가 필요하며, 일단 데이터를 담은 Message 객체를 Handler 로 보내면 해당 객체는 Handler 와 연결된 Message Queue 에 쌓이게 된다.
- Message 클래스에는 public 클래스 변수가 존재한다.
2. 메시지 큐(Message Queue)
- Message 객체를 큐(Queue) 형태로 관리하는 자료 구조
- 큐(Queue)라는 이름처럼 FIFO(First In First Out) 방식으로 작동하여, 메시지는 큐에 들어온 순서에 따라 차례로 저장된다.
3. 루퍼(Looper)
- Message Queue 는 Message 객체 리스트를 관리하는 클래스일 뿐, 큐에 쌓인 메시지 처리를 위한 Handler 를 실행시키지는 않는다.
- 즉, Message Queue 로부터 메시지를 꺼내온 다음, 해당 메시지와 연결된 Handler 를 호출하는 역할은 Looper 가 담당한다.
- 메인 스레드에는 Looper 객체를 사용하여 메시지 루프를 실행하는 코드가 이미 구현되어 있고, 해당 루프 안에서 Message Queue 의 메시지를 꺼내어 처리하도록 만들어져 있다.
4-1. 핸들러(Handler) with Message
- 스레드의 Looper 와 연결된 Message Queue 로 메시지를 보내고 처리할 수 있게 만들어 준다.
- 메인 스레드의 메시지 처리 흐름에서, 메시지 전달과 처리를 위해 개발자가 접근할 수 있는 창구 역할을 수행한다고 할 수 있다.
- 주된 목적은 Handler 를 통해 데이터를 전달하여, 전달된 데이터 처리를 위해 작성된 대상 스레드의 코드가 실행되도록 만드는 것이다. 이를 위해, 메시지 객체에 값을 채워 수신 스레드의 Handler 에 보내고, 수신 측 스레드에서는 handleMessage() 메서드를 오버라이드하여 수신된 메시지 객체를 처리하기 위해 작성된 코드를 실행하는 것이다.
- Handler 객체는 생성과 동시에 해당 스레드에서 동작 중인 Looper 와 Message Queue 에 자동으로 연결된다.
class MainActivity : AppCompatActivity() {
private lateinit var handler: Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_A -> {
val data = msg.obj as String
Log.d("MainActivity", "Received message: $data")
}
}
}
}
val newThread = NewThread(handler)
newThread.start()
}
}
class NewThread(private val handler: Handler) : Thread() {
override fun run() {
while (true) {
val message = handler.obtainMessage()
message.what = MSG_A
message.obj = "Hello from NewThread"
handler.sendMessage(message)
Thread.sleep(1000)
}
}
companion object {
const val MSG_A = 1
}
}
- handleMessage(msg: Message) : 메시지 루프를 통해 Message Queue 로부터 꺼낸 메시지를 처리할 수 있도록, Looper 에 의해 실행되는 메서드
- 다른 스레드로부터 전달된 데이터는 msg 인스턴스에 담겨 있으며, 일반적으로, 정수 타입인 what 변수의 값에 따라 if 또는 switch 등의 조건문으로 처리한다.
- 파라미터, 메시지가 보내지는 시점, 메시지 큐 내 위치 등에 따라 다양한 프로토타입이 존재한다.
- obtainMessage() : 글로벌 메시지 풀(Global Message Pool)로부터 메시지 객체를 가져오는 메서드로, 정적(static)으로 생성된 재사용(recycled) 객체로 관리되기 때문에 새로운 Message 인스턴스를 만드는 것보다 효율적이다.
- Message 객체를 획득할 때 모든 클래스 변수를 사용해야 하는 것은 아니기 때문에, obtainMessage() 메서드는 사용할 필드 종류에 따라 여러 형태의 메서드가 존재한다.
- sendMessage() : 생성한 Message 객체를 수신 측 스레드로 보내는 메서드
- AtTime, Delayed, AtFrontOfQueue 등의 접미사가 붙은 메서드는 메시지가 처리되는 시점을 조절하기 때문에, 메시지 큐 안에서의 메시지 처리 우선순위 또는 스레드 실행 대기 시간 등에 영향을 미칠 수 있다.
4-2. 핸들러(Handler) with Runnable
- 스레드의 Looper 와 연결된 Message Queue 로 Runnable 를 보내고 처리할 수 있게 만들어 준다.
- 메시지를 통해 데이터를 전달하는 번거로운 과정 없이, Handler 에 실행 코드가 담긴 객체를 보내고, 대상 스레드에서는 수신된 객체의 코드를 직접 실행하도록 만든다.
class MainActivity : AppCompatActivity() {
private lateinit var clockTextView: TextView
private val mHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clockTextView = findViewById(R.id.clock)
val runnable = Runnable {
val cal = Calendar.getInstance()
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val strTime = sdf.format(cal.time)
clockTextView.text = strTime
}
class NewRunnable : Runnable {
override fun run() {
while (true) {
Thread.sleep(1000)
mHandler.post(runnable)
}
}
}
val nr = NewRunnable()
val t = Thread(nr)
t.start()
}
}
- post(r: Runnable) : 생성한 Runnable 객체를 수신 측 스레드로 보내는 메서드
- AtFrontOfQueue, AtTime, Delayed 등의 접미사가 붙은 메서드는 메시지큐 내에서의 Runnable 객체의 위치와 Runnable 객체가 처리되는 시점을 직접 지정하기 때문에, Runnable 객체의 처리 우선순위 또는 스레드 실행 대기 시간 등에 영향을 미칠 수 있다.
- Thread 생성 시에 Runnable 인터페이스를 implements 하는 것은 굳이 Thread 클래스를 상속하지 않아도 스레드 실행 코드를 작성할 수 있기 때문이지, Runnable 의 용도가 새로운 스레드를 만드는 것에만 국한된 것은 아니다.
class MainActivity : AppCompatActivity() {
private lateinit var clockTextView: TextView
private val mHandler: Handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clockTextView = findViewById(R.id.clock)
val updateClockRunnable = object : Runnable {
override fun run() {
val cal = Calendar.getInstance()
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val strTime = sdf.format(cal.time)
clockTextView.text = strTime
mHandler.postDelayed(this, 1000)
}
}
mHandler.post(updateClockRunnable)
}
}
5. runOnUiThread()
- Activity 클래스에서 제공되는 메서드
- 개발자가 만든 Runnable 객체를 메인 스레드에서 실행하도록 하는 메서드
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
- 현재 스레드가 메인 스레드인지 여부를 검사하여 메인 스레드가 아니라면 post() 메서드를 실행하고, 메인 스레드라면 Runnable 의 run() 메서드를 직접 실행한다.
class MainActivity : AppCompatActivity() {
private lateinit var clockTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clockTextView = findViewById(R.id.clock)
val runnable = Runnable {
val cal = Calendar.getInstance()
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val strTime = sdf.format(cal.time)
clockTextView.text = strTime
}
class NewRunnable : Runnable {
override fun run() {
while (true) {
Thread.sleep(1000)
runOnUiThread(runnable)
}
}
}
val nr = NewRunnable()
val t = Thread(nr)
t.start()
}
}
'안드로이드 > Android' 카테고리의 다른 글
[CS] 응집도(Cohesion)와 결합도(Coupling) (0) | 2024.12.08 |
---|---|
[Android] 빌드, MultiDex (0) | 2024.11.30 |
[Android] 이미지 최적화(로드 개선) (0) | 2024.11.12 |