Gradle에서src/main/java에 위치한 XML 파일 빌드하기

kindof

·

2023. 5. 13. 00:49

0. 문제

Mybatis를 사용하면 Mapper 인터페이스와 Mapper.xml 파일을 생성해야 합니다. 그리고 일반적으로 Mapper.xml 파일은 XML 설정 파일 분류이기 때문에 resources 하위에 위치시키는데요.

 

하지만 그렇게 되면 아래 사진과 같이 메서드를 정의한 Mapper 인터페이스의 패키지와 실제 쿼리가 정의된 XML 파일의 패키지가 멀리 떨어져(?) 있게 되어 도메인 개발에서 불편함을 겪게 됩니다. 

 

파일 간 탐색이 불편해진다.

 

이러한 불편함을 해소하기 위해 저희 팀에서는 src > main > ... > repository 하위에 Mybatis 인터페이스와 XML 파일을 같이 정의해두고 있습니다.

 

그런데, 여기서도 문제가 하나 있습니다. Gradle CompileJava 작업에서 기본적으로 'src > main > java 내 Java 파일 소스 코드만'을 컴파일하여 .class 파일을 생성하고 이를 classes 작업에서 build/classes 디렉토리에 수집하게 됩니다.

 

그 결과, 아래와 같이 XML 파일이 빌드에서 빠지게 되고

빌드 결과물에 XML 파일이 존재하지 않는다.



XML 파일을 찾을 수 없기 때문에 BindingException이 발생하게 됩니다.

Caused by: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): ~~~~.repository.CommonBlockedListMapper...
    at app//org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:223)
    at app//org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
    at app//org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:59)
    at app//org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
    at app/jdk.proxy3/jdk.proxy3.$Proxy64.selectBlockedChannelIds(Unknown Source)
    at app//com.naver.ntalk.rcmd.core.common.blockedlist.repository.CommonBlockedListLocalCache.refreshLocalCachesInner(CommonBlockedListLocalCache.java:51)
    at app//com.naver.ntalk.rcmd.core.common.blockedlist.repository.CommonBlockedListLocalCache.warmUp(CommonBlockedListLocalCache.java:40)
    at app//com.naver.ntalk.rcmd.core.common.blockedlist.business.CommonBlockedListBOTest.postConstruct(CommonBlockedListBOTest.java:29)
    at java.base@17.0.4.1/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@17.0.4.1/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base@17.0.4.1/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base@17.0.4.1/java.lang.reflect.Method.invoke(Method.java:568)
    at app//org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389)
    at app//org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333)
    at app//org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157)
    ... 88 more

 

 

1. Gradle sourcesSets + task

위의 문제를 해결하기 위해 아래와 같이 build.gradle을 작성할 수 있습니다.

{
    ...
    
    sourceSets {
        main {
            resources {
                srcDirs = ["src/main/java", "src/main/resources", "filters/${profile}"]
                include "**/*.xml"
            }
        }
    }

    task copyResourcesMain(type: Copy){
        from(sourceSets.main.resources.srcDirs){
            exclude("**/*.java")
        }
        into("build/classes/java/main")
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }
    
    tasks.compileJava.dependsOn(copyResourcesMain)

    ...
}

sourceSets는 프로젝트에 대해 여러 세트의 소스 코드를 정의하는 블록입니다. 기본적으로 Gradle은 'main', 'test' 라는 두 개의 소스 세트를 가지고 있습니다.

 

'resources'는 'main'의 하위 블록으로 프로젝트의 리소스(Java 소스 코드가 아닌 파일)를 지정하는 데 사용합니다. 이 곳에 설정 파일과 XML 파일 등이 포함됩니다.

 

저는 resourcessrcDirs 안에 'src/main/java' 경로를 포함하도록 선언했습니다. 그리고 include '**/*.xml' 부분을 통해 src > main > java 안의 XML 파일만 대상으로 지정했습니다. "src/main/resources", "filters/${profile}" 부분은 기존 설정 파일의 위치들입니다.

 

그리고 copyResourcesMain 이라는 Copy Task를 정의했는데요.

 

compileJava로 생성되는 클래스 파일들은 build/classes/java/main 경로에 생성되기 때문에, resources 디렉토리의 내용들을 'build/classes/java/main' 디렉토리에 복사하는 작업을 하도록 정의했습니다. 단, *.java 파일들은 필요없기 때문에 제외합니다.



마지막으로 tasks.compileJava.dependsOn(copyResourcesMian)으로 copyResourcesMain 작업이 compileJava 실행 이전에 수행되도록 정의했습니다.

 

이제 똑같이 빌드를 해보면 아래와 같이 XML 파일도 빌드된 것을 확인할 수 있습니다.

XML 파일도 빌드 성공

 

2. 정리

처음에는 BindingException이 왜 발생하는지부터 머리가 아팠고, 이를 해결하기 위해 Mapper.xml 파일을 resources 하위로 옮기는 방식으로 해결했는데요.

 

약간은 Tricky(?)하지만 위에서 소개한 방법을 사용하면 Mapper.xml 파일이 src 디렉토리 하위에 있어도 상관없이 빌드할 수 있는 장점이 있습니다.

 

정답이 있는 것은 아니니, 개인이나 팀에서 선호하는 방식으로 사용하면 좋을 것 같습니다.

 

감사합니다.