During a project, a major issue has arisen... It is..
The issue occurs when the "Book Continuously" button is pressed repeatedly, causing the error shown above and resulting in the service crashing.
he booking process takes 2 seconds.
From the user's perspective, if it takes too long, they may press the "Book" button repeatedly, leading to errors.
On the second press, a duplicate reservation exception occurs. However, on the third press and onwards…
A database error occurs as follows, and this error propagates to the front end, causing users to see the exception. To sort out this issue, I resolved to learn and improve.
@Transactional
public Long save(Long crewId, ReservationReserveRequest reservationReserveRequest) {
// 예약 테이블관련 작업
Crew crew = crewRepository.findById(crewId)
.orElseThrow(NotFoundCrewException::new);
Schedule schedule = scheduleRepository.findById(reservationReserveRequest.getScheduleId())
.orElseThrow(NotFoundScheduleException::new);
schedule.reserve();
Reservation reservation = reservationRepository.save(new Reservation(schedule, crew));
// 예약 시트 테이블관련 작업
sheetService.save(reservation.getId());
// 알람을 보내는 작업
AlarmInfoDto dto = AlarmInfoDto.of(schedule.getCoach(), crew, schedule.getLocalDateTime());
alarmService.send(dto, AlarmTitle.APPLY);
return reservation.getId();
}
The booking process is as follows:
Operations related to the reservation table.
Operations related to the reservation sheet table.
Sending an alert using an external Slack library.
3-1. A request is sent to an external alert server to send an alert.
3-2. The external alert server sends the alert using the Slack library.
After discussing with the team, we concluded that the issue arises because there are too many transaction units, with step 3 being particularly resource-intensive.
We solved this problem with Asynchronous.
@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class AlarmService {
@Value("${slack.bot.secret-key}")
private String botSecretKey;
private final WebClient botClient;
private final ReservationRepository reservationRepository;
public void requestAlarm(SlackAlarmDto alarmDto) {
try {
botClient.post()
.uri("/api/send")
.header("Authorization", botSecretKey)
.bodyValue(alarmDto)
.retrieve()
.bodyToMono(SlackAlarmDto.class)
.block()
} catch (WebClientException e) {
log.error("슬랙 알람 전송 중 예외가 발생했습니다. {} {}", e.getMessage(), e);
}
}
}
The code above is responsible for sending alerts (corresponding to step 3-1 in the previous description) and should be handled asynchronously.
Let's explore the concept of synchronous vs. asynchronous to find the solution to this problem.
In the problematic logic described above, steps 1, 2, and 3 were all processed synchronously, leading to significant delays.
After understanding asynchrony, it was concluded that not all parts of the logic need to be processed synchronously. It is not necessary to wait for the alert to be sent before notifying the user that the reservation is complete.
The reservation is complete regardless of whether the alert is sent or not. It is inappropriate for the critical reservation logic to be delayed due to the heavy external alert process. The suitable approach is to let the thread handle other tasks while the alert-sending process is managed by a separate thread.
So, how is asynchrony applied in Spring?
The annotation provided by Spring for asynchronous processing.
Methods annotated with this annotation are executed in a separate thread.
To use this annotation, a configuration class annotated with @EnableAsync must be created first.
The method with this annotation must be public.
Methods called within the same class cannot be annotated.
→ This is because Spring annotations (AOP) work with proxies.
Spring uses SimpleAsyncTaskExecutor
to execute methods asynchronously. SimpleAsyncTaskExecutor creates a new thread for each request, but continuously creating threads as requests come in is inefficient.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(5);
executor.setThreadNamePrefix("asyncThread");
executor.initialize();
return executor;
}
}
To address this, we implemented the AsyncConfigurer
interface and overrode the getAsyncExecutor()
method. By doing so, the default executor will be set to the executor defined in getAsyncExecutor().
CorePoolSize: The default number of threads that are kept waiting for tasks.
MaxPoolSize: The maximum number of threads that can run simultaneously.
ThreadNamePrefix: The prefix for the names of the threads in the pool.
@Async
public void requestAlarm(SlackAlarmDto alarmDto) {
try {
botClient.post()
.uri("/api/send")
.header("Authorization", botSecretKey)
.bodyValue(alarmDto)
.retrieve()
.bodyToMono(SlackAlarmDto.class)
.block()
} catch (WebClientException e) {
log.error("슬랙 알람 전송 중 예외가 발생했습니다. {} {}", e.getMessage(), e);
}
}
By adding the @Async
annotation to the methods that need to operate asynchronously.
etc
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("customAsyncExecutor")
public Executor customAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(5);
executor.setThreadNamePrefix("asyncThread");
executor.initialize();
return executor;
}
}
You can register an Executor as a bean and use it for asynchronous processing as follows. If you have multiple asynchronous tasks with different configuration settings, creating and using multiple beans is appropriate.
When using a custom bean for @Async, you need to specify the bean name in the @Async annotation like this:
@Async("customAsyncExecutor")
Each time the test was conducted, there were variations, but it was confirmed through Postman that the asynchronously processed logic was approximately three times faster.
before
after
During the process of encountering and solving the problem, I had the opportunity to learn about a new concept—asynchronous processing. Even though it felt overwhelming at the time, the step-by-step analysis and resolution of the issue turned into an enjoyable learning experience.