테스트 전 데이터베이스를 초기화하는 방법

Jane·2021년 9월 15일
8

최근 프로젝트 시 회고 및 기록의 중요성을 느껴 오늘부터는 배운 내용들을 기록해보려고 한다. 이제 왕초보 단계는 벗어났고 사용법도 어느정도 익숙해졌으니 내부 동작 원리를 이해하는 단계로 나아가보자!💪

들어가기 전에

스프링 빈은 다음과 같은 이벤트 라이프사이클을 갖는다.

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
    • 생성자 주입 시 빈 생성 단계에서 의존 관계 주입이 일어난다.
  3. 의존 관계 주입
    • 필드 주입 및 수정자 주입은 객체 생성 후 의존 관계가 주입된다.
  4. 초기화 콜백
    • 빈이 생성된 후, 빈의 의존관계 주입이 완료된 후 호출된다.
  5. 사용
  6. 소멸 전 콜백
    • 빈이 소멸되기 직전에 호출된다.
  7. 스프링 종료

4번의 초기화 콜백 시 데이터베이스를 초기화하고 싶은데, 우선 InitializingBean 인터페이스를 구현하여 데이터베이스를 초기화해보았다.

InitializingBean이란?

  • InitializingBean은 "모든 속성이 BeanFactory에 의해 설정되면 반응해야 하는 빈에 의해 구현되는 인터페이스"이다.
package org.springframework.beans.factory;

public interface InitializingBean {

	void afterPropertiesSet() throws Exception;

}

afterPropertiesSet()

  • 모든 빈 프로퍼티가 설정된 이후, 빈 인스턴스가 전체 configuration의 유효성 검증과 최종 초기화를 수행할 수 있도록 한다.
    → 빈이 생성되고 모든 프로퍼티 설정(의존관계 주입)이 완료된 후 호출된다.

🔍 Project

이제 프로젝트 코드를 살펴보자!

@Service
public class DatabaseCleanup implements InitializingBean {

    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tableNames;

    @Override
    public void afterPropertiesSet() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                                  .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null)
                                  .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
                                  .collect(Collectors.toList());
    }

먼저 InitializingBean을 구현한 뒤, entityManager에서 persistence unit의 메타모델을 가져왔다.
이후 @Entity 어노테이션이 붙은 클래스들의 이름을 Camel case에서 Snake case로 바꿔준 뒤, 테이블 이름의 리스트를 tableNames 배열에 담아준다.


    @Transactional
    public void execute() {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

이후 entityManager를 flush()해주고,
SET REFERENTIAL_INTEGRITY FALSE를 통해 참조 무결성 제약조건으로 인해 테이블 삭제가 되지 않는 문제를 해결한다.

        for (String tableName : tableNames) {
            entityManager.createNativeQuery("TRUNCATE TABLE \"" + tableName + "\"").executeUpdate();
            entityManager.createNativeQuery("ALTER TABLE \"" + tableName + "\" ALTER COLUMN \"id\" RESTART WITH 1").executeUpdate();
        }

        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }
}

DROP TABLE을 하면 테이블 자체가 삭제되기 때문에 TRUNCATE TABLE을 통해 테이블의 row만 지워준 뒤,
ID 컬럼에 들어가는 시퀀스를 1부터 시작되도록 초기화해준다.
마지막으로 무효화했던 제약 조건을 SET REFERENTIAL_INTEGRITY TRUE를 통해 다시 걸어주면 된다.

이제 AcceptanceTestBase 클래스에 DatabaseCleanup 빈을 주입받자!

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AcceptanceTestBase {

    protected static final String BASE_URL = "http://localhost";

    @LocalServerPort
    protected int port;

    @Autowired
    private DatabaseCleanup databaseCleanup;

    @BeforeEach
    void cleanUpDatabase() {
        databaseCleanup.execute();
    }
}

빈이 생성된 후 의존 관계 주입이 완료되면 초기화 콜백 단계에서 afterPropertiesSet()이 실행되며, 그 이후 execute()가 실행되어 데이터베이스가 성공적으로 초기화된다.

✨ Refactoring

InitializingBean 인터페이스를 구현하면 아래와 같은 단점이 존재한다.

  • InitializingBean은 스프링 전용 인터페이스이기 때문에 스프링 전용 인터페이스에 의존적인 설계를 해야 한다.
  • 초기화 메서드의 이름을 변경할 수 없다.
  • 코드를 수정할 수 없는 외부 라이브러리에는 적용할 수 없다.

따라서 InitializingBean 대신 @PostConstruct 어노테이션을 사용해보자!

@Service
public class DatabaseCleanup {

    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tableNames;

    @PostConstruct
    public void init() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                                  .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null)
                                  .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
                                  .collect(Collectors.toList());
    }
}

그저 InitializingBean을 제거하고 afterPropertiesSet()에 해당하는 초기화 메서드에 @PostConstruct 어노테이션을 붙이면 된다😄


Source

11개의 댓글

comment-user-thumbnail
2021년 9월 16일

로고 여기 쓰셨군요 ㅋㅋ

1개의 답글
comment-user-thumbnail
2021년 9월 17일

안녕하세요? 검색하다가 오게 되었는데 정리가 정말 잘 되어있네요~
잘 참고하겠습니다! 감사합니다~^^

아, 그리고 질문이 있는데요 truncate table을 실행하면 시퀀스도 1로 초기화 되지는 않나요!?
mysql db console에서 생쿼리로 실행했을때는 그렇게 작동하는 것 같았는데 모든 DB에서 그렇게 되는 것은 아니라서 명시적으로 시퀀스 초기화를 하신건지 궁금하네요~

3개의 답글