○ spring-boot-starter-web
○ spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진(view)
○ spring-boot-starter-data-jpa
○ spring-boot-starter(공통) : 스프링 부트 + 스프링 코어 + 로깅
테스트 라이브러리
○ spring-boot-starter-test
핵심 라이브러리
기타 라이브러리
build.gradle의 dependencies에 아래의 코드 추가
=> implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
위의 라이브러리만 추가 하고 gradle 새로 고침하면 auto configration으로 자동 설정된다.
@Entity
@Getter
@Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
private Order order;
@Embedded
private Address address;
// EnumType에는 ORDINAL과 STRING가 존재하는데 STRING를 사용해야한다.
// 이유는 ORDINAL은 INTEGER 타입으로 중간에 추가가 되거나 하면 값이 밀려날 수 있어
// 오류가 발생하게 되므로 꼭 STRING를 사용할 것
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //READY, COMP
}
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
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
}
한부모parent 하위에는 여러 자식child가 존재하므로 일대 다 관계이다.
따라서 child가 기준이 되며 parent를 FK로 참조하게 된다.
엔티티에는 가급적 Setter 사용을 금하자
: Setter가 모두 열려있으면 변경 포인트가 많아서 유지보수가 어렵다.(어디서 값이 들어오는지 찾기 힘듬)
모든 연관관계는 지연로딩(LAZY)으로 설정
: 즉시로딩(EAGER)은 예측이 어렵고, 연관 되어있는 데이터를 모두 가져오기 때문에 문제가 발생할 수 있다.
컬렉션은 필드에서 초기화 하자
: null 문제에서 안전하다
예제 코드
// Member()안에서 초기화하는 방법은 X
private List<Order> orders;
public Member() {
orders = new ArrayList<>();
}
// 필드에서 선언과 동시에 초기화
private List<Order> oreders = new ArrayList<>();
양방향 연관일 경우 연관관계 메서드를 만들어줘야 한다.(기본편 공부할때 다시볼것)
ex) 자동차를 생각하면 자동차 한대에 여러개의 바퀴로 인해 바퀴가 연관관계의 주인이된다(FK).
하지만 보통 자동차를 중심으로 애플리케이션 로직이 동작하기 때문에 자동차에 연관관계 편의 메서드를 작성하게 된다.
사용 코드
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(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@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);
}
}
cascade는 mappedBy(읽기만 가능, 쓰기X), 양방향 등등과 전혀 관계 없이,
단순하게 A->B 관계가 cascade 되어 있으면, A엔티티를 PERSIST할 때 B엔티티도 연쇄해서 함께 PERSIST 해버린다는 뜻.
사용 코드
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
// Order 엔티티를 persist할 때, mappedBy="order"로 연결되어 있는(Order을 FK로 가지고 있는)
// OrderItem 엔티티도 연쇄하여 함께 persist 한다는 뜻.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
JPA에서의 모든 데이터 변경이나 로직들은 가급적이면 @Transctional안에서 실행 되어야 한다.
@Transctional은 기본이 readOnly=false 이다.
단순히 데이터 변경이 없는 조회 로직들은 @Transctional(readOnly=true)를 사용하는게 메모리 차원에서도 유리하다.
사용 코드
@Service
@Transactional(readOnly = true) //findMembers, findOne이 단순 조회로 입력보다 조회가 많아서
//전체 틀은 readOnly=true로 설정한다.
public class MemberService {
@Autowired
private MemberRepository memberRepository;
/**
* 회원 가입
*/
@Transactional //전체 틀이 readOnly=true이기 때문에 다시 선언. 선언시 기본값 false
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
/**
* 회원 전체 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
@NoArgsConstructor : 파라미터가 없는 생성자를 생성
@AllArgsConstructor : 클래스에 존재하는 모든 필드에 대한 생성자를 생성
@RequiredArgsConstructor : 초기회 되지 않은 모든 final필드에 대한 생성자를 생성
사용 코드
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/* @RequiredArgsConstructor로 인해 아래 생성자가 자동생성
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
*/
Spring data JPA를 사용하면 Repository.java의 EntityManager부분도 변경이 가능하다.
원래는 EntityManager은 @PersistenceContext를 붙여줘야 하지만 @Autowired도 가능하게 해준다.
위의 코드를 아래와 같이 변경 가능
@Repository
@RequiredArgsConstructor
public class MemberRepository {
/*
@PersistenceContext
private EntityManager em;
*/
//JPA에서 @PersistenceContext를 @Autowired로도 사용 가능하게 해주기 때문에
//클래스 위에 @RequiredArgsConstructor을 선언해 아래와 같이 한줄로 가능
private final EntityManager em;
public void save(Member member) {
em.persist(member);
}
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//given
Member member = new Member();
member.setName("kim");
//when
Long saveId = memberService.join(member);
//then
assertEquals(member, memberRepository.findOne(saveId));
}
위와 같은 테스트 코드를 실행하고 로그를 보면 insert문이 찍히지 않는다.
그 이유는 테스트 코드에서의 @Transctional은 기본적으로 Rollback를 하기 때문에 memberService.join(member);을
타고 들어가면 나오는 Repository에서의 em.persist에서 쿼리를 날리지 않는다.
테스트 코드에서도 persist가 날리는 쿼리를 보고 싶으면 2가지 방법이 있다.
1. @Rollback(false)이용
@Test
@Rollback(false)
public void 회원가입() throws Exception {
//given
Member member = new Member();
member.setName("kim");
//when
Long saveId = memberService.join(member);
//then
assertEquals(member, memberRepository.findOne(saveId));
}
2. em.flush() 사용
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Autowired EntityManager em;
@Test
public void 회원가입() throws Exception {
//given
Member member = new Member();
member.setName("kim");
//when
Long saveId = memberService.join(member);
em.flush();
//then
assertEquals(member, memberRepository.findOne(saveId));
}
중복된 값 예외 처리 예제
@Test
public void 중복_회원_예외() throws Exception {
//given
Member member1 = new Member();
member1.setName("jpa");
Member member2 = new Member();
member2.setName("jpa");
//when
memberService.join(member1);
try {
memberService.join(member2); //예외가 발생해야 한다!!!
} catch (IllegalStateException e) {
return;
}
//then
fail("예외가 발생해야 한다.");
}
위에서 try~catch구문을 이용한 예외 처리를 @Test 옵션에 추가하여
아래와같이 소스를 간단히 할 수 있다.
@Test(expected = IllegalStateException.class)
public void 중복_회원_예외() throws Exception {
//given
Member member1 = new Member();
member1.setName("jpa");
Member member2 = new Member();
member2.setName("jpa");
//when
memberService.join(member1);
memberService.join(member2); //예외가 발생해야 한다!!!
//then
fail("예외가 발생해야 한다.");
}
엔티티에서 만들어 놓은 생성 메서드 Order.java
위의 메서드를 service딴에서 생성 메서드로만 값을 넣을 수 있도록 해야한다.
@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);
/* 생성 메서드로만 값을 넣어야지 아래 처럼 새로 인스턴스를 생성하고 값을 직접 넣지
못하도록 막아야 한다.
OrderItem orderItem1 = new OrderItem();
orderItem.setItem();
*/
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//주문 저장
orderRepository.save(order);
return order.getId();
}
위처럼 새로 인스턴스를 생성해서 값을 넣지 못하도록 제약 하기 위해 @NoargsConstructor사용
생성자 접근 범위 패키지 단위로 제약
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
//== 생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
}
도메인 모델 패턴은
=> 엔티티에 비지니스 로직을 넣고 Service에는 엔티티와 이어주는 식의 역활만 하는 것.
(Jpa같은 ORM에서 많이 사용)
트랜잭션 스크립트 패턴은
=> 엔티티에는 getter, setter의 역활만하고 비지니스 로직은 Service딴에 몰아 넣는 것.
(Mybatis 등의 형식)
MemberForm.java
import javax.validation.constraints.NotEmpty;
@Getter @Setter
public class MemberForm {
// NotEmpty라는 validation으로 message를 전달 할 수 있다.
@NotEmpty(message = "회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
MemberController.java
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
// @Valid로 @NotEmpty로 설정한 값을 체크하고
// 에러가 있을 시 BindingResult에 담아서 처리할 수 있다.
if (result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
영속성 : 어떤 상태가 계속 유지되는 것.
준영속 엔티티란?
영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다.
jpa는 트랜잭션 커밋시점에 바뀐애들 찾아서 update 후 커밋. flush할때 dirty checking이 일어나기 때문
영속성으로 관리되고 있는 엔티티는 따로 save를 해주지 않아도 변경 감지로 수정된 부분을 업데이트 한다.
아래의 코드는 주문 취소를 했을 경우 별도의 insert, update 없이도 영속성으로 관리가 되고 있어 수정된 부분을 감지하여 DB에 저장한다.
변경감지 사용 코드
@Transactional
void update(Item itemParam) { //itemParam: 파라미터로 넘어온 준영속 상태의 엔티티
// 같은 엔티티를 조회한다.
Item findItem = em.find(Item.class, itemParam.getId());
findItem.setPrice(itemParam.getPrice()); //데이터를 수정
}
// 트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택
// => 트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 동작으로 데이터베이스에 update 실행
병합(merge) 사용코드
// 준영속 상태인 것을 Item findItem에 직접 넣어 영속 상태로 변경한다.
@Transactional
void update(Item itemParam) { //itemParam: 파라미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(item);
}
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경이 가능하지만,
병합(merge)은 모든 속성이 변경되어 변경 사항 값이 없을 경우 null 값이 들어갈 수 있다.
따라서 변경 감지를 사용 하도록 하자.
변경 감지로 적용한 사용 코드
ItemController.java
@PostMapping("/items/{itemId}/edit")
public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form) {
/* 컨트롤러에서 어설프게 엔티티를 생성하지 않는다.
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);*/
// itemService단에서 필요한 부분만 변경해준다.
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getPrice());
return "redirect:/items";
}
ItemService.java
@Transactional
public void updateItem(Long id, String name, int price, int stockQuantity) {
//트랜잭션 안에서 조회를 해서 영속 상태로 조회된 값을 item에 넣는다.
Item item = itemRepository.findOne(id);
item.setName(name);
item.setPrice(price);
item.setStockQuantity(stockQuantity);
}
[6] QueryDSL [1/2] (0) | 2021.07.20 |
---|---|
[5] 스프링 데이터 JPA (0) | 2021.07.08 |
[4] JPA 활용2 강의 내용 [2/2] (0) | 2021.06.17 |
[2] 자바 ORM 표준 JPA 프로그래밍 - 기본편(객체지향 쿼리) [2/2] (0) | 2021.05.20 |
[1] 자바 ORM 표준 JPA 프로그래밍 - 기본편 [1/2] (0) | 2021.05.04 |