💋 코드 저장소
💋 인트로
이번 미션은 상품에 대한 CRUD가 메인이었다.
API 설계에 대해서 고민해볼 수 있었던 미션이었다!
또 완전 좋은 리뷰어를 만나서, 많이 배웠다.
너무 좋은 리뷰를 많이 남겨주셔서 잊기 전에 회고인 척 하는 피드백 정리를 작성해보려고 한다!
💋 CRUD DAO 설계
CRUD에 관해서 dao 인터페이스를 작성하는데, 메서드 시그니처 중에서도 특히 반환값을 어떻게 해야 할지 고민이었다.
public interface ProductDao {
Product save(Product product);
List<Product> findAll();
Product findById(long id);
Product update(Product product);
void deleteById(long id);
}
- UPDATE의 경우에는 반영이 잘 되었는지 확인하기 위해 변화한 리소스를 반환하기도 한다.
- DELETE의 경우에는 리소스를 삭제해 버렸기 때문에 더이상 확인할 필요가 없어서, 리소스를 반환하지 않기도 한다.
💋 PUT vs PATCH
HTTP Method 중 PUT과 PATCH에 대해 확실히 구분하게 되었다!
리뷰어 현구막은 이 내용을 보면 둘의 차이를 이해할 수 있을 거라고 했는데, 글 자체가 너무 좋아서 여러 번 읽어보았다.
둘의 차이를 정리하자면,
- 보통 PUT 은 전체 정보 업데이트(완전히 대체), PATCH 는 일부분 업데이트에 사용된다.
- PUT 은 멱등성을 보장하지만, PATCH 는 멱등성을 보장할 수도, 아닐 수도 있다.
- 그 이유는 PATCH 에는 별다른 제약이 없어 구현방식이 굉장히 자유롭기 때문이다.
[개인적 궁금증] PATCH를 사용해서 수정이 된 부분에 대해서만 변경해서 불필요하게 Body를 키우고 싶지 않은데, 클라이언트에서 보내는 형식이 전체 데이터를 다 보내고 있다면 어떻게 해야할까?
트레이드오프니 내 맘대로 하면 된다는 답변을 받았다.
필요한 부분만 request로 넘기겠다고 클라이언트 측과 합의하면 좋겠지만, 실제로는 클라이언트에서 모든 경우 분기처리 해야 하기 때문에 PUT으로 처리하는 것이 현재에서는 편하다.
💋 RESTful API에 대해서 조금 이해하게 되었음!
아직 설명할 수 있을 정도는 아니지만, 현구막이 준 이 완전 대박 짱좋은 포스팅을 읽고, API 설계가 무엇을 하는 것이고, 그중에서도 RESTful하다는 것은 어떤 의미인지에 대해서 조금은 이해하게 되었다.
RESTful 웹 애플리케이션의 특징에 대해서 설명하는 아래의 완전 교과서적인 글을 보면,
이전에는 어질어질 했는데 이제는 어케든 이해할 수 있게 되었다.
RESTful 웹 서비스의 주요 특징은 다음과 같습니다.
자원(Resource) : 모든 자원은 고유한 URI(Uniform Resource Identifier) 로 식별되어야 합니다.
행위(Verb) : HTTP Method(GET, POST, PUT, DELETE)를 이용해 자원에 대한 행위를 정의합니다.
표현(Representations) : RESTful 웹 서비스는 JSON, XML 등의 다양한 표현 방법을 제공합니다.
상태(Stateless) : RESTful 웹 서비스는 클라이언트와 서버 간 상태를 유지하지 않습니다.
캐시(Cacheable) : RESTful 웹 서비스는 캐시를 사용할 수 있습니다.
계층화(Layered System) : RESTful 웹 서비스는 다중 계층으로 구성될 수 있습니다. (Controller, Service, Repository 등등)
위와 같은 특징으로 인해 RESTful 웹 서비스는 HTTP 프로토콜의 기본적인 요청과 응답만으로도 서비스를 제공할 수 있으며, 클라이언트와 서버 간의 의존성을 줄여 유연하고 확장성 높은 웹 서비스를 제공할 수 있습니다.
현재 이해하고 있는 뚜렷한 특징은, URL에는 행위를 넣는 대신에 리소스를 계층적으로 표현하고, 행위는 HTTP Method로 표현한다는 것이다. 예를 들어 HTTP Method 중 GET은 자원을 조회할 때, POST는 자원을 생성할 때 사용한다.
또 RESTful 웹 서비스는 JSON, XML, HTML, Plain Text 등 다양한 형태의 데이터를 반환할 수 있다. 하지만 JSON 형태의 데이터를 반환하는 것이 가장 일반적이며, 요즘은 대부분의 RESTful 웹 서비스가 JSON을 사용한다. 이는 JSON 형태가 가볍고, 파싱하기 쉽기 때문이다. (예전에는 XML이 거의 표준이었는데, 현재는 대부분 JSON을 사용한다고 한다)
완전히 소화할 수 있게 되면 포스팅해야지!
💋 schema.sql에는 데이터의 스키마를, data.sql에는 초기 데이터를
schema.sql이라는 파일이 있는지도 처음 알게 되었다. 시작은 더미 데이터를 저장하기 위해서 고민하다가 시작되었는데,
이 두 가지 파일에 대해서 조금 더 공부해보고 포스팅도 했다.
또 더미데이터를 추가해주는 방식에 대해서, 크게는 sql을 통해 INSERT하는 방식과 객체 형태로 데이터를 입력하는 방식 두 가지가 있어서 어떤 것이 더 적절할 지에 대해 궁금했다.
💋 계층을 위한 계층이 되어버린 내 Service의 역할 + 클래스 명칭은 좁게!
내가 만든 서비스 계층이었다.
@Service
public class ProductService {
private final ProductDao productDao;
public ProductService(ProductDao productDao) {
this.productDao = productDao;
}
public List<Product> findProducts() {
return productDao.findAll();
}
public void updateProduct(Product product) {
productDao.update(product);
}
public Product createProduct(Product product) {
return productDao.save(product);
}
public void deleteProductBy(long id) {
productDao.deleteById(id);
}
}
현재 이 서비스 계층에서는 다른 도메인 로직 없이, 컨트롤러가 dao 계층으로 넘어가는 함수의 호출만 해주고 있다...
어떻게 보면 꼭 필요한 계층이 아닌, 일반적인 Spring MVC를 설명할 때 등장하는 계층들의 구조를 충족하기 위해서 어거지로 만든, 계층을 위한 계층이라고 볼 수 있을 것 같기도 하다..!
일단은 dao의 구체적인 이름 자체를 controller에서 알지 못하게 하는 정도의 용도는 하고 있었다.
이건 불필요한 계층일까?
학습을 위해서 나쁘지 않다는 답변을 받았고 추가적으로 service 계층이 하는 역할이 무엇인지에 대해서 리뷰어의 생각을 들을 수 있었다.
이 그림을 보고, 스프링이 자바의 객체지향 중 다형성이라는 특징을 정말정말 쉽게 만들 수 있도록 해준다는 느낌을 받았다.
다형성은 언제든지 부품을 갈아끼울 수 있는 것인데, 이렇게 역할 별로 계층만 명확하게 나누어 놓으면, 클라이언트와 서버는 서로 완전 대체 가능한 형태가 된다. 위 두번째 그림에서는 클라이언트가 변화할 때에도 서버에는 아무런 영향이 없이 교체하는 것을 보여주고 있다.
반대로 서버의 기능에 대해 인터페이스(나 상속)를 통해서 추상화를 잘 하게 되면, 클라이언트의 코드에 변경 없이 기능 확장이 가능하다.
물론 추상화에는 비용이 들기 때문에 트레이드오프를 잘 고려해봐야 할 것이다.
* 비용: 개발자가 코드를 한 번 더 열어봐서 구현 클래스가 무엇인지 한 번 더 들어가 봐야 한다.
암튼 위의 코드의 문제는 쓸모없는 계층이라는 것이 아니라, 너무 넓은 범위로 한 클래스가 많은 일을 하고 있었다는 것이었다.
맞다맞다... 모두 맞는말....
나는 말 잘 듣는 깃짱...
곧바로 클래스 내 동작들을 분리해서 아래와 같이 만들었다.
@RestController
@RequestMapping("/admin/products")
public class ProductsController {
private final ProductCreateService createService;
private final ProductUpdateService updateService;
private final ProductDeleteService deleteService;
public ProductsController(
final ProductCreateService createService,
final ProductUpdateService updateService,
final ProductDeleteService deleteService) {
this.createService = createService;
this.updateService = updateService;
this.deleteService = deleteService;
}
// ...
}
💋 DTO에서의 검증은 말이 안되는 형태만 거르는 정도로!
@Valid 어노테이션에 대해서 박스터가 알려줘서, 너무 신난 바람에 DTO에 도메인의 검증 로직과 같은 내용들을 적었다.
public class ProductRequest {
@Size(max = 20, message = "상품 이름은 20자 이내로 입력해야 합니다.")
private final String name;
private final String imgUrl;
@Min(value = 1000, message = "상품 가격은 최소 1000원 이상이어야 합니다.")
private final int price;
// ...
}
💋 JdbcTemplate, DataSource 클래스는 빈으로 어떻게 등록되어 있는걸까?
JdbcTemplate, DataSource 클래스는 dao에서 별다른 노력 없이 바로 DI 된다. 빈으로 등록된 객체만 자동 DI할 수 있는데, 이건 어떻게 그렇게 되는걸까?
클래스를 타고 들어가서 봐도
뭐.. 딱히 빈으로 등록되어 있는 어노테이션은 없다...!
내 어시스턴트 말에 따르면, Spring Boot중에서 starter-jdbc 라이브러리가 알아서 해준다는데!
현구막의 엄청난 설명으로 단방에 이해해버렸다!
💋 사용자의 잘못과 서버의 잘못을 구분해서 예외 처리하기
나는 전역 ControllerAdvice으로부터 아래와 같이 두 가지 에러를 받고 있었다.
@RestControllerAdvice
public class ProductControllerAdvice {
@ExceptionHandler
public ResponseEntity<String> handleException(MethodArgumentNotValidException e) {
return ResponseEntity.badRequest().body(e.getBindingResult().getFieldError().getDefaultMessage());
}
@ExceptionHandler
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.badRequest().body("알 수 없는 에러가 발생했습니다.");
}
}
request를 받을 때 @Valid 어노테이션을 넣고, Request DTO에서 관련한 검증들을 어노테이션을 추가한 후에, 유효하지 않은 값을 입력하려고 하면 MethodArgumentNotValidException이 발생한다. (아주 사소한 부분임)
그 외에 다양한 사용자의 에러가 있을 수 있는데, 나는 나머지 모든 예외를 퉁쳐서, 그냥 알 수 없는 예외라고 받고 있었다.
곧바로 수정했다... ><
그리고 현구막의 포스팅을 하나 또 추천받았는데, 좀 센스 있는 예외 메세지에 대해서도 늘 연습을 해야겠다고 생각했다.
💋 RequestDto, Entity의 타입은 Nullable하게..? (아직 해결 안됨)
(아직 해결 잘 안됨)
나의 삽질 로그를 첨부하고 간다...
💋 View Controller에서는 반환되는 view 이름의 확장자명까지 쓰자!
취향 차이가 많이 갈리는 것 같긴 한데,
뚜렷하게 좋은 이유가 몇 가지 있다.
1. 가독성이 좋습니다:
뷰 이름에 확장자를 포함하면 뷰의 유형을 쉽게 알 수 있습니다.
따라서, 뷰를 찾거나 유지보수할 때 더욱 가독성이 좋습니다.
2. 확장자를 명시함으로써 응답 형식이 명시적으로 설정됩니다:
뷰 이름에 확장자를 포함하면 Spring은 응답 형식을 명시적으로 설정할 수 있습니다.
예를 들어, 브라우저에서 index.html을 요청하면 Spring은 HTML 응답으로 처리합니다.
3. 뷰 이름에 대한 일관성을 유지할 수 있습니다:
뷰 이름을 일관성 있게 유지하면 뷰를 찾거나 변경할 때 불필요한 수정 작업을 줄일 수 있습니다.
4. 일부 브라우저에서는 확장자를 포함해야 정상적으로 렌더링 됩니다:
일부 브라우저에서는 확장자를 포함하지 않으면 정상적으로 렌더링이 되지 않을 수 있습니다.
따라서 뷰 이름에 확장자를 포함시키면 이러한 문제를 방지할 수 있습니다.
'우아한테크코스5기' 카테고리의 다른 글
[우테코] 레벨로그: 레벨2 동안 공부한 내용들을 정리하며 (3) | 2023.06.07 |
---|---|
[우테코] 지하철 미션 회고: 지하철 미션인데 리뷰어도 서브웨이 (0) | 2023.05.24 |
[우테코] 레벨2에서 학습할 키워드 (1) | 2023.04.21 |
[우테코] 인프런 CTO 이동욱님의 '자존감 기둥 세우기' 강연을 듣고 (3) | 2023.04.19 |
[우테코] 레벨1 인터뷰 회고: 학습 측면, 말하기 측면에서의 피드백과 나의 피드백 (0) | 2023.04.15 |