Transaction - READ UNCOMMITTED, Dirty Read 테스트

salgu·2022년 10월 1일
0

db

목록 보기
1/1
post-thumbnail

트랜잭션의 격리 수준(Isolation level)


트랜잭션의 격리 수준(Isolation level)이란 동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것입니다.

격리 수준은 다음과 같이 4가지로 정의할 수 있습니다.

  • READ UNCOMMITTED(커밋되지 않은 읽기)
  • READ COMMITTED(커밋된 읽기)
  • REPEATABLE READ(반복 가능한 읽기)
  • SERIALIZABLE(직렬화 가능)

트랜잭션의 격리 수준에 따라 발생하는 문제점


reference : https://zzang9ha.tistory.com/381

테스트 설명


Spring Boot, JPA로 테스트를 진행할 것이고
테스트 시나리오는 이름을 변경하는 로직이 존재하고 이름이 변경될 때
일정시간에는 이름이 임시로 temp로 변경되었다가
일정 시간이 지나면 새로 바뀔 이름으로 변경되는 시나리오입니다.

ex) "이상규" -> "temp" -> "김형준" (이름 변경 transaction)
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ↑
ㅤㅤㅤDirty Read 시도, temp가 나오면 성공

구현


Member.java

@Entity
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
}
  • name 정보만 가지고있는 간단한 Member Class 입니다.

MemberService.java

	/**
     * 이름을 변경하는데 n초가 걸리고
     * 변경되는 과정에선 temp 란 임시 이름으로 존재합니다.
     * 
     * 핵심 로직을 잘보여주기위해 threadSleep 로직을 따로 빼주었습니다.
     */
    @Transactional
    public Member changeNameWait(Long id, String newName) {
        Member member = memberRepository.findById(id)
                .orElseThrow(IllegalArgumentException::new);
        member.setName("temp");
        memberRepository.flush();

        threadSleep(1000);

        member.setName(newName);
        memberRepository.flush();
        return member;
    }
  • 이름을 바꿀 해당 member를 조회한 뒤 이름을 temp로 변경시키고 flush해주어 update를 해줍니다.
  • temp로 변경되어 조회할 시간을 벌기 위해 thread를 n초 만큼 sleep 시켜줍니다.
    sleep 되었을때 dirty read를 시도합니다.
  • 그 후 새로운 이름으로 이름을 다시 변경해줍니다.
	@Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public Member findByMemberId(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(IllegalAccessError::new);
    }
  • 이름을 변경하는 transaction에 dirty read를 시도할 메소드입니다.
  • @Transactional 어노테이션 옵션에 Isolation.READ_UNCOMMITTED 해주면 다른 로직을 조회할 때 DB에 commit 되지않은 데이터를 가져오게 됩니다.

TrasactionTest.java

	static private String name = "이상규";
    static private String newName = "김형준";

	@Test
    void changeNameWaitDirtyReadTest() throws ExecutionException, InterruptedException {
        final Member member = memberService.saveMember(name);
        assertThat(member.getName()).isEqualTo(name);

        CompletableFuture<Void> 이름변경 = CompletableFuture.runAsync(() -> {
            memberService.changeWaitName(member.getId(), newName);
        });

        CompletableFuture<Member> 중간조회 = CompletableFuture.supplyAsync(()-> {
            threadSleep(500);
            return memberService.findByMemberId(member.getId());
        });
        Member result = 중간조회.get();
        assertThat(result.getName()).isEqualTo("temp");

        CompletableFuture.allOf(이름변경, 중간조회).join();
    }
  • 테스트를 위해 이름을 변경할 객체를 저장해줍니다.
  • 병렬 처리를 하기 위해 CompletableFuture를 사용하여 Thread를 분리시켜 "이름변경" 로직과 "중간조회" 로직을 실행시켜 줍니다.
    • "중간조회" 쓰레드에선 supplyAsync 사용한 이유는 Return값을 받아 조회된 값을 검증하기 위해 사용하였습니다.
  • "이름변경"과 "중간조회"가 비동기 처리되어 요청한 로직이 끝나기전에 테스트코드가 종료 되기 때문에 CompletableFuture.join()을 사용하여 "이름변경"과 "중간조회" 로직이 끝날때까지 기다려줍니다.

test log

Hibernate: select member0_.id as id1_0_0_, member0_.name as name2_0_0_ from member member0_ where member0_.id=?
2022-10-02 00:04:47.579 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-10-02 00:04:47.582 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name2_0_0_] : [VARCHAR]) - [이상규]
Hibernate: update member set name=? where id=?
2022-10-02 00:04:47.587 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [temp]
2022-10-02 00:04:47.587 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
Hibernate: select member0_.id as id1_0_0_, member0_.name as name2_0_0_ from member member0_ where member0_.id=?
2022-10-02 00:04:48.083 TRACE 90860 --- [onPool-worker-5] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-10-02 00:04:48.084 TRACE 90860 --- [onPool-worker-5] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name2_0_0_] : [VARCHAR]) - [temp]
Hibernate: update member set name=? where id=?
2022-10-02 00:04:48.590 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [김형준]
2022-10-02 00:04:48.590 TRACE 90860 --- [onPool-worker-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
  • 이름을 변경하는 changeWaitName 메소드에 transactional을 걸었음에도 불구하고 READ UNCOMMITTED 격리 수준을 가진 트랜잭션이 조회를 하면 commit되지 않은 temp가 조회되고 테스트가 성공합니다.

코드 : https://github.com/salgu1998/Transaction-Isolation-Level-Test

profile
https://github.com/leeeesanggyu, leeeesanggyu@gmail.com

0개의 댓글