본문 바로가기

Spring

JPA와 Transaction

JPA

  • 자바에서 orm기술 표준으로 사용하는 인터페이스

 

orm

  • 자바 객체와 관계형 db의 데이터를 자동으로 매핑해주는 방법

 

Hibernate

  • 자바 언어를 위한 orm 프레임워크
  • JPA 인터페이스를 구현하고 내부적으로 JDBC API를 사용

 

Spring Data JPA

  • jpa를 더 쉽고 편하게 사용할 수 있게 도와주는 spring에서 제공되는 모듈
  • jpa를 한 단계 더 추상화 시킨 jparepository 인터페이스 제공
  • entitymaniger는 jpa에서 관리하는 것
  • @transactional은 스프링에서 관리하는 것

 

Raw JPA

 

 

Persistence Context

  • 엔티티 객체가 생성, 관리, 소멸 되기까지 데이터베이스와 상호작용하는 모든 과정
  • 엔티티를 JPA 영속성 컨텍스트에 영속화(persist) 하게 되면, 이후 JPA 영속성 컨텍스트에서 상태를 관리함

 

Entity Manager Factory

  • jpa에서 엔티티 메니저를 생성하고 관리하는 팩토리 객체
  • 애플리케이션 실행시 한번만 생성됨, 애플리케이션 전체에서 공유됨.
  • 여러 스트레드가 동시에 객체나 메서드에 접근할 때 데이터의 일관성을 보관할 수 있음
  • 엔티티 메시저 생성, 디비 연결, 트랜젝션 관리

Entity Manager

  • 디비와 상호작용하는 객체, 엔티티의 생명 주기를 관리, 여러 스레드가 동시에 접근할 때 일관성 보장 ㄴㄴ
  • 엔티티 객체의 라이프 사이클과 영속성 관리
  • 엔티티 객체에 대해서 디비 crud 연산 수행

 

OSIV(Open Session in View)

영속성 컨텍스트를 View 렌더링이 끝날 때까지 개방된 상태로 유지하는 방식

장점

  • 컨트롤러나 뷰에서도 지연 로딩 사용 가능

단점 : 디비 커넥션 점유

  • 컨트롤러와 뷰 렌더링이 끝날 때까지 디비 커넥션 유지
  • 커넥션 보유 시간이 길어져 서버 자원 많이 사용
  • 과도한 db 커넥션 사용으로 인한 성능 저하 가능성

OSIV를 비활성화(false) 하여 사용하는 것이 좋음

spring.jpa.open-in-view=false

 

 

Proxy

  • 프록시(Proxy): 실제 엔터티 객체 대신, 그 객체를 감싸고 있는 대리 객체
  • 실제 데이터는 데이터베이스에서 가져오지 않고, 필요한 시점에만 쿼리를 실행해 데이터를 조회하는 방식
  • 불필요한 쿼리의 발생을 방지
 

이게 바로 프록시 객체!

무비를 조회할 때 연관관계가 맺어진 감독들 객체를 lazy로 조회

감독들을 제외하고 조회하는 경우에 무비 안에 있는 배우는 프록시로 조회됨

-> 불필요한 DB 통신 미사용 및 DB 부하 감소

 

@Transactional을 사용할 때 private 접근제어자를 사용하면 안 됨

  • @transactional은 aop로 만들어져 있음
  • aop도 내부적으로 프록시를 사용하여 만들어져 있음
  • 프록시가 동작을 하려면 내부적으로 상속이든 구현체든 뭐가 되어서 접근을 해야하는데, 메서드가 private으로 되어 있으면 접근 못함

 

 

@Transactional

  • Spring에서 메서드 또는 클래스에 적용하여 트랜잭션을 선언적으로 관리하는 방법
  • 트랜잭션 범위에 대해 Proxy 패턴을 사용하여 트랜잭션 관리
  • 설정 가능한 옵션 제공
  • 종류 : propagation(기본), isolation, timeout, readOnly, rollbackFor, noRollbackFor
  • readOnly = true로 설정하여 성능상 이점을 가질 수 있음 -> 변경 감지(Dirty Checking)가 비활성화

예시 : rollbackFor - 해당 예외가 발생할 때만 롤백을 수행한다

@Transactional(rollbackFor = {IOException.class, SQLException.class})
    public void registerUser(User user) throws IOException, SQLException {
        userRepository.save(user);
        if (someCondition) {
            throw new IOException("IOException 발생!");  // 롤백
        }
        if (someOtherCondition) {
            throw new SQLException("SQLException 발생!");  // 롤백
        }
    }

 

 

Propagation

  • 트랜잭션이 다른 트랜잭션과 어떻게 상호작용할지를 결정하는 방식
  • 서로 다른 서비스에 있는 트랜잭션끼리 어떻게 관리되고 전파할것인지 결정하는 것
  • 종류
    • REQUIRED (기본값): 이미 진행 중인 트랜잭션이 있으면 그 트랜잭션을 사용하고, 없으면 새 트랜잭션을 시작
    • REQUIRES_NEW: 항상 새 트랜잭션을 시작하며, 진행 중인 트랜잭션은 잠시 중단
    • SUPPORTS: 트랜잭션이 이미 존재하면 그 트랜잭션 내에서 실행하고, 없으면 비트랜잭션 실행
    • MANDATORY: 현재 트랜잭션이 반드시 있어야 하며, 없으면 예외가 발생
    • NEVER: 트랜잭션 없이 실행되어야 하며, 트랜잭션이 존재하면 예외가 발생
    • NESTED: 진행 중인 트랜잭션 내부에 또 다른 트랜잭션을 중첩시켜 실행할 수 있으며, 이 경우 Savepoint를 이용

 

Propagation 예시 (REQUIRES_NEW)

현재 서로 다른 서비스에서 개별적으로 트랜잭션이 관리되고 있음

만약 changeUserRole에서 에러가 발생하면 사용자는 역할을 변경하지 못하고 로그도 찍히지 못할 것이다

기본값이 REQUIRED이기 때문에 saveLog와 changeUserRole은 같은 트랜잭션을 쓰기 때문이다!

@Transactional
    public void saveLog(Long userId, LocalDateTime requestTime) {
        Log log = new Log(userId, requestTime);
        logRepository.save(log);
    }
@Transactional
    public void changeUserRole(AuthUser authUser, long userId, UserRoleChangeRequest userRoleChangeRequest) {
        User requestUser = User.fromAuthUser(authUser);
        logService.saveLog(requestUser.getId(), LocalDateTime.now());
        User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        user.updateRole(UserRole.of(userRoleChangeRequest.getRole()));
    }

 

이때 changeUserRole에서 에러가 발생해도 saveLog는 그와 상관없이 독립적으로 수행되고 싶다면 Propagation 설정을 REQUIRES_NEW로 변경해주면 된다!

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(Long userId, LocalDateTime requestTime) {
    }