스토리 개발에 특정한 단어를 사용자가 작성했을 때 response를 던져야했다.
여러 변수들에 적용되어 로직이 재사용 되기 때문에
Custom Annotation을 만들면 좋겠다고 생각했고
처음 해보는 작업이기 때문에 만드는 과정에서 공부한 내용과
깨달은 점에 대해 기록을 하려고 한다.
제목에 모든 걸 담고 싶은 나머지 너무 못생긴 제목이 되었지만 ..
그리고 공부하다보니 진작에 기본으로 알고 있어야 했던 지식이라서
머쓱하지만 나와 같은 사람들이 많기도 할 것 같아 지식 공유 ..
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = CustomAnnotationValidator.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotationTest {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @Target
: 자바의 메타 어노테이션 중 하나. java.lang.annotation 패키지에 속해있음.
: 다른 어노테이션을 적용할 수 있는 자바 요소 (클래스, 메소드, 필드 등)를 지정하는 데 사용된다.
: ElementType 열거형을 매개변수로 사용하여 어노테이션의 적용 대상을 제한한다.
- @Constraint
: Jakarta Bean Validation API의 일부로, 사용자 정의 검증 어노테이션을 만들 때 사용된다.
: 이 어노테이션은 '검증 로직' 을 수행할 ConstraintValidator 구현 클래스를 '지정' 하는 데 사용된다.
: 즉, 해당 어노테이션을 사용할 때 어떤 걸 검증할거야 ! 라는 의미.
- @Retention(RetentionPolicy.RUNTIME)
: 런타임 동안 이 애노테이션이 유지된다는 뜻.
@RequiredArgsConstructor
public class CustomAnnotationValidator
implements ConstraintValidator<ForbiddenWordsCheckField, String> {
private final ForbiddenWordService forbiddenWordService;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
List<String> wordList = TestService.메서드();
return wordList.stream().noneMatch(value::contains);
}
}
커스텀 어노테이션에서 실제로 검증 로직을 수행하는 클래스이다.
ConstraintValidator 인터페이스의 메서드로, 검증 로직을 구현해야 한다.
그때 사용되는 게 isValid 메서드
(필수적으로 구현해야 한다. 여기에 검증로직을 작성하는 것!)
initialize
: 어노테이션의 속성 값을 초기화하는데 사용
: 해당 메서드도 제공하지만, 필수는 아니다.
isValid
: 검증할 value와 검증 과정에서 사용할 수 있는 컨텍스트 정보를 제공하는 ConstraintValidatorContext 객체를 파라미터로 받아야 한다.
: 결과 값이 true면 검증 통과! 아니면 검증 실패! -> ConstraintViolationException 자동 발생.
만드는 과정은 꽤나 간단해보인다.
(물론 나는 몇 번 코드를 갈아 엎긴 했지만)
(며칠은 걸렸었는데,, 결과만 놓고 보니 ㅋ ,, 씁-슬)
그리고 밥먹듯 사용했던 @NotNull, NotEmpty .. 등 메타 어노테이션 모두
같은 방식으로 동작된다. <- 이제 알았음. 쩝.
이걸 만들면서 추가적으로 공부하게 된 것들이 있어 킵고잉하겠다.
어노테이션을 메서드에 적용해야하고 실행 전 후로 특정 로직을 수행해야한다면
Aspect 클래스를 구현해야 한다.
package com.example.aspect;
import com.example.exception.ValidationException;
import com.example.model.User;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class ValidationAspect {
@Around("@annotation(com.example.aspect.ValidateUser)")
public Object validateUser(ProceedingJoinPoint joinPoint) throws Throwable {
// MethodSignature를 사용해 메서드 정보 얻기.
MethodSignature signature = (MethodSignature) joinPoint.getSignature()
Method method = signature.getMethod();
ValidateUser validateUser = method.getAnnotation(ValidateUser.class);
// 파라미터 값들을 확인
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof User) {
User user = (User) arg;
validate(user);
}
}
return joinPoint.proceed();
}
private void validate(User user) {
if (user.getName() == null || user.getName().isEmpty()) {
throw new ValidationException("User name cannot be empty");
}
}
}
- @Aspect
: 이 클래스가 AOP Aspect임을 나타냄.
- @Around("@annotation(com.example.aspect.ValidateUser)")
: ValidateUser 애노테이션이 붙은 메서드 주위를 감쌈.
- ProceedingJoinPoint
: 호출되는 메서드에 대한 정보와 제어를 제공한다.
- MethodSignature
: Spring AOP에서 메서드에 대한 메타데이터를 제공하는 인터페이스
: AOP Aspect 내에서 메서드의 구체적인 정보를 쉽게 가져올 수 있다.
: 메서드의 반환 타입, 파라미터 타입, 메서드 이름 등을 얻을 수 있는 거임.
joinPoint.getSignature()
: 메서드를 호출하면 Signature 타입의 객체를 반환
: 이 객체를 MethodSignature로 캐스팅하면, 메서드와 관련된 더 많은 정보를 얻을 수 있다.
MethodSignature의 주요 메서드
getMethod()
: 현재 실행 중인 메서드를 반환
getReturnType()
: 메서드의 반환 타입을 반환
getParameterTypes()
: 메서드의 파라미터 타입 배열을 반환
getParameterNames()
: 메서드의 파라미터 이름 배열을 반환
- validate
: 사용자 객체를 검증하는 메서드.
: 예시 코드에선 이름이 비어 있는지 확인하는 거임.
isValid가 false를 반환하는 경우가 검증 실패라고 했다.
검증 실패시, 검증 프로세스가 ConstraintViolation 객체를 생성한다.
여기엔 검증 실패에 대한 '세부 정보'를 포함하고 있다.
어떤 값이 유효하지 않았는지, 어떤 메시지가 반환되어야 하는지 ...
앞서 살펴봤던 커스텀 어노테이션 구현 클래스에서
message() 메서드가 있었는데 이게 ConstraintViolation에 설정된다. 그게 response 객체에 담겨서 와야하는데 ..
특정 경우 Error response를 받지 못했었다.
문제 원인
해결방안
처음에 개발했던 버전으로는 Aspect 구현 클래스에서
검증 메서드의 파라미터에 검증 필드들을 리스트에 담아 특정 로직을 수행하게 하였다.
해당 로직에 대해 캐싱을 했던 상황.
신나게 개발이 끝났다고 MR 을 올렸고 SA님이 질문을 주셨다.
"이렇게 개발하면, 리스트가 달라질 때마다 캐시가 남을 것 같은데 고려해보셨나요?"
.. 아니요
솔직히 나는 지금까지 개발하면서 캐시가 어쩌구 성능이 어쩌구
이야기만 많이 들었지 깊게 고민을 해보지도 알아보지도 않았었다. 쩝.
메서드 캐싱도 코드리뷰에 달려서 무작정 적용했던 것.
기존 내 개발 버전에선 메서드 캐싱을 해도 무의미 했던 것이었다. 🤢
그래서 알아보려고 한다.
현재는 파라미터 전달 없이 구현 클래스에서 검증 로직을 수행하도록 수정했지만,
만약 내가 기존 버전대로 개발했고 API 호출 시 검증 list가 달라질 때마다 캐시가 남고, 이렇게 개발이 되는 게 맞나 싶을 때는 ??
본인은 성능 테스트를 해본 적이 없음 푸하하.
그냥 스토리만 쳐냈었다. 스토리 개발을 진행하면서
필요할 땐 꼭 고려해야 하는 부분이다.
현재 프로젝트에선 redis에 캐싱을 하고 있어서
이를 성능테스트 해볼 수 있는 방법에 대해 알아보려고 한다.
[ 캐시 적용 / 미적용 / 사용자 수에 따른 성능 비교 ]
100명 동시 사용자가 API를 호출한다고 가정한 성능테스트 결과
100명 동시 사용자가 API를 호출한다고 가정한 성능테스트 결과
성능테스트를 해보며, 캐싱을 해야할 것 같은 메서드엔 캐싱을 적용하고
안해도 되는 메서드엔 캐싱 적용을 하지 않으면 된다.
(당연하다)
이번에도 어떤 글을 적을까 며칠을 고민했다.
최신 라이브러리를 소개할까? 했다가 소개하려는 라이브러리가 대중적이지도 않았고
현재 라이브러리 내부 이슈때문에 실제로 사용해볼 수도 없어서 어떤 글을 적을까 하다가
역시 프로젝트에서 새로 배운, 경험한 내용을 적는 게 맞다고 생각이 들었다.
팀 스프린트 할 때마다 솔직히 귀찮은 마음 반, 해야함을 아는 것 반.
근데, 할 때마다 글을 적으며 자기 반성이 엄청 된다.
누군가에겐 '뭐야 넘 당연하고 기초적인 내용이잖아 -_-' 라고 할 수도 있지만 ,,
나에겐 새롭기도 했고 앞으로 더 깊게 알아봐야 할 내용이기도 했기 때문에 ,,
기록을 남긴다.
앞으로 더더 화이팅.
문제 원인, 해결 방안이 뭐였는지..?;;