2차 캐시

vs 1차 캐시

  • 1차 캐시는 영속성 컨텍스트를 통해 이루어진다.
  • 1차 캐시는 하나의 트랜잭션이 시작하고 종료할 때 까지만 유효하다.
  • OSIV(Open Session In View)를 이용하면 클라이언트의 요청이 들어오고 해당 요청이 끝날때 까지 1차 캐시를 유효하게하지만 애플리케이션 관점에서 DB에 대한 접근 횟수를 줄이기에는 역부족이다.

2차 캐시에 대하여

  • 애플리케이션 레벨에서 공유되므로 공유 캐시(shared cache)라 부르기도 한다.
  • 애플리케이션 종료시 까지 캐시가 유지된다.
  • 1차 캐시에 원하는 Entity가 없을 때 2차 캐시를 뒤지고, 이 때에도 없다면 DB를 조회헤 2차 캐시에 해당 Entity를 저장한다. 그리고 해당 Entity에 대한 복사본을 1차 캐시로 반환해준다. 이는 여러 곳에서 같은 객체를 동시에 수정하는 문제를 방지해준다.
  • Persistenct Unit 범위의 캐시이다.
  • 데이터베이스 기본키를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체의 동일성(a==b)을 보장하지 않는다.

설정방법

  1. @Cacheable
    @Entity
    public class Member {
       @Id @GeneratedValue
       private Long id;
    
       ...
    }

    와 같이 @Cacheable 어노테이션을 사용해준다.

  2. <persistencce-unit name="2ndCache">
       <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
    </persistencce-unit>

    와 같이 persistence.xmlshared-cache-mode를 설정함으로써 persistence unit 단위 캐시를 적용해준다.

'Backend > SpringBoot-JPA' 카테고리의 다른 글

@RestController 어노테이션  (0) 2021.04.29
Optimistic/Pessimistic Lock  (1) 2021.04.19
@Transactional  (0) 2021.03.21

@RestController 어노테이션

  • Spring에서 Rest API/Web API를 개발하기 위해 사용하는 어노테이션
  • @Controller@ResponseBody를 포함한다.
  • MessageConvertor가 @RestController에서 다음과 같은 중요한 역할을 수행한다.
    • 외부에서 전달받은 JSON메서드를 내부에서 사용할 수 있는 객체로 변환
    • Controller가 리턴한 객체를 클라이언트에게 JSON으로 변환해 전달
    • @EnableWebMvc를 사용하면 기본으로 제공된다. 이는 Jackson 라이브러리를 추가할 경우 기본으로 설정되어 있다. 만약 Jackson 라이브러리를 추가해 주지 않은 경우 500 오류가 발생한다.
    • @ResponseBody는 자바 객체를 HTTP 요청의 body 내용으로 매핑하는 역할을 수행하며, @RequestBody는 HTTP 요청의 body 내용을 자바 객체로 매핑하는 역할을 수행한다.
@RestController
public class MemberRestController
{
    @RequestMapping(value="api/item/new", method = RequestMethod.POST)
    public Item item(@RequestBody ItemDto item) {
        Item savedItem = itemService.join(item); 
        return savedItem;
    }
}
  • Body 부분의 ItemDto 데이터를 파라미터로 받기위해 @RequestBody를 사용함.
  • @ResponseBody@RestController에 포함되어있어, savedItem이 Body에 맵핑되어 전달된다.

'Backend > SpringBoot-JPA' 카테고리의 다른 글

2차 캐시 - @Cacheable  (0) 2021.04.30
Optimistic/Pessimistic Lock  (1) 2021.04.19
@Transactional  (0) 2021.03.21

LOCK

OPTIMISTIC LOCK

  • 트랜잭션의 대부분은 충돌이 발생하지 않는다는 것을 가정하고 접근하는 방법으로 트랜잭션이 커밋하기 전까지는 충돌 여부를 알 수 없다.
  • JPA에서 OPTIMISTIC LOCK을 @Version을 사용하므로 Entity 정의 시 @Version이 명시된 필드를 추가하도록 하자. 또한, @Version이 정의되어 있다면 OPTIMISTIC LOCK을 사용할 것을 명시하지 않더라도 DEFAULT 적용 된다.
  • 이러한 OPTIMISTIC LOCK은 COMMIT 시점에 충돌을 알 수 있으며 충돌 시 다음과 같은 Exception이 발생한다.
    • javax.persistence.OptimisticLockException in JPA
    • org.hibernate.StaleObjectStateException in Hibernate
    • org.springframework.orm.ObjectOptimisticLockingFailureException in Spring

@Version

  • OPTIMISTIC LOCK 사용 시 @Version 어노테이션을 이용해 버전관리 기능을 추가해야 한다. 이는 Long, Integer, Short, Timestamp 타입에 적용 가능하다.
    • @Version
      private Long version;
  • 엔티티를 수정할 때 마다 버전이 하나씩 자동으로 증가하며, 수정 시 조회 시점의 버전과 수정 시점의 버전이 다르면 예외를 발생시킨다. 이를 통해 second lost update problem최초 커밋만 인정함으로써 방지할 수 있다. 다른 방법으로는 마지막 커밋만 인정, 충돌 내용 병합이 있으나 여기서 다룰 문제는 아니다.
    • tx-1> Member1 조회 (version 1)
    • tx-2> Member1 조회 (version 1)
    • tx-2> Member1 수정 (version 1->2)
    • tx-1> Member1 수정 (충돌 발생)
  • 엔티티 수정 시 다음과 같은 UPDATE 쿼리가 발생된다.
    • UPDATE MEMBER
      SET
         NAME=${NEW_NAME},
         VERSION=${OLD_VERSION + 1} 
      WHERE
         ID=${ID}
         AND VERSION=${OLD_VERSION}
    • 이러한 버전관리 필드는 JPA가 직접 관리하는데, 벌크 연산시에는 개발자가 관리해 주어야 한다.
    • UPDATE MEMBER m SET m.name = ${NEW_NAME}, m.version = m.version + 1

PESSIMISTIC LOCK

  • 트랜잭션의 충돌이 발생한다고 가정하고 우선 LOCK을 걸고 보는 방법이다.
  • SELECT FOR UPDATE, `` 등의 방법이 있다.
  • 데이터를 수정하는 즉시 충돌여부를 확인할 수 있으며, 다음과 같은 Exception이 발생한다.
    • javax.persistence.PessimisticLockException in JPA
    • org.springframework.dao.PessimisticLockingFailureException in Spring
  • PESSIMISTIC LOCK을 얻은 트랜잭션이 LOCK을 풀기 전까지 다른 트랜잭션들은 무한히 대기하게 되므로 다음과같이 timeout을 설정하는 것이 좋다.
    • Map<String, Object> properties = new HashMap<String, Object>();
      // timeout for 10 seconds
      properties.put("javax.persistence.lock.timeout", 10 * 1000);
      
      Member member = em.find(Member.class, id, LockModeType.PESSIMISTIC_WRITE, properties);
  • PessimisticLockScope를 통해 해당 Entity만 Lock을 걸지, 연관관계에 있는 Entity들도 Lock을 걸지 결정할 수 있다.
    • PessimisticLockScope.NORMAL: 해당 Entity만 Lock을 건다.
    • PessimisticLockScope.EXTENDED: 연관된 Entity들도 Lock이 걸린다.

JPA Lock

  • JPA 사용 시, READ COMMITTED + OPTIMISTIC LOCK을 조합하여 사용하는 것이 좋다.
  • Lock을 사용하는 대표적인 예는
    • Entity Manager: Member foundMember = em.find(Member.class, id, LockModeType.OPTIMISTIC);, em.lock(foundMember, LockModeType.OPTIMISTIC);, em.refresh()
    • Query: em.createQuery("select ~").setLockMode(LockModeTpye.OPTIMISTIC_INCREMENT);
    • @NamedQuery(name="lock", query="SELECT ~~", lockMode = OPTIMISTIC_INCREMENT)
      가 있다.

LockModeType의 종류

Lock Mode Type 설명
OPTIMISTIC LOCK OPTIMISTIC - OPTIMISTIC LOCK을 사용한다.
- @Version만 적용했을때에는 Entity 수정 시에만 버전을 확인하지만, 여기에서는 조회만 하여도 버전을 체크함으로써 트랜잭션이 종료될 때 까지 다른 트랜잭션에서 변경하지 않음을 보장해준다.
- 트랜잭션 커밋 시 버전정보를 조회(SELECT)해 Entity의 버전과 같은지 확인한다.
- DIRTY READ, NON-REPEATABLE READ를 방지한다.
OPTIMISTIC LOCK OPTIMISTIC_FORCE_INCREMENT - OPTIMISTIC LOCK과 함께 버전정보를 강제로 증가한다.
- Entity를 수정하지 않아도 트랜잭션 커밋 시 버전정보를 강제로 증가시킨다. 이 때, 조회시점의 버전과 다르면 Exception을 발생시킨다.
- Entity 수정 시 버전 UPDATE가 발생하는데, 트랜잭션 커밋 시점에도 버전정보가 증가하므로 2번의 버전 증가가 발생할 수 있다.
- 연관관계가 있는 Entity들에 대해 연관관계의 주인인 Entity의 변경이 발생하면 연관관계에 있는 Entity의 버전정보도 동일하게 변경된다. (Member 변경 시 Team 또한 Version이 같이 업데이트)
PESSIMISTIC LOCK PESSIMISTIC_READ - PESSIMISTIC LOCK, READ LOCK을 사용한다.
- 데이터를 읽기만하고 수정하지 않는 용도로 사용한다.
- shared lock으로 select ... for share와 같다.
PESSIMISTIC LOCK PESSIMISTIC_WRITE - PESSIMISTIC LOCK, WRITE LOCK을 사용한다.
- Lock이 걸린 row는 다른 트랜잭션이 수정할 수 없다.
- 내부적으로 select ... for update를 사용해 lock을 건다.
- exclusive lock
PESSIMISTIC LOCK PESSIMISTIC_FORCE_INCREMENT - PESSIMISTIC LOCK, 버전정보를 강제로 증가한다.
- PESSIMISTIC LOCK 중 유일하게 버전정보를 사용한다.
- for update nowait을 사용한다.
기타 NONE - LOCK을 사용하지 않는다.
- @Version필드가 있으면 OPTIMISTIC LOCK이 적용된다.
- Entity 수정 시, 버전 필드를 체크하면서 버전을 증가시킨다.

'Backend > SpringBoot-JPA' 카테고리의 다른 글

2차 캐시 - @Cacheable  (0) 2021.04.30
@RestController 어노테이션  (0) 2021.04.29
@Transactional  (0) 2021.03.21

@Transactional

: @Transactional 어노테이션을 사용하면 tx.begin(), tx.commit()을 자동으로 수행해주고, 예외 발생시 rollback처리를 자동으로 해 주는 편리한 기능이다.

public void register(Member member) {
    try {
      tx.begin();
      memberRepository.save(member);
      tx.commit();
    } catch (Exception e) {
      tx.rollback();
    }
}

와 같은 메소드는 @Transactional 어노테이션 사용 시

@Transactional
public void register(Member member) {
    memberRepository.save(member);
}

로 바꿀 수 있다.

 

내부 구현

: Spring은 프록시 객체를 사용해 위와 같이 tx.being()등의 코드를 삽입해 줍니다. 프록시 객체는 다음과 같이 생성되는데

public class MemberServiceProxy {
  private final MemberService memberService;
  private final TransactonManager manager = TransactionManager.getInstance();

  public MemberServiceProxy(MemberService memberService) {
    this.memberService = memberService;
  }

  public void register(Member member) {
    try {
      manager.begin();
      memberService.register(member);
      manager.commit();
    } catch (Exception e) {
      manager.rollback();
    }
  }
}

개발자가 정의한 메소드를 한번 감싼 뒤, 해당 프록시 메소드를 호출함으로써 이루어집니다.

여기서 발생할 수 있는 문제점과 같이 더욱 자세한 내용은 Spring @Transactional 사용시 주의해야할 점를 참고해 주세요.



사용할 수 있는 Options

 

Propagation

: @Transactional 어노테이션이 붙여진 메서드는 하나의 트랜잭션 안에서 진행될 수 있도록 만들어주는 역할을 한다. 그런데 트랜잭션 안에서 트랜잭션이 호출된다면 어떻게 처리할지를 결정하는것이 propagation 옵션이다. @Transactional(propagation = Propagation.NOT_SUPPORTED)와 같은 방식으로 사용할 수 있다.

옵션 설명
REQUIRED - 기본 옵션
- 부모 트랜잭션이 존재한다면 부모 트랜잭션에 합류, 그렇지 않다면 새로운 트랜잭션을 만든다.
- 중간에 자식/부모에서 rollback이 발생된다면 자식과 부모 모두 rollback 한다.
REQUIRES_NEW - 무조건 새로운 트랜잭션을 만든다.
- nested한 방식으로 메소드 호출이 이루어지더라도 rollback은 각각 이루어 진다.
MANDATORY - 무조건 부모 트랜잭션에 합류시킨다.
- 부모 트랜잭션이 존재하지 않는다면 예외를 발생시킨다.
NESTED - 부모 트랜잭션이 존재하면 부모 트랜잭션에 중첩시키고, 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성한다.
- 부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 rollback한다.
- 자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 rollback하지 않는다. 이때 롤백은 부모 트랜잭션에서 자식 트랜잭션을 호출하는 지점까지만 롤백된다. 이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit 된다.
NEVER - 트랜잭션을 생성하지 않는다. 만약 부모 트랜잭션이 존재한다면 예외를 발생시킨다.


 

Isolation

: 격리수준을 정하는 옵션으로, 트랜잭션 내에서 일관성 없는 데이터를 허용하도록 하는 수준을 정한다. Spring에서는 기본적으로 READ_COMMITED를 사용하는데 Persistence Context를 사용함으로써 REPEATABLE_READ가 보장된다. 다음과 같이 @Transactional(isolation = Isolation.READ_COMMITTED) 사용할 수 있다.

옵션 설명
READ_UNCOMMITED - 트랜잭션에서 commit되지않은 데이터를 다른 트랜잭션에서 읽는 것을 허요한다.
- Dirty Read가 발생한다.
READ_COMMITED - 트랜잭션에서 commit되어 확정된 데이터만을 읽는 것을 허용한다.
REPEATABLE_READ - 한 트랜잭션 내에서 수차례 SELECT를 수행하더라도 동일한 값이 읽혀지는 것을 보장한다.
- Phantom read(새로운 row가 추가/삭제되어 새로 입력된 데이터를 읽는/사라지는 현상)는 여전히 발생 가능하다.
SERIALIZABLE - 모든 작업을 하나의 트랜잭션에 처리하는 것과 같은 높은 고립수준을 제공하는데, 이로인해 동시성 처리 효율은 매우 떨어진다.

여기서 발생되는 Row Lock과 같은 내용은 Lock으로 이해하는 Transaction의 Isolation Level을 참고해 주세요.



rollbackFor

  • 특정 예외가 발생할 경우 rollback하는 것을 명시하는 옵션이다. Spring은 기본적으로 RuntimeException과 Error를 롤백 정책으로 가진다.
  • 사용은 @Transactional(rollbackFor = {RuntimeException.class, Error.class}) 과 같이 할 수 있다. 만약 모든 예외에 대해 rollback하고싶다면 Exception.class를 추가하면 된다.
  • 이와 반대로 특정 Exception에 rollback하고 싶지 않다면 noRollbackFor 옵션을 사용하자.


Read-only

  • 트랜잭션을 읽기 전용으로 설정할 수 있다.
  • 이 옵션이 적용된 트랜잭션은 UPDATE, INSERT, DELETE등과같은 쓰기 작업이 진행되면 예외를 발생시킨다.
  • 이 옵션이 적용되지 않은 트랜잭션은 영속성 컨텍스트에 스냅샷을 만들어두고 트랜잭션이 끝날 때 스냅샷과 Entity의 값을 비교해 쓰기지연 SQL 저장소에 UPDATE 쿼리를 생성해 DB에 flush하는 작업을 거치는데, 이러한 과정이 사라지게 되어 효율적인 작업이 수행될 수 있다.
  • @Transactional(readOnly=true)로 사용할 수 있다.

timeout

  • 지정한 시간 내에 해당 메소드가 수행 완료되지 않으면 rollback을 수행한다.
  • @Transactional(timeout=10)와 같이 사용가능하다. (단위는 sec) (default -1로 no-timeout)

'Backend > SpringBoot-JPA' 카테고리의 다른 글

2차 캐시 - @Cacheable  (0) 2021.04.30
@RestController 어노테이션  (0) 2021.04.29
Optimistic/Pessimistic Lock  (1) 2021.04.19

+ Recent posts