티스토리 뷰

 

2024.01.29 - [일상] - [컨퍼런스] 게으른 개발자 컨퍼런스 후기

 

컨퍼런스 이벤트로 받았던 강의였는데, 시간이 꽤 지나서 듣게 됐다.

 

총 1시간 12분짜리의 짧은 강의였지만 나름대로 생각해볼만한 여지를 준 강의였다.

(링크)

 

말 그대로 선착순 이벤트를 만들면서 생길만한 이슈들을 하나씩 짚어가면서 대응하는 법을 알려준다.

 

요구사항은 다음과 같다

선착순 100명에게 할인쿠폰을 제공하는 이벤트를 진행하고자 한다.

이 이벤트는 아래와 같은 조건을 만족하여야 한다.
- 선착순 100명에게만 지급되어야한다.
- 101개 이상이 지급되면 안된다.
- 순간적으로 몰리는 트래픽을 버틸 수 있어야합니다.
https://dramatic-server-aab.notion.site/c01b8f15ee324137a4c0340c8310652a

 

소소한 코드들은 생략하고, 처음은 간단하게 시작한다.

// 쿠폰 발급
public void apply(Long userId) {
    long count = couponRepository.count();
    if (count > 100) {
        return;
    }
    couponRepository.save(new Coupon(userId));
}

작은 서비스에서는 큰 문제가 없을지 모르지만, 요청이 동시에 빠른 속도로 들어오게 된다면 문제가 생긴다.

 

문제 1. race condition 문제

count가 db를 직접 찌르기 때문에 요청이 동시에 빠르게 들어오면 race condition 문제가 발생한다.

 

race condition : 두개 이상의 자원이 공유자원에 접근하여 데이터를 업데이트하려고 할 때 발생

 

그래서 테스트 결과 더 많은 100개보다 더 많은 쿠폰이 저장된다.

 

이 문제를 해결하기 위해서 redis를 사용한다.

@Repository
public class CouponCountRepository {
    private final RedisTemplate<String, String> redisTemplate;
    public CouponCountRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    public Long increment() {
        return redisTemplate
                .opsForValue()
                .increment("coupon_count");
    }
}
// 레디스 레포지토리 추가 후 쿠폰 발급 매서드 수정

public void apply(Long userId) {
    // long count = couponRepository.count();
    long count = couponCountRepository.increment();

    if (count > 100) {
        return;
    }

    couponRepository.save(new Coupon(userId));
}

redis는 싱글쓰레드로 동작하기 때문에 race condition은 발생하지 않을 것이다. 

 

redis의 incr 매서드(1씩 증가)를 이용하면, 속도면에서도 이점을 얻을 수 있다. 모든 쓰레드에서 최신값을 얻을 수 있다.

 

다른 대안은 뭐가 있을까?

1. 자바의 syncronize : 서버가 여러대면 동일한 문제가 발생

2. mysql or redis를 이용한 락 : 쿠폰 생성 - 발급까지 락이 발생해야해서 성능에 문제가 발생

 

그러나... 또다른 문제점 발생할 수 있는데...

 

문제 2. 더 많은 수의 쿠폰을 발급해야해야할 경우라면? 

- 다양한 조건이 붙음

1. RDB가 1분에 100개만 write가 가능

2. 쿠폰 발급 서비스 외의 다른 서비스도 같은 RDB를 이용

3. 쿠폰을 발급하는데 특정시간 이상 소요됨

 

- RDB에 부하로 인해 다른 요청들이 타임 아웃이 날 수 있다.

10:00 쿠폰 발급 10000개

10:01 주문 생성 요청

10:02 회원 가입 요청

 

쿠폰 발급으로 인해 다른 서비스에도 영향이 가니 주의가 필요하다.

 

특히 오토스케일링되는 서버가 문제를 일으킬 가능성이 높다.

 

이 문제를 해결하기위해 kafka를 사용

kafka : 분산 이벤트 스트리밍 플랫폼. producer - topic - consumer 구조로 큐라고 생각하면 편함

 

정확히는 큐를 생성해 요청을 큐에 담아두고 차례로 처리한다.

// producer
@Component
public class CouponCreateProducer {

    private final KafkaTemplate<String, Long> kafkaTemplate;

    public CouponCreateProducer(KafkaTemplate<String, Long> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void create(Long userId) {
        kafkaTemplate.send("coupon_create", userId);
    }

}

public void apply(Long userId) {
    long count = couponCountRepository.increment();

    if (count > 100) {
        return;
    }

    couponCreateProducer.create(userId);
}

// consumer
@KafkaListener(topics = "coupon_create", groupId = "group_1")
public void listener(Long userId) {
    System.out.println(userId);
    couponRepository.save(new Coupon(userId));
}

 

쿠폰 생성 시간을 뒤로 미뤄서 처리량을 조절(유량 제어) 하게 된다. 약간의 텀이 발생한다는건 어쩔 수 없음

 

문제 3. 요구사항 변경 : 쿠폰 개수를 인당 한개로

1. DB에서 쿠폰 타입과 유저 id로 키를 만들어서 제어

- 한 유저가 같은 타입의 쿠폰을 여러개 가질 수도 있기 때문에 실용적인 방법이 아니다.

2. 범위로 락을 잡고 처음에 쿠폰 발급 여부를 가져와서 확인

- 쿠폰 발급은 consumer에서 하도록 처리했기 때문에, 타이밍 이슈가 발생해서 의도치않은 동작할 수 있음(쿠폰 재발급)

- 넓어진 락 범위로 인한 성능 저하

3. redis에서 set을 활용해보자

 

redis의 sadd 매서드를 사용해서 처리한다. sadd 매서드는 존재하면 0, 존재하지 않으면 1을 return 한다.

// redis
@Repository
public class AppliedUserRepository {
    private final RedisTemplate<String, String> redisTemplate;
    public AppliedUserRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    public Long add(Long userId) {
        return redisTemplate
                .opsForSet()
                .add("applied_user", userId.toString());
    }
}

public void apply(Long userId) {
    Long apply = appliedUserRepository.add(userId);
	// 이미 발급했을 때 apply = 0
    if (apply != 1) { 
        return;
    }

    long count = couponCountRepository.increment();

    if (count > 100) {
        return;
    }

    couponCreateProducer.create(userId);
}

 

 

문제 4. 쿠폰을 발급하다 에러가나면 어떻게 하나요?

컨슈머가 에러가 발생한다면? 쿠폰이 발급되지 않았는데 개수만 증가하는 문제가 발생함

 

백업데이터와 로그를 남기는 방식으로 처리

@KafkaListener(topics = "coupon_create", groupId = "group_1")
public void listener(Long userId) {
    // couponRepository.save(new Coupon(userId));
    try {
        couponRepository.save(new Coupon(userId));
    } catch (Exception e) {
        logger.error("failed to create coupon::" + userId);
        failedEventRepository.save(new FailedEvent(userId));
    }
}

 

그리고 이 데이터를 배치 처리해서 재시도하게 만듬

 

마치며

음... 카프카를 쓰는건 좋은데, 문제와 프로젝트 크기에 비해 오버 엔지니어링이 아닌가? 싶음.

 

큐가 필요한 예제였다고 생각함..

 

차라리 레디스의 다양한 용례를 이야기하는게 좋았을 수도 있었겠다.

 

이와 반대로 사례를 통해 문제점들을 설명해주고, 여러 해결책을 언급해줘서 꽤 재밌게 들을 수 있었다.

 

그리고 롬복을 안쓰고 ide 기능으로 해결하는게 꽤 신기했다.

 

다음 강의도 들어볼 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함