Home 7. 엔티티간 1:1 연관 그리고 즉시 로딩과 지연 로딩
Post
Cancel

7. 엔티티간 1:1 연관 그리고 즉시 로딩과 지연 로딩

01. 키를 참조하는 1:1 연관 매핑

예) 서비스에서 사용자에게 멤버십 카드를 발급해 준다고 하면 시스템은 사용자를 표현하기 위한 User 엔티티와 멤버십 카드를 표현하기 위한 MembershipCard 엔티티를 갖게 된다. 한 명의 사용자는 한 장의 멤버십 카드를 소유할 수 있다고 가정했을 때 User 엔티티와 MembershipCard는 1:1 관계를 갖는다.

User와 MembershipCard는 각자 자신의 식별자를 가지며 MembershipCard는 1:1 연관을 맺는 User 객체를 속성(참조키)으로 갖고 있다.

테이블에서는 membership_card 테이블의 user 테이블의 PK를 참조하지만 엔티티 클래스에서는 User 객체를 참조하고 있다.

MembershipCard 객체를 생성하려면 membership_card 테이블과 user 테이블을 함께 조회한 뒤에 MembershipCard 객체와 User 객체를 알맞게 생성해야한다. 이를 쿼리를 이용해 개발자가 직접 처리한다면 복잡하지만 JPA를 이용하면 간단한 설정으로 두 엔티티 객체 간의 1:1 연관을 처리할 수 있다.

02. 참조키를 이용한 1:1 단방향 연관

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Table(name="membership_card")
public class MembershipCard {
    @Id
    @Column(name="card_number")
    private String number;

    @OneToOne   //User 엔티티와 1:1 연관을 가짐을 설정
    @JoinColumn(name="user_email")  //User 객체의 식별자에 해당하는 참조키로 user_email을 지정
    private User owner;     //User 엔티티에 대한 참조
    @Temporal(TemporalType.DATE)
    @Column(name="expirt_date")
    private Date expiryDate;
    private boolean enabled;

    //....
}

1
2
3
4
5
6
7
em.getTransaction().begin();
User owner = em.find(User.class, "asds@asd.com");

MembershipCard memCard = new MembershipCard("1234", owner, new Date());
em.persist(memCard);

em.getTransaction().commit();

만약 영속 객체가 아닌 User 객체를 MembershipCard의 owner 필드에 할당하면 어떻게 될까?

1
2
3
4
5
em.getTransaction().begin();
User notPersistenceUser = new User("jvm@asd.com", "JVM", new Date());
MembershipCard memCard = new MembershipCard("1234", notPersistenceUser, new Date());
em.persist(memCard);
em.getTransaction().commit();

영속 컨텍스트에 저장되지 않은 User 객체를 MembershipCard에 할당하게 되면 에러가 발생한다. 즉 연관에 사용할 엔티티 객체는 반드시 영속 상태로 존재해야 한다.

03. 1:1 연관의 즉시 로딩과 지연 로딩

EntityManager.find()를 이용해서 MembershipCard 객체를 구하면 외부 조인을 이용해서 연관된 User 객체를 한 쿼리로 함께 로딩한다.

이렇게 연관된 객체를 함께 로딩하는 것을 즉시 로딩이라고 한다. 즉시 로딩은 연관된 객체를 함께 불러오는데 이는 연관된 객체를 함께 사용하지 않으면 필요 없는 객체를 로딩하게 된다는 것을 뜻한다.

연관 객체가 필요없는 기능이 더 많다면 지연 로딩을 사용해서 연관된 객체가 필요할 때만 로딩하도록 구현할 수 있다. 지연 로딩은 연관 객체를 실제 사용하는 시점에 로딩하는 방식이다. 지연 로딩을 설정하는 방식은 @OneToOne(fetch=FetchType.LAZY)

1
2
3
4
//지연 로딩을 설정하면 이때 membership_card만 조회한다.
MembershipCard memCard = em.find(MembershipCard.class, "5678");
//실제 User 객체가 필요할 때 user 테이블을 조회한다.
System.out.println(memCard.getOwner().getName());

fetch 속성은 즉시 로딩을 기본 값으로 갖기 때문에 지연 로딩이 필요한 경우 설정해주자

04. 참조키를 이용한 1:1 양방향 연관

JPA는 두 엔티티 간의 양방향 연관을 지원한다. 테이블 구조는 단방향 구조이지만 MembershipCard 클래스, User 클래스를 서로 참조하는 연관을 가질 수 있다.

참조키를 이용한 1:1 연관을 양방향으로 설정하기 위해서 다음과 같이 한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table(name="user")
public class User {
    @Id@Basic
    private String email;

    //...

    //MembershipCard의 user 객체의 변수명인 "owner"로 설정한다.
    @OneToOne(mappedBy="owner")
    priavate MembershipCard membershipCard;
}

DB 테이블에서 두 엔티티 간의 연관은 참조키를 통해서 이루어진다. JPA의 1:1 연관도 내부적으로 DB 테이블의 참조를 기반으로 구현하기 때문에 본질적으로 참조의 방향은 단방향이다. 위 예에서 MembershipCard 엔티티에서 User 엔티티로의 단방향 참조를 갖게 된다. 즉 DB 데이터를 기준으로 User에서 MembershipCard로의 연관은 존재하지 않는다.

JPA는 1:1 연관에서 물리적으로 존재하지 않는 연관을 처리하기 위해 mappedBy를 사용한다. 위 예에서 mappedBy의 속성값은 owner인데 이는 양방향 연관에서 연관을 소유한 쪽이 상대방 엔티티의 owner 속성이라는 것을 의미한다.

05. 주요키를 공유하는 1:1 연관 매핑

두 엔티티가 키를 공유하는 경우도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserBestSight {
    private String email;
    private User user;
    private String name;
    private String description;

    public UserBestSight(User user, String name, String description) {
        this.email = user.getEmail();  //식별자 공유
        this.user = user;
        this.name = name;
        this.description = description;
    }
}

위 예처럼 UserBestSight 객체가 User 객체의 식별자를 공유하므로 주요키를 공유하는 1:1 연관에서는 User 객체 없이 UserBestSight 객체는 존재할 수 없다.

06. 주요키를 공유하는 1:1 단방향 연관

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name="user_best_sight")
public class UserBestSight{
    @Id
    @Column(name="email")
    private String email;

    @OneToOne
    @PrimaryKeyJoinColumn
    private User user;

    private Strig title;
    private String description;

    public UserBestSight(User user, String name, String description) {
        this.email = user.getEmail();  //식별자 공유
        this.user = user;
        this.name = name;
        this.description = description;
    }
}

@PrimaryKeyJoinColumn은 User 타입을 참조할 때 주요키를 이용한다. 이때 주요키는 UserBestSight의 @Id와 매핑되는 컬럼이다. 즉 1:1 연관을 맺는 UserBestSight의 식별자와 User의 식별자는 같은 값을 갖는다.

1
2
3
4
5
6
//UserBestSight 객체를 생성하려면 User 객체도 필요하다.
User user = new User("asd@rret.com", "홍길동", new Date());
UserBestSight bestSight = new UserBestSight(user, "김둘리", "이상사회");
em.persist(user);
em.persist(bestSight);

이렇게 하면 트랜잭션을 커밋할 때 두 개의 insert 쿼리가 실행된다.

07. 주요키를 공유하는 1:1 양방향 연관

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name="user")
public class user {
    @Id
    private String email;

    @OneToOne(mappedBy="user")
    private UserBestSight bestSight;

    //...

    //객체의 생성과 양방향 연관을 함께 처리하고 있다.
    public UserBestSight createBestSight(String title, String desc) {
        this.bestSight = new UserBestSight(this, title, desc);
        return bestSight;
    }

    public UserBestSight getBestSight() {
        return bestSight;
    }
}

1
2
3
4
User user = new User("asd@rret.com", "홍길동", new Date());
UserBestSight bestSight = user.createBestSight("김둘리", "이상사회");
em.persist(user);
em.persist(bestSight);

08. 1:1 연관 끊기

연관 객체와의 관계를 제거하려면 단순히 null을 할당하면 된다. 양방향 연관을 사용하면 양쪽 연관에 모두 null을 할당하면 된다.

09. 자동 생성키와 1:1 연관 저장

JPA는 persist() 실행 시점에 식별자를 생성하는 방식을 제공하고 있다. 자동 증가 컬럼이나 테이블을 이용한 식별자 생성기가 해당한다.

1
2
3
4
5
6
7
8
9
10
11
12
//자동 증가 컬럼을 식별자 생성기로 사용한 예
@Entity
@Table(name="hotel_review")
public class Review {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    //....


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(name="real_user_log")
public class RealUserLog{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    priavate Long id;

    @OneToOne @JoinColumn(name="review_id")
    priavate Review review;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="used_date")
    priavate Date realUsingDate;

    public RealUserLog(Review review, Date realUsingDate) {
        this.review = review;
        this.realUsingDate = realUsingDate;
    }
}

참조키 방식을 사용하므로 RealUserLog 객체를 생성하는 시점에 Review 객체의 식별자가 필요한 것은 아니다. 실제 Review 객체의 식별자가 필요한 시점은 DB에 저장하기 위해 insert 쿼리를 실행하는 시점이다.

1
2
3
4
5
//Review 객체를 저장하기 전에 RealUserLog 객체를 생성해도 문제가 되지 않는다.
Review review = new Review("H001", 5, "최고에요", new Date());
RealUserLog realUserLog = new RealUserLog(review, new Date());
em.persist(review);
em.persist(realUserLog);

반면 주요키를 공유하는 1:1 연관은 연관관계를 맺기 전에 식별자가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Table(name="real_user_log")
public class RealUserLog{
    @Id
    @Column(name="review_id")
    priavate Long reviewId;

    @OneToOne @PrimaryKeyJoinColumn
    priavate Review review;     //주요키를 공유하는 1:1 단방향

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="used_date")
    priavate Date realUsingDate;

    public RealUserLog(Review review, Date realUsingDate) {
        this.reviewId = review.getId();
        this.review = review;
        this.realUsingDate = realUsingDate;
    }
}

1
2
3
4
5
Review review = new Review("H001", 5, "최고에요", new Date());
em.persist(review); //review의 식별자 생성

RealUserLog realUserLog = new RealUserLog(review, new Date());  //review 식별자 공유
em.persist(realUserLog);

자동 증가 컬럼, 시퀀스 같은 식별자 생성기를 이용하는 엔티티와 주요키를 공유하는 1:1 연관을 갖는 경우 식별자를 생성한 뒤 연관을 맺어야 함에 주의하자

10. 지연 로딩, 프록시, EntityManager 범위

하이버네이트는 연관 객체의 지연 로딩을 구현하기 위해 프록시 객체를 사용한다.

1
2
3
MembershipCard card = em.find(MembershipCard.class, "5678");
//출력되는 클래스는 하이버네이트가 생성한 프록시 클래스이다.
System.out.println(card.getOwner().getClass().gatName());

프록시가 한 번 실제 엔티티를 로딩하면 이후 접근에 대해서는 데이터 조회 쿼리를 실행하지 않는다.

프록시를 통해 실제 연관 객체의 값에 접근하는 시점에 DB에서 select 쿼리를 실행한다. 그러므로 DB와의 연결이 끊기면 연관 객체를 로딩할 수 없다. 따라서 지연 로딩을 설정한 객체를 사용해야 하는 경우 EntityManager를 종료하기 전에 연관된 객체에 접근해야 한다.

This post is licensed under CC BY 4.0 by the author.