회원 등록 API - V1
@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;
}
}
}
@Entity
@Getter
@Setter
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private Address address;
@NotEmpty
private String name;
...
}
name 쪽에 @NotEmpty가 쓰였다.
컨트롤러까지 presentation 계층이라 한다. 근데 presentation 계층을 위한 검증 로직이 엔티티에 들어가는 문제가 발생한다. 그리고 어떤 api에서는 @NotEmpty가 필요할 수도 필요하지 않을 수도 있다.
또한 name 필드명을 username으로 바꾸게 되면 API 스펙 자체가 바뀌어 API를 호출하는 쪽에서 오류가 발생할 수 있다.
V1 엔티티를 Request Body에 직접 매핑 문제점
등록 V1
요청 값으로 Member 엔티티를 직접 받는다.
문제점
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기는 어렵다. 엔티티가 변경되면 API 스펙이 변한다.
결론
API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다.
회원 등록 API - V2
@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 CreateMemberRequest {
private String name;
}
V2 엔티티 대신에 DTO를 RequestBody에 매핑
등록 V2
요청 값으로 Member 엔티티 대신에 별도의 DTO를 받는다.
- CreateMemberRequest를 Member 엔티티 대신에 RequestBody와 매핑한다.
- 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
- 엔티티와 API 스펙을 명확하게 분리할 수 있다.
- 엔티티가 변해도 API 스펙이 변하지 않는다.
참고: 실무에서는 엔티티를 API 스펙에 노출하면 안 된다!
회원 수정 API
@PutMapping("/api/v2/member/{id}")
public UpdateMemberResponse updateMember2(
@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 UpdateMemberResponse {
private Long id;
private String name;
public UpdateMemberResponse(Long id, String name) {
this.id = id;
this.name = name;
}
}
@Data
static class UpdateMemberRequest {
private String name;
}
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
위 코드에서 update는 순수하게 변경만 한다.
update 메서드에서 수정 작업 후 엔티티를 반환하여 해당 엔티티 값으로 UpdateMemberResponse 값을 채워도 된다 하지만 이렇게 되면 업데이트를 하면서 member를 조회하게 된다. 업데이트(커맨드)라는 건 엔티티를 바꾸는 변경성 메서드다. 근데 이 메서드가 member 엔티티를 반환하게 되면 id를 가지고 member를 조회하는 꼴이 된다. 즉 커맨드랑 쿼리가 같이 있는 상황이 된다. 커맨드와 쿼리를 분리하는 게 유지보수하기에 좋다. member를 반환하는 게 아닌 id값 정도 반환하는 게 좋다. 그래서 update() 후 Member findMember = memberService.findOne(id)를 통해 member를 조회하는 게 좋다. memberService.findOne(id)은 PK로 조회하는 거라 특별하게 트래픽이 많은 API가 아니라면 성능상 이슈는 없다.
회원 조회 API
회원조회 V1: 응답 값으로 엔티티를 직접 외부에 노출
// 조회 V1: 안 좋은 버전, 모든 엔티티가 노출, @JsonIgnore -> 이건 정말 최악
// api가 이거 하나 인가! 화면에 종속적이지 마라!
@GetMapping("/api/v1/member")
public List<Member> membersV1() {
return memberService.findMembers();
}
조회 V1
응답 값으로 엔티티를 직접 외부에 노출하는 방식
문제점
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
기본적으로 엔티티의 모든 값이 노출된다.
응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위 한 프레젠테이션 응답 로직을 담기는 어렵다. 엔티티가 변경되면 API 스펙이 변한다.
추가로 컬렉션을 직접 반환하면 향후 API 스펙을 변경하기 어렵다.(별도의 Result 클래스 생성으로 해결)
결론
API 응답 스펙에 맞추어 별도의 DTO를 반환한다.
참고: 엔티티를 외부에 노출하지 말자
실무에서는 member 엔티티의 데이터가 필요한 API가 계속 증가하게 된다.
어떤 API는 name 필드가 필요하지만, 어떤 API는 name 필드가 필요 없을 수 있다.
결론적으로 엔티티 대신에 API 스펙에 맞는 별도의 DTO를 노출해야 한다.
회원조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO 사용
@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;
}
- 엔티티를 DTO로 변환해서 반환한다.
- member 엔티티를 memberDto로 변환한다.
- 엔티티가 변해도 API 스펙이 변경되지 않는다.
- 추가로 Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.
- 컬렉션을 감싸지 않고 응답값으로 보내면 json 배열 타입으로 나가서 유연성이 떨어진다.
- 예를 들어 컬렉션을 그대로 반환하다가 컬렉션의 요소 개수들을 추가로 보내달라고 할때 응답값 자체를 수정하게 된다.
'JPA > JPA 활용 2' 카테고리의 다른 글
5. API 개발 고급 - 실무 필수 최적화 (0) | 2024.08.24 |
---|---|
4. API 개발 고급 - 컬렉션 조회 최적화 (0) | 2024.08.24 |
3. API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2024.08.23 |
2. API 개발 고급 - 준비 (0) | 2024.08.22 |