본문 바로가기
캠프/항해 플러스 4기

[1주차] TDD 시작하기

by 핏차 2024. 3. 29.

 

항해 플러스 1주차 과정을 시작하기 전에, 사전 스터디를 신청해 최범균 저자의 「테스트 주도 개발 시작하기」 를 훑어보았다.

타이밍이 좋았다고 생각했던 것이, 그 즈음 회사에서 통합테스트만 작성하다가 mocking을 이용한 서비스 테스트를 도입하기 시작했다.

테스트 주도 개발, TDD는 서비스 로직을 개발할 때 단순하고 작은 케이스 단위의 테스트 코드를 만들어 해당 테스트를 통과하기 위한 로직을 쌓아서 하나의 전체 서비스 로직을 완성하는 방식이다. 바로 그 단위 테스트 작성법을 딱 회사에서 접한 것이다. 참 다행이지..


TDD 기반으로 개발 시작하기

역시 처음에는 정말 더디고 더뎠다. 포인트 충전, 사용 API를 만드는 데 절대 먼저 작성하지 않고 테스트로 성공/실패를 계속 반환하며 그 기반으로 로직을 짜려고 노력했다. 아무것도 없는 함수인 걸 알면서도 테스트 실행을 누르는 것은 생각보다 심적으로 빡셌다.

단위 테스트 기반으로 서비스 로직을 작성하고, 컨트롤러에 서비스 메서드를 연결한 후 통합테스트 작성을 시도했다.

여기서 한 가지 의문이 들었다. 통합 테스트도 발생할 수 있는 유스 케이스에 맞춰 작성하다 보니 결국 너무 비슷했다. (DB를 연결하지 않고 api로 사용해서 그럴지도?) 단위 테스트와 통합 테스트의 각각의 역할이 궁금해졌다.

 

단위 테스트는 Stub을 이용하는 방법과 Mock을 이용하는 방법이 있는데, 나는 Mock으로 처리했다. 하나의 동작 단위에만 집중하게 되어 좋은 것 같았기 때문이다.

@Test
@DisplayName("유저_ID와_충전할_포인트를_입력하면_포인트_충전")
void chargeTest_유저_ID와_충전할_포인트를_입력하면_포인트_충전() {
    // given
    Long id = 1L;
    Long chargeAmount = 1000L;
    UserPoint updateUserPoint = new UserPoint(id, 1000L, 0L);

    // when
    when(lockManager.executeWithLock(anyLong(), any())).thenReturn(updateUserPoint);
    when(userPointRepository.insertOrUpdate(id, chargeAmount)).thenReturn(updateUserPoint);
    UserPoint result = pointService.charge(id, chargeAmount);

    // then
    assertNotNull(result);
    assertEquals(result.id(), 1L);
    assertEquals(result.point(), 1000L);
}

 

동시성 제어하기

부끄럽지만 동시성 제어에 대한 생각을 처음 해봤다. 분산락 사용하지 말라고 하셨고 DB도 안 붙어있으니까 메모리단에서 락을 걸던가, 로직으로 제어하던가, 자바의 기능이 있지 않을까 했다.

찾아보니 ReentrantLock 또는 Syncronized를 이용하여 메모리단에서 제어하는 것이 좋을 것 같았다.

Syncronized 사용과 ReentrantLock 사용의 가장 큰 차이는 병목 현상 위험이였다. 그래서 이번에는 ReentrantLock을 사용했다.

 

ConcurrentHashMap

ConcurrentHashMap<Long, Lock> lockMap = new ConcurrentHashMap<>();
// 구분키 Long 마다 락을 걸어 유저마다 하나의 락으로 동시 동작을 제어할 수 있다.

 

 

동시성 제어가 잘 이루어지는지는 통합 테스트에서 작성한다. 동시성 제어를 처음 해보다 보니까 동시성 테스트 작성하는 것에 대한 고민이 가장 많았다. CompletableFuture.runAsync()를 이용하여 비동기로 동시적 요청을 했다.

 

CompletableFuture.runAsync()

// 포인트 충전 작업
for (int i = 0; i < numberOfTasks; i++) {
    CompletableFuture<Void> futureCharge = CompletableFuture.runAsync(() -> {
        try {
            pointService.charge(userId, chargeAmount);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    futures.add(futureCharge);
}

// 포인트 사용 작업
for (int i = 0; i < numberOfTasks; i++) {
    CompletableFuture<Void> futureUse = CompletableFuture.runAsync(() -> {
        try {
            pointService.use(userId, useAmount);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    futures.add(futureUse);
}

// 모든 작업이 완료될 때까지 기다림
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.get();

 


테스트

 

테스트 코드의 의미?
  • 코드를 작성하는 개발자에게 "너 이거 놓치면 안되고 잘 체크해야 해" 의 유산이다.
  • 테스트 코드는 변경에 민감해야 한다.
    좋은 테스트 코드는 작디 작은 변화에도 (ex. enum 하나 추가) 실패가 잘 되는 코드
  • 테스트 코드가 무엇인가? 에 나의 생각을 담아놓자.
테스트의 종류
  • Unit Test : 단일 모듈 테스트
  • Integration Test : 멀티 모듈 테스트 (실무에서는 보통 Controller ~ DB까지 타고 오는거)
  • End To End Test (E2E) : 종단 테스트 (웹에서부터 시작하여 다시 웹 = 유저부터 유저까지)
실무에서의 테스트 작성 팁
  • 도메인이 테스트의 핵심. Service 로직 단위 테스트에 힘을 주자.
  • 객체에 각 책임을 부여하고 메세지를 던져 사용해라 == 객체 지향적인 개발이 이루어질수록 단위 테스트가 수월하다.
  • 통합 테스트는 특별한 상황을 테스트한다. ex) 동시성 테스트, 쿼리가 잘 동작하는지 등
  • 통합 테스트 시 도메인 단에서 mocking이 잘 되어야지 잘 작성한 코드
// 잘못된 코드
userPoint.save(point = 1000)
// 잘 짠 코드
userPointRepository.save(userPoint)

@SpringBootTest
class UserPointRepositoryTest{
	
}
  • 동시성 테스트는 보통 Controller 또는 Infra 단에서 한다.

TDD 개발의 흐름

어느 레이어부터 작성할 것인가?

: Bottom up / Top down


동시성 제어

동시성 제어 방법
  • 아무것도 이용하지 않는 방법 best
    • 우리는 그동안 unique key 를 이용하여 동시성 제어를 하고 있었다.
  • 메모리 단에서 하는 방법
    • Syncronized
    • ConcurrentHashMap<> (ReentrantLock)
    • 메모리 단에서 제어하면 성능 저하 이슈가 생긴다.
    • 실무에서 사용하는 경우는 대표적으로 게임 개발 등
  • DB를 이용하는 방법
    • 낙관적 락
      • 낙관적 락이 가능하다면 낙관적 락을 사용하는 것이 좋음
      • 하지만 retry를 계속 한다고 해도 성공이 보장되지 않는 것이 단점
    • 비관적 락
      • DB 부하가 심함
      • 대규모 처리 시 비관적 락 사용하면 DB 터짐
  • 분산락 이용하는 방법
    • Redis
    • 분산락을 사용하는 경우? DB 부하를 줄이기 위해
728x90

'캠프 > 항해 플러스 4기' 카테고리의 다른 글

[3주차] WIL  (1) 2024.04.06
[2주차] WIL  (0) 2024.03.29
[2주차] Architecture  (0) 2024.03.29
[1주차] WIL  (0) 2024.03.29
[0주차] 시작하는 마음  (0) 2024.03.23