포스트

2022-11-08-TIL

2022-11-08-TIL

Today I Learned

Spring Actuator

Spring Boot Actuator는 운영 중인 애플리케이션의 상태를 확인하는 엔드포인트를 제공한다. health(상태), info, metrics, env, loggers(로그 레벨 조회·변경), threaddump, heapdump 등이 있다. 보안을 위해 HTTP로는 기본적으로 health만 노출되고, 나머지는 management.endpoints.web.exposure.include로 열어야 한다. JMX로는 shutdown을 뺀 대부분이 기본 노출된다(Spring Boot: Actuator).

1
2
3
4
5
6
7
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  server:
    port: 9001   # 관리용 포트를 서비스 포트와 분리

env나 heapdump는 설정값과 메모리 내용을 그대로 보여 주므로, 열어야 한다면 관리 포트를 내부망에만 두거나 Spring Security로 보호한다. Kubernetes 환경에서는 health 아래의 liveness, readiness 그룹이 프로브 대상으로 쓰인다.

  • https://velog.io/@ha0kim/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9D%B8-%EC%95%A1%EC%85%98-16.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EC%95%A1%EC%B6%94%EC%97%90%EC%9D%B4%ED%84%B0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
  • https://supawer0728.github.io/2018/05/12/spring-actuator/

Transaction Isolation Levels

MySQL InnoDB는 SQL 표준의 네 가지 격리 수준을 모두 지원하고 기본값은 REPEATABLE READ다(MySQL 8.0: Transaction Isolation Levels).

격리 수준일반 SELECT가 보는 것비고
READ UNCOMMITTED커밋되지 않은 변경까지dirty read 가능
READ COMMITTED각 SELECT 시점에 커밋된 데이터같은 트랜잭션에서도 SELECT마다 새 스냅샷
REPEATABLE READ트랜잭션의 첫 읽기 시점 스냅샷일반 SELECT끼리는 결과가 일관됨
SERIALIZABLEREPEATABLE READ와 같되autocommit이 꺼져 있으면 일반 SELECT를 SELECT ... FOR SHARE로 바꿈

문서에서 놓치기 쉬운 점은 잠금 읽기(SELECT ... FOR UPDATE, UPDATE, DELETE)는 스냅샷이 아니라 최신 데이터를 대상으로 한다는 것이다. REPEATABLE READ에서 범위 조건으로 잠금 읽기를 하면 갭 락이나 넥스트 키 락으로 그 범위에 대한 삽입을 막는다. 문서는 같은 REPEATABLE READ 트랜잭션 안에서 잠금 문장과 일반 SELECT를 섞지 말라고 권하는데, 둘이 서로 다른 상태의 테이블을 읽기 때문이다.

  • https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html#isolevel_repeatable-read

JPA Propagation and Isolation Level

Spring @Transactional의 propagation과 isolation 속성은 서로 다른 축이다. 전파 속성은 이미 트랜잭션이 있을 때 참여할지 새로 만들지를 정하고, 격리 수준은 DB 트랜잭션끼리 얼마나 서로를 보지 않을지를 정한다.

  • REQUIRED(기본값): 기존 트랜잭션에 참여하고 없으면 새로 만든다. 같은 물리 트랜잭션에 참여한 안쪽 메서드가 롤백 전용으로 표시하면, 바깥에서 커밋할 때 UnexpectedRollbackException이 난다.
  • REQUIRES_NEW: 항상 독립된 물리 트랜잭션을 연다. 바깥 트랜잭션이 커넥션을 쥔 채로 커넥션을 하나 더 쓰므로, 커넥션 풀이 동시 스레드 수에 비해 작으면 고갈될 수 있다.
  • NESTED: 하나의 물리 트랜잭션 안에서 JDBC savepoint로 부분 롤백한다. JDBC 기반 트랜잭션 매니저에서만 동작한다.

중요한 점은 격리 수준, 타임아웃, readOnly 설정이 새 트랜잭션이 시작될 때만 적용된다는 것이다. REQUIRED로 기존 트랜잭션에 참여한 안쪽 메서드에 다른 격리 수준을 적어도 기본적으로는 조용히 무시되고 바깥 설정을 따른다. 이런 불일치를 거부하게 하려면 트랜잭션 매니저의 validateExistingTransaction을 켠다(Spring Framework: Transaction Propagation).

  • https://devocean.sk.com/blog/techBoardDetail.do?ID=163799
  • https://labs.brandi.co.kr/2019/06/19/hansj.html

JPA Optimistic lock vs Pessimistic lock

두 사용자가 같은 행을 읽고 각자 수정해서 저장하면, 나중에 저장한 쪽이 먼저 저장한 변경을 덮어쓰는 lost update가 생긴다. Vlad Mihalcea의 글은 이를 다루는 두 전략을 충돌을 “막을 것인가, 감지할 것인가”로 구분한다.

비관적 락은 충돌을 미리 막는다. 읽을 때 행에 락을 걸어 다른 트랜잭션이 수정하지 못하게 한다. JPA에서는 @Lock(LockModeType.PESSIMISTIC_WRITE)로 조회하면 SELECT ... FOR UPDATE가 나간다. 확실하지만 락을 기다리는 동안 처리량이 떨어지고, 여러 행을 다른 순서로 잠그면 데드락이 생길 수 있다.

낙관적 락은 충돌을 허용하고 커밋 시점에 감지한다. 엔티티에 @Version 필드를 두면 JPA가 UPDATE 조건에 버전을 넣고 값을 올린다. 그 사이 다른 트랜잭션이 먼저 수정해서 버전이 바뀌었으면 갱신된 행이 0개가 되고 OptimisticLockException이 난다. 버전 필드는 엔티티당 하나이고 int, long, short(및 래퍼 타입)나 java.sql.Timestamp 타입이어야 하며, 값은 애플리케이션이 직접 바꾸지 않는다(Baeldung: Optimistic Locking in JPA).

1
2
3
4
5
6
7
8
9
10
@Entity
public class Stock {
    @Id @GeneratedValue
    private Long id;
    private int quantity;

    @Version
    private Long version;
}
// UPDATE stock SET quantity = ?, version = 6 WHERE id = ? AND version = 5

선택 기준은 충돌 빈도와 재시도 비용이다. 충돌이 드물고 재시도가 싸면 낙관적 락이, 충돌이 잦거나(인기 상품 재고 차감) 재시도가 비싸면 비관적 락이 낫다. 낙관적 락을 쓰면 예외를 잡아 재시도하거나 사용자에게 알리는 처리가 함께 필요하다.

  • https://velog.io/@sgwon1996/JPA-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD
  • https://jaehoney.tistory.com/159
  • https://www.baeldung.com/jpa-optimistic-locking
  • https://vladmihalcea.com/optimistic-vs-pessimistic-locking/
참고한 자료외부 출처 5

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다