본문 바로가기

개발기술/데이터베이스

DB 트랜잭션

 

트랜잭션 정의

트랜잭션 : 데이터 베이스의 상태를 변화시키기 위해 수행하는 작업단위. (DB에 쿼리문을 통해서 CRUD를 하는 행위)

작업단위 :  여기서 작업단위는 작업단위의 세부 작업들을 묶음 정의하기 나름임, Spring에서 @Transacitional으로 표기한다.

 

트랜잭션의 필요성

  • 데이터 일관성 문제: 여러 작업이 하나의 트랜잭션으로 묶이지 않으면, 일부 작업만 완료되고 다른 작업은 실패할 수 있습니다. 이는 데이터의 일관성을 해칠 수 있습니다.
  • 데이터 무결성 문제: 트랜잭션 없이 수행된 작업은 롤백이 불가능합니다. 이는 애플리케이션의 데이터 무결성을 보장하는 데 큰 문제가 될 수 있습니다.
  • 동시성 문제: 트랜잭션은 동시에 접근하는 여러 트랜잭션 간의 충돌을 방지하고, 격리 수준을 통해 데이터의 일관성을 유지합니다.

 

오늘 일기작성하기에도 세부적으로 (1) 오늘의 날씨데이터를 가져오고 (2) 일기를 DB에 저장하기로 나눌수 있음.

여기서 1번이나 2번 중 하나라도 문제가 생긴다면 전체 작업단위를 롤백할 수 있음. 날씨데이터가 있어야 DB에 저장할 수 있기 때문임. 

그러므로 일기작성이라는 작업을 하려면 (1)과 (2)로 이루어진 트랜잭션으로 구성하고 DB가 더러워지지않도록 온전성을 보존할 수 있음. 

일부의 로직이 잘못되더라도 DB가 온전히 보존될것을 알기때문에 편하게 작업을 짤수있음.

 

트랜잭션의 결과는 커밋과 롤백 두가지가 존재하면 이는 별도의 단계임. 쿼리를 실행해도 커밋을 실행해야 수정사항을 반영하는 것, 롤백은 트랜잭션을 수행하다가 예외상황이 발생하면 트랜잭션 전체를 원복시키는 것을 롤백이라고 한다. 

 

JDBC의 트랜잭션 관리

  1. 자동 커밋 모드: 기본적으로, JDBC는 자동 커밋 모드로 동작합니다. 이 모드에서는 각 SQL 문이 실행된 후 자동으로 커밋됩니다.
  2. 수동 커밋 모드: 트랜잭션을 명시적으로 관리하려면 자동 커밋 모드를 비활성화하고 commit() 및 rollback() 메서드를 사용하여 커밋이나 롤백을 수동으로 수행합니다.

JPA의 트랜잭션 관리

 

  • 엔티티 변경: 엔티티 객체를 변경하면 JPA는 이를 자동으로 감지합니다(Dirty Checking).
  • 트랜잭션 커밋: 트랜잭션이 커밋될 때 변경 사항이 데이터베이스에 반영됩니다.
  • 트랜잭션이 설정되지 않은 경우, 데이터 변경이 즉시 데이터베이스에 반영되지 않으며, 엔티티 매니저의 flush 메서드를 명시적으로 호출해야 변경 사항이 데이터베이스에 반영됩니다.

 

  •  

 

 

 

ACID

Atomic (원자성)

  • All or nothing 모든 작업이 실행되거나 혹은 모두 실행되지 않아야한다.
  • 예시) A계좌에서 B계좌로 잔액을 송금할때, A계좌의 송금성공은 B계좌의 수금성공과 함께 성공하거나 함께 실패해야한다. A계좌송금에서 B수금과정에서 뻑나면 모든게 롤 백이되고, 모든 과정이 성공해야만 DB에 반영되게 하는 것.

Consitency (일관성)

  • 트랜잭션 작업의 결과는 항상 일관적이어야한다. 어떤 작업을 했을때 어떤 때는 A의 결과가 나오고 어떤때는 B의 결과가 나와서는 안된다. 모든 트랜잭션이 종료될 시에는 DB의 제약조건을 모두 지키고 있는 상태가 되어야한다.
  • 예시) 잔액은 0원이상이다.
    • 이를 위반하는 트랜잭션은 모두 중단된다.

Isolation(독립성, 격리성)

  • 프로그램 내에는 여러가지 트랜잭션이 존재하지만 트랜잭션은 다른 트랜잭션과 독립적으로 동작해야한다.
  • A 트랜잭션이 하는 일은 B 트랜잭션과 동시에 진행되지 않아야한다.
  • 그러나, 실무적으로는 격리성과 성능은 트레이드 오프 관계에 있는 부분이다.
    • READ_UNCOMMITED>READ_COMMITED>REPEATABLE_READ>SERIALIZABLE 순서로 성능은 떨어지고 격리성(고립성)은 증가한다
      • READ_UNCOMMITED : 한 트랜잭션이 커밋하지도 않았는데 다른 트랜잭션이 데이터를 조회할 수 있음
      • READ_COMMITED : 한 트랜잭션이 끝나면 그것을 읽을 수 있으나, 나의 트랜잭션이 길이가 길면 초반부 트랜잭션과 마지막 부분의 트랜잭션에서 데이터의 값이 달라지는 문제가있다.
      • REPEATABLE_READ : 한 트랜잭션이 시작하는 순간 해당 데이터에 대해서 snapshot을 찍고, 트랜잭션에 의해서 데이터가 변경되더라도 데이터 변경이 없는 것처럼 snapshot의 데이터를 보여준다.
      • SERIALIZABLE : DB가 한번에 한가지 트랜잭션만 실행하여 100% 독립성
  • 일반적으로 MYSQL InnoDB의 기본 값인 REPEATABLE_READ를 많이 활용한다.
    • MySQL: Repeatable Read, PostgreSQL: Read Committed, SQL Server:Read Committed, Oracle:Read Committed, SQLite:Serializable

Durablity(지속성)

  • 트랜잭션이 성공적으로 되어, DB상태를 변화시킨다면 그 변화가 영구적으로 지속되어야한다. commit을 하면 지속(저장)이 꼭 된다.
  • DB 저장이 실패하더라도 모든 로그를 모두 남겨서 결국에는 DB에 순차적으로 모두 반영이 되도록 한다.

 

5. Entity State Management

entities retrieved from the database are in a managed state. Any changes made to these entities within a transaction are automatically persisted when the transaction is committed, without needing to explicitly call save() 

 

Persistence Context and the EntityManager

The EntityManager is the primary JPA interface for interacting with the persistence context. The persistence context acts as a cache of entities; any entity fetched from the database within a session (i.e., a transaction) is added to this context. Once an entity is in the persistence context, any changes made to it are tracked automatically.

Automatic Dirty Checking

One of the key features of the persistence context is dirty checking. This means that at the end of a transaction, the JPA provider will automatically check all managed entities for any changes. If changes are detected, the persistence provider will generate the necessary SQL UPDATE statements to synchronize these changes with the database. This process happens at transaction commit:

 

Entity States

JPA defines several states that an entity can be in:

  1. New (Transient): The entity has been instantiated but is not yet associated with a persistence context. It is not being tracked by any EntityManager and has no representation in the database.
  2.  Managed (Persistent, Dirty Checking주의): The entity is associated with a persistence context, meaning it is being tracked by an EntityManager. Any changes made to the entity while it is in this state will be automatically persisted to the database upon committing the transaction. This is the state referred to when discussing automatic synchronization with the database.
    • Without @Transaction, it automatically update DB because of dirty checking behavior. but with the use of @Transactional annotations (or managing transactions manually) can influence when these updates actually occur, effectively postponing the update operations 
    • When you commit the transaction, JPA performs a dirty check. It compares the current state of the entity with the snapshot taken when it was first read into the persistence context. Since it detects changes, it generates and executes an SQL UPDATE statement to persist those changes to the database.
  3. Detached: The entity was previously managed, but has been detached from the persistence context. Changes to a detached entity are not automatically synchronized with the database.
    • EntityManager.clear(), EntityManager.detach(Object entity), Closing the EntityManager , 

 

Handling Concurrency with Snapshots

In a concurrent environment, the snapshot mechanism plays a crucial role in maintaining data consistency:

  • Isolation and Consistency: The snapshot ensures that each transaction works with a consistent view of an entity. Changes made by one transaction are not visible to other transactions until they are committed, depending on the isolation level of the database.
  • Detecting Concurrency Issues: If two transactions attempt to modify the same entity concurrently, the last transaction to commit may overwrite changes made by the first, leading to a potential lost update problem. Some JPA providers and databases can handle this using optimistic or pessimistic locking mechanisms:
    • Optimistic Locking: Typically uses a version field (@Version) in the entity. Each time an entity is updated, its version number is incremented. If a transaction tries to save an entity with an outdated version number, a OptimisticLockException is thrown.
    • Pessimistic Locking: Involves locking the entity in the database for the duration of the transaction to prevent other transactions from accessing it simultaneously, thus preventing concurrent modifications.

Practical Implications

  • Transaction Management: Using @Transactional or manual transaction boundaries allows you to control when changes within a transaction are committed and thus when dirty checking and synchronization occur. This control is crucial in high-concurrency environments to ensure that business processes are completed without interference from other transactions.
  • Entity Detachment: When an entity is detached, either explicitly through methods like EntityManager.detach() or implicitly by closing the EntityManager, it no longer participates in the persistence context, and its changes will not be automatically saved to the database. This can be useful for carrying an entity across different transactional contexts without risking unwanted automatic database writes.
  • EntityManager Methods: Methods like EntityManager.clear() and EntityManager.detach(Object entity) help manage which entities are being tracked by the current persistence context. clear() removes all entities from the context, effectively resetting it, while detach() removes a specific entity.

 

Snapshot Basics in JPA

When an entity is loaded into the persistence context, JPA takes a snapshot of its state — this snapshot consists of the values of the entity's attributes at the time it was loaded from the database. The snapshot serves as a reference point for the persistence context to identify any changes made to the entity during the transaction.

Role of Snapshots

  1. Dirty Checking:
    • What is Dirty Checking? This is the process JPA uses to determine whether an entity has been changed while it was managed in the persistence context. Just before the transaction is committed, JPA compares the current state of each entity in the persistence context with the snapshot taken when the entity was loaded or last updated.
    • How it Works: If there are differences between the current entity state and the snapshot, JPA concludes that the entity has been modified and needs to be updated in the database. Consequently, JPA generates the appropriate SQL UPDATE statements to synchronize the database with the changes made to the entity.
  2. Concurrency Control:
    • Optimistic Locking: Often used for concurrency control in JPA. This approach typically utilizes a version column in the entity (@Version). Each time the entity is updated, its version number is incremented.
    • Snapshot's Role in Optimistic Locking: When an entity is first read, its version number is also recorded along with its state as part of the snapshot. Upon attempting to commit changes, JPA checks if the entity’s version number in the database matches the one in the snapshot. If they don't match, it means another transaction has concurrently modified the entity, leading to an OptimisticLockException.

 

트랜잭션과 경쟁문제


* 여러 트랜잭션이 경쟁한다면 어떤 문제가 생길 수 있고 그리고 그 문제를 어떻게 해결해본적이 있는지? 면접에서 종종 받고는 함. 여러 트랜잭션이 동시에 동작한다면 어떻게 설계해야하는지 고민해야함. 한 트랜잭션이 DB를 수정하려고하는데 트랜잭션이 완료되기 전에 다른 트랜잭션이 동일 DB를 조회하려고 경쟁한다면 여러가지 문제가 발생할수 있음. 특히, 이는 다수의 사용자가 동시에 접근했을때 문제가 발생할 수 있는 문제임.

 

1. Dirty Read : A트랜잭션이 데이터를 바꾸고 커밋하기전에 B 트렌젝션이 데이터를 조회하여 바뀐 데이터를 가져갔는데 A트랜잭션이 커밋하기전에 롤백이 발생하였으면 B트랜젝션은 존재하지 않던 데이터를 가져가는 것이다. A트랜잭션이 수정하고 있는데 그 데이터를 B트렌잭션이 접근할 수있도록 열려있었기에 발생한 문제임. 

 

- repeatable read로 해결가능 : change uncommited until transaction finished

 

2. Non-repeatable Read : A트랜잭션이 길이가 길고 조회를 2번한다고 했을때, B트랜잭션이 A의 트랜잭션이 1번 조회가 된상태에서 데이터를 바꿔버리면 A트랜잭션 입장에서는 동일한 데이터가 동일하지않고 변해버린 상황이다. 이는 일관성을 해치는 문제다.(트랜잭션 작업의 결과는 일관적이어야한다)

 

- repeatable read로 해결가능 : use snapshot for same data query

 

3. Phantom Read : A트랜잭션이 특정범위의 데이터를 트랜잭션의 초반과 후반에 조회를 하고 그 사이에 트랜잭션 B가 그 값을 바꾼다면 A는 동일한 데이터를 조회하였으나 다른 값을 얻게 될 것임. non repeatable read는 특정값을 트랜잭션 사이에서 경쟁했을 때 생기는 문제이고, phantom read는 특정범위를 트랜잭션 사이에서 경쟁했을 때 발생하는 문제이고 둘다 일관성을 해치는 문제임.

 

- repeatable read로 해결불가능 : This level typically locks individual rows to prevent modifications but does not generally prevent the insertion or deletion of new rows that would affect the results of a query. because these locks typically apply only to the rows read or written to, not to potential new rows that match the query conditions.

 

 

 

스프링에서의 트랜잭션 적용 및 세부설정들

트랜잭션끼리 경쟁하는 일이 없도록 트랜잭션을 일렬로 세워서 순차적으로 처리하면 되지 않나라고 생각할 수 있으나, 그런 설정으로 한다면 성능이 매우 떨어지게 될것임. 성능과 안정성을 모두 만족시키는 것이 백엔드 개발자의 역할임. 

 

스프링에서 트랜잭션 

스프링에서 트랜잭션 처리를 할 수 있는 방법이 여러가지 있으나 일반적인 방법은 @Transactional 어노테이션을 사용하는 방법임. 우선, configuration class위에 @EnableTransactionManagement을 선언하여 TransactionManager클래스  클래스트랜잭션 기능이 적용된 프록시 객체 생성함. 그리고 클래스 혹은 메서드 위에 표기된 @Transactional 어노테이션을 스캔하여 메소드들을 트랜잭션 단위로 등록한다. 어노테이션 적용 우선순위는 method위의 어노테이션 > class위의 어노테이션 설정임. Platform Transaction Manager가 트랜잭션을 확인하고 전체 과정이 완료된 것을 확인하고 나서야 커밋함. 이 어노테이션을 통해서 트랜잭션 단위를 설정함.

 

스프링코드에 반영하기

특정 거래작업은 값이 잠깐 잘못표시되는 것을 절대 허용할 수 없는 상황이 있을 것이고, 특정 조회에서는 값이 잘못표시되는 것을 허용할 수 있는 상황이 있을것이다. 스프링의 트랜잭션 5가지 세부설정으로 얼마나 느슨하게 관리할 것인지 빡빡하게 관리할 것인지 설정할 수 있음.

 

 

1. Isolation(격리수준)

@Transactional(isolation = isolation.DEFAULT) : 지정한 메소드는 해당 격리수준으로 트랜잭션이 실행됨

 

트랜잭션에서 일관성이 없는 데이터를 허용하는 수준으로 위의 격리성에서 표기한 격리수준. 일반적으로 격리수준은 기본적으로 default로 건드리지 않고, 매우 중요한 정보에 대해서는 repeatable_read나 serializable로 엄격하게 관리하여 특별적용한다.

 

- DEFAULT : 사용하고 있는 DB의 기본 격리수준을 따르겠다

- READ_UNCOMMITED : 한 트랜잭션이 커밋하지도 않았는데 다른 트랜잭션이 데이터를 조회할 수 있음 (Dirty Read발생)

- READ_COMMITED : 한 트랜잭션이 끝나면 그것을 읽을 수 있다.  나의 트랜잭션이 길이가 길면 초반부 트랜잭션과 마지막 부분의 트랜잭션에서 데이터의 값이 달라지는 문제가있다.(Dirty Read 방지)

- REPEATABLE_READ : 한 트랜잭션이 완료될때까지, 특정 영역을 shared lock을 걸어서 다른 사용자는 해당하는 영역을 조회할 수 없음.  시작하는 순간 해당 데이터에 대해서 snapshot을 찍고, 트랜잭션에 의해서 데이터가 변경되더라도 데이터 변경이 없는 것처럼 snapshot의 데이터를 보여준다. (Non-repeateable read방지)

- SERIALIZABLE : 트랜잭션이 사용하고 있는 영역에 모두 shared lock을 사용해서 다른 트랜잭션이 해당영역에 접근할 수 없음. DB가 한번에 한가지 트랜잭션만 실행하여 100% 독립성 (Phantom Read 방지)

 

2. Propagation (전파수준) 

@Transactional(

트랜잭션 동작 도중 다른 트잭션을 호출 하는 상황. A트랜잭션 중 B트랜잭션을 호출하는 경우, B트랜잭션을 어떻게 취급할 것인가에 대한 상황이다. A는 부모, B는 자식이라고 칭하자. 

 

- REQUIRED : default값으로 설정됨. 부모 트랜잭션 안에서 자식 트랜잭션을 연계하여 하나로 포함하여 여긴다. 부모 트랜잭션이 존재하지 않으면 자식은 독단적으로 트랜잭션을 생성한다.

 

- SUPPORTS : 부모 트랜잭션 내에서 자식 트랜잭션이 호출되면 하나의 트랜잭션으로 인식되지만, 부모 트랜잭션이 없는 상태에서는 호출되면 트랜잭션을 별도로 만들지 않는다.

 

- REQUIRES_NEW : 부모 트랜잭션 안에서 자식 트랜잭션이 별도로 생성된다. 서로 트랜잭션간 영향을 받지 않는다.

 

- NESTED : 부모 트랜잭션이 있는 경우, 자식 트랜잭션을 중첩트랜잭션으로 생성한다. 이는, 부모트랜잭션은 자식트랜잭션에 종속되지 않지만, 자식 트랜잭션은 부모트랜잭션에 종속되도록 만든다.

 

ex) 사용자 일기 작성 관련해서 프로그램 로그를 DB에 저장하는 상황이라면, 프로그램 로그가 실패한다고 사용자 일기 작성이 롤백이 되면 안될것. 그러나, 사용자 일기 작성이 실패하여 롤백할 경우에는 로그 DB도 롤백해야한다.

 

3. readOnly속성

 

트랜잭션을 읽기 전용 속성으로 지정. 이는 성능을 최적화하기 위해서 사용하거나 트랜잭션 기능에 제약을 걸어두기 위해 사용함. 트랜잭션을 읽기 전용속성으로 지정하면 성능이 빨라진다. 그리고 트랜잭션 내에서 읽기 외에는 작업을 해서는 안되는 작업의 경우 제약을 걸어둠.

 

@Transactional(readOnly=true)

 

4. 트랜잭션 롤백 예외

@Transactional(rollbackFor =Exception.class)

@Transactional(norollbackFor=Exception.class)

특정 예외가 발생했을때에만 트랜잭션 롤백시킬 경우를 설정. default로 runtime exception, error 발생할때 롤백을 하도록 

 

4. Timeout속성

@Transactional(timeout=10)

 

일정 시간 내에서 트랜잭션 끝내지 못하면 롤백시킴.격리 수준이 빡빡해서 해당 작업이 문제가 생겨서 오랬동안 막혀있다면, 다른 트랜잭션도 해당 데이터를 사용하지 못하고 대기하고 있어서 

 

그 외 : 데이터베이스 상에서도 트랜잭션 상 세부설정을 할 수 있음.

 

 

 

LazyLoading and @Transactional

  • Lazy loading : a design pattern commonly used in programming where the initialization of a resource is delayed until it is actually needed. In the case of JPA (Java Persistence API) or Hibernate, lazy loading is used to defer the loading of associated entities from the database until they are specifically accessed in your application. This is particularly useful for improving the performance of your application by avoiding unnecessary database queries.
  • Role of @Transactional in Relation to Lazy Loading: @Transactional ensures that there is an open session or persistence context during the execution of the method. If there is no active session (e.g., if the method is not transactional), accessing a lazily-loaded field might result in an exception (commonly LazyInitializationException). a database session does not actively "wait" until lazy-loaded data is accessed. 
    • database session : It refers to a temporary, interactive connection between an application and a database. During this session, the application can execute queries, fetch data, update records, and perform other transaction-related activities.
      • Isolation and Consistency
      • Transactional Scope
      • Connection Pooling:
      • Entity Lifecycle:
      • Persistence Context: