2022 08 16
2022-08-16¶
Spring Security & Transaction¶
- 참고: https://velog.io/@jakeseo_me/JPA-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD-%EC%A0%95%EB%A6%AC
- 참고: https://ppomelo.tistory.com/147
- 문제 상황
- Security 단에서 Optional<User>를 findKlaytnAddress()로 트랜잭션을 열고 가져오게 되는데...
- 로그를 보아하니 가져는 오는데, 변경 감지를 하고 얘를 트랜잭션 끝나는 시점에 flush를 해주면했는데... 안한다?
- 어허... 엔티티 가져와서 넣어두면 변경 감지 해줘야하는거 국룰 아니야?
@Transactional도 붙여뒀는데 서비스 단에서... Controller로@AuthenticationPrincipal받아서 했는데 말이지? 엔티티 아닌가 이거?2022-08-10 20:24:58.324[TRACE] : No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByKlaytnAddress]: This method is not transactional. Hibernate: select user0_.id as id1_5_, user0_.is_active as is_activ2_5_, user0_.klaytn_address as klaytn_a3_5_, user0_.nickname as nickname4_5_, user0_.phone_number as phone_nu5_5_, user0_.privacy_agreement as privacy_6_5_ from user user0_ where user0_.klaytn_address=? 2022-08-10 20:24:58.342[TRACE] : binding parameter [1] as [VARCHAR] - [0x9f3c619b6f534d123c8fc35607d73eee0161da7b] 2022-08-10 20:24:58.418[TRACE] : Getting transaction for [com.backend.connectable.user.service.UserService.modifyUserByUserDetails] 2022-08-10 20:24:58.419[TRACE] : Completing transaction for [com.backend.connectable.user.service.UserService.modifyUserByUserDetails]
- 문제점
- Spring Security Context에 JPA엔티티를 들고 다니지 말 것! => 준영속 상태로 이어짐
- JPA 엔티티를 SecurityContext 내부에 있는
.contextHolder()에 들고 다님 .contextHolder()에 있는 JPA 엔티티는 아무리 수정해도, 디비나 영속성 컨텍스트에 반영이 되지 않음!- 영속성 컨텍스트에 있는 JPA 엔티티를 변화시켜도
SecurityContext.contextHolder()에 있는 JPA 엔티티는 영속상태가 아니기에 변화가 적용되지 않음! - 따라서 그냥 ID와 같은 문자열 정보만 저장하는게 좋아보임
- JPA 엔티티를 SecurityContext 내부에 있는
- Spring Security Context에 JPA엔티티를 들고 다니지 말 것! => 준영속 상태로 이어짐
- 준영속 상태
- 준영속 상태란?
- 영속 상태의 엔티티가 영속성 컨텍스트에서 "분리" 된 것
- 영속성 컨텍스트에 저장되었다가 분리된 상태로, 현재는 영속 상태가 아닌 상태
- 영속성 컨텍스트가 제공하는 기능을 사용하지 못 함 => 1차 캐시, 변경 감지 등
- 준영속 상태로 만드는 방법
- em.detach(entity) : 특정 엔티티 준영속으로 전환
- em.clear() : 영속성 컨텍스트 초기화
- em.close() : 영속성 컨텍스트 종료
- 준영속 엔티티를 수정하는 2가지 방법
- 변경 감지 기능 사용
- 다시 엔티티 조회하고, 데이터 수정
- 병합 사용
- em.merge(entity)
- 변경 감지 기능 사용
- 준영속 상태는 한번 영속화되었다가 트랜잭션 끝나서 영속성 컨텍스트가 더는 관리하지 않는 엔티티
- 준영속 상태란?
- 왜 Spring Security를 거치면 준영속 상태가 되는거야?
- 필터를 지나 mvc로 넘어가면 엔티티 매니저가 닫혀버리나?
엔티티의 생명주기¶
- 참고: https://tecoble.techcourse.co.kr/post/2020-08-31-entity-lifecycle-1/
- 참고: https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/
- 문제 상황
- 시큐리티로 부터 엔티티 받아서 일대다 연관관계에 있는 엔티티 조회하려니 LazyInitializationException
- 해결 방법 at 서비스단
- 엔티티 한번 더 불러오기 (준영속 => 영속)
- Entity 강제 초기화 (필요한 정보 UserDetails에서 다 끌고오기)
- FetchType.EAGER
- 그런데 말입니다
- Controller에서 준영속 상태인 entity가 지연로딩 불가한 상황
- 영속성 컨텍스트 Controller 살아있도록 하면 되는거 아닌가?
- 그런데 놀랍게도 OSIV는 true임...!
-
OSIV를 알아보기
- Spring Boot에서 자동으로 Bean 설정해주는 Configuration
- OpenEntityManagerInViewIinterceptor를 인터셉터로 등록하여 OSIV 등록 함
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(WebMvcConfigurer.class) @ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class }) @ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class) @ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true) protected static class JpaWebConfiguration { ... @Bean public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() { ... return new OpenEntityManagerInViewInterceptor(); } @Bean public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(OpenEntityManagerInViewInterceptor interceptor) { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addWebRequestInterceptor(interceptor); } }; } }
- OpenEntityManagerInViewIinterceptor를 인터셉터로 등록하여 OSIV 등록 함
- Spring에서 웹 요청 처리과정에서는 Filter를 제외한 대부분의 영역에서
@Transactional없어도 EntityManager의 관리를 받게 된다- 왜냐하면 OSIV는 디폴트 true이기 때문!
- Spring Boot에서 자동으로 Bean 설정해주는 Configuration
- Spring Security는 어떤 방식으로 유저 정보 가져오는데?
- Spring Security는 Filter 기반으로 동작
- DelegatingFilterProxy에서 사용자 요청 가로채 보안이 적용되게끔 함
- SecurityFilter가 존재해 일종의 FilterChain 이루어 동작
- CustomUserDetailsService => UserDetailsService를 구현
- 자 여기서!
- Spring Security에서 Filter단에서 User 정보 가져옴
- OSIV는 Interceptor 단에서 부터 적용됨
- Filter가 Interceptor 보다 먼저 실행되기에, 영속성 컨텍스트는 CustomUserDetailsService에서만 유지됨
- 이러니까 준영속 상태로 엔티티가 Spring MVC에 도착하는 것!
- 해결 방법
- OpenEntityManagerInterceptor의 우선 순위 높이기
- 이걸 Interceptor가 아니라 Filter로 교체하고, Bean으로 등록해서 뚝딱해결
- 근데 과연 OSIV를 Filter단까지 올리는게 좋은 방법일까?
- OpenEntityManagerInterceptor의 우선 순위 높이기
Transaction ReadOnly¶
- 참고: https://www.baeldung.com/spring-transactions-read-only
- 참고: https://willseungh0.tistory.com/75
- 트랜잭션이란?
- 하나 혹은 더 많은 statement를 가진 원자적 연산 => 한번에 성공하거나 모두 실패하거나
- ACID로 대표됨
- Isolation => 'performance' vs 'consistency'
- 언제 트랜잭션 쓰지?
- InnoDB 엔진에서는 모든 statement가 transaction
- 따라서 locking/snapshot 등을 포함할 수 있음
- InnoDB 엔진에서는 모든 statement가 transaction
- 어플리케이션 vs DB
- persistence layer를 다루려면 많은 추상화된 layer를 포함해야 함
- 각 레이어는 제각기 다른 책임이 있음
- DAO => Transcational abstraction => JPA abstraction => ORM Framework => JDBC Driver => RDBMS
- 트랜잭션 관리
- autocommit 속성을 꺼두고 JDBC 드라이버가 트랜잭션 시작함
- BEGIN TRANSACTION statment와 동일
- commit/rollback 둘중하나는 해야해
- 주의할 점
- Connection pool을 사용하는 경우, connection 열고 재사용함
- 이런 이유로, 몇몇 statement는 pending changes를 버리는데 사용될 수 있음
- autocommit
- 트랜잭션이 하나의 쿼리만 실행시킨다면, autocommit=true 가 round-trips를 예방
- 트랜잭션에 여러개의 쿼리가 있다면, 명시적으로 read-only 트랜잭션을 써야함
- 이렇게 해야 write<->read-only 왔다갔다 하는 round trip을 피할 수 있고,
- 영속성 컨텍스트
- 여기서 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있는 헤택이 많음
- 변경 감지를 위해 스냅샷 인스턴스를 보관, 더 많은 메모리를 사용함.
- 읽기 전용으로 엔티티 조회하기
- 스칼라 타입으로 조회하기
- 스칼라 타입으로 필요한 필드만을 조회하는 방식
- 엔티티 객체가 아니니, 영속성 컨텍스트가 결과를 관리하지 않음
- 읽기 전용 쿼리 힌트 사용
- 하이버네이트 전용 힌트 org.hibernate.readOnly 사용시, 엔티티를 읽기 전용으로 조회 가능
- 읽기 전용 트랜잭션 사용
@Transactional(readOnly=true)- 스프링 프레임워크가 하이버네이트 세션 플러시 모드를 MANUAL로 설정함
- 이렇게 하면, 강제 플러쉬 호출하지 않는 한 플러시 안 일어남
- 트랜잭션 커밋하더라도 영속성 컨텍스트는 플러쉬되지 않아 엔티티 등록/수정/삭제 동작 x
- 읽기 전용으로, 영속성 컨텍스트는 변경 감지를 위한 스냅샷 보관하지 않으므로 성능 향상
- 스칼라 타입으로 조회하기