간단한 웹 프로젝트를 진행하고 있는데 테스트할 때마다 매번 DB에 데이터들을 추가해줘야 하는 번거로움이 존재하였다.
이를 해결하기 위해 TestData이라는 클래스를 스프링 빈으로 등록한 후 init() 메서드에 @PostConstruct를 적용하여 TestData 빈이 스프링 컨테이너에 등록될 때 자동으로 테스트 데이터를 DB에 추가하도록 하였다.
Member, MemberService, MemberRepository 클래스는 다음과 같이 구현되어 있다.
MemberService에는 @Transactional을 걸어주었고, MemberRepository에는 @Transactional을 걸어주지 않았다.
@Getter @Setter
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "member_id")
private Long id;
private String username;
}
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Long save(Member member) {
return memberRepository.save(member);
}
public Member findById(Long id) {
return memberRepository.findById(id);
}
}
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
public Long save(Member member) {
em.persist(member);
return member.getId();
}
public Member findById(Long id) {
return em.find(Member.class, id);
}
}
TestData 클래스는 다음과 같다.
@RequiredArgsConstructor
@Transactional
@Component
public class TestData {
private final MemberService memberService;
private final MemberRepository memberRepository;
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
Member member1 = new Member();
member1.setUsername("user A");
Member member2 = new Member();
member2.setUsername("user B");
//memberService.save -> memberRepository.save로 접근하면 정상적으로 작동
memberService.save(member1);
memberService.save(member2);
//바로 memberRepository.save로 접근하면 현재 스레드에서 사용할 수 있는 EntityManager가 없다고 오류 발생
memberRepository.save(member1);
memberRepository.save(member2);
}
}
TestData에 @Transactional을 걸어줬기 때문에 MemberService를 통해 엔티티를 저장하든, MemberRepository를 통해 엔티티를 저장하든 항상 트랜잭션이 적용되기 때문에 정상적으로 동작할 것이라고 생각하였다.
하지만 실제로 동작해보면 MemberService -> MemberRepository를 통해 member 객체를 저장하면 정상적으로 동작되는 반면, 바로 MemberRepository를 통해 member 객체를 저장하면 정상적으로 동작되지 않는 것을 확인하였다.
에러 내용을 살펴보니 실제로 이용할 수 있는 EntityManager가 없기 때문이라는 것을 알 수 있었다.
어차피 MemberService도 결국 내부적으로 MemberRepository를 통해 EntityManager를 호출하는 것인데 왜 전자는 가능하고 후자는 불가능한 것일까?
@Transactional이 걸린 메서드에서 다른 메서드를 호출할 경우, 호출한 메서드에 @Transactional이 걸려있으면 두 메서드는 다른 트랜잭션이며, 걸려있지 않다면 동일한 트랜잭션으로 취급한다.
또한, MemberRepository에는 @Transactional이 없기 때문에 빈 등록 시 스프링 컨테이너는 EntityManager에 우선 가짜 프록시 객체를 주입해준 다음, 다른 트랜잭션에서 해당 Repository에 접근하게 되면 그 트랜잭션의 EntityManager를 호출하여 트랜잭션을 처리해준다.
그렇다면 init() 메서드를 트랜잭션을 A라고 할 경우 다음과 같이 동작될 것이다.
init - MemberService - MemberRepository
- init() 실행 : 트랜잭션 A
- memberService.save : 트랜잭션 B
- memberRepository.save : 트랜잭션 B, EntityManger는 프록시 객체가 있었지만 MemberService의 EntityManager를 이용하여 DB 접근
init - MemberRepository
- init() 실행 : 트랜잭션 A
- memberRepository.save : 트랜잭션 A, EntityManger는 프록시 객체가 있었지만 init의 EntityManager를 이용하여 DB 접근
즉, 두 방법 모두 트랜잭션에 사용되는 EntityManager가 존재하는데 왜 MemberRepository에 바로 접근하는 방법은 사용할 수 있는 EntityManager가 없다고 하는 것일까?
이는 스프링 라이프사이클로 인해 @PostConstruct와 @Transactional AOP가 적용되는 시점이 다르기 때문이다.
@PostConstruct는 해당 빈 자체만 생성되었다고 가정하고 호출된다. 이 말은 해당 빈에 관련된 AOP 등을 포함하여, 전체 스프링 애플리케이션 컨텍스트가 초기화되었다는 것을 보장해주지 않는다는 의미이다.
반면, AOP는 스프링의 후 처리기(Post Processer)가 완전히 동작을 끝낸 다음, 스프링 애플리케이션 컨텍스트의 초기화가 완료되어야 적용된다.
즉, 바로 MemberRepository를 접근하는 경우는 init() 메서드에 @Transactional이 적용되었다는 것이 보장되지 않기 때문에 정상적으로 동작하지 않는 것이다.
반면, MemberService를 통해 MemberRepository를 접근하는 경우는 이미 스프링 애플리케이션 컨텍스트 초기화가 이루어져 @Transactional이 적용된 MemberService의 save() 메서드로 접근하기 때문에 정상적으로 동작하는 것이다.
이러한 라이프사이클 문제를 해결하는 방법은 우회해서 해결하는 다양한 방법들이 존재한다.
- 다른 스프링 빈을 호출해서 사용하는 방법
- AOP를 사용하지 않고 트랜잭션을 직접 코딩하는 방법
- 애플리케이션 컨텍스트가 완전히 초기화된 이벤트를 받아서 호출하는 방법
- 초기화하는 메서드와 초기화를 실행하는 메서드를 분리하는 방법
[ Reference ]
· https://www.inflearn.com/questions/270247
· https://stackoverflow.com/questions/17346679/transactional-on-postconstruct-method
'IT 개인 공부 > Spring' 카테고리의 다른 글
[Spring] Bean Validation (0) | 2021.09.23 |
---|---|
[Spring] 양방향 매핑시 주의점 : toString (0) | 2021.09.15 |
[Spring] AOP가 적용되지 않은 메서드에서 AOP가 적용된 메서드를 호출하면 AOP가 정상적으로 동작할까? (0) | 2021.09.11 |
[Spring] AOP란? (0) | 2021.09.09 |
[Spring] 서블릿 필터 vs 스프링 인터셉터 (0) | 2021.09.05 |
댓글