이전 글에서 예고한데로, 서비스클래스를 리팩토링하며 테스트코드, 디자인패턴에 관한 내 생각을 정리해보았다.
왜 나는 테스트하기 어려운 코드를 리팩토링 대상으로 선택했을까?
프로젝트에서 테스트코드의 본질은 뭘까? 가장 쉽게 떠오르는 답변은 작성한 코드가 올바르게 수행하는지 확인하는것 일 것이다. 그러면 이렇게 생각해볼 수 있다.
굳이 테스트코드 로만 수행 가능한가? 아마 그렇지 않을 것이다. 코드가 올바르게 수행되는지 확인한다면 굳이 코드로만 확인할 필요는 없을 것이다.
직접 개발자 로컬 환경에서도 확인할 수 있는 수단은 있다. 올바르게 수행되는지 확인하는 것은 테스트코드의 본질이라기 보다는 테스트 자체의 본질일 것이다. 테스트는 반드시 코드로만 실행할 필요는 없기 때문이다. 그런데 프로젝트의 테스트코드를 버젼관리시스템에 관리를 하는 이유는 뭘까?
프로젝트에서 테스트코드의 본질은 비즈니스 로직의 실행보장이라고 생각한다.
그렇다면 테스트코드를 만들기 어렵다는 말은 비즈니스 로직이 매우매우 복잡하거나, 비즈니스 로직이 단순하더라도 코드에 테스트 할 수 없거나, 테스트의 의미가 없는 코드 즉 비즈니스 로직과 관계가 없는 코드가 섞여 있다는 의미라고 생각한다.
진행중인 리팩토링은 어떤 이유로 테스트하기 어려웠을까?
아마 예상했겠지만 직관적인 표현으로 적어보았다.
말 그대로 비즈니스 로직이 뚜렷하지 않았다. 이는 트랜잭션 스크립트 패턴이 적용될 때, 나타나는 현상으로
서비스코드와 DB 처리코드의 일부가 하나의 클래스에 같이 존재했다.
즉 서비스코드의 내부구현 방식이 절차지향적으로 작성되어 있었다. 비즈니스로직이 단순할때는 문제가 없지만 점차 비즈니스로직이 복잡해질 수록 변경하는데 어려움을 겪게된다.
해당 서비스코드에 테스트코드가 존재는 했지만, DB처리를 하는 Mybatis 의 기능을 테스트하는 코드라고 볼 수 있었다.
즉 흔히들 말하는 결합도가 높은 상태인 것이다.
테스트코드의 실행시간은 짧을 수록 좋다. 자주 실행될 수록 테스트코드의 실행시간은 짧아야 한다.
자주 실행되어야 하는 코드가 실행시간이 길다면, 실행 횟수가 줄어들고 점점 테스트코드를 무시하게 된다.
그럼 자주 실행되는데 실행시간이 긴 테스트코드는 없을까?
자주 실행되는 것과 실행시간이 길다 라는 것은 동시에 적용되기 어렵다고 생각한다. 왜냐하면 중요한 비즈니스 로직은 기술적인 요소가 빠져있는것이 일반적이기 때문이다.
자주 실행되어야 하는 코드는 아마도 중요한 코드일 것이다. 중요한 코드는 그 프로젝트에서 가장 중요한 규칙, 즉 돈을 버는 것과 관계되있거나 지출되는 돈(100명이 할일을 처리해준다거나…)을 줄여주는 것과 관련된 코드일 것이다.
이런 내용을 확인하는데 기술적인 디테일은 선택사항일뿐이다. 여기서 선택사항이라는 것은 어떤방식으로 구현되는지 강제하지 않고, 외부의 요인으로 변경될 수 있어야 한다고 생각한다.
즉 중요한 비즈니스 로직이 실행시간이 길다는 것은, 그 로직을 구성하는 코드가 특정한 기술적인 디테일 없이는 테스트 할 수 없다는 것이고, 이는 비즈니스 로직과 기술적인 영역의 결합도가 높다는 의미가 된다.
그래서 테스트코드의 실행시간은 좁게는 개발자의 생산성과 관련있고 넓게 보면 그 프로젝트의 설계를 판단하는 지표가 될 수 있다고 본다.
BuyOptionType
값을 기준으로 코드와 규칙들이 결정된다는 것을 확인했다. BuyOptionItem
을 기반으로 한 여러 기능들이 추가될 예정이기 때문에, 그때의 수정을 위해서라도 코드개선이 필요하다고 판단했다. public class BuyOptionService {
public BuyOptionItem createBuyOption(...){
...
if(type.equals(BuyOptionType.A)
{
//AType관련 코드, 규칙들
}
else if(type.equals(BuyOptionType.B)
{
//BType관련 코드, 규칙들
}
...
}
}
public interface BuyOptionItemStrategy
{
void addItems(...);
void updateItems(...);
}
public class ATypeBuyOptionItemStrategy implements BuyOptionItemStrategy
{
@Override
public void addItems(...)
{
//AType 관련 코드
}
}
public class BuyOptionItem
{
...
//이 메소드를 호출하는쪽에서 Type에 맞는 Strategy 를 메소드 주입으로 호출
public void addItems(BuyOptionItemStrategy strategy, Item item)
{
strategy.addItems(item);
}
...
}
BuyOptionItemStrategy
를 만들고 각각의 Type 별로 인터페이스 구현체를 메소드 주입으로 처리했다. 위의 코드 변경작업을 할때 처음부터 어떤 패턴을 써야겠다 라고 정해놓고 하는 것이 아니기 때문이다. 페어프로그래밍과 코드리뷰를 통해 진행하며 코드의 어떤 부분이 자주 바뀌어왔는지, 그리고 그 부분을 어떤 방식으로 추상화 할지 결정하고 코딩했다.
추상화 되는 부분을 중심으로 테스트코드를 만들어 확인했고, 단위테스트로 만들어 실행시간을 빠르게 하여 검증에 부담을 없앴다.