지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능입니다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어듭니다.
ArrayList<String> list = new ArrayList<>();
<>
이 꺽쇠 괄호가 제네릭입니다. 다룰 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여줍니다. 위 코드의 리스트 클래스의 자료형은 String
타입으로 지정되어 문자열 데이터만 적재할 수 있게 됩니다.
class Box {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem() {
return item;
}
}
위와 같은 코드를 제네릭으로 바꿔보면 아래와 같이 됩니다.
class Box<T> { // 지네릭 타입 T를 선언
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
Box<T>
에서 T를 타입 변수(type variable)
라고 하며 Type
의 첫 글자에서 따온 것입니다. 타입 변수는 T가 아닌 다른 것을 사용해도 됩니다.
기존에 Controller의 코드는 아래와 같았습니다.
@PostMapping("/customers")
public ResponseEntity<CustomerResponse> signup(@RequestBody @Valid CustomerDTO customerDTO) {
CustomerResponse customerResponse = customerService.save(customerDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(customerResponse);
}
@PostMapping("/customers/login")
public ResponseEntity<CustomerResponse> login(@RequestBody @Valid CustomerLoginRequest customerLoginRequest) {
CustomerResponse customerLoginResponse = customerService.findByEmailAndPassword(customerLoginRequest);
return ResponseEntity.status(HttpStatus.OK).body(customerLoginResponse);
}
@GetMapping("/customers")
public ResponseEntity<List<CustomerResponse>> findCustomers() {
return ResponseEntity.status(HttpStatus.OK).body(customerService.findAll());
}
@GetMapping("/customers/{id}/address")
public ResponseEntity<AddressDTO> findAddressByCustomerId(@PathVariable Long id) {
return ResponseEntity.status(HttpStatus.OK).body(customerService.findAddressByCustomerId(id));
}
위 코드를 보면
ReponseEntity.status(`상태코드`).body(`전달할 객체`);
를 공통화해줄 수 있다는 생각을 했습니다. 이렇게 하면 코드가 간결해지고 유지보수하기 간편해질 것입니다.
그래서 처음에 생각한건 2가지 방법이 있었습니다.
public class ApiResponse {
public static final ResponseEntity.BodyBuilder OK = ResponseEntity.status(HttpStatus.OK);
}
public class ApiResponse {
public static <T> ResponseEntity<T> OK(T object) {
return ResponseEntity.ok().body(object);
}
}
@GetMapping("/products/search")
public ResponseEntity<ProductPageResponse> search(@ModelAttribute ProductSearchDTO productSearchDTO) {
Pageable pageable = PageRequest.of(productSearchDTO.getPageNumber(), productSearchDTO.getPageSize());
ProductPageResponse productPageResponse = productService.search(productSearchDTO.getKeyword(), pageable);
return OK.body(productPageResponse); // body를 매번 호출해야함.
}
첫 번째 방법은 static final
을 이용해서 불변의 전역변수를 생성 후 반환하여 Controller에서 body()만 바꿔주는 방법입니다.
@GetMapping("/products/search")
public ResponseEntity<ProductPageResponse> search(@ModelAttribute ProductSearchDTO productSearchDTO) {
Pageable pageable = PageRequest.of(productSearchDTO.getPageNumber(), productSearchDTO.getPageSize());
ProductPageResponse productPageResponse = productService.search(productSearchDTO.getKeyword(), pageable);
return OK(productPageResponse);
}
두 번째 방법은 ApiResponse 클래스에서 body의 내용을 포함한 객체를 생성해서 반환해주는 방법입니다.
저는 이 두 번째 방법을 채택하였습니다. 이 방법은 정적 팩토리 메서드 패턴을 활용합니다. 정적 팩토리 메서드는 객체 생성 로직을 캡슐화하고, 메서드를 통해 객체를 생성하여 반환하는 기법입니다. 여기서는 OK()라는 정적 메서드를 통해 ResponseEntity 인스턴스를 생성하고 반환합니다.
@RestController
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/products")
public ResponseEntity<List<ProductDTO>> findAll() {
return OK(productService.findAll());
}
@GetMapping("/products/search")
public ResponseEntity<ProductPageResponse> search(@ModelAttribute ProductSearchDTO productSearchDTO) {
Pageable pageable = PageRequest.of(productSearchDTO.getPageNumber(), productSearchDTO.getPageSize());
ProductPageResponse productPageResponse = productService.search(productSearchDTO.getKeyword(), pageable);
return OK(productPageResponse);
}
@GetMapping("/products/{id}")
public ResponseEntity<ProductDTO> findById(@PathVariable Long id) {
return OK(productService.findById(id));
}
}
위와 같이 좀 더 깔끔하게 리팩토링 할 수 있었습니다.