1. 목적

 - Mysql은 날짜/시간과 관련된 여러 데이터 타입을 제공하는데, 글로벌 서비스를 개발시 여러 시간대 정보가 포함되었을때를 위해서 DB데이터 타입에 대한 확인

 

2. 확인을 위한 테스트 및 결과

 

-- 테스트 데이터 저장용 테이블 생성
CREATE TABLE `timezone_test` (
  `datetime` DATETIME DEFAULT NULL COMMENT 'datetime필드',
  `date` DATE DEFAULT NULL COMMENT 'date필드',
  `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'timestamp필드',
  `int_unixTS` INT(10) UNSIGNED DEFAULT NULL COMMENT 'unix TS를 저장하기 위한 int(고정값 저장)'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='타임존 테스트';

-- 테스트 데이터 insert
INSERT INTO timezone_test VALUES (NOW(), NOW(), NOW(), UNIX_TIMESTAMP());

-- 타임존 변경 전 저장된 데이터 확인
SELECT t.*, UNIX_TIMESTAMP(t.datetime) AS convertUNIX_TS FROM timezone_test AS t;

-- 현재 타임존 정보 확인
SELECT @@GLOBAL.time_zone, @@SESSION.time_zone, @@system_time_zone;

-- 세션의 타임존 변경(예. +1시로 변경)
SET time_zone='+01:00';

-- 타임존 정보 변경 후 현재시간 확인하여 변경되었는지 확인
SELECT NOW();

-- 타임존 변경 후 저장된 값의 내용 조회
-- datetime, date필드는 DB타임존이 변경되어도 기존 값이 변경안됨, timestamp는 DB내부적으로 UTC로 저장 후 DB타임존 설정정보를 이용해서 변환하여 보여줌 -> 즉, timestmap만 타임존이 변경되었을때 영향을 받음
-- 여러 시간대를 사용한다면, 가능하면 timestamp타입으로 데이터를 저장하는게 좋음
SELECT t.*, UNIX_TIMESTAMP(t.datetime) AS convertUNIX_TS FROM timezone_test AS t;

 

 

3. 주의

 - timestamp는 2038년까지만 표시되기에 주의(예. admin시스템 만들때 무한이라는 미래값 처리할때 2999년 과 같은 값으로 처리 불가)

 

 


샘플: 저장된 동일 데이터를 DB타임존 변경 전과 후 비교

 - 타임존 변경 전

DB타임존 변경전의 데이터 확인

 - 타임존 변경 후

DB 타임존을 +9에서 +1로 변경 후 확인 내용. 기존 대비 timestamp만 8시간 전으로 표시됨

 

 

참고

 - https://dev.mysql.com/doc/refman/5.7/en/datetime.html

 

MySQL :: MySQL 5.7 Reference Manual :: 11.2.2 The DATE, DATETIME, and TIMESTAMP Types

11.2.2 The DATE, DATETIME, and TIMESTAMP Types The DATE, DATETIME, and TIMESTAMP types are related. This section describes their characteristics, how they are similar, and how they differ. MySQL recognizes DATE, DATETIME, and TIMESTAMP values in several f

dev.mysql.com

 

참고
 - 타임존 변환 사이트 : https://savvytime.com/converter/pdt-to-kst-utc/aug-1-2021/3am

java 8버전 이상 필요


import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
 * 타임존 변환 샘플 소스
 *
 * @author
 */
public class TimeZoneConvert {

	public static void main(String[] args) {
		pstData();
	}

	/**
	 * PST로 변환 샘플
	 *  - 참고: mysql 기준 쿼리 샘플 SELECT CONVERT_TZ( NOW(), 'UTC', 'Asia/Seoul') AS kst, CONVERT_TZ(NOW(), 'UTC', 'America/Los_Angeles') AS PST
	 */
	public static void pstData() {

		DateTimeFormatter strFMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //문자 출력용 포맷

		//PST(Pacific Standard Time). 태평양표준시로 UTC-8. PST는 흔히 LA타임으로 불리며, ZoneID는 'America/Los_Angeles' 임
		//PST와 KST는 17시간 차이가 남(PST가 17시간 늦음). KST->PST는 17시간 빼면됨
		//String targetStrPST = "Mar 1, 2021 11:42:23 PM PST"; //KST 기준으로 2021-03-02 16:42:23
		String targetStrPST = "Mar 4, 2021 05:32:33 PM PST"; //KST 기준으로 2021-03-02 16:42:23

		DateTimeFormatter targetFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //대상 문자 PST의 포맷
		LocalDateTime targetPstDT = LocalDateTime.parse(targetStrPST, targetFMT);

		ZonedDateTime pstZDT = targetPstDT.atZone(ZoneId.of("America/Los_Angeles")); //PST는 ZoneId가 'America/Los_Angeles' 임
		System.out.println("PST ymdt=> " + pstZDT.toLocalDateTime().format(strFMT));

		ZoneId kstZoneId = ZoneId.of("Asia/Seoul");
		LocalDateTime kstDT = pstZDT.withZoneSameInstant(kstZoneId).toLocalDateTime(); //KST로 변환
		String kstStr = kstDT.format(strFMT);

		System.out.println("KST ymdt => " + kstStr);
		System.out.println("KST ymdt toEpochSecond(유닉스 타임스탬프) => " + kstDT.toEpochSecond(kstZoneId.getRules().getOffset(kstDT)));

	}
    
    /**
	 * PDT로 변환 샘플
	 */
	public static void pdtData() {

		final ZoneId pdtZoneId = ZoneId.of("GMT-07:00"); //PDT는 GMT-07:00
		final DateTimeFormatter strFMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //문자 출력용 포맷

		//참고링크: https://savvytime.com/converter/pdt-to-kst-utc/aug-1-2021/3am
		String targetStrPDT = "Aug 1, 2021 12:03:00 AM PDT"; //KST 기준으로 2021-08-01 16:03:00이고, UTC기준으로는 2021-08-01 07:03:00

		DateTimeFormatter targetFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //대상 문자 포맷
		LocalDateTime targetPdtDT = LocalDateTime.parse(targetStrPDT, targetFMT); //PDT의 LocalDateTime 객체가 생성됨

		// @formatter:off
		System.out.println(
			String.format(
				"PDT 테스트 문자열: %s\n"
				+ "PDT LocalDateTime: %s\n"
				+ "PDT EpochSecond: %s\n"
				+ "PDT -> UTC LocalDateTime: %s\n"
				+ "PDT -> KST LocalDateTime: %s",
				targetStrPDT,
				targetPdtDT.format(strFMT),
				targetPdtDT.atZone(pdtZoneId).toEpochSecond(),
				targetPdtDT.atZone(pdtZoneId).withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime().format(strFMT),
				targetPdtDT.atZone(pdtZoneId).withZoneSameInstant(ZoneId.of("Asia/Seoul")).toLocalDateTime().format(strFMT)
			)
		);

	}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

/**
 * CSV파일을 읽어서 파싱하여 DB(Mysql)에 저장하는 프로그램 샘플
 *  - apache commons-csv를 이용해서 CSV파일을 파싱(참고: https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0 )
 *
 * @author
 */
@Slf4j
public class CsvFileParseSaveToDB {

	private static String targetFilePath = "CSV파일 전체 경로";

	public static void main(String[] args) {

		//한글 깨지 방지를 위해서 characterEncoding=UTF-8 처리
		final String jdbcURL = "jdbc:mysql://DB주소:3306/논리DB명?characterEncoding=UTF-8";
		final String username = "DB ID";
		final String password = "DB 암호";

		final int batchSize = 2_000; //bulk insert시 커밋 갯수

		Connection connection = null;

		try {

			connection = DriverManager.getConnection(jdbcURL, username, password);
			connection.setAutoCommit(false);

			String sql = "insert  into `temp_test`(`name_1`,`name_2`) " + "VALUES (?, ??)";

			PreparedStatement statement = connection.prepareStatement(sql);

			int columnSize = 2; //CSV 데이터 필드 컬럼 갯수

			List<CSVRecord> records = getCsvRecords();
			for (int row = 0; row < records.size(); row++) {

				CSVRecord data = records.get(row);
				for (int fieldIndex = 0; fieldIndex < columnSize; fieldIndex++) {
					statement.setString(fieldIndex + 1, data.get(fieldIndex));
				}

				statement.addBatch();
				if (row % batchSize == 0) {
					statement.executeBatch();
					System.out.println(String.format("statement.executeBatch ing row ==> %s", row));
					connection.commit(); //DB서버 부하분산을 원하는 대용량 처리시 중간중간 커밋

					sleep(1); //부하 분산
				}

			}

			//남아있는 데이터 처리
			System.out.println("나머지 데이터도 executeBatch ");
			statement.executeBatch();
			connection.commit();

			connection.close();

		} catch (IOException ex) {
			System.err.println(ex);
		} catch (SQLException ex) {
			ex.printStackTrace();

			try {
				connection.rollback();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

	}

	private static void sleep(long millis) {

		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private static List<CSVRecord> getCsvRecords() throws IOException {

		File targetFile = new File(targetFilePath);

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			log.debug("\nCSV 헤더\n\t{}\n데이터 샘플\n\t{}\n", parser.getHeaderMap(), records.get(sampleDataRow));
			log.info("\n\t헤더 필드 갯수 :{}\n\t데이터 갯수 :{}\n\t{}번째 row의 데이터 필드 갯수:{}\n\n", parser.getHeaderMap().size(), records.size(), sampleDataRow,
				records.get(sampleDataRow).size());

			return records;
		}
	}

}

1. 필요사항(maven dependency추가)

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-csv</artifactId>
            <version>1.9.0</version>
        </dependency>

 

2. 간단한 java CSV파일 파싱 샘플 소스

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;

/**
 * CSV파일을 읽어서 파싱
 *  - apache commons-csv를 이용해서 CSV파일을 파싱: https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0
 *
 * @author 엄승하
 */
@Slf4j
public class CsvFileParser {

	public static void main(String[] args) throws IOException {

		//파싱할 CSV파일
		File targetFile = new File("CSV파일 경로");

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			log.debug("\nCSV 헤더\n\t{}\n데이터 샘플\n\t{}", parser.getHeaderMap(), records.get(sampleDataRow));

			//샘플 데이터의 필드 데이터를 개행하여 출력
			//			for (String field : records.get(sampleDataRow)) {
			//				System.out.println(field);
			//			}

			log.info("헤더 필드 갯수 :{} | {}번째 row의 데이터 필드 갯수:{}", parser.getHeaderMap().size(), sampleDataRow, records.get(sampleDataRow).size());
			log.info("헤더: {} ", parser.getHeaderMap());

		}

	}
}

 

기존에도 나름의 기준이 있었는데, 최근에 정리가 되어 텍스트로 메모해봅니다.

 

1. 호기심

 - 업무적인 호기심이 있는 사람을 채용

   예) 도메인지식, 실무지식(개발/기획)에 대한 호기심이 있어야 찾아보는 노력을 하고 업무 역량이 빠르게 발전

2. 책임감

 - 담당 업무에 대한 책임감이 있는 사람을 채용

   예) 장애나 버그가 발생했는데 확인/대응 안 하는 개발자는 안됨

3. 컬쳐핏

 - 팀/조직과 잘 어울릴 수 있는 지원자를 채용

 - 혼자서만 일하는게 아님. 우리의 서비스는 혼자서 만들 수 있는 규모가 아니며 점점 복잡해지는데 컬쳐핏이 안 맞는 사람은 채용이 불가

 - 실무능력이 아무리 뛰어나도 컬쳐핏이 안 맞는 사람은 채용 불가

 

P.s. 시점/상황에 따라서 기준이 조금씩 변경은 되는 것 같습니다.

2020년경에 tokumx-> mongo 3.4 -> mongo 4.2로 DB서버 버전업시 어플리케이션 수정을 위해 정리했던 내용입니다.

 - java드라이버의 호환성내용입니다.

 - API는 4.0.4버전을 사용했고, admin은 하위버전의 DB까지 지원해야해서 3.4.3버전을 사용했습니다.

 


당시 조건을 간단히 정리하자면 아래와 같습니다.

  1. 분당 1만건/100Mb 이상의 좀 큰 사이즈의 트래픽이 저장요청 API 어플리케이션으로 몽고DB에 저장됨
  2. mongoDB는 10여개 이상의 셋트(replica형태로)로 aws ec2에 설치해서 사용 중
    1. mongoDB가 자체 개선되기전부터 특정 기능(tokumx의 압축, 파티션 등)을 사용했어야해서 tokumx, mongo 3.4가 혼재되어 있었음
    2. 여러 이슈로 mongo 4.2로 버전업 필요한 상황
  3. 저장 API는 1개의 소스-> 1개의 DB만 커넥션
  4. admin시스템은 1개의 소스->N개의 DB커넥션

 

참고

 - https://docs.mongodb.com/drivers/java/sync/current/compatibility/

 

구)

보통은 검색엔진 elastic search로 검색기능을 개발합니다.

다만, 백오피스 or 사용자가 적음 or 검색엔진 도입하기에는 ROI가 맞지 않는 경우가 생각보다 많습니다.

이런 경우 개인적으로는 mysql n-gram으로 검색 기능을 많이 만듭니다.(일반적으로 RDB는 서비스에서 거의 필수로 사용)

 

관련하여 간단히 내용을 정리해둡니다.

  • 개인 메모 목적으로 작성하는 글이라서 생략되는 부분이 많습니다. 구글을 검색해보면 저보다 더 자세히 정리해둔 분들도 많으니 누락된 부분은 해당글을 확인하거나 mysql 공식 문서를 확인해보세요.

 

  1. N-gram이란?
    1. 전문 검색에서 보통 많이 쓰며 문자열에서 n개의 연속적인 단어 나열
    2. 예) abcd라는 문자
      1. N이 2라면 ab, bc, cd라는 인덱스
      2. N이 3이라면 abc, bcd라는 인덱스
  2. 테이블 생성
    -- 테이블 생성
    CREATE TABLE `articles` (
      `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
      `title` VARCHAR(200) NOT NULL COMMENT '제목',
      `body` TEXT COMMENT '본문',
      PRIMARY KEY (`id`,`title`),
      FULLTEXT KEY `title` (`title`,`body`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='글'​
  3. 사용 예
    -- 테스트 데이터 insert
    INSERT INTO articles(
    	title,
    	body
    ) 
    VALUES(
    	'국민연금 기금 적립금 900조원 돌파',
    	'25일 국민연금에 따르면 올 2분기 기금 적립금은 908조 3000억 원으로 집계됐다. 연금보험료(655조 4000억 원)와 운용 수익금(502조 3000억 원)을 합산한 금액에 연금급여(239조 6000억 원)와 관리 운영비(9조 8000억 원)를 제외한 게 적립금이다'
    )
    ;
    
    
    -- 쿼리 플랜 확인
    EXPLAIN
    SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('연금' IN NATURAL LANGUAGE MODE);​

쿼리 결과 확인

 

  1. n-gram token size
    1. InnoDB에서 n-gram의 최소 토큰 사이즈는 2
      mysql n-gram 토큰 사이즈 확인
    2. 중국어와 같이 1글자도 처리하고 싶으면 1로 변경
      1. 단, DB에 부하가 올라가겠죠.
  2. STOPWORD에 대해서
    1. 영어를 ngram으로 적용하였을 경우, a,for, to와 같은 검색결과 퀄리티를 떨어트리는 단어는 제외됨. 이를 stopword라고 함(참고 링크)
    2. 비 활성화 하고자 하면 innodb_ft_enable_stopword OFF
  3. 추가 팁
    1. AWS 오로라 DB는 ngram_token_size 수정 불가 -> 그냥 mysql rds사용
      1. 개인적으로는 오로라 DB가 좋아서 여러 서비스에서 사용 중
      2. 오라라 DB의 엔진이 업데이트되어서 현재는 수정 가능해졌을 수도 있음

메모. 개인적으로 사용하는 logback file appender 설정

 

  • 서비스를 운영시 파일로 log를 남길때는 아래 사항을 꼭 고려해야함
    • 로그 파일 용량이 너무 커서 vi로 오픈이 힘든걸 막아야함
    • 로그파일을 삭제 없이 계속 저장하면 disk full로 서비스 장애가 발생하니 주의
    • 적당한 보관기간으로 압축해서 롤링

logback.properties 설정

#로그파일 저장 디렉토리(유저/logs 디렉토리 하위에 서비스코드별로 디렉토리 구분)
log.file.dir=/home/svc_user/logs/svc_cd

#로그 파일 명(prefix에 환경코드 추가, 1개 서버에 blue/green 배포하는 경우를 위해서 was port별로 파일명 구분)
log.file.name=real_svc_cd_${server.port}.log

log.file.full=${log.file.dir}/${log.file.name}

#백업
log.file.backup.fileNamePattern=${log.file.dir}/backup/${log.file.name}_%d{yyyy-MM-dd}_%i.zip

#로그 패턴
log.pattern=%d{yyyy-MM-dd HH:mm:ss} [${HOSTNAME}] [%-5level] %logger{35}:%L - %msg%n

 

logback-spring.xml 설정(참고 링크)

 

  • 로그파일 1개는 20M로 유지
  • 최대 50의 로그파일을 유지
  • 최대 1000MB
  • 압축해서 롤링
 <!-- 외부 설정파일을 사용 -->
    <property resource="properties/logback.properties"/>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${log.file.full}</File>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.file.backup.fileNamePattern}</fileNamePattern>
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>50</maxHistory>
            <totalSizeCap>1000MB</totalSizeCap>
        </rollingPolicy>
    </appender>

 

+ Recent posts