ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] JpaRepository를 사용할 때 @Transactional을 붙여야할까?
    jpa 2024. 5. 1. 15:04

    SimpleJpaRepository

    구현체

    JpaRepository는 SimpleJpaRepository를 상속받습니다.  SimpleJpaRepository는 데이터베이스에 상호작용을 위한 기본적인 CRUD 연산을 제공합니다. CRUD 연산에는 기본적으로 @Transactional 애너테이션이 사용됩니다.

    public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
        @Transactional
        public void deleteById(ID id) {
            Assert.notNull(id, "The given id must not be null");
            this.findById(id).ifPresent(this::delete);
        }
    
        @Transactional
        public void delete(T entity) {
            Assert.notNull(entity, "Entity must not be null");
            if (!this.entityInformation.isNew(entity)) {
                Class<?> type = ProxyUtils.getUserClass(entity);
                T existing = this.entityManager.find(type, this.entityInformation.getId(entity));
                if (existing != null) {
                    this.entityManager.remove(this.entityManager.contains(entity) ? entity : this.entityManager.merge(entity));
                }
            }
        }
    
        public Optional<T> findById(ID id) {
            Assert.notNull(id, "The given id must not be null");
            Class<T> domainType = this.getDomainClass();
            if (this.metadata == null) {
                return Optional.ofNullable(this.entityManager.find(domainType, id));
            } else {
                LockModeType type = this.metadata.getLockModeType();
                Map<String, Object> hints = this.getHints();
                return Optional.ofNullable(type == null ? this.entityManager.find(domainType, id, hints) : this.entityManager.find(domainType, id, type, hints));
            }
        }
        
        public List<T> findAll() {
            return this.getQuery((Specification)null, (Sort)Sort.unsorted()).getResultList();
        }
        
        @Transactional
        public <S extends T> S save(S entity) {
            Assert.notNull(entity, "Entity must not be null");
            if (this.entityInformation.isNew(entity)) {
                this.entityManager.persist(entity);
                return entity;
            } else {
                return this.entityManager.merge(entity);
            }
        }
    }

     

    https://spring.io/blog/2011/02/10/getting-started-with-spring-data-jpa 를 확인하면 다음 문구가 나옵니다.


    Additionally, we can get rid of the @Transactional annotation for the method as the CRUD methods of the Spring Data JPA repository implementation are already annotated with @Transactional.


     

    해석을 하자면 Spring Data JPA Repository 구현체는 이미 CRUD 메서드에 대하여 @Transactional이 붙어있다고 합니다. 즉, SimpleJpaRepository가 JpaRepository를 구현체이기 때문에, SimpleJpaRepository가 제공하는 CRUD 메서드에 대해서는 @Transactional을 사용하지 않아도 됩니다.

    public interface CrudRepository<T, ID> extends Repository<T, ID> {
        <S extends T> S save(S entity);
        <S extends T> Iterable<S> saveAll(Iterable<S> entities);
        Optional<T> findById(ID id);
        boolean existsById(ID id);
        Iterable<T> findAll();
        Iterable<T> findAllById(Iterable<ID> ids);
        long count();
        void deleteById(ID id);
        void delete(T entity);
        void deleteAllById(Iterable<? extends ID> ids);
        void deleteAll(Iterable<? extends T> entities);
        void deleteAll();
    }

     

    Spring에 적용해보기

    User 엔티티

    @Entity
    @NoArgsConstructor
    @Getter
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "user_id")
        private Long id;
    
        private String username;
    
        @Builder
        public User(String username) {
            this.username = username;
        }
    }

     

    UserController

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/users")
    public class UserController {
    
        private final UserService userService;
    
        @PostMapping
        public ResponseEntity<User> saveUser(@RequestBody CreateUserRequestDto request) {
            User savedUser = userService.saveUser(request);
            return ResponseEntity.status(HttpStatus.CREATED)
                .body(savedUser);
        }
    }

    UserRepository

    public interface UserRepository extends JpaRepository<User, Long> {
    }

    UserService

    @Service
    @RequiredArgsConstructor
    public class UserService {
    
        private final UserRepository userRepository;
    
        public User saveUser(CreateUserRequestDto request)  {
            User user = User.builder()
                .username(request.getUsername())
                .build();
            User savedUser = userRepository.save(user);
            return savedUser;
        }
    }

     

    UserService는 JpaRepository를 상속받은 UserRepository를 주입받아 사용합니다. 위에서 말했듯이 JpaRepository의 구현체가 CRUD 메서드에 대해서는 이미 @Transactional을 적용하였기 때문에, 서비스 레이어에서 사용자를 저장하는 과정에서 @Transactional을 적용하지 않았습니다.

     

    POST /users 를 호출하면 DB에 정상적으로 사용자가 저장되었음을 알 수 있습니다.

    @Transactional이 없어도 정상적으로 등록된다.

    프로젝트 코드 수정

    Section 엔티티

    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Section extends BaseEntity {
    
        @Id
        @Column(name = "section_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String content; // 내용
        private long likeCnt; // 좋아요 개수
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "retrospective_id")
        private Retrospective retrospective; // 어떤 회고로부터 만들어진 section인지
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id")
        private User user; // 작성자
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "template_section_id")
        private TemplateSection templateSection; // 섹션 템플릿 정보
    
        @OneToMany(mappedBy = "section", cascade = CascadeType.REMOVE)
        private List<Likes> likes = new ArrayList<>();
    
        @OneToMany(mappedBy = "section", cascade = CascadeType.REMOVE)
        private List<Comment> comments = new ArrayList<>();
    
        @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
        @JoinColumn(name = "action_item")
        private ActionItem actionItem;
    
        @Builder
        public Section(String content, long likeCnt, Retrospective retrospective, User user,
            TemplateSection templateSection) {
            this.content = content;
            this.likeCnt = likeCnt;
            this.retrospective = retrospective;
            this.user = user;
            this.templateSection = templateSection;
        }
    
        // 섹션 내용 update
        public void updateSection(String updateContent) {
            this.content = updateContent;
        }
    
        // 좋아요 등록
        public void increaseSectionLikes() {
            this.likeCnt += 1;
        }
        // 좋아요 취소
        public void cancelSectionLikes() {
            this.likeCnt -= 1;
        }
    
        public boolean isSameUser(User user) {
            return this.getUser().getId().equals(user.getId());
        }
    
        public boolean isActionItemsSection() {
            return this.getTemplateSection().getSectionName().equals("Action Items");
        }
    }

     

    현재 프로젝트에서 제가 담당한 코드의 일부분입니다. JpaRepository를 사용하여 단순히 엔티티를 조회 및 저장을 하고 있습니다. CrudRepository를 사용하여 단순히 CRUD를 진행하고 있기 때문에 @Transactional이 필요하지 않습니다.

     

    따라서 @Transactional을 제거 후에 API를 호출해보도록 하겠습니다.

    400 에러

    하지만.... 에러가 발생합니다. 에러 로그를 보니 could not initialize proxy 문제가 발생합니다. 지연로딩과 관련이 있는 듯합니다.

    지연로딩 초기화 오류

    실행 흐름을 분석해보도록 하겠습니다.

    1. JpaRepository를 사용하여 엔티티 조회
      1. 지연로딩 관계에 있는 연관관계 엔티티는 조회되지 않고 proxy 상태로 저장
    2. 조회된 엔티티는 영속성 컨텍스트에 보관
    3. controller에 결과를 반환하기 위해 지연로딩 상태의 엔티티에 접근
    4. 트랜잭션 범위 밖에 지연로딩 엔티티에 접근할 경우, 초기화되지 않은 proxy를 로드하기 때문에 에러가 발생

    문제는 트랜잭션 범위 밖에서 초기화되지 않은 proxy 객체에 접근하였기 때문입니다. 저희 프로젝트는 OSIV(Open Session In View)를 끈 상태에서 개발을 진행하고 있습니다. OSIV를 끄게 되면 트랜잭션 범위 밖에서는 지연로딩 엔티티에 접근할 수 없습니다. 

     

    따라서 현재 코드에는 @Transactional을 사용해야 합니다.

     

    결론

    SimpleJpaRepository는 JpaRepository의 구현체입니다. SimpleJpaRepository에서 제공하는 CRUD 메서드에는 기본적으로 @Transactional이 붙어있습니다. 따라서 단순히 CRUD하는 과정에서는 @Transactional을 사용하지 않아도 됩니다.

     

    그러나 OSIV를 끈 상태로 지연로딩에 있는 엔티티를 같이 조회하였을 때, 트랜잭션 범위 밖에서 proxy 객체에 접근할 수가 없으므로, 유의해서 사용해야 합니다.

     

     

Designed by Tistory.