class Fund(
val code: String,
val name: String,
var team: Team,
) {
fun changeTeam(newTeam: Team) {
this.team = newTeam
}
}
class Team(
val code: String,
val name: String
)
@Service
@Transactional
class FundUpdateService(
private val messengerService: SendMessengerService
) {
private val log = LoggerFactory.getLogger(this::class.java)
fun updateFund() {
log.info("펀드 변경 시작~")
val fund = getFundById()
fund.changeTeam(Team("A02", "IB2팀"))
messengerService.sendAlarm(fund)
log.info("펀드 변경 완료!")
}
private fun getFundById(): Fund {
return Fund("F01", "IB1팀-운용", Team("A01", "IB1팀"))
}
}
@Component
class SendMessengerService {
private val log = LoggerFactory.getLogger(this::class.java)
fun sendAlarm(target: Any) {
log.info("메신저 서버 호출 = {}", target)
for (second in 1..3) {
Thread.sleep(1000)
log.info("{}초 기다림...", second)
}
log.info("메신저 전송 완료 = {}", target)
}
}
class FundUpdateServiceTest {
@Test
@DisplayName("강결합 양상 테스트")
fun `tightly coupled`() {
val fundUpdateService = FundUpdateService(SendMessengerService())
fundUpdateService.updateFund()
}
}
@Component
class SendMessengerService {
private val log = LoggerFactory.getLogger(this::class.java)
fun sendAlarm(target: Any) {
log.info("메신저 서버 호출 = {}", target)
for (second in 1..3) {
Thread.sleep(1000)
log.info("{}초 기다림...", second)
throw RuntimeException("메신저 서버 오류 발생!!!!!!!!!!!!!")
}
log.info("메신저 전송 완료 = {}", target)
}
}
메신저 서비스에서 발생한 오류로 인해 펀드 정보도 변경할 수 없는 일이 발생합니다.
즉, 세부적인 사항 때문에 핵심적인 비즈니스가 이뤄지지 못하게 됩니다.
@Service
@Transactional
class FundUpdateServiceWithEvent(
private val eventPublisher: ApplicationEventPublisher
) {
private val log = LoggerFactory.getLogger(this::class.java)
fun updateFund() {
log.info("펀드 변경 시작~")
val fund = getFundById()
fund.changeTeam(Team("A02", "IB2팀"))
eventPublisher.publishEvent(UpdateEvent(fund))
log.info("펀드 변경 완료!")
}
private fun getFundById(): Fund {
return Fund("F01", "IB1팀-운용", Team("A01", "IB1팀"))
}
}
@Component
class UpdateEventListener(
private val sendMessengerService: SendMessengerService
) {
@TransactionalEventListener
fun listenEvent(updateEvent: UpdateEvent) {
sendMessengerService.sendAlarm(updateEvent.target)
}
}
@EventListener
가 아니라 @TransactionalEventListener
를 사용해아 합니다.@EventListener
는 트랜잭션 범위 내에서 동기적으로 실행되고, @TransactionalEventListener
는 커밋 전/후 등을 자유롭게 지정할 수 있습니다.@SpringBootTest
class FundUpdateServiceWithEventTest {
@Autowired
private lateinit var fundUpdateServiceWithEvent: FundUpdateServiceWithEvent
@Test
@DisplayName("이벤트 분리 테스트")
fun `event isolation test`() {
fundUpdateServiceWithEvent.updateFund()
}
}
이벤트를 분리하였지만 역시 오류가 발생합니다.
이벤트를 분리함으로써 별도의 제어흐름을 가져가는 것처럼 착각할 수 있지만, 실제로는 같은 쓰레드에서 동작함을 확인할 수 있습니다.
@Async
어노테이션은 메서드를 별도의 쓰레드에서 실행하도록 합니다. @Async
메서드에서 발생하는 예외는 해당 스레드에서 처리되며, 원래의 트랜잭션에 영향을 주지 않습니다. RuntimeException
이 발생하더라도 펀드의 변경은 그대로 유지될 수 있습니다.@Async
어노테이션을 사용할 때는 다음과 같은 주의사항이 있습니다:@Async
메서드는 항상 public
이어야 하며, 같은 클래스 내에서 직접 호출하면 비동기로 동작하지 않습니다.@EnableAsync
어노테이션을 Spring Boot 애플리케이션의 설정 클래스에 추가해야 @Async
가 동작합니다.@EnableAsync
@Configuration
class AsyncConfig {
}
@SpringBootApplication
class SpringEventApplication {
@Autowired
private lateinit var fundUpdateServiceWithEvent: FundUpdateServiceWithEvent
@Bean
fun init(): CommandLineRunner {
return CommandLineRunner {
for (i in 1..100) {
fundUpdateServiceWithEvent.asyncUpdateFund()
}
}
}
}
fun main(args: Array<String>) {
runApplication<SpringEventApplication>()
}
@EnableAsync
@Configuration
class AsyncConfig {
@Bean(name = ["customAsyncExecutor"])
fun taskExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 10
executor.maxPoolSize = 20
executor.queueCapacity = 500
executor.setThreadNamePrefix("Async-")
executor.initialize()
return executor
}
}
@Service
@Transactional
class FundUpdateServiceWithEvent(
private val eventPublisher: ApplicationEventPublisher
) {
private val log = LoggerFactory.getLogger(this::class.java)
fun asyncUpdateFund() {
log.info("펀드 변경 시작~")
val fund = getFundById()
fund.changeTeam(Team("A02", "IB2팀"))
eventPublisher.publishEvent(AsyncUpdateEvent(fund))
log.info("펀드 변경 완료!")
}
private fun getFundById(): Fund {
return Fund("F01", "IB1팀-운용", Team("A01", "IB1팀"))
}
}
@Component
class UpdateEventListener(
private val sendMessengerService: SendMessengerService
) {
@TransactionalEventListener()
@Async("customAsyncExecutor")
fun listenEventWithAsync(updateEvent: AsyncUpdateEvent) {
sendMessengerService.sendAlarm(updateEvent.target)
}
}
@Test
@DisplayName("이벤트 분리와 비동기")
fun `async event isolation test`() {
fundUpdateServiceWithEvent.asyncUpdateFund()
}
Async-
로 시작합니다. 호출하는 쓰레드와 명백히 다른 쓰레드입니다.