본문 바로가기
🛠 백엔드/쇼핑몰 클론코딩

[5] 연관매핑

by meteorfish 2022. 12. 30.
728x90

연관매핑이란?

Entity와 연관 관계를 매핑해두고, 해당 Entity와 관련된 Entity를 사용하는 것.

1.1 일대일 단방향 매핑

cart - member

  • 회원 한명 당 하나의 카트를 가지게 설정하려고 함.
  • 카트에 회원의 아이디를 저장하는 칼럼을 만든다.

-> Cart

@Entity
@Table(name = "cart")
@Getter @Setter
@ToString
public class Cart extends BaseEntity {

    @Id
    @Column(name = "cart_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    private Member member;

    public static Cart createCart(Member member){
        Cart cart = new Cart();
        cart.setMember(member);
        return cart;
    }
}
  • @OneToOne : 회원 엔티티와 일대일 매핑함
  • @JoinColumn : name 이름인 외래키로 지장할 칼럼을 지정

이렇게 카트와 회원을 연관매핑을 함.
이제 테스트 코드를 작성하기 위해 Repository 작성 후 테스트 코드를 작성해보자

-> CartTest

    @Test
    @DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")
    public void findCartAndMemberTest(){
        Member member = createMember();
        memberRepository.save(member);
        Cart cart = new Cart();
        cart.setMember(member);
        cartRepository.save(cart);

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

        Cart savedCart = cartRepository.findById(cart.getId())
                .orElseThrow(EntityNotFoundException::new);
        assertEquals(savedCart.getMember().getId(), member.getId());
    }
  • 위 테스트는 cart와 member가 연관매핑되어 있기 때문에, cart 조회 시 쿼리문이 연관 매핑된 member 또한 함께 조회하는 지 알아보려는 테스트이다.
  • 쿼리는 flush()시 DB에 반영되기 때문에 save가 제대로 되는지 실행한다.
  • JPA는 조회 요청이 오면 영속성 컨텍스트의 1차캐시에서 먼저 조회를 하기때문에, DB에서 조회를 위해 영속성 컨텍스트를 초기화한다.
  • orElseThrow() : findById를 통한 값이 없을 경우 다음과 같은 익셉션을 전달한다.
  • 카트에 저장된 회원과 저장하려고 했던 회원이 같은지 검사한다.
  • 위 처럼 한번에 관련된 모든 Entity를 조회하는 방식을 FetchType.EAGER 방식이라고 한다.

1.2 다대일 단방향 매핑

카트, 카트에 담긴 상품, 상품 간의 연관 매핑을 해보자.

  1. 하나에 Cart에는 여러개의 Cart_Item을 담을 수 있다.
  2. 하나의 Item에는 여러개의 Cart_Item에 들어갈 수 있다.

-> CartItem

    @Id
    @GeneratedValue
    @Column(name = "cart_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="cart_id")
    private Cart cart;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    private int count;
  • CartItem에 외래키로 cart_id와 item_id 칼럼이 추가된다.

1.3 다대일/일대다 양방향

member, orders, order_item 사이의 연관관계를 매핑해보겠다.

  • 하나의 member에는 여러개의 order가 들어갈 수 있다.
  • 하나의 order에는 여러개의 order_item이 들어 갈 수 있다.
  • 하나의 item은 여러 order_ite에 들어갈 수 있다.

-> order

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate; //주문일

    ...

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL
            , orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();

-> orderItem

    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
  • 양방향이란 단방향이 서로 매핑된 것을 말한다.
  • 현재 order에선 @ManyToOne으로 order_item을 단방향 매핑했고,
    order_item에선 @OneToMany를 이용해서 order와 매핑했다.

테이블은 외래키 하나로 Join을 사용해서 양방향 조회가 가능하다.

( Join : 2개의 테이블을 엮어서 원하는 데이터를 추출 )
JOIN 관련 )

( order_item의 외래키(order_id) 하나로 order_item과 order 둘 다 알 수 있다. )
또한, 양방향 매핑에선 연관 관계 주인을 설정해야한다.
( 연관 관계 주인은 두 엔티티에 관해 제어[삭제, 추가, 수정,,,]이 가능하고, 주인이 아닌 엔티티는 읽기만 가능 )

연관 관계 주인 설정하기

  • 연관 관계의 주인은 외래키가 있는 곳으로 설정
  • 연관 관계의 주인이 외래키를 관리
  • 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy를 통해 주인을 설정
  • 주인이 아닌 쪽은 읽기만 가능

따라서 주인이 아닌 쪽(order)에서 orderItem과 매핑시 mappedBy를 설정하면 끝!

-> Order

@OneToMany(mappedBy = "order") 
private List<OrderItem> orderItems = new ArrayList<>();

1.4 다대다 매핑

RDBMS에선 다대다 매핑을 표현할 수 없다.
따라서 다대다를 일대다 혹은 다대일 관계로 풀어낸다.

다대다 매핑을 사용하지 않는 이유

  1. 연결 테이블에는 칼럼을 추가 할 수 없다.
  2. 중간 테이블이 있는데 이를 이용하면 어떤 쿼리문이 실행될지 예측 쉽지 않음.

2.1 영속성 전이

영속성 전이 (cascade)

Entity 상태를 변경 시 연관된 Entity 상태 또한 변경 시키는 옵션

CASCADE 종류

  • 단일 Entity에 완전히 종속되거나 부모 Entity와 자식 Entity의 life-cycle이 유사할 때 사용을 추천

Order의 자식인 orderItem에게 영속성 전이를 해보자

-> Order

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

이제 정상적으로 영속성 전이가 되는지 테스트 코드를 작성하자

  • Order와 OrderItem 3개를 생성 후, Order에 OrderItem 3개를 넣는다.
  • ORder를 영속성 컨텍스트에 저장 후 flush한다.
  • Order를 DB에서 조회했을때 OrderItem 3개도 같이 조회되는지 검사한다.

2.2 고아 객체 제거

고아 객체란?

부모 엔티티와 관계가 끊어진 자식 엔티티

고악 객체 제거시 주의사항

참조하는 곳이 하나일때만 사용

  • @OneToOne,@OneToMany에서 사용

부모 엔티티(order)에서 자식 엔티티(orderItem) 어노테이션에 orphanRemoval 옵션을 설정

-> Order

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();

이제 테스트 코드에서 order 객체의 orderItems 필드의 첫번째 인덱스를 제거한다.
그렇게 되면 order_item은 order와의 연관관계가 제거 되어서 고아 객체가 되고 삭제되게 된다.

order.getOrderItems().remove(0); 을 하게 되면
orderItems의 0번째만 사라지는 것인가? orderItems가 다 사라지는 것인가?

3. 지연 로딩

fetch타입에는 즉시로딩(EAGER),지연 로딩(Lazy) 두 가지가 있다.

  • 즉시 로딩이 기본으로 설정되어 있다.
  • 즉시 로딩은 원치 않는 Entity도 한번에 로딩하기 때문에 성능 문제가 발생 할 수 있으므로 Lazy를 추천하다.

연관매핑을 지연 로딩으로 설정 후, 저번과 같은 테스트 코드를 실행해보자

orderItem.getOrder().getClass() 시, Order 객체가 아닌 프록시 객체가 반환되는 것을 볼 수 있다.
HibernateProxy는 실제 사용 시점에만 쿼리문을 실행한다.

4. Auditing

등록시간, 수정시간, 등록자, 수정자를 이용해서 버그를 수정하거나 변경된 대상을 찾을 수 있다.
Spring Data JPA에선 저장 또는 수정 시 자동으로 위를 바꿔주는 기능을 제공. 이를 Auditing이라고 함.

Auditing으로 공통 속성 공통화하기

  1. AuditorAware을 구현하는 구현체 생성 후, 정보를 추적하는 메서드 구현하기
  2. Auditing을 구현할 Config파일 생성 후, AuditorAware 빈 등록
  3. 등록일, 수정일만 가진 BaseTimeEntity 생성
  4. 등록자, 수정자까지 가진 BaseTimeEntity를 상속받는 BaseTime 객체 생성
  5. Auditing 기능을 사용할 엔티티에 상속 추가하기

1

-> AuditorAwareImpl

public class AuditorAwareImpl implements AuditorAware<String> {

    // 현재 로그인한 사용자 정보를 조회하여 사용자의 이름을 등록자와 수정자로 지정
    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String userId = "";
        if(authentication != null){
            userId = authentication.getName();
        }
        return Optional.of(userId);
    }
}

2

-> AuditConfig

@Configuration
@EnableJpaAuditing // JPA의 Auditing 기능 활성화
public class AuditConfig {

    @Bean
    public AuditorAware<String> auditorProvider(){ // AuditorAware을 빈으로 등록
        return new AuditorAwareImpl();
    }
}

3

-> BaseTimeEntity

@EntityListeners(value = {AuditingEntityListener.class}) // Auditing 적용하기 위해
@MappedSuperclass // 공통 매핑 정보가 필요할때 (상속받은 자식 클래스에 매핑 정보만 제공)
@Getter @Setter
public abstract class BaseTimeEntity {

    @CreatedDate // 엔티티가 생성되고 저장될때 시간을 자동으로 저장
    @Column(updatable = false)
    private LocalDateTime regTime;

    @LastModifiedDate // 엔티티 값을 변경할 때 시간을 자동으로 저장
    private LocalDateTime updateTime;
}

4

-> BaseTime

@EntityListeners(value={AuditingEntityListener.class})
@MappedSuperclass
@Getter
public abstract class BaseEntity extends BaseTimeEntity{

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String modifiedBy;
}

5

Member 엔티티에 Auditing 기능을 사용해보자

-> Member

@Entity
@Table(name="member")
@Getter @Setter
@ToString
public class Member extends BaseEntity{

...

이제 테스트 코드를 작성하고 member 엔티티가 저장 혹은 수정 시, 자동으로 공통 속성이 바뀌는지 확인해보자

    @Test
    @DisplayName("Auditing 테스트")
    @WithMockUser(username = "gildong", roles = "USER")
    public void auditingTest(){
        Member newMember = new Member();
        memberRepository.save(newMember);

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

        Member member = memberRepository.findById(newMember.getId()).orElseThrow(EntityNotFoundException::new);

        System.out.println("register time : " + member.getRegTime());
        System.out.println("update time : " + member.getUpdateTime());
        System.out.println("create member : " + member.getCreatedBy());
        System.out.println("modify member : " + member.getModifiedBy());
    }

테스트 코드 결과

적상적으로 통과하는 것을 볼수있다.

728x90

'🛠 백엔드 > 쇼핑몰 클론코딩' 카테고리의 다른 글

[2] JPA  (0) 2023.01.08
[7] Order  (0) 2023.01.07
[6] Item 등록 및 조회  (0) 2023.01.04
[4] Spring Security  (1) 2022.12.30
[3] Thymeleaf  (0) 2022.12.25