[Spring] 운영 환경에서 ddl-auto: create 방지하기

1

들어가며

살다보면 실수를 할 때가 있다.

  • rm -rf ./를 하려다가 rm -rf /를 한다던지

  • where 절 없는 update를 날려본다던지

  • 테스트 DB 조작하려다 운영 DB에 갈겨본다던지

  • product application properties (...ddl-auto: create)

전역한 남자들이 술자리에서 군대 얘기하다보면 내 군생활이 더 힘들었다고 하는데

저 중에 하나라도 달성했다고 하는 개발자가 있다고하면 심심한 위로를 전해줬으면 한다.

나는 이걸 개발자 히든 퀘스트라고 말하고 다닌다.
(히든 업적을 전부 깨고 나면 Delete King 칭호를 얻을 수 있다.)

한번 쯤 겪어보면 좋다.
엔터 한방이 얼마나 신중해지는지..
방어기재가 패시브로 동작한다.
(애초에 권한이 없어서 이런일이 발생하기 어렵기도 하다. 썰을 풀고 싶다면 한번쯤은 ㅎㅎ..)

JPA hibernate.ddl-auto

아무튼 jpa를 사용해보신 분들은 알겠지만 hibernate 프로퍼티 중에 ddl을 생성해주는 놈이 있다.

이놈 또한 아주 지독한 놈이다. 상상 그 이상이다.

일단 이걸 스프링 컨테이너를 부트스트래핑 하는 과정에서 이 옵션에 대한 검증을 진행하고 싶다.

즉 프로퍼티를 읽어서 실제 ddl-auto의 value 값을 가지고 generate ddl을 하기 전에 검증이 완료되었으면 했다.

Environment Abstraction

스프링의 동작 방식을 살짝만 집고 넘어가면 autoconfigure를 통해 environment를 구성한다.

우리가 사용하는

  • application.properties
  • application.yaml(yml))

은 가장 후순위이며 컨디션(값의 유무)에 따라 디폴트를 물던 한다.
(높은 우선순위의 프로퍼티를 덮어 쓸 수 없다.)

시스템 변수, 환경변수 등등 우선순위는 있으니까 궁금하면 찾아보시라!

어떻게 해볼까

일단 위에서 말한대로 부트스트래핑 되는 과정에서 ddl-auto의 값을 검증하고싶다.

JPA에 대한 인프라스트럭처 빈이 자동 구성이 되고 나서 검증하면 무슨소용인가;

우선 해볼 수 있는 것들을 생각해본다.

  1. Entry Point, 메인 메서드에서 스프링을 기동하기 전에 읽어서 한다
    • args로 넘어온 profile 밸류를 한번 검증해서 하드코딩으로 prod인 경우 none으로 강제 치환
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
    
    	if(args.length > 0 && args[0].equals("prod")) {
            System.setProperty("spring.jpa.hibernate.ddl-auto", "none");
            System.setProperty("spring.jpa.properties.hibernate.hbm2ddl.auto", "none");
        }
        
        SpringApplication.run(DemoApplication.class, args);
    }

}
  1. Initialzer를 하나 만들어본다
    • configuration을 하기 전에 사전 작업을 정의한다.
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
    
        SpringApplication application = new SpringApplication(Application.class);
        application.addInitializers(applicationContext -> {
            String[] activeProfiles = applicationContext.getEnvironment().getActiveProfiles();
            for (String activeProfile : activeProfiles) {
                if (activeProfile.equals("prod")) {
                    Map<String, Object> replaceDdlAutoProperty = Map.of(
                            "spring.jpa.hibernate.ddl-auto", "none",
                            "spring.jpa.properties.hibernate.hbm2ddl.auto", "none"
                    );
                    applicationContext.getEnvironment()
                            .getPropertySources()
                            .addFirst(new MapPropertySource("HighestPrecedenceProperties", replaceDdlAutoProperty));
                }
            }
        });
        ApplicationContext applicationContext = application.run(args);
    }

}

예제를 만들어봤지만 둘의 코드는 크게 차이가 없다. 어느정도 스프링에게 위임한 것이다.

일단 둘 다 마음에 들지 않는다. 왜냐면 저 메서드가 호출되는 시점에서는 외부 설정 파일을 읽기 전이다.

PropertySourceLoader가 동작하기 전의 시점이기 때문에 지금 내가 무슨 값을 설정했는지에 대한 검증을 저 단계에서는 비효율적이다.

그러면?

스프링은 많은 후 처리기(Post Processor)를 제공해주는데, Envirionment에 대한 포스트 프로세서 인터페이스가 존재한다. 실제 PropertySourceLoader가 동작할 때 파싱하면 좋겠지만 그 작업은 배꼽이 더 큰 것같다.

이런 저런 핑계로 포스트 프로세서를 활용했다.

final class HibernatePropertiesValidator implements EnvironmentPostProcessor {

    private static final String PRODUCTION_PROFILE_NAME = "prod";

    private static final String SPRING_JPA_HIBERNATE_DDL_AUTO_PROPERTY = "spring.jpa.hibernate.ddl-auto";
    private static final String SPRING_JPA_PROPERTIES_HIBERNATE_HBM2DDL_AUTO_PROPERTY = "spring.jpa.properties.hibernate.hbm2ddl.auto";

    private static final List<String> VALIDATION_PROPERTY_NAMES = Arrays.asList(
            SPRING_JPA_HIBERNATE_DDL_AUTO_PROPERTY,
            SPRING_JPA_PROPERTIES_HIBERNATE_HBM2DDL_AUTO_PROPERTY
    );

    @Override
    public void postProcessEnvironment(
            final ConfigurableEnvironment environment,
            final SpringApplication application
    ) {
        boolean isProdProfileActive = Arrays
                .asList(environment.getActiveProfiles())
                .contains(PRODUCTION_PROFILE_NAME);

        if (isProdProfileActive) {
            validate(environment);
        }
    }

    private void validate(
            final ConfigurableEnvironment environment
    ) {
        for (final String propertyName : VALIDATION_PROPERTY_NAMES) {
            final String propertyValue = environment.getProperty(propertyName);
            if (checkInvalidPropertyValue(propertyValue)) {
                throw new IllegalArgumentException(
                        String.format("In production profile, '%s': '%s' must be set to 'none'. Other values are disallowed."
                                , propertyName
                                , propertyValue
                        )
                );
            }
        }
    }

    private boolean checkInvalidPropertyValue(
            final String propertyValue
    ) {
		// none or validate 
        return propertyValue != null && (!"none".equals(propertyValue));
    }

}

EnvirionmentPostProcessor를 구현하였다.

이건 위의 코드와 다른 점이 있다면

부트스트래핑하는 포스트 프로세서의 구현 클래스를 META-INF/spring.factories에 적어줘야한다.

// ../resources/META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.HibernatePropertiesValidator

이렇게 하고

profile을 prod로 선언한 상태로 애플리케이션을 기동해보면

코드에서 한번 더 막아 줄 수 있다.

꼭 이것 뿐 아니라 특정 프로퍼티에 대한 검증 같은 것도 할 수 있겠죠?

마치며

사실 이렇게 중요한 구성 정보를 그 누구도 확인하지 않고 매너리즘에 빠져 운영서버에 올라가면 안된다. (고해성사)
구성 정보가 바뀌면 면밀한 검토(리뷰)가 필요하며, 코드로 방지하는건 완벽할 수 없고 코드로 해결하려 하면 안된다고 생각'은' 항상 합니다.

위에서 말했지만 한번 쯤 날려먹어보는 것도 좋다. (우리는 성인이니까, 책임은 본인이)
하지만 칭호는 희귀할 수록 가치가 있기 때문에 다른 개발자들이 깨지 않길 바라며 이 글을 마칩니다..

1개의 댓글

comment-user-thumbnail
2024년 7월 24일

좋은 인사이트 감사합니다!! 😋😋

답글 달기