0. 들어가기 전
프로젝트 개발을 진행하다가, 테스트에서 테스트 초기화가 제대로 수행되지 않아서 테스트가 실패하는 상황이 있었습니다.
결과적으로는 @ElementCollection으로 생성되는 테이블이 동적으로 초기화되지 않아서 발생하는 문제였습니다.
그래서 이번 포스팅에서 문제 상황과 @ElementCollection 테이블을 동적으로 가져와서 해결한 방법에 대해서 간단히 다뤄보겠습니다!
1. 문제 상황
프로젝트에서 하나의 기능을 개발하고, 전체 테스트를 실행했을 때
테스트에서 테스트 초기화가 제대로 수행되지 않아서 테스트가 실패하는 상황이 있었습니다.
프로젝트의 테스트에서는 E2E 테스트를 진행할 때 RestAssured를 사용하고 있습니다.
이때 RestAssured가 통합 테스트를 수행하는 스레드와
@Transactional으로 트랜잭션을 관리하는 실제 스프링의 스레드가 다르기 때문에 문제가 발생합니다.
@Transactional으로 트랜잭션을 관리할 때는, 해당 어노테이션이 실행된 스레드에서 트랜잭션 관리가 진행됩니다.
그래서 @SpringBootTest의 WebEnvrionment가 RANDOM_PORT가 아닌 스프링 환경과 동일하다면
@Transactional을 통해 테스트 수행 후 롤백처리가 가능합니다.
하지만, @SpringBootTest의 WebEnvironment가 RANDOM_PORT라면
트랜잭션 관리 스레드와 다른 스레드에서 테스트를 수행하기 때문에 데이터 롤백 처리가 되지 않아서 문제가 발생합니다.
따라서, 이러한 문제를 이전에는 다음과 같이 동적으로 엔티티에 해당하는 테이블을 Truncate하여 초기화했었습니다.
@Component
public class H2TruncateUtils {
@Autowired
private EntityManager em;
@Transactional
public void truncateAll() {
em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
final List<String> tableNames = em.getMetamodel().getEntities().stream()
.map(entityType -> {
final String name = entityType.getName();
return camelToSnake(name);
})
.toList();
for (String tableName : tableNames) {
em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
private String camelToSnake(String str) {
return str.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
}
}
Entity를 관리하는 EntityManager에서 각 엔티티의 이름을 가져와서,
직접 TRUNCATE 쿼리와 테이블 이름을 합쳐서 실행시켜줬습니다.
이때, @ElementCollection을 사용해야하는 기능이 있었고, 해당 기능을 개발하고 테스트를 해보니
위의 클래스에서 @ElementCollection의 테이블 데이터 초기화를 해주지 않아서 발생하는 문제로 파악되었습니다.
2. 문제 원인
@Component
public class H2TruncateUtils {
@Autowired
private EntityManager em;
@Transactional
public void truncateAll() {
em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
final List<String> tableNames = em.getMetamodel().getEntities().stream()
.map(entityType -> {
final String name = entityType.getName();
return camelToSnake(name);
})
.toList();
for (String tableName : tableNames) {
em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
private String camelToSnake(String str) {
return str.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
}
}
그렇다면, 위의 클래스에서 왜 @ElementCollection으로 생성된 테이블은 동적으로 가져오지 못하는 걸까요?
바로, EntityManager의 getMetamodel().getEntities으로는 엔티티들만 가져오기 때문입니다.
따라서, EntityManager에서 @ElementCollection 유형을 가진 테이블을 가져와야 합니다.
3. 해결 (동적으로 @ElementCollection 가져오기)
저는 EntityManger의 getMetamodel().getManagedTypes()를 통해 다음과 같이 구현했습니다.
@Component
public class H2TruncateUtils {
@Autowired
private EntityManager em;
@Transactional
public void truncateAll() {
em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
final List<String> tableNames = em.getMetamodel().getEntities().stream()
.map(entityType -> {
final String name = entityType.getName();
return camelToSnake(name);
})
.toList();
for (String tableName : tableNames) {
em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
List<String> elementCollectionTableNames = em.getMetamodel().getManagedTypes().stream()
.flatMap(managedType -> managedType.getPluralAttributes().stream())
.filter(attributes -> attributes.getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION)
.map(attribute -> {
final String camelDeclaringType = camelToSnake(attribute.getDeclaringType().toString());
final String camelAttributeName = camelToSnake(attribute.getName());
return camelDeclaringType + "_" + camelAttributeName;
})
.toList();
for (String elementCollectionTableName : elementCollectionTableNames) {
em.createNativeQuery("TRUNCATE TABLE " + elementCollectionTableName).executeUpdate();
}
em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
private String camelToSnake(String str) {
return str.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
}
}
위와 같이 ElementCollection 유형을 찾고, @ElementCollection의 테이블 이름 기본 생성 규칙에 따라서
Truncate할 테이블 이름을 지정해줬습니다.
(기본 생성 규칙 : 엔티티 이름 + '_' + @ElementCollection 필드 이름)
이렇게 동적으로 @ElementCollection의 테이블을 가져와서 Truncate를 해주면서 문제를 해결할 수 있었습니다.
4. 한계
위와 같은 방법으로 문제를 해결하긴 했지만, 한계점도 있다고 생각됩니다.
위에서는 완전하게 동적으로 테이블 이름을 가져와서 초기화하는 것이 아니라,
@ElementCollection의 테이블 기본 생성 규칙에 따라서 직접 테이블 이름을 생성해주고 있습니다.
만약 해당 테이블이 비즈니스 로직을 포함한 중요한 테이블일 경우 테이블 이름을 변경해야 할 것입니다.
이렇게 되면 결국 모든 @ElementCollection을 한번에 동적으로 가져올 수 없기 때문에 이러한 한계가 있을 것 같습니다.
이름을 커스텀한 @ElementCollection 테이블이 많아진다면 수동으로 Truncate.sql을 만들어서 관리하는 방법도
고려해볼 필요가 있을 것 같습니다.
'Spring > JPA' 카테고리의 다른 글
[JPA] JPA, Spring Data JPA의 내부 동작 원리 알아보기 (0) | 2024.08.05 |
---|---|
[JPA] JPA N+1 문제 및 근본적인 원인에 대한 개인적인 고찰 (6) | 2024.04.05 |
[JPA] JPA 1:N 관계에서 연관관계 주인을 1 대신 N에 두는 이유 (4) | 2023.08.06 |
[JPA] @JoinColumn 파헤치기 (feat. JPA 연관관계별 사용) (6) | 2023.07.09 |