Java/Spring & Spring Boot

BeanWrapper를 이용하여 객체의 필드값 접근

마손리 2023. 4. 27. 20:39

기존코드

public Member updateMember(Member member) {// 수정시킬 필드값들을 담고있는 Member 엔티티 객체
    Member findMember = findVerifiedMember(member.getMemberId());// 데이터베이스에 저장된 Member 엔티티 객체

    //매개변수로 받은 객체의 필드들을 하나씩 확인하여 값이 들어있는 필드의 값들만 업데이트
    Optional.ofNullable(member.getName())
            .ifPresent(name -> findMember.setName(name));
    Optional.ofNullable(member.getPhone())
            .ifPresent(phone -> findMember.setPhone(phone));
    Optional.ofNullable(member.getMemberStatus())
            .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));

    return memberRepository.save(findMember);
}

위의 메서드는 멤버의 정보를 업데이트할때 사용한 비즈니스 로직이다. 

 

매개변수로 받은 member 객체에는 수정이 될 필드의 값만이 들어있다. 즉, 수정이 필요없는 필드는 Null값을 가지고 있기 때문에 memberRepository를 이용해 member 객체를 바로 업데이트 해줄경우 기존에 있던 컬럼의 값들이 Null이 될수가 있다. 

 

이러한 이유로 데이터베이스에서 기존의 정보를 불러와 member 객체중 Null이 아닌 필드값만을 업데이트 해주어야 한다.

 

하지만 위의 코드는 간단히 보기에도 너무 반복적이고 가독성이 좋지 않다. 또한 Member 엔티티의 필드가 많아질 수록 더 복잡해 질 것이다. 또한 Member 테이블을 위한 업데이트 뿐만아니라 다른 테이블에서의 업데이트 비즈니스 로직들도 위와 같으므로 수정해줄 필요가 있다.

 

위와 같은 코드를 BeanWrapper 인터페이스를 이용하여 불특정 다수의 엔티티 클래스의 필드에 접근하여 수정이 가능하도록 만들어 보자.

 

 

새로운 메서드 추가

@Component
public class CustomBeanUtils<T> {
    public T copyNotNullProperties(T source, T destination) { // 1.
        if (source == null || destination == null || source.getClass() != destination.getClass()) {
            return null;
        }

	// 2.
        final BeanWrapper src = new BeanWrapperImpl(source);
        final BeanWrapper dest = new BeanWrapperImpl(destination);

        for (final Field property : source.getClass().getDeclaredFields()) { // 3.
            Object sourceProperty = src.getPropertyValue(property.getName()); // 4.
            if (sourceProperty != null && !(sourceProperty instanceof Collection<?>)) { // 5.
                dest.setPropertyValue(property.getName(), sourceProperty); // 6.
            }
        }
        return destination;
    }
}

1. 제너릭을 사용하여 하나의 엔티티 객체만을 위한 메서드가 아닌 모든 엔티티 객체를 위한 메서드를 만들어 준다.
source는 클라이언트에서 받은 수정할 데이터를 담은 엔티티 객체이며 destination기존에 DB에 저장되 있던 객체이다. 

if문을 이용하여 한번 더 유효성 검사를 해준다.

 

2. 받은 객체들을 BeanWrapperImpl이라는 랩퍼 클래스로 감싸준다. BeanWrapper 인터페이스를 이용하면 .getPropertyValue()와 .setPropertyValue() 메서드를 이용해 손쉽게 해당 객체의 필드에 부여된 값에 접근하고 수정해 줄 수 있다. 

 

3. 모든 클래스들의 최상위 클래스인 Class의 메서드 .getDeclaredFields()를 이용하면 해당 객체에 선언된 필드들을 가저와 배열 구조로 받을수 있다. 이 배열을 반복문을 돌려 값이 존재하는지 확인해주어야 한다.

 

4. BeanWrapper.getPropertyValue()로 해당객체의 특정한 필드에 부여된 값을 알아내 줄수 있다.

 

5. 찾아낸 필드의 값이 null이 아니거나 어떠한 자료구조가 아닐경우인 필드만을 찾아낸다.

 

6. .setPropertyValue()메서드를 이용하여 해당 필드에 수정할 값을 입력해준다. 

 

 

위와 같이 굳이 번거롭게 BeanWrapper로 객체를 한번 감싸는 이유는 .getClass() 메서드는 객체에 대한 정보가아닌 클래스에 대한 정보만 접근이 가능하다. 즉 해당 클래스의 필드의 이름이나 타입 등, 클래스에 국한되어 있는 정보에만 접근할 수 있다.

또한 해당 코드는 제네릭을 사용하여 불특정 다수 클래스의 객체들에 접근해야한다. 이말은 매개변수로 받은 객체에 어떤 필드가 들어있는지 모르기 때문에 Class와 BeanWrapper의 메서드를 사용하여 필드에 접근을 해주어야만 하기 때문이다.

 

 

수정된 코드

@Service
public class MemberService {
    private final CustomBeanUtils<Member> customBeanUtils; // 1.
    ...
    ...
    public MemberService(CustomBeanUtils customBeanUtils) { // 의존성 주입
        this.customBeanUtils = customBeanUtils;
    }
    
    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Member editedMember =  customBeanUtils.copyNotNullProperties(member, findMember);
        // 한줄의 코드로 줄어듬
        
        return memberRepository.save(editedMember);
    }
    ...
    ...
}

한눈에 봐도 코드가 많이 깔끔해젔다. 또한 해당 메서드는 다른 엔티티의 비즈니스로직에서도 사용이 가능하다. 

1번의 타입 매개변수는 해당 비즈니스계층에서 사용할 엔티티 클래스를 넣어주면 코드가 더 깔끔해진다.

(해당 코드는 제네릭을 사용하므로 반환되는 객체는 Object이다. 그러므로 타입 매개변수를 정해주지 않으면 반환된 객체의 타입을 변환 시켜주어야 한다.)