Skip to content

Commit 61d745e

Browse files
committed
FEAT:(BACKENDDEV-1929) validateUtil > RequiredField 추가
1 parent aa72856 commit 61d745e

File tree

7 files changed

+252
-15
lines changed

7 files changed

+252
-15
lines changed

README.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
이 프로젝트는 Teamo2 Java 기반 프로젝트에서 공통으로 사용되는 유틸리티 클래스들을 제공하는 라이브러리입니다.
44

55
## 버전
6+
67
- 1.0.5
78

89
## 개발 환경
10+
911
- Java 17
1012
- Gradle 8.x
1113

1214
## 주요 기능
1315

1416
### TimeUtil
17+
1518
- 날짜/시간 변환 및 포맷팅
1619
- 나이 계산
1720
- 요일 기반 날짜 추출
@@ -37,7 +40,7 @@ dependencies {
3740
implementation 'kr.teamo2:utilmore:{version}'
3841
}
3942
```
40-
43+
4144
## 패키지 수정 방법
4245

4346
### 1. 프로젝트 클론
@@ -47,22 +50,13 @@ git clone https://github.com/teamo2dev/utilmore-java.git
4750
```
4851

4952
### 2. 버전 수정
50-
- build.gradle 파일에서 다음 부분을 수정합니다.
5153

54+
- build.gradle 파일에서 다음 부분을 수정합니다.
55+
5256
```gradle
53-
publishing {
54-
// repository
55-
publications {
56-
gpr(MavenPublication) {
57-
groupId = 'kr.teamo2'
58-
artifactId = 'utilmore'
59-
version = '1.0.5' // 다음 버전
60-
61-
from(components.java)
62-
}
63-
}
64-
}
57+
version = '1.0.6' // 다음 버전
6558
```
59+
6660
### 3. 패키지 배포
6761

6862
- Prod 브랜치로 푸시 혹은 PR을 생성하여 merge될 때 자동으로 배포됩니다.

lib/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ dependencies {
3030

3131
compileOnly 'org.projectlombok:lombok:1.18.30'
3232
annotationProcessor 'org.projectlombok:lombok:1.18.30'
33+
34+
//springframework
35+
implementation 'org.springframework.boot:spring-boot-starter-web:3.5.0'
3336
}
3437

3538
// Apply a specific Java toolchain to ease working on different environments.
@@ -46,6 +49,8 @@ tasks.named('test') {
4649
useJUnitPlatform()
4750
}
4851

52+
version = '1.0.7'
53+
4954
publishing {
5055
repositories {
5156
maven {
@@ -63,7 +68,7 @@ publishing {
6368
gpr(MavenPublication) {
6469
groupId = 'kr.teamo2'
6570
artifactId = 'utilmore'
66-
version = '1.0.5'
71+
version = version
6772

6873
from(components.java)
6974
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package kr.teamo2.exception.error;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
public interface BaseErrorCode<T extends RuntimeException> {
6+
7+
String name();
8+
9+
String getMessage();
10+
11+
HttpStatus getHttpStatus();
12+
13+
T toException();
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package kr.teamo2.utils.validateUtil;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class RequiredException extends RuntimeException {
7+
8+
private String code;
9+
private String message;
10+
private String fieldName;
11+
12+
public RequiredException(String message) {
13+
super(message);
14+
}
15+
16+
public RequiredException(String code, String message, Throwable cause) {
17+
super(message, cause);
18+
this.code = code;
19+
this.message = message;
20+
this.fieldName = "";
21+
}
22+
23+
public RequiredException(String code, String message, String fieldName) {
24+
super(message);
25+
this.code = code;
26+
this.message = message;
27+
this.fieldName = fieldName;
28+
}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package kr.teamo2.utils.validateUtil;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.FIELD)
10+
public @interface RequiredField {
11+
12+
boolean hasDefaultValue() default false;
13+
14+
String defaultValue() default "";
15+
16+
Class<?> defaultValueType() default String.class;
17+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package kr.teamo2.utils.validateUtil;
2+
3+
import java.lang.reflect.Field;
4+
import java.util.Optional;
5+
import kr.teamo2.exception.error.BaseErrorCode;
6+
import lombok.Getter;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.HttpStatus;
9+
10+
@Getter
11+
@RequiredArgsConstructor
12+
public enum RequiredFieldErrorCode implements BaseErrorCode<RequiredException> {
13+
14+
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "unknown internal error"),
15+
REQUIRED_FIELD_NOT_EXIST(HttpStatus.UNPROCESSABLE_ENTITY, "REQUIRED_FIELD_NOT_EXIST", "required field {} is null");
16+
17+
private final HttpStatus httpStatus;
18+
private final String errorCode;
19+
private final String message;
20+
21+
@Override
22+
public RequiredException toException() {
23+
return new RequiredException(message);
24+
}
25+
26+
@Override
27+
public String getMessage() {
28+
return message;
29+
}
30+
31+
@Override
32+
public HttpStatus getHttpStatus() {
33+
return httpStatus;
34+
}
35+
36+
public RequiredException toRequiredException(Field field) {
37+
String requiredValue = getFieldNameWithClass(field);
38+
String errorMessage = message.replace("{}", requiredValue);
39+
return new RequiredException(errorCode, errorMessage, requiredValue);
40+
}
41+
42+
public RequiredException toRequiredException(Throwable throwable) {
43+
return new RequiredException(errorCode, message, throwable);
44+
}
45+
46+
private String getFieldNameWithClass(Field field) {
47+
return Optional.ofNullable(field)
48+
.map(f -> f.getDeclaringClass().getSimpleName() + "." + f.getName())
49+
.orElse("UnknownField");
50+
}
51+
52+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package kr.teamo2.utils.validateUtil;
2+
3+
import java.lang.reflect.Array;
4+
import java.lang.reflect.Field;
5+
import java.math.BigDecimal;
6+
import java.util.Arrays;
7+
import java.util.Collection;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import lombok.AccessLevel;
11+
import lombok.NoArgsConstructor;
12+
13+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
14+
public class RequiredFieldValidator {
15+
16+
static final List<String> JAVA_PACKAGE_PREFIXES = List.of("java.", "javax.");
17+
18+
public static void validateField(Object object) {
19+
if (object == null) {
20+
return;
21+
}
22+
23+
Class<?> current = object.getClass();
24+
while (current != null && current != Object.class) {
25+
Field[] fields = current.getDeclaredFields();
26+
27+
// validate basic type value
28+
Arrays.stream(fields)
29+
.peek(field -> field.setAccessible(true))
30+
.filter(field -> field.isAnnotationPresent(RequiredField.class))
31+
.filter(field -> isNull(object, field))
32+
.forEach(field -> validate(object, field));
33+
34+
// validate object type
35+
Arrays.stream(fields)
36+
.peek(field -> field.setAccessible(true))
37+
.filter(field -> !isEnum(field.getType()))
38+
.filter(field -> !isJavaPackage(field.getType()))
39+
.forEach(field -> {
40+
try {
41+
Object innerObject = field.get(object);
42+
if (innerObject != null) {
43+
validateField(innerObject);
44+
}
45+
} catch (IllegalAccessException e) {
46+
throw RequiredFieldErrorCode.INTERNAL_ERROR.toRequiredException(e);
47+
}
48+
});
49+
50+
// Collection and Array traversal
51+
Arrays.stream(fields)
52+
.peek(field -> field.setAccessible(true))
53+
.filter(field -> Collection.class.isAssignableFrom(field.getType()) || field.getType().isArray())
54+
.forEach(field -> {
55+
try {
56+
Object value = field.get(object);
57+
if (value instanceof Collection<?> collection) {
58+
for (Object item : collection) {
59+
if (item != null && !isEnum(item.getClass()) && !isJavaPackage(item.getClass())) {
60+
validateField(item);
61+
}
62+
}
63+
}
64+
} catch (IllegalAccessException e) {
65+
throw RequiredFieldErrorCode.INTERNAL_ERROR.toRequiredException(e);
66+
}
67+
});
68+
69+
current = current.getSuperclass();
70+
}
71+
}
72+
73+
private static boolean isJavaPackage(Class<?> type) {
74+
return Optional.ofNullable(type)
75+
.map(Class::getPackage)
76+
.map(Package::getName)
77+
.map(name -> JAVA_PACKAGE_PREFIXES.stream().anyMatch(name::startsWith))
78+
.orElse(false);
79+
}
80+
81+
private static boolean isEnum(Class<?> type) {
82+
return type.isEnum();
83+
}
84+
85+
private static boolean isNull(Object object, Field field) {
86+
field.setAccessible(true);
87+
try {
88+
return field.get(object) == null;
89+
} catch (IllegalAccessException e) {
90+
throw RequiredFieldErrorCode.INTERNAL_ERROR.toRequiredException(e);
91+
}
92+
}
93+
94+
private static void validate(Object object, Field field) {
95+
RequiredField annotation = field.getAnnotation(RequiredField.class);
96+
if (!annotation.hasDefaultValue()) {
97+
throw RequiredFieldErrorCode.REQUIRED_FIELD_NOT_EXIST.toRequiredException(field);
98+
}
99+
try {
100+
field.set(object, getValue(field, annotation));
101+
} catch (IllegalAccessException e) {
102+
throw RequiredFieldErrorCode.INTERNAL_ERROR.toRequiredException(e);
103+
}
104+
}
105+
106+
private static Object getValue(Field field, RequiredField annotation) {
107+
Class<?> defaultValueType = annotation.defaultValueType();
108+
String defaultValue = annotation.defaultValue();
109+
if (defaultValueType == String.class) {
110+
return defaultValue;
111+
}
112+
if (defaultValueType == int.class) {
113+
return Integer.parseInt(defaultValue);
114+
}
115+
if (defaultValueType == BigDecimal.class) {
116+
return new BigDecimal(defaultValue);
117+
}
118+
if (defaultValueType == boolean.class) {
119+
return Boolean.parseBoolean(defaultValue);
120+
}
121+
if (defaultValueType == Array.class) {
122+
return Array.newInstance(field.getType().getComponentType(), 0);
123+
}
124+
return null;
125+
}
126+
}

0 commit comments

Comments
 (0)