메모 목적의 글로써 생략되는 내용이 많이 있을 수 있는점을 감안해주세요

(우아콘 2020참고)

 

  1. querydsl exist 사용 금지
    1. 기본
      1. sql의 exist는 조건을 만족하는 1번째 row를 만나면 쿼리가 바로 종료
      2. count의 경우는 모든 row를 scan해야하기 때문에 성능이 exist보다 안 좋음
    2. qeurydsl의 exist는 count 쿼리를 이용해서 수행되기 때문에 성능이 안 좋음
    3. 직접 구현
      1. limit 1을 추가함(예전에 mysql 생쿼리로 개발할때도 이렇게 했었음..)
        1. 다만, 조회결과가 없으면 0이 아니라 null을 반환하니 null 체크처리 주의
  2. cross join 회피
    1. 나올수 있는 모든 경우의 수를 대상으로 하기때문에 성능이 안좋은 cross join. 피하는게 좋음
    2. querydsl은 묵시적 join사용할때 cross join발생할 수 있음
    3. 명시적 join으로 회피
      1. 예) innerJoin 메소드로 명시적 처리
  3. Entity 보다는 DTO를 우선 사용
    1. entity 조회시
      1. 실시간으로 Entity 변경이 필요한 경우에는 장점 -> 다만 회사 프로덕션 환경에서 이럴일은 없음
      2. hibernate 캐시 불필요
      3. 불필요한 컬럼 조회
      4. OneToOne N+1  쿼리 등
      5. 단순 조회 기능에서는 성능 이슈 요소가 많음
    2. DTO 조회시
      1. 고강도 성능 개선, 대량의 데이터 조회가 필요한 경우
      2. 조회 컬럼 최소화하기
        1. as 표현식으로 대체하면 DB에실행되는쿼리에서 as컬럼은 제외됨
          1. 예) Expressions.asNumber(bookNo).as("bookNo")
      3. Select 컬럼에 Entity 자제
        1. 불필요 신규 Entity의 모든 컬럼이 조회될 수 있음
        2. OntToOne 관계에 대해서 매건마다 조회됨(N+1무조건 발생하게 됨)
  4. Group by 최적화
    1. mysql에서는 order by null 을 이용하면 file sort가 발생하지 않는 기능이 존재(개인적으로도 많이 사용)
    2. querydsl에서는 order by null을 지원하지 않기 때문에 직접 구현해서 사용
      1. 정렬이 필요하더라도 조회결과가 100건 이하라면 어플리케이션에서 정렬하는걸 고려(페이징이 아닐때만)
        1. was는 scale out이 가능하지만 DB는 어려움
  5. 커버링 인덱스 사용
    1. 개인적으로도 대용량 게시판 서비스의 페이징 개념 만들대 사용함
    2. jpql은 from절의 서브쿼리를 지원하지 않기 때문에 우회처리가 필요
      1. 쿼리를 2개로 나눠서 실행
        1. Cluster Key(PK와 같은)를 커버링 인덱스로 빠르게 조회하고
        2. 조회된 Key로 select쿼리를 in 쿼리로 실행
  6. update 최적화
    1. 무분별한 DirtyChecking을 꼭 확인해야함
      1. 실시간 비지니스 처리, 실시간 단건 처리시
        1. 하이버네이트 캐시는 일괄 업데이트시 캐시 갱신이 안되기때문에
      2. Querydsl.update
        1. 대량의 데이터를 일괄로 Update 처리시
  7. bulk insert
    1. JPA에서는 auto_increment일때는 insert합치기가 적용되지 않는 문제가 있음
    2. jdbcTemplate롤 bulk insert는 처리 가능하나, 컴파일 체크, Type-safe 개발이 어려움
      1. 문자열로 쿼리를 작성해야해서
    3. 따로 개발해서 진행할수도 있지만... 개인적으로 고민좀 되는 부분이 있음
  1. 브랜치 생성

 

local에서 remote 브랜치로부터 신규 브랜치를 생성

 

 

 

remote에 test-1 브랜치 생성됨을 확인 가능

 

 

 

  1. test-1 브랜치를 master로 merge하기

intelliJ 에서 Git-> Merge 메뉴
master에 test-1 브랜치를 Merge 실행
이후 remote에 푸시하면 test-1에서 작업한 내용이 머지되어 remote까지 반영되었음을 확인 가능

 

 

 

참고 : 에버노트에 작성했던 글을 옮겨서 블로그에 작성한건데 양식 유지가 안되서 읽기가 조금 안 좋네요


 

  1. 목적
    1. 서비스를 운영중에 프로세스 재 시작없이 로그 레벨을 변경해야할 경우가 있을 수 있습니다.(Dynamically modify the log level)
      1. 예) 특정 케이스 디버깅을 위해서 프로덕션에서 디버그 레벨로 변경
    2. 여러가지 방법이 있을 수 있는데, alibaba의 arthas라는 프로젝트를 이용해서 변경해보고 arhas의 사용법을 익혀봄으로써 트러블슈팅 역량을 향상해보겠습니다.
  2. 참고 링크(arthas)
    1. github
    2. onlin tutorials
    3. documentation
    4. intellij plugin

3. dzone 참고 링크


  1. arthas 설치 및 실행
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

-- 사용법 확인하고 싶으면 -h옵션
java -jar arthas-boot.jar -h

 

  1. logger command를 이용해서 로그레벨 변경 방법
    1. 주의: 로그레벨을 낮추고 확인이 완료되면 잊지말고 원복 필요
    2. logger command
-- 현재 어플리케이션의 로그 레벨확인
logger

-- search class detail
예) sc -d yourClassName

-- ROOT라는 로거의 레벨을 debug로 변경
-- logger -c 클래스로더의해시--name ROOT --level debug
예) logger -c 51081592 --name ROOT --level debug

 

샘플

  1. arthas에서 로그레벨 변경 명령어
  2. 어플리케이션에서 로그가 변경되어 출력되는 모습
    1. logger 명령어로 ERROR레벨 이상으로 변경한 결과 -> ERROR레벨 이상의 로그만 출력됨을 확인 가능

 

 

내부 신입 개발자 등을 위해서 간단히 프로토타이핑한 java stream filter 중복제거 소스입니다.

package stream;

import lombok.Data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 내부 개발자 교육을 위한 간단한 java Stream 샘플 소스
 *  - 중복제거
 *
 * @author 엄승하
 */
public class StreamDistinct {

	public static void main(String[] args) {

		//테스트 데이터 생성
		int cnt = 10_000;
		List<TestVO> list = new ArrayList<>(cnt);
		for (int i = 1; i <= cnt; i++) {
			list.add(getTestVO(1, i)); //usreId는 동일하게 생성
		}

		//중복되지 않는 userId 추가 생성
		list.add(getTestVO(2, 10));
		list.add(getTestVO(3, 11));
		list.add(getTestVO(4, 12));

		System.out.println(String.format("\n테스트 list의 데이터 갯수:%d\n", list.size()));

		long start = System.currentTimeMillis();
		List<String> result = list.stream().filter(distinctByKey(m -> m.getUserId())).map(TestVO::getUserId).collect(Collectors.toList()); //java stream을 이용해서 유니크한 userId리스트만 모으기
		long end = System.currentTimeMillis();

		System.out.println("== Start: 중복 제거된 userId 리스트");

		for (String userId : result) {
			System.out.println(userId);
		}
		System.out.println("== End: 중복 제거된 userId 리스트");

		System.out.println("\n중복제거 stream filter 소요시간(millis): " + (end - start));

	}

	private static TestVO getTestVO(int suffixUserId, int suffixBookName) {

		TestVO vo = new TestVO();
		vo.setUserId("eom_" + suffixUserId);
		vo.setBookName("book_" + suffixBookName);

		return vo;
	}

	/**
	 * 특정 키로 중복제거
	 *
	 * @param keyExtractor
	 * @param <T>
	 * @return
	 */
	private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
		Map<Object, Boolean> map = new HashMap<>();
		return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
	}

	@Data
	private static class TestVO {

		private String userId;
		private String bookName;

	}
}

 

실행하면 아래와 같은 결과가 나옵니다.

테스트 list의 데이터 갯수:10003

== Start: 중복 제거된 userId 리스트
eom_1
eom_2
eom_3
eom_4
== End: 중복 제거된 userId 리스트

중복제거 stream filter 소요시간(millis): 2

사설 인증키를 blob으로 DB에 저장하고,

admin 시스템 만들어서 관리 자동화 하는 기능을 만드는데 가끔 잊어버립니다.(여러 랭귀지를 쓰다보니...)

 

잊어버릴때 찾기 위해서 필요한 부분을 메모 목적으로 blog에 작성해보겠습니다.

 

/**
    * 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);
    }
}

몇년전 회사 시스템 고도화 과정에서 아이디어를 많이 얻은 프로젝트가 있는데 요즘에는 MSA 아키텍쳐 등에 대한 부분 등 많은 내용이 추가되었고 꾸준히 업데이트도 되는 것 같다.

 

그래서 팀에 후배 개발자가 입사하면 공부해보라고 알려주기도 한다.

 

해당 프로젝트는 jhipster라고 하며 주소는 www.jhipster.tech/ 이다.

 - git 주소는 github.com/jhipster/generator-jhipster

 

덧) 몇년전 외국 컨퍼런스 갔을때 하얀 머리의 할아버지가 MSA관련 강의를 열심히 듣고 계시던게 갑자기 생각나네..

  • pom.xml에 디펜더시 추가(maven 기준)
    • 버전은 maven repo나 git에서 확인
		<!-- https://mvnrepository.com/artifact/io.sentry/sentry-logback -->
        <dependency>
            <groupId>io.sentry</groupId>
            <artifactId>sentry-logback</artifactId>
            <version>3.2.0</version>
        </dependency>

 

  • logback.properties에 설정 추가
    • logback-spring.xml에서 참조하는 properties파일
#sentry에 전송시 environment 구분 값(ex. local, alpha, beta, qa, real)
sentry.environment=입력필요

#sentry 에서 dsn정보 확인 후 입력
sentry.dsn=입력필요
  • logback-spring.xml 에 설정 추가
    • 아래 appender추가 후 전송할 logger에 꼭 appender-ref ref를 추가해야함
<!-- 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>

 

 

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

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

 

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

	}

}

+ Recent posts