[JPA] 8.JPA 활용-1

재우·2025년 10월 12일

JPA

목록 보기
8/11

참고1

  • JPA를 사용하기 때문에, 엔티티매니저가 있어야한다.
@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

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

    public Member find(Long id) {
        return em.find(Member.class, id);
    }
}
  • @PersistenceContext를 사용하면 내부적으로 자동으로 엔티티매니저를 생성해서 주입해준다.

참고2

@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(value = false)
    public void testMember() throws Exception {
        //given
        Member member = new Member();
        member.setUsername("memberA");

        //when
        Long savedId = memberRepository.save(member);

        //then
        Member findMember = memberRepository.find(savedId);
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
	    System.out.println("findMember == member" + (findMember == member)); // 엔티티 동일성 보장
    }

}
  • 참고로, 엔티티 매니저를 통한 작업은 항상 트랜잭션 내에서 작동되어야한다.
  • @Transactional이 테스트를 하는 부분이 아닌 일반적인 부분에 있으면 정상적으로 동작하지만, 테스트를 하는 부분(@Test)에 있으면 테스트를 하는 로직이 끝나면 자동으로 롤백을 한다. 그래서 @Rollback(false)를 해주면, 롤백하지않고 커밋한다.
  • 동일한 트랜잭션 내에서는, 저장을 하고 조회를 할 때 동일한 영속성 컨텍스트를 사용한다.
  • 참고로 @Transactional(readOnly = true) 옵션은 데이터를 단순히 조회만 하는곳에서 사용하면 성능이 향상된다. 저장이나 변경하는쪽에서는 readOnly = true를 하면 안된다. 그러면 저장이나 변경이 안된다.


참고3

  • 하나의 회원은 여러주문을 할 수 있다.(회원과 주문은 일대다 관계)

  • 하나의 주문에 여러개의 상품이 있을 수 있다. 하나의 상품이 여러개의 주문에 있을 수 있다.(주문과 상품은 다대다 관계)
    ==> 다대다 관계를 중간에 엔티티를 추가

    • 하나의 주문에 여러개의 주문상품이 있을 수 있다.(OneToMany)

    • 하나의 주문상품은 하나의 주문에 있을 수 있다.(하나의 주문상품은 하나의 주문이다)(ManyToOne)

    • 하나의 주문상품은 하나의 상품에 있을 수 있다.(하나의 주문상품은 하나의 상품이다)(ManyToOne)

    • 하나의 상품이 여러개의 주문상품에 있을 수 있다.(OneToMany)

      참고로, 다대다 관계를 중간에 엔티티를 추가하면 항상 해당 중간 엔티티에서는 각각을 ManyToOne으로 연관관계를 해야한다.

  • 하나의 카테고리에 여러개의 상품이 있을 수 있고, 하나의 상품이 여러개의 카테고리를 가질 수 도있다.(ManyToMany)

논리적으로 설명한것보다, DB관점에서 보면 이해하기 쉽다.


참고4

자기 자신과의 연관관계

  • Category는 계층구조(트리구조)를 가질 수 있는 엔티티이다.
    즉, 어떤 카테고리는 부모 카테고리를 가질 수도 있고, 자식 카테고리들을 가질 수도 있다.

  • 카테고리와 카테고리의 관계는 다대일 양방향 연관관계이다.
    예를들어 Member와 Team처럼 각각의 엔티티가 있으면 각각의 클래스에서 다대일 양방향 연관관계를 설정해주면 되지만, 카테고리는 자기자신과의 연관관계를 맺는 구조이므로, Category클래스안에서 다대일 양방향 연관관계를 맺어준것이다. 즉, Member–Team 관계를 하나의 클래스 안에서 구현한 것이라고 생각하면된다.

public class Category {

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent; // 현재 카테고리 엔티티의 부모 카테고리를 가리킨다.


    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>(); // 현재 카테고리 엔티티의 자식 카테고리를 가리킨다.
    
}
  • 보통 이렇게 다대일 양방향 연관관계는 서로 다른 두 엔티티에서 구현하지만, Category는 자기 자신과의 연관관계를 갖고 있어서 같은 클래스 안에서 부모(parent)와 자식(child) 관계를 다대일 양방향 연관관계로 구현한 것이다.
  • 여기서 mappedBy 부분은 쉽게 생각해서, 연관관계가 서로 다른 두 엔티티에서 구현되고 있다고 생각하면 이해하기 쉽다. 양방향 연관관계에서, 연관관계의 주인이 아닌쪽에서 mappedBy를 사용하기 때문이다.

조회

category_idnameparent_id
1전자제품NULL
2냉장고1
3세탁기1

위와 같은 데이터가 있다고 했을때,

Category category = em.find(Category.class, 2L); // 냉장고
Category parent = category.getParent(); // 전자제품
category.getParent()를 호출하면, JPA는 내부적으로 CATEGORY 테이블에서 현재 category의 JoinColumn으로 명시했던 parent_id값(=외래키값)을 가져와서 CATEGORY테이블의 기본키를 참조하여 해당 Category 데이터를 조회하고, Category 객체를 생성해서 반환한다.
1. category.getParent()가 호출되면 내부적으로 CATEGORY테이블에서 현재 category의 JoinColumn으로 명시했던 parent_id값(=외래키값)을 가져온다.
2. Category테이블의 기본키가 이 외래키와 일치하는 Category 데이터를 찾는 SQL쿼리를 실행한다.
3. 조회된 데이터로 Category객체를 생성해서 반환한다.

Category category = em.find(Category.class, 1L); // 전자제품
List children = category.getChild(); // 냉장고, 세탁기
category.getChild()를 호출하면, JPA는 내부적으로 CATEGORY테이블에서 현재 category의 기본키값을 가져와서 CATEGORY테이블의 외래키를 참조하여 해당 Category데이터를 조회하고, Category객체를 생성해서 기존 비어있는 리스트에 추가한다.
1. category.getChild()가 호출되면 내부적으로 CATEGORY테이블에서 현재 category의 기본키값을 가져온다.
2. Category테이블의 외래키인 parent_id가 이 기본키와 일치하는 Category 데이터를 찾는 SQL쿼리를 실행한다.
ex) SELECT * FROM CATEGORY WHERE parent_id = 1;
3. 조회된 데이터로 Category객체를 생성해서 기존 비어있는 리스트에 추가한다

참고로, 부모 카테고리의 parent_id는 항상 NULL이다. parent_id는 현재 카테고리의 부모 카테고리의 pk를 가리키는 외래키이다. 하지만 그 위의 부모가 없으므로 참조할 pk값이 없다. 그래서 NULL이다.

저장

Category parent = new Category(); // 부모 카테고리 생성
parent.setName("전자제품");

Category child1 = new Category(); // 자식 카테고리 생성
child1.setName("냉장고");

Category child2 = new Category();
child2.setName("세탁기");

parent.addChildCategory(child1);
parent.addChildCategory(child2);

em.persist(parent);   // 부모 먼저 저장
em.persist(child1);   // 자식 저장
em.persist(child2);   // 자식 저장
  • 일반적으로는 부모를 먼저 저장하는 것이 안전하고 권장되는 방식이다.
    부모를 먼저 저장하면 DB에서 부모의 기본키(PK)가 생성되고, 이 값이 자식 엔티티의 외래키(parent_id)에 설정되어 자식도 정상적으로 저장할 수 있기 때문이다.

참고5

  • 일반 클래스 - 기본생성자가 아닌 다른 생성자를 작성하려고 하면 기본생성자를 명시적으로 작성하지 않아도 된다.
  • 엔티티클래스(@Entity) - 기본생성자가 아닌 다른 생성자를 작성하려고 하면 기본생성자를 명시적으로 작성해야한다.
  • 임베디드클래스(@Embeddable) - 기본생성자가 아닌 다른 생성자를 작성하려고 하면 기본생성자를 명시적으로 작성해야한다.

그리고 셋다 아무 생성자도 명시적으로 작성하지않으면, 내부적으로 자동으로 기본생성자를 추가해주고, 기본생성자가 아닌 다른생성자를 하나라도 작성하면 기본생성자는 자동으로 추가되지않는다.


참고6

  • Assertions.assertEquals(member1, member2);
    ==> member1.equals(member2)하는것과 동일하다.
    ==> 만약 값이 int이면 값비교를하고, String이면 내부적으로 String.equals()가 값 비교로 오버라이드 되었으므로 값비교를한다. 객체면 참조비교를한다.
    ==> 만약 값이 enum상수(enum객체)이면 객체처럼 참조비교를 한다.

참고7

Item과 Book이 상속관계에 있다고 했을 때,

public Item findOne(Long id) {
	return Book객체;
}

이게 성립되는 이유는 다형성 때문이다.
즉, Item item = new Book();이 되는것과 Item item = findOne();을 하는것이 같은 원리라고 생각하면된다.
return 되는 객체는 자식객체이지만, 메서드의 반환타입을 부모타입으로 지정할 수 있다. ==> 상속, 다형성의 원리
그래서 Item item = findOne();을 하게되면 item이 참조하는 실제 객체는 자식객체이다.


참고8

  • 식별자인 id가 영속상태였던 적이있는 엔티티의 id값과 동일하면 준영속엔티티로 볼 수 있다.
    영속상태가 되어서 db에 저장된 적이 있는 엔티티의 식별자를 가지고 있는 엔티티는 준영속 엔티티이다.
    즉, 식별자를 기준으로 영속상태가 되어버린 엔티티가 있는데, 현재는 더이상 영속성 컨텍스트가 관리하지 않으면 준영속 상태이다.

0개의 댓글