컴퓨터 프로그래밍/Spring

[Spring] TransactionalEventListener

한33 2024. 10. 11. 10:14


ManagerRegistLogService

@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void ManagerRegistLog(ManagerRegistLogEvent managerRegistLogEvent) {

    ManagerRegistLog managerRegistLog = ManagerRegistLog.createManagerRegistLog(
            managerRegistLogEvent.getTodoTitle(),
            managerRegistLogEvent.getUserEmail(),
            managerRegistLogEvent.getManagerEmail(),
            managerRegistLogEvent.getManagerRegistLogEnum()
    );
    managerRegistLogRepository.save(managerRegistLog);
}

 

 

@Transactional(propagation = Propagation.REQUIRES_NEW)

 

트랜잭션 전파 옵션 중 기존 트랜잭션이 있으면 새로운 트랜잭션을 만드는 REQUIRES_NEW 옵션을 선택

→ 매니저 등록과는 별개로 로그가 찍히는 것을 계획

 

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)

 

TransactionalEventListener 의 phase 옵션에서 AFTER_COMPLETION 을 선택해 지정한 매니저 등록 트랜잭션이 commit 을 성공하든, 실패하든 ManagerRegistLog 메서드가 실행되도록 설정

 

+ AFTER_COMPLETION, AFTER_COMMIT, AFTER_ROLLBACK, BEFORE_COMMIT 등의 옵션이 있다.

 

→ 매니저 등록 트랜잭션이 commit 성공할 때와 실패할 때의 로직이 기존에는 일부 다른 부분이 있었지만, 동일한 로직으로 구현이 가능하게끔 커스터마이징했다.


 

ManagerRegistLogEvent

@Getter
public class ManagerRegistLogEvent {

    private final String todoTitle;
    private final String userEmail;
    private final String managerEmail;
    private final ManagerRegistLogEnum managerRegistLogEnum;

    public ManagerRegistLogEvent(String todoTitle, String userEmail, String managerEmail, ManagerRegistLogEnum managerRegistLogEnum) {
        this.todoTitle = todoTitle != null? todoTitle : "Unknown Title";
        this.userEmail = userEmail != null? userEmail : "Unknown Email";
        this.managerEmail = managerEmail != null? managerEmail : "Unknown Email";
        this.managerRegistLogEnum = managerRegistLogEnum;
    }
}

 

commit 성공할 때와 다르게 실패할 때에는 데이터들의 조회를 실패해 예외처리가 발생해서 commit 실패를 하는 것이기 때문에 ManagerRegistLogEvent 를 만들어서 데이터가 null 일 시에 "Unknown Title" 과 같이 직접적으로 문자열을 넣어주었다.

 

→ Log 를 저장할 시에 하나라도 null 이 있으면 데이터베이스에 저장이 안되는 에러가 발생했었다.

→ 위처럼 삼항연산자를 이용해 null 값을 없애주면 commit 성공, 실패시 구현되는 로직을 동일하게 가져갈 수 있었다.


ManagerService

@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {

    // 일정을 만든 유저
    User user = User.fromAuthUser(authUser);
    Todo todo = todoRepository.findById(todoId)
            .orElseThrow(() -> {
                applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(null, null, null, REGIST_LOG_FAIL__NOT_FOUND_TODO));
                return new InvalidRequestException("Todo not found");
            });


    if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
        applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(todo.getTitle(), todo.getUser().getEmail(), null, REGIST_LOG_FAIL_DUBLICATED_USER));
        throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 유효하지 않거나, 일정을 만든 유저가 아닙니다.");
    }

    User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
            .orElseThrow(() -> {
                applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(todo.getTitle(), todo.getUser().getEmail(), null, REGIST_LOG_FAIL_NOT_FOUND_MANAGER));
                return new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다.");
            });


    if (ObjectUtils.nullSafeEquals(user.getId(), managerUser.getId())) {
        applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(todo.getTitle(), todo.getUser().getEmail(), managerUser.getEmail(), REGIST_LOG_FAIL_DUBLICATED_USER));
        throw new InvalidRequestException("일정 작성자는 본인을 담당자로 등록할 수 없습니다.");
    }

    Manager newManagerUser = new Manager(managerUser, todo);
    Manager savedManagerUser = managerRepository.save(newManagerUser);

    // 매니저 등록 성공시 TransactionalEventListener 을 이용해 로그 저장
    applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(todo.getTitle(), todo.getUser().getEmail(), managerUser.getEmail(), REGIST_LOG_SUCCESS));

    return new ManagerSaveResponse(
            savedManagerUser.getId(),
            new UserResponse(managerUser.getId(), managerUser.getEmail(), managerUser.getNickname())
    );
}

 

매니저가 등록될 때 사용되는 saveManager 메서드는 기존에 각 각의 상황에 따라 예외처리가 발생했다.

 

나는 그 각 각의 상황에 맞게 커스터마이징해서 Log 데이터베이스에 저장시키고 싶었기 때문에 해당 예외처리가 발생하기 전에 이벤트리스너 로직이 작동되도록 상황에 맞는 상수를 넣어 각 각 다르게 넣어줬다.

 

* LazyInitializationException 발생 문제

 

예외처리가 되면 세션이 종료된다.

 

하지만 나는 이후에 트랜잭션을 하나 더 열어서 예외처리된 문구를 넣고싶었기 때문에, 세션이 종료되면 지연로딩 처리된 데이터를 미리 가져오지 않으면 데이터를 가져올 수가 없었다.

 

 

원래 ManagerRegistLogEvent 에서 Todo, Manager 를 가져왔었는데, 이렇게 되면 지연로딩 설정이 되어있기 때문에

 

LazyInitializationException 이 발생한다.

 

그래서 내가 찍고 싶었던 로그에서 정확히 필요한 Title, UserName 등을 String 으로 다이렉트로 받아오게했고, 

 

해당 연관 엔터티를 즉시로딩으로 수정하기보다 아예 연관되지 않게 지워서 해결했다.

 

ManagerRegistLogEnum

@Getter
@AllArgsConstructor
public enum ManagerRegistLogEnum {

    REGIST_LOG_SUCCESS("등록 성공"),

    REGIST_LOG_FAIL_DUBLICATED_USER("등록 실패 - 본인을 담당자로 등록 요청"),

    REGIST_LOG_FAIL_NOT_FOUND_MANAGER("등록 실패 - 등록하려는 유저 조회 실패"),

    REGIST_LOG_FAIL__NOT_FOUND_USER("등록 실패 - 로그인 유저 조회 실패"),

    REGIST_LOG_FAIL__NOT_FOUND_TODO("등록 실패 - 할 일 조회 실패");

    private final String message;
}

 

이처럼 이후에 로그 문구라든지 변경이 필요할 때 간단하게 할 수 있도록 enum 으로 만들어서 관리했다.

 

// 매니저 등록 성공시 TransactionalEventListener 을 이용해 로그 저장
applicationEventPublisher.publishEvent(new ManagerRegistLogEvent(todo.getTitle(), todo.getUser().getEmail(), managerUser.getEmail(), REGIST_LOG_SUCCESS));

 

매니저 등록에 성공하면 역시 트랜잭션 이벤트 리스너가 작동하도록 설정했다.

 

'컴퓨터 프로그래밍 > Spring' 카테고리의 다른 글

[Spring] 페이지네이션 정리 코드  (0) 2024.10.23
[Spring] Discord 알림 구현  (0) 2024.10.16
[Spring] Projection 및 예시코드 설명  (0) 2024.10.07
[Spring] Spring Security  (0) 2024.10.04
[Spring] QueryDSL  (1) 2024.10.03