블로그 이름 뭐하지
[트러블슈팅] 더미데이터 만들어 조회하기 본문
해당 작업을 시도하게 된 계기
프로젝트에서 관리자 페이지를 맡았다.
해당 페이지는 관리자가 유저에게 보낸 메세지의 히스토리를 관리하는 작업을 포함했는데,
한 유저당 하나의 히스토리 데이터가 만들어지므로 유저가 많다면 하루에도 수천개의 데이터가 쌓이게 된다.
그런 작업을 테스트하기 위해서는 대용량의 더미데이터가 필요하다고 생각했고,
그와 동시에 발생하는 성능 문제를 해결하고자 했다.
1. 테스트 코드에서 더미데이터 만들기
실제로 더미데이터를 DB에 저장해야하는 작업을 시행해야 하므로 Mockito가 아닌 SpringBootTest를 사용한다. 최종 목표는 10만개 이상의 데이터를 생성하는 것이지만 빠른 테스트를 위해 1만개부터 생성이 되는지 확인한다.
measurePerformance()는 더미데이터 생성에 드는 시간을 정확히 측정하기 위해 만든 메서드이고,
makeDummyData()는 더미데이터를 생성하는 메서드이다.
History는 user와 news를 의존하고 있기 때문에 10개씩 미리 생성해두고,
1-10까지 랜덤하게 id를 찾아 객체를 찾아오는 방식을 사용했다.
@SpringBootTest
public class HistoryTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
@Test
void measurePerformance() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
makeDummyData();
stopWatch.stop();
System.out.println("걸린 시간: " + stopWatch.getTotalTimeMillis() + " ms");
}
@Test
void makeDummyData(){
for(long i = 0; i < 10000; i++){
newsHistory();
}
}
Long randomNumber = (long)(Math.random()*10);
void newsHistory(){
Long userId = randomNumber;
Long newsId = randomNumber;
User user = userRepository.findById(userId).get();
News news = newsRepository.findById(newsId).get();
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.build();
newsHistoryRepository.save(newsHistory);
}
}
#1. 문제 발생_ 모듈을 찾을 수 없는 오류
[Test worker] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [itcast.HistoryTest]: HistoryTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
Found multiple @SpringBootConfiguration annotated classes [Generic bean: class=itcast.AdminApplication; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\user\IdeaProjects\it-cast\admin\build\classes\java\main\itcast\AdminApplication.class], Generic bean: class=itcast.CommonApplication; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in URL [jar:file:/C:/Users/user/IdeaProjects/it-cast/common/build/libs/common-0.0.1-SNAPSHOT-plain.jar!/itcast/CommonApplication.class]]
java.lang.IllegalStateException: Found multiple @SpringBootConfiguration annotated classes [Generic bean: class=itcast.AdminApplication; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\user\IdeaProjects\it-cast\admin\build\classes\java\main\itcast\AdminApplication.class], Generic bean: class=itcast.CommonApplication; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in URL [jar:file:/C:/Users/user/IdeaProjects/it-cast/common/build/libs/common-0.0.1-SNAPSHOT-plain.jar!/itcast/CommonApplication.class]]
at org.springframework.util.Assert.state(Assert.java:101)
위와 같은 오류가 발생하며 테스트 실행이 되지 않았다.
에러 메세지를 보면 `Found multiple @SpringBootConfiguration annotated classes`
즉, @SpringBootConfiguration이 여러 개라 그 중 어떤 걸 테스트 하려하는지 찾을 수 없다는 뜻인 것 같았다.
현재 프로젝트는 멀티 모듈을 사용하고 있었으므로 해당 모듈에 대한 테스트라는 것을 정확히 인지시켜줄 필요가 있었다. 나는 그 중에서도 AdminApplication에 대한 테스트를 진행하고 있어 다음과 같이 기입해 주었다.
@SpringBootTest(classes = AdminApplication.class)
#2. 문제 발생_Jwt key 오류
다시 실행하려 하니 무지막지하게 긴 오류가 발생했다.
아래를 살피니 다음과 같은 오류 원인을 찾을 수 있었다.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'authCheckInterceptor' defined in URL [jar:file:/C:/Users/user/IdeaProjects/it-cast/common/build/libs/common-0.0.1-SNAPSHOT-plain.jar!/itcast/jwt/AuthCheckInterceptor.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
Caused by: org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'jwt.secret.key' in value "${jwt.secret.key}"
User를 사용하기 위해서는 Jwt가 필요한데, 현재 Jwt secret 키가 없다는 뜻인 것 같다. 당시의 나는 테스트를 진행하는 모듈에만 키가 있으면 된다고 생각했는데, 테스트에도 별개로 키가 필요한 것이었다. Run > Edit Configurations 로 환경설정에 들어가 사용하는 테스트의 Environment variables에 키 값을 넣자 해결되었다.
#3. 성능개선_select 쿼리 제거

Hibernate:
select
u1_0.id,
u1_0.article_type,
u1_0.created_at,
u1_0.email,
u1_0.interest,
u1_0.kakao_email,
u1_0.modified_at,
u1_0.nickname,
u1_0.phone_number,
u1_0.sending_type
from
users u1_0
where
u1_0.id=?
Hibernate:
select
n1_0.id,
n1_0.content,
n1_0.created_at,
n1_0.interest,
n1_0.link,
n1_0.modified_at,
n1_0.original_content,
n1_0.published_at,
n1_0.rating,
n1_0.send_at,
n1_0.status,
n1_0.thumbnail,
n1_0.title
from
news n1_0
where
n1_0.id=?
Hibernate:
/* insert for
itcast.domain.newsHistory.NewsHistory */insert
into
news_history (created_at, modified_at, news_id, user_id)
values
(?, ?, ?, ?)
findById를 이용해서 하나씩 객체를 가져오고, 각각의 객체를 가져올 때마다 save를 진행하다보니
데이터 하나를 저장하는데도 select 2번, insert 1번. 총 3개의 쿼리가 발생했다.
효율성이 떨어지고 시간과 리소스를 많이 잡아먹는 방식이므로, 코드 개선이 필요했다.
▶ 개선한 코드
select 쿼리가 매번 발생하지 않도록 user와 news 객체를 findAll로 미리 가져와 List에 저장했다.
이후 가져온 List의 크기만큼 랜덤값을 발생시켜 객체를 넣어 DB에 저장한다.
@SpringBootTest(classes = AdminApplication.class)
public class HistoryTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
List<User> users;
List<News> newsList;
@BeforeEach
void init() {
users = userRepository.findAll();
newsList = newsRepository.findAll();
}
@Test
void measurePerformance() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
makeDummyData();
stopWatch.stop();
System.out.println("걸린 시간: " + stopWatch.getTotalTimeMillis() + " ms");
}
@Test
void makeDummyData(){
for(long i = 0; i < 10000; i++){
User user = users.get((int) (Math.random() * users.size()));
News news = newsList.get((int) (Math.random() * newsList.size()));
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.build();
newsHistoryRepository.save(newsHistory);
}
}
}

Hibernate:
/* insert for
itcast.domain.newsHistory.NewsHistory */insert
into
news_history (created_at, modified_at, news_id, user_id)
values
(?, ?, ?, ?)
무수한 select 쿼리가 사라지면서 시간이 대폭 줄어들었다.
133,000ms > 58,000ms (약 56.39% 속도향상)
#4. 성능개선_saveAll로 저장
데이터 하나를 저장할 때, save가 한 번씩 발생하는 것은 비효율적이다.
user와 news를 findAll로 찾아 List로 한 번에 저장했던 것처럼,
history도 List로 한번에 저장했다가 saveAll을 하면 insert 쿼리의 양을 줄일 수 있을 것 같았다.
@SpringBootTest(classes = AdminApplication.class)
public class HistoryTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
@Test
void measurePerformance() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
makeDummyData();
stopWatch.stop();
System.out.println("걸린 시간: " + stopWatch.getTotalTimeMillis() + " ms");
}
@Test
void makeDummyData() {
List<User> users = userRepository.findAll();
List<News> newsList = newsRepository.findAll();
List<NewsHistory> newsHistories = new ArrayList<>();
for (long i = 0; i < 10000; i++) {
User user = users.get((int) (Math.random() * users.size()));
News news = newsList.get((int) (Math.random() * newsList.size()));
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.build();
newsHistories.add(newsHistory);
if (newsHistories.size() % 100 == 0) {
newsHistoryRepository.saveAll(newsHistories);
newsHistories.clear();
}
}
if (!newsHistories.isEmpty()) {
newsHistoryRepository.saveAll(newsHistories);
}
}
}

Hibernate:
/* insert for
itcast.domain.newsHistory.NewsHistory */insert
into
news_history (created_at, modified_at, news_id, user_id)
values
(?, ?, ?, ?)
100개의 데이터를 한번에 insert할 것이라는 예상과 다르게 여전히 하나의 데이터마다 insert 쿼리가 붙었지만, 이상하게도 큰 폭으로 속도가 향상되었다.
58,000ms > 11,000ms (약 81.03% 속도향상)
왜 데이터가 한 번에 저장되지 않은걸까?
찾아보니 news_history의 id에 붙은 auto_increment 속성 때문이었다.
auto_increment는 엔티티의 기본 키가 GenerationType.IDENTITY 속성일 때 적용되는데, 해당 속성의 경우 insert가 수행된 이후에 ID 값을 반환받는다. saveAll로 배치 처리를 할 경우에는 Insert Into ... values (...), (...), (...) 식으로코드를 한 번에 삽입받게 되므로, Id 값이 지정되지 못해 사용할 수 없게 되는 것이다.
배치 처리도 되지 않았는데 왜 속도가 향상한 걸까?
100개 데이터를 List에 저장 후 saveAll을 호출하여 메모리를 효율적으로 사용했고,
10000개의 데이터를 100번의 트랜잭션으로 처리하면서, 트랜잭션 생성/종료 시에 들어가는 부하를 줄였다.
또한 100개를 처리한 이후 clear로 캐시를 초기화하여 불필요한 저장을 방지했기 때문이다.
그럼 트랜잭션이 줄어들면 속도도 같이 줄어들겠네?
그런 생각을 가지고 1000개당 하나의 트랜잭션을 설정했더니 100개를 설정할 때와 별 차이가 없었다.
혹시 데이터 양이 부족해서 수치가 나타나지 않나 싶어 총 데이터를 10만개로 지정하고,
각각 10000개, 1000개 당 하나의 트랜잭션을 설정했더니 결과가 아래와 같았다.


아주 조금 개선되긴 했으나 큰 차이를 보이지 않는 것을 알 수 있다.
이유는 어플리케이션이 DB와 통신하는 속도와 DB가 데이터를 저장하는 속도가 그대로이기 때문이다.
또한 배치크기가 늘어나면 리스트에 더 많은 객체가 쌓여 되려 성능저하를 일으키기 때문에
트랜잭션이 줄어든다고 해서 속도가 드라마틱하게 감소하지 않는 것이다.
2. 만들어진 더미데이터를 조회하기
테스트할 더미데이터가 완성되었으니, 이제 조회를 해보자.
더미데이터는 말그대로 테스트 용도이므로 조회 후 삭제하는 것이 좋다고 판단했다.
@BeforeEach과, @AfterEach를 사용해 더미데이터를 생성, 삭제하고 조회 테스트를 진행한다.
@SpringBootTest(classes = AdminApplication.class)
public class HistoryTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
@BeforeEach
@DisplayName("1만개 더미데이터 생성")
void makeDummyData() {
List<User> users = userRepository.findAll();
List<News> newsList = newsRepository.findAll();
List<NewsHistory> newsHistories = new ArrayList<>();
for (long i = 0; i < 10000; i++) {
User user = users.get((int) (Math.random() * users.size()));
News news = newsList.get((int) (Math.random() * newsList.size()));
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.build();
newsHistories.add(newsHistory);
if (newsHistories.size() % 100 == 0) {
newsHistoryRepository.saveAll(newsHistories);
newsHistories.clear();
}
}
if (!newsHistories.isEmpty()) {
newsHistoryRepository.saveAll(newsHistories);
}
}
@Test
@DisplayName("히스토리 조회 성공")
public void successNewsHistoryRetrieve() {
// Given
Long userId = 1L;
Long newsId = null;
LocalDate createdAt = LocalDate.now();
int page = 0;
int size = 20;
Pageable pageable = PageRequest.of(page, size);
// When
Page<AdminNewsHistoryResponse> newsHistories = newsHistoryRepository.findNewsHistoriesByCondition(userId, newsId, createdAt, pageable);
// Then
assertNotNull(newsHistories);
assertFalse(newsHistories.isEmpty());
assertEquals(1L, newsHistories.getContent().get(6).userId());
assertEquals(20, newsHistories.getContent().size());
}
@AfterEach
@DisplayName("더미데이터 삭제")
void deleteDummyData() {
newsHistoryRepository.deleteAll();
}
}

Hibernate:
/* delete for
itcast.domain.newsHistory.NewsHistory */delete
from
news_history
where
id=?
10,000개의 더미데이터 생성에 걸린 시간은 평균적으로 11초 가량이었는데, 삭제가 추가되자 두 배가 되었다. 생성할 때처럼 하나씩 삭제하고 있기 때문이다. 무척 비효율적인데다, 삭제 쿼리로 뒤덮여 정작 확인해야하는 조회 쪽 로그는 보이지도 않아 개선이 필요해 보였다.
#4. 성능개선_delete 쿼리 재설계
deleteAll은 saveAll과 마찬가지로 각 Entity에 개별적으로 Delete 쿼리를 실행하므로,
JPA가 아닌, NativeQuery를 이용해 한 번에 삭제를 진행한다.
NativeQuery는 순수한 쿼리로, JPQL이 아닌 SQL을 직접 정의한다.
쿼리문을 직접적으로 실행시켜 속도가 빠르고, SQL 언어를 전체적으로 사용할 때 유용하다.
//NewsHistoryRepository
public interface NewsHistoryRepository extends JpaRepository<NewsHistory, Long>, CustomNewsHistoryRepository {
@Modifying
@Query(value = "DELETE FROM news_history", nativeQuery = true)
void deleteAllDummyData();
}
//테스트 코드
@SpringBootTest(classes = AdminApplication.class)
public class HistoryTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
@BeforeEach
@DisplayName("1만개 더미데이터 생성")
void makeDummyData() {
List<User> users = userRepository.findAll();
List<News> newsList = newsRepository.findAll();
List<NewsHistory> newsHistories = new ArrayList<>();
for (long i = 0; i < 10000; i++) {
User user = users.get((int) (Math.random() * users.size()));
News news = newsList.get((int) (Math.random() * newsList.size()));
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.build();
newsHistories.add(newsHistory);
if (newsHistories.size() % 100 == 0) {
newsHistoryRepository.saveAll(newsHistories);
newsHistories.clear();
}
}
if (!newsHistories.isEmpty()) {
newsHistoryRepository.saveAll(newsHistories);
}
}
@Test
@DisplayName("히스토리 조회 성공")
public void successNewsHistoryRetrieve() {
// Given
Long userId = 1L;
Long newsId = null;
LocalDate createdAt = LocalDate.now();
int page = 0;
int size = 20;
Pageable pageable = PageRequest.of(page, size);
// When
Page<AdminNewsHistoryResponse> newsHistories = newsHistoryRepository.findNewsHistoriesByCondition(userId, newsId, createdAt, pageable);
// Then
assertNotNull(newsHistories);
assertFalse(newsHistories.isEmpty());
assertEquals(1L, newsHistories.getContent().get(6).userId());
assertEquals(20, newsHistories.getContent().size());
}
@AfterEach
@DisplayName("더미데이터 삭제")
void deleteDummyData() {
newsHistoryRepository.deleteAllDummyData();
}
}
#5. 문제 발생_ @Transactional 누락 오류
Executing an update/delete query
Executing an update/delete query
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query
jakarta.persistence.TransactionRequiredException: Executing an update/delete query
at app//org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:559)
at app//org.hibernate.query.spi.AbstractQuery.executeUpdate(AbstractQuery.java:647)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-12-30T15:43:22.073+09:00 INFO 28032 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-12-30T15:43:22.075+09:00 INFO 28032 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-12-30T15:43:22.093+09:00 INFO 28032 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
> Task :admin:test
HistoryTest > 히스토리 조회 성공 FAILED
org.springframework.dao.InvalidDataAccessApiUsageException at HistoryTest.java:89
Caused by: jakarta.persistence.TransactionRequiredException at HistoryTest.java:89
1 test completed, 1 failed
> Task :admin:test FAILED
[Incubating] Problems report is available at: file:///C:/Users/user/IdeaProjects/it-cast/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':admin:test'.
> There were failing tests. See the report at: file:///C:/Users/user/IdeaProjects/it-cast/admin/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
org.springframework.dao.InvalidDataAccessApiUsageException at HistoryTest.java:89
Caused by: jakarta.persistence.TransactionRequiredException at HistoryTest.java:89
에러가 뜬다.
insert/update/delete 시에 @Transactional을 기입하지 않아 생기는 오류이다.
JPA를 사용한 것이 아닌데도 왜 @Transactional이 필요한걸까?
@Transactional은 데이터가 변경이 될 때 일관적인 데이터를 유지하기 위해 필요한 것으로,
JPA가 아니더라도 변경사항이 있으면 변경 작업의 일환으로 간주된다.
사용한 native Query의 경우 delete로 모든 데이터를 삭제하여 변경사항이 발생했으므로,
아래와 같이 @Transactional을 기입하여, DB와 자동으로 동기화 시켜야한다.
@Transactional
@SpringBootTest(classes = AdminApplication.class)
public class HistoryTest {
에러를 해결하면 아래와 같이 삭제 쿼리가 한 번만 발생하며, 그와 동시에 속도가 향상된 것을 확인할 수 있다.
21,705ms > 12,783ms (약 41.10% 속도향상)

#6. 성능개선_조회에 인덱싱 설정
인덱싱(indexing)이란 검색의 범위를 좁혀 조회 속도를 향상시키기 위한 것이다.
예를 들어 10만 개의 데이터가 있고, 내가 찾는 데이터가 DB의 가장 마지막에 위치한다면
10만개를 전부 조회해서 찾아내게 되므로 비효율적이다.
이럴 때 내가 찾고자 하는 데이터의 컬럼에 인덱싱을 걸게되면
해당 값을 기준으로 찾게되므로 더욱 효율적이고 빠르게 처리할 수 있게 된다.
하지만 아무 곳에나 인덱싱을 걸게되면 조회 외의 다른 기능을 사용할 때,
인덱스 또한 업데이트를 진행하여 성능이 저하될 수 있다.
인덱스를 사용하는 경우는 다음과 같다.
1) 유일한 데이터의 개수가 높은 컬럼 (즉 값의 중복이 적은 경우)
2) 활용도가 높은 컬럼 (where 절에서 자주 사용되는 경우)
아래는 1만개, 10만개의 데이터를 기준으로 인덱싱을 적용하기 전후의 결과 값을 캡처한 것이다.




각각 786ms > 717ms (8.77% 속도 개선), 1046ms > 967ms (8.50% 속도 개선) 의 결과 값이 나왔다.
개선 효과가 미미한 이유는 더미데이터에 사용한 userId와 newsId, createdAt에 중복된 값이 많기 때문이다.
history의 경우 변경할 일이 없어, 효과가 미미해도 인덱싱을 적용하는 것이 성능 개선에 도움이 될 것이라 판단했다.
#7. 문제 발견_ 테스트 코드 실행시 기존의 데이터가 날아가는 문제
현재의 코드는 테스트 코드의 더미데이터를 생성, 삭제하는 과정에서 기존의 데이터가 전부 날아가게 되는 구조이다.
더미데이터 생성, 삭제 과정을 제거하거나 새로운 DB를 사용하는 것이 더 좋겠지만, 일단 컬럼(boolean is_dummy) 하나를 더 만들어 테스트 코드와 기존 데이터를 분리시키는 방안을 선택했다.
//History Entity
@Getter
@Entity
@Table(name = "news_history", indexes = {
@Index(name = "idx_user_news_created", columnList = "news_id, user_id, created_at")})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class NewsHistory extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "news_id")
private News news;
@Column(name = "is_dummy", nullable = false)
private boolean isDummy;
//더미데이터 생성용 빌더
@Builder
public NewsHistory(User user, News news, boolean isDummy) {
this.user = user;
this.news = news;
this.isDummy = isDummy;
}
}
//Repository
public interface NewsHistoryRepository extends JpaRepository<NewsHistory, Long>, CustomNewsHistoryRepository {
@Modifying
@Query(value = "DELETE FROM news_history where is_dummy = true", nativeQuery = true)
void deleteAllDummyData();
}
//test code
@Transactional
@SpringBootTest(classes = AdminApplication.class)
public class AdminNewsHistoryServiceTest {
@Autowired
NewsHistoryRepository newsHistoryRepository;
@Autowired
UserRepository userRepository;
@Autowired
NewsRepository newsRepository;
@BeforeEach
@DisplayName("10만개 더미데이터 생성")
void makeDummyData() {
List<User> users = userRepository.findAll();
List<News> newsList = newsRepository.findAll();
List<NewsHistory> newsHistories = new ArrayList<>();
for (long i = 0; i < 100000; i++) {
User user = users.get((int) (Math.random() * users.size()));
News news = newsList.get((int) (Math.random() * newsList.size()));
NewsHistory newsHistory = NewsHistory.builder()
.user(user)
.news(news)
.isDummy(true)
.build();
newsHistories.add(newsHistory);
if (newsHistories.size() % 1000 == 0) {
newsHistoryRepository.saveAll(newsHistories);
newsHistories.clear();
}
}
if (!newsHistories.isEmpty()) {
newsHistoryRepository.saveAll(newsHistories);
}
}
@Test
@DisplayName("히스토리 조회 성공")
public void successNewsHistoryRetrieve() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// Given
Long userId = 1L;
Long newsId = 2L;
LocalDate createdAt = LocalDate.now();
int page = 0;
int size = 20;
Pageable pageable = PageRequest.of(page, size);
// When
Page<AdminNewsHistoryResponse> newsHistories = newsHistoryRepository.findNewsHistoriesByCondition(userId, newsId, createdAt, pageable);
// Then
assertNotNull(newsHistories);
assertFalse(newsHistories.isEmpty());
stopWatch.stop();
System.out.println("걸린 시간: " + stopWatch.getTotalTimeMillis() + " ms");
}
@AfterEach
@DisplayName("더미데이터 삭제")
void deleteDummyData() {
newsHistoryRepository.deleteAllDummyData();
}
}
적용 후기
처음에는 그냥 조회에 인덱싱을 적용해서 성능을 개선해보고 싶은 마음에 시작한 것이었는데, 배보다 배꼽이 더 커지는 바람에 당황스러웠다. 게다가 데이터 값 문제로 인덱싱은 개선 효과도 별로 없었다. 그래도 직접 성능이 개선되는 것을 확인하니 뿌듯하고, 다음에는 스레드풀을 이용해 user와 news를 비동기로 생성하여, 제대로 된 인덱싱 효과를 확인하고 싶다는 생각이 들었다.
'트러블슈팅' 카테고리의 다른 글
[트러블슈팅] MySql 비밀번호를 잊어버렸을 때 & MySql 재설치 (0) | 2024.09.26 |
---|