1-3 API 개발 기본 - 회원 조회 API

shin·2024년 2월 18일
post-thumbnail

회원 조회 API 개발

  • 회원조회 V1 : 응답 값으로 엔티티를 직접 외부에 노출
  • 회원조회 V2 : 응답 값으로 엔티티가 아닌 별도의 DTO 사용
  • 결론 미리보기 : 회원조회 API도 회원등록/수정 API와 마찬가지로 엔티티가 아닌 별도의 DTO를 사용하는 것이 좋음

application.yml

  • 데이터 등록이나 수정없이 조회기능만 수행할 것이기 때문에 ddl-autocreate에서 none으로 변경
  • ddl table drop하지 않고 한 번 데이터를 넣어 두면 계속 반복해서 그 데이터베이스에 있는 데이터를 그대로 쓸 수 있도록 세팅
spring:
  jpa:
    hibernate:
      ddl-auto: none
...(생략)...

  • 조회용 테스트 회원 데이터 등록

[ 회원조회 API v1 ]

응답 값으로 엔티티를 직접 외부에 노출

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

    /**
     * 조회 V1 :  응답 값으로 엔티티를 직접 외부에 노출
     * @return List<Member>
     */
    @GetMapping("/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }
  • 위 코드로 작성된 회원조회 API는 정말 좋지 않은 버전
  • API 실행 결과
@Entity
@Getter @Setter
public class Member {

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

    private String name;

    @Embedded
    private Address address;

    @JsonIgnore
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

}
  • @JsonIgnore

    • 데이터를 주고 받을 때 해당 어노테이션이 붙은 데이터는 Ignore 되어서 Response 값이 보이지 않게 됨
  • API는 하나만 존재하는 것이 아니기 때문에 화면에 종속적으로 작성하면 안됨

  • 모든 엔티티가 노출되고 있음

    • 실무에서는 member 엔티티의 데이터가 필요한 API가 계속 증가하게 됨
    • 어떤 API는 name 필드가 필요하지만, 어떤 API는 name 필드가 필요하지 않을 수 있음
    • 결론적으로 엔티티 대신에 API 스펙에 맞는 별도의 DTO를 노출해야함

문제점

  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가됨

  • 응답 스펙을 맞추기 위해 로직이 추가됨 (@JsonIgnore, 별도의 뷰 로직 등)

  • 실무에서는 같은 에닡티에 대해 API가 용도에 따라 다양하게 만들어짐

    • 따라서 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어려움
  • 엔티티가 변경되면 API 스펙이 변함

결론

  • API 응답 스펙에 맞추어 별도의 DTO를 반환하는 것이 좋음

+) 추가 문제점

[
    {
        "id": 1,
        "name": "member1",
        "address": {
            "city": "seoul",
            "street": "street1",
            "zipcode": "101010"
        },
        "orders": []
    },
    {
        "id": 2,
        "name": "member2",
        "address": {
            "city": "paris",
            "street": "street1",
            "zipcode": "202020"
        },
        "orders": []
    },
    ...
]
  • 기존에 json 형태의 response는 array를 그대로 바로 반환하는 형태
  • 컬렉션을 직접 반환하면 향후 API 스펙을 변경하기 어려워짐
  • 스펙이 굳어버려서 확장을 할 수 없게 되면서 유연성이 확 떨어짐
[
    count : 1
    data : [
      {
          "id": 1,
          "name": "member1",
          "address": {
              "city": "seoul",
              "street": "street1",
              "zipcode": "101010"
          },
          "orders": []
      },
      {
          "id": 2,
          "name": "member2",
          "address": {
              "city": "paris",
              "street": "street1",
              "zipcode": "202020"
          },
          "orders": []
      },
      ...
	]
]
  • 위 응답과 같이 표현이 되도록 하는 것이 좋음
  • 별도의 Result 클래스 생성으로 해결(V2)
    • 카운트라는 필드를 추가하기 용이해짐


[ 회원조회 API V2 ]

  • 응답 값으로 엔티티가 아닌 별도의 DTO 사용
  • Result 클래스로 collection을 감싸서 반환
@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

...

    /**
     * 조회 V2 : 응답 값으로 엔티티가 아닌 별도의 DTO 사용
     * @return Result
     */
    @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);

    }
  • 멤버 엔티티에서 이름을 꺼내서 DTO로 넣고(map으로 바꿔치기) collect해서 toList로 바꾸기

  • 추후 MemberDto를 수정해서 필드를 추가할 수 있음

  • Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있음

    • list를 collection로 바로 내보내면, 바로 json 배열 타입으로 나가버리기 때문에 유연성이 확 떨어짐
  • MemberDto가 object이고, 이것을 Result 클래스를 이용해서 data로 한 번 감싸준 형태로 출력됨

장점

  • 엔티티를 DTO로 변환해서 반환함
  • 엔티티가 변해도 API 스펙이 변경되지 않음
  • API 스펙이 DTO와 1:1 관계가 됨
  • 필요한 필드만 노출하는 것이 변경 위험도를 줄이고 유지 보수하기 좋음

+) 추가 문제점 해결

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;
    
    ...
    @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;
    }
  • Result 클래스로 collection을 감싸서 반환하는 방식을 사용함으로써 원하는 필드를 쉽게 추가할 수 있게 됨
  • count 필드를 추가하여 출력하기가 용이해짐



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

profile
Backend development

0개의 댓글