Spring/SpringBoot&JPA

[스프링부트와 JPA 활용 2] Section1. API 개발 기본

y-seo 2023. 11. 22. 04:08

회원 등록 API

  • 단순히 SQL로 API를 끌어오지 않아도 된다.
  • JPA는 Entity가 있기 때문에 API를 설계할 때 주의해야 한다.
  • API를 test 하기 위해 Postman을 설치한다.
 

Download Postman | Get Started for Free

Try Postman for free! Join 25 million developers who rely on Postman, the collaboration platform for API development. Create better APIs—faster.

www.postman.com

  • 템플릿 엔진을 사용하여 렌더링 하는 Controller와 API 스타일의 Controller 패키지를 분리한다.
    • 예외 처리 등을 할 때 패키지 단위로 할 때가 많은데 화면과 API는 공통 처리 해야 하는 요소가 많이 다르다.
    •  화면은 템플릿 엔진에서 문제가 발생하면 공통 에러 HTML이 나와야 한다.
    • API는 공통 에러용 JSON API 스펙이 나가야 한다.
  • @RestController = @Controller + @ResponseBody
  • jpashop/src/main/java/jpabook/jpashop/api/MemberApiController.java 생성
package jpabook.jpashop.api;

import jakarta.validation.Valid;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member){ //json으로 온 body를 member에 그대로 매핑한다.
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberResponse{ //응답값
        private Long id;

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

 

Emtity에 다른 제약 조건을 걸지 않아 null 값으로 들어가기도 한다.

  • 위 코드는 Entity와 API가 1:1로 매핑되어 있다.
    • Entity는 여러 곳에서 사용되어서 이리저리 바뀔 일이 많은데 Entity를 바꾸었다고 해서 API가 만들어둔 스펙 자체가 바꾸니다는 것이 문제점이다.
    • Entiy를 외부에서 json 오는 것을 바인딩 받는 데에 사용하면 안된다.
    • API 스펙을 위한 별도의 DTO를 만들어야 한다.
    • API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 하여 받는 것이 좋다.
  • 위에 맞게 코드를 추가해보자.
...

@RestController
@RequiredArgsConstructor
public class MemberApiController {

	...

    @PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request){
        Member member = new Member();
        member.setName(request.getName());
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }
    
    ...

    @Data
    static class CreateMemberRequest{
        private String name;
    }


}

  • 이렇게 수정하면 Member Entity가 변경되면 컴파일 오류가 나기 때문에 미리 수정이 가능하다.
    • 중간에서 파라미터와 Entity를 컨트롤하여 매핑해준다.
  • 어떤 파라미터 값들이 넘어오는지도 DTO를 통해 명확히 알 수 있다.

 

회원 수정 API

  • MemberApiController.java 에 코드 추가
...

@RestController
@RequiredArgsConstructor
public class MemberApiController {

	...

    //회원 수정 API
    @PutMapping("/api/v2/members/{id}") //id로 path variable을 가져간다
    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;
    }
}

 

회원 조회 API

  • application.yml 에서 JPA의 설정을 아래와 같아 수정한다.
      ddl-auto: none #데이터를 한 번 넣어두면 계속 반복하여 DB의 데이터를 계속 쓸 수 있다.
  • MemberApiController.java 에 코드 추가
    //회원 조회 API
    @GetMapping("/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }

  • 위 코드의 문제점
    • Entity를 직접 노출하므로 Entity의 정보들(orders)이 외부에 노출되어 버린다.
    • Entity에 Presentation 계층을 위한 Logic이 추가되었다.
    • Entity가 변경되면 API spec이 바뀐다.
    • 응답 spec을 맞추기 위한 로직이 추가된다.
    • 실무에서는 같은 Entity에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 Entity에 각각의 API를 위한 Presentation 응답 Logic을 담기는 어렵다.
    • Array을 반환하면 spec을 확장할 수 없어 유연성이 떨어진다.
  • 결국 API 응답 spec에 맞춘 DTO를 사용하는 것이 좋다.
  • MemberApiController.java에 코드 추가
    @GetMapping("/api/v1/members")
    public Result membersV2() {
        List<Member> findMembers = memberService.findMembers();
        List<MemberDto> collect = findMembers.stream() //List Member을 List Member DTO로 변환
                .map(m -> new MemberDto(m.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;
    }

  • 이렇게 하면 DTO와 API spec이 1:1 관계가 된다.