클래스의 상속을 허용하면 문제가 발생할 수 있다.

기존 클래스(A) 내부에서 먼저 정의 및 호출되는 함수(F)가 재정의 가능한 함수일 때, 다른 클래스(B)가 A 클래스를 상속한 다음 F 함수를 재정의(F’)하여 사용할 수 있는데 모종의 이유로 클래스 A 에서 F 함수를 수정하면 클래스 B 의 F’ 함수에서 문제가 생길 수 있다.

이러한 문제가 왜 발생하는지 좀 더 자세히 알아보고, 어떻게 방지할 수 있는지 알아보자.


주석을 잘 남겨라

• 외부에서 상속용 클래스의 메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨라

• 상속용 클래스는 재정의 가능한 메서드들을 내부에서 어떻게 이용하는지 문서로 남겨야 한다.

• 클래스 내부에서 재정의 가능한 메서드를 호출하고 있다면, 그 사실과 어떤 순서로 호출하는지,

    각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 문서로 남겨라

    ◦  재정의 가능 메서드 : final이 아닌 모든 public, protected 메서드

• 아니 그냥 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨라!

    ◦  백그라운드 스레드나 정적 초기화 과정에서도 호출이 일어날 수 있음

• 메서드에서 종종 Implementation Requirements로 시작하는 주석을 발견할 수 있는데, 이는 메서드의 내부 동작 방식에 대한 설명을

    적어놓은 주석이다.

    ◦  Command Line에서 -tag “implSpec:a:Implementation Requirements:” 명령어를 입력해 @implSpec 어노테이션을 새롭게

         지정할 수 있는데, 이 어노테이션을 주석에 붙이면 Implementation Requirements 라는 문구가 자동으로 생성된다.

         (자바독의 커스텀 태그 기능)

    ◦  자바 개발팀에서 내부적으로 사용하는 규약

    ◦  Annotation 이름을 반드시 @implSpec으로 해야할 필요는 없지만, 언젠가 표준 태그로 정의될지도 모르니

         이왕이면 자바 개발팀과 같은 방식으로 사용하는 것을 추천한다. (by 옮긴이)

→ java.util.AbstractCollection의 remove 메서드

• @implSpec은 이 클래스를 상속하여 메서드를 재정의했을 때 나타날 효과를 상세히 설명하고 있다.

    ◦  java.util.AbstractCollection의 remove 메서드는 내부적으로 iterator의 remove 메서드를 사용하고 있다.

    ◦  위 설명에 의하면 iterator 인터페이스를 구현한 구현체에 remove 메소드가 구현되어 있지 않은 경우

        UnsupportedOperationException이 발생한다.

• iterator 메서드를 재정의하면 java.util.AbstractCollection의 remove 메서드의 동작에 영향을 준다.

    ◦  iterator 메서드를 재정의하면 반드시 remove 메소드도 함께 구현해 주어야 java.util.AbstractCollection의 remove 메서드를

         오류 없이 사용할 수 있다.

• 이처럼 클래스를 안전하게 상속할 수 있도록 하려면 클래스의 내부 구현 방식을 설명으로 남겨야 한다.

    ◦  @implSpec 어노테이션을 적극 활용하라


클래스의 Hook 을 잘 선별하여 protected 메서드 형태로 공개하라

• 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 내부 동작 과정에 끼어들 수 있는 훅(hook)을

    protected 메서드(재정의 가능한)로 제공하는것이 좋다.

• removeRange 메서드는 특정 리스트 또는 부분 리스트의 clear 메서드에서 사용된다고 나와있다.

• 또한 해당 메서드를 재정의하면 특정 리스트 또는 부분 리스트의 clear 메서드를 고성능으로 만들 수 있다고 명시되어있다.

• removeRange 메서드를 protected 접근 제어자로 제공한 이유는 단지 하위 클래스의 clear 메서드를 고성능으로 만들기 쉽게

    하기 위해서이다.

 

• 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까?

    ◦  심사숙고해서 잘 예측해본 다음 실제 하위 클래스를 만들어 실험해보는것이 최선

          -  상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는것이 유일한 방법이다.

          -  한 3개 정도 만들어 보는 것이 적당하다. (이 중 하나 이상은 제 3자가 작성해볼 것)

          -  상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

• protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 하나 너무 적게 노출해서 상속으로 얻는 이점마저

    없애지 않도록 주의해야 한다.

• 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하는 편이 낫다.


상속을 허용하는 클래스가 지켜야 할 제약

• 상속을 허용하는 클래스의 생성자는 클래스 내부의 재정의 가능 메서드를 호출하면 안된다.

    ◦  프로그램 오작동의 원인이 됨

    ◦  상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출되기 때문

    ◦  하위 클래스에서 재정의된 메서드가 하위 클래스의 생성자보다 먼저 호출된다.

    ◦  재정의된 메서드가 하위 클래스의 생성자에서 초기화되는 값에 의존한다면 의도대로 동작하지 않을 것

public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
}
public final class Sub extends Super {
    private final Instant instant;
    Sub() { instant = Instant.now(); }

    @Override
    public void overrideMe() { System.out.println(instant); } 
}

Sub sub = new Sub();  // null 출력
sub.overrideMe();     // instant 출력

• 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화 하기도 전에 overrideMe 함수를 호출하기 때문에

    이러한 문제가 발생한다.

• overrideMe 메서드에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe 메서드를 호출할 때

    NullPointerException을 던지게 될 것이다.

    ◦  위 코드에서는 println이 null 입력을 처리해주기 때문에 예외는 발생하지 않음


상속용 클래스의 설계와 Cloneable/Serializable 인터페이스

• 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.

    ◦  clone 과 readObject 메서드는 생성자와 비슷한 효과를 내기 때문 : 새로운 객체를 생성한다.

    ◦  즉 상속용 클래스에서 Cloneable 또는 Serializable을 구현하게 된다면 이들을 구현할 때 따르는 제약도 생성자와 비슷하다.

    ◦  clone 과 readObject 메서드 모두 직/간접적으로 재정의 가능한 메서드를 호출해서는 안된다.


상속용으로 설계되지 않은 일반적인 구체 클래스

• 상속용으로 설계하지 않은 클래스는 상속을 금지하라

• 상속용으로 설계되지 않은 일반적인 구체 클래스는 final도 아니고 상속용으로 설계/문서화되지 않았다.

    ◦  그대로 두면 위험하다. / 누가 상속해버릴지 몰라... 누가 줄줄이 도미노 문제 만들어놓을지 몰라...

• 클래스의 상속을 금지시키는 두가지 방법

    ◦  클래스를 final로 선언하는 방법

    ◦  모든 생성자를 private 또는 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법

          -  클래스 내부에서 다양한 하위 클래스를 만들어 쓰는 등의 유연한 작업을 가능하게 해준다.

• 상속을 금지해도 괜찮은 것인가?

    ◦  클래스가 특정 인터페이스를 구현한 클래스라면 상속을 금지해도 개발하는데 문제가 없다.

          -  예) Set / List / Map

    ◦  단 구체 클래스가 인터페이스를 구현하지 않았는데 상속을 금지해 버리면 상당히 불편해질 수 있다.

    ◦  이런 경우에는 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 해당 사실을 문서로 남기자.

          -  메서드를 재정의해도 다른 메서드의 동작에 영향을 주지 않게 만들기 위해


클래스 동작을 유지하면서, 재정의 가능 메서드를 사용한 부분을 제거하는 법

public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
}
public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
    private void helperMethod() { ... }
}
public class Super {
    public Super() { helperMethod(); }
    public void overrideMe() { helperMethod(); }
    private void helperMethod() { ... }
}

• 새로운 private helperMethod(도우미 메서드)를 생성한다.

• 각각의 재정의 가능 메서드에 작성된 코드를 helperMethod로 옮긴다.

• 기존 재정의 가능 메서드가 이 helperMethod를 호출하도록 수정한다.

• 기존 재정의 가능 메서드를 호출하는 다른 코드 역시 이 helperMethod를 호출하도록 수정한다.

Comparable 인터페이스의 유일무이한 메서드, compareTo

• comapreTo = 값을 비교하는 method

A.compareTo(B)
A < B : 음수 반환 ( sgn(A.compareTo(B)) = -1 )
A = B : 0 반환 ( sgn(A.compareTo(B)) = 0 )
A > B : 양수 반환 ( sgn(A.compareTo(B)) = -1 )

• compareTo는 Object의 메서드가 아닌, Comparable 인터페이스의 메서드이다.

public class ClassName implements Comparable<ClassName> { ... }
public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.println(a.compareTo(b)); // -1
    }
}
public class Main {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a.compareTo(b)); // error: int cannot be dereferenced
    }
}

• equals 메서드처럼 값이 같은지 비교하는 기능 뿐만 아니라, 값의 대소비교가 가능하다.

• 값의 순서를 매기는데 유용하게 사용된다 : 검색/정렬 메서드, 자동 정렬 Collection 등에서 사용됨

public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

• Generic Type 으로 다양한 객체 타입에서 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        Integer iA = 1; Integer iB = 2;
        Float fA = 1F; Float fB = 2F;
        Double dA = 1.0; Double dB = 2.0;
        
        System.out.println(iA.compareTo(iB)); // -1
        System.out.println(fA.compareTo(fB)); // -1
        System.out.println(dA.compareTo(dB)); // -1
    }
}

• 직접 작성한 클래스에서 Comparable을 구현하면 해당 인터페이스를 구현하는 다양한 제네릭 알고리즘과
    Collection을 사용할 수 있다는 장점이 있다.

    ◦  Java 라이브러리의 모든 값 클래스(Integer, Float, Double 등)와 열거 타입이 Comparable을 구현한다.

    ◦  알파벳, 숫자, 날짜 등 순서가 명확한 값 클래스를 작성할 때는 꼭 Comparable 인터페이스를 구현하자.

public final class Integer extends Number
        implements Comparable<Integer>, Constable, ConstantDesc { ... }
public abstract class Enum<E> extends Enum<E>>
        implements Constable, Comparable<E>, Serializable { ... }

• 정렬된 컬렉션인 TreeSet, TreeMap 과 검색/정렬 기능을 제공하는 유틸리티 클래스 Collections, Arrays 역시
   Comparable을 활용한다.


compareTo 메서드의 세가지 규약

A.compareTo(B)
A < B : 음수 반환 ( sgn(A.compareTo(B)) = -1 )
A = B : 0 반환 ( sgn(A.compareTo(B)) = 0 )
A > B : 양수 반환 ( sgn(A.compareTo(B)) = -1 )

• sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

    ◦  모든 x, y에 대해 x > y 이면 y < x 이다.

    ◦  한쪽에서 예외가 발생하면 다른 쪽에서도 응당 예외가 발생해야 함

 

• x.compareTo(y) > 0 && y.compareTo(z) > 0 이면 x.compareTo(z)

    ◦  모든 x, y, z 에 대해 x > y 이고 y > z 이면 x > z 이다

 

• x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))

    ◦  x = y 이면, 모든 z에 대해 x > z , x < z 의 참/거짓 여부는 y > z , y < z 의 참/거짓 여부와 동일

 

• (권장) x.compareTo(y) == 0 이면 x.equals(y) == 0 이다.

    ◦  Comparable을 구현하지만 위 사항을 지키지 않는 클래스는 반드시 그 사실을 명시해줘야 한다.

    ◦  주의! 이 클래스의 순서는 equals 메서드와 일관되지 않다.

 

• 위 규약들을 어기는 것이 특정한 오류를 발생시키는 것은 아니지만, 크고 작은 문제들이 발생할 수 있다.

• 정렬된 컬렉션들은 값이 같은지를 비교할 때 equals 대신 compareTo를 사용하기 때문

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");

        System.out.println(a.compareTo(b)); // 0
        System.out.println(a.equals(b)); // false

				HashSet<Object> A = new HashSet<>(); // 동치 비교에 equals 사용
        TreeSet<Object> B = new TreeSet<>(); // 동치 비교에 compareTo 사용

        A.add(a); A.add(b);
        B.add(a); B.add(b);

        System.out.println(A.size()); // 2
        System.out.println(B.size()); // 1
    }
}

compareTo 메서드의 작성 요령

• 타입이 다른 객체를 신경쓰지 않아도 됨 : 대부분 ClassCastException을 던지는 식으로 구현한다.

    ◦  일반적으로 객체간의 비교는 객체들이 구현한 공통 인터페이스를 매개로 이루어지기 때문

• 기존 클래스를 확장한 구체 클래스에서 새로운 값 Component를 추가하면 compareTo 규약을 위반하는 상황이 발생할 수 있으니

    주의하자.

    ◦  만약 새로운 값 Component를 추가하고 싶다면, 새로운 클래스를 만들어서 이 클래스에 원래 클래스의 인스턴스를 가리키는

         필드를 두자. 그 다음 내부 인스턴스를 반환하는 메서드르 제공하면 된다.

    ◦  이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있으며 필요에따라 바깥 클래스의 인스턴스를 원래

         클래스의 인스턴스로 다룰수도 있다.

• Comparable은 제네릭 인터페이스 이므로 compareTo 메서드의 parameter 타입은 컴파일 타임에 정해진다.

    즉 입력 인수의 타입을 확인하거나 형변환할 필요가 없음

    ◦  parameter 타입이 잘못되었다면 컴파일 자체가 되지 않는다.

    ◦  null 이 인수로 들어오면 NullPointerException이 Throw 되어야 한다.

• compareTo 메서드는 값이 동일한지를 비교하는데 보다는 값의 대소와 순서를 비교하는데 주로 사용된다.

    ◦  클래스에 핵심 필드가 여러개라면 가장 핵심적인 필드부터 비교해나가자.

public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode); // 가장 중요한 필드

    if (result == 0) {
        result = Short.compare(prefix, pn.prefix); // 두번째로 중요한 필드

				// 세번째로 중요한 필드
        if (result == 0) result = Short.compare(lineNum, pn.lineNum);
    }
    return result;
}

• 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출하면 된다.


Java 제공 비교자의 활용

@Override
public int compare(A,B){
    if (A > B) { return 1; }
		else if (A < B) { return -1; }
		else { return 0; } 
}

• Comparable을 구현하지 않은 필드나, 표준이 아닌 순서로 비교해야 한다면 Java에서 제공하는 비교자(Comparator)를 사용할 수 있다.

• 객체 참조 필드가 하나인 비교자 : compare

• Comparator 인터페이스를 구현할 때 작성해야하는 메서드로 compare()에 2개의 인자를 넘겨 내부 구현에 따라 int 결과 값을 리턴한다.


관계연산자 >, < 사용에 주의하라

• compareTo 메서드에서 관계 연산자 >, <를 사용하는 방식은 거추장스럽고 오류를 유발하기 쉽다.

• Java7 부터 라이브러리의 모든 값 클래스(Integer, Float, Double 등)가 Comparable을 구현하고 있으므로,

    해당 기능을 사용하는것이 권장된다.


Java의 비교자 생성 메서드와 객체 참조용 비교자 생성 메서드

• 비교자 생성 메서드를 사용하면 method chaining 방식으로 비교자를 생성할 수 있다.

private static final Comparator<PhoneNumber> COMPARATOR =
        Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.prefix)
                .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

• comparing 객체 참조를 키에 맵핑하는 키 추출 함수를 인수로 받아 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드

    ◦  입력 인수의 타입을 명시해줘야 한다. Java의 타입 추론 능력이 해당 타입을 인식한 만큼 강력하지 않으므로

• thenComparing 키 추출자 함수를 입력받아 다시 비교자를 반환하는 인스턴스 메서드

    ◦  원하는 만큼 연달아 호출할 수 있다.

    ◦  입력 인수의 타입을 명시해줄 필요가 없다. 자바의 타입 추론 능력이 이정도는 추론 가능하므로

• 자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.

    ◦  InputStream, OutputStream 등의 IO 라이브러리

    ◦ 터미널 등에서 명령어를 이용해 메모장을 열고, close 하지 않은 상태에서 메모장 파일을 마우스 클릭으로 실행해보면
        위와 같은 에러메세지를 확인할 수 있다.

    ◦ java.sql.Connection 등의 JDBC 사용을 위한 라이브러리

• 어떠한 이유(오류 또는 개발자의 실수)에 의해서 close가 안되면, 프로그램에 문제가 생길 수 있다.

• 이런 자원의 상당수가 안전망으로 finalizer를 활용하고 있긴 하나, 그리 믿을만하지는 못하다.


자원의 닫힘을 보장해주는 수단 : try-finally

• try와 finally는 짝꿍으로 붙어다녀야 하며, try블록에서 예외가 나던/나지않던 finally 안의 구문은 실행된다.

static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try { return br.readLine(); }
    finally { br.close(); }
} // read 만 하고 close 하는 경우
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try { OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
        } finally { out.close(); }
    } finally { in.close(); }
} // read 도 하고 write 도 하고싶은 경우 : close 를 2번 해줘야 한다.

• 코드의 가독성이 너무 나쁘다. 자원이 100개가 되면 try-finally가 100개가 생긴다.

static String firstLineOfFile(String path) throws IOException {
	  BufferedReader br = new BufferedReader(new FileReader(path));
	  try { return br.**readLine**(); }
		finally { br.**close**(); }
}

• 특히 예외는 try 블록과 finally 블록 모두에서 발생할 수 있는데, 위 예시에서 기기에 문제가 생기는 경우

    ◦  readLine 메서드가 먼저 예외를 던진다 : 파일을 읽을 수 없습니다.

    ◦  그 다음 close 메서드가 예외를 던진다 : 파일을 close 할 수 없습니다.

     → 이런 상황이 발생하면 스택 추적 내역에 첫번째 예외에 관한 정보는 남지 않는다 : 디버깅이 어려워짐


해결책의 등장 : try-with-resources

• try 안에 파라미터로 자원을 넘겨주는 방식

• try 문에서 선언된 객체들에 대해서 try가 종료될 때 자동으로 자원을 해제해주는 기능이 있다.

• 먼저 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.

    ◦  AutoCloseable 인터페이스 : void 타입의 close 메서드 하나만 덩그러니 정의된 인터페이스

public interface AutoCloseable { void close() throws Exception; }

    ◦  public interface AutoCloseable { void close() throws Exception; }

    ◦  try-with-resources가 모든 객체의 close를 호출해주지는 않음

    ◦  AutoCloseable을 구현한 객체만 close가 호출된다.

public abstract class BufferedReader implements AutoCloseable { ... }
static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
				return br.readLine();
		}
}
static void copy(String src, String dst) throws IOException {
    try (Input in = new FileInput(src); Output out = new FileOutput(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
    }
}

• 코드의 가독성이 훨씬 좋아지고 문제 진단에도 훨씬 유리하다.

• readLine 과 close 에서 모두 예외가 발생하면 먼저 발생한 readLine의 예외가 기록되고, close에서 발생한 예외는 숨겨지기 때문

• 이렇게 숨겨진 예외들은 스택 추적 내역에 숨겨졌다(suppressed)는 꼬리표를 달고 출력된다.

    ◦  Throwable의 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.

void addSuppressed(Throwwable exception)
public final Throwable[] getSuppressed()

    ◦  숨겨지는것 보단, 더 자세한 출력이 진짜 목적


try-with-resources 와 함께 쓰이는 catch 구문

• try-with-resources와 catch 절을 함께 쓰면 try 문을 중첩하지 않고도 다수의 예외를 처리할 수 있다.

try {
   // 프로그램에서 사용하는 일반적인 코드를 입력
   // 코드 실행 중 에러가 나면 그 자리에서 중단되고 catch문으로 이동
   // 오류가 없다면 try 안의 구문을 모두 실행한다.
} catch(Exception e) {
   // try에서 오류가 나면 catch안의 내용을 실행
   // try에 오류가 없다면 catch는 실행되지 않는다.
}
static String firstLineOfFile (String path, String defaultVal){
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) { return defaultVal; }
} // 파일을 여는 것에 실패하거나, 데이터를 읽지 못했을 때 예외 대신 기본값을 반환하도록 수정된 코드

• 예외처리란, 프로그래머가 예기치못한 예외의 발생에 미리 대처하는 코드를 작성하는 것

    ◦  (나쁜)사용자가 발생시키는 예외에 대해, 개발자가 미리 대처를 해줄 수 있다.

    ◦  실행중인 쓰레드비정상적인 종료를 막고 상태를 정상상태로 유지하는 것이 목적

• 예외가 처리되지 못한경우, 쓰레드은 비정상적으로 종료되며 처리되지 못한 예외의 원인을 JVM의 예외처리기

    (UncaughtExceptionHandler)가 화면에 출력해준다.

Garbage Collector

• 자바는 Garbage Collector 를 갖춘 언어

 메모리를 직접 관리해야하는 C/C++과 달리, 다 쓴 객체를 알아서 회수해준다.

 JVM에서 GC의 스케줄링을 담당

 객체는 힙 영역에 저장되고 스택 영역에 이를 가리키는 주소값이 저장되는데 참조되지 않는
    (자신을 가리키는 포인터가 없는, unreachable) 객체를 메모리에서 제거한다.

Person person = new Person();
person.setName("Mang");

// garbage 발생
person = new Person();
person.setName("MangKyu");

메모리 누수

• 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상

• Old 영역에 계속 누적된 객체로 인해 Major GC가 빈번하게 발생하게 되면서 성능 저하를 불러온다.

• 디스크 페이징이나 OutOfMemoryError 를 일으켜 프로그램이 종료될 수도 있다.

public Object pop() {
	if(size==0) throw new EmptyStackException();
	return elements[--size];
}
public Object pop() {
	if(size==0) throw new EmptyStackException();
	Object result = elements[--size];
	elements[size] = null;
	return result;
}

해결 방법(1) : null 처리

• 해당 참조를 다 썼을 때 null 처리(참조 해제)를 해주는 방법

• 사용이 끝난 참조에 실수로 접근하는 경우 NullPointerException을 내주는 효과도 있다.

• 예외적인 경우에만 사용하자 : 자기 메모리를 직접 관리하는 클래스의 경우

    ◦  그 외에는 프로그램을 필요 이상으로 지저분하게 만들 뿐


해결방법(2) : 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내기

• 변수의 범위를 최소가 되게 정의하자

for(int i=0;i<10;i++){...} // i 의 범위를 10 미만으로 제한했다.

기타 메모리 누수가 발생하는 상황 및 해결책

1) 캐시(cache)에 객체를 넣어두고 잊어버리는 경우

• java.lang.ref 패키지의 WeakReference 클래스를 사용하는 방법

WeakReference<Integer> A = new WeakReference<Integer>(new Integer(1));
Integer B = A.get() // B = 1
B = null; 
C = A.get();
// Integer 객체를 가리키는 참조가 WeakReference 객체 A 뿐 : 1은 GC 대상

• WeakHashMap 을 사용하는 방법

    ◦  일반적인 HashMap의 경우 일단 Map안에 Key와 Value가 put되면 사용여부와 관계없이 해당 내용은 삭제되지 않는다.

    ◦  WeakReference를 이용해 HashMap의 Key를 구현하는 WeakHashMap을 이용하자.

• 백그라운드 스레드(ScheduledThreadPoolExecutor)를 활용하는 방법

    ◦  어떤 작업을 일정 시간 지연 후에 수행하거나, 일정 시간 간격으로 주기적으로 실행해야 할 때 사용

• 캐시에 새 엔트리를 추가할 때 부수 작업을 수행하는 방법

    ◦  LinkedHashMap 을 사용하는 방법 (removeEldestEntry)(10)

         - 일정 사이즈가 차면 가장 오래된 값을 지우고, 그 자리에 방금 들어온 값을 대체 한다.

 

2) 콜백(callback) / 리스너(listener)

• 피호출자(Callee)가 호출자(Caller)를 다시 호출하는 함수 : 콜백함수

    ◦  비동기적 처리를 하기 위한 디자인 패턴의 한 종류

• 콜백을 등록만 하고 명확히 해지하지 않는 경우 메모리 누수가 발생한다

• 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다.

    ◦  WeakHashMap 에 키로 저장하는 방법


참고자료

 

[JAVA] 가비지 컬렉터(Garbage Collector)

객체의 소멸과 가비지 자바는 객체를 생성하는 new 연산자는 있지만 객체를 소멸시키는 연산자는 없음 => 개발자가 마음대로 객체를 소멸시킬 수 없음 객체 소멸이란 new에 의해 생성된 객체 공간

transferhwang.tistory.com

 

클래스와 자원

• 클래스는 보통 하나 이상의 자원에 의존한다.

    ◦  의존한다 = 관계성이 있다 / 자원이 클래스의 동작에 영향을 준다.

    ◦  예 : 맞춤법 검사기는 사전 객체에 의존한다. (사전에 존재하는 단어가 아니면 밑줄)


하나 이상의 자원에 존재하는 클래스(1) : 맞춤법 검사기의 잘못된 구현 방식

public class SpellChecker {
	private static final Lexicon dictionary = ... ;
	private SpellChecker() {} // 객체 생성 방지용 생성자

	public static boolean isValid(String word) { ... }
}
public class SpellChecker {
	private final Lexicon dictionary = ... ;

	public static SpellChecker INSTANCE = new SpellChecker();
	private SpellChecker() {}
	
	public static boolean isValid(String word) { ... }
}

• 위의 두가지 방식은 클래스가 단 하나의 사전 객체에만 의존한다고 가정하고 있다 BAD

    ◦  일반적으로 사전은 하나로 구성되어 있지 않고, 여러개로 나누어져 있다. (전세계 언어 사전 X)

    ◦  제안) final 한정자를 제거하고, 다른 사전으로 교체하는 메서드를 추가?

         -  어색하고 오류를 내기 쉽다 + 멀티스레드 환경에서는 사용할 수 없다.

public class SpellChecker {
	private static Lexicon dictionary = ...;

  public static SpellChecker INSTANCE = new SpellChecker();
	private SpellChecker() {}

	public static boolean isValid(String word) { ... }
  public static void changeDictionary(Lexicon new) { dictionary = new; }
}

의존 객체 주입 방법

• 해당 클래스의 인스턴스를 생성할 때 생성자에게 필요한 자원을 넘겨주는 방법

public class SpellChecker {
		private final Lexicon dictionary;
		
		public SpellChecker(Lexicon dictionary){
				this.dictionary = Objects.requireNonNull(dictionary);
		}
		public boolean is Valid(String word) { ... }
}
public class KoreanDict implements Lexicon { ... }
public class EnglishDict implements Lexicon { ... }

Lexicon kordict = new KoreanDict();
Lexicon engdict = new EnglishDict();

SpellChecker korchecker = new SpellChecker(kordict);
SpellChecker engchecker = new SpellChecker(engdict);

korcheker.isVaild("한국말");
engcheker.isVaild("English");

불변성을 보장하기 때문에 여러 클래스가 같은 의존 객체들을 공유할 수 있다

    ◦  this.dictionary 로 받아서 사용했기 때문

• 정적 팩토리와 빌더에서도 이런 방식으로 의존 객체를 넘겨줄 수 있다.


응용 : 생성자에 자원 팩토리를 넘겨주는 방식 (팩토리 메서드 패턴)

 팩토리 = 호출될 때 마다 특정 타입의 인스턴스를 만들어주는 객체

 Supplier<T> 인터페이스 = 팩토리의 완벽한 표현

    ◦  함수형 인터페이스 : 매개변수는 없고, 반환값만 있다.

~~String supplier= new String();
supplier =  "Hello World";~~

Supplier<String> supplier= () -> "Hello World";

String result = supplier.get();
System.out.println(result); // Hello World

• Supplier<T>를 입력으로 받는 메서드는 한정적 와일드카드 타입(bounded wildcard type)을 사용하여
    팩터리의 타입 매개변수를 제한해야 한다.

    ◦  와일드 카드 : 제네릭 코드에서 물음표(?) 로 표기되어 있는 것. 아직 알려지지 않은 타입 의미.

    ◦  한정적 와일드카드 : (?) 가 무언가를 extend 한다.

    ◦  명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩토리를 넘길 수 있게 됨

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

 


의존 객체 주입 방식의 장점

• 코드의 유연성과 재사용성, 테스트의 용이성을 개선시킴

• 그러나 의존성이 많은 큰 프로젝트에서는 코드를 어지럽게 만들기도 함

    ◦  Spring 등의 의존 객체 주입 프레임워크를 사용하는 것이 좋음

          -  의존 객체를 직접 주입하도록 설계된 API를 사용한다.

@Controller
public class MemberController {
		private final MemberService memberService;

		@Autowired
		public MemberController(MemberService memberService) {
				this.memberService = memberService;
		}
}

• 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다.

• 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI(Dependency Injection), 의존성 주입이라고 한다.