티스토리 뷰

728x90
반응형

예전 글에서 도메인을 중심에 둔 디자인 접근방식인 도메인 주도 설계에 대해 본 적이 있다.

 

2022.09.10 - [개발/Spring] - [Spring]도메인 주도 설계(Domain Driven Design, DDD)

 

[Spring]도메인 주도 설계(Domain Driven Design, DDD)

예전에 Spring Data JDBC에 대해 다루며 DDD에 대해 잠깐 언급한 적이 있다. 2022.08.29 - [개발/Spring] - [Spring]Spring Data JDBC - 도메인 엔티티&테이블 설계 [Spring]Spring Data JDBC - 도메인 엔티티&테..

gnidinger.tistory.com

이번 글에선 테스트가 중심이 된 개발 프로세스인 테스트 주도 개발(Test Driven Development, TDD)에 대해 살핀다.

 

Test Driven Development, TDD

TDD란 이름 그대로 테스트를 중심에 둔 소프트웨어 개발 프로세스이다.

 

조금 더 구체적으로 위 그림을 보자면 TDD는

 

를 핵심 가치로 해서

 

  1. 요구사항을 검증하는 자동화된 테스트 케이스 작성
  2. 케이스를 통과하기 위한 최소한의 코드 생성
  3. 작성한 코드를 표준에 맞도록 리팩터링

과 같은 짧은 개발 사이클을 반복하며 앱을 구현하는 방식을 말한다.

 

독특한 점은 모아진 요구사항을 바탕으로 코드를 작성하고 테스트 케이스를 작성하는 것이 아닌

 

테스트 케이스를 먼저 작성하고 검증을 통과하는 코드를 작성하는 방식을 취하고 있다는 점이다.

 

Conventional Development Workflow

 

위에서 잠깐 언급한 대로, 전통적인 개발 방식은 TDD와는 사뭇 다른 모습을 보인다.

 

보는 관점에 따라 더 합리적으로 느껴지기도 하는 전통적인 방식은 대략 아래와 같다.

 

  1. Case Study - 요구사항 수집

    • 이해관계자(도메인 전문가, 개발자 등)이 모여 서비스 컨셉과 그에 따른 요구사항 수집
    • 요구사항에 맞춰 UI 설계와 함께 구체적인 기능 정의
  2. Development Area - 요구사항에 맞춘 앱 디자인

    • 요구사항과 UI 설계서를 기반으로 도메인 모델 도출
    • 도메인 모델을 기반으로 각 계층에 맞는 클래스와 인터페이스 설계로 앱에 대한 큰 그림 작성
    • 큰 그림을 바탕으로 클래스와 인터페이스의 틀 작성
    • 클래스와 인터페이스 내에 메서드 정의 및 코드 구현
    • 테스트 코드 작성 및 테스트
    • 디버깅

코드를 먼저 구현하고 테스트 케이스를 나중에 작성하는 방식이다.

 

실제로 구현하지도 않은 코드에 대한 테스트 케이스를 작성한다는 게 쉽지 않기에

 

여전히 많은 곳에서 쓰이고 있는 패턴이라 할 수 있다.

 

TDD Workflow

 

TDD는 전통적인 방식을 뒤집은 순서도를 보인다.

 

꾸준히 언급했듯이 

 

라는 가치 아래, TDD의 개발 방식은 아래와 같은 순서를 보인다.

 

글 초입에 적었던 

 

  1. 요구사항을 검증하는 자동화된 테스트 케이스 작성
  2. 케이스를 통과하기 위한 최소한의 코드 생성
  3. 작성한 코드를 표준에 맞도록 리팩터링

을 잘 적용하고 있는 것을 확인할 수 있다.

 

추가로 두 순서도를 비교하면 아래와 같이 된다.

 

 

Validator Test with TDD

 

계속해서 하나의 기능을 구현하며 실제적인 TDD 개발에 대해 알아보자.

 

이번 글에서 구현할 기능은 비밀번호 유효성 검증인데, 기준은 일반적으로 쓰이는 비밀번호 생성 규칙과 크게 다르지 않다.

 

  • 길이(length)는 8~20 사이
  • 알파벳 소문자 + 알파벳 대문자 + 숫자 + 특수 문자 형태로 구성
  • 알파벳 /소문자와 숫자를 제외한 모든 문자는 특수문자라 가정

위 조건을 모두 만족해야만 유효성 검증이 통과된다.

 

먼저 테스트 패키지에 tdd 하위 패키지를 생성한 후, 테스트 클래스와 테스트 케이스의 이름을 정한다.

public class PasswordValidatorTest {
    @DisplayName("Password Validation Test: 모든 조건에 만족")
    @Test
    public void validatePassword() {
    }
}

 

Add a Test

 

이어서 단순히 모든 검증을 통과시키는 테스트를 작성하자.

 

테스트 데이터(Abcd1234!)는 모든 검증에 통과하는 문자열로 정했다.

 

TDD에서는 이처럼 모든 조건을 만족하는 테스트를 먼저 진행하고

 

단계적으로 조건에 맞지 않는 테스트를 진행하며 테스트와 로직을 완성해간다.

 

그런데 위 사진엔 존재하지 않는 클래스를 이용했기 때문에 붉은 글씨로 에러가 표시되고 있다.

 

컴파일 에러 해결을 위해 위에 사용한 클래스와 메서드를 생성한다.

public class PasswordValidator {
    public void validate(String password) {
    }
}

위 사진과 같이 에러가 사라지는 것을 확인할 수 있다.

 

이 상태로 테스트를 진행하면 당연하게도 "passed"를 만날 수 있게 된다.

 

이런 식으로 TDD에선 에러를 잡고 테스트에 통과할 만큼의 코드만 순서대로 작성한다.

 

Create a Failing Unit Test

 

계속해서 검증에 실패하는 코드를 만들어보자.

 

위에선 모든 조건에 만족하는 테스트를 적었으니 이번엔 비밀번호에서 특수문자가 빠지는 경우에 대해 작성한다.

 

public class PasswordValidatorTest {
    ...

    @DisplayName("특수문자 포함 안 됨")
    @Test
    public void validatePasswordWithoutSpecialCharacter() {
        // given
        String password = "Abcd1234";

        // when
        PasswordValidator validator = new PasswordValidator();
        Executable executable = () -> validator.validate(password);

        // then
        assertDoesNotThrow(executable);
    }
}

특수문자가 없는 경우를 위한 테스트 케이스를 추가했다.

 

추가로 특수문자가 포함되지 않을 경우 실패하는 테스를 구현한다.

public class PasswordValidator {
    public void validate(String password) {
        boolean containSpecialCharacter = password.chars() // 검증
                .anyMatch(ch -> !(Character.isDigit(ch) || Character.isAlphabetic(ch)));

        if(!containSpecialCharacter) {
            throw new RuntimeException("Invalid Password"); // 예외 던지기
        }
    }
}

특수문자가 포함되었는지 검증한 후 포함되지 않았을 경우 예외를 던지도록 구현했다.

 

위와 같이 구현한 후 테스트 케이스를 실행하면 "failed"를 만날 수 있다.

public class PasswordValidatorTest {
    ...

    @DisplayName("특수문자 포함 안 됨")
    @Test
    public void validatePasswordWithoutSpecialCharacter() {
        // given
        String password1 = "Abcd1234!"; 
        String password2 = "Abcd1234"; // 특수문자 제거

        // when
        PasswordValidator validator = new PasswordValidator();
        Executable executable1 = () -> validator.validate(password1);
        Executable executable2 = () -> validator.validate(password2);

        // then
        assertDoesNotThrow(executable1);
        assertDoesNotThrow(executable2);
    }
}

테스트 케이스 검증을 위해 조건을 만족하는 케이스를 추가해 검증한다.

 

Refactor

 

위의 Validator 클래스를 아래와 같이 정규표현식을 이용해 깔끔하게 리팩터링 한다.

public class PasswordValidator {
    public void validate(String password) {

        if(!Pattern.matches("(?=.*\\W)(?=\\S+$).+", password)) {
            throw new RuntimeException("Invalid Password");
        }
//        boolean containSpecialCharacter = password.chars()
//                .anyMatch(ch -> !(Character.isDigit(ch) || Character.isAlphabetic(ch)));
//
//        if(!containSpecialCharacter) {
//            throw new RuntimeException("Invalid Password");
//        }
    }
}

코드 수정 후 다시 테스트를 실행해 영향이 있는지 살핀다.

 

이런식으로 비밀번호가

 

  • 특수문자 + 알파벳 소문자를 포함하는지 테스트, 검증, 리팩터링
  • 특수문자 + 알파벳 소문자 + 알파벳 대문자를 포함하는지 테스트, 검증, 리팩터링
  • 특수문자 + 알파벳 소문자 + 알파벳 대문자 + 숫자를 포함하는지 테스트, 검증, 리팩터링
  • 특수문자 + 알파벳 소문자 + 알파벳 대문자 + 숫자를 포함하며 길이가 8~20인지 테스트, 검증, 리팩터링

하는 식으로 처음 그림의 사이클을 돌며 앱을 완성시켜나가면 된다.

 

 

Summary

 

Features

 

  • 모든 조건을 만족하는 테스트를 먼저 진행, 단계적으로 실패하는 테스트를 진행하며 테스트와 로직을 완성
  • 실패하는 테스트의 에러를 잡고 테스트에 통과할 만큼의 코드만 순서대로 작성
  • 리팩터링
  • 반복

 

Adventages

 

  • 테스트를 통과할 코드만 작성하기 때문에 한 번에 많은 기능을 구현할 필요가 없음
  • 사이클을 돌며 검증 범위가 넓어짐에 따라 기능 구현도 함께 완성됨
  • 작은 단위의 리팩터링으로 리팩터링에 소요되는 비용 감소
  • 꾸준한 리팩터링으로 코드 품질 유지 및 협력 증진
  • 코드 수정과 테스트 사이의 간격이 좁아 피드백이 빠름

 

Disadvantages

 

  • 상대적으로 익숙하지 않음
  • 익숙하지 않음에서 나오는 생산성 저하
  • 단순한 앱일수록 뻔한 테스트 케이스의 반복이 됨
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함