JAVA-Validator 사용시 유의사항
도메인 클래스 생성 시 도메인 규칙 검증
신규 프로젝트를 진행 하면서 도메인 객체 생성 시 생성 규칙이 필요함을 느꼈고, javax.validation
api를 구현한 hibernate-validator
를 사용하여 생성자에서 규칙을 검증하기로 했다.
javax.validator.Validator 클래스를 수동으로 불러와 이용하기로 결정
검색 결과 간단히 이런 추상 클래스를 만들었고
public abstract class SelfValidator<T> {
private Validator validator;
public SelfValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validate() {
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
검증하고 싶은 클래스에서 SelfValidator<T>
를 상속 받은 후 생성자에서 validate() 메서드를 호출하는 방식으로 구현 하였고, 의도대로 동작 하였다.
@Getter
public class Person extends SelfValidator<Domain> {
@NotEmpty
private String name;
@Min(10)
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
this.validate();
}
}
라이브 오픈 전 문제점 발견
nGrinder를 사용하여 부하테스트를 하였는데, 생각보다 tps가 많이 나오지 않는 것이었다.
원인을 찾기 위해 코드 여기저기를 살펴보았지만 눈으로는 병목지점을 찾기가 너무 어려웠다. APM이라도 적용 되어있다면 좋겠지만 그렇지 않은 상태였고 이거를 위해 APM을 세팅하는것도(물론 장기적으로 보면 필요하지만) 번거롭고 시간이 오래 걸리는 일이었다.
분석 방법을 찾다
원인을 찾기 위해 다방면으로 알아보던 중 우아한형제들 기술블로그에서 성능, 부하 스트레스 테스트에 관한 글을 보았고 Thread Dump
를 분석 하여 병목지점을 찾아가는 방법을 보았다.
[root@ip-10-10-10-10 bin]# jps -v
14326 my-project-1.0-SNAPSHOT.jar -Dspring.profiles.active=stress -Dserver.port=8080
[root@ip-10-10-10-10 bin]# jstack 14326 > dump.log
원인을 찾았고
이렇게 덤프를 텍스트 파일로 떨구고 확인해보니 한 스레드가 락을 잡고있었고
"http-nio-8080-exec-600" #662 daemon prio=5 os_prio=0 cpu=356.60ms elapsed=38.67s tid=0x00007ff66c03f800 nid=0x15cf runnable [0x00007ff5f5322000]
java.lang.Thread.State: RUNNABLE
at org.springframework.boot.loader.jar.Handler.parseURL(Handler.java:228)
at java.net.URL.<init>(java.base@11.0.17/URL.java:674)
at java.net.URL.<init>(java.base@11.0.17/URL.java:541)
at jdk.internal.loader.URLClassPath$Loader.getResource(java.base@11.0.17/URLClassPath.java:635)
at jdk.internal.loader.URLClassPath.getResource(java.base@11.0.17/URLClassPath.java:315)
at java.net.URLClassLoader$1.run(java.base@11.0.17/URLClassLoader.java:455)
at java.net.URLClassLoader$1.run(java.base@11.0.17/URLClassLoader.java:452)
at java.security.AccessController.doPrivileged(java.base@11.0.17/Native Method)
at java.net.URLClassLoader.findClass(java.base@11.0.17/URLClassLoader.java:451)
at java.lang.ClassLoader.loadClass(java.base@11.0.17/ClassLoader.java:589)
- locked <0x0000000088d5d390> (a java.lang.Object)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
at java.lang.ClassLoader.loadClass(java.base@11.0.17/ClassLoader.java:522)
at java.lang.Class.forName0(java.base@11.0.17/Native Method)
at java.lang.Class.forName(java.base@11.0.17/Class.java:398)
at org.hibernate.validator.internal.util.privilegedactions.IsClassPresent.run(IsClassPresent.java:32)
at org.hibernate.validator.internal.util.privilegedactions.IsClassPresent.run(IsClassPresent.java:14)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.run(ConstraintHelper.java:1164)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.isClassPresent(ConstraintHelper.java:1154)
392개의 스레드가 BLOCKED
되어있었다.
"http-nio-8080-exec-747" #809 daemon prio=5 os_prio=0 cpu=484.26ms elapsed=50.24s tid=0x00007ff66c387800 nid=0x1662 waiting for monitor entry [0x00007ff5e12cf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(java.base@11.0.17/ClassLoader.java:569)
- waiting to lock <0x0000000088d5d390> (a java.lang.Object)
at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
at java.lang.ClassLoader.loadClass(java.base@11.0.17/ClassLoader.java:522)
at java.lang.Class.forName0(java.base@11.0.17/Native Method)
at java.lang.Class.forName(java.base@11.0.17/Class.java:398)
at org.hibernate.validator.internal.util.privilegedactions.IsClassPresent.run(IsClassPresent.java:32)
at org.hibernate.validator.internal.util.privilegedactions.IsClassPresent.run(IsClassPresent.java:14)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.run(ConstraintHelper.java:1164)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.isClassPresent(ConstraintHelper.java:1154)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.isJavaMoneyInClasspath(ConstraintHelper.java:1120)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.<init>(ConstraintHelper.java:432)
at org.hibernate.validator.internal.metadata.core.ConstraintHelper.forAllBuiltinConstraints(ConstraintHelper.java:395)
at org.hibernate.validator.internal.engine.ValidatorFactoryImpl.<init>(ValidatorFactoryImpl.java:175)
at org.hibernate.validator.HibernateValidator.buildValidatorFactory(HibernateValidator.java:38)
at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.buildValidatorFactory(AbstractConfigurationImpl.java:451)
at javax.validation.Validation.buildDefaultValidatorFactory(Validation.java:103)
at my.project.util.validator.SelfValidator.<init>(SelfValidator.java:10)
at my.project.application.port.in.Person.<init>(Person.java:--)
....
확인해보니 ValidatorFactory
가져오는 부분이 문제였다.
가장 처음 든 생각은 도대체 왜 여기서? 라는 생각이었는데 잠시 벙쪄있다가 stacktrace를 거슬러 올라가 보니 원인을 알 수 있었다.
위에 http-nio-8080-exec-747
스레드의 스택드레이스를 보면 가장 윗부분에 at java.lang.ClassLoader.loadClass
부분을 볼 수 있다. 코드를 살펴보면
빨간 박스친 부분에서 보면 알 수 있듯이 synchronized
처리가 되어있다!!
곧바로 해결!
이 부분을 보고 곧바로 SelfValidator<T>
클래스를 아래와 같이 바꿨고
public abstract class SelfValidator<T> {
// javax.validation.Validation 클래스의 주석을 보면 thread-safe 하다고 나와있다.
private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private Validator validator;
public SelfValidator() {
validator = factory.getValidator();
}
protected void validate() {
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
다시 성능 테스트를 하니 적절한 tps가 나왔다.
유의사항
Validation.buildDefaultValidatorFactory();
로 ValidatorFactory
를 불러올 때 synchronized
키워드 때문에 병목현상이 발생할 수 있기때문에 신경을 잘 써줘야 한다.