diff --git a/src/main/java/com/project/global/config/CacheProperties.java b/src/main/java/com/project/common/config/CacheProperties.java similarity index 95% rename from src/main/java/com/project/global/config/CacheProperties.java rename to src/main/java/com/project/common/config/CacheProperties.java index 4b982b4..b79b39b 100644 --- a/src/main/java/com/project/global/config/CacheProperties.java +++ b/src/main/java/com/project/common/config/CacheProperties.java @@ -1,4 +1,4 @@ -package com.project.global.config; +package com.project.common.config; import java.time.Duration; import java.util.HashMap; diff --git a/src/main/java/com/project/global/config/JpaConfig.java b/src/main/java/com/project/common/config/JpaConfig.java similarity index 84% rename from src/main/java/com/project/global/config/JpaConfig.java rename to src/main/java/com/project/common/config/JpaConfig.java index ce10e48..dbcb12a 100644 --- a/src/main/java/com/project/global/config/JpaConfig.java +++ b/src/main/java/com/project/common/config/JpaConfig.java @@ -1,4 +1,4 @@ -package com.project.global.config; +package com.project.common.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/com/project/global/config/QueryDslConfig.java b/src/main/java/com/project/common/config/QueryDslConfig.java similarity index 93% rename from src/main/java/com/project/global/config/QueryDslConfig.java rename to src/main/java/com/project/common/config/QueryDslConfig.java index faa0f24..7ddf6dd 100644 --- a/src/main/java/com/project/global/config/QueryDslConfig.java +++ b/src/main/java/com/project/common/config/QueryDslConfig.java @@ -1,4 +1,4 @@ -package com.project.global.config; +package com.project.common.config; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; diff --git a/src/main/java/com/project/global/config/RedisConfig.java b/src/main/java/com/project/common/config/RedisConfig.java similarity index 78% rename from src/main/java/com/project/global/config/RedisConfig.java rename to src/main/java/com/project/common/config/RedisConfig.java index 8f96bfb..e2a8504 100644 --- a/src/main/java/com/project/global/config/RedisConfig.java +++ b/src/main/java/com/project/common/config/RedisConfig.java @@ -1,4 +1,4 @@ -package com.project.global.config; +package com.project.common.config; import java.util.List; @@ -30,15 +30,6 @@ public RedisTemplate familyStringRedisTemplate( return template; } - @Bean - @SuppressWarnings("unchecked") - public DefaultRedisScript> policyConstraintUpdateScript() { - DefaultRedisScript> script = new DefaultRedisScript<>(); - script.setLocation(new ClassPathResource("lua/policy_constraint_update.lua")); - script.setResultType((Class>) (Class) List.class); - return script; - } - @Bean @SuppressWarnings("unchecked") public RedisScript> usageUpdateScript() { diff --git a/src/main/java/com/project/global/config/ThreadPoolConfig.java b/src/main/java/com/project/common/config/ThreadPoolConfig.java similarity index 100% rename from src/main/java/com/project/global/config/ThreadPoolConfig.java rename to src/main/java/com/project/common/config/ThreadPoolConfig.java diff --git a/src/main/java/com/project/common/config/TimeConfig.java b/src/main/java/com/project/common/config/TimeConfig.java new file mode 100644 index 0000000..315eeb1 --- /dev/null +++ b/src/main/java/com/project/common/config/TimeConfig.java @@ -0,0 +1,17 @@ +package com.project.common.config; + +import java.time.Clock; +import java.time.ZoneId; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeConfig { + public static final ZoneId ASIA_SEOUL = ZoneId.of("Asia/Seoul"); + + @Bean + public Clock clock() { + return Clock.system(ASIA_SEOUL); + } +} diff --git a/src/main/java/com/project/global/exception/ApplicationException.java b/src/main/java/com/project/common/exception/ApplicationException.java similarity index 60% rename from src/main/java/com/project/global/exception/ApplicationException.java rename to src/main/java/com/project/common/exception/ApplicationException.java index b6dc8c3..0b014be 100644 --- a/src/main/java/com/project/global/exception/ApplicationException.java +++ b/src/main/java/com/project/common/exception/ApplicationException.java @@ -1,6 +1,6 @@ -package com.project.global.exception; +package com.project.common.exception; -import com.project.global.exception.code.BaseErrorCode; +import com.project.common.exception.code.BaseErrorCode; public class ApplicationException extends BaseException { diff --git a/src/main/java/com/project/global/exception/BaseException.java b/src/main/java/com/project/common/exception/BaseException.java similarity index 86% rename from src/main/java/com/project/global/exception/BaseException.java rename to src/main/java/com/project/common/exception/BaseException.java index 246a57f..6a464a3 100644 --- a/src/main/java/com/project/global/exception/BaseException.java +++ b/src/main/java/com/project/common/exception/BaseException.java @@ -1,6 +1,6 @@ -package com.project.global.exception; +package com.project.common.exception; -import com.project.global.exception.code.BaseErrorCode; +import com.project.common.exception.code.BaseErrorCode; import lombok.Getter; diff --git a/src/main/java/com/project/global/exception/ErrorResponse.java b/src/main/java/com/project/common/exception/ErrorResponse.java similarity index 65% rename from src/main/java/com/project/global/exception/ErrorResponse.java rename to src/main/java/com/project/common/exception/ErrorResponse.java index b0d2f4c..14e2e49 100644 --- a/src/main/java/com/project/global/exception/ErrorResponse.java +++ b/src/main/java/com/project/common/exception/ErrorResponse.java @@ -1,3 +1,3 @@ -package com.project.global.exception; +package com.project.common.exception; public record ErrorResponse(int status, String code, String message) {} diff --git a/src/main/java/com/project/global/exception/code/BaseErrorCode.java b/src/main/java/com/project/common/exception/code/BaseErrorCode.java similarity index 81% rename from src/main/java/com/project/global/exception/code/BaseErrorCode.java rename to src/main/java/com/project/common/exception/code/BaseErrorCode.java index cbe39bd..d4a25de 100644 --- a/src/main/java/com/project/global/exception/code/BaseErrorCode.java +++ b/src/main/java/com/project/common/exception/code/BaseErrorCode.java @@ -1,4 +1,4 @@ -package com.project.global.exception.code; +package com.project.common.exception.code; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/project/global/exception/code/FamilyErrorCode.java b/src/main/java/com/project/common/exception/code/FamilyErrorCode.java similarity index 95% rename from src/main/java/com/project/global/exception/code/FamilyErrorCode.java rename to src/main/java/com/project/common/exception/code/FamilyErrorCode.java index 41369b4..cf4ad27 100644 --- a/src/main/java/com/project/global/exception/code/FamilyErrorCode.java +++ b/src/main/java/com/project/common/exception/code/FamilyErrorCode.java @@ -1,4 +1,4 @@ -package com.project.global.exception.code; +package com.project.common.exception.code; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/project/global/exception/code/GlobalErrorCode.java b/src/main/java/com/project/common/exception/code/GlobalErrorCode.java similarity index 92% rename from src/main/java/com/project/global/exception/code/GlobalErrorCode.java rename to src/main/java/com/project/common/exception/code/GlobalErrorCode.java index 92dead0..42d46c1 100644 --- a/src/main/java/com/project/global/exception/code/GlobalErrorCode.java +++ b/src/main/java/com/project/common/exception/code/GlobalErrorCode.java @@ -1,4 +1,4 @@ -package com.project.global.exception.code; +package com.project.common.exception.code; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/project/global/exception/code/PolicyErrorCode.java b/src/main/java/com/project/common/exception/code/PolicyErrorCode.java similarity index 95% rename from src/main/java/com/project/global/exception/code/PolicyErrorCode.java rename to src/main/java/com/project/common/exception/code/PolicyErrorCode.java index 7b2b440..b2b74ba 100644 --- a/src/main/java/com/project/global/exception/code/PolicyErrorCode.java +++ b/src/main/java/com/project/common/exception/code/PolicyErrorCode.java @@ -1,4 +1,4 @@ -package com.project.global.exception.code; +package com.project.common.exception.code; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/project/global/util/BaseEntity.java b/src/main/java/com/project/common/util/BaseEntity.java similarity index 96% rename from src/main/java/com/project/global/util/BaseEntity.java rename to src/main/java/com/project/common/util/BaseEntity.java index 2752a6c..54ac734 100644 --- a/src/main/java/com/project/global/util/BaseEntity.java +++ b/src/main/java/com/project/common/util/BaseEntity.java @@ -1,4 +1,4 @@ -package com.project.global.util; +package com.project.common.util; import java.time.LocalDateTime; diff --git a/src/main/java/com/project/global/util/LogSanitizer.java b/src/main/java/com/project/common/util/LogSanitizer.java similarity index 96% rename from src/main/java/com/project/global/util/LogSanitizer.java rename to src/main/java/com/project/common/util/LogSanitizer.java index e6d0db5..77328f8 100644 --- a/src/main/java/com/project/global/util/LogSanitizer.java +++ b/src/main/java/com/project/common/util/LogSanitizer.java @@ -1,4 +1,4 @@ -package com.project.global.util; +package com.project.common.util; import java.util.regex.Pattern; diff --git a/src/main/java/com/project/global/util/MapStringObjectConverter.java b/src/main/java/com/project/common/util/MapStringObjectConverter.java similarity index 97% rename from src/main/java/com/project/global/util/MapStringObjectConverter.java rename to src/main/java/com/project/common/util/MapStringObjectConverter.java index 1738281..4067e24 100644 --- a/src/main/java/com/project/global/util/MapStringObjectConverter.java +++ b/src/main/java/com/project/common/util/MapStringObjectConverter.java @@ -1,4 +1,4 @@ -package com.project.global.util; +package com.project.common.util; import java.io.IOException; import java.util.Map; diff --git a/src/main/java/com/project/global/util/RedisKeyGenerator.java b/src/main/java/com/project/common/util/RedisKeyGenerator.java similarity index 99% rename from src/main/java/com/project/global/util/RedisKeyGenerator.java rename to src/main/java/com/project/common/util/RedisKeyGenerator.java index 29c3b79..d36e908 100644 --- a/src/main/java/com/project/global/util/RedisKeyGenerator.java +++ b/src/main/java/com/project/common/util/RedisKeyGenerator.java @@ -1,4 +1,4 @@ -package com.project.global.util; +package com.project.common.util; import java.time.LocalDate; import java.time.format.DateTimeFormatter; diff --git a/src/main/java/com/project/domain/customer/entity/Customer.java b/src/main/java/com/project/domain/customer/entity/Customer.java index 378cb13..520e97b 100644 --- a/src/main/java/com/project/domain/customer/entity/Customer.java +++ b/src/main/java/com/project/domain/customer/entity/Customer.java @@ -7,7 +7,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/com/project/domain/customer/entity/CustomerQuota.java b/src/main/java/com/project/domain/customer/entity/CustomerQuota.java index a0a35f7..ed1f37c 100644 --- a/src/main/java/com/project/domain/customer/entity/CustomerQuota.java +++ b/src/main/java/com/project/domain/customer/entity/CustomerQuota.java @@ -10,7 +10,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/family/entity/Family.java b/src/main/java/com/project/domain/family/entity/Family.java index c28febd..3980fbb 100644 --- a/src/main/java/com/project/domain/family/entity/Family.java +++ b/src/main/java/com/project/domain/family/entity/Family.java @@ -7,7 +7,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/family/entity/FamilyMember.java b/src/main/java/com/project/domain/family/entity/FamilyMember.java index 0c54440..fe84573 100644 --- a/src/main/java/com/project/domain/family/entity/FamilyMember.java +++ b/src/main/java/com/project/domain/family/entity/FamilyMember.java @@ -9,8 +9,8 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import com.project.common.util.BaseEntity; import com.project.domain.customer.enums.RoleType; -import com.project.global.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/family/entity/FamilyQuota.java b/src/main/java/com/project/domain/family/entity/FamilyQuota.java index f9ab290..b008667 100644 --- a/src/main/java/com/project/domain/family/entity/FamilyQuota.java +++ b/src/main/java/com/project/domain/family/entity/FamilyQuota.java @@ -10,7 +10,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/policy/constant/PolicyConstraintKeyConstants.java b/src/main/java/com/project/domain/policy/constant/PolicyConstraintKeyConstants.java deleted file mode 100644 index 60eccc5..0000000 --- a/src/main/java/com/project/domain/policy/constant/PolicyConstraintKeyConstants.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.project.domain.policy.constant; - -public final class PolicyConstraintKeyConstants { - public static final String BLOCK_ACCESS = "BLOCK:ACCESS"; - public static final String BLOCK_APP = "BLOCK:APP"; - public static final String BLOCK_APP_PREFIX = "BLOCK:APP:"; - public static final String BLOCK_TIME = "BLOCK:TIME"; - public static final String LIMIT_DATA_MONTHLY = "LIMIT:DATA:MONTHLY"; - public static final String VERSION_FIELD_PREFIX = "ver:"; - - private PolicyConstraintKeyConstants() {} -} diff --git a/src/main/java/com/project/domain/policy/constant/PolicyRuleKeyConstants.java b/src/main/java/com/project/domain/policy/constant/PolicyRuleKeyConstants.java deleted file mode 100644 index 122f8a0..0000000 --- a/src/main/java/com/project/domain/policy/constant/PolicyRuleKeyConstants.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.project.domain.policy.constant; - -public final class PolicyRuleKeyConstants { - public static final String LIMIT_BYTES = "limitBytes"; - public static final String START = "start"; - public static final String END = "end"; - public static final String REASON = "reason"; - public static final String BLOCKED_APPS = "blockedApps"; - - private PolicyRuleKeyConstants() {} -} diff --git a/src/main/java/com/project/domain/policy/entity/Policy.java b/src/main/java/com/project/domain/policy/entity/Policy.java index 0f6586d..055fd99 100644 --- a/src/main/java/com/project/domain/policy/entity/Policy.java +++ b/src/main/java/com/project/domain/policy/entity/Policy.java @@ -12,10 +12,10 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import com.project.common.util.BaseEntity; +import com.project.common.util.MapStringObjectConverter; import com.project.domain.customer.enums.RoleType; import com.project.domain.policy.enums.PolicyType; -import com.project.global.util.BaseEntity; -import com.project.global.util.MapStringObjectConverter; import lombok.AccessLevel; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/project/domain/policy/entity/PolicyAssignment.java b/src/main/java/com/project/domain/policy/entity/PolicyAssignment.java index 3c3a7f3..ac0cf3c 100644 --- a/src/main/java/com/project/domain/policy/entity/PolicyAssignment.java +++ b/src/main/java/com/project/domain/policy/entity/PolicyAssignment.java @@ -9,7 +9,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/project/domain/policy/enums/PolicyType.java b/src/main/java/com/project/domain/policy/enums/PolicyType.java index 17c38d2..e1ef980 100644 --- a/src/main/java/com/project/domain/policy/enums/PolicyType.java +++ b/src/main/java/com/project/domain/policy/enums/PolicyType.java @@ -1,9 +1,15 @@ package com.project.domain.policy.enums; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum PolicyType { - MONTHLY_LIMIT, - TIME_BLOCK, - MANUAL_BLOCK, - APP_BLOCK, - WEBSITE_BLOCK; + MONTHLY_LIMIT("LIMIT:DATA:MONTHLY"), + TIME_BLOCK("BLOCK:TIME"), + MANUAL_BLOCK("BLOCK:ACCESS"), + APP_BLOCK("BLOCK:APP"); + + private final String redisKey; } diff --git a/src/main/java/com/project/domain/policy/enums/PolicyUpdateStatus.java b/src/main/java/com/project/domain/policy/enums/PolicyUpdateStatus.java deleted file mode 100644 index 01eece1..0000000 --- a/src/main/java/com/project/domain/policy/enums/PolicyUpdateStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.project.domain.policy.enums; - -public enum PolicyUpdateStatus { - APPLIED -} diff --git a/src/main/java/com/project/domain/policy/service/helper/PolicyAssignmentSyncHelper.java b/src/main/java/com/project/domain/policy/helper/PolicyAssignmentSyncHelper.java similarity index 74% rename from src/main/java/com/project/domain/policy/service/helper/PolicyAssignmentSyncHelper.java rename to src/main/java/com/project/domain/policy/helper/PolicyAssignmentSyncHelper.java index 893206a..4d1e02e 100644 --- a/src/main/java/com/project/domain/policy/service/helper/PolicyAssignmentSyncHelper.java +++ b/src/main/java/com/project/domain/policy/helper/PolicyAssignmentSyncHelper.java @@ -1,4 +1,4 @@ -package com.project.domain.policy.service.helper; +package com.project.domain.policy.helper; import java.util.LinkedHashMap; import java.util.List; @@ -11,14 +11,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.project.domain.policy.constant.PolicyRuleKeyConstants; import com.project.domain.policy.entity.Policy; import com.project.domain.policy.entity.PolicyAssignment; import com.project.domain.policy.enums.PolicyType; import com.project.domain.policy.infra.cache.dto.PolicyConstraintRedisHash; import com.project.domain.policy.repository.PolicyAssignmentRepository; import com.project.domain.policy.repository.PolicyRepository; -import com.project.global.common.TimeConstants; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -81,10 +79,8 @@ private void applyAssignmentConstraints( Policy policy = policyById.get(assignment.getPolicyId()); Map rules = parseRulesToMap(assignment.getRules()); - long assignmentVersion = resolveAssignmentVersion(assignment); - // ERD 표준: policyType + rules JSON 스키마를 Redis constraints로 변환 - applyErdRulesByPolicyType(policy.getPolicyType(), rules, constraints, assignmentVersion); + applyErdRulesByPolicyType(policy.getPolicyType(), rules, constraints); } // 제약 계산 대상인지(활성 + 유효 policy 존재) 판별 @@ -97,15 +93,13 @@ private boolean isApplicableAssignment( private void applyErdRulesByPolicyType( PolicyType policyType, Map rules, - PolicyConstraintRedisHash constraints, - long assignmentVersion) { + PolicyConstraintRedisHash constraints) { // policy type마다 rules JSON 스키마가 다르므로 전용 변환기로 분기 switch (policyType) { - case MONTHLY_LIMIT -> - applyMonthlyLimitConstraint(rules, constraints, assignmentVersion); - case TIME_BLOCK -> applyTimeBlockConstraint(rules, constraints, assignmentVersion); - case MANUAL_BLOCK -> applyManualBlockConstraint(rules, constraints, assignmentVersion); - case APP_BLOCK -> applyAppBlockConstraint(rules, constraints, assignmentVersion); + case MONTHLY_LIMIT -> applyMonthlyLimitConstraint(rules, constraints); + case TIME_BLOCK -> applyTimeBlockConstraint(rules, constraints); + case MANUAL_BLOCK -> applyManualBlockConstraint(rules, constraints); + case APP_BLOCK -> applyAppBlockConstraint(rules, constraints); default -> log.warn( "Unsupported policy type for ERD rules conversion. policyType={}", @@ -115,50 +109,42 @@ private void applyErdRulesByPolicyType( // 월 제한 정책의 rules를 LIMIT:DATA:MONTHLY 제약으로 변환 private void applyMonthlyLimitConstraint( - Map rules, - PolicyConstraintRedisHash constraints, - long assignmentVersion) { + Map rules, PolicyConstraintRedisHash constraints) { // limitBytes(ERD) -> LIMIT:DATA:MONTHLY(Redis) - Long limitBytes = toPositiveLong(rules.get(PolicyRuleKeyConstants.LIMIT_BYTES)); + Long limitBytes = toPositiveLong(rules.get(PolicyConstraintMapper.LIMIT_BYTES)); if (limitBytes == null) { return; } - constraints.putMonthlyLimit(limitBytes, assignmentVersion); + constraints.putMonthlyLimit(limitBytes); } // 시간대 차단 정책의 rules를 시작/종료 제약으로 변환 private void applyTimeBlockConstraint( - Map rules, - PolicyConstraintRedisHash constraints, - long assignmentVersion) { + Map rules, PolicyConstraintRedisHash constraints) { // start/end(ERD, HH:mm) -> BLOCK:TIME(Redis, HHMM-HHMM) - String start = toHhmm(rules.get(PolicyRuleKeyConstants.START)); - String end = toHhmm(rules.get(PolicyRuleKeyConstants.END)); + String start = toHhmm(rules.get(PolicyConstraintMapper.START)); + String end = toHhmm(rules.get(PolicyConstraintMapper.END)); if (start != null && end != null) { - constraints.putTimeBlockRange(start + "-" + end, assignmentVersion); + constraints.putTimeBlockRange(start + "-" + end); } } // 수동 차단 정책의 rules를 접근 차단 제약으로 변환 private void applyManualBlockConstraint( - Map rules, - PolicyConstraintRedisHash constraints, - long assignmentVersion) { + Map rules, PolicyConstraintRedisHash constraints) { // reason 값이 존재하면 접근 차단 활성화로 간주 - if (rules.get(PolicyRuleKeyConstants.REASON) == null) { + if (rules.get(PolicyConstraintMapper.REASON) == null) { return; } - constraints.putManualBlock(assignmentVersion); + constraints.putManualBlock(); } // 앱 차단 배열을 개별 BLOCK:APP:{appId} 제약들로 확장 private void applyAppBlockConstraint( - Map rules, - PolicyConstraintRedisHash constraints, - long assignmentVersion) { + Map rules, PolicyConstraintRedisHash constraints) { // blockedApps 배열의 각 appId를 BLOCK:APP:{appId}=1 제약으로 반영 - Object blockedAppsObj = rules.get(PolicyRuleKeyConstants.BLOCKED_APPS); + Object blockedAppsObj = rules.get(PolicyConstraintMapper.BLOCKED_APPS); if (!(blockedAppsObj instanceof List blockedApps)) { return; } @@ -167,7 +153,7 @@ private void applyAppBlockConstraint( .map(String::valueOf) .map(appId -> appId.trim().toLowerCase(Locale.ROOT)) .filter(appId -> !appId.isBlank()) - .forEach(appId -> constraints.putBlockedApp(appId, assignmentVersion)); + .forEach(constraints::putBlockedApp); } // rules JSON 문자열을 Map으로 파싱하고 실패 시 빈 맵으로 대체 @@ -183,25 +169,6 @@ private Map parseRulesToMap(String rulesJson) { } } - // assignment의 시간 정보를 epoch millis 버전으로 변환 - private long resolveAssignmentVersion(PolicyAssignment assignment) { - if (assignment.getUpdatedAt() != null) { - return assignment - .getUpdatedAt() - .atZone(TimeConstants.ASIA_SEOUL) - .toInstant() - .toEpochMilli(); - } - if (assignment.getCreatedAt() != null) { - return assignment - .getCreatedAt() - .atZone(TimeConstants.ASIA_SEOUL) - .toInstant() - .toEpochMilli(); - } - return System.currentTimeMillis(); - } - // 양수 long 값만 허용하고 나머지는 null로 처리 private Long toPositiveLong(Object value) { if (value == null) { diff --git a/src/main/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapper.java b/src/main/java/com/project/domain/policy/helper/PolicyConstraintMapper.java similarity index 78% rename from src/main/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapper.java rename to src/main/java/com/project/domain/policy/helper/PolicyConstraintMapper.java index 6617b4a..d7252fc 100644 --- a/src/main/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapper.java +++ b/src/main/java/com/project/domain/policy/helper/PolicyConstraintMapper.java @@ -1,4 +1,4 @@ -package com.project.domain.policy.service.helper; +package com.project.domain.policy.helper; import java.util.LinkedHashSet; import java.util.List; @@ -11,53 +11,56 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; -import com.project.domain.policy.constant.PolicyRuleKeyConstants; +import com.project.domain.policy.enums.PolicyType; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor -public class PolicyConstraintEventMapper { +public class PolicyConstraintMapper { + public static final String LIMIT_BYTES = "limitBytes"; + public static final String START = "start"; + public static final String END = "end"; + public static final String REASON = "reason"; + public static final String BLOCKED_APPS = "blockedApps"; private static final Pattern HHMM_PATTERN = Pattern.compile("^\\d{4}$"); private final ObjectMapper objectMapper; - public String normalizeValue(String policyKey, String newValue) { + public String normalizeValue(PolicyType type, String newValue) { // 삭제 이벤트(빈 값)는 Redis에서 HDEL 대상이 되도록 null 반환 if (isBlank(newValue)) { return null; } - // policyKey별 rules JSON 스키마를 Redis 저장 포맷으로 정규화 - return switch (policyKey) { - case PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY -> normalizeMonthlyLimit(newValue); - case PolicyConstraintKeyConstants.BLOCK_TIME -> normalizeTimeBlock(newValue); - case PolicyConstraintKeyConstants.BLOCK_ACCESS -> normalizeManualBlock(newValue); - case PolicyConstraintKeyConstants.BLOCK_APP -> normalizeAppBlock(newValue); - default -> throw new IllegalArgumentException("Unsupported policy key: " + policyKey); + // PolicyType별 rules JSON 스키마를 Redis 저장 포맷으로 정규화 + return switch (type) { + case MONTHLY_LIMIT -> normalizeMonthlyLimit(newValue); + case TIME_BLOCK -> normalizeTimeBlock(newValue); + case MANUAL_BLOCK -> normalizeManualBlock(newValue); + case APP_BLOCK -> normalizeAppBlock(newValue); + default -> throw new IllegalArgumentException("Unsupported policy type: " + type); }; } private String normalizeMonthlyLimit(String newValue) { // {"limitBytes": 123} -> "123" JsonNode rules = parseRulesJson(newValue); - JsonNode limitBytesNode = rules.get(PolicyRuleKeyConstants.LIMIT_BYTES); - return String.valueOf(toPositiveLong(limitBytesNode, PolicyRuleKeyConstants.LIMIT_BYTES)); + JsonNode limitBytesNode = rules.get(LIMIT_BYTES); + return String.valueOf(toPositiveLong(limitBytesNode, LIMIT_BYTES)); } private String normalizeTimeBlock(String newValue) { // {"start":"22:00","end":"07:00"} -> "2200-0700" JsonNode rules = parseRulesJson(newValue); - String start = - toHhmm(rules.get(PolicyRuleKeyConstants.START), PolicyRuleKeyConstants.START); - String end = toHhmm(rules.get(PolicyRuleKeyConstants.END), PolicyRuleKeyConstants.END); + String start = toHhmm(rules.get(START), START); + String end = toHhmm(rules.get(END), END); return start + "-" + end; } private String normalizeManualBlock(String newValue) { // reason 존재 여부로 수동 차단 활성화 판단 -> Redis 값은 "1" JsonNode rules = parseRulesJson(newValue); - JsonNode reasonNode = rules.get(PolicyRuleKeyConstants.REASON); + JsonNode reasonNode = rules.get(REASON); if (reasonNode == null || reasonNode.isNull() || reasonNode.asText().isBlank()) { throw new IllegalArgumentException("MANUAL_BLOCK requires reason"); } @@ -70,12 +73,12 @@ private String normalizeAppBlock(String newValue) { } public Set normalizeAppBlockValueAsSet(String newValue) { - // BLOCK:APP 동기화용으로 앱 ID 집합을 직접 반환 + // APP_BLOCK 동기화용으로 앱 ID 집합을 직접 반환 if (isBlank(newValue)) { return Set.of(); } JsonNode rules = parseRulesJson(newValue); - JsonNode blockedAppsNode = rules.get(PolicyRuleKeyConstants.BLOCKED_APPS); + JsonNode blockedAppsNode = rules.get(BLOCKED_APPS); if (blockedAppsNode == null || !blockedAppsNode.isArray()) { throw new IllegalArgumentException("blockedApps must be an array"); } diff --git a/src/main/java/com/project/domain/policy/service/helper/PolicyConstraintWarmupHelper.java b/src/main/java/com/project/domain/policy/helper/PolicyConstraintWarmupHelper.java similarity index 95% rename from src/main/java/com/project/domain/policy/service/helper/PolicyConstraintWarmupHelper.java rename to src/main/java/com/project/domain/policy/helper/PolicyConstraintWarmupHelper.java index 950394d..4cfc8da 100644 --- a/src/main/java/com/project/domain/policy/service/helper/PolicyConstraintWarmupHelper.java +++ b/src/main/java/com/project/domain/policy/helper/PolicyConstraintWarmupHelper.java @@ -1,11 +1,11 @@ -package com.project.domain.policy.service.helper; +package com.project.domain.policy.helper; import java.util.Map; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import com.project.global.util.RedisKeyGenerator; +import com.project.common.util.RedisKeyGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHash.java b/src/main/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHash.java index 8039f6f..78d2535 100644 --- a/src/main/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHash.java +++ b/src/main/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHash.java @@ -4,7 +4,7 @@ import java.util.Locale; import java.util.Map; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; +import com.project.domain.policy.enums.PolicyType; public final class PolicyConstraintRedisHash { private final Map hash; @@ -17,39 +17,27 @@ public static PolicyConstraintRedisHash create() { return new PolicyConstraintRedisHash(new LinkedHashMap<>()); } - public void putMonthlyLimit(long limitBytes, long version) { - putWithVersion( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, - String.valueOf(limitBytes), - version); + public void putMonthlyLimit(long limitBytes) { + put(PolicyType.MONTHLY_LIMIT.getRedisKey(), String.valueOf(limitBytes)); } - public void putTimeBlockRange(String timeRange, long version) { - putWithVersion(PolicyConstraintKeyConstants.BLOCK_TIME, timeRange, version); + public void putTimeBlockRange(String timeRange) { + put(PolicyType.TIME_BLOCK.getRedisKey(), timeRange); } - public void putManualBlock(long version) { - putWithVersion(PolicyConstraintKeyConstants.BLOCK_ACCESS, "1", version); + public void putManualBlock() { + put(PolicyType.MANUAL_BLOCK.getRedisKey(), "1"); } - public void putBlockedApp(String appId, long version) { - putWithVersion( - PolicyConstraintKeyConstants.BLOCK_APP_PREFIX - + appId.trim().toLowerCase(Locale.ROOT), - "1", - version); + public void putBlockedApp(String appId) { + put(PolicyType.APP_BLOCK.getRedisKey() + ":" + appId.trim().toLowerCase(Locale.ROOT), "1"); } public Map toMap() { return hash; } - private void putWithVersion(String key, String value, long version) { + private void put(String key, String value) { hash.put(key, value); - hash.put(buildVersionField(key), String.valueOf(version)); - } - - private String buildVersionField(String key) { - return PolicyConstraintKeyConstants.VERSION_FIELD_PREFIX + key; } } diff --git a/src/main/java/com/project/domain/policy/infra/messaging/PolicyKafkaConsumer.java b/src/main/java/com/project/domain/policy/infra/messaging/PolicyKafkaConsumer.java deleted file mode 100644 index 1b8ea9a..0000000 --- a/src/main/java/com/project/domain/policy/infra/messaging/PolicyKafkaConsumer.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.project.domain.policy.infra.messaging; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.stereotype.Component; - -import com.dabom.messaging.kafka.contract.KafkaConsumerGroups; -import com.dabom.messaging.kafka.contract.KafkaEventTypes; -import com.dabom.messaging.kafka.contract.KafkaTopics; -import com.dabom.messaging.kafka.event.KafkaEventMessageSupport; -import com.dabom.messaging.kafka.event.consumer.KafkaEventConsumer; -import com.dabom.messaging.kafka.event.dto.EventEnvelope; -import com.dabom.messaging.kafka.event.dto.policy.PolicyUpdatedPayload; -import com.fasterxml.jackson.core.type.TypeReference; -import com.project.domain.policy.service.PolicyConstraintSyncService; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class PolicyKafkaConsumer implements KafkaEventConsumer { - - private final KafkaEventMessageSupport kafkaEventMessageSupport; - private final PolicyConstraintSyncService policyConstraintSyncService; - - @KafkaListener( - topics = KafkaTopics.POLICY_UPDATED, - groupId = KafkaConsumerGroups.DABOM_PROCESSOR_USAGE_POLICY) - public void consume(ConsumerRecord consumerRecord) { - consume(consumerRecord, kafkaEventMessageSupport); - } - - @Override - public String eventType() { - return KafkaEventTypes.POLICY_UPDATED; - } - - @Override - public TypeReference> typeReference() { - return new TypeReference<>() {}; - } - - @Override - public void handle(EventEnvelope envelope, String recordKey) { - policyConstraintSyncService.sync(envelope, recordKey); - } -} diff --git a/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncService.java b/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncService.java deleted file mode 100644 index 01cffd7..0000000 --- a/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncService.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.project.domain.policy.service; - -import com.dabom.messaging.kafka.event.dto.EventEnvelope; -import com.dabom.messaging.kafka.event.dto.policy.PolicyUpdatedPayload; - -public interface PolicyConstraintSyncService { - void sync(EventEnvelope envelope, String recordKey); -} diff --git a/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImpl.java b/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImpl.java deleted file mode 100644 index 21b37bb..0000000 --- a/src/main/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImpl.java +++ /dev/null @@ -1,463 +0,0 @@ -package com.project.domain.policy.service; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.script.RedisScript; -import org.springframework.stereotype.Service; - -import com.dabom.messaging.kafka.event.dto.EventEnvelope; -import com.dabom.messaging.kafka.event.dto.policy.PolicyUpdatedPayload; -import com.project.domain.family.repository.FamilyMemberRepository; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; -import com.project.domain.policy.service.helper.PolicyConstraintEventMapper; -import com.project.domain.policy.service.helper.PolicyEventValidator; -import com.project.global.common.TimeConstants; -import com.project.global.exception.ApplicationException; -import com.project.global.exception.code.PolicyErrorCode; -import com.project.global.util.LogSanitizer; -import com.project.global.util.RedisKeyGenerator; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PolicyConstraintSyncServiceImpl implements PolicyConstraintSyncService { - private static final String VALUE_LOG_SUFFIX = ", value={}"; - private static final String LUA_RESULT_APPLIED = "APPLIED"; - private static final String SKIP_REASON_MISSING_CONSTRAINTS_KEY = "MISSING_CONSTRAINTS_KEY"; - - private final RedisTemplate familyStringRedisTemplate; - private final RedisScript> policyConstraintUpdateScript; - private final RedisKeyGenerator redisKeyGenerator; - private final FamilyMemberRepository familyMemberRepository; - private final PolicyEventValidator policyEventValidator; - private final PolicyConstraintEventMapper policyConstraintEventMapper; - private final LogSanitizer logSanitizer; - - @Value("${app.kafka.dedup.policy-ttl-seconds}") - private long dedupTtlSeconds; - - // policy-updated 이벤트의 진입점: - // 1) payload 검증 -> 2) Lua 적용 - @Override - public void sync(EventEnvelope envelope, String recordKey) { - PolicyUpdatedPayload payload = envelope.payload(); - String eventId = envelope.eventId(); - - // payload/eventId/필수값 검증 - if (!policyEventValidator.isValidPayload(payload, eventId, recordKey)) { - return; - } - - // 이벤트 payload에서 정책 키/대상/버전을 추출 - String policyKey = payload.policyKey(); - Long targetCustomerId = payload.targetCustomerId(); - // timestamp를 버전으로 사용해 역순 이벤트에서도 최신값만 반영 - long eventVersion = resolveEventVersion(envelope); - - // 삭제/갱신 모두 policyKey whitelist 검증 - if (!policyEventValidator.isAllowedPolicyKey(policyKey)) { - log.warn( - "Invalid policy key. eventId={}, familyId={}, customerId={}, field={}", - logSanitizer.sanitize(eventId), - payload.familyId(), - targetCustomerId, - logSanitizer.sanitize(policyKey)); - return; - } - - // 정책 타입별로 Redis 저장 형식에 맞는 값으로 정규화 - NormalizedPolicyValue normalizedPolicyValue = - resolveNormalizedPolicyValue(payload, eventId, targetCustomerId); - if (normalizedPolicyValue == null) { - return; - } - - // familyId/targetCustomerId가 모두 없으면 전체 활성 구성원에 대해 정책을 반영 - if (payload.familyId() == null && targetCustomerId == null) { - processGlobalPolicyUpdate(eventId, policyKey, eventVersion, normalizedPolicyValue); - return; - } - - // targetCustomerId가 있으면 해당 customer만 반영 - if (targetCustomerId != null) { - // family-customer 소속 관계 검증 - if (!isValidFamilyMember(payload.familyId(), targetCustomerId)) { - log.warn( - "Skip policy update due to invalid family-customer relation." - + " eventId={}, familyId={}, customerId={}, field={}", - logSanitizer.sanitize(eventId), - payload.familyId(), - targetCustomerId, - logSanitizer.sanitize(policyKey)); - return; - } - - // 단일 고객 정책 업데이트(Redis 반영) - processCustomerPolicyUpdate( - eventId, - payload.familyId(), - targetCustomerId, - policyKey, - eventVersion, - normalizedPolicyValue); - return; - } - - // targetCustomerId가 없으면 family 전체(active customer)에게 반영 - List customers = - familyMemberRepository.findAllActiveTargetsByFamilyId(payload.familyId()); - int appliedCount = 0; - int skippedCount = 0; - - // family 구성원 단위로 동일 정책을 순차 반영 - for (FamilyMemberRepository.FamilyMemberTargetProjection customer : customers) { - boolean applied = - processCustomerPolicyUpdate( - eventId, - customer.getFamilyId(), - customer.getCustomerId(), - policyKey, - eventVersion, - normalizedPolicyValue); - if (applied) { - appliedCount++; - } else { - skippedCount++; - } - } - - log.info( - "Processed family-wide constraint. eventId={}, familyId={}, appliedCount={}," - + " skippedCount={}, field={}" - + VALUE_LOG_SUFFIX, - logSanitizer.sanitize(eventId), - payload.familyId(), - appliedCount, - skippedCount, - logSanitizer.sanitize(policyKey), - logSanitizer.sanitize(normalizedPolicyValue.normalizedNewValue())); - } - - private void processGlobalPolicyUpdate( - String eventId, - String policyKey, - long eventVersion, - NormalizedPolicyValue normalizedPolicyValue) { - List members = - familyMemberRepository.findAllActiveTargets(); - AtomicInteger appliedCount = new AtomicInteger(0); - AtomicInteger skippedCount = new AtomicInteger(0); - - members.parallelStream() - .forEach( - member -> { - boolean applied = - processCustomerPolicyUpdate( - eventId, - member.getFamilyId(), - member.getCustomerId(), - policyKey, - eventVersion, - normalizedPolicyValue); - if (applied) { - appliedCount.incrementAndGet(); - } else { - skippedCount.incrementAndGet(); - } - }); - - log.info( - "Processed global constraint. eventId={}, appliedCount={}, skippedCount={}," - + " field={}" - + VALUE_LOG_SUFFIX, - logSanitizer.sanitize(eventId), - appliedCount, - skippedCount, - logSanitizer.sanitize(policyKey), - logSanitizer.sanitize(normalizedPolicyValue.normalizedNewValue())); - } - - private NormalizedPolicyValue resolveNormalizedPolicyValue( - PolicyUpdatedPayload payload, String eventId, Long targetCustomerId) { - // 비활성화 정책이면 삭제(HDEL)로 처리되도록 null 값을 전달 - if (!payload.isActive()) { - return new NormalizedPolicyValue(null, Set.of()); - } - - try { - if (PolicyConstraintKeyConstants.BLOCK_APP.equals(payload.policyKey())) { - // BLOCK:APP은 앱 목록 집합 + CSV 문자열을 함께 계산 - Set normalizedBlockedApps = - policyConstraintEventMapper.normalizeAppBlockValueAsSet(payload.newValue()); - return new NormalizedPolicyValue( - String.join(",", normalizedBlockedApps), normalizedBlockedApps); - } - - return new NormalizedPolicyValue( - policyConstraintEventMapper.normalizeValue( - payload.policyKey(), payload.newValue()), - Set.of()); - } catch (IllegalArgumentException e) { - log.warn( - "Invalid policy value. eventId={}, familyId={}, customerId={}, field={}," - + " rawValue={}, reason={}", - logSanitizer.sanitize(eventId), - payload.familyId(), - targetCustomerId, - logSanitizer.sanitize(payload.policyKey()), - logSanitizer.sanitize(payload.newValue()), - logSanitizer.sanitize(e.getMessage())); - return null; - } - } - - // family 내 유효한 customer인지 확인 - private boolean isValidFamilyMember(Long familyId, Long customerId) { - return familyMemberRepository.existsByFamilyIdAndCustomerIdAndDeletedAtIsNull( - familyId, customerId); - } - - private boolean processCustomerPolicyUpdate( - String eventId, - Long familyId, - Long customerId, - String policyKey, - long eventVersion, - NormalizedPolicyValue normalizedPolicyValue) { - // 업데이트 이벤트는 기존 캐시 갱신만 담당하고, 캐시 미스는 스킵 - if (!hasConstraintsKey(familyId, customerId)) { - logResult( - eventId, - familyId, - customerId, - policyKey, - normalizedPolicyValue.normalizedNewValue(), - SKIP_REASON_MISSING_CONSTRAINTS_KEY); - return false; - } - - if (PolicyConstraintKeyConstants.BLOCK_APP.equals(policyKey)) { - // BLOCK:APP은 현재 Redis 상태와 목표 앱 목록을 diff로 동기화 - boolean changed = - syncBlockedAppsToCustomer( - eventId, - familyId, - customerId, - normalizedPolicyValue.normalizedBlockedApps(), - eventVersion); - logResult( - eventId, - familyId, - customerId, - policyKey, - normalizedPolicyValue.normalizedNewValue(), - changed ? LUA_RESULT_APPLIED : "NO_CHANGES"); - return changed; - } - - // 일반 정책은 Lua 원자 연산으로 단건 반영 - String result = - applyConstraintToCustomer( - eventId, - eventVersion, - familyId, - customerId, - policyKey, - normalizedPolicyValue.normalizedNewValue()); - logResult( - eventId, - familyId, - customerId, - policyKey, - normalizedPolicyValue.normalizedNewValue(), - result); - return LUA_RESULT_APPLIED.equals(result); - } - - private boolean hasConstraintsKey(Long familyId, Long customerId) { - String constraintsKey = - redisKeyGenerator.generateFamilyCustomerConstraintsKey(familyId, customerId); - Boolean exists = familyStringRedisTemplate.hasKey(constraintsKey); - return Boolean.TRUE.equals(exists); - } - - private boolean syncBlockedAppsToCustomer( - String eventId, - Long familyId, - Long customerId, - Set desiredBlockedApps, - long eventVersion) { - // 현재 Redis에 저장된 앱 차단 필드(BLOCK:APP:{appId})를 조회 - String constraintsKey = - redisKeyGenerator.generateFamilyCustomerConstraintsKey(familyId, customerId); - Set currentBlockedApps = loadBlockedApps(constraintsKey); - - // 현재값 기준으로 삭제/추가 대상(diff)을 계산 - Set appsToDelete = new LinkedHashSet<>(currentBlockedApps); - appsToDelete.removeAll(desiredBlockedApps); - - Set appsToAdd = new LinkedHashSet<>(desiredBlockedApps); - appsToAdd.removeAll(currentBlockedApps); - - if (appsToDelete.isEmpty() && appsToAdd.isEmpty()) { - return false; - } - - int appliedCount = 0; - - // 앱별 Lua 실행으로 dedup/stale/version 검증을 동일하게 적용 - for (String appId : appsToDelete) { - String appField = PolicyConstraintKeyConstants.BLOCK_APP_PREFIX + appId; - String result = - applyConstraintToCustomer( - eventId + ":" + appField, - eventVersion, - familyId, - customerId, - appField, - null); - if (LUA_RESULT_APPLIED.equals(result)) { - appliedCount++; - } - } - - for (String appId : appsToAdd) { - String appField = PolicyConstraintKeyConstants.BLOCK_APP_PREFIX + appId; - String result = - applyConstraintToCustomer( - eventId + ":" + appField, - eventVersion, - familyId, - customerId, - appField, - "1"); - if (LUA_RESULT_APPLIED.equals(result)) { - appliedCount++; - } - } - - return appliedCount > 0; - } - - private Set loadBlockedApps(String constraintsKey) { - // constraints hash의 field 목록 중 BLOCK:APP: prefix만 추출해 앱 ID 집합으로 변환 - Set fields = familyStringRedisTemplate.opsForHash().keys(constraintsKey); - if (fields.isEmpty()) { - return Set.of(); - } - - return fields.stream() - .map(String::valueOf) - .filter(field -> field.startsWith(PolicyConstraintKeyConstants.BLOCK_APP_PREFIX)) - .map( - field -> - field.substring( - PolicyConstraintKeyConstants.BLOCK_APP_PREFIX.length())) - .filter(appId -> !appId.isBlank()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private String applyConstraintToCustomer( - String eventId, - long eventVersion, - Long familyId, - Long customerId, - String policyKey, - String newValue) { - // dedupKey: 동일 이벤트 재처리 방지, constraintsKey: 실제 제약값 저장 키 - String dedupKey = redisKeyGenerator.generatePolicyEventDedupKey(eventId, customerId); - String constraintsKey = - redisKeyGenerator.generateFamilyCustomerConstraintsKey(familyId, customerId); - // 삭제 이벤트는 빈 문자열로 정규화해 Lua 스크립트에서 일관되게 처리 - String normalizedNewValue = (newValue == null || newValue.isBlank()) ? "" : newValue; - - List result; - try { - // Lua 스크립트가 dedup + stale + HSET/HDEL을 원자적으로 수행한다. - result = - familyStringRedisTemplate.execute( - policyConstraintUpdateScript, - List.of(dedupKey, constraintsKey), - String.valueOf(dedupTtlSeconds), - policyKey, - normalizedNewValue, - String.valueOf(eventVersion)); - } catch (Exception e) { - // Redis/Lua 실패는 비즈니스 예외로 전환해 상위에서 실패를 인지하게 함 - log.error( - "Failed to sync policy constraint to Redis. eventId={}, familyId={}," - + " customerId={}, field={}" - + VALUE_LOG_SUFFIX, - logSanitizer.sanitize(eventId), - familyId, - customerId, - logSanitizer.sanitize(policyKey), - logSanitizer.sanitize(newValue), - e); - throw new ApplicationException(PolicyErrorCode.POLICY_REDIS_SYNC_FAILED); - } - // Lua 결과가 비정상이면 무시하지 않고 예외 처리 - if (result == null || result.isEmpty()) { - log.error( - "Invalid Redis Lua result. eventId={}, familyId={}, customerId={}, field={}", - logSanitizer.sanitize(eventId), - familyId, - customerId, - logSanitizer.sanitize(policyKey)); - throw new ApplicationException(PolicyErrorCode.POLICY_REDIS_INVALID_RESULT); - } - - return result.get(0); - } - - private long resolveEventVersion(EventEnvelope envelope) { - // timestamp가 없으면 현재 시각을 버전으로 대체 - if (envelope.timestamp() == null) { - return System.currentTimeMillis(); - } - // timestamp를 KST 기준 epoch millis로 변환해 Lua 버전 비교에 사용 - return envelope.timestamp().atZone(TimeConstants.ASIA_SEOUL).toInstant().toEpochMilli(); - } - - private void logResult( - String eventId, - Long familyId, - Long customerId, - String policyKey, - String newValue, - String result) { - // APPLIED 외 값(중복/버전역전 등)은 skip 사유로 기록해 추적 가능하게 함 - if (LUA_RESULT_APPLIED.equals(result)) { - log.info( - "Updated customer constraint. eventId={}, familyId={}, customerId={}, field={}" - + VALUE_LOG_SUFFIX, - logSanitizer.sanitize(eventId), - familyId, - customerId, - logSanitizer.sanitize(policyKey), - logSanitizer.sanitize(newValue)); - return; - } - - log.info( - "Skipped customer constraint update. eventId={}, familyId={}, customerId={}," - + " field={}, reason={}", - logSanitizer.sanitize(eventId), - familyId, - customerId, - logSanitizer.sanitize(policyKey), - logSanitizer.sanitize(result)); - } - - private record NormalizedPolicyValue( - String normalizedNewValue, Set normalizedBlockedApps) {} -} diff --git a/src/main/java/com/project/domain/policy/service/helper/PolicyEventValidator.java b/src/main/java/com/project/domain/policy/service/helper/PolicyEventValidator.java deleted file mode 100644 index 4ae482e..0000000 --- a/src/main/java/com/project/domain/policy/service/helper/PolicyEventValidator.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.project.domain.policy.service.helper; - -import org.springframework.stereotype.Component; - -import com.dabom.messaging.kafka.event.dto.policy.PolicyUpdatedPayload; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -public class PolicyEventValidator { - public boolean isValidPayload(PolicyUpdatedPayload payload, String eventId, String recordKey) { - if (payload == null) { - log.warn("policy-updated payload is null. recordKey={}", recordKey); - return false; - } - - if (eventId == null || eventId.isBlank()) { - log.warn( - "policy-updated eventId is empty. familyId={}, customerId={}, policyKey={}", - payload.familyId(), - payload.targetCustomerId(), - payload.policyKey()); - return false; - } - - if (payload.policyKey() == null || payload.policyKey().isBlank()) { - log.warn( - "Invalid policy-updated payload. eventId={}, familyId={}, customerId={}," - + " policyKey={}", - eventId, - payload.familyId(), - payload.targetCustomerId(), - payload.policyKey()); - return false; - } - - return true; - } - - public boolean isAllowedPolicyKey(String policyKey) { - if (policyKey == null || policyKey.isBlank()) { - return false; - } - return PolicyConstraintKeyConstants.BLOCK_ACCESS.equals(policyKey) - || PolicyConstraintKeyConstants.BLOCK_TIME.equals(policyKey) - || PolicyConstraintKeyConstants.BLOCK_APP.equals(policyKey) - || PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY.equals(policyKey) - || policyKey.startsWith(PolicyConstraintKeyConstants.BLOCK_APP_PREFIX); - } -} diff --git a/src/main/java/com/project/domain/usage/entity/UsageEventOutbox.java b/src/main/java/com/project/domain/usage/entity/UsageEventOutbox.java index 879382f..53544e4 100644 --- a/src/main/java/com/project/domain/usage/entity/UsageEventOutbox.java +++ b/src/main/java/com/project/domain/usage/entity/UsageEventOutbox.java @@ -13,8 +13,8 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import com.project.common.util.BaseEntity; import com.project.domain.usage.enums.UsageOutboxStatus; -import com.project.global.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/usage/entity/UsageRecord.java b/src/main/java/com/project/domain/usage/entity/UsageRecord.java index 5f23bfd..1cc387d 100644 --- a/src/main/java/com/project/domain/usage/entity/UsageRecord.java +++ b/src/main/java/com/project/domain/usage/entity/UsageRecord.java @@ -11,7 +11,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import com.project.global.util.BaseEntity; +import com.project.common.util.BaseEntity; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/project/domain/usage/infra/messaging/UsageEventsConsumer.java b/src/main/java/com/project/domain/usage/infra/messaging/UsageEventsConsumer.java index 8459070..acfebe7 100644 --- a/src/main/java/com/project/domain/usage/infra/messaging/UsageEventsConsumer.java +++ b/src/main/java/com/project/domain/usage/infra/messaging/UsageEventsConsumer.java @@ -12,9 +12,9 @@ import com.dabom.messaging.kafka.event.dto.EventEnvelope; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; import com.fasterxml.jackson.core.type.TypeReference; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.service.UsageSyncService; import com.project.domain.usage.service.helper.UsageEventValidator; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/UsagePersistServiceImpl.java b/src/main/java/com/project/domain/usage/service/UsagePersistServiceImpl.java index 9540f6f..0a62738 100644 --- a/src/main/java/com/project/domain/usage/service/UsagePersistServiceImpl.java +++ b/src/main/java/com/project/domain/usage/service/UsagePersistServiceImpl.java @@ -8,6 +8,8 @@ import org.springframework.transaction.annotation.Transactional; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; +import com.project.common.config.TimeConfig; +import com.project.common.util.LogSanitizer; import com.project.domain.family.repository.FamilyMemberRepository; import com.project.domain.usage.enums.UsagePersistProcessResult; import com.project.domain.usage.service.dto.UsagePersistPayload; @@ -15,8 +17,6 @@ import com.project.domain.usage.service.helper.FamilyQuotaWriter; import com.project.domain.usage.service.helper.UsagePersistEventValidator; import com.project.domain.usage.service.helper.UsageRecordWriter; -import com.project.global.common.TimeConstants; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -101,14 +101,14 @@ private boolean isValidFamilyMember(Long familyId, Long customerId) { // 이벤트 시각을 정산 월로 변환하고 이상 값이면 현재 월로 보정한다. private LocalDate resolveCurrentMonth(String eventTime) { - LocalDate currentMonth = LocalDate.now(TimeConstants.ASIA_SEOUL).withDayOfMonth(1); + LocalDate currentMonth = LocalDate.now(TimeConfig.ASIA_SEOUL).withDayOfMonth(1); if (eventTime == null || eventTime.isBlank()) { return currentMonth; } try { LocalDate parsedMonth = LocalDateTime.parse(eventTime) - .atZone(TimeConstants.ASIA_SEOUL) + .atZone(TimeConfig.ASIA_SEOUL) .toLocalDate() .withDayOfMonth(1); diff --git a/src/main/java/com/project/domain/usage/service/UsageSyncServiceImpl.java b/src/main/java/com/project/domain/usage/service/UsageSyncServiceImpl.java index 4f98e6d..ba7d9f5 100644 --- a/src/main/java/com/project/domain/usage/service/UsageSyncServiceImpl.java +++ b/src/main/java/com/project/domain/usage/service/UsageSyncServiceImpl.java @@ -17,7 +17,10 @@ import com.dabom.messaging.kafka.event.dto.notification.NotificationPayload; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; import com.dabom.messaging.kafka.metrics.KafkaMetrics; -import com.project.domain.policy.service.helper.PolicyConstraintWarmupHelper; +import com.project.common.config.TimeConfig; +import com.project.common.util.LogSanitizer; +import com.project.common.util.RedisKeyGenerator; +import com.project.domain.policy.helper.PolicyConstraintWarmupHelper; import com.project.domain.usage.service.dto.UsageUpdateResult; import com.project.domain.usage.service.helper.UsageEventOutboxService; import com.project.domain.usage.service.helper.UsageFamilyMembershipCacheHelper; @@ -26,9 +29,6 @@ import com.project.domain.usage.service.helper.UsageNotificationPublisher; import com.project.domain.usage.service.helper.UsageProcessingDecisionMapper; import com.project.domain.usage.service.helper.UsageRedisWarmupHelper; -import com.project.global.common.TimeConstants; -import com.project.global.util.LogSanitizer; -import com.project.global.util.RedisKeyGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -245,7 +245,7 @@ private LocalDateTime resolveEventDateTime(String eventTime) { log.debug("Failed to parse eventTime: {}", logSanitizer.sanitize(eventTime)); } } - return LocalDateTime.now(TimeConstants.ASIA_SEOUL); + return LocalDateTime.now(TimeConfig.ASIA_SEOUL); } // 앱 차단 키 비교에 사용하도록 appId를 정규화한다. diff --git a/src/main/java/com/project/domain/usage/service/helper/CustomerQuotaWriter.java b/src/main/java/com/project/domain/usage/service/helper/CustomerQuotaWriter.java index b198111..855fbcd 100644 --- a/src/main/java/com/project/domain/usage/service/helper/CustomerQuotaWriter.java +++ b/src/main/java/com/project/domain/usage/service/helper/CustomerQuotaWriter.java @@ -5,10 +5,10 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; +import com.project.common.util.LogSanitizer; import com.project.domain.customer.entity.CustomerQuota; import com.project.domain.customer.repository.CustomerQuotaRepository; import com.project.domain.usage.service.dto.UsagePersistPayload; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/helper/FamilyQuotaWriter.java b/src/main/java/com/project/domain/usage/service/helper/FamilyQuotaWriter.java index 0e07e8d..7fca76b 100644 --- a/src/main/java/com/project/domain/usage/service/helper/FamilyQuotaWriter.java +++ b/src/main/java/com/project/domain/usage/service/helper/FamilyQuotaWriter.java @@ -5,11 +5,11 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; +import com.project.common.exception.ApplicationException; +import com.project.common.exception.code.FamilyErrorCode; +import com.project.common.util.LogSanitizer; import com.project.domain.family.entity.FamilyQuota; import com.project.domain.family.repository.FamilyQuotaRepository; -import com.project.global.exception.ApplicationException; -import com.project.global.exception.code.FamilyErrorCode; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/helper/UsageFamilyMembershipCacheHelper.java b/src/main/java/com/project/domain/usage/service/helper/UsageFamilyMembershipCacheHelper.java index 1dfc3f0..6c571cd 100644 --- a/src/main/java/com/project/domain/usage/service/helper/UsageFamilyMembershipCacheHelper.java +++ b/src/main/java/com/project/domain/usage/service/helper/UsageFamilyMembershipCacheHelper.java @@ -9,8 +9,8 @@ import org.springframework.stereotype.Component; import com.dabom.messaging.kafka.error.KafkaMessageProcessingException; +import com.project.common.util.RedisKeyGenerator; import com.project.domain.family.repository.FamilyMemberRepository; -import com.project.global.util.RedisKeyGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/helper/UsageLuaExecutor.java b/src/main/java/com/project/domain/usage/service/helper/UsageLuaExecutor.java index 5635119..fcb7386 100644 --- a/src/main/java/com/project/domain/usage/service/helper/UsageLuaExecutor.java +++ b/src/main/java/com/project/domain/usage/service/helper/UsageLuaExecutor.java @@ -7,8 +7,8 @@ import org.springframework.stereotype.Component; import com.dabom.messaging.kafka.error.KafkaMessageProcessingException; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.service.dto.UsageUpdateResult; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/helper/UsagePersistEventValidator.java b/src/main/java/com/project/domain/usage/service/helper/UsagePersistEventValidator.java index 4c4d771..06fca7b 100644 --- a/src/main/java/com/project/domain/usage/service/helper/UsagePersistEventValidator.java +++ b/src/main/java/com/project/domain/usage/service/helper/UsagePersistEventValidator.java @@ -2,9 +2,9 @@ import org.springframework.stereotype.Component; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.enums.UsagePersistProcessResult; import com.project.domain.usage.service.dto.UsagePersistPayload; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/project/domain/usage/service/helper/UsageRecordWriter.java b/src/main/java/com/project/domain/usage/service/helper/UsageRecordWriter.java index de110e8..4d26642 100644 --- a/src/main/java/com/project/domain/usage/service/helper/UsageRecordWriter.java +++ b/src/main/java/com/project/domain/usage/service/helper/UsageRecordWriter.java @@ -5,10 +5,10 @@ import org.springframework.stereotype.Service; +import com.project.common.config.TimeConfig; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.repository.UsageRecordRepository; import com.project.domain.usage.service.dto.UsagePersistPayload; -import com.project.global.common.TimeConstants; -import com.project.global.util.LogSanitizer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -50,17 +50,15 @@ public boolean persistUsageRecord( private LocalDateTime resolveEventTime(String eventTime) { // event_time이 없거나 파싱 실패면 현재 KST 시각을 사용한다. if (eventTime == null || eventTime.isBlank()) { - return LocalDateTime.now(TimeConstants.ASIA_SEOUL); + return LocalDateTime.now(TimeConfig.ASIA_SEOUL); } try { - return LocalDateTime.parse(eventTime) - .atZone(TimeConstants.ASIA_SEOUL) - .toLocalDateTime(); + return LocalDateTime.parse(eventTime).atZone(TimeConfig.ASIA_SEOUL).toLocalDateTime(); } catch (DateTimeParseException e) { log.warn( "Invalid eventTime format. Fallback to now(KST). eventTime={}", logSanitizer.sanitize(eventTime)); - return LocalDateTime.now(TimeConstants.ASIA_SEOUL); + return LocalDateTime.now(TimeConfig.ASIA_SEOUL); } } } diff --git a/src/main/java/com/project/domain/usage/service/helper/UsageRedisWarmupHelper.java b/src/main/java/com/project/domain/usage/service/helper/UsageRedisWarmupHelper.java index f61a568..de2219b 100644 --- a/src/main/java/com/project/domain/usage/service/helper/UsageRedisWarmupHelper.java +++ b/src/main/java/com/project/domain/usage/service/helper/UsageRedisWarmupHelper.java @@ -12,6 +12,7 @@ import org.springframework.data.redis.core.types.Expiration; import org.springframework.stereotype.Service; +import com.project.common.config.TimeConfig; import com.project.domain.customer.entity.CustomerQuota; import com.project.domain.customer.repository.CustomerQuotaRepository; import com.project.domain.family.entity.Family; @@ -20,7 +21,6 @@ import com.project.domain.family.repository.FamilyRepository; import com.project.domain.usage.infra.cache.dto.FamilyInfoRedisHash; import com.project.domain.usage.service.dto.FamilyInfo; -import com.project.global.common.TimeConstants; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -159,7 +159,7 @@ public boolean ensureCustomerUsageCached( long usedBytes = (quota == null) ? 0L : Math.max(0L, quota.getMonthlyUsedBytes()); long nextMonthStartEpochSecond = - eventMonth.plusMonths(1).atStartOfDay(TimeConstants.ASIA_SEOUL).toEpochSecond(); + eventMonth.plusMonths(1).atStartOfDay(TimeConfig.ASIA_SEOUL).toEpochSecond(); // 키가 없어도 월초 첫 트래픽을 안전하게 처리하도록 0으로 시드하고 만료를 설정함 Boolean written = diff --git a/src/main/java/com/project/global/common/TimeConstants.java b/src/main/java/com/project/global/common/TimeConstants.java deleted file mode 100644 index 578824a..0000000 --- a/src/main/java/com/project/global/common/TimeConstants.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.project.global.common; - -import java.time.ZoneId; - -public class TimeConstants { - public static final ZoneId ASIA_SEOUL = ZoneId.of("Asia/Seoul"); - - private TimeConstants() {} -} diff --git a/src/main/resources/lua/policy_constraint_update.lua b/src/main/resources/lua/policy_constraint_update.lua deleted file mode 100644 index c9754b5..0000000 --- a/src/main/resources/lua/policy_constraint_update.lua +++ /dev/null @@ -1,61 +0,0 @@ --- policy_constraint_update.lua --- --- Purpose: --- Atomically apply one customer constraint update with dedup + stale-event guard. --- --- KEYS --- KEYS[1]: event:dedup:policy:{eventId}:{customerId} --- KEYS[2]: family:{familyId}:customer:{customerId}:constraints --- --- ARGV --- ARGV[1]: dedup_ttl_seconds --- ARGV[2]: policy_key --- ARGV[3]: new_value (blank => delete) --- ARGV[4]: event_timestamp_epoch_millis --- --- Return (array) --- {"APPLIED", "HSET"|"HDEL"} --- {"DUPLICATE"} --- {"STALE", last_applied_version} --- {"INVALID_REQUEST", reason} - -local dedup_key = KEYS[1] -local constraints_key = KEYS[2] - -local dedup_ttl = tonumber(ARGV[1]) -local policy_key = ARGV[2] -local new_value = ARGV[3] -local event_version = tonumber(ARGV[4]) -local version_field = "ver:" .. policy_key - -if not dedup_ttl or dedup_ttl <= 0 then - return {"INVALID_REQUEST", "INVALID_DEDUP_TTL"} -end - -if not policy_key or policy_key == "" then - return {"INVALID_REQUEST", "EMPTY_POLICY_KEY"} -end - -if not event_version then - return {"INVALID_REQUEST", "INVALID_EVENT_VERSION"} -end - -local first_seen = redis.call("SET", dedup_key, "1", "NX", "EX", dedup_ttl) -if not first_seen then - return {"DUPLICATE"} -end - -local last_applied = tonumber(redis.call("HGET", constraints_key, version_field) or "-1") -if event_version < last_applied then - return {"STALE", tostring(last_applied)} -end - -if not new_value or new_value == "" then - redis.call("HDEL", constraints_key, policy_key) - redis.call("HDEL", constraints_key, version_field) - return {"APPLIED", "HDEL"} -end - -redis.call("HSET", constraints_key, policy_key, new_value) -redis.call("HSET", constraints_key, version_field, tostring(event_version)) -return {"APPLIED", "HSET"} diff --git a/src/test/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHashTest.java b/src/test/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHashTest.java index e16b85b..1288d78 100644 --- a/src/test/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHashTest.java +++ b/src/test/java/com/project/domain/policy/infra/cache/dto/PolicyConstraintRedisHashTest.java @@ -2,20 +2,107 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Map; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class PolicyConstraintRedisHashTest { @Test - @DisplayName("차단 앱 키는 소문자로 정규화해 저장한다") - void putBlockedApp_NormalizesToLowercase() { + @DisplayName("putMonthlyLimit은 LIMIT:DATA:MONTHLY 필드에 바이트 값을 저장한다") + void putMonthlyLimit_storesLimitBytesUnderMonthlyLimitField() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putMonthlyLimit(1073741824L); + + Map result = hash.toMap(); + assertThat(result).containsEntry("LIMIT:DATA:MONTHLY", "1073741824"); + } + + @Test + @DisplayName("putTimeBlockRange는 BLOCK:TIME 필드에 시간 범위를 저장한다") + void putTimeBlockRange_storesTimeRangeUnderTimeBlockField() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putTimeBlockRange("2200-0700"); + + Map result = hash.toMap(); + assertThat(result).containsEntry("BLOCK:TIME", "2200-0700"); + } + + @Test + @DisplayName("putManualBlock은 BLOCK:ACCESS 필드에 1을 저장한다") + void putManualBlock_storesOneUnderAccessBlockField() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putManualBlock(); + + Map result = hash.toMap(); + assertThat(result).containsEntry("BLOCK:ACCESS", "1"); + } + + @Test + @DisplayName("putBlockedApp은 BLOCK:APP:{appId} 필드에 1을 저장한다") + void putBlockedApp_storesOneUnderAppBlockField() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putBlockedApp("com.youtube.app"); + + Map result = hash.toMap(); + assertThat(result).containsEntry("BLOCK:APP:com.youtube.app", "1"); + } + + @Test + @DisplayName("putBlockedApp은 appId의 앞뒤 공백을 제거하고 소문자로 저장한다") + void putBlockedApp_trimsAndLowercasesAppId() { PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); - hash.putBlockedApp(" Com.YouTube.App ", 123L); + hash.putBlockedApp(" Com.YouTube.App "); + + Map result = hash.toMap(); + assertThat(result).containsEntry("BLOCK:APP:com.youtube.app", "1"); + } + + @Test + @DisplayName("여러 정책을 동시에 저장하면 toMap이 모든 필드를 반환한다") + void toMap_returnsAllFieldsWhenMultiplePoliciesStored() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putMonthlyLimit(500000000L); + hash.putTimeBlockRange("2300-0600"); + hash.putManualBlock(); + hash.putBlockedApp("com.instagram.app"); + + Map result = hash.toMap(); + assertThat(result) + .containsEntry("LIMIT:DATA:MONTHLY", "500000000") + .containsEntry("BLOCK:TIME", "2300-0600") + .containsEntry("BLOCK:ACCESS", "1") + .containsEntry("BLOCK:APP:com.instagram.app", "1") + .hasSize(4); + } + + @Test + @DisplayName("putBlockedApp을 여러 번 호출하면 각 appId가 독립 필드로 저장된다") + void putBlockedApp_storesEachAppAsIndependentField() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); + + hash.putBlockedApp("com.tiktok.app"); + hash.putBlockedApp("com.instagram.app"); + + Map result = hash.toMap(); + assertThat(result) + .containsEntry("BLOCK:APP:com.tiktok.app", "1") + .containsEntry("BLOCK:APP:com.instagram.app", "1") + .hasSize(2); + } + + @Test + @DisplayName("create로 생성한 해시는 초기 상태가 비어 있다") + void create_returnsEmptyMapInitially() { + PolicyConstraintRedisHash hash = PolicyConstraintRedisHash.create(); - assertThat(hash.toMap()) - .containsEntry("BLOCK:APP:com.youtube.app", "1") - .containsEntry("ver:BLOCK:APP:com.youtube.app", "123"); + assertThat(hash.toMap()).isEmpty(); } } diff --git a/src/test/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImplTest.java b/src/test/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImplTest.java deleted file mode 100644 index 9a6cd61..0000000 --- a/src/test/java/com/project/domain/policy/service/PolicyConstraintSyncServiceImplTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.project.domain.policy.service; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.time.LocalDateTime; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.script.RedisScript; -import org.springframework.test.util.ReflectionTestUtils; - -import com.dabom.messaging.kafka.contract.KafkaEventTypes; -import com.dabom.messaging.kafka.event.dto.EventEnvelope; -import com.dabom.messaging.kafka.event.dto.policy.PolicyUpdatedPayload; -import com.project.domain.family.repository.FamilyMemberRepository; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; -import com.project.domain.policy.service.helper.PolicyConstraintEventMapper; -import com.project.domain.policy.service.helper.PolicyEventValidator; -import com.project.global.util.LogSanitizer; -import com.project.global.util.RedisKeyGenerator; - -@ExtendWith(MockitoExtension.class) -class PolicyConstraintSyncServiceImplTest { - - @InjectMocks private PolicyConstraintSyncServiceImpl service; - - @Mock private RedisTemplate familyStringRedisTemplate; - @Mock private RedisScript> policyConstraintUpdateScript; - @Mock private RedisKeyGenerator redisKeyGenerator; - @Mock private FamilyMemberRepository familyMemberRepository; - @Mock private PolicyEventValidator policyEventValidator; - @Mock private PolicyConstraintEventMapper policyConstraintEventMapper; - @Mock private LogSanitizer logSanitizer; - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(service, "dedupTtlSeconds", 60L); - lenient() - .when(logSanitizer.sanitize(nullable(String.class))) - .thenAnswer( - invocation -> { - String raw = invocation.getArgument(0); - return raw == null ? "null" : raw; - }); - } - - @Test - @DisplayName("constraints key가 없으면 일반 정책 업데이트를 스킵한다") - void sync_SkipWhenConstraintsKeyMissing() { - PolicyUpdatedPayload payload = - new PolicyUpdatedPayload( - 10L, 20L, PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, "1024", true); - EventEnvelope envelope = - new EventEnvelope<>( - "evt-1", KafkaEventTypes.POLICY_UPDATED, LocalDateTime.now(), payload); - - given(policyEventValidator.isValidPayload(payload, "evt-1", "record-1")).willReturn(true); - given( - policyEventValidator.isAllowedPolicyKey( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY)) - .willReturn(true); - given( - policyConstraintEventMapper.normalizeValue( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, "1024")) - .willReturn("1024"); - given(familyMemberRepository.existsByFamilyIdAndCustomerIdAndDeletedAtIsNull(10L, 20L)) - .willReturn(true); - given(redisKeyGenerator.generateFamilyCustomerConstraintsKey(10L, 20L)) - .willReturn("family:10:customer:20:constraints"); - given(familyStringRedisTemplate.hasKey("family:10:customer:20:constraints")) - .willReturn(false); - - service.sync(envelope, "record-1"); - - verify(familyStringRedisTemplate, never()) - .execute(eq(policyConstraintUpdateScript), anyList(), any(), any(), any(), any()); - } - - @Test - @DisplayName("constraints key가 있으면 일반 정책 업데이트를 Lua로 반영한다") - void sync_ApplyWhenConstraintsKeyExists() { - PolicyUpdatedPayload payload = - new PolicyUpdatedPayload( - 10L, 20L, PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, "1024", true); - EventEnvelope envelope = - new EventEnvelope<>( - "evt-2", KafkaEventTypes.POLICY_UPDATED, LocalDateTime.now(), payload); - - given(policyEventValidator.isValidPayload(payload, "evt-2", "record-2")).willReturn(true); - given( - policyEventValidator.isAllowedPolicyKey( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY)) - .willReturn(true); - given( - policyConstraintEventMapper.normalizeValue( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, "1024")) - .willReturn("1024"); - given(familyMemberRepository.existsByFamilyIdAndCustomerIdAndDeletedAtIsNull(10L, 20L)) - .willReturn(true); - given(redisKeyGenerator.generateFamilyCustomerConstraintsKey(10L, 20L)) - .willReturn("family:10:customer:20:constraints"); - given(familyStringRedisTemplate.hasKey("family:10:customer:20:constraints")) - .willReturn(true); - given(redisKeyGenerator.generatePolicyEventDedupKey("evt-2", 20L)) - .willReturn("event:dedup:policy:evt-2:20"); - given( - familyStringRedisTemplate.execute( - eq(policyConstraintUpdateScript), - anyList(), - any(), - any(), - any(), - any())) - .willReturn(List.of("APPLIED", "HSET")); - - service.sync(envelope, "record-2"); - - verify(familyStringRedisTemplate, times(1)) - .execute(eq(policyConstraintUpdateScript), anyList(), any(), any(), any(), any()); - } - - @Test - @DisplayName("BLOCK:APP에서 constraints key가 없으면 diff 동기화를 수행하지 않는다") - void sync_BlockAppSkipWhenConstraintsKeyMissing() { - PolicyUpdatedPayload payload = - new PolicyUpdatedPayload( - 10L, - 20L, - PolicyConstraintKeyConstants.BLOCK_APP, - "[\"app1\",\"app2\"]", - true); - EventEnvelope envelope = - new EventEnvelope<>( - "evt-3", KafkaEventTypes.POLICY_UPDATED, LocalDateTime.now(), payload); - - given(policyEventValidator.isValidPayload(payload, "evt-3", "record-3")).willReturn(true); - given(policyEventValidator.isAllowedPolicyKey(PolicyConstraintKeyConstants.BLOCK_APP)) - .willReturn(true); - given(policyConstraintEventMapper.normalizeAppBlockValueAsSet("[\"app1\",\"app2\"]")) - .willReturn(new LinkedHashSet<>(Set.of("app1", "app2"))); - given(familyMemberRepository.existsByFamilyIdAndCustomerIdAndDeletedAtIsNull(10L, 20L)) - .willReturn(true); - given(redisKeyGenerator.generateFamilyCustomerConstraintsKey(10L, 20L)) - .willReturn("family:10:customer:20:constraints"); - given(familyStringRedisTemplate.hasKey("family:10:customer:20:constraints")) - .willReturn(false); - - service.sync(envelope, "record-3"); - - verify(familyStringRedisTemplate, never()).opsForHash(); - verify(familyStringRedisTemplate, never()) - .execute(eq(policyConstraintUpdateScript), anyList(), any(), any(), any(), any()); - } -} diff --git a/src/test/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapperTest.java b/src/test/java/com/project/domain/policy/service/helper/PolicyConstraintMapperTest.java similarity index 65% rename from src/test/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapperTest.java rename to src/test/java/com/project/domain/policy/service/helper/PolicyConstraintMapperTest.java index 9a596e5..e6174a7 100644 --- a/src/test/java/com/project/domain/policy/service/helper/PolicyConstraintEventMapperTest.java +++ b/src/test/java/com/project/domain/policy/service/helper/PolicyConstraintMapperTest.java @@ -7,19 +7,17 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.ObjectMapper; -import com.project.domain.policy.constant.PolicyConstraintKeyConstants; +import com.project.domain.policy.enums.PolicyType; +import com.project.domain.policy.helper.PolicyConstraintMapper; -class PolicyConstraintEventMapperTest { - private final PolicyConstraintEventMapper mapper = - new PolicyConstraintEventMapper(new ObjectMapper()); +class PolicyConstraintMapperTest { + private final PolicyConstraintMapper mapper = new PolicyConstraintMapper(new ObjectMapper()); @Test @DisplayName("MONTHLY_LIMIT JSON은 Redis 월 제한 값으로 정규화한다") void monthlyLimitJson_normalizeToLongString() { String normalized = - mapper.normalizeValue( - PolicyConstraintKeyConstants.LIMIT_DATA_MONTHLY, - "{\"limitBytes\": 6591920494}"); + mapper.normalizeValue(PolicyType.MONTHLY_LIMIT, "{\"limitBytes\": 6591920494}"); assertThat(normalized).isEqualTo("6591920494"); } @@ -29,7 +27,7 @@ void monthlyLimitJson_normalizeToLongString() { void timeBlockJson_normalizeToRange() { String normalized = mapper.normalizeValue( - PolicyConstraintKeyConstants.BLOCK_TIME, + PolicyType.TIME_BLOCK, "{\"start\":\"23:00\",\"end\":\"08:00\",\"timezone\":\"Asia/Seoul\"}"); assertThat(normalized).isEqualTo("2300-0800"); @@ -39,8 +37,7 @@ void timeBlockJson_normalizeToRange() { @DisplayName("MANUAL_BLOCK JSON은 BLOCK:ACCESS 활성값으로 정규화한다") void manualBlockJson_normalizeToOne() { String normalized = - mapper.normalizeValue( - PolicyConstraintKeyConstants.BLOCK_ACCESS, "{\"reason\":\"MANUAL\"}"); + mapper.normalizeValue(PolicyType.MANUAL_BLOCK, "{\"reason\":\"MANUAL\"}"); assertThat(normalized).isEqualTo("1"); } @@ -48,7 +45,7 @@ void manualBlockJson_normalizeToOne() { @Test @DisplayName("제약 해제(newValue null)는 삭제로 처리한다") void nullValue_normalizeToNull() { - String normalized = mapper.normalizeValue(PolicyConstraintKeyConstants.BLOCK_TIME, null); + String normalized = mapper.normalizeValue(PolicyType.TIME_BLOCK, null); assertThat(normalized).isNull(); } @@ -58,7 +55,7 @@ void nullValue_normalizeToNull() { void appBlockJson_normalizeToCsv() { String normalized = mapper.normalizeValue( - PolicyConstraintKeyConstants.BLOCK_APP, + PolicyType.APP_BLOCK, "{\"blockedApps\":[\"com.youtube.app\", \"com.game.app\"]}"); assertThat(normalized).isEqualTo("com.youtube.app,com.game.app"); @@ -68,10 +65,7 @@ void appBlockJson_normalizeToCsv() { @DisplayName("TIME_BLOCK JSON 필수 필드가 없으면 예외를 던진다") void invalidTimeBlockJson_throwException() { assertThatThrownBy( - () -> - mapper.normalizeValue( - PolicyConstraintKeyConstants.BLOCK_TIME, - "{\"start\":\"23:00\"}")) + () -> mapper.normalizeValue(PolicyType.TIME_BLOCK, "{\"start\":\"23:00\"}")) .isInstanceOf(IllegalArgumentException.class); } } diff --git a/src/test/java/com/project/domain/usage/infra/messaging/UsageEventsConsumerTest.java b/src/test/java/com/project/domain/usage/infra/messaging/UsageEventsConsumerTest.java index b05b450..f038580 100644 --- a/src/test/java/com/project/domain/usage/infra/messaging/UsageEventsConsumerTest.java +++ b/src/test/java/com/project/domain/usage/infra/messaging/UsageEventsConsumerTest.java @@ -30,9 +30,9 @@ import com.dabom.messaging.kafka.event.KafkaEventMessageSupport; import com.dabom.messaging.kafka.event.dto.EventEnvelope; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.service.UsageSyncService; import com.project.domain.usage.service.helper.UsageEventValidator; -import com.project.global.util.LogSanitizer; @ExtendWith(MockitoExtension.class) class UsageEventsConsumerTest { diff --git a/src/test/java/com/project/domain/usage/service/UsagePersistServiceImplTest.java b/src/test/java/com/project/domain/usage/service/UsagePersistServiceImplTest.java index fb96ddd..f8f88eb 100644 --- a/src/test/java/com/project/domain/usage/service/UsagePersistServiceImplTest.java +++ b/src/test/java/com/project/domain/usage/service/UsagePersistServiceImplTest.java @@ -19,13 +19,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; +import com.project.common.util.LogSanitizer; import com.project.domain.family.repository.FamilyMemberRepository; import com.project.domain.usage.service.dto.UsagePersistPayload; import com.project.domain.usage.service.helper.CustomerQuotaWriter; import com.project.domain.usage.service.helper.FamilyQuotaWriter; import com.project.domain.usage.service.helper.UsagePersistEventValidator; import com.project.domain.usage.service.helper.UsageRecordWriter; -import com.project.global.util.LogSanitizer; @ExtendWith(MockitoExtension.class) class UsagePersistServiceImplTest { diff --git a/src/test/java/com/project/domain/usage/service/UsageSyncServiceImplTest.java b/src/test/java/com/project/domain/usage/service/UsageSyncServiceImplTest.java index 01a7878..8557637 100644 --- a/src/test/java/com/project/domain/usage/service/UsageSyncServiceImplTest.java +++ b/src/test/java/com/project/domain/usage/service/UsageSyncServiceImplTest.java @@ -35,7 +35,9 @@ import com.dabom.messaging.kafka.event.dto.notification.NotificationType; import com.dabom.messaging.kafka.event.dto.usage.UsagePayload; import com.dabom.messaging.kafka.metrics.KafkaMetrics; -import com.project.domain.policy.service.helper.PolicyConstraintWarmupHelper; +import com.project.common.util.LogSanitizer; +import com.project.common.util.RedisKeyGenerator; +import com.project.domain.policy.helper.PolicyConstraintWarmupHelper; import com.project.domain.usage.service.dto.UsageUpdateResult; import com.project.domain.usage.service.helper.UsageEventOutboxService; import com.project.domain.usage.service.helper.UsageFamilyMembershipCacheHelper; @@ -44,8 +46,6 @@ import com.project.domain.usage.service.helper.UsageNotificationPublisher; import com.project.domain.usage.service.helper.UsageProcessingDecisionMapper; import com.project.domain.usage.service.helper.UsageRedisWarmupHelper; -import com.project.global.util.LogSanitizer; -import com.project.global.util.RedisKeyGenerator; @ExtendWith(MockitoExtension.class) class UsageSyncServiceImplTest { diff --git a/src/test/java/com/project/domain/usage/service/helper/FamilyQuotaWriterTest.java b/src/test/java/com/project/domain/usage/service/helper/FamilyQuotaWriterTest.java index 393939c..50e4421 100644 --- a/src/test/java/com/project/domain/usage/service/helper/FamilyQuotaWriterTest.java +++ b/src/test/java/com/project/domain/usage/service/helper/FamilyQuotaWriterTest.java @@ -23,11 +23,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.DataIntegrityViolationException; +import com.project.common.exception.ApplicationException; +import com.project.common.exception.code.FamilyErrorCode; +import com.project.common.util.LogSanitizer; import com.project.domain.family.entity.FamilyQuota; import com.project.domain.family.repository.FamilyQuotaRepository; -import com.project.global.exception.ApplicationException; -import com.project.global.exception.code.FamilyErrorCode; -import com.project.global.util.LogSanitizer; @ExtendWith(MockitoExtension.class) class FamilyQuotaWriterTest { diff --git a/src/test/java/com/project/domain/usage/service/helper/UsageLuaExecutorTest.java b/src/test/java/com/project/domain/usage/service/helper/UsageLuaExecutorTest.java index 2350e66..fdf6d0f 100644 --- a/src/test/java/com/project/domain/usage/service/helper/UsageLuaExecutorTest.java +++ b/src/test/java/com/project/domain/usage/service/helper/UsageLuaExecutorTest.java @@ -26,8 +26,8 @@ import org.springframework.data.redis.core.script.RedisScript; import com.dabom.messaging.kafka.error.KafkaMessageProcessingException; +import com.project.common.util.LogSanitizer; import com.project.domain.usage.service.dto.UsageUpdateResult; -import com.project.global.util.LogSanitizer; @ExtendWith(MockitoExtension.class) class UsageLuaExecutorTest {