본문 바로가기

스프링 (인프런)/스프링부트

API 개발 기본

1. 회원 등록 API

Postman 설치

- https://www.postman.com

 

Postman API Platform | Sign Up for Free

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster.

www.postman.com

- 여기서 postman을 다운받아야 한다.

 

 

 

MemberApiController - v1

@RestController
@RequiredArgsConstructor
public class MemberApiController {
    private final MemberService memberService;

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {   // @RequestBody : json으로 온 body를 Member로 매핑해줌
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberResponse {
        private Long id;

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

- Member에 대한 API 컨트롤러다.

- CreateMemberResponse는 자바의 내부 클래스다.

 

- saveMemberV1()은 "/api/v1/members"에 대한 http의 POST 요청을 처리하는 메서드다.

    - json으로 온 body를 saveMemberV1은 @Requesbody를 통해 Member 객체로 변환한다.

    - @Valid는 해당 객체에 붙은 제약 조건을 알아서 검사한다.

        - 예를 들어, Member 클래스의 name 필드에 @NotEmpty를 걸고 이름 없이 등록하면 알아서 막아준다.

    - 메서드 내에서는, 파라미터로 들어온 member를 join()하고 CreateMemberResponse 객체를 생성해서 반환한다.

 

- CreateMemberResponse 내부 클래스는 회원 생성 API의 응답 데이터를 담는 객체로서 기능한다.

    - 그래서 saveMemberV1()의 결과, POST를 친 클라이언트에게 회원의 식별자를 돌려주게 된다.

 

 

 

Postman에서 json의 body 전송

- Headers를 선택해서 다음과 같이 채우고, Body의 raw에서 다음과 같이 입력해서 해당 주소로 POST 명령어를 Send 해봤다.

    - ( 나도 지금은 자세히 모르겠어서 강의대로 따라하기만 했다 ㅠㅠ )

 

- 어쨌거나 hello!라는 name을 가진 회원 객체가 DB에 등록되었고, 결과로는 식별 id를 돌려받았다.

 

- 그리고 인텔리제이에서 스프링을 돌려 놨었는데, 여기서도 회원 등록 로그가 뜬다.

 

 

 

V1 엔티티를 Request Body에 직접 매핑

문제점 : 엔티티와 API가 서로 독립적이지 않다.

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

- 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty )

- 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기는 어렵다.

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

 

 

 

결론

- API 요청 스펙에 맞춰 별도의 DTO를 파라미터로 받는다.

 

 

 

MemberApiController - 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 {
    @NotEmpty
    private String name;
}

- 시그니쳐가 비슷해 보이지만 Member 엔티티 대신 CreateMemberRequest라는 내부 클래스(DTO)를 대신 쓰고 있다.

- CreateMemberRequest는 클라이언트로부터 이름과 함께 등록 요청을 받는다.

- 그러면 saveMemberv2()에서는 멤버 객체를 직접 생성해서 이름만 외부에서 받고, join()해서 Response를 돌려준다.

 

- v1과 달리,

    - CreateMemberRequest 클래스를 보면 API 레벨에서 필요한 것이 뭔지 알 수 있다.

    - name에 @NotEmpty와 같은 제약 조건을 API 스펙에 맞춰 걸어줄 수 있다.

    - Member 엔티티가 변경되더라도 API 스펙에 영향이 가지 않는다.

 

 

 

2. 회원 수정 API

- 명령어를 PUT으로 가져간다.

- PUT은 동일한 연산(이름을 "hello"로 변경)을 여러 번 요청해도 동일할 때 사용한다.

 

 

 

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

- 앞서 언급했던 것처럼 PUT을 받기 위해 @PutMapping을 걸고, id에 맞춰서 url을 받고 있다.

- updateMemberV3()는 조회 쿼리에 사용하기 위해 @PathVariable을 사용한다.

- 그리고 회원 생성 API에서와 동일하게

    - 클라이언트에게 요청에 대한 반환값을 보내기 위해 UpdateMemberResponse를 정의했다.

    - DTO 방식을 쓰기 위해 UpdateMemberRequest를 정의했다.

 

 

 

@Transactional
public void update(Long id, String name) {
    Member member = memberRepository.findOne(id);
    member.setName(name);
}

- MemberService에 update()를 호출해서 변경 기능을 위임하고 있는데, 아직 만들지 않아서 새로 정의했다.

- 트랜잭션 안에서 식별자 값으로 회원 객체를 들여오기 때문에 JPA가 관리하고, 따라서 변경 감지 기능이 작동한다.

 

 

 

- 바뀐 멤버 객체는 @Transactional을 통해 flush()되었을 것이고, 이 변경 내용을 반환하기 위해 findOne()을 호출한다.

- 참고로, 서비스 계층의 update()를 통해 멤버 객체를 넘겨줘도 되는데, 굳이 컨트롤러에서 다시 findOne()을 통해 가져온다.

    - 이는 "커맨드와 쿼리를 분리하라"는 명제 때문에 update()는 상태만 변경하게 되었다.

 

 

 

참고!

- CQS(command-query separation)은 무슨 뜻일까?

- 커맨드는 상태를 변경하는 메서드를 뜻하고, 쿼리는 값을 조회해서 반환하는 메서드를 뜻한다.

- 그래서 위 예시를 들었을 때, update()는 커맨드에 findOne()은 쿼리에 속한다.

 

 

 

3. 회원 조회 API

ddl-auto: none

- 우선 스프링을 시작할 때마다 데이터가 초기화되는 것을 방지하기 위해 applicatoin.yml에서 ddl-auto를 none으로 세팅한다.

 

 

 

membersV1()

@GetMapping("/api/v1/members")
public List<Member> membersV1() {
    return memberService.findMembers();
}

- http 명령어 중 GET에 대해 제공하는 메서드다.

- 간단하게 서비스 계층의 findMembers()를 호출하여, 회원 리스트를 반환한다.

 

- postman에서 받은 결과 정보다.

- 문제는, 우리가 회원의 주문 정보까지는 필요로 하지 않는다는 점이다.

- 그런데 findMembers()는 엔티티 자체를 반환하기 때문에 외부에 노출되는 것이 문제다.

 

 

 

@JsonIgnore
@OneToMany( mappedBy = "member" )               // 일대다 관계. 연관관계 주인을 넘겨주기 위해 'mappedBy' 활용
private List<Order> orders = new ArrayList<>();

- 그래서 다음과 같이 엔티티 클래스의 일부 필드만 @JsonIgnore를 붙여서 Json API에 이 필드가 반환되지 않도록 막을 수 있다.

- 그렇지만 다른 API와 호환되지 않는다는 단점이 있다.

    - 한 엔티티에 다양한 API 각각에 대한 프레젠테이션 응답 로직을 담는 것이 매우 어렵다.

- 그리고 엔티티 클래스에 프레젠테이션 계층에 대한 로직이 들어간다는 단점이 있다.

- 그리고 엔티티가 변경되면 API 스펙도 영향을 받아서 수정해야 한다.

 

 

 

membersV2()

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

- V1에서는 엔티티를 반환했지만 V2에서는 DTO를 반환하고 있다.

    - MemberDTO 클래스의 필드는 name 문자열뿐이기 때문에, 실제 Member 객체와는 거의 독립적인 수준이다.

 

- 물론 서비스 계층의 findMembers()를 호출한다는 점은 동일하다.

- 그러나 이를 문자열 필드만 가진 MemberDTO로 회원의 name만 걸러서 옮겨 담는다.

- 그리고 지네릭을 가진 Result 클래스로 한 번 더 감싸서 반환한다.

    - 만약 향후 일부 필드를 더 담게 된다면, Result가 지네릭 타입을 가지므로 MemberDTO에 필드만 추가하면 된다.

 

@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.size(), collect);
}

@Data
@AllArgsConstructor
static class Result<T> {
    private int count;
    private T data;
}

- 회원 엔티티를 곧바로 반환하는 V1에서는 불가능했던, 회원 수에 대한 count 등을 쉽게 추가할 수 있다.

 

- 결과 화면이다.

'스프링 (인프런) > 스프링부트' 카테고리의 다른 글

API 개발 고급 - 지연 로딩과 조회 성능 최적화  (0) 2023.07.14
API 개발 고급 - 준비  (0) 2023.07.13
웹 계층 개발  (0) 2023.07.08
주문 도메인 개발  (0) 2023.07.07
상품 도메인 개발  (0) 2023.07.06