REST API 개발에 초점을 맞출 것임
-> 등록, 수정, 조회 REST API 개발
-> API 개발 실무 노하우
-> JPA를 사용하여 성능 최적화를 중점으로 볼 것임! 얘를 사용하면 진짜 성능이 많이 올라갈것이니깐 성능 최적화를 해보자
회원 API 개발하기
-> 등록, 수정, 조회 API 개발할것
POSTMAN을 사용해서 API 테스트 해볼 것임!
회원 API 개발하기
-> 등록, 수정, 조회 API 개발할것
@RestController == ResponseBody + Controller
-> @ResponseBody는 데이터를 직접 보내게 해주는 어노테이션
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
// Valid를 하면 validation이 자동으로 돼
//API 통신해서 JSON으로 온 바디를 멤버 데이터로 매핑해서 바꿔줌
@PostMapping("/api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member){
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberResponse{
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
}
--> POST 방식으로 API 통신 성공
-> 터미널 창에서도 잘 띄워지는것을 볼 수 있다
-> 근데 name을 뺴고 넣어도 저장이 되는 것을 볼 수 있어 NULL로!
@NotEmpty
private String name;
-> 하지만 만약 member domain 에 이렇게 @NotEmpty 를 붙여주면 이름을 뺴놓고는 POST가 안됨
지금 화면에 나오는 presentation을 위한 validation 로직이 entity에 모두 들어가 있는 것이기 때문에 안좋은 코드임
-> entity의 속성이름을 바꿔버리면 API 호출할 때 그것을 모를 시 호출이 안되는 상황이 있을 수 있다는거임!
-> Entity는 자주 바뀔 수 있기 때문에 되게 안좋은 상황.
-> 따라서 API 요청 스펙에 맞춰서 DTO를 만들어야 함!
실무에서는
1) Entity 화면에 노출 금지
2) Entity 파라미터로 받는것 금지
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request){
//파라미터로 멤버, 즉 entity를 안받아서 member객체 생성해줌
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
//DTO 만들어주기
@Data
static class CreateMemberRequest{
private String name;
}
-> 얘도 잘 되는게 보임!
-> 이렇게하면 장점이
1. API 스펙이 멤버 엔티티가 변동되어도 바뀌지 않음.
-> 코드 안에서 멤버를 선언을하니깐!
2. DTO로 받으면 어떻게 개발되고 어떤거 조심해야하는지 알아
-> API만들땐 이렇게 DTO만들어서 하자! 그게 정석임
@NotEmpty 하고싶으면 DTO 안에서 해주면 돼! 그러면 엔티티 건드리않고 DTO에서 변경해줄 수 있음.
즉, Entity 노출 절 대 금 지 !!!!
수정은 put으로 사용할 것임
-> 같은 것을 호출한다해서 여러번 바뀌는게 아니라 URL 조금 변경해야해
@Transactional
public void update(Long id, String name) {
//영속상태인 멤버를 setname으로 바꿔주면 트랜젝션 커밋 되는 순간에 jpa가 플러시하고 변경된거 반영
Member member = memberRepository.findOne(id);
member.setName(name);
}
-> 만약 update에서 뭔갈 반환을 한다면 커맨드랑 쿼리랑 같이있는 코드가 됨.
-> 즉, 반환을 하는 커맨드와 변경을 위해 조회를 하는 쿼리가 같이있어
-> 그래서 보통 putmapping 쪽에서 건드는게 좋음
//URL 에 {id}를 넣어줌으로서 몇번째 아이디를 수정할지 지칭
//Update 요청 DTO랑 응답 DTO 등록이랑 별개로 만들어줌
//등록이랑 수정은 API 스펙이 달라서 별도의 응답요청 가져가는게 좋음
@PutMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateMemberRequest request){
//수정할 땐 가급적이면 변경감지 사용
memberService.update(id, request.getName());
//이렇게 커맨드랑 쿼리 분리하면 유지보수성 증가!
Member findMember = memberService.findOne(id);
return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
@Data
static class UpdateMemberRequest{
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse{
private Long id;
private String name;
}
-> URL 마지막에 멤버 아이디 넣는거 잊지말기 !
-> 업데이트 쿼리도 날려진 것
application.yml 에서 ddl-auto : none 으로 해주면 테이블 계속 드랍이 안돼서 테스트 해볼 땐 편할것임
1) Get 메소드중 가장 비효율적이고 단순한 메소드
//가장 안좋은 버전의 API
@GetMapping("/api/v1/members")
public List<Member> membersV1(){
return memberService.findMembers();
}
-> 얘도 잘 반환은 됨
-> 근데 앤티티를 직접노출하면 필요없는 orders도 빈리스트로 왔어
-> 회원 데이터만 뽑고싶으면 member에서 order위에 @JsonIgnore 써주면돼 그러면 지금은 해결이 되는데, 다른 다양한 api들을 쓸때 문제가돼! 왜냐면 url마다 필요한 정보가 달라서 엔티티에 저런 어노테이션 쓰는게 위험해
-> entity에 presentation 기능들이 들어가면 이래서 위험해
-> 즉, entity를 이렇게 노출하면 안됨!
2) V2
//V2
@GetMapping("/api/v2/members")
public Result memberV2(){
//얘를 멤버 디티오로 바꿔서 넘길것임
List <Member> findMembers = memberService.findMembers();
//스트림 사용하기
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
//이렇게 안하고 V1 방식으로 하면 JSON 배열 방식으로 나가기 때문에 유연성이 떨어짐
return new Result(collect);
}
@Data
@AllArgsConstructor
//멤버 디티오
//고객의 이름만을 반환하는 DTO
static class MemberDto{
private String name;
}
@Data
@AllArgsConstructor
static class Result<T>{
private T data;
}
-> 결과값이 배열에서 {} 이걸로 바뀜. 형식이 유연해져!
-> API 스펙에서 내가 노출할 것만 노출해! DTO랑 1:1로 !
-> 이래야 유연하게 할 수 있고 유지보수도 쉬워져
1) 조회용 샘플 데이터 입력
2) 지연 로딩과 조회 성능 최적화
3) 컬렉션 조회 최적화
-> 조인을 했는데 데이터가 뻥튀기가 될 경우가 있어 (1:N 일때)
4) 페이징과 한계 돌파
5) OSIV와 성능 최적화
API 개발 고급 설명을 위한 샘플 데이터 입력
-> 최종적으로 주문이 두건 이루어진 주문 데이터 만드는것임
package jpabook.jpashop;
import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
/*
* userA
JPA1 BOOK
JPA2 BOOK
* userB
* SPRING1 BOOK
* SPRING2 BOOK
* */
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
@PostConstruct
public void init(){
initService.dbInit1();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService{
private final EntityManager em;
public void dbInit1(){
Member member = new Member();
member.setName("userA");
member.setAddress(new Address("서울", "1", "1111"));
//멤버를 영속성컨텍스트로 만들기
em.persist(member);
//책 만들기
Book book1 = new Book();
book1.setName("Jpa1 Book");
book1.setPrice(10000);
book1.setStockQuantity(100);
em.persist(book1);
Book book2 = new Book();
book2.setName("Jpa2 Book");
book2.setPrice(10000);
book2.setStockQuantity(100);
em.persist(book2);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
//이러면 주문 생성 완료
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
em.persist(order);
}
}
}
-> 잘 들어가있쥬?
-> 디비에도 잘 들어가있다!
중복되는 코드는 extract method -> option +cmd + M 사용해서 만들기
package jpabook.jpashop;
import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
/*
* userA
JPA1 BOOK
JPA2 BOOK
* userB
* SPRING1 BOOK
* SPRING2 BOOK
* */
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
@PostConstruct
public void init(){
initService.dbInit1();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService{
private final EntityManager em;
public void dbInit1(){
Member member = createMember("userA", "서울", "1", "1111");
//멤버를 영속성컨텍스트로 만들기
em.persist(member);
//책 만들기
Book book1 = createBook("Jpa1 Book", 10000, 100);
em.persist(book1);
Book book2 = createBook("Jpa2 Book", 20000, 100);
em.persist(book2);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
//이러면 주문 생성 완료
Delivery delivery = createDelivery(member);
Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
em.persist(order);
}
private static Delivery createDelivery(Member member) {
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
return delivery;
}
private static Book createBook(String name, int price, int stockQuantity) {
Book book1 = new Book();
book1.setName(name);
book1.setPrice(price);
book1.setStockQuantity(stockQuantity);
return book1;
}
private static Member createMember(String name, String city, String street, String zipcode) {
Member member = new Member();
member.setName(name);
member.setAddress(new Address(city, street, zipcode));
return member;
}
//두번째 유저 만들기
public void dbInit2(){
Member member = createMember("userB", "진주", "2", "2222");
//멤버를 영속성컨텍스트로 만들기
em.persist(member);
//책 만들기
Book book1 = createBook("Spring1 Book", 20000, 200);
em.persist(book1);
Book book2 = createBook("Spring2 Book", 40000, 300);
em.persist(book2);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
//이러면 주문 생성 완료
Delivery delivery = createDelivery(member);
Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
em.persist(order);
}
}
}
-> 잘 들어가잇다!
이제 얘네를 가지고 성능 최적화된 고급 API 기술을 배워볼 것