πŸ“š[Spring] Spring Event

텁텁·2025λ…„ 6μ›” 20일

νŒ€ ν”„λ‘œμ νŠΈμ—μ„œ 진행쀑 μœ μ €λ„λ©”μΈμ˜ μš”κ΅¬μ‚¬ν•­μ— νƒœμŠ€ν¬ 도메인과 μ—°κ΄€λ˜μ–΄ μžˆλŠ” ν›„μ²˜λ¦¬ μž‘μ—…μ΄ μžˆμ—ˆλ‹€.

μš”κ΅¬μ‚¬ν•­
νšŒμ›νƒˆν‡΄ κΈ°λŠ₯

  • νƒˆν‡΄ μ‹œ ν•΄λ‹Ή μ‚¬μš©μžκ°€ λ‹΄λ‹Ήν•œ νƒœμŠ€ν¬λŠ” λ―Έν• λ‹Ή μƒνƒœλ‘œ λ³€κ²½

μš”κ΅¬μ‚¬ν•­μ— 맞좰 νšŒμ› νƒˆν‡΄ μ‹œ ν•΄λ‹Ή μ‚¬μš©μžκ°€ λ‹΄λ‹Ήν•˜κ³  μžˆλŠ” Task μ—”ν‹°ν‹°λ“€μ˜ assignedTo 값을 null 둜 λ°”κΎΈλŠ” ν›„μ²˜λ¦¬κ°€ ν•„μš”ν–ˆλ‹€.
이 λ‘œμ§μ„ 도메인 뢄리λ₯Ό μœ„ν•΄ 이벀트 λ¦¬μŠ€λ„ˆ λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν•˜κΈ°λ‘œ κ²°μ •ν–ˆλ‹€.


이벀트 - UserDeletedEvent

// μœ μ € μ‚­μ œμ‹œ λ°œν–‰λ  이벀트
@AllArgsConstructor
@Getter
public class UserDeletedEvent {
	private Long userId;
}

사싀상 ν›„μ²˜λ¦¬μ— ν•„μš”ν•œ 값은 μ‚­μ œλœ μœ μ €μ˜ ID λΏμ΄λ―€λ‘œ 이벀트 κ°μ²΄λŠ” λ‹¨μˆœν•˜κ²Œ κ΅¬μ„±ν•˜μ˜€λ‹€.

이벀트 λ°œν–‰ - UserService

public void deleteUser(...) {
	...
		user.softDelete();

		//μœ μ € μ‚­μ œ ν›„ 이벀트 λ°œν–‰
		eventPublisher.publishEvent(new UserDeletedEvent(user.getId()));
	}

UserService 의 μ‚­μ œ λ©”μ„œλ“œ μ’…λ£Œ μ‹œμ μ— 이벀트λ₯Ό λ°œν–‰ν•œλ‹€.


이벀트 λ¦¬μŠ€λ„ˆ - UserDeletedEventHandler

@Component
@RequiredArgsConstructor
public class UserDeletedEventHandler {

	private final TaskService taskService;

	/*
	μœ μ € μ‚­μ œ 이벀트 λ¦¬μŠ€λ„ˆ
	- μ΄λ²€νŠΈμ—μ„œ λ°›μ•„μ˜¨ μ‚­μ œλœ μœ μ €μ˜ μ•„μ΄λ””κ°’μœΌλ‘œ ν…ŒμŠ€ν¬ μ„œλΉ„μŠ€μ˜ ν›„μ²˜λ¦¬λ₯Ό μ§„ν–‰
	 */
	@EventListener
	public void handleUserDeleted(UserDeletedEvent event) {
		taskService.ownerDeletionForTask(event.getUserId());
	}

}

Task λ„λ©”μΈμ—μ„œ ν•΄λ‹Ή 이벀트λ₯Ό μˆ˜μ‹ ν•˜μ—¬ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ λ¦¬μŠ€λ„ˆλ₯Ό κ΅¬ν˜„ν–ˆλ‹€.


문제 λ°œμƒ

TaskService

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class TaskService {
    ...
	public void ownerDeletionForTask(Long userId) {
		List<Task> foundTasks = taskRepository.findByAssignedTo_IdAndDeletedFalse(userId);

		foundTasks.forEach(Task::unassignTask);
	}
    ...
}

TaskService μ—λŠ” 클래슀 λ ˆλ²¨μ— @Transactional이 μ„ μ–Έλ˜μ–΄ μžˆμ–΄ 이벀트 λ¦¬μŠ€λ„ˆ λ‚΄λΆ€μ—μ„œ ν˜ΈμΆœν•œ ownerDeletionForTask() λ©”μ„œλ“œ λ˜ν•œ νŠΈλžœμž­μ…˜μ΄ 적용될 κ²ƒμœΌλ‘œ μ˜ˆμƒν–ˆλ‹€.
λ”°λΌμ„œ μ—”ν‹°ν‹°μ˜ ν•„λ“œ 값을 λ³€κ²½ν•œ ν›„ save() 없이도 더티 μ²΄ν‚ΉμœΌλ‘œ DB에 반영될 것이라 κΈ°λŒ€ν–ˆμ§€λ§Œ μ˜ˆμƒκ³Ό 달리 λ³€κ²½λœ 값은 DB에 λ°˜μ˜λ˜μ§€ μ•Šμ•˜λ‹€.


문제 원인

이벀트 λ¦¬μŠ€λ„ˆμ—μ„œ νŠΈλžœμž­μ…˜μ΄ μ„ μ–Έλœ μ„œλΉ„μŠ€ λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν–ˆμ§€λ§Œ
호좜 μ‹œμ μ΄λ‚˜ νŠΈλžœμž­μ…˜ 경계 문제둜 인해 더티 체킹이 λ™μž‘ν•˜μ§€ μ•Šμ•„ DB 반영이 λ˜μ§€ μ•Šμ•˜λ‹€.


ν•΄κ²° 방법

	public void ownerDeletionForTask(Long userId) {
		List<Task> foundTasks = taskRepository.findByAssignedTo_IdAndDeletedFalse(userId);

		foundTasks.forEach(Task::unassignTask);
	    taskRepository.saveAll(foundTasks); //λͺ…μ‹œμ μœΌλ‘œ μ €μž₯
	}

νŠΈλžœμž­μ…˜μ΄ μ μš©λ˜μ§€ μ•ŠμŒμ„ ν™•μΈν•˜κ³  λͺ…μ‹œμ μœΌλ‘œ saveAll()을 ν˜ΈμΆœν•˜μ—¬ DB λ°˜μ˜μ„ 보μž₯ν•˜λ„λ‘ μˆ˜μ •ν–ˆλ‹€.


마치며

이번 ν”„λ‘œμ νŠΈμ—μ„  λ‹¨μˆœν•œ ꡬ쑰둜 이벀트λ₯Ό μ‚¬μš©ν•΄λ΄€μ§€λ§Œ λ‹€μ–‘ν•œ 상황에 μ μš©ν•  수 μžˆλŠ” κΈ°λŠ₯μ΄λΌλŠ” μ μ—μ„œ 쒋은 κ²½ν—˜μ΄μ—ˆλ‹€.
λ‹€μŒ ν”„λ‘œμ νŠΈμ—μ„  보닀 μ‹€μš©μ μ΄κ³  의미 μžˆλŠ” λ°©μ‹μœΌλ‘œ 이벀트λ₯Ό λ„μž…ν•΄ λ³΄λŠ” 것도 κ³ λ―Όν•΄λ³Ό 생각이닀.

profile
μ°¨κ·Όμ°¨κ·Ό

2개의 λŒ“κΈ€

comment-user-thumbnail
2025λ…„ 6μ›” 24일

"호좜 μ‹œμ μ΄λ‚˜ νŠΈλžœμž­μ…˜ 경계 문제둜 인해" λΌλŠ” 뢀뢄이 핡심인 것 같은데, μ •ν™•νžˆ μ–΄λ–€ μ΄μœ μ— μ˜ν•΄μ„œ λ™μž‘ν•˜μ§€ μ•ŠλŠ” κ±΄κ°€μš”?

1개의 λ‹΅κΈ€