인프런 커뮤니티 질문&답변

최한슬님의 프로필 이미지
최한슬

작성한 질문수

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

회원 기능 테스트

@Transacional의 범위에 대해서 궁금한 점이 하나 있습니다!

해결된 질문

작성

·

447

1

안녕하세요 강사님. 항상 훌륭한 강의 감사드립니다.

스프링 MVC 1, 2편에서 사용했던 프로젝트에 JPA를 적용시키는 도중 궁금한점이 하나 생겨서 질문드립니다.

TestInitData 클래스에 @PostConstruct로 데이터베이스에 초기 데이터들을 넣어두려고 합니다.

package com.myservice.web.test;

import com.myservice.domain.item.Item;
import com.myservice.domain.item.ItemRepository;
import com.myservice.domain.member.Grade;
import com.myservice.domain.member.Member;
import com.myservice.domain.member.MemberRepository;
import com.myservice.domain.member.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;

@Slf4j
@Component
@RequiredArgsConstructor
@Transactional
public class TestDataInit {

private final ItemRepository itemRepository;
private final MemberService memberService;
private final MemberRepository memberRepository;

/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
itemRepository.save(new Item("itemC", 15000, 15));

Member member1 = new Member();
member1.setLoginId("manager");
member1.setPassword("manager");
member1.setUsername("최한슬");
member1.setGrade(Grade.MANAGER);

Member member2 = new Member();
member2.setLoginId("user");
member2.setPassword("user");
member2.setUsername("USER");

//바로 memberRepository.save로 접근하면 현재 스레드에서 사용할 수 있는 EntityManager가 없다고 오류 발생
memberRepository.save(member1);
memberRepository.save(member2);
//memberService.save -> memberRepository.save로 접근하면 정상적으로 작동
memberService.save(member1);
memberService.save(member2);
}

}

또한, memberRepository와 memberService는 다음과 같습니다.

[memberRepository]

package com.myservice.domain.member;

import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface MemberRepository {

Long save(Member member);

Optional<Member> findById(Long id);

Optional<Member> findByLoginId(String loginId);

List<Member> findAll();
}
package com.myservice.domain.member;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

@Repository
@RequiredArgsConstructor
@Primary
public class JpaMemberRepository implements MemberRepository {

private final EntityManager em;

@Override
public Long save(Member member) {
em.persist(member);
return member.getId();
}

@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}

@Override
public Optional<Member> findByLoginId(String loginId) {
Member member = em.createQuery("select m from Member m where m.loginId = :loginId", Member.class)
.setParameter("loginId", loginId)
.getResultStream()
.findAny()
.orElse(null);

return Optional.ofNullable(member);
}

@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}

}

[memberService]

package com.myservice.domain.member;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;

public void save(Member member) {
memberRepository.save(member);
}
}

현재 MemberService에는 @Transactional이 걸려있고, memberRepository에는 @Transactional 걸려있지 않습니다.

궁금한점은 TestDataInit 클래스의 init() 메서드에서 바로 memberRepository로 접근하게되면 사용할 수 있는 EntityManager가 없다고 나오며,

memberService->memberRepository로 접근하게 되면 정상적으로 처리되는 것을 확인하였습니다.

두 방식 모두 결국 memberRepository를 통해 save를 수행하게 되는데 바로 memberRepository의 접근은 오류가 발생하고 memberService를 통한 접근은 정상적으로 처리되는 이유를 모르겠습니다.

답변 4

1

김영한님의 프로필 이미지
김영한
지식공유자

아 한슬님 무엇이 문제인지 알겠네요.

@Transactional이 @PostConstruct에 함께 걸려있으면 스프링 라이프사이클 문제때문에 @Transactional이 정상 동작하지 않을 수 있습니다.

@PostConstruct가 먼저 실행되고 이후에 @Transactional AOP가 적용되기 때문입니다.

간단한 해결방안은 제가 강의에서 한 것 처럼 초기화 하는 메서드와 초기화를 실행하는 메서드를 분리해주세요.

그러면 라이프사이클 문제가 해결됩니다.

감사합니다.

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 최한슬님

다음을 참고해주세요.

https://www.inflearn.com/questions/158967

https://www.inflearn.com/questions/159466

감사합니다.

0

최한슬님의 프로필 이미지
최한슬
질문자

감사합니다. 강사님 ㅠㅠ, 몇일동안 많이 고민했었는데 강사님 덕분에 속이 뚫린 느낌입니다!

AOP는 모든 스프링 빈이 처리되고 나서 적용되기 때문에 TestDataInit 클래스가 빈에 등록될때 호출되는 @PostConstruct 시점에는 AOP 중 하나인 @Transactional이 적용되는 것을 보장해주니 않는 것이군요.

그렇다면 MemberService로 접근해서 성공한 것도 정확히 말한다면 @Transactional이 보장되지 않지만 운이 좋아서 성공한 것이라고 생각하면 되는 것일까요?

김영한님의 프로필 이미지
김영한
지식공유자

MemberService로 접근하는 것은 @PostConstruct 본인이 아니라 다른 외부 객체를 호출한 것이기 때문에 이때는 @Transactional이 잘 적용됩니다.

의존관계 주입을 받을 때는 AOP가 이미 적용된 부분을 받게 됩니다.

감사합니다.

최한슬님의 프로필 이미지
최한슬
질문자

감사합니다!!!

0

최한슬님의 프로필 이미지
최한슬
질문자

안녕하세요 강사님, 올려주신 두 링크를 확인하고 많은 고민을 해봤습니다.

제가 이해한 바는 다음과 같습니다.

[첫번째 링크]

Repository에는 @Transactionl이 없기 때문에 EntityManger에 우선 프록시 객체를 주입해준 다음 다른 트랜잭션에서 해당 Repository에 접근하게 되면 그 EntityManger를 사용하겠다.

[두번쨰링크]

메서드 A에 @Transactional을 걸었다면 메서드 내부에 트랜잭션이 전파되기 때문에 A에서 호출되는 다른 기능들은 동일한 트랜잭션이다.

두 링크를 다음과 같이 이해하였습니다.

그렇다면 init() 메서드를 트랜잭션 A라고 가정하겠습니다.

[MemberService]

init()에서 memberService.save 호출 => 트랜잭션 A

memberService에 @Transactional이 있음, 따라서 memberService.save => 트랜잭션 B

memberRepository.save, Repository는 @Transactional이 없으니 EntityManager에는 프록시 객체가 있음. memberService로 인해 호출되었으니 meberService의 EntityManger를 사용해서 디비에 저장 => 트랜잭션 B

[MemberRepository]

init()에서 memberRepository 호출 => 트랜잭션 A

memberRepository.save, Repository는 @Transactional이 없으니 EntityManager에는 프록시 객체가 있음. init()로 인해 호출되었으니 init()의 EntityManger를 사용해서 디비에 저장 => 트랜잭션 A

그렇다면 두 방법 모두 EntityManger에는 프록시객체가 아닌 트랜잭션 A or B의 EntityManger가 존재하는 것이 아닌건가요?

둘다 존재한다면 MemberService를 거치지 않고 바로 MemberRepository.save()로 처리했을때 오류가 발생하는지 모르겠습니다.

최한슬님의 프로필 이미지
최한슬

작성한 질문수

질문하기