Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ jobs:
touch ./src/main/resources/application.yml
echo "${{ secrets.APPLICATION_YML }}" >> src/main/resources/application.yml

- 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

Comment on lines +31 to +36
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

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ out/

.env

src/main/resources/application.yml
src/main/resources/application.yml
src/main/resources/application-local.yml
src/main/resources/application-prod.yml
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ dependencies {
implementation(Dependency.JDSL.SPRING_DATA_JPA_SUPPORTER)

// Discord
implementation (Dependency.Discord.WEB_HOOK)
implementation(Dependency.Discord.WEB_HOOK)

// Redis
implementation(Dependency.Redis.STARTER_DATA)
}

kotlin {
Expand Down
Empty file modified buildSrc/gradlew
100755 → 100644
Empty file.
7 changes: 7 additions & 0 deletions buildSrc/src/main/kotlin/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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"
}

}
Empty file modified gradlew
100755 → 100644
Empty file.
Empty file modified scripts/local-develop-env-reset
100755 → 100644
Empty file.
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
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
}

41 changes: 41 additions & 0 deletions src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt
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
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 ...
}


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
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.



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
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 주소 추출 방식을 개선해야 합니다.

request.remoteAddr는 프록시나 로드 밸런서 뒤에 있을 경우 실제 클라이언트 IP를 얻지 못할 수 있습니다. 일반적으로 'X-Forwarded-For' 또는 'X-Real-IP' 헤더를 확인하는 것이 더 신뢰성이 있습니다.

-val userIp = request.remoteAddr
+val userIp = request.getHeader("X-Forwarded-For")?.split(",")?.get(0)
+    ?: request.getHeader("X-Real-IP")
+    ?: request.remoteAddr
📝 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
val userIp = request.remoteAddr
// 기존 방식: 단순히 remoteAddr 만 사용
- val userIp = request.remoteAddr
+ val userIp = request.getHeader("X-Forwarded-For")?.split(",")?.get(0)
+ ?: request.getHeader("X-Real-IP")
+ ?: request.remoteAddr

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
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)
+}


return true
}
Comment on lines +16 to +29
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 키에 만료 시간(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
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
//
}


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
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)
}

}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/com/coffee/api/config/RedisConfig.kt
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
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}")
}
}
}


@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val uri = URI(redisUrl)
return LettuceConnectionFactory(uri.host, uri.port)
}
Comment on lines +20 to +24
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)
}


@Bean
fun redisTemplate(): RedisTemplate<Any, Any> {
val template = RedisTemplate<Any, Any>()
template.connectionFactory = redisConnectionFactory()
return template
}
Comment on lines +26 to +31
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
}

}