-
Notifications
You must be signed in to change notification settings - Fork 0
SCRUM-117 식당 주인은 사용자가 예약을 하면 해당 알림을 받을 수 있다 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 경로 제거 및 아티팩트 명 일치
워크플로는 ~/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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
빌드 산출물에 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔티티를 애플리케이션 코드에서 생성/수정할 수 없습니다 — 생성자/빌더 등 도메인 메서드 추가 필요
현재 @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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 비밀키 포함(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 처리 로직 누락으로 메시지 처리 안정성 저하
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
비어 있는 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