F-LAB JAVA · 6주차 · Phase 1 · JUnit 테스트
🧪 Part A 시작 — 테스트로 검증하는 코드
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
테스트는 코드가 의도대로 동작하는지 사람이 매번 눈으로 확인하는 대신 코드로 자동 검증하는 것으로, 자동화·격리·빠름·반복 가능이라는 조건을 갖춘 단위 테스트는 변경에 대한 자신감을 주며, 테스트하기 쉬운 코드가 곧 잘 설계된 코드다.
main() 으로 검증하면 출력을 사람이 매번 눈으로 보고 판단 해야 하고, 코드가 바뀔 때마다 모든 기능을 다시 확인해야 하는데 이는 사람이 감당할 수 없다.
단위 테스트 는 코드가 코드를 검증하게 하여, 한 번 작성해두면 버튼 한 번으로 전체 기능을 자동 검증한다.
좋은 단위 테스트의 조건은 (1) 자동화 (사람 개입 없이 실행), (2) 격리 (다른 테스트의 영향 없음), (3) 빠름 (자주 돌릴 수 있음), (4) 반복 가능 (같은 결과 보장) 이다.
그리고 테스트하기 쉬운 코드는 의존성이 분리되고 (5주차 DI), 결합도가 낮아 자연히 잘 설계된 코드이므로, "테스트 가능한 코드 = 좋은 코드" 라는 명제가 성립한다.
단위 테스트 = 자동 검사 라인:
main() 검증 (수동 점검):
- 정비사가 직접 시동 걸고
- 눈으로 계기판 봄
- 차 1대마다 사람이
- 부품 바꾸면 또 전부 점검
→ 느리고 실수
단위 테스트 (자동 검사):
- 검사 기계가 자동 측정
- 합격/불합격 자동 판정
- 1000대도 빠르게
- 부품 바꿔도 버튼 한 번
→ 빠르고 정확
좋은 검사 조건:
- 자동화: 사람 없이
- 격리: 1대 검사가 다음 차 영향 X
- 빠름: 자주
- 반복: 같은 차 같은 결과
검사하기 쉬운 차 = 잘 만든 차:
- 부품 분리 (DI)
- 모듈화
→ 검사 쉬움 = 설계 좋음
→ 단위 테스트 = 코드가 코드를 자동 검증 (자동화/격리/빠름/반복), 테스트 쉬움 = 좋은 설계.
1. 왜 테스트인가
2. main() 검증의 한계
3. 변경 시 검증 부담
4. 코드가 코드를 검증
5. 좋은 단위 테스트 조건
6. 테스트 가능 = 좋은 코드
7. 5주차 DI와 연결
8. 테스트의 자신감
9. 면접 + 자기 점검
테스트 목적:
코드가 의도대로 동작하는지 검증.
- 버그 조기 발견
- 변경 안전성
- 문서 역할
검증의 필요:
코드 작성 후:
- 맞게 동작하나?
- 어떻게 확인?
→ 검증 방법 필요
검증 방법:
1. 수동 (main, 눈으로)
- 직접 실행/확인
2. 자동 (단위 테스트)
- 코드가 검증
// ShipmentDao 가 맞게 동작하나?
// 방법 1: main (수동)
public class ManualCheck {
public static void main(String[] args) {
ShipmentDao dao = new ShipmentDao(...);
dao.add(new Shipment(1L, "BL001", BigDecimal.TEN));
Shipment found = dao.get(1L);
System.out.println(found.getBlNo()); // 눈으로 "BL001" 확인
// 사람이 출력 보고 판단
}
}
// 방법 2: 단위 테스트 (자동) — 다음 섹션들
class ShipmentDao {
ShipmentDao(Object o) {}
void add(Shipment s) {}
Shipment get(Long id) { return null; }
}
record Shipment(Long id, String blNo, java.math.BigDecimal weight) {
String getBlNo() { return blNo; }
}
왜 테스트가 필요한가?
답:
1. 목적:
필요:
방법:
이점:
main() 검증:
- System.out.println
- 출력을 사람이 봄
- 맞는지 판단
→ 사람 개입
한계:
1. 사람이 매번 확인
- 눈으로 판단
- 실수 가능
2. 자동 판정 X
- 통과/실패 자동 X
3. 느림
- 사람 속도
// main 검증 한계
public static void main(String[] args) {
Shipment s = dao.get(1L);
System.out.println(s.getBlNo()); // "BL001" 출력
// → 사람이 "맞네" 판단
// → 자동 아님, 매번 봐야
}
규모의 문제:
기능 10개:
- 10번 출력 확인
기능 100개:
- 100번 확인 (불가)
→ 사람이 못 함
// main 검증의 한계 (ILIC 431 API)
public class ManualVerification {
public static void main(String[] args) {
// ShipmentDao 검증
ShipmentDao shipmentDao = new ShipmentDao();
Shipment s = shipmentDao.get(1L);
System.out.println("배송: " + s); // 눈으로
// BookingDao 검증
BookingDao bookingDao = new BookingDao();
System.out.println("예약: " + bookingDao.get(1L)); // 또 눈으로
// InvoiceDao, FreightCalculator, ... 수십 개
// → 431 API 를 매번 눈으로? 불가능
}
}
class ShipmentDao { Shipment get(Long id) { return null; } }
class BookingDao { Object get(Long id) { return null; } }
class Shipment { }
main() 검증 vs 단위 테스트의 본질적 차이는?
답:
1. main:
한계:
규모:
차이:
변경의 영향:
코드 변경:
- 변경한 부분
- 영향받는 다른 부분
→ 전체 재검증 필요
회귀 (Regression):
변경이 기존 기능 깨뜨림:
- 의도치 않은 부작용
- 멀쩡하던 게 고장
→ 회귀 테스트 필요
수동 재검증:
변경할 때마다:
- 모든 기능 다시 확인
- 사람이 (불가)
→ 변경 두려움
자동 재검증:
단위 테스트:
- 변경 후 전체 실행
- 깨진 곳 자동 발견
→ 변경 자신감
// 변경 시 검증 부담
// FreightCalculator 운임 정책 변경
@Service
public class FreightCalculator {
public BigDecimal calculate(Shipment s) {
// 정책 변경 (예: 할인 추가)
return s.getWeight().multiply(BigDecimal.valueOf(9)); // 10 → 9
}
}
// 수동: 운임 쓰는 모든 곳 재확인 (불가)
// 자동: 테스트 실행 → 영향받는 곳 자동 발견
@Test
void calculate_returns_correct_freight() {
FreightCalculator calc = new FreightCalculator();
BigDecimal result = calc.calculate(new Shipment(BigDecimal.TEN));
assertThat(result, is(BigDecimal.valueOf(90))); // 자동 검증
// 정책 변경 시 이 테스트가 잡아줌
}
class FreightCalculator {
java.math.BigDecimal calculate(Shipment s) { return null; }
}
record Shipment(java.math.BigDecimal weight) {
java.math.BigDecimal getWeight() { return weight; }
}
코드 변경 시 검증 부담 문제는?
답:
1. 변경 영향:
회귀:
수동:
자동:
코드가 코드를 검증:
테스트 코드:
- 대상 코드 실행
- 결과 자동 비교
- 통과/실패 판정
→ 사람 개입 X
// 단언으로 자동 검증
@Test
void test() {
Shipment s = dao.get(1L);
// 자동 비교 (사람 눈 X)
assertThat(s.getBlNo(), is("BL001"));
// 일치 → 통과, 불일치 → 실패
}
통과/실패:
테스트 실행:
- 초록 (통과)
- 빨강 (실패)
→ 자동 판정
일괄 실행:
모든 테스트:
- 버튼 한 번
- 전체 실행
- 결과 종합
→ 100개도 빠르게
// 코드가 코드를 검증 (ILIC)
class ShipmentDaoTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
}
@Test
void add_then_get_returns_same_shipment() {
// 대상 코드 실행
Shipment shipment = new Shipment(1L, "BL001", BigDecimal.TEN);
dao.add(shipment);
Shipment found = dao.get(1L);
// 자동 검증 (코드가)
assertThat(found.getBlNo(), is("BL001")); // 자동
assertThat(found.getWeight(), is(BigDecimal.TEN)); // 자동
// 사람이 눈으로 X
}
@Test
void get_nonexistent_returns_null() {
assertThat(dao.get(999L), is(nullValue())); // 자동
}
// 버튼 한 번 → 모든 테스트 자동 실행
}
class ShipmentDao {
void add(Shipment s) {}
Shipment get(Long id) { return null; }
}
"코드가 코드를 검증" 의 의미는?
답:
1. 자동 검증:
단언:
통과/실패:
일괄:
좋은 단위 테스트:
1. 자동화 (Automated)
2. 격리 (Isolated)
3. 빠름 (Fast)
4. 반복 가능 (Repeatable)
자동화:
사람 개입 없이:
- 실행 자동
- 판정 자동
→ 버튼 한 번
격리:
다른 테스트 영향 X:
- 독립 실행
- 순서 무관
- 상태 공유 X
빠름:
자주 돌릴 속도:
- 밀리초 단위
- 수천 개도 빠르게
→ 자주 실행 (즉시 피드백)
반복 가능:
같은 결과 보장:
- 몇 번 돌려도 같음
- 환경 무관
→ 신뢰
FIRST 원칙:
Fast (빠름)
Isolated (격리)
Repeatable (반복 가능)
Self-validating (자가 검증)
Timely (적시)
→ 좋은 테스트 기준
// 좋은 단위 테스트 (ILIC)
class FreightCalculatorTest {
private FreightCalculator calculator;
@BeforeEach
void setUp() {
// 격리: 매번 새 인스턴스
calculator = new FreightCalculator();
}
@Test
void calculate_sea_freight() {
// 자동화: 자동 실행/판정
Shipment s = new Shipment(BigDecimal.valueOf(100));
BigDecimal result = calculator.calculate(s);
// 자가 검증
assertThat(result, is(BigDecimal.valueOf(1000)));
// 빠름: DB 없이 (밀리초)
// 반복 가능: 항상 같은 결과
}
}
// FIRST:
// - Fast (DB 없음)
// - Isolated (새 인스턴스)
// - Repeatable (외부 의존 X)
// - Self-validating (assertThat)
class FreightCalculator {
java.math.BigDecimal calculate(Shipment s) { return null; }
}
record Shipment(java.math.BigDecimal weight) {}
좋은 단위 테스트의 조건은?
답:
1. 4조건:
자동화:
격리:
FIRST:
테스트 가능 = 좋은 코드:
테스트하기 쉬운 코드:
- 의존성 분리
- 결합도 낮음
- 모듈화
→ 잘 설계된 코드
테스트 어려운 코드:
- 강결합 (의존성 직접 생성)
- 거대한 메서드
- 숨은 의존성
- 정적 상태
→ 설계 나쁨 신호
테스트 쉬운 코드:
- 의존성 주입 (DI)
- 작은 단위
- 명확한 입출력
- 인터페이스
→ 설계 좋음
테스트가 설계 피드백:
"테스트하기 어렵다"
= "설계가 나쁘다" 신호
→ 테스트 작성하며 설계 개선
→ TDD 정신
// 테스트 어려운 코드 (강결합)
public class ShipmentServiceBad {
public void process(Shipment s) {
// 의존성 직접 생성 (테스트 어려움)
ShipmentDao dao = new ShipmentDao(); // 강결합
dao.add(s);
// 테스트 시 실제 DB 필요 (Mock 불가)
}
}
// 테스트 쉬운 코드 (DI)
public class ShipmentServiceGood {
private final ShipmentDao dao;
public ShipmentServiceGood(ShipmentDao dao) {
this.dao = dao; // 주입 (테스트 쉬움)
}
public void process(Shipment s) {
dao.add(s);
}
}
// 테스트 (Mock 주입)
@Test
void process_calls_dao_add() {
ShipmentDao mockDao = mock(ShipmentDao.class);
ShipmentServiceGood service = new ShipmentServiceGood(mockDao);
Shipment s = new Shipment();
service.process(s);
verify(mockDao).add(s); // DB 없이 검증
}
// DI → 테스트 쉬움 → 좋은 설계
class ShipmentDao { void add(Shipment s) {} }
class Shipment { }
테스트 가능한 코드 = 좋은 코드인 이유는?
답:
1. 명제:
어려운 코드:
쉬운 코드:
피드백:
DI ↔ 테스트:
5주차 DI:
- 의존성 외부 주입
테스트:
- Mock 주입 가능
- 격리
→ DI 가 테스트 가능하게
// DI 덕분에 Mock 주입
@Test
void test() {
ShipmentDao mock = mock(ShipmentDao.class); // 가짜
ShipmentService service = new ShipmentService(mock); // 주입
// 실제 DB 없이 테스트
}
// DI 없으면 Mock 주입 불가
격리 테스트:
DI + Mock:
- 대상만 테스트
- 의존성은 가짜
- 외부 영향 X
→ 단위 테스트 격리
5주차 보상:
5주차:
- DI 로 결합도 ↓
6주차:
- 그 덕분에 테스트 쉬움
→ 좋은 설계의 보상
// 5주차 DI → 6주차 테스트
// 5주차: 생성자 주입 (DI)
@Service
public class ShipmentService {
private final ShipmentDao shipmentDao;
private final FreightCalculator calculator;
public ShipmentService(ShipmentDao dao, FreightCalculator calc) {
this.shipmentDao = dao; // DI
this.calculator = calc; // DI
}
public void createBooking(Shipment s) {
BigDecimal freight = calculator.calculate(s);
s.setFreight(freight);
shipmentDao.add(s);
}
}
// 6주차: DI 덕분에 테스트 쉬움
@Test
void createBooking_calculates_and_saves() {
// Mock 주입 (DI 덕분)
ShipmentDao mockDao = mock(ShipmentDao.class);
FreightCalculator mockCalc = mock(FreightCalculator.class);
when(mockCalc.calculate(any())).thenReturn(BigDecimal.valueOf(1000));
ShipmentService service = new ShipmentService(mockDao, mockCalc);
Shipment s = new Shipment();
service.createBooking(s);
verify(mockDao).add(s); // 검증 (DB 없이)
}
// 5주차 DI 가 6주차 테스트를 가능하게
class ShipmentDao { void add(Shipment s) {} }
class FreightCalculator { java.math.BigDecimal calculate(Shipment s) { return null; } }
class Shipment { void setFreight(java.math.BigDecimal f) {} }
5주차 DI와 테스트의 연결은?
답:
1. DI ↔ 테스트:
Mock 주입:
격리:
보상:
변경 자신감:
테스트 있으면:
- 변경 후 실행
- 깨진 곳 발견
- 안심하고 변경
→ 리팩토링 자신감
리팩토링 안전망:
테스트 = 안전망:
- 리팩토링 중 깨지면
- 즉시 발견
→ 적극 개선 가능
문서 역할:
테스트 = 사용 예:
- 코드 사용법 보여줌
- 의도 명시
- 살아있는 문서
→ 코드 이해 도움
테스트 없는 위험:
- 변경 두려움
- 회귀 버그
- 개선 안 함 (악순환)
- 품질 저하
→ 기술 부채
// 테스트의 자신감 (ILIC)
// 테스트가 있으면:
class ShipmentServiceTest {
@Test void createBooking_works() { /* ... */ }
@Test void cancelBooking_works() { /* ... */ }
@Test void freight_calculated_correctly() { /* ... */ }
// ... 수십 개 테스트
}
// 운임 정책 리팩토링 시:
// 1. 코드 변경
// 2. 테스트 실행 (버튼 한 번)
// 3. 모두 초록 → 안심
// 4. 빨강 → 즉시 발견·수정
// → 431 API 를 자신 있게 변경
// → 테스트 = 안전망 + 문서
테스트가 주는 자신감은?
답:
1. 변경 자신감:
리팩토링 안전망:
문서:
없으면:
| Q | 핵심 답변 |
|---|---|
| 왜 테스트? | 자동 검증, 변경 안전 |
| main vs 테스트? | 사람 vs 코드 |
| 변경 부담? | 회귀, 재검증 |
| 코드가 검증? | 자동 단언 |
| 좋은 테스트? | 자동화/격리/빠름/반복 |
| 테스트 = 좋은 코드? | 테스트 쉬움 = 설계 좋음 |
| DI 연결? | Mock 주입 |
| 자신감? | 리팩토링 안전망 |
| FIRST? | 테스트 원칙 |
| 없으면? | 변경 두려움 |
답:
답:
답:
답:
답:
1. 왜 테스트
2. 좋은 단위 테스트
3. 5주차 연결
이번 Unit에서 테스트의 필요성을 봤다면, 다음은 assertThat과 매처.
🧪 Phase 1 — JUnit 테스트
✅ Unit 1.1 단위 테스트의 필요성 ← 여기
⏭ Unit 1.2 assertThat과 매처
⏭ Unit 1.3 JUnit 실행 방식
⏭ Unit 1.4 매번 새 오브젝트
⏭ Unit 1.5 픽스처와 @BeforeEach
🧪 Part A — 학습 도구와 환경
Phase 1 — JUnit 테스트 (1/5 진행)
총: 1/28 Unit
🧪 Part A 시작 — 테스트로 검증하는 코드