API 패키지를 생성
API를 작성하는 곳과 화면을 렌더링하고 화면관련된 일을 하는 곳은 분리되어야 한다.
MemberApiController 생성
@RestController 어노테이션 설정
api 관련 컨트롤러는 @Controller와 @ResponseBody, @RequiredArgsConstructor가 필요하다. 이때 @Controller와 @ResponseBody는 @RestController로 축약할 수 있다.
api 파라미터 어노테이션 :: @RequestBody
JSON으로 보내진 바디 내용을 지정한 클래스 인스턴스로 값이 변환된다. 이 역할을 @RequestBody가 수행하게 된다.
api 파라미터 어노테이션 :: @Valid
@Valid는 javax.validation패키지에 있는 어노테이션이 걸려있는 필드들을 검증하는 역할을 한다. 만약 @NotEmpty
로 String name이 거려 있다면, null이거나 공백으로 들어가기만 해도 오류가 발생하게 된다. 이 역할을 한다.
엔티티에 javax.validation 관련 어노테이션 붙이는 것은 주의
엔티티 필드명을 바꾸기만 해도 API 스펙 자체가 달라지기 때문에 매번 오류로 고생하게 된다. 예로,엔티티에 있는 필드 String name을 userName으로 변경하기만 해도 API 스펙에서 바로 오류를 발생하게 된다. 엔티티와 API 스펙이 1:1로 매핑되어있기 때문에 발생하는 문제.
➡️별도의 DTO를 API파라미터로 받는 것이 권장
➡️API파라미터에 엔티티를 작성하지 말아라
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/api/v1/members")
public CreateMemberResponse saveMemberV1(@RequestBody @Validated 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;
}
}
}
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Validated CreateMemberRequest request){
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberResponse{
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
}
⭐별도의 DTO를 사용하면 좋은점
1. 엔티티 스펙이 바뀌더라도 API 스펙은 변경하지 않아도 된다.
엔티티 스펙으로 변환해주는 메서드들이 setter이므로 API 스펙은 영향을 받지 않는다.
2. 파라미터 값을 명확히 알 수 있다.
필요로 하는 파라미터 값을 명확히 이해할 수 있고 어떤 API 스펙에서 필요로 하는 건지 알 수 있다. 유지보수에 큰 장점이 있다.
3. 엔티티를 외부에 노출하지 않을 수 있다.
외부에 내부 로직을 노출하는 위험이 있을 수 있는데, DTO를 사용하게 되면 매우 안전하다.
4. 엔티티와 API스펙을 명확히 분리할 수 있다.
🌟따라서 요청이 들어오는 것과 나가는 Request, Response는 모두 DTO를 사용🌟
api controller
@PutMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateMemberRequest request){
memberService.update(id, request.getName());
}
service
@Transactional
public void update(Long id, String name){
Member member = findOne(id);
member.setName(name);
}
void
이거나 id값 정도만 리턴하도록 권장 @PutMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateMemberRequest request){
memberService.update(id, request.getName());
Member member = memberService.findOne(id);
return new UpdateMemberResponse(member.getId(), member.getName());
}
@Data
static class UpdateMemberRequest{
@NotEmpty
private String name;
}
@Data @AllArgsConstructor
static class UpdateMemberResponse{
private Long id;
private String name;
}
@Transactional
public void update(Long id, String name){
Member member = findOne(id);
member.setName(name);
}
@GetMapping("/api/v1/members")
public List<Member> getMemberV1(){
return memberService.findMembers();
}
🫸문제점
➡️응답 API 스펙에 맞게 별도의 DTO를 반환한다.
@GetMapping("/api/v2/members")
public Result getMemberV2(){
List<Member> members = memberService.findMembers();
List<MemberDTO> collect = members.stream().map(
member -> new MemberDTO(member.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;
}
이런 방법을 사용한다면 map을 돌면서 member ▶️ MemberDTO 클래스로 감싸게 되고 최종적으로 리턴값으로 MemberDTO list ▶️ Result 클래스로 한번 더 감싸게 된다. 향후 필요한 필드를 추가할 수 있는 이점이 있다.**
장애의 90%는 대부분 조회에서 난다. 조회쪽을 어떻게 설계하면 좋을까를 소개하고자 한다.
/*
* 총 주문 2개
* userA:
* JPA1 BOOK
* JPA2 BOOK
*
* userB:
* SPRING1 Book
* SPRING2 Book
*
* */
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
@PostConstruct
public void init(){
initService.dbInit1();
initService.dbInit2();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService {
private final EntityManager em;
public void dbInit1() {
Member member = createMember("userA", "서울", "1", "1111");
em.persist(member);
Book book1 = createBook("JPA1 BOOK", 10000, 100);
em.persist(book1);
Book book2 = createBook("JPA2 BOOK", 20000, 100);
em.persist(book2);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
Order order = Order.createOrder(member, createDelivery(member),
orderItem1, orderItem2);
em.persist(order);
}
public void dbInit2() {
Member member = createMember("userB", "진주", "2", "2222");
em.persist(member);
Book book1 = createBook("SPRING1 BOOK", 20000, 200);
em.persist(book1);
Book book2 = createBook("SPRING2 BOOK", 40000, 300);
em.persist(book2);
Delivery delivery = createDelivery(member);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);
Order order = Order.createOrder(member, delivery, orderItem1,
orderItem2);
em.persist(order);
}
private Member createMember(String name, String city, String street,
String zipcode) {
Member member = new Member();
member.setName(name);
member.setAddress(new Address(city, street, zipcode));
return member;
}
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
return book;
}
private Delivery createDelivery(Member member) {
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
return delivery;
}
}
}