테스트하기 어려웠던 서비스 클래스

Su hwan Choi·2022년 12월 3일
0

이전 글에서 예고한데로, 서비스클래스를 리팩토링하며 테스트코드, 디자인패턴에 관한 내 생각을 정리해보았다.

왜 테스트 하기 어려운가?

왜 나는 테스트하기 어려운 코드를 리팩토링 대상으로 선택했을까?

테스트코드는 생각보다 많은 역할이 있다

프로젝트에서 테스트코드의 본질은 뭘까? 가장 쉽게 떠오르는 답변은 작성한 코드가 올바르게 수행하는지 확인하는것 일 것이다. 그러면 이렇게 생각해볼 수 있다.
굳이 테스트코드 로만 수행 가능한가? 아마 그렇지 않을 것이다. 코드가 올바르게 수행되는지 확인한다면 굳이 코드로만 확인할 필요는 없을 것이다.
직접 개발자 로컬 환경에서도 확인할 수 있는 수단은 있다. 올바르게 수행되는지 확인하는 것은 테스트코드의 본질이라기 보다는 테스트 자체의 본질일 것이다. 테스트는 반드시 코드로만 실행할 필요는 없기 때문이다. 그런데 프로젝트의 테스트코드를 버젼관리시스템에 관리를 하는 이유는 뭘까?

프로젝트에서 테스트코드의 본질은 비즈니스 로직의 실행보장이라고 생각한다.
그렇다면 테스트코드를 만들기 어렵다는 말은 비즈니스 로직이 매우매우 복잡하거나, 비즈니스 로직이 단순하더라도 코드에 테스트 할 수 없거나, 테스트의 의미가 없는 코드 즉 비즈니스 로직과 관계가 없는 코드가 섞여 있다는 의미라고 생각한다.

진행중인 리팩토링은 어떤 이유로 테스트하기 어려웠을까?

비즈니스 로직이 뚜렷하지 않다.

아마 예상했겠지만 직관적인 표현으로 적어보았다.
말 그대로 비즈니스 로직이 뚜렷하지 않았다. 이는 트랜잭션 스크립트 패턴이 적용될 때, 나타나는 현상으로
서비스코드와 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(...);
}
  • 모든 타입에서 인터페이스 구현을 할 수 있도록, interface 정의한다.
public class ATypeBuyOptionItemStrategy implements BuyOptionItemStrategy
{
	@Override
	public void addItems(...)
	{
    //AType 관련 코드
	}
}
  • 타입별로 위의 interface를 인터페이스구현을 통한 클래스를 만든다.
public class BuyOptionItem
{
	...
	//이 메소드를 호출하는쪽에서 Type에 맞는 Strategy 를 메소드 주입으로 호출
	public void addItems(BuyOptionItemStrategy strategy, Item item)
	{
		strategy.addItems(item);
	}
	...
}
  • 사용하는 메소드는 interface를 파라미터 타입으로 정의하고, 내부에서 interface의 메소드를 호출한다.

  • 처리해야 하는 규칙들의 interface로 BuyOptionItemStrategy 를 만들고 각각의 Type 별로 인터페이스 구현체를 메소드 주입으로 처리했다.

디자인패턴에 관한 생각

  • 디자인패턴은 책보고 따라하는것, 혹은 외우는 것이 아니다.
    • 외워서 쓸 수 있는 것이 아니다. 책에서 예로 들고 있는 내용은, 굉장히 넓은 범위에 적용가능한 것을 예로 들어 설명한다. 그럼 디자인패턴 서적은 어떤 관점에서 봐야 할까?
  • 각각의 패턴에서 강조하는 특징을 알아야 한다.
    • 패턴에서 강조하는 특징이야 말로 그 패턴의 본질이기 때문이다 .
    • 가령 Command 패턴, Strategy 패턴, State 패턴 모두 객체에 어떤 메소드를 캡슐화 한다는 관점에서는 동일하다.
      • Command 패턴은 ‘작업’ 이라는 캡슐화된 객체와 실제 수행하는 객체 사이에 결합도를 줄이는 것이 Command 패턴의 본질이라 본다.
      • Strategy 패턴은 한 객체에서 여러가지 처리 방법중 하나를 선택할 수 있을때 클래스로 캡슐화된 처리방법이 외부에서 주어진다.
      • State 패턴은 처리방법을 내부 객체 상태와 연결해서 처리한다.

결론

위의 코드 변경작업을 할때 처음부터 어떤 패턴을 써야겠다 라고 정해놓고 하는 것이 아니기 때문이다. 페어프로그래밍과 코드리뷰를 통해 진행하며 코드의 어떤 부분이 자주 바뀌어왔는지, 그리고 그 부분을 어떤 방식으로 추상화 할지 결정하고 코딩했다.
추상화 되는 부분을 중심으로 테스트코드를 만들어 확인했고, 단위테스트로 만들어 실행시간을 빠르게 하여 검증에 부담을 없앴다.

0개의 댓글