Programming/Java

JPA @OneToOne 일대일 연관 관계 정리 및 LazyLoding 이슈

Jan92 2022. 10. 2. 22:36

@OneToOne

JPA를 사용하면서도 연관 관계 매핑을 쓰지 않다가 이번 프로젝트에서 연관 관계를 적용하기 시작하며 정리한 내용이며, JPA 연관 관계 매핑 중에서 1:1 연관 관계인 @OneToOne에 대해 정리한 내용입니다.

 

 

@OneToOne

일대다(1:N), 다대일(N:1) 관계에서는 다(N) 쪽이 항상 외래 키를 가지고 있지만, 일대일(1:1) 관계에서는 주 테이블이나 대상이 되는 테이블 양쪽 모두 외래 키를 가질 수 있습니다. 때문에 일대일 관계를 적용할 때는 주 테이블과 대상이 되는 테이블, 어느 쪽에 외래 키를 둘지 선택해야 하는데요.

 

JPA에서는 외래 키를 갖는 쪽이 연관 관계의 주인이 되고, 연관 관계의 주인이 데이터베이스 연관 관계와 매핑되어 외래 키를 관리(등록, 수정, 삭제)할 수 있기 때문에 해당 설정이 중요합니다.

 

/*

데이터베이스는 컬렉션을 담을 수 없기 때문에 일대다, 다대일의 경우 일이 되는 쪽에서 외래 키를 가지는 것이 불가능합니다.

따라서 해당 경우에는 항상 다가 되는 쪽에서 외래 키를 가지게 되는 것입니다.

*/

 

이어지는 내용을 통해서는 1:1 연관 관계 매핑 @OneToOne이 적용되는 방식과 장단점, 그리고 이슈가 되는 부분을 살펴보겠습니다.

 

1-1. 일대일 연관 관계에서 외래 키가 주 테이블에 있는 경우 - 단방향

1-2. 일대일 연관 관계에서 외래 키가 주 테이블에 있는 경우 - 양방향

2-1. 일대일 연관 관계에서 외래 키가 대상 테이블에 있는 경우 - 양방향

(1:1 관계에서 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않습니다.)

 

 


 

 

1. 외래 키가 주 테이블에 있는 경우 

외래 키가 주 테이블(user)에 있는 경우

주 테이블 user / 대상 테이블 user_info (주 테이블인 user에 user_info_idx가 있는 경우)

 

이 경우는 많이 사용되는 객체인 주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾는 방식인데요.

주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인할 수 있다는 장점이 있지만, 만약 값이 없는 경우 외래 키에 Null을 허용해야 하는데, 이것은 DB 측면이나 비즈니스 로직의 검증 부분에서 봤을 때 좋지 않은 부분이 됩니다.

 

 

1-1. 일대일 단방향 (주 테이블에 외래 키가 있는 경우)

1:1 단방향

@Table(name = "user")
@Entity
public class User {

    ...

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_info_idx", referencedColumnName = "idx")
    private UserInfo userInfo;
}

(User Entity)

 

주 객체인 User Entity에 UserInfo 객체를 필드로 선언하고 @OneToOne 어노테이션을 적용하며, User 객체를 통해서 UserInfo 객체를 조회할 수 있는 구조입니다.

UserInfo 객체에 대한 로딩을 지연 로딩(LAZY)으로 설정하여 User를 조회하였을 때 UserInfo는 프록시 형태로 조회되며, 실제 UserInfo에 대한 조회가 필요할 때 쿼리를 날리게 됩니다.

 

/*

@JoinColumn 어노테이션의 namereferencedColumnName은 착각하지 않도록 주의해야 하는데요.

name은 단순하게 매핑할 외래 키의 이름, 즉 컬럼 명을 만들어주는 것이며, referencedColumnName은 해당 외래 키가 대상이 되는 테이블의 어떤 컬럼을 참조하는지를 지정해주는 것입니다. 따라서 name 같은 경우에는 마음대로 지정해주어도 되지만 referencedColumnName은 마음대로 지정했을 경우 예외가 발생하게 됩니다.

*/

 

 

1-2. 일대일 양방향 (주 테이블에 외래 키가 있는 경우)

1:1 양방향

@Table(name = "user")
@Entity
public class User {

    ...

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_info_idx", referencedColumnName = "idx")
    private UserInfo userInfo;
}

(User Entity)

 

@Table(name = "user_info")
@Entity
public class UserInfo {

    ...
    
    @OneToOne(mappedBy = "userInfo")
    private User user;
}

(UserInfo Entity)

 

양방향 매핑의 경우는 UserInfo Entity에 추가로 User 객체를 필드로 선언하고 @OneToOne 어노테이션을 적용합니다.

이때 @OneToOne 어노테이션에는 mappedBy 속성을 통해 연관 관계의 주인을 지정해줍니다.

(User 테이블이 외래 키를 가진 연관 관계의 주인이기 때문에 User Entity의 userInfo를 연관 관계의 주인으로 설정합니다.)

 

이 경우 UserInfo를 조회했을 때 User 객체가 지연 로딩(LAZY) 되도록 설정할 수는 있지만 실제로는 지연 로딩이 적용되지 않는데요.

프록시의 한계로 인해 외래 키를 직접 관리하지 않는 일대일 관계에서는 지연 로딩으로 설정해도 즉시 로딩이 적용되는데, 더 자세한 이유는 아래에서 다시 살펴보겠습니다.

 

/*

mappedBy의 경우 연관 관계의 주인을 지정해주는 속성으로, 연관 관계의 주인은 해당 속성을 갖지 않습니다.

*/

 

 

 

2. 외래 키가 대상이 되는 테이블에 있는 경우

외래 키가 대상이 되는 테이블(user_info)에 있는 경우

주 테이블 user / 대상 테이블 user_info (대상 테이블인 user_info에 user_idx가 있는 경우)

 

대상 테이블은 존재하는데 주 테이블이 존재하지 않는 경우는 없기 때문에 user_info 테이블의 user_idx 필드에 null이 허용될 필요가 없다는 장점이 있습니다.

또한 확장성을 고려하여 대상 테이블이 일대일(1:1)에서 일대다(1:N) 관계로 바뀌었을 때도 테이블 구조를 유지할 수 있는데요.

User 테이블과 매핑된 UserInfo 같은 테이블들이 계속 추가되더라도 User 테이블은 수정 없이 유지할 수 있다는 장점도 있습니다.

 

/*

일대다의 경우 다 쪽에서 무조건 외래 키를 가져야 하는데, 기존에 주 테이블에서 외래 키를 가진 1:1 관계에서 1:N가 되는 경우 테이블 구조가 변경되어야 하는 상황이 발생합니다.

*/

 

 

2-1. 일대일 양방향 (대상 테이블에 외래 키가 있는 경우)

1:1 양방향

(외래 키가 대상 테이블에 존재하지 않는 단방향 관계는 앞서 언급했던 것처럼 JPA에서 지원되지 않습니다.)

 

@Table(name = "user")
@Entity
public class User {

    ...
    
    @OneToOne(mappedBy = "user")
    private UserInfo userInfo;
}

(User Entity)

 

@Table(name = "user_info")
@Entity
public class UserInfo {

    ...
    
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_idx", referencedColumnName = "idx")
    private User user;
}

(UserInfo Entity)

 

UserInfo Entity의 User 객체를 필드로 선언하고 @OneToOne 어노테이션을 설정합니다.

User Entity에도 마찬가지로 UserInfo 객체를 필드로 선언하고 @OneToOne 어노테이션을 설정하는데요. User Entity의 UserInfo 필드에는 mappedBy 속성을 통해 연관 관계의 주인이 UserInfo에 있음을 알립니다.

 

 


 

 

@OneToOne FetchType.LAZY 적용이 안 되는 이슈(N+1 문제)

JPA 구현체인 Hibernate에서는 @OneToOne 양방향 매핑 시 지연 로딩으로 설정하여도 지연 로딩(LAZY)이 동작하지 않고, 즉시 로딩(EAGER)이 동작하는 이슈가 있는데요.

 

정확하게는 테이블을 조회할 때, 외래 키를 가지고 있는 테이블(연관 관계의 주인)에서는 외래 키를 가지지 않은 쪽에 대한 지연 로딩은 동작하지만, mappedBy 속성으로 연결된 외래 키를 가지지 않은 쪽에서 테이블을 조회할 경우 외래 키를 가지고 있는 테이블(연관 관계의 주인)에 대해서는 지연 로딩이 동작하지 않고 N+1 쿼리가 발생하게 되는 것입니다.

 

해당 이슈는 JPA의 구현체인 Hibernate에서 프록시 기능의 한계로 인해 발생하는데요.

user 테이블이 외래 키인 user_info_idx를 가지고 있는 연관 관계의 주인이고, user_info 테이블은 외래 키가 없는 경우를 예시로 생각했을 때, user_info 테이블의 입장에서는 user 테이블에 대한 외래 키가 없기 때문에 UserInfo Entity 입장에서는 UserInfo에 연결되어 있는 user가 null인지 아닌지를 조회해보기 전까지는 알 수 없습니다.

이처럼 UserInfo에 연결된 user 객체가 null 인지 여부를 알 수 없기 때문에 Proxy 객체를 만들 수 없는 것이며, 때문에 무조건 연결된 user가 있는지 여부를 확인하기 위한 쿼리가 실행되는 것입니다.

 

(UserInfo Entity를 조회했을 때 연결된 User Entity에 대해서 무조건 즉시 로딩이 적용되며, 따라서 N+1 조회가 발생하게 되는 것)

 

 

 

 

< 참고 자료 >

 

Spring Boot + JPA step-08: OneToOne 관계 설정 팁 | Popit

그동안 개인 블로그에 포스팅 해왔던 Spring Boot + JPA를 앞으로는 Popit에도 진행하게 되었습니다. 이전 포스팅에 대해는 아래 목차를 참조해주세요. 목차 step-01 : Account 생성, 조회, 수정 API를 간단

www.popit.kr

 

[JPA] @OneToOne 관계에서 N + 1 발생하는 이유가 무엇일까?

@OneToOne 관계에서 N + 1이 발생하는 이유가 무엇일까? 이번 글에서는 @OneToOne 관계에서 N + 1 문제가 발생하는 원인이 무엇인지 해결하기 위해서는 어떤 대안들이 있는지 정리해보겠습니다. OneToOne

devlog-wjdrbs96.tistory.com