일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Spring Data JPA
- 값 타입 컬렉션
- 스프링
- jpa 활용
- 컬렉션 조회 최적화
- 타임리프 문법
- JPA
- Bean Validation
- 김영한
- 기본문법
- 프로젝트 환경설정
- 스프링MVC
- 실무활용
- JPA 활용 2
- 임베디드 타입
- 일론머스크
- API 개발 고급
- 로그인
- 페이징
- 타임리프
- 검증 애노테이션
- 스프링 데이터 JPA
- 스프링 mvc
- 벌크 연산
- 예제 도메인 모델
- JPQL
- 불변 객체
- 트위터
- JPA 활용2
- QueryDSL
- Today
- Total
RE-Heat 개발자 일지
[JPA 활용2] [1] API 개발 기본 본문
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] 회원 등록 API
MemberApiController - 회원등록 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
@AllArgsConstructor
static class CreateMemberResponse {
private Long id;
}
- @RestController = @Controller + @ResponseBody => JSON 반환 API용
- @RequestBody : 클라이언트에서 json 형식으로 데이터가 들어온다고 가정한다.
- @Valid : 요청 데이터를 받을 때 발생되는 오류 처리
- CreateMemberResponse 리턴용 객체 => Spring이 알아서 JSON화 해서 리턴해준다.
실행화면 - @NotEmpty 적용 이전
- 값을 넣어주지 않아도 id값이 부여되는 것을 확인할 수 있다.
- 이를 방지하기 위해선 반환한 Member의 name필드에 @NotEmpty 애노테이션을 붙여주면 된다.
참고] @NotBlank vs @NotEmpty vs @NotNull
- @NotBlank : null, "", " " 모두 허용하지 않는다.
- @NotEmpty : null과 "" 허용하지 않는다. 하지만 " "는 허용한다.
- @NotNull : null은 허용하지 않지만, "", " "는 허용한다.
실행화면 - @NotEmpty 적용 후
API 예외처리 관련 내용은 스프링 MVC 2편 - [9] API 예외 처리 참조
■ V1 : 엔티티를 RequestBody에 직접 매핑하면 생기는 문제점
- 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
- 엔티티에 API 검증을 위 한 로직이 들어간다. (@NotEmpty 등등)
- 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모
든 요청·요구사항을 담기는 어렵다. - 엔티티가 변경되면 API 스펙이 변한다.
- 예를 들어 String name이 String username으로 변경되면 클라이언트 측에서 보내는 API 요청 데이터를 전부 바꿔야 하는 번거로움이 발생한다.
결론 : API 요청 스펙에 맞춰 별도의 DTO를 파라미터로 받는다.
MemberApiController - 회원등록 V2(엔티티 대신 DTO를 RequestBody에 매핑)
@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;
}
- Member 엔티티 대신 CreateMemberRequest를 RequestBody와 매핑한다.
- 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
- ex) @NotEmpty를 부담 가지지 않고 Dto에 붙일 수 있다.
- 엔티티와 API 스펙을 명확하게 분리할 수 있다.
- 클라이언트에 요구하는 다양한 스펙도 전용 DTO로 맞춰줄 수 있다.
- 엔티티가 변해도 API 스펙이 변하지 않는다.
- DTO에서 엔티티를 스펙에 맞게 변환해 주는 과정이 있어 엔티티에 변경점이 생겨도 문제가 없다.
참고] 실무에서는 엔티티를 API 스펙에 노출하면 안 된다!
[2] 회원 수정 API
MemberApiController - 회원 수정
//회원 수정
@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;
}
- 요청·응답용 DTO 추가
- 영한님 : 엔티티는 @Getter 정도만 사용하나, DTO는 @Data 등 롬복 애노테이션 자유롭게 사용한다.
- 수정 목적이므로 @PutMapping 사용
- MemberService에 update 메소드 추가
참고] void가 아닌 Member객체로 반환하면 커맨드(update는 커맨드 메서드)와 쿼리(조회)가 같이 있는 꼴이다. 따라서 update 메소드는 변경감지로 update만 시켜주고 Controller에선 바뀐 값을 조회하는 식으로 확실하게 분리해 준다.
[3] 회원 조회 API
MemberApiController - 회원 조회 V1
//회원 조회
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
조회 V1 : 응답 값으로 엔티티(Member)를 직접 외부에 노출한 버전이다.
■ 문제점
- 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
- 기본적으로 엔티티의 모든 값이 노출된다.
- 응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
- 실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의
API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.- 어떤 클라이언트는 orders를 노출하고 싶다고 하면 일이 복잡해진다.
- 엔티티가 변경되면 API 스펙이 변한다.
- 앞서 말한 String name => String username으로 바뀌는 케이스를 떠올려 보자.
- 컬렉션을 직접 반환하면 향후 API 스펙을 변경하기 어렵다.
- List<Member>를 그대로 반환하면 [] 배열 형태로 반환돼 더는 확장할 수 없다. 예를 들어 컬렉션 크기인 count 데이터를 추가하고 싶다고 해도 이런 상황에선 할 수 없다.
해결책 : 별도의 DTO를 사용하자!
MemberApiController - 회원 조회 API V2
@GetMapping("/api/v2/members")
public Result membersV2() {
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;
}
- MemberDTO로 반환 (List<Member> => List<MemberDTO>)
- Result<T> : 컬렉션을 직접 반환하면 확장하기 어려우므로 추가로 Result 클래스로 컬렉션을 감싸서 반환한다.
실행결과
■ []로 닫힌 형태가 아니기 때문에 count값을 추가할 수 있다.
public Result membersV2() {
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;
}
- Result에 int count 추가 후 그 값에 컬렉션 size를 넣어줌
실행결과
최종결론 : 엔티티를 외부에 노출하지 마라!!(추천이 아닌 필수사항)
'백엔드 > JPA' 카테고리의 다른 글
[JPA 활용2] [3] API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2023.08.31 |
---|---|
[JPA 활용2] [2] API 개발 고급 - 준비 (0) | 2023.08.31 |
[JPA 활용1] [5] 웹 계층 개발(하편) (0) | 2023.08.27 |
[JPA 활용1] [5] 웹 계층 개발(상편) (0) | 2023.08.26 |
[JPA 활용1] [4] 상품·주문 도메인 개발 (0) | 2023.08.25 |