CI시 JPA Entity와 DB Schema 검증하기

2024. 6. 28. 16:08프로젝트/[EATceed] 몸무게 증량 어플

728x90

 

 

최근 PR에서 발생한 데이터베이스 스키마와 JPA 엔티티 간의 불일치 문제로 인해 서버가 과부화되는 문제가 발생했습니다. 이로 인해 EC2의 CPU 사용률과 CPU 크레딧 사용량을 급증시키고, 결국 예상보다 높은 비용이 발생하게 되었습니다.

 

이를 방지하기 위해 CI 파이프라인에 JPA Entity와 DB Schema 검증 단계를 추가하였습니다.


문제 상황

스키마 오류로 인해 서버 애플리케이션이 지속적으로 재시작되는 상황이 발생했습니다. 이는 다음과 같은 문제를 야기했습니다.

  • 서버 과부하: 애플리케이션이 지속적으로 재시작되면서 EC2 인스턴스의 CPU 사용률과 CPU 크레딧 사용량이 급증
  • 비용 증가: 예상보다 높은 비용이 발생

500

 

문제 해결 방법 

이러한 문제를 방지하기 위해, JPA Entity와 DB Schema가 일치하는지 검증하는 테스트를 추가하게 되었습니다.

해당 문제를 해결하기 위해 어떤 방식으로 수행해야 할지 고민하였습니다.

처음에 고려한 것은 Flyway를 사용하는 것이었습니다. 하지만 아래와 같은 이유로 이를 도입하지 않았습니다.

 

Flyway를 사용하지 않은 이유

 

Flyway의 주 목적은 데이터베이스 스키마의 마이그레이션을 자동화하는 것입니다. 이는 개발과 배포 과정에서 데이터베이스의 스키마가 일관되게 유지되도록 도와줍니다. 그러나 제가 직면한 문제는 JPA Entity와 DB Schema의 일치 여부를 검증하는 것입니다. Flyway는 이 목적에 적합하지 않습니다.

 

JPA Hibernate ddl-auto=validate를 이용한 방식

 

테스트 시 사용하는 데이터베이스가 실제 개발 서버의 데이터베이스와 일치하기 때문에 해당 방식을 사용할 수 있었습니다. 또한, JPA Entity와 DB Schema의 일치 여부를 검증하는 목적에 더 적합하다고 판단했고, 단순히 테스트 클래스를 구현하는 것만으로 충분하기 때문에 Flyway를 사용하는 것보다 리소스가 더 적게 든다고 판단했습니다.

 

문제 해결 시 고민한 점

 

@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=validate")  
@AutoConfigureTestDatabase(replace = Replace.NONE)  
@Import(QueryDslConfig.class)  
public class SchemaValidationTest extends ContainerTest{  
  
@Test  
public void testSchemaValidity() {}  
}

 

처음에는 위와 같이 구현했습니다. 하지만 이 방식은 Run all test 시에는 해당 테스트가 통과 or 실패하지만 개별적으로 해당 클래스만 테스트할 시에는 통과하지 않는 문제가 있었습니다.

 

이러한 문제가 발생하는 이유

 

Run all Tests할 시

 

문제가 발생하는 이유는 테스트 컨테이너를 싱글톤 방식으로 사용하고 있기 때문입니다.

 

ContainerTest에서 ActiveProfile test를 사용하고 있고, test 프로필의 ddl-auto 속성은 create-drop으로 데이터베이스 스키마를 생성하고 테스트가 끝나면 이를 삭제하는 방식입니다.

따라서, 해당 테스트 전에 통합테스트가 실행이 테스가 성공하게 됩니다.

 

결국, 테스트 격리가 제대로 이루어지지 않고 있습니다.
 

SchemaValidationTest 실행 전에 IntegrationTest가 있어야 성공하는 모습



단독 테스트 수행

 

SchemaValidationTest를 단독 수행하면, 테스트 컨테이너에 스키마가 세팅되어있지 않기 때문에 SchemaManagementException 이 발생합니다.

 

 

Run all Test 시에도 테스트 순서에 영향을 받지 않고 테스트가 격리되어 단독으로 수행될 수 있는 방법을 고민해본 결과, 해당 테스트만을 위한 테스트 컨테이너를 사용하는 것이 최선의 방법이라는 결론에 도달했습니다.

 

 

SchemaValidationTest 클래스만을 위한 테스트 컨테이너 세팅

 

@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=validate")
@AutoConfigureTestDatabase(replace = NONE)
@Import(QueryDslConfig.class)
@Testcontainers
public class SchemaValidationTest {

    @Container
    static MariaDBContainer<?> schemaValidationMariaDB =
            new MariaDBContainer<>(DockerImageName.parse("mariadb:10.6"));

    @Container
    static GenericContainer<?> redisContainer =
            new GenericContainer<>(DockerImageName.parse("redis:6-alpine")).withExposedPorts(6379);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", schemaValidationMariaDB::getJdbcUrl);
        registry.add("spring.datasource.username", schemaValidationMariaDB::getUsername);
        registry.add("spring.datasource.password", schemaValidationMariaDB::getPassword);
        registry.add("spring.redis.host", redisContainer::getHost);
        registry.add("spring.redis.port", () -> redisContainer.getMappedPort(6379).toString());
    }

    @Test
    public void testSchemaValidity() {
        // 스키마 유효성 검사 로직
    }
}

 

 

해당 코드를 작성하면서 static 사용 여부에 관해 고민하였습니다.

 

static을 사용 여부

 

schemaValidationMariaDB와 redisContainer를 static으로 설정하는 것이 좋을지 고민했습니다. Static으로 설정하면 해당 컨테이너가 JVM 내에서 메서드 영역의 일부를 차지하기 때문에 낭비가 될 수 있습니다.

 

그러나 @DynamicPropertySource 메서드는 컨테이너 인스턴스의 설정을 동적으로 컨텍스트에 저장하려면 컨텍스트 초기화 전에 실행되어야 하므로 static으로 설정이 필요했습니다.

 

 

테스트 컨테이너에 Schema.sql 파일을 적용하기

 

이제 해당 테스트 컨테이너에 배포 서버의 스키마를 설정하는 데 사용하는 schema.sql 파일을 적용해주면 됩니다.

 

 

@Container와 withInitScript 메서드 사용 불가 - 경로 문제

 

@Container
static MariaDBContainer<?> schemaValidationMariaDB =
        new MariaDBContainer<>(DockerImageName.parse("mariadb:10.6"))
        .withInitScript("classpath:db/schema.sql");

 

 

withInitScript는 클래스패스를 이용한 파일 경로를 요구합니다.

 

하지만, 제가 사용하고자 하는 파일은 클래스패스를 이용하지 못하는 범주 내에 있었습니다.

 

 


즉, resources/gaebaljip-develop-environment/mariadb-init/01_schema.sql 파일은 클래스패스 외부에 위치해 있었습니다.

 

따라서 아래와 같이 파일 시스템 경로를 사용해서 원하는 경로의 파일의 경로를 가져오는 메서드를 작성하였습니다.

 

private static String getAbsolutePath() {
    String relativePath = "resources/gaebaljip-develop-environment/mariadb-init/01_schema.sql";
    String currentDir = System.getProperty("user.dir");
    return Paths.get(currentDir, relativePath).normalize().toAbsolutePath().toString();
}

 

 

스크립트 실행 메서드 구현

 

withInitScript 메서드를 이용하지 못 하기 때문에 스크립트를 실행할 수 있는 메서드를 작성하였습니다.

 

private static void runInitScript(MariaDBContainer<?> container, String scriptPath)
        throws SQLException, IOException {
    String script = new String(Files.readAllBytes(Paths.get(scriptPath)));
    String[] commands = script.split(";");

    try (Connection connection = container.createConnection("");
         Statement statement = connection.createStatement()) {
        for (String command : commands) {
            command = command.trim();
            if (!command.isEmpty()) {
                statement.execute(command);
            }
        }
    }
}

 

try-with-resources 구문을 사용하여 데이터베이스 연결과 명령문 실행을 처리했습니다. 이를 통해 자원을 효율적으로 관리하고, 예외 발생 시 자원이 자동으로 해제되도록 했습니다.

 

최종 코드는 아래의 PR에서 확인할 수 있습니다.

 

https://github.com/JNU-econovation/EATceed/pull/329

 

[BE/feat] CI/CD 개선: JPA Entity 검증, Trigger 수정, Docker-compose 설정 업데이트, 최소 테스트 커버리지 제

⚠️ 관련 이슈 #321 📢 주요 변경사항 스키마 불일치 이슈로 인해 서버가 과부화되는 문제가 발생하여 CI시 스키마 검증하는 것이 필요하다고 판단 이에, SchemaValidationTest 클래스를 생성하여 CI시

github.com

 

 

 

기대 효과

 

SchemaValiationTest를 통해, CI 시 JPA Entity와 DB Schema의 일치성을 검증하여, 서버의 안정성을 높이고 예상치 못한 비용 증가를 방지할 수 있습니다.

 

 

 

긴 글 읽어주셔서 감사합니다!

728x90