본문 바로가기
IT 개인 공부/Spring

[Spring] @PostConstruct에서 @Transactional 처리 시 문제점

by Libi 2021. 9. 17.
반응형

간단한 웹 프로젝트를 진행하고 있는데 테스트할 때마다 매번 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

반응형

댓글