🚨서론
시퀀스를 테스트하기위해 Mock Data를 넣던 도중 너무 많은 소요시간이 걸려 이러한 문제를 해결하기위해 멀티 스레드를 활용하였습니다.
✌️본론
기존에 저는 Mock Data를 넣을때 ApplicationRunner를 상속 받아 서비스가 실행될 때 단순 반복문을 통해 목데이터를 넣어주는 방식을 사용했습니다. 하지만 많은 데이터를 넣어 부하테스트를 해보고 싶은 욕심에 10만개의 데이터를 넣어봤습니다! 결과는 약 25분 후에야 유저에 대한 목데이터가 10만개가 넣어졌습니다.😂
이 문제를 해결하기 위해 멀티 스레드를 활용하여 코드를 리팩토링 하였습니다.
MockDataInializer에서는 전체 유저수, 배치 사이즈, 스레드를 설정해주었습니다. 또한 스레드를 생성하여 목데이터를 생성하는 실제 로직이 담겨있는 generateUserBatch를 각 스레드가 호출하도록 구현하였습니다.
별도의 서비스로 분리한 이유는 @Transactional이 동작하도록 하기 위함과, 추후에 User서비스이외에도 다양한 목 데이터를 넣어주기 위하여 분리하였습니다.
전체 코드는 다음과 같습니다.
MockDataInitializer
package sequence.sequence_member.global.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
@Component
@EnableAsync
@Slf4j
@RequiredArgsConstructor
public class MockDataInitializer implements ApplicationRunner {
private final DataCreateService dataCreateService;
private static final int TOTAL_USERS = 100000;
private static final int BATCH_SIZE = 10000;
private static final int THREAD_POOL_SIZE = 10; // 병렬 실행할 스레드 개수
@Override
public void run(ApplicationArguments args) {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int batchNumber = 0; batchNumber < TOTAL_USERS / BATCH_SIZE; batchNumber++) {
int finalBatchNumber = batchNumber;
CompletableFuture<Void> future = CompletableFuture.runAsync(() ->
dataCreateService.generateUserBatch(finalBatchNumber, BATCH_SIZE), executorService);
futures.add(future);
}
// 모든 비동기 작업이 끝날 때까지 기다림
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allOf.join(); // 모든 작업이 끝날 때까지 대기
executorService.shutdown();
}
}
DataCreateService
package sequence.sequence_member.global.utils;
import com.github.javafaker.Faker;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sequence.sequence_member.global.enums.enums.*;
import sequence.sequence_member.member.entity.*;
import sequence.sequence_member.member.repository.MemberRepository;
@Service
@RequiredArgsConstructor
@Slf4j
public class DataCreateService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
private static final Faker faker = new Faker();
@Async
@Transactional
public CompletableFuture<Void> generateUserBatch(int batchNumber, int batchSize) {
List<MemberEntity> members = new ArrayList<>();
long startTime = System.currentTimeMillis(); // 시작 시간 측정
log.info("데이터 삽입 시작 (멀티스레드)");
String password = passwordEncoder.encode("password");
try {
for (int i = 0; i < batchSize; i++) {
MemberEntity member = new MemberEntity();
member.setUsername(batchNumber + "_" + i + "_username");
member.setPassword(password);
member.setName(batchNumber + "_" + i + "_" + faker.name().name());
member.setBirth(Date.from(faker.date().birthday(18, 50).toInstant().atZone(ZoneId.systemDefault()).toInstant()));
member.setGender(faker.bool().bool() ? MemberEntity.Gender.M : MemberEntity.Gender.F);
member.setAddress(faker.address().fullAddress());
member.setPhone(faker.numerify("010-####-####")); // 한국식 번호
member.setEmail(batchNumber + "_" + i + "_" + faker.internet().emailAddress());
member.setIntroduction(faker.lorem().sentence());
member.setPortfolio(faker.internet().url());
member.setNickname(batchNumber + "_" + i + "_" + faker.funnyName().name());
member.setSchoolName(faker.educator().university());
member.setProfileImg(faker.internet().avatar());
member.setDeleted(false);
// EducationEntity 생성
EducationEntity education = new EducationEntity(
faker.educator().university(),
faker.educator().course(),
faker.number().numberBetween(1, 6) + "학년",
Date.from(faker.date().past(2000, java.util.concurrent.TimeUnit.DAYS).toInstant().atZone(ZoneId.systemDefault()).toInstant()),
Date.from(faker.date().future(1000, java.util.concurrent.TimeUnit.DAYS).toInstant().atZone(ZoneId.systemDefault()).toInstant()),
faker.options().option(Degree.class),
List.of(faker.options().option(Skill.class)),
List.of(faker.options().option(ProjectRole.class)),
member
);
member.setEducation(education);
// 랜덤 Award 추가 (0~5개)
IntStream.range(0, faker.number().numberBetween(0, 6)).forEach(j -> member.getAwards().add(
new AwardEntity(
faker.options().option(AwardType.class),
faker.company().name(),
faker.book().title(),
Date.from(faker.date().past(1000, java.util.concurrent.TimeUnit.DAYS).toInstant().atZone(ZoneId.systemDefault()).toInstant()),
member
)
));
// 랜덤 Career 추가 (0~5개)
IntStream.range(0, faker.number().numberBetween(0, 6)).forEach(j -> {
LocalDate startDate = LocalDate.now().minusYears(faker.number().numberBetween(1, 5));
LocalDate endDate = startDate.plusYears(faker.number().numberBetween(1, 3));
member.getCareers().add(new CareerEntity(
faker.company().name(),
startDate,
endDate,
faker.lorem().sentence(),
member
));
});
// 랜덤 Experience 추가 (0~5개)
IntStream.range(0, faker.number().numberBetween(0, 6)).forEach(j -> {
LocalDate startDate = LocalDate.now().minusYears(faker.number().numberBetween(1, 5));
LocalDate endDate = startDate.plusYears(faker.number().numberBetween(1, 3));
member.getExperiences().add(new ExperienceEntity(
faker.options().option(ExperienceType.class),
faker.job().title(),
startDate,
endDate,
faker.lorem().sentence(),
member
));
});
members.add(member);
}
memberRepository.saveAll(members);
memberRepository.flush();
System.out.println("Batch " + batchNumber + " 저장 완료");
} catch (Exception e) {
System.err.println("Batch " + batchNumber + " 생성 중 에러 발생: " + e.getMessage());
e.printStackTrace();
}
long endTime = System.currentTimeMillis(); // 종료 시간 측정
long totalTime = endTime - startTime; // 총 시간 계산
log.info("데이터 삽입 완료");
log.info("전체 데이터 생성 시간: " + totalTime + "ms");
return CompletableFuture.completedFuture(null);
}
}
+ 추가로 기존 로직에서는 매번 password를 암호화하는 과정을 반복문 안에 넣어서 실행하였습니다.
String password = passwordEncoder.encode("password");
하지만 확인해보니 해당 로직에서 전체 99%를 차지하여 많은 시간이 소요되었습니다.
목데이터 이므로 비밀번호를 모두 동일하게 가져가기 때문에 비밀번호를 한번만 암호화하고 해당 암호화된 비밀번호를 전체 유저가 공유하도록 수정하였습니다.
이렇게 하니 10만개의 데이터를 기준으로 기존 약25분에서 4분으로 단축 될수 있었습니다.😊
정리
1. 대량의 목데이터를 생성할때는 멀티스레드를 활용하자.
2. 비밀번호 암호화 로직은 시간이 많이 소요된다. 한번만 실행하고 모든 비밀번호를 공유하도록 하자.
3. JMETER로 본격적인 부하테스트하기전에 기능 개발중에는 인텔리제이 기능을 활용해서 진행하면 편리할것 같다.
'IT 서비스 > 시퀀스' 카테고리의 다른 글
프론트에서 서버로 쿠키 전달 되지 않음(SameSite 정책 문제) (1) | 2025.04.07 |
---|---|
[ CI/CD] GithubAction + Docker를 활용한 SpringBoot CI/CD 과정 (0) | 2025.02.26 |
[시퀀스] 10명의 백엔드 개발자들은 어떻게 협업할까? (0) | 2024.12.21 |
댓글