[Spring Data JPA Tutorial] 11. Generic을 이용하여 Response 일반화하기

2022. 4. 17. 17:46Spring/Spring Data JPA Tutorial

반응형
  1. Response 타입 개선의 필요성
  2. Response 클래스 정의
    1. MessageUtils
    2. BaseResponse
    3. DataResponse<T>
  3. 변경하기
    1. user 단건 조회
    2. user 수정
    3. user 삭제
    4. user 추가
    5. user 전체 조회
    6. store 단건 조회
    7. store 전체 조회
  4. 변경된 Swagger 화면 및 추후 개선해야할 사항

1. Response 타입 개선의 필요성

이전 시간에 만들었던 api들의 Response 타입은 모두 Map 이었습니다.
Map으로 만드는 것이 기능상으로 문제될 것은 없지만, OpenAPI 웹문서 상에 response 응답값에 대한 도움을 받기 어렵다는 단점이 있습니다.

03-5 03-4



Swagger 웹 페이지로 API 문서를 대체하기 위해서는, Swagger 웹페이지만으로도 api 형태를 최대한 쉽게 알아볼 수 있도록 정의하는 것이 중요합니다.

또, 공통적으로 이용될 Response 의 골격을 공통 클래스로 정의하여 만들게 되면, api를 이용하는 client 개발 측에서도 사용하기 편리해질 수 있습니다.


기존에 개발했었던 store 조회 api와 user 조회 api의 response 형태를 확인해보면, 매우 유사하다는 것을 알 수 있습니다.

result에 api 조회 성공 결과에 따라 SUCCESS 또는 FAIL 이 설정되고,
entity 조회 결과가 user나 store 에 담기고 있습니다.
조회 실패시엔 reason에 조회 실패 이유가 담기고 있네요.

@GetMapping("/{id}")
public Map<String, Object> select(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  User user = userService.select(id);
  if(user != null) {
    response.put("result", "SUCCESS");
    response.put("user", user);
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}
@GetMapping("/{id}")
public Map<String, Object> select(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  Store store = storeService.select(id);
  if(store != null) {
    response.put("result", "SUCCESS");
    response.put("store", store);
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 가게 정보가 없습니다. 가게 id를 확인해주세요.");
  }

  return response;
}

그 외에도, user 수정 api, user 삭제 api 도 Response 형태가 유사합니다.

@PutMapping("/{id}")
public Map<String, Object> update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
  Map<String, Object> response = new HashMap<>();

  int res = userService.update(id, user);
  if(res > 0) {
    response.put("result", "SUCCESS");
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}
@DeleteMapping("/{id}")
public Map<String, Object> delete(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  if(userService.delete(id) > 0) {
    response.put("result", "SUCCESS");
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}

이번 시간에는, Response로 자주 이용될 골격을 정의하여, 기존 api를 개선할 것입니다.


2. Response 클래스 정의

2.1. MessageUtils

Response 클래스를 정의하기 앞서, 프로젝트 전역적으로 이용될 메시지는 MessageUtils에 정의하여 공통 문자열을 유틸화 할 것입니다.

public class MessageUtils {
    public static final String success = "SUCCESS";
    public static final String fail = "FAIL";
}

2.2. BaseResponse

별도로 result를 정의하지 않을 경우에는 성공된 Response로 간주합니다.

@Getter
@Setter
public class BaseResponse {
    private String result;
    private String reason;

    public BaseResponse(){
        this.result = MessageUtils.SUCCESS;
        this.reason  = "";
    }
    public BaseResponse(String result){
        this.reason = MessageUtils.FAIL;
        this.result = result;
    }

}

2.3. DataResponse<T>

store, user, stores, users 등 조회하여 response에 담게될 모든 값들을 data라는 필드값에 설정하여 Response를 일반화시킵니다.

각 데이터 타입은 Generic을 이용하여 주입받도록 정의합니다.
마찬가지로 data를 생성자에 주입받을 경우, 이 response는 성공한 response로 간주합니다.

@Getter
public class DataResponse<T> extends BaseResponse {
    private T data;

    public DataResponse(T data) {
        super();
        this.data = data;
    }
}

3. 변경하기

3.1. user 단건 조회

3.1.1. 수정 전

@GetMapping("/{id}")
public Map<String, Object> select(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  User user = userService.select(id);
  if(user != null) {
    response.put("result", "SUCCESS");
    response.put("user", user);
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}

3.1.2. 수정 후

@GetMapping("/{id}")
public BaseResponse select(@PathVariable("id") long id) {
  User user = userService.select(id);
  if(user != null) {
    return new DataResponse<>(user);
  }
  return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.2. user 수정

3.2.1. 수정 전

@PutMapping("/{id}")
public Map<String, Object> update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
  Map<String, Object> response = new HashMap<>();

  int res = userService.update(id, user);
  if(res > 0) {
    response.put("result", "SUCCESS");
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}

3.2.2. 수정 후

@PutMapping("/{id}")
public BaseResponse update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
  if(userService.update(id, user) > 0) {
    return new BaseResponse();
  }
  return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.3. user 삭제

3.3.1. 수정 전

@DeleteMapping("/{id}")
public Map<String, Object> delete(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  if(userService.delete(id) > 0) {
    response.put("result", "SUCCESS");
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
  }

  return response;
}

3.3.2. 수정 후

@DeleteMapping("/{id}")
public BaseResponse delete(@PathVariable("id") long id) {
  if(userService.delete(id) > 0) {
    return new BaseResponse();
  }
  return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.4. user 추가

3.4.1. 수정 전

@PostMapping("")
public Map<String, Object> insert(@RequestBody @Valid final UserRequest user) {
  Map<String, Object> response = new HashMap<>();

  if(userService.insert(user)) {
    response.put("result", "SUCCESS");
  } else {
    response.put("result", "FAIL");
    response.put("reason", "이미 등록된 회원 정보입니다.");
  }

  return response;
}

3.4.2. 수정 후

@PostMapping("")
public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
  if(userService.insert(user)) {
    return new BaseResponse();
  }
  return new BaseResponse("이미 등록된 회원 정보입니다.");
}

3.5. user 전체 조회

3.5.1. 수정 전

@GetMapping("")
public Map<String, Object> selectAll() {
  Map<String, Object> response = new HashMap<>();

  List<User> users = userService.selectAll();
  response.put("result", "SUCCESS");
  response.put("user", users);

  return response;
}

3.5.2. 수정 후

@GetMapping("")
public DataResponse<List<User>> selectAll() {
  List<User> users = userService.selectAll();
  return new DataResponse<>(users);
}

3.6. store 단건 조회

3.6.1. 수정 전

@GetMapping("/{id}")
public Map<String, Object> select(@PathVariable("id") long id) {
  Map<String, Object> response = new HashMap<>();

  Store store = storeService.select(id);
  if(store != null) {
    response.put("result", "SUCCESS");
    response.put("store", store);
  } else {
    response.put("result", "FAIL");
    response.put("reason", "일치하는 가게 정보가 없습니다. 가게 id를 확인해주세요.");
  }

  return response;
}

3.6.2. 수정 후

@GetMapping("/{id}")
public BaseResponse select(@PathVariable("id") long id) {
  Store store = storeService.select(id);
  if(store != null) {
    return new DataResponse<>(store);
  }
  return new BaseResponse("일치하는 가게 정보가 없습니다. 가게 id를 확인해주세요.");
}

3.7. store 전체 조회

3.7.1. 수정 전

@GetMapping("")
public Map<String, Object> selectAll() {
  Map<String, Object> response = new HashMap<>();

  List<Store> stores = storeService.select();
  response.put("result", "SUCCESS");
  response.put("stores", stores);

  return response;
}

3.7.2. 수정 후

@GetMapping("")
public DataResponse<List<Store>> selectAll() {
  List<Store> stores = storeService.select();
  return new DataResponse<>(stores);
}

4. 변경된 Swagger 화면 및 추후 개선해야할 사항

4.1. 개선된 Swagger 화면

4.1.1. 변경 전

기존에, Map으로 Response 객체를 정의했을 때에는 response Example Value가 모두 아래와 같은 형태로 출력되었습니다.

03-5

03-4



4.1.2. 변경 후

Response 클래스를 별도로 생성하고, 명시적으로 Response로 정의하자, Swagger 웹문서에 아래와 같이 Response Example Value가 표출되는 것을 확인할 수 있습니다.

03-7

stores 전체 조회 역시, store 엔티티에 맞춰 표현됩니다.

03-8



user 수정이나 추가, 삭제 등의 api들도 response 모양이 간소화되어 한눈에 보기 쉽습니다.

03-9

03-12



4.2. 개선해야할 사항

다만, user나 store 단건 조회에서는 Response 예시가 우리가 원하는 모습이 아닙니다.

03-11

03-10



이는, 단건 조회 api에서, 조회 실패시에 대한 Response를 BaseResponse로 정의하여, 컨트롤러 메서드의 Response 타입을 DataResponse의 superclass인 BaseResponse로 정의했기 때문입니다.

@GetMapping("/{id}")
public BaseResponse select(@PathVariable("id") long id) {
  User user = userService.select(id);
  if(user != null) {
    return new DataResponse<>(user);
  }
  return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

물론, api를 실행하는데에는 문제가 없습니다.

그러나, 이런 부분에 대해서도 개선이 된다면, Swagger를 이용하는 client 사용자에게 편의성을 높일 수 있을 것입니다.

다음 시간에는 이 부분을 개선할 수 있는 방안에 대해서 알아볼 것입니다.

GitHub에서 demo 프로젝트(v1.0.7)를 다운받아 볼 수 있습니다.

728x90
반응형