-
Notifications
You must be signed in to change notification settings - Fork 0
[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
base: develop
Are you sure you want to change the base?
Conversation
""" Walkthrough이번 변경에서는 Redis(ElastiCache) 연동 및 일일 방문자 수 집계 기능이 도입되었습니다. Redis Starter 의존성이 추가되고, 방문자 정보를 도메인 및 엔티티 계층에 설계하여 JPA 및 Redis를 통한 방문자 데이터 저장과 집계를 구현합니다. 방문자 요청 시 Interceptor를 통해 Redis에 기록하고, 스케줄러가 주기적으로 Redis의 방문자 데이터를 영속 저장소로 옮깁니다. Redis 연결을 위한 Spring 설정 클래스도 추가되었습니다. Changes
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
Assessment against linked issues
Poem
Tip ⚡💬 Agentic Chat (Pro Plan, General Availability)
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
📒 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 리포지토리에 위임하는 방식으로 메소드가 간결하게 구현되어 있습니다.
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" | ||
} |
There was a problem hiding this comment.
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.
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" | |
} |
interface VisitorRepository { | ||
|
||
fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean | ||
fun save(visitor: VisitorEntity): VisitorEntity | ||
} |
There was a problem hiding this comment.
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.
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 | |
} |
@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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
필드 제약조건 추가 필요
현재 userIp
와 userAgent
필드에 길이 제약이 없습니다. 이로 인해 예상치 못하게 긴 값이 저장될 경우 데이터베이스 문제가 발생할 수 있습니다.
@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.
@Scheduled(initialDelay = 3000000, fixedDelay = 3000000) | ||
fun updateVisitorData() { | ||
val keys = redisTemplate.keys("*_*") ?: return |
There was a problem hiding this comment.
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.
@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 | |
// … | |
} |
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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
방문자 데이터 처리 로직에 로깅 및 오류 처리 추가 필요
현재 방문자 데이터 처리 로직은 기본적인 기능은 제공하지만, 다음과 같은 개선이 필요합니다:
- 로깅이 없어 모니터링과 디버깅이 어렵습니다.
- 데이터베이스 연산에 대한 예외 처리가 없습니다.
- 대량의 키 세트를 처리할 때 페이징 처리가 없습니다.
- 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.
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) | |
} |
val valueOperations: ValueOperations<String, String> = redisTemplate.opsForValue() | ||
|
||
if (!redisTemplate.hasKey(key)) { | ||
valueOperations.set(key, userAgent) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
+}
@Bean | ||
fun redisConnectionFactory(): RedisConnectionFactory { | ||
val uri = URI(redisUrl) | ||
return LettuceConnectionFactory(uri.host, uri.port) | ||
} |
There was a problem hiding this comment.
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.
@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) | |
} |
@Bean | ||
fun redisTemplate(): RedisTemplate<Any, Any> { | ||
val template = RedisTemplate<Any, Any>() | ||
template.connectionFactory = redisConnectionFactory() | ||
return template | ||
} |
There was a problem hiding this comment.
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.
@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 | |
} |
@Configuration | ||
@EnableRedisRepositories | ||
class RedisConfig( | ||
@Value("\${spring.data.redis.url}") | ||
private val redisUrl: String | ||
) { |
There was a problem hiding this comment.
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.
@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}") | |
} | |
} | |
} |
class Visitor private constructor( | ||
override val id: Id, | ||
val userIp: String, | ||
val userAgent: String, | ||
val date: LocalDate | ||
) : AbstractDomain<Visitor, Visitor.Id>() { |
There was a problem hiding this comment.
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.
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 ... | |
} |
There was a problem hiding this 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
📒 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
- 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 | ||
|
There was a problem hiding this comment.
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
관련 이슈
close #60
주요 변경 사항
RedisInterceptor
를 구현했습니다.preHandle
단계에서 Redis에서 키 값을 확인하고, 조건에 따라 요청을 차단합니다.WebMvcConfigurer
를 통해/api/**
경로에 대해 Interceptor를 등록했습니다.RedisInterceptor
는adapter/in/restapi/interceptor
에 위치시켰습니다.기타
Summary by CodeRabbit
Summary by CodeRabbit