Spring
JPA
JPA를 사용하며 겪은 에러 모음

에러

JPA를 사용하며 겪은 에러 모음

대소문자 지정 오류

Caused by: jakarta.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing table [member]

라는 에러가 났다. 혹시나 싶어 create로 돌려보니 MEMBER 테이블을 냅두고 member 테이블이 생성되었고 이를 해결하기 위해서 구글링을 하다가

이런 블로그를 찾았다.

저장시 영속성 컨텍스트 오류

아래 코드에서 모킹을 통해서 teamService를 사용하려고 했는데 memberService.save()에서 에러가 나는 것을 알 수 있다.

@SpringBootTest
@Transactional
class MemberServiceTest {
 
    @Autowired
    private MemberService memberService;
 
    private TeamService teamService = mock(TeamService.class);
 
    @Test
    void 멤버_저장_테스트() {
        // given
        when(teamService.findTop1ById("팀A")).thenReturn(Team.builder().id("팀A").name("팀A").build());
        Team teamA = teamService.findTop1ById("팀A");
        Member member = Member.builder().id("memberA").username("memberA").team(teamA).build();
 
        // when
        Member savedMember = memberService.save(member);
 
        // then
        assertEquals(member.getUsername(), savedMember.getUsername());
        assertEquals(member.getId(), savedMember.getId());
    }
}

memberService.save()를 실행하면 아래와 같이 쿼리가 날아가는 것을 확인할 수 있는데

select m1_0.MEMBER_ID,t1_0.TEAM_ID,t1_0.name,m1_0.username 
from MEMBER m1_0 left join TEAM t1_0 on t1_0.TEAM_ID=m1_0.TEAM_ID 
where m1_0.MEMBER_ID=?

이 쿼리는 JPA가 Member 엔티티를 데이터베이스에서 로드할 때 된다.
MemberService.save(member) 메서드를 호출하면, JPA는 Member 엔티티를 데이터베이스에 저장하고 Member 엔티티를 다시 로드하여 영속성 컨텍스트에 저장된 상태와 데이터베이스의 상태가 일치하는지 확인합니다.
이 때 Team을 저장한적이 없어서 쿼리에서 오류가 발생하는 것이다.

🚫

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

Lazy Loading과 영속성 컨텍스트의 관계, 그리고 Transactional에 관하여

아래와 같은 테스트 코드를 작성했는데,

@SpringBootTest
class ProductServiceTest {
    @Autowired
    private ProductService productService;
    @Autowired
    private MemberService memberService;
 
    @Test
    @DisplayName("검색된 Product에서 Member를 찾을 수 있다.")
    void PRODUCT_MEMBER_양방향_연관테스트() {
        // given
        Product productA = Product.builder().id("productA").name("productA").build();
        Product savedProduct = productService.save(productA);
        Member member = Member.builder().id("memberA").username("memberA").build();
        member.getProducts().add(productA);
        Member savedMember = memberService.save(member);
 
        // when
        Product searchedProduct = productService.findById(productA.getId());
 
        // then
        assertEquals(searchedProduct.getMembers().get(0).getId(), savedMember.getId());
    }
}

failed to lazily initialize a collection of role: org.jpaassociationmapping.domain.Product.members: could not initialize proxy - no Session

라는 에러가 발생하게된다. 이는 김영한 님이 인프런에서 언급하신 적이 있는 에러인데 (opens in a new tab) @Transactional Annotation을 붙여줌으로써 해결해줄 수 있다.

Transactional이란?

보통 DB에서 Transaction을 지원한다는 의미는 여러 쿼리가 실행될 때 에러가 나면 모든 상황을 롤백하는 기능을 의미한다. 즉, 데이터베이스의 상태를 변경하는 작업 또는 한 번에 수행되야 하는 연산들을 의미한다.

그래서 트랜잭션은 다음과 같은 특징을 갖고 있는데

  1. 원자성(Atomicity) : 한 트랜잭션에서 실행한 작업들은 하나의 단위로 처리한다. 즉, 모두 성공 또는 모두 실패한다.
  2. 일관성(Consistency) : 트랜잭션이 성공적으로 완료되면 데이터베이스는 일관된 상태로 유지된다.
  3. 독립성(Isolation) : 둘 이상의 트랜잭션이 동시에 실행될 때, 각각의 트랜잭션은 다른 트랜잭션의 연산에 영향을 받지 않는다.
  4. 지속성(Durability) : 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영된다.

그래서 Transactional 메서드를 클래스, 인터페이스 위에 붙여서 하나의 트랜잭션으로 묶어주는 것이다.

그래서 저 에러와 무슨 연관이 있는데? 🤔

영한님이 설명을 잘 해주셨지만 내 코드를 기준으로 설명해보자면

Product searchedProduct = productService.findById(productA.getId());

라인에서 Product의 멤버를 가져오는데 이 때 hibernate의 세션이 닫혀서 발생하는 에러인 것이다.

에러 해결 방법

Fetch 방식변경

Products 쪽에서 FetchType을 EAGER로 바꿔주면 된다.

Product
@Entity(name = "PRODUCT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter @Builder
public class Product {
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;
 
    private String name;
 
    // 역방향 추가
    // 연관관계의 주인은 Member이고 Member의 products 필드에 의해 매핑된다.
    @ManyToMany(mappedBy = "products", fetch = FetchType.EAGER)
    private List<Member> members;
}

Reference

https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h (opens in a new tab)