JPA 활용1 - 복습
- 1. 생성자 주입
- 2. Transactional
- 3. 테스트 메모리 DB로 띄우기
- 4. 도메인 주도 설계
- 5. 생성자 대신 정적 팩토리 메서드
- 6. 주석
- 7. 로그
- 8. 컨트롤러
- 9. @PathVariable, @ModelAttribute
- 10. merge
- 11. 컨트롤러에 어설프게 엔티티 생성 금지
활용편은 배운내용을 바탕으로 만들어보는 강의라 모든 코드를 작성할 수 없다. 따라서 1회차 복습 과정에서 중간중간 기억해야할 내용만 따로 모아서 정리했다.
1. 생성자 주입
repository, EntityManager 등등 주입하기 위해 여러 가지 주입이 있지만 생성자 주입이 가장 좋다고 했었다.
public class MemberService {
// 반드시 필요하고 변경한 필요 없으므로 final
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
위 코드가 정석적인 코드인데 lombok 라이브러리를 활용하면 아래와 같이 간단하게 만들 수 있다. @Autowired는 생성자가 단 1개만 있다면 생략해도 된다.
@RequiredArgsConstructor // final 필드만 가지고 생성자 만들어줌 , 생성자 1개 -> autowired 생략 가능
public class MemberService {
private final MemberRepository memberRepository;
}
- @AllArgsConstructor : 모든 필드를 가지고 생성자를 만들어줌
- @RequiredArgsConstructor : final 필드만 가지고 생성자를 만들어줌
- @NoArgsConstructor : 파라미터가 없는 기본 생성자를 만들어줌
- @NoArgsConstructor(access = AccessLevel.PROTECTED) : 속성으로 접근제한자 지정 가능
눈에 보이진 않지만 애노테이션으로 인해 컴파일 시점에 생성자가 만들어진다. 생성자가 1개일 때는 Autowired를 생략해도 되므로 Autowired가 없어서 자동으로 주입된다.
public class MemberRepository {
@PersistenceContext
private final EntityManager em;
}
EntityManager은 @PersistenceContext를 붙여주면 자동으로 주입된다. 그런데 spring data jpa에서 EntityManger 역시 Autowired로 생성자 주입이 가능하도록 지원해준다. 따라서 위에서 했던 것 처럼 lombok으로 간단하게 해결할 수 있다.
2. Transactional
JPA의 모든 데이터 변경은 Transactional 안에서 수행되어야 한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 회원 가입
@Transactional
public Long join(Member member){
validateDuplicateMember(member); // 중복회원 검증
memberRepository.save(member);
return member.getId();
}
}
보통은 위 코드처럼 @Transactional 옵션에 readOnly 옵션을 붙이고 데이터가 변경되는 부분에 따로 @Transactional을 붙여서 코딩한다. 그런데 만약 클래스의 메서드들 대부분 커맨드성이여서 거의 쓰기만 한다면 전체를 @Transactional로 해주고 조회부분만 @Transactional(readOnly=true)로 해주면 된다. readOnly옵션은 false가 기본값인데 true 주면 flush를 생략해서 데이터를 조회하는 부분에 있어서 약간의 성능 향상이 가능하다.
@Transactional 애노테이션을 Service에 달던가 Repository에 달던가 어디에 달든 상관없다. 하지만 롤백을 생각해보면 비즈니스 로직이 있는 곳에서 트랜잭션을 시작하는 것이 좋으므로 비즈니스 로직이 있는 service에 트랜잭션을 붙여주는게 좋다. 참고로 @Transactional를 test에서 사용하게 되면 데이터가 DB에 들어가지 않고 롤백된다.
3. 테스트 메모리 DB로 띄우기
지금까지는 테스트를 실제 외부 db를 사용했다. 테스트 케이스를 돌리는데 db를 설치하는게 귀찮고 기본적으로 테스트 후 데이터가 초기화되는게 좋다. 따라서 테스트를 완전히 격리된 환경에서 하는게 좋다. 이러한 해결책으로 자바를 띄울 때 살짝 DB를 새로 만들어서 띄우는 방법이 메모리 DB를 사용하는 방법이다.
기본적으로 실제 운영에서의 설정과 테스트에서의 설정은 다르기 때문에 resources를 각각 만들어주고 application.yml를 따로 만들어 준다. 운영 로직에서는 main에 있는 resources가 test에서는 test에 있는 resources가 우선권을 가진다.
spring:
datasource:
# url만 메모리로 바꿔주면 된다.
url: jdbc:h2:mem:test
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
그냥 기존 yml 설정에서 url만 메모리로 바꿔주면 된다. url은 h2 db홈페이지에서 cheat sheet에 in-memory url를 복사해서 붙여넣어주면 된다.
spring boot를 사용한다면 더 쉽게 가능하다.
spring:
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
spring 설정에서는 별도의 설정이 없다면 그냥 메모리 DB로 돌린다.
4. 도메인 주도 설계
@Entity
@Getter @Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
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<>();
// 비즈니스 로직//
/**
* 재고 증가
*/
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;
}
}
Item 엔티티는 stockQuantity를 필드로 가지고 있다. 도메인 주도 설계에서는 엔티티 자체에서 해결할 수 있는 것들은 엔티티 안에 비즈니스 로직을 넣는게 좋다. 그렇게 설계하는 것이 더 객체 지향적이고 응집도가 있다. 이에 따라 여기서는 Item 엔티티가 stockQuantity를 가지고 있으니 이 필드에 대한 비즈니스 로직을 엔티티 안에 작성한 것이다. 예제이기 때문에 setter을 열어줬지만 사실상 실무에서는 열지 않고 생성자로 초기에 값 설정을 다 한 뒤에 의미있는 메서드를 만들어 값을 수정한다.
도메인 엔티티에 핵심 비즈니스 로직이 있고 Service 계층은 단순히 필요한 요청을 엔티티에 위임하는 역할을 하도록 설계하는 것을 도메인 모델 패턴이라고 한다.
참고로 도메인 주도 설계를 했다면 테스트를 할 때 각 엔티티에 메서드마다 순수하게 잘 동작하는지 단위 테스트를 하는게 좋은 테스트이다.
5. 생성자 대신 정적 팩토리 메서드
정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드라고 보면 된다. 지금까지는 그냥 생성자를 사용해서 객체를 생성했다. 정말 간단한 경우 그냥 생성자를 만들어서 사용하지만 점점 복잡해질수록 생성자를 사용하기 보다는 정적 팩토리 메서드를 사용해야 더 많은 이점이 있다. 아래 코드를 보자.
@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(fetch=FetchType.LAZY,cascade = CascadeType.ALL)
@JoinColumn(name="delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
// 코드 생략 //
// 기본 생성자 protected 로 막기
// jpa에서 protected로 놓으면 개발자는 쓰지 말라는 거라고 보면 된다.
protected Order(){}
// 생성 메서드 //
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
// 예제라서 setter을 열었지만
// 실무였다면 스펙상 기본 생성자 하나 protect로 막아주고
// 여기서 사용하기 위해 파라미터가 있는 생성자 만들어 protect로 막아주고
// 파라미터 있는 생성자로 여기서 사용하는 방식으로 설계해야한다
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;
}
}
정적 팩토리 메서드는 단순하게 static으로 클래스에 붙은 객체를 만드는 메서드라고 생각해도 된다. 위의 order의 경우 생성이 단순하지 않다. 따라서 생성자보다는 정적 팩토리 메서드를 사용하는게 좋다. 실무에서는 대부분이 단순하지 않으므로 그냥 생성자보다 정적 팩토리 메서드를 사용한다고 기억해도 될 것 같다. 이점은 다음과 같다.
- 이름을 가질 수 있다.
- 객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있다. new 라는 키워드를 통해 객체를 생성하는 생성자는 내부 구조를 잘 알고있어야 목적에 맞게 객체를 생성할 수 있다. 하지만 정적 팩토리 메서드를 사용하면 메서드 이름에 객체 생성의 목적을 담아낼 수 있따.
- 하위 자료형 객체를 반환할 수 있다.
- 객체 생성을 캡슐화할 수 있다.
- 캡슐화 : 데이터 은닉을 말한다. 여기서는 생성자를 클래스의 메서드 안으로 숨기면서 내부 상태를 외부에 드러낼 필요없이 객체 생성 인터페이스를 단순화 시킬 수 있다.
- DTO와 Entity 간에 자유롭게 형 변환이 가능해야 하는데, 정적 팩토리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다.
- 호출할 때마다 새로운 객체를 생성할 필요가 없다.
- 자주 사용되는 요소의 개수가 정해져있다면 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만들 수 있다.
주의점
- static 사용
- 정적 팩토리 메서드를 사용하기로 했다면 생성자를 막아줘야 한다.
- 누구는 정적 팩토리 메서드를, 누구는 생성자를 사용할 수도 있기 때문
위에 코드에서는 직접 기본 생성자를 만들어 protected로 줬는데 lombok을 이용하면 코드를 줄일 수 있다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
위 애노테이션을 클래스에 붙여주면 파라미터 없는 기본 생성자를 만들어주는데 access를 protected로 해주면 protected 기본 생성자가 만들어진다.
6. 주석
아래 코드는 원래 위에 같이 있던 코드인데 따로 설명하려고 나눴다. 실무를 하다보면 코드가 정말 길어지므로 그에 따른 주석을 아래와 같이 달아주는게 나중에 보기 편하다. 비즈니스 로직에 대해서 크게 주석을 주고 그에 따른 내용을 적어준다. 조회 로직의 경우 크게 조회 로직이라고 적어주고 그에 따른 세부 조회 로직에 대해서 따로 주석을 달아준다.
// 비즈니스 로직 //
/**
* 주문 취소
*/
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;
}
7. 로그
@Controller
public class HomeController{
// slf4j 로그 사용
Logger log LoggerFactory.getLogger(getClass());
@RequestMapping("/")
public String home(){
log.info("home controller");
return "home";
}
}
로그를 사용할 때 저렇게 log를 뽑아놓고 log.info로 로그에 찍는다. 아래와 같이 콘솔에 찍힌다.
lombok을 사용하면 코드를 줄일 수 있다.
@Controller
@Slf4j // lombok에 있는 애노테이션
public class HomeController{
// lombok으로 log 코드 컴파일 시 자동으로 들어감
@RequestMapping("/")
public String home(){
log.info("home controller");
return "home";
}
}
8. 컨트롤러
코드 설명하기 전에 알아두어야 할 것
- @RequestMapping은 GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE를 모두 보낼 수 있다. 기능별로 간략하게 줄인 것이 @GetMapping, @PostMapping 등이 있다.
- 웹쪽에서 정보를 Member로 바로 받지 않고 Form으로 한 번 걸러서 받는 이유
- 요구사항이 정말 간단하면 Form없이 Member 엔티티를 그대로 써도 된다.
- 실무는 요구사항이 화면과 엔티티가 1:1로 매칭되는 경우가 거의 없다. 엔티티를 Form으로 써버리면 엔티티가 화면을 처리하기 위한 기능이 점점 증가한다. 엔티티가 화면 종속적인 기능이 생기다보면 점점 엔티티가 지저분해진다. 결과적으로 유지보수하기 어려워진다. JPA를 사용할 때 조심해야할 점은 엔티티를 최대한 순수하게 유지해야 한다. 오직 핵심 비즈니스 로직에만 dependency가 있도록 설계하는 것이 중요하다.
- 화면에 맞는 API는 Form 객체 또는 DTO를 사용해야 한다.
- 스프링 부트 2.3이상부터는 @NotEmpty 등의 validation을 사용하려면 라이브러리 추가 필요
- implementation ‘org.springframework.boot:spring-boot-starter-validation’
- 여기서는 NotEmpty애노테이션을 사용했는데 애노테이션 타고 들어가서 아래 그림의 빨간색 표시를 클릭하면 어떤 애노테이션이 있는지 확인 가능
- API를 만들 때는 절대 엔티티를 반환하면 안된다. -> 엔티티를 반환하게 될 경우, 만약 엔티티를 수정하면 API 스펙이 변하게 된다.
// MemberForm 클래스 파일
@Getter @Setter
public class MemberForm {
// 값이 비면 오류가 나는 애노테이션
@NotEmpty(message = "회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
// MemberController 클래스 파일
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/* 회원가입 */
@GetMapping("/members/new")
public String createForm(Model model){
model.addAttribute("memberForm",new MemberForm());
return "members/createMemberForm";
}
@PostMapping("/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:/"; // 첫 화면으로 보내기
}
/* 회원 조회 */
@GetMapping("/members")
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members",members);
return "members/memberList";
}
}
첫 번째 GetMapping 파트
@GetMapping부터 보자. “/members/new” url로 타고 들어오면 Spring에서는 알아서 Model을 만들어서 파라미터로 넣어준다. 그럼 그 model 안에 memberForm 빈껍데기를 담아준다. “/members/new”로 들어온 화면에서는 return members/createMemberForm을 통해 createMemberForm.html을 화면에 뿌려준다. 이때 model도 함께 프론트쪽으로 보낸다.
PostMapping 파트
프론트쪽에서 form 태그에 action=”/members/new” method = post라고 적혀있을 것이다. 백쪽에서 보낸 빈껍데기 memberForm을 프론트에서 채워서 action의 url로 다시 보내준다. 첫 파라미터로 MemberForm을 적어줬고 프론트쪽에서 보낸 MemberForm이 이 파라미터로 들어온다. 앞에 @validate 애노테이션은 MemberForm에서 적어줬던 @NotEmpty 같은 애노테이션이 동작하게 해준다.
BindingResult는 spring이 제공하는 기능인데 만약 BindingResult 파라미터를 적어주지 않는다면 프론트에서 name값을 적지 않고 넘겨줬다면 MemberForm에서 @NotEmpty 애노테이션에 의해 오류가 발생해서 컨트롤러로 코드가 안넘어가고 팅긴다. 하지만 BindingResult를 파라미터로 넣어주면 오류가 발생하면 BindingResult에 오류가 담기고 컨트롤러 코드가 실행된다. 따라서 이 오류를 이용한 코드를 만들 수 있다. if문을 통해 오류가 담겨있다면 createMemberForm.html을 화면에 다시 뿌려주도록 했다. 이때 spring이 BindingResult를 화면까지 끌고 가서 프론트쪽에서 사용할 수 있도록 해준다. @NotEmpty에 메시지를 넣어줬었는데 BindingResult는 이 오류가 담겨있으니 프론트에서 코딩으로 이 메세지를 사용할 수 있다.
두 번째 GetMapping 파트
JPA로 DB에서 member들을 땡겨와 모델에 넣어주고 memberList.html을 화면에 뿌릴 때 같이 보내준다. 그럼 프론트쪽에서 이 모델을 가지고 코딩해서 화면에 뿌린다. 위에서 언급했듯이 원래는 Form이나 DTO로 한 번 걸러서 프론트에 보내야 한다. 위는 그냥 예제기도하고 간단해서 엔티티를 넘겼다.
9. @PathVariable, @ModelAttribute
@PathVariable
/**
* 상품 수정 폼
*/
@GetMapping(value = "/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
// bookform 빈껍데기 만들어서 현재의 상태를 다 채워주고
// 화면에 같이 넘김
Book item = (Book) itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
model.addAttribute("form", form);
return "items/updateItemForm";
}
상품을 수정하는 코드이다. 웹에서 상품의 정보를 수정하려고 클릭한다고 가정하자. 근데 여러 상품이 있으면 어떤 상품을 수정할 때마다 타고오는 url정보가 다를 것이다. 이를 해결해 주는 것이 @PathVariable 이다. {}로 해당 부분을 감싸주고 파라미터에 @PathVariable애노테이션을 붙여주면 {}로 들어온 값이 해당 파라미터 값으로 들어간다.
아이템 id를 가져왔으니 해당 아이템을 찾아와서 BookForm 빈껍데기에 현재 아이템 정보를 다 채워주고 화면에 updateItemForm.html을 뿌릴 때 같이 model로 던져준다. 그럼 이 모델을 가지고 프론트단에서 현재 상태를 화면에 같이 뿌려주도록 작업하면 된다.
@ModelAttribute
/**
* 상품 수정
*/
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@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);
return "redirect:/items";
}
위는 상품 수정 코드이다. 수정 폼에서 수정 정보를 받아와서 실제로 수정하는 작업을 하는 것이다. 여기서는 itemId를 사용하지 않으니 파라미터로 안받았다. 상품을 수정했으니 form을 가져와서 db에 반영해주고 반영한 것을 다시 화면에 뿌려줘야 하니 model로 감싸서 다시 화면에 보내줘야한다. 보낼 때 model.addAttribute 로 세팅해줬었는데 이 코드가 귀찮으니 애노테이션으로 해결해주는 것이 @ModelAttribute이다. 괄호 안에 값을 키값이라고 보고 value로 form을 넣어준다고 보면 된다. 자세한 건 MVC 강의에서 공부한다.
10. merge
실무에서는 merge 대신 dirty checking(변경 감지)를 사용하는게 훨씬 좋다. 그래도 개념정도는 알아두자.
@Transactional 에서 JPA로 데이터를 꺼내오고 수정하면 추가적인 작업을 하지 않아도 커밋 시점에 dirty checking으로 알아서 변경된 부분의 쿼리를 날려준다고 배웠다. merge는 하나씩 변경된 부분을 확인해서 날리는게 아니라 통채로 바꿔버리는 방법이다.
public void save(Item item){
if (item.getId() == null){
em.persist(item);
} else{
em.merge(item);
}
}
위의 코드는 파라미터로 들어온 item이 id를 가지고 있다면 이미 있던 아이템으로 merge하도록 하는 코드다. merge의 동작 과정은 다음과 같다.
merge가 실행되면 먼저 영속성 컨텍스트의 1차 캐시에서 찾아보고 없으면 db에서 가져온다. db에서 가져온 것은 영속성 컨텍스트 1차 캐시로 도착한다. 그럼 이것을 merge로 들어온 파라미터의 값으로 덮어씌워버리고 merge된 것을 반환한다. 반환하는 것은 영속상태에 해당하지만, 이때 파라미터로 들어온 것은 영속 상태가 되는게 아니다. JPA가 관리하지 않는다. dirty checking처럼 일일이 변경하지 않고 한 번에 바꿔주기 때문에 편해보이지만 merge는 데이터값이 없으면 null로 업데이트 해버리는 치명적인 단점이 있다. 만약 위의 코드에서 item의 수량은 변하지 않도록 하려고 파라미터로 넘어온 item의 수량 필드값을 빈칸으로 넣었다면 원래 값이 유지되는게 아니라 덮어씌워버리므로 null이 업데이트 되는 것이다. 사실상 실무에서는 대부분 제약이 걸려있고 업데이트하는 필드가 매우 제한적이다. 따라서 merge를 사용하지 말고 dirty checking을 사용하자.
11. 컨트롤러에 어설프게 엔티티 생성 금지
/**
* 상품 수정
*/
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@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);
return "redirect:/items";
}
위 코드는 위쪽에서 상품 수정에서 사용했던 코드이다. 예제이므로 편의상 setter을 넣었지만 실제 사용하려면 의미있는 메서드를 만들어서 사용한다. 예를 들면, book 엔티티 안에 change(price,name,stockQuantity) 같은 의미있는 메서드를 만들고 사용해야 한다.
위 코드에서는 컨트롤러에 있는 코드인데 수정을 위해 어설프게 엔티티를 생성했다. 코드를 보면 form으로 웹계층에서 데이터를 받아서 book 엔티티 껍데기에다가 데이터를 넣어주고 모델링해서 프론트로 던져줬다. 이렇게 어설프게 껍데기를 컨트롤러에서 만들면 좋지 않다. 해결방법은 아래와 같다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId,form.getName(),form.getPrice(),form.getStockQuantity())
return "redirect:/items";
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId,String name, int price, int stockQuantity){
Item findItem = itemRepository.findOne(itemId);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
}
form으로 받은 데이터를 그대로 뽑아서 서비스 계층으로 넘긴다. 그럼 서비스 계층에서 수정 메서드를 만들어서 엔티티의 수정 메서드를 호출하면 된다. 여기서는 예제라 setter을 사용했지만 실제로는 의미있는 메서드를 사용하면 된다. 만약 업데이트할 데이터가 많아서 파라미터가 많아진다면 서비스 계층에 DTO를 하나 만들면 된다. form을 그냥 넘기면 되지 않을까 싶을텐데 form은 웹 계층에서 사용하기로 했으므로 사용별로 나눠주는게 좋다. 서비스 계층에 DTO를 만들면 다음과 같이 받으면 된다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId,UpdateItemDto itemDto){
Item findItem = itemRepository.findOne(itemId);
findItem.setName(itemDto.getName);
findItem.setPrice(itemDto.getPrice);
findItem.setStockQuantity(itemDto.getStockQuantity);
}
}
컨트롤러에서는 dto 하나 만들어서 form데이터 값 dto에 넣어주고 파라미터로 dto를 서비스 계층으로 넘기면 된다. id값과 dto를 따로 넘겼는데 그냥 dto에 다 넣어서 넘겨도 된다. 이렇게 코딩하는 것이 가장 좋은 방법이고 아래 사항들을 기억하자.
- 컨트롤러에 엔티티를 만들지 않도록 한다. 서비스 계층으로 넘겨야 할 데이터가 적다면 form에서 getter을 사용해 바로 넘긴다. 데이터가 많다면 DTO를 만들어 넘긴다. 여기서 반드시 트랜잭션이 있는 서비스 계층에 식별자 id값과 변경할 데이터를 명확하게만 전달해야한다.
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 변경한다. -> 트랜잭션이 있으므로 변경감지
본 포스팅은 인프런 김영한님의 ‘실전! 스프링 부트와 JPA 활용1’ 강의를 듣고 정리한 내용을 바탕으로 복습을 위해 작성하였습니다. [강의 링크]