Book/스프링부트 핵심가이드

연관관계 매핑 (of. 일대일(1:1) 매핑)

블로그 주인장 2023. 11. 14.

연관관계 매핑

RDBMS를 사용할 때는 테이블 하나만 사용해서 애플리케이션의 모든 기능을 구현하는 것은 어렵다.

JPA를 사용하는 애플리케이션에서도 테이블의 연관관곌르 엔티티 간의 연관관계로 표현이 가능하다.

그 중에서 일대일(1:1) 매핑 방식에 대해 알아보겠습니다.


연관관계 매핑 종류와 방향

  • One To One : 일대일(1:1)

 

연관관계 이해


[예시] 재고관리시스템

  1. 재고로 등록되어 있는 상품 엔티티에는 공급업체의 정보 엔티티가 매핑되어있다.
  2. 공급업체 입장에서는 한 가게에 납품하는 상품이 여러 개가 있을 수 있으므로 상품 엔티티와 일대다 관계가 된다.
  3. 상품 입장에서 보면 하나의 공급업체에 속하게 되므로 다대일 관계가 된다.

상품 테이블과 공급업체 테이블의 관계

  • 데이터베이스에서 두 테이블의 연관관계를 설정하면 외래키를 통해 서로 조인해서 참조하는 구조로 생성
  • JPA를 사용하는 객체지향 모델링에서는 엔티티 간의 참조 방향을 설정할 수 있다.
    • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
    • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식이다.
  • 연관관계를 설정하면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 된다.
  • 주인(Owner) : 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행할 수 있다.

 

일대일 단방향 매핑


상품 테이블과 상품 정보 테이블의 일대일 관계

 

상품 정보 엔티티

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "product_detail")
public class ProductDetail extends BaseEntity{
    @Id
    @GeneratedValue
    private Long id;

    private String description;

    @OneToOne
    @JoinColumn(name = "product_number")
    private Product product;
}
  • @OneToOne 어노테이션
    • 다른 엔티티 객체를 필드로 정의했을 시에, 일대일 연관관계로 매핑하기 위해 사용된다.
  • @JoinColumn 어노테이션
    • 매핑할 외래키를 지정한다.
    • 기본값이 설정되어 있어 자동으로 이름을 매핑하지만 의도한 이름이 들어가지 않기 때문에, name 속성을 이용하여 원하는 컬럼명을 지정하는 것이 좋다.
    • @JoinColumn을 선언하지 않으면 엔티티를 매핑하는 중간 테이블이 생기면서 관리 포인트가 늘어나서 좋지 않다.
  • @JoinColumn 어노테이션이 사용할 수 있는 속성
    • name : 매핑할 외래키의 이름을 설정
    • referencedColumnName : 외래키가 참조할 상대 테이블의 컬럼명을 지정한다.
    • foreignKey : 외래키를 생성하면서 지정할 제약 조건을 설정(unique, nullable, insertable, updatable ...)

 

상품 정보 리포지토리 인터페이스

  • 생성한 상품 정보 엔티티를 활용할 수 있도록 리포지토리를 생성한다.
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
    
}

 

연관관계 테스트 코드

@SpringBootTest
class ProductDetailRepositoryTest {

    @Autowired
    ProductDetailRepository productDetailRepository;

    @Autowired
    ProductRepository productRepository;

    @Test
    void saveAndReadTest1() {
        Product product = Product.builder()
                .name("Springboot Jpa")
                .price(5000)
                .stock(500)
                .build();

        productRepository.save(product);

        ProductDetail productDetail = new ProductDetail();
        productDetail.setProduct(product);
        productDetail.setDescription("springboot + jpa");

        productDetailRepository.save(productDetail);

        //생성 데이터 조회
        System.out.println("product: " + productDetailRepository.findById(
                productDetail.getId()).get().getProduct());

        System.out.println("productDetail: " + productDetailRepository.findById(
                productDetail.getId()).get());
    }
}
  • 테스트 코드를 실행하기 위해서는 상품과 상품정보에 매핑된 리포지토리에 대한 의존성을 주입받아야한다.
  • 그리고, 테스트 상에서 조회할 엔티티 객체를 저장한다.
  • 일대일 단방향 연관관계를 설정했기 때문에, Repository에서 detail 객체를 조회 후에 연관 매핑된 product 객체를 조회할 수 있다.
    Hibernate: 
        select
            productdet0_.id as id1_1_0_,
            productdet0_.created_at as created_2_1_0_,
            productdet0_.updated_at as updated_3_1_0_,
            productdet0_.description as descript4_1_0_,
            productdet0_.product_number as product_5_1_0_,
            product1_.number as number1_0_1_,
            product1_.created_at as created_2_0_1_,
            product1_.updated_at as updated_3_0_1_,
            product1_.name as name4_0_1_,
            product1_.price as price5_0_1_,
            product1_.stock as stock6_0_1_ 
        from
            product_detail productdet0_ 
        left outer join
            product product1_ 
                on productdet0_.product_number=product1_.number 
        where
            productdet0_.id=?​
  • select 구문을 보면 productDetail과 product 객체가 동시 조회된다.
  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것을 뜻한다.
  • left outer join : @OneToOne 어노테이션 인터페이스 사용으로 인해 발생

 

@OneToOne 어노테이션 인터페이스

public @interface OneToOne {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.EAGER;

    boolean optional() default true;

    String mappedBy() default "";

    boolean orphanRemoval() default false;
}
  1. 기본 fetch 전략으로 즉시 로딩 전략(EAGER) 이 채택된 것을 볼 수 있다.
  2. optional() 메서드는 기본적으로 true로 설정되어 있다.
    • 기본값이 true인 상태는 매핑되는 값이 Nullable이라는 것을 의미한다.
    • 반드시 값이 있어야 한다면 엔티티의 속성값을 변경해줘야한다.  👉 @OneToOne(optional = false)
  3. @OneToOne(optional = false) 지정한 경우에는 left out join 👉 left inner join 으로 바뀌어서 실행된다.

 

일대일 양방향 매핑


 

양방향 매핑을 위해 엔티티 추가

  • 객체에서의 양방향 개념은 양쪽에서 단방향으로 서로 매핑하는 것을 의미한다.
public class Product extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long number;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    @OneToOne
    private ProductDetail productDetail;
}
  • 양쪽에서 외래키를 가지고 있는 경우에는 join이 2번 발생되는 효율성이 떨어지는 경우가 발생한다.
  • 실제 데이터베이스에서도 테이블 간 연관관계를 맺으면 한쪽 테이블이 외래키를 가지는 구조로 이루어진다.

 

mappedBy 속성을 이용한 엔티티 클래스

  • 실제 데이터베이스의 연관관계를 반영해서, 한쪽의 테이블에서만 외래키를 바꿀 수 있도록 정의하는 것이 좋다.
  • 엔티티는 양방향으로 매핑하되 한쪽에게만 외래키를 줘야하는데, 이 때 사용하는 것이 mappedBy이다.
  • mappedBy는 어떤 객체가 주인인지 표시하는 속성이라고 볼 수 있다.
public class Product extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long number;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    @OneToOne(mappedBy = "product")
    private ProductDetail productDetail;
}

 

반응형

댓글