[JPA-4] JPA 일대일(1:1) @One-To-One 연관관계

1. 들어가며

이번 포스팅에서는 일대일 (1:1) 매핑에 대해서 알아보겠습니다.

2. 개발 환경

포스팅에서 작성한 코드는 깃허브에 올라가 있어요.

  • OS : Mac OS
  • IDE: Intellij
  • Java : JDK 1.8
  • Source code :

  • Software management tool : Maven

3. 일대일 (1:1) 연관관계

일대일 관계에서는 반대도 일대일 관계가 됩니다. 다대일 관계에서는 다(N)쪽이 항상 외래 키를 가지고 있지만, 일대일 관계에서는 주 테이블이나 대상 테이블에 외래 키를 둘 수 있어서 개발 시 어느 쪽에 둘지를 선택해야 합니다.

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

주 테이블에 외래 키가 있으면 주 객체에도 객체 참조를 두는 구조로 매핑을 하게 됩니다.

  • 주 테이블 : User

    • 외래 키(phone_id)가 있는 경우
  • 대상 테이블 : CelluarPhone
image 1

3.1.1 일대일 단방향

일대일 단방향으로 설정해보겠습니다. 주 객체인 User 엔티티에 @OneToOne 선언 이후 대상 테이블인 CellularPhone 객체를 선언합니다. User 객체를 통해서 사용자의 핸드폰 정보를 조회할 수 있는 구조입니다.

image 2
@Table(name = "user")
public class User extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id", unique = true, nullable = false)
    private Long id;

    private String username;
    ...(생략)...

    @OneToOne
    @JoinColumn(name = "id")
    private CellularPhone cellularPhone;
		...(생략)...
}
@Table(name = "cellular_phone")
public class CellularPhone extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "phone_id")
    private Long id;

    private String phoneNumber;

    private String model;
		...(생략)...
}

UserCellularPhone 객체를 저장하고 조회해보겠습니다.

@Test
public void save_user_phone() {
  CellularPhone cellularPhone = CellularPhone.builder()
    .model("android")
    .phoneNumber("010-2342-5234")
    .build();
  phoneRepository.save(cellularPhone);

  User user = User.builder()
    .name("Frank")
    .email("sdf@sdf.com")
    .username("id1234")
    .password("1234")
    .build();
  
  user.setCellularPhone(cellularPhone); //연관관계를 맺음

  userRepository.save(user);

  List<User> users = userRepository.findAll();
  assertThat(users.get(0).getName()).isEqualTo("Frank");
  assertThat(users.get(0).getCellularPhone().getPhoneNumber()).isEqualTo("010-2342-5234");
}

3.1.2 일대일 양반향

이제 양반향으로 설정해볼까요? CellularPhone 객체에도 User 객체를 가지도록 합니다.

image 4

CellularPhone 엔티티에 추가로 @OneToOne 어노테이션을 선언합니다. 그리고 양방향이므로 mappedBy 속성으로 연관 관계의 주인을 지정해줍니다. user 테이블에 외래 키를 가지고 있음으로 UsercellularPhone을 연관관계 주인으로 설정합니다.

@Table(name = "cellular_phone")
public class CellularPhone extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "phone_id")
    private Long id;
  	...(생략)...

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

Unit Test로 엔티티 저장후 조회해보겠습니다.

@Test
public void save_user_phone() {
  CellularPhone cellularPhone = CellularPhone.builder()
    .model("android")
    .phoneNumber("010-2342-5234")
    .build();

  User user = User.builder()
    .name("Frank")
    .email("sdf@sdf.com")
    .username("id1234")
    .password("1234")
    .build();
  user.setCellularPhone(cellularPhone);

  cellularPhone.setUser(user); //CellularPhone에서 User 객체 설정
  userRepository.save(user);
  phoneRepository.save(cellularPhone);

  List<User> users = userRepository.findAll();
  assertThat(users.get(0).getName()).isEqualTo("Frank");
  assertThat(users.get(0).getCellularPhone().getPhoneNumber()).isEqualTo("010-2342-5234");

  assertThat(cellularPhone.getUser().getName()).isEqualTo("Frank"); //CellularPhone 객체에서 User 정보 확인한다
}

주의사항

일대일 관계에서 지연 로딩으로 설정을 해도 즉시 로딩이 되는 경우가 있습니다. 예를 들면,

  • User.cellularPhone : 지연 로딩이 된다
  • CellularPhone.user : 지연 로딩이 안된다

    • 프록시의 한계로 인해서 외래 키를 직접 관리하지 않는 일대일 관계에서는 지연 로딩으로 설정을 해도 즉시 로딩이 된다

참고로 @OneToOne 어노테이션의 기본 fetch 타입은 즉시 로딩(EAGER)입니다.

@Test
public void 일대일관계에서_지연로딩_테스트() {
saveUserWithPhones(1, 1);

List<User> users = userRepository.findAll();
assertThat(users.get(0).getCellularPhone().getModel()).startsWith("android"); //(1).지연로딩으로 동작한다

List<CellularPhone> phones = phoneRepository.findAll(); //(2).User도 같이 조회된다.
}
  • (1) users.get(0).getCelluarPhone().getModel()을 호출 할때 SQL 구문이 실행되어 지연로딩이 잘 되는 것을 확인할 수 있다
  • (2) CelluarPhone.user는 지연로딩으로 설정되어 있지만, findAll() 호출시 즉시 로딩되는 것을 확인할 수 있다

3.2 대상 테이블에 외래 키가 있는 경우

외래 키가 주 테이블이 아니라 대상 테이블에 존재하는 경우에는 어떻게 달라지는 알아보겠습니다.

  • 주 테이블 : User
  • 대상 테이블 : CellularPhone

    • 외래 키(user_id)가 있는 경우
image 5

3.2.1 일대일 단방향

외래 키는 cellular_phone 테이블에 있고 아래와 같은 일대일 연관관계는 JPA에서 지원하지 않아 매핑할 수 없습니다.

image 2

3.2.2 일대일 양반향

image 4

대상 테이블인 celluar_phone에 외래 키를 두고 싶으면 아래와 같이 설정하면 됩니다. CellularPhone 엔티티에 @OneToOne 어노테이션으로 설정하고 User 엔티티에서는 @OneToOne 어노테이션과 mappedBy 속성으로 외래 키를 소유하고 있는 CellularPhoneuser를 연관관계 주인으로 지정합니다.

@Table(name = "cellular_phone")
public class CellularPhone extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "phone_id")
    private Long id;
		...(생략)...

    @OneToOne
    @JoinColumn(name = "id")
    private User user;
		...(생략)...
}
@Table(name = "user")
public class User extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id", unique = true, nullable = false)
    private Long id;
		...(생략)...
          
    @OneToOne(mappedBy = "user")
    private CellularPhone cellularPhone;
		...(생략)...
}

4. 참고

Loading script...