스프링 데이터 JPA 기능 중에서 Pageable과 Page를 사용하면 쉽게 페이징 처리를 할 수 있어 편리하다. 하지만 특정 행부터 일정 개수의 데이터를 조회하고 싶은 경우에는 Pageable과 Page가 적합하지 않다(예를 들어 21번째 행부터 21개의 데이터를 읽어오고 싶은 경우). 특정 행부터 일정 개수의 데이터를 조회할 수 있는 기능을 모든 리포지토리에 적용할 필요가 생겼는데 이를 위해 다음 작업을 진행했다.

  • Rangeable 클래스 추가 : 조회할 범위 값 저장(Pageable 대체).
  • RangeableExecutor 인터페이스 : Rangeable 타입을 사용하는 조회 메서드 정의.
  • RangeableRepository 인터페이스 : 스프링 데이터 JPA Repository 인터페이스와 RangeableExecutor 인터페이스를 상속.
  • RangeableRepositoryImpl 클래스 : 스프링 데이터 JPA의 기본 구현체를 확장. RangeableRepository 인터페이스의 구현을 제공.

스프링 데이터 JPA에서 모든 리포지토리에 동일 기능을 추가하는 방법은 스프링 데이터 JPA 레퍼런스를 참고한다.

예제 코드 : https://github.com/madvirus/spring-data-jpa-rangeable

 

madvirus/spring-data-jpa-rangeable

init. Contribute to madvirus/spring-data-jpa-rangeable development by creating an account on GitHub.

github.com

Rangeable 클래스

import org.springframework.data.domain.Sort;

public class Rangeable {
    private int start;
    private int limit;
    private Sort sort;

    public Rangeable(int start, int limit, Sort sort) {
        this.start = start;
        this.limit = limit;
        this.sort = sort;
    }

    public int getStart() {
        return start;
    }

    public int getLimit() {
        return limit;
    }

    public Sort getSort() {
        return sort;
    }
}

* start : 시작행, limit : 개수, sort : 정렬

RangeableExecutor 인터페이스

import org.springframework.data.jpa.domain.Specification;

import java.util.List;

public interface RangeableExecutor<T> {
    List<T> getRange(Specification<T> spec, Rangeable rangeable);
}

RangeableRepository 인터페이스

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;

import java.io.Serializable;

@NoRepositoryBean
public interface RangeableRepository<T, ID extends Serializable>
        extends Repository<T, ID>, RangeableExecutor<T> {
}

RangeableRepositoryImpl 클래스

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.io.Serializable;
import java.util.List;

public class RangeableRepositoryImpl<T, ID extends Serializable>
        extends SimpleJpaRepository<T, ID>
        implements RangeableRepository<T, ID> {

    public RangeableRepositoryImpl(
            JpaEntityInformation<T, ?> entityInformation, 
            EntityManager entityManager) {
        super(entityInformation, entityManager);
    }

    @Override
    public List<T> getRange(Specification<T> spec, Rangeable rangeable) {
        TypedQuery<T> query = getQuery(
                spec, getDomainClass(), rangeable.getSort());

        query.setFirstResult(rangeable.getStart());
        query.setMaxResults(rangeable.getLimit());

        return query.getResultList();
    }
}

* 기본 구현체인 SimpleJpaRepository 클래스를 확장해서 getRange() 구현

@EnableJpaRepositories로 기본 구현 지정

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(repositoryBaseClass = RangeableRepositoryImpl.class)
public class SpringJpaConfiguration {
}

리포지토리에서 RangeableExecutor 인터페이스 사용

import org.springframework.data.repository.Repository;
import rangeable.jpa.RangeableExecutor;

public interface CommentRepository 
        extends Repository<Comment, Long>, RangeableExecutor<Comment> {
}

Rangeable로 일정 범위 조회

List<Comment> comments = repository.getRange(
        someSpec,
        new Rangeable(10, 5, Sort.by("id").descending()));

 

MockK는 코틀린을 위한 Mock 프레임워크이다. 자바에서 주로 사용하는 Mockito와 유사해서 약간만 노력하면 쉽게 적응할 수 있다. 이 글에서는 MockK의 간단한 사용법을 소개하며 더 다양한 사용법은 https://mockk.io/ 사이트에서 확인할 수 있다.

의존 설정

MockK를 사용하려면 먼저 다음 의존을 추가한다.

<dependency>
    <groupId>io.mockk</groupId>
    <artifactId>mockk</artifactId>
    <version>1.9.3</version>
    <scope>test</scope>
</dependency>

코틀린 1.2 버전을 사용하면 1.9.3.kotlin12 버전을 사용한다.

모의 객체 생성

io.mockk.mockk 함수를 이용해서 모의 객체를 생성한다. 다음은 생성 예이다.

// 1. mockk<타입>()
private val mockValidator1 = mockk<CreationValidator>()

// 2. 타입 추론
private val mockValidator2 : CreationValidator = mockk()

mockk 함수는 타입 파라미터를 이용해서 생성할 모의 객체의 타입을 전달받는다. 변수나 프로퍼티의 타입이 명시적으로 정의되어 있으면 타입 추론이 가능하므로 생략해도 된다.

Answer 정의

모의 객체를 생성했다면 모의 객체가 어떻게 동작할지 정의할 차례이다. 아주 간단하다. io.mockk.every 함수를 사용하면 된다. 다음은 예이다.

@Test
fun someMockTest() {
    every { mock.someMethod(1) } returns "OK" // "OK" 리턴
    every { mock.someMethod(2) } throws SomeException() // 익셉션 발생
    every { mock.call() } just Runs // Unit 함수 실행
    
    assertEquals("OK", mock.someMethod(1))
    assertThrows<SomeException> { mock.someMethod(2) }
}

임의의 인자와 일치

임의의 인자 값과 일치하도록 설정하려면 any()를 사용한다.

@Test
fun someMockTest() {
    every { mock.anyMethod(any(), 3) } returns "OK"
    
    assertEquals("OK", mock.anyMethod(10, 3))
}

Relaxed mock

MockK는 호출 대상에 대한 스텁 정의를 하지 않으면 오류를 발생한다. 

val mock = mockk<Some>()

mock.someMethod(1) // --> io.mockk.MockKException: no answer found for: Some(#1).someMethod(1)

이를 완화하는 방법은 Relaxed mock을 생성하는 것이다. mockk()의 relaxed 파라미터 값을 true로 전달하면 Relaxed mock을 생성할 수 있다.

val mock = mockk<Some>(relaxed = true)
mock.someMethod(1) // --> 0 리턴

리턴 타입이 Unit인 함수는 relaxUnitFun 파라미터 값을 true로 전달한다.

호출 여부 검증

io.mockk.verify 함수를 사용해서 호출 여부를 검증할 수 있다.

val mock = mockk<Some>(relaxed = true)

mock.someMethod(1)
mock.anyMethod(1, 3)

verify { mock.someMethod(1) }
verify { mock.anyMethod(any(), 3) }

인자 캡처

인자를 캡처하고 싶을 땐 slot()과 capture()를 사용한다. 다음은 사용 예를 보여준다.

val mock = mockk<Some>()

val argSlot = slot<Int>()
every { mock.someMethod(capture(argSlot)) } returns 3

mock.someMethod(5)

val realArg = argSlot.captured
assertEquals(5, realArg)

이 글에서는 기본적인 MockK의 사용법을 소개했다. MockK는 더 다양한 기능을 제공하므로 https://mockk.io 사이트를 구경해보자. 도움이 되는 기능을 찾을 수 있을 것이다.

+ Recent posts