Skip to content

Conversation

@strangehoon
Copy link

@strangehoon strangehoon commented Apr 9, 2025

[4주차] 비관적 락, 낙관적 락, 분산락 구현

PR 제목은 변경 사항의 요약을 담아야 합니다.
예시: "[1주차] 사용자 로그인 기능 구현"

📝작업 내용

지난주 3주차에서 마무리하지 못했던 부분을 보완했습니다.

  1. 상영중인 영화 조회 api에 페이징 적용, 장르별 1page에 캐싱 적용
  2. common 모듈 추가
  3. reservation 테이블에 미리 데이터를 넣어두는 방식으로 변경하여 단계별 락 구현

🔒해결하려는 문제 혹은 고민이 되었던 부분을 남겨주세요.(문제 수 만큼 복사해서 사용할 것)

지난주에는 INSERT 쿼리에 대한 동시성 제어 방식을 고려했지만, 그보다는 미리 screening 테이블에 좌석 데이터를 생성해두고, isReserved 컬럼을 통해 예약 여부를 판단하는 방식이 더 적절하다고 판단했습니다.
screeningId = 1일 때 userA가 seatId={1,2,3}을, userB가 seatId={3,4,5}를 동시에 요청하는 상황에서는 단순히 seatId에만 락을 걸 경우 충돌을 제대로 제어할 수 없습니다.
따라서, screeningId와 seatIds 조합에 대해 분산락을 걸 수 있도록 Redisson의 Multi Lock을 활용해 동시성을 제어했습니다.

기타 사항 📌

  • 기능이 추가될 때마다 해당 기능이 어떤 모듈에 위치하는 것이 적절할지 판단하기 애매한 부분들이 있었습니다. 코치님께서 보시기에 구조상 어색한 부분이 있다면 한 번 확인해주시면 감사하겠습니다.

    • 현재 도메인 모듈의 common 패키지내에 AOP 관련된 클래스들, 그리고 LockTemplate 클래스를 두었는데 위치가 적절한지 잘 모르겠습니다. AOP 관련된 설정, 분산락은 경우에 따라서 api 모듈쪽에서도 활용될 수 있을거 같아 common 모듈에 두는 것도 고려중입니다. 코치님께서는 어떻게 생각하시는지 궁금합니다.
    • Layered Architecture 기반 멀티모듈 구조도 실무에서 자주 사용하는지 궁금합니다.
  • 부하 테스트 결과, AOP 방식의 평균 응답 시간은 약 31.25ms, 함수형 방식은 약 33ms로 두 방식 간 큰 성능 차이는 나타나지 않았습니다. (VUs: 30, Duration: 30분)

    • 저는 AOP 방식에서 별도의 메서드를 활용해 임계영역을 좁혔기 때문에 함수형 방식이랑 차이가 별로 없었다고 생각합니다. 하지만 부하가 많았을 때는 프록시 기술을 사용하는 AOP 방식이 함수형 방식보다는 미세하게 성능상 이점이 있을거라 생각하고 있습니다. 코치님께서는 어떻게 생각하시는지 궁금합니다.
    • AOP 방식을 사용하더라도 임계영역을 별도의 메서드를 분리하면 함수형 방식이랑 큰 차이가 없을거라 생각하는데, 현업에서 AOP 방식과 함수형 방식을 선택하는 기준이 궁금합니다.
  • lease time, wait time 설정 기준

    • wait time : 만약 서비스에서 사용자의 요청-응답 시간이 최대 3초를 넘기지 않았으면 한다면, 2초 정도로 설정하는 것이 좋아보입니다.
    • lease time : 만약 작업의 평균 응답 시간이 500ms라면, 여유를 둬서 1.5초 정도로 설정하는 것이 좋아보입니다. 추가로 2차적으로 낙관적락도 활용해서 데이터 정합성을 유지하는것이 좋아보입니다.

@ann-mj
Copy link

ann-mj commented Apr 22, 2025

@strangehoon 안녕하세요. 리뷰 시작하겠습니다.

@ann-mj
Copy link

ann-mj commented Apr 22, 2025

현재 도메인 모듈의 common 패키지내에 AOP 관련된 클래스들, 그리고 LockTemplate 클래스를 두었는데 위치가 적절한지 잘 모르겠습니다. AOP 관련된 설정, 분산락은 경우에 따라서 api 모듈쪽에서도 활용될 수 있을거 같아 common 모듈에 두는 것도 고려중입니다. 코치님께서는 어떻게 생각하시는지 궁금합니다.

  • 도메인 모듈 내에는 가급적 설정과 관련된 파일들은 포함시키지 않는 게 좋습니다.
  • common 모듈에 두면 좋을 듯 합니다.

MovieController.class,
ReservationController.class
})
public abstract class IntegrationControllerSupport {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추상 클래스를 통해 관리하신 부분 좋습니다 👍

public class ReservationResponse {
private final Long userId;
private final Long screeningId;
private final List<Long> reservedSeatIds;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필수 정보들 모두 포함시켜 주셨네요 좋습니다 💯


// 1. 해당 상영 시간표에서 이미 예약한 좌석 수 확인
int alreadyReservedCount = reservationRepository.countByUserIdAndScreeningId(request.getMemberId(), request.getScreeningId());
int alreadyReservedCount = reservationRepository.countByUserIdAndScreeningId(request.getUserId(), request.getScreeningId());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래의 예외들의 경우, 무조건 IllegalArgumentException 이라기 보다는
비즈니스 정책 상의 예외라면, 무조건 400으로 떨어지는 예외 외에도 명확한 의미를 전달해줄 수 있는 InvalidFormatException 등의 이름을 활용해봐도 좋을 듯 합니다.

@Aspect
@Component
@RequiredArgsConstructor
public class DistributedMultiLockAspect {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요런 건 application 계층을 하나 만들어둬도 좋을 것 같아요.
domain 에 너무 많은 책임이 들어가있는 느낌이 듭니다.

public void handle(ReservationCompletedEvent event) {
String message = "회원 %s님, 좌석 %d개 예약이 완료되었습니다."
.formatted(event.getUserName(), event.getSeatCount());
messageService.send(message);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비동기로 메일 보내는 로직까지 구현해주셨네요 좋습니다 :)

reservationValidator.validate(request);

User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다."));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserNotFound 라는 예외를 던져도 좋겠네요 :)


@Getter
@Builder
public class ReservationCompletedEvent {
Copy link

@ann-mj ann-mj Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request 패키지 내에 두신 이유가 있을까요?
event 라는 패키지를 만들어도 될거같네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 event 패키지 만드는게 좋아보이네요 ㅎㅎ


@Configuration
@EnableAsync
public class AsyncConfig {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디폴트 설정만 사용하기 보다는, 실제로 어떤 설정이 되는지 고민해보시고, 커스텀도 해보시면 좋습니다.
쓰레드 개수 설정도 해보시고요 :)

@ann-mj
Copy link

ann-mj commented Apr 22, 2025

고생하셨습니다 👍

좋았던 점

  • 예약 api 를 검증, 구현까지 꼼꼼하게 해주셨습니다.
  • 함수형 분산락 구현이 깔끔했습니다!
  • waitTime, leaseTime 을 잘 설정해주셨습니다.
  • spring 에서 이벤트 퍼블리셔를 사용해서 이벤트 관리를 한 점이 인상적이었습니다.

아쉬웠던 점

  • domain 모듈에 많은 책임이 들어있어서, 다른 모듈(application or api 등) 로 분리해서 관리해도 좋을 것 같습니다.

질문에 대한 답변

  • AOP 방식을 사용하더라도 임계영역을 별도의 메서드를 분리하면 함수형 방식이랑 큰 차이가 없을거라 생각하는데, 현업에서 AOP 방식과 함수형 방식을 선택하는 기준이 궁금합니다.
    • 함수형 방식이 간단해서 주로 사용합니다. AOP 로 사용한 코드는 거의 못봤네요!
  • 저는 AOP 방식에서 별도의 메서드를 활용해 임계영역을 좁혔기 때문에 함수형 방식이랑 차이가 별로 없었다고 생각합니다. 하지만 부하가 많았을 때는 프록시 기술을 사용하는 AOP 방식이 함수형 방식보다는 미세하게 성능상 이점이 있을거라 생각하고 있습니다. 코치님께서는 어떻게 생각하시는지 궁금합니다.
    • 성능상 차이는 없는 것 같습니다. key-value 형태로 키를 저장하는 방식인 함수형이 좀 더 가볍지 않을까 생각됩니다!
  • Layered Architecture 기반 멀티모듈 구조도 실무에서 자주 사용하는지 궁금합니다.
    • 주로 Layered Architecture 나 헥사고날을 많이 사용하게 되는 것 같습니다.

@strangehoon
Copy link
Author

리뷰 감사합니다!! 관련해서 리팩토링 해보겠습니다. 고생하셨습니다!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants