Readable-code 인프런 워밍업 4주차 후기

Readable-code 인프런 워밍업 4주차 후기

📌 2025-03-30 Readable-code 학습 회고

목차

1. 강의 수강

  1. 일주일간 강의 요약 정리

  2. 일주일간 강의 회고

  3. 다음 주에 학습목표

2. 미션

  1. 미션을 해결하는 과정

    • 어떤 관점에서 접근했는지

    • 문제를 해결하는 과정

    • 왜 그런 식으로 해결했는지

  2. 미션 해결에 대한 간단한 회고

강의 수강

강의 요약

  1. 하나의 레이어를 테스트하고 싶은 이유

  2. 모의 객체가 무엇인지 확인하고 이를 활용하여 테스트를 쉽게 만들어보자

하나의 레이어를 테스트하고 싶은 이유

상위 레이어로 갈 수 록 준비할게 많아집니다. 그리고 의존성도 그만큼 많이 늘어나게 되는데 최상위 레이어를 테스트하기 위해서 불필요한 의존성을 모두 가져올 필요가 없습니다. 최상위 레이어는 웹 컴포넌트 기능으로 사용자의 요청을 받아 적절한 서비스 계층으로 위임하고 결과에 대한 메세지만 사용자에게 전달해주면 됩니다. 최상위 계층을 테스트할 때에는 이미 하위 계층과 단위 테스트를 모두 단단하게 완성하면서 올라왔기에 정상적으로 처리된다는 것이 보장됩니다.최상위 계층은 정상 응답 포멧과 오류 응답 포멧 그외 예외 케이스에 대해서 우리가 예상한 양식대로 사용자에게 전달되는지 테스트를 하고 싶습니다.

최상위 레이어를 제외하고 서비스 계층 아래는 상황 분류에 따라 컨트롤러 계층에서 어떻게 반환하는지만 확인하면 됩니다.

모든 리포지토리부터 서비스와 그외 컴포넌트를 모두 등록할 필요없이 컨트롤러 계층과 웹 관련된 컴포넌트가 정상 동작하는지만 확인하면 되기에 관련된 빈만 등록하게 한 경량 컨테이너를 제공합니다.

@webMvcTest입니다. 파라미터로 등록할 컨트롤러의 이름을 넣을 수 있습니다.

 

모의 객체를 사용해보자

모의 객체는 개발자가 설정한 요청과 응답만 반환하는 오브젝트로 테스트에서 불필요한 리소스 낭비나 경험하기 어려운 상황에 대한 테스트를 가능하게 합니다.

@Mock,@Spy,@InjectMocks 와 같이 스프링 컨테이너 없이 모의 객체를 테스트 프레임워크 레벨에서 만들어주는 것과

@MockBean, @SpyBean과 같이 스프링 컨테이너에 원본 오브젝트가 초기화전에 모의객체 프록시가 등록되는 방식을 사용할 수있습니다.

모의 객체의 역할 종류는 5가지로 나뉩니다.

  1. Dummy

  2. Fake

  3. Stub

  4. Spy

  5. Mock

첫 번째 더미는 아무것도 하지 않은 모의 객체입니다. 개발자는 아무 구현하지 않고 의존성 컴파일 예외만 방지기

두 번째 페이크는 실제 오브젝트와 유사한 기능을 하나 가벼운 수준으로 테스트를 하기 편하게 합니다.

세 번째 스툽은 사용자가 설정한 값 그대로 반환하는 녹음기 오브젝트입니다.

네 번째 스파이는 실제 객체와 동일하나 특정 기능만 개발자가 지정하여 문제가 어느지점인지 확인할 수 있습니다.

다섯 번째는 목입니다. 목은 행위에 다한 기대를 명세합니다.

Stub과 Mock 차이

Stub은 어떤 기능을 요청했을 때 이런 기능을 요청하니 내부 값이 변경되어 내부 상태를 확인할 때 사용합니다.

Mock은 행위에 대한 결과 값을 검증하는 방식입니다.

강의 회고

이번 주는 Readable-code와 Practical-Testing의 마무리를 짓는 한 주가 되었습니다.

예전에 이미 봐둔 Practical-Testing 강의를 다시 보니 Readable-code에서 말씀하신 내용이 되새겨졌습니다.

테스트 코드는 '문서'라고 말씀하신 내용에 덧붙여 코드 자체가 '문서'다 라고 마무리가 되었어요

 

테스트 코드에 필요한 여러가지 어노테이션에 대한 설명을 알려주셨지만

핵심 내용은 어노테이션을 잘 활용하자가 아닌 테스트 코드의 목적을 한 눈에 들어오기 위해

  • @DisplayName@xxxxTest 어노테이션을 활용하기

  • Test Fixture 정리하기

  • 한 문단에 한 주제!( 추상과 구체 )

그리고 테스트도 실제와 동일한지 확인하기 위해 스프링 컨테이너를 초기화하기 위해 설정 정보를 전달해야합니다.

main 스프링 컨테이너와 다르게 @MockBean이나 @SpyBean등 특정 어노테이션으로 설정 정보가 달라질 수 있습니다.

그래서 테스트 환경마다 다른 스프링 컨테이너를 매번 초기화해야하므로 같은 구성 정보를 사용하는 테스트를 모아 테스트 비용도 줄일 수 있었습니다.

 

이 모든 것은 테스트 개발을 잘하자가 아니라 자동화 테스트를 코드의 안정성을 높이고

테스트 코드 자체도 다른 개발자가 이해하기 쉽고 목적을 알기 쉽게하기 위해 신경써야한다는 것을 느꼈습니다.

 

다른 개발자가 코드를 읽고, 유지보수를 하는 비용을 줄이기 위해 코드 가독성이 중요하며

테스트 코드 마저도 코드의 가독성이 중요하다는 것을 강조하셨습니다.

 

지금까지 테스트 코드를 작성하면서 가독성에 대한 생각을 계속 했었지만

Readable-code를 통해서 단계별로 코드를 진행하다보니 코드의 구체적인 가독성 높이는 방법을 알게 되었습니다.

 

미션

미션 해결방법

  1. 레이어별로 어떤 특징이 있는지 자기만의 언어로 표현하기

  2. 어떻게 테스트를 하면 좋을지 자기만의 언어로 표현하기

  3. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.

  4. 아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요? (@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)

미션 해결 방법은 강사님께서 알려주신 것이 나만의 언어로 표현할 수 있는 만큼 이해가 되었는지 확인하려면 진행했습니다.

먼저 레이어별로 어떤 역할을 해야할까 고민을 했습니다.

  1. 상위 계층으로 갈 수록 추상화 레벨이 높아야하고

  2. 하위 계층으로 갈 수록 추상화 레벨이 낮아집니다.

핵심은 비즈니스 로직이 다른 기술들에 의해 변동이 생기면 안되도록 해야한다고 생각했습니다.

그래서 컨트롤러 계층에서 서비스 계층에게 dto를 전달할 때나 리포지토리에서 예외가 발생하여 서비스 계층으로 전달 될때도 마찬가지입니다.

특정 기술에 종속된 예외가 발생되지 않도록 스프링 부트가 예외마저 추상화해서 전달하게 됩니다.

그래서 생각을 했을 때 서비스 계층에서는 실제 사용자에게 전달되는 메세지 내용과 어떤 컨트롤러가 요청할 수 있도록 자신이 사용하는 dto를 노출하는 것이 중요하다고 생각했습니다.

 

그리고 3번은 직접 코드로 작성해보면서 느낌점을 나열해보려고 했습니다.

4번은 readable-code에서 말씀해주신 내용을 생각해보면서 그리고 테스트 코드 강의를 생각해보면서 다른 개발자가 이 테스트 코드를 보고 한 번에 이해할 수 있는 test fixture가 될 수 있을지 고민했습니다.

미션 회고

@Test
public void testTransactionManagerActiveWithMocking() {
  Product product = Product.builder().productNumber("001").name("아메리카노").build();
  Order order = Order.builder().orderStatus(OrderStatus.INIT).build();
​
  // saveOrder 모킹 + 트랜잭션 매니저 상태 확인
  doAnswer(invocation -> {
    boolean isTxActive = TransactionSynchronizationManager.isActualTransactionActive();
    System.out.println("트랜잭션" + isTxActive);
    throw new IOException("");
    // 여기서 DB 작업 없이 단순 확인만
​
  }).when(productService).saveOrder(any(Order.class));
​
  // 실행
  productService.saveProductAndOrder(order, product);
​
  // 검증
  verify(productService).saveOrder(order);
}

처음에 저는 테스트 코드를 작성하기 전에 @SpyBean과 원본 객체의 @Transactional이 만나면 @SpyBean이 나중에 적용되는 것으로 알고 있었습니다.

spy(transaction(object)) 구조로 모의 객체빈이 제일 외부에 있어서 지정한 반환값을 설정하면 그 메서드만 호출을 안하니까 트랜잭션이 적용 안되는줄 알고 있었습니다.

 

반전

테스트를 해보면서 transactionalmockito의 기술들은 결국 스프링 생명주기를 따라가게 될텐데 어떻게 우선순위가 정해지는 걸까? 생각을 하면서 실제 테스트 코드에 트랜잭션 매니저에서 트랜잭션이 실행중인지 확인해보려고 로그를 찍어봤으나 동작하지 않길래 제가 기존에 알고 있는 지식이 맞았구나 생각했습니다.

 

그런데 그러면 spy 메서드로 지정한 메서드를 트랜잭션을 적용하려면 어떻게 해야하는건가?

코드레벨로 작성해야하는건가? 이런 생각에 spy으로 프록시를 만드는 beanpostprocessor를 확인해보니 트랜잭션aop보다 먼저 실행되는 생명주기 메서드를 사용했습니다.

 

덕분에 테스트 코드를 작성해보고 역할에 대해서 코드로 실행하다보니 잘못된 정보를 확인할 수 있었습니다.

 

출처

 

강의 링크

Readable Code: 읽기 좋은 코드를 작성하는 사고법

Practical Testing: 실용적인 테스트 가이드


댓글을 작성해보세요.

채널톡 아이콘