From c5b23c38738b51aa0bfaa563f565590cae35e256 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Mon, 6 Jul 2026 17:47:48 +0900 Subject: [PATCH] =?UTF-8?q?PortalProperty=20=EB=B6=80=ED=8C=85=20=EC=8B=9C?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=ED=82=A4=20=EC=A0=90=EA=B2=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20-=20(PROPERTY=5FGROUP=5FNAME,?= =?UTF-8?q?=20PROPERTY=5FNAME)=20=EC=9C=A0=EC=9D=BC=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20-=20=EC=A4=91=EB=B3=B5=20=EB=B0=9C=EA=B2=AC=20?= =?UTF-8?q?=EC=8B=9C=20ERROR=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=8F=20=EC=88=98=EB=8F=99=20=EC=A0=95=EB=A6=AC=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20-=20JSESSIONID=20=EC=BF=A0=ED=82=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD=20(PORTAL=5FJSESSIONID=20?= =?UTF-8?q?=E2=86=92=20JSESSIONID=5FPORTAL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortalPropertyDuplicateChecker.java | 55 +++++++++++++++++++ src/main/resources/application.yml | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java diff --git a/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java b/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java new file mode 100644 index 0000000..0e3a7ae --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java @@ -0,0 +1,55 @@ +package com.eactive.apim.portal.config; + +import com.eactive.apim.portal.portalproperty.repository.PortalPropertyRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 기동 시 PTL_PROPERTY 무결성 점검. + * + *

PTL_PROPERTY 는 (PROPERTY_GROUP_NAME, PROPERTY_NAME) 조합이 유일해야 하지만, 운영 정책상 + * DB에 유니크 제약을 걸지 않는다. 대신 부팅 완료 시점에 중복 키를 스캔하여 존재하면 ERROR 로그로 + * 남긴다. (Spring Data 리포지토리는 인터페이스라 초기화 훅이 없어, 리포지토리를 사용하는 startup + * 리스너로 점검한다.)

+ * + *

중복은 {@code getOrCreateProperty} 최초 조회가 동시 요청으로 경합할 때 각각 INSERT되며 생길 수 + * 있다. 제약이 없으므로 중복 행은 자동 제거되지 않는다 — 로그를 보고 수동 정리해야 한다.

+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PortalPropertyDuplicateChecker implements ApplicationListener { + + private final PortalPropertyRepository portalPropertyRepository; + + @Override + @Transactional(readOnly = true) + public void onApplicationEvent(ApplicationReadyEvent event) { + List duplicates; + try { + duplicates = portalPropertyRepository.findDuplicatePropertyKeys(); + } catch (Exception e) { + log.error("[PortalProperty 무결성] 중복 점검 쿼리 실패", e); + return; + } + + if (duplicates == null || duplicates.isEmpty()) { + log.info("[PortalProperty 무결성] (PROPERTY_GROUP_NAME, PROPERTY_NAME) 중복 없음"); + return; + } + + // 1) (group, name) 은 유일해야 한다 + // 2) 중복된 값 목록을 남긴다 + log.error("[PortalProperty 무결성] (PROPERTY_GROUP_NAME, PROPERTY_NAME) 조합은 유일해야 합니다. " + + "DB 유니크 제약이 없어 중복 {}건이 방치되어 있습니다. 수동 정리 필요:", duplicates.size()); + for (Object[] row : duplicates) { + log.error(" - PROPERTY_GROUP_NAME='{}', PROPERTY_NAME='{}', count={}", row[0], row[1], row[2]); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 04ddb28..64dbae5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,7 +4,7 @@ server: session: timeout: 10m cookie: - name: PORTAL_JSESSIONID + name: JSESSIONID_PORTAL encoding: charset: UTF-8 port: '39130'