6주차 Unit 1.4 — 매번 새 오브젝트를 만드는 이유

Psj·2026년 5월 28일

F-lab

목록 보기
185/240

Unit 1.4 — 매번 새 오브젝트를 만드는 이유

F-LAB JAVA · 6주차 · Phase 1 · JUnit 테스트


📌 학습 목표

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

  • 매번 새 오브젝트를 만드는 핵심 이유 는?
  • "독립적 실행 보장" 의 의미는?
  • 인스턴스 변수를 부담 없이 사용 할 수 있는 이유는?
  • 테스트 간 의존성 제거 는?
  • 실행 순서 무관 의 의미는?
  • 인스턴스 변수에 비싼 자원 을 만들면?
  • 공유가 필요한 자원 은 어떻게 처리하나?
  • @BeforeAll 의 트레이드오프 는?
  • 테스트 설계 원칙 은?

🎯 핵심 한 문장

JUnit 이 매 테스트마다 새 오브젝트를 만드는 이유는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장하기 위해서이며, 덕분에 인스턴스 변수를 부담 없이 쓸 수 있지만 비싼 자원을 매번 만들면 느려지므로 공유 자원은 @BeforeAll 로 처리한다.
매번 새 오브젝트를 만드는 핵심 이유는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장 하기 위해서다.
이로써 얻는 이점은 (1) 인스턴스 변수를 부담 없이 사용 가능 (어차피 다음 테스트에서 초기화됨), (2) 테스트 간 의존성 제거, (3) 실행 순서 무관 이다.
다만 인스턴스 변수에 비싼 자원 (예: DB Connection) 을 만들면 매번 생성 되어 테스트가 느려지는 단점이 있다.
따라서 정말 공유가 필요하고 생성 비용이 큰 자원은 @BeforeAll (전체 1회, static) 로 처리하되, 이는 격리를 일부 포기하는 트레이드오프이므로 상태 없는 자원에만 신중히 적용한다.

비유 — 일회용 vs 재사용 식기

매번 새 오브젝트 = 일회용 식기:

매번 새 오브젝트 (일회용):
  - 손님마다 새 접시 (인스턴스)
  - 이전 음식 안 묻음 (격리)
  - 독립적
  → 위생 보장 (간섭 X)

부담 없이 사용:
  - 접시에 뭘 담든
  - 다음 손님 새 접시
  - 안 씻어도 됨 (버림)

비싼 자원 매번 (낭비):
  - 손님마다 새 오븐 설치?
  - 비효율 (DB Connection)

공유 자원 (@BeforeAll):
  - 오븐은 1대 공유 (주방 세팅)
  - 한 번 설치
  - 단, 오븐에 상태 남으면 위험

→ 매번 새 오브젝트 = 독립 실행 보장 (일회용), 비싼 자원은 @BeforeAll (공유, 신중).


🧭 9개 섹션 로드맵

1. 핵심 이유 (독립 실행)
2. 독립적 실행 보장
3. 부담 없는 인스턴스 변수
4. 의존성 제거
5. 실행 순서 무관
6. 비싼 자원의 문제
7. 공유 자원 처리 (@BeforeAll)
8. 트레이드오프
9. 면접 + 자기 점검

1️⃣ 핵심 이유 (독립 실행)

1.1 핵심 이유

매번 새 오브젝트 이유:

  "각 테스트가 서로 영향을 주지 않고
   독립적으로 실행됨을 확실히 보장"

  → 독립 실행 보장

1.2 영향 차단

영향 차단:

  공유 인스턴스라면:
    - 한 테스트 상태가
    - 다음에 영향

  새 인스턴스:
    - 영향 차단

1.3 확실히 보장

확실히 보장:

  "확실히" 가 중요:
    - 실수로 공유 X
    - 강제 격리
    - 프레임워크 보장

→ 개발자 실수 방지

1.4 ILIC 의 맥락

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

매번 새 오브젝트를 만드는 핵심 이유는?

:
1. 핵심 이유:

  • 독립 실행 보장
  1. 영향 차단:

    • 상태 격리
  2. 확실히:

    • 강제 격리
  3. 목적:

    • 실수 방지

2️⃣ 독립적 실행 보장

2.1 독립 실행

독립 실행:

  각 테스트:
    - 혼자 실행 가능
    - 다른 테스트 불필요
    - 자기 완결

2.2 자기 완결

자기 완결:

  한 테스트:
    - 준비 (BeforeEach)
    - 실행
    - 검증
    - 정리

  → 스스로 완결

2.3 격리의 수준

격리 수준:

인스턴스 상태:
  - 자동 격리 (새 인스턴스)

외부 상태 (DB):
  - 수동 격리 (deleteAll)

2.4 ILIC 의 맥락

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

2.5 자기 점검 답변

"독립적 실행 보장" 의 의미는?

:
1. 독립 실행:

  • 혼자 실행 가능
  1. 자기 완결:

    • 준비~정리
  2. 격리 수준:

    • 인스턴스/외부
  3. 목적:

    • 다른 테스트 불필요

3️⃣ 부담 없는 인스턴스 변수

3.1 부담 없음

부담 없는 인스턴스 변수:

  인스턴스 변수에:
    - 자유롭게 데이터
    - 다음 테스트 초기화
    - 누적 걱정 X

→ 편하게 사용

3.2 왜 부담 없나

왜 부담 없나:

  매번 새 인스턴스:
    - 인스턴스 변수 새로
    - 이전 잔여 X

→ 정리 불필요

3.3 픽스처로 활용

// 인스턴스 변수 = 픽스처 (부담 없이)
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
    }
}

3.4 vs 지역 변수

인스턴스 변수 vs 지역 변수:

지역 변수:
  - 메서드 내
  - 매번 선언

인스턴스 변수 (픽스처):
  - @BeforeEach 한 곳
  - 여러 테스트 공통
  - 중복 제거

3.5 ILIC 의 맥락

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

3.6 자기 점검 답변

인스턴스 변수를 부담 없이 사용할 수 있는 이유는?

:
1. 부담 없음:

  • 다음 테스트 초기화
  1. :

    • 새 인스턴스
  2. 픽스처:

    • @BeforeEach
  3. 이점:

    • 정리 불필요

4️⃣ 의존성 제거

4.1 테스트 간 의존성

테스트 간 의존성:

  나쁜 경우:
    - test_b 가 test_a 결과 의존
    - test_a 실행 후만 통과

→ 의존성 (제거 대상)

4.2 의존성 제거

의존성 제거:

  새 인스턴스:
    - 각 테스트 독립
    - 다른 테스트 결과 무관

→ 의존성 차단

4.3 의존 안티패턴

// ❌ 의존 (안티패턴)
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 안 돌면 실패
    }
}

4.4 독립 설계

// ✓ 독립 (각자 준비)
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) {}

4.5 ILIC 의 맥락

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

4.6 자기 점검 답변

테스트 간 의존성 제거는?

:
1. 의존성:

  • test_b 가 test_a 의존 (나쁨)
  1. 제거:

    • 새 인스턴스
  2. 안티패턴:

    • static 공유
  3. 독립 설계:

    • 각자 준비

5️⃣ 실행 순서 무관

5.1 순서 무관

실행 순서 무관:

  테스트 순서:
    - 어떤 순서든 OK
    - 결과 동일

→ 순서 보장 안 해도 됨

5.2 왜 가능

왜 가능:

  독립 + 새 인스턴스:
    - 각 테스트 격리
    - 순서 영향 X

→ 순서 무관

5.3 순서 의존 위험

순서 의존 위험:

  순서 의존하면:
    - 환경 따라 순서 다름
    - 가끔 실패 (flaky)
    - 디버깅 어려움

→ 순서 무관 설계

5.4 병렬 실행 가능

병렬 실행 가능:

  순서 무관 + 독립:
    - 병렬 실행 OK
    - 속도 ↑

  JUnit 5 병렬 지원

5.5 ILIC 의 맥락

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

5.6 자기 점검 답변

실행 순서 무관의 의미는?

:
1. 순서 무관:

  • 어떤 순서든 OK
  1. :

    • 독립 + 새 인스턴스
  2. 위험:

    • 순서 의존 (flaky)
  3. 이점:

    • 병렬 실행

6️⃣ 비싼 자원의 문제

6.1 비싼 자원

비싼 자원:

  생성 비용 큰 것:
    - DB Connection
    - 네트워크 연결
    - 무거운 객체

  → 인스턴스 변수면 매번 생성

6.2 매번 생성 문제

매번 생성 문제:

  @BeforeEach 에 비싼 자원:
    - 테스트마다 생성
    - 100 테스트 = 100번
    - 느림

→ 테스트 속도 ↓

6.3 예시

// ❌ 비싼 자원 매번 (느림)
class SlowTest {
    private Connection connection;
    
    @BeforeEach
    void setUp() throws Exception {
        // 매 테스트마다 DB 연결 (비쌈)
        connection = DriverManager.getConnection(url, user, pwd);
        // 100 테스트 = 100번 연결 (느림)
    }
}

6.4 속도 영향

속도 영향:

  비싼 자원 × 테스트 수:
    - 누적 비용
    - 전체 테스트 느림
    - 자주 못 돌림

→ 빠름 (FIRST) 위반

6.5 ILIC 의 맥락

// 비싼 자원 문제
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 {}

6.6 자기 점검 답변

인스턴스 변수에 비싼 자원을 만들면?

:
1. 비싼 자원:

  • DB Connection 등
  1. 매번 생성:

    • 테스트마다
  2. 문제:

    • 느림 (누적)
  3. 위반:

    • 빠름 (FIRST)

7️⃣ 공유 자원 처리 (@BeforeAll)

7.1 @BeforeAll 공유

// 공유 자원 — @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();
    }
}

7.2 분리 전략

분리 전략:

@BeforeAll (한 번):
  - 비싼 자원
  - DataSource, 컨테이너

@BeforeEach (매번):
  - 가벼운 픽스처
  - 상태 초기화

7.3 상태 초기화 병행

상태 초기화 병행:

  @BeforeAll 로 공유 자원:
    - 하지만 상태는?

  @BeforeEach 로 초기화:
    - deleteAll
    - 격리 유지

→ 자원 공유 + 상태 격리

7.4 Testcontainers

Testcontainers:

  실제 DB 컨테이너:
    - @BeforeAll 로 시작
    - 모든 테스트 공유
    - 현실적 통합 테스트

→ 비싼 자원 공유 사례

7.5 ILIC 의 맥락

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

7.6 자기 점검 답변

공유가 필요한 자원은 어떻게 처리하나?

:
1. @BeforeAll:

  • 한 번 (static)
  1. 분리:

    • All: 비싼 자원
    • Each: 픽스처
  2. 상태 초기화:

    • @BeforeEach 병행
  3. :

    • DataSource, 컨테이너

8️⃣ 트레이드오프

8.1 격리 vs 속도

격리 vs 속도:

매번 새 (격리):
  + 완전 격리
  - 비싼 자원 느림

공유 (@BeforeAll):
  + 빠름
  - 격리 일부 포기

8.2 공유의 위험

공유의 위험:

  @BeforeAll 자원에 상태:
    - 테스트 간 영향
    - 격리 깨짐

→ 상태 없는 자원만 공유

8.3 신중한 적용

신중한 적용:

  공유 가능:
    - 상태 없음 (DataSource)
    - 읽기 전용

  공유 위험:
    - 변경 가능 상태
    - → @BeforeEach 초기화

8.4 균형

균형:

  기본: @BeforeEach (격리)
  예외: @BeforeAll (비싼 + 상태 없음)

  + 상태는 @BeforeEach 초기화

→ 격리 + 속도 균형

8.5 ILIC 의 맥락

// 트레이드오프 균형 (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 {}

8.6 자기 점검 답변

@BeforeAll의 트레이드오프는?

:
1. 격리 vs 속도:

  • 매번(격리) vs 공유(속도)
  1. 공유 위험:

    • 상태 있으면 영향
  2. 신중:

    • 상태 없는 자원만
  3. 균형:

    • All(비싼) + Each(상태 초기화)

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
매번 새 이유?독립 실행 보장
독립 실행?영향 X, 자기 완결
부담 없는 변수?다음 초기화
의존성 제거?각 독립
순서 무관?격리, 병렬 가능
비싼 자원?매번 생성 (느림)
공유 자원?@BeforeAll
트레이드오프?격리 vs 속도
공유 위험?상태 영향
균형?All(비싼)+Each(초기화)

9.2 자기 점검 체크리스트

핵심 이유

  • 독립 실행

독립 실행

  • 자기 완결

부담 없는 변수

  • 다음 초기화

의존성 제거

  • 각 독립

순서 무관

  • 병렬

비싼 자원

  • 매번 느림

공유 자원

  • @BeforeAll

트레이드오프

  • 격리 vs 속도

9.3 추가 심화 질문

Q1: @TestInstance(PER_CLASS) 트레이드오프?

답:

  • 인스턴스 1개 (클래스당)
  • @BeforeAll 비static 가능
  • 하지만 격리 약화
  • 상태 관리 주의

Q2: 테스트 병렬 실행?

답:

  • JUnit 5 @Execution(CONCURRENT)
  • 독립 테스트만 안전
  • 공유 자원 주의
  • 속도 ↑

Q3: 통합 테스트 격리?

답:

  • @Transactional (롤백)
  • @Sql (초기화)
  • Testcontainers
  • @DirtiesContext

Q4: Spring 테스트 컨텍스트 캐싱?

답:

  • ApplicationContext 재사용
  • 테스트 간 공유 (성능)
  • @DirtiesContext 시 재생성
  • 비싼 컨텍스트 공유

Q5: flaky 테스트?

답:

  • 가끔 실패 (불안정)
  • 순서/시간/외부 의존
  • 격리 부족 신호
  • 제거 필요

🎯 핵심 요약 — 3줄 정리

1. 매번 새 오브젝트 이유

  • 각 테스트가 독립적으로 실행됨을 확실히 보장
  • 인스턴스 변수 부담 없이 사용, 의존성 제거, 순서 무관

2. 비싼 자원의 문제

  • 인스턴스 변수에 DB Connection 등 만들면 매번 생성 (느림)
  • 빠름 (FIRST) 위반

3. 공유 자원 처리

  • @BeforeAll (전체 1회, static) 로 비싼 자원 공유
  • 단, 상태 없는 자원만 (상태는 @BeforeEach 초기화) — 격리 vs 속도 균형

📚 다음으로...

Unit 1.5 — 픽스처(Fixture)와 @BeforeEach (Phase 1 완주)

이번 Unit에서 새 오브젝트 이유를 봤다면, 다음은 픽스처 (Phase 1 마지막).

  • 픽스처 정의 (공통 준비물)
  • @BeforeEach 로 생성
  • 테스트 결과의 일관성
  • 시작 상태 보장 (deleteAll)

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 완주

6주차 누적 진행

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

총: 4/28 Unit
profile
Software Developer

0개의 댓글