프로젝트를 진행하던 중 대용량 데이터(백만건)를 넣고 조회 성능을 향상시키라는 요구사항이 있었다. 직접 포스트 맨에서 하나 하나 넣을 수 없으니 테스트 코드로 for문을 돌려 batchUpdate를 이용해 한 번에 저장할 수 있도록 했다.
그래서 통합 테스트를 하는데 테스트 자체가 ignored가 되는 문제가 발생했다.
이런 적은 처음이라 검색하던 중, @ContextConfiguration을 붙여주라는 글이 대부분이라 붙여줬더니 이젠 ignored가 되는 대신 다른 에러가 뜨기 시작했다. 읽어보니 의존성 주입이 안됐다는 오류인 것 같았다. 분명히 @Autowired를 붙여줬는데도 해결되지 않아 각 controller, service 계층에서 필요한 의존 관계에 전부 @Autowired를 달아주었다.
@SpringBootTest
@Transactional
@Commit
@ContextConfiguration(classes = UserController.class)
class UserControllerTest {
@Autowired
JdbcTemplate jdbcTemplate = new JdbcTemplate();
@Autowired
UserRepository userRepository;
@Autowired
UserService userService;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserController userController;
// test code ...
}
발생했던 에러
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'org.example.expert.domain.user.service.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.example.expert.domain.user.service.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Error creating bean with name 'userController': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'org.example.expert.domain.user.service.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.example.expert.domain.user.service.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
왜 이런 오류가 나타난 걸까?
결론부터 말하자면 @SpringBootTest와 @ContextConfiguration에 대해 깊이 이해하지 못한 탓에 발생한 오류였다. 하나씩 살펴보자.
@SpringBootTest
@SpringBootTest 어노테이션은 Spring Boot 테스트를 위한 어노테이션으로, Spring 애플리케이션 컨텍스트 전체를 로드하여 통합 테스트를 수행할 수 있게 해준다. 이 어노테이션을 사용하면 애플리케이션이 실제로 구동되는 환경과 유사한 환경에서 테스트를 진행할 수 있다.
전체 애플리케이션 컨텍스트를 로드한다는 뜻은 모든 빈(bean)과 설정(configuration)을 포함한 환경이 실제 구동 시와 동일하게 구성된다는 의미이다. 덕분에 애플리케이션의 구성 요소들이 정상적으로 동작하는지 통합적으로 확인할 수 있다.
- 빈 초기화
- 환경 설정
- 데이터베이스 설정
@SpringBootTest는 자동 설정 기능을 통해 필요한 모든 설정을 자동으로 적용한다. 즉, 수동으로 모든 의존성을 주입하거나 설정을 따로 추가할 필요 없이 Spring Boot가 알아서 필요한 설정을 관리해준다. 그렇기에 다양한 구성 요소들, 컨트롤러, 서비스, 레포지토리 등과 같은 것들이 잘 연결되어 동작하는지 확인할 수 있다.
따라서 내가 @Autowired를 붙여주든 안 붙이든 아~무런 상관이 없다는 것...
문제는 저곳이 아니었다.
@ContextConfiguration
@ContextConfiguration 어노테이션은 테스트에 사용할 애플리케이션 컨텍스트의 설정 파일이나 클래스를 지정하는데 사용된다. 주로 통합 테스트에서 활용되며, 다양한 방식으로 애플리케이션 컨텍스트를 로드하여 테스트를 지원한다.
주요 역할은 다음과 같다.
- 테스트를 위한 애플리케이션 컨텍스트 로딩
- @ContextConfiguration을 통해 Spring 애플리케이션 컨텍스트를 테스트 환경에서 로드하고, 해당 컨텍스트 내에서 선언된 빈(bean)들을 사용할 수 있게 한다.
- 설정 파일 또는 설정 클래스 지정
- 애플리케이션 컨텍스트를 설정하기 위해 XML 설정 파일이나 자바 설정 클래스를 지정할 수 있다. 지정한 파일 또는 클래스에 따라 테스트에 필요한 빈을 로드하고 사용할 수 있다.
[XML 설정 파일을 사용하는 경우]
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MyTest {
@Autowired
private MyService myService;
@Test
public void testService() {
assertNotNull(myService);
}
}
주로 Spring 3 이전에 사용되었다.
locations로 설정 파일 경로를 지정하고 classpath:로 클래스 패스에서 파일을 찾는다.
xml 설정 파일에서 정의된 빈을 테스트에서 @Autowired 등을 통해 주입받을 수 있다.
[자바 설정 클래스를 사용하는 경우]
@ContextConfiguration(classes = AppConfig.class)
public class MyTest {
@Autowired
private MyService myService;
@Test
public void testService() {
assertNotNull(myService);
}
}
classes 속성을 통해 자바 설정 클래스를 지정한다. 여기서 AppConfig는 @Configuration 어노테이션이 적용된 설정 클래스다.
설정 클래스에서 선언된 빈들을 @Autowired로 주입받아 사용할 수 있다.
[다수의 설정 파일 또는 설정 클래스 지정]
@ContextConfiguration(classes = {AppConfig.class, SecurityConfig.class})
public class MyTest {
@Autowired
private MyService myService;
@Autowired
private SecurityService securityService;
@Test
public void testServices() {
assertNotNull(myService);
assertNotNull(securityService);
}
}
여러 개의 설정 파일이나 설정 클래스를 지정할 수 있다. 이 경우, Spring은 지정된 모든 설정을 로드하여 하나의 애플리케이션 컨텍스트를 만든다.
결국 @ContextConfiguration은 적절한 애플리케이션 컨텍스트 설정을 통해 테스트에서 필요한 빈만 로드하도록 설정을 최소화하는 역할을 한다. 이를 통해 설정 파일 또는 자바 설정 클래스를 지정하여 테스트에 필요한 빈만을 로드하고도 통합 테스트를 진행할 수 있다. 복잡한 애플리케이션의 일부만 테스트하거나 필요한 설정만 로드하여 테스트를 효율적으로 진행할 수 있는 것이다.
차이점이 보이는지...
@SpringBootTest vs @ContextConfiguration
SpringBootTest는 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 수행한다. Spring Boot의 자동 구성 기능을 활용할 수 있다.
근데 ContextConfiguration은 좀 더 세밀한 설정을 위해 특정 설정 파일이나 클래스를 지정하여 컨텍스트를 로드할 때 사용된다. Spring Boot 자동 구성이 필요 없거나, 특정 설정만 테스트하고 싶을 때 유용하다.
이 둘을 함께 사용하니 에러가 터진 것이었다.
1. 중복된 컨텍스트 로드
@SpringBootTest는 전체 Spring Boot 애플리케이션 컨텍스트를 로드하고, 애플리케이션의 자동 설정 및 빈을 활성화한다. 반면, @ContextConfiguration은 지정한 설정 파일이나 설정 클래스를 기반으로 별도의 컨텍스트를 로드한다.
이 두 어노테이션을 함께 사용하면 두 개의 컨텍스트가 동시에 로드될 수 있다.
- 빈 충돌: 동일한 빈이 서로 다른 컨텍스트에서 로드되면 빈의 중복 등록으로 인해 주입 과정에서 에러가 발생 가능
- 의존성 주입 실패: 특정 빈이 올바르게 로드되지 않거나, 여러 컨텍스트에서 혼란을 초래해 주입이 실패할 가능
2. 설정 파일 간의 불일치
@SpringBootTest는 자동 설정을 통해 Spring Boot의 다양한 설정을 활성화한다. 그런데 @ContextConfiguration을 통해 별도의 설정 파일을 로드하면 불일치한 설정이 존재할 수 있다. 이 경우, 빈의 생성 또는 주입이 예상과 다르게 동작할 수 있는 것이다.
예를 들어 Spring Boot의 application.properties 또는 application.yml 파일에 정의된 설정과 @ContextConfiguration에서 로드된 설정 파일이 서로 충돌할 때, 빈을 제대로 생성하지 못하거나 주입이 실패할 수 있다.
3. 컨텍스트 로딩 순서 문제
@SpringBootTest는 애플리케이션의 자동 설정과 함께 컨텍스트를 전역적으로 로드하지만, @ContextConfiguration은 별도의 설정에 따라 특정 부분만 로드할 수 있다. 이때, 로딩 순서가 잘못되어 빈이 아직 로드되지 않았는데 주입을 시도하는 상황이 발생할 수 있다.
특히 빈이 의존하고 있는 다른 빈들이 아직 로드되지 않은 상태에서 주입을 시도하면 NoSuchBeanDefinitionException 또는 NullPointerException 같은 에러가 발생할 수 있다.
나는 이 중 1번과 3번의 문제가 발생한 것이었다.
@ContextConfiguration을 제거한 후 빈 테스트 코드를 다시 돌려보니....
짠! 성공했다.
인생은 허무한 것이다.
Test ignored?
그럼 왜 백만 건의 유저를 저장하는 테스트가 ignored 되면서 아예 실행도 되지 않았을까?
예측 가능한 이유는 다음과 같다.
1. 메모리 부족 문제
대규모 데이터를 한 번에 처리하려면 모든 데이터를 메모리에 로드하고 유지해야 한다. 백만 건의 유저 데이터를 리스트에 저장한 후 이를 한 번에 데이터베이스에 저장하려는 시도는 JVM 힙 메모리 한계를 초과할 가능성이 높다. 특히 생성되는 유저 데이터가 객체로 만들어지고, 메모리 안에서 유지된 상태로 저장을 시도하면 JVM 메모리가 부족해져 테스트가 실패할 수 있는 것이다.
Spring Boot 테스트 환경에서 @SpringBootTest는 전체 애플리케이션 컨텍스트를 로드하는데, 여기에 백만 건의 데이터를 한 번에 저장하려고 할 때 JVM이 충분한 메모리를 제공하지 못할 수 있다. 따라서 메모리 부족으로 인해 테스트가 무시되거나 OutOfMemoryError 같은 예외가 발생할 수 있다.
2. 트랜잭션 처리 한계
한 번에 대규모 데이터를 저장할 때 트랜잭션 처리에도 부담이 생긴다. 일반적으로 한 번에 대량의 데이터를 데이터베이스에 삽입할 경우, 트랜잭션 크기가 너무 커져서 성능 저하나 장애가 발생할 수 있기 때문이다.
결국 트랜잭션 오버헤드 문제인데, 트랜잭션 내에서 한 번에 너무 많은 데이터를 처리하면 데이터베이스에서 트랜잭션 관리에 더 많은 시간을 소비하게 되고 이로 인해 성능이 저하되거나 트랜잭션이 중단, 실패할 수 있다.
해결방법
1000건을 저장했을 땐 아무런 문제가 없어서 결국 1000건씩 나눠 저장함으로써 해결했다. 이를 Batch Insert라고 하는데, 대량 데이터를 처리할 때는 배치 처리를 사용하는 것이 일반적이다. 한 번에 너무 많은 데이터를 저장하는 대신 적절한 크기로 분할하여 Batch Insert를 사용하면 성능도 향상시킬 수 있고 메모리 사용량도 줄일 수 있기 때문이다.
이를 통해 메모리와 트랜잭션에 부담을 줄일 수 있었다.
'내배캠 > TIL' 카테고리의 다른 글
gateway http header too large 오류 해결 (1) | 2024.11.15 |
---|---|
[AWS] RDS 생성 및 연동하기 (4) | 2024.10.09 |
락(Lock) 이란? (1) | 2024.10.09 |
[스프링] JWT를 이용한 Spring Security 구현 (문제 & 해결) (1) | 2024.10.05 |
[스프링] 트랜잭션 활용과 주의사항 (1) | 2024.10.01 |