Skip to content

[FEAT] elasticache 연결 및 방문자정보(ipAddress 등) 수집 기능 구현 #68

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

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

LEEJaeHyeok97
Copy link
Member

@LEEJaeHyeok97 LEEJaeHyeok97 commented Apr 17, 2025

관련 이슈

close #60

주요 변경 사항

주요 변경 사항에 대해 작성해주세요.

  • 요청에 대해 Redis 캐시 검사를 수행하는 RedisInterceptor를 구현했습니다.
  • 인터셉터는 클라이언트 요청 전 preHandle 단계에서 Redis에서 키 값을 확인하고, 조건에 따라 요청을 차단합니다.
  • WebMvcConfigurer를 통해 /api/** 경로에 대해 Interceptor를 등록했습니다.
  • 헥사고날 아키텍처 원칙에 따라, RedisInterceptoradapter/in/restapi/interceptor에 위치시켰습니다.

기타

고려해야 하는 내용을 작성해 주세요.

  • Redis 설정이 없는 로컬 환경을 위한 yml profile 분리를 고려할 시점에 도달했습니다.(로컬에서 elasticache연결 불가 - vpc 문제)

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능
    • 방문자 정보를 관리하는 도메인 모델, 엔티티, 변환기, 저장소 인터페이스 및 구현체가 추가되었습니다.
    • 방문자 단일 방문 기록을 위한 인터셉터가 도입되어, IP와 날짜별로 Redis에 방문 정보를 저장합니다.
    • 방문자 정보를 주기적으로 Redis에서 데이터베이스로 이전하는 스케줄러가 추가되었습니다.
    • Redis 연동을 위한 설정 및 의존성이 추가되었습니다.

@LEEJaeHyeok97 LEEJaeHyeok97 added the ✨ feature New feature or request label Apr 17, 2025
@LEEJaeHyeok97 LEEJaeHyeok97 requested a review from kssumin April 17, 2025 18:51
@LEEJaeHyeok97 LEEJaeHyeok97 self-assigned this Apr 17, 2025
@LEEJaeHyeok97 LEEJaeHyeok97 linked an issue Apr 17, 2025 that may be closed by this pull request
4 tasks
Copy link

coderabbitai bot commented Apr 17, 2025

"""

Walkthrough

이번 변경에서는 Redis(ElastiCache) 연동 및 일일 방문자 수 집계 기능이 도입되었습니다. Redis Starter 의존성이 추가되고, 방문자 정보를 도메인 및 엔티티 계층에 설계하여 JPA 및 Redis를 통한 방문자 데이터 저장과 집계를 구현합니다. 방문자 요청 시 Interceptor를 통해 Redis에 기록하고, 스케줄러가 주기적으로 Redis의 방문자 데이터를 영속 저장소로 옮깁니다. Redis 연결을 위한 Spring 설정 클래스도 추가되었습니다.

Changes

파일/경로 변경 요약
build.gradle.kts, buildSrc/src/main/kotlin/Dependency.kt Redis(Spring Data Redis) 의존성 추가 및 Dependency 객체에 Redis 버전/의존성 상수 추가
src/main/kotlin/com/coffee/api/config/RedisConfig.kt Redis 연결 및 RedisTemplate 빈을 제공하는 Spring 설정 클래스 추가
src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt 방문자 도메인 객체(Visitor) 및 ID 타입 정의, 팩토리 메서드 추가
src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt Visitor <-> VisitorEntity 변환을 위한 Converter 클래스 추가
src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt JPA 엔티티 VisitorEntity 정의, 방문자 속성 및 팩토리 메서드 포함
src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt 방문자 저장소 인터페이스 정의(존재 여부 확인, 저장 메서드)
src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt Spring Data JPA 리포지토리 인터페이스 정의
src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt VisitorRepository 구현체 추가(JPA 리포지토리 위임)
src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt HandlerInterceptor 구현, 요청 시 Redis에 방문자 정보 기록
src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt 스케줄러 추가, Redis에서 방문자 데이터 읽어 JPA 저장 및 Redis 키 삭제

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SingleVisitInterceptor
    participant Redis
    participant VisitorScheduler
    participant VisitorRepository
    participant DB

    Client->>SingleVisitInterceptor: HTTP 요청
    SingleVisitInterceptor->>Redis: IP+날짜 키 존재 확인
    alt 키 없음
        SingleVisitInterceptor->>Redis: 키에 User-Agent 저장
    end
    SingleVisitInterceptor-->>Client: 요청 계속 진행

    Note over VisitorScheduler: 50분마다 실행
    VisitorScheduler->>Redis: "*_*" 패턴 키 조회
    loop 각 키마다
        VisitorScheduler->>Redis: User-Agent 값 조회
        VisitorScheduler->>VisitorRepository: IP+날짜 존재 확인
        alt 미존재
            VisitorScheduler->>VisitorRepository: VisitorEntity 저장
        end
        VisitorScheduler->>Redis: 키 삭제
    end
Loading

Assessment against linked issues

Objective (이슈 번호) Addressed Explanation
Redis(ElastiCache) 연동 (#60)
방문자 수 집계 (#60)
조회 성능 개선: Redis 활용 (#60)
집계된 데이터 디스코드로 전송 (#60) 디스코드 전송 로직은 구현되지 않음
필요시 페이징 최적화 및 배치 쿼리 적용 (#60) 페이징/배치 쿼리 관련 구현이 명확히 드러나지 않음

Poem

( ˘ ³˘)♥

Redis에 토끼 발자국 콩콩 찍고,
방문자 집계는 스케줄러가 척척!
스프링과 함께 캐시도 연결,
매일매일 기록이 쏙쏙!
오늘도 코드밭에 당근 한 움큼,
집계 기능에 토끼가 방긋!

(⁎ᵕᴗᵕ⁎)🐇
"""

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@LEEJaeHyeok97 LEEJaeHyeok97 changed the title [FEAT] elasticache 연결 및 방문자정보(ip 외) 수집 기능 구현 [FEAT] elasticache 연결 및 방문자정보(ipAddress 등) 수집 기능 구현 Apr 17, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (6)
src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt (1)

8-12: JpaRepository 인터페이스 활용 최적화

JpaRepository는 이미 save 메소드를 제공하므로, 이를 명시적으로 재정의할 필요가 없습니다. 코드 중복을 피하고 인터페이스를 더 간결하게 만들 수 있습니다.

다음과 같이 수정하는 것을 권장합니다:

interface VisitorJpaRepository : JpaRepository<VisitorEntity, UUID> {
    fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean
-
-    fun save(visitorEntity: VisitorEntity): VisitorEntity
}

save 메소드는 JpaRepository에서 상속받기 때문에 명시적으로 선언하지 않아도 사용할 수 있습니다.

src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt (1)

22-29: 엔티티 변환 로직에 예외 처리 추가 고려

도메인 모델에서 엔티티로 변환하는 로직은 잘 구현되어 있지만, domain.id.value 접근 시 null이 반환될 가능성에 대한 예외 처리가 없습니다. 안전한 처리를 위해 예외 처리를 추가하는 것이 좋습니다.

override fun toEntity(domain: Visitor): VisitorEntity {
+   val id = domain.id.value ?: throw IllegalArgumentException("Visitor ID cannot be null")
    return VisitorEntity(
-       id = domain.id.value,
+       id = id,
        userIp = domain.userIp,
        userAgent = domain.userAgent,
        date = domain.date
    )
}
src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt (1)

16-18: save 메소드에 로깅 추가 고려

리포지토리 구현이 단순하고 명확하지만, 디버깅 및 모니터링을 위한 로깅이 없습니다. 중요 작업에 대한 로깅을 추가하는 것이 좋습니다.

override fun save(visitor: VisitorEntity): VisitorEntity {
+   log.debug("Saving visitor entity: IP={}, date={}", visitor.userIp, visitor.date)
    return visitorJpaRepository.save(visitor)
}

이를 위해 클래스에 로거 추가가 필요합니다:

@Repository
class VisitorRepositoryImpl(
    private val visitorJpaRepository: VisitorJpaRepository
) : VisitorRepository {
+   private val log = LoggerFactory.getLogger(this::class.java)
src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt (1)

11-14: 로깅 기능을 추가해야 합니다.

디버깅 및 모니터링을 위해 Redis 작업 및 방문자 정보에 대한 로깅을 추가하는 것이 좋습니다.

@Component
class SingleVisitInterceptor(
    private val redisTemplate: RedisTemplate<String, String>
-) : HandlerInterceptor {
+) : HandlerInterceptor {
+    private val logger = LoggerFactory.getLogger(SingleVisitInterceptor::class.java)

그리고 메서드 내에서 로깅 추가:

try {
    if (!redisTemplate.hasKey(key)) {
        valueOperations.set(key, userAgent)
        redisTemplate.expire(key, Duration.ofDays(2))
+       logger.debug("새로운 방문자 정보 저장: IP={}, 날짜={}", userIp, today)
    }
} catch (e: Exception) {
    logger.error("Redis 작업 중 오류 발생: ${e.message}", e)
}
src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt (2)

16-38: 사용자 에이전트 정규화 및 IP 주소 형식 처리가 필요합니다.

User-Agent 문자열은 매우 길고 세부 버전 정보를 포함할 수 있어, 정규화 없이 저장하면 중복 데이터가 발생할 수 있습니다. 또한 IPv4와 IPv6 주소 형식에 대한 고려가 필요합니다.

companion object {
    @JsonCreator
    fun create(
        id: UUID,
        userIp: String,
        userAgent: String,
        date: LocalDate
    ): Visitor {
+       // IP 주소 정규화 (IPv4, IPv6 고려)
+       val normalizedIp = normalizeIpAddress(userIp)
+       
+       // User-Agent 정규화 (주요 브라우저/OS 정보만 추출)
+       val normalizedUserAgent = normalizeUserAgent(userAgent)
+       
        return Visitor(
            id = UUIDTypeId.from(id),
-           userIp = userIp,
-           userAgent = userAgent,
+           userIp = normalizedIp,
+           userAgent = normalizedUserAgent,
            date = date
        )
    }

    operator fun invoke(
        id: UUID,
        userIp: String,
        userAgent: String,
        date: LocalDate
    ): Visitor = create(id, userIp, userAgent, date)
+
+    // IP 주소 정규화 메서드
+    private fun normalizeIpAddress(ip: String): String {
+        // IPv4 및 IPv6 주소 정규화 로직 구현
+        return ip.trim()
+    }
+
+    // User-Agent 정규화 메서드
+    private fun normalizeUserAgent(userAgent: String): String {
+        // 주요 브라우저/OS 정보만 추출하는 로직 구현
+        return userAgent.trim()
+    }
}

1-41: 클래스 및 메서드에 대한 문서화가 필요합니다.

도메인 모델의 의도와 목적을 더 명확하게 이해할 수 있도록 KDoc 형식의 문서화 주석을 추가하는 것이 좋습니다.

package com.coffee.api.cafe.domain

import com.coffee.api.common.domain.AbstractDomain
import com.coffee.api.common.domain.UUIDTypeId
import com.fasterxml.jackson.annotation.JsonCreator
import java.time.LocalDate
import java.util.*

+/**
+ * 웹사이트 방문자 정보를 나타내는 도메인 엔티티.
+ * 각 방문자는 고유 ID, IP 주소, 사용자 에이전트 정보, 방문 날짜로 구성됩니다.
+ */
class Visitor private constructor(
    override val id: Id,
    val userIp: String,
    val userAgent: String,
    val date: LocalDate
) : AbstractDomain<Visitor, Visitor.Id>() {

    companion object {
+       /**
+        * UUID, IP 주소, 사용자 에이전트, 날짜로부터 방문자 객체를 생성합니다.
+        * 이 메서드는 JSON 역직렬화에 사용됩니다.
+        *
+        * @param id 방문자의 고유 ID
+        * @param userIp 방문자의 IP 주소
+        * @param userAgent 방문자의 브라우저/OS 정보
+        * @param date 방문 날짜
+        * @return 새로운 방문자 객체
+        */
        @JsonCreator
        fun create(
            id: UUID,
            userIp: String,
            userAgent: String,
            date: LocalDate
        ): Visitor {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f0e77e and c77118f.

📒 Files selected for processing (11)
  • build.gradle.kts (1 hunks)
  • buildSrc/src/main/kotlin/Dependency.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/config/RedisConfig.kt (1 hunks)
🔇 Additional comments (8)
build.gradle.kts (1)

81-84: Redis 의존성이 빌드 설정에 추가되었습니다.

Redis 의존성이 기존 의존성 구조에 맞게 잘 추가되었습니다. 주석과 함께 명확하게 구분되어 있어 코드 가독성이 좋습니다.

src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt (2)

10-17: 적절한 엔티티 정의입니다.

JPA 엔티티 클래스를 잘 정의하셨습니다. @Entity@Table 어노테이션이 올바르게 사용되었으며, BaseEntity를 상속받아 공통 필드를 활용하고 있습니다.


32-41: 팩토리 메소드가 적절히 구현되었습니다.

팩토리 메소드 패턴을 사용하여 엔티티 생성을 캡슐화하고, UUID를 자동으로 생성하는 접근 방식이 좋습니다.

src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt (2)

8-12: 컨버터 클래스 구조가 적절합니다.

도메인 객체와 엔티티 간의 변환을 위한 컨버터 클래스가 잘 정의되어 있습니다. @Component 어노테이션으로 빈 등록이 적절히 이루어지고 있습니다.


13-20: 도메인 변환 로직이 명확합니다.

엔티티에서 도메인 모델로의 변환 로직이 간결하고 명확합니다. 모든 필드가 적절히 매핑되어 있습니다.

src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt (1)

11-18: 스케줄러 컴포넌트 구조가 적절합니다.

스케줄러 클래스가 @Component 어노테이션으로 적절히 등록되어 있으며, 필요한 의존성이 생성자 주입 방식으로 잘 처리되어 있습니다. 로거 설정도 잘 되어 있습니다.

src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt (2)

8-11: 적절한 리포지토리 구현입니다.

@Repository 어노테이션으로 적절히 리포지토리 빈이 등록되어 있으며, 생성자 주입을 통해 JPA 리포지토리를 의존성으로 받고 있습니다.


12-14: existsByUserIpAndDate 메소드가 잘 구현되어 있습니다.

JPA 리포지토리에 위임하는 방식으로 메소드가 간결하게 구현되어 있습니다.

Comment on lines +73 to +78
object Redis {
private const val REDIS_VERSION = "2.7.7"
private const val BASE = "org.springframework.boot"

val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$REDIS_VERSION"
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Redis 의존성 정의가 추가되었습니다.

Redis 통합을 위한 의존성이 적절하게 추가되었습니다. 다른 의존성 객체와 동일한 패턴을 잘 따르고 있습니다.

단, Spring Boot Redis Starter의 버전(2.7.7)이 현재 최신 버전보다 낮을 수 있습니다. 프로젝트의 Spring Boot 버전과 일치하는지 확인하세요.


🏁 Script executed:

#!/bin/bash
# Spring Boot 버전과 Redis 버전이 일치하는지 확인합니다
grep -n "VERSION = Plugin.SPRING_BOOT.version" buildSrc/src/main/kotlin/Dependency.kt
grep -n "REDIS_VERSION" buildSrc/src/main/kotlin/Dependency.kt

Length of output: 346


추가로 Plugin.SPRING_BOOT.version이 실제로 어떤 값인지 확인하기 위해 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# buildSrc/src/main/kotlin 폴더에서 SPRING_BOOT 정의 및 버전 확인
grep -R "SPRING_BOOT" -n buildSrc/src/main/kotlin

Length of output: 239


Spring Boot 버전 불일치: Redis 의존성 수정 필요

Redis 스타터 버전(2.7.7)이 프로젝트의 Spring Boot 버전(3.4.1)과 일치하지 않아 호환성 문제가 발생할 수 있습니다. REDIS_VERSION을 제거하고 기존 VERSION 상수를 사용하거나 Plugin.SPRING_BOOT.version을 참조하도록 변경해주세요.

  • buildSrc/src/main/kotlin/Dependency.kt (73–78번째 줄)
-    object Redis {
-        private const val REDIS_VERSION = "2.7.7"
-        private const val BASE = "org.springframework.boot"
-
-        val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$REDIS_VERSION"
-    }
+    object Redis {
+        private const val BASE = "org.springframework.boot"
+
+        val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$VERSION"
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
object Redis {
private const val REDIS_VERSION = "2.7.7"
private const val BASE = "org.springframework.boot"
val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$REDIS_VERSION"
}
object Redis {
private const val BASE = "org.springframework.boot"
val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$VERSION"
}

Comment on lines +6 to +10
interface VisitorRepository {

fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean
fun save(visitor: VisitorEntity): VisitorEntity
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

헥사고날 아키텍처 원칙에 맞게 포트 인터페이스를 개선하세요.

이 인터페이스는 애플리케이션 계층(port)에 있지만 인프라스트럭처 계층의 엔티티(VisitorEntity)를 직접 사용하고 있습니다. 헥사고날 아키텍처에서는 애플리케이션 계층이 인프라스트럭처 계층에 의존하지 않아야 합니다.

다음과 같이 수정하는 것을 권장합니다:

-import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity
+import com.coffee.api.cafe.domain.Visitor
 import java.time.LocalDate

 interface VisitorRepository {

     fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean
-    fun save(visitor: VisitorEntity): VisitorEntity
+    fun save(visitor: Visitor): Visitor
 }

이렇게 하면 애플리케이션 계층은 도메인 모델만 사용하게 되고, 엔티티와 도메인 모델 간의 변환은 인프라스트럭처 계층의 구현체에서 담당하게 됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface VisitorRepository {
fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean
fun save(visitor: VisitorEntity): VisitorEntity
}
import com.coffee.api.cafe.domain.Visitor
import java.time.LocalDate
interface VisitorRepository {
fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean
fun save(visitor: Visitor): Visitor
}

Comment on lines +19 to +29
@Id
var id: UUID = id
protected set

var userIp: String = userIp
protected set

var userAgent: String = userAgent
protected set
var date: LocalDate = date
protected set
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

필드 제약조건 추가 필요

현재 userIpuserAgent 필드에 길이 제약이 없습니다. 이로 인해 예상치 못하게 긴 값이 저장될 경우 데이터베이스 문제가 발생할 수 있습니다.

    @Id
    var id: UUID = id
        protected set

+   @Column(length = 45)
    var userIp: String = userIp
        protected set

+   @Column(length = 255)
    var userAgent: String = userAgent
        protected set
    var date: LocalDate = date
        protected set

또한 조회 성능 향상을 위해 인덱스 추가를 고려해보세요:

@Entity
@Table(name = "visitors")
+@Index(name = "idx_visitor_ip_date", columnList = "userIp,date", unique = true)
class VisitorEntity(

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +19 to +21
@Scheduled(initialDelay = 3000000, fixedDelay = 3000000)
fun updateVisitorData() {
val keys = redisTemplate.keys("*_*") ?: return
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

스케줄링 설정 개선 필요

고정된 지연 시간(3,000,000ms = 50분)이 하드코딩되어 있습니다. 이는 환경별로 쉽게 조정할 수 없으므로 설정 파일에서 값을 읽어오는 방식으로 개선하는 것이 좋습니다. 또한 Redis 키 검색 패턴 "_"는 의도하지 않은 키와 일치할 가능성이 있습니다.

-@Scheduled(initialDelay = 3000000, fixedDelay = 3000000)
+@Scheduled(initialDelayString = "${visitor.scheduler.delay:3000000}", fixedDelayString = "${visitor.scheduler.delay:3000000}")
fun updateVisitorData() {
-   val keys = redisTemplate.keys("*_*") ?: return
+   val keys = redisTemplate.keys("visitor:*_*") ?: return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Scheduled(initialDelay = 3000000, fixedDelay = 3000000)
fun updateVisitorData() {
val keys = redisTemplate.keys("*_*") ?: return
@Scheduled(
initialDelayString = "\${visitor.scheduler.delay:3000000}",
fixedDelayString = "\${visitor.scheduler.delay:3000000}"
)
fun updateVisitorData() {
val keys = redisTemplate.keys("visitor:*_*") ?: return
//
}

Comment on lines +23 to +38
for (key in keys) {
val parts = key.split("_")
if (parts.size != 2) continue

val userIp = parts[0]
val date = runCatching { LocalDate.parse(parts[1]) }.getOrNull() ?: continue

val userAgent = redisTemplate.opsForValue().get(key) ?: continue

if (!visitorRepository.existsByUserIpAndDate(userIp, date)) {
val visitor = VisitorEntity.of(userIp, userAgent, date)
visitorRepository.save(visitor)
}

redisTemplate.delete(key)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

방문자 데이터 처리 로직에 로깅 및 오류 처리 추가 필요

현재 방문자 데이터 처리 로직은 기본적인 기능은 제공하지만, 다음과 같은 개선이 필요합니다:

  1. 로깅이 없어 모니터링과 디버깅이 어렵습니다.
  2. 데이터베이스 연산에 대한 예외 처리가 없습니다.
  3. 대량의 키 세트를 처리할 때 페이징 처리가 없습니다.
  4. Redis 키 접근의 동시성 문제에 대한 처리가 없습니다.
fun updateVisitorData() {
    val keys = redisTemplate.keys("*_*") ?: return
+   log.info("Found {} visitor keys to process", keys.size)
+   var processed = 0
+   var failed = 0

    for (key in keys) {
+       try {
            val parts = key.split("_")
            if (parts.size != 2) continue

            val userIp = parts[0]
            val date = runCatching { LocalDate.parse(parts[1]) }.getOrNull() ?: continue

            val userAgent = redisTemplate.opsForValue().get(key) ?: continue
+           log.debug("Processing visitor: IP={}, date={}", userIp, date)

            if (!visitorRepository.existsByUserIpAndDate(userIp, date)) {
                val visitor = VisitorEntity.of(userIp, userAgent, date)
                visitorRepository.save(visitor)
+               processed++
            }

            redisTemplate.delete(key)
+       } catch (e: Exception) {
+           log.error("Error processing visitor key {}: {}", key, e.message, e)
+           failed++
+       }
    }
+   log.info("Visitor processing completed: {} processed, {} failed", processed, failed)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (key in keys) {
val parts = key.split("_")
if (parts.size != 2) continue
val userIp = parts[0]
val date = runCatching { LocalDate.parse(parts[1]) }.getOrNull() ?: continue
val userAgent = redisTemplate.opsForValue().get(key) ?: continue
if (!visitorRepository.existsByUserIpAndDate(userIp, date)) {
val visitor = VisitorEntity.of(userIp, userAgent, date)
visitorRepository.save(visitor)
}
redisTemplate.delete(key)
}
fun updateVisitorData() {
val keys = redisTemplate.keys("*_*") ?: return
log.info("Found {} visitor keys to process", keys.size)
var processed = 0
var failed = 0
for (key in keys) {
try {
val parts = key.split("_")
if (parts.size != 2) continue
val userIp = parts[0]
val date = runCatching { LocalDate.parse(parts[1]) }
.getOrNull() ?: continue
val userAgent = redisTemplate.opsForValue().get(key) ?: continue
log.debug("Processing visitor: IP={}, date={}", userIp, date)
if (!visitorRepository.existsByUserIpAndDate(userIp, date)) {
val visitor = VisitorEntity.of(userIp, userAgent, date)
visitorRepository.save(visitor)
processed++
}
redisTemplate.delete(key)
} catch (e: Exception) {
log.error("Error processing visitor key {}: {}", key, e.message, e)
failed++
}
}
log.info("Visitor processing completed: {} processed, {} failed", processed, failed)
}

Comment on lines +22 to +26
val valueOperations: ValueOperations<String, String> = redisTemplate.opsForValue()

if (!redisTemplate.hasKey(key)) {
valueOperations.set(key, userAgent)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Redis 작업에 대한 예외 처리가 필요합니다.

Redis 서버 연결 실패나 작업 중 예외가 발생할 경우 애플리케이션이 중단될 수 있습니다. try-catch 블록을 추가하여 예외를 적절히 처리해야 합니다.

-if (!redisTemplate.hasKey(key)) {
-    valueOperations.set(key, userAgent)
-}
+try {
+    if (!redisTemplate.hasKey(key)) {
+        valueOperations.set(key, userAgent)
+        redisTemplate.expire(key, Duration.ofDays(2))
+    }
+} catch (e: Exception) {
+    // Redis 연결 실패 시에도 요청은 계속 처리되도록 함
+    logger.error("Redis 작업 중 오류 발생: ${e.message}", e)
+}

Comment on lines +19 to +23
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val uri = URI(redisUrl)
return LettuceConnectionFactory(uri.host, uri.port)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redis 연결에 보안 및 추가 설정이 필요합니다.

현재 구현은 Redis URL에서 호스트와 포트만 파싱하고 있으며, 비밀번호, SSL 설정, 연결 타임아웃 등이 고려되지 않았습니다. 프로덕션 환경에서는 이러한 설정이 중요합니다.

@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
    val uri = URI(redisUrl)
-   return LettuceConnectionFactory(uri.host, uri.port)
+   val redisStandaloneConfiguration = RedisStandaloneConfiguration(uri.host, uri.port)
+   
+   // 비밀번호가 있는 경우 설정
+   if (uri.userInfo != null) {
+       val userInfo = uri.userInfo.split(":")
+       if (userInfo.size > 1) {
+           redisStandaloneConfiguration.password = RedisPassword.of(userInfo[1])
+       }
+   }
+   
+   val clientConfig = LettuceClientConfiguration.builder()
+       .commandTimeout(Duration.ofSeconds(5))
+       .shutdownTimeout(Duration.ZERO)
+       .build()
+       
+   return LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val uri = URI(redisUrl)
return LettuceConnectionFactory(uri.host, uri.port)
}
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val uri = URI(redisUrl)
// 기존 LettuceConnectionFactory 대신 RedisStandaloneConfiguration + LettuceClientConfiguration 사용
val redisStandaloneConfiguration = RedisStandaloneConfiguration(uri.host, uri.port)
// 비밀번호가 있는 경우 설정
uri.userInfo?.let { userInfo ->
userInfo.split(":").takeIf { it.size > 1 }?.get(1)?.let { pwd ->
redisStandaloneConfiguration.password = RedisPassword.of(pwd)
}
}
val clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(5))
.shutdownTimeout(Duration.ZERO)
.build()
return LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig)
}

Comment on lines +25 to +30
@Bean
fun redisTemplate(): RedisTemplate<Any, Any> {
val template = RedisTemplate<Any, Any>()
template.connectionFactory = redisConnectionFactory()
return template
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

RedisTemplate 시리얼라이저 및 의존성 주입 개선이 필요합니다.

현재 RedisTemplate 구현은 적절한 시리얼라이저를 설정하지 않고 있으며, redisConnectionFactory() 메서드를 직접 호출하고 있습니다. 이는 의존성 주입 원칙에 맞지 않으며 테스트를 어렵게 만듭니다.

@Bean
-fun redisTemplate(): RedisTemplate<Any, Any> {
-    val template = RedisTemplate<Any, Any>()
-    template.connectionFactory = redisConnectionFactory()
+fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, String> {
+    val template = RedisTemplate<String, String>()
+    template.connectionFactory = redisConnectionFactory
+    
+    // 직렬화 설정
+    val stringSerializer = StringRedisSerializer()
+    template.keySerializer = stringSerializer
+    template.valueSerializer = stringSerializer
+    template.hashKeySerializer = stringSerializer
+    template.hashValueSerializer = stringSerializer
+    
+    // 트랜잭션 활성화
+    template.setEnableTransactionSupport(true)
    return template
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
fun redisTemplate(): RedisTemplate<Any, Any> {
val template = RedisTemplate<Any, Any>()
template.connectionFactory = redisConnectionFactory()
return template
}
@Bean
fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, String> {
val template = RedisTemplate<String, String>()
template.connectionFactory = redisConnectionFactory
// 직렬화 설정
val stringSerializer = StringRedisSerializer()
template.keySerializer = stringSerializer
template.valueSerializer = stringSerializer
template.hashKeySerializer = stringSerializer
template.hashValueSerializer = stringSerializer
// 트랜잭션 활성화
template.setEnableTransactionSupport(true)
return template
}

Comment on lines +12 to +17
@Configuration
@EnableRedisRepositories
class RedisConfig(
@Value("\${spring.data.redis.url}")
private val redisUrl: String
) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redis 연결 오류 처리가 필요합니다.

Redis 서버에 연결할 수 없는 경우에 대한 오류 처리가 없습니다. 특히 로컬 개발 환경에서 Redis 없이 애플리케이션이 시작될 수 있도록 조건부 설정을 고려해야 합니다.

@Configuration
@EnableRedisRepositories
+@ConditionalOnProperty(
+    name = ["spring.data.redis.enabled"],
+    havingValue = "true",
+    matchIfMissing = true
+)
class RedisConfig(
    @Value("\${spring.data.redis.url}")
    private val redisUrl: String
) {
+    private val logger = LoggerFactory.getLogger(RedisConfig::class.java)
+    
+    @PostConstruct
+    fun logRedisConnection() {
+        try {
+            val uri = URI(redisUrl)
+            logger.info("Redis 연결 구성: 호스트={}, 포트={}", uri.host, uri.port)
+        } catch (e: Exception) {
+            logger.warn("Redis URL 파싱 중 오류 발생: ${e.message}")
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Configuration
@EnableRedisRepositories
class RedisConfig(
@Value("\${spring.data.redis.url}")
private val redisUrl: String
) {
@Configuration
@EnableRedisRepositories
@ConditionalOnProperty(
name = ["spring.data.redis.enabled"],
havingValue = "true",
matchIfMissing = true
)
class RedisConfig(
@Value("\${spring.data.redis.url}")
private val redisUrl: String
) {
private val logger = LoggerFactory.getLogger(RedisConfig::class.java)
@PostConstruct
fun logRedisConnection() {
try {
val uri = URI(redisUrl)
logger.info("Redis 연결 구성: 호스트={}, 포트={}", uri.host, uri.port)
} catch (e: Exception) {
logger.warn("Redis URL 파싱 중 오류 발생: ${e.message}")
}
}
}

Comment on lines +9 to +14
class Visitor private constructor(
override val id: Id,
val userIp: String,
val userAgent: String,
val date: LocalDate
) : AbstractDomain<Visitor, Visitor.Id>() {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 값에 대한 유효성 검사가 필요합니다.

사용자 IP 주소와 User-Agent에 대한 유효성 검사가 없습니다. 이는 잘못된 데이터가 도메인 모델에 저장될 수 있다는 것을 의미합니다.

class Visitor private constructor(
    override val id: Id,
    val userIp: String,
    val userAgent: String,
    val date: LocalDate
) : AbstractDomain<Visitor, Visitor.Id>() {
+
+    init {
+        require(userIp.isNotBlank()) { "사용자 IP는 비어 있을 수 없습니다" }
+        require(userAgent.isNotBlank()) { "User-Agent는 비어 있을 수 없습니다" }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Visitor private constructor(
override val id: Id,
val userIp: String,
val userAgent: String,
val date: LocalDate
) : AbstractDomain<Visitor, Visitor.Id>() {
class Visitor private constructor(
override val id: Id,
val userIp: String,
val userAgent: String,
val date: LocalDate
) : AbstractDomain<Visitor, Visitor.Id>() {
init {
require(userIp.isNotBlank()) { "사용자 IP는 비어 있을 수 없습니다" }
require(userAgent.isNotBlank()) { "User-Agent는 비어 있을 수 없습니다" }
}
// ... rest of the class ...
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c77118f and 8b7d758.

📒 Files selected for processing (4)
  • .github/workflows/deploy.yml (1 hunks)
  • .gitignore (1 hunks)
  • src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt (1 hunks)
  • src/main/kotlin/com/coffee/api/config/RedisConfig.kt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/kotlin/com/coffee/api/config/RedisConfig.kt
  • src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt

Comment on lines +31 to +36
- name: create application-prod.yml file
run: echo "${{ secrets.APPLICATION_PROD_YML }}" > src/main/resources/application-prod.yml

- name: create application-local.yml file
run: echo "${{ secrets.APPLICATION_LOCAL_YML }}" > src/main/resources/application-local.yml

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

YAML 프로필 파일 생성 방법 개선 및 Secret 적용 검증 요청

  • 멀티라인 YAML(인덴트, 주석 등)을 완벽히 유지하려면 echo 대신 cat <<EOF 블록 또는 printf '%s\n'를 사용하세요.
  • 각 파일 생성 시 > 연산자를 통일하여 덮어쓰기를 사용하고, 불필요한 빈 라인이 생기지 않도록 합니다.
  • GitHub Secrets(APPLICATION_PROD_YML, APPLICATION_LOCAL_YML)가 올바르게 설정되어 있는지 확인해주세요.

예시:

- name: create application‑prod.yml file
  run: |
    cat <<EOF > src/main/resources/application-prod.yml
${{ secrets.APPLICATION_PROD_YML }}
EOF

- name: create application‑local.yml file
  run: |
    cat <<EOF > src/main/resources/application-local.yml
${{ secrets.APPLICATION_LOCAL_YML }}
EOF

.github/workflows/deploy.yml (31–36) 멀티라인 YAML 생성 방식 개선 및 Secrets 검증 요청

  • echo 대신 cat <<EOF 또는 printf '%s\n'를 사용해 멀티라인 YAML(인덴트·주석 등)이 정확히 보존되도록 변경하세요.
  • 파일 생성 시 > 연산자로 덮어쓰기를 통일해 불필요한 빈 라인이 삽입되지 않도록 합니다.
  • GitHub Secrets(APPLICATION_PROD_YML, APPLICATION_LOCAL_YML)가 올바르게 설정되어 있는지 반드시 확인해주세요.

예시 수정안:

- name: create application-prod.yml file
  run: |
    cat <<EOF > src/main/resources/application-prod.yml
${{ secrets.APPLICATION_PROD_YML }}
EOF

- name: create application-local.yml file
  run: |
    cat <<EOF > src/main/resources/application-local.yml
${{ secrets.APPLICATION_LOCAL_YML }}
EOF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEAT] ElastiCache 연동 및 일일 방문자 수 집계 기능 구현
1 participant