실전! 스트링 부트와 JPA 활용 2 - API 개발 기본

이태휘·2022년 11월 1일
0
post-custom-banner

REST API 개발에 초점을 맞출 것임

-> 등록, 수정, 조회 REST API 개발
-> API 개발 실무 노하우
-> JPA를 사용하여 성능 최적화를 중점으로 볼 것임! 얘를 사용하면 진짜 성능이 많이 올라갈것이니깐 성능 최적화를 해보자

회원 등록 API

회원 API 개발하기
-> 등록, 수정, 조회 API 개발할것

POSTMAN을 사용해서 API 테스트 해볼 것임!

  • api 패키지를 따로 만들어서 관리를 해주자.

회원 API 개발하기
-> 등록, 수정, 조회 API 개발할것

@RestController == ResponseBody + Controller

-> @ResponseBody는 데이터를 직접 보내게 해주는 어노테이션

  • MemberApiController
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 파라미터로 받는것 금지

  • 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 노출 절 대 금 지 !!!!

회원 수정 API

수정은 put으로 사용할 것임
-> 같은 것을 호출한다해서 여러번 바뀌는게 아니라 URL 조금 변경해야해

  • MemberService에 만들어주기
	@Transactional
    public void update(Long id, String name) {
        //영속상태인 멤버를 setname으로 바꿔주면 트랜젝션 커밋 되는 순간에 jpa가 플러시하고 변경된거 반영
        Member member = memberRepository.findOne(id);
        member.setName(name);
    }

-> 만약 update에서 뭔갈 반환을 한다면 커맨드랑 쿼리랑 같이있는 코드가 됨.
-> 즉, 반환을 하는 커맨드와 변경을 위해 조회를 하는 쿼리가 같이있어
-> 그래서 보통 putmapping 쪽에서 건드는게 좋음

  • 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 마지막에 멤버 아이디 넣는거 잊지말기 !

-> 업데이트 쿼리도 날려진 것

회원 조회 API

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를 이렇게 노출하면 안됨!

  • 또한, JSON이 배열형식으로 반환하기 떄문에 이 형식을 유지해야해서 안조아

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로 !
-> 이래야 유연하게 할 수 있고 유지보수도 쉬워져

API 개발 고급 소개

1) 조회용 샘플 데이터 입력
2) 지연 로딩과 조회 성능 최적화
3) 컬렉션 조회 최적화
-> 조인을 했는데 데이터가 뻥튀기가 될 경우가 있어 (1:N 일때)
4) 페이징과 한계 돌파
5) OSIV와 성능 최적화

조회용 샘플 데이터 입력

API 개발 고급 설명을 위한 샘플 데이터 입력

-> 최종적으로 주문이 두건 이루어진 주문 데이터 만드는것임

  • InitDb 클래스를 만들어서 작성함
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 기술을 배워볼 것

profile
풀스택 개발자 가보자구~
post-custom-banner

0개의 댓글