JPA : JPA를 활용한 회원 관련 API 개발

    이 포스팅은 인프런 김영한님의 JPA 강의를 듣고 복습하며 정리한 내용입니다.


    API 개발

    어느 순간 앱을 개발해야 할 시점이 왔다고 가정하자. 이 시점에서는 기존에 웹을 개발하던 것처럼 View Template을 반환하는 것이 아닌 API 형식으로 데이터를 주고 받아야한다. 어플리케이션은 API 데이터를 받아, 클라이언트에서 필요한 것들을 구현해주기 때문이다.

     


    API 개발을 할 때 주의해야 할 사항 

     

    1. 엔티티를 파라메터로 받지 마라.

    엔티티는 해당 API 뿐만 아니라 정말 다양한 곳에서 사용될 수 있다. 예를 들어 엔티티의 name이 userName으로 바뀌었다고 가정해보자. 이럴 경우, 어느 순간 특정 API 컨트롤러가 고장이 나는 경우가 발생할 수 있다. 따라서 DTO(Data Transfer Obejct)를 이용해서 클라이언트로 값을 받는다.

     

    2. 엔티티를 응답으로 보내지마라.

    동일한 이유로 엔티티를 응답으로 내보내면 안된다. 클라이언트는 특정 API 스펙에 의존하고 있을텐데, 이 때 엔티티가 바뀌게 되면 클라이언트에서는 먹통이 된다. 따라서 응답으로 보낼 때는 DTO(Data Transfer Object)를 활용해서 응답한다.

    public class Member {
        @JsonIgnore
        @OneToMany(mappedBy = "member") 
        private List<Order> orders = new ArrayList<>();
    }

    또한, 엔티티를 돌려주게 될 경우 도메인 계층이 웹 계층에 의존하게 된다는 문제도 있다. 예를 들어 엔티티를 응답할 때, Member 안의 orders라는 값이 API 스펙에서는 필요없다고 해보자. API SPEC을 맞추기 위해 @JsonIgnore를 사용할 수 있다. 이 경우, 엔티티에 응답을 위한 웹계층 로직이 들어가버린다. 이렇게 되며 양방향 의존관계가 생긴다는 문제가 있다. 

    이렇다고 문제가 해결되지도 않는다. 다른 API에서는 저 orders라는 값이 필요할 수도 있기 때문에 근본적인 문제 해결조차 되지 않는다. 

     

    3. 응답은 가급적이면 JSON List 형식으로 보내지 않는다.

    JSON List 형식으로 보내게 될 경우, 이 응답에 변수를 추가하기가 쉽지 않다. 왼쪽 같은 경우에 "count"라는 스펙이 추가된다면, 넣을 방법이 매우 복잡해진다. 반면 오른쪽처럼 리스트를 JSON 안쪽에 포함시켜주면, 손쉽게 "count"라는 스펙을 추가할 수 있다. 

    즉, LIST<DTO>를 한번 더 DTO로 감싸서 응답을 내보내 주게 되면 오른쪽과 같이 편리하게 관리할 수 있다. 

     


    코드 실습

    https://github.com/chickenchickenlove/JPA1/blob/b93ea930dd097adadaa5caf72deea79fcc996549/jpashop/main/java/hellojpa/jpa/api/MemberApiController.java

    엔티티를 파라메터로 받는 회원등록 API

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody Member member) {
        Long saveId = memberService.join(member);
        return new CreateMemberResponse(saveId);
    }
    • 엔티티를 API에서 직접 받기 때문에 엔티티 수정되면 큰 장애가 발생할 수 있다. 

     

    DTO를 파라메터로 받는 회원등록 API

    @PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody CreateMemberRequest request) {
      
        Member member = new Member();
        member.setName(request.getName());
    
        Long saveId = memberService.join(member);
        return new CreateMemberResponse(saveId);
    }
    
        @Getter
        static class CreateMemberRequest{
            private String name;
        }
    
    
        @Data
        @NoArgsConstructor
        static class CreateMemberResponse {
            private Long id;
    
            public CreateMemberResponse(Long id) {
                this.id = id;
            }
        }
    • DTO를 통해서 파라메터를 받고, 응답해주기 때문에 엔티티 변경과 좀 더 무관해진다.

     

    엔티티를 받아 수정, 엔티티로 응답하는 수정 API

    @PutMapping("/api/v2/members/{memberId}")
    public UpdateMemberResponse updateMemberV2(@RequestBody UpdateMemberRequest request,
                                               @PathVariable(name = "memberId") Long id) {
    
        memberService.updateMember(id, request.getName());
        Member findMember = memberService.findOne(id);
    
        return new UpdateMemberResponse(findMember.getId(), findMember.getName());
    }
    @Getter
        static class UpdateMemberRequest {
            private String name;
    
            public UpdateMemberRequest(String name) {
                this.name = name;
            }
        }
    
    
        @Getter
        @AllArgsConstructor
        static class UpdateMemberResponse {
            private Long id;
            private String name;
        }
    • DTO를 통해서 파라메터를 받고, 응답해주기 때문에 엔티티 변경과 좀 더 무관해진다.

     

    엔티티를 반환하는 회원조회 API

    @GetMapping("/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }
    • 엔티티를 직접 돌려주기 때문에 엔티티 변경에 민감하다.
    • API 응답 SPEC에는 필요없는데 엔티티에 있는 값이 있다면 @JsonIgnore 처리를 해야한다. 즉, 도메인이 웹에 의존하게 된다.

     

    엔티티 리스트를 DTO로 감싸서 응답하는 회원조회 API

    @GetMapping("/api/v3/members")
    public Result membersV3() {
        List<Member> members = memberService.findMembers();
    
        List<MemberDto> collect = members.stream()
                .map(member -> new MemberDto(member.getName()))
                .collect(Collectors.toList());
        return new Result(collect);
    }
    
        @Data
        static class Result {
            private List<MemberDto> members;
    
            public Result(List<MemberDto> members) {
                this.members = members;
            }
        }
    
    
        @Data
        static class MemberDto {
            private String name;
    
            public MemberDto(String name) {
                this.name = name;
            }
        }
    • DTO 리스트를 다시 한번 DTO로 감싸서 반환했다.
    • 엔티티 변경에 둔감하고, DTO에 다른 Spec을 추가하는데도 문제가 없다. 

     

    'Spring > JPA' 카테고리의 다른 글

    JPA : Order와 관련된 API 구현하기  (0) 2022.01.15
    JPA : JPA를 DTO로 바로 접근하기  (0) 2022.01.15
    JPA : @Transacitonal 관련 정리  (0) 2022.01.10
    JPA 프로젝트를 다시 하면서 정리  (0) 2022.01.09
    JPA : Fetch Join  (0) 2021.12.02

    댓글

    Designed by JB FACTORY