λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
Spring

[Spring] 객체볡사 BeanUtils.copyProperties() & μž‘λ™ 원리

by 주발2 2021. 2. 20.
λ°˜μ‘ν˜•

 μ•ˆλ…•ν•˜μ„Έμš”~ 이전에 μš΄μ˜ν•˜λ˜ λΈ”λ‘œκ·Έ 및 GitHub, 곡뢀 λ‚΄μš©μ„ μ •λ¦¬ν•˜λŠ” Study-GitHub κ°€ μžˆμŠ΅λ‹ˆλ‹€!

 λ„€μ΄λ²„ λΈ”λ‘œκ·Έ

 GitHub

Study-GitHub

 πŸ”


βœ” BeanUtils.copyProperties()

 

μ•ˆλ…•ν•˜μ„Έμš”, μ΄λ²ˆμ— 정리할 λ‚΄μš©μ€ Spring의 BeanUtils 클래슀의 copyProperties λ©”μ†Œλ“œ μž…λ‹ˆλ‹€.

 

 

졜근 μŠ€ν”„λ§μ„ κ³΅λΆ€ν•˜λ©° Entity와 Dto μ‚¬μ΄μ—μ„œ 값을 λ³΅μ‚¬ν• λ•Œ 이 λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•˜λŠ”κ±Έ λ΄€μ—ˆλŠ”λ°μš”,

λ”°λΌμ„œ μ •λ¦¬ν•΄λ³΄κ³ μž μž‘μ„±ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

  public UserDto(User source) {
    copyProperties(source, this);

    this.profileImageUrl = source.getProfileImageUrl().orElse(null);
    this.lastLoginAt = source.getLastLoginAt().orElse(null);
  }

 

 

copyProperties λ©”μ†Œλ“œλŠ” 원본 객체λ₯Ό 볡사할 λ•Œ μ‚¬μš©ν•˜λŠ” λ©”μ†Œλ“œμΈλ°μš”,

λ§Œμ•½ 기쑴의 객체λ₯Ό 볡사할 객체가 ν•„μš”ν•œλ° Setter λ©”μ†Œλ“œλ‘œ 값을 μ„€μ •ν•œλ‹€λ©΄...?

ν•„λ“œμ˜ κ°―μˆ˜κ°€ 적으면 μƒκ΄€μ—†κ² μ§€λ§Œ, λ§Žμ•„μ§€λ©΄ μ½”λ“œλ„ λ°©λŒ€ν•΄μ§€κ³  λ²ˆκ±°λ‘œμ›Œμ§€λŠ”λ°μš”..

μ΄λ•Œ copyProperties λ©”μ†Œλ“œλ₯Ό 톡해 νŽΈν•˜κ²Œ 볡사λ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 😘

 

μ •ν™•ν•œ μžλ£ŒλŠ” κ³΅μ‹λ¬Έμ„œ μ—μ„œ ν™•μΈν•˜μ‹œλ©΄ λ©λ‹ˆλ‹€!


 

βœ” copyProperties() λ©”μ†Œλ“œ

제 μ½”λ“œμ— μžˆλŠ” BeanUtils ν΄λž˜μŠ€λŠ” Spring Framework의 5.2.10 버전이고 μ΅œμ‹  버전은 5.3.4 μΈλ°μš”,

μ΅œμ‹  λ²„μ „μ—μ„œλŠ” λ©”μ†Œλ“œκ°€ 3개인데 제 μ½”λ“œμ—λŠ” λ©”μ†Œλ“œκ°€ 4κ°œκ°€ μ˜€λ²„λ‘œλ”© λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€.

 

Spring Framework 5.2.10

 

Spring Framework 5.3.4

BeanUtils 클래슀의 copyProperties λ©”μ„œλ“œλŠ” μœ„μ™€ 같이 μ„Έ 개의 λ©”μ†Œλ“œκ°€ μžˆμŠ΅λ‹ˆλ‹€. 

* Spring Framework 5.3.4 API μž…λ‹ˆλ‹€.

 

 

두 νŒŒλΌλ―Έν„°λŠ” λ‹€μŒκ³Ό 같은 νŠΉμ§•μ„ κ°€μ§‘λ‹ˆλ‹€.

    • 첫 번째 인자인 source μ—λŠ” getter λ©”μ†Œλ“œκ°€ μ‘΄μž¬ν•΄μ•Ό ν•©λ‹ˆλ‹€.

    • 두 번째 인자인 target μ—λŠ” setter λ©”μ†Œλ“œκ°€ μ‘΄μž¬ν•΄μ•Ό ν•©λ‹ˆλ‹€.

 

 

 

λ©”μ†Œλ“œμ˜ κ°―μˆ˜λŠ” 크게 μ€‘μš”ν•˜μ§€ μ•Šμ€ 것 κ°™κ³ , λ‹€μŒ 두 λ©”μ†Œλ“œμ— λŒ€ν•΄ μžμ„Ένžˆ 보면 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€. 😁

public static void copyProperties(Object source, Object target) throws BeansException {
    copyProperties(source, target, (Class)null, (String[])null);
}

public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
    copyProperties(source, target, (Class)null, ignoreProperties);
}

sourceλŠ” 원본 객체, targetλŠ” 볡사할 κ°μ²΄μž…λ‹ˆλ‹€.

ignorePropertiesλŠ” κ°€λ³€ 인자둜 νŒŒλΌλ―Έν„°λ₯Ό λ°›κ³  μžˆλŠ”λ°μš”, μ΄λŠ” 객체λ₯Ό 볡사할 λ•Œ 섀정을 μ œμ™Έν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

 

 

이제 예제λ₯Ό 톡해 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€!

package com.github.prgrms.social.util;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class BeanUtil {

    static class Source {
        private String name;
        private int age;
        private String email;

        public Source(String name, int age, String email) {
            this.name = name;
            this.age = age;
            this.email = email;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }

        @Override
        public String toString() {
            return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
                    .append("name", name)
                    .append("age", age)
                    .append("email", email)
                    .toString();
        }
    }

    static class Target {
        private String name;
        private int age;
        private String email;

        public Target(String name, int age, String email) {
            this.name = name;
            this.age = age;
            this.email = email;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }

        @Override
        public String toString() {
            return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
                    .append("name", name)
                    .append("age", age)
                    .append("email", email)
                    .toString();
        }

    }

    public static void main(String[] args) {
        Source source = new Source("JuHyun", 20, "a@a.com");
        Target target = new Target("JuBal", 30, "b@b.com");

        target.setName(source.getName());
        target.setAge(source.getAge());
        target.setEmail(source.getEmail());

        System.out.println(source.toString());
        System.out.println("===================");
        System.out.println(target.toString());
    }

}

 

 

 

 

Source와 Target ν΄λž˜μŠ€μ—λŠ” name, age, email의 속성이 있고 getter, setter, toString λ©”μ†Œλ“œκ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€.

(사싀 μœ„μ—μ„œ λ§μ”€λ“œλ¦°λŒ€λ‘œ, Source ν΄λž˜μŠ€μ—λŠ” getter λ©”μ†Œλ“œλ§Œ, Target ν΄λž˜μŠ€μ—λŠ” setter λ©”μ†Œλ“œλ§Œ μ‘΄μž¬ν•΄λ„ μ •μƒμ μœΌλ‘œ μž‘λ™ν•©λ‹ˆλ‹€ 😊)

 

기쑴의 Source 객체λ₯Ό Target 객체에 λ³΅μ‚¬ν•˜κΈ° μœ„ν•΄ setter λ©”μ†Œλ“œλ‘œ λͺ¨λ‘ 값을 μ§€μ •ν•΄μ£ΌλŠ”λ°μš”,

        target.setName(source.getName());
        target.setAge(source.getAge());
        target.setEmail(source.getEmail());

μœ„μ˜ 경우 ν•„λ“œκ°€ λͺ‡ 개 없기에 λ¬Έμ œκ°€ λ˜μ§€λŠ” μ•Šμ§€λ§Œ, μ΄λŸ¬ν•œ ν•„λ“œκ°€ λ§Žμ•„μ§ˆμˆ˜λ‘ μ½”λ“œκ°€ λ°©λŒ€ν•΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

 

 

λ”°λΌμ„œ μœ„ μ½”λ“œλ₯Ό BeanUtils 클래슀의 copyProperties λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ κ°„λž΅ν•˜κ²Œ 객체λ₯Ό 볡사할 수 μžˆμŠ΅λ‹ˆλ‹€.

    public static void main(String[] args) {
        Source source = new Source("JuHyun", 20, "a@a.com");
        Target target = new Target("JuBal", 30, "b@b.com");
        
        BeanUtils.copyProperties(source, target);

        System.out.println(source.toString());
        System.out.println("=================================================");
        System.out.println(target.toString());
    }

 

 

μœ„λŠ” Source의 λͺ¨λ“  속성을 λ³΅μ‚¬ν•˜λŠ” νŠΉμ§•μ΄ μžˆλŠ”λ°μš”,

λ§Œμ•½ λ‚˜λŠ” λͺ¨λ“  속성이 μ•„λ‹Œ νŠΉμ • μ†μ„±λ§Œ λ³΅μ‚¬ν•˜κ³ μž ν•œλ‹€.. 

ν• λ•Œ μ‚¬μš©ν•˜λŠ” 것이 ignoreProperties νŒŒλΌλ―Έν„° μž…λ‹ˆλ‹€.

    public static void main(String[] args) {
        Source source = new Source("JuHyun", 20, "a@a.com");
        Target target = new Target("JuBal", 30, "b@b.com");

        BeanUtils.copyProperties(source, target, "age", "email");

        System.out.println(source.toString());
        System.out.println("=================================================");
        System.out.println(target.toString());
    }

μœ„ κ²°κ³Όλ₯Ό λ³΄μ‹œλ©΄ age 와 email은 λ¬΄μ‹œν•˜λ„λ‘ μ„€μ •ν–ˆμœΌλ―€λ‘œ, μ‹€μ œμ μœΌλ‘œλŠ” name κ°’λ§Œ 볡사가 λ©λ‹ˆλ‹€.


 

 

 

βœ” 디버깅

 

μ‹€μ œ λ³΅μ‚¬λ˜λŠ” copyProperties λ©”μ†Œλ“œμ˜ μ½”λ“œλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
            }

            actualEditable = editable;
        }

        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        PropertyDescriptor[] var7 = targetPds;
        int var8 = targetPds.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            PropertyDescriptor targetPd = var7[var9];
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }

                            Object value = readMethod.invoke(source);
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }

                            writeMethod.invoke(target, value);
                        } catch (Throwable var15) {
                            throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
                        }
                    }
                }
            }
        }

    }

μ½”λ“œκ°€ μ’€ λ³΅μž‘ν•œλ°μš”, μ–΄λ–»κ²Œ λ™μž‘ν•˜λŠ”μ§€ κΆκΈˆν•΄μ„œ 디버깅을 톡해 κ°„λž΅ν•˜κ²Œ! μ‚΄νŽ΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

(잘λͺ»λœ 뢀뢄이 있으면 λ§μ”€ν•΄μ£Όμ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€πŸ€ž)

 

 

 

μœ„μ˜ 두 지점에 포인트λ₯Ό κ±Έκ³  디버깅을 μ‹€ν–‰ν•΄ λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

 

 

 

1) editable 체크

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
            }

            actualEditable = editable;
        }

λ¨Όμ € editable은 nullμ΄λ―€λ‘œ μœ„ λ¬Έμž₯은 ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.

 

 

 

2) λ°°μ—΄μ˜ 길이(ν•„λ“œ) 및 ignoreList(속성 λ¬΄μ‹œν•  ν•„λ“œ) 지정

        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        PropertyDescriptor[] var7 = targetPds;
        int var8 = targetPds.length;

 

μœ„μ™€ 같이 targetPds λ°°μ—΄μ˜ 길이가 λ‚˜νƒ€λ‚˜λŠ”λ°μš”, 

μœ„ actualEditable은 target.getClass() μ—μ„œμ˜ Class μž…λ‹ˆλ‹€.

 

λ°°μ—΄μ˜ κΈΈμ΄λŠ” ν•„λ“œ κ°―μˆ˜μ™€ classλ₯Ό ν•©μΉœ 4 κ°€ λ©λ‹ˆλ‹€.

λ˜ν•œ ignoreList에 섀정을 λ¬΄μ‹œν•  property듀이 지정이 λ˜λŠ”λ°μš”,

ν˜„μž¬ age와 email을 μ§€μ •ν–ˆμœΌλ―€λ‘œ, μœ„ ignoreListμ—λŠ” "age" 와 "email" 의 값이 μ €μž₯이 λ©λ‹ˆλ‹€.

 

 

 

3) forλ¬Έ - μ‹€μ œ ν•„λ“œ 볡사

        for(int var9 = 0; var9 < var8; ++var9) {
            PropertyDescriptor targetPd = var7[var9];
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null) {
                        ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
                        ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
                        if (targetResolvableType.isAssignableFrom(sourceResolvableType)) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }

                                Object value = readMethod.invoke(source);
                                if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                    writeMethod.setAccessible(true);
                                }

                                writeMethod.invoke(target, value);
                            } catch (Throwable var17) {
                                throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var17);
                            }
                        }
                    }
                }
            }
        }

λ§ˆμ§€λ§‰μœΌλ‘œ forλ¬Έ μž…λ‹ˆλ‹€.

μœ„μ—μ„œλŠ” PropertyDescriptor 배열을 μˆœνšŒν•˜λ©° ignoreList에 ν¬ν•¨λœ(μ„€μ • μ œμ™Έ) ν•„λ“œμΌ 경우, if문을 κ±΄λ„ˆλ›°κ³ ,

ignoreList에 ν¬ν•¨λ˜μ§€ μ•Šμ€(κ°’ 볡사) ν•„λ“œμΌ 경우, if문을 톡해 μ‹€μ œ 값을 λ³΅μ‚¬ν•˜λŠ” μ½”λ“œμž…λ‹ˆλ‹€.

 

μœ„μ™€ 같이 age, email의 경우 λͺ¨λ‘ κ±΄λ„ˆλ›°μ§€λ§Œ

name의 경우 ifλ¬Έ λ‚΄λΆ€λ‘œ λ“€μ–΄κ°€μ„œ 값을 λ³΅μ‚¬ν•©λ‹ˆλ‹€.

 

 

μ‹€μ œ Target 클래슀의 setterλ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— 볡사할 κ°μ²΄μ—λŠ” setter λ©”μ†Œλ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€.

 

 

 

μ‹€μ œ Source 클래슀의 getterλ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— κΈ°μ‘΄ κ°μ²΄λŠ” getter λ©”μ†Œλ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€.

 

 

μ‹€μ œ 값을 λ³΅μ‚¬ν•˜λŠ” λΆ€λΆ„μž…λ‹ˆλ‹€.

writeMethod의 invokeλ₯Ό 톡해 target(볡사할 객체)에 value(name="JuHyun") 의 값을 μ„€μ •ν•©λ‹ˆλ‹€.

 

 

 

μœ„ writeMethod.invoke(target, value) 의 λ‚΄λΆ€λ‘œ λ“€μ–΄κ°€λ©΄ μœ„μ™€ 같이 setter λ©”μ†Œλ“œκ°€ 싀행이 λ©λ‹ˆλ‹€.

λ¦¬ν”Œλ ‰μ…˜μ„ 톡해 μž‘λ™ν•˜λ―€λ‘œ, value 값을 target에 값을 μ„€μ •ν•˜λŠ” κ±Έ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

λ°˜μ‘ν˜•

λŒ“κΈ€