조금 늦었지만, 이미 떨어져버린 망각곡선을 최대한 막기 위해...
참고: spring-boot-devtools 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이
View 파일 변경이 가능하다.
메뉴 -> build -> Recompile
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
spring.jpa.hibernate.ddl-auto
운영 장비에서는 절대 crate, create-drop, update 사용하면 안된다. -> 데이터가 변경되거나 날아가버릴 위험 존재.
개발 초기 단계는 create 또는 update
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate 또는 none
주문과 상품의 다대다 관계를 중간에 주문 상품이라는 매핑 테이블을 이용하여 일대다, 다대일의 관계로 풀어냈다.
Member
가 주문 리스트를 가지는 것이 타당해보이지만, 객체 세상은 실제 세계와 다르다. Order
가 Member
를 참조하는 것으로 충분하다.
외래키가 있는 곳을 연관관계의 주인으로 정해라.
실제 DB테이블에서 외래키가 있는 곳을 연관관계의 주인으로 정해야, 해당 객체를 업데이트할 때 그 내부에서 외래키값도 업데이트된다. 그렇지 않은 경우에는 외래키 업데이트를 위해 추가 업데이트 쿼리가 발생하고 유지보수가 어려워진다.
연관관계의 주인이라고 해서 비즈니스 로직상 우위에 있는 것이 아니다.
엔티티에는 가급적 Setter를 사용하지 말기
실무에서는 가급적 Getter는 열어두고 Setter는 필요한 경우에만 사용하는 것을 추천한다. Setter를 막 열어두면 엔티티에가 도대체 왜 변경되는지 추적하기 점점 힘들어지기 때문이다.
-Setter 대신 변경을 위한 비즈니스 메서드를 별도로 설정하는 것도 방법.
모든 연관관계는 지연로딩으로 설정
- 즉시로딩은 예측하지 못한 SQL문이 실행되어, 추적하기 어렵다.
- JPQL 사용시 N+1 문제가 발생한다.
🕵️ N+1 문제란
처음 조회할 때 SQL문 1번에 조회한 데이터의 갯수 N만큼 쿼리가 추가로 나가는 것. 즉 EAGER로 설정된 연관관계가 있는 엔티티를 N개 조회하면 해당 엔티티마다 즉시로딩으로 설정된 데이터를 추가로 조회하는 쿼리가 나간다.
@XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야
한다.
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
테이블은 엔티티 타입처럼 타입이 없으므로 pk값을 그냥 id로 설정하면 구분이 어렵다. -> 테이블명 + i
로 컬럼명을 설정
실습을 진행한 코드는 따로 정리하지 않고, 과정에서 사용된 기술들만 정리
@Transactional(readOnly = true)
public class MemberService {
...
}
@Transactional : 트랜잭션, 영속성 컨텍스트
readOnly=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지
않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋으므로, 메모리 DB를 사용하는 것이 가장 이상적이다.
또 실제 애플리케이션 실행환경과는 다르므로 설정 파일을 다르게 사용하자.
- test/resources/application.yml
해당 경로에 설정파일을 따로 추가하면 된다.
- 스프링 부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러리를 보고 찾아준다. 추가로 ddl-auto 도 create-drop 모드로 동작한다.
엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것
ex) Order Entity
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
//==연관관계 메서드==//
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);
}
//==생성 메서드==//
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;
}
//==비즈니스 로직==//
/** 주문 취소 */
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException("이미 배송완료된 상품은 취소가
불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
//==조회 로직==//
/** 전체 주문 가격 조회 */
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
setMember()
,addOrderItem()
, setDelivery()
양방향 연관관계를 설정하게 되면, 연관관계 메소드를 만들어서 양쪽 객체 모두에 대하여 연관관계가 설정되도록 한다.
createOrder()
이런식으로 생성자대신 정적 팩토리 메소드를 통해 객체를 생성하게 하면 다양한 장점이 있다.
cancel()
, getTotalPrice()
OrderSearch
라는 객체를 생성하여, 해당 조건에 따라서 Order
를 검색하는 상황이다.
public class OrderSearch {
private String memberName; //회원 이름
private OrderStatus orderStatus;//주문 상태[ORDER, CANCEL]
그런데 JPQL로 쿼리를 생성하려면 매우 복잡해지고 유지보수성이 떨어지게 된다. -> 조건문으로 일일히 확인하고 문자열을 이용해 쿼리를 직접 만들어나가야 한다.
대안으로 JPA Criteria, Querydsl이 있는데 전자의 경우는 매우 복잡하다. -> Querydsl을 이용해 동적 쿼리 생성
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
이런 식으로 레이아웃을 구성해서 중복되는 부분을 처리할 수 있다.
Thymeleaf는 이것조차 생략할 수 있는 방법을 제공하는데, 필요하면 공식 Document를 참고하도록 하자.
public String create(@Valid MemberForm memberForm, BindingResult result){
if(result.hasErrors()){
return "members/createMemberForm2";
}
...
}
@valid
어노테이션을 사용하면 해당 객체의 Validation 정보를 적용하여 검증한다.
이 어노테이션이 붙은 파라미터 뒤에, BindingResult
를 받으면, 에러가 발생하여도 result
에 에러를 담아 해당 컨트롤러를 실행한다.
Spring 과 Tymeleaf는 연동이 잘 돼있어서, 에러가 발생한 상태로 템플릿을 렌더링하면, 해당 에러도 같이 넘겨준다.
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>
도메인과 실제 요청으로 받는 파라미터는 구성이 다르다.
엔티티를 이용해 모든 것을 처리하려 하면, 엔티티가 지저분해진다.
엔티티를 최대한 순수한 상태로 유지하는 것이 좋다.
따라서 MemberForm과 같이 따로 Dto를 만들어 값을 받는 것이 옳다.
@PostMapping(value = "/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
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:/";
}
회원가입을 하는 코드이다.
여기서 MemberForm 객체를 따로 생성하여 정보를 받고, Member
객체를 생성하여 직접 설정해주는 작업을 하였다.
이렇게 하는 이유는 엔티티를 직접 사용하여 값을 받으면 문제가 많이 발생하기 때문이다.
1. 엔티티가 화면에 의존적이게 된다.
Valid같은 기능을 이용하기 위해 엔티티에 화면 처리에 필요한 코드를 추가해야 한다. -> 엔티티가 화면 종속적으로 변하고 유지보수성이 떨어진다.
2. 엔티티의 변경이 api에 영향을 끼친다.
만약 엔티티에 데이터를 추가하거나 변경, 삭제하게 되면 해당 엔티티를 이용하는 모든 api를 수정해야한다.
3. 엔티티에 대한 신뢰성이 떨어진다.
파라미터로 넘어온 엔티티에 어떤 값이 들어있는지 알 수 없다.
화면이나 API에 맞는 폼 객체나 DTO를 생성해서 사용할 것.
@GetMapping("items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
Book book = (Book)itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(book.getId());
form.setName(book.getName());
form.setPrice(book.getPrice());
form.setStockQuantity(book.getStockQuantity());
form.setAuthor(book.getAuthor());
form.setIsbn(book.getIsbn());
model.addAttribute("form",form);
return "items/updateItemForm";
}
@PathVariable
을 사용할 때는 주의해야한다.준영속 엔티티를 수정하는 방법은 두가지가 있다.
1. 변경감지(Dirty checking)
2. 병합(merge)
@Transactional
void update(Item itemParam) {
Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다.
findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
}
위의 코드처럼 이미 가지고 있는 id값을 이용해 조회하여 영속성 컨텍스트로 만들고 수정한다.
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(itemParam);
}
merge를 이용하면 JPA가 파라미터로 넘긴 객체의 id값을 이용하여 해당 객체를 조회하고 조회한 객체에 파라미터로 넘긴 객체의 값으로 '모두' 대체한다.
즉, 모든 필드를 교체하므로 병합시에 값이 없으면 null로 업데이트 될 수 있는 위험이 있다.
엔티티를 변경할 때는 항상 변경 감지를 사용하자.
서비스계층에서 트랜잭션이 이뤄지므로 서비스으로 필요한 값들만 넘기고 거기서 처리하는 게 낫다 -> 특히 데이터 변경
컨트롤러에서 바로 리포지토리 호출하는 것도 고려해볼만하다.
다이아 가야지 초심 잃었네