김영환님의 강의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 보면서 공부한 내용입니다.
📝 V1
@RestController // @Controller + @ResponseBody 를 합친 어노테이션
// => 데이터 자체를 json이나 xml으로 바로 보내자라는 의미
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member){
// api만들 때 항상 엔티티를 파라미터로 받지 말기 + 웹에 노출해서도 안됨
// => api를 위한 별도의 DTO를 만들어야함
return new CreateMemberResponse(id);
Long id = memberService.join(member);
}
@Data
static class CreateMemberResponse{
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
}
📝 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 { // DTO
private String name;
}
@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;
}
📝 결과
📝 V1
@GetMapping("/api/v1/members")
public List<Member> membersV1(){
return memberService.findAll();
}
// 엔티티를 직접 노출하게 되면 엔티티에 있는 정보들이 다 외부에 노출이 됨
✅ 회원정보만 조회하고 싶은데 orders도 함께 조회됨
즉, 기본적으로 엔티티의 모든 값이 노출됨
✅ 실무에서는 같은 엔티티에 대해 api가 용도에 따라 다양하게 만든어지는데, 한 엔티티에 각각의 api를 위한 프레젠테이션 응답 로직을 담기 어려움
✅ 엔티티가 변경되면 api 스펙이 변함
📝 V2 (정석)
@GetMapping("/api/v2/members")
public Result MemberV2(){
List<Member> members = memberService.findAll();
List<Object> collect = members.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;
}
✅ 노출하고 싶은 데이터만 노출 가능
✅ 엔티티가 변경되어도 api 스펙이 변하지 않음
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
}
✅ 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭 한 곳을 @JsonIgnore
처리 해야한다. 아니면 무한 루프 발생함
✅ 엔티티를 API응답으로 외부로 노출하는 것은 좋지 않으므로 DTO로 변환해서 반환하는 것이 더 좋다
✅ 즉시 로딩으로 설정하면 연관관계가 필요없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)
을 사용해라
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
@Data
static class SimpleOrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order){
orderId = order.getId();
name = order.getMember().getName();
// Lazy 초기화 => getMember() 까지는 프록시이지만
// getName()을 가져오는 순간 실제 name을 끌고와야되기 때문)
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // Lazy 초기화
}
}