์ฌ์ฉ์ ์ ์ฅ ์ ์ฌ์ฉ์์ ์ ๋ณด์ ์ผ์นํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง ์กฐํ์ฉ ํ๋กํ ๋ฐ์ดํฐ๋ ํจ๊ป ์ ์ฅํ๋ค๊ณ ๊ฐ์ ํ์.
์ด ๋ ๋ฐ์ดํฐ๋ ๊ฐ๊ฐ ๋ค๋ฅธ ํ
์ด๋ธ์ ์กด์ฌํ์ง๋ง ๊ฐ์ ์ ๋ณด๋ฅผ ๊ณต์ ํ๋ฏ๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅ๋ฐ์์ผ ํ๋ค.
์ผ๋ฐ์ ์ธ ๋ชจ๋๋ฆฌ์ ์ํคํ
์ฒ(monolithic architecture) ์์๋ ๋ค์๊ณผ ๊ฐ์ด ๋ ๊ฐ์ ๋ ํฌ์งํฐ๋ฆฌ๋ฅผ ์๋น์ค์ ์ฃผ์
ํ๊ณ
๋จ์ผ ํธ๋์ญ์
๋ด์์ ์ฒ๋ฆฌํจ์ผ๋ก์จ ์์์ฑ์ ํตํด ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
@Transactional
public CreateUserResponse createUserAndProfile(CreateRequest request){
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = new User(request.getEmail(), request.getName(), encodedPassword);
Profile profile = new Profile(request.getEmail, request.getName());
userRepository.save(user);
profileRepository.save(profile);
...
}
์์ createUserAndProfile() ๋ฉ์๋๋ @Transactional ์ด๋
ธํ
์ด์
์ผ๋ก ํธ๋์ญ์
์ด ๋ฌถ์ฌ ์๊ธฐ ๋๋ฌธ์ ์ค๊ฐ์ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ ์ฒด ํธ๋์ญ์
์ด ๋กค๋ฐฑ๋์ด ๋ฐ์ดํฐ์ ์์์ฑ๊ณผ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
ํ์ง๋ง ๋ ๋ฐ์ดํฐ๊ฐ ์๋ก ๋ค๋ฅธ ํธ๋์ญ์
์์ ์ฒ๋ฆฌ๋ ๊ฒฝ์ฐ ๋ฐ์ดํฐ์ ์์์ฑ์ ๋ณด์ฅ๋ฐ์ ์ ์๋ค.
๋๋ฉ์ธ ๊ฐ ์์กด๋๋ฅผ ๋ฎ์ถ๊ธฐ ์ํด ์ฌ์ฉ์ ์ ์ฅ ์ดํ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ์ฌ ํ๋กํ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ค๊ณ ๊ฐ์ ํด๋ณด์.
@Transactional
public CreateUserResponse signup(SignupRequest request) {
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = new User(request.getEmail(), request.getName(), encodedPassword);
User savedUser = userRepository.save(user);
//ํ์๊ฐ์
์ด๋ฒคํธ ๋ฐํ
eventPublisher.publishEvent(
UserRegisteredEvent.of(savedUser.getUserId(), savedUser.getEmail(),
savedUser.getName())
);
...
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlerUserRegisteredEvent(UserRegisteredEvent event) {
Profile profile = Profile.fromUserEvent(
event.getUserId(),
event.getEmail(),
event.getName()
);
profileRepository.save(profile);
}
์ ์ฝ๋์์ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) ์ด๋
ธํ
์ด์
์ ํด๋น ์ด๋ฒคํธ๊ฐ ๋ฐํ๋ ํธ๋์ญ์
์ด ์ ์์ ์ผ๋ก ์ปค๋ฐ๋ ์ดํ์ ์ด๋ฒคํธ ๋ก์ง์ด ์คํ๋๋๋ก ๋ณด์ฅํ๋ค.
๋ฐ๋ผ์ ์ฌ์ฉ์ ์ ์ฅ(User)์ ์ปค๋ฐ์ด ์๋ฃ๋ ์ํ์ด๋ฏ๋ก ์ด๋ฒคํธ ๋ฆฌ์ค๋์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋กํ ์ ์ฅ์ด ์์๋๋ค.
ํ์ง๋ง ์ด ํ์ ์์
์ค์ ์์์น ๋ชปํ ์์ธ๊ฐ ๋ฐ์ํ ์ ์๋ค.
์ฌ์ค ํด๋น ๊ตฌ์กฐ์์๋ @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) ์ผ๋ก ์ค์ ํ๋ฉด ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ํธ๋์ญ์
์ปค๋ฐ ์ ์ ์คํ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง๋ ๋์ผํ ํธ๋์ญ์
์์์ ์ฒ๋ฆฌํ ์ ์์ด ์ด๋ก ์ ์ผ๋ก๋ ๋ฐ์ดํฐ ์์์ฑ ๋ณด์ฅ์ด ๊ฐ๋ฅํด์ง๋ค.
ํ์ง๋ง ์ค๋ฌด์์๋ ์ด ๋ฐฉ์๋ณด๋ค AFTER_COMMIT ์ด ์ ํธ๋๋๋ฐ ๊ทธ ์ด์ ๋ ํฌ๊ฒ ๋ ๊ฐ์ง ์ด์ ๊ฐ ์๋ค.
BEFORE_COMMIT์ ๋ฉ์ธ ํธ๋์ญ์
๋์ค ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์คํ๋์ง ์๊ฑฐ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ค ์์ธ๊ฐ ํธ๋์ญ์
์ ์ฒด์ ์ํฅ์ ์ค ์ ์๋ค.BEFORE_COMMIT์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ๋ฉ์ธ ํธ๋์ญ์
์ ์ฝํ๊ฒ ๋๋ฏ๋ก ๊ฒฐ๊ณผ์ ์ผ๋ก ๋๋ฉ์ธ ๊ฐ ๊ฒฐํฉ๋๊ฐ ์๊ธด๋ค.๋ฌด๊ฒฐ์ฑ๋ง ๊ณ ๋ คํ๋ฉด BEFORE_COMMIT๋ ์ ํจํ ์ ํ์ผ ์ ์์ง๋ง ํธ๋์ญ์
๊ฒฝ๊ณ ๋ถ๋ฆฌ, ๋๋ฉ์ธ ๊ฒฐํฉ๋ ์ต์ํ, ์คํจ ์ ์ ์ฐํ ๋ณต๊ตฌ๋ฅผ ๊ณ ๋ คํ๋ค๋ฉด ๋ณด์ ํธ๋์ญ์
์ด๋ Outbox ํจํด์ ํตํ ์ ๊ทผ์ด ๋ ์ ์ ํ ์ ํ์ผ ์ ์๋ค.
๋ณด์ ํธ๋์ญ์ ์ ๋ถ์ฐ ์์คํ ๊ณผ ๊ฐ์
์๋ ์์ ์ ๋๋๋ฆด ์ ์๋ ํ๊ฒฝ ์์ ์คํจํ ์์ ์ ๋ํด ๋ฐ๋ ๋ฐฉํฅ์ ๋์์ ์คํํด ์ํ๋ฅผ ๋ณต๊ตฌํ๋ ํจํด์ด๋ค.
์๋ฅผ ๋ค์ด ์ฌ์ฉ์ ์ ์ฅ ํ ํ๋กํ ์ ์ฅ์ ์คํจํ ๊ฒฝ์ฐ ์ฌ์ฉ์ ๋ฑ๋ก์ ์ทจ์ํ๊ฑฐ๋ ๋ฌดํจํํ๋ ๋ฑ์ ๋ณด์ ์์ ์ ์ํํ์ฌ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ง์ถ๋ค.
Outbox ํจํด์ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์์คํ ์์ ์ด๋ฒคํธ ๋ฐํ๊ณผ ๋ฐ์ดํฐ ์ ์ฅ์ ๋์ผํ ํธ๋์ญ์ ์์์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ํจํด์ด๋ค.
๋ฉ์ธ ํธ๋์ญ์
์์์ ๋๋ฉ์ธ ๋ฐ์ดํฐ์ ํจ๊ป ์ด๋ฒคํธ ๋ ์ฝ๋(Outbox ํ
์ด๋ธ) ๋ฅผ ํจ๊ป ์ ์ฅํ๊ณ ์ค์ผ์ค๋ฌ๋ Kafka ๋ฑ ๋ณ๋์ ํ๋ก์ธ์๊ฐ Outbox ํ
์ด๋ธ์ ๋ชจ๋ํฐ๋งํ์ฌ ํด๋น ์ด๋ฒคํธ๋ฅผ ๋ฉ์ธ์ง ํ(Message Queue)๋ก ๋น๋๊ธฐ ์ ์กํ๋ค.
์ด๋ฅผ ํตํด ์ด๋ฒคํธ ์ ์ค์ ๋ฐฉ์งํ๊ณ DB์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ง ์ด๋ฒคํธ๋ก ๋ฐํ๋๋๋ก ๋ณด์ฅํด ๋ฐ์ดํฐ์ ์ด๋ฒคํธ ๊ฐ ์ ํฉ์ฑ์ ํ๋ณดํ ์ ์๋ค.
๋๋ ์์ ๊ฐ์ ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณด์ ํธ๋์ญ์ ๋ฐฉ์์ ๋์ ํ์ฌ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ฆฌํฉํ ๋งํ์๋ค.
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlerUserRegisteredEvent(UserRegisteredEvent event) {
try {
Profile profile = Profile.fromUserEvent(
event.getUserId(),
event.getEmail(),
event.getName()
);
profileRepository.save(profile);
} catch (Exception e) {
log.info("ํ๋กํ ์์ฑ ์คํจ: userId={}, error={}", event.getUserId(), e.getMessage());
eventPublisher.publishEvent(UserSaveFailedEvent.of(event.getUserId()));
}
}
์ด๋ฒคํธ ๋ฆฌ์ค๋๋ try-catch๋ฅผ ํตํด ์์ ๊ฐ๋ฅํ ์์ธ๋ฅผ ๋ฐฉ์ดํ๊ณ ์คํจ ์ UserSaveFailedEvent๋ผ๋ ๋ณด์ ์ด๋ฒคํธ๋ฅผ
๋ฐํํ์ฌ ํ์์กฐ์น๊ฐ ์ด์ด์ง ์ ์๋๋ก ์ ์ฐํ๊ฒ ์ค๊ณํ๋ค.
@EventListener
public void handlerUserSaveFailedEvent(UserSaveFailedEvent event) {
try {
authRepository.deleteByAuthId(event.getAuthId());
} catch (Exception e) {
log.error("Auth ๋กค๋ฐฑ ์คํจ : authId={}", event.getAuthId());
}
}
๋ณด์ ์ด๋ฒคํธ ๋ฆฌ์ค๋์์๋ ์ฌ์ฉ์ ๋ฑ๋ก์ ์ทจ์ํ๋ ๋์์ ์ํํ์ฌ Profile ์ ์ฅ ์คํจ๋ก ์ธํด ๋ถ์์ ํ๊ฒ ๋จ์ ์ฌ์ฉ์ ๋ฐ์ดํฐ(User)๋ฅผ ์ ๊ฑฐํ์ฌ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ์ ์งํ ์ ์๋๋ก ํ๋ค.
์ด๋ฒคํธ ์ฒ๋ฆฌ ์ค ์คํจ ์ํฉ์ ๊ฐ์งํ๊ณ ๋ณด์ ํธ๋์ญ์
์ ๋ฐํํจ์ผ๋ก์จ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ์ ์ฅ์ ์ ์ ์งํ๋ฉฐ ํธ๋์ญ์
๋ถ๋ฆฌ๋ก ์ธํ ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ ๋ฅผ ํ์ค์ ์ธ ๋ฐฉ์์ผ๋ก ๋ณด์ํ๋ ๊ฒ์ด๋ค.
์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ์์ ๋ฐ์ํ ์ ์๋ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ ์ ๊ทธ ํด๊ฒฐ์ฑ
์ผ๋ก ๋ณด์ ํธ๋์ญ์
ํจํด์ ์ดํด๋ดค๋ค.
ํธ๋์ญ์
๊ฒฝ๊ณ๊ฐ ๋ถ๋ฆฌ๋ ํ๊ฒฝ์์๋ ์์ ์ ์ผ๋ก ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ์ ์งํ๊ธฐ ์ํด์ ์คํจ๋ฅผ ๊ฐ์งํ๊ณ ์ ์ ํ ๋ณต๊ตฌํ๋ ์ค๊ณ๊ฐ ๋ฐ๋์ ํ์ํ๋ค๊ณ ์๊ฐ๋๋ค.
๋ค์์๋ Outbox ํจํด์ ์ค์ ์ ์ฉํด๋ณด๊ณ ๋ณด์ ํธ๋์ญ์
๊ณผ ์ด๋ป๊ฒ ์กฐํฉํ ์ง ๊ณ ๋ฏผํด๋ณด๋ ๊ฑธ ๋ชฉํ๋ก ํ๊ฒ ๋ค.