LostCatBox

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

Word count: 1.4kReading time: 8 min
2025/03/24 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 학습 정리
    1. 1.1. Test 가 중요한 이유
    2. 1.2. 테스트 종류
    3. 1.3. Test Double
      1. 1.3.1. Mock
      2. 1.3.2. Stub
      3. 1.3.3. (중요) 경계값 검증
    4. 1.4. 테스트 작성 시 주의사항
  2. 2. [1주차] 공개 Q&A 요약
    1. 2.1. mock 과 stub 차이
    2. 2.2. controller layer와 service layer에서 검증(valid) 차이
    3. 2.3. 동시에 여러번 충전, 여러번 사용에 대해서 어떻게 테스트 -> 통합테스트 추천
      1. 2.3.1. 동시성 이슈가 통합테스트인 이유
    4. 2.4. 꿀팁) 테스트 패키지 구조
    5. 2.5. 검증의 범위 및 의미
    6. 2.6. 작업의 단위 하나 = 트랜잭션
      1. 2.6.1. 질문
      2. 2.6.2. 답변
    7. 2.7. 비즈니스 로직이 거의없는 DAO 단순 참조하는 함수의 테스트 여부?
    8. 2.8. 단위 테스트 대상
      1. 2.8.1. 유저 포인트 조회 기능
      2. 2.8.2. 위에 조건에서 유저포인트 조회 함수 테스트
    9. 2.9. 동시성 테스트란 어떻게 짜는가?
      1. 2.9.1. 동시성 테스트
      2. 2.9.2. 동시성 테스트시 라이브러리 참조
  3. 3. 질문
  4. 4. 멘토링
    1. 4.1. 단위테스트, 통합테스트
    2. 4.2. 동시성 이슈 발생하는곳