API도 Request와 Response로 데이터가 이동하기 때문에 Controller를 정의한다. 일반적으로 @Controller가 아닌 @RestController 어노테이션을 사용하는데, @RestController 는 @Controller에 @ResponseBody가 추가된 것으로 Json 형태로 객체 데이터를 반환할 수 있다.
@RestController
@RequiredArgsConstructor
public class MemberApiController {
...
}
1. Entity를 파라미터로 처리하지 않기(feat. 회원 생성)
하단의 코드는 회원을 추가하는 두 가지 방식이다. 첫 번째 방식은 Member Entity를 인자로 받아서 처리하고 있고, 두 번째 방식은 CreateMemberRequest라는 별도의 DTO 객체를 생성해서 처리하고 있다.
실제 서비스에서는 회원 Entity를 위한 API가 다양하게 만들어지는데, 하나의 Entity에 여러 API를 위한 모든 요청 요구사항을 담기는 어렵다. 그러므로 요구 사항을 충족하는 프로퍼티를 포함하는 별도의 DTO 객체를 생성해서 이를 파라미터로 사용해야 하는데, 다음과 같은 장점이 있다.
① Entity와 Presentation 계층을 위한 로직을 분리할 수 있다.
② Entity와 API 스펙을 명확하게 분리할 수 있다.
③ Entity가 변해도 API 스펙이 변하지 않는다.
// 방법 1 - 사용 X
@PostMapping("/api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
// 방법 2 - 사용 O
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
memberService.join(member);
return new CreateMemberResponse(member.getId());
}
// Member 생성 DTO 객체 생성
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
2. 회원 수정, 조회
1) 회원 수정
마찬가지로 UpdateMemberRequest라는 DTO를 생성한 후 이를 파라미터로 수정 메서드를 구현하였다. update 함수가 없었기 때문에 memberService에서 @Transactional로 메서드를 선언하여 JPA가 commit 시점에서 "변경 감지"를 통해 DB에 반영되게 하였다.
@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());
}
// Member 수정 - DTO 객체 생성
@Data
static class UpdateMemberRequest {
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse {
private Long id;
private String name;
}
// MemberService - 회원 수정 로직 추가
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
2. 회원 조회
Member Entity에는 Order 등 여러 필드가 있다. 굳이 방법 1처럼 모든 필드 값을 다 반환할 필요가 있을까? 보안적으로도 좋지 않은 선택이다. 마찬가지로 방법 2처럼 원하는 필드만 포함하고 있는 DTO를 사용하자.
// 방법 1
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
// 방법 2
@GetMapping("/api/v2/members")
public SearchResult membersV2() {
List<Member> findMembers = memberService.findMembers();
List<MemberDTO> collect = findMembers.stream()
.map(m -> new MemberDTO(m.getName()))
.collect(Collectors.toList());
return new SearchResult(collect.size(), collect);
}
// 조회 - DTO 객체 생성
@Data
@AllArgsConstructor
static class SearchResult<T> {
private int count; // json에 다른 필드를 넣기에도 용이하다
private T data;
}
@Data
@AllArgsConstructor
static class MemberDTO {
private String name;
}
DTO를 따로 생성해서 만들면 Entity와 API 스펙이 명확하게 분리되기 때문에 다른 필드를 넣기에도 용이하다!