본 글은 강의를 들으며 메모해놓은것을 정리하기 위한 글입니다.
잘못된 정보가 있다면 댓글로 편하게 정정 부탁드리겠습니다. 🙏
강좌에서는 API 패키지를 만들때 주로 템플릿 엔진으로 랜더링을 하는 컨트롤러와 API 스타일의 컨트롤러를 아예 분리하기를 권장한다. 권장하는 이유는 예외처리를 하게 될때 보통 패키지 단위로 하게되는데, View단과 API단의 공통요소가 다르기 때문이다.
@Controller
와 @Responsebody
를 보통 컨트롤러에 적용하지만 이 둘을 합친 @RestController
를 적용해도 무방하다.
(@RestController : RestAPI 스타일로 만들기 위해 활용할 수 있는 Spring Annotation이다.)
@PostMapping("url")
public CreateMemberResponse saveMember(@RequestBody @Valid Member member) { //Entity를 파라미터로 받고 있다.
//회원등록 로직
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
위와 같은 로직이여도 회원가입 기능은 문제없이 작동한다. 하지만 큰 문제점이 하나 있는데, 바로 Entity와 API가 1:1 맵핑되어 있다는 것이다. 이는 만약 Entity의 상태값의 변수명등을 변경하게 된다면 API spec도 함께 변경이 되어비리기 때문에 Side Effect와 같은 큰 장애를 발생시킬 가능성이 있다.
Side Effect의 가능성을 없애버리려면 어떻게 바꿔야 할까?
@PostMapping("url")
public CreateMemberResponse saveMember(@RequestBody @Valid CreateMemberRequest request) {
//회원등록 로직
}
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
}
기존에 파라미터로 Entity를 받던 형식을 Request DTO로 받는 형식으로 변경하여 Entity와 API spec과의 관계를 끊어냈다.
(Entity - API 👉 Entity - DTO - API)
만약 이런 상황에서 요청받는 데이터중 name
이 null
값이 아니여야 한다면, Entity가 아닌 Request DTO에서 해당 항목값에 NotEmpty
를 추가해주면 된다.
(@NotEmpty : Schema Table에서 NOT NULL 처리하는것과 유사한 기능의 Annotation)
회원정보를 수정하는 API 기능을 만들때 HTTP PUT요청에 적합한 @PutMapping으로 작성한다. 그리고 PUT
으로 호출하게 되면 데이터를 수정하여 결과를 대체하므로 계속 수정 요청을 하면 최종 결과는 계속 대체되므로 동일하다. 이를 멱등하다
라고 표현하기도 하고, 비슷한 맥락으로 GET
과 DELETE
또한 멱등하다
라고 표현할 수 있다.
@PutMapping("url/{id}")
public UpdateMemberResponse updateMember(@PathVariable("id) Long id, @RequestBody @Valid UpdateMemberRequest request) {
//회원수정 로직
}
@Data
static class UpdateMemberRequest {
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse {
private Long id;
private String name;
}
회원등록때 썼던 DTO는 등록API와 수정API의 spec이 다르므로 동일하게 활용하면 안된다. 그러므로, 회원수정에 맞는 DTO(UpdateMemberRequest, UpdateMemberResponse)를 만들어야 한다.
아울러, 수정API의 service단에서는 가급적이면 변경감지
를 쓰는것을 권장한다. 아래와 같이 말이다.
public class MemberService {
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id); //id를 DB에서 찾아오면서 1차 캐시에 member가 올라간다.
member.setName(name); //Entity와 스냅샷의 변경점이 생긴것을 감지한다.
}
}
위 코드에서 사실 Member
로 반환해도 로직에는 이상없지만 커맨드와 쿼리를 철저하게 분리하는것이 유지보수성 증대면에서 탁월하다.
(이부분은 아직 100% 이해가는 부분이 아니라, 참고만 부탁드립니다.)
회원정보 조회할때 단순하게 아래 코드처럼 구현할 수도 있다.
@GetMapping("url")
public List<Member> members() {
return memberService.findMembers();
}
하지만 이렇게 구현을 해놓고 요청을 하면, Member Entity가 반환되면서 개인정보와 같이 보안이 중요한 정보들도 같이 반환하게 되버린다. 물론 @Jsonignore를 Entity에서 숨기고자 하는 상태값에 적용하여 조회데이터를 수정하여 반환할수도 있지만, 이렇게 되면 Entity에 화면기능이 추가된것이므로 순수하지가 않게 된다.
필요한 내용만 반환해서 조회하게 하려면 어떻게 바꿔야할까?
@GetMapping("url")
public Result members() {
//회원조회 로직
return new Result(collet);
}
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
바로 Member List를 반환하지 않고, MemberDto로 변환하여 조회에 필요한 data만 담아서 훨씬 안전하게 반환할 수 있다.(유지보수 측면에서도 훨씬 낫다.)