1-1. API 개발 기본 - 회원 등록 API

shin·2024년 1월 29일
  • API와 일반 화면은 공통 처리하는 부분이 많이 다름
    • 일반 화면은 에러 발생시 공통 에러 HTML이 나와야 함
    • 반면에 API는 공통 에러용 JSON API 스펙이 나와야 함
    • 따라서 API 패키지를 따로 분리하여 작성
  • @RestController
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 * @since 4.0.1
	 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}
  • REST API 스타일로 생성
  • @ResponseBody : 데이터 자체를 바로 json이나 xml로 보낼때 사용하는 annotation

[ 회원 API 버전 1 ]

  • 요청 값으로 Member 엔티티를 직접 받음
package jpabook.jpashop.api;

import jakarta.validation.Valid;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

    /**
     * 등록 V1
     * @param member
     * @return
     */
    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid 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;
        }
        
    }


}
  • Post 수행 시, 회원 id 생성
@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @NotEmpty
    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
    
}
  • Member entity의 name에 @NotEmpty를 설정하면 비어있는 값을 전달할 경우 아래와 같이 validation이 자동으로 수행됨

문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가됨
  • 엔티티에 API 검증을 위한 로직이 들어감 (@NotEmpty 등등)
  • 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기는 어려움
  • 엔티티가 변경되면 API 스펙이 변함
  • 이로 인해 큰 장애가 발생하게 될 수 있음

결론

  • 엔티티를 파라미터로 받으면 안됨
  • 엔티티를 외부에 노출해서도 안됨
  • API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받아야 함

[ 회원 API 버전 2 ]

  • 요청 값으로 Member 엔티티 대신에 별도의 DTO를 받음
public class MemberApiController {

    private final MemberService memberService;

    @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;

    }

    @Data
    static class CreateMemberResponse {

        private Long id;

        public CreateMemberResponse(Long id){
            this.id = id;
        }

    }

}
  • CreateMemberRequestMember 엔티티 대신에 RequestBody와 매핑함

  • 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있음

  • 엔티티와 API 스펙을 명확하게 분리할 수 있음

  • 엔티티가 변해도 API 스펙은 변하지 않음

  • 만약 name에서 userName으로 바뀌어도 컴파일 시점에서 오류가 발생하여 확인이 가능함

  • 실무에서는 엔티티를 API 스펙에 노출하면 안됨

  • API 스펙 문서를 까보지 않아도 DTO를 받으면 API 스펙이 어떻게 되어있는지 알 수 있음

    • DTO에 @NotEmpty를 걸어주면 검증 유무 확인도 용이함
  • 테스트 완료

강의 출처 : 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

profile
Backend development

0개의 댓글