Skip to content

feat: 1시부터 3시 피크타임 좋아요 2배 이벤트#112

Merged
wlgns12370 merged 1 commit intomainfrom
feat/#111
Mar 16, 2026
Merged

feat: 1시부터 3시 피크타임 좋아요 2배 이벤트#112
wlgns12370 merged 1 commit intomainfrom
feat/#111

Conversation

@wlgns12370
Copy link
Copy Markdown
Contributor

@wlgns12370 wlgns12370 commented Mar 16, 2026

관련 이슈


설계 상세

1. 좋아요 2배 이벤트 — LocalTime 기반 multiplier

핵심 의사결정: Redis flag 방식 대신 LocalTime 체크 채택

Redis flag 방식은 이벤트 on/off를 즉시 반영할 수 있지만, Blue-Green 배포 시
신구 서버가 동일한 Redis key를 바라보면 배포 도중 이벤트 적용 여부가 불일치할 수 있음.
시간 기반 체크는 공유 상태가 없어 어떤 서버 인스턴스든 동일하게 동작하며, 추가 Redis I/O도 없음.

// BoothLikeService.java
private int getLikeMultiplier() {
    LikeProperties.DoubleEvent event = likeProperties.doubleEvent();
    if (!event.enabled()) return 1;  // 이벤트 비활성화 시 즉시 반환

    LocalTime now = LocalTime.now(ZoneId.of("Asia/Seoul")); // 타임존 명시
    LocalTime start = LocalTime.parse(event.startTime());
    LocalTime end   = LocalTime.parse(event.endTime());

    return (!now.isBefore(start) && now.isBefore(end)) ? 2 : 1;
}

multiplier 계산만 별도 메서드로 분리하여 기존 like() 흐름을 변경하지 않음.

// like() 호출부 — 기존 로직 변경 없음
int multiplier = getLikeMultiplier();
Double score = redisTemplate.opsForZSet().incrementScore(RANKING_KEY, String.valueOf(boothId), multiplier);

env 외부화 — 재빌드 없이 이벤트 시간 변경

like:
  double-event:
    enabled: ${LIKE_DOUBLE_EVENT_ENABLED:false}
    start-time: ${LIKE_DOUBLE_EVENT_START:12:00}
    end-time: ${LIKE_DOUBLE_EVENT_END:14:00}

@ConfigurationProperties record를 사용해 타입 안전하게 바인딩.

@ConfigurationProperties(prefix = "like")
public record LikeProperties(RateLimit rateLimit, DoubleEvent doubleEvent) {
    public record DoubleEvent(boolean enabled, String startTime, String endTime) {}
}

2. Rate Limiting — 필터 체인 3단계 구성

좋아요 도배 방지를 서비스 레이어가 아닌 필터 레이어에서 차단.
서비스 레이어까지 요청이 도달하기 전에 429를 반환하므로 DB/Redis 부하를 줄임.

Request
  │
  ▼
[Order 1] ClientIpFilter          — X-Forwarded-For에서 실제 IP 추출 → request attribute 저장
  │
  ▼
[Order 2] DeviceIdCookieFilter    — 쿠키에서 device_id 읽기, 없으면 UUID 발급 → request attribute 저장
  │
  ▼
[Order 3] LikeRateLimitFilter     — IP + DeviceId 조합으로 Rate Limit 판단, 초과 시 429 반환
  │
  ▼
Controller → BoothLikeService

LikeRateLimiter — Redis increment 기반 슬라이딩 카운터

// LikeRateLimiter.java
public boolean isAllowed(String clientIp, String deviceId) {
    String key = "like:rate:%s:%s".formatted(clientIp, deviceId);

    long currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(key))
        .map(Long::parseLong).orElse(0L);

    if (currentCount >= likeProperties.rateLimit().maxLikes()) return false;

    Long newCount = redisTemplate.opsForValue().increment(key);
    if (newCount == 1) {
        redisTemplate.expire(key, Duration.ofSeconds(likeProperties.rateLimit().ttlSeconds()));
    }
    return true;
}

key가 처음 생성될 때(newCount == 1)만 TTL을 설정해 window를 고정.

LikeRateLimitFilter — /booths/*/likes 경로만 적용

// LikeRateLimitFilter.java
private boolean isLikesPath(HttpServletRequest request) {
    return "POST".equalsIgnoreCase(request.getMethod())
        && request.getRequestURI().matches(".*/booths/[^/]+/likes$");
}

정규식으로 경로를 판단해 다른 API에는 필터가 개입하지 않음.
Rate Limit 초과 시 서비스 레이어 없이 필터에서 즉시 429 반환.

FilterConfig — 필터 등록 및 쿠키 설정 외부화

// FilterConfig.java
@Value("${device-id.cookie.max-age}")   private int cookieMaxAge;
@Value("${device-id.cookie.same-site}") private String cookieSameSite;
@Value("${device-id.cookie.secure}")    private boolean cookieSecure;

쿠키 속성(max-age, SameSite, Secure)을 FilterConfig에서 주입받아
DeviceIdCookieFilter에 전달. 필터 내부에 설정을 하드코딩하지 않음.


테스트 시나리오

  • 이벤트 시간대 내 좋아요 요청 → 점수가 2씩 증가하는지 확인
  • 이벤트 시간대 외 좋아요 요청 → 점수가 1씩 증가하는지 확인
  • LIKE_DOUBLE_EVENT_ENABLED=false → 시간대 내에도 1배로 적용되는지 확인
  • Rate Limit 초과 요청 → HTTP 429 반환 확인
  • Rate Limit 창(TTL) 지난 후 재요청 → 정상 응답 확인
  • OPTIONS 요청 → DeviceId 쿠키 미발급 확인 (CORS preflight 오염 방지)

환경변수 설정 예시

# 이벤트 활성화 및 시간 설정
LIKE_DOUBLE_EVENT_ENABLED=true
LIKE_DOUBLE_EVENT_START=12:00
LIKE_DOUBLE_EVENT_END=14:00

# Rate Limit 설정
LIKE_RATE_LIMIT_TTL_SECONDS=10
LIKE_RATE_LIMIT_MAX_LIKES=5

@wlgns12370 wlgns12370 added the Ship 리뷰 없이 변경을 바로 머지하는 경우 label Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

피크타임(1시~3시) 동안 좋아요 점수에 2배 배수를 적용하는 이벤트 기능이 추가되었습니다. 동시에 메서드 인자 타입 불일치 예외에 대한 전역 예외 처리기도 추가되었습니다.

Changes

Cohort / File(s) Summary
좋아요 이벤트 배수 기능
src/main/java/kr/co/knuserver/application/booth/BoothLikeService.java
더블 이벤트 기능을 위한 설정 필드 3개 추가(doubleEventEnabled, doubleEventStart, doubleEventEnd). getLikeMultiplier() 메서드를 통해 현재 Seoul 시간대를 기반으로 배수(1 또는 2) 계산. like() 메서드에서 하드코딩된 배수 1을 대체하여 동적 배수 적용.
예외 처리 개선
src/main/java/kr/co/knuserver/global/handler/GlobalExceptionHandler.java
MethodArgumentTypeMismatchException 핸들러 추가. 메서드 인자 타입 불일치 시 400 BAD_REQUEST 응답 반환.

Sequence Diagram

sequenceDiagram
    participant Client
    participant BoothLikeService
    participant Config
    participant TimeProvider as TimeProvider<br/>(Seoul TZ)

    Client->>BoothLikeService: like(boothId)
    activate BoothLikeService
    BoothLikeService->>BoothLikeService: getLikeMultiplier()
    activate BoothLikeService
    BoothLikeService->>Config: doubleEventEnabled?
    Config-->>BoothLikeService: true/false
    alt Event Enabled
        BoothLikeService->>TimeProvider: getCurrentTime(Seoul)
        TimeProvider-->>BoothLikeService: current time
        BoothLikeService->>BoothLikeService: check if within<br/>doubleEventStart~End
        alt Within Event Window
            BoothLikeService-->>BoothLikeService: return 2
        else Outside Window
            BoothLikeService-->>BoothLikeService: return 1
        end
    else Event Disabled
        BoothLikeService-->>BoothLikeService: return 1
    end
    deactivate BoothLikeService
    BoothLikeService->>BoothLikeService: score = baseScore × multiplier
    BoothLikeService-->>Client: success response
    deactivate BoothLikeService
Loading

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 한 마리 토끼, 시간 계산법을 배웠네,
1시부터 3시까지, 좋아요는 두 배!
설정과 예외도 척척 정렬되고,
이벤트 성공, 귀여운 배치 완성! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning BoothLikeService의 이벤트 기능 외에도 GlobalExceptionHandler에 MethodArgumentTypeMismatchException 처리가 추가되었으나, 이는 이슈 요구사항에 언급되지 않은 변경사항입니다. GlobalExceptionHandler 변경사항이 이슈 #111의 범위를 벗어나므로, 별도의 이슈/PR로 분리하거나 변경사항을 제거하는 것을 고려해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명이 템플릿 대비 최소한의 정보만 포함하고 있습니다. '구현한 기능' 섹션에만 이슈 참조가 있고, '논의하고 싶은 내용'과 '기타' 섹션이 비어있습니다. 더 구체적인 구현 내용, 변경사항 설명, 그리고 필요한 논의 사항들을 추가하여 설명을 보완해주세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경사항의 주요 내용을 명확하게 설명하고 있습니다. '1시부터 3시 피크타임 좋아요 2배 이벤트' 기능이 코드 변경과 정확히 일치합니다.
Linked Issues check ✅ Passed 변경사항이 이슈 #111의 요구사항을 충족합니다. 13시부터 15시(1시부터 3시) 동안 좋아요를 2배로 하는 이벤트 기능이 구현되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#111
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wlgns12370 wlgns12370 merged commit 0a2788c into main Mar 16, 2026
1 of 2 checks passed
@wlgns12370 wlgns12370 self-assigned this Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ship 리뷰 없이 변경을 바로 머지하는 경우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[이벤트] 1시부터 3시 피크타임 좋아요 2배 이벤트

1 participant