์ด์ Velog์ ์ธ๋ถ ์๋น์ค ์์ฒญ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ฆฌ๋ทฐ ์ ๋ก๋ ์์ฒญ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง๋ ๊ฐ๊ฒฐํฉ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ํด๋ณด์. (2023.11.02) ๋ก๋ถํฐ ๋ง์ด๊ทธ๋ ์ด์ ๋ ๊ธ์ ๋๋ค.
๋๊ธฐ
๋ฆฌ๋ทฐ๋ ๋ณธ ํ๋ก์ ํธ์ ํต์ฌ์ผ๋ก์, ์ต๋ํ ๋ง์ ๋ฆฌ๋ทฐ๋ฅผ ์ ๋ ฅ๋ฐ๊ณ ์ด๋ฅผ ๋ถ์ํด ๊ฐ์น์๋ ์ ๋ณด๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ์ด๋ฌํ ๋ฆฌ๋ทฐ์ ์ ๋ก๋๋ ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ ํตํด ์งํ๋ฉ๋๋ค.
AWS S3๋ฅผ ํตํ ์ฌ์ง ์ ๋ก๋์ AI ๋ฆฌ๋ทฐ ๋ถ์ ๋ชจ๋ธ์ ํตํ ๋ฆฌ๋ทฐ ๋ถ์ ์์ฒญ์ด๋ผ๋ 2๊ฐ์ ์ธ๋ถ ์๋น์ค๋ฅผ ๊ฑฐ์น ํ ในใ ฃ๋ทฐ ๋ฐ์ดํฐ๊ฐ DB์ ์ ์ฅ๋ฉ๋๋ค. ์ฆ, ๋๊ธฐ๋ก ์๋ํ๊ณ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ด๊ธฐ ๋๋ฌธ์, ํด๋ผ์ด์ธํธ๊ฐ ๋ฆฌ๋ทฐ๋ฅผ ์ ๋ก๋ํ ๋ ์ธ ๊ฐ์ง ๊ณผ์ ์ด ๋ชจ๋ ์๋ฃ๋๋ ๊ธด ์๊ฐ์ ๊ธฐ๋ค๋ ค์ผ ํ๊ณ ์ธ๋ถ ์๋น์ค ์ค ํ๋๋ผ๋ ์คํจํ๋ฉด ๋ฆฌ๋ทฐ ์ ๋ก๋๊ฐ ์คํจํ๋ ๊ฐ๊ฒฐํฉ ๊ตฌ์กฐ์ด๋ค.
๊ฐ์
๋ฐ๋ผ์ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํด ์์ ์ฑ์ ํ๋ณดํ๊ณ , ํ์ํ ์์ ์ด ์๋ฃ๋๋ฉด ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๊ณ ๋น๋๊ธฐ๋ก ๋๋จธ์ง ์์ ์ ์งํํ๋๋ก ๋ณ๊ฒฝํด์ผ ํ๋ค.
ThreadPoolTaskExecutor
ThreadPoolTaskExecutor ์ค์
@EnableAsync
@Configuration
public class AsyncConfig {
private static final int EXECUTOR_CORE_POOL_SIZE = 50;
private static final int EXECUTOR_QUEUE_CAPACITY = 5000;
private static final int EXECUTOR_MAX_POOL_SIZE = Integer.MAX_VALUE;
private static final String EXECUTOR_THREAD_NAME_PREFIX = "async-task-executor-";
@Bean
public Executor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(EXECUTOR_CORE_POOL_SIZE);
executor.setQueueCapacity(EXECUTOR_QUEUE_CAPACITY);
executor.setMaxPoolSize(EXECUTOR_MAX_POOL_SIZE);
executor.setThreadNamePrefix(EXECUTOR_THREAD_NAME_PREFIX);
executor.initialize();
return executor;
}
}
- @EnableAsync : ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ธฐ๋ฅ์ ํ์ฑํํ๋ค
- CorePoolSize : ๋์์ ์คํ์ํฌ ์ฐ๋ ๋์ ๊ฐ์ (default: 1)
- MaxPoolSize : ์ฐ๋ ๋ ํ์ ์ต๋ ์ฌ์ด๋ (default: Integer.MAX_VALUE)
- QueueCapacity : ํ์ ์ฌ์ด์ฆ (default: Integer.MAX_VALUE)
ThreadPoolTaskExecutor์ ๊ธฐ๋ณธ์ ์ธ ๋์์๋ฆฌ
- CorePoolSize๋งํผ ์ด๋ฏธ ์ฐ๋ ๋๊ฐ ์ ์ ๋๊ณ ์์ผ๋ฉด, ๋์น๋ ์ฐ๋ ๋ ์์ฒญ์ ํ์ ๋ฃ๋๋ค.
- ์ฐ๋ ๋๊ฐ ๋ชจ๋ ์ ์ ๋์ด ์๊ณ QueueCapacity๋งํผ ์ฐ๋ ๋๊ฐ ์ฐผ์ผ๋ฉด, ๋์น๋ ์ฐ๋ ๋ ์์ฒญ์ MaxPoolSize๋งํผ ์ฐ๋ ๋ ํ์ ์ ์ฅํ๋ค.
- ์ฐ๋ ๋ ํ๊น์ง ๊ฐ๋์ฐฌ๋ค๋ฉด, java.util.concurrent.RejectedExecutionException ์์ธ๊ฐ ๋ฐ์๋๋ค.
์ ์ฉ
๋น๋๊ธฐ ์ฒ๋ฆฌํ ์ฝ๋๋ฅผ Runnable๋ก ์ ์ธํ ํ Executor.execute(runnable)๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์์ด๊ฒ ์ง๋ง, ์คํ๋ง์ ํน์ง์ธ ์ด๋ ธํ ์ด์ ์ ํ์ฉํ๋ค๋ฉด ๊ต์ฅํ ์ฝ๊ณ ์ฝ๋๊ฐ ๊น๋ํด์ง๋๋ค. ๋จ์ง, ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํ ํจ์์ @Async๋ฅผ ๋ถ์ด๊ณ ์คํํ๋ฉด ๋๋ค. ๋ง์ฝ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ ๋ฉ์๋์ ๋ฐํ๊ฐ์ ๋ฐ๊ณ ์ถ์ผ๋ฉด, Future, CompleteFuture, ListenableFuture์ ์ฌ์ฉํ๊ณ ๋ฐํ๊ฐ ์์ด ์คํ๋ง ํ๊ณ ์ถ๋ค๋ฉด void๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
public class ReviewService {
~~
@Transactional
public Long create(String partnerDomain, String reservationPartnerCustomId, ReviewCreateRequest reviewCreateRequest, List<MultipartFile> reviewImageFiles) {
// Get reservation
final Reservation reservation = reservationService.findByPartnerDomainAndPartnerCustomId(partnerDomain, reservationPartnerCustomId);
validateAlreadyReviewedByReservationId(reservation.getId());
// Create review
Review review = reviewCreateRequest.toEntity(reservation);
reviewRepository.save(review);
reservation.getTravelProduct().addReviewInfo(review.getRating());
// Create review images
if (reviewImageFiles != null) {
reviewImageService.createAll(reviewImageFiles, review);
}
// Create review tag
reviewTagService.createAll(review);
// Impl Requesting review inference through kafka
return review.getId();
}
public class ReviewImageService {
~~
@Async
public void createAll(List<MultipartFile> reviewImageFiles, Review review) {
for (MultipartFile reviewImageFile : reviewImageFiles) {
ReviewImage reviewImage = ReviewImage.builder()
.fileName(uploadReviewImageFilesOnS3(reviewImageFile))
.review(review)
.build();
reviewImage = reviewImageRepository.save(reviewImage);
review.addReviewImage(reviewImage);
}
}
}
public class ReviewTagService {
~~
@Async
public void createAll(Review review) {
// Ask inference review tag
ReviewTagInferenceRequest reviewTagInferenceRequest = new ReviewTagInferenceRequest(review.getContent());
ReviewTagInferenceResponse reviewTagInferenceResponse = reviewTagInferenceClient.inferenceReview(reviewTagInferenceRequest);
// Create review tags
List<ReviewTagCreateRequest> reviewTagCreateRequests = reviewTagInferenceResponse.getBody().getResults().stream().map(ReviewTagCreateRequest::new).toList();
for (ReviewTagCreateRequest reviewTagCreateRequest : reviewTagCreateRequests) {
ReviewTag reviewTag = reviewTagCreateRequest.toEntity(review);
reviewTag = reviewTagRepository.save(reviewTag);
review.addReviewTag(reviewTag);
}
}
}
ThreadSafe
๋น๋๊ธฐ์ ๋ฉํฐ์ฐ๋ ๋๋ฅผ ์๋ ์ฌ๋์ ๋ณธ ์ฝ๋๊ฐ Thread Safeํ์ง ์์ ์ฝ๋์ฒ๋ผ ๋ณด์ธ๋ค.
AWS S3์ ์ ๊ทผํ๊ธฐ ์ํ AWSS3Client์ ์ธ์คํด์ค ๋ณ์๊ฐ ์ฌ๋ฌ ์ฐ๋ ๋๋ก๋ถํฐ ๊ณต์ ๊ฐ ๋๊ณ ์์ง๋ง, ์ด๋ฏธ ThreadSafe์ฒ๋ฆฌ๋์ด ์ด๋ ธํ ์ด์ ์ผ๋ก ๋ฌธ์ํ๋์ด ์๋ค.
AI ๋ฆฌ๋ทฐ๋ถ์ ๋ชจ๋ธ ์๋ฒ์ API ์์ฒญํ๊ธฐ ์ํด ์ฌ์ฉํ๋ OpenFeign ๋ํ ์ปค์คํ Decoder, Encoder, Logger, Retryer, ParamEncoder, ErrorDecoder๋ฅผ ์ฌ์ฉํ์ง ์๋ ์ด์ Thread Safeํ๋ค.
@Retryable
ํ์ง๋ง ๋ฌธ์ ๋ ์ฌ์ง ์ ๋ก๋์ ๋ฆฌ๋ทฐ ๋ถ์์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ๋ฏ๋ก ์ฑ๊ณต์ ๋ณด์ฅํ ์ ์๋ค. ๋ง์ฝ ์ ๊น์ ๋คํธ์ํฌ ์ค๋ฅ๋ก ์ธ๋ถ ์๋น์ค ์์ ์ด ์คํจํ๋ค๋ฉด ์ ๋ณด์ ์์ค์ด ํด ๊ฒ์ด๋ค. ์ด๋ฅผ ์ํ ์ต์ํ์ ์์ ์ฅ์น๋ฅผ ์กฐ์ฌํด๋ณด๋ ๋์ค OpenFeign๊ณผ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ํจ๊ป ์ฐ์ด๋ ์๋ฆฌ์ฆ ์ค ํ๋๋ก Retryer๊ฐ ์์๋ค. ํน์ ์ํฉ์์ ํจ์๋ฅผ ์ฌ์๋ํ๋๋ก ์ค์ ํ๋ ์ด๋ ธํ ์ด์ ์ด๋ค.
Spring Retry์ AOP๊ด๋ จ๋ ์์กด์ฑ์ ์ถ๊ฐํ๊ณ , ๋น๋๊ธฐ ์์ ์ ์ํด ์ถ๊ฐํ ๊ฒ์ด๋๊น AsyncConfig์@EnableRetry๋ฅผ ์ถ๊ฐํ๋ค.
implementation 'org.springframework.retry:spring-retry:1.3.4'
implementation 'org.springframework:spring-aspects:5.3.30'
@EnableRetry
@EnableAsync
@Configuration
public class AsyncConfig {
์ ์ฉ
- @Retryable : ์ฌ์๋ํ ํจ์ ์ค์
- value : ์ฌ์๋ ํ์ผ์ Exception type
- maxAttempt : ์ต๋ ์ฌ์๋ ํ์ (default : 3)
- backoff : ๊ฐ ์ฌ์๋ ์ ๋๊ธฐํ๋ ๊ฒ์ ๋ํ ์ค์
- delay : ๋๊ธฐ ์๊ฐ, ms (defaut : 1000ms)
AWS ์์ธ๋ฅผ ์บ์นํ ExternalServiceException ์์ธ์ ๊ฒฝ์ฐ ์ฌ์๋
@Async
@Retryable(
value = {ExternalServiceException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 10000)
)
private String uploadReviewImageFilesOnS3(MultipartFile reviewImage) {
try {
String fileName = System.currentTimeMillis() + "_" + reviewImage.getOriginalFilename();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(reviewImage.getContentType());
metadata.setContentLength(reviewImage.getSize());
amazonS3Client.putObject(s3ImageBucketName, fileName, reviewImage.getInputStream(), metadata);
return fileName;
} catch (SdkClientException e) {
throw new ExternalServiceException(AWS_S3_CLIENT_ERROR);
} catch (IOException e) {
throw new DomainLogicException(REVIEW_IMAGE_FILE_IO_ERROR);
}
}
Feign ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ฌ์๋
public class ReviewTagService {
~~
@Async
@Retryable(
value = {FeignClientException.class, FeignServerException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 10000)
)
public void createAll(Review review) {
// Ask inference review tag
ReviewTagInferenceRequest reviewTagInferenceRequest = new ReviewTagInferenceRequest(review.getContent());
ReviewTagInferenceResponse reviewTagInferenceResponse = reviewTagInferenceClient.inferenceReview(reviewTagInferenceRequest);
// Create review tags
List<ReviewTagCreateRequest> reviewTagCreateRequests = reviewTagInferenceResponse.getBody().getResults().stream().map(ReviewTagCreateRequest::new).toList();
for (ReviewTagCreateRequest reviewTagCreateRequest : reviewTagCreateRequests) {
ReviewTag reviewTag = reviewTagCreateRequest.toEntity(review);
reviewTag = reviewTagRepository.save(reviewTag);
review.addReviewTag(reviewTag);
}
}
public interface ReviewTagInferenceClient {
@GetMapping
ReviewTagInferenceResponse inferenceReview(@RequestBody ReviewTagInferenceRequest reviewTagInferenceRequest) throws FeignException;
}
๋ง๋ฌด๋ฆฌ
์ด ๊ณผ์ ์ ํตํด ๋ฆฌ๋ทฐ ์ ๋ก๋ ๊ณผ์ ์ด ๋ชฉํ๋ก ํ๋ ๋น๋๊ธฐ ์ฝ๊ฒฐํฉ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ๋์๋ค. ์ธ๋ถ ์์คํ ์ ์ํฅ์ ๋ฐ์ง ์๋ ์์ ์ ์ธ ๊ตฌ์กฐ์ด๋ฉฐ, API ๋ฐํ ์๋๋ 8.5 sec -> 0.2 sec์ผ๋ก ๋จ์ถํ ์ ์์๋ค. ์ธ๋ถ ์์คํ ์ WAS ์๋ฒ์ ์ฐ๊ฒฐํ๋ ๊ณผ์ ์์ ๊ณ ๋ฏผํด์ผ ํ๋ ์ ์ ๊ฒช๊ณ ๊ฐ์ ํ๋ ๊ฒฝํ์ ํ ์ ์์๊ณ , ์ ๋ต์ธ์ง๋ ๋ชจ๋ฅด๊ฒ ์ง๋ง ๋๋ฆ๋๋ก์ ๋ฌธ์ ๋ถ์๊ณผ ํด๊ฒฐ์ ํตํ ๊ฐ์ ์ ํ์๋ค.
์ด๋ณด๋ค ๋ ์ข์ ๋ฐฉ์์ด ์๋์ง๊ฐ ๊ถ๊ธํ๊ณ , ๊ธฐ๋ฅ์ด ์ธ๋ถ ์์คํ ๊ฐ์ ํต์ ์ ํตํด ๊ตฌํํ๋ MSA๋ ์ด๋ป๊ฒ ๊ตด๋ฌ๊ฐ๋ ์ง๋ ๊ถ๊ธํด์ก๋ค.