티스토리 뷰

웹 애플리케이션 개발 시 서비스를 위해서 api를 이용한 통신을 하게 되므로 이를 구현해보겠다. 이때, DB 및 repository에서 사용되는 Entity가 아닌 데이터 전달용도로 DTO객체를 따로 생성해서 사용해야 되는데 이에 대해 알아보겠다.

 

DTO : Data Transfer Object

회원 생성 API

회원 생성 api

  • POST ("api/v2/members")
{
    "name": "최똥꼬",
    "email": "dnstlr2999@naver.com7",
    "address": {
        "zipcode" : "13928",
        "streetAdr": "안양시 동안구 99로 999",
        "detailAdr": "999-999"
        }
}

Member 객체

@Entity
@Getter @Setter
public class Member {

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

    private String name;

    private String email;

    @Embedded
    private Address address;

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

 

DTO를 사용하지 않고 Entity 그대로 api를 통해 구현하는 경우

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

등록

- 요청 값으로 Member 엔티티를 직접 받는다.

 

Entity를 바로 받는 경우 문제점

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

    Ex) 널값을 방어하기 위해 @NotNull 등을 달아줘야 하는 등 Entity에 대해 추가 제약이 붙는다

- API마다 필수 요소들이 다를수 있는데도 일률적으로 제한해야 한다.

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

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

    -> 즉 api가 Entity에 의존적이게 된다.

 

결론

- API 요청 양식에 맞추어 별도의 DTO를 생성하고, entity와 분리해서 받아서 처리하자.

 

DTO를 사용하여 api와 Entity를 구분하는 경우

@PostMapping("api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {

    Member member = new Member();
    member.setName(request.getName());
    member.setEmail(request.getEmail());
    member.setAddress(request.getAddress());

    Long id = memberService.join(member);
    return new CreateMemberResponse(id);
}

등록

- 요청 값으로 Member 엔티티 대신에 별도의 DTO를 받는다

 

회원 생성을 위한 DTO

@Data
static class CreateMemberRequest {
    private String name;
    private String email;
    private Address address;
}

회원 이름 수정 API

회원이름 수정 api

- PATCH ("api/v2/members/{id}")

 

회원 이름을 수정하기 위한 DTO를 통해서 api를 받아서 처리

@PatchMapping("api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2 (
			@PathVariable("id") Long id,
			@RequestBody @Valid UpdateMemberRequest request)
{
		memberService.update(id, request.getName());
		Member findMember = memberService.findMember(id);
		return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}

 

회원 이름 변경용 DTO (이름만 받아서 변경)

@Data
static class UpdateMemberRequest {
	private String name;
}

 

회원 이름 변경에서도 마찬가지로 DTO를 따로 생성해서 필요한 이름만 받고 id를 통해 변경할 회원에 대해 이름을 변경하도록 했다.

 


회원 조희 API

회원 목록 조희시 바로 service()에 구현된 조회를 통해 바로 회원목록 반환시 문제점이 발생할 수 있다.

 

회원 목록을 바로 반환시 문제점

  • 기본적으로 Entity의 모든값이 노출되므로 매우 위험하다.
  • 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
    1. 값을 내보내지 않기 위한 @JsonIgnore 등..
  • 하나의 엔티티에 연결된 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 매우 어렵다.
  • 엔티티가 변경되면 API 스펙이 변한다.
    1. 회원의 이름을 name: "이름"으로 가져왔는데, entity의 name이 username으로 변경되어서 username으로 가져오게 되면 전체적인 api구조가 꼬이게 된다.
  • 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다.
    1. 별도의 반환을 위한 Result 클래스 생성으로 해결한다

결론

  • API 응답 양식에 맞추어 별도의 DTO를 생성해서 반환하자

 

회원 조회 용 DTO를 통해서 api 처리

@GetMapping("api/v2/members")
public Result<?> memberV2() {
	List<Member> members = memberService.findAllMembers();
	List<MemberDto> collect = members.stream()
			.map(m -> new MemberDto(m.getName(), m.getEmail()))
			.collect(Collectors.toList());

	return new Result<>(collect);
}

 

회원 리스트를 배열로 바로 보내지 않고 data로 포함해서 보내기 위한 응답 객체

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

 

회원 조회용 DTO (이름과 이메일만 반환)

@Data
@AllArgsConstructor
static class MemberDto {
	private String name;
	private String email;
}

 

이렇게 총 회원 생성, 회원 이름 변경, 회원 목록 조회를 위한 API를 구현해 보았다. 무엇보다 Entity를 바로 api 로직에서 사용하면 안되므로 api와 entity간의 의존관계를 없애기 위해 중간에 DTO 객체를 통해서 구현하도록 했다.

 

또한, 목록을 반환할 때 json 형식을 깨뜨리지 않기 위해 바로 배열로 반환하지 않기 위해 data로 바꿔서 반환하게 하면 추후 api의 확장이 가능한 유연한 구조를 갖게 하는 것이 중요했다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday