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

Jimin·2023년 5월 14일
0

멋쟁이사자처럼

목록 보기
5/10
post-thumbnail

JPA 1 내용

회원 Entity

@Entity
@Getter @Setter
public class Member {

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

    @NotEmpty
    private String name;

    @Embedded
    private Address address;

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

}

@Entity

@Entity 애노테이션은 데이터베이스의 테이블과 일대일로 매칭되는 객체 단위이며, Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미하게 된다.

객체의 인스턴스를 구분하기 위한 유일한 키값을 가지는데 이것은 테이블 상의 Primary Key 와 같은 의미를 가지며 @Id 애노테이션으로 표기된다.

@Table

@Entity
@Table(name = "MEMBER")
public class Member {
	...
}

@GeneratedValue

Java 의 Long Type 사용 → DB의 BigInteger Type

@Embedded

@Embedded
private Address address;
@Embeddable
@Getter
public class Address {

    private String city;
    private String street;
    private String zipcode;

    protected Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

@NotEmpty

vaild 에 사용

@JsonIgnore

일대다에 사용

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

@JsonManagedReference & @JsonBackReference

순환참조를 방어하기 위한 Annotaion.

  • 부모 클래스 → @JsonManagedReference
  • 자식 클래스 → @JsonBackReference

DTO 사용하기

참고

JPA 기본 Annotation 정리

JPA 순환참조와 해결방법


회원 등록 API

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

MemberApiController

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

    /**
     * 등록 V1: 요청 값으로 Member 엔티티를 직접 받는다.
     * 문제점
     * - 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
     * - 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
     * - 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를
     위한 모든 요청 요구사항을 담기는 어렵다.
     * - 엔티티가 변경되면 API 스펙이 변한다.
     * 결론
     * - API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다.
     * */

    @PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberRequest {
        @NotEmpty
        private String name;
    }

    @Data
    static class CreateMemberResponse {
        private Long id;

        public CreateMemberResponse(Long id) {
            this.id = id;
        }
    }
  • 문제점
    • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
    • 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
    • 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 모든 요청 요구사항을 담기는 어렵다.
    • 엔티티가 변경되면 API 스펙이 변한다.
  • 결론
    • API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다.

V2 엔티티 대신에 DTO를 RequestBody에 매핑

MemberApiController

    /**
     * 등록 V2: 요청 값으로 Member 엔티티 대신에 별도의 DTO를 받는다.
     */
    @PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
        Member member = new Member();
        member.setName(request.name);
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }
  • CreateMemberRequest 를 Member 엔티티 대신에 RequestBody와 매핑한다.
  • 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
  • 엔티티와 API 스펙을 명확하게 분리할 수 있다.
  • 엔티티가 변해도 API 스펙이 변하지 않는다.




회원 수정 API

회원 수정도 DTO를 요청 파라미터에 매핑
MemberApiController

    /**
     * 수정 API
     */
    @PatchMapping("/api/v2/members/{id}")
    public UpdateMemberResponse updateMemberV2(
            @PathVariable Long id,
            @RequestBody @Valid UpdateMemberRequest request) {
        memberService.update(id, request.getName()); // update
        Member findMember = memberService.findOne(id); // select 구분
        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;
    }

MemberService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

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


회원 조회 API

회원조회 V1: 응답 값으로 엔티티를 직접 외부에 노출

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;
    
    
    /**
     * 조회 V1: 응답 값으로 엔티티를 직접 외부에 노출한다.
     * 문제점
     * - 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
     * - 기본적으로 엔티티의 모든 값이 노출된다.
     * - 응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
     * - 실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의
     API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.
     * - 엔티티가 변경되면 API 스펙이 변한다.
     * - 추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다.(별도의 Result 클래스
     생성으로 해결)
     * 결론
     * - API 응답 스펙에 맞추어 별도의 DTO를 반환한다. */
     
    //조회 V1: 안 좋은 버전, 모든 엔티티가 노출, @JsonIgnore -> 이건 정말 최악, api가 이거 하나인가! 화면에 종속적이지 마라!
    @GetMapping("/api/v1/members")
    public List<Member> membersV1() {
        return memberService.findMembers();
    }
 
 }

조회 V1: 응답 값으로 엔티티를 직접 외부에 노출한다.

문제점

  1. 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
  2. 기본적으로 엔티티의 모든 값이 노출된다.
  3. 응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore , 별도의 뷰 로직 등등)
  4. 실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.
  5. 엔티티가 변경되면 API 스펙이 변한다.
  6. 추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다.(별도의 Result 클래스 생성으로 해결)

결론

API 응답 스펙에 맞추어 별도의 DTO 를 반환한다.

회원조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO 사용

	/**
     * 조회 V2: 응답 값으로 엔티티가 아닌 별도의 DTO를 반환한다.
     * */
    @GetMapping("/api/v2/members")
    public Result membersV2() {
        List<Member> findMembers = memberService.findMembers();
        //엔티티 -> DTO 변환
        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; // Generic 사용

    }

    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String name;
    }
  • 엔티티를 DTO로 변환해서 반환한다.
  • 엔티티가 변해도 API 스펙이 변경되지 않는다.
  • 추가로 Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.

profile
https://github.com/Dingadung

0개의 댓글