Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1단계 - DB 복제와 캐시] 알파카(최휘용) 미션 제출합니다. #71

Open
wants to merge 9 commits into
base: slimsha2dy
Choose a base branch
from

Conversation

slimsha2dy
Copy link
Member

@slimsha2dy slimsha2dy commented Oct 18, 2024

안녕하세요 포케! 리뷰로 만나는 건 처음이네요 ㅎㅎ
잘 부탁드립니다!

복제 지연 해결 방법

일단 제가 사용한 방법은 실패 시 writer DB 재시도입니다.
고려한 방법들은 아래와 같습니다.

  1. 사용자가 수정한 내용을 읽을 때는 writer db에서 읽음
  2. 마지막 갱신 시간 후 1 분 동안은 writer db에서 읽음
  3. 캐시 사용
  4. 실패할 시 Writer로 재시도
  5. Writer에서 읽는다

이 중 아래의 요소들을 고려해서 4번인 재시도를 결정하게 되었습니다.

  • getCoupon의 성능
  • getCoupon의 실패 가능성
  • 효율성
  • 구현의 쉬운 정도

getCoupon의 사용

쿠폰을 조회하는 getCoupon을 사용하는 경우는 언제인가 생각해봤는데요.
크게 createCoupon을 통해 생성한 쿠폰을 확인하는 경우, 쿠폰을 사용자에게 발행하는 경우(MemberCoupon), 사용자가 쿠폰을 사용하는 경우 등을 생각했습니다. 이 중 발행과 쿠폰 사용은 빈번하게 일어난다고 생각합니다. 저희가 실제로 온라인 쇼핑몰을 사용할 때를 생각해 봐도 매 구매마다 쿠폰을 사용할 정도이기 때문에 성능은 최대한 신경 써야 한다고 생각했습니다.
이에 따라 5번을 고려하지 않았습니다. 읽기 요청을 매번 writer에서 할 경우 reader DB 복제로 인한 이득을 볼 수 없습니다. reader DB는 scale out이 쉽기 때문에 요청이 많아질 경우 이에 대한 성능 개선이 비교적 용이하기 때문에 이를 활용하기 위해선 최대한 많은 요청을 reader DB로 처리해야 합니다. 따라서 reader DB를 사용하지 않는 5번은 제외했습니다.

실패 가능성과 효율성

getCoupon을 했는데 실제 값이 없는 경우가 언제 있을지 생각해봤습니다.
그런데 쿠폰을 사용자에게 발행하는 경우나 쿠폰을 사용할 때 없는 쿠폰을 통해 getCoupon을 요청할 일은 없다고 봐도 될 것 같습니다. 즉, getCoupon이 실패하는 경우 중 생각나는 경우는 LMS에서의 시나리오처럼 발급 직후 다시 확인하는 경우입니다. 따라서 저희가 복제 지연을 해결하더라도 이를 사용할 일은 거의 없다고 봐도 될 것 같습니다. 이에 따라 이 복제 지연을 해결하기 위해 추가적인 캐시를 두는 것은 합리적인 방법이 아니라고 생각했습니다. 인메모리 캐시를 두기 위해 추가적인 메모리를 사용해야 하는 것에 비해 실제로 이를 사용할 일은 거의 없기 때문입니다. 또한 분산 서버를 고려할 경우 레디스 같은 캐시를 사용해야 할텐데 이 비용조차도 해결하는 문제를 고려해 보면 너무 지나치다는 생각이 들었습니다. 따라서 3번을 제외했습니다.

구현의 쉬운 정도

1, 2, 4번 모두 합리적인 방법이라고 생각했습니다. 따라서 미션 마감일을 고려해 이 중 가장 구현 난이도가 낮다고 판단되는 4번의 방식을 채택했습니다.


여기까지입니다. 이에 대한 포케의 생각과 해결 방법이 궁금하네요. 잘 부탁드립니다 ㅎㅎ~

Copy link
Member

@fromitive fromitive left a comment

Choose a reason for hiding this comment

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

안녕하세요 알파카

테스트도 꼼꼼히 작성해주셨고 무엇보다 코드가 읽기 쉬워서 리뷰하기 편했습니다 😄

파카가 제시해주신 방법과 코드에 대해 몇 가지 코멘트를 남겼는데요, 확인 부탁드립니다 🙏

파카파카 알파카 화이팅!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 🔥

Comment on lines +63 to +67
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
return jpaTransactionManager;
}
Copy link
Member

Choose a reason for hiding this comment

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

정말 궁금해서 질문드려요~~ 이 설정의 역할은 무엇인가요? 😄

private DiscountMount discountMount;
@Embedded
private MinimumMount minimumMount;
private Category category;
Copy link
Member

Choose a reason for hiding this comment

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

오 enum은 Enumerated 붙여주지 않아도 엔티티에 넣을 수 있군요..!

Comment on lines +44 to +48
private void validateDiscountRate(DiscountMount discountMount, MinimumMount minimumMount) {
float discountRate = (float) discountMount.getDiscountMount() / minimumMount.getMinimumMount() * 100;
int truncatedDiscountRate = (int) discountRate;
if (truncatedDiscountRate < MINIMUM_DISCOUNT_RATE || truncatedDiscountRate > MAXIMUM_DISCOUNT_RATE) {
throw new IllegalArgumentException("쿠폰 할인율은 3% 이상, 20% 이하여야 합니다.");
Copy link
Member

Choose a reason for hiding this comment

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

DiscountMount 내부에서 검증하면 안되나요??

할인율을 검증하는 것 같은데 mount라는 네이밍은 어떤 이유로 결정하셨을까요?

Copy link
Member Author

@slimsha2dy slimsha2dy Oct 20, 2024

Choose a reason for hiding this comment

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

DiscountMount는 할인 금액이고 할인율을 검증하기 위해서는 할인 금액 DiscountMount와 최소 주문 금액 MinimumMount가 둘 다 필요합니다.
따라서 할인 금액인 DiscountMount에서 할인율을 검증하는 건 저는 어색하다고 생각하는데요. 할인율은 할인 금액과 최소 주문 금액을 통해서 계산해야 하기 때문에 이를 둘 다 알고 있는 쿠폰에서 검증하는 것이 좀 더 적절하다고 생각합니다!
혹시 제가 놓친 부분이나 다른 의견 있으신가요??

Copy link
Member

Choose a reason for hiding this comment

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

제가 착각했네요.. 혼란을 줘서 죄송합니다.. discountrate를 별도의 객체로 분리하지 않은 이유가 있으신가요?

Comment on lines +20 to +24
private void validate(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다.");
}
}
Copy link
Member

Choose a reason for hiding this comment

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

아래의 요구사항은 어디서 반영이 되었을까요? 😄

  • 발급 기간
    • 시작일은 종료일보다 이전이어야 한다. 시작일과 종료일이 같다면, 해당 일에만 발급할 수 있는 쿠폰이 된다.
      • 시작일 00:00:00.000000 부터 발급할 수 있다.
      • 종료일 23:59:59.999999 까지 발급할 수 있다.

Comment on lines +27 to +28
return couponRepository.findById(couponId)
.orElse(getCouponFromWriter(couponId));
Copy link
Member

Choose a reason for hiding this comment

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

알파카의 접근 방식은 매우 직관적이고 가독성이 있었서 다른 동료개발자들이 이해하기 편했어요.
저는 아래의 상황 또한 고려를 했었는데요 파카의 의견의 궁금합니다~~

읽기 요청을 매번 writer에서 할 경우 reader DB 복제로 인한 이득을 볼 수 없습니다. reader DB는 scale out이 쉽기 때문에 요청이 많아질 경우 이에 대한 성능 개선이 비교적 용이하기 때문에 이를 활용하기 위해선 최대한 많은 요청을 reader DB로 처리해야 합니다. 따라서 reader DB를 사용하지 않는 5번은 제외했습니다.

복제지연이 지금은 1초로 고정되어 있지만, production 환경에서는 지연이 얼마나 걸리는지 고려해 봐야한다고 생각해요~ 특수하게 2초, 3초가 될 수있고 장애가 나서 수 분으로도 올라갈 도 있겠죠. 따라서 getCoupon 메서드는 지연시간동안 writer DB가 담당하게 될 것 같아요. 알파카가 채택한 방법은 지연시간이 길어질수록 writer DB에 부담이 가는 전략이라고 생각했습니다.

저는 getCoupon메서드는 온전히 reader DB에서 읽어야 한다고 생각하기 때문에 getCouponread-only를 유지하고 getCouponFromWriter의 메서드를 public으로 제공했습니다.

쿠폰을 생성하는 주체는 서비스 이용자가 아닌 관리자임을 가정했고, 토미의 수업에서 예기해주신 것 처럼 관리자 전용 시스템으로 분리되어 제공된다고 생각했습니다.

이용자 대비 관리자 수는 적을 것임을 전제조건을 둘 때 getCouponFromWriter 메서드는 getCoupon보다 호출하는 횟수가 적을 것이다라고 판단했습니다. 따라서 getCoupon 자체로는 복제지연은 해결할 순 없을 것이다라고 판단했어요.

만약 조건이 getCoupon을 무조건 호출했을 때 복제지연을 해결해야한다면 저는 차선책으로 캐시메모리를 써서 해결 할 것 같아요. RDB와 데이터 일관성이 맞지 않을 수 있지만, 메모리에 직접 저장되기 때문에 복제지연 시간보다 빨리 반영 될 것 같습니다 ㅎㅎ writer DB에도 부담이 가지 않구요

Copy link
Member Author

@slimsha2dy slimsha2dy Oct 20, 2024

Choose a reason for hiding this comment

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

도메인의 성격을 한 번 생각해 봤을 때, 위에서 말씀드린 것처럼 실제로 지연으로 인한 writer DB 읽기는 거의 일어나지 않을 것 같다는 점에서 실제로 writer DB에는 부담이 가지 않을 것으로 예상됩니다.
getCouponFromWriter 메서드의 경우에도 매번 writer DB를 참조할텐데 어쩌면 writer DB에 대한 부담이 될 수도 있을 것 같습니다. 하지만 사실 관리자가 저 쿠폰을 확인할 일은 거의 없을 테니 큰 부담이 되지 않을 것이라고 예측할 수 있겠죠? 마찬가지로 쿠폰을 생성하고 1초만에 getCoupon을 사용할 일도 거의 없을 것 같습니다. 아마 기껏해야 관리자가 쿠폰이 잘 생성됐는지 확인할 때 정도라고 예측할 수 있습니다. 혹시나 사용자가 그 사이에 접근하더라도 그건 아주 드물기 때문에 writer DB의 부담 증가 폭은 아주 적을 것 같습니다. 또한 관리자가 1초 내로 확인하지 않는 한 writer DB는 아예 접근하지 않을 것입니다.

지연시간이 길어지는 건 사실 크게 신경 쓰지 않았습니다. 2~3초 정도로 늘어나더라도 위에서 설명했던 것처럼 큰 문제가 되지 않고, 무엇보다 실제로 장애가 발생해서 복제 지연에 문제가 생기는 것은 복제 지연에 대한 해결책으로 해결할 수 있는 것이 아니니까요.
또한 만약 getCouponFromWriter로 메서드를 분리하더라도 장애가 발생해 복제 지연이 수 분으로 늘어나게 되면 아예 사용자 측에서는 쿠폰 사용 자체가 안되니까 오히려 복제 DB의 장애가 서비스 장애로 전파되는 더 큰 문제가 생길 것 같습니다. 그런 점에서 생각해 보면 복제 DB의 장애가 writer DB의 부담 증가에서만 끝날 수 있다면 꽤나 괜찮은 비용이라는 생각이 드네요.
이 관점에 대해서 포케는 어떻게 생각하시나요?

+포케의 리뷰어는 아니지만 포케의 방식도 읽어보고 고민을 한 번 해봤습니다 ㅎㅎ. 제 의견이 궁금하시면 이 아래 부분도 읽어보시면 될 것 같습니다. 포케의 방식은 쿠폰 DB의 복제 지연을 관리자의 쿠폰 조회라는 문제로 정의하고 해결한 것으로 보이는데요. 그렇다면 정말 만약에 복제 지연 시간 사이에 사용자가 접근하려고 하는 경우가 있을 수 있다면 이에 대한 복제 지연은 해결할 수 없을 것으로 예상됩니다. 포케의 말씀처럼 복제 지연 시간이 실제 프로덕션 환경에서 늘어날 경우 아예 일어나지 않는다고 단정할 수는 없을 것 같은데요.
이런 경우는 절대 일어나지 않을 것이라고 생각하셨기 때문에 이 방식을 택하신 걸까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

+그리고 캐시의 경우에는 언급했던 것처럼 많이 오버엔지니어링이라고 생각합니다..ㅎㅎ
발생 확률이 매우 낮은 문제를 해결하기 위해 너무 큰 비용을 소모하는 방법이니까요. 개인적으로는 너무 기술적인 관점에서의 해결 방법이라고 생각합니다. 이에 대해서는 어떻게 생각하시나요?

Copy link
Member

Choose a reason for hiding this comment

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

그런 점에서 생각해 보면 복제 DB의 장애가 writer DB의 부담 증가에서만 끝날 수 있다면 꽤나 괜찮은 비용이라는 생각이 드네요.

reader DB의 장애가 서비스 장애로 전파되는 상황이 더 안 좋다고 판단하셨군요. 알파카가 제 코드를 보셨듯이 저는 getCoupon을 할 때 무조건 reader DB에서 조회하도록 현황을 유지했어요. 복제 지연으로 인한 장애가 발생하면 오랫동안 조회하지 못하는 상황을 고려하지 않았습니다. 서비스 전체보다는 쿠폰이라는 관점에서만 생각했네요 ㅎㅎ;

. 포케의 말씀처럼 복제 지연 시간이 실제 프로덕션 환경에서 늘어날 경우 아예 일어나지 않는다고 단정할 수는 없을 것 같은데요. 이런 경우는 절대 일어나지 않을 것이라고 생각하셨기 때문에 이 방식을 택하신 걸까요?

이 관점에서는 절대 일어나지 않다고 생각하진 않았어요. 말씀 주신것 처럼 직접 조회를 하는 상황은 관리자의 쿠폰 조회라는 문제로 정의했고, 관리자 페이지에서 생성한 쿠폰이 바로 보이지 않을때 어떻게 해결할 것인지에 중점을 두었습니다.

관리자가 구매자들에게 쿠폰을 발급하자마자 바로 사용할 수 있게 할 것 같진 않는다고 생각했고(실제 관리자가 발급한 쿠폰은 수 분의 검토를 거쳐서 발행되지 않을까요?) 쿠폰 생성 후 구매자가 쿠폰을 사용 가능하기 까지의 시간은 이미 복제 지연을 감수할 정도로 충분할 것이다라고 판단했습니다.

복제가 되지 않아 장애상황이 발생 할 수 있다는 고려하지 않았습니다.

+그리고 캐시의 경우에는 언급했던 것처럼 많이 오버엔지니어링이라고 생각합니다..ㅎㅎ
발생 확률이 매우 낮은 문제를 해결하기 위해 너무 큰 비용을 소모하는 방법이니까요. 개인적으로는 너무 기술적인 관점에서의 해결 방법이라고 생각합니다. 이에 대해서는 어떻게 생각하시나요?

공감합니다 ㅎㅎ 가용성을 위해 reader writer DB를 나눴는데 굳이 복제지연이 발생한다는 이유로 새 시스템을 도입한다는건 모험이라고 생각합니다. 운영하는 시스템이 쇼핑몰 시스템(?) 이라고 가정한다면 데이터 변경과 생성 요청(writer DB에 가는 요청이겠죠)이 데이터 조회로 가는 요청(reader DB)보다 훨씬 적을 텐데, writer DB와 Reader DB의 하드웨어 스팩이 동일하다면 복제지연으로 인한 문제는 writer DB로 충분히 해결할 것으로 보입니다. 하지만 reader DB writer DB 둘 다 트래픽을 감당하지 못한다는 상황이라면? 캐시메모리 선택을 좀 더 긍정적으로 고려할 것으로 보입니다..! 알파카는 이런 상황에선 어떻게 생각하시나요?

Comment on lines +11 to +14
@Transactional(propagation = Propagation.REQUIRES_NEW)
public <T> T executeOnWriter(Supplier<T> supplier) {
return supplier.get();
}
Copy link
Member

Choose a reason for hiding this comment

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

이 코드에 대해서 설명해주실 수 있을까요? getCouponFromWriter가 @Transactional 어노테이션 상관 없이 새로운 트렌젝션을 붙이는 것 같은데 재사용성을 고려하신걸까요?

@DisplayName("범위 내의 할인율로 생성할 수 있다.")
void discountRate(int discountMount) {
assertDoesNotThrow(() -> new Coupon(
new CouponName("총대마켓쿠폰"),
Copy link
Member

Choose a reason for hiding this comment

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

총대마켓 샤라웃 👍

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