Skip to content

[3주차] 예약API, 비관 락, 낙관 락, 분산 락 구현 및 테스트#84

Merged
pyuseon merged 31 commits intohanghae-skillup:orkrj2from
orkrj:3rdweek
Feb 4, 2025
Merged

[3주차] 예약API, 비관 락, 낙관 락, 분산 락 구현 및 테스트#84
pyuseon merged 31 commits intohanghae-skillup:orkrj2from
orkrj:3rdweek

Conversation

@orkrj
Copy link

@orkrj orkrj commented Jan 31, 2025

3주차 과제

개인 사정으로 늦게 제출한 점 다시 한 번 죄송합니다.

프로젝트를 재설계한 부분은 커밋 볼륨을 크게 가져갔고,

3주차 과제 커밋은 이모지를 활용하여 강조하였습니다.


작업 내용

  1. 2주차까지 작업한 프로젝트를 버리고, 새롭게 설계하여 작업했습니다. 주요 이유는 다음과 같습니다.
  • VO 및 일급 컬렉션을 활용해 책임을 최대한 분리했지만, 직렬화/역직렬화 및 DTO 매핑 비용이 예상보다 크게 발생했습니다.
  • 도메인과 엔티티를 분리했지만, 이번 프로젝트에선 순수한 도메인 로직을 분리하여 얻을 수 있는 이점을 체감하지 못했습니다.
  • 객체 변환 과정에서 여러 객체에 전파되는 영향이 커져, 오히려 결합도가 높아지는 문제가 발생했습니다.

  1. 다음과 같이 응답하는 예약 API 를 구현하였습니다.
  • 200
  • 하나의 상영에 대해 한 사람당 최대 5개의 좌석만 예매 가능합니다.
  • 하나의 상영에 대해 최대 2개까지 예약할 수 있습니다.
  • 하나의 상영에 대해 5개의 표를 예매하는 경우 좌석은 붙어있어야 합니다.
  • 이미 예매된 좌석입니다.

  1. 비관적 락(Pessimistic Lock) 적용 및 테스트
  • 10개의 스레드로 동시 요청을 보내 DB 레벨의 락이 정상적으로 동작하는지 테스트했습니다.
  • 1개의 요청이 성공되고, 9개의 요청이 예외 처리되었습니다.

  1. 낙관적 락(Optimistic Lock) 적용 및 테스트
  • 10개의 스레드로 동시 요청을 보내 version 을 활용한 롤백이 정상적으로 동작하는지 테스트했습니다.
  • 1개의 요청이 성공되고, 9개의 요청이 롤백되었습니다.

  1. 분산 락(Distributed Lock) & 낙관적 락 적용 및 테스트
  • 10개의 스레드로 동시 요청을 보내 Redis를 이용한 분산 락이 동작하는지 검증했습니다.
  • 1개의 요청이 성공되고, 1개의 요청이 롤백, 8개의 요청이 거절되었습니다.

  1. 분산 락 + 낙관적 락 부하 테스트
  • 부하 테스트 결과 응답에 평균 63ms, 최대 515ms 가 소요되어 leaseTime 을 최대 응답의 약 4배(2s)로 설정했습니다.
  • 영화 예매는 대체 선택지(좌석 및 상영 시간)가 많기에 락 대기 시간을 짧게 줌으로써 쾌적한 사용을 고려해 1s로 설정했습니다.

이번 주차에서 고민되었던 지점이나, 어려웠던 점을 알려 주세요.

  • 락을 처음 적용해 보면서 다양한 동작 방식이 있다는 것을 알게 되었지만, 어떤 용도로 사용해야 할지 체감하는 과정이 어려웠습니다.
  • 특히, 락의 동작 원리 차이를 이해하고, 각각의 방식이 어떤 상황에서 적절한지 판단하는 과정에서 시간이 많이 소요되었습니다.
  • 설계 과정이 정말 어려웠습니다. 최대한 깔끔하게 구조를 잡으려 했지만, 중복이 발생하는 부분을 완전히 제거하기가 쉽지 않았습니다.

기타 질문

  • 멀티 스레드 환경에서 @transactional을 사용하면 단일 스레드에서만 롤백이 적용되기 때문에 테스트 데이터가 정상적으로 롤백되지 않았습니다. 그래서 @AfterEach 같은 후처리를 적용했지만, 이로 인해 순수한 테스트 결과(응답 시간 등)를 확인하기 어려웠습니다. 실무에서는 동시성 테스트를 어떻게 진행하는지 궁금합니다.

orkrj added 30 commits January 27, 2025 04:41
private LocalDateTime updatedAt;

private LocalDateTime deletedAt;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

system properties 에 작성자, 수정자도 추가하면 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

넵 추가하겠습니다!

@RequestParam(required = false) Genre genre
) {

return ResponseEntity.ok(movieService.findMoviesPlayingWithFilters(title, genre));
Copy link
Contributor

Choose a reason for hiding this comment

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

불필요한 줄바꿈 혹은 괄호 등의 기본적인 컨벤션을 조금만 더 신경 써주시면 좋을 것 같아요 !

Copy link
Author

@orkrj orkrj Feb 1, 2025

Choose a reason for hiding this comment

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

기본적으로 파라미터 구간은 닫힌 괄호를 기준으로 블록을 쉽게 구별할 수 있도록 줄 바꿈을 하고,
블록 내부 줄바꿈은 의미 단위로 구분하기 위해 사용하는데,
두 원칙 외에 무의식적으로 줄바꿈 해버리는 부분들이 있네요ㅠㅠ
이부분 조심하겠습니다!

}
}

private void checkReservationPolicy(Reservation reservation, List<ReservationSeat> reservationSeats, int limit) {
Copy link
Contributor

Choose a reason for hiding this comment

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

전체적으로 함수들이 SRP 가 지켜진 모습이네요 👍🏿

}

private boolean isAbusing(List<ReservationSeat> reservationSeats, int limit) {
if (reservationSeats.size() == limit) {
Copy link
Contributor

Choose a reason for hiding this comment

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

이런 함수들은 Service 에 있어야 하는건지 혹은 Domain 에 있어야 하는 건지
고민을 해주시면 좋을 것 같습니다 ~

Copy link
Author

@orkrj orkrj Feb 1, 2025

Choose a reason for hiding this comment

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

앗 그러네요! 도메인에 두는 게 훨씬 더 직관적일 것 같아요. 리팩토링하겠습니다!

checkDoubleBooking(scheduleSeats);

List<ReservationSeat> reservationSeats = toReservationSeats(reservation, seats);
checkReservationPolicy(reservation, reservationSeats, 5);
Copy link
Contributor

Choose a reason for hiding this comment

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

매직넘버(5) 는 상수로 관리하면 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

넵 놓쳤던 부분이었던 것 같습니다. 리팩토링하겠습니다!

@DistributedLock(
key = "#{#request.scheduleId}",
waitTime = 1, // second
leaseTime = 2 // second
Copy link
Contributor

Choose a reason for hiding this comment

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

변수명을 waitSec 등으로 설정하면 주석이 필요 없겠네요 !

Copy link
Author

Choose a reason for hiding this comment

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

아..!! 그러네요 미처 생각 못했네요

return point.proceed();
} finally {
lock.unlock();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

finally 구문에서 lock.unlock 은 현재 스레드가 자신의 락만을 해제함을 보장할 수 있나요 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

추가로 GC Stop the world 현상이 길어지는 경우 tryLock 만으로 동시성 이슈를 해결하기 충분한지 deep dive 해보시는 것도 좋을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

finally 구문에서 lock.unlock 은 현재 스레드가 자신의 락만을 해제함을 보장할 수 있나요 ?

아 내부적으로 자신의 스레드인지 체크해서 본인이 획득한 락만 해제해야 할 것 같습니다.
놓쳤던 부분인 것 같습니다. 수정하겠습니다.

Copy link
Author

Choose a reason for hiding this comment

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

추가로 GC Stop the world 현상이 길어지는 경우 tryLock 만으로 동시성 이슈를 해결하기 충분한지 deep dive 해보시는 것도 좋을 것 같습니다.

이 부분이 어렵네요. leaseTime 을 늘리려고 하니 적정선을 모르겠고,
구글링을 하니 자동 연장 기능이 있다는데, 한 번 활용해보겠습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

@orkrj 👍🏿 fencing token 이라는 개념으로 찾아보시는 것도 좋을 것 같습니다 ~

Copy link
Author

Choose a reason for hiding this comment

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

@orkrj 👍🏿 fencing token 이라는 개념으로 찾아보시는 것도 좋을 것 같습니다 ~

오오 분산 락에 토큰을 또 적용할 수 있네요. 정합성 유지하는데 많이 도움이 될 것 같아요.
흥미로운 주제라 계속 찾아보고 있습니다 감사합니다!


@Repository
@RequiredArgsConstructor
public class JpaScheduleSeatRepositoryAdapter implements ScheduleSeatRepository {
Copy link
Contributor

Choose a reason for hiding this comment

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

port & adapter 패턴을 잘 적용해주셨네요 👍🏿

private final MemberService memberService;
private final ScheduleService scheduleService;
private final SeatService seatService;
private final ScheduleSeatService scheduleSeatService;
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 Service 는 도메인 서비스가 아닌 애플리케이션 서비스로서, 다양한 도메인 로직을 오케스트레이션 하는 역할 인것 같아요. 하지만 실상 Service 내부의 로직에는 도메인 로직이 포함되어있네요.

이 경우 지금처럼 애플리케이션 Service 들이 여러 애플리케이션 Service 를 의존하게 되는 경우,
서비스 규모가 커질 수록 Service Dependency Graph 가 추적하기 되게 힘들 정도로 복잡해집니다.
경우에 따라 개발자들이 나중에는 의존성에 대해서 신경 쓰는 것을 포기하는 수준까지 올 수 있습니다.

이런 문제를 해결하기 위해서 애플리케이션 Service 들이 다른 애플리케이션 Service 를 의존하지 않도록� 하는 방법도 실무에서 자주 사용됩니다.

Copy link
Author

Choose a reason for hiding this comment

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

기존에 ‘하나의 서비스에 하나의 레포지토리’라는 원칙을 고수했는데,
생각해보니 이를 지키려다 보니 포트-어댑터 패턴을 충분히 활용하지 못했던 것 같습니다.
다른 애플리케이션 서비스가 아닌, 레포지토리를 직접 활용하여 도메인 객체를 조회하고 조작하는 방식으로
오케스트레이션하도록 리팩토링을 진행하겠습니다.

DDD를 적용하는 게 생각보다 어렵네요ㅜㅜ

Copy link
Contributor

Choose a reason for hiding this comment

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

넵 도메인로직과 애플리케이션 로직을 명확히 분리하고, 이 과정에서 도메인서비스가 별도로 필요하진 않은지
필요 없다면 말씀 주신 것 처럼, 애플리케이션 서비스가 다른 애플리케이션 서비스를 참조하지 않게 Repository 를 호출해서 로직을 작성하는 방식으로 개선해도 좋을 것 같습니다 !

Copy link
Author

Choose a reason for hiding this comment

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

넵 도메인로직과 애플리케이션 로직을 명확히 분리하고, 이 과정에서 도메인서비스가 별도로 필요하진 않은지 필요 없다면 말씀 주신 것 처럼, 애플리케이션 서비스가 다른 애플리케이션 서비스를 참조하지 않게 Repository 를 호출해서 로직을 작성하는 방식으로 개선해도 좋을 것 같습니다 !

넵 조언주신 방향으로 리팩토링 완료하였습니다!
설계.. 익숙해지면 뚝딱 뚝딱 해낼 수 있겠죠..?

@BAEKJungHo
Copy link
Contributor

BAEKJungHo commented Feb 1, 2025

@orkrj 님, 구조 및 코드 전반적으로 많이 신경써주신 모습이 보입니다 👍🏿 고생하셨습니다 ~

좋았던 점

  • 코드가 전반적으로 깔끔하고, 구조에 대한 고민을 많이 하신 흔적이 느껴집니다. 많은 인사이트를 얻어가셨을 것 같습니다.
  • 로직이 전반적으로 깔끔합니다. SRP 를 신경 써주신 모습이 인상깊습니다.
  • 분산락을 걸때 waitTime, leaseTime 에 대한 근거를 기반으로 잘 설정해주셨습니다. 테스트도 진행해주셔서 좋았습니다.

아쉬웠던 점

  • 함수형 기반으로 분산락도 진행해보시면 좋을 것 같습니다.
  • 애플리케이션 로직과 도메인 로직의 차이, 역할에 대한 고민을 추가로 하시면 더 좋은 코드가 나올 것 같습니다.
  • 예약 후 메시지 발송 로직이 빠져있습니다.

리뷰 포인트 및 추가 질문에 대한 답변

멀티 스레드 환경에서 @transactional을 사용하면 단일 스레드에서만 롤백이 적용되기 때문에 테스트 데이터가 정상적으로 롤백되지 않았습니다. 그래서 @AfterEach 같은 후처리를 적용했지만, 이로 인해 순수한 테스트 결과(응답 시간 등)를 확인하기 어려웠습니다. 실무에서는 동시성 테스트를 어떻게 진행하는지 궁금합니다.

일반적으로 동시성 테스트라는 것을 별도의 시나리오로 빼서 진행하진 않습니다. 개발자들이 직접 검증을 하는 정도가 대부분입니다.
이 경우는 보통 성능 측정 보다는 로직이 동시성 이슈를 막을 수 있도록 작성되어있는지를 검증합니다.
예를 들면, OptimisticLock 에 걸린경우 예외 처리를 제대로 하는지 등을 검증하면서, assert 에서는 (예약 성공 카운트가) 기대값에 일치하는지 등을 검증합니다.

@orkrj orkrj closed this Feb 1, 2025
@orkrj orkrj deleted the 3rdweek branch February 1, 2025 08:44
@orkrj orkrj restored the 3rdweek branch February 1, 2025 08:44
@orkrj orkrj reopened this Feb 1, 2025
@pyuseon pyuseon merged commit de924af into hanghae-skillup:orkrj2 Feb 4, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants