
์ด ๊ธ์์๋ Spring ์๋ฒ์์ ์ผ๋ถ ๋ก์ง์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๋ค๋ฃน๋๋ค.
๊ตฌํ ๋ฐฉ๋ฒ์๐งญ ํ๊ฒฝ ์ค์ ๋ถํฐ ๋์์์ต๋๋ค.
์ผ๋จ ๋๋ ๋๊ธฐ ์ฒ๋ฆฌ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์๊ฐํ๋ฉด์ ๊ตฌํํด๋ณธ ๊ฒฝํ์ด ์์๋ค. ๋ฌผ๋ก ๊ทธ๊ฒ ๋ญ์ง ๋ฐฐ์ฐ๊ธด ํ์ง๋ง ํ๋ถ ๋์๋ ์ผ๋ฐ์ ์ผ๋ก ๋๊ธฐ ์ฒ๋ฆฌ๋ง ์ฌ์ฉํ๊ธฐ์, ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ ํน์ํ ์ํฉ์ ์ง์ ์ ์ผ๋ก ๊ฒช์ด๋ณธ ์ ์ด ์์๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋ฐ๋ฐ ์ง์~ ๊ทธ ํน์ํ ์ํฉ์ด ์๊ฒผ๋ค. ๊ทธ๋์ ์ผ๋จ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ํ ๊ฐ๋
์ ์์๋ณด๊ฒ ๋์๋ค. API ์ธก๋ฉด์์ ๋ด๊ฐ ์ดํดํ ๋๊ธฐ, ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ๋ค.
๋๊ธฐ ์ฒ๋ฆฌ(Synchronous)๋ ์์ฐจ์ ์ผ๋ก ์ผ์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด๋ค. A ์์ฒญ ํ์ B ์์ฒญ์ด ๋ค์ด์์ ๋, A ์์ฒญ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋๋ฉด ์๋ต์ ๋ณด๋ด๊ณ B ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์์ด๋ค. ์ผ๋ฐ์ ์ผ๋ก ์ฐ๋ฆฌ๊ฐ ์ต์ํ ๋ฐฉ์์ด๋ค.
๋น๋๊ธฐ ์ฒ๋ฆฌ(Asynchronous)๋ ์๋ต๊ณผ ๋ฌด๊ดํ๊ฒ ๋ค์ ์ผ์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด๋ค. ์์ ๊ฐ์ ์์๋ก ๋ค์ด๋ณด์๋ฉด, A ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋์์ B ์์ฒญ์ ์ฒ๋ฆฌํ๋ค. ๊ฐ ์์ฒญ์ ์๋ต์ ๊ฐ ์์ฒญ์ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋ ์์ ์ ๋ฐ์ก๋๋ค. ์ด๋ ํ๋์ ์์ ์ด ๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ค์ ์์ ์ ์ํํ๋ค๋ ์ ์ด ๋๊ธฐ ์ฒ๋ฆฌ์ ๋ช ํํ ์ฐจ์ด์ ์ ๋ณด์ธ๋ค.
์งํ ์ค์ธ ํ๋ก์ ํธ์์ ํ์ผ์ ์
๋ก๋ํ๊ณ , ํด๋น ํ์ผ์ ๋ํ ์ธํผ๋ฐ์ค ์งํ ํ ๊ฒฐ๊ณผ๊ฐ์ ์ ์ฅํ๋ API๊ฐ ์๋ค. ํธ์์ upload api๋ผ ๋ถ๋ฅด๊ฒ ๋ค. upload api์ ์ ์ฒด ๋ก์ง์ ์คํํ๊ณ ์๋ต์ ์คฌ์ ๋ ์๋ต ์๊ฐ์ด ๋๋ฌด ์ค๋ ๊ฑธ๋ ธ๋ค. ํนํ ์ธํผ๋ฐ์ค ์๊ฐ์ด ๊ธธ์๋ค. 2~3๊ฐ์ ํ์ผ์ ์
๋ก๋ํ์ ๋์๋ ๊ด์ฐฎ์์ง๋ง, 500~1000๊ฐ์ ํ์ผ์ ์ฌ๋ ธ์ ๊ฒฝ์ฐ ์์คํ
์ด ๋ถํ๋ฅผ ๊ฒฌ๋์ง ๋ชปํ๊ณ ํฐ์ง๋ ํ์์ด ๋ฐ์ํ๋ค. ๋ํ ๋๋์ ํ์ผ์ ์
๋ก๋ํ ๊ฒฝ์ฐ ์๋ต ์๊ฐ์ด ๊ณผ๋ํ๊ฒ ์ค๋ ๊ฑธ๋ ธ๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ๋ ๋ถ๋ถ๊น์ง๋ ๋๊ธฐ๋ก, ์ดํ์ ์ธํผ๋ฐ์ค ๊ณผ์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ๋๋ก ์ค๊ณํ๋ค. ์๋์ ๋ก์ง์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ํ๋์ API์์ ๋๊ธฐ ๋ก์ง๊ณผ ๋น๋๊ธฐ ๋ก์ง์ ๊ตฌ๋ถํด ๊ตฌํํ๋ค.

Spring์์๋ @Async ์ด๋
ธํ
์ด์
์ผ๋ก ๋น๋๊ธฐ ๋ฉ์๋๋ฅผ ์ค์ ํ๋ค. ํด๋น ์ด๋
ธํ
์ด์
์ Spring AOP์ ์ํด ํ๋ก์ ๋ฐฉ์์ผ๋ก ์๋๋๋ค.

Spring Context์ ๋ฑ๋ก๋ Async Bean์ด ํธ์ถ๋๋ฉด Spring์ด ๊ฐ์
ํด ํด๋น Async Bean์ ํ๋ก์ ๊ฐ์ฒด๋ก Wrappingํ๋ค. ์ปจํ
์ด๋์ ์ํด Bean์ผ๋ก ๋ฑ๋ก๋๋ ์์ ์ ํ๋ก์ ๊ฐ์ฒดํํ๋ ๊ฒ์ด๋ค.
ํธ์ถํ ๊ฐ์ฒด๋ ์ค์ง์ ์ผ๋ก AOP๋ฅผ ํตํด ๋ง๋ค์ด์ง ํ๋ก์ ๊ฐ์ฒดํ๋ Async Bean์ ์ฐธ์กฐํ๊ฒ ๋๋ค.
@Async๋ฅผ ์ฌ์ฉํ ๋์๋ ์๋์ ์ ์์ฌํญ์ ์ง์ผ ์ฌ์ฉํด์ผ ํ๋ค.
1. ๋น๋๊ธฐ ๋ฉ์๋์ ์ ๊ทผ ์ง์ ์
private์ฌ์ฉ ๋ถ๊ฐ
2. self-invocation(์๊ฐ ํธ์ถ) ๋ถ๊ฐ, ์ฆ inner method ์ฌ์ฉ ๋ถ๊ฐ
๋น๋๊ธฐ ๋ฉ์๋์ ์ ๊ทผ ์ง์ ์๋ฅผ private์ผ๋ก ์ง์ ํ๋ฉด AOP๊ฐ ๊ฐ๋ก์ฑ ํ๋ก์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค ๋ ์ด์ ์ ๊ทผํ ๋ ์ด์ ์ ๊ทผํ ์ ์์ผ๋ฏ๋ก private method๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
self-invocation์ ๊ฒฝ์ฐ, ํ๋ก์ ๊ฐ์ฒด๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ง์ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ธฐ์ Async๊ฐ ๋์ํ์ง ์๋๋ค. ๊ทธ๋ ๊ธฐ์ ๊ผญ ์ ์์ฌํญ์ ์ง์ผ์ ์ฌ์ฉํด์ผ ํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก spring-boot-starter-web์ ๋ด์ฅ๋์ด ์์ง๋ง, ๊ทธ๋๋ ์์กด์ฑ ์ค์ ์ด ์ ๋์ด์๋์ง ํ ๋ฒ ๋ ํ์ธํ๋ค.
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
...
๋น๋๊ธฐ ์ค์ ์ ์ํด AsyncConfig ํด๋์ค๋ฅผ ๋ง๋ค์ด ๊ด๋ฆฌํด์ค๋ค. ์ด๋ @EnableAsync ์ด๋
ธํ
์ด์
์ ๋ฌ์ ๋น๋๊ธฐ ๋ฉ์๋ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋๋ก ์๋์ ๊ฐ์ด ์ค์ ํ๋ค.
์ด ๊ฒฝ์ฐ์๋ ์๋ฒ๊ฐ ํฐ์ง์ง ์๋๋ก ๋๋ ค๋ ์์ ์ ์ผ๋ก ์ด์ํ๊ธฐ ์ํด CORE_POOL_SIZE์ MAX_POOL_SIZE๋ฅผ 1๋ก ์ค์ ํ๋ค. ๊ฐ ์์์ ์๋ฏธ๋ ๋ค์๊ณผ ๊ฐ๋ค.
CORE_POOL_SIZE: ์ค๋ ๋ ํ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ง๋๋ ์ค๋ ๋ ์ ์ ์
โ ์์๋๋ ์ต๋ ๋์ ์์ ์์ ๊ฐ๊น์ด ๊ฐ์ผ๋ก ์ค์ ํ๋ ๊ฒ์ ๊ถ์ฅ
MAX_POOL_SIZE: ์ค๋ ๋ ํ์ด ํ์ฅํ ์ ์๋ ์ต๋ ์ค๋ ๋ ์ ์ ์
โ ์๋์ queue๊ฐ ๊ฐ๋ ์ฐผ์ ๋ ์๋ก์ด ์์ฒญ์ด ๋ค์ด์จ ๊ฒฝ์ฐ, ํ์ฅํ ์ค๋ ๋ ํ ์
QUEUE_CAPACITY: ์ค๋ ๋ ํ์์ ์ฌ์ฉํ ์ต๋ ํ์ ํฌ๊ธฐ
package example.global.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ค์
*/
@EnableAsync
@Configuration
public class AsyncConfig {
private static final int CORE_POOL_SIZE = 1;
private static final int MAX_POOL_SIZE = 1;
private static final int QUEUE_CAPACITY = 1000;
/**
* ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํ Executor ์ค์ , Core Pool Size, Max Pool Size, Queue Capacity ์ค์
*
* @return ThreadPoolTaskExecutor
*/
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setThreadNamePrefix("EXAMPLE-ASYNC-");
executor.initialize();
return executor;
}
}
์ด ๊ฒฝ์ฐ์๋ ํฅํ ์ ์ง๋ณด์์ฑ๊ณผ ์ฝ๋์ ์ ์ฐ์ฑ, ๊ฐ๋
์ฑ์ ์ํด Async ๋ฉ์๋๋ฅผ ๋ฐ๋ก ๊ด๋ฆฌํ๋ AsyncService ํด๋์ค๋ฅผ ๋ณ๋๋ก ๊ตฌํํด ๊ด๋ฆฌํ๋ค. ๋น๋๊ธฐ ๋ฉ์๋์ @Async ์ด๋
ธํ
์ด์
์ ๋ฌ์์ฃผ๋ฉด ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค.
๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ๋๋ ๋ฉ์๋์ ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋ ํ์ ๋ฉ์๋๊ฐ ์๋ ๊ฒฝ์ฐ์๋ ๋ค๋ฅธ ํด๋์ค์ @Async ์ด๋
ธํ
์ด์
์ ๋ฌ์ ๊ตฌํํด์ผํ๋ค. ๋์ผํ ํด๋์ค์ ๊ตฌํ ์ self-invocation์ ์ฐ๋ ค๊ฐ ์๋ค.
์๋์ ํด๋์ค๋ ์ธํผ๋ฐ์ค๋ฅผ ์งํํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ๋ ๋น๋๊ธฐ ๋ฉ์๋๋ฅผ ๊ตฌํํ ๊ฒ์ด๋ค.
/**
* Async service - to process async task
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AsyncService {
private final StudyDao studyDao;
private final PatientDao patientDao;
private final InvalidInstanceDao invalidInstanceDao;
private final InstanceDao instanceDao;
private final SecondaryCaptureDao secondaryCaptureDao;
private final ReportDao reportDao;
private final InferenceManager inferenceManager;
/**
* Async process(1. save instance information in database,
* 2. do inference and save result in database
* 3. get secondary capture list
* 4. get report
* 5. save report in database)
*
* @param originPath origin dicom file path
* @param accountEntity account entity
*/
@Async
protected void asyncProcess(String originPath, DicomTagValue dicomTagValue, AccountEntity accountEntity, Boolean isInvalid, String errorMessage) {
String pngPath = FileManager.getInstanceThumbnailPath(originPath);
// save instance information in database
InstanceEntity instanceEntity = saveInstanceInfo(originPath, dicomTagValue, accountEntity, pngPath);
log.info("instanceEntity id = {}", instanceEntity.getInstanceId());
if (isInvalid) {
invalidInstanceDao.saveInvalidInstance(instanceEntity, errorMessage);
inferenceManager.getThumbnail(instanceEntity.getOriginDcmPath(),
instanceEntity.getOriginPngPath());
return ;
}
try {
// do inference and save result in database
InferenceResponseDto inferenceResponseDto = inferenceManager.getInferenceResult(instanceEntity, pngPath);
// save inference result in database
secondaryCaptureDao.saveWithSecondaryCaptureInfo(inferenceResponseDto, instanceEntity);
// get secondary capture list and get report
StudyEntity studyEntity = instanceEntity.getStudy();
// get report
ReportResponseDto reportResponseDto = inferenceManager.getReportFromStudyEntity(studyEntity);
if (reportResponseDto == null) {
log.error("Failed to get report");
return ;
}
// save report in database
reportDao.saveReportWithReportResponseDto(reportResponseDto, studyEntity);
} catch (InferenceFailureException e) {
invalidInstanceDao.saveInvalidInstance(instanceEntity, e.getMessage());
} catch (Exception e) {
log.error("Failed to get inference result", e);
}
}
// private method๋ค์ ์๋ต
}
์ดํ ๋น๋๊ธฐ ๋ฉ์๋๋ฅผ service ๋ก์ง์์ ํธ์ถํ๋ฉด ์๋์ผ๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๋ด ๊ฒฝ์ฐ์๋ ์ธํผ๋ฐ์ค๋ฅผ ์งํํ๊ธฐ ์ง์ ๊น์ง์ ๋ก์ง์ ๊ตฌํํ InstanceService.java ํด๋์ค์์ ํธ์ถํด์คฌ๋ค.
/**
* Instance service
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class InstanceService {
private final AsyncService asyncService;
private final DicomValidator dicomValidator;
private final AccountDao accountDao;
/**
* Upload dicom image
*
* @param file image file to upload
* @param principal user data(email)
*/
public void uploadInstance(MultipartFile file, Principal principal) {
// ๋๊ธฐ ๋ก์ง ์ฒ๋ฆฌ ์๋ต
// ๋น๋๊ธฐ ๋ก์ง ํธ์ถ
asyncService.asyncProcess(originPath, dicomTagValue, accountEntity, isInvalid, errorMessage);
}
}
์์ ์ธ๊ธํ์ง๋ง, ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ ์ค๋ ๋๊ฐ ๊ต์ฅํ ์ค์ํ๋ค. ๊ทธ ์ค์์๋ ๊ฐ์ฅ ์ฃผ์ํด์ผ ํ ์ค๋ ๋ ๊ด๋ จ ์ด์๋ ๋ค์๊ณผ ๊ฐ๋ค.
๋น๋๊ธฐ ์ฒ๋ฆฌ๋ ์ฃผ๋ก ๋ณ๋์ ์ค๋ ๋๋ฅผ ์์ฑํ์ฌ ์์
์ ์ฒ๋ฆฌํ๋ค. Spring์์๋ @Async ์ฌ์ฉ ์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ค๋ ๋ ํ(Thread pool)์ ์ฌ์ฉํ๋๋ฐ, ์ด๋ฅผ ์ ๋๋ก ๊ด๋ฆฌํ์ง ์์ผ๋ฉด ์๋์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ฐ๋ ค๊ฐ ์๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ค์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ค๋ ๋ ํ์ ํฌ๊ธฐ๋ฅผ ์ ์ ํ๊ฒ ์ค์ ํด์ผ ํ๋ค. ์ด๋ CPU ์ฝ์ด ์์ ์์คํ ์ ์ฑ๋ฅ, ๋ถํ ๋ถ์ฐ ๋ฑ์ ๊ณ ๋ คํด ๊ฒฐ์ ํด์ผ ํ๋ค. ๋ํ ์์ ํ์ ํฌ๊ธฐ์ ์ค๋ ๋ ํ์ ๋ฆฌ์ ์ ์ ์ฑ ์ ์ ์ ํ ์ค์ ํ๋ ๊ฒ์ ํตํด ๊ณผ๋ํ ์ค๋ ๋ ์์ฑ์ ๋ง์ ์ ์๋ค.
๋น๋๊ธฐ ์ฒ๋ฆฌ์์๋ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์์ ์ ์ํํ๋ฏ๋ก, ๊ณต์ ์์์ ์ ๊ทผํ ๋ ์ค๋ ๋ ์์ ์ฑ์ ํ๋ณดํด์ผ ํ๋ค. DB์ concurrency control๊ณผ๋ ๋น์ทํ ๋ฌธ์ ์ด๋ค. ๋๊ธฐํ๋์ง ์์ ๊ณต์ ์์์ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์ ๊ทผ ์ ๋ฐ์ดํฐ ์์์ด๋ ์์ํ์ง ๋ชปํ ๋์์ด ๋ฐ์ํ ์ฐ๋ ค๊ฐ ์๋ค.
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ณต์ ์์์ ์ ๊ทผํ ๋์๋ synchronized ๋ธ๋ก, ReentrantLock ๋ฑ์ผ๋ก ๋๊ธฐํ ์ฒ๋ฆฌ๋ฅผ ํ๋ค. ๊ฐ๋ฅํ๋ค๋ฉด ๋ถ๋ณ ๊ฐ์ฒด(Immutable object)๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋ ๋ ์์ ์ฑ์ ๋ณด์ฅํ ์ ์๋ค. ๋ํ ConcurrentHashMap, CopyOnWriteArrayList ๋ฑ๊ณผ ๊ฐ์ด ์ค๋ ๋์ ์์ ํ ์๋ฃ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๊ถ์ฅ๋๋ค.
์์์ ์ธ๊ธํ ๋ฌธ์ ๋ค ์ธ์๋ ๋น๋๊ธฐ ์ฒ๋ฆฌ ์์๋ ๋ฆฌ์์ค ๊ด๋ฆฌ, ์์กด์ฑ ๊ด๋ฆฌ ๋ฑ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ฐ๋ ค๊ฐ ์๋ค. ์ด์ฒ๋ผ ๋ง์ ์ํ์ ์๊ณ ์๋ ๋ฐฉ์์ ์ฌ์ฉํ ๋์๋ ์ด์ ๋ํ ํ
์คํธ๊ฐ ์ ํ๋์ด์ผ ํ๋ค.
ํนํ ์ค๋ ๋์ ๊ฒฝ์ฐ, ์ด๋ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ง ์ด๋ ์ ๋ ์์ธกํ ์ ์๊ธฐ ๋๋ฌธ์ ์ด์ ๋ํ ๋ณด์์ฑ
์ ์๋ฆฝํ๊ณ ์ ์ฉํ ํ ํ
์คํธ๋ฅผ ๊ฑฐ์ณ ๋ด ํ๊ฒฝ์ ๋ง๋ ์ค์ ์ ์ฐพ๋ ๊ฒ์ด ์ค์ํ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
๋ฆฌ์์ค์ ๊ฒฝ์ฐ, ํ
์คํธ ์ ์ ์ ํ ์ค์ ์ ์ฐพ์ง ๋ชปํ๋ค๋ฉด ํน์ ์ํ๋ ์ฑ๋ฅ์ด ๋์ค์ง ์๋๋ฐ ๋น๋๊ธฐ ์์ฒญ์ ์จ์ผ ํ๋ค๋ฉด ๋ถํ ๋ถ์ฐ์ ์ํด ์๋ฒ ์ฆ์ค ๋ฑ์ ๋์ฑ
๋ ๊ณ ๋ คํด๋ณผ ์ ์๋ค.