글로벌 서비스 개발 중에 갑자기 fiddler로 크롬에서 캡쳐가 안되는 문제가 발생했습니다.
확인해보니, 테스트를 위해서 설치한 VPN 확장도구 때문이네요.
비 활성화 후 정상으로 작동합니다.
참고
'유용한 프로그램' 카테고리의 다른 글
| 보안문제 테스트를 위한 툴 (0) | 2022.05.04 |
|---|---|
| 스크린 캡쳐 프로그램 ShareX- 무료 (1) | 2020.09.12 |
글로벌 서비스 개발 중에 갑자기 fiddler로 크롬에서 캡쳐가 안되는 문제가 발생했습니다.
확인해보니, 테스트를 위해서 설치한 VPN 확장도구 때문이네요.
비 활성화 후 정상으로 작동합니다.
참고
| 보안문제 테스트를 위한 툴 (0) | 2022.05.04 |
|---|---|
| 스크린 캡쳐 프로그램 ShareX- 무료 (1) | 2020.09.12 |
<!-- https://mvnrepository.com/artifact/io.sentry/sentry-logback -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>3.2.0</version>
</dependency>
#sentry에 전송시 environment 구분 값(ex. local, alpha, beta, qa, real)
sentry.environment=입력필요
#sentry 에서 dsn정보 확인 후 입력
sentry.dsn=입력필요
<!-- Configure the Sentry appender -->
<appender name="SENTRY" class="io.sentry.logback.SentryAppender">
<!-- Optionally change minimum Event level. Default for Events is ERROR -->
<minimumEventLevel>ERROR</minimumEventLevel>
<!-- Optionally change minimum Breadcrumbs level. Default for Breadcrumbs is INFO -->
<minimumBreadcrumbLevel>DEBUG</minimumBreadcrumbLevel>
<options>
<!-- NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard -->
<dsn>${sentry.dsn}</dsn>
<environment>${sentry.environment}</environment>
</options>
</appender>
| intelliJ GUI를 이용한 GIT 브랜치 merge 방법 (0) | 2021.04.30 |
|---|---|
| pinpoint 설치 방법- 1.8.4 버전 (0) | 2019.06.26 |
| 오라클 사이트에서 JDK8 다운로드해서 리눅스 설치용 wget 명령어 생성 방법 (0) | 2019.05.06 |
| nexus 설치 및 셋팅 (0) | 2019.03.22 |
| pinpoint 설치 방법- 1.8.1 버전 (0) | 2018.12.21 |
PVS-Studio Analyzer 셋팅 참고 메모
How to Run PVS-Studio Java
PVS-Studio Java static code analyzer consists of 2 main parts: the analyzer core, which performs the analysis, and plugins for integration into build systems and IDEs.
www.viva64.com
2026-06 간단히 내용 몇 가지 업데이트
- 원래 작성했던 글은 메모 및 간단히 동료에게 공유해줄 목적이어서 실제 구현대비 누락된 내용이 많았습니다.
- 시간될때마다 내용 업데이트를 해보겠습니다.(but, 요즘에는 AI가 워낙 좋아서 요구사항을 잘 프롬프트하면 더 좋은 결과가 나와서 효용성이 없을지도..)
정규표현식은 유연하지만 텍스트를 검사할 때 백트래킹(Backtracking) 이라는 연산 과정을 거치기 때문에 기본적으로 무거운 작업입니다.
Aho-Corasick 알고리즘은 애초에 '다중 문자열 검색'을 위해 탄생한 알고리즘입니다.
정규화는 인덱스가 틀어져서 마스킹(*** 치환)이 까다로운데, 실무에서는 성능을 위해 Aho-Corasick을 쓰면서도, 이 문제를 '인덱스 매핑(Index Mapping)' 기법으로 해결합니다.
최종 권장: 성능이 중요한 서비스(트래픽이 많거나 텍스트 길이가 긴 게시판, 실시간 채팅 등)라면 무조건 텍스트 정규화 + Aho-Corasick (필요시 인덱스 매핑 추가) 조합으로 개발하시는 것을 강력히 추천합니다!
CREATE TABLE `banned_word` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '고유 식별자 (PK)',
`word` VARCHAR(100) NOT NULL COMMENT '금칙어 (다국어 및 이모지 지원)',
`category` VARCHAR(50) DEFAULT 'GENERAL' COMMENT '금칙어 유형 (예: 욕설, 도박, 혐오, 광고 등)',
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '적용 여부 (1: 활성, 0: 비활성)',
`created_by` VARCHAR(50) DEFAULT NULL COMMENT '등록자 ID 또는 이름',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '등록 일시',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 일시',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_banned_word_word` (`word`),
KEY `idx_banned_word_is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='금칙어 관리 테이블';
💻 Aho-Corasick 금칙어 필터링 구현체 (Java 21)
import java.util.*;
public class BannedWordFilter {
// 1. Trie 노드 정의 (내부 클래스)
private static class Node {
// 자식 노드들 (검색 속도를 위해 HashMap 사용)
final Map<Character, Node> children = new HashMap<>();
// 매칭 실패 시 돌아갈 링크 (성능의 핵심!)
Node fail;
// 이 노드에서 끝나는 금칙어가 있는지 여부
boolean isOutput;
}
private final Node root = new Node();
/**
* 금칙어 목록을 받아 Aho-Corasick 오토마타를 구성합니다.
* (이 작업은 무거우므로 Spring Boot 실행 시 한 번만 수행하여 캐싱하는 것을 권장)
*/
public void buildAutomaton(List<String> bannedWords) {
// [Step 1] Trie 구조 만들기
for (String word : bannedWords) {
Node current = root;
for (char c : word.toCharArray()) {
current.children.computeIfAbsent(c, k -> new Node());
}
current.isOutput = true;
}
// [Step 2] 실패 링크(Failure Link) 구성 (BFS 방식)
Queue<Node> queue = new ArrayDeque<>();
for (Node child : root.children.values()) {
child.fail = root;
queue.offer(child);
}
while (!queue.isEmpty()) {
Node current = queue.poll();
for (Map.Entry<Character, Node> entry : current.children.entrySet()) {
char c = entry.getKey();
Node child = entry.getValue();
// 실패 링크 찾기
Node failNode = current.fail;
while (failNode != null && !failNode.children.containsKey(c)) {
failNode = failNode.fail;
}
if (failNode == null) {
child.fail = root;
} else {
child.fail = failNode.children.get(c);
}
// 부모나 실패 링크에 금칙어가 포함되어 있다면, 자식 노드도 금칙어 포함으로 간주
child.isOutput |= child.fail.isOutput;
queue.offer(child);
}
}
}
/**
* 사용자가 입력한 텍스트에 금칙어가 포함되어 있는지 검사합니다.
*/
public boolean containsBannedWord(String text) {
if (text == null || text.isBlank()) {
return false;
}
// [핵심] 특수문자, 숫자, 공백 제거 (욕1설, 욕!설 -> 욕설)
String normalizedText = text.replaceAll("[^가-힣a-zA-Z]", "");
Node current = root;
for (char c : normalizedText.toCharArray()) {
// 현재 문자가 자식 노드에 없으면 실패 링크를 따라감 (백트래킹 방지)
while (current != null && !current.children.containsKey(c)) {
current = current.fail;
}
if (current == null) {
current = root; // 루트까지 와도 없으면 다시 처음부터
continue;
}
current = current.children.get(c);
// 금칙어를 발견한 경우 즉시 true 반환
if (current.isOutput) {
return true;
}
}
return false;
}
// --- 테스트용 메인 메서드 ---
public static void main(String[] args) {
BannedWordFilter filter = new BannedWordFilter();
// 1. 금칙어 트리 구성 (서버 시작 시 1회 실행)
List<String> bannedWords = List.of("욕설", "도박", "광고");
filter.buildAutomaton(bannedWords);
// 2. 텍스트 검사
String input1 = "이런 욕1설, 진짜 나쁜 놈이네";
String input2 = "도!!!박 사이트 오세요";
String input3 = "안녕하세요 반갑습니다";
System.out.println("input1 검사 결과: " + filter.containsBannedWord(input1)); // true
System.out.println("input2 검사 결과: " + filter.containsBannedWord(input2)); // true
System.out.println("input3 검사 결과: " + filter.containsBannedWord(input3)); // false
}
}
서비스를 개발하다보면 항상 금칙어처리가 필요하게 됩니다.
금칙어 갯수가 적으면 상관 없는데 갯수가 많으면(특히 중국 서비스하면..;) 성능을 잘 생각해서 처리해야 합니다.
관련해서 참고용 TC를 만들어봤습니다.
간단히 만들어서 TC종류는 많지 않고 부족한 부분이 있을 수 있습니다.
1. 금칙어 저장테이블 DDL(참고용)
-- 금칙어 테이블 DDL샘플(Mysql). 글로벌 다국어를 감안하여 금칙어 컬럼은 'utf8mb4_bin'로 정의
CREATE TABLE `bad_word` (
`pk` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'pk',
`use_yn` ENUM('Y','N') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Y' COMMENT '사용여부',
`bad_word` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '금칙어',
PRIMARY KEY (`pk`),
UNIQUE KEY `UNQ_badWord` (`bad_word`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='금칙어'
2. maven dependency
<!-- https://mvnrepository.com/artifact/org.ahocorasick/ahocorasick -->
<dependency>
<groupId>org.ahocorasick</groupId>
<artifactId>ahocorasick</artifactId>
<version>0.4.0</version>
</dependency>
3. test case
import org.ahocorasick.trie.Emit;
import org.ahocorasick.trie.Trie;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.concurrent.TimeUnit;
/**
* 금칙어 성능 테스트
* - 대량(10만개 이상)의 금칙어 키워드 존재시 금칙어 여부 판단에 성능 이슈가 없도록 처리하는 테스트(샘플) 소스
* - 아호코라식 알고리즘을 활용: https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_algorithm
*
* @author
*/
class BadWordPerformanceTest {
private static int initDummyBadwordCnt = 100_000; //더미용 금칙어 초기화 갯수
private static final String findBadword = "개새끼"; //테스트용 금칙어
private static final String findBadword2 = "소새끼"; //테스트용 금칙어2
private static LinkedHashSet<String> badwords = new LinkedHashSet<>(); //linkedhaset이 contain 성능이 가장 좋음: https://dzone.com/articles/java-collection-performance
private static Trie badwordsTrie; //아호코라식용
@BeforeAll
static void init() {
for (int i = 1; i <= initDummyBadwordCnt; i++) {
//String randomBadWord = RandomStringUtils.random(30, false, false);
String randomBadWord = RandomStringUtils.randomAlphanumeric(30);
badwords.add(randomBadWord);
}
System.out.println(String.format("init 금칙어 갯수(컬렉션용): %d", badwords.size()));
//아호코라식용 초기화
long startInitAho = System.currentTimeMillis();
badwordsTrie = Trie.builder().addKeywords(badwords).addKeyword(findBadword).addKeyword(findBadword2).build(); //시간이 많이걸리니까 가능하면 초기화 후 재 사용
//badwordsTrie = Trie.builder().addKeywords(badwords).addKeyword(findBadword).addKeyword(findBadword2).onlyWholeWords().build(); //시간이 많이걸리니까 가능하면 초기화 후 재 사용
//badwordsTrie = Trie.builder().ignoreCase().ignoreOverlaps().addKeywords(badwords).build(); //아호코라식용 초기화
long endInitAho = System.currentTimeMillis();
System.out.println("아호코라식 초기화 소요시간(ms): " + (endInitAho - startInitAho));
}
/**
* 아호코라식으로도 완전일치 테스트가 가능하지만 java컬렉션을 이용해서도 구현
*/
@Test
@Timeout(value = 20, unit = TimeUnit.MILLISECONDS)
public void 금칙어_완전일치_테스트() {
badwords.add(findBadword); //테스트용 금칙어를 금칙어 셋에 추가해둠(성능 테스트를 위해 만든 대량의 금칙어에 추가)
final String notExistBadword = findBadword + System.currentTimeMillis(); //확률적으로 존재할 수 없는 금칙어
long startExactNano = System.nanoTime();
long startExactms = System.currentTimeMillis();
Assert.assertTrue(badwords.contains(findBadword));
Assert.assertFalse(badwords.contains(notExistBadword));
long endExactNano = System.nanoTime();
long endExactMs = System.currentTimeMillis();
System.out.println("\n\n완전일치 금칙어 find 소요시간(nano): " + (endExactNano - startExactNano));
System.out.println("완전일치 금칙어 find 소요시간(ms): " + (endExactMs - startExactms));
}
/**
* 성능을 위해서 포함여부 체크는 아호코라식 알고리즘을 사용
* - 구현 java 라이브러리: https://github.com/robert-bor/aho-corasick (maven mvnrepository에는 배포를 안하니 참고해서 직접 구현하거나 소스 내려받아서 빌드 후 사용)
*/
@Test
@Timeout(value = 20, unit = TimeUnit.MILLISECONDS)
public void 금칙어_포함여부_아호코라식알고리즘기반_테스트() {
String targetText_1 = "개새끼들이 뛰어놀고 있어요. 소 는 없어요";
Collection<Emit> emits_1 = excuteAho(targetText_1);
Assert.assertTrue(emits_1.size() == 1);
String targetText_2 = "개새끼들이 뛰어놀고 있어요. 옆에는 소새끼들이 있어요";
Collection<Emit> emits_2 = excuteAho(targetText_2);
Assert.assertTrue(emits_2.size() == 2);
String targetText_3 = "개가 뛰어놀고 있어요. 옆에는 소도 있어요";
Collection<Emit> emits_3 = excuteAho(targetText_3);
System.out.println(emits_3);
Assert.assertTrue(emits_3.size() == 0);
}
private Collection<Emit> excuteAho(String targetText) {
System.out.println("\n===== excuteAho: Start ");
System.out.println("금칙어가 존재하는지 검사할 텍스트:==>" + targetText);
long startNano = System.nanoTime();
long startMs = System.currentTimeMillis();
Collection<Emit> emits = badwordsTrie.parseText(targetText);
System.out.println("검출된 금칙어 갯수: " + emits.size());
for (Emit emit : emits) {
System.out.println(String.format(" 금칙어 '%s'에 매칭됨", emit.getKeyword()));
}
long endNano = System.nanoTime();
long endMs = System.currentTimeMillis();
long duNano = endNano - startNano;
long duMs = endMs - startMs;
System.out.println(String.format("아호코라식 기반 금칙어 판별 소요시간. '%d(nano)' | '%d(ms)'", duNano, duMs));
System.out.println("===== excuteAho: End ");
return emits;
}
}
| java stream filter를 이용한 중복 제거 방법(샘플) (0) | 2021.04.06 |
|---|---|
| java 사설인증키 처리 - 1 (0) | 2021.03.31 |
| JVM 계열 Local cache 추천 - 2020년 기준 (0) | 2020.11.09 |
| java jvm계열 로컬캐시 (0) | 2020.09.02 |
| 유효한 IP인지 체크하는 java소스 샘플 (0) | 2020.06.11 |
/**
* 간단한 컬렉션 contains 성능테스트
* - https://dzone.com/articles/java-collection-performance
*
* @param args
*/
public static void main(String[] args) {
long min = 999999999;
long max = 0;
for (int t = 0; t < 200; t++) {
int cnt = 30000;
LinkedHashSet<String> badwords = new LinkedHashSet<String>(); //min:500 || max:15200
//ArrayList<String> badwords = new ArrayList<String>(); min:148100 || max:1110400
//TreeSet<String> badwords = new TreeSet<String>(); //min:3000 || max:31000
for (int i = 1; i <= cnt; i++) {
badwords.add(String.valueOf(i));
}
long startMs = System.currentTimeMillis();
long start = System.nanoTime();
badwords.contains(String.valueOf(cnt));
long end = System.nanoTime();
long endMs = System.currentTimeMillis();
long du = end - start;
System.out.println(du);
if (du > max) {
max = du;
}
if (du < min) {
min = du;
}
System.out.println("ms: " + (endMs - startMs));
}
System.out.println("\n\n");
System.out.println("min:" + min + " || max:" + max);
}| 성능 유지보수 등을 감안한 java 코딩 방법(참고) (0) | 2022.02.10 |
|---|---|
| java 생태계의 scaffold 및 MSA 참고 가능한 프로젝트ld 및 MSA 참고 가능한 프로젝트 (0) | 2021.03.24 |
| javapns 처리 예외 : javax.net.ssl.SSLException 관련 (0) | 2018.03.30 |
전에는 Guava cache, Ehcache등을 많이 사용했는데, 최근에는 Caffeine가 권장되고 있음
추후 여유되면
| java 사설인증키 처리 - 1 (0) | 2021.03.31 |
|---|---|
| 금칙어 처리(with Aho corasick 알고리즘) (2) | 2020.11.12 |
| java jvm계열 로컬캐시 (0) | 2020.09.02 |
| 유효한 IP인지 체크하는 java소스 샘플 (0) | 2020.06.11 |
| java(with spring)에서 MultipartFile를 통한 웹에서 사설 인증키를 읽어서 String으로 변환하는 샘플 소스 (0) | 2020.04.28 |
1. maven 디펜더시 추가
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
2. 샘플 소스
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* OKHttp를 이용한 요청 샘플
*
* @author 엄승하
*/
public class OkHttpSample {
static String reqUrl = "https://www.naver.com";
//OkHttp를 사용하기 위한 client 생성(기본적으로 new시 커넥션풀 셋팅됨), 1개를 재 사용해야함
// @formatter:off
static OkHttpClient client = new OkHttpClient.Builder()
//default timeout for client annotated requests
.readTimeout(1000, TimeUnit.MILLISECONDS).connectTimeout(200, TimeUnit.MILLISECONDS).writeTimeout(500, TimeUnit.MILLISECONDS)
//.addInterceptor(new TimeoutInterceptor())
.build();
// @formatter:on
public static void main(String[] args) {
reqGetSample();
reqGetExtendTimeout();
reqPostSample();
}
/**
* HTTP Get요청 샘플
*/
public static void reqGetSample() {
try {
//Request request = new Request.Builder().addHeader("X-ReqSvcCd", "TESTER").addHeader("CloudFront-Viewer-Country", "DE").url(reqUrl).build();
Request request = new Request.Builder().addHeader("X-ReqSvcCd", "TESTER").url(reqUrl).build();
Response response = client.newCall(request).execute();
responseProcess(response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 타임아웃을 늘린 요청(기본 클라이언트는 존재하지만 타임아웃값만 확장)
*/
public static void reqGetExtendTimeout() {
try {
OkHttpClient extendedTimeoutClient = client.newBuilder().readTimeout(1200, TimeUnit.MILLISECONDS).build(); //해당 요청만 타임아웃 늘림
Request request = new Request.Builder().addHeader("X-ReqSvcCd", "TESTER").addHeader("CloudFront-Viewer-Country", "DE").url(reqUrl).build();
Response response = extendedTimeoutClient.newCall(request).execute();
responseProcess(response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* HTTP POST 요청 샘플
*/
public static void reqPostSample() {
try {
RequestBody formBody = new FormBody.Builder().add("user", "tester").build(); //FormBody사용시 content-type는 application/x-www-form-urlencoded
Request request = new Request.Builder().addHeader("X-ReqSvcCd", "TESTER").url(reqUrl).post(formBody).build();
Response response = client.newCall(request).execute();
responseProcess(response);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void responseProcess(Response response) throws IOException {
if (response.isSuccessful()) {
String rtnMsg = response.body().string();
System.out.println(String.format("성공. 응답 msg => '%s", rtnMsg));
} else {
System.out.println(String.format("응답에러(200이 아님). 응답 code:'%s'\n errMsg:'%s'", response.code(), response.body().string()));
}
System.out.println("\n");
}
}

| Hyperledger fabric- 블럭체인 플랫폼 (0) | 2021.05.21 |
|---|---|
| RESTful API 설계에 대한 개인적인 생각 - 1 (0) | 2021.04.29 |
| HTTP 응답헤더에 서버 종류 삭제 - 보안처리 목적 (0) | 2020.07.10 |
| git repository 변경하기 (0) | 2020.04.22 |
| SVN -> GIT 변경방법(커밋이력 보존됨) (0) | 2020.01.31 |