[Spring] 스프링 부트와 JPA 활용2(API 개발과 서능 최적화) - API 개발 기본

밀크야살빼자·2023년 5월 29일
0
post-thumbnail

스프링 부트와 JPA 활용2 - API 개발과 서능 최적화 [김영한 강사님]

회원 등록 API

MemberApiController

package jpabook.jpashop.api;

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

import javax.validation.Valid;

@RestController //controller + ResponseBody
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

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

    }

    @PostMapping("/api/v2/members")
    // RequestBody는 json으로 온 바디를 그대로 매핑해서 넣어줌
    // @Valid -> 엔티티에 제약조건을 넣어주면 검증해줌
    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;
    }
    @Data
    static class CreateMemberResponse{
        private Long id;

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

        }
    }
}

Request Body에 직접 매핑
문제점

  • 엔티티 프레젠테이션 계층을 위한 로직이 추가된다.
  • 엔티티에 API 검증을 위한 로직이 들어간다(@NotEmpty 등등)
  • 엔티티가 변경되면 API 스페익 변한다.

결론

  • API 요청 스펙에 맞춰 별도의 DTO를 파라미터로 받는다.

회원 수정 API

@PutMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request) {
	//transactional이 끝남
    memberService.update(id, request.getName()); 
    //id를 가지고 쿼리를 날려 정상적으로 잘 됐는지 확인
    Member findMember = memberService.findOne(id); 
    //responseDto를 만들어서 반환
    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;
}
public class MemberService {
    
    private final MemberRepository memberRepository;
    
   
    
    @Transactional //Transactional 있는 상태에서 조회하면,
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id); // 영속성 컨텍스트에서 member를 가져옴
        member.setName(name); //해당 값을 수정하면 엔티티가 바뀜
        // transactional이 끝나고 commit 되는 시점에서 jpa가 변경감지 실행을 함 -> update 쿼리 날림
     
    }
}
  • DTO에는 자체 로직이 들어있지 않기 때문에 @Data 많이 사용
  • DB에서 영속성 컨텍스트로 Member 가져욤 @Transactional 어노테이션에 의해 트랜잭션 종료 시 commit 발생
  • update에서 Member를 반환해도 되지만 변경 메소드에서 Member를 반환하면 조회하는 것과 같다

커맨드와 쿼리를 분리
오류 정정
회원 수정 API updateMemberV2은 회원 정보를 부분 업데이트 한다. 여기서 PUT 방식을 사용했는데, PUT은 전체 업데이트를 할 때 사용하는 것이 맞다. 부분 업데이트를 하려면 PATCH를 사용하는 것이 REST 스타일에 맞다.

회원 조회 API

엔티티 노출

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    
    private final MemberService memberService;
    
    @GetMapping("/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }
}

문제점

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

결론

  • API 응답 스펙에 맞추어 별도의 DTO를 반환한다.
  • 조회 V1: 안 좋은 버전, 모든 엔티티가 노출, @JsonIgnore -> 이건 정말 최악, api가 이거 하나인가! 화면에 종속적이면 안된다.

❗참고❗
엔티티를 외부에 노출하면 안된다. 실무에서는 member 엔티티의 데이터가 필요한 API가 계속 증가하게 된다. 어떤 API는 name 필드가 필요하지만, 어떤 API는 name 필드가 필요없을 수 있다. 결론적으로 엔티티 대신에 API 스펙에 맞는 별도의 DTO를 노출해야 한다.

DTO 사용

@GetMapping("/api/v2/members")
public Result membersV2() {
    
    List<Member> findMembers = memberService.findMembers();
    //엔티티 -> DTO 변환
    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;
}
  • 엔티티를 DTO로 변환해서 반환한다.
  • 엔티티가 변해도 API 스펙이 변경되지 않는다.
  • 추가로 Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.
profile
기록기록기록기록기록

0개의 댓글