본문 바로가기
SpringBoot/테스트 코드 | TDD

[Spring] static 메소드 호출이 있는 코드를 테스트 가능하게 만드는 방법 (feat. PSA와 DI)

by rla124 2024. 8. 6.

그동안 프레임워크 공부를 하면서 아쉬웠던 부분은 프레임워크의 본질보다 당장 API 개발에 쓰일 수 있는 "실용성"과 "구현 속도"에 더 우선 순위를 둔 점이다. 이 점을 반성하며 스프링에서 놓치고 있었거나 잘못 이해하고 있었을지 모르는 스프링의 특징을 단단하게 다지고자 한다.

 

GitHub gitHub = GitHub.connect(); GitHub API를 쓰지 않고 테스트 코드를 작성할 수 있는 방법은?

아래는 특정 깃허브 레포지토리의 이름을 input으로 받으면 이 레포지토리의 point를 계산하여 int로 반환하는 코드이다. 

import org.kohsuke.github.*;
import java.io.IOException;

public class RepositoryRank {

	public int getPoint(String repositoryName) throws IOException {
		GitHub gitHub = GitHub.connect(); // static 호출된 이 부분을 테스트 가능하도록 바꿔보자
		GHRepository repository = gitHub.getRepository(repositoryName);

		int points = 0;
		if (repository.hasIssues()) {
			points += 1;
		}

		if (repository.getReadme() != null) {
			points += 1;
		}

		if (repository.getPullRequests(GHIssueState.CLOSED).size() > 0) {
			points += 1;
		}

		points += repository.getStargazersCount();
		points += repository.getForksCount();

		return points;
	}

	public static void main(String[] args) throws IOException {
		RepositoryRank spring = new RepositoryRank();
		int point = spring.getPoint("rla124/Flavor-Profiling");
		System.out.println(point);
	}
}

 

하지만 이 클래스에 대해 테스트 코드를 작성하려고 하면 위 코드 중 아래 코드 한 줄이 테스트 코드를 만드는데 있어 굉장히 불편함을 안겨준다. static 메소드가 아래와 같이 return 값이 있다면 전부 생성자로 받게 해야 하는지, 만약에 return이 없다면 static 메소드 호출을 어떻게 해야하는지에 대한 문제에 직면하게 된다. 하지만 static까지 mocking을 해주는 툴이 있는데 이를 배제하고! mocking을 도와주는 프레임워크 없이 테스트를 가능하게 바꿔보기 위해 알아가는 것이 목적이다. 

GitHub gitHub = GitHub.connect();

 

이러한 상황에서 PSA를 적용해보자!

 

Portable Service Abstraction는 함축적으로 말하면 추상화를 얻는 것이다. 어노테이션과 여러가지 복잡한 인터페이스들 그리고 기술들을 기반으로 사용자가 기존의 코드를 거의 변경하지 않고, 웹기술 스택을 간편하게 바꿀 수 있도록 도와준다는 스프링의 특징 중 하나이다.

이를 얻는 이유로는 앞선 문장이 포함하는 여러 가지 사례들(예를 들어 Spring Web MVC를 사용하면 직접 HttpServlet을 상속 받아 doGet(), doPost()를 구현하는 등 작업을 해주지 않아도 Spring 뒷단에서 제공하는 여러 기능들이 알아서 해준다.. @Controller 사용하면 요청을 매핑할 수 있는 컨트롤러 역할을 수행하는 클래스가 되듯이.. 그리고 프레임워크와 DB 연결에 필요한 JDBC Driver의 경우도 인터페이스로 추상화가 되어있기 때문에 ORACLE이든 MYSQL이든 비즈니스 로직 변경 없이도 다른 서비스로 쉽게 교체 가능하다는 이점이 있다.. 이러한 점이 PSA의 예시이다..) 있지만 테스트하기 어려운 코드를 테스트하기 편리하게 바꿀 수 있다는 것이 큰 장점이다. 

 

PSA를 적용하여 테스트 가능한 코드로 만들기

편의상 인터페이스 등을 별도의 파일에 적는 것이 아니라 같은 코드 파일 내 작성했다.

 

import org.kohsuke.github.*;
import java.io.IOException;

public class RepositoryRank {

	interface GithubService { // 1. 임의의 인터페이스를 정의
		GitHub connect();
	}

	class DefaultGitHubService implements GithubService { // 2. 인터페이스 구현

		@Override
		public GitHub connect() throws IOException {
			return GitHub.connect();
		}
	}

	private GithubService githubService; // 3. 주입받을 수 있는 형태로 만들기 -> DI

	public RepositoryRank(GithubService githubService) { // 3. DI
		this.githubService = githubService;
	}
	public int getPoint(String repositoryName) throws IOException {
		GitHub gitHub = githubService.connect(); // 이제 이 코드는 테스트가 가능한 코드가 된다
		GHRepository repository = gitHub.getRepository(repositoryName);

		int points = 0;
		if (repository.hasIssues()) {
			points += 1;
		}

		if (repository.getReadme() != null) {
			points += 1;
		}

		if (repository.getPullRequests(GHIssueState.CLOSED).size() > 0) {
			points += 1;
		}

		points += repository.getStargazersCount();
		points += repository.getForksCount();

		return points;
	}

	public static void main(String[] args) throws IOException {
		RepositoryRank spring = new RepositoryRank();
		int point = spring.getPoint("rla124/Flavor-Profiling");
		System.out.println(point);
	}
}

 

그러면 이제 테스트 코드를 작성할 때  위 클래스의 필드인 gitHubService를 주입하여 mock GitHub 인스턴스를 만들어 넘겨주면 된다. 

 


static 호출을 처리할 때 모든 static 호출로 받아오는 결과를 다 매개변수로 받아오게 할 수는 없으므로..

return 값이 없는 static 호출이라면 위와 같은 방법대로 인터페이스로 감싸는 방법이 좋다