Spring

[ Spring ] 비관적 락 성능 개선을 해보자

haenni 2025. 9. 30. 18:16
Notion에서 작성 된 글입니다. 템플릿이 깨진다면 Notion을 확인해주세요. 

 

 

비관적 락 성능 개선을 해보자 | Notion

들어가기 앞서 . . .

hail-buttercup-c86.notion.site

 

들어가기 앞서 . . .

동시성 보장을 위해 비관적 락을 적용했지만, 부하 테스트에서 TPS가 1.3으로 너무 낮게 측정되는 문제를 확인했다.

락 자체를 포기하지 않은 상태에서(=비관적 락 유지) 어디에서 병목이 생기고 무엇을 줄이면 성능을 끌어올릴 수 있는지 검증·개선해 보고자 이 글을 정리했다.

 


비관적 락 적용 후 성능

부하 테스트를 진행하며 성능이 너무 느리게 나와 가장 먼저 드는 의문이 DB가 잘못설계되었나?라는 생각이 들었다.

쿼리를 개선할 수 있는 부분이 있지 않나해서 요청할 때 실행되는 SQL을 먼저 뜯어보았다.

 

해결과정1: 필요없는 테이블을 조회하지말자

연관관계 LAZY 설정

2025-08-29T16:48:10.094+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_email=?
2025-08-29T16:48:10.117+09:00  INFO 11684 --- [nio-8081-exec-2] c.p.t.global.security.filter.JwtFilter   : Authentication: UsernamePasswordAuthenticationToken [Principal=com.profect.tickle.global.security.util.principal.CustomUserDetails@265291ab, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[MEMBER]]
2025-08-29T16:48:10.172+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        e1_0.event_id,
        e1_0.event_accrued,
        e1_0.coupon_id,
        e1_0.event_created_at,
        e1_0.event_goal_price,
        e1_0.event_name,
        e1_0.event_per_price,
        e1_0.status_id,
        e1_0.event_type,
        e1_0.event_updated_at 
    from
        event e1_0 
    where
        e1_0.event_id=? for no key update
2025-08-29T16:48:10.191+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        s1_0.seat_id,
        s1_0.seat_created_at,
        s1_0.event_id,
        s1_0.member_id,
        s1_0.performance_id,
        s1_0.preempted_at,
        s1_0.preempted_until,
        s1_0.preemption_token,
        s1_0.reservation_id,
        s1_0.seat_code,
        s1_0.seat_grade,
        s1_0.seat_number,
        s1_0.seat_price,
        s1_0.status_id 
    from
        seat s1_0 
    where
        s1_0.event_id=?
2025-08-29T16:48:10.244+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_id=?
2025-08-29T16:48:10.255+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        p1_0.member_id,
        p1_0.point_id,
        p1_0.point_created_at,
        p1_0.point_credit,
        p1_0.point_order_id,
        p1_0.point_target 
    from
        point p1_0 
    where
        p1_0.member_id=?
2025-08-29T16:48:10.291+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    insert 
    into
        point
        (point_created_at, point_credit, member_id, point_order_id, point_target) 
    values
        (?, ?, ?, ?, ?)
2025-08-29T16:48:10.331+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    update
        event 
    set
        event_accrued=?,
        coupon_id=?,
        event_created_at=?,
        event_goal_price=?,
        event_name=?,
        event_per_price=?,
        status_id=?,
        event_type=?,
        event_updated_at=? 
    where
        event_id=?
2025-08-29T16:48:10.343+09:00 DEBUG 11684 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    update
        member 
    set
        member_birthday=?,
        member_deleted_at=?,
        member_email=?,
        host_biz_address=?,
        host_biz_bank=?,
        host_biz_bank_number=?,
        host_biz_ceo=?,
        host_biz_depositor=?,
        host_biz_ecommerce_registration_number=?,
        host_biz_name=?,
        host_biz_number=?,
        member_img=?,
        member_role=?,
        member_nickname=?,
        member_pw=?,
        member_number=?,
        member_point_balance=?,
        member_updated_at=? 
    where
        member_id=?
2025-08-29T16:48:33.127+09:00  INFO 11684 --- [MessageBroker-2] 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 = 4, active threads = 1, queued tasks = 2, completed tasks = 1]

위의 이벤트 응모 요청 시 도출되는 SQL을 확인해보면 Seat 테이블이 조회되는 것을 확인할 수 있다. 이벤트 응모할 때 해당 좌석에 대한 정보가 필요없는데 왜 조회하는걸까?

 

현재 Event와 연관관계가 설정되어있는 Seat에 Lazy설정이 아닌 디폴트 설정이 되어있었다.

 

만약 Event에 응모되면 Seat정보가 필요하기때문에, FetchType Lazy로 명시하여 필요할 때만 조회하도록 하자.

요청을 한 번 다시 보내보았다. Seat가 조회되지않는 것을 확인할 수 있었다.

 

025-08-29T17:39:32.052+09:00  INFO 12686 --- [nio-8081-exec-2] c.p.t.global.security.filter.JwtFilter   : Token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyNkBleGFtcGxlLmNvbSIsImF1dGhvcml0aWVzIjpbIk1FTUJFUiJdLCJ1c2VySWQiOjYsIm5pY2tuYW1lIjoi7Jyg7KCANiIsImlzcyI6InRpY2tsZS1hcGkiLCJpYXQiOjE3NTY0NTY3NzEsImV4cCI6MTc1NjU0MzE3MX0.8lePT4WSW8ziHYPzuZpv2XYeLLjYrIkTK7DxiNLfkko84TfQhkhhiLetmsjiIuaBqLtKvbqiw3jBBVdrujWUmA
2025-08-29T17:39:32.062+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_email=?
2025-08-29T17:39:32.080+09:00  INFO 12686 --- [nio-8081-exec-2] c.p.t.global.security.filter.JwtFilter   : Authentication: UsernamePasswordAuthenticationToken [Principal=com.profect.tickle.global.security.util.principal.CustomUserDetails@17bb47b8, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[MEMBER]]
2025-08-29T17:39:32.104+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        e1_0.event_id,
        e1_0.event_accrued,
        e1_0.coupon_id,
        e1_0.event_created_at,
        e1_0.event_goal_price,
        e1_0.event_name,
        e1_0.event_per_price,
        e1_0.status_id,
        e1_0.event_type,
        e1_0.event_updated_at 
    from
        event e1_0 
    where
        e1_0.event_id=? for no key update
2025-08-29T17:39:32.125+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_id=?
2025-08-29T17:39:32.138+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        p1_0.member_id,
        p1_0.point_id,
        p1_0.point_created_at,
        p1_0.point_credit,
        p1_0.point_order_id,
        p1_0.point_target 
    from
        point p1_0 
    where
        p1_0.member_id=?
2025-08-29T17:39:32.160+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    insert 
    into
        point
        (point_created_at, point_credit, member_id, point_order_id, point_target) 
    values
        (?, ?, ?, ?, ?)
2025-08-29T17:39:32.186+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    update
        event 
    set
        event_accrued=?,
        coupon_id=?,
        event_created_at=?,
        event_goal_price=?,
        event_name=?,
        event_per_price=?,
        status_id=?,
        event_type=?,
        event_updated_at=? 
    where
        event_id=?
2025-08-29T17:39:32.196+09:00 DEBUG 12686 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    update
        member 
    set
        member_birthday=?,
        member_deleted_at=?,
        member_email=?,
        host_biz_address=?,
        host_biz_bank=?,
        host_biz_bank_number=?,
        host_biz_ceo=?,
        host_biz_depositor=?,
        host_biz_ecommerce_registration_number=?,
        host_biz_name=?,
        host_biz_number=?,
        member_img=?,
        member_role=?,
        member_nickname=?,
        member_pw=?,
        member_number=?,
        member_point_balance=?,
        member_updated_at=? 
    where
        member_id=?
2025-08-29T17:40:05.209+09:00  INFO 12686 --- [MessageBroker-2] 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 = 4, active threads = 1, queued tasks = 2, completed tasks = 1]

 

해결과정2: 멤버의 포인트를 차감하는 SQL문을 줄여보자

UPDATE문 한 번만 날리기

현재 멤버의 포인트를 차감하기 위해서 멤버를 조회하고(SELECT) + 포인트를 차감하는(UPDATE)로직으로 작성되어있다.

SELECT + UPDATE문을 한 번에 UPDATE하도록 변경해보자.

서비스 로직에서 deductPoint 메서드로 따로 포인트를 감소시키는 기능을 분리하였다.

 

수정 전 Service

 

 

수정 전 Member

 

수정 후 Service

 

 

수정 후 Repository

 

2025-08-29T20:06:00.916+09:00  INFO 15204 --- [nio-8081-exec-2] c.p.t.global.security.filter.JwtFilter   : Authentication: UsernamePasswordAuthenticationToken [Principal=com.profect.tickle.global.security.util.principal.CustomUserDetails@4f9b4b44, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[MEMBER]]
2025-08-29T20:06:00.955+09:00 DEBUG 15204 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        e1_0.event_id,
        e1_0.event_accrued,
        e1_0.coupon_id,
        e1_0.event_created_at,
        e1_0.event_goal_price,
        e1_0.event_name,
        e1_0.event_per_price,
        e1_0.status_id,
        e1_0.event_type,
        e1_0.event_updated_at 
    from
        event e1_0 
    where
        e1_0.event_id=? for no key update
2025-08-29T20:06:01.005+09:00 DEBUG 15204 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    UPDATE
        member 
    SET
        member_point_balance = member_point_balance - ?,
        member_updated_at = NOW() 
    WHERE
        member_id = ?   
        AND member_point_balance >= ? 
2025-08-29T20:06:01.022+09:00 DEBUG 15204 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_id=?
2025-08-29T20:06:01.035+09:00 DEBUG 15204 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    select
        p1_0.member_id,
        p1_0.point_id,
        p1_0.point_created_at,
        p1_0.point_credit,
        p1_0.point_order_id,
        p1_0.point_target 
    from
        point p1_0 
    where
        p1_0.member_id=?
2025-08-29T20:06:01.126+09:00 DEBUG 15204 --- [nio-8081-exec-2] org.hibernate.SQL                        : 
    insert 
    into
        point
        (point_created_at, point_credit, member_id, point_order_id, point_target) 
    values222222
        (?, ?, ?, ?, ?)

 

memberSelect문 줄이기

현재 한 번 이벤트에 응모할 시 실행되는 SQL문이다.

원래 우리의 로직은 → → 이므로, 이렇게 되어야하는데 실제로 실행되는 SQL문은

→ → 이다. member select가 너무 잦으니 줄여보자.

    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_email=?
2025-09-02T12:56:00.774+09:00  INFO 31485 --- [nio-8081-exec-1] c.p.t.global.security.filter.JwtFilter   : Authentication: UsernamePasswordAuthenticationToken [Principal=com.profect.tickle.global.security.util.principal.CustomUserDetails@20439a0f, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[MEMBER]]
2025-09-02T12:56:00.801+09:00 DEBUG 31485 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    select
        e1_0.event_id,
        e1_0.event_accrued,
        e1_0.coupon_id,
        e1_0.event_created_at,
        e1_0.event_goal_price,
        e1_0.event_name,
        e1_0.event_per_price,
        e1_0.status_id,
        e1_0.event_type,
        e1_0.event_updated_at 
    from
        event e1_0 
    where
        e1_0.event_id=? for no key update
2025-09-02T12:56:00.900+09:00 DEBUG 31485 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    update
        member m1_0 
    set
        member_point_balance=(m1_0.member_point_balance-?) 
    where
        m1_0.member_id=? 
        and m1_0.member_point_balance>=?
2025-09-02T12:56:00.992+09:00 DEBUG 31485 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    update
        event 
    set
        event_accrued=?,
        coupon_id=?,
        event_created_at=?,
        event_goal_price=?,
        event_name=?,
        event_per_price=?,
        status_id=?,
        event_type=?,
        event_updated_at=? 
    where
        event_id=?
2025-09-02T12:56:01.193+09:00 DEBUG 31485 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_id=?
2025-09-02T12:56:01.278+09:00 DEBUG 31485 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    select
        p1_0.member_id,
        p1_0.point_id,
        p1_0.point_created_at,
        p1_0.point_credit,
        p1_0.point_order_id,
        p1_0.point_target 
    from
        point p1_0 
    where
        p1_0.member_id=?

 

@Service
@RequiredArgsConstructor
public class PostActionsService {
    private final PointRepository pointRepository;
    private final MemberRepository memberRepository;
    private final SeatRepository seatRepository;
    private final ReservationRepository reservationRepository;
    private final PerformanceRepository performanceRepository;
    private final StatusProvider statusProvider;

    // (락 밖) 포인트 내역 저장
    @Transactional
    public void recordPointHistory(Long memberId, int amount, PointTarget target) {
        pointRepository.save(Point.deduct(memberRepository.getReferenceById(memberId), amount, target));
    }
}

// Member
@Builder
public class Member {

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Point> points = new ArrayList<>();
}
// Point
public class Point {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;
}

Member와 Point가 서로 양방향이기때문에, Point 저장 시, JPA가 반대편 컬렉션인 Member와 일관성을 맞추기 위해서 컬렉션을 초기화하여 member select가 발생하는 것 이였다.(cascade=ALL, orphanRemoval=true가 붙은 @OneToMany)

 

// Member
@Builder
public class Member {

    // @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    // private List<Point> points = new ArrayList<>();
}
pointRepository.findByMemberId(memberId)

마이페이지에서 사용자의 포인트 내역을 조회하기 위해 양방향 관계를 설정해주었는데, 양방향 관계를 단방향 관계로 변경하고 포인트 내역 조회시 멤버의 ID로 포인트를 조회하도록 변경하였다.

 

결과

    select
        m1_0.member_id,
        m1_0.member_birthday,
        m1_0.member_created_at,
        m1_0.member_deleted_at,
        m1_0.member_email,
        m1_0.host_biz_address,
        m1_0.host_biz_bank,
        m1_0.host_biz_bank_number,
        m1_0.host_biz_ceo,
        m1_0.host_biz_depositor,
        m1_0.host_biz_ecommerce_registration_number,
        m1_0.host_biz_name,
        m1_0.host_biz_number,
        m1_0.member_img,
        m1_0.member_role,
        m1_0.member_nickname,
        m1_0.member_pw,
        m1_0.member_number,
        m1_0.member_point_balance,
        m1_0.member_updated_at 
    from
        member m1_0 
    where
        m1_0.member_email=?
2025-09-02T13:49:23.166+09:00  INFO 32791 --- [nio-8081-exec-1] c.p.t.global.security.filter.JwtFilter   : Authentication: UsernamePasswordAuthenticationToken [Principal=com.profect.tickle.global.security.util.principal.CustomUserDetails@73b6b1df, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[MEMBER]]
2025-09-02T13:49:23.207+09:00 DEBUG 32791 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    select
        e1_0.event_id,
        e1_0.event_accrued,
        e1_0.coupon_id,
        e1_0.event_created_at,
        e1_0.event_goal_price,
        e1_0.event_name,
        e1_0.event_per_price,
        e1_0.status_id,
        e1_0.event_type,
        e1_0.event_updated_at 
    from
        event e1_0 
    where
        e1_0.event_id=? for no key update
2025-09-02T13:49:23.410+09:00 DEBUG 32791 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    update
        member m1_0 
    set
        member_point_balance=(m1_0.member_point_balance-?) 
    where
        m1_0.member_id=? 
        and m1_0.member_point_balance>=?
2025-09-02T13:49:23.557+09:00 DEBUG 32791 --- [nio-8081-exec-1] org.hibernate.SQL                        : 
    update
        event 
    set
        event_accrued=?,
        coupon_id=?,
        event_created_at=?,
        event_goal_price=?,
        event_name=?,
        event_per_price=?,
        status_id=?,
        event_type=?,
        event_updated_at=? 
    where
        event_id=?

 

TPS가 소폭 상승된 것을 확인할 수 있다.

 

 


해결방안3. 락 거는 범위를 최소화해보자.

락 범위 최소화

현재의 코드는 아래와 같다.

		@Transactional(propagation = Propagation.REQUIRES_NEW)
    public TicketApplyResponseDto applyTicketEventOnce(Long eventId) {
        final Long memberId = SecurityUtil.getSignInMemberId();
        Event event = getEvent(eventId);

        final short perPrice = event.getPerPrice();

        int updated = memberRepository.tryDeductPoint(memberId, perPrice);
        if (updated == 0) {
            throw new BusinessException(ErrorCode.INSUFFICIENT_POINT);
        }

        Member member = memberRepository.getReferenceById(memberId);
        Point point = Point.deduct(member, perPrice, PointTarget.EVENT);
        pointRepository.save(point);

        event.accumulate(event.getPerPrice());

        boolean isWinner = (event.getAccrued() >= event.getGoalPrice());
        if (isWinner) {
            event.updateStatus(statusProvider.provide(StatusIds.Event.COMPLETED));

            Seat seat = seatRepository.findByEventId(eventId)
                    .orElseThrow(() -> new BusinessException(ErrorCode.SEAT_NOT_FOUND));

            Reservation r = Reservation.create(
                    member,
                    seat.getPerformance(),
                    statusProvider.provide(StatusIds.Reservation.PAID),
                    event.getAccrued()
            );
            r.assignSeat(seat);
            Status reserved = statusProvider.provide(StatusIds.Seat.RESERVED);
            seat.completeReservation(member, reserved, null);

            reservationRepository.save(r);
	        }

위의 코드를 수행하는동안 락에 걸려있게 된다.

지금은 이벤트 응모 → 포인트 감소 → 우승자 확인 → 우승자일 시 티켓 발급 까지 이루어져있는데, 이렇게 락에 걸려있는 범위가 크면 대기시간이 더 늘어나지않을까?

LOCK의 범위가 너무 넓어진 것 같으니, 분리해보자

  1. 이벤트 응모 후 포인트 차감 후 목표금액에 누적. 목표 금액에 도달시 이벤트 상태 종료로 변경
  2. 포인트 내역을 저장
  3. 우승자일 시, 좌석 상태를 변경하고 예매권을 발급

PessimisticEventApplyExecutor

@Service
@RequiredArgsConstructor
public class PessimisticEventApplyExecutor {

    private final SeatRepository seatRepository;
    private final CouponRepository couponRepository;
    private final CouponReceivedRepository couponReceivedRepository;
    private final EventRepository eventRepository;
    private final MemberRepository memberRepository;
    private final StatusProvider statusProvider;
    private final EventCoreLockService core;

    public TicketApplyResponseDto applyTicketEventOnce(Long eventId) {
        Long memberId = SecurityUtil.getSignInMemberId();

        EventDecision dec = core.applyCore(eventId, memberId);

        return TicketApplyResponseDto.from(dec.eventId(), dec.memberId(), dec.winner());
    }
}

EventCoreLockService

이벤트 응모 Service

@Service
@RequiredArgsConstructor
public class EventCoreLockService {
    private final EventRepository eventRepository;
    private final MemberRepository memberRepository;
    private final StatusProvider statusProvider;
    private final ApplicationEventPublisher publisher;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public EventDecision applyCore(Long eventId, Long memberId) {
        Long seatId = null;
        Event event = eventRepository.findForUpdateById(eventId)
                .orElseThrow(() -> new BusinessException(ErrorCode.EVENT_NOT_FOUND));

        if (!StatusIds.Event.IN_PROGRESS.equals(event.getStatus().getId())) {
            throw new BusinessException(ErrorCode.EVENT_NOT_IN_PROGRESS);
        }

        short perPrice = event.getPerPrice();

        int memberPoint = memberRepository.tryDeductPoint(memberId, perPrice);
        if (memberPoint == 0) throw new BusinessException(ErrorCode.INSUFFICIENT_POINT);

        event.accumulate(perPrice);

        boolean winner = event.getAccrued() >= event.getGoalPrice();

        if (winner) {
            event.updateStatus(statusProvider.provide(StatusIds.Event.COMPLETED));
            seatId = event.getSeat().getId();
        }

        publisher.publishEvent(new TicketApplied(
                memberId,
                seatId,
                perPrice,
                event.getAccrued(),
                PointTarget.EVENT,
                winner
        ));

        return new EventDecision(event.getId(), memberId, perPrice, winner, event.getAccrued(), seatId);
    }
}

PostActionsService

이벤트 응모 후, 후속처리 서비스

@Slf4j
@Service
@RequiredArgsConstructor
public class PostActionsService {
    private final PointRepository pointRepository;
    private final MemberRepository memberRepository;
    private final SeatRepository seatRepository;
    private final ReservationRepository reservationRepository;
    private final PerformanceRepository performanceRepository;
    private final StatusProvider statusProvider;
    private final EntityManager entityManager;

    //포인트 내역 저장
    @Transactional
    public void recordPointHistory(Long memberId, int amount, PointTarget target) {
        Member member = memberRepository.getReferenceById(memberId);
        var p = Point.deduct(member, amount, target);
        pointRepository.save(p);
    }
    
    @Transactional
    public void reserveSeatAndCreateReservation(Long seatId, Long memberId, int accrued) {
        final Long RESERVED = statusProvider.provide(StatusIds.Seat.RESERVED).getId();
        final Long AVAILABLE = statusProvider.provide(StatusIds.Seat.AVAILABLE).getId();

        int updated = seatRepository.tryReserveSeat(seatId, memberId, RESERVED, AVAILABLE);
        if (updated == 0) return; // 경합에서 졌으면 조용히 종료

        Long perfId = seatRepository.findPerformanceIdBySeatId(seatId);

        Reservation r = Reservation.create(
                memberRepository.getReferenceById(memberId),
                performanceRepository.getReferenceById(perfId),
                statusProvider.provide(StatusIds.Reservation.PAID),
                accrued
        );
        r.assignSeat(seatRepository.getReferenceById(seatId));
        reservationRepository.save(r);
    }
}

TicketAppliedListener

@Slf4j
@Component
@RequiredArgsConstructor
public class TicketAppliedListener {
    private final PostActionsService post;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(TicketApplied e) {
        log.info("[TicketAppliedListener] start member={}, winner={}, seatId={}, perPrice={}, accrued={}",
                e.memberId(), e.isWinner(), e.seatId(), e.perPrice(), e.accrued());
        try {
            post.recordPointHistory(e.memberId(), e.perPrice(), e.target());
            if (e.isWinner() && e.seatId() != null) {
                post.reserveSeatAndCreateReservation(e.seatId(), e.memberId(), e.accrued());
            }
            log.info("[TicketAppliedListener] done member={}", e.memberId());
        } catch (Exception ex) {
            log.error("[TicketAppliedListener] failed member={} reason={}", e.memberId(), ex.toString(), ex);

        }
    }
}

 

결과

TPS가 85로 높아졌다.

 


마무리하며

현재는 비관적 락을 적용하여 한 명이 접근했을 때 락을 걸어서 다른 사용자들은 대기하도록 하였다.

하지만 만약에 100만 명의 요청이 온다면 최소 100만 번의 데이터베이스 콜과 업데이트가 이루어져야하는데 이것이 과연 효율적일까를 생각하게 되었다. 또, 비관적 락을 적용하였을 때, 커넥션 풀 사이즈가 30일때 락을 흭득한 한 개의 요청만 처리하고, 29개의 요청은 대기를 하게 되는데 이게 과연 좋은 설계라고 할 수 있을까를 고민하게되었다.

이후에 다른 추가적은 기술을 도입하여 성능을 개선해볼 예정이다.