[스프링과 JPA활용 2편] 요약 정리1

sonnng·2023년 11월 2일
0

Spring

목록 보기
23/41

API개발 기본

회원등록/수정/조회 API

(1) 회원등록 API

  • API 패키지를 생성
    API를 작성하는 곳과 화면을 렌더링하고 화면관련된 일을 하는 곳은 분리되어야 한다.

  • MemberApiController 생성

  1. @RestController 어노테이션 설정
    api 관련 컨트롤러는 @Controller와 @ResponseBody, @RequiredArgsConstructor가 필요하다. 이때 @Controller와 @ResponseBody는 @RestController로 축약할 수 있다.

  2. api 파라미터 어노테이션 :: @RequestBody
    JSON으로 보내진 바디 내용을 지정한 클래스 인스턴스로 값이 변환된다. 이 역할을 @RequestBody가 수행하게 된다.

  3. api 파라미터 어노테이션 :: @Valid
    @Valid는 javax.validation패키지에 있는 어노테이션이 걸려있는 필드들을 검증하는 역할을 한다. 만약 @NotEmpty로 String name이 거려 있다면, null이거나 공백으로 들어가기만 해도 오류가 발생하게 된다. 이 역할을 한다.

  4. 엔티티에 javax.validation 관련 어노테이션 붙이는 것은 주의
    엔티티 필드명을 바꾸기만 해도 API 스펙 자체가 달라지기 때문에 매번 오류로 고생하게 된다. 예로,엔티티에 있는 필드 String name을 userName으로 변경하기만 해도 API 스펙에서 바로 오류를 발생하게 된다. 엔티티와 API 스펙이 1:1로 매핑되어있기 때문에 발생하는 문제.
    ➡️별도의 DTO를 API파라미터로 받는 것이 권장
    ➡️API파라미터에 엔티티를 작성하지 말아라



POST API 스펙만드는 방법 버전1

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    private final MemberService memberService;

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Validated 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 스펙만드는 방법 버전2

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    private final MemberService memberService;

    @PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody @Validated CreateMemberRequest request){

        Member member = new Member();
        member.setName(request.getName());
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberResponse{
        private Long id;

        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
}


⭐별도의 DTO를 사용하면 좋은점
1. 엔티티 스펙이 바뀌더라도 API 스펙은 변경하지 않아도 된다.
엔티티 스펙으로 변환해주는 메서드들이 setter이므로 API 스펙은 영향을 받지 않는다.
2. 파라미터 값을 명확히 알 수 있다.
필요로 하는 파라미터 값을 명확히 이해할 수 있고 어떤 API 스펙에서 필요로 하는 건지 알 수 있다. 유지보수에 큰 장점이 있다.
3. 엔티티를 외부에 노출하지 않을 수 있다.
외부에 내부 로직을 노출하는 위험이 있을 수 있는데, DTO를 사용하게 되면 매우 안전하다.
4. 엔티티와 API스펙을 명확히 분리할 수 있다.
🌟따라서 요청이 들어오는 것과 나가는 Request, Response는 모두 DTO를 사용🌟


(2) 회원수정 API

  1. controller가 아닌 service 클래스에서 변경감지가 이뤄질 수 있도록 한다.
    controller 클래스에서는 service클래스의 update메서드를 호출, service클래스에서 직접 영속성 컨텍스트와 트랜잭션 관련 활동을 이뤄질 수 있도록 해야한다. 즉, find로 영속성 컨텍스트에서 왔다갔다하도록 한다.

api controller

@PutMapping("/api/v2/members/{id}")
    public UpdateMemberResponse updateMemberV2(
            @PathVariable("id") Long id,
            @RequestBody @Valid UpdateMemberRequest request){
        memberService.update(id, request.getName());
    }

service

@Transactional
    public void update(Long id, String name){
        Member member = findOne(id);
        member.setName(name);
    }
  1. service 클래스에서 update기능을 가진 메서드는 반환값이 되도록이면 void 이거나 id값 정도만 리턴하도록 권장
    "커맨드"와 "쿼리"를 분리하기 위해서 이렇게 작성한다고 한다. 또한 유지보수성도 증가할 수 있는 특징이 있다.

PUT API 스펙만드는 방법

  • API Controller
	@PutMapping("/api/v2/members/{id}")
    public UpdateMemberResponse updateMemberV2(
            @PathVariable("id") Long id,
            @RequestBody @Valid UpdateMemberRequest request){
        memberService.update(id, request.getName());
        Member member = memberService.findOne(id);
        return new UpdateMemberResponse(member.getId(), member.getName());
    }

    @Data
    static class UpdateMemberRequest{
        @NotEmpty
        private String name;
    }

    @Data @AllArgsConstructor
    static class UpdateMemberResponse{
        private Long id;
        private String name;
    }

  • Service
 	@Transactional
    public void update(Long id, String name){
        Member member = findOne(id);
        member.setName(name);
    }

(3) 회원조회 API

조회V1 :: 응답 값으로 엔티티를 직접 외부에 노출

 	@GetMapping("/api/v1/members")
    public List<Member> getMemberV1(){
        return memberService.findMembers();
    }

🫸문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
    - @JsonIgnore 어노테이션이 대표적인 특징이다. JSON반환시 이 어노테이션이 붙은 필드 데이터들은 모두 응답에 사용되지 않는다.
  • 기본적으로 엔티티의 모든 값이 노출된다.
  • 같은 엔티티에 대해 여러 API가 용도에 따라 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기 힘들다.
  • 엔티티가 변경되면 API스펙이 변한다.
  • 추가로 컬렉션을 직접 반환시 향후 API 스펙 변경이 어렵다.

➡️응답 API 스펙에 맞게 별도의 DTO를 반환한다.



조회V2 :: 응답 값으로 엔티티가 아닌 별도의 DTO 사용

	@GetMapping("/api/v2/members")
    public Result getMemberV2(){
        List<Member> members = memberService.findMembers();
        List<MemberDTO> collect = members.stream().map(
                member -> new MemberDTO(member.getName()))
                .collect(Collectors.toList());
        return new Result(collect);
    }
    
    
    @Data
    @AllArgsConstructor
    static class Result<T>{
        private T data;
    }

    @Data
    @AllArgsConstructor
    static class MemberDTO{
        private String name;
    }

이런 방법을 사용한다면 map을 돌면서 member ▶️ MemberDTO 클래스로 감싸게 되고 최종적으로 리턴값으로 MemberDTO list ▶️ Result 클래스로 한번 더 감싸게 된다. 향후 필요한 필드를 추가할 수 있는 이점이 있다.**




API개발 고급 (조회부분)

장애의 90%는 대부분 조회에서 난다. 조회쪽을 어떻게 설계하면 좋을까를 소개하고자 한다.

(1) 조회용 샘플 데이터 입력

/*
* 총 주문 2개
* 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();
        initService.dbInit2();
    }

    @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);
            Order order = Order.createOrder(member, createDelivery(member),
                    orderItem1, orderItem2);
            em.persist(order);
        }
        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);
            Delivery delivery = createDelivery(member);
            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);
            Order order = Order.createOrder(member, delivery, orderItem1,
                    orderItem2);
            em.persist(order);
        }
        private 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;
        }
        private Book createBook(String name, int price, int stockQuantity) {
            Book book = new Book();
            book.setName(name);
            book.setPrice(price);
            book.setStockQuantity(stockQuantity);
            return book;
        }
        private Delivery createDelivery(Member member) {
            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            return delivery;
        }
    }
}

0개의 댓글