백기선님의 영상을 통해 psa 개념을 배웠다

Portable Service Abstraction

사실 스프링을 공부한 지 얼마 되지 않은 사람으로서, psa 라는 개념에 생소하였다. 아래의 블로그 글을 통해 psa 개념을 이해하는 데에 도움을 받을 수 있었다.

Spring psa

Spring PSA 는 Portable Service Abstraction 의 약자로 휴대용 서비스 추상화라는 의미를 가진다. 우선, 서비스 추상화란 무엇일까? 특정 서비스가 추상화되어있다는 것은 서비스의 내용을 모르더라도 해당 서비스를 이용할 수 있다는 것을 의미한다. 예를 들어, 우리는 JDBC Driver 를 사용해 데이터베이스에 접근하지만 JDBC Driver 가 어떻게 구현되어 있는지는 관심이 없다. 실제 구현부를 추상화 계층으로 숨기고 핵심적인 요소만 개발자에게 제공함으로써 실제 구현부를 모르더라도 해당 서비스를 이용할 수 있도록 하는 것이다.

그러면 이러한 서비스 추상화에 Portable 이 붙으면 무엇을 의미하는지 이야기해보자. Portable 은 휴대용이라는 의미로 JDBC Driver 의 종류를 비즈니스 로직의 수정없이 언제든지 변경할 수 있는 것을 의미한다. 즉, MySQL Driver 를 사용하다가 어느순간 Oracle Driver 로 변경한다고 해서 프로젝트의 비즈니스 로직에 변화가 없다는 것이다. 이런 기능이 가능한 것은 추상화 계층이 존재하기 때문이다. 모든 JDBC Driver 는 공통적인 인터페이스를 가지고 있기 때문에 해당 인터페이스를 구현하는 어떤 것으로 대체되든 프로젝트에 영향이 없어지는 것이다.



백기선님의 영상 예제 코드

test 가 용이하지 못한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.kohsuke.github.*;
import java.io.IOException;

public class RepositoryRank {
	public int getPoint (String repositoryName) throws IOException {
		GitHub gitHub = GitHub.connect();
		GitHubRepository 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 repositoryRank = new RepositoryRank();
		int point = repositoryRank.getPoint("whiteship/live-study");
		System.out.println(point);
	}
}

getPoint() 메소드에서 Github 객체를 직접 선언하여 사용하고 있다. 이에 대한 test 코드를 작성할 때 발생하는 문제는 다음과 같다.

  • 해당 Github 라이브러리를 직접 호출할 경우
    • 무겁다
    • API 호출 횟수에 제한이 있을 수도 있다
  • Mock 객체를 만들어 사용하려고 할 경우
    • 이를 해당 getPoint() 메소드에 연결시킬 방법이 마땅치가 않다.

psa 를 적용하여 testability 를 높인 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.kohsuke.github.*;
import java.io.IOException;

public class RepositoryRank {
	
	interface GitHubService {
		GitHub connect() throws IOException;
	}
	
	class DefaultGitHubService implements GitHubService {
		@Override
		public GitHub connect() throws IOException {
			return GitHub.connect();
		}
	}

	private GitHubService gitHubService;
	public RepositoryRank (GitHubService gitHubService) {
		this.gitHubService = gitHubService;
	}

	public int getPoint(String repositoryName) throws IOException {
		GitHub gitHub = gitHubService.connect();
		GitHubRepository 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 repositoryRank = new RepositoryRank(new DefaultGitHubService());
		int point = repositoryRank.getPoint("whiteship/live-study");
		System.out.println(point);
	}
}
  • GithubService 인터페이스를 선언하고, 이를 구현하는 DefaultGitHubService 클래스를 만든다.
  • RepositoryRank 의 인스턴스를 만들 때, 생성자 주입을 통해 GithubService 객체를 주입해준다.

  • 이렇게 하면 테스트 코드에서 무엇이 개선될까?
    • 이전 예시와 다르게 이렇게 생성자 주입을 통해 GithubService 를 주입받기 때문에 테스트 코드에서 GithubService 에 해당하는 mock 객체를 만들고 이를 RepositoryRank 클래스를 선언할 때 인자로 넘겨주면 된다.
    • 아래의 테스트 코드와 같이 mock 클래스들을 만들고, 해당 객체들의 리턴값을 설정해준 뒤 RepositoryRank 클래스의 인자로 넘겨주었다.

example test code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.junit.Test;
import java.io.IOException;
import static org.mockito.Mockito.*;

public class RepositoryRankTest {

    @Test
    public void testGetPoint() throws IOException {
        GitHubService gitHubService = mock(GitHubService.class);
        GitHub gitHub = mock(GitHub.class);
        GHRepository repository = mock(GHRepository.class);
        when(gitHubService.connect()).thenReturn(gitHub);
        when(gitHub.getRepository("whiteship/live-study")).thenReturn(repository);
        when(repository.hasIssues()).thenReturn(true);
        when(repository.getReadme()).thenReturn(mock(GHContent.class));
        when(repository.getPullRequests(GHIssueState.CLOSED)).thenReturn(mock(GHPagedIterable.class));
        when(repository.getStargazersCount()).thenReturn(1);
        when(repository.getForksCount()).thenReturn(1);

        RepositoryRank repositoryRank = new RepositoryRank(gitHubService);
        int point = repositoryRank.getPoint("whiteship/live-study");
        assertEquals(5, point);
    }

}

comments powered by Disqus