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 복제와 캐시] 산초(나영서) 미션 제출합니다. #73

Open
wants to merge 15 commits into
base: nayonsoso
Choose a base branch
from

Conversation

nayonsoso
Copy link

@nayonsoso nayonsoso commented Oct 18, 2024

안녕하세요 무빈😊
우테코의 마지막 미션에서 만나게 되어 정말 반갑습니다.
제가 고민한 내용들을 간단히 적어봤어요~
(고민한 내용들을 그대로 드러내고자 '~다' 체를 사용했습니다 ㅎㅎ)


문제 상황 분석

쿠폰을 생성하고 나서 바로 그 쿠폰을 조회하려 할 때 발생하는 문제이다.
즉, writer 에 저장이 완료되었더라도, 읽어올 때 reader에서 읽어오기 때문에 아직 복제가 안되면 문제가 발생한다.
만약 이 서비스가 내부 직원들을 대상으로 하는 것이라면, 성능이 크게 중요하지 않을 수도 있다.
하지만 대상이 사용자들이라면? 성능이 중요하다.
그리고 쿠폰이라는 도메인은 돈과 관련되어있으므로 정합성도 못지 않게 중요하다.
굳이 우위를 가리자면, '정합성 > 성능'일 것이다.

따라서 가장 반드시 지켜야 하는 조건은
❗️저장된 쿠폰이 조회가 되는 것 & 저장되지 않은 쿠폰은 조회되지 않는 것❗️
이다.


복제 지연 해결 방법

1/ 클라이언트에서 or 서버에서 일정 시간 대기 후 읽어오는 방법

  • 구현은 가장 쉽겠지만, 성능이 좋지 않을 것이다.

2/ writer 에 저장 후 값을 캐싱해고, read 시 캐시를 읽는 방법

  • writer 에 저장이 정상적으로 된게 보장이 된다면, 정합성의 문제는 없을 것 같다.
  • 하지만 '캐시'라는 새로운 장치가 들어온다는 점에서 성능 측면에서 고려할게 많아진다.
  • 만약 이 캐시가 remote 캐시라면, 네트워크 오버해드가 발생한다.
  • local 캐시라면, 각 서버간 공유가 되지 않는 문제가 발생할 수 있다.
  • 예를 들어서, 5대의 ec2 에 로드 밸런싱을 하고 있다면 local 캐시는 의미가 없어진다.
  • session 을 사용하고 sticky session 기능을 사용한다면 문제가 되지 않을 수 있지만, 이 경우 캐싱이 스티키 세션에 의존하게 되어버린다.
  • 따라서 이 방법을 사용한다면 remote cache 를 사용해야 할 것이다.

3/ reader 에 없다면 writer 에서 읽어오는 방법

  • 데이터베이스가 다르다면, 데이서 소스가 다를 것이고, 서로 다른 커넥션이 생기게 된다.
  • 하나의 트랜잭션에서 두개의 커넥션이 생긴다면, 트랜잭션의 시간이 더 길어지고, lock 하고 있는 리소스들도 많아진다.
  • 이 과정에서 일반적인 트랜잭션보다 오버헤드가 발생한다.

결론

완벽한 방법은 없다🙀
그나마 고르자면 2번 또는 3번일 것인데, 그 빈도를 생각해보자면..

2번은 모든 조회 요청에 대해 remote 캐시를 조회한다.
3번은 'reader 에 없는 경우'에만 write 에 접근힌다.
따라서 발생 빈도가 적은 3번의 방법이 적절해보인다👍

하지만 복제 지연 시간이 유의미하게 길어진다면, 2번이 더 적절한 방법일 수도 있다.

@nayonsoso nayonsoso self-assigned this Oct 18, 2024
Copy link

@hjk0761 hjk0761 left a comment

Choose a reason for hiding this comment

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

산초 안녕하세요!
마지막 미션에서 뵙게 되어 반가워요!!

복제 지연 해결 방법을 다양하게 분석해주셨네요!
생각한 방법들과 이유, 장단점 잘 파악해주신 것 같아요 👍
결론은 발생 빈도의 관점에서 DataSource 를 WRITER 로 선택하는 방안을 채택해 주신 것 같아요 ㅎㅎ

이렇게 생각한 과정에서 궁금한 점이 있어요!
결정에 앞서 쿠폰 조회 서비스의 사용 대상에 대해 언급해 주셨는데요,
산초는 쿠폰 생성 및 조회 서비스는 내부 직원 혹은 사용자 중에 누가 사용할 것이라고 생각하셨나요?
지금의 getCoupon() 메서드는 둘 중에 어느 대상을 고려해서 만든 건지, 그리고 그렇게 생각하신 이유가 궁금해요!

이번 미션 잘 부탁드리고, 미션과 프로젝트 모두 화이티입니다!


public class DataSourceRouter extends AbstractRoutingDataSource {

private static final ThreadLocal<DataSourceType> currentDataSource = new ThreadLocal<>();
Copy link

Choose a reason for hiding this comment

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

ThreadLocal 로 구현 잘 해주신 것 같습니다 👍
혹시 ThreadLocal 을 사용해야 하는 이유를 설명해줄 수 있나요?

import lombok.Getter;

@Getter
public class Coupon {
Copy link

Choose a reason for hiding this comment

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

도메인도 객체지향적으로 잘 분리해주셨네요!!

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class CouponEntity {
Copy link

Choose a reason for hiding this comment

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

엔티티와 도메인을 분리해주셨네요! 저는 도메인과 엔티티를 혼용하다보니 필드 구성과 스키마 설계도 좀 쉽지 않았는데, 배워갑니다!!

Comment on lines +33 to +38
if (couponEntity.isEmpty()) {
DataSourceRouter.setDataSourceType(DataSourceType.WRITER);
couponEntity = couponRepository.findById(id);
}
if(couponEntity.isEmpty()) {
throw new IllegalArgumentException("존재하지 않는 쿠폰입니다.");
Copy link

Choose a reason for hiding this comment

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

isEmpty() 로 CouponEntity 객체를 두 번 검사하게 되는데,
처음 검사는 복제지연을 피하기 위한 검사이고, 다음 검사가 정말 쿠폰이 존재하는지에 대한 검사네요!

이렇게 두 번 같은 확인을 하도록 설계하신 이유가 있는지 궁금합니다!

Copy link

Choose a reason for hiding this comment

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

추가적으로, Service 가 DataSourceType 과 DataSourceRouter 를 직접적으로 알게 되는데, 서비스가 많은 책임을 가지고 있는 것 처럼 느껴지는 것 같아요!
이에 대해 어떻게 생각하시나요?

private Optional<CouponEntity> getCouponById(Long id) {
try {
return couponRepository.findById(id);
} catch (SQLGrammarException ex) {
Copy link

Choose a reason for hiding this comment

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

SQLGrammerException 을 캐치해서 Optional.empty() 를 반환하도록 구현한 의도가 궁금합니다!

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