개발일지

[트러블슈팅] 트랜잭션 적용 안됨 | @Transactional 적용 안됨 | 알림 안보내짐

kimslab01 2024. 11. 7. 14:58

 

 

 

1. 문제 정의

관심 물품으로 설정한 물품의 가격이 변동되었을 때, AOP로 알림을 보내려고 함.

하지만 service에서는 제대로 oldPrice와 newPrice가 인식되지만 AOP에서는 oldPrice가 newPrice의 값과 일치하게 인식되는 문제 발생.

@AfterReturning(value = "execution(* com.sprarta.sproutmarket.domain.item.service.ItemService.updateContents(..))", returning = "itemResponse", argNames = "joinPoint,itemResponse")
    public void notifyUsersAboutPriceChange(JoinPoint joinPoint, Object itemResponse) {
        Object[] args = joinPoint.getArgs();
        Long itemId = (Long) args[0];  // itemId 추출
        ItemContentsUpdateRequest itemContentsUpdateRequest = (ItemContentsUpdateRequest) args[1]; // 요청 객체 추출

        // DB 에서 아이템 조회
        Item item = itemRepository.findByIdOrElseThrow(itemId);
        int oldPrice = item.getPrice();
        int newPrice = itemContentsUpdateRequest.getPrice();

        // 가격이 변경되었는지 확인
        log.info("Item ID: {}, Old Price: {}, New Price: {}", itemId, oldPrice, newPrice);

        if (oldPrice != newPrice) {
            log.info("Price has changed for Item ID: {}. Notifying interested users...", itemId);

            List<User> interestedUsers = interestedItemService.findUsersByInterestedItem(itemId);

            // 관심 상품 사용자들에게 알림 전송
            for (User user : interestedUsers) {
                String notificationMessage = "관심 상품의 가격이 변경되었습니다. 새로운 가격: " + newPrice;
                log.info("Sending notification to User ID: {} with message: {}", user.getId(), notificationMessage); // 전송할 메시지 로그
                sendNotification(user.getId(), notificationMessage);
            }

            log.info("Notifications sent to {} users about the price change for Item ID: {}", interestedUsers.size(), itemId);
        } else {
            log.info("No price change for Item ID: {}. No notifications sent.", itemId);
        }
    }
Hibernate: 
    /* SELECT
        u 
    FROM
        User u 
    WHERE
        u.email = :email */ select
            u1_0.id,
            u1_0.address,
            u1_0.created_at,
            u1_0.email,
            u1_0.modified_at,
            u1_0.nickname,
            u1_0.password,
            u1_0.phone_number,
            u1_0.profile_image_url,
            u1_0.rate,
            u1_0.status,
            u1_0.user_role,
            u1_0.username 
        from
            users u1_0 
        where
            u1_0.email=?
Hibernate: 
    /* SELECT
        i 
    FROM
        Item i 
    WHERE
        i.id = :id 
        AND i.seller.id = :userId */ select
            i1_0.id,
            i1_0.category_id,
            i1_0.created_at,
            i1_0.description,
            i1_0.item_sale_status,
            i1_0.modified_at,
            i1_0.price,
            i1_0.user_id,
            i1_0.status,
            i1_0.title 
        from
            items i1_0 
        where
            i1_0.id=? 
            and i1_0.user_id=?
2024-11-07T00:18:54.182+09:00  INFO 9896 --- [sprout-market] [nio-8080-exec-3] c.s.s.domain.item.service.ItemService    : Updated Item ID: 8, New Price: 2001
Hibernate: 
    select
        u1_0.id,
        u1_0.address,
        u1_0.created_at,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.phone_number,
        u1_0.profile_image_url,
        u1_0.rate,
        u1_0.status,
        u1_0.user_role,
        u1_0.username 
    from
        users u1_0 
    where
        u1_0.id=?
2024-11-07T00:18:54.200+09:00  INFO 9896 --- [sprout-market] [nio-8080-exec-3] c.s.s.d.n.aspect.NotificationAspect      : Item ID: 8, Old Price: 2001, New Price: 2001
2024-11-07T00:18:54.200+09:00  INFO 9896 --- [sprout-market] [nio-8080-exec-3] c.s.s.d.n.aspect.NotificationAspect      : No price change for Item ID: 8. No notifications sent.
Hibernate: 
    /* update
        for com.sprarta.sproutmarket.domain.item.entity.Item */update items 
    set
        category_id=?,
        description=?,
        item_sale_status=?,
        modified_at=?,
        price=?,
        user_id=?,
        status=?,
        title=? 
    where
        id=?
2024-11-07T00:19:09.120+09:00  INFO 9896 --- [sprout-market] [MessageBroker-1] o.s.w.s.c.WebSocketMessageBrokerStats    : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]

 

결국 변화가 없다고 감지하여 알림이 가지 않는 상황 발생.

 

 

 

 

2. 사실 수집

Hibernate: 
    /* SELECT
        u 
    FROM
        User u 
    WHERE
        u.email = :email */ select
            u1_0.id,
            u1_0.address,
            u1_0.created_at,
            u1_0.email,
            u1_0.modified_at,
            u1_0.nickname,
            u1_0.password,
            u1_0.phone_number,
            u1_0.profile_image_url,
            u1_0.rate,
            u1_0.status,
            u1_0.user_role,
            u1_0.username 
        from
            users u1_0 
        where
            u1_0.email=?
Hibernate: 
    /* SELECT
        i 
    FROM
        Item i 
    WHERE
        i.id = :id 
        AND i.seller.id = :userId */ select
            i1_0.id,
            i1_0.category_id,
            i1_0.created_at,
            i1_0.description,
            i1_0.item_sale_status,
            i1_0.modified_at,
            i1_0.price,
            i1_0.user_id,
            i1_0.status,
            i1_0.title 
        from
            items i1_0 
        where
            i1_0.id=? 
            and i1_0.user_id=?
Hibernate: 
    /* update
        for com.sprarta.sproutmarket.domain.item.entity.Item */update items 
    set
        category_id=?,
        description=?,
        item_sale_status=?,
        modified_at=?,
        price=?,
        user_id=?,
        status=?,
        title=? 
    where
        id=?
Hibernate: 
    select
        u1_0.id,
        u1_0.address,
        u1_0.created_at,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.phone_number,
        u1_0.profile_image_url,
        u1_0.rate,
        u1_0.status,
        u1_0.user_role,
        u1_0.username 
    from
        users u1_0 
    where
        u1_0.id=?

 

이 쿼리를 볼 때, 사용자를 조회, 아이템 조회, 업데이트, 사용자 조회까지는 되는데
그 뒤에 가격 변동 후 알림 발송 로직, 이벤트 발생, 알림 전송이 제대로 되지 않고 있음.

 

 

 

 

3. 원인 추론

가격이 업데이트된 후 Item 객체가 다시 로드되지 않기 때문이라고 판단. @Transactional이 적용된 메서드에서 가격 변경이 발생하고, 그 후에 Aspect에서 가격을 가져올 때는 이미 트랜잭션이 커밋되지 않아 oldPrice가 올바르게 업데이트되지 않는 상황.

 

 

 

 

4. 조치 및 방안 검토

@Transactional 메서드 내에서 가격 변경 후 flush()를 호출하여 즉시 데이터베이스에 반영한 후,
가격 변경 이벤트를 발행하여 관심 있는 사용자에게 알림을 전송합니다.
트랜잭션 커밋 후에 알림 전송 로직이 실행되므로, 변경된 가격을 올바르게 인식하고 알림을 보낼 수 있게 됨.

 

1. updateContents

    @Transactional
    public ItemResponse updateContents(Long itemId, ItemContentsUpdateRequest itemContentsUpdateRequest, CustomUserDetails authUser) {
        Item item = itemRepository.findByIdAndSellerIdOrElseThrow(itemId, User.fromAuthUser(authUser).getId());

        int oldPrice = item.getPrice(); // 이전 가격

        item.changeContents(
                itemContentsUpdateRequest.getTitle(),
                itemContentsUpdateRequest.getDescription(),
                itemContentsUpdateRequest.getPrice()
        );

        // flush()를 호출하여 변경 사항을 데이터베이스에 반영
        entityManager.flush();

        int newPrice = item.getPrice(); // 이제 변경된 가격을 가져옴

        // 로그 추가: 가격 변경 감지
        log.info("Updated Item ID: {}, Old Price: {}, New Price: {}", itemId, oldPrice, newPrice);

        // 가격이 변경된 경우 이벤트 발행
        if (oldPrice != newPrice) {
            eventPublisher.publishEvent(new PriceChangeEvent(itemId, newPrice));
        }

        return ItemResponse.builder()
                .title(item.getTitle())
                .price(item.getPrice())
                .description(item.getDescription())
                .nickname(item.getSeller().getNickname())
                .build();
    }

 

가격 가져오기: oldPrice는 item.getPrice()를 통해 기존 가격을 가져오고
가격 변경: changeContents 메서드 호출로 가격을 변경.
flush() 호출: entityManager.flush()를 사용하여 변경 사항을 데이터베이스에 즉시 반영. 이렇게 하면 Aspect에서 oldPrice와 newPrice를 올바르게 비교할 수 있게 됨.
이벤트 발행: 가격이 변경된 경우, 가격 변경 이벤트를 발행하여 관심 있는 사용자에게 알림을 보낼 준비.

 

2. PriceChangeEvent

@Getter
public class PriceChangeEvent {
    private final Long itemId;
    private final int newPrice;

    public PriceChangeEvent(Long itemId, int newPrice) {
        this.itemId = itemId;
        this.newPrice = newPrice;
    }
}

 

이벤트 클래스: 가격 변경 이벤트를 나타내는 간단한 DTO. 가격이 변경된 아이템의 ID와 새 가격을 담고 있고,

이건 DB와 관련 없는 DTO이기 때문에 @Entity 등의 어노테이션을 붙이고 연관관계를 만들 필요가 없음. 

 

3. handlePriceChangeEvent

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handlePriceChangeEvent(PriceChangeEvent event) {
        Long itemId = event.getItemId();
        int newPrice = event.getNewPrice();

        List<User> interestedUsers = interestedItemService.findUsersByInterestedItem(itemId);

        for (User user : interestedUsers) {
            sendNotification(user.getId(), "관심 상품의 가격이 변경되었습니다. 새로운 가격: " + newPrice);
        }
    }

 

이벤트 리스너: 가격 변경 이벤트를 처리. @TransactionalEventListener 어노테이션을 사용하여 트랜잭션 커밋 후에 이 메서드가 실행되도록 설정. 이로 인해 가격 변경이 확실히 데이터베이스에 반영된 후에 알림을 전송.
알림 전송: 관심 있는 사용자 목록을 가져오고, 각 사용자에게 가격 변경 알림을 전송.

 

 

 

 

5. 결과 관찰

flush를 적용 시키고 @TransactionalEventListener 어노테이션을 사용하니,

Hibernate: 
    /* SELECT
        u 
    FROM
        User u 
    WHERE
        u.email = :email */ select
            u1_0.id,
            u1_0.address,
            u1_0.created_at,
            u1_0.email,
            u1_0.modified_at,
            u1_0.nickname,
            u1_0.password,
            u1_0.phone_number,
            u1_0.profile_image_url,
            u1_0.rate,
            u1_0.status,
            u1_0.user_role,
            u1_0.username 
        from
            users u1_0 
        where
            u1_0.email=?
Hibernate: 
    /* SELECT
        i 
    FROM
        Item i 
    WHERE
        i.id = :id 
        AND i.seller.id = :userId */ select
            i1_0.id,
            i1_0.category_id,
            i1_0.created_at,
            i1_0.description,
            i1_0.item_sale_status,
            i1_0.modified_at,
            i1_0.price,
            i1_0.user_id,
            i1_0.status,
            i1_0.title 
        from
            items i1_0 
        where
            i1_0.id=? 
            and i1_0.user_id=?
Hibernate: 
    /* update
        for com.sprarta.sproutmarket.domain.item.entity.Item */update items 
    set
        category_id=?,
        description=?,
        item_sale_status=?,
        modified_at=?,
        price=?,
        user_id=?,
        status=?,
        title=? 
    where
        id=?
2024-11-07T12:57:41.016+09:00  INFO 29484 --- [sprout-market] [nio-8080-exec-6] c.s.s.domain.item.service.ItemService    : Updated Item ID: 8, Old Price: 2008, New Price: 2009
Hibernate: 
    select
        u1_0.id,
        u1_0.address,
        u1_0.created_at,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.phone_number,
        u1_0.profile_image_url,
        u1_0.rate,
        u1_0.status,
        u1_0.user_role,
        u1_0.username 
    from
        users u1_0 
    where
        u1_0.id=?
Hibernate: 
    /* SELECT
        ii 
    FROM
        InterestedItem ii 
    WHERE
        ii.item.id = :itemId */ select
            ii1_0.id,
            ii1_0.item_id,
            ii1_0.user_id 
        from
            interested_item ii1_0 
        where
            ii1_0.item_id=?
2024-11-07T12:57:41.027+09:00  INFO 29484 --- [sprout-market] [nio-8080-exec-6] c.s.s.d.i.service.InterestedItemService  : Found 1 interested users for item 8

제대로 적용이 되는 모습!

 

 

 

+ 부가적으로 flush를 사용한 이유

 

1. 트랜잭션의 지연 로딩

- JPA에서는 기본적으로 모든 변경 사항을 지연 로딩. 즉, 실제로 데이터베이스에 반영되기 전에 변경 사항이 메모리 내에서 관리하게 됨.

- flush()를 호출하지 않으면, 트랜잭션이 커밋될 때까지 데이터베이스에 아무런 변경 사항이 반영되지 않음. 이로 인해 트랜잭션 내에서 값이 변경된 이후에 다른 메서드나 Aspect에서 이 값을 참조할 경우, 이전 값이 그대로 유지될 수 있게 됨.

 

2. 데이터의 일관성

- flush()를 사용하여 데이터베이스와 애플리케이션의 상태를 일관되게 유지할 수 있게 됨. 예를 들어, 가격이 변경된 후 해당 가격을 기준으로 후속 작업을 수행해야 할 때, flush()를 통해 변경된 가격을 즉시 데이터베이스에 반영함으로써, 이후 로직에서 최신 상태의 데이터를 참조할 수 있음.

 

3. 이벤트 발생 시점 조정

- 이벤트를 발행하기 전에 flush()를 호출하면, 이벤트 핸들러가 이 변경 사항을 기반으로 올바르게 동작하게 됨. 즉, 가격이 변경된 후 관련된 이벤트를 발행할 때, flush()를 통해 데이터베이스에 반영된 가격을 기반으로 이벤트를 생성할 수 있음.

 

4. 성능

- flush()는 성능에 영향을 줄 수 있지만, 데이터의 일관성과 정확성을 보장하기 위해 필요할 때 사용됨. 일반적으로 필요한 상황에서만 호출하여 성능 저하를 최소화해야 함.