메서드- 규칙 39 필요하다면 방어적 복사본을 만들라.

JAVA/Effective Java|2018. 5. 29. 23:25

포인터를 사용하지 않아 잘못된 메모리 접근으로 다른 메모리 영역에 데이터를 건드려서 beffer overrun(오버플로우)등의 오류는 자바에서는 발생하지 않는다.

하지만 아무리 안전한 언어를 사용한다고 해도, 스스로 노력하지 않는 경우, 클래스의 클라이언트가 불변식(invariant)을 깨버릴 수 있기 때문에, 조금 더 방어적인 프로그래밍을 해야 한다.

예를 통해서 확인해보자.

결재정보를 담고 있는 BillTimeObj라는 클래스는 결재 날짜 정보를 가지고 있는 Date 클래스를 생성자를 통해서 전달받는다.
BillTimeObj 클래스는 final로 선언되어 있어서 변경되지 않을 것 같아 보이지만 생성자로 전달되는 Date 객체가 변경이 가능하기 때문에 이는 불변식이 깨져버린다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package mehod39;
 
import java.util.Date;
 
public final class BillTimeObj {
    private Date payTime;
 
    public BillTimeObj(Date payTime) {
       if (payTime.compareTo(new Date()) > 0) {
            new IllegalArgumentException("현재 시간보다 빠를 수 없습니다.");
        }
        
        this.payTime = payTime;
    }
 
    public Date getPayTime() {
        return payTime;
    }
 
    public void setPayTime(Date payTime) {
        this.payTime = payTime;
    }
    
    public static void main(String args[]) {
        Date payDate = new Date();
        BillTimeObj p = new BillTimeObj(payDate);
        payDate.setTime(12321);
    }
 
}
cs



이런 문제를 예방하기 위해서 생성자로 전달되는 변경 가능 객체를 반드시 방어적으로 복사해서 그 복사하도록 해야 한다.

아래 코드와 같이 전달 받은 Date 객체의 정보를 이용하여 새로운 Date 객체를 만들어서 전달된 객체의 변경의 요지를 미리 차단해야 한다.


1
2
3
4
5
6
7
8
9
public BillTimeObj(Date payTime) {
        
        this.payTime = new Date(payTime.getTime());
        
        if (payTime.compareTo(new Date()) > 0) {
            new IllegalArgumentException("현재 시간보다 빠를 수 없습니다.");
        }
        
    }
cs



하지만 주의할 점이 몇가지 있다.

첫 번재 사례는 다음과 같다.
- 생성자에서 진행하는 인자의 유효성 검사는 방어적 복사본을 만든 후에 진행해야 한다.
이는 자연스러워 보이지 않을 수도 있다. 하지만 이렇게 진행하는 이유는 인자를 검사한 직후 복사본이 만들어지기 직전까지의 시간 사이(window of vulerablity)에 다른 스레드가 인자를 변경해 버리는 경우가 있어 이를 방지하기 위해서 이다.

두 번재 사례는 다음과 같다.
- 만약 생성자를 통해서 전달된 데이터가 변경가능한 객체일 때 이를 복사하기 위해서 clone() 메소드를 호출하고 싶을 때는 해당 클래스가 final로서 추가적으로 상속가능한 클래스가 아니어야 한다. 그 이유는 추가적으로 상속할 수 있는 경우 해당 객체의 clone()메소드를 실행 했을 때, 해당 객체가 반환된다고 보장할 수 없기 때문이다.



1
2
3
4
5
6
ex) 
private Animal animal;
public Test(Animal animal) {
   this.animal = animal.clone(); // 이렇게 진행할 경우 전달 받은 객체가 Animal객체가 아니라 이를 
                                                    // 상속한 객체의 clone이 실행될 수 있다.
}
cs




생성자 이외에도 접근자들에 대해서 방어적 복사본을 반환하도록 해야한다. 
왜냐하면 값이 변경될 여지가 있기 때문이다.



1
2
3
public Date getPayTime() {
    return new Date(payTime.getTime());
}
cs



하지만 이런 방어적 복사본을 만들면 성능에 어느 정도 손해를 볼 수 있기 때문에, 적절하지 않을 수도 있다. 만약 클라이언트에서 정말로 변경할 일이 없다고 판단이 된다면, 방어적 복사본을 꼭 만들지 않아도 된다. 하지만! 꼭 클래스 문서에 명시해 주어야 한다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙39 인용.

댓글()

모든 객체의 공통 메서드 - 규칙 11 clone을 재정의할 때는 신중하라

JAVA/Effective Java|2018. 5. 29. 22:25

Cloneable 객체의 복제를 허용한다는 사실을 알리는데 쓰려고 고안된 인터페이스이다.
 
하지만
 
Cloneable 선언된 메서드가 없는 마커 인터페이스로,
 
상위 클래스의 protected 멤버가 어떻게 동작할지 규정하는 용도로 쓰인다.
 
또한 
 
객체의 cloneable 구현하면, Object clone 메서드는 해당 객체를 필드 단위로 복사한 객체를 반환한다.
 
Cloneable 구현하지 않았다면, clone 메서드는 CloneNotSupportedException 던질 것이다.
 
 
Java API 문서에 기재된 clone 메서드의 명세는 다음과 같다.
 
 객체의 복사본을 만들어서 반환한다.
 - "복사" 정확한 의미는 클래스마다 다르다.
 일반적으로 x.clone() != x  참이다.
 - x.clone().getClass() == x. getClass() 반드시 참인 것은 아니다.
 - x.clone().equals(x) 또한 반드시 참인 것은 아니다.
 
=> 객체를 복사하면 보통 같은 클래스의 새로운 객체가 만들어지며내부 자료 구조까지 복사되지만 어떠한 생성자도 호출되지 않는다.
 
일반적으로 clone 정상적으로 동작하려면 해당 객체의 상위 클래스에 public 또는 protected clone 메서드가 정의되어 있어야 한다.



1
2
3
4
5
6
7
8
@Override
public Test clone() {
    try {
        return (Test) super.clone();
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
cs



그러나 만약 clone에서 복제하는 항목 중에 변경 가능한 객체에 대한 참조 필드가 있을 경우,
 
이런 필드는 복사본 또는 객체의 값을 변경할 경우 다른 객체의 상태 또한 변경되기 때문에
 
이는 여러 문제를 야기할  있다.

문제상황



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Test 객체 클래스
public class Test implements Cloneable {
    private int a;
    private int b;
    private String c;
    private List<String> list;
}
 
// Test 클래스의 재 정의 메서드 clone
@Override
public Test clone() {
    try {
        Test clone = (Test) super.clone();
        return clone;
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
 
 
// 참조필드가 있는 경우 얕은 복사로 인해 같은 값이 변경되는 문제
public static void main(String argsp[]) {
    Test a = new Test();
    
    Test cloneA = a.clone();
    cloneA.getList().add("test");
    System.out.println(printData(a.getList()));        
    System.out.println(printData(cloneA.getList()));
}
cs



그렇기 때문에 다음과 같이 복제를 하여 얕은 복사가 아닌 깊은 복사를 진행하여
 
복제된 객체와 원래 객체가 서로 다른 객체로 분리되어 복사가 진행되어야 한다.


해결된 상황



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 깊은 복사를 이용한 방법
@Override
public Test clone() {
    try {
        Test clone = (Test) super.clone();
        List<String> temp = new ArrayList<>();
        temp.addAll(list); // 또는 (List<String) list.clone()
        clone.list = temp;
        return clone;
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
 
 
public static void main(String argsp[]) {
    Test a = new Test();
    
    Test cloneA = a.clone();
    cloneA.getList().add("test");
    System.out.println(printData(a.getList()));        
    System.out.println(printData(cloneA.getList()));
}
 
public static String printData(List<String> list) {
    StringBuilder stb = new StringBuilder();
    for (String st : list) {
        stb.append(st);
    }
    
    return stb.toString();
}
 
 
결과 
ddaacc
ddaacctest
cs




Cloneable 재정의  경우에는 Object.clone() 기존 방법을 그대로 계승하여야 하며,
다중스레드에 안전해야 하는 클래스는 clone 동기화 매커니즘을 만들어야 한다.
 
그렇지만 이렇게 구태여서 힘들게 clone 메서드를 구현할 필요가 있는지부터 생각해  필요가 있다.
 
객체를 복사할 대안을 제공하거나아예 복제기능을 제공하지 않는 것도  다른 방법일 수도 있다.
 
예를 들어 변경 불가능한 클래스는 객체 복제를 허용하지 않거나객체 복제를 제공하는 복사 생성자나 복사 팩토리를 제공하는 것이  나을 수도 있다.
 
왜냐하면
 

만약 제대로된 clone 메서드를 정상적으로  정의하여 사용하지 못할 경우에는 더욱 큰 문제를 야기 할 수 있기 때문이다.

그러므로 

Cloneable 인터페이스를 계승하지 말고 복사생성자나 복사 팩토리를 이용하여 복사 개념을 사용하는 것이  현명할  있다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙11 인용.


댓글()

git사용을 위한 ssh-keygen 설정

IT 지식/Git|2018. 5. 27. 21:26

git을 사용하여 repository에 커밋하고 pull 받기위해서는 ssh 연결이 필요하다.

이 과정에서 사용되는 keygen 설정을 진행해보자. 



[진행방법]


1. git을 먼저 설치한다.
2. window의 경우 git bash를 실행시키고, mac인 경우 terminal을 실행 시킨다.
3. ssh-keygen -t rsa -C "git계정"을 입력하고 계속 엔터를 입력한다.
4. 사용자 위치에 생성된 public key를 복사해서 git 관리 홈페이지에 ssh 부분에 입력한다.

댓글()