ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Mvc] JPA 환경에서 Rest API를 잘 작성하는 방법
    Spring 2020. 11. 7. 23:25
    반응형

    * 이 글은 김영한님의 [실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 강의를 참고하여 작성 하였습니다.

     

     

    1. 회원 등록 API

    - 문제가 있는 version 1 API

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
    	Long id = memberService.join(member);
    	return new CreateMemberResponse(id);
    }

     

    @Entity
    public class Member {
    
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
    
        @NotEmpty ---------> 문제
        private String name;
    
        @Embedded
        private Address address;
    
        @JsonIgnore
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
    
    }
    • 프레젠테이션 계층(화면에 종속적인)을 위한 validation 코드가 애플리케이션 전반에 사용하는 Entity인 Member에 들어간다.
    • 문제 1 ==> 위의 post method '/api/v1/members' API에서는 Member의 name이 필수 값이어서 @NotEmpty 애노테이션이 필요하지만, 다른 API에서는 name 값이 필요하지 않을 수도 있다.
    • 문제 2 ==> DB 구조가 변경되어 Member Entity의 name이 username으로 바뀐다면, API 스펙이 바뀐다. 예를들면, 클라이언트에서 API를 호출할 때 "name": "member1"이었지만 "username": "member1"로 변경된다.
    • Request Body의 JSON 바인딩 객체를 Member가 아니라 해당 API에 맞는 DTO 클래스를 작성하여 개선해보자.

     

    - 개선된 version 2 API

    @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);
    }
    
    // inner class
    @Data
    static class CreateMemberRequest {
    	@NotEmpty
    	private String name;
    }
    
    // inner class
    @Data
    static class CreateMemberResponse {
    	private Long id;
    
    	public CreateMemberResponse(Long id) {
    		this.id = id;
    	}
    }
    • 회원을 등록하는 POST '/api/v2/members' API의 Request 규격에 맞춘 CreateMemberRequest라는 DTO 클래스를 생성해서 Member 대신 사용하였다.
    • 문제 1 해결 ==> Member 대신 DTO 클래스의 name 필드에 @NotEmpty를 사용하여 API에 종속적인 코드를 Entity 클래스에서 제거하였다.
    • 문제 2 해결 ==> Member의 name 필드가 username으로 변경되어도 DTO의 name field는 그대로이기 때문에 API 스펙이 변경되지 않는다.
    • 장점 1 ==> API에 종속적인 DTO를 정의함으로써 유지보수 단계에 Client에서 어떤 데이터가 넘어오는지 DTO 클래스의 필드만 보고 한눈에 알 수 있다.

     

     

    2. 회원 수정 API

    수정 API는 등록과 같은 맥락이므로 생략합니다.

     

     

    3. 회원 조회 API

    - 문제가 있는 version 1

        @GetMapping("/api/v1/members")
        public List<Member> membersV1() {
            return memberService.findMembers();
        }
    • 문제 1 ==> Member Entity를 response함으로써 orders같은 숨기고 싶은 정보가 노출된다. @JsonIgnore를 orders 필드에 추가해서 숨길 수 있지만, orders가  필요한 API에서도 조회할 수 없고, 등록 version 1 API처럼 프레젠테이션 계층에 종속적인 코드가 Entity에 추가된다.
    [ ==> array
    
    -> 이곳에 새로운 필드를 입력하지 못함
    ex) "count": 1 ==> 불가
    
      {
        "id": 1,
        "name": "hello-name",
        "address": null,
        "orders":[]
      },
        {
          "id": 2,
          "name": "member1",
          "address":{
          "city": "서울",
          "street": "test",
          "zipcode": "111-111"
        },
        "orders":[]
      },
      {
        "id": 3,
        "name": "member2",
        "address":{
        "city": "부산",
        "street": "거리",
        "zipcode": "20302-2323"
        },
        "orders":[]
      }
    ]
    • 문제 2 ==> List를 반환함으로써 API response 스펙을 확장할 수 없음.

     

     

    - 개선된 version 2

        @GetMapping("/api/v2/members")
        public Result<List<MemberDto>> memberV2() {
            List<Member> findMembers = memberService.findMembers();
    
            List<MemberDto> collect = findMembers.stream()
                    .map(member -> new MemberDto(member.getName()))
                    .collect(toList());
    
            return new Result<>(collect);
        }
    
        @Data
        @AllArgsConstructor
        static class Result<T> {
            private T data;
        }
    
        @Data
        @AllArgsConstructor
        static class MemberDto {
            private String name;
        }
    • 문제 1 해결 ==> 등록과 동일하게 프레젠테이션 계층에 종속적인 DTO를 사용해서 해당 API에서 노출하고 싶은 데이터만 DTO에 정의하여 사용할 수 있다.
    { ==> json object
    ==> "count": 1 ==> 필드 추가 가능
      "data":[
        {
        	"name": "hello-name"
        },
        {
        	"name": "member1"
        },
        {
        	"name": "member2"
        },
        {
        	"name": ""
        }
      ]
    }

     

    • 문제 해결 2 ==> List를 반환하는 것이 아닌 Rsult 클래스를 정의해서 API 응답 스펙을 유연하게 만들었다. (추가하고싶은 데이터를 Result의 필드에 추가하여 사용한다.)

     

    4. 결론

    • Entity는 API를 통해서 웹에 절대 노출하면 안된다. (해당 API에 종속적인 DTO 클래스를 만들어 사용하자.)
    • List를 직접 반환하지 말자.

    끝.

    반응형
Designed by Tistory.