IT/CS 공부

[CS] 방어적 복사(Defensive Copy)

박소민 2025. 5. 22. 10:27
방어적 복사(Defensive Copy)란?
: 외부에서 전달된 객체나 컬렉션이 내부 데이터에 영향을 주는 것을 막기 위해 복사본을 만들어 사용하는 방법

이 방식은 특히 객체 간 데이터 공유로 인해 발생할 수 있는 예기치 못한 버그나 사이드 이펙트(부작용)를 방지하는 데 유용

왜 필요할까?
자바에서는 객체나 컬렉션을 다른 클래스에 전달하면, 그 참조값(주소)이 공유되기 때문에
외부에서 값을 바꾸면 내부 데이터도 바뀌는 문제가 발생할 수 있습니다.

이를 막기 위해 전달받은 값을 복사해서 사용하는 것이 방어적 복사

방어적 복사를 사용하는 대표적인 시점
  • 생성자에서
    • 외부에서 넘겨준 값을 객체 내부에 저장할 때, 원본을 직접 사용하지 않고 복사본을 저장
public class Lotto {
    private final List<Integer> numbers;

    public Lotto(List<Integer> numbers) {
        this.numbers = new ArrayList<>(numbers); // 복사본 저장
    }
}

// 이렇게 하면 외부에서 넘겨준 numbers 리스트를 변경해도, 내부의 numbers는 영향을 받지 않습니다.

 

 

  • Getter 메서드에서
    • 내부 데이터를 외부로 전달할 때도, 복사본을 반환합니다.
public List<Integer> getNumbers() {
    return new ArrayList<>(numbers); // 원본이 아닌 복사본 반환
}

// 이렇게 하면 외부에서 리스트를 받아 변경해도, 내부 데이터는 안전하게 보호됩니다.

 

 

실전 예제와 문제점
public class Lotto {
    private final List<LottoNumber> numbers;

    public Lotto(List<LottoNumber> numbers) {
        validateSize(numbers);
        this.numbers = new ArrayList<>(numbers);  // 방어적 복사
    }
}
  • 표면적으로는 잘 작성된 코드처럼 보이지만, 다음 두 가지 문제가 존재
문제 1 ) 얕은 복사 : 복사가 얕게 이루어져 내부 객체는 여전히 공유됨

- numbers 리스트는 복사했지만, 리스트 안에 있는 객체는 복사하지 않았기 때문에
외부에서 LottoNumber 객체를 수정하면 Lotto 내부의 값도 같이 바뀝니다.

 

  • 해결 방법
    1. LottoNumber를 불변(Immutable) 객체로 만들기
      • 불변 객체 : 한 번 생성되면 내부 상태가 절대 바뀌지 않는 객체
아래 코드
- final 클래스: 상속을 막아 불변성 유지
- 필드에 final: 생성 후 값 변경 방지
- setter 없음: 외부에서 값 수정 불가능
public final class LottoNumber {
    private final int number;

    public LottoNumber(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }

    // 변경 메서드 제거! setNumber 같은 메서드가 없어야 불변
}

 

2. 깊은 복사(Deep Copy)

// 깊은 복사 구현 예시
public class LottoNumber {
    private int number;

    public LottoNumber(int number) {
        this.number = number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }

    // 깊은 복사를 위한 복제 메서드
    public LottoNumber copy() {
        return new LottoNumber(this.number);
    }
}
// Lotto 생성자에서 깊은 복사 사용
public class Lotto {
    private final List<LottoNumber> numbers;

    public Lotto(List<LottoNumber> numbers) {
        // 리스트 복사 + 각 요소도 복사
        List<LottoNumber> copy = numbers.stream()
            .map(LottoNumber::copy)
            .collect(Collectors.toList());

        this.numbers = copy;
    }
}

 

 

문제 2 ) 검증 전에 외부에서 리스트를 변경할 수 있음

- validateSize() 호출 직후에 외부에서 numbers 리스트를 변경할 수 있습니다.
그 결과, 검증은 통과했지만 실제 저장된 값은 유효하지 않을 수 있습니다.
public Lotto(List<LottoNumber> numbers) {
    validateSize(numbers);               // 검증 통과
    this.numbers = new ArrayList<>(numbers);  // 그 사이 외부에서 리스트를 바꿈
}
  • 해결 방법
    • 방어적 복사를 먼저 수행한 후 검증을 진행
public Lotto(List<LottoNumber> numbers) {
    List<LottoNumber> copy = new ArrayList<>(numbers);
    validateSize(copy);
    this.numbers = copy;
}

 

'IT > CS 공부' 카테고리의 다른 글

[CS] URI, URL, URN  (1) 2025.05.28
[CS] CPU 스케줄링  (0) 2025.05.28
[CS] Call By Value와 Call By Reference  (0) 2025.05.22
[CS] Redis가 싱글 스레드로 동작하는 이유  (0) 2025.05.20
[CS] CAP 정리  (0) 2025.05.15