본문 바로가기
Spring/JPA 2

1. API 개발 기본

by wch_t 2023. 11. 16.

1. 진행하면서 맞닥뜨린 문제 (@ResponseBody), 검증

     1) Post 요청으로 클라이언트의 데이터를 CreateMemberRequest(DTO) 타입의 request 객체에 매핑을 해준다.

     2) 이 때, HTTP 요청의 본문(Member)과 request가 매핑이 되는데, 이 때 반드시 필드의 이름이 일치해야 한다.

public class Member {
    private String username;
}
@RestController
@RequiredArgsConstructor
public class MemberApiController {
    @PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
        //System.out.println("name = 1" + request.getUsername());

        Member member = new Member();
        member.setUsername(request.getUsername());

        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }
    
    @Data
    static class CreateMemberRequest {
        private String username;
    }

}

 

plus. 어노테이션 정리

- @RestController = @ResponseBody + @Controller

     해당 컨트롤러의 모든 메서드에서 @ResponseBody를 추가하지 않아도 된다.
     즉, 모든 메서드의 반환 값이 HTTP 응답으로 직접 전송된다.

 

- @ResponseBody

     해당 메서드의 반환 값을 HTTP 응답 본문으로 직접 전송하는 어노테이션이다.
     이 때 View Resolver를 거치지 않고 직접 클라이언트로 전송된다.

 

- @RequestBody

     HTTP 요청 본문(body)에 있는 데이터를 자바 객체로 변환하는 역할이다.
     주로 POST, PUT과 같은 HTTP 요청에서 클라이언트가 전송한 데이터를 받아오기 위해 사용된다.

 

 

2. API 스타일의 컨트롤러

지금까지 JPA 활용1에서는 템플릿 엔진(View(Form), Model..)을 사용해서 렌더링 하는 컨트롤러를 작성했다.

일단 API 스타일은 '클라이언트 사이드 렌더링(CSR)'로 서버는 데이터를 제공하고, 클라이언트(프론트엔드)가 이 데이터를 기반으로 사용자 인터페이스를 렌더링한다.

사용하는 주된 이유는 서버와 프론트엔드 간의 독립성, 다양한 플랫폼 지원 등으로
모바일 / 싱글 페이지 / 웹 애플리케이션에서 사용되며, 서버와 클라이언트 간의 효율적인 통신 및 데이터 교환을 가능하게 한다.

 

[기존 템플린 엔진 컨트롤러, MemberController]

@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @GetMapping("members/new")
    public String createForm(Model model) {
        model.addAttribute("memberForm", new MemberForm());
        return "members/createMembersForm";
    }

    @PostMapping("members/new")
    public String create(@Valid MemberForm memberForm, BindingResult result) {

        if (result.hasErrors())
            return "members/createMembersForm";

        Address address = new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode());

        Member member = new Member();
        member.setUsername(memberForm.getName());
        member.setAddress(address);

        memberService.join(member);
        return "redirect:/";
    }

    @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

 

 

3. API를 만들 때, request / response 를 엔티티로 받지 않고 별도의 DTO로 받아야 하는 이유

문제점

     1) 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.

          - 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty, @JsonIgnore...)
          - 실무에서는 1개의 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기 어렵다

     2) 엔티티가 변경되면 API 스펙이 변한다.

          - 당장 생각해보아도, 위와 같은 문제가 발생한다. (username → name)

 

     3) 엔티티에 있는 정보들이 모두 외부에 노출하게 된다.

          - 해당 API에서 필요없는 정보들까지도 노출된다. (1-1. 문제점과 연관)

 

     Specific case) '회원 전체 조회'와 같은 로직에서 결과값을 배열로 반환하게 된다.

          (1) 위 경우는 List<Member>로 엔티티 자료구조를 반환할 때, json 결과값이 배열로 맞게 된다.

          (2) 배열로 받을 경우, 관련 필드를 더 이상 추가할 수 없어 확장성과 유연성이 떨어진다.

          (3) 따라서 'Generic'을 사용하여 매핑한 후 반환하도록 하자 ( '{  }' 반환 )

 

     cf. 실무에서는 엔티티를 API 스펙에 노출하면 안된다.

    // 이 부분을 말하시는 걸까?
    
    @GetMapping("/api/v1/members")
    public List<Member> membersV1(){
        return memberService.findMembers();
    }

 

 

해결책

     - 따라서 각 API 요청 스펙에 맞추어 별도의 DTO를 사용해야 한다.

 

 

[강의 핵심]

API를 만들 때는 파라미터를 받든 반환하든 절대 엔티티를 사용해서는 안된다.
꼭 API 스펙에 맞는 DTO를 만들어서 활용해아 한다.