NEXTSTEP 5주차 - 테스트 리팩터링, 그 속으로

gibeom·2023년 3월 21일
0

멘토링

목록 보기
14/15
post-thumbnail

멀고 험난한 리팩터링, 안전하게라도 하자

요즘 개발 커뮤니티에서는 소위 금지어(?)로 불리는 몇 가지 키워드가 있는 것 같다.
"클린 코드", "DDD", "MSA", "헥사고날" 등등....
나도 저런 키워드들 중 제대로 마스터(?)한 것들이 한 개도 없지만, 공통적으로 느껴지는 것은 있다.
저들의 공통 목적은 결국 많은 사람이 하나의 프로젝트로 협업할 때, 더욱 생산성을 높이며 빠르게 개발할 수 있도록 도와주는 친구들인 것 같다.

코드가 클린하지 않고 짠 사람만 알아볼 수 있는 코드라면, 당장 그 기능을 수정하거나 확장해나갈 때 코드 분석부터 시작해야 한다.
하지만 가독성이 좋은 코드라면 더욱 빠르게 기존 코드에 요구 사항을 반영할 수 있을 것이다.
DDD나 MSA 역시 각 도메인별로 모듈과 DB를 분리하여, 독립적으로 개발함으로써 각각의 요구 사항들을 빠르게 쳐내면서 성장한다.
뭐 물론 트랜잭션 관리나 기타 고려해줘야 할 머리 아픈 내용들이 많겠지만, 이건 잠시 미뤄두고 싶다.
헥사고날도 마찬가지로 유스 케이스는 공통적으로 사용함으로써 다양한 외부 환경에 맞게 어댑터를 자유롭게 갈아 끼워 사용하기 좋은 아키텍처다.
따라서 유스케이스와 어댑터 간의 의존성을 분리하여 서로 간의 인터페이스만 정의해준다면 내부 로직들은 따로따로 각개전투(개발)가 가능하다.

결론적으로는 회사에서 가장 중요한 요소는 생산성이기 때문에, 개발 속도를 높이기 위한 여러 가지 방법들을 사용하는 게 아닐까 싶다. 스타트업 같은 경우에는 빠르게 변화에 대응하는 것이 곧 생존과 직결될 테니...


본론으로 들어가자면 어떤 회사에서 프로젝트를 처음 시작한다고 했을 때, 정말 뛰어난 개발자들이 킥오프 시점부터 참여해서 정말 좋은 아키텍처와 설계를 해놓는다면 더할나위 없이 좋겠지만, 웬만한 곳은 그렇지 못할 것이다.
그렇다면 기존 코드를 계속해서 리팩토링해 나가야된다는 것인데, 별다른 안전 장치 없이 곧바로 리팩토링에 들어간다면... 아마 이곳 저곳에서 사이드 이펙트가 많이 날 것이다. (경험담)
또한 위의 키워드들처럼 모듈을 분리하는 경우에도, 별다른 안전 장치가 없다면 참.. 막막할 것이다.
그럼 이 안전 장치를 어떻게 해놓는 것이 좋을까?


백엔드 기준으로 생각해보겠다.
Spring MVC로 구성된 백엔드 API 애플리케이션은 프론트와 분리되어 있는 상태이다.
즉 클라이언트와 서버 간의 약속을 기반으로 각자 각개전투로 개발하는 것이다. 여기에서 약속을 우리는 HTTP 프로토콜이라고 부른다.
그럼 백엔드 기준에서 어떠한 값들로 요청이 들어올 때 약속된 결과값만 문제 없이 내려온다면, 우리는 사이드 이펙트로부터 한결 편해지지 않을까?
물론 약속된 결과값이 반환된다고 해도, 내부적인 동작이 의도한 대로 돌아가는지는 당연히 확인해야겠지만 그건 일단 이후 문제다.
모든 요청에 대한 응답 값이 테스트 코드로부터 보장된다면, 즉 외부 보호 장벽이 둘러싸여 있다면 우리는 더욱 마음 편히 내부를 지지고 볶으면서 리팩토링을 진행할 수 있을 것이다.
이 외부 보호 장벽을 인수 테스트가 해줄 수 있다.
단 여기서 말하는 인수 테스트는 특정 범위와 구현 방법이 딱히 정해져 있기보다는, 테스트 의도에 따라 달라지는 것 같다.
내가 주로 사용하는 인수 테스트는 블랙박스의 성격을 가진 테스트로 내부 구현이 어떻게 돌아가는지는 관심 없고 단지 가장 끝 단에서 요구 사항에 정확하게 동작하는지 확인하는 테스트이다.
그 요구 사항은 프론트엔드와의 약속이 될 수도 있고, PM의 기획서 기반이 될 수도 있고, 업체의 실제 요구 사항 기반이 될 수도 있다.
우리가 만드는 프로그램은 요구사항만 완벽히 동작한다면, 일단 1차적으로는 안심일 테니 말이다.


참 주저리주저리 말이 많았지만, 하고 싶은 말은 다음과 같다.

  • 외부 장벽 테스트를 둘러쌓아 놓자
  • 외부 보호막을 기반으로 점진적으로 리팩토링을 해나가자
  • 외부 보호막으로는 부족하므로 각각의 통합/단위테스트도 보강해나가자
  • 실무에서는 리소스가 부족할테니, 주요 비즈니스 로직만이라도 테스트해놓자.
    • 나머지 테스트들은 버그가 발생했을 때 재발 방지 테스트를 하나 둘 씩 쌓아가면서 테스트 코드를 효율적으로 관리해나가자
    • (당연히 시간날 때 틈틈히 외부 장벽을 만들어 놓는 것이 베스트!)

마지막으로 아래는 ATDD과정의 마지막 주차인 "테스트 리팩터링"이라는 주제를 학습하며 정리해놓은 글이다.

객체 참조

  • 객체 간 참조는 무조건 안좋은 것인가? -> 그렇지 않다.
  • 객체 간 직접적인 참조는 결합도와 복잡도가 증가하지만, 다른 객체로의 탐색(객체 그래프 탐색)이 가능해진다.

직접 참조

  • 하나의 생애주기인 객체들 끼리 의존 관계를 묶어줄 때는 직접 참조가 아주 유용하다
  • 하나의 생애주기라면 아주 빈번하게 두개가 같이 사용될 가능성이 높다
    • 객체 그래프 탐색, 영속성 전이, 고아 객체 처리 등

간접 참조

  • 만약 각자 다른 생애주기의 객체들이라면, 간접 참조를 통해 복잡도를 감소시킬 수 있다
    • 직접 참조를 할 경우 머리아픈 일들을 만날 가능성이 있다 (ex. N+1)

테스트 하기 쉬운 코드로 개발하기

https://www.youtube.com/watch?v=Cz_a2gQp63c&ab_channel=OKKY

테스트 하기 어려운 코드

  • 같은 입력에 항상 같은 결과를 반환하지 않는 코드
  • 외부 상태를 변경하는 코드

어떻게 테스트 하기 쉬운 코드로 만들까?

  • 테스트 하기 쉬운 코드와 어려운 코드를 분리하자
    • 테스트가 어려운 영역을 메서드 외부로 빼고 결과값을 메서드 인자로 받도록 변경
    • 바깥으로 뺀 테스트하기 어려운 영역은 통합 테스트로 커버되도록 진행

인수 테스트 리팩터링

  • 응답 코드만 검증하기 보다는 실제 데이터가 저장되었는지에 대한 검증도 같이 하면 좋음
    • 단, 이렇게 되면 테스트 끼리의 중복된 검증이 일어남
      • ex) 생성 테스트 ↔ 조회 테스트 (생성 후 조회 검증)
    • 중복된 테스트 검증을 묶어서 재사용하는 방법도 좋은 방법이 될 수 있음
      • 수강 신청 요청 + 수강 신청됨 = 강의 수강 신청 되어있음
  • 하지만 검증을 묶는 방법으로는 시나리오를 중복으로 검증한다는 것을 해소할 수 없음
    • 따라서 인수 테스트를 통합함으로써 장점을 챙겨갈 수도 있다
    • 하지만 모든 사이드 케이스를 인수 테스트로 검증하기 보다는, 비즈니스 로직 관련 기능은 단위 테스트로 위임하는 것이 좀 더 효율적이고 경제적으로 관리 가능

레거시 리팩터링

  • 가능하다면 인수 테스트로 외부 장벽을 먼저 둘러놓은 상태에서 프러덕션 코드의 리팩토링을 들어가자
  • 단, 인수테스트가 둘러싸여졌다고 바로 기존 코드를 수정한다면 변경한 부분을 의존하는 부분의 모든 테스트가 빨간불이 들어올 것이다
    1. 리팩토링을 진행할 코드의 테스트 코드를 새로 작성
    2. 기존 코드를 건드리지 않고 리팩토링 코드를 새로 작성 (중복 발생 감수)
      • 테스트 코드가 검증할 수 있는 범위 내에서만 리팩터링 진행
    3. 리팩토링 코드가 완성됐다면 기존 레거시 코드 + 기존 테스트 코드 날려버리기
  • 이렇게 진행한다면 리팩터링 코드가 완성될 때까지 기존 코드는 그대로 살아 있으므로 사이드 이펙트가 일어나지 않음 + 작업 도중 다른 일 하다 와도 됨

테스트 환경

@ExtendWith

  • 단위 테스트간에 공통적으로 사용할 기능을 주입하기 위해 사용

@ExtendWith(MockitoExtension.class)

  • Mockito 기능 사용 가능 (ex. @Mock)
  • 만약 환경을 구성해주지 않으면 어노테이션 주입은 사용 불가하지만
    LineRepository lineRepository = mock(LineRepository.class)로는 Extension 없이도 사용 가능

@ExtendWith(SpringExtension.class)

  • Spring의 컨테이너 사용 가능
  • 컨테이너 사용 가능하니 @MockBean도 주입해서 사용 가능

@SpringBootTest

  • Spring의 컨테이너 사용 가능 + 컨테이너에 프러덕션 빈들을 모두 채워줌
    • @SpringBootApplication을 통해 실행하는 서버의 환경과 동일한 설정
  • 모든 빈을 올리기 때문에 테스트 시간이 오래걸림

@WebMvcTest

  • Presentation 계층만 테스트할 때 사용 (Controller, Interceptor, Resolver 등)

@DataJpaTest

  • Spring Data JPA 사용 시 Repository 관련 빈들만 컨텍스트에 올려줘서 테스트에 사용할 수 있도록 해줌
  • 다른 DB도 사용 가능 (ex. @DataJdbcTest, @DataRedisTest)

질문

  • 통합 테스트 진행 시 특정 빈들만 필요할 때는 @SpringBootTest보다 @ExtendWith(SpringExtension.class)로 선언하고 필요한 빈들만 따로 주입해서 사용하는 방식이 많이 쓰이나요??
    • @ExtendWith(SpringExtension.class)를 사용하면 훨씬 속도가 빠르긴 하겠지만, 실제로 서비스 테스트를 진행한다고 했을 때 일반적으로 5~6개의 빈들에 의존을 하기 때문에 @SpringBootTest를 사용하는것이 좀 더 편할 수도 있다
    • 의존 객체가 적다면 충분히 사용할만한 가치가 있을 것이다 → 비용 절감(속도 증가)

API 문서 자동화

Swagger

  • UI를 지원하여 API Call하여 테스트하는 기능을 지원
  • RestDocs에 비해 기능이 많지만, 프러덕션 코드에 오염이 발생하는 단점

RestDocs

Spring Rest Docs 프로세스

Test 실행 → 문서 조각(Snippets) 발생 → 템플릿에 맞게 스니펫들이 끼워맞춰짐 → 문서로 전환

profile
꾸준함의 가치를 향해 📈

0개의 댓글