프로젝트 개요 : 이것이 취업을 위한 백엔드 개발이다 클론코딩 - 상품관리 애플리케이션
프로젝트 환경 : IntelliJ, SpringBoot, MySQL
프로젝트 코드 : https://github.com/smkim9202/ProductManagement
유효성 검사 위치
각 필드별 요구사항
상품 번호
- 1부터 시작, 추가시 1씩 증가
- 동일한 상품 번호는 존재 불가
상품 이름
- 1~100글자 사이의 문자열
- 동일한 상품 이름 존재 불가
가격
- 0~1,000,000원 사이의 값
재고 수량
- 0~9,999개 사이의 값
도메인 지식
- 길이, 자료형 등의 도메인 지식
- 도메인 객체 내부에 유효성 검사를 실행 - 도메인지식은 객체 밖으로 빠져 나가지 않는 것이 좋음
- 도메인 지식을 도메인 계층 밖으로 공개하면 중복된 코드 양산, 클래스 응집도 낮아짐
- 서비스 코드에서 도메인 지식을 추가시 핵심 비지니스(레포지토리에 객체 저장, DTO와 도메인객체 변환)가 보이지 않음
- null값을 반환하는 불필요한 코드 작성, 예외적인 상황을 정상적인 상황인 것처럼 반환 값을 줌
- 도메인 객체 유효성 검사는 stter에서 검사, Bean Validation 사용하여 검사 등 여러 방법이 있음
데이터 자체의 유효성
- 상품 이름 필드를 사용자가 JSON에 포함시키지 않거나, null 데이터를 보내고 있는 경우 등 데이터 그 자체가 유효한지 검사
- DTO에서 유효성 검사 실행
도메인 객체 유효성 검사
Bean Vaildation을 통한 유효성 검사
Bean Vaildation
- JSR(Java Specification Requests, 자바 스펙 문서)-303 문서에 설명되어 있음
- 우리가 실제 사용하는 것은 JSR-303 스펙을 구현한 구현체로, JSR-303이라는 스펙을 사용하여 유효성 검사하는 행위
- 실제로 주로 사용되는 스펙은 JSR-380이라는 확장된 스펙
- JSR-303이 Bean Validation 1.0 JSR-380이 Bean Validation 2.0 버전
Product 생성 시 유효성 검사 추가
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Bean Validation 라이브러리 추가
메이븐 저장소에서 spring-boot-starter-validation 검색해서 최신 버전을 pom.xml에 추가 후 다운로드
<version> 태그는 생략 : 스프링 부트와 관련한 의존성에 version 생략시 상단에 위치한 <parnet> 태그 내 spring-boot-starter-parent 의존성의 버전을 따라간다
Product.java
@Size(min = 1, max = 100)
private String name;
@Max(1_000_000)
@Min(0)
private Integer price;
@Max(9_999)
@Min(0)
private Integer amount;
ValidationService.java
package kr.co.api.product.management.application;
import jakarta.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
public class ValidationService {
public <T> void checkValid(@Valid T validationTarget){
//public <T> void checkValid(@Valid Product validationTarget){ //오직 Product만의 checkValid
//do nothing
}
}
@Service 애너테이션
해당 클래스를 인스턴스화하여 빈으로 등록
@Validated 애너테이션
해당 클래스에 있는 메서드들 중 @Valid가 붙은 메서드 매개변수를 유효성 검사하겠다는 의미
Product에 붙인 Bean Validation 애너테이션을 기준으로 검사가 이루어진다
<T>
해당 메서드 내에서 T라는 이름의 제네릭 사용하겠다는 선언, T에는 어떤 타입이든 올 수 있다는 것을 의미로 코드 중복을 피할 수 있다
만약 Product만의 checkValid 메서드를 만들려면 T대신 Product를 타입으로 넣어준다
checkValid 메서드
아무것도 하는 일이 없고, 인자를 담아 호출하는 것만으로 유효성에 대한 검증이 이루어진다.
SimpleProductService.java
@Service
public class SimpleProductService {
private ListProductRepository listProductRepository;
private ModelMapper modelMapper;
private ValidationService validationService;
@Autowired
SimpleProductService(ListProductRepository listProductRepository,
ModelMapper modelMapper, ValidationService validationService){
this.listProductRepository = listProductRepository;
this.modelMapper = modelMapper;
this.validationService = validationService;
}
public ProductDto add(ProductDto productDto){
// 1.ProductDto를 Product로 변환하는 코드
Product product = modelMapper.map(productDto, Product.class);
// 도메인 지식 유효성검사
validationService.checkValid(product);
// 2.레포지토리를 호출하는 코드
Product saveProduct = listProductRepository.add(product);
// 3. Product를 ProductDto로 변환하는 코드
ProductDto saveProductDto = modelMapper.map(saveProduct, ProductDto.class);
// 4. DTO 반환하는 코드
return saveProductDto;
}
...
}
ValidationService 의존성 주입
ModelMapper가 DTO를 Entity로 변환한 직후 바로 ValidationService의 checkValid 메서드를 호출하며 인자로 변환된 Product를 넘김
checkValid 메서드를 호출한 후에는 ValidationService가 유효성을 검증
Product 유효성 검사 테스트
//요청
{"name":"","price":200,"amount":100}
//응답
{
"timestamp": "2025-03-20T11:45:24.899+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/products"
}
//로그
jakarta.validation.ConstraintViolationException:
checkValid.validationTarget.name: 크기가 1에서 100 사이여야 합니다
//요청
{"name":"연필","price":-30,"amount":100}
//응답
{
"timestamp": "2025-03-20T11:48:22.872+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/products"
}
//로그
jakarta.validation.ConstraintViolationException:
checkValid.validationTarget.price: 0 이상이어야 합니다
//요청
{"name":"연필","price":2000000,"amount":999999}
//응답
{
"timestamp": "2025-03-20T11:50:00.558+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/products"
}
//로그
jakarta.validation.ConstraintViolationException:
checkValid.validationTarget.price: 1000000 이하여야 합니다,
checkValid.validationTarget.amount: 9999 이하여야 합니다
HTTP 응답 상태코드가 400에러(파라미터 오류)가 오지 않고 500에러(서버 요청 처리 과정 오류) 오는 이유
500에러가 발생한 경우 id가 증가하지 않음 => 유효성 검사 실패한 상품이 리스트에 저장되지 않음
ConstraintViolationException을 처리해 주지 않고 컨트롤러에서까지 throw되고 있기 때문에 500에러가 발생
500에러를 반환하려면 예외를 처리하지 않고 컨트롤러 밖으로 throw하면 되고, 적절한 상태 코드로 반환해주는 전역 예외 핸들러 추가시 에러에 맞는 상태코드 반환 됨
응답 바디 활용
유효성 검사 실패 정보가 로그에만 나오고 응답 바디에는 안나오는 상황
응답 바디에 실패정보를 포함 시키는 일도 전역 예외 핸들러로 처리 가능
컨트롤러 유효성 검사
Bean Vaildation을 통한 유효성 검사
ProductDto.java
@NotNull
private String name;
@NotNull
private Integer price;
@NotNull
private Integer amount;
@NotNull : 오직 null만 허용하지 않음, ""(빈 문자열)과 " "(띄어쓰기) 허용
@NotEmpty : null과 ""(빈 문자열) 둘 다 허용하지 않음, " "(띄어쓰기) 허용
@NotBlank : null과 ""과 " " 전부 허용하지 않음
ProductController.java
@RequestMapping(value = "/products", method= RequestMethod.POST)
public ProductDto createProduct(@Valid @RequestBody ProductDto productDto){
return simpleProductService.add(productDto);
}
컨트롤러 매개변수에 @Valid 애너테이션 달아주고, 컨트롤러 클래스에 @Validated 달아 줄 필요는 없음
ProductDto 유효성 검사 테스트
//요청
{"name":"연필","amount":999999}
//응답
{
"timestamp": "2025-03-20T12:17:38.844+00:00",
"status": 400,
"error": "Bad Request",
"path": "/products"
}
//로그
Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in public kr.co.api.product.management.
presentation.ProductDto kr.co.api.product.management.presentation.
ProductController.createProduct(kr.co.api.product.management.presentation.ProductDto):
[Field error in object 'productDto' on field 'price': rejected value [null];
codes [NotNull.productDto.price,NotNull.price,NotNull.java.lang.Integer,NotNull];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [productDto.price,price]; arguments [];
default message [price]]; default message [널이어서는 안됩니다]] ]
'프로젝트 > clone coding' 카테고리의 다른 글
[백엔드 개발 : ProductManagement] 상품 관리 애플리케이션 DB 연동(MySQL) (0) | 2025.04.04 |
---|---|
[백엔드 개발 : ProductManagement] 전역 예외 핸들러 추가 (0) | 2025.03.27 |
[백엔드 개발 : ProductManagement] 상품 조회/수정/삭제 구현 (0) | 2025.03.19 |
[백엔드 개발 : ProductManagement] DTO와 getter, setter (0) | 2025.03.18 |
[백엔드 개발 : ProductManagement] 상품 추가 구현(프로젝트 구조 잡기) (0) | 2025.03.17 |