TDD 책을 읽고 적용해보자

ollie·2023년 9월 12일
1

배경 🐈

자바 문제를 풀다가 작은 단위 코드들이 올바르게 동작하는지 그때그때 확인하고 싶다는 생각이 들었습니다. TDD를 적용하면 좋을 것 같아서 '테스트 주도 개발 시작하기'란 책을 읽고 코드에 적용해보게 되었습니다.


TDD

'테스트 주도 개발 시작하기' 라는 책을 읽고 이를 바탕으로 내용을 정리했습니다.

TDD 사용 이전 개발

  • 기능 구현에 대한 설계(어떤 클래스와 인터페이스, 메서드를 구현할지 고민)를 하고, 전체 구현을 한 다음 기능을 테스트하는 방식을 수행
  • 올바르게 동작하지 않으면 전체 코드 안에서 디버깅하며 이유를 찾는 방식
    • 한 번에 구현하는 코드가 많을 수록 디버깅 시 스트레스 발생❗
  • 최초의 코드 작성 시간보다 버그 찾는 시간이 더 오래 걸림

TDD 개념

  • 테스트 주도 개발(Test-Driven Development)의 약자
  • 간단히 선 테스트 후 기능 구현
  • 기능 검증하는 테스트 코드 작성 후 테스트를 통과시키기 위한 기능 구현

TDD 적용 방법

  1. 테스트 코드 먼저 짜기
    • 구현 클래스와 메서드에 대한 고민을 먼저 하는 것이 포인트!
    • 클래스 이름과 메서드 이름, 파라미터, 파라미터 타입, 반환값, 정적 메서드 여부에 대한 고민을 하고 그걸 바탕으로 테스트 코드 구현
  2. test 코드와 함께 클래스와 메서드 구현
    • 이때 로직을 넣지말고 정답을 하드 코딩을 리턴 ( return 3 )
  3. 테스트 코드에 검증 코드 추가
    ( asssertEquals(5. Calculator.plus(4, 1)) )
  4. 메서드에 기능 구현
public class CalculatorTest {
	@Test
	void plus(){
		int result = Calculator.plus(1, 2);
		assertEquals(3, result);
		//asssertEquals(5. Calculator.plus(4, 1));
	}
}

public class Calculator {
	public static int plus(int a1, int a2){
		return 3;
        //return 5;
		//return a1 + a2;
	}
}
  • TDD는 테스트 코드를 먼저 작성하는 과정에서 테스트 대상이 될 클래스 이름, 메서드 이름, 파라미터 개수, 리턴 타입, 정적 메서드 여부를 고민했고, 이 과정은 실제 코드를 설계하는 과정과 유사합니다.
  • TDD는 테스트를 먼저 작성해서 테스트를 실패하면 테스트를 통과할만큼 코드를 추가하는 과정을 반복하면서 점진적으로 기능을 완성해나가는 과정입니다.

TDD 적용 예시 ( 암호 검사기 )

암호 검사기 기능을 구현하는 과정에서 TDD를 써봅시다.

암호 검사기

  • 문자열 검사하여 규칙 준수하는지에 따라 암호 ( 약함, 보통, 강함 ) 구분
  • 조건
    • 길이가 8글자 이상
    • 0 부터 9 사이의 숫자 포함
    • 대문자 포함
    • 세 규칙을 모두 충족하면 암호는 강함
    • 2개의 규칙을 충족하면 암호는 보통
    • 1개 이하의 규칙을 충족하면 암호는 약함

[테스트 1] 모든 규칙을 충족하는 경우

  • 첫번째 테스트는 가장 쉽거나 가장 예외적인 상황을 선택해야 합니다.
    • 가장 구현할 것이 적은 것 먼저 고르는 것이 좋습니다.
    • 첫 번째 구현 때부터 구현할 것이 많으면 난이도가 너무 높아지기 때문에 지양합니다.
  • 테스트 코드는 딱 컴파일 에러를 제거할 만큼만 구현합니다.
public class PasswordStrengthMeterTest {
    
    @Test
    void 모든조건충족_강함(){
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("ab12!@AB");
        Assertions.assertThat(PasswordStrength.STRONG).isEqualTo(result);
    }
}
public enum PasswordStrength {
    STRONG
}
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
		return PasswordStrength.STRONG;
    }
}

[테스트 2] 글자 길이만 8미만이고 나머지 조건은 충족하는 경우

  • 글자 길이만 8미만이고 나머지 조건을 충족하는 경우, 암호 강도 ‘보통’
	@DisplayName("8글자만 미충족할 경우, 암호 강도 보통")
    @Test
    void 글자수만_미충족_보통(){
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("ab12!@A");
        Assertions.assertThat(PasswordStrength.NOMAL).isEqualTo(result);
    }
public enum PasswordStrength {
    STRONG, NOMAL
}
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        if(s.length() < 8) {
            return PasswordStrength.NOMAL;
        }
        return PasswordStrength.STRONG;
    }
}

[테스트 3] 숫자를 포함하지 않고 나머지 조건은 충족하는 경우

  • 숫자를 포함하지 않고 나머지 조건을 충족하는 경우, 암호 강도 ‘보통’
	@DisplayName("숫자만 미충족할 경우, 암호 강도 보통")
    @Test
    void 숫자만_미충족_보통() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("abcd!@AB");
        Assertions.assertThat(PasswordStrength.NOMAL).isEqualTo(result);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        if(s.length() < 8) {
            return PasswordStrength.NOMAL;
        }
        if(notContainNumber) {
            return PasswordStrength.NOMAL;
        }
        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
}

테스트 코드 정리

  • 지금까지 작성한 3개의 테스트 코드를 유지보수 해줍시다.
  • TDD는 테스트 코드를 통과하면 통과한 코드들의 중복된 코드를 대상으로 지속적인 리팩토링이 가능 합니다.
  • 테스트 코드도 코드 ❗ 따라서 유지보수의 대상입니다. ❗
    • 중복을 제거하거나 더 가독성 있게 수정할 필요가 있습니다.
public class PasswordStrengthMeterTest {
    private PasswordStrengthMeter meter = new PasswordStrengthMeter();
    
		private void assertStrength(String password, PasswordStrength expStr){
        PasswordStrength result = meter.meter(password);
        Assertions.assertThat(expStr).isEqualTo(result);
    }

    @Test
    void 모든조건충족_강함(){
        assertStrength("ab12!@AB", PasswordStrength.STRONG);
    }

    @DisplayName("8글자만 미충족할 경우, 암호 강도 보통")
    @Test
    void 글자수만_미충족_보통(){
        assertStrength("ab12!@A", PasswordStrength.NOMAL);
        PasswordStrength result = meter.meter("ab12!@A");
        Assertions.assertThat(PasswordStrength.NOMAL).isEqualTo(result);
    }
    @DisplayName("숫자만 미충족할 경우, 암호 강도 보통")
    @Test
    void 숫자만_미충족_보통() {
        assertStrength("abcd!@AB", PasswordStrength.NOMAL);
    }
}

[테스트 4] 값이 없는 경우

  • 아예 값이 없는 경우의 IllegalArgumentException 에러를 발생시킵니다.
	@DisplayName("null값이 들어온 경우, 에러를 터뜨린다.")
    @Test
    void null값_에러(){
        Assertions.assertThatThrownBy(() ->
            meter.meter("")
        ).isInstanceOf(IllegalArgumentException.class).hasMessage("비밀번호를 입력해주세요");
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(s.length() < 8) {
            return PasswordStrength.NOMAL;
        }
        if(notContainNumber) {
            return PasswordStrength.NOMAL;
        }
        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
}

[테스트 5] 대문자를 포함하지 않고 나머지 조건을 충족하는 경우

  • 대문자를 포함하지 않고 나머지 조건을 충족하는 경우, 암호 강도 ‘보통’
	@DisplayName("대문자만 미충족할 경우, 암호 강도 보통")
    @Test
    void 대문자만_미충족_보통() {
        assertStrength("ab12!@ab", PasswordStrength.NOMAL);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        boolean notContainUpperCase = !checkUpperCase(s);
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(s.length() < 8) {
            return PasswordStrength.NOMAL;
        }
        if(notContainNumber) {
            return PasswordStrength.NOMAL;
        }
        if(notContainUpperCase) {
            return PasswordStrength.NOMAL;
        }
        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

[테스트 6] 길이가 8글자 이상인 조건만 충족하는 경우

  • 길이가 8글자 이상인 조건만 충족하는 경우, 암호 강도 ‘약함’
	@DisplayName("8글자만 충족하는 경우, 암호 강도 약함")
    @Test
    void 글자수만_충족_약함(){
        assertStrength("aaaaaaaa", PasswordStrength.WEAK);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        boolean notContainUpperCase = !checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(enoughLength && notContainNumber && notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength) return PasswordStrength.NOMAL;
        if(notContainNumber) return PasswordStrength.NOMAL;
        if(notContainUpperCase) return PasswordStrength.NOMAL;

        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

[테스트 7] 숫자 포함 조건만 충족하는 경우

  • 숫자 포함 조건만 충족하는 경우, 암호 강도 ‘약함’
	@DisplayName("숫자만 충족하는 경우, 암호 강도 약함")
    @Test
    void 숫자만_충족_약함(){
        assertStrength("aa123", PasswordStrength.WEAK);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        boolean notContainUpperCase = !checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(enoughLength && notContainNumber && notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength && !notContainNumber && notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength) return PasswordStrength.NOMAL;
        if(notContainNumber) return PasswordStrength.NOMAL;
        if(notContainUpperCase) return PasswordStrength.NOMAL;

        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

[테스트 8] 대문자 포함 조건만 충족하는 경우

  • 대문자 포함 조건만 충족하는 경우, 암호 강도 ‘약함’
	@DisplayName("대문자만 충족하는 경우, 암호 강도 약함")
    @Test
    void 대문자만_충족_약함(){
        assertStrength("abBB", PasswordStrength.WEAK);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        boolean notContainNumber = !checkNumber(s);
        boolean notContainUpperCase = !checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(enoughLength && notContainNumber && notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength && !notContainNumber && notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength && notContainNumber && !notContainUpperCase) return PasswordStrength.WEAK;
        if(!enoughLength) return PasswordStrength.NOMAL;
        if(notContainNumber) return PasswordStrength.NOMAL;
        if(notContainUpperCase) return PasswordStrength.NOMAL;

        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

meter() 메서드 리팩토링

  • notContainxxx을 쓰는 게 가독성 떨어져서 containxxx로 수정합니다.
  • if 문 줄이기 위해 책에서 나온 아이디어인 metCount 가져와서 사용해봅니다.
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        int metCount = 0;
        boolean containNumber = checkNumber(s);
        boolean containUpperCase = checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;

        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(enoughLength) metCount++;
        if(containNumber) metCount++;
        if(containUpperCase) metCount++;

        if(metCount == 1) return PasswordStrength.WEAK;
        if(metCount == 2) return PasswordStrength.NOMAL;
        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

[테스트 9] 아무 조건도 충족하지 않는 경우

  • 아무 조건도 충족하지 않는 경우(0개), 암호 강도 ‘약함’
	@DisplayName("아무 조건도 충족하지 않는 경우, 암호 강도 약함")
    @Test
    void 모든조건_미충족_약함(){
        assertStrength("abs", PasswordStrength.WEAK);
    }
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        int metCount = 0;
        boolean containNumber = checkNumber(s);
        boolean containUpperCase = checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;

        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(enoughLength) metCount++;
        if(containNumber) metCount++;
        if(containUpperCase) metCount++;

        if(metCount <= 1) return PasswordStrength.WEAK;
        if(metCount == 2) return PasswordStrength.NOMAL;
        return PasswordStrength.STRONG;
    }
    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
}

코드 가독성 개선

  • metCount 계산 로직을 빼서 메서드로 만들자는 책의 아이디어를 가져와서 사용합니다.
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s){
        if(s.isBlank()) {
            throw new IllegalArgumentException("비밀번호를 입력해주세요");
        }
        if(getMetCount(s) <= 1) return PasswordStrength.WEAK;
        if(getMetCount(s) == 2) return PasswordStrength.NOMAL;
        return PasswordStrength.STRONG;
    }

    private int getMetCount(String s) {
        int metCount = 0;
        boolean containNumber = checkNumber(s);
        boolean containUpperCase = checkUpperCase(s);
        boolean enoughLength = s.length() >= 8;

        if(enoughLength) metCount++;
        if(containNumber) metCount++;
        if(containUpperCase) metCount++;
        return metCount;
    }

    private boolean checkNumber(String s){
        for(char c: s.toCharArray()){
            if(Character.isDigit(c)) {
                return true;
            }
        }
        return false;
    }
    private boolean checkUpperCase(String s) {
        for(char c: s.toCharArray()){
            if(Character.isUpperCase(c)) {
                return true;
            }
        }
        return false;
    }
    
}

Lotto 문제에 TDD 적용하기

Lotto 문제 레포지토리

당첨 번호 조건

Lotto 문제 중 당첨 번호의 경우, 아래와 같은 조건이 있습니다. 이 조건을 바탕으로 TDD를 적용해보겠습니다.

  • 숫자 범위 1 ~ 45 확인
  • 서로 다른 숫자 7개 확인
    • 예외 발생 시, IllegalArgumentException 발생 후 [ERROR]로 시작하는 메시지 출력

구현해야 할 테스트 코드 조건

구현해야 할 테스트 조건에 대해 정리해보면 다음과 같습니다.

  • 빈값 여부
  • 숫자만으로 이루어졌는지 여부 확인
  • 숫자 범위 1 ~ 45 확인
  • 서로 다른 숫자 6개 확인
public class NumberValidateTest {
    
    private void assertLuckNumber(String luckyNumber, String errorMessage){
        assertThatThrownBy(()->
                new LuckyNumber(luckyNumber))
                .isInstanceOf(IllegalArgumentException.class).hasMessage(errorMessage);

    }
    
    @DisplayName("null값 예외")
    @Test
    void null값_예외(){
        assertLuckNumber("", CommonErrorMessage.PAYMENT_BLANK_ERROR);
    }
    
    @DisplayName("숫자 조건 미충족 예외")
    @Test
    void 숫자로만_미충족_예외(){
        assertLuckNumber("1,2,3,4,보아,6", CommonErrorMessage.PAYMENT_NUMBER_ERROR);
    }
    
    @DisplayName("숫자 범위 미충족 예외")
    @Test
    void 숫자범위_미충족_예외(){
        assertLuckNumber("1,2,3,4,5,100", NumberErrorMessage.NUMBER_RANGE_ERROR);
     }

    @DisplayName("서로 다른 숫자 미충족 예외")
    @Test
    void 서로다른숫자_미충족_예외(){
        assertLuckNumber("1,1,2,3,4,5", NumberErrorMessage.NUMBER_SAME_ERROR);
    }
    
    @DisplayName("6개 숫자 미충족 예외")
    @Test
    void 숫자갯수_미충족_예외(){
        assertLuckNumber("1,2,3,4,5",NumberErrorMessage.NUMBER_LENGTH_ERROR);
    }
    
    @DisplayName("당첨 번호가 모든 조건 충족")
    @Test
    void 당첨번호_모든조건_충족(){
        Assertions.assertDoesNotThrow(()->
                new LuckyNumber("1,2,3,4,5,6"));
    }
}
public class LuckyNumber {
    private final List<Integer> luckyNumber;

    public LuckyNumber(String userLuckyNumber){
        validate(userLuckyNumber);
        this.luckyNumber = convertToList(userLuckyNumber);
    }
    public List<Integer> getLuckyNumber(){
        return luckyNumber;
    }

    private List<Integer> convertToList(String userLuckyNumber) {
        ArrayList<Integer> result = new ArrayList<>();
        String[] splitUnit = userLuckyNumber.split(",");
        for(String number: splitUnit) {
            result.add(Integer.parseInt(number));
        }
        return result;
    }

    private void validate(String userLuckyNumber){
        List<Integer> result = new ArrayList<>();
        String[] splitUnit = userLuckyNumber.split(",");

        if(isBlank(userLuckyNumber)) throw new IllegalArgumentException(CommonErrorMessage.PAYMENT_BLANK_ERROR);
        if(areNotSixDigits(result)) throw new IllegalArgumentException(NumberErrorMessage.NUMBER_LENGTH_ERROR);
        if(hasSameDigit(result)) throw new IllegalArgumentException(NumberErrorMessage.NUMBER_SAME_ERROR);
        for(String num: splitUnit) {
            checkOnlyNumber(num);
            checkNumberRange(result, num);
        }
    }

    private static void checkNumberRange(List<Integer> result, String num) {
        int number = Integer.parseInt(num);
        if(isNotInRange(number)) throw new IllegalArgumentException(NumberErrorMessage.NUMBER_RANGE_ERROR);
        result.add(number);
    }

    private void checkOnlyNumber(String num) {
        if(isNotNumber(num)) throw new IllegalArgumentException(CommonErrorMessage.PAYMENT_NUMBER_ERROR);
    }

    private static boolean isNotInRange(int number) {
        return number < 1 || number > 45;
    }

    private static boolean isBlank(String userLuckyNumber) {
        return userLuckyNumber.isBlank();
    }

    private static boolean areNotSixDigits(List<Integer> result) {
        return result.size() != 6;
    }

    private boolean isNotNumber(String number){
        return !number.matches("\\d+");
    }

    private boolean hasSameDigit(List<Integer> number){
        HashSet<Object> temp = new HashSet<>();
        for(int num : number){
            temp.add(num);
        }
        if(temp.size()!=6) return true;
        return false;
    }
}
public class NumberErrorMessage {
    public static final String NUMBER_RANGE_ERROR = "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.";
    public static final String NUMBER_SAME_ERROR = "[ERROR] 서로 다른 숫자로 이루어져 있어야 합니다.";
    public static final String NUMBER_LENGTH_ERROR = "[ERROR] 로또 번호는 6개로 이루어져 있어야 합니다.";
    public static final String BOUNS_NUMBER_SAME_ERROR = "[ERROR] 보너스 번호는 로또 번호와 다른 숫자여야 합니다.";
}
public class CommonErrorMessage {
    public static final String PAYMENT_NUMBER_ERROR = "[ERROR] 숫자로만 이루어져 있어야 합니다.";
    public static final String PAYMENT_BLANK_ERROR = "[ERROR] 값이 없습니다.";
}

느낀점 💡

이 책을 보기 전에 'TDD를 하면 기능과 테스트 개발을 같이 하게 되어 시간이 2배가 드는 일인데, 모듈이 잘 돌아간다는 신뢰성을 획득하게 되더라도 과연 그것만으로 TDD를 하는 것이 맞을까?'라고 TDD의 필요성에 대해 의문점이 들었습니다. 그 부분에 대해 책은 우리가 개발하는 코드들이 '소프트웨어 품질'이고, TDD는 코드가 의도대로 동작하지 않아 서비스의 품질이 떨어뜨리는 것을 막는 효과적인 방법이라고 설명하는 것 같았습니다.

그리고 기존에 생성한 테스트 코드는 서비스를 운영하는 과정에서 새로운 기능 추가 등의 과정을 거칠 때 기존 코드가 작동하지 않는다는 등의 문제를 방지하는 도구로서 존재합니다. 서비스의 품질을 유지시키는 중요한 도구이기 때문에 제품 코드와 동일하게 유지 보수의 대상이 된다는 사실을 알게 되습니다.

개발이 단순히 기능을 구현한다기보다 사용자에게 제공하는 일정 이상의 품질을 유지해야 하는 제품이라고 이해하게 되면서 TDD가 강조되는 이유를 깨닫게 되는 시간이었습니다.

profile
생각하는 개발자가 되겠습니다 💡

0개의 댓글