ERD

⚡️  노션에 정리한 내용

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

 

JPA - CascadeType.REMOVE vs orphanRemoval = true

CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보았습니다.

velog.io

 

 

 

엔티티 공통 속성 공동화


  • 보통 등록시간(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는 현재 스레드에서 인증 정보를 관리
    • Optionalnull 값을 처리하는 안전한 방법을 제공

 

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

 

SpringSecurity 테스트 어노테이션

Spring Security 프레임워크가 적용된 상태일때 쓰는 테스트용 어노테이션이다.Spring Security 에서 인증된 사용자를 Mock으로 만들어서 테스트를 수행할 수 있도록 한다.상황을 가정해보자.Spring Security

velog.io

 

// 실행 결과
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'

+ Recent posts