팀원이 작성한 코드에서 성능 튜닝이 필요한 코드를 찾았다.
override fun patchAdmin(routineId: Int, newAdminId: Int, authentication: Authentication): ResponseEntity<CoBoResponseDto<CoBoResponseStatus>> {
val user = userRepository.findById(authentication.name.toInt())
.orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
val newAdmin = userRepository.findById(newAdminId)
.orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
val routine = routineRepository.findById(routineId)
.orElseThrow{throw NoSuchElementException("일치하는 루틴이 없습니다.")}
if (routine.admin != user)
throw IllegalAccessException("수정 권한이 없습니다.")
if (!participationRepository.existsByUserAndRoutine(newAdmin, routine))
throw NoSuchElementException("참여 정보가 없는 사용자입니다.")
routine.admin = newAdmin
routineRepository.save(routine)
return CoBoResponse<CoBoResponseStatus>(CoBoResponseStatus.SUCCESS).getResponseEntity()
}
해당 코드이다.
무려 데이터베이스에 5번 접근하게 된다.
마지막에 save하는 부분은 데이터베이스에서 데이터를 가져온 후 호출해야 하기 때문에 마지막에 실행해야 하지만, 위의 4개의 접근은 의존성이 존재하지 않기 때문에 4개를 각각 다른 쓰레드에서 비동기적으로 실행이 가능하다.
if (!participationRepository.existsByUserAndRoutine(newAdmin, routine))
throw NoSuchElementException("참여 정보가 없는 사용자입니다.")
현재 이 코드는 의존성이 존재하지만, JPA가 아닌 QueryDsl을 사용하여 리펙토링해서 의존성을 없애도록 하였다.
participationRepository.existsByUserIdAndRoutineIdByQueryDsl(newAdminId, routineId)
이렇게 Entity가 아닌 Int 타입의 Id로 검색하도록 코드를 변경했다.
override fun existsByUserIdAndRoutineIdByQueryDsl(userId: Int, routineId: Int): Boolean {
return jpaQueryFactory
.select(participation)
.from(participation)
.leftJoin(participation.user)
.where(
participation.routine.id.eq(routineId),
participation.user.kakaoId.eq(userId)
).fetchFirst() != null
}
이 부분은 QueryDsl의 코드이다.
이렇게 저 코드에서도 비동기적으로 실행 할 수 있도록 의존성을 제거하였다.
이제 application 단계에서 코드들을 비동기적으로 처리하여 속도를 높여보자.
override fun patchAdmin(routineId: Int, newAdminId: Int, authentication: Authentication): ResponseEntity<CoBoResponseDto<CoBoResponseStatus>> {
val userCompletableFuture = CompletableFuture.supplyAsync{
userRepository.findById(authentication.name.toInt())
.orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
}
val newAdminFuture = CompletableFuture.supplyAsync {
userRepository.findById(newAdminId)
.orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
}
val routineFuture = CompletableFuture.supplyAsync{
routineRepository.findById(routineId)
.orElseThrow{throw NoSuchElementException("일치하는 루틴이 없습니다.")}
}
val isParticipationFuture = CompletableFuture.supplyAsync{
participationRepository.existsByUserIdAndRoutineIdByQueryDsl(newAdminId, routineId)
}
return CompletableFuture.allOf(userCompletableFuture, newAdminFuture, routineFuture, isParticipationFuture)
.thenApplyAsync {
val user = userCompletableFuture.get()
val newAdmin = newAdminFuture.get()
val routine = routineFuture.get()
val isParticipation = isParticipationFuture.get()
if (routine.admin != user)
throw IllegalAccessException("수정 권한이 없습니다.")
if (!isParticipation)
throw NoSuchElementException("참여 정보가 없는 사용자입니다.")
routine.admin = newAdmin
routineRepository.save(routine)
CoBoResponse<CoBoResponseStatus>(CoBoResponseStatus.SUCCESS).getResponseEntity()
}.get()
}
이렇게 코드를 변경하였다.
위의 4개를 CompletableFuture로 담아 비동기적으로 실행하였으며, 4개의 쓰레드가 모두 완료되었을 때를 allOf로 가져와 한 곳에서 모아 결과를 확인 한 후 다시 routineRepository에 저장하도록 하였다.
위의 4개의 접근은 한번에 실행하기 때문에 사실상 저 코드는 2개의 데이터베이스 접근의 시간과 비슷하게 튜닝이 되었을 것이다.
얼마나 속도가 좋아졌는지 궁금하여 로그로 시간을 찍어보았다.
아래 사진은 기존 코드의 실행시간이다.
아래 사진은 튜닝한 코드의 실행 시간이다.
데이터베이스의 접근 4개를 동시에 처리해서 그런지, 2배 이상의 성능 향상이 일어난 것을 볼 수 있다.
최근에 리액티브 프로그래밍에 재미를 느껴 따라해보았는데, 비동기가 왜 필요한지를 다시 한 번 느낄 수 있었던 것 같다.
'크무톡톡 프로젝트' 카테고리의 다른 글
네이버 Oauth 로그인, SpringBoot (2) | 2024.07.23 |
---|---|
Nginx로 Swagger Proxy_pass (1) | 2024.07.22 |
Springboot와 DialogFlow 연동 - API (0) | 2024.01.17 |
SMTP 서버 구축 (0) | 2024.01.04 |
EC2에 Java 17 설치 (1) | 2023.12.29 |