개요

본 포스팅의 DB환경은 MySQL 5.7.30 입니다. 테스트를 원하신다면 AUTO_COMMIT을 false로 하고 진행해주세요.

트랜잭션 격리 수준이란 ?

  • 동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할 지 말지를 결정한다.

트랜잭션 격리 수준의 종류

  • READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 등으로 나뉜다.

왜 트랜잭션 격리 수준을 알아야 하는 가?

  • 동시성 문제가 있다. 데이터 부정합 문제점이 있는데, 이는 트랜잭션 격리 수준에 따라 달라진다. 아래 그림은 트랜잭션 격리 수준에 따른 데이터 부정합 표이다. 각각의 데이터 부정합의 대한 설명은 아래에서 다시 다룰 것이다.

테스트를 위한 준비

  • AUTO_COMMIT을 false로 설정한다.
  • 아래와 같이 간단한 테이블을 만들고, 데이터를 집어넣자.
create table account (
    account_id int AUTO_INCREMENT,
    login_name varchar(255) not null,
    primary key(account_id)
)charset='utf8';

insert into account(login_name)
values ('eunmo');
  • 트랜잭션 격리 수준을 설정하는 SQL과 확인하는 SQL은 다음과 같다.

    • ex ) READ UNCOMMITTED 로 설정하고 싶을 때.

      SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
      
      SHOW VARIABLES LIKE 'TX_ISOLATION';
      
    • 결과

주의!!! 테스트 할 때는 서로 다른 세션에서 수행해야한다! 현재 세션 확인하는 법 : select connection_id();

트랜잭션 격리 수준의 종류

READ UNCOMMITED

  • 각 트랜잭션에서의 변경 내용이 COMMIT이나 ROLLBACK 여부에 상관없이 다른 트랜잭션에서 보여진다. 실험을 해보자.

    1번 세션

    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    
    UPDATE ACCOUNT
    SET LOGIN_NAME = 'GRACE LOVE'
    WHERE ACCOUNT_ID = 1;
    

    2번 세션

    begin;
    select * from account;
    

    결과

    문제

    • 결론적으로 1번 세션 트랜잭션에서 작업을 아직 완료하지 않았는데도 다른 트랜잭션이 이를 볼 수 있다.
    • 이렇게 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션이 볼 수 있게 되는 현상을 Dirty read라고 한다.

    결론

    • 1번 세션에서 UPDATE 를 하고 COMMIT 하지 않았는데도 2번 세션에서는 UPDATE한 'GRACE LOVE' 가 보인다.
    • READ UNCOMMITED는 이런 데이터 부정합을 초래하므로 권장하지 않는다. 최소한 READ COMMITED 를 사용하자.

READ COMMITED

  • 어떤 트랜잭션에서 데이터를 변경했더라도 COMMIT이 완료된 데이터만 다른 트랜잭션에서 볼 수 있다.

  • 오라클에서 기본적으로 사용되는 격리수준이다.

    1번 세션

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    
    UPDATE ACCOUNT
    SET LOGIN_NAME = 'GRACE LOVE'
    WHERE ACCOUNT_ID = 1;

    2번 세션

    begin;
    select * from account;
    

    결과 - COMMIT 실행 이전

    결과2 - COMMIT 실행 이후

    • 설명한 대로 COMMIT을 하지 않았기 때문에 UPDATE 한 값인 'GRACE LOVE' 가 다른 트랜잭션에선 보이지 않는다.
    • 1번 세션에서 COMMIT을 수행한 뒤 다시 2번 세션에서 조회해보자.

    문제

    • Dirty Read와 같은 부정합 문제는 발생하지 않는다.
    • 하지만 Non-Repeatable Read문제가 발생한다.
    • Non-Repeatable Read란 하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때 항상 같은 결과를 가져와야하는 Repeatable Read 정합성에 어긋나는 것이다.

    결론

    • SELECT 쿼리 결과는 ACCOUNT 테이블에서 가져온 것이 아니라, UNDO 영역에 백업된 컬럼 값을 가져오는 것이다.

REPEATABLE READ

  • InnoDB 스토리지 엔진의 기본 전략이다.
  • READ COMMITTED 와의 차이점은 UNDO 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가냐가 다르다.
  • 1번 세션이 UPDATE후 COMMIT 해도 다른 트랜잭션은 여전히 UNDO 영역을 바라보고있다.
  • PHANTOM READ가 발생할 수 있다고 하는데 InnoDB에선 발생하지 않는다 한다. 실제로 InnoDB 엔진에서 재현해보려했으나 실패했다.
    • PHANTOM READ ? 다른 트랜잭션에서 수행한 변경작업에 의해 레코드가 보였다 안보였다 하는 현상을 말한다.

SERIALIZABLE

  • 가장 엄격한 격리수준. 그만큼 동시처리성능도 가장 떨어진다. 어차피 InnoDB는 PHANTOM READ가 발생하지 않기 때문에 굳이 쓸 필요가 없다.

레퍼런스

Real MySQL - 이성욱 지음. 책 구매 링크 : http://www.yes24.com/Product/Goods/6960931

'DB' 카테고리의 다른 글

[ACID] Atomicity - 원자성  (0) 2024.02.28
[MySQL] InnoDB와 MyISAM 차이 (트랜잭션)  (0) 2020.07.07
UNION  (0) 2019.12.07
GROUP BY  (0) 2019.12.07

가장 큰 차이는 트랜잭션의 유무가 아닌가 싶다.
직접 테스트해보고 확인한 결과를 기록한다.

create table tab_myisam (
    fdpk int not null,
    primary key(fdpk)
)engine=MyISAM;


create table tab_innodb (
    fdpk int not null,
    primary key(fdpk)
)engine=InnoDB;


insert into tab_myisam (fdpk) values (3);
insert into tab_innodb (fdpk) values (3);
  • tab_myisam과 tab_innodb를 생성했다. 스토리지엔진은 이름에 따라 각각 MyISAM과 InnoDB로 설정해줬다.
  • 값을 넣어준다. 두 테이블 모두 fdpk 라는 컬럼에 3 이라는 값을 넣어줬다. 이제 실험!

insert into tab_myisam (fdpk) values(1), (2), (3);
insert into tab_innodb (fdpk) values(1), (2), (3);
  • fdpk에 1, 2, 3을 넣는다.
  • 우리가 예상하기로는 1, 2를 넣고 3을 넣는 순간 unique 제약 조건에 걸려서 모두 롤백되어야 한다.
  • myisam엔진을 쓰는 테이블인 tab_myisam을 select 해보면, (select * from tab_myisam)
    1,2,3 모두 값이 들어가있다. 트랜잭션을 지원하지 않기 때문!
  • 그에 반해 innodb 엔진을 쓰는 테이블인 tab_innodb를 select해보면, (select * from tab_innodb)
    처음에 값을 넣어준 3 외에는 없는 걸로 나온다. 트랜잭션을 지원하기 때문이다!

```

'DB' 카테고리의 다른 글

[ACID] Atomicity - 원자성  (0) 2024.02.28
트랜잭션 격리 수준(isolation level)  (0) 2020.07.14
UNION  (0) 2019.12.07
GROUP BY  (0) 2019.12.07

QueryDsl이란?

  • 오픈소스 프로젝트다.
  • 일반적으로 복잡한 Creteria를 대체하는 JPQL 빌더다.
  • JPA의 표준스펙이 아니므로 약간의 설정이 더 필요하다.
  • 복잡한 쿼리와 동적쿼리를 깔쌈하게 해결해준다.
  • 쿼리를 자바 코드로 작성할 수 있다. 따라서 문법오류를 컴파일단계에서 잡아줄 수 있다.

JPQL vs QueryDSL

JPQL

String username = "gracelove"
String query = "select m from Member m"+
 "where m.username = :username";

List<Member> result = em.createQuery(query, Member.class)
                            .getResult.List();
  • 이 코드에 문제가 있다. 쉽게 발견할 수 있는가?
    • String query에 가독성을 위해 개행을 줬다. 이걸 한 줄로 풀어보자.
    • select m from Member mwhere m.username = :username
    • 한 줄로 풀어보니, "...m where..."가 아니라 "...mwhere..."다.
    • 컴파일러가 잡아주지도 않는다. 런타임때 이 쿼리를 실행시키면 그제서야 에러가 난다.

QueryDSL

String username = "gracelove
List<Member> result = queryFactory
                            .select(member)
                            .from(member)
                            .where(member.username.eq(username))
                            .fetch();
  • 자바 코드기 때문에 ide의 도움을 받을 수 있다. 또 컴파일 단계에서 오류를 발견할 수 있다. 게다가 자바 코드다 보니, 메서드추출도 할 수 있어서 재사용성 또한 높아진다.

maven에서의 설정방법

pom.xml 의존성 추가

    <dependencies>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
    </dependencies>
  • 의존성을 위와 같이 추가한다.

pom.xml 빌드 플러그인 추가

<build>
  <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
  • 이렇게 설정한뒤 mvn compile 을 실행시키자. 그럼 @Entity를 붙인 클래스들이 Q타입으로 새롭게 생성된다. ex) Account -> QAccount
    • target 디렉토리 밑에 Q타입의 class가 생성된 걸 볼 수 있다.

사용하기.

  • 먼저 Entity 클래스들을 만들어보자.
  • Member 클래스와 Team 클래스를 만들 것이다.
  • 연관관계는 다 대 일이다. 양방향으로 건다. 연관관계의 주인 즉, fk를 갖고있는 건 '다' 쪽인 Member 클래스다.

Member.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "username", "age"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;


    public Member(String username) {
        this(username, 0, null);
    }


    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

Team.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "name"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

사용 코드

  • 먼저 EntityManager와 JPAQueryFactory를 필드로 두고, EntityManager는 빈을 주입받자.

  • 그 다음 JPAQueryFactory의 인스턴스를 생성하는데, 생성자 인자로 EntityManager를 주자.

    • JPAQueryFactory는 스레드 세이프 하기 때문에 빈으로 등록시켜서 싱글톤으로 이용해도 괜찮다.
  • 테스트를 해볼 데이터를 @BeforeEach 로 각 테스트 이전에 넣어준다.

    @SpringBootTest
    @Transactional
    class MemberTest {
    
      @PersistenceContext //@Autowired
      EntityManager em;
      JPAQueryFactory queryFactory;
    
      @BeforeEach
      public void testEntity() {
          queryFactory = new JPAQueryFactory(em);
    
          Team teamA = new Team("teamA");
          Team teamB = new Team("teamB");
          em.persist(teamA);
          em.persist(teamB);
    
          Member member1 = new Member("member1", 10, teamA);
          Member member2 = new Member("member2", 20, teamA);
    
          Member member3 = new Member("member3", 30, teamB);
          Member member4 = new Member("member4", 40, teamB);
          em.persist(member1);
          em.persist(member2);
          em.persist(member3);
          em.persist(member4);
      }
      ...
    }
  • 이렇게 한다면 디비에는 다음과 같이 값이 들어갈 것이다.

전체 소스 : https://github.com/gracelove91/querydsl-practice/blob/master/src/test/java/kr/gracelove/querydsl/entity/MemberTest.java

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

[JPA]open-session-in-view 를 알아보자  (1) 2020.07.01
@EntityGraph  (0) 2020.02.11
순수 JPA레포지토리의 페이징과 스프링 데이터 JPA레포지토리의 페이징.  (0) 2020.02.10
연관관계의 주인  (0) 2020.01.27
준영속 상태  (0) 2020.01.16

Open-In-View 또는 Open-Session-In-View 또는 Open-EntityManager-In-View 란?

  • 관례상 OSIV 라고 한다.
  • true일 경우 영속성 컨텍스트가 트랜잭션 범위를 넘어선 레이어까지 살아있다.
    • Api라면 클라이언트에게 응답될 때까지, View라면 View가 렌더링될 때까지 영속성컨텍스트가 살아있다.
  • false일 경우 트랜잭션을 종료할 때 영속성 컨텍스트 또한 닫힌다.
  • 기본값은 true 이다

코드

간단히 코드로 false와 true일 때 어떤 차이가 있는지 살펴보자.

  • Member.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

@ToString(of = "name")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @Builder
    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }


}
  • Team.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    private String teamName;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String teamName) {
        this.teamName = teamName;
    }
}

Member와 Team은 다 대 일 관계이며, 서로 양방향 연관관계를 맺었다.
Lazy 로딩이기 때문에 Member를 찾게된다면 Member 안의 Team은 프록시객체가 된다.
Team의 프로퍼티를 건드려야 비로소 Team 객체가 로딩된다.

  • Inin.class
/**
 * @author : Eunmo Hong
 * @since : 2020/07/01
 */

@RequiredArgsConstructor
@Component
public class Init implements ApplicationRunner {
    private final MemberRepository memberRepository;
    private final TeamRepository teamRepository;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Team teamA = teamRepository.save(new Team("teamA"));
        memberRepository.save(Member.builder()
                .name("member1")
                .team(teamA)
                .build());
    }
}

어플리케이션이 구동되면 실행되는 코드다.
이름이 "teamA" 인 Team 객체를 save 한다.
이름이 "member1" 이며 앞서 save한 Team에 소속된 Member를 Save 한다.
그렇다면 DB 상황은 다음과 같다.

  • MemberController.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

@RequiredArgsConstructor
@RestController
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/members/{name}")
    public MemberDto findMember(@PathVariable String name) {
        Member member = memberService.findByName(name);
        return MemberDto.builder()
                .name(member.getName())
                .teamName(member.getTeam().getTeamName())
                .build();
    }
}

간단한 컨트롤러다.
Query String 으로 들어온 String name 으로 해당하는 Member를 찾고,
MemberDto에 Member의 이름과, Member가 속한 Team의 이름을 Set 해주고 JSON으로 반환한다.
화면엔 이렇게 뜬다.

Controller의 return ...teamName(member.getTeam().getTeamName()) 코드에서
Proxy 객체인 Team의 프로퍼티인 TeamName 을 건드렸기 때문에 Lazy loading 이 됐다. 따라서 결과도 잘 나온다.

하지만 OSIVfalse 로 설정한다면?

  • application.yml
    spring:
    datasource:
      url: jdbc:h2:tcp://localhost/~/jpa-sample
      username: sa
      password:
    jpa:
      open-in-view: false

이와 같이 org.hibernate.LazyInitializationException: could not initialize proxy [kr.gracelove.osivdemo.domain.Team#1] - no Session 이라는 익셉션과 함께 500 에러가 뜨는 걸 볼 수 있다.

이는 영속성컨텍스트가 Transaction 범위 바깥인 Controller에서 Lazy loading 을 시도했기 때문이다.
영속성 컨텍스트가 닫혔다면 Lazy loading 또한 할 수 없다!

정리

  • OSIV가 true
    • 사용자에게 응답 또는 view가 렌더링 될 때까지 영속성컨텍스트를 유지한다.
  • OSIV가 false
    • Transaction이 끝나면 영속성컨텍스트 또한 닫힌다.

너무나도 당연하게 OSIV를 true로 두는 게 좋아보이지만! 단점이 존재한다.
영속성 컨텍스트를 유지한다는 건, DB Connection 또한 계속 가지고 있다는 뜻이다.
실시간 트래픽이 중요한 어플리케이션에서는 DB Connection이 모자를 수 있다. 성능이 중요하다면 OSIV는 false로 설정하자.

전체 코드는 다음 링크에서 볼 수 있다.

https://github.com/gracelove91/jpa-practice/tree/master/osiv-demo

레퍼런스

실전! 스프링 부트와 JPA활용 2편 - 김영한님
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

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

[QueryDSL] QueryDSL 사용법  (1) 2020.07.07
@EntityGraph  (0) 2020.02.11
순수 JPA레포지토리의 페이징과 스프링 데이터 JPA레포지토리의 페이징.  (0) 2020.02.10
연관관계의 주인  (0) 2020.01.27
준영속 상태  (0) 2020.01.16

+ Recent posts