avatar
Octoping Blog

HashMap으로 Repository 모킹 간편하게 하기

반복되는 Repository Stub 코드 줄이기
BackendJavaSpringTest Code
Nov 6
·
6 min read

public class ArticleService {
    private final ArticleRepository articleRepository;
    private final UserRepository userRepository;

    public Article writeArticle(ArticleWriteRequest request) {
        // ...
    }
}

Layered Architecture와 함께 Spring으로 서비스를 구현하다보면 필연적으로 Repository 의존성이 생기게 된다. 그리하면 당연히 서비스의 테스트를 짜기 위해서는 Repository의 의존성을 주입해주어야 한다.

class ArticleServiceTest {
    @Test
    void writeArticleTest() {
        // given
        ArticleRepository repository = mock(ArticleRepository::class);
        when(repository.save(any())).thenReturn(
            Article.builder()
                .id(1)
                .title("title")
                .content("content")
                .build()
        );

        ArticleService articleService = new ArticleService(
            repository,
            mock(UserRepository::class)
        );
        
        // when
        articleService.writeArticle(
            new ArticleWriteRequest("title", "content")
        );

        // then
        assertThat(repository.findAll()).hasSize(1);
        assertThat(repository.findById(1).getTitle()).isEqualTo("title");
    }
}

하지만 서비스의 테스트를 하기 위해 매번 @SpringBootTest를 할 수 없는 노릇이고, 매번 이렇게 모킹을 할 경우 매번 when을 통해 Repository의 동작을 작성해주어야 하므로 실제로 Service가 제대로 동작하는지를 확인하기 조금 까다롭다.

public class ArticleMockRepository implements ArticleRepository {
    private final Map<Long, Article> articleMap = new HashMap<>();

    public Article save(Article a) {
        articleMap.put(a);
        return a;
    }
    
    public Optional<Article> findById(long id) {
        return Optional.ofNullable(articleMap .get(id));
    }

    public List<Article> findAll() {
        return List.copyOf(articleMap.values());
    }

    public void clear() {
        map.clear();
    }
}

그렇기 때문에 나는 Stub을 통해 진짜 객체를 넣어주는 것을 선호한다. 레포지토리는 HashMap을 통해 구현할 수 있으니!

하지만 Repository는 비즈니스 객체의 갯수만큼 존재할 것이고, 이걸 매번 다 짜려면 굉장히 귀찮은데.

그러니 제네릭을 활용하여 공통화를 해보자.

제네릭을 사용해 공통화하기

public class HashMapRepository<T> {
    private final Map<Long, T> map = new HashMap<>();

    protected void save(long id, T entity) {
        map.put(id, entity);
    }

    public Optional<T> findById(long id) {
        return Optional.ofNullable(map.get(id));
    }

    public List<T> findAll() {
        return List.copyOf(map.values());
    }

    // 원하는 조회 조건 생성용
    protected Optional<T> findBy(Predicate<T> predicate) {
        return map.values().stream()
                .filter(predicate)
                .findFirst();
    }

    protected List<T> findAllBy(Predicate<T> predicate) {
        return map.values().stream()
                .filter(predicate)
                .toList();
    }

    public void clear() {
        map.clear();
    }
}

다음과 같이 HashMapRepository를 구현할 수 있다. 이렇게 구현한다면 이제 각 도메인 별로 레포지토리를 만들기는 간단하다.

class ArticleMockRepository extends HashMapRepository<Article> implements ArticleRepository {
    @Override
    public Article save(Article article) {
        super.save(article.getId(), article);
        return article;
    }

    @Override
    public Optional<Article> findByAuthorId(long authorId) {
        return findBy(it -> it.getAuthorId() == authorId);
    }
}

다음과 같이 save만 재정의해주면 된다.

만약 원하는 조회 조건이 필요하다면, findBy, findAllBy를 이용해서 원하는 조건을 Predicate 로 넣어주는 방식을 이용해서 추가해주면 된다.

적용과 함께 마무리

class ArticleServiceTest {
    @Test
    void writeArticleTest() {
        // given
        ArticleRepository repository = new ArticleMockRepository();

        ArticleService articleService = new ArticleService(
            repository,
            new UserMockRepository()
        );
        
        // when
        articleService.writeArticle(
            new ArticleWriteRequest("title", "content")
        );

        // then
        assertThat(repository.findAll()).hasSize(1);
        assertThat(repository.findById(1).getTitle()).isEqualTo("title");
    }
}

꽤나 깔끔한 모양새로 테스트를 작성할 수 있게 됐다.

구현해야 할 메소드가 너무 많아요

1985

이런 식으로 Stub 객체를 만드려고 했더니 ArticleRepository 안의 인터페이스가 너무 많아서 모두 구현하기 힘들 수도 있다. ( JpaRepository 를 그대로 상속받아서 사용 중인 경우 특히 그렇다)

하지만 그렇다고 Stub을 포기하고 Mock을 시도하는 것은, 테스트 코드가 알려주는 코드 악취를 모킹이라는 강력한 도구로 감추고 숨는 것에 지나지 못한다.

그럴 때에는 설계를 조금 고쳐보는 것은 어떨까? 이전 블로그의 글을 링크 걸어본다.

Service 계층에 적당한 무게의 Repository 의존성 지우기
시작하기 전에 계층형 아키텍처에서 개발을 진행할 때 우리는 보통 Controller, Service, Repository 이렇게 3개의 계층의 구조로 짜게 된다. 이 3개의 계층은 Controller에서 Service로, Service에서 Repository로 의존성이 흐르는데, 이 중에서도 Repository 부분을 보통 JpaRepository나 Mybatis mapper 같은 클래스로 직접 의존성으로 사용하는 경우가 많다. 하지만 Service 계층에서 바로 이런 클래스들에 의존하게 될 경우 몇 가지 문제가 생기게 된다. 이 글에서는 쉬운 예제 작성을 위해 JpaRepository를 사용하겠다. Service에서 바로 JpaRepository를 의존할 때 생기는 문제 Service 계층이 세부 사항..
https://octoping.tistory.com/27

- 컬렉션 아티클






반갑습니다 😄