[tdd] TDD의 핵심
소프트웨어 개발은 학습의 과정이다
- 흥미로운 프로젝트(== 가장 큰 이익을 줄 만한 프로젝트)에는
예상치 못한 요소
가 상당히 많다 - 갖가지 중요한 구성 요소가 조합된 시스템은
너무나 복잡
해서, 개인이 해당 시스템의 모든 가능성을이해하기는 어렵다
- 프로젝트에 관련된 모두는 프로젝트가 진행되면서
배우는 것
이 있어야 한다- 프로젝트에서 무엇을
달성해야 하는지
? 잘못 이해
하고 있는 바를 식별해결
하고자협업
해야 한다
- 프로젝트에서 무엇을
피드백은 가장 기본적인 도구다
팀애서 취할 수 있는 가장 좋은 접근법은 경험에 의거한 피드백을 이용해 시스템과 그 용도에 관해 배운 다음, 이렇게 배운 바를 다시 시스템에 적용하는 것이다
그러므로 이러한 피드백을 얻을 수 있는, 반복적인 활동 주기가 필요하다
- 저자는 모든 개발에
초에서 월 단위에 이르는 중첩된 고리형 시스템
의 피드백 주기를 적용했다- 짝 프로그래밍, 단위 테스트, 인수테스트, 일별 회의, 반복 주기, 출시 등을 사용하여 고리를 구성한다
- 하나의 고리마다 산출물이 나오고, 이 산출물이 피드백으로 드러나게 된다
- 팀에서는 빠르게 오해를 발견하고 수정할 수 있다
- 이렇듯, 중첩된 피드백 고리는 서로를 강화한다
안쪽 고리는 기술적 세부 사항
에 좀 더 집중한다- 단위 코드의 역할과 시스템 나머지 부분과의 통합 여부
바깥쪽 고리는 조직과 팀
에 좀 더 집중한다- 사용자의 요구를 충족하는지, 팀이 효과적으로 운영되고 있는지
- 프로젝트의 어떠한 측면에 대해서도 피드백을 일찍 받을수록 좋다
TDD가 왜 중요할까?
시스템 규모를 믿을 수 있는 방식으로 키우고, 늘 일어나는 예상치 못한 변화에 대처하고 싶다면 두가지 기술적인 토대가 필요하다
- 테스트 자동화
- 시스템 규모와 상관없이 수동테스트를 자주 하는것은 비실용적이다
- 회기 오류를 잡아줄 꾸준한 테스트가 필요하다
- 기존 기능을 망가뜨리지 않고 새 기능을 추가할 수 있다
- 꾸준한 리팩토링
- 개발자들은 코드를 작성하는 것보다 코드를 읽는데 훨씬 더 시간을 많이 보낸다
- 그러므로 코드를 가능한 한 단순하게 유지해야 한다
- 단순함에는 노력이 많이 들어간다
- 설계 개선하고 단순화하며, 중복을 제거하며, 코드가 명확하게 자신의 역할을 표현하게끔 코드를 사용할때마다 꾸준히 리팩토링 해야한다
하지만 여기서 문제는, 대부분의 개발자들이 테스트 작성을 업무
로 보지 않으며, 따분하다고 여기기 까지 한다는 것이다.
테스트 주도 개발은 이러한 상황을 근본적으로 뒤집는다.
작업을 완료한 후 작업 결과를 검증하려고 테스트를 작성하는 것이 아니라(Test Last),
코드를 작성하기 전에 테스트를 먼저 작성한다.
즉, 테스트 작성 자체가 설계 활동이 되는 것이다.
- 테스트를 먼저 작성함으로써 코드에서 하고 싶은바에 대한 생각을 명확하게 하고,
- 테스트를 먼저 작성하려는 노력으로 설계 아이디어의 품질에 대한 피드백도 빠르게 얻을 수 있게된다
- 코드를 테스트하기 쉽게 만들면 좀 더 깔끔하고 모듈화된 코드가 만들어지기 때문이다
TDD 간단 정리
TDD 작성 순서는 아래와 같다
다들 잘 아는 Red-Green-Refactor
cycle이다.
- 테스트를 작성한다
- 프로덕션 코드가 없으므로 당연히 실패한다(Red)
- 해당 테스트가 동작하게(Green) 만든다
- 테스트는 건드리지 않고 프로덕션 코드를 리팩토링 한다
- 이를 반복한다
테스트를 먼저 작성함으로써 얻는 이점은 아래와 같다
- 다음 작업에 대한 인수 조건이 명확해진다
- 작업이 끝나는 시점을 스스로 알아내야 하기 때문이다
- 느슨하게 결합된 구성 요소를 작성할 수 있게 된다
- 격리된 상태에서, 더 높은 수준으로, 모두 결합된 상태로 구성 요소를 손쉽게 테스트 할 수 있다
- 코드가 하는 일에 대한 설명이 더해진다
- 테스트 코드는 일종의 스펙 문서 역할도 한다
- 완전한 회귀 스위트가 늘어난다
사실상 2,3,4 번은 Test First Development 보단 단순히 테스트 작성에서 얻을 수 있는 이점들이다.
하지만 언급했다시피, 테스트를 나중에 작성하는 방식은 힘들고, 현실적으로 위의 이점들을 다 가져가지 못할 가능성이 크다.
그리고 테스트를 실행하면 얻는 이점은 아래와 같다
- 컨텍스트를 선명하게 인지하는 동안 오류를 탐지한다(?)
- 언제 작업이 충분히 완료되었는지 알게된다
- ‘금도긋’ 하듯 과도한 최적화를 하거나 불필요한 기능을 더하지 않게 된다
TDD의 황금률(?)
실패하는 테스트 없이는 새 기능을 작성하지 말라
좀 더 큰 그림
어플리케이션 내에 있는 클래스들을 대상으로 단위 테스트를 작성하는 것으로 TDD를 시작해보고 싶을 수 있으나, 결과적으로 단위 테스트만 있는 프로젝트는 TDD 프로세스가 주는 아주 중요한 혜텍을 놓치는 셈이 된다.
좀 더 큰 그림을 봐야한다.
(물론 단위테스트가 없는 프로젝트 보다는 백배 천배 낫다)
기존의 TDD 라이프 사이클에서 앞단에 인수 테스트(Acceptance test) 작성
이라는 부분이 추가 되었다.
(인수테스트-단위테스트-통합테스트-전-구간-테스트)
여기서는 전 구간 테스트를 사용하여 인수 테스트를 수행한다
그리고 TDD의 황금률에, 이 인수 테스트까지 같이해서 코드 작성을 시작한다.
- 실패하는 인수 테스트를 작성한다
- 이 테스트가 실패한다는 말은 아직까지 해당 기능을 구현하지 않았다는 것을 보여준다
- 단위 테스트들을 작성해나가며 기능을 완성해나간다
- 전 구간 테스트이기 때문에, 많은 단위 테스트들을 포함한다
- TDD cycle을 사용하여 단위 기능들을 개발해나간다
- 인수테스트가 통과하면, 작업이 끝난다
- 이때서야 시스템이 배포될 수 있다
외부 품질과 내부 품질
- 외부 품질
- 시스템이 고객과 사용자의 요구를 얼마나 잘 충족하는가(기능, 신뢰성, 가용성, 응답성 등)이다
- 보통 계약의 일부라, 이를 이해하지 못할 사람은 없다
- 전 구간 테스트를 사용한다
- 전 구간 테스트로는 코드를 얼마나 잘 작성했는지는 알 수 없다
- 내부 품질
- 시스템이 개발자와 관리자의 요구를 얼마나 잘 충족하는가(이해하기 쉬운가, 변경하기 쉬운가) 이다
- 외부 품질과 똑같이 중요하지만, 달성하기는 많이 어렵다
- 단위 테스트를 사용한다
- 단위 테스트로는 시스템이 전체적으로 동작하는지를 충분히 확신할 수 없다
높은 내부 품질은 어떻게 달성할 수 있을까?
높은 응집과 낮은 결합
을 유지해야한다- 코드의 동작 방식을 얼마나 쉽게 바꿀 수 있는지를 설명하는 척도이기 때문이다
- 한 요소의 변경이 다른 요소의 변경에 영향을 미친다면, 그 두 요소는
결합
되어 있는 것이다.- 이를 최대한 낮게 유지해야 한다
응집도
는 해당 요소의 책임이 의미있는 단위를 형성하는지 나타내는 척도이다- 날짜와 URL을 파싱하는 기능이 같이 들어간 클래스는 응집도가 낮다고 표현할 수 있다
- 그리고, 두 가지 기능을 다 잘할 가능성이 낮다
- 객체에 대한 단위테스트를 많이 해야한다
- 객체를 생성하고, 객체의 의존성을 제공하며, 객체와 상호 작용하고, 예상대로 동작하는지 검사할 필요가 있다
- 설계를 잘못하면 단위 테스트를 작성하거나 이해하기 어렵다
- 예를 들면 클래스가 멀리 떨어져있는 시스템의 일부와 긴밀하게 결합되어 있거나, 암시적인 의존성이 있거나, 불분명한 책임이 너무 많을때 등이 있다
결과적으로 높은 응집과 낮은 결합
을 가진 객체가 많아야 높은 내부 품질을 유지할 수 있게 되는데, 테스트 작성이 이를 측정하는 기준점이 될 수 있다.
TDD는 테스트를 먼저 작성함으로써, 이러한 설계에 관한 즉각적인 피드백을 바로 얻을 수 있다는 큰 장점이 있다.
[spring] spring batch AsyncTaskExecutor 등록
Job들을 비동기로 실행시키고 싶을 경우 사용한다.
아래와 같이 빈으로 등록한다
1 | @RequiredArgsConstructor |
메서드의 이름이 bean의 이름이 된다.
사용하는 곳은 아래와 같다
1 | @Service |
스프링은 타입 injection을 먼저 수행하고, 똑같은 타입이 있으면 이름으로 injection을 수행한다.
만약 JobLauncher가 asyncJobLauncher 외에 더 등록되어 있다면 injection에서 오류가 발생할테니 위처럼 이름을 지정해줘야 한다.
[tdd] 상태검증과 행위검증, stub과 mock 차이
- SUT(System Under Test) : 주요 객체(primary object)
- 협력객체(collaborator) : 부차적 객체(secondary objects)
- 테스트 더블(Test Double) : 테스팅을 목적으로 진짜 객체대신 사용되는 모든 종류의 위장 객체
- Dummy, Fake Object, Stub, Mock
상태검증 vs 행위검증
https://minslovey.tistory.com/97
-
상태검증은 메서드가 수행된 후 SUT나 협력객체의
상태
를 살펴봄으로써 올바로 동작했는지를 판단하게 된다1
2
3
4SomeClass someClass = new SomeClass();
someClass.someMethod();
assertThat(someMethod.someStatus()).isEqualTo(true);someStatus값이 true인지
상태
검사 -
행위검증은 상태검증과는 다르게 SUT가 협력객체의 특정 메서드가 호출되었지 등의
행위
를 검사함으로써 올바로 동작했는지 판단하게 된다1
2
3SomeClass someClass = new SomeClass();
verify(someClass).someMethod();someClass의 someMethod가 실행되었는지
행위
검사
stub vs mock
http://testing.jabberstory.net/
많은 테스트 더블들이 있지만, 테스트 더블들의 역할이 딱딱 나뉘어져 있지도 않고, 서로가 서로의 특성을 조금씩 포함하므로, 대표적으로 stub과 mock만을 구분한다.
-
stub
- 호출이되면 미리 준비된 답변으로 응답하는 것
- 테스트시에 프로그램된 것 외에는 응답하지 않는다
- 협력객체의 특정 부분이 테스트하기 힘들 경우 stub을 사용하면 수월하게 테스트할 수 있다
- 일반적으로 우리가 mock으로 잘못 알고있다
-
mock
- 다른 테스트더블과는 다르게 행위검증 사용을 추구한다
- 행위를 기록하는 식의 로직이 들어가있겠지…
SUT가 실제 협력객체와 대화하고 있다고 믿게해야하므로, 모든 테스트 더블들의 동작이나 형태는 같다.
하지만 여기서 mock은, 행위검증을 추구한다는 것 자체가 다른 테스트 더블과의 큰 차이점이다.
classicist, mockist
- classicist들은 가능하면 항상 진짜 객체를 사용하고, 진짜 객체를 사용하기 만만치 않으면 더블을 사용한다
- 그리고 항상 상태검증을 사용하려고 한다
- mockist들은 관심있는 행위를 가진 모든 객체에 모의객체를 사용하려고 한다
- 그리고 항상 행위검증을 사용하려고 한다 향
간단한 협력과 간단하지 않은 협력에서의 둘의 선택
- 간단한 협력
- classicist라면 실제 객체를 사용해 상태검증을 할 것이다
- mockist라면 mock을 쓰고 행위검증을 할 것이다
- 간단하지 않은 협력
- mockist라면 당연히 mock을 쓰고 행위검증을 사용한다. 간단하지 않은 협력에서 mock의 장점이 부각(?)되기 때문이다
- classicist라면 상황에 맞춰 가장 쉬운 방법을 사용하려 한다. 테스트 더블이 필요하다면 테스트 더블을 사용할 것이다.
언제 상태검증? 언제 행위검증?
현재까지 이해한 바로 작성해보면,
행위검증의 경우 특정 메서드의 호출
과 같은 것을 검증하기 때문에, 구현에 굉장히 의존적이게 된다
이 말인 즉 프로덕션 코드가 변경되면 테스트코드가 변경될 확률이 높아진다는 것을 의미한다
테스트는 그것을 작성하면서 설계에 피드백을 받을 수 있다는 장점이 있는데(테스트가 어렵다면 설계에 문제가 있는것은 아닌지 의심해보는 과정)
행위검증의 경우 상태검증보다 테스트 작성이 쉬워서 이런 피드백을 받을 기회(?)가 많이 없어지게 된다
강력한 행위검증 라이브러리(powermock)같은 것을 쓰게 되면 이런 부분을 놓치게 될 가능성이 더 크다
예를 들어 powermock의 경우 private 메서드도 테스트할 수 있고, static 클래스도 주입할 수 있는 등 강력한데, 이러한 특징때문에 우리는 설계가 잘못되었다는 의심을 하지 않고 지나갈 확률이 커진다
상태검증의 경우 상태를 검증하기 위해 상태를 노출하는 메서드가 많이 추가될 수 있다
mockist들은 이것을 큰 요소로 여긴다
이러한 이유로 대체적이면 상태검증을 사용하는것이 좋다
하지만 상태검증을 하기 힘든 경우들이 종종 있다
예를 들면 알람
같은 것이다
이런 테스트의 경우, 통합 테스트를 고려해보는 것도 좋다
(알람을 쏘고 로그를 읽어와서 상태검증을 할 수도 있다)
근데 또 이게 단점이 많은게,
전체적인 테스트 시간이 늘어나고(피드백을 받는 시간이 늘어난다), 문제가 발생했을때 찾기가 쉽지 않아진다
SUT가 아닌 다른곳에 의존성이 생길수 있다
이러한 이유 때문에 행위검증을 선택하는 경우도 종종 있다
구현에 의존적이라는 행위검증의 특징을 안고 가는것이 좀 별로긴 하지만, mockito같은 라이브러리를 적절히 사용하고, 테스트를 잘 구성하면 이러한 단점도 어느정도 커버할 수 있다고 한다
[java] interface에 static method
interface에 선언한 static method는 일반적으로 우리가 정의하는 메서드와는 다르다.
- body가 있어야 한다
- implements 한곳에서 override가 불가능하다
1 | interface SomeInterface { |
마치 java8의 default method와 약간 비슷하다. 하지만 static method는 override가 불가능하다는 것이 특징이다.
[linux] port로 process 찾기
https://stackoverflow.com/questions/3855127/find-and-kill-process-locking-port-3000-on-mac
mac, centos7 에서 아래의 명령어로 찾을 수 있다고 함
1 | netstat -vanp tcp | grep 9999 |
뒤에서 4번째 컬럼이 process 번호이다
[db] mysql group_concat
그룹화된 컬럼을 쭉 나열할 때 쓸 수 있는 함수이다
1 | select team, group_concat(id) from user group by team; |
1 | A_team | 1,3,5 |
기본 구분자는 ,
이고, 변경 가능하다
1 | select team, group_concat(id separator '|') from user group by team; |
앞뒤로 문자를 붙일수도 있다
1 | -- 1__, 2__, 3__ |
임시변수를 메서드 호출로 전환
수식의 결과를 저장하는 임시변수가 있을 땐,
그 수식을 빼내어 메서드로 만든 후, 임시변수 참조 부분을 전부 수식으로 교체하자
새로 만든 메서드는 다른 메서드에서도 호출 가능하다
특징
- 메서드 추출을 적용하기 전에 적용해야 한다
- 지역변수가 많을수록 메서드 추출이 힘들어지기 때문이다
- 임시변수를 메서드 호출로 수정하면 클래스 안 모든 메서드가 그 정보에 접근할 수 있다
- 적용하는 가장 간단한 상황은 임시변수에 값이 한번만 대입되고 대입문을 이루는 수식에 문제가 없을 때이다
방법
- 값이 한번만 대입되는 임시변수를 찾는다
- 값이 여러변 대입되는 임시변수가 있으면
임시변수 분리
를 적용한다
- 값이 여러변 대입되는 임시변수가 있으면
- 그 임시변수를 final로 선언한다
- 정말로 임시변수들이 값을 한번만 대입받는지 시험해볼수 있다
- 대입문 우변을 빼내어 메서드로 만든다
- 처음에는 private 으로 만들고, 나중에 더 여러곳에서 사용하게 되면 접근제한을 완화한다
- 추출 메서드에서 문제가 없는지(객체의 값을 변경한다거나) 확인한다
- 임시변수를 대상으로
임시변수 내용 직접 삽입
을 적용한다
예시
-
임시변수의 값을 루프를 돌면서 변경하는 경우가 많다
- 이럴떈 먼저 루프 자체를 메서드로 변경하고, 메서드 수행 결과를 임시변수에 담아 사용한다
- 이 임시변수를 전부 메서드 호출로 변경한다
- 여기서 성능이 느려질수도 있지만, 대체로 문제없다
- 리팩토링을 잘 할수록 더욱 강력한 최적화가 가능하기 때문이다
-
특정 로직의 메서드를 추출하려고 했는데 사용하는 임시변수가 많을 경우, 각 임시변수들에 대해 이 기법을 적용해나가면서 점진적으로 메서드를 분리해낼 수 있다
1
2
3
4
5
6
7
8
9
10
11int getPrice(){
int originalPrice = quantity * itemPrice; // 구매하고 싶은 개수 * 상품 가격
double discountFactor = 1.0;
if(originalPrice > 100000)
discountFactor = 0.8;
else if(originalPrice > 50000)
discountFactor = 0.9;
return originalPrice * discountFactor;
}이 상태에서 discountFactor 구하는 부분을 분리하려면 originalPrice가 전달되어야 하므로,
originalPrice 먼저 분리하고, 이 임시변수를 전부 메서드 호출로 변경한다1
2
3
4
5
6
7
8
9
10
11
12
13
14int getPrice(){
double discountFactor = 1.0;
if(getOriginalPrice() > 100000)
discountFactor = 0.8;
else if(getOriginalPrice() > 50000)
discountFactor = 0.9;
return getOriginalPrice() * discountFactor;
}
int getOriginalPrice(){
return quantity * itemPrice;
}이후 discountFactor 구하는 부분을 손쉽게 분리할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int getPrice(){
return getOriginalPrice() * getDiscountFactor();
}
int getDiscountFactor(){
if(getOriginalPrice() > 100000)
return 0.8;
else if(getOriginalPrice() > 50000)
return 0.9;
else
return 1.0;
}
int getOriginalPrice(){
return quantity * itemPrice;
}