본문 바로가기
Dev/JPA

[JPA] JPA 활용 II - API 개발 기본

by dev_jsk 2021. 9. 29.
728x90
반응형

회원 등록, 수정, 조회 API 개발을 학습해보자

준비사항

포스트맨 설치

API 테스트 툴을 설치한다. (https://www.postman.com/)

포스트맨 설정

REST API 테스트 시 기본적인 설정

Content-Type을 application/json으로 설정
Body 유형 raw 및 JSON 선택, 예시와 같은 양식으로 데이터 전송

회원등록 API

엔티티를 사용하는 방식과 DTO를 사용하는 방식을 각각 구성해보자

엔티티 사용 회원 컨트롤러 구성

package jpabook.jpashop.api;

import org.springframework.web.bind.annotation.RestController;

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

    /**
     * 등록 V1: 요청 값으로 Member 엔티티를 직접 받는다.
     */
    @PostMapping(value="/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberResponse {
        private Long id;

        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
    
}
  • @RestController = @Controller + @ResponseBody
    - @Controller : 전통적인 스프링 MVC 컨트롤러로 Model 객체를 만들어 데이터를 담고 View를 찾는다.
    - @ResponseBody : 자바 객체를 HTTP 응답 본문의 객체로 변환하여 클라이언트에 전송한다.
    - @RequestBody : HTTP 요청 본문에 담긴 값을 자바 객체로 변환한다.
  • @Data 어노테이션은 Lombok에서 제공하는 것으로 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 어노테이션을 모두 포함하는 것이다.

엔티티 사용시 문제점

  • 엔티티에 API 검증을 위한 로직이 들어가게 되는데 이때 이 로직이 다른 API에서는 필요하지 않을 수 있어 문제가 발생한다.
  • 엔티티가 변경되면 API 스펙이 변경된다.
  • 엔티티 내 중요 정보(모든 값)가 노출된다.
  • 엔티티 내 변수 중 API 스펙을 확인하지 않고서는 어떤값이 바인딩 되는지 알 수 없다.
  • 엔티티 내에 여러 API를 위한 모든 요구사항을 담기 어렵다.

따라서 API 요청 스펙에 맞춰 별도의 DTO를 구성하여 파라미터로 사용해야 한다.

DTO 사용 회원 컨트롤러 구성

package jpabook.jpashop.api;

import org.springframework.web.bind.annotation.RestController;

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

    /**
     * 등록 V2: 요청 값으로 Member 엔티티 대신에 별도의 DTO를 받는다.
     */
    @PostMapping(value="/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;
    }
    
}
  • CreateMemberRequest라는 DTO 내부 클래스를 만들어 파라미터로 사용한다.

DTO 사용해야 하는 이유

  • 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
  • 엔티티와 API 스펙을 명확하게 분리할 수 있어 엔티티가 변해도 API 스펙이 변하지 않는다.

따라서 실무에서는 절대로 엔티티를 API 파라미터로 받거나 외부에 노출해선 안된다.

회원등록 API 동작 결과

요청 URL 및 데이터, 결과 데이터
회원등록 Insert SQL 및 파라미터 바인딩 로그

회원수정 API

DTO를 사용하여 회원수정 구성

회원 컨트롤러 구성

package jpabook.jpashop.api;

import org.springframework.web.bind.annotation.RestController;

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

    /**
     * 수정 API
     */
    @PutMapping(value="/api/v2/members/{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;
    }
    
}
  • @PutMapping
    - 새로운 데이터를 생성하거나 기존 데이터를 대체할 때 사용한다.
    - 주로 데이터 수정 동작에 많이 사용되며 주로 전체 업데이트 시 사용하고 부분 업데이트는 @PatchMapping이나 @PostMapping을 사용하는 것이 REST 스타일에 맞다.
  • @PostMapping@PutMapping 차이
    - @PostMapping은 동일한 요청을 계속해도 결과는 매번 달라지지만 @PutMapping은 동일한 요청을 보낼경우 결과가 매번 같다. 이러한 성질을 멱등성이라 한다.
    - @PostMapping은 주로 데이터 생성 동작에 많이 사용된다.

회원 서비스 구성

package jpabook.jpashop.service;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    /**
     * update one Member
     * @param id
     * @param name
     */
    @Transactional
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id);
        member.setName(name);
    }
}
  • 변경감지를 사용하여 회원정보를 수정한다.

회원수정 API 동작 결과

PUT 방식 요청 URL 및 데이터, 결과 데이터
회원수정 Update SQL 및 파라미터 바인딩 로그

회원조회 API

엔티티를 사용하는 방식과 DTO를 사용하는 방식을 각각 구성해보자

엔티티 사용시 회원 컨트롤러 구성

package jpabook.jpashop.api;

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

    /**
     * 조회 V1: 응답 값으로 엔티티를 직접 외부에 노출한다.
     */
    @GetMapping(value="/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }
}
  • 조회 결과를 회원 엔티티로 반환한다.

엔티티 사용시 문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
  • 엔티티의 모든 값이 노출된다.
  • 응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직..)
  • 같은 엔티티에 대해 용도에 따른 API가 다양하게 만들어지는데 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기 어렵다.
  • 엔티티가 변경되면 API 스펙이 변한다.
  • 컬렉션을 직접 반환하면 향후 API 스펙을 변경하기 어렵다. (별도의 Result 클래스를 생성하여 해결)

따라서 API 응답 스펙에 맞춰 별도의 DTO를 구현하여 반환해야 한다.

DTO 사용시 회원 컨트롤러 구성

package jpabook.jpashop.api;

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

    /**
     * 조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO를 반환한다.
     */
    @GetMapping(value="/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;
    }
}
  • 결과를 전달할 클래스 Result<T>와 DTO를 내부 클래스로 구성한다.
  • 자바 스트림을 이용하여 조회한 데이터를 Map으로 구성한 후 리스트로 반환한다.
  • Result 클래스를 구성한 이유는 결과 데이터 이외에 필요한 필드를 추가하여 사용하기 위함이다.

DTO 사용해야 하는 이유

  • 엔티티가 변해도 API 스펙이 변하지 않는다.
  • 컬렉션을 Result 클래스로 감싸서 향후 필요한 필드를 추가할 수 있다.

Result 클래스 구성 이유

컬렉션 이외 필요한 필드를 추가하여 사용하기 위함이다.

@GetMapping(value="/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, collect.size());
}
    
@Data
@AllArgsConstructor
static class Result<T> {
    private T data;
    private int count;    // 추가필드
}
    
@Data
@AllArgsConstructor
static class MemberDTO {
    private String name;
}

결과 데이터에 count가 추가됐다.

회원조회 API 동작 결과

전체 회원조회이기 때문에 요청 데이터는 없고 결과 데이터만 있다.
전체회원 Select SQL

 

728x90
반응형

댓글