⚡️ 노션에 정리한 내용
https://www.notion.so/9-JPA-Hibernate-Mappings-286fc7eaad36490ba96cfe3fab99aba5?pvs=4
일대일 단방향 매핑 @OneToOne
- 회원(Member) 엔티티와 장바구니(Cart) 엔티티 매핑 설정
@Entity
@Table(name = "cart")
@Data
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_id")
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
- 회원 엔티티와 일대일로 매핑
- @JoinColumn 어노테이션을 사용해 매핑할 외래키 지정
- 회원 엔티티에는 장바구니 엔티티와 관련된 소스 x
- 장바구니 엔티티가 일방적으로 회원 엔티티 참조 (= 일대일 단방향 매핑)
테스트 코드
- CartRepository 만든 후 테스트 코드 작성
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class CartTest {
@Autowired
CartRepository cartRepository;
@Autowired
MemberRepository memberRepository;
@Autowired
PasswordEncoder passwordEncoder;
@PersistenceContext
EntityManager em;
// 회원 엔티티 생성
public Member createMember() {
MemberFormDTO memberFormDTO = MemberFormDTO.builder()
.name("test")
.email("test@test.com")
.password("1234")
.address("123 st")
.build();
return Member.createMember(memberFormDTO, passwordEncoder);
}
@Test
@DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")
public void findCartAndMemberTest() {
// 1. Member 객체 생성 및 저장
Member member = createMember();
memberRepository.save(member);
// 2. Cart 객체 생성 및 설정
Cart cart = new Cart();
cart.setMember(member);
cartRepository.save(cart);
// 3. 영속성 컨텍스트 플러시 및 초기화
em.flush();
em.clear();
// 4. Cart 조회 및 검증
Cart savedCart = cartRepository.findById(cart.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(savedCart.getMember().getMemberId(), member.getMemberId());
}
}
- JPA는 영속성 컨텍스트에 데이터를 저장 후 트랜잭션이 끝날 때 flush() 호출하여 DB에 반영
- em.flush()는 현재 영속성 컨텍스트의 변경 사항을 데이터베이스에 반영
- em.clear()는 영속성 컨텍스트를 초기화하여, 현재 영속성 컨텍스트에 저장된 모든 엔티티를 비움. 이를 통해 새롭게 조회할 때 데이터베이스에서 직접 정보를 가져올 수 있다.
- 조회된 Cart가 없으면 EntityNotFoundException을 발생
💡 여기서 드는 의문! 꼭 Entity Manager를 사용해야 할까? ➡️ repository만 사용해서 테스트 할 수 있음!
단, EntityManager를 사용하는 것은 엔티티의 영속성 컨텍스트를 직접 제어하고, 데이터베이스와의 동기화를 정확히 맞추는 데 유리함. 따라서 기본적인 테스트에는 레포지토리만 사용해도 되지만, 더 정밀한 테스트나 상태 관리를 위해 EntityManager를 사용할 수도 있다.
⚡️ 테스트에서 EntityManager의 사용하는 이유
1. 영속성 컨텍스트 초기화 : 테스트가 완료된 후, EntityManager의 clear() 메서드를 호출하여 영속성 컨텍스트를 초기화. 이는 테스트가 독립적으로 실행될 수 있도록 보장함.
2. 최신 상태 조회 : EntityManager를 사용하여 영속성 컨텍스트의 캐시를 지우고, 데이터베이스에서 최신 상태를 직접 조회할 수 있다. 이는 테스트가 데이터베이스의 실제 상태를 반영하도록 함.
3. 데이터 일관성 보장 : EntityManager를 통해 데이터베이스와 엔티티의 상태를 동기화함으로써 테스트의 정확성을 높임.
다대일 단방향 매핑 @ManyToOne
- 장바구니에는 여러 개의 상품을 담을 수 있고, 같은 상품을 여러 개 주문 할 수도 있음!
@Entity
@Table(name = "cart_item")
@Data
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@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;
}
다대일/일대다 양방향 매핑
- 주문과 주문 상품을 양방향 매핑으로 설정
public enum OrderStatus {
ORDER, CANCEL
}
@Entity
@Table(name = "orders")
@Data
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate; // 주문일
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; // 주문상태
@OneToMany(mappedBy = "order",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
}
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@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;
private int orderPrice; // 주문가격
private int count; // 수량
}
- 양방향 매핑에서는 연관 관계의 주인을 설정해야 함.
- ORDER_ID를 외래키로 조인하면 주문에 속한 상품이 어떤 상품들이 있는지 알 수 있고, 주문 상품은 어떤 주문에 속하는지 알 수 있다. 즉, 테이블은 외래키 하나로 양방향 조회가 가능!
- mappedBy 값으로 order를 적어준 이유는 OrderItem 에 있는 Order에 의해 관리된다는 의미
✓ 연관 관계의 주인은 외래키가 있는 곳으로 설정
✓ 연관 관계의 주인이 외래키를 관리(등록,수정,삭제)
✓ 주인이 아닌 쪽은 연관 관계 매핑시 mappedBy 속성 값으로 연관 관계의 주인을 설정
✓ 주인이 아닌 쪽은 읽기만 가능
영속성 전이 테스트
- OrderRepository 생성 후 주문 엔티티에서 @OneToMany 어노테이션에 cascade 옵션 설정
- 테스트 코드 설정
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class OrderRepositoryTest {
@Autowired
OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@PersistenceContext
EntityManager em;
// 주문할 상품 생성
public Item createItem() {
Item item = Item.builder()
.itemNm("test")
.price(10000)
.stockNum(100)
.itemDetail("detail")
.itemSellStatus(ItemSellStatus.SELL)
.build();
return item;
}
@Test
@DisplayName("영속성 전이 테스트")
public void cascadeTest() {
// Order 객체를 생성
Order order = new Order();
for (int i=0; i < 3; i++) {
// 아이템 생성 및 저장
Item item = this.createItem();
itemRepository.save(item);
// 주문 아이템 생성
OrderItem orderItem = OrderItem.builder()
.item(item)
.order(order)
.orderPrice(1000)
.count(10)
.build();
order.getOrderItems().add(orderItem);
}
// 주문 저장 및 플러시
orderRepository.saveAndFlush(order);
// 엔티티 매니저 초기화
em.clear();
// 저장된 주문 조회
Order savedOrder = orderRepository.findById(order.getId())
.orElseThrow(EntityNotFoundException::new);
// savedOrder의 OrderItem 수가 3개인지 검증
assertEquals(3, savedOrder.getOrderItems().size());
}
}
고아 객체 제거
- 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 고아 객체라고 함.
- 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있다.
- 참조하는 곳이 하나일 때만 사용가능
- @OneToOne, @OneToMany 어노테이션에서 옵션으로 사용 "orphanRemoval = true"
- 주문 엔티티에서 주문 상품을 삭제했을 때 orderItem 엔티티가 삭제되는지 테스트
orphanRemoval의 동작 원리
• 자동 삭제: orphanRemoval = true를 설정하면, 부모 엔티티에서 자식 엔티티를 제거하면 자식 엔티티가 데이터베이스에서도 자동으로 삭제.
• 영속성 컨텍스트: 이 설정은 영속성 컨텍스트의 상태와 데이터베이스의 상태를 동기화하는 데 유용. 자식 엔티티가 부모 엔티티와의 관계에서 제거될 때, 해당 자식 엔티티를 삭제하여 데이터베이스에 불필요한 데이터가 남지 않도록 함.
orphanRemoval 사용 시 주의 사항
1. 무결성: 자식 엔티티가 다른 엔티티와 관계가 있을 때, orphanRemoval이 무결성을 위협할 수 있다. 예를 들어, 자식 엔티티가 다른 부모와 연결되어 있거나 다른 엔티티에 의해 참조되는 경우에는 주의가 필요.
2. 성능: orphanRemoval을 사용할 때, 자식 엔티티가 많을 경우 성능에 영향을 미칠 수 있다. 특히 대규모 데이터를 다룰 때 성능을 고려해야 함.
3. 양방향 관계: 양방향 관계에서 orphanRemoval을 사용할 때는 자식 엔티티가 두 부모 중 하나에서만 관리되도록 주의해야 함. 이 설정을 잘못 사용할 경우, 데이터베이스 상태와 애플리케이션 상태 간의 불일치가 발생할 수 있다.
@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 = OrderItem.builder()
.item(item)
.order(order)
.orderPrice(1000)
.count(10)
.build();
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.getOrderItems().remove(0);
em.flush();
}
https://velog.io/@yuseogi0218/JPA-CascadeType.REMOVE-vs-orphanRemoval-true
엔티티 공통 속성 공동화
- 보통 등록시간(regDate), 수정시간(modDate) 멤버변수가 공통으로 들어가는 경우가 빈번.
- Spring Data Jpa 에서는 Auditing 기능을 제공하여 엔티티가 저장 또는 수정될 때 자동으로 등록일, 수정일, 등록자, 수정자를 입력해줌.
- Audit 의 사전적 정의는 '감시하다' 이다. 즉, 엔티티의 생성과 수정을 감시하고 있는 것.
1. AuditorAware 인터페이스 구현
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 현재 SecurityContext에서 인증 객체를 가져옴
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = "";
if(authentication != null){
userId = authentication.getName();
}
return Optional.of(userId);
}
}
- getCurrentAuditor() 메서드는 현재 인증된 사용자의 정보를 반환. JPA의 감시 어노테이션(@CreatedBy, @LastModifiedBy)이 이 메서드를 호출하여 엔티티의 생성자나 수정자를 자동으로 설정하는 데 사용
- SecurityContextHolder는 현재 스레드에서 인증 정보를 관리
- Optional은 null 값을 처리하는 안전한 방법을 제공
2. Auditing 기능을 사용하기 위해 Config 파일 생성
@Configuration
@EnableJpaAuditing // JPA의 Auditing 기능을 활성화함
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
}
- AuditConfig 클래스는 JPA 감시 기능을 활성화하고, 현재 감시자의 정보를 제공하는 AuditorAware 빈을 설정하는 역할
3. BaseTimeEntity / BaseEntity
- 등록자, 수정자를 넣지 않아도 되는 테이블은 BaseTimeEntity만 상속받고 모두 필요한 경우는 BaseEntity를 상속받도록 설정함.
- Member 엔티티에 Auditing 기능을 적용하기 위해 BaseEntity 클래스를 상속받도록 함.
@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter
@Setter
public abstract class BaseTimeEntity {
@CreatedDate
@Column(name = "register_date", updatable = false)
private LocalDateTime regDate; // 등록시간
@LastModifiedDate
@Column(name = "modify_date")
private LocalDateTime modDate; // 수정시간
}
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy; // 등록자
@LastModifiedBy
private String modifiedBy; // 수정자
}
📍 테스트 코드
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberTest {
@Autowired
MemberRepository memberRepository;
@PersistenceContext
EntityManager em;
@Test
@DisplayName("Auditing 테스트")
@WithMockUser(username = "dora", roles = "USER")
public void auditingTest() {
Member tempMember = new Member();
memberRepository.save(tempMember);
em.flush();
em.clear();
Member member = memberRepository.findById(tempMember.getMemberId())
.orElseThrow(EntityNotFoundException::new);
System.out.println("register time : " + member.getRegDate());
System.out.println("update time : " + member.getModDate());
System.out.println("create member : " + member.getCreatedBy());
System.out.println("modify member : " + member.getModifiedBy());
}
}
- @WithMockUser 어노테이션 : Spring Security의 테스트 지원 기능으로, 테스트 중에 인증된 사용자를 모의(mock)하여 보안 관련 기능을 테스트할 수 있다. 여기서는 username을 "dora"로 설정하고 roles를 "USER"로 설정.
https://velog.io/@onetuks/WithMockUser-WithUserDetails-WithSecurityContext
// 실행 결과
Hibernate:
insert
into
member
(address, created_by, email, modify_date, modified_by, name, password, register_date, role, member_id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate:
select
m1_0.member_id,
m1_0.address,
m1_0.created_by,
m1_0.email,
m1_0.modify_date,
m1_0.modified_by,
m1_0.name,
m1_0.password,
m1_0.register_date,
m1_0.role
from
member m1_0
where
m1_0.member_id=?
register time : 2024-08-02T21:54:44.335837
update time : 2024-08-02T21:54:44.335837
create member : dora
modify member : dora
내용 참고 : 책 '스프링 부트 쇼핑몰 프로젝트 with JPA'
'Spring & Spring Boot' 카테고리의 다른 글
Spring Boot 쇼핑몰 프로젝트 | 상품 수정 (0) | 2024.08.13 |
---|---|
Spring Boot 쇼핑몰 프로젝트 | 상품 등록 (0) | 2024.08.04 |
Spring Boot 쇼핑몰 프로젝트 | 로그인/로그아웃 화면 연동, 페이지 권한 설정 (0) | 2024.08.01 |
Spring Boot 쇼핑몰 프로젝트 | 로그인 기능 구현 (0) | 2024.08.01 |
Spring Boot 쇼핑몰 프로젝트 | 회원가입 구현 (0) | 2024.07.31 |