티스토리 뷰

프로그램에서 복사는 데이터의 복제본을 만드는 것을 의미하는데 많이 실수하는 것이 Shallow Copy와 Deep Copy이다, 특히 객체를 가지고 작업을 하는 경우 특히 주의해야 한다. 다음 코드를 보자  ( 코드는 setter, getter, equals를 작성해야 하지만  lombok 라이브러리를 사용하였다. )

@Getter
@Setter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
class 서비스요소VO implements Cloneable {
    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;


    public  서비스요소VO(서비스요소VO p서비스요소VO)  {
        this.이벤트코드 = p서비스요소VO.get이벤트코드();
        this.요소코드 = p서비스요소VO.get요소코드();
        this.파라메터 = p서비스요소VO.get파라메터();
    }

    public static 서비스요소VO 서비스요소VOFactory(서비스요소VO p서비스요소VO)  {
        return new 서비스요소VO(p서비스요소VO);
    }

    public  서비스요소VO 서비스요소복제()  {
        try {
            return (서비스요소VO) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

@SneakyThrows(JsonProcessingException.class)
public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001002");

    서비스요소VO 서비스요소VO_shallo = 서비스요소VO_01;
    서비스요소VO_shallow.set파라메터("F00=001003");
    boolean is서비스요소VO_shallow = 서비스요소VO_01.equals(서비스요소VO_shallow);
    System.out.println("is서비스요소VO_shallow : " + is서비스요소VO_shallow);

    서비스요소VO 서비스요소VO_Cloneable = 서비스요소VO_01.서비스요소복제();
    서비스요소VO_Cloneable.set파라메터("F00=001004");
    boolean is서비스요소VO_Cloneable = 서비스요소VO_01.equals(서비스요소VO_Cloneable);
    System.out.println("is서비스요소VO_Cloneable : " + is서비스요소VO_Cloneable);

    서비스요소VO 서비스요소VO_Constructor= new 서비스요소VO(서비스요소VO_01);
    서비스요소VO_Constructor.set파라메터("F00=001004");
    boolean is서비스요소VO_Constructor = 서비스요소VO_01.equals(서비스요소VO_Constructor);
    System.out.println("is서비스요소VO_Constructor : " + is서비스요소VO_Constructor);

    서비스요소VO 서비스요소VO_Factory = 서비스요소VO.서비스요소VOFactory(서비스요소VO_01);
    서비스요소VO_Factory.set파라메터("F00=001004");
    boolean is서비스요소VO_Factory = 서비스요소VO_01.equals(서비스요소VO_Factory);
    System.out.println("is서비스요소VO_Factory : " + is서비스요소VO_Factory);

    서비스요소VO 서비스요소VO_Lombok = 서비스요소VO_01.toBuilder().build();
    서비스요소VO_Lombok.set파라메터("F00=001004");
    boolean is서비스요소VO_Lombok= 서비스요소VO_01.equals(서비스요소VO_Lombok);
    System.out.println("is서비스요소VO_Lombok : " + is서비스요소VO_Lombok);

    ObjectMapper objectMapper = new ObjectMapper();
    서비스요소VO 서비스요소VO_ObjectMapper = objectMapper
        .readValue(objectMapper
            .writeValueAsString(서비스요소VO_01), 서비스요소VO.class);
    서비스요소VO_ObjectMapper.set파라메터("F00=001004");
    boolean is서비스요소VO_ObjectMapper = 서비스요소VO_01.equals(서비스요소VO_ObjectMapper);
    System.out.println("is서비스요소VO_ObjectMapper : " + is서비스요소VO_ObjectMapper);
 }

@SneakyThrows : lombok 에서 제공하는 것으로 throws 나 try-catch 구문을 통해서 Exception에 대해 번거롭게 명시적으로 예외 처리를 해줘야 하는 경우에 @SneakyThrows 어노테이션을 사용하여 명시적인 예외 처리를 생략할 수 있다. 그러나 사용 시 주의를 해야 한다.(룸북의 공식 홈페이지에서는 이 어노테이션은 논쟁의 여지가 있어 사용 시 신중하게 사용해야 한다). 해당 어노테이션을 없애면 다음과 같이 작성해야 한다.

ObjectMapper objectMapper = new ObjectMapper();
서비스요소VO 서비스요소VO_ObjectMapper = null;
try {
   서비스요소VO_ObjectMapper = objectMapper.readValue(
      objectMapper.writeValueAsString(서비스요소VO_01), 서비스요소VO.class);
} catch (JsonProcessingException e) {
   throw new RuntimeException(e);
}

 

1. Shallow Copy

서비스요소VO 서비스요소 VO_shallow = 서비스요소VO_01;  구문은 서비스요소VO_shallow에 서비스요소_01 값은 복사하여 틀린 값이라고 생각하지만 실제는 주소를 복사하는 것으로 같은 값이 되는 것으로 주소만 복사하는 것을 복사(Shallow Copy)이다

서비스요소VO 서비스요소VO_01 = new 서비스요소VO();
서비스요소VO_01.set이벤트코드("ACT");
서비스요소VO_01.set요소코드("FTR000001");
서비스요소VO_01.set파라메터("F00=001002");

서비스요소VO 서비스요소VO_shallow = 서비스요소VO_01;
서비스요소VO_shallow.set파라메터("F00=001003");
boolean is서비스요소VO_shallow = 서비스요소VO_01.equals(서비스요소VO_shallow);
System.out.println("is서비스요소VO_shallow : " + is서비스요소VO_shallow);

수행 결과를 보면 서비스요소VO_01과 서비스요소VO_shallow는 주소 10450으로 같은 주소를 가지고 있어서 파라미터 값을 "F00=001003"으로 변경 시 서비스요소VO_01과 서서비스요소 VO_shallow이 모두 변경이 된다.

 

 


2. Deep Copy

Shallow Copy와 틀리게 일반적으로 복사를 생각하는 것처럼 변경한 객체의 데이터를 변경해도 원본의 데이터 변경이 없는 것을 Deep Copy라 한다. 즉 새로운 주소에 값을 변경하는 것으로 자바에서는 직접 구현하는 방법과 라이브러리를 이용하는 방법이 있다.

직접 구현하는 방법 

- Cloneable을 상속받아 clone() 메서드를 재정의 : Cloneable Interface를 사용하는 방법으로 Cloneable을 상속받아 clone() 메서드를 재정의하는 방법으로 하위 객체는 지원하지 않는 방법으로 추천하지 않는 방법이다.
- 생성자에 자기 자신의 객체를 파라미터로 받아 필드를 재정의 : Copy Constructor과 Copy Factory를 이용하는 방법으로 구현 방식에 차이가 있다. 

2-1. Cloneable Interface

@Getter
@Setter
class 서비스요소VO implements Cloneable {
public  서비스요소VO 서비스요소복제()  {

    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;
    
    public  서비스요소VO 서비스요소복제()  {
        try {
            return (서비스요소VO) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001003");
    
    서비스요소VO 서비스요소VO_Cloneable = 서비스요소VO_01.서비스요소복제();
    서비스요소VO_Cloneable.set파라메터("F00=001004");
    boolean is서비스요소VO_Cloneable = 서비스요소VO_01.equals(서비스요소VO_Cloneable);
    System.out.println("is서비스요소VO_Cloneable : " + is서비스요소VO_Cloneable);
}

클래스 작성 시  Cloneable를 상속받고 ( implements Cloneable ) 서비스요소VO 서비스요소복제()와 같은 메서드를 만들어야 한다. 그러고 나서 만든 서비스요소복제() 메서드를 사용하여 다른 변수에 할당한다. 수행하면 틀린 주소 서비스요소VO_01는 10450, 서비스요소 VO_Cloneables는 10458에 할당되어 서비스요소 VO_Cloneables의 값을 변경해도 값이 다르게 된다.

 


2-2. Copy Constructor

@Getter
@Setter 
@NoArgsConstructor 
@EqualsAndHashCode
class 서비스요소VO {
    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;

    
    public  서비스요소VO(서비스요소VO p서비스요소VO)  {
        this.이벤트코드 = p서비스요소VO.get이벤트코드();
        this.요소코드 = p서비스요소VO.get요소코드();
        this.파라메터 = p서비스요소VO.get파라메터();
    }
 
}


public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001003"); 

    서비스요소VO 서비스요소VO_Constructor= new 서비스요소VO(서비스요소VO_01);
    서비스요소VO_Constructor.set파라메터("F00=001004");
    boolean is서비스요소VO_Constructor = 서비스요소VO_01.equals(서비스요소VO_Constructor);
    System.out.println("is서비스요소VO_Constructor : " + is서비스요소VO_Constructor);
 }

서비스요소 VO을 받는 생성자를 만들어서 사용하는 것으로 옆 이미지를 보면 서비수요소 VO_01, 서비스요소 VO_Constructor의 주소가 각각 10450, 10463으로 값울 변경해도 영향을 받지 않는다,.

서비수요소 VO_01.get파라미터() : F00=001003
서비스요소 VO_Constructor.get파라미터() : F00=001004

 


2-3. Copy Factory

@Getter
@Setter 
@NoArgsConstructor 
@EqualsAndHashCode
class 서비스요소VO  {
    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;


    public  서비스요소VO(서비스요소VO p서비스요소VO)  {
        this.이벤트코드 = p서비스요소VO.get이벤트코드();
        this.요소코드 = p서비스요소VO.get요소코드();
        this.파라메터 = p서비스요소VO.get파라메터();
    }

    public static 서비스요소VO 서비스요소VOFactory(서비스요소VO p서비스요소VO)  {
        return new 서비스요소VO(p서비스요소VO);
    }
}

public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001003");
    

    서비스요소VO 서비스요소VO_Factory = 서비스요소VO.서비스요소VOFactory(서비스요소VO_01);
    서비스요소VO_Factory.set파라메터("F00=001004");
    boolean is서비스요소VO_Factory = 서비스요소VO_01.equals(서비스요소VO_Factory);
    System.out.println("is서비스요소VO_Factory : " + is서비스요소VO_Factory);
 }

기본적으로 자신의 객체를 받는 기본 생성자를 생성 후 static 메서드를 생성하여 객체를 생성하는 방법으로 Copy Constructor와 동일한 한다, static으로 메서드를 생성하는 것은 프로그램 어느 곳에서도 접근이 가능하게 하기 때문이다,

서비수요소VO_01.get파라메터() : F00=001003
서비스요소VO_Factory.get파라메터() : F00=001004


라이브러리를 이용하는 방법

- lombok의 toBuilder 사용
- ObjectMapper 
- MapStruct 

2-4. lombok의 toBuilder 사용

lombok을 사용하기 위해서는 Maven Repository에서 의존성 주입을 해아 한다.

<dependency> <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

@Getter
@Setter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
class 서비스요소VO  {
    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;
}

 
public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001002");

    서비스요소VO 서비스요소VO_Lombok = 서비스요소VO_01.toBuilder().build();
    서비스요소VO_Lombok.set파라메터("F00=001004");
    boolean is서비스요소VO_Lombok= 서비스요소VO_01.equals(서비스요소VO_Lombok);
    System.out.println("is서비스요소VO_Lombok : " + is서비스요소VO_Lombok);
 }

lombok toBuilder를 사용하기 위해서는 모든 멤버 변수를 받는 생성자라 필요해서 @AllArgsConstructor를 사용했으며, toBuilder를 사용하기 위해서 @Builder(toBuilder = true)를 사용한다, 
서비스요소VO_01.toBuilder().build()로 변수에 할당하면 된다,.

서비수요소VO_01.get파라메터() : F00=001003
서비스요소VO_Lombook.get파라메터() : F00=001004

 

2-5. ObjectMapper

ObjectMapper을 사용하기 위해서는 Maven Repository에서 의존성 주입을 해아 한다.

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.14.1</version>
</dependency>
@Getter
@Setter
@EqualsAndHashCode
class 서비스요소VO implements Cloneable {
    private String 이벤트코드;
    private String 요소코드;
    private String 파라메터;
}

@SneakyThrows(JsonProcessingException.class)
public void ObjectCopy()  {
    서비스요소VO 서비스요소VO_01 = new 서비스요소VO();

    서비스요소VO_01.set이벤트코드("ACT");
    서비스요소VO_01.set요소코드("FTR000001");
    서비스요소VO_01.set파라메터("F00=001002");

    ObjectMapper objectMapper = new ObjectMapper();
    서비스요소VO 서비스요소VO_ObjectMapper = objectMapper
        .readValue(objectMapper
            .writeValueAsString(서비스요소VO_01), 서비스요소VO.class);
    서비스요소VO_ObjectMapper.set파라메터("F00=001004");
    boolean is서비스요소VO_ObjectMapper = 서비스요소VO_01.equals(서비스요소VO_ObjectMapper);
    System.out.println("is서비스요소VO_ObjectMapper : " + is서비스요소VO_ObjectMapper);
 }

ObjectMapper 객체를 생성하고 사용한다.

서비수요소VO_01.get파라메터() : F00=001003
서비스요소VO_ObjectMapper.get파라메터() : F00=001004

 

 


2-6. MapStruct 

MapStruct을 사용하기 위해서는 Maven Repository에서 의존성 주입을 해아 한다.

<dependency>
	<groupId>org.mapstruct</groupId>
	<version>1.5.3.Final</version>
</dependency>

lombok과 같이 사용하는 경우 다음과 같이 pom.xml을 수정 해야 한다.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-compiler-plugin</artifactId>
	<configuration>
		<annotationProcessorPaths>
			<path>
				<groupId>org.mapstruct</groupId>
					<artifactId>mapstruct-processor</artifactId>
						<version>1.5.3.Final</version>
			</path>
			<path>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok-mapstruct-binding</artifactId>
				<version>0.2.0</version>
			</path>
			<path>
				<groupId>org.projectlombok</groupId>
					<artifactId>lombok</artifactId>
					<version>1.18.24</version>
			</path>
		</annotationProcessorPaths>
	</configuration>
</plugin>

mapper interface를 만든다.

@Mapper
public interface CopyMapper {
    CopyMapper copyMapper =  Mappers.getMapper(CopyMapper.class);
    SvcFtrVO toSvcFtrVO(SvcFtrVO fromSvcFtrVO);
}

Mapstruct 인터페이스는 compile과정에서 구현체가 자동으로 생성된다, 

public void MapStructCopy() {
	SvcFtrVO svcFtrVO = SvcFtrVO.builder()
                .이벤트코드("ACT")
                .요소코드("FTR000001")
                .파라메터("F00=001003")
                .build();

	SvcFtrVO svcFtrVO_01 = CopyMapper.copyMapper.toSvcFtrVO(svcFtrVO);
	svcFtrVO_01.set파라메터("F00=001004");
	boolean isSvcFtrVO_01 = svcFtrVO.equals(svcFtrVO_01);
	System.out.println("isSvcFtrVO_01 : " + issvcFtrVO_01);
    }

Mapstruct 인터페이스를 생성한 후 생성된 인터페이스(컴파일 시 구현채 생성됨)를 사용해서 변수애 할당 한다.,

svcDtrVO.get파라메터() : F00=001003
svcDtrVO_01r.get파라메터() : F00=001004