Conversation
Walkthrough새 CI/CD 워크플로우와 배포 스크립트 추가, Kafka 의존성 및 소비자/DTO/리포지토리/엔티티 도입, 웹푸시 패키지 이동 및 구독 업데이트 로직 변경, 환경별 YML·시크릿 추가, 테스트 import 경로 정리 및 .gitignore 항목 추가가 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Producer as 외부 프로듀서
participant Kafka as Kafka (notification)
participant App as Notification Service
note right of Kafka: 토픽: notification
Producer->>Kafka: publish(NotificationMessage)
Kafka-->>App: deliver(NotificationMessage)
rect rgba(230,245,255,0.5)
App->>App: @KafkaListener consume(message)
App->>NotificationChecker: check(recipient, recipientType)
NotificationChecker-->>App: enabledChannels (List)
App->>NotificationSender: send(message, enabledChannels)
end
sequenceDiagram
autonumber
actor Client
participant API as WebPushController
participant Svc as WebPushService
participant Repo as PushSubscriptionRepository
Client->>API: subscribe(request)
API->>Svc: subscribe(request)
Svc->>Repo: findByUserIdAndEndpoint(userId, endpoint)
alt 기존 구독 있음
Svc->>Repo: update existing (p256dh/auth 등) → save
Repo-->>Svc: updated entity
else 없음
Svc->>Repo: save(new entity)
Repo-->>Svc: created entity
end
Svc-->>API: SubscribeResponse
API-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
build.gradle (1)
30-31: Kafka/Mail 스타터 추가는 합리적입니다. 테스트 편의성 위해 spring-kafka-test도 함께 추가하세요.
- 컨슈머 동작·역직렬화 검증을 위해 내장 임베디드 브로커/유틸을 제공하므로 테스트 작성이 쉬워집니다.
다음 의존성을 테스트 스코프로 추가해 주세요:
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.kafka:spring-kafka' + testImplementation 'org.springframework.kafka:spring-kafka-test' }src/main/java/com/wellmeet/consumer/NotificationConsumer.java (1)
8-11: @requiredargsconstructor는 현재 불필요합니다.
- 주입받는 final 필드가 없어 효과가 없습니다. 유지하려면 의존성(메일 발송/알림 서비스 등)을 주입하거나, 제거하세요.
-@Slf4j -@Service -@RequiredArgsConstructor +@Slf4j +@Service public class NotificationConsumer {src/main/resources/application-local.yml (1)
38-45: Kafka 설정 중복 최소화 및 토픽명 프로퍼티 추가 권장
- 역직렬화/auto-offset-reset은 Boot 또는 수동 팩토리 한쪽만 단일 소스로 관리하는 것이 좋습니다(중복 방지).
- 리스너에서 토픽을 프로퍼티로 읽도록 바꿨으므로, 토픽 프로퍼티를 추가해 주세요.
kafka: bootstrap-servers: localhost:9092 consumer: group-id: notification-group auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer +notification: + topic: notificationCI에서 사용하는 테스트 프로파일(application-test.yml)이 없다면, 동일한
bootstrap-servers와group-id를 가진 테스트 전용 설정 파일 추가를 고려해 주세요..github/workflows/Dev_CI.yml (1)
86-87: 테스트 단계에 프로파일 지정으로 환경 결정성을 높이세요.
- Kafka/MySQL 연결 설정을 명확히 하려면 테스트 프로파일을 활성화하는 것이 안전합니다.
- - name: Run Tests With Gradle - run: ./gradlew test + - name: Run Tests With Gradle + run: SPRING_PROFILES_ACTIVE=test ./gradlew test원하시면 최소 설정의
application-test.yml템플릿을 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
.github/workflows/Dev_CI.yml(1 hunks)build.gradle(1 hunks)src/main/java/com/wellmeet/config/KafkaConfig.java(1 hunks)src/main/java/com/wellmeet/consumer/NotificationConsumer.java(1 hunks)src/main/resources/application-local.yml(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/wellmeet/consumer/NotificationConsumer.java (1)
src/main/java/com/wellmeet/WellmeetNotificationApplication.java (2)
SpringBootApplication(8-16)main(13-15)
🔇 Additional comments (1)
src/main/java/com/wellmeet/config/KafkaConfig.java (1)
20-24: 하드코딩된 groupId와 프로퍼티 동시 사용은 혼란을 유발합니다.
- 컨슈머 그룹은 프로퍼티만 신뢰하도록 하고, 리스너 애노테이션에서의 하드코딩은 제거하는 편이 안전합니다. NotificationConsumer에서 처리 제안 드립니다(아래 파일 코멘트 참고).
| MYSQL_ROOT_PASSWORD: "" | ||
| MYSQL_ALLOW_EMPTY_PASSWORD: "yes" | ||
| MYSQL_DATABASE: wellmeet_noti_test | ||
| ports: | ||
| - 3306:3306 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
MySQL DB 이름 불일치 가능성(wellmeet_noti_test vs wellmeet_noti)
- 워크플로우는
MYSQL_DATABASE: wellmeet_noti_test를 생성하지만,application-local.yml은wellmeet_noti를 가리킵니다. 테스트 프로파일이 없다면 커넥션 실패/빈 스키마 가능성이 있습니다.
테스트 프로파일 유무와 사용 DB를 확인해 주세요:
- 선택 1:
Run Tests단계에SPRING_PROFILES_ACTIVE=test를 설정하고application-test.yml에서wellmeet_noti_test사용. - 선택 2: 로컬 프로파일 대신 테스트에서 사용하도록
SPRING_PROFILES_ACTIVE=local을 지정.
🏁 Script executed:
#!/bin/bash
# 테스트 프로파일 및 DB URL 탐색
fd -HI "application-test.yml" || echo "⚠️ application-test.yml 없음"
rg -n "spring\.datasource\.url" -C2 src/main/resources || trueLength of output: 133
테스트·로컬 프로파일에서 실제로 사용 중인 DB 이름과 URL을 명확히 파악하기 위해 아래 스크립트를 실행해 주세요:
이 결과로 wellmeet_noti vs wellmeet_noti_test 참조 여부를 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
echo "===== application-local.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application-local.yml || true
echo "===== application-test.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application-test.yml || trueLength of output: 793
Run Tests 단계에서 SPRING_PROFILES_ACTIVE 설정 여부를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
echo "===== Run Tests 섹션 ====="
rg -n "Run Tests" -C5 .github/workflows/Dev_CI.yml || true
echo "===== SPRING_PROFILES_ACTIVE 설정 ====="
rg -n "SPRING_PROFILES_ACTIVE" -C3 .github/workflows/Dev_CI.yml || echo "⚠️ SPRING_PROFILES_ACTIVE 설정 없음"Length of output: 527
default 프로파일로 실행될 때 로드되는 application.yml의 datasource 설정 유무를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
echo "===== application.yml 파일 위치 확인 ====="
fd -HI src/main/resources/application.yml || echo "⚠️ application.yml 없음"
echo "===== application.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application.yml || trueLength of output: 359
Run Tests 단계에 SPRING_PROFILES_ACTIVE=test 설정
application-test.yml은 jdbc:mysql://localhost:3306/wellmeet_noti_test를, application-local.yml은 …/wellmeet_noti를 참조하지만 기본 프로파일(application.yml)이 없어 워크플로우에서 프로파일을 지정하지 않으면 잘못된 DB로 연결되거나 커넥션 실패가 발생합니다.
.github/workflows/Dev_CI.yml의 Run Tests With Gradle 단계에 아래 env 설정을 추가해 주세요:
- name: Run Tests With Gradle
run: ./gradlew test
+ env:
+ SPRING_PROFILES_ACTIVE: test📝 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.
| MYSQL_ROOT_PASSWORD: "" | |
| MYSQL_ALLOW_EMPTY_PASSWORD: "yes" | |
| MYSQL_DATABASE: wellmeet_noti_test | |
| ports: | |
| - 3306:3306 | |
| - name: Run Tests With Gradle | |
| run: ./gradlew test | |
| env: | |
| SPRING_PROFILES_ACTIVE: test |
🤖 Prompt for AI Agents
In .github/workflows/Dev_CI.yml around lines 25 to 29 the CI job doesn't set
SPRING_PROFILES_ACTIVE, so Gradle tests can pick the wrong profile/DB; add an
environment variable SPRING_PROFILES_ACTIVE=test to the "Run Tests With Gradle"
step's env block so the test profile (and application-test.yml DB URL) is used
during test execution.
| --health-cmd="curl -f http://localhost:8080/commands/stat || exit 1" | ||
| --health-interval=10s | ||
| --health-timeout=10s | ||
| --health-retries=5 |
There was a problem hiding this comment.
Zookeeper 헬스체크 커맨드가 잘못되었습니다.
- cp-zookeeper 기본 이미지에는 8080 HTTP 엔드포인트가 없습니다.
ruok체크로 교체하세요.
- --health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
+ --health-cmd="bash -c 'echo ruok | nc -w 2 localhost 2181 | grep imok'"
--health-interval=10s
--health-timeout=10s
--health-retries=5📝 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.
| --health-cmd="curl -f http://localhost:8080/commands/stat || exit 1" | |
| --health-interval=10s | |
| --health-timeout=10s | |
| --health-retries=5 | |
| --health-cmd="bash -c 'echo ruok | nc -w 2 localhost 2181 | grep imok'" | |
| --health-interval=10s | |
| --health-timeout=10s | |
| --health-retries=5 |
🤖 Prompt for AI Agents
.github/workflows/Dev_CI.yml lines 44-47: the current health-cmd targets a
non-existent HTTP endpoint on port 8080; replace it with a Zookeeper "ruok"
check against the Zookeeper client port (2181). Update the --health-cmd to run a
TCP check that sends "ruok" and verifies the "imok" response (for example use:
echo ruok | nc -w1 localhost 2181 | grep imok || exit 1), keeping the existing
--health-interval, --health-timeout, and --health-retries values.
| image: confluentinc/cp-kafka:7.0.1 | ||
| env: | ||
| KAFKA_BROKER_ID: 1 | ||
| KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 | ||
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT | ||
| KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 | ||
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Kafka 리스너 바인딩 명시(KAFKA_LISTENERS) 추가 권장
- 일부 환경에서
KAFKA_ADVERTISED_LISTENERS만 설정 시 부트 실패 사례가 있어, 리스너 바인딩을 명시하는 편이 안전합니다.
kafka:
image: confluentinc/cp-kafka:7.0.1
env:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092📝 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.
| image: confluentinc/cp-kafka:7.0.1 | |
| env: | |
| KAFKA_BROKER_ID: 1 | |
| KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 | |
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT | |
| KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 | |
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | |
| kafka: | |
| image: confluentinc/cp-kafka:7.0.1 | |
| env: | |
| KAFKA_BROKER_ID: 1 | |
| KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 | |
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT | |
| KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 | |
| KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 | |
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 |
🤖 Prompt for AI Agents
.github/workflows/Dev_CI.yml around lines 50 to 56: the workflow sets
KAFKA_ADVERTISED_LISTENERS but omits KAFKA_LISTENERS which can cause Kafka to
fail to bind in some environments; add an env entry KAFKA_LISTENERS set to
PLAINTEXT://0.0.0.0:9092 (or matching interface and port you intend to bind)
alongside the existing advertised listeners so the broker explicitly binds to
the correct network interface.
| @Bean | ||
| public ConsumerFactory<String, String> consumerFactory() { | ||
| Map<String, Object> props = new HashMap<>(); | ||
| props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); | ||
| props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); | ||
| props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | ||
| props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | ||
| props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); | ||
| return new DefaultKafkaConsumerFactory<>(props); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Boot 자동 구성과 중복 설정 제거 + 에러 핸들러 적용 권장
- 수동 props 구성은 application-local.yml과 중복이며, Boot의 Kafka 설정(에러 핸들러, 컨슈머 설정 등)을 일부 놓칠 수 있습니다.
다음처럼 Boot의 ConcurrentKafkaListenerContainerFactoryConfigurer를 이용해 공통 설정과 에러 처리(backoff)까지 적용해 주세요. 또한 코드의 AUTO_OFFSET_RESET 하드코딩을 제거하여 프로퍼티만 신뢰하게 만드는 것을 권장합니다.
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.DefaultErrorHandler;
+import org.springframework.util.backoff.FixedBackOff;
@@
- @Bean
- public ConsumerFactory<String, String> consumerFactory() {
- Map<String, Object> props = new HashMap<>();
- props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
- props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
- props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
- props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
- props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
- return new DefaultKafkaConsumerFactory<>(props);
- }
+ @Bean
+ public ConsumerFactory<String, String> consumerFactory() {
+ Map<String, Object> props = new HashMap<>();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ // auto.offset.reset은 application-*.yml만 신뢰 (중복 제거)
+ return new DefaultKafkaConsumerFactory<>(props);
+ }
@@
- @Bean
- public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
- ConcurrentKafkaListenerContainerFactory<String, String> factory =
- new ConcurrentKafkaListenerContainerFactory<>();
- factory.setConsumerFactory(consumerFactory());
- return factory;
- }
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
+ ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
+ ConsumerFactory<String, String> consumerFactory) {
+ ConcurrentKafkaListenerContainerFactory<String, String> factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ // Boot의 spring.kafka.* 설정을 공통으로 적용
+ configurer.configure(factory, consumerFactory);
+ // 단순 재시도(1s 간격, 최대 3회)
+ factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 3)));
+ return factory;
+ }📝 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 | |
| public ConsumerFactory<String, String> consumerFactory() { | |
| Map<String, Object> props = new HashMap<>(); | |
| props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); | |
| props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); | |
| props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | |
| props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | |
| props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); | |
| return new DefaultKafkaConsumerFactory<>(props); | |
| } | |
| // src/main/java/com/wellmeet/config/KafkaConfig.java | |
| import org.springframework.context.annotation.Configuration; | |
| import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; | |
| import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer; | |
| import org.springframework.kafka.core.ConsumerFactory; | |
| import org.springframework.kafka.core.DefaultKafkaConsumerFactory; | |
| import org.springframework.kafka.listener.DefaultErrorHandler; | |
| import org.springframework.util.backoff.FixedBackOff; | |
| import org.apache.kafka.clients.consumer.ConsumerConfig; | |
| import org.apache.kafka.common.serialization.StringDeserializer; | |
| @Configuration | |
| public class KafkaConfig { | |
| @Bean | |
| public ConsumerFactory<String, String> consumerFactory() { | |
| Map<String, Object> props = new HashMap<>(); | |
| props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); | |
| props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); | |
| props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | |
| props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | |
| // auto.offset.reset은 application-*.yml만 신뢰 (중복 제거) | |
| return new DefaultKafkaConsumerFactory<>(props); | |
| } | |
| @Bean | |
| public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory( | |
| ConcurrentKafkaListenerContainerFactoryConfigurer configurer, | |
| ConsumerFactory<String, String> consumerFactory) { | |
| ConcurrentKafkaListenerContainerFactory<String, String> factory = | |
| new ConcurrentKafkaListenerContainerFactory<>(); | |
| // Boot의 spring.kafka.* 설정을 공통으로 적용 | |
| configurer.configure(factory, consumerFactory); | |
| // 단순 재시도(1s 간격, 최대 3회) | |
| factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 3))); | |
| return factory; | |
| } | |
| } |
| @KafkaListener(topics = "notification", groupId = "notification-group") | ||
| public void consume(String message) { | ||
| log.info("Received message from notification topic: {}", message); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토픽/그룹 하드코딩 제거 + 메타데이터 포함 로깅으로 운용성 개선
- 프로퍼티 기반으로 외부화하여 환경별 유연성을 높이고, 파티션/오프셋까지 로깅하면 장애 추적이 쉬워집니다.
-import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
@@
- @KafkaListener(topics = "notification", groupId = "notification-group")
- public void consume(String message) {
- log.info("Received message from notification topic: {}", message);
+ @KafkaListener(
+ topics = "${notification.topic:notification}",
+ groupId = "${spring.kafka.consumer.group-id}"
+ )
+ public void consume(ConsumerRecord<String, String> record) {
+ log.info("Consumed reservation event: topic={}, partition={}, offset={}, key={}, value={}",
+ record.topic(), record.partition(), record.offset(), record.key(), record.value());
}메일/SMS/웹푸시 전달 로직을 붙이는 구현이 필요하면, 인터페이스와 간단한 어댑터(이벤트→도메인 DTO 변환) 초안을 제공하겠습니다.
📝 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.
| @KafkaListener(topics = "notification", groupId = "notification-group") | |
| public void consume(String message) { | |
| log.info("Received message from notification topic: {}", message); | |
| } | |
| import org.springframework.kafka.annotation.KafkaListener; | |
| import org.apache.kafka.clients.consumer.ConsumerRecord; | |
| @KafkaListener( | |
| topics = "${notification.topic:notification}", | |
| groupId = "${spring.kafka.consumer.group-id}" | |
| ) | |
| public void consume(ConsumerRecord<String, String> record) { | |
| log.info("Consumed reservation event: topic={}, partition={}, offset={}, key={}, value={}", | |
| record.topic(), | |
| record.partition(), | |
| record.offset(), | |
| record.key(), | |
| record.value()); | |
| } |
| mail: | ||
| host: smtp.gmail.com | ||
| port: 587 | ||
| username: ${secret.mail.username:test@example.com} | ||
| password: ${secret.mail.password:testpassword} | ||
| properties: | ||
| mail: | ||
| smtp: | ||
| auth: true | ||
| starttls: | ||
| enable: true | ||
| required: true | ||
| timeout: 5000 | ||
| connectiontimeout: 5000 | ||
| writetimeout: 5000 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
메일 설정은 합리적입니다. 로컬 시크릿 유출 방지 확인 및 타임아웃 단위 일관성 체크
${secret.*}기본값이 있어 안전합니다. 다만local-secret.yml이 .gitignore에 포함되어 있는지 확인해 주세요.timeout/connectiontimeout/writetimeout단위(ms) 일관성은 OK.
다음 스크립트로 시크릿 파일이 gitignore 되는지 확인해 주세요:
🏁 Script executed:
#!/bin/bash
rg -n "^local-secret\.yml$" .gitignore || echo "⚠️ .gitignore에 local-secret.yml 항목이 없습니다."Length of output: 133
local-secret.yml을 .gitignore에 추가하세요
현재 .gitignore에 local-secret.yml이 없어 시크릿 정보가 커밋될 위험이 있습니다. 해당 파일을 .gitignore에 추가해 주시기 바랍니다.
🤖 Prompt for AI Agents
In src/main/resources/application-local.yml around lines 23 to 37, sensitive
mail credentials are being referenced and you must prevent committing
local-secret.yml; add a line "local-secret.yml" to the repository's .gitignore,
then if local-secret.yml was already committed remove it from the index with git
rm --cached local-secret.yml and commit the change; ensure developers create or
update a local-secret.yml from a template placed in the repo (e.g.,
local-secret.yml.example) instead of committing real secrets.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
44-61: 업데이트 테스트의 의도 강화: “동일 엔티티 재사용”을 단언하세요.
현재 검증은 필드 값 변경만 보장합니다. 새 엔티티를 생성해도 통과할 수 있어 “업데이트” 의미가 약합니다. 기존 엔티티를 재사용했는지와 중복 생성이 없는지를 함께 단언해 주세요.SubscribeResponse response = webPushService.subscribe(userId, request); PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get(); assertAll( + () -> assertThat(response.subscriptionId()).isEqualTo(pushSubscription.getId()), () -> assertThat(subscription.getUserId()).isEqualTo(userId), () -> assertThat(subscription.getEndpoint()).isEqualTo(endpoint), () -> assertThat(subscription.getP256dh()).isEqualTo(request.p256dh()), - () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()) + () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()), + // 같은 유저/엔드포인트에 대해 레코드가 1건인지 확인해 “업데이트”语義를 강화 + () -> assertThat(pushSubscriptionRepository.count()).isEqualTo(1) );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
.github/workflows/Dev_CI.yml(1 hunks).gitignore(1 hunks)src/main/resources/application-test.yml(1 hunks)src/test/java/com/wellmeet/webpush/WebPushServiceTest.java(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
- .github/workflows/Dev_CI.yml
🧰 Additional context used
🧬 Code graph analysis (2)
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
src/test/java/com/wellmeet/webpush/WebPushControllerTest.java (5)
Nested(22-83)Test(25-44)WebPushControllerTest(20-164)Test(136-149)Nested(133-163)
src/main/resources/application-test.yml (2)
src/test/java/com/wellmeet/WellmeetNotificationApplicationTests.java (1)
SpringBootTest(6-12)src/test/java/com/wellmeet/BaseServiceTest.java (1)
ExtendWith(9-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
🔇 Additional comments (2)
src/main/resources/application-test.yml (1)
23-29: String 메시지 소비 시 StringDeserializer 유지
NotificationConsumer.consume 메서드가 String 타입 메시지를 수신하고 있으므로, 현재 StringDeserializer 설정을 그대로 사용해도 무방합니다. 향후 DTO 타입으로 변경 시 JsonDeserializer 및 trusted packages 설정을 적용하세요.src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
58-59: 업데이트 후 필드 값 단언은 적절합니다.
변경된 p256dh/auth가 저장소 값과 일치하는지 검증한 부분은 명확합니다.
| kafka: | ||
| bootstrap-servers: localhost:9092 | ||
| consumer: | ||
| group-id: notification-group | ||
| auto-offset-reset: earliest | ||
| key-deserializer: org.apache.kafka.common.serialization.StringDeserializer | ||
| value-deserializer: org.apache.kafka.common.serialization.StringDeserializer |
There was a problem hiding this comment.
🛠️ Refactor suggestion
테스트 안정성을 위해 group-id는 매 실행마다 난수화하고, 브로커 주소는 환경변수로 주입하세요.
고정 group-id(notification-group)는 오프셋이 누적되어 재실행 시 예측 불가 동작을 유발할 수 있습니다. 또한 로컬에서 카프카가 없으면 테스트가 즉시 실패합니다. 아래와 같이 수정하면 CI/로컬 모두에서 플레이키니스가 줄어듭니다.
kafka:
- bootstrap-servers: localhost:9092
+ bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
consumer:
- group-id: notification-group
+ group-id: wellmeet-test-${random.uuid}
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ listener:
+ # CI 부팅 레이스로 토픽이 늦게 생성될 때 실패를 줄입니다(필요 시 조정).
+ missing-topics-fatal: false추가로, @KafkaListener에 groupId를 하드코딩했다면 프로퍼티로 일원화하는 편이 유지보수에 유리합니다. 예: @KafkaListener(topics="notification", groupId="${spring.kafka.consumer.group-id}")
📝 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.
| kafka: | |
| bootstrap-servers: localhost:9092 | |
| consumer: | |
| group-id: notification-group | |
| auto-offset-reset: earliest | |
| key-deserializer: org.apache.kafka.common.serialization.StringDeserializer | |
| value-deserializer: org.apache.kafka.common.serialization.StringDeserializer | |
| kafka: | |
| bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} | |
| consumer: | |
| group-id: wellmeet-test-${random.uuid} | |
| auto-offset-reset: earliest | |
| key-deserializer: org.apache.kafka.common.serialization.StringDeserializer | |
| value-deserializer: org.apache.kafka.common.serialization.StringDeserializer | |
| listener: | |
| # CI 부팅 레이스로 토픽이 늦게 생성될 때 실패를 줄입니다(필요 시 조정). | |
| missing-topics-fatal: false |
🤖 Prompt for AI Agents
In src/main/resources/application-test.yml around lines 23-29, replace the
hardcoded kafka.bootstrap-servers and consumer.group-id with
environment/property-driven values: make bootstrap-servers read from an
environment variable (e.g. KAFKA_BOOTSTRAP_SERVERS) with a localhost:9092
fallback, and make group-id derive from a property that defaults to a generated
random value (e.g. Spring's random uuid) so each test run uses a unique group
id; also audit code for any @KafkaListener usages that hardcode groupId and
change them to reference the consumer.group-id property to centralize
configuration.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (11)
src/main/resources/application-dev.yml (6)
8-8: MySQL JDBC URL에 표준 파라미터 추가 및 인코딩/타임존 안정화 제안한글/이모지, 타임존, SSL 핸드셰이크 이슈 예방을 위해 JDBC URL에 권장 파라미터를 추가하는 것이 안전합니다. YAML에서
&포함 시 전체를 문자열로 감싸는 걸 잊지 마세요.- url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + url: "jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true"
16-16: Hibernate Dialect 명시값 점검 (Boot 3/Hibernate 6 기준)
org.hibernate.dialect.MySQLDialect는 버전별 추론이 가능하지만, MySQL 8 사용 시MySQL8Dialect를 명시하거나(또는 Dialect 자체를 제거해 자동감지에 맡기기) 하는 편이 일관됩니다.- dialect: org.hibernate.dialect.MySQLDialect + dialect: org.hibernate.dialect.MySQL8Dialect # 또는 아래처럼 완전 제거도 고려 # - dialect: org.hibernate.dialect.MySQLDialect
11-16: OSIV 비활성화 권장웹 계층 Lazy 로딩으로 인한 N+1/지연 예외를 줄이기 위해 Dev에서도 OSIV를 꺼두는 것을 권장합니다.
jpa: show-sql: true properties: hibernate: format_sql: true - dialect: org.hibernate.dialect.MySQLDialect + dialect: org.hibernate.dialect.MySQL8Dialect + open-in-view: false
23-37: Gmail SMTP 호환성 보강 (TLS 1.2 강제 권장)환경에 따라 TLS 프로토콜 미스매치가 간헐적으로 발생할 수 있어
ssl.protocols를 명시하는 것을 권장합니다.mail: smtp: auth: true starttls: enable: true required: true timeout: 5000 connectiontimeout: 5000 writetimeout: 5000 + ssl: + protocols: TLSv1.2
46-47: CORS 도메인 확장성단일 Origin만 허용 중입니다. 로컬/프론트 미러 포트(5173/4173 등)나 개발 도메인(예: *.dev)을 추가로 허용할 필요가 있다면 배열 형태로 관리하는 편이 낫습니다. 이 키가 커스텀 설정이면 해당 Bean에서 배열 지원 여부도 확인 바랍니다.
5-5: dev-secret.yml 관리 확인
classpath:dev-secret.yml을 CI에서 주입하는 구조라면, 저장소에는 커밋되지 않도록.gitignore포함 여부와 PR의 시크릿 노출 여부를 다시 한 번 점검해 주세요.scripts/dev/replace-new-version.sh (2)
1-1: 스クリ프트 안전성 강화: 방어적 Bash 옵션 추가예상치 못한 실패 시 조기에 종료되도록
set -euo pipefail을 추가하는 것을 권장합니다.#!/bin/bash +set -euo pipefail
30-36: 시작 실패 가시성 향상 (간단 헬스체크/포트 확인)백그라운드 실행 후 포트 리슨 여부를 짧게 확인하면 배포 실패를 빠르게 감지할 수 있습니다.
echo "애플리케이션이 백그라운드에서 실행되었습니다." echo "로그 확인: tail -f $APP_LOG_FILE" echo "=== 배포 완료 ===" +for i in {1..30}; do + if lsof -i:8080 -sTCP:LISTEN >/dev/null 2>&1; then + echo "서비스가 포트 8080에서 수신 중입니다." + break + fi + sleep 1 +done.github/workflows/Dev_CD.yml (3)
78-79: 배포 스크립트와 아티팩트 경로/권한 정합성 확인스크립트는
$HOME/app/app.jar(권장) 또는/home/ubuntu/app/app.jar(현재) 중 어느 쪽을 가정하는지 일치가 필요합니다. 스크립트 수정안대로$HOME/app를 쓰면 워크플로의 다운로드 경로(~/app)와 일치합니다. 실행 전 실행 권한을 부여하는 것도 안전합니다.- name: Replace application to latest - run: sudo sh ~/app/scripts/replace-new-version.sh + run: | + chmod +x ~/app/scripts/replace-new-version.sh || true + sudo bash ~/app/scripts/replace-new-version.sh
24-37: 시크릿 파일 주입 안정성멀티라인 시크릿의 줄바꿈 손상 방지를 위해
printf사용이나heredoc이 더 안전합니다.- - name: Setting dev-secret.yml - run: | - echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml + - name: Setting dev-secret.yml + run: | + printf "%s" "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
15-23: 동시 배포 충돌 방지(concurrency) 권장동일 브랜치로 연속 푸시 시 배포 경합을 방지하려면 concurrency를 추가해 주세요.
jobs: + concurrency: + group: dev-cd-${{ github.ref }} + cancel-in-progress: true test:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
.github/workflows/Dev_CD.yml(1 hunks).github/workflows/Dev_CI.yml(1 hunks)scripts/dev/replace-new-version.sh(1 hunks)src/main/resources/application-dev.yml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- .github/workflows/Dev_CI.yml
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/Dev_CD.yml
63-63: label "dev-owner" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2022", "windows-2019", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "ubuntu-20.04", "macos-latest", "macos-latest-xl", "macos-latest-xlarge", "macos-latest-large", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14", "macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file
(runner-label)
🪛 Shellcheck (0.10.0)
scripts/dev/replace-new-version.sh
[warning] 36-36: sudo doesn't affect redirects. Use ..| sudo tee file
(SC2024)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build-and-push
- GitHub Check: test / build-and-push
🔇 Additional comments (3)
src/main/resources/application-dev.yml (2)
19-22: schema.sql 항상 실행 설정의 영향 확인
spring.sql.init.mode: always는 Dev 부팅 때마다 스키마 초기화를 시도합니다. 샘플 데이터/DDL 중복 적용, 수동 마이그레이션 충돌 우려가 없는지 확인 바랍니다. Flyway/Liquibase 사용 계획이 있다면 해당 설정과의 우선순위도 점검이 필요합니다.
39-44: Kafka 역직렬화 방식 확인현재 StringDeserializer로 설정되어 있습니다. 예약 알림 페이로드가 JSON이면
JsonDeserializer및spring.json.trusted.packages설정이 필요합니다. 토픽 스키마/컨슈머 코드와 합치되는지 재확인 바랍니다..github/workflows/Dev_CD.yml (1)
61-64:runs-on: dev-owner만으로 해당 라벨의 셀프호스티드 러너에 매칭됩니다. self-hosted 라벨 추가는 권장사항일 뿐 필수 매칭 조건이 아닙니다. (docs.kkgithub.com, docs.github.com)Likely an incorrect or invalid review comment.
| - name: bootJar with Gradle | ||
| run: ./gradlew :api-owner:bootJar --info | ||
|
|
||
| - name: Change artifact file name | ||
| run: mv build/libs/*.jar build/libs/app.jar | ||
|
|
There was a problem hiding this comment.
bootJar 산출물 경로 불일치
:api-owner:bootJar를 빌드하면 JAR는 api-owner/build/libs에 생성됩니다. 현재 mv build/libs/*.jar ...는 잘못된 경로입니다. 아래처럼 서브모듈 경로를 사용하세요.
- - name: bootJar with Gradle
- run: ./gradlew :api-owner:bootJar --info
-
- - name: Change artifact file name
- run: mv build/libs/*.jar build/libs/app.jar
+ - name: bootJar with Gradle
+ run: ./gradlew :api-owner:bootJar --info
+
+ - name: Change artifact file name
+ run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar📝 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.
| - name: bootJar with Gradle | |
| run: ./gradlew :api-owner:bootJar --info | |
| - name: Change artifact file name | |
| run: mv build/libs/*.jar build/libs/app.jar | |
| - name: bootJar with Gradle | |
| run: ./gradlew :api-owner:bootJar --info | |
| - name: Change artifact file name | |
| run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar |
🤖 Prompt for AI Agents
.github/workflows/Dev_CD.yml around lines 41 to 46: the workflow moves the built
JAR from build/libs but the bootJar task for :api-owner produces the artifact
under api-owner/build/libs; update the move command to reference the submodule
path (api-owner/build/libs/*.jar -> api-owner/build/libs/app.jar) so it targets
the actual output location (or alternatively run the mv from the api-owner
directory); replace the incorrect mv build/libs/... with the correct
api-owner/build/libs/... path.
| - name: Upload artifact file | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: app-artifact | ||
| path: ./build/libs/app.jar | ||
| if-no-files-found: error | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
업로드 경로도 서브모듈 경로로 정합성 유지
아티팩트 업로드 경로를 실제 위치로 수정해야 합니다.
- - name: Upload artifact file
+ - name: Upload artifact file
uses: actions/upload-artifact@v4
with:
name: app-artifact
- path: ./build/libs/app.jar
+ path: ./api-owner/build/libs/app.jar
if-no-files-found: error📝 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.
| - name: Upload artifact file | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: app-artifact | |
| path: ./build/libs/app.jar | |
| if-no-files-found: error | |
| - name: Upload artifact file | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: app-artifact | |
| path: ./api-owner/build/libs/app.jar | |
| if-no-files-found: error |
🤖 Prompt for AI Agents
In .github/workflows/Dev_CD.yml around lines 47 to 53, the artifact upload path
points to ./build/libs/app.jar which doesn't match the submodule layout; update
the path to the actual artifact location inside the submodule (for example
./your-submodule-name/build/libs/app.jar) so the Upload artifact step points to
the correct relative path, and verify if-no-files-found behavior is appropriate.
| PID=$(lsof -t -i:8080) | ||
|
|
||
| # 프로세스 종료 | ||
| if [ -z "$PID" ]; then | ||
| echo "No process is using port 8080." | ||
| else | ||
| echo "Killing process with PID: $PID" | ||
| kill -15 "$PID" | ||
|
|
||
| # 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인 | ||
| if [ $? -eq 0 ]; then | ||
| echo "Process $PID terminated successfully." | ||
| else | ||
| echo "Failed to terminate process $PID." | ||
| fi | ||
| fi |
There was a problem hiding this comment.
🛠️ Refactor suggestion
종료 대기/강제 종료 루프 추가 필요
kill -15 성공 여부만 확인하고 포트 해제가 보장되지 않습니다. 일정 시간 대기 후 미종료 시 -9로 강제 종료하는 루프를 권장합니다.
-if [ -z "$PID" ]; then
+if [ -z "$PID" ]; then
echo "No process is using port 8080."
else
echo "Killing process with PID: $PID"
kill -15 "$PID"
# 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
if [ $? -eq 0 ]; then
echo "Process $PID terminated successfully."
else
echo "Failed to terminate process $PID."
fi
+ # 포트 해제 대기(최대 30초), 미해제 시 SIGKILL
+ for i in {1..30}; do
+ if ! lsof -t -i:8080 >/dev/null; then
+ break
+ fi
+ sleep 1
+ done
+ if lsof -t -i:8080 >/dev/null; then
+ echo "Force killing process $PID"
+ kill -9 "$PID" || true
+ fi
fi📝 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.
| PID=$(lsof -t -i:8080) | |
| # 프로세스 종료 | |
| if [ -z "$PID" ]; then | |
| echo "No process is using port 8080." | |
| else | |
| echo "Killing process with PID: $PID" | |
| kill -15 "$PID" | |
| # 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인 | |
| if [ $? -eq 0 ]; then | |
| echo "Process $PID terminated successfully." | |
| else | |
| echo "Failed to terminate process $PID." | |
| fi | |
| fi | |
| PID=$(lsof -t -i:8080) | |
| # 프로세스 종료 | |
| if [ -z "$PID" ]; then | |
| echo "No process is using port 8080." | |
| else | |
| echo "Killing process with PID: $PID" | |
| kill -15 "$PID" | |
| # 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인 | |
| if [ $? -eq 0 ]; then | |
| echo "Process $PID terminated successfully." | |
| else | |
| echo "Failed to terminate process $PID." | |
| fi | |
| # 포트 해제 대기(최대 30초), 미해제 시 SIGKILL | |
| for i in {1..30}; do | |
| if ! lsof -t -i:8080 >/dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if lsof -t -i:8080 >/dev/null; then | |
| echo "Force killing process $PID" | |
| kill -9 "$PID" || true | |
| fi | |
| fi |
| JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) | ||
|
|
||
| echo "JAR 파일 실행: $JAR_FILE" | ||
|
|
There was a problem hiding this comment.
하드코딩된 경로 제거 및 아티팩트 명 일치
워크플로는 ~/app로 다운로드하며 파일명은 app.jar입니다. 스크립트는 /home/ubuntu/app/*.jar를 탐색하여 환경에 따라 실패합니다. $HOME 기반 경로와 고정 파일명을 사용하세요. JAR 미존재 시 즉시 실패하도록 체크를 추가합니다.
-JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)
+APP_DIR="${APP_DIR:-$HOME/app}"
+JAR_FILE="$APP_DIR/app.jar"
+if [ ! -f "$JAR_FILE" ]; then
+ echo "JAR not found: $JAR_FILE"
+ exit 1
+fi
-
-echo "JAR 파일 실행: $JAR_FILE"
+echo "JAR 파일 실행: $JAR_FILE"📝 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.
| JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) | |
| echo "JAR 파일 실행: $JAR_FILE" | |
| APP_DIR="${APP_DIR:-$HOME/app}" | |
| JAR_FILE="$APP_DIR/app.jar" | |
| if [ ! -f "$JAR_FILE" ]; then | |
| echo "JAR not found: $JAR_FILE" | |
| exit 1 | |
| fi | |
| echo "JAR 파일 실행: $JAR_FILE" |
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 20 to 23, the script
currently hardcodes /home/ubuntu/app/*.jar which can fail in other environments
and with the known artifact name; change it to use $HOME-based path and the
fixed filename (e.g. $HOME/app/app.jar) and add an existence check that
immediately exits non-zero if the JAR is missing; ensure subsequent code uses
the JAR_FILE variable set to that path and that the script prints the JAR path
before proceeding.
| APP_LOG_DIR="/home/ubuntu/app/logs" | ||
| APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log" | ||
|
|
||
| echo "애플리케이션 로그 파일: $APP_LOG_FILE" | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
로그 디렉터리 생성 누락
로그 디렉터리가 없으면 리다이렉션이 실패합니다. mkdir -p 추가를 권장합니다.
APP_LOG_DIR="/home/ubuntu/app/logs"
APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"
echo "애플리케이션 로그 파일: $APP_LOG_FILE"
+mkdir -p "$APP_LOG_DIR"📝 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.
| APP_LOG_DIR="/home/ubuntu/app/logs" | |
| APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log" | |
| echo "애플리케이션 로그 파일: $APP_LOG_FILE" | |
| APP_LOG_DIR="/home/ubuntu/app/logs" | |
| APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log" | |
| echo "애플리케이션 로그 파일: $APP_LOG_FILE" | |
| mkdir -p "$APP_LOG_DIR" |
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 25 to 29, the script sets
APP_LOG_DIR and APP_LOG_FILE but doesn't ensure the log directory exists which
can cause redirection failures; add a directory creation step before using
APP_LOG_FILE by running mkdir -p "$APP_LOG_DIR" and check its exit status (exit
with non-zero and a clear error message if creation fails) so the log file can
be created reliably.
| sudo nohup java \ | ||
| -Dspring.profiles.active=dev \ | ||
| -Duser.timezone=Asia/Seoul \ | ||
| -Dserver.port=8080 \ | ||
| -Ddd.service=wellmeet-notification \ | ||
| -Ddd.env=dev \ | ||
| -jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 & |
There was a problem hiding this comment.
SC2024: sudo가 리다이렉션에 영향 없음 — root 권한으로 파일 쓰기 보장 필요
현재 리다이렉션은 호출 쉘 권한으로 수행되어 권한 오류가 날 수 있습니다. sudo sh -c로 전체 커맨드를 승격하세요.
-sudo nohup java \
- -Dspring.profiles.active=dev \
- -Duser.timezone=Asia/Seoul \
- -Dserver.port=8080 \
- -Ddd.service=wellmeet-notification \
- -Ddd.env=dev \
- -jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 &
+sudo sh -c "nohup java \
+ -Dspring.profiles.active=dev \
+ -Duser.timezone=Asia/Seoul \
+ -Dserver.port=8080 \
+ -Ddd.service=wellmeet-notification \
+ -Ddd.env=dev \
+ -jar \"$JAR_FILE\" >> \"$APP_LOG_FILE\" 2>&1 &"📝 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.
| sudo nohup java \ | |
| -Dspring.profiles.active=dev \ | |
| -Duser.timezone=Asia/Seoul \ | |
| -Dserver.port=8080 \ | |
| -Ddd.service=wellmeet-notification \ | |
| -Ddd.env=dev \ | |
| -jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 & | |
| sudo sh -c "nohup java \ | |
| -Dspring.profiles.active=dev \ | |
| -Duser.timezone=Asia/Seoul \ | |
| -Dserver.port=8080 \ | |
| -Ddd.service=wellmeet-notification \ | |
| -Ddd.env=dev \ | |
| -jar \"$JAR_FILE\" >> \"$APP_LOG_FILE\" 2>&1 &" |
🧰 Tools
🪛 Shellcheck (0.10.0)
[warning] 36-36: sudo doesn't affect redirects. Use ..| sudo tee file
(SC2024)
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 30 to 36, the nohup command's
redirection runs under the calling shell (not sudo) which can cause permission
errors when writing the log; wrap the entire command and its redirection in a
privileged shell by using sudo sh -c '... &' so that the redirection is executed
as root, ensure proper quoting of variables inside the single-quoted command or
escape inner quotes as needed, and keep nohup and the trailing ampersand to run
the process in background.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (22)
.github/workflows/Dev_CD.yml (2)
32-37: Gradle 캐시 활성화로 빌드 시간 단축setup-java에서 Gradle 캐시를 켜면 CI가 빨라집니다.
- name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '21' + cache: 'gradle'
78-79: 동시 배포 경합 방지(concurrency) 권장동일 브랜치에서 연속 푸시 시 이전 배포와 경합할 수 있습니다. concurrency 그룹으로 이전 작업 취소를 권장합니다.
예시(파일 상단 jobs 옆에 추가):
concurrency: group: dev-cd-${{ github.ref }} cancel-in-progress: truesrc/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (3)
20-20: ownerId 무결성(Null/중복) 보장 추가 권장1 owner 1 row 설계라면 NotNull+Unique 제약을 명시하세요(쿼리 성능/데이터 정합성).
- private String ownerId; + @jakarta.persistence.Column(nullable = false, unique = true) + private String ownerId;운영: 스키마를 DDL 자동생성이 아닌 마이그레이션으로 관리한다면, 해당 Unique 인덱스 추가 작업(Flyway/Liquibase)도 포함해 주세요.
21-26: 플래그 필드 확장성 개선 검토(선택)이벤트×채널 조합이 늘면 필드 폭증 위험. Enum(NotificationType, Channel) 기반의 설정 맵(예: Map<NotificationKey, Boolean>) 또는 @embeddable로 구조화하는 방안을 고려해 보세요.
11-14: BaseEntity 상속 일관성 적용 권장PushSubscription은 BaseEntity를 상속하고 있으나 OwnerNotificationEnabled는 상속하지 않습니다 — 생성/수정 시각 등 공통 필드(감사) 관리를 위해 OwnerNotificationEnabled도 BaseEntity를 상속하도록 적용하세요.
참고: BaseEntity: src/main/java/com/wellmeet/common/domain/BaseEntity.java
사용 예: src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (public class PushSubscription extends BaseEntity)
대상: src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.javasrc/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (1)
12-15: Enum 필드 'name'은 혼동 소지 → 'code'로 변경 고려, category는 enum화 권장
- Enum.name()과 의미 충돌 소지가 있는 필드명 ‘name’ 대신 ‘code’/‘type’ 등이 명확합니다.
- category를 문자열이 아닌 별도 enum(NotificationCategory: HIGH/…)으로 두면 오타 방지 및 검증이 용이합니다.
참고 변경안(로컬 범위, 참조처 일괄 수정 필요):
- private final String name; - private final String category; + private final String code; + private final NotificationCategory category; private final String topic; private final String source;src/main/java/com/wellmeet/notification/webpush/WebPushController.java (1)
18-31: userId 유효성 검증 추가 및 @validated 적용 권장@RequestParam userId에 @notblank 추가 시 입력 검증 강화. 클래스 레벨 @validated로 메서드 파라미터 검증 활성화 권장.
package com.wellmeet.notification.webpush; import com.wellmeet.notification.webpush.dto.SubscribeRequest; import com.wellmeet.notification.webpush.dto.SubscribeResponse; import com.wellmeet.notification.webpush.dto.TestPushRequest; import com.wellmeet.notification.webpush.dto.UnsubscribeRequest; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; ... -@RestController -@RequiredArgsConstructor +@RestController +@RequiredArgsConstructor +@Validated public class WebPushController { ... - @RequestParam String userId + @RequestParam @NotBlank String userId ) { return webPushService.subscribe(userId, subscribeRequest); }src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (2)
13-16: (선택) 중복 방지를 위한 유니크 제약 추천userId+endpoint 조합에 DB 유니크 인덱스를 두면 동일 구독 중복 저장을 구조적으로 차단할 수 있습니다(레포지토리에도 existsBy가 존재).
import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; ... -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + uniqueConstraints = @UniqueConstraint( + name = "uk_push_subscription_user_endpoint", + columnNames = {"userId", "endpoint"} + ) +) public class PushSubscription extends BaseEntity {
44-47: 업데이트 시 재활성화 여부 반영 고려update 시 키만 갱신되고 active 상태는 유지됩니다. 재구독 시 활성화를 보장하려면 active=true 설정을 고려하세요.
public void update(PushSubscription updatedSubscription) { this.p256dh = updatedSubscription.p256dh; this.auth = updatedSubscription.auth; + this.active = true; }src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java (3)
29-31: ObjectMapper 직접 생성 대신 빈 주입 사용 권장애플리케이션 전역 설정(모듈/날짜/가시성 등)을 공유하려면 Spring 빈으로 주입하세요.
- private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper;
53-67: 요청의 data 필드가 payload에 반영되지 않음클라이언트가 전달한 data를 기본값과 병합하는 것이 자연스럽습니다.
private Map<String, Object> getNotificationPayload(TestPushRequest request) { Map<String, Object> notificationPayload = new HashMap<>(); notificationPayload.put("title", request.title()); notificationPayload.put("body", request.body()); notificationPayload.put("icon", "/icon-192x192.png"); notificationPayload.put("badge", "/badge-72x72.png"); notificationPayload.put("vibrate", new int[]{100, 50, 100}); notificationPayload.put("requireInteraction", false); - Map<String, Object> defaultData = new HashMap<>(); - defaultData.put("url", "/notifications"); - defaultData.put("timestamp", System.currentTimeMillis()); - notificationPayload.put("data", defaultData); + Map<String, Object> data = new HashMap<>(); + if (request.data() != null) { + data.putAll(request.data()); + } + data.putIfAbsent("url", "/notifications"); + data.put("timestamp", System.currentTimeMillis()); + notificationPayload.put("data", data); return notificationPayload; }
69-77: InterruptedException 처리 시 인터럽트 상태 복원스레드 인터럽트가 손실되지 않도록 재설정하세요.
- } catch (JoseException | GeneralSecurityException | IOException | ExecutionException | InterruptedException e) { - throw new WellMeetNotificationException(ErrorCode.WEB_PUSH_SEND_FAILED); + } catch (JoseException | GeneralSecurityException | IOException | ExecutionException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new WellMeetNotificationException(ErrorCode.WEB_PUSH_SEND_FAILED); }src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java (1)
5-20: 응답에서 p256dh/auth 노출 필요성 재검토클라이언트가 즉시 재사용하지 않는다면 민감 키를 응답 본문에 포함하지 않는 편이 안전합니다. 최소한 로그(요청/응답 본문)에서는 마스킹하세요.
src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java (1)
8-13: endpoint 형식 검증(옵션) 추가 제안웹푸시 endpoint는 URL 형태가 일반적입니다. Hibernate Validator의 @url을 함께 적용하면 조기 검증에 유용합니다.
적용 예:
import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.URL; public record SubscribeRequest( - @NotBlank - String endpoint, + @NotBlank @URL + String endpoint,src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)
7-14: 헤더 DTO 역직렬화 보강(Setter + unknown 필드 허용)Kafka JSON 역직렬화를 위해 setter가 필요합니다. unknown 필드 허용도 추가를 권장합니다.
package com.wellmeet.notification.consumer.dto; import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; +import lombok.Setter; import lombok.NoArgsConstructor; @Getter +@Setter @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class MessageHeader { private String messageId; private LocalDateTime timestamp; private String source; }src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)
7-15: Payload DTO도 역직렬화 보강 필요Setter 부재 시 Jackson 바인딩 실패 위험이 있습니다. unknown 필드 허용도 추가 권장.
package com.wellmeet.notification.consumer.dto; import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; +import lombok.Setter; import lombok.NoArgsConstructor; @Getter +@Setter @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ReservationCreatedPayload extends NotificationPayload { private Long reservationId; private String customerName; private LocalDateTime reservationTime; private int partySize; }src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
46-61: 중복 레코드 방지/식별자 유지까지 함께 검증 권장(옵션)업데이트 시 기존 엔티티 재사용을 보장하려면 ID 유지도 확인해 두는 편이 좋습니다.
- PushSubscription pushSubscription = new PushSubscription(userId, endpoint, "p256dh", "auth"); - pushSubscriptionRepository.save(pushSubscription); + PushSubscription pushSubscription = new PushSubscription(userId, endpoint, "p256dh", "auth"); + PushSubscription saved = pushSubscriptionRepository.save(pushSubscription); @@ - PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get(); + PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get(); @@ assertAll( () -> assertThat(subscription.getUserId()).isEqualTo(userId), () -> assertThat(subscription.getEndpoint()).isEqualTo(endpoint), () -> assertThat(subscription.getP256dh()).isEqualTo(request.p256dh()), () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()), + () -> assertThat(response.subscriptionId()).isEqualTo(saved.getId()) );src/main/java/com/wellmeet/config/KafkaConfig.java (1)
34-34: auto.offset.reset 하드코딩 제거 권장동일 설정이 application-*.yml에 있다면 코드 중복입니다. 프로퍼티만 신뢰하세요.
- props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");src/main/java/com/wellmeet/notification/webpush/WebPushService.java (4)
27-31: O(n) 조회 → 단건 조회로 단순화특정 endpoint 존재 여부만 필요하므로 단건 조회 메서드로 대체해 쿼리·메모리 비용을 줄이세요.
- List<PushSubscription> existingSubscriptions = pushSubscriptionRepository.findByUserId(userId); - Optional<PushSubscription> pushSubscription = existingSubscriptions.stream() - .filter(subscription -> subscription.isSameEndpoint(request.endpoint())) - .findAny(); + Optional<PushSubscription> pushSubscription = + pushSubscriptionRepository.findByUserIdAndEndpoint(userId, request.endpoint());레포지토리에 아래 시그니처가 있는지 확인/추가 필요:
Optional<PushSubscription> findByUserIdAndEndpoint(String userId, String endpoint);
43-50: 읽기 전용 트랜잭션으로 의도 명확화조회 후 전송만 수행합니다. 읽기 전용으로 지정해 JPA 플러시 비용을 방지하세요.
- public void sendTestPush(String userId, TestPushRequest request) { + @Transactional(readOnly = true) + public void sendTestPush(String userId, TestPushRequest request) {
52-58: exists+delete 두 번 쿼리 → 단일 삭제 결과 체크로 축소TOCTOU와 불필요한 왕복을 줄이세요. 삭제 결과로 존재 여부 판정이 가능합니다.
- @Transactional - public void unsubscribe(String userId, UnsubscribeRequest request) { - if (!pushSubscriptionRepository.existsByUserIdAndEndpoint(userId, request.endpoint())) { - throw new WellMeetNotificationException(ErrorCode.SUBSCRIPTION_NOT_FOUND); - } - pushSubscriptionRepository.deleteByUserIdAndEndpoint(userId, request.endpoint()); - } + @Transactional + public void unsubscribe(String userId, UnsubscribeRequest request) { + long deleted = pushSubscriptionRepository.deleteByUserIdAndEndpoint(userId, request.endpoint()); + if (deleted == 0) { + throw new WellMeetNotificationException(ErrorCode.SUBSCRIPTION_NOT_FOUND); + } + }레포지토리 메서드가 삭제 건수를 반환하도록 시그니처가
long deleteByUserIdAndEndpoint(...)인지 확인/변경이 필요합니다.
25-41: (권장) 유니크 제약으로 중복 구독 보호동시 구독 요청 시 중복 레코드 생성 방지를 위해
(user_id, endpoint)에 유니크 인덱스를 추가하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (24)
.github/workflows/Dev_CD.yml(1 hunks)src/main/java/com/wellmeet/config/KafkaConfig.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java(1 hunks)src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/WebPushController.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/WebPushService.java(2 hunks)src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java(1 hunks)src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java(1 hunks)src/main/resources/local-secret.yml(1 hunks)src/test/java/com/wellmeet/BaseControllerTest.java(1 hunks)src/test/java/com/wellmeet/BaseServiceTest.java(1 hunks)src/test/java/com/wellmeet/webpush/WebPushControllerTest.java(1 hunks)src/test/java/com/wellmeet/webpush/WebPushServiceTest.java(3 hunks)
✅ Files skipped from review due to trivial changes (3)
- src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java
- src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java
- src/test/java/com/wellmeet/webpush/WebPushControllerTest.java
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (1)
Getter(13-52)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (2)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
Getter(6-13)src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
Getter(6-13)
src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
Getter(6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
Getter(6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)
Getter(7-14)src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
Getter(6-13)src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
Getter(6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)
Getter(7-14)src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
Getter(6-13)src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)
Getter(7-15)
🪛 Gitleaks (8.28.0)
src/main/resources/local-secret.yml
[high] 4-4: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🪛 actionlint (1.7.7)
.github/workflows/Dev_CD.yml
63-63: label "dev" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2022", "windows-2019", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "ubuntu-20.04", "macos-latest", "macos-latest-xl", "macos-latest-xlarge", "macos-latest-large", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14", "macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file
(runner-label)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test / build-and-push
🔇 Additional comments (14)
.github/workflows/Dev_CD.yml (3)
41-45: bootJar 산출물 경로 불일치 — 서브모듈 경로 사용 필요이전 리뷰와 동일 이슈입니다. 멀티모듈(:api-owner)이라면 산출물은 api-owner/build/libs 하위입니다.
다음처럼 수정하세요:
- - name: bootJar with Gradle - run: ./gradlew bootJar --info - - - name: Change artifact file name - run: mv build/libs/*.jar build/libs/app.jar + - name: bootJar with Gradle + run: ./gradlew :api-owner:bootJar --info + + - name: Change artifact file name + run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar또한 다중 매칭 시 mv 실패를 방지하려면 Gradle에서 archiveFileName을 app.jar로 고정하는 것을 권장합니다(Gradle 설정 변경).
47-52: 아티팩트 업로드 경로 불일치서브모듈 산출물 경로로 수정 필요합니다.
- name: Upload artifact file uses: actions/upload-artifact@v4 with: name: app-artifact - path: ./build/libs/app.jar + path: ./api-owner/build/libs/app.jar if-no-files-found: error
62-64: self-hosted 러너 라벨 명시 권장runs-on: dev는 GitHub 호스티드에는 없으며, self-hosted 러너 라벨 매칭 실패 가능성이 있습니다. self-hosted 및 OS 라벨을 함께 지정하세요.
- runs-on: dev + runs-on: [self-hosted, linux, dev]dev 러너에 위 라벨이 실제로 부여되어 있는지 확인 부탁드립니다.
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
20-20: 용어 일관성 확인 — userId와 ownerId는 분리되어 사용됩니다webpush 도메인(예: PushSubscription, PushSubscriptionRepository, WebPushController/WebPushService)은 userId를 사용하고, OwnerNotificationEnabled에는 ownerId만 정의되어 있습니다. rg 검색 결과 교차 사용이나 조회/조인 혼동은 발견되지 않았습니다.
src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java (1)
7-16: LGTM레코드 + Bean Validation 조합 적절합니다.
src/test/java/com/wellmeet/BaseServiceTest.java (1)
3-15: LGTM패키지 경로 변경에 따른 import 업데이트 적절합니다.
src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java (1)
8-16: LGTMJPA 시그니처와 파생 쿼리 메서드 정의 적절합니다.
src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java (1)
1-3: 패키지/임포트 경로 리네임 적절
notification.webpush네임스페이스 정리 방향 좋습니다.src/test/java/com/wellmeet/BaseControllerTest.java (1)
3-3: 패키지 이동 반영 LGTM리포지토리 임포트 경로 수정 적절합니다.
src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java (1)
1-3: 패키지/임포트 경로 리네임 LGTMsrc/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (2)
14-17: 토픽/그룹 하드코딩 제거 + 레코드 메타데이터 로깅으로 운용성 향상기 존 코멘트와 동일 제안입니다. 프로퍼티로 외부화하고 파티션/오프셋까지 로그에 포함하세요.
package com.wellmeet.notification.consumer; import com.wellmeet.notification.consumer.dto.NotificationMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; +import org.apache.kafka.clients.consumer.ConsumerRecord; @Slf4j @Service @RequiredArgsConstructor public class NotificationConsumer { - @KafkaListener(topics = "notification", groupId = "notification-group") - public void consume(NotificationMessage message) { - log.info("Received message from notification topic: {}", message); + @KafkaListener( + topics = "${notification.topic:notification}", + groupId = "${spring.kafka.consumer.group-id}" + ) + public void consume(ConsumerRecord<String, NotificationMessage> record) { + log.info("Consumed notification: topic={}, partition={}, offset={}, key={}, payload={}", + record.topic(), record.partition(), record.offset(), record.key(), record.value()); } }
14-17: Kafka JSON 역직렬화 설정 확인 — 다형성 타입 힌트 점검 필요
- kafkaListenerContainerFactory / ConsumerFactory가 NotificationMessage 타입으로 Bean으로 등록되어 있음 — src/main/java/com/wellmeet/config/KafkaConfig.java (consumerFactory(), kafkaListenerContainerFactory()).
- JsonDeserializer 설정 확인됨: USE_TYPE_INFO_HEADERS=false, TRUSTED_PACKAGES="com.wellmeet.consumer.dto", VALUE_DEFAULT_TYPE=NotificationMessage.class.
- 다형성 위험: 코드베이스에서 @JsonTypeInfo/@JsonSubTypes 등 타입 힌트 사용 흔적 없음. NotificationMessage 내부에 다형성 payload(NotificationPayload 등)가 있다면 현재 설정(USE_TYPE_INFO_HEADERS=false + 단일 VALUE_DEFAULT_TYPE)로 하위 타입 식별이 안 되어 역직렬화 실패 가능. 조치(선택 중 하나): payload 클래스에 @JsonTypeInfo/@JsonSubTypes 추가, 또는 USE_TYPE_INFO_HEADERS=true로 헤더 기반 타입 전달 사용(프로듀서도 헤더 포함), 또는 커스텀 Deserializer 적용.
- group-id는 외부화되어 있음(@value("${spring.kafka.consumer.group-id}")).
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
46-61: 동일 유저/엔드포인트 재구독 시 업데이트 검사 LGTM변경된 업데이트 동작(assert p256dh/auth 갱신) 검증이 정확합니다.
src/main/java/com/wellmeet/config/KafkaConfig.java (1)
42-48: Boot 자동 구성 활용 + 에러 핸들러 적용 권장수동 팩토리 설정 대신 Boot의 configurer를 사용해
spring.kafka.*를 일관 적용하고, 공통 에러 핸들러(백오프 포함)를 설정하세요.- @Bean - public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory() { - ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory = - new ConcurrentKafkaListenerContainerFactory<>(); - factory.setConsumerFactory(consumerFactory()); - return factory; - } + @Bean + public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory( + org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer configurer, + ConsumerFactory<String, NotificationMessage> consumerFactory) { + ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory = + new ConcurrentKafkaListenerContainerFactory<>(); + configurer.configure(factory, consumerFactory); + factory.setCommonErrorHandler(new org.springframework.kafka.listener.DefaultErrorHandler( + new org.springframework.util.backoff.FixedBackOff(1000L, 3))); + return factory; + }추가 import:
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer; import org.springframework.kafka.listener.DefaultErrorHandler; import org.springframework.util.backoff.FixedBackOff;
| - name: Setting dev-secret.yml | ||
| run: | | ||
| echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml | ||
|
|
There was a problem hiding this comment.
빌드 산출물에 dev-secret.yml 포함 위험
빌드 단계에서 dev-secret.yml을 리소스에 기록하면 업로드되는 JAR 내부에 비밀이 포함됩니다. 이는 아티팩트 유출 리스크입니다. 배포 단계에서만 파일을 생성·주입하도록 이동하세요.
다음처럼 빌드 단계에서 제거하고, 배포 단계에서 생성하도록 변경:
- - name: Setting dev-secret.yml
- run: |
- echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
+ # dev-secret.yml은 빌드 아티팩트에 포함하지 않습니다.배포 단계에 추가:
steps:
+ - name: Create dev-secret.yml on runner
+ run: |
+ mkdir -p ~/app/config
+ echo "${{ secrets.DEV_SECRET_YML }}" > ~/app/config/dev-secret.yml
+
- name: Download artifact file
uses: actions/download-artifact@v4
with:
name: app-artifact
path: ~/app
+
+ - name: Verify secret file exists
+ run: test -s ~/app/config/dev-secret.yml애플리케이션이 외부 파일을 읽도록(코드/스크립트 측) JVM 옵션 예:
-Dspring.config.import=optional:file:~/app/config/dev-secret.yml
📝 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.
| - name: Setting dev-secret.yml | |
| run: | | |
| echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml | |
| # dev-secret.yml은 빌드 아티팩트에 포함하지 않습니다. | |
| steps: | |
| - name: Create dev-secret.yml on runner | |
| run: | | |
| mkdir -p ~/app/config | |
| echo "${{ secrets.DEV_SECRET_YML }}" > ~/app/config/dev-secret.yml | |
| - name: Download artifact file | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: app-artifact | |
| path: ~/app | |
| - name: Verify secret file exists | |
| run: test -s ~/app/config/dev-secret.yml |
🤖 Prompt for AI Agents
.github/workflows/Dev_CD.yml lines 28-31: the workflow writes dev-secret.yml
into src/main/resources during the build which embeds secrets into the produced
JAR; remove that run step from the build job, and instead add a step in the
deploy job that creates the file on the runner (or injects it as a secret file)
just before deployment; ensure the build uses no local secret file, update
deployment steps to write the secret from the GitHub secret into an external
path (e.g., ~/app/config/dev-secret.yml) and/or set the JVM/SPRING config to
import an external optional file (for example via
-Dspring.config.import=optional:file:~/app/config/dev-secret.yml) so secrets are
injected at deploy time only and not baked into artifacts.
| props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false); | ||
| props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto"); | ||
| props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class); | ||
| return new DefaultKafkaConsumerFactory<>(props); |
There was a problem hiding this comment.
JsonDeserializer TRUSTED_PACKAGES 경로 오타로 역직렬화 실패 가능성
DTO 패키지명이 com.wellmeet.notification.consumer.dto인데, 신뢰 패키지가 com.wellmeet.consumer.dto로 되어 있어 역직렬화가 실패할 수 있습니다. 또한 VALUE_DEFAULT_TYPE은 FQCN 문자열로 지정하는 편이 안전합니다.
다음 최소 패치 권장:
- props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto");
- props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
+ props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.notification.consumer.dto");
+ props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.wellmeet.notification.consumer.dto.NotificationMessage");📝 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.
| props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false); | |
| props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto"); | |
| props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class); | |
| return new DefaultKafkaConsumerFactory<>(props); | |
| props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false); | |
| props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.notification.consumer.dto"); | |
| props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.wellmeet.notification.consumer.dto.NotificationMessage"); | |
| return new DefaultKafkaConsumerFactory<>(props); |
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/config/KafkaConfig.java around lines 36 to 39, the
JsonDeserializer TRUSTED_PACKAGES value is incorrect (com.wellmeet.consumer.dto)
which can cause deserialization failures; change it to the correct package
com.wellmeet.notification.consumer.dto and set
JsonDeserializer.VALUE_DEFAULT_TYPE to the fully-qualified class name string
"com.wellmeet.notification.consumer.dto.NotificationMessage" (rather than the
class literal) so the deserializer can resolve the type reliably; keep
USE_TYPE_INFO_HEADERS = false and return the DefaultKafkaConsumerFactory with
the updated props.
| @Getter | ||
| @NoArgsConstructor | ||
| public class NotificationInfo { | ||
|
|
||
| private String type; | ||
| private String category; | ||
| private String recipient; |
There was a problem hiding this comment.
🧩 Analysis chain
Jackson 역직렬화 호환성 점검 + 타입 안전성 개선 제안
- 현재 @Getter, @NoArgsConstructor만으로는 기본 ObjectMapper 설정에서 private 필드 역직렬화가 실패할 수 있습니다. 필드 기반 접근 허용 또는 세터/생성자 제공이 필요합니다.
- 또한 type/category 문자열은 오타·불일치 위험이 있으니, enum 기반(예: NotificationType, 별도 Category enum)으로의 전환을 고려해 주세요.
적용 예시(필드 접근 허용 및 unknown 필드 무시):
package com.wellmeet.notification.consumer.dto;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonIgnoreProperties(ignoreUnknown = true)
public class NotificationInfo {
private String type;
private String category;
private String recipient;
}다음 스크립트로 역직렬화 설정(필드 가시성/JsonDeserializer/신뢰 패키지) 존재 여부를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# Jackson 필드 가시성/AutoDetect/Deserializer 설정 탐색
rg -n -C2 -e '@JsonAutoDetect' -e 'setVisibility' -e 'JsonAutoDetect.Visibility' -e 'fieldVisibility' --type=java --type=yml --type=properties
# Spring Kafka JsonDeserializer 및 trusted.packages 설정 탐색
rg -n -C2 -e 'JsonDeserializer' -e 'spring.kafka.consumer' -e 'trusted.packages' --type=java --type=yml --type=properties
# 커스텀 ObjectMapper Bean 정의 탐색
rg -n -C2 -P 'ObjectMapper\s+\w+\s*\(' --type=javaLength of output: 55
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== File: NotificationInfo.java (first 200 lines) ==="
sed -n '1,200p' src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java || true
echo
echo "=== Search: Jackson annotations / ObjectMapper config / module registration ==="
rg -n -C2 --hidden -S '@JsonAutoDetect|@JsonIgnoreProperties|JsonAutoDetect.Visibility|setVisibility|fieldVisibility|Jackson2ObjectMapperBuilder|new ObjectMapper|@Bean\s+ObjectMapper|registerModule|SimpleModule|addDeserializer|@JsonComponent' || true
echo
echo "=== Search: Kafka JsonDeserializer / trusted.packages / spring.kafka.consumer ==="
rg -n -C2 --hidden -S 'JsonDeserializer|trusted.packages|spring.kafka.consumer|value-deserializer|key-deserializer|spring.kafka.properties' || true
echo
echo "=== Search: application.yml / application.yaml / application.properties for trusted.packages or spring.kafka.consumer ==="
rg -n -C2 --hidden -S 'trusted.packages|spring.kafka.consumer' -g 'application.*' -g '**/application.*' || true
echo
echo "=== Search: any mention of NotificationInfo usage (deserialization targets) ==="
rg -n -C2 --hidden -S 'NotificationInfo' || trueLength of output: 5962
조치 필요 — NotificationInfo 역직렬화 접근성 설정 및 Kafka JsonDeserializer trusted.packages 수정(치명적)
- NotificationInfo (src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java): 현재 @NoArgsConstructor + private 필드(@Getter)만 있어 Jackson 기본 설정으로는 역직렬화 실패 가능. 클래스에 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 및 @JsonIgnoreProperties(ignoreUnknown = true)를 추가하거나, 각 필드에 세터/ @JsonProperty 또는 @JsonCreator 생성자를 제공해 역직렬화를 보장하세요.
- KafkaConfig (src/main/java/com/wellmeet/config/KafkaConfig.java): props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto"); — 실제 DTO 패키지(com.wellmeet.notification.consumer.dto)와 불일치합니다. trusted.packages 값을 실제 패키지로 수정하거나 필요한 패키지를 포함시키지 않으면 JsonDeserializer가 역직렬화를 차단합니다.
- 권장 변경: type/category 필드를 enum(NotificationType, Category)으로 전환해 타입 안정성 확보.
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java
around lines 6-12, the class only has @NoArgsConstructor and private fields with
@Getter which can prevent Jackson from deserializing; update the class to allow
Jackson deserialization by adding @JsonIgnoreProperties(ignoreUnknown = true)
and @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) to the
class or by adding setters/@JsonProperty annotations or a @JsonCreator
constructor for all fields; additionally, fix Kafka config in
src/main/java/com/wellmeet/config/KafkaConfig.java by setting
JsonDeserializer.TRUSTED_PACKAGES to the actual DTO package
"com.wellmeet.notification.consumer.dto" (or include it alongside existing
packages); optionally migrate type and category to enums (NotificationType,
Category) for stronger typing and update deserialization accordingly.
| @Getter | ||
| @NoArgsConstructor | ||
| public class NotificationMessage { | ||
|
|
||
| private MessageHeader header; | ||
| private NotificationInfo notification; | ||
| private NotificationPayload payload; | ||
| } |
There was a problem hiding this comment.
Kafka/Jackson 역직렬화 실패 가능성: setter 부재 + 다형성 payload 문제
- 현재 DTO는 getter만 있어 Jackson이 바인딩에 실패할 수 있습니다.
- 필드
payload가 추상 타입(NotificationPayload)이라 다형성 역직렬화 설정이 없으면 실패합니다.
다음 패치를 권장합니다(Setter + unknown 필드 허용):
package com.wellmeet.notification.consumer.dto;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
+import lombok.Setter;
import lombok.NoArgsConstructor;
@Getter
+@Setter
@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
public class NotificationMessage {
private MessageHeader header;
private NotificationInfo notification;
private NotificationPayload payload;
}또한, 다형성 역직렬화를 위해 NotificationPayload에 타입 정보를 선언하세요(다른 파일 변경):
// src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java
package com.wellmeet.notification.consumer.dto;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "payloadType")
@JsonSubTypes({
@JsonSubTypes.Type(value = ReservationCreatedPayload.class, name = "RESERVATION_CREATED")
})
public abstract class NotificationPayload {}생성 측에서 JSON에 "payloadType": "RESERVATION_CREATED"를 포함시키세요.
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java
around lines 6 to 13, Jackson deserialization can fail because the class only
exposes getters and the payload field is an abstract type without polymorphic
type info; add setters for all fields and annotate the class with
@JsonIgnoreProperties(ignoreUnknown = true) so unknown Kafka fields are allowed,
and update the abstract NotificationPayload class (separate file) to include
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
property = "payloadType") and @JsonSubTypes registrations for concrete payload
implementations; finally ensure producers include "payloadType" in the JSON
(e.g., "RESERVATION_CREATED") so Jackson can instantiate the correct subtype.
| @Getter | ||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class OwnerNotificationEnabled { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| private String ownerId; | ||
| private boolean reservationRequestWebPushEnabled; | ||
| private boolean reservationRequestEmailEnabled; | ||
| private boolean reservationCancelWebPushEnabled; | ||
| private boolean reservationCancelEmailEnabled; | ||
| private boolean promotionWebPushEnabled; | ||
| private boolean promotionEmailEnabled; | ||
| } |
There was a problem hiding this comment.
엔티티를 애플리케이션 코드에서 생성/수정할 수 없습니다 — 생성자/빌더 등 도메인 메서드 추가 필요
현재 @Getter만 있고 모든 필드가 private이라 새 인스턴스 생성 시 값 설정이 불가합니다(특히 ownerId). 영속화/갱신을 위한 최소 생성자·빌더를 추가하세요.
아래와 같이 생성자(+빌더)를 추가하면 사용 가능합니다:
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OwnerNotificationEnabled {
@@
private boolean promotionWebPushEnabled;
private boolean promotionEmailEnabled;
+
+ @lombok.Builder
+ public OwnerNotificationEnabled(
+ String ownerId,
+ boolean reservationRequestWebPushEnabled,
+ boolean reservationRequestEmailEnabled,
+ boolean reservationCancelWebPushEnabled,
+ boolean reservationCancelEmailEnabled,
+ boolean promotionWebPushEnabled,
+ boolean promotionEmailEnabled
+ ) {
+ this.ownerId = ownerId;
+ this.reservationRequestWebPushEnabled = reservationRequestWebPushEnabled;
+ this.reservationRequestEmailEnabled = reservationRequestEmailEnabled;
+ this.reservationCancelWebPushEnabled = reservationCancelWebPushEnabled;
+ this.reservationCancelEmailEnabled = reservationCancelEmailEnabled;
+ this.promotionWebPushEnabled = promotionWebPushEnabled;
+ this.promotionEmailEnabled = promotionEmailEnabled;
+ }
}📝 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.
| @Getter | |
| @Entity | |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | |
| public class OwnerNotificationEnabled { | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |
| private Long id; | |
| private String ownerId; | |
| private boolean reservationRequestWebPushEnabled; | |
| private boolean reservationRequestEmailEnabled; | |
| private boolean reservationCancelWebPushEnabled; | |
| private boolean reservationCancelEmailEnabled; | |
| private boolean promotionWebPushEnabled; | |
| private boolean promotionEmailEnabled; | |
| } | |
| @Getter | |
| @Entity | |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | |
| public class OwnerNotificationEnabled { | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |
| private Long id; | |
| private String ownerId; | |
| private boolean reservationRequestWebPushEnabled; | |
| private boolean reservationRequestEmailEnabled; | |
| private boolean reservationCancelWebPushEnabled; | |
| private boolean reservationCancelEmailEnabled; | |
| private boolean promotionWebPushEnabled; | |
| private boolean promotionEmailEnabled; | |
| @lombok.Builder | |
| public OwnerNotificationEnabled( | |
| String ownerId, | |
| boolean reservationRequestWebPushEnabled, | |
| boolean reservationRequestEmailEnabled, | |
| boolean reservationCancelWebPushEnabled, | |
| boolean reservationCancelEmailEnabled, | |
| boolean promotionWebPushEnabled, | |
| boolean promotionEmailEnabled | |
| ) { | |
| this.ownerId = ownerId; | |
| this.reservationRequestWebPushEnabled = reservationRequestWebPushEnabled; | |
| this.reservationRequestEmailEnabled = reservationRequestEmailEnabled; | |
| this.reservationCancelWebPushEnabled = reservationCancelWebPushEnabled; | |
| this.reservationCancelEmailEnabled = reservationCancelEmailEnabled; | |
| this.promotionWebPushEnabled = promotionWebPushEnabled; | |
| this.promotionEmailEnabled = promotionEmailEnabled; | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled
around lines 11 to 27, the entity only has @Getter and a protected no-args
constructor so callers cannot set required fields (e.g., ownerId) to create or
update instances; add a minimal constructor and/or a @Builder (or static
factory) that accepts ownerId and the notification flag fields and use the
existing protected no-args constructor for JPA; ensure the constructor/builder
sets all fields and keep the no-arg constructor protected for JPA compliance so
instances can be created and persisted/updated by application code.
| secret: | ||
| vapid: | ||
| public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs | ||
| private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU | ||
| subject: mailto:admin@wellmeet.com |
There was a problem hiding this comment.
하드코딩된 비밀키 포함(Gitleaks 검출) — 즉시 제거 및 키 회전 필요
비밀키가 저장소에 커밋되었습니다. 리포지토리에서 파일 제거 + 히스토리 정리(BFG/git filter-repo) + VAPID 키 즉시 재발급·교체가 필요합니다. 실제 비밀은 런타임(환경변수/외부 파일/Secret Manager)을 통해 주입하세요. 예시 파일만 커밋하세요.
다음과 같이 임시로 플레이스홀더로 치환하시고(그래도 키는 반드시 회전하세요), 실제 값은 .gitignore된 외부 파일로 관리하세요:
-secret:
- vapid:
- public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs
- private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU
- subject: mailto:admin@wellmeet.com
+secret:
+ vapid:
+ public-key: REPLACE_ME_PUBLIC_KEY
+ private-key: REPLACE_ME_PRIVATE_KEY
+ subject: mailto:REPLACE_ME@example.com그리고 루트 .gitignore에 추가하세요(파일 외부 변경 제안):
# secrets
src/main/resources/*-secret.yml
src/main/resources/local-secret.yml
운영 절차:
- 현재 커밋된 키 폐기 및 재발급(VAPID 키 페어).
- 저장소 히스토리에서 해당 파일 완전 제거(BFG/git filter-repo).
- 애플리케이션은 spring.config.import=optional:file:/path/to/dev-secret.yml 등 외부 경로에서 주입.
📝 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.
| secret: | |
| vapid: | |
| public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs | |
| private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU | |
| subject: mailto:admin@wellmeet.com | |
| secret: | |
| vapid: | |
| public-key: REPLACE_ME_PUBLIC_KEY | |
| private-key: REPLACE_ME_PRIVATE_KEY | |
| subject: mailto:REPLACE_ME@example.com |
🧰 Tools
🪛 Gitleaks (8.28.0)
[high] 4-4: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In src/main/resources/local-secret.yml lines 1-5 there are hardcoded VAPID
private/public keys committed to the repo; remove these secrets immediately and
replace them with placeholders, add src/main/resources/local-secret.yml (and
pattern *-secret.yml) to .gitignore, and stop committing real secrets; rotate
the VAPID key pair now (revoke/issue new keys) and scrub the repository history
using BFG or git-filter-repo to remove the leaked file; change the app to load
VAPID values from a runtime secret source (environment variables, an external
ignored file like /path/to/dev-secret.yml loaded via
spring.config.import=optional:file:..., or a secret manager) and keep only an
example/template secret file in the repo.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (2)
18-23: 토픽/그룹 하드코딩 문제는 이전 리뷰에서 지적되었습니다이전 리뷰 코멘트에서 토픽과 그룹 ID를 프로퍼티로 외부화하고 메타데이터 로깅을 추가하도록 제안했습니다.
18-23: 이전 리뷰에서 이미 지적된 사항입니다.토픽명과 그룹 ID가 하드코딩되어 있는 문제는 이전 리뷰 코멘트에서 이미 제기되었습니다. 프로퍼티 기반 외부화를 통해 환경별 유연성을 확보하는 것이 권장됩니다.
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (2)
9-22: 생성자/빌더 누락 문제는 이전 리뷰에서 지적되었습니다이전 리뷰 코멘트에서 엔티티 인스턴스 생성 및 필드 설정을 위한 생성자와
@Builder추가를 제안했습니다. 해당 제안사항을 참고하세요.
9-22: 이전 리뷰에서 이미 지적된 사항입니다.엔티티 인스턴스를 애플리케이션 코드에서 생성할 수 있도록 빌더 또는 생성자를 추가해야 하는 문제는 이전 리뷰 코멘트에서 상세한 해결 방안과 함께 이미 제기되었습니다.
🧹 Nitpick comments (1)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
9-22: UserNotificationEnabled와 코드 중복
OwnerNotificationEnabled와UserNotificationEnabled(UserNotificationEnabled.java:8-23)가 거의 동일한 구조를 가지고 있습니다. 공통 필드와 동작을 추상화하여@MappedSuperclass를 활용하거나, 공통 인터페이스를 도입하면 유지보수성이 향상됩니다.예시: 공통 알림 설정 필드를 추상 클래스로 추출
@MappedSuperclass @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class NotificationEnabled { private boolean webPushEnabled; private boolean emailEnabled; // 공통 알림 채널 필드 }그런 다음 각 엔티티에서 확장:
@Entity public class OwnerNotificationEnabled extends NotificationEnabled { @Id private String ownerId; // Owner 전용 필드 }단, 현재 Owner와 User의 알림 타입이 다르므로(예: reservationRequest vs reservationConfirm) 추상화 수준을 신중히 결정하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/NotificationSender.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java(1 hunks)src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java(1 hunks)src/main/java/com/wellmeet/notification/domain/NotificationHistory.java(1 hunks)src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java(1 hunks)src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java(1 hunks)src/main/java/com/wellmeet/notification/repository/NotificationHistoryRepository.java(1 hunks)src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java(1 hunks)src/main/java/com/wellmeet/notification/repository/UserNotificationEnabledRepository.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java
- src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/wellmeet/notification/domain/NotificationHistory.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
Getter(6-14)src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
Getter(9-22)src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
Getter(9-24)
src/main/java/com/wellmeet/notification/consumer/NotificationSender.java (1)
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java (1)
Component(9-19)
src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
Getter(9-22)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
Getter(9-24)
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java (1)
src/main/java/com/wellmeet/notification/consumer/NotificationSender.java (1)
Component(10-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build-and-push
- GitHub Check: test / build-and-push
🔇 Additional comments (3)
src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java (2)
7-9: 표준 JPA 리포지토리 패턴이 올바르게 적용되었습니다
JpaRepository를 확장하여 기본 CRUD 작업을 상속받고, ID 타입을String으로 올바르게 지정했습니다. 현재 단계에서 커스텀 쿼리 메서드가 필요 없다면 이 구현으로 충분합니다.
1-9: LGTM!표준 Spring Data JPA 리포지토리 인터페이스가 올바르게 선언되었습니다.
OwnerNotificationEnabled엔티티의 기본 CRUD 작업을 지원합니다.src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (1)
10-16: 의존성 주입 및 서비스 구조가 적절합니다.Lombok의
@RequiredArgsConstructor를 통한 생성자 주입 패턴이 올바르게 적용되었으며, 필요한 협력 객체들이 명확하게 선언되어 있습니다.
| public List<String> check(String recipient, String recipientType) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
null 반환으로 인한 NPE 및 채널 확인 실패
Line 16: check가 언제나 null을 반환하면, 이를 사용하는 측에서 즉시 NullPointerException이 발생하거나 채널 판별이 아예 불가능해집니다. 실제 리포지토리 조회 후 활성화된 채널 목록을 돌려주는 로직을 구현해 주세요(비어 있으면 최소한 빈 리스트라도 반환해야 합니다).
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java
around lines 16 to 18, the check method currently returns null which causes NPEs
and prevents channel determination; replace this stub with logic that queries
the notification/channel repository using recipient and recipientType, filters
for active/enabled channels, returns a List<String> of channel identifiers, and
never returns null (return Collections.emptyList() when none found); also catch
repository exceptions, log appropriately and return an empty list on error.
| public void consume(NotificationMessage message) { | ||
| List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(), | ||
| message.getNotification().getRecipientType()); | ||
| notificationSender.send(message, enabled); | ||
| } |
There was a problem hiding this comment.
에러 처리 로직 누락으로 메시지 처리 안정성 저하
notificationChecker.check() 또는 notificationSender.send()에서 예외 발생 시 Kafka 리스너의 기본 동작에 따라 메시지가 재처리되거나 DLQ로 이동할 수 있습니다. 명시적인 에러 핸들링과 로깅을 추가하여 장애 추적 및 복구 전략을 개선하세요.
다음 diff를 적용하여 에러 처리 및 로깅을 추가하세요:
@KafkaListener(topics = "notification", groupId = "notification-group")
public void consume(NotificationMessage message) {
+ try {
+ log.info("Processing notification message: recipient={}, type={}",
+ message.getNotification().getRecipient(),
+ message.getNotification().getRecipientType());
+
List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(),
message.getNotification().getRecipientType());
notificationSender.send(message, enabled);
+
+ log.info("Successfully processed notification message for recipient={}",
+ message.getNotification().getRecipient());
+ } catch (Exception e) {
+ log.error("Failed to process notification message: recipient={}, error={}",
+ message.getNotification().getRecipient(), e.getMessage(), e);
+ throw e; // Re-throw to trigger Kafka retry/DLQ logic
+ }
}📝 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.
| public void consume(NotificationMessage message) { | |
| List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(), | |
| message.getNotification().getRecipientType()); | |
| notificationSender.send(message, enabled); | |
| } | |
| @KafkaListener(topics = "notification", groupId = "notification-group") | |
| public void consume(NotificationMessage message) { | |
| try { | |
| log.info("Processing notification message: recipient={}, type={}", | |
| message.getNotification().getRecipient(), | |
| message.getNotification().getRecipientType()); | |
| List<String> enabled = notificationChecker.check( | |
| message.getNotification().getRecipient(), | |
| message.getNotification().getRecipientType()); | |
| notificationSender.send(message, enabled); | |
| log.info("Successfully processed notification message for recipient={}", | |
| message.getNotification().getRecipient()); | |
| } catch (Exception e) { | |
| log.error("Failed to process notification message: recipient={}, error={}", | |
| message.getNotification().getRecipient(), e.getMessage(), e); | |
| throw e; // Re-throw to trigger Kafka retry/DLQ logic | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java
around lines 19 to 23, add explicit error handling and logging around the calls
to notificationChecker.check(...) and notificationSender.send(...) so exceptions
are not silently propagated: wrap the existing logic in a try-catch, log a
detailed error (include message id, recipient, recipientType and stacktrace)
when either call fails, and then decide to either rethrow a
RuntimeException/KafkaException to let the Kafka listener handle retries/DLQ or
call a recovery/dlqPublisher if you have one; ensure logging uses the class
logger and includes enough context to trace the message and failure.
| public void send(NotificationMessage payload, List<String> enabled) { | ||
|
|
||
| } |
There was a problem hiding this comment.
비어 있는 send 구현으로 기능이 수행되지 않습니다
Line 17: send 메서드가 비어 있어 NotificationConsumer가 호출해도 실제 알림 전송이나 이력 저장이 전혀 이뤄지지 않습니다. 이는 본 PR의 핵심 기능을 막는 치명적 문제입니다. 최소한 enabled 채널을 순회해 WebPushService 호출과 NotificationHistoryRepository 저장 로직을 구현해 주세요.
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationSender.java
around lines 17 to 19, the send method is empty so notifications are never sent
or recorded; implement it to iterate over the provided enabled channel list, for
each channel call the appropriate service (e.g., WebPushService.send or other
channel-specific send methods), build and persist a NotificationHistory entity
via NotificationHistoryRepository.save (include payload, channel, status,
timestamp), and catch/log exceptions per-channel so one failure doesn't stop
others; ensure dependencies (WebPushService, NotificationHistoryRepository,
logger) are injected/used and update method signature behavior to
return/propagate errors if required by surrounding code.
🚩 Jira Ticket
SCRUM-117
🗣️ 리뷰 요구사항 (선택)
Summary by CodeRabbit