실무를 하다보면, 비슷한 데이터를 가진 두 객체를 병합해야 할 경우가 있다.
DB에서 값을 가져와서 화면출력을 해야하는데, DB A에서 조회되는 컬럼과, DB B에서 조회되는 컬럼이 달라서 비지니스 로직 병합하여 보야줘야 하는 경우..
Jackson 라이브러리를 사용할까
라고 생각했다가 필드값을 다른 객체값으로 덮는게 아니라 특정 조건에 부합되는 경우가 필요했다.
A에서 조회된 컬럼값이 없어서 Null로 부여된다면, B의 값이 대체한다. 즉, A값이 있다면 B의값으로 대체하지 않는다.
해당 문제를 해결하기 위해 적용했던 2가지 방식에 대해 알아보자.
원하는 시나리오는 다음과 같다
1. objectA에는 필드 A,B,C가 존재한다. 하지만 C의 값은 Null이다.
public static void main(String[] args) {
ObjectA objectA = ObjectA.builder().FieldA("A").FieldB("B").build();
ObjectB objectB = ObjectB.builder().FieldA("A").FieldB("B-1").FieldC("C").FieldD("D").build();
System.out.println(objectA);
System.out.println(objectB);
//FunctionalInterfaceTest.ObjectA(FieldA=A, FieldB=B, FieldC=null)
//FunctionalInterfaceTest.ObjectB(FieldA=A, FieldB=B-1, FieldC=C, FieldD=D)
// Interface mergeObjectsInterface(List.of(objectA), ObjectA::getFieldA, List.of(objectB), ObjectB::getFieldA);
System.out.println(objectA);
mergeObjectsFunction(List.of(objectA), ObjectA::getFieldA, List.of(objectB), ObjectB::getFieldA);
System.out.println(objectA);
//FunctionalInterfaceTest.ObjectA(FieldA=A, FieldB=B, FieldC=C)
//FunctionalInterfaceTest.ObjectA(FieldA=A, FieldB=B, FieldC=C)}
@Getter @Setter @Builder @ToString
public static class ObjectA {
private String FieldA;
private String FieldB;
private String FieldC;
}
@Getter @Setter @Builder @ToString
public static class ObjectB {
private String FieldA;
private String FieldB;
private String FieldC;
private String FieldD;
}
ObjectA
는 FieldA
, FieldB
에 값이 할당되고 FieldC
는 Null
ObjectB
는 FieldA
, FieldB
, FieldC
에 값이 할당ObjectA
와 ObjectB
는 FieldA
의 키값을 가지며 매칭이 진행ObjectA
의 FieldC
는 값이 Null
이고 ObjectB
의 FieldC
는 Null
이 아니므로 ObjectA-FieldC
의 값은 Update
ObjectB
의 FieldB
이 Null
이 아니지만 ObjectA
의 FieldB
가 Null
이 아니므로 처리 대상이 아님ObjectB
의 FieldD
는 ObjectA
에 있는 필드가 아니므로 대상이 아님private static <T, U> void mergeObjects(T objectA, U objectB) {
// A 클래스의 모든 필드에 대해 반복
for (Field fieldA : objectA.getClass().getDeclaredFields()) {
try {
// 필드의 이름 가져오기
String fieldName = fieldA.getName();
// B 클래스에서 동일한 이름의 필드 찾기
Field fieldB = null;
for (Field declaredField : objectB.getClass().getDeclaredFields()) {
if (declaredField.getName().equals(fieldName)) {
fieldB = declaredField;
break;
}
}
// 필드가 존재하는 경우에만 처리
if (fieldB != null) {
// 필드의 값을 가져오고 설정
fieldA.setAccessible(true);
fieldB.setAccessible(true);
// A 객체의 값이 null이면 B 객체의 값을 사용
if (fieldA.get(objectA) == null) {
fieldA.set(objectA, fieldB.get(objectB));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
objectA
의 클래스에서 모든 선언된 필드에 대해 반복objectA
각 필드의 이름을 가져와서 objectB
의 클래스에서 동일한 이름의 필드를 조회break
, 루프를 종료setAccessible(true)
), fieldA
의 값이 null
인 경우에만 fieldB
의 값을 fieldA
에 설정fieldA
의 값이 이미 존재한다면 변경하지 않고 Pass 처리List<A>
- 완성 되는 객체 리스트Method reference A
- 완성 되는 함수 참조List<B>
- 추가 되는 객체 리스트Method reference B
- 추가 되는 함수 참조interface FieldExtractor<T> {
String extract(T object);
}
private static <T, U> void mergeObjectsInterface(
List<T> objectAList, FieldExtractor<T> extractorA,
List<U> objectBList, FieldExtractor<U> extractorB
) {
for (T objectA : objectAList) {
String valueA = extractorA.extract(objectA);
U objectB = objectBList.stream()
.filter(x -> extractorB.extract(x).equals(valueA))
.findAny().orElse(null);
if (Objects.nonNull(objectB)) {
// Assuming mergeObjects method exists
mergeObjects(objectA, objectB);
}
}
}
FieldExtractor<T>
로 대상 필드로 하는 FieldExtractor
구현체 정의를 파라미터로 전달objectAList
의 각 객체에 대해 반복FieldExtractor.extract()
를 사용하여 각 객체에서 특정 필드 값을 추출objectB
를 objectBList
에서 조회mergeObjects
메서드를 호출하여 두 객체를 병합Case 1.
의 방법도 인터페이스고, 해당 방법도 Functional Interface
니 구조는 결국 같다.
private static <T, U> void mergeObjectsFunction(
List<T> objectAList, Function<T, String> extractorA,
List<U> objectBList, Function<U, String> extractorB
) {
for (T objectA : objectAList) {
String valueA = extractorA.apply(objectA);
U objectB = objectBList.stream()
.filter(x -> extractorB.apply(x).equals(valueA))
.findAny().orElse(null);
if (Objects.nonNull(objectB)) {
// Assuming mergeObjects method exists
mergeObjects(objectA, objectB);
}
}
}
Function<T, String>
로 대상 필드로 하는 Functional Interface
구현체 정의를 파라미터로 전달objectAList
의 각 객체에 대해 반복Function.apply()
를 사용하여 각 객체에서 특정 필드 값을 추출objectB
를 objectBList
에서 조회mergeObjects
메서드를 호출하여 두 객체를 병합