| 어노테이션 | 설명 |
|---|---|
@Entity | 이 클래스는 DB 테이블과 연결되는 엔티티이다. |
@Table(name = "테이블명") | 사용할 테이블 이름을 지정한다. |
@Id | 기본 키(PK) 를 지정한다. |
@GeneratedValue | 기본 키 값을 자동 생성한다. |
@Column(name = "컬럼명") | 필드와 DB 컬럼을 연결한다. |
@Column(nullable = false) | NULL을 허용하지 않는다. |
@Column(unique = true) | 중복을 허용하지 않는다. |
@Column(length = 100) | 문자열의 최대 길이를 지정한다. |
@ManyToOne | 다수의 엔티티가 하나의 엔티티를 참조한다. |
@OneToMany | 하나의 엔티티가 여러 엔티티를 관리한다. |
@OneToOne | 1:1 관계를 나타낸다. |
@ManyToMany | 다대다 관계를 나타낸다. (실무에서는 잘 사용하지 않음) |
@JoinColumn(name = "컬럼명") | 외래 키(FK) 컬럼을 지정한다. |
mappedBy = "필드명" | 연관관계의 주인이 아님을 의미한다. |
@Enumerated(EnumType.STRING) | Enum을 문자열(String) 로 저장한다. |
@Enumerated(EnumType.ORDINAL) | Enum을 숫자(순서) 로 저장한다. (권장하지 않음) |
@Getter | Getter 메서드를 자동 생성한다. |
@Setter | Setter 메서드를 자동 생성한다. |
@NoArgsConstructor | 기본 생성자를 자동 생성한다. |
@AllArgsConstructor | 모든 필드를 초기화하는 생성자를 자동 생성한다. |
@Builder | Builder 패턴으로 객체를 생성할 수 있게 한다. |
| 코드 | 설명 |
|---|---|
private Long id; | 엔티티의 기본 키를 저장한다. |
private Member member; | Member 엔티티를 참조하여 회원 정보를 저장한다. |
private LocalDateTime regTime; | 등록 시간을 저장한다. |
private LocalDateTime updateTime; | 수정 시간을 저장한다. |
private OrderStatus orderStatus; | 주문 상태(Enum) 를 저장한다. |
상태 변화가 일어나면 전파를 시켜야 한다. -> casecade
부모 One 자식 Many
All -> 부모가 하는 것 모드를 자식도 모두한다
orphanRemoval = true
부모 엔티티와 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.
즉, 부모가 더 이상 관리하지 않는 자식 객체를 JPA가 DB에서도 삭제해준다.
Cascade는 부모의 행동을 전파하고, orphanRemoval은 부모가 버린 자식을 삭제한다.
JPA에서 연관된 엔티티를 언제 가져올지 결정하는 설정이다.
쉽게 말하면 지금 필요한 데이터만 가져오고, 연관 데이터는 실제로 사용할 때 가져와라 라는 의미이다.
지연 로딩은 연관된 엔티티 데이터를 처음부터 가져오지 않고, 실제로 사용할 때 가져오는 방식이다.
쉽게 말하면 "일단 필요한 데이터만 조회하고, 연결된 데이터는 필요할 때 조회한다." 라는 의미이다.
| 장점 | 설명 |
|---|---|
| 성능 향상 | 필요한 데이터만 조회 |
| 불필요한 SQL 감소 | 사용하지 않는 데이터 조회 방지 |
| 메모리 절약 | 많은 연관 객체를 미리 올리지 않음 |
자동으로 연결을 해주는 라이브러리
서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter, setter를 통해 값을 복사해 객체를 반환해준다.
package com.shop.dto;
import com.shop.entity.ItemImg;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;
@Getter
@Setter
public class ItemImgDto {
private Long id;
private String imgName;
private String oriImgName;
private String imgUrl;
private String repImgYn;
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDto of(ItemImg itemImg) {
return modelMapper.map(itemImg, ItemImgDto.class);
}
}
package com.shop.dto;
import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.mapping.Array;
import org.modelmapper.ModelMapper;
import org.springframework.ui.ModelMap;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class ItemFormDto {
// Item
private Long id;
@NotBlank(message = "상품명은 필수 입력 값입니다.")
private String itemNm;
@NotNull(message = "가격은 필수 입력 값입니다.")
private Integer price;
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String itemDetail;
@NotNull(message = "재고는 필수 입력 값입니다.")
private Integer stockNumber;
private ItemSellStatus itemSellStatus;
// --------------------------------------------------------------
// ItemImg
private List<ItemImgDto> itemImgDtoList = new ArrayList<>(); // 상품 이미지 정보
private List<Long> itemImgIds = new ArrayList<>(); // 상품 이미지 아이디
// ---------------------------------------------------------------
// ModelMapper
private static ModelMapper modelMapper = new ModelMapper();
public Item createItem() {
// ItemFormDto -> Item 연결
return modelMapper.map(this, Item.class);
}
public static ItemFormDto of(Item item) {
// Item -> ItemFormDto 연결
return modelMapper.map(item, ItemFormDto.class);
}
}
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
@GetMapping(value = "/admin/item/new")
public String itemForm(Model model) {
model.addAttribute("itemFormDto", new ItemFormDto());
return "/item/itemForm";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!--사용자 스크립트 추가-->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function() {
var errorMessage = [[${errorMessage}]];
if (errorMessage != null) {
alert(errorMessage);
}
bindDomEvent();
});
function bindDomEvent() {
$(".imgFile.form-control").on("change", function() {
// a.jpg
var fileName = $(this).val().split("\\").pop();
var fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
// 확장자 추출
fileExt = fileExt.toLowerCase(); // 소문자 소환
if (fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif"
&& fileExt != "png" && fileExt != "bmp") {
alert("이미지 파일만 등록이 가능합니다.");
$(this).val("");
return;
}
});
}
</script>
</th:block>
<!--사용자 CSS 추가-->
<th:block layout:fragment="css">
<style>
.input-group {
margin-bottom: 15px;
}
.img-div {
margin-bottom: 10px;
}
.fieldError {
color: #db2130;
}
</style>
</th:block>
<div layout:fragment="content">
<form role="form" class="container" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
<p class="h2">상품 등록</p>
<input type="hidden" th:field="*{id}">
<div class="row mb-4">
<select th:field="*{itemSellStatus}" class="form-select">
<option value="SELL">판매중</option>
<option value="SOLD_OUT">품절</option>
</select>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품명</span>
</div>
<input type="text" th:field="*{itemNm}" class="form-control" placeholder="상품명을 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('itemNm')}" th:errors="*{itemNm}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">가격</span>
</div>
<input type="number" th:field="*{price}" class="form-control" placeholder="상품의 가격을 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">재고</span>
</div>
<input type="number" th:field="*{stockNumber}" class="form-control" placeholder="상품의 재고를 입력해주세요.">
</div>
<p th:if="${#fields.hasErrors('stockNumber')}" th:errors="*{stockNumber}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품 상세 내용</span>
</div>
<textarea class="form-control" aria-label="With textarea" th:field="*{itemDetail}"></textarea>
</div>
<p th:if="${#fields.hasErrors('itemDetail')}" th:errors="*{itemDetail}" class="fieldError">Incorrect data</p>
<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="row" th:each="num : ${#numbers.sequence(1,5)}">
<div class="input-group mb-3">
<label class="imgFile input-group-text" th:text="'상품이미지' + ${num}"></label>
<input type="file" class="imgFile form-control" name="itemImgFile">
</div>
</div>
</div>
<div th:if="${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="row" th:each="itemImgDto, status : ${itemFormDto.itemImgDtoList}">
<div class="input-group mb-3">
<label class="imgFile input-group-text"
th:text="${not #strings.isEmpty(itemImgDto.oriImgName) ? itemImgDto.oriImgName : '상품이미지' + (status.index + 1)}"></label>
<input type="file" class="imgFile form-control" name="itemImgFile">
<input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
</div>
</div>
</div>
<div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align : center">
<button th:formaction="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
</div>
<div th:if="${not #strings.isEmpty(itemFormDto.id)}" style="text-align : center">
<button th:formaction="@{'/admin/item/' + ${itemFormDto.id}}" type="submit" class="btn btn-primary">수정</button>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shop</groupId>
<artifactId>shop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name/>
<description/>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package com.shop.dto;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
public class ItemDto {
private Long id;
private String itemNm;
private Integer price;
private String itemDetail;
private String sellStatCd;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "cart")
@Getter
@Setter
@ToString
public class Cart {
@Id
@Column(name = "cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity{
@Id
@Column(name = "item_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id; // 상품 코드
@Column(nullable = false, length = 50)
private String itemNm; // 상품명
@Column(name = "price", nullable = false)
private int price; // 가격
@Column(nullable = false)
private int stockNumber; // 수량
@Lob
@Column(nullable = false)
private String itemDetail; // 상품 상세 설명
@Enumerated
private ItemSellStatus itemSellStatus; // 상품 판매 상태
// private LocalDateTime regTime; // 등록 시간
//
// private LocalDateTime updateTime; // 수정 시간
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "member_item",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Member> member;
}
package com.shop.entity;
import com.shop.dto.MemberFormDto;
import com.shop.repository.CartRepository;
import com.shop.repository.MemberRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
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 static org.junit.jupiter.api.Assertions.assertEquals;
@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 = 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);
em.flush(); // 반영하다
em.clear(); // 정리
Cart savedCart = cartRepository.findById(cart.getId()).orElseThrow(EntityNotFoundException::new);
assertEquals(savedCart.getMember().getId(), member.getId());
}
}
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class 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;
}
package com.shop.repository;
import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
package com.shop.constant;
public enum OrderStatus {
ORDER, CANCEL
}
package com.shop.entity;
import com.shop.constant.OrderStatus;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order extends BaseEntity {
@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) // 양방향이면 주인과 부하 -> order가 orderItem 관리한다
private List<OrderItem> orderItems = new ArrayList<>();
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
// private LocalDateTime regTime;
//
// private LocalDateTime updateTime;
}
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderItemRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
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 java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class OrderTest {
@Autowired
OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@PersistenceContext
EntityManager em;
@Autowired
MemberRepository memberRepository;
@Autowired
OrderItemRepository orderItemRepository;
public Item creatItem() {
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.creatItem();
itemRepository.save(item);
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(10);
orderItem.setOrderPrice(1000);
orderItem.setOrder(order);
order.getOrderItems().add(orderItem);
}
orderRepository.saveAndFlush(order);
em.clear();
Order savedOrder = orderRepository.findById(order.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(3, savedOrder.getOrderItems().size());
}
public Order createOrder() {
Order order = new Order();
for (int i = 0; i < 3; i++) {
Item item = this.creatItem();
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.getOrderItems().remove(0);
em.flush();
}
@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("=========================================");
}
}
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> {
// Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NullPointError가 발생하지 않으며,
// 클래스이기 때문에 각종 메서드를 제공한다.
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = "";
if (authentication != null) {
// 현재 로그인 한 사용자의 정보를 조회하여 사용자의 이름을 등록자와 수정자로 지정
userId = authentication.getName();
}
return Optional.of(userId);
}
}
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
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
}
package com.shop.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
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 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;
}
package com.shop.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity{
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String modifiedBy;
}
package com.shop.repository;
import com.shop.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
}
package com.shop.entity;
import com.shop.constant.Role;
import com.shop.dto.MemberFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
@Entity
@Table(name="member")
@Getter
@Setter
@ToString
public class Member extends BaseEntity{
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@Column(unique = true)
private String email;
private String password;
private String address;
@Enumerated(EnumType.STRING)
private Role role;
// static 객체 안만들어도 바로 사용
// MemberFormDto, PasswordEncoder
public static Member createMember(MemberFormDto memberFormDto,
PasswordEncoder passwordEncoder) {
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
}
package com.shop.entity;
import com.shop.repository.MemberRepository;
import jakarta.persistence.EntityExistsException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
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(EntityExistsException::new);
System.out.println("register time: " + member.getRegTime());
System.out.println("update time: " + member.getUpdateTime());
System.out.println("create member: " + member.getCreateBy());
System.out.println("modify member: " + member.getModifiedBy());
}
}
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class OrderItem extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice;
private int count;
// private LocalDateTime regTime;
//
// private LocalDateTime updateTime;
}
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "item_img")
@Getter
@Setter
public class ItemImg extends BaseEntity {
@Id
@Column(name = "item_img_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String imgName;
private String oriImgName;
private String imgUrl;
private String repImgYn;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
public void updateItemImg(String oriImgName, String imgName, String imgUrl) {
this.oriImgName = oriImgName;
this.imgName = imgName;
this.imgUrl = imgUrl;
}
}
package com.shop.service;
import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
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 saveMemberTest() {
Member member = createMember();
Member savedMember = memberService.saveMember(member);
// assertEquals member savedMember와 데이터가 같은지 비교한다.
assertEquals(member.getEmail(), savedMember.getEmail());
assertEquals(member.getName(), savedMember.getName());
assertEquals(member.getAddress(), savedMember.getAddress());
assertEquals(member.getPassword(), savedMember.getPassword());
assertEquals(member.getRole(), savedMember.getRole());
}
@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMemberTest() {
Member member1 = createMember();
Member member2 = createMember();
memberService.saveMember(member1);
Throwable e = assertThrows(IllegalStateException.class, () -> {
memberService.saveMember(member2);
});
assertEquals("이미 가입된 회원입니다.", e.getMessage());
}
}
package com.shop.repository;
import com.shop.entity.Cart;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CartRepository extends JpaRepository<Cart, Long> {
}
package com.shop.repository;
import com.shop.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email);
}
오늘도 스프링을 배웠다.
할 수록 더 복잡해지는 거 같다 ㅜ