phantasmicmeans 기술 블로그

약 500건의 unit test를 작성하고 느낀점 본문

Tech

약 500건의 unit test를 작성하고 느낀점

phantasmicmeans 2022. 7. 5. 01:13

약 1년간 이곳저곳에 약 500건의 unit test (+ 약간의 통합 테스트)를 작성했습니다.

테스트에 대한 깊은 지식은 아직도 없고.. 아래 정도에 도달한 것 같습니다.

 

  • 이제 유닛 테스트를 어떻게 작성해야 하는지 알겠다.
  • 직접 겪어보니 왜 작성해야 하는지 이해가 된다.
  • 왜 테스트 작성이 어려운지 알겠다.

현재의 제가 지금까지 테스트를 작성해오며 느꼈던 점 / 생각했던 것들을 적어봤습니다.

이 생각들은 앞으로도 계속 변할 수 있을 것 같기도 해서 몇 년 후 재밌게 읽어볼 수 있을 것 같습니다.

 

목차는 아래와 같습니다.

 

  • 1. 계층 짬뽕 지름길을 피해 보자
  • 2. Spring Data Repository 너 이놈 ..
  • 3. 넓은 의미의 서비스
  • 4. 테스트가 없는 프로젝트에 신규 테스트 작성은?
  • 5. 현재보다는 과거의 테스트 코드가 좋습니다.
  • 6. 테스트가 없어도 개발 충분히 가능하지 않나?
  • 7. 그럼에도 해볼만한가?

최대한 쉽게 작성해보려 했는데 잘 되었을지는 모르겠습니다 ㅎㅎ

 

들어가며

테스트를 수단에 따라 나누면 아래와 같다고 합니다

출처: Effective Unit Testing

구분 흐름 비고
품질 보증 수단 설계 -> 코딩 -> 테스트 작성 - 코드 작성 후 정확한 구현을 검사
- 코드가 커져도 계속 잘 동작하는지 검사
설계 수단 테스트 작성 -> 코딩 -> 리팩토링 (설계) - 테스트 선행 개발
- 테스트 주도 개발 (TDD)

 

테스트의 가치

  • unit test의 main 목표가 설계는 아니겠지만, 테스트의 가치는 설계 수단으로 사용될 때 더 크다고 합니다.
  • unit test가 설계에 도움을 준다는 사실은 일반적으로 널리 알려져 있습니다.

저는 테스트 작성에 대한 이해도와 지식이 0에 가까웠기에 품질 보증 수단으로 사용하는 것부터 시작했고, 설계 -> 코드 작성 -> 테스트 작성 순서로 진행했습니다.

 

아래글에서도 종종 등장하겠지만, 품질 보증 수단으로 잘 사용하려면 결국 이는 또 설계와 이어집니다.

 

1. 계층 짬뽕 지름길을 피해 보자

일반적으로 계층형 아키텍처에서는 아래와 같은 구조를 가지고, 특정 컴포넌트는 동일 계층의 컴포넌트나 아래 계층의 컴포넌트에만 접근합니다.

 

  • 웹 계층
  • 도메인 비즈니스 계층 (ex. service)
  • 영속성 계층

일정이 촉박하거나, 중요한 작업이 아닌 경우 지름길을 택하게 되고, 대표적으로는 아래와 같이 계층을 건너뛰는 경우가 자주 생깁니다.

 

  • 웹 계층 -> 영속성 계층
  • 웹 계층 -> 도메인 비즈니스 계층 & 영속성 계층 모두

간단한 작업의 경우 이런 방향에 대해 부정적으로 생각하지 않습니다. 오히려 생산성 측면에서 이득을 챙길 건 챙겨야 한다고 생각합니다.

다만 이런 케이스가 점점 비대해지면 아래와 같은 문제들이 생겨납니다.

 

  • 웹 계층에 핵심 비즈니스 로직들 모두 존재하게 됨
  • 웹 계층 테스트에서 영속성 계층까지 mocking 해야 함
  • 웹 계층이 비대해짐 -> dependency를 이해하는 데 시간을 쏟아야함 -> unit test 복잡도가 올라감
  • 웹 계층에서 테스트하지 않아도 될 부분들까지 test 해야 하는 상황이 생김 -> test 하기 어려우니, 과감히 포기

이는 테스트를 작성하지 않게 하는 하나의 원인이 될 수 있기에, 계층 결합을 고려 하지 않는 습관이 생겼습니다.

 

2. Spring Data Repository 너 이놈 ..

모두 공감하시겠지만 개발시에는 이 친구 참 편합니다. 알아서 구현체를 끼워 넣어주니 인터페이스만 extends만 하면 정의된 모든 api를 사용할 수 있습니다.

 

다만 유닛 테스트 작성할 때는 한숨이 나옵니다. 구현해야 할 명세가 너무 많다 보니 stub으로 구현할 수 없기 때문이죠.

영속성 레이어는 stub으로 구현해서 인메모리 형태로 가져가면 유닛 테스트 작성이 수월해지기에 뭔가 방법을 찾고자 했습니다.
(mock이 좋고 나쁘고를 떠나서 항상 mocking 코드를 작성하는 것은 귀찮으니까요ㅠ)

 

클린 아키텍처 (a.k.a 헥사고날 아키텍처) 구현 예제에서 힌트를 찾을 수 있었는데요, 짧게 설명 드리면 아래와 같습니다.

spring data repository를 상속받는 repository를 참조하지 않고, 어댑터에 의해 구현된 인터페이스를 참조하는 방식입니다.
(레이어가 하나 더 생긴다는 단점도 있습니다.)

1. 코드 구성

펼치기
// 자체 정의 repository 인터페이스
public interface CouponEventRepository {
    Mono<CouponEvent> save(CouponEvent event);
    Mono<CouponEvent> findById(String id);
    Mono<CouponEvent> findByEventType(CouponEventType couponEventType);
        ... 
}
// data repository 상속 인터페이스
public interface SpringDataCouponEventRepository extends ReactiveMongoRepository<CouponEvent, String> {
    Mono<CouponEvent> findDistinctByEventTypeIs(CouponEventType couponEventType);
    Flux<CouponEvent> findAllByEndDatetimeGreaterThanEqual(LocalDateTime endDatetime);
        ... 
}
// adapter
@Component
public class CouponEventPersistenceAdapter implements CouponEventRepository {
    private final SpringDataCouponEventRepository couponEventRepository;
    private final ReactiveMongoTemplate template;
    CouponEventPersistenceAdapter(SpringDataCouponEventRepository couponEventRepository, ReactiveMongoTemplate template) {
        this.couponEventRepository = couponEventRepository;
        this.template = template;
    }
    @Override
    public Mono<CouponEvent> save(CouponEvent event) {
        return couponEventRepository.save(event);
    }
    @Override
    public Mono<CouponEvent> findById(String id) {
        return couponEventRepository.findById(id);
    }
    ...
}
// service
@Service
public class CouponEventRegisterService {
    private final CouponEventRepository couponEventRepository;
    public CouponEventRegisterService(CouponEventRepository couponEventRepository) {
        this.couponEventRepository = couponEventRepository;
    }
    public Mono<CouponEvent> registerCouponEvent(final CouponEventRegisterRequest registerRequest) {
        Set<String> channelTarget = registerRequest.getChannelTarget();
        return CouponEventChannelTargetValidator.validate(channelTarget)
                .map(ignore -> CouponEventMapper.INSTANCE.toEntity(registerRequest))
                .flatMap(couponEventRepository::save);
    }
        ... 
}

2. stub 구현

펼치기
public class TestCouponEventRepository implements CouponEventRepository {
    private List<CouponEvent> events = new ArrayList<>();
    @Override
    public Mono<CouponEvent> save(CouponEvent event) {
        events.add(event);
        return Mono.just(event);
    }
    @Override
    public Mono<CouponEvent> findById(String id) {
        for (CouponEvent event : events) {
            if (event.getId().equals(id)) {
                return Mono.just(event);
            }
        }
        return Mono.empty();
    }
    @Override
    public Mono<CouponEvent> findByEventType(CouponEventType couponEventType) {
        List<CouponEvent> result = events.stream()
                .filter(event -> couponEventType.equals(event.getEventType()))
                .collect(Collectors.toList());
        if (result.size() > 1) {
            return Mono.error(new RuntimeException("동일한 이벤트 타입의 쿠폰이 존재합니다."));
        }
        if (result.size() == 1) {
            return Mono.just(result.get(0));
        }
        return Mono.empty();
    }
       ...
}

3. 테스트

펼치기 image

 

관련해서 감명깊게? 읽은 글을 공유드립니다.

3. 넓은 의미의 서비스

사실 서비스 레이어에만 해당하는 이야기는 아니며, 어떤 컴포넌트든 동일한 맥락일 수 있겠지만 편의상 서비스 레이어라고 하겠습니다.
가끔 작업하다 보면 많은 컴포넌트를 참조하는 아주 넓은 의미의 서비스를 생성해냅니다.

 

계층형 아키텍처는 서비스의 너비에 대한 규칙을 강제하지 않기에 많은 유스케이스를 담당하는 아주 넓은 서비스가 만들어지곤 합니다.

출처: 만들면서 배우는 클린 아키텍처

넓은 서비스는 영속성 혹은 동일 계층의 서비스에 많은 의존성을 가지게 됩니다. 의존 컴포넌트가 많다 보니 모든 stub 구현은 쉽지 않습니다.


테스트는 많은 mocking으로 도배 될 것이 분명하고, 결국 내가 지금 테스트를 작성하는 건지.. mocking 연습을 하는 건지.. 현타가 찾아옵니다.

 

테스트 작성이 많은 피로감을 동반하기에 과감히 작성하지 않게 되며, 이는 테스트를 작성하지 않게 하는 큰 원인이지 않을까 싶습니다.

 

4. 테스트가 없는 프로젝트에 신규 테스트 작성은?

이 항목도 결국 테스트 용이성(설계)에 귀결되는 부분이라고 생각합니다.

제 생각은 요렇습니다

  • 새로 짜는 코드는 테스트를 작성한다.
  • 기존 코드는 이해도가 올라올 때까지 작성하지 않는다 > 수동 테스트한다.

테스트가 없던 코드는 결국 테스트 하기 힘든 구조를 만들어내 왔을 가능성이 높을 겁니다.

 

  • 이는 필연적이라 생각하고, 이게 좋다/나쁘다의 이야기는 절대 아닙니다 (오해하시면 안 됩니다)
  • 유닛 간의 종속성이 적절히 쪼개지도록 설계가 되어야 유닛 테스트 작성이 수월합니다. (커플링이 낮아야) 
    • 유닛 테스트를 계속 작성했더라도 이는 매우 어려운 부분입니다.
    • 그렇다고 App의 모든 코드가 독립적으로 동작하는 것은 말이 안 됩니다.
    • 이런 설계를 더 잘해보려고 TDD를 하기도 합니다.

많은 수단(e.g. powermock, reflection 등)을 동원하면 어떤 방법으로든 지지고 볶아서 테스트할 수 있겠지만, 아마 테스트 작성에 대부분의 시간을 쏟게 될 것으로 예상되고 엄청난 의지가 필요하다고 봅니다.

 

결국, 테스트 작성이 쉬워지려면 테스트 용이성(설계)을 갖춘 코드가 필요합니다.

 

간단하게는 아래부터 시작할 수 있겠죠

따라서 이해도를 먼저 올린 후 코드를 변경하며 테스트를 새로 작성하는것이 합리적인 방안이지 않을까 생각합니다.

(레거시 프로젝트를 전체 리팩토링한다거나 그런 결의 이야기는 아닙니다)

 

5. 현재보다는 과거의 테스트 코드가 좋습니다.

지금 작성하고 있는 테스트가 버그를 당장 획기적으로 줄여준다고 생각하지는 않습니다.

신규 코드에 대해 테스트를 작성했음에도 불구하고 곧바로 버그가 발견될 수 있습니다. 잘못 작성했거나 케이스를 놓쳤다는 이야기겠죠.


테스트 작성도 결국 사람이 하는것이기에 실수도 있을뿐더러 빼먹고 작성하지 않는 케이스도 많습니다.

다만 잘 작성된 과거의 테스트 케이스들은 현재 작업에 아주 큰 도움이 됩니다.

 

저는 이런 효과를 톡톡히 봤는데요.

 

수치상으로 말씀드리기는 힘든 영역이지만, 지난 1년을 떠올려보면 일정과 피로감이 몰리던 시즌에 제 목숨?을 여러 번 살려주었습니다

동일 코드를 평생 혼자 개발하는 것이 아니기에, 신규 인력에게 좋은 방어막을 형성해 주겠죠.

 

항상 여유롭게 개발한다면 많은 엣지 케이스들과 히스토리를 복합해서 고려할 수 있겠지만, 여유롭게 개발할 수 있는 시즌은 얼마 없겠죠..?

 

6. 테스트가 없어도 개발 충분히 가능하지 않나?

이전에는 당연히 가능하다고 생각했고 현재도 가능하다고 생각합니다.
다만 현재는 아래 전제가 추가되었습니다.

  • 프로젝트 오너가 존재하고, 히스토리에 약한 신규 인력이 작업하더라도 오너가 체크해줄 여력이 있다.
  • 프로젝트가 어느 정도 사이즈에서 크게 달라지지 않는다.

테스트 케이스 없이 참조(사용처)와 히스토리가 많은공통 모듈 혹은 공통 로직을 수정해야 할 때 생각의 변화입니다.

 

  • 1년 전
    • 크게 무서울 건 없다. 나의 하루는 48시간이고, 나는 할 수 있다!
    • 시간을 두고 충분히 영향도 파악 > 해볼 만큼 테스트 후 QA 진행
    • QA 절차에서 버그가 발견되면 수정 > 발견되지 않는다면.. 나는 어쨌든 최선을 다했다
  • 현재:
    • 100bp 금리 인상
    • 기존 작업자에게 문의한다. 변경 가능할까요? > Nope! > 새 로직을 작성한다
    • 시간이 남으니 다른 일도 가능하다. 1석 2조다

물론 잘 모르는 프로젝트라는 전제입니다.

7. 그럼에도 해볼만한가?

테스트가 단순히 버그 막는 용도에서 끝나지 않는다는 사실을 깨닫고 나니 충분히 해볼만하다고 생각합니다 (홍보는 아니구요ㅎㅎ)
개발자로 살아감에 있어서 꼭 필요한 수양 중에 하나이지 않나? 라는 생각도 있습니다.

(몇 년 후 생각이 달라질 수 있어요)

 

유닛 테스트는 어떤 방법이든 결국 설계와 맞닿게 됩니다.

 

신규 코드 기준으로.. 코드 작성 후 -> 유닛 테스트를 작성하면 테스트 작성이 어려워지는 순간이 자주 찾아옵니다.

그때마다 코드를 돌아보면 설계가 틀어졌다거나, 과도한 책임을 가지고 있다거나.. 등 코드에서 문제를 찾을 수 있는 케이스가 대부분일거라고 생각합니다. (제가 매번 하는 경험입니다)

 

결국 유닛 테스트는 요구사항을 해결하기 위해 어떤 객체와 어떻게 협력해야 하는지 / 해당 객체가 올바른 책임을 지고는 있는지 / 명세보다는 구현에 집중하진 않았는지 등 평소보다 더 깊은 수준의 사고를 경험케 합니다.

 

이 과정들이 더 나은 소프트웨어를 향해 가는 데 많은 도움이 될 것이라 믿습니다.

 

마치며

처음 테스트를 시작할 때는.. 공부도 하고 적용도 해보며 팀원들에게 전파해야겠다 라는 생각을 하곤 했었는데 ㅎㅎ
테스트라는 것이 굉장히 심오한 주제라는 것을 깨닫고 아직 멀었구나 라는 생각을 합니다.

 

주절주절 쪼렙 개발자가 이상한 소리 좀 많이 했네요 ㅎㅎ


저와 비슷한 생각을 했다가 생각이 바뀌게 된 분이 계신다면 관련해서 편하게 말씀 주셔도 좋습니다

마무리하겠습니다. 읽어주셔서 감사합니다!

'Tech' 카테고리의 다른 글

유닛테스트 3장- 단위 테스트 구조  (0) 2023.07.10
BFS로 서비스 스펙 나름 재밌게 친 썰  (0) 2022.07.05
MongoDB CDC, Change Streams  (0) 2021.11.12
Neo4j memory 구성  (0) 2020.07.01
Kafka  (0) 2020.06.03
Comments