@Valid 어노테이션과 jakarta 제약 조건을 이용한 입력 값 예외 처리
@Entity
@Getter
@Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
// 제약 조건
@NotEmpty
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@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;
}
}
}
- @Valid 어노테이션을 이용해서 @NotEmpty 등의 제약조건을 스프링 부트에서 자동으로 검증해서 예외를 내줄 수 있다.
- 하지만 이렇게 하면, 예외 처리가 Entity에 집약되어 있다. 즉, 엔티티의 속성 변수명이 바뀌면 API 스펙이 바뀌는 문제가 발생한다.
- 즉, 엔티티 수정 -> API 스펙 변경으로 이어지는(엔티티와 API 스펙이 1:1로 딱 매핑) 큰 문제가 있다.
- 즉 이전에도 말했듯, 엔티티 자체를 주고 받는 것을 지양하고 API 요청 스펙에 해당하는 별도의 DTO를 만들어서 그것을 이용해서 데이터를 주고 받아야 한다. -> 엔티티를 외부에 노출하지 말고 파라미터로 주고받지 말라.
- @NotEmpty 등의 제약 조건도 API 요청 스펙에 맞게, DTO에 그렇게 설정해주면 된다.
- 이렇게 하면, 데이터는 API 요청 스펙에 맞게 만들어진 DTO를 통해 주고 받아지기 때문에 엔티티를 수정하더라도 API 스펙은 변경되지 않는다.
- 결론적으로, DTO를 까보면 API 요청 스펙이 뭔지, 이 속성 값은 어디에서 채워지는 건지 등 DTO를 보고 알 수 있다.
- 그래서 DTO 수가 많아질 수 있지만, 안정성과 유지보수를 위해 이렇게 해야 한다.
DTO를 통해 데이터를 주고 받는 형태로 수정
@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);
}
@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);
}
// DTO (임시)
@Data
static class CreateMemberRequest {
private String name;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
}
커맨드와 쿼리를 분리해서 개발
@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());
}
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
회원 조회 DTO
@RestController
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@GetMapping("/api/v2/members")
public Result memberV2() {
List<Member> findMembers = memberService.findMembers();
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect);
}
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
}
- 그대로 엔티티를 반환하여 배열 형태로 반환하지 않고, 한 번 감싸서 전달하기 때문에 확장할 때도 유연하고, 엔티티를 수정하더라도 API 스펙이 변하지 않는다. -> 엔티티를 외부에 노출하지도 않는다