JavaRush /Java Blog /Random-KO /REST API 및 데이터 검증
Денис
레벨 37
Киев

REST API 및 데이터 검증

Random-KO 그룹에 게시되었습니다
첫 번째 부분에 대한 링크: REST API 및 다음 테스트 작업 음, 우리 애플리케이션이 작동하고 있고 애플리케이션으로부터 일종의 응답을 얻을 수 있지만 이것이 우리에게 무엇을 제공합니까? 유용한 작업을 수행하지 않습니다. 말하자마자, 유용한 것을 구현해 봅시다. 우선, build.gradle에 몇 가지 새로운 종속성을 추가해 보겠습니다. 이는 우리에게 유용할 것입니다.
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.springframework.boot:spring-boot-starter-validation'
그리고 처리해야 하는 실제 데이터부터 시작하겠습니다. 지속성 패키지로 돌아가서 엔터티 채우기를 시작하겠습니다. 기억하시는 것처럼, 우리는 하나의 필드만 존재하도록 남겨두고 `@GeneratedValue(strategy = GenerationType.IDENTITY)`를 통해 자동이 생성됩니다. 첫 번째 장의 기술 사양을 기억해 봅시다:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
처음으로 필드가 충분하므로 구현을 시작하겠습니다. 처음 세 필드는 질문을 제기하지 않습니다. 이는 일반적인 줄이지만 급여 필드는 이미 암시적입니다. 왜 실제 라인인가? 실제 작업에서도 이런 일이 발생합니다. 고객이 당신에게 와서 "이 페이로드를 보내려고 하니 당신이 그것을 처리하세요"라고 말합니다. 물론 어깨를 으쓱하고 합의에 도달하여 필요한 형식으로 데이터를 전송하는 것이 더 낫다고 설명할 수 있습니다. 우리가 스마트 클라이언트를 만났고 숫자 형식으로 숫자를 전송하는 것이 더 낫다는 데 동의했다고 가정하고 돈에 대해 이야기하고 있으므로 Double로 두십시오. 페이로드의 다음 매개변수는 고용 날짜가 될 것이며, 클라이언트는 합의된 형식(yyyy-mm-dd)으로 이를 우리에게 보낼 것입니다. 여기서 y는 년, m은 일, d는 일(2022-)을 담당합니다. 08-12. 현재 마지막 필드는 클라이언트에게 할당된 작업 목록입니다. 분명히 Task는 데이터베이스의 또 다른 엔터티이지만 아직 이에 대해 많이 알지 못하므로 이전에 Employee에서 했던 것처럼 가장 기본적인 엔터티를 생성하겠습니다. 지금 우리가 가정할 수 있는 유일한 것은 한 직원에게 두 개 이상의 작업이 할당될 수 있다는 것입니다. 따라서 일대다 비율인 일대다 접근 방식을 적용하겠습니다. 보다 구체적으로, 직원 테이블의 한 레코드는 작업 테이블 의 여러 레코드에 해당할 수 있습니다 . 또한 한 직원을 다른 직원과 명확하게 구분할 수 있도록 고유 번호 필드와 같은 것을 추가하기로 결정했습니다. 현재 Employee 클래스는 다음과 같습니다:
@Entity
@Data
@Accessors(chain = true)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    Long id;

    @NotBlank
    @Column(unique = true)
    private String uniqueNumber;
    private String firstName;
    private String lastName;
    private String department;
    private Double salary;
    private LocalDate hired;

    @OneToMany
    @JoinColumn(name = "employee_id")
    List<Task> tasks = new ArrayList<>();
}
Task 엔터티에 대해 다음 클래스가 생성되었습니다.
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
앞서 말했듯이 Task에는 새로운 내용이 표시되지 않으며 이 클래스에 대해 Employee 저장소의 복사본인 새 저장소도 생성되었습니다. 제공하지는 않겠습니다. 비유를 통해 직접 만들 수 있습니다. 그러나 Employee 클래스에 대해 이야기하는 것은 의미가 있습니다. 앞서 말했듯이 여러 필드를 추가했지만 지금은 마지막 필드인 작업에만 관심이 있습니다. 이는 List<Task> 작업 이며, 빈 ArrayList로 즉시 초기화되고 여러 주석으로 표시됩니다. 1. @OneToMany 앞서 말했듯이 이것이 작업 대 직원의 비율이 될 것입니다. 2. @JoinColumn - 엔터티가 조인되는 열입니다. 이 경우 직원의 ID를 가리키는 Employee_id 열이 작업 테이블에 생성되어 ForeighnKey 역할을 하게 됩니다. 이름이 신성해 보이지만 원하는 대로 열 이름을 지정할 수 있습니다. ID뿐만 아니라 일종의 실제 열을 사용해야 하는 경우 상황은 좀 더 복잡해지며 이 주제는 나중에 다루겠습니다. 3. ID 위에 @JsonIgnore라는 새로운 주석이 있음을 발견했을 수도 있습니다. id는 내부 엔터티이므로 반드시 클라이언트에 반환할 필요는 없습니다. 4. @NotBlank는 유효성 검사를 위한 특수 주석으로, 필드가 null이거나 빈 문자열이 아니어야 함을 나타냅니다. 5. @Column(unique = true)은 이 열이 고유한 값을 가져야 함을 나타냅니다. 따라서 우리에게는 이미 두 개의 엔터티가 있으며 서로 연결되어 있습니다. 이제 이를 프로그램에 통합할 때가 왔습니다. 서비스와 컨트롤러를 다루겠습니다. 우선, getAllEmployees() 메서드에서 스텁을 제거하고 이를 실제로 작동하는 것으로 바꿔 보겠습니다.
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
따라서 우리 저장소는 데이터베이스에서 사용 가능한 모든 것을 긁어서 우리에게 제공합니다. 작업 목록도 선택한다는 점은 주목할 만합니다. 하지만 긁어내는 것은 분명 흥미롭지만 아무것도 없다면 긁어내는 것은 무엇입니까? 맞습니다. 즉, 거기에 무엇인가를 어떻게 넣을지 알아내야 한다는 뜻입니다. 우선 컨트롤러에 새로운 메서드를 작성해 보겠습니다.
@PostMapping("${application.endpoint.employee}")
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
이것은 @PostMapping입니다. 직원 엔드포인트로 들어오는 POST 요청을 처리합니다. 일반적으로 이 컨트롤러에 대한 모든 요청은 하나의 엔드포인트로 전달되므로 이를 조금 단순화하자고 생각했습니다. application.yml의 멋진 설정을 기억하시나요? 문제를 해결해 봅시다. 이제 애플리케이션 섹션이 다음과 같이 보이도록 합니다.
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
이것이 우리에게 무엇을 주는가? 컨트롤러에서 각 특정 메서드에 대한 매핑을 제거할 수 있고 엔드포인트는 @RequestMapping("${application.endpoint.employee}") 주석 의 클래스 수준에서 설정된다는 사실입니다 . 이것이 바로 지금의 장점입니다. 우리 컨트롤러:
@RestController
@RequestMapping("${application.endpoint.employee}")
@RequiredArgsConstructor
public class EmployeeController {

    private final EmployeeService employeeService;

    @GetMapping
    public ResponseEntity<List<Employee>> getEmployees() {
        return ResponseEntity.ok().body(employeeService.getAllEmployees());
    }

    @PostMapping
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}
그러나 계속 진행합시다. createOrUpdateEmployee 메소드에서는 정확히 어떤 일이 발생하나요? 분명히 우리 EmployeeService에는 모든 저장 작업을 담당하는 저장 메소드가 있습니다. 또한 이 메서드는 설명이 필요 없는 이름으로 예외를 발생시킬 수 있다는 것도 분명합니다. 저것들. 일종의 검증이 수행되고 있습니다. 그리고 대답은 무엇이 잘못되었는지 목록이 포함된 201 Created인지 400 badRequest인지 여부와 같은 검증 결과에 직접적으로 의존합니다. 앞으로 이것이 우리의 새로운 검증 서비스입니다. 들어오는 데이터에 필수 필드가 있는지 확인하고(@NotBlank 기억하시나요?) 그러한 정보가 우리에게 적합한지 여부를 결정합니다. 저장 방법으로 넘어가기 전에 이 서비스를 검증하고 구현해 보겠습니다. 이를 위해 우리 서비스를 넣을 별도의 검증 패키지를 만들 것을 제안합니다.
import com.example.demo.exception.ValidationException;
import com.example.demo.persistence.entity.Employee;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

@Service
@RequiredArgsConstructor
public class ValidationService {

    private final Validator validator;

    public boolean isValidEmployee(Employee employee) throws ValidationException {
        Set<Constraintviolation<Employee>> constraintViolations = validator.validate(employee);

        if (CollectionUtils.isNotEmpty(constraintViolations)) {
            throw new ValidationException(buildViolationsList(constraintViolations));
        }
        return true;
    }

    private <T> List<Violation> buildViolationsList(Set<Constraintviolation<T>> constraintViolations) {
        return constraintViolations.stream()
                                   .map(violation -> new Violation(
                                                   violation.getPropertyPath().toString(),
                                                   violation.getMessage()
                                           )
                                   )
                                   .toList();
    }
}
클래스가 너무 큰 것으로 판명되었지만 당황하지 마십시오. 지금 알아낼 것입니다 :) 여기서는 이미 만들어진 유효성 검사 라이브러리 javax.validation 의 도구를 사용합니다 . 이 라이브러리는 우리가 만든 새로운 종속성에서 나왔습니다. build.graddle 구현에 추가됨 'org.springframework.boot:spring-boot-starter -validation' 우리의 오랜 친구 Service 및RequireArgsConstructor 이미 이 클래스에 대해 알아야 할 모든 것을 알려주었고 비공개 최종 유효성 검사기 필드도 있습니다. 그는 마술을 할 것입니다. 우리는 Employee 엔터티를 전달할 수 있는 isValidEmployee 메서드를 만들었습니다. 이 메서드는 나중에 작성하게 될 ValidationException을 발생시킵니다. 예, 이는 우리 요구 사항에 대한 사용자 정의 예외입니다. validator.validate(employee) 메소드를 사용하면 ConstraintViolation 객체 목록을 얻을 수 있습니다. 이는 이전에 추가한 유효성 검사 주석과의 모든 불일치입니다. 추가 논리는 간단합니다. 이 목록이 비어 있지 않으면(즉, 위반이 있는 경우) 예외를 발생시키고 위반 목록을 작성합니다. - buildViolationsList 메서드 이 메서드는 일반 메서드입니다. 다양한 개체의 위반 목록을 작업할 수 있습니다. 나중에 다른 항목을 검증하면 유용할 수 있습니다. 이 방법은 실제로 무엇을 합니까? 스트림 API를 사용하여 위반 목록을 살펴봅니다. map 메소드의 각 위반을 자체 위반 객체로 변환하고 결과 객체를 모두 목록으로 수집합니다. 우리는 그를 돌려보내고 있습니다. 우리 자신의 위반 대상은 또 무엇입니까? 간단한 기록은 이렇습니다
public record Violation(String property, String message) {}
레코드는 논리나 다른 것이 없이 데이터가 있는 객체가 필요한 경우 Java의 특별한 혁신입니다. 나 자신도 왜 이런 일이 일어났는지 아직 이해하지 못하지만 때로는 꽤 편리한 일입니다. 일반 클래스처럼 별도의 파일로 생성해야 합니다. 사용자 정의 ValidationException으로 돌아가면 다음과 같습니다.
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
이는 모든 위반 목록을 저장하며, Lombok 주석 - Getter가 목록에 첨부되고 또 다른 Lombok 주석을 통해 필수 생성자를 "구현"했습니다. 여기서는 isValid의 동작을 올바르게 구현하지 않았다는 점에 주목할 가치가 있습니다. ... 메서드를 사용하면 true 또는 예외를 반환하지만 일반적인 False로 제한하는 것이 좋습니다. 이 오류를 클라이언트에 반환하고 싶기 때문에 예외 접근 방식을 사용합니다. 즉, 부울 메서드에서 true 또는 false 이외의 항목을 반환해야 합니다. 순수 내부 유효성 검사 방법의 경우 예외를 발생시킬 필요가 없습니다. 여기에는 로깅이 필요합니다. 그러나 EmployeeService로 돌아가서 여전히 객체 저장을 시작해야 합니다. 이제 이 클래스가 어떻게 보이는지 살펴보겠습니다.
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final ValidationService validationService;

    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Transactional
    public void save(Employee employee) throws ValidationException {
        if (validationService.isValidEmployee(employee)) {
            Employee existingEmployee = employeeRepository.findByUniqueNumber(employee.getUniqueNumber());
            if (existingEmployee == null) {
                employeeRepository.save(employee);
            } else {
                existingEmployee = updateFields(existingEmployee, employee);
                employeeRepository.save(existingEmployee);
            }
        }
    }

    private Employee updateFields(Employee existingEmployee, Employee updatedEmployee) {
        return existingEmployee.setDepartment(updatedEmployee.getDepartment())
                               .setSalary(updatedEmployee.getSalary())
            				 //TODO implement tasks merging instead of replacement
                               .setTasks(updatedEmployee.getTasks());
    }
}
새로운 최종 속성인 private final ValidationService ValidationService를 확인하세요. save 메소드 자체에는 @Transactional 주석이 표시되어 RuntimeException이 수신되면 변경 사항이 롤백됩니다. 먼저 방금 작성한 서비스를 사용하여 수신 데이터의 유효성을 검사합니다. 모든 일이 순조롭게 진행되면 데이터베이스에 기존 직원이 있는지 확인합니다(고유 번호를 사용하여). 그렇지 않은 경우 새 항목을 저장하고, 있는 경우 클래스의 필드를 업데이트합니다. 아, 그렇군요. 실제로 어떻게 확인하나요? 예, 매우 간단합니다. Employee 저장소에 새로운 메서드를 추가했습니다.
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
주목할만한 점은 무엇입니까? 여기서는 사용할 수 있는 논리나 SQL 쿼리를 작성하지 않았습니다. Spring은 단순히 메소드 이름을 읽는 것만으로 내가 원하는 것을 결정합니다. ByUniqueNumber를 찾아 해당 문자열을 메소드에 전달합니다. 필드 업데이트로 돌아가서 여기서는 상식을 사용하고 부서, 급여 및 작업만 업데이트하기로 결정했습니다. 이름을 변경하는 것은 허용되는 일이지만 여전히 일반적이지 않기 때문입니다. 그리고 채용 날짜 변경은 일반적으로 논란의 여지가 있는 문제입니다. 여기서 무엇을 하면 좋을까요? 작업 목록을 결합하지만 아직 작업이 없고 이를 구별하는 방법을 모르기 때문에 TODO를 종료하겠습니다. 프랑켄슈타인을 발사해 봅시다. 설명하는 것을 잊지 않았다면 작동할 것입니다. 하지만 먼저 얻은 클래스 트리는 다음과 같습니다. 수정된 REST API 및 데이터 유효성 검사 - 1 클래스는 파란색으로 강조 표시되고, 새 클래스는 녹색으로 강조 표시되며, 작업하면 이러한 표시를 얻을 수 있습니다. git 저장소가 있지만 git은 우리 기사의 주제가 아니므로 이에 대해서는 다루지 않겠습니다. 따라서 현재 우리는 GET 및 POST 두 가지 방법을 지원하는 하나의 엔드포인트를 보유하고 있습니다. 그런데 엔드포인트에 대한 몇 가지 흥미로운 정보가 있습니다. 예를 들어 getAllEmployees 또는 createEmployees와 같이 GET 및 POST에 별도의 엔드포인트를 할당하지 않은 이유는 무엇입니까? 모든 것이 매우 간단합니다. 모든 요청에 ​​대해 단일 지점을 갖는 것이 훨씬 더 편리합니다. 라우팅은 HTTP 메서드를 기반으로 발생하며 직관적이므로 getAllEmployees, getEmployeeByName, get... update... create... delete...의 모든 변형을 기억할 필요가 없습니다. 얻은 결과를 테스트해 보겠습니다. 이전 기사에서 이미 Postman이 필요하다고 썼으며 이제 설치할 시간입니다. 프로그램 인터페이스에서 새로운 POST 요청을 생성 REST API 및 데이터 유효성 검사 - 2 하고 전송을 시도합니다. 모든 것이 잘 진행되었다면 화면 오른쪽에 Status 201이 표시될 것입니다. 그러나 예를 들어 고유 번호(검증이 있는) 없이 동일한 항목을 보낸 경우 다른 대답을 얻습니다. REST API 및 데이터 유효성 검사 - 3 전체 선택이 어떻게 작동하는지 확인해 보겠습니다. 동일한 엔드포인트에 대해 GET 메서드를 생성하여 보냅니다. . REST API 및 데이터 유효성 검사 - 4 저처럼 모든 일이 여러분에게도 잘 풀렸기를 진심으로 바랍니다. 다음 에서 뵙겠습니다 .
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION