Skip to content
Merged
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Kafka
implementation 'org.springframework.kafka:spring-kafka'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.project.controller;

import java.util.Map;
import java.util.UUID;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.project.notification.consumer.UsageNotificationEvent;
import com.project.notification.service.MessageSendService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class TestNotificationController {

private final MessageSendService messageSendService;

/** [테스트용] Kafka 없이 HTTP 요청으로 알림 발송 로직 직접 트리거 */
@PostMapping("/test/send-notification")
public String sendTest(@RequestBody Map<String, Object> request) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

요청 본문을 Map<String, Object>로 받는 것은 타입 안전성을 보장하지 않으며, 런타임에 ClassCastException과 같은 예외를 발생시킬 수 있습니다. 또한 API의 명세를 코드만으로 파악하기 어렵게 만듭니다. 별도의 DTO(Data Transfer Object) 클래스를 정의하여 사용하면 코드가 더 견고해지고 가독성이 향상되며, API 명세가 명확해집니다.


// 1. Postman JSON 데이터를 추출
Long subId = Long.valueOf((Integer) request.get("subId"));
String email = (String) request.get("email");
String phoneNumber = (String) request.get("phoneNumber");
Long templateGroupId = Long.valueOf((Integer) request.get("templateGroupId"));
Comment on lines +28 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

request.get("...")의 결과를 Integer로 캐스팅한 후 Long.valueOf()를 호출하는 것은 ClassCastException을 유발할 수 있습니다. JSON 파서가 숫자를 Integer, Long, Double 등 다양한 타입으로 해석할 수 있기 때문입니다. 더 안전한 방법은 Number 타입으로 캐스팅한 후 longValue()를 호출하는 것입니다.

Suggested change
Long subId = Long.valueOf((Integer) request.get("subId"));
String email = (String) request.get("email");
String phoneNumber = (String) request.get("phoneNumber");
Long templateGroupId = Long.valueOf((Integer) request.get("templateGroupId"));
Long subId = ((Number) request.get("subId")).longValue();
String email = (String) request.get("email");
String phoneNumber = (String) request.get("phoneNumber");
Long templateGroupId = ((Number) request.get("templateGroupId")).longValue();


// variables는 리스트가 포함될 수 있으므로 Object로 캐스팅
Map<String, Object> variables = (Map<String, Object>) request.get("variables");

// 2. 가짜 이벤트 객체(UsageNotificationEvent) 생성
UsageNotificationEvent event =
new UsageNotificationEvent(
UUID.randomUUID(), // 임의의 Event ID 생성
templateGroupId,
new UsageNotificationEvent.SubscriptionInfo(subId, phoneNumber, email),
variables);

log.info("[TEST TRIGGER] subId={}, groupId={}", subId, templateGroupId);

// 3. 서비스 로직 실행 (템플릿 조립 -> Mock Server 전송)
messageSendService.processEvent(event);

return "Test Triggered! EventID: " + event.eventId();
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/project/global/util/MaskingUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.project.global.util;

public final class MaskingUtil {

private MaskingUtil() {}

// 마스킹 010-**12-**12의 형식
public static String maskPhone(String phone) {
if (phone == null || phone.isBlank()) {
return phone;
}

// 숫자만 추출
String digits = phone.replaceAll("\\D", "");

// 휴대폰 번호 길이 최소 검증 (010XXXXXXXX 기준)
if (digits.length() != 11) {
return "***";
}

String first = digits.substring(0, 3);
String middle = digits.substring(3, 7);
String last = digits.substring(7, 11);

// 010-**34-**12
return String.format("%s-**%s-**%s", first, middle.substring(2), last.substring(2));
}

public static String maskEmail(String email) {
if (email == null || email.isBlank()) {
return email;
}

int at = email.indexOf('@');
if (at < 0) {
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

maskEmail 메소드는 이메일 주소에 '@' 문자가 없을 경우 null을 반환합니다. 이는 호출하는 쪽에서 null 처리를 해주지 않으면 NullPointerException을 유발할 수 있는 위험한 방식입니다. 다른 유효하지 않은 입력 케이스처럼 ***와 같은 마스킹된 문자열을 반환하도록 수정하는 것이 더 안전합니다.

Suggested change
return null;
return "***";

} // @ 없으면 null

String local = email.substring(0, at);
String domain = email.substring(at); // "@domain.com"

// local 비어있으면 "***@domain.com"
if (local.isEmpty()) {
return "***" + domain;
}

// local 1글자 이상이면 "첫 글자 + *** + @domain.com"
return local.substring(0, 1) + "***" + domain;
}

public static Object maskByFieldName(String key, Object value) {
if (!(value instanceof String strVal)) return value;
String lowerKey = key.toLowerCase();

if (lowerKey.contains("email")) return maskEmail(strVal);
if (lowerKey.contains("phone") || lowerKey.contains("contact")) return maskPhone(strVal);

return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public record UsageNotificationEvent(
UUID eventId,
Long templateGroupId,
SubscriptionInfo subscriptionInfo,
Map<String, String> variables) {
Map<String, Object> variables) {

public record SubscriptionInfo(Long subId, String phoneNumber, String email) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.Map;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand All @@ -15,7 +14,9 @@
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import com.project.global.util.JsonMapConverter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import com.project.notification.infra.entity.enums.Channel;
import com.project.notification.infra.entity.enums.MessageStatus;

Expand Down Expand Up @@ -68,7 +69,7 @@ public class MessageLog {
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;

@Convert(converter = JsonMapConverter.class)
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "request_payload", columnDefinition = "jsonb")
private Map<String, Object> requestPayload;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ public class TemplateGroup {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "group_id")
private Long groupId;
@Column(name = "id")
private Long id;

@Column(name = "group_code", nullable = false, unique = true, length = 50)
private String groupCode;
@Column(name = "code", nullable = false, unique = true, length = 50)
private String code;

@Column(name = "group_name", nullable = false, length = 100)
private String groupName;
@Column(name = "name", nullable = false, length = 100)
private String name;

@Column(name = "description")
private String description;
Expand All @@ -38,9 +38,9 @@ public class TemplateGroup {
private LocalDateTime createdAt;

@Builder
public TemplateGroup(String groupCode, String groupName, String description) {
this.groupCode = groupCode;
this.groupName = groupName;
public TemplateGroup(String code, String name, String description) {
this.code = code;
this.name = name;
this.description = description;
this.createdAt = LocalDateTime.now();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public class TemplateVersion {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "version_id")
private Long versionId;
@Column(name = "id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "group_id", nullable = false)
Expand All @@ -48,7 +48,7 @@ public class TemplateVersion {
private String body;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 10)
@Column(name = "template_status", nullable = false, length = 10)
private TemplateStatus status;

@Column(name = "version", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@

public interface TemplateGroupJpaRepository extends JpaRepository<TemplateGroup, Long> {

Optional<TemplateGroup> findByGroupCode(String groupCode);
Optional<TemplateGroup> findByCode(String code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

public interface TemplateGroupRepository {

Optional<TemplateGroup> findByGroupCode(String groupCode);
Optional<TemplateGroup> findByCode(String code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class TemplateGroupRepositoryImpl implements TemplateGroupRepository {
private final TemplateGroupJpaRepository templateGroupJpaRepository;

@Override
public Optional<TemplateGroup> findByGroupCode(String groupCode) {
return templateGroupJpaRepository.findByGroupCode(groupCode);
public Optional<TemplateGroup> findByCode(String code) {
return templateGroupJpaRepository.findByCode(code);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@ public interface TemplateVersionJpaRepository extends JpaRepository<TemplateVers

@Query(
"SELECT tv FROM TemplateVersion tv "
+ "WHERE tv.templateGroup.groupCode = :groupCode "
+ "WHERE tv.templateGroup.code = :code "
+ "AND tv.channel = :channel "
+ "AND tv.status = :status "
+ "ORDER BY tv.version DESC "
+ "LIMIT 1")
Optional<TemplateVersion> findLatestByGroupCodeAndChannelAndStatus(
@Param("groupCode") String groupCode,
Optional<TemplateVersion> findLatestByCodeAndChannelAndStatus(
@Param("code") String code,
@Param("channel") Channel channel,
@Param("status") TemplateStatus status);

@Query(
"SELECT tv FROM TemplateVersion tv "
+ "WHERE tv.templateGroup.groupId = :groupId "
+ "WHERE tv.templateGroup.id = :id "
+ "AND tv.channel = :channel "
+ "AND tv.status = :status "
+ "ORDER BY tv.version DESC "
+ "LIMIT 1")
Optional<TemplateVersion> findLatestByGroupIdAndChannelAndStatus(
@Param("groupId") Long groupId,
Optional<TemplateVersion> findLatestByIdAndChannelAndStatus(
@Param("id") Long id,
@Param("channel") Channel channel,
@Param("status") TemplateStatus status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ public class TemplateVersionRepositoryImpl implements TemplateVersionRepository
@Override
public Optional<TemplateVersion> findLatestActiveByGroupCodeAndChannel(
String groupCode, Channel channel) {
return templateVersionJpaRepository.findLatestByGroupCodeAndChannelAndStatus(
return templateVersionJpaRepository.findLatestByCodeAndChannelAndStatus(
groupCode, channel, TemplateStatus.ACTIVE);
}

@Override
public Optional<TemplateVersion> findLatestActiveByGroupIdAndChannel(
Long groupId, Channel channel) {
return templateVersionJpaRepository.findLatestByGroupIdAndChannelAndStatus(
return templateVersionJpaRepository.findLatestByIdAndChannelAndStatus(
groupId, channel, TemplateStatus.ACTIVE);
}
}
14 changes: 2 additions & 12 deletions src/main/java/com/project/notification/sender/EmailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

import com.project.global.util.MaskingUtil;
import com.project.notification.dto.EmailSendRequest;
import com.project.notification.dto.SendResponse;

Expand All @@ -31,7 +32,7 @@ public SendResponse send(EmailSendRequest request) {
log.info(
"[EMAIL] Sending to subId: {}, email: {}, subject: {}",
request.subId(),
maskEmail(request.email()),
MaskingUtil.maskEmail(request.email()),
request.subject());

if (!mockServerEnabled) {
Expand Down Expand Up @@ -62,15 +63,4 @@ public SendResponse send(EmailSendRequest request) {
return new SendResponse(null, "FAIL");
}
}

private String maskEmail(String email) {
if (email == null || email.length() < 5) {
return "***";
}
int atIndex = email.indexOf('@');
if (atIndex <= 1) {
return "***";
}
return email.substring(0, 2) + "***" + email.substring(atIndex);
}
}
10 changes: 2 additions & 8 deletions src/main/java/com/project/notification/sender/SmsSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

import com.project.global.util.MaskingUtil;
import com.project.notification.dto.SendResponse;
import com.project.notification.dto.SmsSendRequest;

Expand All @@ -31,7 +32,7 @@ public SendResponse send(SmsSendRequest request) {
log.info(
"[SMS] Sending to subId: {}, phone: {}",
request.subId(),
maskPhone(request.phone()));
MaskingUtil.maskPhone(request.phone()));

if (!mockServerEnabled) {
String mockMessageId = "mock-sms-" + UUID.randomUUID();
Expand Down Expand Up @@ -61,11 +62,4 @@ public SendResponse send(SmsSendRequest request) {
return new SendResponse(null, "FAIL");
}
}

private String maskPhone(String phone) {
if (phone == null || phone.length() < 4) {
return "***";
}
return phone.substring(0, phone.length() - 4) + "****";
}
}
Loading
Loading