Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 (1) - 테스트에 대한 개념

조갱·2024년 1월 1일
0

스프링 강의

목록 보기
8/16

https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 인프런 강의에 대한 주관적인 내용입니다.!

강의를 수강하게 된 배경

나는 커머스 도메인에서 주문 관련 개발을 하는 주문파트에 소속되어있다.
그 중에서도, 현재는 가장 복잡한 도메인인 클레임을 주 도메인으로 개발중인데,,
클레임은 진짜 극악무도하게 복잡하다.

정책도 복잡하고, use-case도 너무 복잡하다.

예를 들면, 고객이 아래와 같은 상품을 주문했다.

  • 배송비는 최소부과 (묶음배송이 가능한 상품 중, 가장 적은 배송비를 낸다.)
  • 무료배송 상품 1개 (10,000원)
  • 3만원 이상 조건부 무료배송 상품 2개 (각 5,000원 / 총 10,000원, 원래는 배송비 5,000원)
  • 고정 3000원 유료배송 상품 2개 (각 5,000원 / 총 10,000원)

총 3만원을 구매하여 조건부 무료배송 혜택을 받고, 무료배송이 있기 때문에 최소부과 배송비에 의해 최종 배송비는 0원이다.

근데 무료배송 상품을 반품신청 하면 무료배송 조건도 깨지고, 조건부 무료배송도 깨져서 배송비는 최소부과에 의해 고정 3000원이 부과된다.

이 상태(무료배송 상품의 클레임이 완료되지 않은) 에서 고정 3000원 상품 2개도 반품신청을 하여, 배송비가 5000원 부과되는 중에 무료배송 상품의 클레임을 철회하고... 또... 😵

클레임에서도 배송비 관련된 로직은 머리가 몹시 터져버린다.
그래서 배송비 관련 로직을 수정할때면 손발이 덜덜 떨린다.

이미 테스트코드가 있기는 하지만, 신규 개발된 로직은 테스트코드가 없거나, 빈약한게 사실이다.

그래서 좀더 탄탄한 테스트 코드를 만들고자 했다.

강의 한줄 요약

의존성 역전을 잘 활용하여 좋은 아키텍쳐와 테스트 가능한 환경을 만들자.

강의 세줄 감상평

내용은 너무 좋고, 이상적인데... 초기 프로젝트가 아닌 이미 수백,수천개의 클래스, 수만 줄이 넘는 실무 코드에 적용할 수 있는 방법은 아닌것 같다.

그럼에도 본 강의는 중형(통합)테스트를 소형(단위)테스트로 쪼개는 방법을 실습을 통해 잘 설명해주고 있다.

(중, 대형 서비스의) 실무에서 당장 적용이 필요해서 듣기보다는,
소형서비스에 적용, 테스트에 대한 개념, Spring에서 어떻게 h2, mock 없이 테스트코드를 짤 수 있을까? 에 대한 내용이 필요한 사람들이 들으면 정말 좋은 강의이다 :+1:

강의에서 다루는 내용

테스트의 목적

테스트의 가장 큰 목적은 회귀버그 방지이다.
테스트 코드를 잘 작성한다면, 이러한 회귀버그를 방지할 수 있다.

* 회귀버그 : 이전에 제대로 작동하던 소프트웨어 기능에 문제가 생기는 것

테스트의 필요성

레거시 코드 : 테스트 루틴이 없는 코드 -마이클페더스-

  • 잘 돌아가던 코드가 이번 배포로 인해서 동작하지 않는 상황 방지
    • 회귀(Regression) 버그 방지
    • 개발자들의 코드 수정을 무섭지 않게 방지해준다.
  • 좋은 아키텍처를 유도한다. (SOLID)
    • S (단일 책임 원칙)
      테스트는 명료하고 간단하게 작성해야 하기 때문에, 단일 책임 원칙을 가짐
    • O (개방 폐쇄 원칙)
      테스트 컴포넌트와 프로덕션 컴포넌트를 나눠 작업하게 되고, 필요에 따라 이 컴포넌트를 자유자재로 탈부착 가능하게 개발하게 된다.
    • L (리스코프 치환 원칙)
      이상적으로 테스트는 모든 케이스에 대해 커버하고 있으므로, 서브 클래스에 대한 치환 여부를 테스트가 알아서 판단해준다.
    • I (인터페이스 분리 원칙)
      테스트는 그 자체로 인터페이스를 직접 사용해볼 수 있는 환경.
      불필요한 의존성을 실제로 확인할 수 있는 샌드박스
    • D (의존성 역전 원칙)
      가짜 객체를 이용하여 테스트를 작성하려면, 의존성이 역전되어 있어야 하는 경우가 생김

레거시 프로젝트에 테스트 코드 적용하기

레거시에 테스트를 넣으려면 코드 개선이 필요하다.

  1. 우선, 도메인 단위의 테스트 코드를 작성하여 성공 케이스를 만든다.
  2. 의존성 역전을 통해 프로젝트를 리팩토링 한다.
  3. 리팩토링할 때마다 1번에서 성공했던 테스트 코드를 돌려서, 여전히 성공하는지 검증한다.
  4. 위 123번을 계속 반복해서 전체 프로젝트를 개선한다.

(너무 이상적인 내용이다 ㅠ 이미 수만줄의 코드가 작성된 프로젝트에서는 거의 불가능에 가까운.)

TDD (테스트 주도 개발)

  1. 깨지는 테스트를 먼저 작성한다 (RED 단계)
    -> 테스트 로직을 TODO("Not Implemented") 로 채우더라도, 우선 테스트가 필요한 케이스들을 무지성으로 리스트업 한다.
    -> 테스트가 실패하는지까지 확인한다.
  2. 깨지는 테스트를 성공시킨다 (GREEN 단계)
  3. 리팩토링한다. (BLUE 단계)
    -> GREEN 단계에서 테스트를 성공시켰기 때문에, 리팩토링을 파괴적으로 해도 괜찮다.

장점

  • 깨지는 테스트를 먼저 작성해야하기 때문에, 인터페이스를 먼저 만드는 것이 강제된다.
    -> 인터페이스에 먼저 만드는 것은 객체지향의 핵심 원리인 행동에 집중하게 된다.
    -> 대부분들의 개발자들은 인터페이스를 따르지 않고 구현체부터 만들기 때문.
  • 장기적인 관점에서 개발 비용 감소

단점

  • 초기 개발 비용이 크다.
  • 난이도가 높다.
  • 요구사항이 명확하지 않거나, 프로젝트의 성공 여부가 불확실한 경우에는 쉽지 않다.
    • (예: 스타트업)
    • 이유에 대해서는, 아래 테스트 작성이 용이한 코드의 그래프를 참고해보자.

테스트 작성이 용이한 코드와, 좋은 아키텍쳐와의 관계

아래 사진은, 테스트 코드를 적용했을 때 (붉은색) 와, 적용하지 않았을 때 (파란색)
상황에서 시간에 따른 피쳐 개발 시 부담감을 나타내는 그래프이다. 초반에는 테스트코드를 적용하는 시간이 오래 걸리기 때문에 붉은 (테스트코드 적용) 영역이 부담이 더 크지만, 후반으로 갈 수록 푸른 (테스트코드 미적용) 영역의 부담이 더 크다는걸 알 수 있다.

이제는 좋은 아키텍쳐와 그렇지 않은 아키텍쳐를 적용했을 때, 피쳐 개발의 부담감을 알아보자. 놀랍게도, 아키텍쳐 차이의 그래프는 테스트코드 적용 유무와 동일한 그래프를 가지고있다.
즉, 좋은 아키텍쳐는 테스트 작성이 용이한 코드라고 말할 수 있고, 반대로
테스트 작성이 용이한 코드는 좋은 아키텍쳐라고 말할 수 있다.

테스트의 3분류

테스트는 위와 같이 3개의 분류로 나누어진다. 각 용어별로 의미가 모호하기에, 구글에서는 아래와 같이 분류한다고 한다. (사실 아래가 더 모호한것 같기도 한데. 강의는 그렇다고 한다.)

각 단계별 의미를 살펴보면 아래와 같다.

  • E2E : 대형 테스트 (5%)
    • 멀티 서버 가능.
    • End to End 테스트
  • INTEGRATION : 중형 테스트 (15%)
    • 대형 테스트보다 약간 더 완화된 기준
    • 단일 서버, 멀티 프로세스, 멀티 스레드 사용 가능
    • H2같은 테스트 DB를 사용할 수 있다.
    • 단위 테스트보다 느리다.
    • 멀티 스레드를 사용하기 때문에 결과가 항상 일치하지 않을 수 있다.
      (h2 db가 잘못될 수 있음)
  • UNIT : 소형 테스트 (80%)
    • 단일 서버 / 단일 프로세스 / 단일 스레드에서 돌아가는 테스트
    • Disk IO가 있으면 안되고, Blocking Call 이 있어서도 안된다.
    • 즉, Thread.sleep이 테스트에 있으면 소형 테스트가 아니다.

일반적으로 Spring 으로 웹개발을 하다보면, 중형 테스트가 많은데,
이를 소형 테스트로 쪼개는 작업이 필요하다.
(이 부분은 전적으로 공감한다.)

테스트에 필요한 개념

개념

  • SUT : System Under Test : 테스트 하려는 대상
  • BDD : Behaviour driven development : 어디에 어떻게 테스트를 해야하지? userStory 를 강조함. given-when-then 을 강조. (3A)
  • 상호 작용 테스트 (Interaction Test) : 대상 함수의 구현을 호출하지 않으면서 그 함수가 어떻게 호출되는지를 검증하는 기법
  • 상태 기반 검증 vs 행위 기반 검증
  • 테스트 픽스처 : 테스트에 필요한 자원을 생성하는 것
  • 비욘세 규칙 : 유지하고 싶은 상태나, 정책이 있으면 테스트 코드를 알아서 작성해야 한다.
  • Testability: 테스트 가능성. 소프트웨어가 테스트 가능한 구조인가?
  • Test ouble: 테스트 대역 (가짜객체)

대역 (가짜 객체)

  • Dummy : 아무런 동작도 하지 않고, 그저 코드가 정상적으로 돌아가기 위해 전달하는 객체
  • Fake : Local 에서 사용하거나 테스트에서 사용하기 위해 만들어진 가짜 객체, 자체적인 로직이 있다는게 특징
  • Stub : 미리 준비된 값을 출력하는 객체 (일반적으로 mockito 객체를 사용)
  • Mock : 메소드 호출을 확인하기 위한 객체. 자가 검증 능력을 갖춤. 사실상 테스트 더블과 동일한 의미로 사용됨
  • Spy : 메소드 호출을 전부 기록했다가 나중에 확인하기 위한 객체

의존성

컴퓨터 공학에서 말하는 의존성은 결합이랑 같은 개념이고, 다른 객체의 함수를 사용하는 상태. 즉, A는 B를 사용하기만 해도 의존한다.

의존성 역전

의존성 역전과 주입은 다른 개념이다.
Dependency Injection : 의존성 주입 (DI)
Dependency Inversion : 의존성 역전 (SOLID-DIP)

  1. 의존성 역전은 상위, 하위 모듈 모두 추상화에 의존해야 한다.
    -> 상위 Chef, 하위 Beef가 모두 추상화인 Meat에 의존
  2. 세부 사항에 의존해서는 안되고, 세부 사항이 추상화에 의존해야 한다.
    의존성 역전은 화살표의 방향을 바꾸는 테크닉이다.

의존성과 테스트

테스트를 잘하려면, 의존성 주입과 의존성 역전을 잘 다룰 수 있어야 한다.

SOLID -> DIP : 의존성 역전 원칙

대부분의 소프트웨어 문제는 의존성 역전으로 해결이 가능하다.

문제 상황

해결 방법 (의존성 역전)

프로덕션 환경 로직

테스트 환경 로직

Testability

테스트 가능성 : 얼마나 쉽게 input을 변경하고, output을 쉽게 검증할 수 있는가?

> Testability가 떨어지는 경우

  • 하드코딩 : 파일이 존재하지 않을 때 테스트할 수 없다.
  • 외부 시스템 : 하드 코딩된 외부 시스템과 연동이 되어있는 경우
  • 감춰진 결과 : 외부에서 결과를 볼 수 없는 경우
    (로직 수행 후 단순히 print하여 결과를 출력하는 경우)

엔티티에 대하여

이 강의에서는 Entity를 분리하여 생각해야 한다고 정의하고 있다.

  • 도메인 엔티티 : 비즈니스 영역을 해결하는 모델
  • DB 엔티티 : RDB에 저장되는 객체
  • 영속성 객체 : ORM

RDB를 사용하는 경우

NoSQL을 사용하는 경우

위와 같이, UserEntity는 사용하는 DB의 특성에 따라 달라질 수 있는데,
우리는 일반적으로 JpaEntity를 DomainEntity와 구분짓지 않고 사용하고 있었다.

이렇게 되면, 우리의 코드는 RDB, Jpa에 의존적인 코드가 되어버리기 때문에
DomainEntity <> DB Entity는 분리되어야 한다.

기타 테스트코드 작성 시 조언

  • private 메소드는 테스트할필요 없다.
    -> private 메소드를 테스트하고자 한다면, 설계를 고려해봐야 함
  • final 메소드를 stub하는 상황을 피해야 한다.
  • 테스트에 논리 로직 (for, if,,,)를 넣지 말자
profile
A fast learner.

0개의 댓글