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에 정상적으로 사용자가 저장되었음을 알 수 있습니다.
프로젝트 코드 수정
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를 호출해보도록 하겠습니다.
하지만.... 에러가 발생합니다. 에러 로그를 보니 could not initialize proxy 문제가 발생합니다. 지연로딩과 관련이 있는 듯합니다.
실행 흐름을 분석해보도록 하겠습니다.
- JpaRepository를 사용하여 엔티티 조회
- 지연로딩 관계에 있는 연관관계 엔티티는 조회되지 않고 proxy 상태로 저장
- 조회된 엔티티는 영속성 컨텍스트에 보관
- controller에 결과를 반환하기 위해 지연로딩 상태의 엔티티에 접근
- 트랜잭션 범위 밖에 지연로딩 엔티티에 접근할 경우, 초기화되지 않은 proxy를 로드하기 때문에 에러가 발생
문제는 트랜잭션 범위 밖에서 초기화되지 않은 proxy 객체에 접근하였기 때문입니다. 저희 프로젝트는 OSIV(Open Session In View)를 끈 상태에서 개발을 진행하고 있습니다. OSIV를 끄게 되면 트랜잭션 범위 밖에서는 지연로딩 엔티티에 접근할 수 없습니다.
따라서 현재 코드에는 @Transactional을 사용해야 합니다.
결론
SimpleJpaRepository는 JpaRepository의 구현체입니다. SimpleJpaRepository에서 제공하는 CRUD 메서드에는 기본적으로 @Transactional이 붙어있습니다. 따라서 단순히 CRUD하는 과정에서는 @Transactional을 사용하지 않아도 됩니다.
그러나 OSIV를 끈 상태로 지연로딩에 있는 엔티티를 같이 조회하였을 때, 트랜잭션 범위 밖에서 proxy 객체에 접근할 수가 없으므로, 유의해서 사용해야 합니다.