JPA를 사용하며 API를 개발할 때 올바른 방향에 대해 정리할 수 있는 강의

 

강의에 앞서 postman을 설치하자. 다른 restAPI 툴을 사용해도 됩니다.

 

Download Postman | Get Started for Free

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

www.postman.com

 

 

회원 등록 API


jpashop.api.memberApiController.java

package jpabook.jpashop.api;

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

import javax.validation.Valid;

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

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

    @Data
    static class CreateMemberResponse {
        private Long id;

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

postman에서 api test

파라미터로 데이터를 보내는 것이 아니기때문에 Body에서 raw, JSON 형식으로 바꿔준 후, name을 적어 POST 요청을 보내면 id 값이 1으로 정상적으로 동작하는 것을 볼 수 있습니다. 

 

현재는 Entity에 아무런 제약조건을 걸지 않았기때문에 데이터를 넣지 않아도 @GeneratedValue를 설정한 PK를 제외한 모든 필드가 NULL로 만들어 집니다.

show_sql = true 설정으로 인해 발생한 쿼리문

위와 같이 NULL 값을 포함하지 않게 설정을 하고 싶다면 아래와 같이 @NotEmpty를 추가해주면 됩니다.

@NotEmpty 추가 후 돌리면 name에 값을 설정하지 않았기 때문에 에러가 발생한 것을 볼 수 있습니다.

 

알아야 할 점은 화면에 보여지는 계층을 presentation 계층이라 합니다. 현재 프레젠테이션 계층에 대한 검증 로직이 Member Entity에 추가가 된 것 입니다. 아무런 문제가 없어보이지만, 여기서 발생하는 문제가 있습니다.

  1. 두 개 이상의 api가 있을 때, 하나의 api에서는 @NotEmpty와 같은 검증 로직이 필요하지만 다른 하나의 api에서는 검증 로직이 필요가 없을 수도 있습니다. 
  2. Entity의 필드명을 바꿀 때, name으로 사용하고 있던 필드 명을 username으로 어떤 팀이 바꿨다고 가정해봅시다. 기존 api를 사용할 때 name으로 사용하던 코드들이 모두 정상작동하지 않게됩니다. 
    여기서 알아야할 점은 엔티티를 손보았을 때, api 스펙이 자체가 변하는 것이 문제입니다. Entity라는 것은 굉장히 여러 곳에서 사용하기 때문에 바뀔 확률이 높습니다. 그렇기 때문에, Entity를 손보아도 해당 Entity와 연관된 모든 api 스펙에 영향이 가지 않도록 설계하는 것이 중요합니다. 그렇기때문에 DTO 사용하는 습관을 길러야 합니다.

MemberApiController V2 추가

package jpabook.jpashop.api;

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

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    ...

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

    @Data
    static class CreateMemberRequest{
        private String name;
    }

    ...
}

V2 장점

  1. CreateMemberRequest라는 클래스를 만들어 name만 받도록 api를 설계했습니다. 위와 같이 설계하면 presentation 계층과 Entity 계층을 분리하기 때문에 유지보수가 편리해집니다. 
  2. V1처럼 Member를 파라미터로 두는 경우 알아서 바인딩되어 데이터가 들어가기 때문에 어떤 값이 들어오는지 알 수 없습니다. 하지만, V2의 경우 필요한 데이터만 필드를 만들기 때문에 들어가는 데이터를 알 수 있습니다.
  3. 검증 로직을 Entity에 추가하지 않고 DTO에 추가할 수 있습니다.

 

 

 

회원 수정 API


MemberApiController update 코드 추가

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    
    ...

    @PutMapping("/api/v2/member/{id}")
    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;
    }

    ...
}

 

MemberService 코드 추가

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jpabook.jpashop.repository.MemberRepository;

import java.util.List;

@Service
@RequiredArgsConstructor            // final로 이루어진 필드로만 생성자를 만들어 줌
@Transactional(readOnly = true) //조회 성능 최적화
public class MemberService {

    ...

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

 

테스트

왼쪽 : POST                 /               오른쪽 : PUT

 

 

 

 

회원 조회 API


MemberApiController 코드 추가

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    ...

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

    ...
}

테스트

문제점

  1. 회원에 대한 정보만 조회하려했지만, orders에 대한 정보도 포함되어 있는 것을 볼 수 있다.
    • @JsonIgnore라는 기능을 사용해서 포함시키지 않을 수 있다. 하지만, 위에서 말했듯 Entity는 다양한 api를 통해 호출을 한다. 하나의 api는 orders를 포함시키지 않아야하지만, 다른 하나의 api는 orders를 포함해야될 수 있다. 이러한 상황에서 문제가 생긴다.
  2. 만약, 로그인 기능이라 아이디랑 패스워드가 있다면 노출의 우려가 있다.
  3. 엔티티가 변경되면 api 스펙이 변경된다.
다시한번 강조하지만, presentation 계층과 Entity 계층을 분리하자!! DTO를 사용하자

 

MemberApiController 코드 추가

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    
    ...

    @GetMapping("/api/v2/members")
    public Result memberV2() {
        List<Member> findMembers = memberService.findMembers();
        List<MemberDTO> collect = findMembers.stream()
                .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;
    }

    ...
}

 

테스트

이렇게 두번 감싸면 필요한 필드를 추가할 수도 있다. (array가 첫 시작이 아니기때문에 가능합니다.) 예를들어, 조회한 데이터가 몇개인지 추가해봅시다.

 

MemberApiController 코드 추가

    @GetMapping("/api/v2/members")
    public Result memberV2() {
        List<Member> findMembers = memberService.findMembers();
        List<MemberDTO> collect = findMembers.stream()
                .map(m -> new MemberDTO(m.getName()))
                .collect(Collectors.toList());

        return new Result(collect.size(), collect);
    }

    @Data
    @AllArgsConstructor
    static class Result<T> {
        private int count;
        private T data;
    }

테스트

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의

스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts