F-LAB JAVA · 6주차 · Phase 1 · JUnit 테스트
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
JUnit 이 매 테스트마다 새 오브젝트를 만드는 이유는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장하기 위해서이며, 덕분에 인스턴스 변수를 부담 없이 쓸 수 있지만 비싼 자원을 매번 만들면 느려지므로 공유 자원은 @BeforeAll 로 처리한다.
매번 새 오브젝트를 만드는 핵심 이유는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장 하기 위해서다.
이로써 얻는 이점은 (1) 인스턴스 변수를 부담 없이 사용 가능 (어차피 다음 테스트에서 초기화됨), (2) 테스트 간 의존성 제거, (3) 실행 순서 무관 이다.
다만 인스턴스 변수에 비싼 자원 (예: DB Connection) 을 만들면 매번 생성 되어 테스트가 느려지는 단점이 있다.
따라서 정말 공유가 필요하고 생성 비용이 큰 자원은 @BeforeAll (전체 1회, static) 로 처리하되, 이는 격리를 일부 포기하는 트레이드오프이므로 상태 없는 자원에만 신중히 적용한다.
매번 새 오브젝트 = 일회용 식기:
매번 새 오브젝트 (일회용):
- 손님마다 새 접시 (인스턴스)
- 이전 음식 안 묻음 (격리)
- 독립적
→ 위생 보장 (간섭 X)
부담 없이 사용:
- 접시에 뭘 담든
- 다음 손님 새 접시
- 안 씻어도 됨 (버림)
비싼 자원 매번 (낭비):
- 손님마다 새 오븐 설치?
- 비효율 (DB Connection)
공유 자원 (@BeforeAll):
- 오븐은 1대 공유 (주방 세팅)
- 한 번 설치
- 단, 오븐에 상태 남으면 위험
→ 매번 새 오브젝트 = 독립 실행 보장 (일회용), 비싼 자원은 @BeforeAll (공유, 신중).
1. 핵심 이유 (독립 실행)
2. 독립적 실행 보장
3. 부담 없는 인스턴스 변수
4. 의존성 제거
5. 실행 순서 무관
6. 비싼 자원의 문제
7. 공유 자원 처리 (@BeforeAll)
8. 트레이드오프
9. 면접 + 자기 점검
매번 새 오브젝트 이유:
"각 테스트가 서로 영향을 주지 않고
독립적으로 실행됨을 확실히 보장"
→ 독립 실행 보장
영향 차단:
공유 인스턴스라면:
- 한 테스트 상태가
- 다음에 영향
새 인스턴스:
- 영향 차단
확실히 보장:
"확실히" 가 중요:
- 실수로 공유 X
- 강제 격리
- 프레임워크 보장
→ 개발자 실수 방지
class ShipmentDaoTest {
private ShipmentDao dao;
private List<Shipment> testData; // 인스턴스 변수
@BeforeEach
void setUp() {
dao = new ShipmentDao(); // 매번 새 dao
testData = new ArrayList<>(); // 매번 새 리스트
}
@Test
void test_a() {
testData.add(new Shipment(1L));
dao.add(testData.get(0));
// test_a 의 상태
}
@Test
void test_b() {
// test_a 영향 X (새 인스턴스)
assertThat(testData, is(empty())); // 빈 상태
// 독립 실행 보장
}
}
class ShipmentDao { void add(Shipment s) {} }
record Shipment(Long id) {}
매번 새 오브젝트를 만드는 핵심 이유는?
답:
1. 핵심 이유:
영향 차단:
확실히:
목적:
독립 실행:
각 테스트:
- 혼자 실행 가능
- 다른 테스트 불필요
- 자기 완결
자기 완결:
한 테스트:
- 준비 (BeforeEach)
- 실행
- 검증
- 정리
→ 스스로 완결
격리 수준:
인스턴스 상태:
- 자동 격리 (새 인스턴스)
외부 상태 (DB):
- 수동 격리 (deleteAll)
class IndependentTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll(); // 외부 상태도 독립
}
// 각 테스트가 자기 완결 (독립)
@Test
void test_create() {
// 준비
Shipment s = new Shipment(1L);
// 실행
dao.add(s);
// 검증
assertThat(dao.count(), is(1));
// → 혼자 실행 가능
}
@Test
void test_delete() {
// 자기만의 준비 (test_create 불필요)
dao.add(new Shipment(2L));
dao.delete(2L);
assertThat(dao.count(), is(0));
// → 독립
}
}
class ShipmentDao {
void add(Shipment s) {}
void delete(Long id) {}
void deleteAll() {}
int count() { return 0; }
}
record Shipment(Long id) {}
"독립적 실행 보장" 의 의미는?
답:
1. 독립 실행:
자기 완결:
격리 수준:
목적:
부담 없는 인스턴스 변수:
인스턴스 변수에:
- 자유롭게 데이터
- 다음 테스트 초기화
- 누적 걱정 X
→ 편하게 사용
왜 부담 없나:
매번 새 인스턴스:
- 인스턴스 변수 새로
- 이전 잔여 X
→ 정리 불필요
// 인스턴스 변수 = 픽스처 (부담 없이)
class DaoTest {
private ShipmentDao dao; // 픽스처
private Shipment testShipment; // 픽스처
@BeforeEach
void setUp() {
dao = new ShipmentDao();
testShipment = new Shipment(1L, "BL001");
}
@Test
void test() {
dao.add(testShipment); // 부담 없이 사용
// 다음 테스트는 새 testShipment
}
}
인스턴스 변수 vs 지역 변수:
지역 변수:
- 메서드 내
- 매번 선언
인스턴스 변수 (픽스처):
- @BeforeEach 한 곳
- 여러 테스트 공통
- 중복 제거
class FreightServiceTest {
// 인스턴스 변수 (부담 없이 = 픽스처)
private FreightService service;
private ShipmentDao mockDao;
private Shipment sampleShipment;
@BeforeEach
void setUp() {
// 매번 새로 (부담 없음)
mockDao = mock(ShipmentDao.class);
service = new FreightService(mockDao);
sampleShipment = new Shipment(1L, BigDecimal.valueOf(100));
}
@Test
void calculate_test() {
BigDecimal result = service.calculate(sampleShipment);
assertThat(result, is(greaterThan(BigDecimal.ZERO)));
// sampleShipment 자유롭게 사용
}
@Test
void another_test() {
// sampleShipment 는 새것 (이전 영향 X)
service.calculate(sampleShipment);
}
}
class FreightService {
FreightService(ShipmentDao d) {}
java.math.BigDecimal calculate(Shipment s) { return null; }
}
class ShipmentDao {}
record Shipment(Long id, java.math.BigDecimal weight) {}
인스턴스 변수를 부담 없이 사용할 수 있는 이유는?
답:
1. 부담 없음:
왜:
픽스처:
이점:
테스트 간 의존성:
나쁜 경우:
- test_b 가 test_a 결과 의존
- test_a 실행 후만 통과
→ 의존성 (제거 대상)
의존성 제거:
새 인스턴스:
- 각 테스트 독립
- 다른 테스트 결과 무관
→ 의존성 차단
// ❌ 의존 (안티패턴)
class BadTest {
static List<Shipment> shared = new ArrayList<>(); // 공유 (static)
@Test
void test1_add() {
shared.add(new Shipment(1L)); // test1 이 추가
}
@Test
void test2_check() {
assertThat(shared, hasSize(1)); // test1 의존 (나쁨)
// test1 안 돌면 실패
}
}
// ✓ 독립 (각자 준비)
class GoodTest {
private List<Shipment> data;
@BeforeEach
void setUp() {
data = new ArrayList<>(); // 매번 새로
}
@Test
void test1() {
data.add(new Shipment(1L));
assertThat(data, hasSize(1));
}
@Test
void test2() {
// 자기 준비 (test1 무관)
data.add(new Shipment(2L));
assertThat(data, hasSize(1));
}
}
record Shipment(Long id) {}
class DependencyFreeTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll(); // 독립 보장
}
// 각 테스트 독립 (의존성 X)
@Test
void create_shipment() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1));
// 다른 테스트 결과에 의존 X
}
@Test
void update_shipment() {
// create_shipment 의존 X (자기 준비)
dao.add(new Shipment(2L));
dao.updateStatus(2L, "SHIPPED");
assertThat(dao.get(2L).getStatus(), is("SHIPPED"));
}
// 순서 바꿔도, 하나만 돌려도 OK
}
class ShipmentDao {
void add(Shipment s) {}
void deleteAll() {}
void updateStatus(Long id, String s) {}
Shipment get(Long id) { return null; }
int count() { return 0; }
}
class Shipment {
Shipment(Long id) {}
String getStatus() { return null; }
}
테스트 간 의존성 제거는?
답:
1. 의존성:
제거:
안티패턴:
독립 설계:
실행 순서 무관:
테스트 순서:
- 어떤 순서든 OK
- 결과 동일
→ 순서 보장 안 해도 됨
왜 가능:
독립 + 새 인스턴스:
- 각 테스트 격리
- 순서 영향 X
→ 순서 무관
순서 의존 위험:
순서 의존하면:
- 환경 따라 순서 다름
- 가끔 실패 (flaky)
- 디버깅 어려움
→ 순서 무관 설계
병렬 실행 가능:
순서 무관 + 독립:
- 병렬 실행 OK
- 속도 ↑
JUnit 5 병렬 지원
class OrderIndependentTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll();
}
// 순서 무관 (어떤 순서든 통과)
@Test
void test_alpha() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1));
}
@Test
void test_beta() {
dao.add(new Shipment(2L));
dao.add(new Shipment(3L));
assertThat(dao.count(), is(2));
}
@Test
void test_gamma() {
assertThat(dao.count(), is(0)); // 빈 시작
}
// alpha → beta → gamma 든
// gamma → alpha → beta 든
// 모두 통과 (순서 무관)
// → 병렬 실행도 가능
}
class ShipmentDao {
void add(Shipment s) {}
void deleteAll() {}
int count() { return 0; }
}
record Shipment(Long id) {}
실행 순서 무관의 의미는?
답:
1. 순서 무관:
왜:
위험:
이점:
비싼 자원:
생성 비용 큰 것:
- DB Connection
- 네트워크 연결
- 무거운 객체
→ 인스턴스 변수면 매번 생성
매번 생성 문제:
@BeforeEach 에 비싼 자원:
- 테스트마다 생성
- 100 테스트 = 100번
- 느림
→ 테스트 속도 ↓
// ❌ 비싼 자원 매번 (느림)
class SlowTest {
private Connection connection;
@BeforeEach
void setUp() throws Exception {
// 매 테스트마다 DB 연결 (비쌈)
connection = DriverManager.getConnection(url, user, pwd);
// 100 테스트 = 100번 연결 (느림)
}
}
속도 영향:
비싼 자원 × 테스트 수:
- 누적 비용
- 전체 테스트 느림
- 자주 못 돌림
→ 빠름 (FIRST) 위반
// 비싼 자원 문제
class ExpensiveResourceTest {
private DataSource dataSource;
private ShipmentDao dao;
// ❌ 매번 DataSource (비쌈)
@BeforeEach
void setUpBad() {
dataSource = createHikariDataSource(); // 풀 생성 (비쌈)
dao = new ShipmentDao(dataSource);
// 매 테스트마다 풀 생성 → 느림
}
// ✓ DataSource 공유, dao 만 매번 (다음 섹션)
private DataSource createHikariDataSource() { return null; }
}
class ShipmentDao { ShipmentDao(DataSource ds) {} }
interface DataSource {}
인스턴스 변수에 비싼 자원을 만들면?
답:
1. 비싼 자원:
매번 생성:
문제:
위반:
// 공유 자원 — @BeforeAll
class SharedResourceTest {
private static DataSource dataSource; // 공유 (static)
private ShipmentDao dao;
@BeforeAll
static void setUpClass() {
// 한 번만 (비싼 자원)
dataSource = createDataSource();
}
@BeforeEach
void setUp() {
// 매번 (가벼운 것 + 상태 초기화)
dao = new ShipmentDao(dataSource);
dao.deleteAll();
}
}
분리 전략:
@BeforeAll (한 번):
- 비싼 자원
- DataSource, 컨테이너
@BeforeEach (매번):
- 가벼운 픽스처
- 상태 초기화
상태 초기화 병행:
@BeforeAll 로 공유 자원:
- 하지만 상태는?
@BeforeEach 로 초기화:
- deleteAll
- 격리 유지
→ 자원 공유 + 상태 격리
Testcontainers:
실제 DB 컨테이너:
- @BeforeAll 로 시작
- 모든 테스트 공유
- 현실적 통합 테스트
→ 비싼 자원 공유 사례
class ShipmentDaoIntegrationTest {
private static DataSource dataSource; // 공유 (비쌈)
private ShipmentDao dao;
@BeforeAll
static void initDataSource() {
// 비싼 자원 한 번만 (풀 생성)
dataSource = createHikariDataSource();
}
@BeforeEach
void setUp() {
// 가벼운 것 매번 + 상태 격리
dao = new ShipmentDao(dataSource);
dao.deleteAll(); // 깨끗한 시작 (격리)
}
@Test
void test_add() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1));
}
@Test
void test_independent() {
// DataSource 는 공유, 상태는 격리 (deleteAll)
assertThat(dao.count(), is(0));
}
@AfterAll
static void closeDataSource() {
// 자원 해제 (한 번만)
}
static DataSource createHikariDataSource() { return null; }
}
class ShipmentDao {
ShipmentDao(DataSource ds) {}
void add(Shipment s) {}
void deleteAll() {}
int count() { return 0; }
}
interface DataSource {}
record Shipment(Long id) {}
공유가 필요한 자원은 어떻게 처리하나?
답:
1. @BeforeAll:
분리:
상태 초기화:
예:
격리 vs 속도:
매번 새 (격리):
+ 완전 격리
- 비싼 자원 느림
공유 (@BeforeAll):
+ 빠름
- 격리 일부 포기
공유의 위험:
@BeforeAll 자원에 상태:
- 테스트 간 영향
- 격리 깨짐
→ 상태 없는 자원만 공유
신중한 적용:
공유 가능:
- 상태 없음 (DataSource)
- 읽기 전용
공유 위험:
- 변경 가능 상태
- → @BeforeEach 초기화
균형:
기본: @BeforeEach (격리)
예외: @BeforeAll (비싼 + 상태 없음)
+ 상태는 @BeforeEach 초기화
→ 격리 + 속도 균형
// 트레이드오프 균형 (ILIC)
class BalancedTest {
// 공유 (상태 없는 비싼 자원)
private static DataSource dataSource; // OK 공유
private static FreightRateTable rateTable; // OK 공유 (불변)
// 매번 (상태 있는 것)
private ShipmentDao dao;
@BeforeAll
static void initShared() {
dataSource = createDataSource(); // 비쌈, 상태 없음
rateTable = loadFreightRates(); // 불변 (읽기 전용)
}
@BeforeEach
void setUp() {
dao = new ShipmentDao(dataSource); // 가벼움
dao.deleteAll(); // 상태 격리
}
// 균형:
// - DataSource/rateTable: 공유 (빠름, 상태 없음)
// - DB 데이터: 매번 초기화 (격리)
static DataSource createDataSource() { return null; }
static FreightRateTable loadFreightRates() { return null; }
}
class ShipmentDao {
ShipmentDao(DataSource ds) {}
void deleteAll() {}
}
interface DataSource {}
class FreightRateTable {}
@BeforeAll의 트레이드오프는?
답:
1. 격리 vs 속도:
공유 위험:
신중:
균형:
| Q | 핵심 답변 |
|---|---|
| 매번 새 이유? | 독립 실행 보장 |
| 독립 실행? | 영향 X, 자기 완결 |
| 부담 없는 변수? | 다음 초기화 |
| 의존성 제거? | 각 독립 |
| 순서 무관? | 격리, 병렬 가능 |
| 비싼 자원? | 매번 생성 (느림) |
| 공유 자원? | @BeforeAll |
| 트레이드오프? | 격리 vs 속도 |
| 공유 위험? | 상태 영향 |
| 균형? | All(비싼)+Each(초기화) |
답:
답:
답:
답:
답:
1. 매번 새 오브젝트 이유
2. 비싼 자원의 문제
3. 공유 자원 처리
이번 Unit에서 새 오브젝트 이유를 봤다면, 다음은 픽스처 (Phase 1 마지막).
🧪 Phase 1 — JUnit 테스트
✅ Unit 1.1 단위 테스트의 필요성
✅ Unit 1.2 assertThat과 매처
✅ Unit 1.3 JUnit 실행 방식
✅ Unit 1.4 매번 새 오브젝트 ← 여기
⏭ Unit 1.5 픽스처와 @BeforeEach — Phase 1 완주
🧪 Part A — 학습 도구와 환경
Phase 1 — JUnit 테스트 (4/5 진행)
총: 4/28 Unit