크레용은 메일 발신 기능을 제공한다.
지원자명, 면접 장소, 면접 일자 등 지원자 정보를 템플릿으로 구성해 한 번의 클릭으로 모든 지원자에게 커스텀 메일을 전송할 수 있다.
크레용은 AWS SES를 사용해 메일 전송 기능을 구현했다.
우리는 빈곤💸 대학생팀이기에(ㅎㅎ) 프리티어라는 작은 자원을 소중하게 사용해야 한다.
내가 운영했던 동아리(Leets)를 기준으로, 지원자를 약 100명으로 가정해보자. 이 동아리가 나쁜 맘을 먹고 하루에 메일 전송을 500번 해버리면 다른 동아리는 메일 기능을 사용할 수 없다 ㅜㅅㅜ
그래서 나쁜 동아리가 메일 전송 기능을 독점하지 않도록 제한을 두고자 한다.
Redis는 DB보다 빠르고 동시성에 강하며 TTL 설정이 가능하기 때문에 전송 가능 횟수를 저장하기에 적합하다고 판단했다.
SET global:email:total 50000 EX 86400
SET global:email:{clubId} 300 EX 86400
global:email:{}
: 남은 전송 가능 횟수를 저장하는 키Spring에서 RedisTemplate을 활용했다.
@Component
@RequiredArgsConstructor
public class MailRateLimiter {
private static final String TOTAL_KEY = "global:email:total";
private static final String CLUB_KEY = "global:email:";
private static final Map<String, Long> LIMIT = Map.of(
TOTAL_KEY, 50_000L,
CLUB_KEY, 300L
);
private static final Duration TTL = Duration.ofHours(24);
private final RedisTemplate<String, String> rateLimitRedisTemplate;
public boolean isRateLimited(UUID clubId, int requestSize) {
String clubKey = CLUB_KEY + clubId;
if (exceededLimit(requestSize, clubKey)) {
return true;
}
rateLimitRedisTemplate.opsForValue().decrement(TOTAL_KEY, requestSize);
rateLimitRedisTemplate.opsForValue().decrement(clubKey, requestSize);
return false;
}
private boolean exceededLimit(int requestSize, String clubKey) {
long totalRemaining = getRemaining(TOTAL_KEY, LIMIT.get(TOTAL_KEY));
long clubRemaining = getRemaining(clubKey, LIMIT.get(CLUB_KEY));
return totalRemaining < requestSize || clubRemaining < requestSize;
}
private long getRemaining(String key, long amount) {
String value = rateLimitRedisTemplate.opsForValue().get(key);
if (value != null) {
return Long.parseLong(value);
}
rateLimitRedisTemplate.opsForValue().set(key, String.valueOf(amount), TTL);
return amount;
}
}
false
반환true
반환이렇게 Redis를 활용하여 간단하게 요청 제한 기능을 구현할 수 있었다. 원래는 Bucket4j를 사용해서 요청에 대한 Rate Limit을 제한하려 했다. 그러나 일정 시간을 간격으로 요청에 제한을 둔다면, A 동아리가 메일을 전송함으로써 B 동아리가 일정기간 대기해야하는 상황이 발생할 것이라 판단했다.
그리고 아직 해결하지 못한 문제가 있다.
Redis는 단일 스레드 기반이라서 INCR, DECR는 atomic하게 동작하지만,
현재 Check-Then-Act 패턴을 적용하고 있다. 이로 인해 경쟁 상태가 발생할 수 있다.
다음에는 Redis에서 경쟁 상태를 방지할 수 있는 방법을 알아보고 적용해 봐야지