isolation level이란 트랜잭션에서 일관성이 없는 데이터를 어디까지 허용하는 수준

트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 Isolation Level이라고 합니다. 예를 들어, 한 사용자가 어떠한 데이터를 수정하고 있는 경우 다른 사용자들이 그 데이터에 접근하는 것을 차단함으로써 완전한 데이터만을 사용자들에게 제공하게 됩니다. 또한, 많은 사용자들의 수정 작업으로 인하여 통계 자료를 작성할 수 없는 사용자를 위하여 읽기 작업을 수행할 수 있도록 Isolation Level을 변경할 수 있습니다. 
Isolation Level을 조정하는 경우 동시성이 증가되는데 반해 데이터의 무결성에 문제가 발생할 수 있거나, 데이터의 무결성을 완벽하게 유지하는 데 반하여 동시성이 떨어질 수 있으므로 그 기능을 완벽하게 이해한 후에 사용해야 합니다.

 

@Transactional (isolation = Isolation.DEFAULT | READ_COMMITTED | READ_UNCOMMITTED | REPEATABLE_READ | SERIALIZABLE)

 

다음은 MySQL에서 지원하는 네 종류의 Transaction Isolation Level입니다.

READ UNCOMMITTED

  • COMMIT 되지 않은 데이터에 다른 트랜잭션에서 접근할수 있다.
  • INSERT, UPDATE, DELETE 후 COMMIT 이나 ROLLBACK에 상관없이 현재의 데이터를 읽어온다.
  • ROLLBACK이 될 데이터도 읽어올 수 있으므로 주의가 필요하다.
  • LOCK이 발생하지 않는다.

READ COMMIITED

  • Oracle DBMS의 경우 Default LEVEL이다.
  • COMMIT 된 데이터에 다른 트랜잭션에서 접근할 수 있다.
  • 구현 방식이 차이 때문에 Query를 수행한 시점의 데이터와 정확하게 일치하지 않을 수 있다.
  • LOCK이 발생하지 않는다.
  • MySQL에서 많은 양의 데이터를 복제하거나 이동할 때 이 LEVEL을 추천한다.

REPEATABLE READ

  • MySQL InnoDB의 경우 Default LEVEL이다.
  • SELECT시 현재 시점의 스냅샷을 만들고 스냅샷을 조회한다.
  • 동일 트랜잭션 내에서 일관성을 보장한다.
  • record lock과 gap lock이 발생한다.
  • CREATE SELECT, INSERT SELECT시 lock이 발생한다.

SERIALIZE

  • 가장 강력한 LEVEL이다.
  • SELECT 문에 사용하는 모든 테이블에 shared lock이 발생한다.

위의 Transaction Isolation Level 은 Read Uncommited 에서 Serializable  순으로 Concurrency 는 높아지고 속도는 느려진다. 따라서 이 둘의 균형을 잘 맞추는 것이 중요합니다.

Isolation Level  에 따라 나타나는 현상이 세가지가 있습니다.

  • Dirty Read
    • 어떤 트랜잭션에서 아직 실행이 끝난지 않은 다른 트랜잭션에 의한 변경 사항을 보게 되는 되는 경우를 dirty read 라고 합니다. 만약 원래 트랜잭션에서 그 변경 사항을 롤백하면 그 데이터를 읽은 트랜잭션은 dirty 데이터를 가지고 있다고 말합니다.
  • Repeatable Read
    • 어떤 트랜잭션에서 같은 질의를 사용했을 때 질의를 아무리 여러번 해도 그리고 다른 트랜잭션에서 아무리 여러 번 그 행을 변경해도 항상 같은 데이터만 읽어드리는 경우를 repeatable read 라고 합니다. 즉 repeatable read 가 요구되는 트랜잭션에서는 다른 트랜잭션에 의한 변경 사항을 볼 수가 없습니다. 그러한 변경 사항을 보고 싶으면 어플리케이션에서 트랜잭션을 새로 시작해야 합니다.
  • Phantom read
    • phantom read 는 다른 트랜잭션에 의한 변경 사항으로 인해 현재 사용 중인 트랜잭션의 WHERE 절의 조건에 맞는 새로운 행이 생길 수 있는 경우에 관한 것입니다. 예를 들어, 잔고가 $100 미만인 계좌를 모두 찾아내는 트랜잭션이 있고, 이 트랜잭션에서 그 데이터를 두 번 읽는다고 가정합시다. 처음 데이터를 읽어들이고 난 후에 다른 트랜잭션에서 잔고가 $0인 계좌를 새로 만들면 이 계좌도 잔고가 $100 이하라는 조건에 맞게 됩니다. 트랜잭션 격리 수준(Transaction Isolation Level)에서 phantom read 를 지원하면 새로운 “유령(phantom)”행이 나오지만 지원하지 않으면 새로 생긴 행을 볼 수 없습니다.
Isolation Level Dirty Read Nonrepeatable Read Phantom Read
READ UNCOMMITTED Permitted Permitted Permitted
READ COMMITTED Permitted Permitted
REPEATABLE READ Permitted
SERIALIZABLE

Tabel1. Ansi Isolation Levels

Mysql 의 InnoDB 스토리지 엔진의 기본 Isolation Level이 REPEATABLE-READ 이고 Oracle 은 READ-COMMITED 입니다. 각 DBMS별 isolation level 에 자세한 내용은 다음 링크에서 참조할 수 있습니다.

Oracle 은 READ-COMMITED 와 SERIALIZABLE 만 지원하며 나머지 두가지 isolation level  은 지원하지 않습니다.

isolation level 확인

mysql docker의 isolation level은 아래와 같습니다.

MySQL InnoDB의 Isolation LEVEL 문서

https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

 

'Database' 카테고리의 다른 글

샤딩과 파티셔닝 개념 정리  (0) 2018.10.12
DB 정규화  (0) 2014.12.10

먼저 중요 패키지 버전을 보면 아래와 같다.

  • springboot 2.1.5.RELEASE
    • spring-data 2.1.5.RELEASE
  • spring-core 5.1.7.RELEASE
  • hibernate-core 5.3.10.Final
  • mysql 5.7.21-log

entity는 아래와 같습니다.

@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "T_NOTIFICATION")
public class Notification {

    @EmbeddedId
    private NotificationId id;

    @CreatedDate
    @Column(name = "CREATE_DT", nullable = false, updatable = false, columnDefinition = "datetime(3)")
    private Instant createdDate;

    @LastModifiedDate
    @Column(name = "RECEIVER_RESPONSE_DT", nullable = true, updatable = false, columnDefinition = "datetime(3)")
    private Instant receiverResponseDate;

    @Column(name = "NOTI_REASON", nullable = false, length = 64)
    private String notiReason;

    @ColumnDefault("1")
    @Column(name = "SEND_COUNT", nullable = false, columnDefinition = "tinyint")
    private int sendCount;

    @Column(name = "REQUEST_USER", nullable = false, length = 36)
    private String requestUser;

    @Column(name = "SEND_MESSAGE", nullable = false, columnDefinition = "text")
    private String message;

    @Builder.Default
    @OneToMany(mappedBy = "notification", fetch = FetchType.EAGER, cascade = {CascadeType.ALL}, orphanRemoval = true)
    private Collection<NotificationMessage> notificationMessages = new ArrayList<NotificationMessage>();
}
@Embeddable
@AllArgsConstructor
@NoArgsConstructor
@Data
public class NotificationId implements Serializable {

    private static final long serialVersionUID = 1L;

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "NOTI_SEQ", unique = true, updatable = false, nullable = false)
    private BigInteger NotiSeq;

    @Column(name = "PROJECT_ID", nullable = false, length = 36)
    private String projectId;

    @Column(name = "RECEIVER_ID", nullable = false, length = 128)
    private String receiverId;

    @Column(name = "NOTI_GID", nullable = false, length = 128)
    private String notiId;

}
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "T_NOTIFICATION_MESSAGE")
public class NotificationMessage {

    @Id
    @Column(name = "MESSAGE_ID", nullable = false, length = 100)
    private String messageId;

    @ColumnDefault("SEND")
    @Column(name = "STATE", nullable = false, length = 10)
    private String state;

    @CreatedDate
    @Column(name = "SEND_DT", nullable = false, columnDefinition = "datetime(3)")
    private Instant sendDate;

    @Column(name = "END_DT", nullable = true, columnDefinition = "datetime(3)")
    private Instant endDate;

    @Column(name = "SEND_RESULT_CODE", nullable = true, columnDefinition = "smallint(5)")
    private Integer resultCode;

    @Column(name = "SEND_RESULT_REASON", nullable = true, length = 1024)
    private String resultReason;

    @Column(name = "SEND_RESULT_MESSAGE", nullable = true, columnDefinition = "text")
    private String resultMessage;

    @ManyToOne
    @JoinColumns(value = {
            @JoinColumn(name = "NOTI_SEQ", referencedColumnName = "NOTI_SEQ"),
            @JoinColumn(name = "PROJECT_ID", referencedColumnName = "PROJECT_ID"),
            @JoinColumn(name = "RECEIVER_ID", referencedColumnName = "RECEIVER_ID"),
            @JoinColumn(name = "NOTI_GID", referencedColumnName = "NOTI_GID"),
    }, foreignKey = @ForeignKey(name = "FK_T_NOTIFICATION_TO_T_NOTIFICATION_MESSAGE"))
    private Notification notification;
}

Notification class에 EmbeddedId를 설정하여 복합키를 사용하고 있고 NotificationId class에 정의된 복합키중에 notiSeq는 @GeneratedValue로 설정하여 auto increment를 사용하려고 하였습니다. 실제 코드에서는 

@Override
public Notification insertOrUpdatePushMessage(Message message) {
    Notification notification = convertMessageToNotification(message);
    return this.repository.saveAndFlush(notification);
}

Message를 받아서 Notification으로 변경해서 save()를 하죠. 그런데 문제는 save()하고 return된 Notification 객체에는 notiSeq가 Null이 들어오는 것이었습니다.

사실 hibernate의 batch를 사용하고 있어서 auto increment를 가져오는 타이밍이 persistant와 안 맞나 보다 하고 batch도 사용하지 않아 봤는데 실패!!! 그럼 flush를 안 해서 그런가??? 하고 위 코드처럼 saveAndFlush()로 바꿔도 실패!!! 멘붕 중에 아래 블로그를 찾았다.

https://kihoonkim.github.io/2017/01/27/JPA(Java%20ORM)/3.%20JPA-%EC%97%94%ED%8B%B0%ED%8B%B0%20%EB%A7%A4%ED%95%91/

 

(JPA - 3) 엔티티 매핑

앞에서 영속성 컨텍스트에 객체(엔티티)를 저장하는 것을 보았다.객체와 관계형 데이터베이스의 다른 개념을 어떻게 매핑하고 데이터가 반영되는지 알아보자. 객체(엔티티)를 영속성 컨테스트에 저장(persist) 후 트랜젝션이 커밋되는 시점에각 데이터베이스에 맞는 SQL을 생성하여 데이터베이스로 보내진다.객체를 어떻게 엔티티로 만들고 테이블과 매핑하는지 알아보

kihoonkim.github.io

두 방식 중 무엇을 사용하든 복합키에는 @GeneratedValue를 통해 기본키를 자동생성할 수 없다.
반드시 직접 할당 전략을 사용해야 된다.

오!!! 복합키는 안 되는 거구나.... 허무함과 안도감이 교차했다. 나중에 왜 그런지 hibernate문서를 좀 더 찾아봐야겠지만 일단 믿고 가기로 했다. 

결론은 복합키에 unique한 auto increment를 사용하는 게 모순이라고 판단해서 auto increment만 PK로 변경해서 진행 중이다.

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

N + 1 문제 해결 1  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04
JPA 기본 키 전략  (0) 2019.06.27

+ Recent posts