LostCatBox

(항해) 1주차 TDD 자료 정리 및 동시성 자료 정리

Word count: 1.4kReading time: 8 min
2025/03/24 7 Share

[1주차] TDD 학습 정리

Test 가 중요한 이유

  • 기능개발 필요 시 -> 어떤 테스트의 통과여부가 기능에 대한 검증 역할을 할수 있을지 생각 필요
    • 초기 시 기능 먼저 개발 -> 내가 어떤 테스트를 먼저 짜야할지 항상 생각 -> 나중에 추가적으로 채울수있음
    • TDD 실력이 채워진 후 테스트 작성 -> 기능 개발

테스트 종류

  • End-to-end
    • 대상 : 전체 애플리케이션의 흐름
    • 목적 : 애플리케이션이 제공하는 기능을 사용자 시나리오 기반으로 문제 없는지 점검
      • 내가 정의한 흐름의 처음부터 끝까지를 테스트하는 것
  • integration testing(통합 테스트)
    • 대상 : 서로 다른 module / system 의 상호작용
    • 목적 : 맞물려 돌아가는 기능이 모여 정상적으로 원하는 기능을 제공하는지 점검
      • 예시 : 각 손가락이 꺽이는지는 이미 유닛 테스트 진행완료됨에 따라 손가락이 유기적으로 움직이는지만 테스트
      • 보통 단위테스트에서 모두 테스트되며, 인풋 아웃풋만 테스트(블랙 박스테스트)
      • 현재 정의로는 통합테스트 = @SpringBootTest 사용한다.
  • unit testing(단위 테스트)
    • 대상 : 단일 기능 혹은 작은 단위의 함수/객체 등 (독립적인 객체에 대한 테스트)
    • 목적 : 가벼운 비용으로 새로운 기능 혹은 개선이 기존의 rule 을 위배하지 않는지 점검
      • 예시 : 각 손가락이 꺽이는지는 유닛 테스트
      • 단위테스트를 모두 작성하며, 화이트 박스 테스트
      • A service 만 테스트함(B service에 의존성이있다면 지워줄꺼다. )

Test Double

  • 테스트 더블은 실제 컴포넌트를 대체할 수 있도록 하는 대역이다
    • mock, stub은 결국 다른 객체와 의존성을 지우기위해 사용한다
  • 실제 컴포넌트에 대해 행동을 모방하고, 이를 통해 기존의 강한 결합도를 낮추고 테스트 중 제어 가능하도록 한다.

Mock

  • 테스트를 위해 특정 기능에 대해 정해진 응답을 제공하는 객체
  • 입력과 상관없이 어떤 행동을 할 지에 초점을 맞춘 객체
    (=”아 모르겠고~” 정해진 응답제공
    (=그니까 대충대충 뭐가오든 반환해, 지금 테스트할때 mock처리대상에 상태가 중요한게아니야)
  • Stub보다 넓은 개념
  • Mock Library 를 통해 특정 행동에 대한 출력을 정의

Stub

  • 테스트에 필요한 호출에 대해 미리 준비된 응답을 제공하는 객체
  • 입력에 대해 어떤 상태를 반영하는 지에 초점을 맞춘 객체
  • Interface 기반으로 테스트에서 보고자하는 (혹은 필요로 하는) 구현에 집중한 구현체를 정의
    • 이런 숫자(1L, 2L)가 들어가면 이렇게 각각 대답해줘 => stub 입력을 특정했기 때문에

(중요) 경계값 검증

  • 0보다 적거나 같은, 4보다 크보다 같은 등은 0, 4에 대한 근처를 테스트해야함
  • 유저 A, 유저 B 에 대한 테스트같은 경우 -> mock 아닌 stub이 필요함.(인풋에 따라 아웃풋이 바뀌는 어떤 상태를 반환해야하므로)

테스트 작성 시 주의사항

  • TC( =TestCase) 를 “뾰족하게” 뽑아서 기능에 “예민하게” 반응하는 테스트를 작성했는가?
  • 테스트 커버리지 100% 가 아니라, 정확히 기능의 동작을 확인하는 테스트를 작성해 주세요.
    • 어떤 테스트 명세 작성해서 정확한 기능 동작을 검증 할수있을까 고민필요(=뾰족한 테스트)
  • 주요 기능에서 private 접근자, 객체 간의 강결합 같이 테스트 불가능한 코드는 가능한 한 지양하는 것이 좋습니다.
    • private -> public 대신 역할 분리를 하는 것도 고려해야함
      • 단순히 클래스에서 함수분리한다고 private으로 바꾸면, 해당 책임을 검증하기 위해 결국 통합 테스트로 밖에 테스트 못한다 -> 이는 결국 통합테스트에서 모든 객체들이 유기적으로 움직이는지 모두 알고있어야 통합테스트가 가능함.
      • private 메서드를 다른 메서드에서도 사용하고있다면, 다른 메서드 모두 통합테스트에 동일 케이스 조건을 모두 작성해야함
  • 정답을 줫는데 정답을 응답하면 정상이다. 이를 테스트할 필요가 있을까?
    • *어? 안되는데 -> 실패케이스를 모두 테스트 한다면, 역으로 정상 기능 동작 증명가능하다. *
      • 실패 케이스 테스트는 기본적으로 if ->then으로 작성한다.
        • page가 0보다 작으면 검색을 실패(어떤 Exception 동작한다 등 구체화)한다.
        • pazeSize가 0보다 작으면 검색을 실패(어떤 Exception 동작한다 등 구체화)한다.
  • 객체 또는 함수에 대해서 테스트 작성 시, 함수 또는 객체 하나가 많은 작업에 대한 책임(연관 테스트가 많음) 을 갖고있다면, 책임 분리 해야한다는 신고다.

[1주차] 공개 Q&A 요약

mock 과 stub 차이

stub : 단순 값 반환에 대해서 검증하면서 왜 “상태” 검증이라고 하는가?

  • 객체의 상태 = “반환 값” -> 반환 값에 따라 테스트의 결과가 달라야함을 뜻함.

    • 따라서 상태에 대해서 다른 결과를 테스트로 검증하기때문에 “상태” 검증

    • When(service.call(1L).willReturn(List(1L, 2L, 3L)))

    • 아래 ARepositoryStub은 상태검증의 Stub

      • interface ARepository {
             fun getA(): Long   
        }
        
        class ARepositoryStub : ARepository {
               override fun getA(): Long {
                return 1L
               }
        }
        <!--0-->
        
        
        
        

controller layer와 service layer에서 검증(valid) 차이

  • (클럽문지기) Controller valid -> 검증의 관심사 숫자야? 컨트롤러에서는 돈은 음수면 안되는데?, 비즈니스에서의 관심사는 아님
    • 좌석인지, 입석인지 검증안함. 입장가능한지만 검증
  • (클럽 카운터) service -> 이미 타입은 안다. 호출되는거니까. 서비스 입장에서는 돈은 음수면 안되는데?
    • 좌석인지, 입석인지, 입장료가 있는지, 이용가능한지까지 검증
    • @Valiated는 올바른 값인지만 판단하고보통, 비즈니스 로직과 관련있는경우, 무조건 service 나 도메인 모델이서 검증

동시에 여러번 충전, 여러번 사용에 대해서 어떻게 테스트 -> 통합테스트 추천

스크린샷 2025-03-24 21.41.35

  • 단위테스트로 진행 시에는 내가 정해놓은 stub의 값을 뱉기때문에 의미가없다.
  • 통합테스트시 when완료 후 then 에서 db접근하여 결과값과 예측결화 조회 검증

동시성 이슈가 통합테스트인 이유

스크린샷 2025-03-24 21.43.37

  • 동시성 이슈가 발생하는지에 대한 테스트 -> 같은 자원에 동시로 접근할때 -> 즉, 진짜 2개는 객체여아함-> 통합테스트이다.
  • Tip: “행동의 경계” 객체에서 테스트합니다
    • 즉 현재 Service 에서 DB에 접근할때, 동시성이슈가 생기므로, Service 기준으로 테스트 하면된다.


꿀팁) 테스트 패키지 구조

1
2
3
4
5
6
7
/main/src/point/PointService.java <- 요거 테스트할거임

/test/src/point/
- PointServiceTest.java <- 유닛테스트
- PointServiceIntegrationTest.java <- 통합테스트
- PointServiceE2ETest.java <- E2E 테스트
-> 왜 ? import 했을 때, 내 "타겟 객체" 과 가까운 혹은 같은 위치에 있어야 추적이 용이하지 않을까?

검증의 범위 및 의미

  • 비즈니스 로직으로 의미가 있는 것만 테스트하면됨. (테스트 커버리지100%필요없다)
  • 단위 테스트 코드를 작성 할때, red-green-refactor(RGB) 사이클을 통해서 작성하는 것이꼭 필요한가? -> 제일 중요한건, TC를 “뾰족하게” 뽑아서 기능에 “예민하게” 반응하는 테스트를 작성했는가 에 대해서 집중하자
1
2
3
4
5
6
7
8
9
테스트 (주어진 id 의 유저가 없다면, 0원을 가진 유저를 반환한다.)
-> 모호하다 / 유저는 있는데 0원인 경우? 검증 방법.
==> Repository 테스트 해요 말아요?
===> 기본 유저 조회를 테스트해야하나?

===> 나 진짜 새 유저인지 테스트해보고 싶어.
===> 통합테스트로 "내역" 조회해보고 비어있는 지 검증하면 됨.
===> 헌 유저인데 0원인지 테스트해보고 싶어.
===> 통합테스트로 "내역" 조회해보고 1개 이상인지 검증하면 됨.

작업의 단위 하나 = 트랜잭션

질문

  • 포인트 충전시 UserPointTable, PointHistoryTable 테이블에 저장하는데
    테스트시 UserPointTable, PointHistoryTable 각각 테이블에 들어간 값을 확인하면 될까요?
    혹시 UserPointTable가 들어가고 어떠한 이유로 PointHistoryTable에 들어가지 못했다면,
    해당 테스트는 실패라고 봐야하는건지 궁금합니다.
    테스트 한 METHOD에서 UserPointTable 테이블에 들어가는지, PointHistoryTable 테이블에 들어가는지 2개이상의 내용을 검증하게되는 것 같아 혼란스러워서요..

답변

  • 당연히 YES.

  • [내가 검증하고자 하는 기능이 충전할 시, 유저의 돈과 내역은 정상적으로 반영되어야 한다.] 를 검증한다면, 내가 생각하는 “기능의 단위” 가 <”유저의 돈이 변경 반영” + “내역 저장”> <– 작업의 단위 = Transaction


비즈니스 로직이 거의없는 DAO 단순 참조하는 함수의 테스트 여부?

  • 통합테스트만 진행.
  • 비즈니스로직(자기 고유의 기능)없음 -> 단위 테스트 없음 ->

단위 테스트 대상

유저 포인트 조회 기능

1
2
3
4
5
6
7
8

PointService {

유저포인트조회(userId): 유저포인트 {
if (userId <= 0) throw 너 누구야 // 단위 테스트 대상
return 유저포인트테이블.get(userId) // 단위 테스트 대상 x -> 비즈니스로직도 아니고,단순한 메서드임
}
}

위에 조건에서 유저포인트 조회 함수 테스트

1. 단위테스트 필요한가?
    - userId 가 0일 때 예외 발생하는지?
    - userId 가 음수일 때 예외 발생하는지?
    - userId 가 양수일 때 예외 발생 안하는지?
2. 통합테스트
    - 그냥 유저를 조회하면 0원이다.
    - 그 유저에 대해 충전하고 조회하면 충전한 금액을 반환해야 한다.

동시성 테스트란 어떻게 짜는가?

동시성 테스트

원리: 동시에 같은 자원에 접근했을때, 내가 의도한 대로 결과를 갖는가?

  1. 동시에 같은 자원에 접근했을 떄, 한 명이 실패하는가?

    1. 같이 수정하려고 하면 수정하는 동안 접근한 애는 실패시킬때

      • 결과를 조회해서 내 생각과 맞아 떨어지는지
      • 한명은 실패 했는지도 검증
  2. 동시에 같은 자원에 접근했을 때, 두 요청이 다 반영되어야 하는가?

    1. 같은 수정하려면 하면 모든 요청이 줄지어서 성공시킬 때

      • 결과를 조회해서 내 생각과 맞아 떨어지는지
      • 실패하지않았는지 검증하면 좋다 안해도됨
        • 테스트 코드의 원리 -> throw되면 실패 -> 검증할 필요가 없다

동시성 테스트시 라이브러리 참조

  • CountDownLatch ( 멀티쓰레드 기반의 동기화 도구, 특정 쓰레드가 다른 쓰레드 작업 끝날때까지 기다림)

    • 테스트 쓰레드 에서 멀티 쓰레드로 다양한 행동을 “동시에” 수행시킨다.

    • 다 끝날때까지 기다린다.

    • 그리고 결과를 조회해서 검증한다.

  • 예시코드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int threadCount =3

    CountDownLatch latch = new CountDownLatch(threeCount)

    for (int i = 0; i < threadCount; i++){
    new Thread(() -> {
    //동작
    latch.countDown(); //latch 값 감소
    }).start()

    }
    latch.await(); //내 모든 쓰레드가 countDown() 시킬때까지 대기 // latch=0이 될때까지 기다림
    // latch =0 이 되면 다음 블록 실행

    assert() // 검증
  • CompletableFuture : 이건 JVM 의 멀티 쓰레드 치트키 인데, 그냥 latch써보기

질문

  • 아래보면 PointService 가 두개의 구현체를 각자 받아서 처리하고있는데, 해당 부분을 테스트한다면, 단위테스트와 통합테스트 둘다 해야하는가? 아니면 단순히 mock테스트로 호출여부만 판단하면될까?
  • 내가 이해한게맞나?
    • service -> service 호출가능, service들을 최대한 쪼개면 테스트 가 용이해 질텐데 맞는 방향인가?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

override fun chargePoint(request: ChargePointRequest): UserPoint {
val userPoint = userPointTable.selectById(request.userId)
val chargedPoint = userPoint.chargePoint(request.amount)

val savedUserPoint = userPointTable.insertOrUpdate(chargedPoint.id, chargedPoint.point)

pointHistoryTable.insert(request.userId, request.amount, TransactionType.CHARGE, savedUserPoint.updateMillis)

return savedUserPoint
}

override fun usePoint(request: UsePointRequest): UserPoint {
val userPoint = userPointTable.selectById(request.userId)
val chargedPoint = userPoint.usePoint(request.amount)

val savedUserPoint = userPointTable.insertOrUpdate(chargedPoint.id, chargedPoint.point)

pointHistoryTable.insert(request.userId, request.amount, TransactionType.USE, savedUserPoint.updateMillis)
return savedUserPoint
}

멘토링

단위테스트, 통합테스트

최대한 예외처리, fail처리를 검증하는것으로 단위테스트 처리

성공에 대해서만 통합테스트진행

외부시스템 또는 본인 객체말고의 연동관련에 대해서는 통합테스트 진행해야함. 실패, 예외처리 등등

동시성 이슈 발생하는곳

  • 같은 자원에 서로 경쟁하게 되는곳
  • 논리적 원자성을 보장해야하는 곳
  • @Transactional 사용하는곳
CATALOG
  1. 1. [1주차] TDD 학습 정리
  2. 2. [1주차] 공개 Q&A 요약
  3. 3. 질문
  4. 4. 멘토링