int price에 @NotNull을 사용할 때, price가 null이어도 동작한다고?

2023. 4. 27. 17:43자바/스프링 삽질 기록

728x90
반응형

이번 웹 장바구니 미션을 진행하면서 Validation과 같은 어노테이션을 한번 적용해봤다.

 

Validation

Validiation은 어노테이션을 기반으로 어떤 변수의 값을 제한할 수 있도록 돕는다. 이번 미션을 진행하면서는 필드 위 쪽에 Validation 관련 어노테이션을 붙여서 사용했지만 메서드 인자 앞에 어노테이션을 붙여서 사용하거나 리턴값에서도 사용할 수 있다. 

 

baeldung에서는 Validation 관련 어노테이션에 대해 아래와 같이 설명하고 있다.

@NotNull validates that the annotated property value is not null.
@AssertTrue validates that the annotated property value is true.
@Size validates that the annotated property value has a size between the attributes min and max; can be applied to String, Collection, Map, and array properties.
@Min validates that the annotated property has a value no smaller than the value attribute.
@Max validates that the annotated property has a value no larger than the value attribute.
@Email validates that the annotated property is a valid email address.
@NotEmpty validates that the property is not null or empty; can be applied to String, Collection, Map or Array values.
@NotBlank can be applied only to text values and validates that the property is not null or whitespace.
@Positive and @PositiveOrZero apply to numeric values and validate that they are strictly positive, or positive including 0.
@Negative and @NegativeOrZero apply to numeric values and validate that they are strictly negative, or negative including 0.
@Past and @PastOrPresent validate that a date value is in the past or the past including the present; can be applied to date types including those added in Java 8.
@Future and @FutureOrPresent validate that a date value is in the future, or in the future including the present.

 

뭐 사실 어노테이션 이름만 보더라도 직관적으로 이해가 되기 때문에 사용하는 데에는 큰 어려움이 없다.

 

Validation 사용 방법

 

 

 

먼저 위와 같이 spring-boot-starter-validation 의존성을 추가해준다.

 

그리고 아까 이야기했던 것처럼 @NotNull, @NotBlack, @Min 등등 필드에 대해서 어노테이션을 사용할 수 있다. message는 validation에서 걸려서 예외가 터졌을 때 전달되는 메세지를 의미한다. @Min, @Max, @Size와 같은 값들은 내부에 value나 max, min 등의 값을 가지고 있는데, 이는 기준값을 설정해주는 거라고 생각하면 된다. 이를테면 @Size(max = 30)이라고 작성하면 길이는 최대 30자로 제한할 수 있다. 

 

이제 위의 객체를 사용할 때 @Valid라는 어노테이션을 붙인다면 각 필드별로 작성된 Validation 어노테이션을 사용해 검증하겠다는 것을 의미한다. @Valid를 사용하지 않는 경우에는 Validation이 적용되지 않는다. 

 

만약에 조금 복잡한 검증을 하고 싶으면?

 

 

위와 같이 클래스의 필드에 Validation 어노테이션을 사용해서 검증을 한다면 해당 객체를 유연성있게 다루기 어렵다는 단점이 발생한다. 모든 예외 검증에 대해서 검증을 하거나, 검증을 하지 않거나 하는 방법 밖에 없기 때문에 모든 검증을 일괄적으로 적용한다고 생각하면 된다. 

 

예를 들어 다음과 같은 제한 조건이 있다고 가정해보자.

 

나이가 20세 미만인 사람들은 10000원까지만 현질할 수 있다.

나이가 20세 이상인 사람들은 30000원까지만 현질할 수 있다.

 

 

위의 경우에는 나이별로 현질할 수 있는 금액의 제한이 있는데, 이러한 경우에는 위에서 제공해준 Validation 어노테이션만으로는 처리할 수 있다. 따라서  ConstraintsValidator를 구현한 Validator를 따로 만들어준 뒤에 이를 사용하는 식으로 검증해야한다. 

 

ConstraintsValidator는 제한조건을 정의할 때 구현해야하는 인터페이스라고 생각하면 된다.

이후 구현한 클래스에 대해서 @Constraint 어노테이션에서 붙여서 사용하면 된다.

그러면 해당 어노테이션을 CustomValidator로 활용할 수 있다.

 

지금으로선 조금 복잡하니, 나중에 필요할 때 다시 찾아봐야겠다.

 

 

그래서 문제가 뭐였지...??

 

자, 이게 내가 작성했던 코드이다. 여기서 price에 한번 집중해보자 price는 NotNull 어노테이션과 Min, Max 어노테이션으로 값을 지정했다.

 

즉, price에는 0원 이상 100만원 이하의 값이 반드시 들어와야한다.

 

그리고 이는 Controller의 addItem 메서드에서 사용된다. 

 

@Valid 인자를 사용하고 있어서 ItemRequest 객체는 검증을 올바르게 할 것이다.

 

자, 이제 post 요청을 보내보자.

 

?????????????

 

 

분명 예외가 발생해야하는데 왜 정상적으로 동작했지??

 

자... 이 부분이 페어와 미션을 하면서 발생했던 문제였다. 

 

이럴 때는 구글링을...! 해봐도 좋지만 뭐... 공부하려고 하는거니까 직접 메서드를 파보도록 하겠다.

 

도대체 문제가 뭐야?

 

먼저 ItemRequest 생성자 부분에 break point를 걸고 한번 디버깅을 해봤다.

자... 뭔가 복잡한 무언가가 떴는데... 아래 쪽에 익숙한 무언가가 있는 것을 확인할 수 있다.

 

 

ObjectMapper 뭐시기... DefaultDeserializationContext 뭐시기... BeanDeserializer 뭐시기...

 

이전 포스팅으로 작성했던 @RequestBody에서 json으로 전달된 객체를 binding할 때 위의 과정을 거친다고 설명한 적이 있었다.

 

2023.04.18 - [자바/스프링 삽질 기록] - Spring에서 @RequestBody로 전달받는 객체의 필드에 final 키워드를 붙일 수 있을까?

 

그런데 ItemRequest로 바인딩 되기 이전에 ObjectMapper로 인해 값이 다 설정되는 것을 확인할 수 있다.

 

여기서는 price가 0이라는 값으로 매핑이 되어있는 것을 볼 수 있다. 

위의 과정에서 price에 값이 없었는데, 0이 들어간 것을 확인할 수 있다. 제대로 키 값이 전달되지 않는 int인 경우에는 0의 값이 들어간 것을 알 수 있다. 즉, 데이터를 바인딩하는 과정에서 키 값이 전달되지 않는 경우에는 int는 0으로 매핑되는 것을 알 수 있다. (아마 int에 null을 넣을 수 없기 때문에 0으로 매핑되는 것 같다. 어쨌든 생성자에 값을 전달해야하니까 말이다.)

 

참고로 위의 예시에선 imageUrl이 null 값으로 들어가있다. 이는 json으로 값을 전달받을 때 imageUrl로 전달된 값이 없기 때문이다. jsonProperty를 사용해서 image-url을 imageUrl로 프로퍼티를 설정해줬는데, 처음 바인딩할 때에는 image-url을 기준으로 키 값과 매핑되는 컬럼을 찾다보니 imageUrl이 null 값으로 설정됐다. 이후에 생성자에서 값을 추가할 때 JsonProperty 값을 보고 json의 키 값에 해당하는 값을 가져와서 올바르게 역직렬화가 된다.

 

위에서 볼 수 있듯이 json을 객체로 역직렬화하는 과정에서 어떤 필드의 값을 int로 설정하는 경우에는 null 값이 들어갈 수 없어 0이라는 값이 대신 들어가게된다. 결국 객체를 생성하면서 검증(@Valid)을 하기 이전에 int는 0이라는 값으로 매핑되어서 price에는 0이라는 값이 들어가게 된다.

 

따라서 검증을 실패하지 않게 된다.

 

그러면 어떻게 해야해?

 

이 문제를 해결하려면 price의 데이터 타입을 Integer로 바꾸면 된다. 

 

그러면...!

price에도 null이라는 값이 들어가기 때문에 this.price에는 null이 들어가게되고 검증에서 걸러지는 것을 확인할 수 있다.

 

짜잔~~

 

결론

 

@RequestBody로 @Valid를 사용하는 객체를 역직렬화하는 경우에는 기본 타입이 아니라

wrapper 타입을 사용하자! 

728x90
반응형