회원 등록 API
회원 등록 API - V1
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;
import javax.validation.constraints.NotEmpty;
@RestController
@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);
}
@Data
static class CreateMemberResponse{
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
}
Java
복사
•
엔티티를 RequestBody에 직접 매핑한다.
◦
문제점
▪
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
▪
엔티티에 API 검증을 위한 로직이 들어간다.(ex: @NotEmpty ...)
→예를들어, name필드를 어떤 API에서는 NotEmpty하게 사용하고싶어하지만, 또 다른 API에서는Empty역시 허용할 수 있게되면 문제가 될 수 있다.
▪
실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요구사항을 넣어줄 순 없다.
→ 최종접속시간이 추가되고, 어떤 하나의 API때문에 요금정산 수수료 정산율이 추가되고, 하다보면 끝이 없다.
▪
(중요) 엔티티가 변경되면 API의 스펙이 변한다.
→ Member 엔티티의 name 필드가 username이 되는순간 해당 엔티티를 사용하는 API는 모두 변경되야 한다!!
회원 가입 API- V2
→엔티티 대신 DTO를 RequestBody에 매핑했다.
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;
import javax.validation.constraints.NotEmpty;
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@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);
}
@Data
static class CreateMemberResponse{
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
}
}
Java
복사
•
CreateMemberRequest DTO를 Member 엔티티 대신 RequestBody와 매핑한다.
•
엔티티와 프레젠테이션 계층이 분리되었다.
•
엔티티와 API 스펙을 명확하게 분리할 수 있다.
•
엔티티가 변경되어도 API스펙이 변경되지 않는다.
정리
→ 실무에서는 API스펙에 엔티티가 노출되어서는 안된다. 그렇기 때문에 각각에 API에 맞는 DTO를 만들어서 엔티티와 분리시키는게 중요하다.
엔티티를 그대로 쓸 경우의 장점은 아주 조금 간편해진다는 것 뿐이다.
회원 수정 API
회원 수정 API
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;
import javax.validation.constraints.NotEmpty;
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PutMapping("/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;
}
}
Java
복사
•
RequestBody에 사용할 DTO들은 해당 컨트롤러에서만 사용한다면 굳이 외부로 빼지않고 INNER CLASS로 만들어도 된다.
•
update method(Service단)에서 update한 Member를 그대로 반환해줘도 되지만, 커맨드와 쿼리를 철저히 분리한다는 정책이 있기에
update와 같은 커맨드성 로직에서는 가급적이면 비즈니스 로직 종료 후 그대로 끝내거나 식별자(ID)정도만 반환합니다.
그래서 반환받은 측에서 해당 엔티티가 필요하다면 해당 식별자 등을 이용해 다시 Service단에서 조회해서 가져오는 방법을 사용한다.
회원 조회 API
회원 조회 API - V1
@GetMapping("/api/v1/members")
public List<Member> membersV1(){
return memberService.findMembers();
}
Java
복사
•
V1: 응답 값으로 엔티티를 직접 외부에 노출한다.
•
문제점
1.
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.(@JsonIgnore)
2.
기본적으로 엔티티의 모든 값이 노출된다.
3.
응답 스펙을 맞추기 위해 로직이 추가된다(@JsonIgnore, 별도의 뷰 로직 등등)
4.
실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어렵다
5.
엔티티가 변경되면 API스펙이 변한다.(name → username)
6.
추가로 컬렉션을 직접 반환하면 향후 API스펙 변경의 확장성이 몹시 떨어진다. (별도의 Result클래스 생성으로 해결 → ex: Response Object)
•
결론
→ API응답 스펙에 맞춰 별도의 DTO를 반환한다.
회원 조회 API - V2
@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;
}
@Data
@AllArgsConstructor
static class MemberDto{
private String name;
}
Java
복사
•
엔티티를 DTO로 변환해서 반환한다.
•
엔티티가 변해도 API스펙이 변경되지 않는다.
•
추가로 Result class로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.