6주차 Unit 1.1 — 단위 테스트의 필요성

Psj·2026년 5월 27일

F-lab

목록 보기
182/238

Unit 1.1 — 단위 테스트의 필요성

F-LAB JAVA · 6주차 · Phase 1 · JUnit 테스트
🧪 Part A 시작 — 테스트로 검증하는 코드


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 왜 테스트가 필요한가 ?
  • main() 검증 vs 단위 테스트 의 본질적 차이는?
  • 코드 변경 시 검증 부담 문제는?
  • "코드가 코드를 검증" 의 의미는?
  • 좋은 단위 테스트의 조건 (자동화/격리/빠름/반복) 은?
  • 테스트 가능한 코드 = 좋은 코드 인 이유는?
  • 5주차 DI 와 테스트 의 연결은?
  • 테스트가 주는 자신감 은?
  • 테스트 없는 코드의 위험 은?

🎯 핵심 한 문장

테스트는 코드가 의도대로 동작하는지 사람이 매번 눈으로 확인하는 대신 코드로 자동 검증하는 것으로, 자동화·격리·빠름·반복 가능이라는 조건을 갖춘 단위 테스트는 변경에 대한 자신감을 주며, 테스트하기 쉬운 코드가 곧 잘 설계된 코드다.
main() 으로 검증하면 출력을 사람이 매번 눈으로 보고 판단 해야 하고, 코드가 바뀔 때마다 모든 기능을 다시 확인해야 하는데 이는 사람이 감당할 수 없다.
단위 테스트 는 코드가 코드를 검증하게 하여, 한 번 작성해두면 버튼 한 번으로 전체 기능을 자동 검증한다.
좋은 단위 테스트의 조건은 (1) 자동화 (사람 개입 없이 실행), (2) 격리 (다른 테스트의 영향 없음), (3) 빠름 (자주 돌릴 수 있음), (4) 반복 가능 (같은 결과 보장) 이다.
그리고 테스트하기 쉬운 코드는 의존성이 분리되고 (5주차 DI), 결합도가 낮아 자연히 잘 설계된 코드이므로, "테스트 가능한 코드 = 좋은 코드" 라는 명제가 성립한다.

비유 — 자동차 출고 전 검사

단위 테스트 = 자동 검사 라인:

main() 검증 (수동 점검):
  - 정비사가 직접 시동 걸고
  - 눈으로 계기판 봄
  - 차 1대마다 사람이
  - 부품 바꾸면 또 전부 점검
  → 느리고 실수

단위 테스트 (자동 검사):
  - 검사 기계가 자동 측정
  - 합격/불합격 자동 판정
  - 1000대도 빠르게
  - 부품 바꿔도 버튼 한 번
  → 빠르고 정확

좋은 검사 조건:
  - 자동화: 사람 없이
  - 격리: 1대 검사가 다음 차 영향 X
  - 빠름: 자주
  - 반복: 같은 차 같은 결과

검사하기 쉬운 차 = 잘 만든 차:
  - 부품 분리 (DI)
  - 모듈화
  → 검사 쉬움 = 설계 좋음

→ 단위 테스트 = 코드가 코드를 자동 검증 (자동화/격리/빠름/반복), 테스트 쉬움 = 좋은 설계.


🧭 9개 섹션 로드맵

1. 왜 테스트인가
2. main() 검증의 한계
3. 변경 시 검증 부담
4. 코드가 코드를 검증
5. 좋은 단위 테스트 조건
6. 테스트 가능 = 좋은 코드
7. 5주차 DI와 연결
8. 테스트의 자신감
9. 면접 + 자기 점검

1️⃣ 왜 테스트인가

1.1 테스트의 목적

테스트 목적:

  코드가 의도대로 동작하는지 검증.
  - 버그 조기 발견
  - 변경 안전성
  - 문서 역할

1.2 검증의 필요

검증의 필요:

  코드 작성 후:
    - 맞게 동작하나?
    - 어떻게 확인?

  → 검증 방법 필요

1.3 두 가지 방법

검증 방법:

1. 수동 (main, 눈으로)
   - 직접 실행/확인

2. 자동 (단위 테스트)
   - 코드가 검증

1.4 ILIC 의 맥락

// 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.5 자기 점검 답변

왜 테스트가 필요한가?

:
1. 목적:

  • 의도대로 동작 검증
  1. 필요:

    • 맞게 동작하나 확인
  2. 방법:

    • 수동 vs 자동
  3. 이점:

    • 버그 발견, 변경 안전

2️⃣ main() 검증의 한계

2.1 수동 확인

main() 검증:

  - System.out.println
  - 출력을 사람이 봄
  - 맞는지 판단

→ 사람 개입

2.2 한계

한계:

1. 사람이 매번 확인
   - 눈으로 판단
   - 실수 가능

2. 자동 판정 X
   - 통과/실패 자동 X

3. 느림
   - 사람 속도

2.3 예시

// main 검증 한계
public static void main(String[] args) {
    Shipment s = dao.get(1L);
    System.out.println(s.getBlNo());   // "BL001" 출력
    // → 사람이 "맞네" 판단
    // → 자동 아님, 매번 봐야
}

2.4 규모의 문제

규모의 문제:

  기능 10개:
    - 10번 출력 확인

  기능 100개:
    - 100번 확인 (불가)

→ 사람이 못 함

2.5 ILIC 의 맥락

// 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 { }

2.6 자기 점검 답변

main() 검증 vs 단위 테스트의 본질적 차이는?

:
1. main:

  • 사람이 출력 봄
  1. 한계:

    • 자동 판정 X
    • 실수, 느림
  2. 규모:

    • 많으면 불가
  3. 차이:

    • 사람 vs 코드

3️⃣ 변경 시 검증 부담

3.1 변경의 영향

변경의 영향:

  코드 변경:
    - 변경한 부분
    - 영향받는 다른 부분

  → 전체 재검증 필요

3.2 회귀 (Regression)

회귀 (Regression):

  변경이 기존 기능 깨뜨림:
    - 의도치 않은 부작용
    - 멀쩡하던 게 고장

  → 회귀 테스트 필요

3.3 수동 재검증

수동 재검증:

  변경할 때마다:
    - 모든 기능 다시 확인
    - 사람이 (불가)

  → 변경 두려움

3.4 자동 재검증

자동 재검증:

  단위 테스트:
    - 변경 후 전체 실행
    - 깨진 곳 자동 발견

  → 변경 자신감

3.5 ILIC 의 맥락

// 변경 시 검증 부담

// 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; }
}

3.6 자기 점검 답변

코드 변경 시 검증 부담 문제는?

:
1. 변경 영향:

  • 다른 부분도
  1. 회귀:

    • 기존 깨짐
  2. 수동:

    • 전체 재확인 (불가)
  3. 자동:

    • 테스트 실행

4️⃣ 코드가 코드를 검증

4.1 자동 검증

코드가 코드를 검증:

  테스트 코드:
    - 대상 코드 실행
    - 결과 자동 비교
    - 통과/실패 판정

→ 사람 개입 X

4.2 단언 (Assertion)

// 단언으로 자동 검증
@Test
void test() {
    Shipment s = dao.get(1L);
    
    // 자동 비교 (사람 눈 X)
    assertThat(s.getBlNo(), is("BL001"));
    // 일치 → 통과, 불일치 → 실패
}

4.3 통과/실패

통과/실패:

  테스트 실행:
    - 초록 (통과)
    - 빨강 (실패)

  → 자동 판정

4.4 일괄 실행

일괄 실행:

  모든 테스트:
    - 버튼 한 번
    - 전체 실행
    - 결과 종합

→ 100개도 빠르게

4.5 ILIC 의 맥락

// 코드가 코드를 검증 (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; }
}

4.6 자기 점검 답변

"코드가 코드를 검증" 의 의미는?

:
1. 자동 검증:

  • 테스트가 대상 실행
  • 결과 비교
  1. 단언:

    • assertThat
  2. 통과/실패:

    • 자동 판정
  3. 일괄:

    • 버튼 한 번

5️⃣ 좋은 단위 테스트 조건

5.1 네 가지 조건

좋은 단위 테스트:

1. 자동화 (Automated)
2. 격리 (Isolated)
3. 빠름 (Fast)
4. 반복 가능 (Repeatable)

5.2 자동화

자동화:

  사람 개입 없이:
    - 실행 자동
    - 판정 자동

  → 버튼 한 번

5.3 격리

격리:

  다른 테스트 영향 X:
    - 독립 실행
    - 순서 무관
    - 상태 공유 X

5.4 빠름

빠름:

  자주 돌릴 속도:
    - 밀리초 단위
    - 수천 개도 빠르게

  → 자주 실행 (즉시 피드백)

5.5 반복 가능

반복 가능:

  같은 결과 보장:
    - 몇 번 돌려도 같음
    - 환경 무관

  → 신뢰

5.6 FIRST 원칙

FIRST 원칙:

  Fast (빠름)
  Isolated (격리)
  Repeatable (반복 가능)
  Self-validating (자가 검증)
  Timely (적시)

→ 좋은 테스트 기준

5.7 ILIC 의 맥락

// 좋은 단위 테스트 (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) {}

5.8 자기 점검 답변

좋은 단위 테스트의 조건은?

:
1. 4조건:

  • 자동화/격리/빠름/반복
  1. 자동화:

    • 사람 없이
  2. 격리:

    • 독립
  3. FIRST:

    • 원칙

6️⃣ 테스트 가능 = 좋은 코드

6.1 명제

테스트 가능 = 좋은 코드:

  테스트하기 쉬운 코드:
    - 의존성 분리
    - 결합도 낮음
    - 모듈화

  → 잘 설계된 코드

6.2 테스트 어려운 코드

테스트 어려운 코드:

  - 강결합 (의존성 직접 생성)
  - 거대한 메서드
  - 숨은 의존성
  - 정적 상태

→ 설계 나쁨 신호

6.3 테스트 쉬운 코드

테스트 쉬운 코드:

  - 의존성 주입 (DI)
  - 작은 단위
  - 명확한 입출력
  - 인터페이스

→ 설계 좋음

6.4 테스트가 설계 피드백

테스트가 설계 피드백:

  "테스트하기 어렵다"
    = "설계가 나쁘다" 신호

  → 테스트 작성하며 설계 개선
  → TDD 정신

6.5 ILIC 의 맥락

// 테스트 어려운 코드 (강결합)
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 { }

6.6 자기 점검 답변

테스트 가능한 코드 = 좋은 코드인 이유는?

:
1. 명제:

  • 테스트 쉬움 = 설계 좋음
  1. 어려운 코드:

    • 강결합, 거대
  2. 쉬운 코드:

    • DI, 작은 단위
  3. 피드백:

    • 테스트가 설계 알려줌

7️⃣ 5주차 DI와 연결

7.1 DI 와 테스트

DI ↔ 테스트:

  5주차 DI:
    - 의존성 외부 주입

  테스트:
    - Mock 주입 가능
    - 격리

→ DI 가 테스트 가능하게

7.2 Mock 주입

// DI 덕분에 Mock 주입
@Test
void test() {
    ShipmentDao mock = mock(ShipmentDao.class);   // 가짜
    ShipmentService service = new ShipmentService(mock);   // 주입
    // 실제 DB 없이 테스트
}
// DI 없으면 Mock 주입 불가

7.3 격리 테스트

격리 테스트:

  DI + Mock:
    - 대상만 테스트
    - 의존성은 가짜
    - 외부 영향 X

→ 단위 테스트 격리

7.4 5주차 보상

5주차 보상:

  5주차:
    - DI 로 결합도 ↓

  6주차:
    - 그 덕분에 테스트 쉬움

→ 좋은 설계의 보상

7.5 ILIC 의 맥락

// 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) {} }

7.6 자기 점검 답변

5주차 DI와 테스트의 연결은?

:
1. DI ↔ 테스트:

  • DI 가 테스트 가능하게
  1. Mock 주입:

    • DI 덕분
  2. 격리:

    • 의존성 가짜
  3. 보상:

    • 좋은 설계 → 테스트 쉬움

8️⃣ 테스트의 자신감

8.1 변경 자신감

변경 자신감:

  테스트 있으면:
    - 변경 후 실행
    - 깨진 곳 발견
    - 안심하고 변경

→ 리팩토링 자신감

8.2 리팩토링 안전망

리팩토링 안전망:

  테스트 = 안전망:
    - 리팩토링 중 깨지면
    - 즉시 발견

  → 적극 개선 가능

8.3 문서 역할

문서 역할:

  테스트 = 사용 예:
    - 코드 사용법 보여줌
    - 의도 명시
    - 살아있는 문서

→ 코드 이해 도움

8.4 테스트 없는 위험

테스트 없는 위험:

  - 변경 두려움
  - 회귀 버그
  - 개선 안 함 (악순환)
  - 품질 저하

→ 기술 부채

8.5 ILIC 의 맥락

// 테스트의 자신감 (ILIC)

// 테스트가 있으면:
class ShipmentServiceTest {
    @Test void createBooking_works() { /* ... */ }
    @Test void cancelBooking_works() { /* ... */ }
    @Test void freight_calculated_correctly() { /* ... */ }
    // ... 수십 개 테스트
}

// 운임 정책 리팩토링 시:
// 1. 코드 변경
// 2. 테스트 실행 (버튼 한 번)
// 3. 모두 초록 → 안심
// 4. 빨강 → 즉시 발견·수정

// → 431 API 를 자신 있게 변경
// → 테스트 = 안전망 + 문서

8.6 자기 점검 답변

테스트가 주는 자신감은?

:
1. 변경 자신감:

  • 깨진 곳 발견
  1. 리팩토링 안전망:

    • 즉시 발견
  2. 문서:

    • 사용 예
  3. 없으면:

    • 변경 두려움

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
왜 테스트?자동 검증, 변경 안전
main vs 테스트?사람 vs 코드
변경 부담?회귀, 재검증
코드가 검증?자동 단언
좋은 테스트?자동화/격리/빠름/반복
테스트 = 좋은 코드?테스트 쉬움 = 설계 좋음
DI 연결?Mock 주입
자신감?리팩토링 안전망
FIRST?테스트 원칙
없으면?변경 두려움

9.2 자기 점검 체크리스트

왜 테스트

  • 자동 검증

main 한계

  • 사람 개입

변경 부담

  • 회귀

코드가 검증

  • 단언

좋은 테스트

  • 4조건

테스트 = 좋은 코드

  • 명제

DI 연결

  • Mock

자신감

  • 안전망

9.3 추가 심화 질문

Q1: TDD 란?

답:

  • Test-Driven Development
  • 테스트 먼저 작성
  • Red-Green-Refactor
  • 설계 개선 효과

Q2: 단위 vs 통합 테스트?

답:

  • 단위: 한 클래스 (격리, Mock)
  • 통합: 여러 컴포넌트 (DB 등)
  • 단위 빠름, 통합 현실적
  • 둘 다 필요

Q3: 테스트 커버리지?

답:

  • 코드 실행 비율
  • 높다고 좋은 건 X
  • 의미 있는 테스트 중요
  • 핵심 로직 우선

Q4: Mock vs Stub?

답:

  • Stub: 정해진 값 반환
  • Mock: 호출 검증
  • 둘 다 테스트 더블
  • 목적 다름

Q5: 테스트 피라미드?

답:

  • 단위 (많이, 빠름)
  • 통합 (중간)
  • E2E (적게, 느림)
  • 아래가 넓은 피라미드

🎯 핵심 요약 — 3줄 정리

1. 왜 테스트

  • main() 은 사람이 매번 눈으로 확인 (규모/변경에 한계)
  • 단위 테스트 = 코드가 코드를 자동 검증

2. 좋은 단위 테스트

  • 자동화 / 격리 / 빠름 / 반복 가능 (FIRST)
  • 테스트하기 쉬운 코드 = 잘 설계된 코드

3. 5주차 연결

  • DI 덕분에 Mock 주입·격리 테스트 가능
  • 테스트 = 변경 자신감 + 리팩토링 안전망

📚 다음으로...

Unit 1.2 — assertThat과 매처(Matcher)

이번 Unit에서 테스트의 필요성을 봤다면, 다음은 assertThat과 매처.

  • assertThat(actual, matcher)
  • is() 등 Hamcrest 매처
  • 전통 assertEquals vs Hamcrest
  • 자연어처럼 읽힘

Phase 1 진행 상황

🧪 Phase 1 — JUnit 테스트
  ✅ Unit 1.1 단위 테스트의 필요성 ← 여기
  ⏭ Unit 1.2 assertThat과 매처
  ⏭ Unit 1.3 JUnit 실행 방식
  ⏭ Unit 1.4 매번 새 오브젝트
  ⏭ Unit 1.5 픽스처와 @BeforeEach

6주차 누적 진행

🧪 Part A — 학습 도구와 환경
  Phase 1 — JUnit 테스트 (1/5 진행)

총: 1/28 Unit

🧪 Part A 시작 — 테스트로 검증하는 코드

profile
Software Developer

0개의 댓글