서비스를 개발하다보면 항상 금칙어처리가 필요하게 됩니다.

금칙어 갯수가 적으면 상관 없는데 갯수가 많으면(특히 중국 서비스하면..;) 성능을 잘 생각해서 처리해야 합니다.

 

관련해서 참고용 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;

	}

}

전에는 Guava cache, Ehcache등을 많이 사용했는데, 최근에는 Caffeine가 권장되고 있음

 

  1. Java 8 이상에서만 사용 가능
  2. Spring에서도 지원하는 구현체가 추가되었고 5.0에서는 Guava Cache 지원이 없어짐
  3. Guava cache 개발자가 다시 만든 라이브러리
  4. 앞으로 발전 가능성도 높은편

 

추후 여유되면 

java 로컬캐시

 -Guava cache보다 좋음

- https://github.com/ben-manes/caffeine

import org.apache.commons.validator.routines.InetAddressValidator;

위 import 후에 아래 소스

String decodedIp = URLDecoder.decode(nidIp, "UTF-8"); //IPV6의 경우 인코딩되어서 들어옴
if (InetAddressValidator.getInstance().isValid(decodedIp) == false) {
return null;
}

 

 

/**

* private key 내용을 얻어옴

* - -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴

*

* @param privateKeyFile

* @return

*/

private String getPrivateKeyBody(MultipartFile privateKeyFile) {

 

try (BufferedReader br = new BufferedReader(new InputStreamReader(privateKeyFile.getInputStream()))) {

 

String line;

StringBuilder sb = new StringBuilder();

while ((line = br.readLine()) != null) {

if (line.contains("PRIVATE KEY")) { //guard line은 pass

continue;

}

sb.append(line);

}

 

return sb.toString();

 

} catch (Exception e) {

log.error(e.getMessage(), e);

throw new RuntimeException(e.getMessage(), e);

}

}

 

 

import java.util.Properties;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.URLName;
import javax.mail.search.FlagTerm;
/**
 * IMAP 이메일을 가져오는 샘플 소스
 *  - 주의: 간단히 POC한 소스라서 예외처리 및 부가처리는 완벽히 안되어 있음
 *
 * @author 엄승하
 */
public class IMAPEmailGetSample {
     public static void main(String[] args) throws Exception {
          System.out.println("-- IMAP Emal 가져오기: Start\n\n");
          String host = "입력필요"; //imap 호스트 주소. ex) imap.gmail.com
          String userEmail = "입력필요"; //유저 이메일 주소
          String password = "입력필요"; //유저 암호
          IMAPMailService mailService = new IMAPMailService();
          mailService.login(host, userEmail, password);
          int messageCount = mailService.getMessageCount();
          //테스트 목적이라서 5개 초과이면 5개만 처리: TODO 삭제
          if (messageCount > 5) {
               messageCount = 5;
          }
          Message[] msgArray = mailService.getMessages(false);
          for (int i = 0; i < messageCount; i++) {
               Message msg = msgArray[i];
               if (msg.getSubject() != null) {
                    System.out.println(String.format("컨텐츠타임: %s", msg.getContentType()));
                    System.out.println(String.format("발신자[0]: %s", msg.getFrom()[0]));
                    System.out.println(String.format("메일제목: %s", msg.getSubject()));
                    String mailText = mailService.getEmalText(msg.getContent());
                    System.out.println(String.format("메일내용: %s", mailText));
               }
          }
          mailService.logout(); //로그아웃
          System.out.println("\n\n-- IMAP Emal 가져오기: 종료");
     }
}
/**
 * IMAP 관리 inner클래스
 *  - 참고: https://javapapers.com/java/receive-email-in-java-using-javamail-gmail-imap-example/
 * 
 * @author 엄승하
 */
class IMAPMailService {
     private Session session;
     private Store store;
     private Folder folder;
     // hardcoding protocol and the folder
     // it can be parameterized and enhanced as required
     private String protocol = "imaps";
     private String file = "INBOX";
     public IMAPMailService() {
     }
     public boolean isLoggedIn() {
          return store.isConnected();
     }
     /**
      * 메일 본문 텍스트 내용을 가져옴
      *
      * @param content
      * @return
      * @throws Exception
      */
     public String getEmalText(Object content) throws Exception {
          //TODO: 개발 필요
          System.out.println("####  컨텐츠 타입에 따라서 text body 또는 멀티파트 처리 기능 구현이 필요");
          if (content instanceof Multipart) {
               System.out.println("Multipart 이메일임");
          } else {
               System.out.println(content);
          }
          return null;
     }
     /**
      * to login to the mail host server
      */
     public void login(String host, String username, String password) throws Exception {
          URLName url = new URLName(protocol, host, 993, file, username, password);
          if (session == null) {
               Properties props = null;
               try {
                    props = System.getProperties();
               } catch (SecurityException sex) {
                    props = new Properties();
               }
               session = Session.getInstance(props, null);
          }
          store = session.getStore(url);
          store.connect();
          folder = store.getFolder("inbox"); //inbox는 받은 메일함을 의미
          //folder.open(Folder.READ_WRITE);
          folder.open(Folder.READ_ONLY); //읽기 전용
     }
     /**
      * to logout from the mail host server
      */
     public void logout() throws MessagingException {
          folder.close(false);
          store.close();
          store = null;
          session = null;
     }
     public int getMessageCount() {
          //TODO: 안 읽은 메일의 건수만 조회하는 기능 추가
          int messageCount = 0;
          try {
               messageCount = folder.getMessageCount();
          } catch (MessagingException me) {
               me.printStackTrace();
          }
          return messageCount;
     }
     /**
      * 이메일 리스트를 가져옴
      *
      * @param onlyNotRead 안읽은 메일 리스트만 가져올지 여부
      * @return
      * @throws MessagingException
      */
     public Message[] getMessages(boolean onlyNotRead) throws MessagingException {
          if (onlyNotRead) {
               return folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
          } else {
               return folder.getMessages();
          }
     }
}
2022-10-31 기준으로 업데이트해뒀습니다.
 
  • AdoptoOpenJDK관리 주체 변경에 따른 URL변경 등의 내용 업데이트
  • 참고로 https://github.com/adoptium/temurin11-binaries/releases 에서 필요한 환경에 맞춰서 최신 버전 다운로드
    • 예) 리눅스 64bit 핫스팟이라면 OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz 다운로드
 

커맨드 예)

#다운로드
wget 'https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.17%2B8/OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz' -O ~/apps/OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz
#압축해제
cd ~/apps && tar -xzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz && rm -f OpenJDK11U-jdk_x64_linux_hotspot_11.0.17_8.tar.gz

#심볼릭링크(필요시)
cd ~/apps && ln -s jdk-11.0.7+10 jdk_11

#DNS TTL수정(10초로, 어플리케이션마다 틀려야할 수 있음)
echo 'networkaddress.cache.ttl=10' >> ~/apps/jdk-11.0.17+8/conf/security/java.security
 
 
 
 

Releases · adoptium/temurin11-binaries

Temurin11 binaries. Contribute to adoptium/temurin11-binaries development by creating an account on GitHub.

github.com

 

openjdk 11설치 방법 및 기본 설정
  • 유저 home디렉토리/apps 하위에 설치한다는 전제
 
 
 
 
[설치]
#JDK 다운로드 및 설치(가능하면 adoptopen JDK판 사용)
 
  1. adoptopenJDK 배포판( https://adoptopenjdk.net/releases.html?variant=openjdk11&jvmVariant=hotspot 에서 최신버전 확인 후 설치. 리눅스서버를 예로들면 OS는 Linux, Architecture X64)
    1. 명령어 예)
      1. cd ~/apps && tar -xzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.7_10.tar.gz && rm -f OpenJDK11U-jdk_x64_linux_hotspot_11.0.7_10.tar.gz && ln -s jdk-11.0.7+10 jdk_11
  2. 오라클 배포판(약 6개월만 패치 지원)
    1. cd ~/apps && tar -xzf openjdk-11.0.2_linux-x64_bin.tar.gz && rm -f openjdk-11.0.2_linux-x64_bin.tar.gz && ln -s jdk-11.0.2 jdk_11
 
 
 
[기본설정(튜닝)]
 
1. JAVA DNS TTL 수정
echo 'networkaddress.cache.ttl=10' >> ~/apps/jdk_11/conf/security/java.security
 
# 환경변수 추가
echo 'export JAVA_11_HOME=~/apps/jdk_11' >> ~/.bashrc
source ~/.bashrc
 
#확인 방법
$JAVA_11_HOME/bin/java -version
=> java 11버전으로 나오면 됨
 
 
2. 난수 생성시 필요한 엔트로피 고갈되어 발생하는 문제 처리
원인 : java.security.SecureRandom 클래스는 Linux에서는 기본적으로 /dev/random 을 이용해서 난수를 생성하는데 필요한 엔트로피가 고갈되면 램덤값을 생성하는 로직이 대기하게 되고, 성능에 문제가 생길수 있음
 
해결 방법
- java 어플리케이션 시작시 -Djava.security.egd=file:/dev/./urandom 옵션을 주고 실행해서 해결
 
 

 

apache http client의 타임아웃 관련(기본 값 등) - 버전마다 틀릴 수 있으며 아래는 4.5.3 기준





* 요약 

 - 0으로 셋팅하면 무한이고, 음수로 셋팅하면 OS 시스템의 기본값을 따른다.

 - 리눅스 계열에서 시스템 기본값은 net.ipv4  timeout time wait쪽을 살펴보면 되는 것 같음

      




[참고 - 소스]


 /**

     * Determines the timeout in milliseconds until a connection is established.

     * A timeout value of zero is interpreted as an infinite timeout.

     * <p>

     * A timeout value of zero is interpreted as an infinite timeout.

     * A negative value is interpreted as undefined (system default).

     * </p>

     * <p>

     * Default: {@code -1}

     * </p>

     */

    public int getConnectTimeout() {

        return connectTimeout;

    }

+ Recent posts