- @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() { ... } }
같은 타입의 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
의 우선 순위가 높음
특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계.
- 검증의 역할
- 검증을 통해 오류 발생 시 유저에게 적절한 메시지 제공
- 검증 오류로 인한 비정상적인 동작 방지
- 사용자가 입력한 데이터 유지
- 검증의 종류
- 프론트 검증
- 유저가 조작할 수 있어서 보안에 취약
- 하지만 유저 사용성을 위해 필요
ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가- 서버 검증
- 프론트 검증 없이 서버에서만 검증한다면 유저 사용성이 저하됨
- API 명세서의 Response에 검증 오류를 적어야함
- 그에 맞는 대응 가능- 서버 검증은 선택이 아닌 필수
- 데이터베이스 검증
- Not Null, Default와 같은 제약조건 설정
- 최종 방어선 역할
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가 호출됨
- 파라미터에 대한 검증을 Conroller에서 하게 되면, Controller가 너무 커지며, 단일 책임 원칙(SRP)를 위배
- 객체의 필드나 메소드에 제약 조건을 설정하여, 올바른 값인지 검증하는 표준화된 방법
- Bean Validation은 기술 표준 인터페이스
- 다양한 어노테이션과 인터페이스로 구성
- Bean Validation 구현체인 Hibernate Validator 사용
@Getter public class SignUpRequestDto { @NotBlank private String name; @NotNull @Range(min = 1, max = 120) private Integer age; }
어노테이션만 선언해도 검증이 완료되는 이유는 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 가능@Valid
는MethodArgumentNotValidException
를 예외로 발생@Validated
는ConstraintViolationException
를 예외로 발생
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 [메시지 수정 가능]
- 요구사항
- 상품
- 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 기능 사용
동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 활용.
// 저장용 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
만 지원
@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 에러가 콘솔에 출력