ResponseEntity의 사용법 및 유지보수
ResponseEntity를 쓰는 이유
일반적으로 Controller에서 아래와 같이 객체를 Return 하는 경우 HTTP 응답을 제어할 수가 없습니다.
@GetMapping("/")
public User getUser() {
User user = userService.getUser();
return user;
}
하지만 REST API로 만든다면 클라이언트와 서버 간의 통신에 필요한 정보를 제공해야 합니다.
그럴 때 ResponseEntity를 사용한다면 적절한 상태 코드와 응답 헤더 및 응답 본문을 생성해서 클라이언트에 전달할 수 있습니다.
사용 방법은 아래와 같습니다. 성공을 의미하는 OK(200 code)와 함께 user 객체를 Return 하는 코드입니다.
@GetMapping("/")
public ResponseEntity<User> getUser() {
User user = userService.getUser();
return ResponseEntity.ok(user);
}
ResponseEntity를 잘 쓰는 방법
Return은 생성자보다는 빌더 패턴 사용
생성자 패턴
return new ResponseEntity(body, headers, HttpStatus.valueOf(200));
빌더 패턴
return ResponseEntity.ok()
.headers(headers)
.body(body);
ResponseEntity.ok()는 정적 팩토리 메서드입니다.
그리고 뒷부분을 메소드 체이닝으로 연결한 빌더 패턴을 사용하는 것이 의미가 더 직관적이고 유지보수에 좋습니다.
ResponseEntity의 Body 타입을 명시하라
public ResponseEntity getUser() {
위와 같이 메소드를 작성하면 ResponseEntity의 타입을 작성하지 않은 것이라 Object(모든 자바의 부모 클래스)를 Return으로 받습니다.
위의 코드는 아래의 코드와 기능적으로 의미가 같습니다.
public ResponseEntity<Object> getUser() {
ResponseEntity의 타입은 명시하지 않으면 Object 타입을 Return 해줍니다.
다만 대부분의 상황에서는 유지보수를 위해 타입을 명시해 주는 것이 직관적이므로 좋습니다.
타입을 여러 개 받고 싶다면 Return 타입은 Object 대신 와일드카드 사용
반환 타입이 명확하지 않아도 Return 된다
public ResponseEntity<Object> getUsers() {
List<User> users = userService.getUsers();
return ResponseEntity.ok(users);
}
public ResponseEntity<?> getUsers() {
List<User> users = userService.getUsers();
return ResponseEntity.ok(users);
}
보통 타입을 여러개 받고 싶은 경우 아래와 같이 Object나 와일드카드를 사용할 수 있습니다.
이러한 방법은 개발 구성원들끼리 공유될 경우 개발 생산성을 높일 수 있기 때문에 좋은 선택이 될 수 있습니다.
둘 모두 사용 가능하지만 Return 할 때 객체의 타입이 명확하지 않을 때는 Object를 사용하는 것보다 와일드카드를 사용하는 것이 좋습니다.
정해지지 않은 타입을 반환한다는 점에서는 Object나 와일드카드나 같은 기능을 하지만 와일드카드를 사용하면 반환할 수 있는 객체의 타입이 명확하지 않아도 사용이 가능합니다.
예시로는 아래와 같습니다.
@GetMapping("/users")
public ResponseEntity<?> getUsers() {
List<?> users = userService.getUsers();
return ResponseEntity.ok(users);
}
와일드카드, Object vs 사용자 객체
와일드카드나 Object를 이용해서 API를 만든다면 해당 API를 사용하는 사용자는 불필요한 형변환 작업을 해줘야 하는 단점이 존재합니다.
아래는 (List<User>)로 불필요한 형변환을 하는 예제입니다.
public ResponseEntity<Object> getUsers() {
List<User> users = userService.getUsers();
return ResponseEntity.ok(users);
}
ResponseEntity<Objects> response = restTemplate.getForEntity("/users", Objects.class);
List<User> users = (List<User>) response.getBody();
그래서 와일드카드가 아닌 타입 파라미터를 사용한다면 컴파일 타임에 자동 형변환이 되므로 개발자는 따로 형변환을 해줄 필요가 없습니다.
@GetMapping("/")
public ResponseEntity<T> getUser() {
T user = userService.getUser();
return ResponseEntity.ok(user);
}
하지만 API 설계 측면에서는 타입 파라미터를 이용하는 것보다 명시적으로 사용자 객체를 지정해 주는 것이 더 좋습니다.
@GetMapping("/users")
public ResponseEntity<User> getUser() {
User user = userService.getUsers();
return ResponseEntity.ok(user);
}
객체를 명시적으로 쓰는 것이 해당 라이브러리를 사용하는 개발자들한테 더 직관적으로 보이기 때문에 어떻게 사용하는지 더 직관적입니다.
하지만 하나의 프로젝트 내에 구성원들끼리만 사용하는 개발이라면 와일드카드를 사용해서 개발하는 것이 생산성이 더 올라갈 수 있습니다.
각각의 방식은 어떤 목적을 가지고 있느냐에 Trade-Off가 존재하기 때문에 각각 장단점이 있습니다.
결론적으로 API를 제공하는 형태라면 타입은 항상 명시적으로 작성하는 것이 좋고 개발 구성원들끼리 단일 프로젝트를 설계한다면 와일드카드로 전체 타입을 받는 형태로 개발하는 것이 생산성에 더 이로울 수 있습니다.
물론 단일 프로젝트라 하더라도 규모가 크다면 와일드카드가 좋지 않은 선택이 될 수도 있겠습니다.