Framework/Spring

[Validation] 데이터 검증, 비즈니스 로직 검증

simDev1234 2022. 9. 21. 04:41

|  Validation

- 지난 번 스프링을 처음 배울 당시 아래와 같이 Validation에 대한 개념을 배웠었다.

✅ Validation은 유효성 검증을 말하며, 주로 HTTP Request에서 잘못된 내용을 검증할 때 사용한다.
- 데이터 검증     : 필수 데이터 / 문자열 길이 및 숫자형 데이터 범위 / 이메일 및 신용카드 번호 등 형식 확인
- 비즈니스 검증 : 서비스 정책에 따라 데이터 검증
✅ Validation 방식
1) Java Bean Validation : dto클래스 맴버에 Annotaion(ex. @NotBlank, @Size, @Email...)을 붙이는 방식
2) Spring validator 인터페이스 구현을 통한 validation 
✅ Validation 주의사항 : Validation을 멀리 두면 테스트 및 유지보수성이 떨어지므로 가능한 Annotaion방식을 쓰는 게 좋다.
* 수업에서의 제안 : 1차로 dto에 Annotation방식 사용, 2차로 비즈니스 검증 후 실패 시 Custom Exception throw

- 오늘은 계좌 프로그램의 계좌 생성 기능에 대한 단위 테스트를 하면서 위 데이터를 검증하고, 비즈니스 로직을 검정하는 실습해보았다.

- 이것을 연습 삼아서 동물 병원 차트를 생성하는 걸로 바꿔서 해보았다.

 

1.  데이터 검증하기 - Common Validation Annotations(Java Bean Validation)

  • @NotNull: to say that a field must not be null.
  • @NotEmpty: to say that a list field must not empty.
  • @NotBlank: to say that a string field must not be the empty string (i.e. it must have at least one character).
  • @Min and @Max: to say that a numerical field is only valid when it’s value is above or below a certain value.
  • @Pattern: to say that a string field is only valid when it matches a certain regular expression.
  • @Email: to say that a string field must be a valid email address.

    [출처] https://reflectoring.io/bean-validation-with-spring-boot/

- 수업에서는 HTTP 요청/응답 데이터를 검증할 때, DTO객체에서 Java Bean Validation을 썼다.

- 연습 삼아 동물 병원 차트를 통해 정리해보았다.

- 아래는 동물 병원 차트 도메인 객체

// import 생략

// Lombok getter/setter/생성자 생략
@Entity
@EntityListener(AuditingEntityListener.class)
public class Chart{

    @Id
    @GeneratedValue   // sequence (auto-increment)
    private Long id;
    
    private String chartNo;   // 차트번호 (ex. AA20221211 - 종분류+생성일자)

    @ManyToOne
    private Owner owner;      // 주인
    private Patient patient;  // 환자 기본 정보 (이름, 나이, 성별 등)
    private VaccineRecord vaccineRecord; // 백신 여부
    
    private String memo;      // 증상 메모
    private String diagnose;  // 진단명 
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

[1] DTO : Request/Response Data-binding용 객체에 제약조건 걸기

[ 예시 ] 동물 병원 차트 생성 DTO - Request와 Response 시 Data-binding용
- 요청시 : 주인 id와 동물 id는 반드시 받아 존재하는지 확인한다. / 차트에 메모와 진단이 있을 수도 있고 없을 수도 있다.
- 응답시 : 생성된 차트의 차트번호 반환
// import 생략

public class CreateChart{

   // lombok 게터/세터/생성자 생략
   public static class Request{
    	@NotNull
        @Min(1)
    	private Long ownerId;
        @NotNull
        @Min(1)
        private Long patientId;
        private String memo;
        private String diagonose;
   }
   
   // lombok 게터/세터/생성자 생략
   public static class Response{
        @NotNull
        private String chartNo;
   }
   
   public static Response from(ChartDto chartDto){
        return Response.builder()
        	.ownerId(chartDto.getOwnerId())
        	.patientId(chartDto.getPatientId())
        	.memo(chartDto.getMemo())
        	.diagonose(chartDto.getDiagnose())
        	.build();
   }

}

 

[2] DTO :  도메인에서 필요한 데이터만 넘겨주는 객체 생성

* 지연 로딩을 위해서 필요한 객체이다. (지연 로딩이란? JPA에서 프록시 객체 - 참조값을 가진다 - 로 Entity간 연관관계를 주입할 때, 한 번에 모든 데이터를 올리는 것이 아니라 필요에 따라 지연하여 데이터를 로딩하는 방식)

* 지연 로딩을 하게 되면 사용자의 요청사항에 유연하게 반응할 수 있다.

// import 생략

// lombok 관련 annotation 생략
public class ChartDto {
    private String chartNo;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public static ChartDto fromEntity(Chart chart){
    	return ChartDto.builder()
            .chartNo(chart.getChartNo())
            .createdAt(chart.getCreatedAt())
            .updatedAt(chart.getUpdatedAt())
            .build();
    }
}

 

[3] Controller에서 @Valid로 요청 시 데이터 제약사항 검증하기

[ 예시 ] 동물 병원 차트 생성 컨트롤러 - REST 방식의 컨트롤러

POST http://ip주소:port번호/chart
content-type: application/json

{
        "ownerId": 1,
        "name": "여름이"
}
// import 생략

@RestController
@RequiredArgsConstructor
public class ChartController{
    private final ChartService chartService;
    
    @PostMapping("/chart")
    public CreateChart.Response createChart(@RequestBody @Valid CreateChart.Request request){
    	
        return CreateChart.Response.from(
        	chartService.createChart(
        		request.getOwnerId(),
        		request.getPatientId(),
        	)
        );
        
    }
}

 

2.  비즈니스 로직 검증하기 - Custom Exception

[1] exception : 예외사항에 대한 Custom Exception을 만든다.

(1) 먼저 Enum으로 예외 코드를 만든다.
(2) 해당 Enum을 담은 커스텀 Exception을 만든다.
// import 생략

@Getter
@AllArgsConstructor
public Enum ErrorCode {
    OWNER_NOT_FOUND("주인이 없습니다."),
    PATIENT_NOT_FOUND("환자가 없습니다."),
    CHART_NO_NOT_FOUND("차트번호를 찾을 수 없습니다.")
    
    private String description;
}
// import 생략

// lombok 생략
public class ChartException extends RuntimeException {
    ErrorCode errorCode;
    String errorMessage;
    
    public ChartException(ErrorCode errorCode) {
    	this.errorCode = errorCode;
        this.errorMessage = errorCode.getDescription();
    }
}

 

[2] service : 비즈니스 로직에 따라 서비스 객체 생성하기

[ 예시 ] 동물 병원 차트 생성 서비스 - 비즈니스 로직 검증
- 주인이 있는지 조회하기 
- 동물이 있는지 조회하기
- 동물의 차트가 100개를 넘어가는지 확인 (*validate를 위해 임의로 만든 것)
- 최신 차트 번호 생성
- 차트 저장 후, 차트 정보를 넘긴다.
// import 생략

@Service
@RequiredArgsConstructor
public class ChartService {
    private final ChartRepository chartRepository;
    private final OwnerRepository OwnerRepository;
    private final PatientRepository PatientRepository;
    
    @Transactional
    public ChartDto createChart(Long ownerId, Long patinetId){
    	Onwer owner = ownerRepositroy.findById(ownerId)
        	.orElseThrow(() -> new ChartException(ErrorCode.OWNER_NOT_FOUND));
            
        Patient patient = patientRepository.findById(patientId)
        	.orElseThrow(() -> new ChartException(ErrorCode.PATIENT_NOT_FOUND));
            
        // 차트 100개 넘는지 별도로 validate 처리
        // -- [ctrl]+[alt]+[M]으로 메소드를 extract
        validate(patient);  
        
        String chartNo = chartRepository.findFirstByOrderByIdDesc()
        	.map(chart -> (Integer.parseInt(chart.getChartNo().substring(2) + 1)) + "")
        	.orElseThrow(() -> new ChartException(ErrorCode.CHART_NO_NOT_FOUND));
        
        return ChartDto.fromEntity(
        	chartRepository.save(
            	Chart.builder()
                	//생략
                	.build()
            )
        );        
    }
    
    public void validate (Patient patient) {
    	if (chartRepository.CountByPatient(patient) == 100) {
        	throw new ChartException(ErrorCode.MAX_CHART_COUNT_100);
        }
    }
}

 

[ 참고 및 출처 ]

부트캠프 수업 내용

https://reflectoring.io/bean-validation-with-spring-boot/