일반적인 서비스에서는 여러 Entity의 다양한 필드들을 유저나 관리자의 API 호출등으로 수정하게 됩니다. 단순히 조회수 등이 추가 되는것이 아니라 도메인의 메인 프로퍼티들이 변경됩니다. 유저의 전화번호나 주소 부터, 병원의 이미지uri 나 설명등 과 같은.
이런것들은 보통 Restful 에서 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 3 같은 API 를 통해 수정이 되게 되는데, 구현 방식에 따라 몇가지 차이가 있을 수 있습니다.
- 클라이언트에서 수정요청 보낼 시, 필요한 모든 필드를 채워서 보내는 경우
-> 이 경우에는 해당 요청을 받아낼 Request 용 DTO 등을 만들거나 해서 해당 정보를 받아온 후, 무조건 기존에 있던 데이터의 DTO 의 모든 필드를 다 assign 해버리면 됩니다. 변경이 되든 안되든 그 값이 다 담겨져 있을 테니까요 - 반대로 모든 필드가 안 채워서 오는 경우
-> 이 경우에는 바꾸고자 하는 필드인.. 예를 들어 전화번호는 새로운 번호가 담겨왔는데, 유저의 주소는 안담겨져서 null 이 올 수 있습니다. 이 때 null 은 기존 데이터에 반영이 되면 안됩니다.
- 우리의 상황은 2번에 더해 현재 legacy 로 남겨진 도메인들 중에는 애초에 RequestDTO 를 쓰지 않고, @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 4 자체를 Body 형태로 주고받고 있는 경우도 있었습니다. 그러면 어떤 필드가 수정가능한 필드이고, 어떤 필드는 수정불가 필드인지가 코드만 보고 알기 어려워집니다.
(애초에 DTO 만들어서 Entity 와 분리시켜야 겠지만 현재 레거시를 다 들어내기 힘들다는 가정하에)
아래 코드를 보면 특정 도메인의 필드들을 볼 수 있는데, @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 5 라는 메소드가 보입니다. 수정 api 를 날리면 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 5 메소드를 통해서 새로운 값을 반영하도록 서비스단을 공통화 해둔 상태였습니다.
// 공통 서비스에서 엔티티 업데이트 시 사용하는 메소드 - 엔티티의 merge 메소드를 사용하고 있습니다. public ResponseResult<T> update(long id, T t) throws CommonException { T oldT = getBy(id); boolean updated = oldT.merge(t); getDao().update(oldT); return new ResponseResult<>(updated ? "SUCCESS" : "NOT_AFFECTED", oldT); } public class Testdomain extends Basedomain{ private String targetView; private BannerType bannerType; @Size(max = 10) @Column(length = 10) private String colorCode; @NotEmpty @Size(max = 100) @Column(length = 100) private String actions; @NotEmpty private String bannerImage; @Transient private String title; private int viewCount; // basedomain 에서부터 상속해서 쓰는 merge 메소드. // Service 단에서도 부모 service 를 만들어서 update 시 merge 메소드를 사용하게 공통화해둔 상태 @Override public boolean merge(Object fromObj) throws CommonException { boolean updated = super.merge(fromObj); if(fromObj instanceof Banner) { Banner from = (Banner)fromObj; if(Util.canUpdate(this.targetView, from.targetView)) { this.targetView = from.targetView; updated = true; } if(Util.canUpdate(this.bannerType, from.bannerType)) { this.bannerType = from.bannerType; updated = true; } if(Util.canUpdate(this.colorCode, from.colorCode)) { this.colorCode = from.colorCode; updated = true; } if(Util.canUpdate(this.actions, from.actions)) { this.actions = from.actions; updated = true; } if(Util.canUpdate(this.bannerImage, from.bannerImage)) { this.bannerImage = from.bannerImage; updated = true; } } return updated; } }위 코드는 여러 단점이 있는데
- 매 필드가 추가+수정+삭제 될때마다 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 5 method 를 건드려야 한다는 점. 추후에 아무리 클라이언트에서 값을 던져도 반영이 되지 않는 상황들이 몇번 있었는데 이걸 빼먹어서였던 적이 많았ㅠ
- 수정할 필드가 많아질수록 코드가 쭉쭉 늘어나고 길어져서 readability 도 좋지않다.
- 무엇보다 뭔가 개발자가 싫어하게 반복적이서.. 고쳐야될것만 같은 막 의무감을 준다.
그럼에도 불구하고, 작업하려면 모든 클래스의 필드에 직접 접근해야 다보니 쉽게 공통코드화 할 수는 없었습니다.
그러던 어느날, 당시 새로 합류한 카이의 코멘트를 보고 다시금 각성! 띠용! 이럴수는 없어! 해결을 해야겠다!!!
해결 방법 고민하지만 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 8 에는 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 9 이 있습니다. TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 0 내의 필드와 메소드등에 접근해서 활용할 수 있게 해주는 이 util 을 이용해서 자동화를 도전해보고 싶어졌습니다.
요구사항은 2가지
- 특정 필드만 수정(merge) 대상이어야 한다.
- 어떤 필드는 예외적으로 null 을 던지면 null 로 바뀌어야 한다 (가끔 이런 케이스가 있음)
클래스의 필드를 잔뜩 가져오는 것은 리플렉션으로 해결한다면, 그 중 특정 필드만 수정 가능하다는 체크는 어떻게 할것인가?
바로 TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 1 을 활용! Custom annotation 을 만들어서 해당 필드에 달아주고, 그 필드들만 대상으로 merge 를 진행해보려 합니다.
일단 리플렉션부터 시작해봅니.
Reflection 을 이용한 필드 접근
아래는 리플렉션을 알아보기 위해 짜 본 테스트 코드
@RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } }Class 의 TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 2 를 통해 private field 들을 가져올 수 있고, 각 필드 메소드를 간단 소개하자면
- TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 3 : 필드 타입 클래스
- TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 4 : 필드에 달린 어노테이션의 클래스들
- TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 5 : 해당 필드가 어떤 클래스에서 선언되었는지 (부모클래스일수도 있음)
- TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 6 : 프라이빗 필드는 보통 접근이 불가. true 로 해줘야 가능
- TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 7 : 특정 옵젝트에서 해당 필드의 value 를 가져오기
수행 결과는 아래와 같습니다.
TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123자 이제 어느정도 이해가 되었으니, 어노테이션을 제작해봅니다.
Custom Annotation 제작 및 적용
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; }위와 같이 TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 8를 만들면 어노테이션이 생성됩니다.
안에 있는 필드 TestEvery.Obj(a=abc, b=11, c=false, d=123) ======== java.lang.String [@java.lang.Deprecated()] // 어노테이션도 잘 나옴 class com.healing.beauty.TestEvery$Obj abc // 값도 잘 나옴 ======== long [] class com.healing.beauty.TestEvery$Obj 11 ======== java.lang.Boolean [] class com.healing.beauty.TestEvery$Obj false ======== java.lang.String [] class com.healing.beauty.TestEvery$Obj 123 9 은 추후 어노테이션의 프로퍼티로 사용될 수 있습니다. null 을 무시하지 않는 케이스를 위해 만들어 두었습니다.
다시 TestCase 로 돌아가서 이번에는 어노테이션에 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 0 를 넣어보겠습니다.
@AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } }위에 처럼 짜서 돌려보면 annotation 값에 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 1 라는 어노테이션이 달려있을때만 해당 어노테이션 객체가 들어오게 됩니다. 그러면 저렇게 들어오는 필드일때만 비교해서 넣어주고, 아니면 무시하고! 하면 첫번째 요구사항이 해결됩니다.
1.특정 필드만 수정(merge) 대상이어야 한다.
그럼 이제 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 2이 필요합니다.
public static <T> boolean canUpdate(T to, T from) { if(from != null && !from.equals(to)) { return true; } return false; }두 값을 비교해서 source 값이 null 이 아닌 경우에만 equals 로 동일 체크를 해서 다르면 update 대상임을 알려줍니다. (위)
반대로 source 가 null 이어도 기존 값이 null 이 아니면 반영을 해줘야 하는 요구사항도 있었으니 그 부분을 처리하기 위한 메소드도 추가합니다. (아래)
이렇게 처리하고 필요할때마다 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 3 로 해주면 분기가 가능합니다. 두번째 요구사항도 해결방법이 보입니다.
2.어떤 필드는 예외적으로 null 을 던지면 null 로 바뀌어야 한다 (가끔 이런 케이스가 있음)
위 2가지 메소드와 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 4 을 이용해 2개의 오브젝트에서 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 0 붙은 필드들을 찾아내 값을 변경해주는 메소드를 구성합니다.
아래에 소스별로 주석을 달아두었으니 참고해주시길.
Merge 메소드 (code#1)
/**** * 자체 Util 클래스 안에 머지 메소드 구현 * 두개의 머지가능한 오브젝트에서 @Merge 어노테이션을 활용해 * 해당 필드들이 머지 가능한지 체크해서 sourceObj 의 머지가능한 값을 targetObj 로 넣어준다. * @param targetObj * @param sourceObj * @return * @throws IllegalAccessException */ public static <T> boolean merge(T targetObj, T sourceObj) { if(targetObj.getClass() != sourceObj.getClass()) { throw new IllegalArgumentException("The two parameter objects should be the same class"); } boolean updated = false; Annotation annotation; List<String> mergedList = new LinkedList<>(); try { for(Field field : targetObj.getClass().getDeclaredFields()) { // Merge annotation field 체크 annotation = field.getAnnotation(Merge.class); if(annotation == null) continue; // 각 object 에서 해당 필드 값 빼오기위해선 해줘야한다 field.setAccessible(true); Object oldValue = field.get(targetObj); Object newValue = field.get(sourceObj); boolean canMerge = false; // Merge annotation 의 ignoreNull 프로퍼티를 이용해서 null 값 처리 분기 if(((Merge)annotation).ignoreNull()) { // 기본형 ( source 필드 null 은 무시하고 두 밸류가 다른지를 체크) if(Util.canUpdate(oldValue, newValue)) { canMerge = true; } } else { // null 이 올라오면 null 로 변경될 수 있습니다는 가정하에 두 밸류 다른지 여부 체크 if(Util.canUpdateAlbeitNull(oldValue, newValue)) { canMerge = true; } } // 값이 변경되는 케이스 if(canMerge) { field.set(targetObj, newValue); updated = true; mergedList.add(String.format("%s : %s -> %s", field.getName(), oldValue, newValue)); } } } catch(IllegalAccessException e) { logger.error("error occurs during Merge fields of "+targetObj.getClass(), e); } if(updated) { logger.info(String.join("\n", mergedList)); } return updated; }이렇게 해서 완성!! 아까는 나오지 않았던 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 6 값을 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 7 해주는 메소드가 나왔는데, 대상 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Merge { // null 인 필드는 무시 - 추후 자세히 설명 boolean ignoreNull() default true; } 8 와 value 만 넣어주면 됩니다.
실제로 테스트 케이스에서 다시 시도해봅니다.
(code#2)
@AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge(ignoreNull = false) private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); Obj obj2 = new Obj("def", 33, null, null); System.out.println(obj1); System.out.println(obj2); System.out.println(Util.merge(obj1, obj2)); System.out.println(obj1); assertThat(obj1.getA(), is("abc")); assertThat(obj1.getB(), is(33L)); assertThat(obj1.getC(), is(false)); assertNull(obj1.getD()); }아래 3개(b,c,d)만 @Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Merge {
// null 인 필드는 무시 - 추후 자세히 설명
boolean ignoreNull() default true;
}
0가 붙어으니 a 필드는 서로 달라도 안 바뀌어야 하며, c 는 null 무시하고 d 는 null 이 되어야 한다.
그러니 결과는 @AllArgsConstructor
@Data
class Obj {
private String a;
@Merge
private long b;
@Merge
private Boolean c;
@Merge
private String d;
}
@Test
public void mergeTest() {
Obj obj1 = new Obj("abc", 11, false, "123");
System.out.println(obj1);
for(Field field : obj1.getClass().getDeclaredFields()) {
annotation = field.getAnnotation(Merge.class);
}
}
0 이어야 하고, Test 케이스는 모두 통과했다!!! 성공!!
이제 이 메소드를 잘 활용만 하면 끄읏!!! 아까 다시 TestDomain 클래스 로 돌아가서 바꿔준다면?
(code#3)
public class Testdomain extends Basedomain{ private String targetView; @Merge // 머지 등장!! private BannerType bannerType; @Size(max = 10) @Column(length = 10) @Merge private String colorCode; @NotEmpty @Size(max = 100) @Column(length = 100) @Merge private String actions; @NotEmpty @Merge private String bannerImage; @Transient private String title; private int viewCount; // 아까 30여줄이 넘던 코드가 다 사라지고 이거 하나로 끝. // 게다가 이런 도메인 모델이 50여개 있었습니다고 치면... 무려 1500 줄이 줄어들고, 관리도 편해지게 됩니다. @Override public boolean merge(Object sourceObj) throws CommonException { return Util.merge(this, sourceObj); } } 마치며오래 전 부터 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 9 으로 어떻게 해볼수 있지 않을까 상상만 하다가 그쳤었는데, @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 2 활용까지 더해서 구현을 마칠 수 있었습니다. 예상보다 막히는 부분이 없어서 빨리 끝날 수 있어서 좋았구요. (테스트 코드부터 단위별로 짜면서 시작해보았는데.. 역시 TDD 인가!?)
예전부터 구상만 하던 거였는데 새로 입사하신 분이 이 레거시 코드의 불편함을 재기해주면서 확 삘 받아서 작업에 들어갈 수 있었습니다.
역시 새로운 사람과 새로운 환경에서의 새로운 자극이, 가끔은 기존에 습관이나 패턴에 익숙해진 우리들을 깨우치고 다시 바꿔나갈 수 있게 해주는것 같아서 매우 좋은 기회였습니다. 마치 3년 묵은 변비를.... 여기까지. fin.
슬프게도 @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 3 를 통해서 필드를 가져오는 경우, @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 4 의 필드는 가져오지 못한다는 것을 깨달았고**(private 이기 때문에)**, 결국 merge 메소드를 사용할때 슈퍼 클래스의 필드가 머지되지 않는 오류가 있었습니다..ㅠㅠ
해결방법 다시 고민
3번째 요구사항이 생겼습니다.
- 상속받은 entity class 라고 하면, 모든 super class 들의 (부모 entity) 필드도 가져와서 Merge 할게 있으면 해줘야 한다
그래서 찾아보니 역시 @RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 9 안에는 super class 에도 접근할 수 있는 방법이 있습니다.
@RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 0흠 그런데 만약 상속이 여러번 중첩되고 한다면... @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 6 하게 가져올 수 는 있겠지만 뭔가 점점 복잡해지는 상황이 옵니다 아아악..ㅠ
그러던 순간, 새로운 방법을 발견했습니다. @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 7 이 제공해주는 Util 에서 존재하는 모든 필드를 접근해서 수행하는 메소드가 있다는 사실!?!
역시 스프링 너희들은 다 계획이 있구나..?!
Access to private inherited fields via reflection in Java
Spring has a nice Utility class ReflectionUtils that does just that: it defines a method to loop over all fields of all super classes with a callback: ReflectionUtils.doWithFields()
자세한 건 위의 링크를 보시면 아시겠지만 (스택오버플로우는 사랑입니다) 결국 저 메소드를 사용해서 필터부분과 필드접근부분을 잘 짜면 해결이 가능해 보인다는 사실. 그래서 해보았습니다.
To-be Util.merge method
(code#1 → code#4)
@RunWith(MockitoJUnitRunner.class) public class TestMerge { @AllArgsConstructor @Data class Obj { // 어노테이션 가져와지는지 궁금해서 디프리케이티드 붙여봄 @Deprecated private String a; private long b; private Boolean c; private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); // private filed 들은 `getDeclaredFields()` 를 써서 가져올 수 있습니다. for(Field field : obj1.getClass().getDeclaredFields()) { System.out.println("========"); System.out.println(field.getType().getSimpleName()); System.out.println(Arrays.toString(field.getAnnotations())); System.out.println(field.getDeclaringClass().toString()); // private 은 accessible 을 true 로 해줘야 리플렉션으로 값 접근이 가능 field.setAccessible(true); System.out.println(field.get(obj1)); } } } 1그리고 기존 #code2 부분에 있는 Test 코드에 이어서 한가지 테스트케이스를 더 추가해봤습니다.
기존 @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 8 라는 클래스를 상속받아 쓰는 @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 9 를 만들고, 해당 클래스의 오브젝트를 2개 만들어서 머지할 때, super class 인 @AllArgsConstructor @Data class Obj { private String a; @Merge private long b; @Merge private Boolean c; @Merge private String d; } @Test public void mergeTest() { Obj obj1 = new Obj("abc", 11, false, "123"); System.out.println(obj1); for(Field field : obj1.getClass().getDeclaredFields()) { annotation = field.getAnnotation(Merge.class); } } 8 의 필드들도 합쳐지는지를 보는 테스트입니다.