JPA 활용편 2 - API 개발 기본

Stella·2022년 5월 20일
0

Java

목록 보기
14/18

API 개발 기본

controller랑 구조 분리

  • Template 엔진을 사용하는 controller랑 api 사용하는 controller랑 구조 분리.
    공통으로 예외 처리를 할 때, 패키지 단위의 구성으로 공통 처리를 많이 함 -> api랑
  • template이랑 공통 처리할 요소가 다름.
  • template의 경우 예외 발생 시 공통 에러 화면이 나와야함.
  • api는 공통에러용 json 스펙이 나가야함.

@RestController

  • Controller + ResponseBody(data자체를 json이나 xml로 보내자) : RestController

DTO 사용

회원 등록

MemberApiController

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

Member

public class Member {

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

    @NotEmpty
    private String name;

    @Embedded //Address의 Embeddable이나 Member의 Embedded 하나만 있어도 가능.
    private Address address;

    @OneToMany(mappedBy = "member")//order 테이블에 있는 member에 mapping, 읽기전용, 변경 불가능
    private List<Order> orders = new ArrayList<>();
  • 위의 코드는 Parameter에 Member Entity를 직접 받음.
  • 프레젠테이션 계층을 위한 검증 로직이 entity에 들어가 있음.
  • entity에 API 검증을 위한 로직이 있음(@NotEmpty 등등)
    • API 마다 @NotEmpty 가 필요할 수도 있고 없을 수도 있음.
  • 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를
    위한 모든 요청 요구사항을 담기는 어려움.
  • Entity 변경되면 API 스펙이 변함
    • Entity의 name을 username으로 바꾸면 API의 스펙 자체가 변경되어버림!!!
  • Entity의 field 중 어느 값이 parameter로 넘어오는 지 api문서를 봐야 알 수 있음.
  • API를 만들 때 Entity를 Parameter로 받거나 외부에 노출하면 안됨!!

API 스펙을 위한 별도의 DTO(Data Transfer Object) 사용!

MemberApiController

@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 {
    	@NotEmpty
        private Long id;

        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
  • Entity가 바껴도 API 스펙은 바뀌지 않음.
  • Parameter로 뭐가 넘어오는지 확실히 알 수 있음.
  • validation도 API에 맞춰서 설정 가능.
  • 유지보수할 때 편함.
  • Entity를 외부에 노출하지않고 api 스펙에 맞는 별도의 dto 사용하는 게 API 만들 때 정석

회원 수정

MemberService

    @Transactional
    public void update(Long id, String name){
        Member member = memberRepository.findOne(id); // 영속상태의 member
        member.setName(name); // 영속상태의 member의 이름을 setName으로 변경하고, @Transactional에 의해서 @Transactional aop가 끝나는 시점에 commit이 되고, jpa가 영속성 컨테스트 flush하고 db에 commit

    }

MemberApiController

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

회원 조회

Entity를 절대 외부에 직접 반환하지말고 항상 DTO로 바꾸기!!!!

수정 전

MemberApiController

@GetMapping("/api/v1/members")
	// List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐
    public List<Member> membersV1() {
        return memberService.findMembers();
    }

문제점
1. Entity를 직접 노출하면 entity에 있는 정보들이 모두 외부 노출!
2. List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐

위의 문제를 해결하기 위해서 Member Entity에 @JsonIgnore 추가

Member

 @JsonIgnore // JSON으로 반환 할때 이 필드는 제외 -> 이런 경우 다른 API를 만들 때 문제가 됨
    @OneToMany(mappedBy = "member")//order 테이블에 있는 member에 mapping, 읽기전용, 변경 불가능
    private List<Order> orders = new ArrayList<>();

문제점
1. 다른 API를 만들 때 문제가 됨. JsonIgnore를 달아둔 필드가 필요한 api도 있을 것!
2. Entity에 프레젠테이션 계층을 위한 logic이 들어갔음 -> entity로 의존관계가 들어와야하는데, entity에서 의존관계가 나가버림 -> 양방향 의존관계가 걸리면서 애플리케이션 수정이 어려워짐
3. 엔티티가 변경되면 API 스펙이 변함
4. 추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어려움 -> 별도의 Result 클래스 생성으로 해결

응답

[
    {
        "id": 1,
        "name": "new-hello",
        "address": null
    },
    {
        "id": 2,
        "name": "member1",
        "address": {
            "city": "서울",
            "street": "test",
            "zipcode": "11111"
        }
    },
    {
        "id": 3,
        "name": "member2",
        "address": {
            "city": "부산",
            "street": "test2",
            "zipcode": "22222"
        }
    }
]

문제점
List를 collection 타입으로 바로 내면 json 배열 타입으로 나가버림 -> 유연성이 확 떨어짐

결론!!!

API 응답 스펙에 맞추어 별도의 DTO 반환!

수정 후

MemberApiController

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

    @Data
    @AllArgsConstructor
    static class Result<T> {
   		 // 필요한 데이터 확장 용이
        // private int count;
        private T data;
    }
    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String name;
    }

map을 사용해서 Member를 사용해서 MemberDto로 변환
Result < T> 를 사용해서 반환 데이터에 확장성 추가

응답

{
    "data": [
        {
            "name": "member1"
        },
        {
            "name": "member2"
        }
    ]
}

리스트로 바로 반환 하지 않음

profile
Hello!

0개의 댓글