Spring Bean, Validation

star_pooh·2024년 12월 17일
0

TIL

목록 보기
35/39
post-thumbnail

Spring Bean

Spring Bean 등록(1)

  • @ComponentScan(자동 등록)
    • 특정 패키지 내에서 @Component 어노테이션이 붙은 클래스를 자동으로 찾아서 Bean으로 등록
      • @Service, @Repository, @Controller 같은 어노테이션도 @Component가 포함되어 있음
    • 스캐닝 범위는 주로 어플리케이션의 루트(최상위) 패키지
      • basePackages 옵션으로 스캔할 패키지 선택 가능
    • @SpringBootApplication에 포함되어 있음
      • 스프링 프로젝트를 생성하면 메인 메소드가 있는 클래스 상단에 @SpringBootApplication이 존재
  • @Configuration & @Bean(수동 등록)
    • @Configuration이 있는 클래스를 Bean으로 등록하고 해당 클래스 파싱
    • @Bean이 있는 메소드를 찾아 Bean을 생성하며, 해당 메소드명이 Bean의 이름이 됨
@Configuration
public class AppConfig {
    @Bean
    public TestService testService() {
    	...
    }
}
  • Bean 충돌
    • 이름이 같은 Bean이 등록되려고 하는 경우, 충돌 발생
      • 자동 등록 vs 자동 등록에서 충돌이 발생하는 경우, ConflictingBeanDefinitionException가 발생
      • 수동 등록 vs 자동 등록에서 충돌이 발생하는 경우, 수동 등록이 자동 등록을 오버라이딩 해서 우선권을 가짐

의존관계 주입

  • 생성자 주입
    • 생성자를 통해 의존성을 주입하는 방법
    • 최초에 한 번 생성된 후 값이 수정되지 못함(불변, 필수)
    • 애플리케이션 실행과 동시에 의존성이 주입되기 때문에 시스템 안정성이 향상됨
    • 스프링 팀에서 권장하는 방법
@Component
public class Car {
    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
       ...
    }
}

💡 @RequiredArgsConstructor

  • 생성자 주입 방식에서 반복되는 코드를 편하게 작성하기 위해 Lombok에서 제공되는 어노테이션
  • final 필드를 모아서 생성자를 자동으로 만들어주는 역할
  • 컴파일 시점에 자동으로 생성자 코드를 생성
@Component
@RequiredArgsConstructor
public class MyApp {
	// 필드에 final 키워드 필수
    private final MyService myService;

    // Annotation Processor가 만들어 주는 코드
    // public MyApp(MyService myService) {
    //     this.myService = myService;
    // }

    public void run() {
        myService.doSomething();
    }
}
  • Setter 주입
    • Setter 메소드를 통해 의존성을 주입하는 방법
    • 선택하거나 변경 가능한 의존관계에 사용
    • 클래스 안에 의존성이 숨겨져 있어서 유지보수 어려움
    • 의존성이 변경될 가능성이 있기 때문에 시스템 안정성이 떨어짐
@Component
public class Car {
    private Engine engine;

    @Autowired // 세터 주입
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
       ...
    }
}
  • 필드 주입
    • 필드에 직접적으로 주입하는 방법
    • 코드는 간결하지만 스프링이 없으면 동작하지 않음
    • 클래스 안에 의존성이 숨겨져 있어서 유지보수 어려움
    • 의존성이 변경될 가능성이 있기 때문에 시스템 안정성이 떨어짐
@Component
public class Car {
    @Autowired // 필드 주입
    private Engine engine;

    public void start() {
       ...
    }
}

Spring Bean 등록(2)

같은 타입의 Bean이 충돌했을 경우

  • @Qualifier
    • Bean 등록시 추가 구분자를 붙여줌
@Component
@Qualifier("firstService")
public class MyServiceImplV1 implements MyService { ... }

@Component
@Qualifier("secondService")
public class MyServiceImplV2 implements MyService { ... }

@Component
public class ConflictApp {
	private MyService myService;

	// 생성자 주입에 구분자 추가
    @Autowired
    public ConflictApp(@Qualifier("firstService") MyService myService) {
    	this.myService = myService;
    }
}        
  • @Primary
    • 지정된 Bean이 우선 순위
@Component
public class MyServiceImplV1 implements MyService { ... }

@Component
@Primary
public class MyServiceImplV2 implements MyService { ... }

@Component
public class ConflictApp {
		private MyService myService;

		@Autowired
		public ConflictApp(MyService myService) {
				this.myService = myService;
		}
	...
}

💡 실제 적용 사례

  • Database가 (메인 MySQL, 보조 Oracle) 두 개 존재하는 경우
    • 기본적으로 MySQL을 사용할 때 @Primary를 사용
    • 필요할 때 @Qualifier로 Oracle을 사용
    • 동시에 사용되는 경우 @Qualifier 의 우선 순위가 높음

Validataion(검증)

Validation

특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계.

  • 검증의 역할
    • 검증을 통해 오류 발생 시 유저에게 적절한 메시지 제공
    • 검증 오류로 인한 비정상적인 동작 방지
    • 사용자가 입력한 데이터 유지
  • 검증의 종류
    • 프론트 검증
      • 유저가 조작할 수 있어서 보안에 취약
      • 하지만 유저 사용성을 위해 필요
        ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가
    • 서버 검증
      - 프론트 검증 없이 서버에서만 검증한다면 유저 사용성이 저하됨
      • API 명세서의 Response에 검증 오류를 적어야함
        - 그에 맞는 대응 가능
      • 서버 검증은 선택이 아닌 필수
    • 데이터베이스 검증
    • Not Null, Default와 같은 제약조건 설정
    • 최종 방어선 역할

BindingResult

Spring에서 기본적으로 제공되는 검증 오류를 보관하는 객체. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관.

  • BindResult
    • Errors 인터페이스를 상속받은 인터페이스
    • Errors 인터페이스는 에러의 저장과 조회 기능 제공
    • BindingResult는 addError() 와 같은 추가적인 기능 제공
    • BeanPropertyBindingResult는 Spring이 기본적으로 사용하는 구현체
@Data
public class MemberCreateRequestDto {
    private Long point;
    private String name;
    private Integer age;
}

@Controller
public class BingdingResultController {
    @PostMapping("/v1/member")
    public String createMemberV1(@ModelAttribute MemberCreateRequestDto request, Model model) {
        System.out.println("/V1/member API가 호출되었습니다.");
        model.addAttribute("point", request.getPoint());
        model.addAttribute("name", request.getName());
        model.addAttribute("age", request.getAge());
        return "complete";
    }
}
  • 파라미터에 BindingResult가 없을 때 잘못된 요청을 보낸 경우
    • 검증 오류(400 Bad Request)가 발생하고 Controller가 호출 되지 않음
@Controller
public class BindingResultController {
    @PostMapping("/v2/member")
    public String createMemberV2(
    	// @ModelAttribute 뒤에 BindingResult 위치
    	@ModelAttribute MemberCreateRequestDto request,
    	BindingResult bindingResult,
    	Model model) {
        System.out.println("/V2/member API가 호출되었습니다.");

		// BindingResult의 에러 출력
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        System.out.println("allErrors = " + allErrors);

        model.addAttribute("point", request.getPoint());
        model.addAttribute("name", request.getName());
        model.addAttribute("age", request.getAge());
        return "complete";
    }
}
  • 파라미터에 BindingResult가 있을 때 잘못된 요청을 보낸 경우
    • BindingResult는 검증 대상 파라미터 뒤에 위치해야 함
    • @ModelAttrbute 필드 또는 객체에 파라미터 바인딩 오류 발생
      • BindingResult에 오류가 보관되고 Controller가 호출됨
      • @ModelAttribute는 파라미터를 필드 하나하나에 바인딩하기 때문에 어떤 필드에 오류가 발생할 경우, 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출됨

Bean Validation

  • 파라미터에 대한 검증을 Conroller에서 하게 되면, Controller가 너무 커지며, 단일 책임 원칙(SRP)를 위배
  • 객체의 필드나 메소드에 제약 조건을 설정하여, 올바른 값인지 검증하는 표준화된 방법
    • Bean Validation은 기술 표준 인터페이스
    • 다양한 어노테이션과 인터페이스로 구성
@Getter
public class SignUpRequestDto {
	@NotBlank
	private String name;

	@NotNull
	@Range(min = 1, max = 120)
	private Integer age;
}

어노테이션 정보(공식문서)

Validator

어노테이션만 선언해도 검증이 완료되는 이유는 Validator가 존재하기 때문인데, validation 라이브러리를 설정하면 'org.springframework.boot:spring-boot-starter-validation' 자동으로 Bean Validator를 Spring에 통합되도록 설정해줌.

  • 동작 순서
    • Spring Boot Application 실행 시 자동으로 Bean Validator 통합
      • LocalValidatorFactoryBean 을 Global Validator로 등록
      • Global Validator가 Default로 적용되어 있으니 @Valid, @Validated 적용
      • Bean Validation Annotation(@NotNull, @NotBlank, @Max ...)에 대한 검증 수행
      • Validation Error가 발생하면 FieldError, ObjectError를 생성하여 BindingResult에 보관

💡 LocalValidatorFactoryBean의 클래스 다이어그램

💥 Global Validator를 수동으로 등록하면 LocalValidatorFactoryBean를 등록하지 않는다.

  • @Valid, @Validated 차이점
    • @Valid 는 Java 표준이고 @Validated 는 Spring에서 제공
    • @Validated를 통해 Group Validation 또는 Controller 이외 계층에서 Validation 가능
    • @ValidMethodArgumentNotValidException를 예외로 발생
    • @ValidatedConstraintViolationException를 예외로 발생

에러 메시지

Spring의 Bean Validation은 디폴트로 제공하는 메시지가 존재하며, 임의로 수정 가능

  • BindingResult에 등록된 검증 오류에는 어노테이션 이름으로 오류가 등록되어 있음
  • 어노테이션의 message 속성을 사용해서 메시지 수정 가능
@Data
public class TestDto {
	@NotNull(message = "메시지 수정 가능")
	private String stringField;
}
Field error in object 'testDto' on field 'integerField': rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [널이어서는 안됩니다]

👇 메시지 수정 후

Field error in object 'testDto' on field 'integerField': rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [메시지 수정 가능]

Bean Validtaion의 충돌

  • 요구사항
    • 상품
      • id (식별자)
      • name (이름)
      • price (가격)
      • count (재고)
    • 상품 등록 API
      • 식별자 값은 필수가 아니다.
      • name은 null, “”, “ “을 허용하지 않는다.
      • price는 10 ~ 10000 사이의 숫자로 생성한다.
      • count는 1 ~ 999 사이의 숫자로 생성한다.
    • 상품 수정 API
      • 식별자 값이 필수이다.
      • name은 null, “”, “ “을 허용하지 않는다.
      • price는 무제한으로 허용한다.
      • count는 1 ~ 999 사이의 숫자로 생성한다.
  • 등록과 수정API에 공통된 RequestDto를 사용할 수 없음
    • SaveRequestDto, UpdateRequestDto를 따로 사용(주로 사용하는 방법)
    • Bean Validation의 groups 기능 사용

groups

동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 활용.

// 저장용 group
public interface SaveCheck {
}

// 수정용 group
public interface UpdateCheck {
}

@Data
public class ProductRequestDtoV2 {
    // 저장, 수정 모두 적용
    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String name;

    // 사용하는 모든 곳에 적용
    @NotNull
    // 저장만 적용
    @Range(min = 10, max = 10000, groups = SaveCheck.class)
    private Integer price;

    @NotNull
    @Range(min = 1, max = 999)
    private Integer count;
}

@RestController
public class ProductController {
    @PostMapping("/v2/product")
    public String save(
            // 저장 속성값 설정
            @Validated(SaveCheck.class) @ModelAttribute ProductRequestDtoV2 requestDtoV2) {
        return "상품 생성이 완료되었습니다";
    }

    @PutMapping("/v2/product/{id}")
    public String update(
            @PathVariable Long id,
            // 수정 속성값 설정
            @Validated(UpdateCheck.class) @ModelAttribute ProductRequestDto test) {
        return "상품 수정이 완료되었습니다.";
    }
}
  • groups 속성을 사용하면 각각 다르게 검증 적용 가능
    • 가독성이 떨어지고 코드 복잡도 증가
    • @Validated만 지원

@RequestBody

@Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용 가능. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용.

@Data
public class ExampleRequestDto {
	@NotBlank
	private String field1;

	@NotNull
	@Range(min = 1, max = 150)
	private Integer field2;
}

@Slf4j
@RestController
public class RequestBodyController {
    @PostMapping("/example")
    public Object save(
    	@Validated @RequestBody ExampleRequestDto dto, BindingResult bindingResult) {
        log.info("RequestBody Controller 호출");

        if(bindingResult.hasErrors()) {
            log.info("validation errors={}", bindingResult);
            // Field, Object Error 모두 Json으로 반환
            return bindingResult.getAllErrors();
        }
        return dto;
    }
}
  • RestAPI 요청에 따른 결과
    • 성공 요청
      • Controller 정상 호출
      • 응답 반환
    • 실패 요청 : Json 객체를 변환하는 것 자체가 실패
      • Controller 호출 되지 않음
      • 반드시 Json → 객체 변환이 되어야 검증 진행
    • 검증 오류 요청 : Json 객체로 변환하는 것은 성공했지만, 검증에서 실패
      • bindingResult.getAllErrors()가 MessageConverter에 의해 Json으로 변환되어 반환
      • Controller 호출
      • log로 작성한 bindingResult 에러가 콘솔에 출력

0개의 댓글

관련 채용 정보