생명주기 메서드
고친 사람 github-actions[bot]
생명주기 메서드는 프레임워크가 정해진 때에 알아서 불러 주는 메서드입니다. 개발자는 그때 할 일만 적어 둡니다. 직접 부르지는 않습니다. 테스트에서는 테스트마다 앞뒤의 준비와 정리를 맡습니다. 객체나 화면이 생기고 사라지는 때를 다루는 다른 프레임워크도 같은 이름을 씁니다.
쉽고 빠른 이해
무슨 일을 하나 — 정해진 때가 오면 불릴 메서드를 적어 둡니다. 「테스트마다 그 앞에서 빈 장바구니를 하나 만든다」를 적어 둔 메서드가 그런 메서드입니다.
왜 이렇게 하나 — 테스트마다 같은 준비 코드를 되풀이하지 않아도 됩니다. 테스트가 실패해도 뒷정리는 빠짐없이 돕니다. 그래서 앞 테스트가 남긴 흔적이 다음 테스트로 넘어가지 않습니다.
어떻게 도나
- 메서드에 「언제 불러 달라」는 표시를 붙이거나 정해진 이름으로 짓습니다
- 테스트를 돌리는 프로그램이 그 표시를 보고 때에 맞춰 부릅니다
- 테스트 하나마다 준비, 테스트, 정리 순서로 돕니다
- 테스트 전체의 처음과 끝에 한 번만 도는 준비·정리도 따로 둘 수 있습니다
대가 — 테스트 본문만 읽어서는 무엇이 준비됐는지 안 보입니다. 처음 한 번만 도는 준비를 쓰면 여러 테스트가 같은 값을 나눠 씁니다. 한 테스트가 그 값을 고치면 다른 테스트가 흔들립니다.
상세
식당에는 때마다 정해진 일이 있습니다. 문을 열기 전에는 재료를 손질합니다. 손님 한 팀이 나가면 그 식탁을 치우고 새로 차립니다. 문을 닫으면 불을 끄고 설거지를 합니다.
생명주기 메서드는 이런 「때가 되면 할 일」을 적어 둔 메서드입니다. 언제 부를지 정하고 부르는 쪽은 프레임워크입니다. 개발자는 메서드를 적어 둘 뿐 자기 코드에서 부르지 않습니다.
생명주기는 무언가가 만들어져서 쓰이다가 사라지기까지의 흐름입니다. 테스트 하나도 준비되고, 돌고, 치워지는 흐름을 거칩니다. 생명주기 메서드는 이 흐름의 마디마다 끼어들 수 있게 열어 둔 통로입니다.
이 절은 테스트 코드의 생명주기 메서드를 중심으로 봅니다. 장바구니를 확인하는 테스트 클래스 하나를 들고 메서드가 언제 불리는지 봅니다. 끝에서는 테스트 밖의 프레임워크가 같은 이름을 어떻게 쓰는지 짧게 짚습니다.
테스트를 이루는 세 가지
생명주기 메서드를 보기 전에 테스트 코드가 무엇으로 이루어지는지 봅니다. 낱말 셋이 계속 나옵니다.
테스트 메서드는 확인 하나를 맡는 메서드입니다. 「장바구니에 2만 원을 담으면 합계가 2만 원이다」 같은 확인 하나가 메서드 하나입니다.
테스트 클래스는 한 대상을 확인하는 테스트 메서드를 모아 둔 클래스입니다. 장바구니 클래스 Cart 를 확인하는 메서드는 CartTest 라는 클래스 하나에 모읍니다.
테스트 러너는 테스트를 찾아 하나씩 돌리고 결과를 모아 보여 주는 프로그램입니다. 테스트 메서드도 생명주기 메서드도 사람이 아니라 러너가 부릅니다.
러너는 대개 테스트 프레임워크에 들어 있습니다. 테스트 프레임워크는 테스트를 짜고 돌리는 데 쓰는 프레임워크입니다. 앞에서 부르는 쪽이 프레임워크라고 했습니다. 테스트에서 그 일을 직접 맡는 부분이 러너입니다.
테스트 앞뒤에 끼어드는 네 메서드
테스트 프레임워크는 대개 끼어들 때를 넷 줍니다. 테스트 메서드 하나의 앞과 뒤, 그리고 테스트 클래스 전체의 처음과 끝입니다.
| 메서드 | 불리는 때 | 흔히 맡기는 일 |
|---|---|---|
| 준비 메서드 | 테스트 메서드마다 그 앞 | 검사할 객체를 새로 만든다 |
| 정리 메서드 | 테스트 메서드마다 그 뒤 | 테스트가 만든 임시 파일을 지운다 |
| 클래스 준비 | 첫 테스트 메서드 앞에 한 번 | 테스트용 데이터베이스를 띄운다 |
| 클래스 정리 | 마지막 테스트 메서드 뒤에 한 번 | 띄운 데이터베이스를 내린다 |
네 메서드는 둘씩 짝을 이룹니다. 앞에서 만든 것을 뒤에서 치우는 짝입니다. 준비 메서드가 만든 것은 정리 메서드가, 클래스 준비가 띄운 것은 클래스 정리가 거둡니다.
준비 메서드가 갖춰 두는 준비물을 테스트 픽스처라고 부릅니다. 빈 장바구니, 상품 몇 개가 든 데이터베이스처럼 테스트가 기대는 값과 환경입니다.
코드로 본 네 메서드
자바의 테스트 프레임워크 JUnit 5 로 장바구니 테스트를 짜 봅니다. JUnit 은 메서드 위에 붙이는 표시로 그 메서드를 언제 부를지 알아봅니다. @ 로 시작하는 이 표시를 어노테이션이라고 합니다.
class CartTest {
static Database db;
Cart cart;
@BeforeAll // 처음 한 번
static void startDb() {
db = Database.start();
}
@BeforeEach // 테스트마다 앞
void setUp() {
cart = new Cart(db);
}
@Test
void 담으면_합계가_는다() {
cart.add(20000);
int sum = cart.total(); // 20000
assertEquals(20000, sum);
}
@AfterEach // 테스트마다 뒤
void tearDown() {
cart.clear();
}
@AfterAll // 마지막 한 번
static void stopDb() {
db.stop();
}
}
어노테이션 줄 오른쪽 주석은 러너가 그 메서드를 부르는 때입니다. @Test 가 붙은 메서드가 테스트 메서드입니다. 나머지 넷이 생명주기 메서드입니다. 테스트 메서드 안에는 장바구니를 만드는 줄이 없습니다. 그 일은 setUp 이 맡았습니다.
assertEquals 는 나온 값이 기대한 값과 같은지 맞춰 보는 단언입니다. 둘이 다르면 테스트는 그 줄에서 멈춥니다. 그 테스트는 실패로 적힙니다.
클래스 준비와 클래스 정리에는 static 이 붙어 있습니다. 까닭은 JUnit 이 테스트 객체를 만드는 방식에 있습니다. JUnit 은 테스트 메서드를 하나 부를 때마다 CartTest 의 객체를 새로 만듭니다. 테스트 메서드가 둘이면 객체도 둘 생깁니다.
클래스 준비는 첫 테스트보다 먼저 한 번만 돕니다. 그때는 아직 CartTest 객체가 하나도 없습니다. 그래서 객체 없이 클래스 이름으로 부를 수 있는 정적 메서드여야 합니다.
여러 테스트가 함께 쓸 db 필드에도 같은 까닭으로 static 이 붙습니다. 객체마다 따로 생기는 필드에 담으면 다음 테스트의 새 객체에서는 그 값이 비어 있습니다. 테스트마다 새로 만드는 cart 는 객체마다 따로 있어도 되므로 static 이 없습니다.
한 클래스를 도는 순서
테스트 메서드가 A 와 B 둘이면 러너는 아래 순서로 부릅니다. 이 그림이 보이려는 것은 횟수의 차이입니다.
flowchart TD
S["클래스 준비 · 한 번"] --> A1
subgraph TA["A 를 돌 때"]
A1["준비 메서드"] --> A2["테스트 메서드 A"] --> A3["정리 메서드"]
end
A3 --> B1
subgraph TB["B 를 돌 때"]
B1["준비 메서드"] --> B2["테스트 메서드 B"] --> B3["정리 메서드"]
end
B3 --> E["클래스 정리 · 한 번"]
준비 메서드와 정리 메서드는 두 번씩 불립니다. 테스트 메서드가 열이면 열 번씩입니다. 클래스 준비와 클래스 정리는 테스트 메서드가 몇 개든 한 번씩입니다.
실패해도 도는 정리 메서드
테스트 메서드가 단언에서 실패하면 그 메서드는 도중에 멈춥니다. 그래도 러너는 정리 메서드를 부릅니다. 테스트 안에 뒷정리 코드를 적어 두었다면 실패한 순간 그 줄까지 가지 못합니다.
정리를 빠뜨리면 흔적이 남습니다. 앞 테스트가 데이터베이스에 넣은 행이 남아 있으면 뒤 테스트는 빈 데이터베이스를 기대했다가 엉뚱한 값을 봅니다. 정리 메서드는 테스트의 성공과 실패에 상관없이 이 흔적을 거둡니다.
테스트마다 도는 준비와 한 번만 도는 준비
준비를 테스트마다 다시 하면 테스트끼리 서로 기대지 않습니다. 앞 테스트가 장바구니에 무엇을 담았든 뒤 테스트는 새 장바구니에서 시작합니다. 테스트끼리 이렇게 영향을 주고받지 않게 떼어 두는 것이 테스트 격리입니다.
대신 준비가 오래 걸리면 그 시간이 테스트 수만큼 쌓입니다. 데이터베이스를 띄우는 데 몇 초가 걸리면 테스트가 백 개일 때 수백 초가 됩니다. 그래서 오래 걸리는 준비는 클래스 준비로 옮겨 한 번만 합니다.
클래스 준비로 만든 것은 모든 테스트가 나눠 씁니다. 한 테스트가 그 값을 고치면 다음 테스트가 고쳐진 값을 봅니다. 테스트를 도는 순서가 바뀌면 결과도 바뀝니다. 이런 상태를 테스트 순서 의존성이라고 부릅니다.
그래서 흔히 둘을 섞어 씁니다. 띄우는 데 오래 걸리는 데이터베이스는 클래스 준비로 한 번 띄웁니다. 테스트가 고치는 장바구니는 만드는 데 순식간이라 준비 메서드로 매번 새로 만듭니다. 위 코드도 그렇게 나뉘어 있습니다.
프레임워크마다 다른 이름
네 때를 주는 것은 테스트 프레임워크마다 같습니다. 이름과 알아보는 방법이 다릅니다. 아래 표의 JUnit 4 는 JUnit 5 의 앞 판입니다. unittest 와 pytest 는 파이썬의 테스트 프레임워크입니다.
| 프레임워크 | 테스트마다 앞 · 뒤 | 클래스에 한 번 앞 · 뒤 |
|---|---|---|
| JUnit 5 | @BeforeEach · @AfterEach |
@BeforeAll · @AfterAll |
| JUnit 4 | @Before · @After |
@BeforeClass · @AfterClass |
| unittest | setUp · tearDown |
setUpClass · tearDownClass |
| pytest | setup_method · teardown_method |
setup_class · teardown_class |
JUnit 은 어노테이션을 보고 알아봅니다. 메서드 이름은 아무렇게나 지어도 됩니다. unittest 와 pytest 는 이름을 보고 알아봅니다. setUp 이라는 이름의 메서드가 있으면 러너가 그것을 준비 메서드로 부릅니다.
pytest 에서는 이 메서드보다 픽스처 함수를 더 흔히 씁니다. 앞에서 본 테스트 픽스처를 갖춰 주는 함수라서 이런 이름이 붙었습니다. 준비와 정리를 함수 하나에 적습니다. 그 준비물이 필요한 테스트가 인자로 받아 씁니다.
import pytest
@pytest.fixture
def cart():
c = Cart() # 준비
yield c # 테스트에 넘김
c.clear() # 정리
def test_total(cart):
cart.add(20000)
assert cart.total() == 20000
yield 앞이 준비입니다. 뒤가 정리입니다. 테스트 함수가 인자 이름을 cart 로 적으면 pytest 가 이름이 같은 픽스처 함수를 찾아 그 값을 넘겨 줍니다. 테스트가 끝나면 yield 뒤로 돌아와 정리를 마칩니다.
부르는 쪽이 프레임워크라는 것
보통은 내가 쓴 코드가 라이브러리를 부릅니다. 생명주기 메서드는 거꾸로입니다. 내가 적어 둔 메서드를 프레임워크가 부릅니다.
제어의 역전은 이처럼 부르는 쪽과 불리는 쪽이 뒤바뀐 구조를 말합니다. 언제 부를지를 프레임워크가 정하므로 개발자는 순서를 잊을 수 없습니다. 정리 메서드가 실패한 테스트 뒤에도 빠짐없이 도는 것도 이 때문입니다.
테스트 밖의 생명주기 메서드
같은 이름은 테스트 밖에서도 흔히 씁니다. 프레임워크가 무언가를 만들고 없애는 일을 맡으면, 그 마디마다 끼어들 메서드를 열어 둡니다.
아래 표에 나오는 세 낱말을 먼저 풉니다. 빈은 스프링이 만들어 관리하는 객체입니다. 액티비티는 안드로이드 앱의 화면 하나입니다. 컴포넌트는 리액트로 만드는 웹 화면의 조각입니다.
| 분야 | 생명주기를 거치는 것 | 메서드 예 |
|---|---|---|
| 스프링 | 빈 | @PostConstruct · @PreDestroy |
| 안드로이드 | 액티비티 | onCreate · onDestroy |
| 리액트 | 컴포넌트 | componentDidMount · componentWillUnmount |
백엔드 개발자가 가장 자주 만나는 것은 스프링입니다. 빈이 만들어진 직후에 연결을 여는 일이나, 빈이 사라지기 직전에 연결을 닫는 일을 이런 메서드에 맡깁니다.
무엇이 생기고 사라지는지는 분야마다 다릅니다. 그래도 구조는 같습니다. 프레임워크가 때를 정합니다. 개발자는 그때 할 일을 적어 둡니다.
관련 항목
생명주기 메서드가 들어 있는 테스트 코드의 구성 요소
테스트 클래스 · 테스트 메서드 · 테스트 케이스 · 테스트 픽스처 · 단언 · 테스트 스위트
생명주기 메서드를 부르는 도구
테스트 러너 · 테스트 프레임워크 · xUnit · JUnit · unittest · pytest · NUnit · TestNG
테스트 생명주기 메서드의 종류
준비 메서드 · 정리 메서드 · 클래스 준비 메서드 · 클래스 정리 메서드 · setUp · tearDown · 픽스처 함수
생명주기 메서드를 잘못 쓸 때 생기는 문제
테스트 순서 의존성 · 공유 픽스처 · 불안정한 테스트 · 테스트 격리 · 깨지기 쉬운 테스트
생명주기 메서드를 받치는 설계 원리
제어의 역전 · 콜백 · 훅 · 템플릿 메서드 패턴 · 프레임워크
생명주기 메서드를 짜는 데 쓰는 언어 기능
어노테이션 · 정적 메서드 · 리플렉션 · 상속 · 데코레이터
테스트 밖에서 생명주기 메서드를 여는 프레임워크
Spring · 스프링 빈 · 빈 생명주기 · Android · 액티비티 생명주기 · React · 컴포넌트 생명주기
생명주기 메서드가 속하는 상위 분류
다른 이름: lifecycle method · 라이프사이클 메서드 · 생명 주기 메서드 · lifecycle hook · 라이프사이클 훅