OneToOne 관계는 과연 지연로딩이 되는가?

wally·2022년 5월 22일
5

[JPA] JPA 모든것

목록 보기
2/2

목차

  1. JPA 연관관계 매핑 - @OneToOne
  2. JPA 프록시
  3. 즉시로딩과 지연로딩이란?
  4. OneToOne 은 즉시로딩인가 지연로딩인가

1. JPA 연관관계 매핑 - @OneToOne

  • JPA에서 단방향 매핑은 @JoinColumn과 @OneToOne 를 통해 할 수 있다.

@Entity(name = "Team")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "team_id")
    private Long id;

    private String teamName;
}

@Entity(name = "Member")
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @OneToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

  • 양방향 매핑은 Team Class에 Member를 추가하고 mappedBy를 통해 맺을 수 있다.
  • 연관관계는 단방향 매핑으로 다 맺어진것이다.
  • 객체 그래프 탐색을 위해 추가 설정을 하는 것이다.
  • OneToOne 의 경우 연관관계 주인은 편한 곳으로 지정하면 된다.
  • 연관관계의 주인 쪽에서 @JoinColumn 을 통해 관계를 맺고 반대쪽은 mappedBy 속성을 사용하여 관계를 맺으면 된다.
    @OneToOne(mappedBy = "team")
private Member member;

2. JPA 프록시

어떤 A와 B가 연관관계가 있을 때 A를 호출할 때 항상 B도 함께 호출되면 효율적이지 못하다. 이럴 때 프록시를 활용하여 문제를 해결할 수 있다.

  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 게 아닌 프록시를 통해 실제 엔티티에 접근하는 것
  • 즉 프록시 객페를 가지고 있다가 실제 값이 호출 될때 db 에서 데이터를 가지고 온다.
  • 영속성 콘텍스트에 이미 실제 엔티티가 있으면 getReference를 하여도 실제 엔티티가 조회된다.
  • 준영속 상태에서 프록시를 통해 값을 가져오려고 하면 에러가 발생한다.

1) Client가 getName()을 호출한다.
2) 호출받은 프록시가영속성 콘텍스트에 초기화를 요청한다.
3) 영속성 콘텍스트에 이미 값이 있다면 바로 4번으로 갈 테지만 그렇지 않다면 DB에 조회한다.
4) 조회된 데이터로 실제 엔티티를 생성한다.
5) 생성된 실제 엔티티를 프록시가 조회하여 반환해준다.

System.out.println("=================== 호출 전 ===================");
        System.out.println("findMember.getClass() = "+findMember.getClass());

        System.out.println("findMember.getName() 첫번째 호출 = "+findMember.getName());
        System.out.println("findMember.getName() 두번째 호출 = "+findMember.getName());
        System.out.println("=================== 호출 후 ===================");
===================호출 전===================
        findMember.getClass()=

class blogJpa.Member$HibernateProxy$PnhnxVd7
        Hibernate:
        select
        생략...
        from
        Member member0_
        left outer join
        Team team1_
        on member0_.team_id=team1_.team_id
        where
        member0_.member_id=?
        findMember.getName()첫번째 호출=Dexter
        findMember.getName()두번째 호출=Dexter
        ===================호출 후===================

3. 즉시로딩과 지연로딩이란?

즉시 로딩이란 객체 A를 조회할 때 A와 연관된 객체들을 한 번에 가져오는 것이다.
지연 로딩이란 객체 A를 조회할 때는 A만 가져오고 연관된 애들은 프록시 초기화 방법으로 가져온다.

EAGER, LAZY

@ManyToOne, @OneToOne : 기본값이 즉시 로딩
@OneToMany, @ManyToMany : 기본값이 지연 로딩

즉시 로딩의 성능과 문제점

  • 관련된 모든 객체를 한번에 db 에서 가져오기 때문에 편하게 느껴진다.
  • 실제 엔티티의 연관관계가 깊어지면 즉시 로딩이 연관된 모든 엔티티들을 조회하게 된다.
  • 그 엔티티들을 조회할 때 따로따로 조회하므로 N + 1문제 가 발생한다.
  • 결론적으로 가급적 모든 관계는 지연 로딩으로 설정해야 한다
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
Team team1=new Team();
team1.setTeamName("team1");
em.persist(team1);

Team team2=new Team();
team2.setTeamName("team2");
em.persist(team2);

Member m1=new Member();
m1.setName("Dexter");
m1.setTeam(team1);
em.persist(m1);

Member m2=new Member();
m2.setName("James");
m2.setTeam(team2);
em.persist(m2);

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

List<Member> members = em.createQuery("select m from Member m", Member.class)
        .getResultList();
  • SQL 을 확인하면 Member 은 한번에 가져오지만 team 은 가져올 때 마다 쿼리를 따로따로 날리는 것을 알 수 있다.
  • 만약 Member가 1개 있고, 100개의 팀에 그 멤버가 존재한다면 Member 을 select 할 시 총 101개의 쿼리문이 나가게 된다.
  • 지연 로딩도 n+1 문제가 존재하지만 fetch join 을 통해 한번에 데이터를 가져 올수 있다.

[성현님의 블로그를 참고해 주세요 - https://goshk95.tistory.com/218]

4. OneToOne 은 즉시로딩인가 지연로딩인가

우리는 앞에서 @OneToOne 은 기본값이 즉시 로딩이고 즉시 로딩의 N+1 등의 문제들을 페치조인으로 회피하기 위해 지연로딩을 권장한다고 하였다. 그렇다면 이질문을 한 이유가 무엇일까?

결론적으로 말하면 OneToOne 은 양방향 매핑의 경우 지연로딩이 적용되지 않는 문제가 존재한다.

  • @OneToOne 은 즉시 로딩이다.
  • FetchType을 LAZY로 변경해서 쓰기를 추천한다
  • 단방향 @OneToOne 관계에서는 지연로딩 적용에 문제가 없다
  • 양방향 @OneToOne 관계에서는 FetchType을 Lazy로 설정하더라도 Eager로 동작하는 경우가 있다.
  • 정확하게 말하면 @OneToOne 양방향 연관 관계에서 연관 관계의 주인이 아닌 쪽 엔티티를 조회할 때, Lazy로 동작할 수 없다.

테스트 코드


@Entity(name = "Member")
@Getter
@Setter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public void setTeam(Team team) {
        team.setMember(this);
        this.team = team;
    }
}

@Entity(name = "Team")
@Getter
@Setter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "team_id")
    private Long id;

    private String teamName;

    @OneToOne(fetch = FetchType.LAZY, mappedBy = "team")
    private Member member;

    public void setMember(Member member) {
        this.member = member;
    }
}

원인분석

  1. 지연(LAZY)로딩을 사용하기 위해서는 JPA 구현체에서 "프록시를 만들어 주어야 한다."
  • 전제 조건이 하나 더 있다.
  • JPA 구현체는 연관 관계 엔티티에 null 또는 프로시 객체가 할당되어야만 한다.
  1. @OneToOne 에서 null 은 연관 관계 해당 엔티티가 없는 것을 의미하고, 프록시 객체는 연관관계 해당 엔티티가 존재한다 볼 수 있다.
    • One-To-One 에서는 Lazy Loading이 잘 작동하지 않는다.
      • 이유는 null 값이 가능한 OneToOne의 경우(optional=true 상황) 프록시 객체로 감쌀 수 없기 때문이다.
      • 만약 null 값이 가능한 OneToOne 에 프록시 객체를 넣는다면, 이미 그 순간 결코 null 이 아닌 프록시 객체를 리턴하는 상태가 돼 버리기 때문이다.
      • 따라서 JPA 구현체는 기본적으로 One-To-One 관계에 Lazy 를 허용하지 않고, 즉시 값을 읽어 들인다.

optional: 데이터가 필수적(not null)인지, 아닌지(null) 여부 ( default: true )

  1. DB 관점에서 연관관계 주인인 엔티티에 매핑된 테이블에는 연결된 엔티티의 존재를 알 수 있는 컬럼이 존재한다.

  2. null 이 있으면 연관 엔티티가 없는것 이고, null 이 아니면 프록시 객체 설정이 가능하여 LAZY 처리가 가능한다.

  1. 반대로 연관관계 주인이 아닌 테이블에서는 컬럼 자체가 존재하지 않는다. 즉 해당 테이블만 읽어서는 연관관계 엔티티 존재 여부를 알 수 없다.
  2. 이런 경우 값이 있는지 없는 지 알 수 없기 때문에 null 을 넣기도 애매하고 null 이 아니라는전제가 없기 때문에 프록시를 만들기도 애매하다.
    • (프록시로 만들고 LAZY 로딩을 하니 데이터가 없어 null 을 리턴하겠다 이런 방식은 불가)
    • 또한 2번에서 언급했듯이 null 을 프록시로 감싸버리면 null 이 아닌 프록시 객체상태로 리턴하게 됨으로 null 값 처리가 불가능해진다.
  3. 따라서 @OneToOne 양방향 연관 관계에서 연관 관계의 주인이 아닌 쪽 엔티티를 조회할 때, Lazy로 동작할 수 없다.

@OneToMany 는 괜찮은가?

  1. 연관관계의 주인은 외래키를 관리하는 Many 쪽이다
  2. One 쪽에서는 다의 존재를 알 수 있는 방법이 없다.
  3. 하지만 Many 를 컬렉션으로 관리하기 때문에 null 을 표현할 방법이 있다(size).
  4. 무조건 프로시 객체를 만들어 놓고 조회시 빈 컬렉션을 리턴하면 표현이 가능하다.
    • (null 을 리턴하지 않고 null 과 같이 연관 관계가 없음을 표현할 size=0 이 존재하기 때문)

@ManyToOne 는 괜찮은가?

  1. Many 쪽은 컬럼 존재를 알 수 있기 때문에 처리 가능하다

@OneToOne 지연로딩 문제 해결책

  1. optional=false 설정을 통해 무조건 Lazy Loading 을 시킨다. 이를 통해 반드시 연관 시 값이 존재함이 보장되어야 한다.
    • PrimaryKeyJoin 의 경우에는 optional=false 일 경우에 데이터 저장 순서가 꼬여버린다. 정상적인 optional=false가 작동하려면 ForeignKey Join을 해야한다.
  2. fetch join 으로 같이 가져와 해결 가능
  3. 억지로 엔티티에 FK 롤 집어 넣어 해결
  4. @ElementCollection 을 통해 컬렉션을 사용하고 unique 조건으로 데이터가 오직 1개만 들어가게 만든다.
  5. OneToOne -> OneToMany + ManyToOne 분리 방법(좋은 방법은 아닌거 같다)

[참고 사이트]

https://jeong-pro.tistory.com/249

https://kwonnam.pe.kr/wiki/java/jpa/one-to-one

profile
클린코드 지향

1개의 댓글

comment-user-thumbnail
2025년 3월 2일

Thanks for sharing! The AI-powered Hear Me Out platform is a fascinating tool that leverages artificial intelligence to enhance user interaction. It's impressive how AI technology is making communication more engaging and intuitive.
By the way, if you're interested in AI and interactive experiences, check out this creative platform:
hearmeout online

답글 달기

관련 채용 정보