회고

콘서트 예약 프로젝트 회고

반응형

콘서트 예약 프로젝트를 진행하며 코딩을 하기 전에 시퀀스 다이어그램, ERD, API 명세를 만들면서 이걸 내가 할 수 있을까 라는 생각이 가장 먼저 들었지만 도메인을 나눠서 그 도메인에 맞는 책임과 로직을 구현하다 보니 하나씩 완성할 수 있었다.

 

오늘은 고민을 많이 한 2가지 케이스에 대한 내용을 회고하려고 한다.

- 대기열 구현

- 포인트 충전 동시성 문제

 

대기열 구현

대기열 로직을 구현하고 동시성 테스트 케이스를 2가지 작성을 하였다. (테스트 환경 : 대기열이 0명인 경우)

1. 한명의 사용자가 대기열 신청을 동시에 여러 번 하는 경우 한 번만 신청이 완료되어야 한다.

2. 100명의 사용자가 동시에 신청하는 경우 ONGOING 상태를 가진 사용자가 QUEUE_LIMIT(대기열 제한수)와 같아야 한다.

 

앞전에 작성한 내용으로 AtomicLong을 사용해서 대기열 카운트를 제어해서 2번의 동시성 케이스를 통과를 하였지만 AtomicLong을 사용해서 처리한 경우에 분산환경에서는 의미가 없는 상태라는 피드백을 받아서 다시 수정을 하기로 했다.

 

2024.04.10 - [프로그래밍 언어/Java] - [Java] AtomicLong으로 동시성 문제 해결 (feat. 비관적 락을 사용하지 못한 이유)

 

[Java] AtomicLong으로 동시성 문제 해결 (feat.비관적 락을 사용 하지 못한 이유)

콘서트 예매 사이드 프로젝트를 진행하면서 대기열 관련해서 개발을 진행하면서 비관적 락을 사용해서 대기열 동시성을 풀어 나가려고 했습니다. 요구사항 중 멀티 스레드 환경에서 여러 번의

jhost.tistory.com

 

수정한 내역

1. 대기열 테이블에 unique 제약조건 추가(userId_status)

2. 기존 대기열 상태중 WAIT, ONGOING, DONE 상태에서 DONE 상태를 삭제

-> 대기열 만료시 상태를 DONE으로 update 하는 과정에서 unique 제약조건 위배 되기 때문에, 만료된 상태를 관리하는 별도의 필드를 추가 isExpired (true, false)하여 관리

 

수정한 후 동시성 테스트 케이스 실행 결과 이번엔 반대로 1번은 성공하지만 2번은 실패하는 상황..

1. 한명의 사용자가 대기열 신청을 동시에 여러 번 하는 경우 한 번만 신청이 완료되어야 한다. (성공)

-> 1번의 상황은 unique제약 조건으로 인해 insert자체가 되지 않아서 당연히 성공할 거라고 예상했다.

 

2. 100명의 사용자가 동시에 신청하는 경우 ONGOING 상태를 가진 사용자가 QUEUE_LIMIT(대기열 제한수)와 같아야 한다. (실패)

-> 2번 상황이 실패하는 이유는 대기열에 ONGOING 상태인 사용자가 QUEUE_LIMIT보다 적은 경우 ONGOING상태로 진입이 되어야 하기 때문에 요청이 들어오는 경우  SELECT COUNT(*) FORM QUEUE WHERE status = 'ONGOING'  ONGOING 상태의 카운트를 확인하여 비교하여 대기열 추가

 

public void assignQueueStatusByAvailability(Queue queue) {
    //findByStatusWithPessimisticLock -> 비관적 락을 걸어도 동시성 의미 없는 상태
    List<Queue> ongoingStatus = iQueueRepository.findByStatusWithPessimisticLock(WaitingStatus.ONGOING);

    if (ongoingStatus.size() < QUEUE_LIMIT) {
        queue.toOngoing(QUEUE_EXPIRED_TIME);
    } else {
        queue.toWait();
    }
}

 

count() 쿼리의 경우 집계 연산이기 때문에 비관적 락을 걸 수 없기 때문에, 이를 해결해 보기 위해서 위에 코드처럼 SELECT 쿼리에 락을 걸어서 시도를 해봤지만 여전히 실패..

기존 대기열에 있는 ROW들에 대한 LOCK은 사실상 의미가 없기 때문에 영향이 없다. 

 

사실상 syncronized 키워드를 사용하거나, RDB 대신 REDIS를 사용하면 쉽게 해결할 수 있지만 이왕 시작한 거 마무리를 하고 싶은 욕심이 있다.

 

아직 해결한 문제는 아니지만 저 2가지 케이스를 동시에 성공하기 위해서는 2가지 방법이 있을 것 같다.

1. 대기열 관련 테이블을 2개의 테이블로 구성하여 해결하는 방법

wait_table(status - WAIT), ongoing_table(status - ONGOING, DONE)

- 사용자가 대기열 신청을 하는 경우 ongoing 카운트와 상관없이 무조건 WAIT_TABLE에 insert

- 대기열 만료 스케쥴러 (ongoing_table 에서 만료 시간이 지난 사용자는 DONE 상태로 업데이트)

- 대기열 갱신 스케쥴러 (wait_table 에서 진입한 순서대로 QUEUE_LIMIT - ONGOING count() 만큼 ONGOING 상태로 갱신)

 

위 방법으로 해결할 경우 사실상 내가 고민한 동시성 고민을 할 필요가 없다.

 

2. ONGOING count의 컬럼만 존재하는 QUEUE_ONGOING_COUNT 테이블을 추가해서 ONGOING 카운트를 관리하는 방법 

- 카운트 조회시 비관적 락을 잡아서 해결하는 방법

 

나머지 기능 구현을 완료하고 나서 아마도 위 2가지 방법 중 하나로 수정을 해야 할 것 같다.

 

포인트 충전 동시성 문제

point_table -> id, user_id, point로 간단하게 구성되어 있다.

 

문제가 발생하는 상황은 사용자가 포인트를 최초(첫) 충전하는 경우 발생하게 된다.

@Override
public Point findPointByUserId(Long userId) {
    return pointJpaRepository.findByUserId(userId)
            .map(PointConverter::toDomain)
            .orElseGet(() -> Point.builder()
                    .userId(userId)
                    .point(0L)
                    .build());
}

 

포인트 최초 충전시 point_table에 user_id에 대한 row가 없기 때문에 0L으로 도메인을 return 해서 서비스에서 point 객체에 충전할 금액을 더해서 save를 하게 된다.

 

기존에 포인트가 있는 유저의 충전이 다수로 들어올 경우에는 findByUserId 쿼리에서 락을 잡아서 해결하면 되지만, 첫 충전하는 경우에는 이런 상황을 어떤 식으로 풀어야 할지 도저히 모르겠다.

 

위와 같은 상황이 많이 나올 것 같은데 공부를 더 해봐야 될 것 같다..

위 내용 말고도 프로젝트를 진행하면서 아직 풀지 못한 내용이 많은데 하나씩 내용을 정리하는 시간을 가져야 할 것 같다.

 

 

 

반응형

'회고' 카테고리의 다른 글

항해플러스 8주차 회고  (0) 2024.05.11
항해 플러스 5주차 회고  (0) 2024.04.20
항해 플러스 4주차 회고  (0) 2024.04.13
항해 플러스 3주차 회고  (0) 2024.04.08
항해 플러스 2주차 회고  (0) 2024.04.08