액티비티
고친 사람 github-actions[bot]
액티비티는 안드로이드 앱에서 화면 한 장을 띄우고 사용자의 손길을 받아 주는 부품입니다. 사용자가 앱을 열면 액티비티 하나가 떠서 화면을 채웁니다. 업무를 자동으로 넘기는 워크플로에서는 같은 이름이 업무 절차를 이루는 한 단계를 가리킵니다. 두 뜻은 이름만 같고 서로 이어지지 않습니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 안드로이드 앱의 화면 한 장을 맡습니다. 메일 앱이라면 받은 편지함 화면이 액티비티 하나이고, 메일 한 통을 읽는 화면이 또 하나입니다. 워크플로에서는 「결제 승인」 같은 업무 한 단계를 같은 이름으로 부릅니다.
왜 이렇게 하나 — 폰은 화면이 하나이고 메모리가 작습니다. 어느 화면을 앞에 둘지, 안 보이는 화면을 언제 치울지는 앱이 아니라 운영체제가 정해야 합니다. 그래서 화면을 운영체제가 다룰 수 있는 부품으로 떼어 둡니다.
어떻게 도나
- 앱은 액티비티 클래스를 써 두기만 하고, 객체는 운영체제가 만듭니다
- 화면이 보이고 가려지고 닫힐 때마다 운영체제가 정해진 메서드를 불러 알립니다
- 새 화면을 열면 앞 화면 위에 쌓이고, 뒤로 가기를 누르면 맨 위가 닫힙니다
대가 — 액티비티는 기기를 가로로 눕혀 화면 방향이 바뀌기만 해도 새로 만들어집니다. 들고 있던 값은 그때 사라지므로 따로 챙겨 둬야 합니다.
상세
이 절은 「액티비티」의 뜻들을 표로 가른 뒤, 안드로이드와 워크플로의 뜻을 차례로 펼칩니다.
액티비티가 가리키는 물건들
「액티비티」는 설계 그림에서도 쓰여서 뜻이 셋입니다. 셋째는 곁가지라 짧게 짚고, 앞의 두 뜻을 주로 봅니다. 뜻마다 끊어 내는 대상이 다릅니다. 안드로이드는 앱을 화면 단위로 끊고, 워크플로는 업무를 단계 단위로 끊습니다.
| 맥락 | 액티비티 하나가 가리키는 것 | 예 |
|---|---|---|
| 안드로이드 앱 | 화면 한 장을 맡는 부품 | 받은 편지함 화면 |
| 워크플로 | 업무 절차를 이루는 한 단계 | 주문 처리 중 「결제 승인」 |
| 설계 그림 | 동작 여럿이 이어지는 흐름 하나 | 주문 접수에서 배송까지 그린 그림 |
셋째 줄은 UML(Unified Modeling Language, 통합 모델링 언어)의 액티비티 다이어그램에서 쓰는 말입니다. 동작이 어떤 순서로 흐르고 어디서 갈리는지를 그린 그림입니다.
아래 소절들은 앞의 두 뜻을 따로 펼칩니다. 이 둘이 서로 가장 멉니다. 대화에서 제일 자주 부딪히는 것도 이 둘입니다.
안드로이드의 액티비티
안드로이드는 스마트폰과 태블릿에서 도는 운영체제입니다. 안드로이드 앱은 몇 가지 부품을 조립해서 만듭니다. 앱 컴포넌트는 운영체제가 알아보고 직접 띄우는 이 부품들입니다.
액티비티는 그중 화면을 맡는 부품입니다. 액티비티 하나가 화면 한 장을 채웁니다. 그 화면에서 일어나는 터치와 입력도 액티비티가 받습니다. 메일 앱이라면 받은 편지함이 액티비티 하나, 메일 읽기 화면이 또 하나입니다.
액티비티는 앱으로 들어오는 입구이기도 합니다. 홈 화면의 아이콘을 누르면 운영체제가 그 앱의 첫 액티비티를 띄웁니다. 다른 앱이 사진을 공유하면서 내 앱의 글쓰기 화면을 곧바로 열 수도 있습니다.
앱에 main 함수가 없다
백엔드 서버 프로그램은 main 함수에서 시작합니다. 서버를 띄우면 그 함수가 돌면서 요청을 기다리는 반복을 스스로 엽니다.
안드로이드 앱에는 개발자가 쓰는 main 이 없습니다. 액티비티 객체를 만드는 쪽은 앱이 아니라 운영체제입니다. 앱은 액티비티 클래스를 써 두기만 합니다. 정해진 메서드를 부르는 것도 운영체제입니다.
이렇게 틀이 객체를 만들고 개발자 코드를 불러 주는 방식을 제어의 역전이라고 부릅니다. 서블릿 컨테이너가 서블릿 객체를 만들고 요청마다 그 메서드를 부르는 것과 같은 꼴입니다.
운영체제가 이 일을 쥐는 까닭은 폰의 사정에 있습니다. 화면은 하나이고 메모리는 작습니다. 어느 화면을 앞에 둘지, 안 보이는 화면을 언제 치울지를 앱마다 제멋대로 정하면 폰 전체가 버티지 못합니다.
매니페스트와 인텐트
운영체제가 액티비티를 만들려면 앱에 어떤 액티비티가 있는지 먼저 알아야 합니다. 그래서 앱은 가진 액티비티를 매니페스트에 적어 둡니다. 매니페스트는 앱 꾸러미 안에 든 설정 파일입니다. 앱의 부품 목록과 필요한 권한을 담습니다. 아이콘을 눌렀을 때 처음 띄울 액티비티도 여기에 표시합니다.
한 화면에서 다른 화면을 열 때도 액티비티 객체를 직접 만들지 않습니다. 「이 액티비티를 열어 달라」는 요청을 운영체제에 보냅니다. 이 요청을 담는 객체가 인텐트입니다.
인텐트에 클래스 이름 대신 「사진을 공유하고 싶다」 같은 하고 싶은 일을 적을 수도 있습니다. 그러면 운영체제가 그 일을 받겠다고 매니페스트에 적어 둔 액티비티를 찾아 띄웁니다. 다른 앱이 내 앱의 화면을 여는 것도 이 길로 갑니다.
액티비티를 쓰는 코드
아래는 코틀린으로 쓴 가장 작은 액티비티입니다. 모양만 보면 됩니다.
class InboxActivity : Activity() {
override fun onCreate(saved: Bundle?) {
super.onCreate(saved)
setContentView(R.layout.inbox)
}
}
Activity 를 물려받아 화면 하나를 맡을 클래스를 만듭니다. onCreate 는 운영체제가 이 액티비티 객체를 막 만들었을 때 부르는 메서드입니다. 그 안의 setContentView 가 화면 배치를 붙입니다.
R.layout.inbox 는 버튼과 목록을 어디에 둘지 적은 레이아웃 파일을 가리킵니다. 인자로 들어오는 saved 는 전에 저장해 둔 화면 상태입니다. 처음 뜰 때는 비어 있습니다(null). 이 값은 뒤의 「운영체제가 액티비티를 치우는 때」 소절에서 다시 나옵니다.
다른 화면을 여는 코드는 이렇게 생겼습니다.
val intent = Intent(this, MailActivity::class.java)
startActivity(intent)
인텐트에 열고 싶은 액티비티의 클래스를 담아 startActivity 로 운영체제에 넘깁니다. 그러면 운영체제가 MailActivity 객체를 만들고 그 onCreate 를 부릅니다.
생명주기
액티비티는 떠 있는 동안 여러 상태를 거칩니다. 만들어지고, 화면에 보이고, 맨 앞에서 입력을 받다가, 가려지고, 없어집니다. 이 상태들이 옮겨 가는 순서가 액티비티의 생명주기입니다.
상태가 바뀔 때마다 운영체제는 액티비티의 정해진 메서드를 부릅니다. 콜백은 개발자가 부르지 않고 틀이 불러 주는 이런 메서드입니다. 앞 코드의 onCreate 가 그 첫 번째입니다.
stateDiagram-v2
state "만들어짐" as C
state "보임" as S
state "맨 앞 · 입력을 받음" as R
state "가려짐" as T
[*] --> C: onCreate
C --> S: onStart
S --> R: onResume
R --> S: onPause
S --> T: onStop
T --> S: onRestart · onStart
T --> [*]: onDestroy
그림의 화살표 이름이 콜백입니다. 콜백은 들어갈 때와 나올 때가 짝을 이룹니다. onRestart 하나를 빼면 여섯 개가 세 짝으로 줄어듭니다. onRestart 는 아래에서 따로 봅니다.
| 무엇이 바뀌나 | 들어갈 때 | 나올 때 |
|---|---|---|
| 객체가 생기고 없어짐 | onCreate |
onDestroy |
| 화면에 보이고 가려짐 | onStart |
onStop |
| 맨 앞에서 입력을 받고 놓음 | onResume |
onPause |
메일을 읽다가 홈 버튼을 누르면 onPause 와 onStop 이 차례로 불립니다. 다시 앱으로 돌아오면 onRestart, onStart, onResume 이 불립니다. 가려져 있던 액티비티가 다시 보일 때만 onRestart 가 한 번 끼어듭니다.
개발자는 이 짝에 맞춰 할 일을 나눕니다. 카메라나 위치 센서처럼 붙잡고 있으면 전지를 먹는 자원은 onPause 나 onStop 에서 놓습니다. 그리고 돌아올 때 짝이 되는 콜백에서 다시 잡습니다. 놓지 않으면 사용자가 안 보는 동안에도 자원이 계속 돕니다.
운영체제가 액티비티를 치우는 때
액티비티 객체는 운영체제가 만들었으니 치우는 것도 운영체제입니다. 사용자가 뒤로 가기를 눌러 화면을 닫을 때만 치우는 것이 아닙니다. 사용자가 아무것도 닫지 않았는데 치우는 때가 둘 있습니다.
첫째는 기기를 눕혀 화면 방향이 바뀔 때입니다. 가로와 세로는 화면 배치가 다르므로, 운영체제는 기본으로 액티비티를 없애고 새로 만듭니다. 글자 크기나 언어 설정이 바뀔 때도 같습니다. 이렇게 기기 설정이 바뀌는 일을 구성 변경이라고 부릅니다.
둘째는 메모리가 모자랄 때입니다. 운영체제는 사용자가 안 보는 앱이 도는 프로세스(운영체제가 앱 하나에 내주는 실행 단위)를 끝내서 메모리를 돌려받습니다. 사용자가 그 앱으로 돌아오면 액티비티를 다시 만듭니다.
두 경우 모두 액티비티 객체의 필드에 들고 있던 값이 사라집니다. 입력하던 글이 화면 방향이 바뀌자마자 지워지면 사용자는 앱이 고장 났다고 느낍니다.
그래서 운영체제는 치우기 전에 onSaveInstanceState 를 부릅니다. 액티비티는 여기서 되살릴 값을 번들에 담습니다. 번들은 키와 값을 담는 작은 꾸러미입니다. 앞 코드에서 onCreate 가 saved 로 받던 것이 이 번들입니다.
번들은 운영체제가 앱 밖에서 맡아 두는 꾸러미입니다. 그래서 화면 방향이 바뀔 때도, 프로세스가 끝났다가 앱이 다시 뜰 때도 남아 있습니다. 대신 앱 밖으로 넘기고 맡기는 데 품이 들어서, 입력하던 글처럼 작은 값만 담습니다.
화면 방향이 바뀔 때만 버티면 되는 큰 값은 뷰모델에 둡니다. 뷰모델은 구성 변경을 건너 살아남는 객체입니다. 앱 안에 사는 객체라서 프로세스가 끝나면 함께 사라집니다.
꼭 남아야 하는 값은 데이터베이스 같은 저장소에 씁니다. 세 곳을 「무엇을 건너 살아남나」로 모으면 이렇습니다.
| 담는 곳 | 화면 방향이 바뀔 때 | 프로세스가 끝날 때 | 담는 값 |
|---|---|---|---|
| 번들 | ✓ | ✓ | 입력하던 글처럼 작은 값 |
| 뷰모델 | ✓ | ✗ | 불러온 목록처럼 큰 값 |
| 데이터베이스 | ✓ | ✓ | 꼭 남아야 하는 값 |
번들과 데이터베이스는 둘 다 살아남지만 담는 크기가 다릅니다. 큰 값을 오래 남겨야 하면 데이터베이스로 갑니다.
백엔드 개발자에게는 익숙한 규칙입니다. 요청 사이의 상태를 서버 메모리에 두지 않고 바깥 저장소에 두는 무상태 서버와 같은 까닭입니다. 객체가 언제 사라질지 내가 정하지 못하면, 남겨야 할 값은 객체 밖에 둬야 합니다.
백 스택
액티비티를 열면 새 액티비티가 앞의 것 위에 쌓입니다. 뒤로 가기를 누르면 맨 위가 닫히고 바로 아래의 것이 다시 보입니다. 이렇게 쌓인 더미가 백 스택입니다.
flowchart TD
N["새 액티비티를 연다"] --> A
subgraph T["백 스택 · 위가 지금 보이는 화면"]
A["메일 쓰기 · 맨 위"]
B["메일 읽기 · 가려짐"]
C["받은 편지함 · 가려짐"]
A --- B --- C
end
그림에서 사용자는 받은 편지함에서 메일을 열고, 거기서 답장 쓰기를 열었습니다. 뒤로 가기를 한 번 누르면 메일 쓰기가 닫히고 메일 읽기가 보입니다. 한 번 더 누르면 받은 편지함으로 돌아옵니다.
사용자가 한 가지 일을 하려고 거쳐 간 액티비티들의 묶음을 태스크(task)라고 부릅니다. 한 태스크가 백 스택 하나를 가집니다. 다른 앱의 액티비티도 같은 태스크에 쌓일 수 있습니다. 메일에 사진을 붙이려고 카메라 앱의 화면을 열면, 그 화면이 메일 쓰기 위에 쌓입니다.
액티비티 옆의 앱 컴포넌트
액티비티는 네 가지 앱 컴포넌트 중 하나입니다. 나머지 셋은 화면이 없습니다. 운영체제가 알아보고 직접 띄운다는 점은 넷이 같습니다.
| 앱 컴포넌트 | 맡는 일 |
|---|---|
| 액티비티 | 화면 한 장을 맡고 입력을 받는다 |
| 서비스 | 화면 없이 뒤에서 작업을 이어 간다 |
| 브로드캐스트 리시버 | 전지 부족처럼 시스템이 알리는 일에 반응한다 |
| 콘텐츠 프로바이더 | 앱의 데이터를 다른 앱에 내준다 |
프래그먼트는 이 넷에 들지 않습니다. 프래그먼트는 액티비티 안의 한 구역을 빌려 들어가는 화면 조각입니다. 혼자서는 뜨지 못합니다. 요즘 앱은 액티비티 하나만 두고 그 안에서 프래그먼트를 갈아 끼우는 방식도 많이 씁니다.
워크플로의 액티비티
이 소절부터 뜻이 통째로 바뀝니다. 안드로이드도 화면도 나오지 않습니다.
워크플로는 업무를 정해진 규칙에 따라 사람과 시스템 사이로 넘기는 일을 자동으로 처리하는 것입니다. 무엇을 어떤 순서로 넘길지는 프로세스 정의에 적어 둡니다. 여기서 프로세스는 운영체제의 실행 단위가 아니라 업무 절차라는 뜻입니다.
액티비티는 그 프로세스를 이루는 한 단계입니다. 주문 처리라면 「재고 확인」, 「결제 승인」, 「배송 요청」이 각각 액티비티입니다. 프로세스 정의는 이 액티비티들과 그 사이를 잇는 규칙으로 이루어집니다.
액티비티는 기계가 할 수도 있고 사람이 할 수도 있습니다. 「결제 승인」은 결제 시스템을 부르는 자동 액티비티입니다. 「환불 검토」는 담당자가 보고 판단하는 수동 액티비티입니다. 이 일은 맡을 워크플로 참여자에게 배정됩니다.
워크플로 엔진은 프로세스를 돌리는 프로그램입니다. 엔진은 액티비티를 가장 작은 일 단위로 봅니다. 앞 액티비티가 끝나면 규칙을 보고 다음 액티비티를 시작시킵니다.
액티비티 단위로 다시 돌리기
프로세스 정의를 그림이나 설정 파일 대신 코드로 적는 워크플로 엔진도 있습니다. 이런 엔진은 대개 바깥 시스템을 부르는 일을 액티비티로 떼어 둡니다. 결제 시스템 호출처럼 네트워크를 타는 일은 중간에 실패할 수 있기 때문입니다.
그래서 여기서 액티비티는 앞 소절보다 뜻이 좁습니다. 업무 한 단계라기보다 바깥 시스템을 부르는 일 하나를 떼어 둔 단위입니다.
떼어 두면 실패한 단계만 골라 다시 돌릴 수 있습니다. 엔진은 끝난 액티비티의 결과를 기록해 둡니다. 실패하면 그 액티비티만 재시도합니다. 앞 단계를 처음부터 다시 하지 않습니다.
flowchart TD
A["재고 확인 · 끝남 · 결과 기록됨"] --> B["결제 승인"]
B -->|실패| B
B -->|성공| C["배송 요청"]
그림에서 결제 승인이 실패하면 그 액티비티만 되돌아 다시 돕니다. 재고 확인은 다시 하지 않습니다.
그래서 액티비티는 두 번 돌아도 결과가 같게 짭니다. 응답이 사라졌을 뿐 첫 시도의 결제는 끝났을 수 있기 때문입니다. 멱등성은 여러 번 해도 한 번 한 것과 결과가 같은 성질입니다.
어느 뜻인지 가르는 단서
대화나 문서에서 「액티비티」가 나오면 함께 붙은 낱말을 봅니다. 대개 그것만으로 어느 뜻인지 갈립니다.
| 함께 나오는 말 | 뜻 |
|---|---|
화면 · 인텐트 · 매니페스트 · onCreate · 뒤로 가기 |
안드로이드 |
| 프로세스 정의 · 단계 · 담당자 · 엔진 · 재시도 | 워크플로 |
| 다이어그램 · 흐름 · 분기 · UML | 설계 그림 |
백엔드 개발자가 두 뜻을 한 대화에서 만나는 때가 있습니다. 모바일 팀과 서버 팀이 함께 주문 기능을 짤 때입니다. 모바일 쪽이 말하는 액티비티는 결제 화면이고, 서버 쪽이 말하는 액티비티는 결제 단계입니다.
관련 항목
액티비티와 함께 안드로이드 앱을 이루는 컴포넌트
Android · 앱 컴포넌트 · 서비스 (Android) · 브로드캐스트 리시버 · 콘텐츠 프로바이더 · 프래그먼트
액티비티를 선언하고 띄우는 장치
매니페스트 · 인텐트 · 인텐트 필터 · 백 스택 · 런치 모드 · 딥 링크 · 권한
액티비티의 생명주기와 상태를 지키는 수단
생명주기 · 콜백 · 구성 변경 · 번들 (Android) · 뷰모델 · 프로세스 · 데이터베이스
액티비티 화면을 그리는 부품
뷰 · 뷰 그룹 · 레이아웃 · Jetpack Compose · 메인 스레드 · Kotlin
안드로이드 액티비티와 같은 설계 원리를 쓰는 서버 쪽 구조
제어의 역전 · 서블릿 컨테이너 · 서블릿 · 무상태 · 프레임워크
워크플로 액티비티를 담는 구조
워크플로 · 프로세스 정의 · 워크플로 엔진 · 워크플로 참여자 · 워크 아이템 · 하위 프로세스 · 프로세스 인스턴스
워크플로 액티비티의 실패를 다루는 장치
재시도 · 멱등성 · 타임아웃 · 사가 · 보상 트랜잭션
액티비티라는 이름을 함께 쓰는 설계 표기법
UML · 액티비티 다이어그램 · BPMN · 순서도
다른 이름: activity · Activity · 안드로이드 액티비티 · 워크플로 액티비티