연관 관계 매핑3 - 지연로딩, Auditing 공통 속성 처리

진크·2022년 2월 27일
0
post-thumbnail

1. 지연 로딩

OrderItem을 조회하기 위해 JpaRepository를 상속 받는 OrderItemRepository 인터페이스를 생성합니다.

package me.jincrates.gobook.domain.orders;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
}
package me.jincrates.gobook.domain.orders;

//...기존 임포트 생략

@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class OrderTest {

	  //...기존 코드 생략

    @Autowired
    OrderItemRepository orderItemRepository;
    
    @Test
    @DisplayName("지연 로딩 테스트")
    public void lazyLoadingTest() {
        Order order = this.createOrder();
        Long orderItemId = order.getOrderItems().get(0).getId();
        em.flush();;
        em.clear();
        
        OrderItem orderItem = orderItemRepository.findById(orderItemId)
                .orElseThrow(EntityNotFoundException::new);
        System.out.println("Order class : " + orderItem.getOrder().getClass());
    }
}

테스트 코드에서 orderItem 데이터를 조회하면 콘솔창에서 엄청나게 긴 쿼리문을 볼 수 있습니다. orderItem 엔티티 하나를 조회했을 뿐인데, order_item 테이블과 item, orders, member 테이블을 조인해서 한꺼번에 가지고 오고 있습니다.

즉시 로딩을 사용하면 사용하지 않는 데이터도 한꺼번에 조회하므로 성능 이슈가 발생할 수 있기 때문에 지연 로딩 방식을 사용해야 합니다. 프로젝트 내에 모든 연관 관계를 FetchType.LAZY 방식으로 설정하겠습니다.

package me.jincrates.gobook.domain.orders;

//...임포트 생략

@Getter @ToString
@NoArgsConstructor
@Table(name = "order_item")
@Entity
public class OrderItem {

    //...코드 생략

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}
@Test
@DisplayName("지연 로딩 테스트")
public void lazyLoadingTest() {
    Order order = this.createOrder();
    Long orderItemId = order.getOrderItems().get(0).getId();
    em.flush();;
    em.clear();
    
    OrderItem orderItem = orderItemRepository.findById(orderItemId)
            .orElseThrow(EntityNotFoundException::new);
    System.out.println("Order class : " + orderItem.getOrder().getClass());
    System.out.println("========================================");
    orderItem.getOrder().getOrderDate();
    System.out.println("========================================");
}

2. Auditing을 이용한 엔티티 공통 속성 처리

엔티티를 설계하다보면 등록시간, 수정시간, 등록자, 수저장 등의 공통된 변수들이 있기 마련입니다. 매번 엔티티에 동일한 코드를 작성할 수도 있지만 Spring Data Jpa에서는 Auditing 기능을 제공하여 엔티티가 저장 또는 수정될 때 자동으로 등록일, 수정일, 등록자, 수정자를 입력해줍니다. 공통 멤버 변수들을 추상 클래스로 만들고 해당 추상 클래스를 상속받는 형태로 엔티티를 리팩토링하겠습니다.

AuditorAwareImpl

현재 로그인한 사용자의 정보를 등록자와 수정자로 지정하기 위해서 AuditorAware 인터페이스 구현한 클래스를 생성합니다.

package me.jincrates.gobook.config;

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

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);
    }
}
  • authentication.getName() : 현재 로그인한 사용자의 정보를 조회하여 사용자의 이름을 등록자와 수정자로 지정합니다.

Auditing Config

package me.jincrates.gobook.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class Auditing {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return new AuditorAwareImpl();
    }
}
  • 등록자와 수정자를 처리해주는 AuditorAware을 빈으로 등록합니다.

BaseTimeEntity 추상 클래스

package me.jincrates.gobook.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

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

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime regTime;

    @LastModifiedDate
    private LocalDateTime updateTime;
}
  • @EntityListeners : Auditing 적용
  • @MappedSuperclass : 공통 매핑 정보가 필요할 때 사용하는 어노테이션으로 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공합니다.
  • @CreatedDate : 엔티티가 생성되어 저장될 때 시간을 자동으로 저장합니다.
  • @LastModifiedDate : 엔티티의 값을 변경할 때 시간을 자동으로 저장합니다.

BaseEntity 추상 클래스

BaseEntity는 위에서 만든 BaseTimeEntity를 상속받습니다.

package me.jincrates.gobook.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

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

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

    @LastModifiedBy
    private String modifiedBy;
}

테스트 코드 작성

Member 엔티티가 BaseEntity 엔티티를 상속받도록 수정합니다.

package me.jincrates.gobook.domain.members;

//...기존 임포트 생략

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

    //...기존 코드 생략
}
package me.jincrates.gobook.domain.members;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class MemberTest {

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    @Test
    @DisplayName("Auditing 테스트")
    @WithMockUser(username = "mockUser", 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());
    }
}
  • @WithMockUser : 스프링 시큐리티에서 제공하는 어노테이션으로 @WithMockUser에 지정한 사용자가 로그인 상태라고 가정하고 테스트를 진행할 수 있습니다.

OrderItem, Cart, CartItem, Item, Order도 똑같이 BaseEntity를 상속받도록 수정합니다.

profile
철학있는 개발자 - 내가 무지하다는 것을 인정할 때 비로소 배움이 시작된다.

0개의 댓글