레이블이 java인 게시물을 표시합니다. 모든 게시물 표시
레이블이 java인 게시물을 표시합니다. 모든 게시물 표시

2017년 5월 17일 수요일

effective java 규칙 11. clone을 재정의할 때는 신중하라

객체의 복사기능을 제공 하려면 Object 의 clone method 를 재정의 해야한다.
Object 의 clone 메서드는 protected 로 제한되어 있다. 그리고 모든 class 는 object 를 상속받는다. 
따라서 모든 class 는 자기자신을 복제할 수 있다 -cloneable implements 시-. 하지만 이렇게 구현된 클래스를 사용하게되는 클라이언트 코드에 구현한 클래스의 clone 을 제공 하는 것은 좀더 까다로운 규칙이 존재한다.
여기서 제공이 의미하는것은 어떠한 class 를 구현하고 그 class 를 사용하는 클라이언트 코드에서 그 class 로 생성된 객체의 복사의 기능을 제공 한다는 의미이다.

위에서 언급 되었듯이 Object  의 clone 은 protected 이다. 따라서 클라이언트 코드에서는 (복사를 제공할) 클래스의 clone method 를 호출할 수 가 없다.

그렇기 때문에 복사기능을 제공할 클래스에선 Object 의 clone 메소드를 재정의(override) 해야 하며
재정의시 접근 제한자를 public 으로 변경해야한다. (override 시 더 넓은 허용범위로의 변경은 허용되지만 그반대는 불가)

단순히 이렇게 clone 을 override 하고 그 override 한 메소드에서 object 의 clone 메소드를 호출하게 하면
해당 객체는 CloneNotSupportedException 을 throw 한다.
그 이유는 Object 의 clone 은 native 메서드이고 이 메서드가 실행 되려면 대상 클래스가 Cloneable 이라는 믹스인 인터페이스 - 선택적 기능을 제공한다는 선언 - 로 복제를 허용한다는 사실을 알려야 한다.
다만 재정의한 class 에서 super.clone() 으로 object 의 clone 을 사용하지 않고 생성자로 객체를 생성해 복제와 유사한 효과를 내어 생성된 객체를 반환한다면 - clone 의 일반규약에는 위배되지만 - Cloneable 을 생략할 수 있다.

즉 class 에서 clone method 를 제공하려면
1. Cloneable interface 를 구현 해야하며 - mixin interface (주 기능 이외의 선택적 기능의 제공자로 선언) 로서의 역할
      - Cloneable 선언하지 않을 경우 CloneNotSupportedException 을 throw 한다.
2. Object.clone() 을 Override 해야 한다.
      - 이때 override clone method 의 접근제한자는 public 으로 변경해야만 상속구조 이외의 class (예 : client class) 에서
      clone 메소드를 사용할 수 있다 - Object.clone() 의 접근제한자는 protected 이므로 -
      - super.clone() 호출

[sample]

/**
* Created by fall1999y on 2017. 5. 12. 오전 6:26
* <pre>
* <b>Description</b>
*
* class 에서 clone method 를 제공하려면
* 1. Cloneable interface 를 구현 해야하며 - mixin interface (주 기능 이외의 선택적 기능의 제공자로 선언) 로서의 역할
* - Cloneable 선언하지 않을 경우 CloneNotSupportedException 을 throw 한다.
* 2. Object.clone() 을 Override 해야 한다.
* - 이때 override clone method 의 접근제한자는 public 으로 변경해야만 상속구조 이외의 class (예 : client class) 에서
* clone 메소드를 사용할 수 있다 - Object.clone() 의 접근제한자는 protected 이므로 -
* - super.clone() 호출
*
* Object clone 의 일반규약
* 1. x.clone != x
* 2. x.clone().getClass() == x.getClass() - 필수 조건은 아님 ( override 시 하위 클래스를 return 할 수 도 있다. )
* 3. x.clone().equals(x) - '값'의 동일성을 의미함, 필수 조건은 아님 ( 기본적으로 object 의 equals 는 객체 주소를 비교하고 재정의시 구현에 따라 참이 아니게 될 수 도 있다.)
* 4. 어떤 생성자도 호출되지 않는다. - final class 에서는 생성자를 호출해도 무방하다.
* </pre>
*/
public class CloneSample implements Cloneable {

private String var;

public CloneSample(String var) {
this.var = var;
}

/**
* Object 의 clone method 의 return type 은 Object 이지만
* override method 의 return type 이 Object 가 아닌 class 로 형변환하므로
* 클라이언트에서 형변환하는 수고를 덜 수 있다.
* - 라이브러리가 할 수 있는 일을 클라이언트에게 미루지 말아야 더 좋은 코드
*
* * override 시 접근제한자의 더 넓은 허용범위로 변경은 허용된다.
* * override 시 return type 은 하위 클래스로 변경 가능하다 (여기서는 Object -> CloneSample 로 return)
* - jdk 1.5 이후
* - 제네릭의 공변 반환형 (covariant return type)
* - override(재정의) 메서드의 반환값 자료형은 재정의되는 메서드의 반환값 자료형의 하위 클래스가 될 수 있다.
* - 아래와 같은 코드로 현재 클래스의 하위 자료형을 return 할 수 도 있다
* return (CloneSampleExt) new CloneSampleExt("a");
*
* @return
* @throws CloneNotSupportedException
*/
@Override
public CloneSample clone() throws CloneNotSupportedException {
// 재정의할 때는 반드시 super.clone 을 호출해 얻은 객체를 반환해야 원하는 클래스의 객체가 생성
// 상속구조의 모든 클래스가 super.clone 을 호출한다면 최종적으로 Object 의 clone 메서드를 호출하게 됨
return (CloneSample) super.clone();

// 아래 코드는 clone 메서드의 일반 규약 '어떤 생성자도 호출되지 않는다' 에 위배되지만 - final class 에서의 재정의에는 - 충분히 실행 가능한 코드
// 그렇지만 비 final class 에서 clone 을 재정의할 때는 반드시 super.clone 을 호출해 얻은 객체를 반환 해야해야만 이후에 상속받는 class 에서
// clone override 코드에서 super.clone 을 했을때 현재 클래스 객체가 반환될 거라는 가정을 충족 시킬수 있다.

// return (CloneSampleExt) new CloneSampleExt("a");
}

public static void main(String[] args) throws CloneNotSupportedException {
CloneSample c = new CloneSample("test");

Object c1 = c.clone();

System.out.println(c1);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("var", var)
.toString();
}
}


clone 를 override 시 지켜야 할 일반 규칙은 아래와 같다.

1. x.clone != x
2. x.clone().getClass() == x.getClass() - 필수 조건은 아님 ( override 시 하위 클래스를 return 할 수 도 있다. )
3. x.clone().equals(x) - '값'의 동일성을 의미함, 필수 조건은 아님 ( 기본적으로 object 의 equals 는 객체 주소를 비교하고 재정의시 구현에 따라 참이 아니게 될 수 도 있다.)
4. 어떤 생성자도 호출되지 않는다. - final class 에서는 생성자를 호출해도 무방하다.

만약 클래스의 모든 필드가 기본 자료형이거나 변경 불가능 객체 참조라면 위와 같이 구현하기만 하면 된다.

하지만 참조 변수가 있을때는 좀더 복잡한 과정이 필요하다.
복잡한 과정을 살펴보기전에 다음을 이해해야 한다.
"clone 메서드는 또 다른 형태의 생성자이며 이 메서드로 인해 원 객체를 손상시키면 안되고 복사본의 불변식도 제대로 만족시켜야 한다.”
effective java 에선 위와 같이 clone 을 정의 하고 있으며 위 정의를 만족하는 clone 메서드를 재정의하라고 한다.
참조변수는 reference 주소”값"이 저장되어 있고 기본자료형은 “값” 이 저장되어 있다.
object clone 은 값을 복사하는 native method 이며 참조변수가 가지고 있는 값인 주소를 복사 - Shallow Copy - 하는 object clone 이 동작하는 방식이 이치에 어긋나지 않다. 

하지만 책에서의 정의를 만족하려면 - 우리가 기대하는 복사라는 기능을 만족하려면 - 
실제 값의 복사 - deep copy - 과정이 필요하다.
따라서 배열이나 객체를 담고 있는 변수는 clone override 시 추가적인 작업이 필요하다.



private Object[] elemetns;
private List list;
private ArrayList array;

@Override
protected Object clone() throws CloneNotSupportedException {
CloneTest obj = (CloneTest) super.clone();
// 참조형 변수를 위한 추가적인 작업
obj.elemetns = elemetns.clone();
obj.array = (ArrayList) array.clone();

// List 는 interface 이기 때문에 clone 메서드가 없다. 따라서 아래와 같은 코드는 불가능하다
// obj.list = list.clone();
// 아래와 같이 해줘야 한다
obj.list = (List) ((ArrayList) list).clone();

return obj;
}

그리고 직접구현한 배열형 자료구조의 원소가 객체형일때는 각 원소도 copy 를 지원할 수 있도록 만들어야 한다. - deep copy

이렇게 clone 의 대해 알아봤다. 하지만 effective java 에선 이러한 복잡한 clone 메서드가 정말 필요한지 반문한다.
책의 결론은 차라리 객체를 복사할 대안을 제공하거나 아에 복제 기능을 제공하지 말라고 조언한다.
책에서 제시하는 clone 의 대안은
복사 생성자
public Yum(Yum copy) {
    this.some1 = copy.some1;
    ...
}
또는 복사 팩토리 메서드를 제시하고 있다.
public static Yum newInstance(Yum yum) {
    ...
}

위 방법으로 언어 외적 객체 생성수단 (native method) 에 의존하지 않으며 여러 제약 조건에 자유로워 진다.

결론은
Cloneable 을 계승하는 인터페이스는 만들지 말아야 하며, 계승 목적으로 설계하는 클래스는 Cloneable을 구현하지 말아야 한다.

 

2017년 5월 10일 수요일

effective java 규칙 10. toString 은 항상 재정의하라

규칙을 설명하기 전에 toString 의 목적을 알아보자.
toString 메서드의 목적은 객체의 정보를 문자화 시키는데 있다.
여기서 객체의 정보란 인스턴스 변수에 저장된 값을 뜻한다.

우리는 이러한 toString() 를 사용함으로써 logging 등에 유용하게 사용할 수 있다.
이외의 용도는 논의 할 필요성이 있다. 
임의의 비즈니스 로직에서 객체의 toString 결과를 사용하는 코드가 필요한지는 의문이 든다. 
(비즈니스를 처리하는 로직은 별도의 메소드를 만들어 처리해야 할 것 같다.)


이렇게 객체의 정보를 문자화 시키는 Object 클래스의 toString public method 는 다음과 같이 정의되어 있다.

public String toString() {
    return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());}

기본 결과 형태가 나타내는 정보는 위의 정의에서 나타나듯이 해당 클래스의 이름 + @ (구분자) +  hashCode 형태이다.
이 결과로 얻을수 있는 유용한 정보는 객체의 클래스 명 정도이다.
hashCode 는 이전 규칙( rule 9) 에서 언급 했듯이 hash 알고리즘으로 계산된 결과이고
같은 객체에 대해선 같은 hashCode 가 보장되지만 다른 객체에선 항상 다른 hashCode 값이 보장이 되지 않으므로 객체의 식별자 역할을 하기에는 부족하다. (참고용 정도?)

그렇기 때문에 toString method 를 객체가 가지고 있는 인스턴스 변수를 표현하도록 override 해야 한다.

이쯤에서 toString() 의 일반규약을 알아보자.
1. 사람이 읽기 쉽도록 간략하지만 유용한 정보를 제공
2. 모든 하위 클래스는 이 메서드를 재정의함이 바람직하다.

위와 같은 일반규약을 가지고 있고 책(effective java) 에서는 객체 내의 중요 정보를 전부 담아 반환 (많을 경우 요약정보로 반환) 하는 것을 권장한다.

그리고 저자는 toString 구현시 그 포멧을 문서에 명시하는 것에 대해 설명하고 있고 
(예:bigInteger, BigDecimal 등 boxing class.. / 그리고 값 클래스는 형식 명시 권장함)
형식을 명시 하지 않을 경우에도 (명시 할 때도 마찬가지로) 그 의도는 문서에 남겨야 한다고 한다.
(이런 입장 역시 -내 생각과는 다르게- toString 의 활용처를 좀더 넓게 보고 있는 것이다.)

의도를 문서에 남기는 것은 로깅시에도 필요한 경우가 많기 때문에 동의할 수 있는 부분이다.
하지만 포멧을 지정한다는 (주석으로 명시 한다는 문제가 아니라 비즈니스 로직에 사용하기 위해 포멧을 지정 한다는) 것은
내 개발 스타일에는 맞지 않는 것 같다.

그리고 toString 이 반환하는 정보는 별도의 접근자 (method 등.. getter)를 가져야 한다.
그렇지 않으면 해당 클래스를 사용하는 프로그래머는 toString 을 파싱해서 정보를 가져오도록 시도할 것이다.
(toString()  이 api 화됨)
이런 시도는 성능상에 불이익 및 오류등 안전하지 못한 시스템을 만들 가능성이 높아진다.
- 생각해 볼 문제 : 
그렇다면 클라이언트 코드에서 사용하지 못하도록 private 선언한 변수는 toString 에서 그 정보가 나타나지 않도록 해야 하는가?
그 정보가 로깅에 중요한 정보라도? - 협업의 공통 rule 로써 관리해야 할 것 같다. toString 의 활용법 제한등

- toString method 호출
* print 관련 메서드에서 객체를 사용할 때  (println, printf 등..)
System.out.println(c);


* 문자열 연결 연산자의 피 연산자로 사용될 때
String c = "a"+t;


* assert 구문이 실행될때
※ assert는 특정조건이 참일 때만 프로그램이 계속된다.
이러한 assert 구문은 java 실행시 vm option 에 -ea 를 주어야 적용된다.

assert c==null : t;

* 디버거에 객체가 전달될때


위와 같은 경우에 객체의 toString 메서드가 자동 호출 된다.


2017년 5월 6일 토요일

effective java 규칙 9. equals를 재정의할 때는 반드시 hashCode 도 재정의하라.

Object 의 hashcode 

Object 의 hash code 는 heap 에 생성되는 객체의 주소(reference 주소)를 바탕으로 생성된다.
따라서 객체의 주소가 다르다면 (값은 같을 지라도) 같은 hash code 는 가질 수 없다.

hashcode 의 목적

객체를 구별하는데 그 목적이 있다.
구별이란 주소(reference)의 동치 판단의 의미와 의 동치 판단으로 구분된다.

객체를 구별하는데 그 객체가 저장된 주소 가 동일한 즉 실제로 똑같은 같은 메모리상 위치에 저장된 같은 객체인지를 판단하려면
Object 의 기본적인 hash code 를 사용하면된다.

그러나 객체의 중요필드의 값이 같다면 같은 객체라고 판단하는 비즈니스 로직이 있을 경우
hashcode 는 equals 메소드와 마찬가지로 override 되어야 한다.
그래야만 hashcode 메소드의 일반규약을 사용하는 클래스에서 제대로된 작동을 보장할 수 있다. (HashMap 이나 HashTable 등..)

hashCode() 의 일반규약

두 객체의 equals 의 결과가 true 이면 hashcode 는 동일
두 객체의 equals 의 결과가 false 이더라도 hashcode 는 항상 다르지는 않다. = 객체가 같지 않더라도 hashcode 는 동일할 수 있다
    - hashcode 란 계산된 결과(숫자) 이기 때문에 계산 알고리즘 (Hashing Algorith) 이 항상 완전한 해시 함수를 구현하지 않으면 
    hashcode 는 동일 할 수 있다.
    - Boolean 같이 서로 구별되는 객체의 종류가 적거나 Number 형 객체는 객체가 나타내려는 값 자체를 해시 값으로 사용할 수 있기 때문에 완전한 해시 함수 대상이 되지만 String 이나 pojo 에 대하여 완전한 해시 함수를 제작하는 것은 사실상 불가능
    - 그러나 거의 대부분 서로 다른 hashCode 값이 나오면 hashcode 를 사용하는 collections 에서 성능이 향상된다.

※ 완전한 해시 함수
x.equals(y) 가 false 일때 x.hashCode() != y.hashCode() 가 항상 성립할때

hashCode() override 시 가장 중요한 핵심 규약은 hashCode() 일반 규약의 첫번째 설명 이다.
-같은 객체는 같은 해시 코드값을 가진다.-

hashCode 지침
다음은 effective java 에서 제안하는 hashCode 지침이다.
   1. 초기값으로 0이 아닌 int 상수를 지정
  1. 중요 필드값을 number 형태의 결과로 나타날수 있도록 계산
    • boolean 일경우 f ? 1: 0
    • int 보다 범위가 작은 타입(byte,char,short,int)일 경우 (int) f 
    • long 일 경우 (int) (f ^ (f >>> 32))
    • float : Float.floatToIntBits(f) , double : Double.doubleToLongBits(f)
    • 객체참조 일때 hashCode 메서드를 재귀적으로 호출하여 계산, null 일 경우 0 반환
    • 배열일때 각 원소별 계산 후 result = 31 * result + c 로 결합 - c는 계산값 result 는 초기값(누산값)
      • Arrays.hashCode 메서드 가운데 하나를 사용 가능
    • 결합 : result = 31 * result + c 로 결합 - c는 계산값 result 는 초기값(누산값)
      • 31 의미 : 소수이면서 홀수 , 분포도를 좋게 하기 위해 소수를 사용, Mersenne prime
      • * 곱셈의 의미 : (String 해시함수가 곱셈이 없이 단순 누산이였다면 순서만 바뀐 문자열의 해시 코드가 동일해짐)
  2. result 반환
  3. 점검 - 동치 관계일때 같은 해시 코드 값인지 확인 및 테스트
  4. 중보필드는 계산과정에서 제외, 다른 필드에서 유도될 수 있는 필드 제외

그외 검토할 사항

변경불가능 객체의 hashCode 계산시 계산비용이 높다면 캐시에 저장할 필요성
    대부분의 객체가 해시 키로 사용된다면 객체를 생성할 때 해시 코드를 계산
    그렇지 않다면 hashCode 값의 초기화 지연 기법

성능 개선목적으로 중요 필드의 계산 과정을 누락시키면 안됨 
    - 해시값 품질이 저하되어 해시 테이블의 성능을 매우 떨어뜨림

샘플코드


private int a;
private int b;
private int c;

public HashCodeTest(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}

// 해시코드 초기화 지연 기법 사용시
private volatile int hashCode;
@Override
public int hashCode() {

int result = hashCode;
// 초기화 지연 기법 적용
if(result == 0) {
result = 17;
result = 31 * result + a;
result = 31 * result + b;
result = 31 * result + c;

hashCode = result;
}

return result;

}

2017년 5월 5일 금요일

effective java 규칙 8. equals를 재정의할 때는 일반 규약을 따르라

요약 : class 를 설계할때 Object class 의 비 final 메소드를 재정의 할때는 각 비 final 메소드들의 일반 규약에 따라 재정의 해야한다.
그래야 일반 규약으로 작동하는 class 와 같이 사용이 가능하며 규약을 따르지 않으면 정상적인 작동을 보장하지 않는다.
예를 들자면 Set 과 같은 경우 외부로부터 인자를 받아 내부 배열변수에 저장한다.
이때 중복된 인자는 저장 하지 않는다. 중복된 인자인지 여부를 equals 로 체크한다고 하면 (실제로는 hashCode() 로 비교하는듯하다.) 이 Set 타입의 객체는 add method 로 전달 받을 인자(외부에서 add 메소드 로 입력받은 인자)는 일반 규약을 따라 equals 가 재정의 되었다고 전제한 상태에서 add method 로 부터 들어온 인자를 Set 내부에 배열로 담겨진 기존 인자들과 일반 규약에 의거해 비교할 것이기 때문에 일반 규약을 따르지 않는 인자들의 정상적인 동작을 보장할 수 없다.

그래서 일반적으로 class 를 작성할때 Object 의 비 final 메소드를 재정의 한다.

그렇지만 object class 의 equals method 를 재정의 할 때 (재정의 자체가 쉬워보이는 것 만큼) 생각보다 실수할 여지가 많다.
그렇기 때문에 객체간의 동일성 비교가 필요하지 않을 경우엔 Object class 의 equals method 를 override 하지 않아도 된다.
재정의 하지 않을 경우 모든 객체는 자기 자신하고만 같게 된다.(object class 의 equals 의 기본규칙)

다음은 Object class 의 equals method 재정의가 불필요한 경우이다.

    1. 각각의 객체가 고유하다. 값을 나타내는 클래스가 아닌 활성 개체를 나타내는 클래스 일 경우
  • Thread class 같은 경우
  • 실행 단위를 표현하는 Thread 는 값이 동일성 비교가 필요없다. a thread 와 b thread 객체는 단지 실행단위이기 때문에 동일성 비교는 필요 없고 thread 는 고유한 단위이기 때문에 동일성비교는 object 의 equas 메소드의 일반 규약만으로도 충분하다.
  • 값의 비교가 필요없는 개체일 경우, 기능을 모아놓은 클래스 같은 경우
    2. 논리적 동일성 검사 방법이 있건 없건 상관없다. equals method 유무가 의미가 없다.
            java.util.Random class 같은 경우 equals 를 override 하여 생성된 난수값이 같은지를 비교할 순 있지만
            Random class 의 목적이 객체의 동일성 비교에 있지 않은 만큼 equals 비교는 무의미 하다.
            즉 class 설계자가 동일성을 비교할 필요가 없도록 의도하여 설계하였다면 equals 의 비교는 무의미 하다.
    3. class 가 private 으로 선언되어있거나 package-private 이면서 equals 를 호출할 일이 없다면 equals 의 override는 불필요하다.
            그래도 클라이언트가 의도와 다르게 equals 비교를 포함한 로직을 작성할 수도 있기에
            override equals 를 호출할때 exception 을 throw 하도록 구성하는 것을 권장

            @Override
            public Boolean eqauls(Object o) {
                throw new AssertionError(); // 호출되면 안 되는 메서드를 호출시 exception throw
            }  
        private class 는 내부 클래스에서만 지정 가능한 접근 한정자 이며 그렇지 않을 경우 compile 시점에 error 발생한다.
            내부 private class 에 구현로직을 만들어 외부 client 코드에서 사용할때는 내부 private class 에 
            접근할 방법이 없기 때문에 구현 로직의 캡슐화가 가능하며 
            외부 client 코드에서의 내부 클래스의 기능을 활용 하려 할 때는 
            내부 private class 를 싸고 있는 class 의 제공된 method 를 통해서만(api) 
            내부 private class 의 기능을 사용할 수 밖에 없기 때문에 
            내부클래스(로직을 구현하는 클래스)의 클래스 설계자의 의도에 맞지 않는 오사용을 막을 수 있다.

    4. 상위 클래스에서 재정의된 equals method 가 상속받은 하위 클래스에서 사용하기 적합하면 하위 클래스에서 다시 재정의 할 필요는 없다.
            대부분의 Set의 하위 클래스는 AbstractSet 의 equals를 그대로 사용
            (List, Map 도 동일 AbstractList, AbstractMap)

    5. 싱글턴 객체일때 - 싱글턴 클래스에서 생성할 수 있는 객체는 최대 한개 이기 때문에 Object 의 equals 로서 충분히 비교가 가능하다. 

이와같이 5가지 경우에는 재정의가 불필요하다.
5가지의 경우는 다음과 같이 3가지 경우로 요약이 가능하다.
  1. 동일성 비교가 필요 없거나 동일성 비교가 무의미한 클래스
  2. 상위 클래스의 equals 가 하위 클래스의 동일성 비교에 충분할 경우
  3. 싱글턴 객체를 생성하는 클래스
는 equals 의 override(재정의)가 불필요하다.

불필요한 경우를 뒤집어 생각한다면 필요한 경우를 유추할 수 있다.
즉 상반된 경우엔 Object class 의 equals 를 재정의를 해야만 한다.
보통 다음과 같은 경우에 equals method 를 override 한다.

    1. 논리적 동일성 검사가 필요한 경우
        객체의 동일성이 아닌 논리적인 동일성이 필요한 객체일때
    2. 상위클래스에서 재정의한 equals method 가 하위클래스에서 사용하기 어려울때

이런 조건에 부합하는 객체는 보통 값 객체이다.
Integer 나 Double 객체의 동일성은 객체 자체가 동일한지 여부가 중요한게 아니라 
값이 동일한지가 중요하기 때문이다. 즉 논리적 동일성 검사가 필요한 경우이다.
그렇지만 값 객체라 하더라도 singleton 객체이면 역시 equals override 는 필요없다.
무조건 단일 객체만 생성되는 Enum class(객체동일성이 곧 논리적 동일성) 가 그런 경우이다.

이렇게 논리적 동일성 검사가 재대로 정의된 값 객체는 Set 자료형에 인자로 전달 될 수 있는등 일반 규약에 따르는 클래스에서 사용할 수 있다. (만약 Set 객체에 전달된 인자 객체가 일반 규약을 따르지 않는다면 결과를 예측하기 어렵다.)

자 그럼 일반 규약에는 어떤것이 있는지 알아보자.

equals method 일반규약은 다음과 같이 5개의 항목으로 정의된다.
  1. 반사성 (자신과의 동치성 비교시 equal true)
  2. 대칭성 (a.equals(b) == true 이면 b.equals(a) == true)
  3. 추이성 (a.equals(b) == true, b.equals(c) == true 이면 a.equals(c) = true)
  4. 일관성 (equals 비교 횟수와 시점에 상관없이 항상 같은 결과)
  5. null 에 대한 비 동치성 (null 과의 equals 비교시 항상 false 반환 ex : a.equals(null) == false)

각 항목을 자세히 살펴보자.
equals method 의 일반규약 상세
1. 반사성 
    고의적으로도 깨트리기 어려운 규칙
    override 한 equals 메소드의 로직이 항상 false 를 반환하도록 하면 가능
2. 대칭성 
    a==b 이면 b==a
    비교하는 두 객체의 type 이 다를 경우 (equals method 의 parameter type 은 Object 이기 때문게 충분히 가능한 상황)
    a 객체에서는 b의 값을 casting 해서 값을 비교하는 로직이 있지만
    b 의 equals 로직에는 없을 때 false 가 발생 할 수 있다.
3. 추이성 
    상속구조의 클래스들 equals 비교에서 발생할 가능성이 있다.
    a 를 상속한 b타입의 b1객체 가 a와의 동치성을 비교하기위해  
    a의 속성만(b의 upcasting) 으로 비교해서 equals 가 true 가 되었고
    b타입의 b2 객체가 b1와 의 동치성을 비교할 때는 같은 b타입이기 때문에  b타입에만 있는 멤버변수까지 동치성 비교에
    사용하였다면 
    결국 a = b1 과 같고 a = b2 와 같을순 있지만 b1 != b2 가 될 수도 있다.
    일부 견해에서는 이러한 예외상황을 막기위해 비교하는 파라미터 타입을 좀더 엄격히 관리해 
    (interface of 대신 getClass()) 같은 타입(같은 클래스)이 아니면 false 를 리턴하도록 해야 한다는 주장도 있다.
    하지만 그와 같은 주장은 리스코프 대체 원칙에 위배되는 결과를 초래하기 때문에 받아들이기 힘든 견해이다.
    상위 자료형의 어떠한 메서드에서 상위 자료형을 계승한 자료형의 객체를 담는 컬렉션 변수가 있을때를 가정할때
    상위 자료형의 equals 가 getClass 로 파라미터 타입을 제한한다면
    컬렉션 변수의 contains 메소드는 잘 작동하지 못할 것이다.

    그렇다면 계승구조의 클래스들의 추이성을 보장하는 방법은 무엇일까
    불행하게도 객체 생성가능 클래스를 계승하여 새로운 값 컴포넌트를 추가하면서 추이성 규약을 어기지 않을 방법은 없다.
    effective java 에서 제시하는 대안은 클래스의 계승구조를 버리고 구성(composition) 하는 방식을 제안하고 있다.

    참고로 계승 구조에서 발생하는 추이성 문제에 해당하지 않는 상황은 부모클래스(계승모체)를 abstract 로 선언하는 경우이다.
    abstract 로 선언된 class 는 객체를 직접 만들수 없으므로 추이성 문제가 발생하지 않는다.
    

    ※ 리스코프 대체 원칙 (Liskov substitution principle)
    어떤 자료형의 중요한 속성은 하위 자료형에도 유지되어서 그 자료형을 위한 메서드는 하위 자료형에도 잘 동작해야 한다는 원칙

4. 일관성 
    일단 같다고 판정된 객체는 추후 변경되지 않는 한 계속 같아야 한다.
    즉 변경불가능 객체의 동치관계는 변하지 않는다.

    또한 변경 가능 여부와 상관없이 신뢰성이 보장되지 않는 자원들을 비교하는 equals를 구현하는 것은 삼가 해야 한다.
    즉 equals 메서드는 메모리에 존재하는 객체들만 사용해서 결정적 계산을 수행하도록 구현해야 한다.
    (java.net.URL 클래스와 같이 네트워크 상태에 따라 같은 결과가 나온다는 보장이 없는 클래스에는 equals 구현을 삼가)

5. null 에 대한 비 동치성
    equals 의 파라미터가 null 일 경우 false 를 반환해야 한다는 규약이다.
    이 규약의 예외상황은 NullPointerException 이다. 즉 false 을 반환하지 못하고
    예외를 throw 하기 때문에 일반 규약을 충족하지 못한다.
    단 equals 메소드에서 interface of 로 형 검사를 한다면 instance of 특성상 첫번째 피연사자가 null 이면 
    무조건 false 를 반환하기때문에 형검사 로직을 포함하면 null 에 대한 비 동치성 규약을 지킬수 있다.


이상으로 일반규약의 설명은 마무리하고
다음은 이런 일반 규약을 토대로 equals 를 재정의 할때 권장하는 지침에 대해 살펴보겠다.

equals 재정의 지침 

1. == 연사자를 사용하여 equals 의 인자가 자기 자신인지 검사 (동치성에 의거함)
    - 성능 최적화, 객체에 포함된 값 비교 없이 동치성 결과 반환
2. instance of 자료형 검사 (null 에 대한 비 동치성)
    - null 비 동치성에도 훌륭한 지침이지만 
    계승된 클래스 또는 같은 class가 아니라면 굳이 비교할 필요가 없기 때문에 성능상에 이점이 있다.
3. equals 의 인자를 정확한 자료형으로 변환
    - instance of 자료형 검사로 인해 형 변환은 반드시 성공
4. 중요 필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사
    - 기보자료형은 == 로 비교
    - float 와 double 은 compare 메소드를 사용해 비교 (Nan, -0.0f 같은 상수로 인해)
    - 배열은 모든 원소를 비교 - Arryas.equals 활용 및 참고
    - null 허용 필드 비교시 NullPointerException 발생 회피 
       (field == null ? o.field == null : field.equals(o.field))
    - 비교필드와 같을 때가 많을때
        (field == o.field || (field!=null && field.equals(o.field))) - 성능상의 이점
        equals 메서드 성능은 필드 비교 순서에도 영향을 받을 수 있다.
    - 객체의 요약정보를 담는 필드가 객체의 동일성을 보장할때 요약정보를 비교하는 로직으로 비교 비용을 줄일 수 있다.
5. 대칭성, 추이성, 일관성 검토 및 테스트
6. hashCode 재정의


sample code
@Overridepublic boolean equals(Object o) {
    // 동치성    if(o == this)
        return true;
    // 형검사    if(!(o instanceof Equals))
        return false;
    // 형변환    Equals l = (Equals) o;
    // 중요필드 비교    return l.d == d;}



    

2017년 4월 20일 목요일

effective java 규칙 7. 종료자(finalize() 메소드) 사용을 피하라

제목 그대로다.
가능한 종료자 사용을 피해야 한다.
명제에 대한 설명을 하기전에 종료자가 무엇인지 알아보자.

종료자(finalizer) 는 Object 클래스에 protected 로 정의된 메소드이며 모든 클래스에서 오버로딩(재정의) 가능한 메소드이고
객체가 gc 에 의해 소멸 되기 직전에 실행되는 특징을 가지고 있다.

java.lang.Object
protected void finalize() throws Throwable { }

client override
@Overrideprotected void finalize() {

}

위와 같은 특성으로 인해 객체의 사용이 종료되어 레퍼런스 참조값을 사라진다 하더라도 
객체는 gc 의 대상이 될 뿐 바로 gc 가 수집하여 삭제하지 않아 실행시점이 불투명하다. 
즉 예측이 불가능하고 실행 보장이 되지 않는다.
따라서 finalize 메소드 안에 객체 종료 시점에 꼭 실행되어야할 로직이 있다면 이 로직은  원하는 시점에 실행되지 않는다.
그렇기에 긴급한 작업을 종료자 안에서 처리하면 안된다.
추가로 예를 들자면 db 커넥션을 반환하는 코드나 file io 를 닫는 코드를 finalize() 메소드에 넣게 된다며 gc 가 대상을 수집하기전 까지 해당 로직이 실행되지 않기 때문에 한정된 connection 이 full 되어 오류를 내거나 file 의 경우 한 번에 열 수 있는 개수에 제한이 있어 이역서 오류를 낼 가능성이 커진다.

그리고 더욱 치명적인건 자바 명세에는 종료자가 즉시 실행되어야 한다는 문구도 없지만,
종료자가 반드시 실행되어야 한다는 문구도 없다는 점이다. 따라서 종료자가 실행되지 않은 객체가 남은 상태에서
프로그램이 끝나게 되는 일도 충분히 가능하다.

그리고 이러한 종료자의 더딘 실행(tardy finalization)은 성능상의 이슈도 발생 할 수 있다.
클래스에 종료자를 붙여 놓으면 드물게 객체 메모리 반환이 지연될 수 도 있고 프로그램 속도도 매우 느려질수 있다.
또한 종료자 안에서 예외가 발생하면 어떠한 경고 문구조차 출력되지 않아 디버깅이 어려워진다.

이렇게 종료자는 거의 모든 경우에 좋지 않다.

그렇다면 객체가 종료될때만 꼭 처리되어야 하는 로직은 어떻게 구현해야 할까?
그럴땐 명시적인 종료메서드 (예 : close() ) 를 새로 정의하고 객체가 종료되어야 될 시점에 해당 메소드 를 호출 하는 방식이 좋다
이런 명시적인 종료메서드는 클라이언트 코드에서 try-finally문 (1.7 이후 try-with-resources 대체가능)과 함께 사용되어 객체 종료를 보장한다. 

언뜻 백해무익해 보이는 종료자는 무조건 사용하면 안될것 같지만 그래도 사용에 적합한 케이스가 있다.
그 하나는 클라이언트 코드에서 명시적 종료 메서드 호출을 잊을 경우 보험용으로 모체에 finalization 을 구현하여 혹시나 있을 클라이언트의 실수에 대비 하는것이며 이때 finalization 에는 경고 메시지 로그를 남겨 어떤 클라이언트에서 이런 호출 (bug) 을 하는지 알려야 한다. 그렇지만 이런 안전망을 구현하려 할 때도 추가적인 비용을 감안하여 사용여부를 신중히 선택해야한다.
다른 하나는 네이티브 피어(네이티브객체)와 연결된 객체를 다룰때이다.

※ 종료자의 추가적인 특성 
종료자 연결이 자동으로 이뤄지지 않아 종료자가 선언되어 있는 클래스를 상속받은 클래스의 finalize() 메소드에는 상위 클래스의 finalize 메소드를 호출해야한다.



결론
자원 반환에 최종적 안전장치를 구현하거나 중요하지 않은 네이티브 자원을 종료시키는 것이 아니라면 종료자는 사용하지 말라야 한다.
그럼에도 불구하고 종료자를 사용해야 하는 드문 상황에는 super.finalize 호출을 잊지말자.
객체 종료시점에 실행되는 로직은 명시적인 종료 메서드에 정의하고 종료시점에 해당 메서드를 호출한다. (또는 try-finally문 활용)


effective java 규칙 6. 유효기간이 지난 객체 참조는 폐기하라

생애주기 동안 레퍼런스 참조가 없어진 변수는 gc 에 의해 제거된다.
예를 들면
String a = new String(“abc”);
에서 new String(“abc”); 으로 생성된 객체는 메모리의 heap 에 위치하게 된다.
생성된 객체는 a 라는 변수에 의해 참조되어지고 있기 때문에 gc 의 대상이 아니지만
a = null;  로 참조를 끊어버리면 new String(“abc”) 라는 객체는 gc 의 대상이 되어 제거된다.

그러나 자체적으로 메모리를 관리하는 코드가 있을때 의도치 않게 객체를 계속 보유하게 되는 경우가 있다.

public class StackTest {

// 저장공간 풀 레퍼런스 변수
private Object[] elements;
private int pointer = 0;

public StackTest() {
// 저장공간 풀 생성
elements = new Object[10];
}

public void push(Object o) {
elements[pointer++] = o;
}

public Object pop() {
return elements[--pointer];
}
}

위 코드와 같이 StackTest 생성자에는 저장공간을 관리하는 즉 자체적으로 메모리를 관리하는 elements 라는 변수가 있다.
위 객체를 stack 자료구조 의도로 사용했을때
pop() 메소드를 사용하면 마지막에 담긴 object 를 반환되어야 하고 반환된 객체는 이제 StackTest elements 에서 접근할 수가 없다.
하지만 gc는 여전히 해당 객체가 elements 에 담겨있으므로 (논리적으로 접근이 불가능하지만 elements 에 담겨져 있긴하다.)
gc의 대상으로 선정하지 않는다. 이문제를 해결하려면 pop 메소드 호출시 
element[pointer] == null; // pointer 위치의 객체 ‘참조' 값을 null 처리 함
이라는 만기참조 제거 를 명시적으로 해야 gc 는 방금 pop 에 의해 반환된 객체가 더이상 참조되어 지지 않는 것을 알게되 gc의 대상으로 선정한다.
이렇게 자체적으로 관리하는 메모리가 있는 클래스는 메모리의 누수가 발생하지 않도록 주의해야하며
예외적인 조치를 해야한다 (null 할당)

메모리 누수가 발생 할 가능성이 있는 또 다른 경우는
cache 를 구현 할 때이다.
객체참조를 cache 로 사용할 Map 등의 자료형에 넣어 놓고 잊어버리게 되면 - cache 메모리 관리 로직에서 빠지는등 - cache 의 생애주기 동안 map 에 담아놓은 객체는 gc 의 대상이 되지 않는다.
이런 경우를 해결할 수 있는 첫번째 방법은
캐시 바깥에서 키에 대한 참조가 만기 참조가 되는 순간 캐시에서 자동으로 삭제 되게 하는 전략으로
WeakHashMap 을 사용하는 방법이다.

public Map weakMap = new java.util.WeakHashMap();

/*
key 값을 "" 리터럴 스트링으로 생성하거나 기본자료형으로 생성하면
해당 값은 상수풀에 등록이 되어 있게 되고 gc 에 의해 지워지지 않기 때문에
WeakHashMap 의 만기참조시 삭제하는 전략을 사용할 수 없다
*/
// 외부에서 키를 관리한다고 가정한 변수
// private static String key = "a";
private static String key = new String("k"); // or // Integer key = new Integer(1);

public static void main(String[] args) throws InterruptedException {

WeakHashMapTest test = new WeakHashMapTest();

test.weakMap.put(key, 123);

test.someMethod();
key = null;
test.someMethod();

}

public void someMethod() throws InterruptedException {

for(int i =0 ; i<3; i++) {
Thread.sleep(500);
System.gc();
System.out.println(weakMap);
}

}
실행 결과
{k=123}
{k=123}
{k=123}
// 키의 참조값이 제거된 후 cache 에서 해당 키값을 가진 항목이 제거됨
{}
{}
{}

그리고 메모리 누수가 흔히 발견되는 또 한 곳은 callback 함수를 입력받는 서비스로 인해 발생한다.
클라이언트 코드에서 인자로 받은 callback 함수를 명시적으로 제거하지 않을 경우 메모리에 점유된 상태가 된다. 
이런 경우에도 콜백에 대한 약한 참조 (Weak reference)를 사용 하면 메모리 누수 문제를 해결 할 수 있다.
WeakHashMap의 키로 저장하는 것이 그 예이다.

public class CallBackTest {

public static void main(String[] args) {
SomeClass sc = new SomeClass();


SomeClass.CB callback = new SomeClass.CB() {
@Override
public void cbMethod() {
System.out.println("i'm callback function !");
}
};

sc.someMethod(callback);

// 명시적 제거(callback = null;)를 하지 않을 경우 메모리 누수 발생

// 따라서 callback 을 담을 WeakHashMap 을 생성하고 callback 을 담는다. 이때 키에 해당하는 값은 변수에 담고
// someMethod의 사용이 끝나면 키를 제거한다
}

}

class SomeClass {

interface CB {
void cbMethod();
}

public void someMethod(CB callback) {
callback.cbMethod();

}
}


2017년 4월 18일 화요일

effective java 규칙 5. 불필요한 객체는 만들지 말라

String 클래스는 잘못된 이해로 인해 불필요한 객체를 만들 가능성이 크다.
많이 알고들 있듯이 String 은 참조 자료형이다. 즉 값의 주소를 가지고 있는 변수이다.
따라서 개발자들이 String 변수를 생성할 때
String abc = new String(“abc”) 와  String abc = “abc” 의 객체 생성의 부담이 같다고 생각할 수 있다.
(물론 느낌상 new String 으로 생성하는게 더 자원을 많이 소비할 것 같긴하다.)

하지만 두 생성 방식은 전혀 다르게 동작한다.
new 로 생성하면 다른 인스턴스를 만들때 처럼 heap 메모리에 객체가 생성된다.
= “abc”  처럼 큰따옴표 방식으로 객체를 생성할 때도 heap 메모리에 객체가 생성된다.
heap 에 저장되는 것 자체는 두 방식 모두 동일하지만 큰 따옴표 방식은 heap 메모리 내에서도
특별한 영역에 값이 저장된다. 그 영역은 String Constant Pool 이라는 곳이고 
이곳에는 기존에 만들어진 문자열 값이 저장되어 있다.
따라서 “abc” 라는 문자열이 큰따옴표 방식으로 생성된 적이 있으면 
이 값은 String Constant Pool 영역에 저장되고 또다시 같은 값을 “abc” 라는 형식으로 생성한다면
기존에 생성된  “abc” 와 같은 레퍼런스를 가지게된다.

그러나 new String(“abc”) 로 String 객체를 생성할 때마다 heap 영역의 어딘가에 “abc” 값이 매번 새롭게 저장되어
새로운 레퍼런스를 반환하게된다.

즉 String 객체를 생성할 때 큰 따옴표 방식으로 생성해야만 동일한 문자열을 다시 String 변수에 할당할때
추가적인 객체 생성작업이 일어나지 않는다. 


※ new String 으로 생성한 객체를 String Constant Pool 영역에 넣는 방법도 있다.
String newStr = new String(“abc”);
newStr.intern();

intern 메소드는 heap 메모리에 있는 String 값을 String constant pool 영역으로 복사시키고 stirng constant pool 에 복사된 주소를 반환한다.

public static void main(String[] args) {
String a = new String("a"); // heap 에 매번 새롭게 생성
String b = "a"; // string constant pool 에 생성
System.out.println(a==b); // false
System.out.println(a.intern() == b); // true
System.out.println(a == b); // false
String c = a.intern();
System.out.println( a == c); // false
System.out.println( b == c); // true

System.out.println("----------------");

}

결론은 쓸데 없이 new String 하지 말자.
(모두 다른 문자열 생성하는 것이면 new 로 생성하나 큰 따옴표로 생성하나 성능상의 차이점은 없지 않을까?)


생성자와 정적 팩터리 메서드를 함께 제공하는 변경 불가능 클래스의 경우 정적 팩터리 메서드를 이용하면 불필요한 객체 생성을 피할 수 있을 때가 많다.

예로 Boolean 클래스는 new Boolean(String) 보다는 Boolean.valueOf(String) 으로 생성하면 동일한 (클래스 로드시 미리 생성된 객체) 를 반환 받을수 있어 객체 생성의 부담을 피할수 있다.

그리고 어떠한 메소드 내부에서 동일한 객체를 항상 생성하는 로직이 있다면 정적 초기화 블록으로 생성 로직을 이동시켜 성능을 개선하는 것이 좋다.

public class PersonClient {

public static void main(String[] args) {
Person p = new Person();

// 이렇게 하면 Calendar 객체가 1000 번 생성된다
for(int i=0; i< 1000; i++)
p.someMethod();
}

}
class Person {

public void someMethod() {
// getInstance 라는 메소드명 때문에 singleton 으로 오해할 수 있으나 호출시마다 매번 새로운 객체를 생성해 반환한다.
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());

System.out.println(cal.get(Calendar.SECOND));
}
}

위 코드를 정적 초기화 블록으로 개선한다면

private static Calendar cal;

static {
cal = Calendar.getInstance();
}

/**
* 만들고 보니 date 객체도 계속 생성되긴 하지만
* cal 객체는 class 초기화시 한번만 생성된다.
* 다만 improveMethod 가 호출되지 않는다면 불필요한 cal 객체가 생긴샘이다.
* 이런 점을 보완하기 위해 improveMethod가 처음으로 호출될때만 객체를 생성시키는
* 초기화 지연 (lazy initialization) 기법을 사용할 수 있지만 구현이 복잡해지고 성능이 크게 향상되지도 않는다.
*/
public void improveMethod() {
cal.setTime(new Date());
System.out.println(cal.get(Calendar.SECOND));
}

위와 같이 구현 할 수 있다.



또다른 불필요한 객체가 생성되는 경우는
jdk 1.5 부터의 autoboxing (자동 객체화)로 인해 발생하기 쉽다.
기본자료형과 객체 표현형을 섞어 쓸때 둘 간의 변환이 자동으로 이뤄진다.
 
Long sum = 0L;
for(long i = 0; i < Integer.MAX_VALUE; i++)
    sum += i;

이와 같은 코드가 있을때 for 문의 += 연산이 일어날때마다 long 기본형인 i 변수는 Long 타입의 객체로 변환되어 Long 객체 sum에 저장된다. 즉 Integer.MAX_VALUE 만큼 객체가 생성되는 것이다.



참고로 객체풀을 만들어 객체를 재사용 하는 방법은 객체 생성 비용이 극단적으로 높지 않다면 사용하지 않는 것이 좋다.
DB 연결 같은 경우는 접속비용이 충분히 높으므로 풀을 만들어 재사용하는 것이 타당하나
일반적인 경우 코드가 복잡해지고 성능도 오히려 떨어질 수 있다.

즉 가벼운 객체라면 최신 jvm 에서는 고도화된 gc 가 있어 풀을 만들어 재사용하는 대신 새로 생성하는 편이 월등한 성능을 보여준다.

effective java 규칙 4. 객체 생성을 막을 때는 private 생성자를 사용하라.

클래스의 객체의 생성을 막을 수 있는 방법은 두가지가 있다.
하나는 클래스를 추상 클래스 (abstract class) 로 만드는 것이고 다른 하나는 생성자를 private 으로 만드는 것이다.

그전에 왜 객체 생성을 막는 클래스를 만들어야 하는지 생각해봐야 할 것 같다.
객체 생성을 막으면 객체 지향적으로 개발을 할 수가 없다. 객체 지향적 언어를 절차적으로 사용할 수 밖에 없게 된다.
그러나 이런 클래스도 필요 할 경우가 있다. 다음의 세가지 경우가 이에 해당한다.

* 객체 생성을 막아야 할 경우 = 유틸리티 클래스
1. 자바의 기본 자료형 또는 배열에 적용되는 메서드를 한군데 모아둘때 (java.lang.Math, java.util.Array)
2. 특정 인터페이스를 구현하는 객체를 만드는 팩터리 메서드 (java.util.Collections)
3. final 클래스에 적용할 메서드들을 모아 놓을때 (잘 이해가 안간다.)
    - final 클래스는 상속을 불가능하게 만든다.
    - 상속이 불가능하다는 것은 하위 클래스가 없다는 것이고 하위 클래스에서 메서드를 추가할 수도 없다.
    - 따라서 final 클래스는 api 클래스를 만들때 주로 사용한다. 
      api클래스를 final 로 만들면 최종 사용자가 api 클래스를 상속하여 변형하는 것을 막을 수 있다.


이제 왜 객체 생성을 막는 방법을 알아야 하는지 이해가 갈 것이다.
주제로 다시 돌아와서 객체생성을 막는 방법을 알아보자

1. 추상 클래스
추상 클래스 자체로 보자면 객체 생성은 막을 수 있다.
다만 추상 클래스는 다른 어떤 하위 클래스에서 상속 할 때 추상 클래스의 특성을 가진 하위 클래스가 되고
그 하위 클래스는 객체 생성이 가능하다. 
추상 클래스는 하위 클래스들을 추상화 한 것으로 원래 목적을 고려 하면 추상 클래스 선언으로 객체 생성을
막는 시도는 잘못된 생각이다.

2. 생성자를 private 로 선언

public class UtilityClass {
/**
* 선언된 생성자가 없을때 기본생성자가 생성되기 때문에
* 기본 생성자가 자동 생성되지 못하도록 하여 객체 생성 방지
*
* private 선언으로 외부에서 접근 하지 못하도록
* reflection 으로 생성자를 실행 시키는 것을 방지하고
* 클래스 안에서 실수로 생성자를 호출하면 바로 알 수 있게 하기 위해
* Exception 을 throw 하도록 구현
*
*/
private UtilityClass() {
throw new AssertionError();
}
}

코드의 주석을 통해 private 으로 생성자를 만들어 객체를 생성 불가능하게 만드는 방법을 설명했다.
이 방법은 하위 클래스를 만들 수 없게 하기 때문에 abstrace class 와는 다게게 상속을 통해 객체를 생성하지 못하도록 한다. (모든 생성자는 상위 클래스의 생성자를 명시적으로든 아니면 묵시적으로든 호출할 수 있어야 하기 때문에)


2017년 4월 17일 월요일

effective java 규칙 3. private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라 (singleton)

하나의 객체만을 사용하도록 강제할때 주로 사용하는 패턴으로 singleton pattern 이 있다.
singleton pattern은 매우 흔히 사용되는 패턴으로 그 구현 방법도 여러 가지이다.

1. public final 필드를 이용한 방법은 가장 손쉽게 싱글턴 객체를 생성 하는 방법이다.

public class SingletonByPublicStaticVariable {

public static final SingletonByPublicStaticVariable INSTANCE = new SingletonByPublicStaticVariable();
private SingletonByPublicStaticVariable() {}

public void test() {
System.out.println("singleton by public static final variable");
}

}
위 코드는 클래스가 로드될때 초기화를 통하여 INSTANCE 변수에 객체가 생성되어 할당된다.
이 방법의 장점은 선언만 보고 싱글턴 여부를 바로 알 수 있지만 클래스가 로드될때 항상 객체가 생성된다.
가령 객체로 생성할 필요없이 public static method 를 사용할때에도 객체는 변수에 할당된다.
그리고 객체가 생성될때 에러처리도 불가능하며 객체 생성 전에 로직이 필요한 경우엔 사용할 수 없다.


2. static 블록을 이용한 방법은 1번 방법을 조금 개선한 방식이다.

public class SingletonByStaticBlock {
private static final SingletonByStaticBlock INSTANCE;
static {
INSTANCE = new SingletonByStaticBlock();
}

public static SingletonByStaticBlock getInstance() {
return INSTANCE;
}

public void test() {
System.out.println("singleton by static block");
}
}
static 초기화 블럭은 클래스가 로딩 될 때 최초 한번만 실행된다.
1번과 비교하여 객체를 생성하기전에 로직을 담을 수 있기 때문에 객체생성 로직 또는 에러처리 구문이나 초기변수 셋팅등의 코드 작성이 가능하다.
예를 들자면 현재시간을 체크하여 연초면 2개의 인자를 받는 생성자를 통해 객체를 생성하고 연말이면 3개의 인자를 받는 생성자를 사용한다던가의 로직 기술이 가능하다.
다만 이방식도 클래스가 로드되는 시점에 무조건 객체가 생성된다.

3. 정적 팩터리를 이용한 싱글턴 방식

public class SingletonByFactory {
private static final SingletonByFactory INSTANCE = new SingletonByFactory();

private SingletonByFactory() {
}

public static SingletonByFactory getInstance() {
return INSTANCE;
}

public void test() {
System.out.println("singleton by factory method");
}
}

이 방식은 싱글턴패턴을 포기하는 상황에서 좀더 유연하게 작동한다.
1번 방식으로 구현했을때 싱글턴을 변경하면 해당 클래스를 사용하는 클라이언트 코드는 모두 수정이 불가피하게 된다.
정적 팩터리를 사용하면 메소드를 사용하기 때문에 내부 구현의 변화에 따른 클라이언트 코드의 영향을 줄일 수 있다.
성능적인 측면에서도 최신 jvm 은 정적 팩터리 메서드 호출을 거의 항상 인라인 처리하기 때문에 1번과 성능차이는 거의 없다.
그리고 이방식은 제너릭 타입을 수용하기에 용이하다.


4. lazy initialization 방식

1~3번 방식은 모두 클래스 로딩시점에 객체가 생성되어진다. lazy initialization 은 이러한 부담을 개선한 방식이다.

public class SingletonByLazyInit {
private static SingletonByLazyInit INSTANCE;
private SingletonByLazyInit() {}

public static SingletonByLazyInit getInstance() {
if(INSTANCE == null)
INSTANCE = new SingletonByLazyInit();

return INSTANCE;
}

public void test() {
System.out.println("singleton by lazy initialization");
}
}
위 코드에 나타난 대로 객체는 getInstance 메소드 실행되고 객체가 생성 된 적이 없어야만 객체가 생성된다.
그러나 이 방식도 잠재된 문제점이 있다.
동일 시점에 getInstance 가 호출될때 가령 multi thread 환경에서 객체가 두개 이상 생성될 가능성이 있다.

5. thread safe lazy initialization 방식
이방식은 4번의 문제점을 개선한 방석이다.

public class SingletonByLazyInitThreadSafe {
private static SingletonByLazyInitThreadSafe INSTANCE;

private SingletonByLazyInitThreadSafe() {
}

public static synchronized SingletonByLazyInitThreadSafe getInstance() {
if(INSTANCE == null)
INSTANCE = new SingletonByLazyInitThreadSafe();

return INSTANCE;
}

public void test() {
System.out.println("singleton by thread safe initialize ");
}
}

코드는 거의 유사하나 getInstance method 에 synchronized 를 선언해 여러 thread 가 동시에 접근하지 못하도록 했다.
그러나 역시 여러 thread 가 getInstance 를 호출하게되면 높은 cost 비용으로 인해 프로그램 전반에 성능저하가 발생한다.

6. Enum 싱글턴 방식

4~5번 문제를 해결하는 effective java 에서 제안 하는 방식은 enum 싱글턴이다. effective java 에서는 이 방식이 가장 낫다고 한다.

public enum SingletonByEnum {
INSTANCE;

public void test() {
System.out.println("singleton by enum");
}
}

enum 방식은 일반적인 우리가 알고 있는 열거형을 싱글턴 패턴에 이용하는 방식이다.
원소가 하나인 enum 자료형을 정의하고 다른 method 들은 class 작성과 동일하게 작성한다.
effective java 에 따르면
단 한번의 인스턴스 생성을 보장하며 사용이 간편하고 직렬화가 자동으로 처리되고 직렬화가 아무리 복잡하게 이루어져도 여러 객체가 생길 일이 없으며, 리플렉션을 통해 싱글턴을 깨트릴수도 없다고 한다.
그리고 multi thread 로부터 안전하다. 
다만 이 방식은 생소하기도 하고 enum 자료형의 일반적인 쓰임새와 혼동될 것 같다는 생각이 든다. 

7. holder idiom 방식

이 방식 또한 thread 에 대해 안전하다
이 방식은 jvm 상에서 클래스 초기화 단계에 따른 특성을 활용한 방식이다.
public class SingletonByIdiom {

private SingletonByIdiom() {

}

private static class Singleton {
private static final SingletonByIdiom INSTANCE = new SingletonByIdiom();
}

public static SingletonByIdiom getInstance() {
return Singleton.INSTANCE;
}

public void test() {
System.out.println("singleton by idiom");
}
}

SingletonByIdiom 클래스는 클래스 로드 시점에 초기화되지만 정적 클래스로 정의된 내부 클래스의 초기화는 해당 시점에 이뤄지지 않는 특성이 있다.
즉 getInstance 를 통해 내부 클래스의 instance 를 호출할때 뒤늦게 초기화 되어 객체를 할당한다.
이 방식이 thread safe 한 이유는 jvm 의 클래스 초기화 과정에서 보장되는 원자적 특성(시리얼하게 동작)을 이용하기 때문이다. 즉 동기화 문제를 jvm이 처리 하도록 한다.
 


[참고 : 클래스가 최초로 초기화 되는 시점]
T에서 선언한 정적 필드가 상수 변수가 아니며 사용되었을 때
T가 클래스이며 T의 인스턴스가 생성될 때
T가 클래스이며 T에서 선언한 정적 메소드가 호출되었을 때
T에서 선언한 정적 필드에 값이 할당되었을 때
T가 최상위 클래스(상속 관계에서)이며 T안에 위치한 assert 구문이 실행되었을 때





직렬화 가능 클래스에서 역직렬화시 새로운 객체가 생기게 되는 문제 (singleton 깨짐, 단 enum 싱글턴 방식은 역직렬화에 따른 문제점이 없다. - holder idiom 방식은 미확인)

싱글턴 특성을 유지하려면 
1. 모든 필드를 transient 로 선언
2. readResolve 메서드 추가

// 싱글턴 상태를 유지하기 위한 readResolve 구현
private Object readResolve() {
    // 동일한 객체가 반환되도록 하는 동시에, 가짜 객체는 gc 가 처리하도록 만듬.
    return INSTANCE;
}


참고 사이트

2017년 4월 16일 일요일

effective java 규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

객체를 생성할때 객체에서 사용할 멤버변수(상태 멤버변수는 아님 - 즉 사용 시점에 따라 값이 달라지는 멤버변수가 아닌)를 외부로 부터 받아야 하는 객체일 경우 생성자를 통하거나 객체 생성 후 setter 메서드를 통해 인자를 전달받는다.
이 멤버변수가 모두 필수 인자일 경우는 별 문제가 되지 않는다. 하나의 생성자에 인자를 모두 포함시키면 된다.
다만 인자의 갯수가 많아지면 가독성에 문제가 생기고 클라이언트 코드에서 객체 생성 코드를 작성하기가 어려워진다.
생각해보라 그 많은 인자를 생성자 인자 순서에 맞게 작성하는 것이 쉽지도 않고 인자타입이 같은 인자들의 순서를 잘 못 기입해도
컴파일러는 알지 못하며 런타임 시점에 문제가 생기게 되어 문제를 발견하기 어려워진다.
위 문제말고도 또 하나의 문제가 발생할 수 있다.

또한 객체의 멤버변수가 모두 필수 인자가 아니라 필수 인자와 선택적 인자가 혼재 되어있을때의 객체 생성시에도 문제가 발생한다.
필수필드와 선택적 필드가 같이 있는 클래스의 객체를 생성하는 방법은 보통 다음 두가지 방법을 사용한다.

1. 하나는 점층적 생성자 패턴 (telescoping constructor pattern) 을 사용하는 것이고
2. 다른 하나는 자바빈 패턴을 사용하는 것이다.

점층적 생성자 패턴은 
필수 인자값은 클래스에 필수 인자를 모두 받는 생성자를 하나 만들고
선택적 인자를 하나 받는 생성자를 추가하며 추가된 생성자 안에 두개의 선택적 인자를 받는 생성자를 추가하는 방식으로 생성자를 쌓아 올리듯 추가하는 방식이다.
이 방식은 선택적 인자의 갯수 만큼 생성자가 증가하며 설정할 필요가 없는 필드에도 인자가 전달된다.
그리고 이방식은 인자가 많을 경우의 문제점인 

가독성에 문제가 생기고 
클라이언트 코드에서 객체생성 코드를 작성하기가 어려워지며 
인자의 순서를 혼동할 가능성이 많고 
타입이 같은 인자의 순서를 혼동할 경우 컴파일러는 알지 못하며 런타임 시점에 오류가 발생할 수 있어 오류를 찾아내기가 어려워지는 문제
” 
이 전혀 해결되지 않는다.
다만 생성자를 통하여 객체를 생성하기 때문에 안정성은 보장이 된다. (멤버변수의 final 과 변경 불가능 객체로 생성 = 객체 일관성 향상)

* 점층적 생성자 패턴을 적용한 클래스
public class Telescoping {

// 필수
private final int year;
private final int month;
private final int day;

// 선택
private final int hour;
private final int minute;
private final int second;


public Telescoping(int year, int month, int day) {
this(year, month, day, 0);
}

public Telescoping(int year, int month, int day, int hour) {
this(year, month, day, hour, 0, 0);
}

public Telescoping(int year, int month, int day, int hour, int minute) {
this(year, month, day, hour, minute, 0);
}

/**
* 인자가 많아 질수록 인자의 순서를 혼동하기 쉽고 사용하기 어려워짐
* @param year
* @param month
* @param day
* @param hour
* @param minute
* @param second
*/
public Telescoping(int year, int month, int day, int hour, int minute, int second) {
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
}

}

두번째 대안인 자바빈(JavaBeans) 패턴은
인자가 없는 (또는 필수 인자만 받는) 생성자를 호출하여 객체를 생성한 다음 설정 메서드(setter method)를 통해 멤버변수 필드의 값을 채우는 것이다.
얼핏 보기에도 이 방식은 점층적 생성자 패턴에서 발생할수 있는 인자의 순서를 혼동하는 문제점도 해결되고 객체를 생성 하기도 쉬워 대안으로 선택하기 쉽다. 
이 패턴의 또 하나의 특징은 멤버변수를 setter 메서드로 설정하기 때문에 멤버변수의 final 지정이 불가능하다.
생성자로 인자를 받지 않기때문에 (필수 인자를 받는 생성자는 논외) 멤버변수는 setter 를 통해 값을 받을 수 밖에 없고 그렇기 때문에 멤버변수에 final 지정이 불가능하다.
이점이 첫번째 객체 생성 방식인 점층적 생성자 패턴과의 또다른 차이점이다.
이 차이점으로 인해 이 자바빈 패턴에는 심각한 단점이 발생한다.
객체 일관성(consistency) 훼손이 가능해지며 항상 변경 가능한 객체가 만들어지기 때문에 변경 불가능(immutable) 클래스를 만들 수 없다 (스레드 안정성을 제공하려면 더 많은 작업이 필요함). 
추가적인 작업을 통하여 일관성을 향상 시킬수는 있지만 매우 까다로우며 거의 쓰이지도 않는다.
그리고 또다른 단점은
1회의 함수(생성자) 호출로 객체 생성을 끝낼 수가 없다는 점이다. 이 단점 역시 객체 생성후 추가 객체 값 설정 작업으로 인해 일관성 문제를 야기 시킨다.

public class JavaBean {

// 필수 - final 선언 하지 못함
private int year;
private int month;
private int day;

// 선택 - final 선언 하지 못함
private int hour;
private int minute;
private int second;

public JavaBean() {
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

public int getHour() {
return hour;
}

public void setHour(int hour) {
this.hour = hour;
}

public int getMinute() {
return minute;
}

public void setMinute(int minute) {
this.minute = minute;
}

public int getSecond() {
return second;
}

public void setSecond(int second) {
this.second = second;
}

public static void main(String[] args) {

/*
가독성은 향상되나 객체 생성을 1회에 끝낼 수 없고
멤버변수를 setter 로 추후에 셋팅이 가능해 일관성이 훼손될 수 있다
*/
JavaBean j = new JavaBean();
j.setYear(2017);
j.setMonth(4);
// ...
j.setSecond(17);

// 일관성 훼손 가능
// j.setYear(2018);
}
}


위 두 패턴의 단점을 개선한 세번째 대안은 빌더(Builder) 패턴이다.
이 패턴은 점층적 생성자 패턴의 안정성과 자바빈 패턴의 가독성을 결합한 대안이다.
기본적인 생성 방법은 코드로 설명하겠다.

* 빌더 패턴을 적용한 클래스
public class SomePerson {

// 필수인자 final 선언가능
private final String name;
private final Date birthday;

// 선택적인자 final 선언가능
private final String address;
private final int height;
private final String socialStanding;
private final Boolean isCyber;

/**
* builder 를 통해서 만 객체 생성할 수 있도록 private 선언
* 빌더 객체에서 실제 객체로 인자 복사
* @param builder
*/
private SomePerson(Builder builder) {
this.name = builder.name;
this.birthday = builder.birthday;

this.address = builder.address;
this.height = builder.height;
this.socialStanding = builder.socialStanding;
this.isCyber = builder.isCyber;
}

public static class Builder {
private String name;
private Date birthday;
private String address;
private int height;
private String socialStanding;
private Boolean isCyber;

/**
* 필수 인자 값은 생성자로 설정
* @param name
* @param birthday
*/
public Builder(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}

public Builder address(String address) {
this.address = address;
return this; // builder 객체 자신을 return
}

public Builder height(int height) {
this.height = height;
return this; // builder 객체 자신을 return
}

public Builder socialStanding(String socialStanding) {
this.socialStanding = socialStanding;
return this;
}

public Builder nomalPeople(String address, int height) {
this.address = address;
this.height = height;
return this;
}

public Builder rankPeople(String socialStanding, String address, int height) {
this.socialStanding = socialStanding;
this.address = address;
this.height = height;
return this;
}

public Builder cyberPeople(String address, int height) {
this.address = address;
this.height = height;
this.isCyber = Boolean.TRUE;
return this;
}

public Builder realPeople(String address, int height) {
this.address = address;
this.height = height;
this.isCyber = Boolean.FALSE;
return this;
}

// 최종적으로 변경 불가능 객체 생성
public SomePerson build() {
return new SomePerson(this);
}
}
}

* 객체를 생성하려는 클라이언트 클래스
public class MakePersonMain {

/**
* 점층적 생성자 패턴과 비교해봤을때
* 빌더패턴으로 객체를 생성시 인자의 이름이 있으므로 인자를 혼동하지 않는다. (자바빈 패턴 장점)
* 또한 빌더패턴으로 생성된 객체는 변경불가능한 객체로 생성이 가능하다. (점층적 생성자 패턴 장점)
* 그리고 빌더 패턴이 갖는 또 한가지의 장점은 선택적 인자를 필요한 만큼 포괄적으로 설정이 가능하며
* 여러 성격의 객체를 만들수 있다.
* 하나의 빌더로 여러 객체를 생성할 수 있다.
* @param args
*/
public static void main(String[] args) {

// 기본생성 (필수인자)
SomePerson person = new SomePerson.Builder("홍길동", new Date()).build();

// 선택적 인자 포함 생성
SomePerson personExt = new SomePerson.Builder("홍길동2", new Date())
.address("조선...").height(160).build();

// 일반인 - 선택적 인자 포괄 생성
SomePerson personExt2 = new SomePerson.Builder("홍길동", new Date())
.nomalPeople("한양", 170).build();
// 양반 - 객체 생성시 명시적으로 객체의 특성을 나타내도록 생성할 수 있다.
SomePerson nobility = new SomePerson.Builder("황희", new Date())
.rankPeople("영의정", "한양", 166).build();

// 하나의 빌더로 여러 객체 생성 가능, 어떤 필드의 값은 자동으로 채움(isCyber 필드와 같은)
SomePerson.Builder builder = new SomePerson.Builder("메트릭스-네오", new Date());
SomePerson cyberMan = builder.cyberPeople("뉴욕", 195).build();
SomePerson realMan = builder.realPeople("지하세계", 185).build();

}

}

요약

필수 인자와 선택적 인자가 있을때 점층적 생성자 패턴
    - 필수 인자들을 받는 생성자를 정의, 선택적 인자를 받는 추가적 생성자들을 쌓아 올리듯 추가
    - 단점 1. 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고 가독성이 떨어짐
    - 단점 2. 설정할 필요가 없는 필드에도 인자를 전닳해야함
    - 단점 3. (많은 선택적 인자를 받는) 생성자 호출시 인자의 타입이 같다면 인자의 순서를 잘못 넣더라도
    컴파일시점에 오류를 검출할 수가 없고 런타임 시점에 문제가 발생하여 오류의 발견이 어려움

    - 장점 1. 생성된 객체의 일관성 = 생성자로 객체를 생성하기 때문에 누락된 값 없이 의도한 객체를 생성

생성자에 전달되는 인자 수가 많을 때
자바빈 패턴
    - 점층적 생성자 패턴의 단점 3. 을 보안
    - 인자 없는 생성자를 호출하여 객체부터 생성하고 설정 메서드(setter methods)들을 호출하여 값을 채움
    - 단점 1. 1회의 호출로 객체 생성을 끝낼 수 없음 = 객체 일관성(consistency)이 일시적으로 깨짐
    - 단점 2. 변경 불가능 클래스를 만들 수 없음 = 스레드 안정성 제공 하기 어려움
    - 단점 3. 누락된 필드값이 발생할 수 있음 = 런타임 시점에 오류 발생, 오류의 발견이 어려움

    - 장점 1. 쉬운 객체 생성, 가독성 향상

빌더 패턴
    - 점층적 생성자 패턴의 안정성 + 자바빈 패턴의 가독성
    - 장점 1. 불필요한 생성자를 만들지 않고 객체 생성 (또는 필수 인자만 받도록)
    - 장점 2. 인자의 순서 무관
    - 장점 3. 가독성 좋음
    - 장점 4. 안정성 (변경 불가능 객체로 생성가능)
    - 장점 5. 객체 생성시 객체의 특성을 나타낼 수 있음

    - 단점 1. 빌더 객체를 추가적으로 생성해야함
    - 단점 2. 코드량 증가

결론
    - 빌더 패턴을 상황에 맞게 적절히 수정해서 사용
        실무에서 멤버변수가 많은 객체를 생성시 항상 빌더 패턴을 사용하는 것은 아니다. 
        일단 DTO 나 VO 등 data entity 인 경우 자바빈 패턴을 사용하는 경우가 더 많고 편리할 때가 많다.
        그 이유는 orm 이나 sqlmap 의 조회 결과를 담을때 builder 패턴을 사용하기가 까다롭다.  mybatis resultmap 의 association 
        빌더패턴으로 객체를 구현하는 것은 인자(인자로 값을 전달 받는 클래스의 멤버변수)가 얼마나 많은지 
        그리고 클라이언트에서 해당 객체를 생성할때 가독성의 중요도등을
        파악해 상황에 맞게 사용하면 될 것 같다.
    - 인자가 많은 생성자를 가진 클래스를 설계할 때, 특히 대부분의 인자가 선택적일 때 유용함
    - 변경이 가능해야 하거나 상태값을 나타내는 변수는 setter method 제공
    - 그 이외의 상황에서는 final 설정 또는 변경 불가능하도록 만듬
    - 빌더 클래스의 설정 메서드는 만드려는 객체를 잘 설명할 수 있도록 만듬, 상황에 따라 여러 인자를 받는 설정 메서드를 만듬
    즉 각각의 비즈니스에 사용되는 인자를 묶어 각 비즈니스를 해결하는 객체를 만듬
    예) Sample s = new Sample.Builder(필수인자, 필수인자..).someBusiness(선택적인자, ...).build();

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor ...