[ 1주차 과제 ] TDD 로 개발하기 Q&A

2025. 3. 24. 23:20향해99 8기

TDD + Mock/Stub + 단위/통합 테스트 + 동시성 테스트

 

TDD란?

테스트 먼저 작성 → 기능 구현 → 리팩터링
Red → Green → Refactor

  • 단위 테스트(Unit Test): 하나의 메서드(기능) 단위 검증
  • 통합 테스트(Integration Test): 여러 클래스가 함께 작동하는지 확인
  • 동시성 제어 테스트: 동시에 여러 요청이 들어올 때도 결과가 올바른지 확인

✅ Stub vs Mock (Test Double)

Stub vs Mock

 

반환값 설정 행동(호출 여부, 횟수) 검증
상태 검증 행동 검증
"이러면 이래라" "이렇게 불렸는가?"
// 예시 인터페이스
interface MemberService {
   fun findIds(userId: Long): List<Long>
}

Stub 예제

// 호출되면 값을 "리턴"만 해주는 더블 (행동 검증 X)
whenever(service.findIds(1L)).thenReturn(listOf(1L, 2L, 3L))

Mock 예제

verify(service).findIds(1L)
// 호출됐는지 "행동 검증" verify(service, times(1)).findIds(any())

👉 Stub: 상태(값) 검증
👉 Mock: 호출(행동) 검증


✅ 단위 테스트 (Service 단독)

class PointServiceTest {

    val repository = mock<PointRepository>()
    val service = PointService(repository)

    @Test
    fun `충전하면 포인트가 증가한다`() {
        // given
        whenever(repository.findById(1L)).thenReturn(Point(1L, 100))

        // when
        service.charge(1L, 100)

        // then
        verify(repository).save(Point(1L, 200))
    }
}

✅ 통합 테스트 (DB까지 진짜로)

@SpringBootTest
class PointServiceIntegrationTest {

    @Autowired
    lateinit var service: PointService

    @Autowired
    lateinit var repository: PointRepository

    @Test
    fun `충전 후 조회하면 충전한 만큼 나온다`() {
        service.charge(1L, 100)
        val result = service.getPoint(1L)
        assertEquals(100, result.amount)
    }
}

✅ 컨트롤러 vs 서비스 역할

  • Controller: "형식적인" 값 검증 (@Valid, 음수인지 등)
  • Service: "비즈니스" 규칙 검증 (최대 한도, 중복 등)
// Controller
@PostMapping("/charge")
fun charge(@Valid @RequestBody req: ChargeRequest) {
    service.charge(req.userId, req.amount)
}

// Service
fun charge(userId: Long, amount: Int) {
    if (amount < 0) throw IllegalArgumentException("음수 불가")
    // 비즈니스 로직
}

✅ 동시성 테스트 예제 (CountDownLatch)

@Test
fun `3명이 동시에 100원씩 충전하면 총 300원이 된다`() {
    val threadCount = 3
    val latch = CountDownLatch(threadCount)

    repeat(threadCount) {
        thread {
            service.charge(1L, 100)
            latch.countDown()
        }.start()
    }

    latch.await()
    assertEquals(300, service.getPoint(1L).amount)
}

✅ 동시성 제어 전략

synchronized, Lock 한 번에 하나만 접근 가능
DB SELECT FOR UPDATE 트랜잭션 단위 잠금
큐 / 이벤트 순차적 처리
Redis Lock 분산 환경에서 사용

동시에 요청했을 때, 하나만 처리해야 할지 / 전부 처리해야 할지를 명확히 정의 후 테스트


✅ 구조 정리 예시

/src/main/kotlin/point/PointService.kt /src/test/kotlin/point/PointServiceTest.kt // 단위 테스트 /src/test/kotlin/point/PointServiceIntegrationTest.kt // 통합 테스트