public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
boolean lengthEnough = s.length() >= 8
boolean containsNum = meetsContainingNumberCriteria(s);
boolean containsUpp = meetsContainingUppercaseCriteria(s);
if (lengthEnough && !containsNum && !containsUpp)return PasswordStrength.WEAK;
if (!lengthEnough && containsNum && !containsUpp)return PasswordStrength.WEAK;
if (!lengthEnough && !containsNum && containsUpp)return PasswordStrength.WEAK; // 추가된 코드
if (!lengthEnough) return PasswordStrength.NORMAL;
if (!containsNum) return PasswordStrength.NORMAL;
if (!containsUpp) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
모든 테스트가 통과하였다.
코드 정리: meter() 메서드 리팩토링
이제 코드를 정리해보자. meter() 메서드의 if 구문이 가독성이 그리 좋아 보이진 않는다.
if (lengthEnough && !containsNum && !containsUpp)return PasswordStrength.WEAK;
if (!lengthEnough && containsNum && !containsUpp)return PasswordStrength.WEAK;
if (!lengthEnough && !containsNum && containsUpp)return PasswordStrength.WEAK;
이 코드는 세 조건 중에서 한 조건만 충족하는 경우 암호 강도가 약하다는 것을 구현한 것이다. if 절은 세 조건 중에서 한 조건만 충족한다는 것을 확인하는 것이다. 그렇다면 다음과 같이 충족하는 조건 개수를 사용하면 어떨까?
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
int metCounts = 0;
boolean lengthEnough = s.length() >= 8
if(lengthEnough) metCounts++;
boolean containsNum = meetsContainingNumberCriteria(s);
if(containsNum) metCounts++;
boolean containsUpp = meetsContainingUppercaseCriteria(s);
if(containsUpp) metCounts++;
if (metCounts == 1)return PasswordStrength.WEAK; // 이전의 if 구문을 하나로 제거하였다.
if (!lengthEnough) return PasswordStrength.NORMAL;
if (!containsNum) return PasswordStrength.NORMAL;
if (!containsUpp) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
다음 코드는 어떻게 리팩토링할 수 있을까?
if (!lengthEnough) return PasswordStrength.NORMAL;
if (!containsNum) return PasswordStrength.NORMAL;
if (!containsUpp) return PasswordStrength.NORMAL;
코드의 의도는 충족하는 조건이 두 개인 경우 암호 강도가 보통이라는 규칙을 표현한 것이다. 즉, 위 코드를 다음과 같이 변경할 수 있다.
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
int metCounts = 0;
boolean lengthEnough = s.length() >= 8
if(lengthEnough) metCounts++;
boolean containsNum = meetsContainingNumberCriteria(s);
if(containsNum) metCounts++;
boolean containsUpp = meetsContainingUppercaseCriteria(s);
if(containsUpp) metCounts++;
if (metCounts == 1)return PasswordStrength.WEAK;
if (metCounts == 2)return PasswordStrength.NORMAL; // 이전의 if 구문을 하나로 제거하였다.
return PasswordStrength.STRONG;
}
이는 조금 더 리팩토링 할 수 있을것 같다.
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
int metCounts = 0;
if(s.length() >= 8) metCounts++;
if(meetsContainingNumberCriteria(s)) metCounts++;
if(meetsContainingUppercaseCriteria(s)) metCounts++;
if (metCounts == 1)return PasswordStrength.WEAK;
if (metCounts == 2)return PasswordStrength.NORMAL; // 이전의 if 구문을 하나로 제거하였다.
return PasswordStrength.STRONG;
}
테스트를 실행해보면 테스트에 실패할 것이다. 이 테스트를 통과시키려면 다음 중 한 가지 방법을 사용하면 된다.
충족 개수가 1개 이하인 경우 WEAK를 리턴하도록 수정
충족 개수가 0개인 경우 WEAK를 리턴하는 코드 수정
충족 개수가 3개인 경우 STRONG을 리턴하는 코드를 추가하고 마지막에 WEAK를 리턴하도록 코드 수정
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
int metCounts = 0;
if(s.length() >= 8) metCounts++;
if(meetsContainingNumberCriteria(s)) metCounts++;
if(meetsContainingUppercaseCriteria(s)) metCounts++;
if (metCounts <= 1)return PasswordStrength.WEAK;
if (metCounts == 2)return PasswordStrength.NORMAL; // 이전의 if 구문을 하나로 제거하였다.
return PasswordStrength.STRONG;
}
지금까지 새로운 테스트를 추가하거나 기존 코드를 수정하면 습관처럼 테스트를 실행했다. 그리고 실패한 테스트가 있다면 그 테스트를 통과시키기 위한 코드를 추가했다. 테스트를 실행하는 것이 어색하지 않을 때까지 반복하자
코드 정리: 코드 가독성 개선
metCounts 변수를 계산하는 부분을 수정해보자
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
int metCounts = getMetCriteriaCounts(s);
if (metCounts <= 1)return PasswordStrength.WEAK;
if (metCounts == 2)return PasswordStrength.NORMAL; // 이전의 if 구문을 하나로 제거하였다.
return PasswordStrength.STRONG;
}
private int getMetCriteriaCounts(String s) {
int metCounts = 0;
if (s.length() >= 8) metCounts++;
if (meetsContainingNumberCriteria(s)) metCounts++;
if (meetsContainingUppercaseCriteria(s)) metCounts++;
return metCounts;
}
meter() 메서드의 가독성이 좋아졌다. meter() 메서드를 처음보는 개발자도 다음과 같이 코드를 읽을 수 있다.
해당 폴더 경로는 배포 대상이 아니므로 이 두 소스 파일을 src/main/java로 이동해야 비로서 구현이 끝난다.
TDD 흐름
TDD는 기능을 검증하는 테스트를 먼저 작성한다.
작성한 테스트를 통과하지 못하면 테스트를 통과할 만큼만 코드를 작성한다.
테스트를 통과한 뒤에는 개선할 코드가 있으면 리팩토링 한다.
리팩토링을 수행한 뒤에는 다시 테스트를 실행해서 기존 기능이 망가지지 않았는지 확인한다.
이 과정을 반복하면서 점진적으로 기능을 완성해 나가는 것이 전형적인 TDD의 흐름이다.
레드 - 그린 - 리팩터
TDD 사이클은 레드(Red) - 그린(Green) - 리팩터(Refactor)로 부르기도 한다. 여기서 레드는 실패하는 테스트를 의미한다. 레드는 테스트 코드가 실패하면 빨간색을 이용해서 실패한 테스트를 보여주는 데서 비롯했다. 비슷하게 그린은 성공한 테스트를 의미한다. 즉 코드를 구현해서 실패하는 테스트를 통과시키는 것을 뜻한다. 마지막으로 리팩터는 이름 그대로 리팩토링 과정을 의미한다.
테스트가 개발을 주도
테스트 코드를 먼저 작성하면 테스트가 개발을 주도하게 된다.
테스트를 작성하는 과정에서 구현을 생각하지 않았다.
단지 해당 기능이 올바르게 동작하는지 검증할 수 있는 테스트 코드를 만들었을 뿐이다.
테스트를 추가한 뒤에는 테스트를 통과시킬 만큼 기능을 구현했다.
아직 추가하지 않은 테스트를 고려해서 구현하지 않았다.
테스트 코드를 만들면 다음 개발 범위가 정해진다.
테스트 코드가 추가되면서 검증하는 범위가 넓어질수록 구현도 점점 완성되어 간다.
지속적인 코드 정리
구현을 완료한 뒤에는 리팩토링을 진행했다.
당장 리팩토링할 대상이나 어떻게 리팩토링해야 할지 생각나지 않으면 다음 테스트를 진행했다.
테스트 코드 자체도 리팩토링 대상에 넣었다.
당장 리팩토링을 하지 않더라도 테스트 코드가 있으면 리팩토링을 보다 과감하게 진행할 수 있다.
잘 동작하는 코드를 수정하는 것은 심리적으로 불안감을 주기 때문에 코드 수정을 꺼리게 만든다.
하지만 해당 기능이 온전하게 동작한다는 것을 검증해주는 테스트가 있으면 코드 수정에 대한 심리적 불안감을 줄여준다.
TDD는 개발 과정에서 지속적으로 코드 정리를 하므로 코드 품질이 급격히 나빠지지 않게 막아주는 효과가 있다.
빠른 피드백
TDD가 주는 이점은 코드 수정에 대한 피드백이 빠르다.
새로운 코드를 추가하거나 기존 코드를 수정하면 테스트를 돌려서 해당 코드가 올바른지 바로 확인할 수 있다.