JPA 활용 1편에서는 JPA로 도메인을 설계하고, Repository와 Service에 로직을 구현했으며, Thymeleaf로 화면단을 구현했다. JPA 활용 2편에서는 이전에 작성한 로직을 기반으로 API를 개발하여 다양한 방법으로 최적화와 DTO 활용을 적용한다.
API 개발 - 회원 등록 API
V1 : 엔티티를 RequestBody에 직접 매핑한다.
@PostMapping("/api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) { // 이렇게 Member를 외부에 노출시키면 안되며 DTO를 파라미터로 받아야 함
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest { // dto
@NotEmpty // 검증 로직을 dto에 추가하여 엔티티와 API 스펙을 분리한다.
private String name;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
위 코드와 같이 Member 객체인 엔티티를 직접 받아와서 구현하는 것은 문제점이 있다.
- 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
- 엔티티에 API 검증을 위한 로직이 들어간다.(@NotNull, @NotEmpty 등)
- 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 API를 위한 모든 요청 요구사항을 담기는 어렵다.
즉, 엔티티가 변경되면 API 스펙이 변하기 때문에 API 요청 스펙에 맞추어 별도의 DTO 파라미터로 받아야 한다.
V2 : 엔티티 대신에 DTO를 RequestBody에 매핑
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) { // @RequestBody에 dto를 매핑
Member member = new Member();
member.setName(request.name);
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest { // dto
@NotEmpty // 검증 로직을 dto에 추가하여 엔티티와 API 스펙을 분리한다.
private String name;
}
이와 같이 CreateMemberRequest라는 DTO를 RequestBody와 매핑하면 엔티티를 직접적으로 노출하지 않는다. 따라서 Member에서 어떤 값이 넘어오는지 모르는 것을 해결할 수 있고, 엔티티가 바뀌어도 API 스펙이 변하지 않아 유지보수가 쉬워진다.
- 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
- 엔티티와 API 스펙을 명확하게 분리할 수 있다.
- 엔티티가 변해도 API 스펙이 변하지 않는다.
즉, 엔티티를 외부에 노출하지 않고 API 스펙에 맞춘 DTO를 만들어서 사용해야 한다. (실무에서는 엔티티를 API 스펙에 노출하면 안된다.)
API 개발 - 회원 수정 API
// ApiMemberController
@PatchMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id,
@RequestBody @Valid UpdateMemberRequest request) { // DTO를 파라미터에 매핑
memberService.update(id, request.getName());
Member findMember = memberService.findOne(id);
return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
/** UpdateMemberRequest, UpdateMemberResponse 는 DTO */
// MemberService
// 회원 수정
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id); // member는 영속 상태
member.setName(name);
}
데이터를 수정할 때는 변경 감지를 사용하는 것이 좋다.(Service 계층에서 @Transactional 사용)
회원 정보를 업데이트하는 과정에서 PUT은 전체 업데이트할 때 사용하는 것이고, 부분 업데이트를 하려면 PATCH나 POST를 사용하는 것이 REST 스타일에 맞다.
API 개발 - 회원 조회 API
V1 : 응답 값으로 엔티티를 직접 외부에 노출
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
회원 등록 V1에서도 볼 수 있듯, Member 엔티티 자체가 직접 외부에 노출되어 있다. 이와 같이 사용했을 때의 문제점은 아래와 같다.
- 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
- 기본적으로 엔티티의 모든 값이 노출된다. -> 조회 결과를 보면 Member 내에서 Order 등까지 모두 노출됨
- 응답 스펙을 맞추기 위해 로직이 추가된다. (별도의 뷰 로직, @JsonIgnore 등) -> @JsonIgnore을 엔티티에 직접 추가된다.
- 실무에서는 같은 엔티티에 대해 API가 여러 용도로 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.
- 엔티티가 변경회면 API 스펙이 변한다.
- 컬렉션을 반환하게 되면 향후 API 스펙을 변경하기 어렵다. -> 별도의 Result 클래스 생성으로 해결
즉, 위와 같은 문제점들을 해결하기 위해 API 응답 스펙에 맞추어 별도의 DTO를 반환하도록 해야 한다. 실무에서는 하나의 엔티티에 대하여 용도에 따른 다양한 엔티티가 구현되는데 어떤 API는 name 필드가 필요하지만, 어떤 API는 name 필드 없이 age 필드만 필요하다. 결론적으로 이런 경우들을 해결하기 위해 엔티티 대신에 API 스펙에 맞는 별도의 DTO를 노출해야 한다.
V2 :응답 값으로 엔티티가 아닌 별도의 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;
}
위 결과는 V1 문제점들을 해결한 것으로 엔티티를 DTO로 변환하여 반환하고 있다. 이렇게 하면 엔티티가 변해도 API 스펙이 변하지 않으며, Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.
반환된 결과를 보면 Result 클래스에 따라 data로 컬렉션을 감싸서 MemberDto 필드들을 배열로 반환한 것을 확인할 수 있다.
[출처]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94 → 이 글은 김영한님의 JPA 활용 2편 강의 중 1장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA 활용 2] 3. API 개발 고급 - 컬렉션 조회 최적화 (0) | 2024.08.17 |
---|---|
[JPA 활용 2] 2. API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2024.08.15 |
[JPA 기본편] 11. 객체지향 쿼리 언어2 - 중급 문법 (0) | 2024.06.24 |
[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.23 |
[JPA 기본편] 9. 값 타입 (0) | 2024.06.21 |