Spring/JPA

[JPA] JPA 정리

미소서식지 2023. 8. 5. 07:39

의문점

 

어떤 엔티티는 왜 @JoinColumn 어노테이션을 추가해주나?

그리고 어떤 엔테티는 왜 @OneToMany와 같은 어노테이션 뒤에 mappedBy라는 속성을 추가해주는가?

연관관계의 주인이라는 개념 때문에 그러한가?

 

 

[JPA] @JoinColumn 확실히 알고가기!!!

안녕하세요 오늘은 JPA 주제로 글을 써보도록 하겠습니다. 테이블들간 연관관계를 설정해 줄때 일대다(1:N) 관계일때 @JoinColumn 어노테이션을 사용해서 해당 컬럼의 이름을 설정해줍니다.... 근데!!

boomrabbit.tistory.com

 

 

https://ch4njun.tistory.com/274

 

[Spring Boot] JPA 에서의 연관관계

JPA 에서 가장 중요한 개념이라고 하면 연관관계 매핑과 영속성 컨텍스트가 있다. 이번 포스팅에서는 이 중에서 연관관계 매핑에 대해서 이야기해보려고 한다. 객체지향 프로그램에서의 객체와

ch4njun.tistory.com

 

mappedBy는 어떤 의미인가?

https://velog.io/@dhk22/JPA-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84

 

JPA - 양방향 연관관계 ( mappedBy )

🔎 객체 간의 관계를 정말 테이블처럼 하기 위한 ..

velog.io

 

mappedBy의 값은 반대쪽에 자신이 매핑되어 있는 필드명을 써주시면 된다.

예제의 경우 Team 자신이 Member의 team에 매핑되어 있으므로 team으로 설정해준 것이다.

 

연관관계란? - 객체 위주 모델링 도입 시 나오는 용어

양방향 관계 시 연관관계 주인에 값 수정 안 해줬을 때

 

// 저장
Member member = new Member();
member.setName("member1");
em.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member); // team 테이블에서 memberList에 member를 추가하려고 했음
em.persist(team);

 

team 테이블에서 memberList에 member를 추가하려고 했으나, 

정작 member 테이블에 존재하는 member에서 수정되어야 하는 TEAM_ID에는 아무것도 반영되지 않았다.

team과 member 사이의 일대다 관계에서 만약 수정이 일어난다면 (1. member의 team 수정, 2. team의 member 수정)

결국 DB 상에서 수정되어야 하는 부분은 FK키를 가지고 있는 MEMBER 테이블의 TEAM_ID 컬럼이 수정되어야 한다.

그러나, 위의 코드는 TEAM 테이블에서 수정이 일어났으므로 DB에서는 아무 동작도 일어나지 않은 것이다.

 

하지만, "아무 동작도 일어나지 않은 것"이란 말을 정확하게 파악할 필요가 있다.

아래의 여러 번 JPA를 실행한 걸 보면 알 수 있듯이, em.persist(team)을 통해 DB에 실제 새롭게 생성한 엔티티 객체가 들어가 있는 것을 볼 수 있다. 

 

즉, 생략된 주어는 "외래키에 대해" 아무런 동작이 일어나지 않았다는 것으로 파악해야 한다.

연관관계의 주인이 아닌 쪽에서는 외래키 읽을 수만 있고 수정이 불가능하다는 것이다.

그것을 코드에서 보면, team.getMembers().add(member) 가 연관관계를 맵핑, 즉 외래키에 대한 수정을 하는 부분인데,

동작하지 않는다는 것을 의미한다.

 

 

한 번 JPA를 실행했을 때 결과

 

한 번 JPA를 실행했을 때 결과

 

여러 번 JPA를 실행했을 때 결과

 

코드를 수정하여 연관관계 주인에게 연관관계를 맵핑해주었을 때는 결과가 달라진다.

 

// 저장
            Team team = new Team();
            team.setName("TeamA");
//            team.getMembers().add(member);
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team); // Member.team 에 연관관계 맵핑하여 외래키 수정
            em.persist(member);

 

위와 같이 Member.team에 코드를 수정해주었을 때 외래키인 TEAM_ID에 정상적으로 방금 새로 생성한 엔티티인 

TEAM의 식별자인 TEAM_ID가 수정되었음을 알 수 있다.

엔티티 관계에서 객체인 team을 member객체에 넣어주면, @JoinColumn으로 연관되어 있는 맵핑에 따라 

DB 테이블에서 TEAM의 PK인 TEAM_ID로 값이 맵핑되고, 외래키의 수정이 일어나게 되는 것이다.

 

 

상속 관계 매핑 (3)

1. 조인 전략

 

@Inheritance(strategy = InheritanceType.JOINED)

 

Movie 추가 시, INSERT가 두 번 일어남

 

 

조회 시, JOIN이 일어남

Hibernate: 
    select
        movie0_.ITEM_ID as ITEM_ID1_2_0_,
        movie0_1_.name as name2_2_0_,
        movie0_1_.price as price3_2_0_,
        movie0_.actor as actor1_5_0_,
        movie0_.director as director2_5_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.ITEM_ID=movie0_1_.ITEM_ID 
    where
        movie0_.ITEM_ID=?
findMovie = jpabook.jpashop.domain.Movie@3337d04c

 

DTYPE 추가 

 

@DiscriminatorColumn

 

기본적으로 엔티티 명으로 DTYPE이 들어가게 됨

ITEM 테이블에서 운영 및 유지보수할 때 조회 시 쉽게 해당 타입을 알 수 있어, 웬만하면 컬럼 넣는 것을 권장함

 

ITEM에 DTYPE 추가 후 SELECT 결과

 

만약 엔티티명이 DTYPE으로 들어가지 않게 하려면,

아래 코드를 상속받는 Movie 클래스 위의 어노테이션으로 두면 그 안의 값이 DTYPE의 value로 들어가게 된다.

 

@DiscriminatorValue("A")

 

Movie 테이블에 @DiscriminatorValue 설정 시 들어가는 값

 

2. 싱글 테이블 전략

ITEM 테이블에 아래 어노테이션 추가

 

@Inheritance(strategy = InheritanceType.SINGLE_TABLE

 

그런데, DTYPE 컬럼 없어도 필수로 DTYPE 생성된다..!!!

결론을 말하자면, DTYPE을 두는 건 운영 상 좋다.

 

@DiscriminatorColumn

 

싱글테이블 전략 수행 시 결과 - Type 구분을 위해 DTYPE이 반드시 필요!

 

INSERT 한 번에 들어감

두 번 할 필요가 없어 성능은 제일 좋음

 

insert 
        into
            Item
            (name, price, actor, director, DTYPE, ITEM_ID) 
        values
            (?, ?, ?, ?, 'Movie', ?)

 

SELECT 할 때도 JOIN 없이 이루어짐

 

select
        movie0_.ITEM_ID as ITEM_ID2_0_0_,
        movie0_.name as name3_0_0_,
        movie0_.price as price4_0_0_,
        movie0_.actor as actor5_0_0_,
        movie0_.director as director6_0_0_ 
    from
        Item movie0_ 
    where
        movie0_.ITEM_ID=? 
        and movie0_.DTYPE='Movie'
findMovie = jpabook.jpashop.domain.Movie@5f95f1e1

 

전략을 수정해도, 즉 DB 구조를 바꿔도 소스코드를 별로 고칠 필요가 없다는 게 JPA 의 큰 장점이다!!

 

3. TABLE PER CLASS

ITEM 테이블은 사라지고 각각의 테이블에 필요한 모든 값들이 들어있다.

 

 

 

INSERT할 때는 괜찮은데, Item id 만 알때 조회 시 UNION ALL로 동작하게 된다.

 

Item findMovie = em.find(Item.class, movie.getId());
System.out.println("findMovie = " + findMovie);

 

작동하는 쿼리는 아래와 같다.

 

Hibernate: 
    select
        item0_.ITEM_ID as ITEM_ID1_2_0_,
        item0_.name as name2_2_0_,
        item0_.price as price3_2_0_,
        item0_.actor as actor1_5_0_,
        item0_.director as director2_5_0_,
        item0_.author as author1_1_0_,
        item0_.isbn as isbn2_1_0_,
        item0_.artists as artists1_0_0_,
        item0_.clazz_ as clazz_0_ 
    from
        ( select
            ITEM_ID,
            name,
            price,
            actor,
            director,
            null as author,
            null as isbn,
            null as artists,
            1 as clazz_ 
        from
            Movie 
        union
        all select
            ITEM_ID,
            name,
            price,
            null as actor,
            null as director,
            author,
            isbn,
            null as artists,
            2 as clazz_ 
        from
            Book 
        union
        all select
            ITEM_ID,
            name,
            price,
            null as actor,
            null as director,
            null as author,
            null as isbn,
            artists,
            3 as clazz_ 
        from
            Album 
    ) item0_ 
where
    item0_.ITEM_ID=?
findMovie = jpabook.jpashop.domain.Movie@3337d04c

 

em.getReference(Member.class, member.getId())

getReference()를 호출하면 select 쿼리가 나가지 않는다.

그러나 아래와 같이 실행하면 name을 받아올 때 select 쿼리가 실행된다.

 

getReference() 호출하는 시점에는 DB에 쿼리를 호출하지 않지만, 해당 값을 실제로 사용하는 시점에는 

DB에 실제로 findMember를 가져오기 위해 getName()이 실행될 때 DB에 쿼리를 보낸다.

 

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
// class jpabook.jpashop.domain.Member$HibernateProxy$K72bfG9W
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());

 

.getClass() 했을 때 Member가 나오는 게 아님

 

class jpabook.jpashop.domain.Member$HibernateProxy$K72bfG9W 라는 가짜 클래스가 나옴

 

 

findMember.id = 36
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_1_0_,
        member0_.createdBy as createdB2_1_0_,
        member0_.createdDate as createdD3_1_0_,
        member0_.lastModifiedBy as lastModi4_1_0_,
        member0_.lastModifiedDate as lastModi5_1_0_,
        member0_.CITY as CITY6_1_0_,
        member0_.USERNAME as USERNAME7_1_0_,
        member0_.STREET as STREET8_1_0_,
        member0_.ZIPCODE as ZIPCODE9_1_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

 

프록시 객체 특징

 

Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 조회한 것 (null)

System.out.println("findMember = " + findMember.getClass()); 
// findMember = class jpabook.jpashop.domain.Member$HibernateProxy$uyWmnVM6

System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName()); // 실제 Member entity 생성해서 target 변수가 객체 가짐
System.out.println("findMember.name = " + findMember.getName()); // 프록시 객체는 처음 사용할 때 한 번만 초기화됨 (또 쿼리 날리는 것 아님)

System.out.println("findMember = " + findMember.getClass()); // 프록시 객체가 실제 엔티티로 바뀌는 것이 아님 - 프록시 객체를 통해서 실제 엔티티에 접근 가능
// findMember = class jpabook.jpashop.domain.Member$HibernateProxy$uyWmnVM

 

영속성 객체의 도움을 받을 수 없는 준영속 상태

 

Member member = new Member();
member.setName("hello");

em.persist(member);

em.flush();
em.clear();

Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy

em.detach(refMember);
// em.close();

System.out.println("refMember = " + refMember.getName());

 

org.hibernate.LazyInitializationException: could not initialize proxy [jpabook.jpashop.domain.Member#56] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
	at jpabook.jpashop.domain.Member$HibernateProxy$CQaFavUq.getName(Unknown Source)
	at jpabook.jpashop.domain.Main.main(Main.java:32)
8월 09, 2023 5:01:19 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]