[3.5] 방어적 복사

Always·2025년 3월 5일
1

매일메일

목록 보기
54/69

다음과 같은 코드가 있다.


import java.util.*;
import java.io.*;
public class LottoTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<LottoNumber> list=new ArrayList<>();
		LottoNumber ln1=new LottoNumber(1);
		LottoNumber ln2=new LottoNumber(2);
		LottoNumber ln3=new LottoNumber(3);
		list.add(ln1); list.add(ln2); list.add(ln3);
		Lotto lotto=new Lotto(list);
		System.out.println("before");
		System.out.println(lotto.getLotto().toString());

		System.out.println("after");

		list.get(2).setValue(10000);
		System.out.println(lotto.getLotto().toString());

	}

}

해당 코드에서는 lottoNumber로 만들어진 리스트를 만들고 이를 이용해서 주입해서 lotto객체를 만들었다.
이 이후 lottoNumber를 변경하면 lottoNumber를 주입해서 만든 Lotto객체 역시 바뀌어있다.


before
[LottoNumber [value=1], LottoNumber [value=2], LottoNumber [value=3]]
after
[LottoNumber [value=1], LottoNumber [value=2], LottoNumber [value=10000]]

이를 막기 위해서 우리는 방어적 복사를 이용한다.

방어적 복사

방어적 복사란

방어적 복사란 원본과의 참조를 끊은 복사본을 만들어서 주입하거나, 반환하는 방식으로, 위처럼 원본의 변경에 의한 예상치 못한 side effect를 방지해서 개발자가 의도한 코드를 짤 수 있다.

주로 생성자에서 필드를 초기화할 때와, getter메서드에 필드를 반환 할 때, 직접 값을 이용해서 복사본을 만들어서 사용한다.

컬렉션 자료구조를 반환하는 경우라면 자바에서는 Unmodified Collection을 이용해서, 변경시 예외를 발생시킬 수 있다.



    public Person(List<String> friends) {
        // 원본을 복사하고, 불변 리스트로 변환
        this.friends = Collections.unmodifiableList(new ArrayList<>(friends));
    }

위 코드에서 방어적 복사 적용

기존의 Lotto 코드는 다음과 같다.

import java.util.ArrayList;
import java.util.List;

public class Lotto {
	private List<LottoNumber> lottos;
	public Lotto(List<LottoNumber> lottos) {
		this.lottos=lottos;
	}
	public List<LottoNumber> getLotto(){
		return this.lottos;
	}

}

이 코드는 생성자에 주입할 때도 참조가 되고, getter를 통해서 반환할 때도 참조를 반환해서 lottos를 변경시 Lotto객체 내부의 lottos역시 함께 변한다.
이 문제를 해결하기 위해서 다음과 같이 코드를 짤 수 있다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Lotto {
	private final List<LottoNumber> lottos;
    public Lotto(List<LottoNumber> lottos) {
        // 리스트 내부 객체까지 복사 (깊은 복사)
        List<LottoNumber> copiedNumbers = new ArrayList<>();
        for (LottoNumber number : lottos) {
            copiedNumbers.add(new LottoNumber(number.getValue())); // 새로운 LottoNumber 생성
        }
        this.lottos = Collections.unmodifiableList(copiedNumbers); // 불변 리스트로 변환
    }

    public List<LottoNumber> getLotto() {
        return lottos; // 불변 리스트 반환
    }
}

내부는 깊은 복사로 참조를 막고, 외부는 unmodifiedList를 사용하는 방법이다.
그냥 변경되면 안되는 컬렉션이면, final키워드를 사용하는 게 훨씬 낫지 않나 라는 개인적인 생각이다.
하지만 만약 변경이 필수적이고 그 내부에 컬렉션을 주입한다면, 위처럼 직접 복사를 이용하는게 맞다고 보여진다.


public class LottoNumber {
	final int value;

	public LottoNumber(int v) {
		this.value=v;
	}
	public int getValue() {
		return value;
	}


	@Override
	public String toString() {
		return "LottoNumber [value=" + value + "]";
	}
}

참고

public class Lotto {
	private final List<LottoNumber> lottos;
	public Lotto(List<LottoNumber> lottos) {
		this.lottos=new ArrayList<>(lottos);
	}
	public List<LottoNumber> getLotto(){
		return new ArrayList<>(lottos);
	}
}

이 코드는 리스트 내부에 있는 값들에 대한 복사가 이루어지는 것이 아니라서 완전한 방어적 복사라고 보기는 힘들다.

profile
🐶개발 블로그

0개의 댓글