모종닷컴

멀티 모듈 프로젝트에서 다중 프로퍼티 파일 다루기 본문

Programming/Spring

멀티 모듈 프로젝트에서 다중 프로퍼티 파일 다루기

모종 2022. 9. 11. 14:50
반응형

멀티 모듈 프로젝트를 사용하다 보면 하위 모듈의 프로퍼티 파일이 필요한 경우가 생깁니다. 예시를 들어보겠습니다. module-a와 module-b가 존재합니다. module-b에는 암호화 관련 서비스가 존재하고 module-a에서는 이 암호화 서비스가 필요하여 module-b를 사용하려고 합니다. 프로젝트의 구조를 보면 아래와 같습니다.

module-a의 프로퍼티 파일(/src/main/resources/application.yml)이 존재하고, module-b의 프로퍼티 파일이 존재합니다.

Module B

@Service
class CipherService(
    @Value("\${encrypt.secretkey}") SECRET_KEY: String
) {
    private val keySpec = SecretKeySpec(SECRET_KEY.toByteArray(), "AES")
    private val ivSpec = IvParameterSpec(SECRET_KEY.substring(0..15).toByteArray())
    private val transformation = "AES/CBC/PKCS5Padding"


    fun encrypt(input: String): String {
        val cipher = Cipher.getInstance(transformation)
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
        val encryptedPayload = cipher.doFinal(input.toByteArray())
        return String(Base64.encodeBase64(encryptedPayload))
    }

    fun decrypt(input: String): String {
        val cipher = Cipher.getInstance(transformation)
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
        val base64Encrypted = Base64.decodeBase64(input.toByteArray())
        return String(cipher.doFinal(base64Encrypted))
    }
}

먼저 위의 코드(암호화 코드)를 먼저 보겠습니다. 암호화 서비스의 경우 "encrypt.secretKey"라는 프로퍼티가 필요합니다. 따라서 module-b의 application.yml에 아래와 같이 프로퍼티를 추가해주었습니다.

encrypt:
  secretKey: eThWmZq4t7w!z%C*F-J@NcRfUjXn2r5u

테스트 컨트롤러 (module-a)

@RestController
@RequestMapping("/modulea")
class TestController(
    private val cipherService: CipherService
) {
    @GetMapping("/encrypt")
    fun encrypt(@RequestParam message: String): ResponseEntity<String> {
        return ResponseEntity.ok(cipherService.encrypt(message))
    }

    @GetMapping("/decrypt")
    fun decrypt(@RequestParam encryptedMessage: String): ResponseEntity<String> {
        return ResponseEntity.ok(cipherService.decrypt(encryptedMessage))
    }
}

다음은 module-a에 테스트 컨트롤러를 만들었습니다. 아래의 설정에서 볼 수 있듯이 module-a는 module-b에 의존하고 있는 상태입니다. 그리고 module-b에 정의되어 있는 CipherService를 사용하고 있습니다.

project(":module-a") {
   apply plugin: 'application'

   mainClassName = "com.example.practice.modulea.ModuleApplication"

   dependencies {
      implementation project(":module-b")
   }
}

module-a 애플리케이션 실행

이 상태에서 module-a를 실행해보도록 하겠습니다. 그러면 아래와 같은 오류가 발생하면서 어플리케이션 실행에 실패하게 됩니다. 

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'encrypt.secretkey' in value "${encrypt.secretkey}"

스프링에서 프로퍼티 파일을 읽는 코드를 봐야 정확하게 이해할 수 있겠지만 지금으로서는 module-a 애플리케이션이 실행하면서 module-b의 프로퍼티 파일을 읽지 못하고 있음을 알 수 있습니다.  그래서 이런 상황에서 module-b의 프로퍼티 파일을 읽을 수 있는 방법 몇 가지를 공유드리려고 합니다.

첫 번째 : module-a에 프로퍼티를 정의해주기

충돌이 일어난 것인지 module-b의 프로퍼티 파일을 읽지 못하고 module-a의 프로퍼티만 읽어 들이고 있는 상태입니다. 따라서 이 프로퍼티를 module-a의 프로퍼티 파일에 추가만 해주면 됩니다. 

encrypt:
  secretkey: eThWmZq4t7w!z%C*F-J@NcRfUjXn2r5u

그리고 실행을 하게 되면 정상적으로 동작이 되고 있음을 알 수 있습니다. 그런데 위 방법은 조금 적절치 못한 것 같았습니다.

  • module-b를 사용하는 모든 곳에다가 이런 식으로 프로퍼티를 추가해주어야 하는가?
  • module-b의 코드가 수정되었을 때(예: 프로퍼티 이름이 secret_key로 변경) 사용하는 모든 곳의 프로퍼티를 빼먹거나 빠뜨리지 않고 수정해줄 수 있을까?
  • 만약 secretKey가 고정되어야 하는 상황이라면?
    • 예시: module-b에는 token이라는 엔티티가 정의되어 있고, token의 속성이 특정 시크릿키로 암호화된 상태다. 외부 모듈에서는 암호화 서비스를 직접 사용하지 못하도록 막아두었다. 정리하자면 암호화 키는 고정되어 있는 상태이고, 외부 모듈에서 만약 다른 암호화 키 값을 사용한다면 token 엔티티의 특정 속성들은 복호화되지 않는다.

 

두 번째 : 프로퍼티 우선순위 로드 이용하기

위의 첫 번째 방법은 여러모로 고려해볼 때 좋은 해결방법이 되지 못할 것 같습니다. 그렇다면 이 프로퍼티는 module-b에 정의되어 있는 게 옳은 것 같은데 module-a에서는 module-b의 프로퍼티 파일을 전혀 읽지 못하고 있으니 어떻게 해야 할까요? 두 번째 방법으로 소개해드리려는 것은 프로퍼티 파일의 위치를 조정하는 것입니다. 스프링은 프로퍼티 파일을 읽어올 때 아래와 같은 우선순위가 존재합니다. 

위 경로의 디렉터리에서 순서대로 프로퍼티 파일이 있는지 검사하고 적중 시 Envrionmonet 객체에 프로퍼티를 추가합니다. 이걸 이용한다면 프로퍼티 파일이 여러 개여도 읽을 수 있습니다.

이 과정이 궁금하다면 ConfigFileApplicationListener 클래스에 디버깅을 걸어놓고 쭉 따라가 보면 됩니다. 시간이 괜찮다면 한번 아래의 사진에 해당하는 부분에 브레이크 포인트를 걸고 디버깅을 시작해보세요.

 

현재 module-a의 프로퍼티 파일은 4번째 우선순위에 해당하는 디렉터리에 있으니 module-b는 3번째 우선순위에 해당하는 디렉터리로 옮겨보겠습니다. module-b의 /resources/application.yml을 /resources/config/application.yml로 옮겼습니다. 이렇게 하니 정상적으로 module-b의 프로퍼티를 읽을 수 있었고, 애플리케이션도 정상 실행되었습니다. 

두 번째 방법으로 인해 프로퍼티 설정을 module-b로 옮기기는 했지만 여전히 불안한 요소들이 존재합니다. 

  • 겹치지 않게 할 수 있는 최대 개수는 4개(/config, current directory, classpath:/config, classpath:/)입니다. module-a가 의존해야 하는 모듈이 4개 이상으로 늘어나게 되면 어떻게 해야 하는 걸까? 

세 번째 : @PropertySource 이용하기

두 번째 방법도 소개드렸지만 여전히 불안한 요소들이 존재합니다. 그래서 이번에는 module-b의 코드에 @PropertySource를 이용하여 명시적으로 프로퍼티를 추가해주는 방법을 볼 것입니다. 현재 이름이 겹치면 module-a의 프로퍼티 파일만 읽히는 걸로 보아서 이름이 충돌 나면서 무시된 것 같아 module-b의 프로퍼티 파일 이름을 module-b.yml로 수정하였고, 변경된 이름의 module-b 프로퍼티를 읽을 수 있도록 module-b 모듈에 코드를 추가해주었습니다.

ConfigFileApplicationListener 디버깅을 하다 보면 알겠지만 spring.config.name 등의 설정이 없다면 기본적으로 프로퍼티 파일을 읽을 때 이름에 "application"가 포함되어야지만 가져올 수 있습니다.
@Configuration
@PropertySource(value = ["classpath:module-b.yml"], factory = YamlPropertySourceFactory::class)
class PropertyConfig {
}

여기서 잠깐 헤매었는데 PropertySource의 경우 디폴트로 파일 확장자가 properties, xml인 것에 대해서만 지원을 하고 있습니다.

따라서 위처럼 factory를 추가해주지 않아 디폴트 factory 값을 사용하면 아래 사진과 같이 프로퍼티가 읽혀버리게 됩니다.

따라서 yml 파일 확장자를 지원하기 위해서는 아래와 같은 팩토리를 만들어 주어야 합니다.

class YamlPropertySourceFactory : DefaultPropertySourceFactory() {
    override fun createPropertySource(name: String?, resource: EncodedResource): PropertySource<*> {
        val factory = YamlPropertiesFactoryBean()
        factory.setResources(resource.resource)
        val properties = factory.`object`
        return PropertiesPropertySource(resource.resource.filename, properties)
    }
}

이제 다시 애플리케이션을 실행해보면 정상적으로 동작하고 있음을 알 수 있습니다. 

결론

멀티 모듈 프로젝트에서 프로퍼티 파일 충돌로 인한 문제에 대하여 3가지 방법을 보았습니다. 프로그래밍에 정답이 하나로 정해져 있는 것은 아니니 가장 적절한 방식을 선택해서 적용하면 될 것 같습니다. 

반응형