ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring, JPA] kotlin에서 JpaRepository default method 사용하기
    웹 개발/Spring Framework 2022. 4. 11. 00:34

     

    JpaRepository를 상속받은 인터페이스에서 default method를 활용할 수 있는데,

     

    그 방법에 대한 포스팅이 많이 보이지 않아 정리해본다.

     

    JpaRepository를 상속받은 interface에서 아무렇지 않게 디폴트 메서드를 구현해서 서비스에서 호출을 하면

     

    jpa에서 쿼리 생성을 못했다는 에러메시지가 나온다.

     

    jpa를 이미 사용하고 있는 사람이라면 query creation을 알고 있을 것이다.

     

    바디가 없는 메서드에 한하여 메서드명을 바탕으로 쿼리를 생성해내는 jpa의 편리한 기능이다.

     

    이 포스팅의 제목처럼 우리는 기본적인 조회처리나 조회 후 데이터 자체의 유무에 대한 exception 처리를 굳이 서비스 로직에서 처리하고 싶지 않다.

     

    interface인 repository에서 default method를 활용하고 싶다.

     

    간단한 예제를 만들어보겠다.

     

    CoffeeFactory.kt

    @Entity
    @Table(name = "coffee_factories")
    class CoffeeFactory(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long = 0,
    
        var name: String,
    
        @Column(nullable = false, updatable = false)
        var createdAt: LocalDateTime = LocalDateTime.now(),
    
        @Column
        var updatedAt: LocalDateTime? = null
    )

     

    NotFoundEntityException.kt

    class NotFoundEntityException(message: String?): RuntimeException(message)

     

    CoffeeFactoryRepository.kt

    interface CoffeeFactoryRepository: JpaRepository<CoffeeFactory, Long> {
        fun findByIdOrThrow(id: Long) = this.findByIdOrNull(id) ?: throw NotFoundEntityException("존재하지 않는 커피 공장 id : $id")
    }

     

    CoffeeFactoryService.kt

    @Service
    class CoffeeFactoryService(
        private val coffeeFactoryRepository: CoffeeFactoryRepository
    ) {
    
        @Transactional(readOnly = true)
        fun get(id: Long): CoffeeFactory {
            return coffeeFactoryRepository.findByIdOrThrow(id)
        }
    }

     

    CoffeeFactoryServiceTest.kt

    (IntegrationBehaviorSpec은 kotest의 BehaviorSpec을 상속받은 @DataJpaTest와 그외 integration test 가능한 설정이 담긴 custom 클래스이다.)

    internal class CoffeeFactoryServiceTest(
        private val coffeeFactoryService: CoffeeFactoryService,
    ): IntegrationBehaviorSpec({
    
        Given("존재하지 않는 커피 공장 id") {
            val id = 1L
    
            When("커피 공장 조회 by id") {
                val exception = shouldThrow<NotFoundEntityException> {
                    coffeeFactoryService.get(id)
                }
    
                Then("NotFoundEntityException 발생") {
                    exception.message shouldContain "$id"
                }
            }
        }
    })

     

    테스트를 돌려본다면 에러가 발생한다.

    메서드명에서 throw라는 property를 찾을 수 없다고 하는데, 이 에러가 우리가 의도하는대로 동작해서 발생한 에러가 아니다.

     

    query creation 기능이 동작하여 throw라는 property를 찾을 수 없다고 에러를 뱉은것이다.

     

    쿼리메서드에서 어떤 메서드일 경우에 쿼리를 생성할지 체크하는 로직은 아래 스크린샷을 보면 알 수 있다.

    spring-data-common-2.6.3 기준,&nbsp;DefaultRepositoryInformation.java

    DefaultRepositoryInformation.java ->  getQueryMethods -> isQueryMethodCandidate

     

    !method.isDefault()를 살펴보면 default method는 쿼리 메서드 생성 후보에서 제외시키는 걸 알 수 있다.

     

    그러나, 코틀린에서 default method를 사용하면 query creation 기능이 동작하지 않는다.


    왜 그럴까?

     

    코틀린의 인터페이스에 대한 default method 방식이 자바 1.8에서 추가된 default method와 다르기 때문이다.

     

    코틀린의 default method는 자바 1.8에 추가된 default method를 타겟팅으로 해서 생긴게 아니었다.

     

    자바 1.6, 1.7 또는 그 이전 버전을 사용하더라도 코틀린의 default method를 사용할 수 있다.

     

    예시를 보자. 아래는 kotlin에서 default method를 사용한 것이다.

    interface Alien {
       fun speak() = "Wubba lubba dub dub"
    }
    
    class BirdPerson : Alien

     

    컴파일을 하면 어떤 모습일지 살펴보자.

    public interface Alien {
      String speak();
    
      public static final class DefaultImpls {
         public static String speak(Alien obj) {
            return "Wubba lubba dub dub";
         }
      }
    }
    public final class BirdPerson implements Alien {
      public String speak() {
        return Alien.DefaultImpls.speak(this);
      }
    }

     

    그림이 그려지는가?

     

    static 클래스, static 메서드를 이용해 interface에서 default method를 사용하는 것처럼 보이는 것이다.

     

    결과적으로, 위에서 코틀린 코드로 만들었던 default method가 query 메서드 후보(candidate)로 체크되어 메서드명에 맞춰 쿼리를 만드려다보니 발생한 예외이다.


    방법

    1. kotlin 1.2 버전 이후부터는 @JvmDefault,  (-Xjvm-default=enable 또는 -Xjvm-default=compatibility 기능이 추가되었다. 향후 deprecated될거라고 한다.
    interface Alien {
       @JvmDefault
       fun speak() = "Wubba lubba dub dub"
    }
    
    class BirdPerson : Alien

     

    2. kotlin 1.4 버전 이후부터의 방법은 -Xjvm-default=all 또는 -Xjvm-default=all-compatibility를 사용한다.

    아래처럼 그대로 default method로 컴파일 된다.

    // -Xjvm-default=all
    public interface Alien {
      default String speak() {
         return "Wubba lubba dub dub";
     }
    }
    public final class BirdPerson implements Alien {}

    -Xjvm-default=all-compatibility는 default method를 상속한 메서드를 recompile하고 해당 메서드에 의존성을 가진 쪽은 컴파일이 되어 있지 않다면 에러가 발생하는데 이런 케이스에 사용한다. 영어 단어처럼 all-compatibility(전체 호환성)다.

     

    보통은 매번 컴파일해서 사용할테니 -Xjvm-default=all를 사용하면 되겠다.

     

    내 케이스는 kotlin 1.6 버전을 사용하고 있어서 두 번째 방법을 사용했고, 이전 버전 사용자는 첫 번째 방법을 고려해볼 수 있을 듯하다.

     

    아래처럼 freeCompilerArgs에 -Xjvm-default=all를 추가하고 rebuild 후 아까 만든 테스트를 돌려보자.

    build.gradle.kts
    성공이다!!

    kotlin의 default method도 java의 defualt method와 동일하게 컴파일 될 줄 알았는데, 이번에 kotlin에서 default method를 어떻게 구현되어있는지 알 수 있는 기회였다.

     

    참고

     

     

     

     

     

Designed by Tistory.