-
[백견불야일타] 5장 연간 관계 매핑SpringBoot/쇼핑몰 프로젝트 with JPA 2022. 9. 20. 03:52
5.1 연관 관계 매핑 종류
연간 관계의 매핑 종류
- 일대일(1:1): @OneToOne - ex) 장바구니 엔티티와 회원 엔티티는 일대일 매핑
- 일대다(1:N): @OneToMany - ex) 장바구니 엔티티와 장바구니 상품 엔티티는 일대다 매핑
- 다대일(N:1): @ManyToOne
- 다대다(N:N): @ManyToMany
방향성
- 엔티티를 매핑할 때는 방향성을 고려하는 것이 중요하다.
- 테이블에서 관계는 항상 양방향이지만, 객체에서는 단방향과 양방향이 존재한다.
5.1.1 일대일 단방향 매핑(장바구니 - 회원)
com.shop.entity.Cart.java
package com.shop.entity; import lombok.Getter; import lombok.Setter; import lombok.ToString; import javax.persistence.*; @Entity @Table(name = "cart") @Getter @Setter @ToString public class Cart { @Id @Column(name = "cart_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long id; // 회원 엔티티와 일대일로 매핑 @OneToOne // 매핑할 외래키를 지정. name 속성에는 매핑할 외래키의 이름을 설정 // name을 명시하지 않으면 JPA가 알아서 ID를 찾지만 컬럼명이원하는 대로 생성되지 않을 수 있음 @JoinColumn(name = "member_id") private Member member; }
- 장바구니(Cart) 엔티티를 만들고 회원 엔티티와 연관 관게 매핑을 설정
- 회원(Member) 엔티티에는 장바구니(Cart) 엔티티와 관련된 소스가 전혀 없다. 장바구니 엔티티가 일방적으로 회원 엔티티를 참조하고 있고, 장바구니와 회원은 1:1 매핑이며 장바구니 엔티티가 회원 엔티티를 참조하는 일대일 단방향 매핑이다.
- cart 테이블은 member_id 컬럼을 외래키(foreign key)로 갖는다. 테이블을 먼저 생성하는 쿼리문이 실행되고 member_id를 foreign key로 지정하는 쿼리문이 실행된다.
com.shop.repository.CartRepository.java
package com.shop.repository; import com.shop.entity.Cart; import org.springframework.data.jpa.repository.JpaRepository; public interface CartRepository extends JpaRepository<Cart, Long> { }
- JpaRepository를 상속받는 CartRepository 인터페이스를 생성
com.shop.entity.CartTest.java
package com.shop.entity; import com.shop.dto.MemberFormDto; import com.shop.repository.CartRepository; import com.shop.repository.MemberRepository; 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.crypto.password.PasswordEncoder; 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.assertEquals; @SpringBootTest @Transactional @TestPropertySource(locations = "classpath:application-test.properties") public class CartTest { @Autowired CartRepository cartRepository; @Autowired MemberRepository memberRepository; @Autowired PasswordEncoder passwordEncoder; // 영속성 컨테스트를 사용하기 위해 EntitiyManager 빈 주입 @PersistenceContext EntityManager em; public Member createMember(){ MemberFormDto memberFormDto = new MemberFormDto(); memberFormDto.setEmail("test@email.com"); memberFormDto.setName("홍길동"); memberFormDto.setAddress("서울시 마포구 합정동"); memberFormDto.setPassword("1234"); return Member.createMember(memberFormDto, passwordEncoder); } @Test @DisplayName("장바구니 회원 엔티티 매핑 조회 테스트") public void findCartAndMemberTest(){ Member member = createMember(); memberRepository.save(member); Cart cart = new Cart(); cart.setMember(member); cartRepository.save(cart); // 회원 엔티티와 장바구니 엔티티를 영속성 컨텍스트에 저장 후 엔티티 매니저로부터 강제로 flush()를 호출하여 데이터베이스에 반영 em.flush(); // JPA는 영속성 컨텍스트로부터 엔티티를 조회 후 영속성 컨텍스트에 엔티티가 없을 경우 데이터베이스를 조회함 // 실제 데이터베이스에서 장바구니 엔티티를 가지고 올 때 회원 엔티티도 같이 가지고오는지 보기 위해서 영속성 컨텍스트를 비워줌 em.clear(); // 저장된 장바구니 엔티티를 조회 Cart savedCart = cartRepository.findById(cart.getId()) .orElseThrow(EntityNotFoundException::new); // 처음에 저장한 member 엔티티의 id와 savedCart에 매핑된 엔티티의 id를 비교 assertEquals(savedCart.getMember().getId(), member.getId()); } }
- JPA는 영속성 컨텍스트에 데이터를 저장 후 트랜잭션이 끝날 때 flush()를 호출하여 데이터베이스에 반영
@OneToOne(fetch = FetchType.EAGER) @JoinColumn(name= "member_id") private Member member;
- 위의 사진과 같이 엔티티를 조회할 때 해당 엔티티와 매핑된 엔티티도 한 번에 조회하는 것을 '즉시 로딩' 이라고 한다. 일대일(@OneToOne, @ManyToOne) 로 매핑할 경우 즉시 로딩을 기본 Fetch 전략으로 설정한다. Cart.java 클래스에서 member 엔티티와 일대일 매핑 관계를 맺어줄 때 따로 옵션을 주지 않으면 위 코드와 같이 FetchType.EAGER(즉시 로딩)으로 설정하는 것과 동일하다.
5.1.2 다대일 단방향 매핑(장바구니 - 상품들)
com.shop.entity.CartItem.java
package com.shop.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @Table(name="cart_item") public class CartItem { @Id @GeneratedValue @Column(name="cart_item_id") private Long id; // 하나의 장바구니에는 여러 개의 상품을 담을 수 있으므로 다대일 관계로 매핑 @ManyToOne @JoinColumn(name="cart_id") private Cart cart; @ManyToOne @JoinColumn(name = "item_id") // 장바구니에 담을 상품의 정보를 알아야 하므로 상품 엔티티를 매핑해줌 // 하나의 상품은 여러 장바구니의 장바구니 상품으로 담길 수 있으므로 마찬가로 다대일 매핑 private Item item; // 같은 상품을 장바구니에 몇 개 담을지 저장 private int count; }
- @JoinColumn(name=" ") : 외래 키 매핑 (생략 가능), name 속성에는 매핑할 외래 키 이름 지정
다대일/일대다 양방향 매핑(장바구니 - 장바구니 상품)
- 다대일 단방향 매핑인 위에서의 예제에서는 장바구니 상품 엔티티가 장바구니를 참조하는 단방향 매핑이다. 장바구니 엔티티에 장바구니 상품 엔티티를 일대다 관계로 매핑을 해준다면 양방향 매핑이 된다.
com.shop.constant.OrderStatus.java
package com.shop.constant; public enum OrderStatus { ORDER, CANCEL }
- 주문의 상태를 나타내는 주문 엔티티
com.shop.entity.Order.java
package com.shop.entity; import com.shop.constant.OrderStatus; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "orders") // 정렬할 때 사용하는 "order" 키워드가 있기 때문에 "orders"로 지정 @Getter @Setter public class Order { @Id @GeneratedValue @Column(name = "order_id") private Long id; @ManyToOne @JoinColumn(name = "member_id") private Member member; // 한 명의 회원은 여러 번 주문을 할 수 있으므로 주문 엔티티 기준에서 다대일 단방향 매핑을 합니다. private LocalDateTime orderDate; // 주문일 // Enum 사용시 기본적으로 순서가 저장되는데, enum의 순서가 바뀔 경우 문제가 발생할 수 있으므로 EnumType.String옵션을 사용해서 // String으로 저장하기를 권장 @Enumerated(EnumType.STRING) private OrderStatus orderStatus; // 주문상태 private LocalDateTime regTime; private LocalDateTime updateTime; }
com.shop.entity.OrderItem.java
package com.shop.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Getter @Setter public class OrderItem { @Id @GeneratedValue @Column(name = "order_item_id") private Long id; @ManyToOne @JoinColumn(name = "item_id") private Item item; // 하나의 상품은 여러 주문 상품으로 들어갈 수 있으므로 주문 상품 기준으로 다대일 단방향 매핑 @ManyToOne @JoinColumn(name = "order_id") private Order order; // 한 번의 주문에 여러 개의 상품을 주문할 수 있으므로 주문 상품 엔티티와 주문 엔티티를 다대일 단방향 매핑 private int orderPrice; // 주문가격 private int count; // 수량 private LocalDateTime regTime; private LocalDateTime updateTime; }
- 주문 상품 엔티티는 장바구니 상품 엔티티(CartItem)와 거의 비슷
- 주문 상품 엔티티와 주문 엔티티의 단방향 매핑을 먼저 설정
- 주문 상품 엔티티 기준에서 다대일 매핑이었으므로 주문 엔티티 기준에서는 주문 상품 엔티티와 일대다 관계로 매핑
- 양방향 매핑에서는 '연관 관계 주인'을 설정해야 한다는 점이 중요
- 엔티티는 테이블과 다르다. 엔티티를 양방향 연관 관계로 설정하면 객체의 참조는 둘인데 외래키는 하나이므로 둘 중 누가 외래키를 관리할지를 정해야 함
- 연관 관계의 주인은 외래키가 있는 곳으로 설정
- 연관 관계의 주인이 외래키를 관리(등록, 수정, 삭제)
- 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy 속성의 값으로 연관 관계의 주인을 설정
- 주인이 아닌 쪽은 읽기만 가능
com.shop.entity.Order.java
@OneToMany(mappedBy = "order") // (1) private List<OrderItem> orderItems = new ArrayList<>(); // (2)
- 위의 내용을 적용
- Order 엔티티에 OrderItem 과 연관 관계 매핑을 추가
- OrderItem 엔티티에서 이미 다대일 단방향 매핑을 했으므로 양방향 매핑이 됨
- (1) - 주문 상품 엔티티와 일대다 매핑, 외래키(order_id)가 order_item 테이블에 있으므로 연관 관계의 주인은 OrderItem 엔티티임. Order 엔티티가 주인이 아니므로 "mappedBy" 속성으로 연관 관계의 주인을 설정함. 속성의 값으로 "order"를 적어준 이유는 OrderItem에 있는 Order에 의해 관리된다는 의미로 해석하면 됨. 즉 연관 관계의 주인의 필드인 order를 mappedBy의 값으로 세팅
- (2) - 하나의 주문이 여러 개의 주문 상품을 갖으므로 List 자료형을 사용해서 매핑
5.1.3 다대다 매핑하기
- 실무에서는 사용하지 않는 매핑 관계
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다를 표현할 수 없다
- 따라서 연결 테이블을 생성해서 다대다 관계를 일대다, 다대일 관계로 풀어냄
public class Item { @ManyToMany @JoinTable( name = "member_item", joinColumns = @JounColumn(name = "member_id"), inverseJoinColumns = @JoinColumn(name = "item_id") ) private List<Member> member; }
- 객체는 테이블과 다르게 컬렉션을 사용해서 다대다 관계를 표현할 수 있다.
- member 엔티티는 item을 리스트 형태로 가질 수 있으며, item 엔티티도 member를 리스트로 가질 수 있다.
- 현재 진행중인 쇼핑몰 프로젝트에서는 다대다 관계를 사용하지 않지만, 코드로 표현해 본다면 위의 코드와 같다.
- 다대다 매핑을 사용하지 않는 이유는 연결 테이블에는 컬럼을 추가 할 수 없기 때문이다. 또한 엔티티를 조회할 때 member 엔티티에서 item을 조회하면 중간 테이블이 있기 때문에 어떤 쿼리문이 실행될지 예측하기도 어렵다.
- 따라서 연결 테이블용 엔티티를 하나 생성한 후 일대다 다대일 관계로 매핑을 하면 된다.
5.2 영속성 전이
5.2.1 영속성 전이란 ?
- cascade - '작은 폭포', '폭포처럼 흐르다'
- 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션
- 이 때 부모는 One, 자식은 Many에 해당, 예를 들어 Order 엔티티가 삭제되었을 때 해당 엔티티와 연관되어 있는 OrderItem 엔티티가 함께 삭제 되거나, Order 엔티티를 저장 할 때 Order 엔티티에 담겨있던 OrderItem 엔티티를 한꺼번에 저장할 수 있다. 상태가 전파되는 모습을 폭포가 흐르는 모습으로 상상하면 떠올리기 쉽다.
- PERSIST - 부모 엔티티가 영속화될 때 자식 엔티티도 영속화
- MERGE - 부모 엔티티가 병합될 때 자식 엔티티도 병합
- REMOVE - 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 삭제
- REFRESH - 부모 엔티티가 refresh되면 연관된 자식 엔티티도 refresh
- DETACH - 부모 엔티티가 detach 되면 연관된 자식 엔티티도 detach 상태로 변경
- ALL - 부모 엔티티의 영속성 상태 변화를 자식 엔티티에 모두 전이
- 단일 엔티티에 완전히 종속적이고 부모 엔티티와 자식 엔티티의 라이프 사이클이 유사할 때 cascade 옵션을 활용하자.
com.shop.repository.OrderRepository.java
package com.shop.repository; import com.shop.entity.Order; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderRepository extends JpaRepository<Order, Long> { }
- 주문 엔티티를 저장하기 위해서 JpaRepository를 상속받는 OrderRepository 인터페이스를 생성
com.shop.entity.Order.java
// 부모 엔티티의 영속성 상태 변화를 자식 엔티티에 모두 전이하는 CascadeType.ALL 옵션을 설정 @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> orderItems = new ArrayList<>();
- @OneToMany 어노테이션에 cascade 옵션을 설정
com.shop.entity.OrderTest.java
package com.shop.entity; import com.shop.constant.ItemSellStatus; import com.shop.repository.ItemRepository; import com.shop.repository.OrderRepository; 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.test.context.TestPropertySource; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.EntityNotFoundException; import javax.persistence.PersistenceContext; import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @TestPropertySource(locations = "classpath:application-test.properties") @Transactional public class OrderTest { @Autowired OrderRepository orderRepository; @Autowired ItemRepository itemRepository; @PersistenceContext EntityManager em; public Item createItem(){ Item item = new Item(); item.setItemNm("테스트 상품"); item.setPrice(10000); item.setItemDetail("상세설명"); item.setItemSellStatus(ItemSellStatus.SELL); item.setStockNumber(100); item.setRegTime(LocalDateTime.now()); item.setUpdateTime(LocalDateTime.now()); return item; } @Test @DisplayName("영속성 전이 테스트") public void cascadeTest(){ Order order = new Order(); for(int i=0; i<3; i++) { Item item = this.createItem(); itemRepository.save(item); OrderItem orderItem = new OrderItem(); orderItem.setItem(item); orderItem.setCount(10); orderItem.setOrderPrice(1000); orderItem.setOrder(order); // 아직 영속성 컨텍스트에 저장되지 않은 orderItem 엔티티를 order 엔티티에 담아줌 order.getOrderItems().add(orderItem); } // order 엔티티를 저장하면서 강제로 flush를 호출하여 영속성 컨텍스트에 있는 객체들을 데이터베이스에 반영 orderRepository.saveAndFlush(order); // 영속성 컨텍스트의 상태 초기화 em.clear(); // 영속성 컨텍스트를 초기화했기 때문에 데이터베이스에서 주문 엔티티를 조회, select 쿼리문이 실행되는 것을 콘솔창에서 확인가능 Order savedOrder = orderRepository.findById(order.getId()) .orElseThrow(EntityNotFoundException::new); // itemOrder 엔티티 3개가 실제로 데이터베이스에 저장되었는지 검사 assertEquals(3, savedOrder.getOrderItems().size()); } }
- 주문 엔티티를 저장할 때 영속성 전이가 일어나는지 알아보는 테스트 코드
- 고객이 주문할 상품을 선택하고 주문할 때 주문 엔티티를 저장하면서 주문 상품 엔티티도 함께 저장되는 경우
5.2.2 고아 객체 제거하기
- 고아 객체 - 부모 엔티티와 연관 관계가 끊어진 자식 엔티티
- 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리 할 수 있음
- 고아 객체 제거 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 다른 곳에서도 참조하고 있는 엔티티인데 삭제하면 문제가 생긴다. OrderItem 엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면 이 기능을 사용하면 안 된다.
- @OneToOne, @OneToMany 어노테이션에서 옵션으로 사용하면 된다.
com.shop.entity.Order.java
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
- 고아 객체 제거를 사용하기 위해서 @OneToMany 어노테이션에 "orphanRemoval = true" 옵션을 추가
com.shop.entity.OrderTest.java
@Autowired MemberRepository memberRepository; // 주문 데이터를 생성해서 저장하는 메소드를 만듬 public Order createOrder() { Order order = new Order(); for (int i = 0; i < 3; i++) { Item item = this.createItem(); itemRepository.save(item); OrderItem orderItem = new OrderItem(); orderItem.setItem(item); orderItem.setCount(10); orderItem.setOrderPrice(1000); orderItem.setOrder(order); order.getOrderItems().add(orderItem); } Member member = new Member(); memberRepository.save(member); order.setMember(member); orderRepository.save(order); return order; } @Test @DisplayName("고아객체 제거 테스트") public void orPhanRemovalTest(){ Order order = this.createOrder(); // order 엔티티에서 관리하고 있는 orderItem 리스트의 0번째 인덱스 요소를 제거 order.getOrderItems().remove(0); em.flush(); }
- 주문 엔티티(부모 엔티티)에서 주문 상품(자식 엔티티)를 삭제했을 때 orderItem 엔티티가 삭제되는지 테스트 코드 작성
5.3 지연 로딩
com.shop.repository.OrderItemRepository.java
package com.shop.repository; import com.shop.entity.OrderItem; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderItemRepository extends JpaRepository<OrderItem, Long> { }
- 주문 데이터 저장 후 OrderItem 엔티티를 조회하기 위해 JpaRepository를 상속 받는 OrderItemRepository 인터페이스 생성
com.shop.entity.OrderTest.java
@Autowired OrderItemRepository orderItemRepository; @Test @DisplayName("지연 로딩 테스트") public void lazyLoadingTest(){ // 기존에 만들었던 주문 생성 메소드를 이용하여 주문 데이터 저장 Order order = this.createOrder(); Long orderItemId = order.getOrderItems().get(0).getId(); em.flush(); em.clear(); // (2) 영속성 컨텍스트의 상태 초기화 후 order 엔티티에 저장했던 주문 상품 아이디를 이용하여 // orderItem을 데이터베이스에서 다시 조회 OrderItem orderItem = orderItemRepository.findById(orderItemId) .orElseThrow(EntityNotFoundException::new); // orderItem 엔티티에 있는 order 객체의 클래스를 출력, Order 클래스가 출력되는 것을 확인할 수 있다. System.out.println("Order class : " + orderItem.getOrder().getClass()); }
- 주문 데이터를 먼저 데이터베이스에 저장하고, 저장한 주문 상품 데이터를 조회
Hibernate: select orderitem0_.order_item_id as order_it1_4_0_, orderitem0_.count as count2_4_0_, orderitem0_.item_id as item_id6_4_0_, orderitem0_.order_id as order_id7_4_0_, orderitem0_.order_price as order_pr3_4_0_, orderitem0_.reg_time as reg_time4_4_0_, orderitem0_.update_time as update_t5_4_0_, item1_.item_id as item_id1_2_1_, item1_.item_detail as item_det2_2_1_, item1_.item_nm as item_nm3_2_1_, item1_.item_sell_status as item_sel4_2_1_, item1_.price as price5_2_1_, item1_.reg_time as reg_time6_2_1_, item1_.stock_number as stock_nu7_2_1_, item1_.update_time as update_t8_2_1_, order2_.order_id as order_id1_5_2_, order2_.member_id as member_i6_5_2_, order2_.order_date as order_da2_5_2_, order2_.order_status as order_st3_5_2_, order2_.reg_time as reg_time4_5_2_, order2_.update_time as update_t5_5_2_, member3_.member_id as member_i1_3_3_, member3_.address as address2_3_3_, member3_.email as email3_3_3_, member3_.name as name4_3_3_, member3_.password as password5_3_3_, member3_.role as role6_3_3_ from order_item orderitem0_ left outer join item item1_ on orderitem0_.item_id=item1_.item_id left outer join orders order2_ on orderitem0_.order_id=order2_.order_id left outer join member member3_ on order2_.member_id=member3_.member_id where orderitem0_.order_item_id=?
- orderItem 엔티티 하나를 조회했을 뿐인데 order_item 테이블과 item, orders, member 테이블을 조인해서 한꺼번에 가져옴
- 일대일, 다대일로 매핑할 경우 기본 전략인 '즉시 로딩'을 통해 엔티티를 함께 가져옴
- 실제 비즈니스에서 매핑되는 엔티티의 개수는 훨씬 많다. 개발자는 쿼리가 어떻게 실행될지 예측할 수 없고 또한 사용하지 않는 데이터도 한꺼번에 조회하므로 성능 문제도 있을 수 있기 때문에 즉시 로딩은 실무에서 사용하지 않는다.
com.shop.entity.OrderItem.java
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id") private Item item; // 하나의 상품은 여러 주문 상품으로 들어갈 수 있으므로 주문 상품 기준으로 다대일 단방향 매핑 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private Order order; // 한 번의 주문에 여러 개의 상품을 주문할 수 있으므로 주문 상품 엔티티와 주문 엔티티를 다대일 단방향 매핑
- FetchType.LAZY (지연 로딩)으로 변경
com.shop.entity.OrderTest.java
@Autowired OrderItemRepository orderItemRepository; @Test @DisplayName("지연 로딩 테스트") public void lazyLoadingTest(){ // 기존에 만들었던 주문 생성 메소드를 이용하여 주문 데이터 저장 Order order = this.createOrder(); Long orderItemId = order.getOrderItems().get(0).getId(); em.flush(); em.clear(); // (2) 영속성 컨텍스트의 상태 초기화 후 order 엔티티에 저장했던 주문 상품 아이디를 이용하여 // orderItem을 데이터베이스에서 다시 조회 OrderItem orderItem = orderItemRepository.findById(orderItemId) .orElseThrow(EntityNotFoundException::new); // orderItem 엔티티에 있는 order 객체의 클래스를 출력, Order 클래스가 출력되는 것을 확인할 수 있다. System.out.println("Order class : " + orderItem.getOrder().getClass()); System.out.println("========================="); orderItem.getOrder().getOrderDate(); System.out.println("========================="); }
com.shop.entity.Cart.java
com.shop.entity.CartItem.java
com.shop.entity.Order.java
- Fetch 전략을 LAZY로 직접 설정
5.4 Auditing을 이용한 엔티티 공통 속성 공통화
- Spring Data Jpa 에서는 Auditing 기능을 제공하여 엔티티가 저장 또는 수정 될 때 자동으로 등록일, 수정일, 등록자, 수정자를 입력해줌
- Audit ( 감시하다 ) - 엔티티의 생성과 수정을 감시
- 이러한 공통 멤버 변수들을 추상 클래스로 만들고, 해당 추상 클래스를 상속받는 형태로 엔티티를 리팩토링
com.shop.config.AuditorAwareImpl.java
package com.shop.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); } }
- 현재 로그인한 사용자의 정보를 등록자와 수정자로 지정하기 위해서 AuditorAware 인터페이스를 구현한 클래스 생성
com.shop.config.AuditConfig.java
package com.shop.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 // JPA의 Auditing 기능 활성화 public class AuditConfig { // 등록자와 수정자를 처리해주는 AuditorAware를 빈으로 등록 @Bean public AuditorAware<String> auditorProvider(){ return new AuditorAwareImpl(); } }
- Auditing 기능을 사용하기 위해서 Config 파일 생성
com.shop.entity.BaseTimeEntity.java
package com.shop.entity; import lombok.Getter; import lombok.Setter; 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; // Auditing을 적용하기 위해서 어노테이션 추가 @EntityListeners(value = {AuditingEntityListener.class}) // 공통 매핑 정보가 필요할 때 사용하는 어노테이션으로 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공 @MappedSuperclass @Getter @Setter public abstract class BaseTimeEntity { @CreatedDate // 엔티티가 생성되어 저장될 때 시간을 자동으로 저장 @Column(updatable = false) private LocalDateTime regTime; @LastModifiedDate // 엔티티의 값을 변경할 때 시간을 자동으로 저장 private LocalDateTime updateTime; }
- 보통 테이블에 등록일, 수정일, 등록자, 수정자를 모두 넣어주지만 어떤 테이블은 등록자, 수정자를 넣지 않는 테이블도 있을 수 있습니다. 그런 엔티티는 BaseTimeEntity만 상속받을 수 있도록 BaseTimeEntity 클래스 작성
com.shop.entity.BaseEntity.java
package com.shop.entity; import lombok.Getter; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; @EntityListeners(value = {AutoCloseable.class}) @MappedSuperclass @Getter public abstract class BaseEntity { @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String modifiedBy; }
- BaseEntity는 위에서 만든 BaseTimeEntity를 상속받고 있다. 등록일, 수정일, 등록자, 수정자를 모두 갖는 엔티티는 BaseEntity를 상속받으면 된다.
com.shop.entity.Member.java
public class Member extends BaseEntity{ ..코드 생략 .. }
- Member 엔티티에 Auditing 기능을 적용하기 위해서 BaseEntity 클래스를 상속받도록 작성
com.shop.entity.MemberTest.java
package com.shop.entity; import com.shop.repository.MemberRepository; 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; @SpringBootTest @Transactional @TestPropertySource(locations="classpath:application-test.properties") public class MemberTest { @Autowired MemberRepository memberRepository; @PersistenceContext EntityManager em; @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()); } }
- 회원 엔티티 저장 시 자동으로 등록자, 수정자, 등록시간, 수정시간이 저장되는지 테스트코드 작성
public class Cart extends BaseEntity {}
com.shop.entity.CartItem.java
com.shop.entity.Item.java
com.shop.entity.Order.java
com.shop.entity.OrderItem.java
- 나머지 엔티티도 BaseEntity를 상속받도록 수정
- 엔티티에 등록 시간(regTime)과 수정 시간(updateTime)이 멤버 변수로 있었다면 삭제 후 상속
'SpringBoot > 쇼핑몰 프로젝트 with JPA' 카테고리의 다른 글
[백견불야일타] 6장 상품 등록 및 조회하기 (0) 2022.10.08 [백견불야일타] 4장 스프링 시큐리티를 이용한 회원 가입 및 로그인 (0) 2022.09.07 [백견불야일타] 3장 Thymeleaf 학습하기 (0) 2022.08.26 [백견불야일타] 2장 Spring Data JPA (0) 2022.08.19 [백견불여일타] 1장 개발환경구축 (0) 2022.08.16