티스토리 뷰
뷰템플릿을 사용하여 통신할 수도 있지만, 프론트 또는 앱과 API 통신을 통해 데이터를 주고받기도 한다. 이때 API 스펙을 작성하여 요청 정보와 응답 정보를 작성하게 된다. API마다 요청과 응답 정보가 다르기 때문에 Entity에 직접 접근해서는 안 된다. 그 이유를 알아보자.
다음은 회원에 대한 Entity이다. 고유 번호, 이름, 주소, 주문 정보 필드를 가지고 있다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
다음은 회원을 생성하는 API스펙이다.
- 요청 스펙 : 회원 이름
- 응답 스펙 : 생성된 회원
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@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;
}
}
}
saveMemberV1() 메서드의 매개변수로 Memer Entity가 넘어왔다. Member Entity에는 고유 번호, 이름, 주소, 주문 정보 필드가 담겨있었다. 근데 API 요청 스펙에는 회원 이름 이외에는 필요가 없다. Entity에 직접 접근하여 필요 없는 필드정보까지 전달받는 것의 문제점은 다음과 같다.
- Entity에 검증 로직을 담아야 한다
- API마다 필드를 검증하는 데이터가 다르다. 2개 이상의 API의 검증 로직을 모두 Entity에 적용하는 것은 불가능하다.
- API마다 요구사항이 다르다.
- Entity에 대한 API가 1개가 아니기 때문에 모든 요구사항을 담을 수가 없다.
2개의 응답 API가 있다고 가정하자. 1번 API는 name 필드가 반드시 존재해야하고, 2번 API는 주문 정보를 포함하지 않은 회원 정보 반환해야 한다. 1번 API에 맞춰 Entity의 name 필드에 @NotEmpty 애너테이션을 사용하였고, 2번 API에 맞춰 orders 필드에 @JsonIgnore 애너테이션을 사용하였다.
API의 요구사항을 맞추겠다고 Entity를 직접 접근하는게 과연 옳을까? 여기서 API가 더 많아지면 Entity의 필드를 계속 수정하는 것이 맞을까? 이것은 옳은 방법이 아니다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@NotEmpty // 1번 API 요구사항
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
@JsonIgnore // 2번 API 요구사항
private List<Order> orders = new ArrayList<>();
}
- Entity 필드가 변경될 수 있다.
- Entity 필드가 변경되면 API 스펙이 변하게 되므로 문제가 발생한다.
Entity의 name 필드가 실수로 인해 name2로 변경되었다. 다른 개발자는 이 사실을 모르고 name 필드로 API를 호출하게 된다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name2; // 필드이름 변경
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
그러면 아래와 같이 당연히 name 필드를 찾을 수 없으므로 문제가 발생한다.
이러한 문제를 해결하기 위해 API마다 DTO(Data Transfer Object)를 생성해야 한다. 해당 API이 요구하는 스펙에 맞춰 DTO를 생성하여 활용해야 한다. 이때 요청과 응답 둘 다 DTO를 만들어야 한다.
요청 스펙에는 name 필드만 필요하므로 CreateMemberRequest에 name 필드만 사용하도록 dto를 생성하고,
응답 스펙에는 id 필드만 필요하기 때문에 id 필드만 담은 dto를 생성하였다.
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMember(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
@Data
static class CreateMemberRequest {
private String name;
}
}