이 포스팅은 (인프런) 김영한 - 실전! 스프링 부트와 JPA 활용1 강의를 정리한 내용입니다.
개발 기술
dependencies
check
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging.level:
org.hibernate.SQL: debug # 쿼리 파라미터 로그 남기기
#org.hibernate.orm.jdbc.bind: trace




controller : request handler, reponse sender
service : transaction 처리, 비즈니스 로직 수행
repository : Data Access object, JPA 사용시 엔티티 매니저 활용(영속성 컨텍스트;컨테이너 로 관리)
domain : 엔티티 모델, DB 테이블 구조와 함께 엔티티 필드와 직접 관련된 메소드 포함. 주로 repository에서 사용. (service 또는 controller에서는 dto 사용 권장)
@Entity
@Getter @Setter
@NoArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
public void save(Member member) {
em.persist(member);
}
public Member findOne(Long id) {
return em.find(Member.class, id); // .find(<조회 타입>, <PK>)
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
// @PersistenceContext EntityManager em;
@Test
public void join() throws Exception {
//given
Member member = new Member();
member.setName("kim");
//when
Long savedId = memberService.join(member);
//then
// em.flush(); // DB에 강제로 쿼리를 날리는 메소드
assertEquals(member, memberRepository.findOne(savedId));
}
@Test
public void check_dupl_member() throws Exception {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
//then
assertThrows(IllegalStateException.class, () -> {
memberService.join(member2); // 에외 발생 의도 지점
});
}
}
스프링부트는 기본적으로 Datasource 설정이 없다면 메모리 모드로 실행을 수행합니다. 따라서 test환경을 설정할 때에 application 파일을 분리할텐데, 이 때 test 환경에서는 메모리 모드 활용을 하는 것도 좋다.
DB는 상속을 지원하지 않습니다. 따라서 자바 코드에서 구현되어있는 상속 관계를 "슈퍼타입", "서브타입" 논리 모델로 가져가면서, DB에 이를 물리적으로 구현하는 방안이 필요합니다.
JPA는 이를 위해 @Inheritance, @DiscriminatorColumn, @DiscriminatorValue 어노테이션을 지원합니다. 이를 이용하여 DB에 해당 논리 모델을 물리적으로 구현하는 방법은 3가지 정도로 추릴 수 있습니다.
@Inheritance(strategy=InheritanceType.XXX)
@DiscriminatorColumn(name="DTYPE")
@DiscriminatorValue("XXX")
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<Category>();
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
}
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
@Getter @Setter
public class BookForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
private String author;
private String isbn;
}
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
private String artist;
private String etc;
}
@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
}
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item
i",Item.class).getResultList();
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}
주문은 이번 프로젝트의 핵심이라고 할 수 있습니다. 주문 기능을 구현하기 위해 엔티티 간 연관관계가 발생하며, 주문 서비스에서는 여러 레포지토리를 참조해야 합니다.
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status; [ORDER, CANCEL]
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
}
@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@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;
}
public enum OrderStatus {
ORDER, CANCEL
}
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
}
public enum DeliveryStatus {
READY, COMP
}
@Entity
@Getter @Setter
public class OrderSearch {
@Id @GeneratedValue
private Long id;
private String memberName; //회원 이름
private OrderStatus orderStatus;//주문 상태[ORDER, CANCEL]
}
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
/**
* 검색기능
*/
public List<Order> findAll(OrderSearch orderSerach) {
return em.createQuery("select o from Order o", Order.class).getResultList();
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
/**
* 주문 *
*/
@Transactional
public Long order(Long memberId, Long itemId, int count) {
// 엔티티 조회
Member member = memberRepository.findOne((memberId));
Item item = itemRepository.findOne(itemId);
// 배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
// 주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
// 주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
// 주문 저장
orderRepository.save(order);
return order.getId();
}
/**
* 주문 취소 *
*/
@Transactional
public void cancelOrder(Long orderId) {
// 주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
/**
* 주문 검색 *
*/
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch);
}
}
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping(value = "/order")
public String createForm(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping(value = "/order")
public String order(@RequestParam("memberId") Long memberId,
@RequestParam("itemId") Long itemId, @RequestParam("count") int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
@GetMapping(value = "/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
@PostMapping(value = "/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
}