-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -69,4 +69,11 @@ object Dependency { | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| val WEB_HOOK = "$BASE:spring-boot-starter-webflux" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+73
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRedis 의존성 정의가 추가되었습니다. 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.ktLength of output: 346 추가로 🏁 Script executed: #!/bin/bash
# buildSrc/src/main/kotlin 폴더에서 SPRING_BOOT 정의 및 버전 확인
grep -R "SPRING_BOOT" -n buildSrc/src/main/kotlinLength of output: 239 Spring Boot 버전 불일치: Redis 의존성 수정 필요 Redis 스타터 버전(
- 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
Suggested change
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||||||||||||||||||||||||
| package com.coffee.api.cafe.application.port.outbound | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity | ||||||||||||||||||||||||||||
| import java.time.LocalDate | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| interface VisitorRepository { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean | ||||||||||||||||||||||||||||
| fun save(visitor: VisitorEntity): VisitorEntity | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 헥사고날 아키텍처 원칙에 맞게 포트 인터페이스를 개선하세요. 이 인터페이스는 애플리케이션 계층(port)에 있지만 인프라스트럭처 계층의 엔티티( 다음과 같이 수정하는 것을 권장합니다: -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
Suggested change
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||
| 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.* | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class Visitor private constructor( | ||||||||||||||||||||||||||||||||||||||||||
| override val id: Id, | ||||||||||||||||||||||||||||||||||||||||||
| val userIp: String, | ||||||||||||||||||||||||||||||||||||||||||
| val userAgent: String, | ||||||||||||||||||||||||||||||||||||||||||
| val date: LocalDate | ||||||||||||||||||||||||||||||||||||||||||
| ) : AbstractDomain<Visitor, Visitor.Id>() { | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||
| @JsonCreator | ||||||||||||||||||||||||||||||||||||||||||
| fun create( | ||||||||||||||||||||||||||||||||||||||||||
| id: UUID, | ||||||||||||||||||||||||||||||||||||||||||
| userIp: String, | ||||||||||||||||||||||||||||||||||||||||||
| userAgent: String, | ||||||||||||||||||||||||||||||||||||||||||
| date: LocalDate | ||||||||||||||||||||||||||||||||||||||||||
| ): Visitor { | ||||||||||||||||||||||||||||||||||||||||||
| return Visitor( | ||||||||||||||||||||||||||||||||||||||||||
| id = UUIDTypeId.from(id), | ||||||||||||||||||||||||||||||||||||||||||
| userIp = userIp, | ||||||||||||||||||||||||||||||||||||||||||
| userAgent = userAgent, | ||||||||||||||||||||||||||||||||||||||||||
| date = date | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| operator fun invoke( | ||||||||||||||||||||||||||||||||||||||||||
| id: UUID, | ||||||||||||||||||||||||||||||||||||||||||
| userIp: String, | ||||||||||||||||||||||||||||||||||||||||||
| userAgent: String, | ||||||||||||||||||||||||||||||||||||||||||
| date: LocalDate | ||||||||||||||||||||||||||||||||||||||||||
| ): Visitor = create(id, userIp, userAgent, date) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| data class Id(override val value: UUID) : UUIDTypeId(value) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.coffee.api.cafe.infrastructure | ||
|
|
||
| import com.coffee.api.cafe.domain.Visitor | ||
| import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity | ||
| import com.coffee.api.common.infrastructure.persistence.DomainEntityConverter | ||
| import org.springframework.stereotype.Component | ||
|
|
||
| @Component | ||
| class VisitorConverter : DomainEntityConverter<Visitor, VisitorEntity>( | ||
| Visitor::class, | ||
| VisitorEntity::class | ||
| ) { | ||
| override fun toDomain(entity: VisitorEntity): Visitor { | ||
| return Visitor( | ||
| id = entity.id, | ||
| userIp = entity.userIp, | ||
| userAgent = entity.userAgent, | ||
| date = entity.date | ||
| ) | ||
| } | ||
|
|
||
| override fun toEntity(domain: Visitor): VisitorEntity { | ||
| return VisitorEntity( | ||
| id = domain.id.value, | ||
| userIp = domain.userIp, | ||
| userAgent = domain.userAgent, | ||
| date = domain.date | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.coffee.api.cafe.infrastructure.persistence.entity | ||
|
|
||
| import com.coffee.api.common.infrastructure.persistence.BaseEntity | ||
| import jakarta.persistence.Entity | ||
| import jakarta.persistence.Id | ||
| import jakarta.persistence.Table | ||
| import java.time.LocalDate | ||
| import java.util.UUID | ||
|
|
||
| @Entity | ||
| @Table(name = "visitors") | ||
| class VisitorEntity( | ||
| id: UUID, | ||
| userIp: String, | ||
| userAgent: String, | ||
| date: LocalDate | ||
| ) : BaseEntity() { | ||
|
|
||
| @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 | ||
|
Comment on lines
+19
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 필드 제약조건 추가 필요 현재 @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(
|
||
|
|
||
|
|
||
| companion object { | ||
| fun of(userIp: String, userAgent: String, date: LocalDate): VisitorEntity { | ||
| return VisitorEntity( | ||
| id = UUID.randomUUID(), | ||
| userIp = userIp, | ||
| userAgent = userAgent, | ||
| date = date | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.coffee.api.cafe.infrastructure.persistence.repository | ||
|
|
||
| import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity | ||
| import org.springframework.data.jpa.repository.JpaRepository | ||
| import java.time.LocalDate | ||
| import java.util.UUID | ||
|
|
||
| interface VisitorJpaRepository : JpaRepository<VisitorEntity, UUID> { | ||
| fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean | ||
|
|
||
| fun save(visitorEntity: VisitorEntity): VisitorEntity | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.coffee.api.cafe.infrastructure.persistence.repository | ||
|
|
||
| import com.coffee.api.cafe.application.port.outbound.VisitorRepository | ||
| import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity | ||
| import org.springframework.stereotype.Repository | ||
| import java.time.LocalDate | ||
|
|
||
| @Repository | ||
| class VisitorRepositoryImpl( | ||
| private val visitorJpaRepository: VisitorJpaRepository | ||
| ) : VisitorRepository { | ||
| override fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean { | ||
| return visitorJpaRepository.existsByUserIpAndDate(userIp, date) | ||
| } | ||
|
|
||
| override fun save(visitor: VisitorEntity): VisitorEntity { | ||
| return visitorJpaRepository.save(visitor) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||
| package com.coffee.api.cafe.presentation.adapter.`in`.restapi.interceptor | ||||||||||||||
|
|
||||||||||||||
| import jakarta.servlet.http.HttpServletRequest | ||||||||||||||
| import jakarta.servlet.http.HttpServletResponse | ||||||||||||||
| import org.springframework.data.redis.core.RedisTemplate | ||||||||||||||
| import org.springframework.data.redis.core.ValueOperations | ||||||||||||||
| import org.springframework.stereotype.Component | ||||||||||||||
| import org.springframework.web.servlet.HandlerInterceptor | ||||||||||||||
| import java.time.LocalDate | ||||||||||||||
|
|
||||||||||||||
| @Component | ||||||||||||||
| class SingleVisitInterceptor( | ||||||||||||||
| private val redisTemplate: RedisTemplate<String, String> | ||||||||||||||
| ) : HandlerInterceptor { | ||||||||||||||
|
|
||||||||||||||
| override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { | ||||||||||||||
| val userIp = request.remoteAddr | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 클라이언트 IP 주소 추출 방식을 개선해야 합니다.
-val userIp = request.remoteAddr
+val userIp = request.getHeader("X-Forwarded-For")?.split(",")?.get(0)
+ ?: request.getHeader("X-Real-IP")
+ ?: request.remoteAddr📝 Committable suggestion
Suggested change
|
||||||||||||||
| val userAgent = request.getHeader("User-Agent") | ||||||||||||||
| val today = LocalDate.now().toString() | ||||||||||||||
| val key = "${userIp}_${today}" | ||||||||||||||
|
|
||||||||||||||
| val valueOperations: ValueOperations<String, String> = redisTemplate.opsForValue() | ||||||||||||||
|
|
||||||||||||||
| if (!redisTemplate.hasKey(key)) { | ||||||||||||||
| valueOperations.set(key, userAgent) | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
+} |
||||||||||||||
|
|
||||||||||||||
| return true | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+16
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Redis 키에 만료 시간(TTL)을 설정해야 합니다. Redis 키에 TTL을 설정하지 않으면 메모리 사용량이 계속 증가할 수 있습니다. 방문자 데이터는 일별로 집계되므로, 키에 적절한 만료 시간을 설정하는 것이 좋습니다. if (!redisTemplate.hasKey(key)) {
valueOperations.set(key, userAgent)
+ redisTemplate.expire(key, Duration.ofDays(2))
} |
||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.coffee.api.cafe.presentation.adapter.`in`.scheduler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.coffee.api.cafe.application.port.outbound.VisitorRepository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.slf4j.LoggerFactory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.redis.core.RedisTemplate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.scheduling.annotation.Scheduled | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.LocalDate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VisitorScheduler( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val redisTemplate: RedisTemplate<String, String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val visitorRepository: VisitorRepository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val log = LoggerFactory.getLogger(this::class.java) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Scheduled(initialDelay = 3000000, fixedDelay = 3000000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun updateVisitorData() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val keys = redisTemplate.keys("*_*") ?: return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 방문자 데이터 처리 로직에 로깅 및 오류 처리 추가 필요 현재 방문자 데이터 처리 로직은 기본적인 기능은 제공하지만, 다음과 같은 개선이 필요합니다:
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.coffee.api.config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.redis.connection.RedisConnectionFactory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.redis.core.RedisTemplate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.redis.repository.configuration.EnableRedisRepositories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.URI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @EnableRedisRepositories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class RedisConfig( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Value("\${spring.data.redis.url}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val redisUrl: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun redisConnectionFactory(): RedisConnectionFactory { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val uri = URI(redisUrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return LettuceConnectionFactory(uri.host, uri.port) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun redisTemplate(): RedisTemplate<Any, Any> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val template = RedisTemplate<Any, Any>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template.connectionFactory = redisConnectionFactory() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return template | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 적용 검증 요청
echo대신cat <<EOF블록 또는printf '%s\n'를 사용하세요.>연산자를 통일하여 덮어쓰기를 사용하고, 불필요한 빈 라인이 생기지 않도록 합니다.APPLICATION_PROD_YML,APPLICATION_LOCAL_YML)가 올바르게 설정되어 있는지 확인해주세요.예시:
.github/workflows/deploy.yml (31–36) 멀티라인 YAML 생성 방식 개선 및 Secrets 검증 요청
echo대신cat <<EOF또는printf '%s\n'를 사용해 멀티라인 YAML(인덴트·주석 등)이 정확히 보존되도록 변경하세요.>연산자로 덮어쓰기를 통일해 불필요한 빈 라인이 삽입되지 않도록 합니다.APPLICATION_PROD_YML,APPLICATION_LOCAL_YML)가 올바르게 설정되어 있는지 반드시 확인해주세요.예시 수정안: