커버링 인덱스(Covering Index)란 무엇인가?

커버링 인덱스는 쿼리가 필요로 하는 모든 컬럼을 포함하는 인덱스를 말합니다. 이 인덱스를 사용하면 테이블의 실제 데이터 페이지에 접근하지 않고도 인덱스만으로 원하는 데이터를 조회할 수 있어 디스크 I/O를 절약할 수 있습니다.

MySQL 공식 문서 정의:

*"쿼리에서 검색되는 모든 컬럼을 포함하는 인덱스입니다. 인덱스 값을 전체 테이블 행을 찾는 포인터로 사용하는 대신, 쿼리는 인덱스 구조에서 값을 반환하여 디스크 I/O를 절약합니다. InnoDB는 MyISAM보다 더 많은 인덱스에 이 최적화 기술을 적용할 수 있습니다. 왜냐하면 InnoDB의 보조 인덱스에는 기본 키 컬럼도 포함되어 있기 때문입니다. InnoDB는 해당 트랜잭션이 끝날 때까지 트랜잭션에 의해 수정된 테이블에 대한 쿼리에 이 기술을 적용할 수 없습니다."*

단일 컬럼 인덱스와 다중 컬럼 인덱스 모두 커버링 인덱스로 활용될 수 있습니다. 적절한 인덱스 설계와 쿼리 작성으로 이 최적화 기법을 최대한 활용할 수 있습니다.


커버링 인덱스를 사용하는 이유

  • 디스크 I/O 감소: 인덱스만으로 데이터를 조회하므로 디스크 접근 횟수가 줄어듭니다.
  • 성능 향상: 디스크 I/O 감소로 쿼리 응답 속도가 빨라집니다.
  • 잠금 경합 감소: 테이블 데이터 페이지에 대한 접근이 줄어들어 잠금 경합이 감소합니다.

클러스터드 인덱스와 비클러스터드 인덱스

클러스터드 인덱스 (Clustered Index)

  • 정의: 테이블의 실제 데이터가 인덱스와 동일한 구조로 저장되는 인덱스입니다.
  • 특징:
    • 테이블 당 하나만 존재합니다.
    • 기본 키(primary key)가 클러스터드 인덱스로 사용됩니다.
  • 장점:
    • 인덱스를 통해 바로 데이터에 접근하므로 조회 속도가 빠릅니다.

비클러스터드 인덱스 (Non-clustered Index)

  • 정의: 인덱스는 별도의 구조로 저장되고, 인덱스 엔트리는 데이터의 물리적 위치를 가리킵니다.
  • 특징:
    • 테이블 당 여러 개의 비클러스터드 인덱스를 가질 수 있습니다.
  • 단점:
    • 인덱스를 통해 데이터를 찾은 후 실제 데이터 페이지를 다시 조회해야 하므로 추가적인 I/O가 발생합니다.

MySQL 공식 문서 인용:

*"클러스터드 인덱스를 통해 행에 접근하는 것은 빠릅니다. 왜냐하면 인덱스 검색이 행 데이터가 있는 페이지로 직접 연결되기 때문입니다. 테이블이 큰 경우, 클러스터드 인덱스 아키텍처는 인덱스 레코드와 다른 페이지에 행 데이터를 저장하는 스토리지 구조와 비교했을 때 디스크 I/O 작업을 절약할 수 있습니다."*


커버링 인덱스 사용과 미사용 시 성능 비교

실험: 100만 건의 데이터로 임시 테이블 생성 후 성능 비교

  1. 데이터 준비

    CREATE TABLE test_table (
        id INT PRIMARY KEY,
        col1 VARCHAR(100),
        col2 VARCHAR(100),
        col3 VARCHAR(100)
    );
    
    INSERT INTO test_table (id, col1, col2, col3)
    SELECT
        t1.number AS id,
        MD5(RAND()) AS col1,
        MD5(RAND()) AS col2,
        MD5(RAND()) AS col3
    FROM
        numbers AS t1  -- numbers 테이블은 1부터 1,000,000까지의 숫자를 가진 테이블이라고 가정
    LIMIT 1000000;
  2. 인덱스 설정

    • 커버링 인덱스 생성

      CREATE INDEX idx_col1_col2 ON test_table (col1, col2);
    • 인덱스 미사용

      • 인덱스를 생성하지 않음.
  3. 쿼리 실행 및 성능 측정

    • 커버링 인덱스 사용 시

      SELECT col1, col2 FROM test_table WHERE col1 = 'some_value';
      • 인덱스만으로 쿼리를 처리하여 빠른 응답 속도를 보입니다.
    • 인덱스 미사용 시

      SELECT col1, col2 FROM test_table WHERE col1 = 'some_value';
      • 전체 테이블 스캔이 발생하여 응답 시간이 느려집니다.
  4. 결과

    • 커버링 인덱스를 사용하면 쿼리 성능이 현저히 향상됩니다.
    • 인덱스를 사용하지 않으면 대량의 데이터를 처리할 때 성능 저하가 발생합니다.

GROUP BY에서의 커버링 인덱스 활용

GROUP BY 절에서도 커버링 인덱스를 활용하여 성능을 개선할 수 있습니다.

  • 인덱스 생성

    CREATE INDEX idx_col1_col2 ON test_table (col1, col2);
  • 쿼리 실행

    SELECT col1, COUNT(*) FROM test_table GROUP BY col1;
  • 설명

    • col1col2를 포함하는 인덱스를 생성하여 GROUP BY 시 인덱스만으로 결과를 도출합니다.
    • 디스크 I/O를 최소화하여 쿼리 성능을 높입니다.

참고 자료

  1. MySQL 공식 문서 - InnoDB Index Types
  2. MySQL 공식 문서 - Covering Indexes

'Database > My SQL' 카테고리의 다른 글

MySQL Index 정리 및 팁  (0) 2022.05.16

데이터 복구 과정에서 학습한 내용 기록

이번에 회사에서 AWS RDS로 DB 서버를 단계적으로 이전하는 과정에서 프로덕션 데이터가 소실되는 상황이 발생하였습니다. 다행히 바이너리 로그(binlog)를 보관하는 기간 내에 있어서 MySQL binlog를 활용하여 데이터를 복구할 수 있었습니다. 이때 학습한 내용을 기록 차원에서 남깁니다.


데이터 복구 과정

  1. 프로덕션 서버에서 binlog 확인

    SHOW BINARY LOGS;

    결과는 다음과 같았습니다:

  2. +---------------+-----------+ | Log_name | File_size | +---------------+-----------+ | binlog.000015 | 724935 | | binlog.000016 | 733481 | +---------------+-----------+

  3. 먼저, 프로덕션 서버에 binlog가 존재하는지 확인하였습니다.

  4. binlog 덤프 진행

    mysqlbinlog --read-from-remote-server --host=<프로덕션 호스트> --raw --user=<계정> --password \
    binlog.000015 binlog.000016

    이 명령어는 지정한 binlog 파일들을 로컬 디렉토리로 가져옵니다.

  5. 프로덕션 호스트로부터 binlog를 로컬로 덤프하기 위해 mysqlbinlog 유틸리티를 사용하였습니다.

  6. binlog에서 INSERT 및 UPDATE 추출

    mysqlbinlog binlog.000015 binlog.000016 --database=<DB명> --base64-output=DECODE-ROWS -v > output.sql
    • --database 옵션은 특정 데이터베이스만 대상으로 합니다.
    • --base64-output=DECODE-ROWS-v 옵션은 binlog 이벤트를 사람이 읽을 수 있는 SQL 형식으로 변환합니다.
  7. 덤프한 binlog 파일에서 특정 데이터베이스의 DML 문을 추출하기 위해 다음과 같이 파싱하였습니다.

  8. SQL 문 실행을 통한 데이터 복구

  9. 추출된 output.sql 파일에서 필요한 INSERT와 UPDATE 문을 확인하고, 이를 데이터베이스에 적용하여 데이터를 복구하였습니다.


향후 개선 사항

이번 복구 과정을 통해 몇 가지 개선할 점을 발견하였습니다.

  1. 자동화 도구 활용

    python binlog2sql.py -h <프로덕션 호스트> -u <계정> -p<비밀번호> \
    --start-file='binlog.000015' --stop-file='binlog.000016' \
    --databases=<DB명> --output-file=output.sql
  2. 수동으로 binlog를 파싱하는 대신, binlog2sql과 같은 도구를 사용하면 더욱 효율적으로 복구 작업을 수행할 수 있습니다. 이 도구는 binlog를 직접 파싱하여 DDL과 DML 쿼리문을 생성해주므로, 수작업을 최소화할 수 있습니다.

  3. Point-in-Time Recovery 활용

    mysqlbinlog --start-datetime="2023-10-01 00:00:00" --stop-datetime="2023-10-01 23:59:59" \
    binlog.000015 binlog.000016 | mysql -u <계정> -p<비밀번호> -h <데이터베이스 호스트> <DB명>
  4. MySQL의 Point-in-Time Recovery 기능을 활용하면 특정 시점까지의 데이터를 복구할 수 있습니다. 이를 통해 binlog를 사용하여 원하는 시점까지 데이터를 복원할 수 있으며, 수동으로 SQL 문을 파싱하지 않아도 됩니다.


복구 진행 시 참고한 자료

  1. MySQL 공식 문서 - mysqlbinlog
  2. https://dev.mysql.com/doc/refman/8.0/en/mysqlbinlog.html
  3. Amazon RDS에서 MySQL 바이너리 로그 액세스하기
  4. https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_LogAccess.MySQL.Binarylog.html
  5. Percona 블로그 - binlog2sql을 활용한 포인트 인 타임 복구
  6. https://www.percona.com/blog/binlog2sql-binlog-to-raw-sql-conversion-and-point-in-time-recovery/

추가로 알아두면 좋은 점

  • binlog_format 설정 확인

    SHOW VARIABLES LIKE 'binlog_format';
  • MySQL의 binlog_format 설정에 따라 binlog에 기록되는 내용이 달라집니다. 일반적으로 복구를 위해서는 ROW 또는 MIXED 포맷이 더 유용합니다.

  • binlog 보관 기간 설정

    SET GLOBAL expire_logs_days = 7;
  • 데이터 복구를 대비하여 binlog의 보관 기간(expire_logs_days)을 적절히 설정하는 것이 중요합니다.

  • 정기적인 백업 수행

  • binlog를 이용한 복구는 최후의 수단일 수 있으므로, 정기적인 데이터베이스 백업을 통해 데이터 소실에 대비해야 합니다.


결론

이번 경험을 통해 binlog의 중요성과 이를 활용한 데이터 복구 방법에 대해 깊이 있게 이해하게 되었습니다. 향후에는 자동화 도구와 MySQL의 내장 기능을 적극 활용하여 더욱 효율적으로 데이터 복구를 진행할 수 있을 것으로 기대합니다.

인덱스(Index)란 무엇인가?

MySQL에서 인덱스는 데이터를 효율적으로 검색하기 위해 사용되는 자료구조입니다. 기본적으로 MySQL은 B-Tree 인덱스를 사용하며, 이는 데이터를 정렬된 상태로 저장하여 빠른 검색을 가능하게 합니다.

B-Tree 구조


B-Tree 인덱스 구조

  • 브랜치 노드: 각 노드는 여러 키와 자식 노드를 가지며, 데이터는 정렬된 형태로 저장됩니다.
  • 정렬된 데이터: 인덱스는 앞의 키를 기준으로 뒤의 키가 정렬되어 있습니다.
  • 빠른 검색: 로그 시간 복잡도로 데이터를 탐색할 수 있습니다.

카디널리티(Cardinality)란?

카디널리티는 데이터의 중복도를 나타내는 수치입니다.

  • 높은 카디널리티 (High Cardinality): 중복도가 낮은 데이터 (예: 주민등록번호, 핸드폰 번호).
  • 낮은 카디널리티 (Low Cardinality): 중복도가 높은 데이터 (예: 성별, 국가 코드).

카디널리티가 높을수록 인덱스의 효율성이 높아집니다.


인덱스 동작 방식

테이블에 인덱스를 설정할 때 고려해야 할 세 가지 경우가 있습니다.

1. 한 컬럼에 인덱스를 설정하는 경우

  • 데이터 중복도가 낮은 컬럼을 선택하여 인덱스를 설정합니다.
  • 높은 카디널리티를 가진 컬럼에 인덱스를 걸면 검색 성능이 향상됩니다.

2. 다중 컬럼에 각각 인덱스를 설정하는 경우

  • 여러 컬럼에 단일 컬럼 인덱스를 각각 설정합니다.
  • 쿼리 실행 시 MySQL은 Index Merge 최적화를 통해 인덱스 결과를 병합합니다.
  • 참고: MySQL Index Merge Optimization

3. 다중 컬럼 인덱스(Multiple-column Index)를 설정하는 경우

  • 여러 컬럼을 하나의 인덱스로 설정합니다.
  • 인덱스 설정 시 카디널리티가 높은 순서로 컬럼을 나열하는 것이 좋습니다.

카디널리티에 따른 인덱스 성능 비교

카디널리티 내림차순 (높은 → 낮은) 카디널리티 오름차순 (낮은 → 높은)
더 나은 성능 제공 상대적으로 낮은 성능 제공

3.1 다중 컬럼 인덱스 사용 조건

  • WHERE 절에서 =, >, <=, BETWEEN, IN 등의 연산자를 사용하는 경우 B-Tree 인덱스가 효율적으로 작동합니다.

    *"B-Tree 자료 구조는 WHERE 절에서 =, >, <=, BETWEEN, IN 등의 연산자에 대응하여 특정 값, 값의 집합 또는 값의 범위를 빠르게 찾을 수 있게 해줍니다."*

  • 조인 시 인덱스가 걸린 컬럼을 사용하면 성능이 향상됩니다.

    *"조인을 수행할 때 다른 테이블에서 행을 가져오기 위해 인덱스를 사용합니다. MySQL은 동일한 타입과 크기로 선언된 컬럼의 인덱스를 더 효율적으로 사용할 수 있습니다."*

  • 인덱스된 컬럼의 데이터 타입과 크기가 동일하면 더욱 효율적입니다.

    • 예를 들어, VARCHAR(10)CHAR(10)은 동일한 크기로 간주되지만, VARCHAR(10)CHAR(15)는 그렇지 않습니다.

인덱스된 컬럼의 크기와 성능

인덱스된 컬럼의 크기가 작을수록 성능이 향상됩니다. 아래는 데이터 타입별 크기와 성능을 비교한 예시입니다.

데이터 타입 크기 성능
BIGINT 8바이트 보통
UUID 16바이트 느림
VARCHAR(32) 최대 32바이트 빠름

풀 테이블 스캔을 피하는 방법

풀 테이블 스캔은 대량의 데이터를 처리할 때 성능 저하를 유발합니다. 이를 피하기 위해 다음을 고려해야 합니다.

  1. 적절한 인덱스 설정: 자주 조회되는 컬럼에 인덱스를 설정합니다.
  2. 쿼리 최적화: 불필요한 전체 테이블 검색을 피하도록 쿼리를 작성합니다.
  3. 통계 정보 최신화: ANALYZE TABLE 명령어를 사용하여 통계 정보를 업데이트합니다.

참고 자료:

Kafka에서의 Exactly Once Delivery 구현

메시지 시스템을 사용하다 보면 메시지의 전달 보장 방식에 대해 고려해야 합니다. 일반적으로 다음 세 가지 전달 방식이 있습니다.

  1. At Least Once Delivery: 메시지가 최소 한 번 전달됩니다. 중복 메시지가 발생할 수 있습니다.
  2. At Most Once Delivery: 메시지가 최대 한 번 전달됩니다. 메시지가 손실될 수 있습니다.
  3. Exactly Once Delivery: 메시지가 정확히 한 번 전달됩니다. 중복이나 손실이 없습니다.

Kafka를 사용할 때, 메시지의 정확한 전달을 보장하는 것은 중요한 이슈입니다. 특히 Consumer 측에서 메시지를 처리하는 과정에서 중복이나 손실 없이 메시지를 정확히 한 번씩 처리하도록 구현하는 방법을 알아보겠습니다.


Kafka의 기본 전달 보장 방식

Kafka는 기본적으로 At Least Once Delivery를 제공합니다. 이는 메시지가 최소 한 번 소비자에게 전달됨을 의미하며, 네트워크 장애나 처리 오류로 인해 중복 메시지가 발생할 수 있습니다.


Exactly Once Delivery를 위한 설정과 구현

1. Idempotent Producer 설정

Producer 측에서 동일한 메시지를 여러 번 전송하더라도 중복 없이 처리되도록 설정할 수 있습니다.

  • enable.idempotence 옵션을 true로 설정합니다.

    Properties props = new Properties();
    props.put("enable.idempotence", true);
  • 이 설정을 통해 프로듀서는 메시지 전송 시 고유한 PID(Producer ID)와 시퀀스 넘버를 사용하여 중복 전송을 방지합니다.

2. Transactions 활용

Kafka는 트랜잭션 기능을 통해 메시지의 원자성을 보장합니다.

  • 트랜잭션 초기화

    producer.initTransactions();
  • 트랜잭션 시작과 종료

    try {
        producer.beginTransaction();
        // 메시지 전송 로직
        producer.send(record);
        producer.commitTransaction();
    } catch (Exception e) {
        producer.abortTransaction();
    }

3. Consumer 측의 처리 보장

Consumer에서 Exactly Once Semantics(EOS)를 구현하기 위해서는 다음을 고려해야 합니다.

  • Kafka Streams API 사용

    Kafka Streams는 EOS를 기본적으로 지원하며, 상태 저장 프로세싱에 유용합니다.

  • 오프셋 커밋 관리

    수동으로 오프셋을 커밋하여 메시지 처리와 오프셋 관리의 원자성을 보장합니다.

    consumer.commitSync();
  • 데이터베이스 트랜잭션과 연계

    메시지 처리 결과를 데이터베이스에 저장할 때, Kafka 오프셋 커밋과 데이터베이스 트랜잭션을 연계하여 원자성을 확보합니다.

이미 현업에서 자주 사용하지만 따로 정리하지는 않아서 이번 기회에 공식 레퍼런스를 보며 내용을 정리해보았다.

스프링 트랜잭션 처리

스프링 공식 문서: 트랜잭션 관리

@Transactional 애노테이션은 인터페이스, 클래스, 또는 메서드에 트랜잭션의 의미를 부여하는 메타데이터이다. 예를 들어, "이 메서드가 호출될 때 새로운 읽기 전용 트랜잭션을 시작하고, 기존 트랜잭션은 일시 중지한다"는 의미를 가진다. @Transactional의 기본 설정은 다음과 같다:

  • 전파(Propagation): PROPAGATION_REQUIRED
  • 격리 수준(Isolation Level): ISOLATION_DEFAULT
  • 읽기-쓰기(Read-Write) 모드: 트랜잭션은 기본적으로 읽기-쓰기 모드이다.
  • 타임아웃(Timeout): 트랜잭션 타임아웃은 기본 트랜잭션 시스템의 기본값을 따르거나, 타임아웃을 지원하지 않을 경우 설정되지 않는다.
  • 롤백 규칙(Rollback Rules): RuntimeException이나 그 하위 클래스가 발생하면 롤백이 트리거되며, 체크드 예외는 롤백을 트리거하지 않는다.

스프링 트랜잭션 처리 중 예외가 발생했을 때, 아래의 옵션들을 명시적으로 사용하여 롤백 여부를 결정할 수 있다:

  • rollbackFor: 롤백을 실행시키는 예외 클래스 목록
  • rollbackForClassName: 롤백을 실행시키는 예외 클래스 이름들
  • noRollbackFor: 롤백을 실행시키지 않는 예외 클래스 목록
  • noRollbackForClassName: 롤백을 실행시키지 않는 예외 클래스 이름들

예를 들어, 특정 체크드 예외에 대해서도 롤백을 원한다면 rollbackFor 옵션을 사용하여 설정할 수 있다.

자바와 스프링 트랜잭션 처리에 대한 오해

구글에서 "자바 트랜잭션 처리"로 검색해 보면 Checked ExceptionUnchecked Exception 에 대한 비교와 함께, 언체크드 예외에 대해 롤백이 수행된다는 내용을 많이 볼 수 있다.

하지만 이는 정확하지 않다. 자바에서는 기본적으로 트랜잭션에 대한 메커니즘을 제공하지 않으므로, 체크드 예외이든 언체크드 예외이든 트랜잭션의 롤백은 프로그래머가 직접 관리해야 한다. 즉, 언체크드 예외에 롤백하는 메커니즘은 스프링 프레임워크를 사용할 때 적용되는 기본 설정이지, 다른 프레임워크나 순수 자바 SDK만을 사용하여 DB 처리를 할 경우에는 해당되지 않는다.

이러한 오해는 자바와 스프링을 동일시하는 데에서 비롯되며, 자바 개발자들 사이에서 흔히 발생하는 잘못된 인식이라고 생각한다.

추가 설명

  • 자바의 예외 처리: 자바는 예외를 체크드 예외와 언체크드 예외로 구분하지만, 이는 컴파일러가 예외 처리를 강제하는지 여부와 관련이 있다. 트랜잭션 롤백과는 직접적인 연관이 없다.
  • 트랜잭션 관리: 순수 자바에서는 Connection 객체를 사용하여 수동으로 트랜잭션을 관리하며, 예외 발생 시 롤백을 직접 호출해야 한다.
  • 스프링의 트랜잭션 관리: 스프링은 AOP를 활용하여 선언적인 트랜잭션 관리를 제공하며, 기본적으로 RuntimeException 발생 시 롤백을 수행한다. 필요에 따라 설정을 변경하여 체크드 예외에도 롤백이 가능하다.

하이버네이트는 내부적으로 두종류의 캐쉬를 지원하는데 하나는 First-level cache 이고 다른 한종류는 Second-level Cache 이다

 

First-level cache?

대부분의 ORM (Object Relational Mapping) 프레임워크가 지원하는것처럼 하이버네이트또한 일차적 캐쉬를 지원한다. 

일차캐쉬는 Hibernate 의 Session 단계에서 지원하는 캐쉬로 가장 비싼 연산작업중 하나인 데이터베이스와의 대화를 줄여주기 위해 존재한다. Session 안에서 동작하는 캐쉬이기 때문에 Session 이 종료되면 캐쉬도 같이 사라지게 된다.

 

Second-level cache?

First level cache 이 세션 단계에서의 캐쉬라면 second level cache 는 session factory 단계에서 지원하는 캐쉬로 session factory 에서 생성되는 session 간에 공유가 된다. 

 

 

표로 다시 정리하면

  First Level  Second Level
범위 Session  Session Factory (all sessions) 
기본 활성화 O X
설정 따른 설정 필요없음 설정필요
캐쉬 백엔드에 따라 다른 설정 추가 필요

 

동시성 캐쉬 전략

이름  
READ_ONLY 읽기에 대한 캐쉬를 생성한다. 설정값 같이 어플리케이션이 시작되고 변화하지 않는 값에 대해 사용
NONSTRICT_READ_WRITE READ_WRITE 와 비슷하지만 때때로 데이터를 읽는 경우에 최신의 데이터를 가지고 오지 않을수도 있늠점에 유의해햐한다. 
어플리케이션이 같은 데이터에 대해 접근하는 일이 많이 없고 강한 트랜잭션 격리가 필요없는 경우 사용
READ_WRITE 읽기와 쓰기에 대해 캐쉬를 생성한다. Seriazliable 트랜잭션 격리수준은 적용되지 않는다
TRANSACTIONAL
트랜잭션 대한 캐쉬를 지원한다. Seriazliable 트랜잭션 격리.
TA Transaction Provider 와 같이 사용하여 분산 트랙잭션을 사용하는 경우 사용하면 좋을 거 같다 

 

Entity 에 대해 캐쉬 사용할때 샘플코드

@Entity(name = "Company")
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public static class Company {

}

 

참조: 

1. https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#caching

'Java > JPA' 카테고리의 다른 글

JPA Cascade Types  (0) 2021.12.19

JPA를 사용해서 엔티티간의 관계를 설정할때 아래와 어노테이션을 작성하는 일이 많은데 어떤 의미를 가지고 있는지 알아보자

@OneToMany(cascade={CascadeType.REFRESH, CascadeType.MERGE}, fetch = FetchType.LAZY)

 

먼저 각각의 CaseType에 들어가기전에 영속성 컨텍스트와 JPA의 상태에 대한 선행지식이 필요하다

 

Persistence Conxtet (영속성 컨텍스트)

공식문서의 정의를 참조해보자 

 

EntityManager (Java(TM) EE 7 Specification APIs)

Interface used to interact with the persistence context. An EntityManager instance is associated with a persistence context. A persistence context is a set of entity instances in which for any persistent entity identity there is a unique entity instance. W

docs.oracle.com

A persistence context is a set of entity instances in which for any persistent entity identity there is a unique entity instance. Within the persistence context, the entity instances and their lifecycle are managed. The 
EntityManager
 API is used to create and remove persistent entity instances, to find entities by their primary key, and to query over entities.

어렵게 정의되어 있지만 간단하게 요약하자면 영속성 컨텍스트가 영속성 컨텍스트안에 있는 엔티티들의 변화를 추적하고 관리한다는 이야기다.

 

엔티티 객체의 상태

상태 설명
Transient 엔티티 객체가 데이터베이스에 아직 저장되기 전의 상태 
Persistent 엔티티 객체가 데이터베이스에 저장된 상태
Detached Persistent 상태였던  엔티티 객체가 더이상 영속성 컨텍스트에 속해있지 않는 상태

JPA의 동작들

동작 설명 특징
Save 하이버네이트 구현체에만 있는 기능 Persist 와 같은 기능.  Persist 는 생성된 ID 를 돌려주지 않으나 Save 는 돌려준다
Persist 엔티티를 영속성 컨텍스트에 포함시킨다 1. 엔티티가 Transient 상태라면 엔티티는 Persistent 상태가 된고 관련 동작들을 전파한다. (PERSIST, ALL로 설정된 경우)
2. 엔티티가 이미 Persistent 상태라면 엔티티에 직접적인 영향은 없다. 그러나 관련 동작들은 여전히 자식으로 전파된다
3. 엔티티가 Detached 상태라면 에러를 발생시킨다
Merge Persistent 상태의 엔티티 객체를 Deatched 상태의 객체의 값들로 업데이트한다  
Update 하이버네이트에만 존재하는 동작으로 Merge 와 같은 동작을 수행한다  
SaveOrUpdate 하이버네이트에만 존재하는 동작으로   

Cascade 종류

 

 

Hibernate ORM 5.6.3.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

JPA 표준에 사용하면 아래와 같이 6가지 Cascade Type 있다

종류 특징
ALL 아래에 기술된 모든 동장들을 자식 엔티티에게 전파한다
PERSIST JPA의 Persist 동작 (save, persist )을 부모 엔티티에서 자식 엔티티에게 전파된다
MERGE 부모 엔티티가 업데이트 될때 자식 엔티티들도 업데이트 된다
REMOVE 자식 엔티티들을 부모 엔티티 삭제시 동시에 삭제한다
REFRESH 데이터베이스로부터 데이터르 다시 읽어 들이는 refresh 동작을 부모에서 자식 엔티티로 전파
DETACH Detach 를 부모에서 자식 엔티티로 전파한다

하이버네이트에만 존재하는 CaseCade Type

종류 특징
REPLICATE Replicate 를 사용할떄 자식엔티티에게도 같은 동작을 전파한다.
**자동생성되는 ID 사용하지 않고 엔티티를 복제할 필요가 있을때 사용하면 좋다.
SAVE_UPDATE 하이버네이트 구현체의 save, update, saveOrUpdate 동작을 수행시에 자식 엔티티에게 같은 동작을전파한다
LOCK 이미 Detached 된 부모 엔티티 객체를 다시 영속성 객체에 추가시에 자식엔티티도 같이 추가된다

 

참조:

1. https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html

2. https://stackoverflow.com/questions/161224/what-are-the-differences-between-the-different-saving-methods-in-hibernate

'Java > JPA' 카테고리의 다른 글

JPA & Hibernate Cache  (0) 2022.01.19

어플리케이션을 구동하기 위해 사용되는 설정값들을 어플리케이션이 돌아가는 플랫폼안에서 보통 환경변수 혹은 파일안에 저장해두고 쓰는데 어플리케이션 설정값을 Zookeepr, AWS Secrets 과 같은 외부 서비스에서 불러들이거나 보안을 위해 설정값들을 메모리 안에서만 저장이 필요한 경우에 활용 가능한 방법이다.

Spring boot 에서 만 활용 가능한 방법

EnvironmentPostProcessor 를 활용

public class PriceCalculationEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, 
      SpringApplication application) {
        PropertySource<?> system = environment.getPropertySources()
          .get(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);

        Map<String, Object> prefixed = names.stream()
          .collect(Collectors.toMap(this::rename, system::getProperty));
        environment.getPropertySources()
          .addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new MapPropertySource("prefixer", prefixed));
    }

}

Spring boot 와 Spring boot를 사용하지 않는 환경에서 활용 가능한 방법

ApplicationContextInitializer 를 활용한 방법

먼저 아래와 같이 프로퍼티 값을 오버라이드 하는 코드를 작성해준다.

public class PropertyOverrideContextInitializer
  implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableWebApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MyConfigProps configProps = Binder.get(environment).bind("my-config", MyConfigProps);
        System.out.println(configProps.getHomekey());
    }
  }

여기서 네가지 방법으로 어플리케이션에 적용 가능하다

1. web.xml 안에 있는 contextInitializerClasses 에 추가 혹은 상응하는 Java Config (Spring MVC)

    <context-param>
        <param-name>contextInitializerClasses</param-name>
        <param-value>com.xxxx.PropertyOverrideContextInitializer</param-value>
    </context-param>

 

자바로 설정시에 

 

@ContextConfiguration(initializers = PropertyOverrideContextInitializer.class)
public class AppConfig {

}

스프링만 사용시 

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
MyAppContextInitializer initializer = new MyAppContextInitializer();
initializer.initialize( ctx );

ctx.register( com.my.classpath.StackOverflowConfiguration.class );
ctx.refresh()

JobLauncher launcher = context.getBean(JobLauncher.class);

 

2. META-INF/spring.factories 추가하여 자동 설정 추가 (Spring Boot) 

org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.context.PropertyOverrideContextInitializer

3. 어플리케이션 추가 실행코드에 추가코드에 추가하는 방법 (Spring boot)

application.addInitializers(PropertyOverrideContextInitializer.class);
application.run(args);

혹은

new SpringApplicationBuilder(YourApp.class)
    .initializers(PropertyOverrideContextInitializer.class);
    .run(args);

4. context.initializer.classes 프라퍼티 설정 (Spring boot)

context.initializer.classes=com.xxxx.PropertyOverrideContextInitializer

참조

  1. https://www.baeldung.com/spring-tests-override-properties
  2. https://stackoverflow.com/questions/35217354/how-to-add-custom-applicationcontextinitializer-to-a-spring-boot-application
  3. https://stackoverflow.com/questions/35048164/spring-applicationcontextinitializer-and-properties
  4. https://stackoverflow.com/questions/13288841/applicationcontextinitializer-in-a-non-web-spring-context

Kubernetes 개발 환경을 위한 Garden IO 소개

배경

최근 많은 기업들이 운영 환경을 Kubernetes로 이전하면서 개발자들은 새로운 과제에 직면하게 되었습니다. 특히 로컬 개발 환경에서 마이크로서비스 아키텍처를 효율적으로 개발하고 테스트하는 것이 주요 과제 중 하나입니다. 기존에는 Docker Compose를 통해 이러한 환경을 쉽게 구성할 수 있었지만, Kubernetes 환경으로 전환하면서 새로운 도구가 필요하게 되었습니다.

이러한 상황에서 Garden IO가 주목받고 있습니다. Garden IO는 Kubernetes와 클라우드를 위한 엔드투엔드 개발 및 테스트 플랫폼으로, 개발부터 테스트, CI까지 일관된 워크플로우를 제공합니다.

Garden IO란?

Garden IO는 다음과 같은 특징을 가진 개발 도구입니다:

  1. Kubernetes 네이티브 개발 환경 지원
  2. 서비스 간 의존성 관리
  3. 환경별 설정 템플릿 제공
  4. 빠른 피드백 루프 제공
  5. 통합 테스트 환경 지원

주요 기능

1. 서비스 의존성 관리

Garden IO의 가장 강력한 기능 중 하나는 서비스 간 의존성을 자동으로 관리해준다는 점입니다. 예를 들어:

  • 서비스 A가 서비스 B에 의존할 경우, Garden은 자동으로 B를 먼저 배포한 후 A를 배포합니다.
  • 의존성 그래프를 자동으로 생성하여 복잡한 마이크로서비스 아키텍처도 효율적으로 관리할 수 있습니다.

2. 설정 템플릿 기능

Docker Compose 사용자들이 익숙해 있는 환경 변수 관리와 설정 커스터마이징을 Garden IO에서도 동일하게 사용할 수 있습니다:

  • 환경별(개발, 스테이징, 운영) 설정 분리
  • 변수 치환 및 템플릿 기능
  • 설정 재사용성 극대화

설치 및 설정 방법

설치하기

Garden IO는 다양한 운영체제에서 쉽게 설치할 수 있습니다:

MacOS:

brew tap garden-io/garden
brew install garden-cli

Linux:

curl -sL https://get.garden.io/install.sh | bash

Windows:

choco install garden-cli

프록시 설정

방화벽 뒤에서 Garden을 실행할 경우, 프록시 설정이 필요할 수 있습니다:

export HTTP_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.example.com"

프로젝트 구성 예시

Garden IO 프로젝트는 garden.yml 파일을 통해 구성됩니다. 아래는 간단한 예시입니다:

kind: Project
name: my-project
environments:
  - name: local
    providers:
      - name: kubernetes
        context: docker-desktop

---
kind: Module
name: backend-service
type: container
services:
  - name: backend
    ports:
      - name: http
        containerPort: 8080
dependencies:
  - database-service

---
kind: Module
name: database-service
type: container
services:
  - name: database
    ports:
      - name: postgres
        containerPort: 5432

장점과 실제 사용 경험

Garden IO를 실제 개발 환경에 도입하면서 경험한 주요 장점들은 다음과 같습니다:

  1. 개발 환경의 일관성: 모든 개발자가 동일한 환경에서 작업할 수 있습니다.
  2. 빠른 피드백: 변경사항을 즉시 확인할 수 있어 개발 생산성이 향상됩니다.
  3. 쿠버네티스 친화적: 운영 환경과 유사한 환경에서 개발할 수 있습니다.
  4. 자동화된 의존성 관리: 복잡한 마이크로서비스 아키텍처도 효율적으로 관리할 수 있습니다.

결론

Garden IO는 Kubernetes 기반의 개발 환경에서 Docker Compose의 편리함을 그대로 가져오면서, 추가적인 기능들을 제공합니다. 특히 마이크로서비스 아키텍처를 개발할 때 서비스 간 의존성 관리와 환경 설정의 유연성을 제공하여 개발자의 생산성을 크게 향상시킬 수 있습니다.

향후 Kubernetes가 더욱 보편화되면서, Garden IO와 같은 도구의 중요성은 더욱 커질 것으로 예상됩니다. 특히 개발 환경과 운영 환경의 간극을 줄이고, 효율적인 개발 워크플로우를 구축하고자 하는 팀에게 훌륭한 선택이 될 것입니다.

사용자가 많아지고 트래픽이 많아질고 시스템 스케일링이 필요해지는 시점이 오면 데이터베이스 캐싱에 대한 고민을 하게된다.

이번 포스팅에서는 데이터베이스 캐싱 전략에 대해 알아보고자 한다

 

 

먼저 들어가기에 앞서 Cache Hit을 알아보자

 

Cache hit 이란?

찾으려는 데이터가 캐쉬에 저장되어 있어 데이터베이스를 거치지 하고 캐쉬에서 바로 데이터를 가져오는 형태.

Cache hit이 많이 발생할수록 데이터 베이스에 대한 부담이 줄어든다.

 

Cache-Asdie

일반 적인 형태의 캐쉬로 어플리케이션이 캐쉬업데이트 업데이트 담당.

캐쉬에 먼저 쿼리문을 보내고 캐쉬에 데이터가 없을시 데이터베이스에서 데이터를 가져온후 캐쉬에 데이터를 업데이트 한다

 

 

장점

1. 데이터베이스 데이터 모델과 캐쉬의 데이터 모델이 다르게 저장 가능

2. 캐쉬 서버가 다운되도 데이터베이스에서 데이터를 가져올수있다.

3. Lazy Loading 

 

단점:

1. 데이터와 캐쉬간의 데이터 불일치 발생 가능성. (Time to live 값을 설정하거나 캐쉬를 무효화하는 전략 수립 필요)

2. 어플리케이션이 캐쉬관리 담당

 

Read-Through (동기적 처리)

캐쉬에 데이터가 없으면 원본 데이터 소스에 접근해서 데이터를 가져오고 캐쉬에 저장

 

장점:

1. Lazy Loading 

2. 어플리케이션이 캐쉬를 관리하지 않음

 

단점:

1. 최초로 데이터를 요청한 사람의 응답속도가 느림.

 

 

Write-Through (동기적 처리)

캐쉬가 데이터베이스와 어플리케이션 사이에 위치하고 있고 캐쉬에 데이터를 저장하고 데이터베이스 이어서 바로 저장.

Read through 전략과 같이 이용되면 Cache Hit 확률을 높일수 있다.

 

장점:

1. Write-Behind 보다 데이터 유실 위험 적음

단점

1. 캐쉬와 데이터베이스에 동시에 저장하기때문에 느린 쓰기 속도

 

 

Write-Behind (비동기적 처리)

캐쉬에만 데데이터를 저장하고,  데이터 소스에는 별도의로 주기적으로 저장하는 방식

장점

1. Write-Through 보다 향상된 쓰기 속도

2. 데이터베이스에 대한 부담을 줄일

 

단점

1. Cache 가 다운되면 데이터 유실 위험

 

 

 

 

+ Recent posts