-
[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를 직접 반환하지 말자.
끝.
반응형'Spring' 카테고리의 다른 글
[스프링 5 레시피] 2장: 스프링 코어 (2-7 ~ 2-12) (0) 2021.03.04 [스프링 5 레시피] 2장: 스프링 코어 (2-1 ~ 2-6) (4) 2021.02.26 [JPA] JpaRepository save() 메서드 주의 사항 (1) 2021.02.17 [JPA] N+1 문제 해결하기 (0) 2020.12.25 [JPA ] Auditing 기능 살펴보기 (1) 2020.11.26