공부함

객체 생성과 파괴 본문

Java/이펙티브 자바

객체 생성과 파괴

찌땀 2024. 1. 2. 22:22

[아이템 1] 생성자 대신 정적 팩터리 메서드를 고려하자

정적 팩터리 메서드는 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드다.

장점

1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체로는 반환될 객체의 특성을 제대로 설명하지 못한다.

정적 팩터리 메서드는 이름만 잘 지으면 반환될 객체의 특성을 잘 묘사할 수 있다.

 

2. 호출될 떄마다 인스턴스를 새로 생성하지 않아도 된다.

생성자는 호출될 때마다 인스턴스를 만들어야 하지만, 정적 패터리 메서드는 그렇지 않다.

반복되는 요청에 같은 객체를 반환할 수 있다. 

따라서 언제 어느 인스턴스를 살아있게 할지 통제할 수 있다. 이런 클래스를 인스턴스 통제 클래스라 한다.

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

반환될 객체의 타입을 자유롭게 선택할 수 있어 유연하다.

API를 만들 때 구현 클래스를 공개하지 않고도 반환할 수 있다. API를 작게 유지할 수 있다. 

인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크의 핵심 기술이다.

public interface Test {
    public static Test getImpl() {
        return new TestImpl();
    }
}

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

반환 타입의 하위 타입이기만 하면 어떤 객체를 반환하든 상관없다.

EnumSet 클래스는 정적 팩터리 메서드에서 원소가 64개 이하면 RegularEnumSet을, 65개 이상이면 JumboEnumSet을 반환한다. 

클라이언트는 두 클래스에 관해 알 필요 없이 EnumSet의 하위 클래스를 반환받기만 하면 된다.

 

5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이런 유연함은 JDBC와 같은 서비스 제공자 프레임워크를 만드는 근간이 된다. 

서비스 제공자 프레임워크에서 프레임워크가 구현체(제공자)를 클라이언트에게 제공하는 역할을 해서 클라이언트를 구현체로부터 분리해준다. 

서비스 제공자 프레임워크는 3가지로 구성된다.

  • 서비스 인터페이스 : 구현체 동작 정의
  • 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
  • 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용 
    • 원하는 구현체 조건 명시
    • 유연한 정적 팩터리

경우에 따라 한가지를 추가한다.

  • 서비스 제공자 인터페이스
    • 구현체 동작을 정의하는 서비스 인터페이스의 객체를 생성하는 팩터리 객체를 설명
    • 서비스 제공자 인터페이스가 없다면 구현체를 객체로 만들 때 리플렉션을 사용해야 함

서비스 제공자 프레임워크에서 서비스 접근 API는 제공자가 제공하는 것 보다 풍부한 인터페이스를 클라이언트에게 제공할 수 있다. 이것을 브리지 패턴이라 한다. 프레임워크도 일종의 강력한 서비스 제공자다. 

단점

1. 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

상속을 위해서는 public이나 protected 생성자가 필요하다. 따라서 정적 팩터리 메서드만 제공하면 상속이 불가능하다.

하지만 이 단점은 상속보다 컴포지션(필드로 갖게 하는 것)을 사용하도록 유도하고, 불변 타입으로 만들려면 이 제약을 지커야 하므로 마냥 단점으로 볼 수는 없다. 

 

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

생성자처럼 API 설명에 명확하게 드러나지 않는다.

API 문서를 잘 작성하고, 흔히 알려진 메서드 명명규칙을 지키는 것이 좋다.

  • from : 매개변수 하나를 받아서 해당 타입의 인스턴스 반환 
  • of : 여러 매개변수를 받아서 적합한 타입의 인스턴스 반환
  • valueOf : from, of의 자세한 버전 
  • instance, getInstance : 매개변수를 받는다면 매개변수로 명시한 인스턴스를 반환한다. 하지만 같은 인스턴스임을 보장하지는 않는다.
  • create, newInstance : instance, getInstance와 같지만 매번 새로운 인스턴스를 반환한다.
  • getType : getInstance와 같지만, 생성할 클래스가 아닌 다른 클래스에 메서드를 정의할 때 쓴다. Type은 생성할 클래스 명이다. 
    • Car car = Factory.getCar();
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 메서드를 정의할 때 쓴다.
  • type : getType 또는 newType의 간결한 버전 
    • Car car = Factory.car();
public 생성자와 정적 팩터리 메서드 장단점을 이해하고 사용해야 한다.
다만 정적 팩터리 메서드가 더 좋은 경우가 많다.

 

[아이템 2] 생성자에 매개변수가 많다면 빌더를 고려하자

정적 팩터리나 생성자는 공통적으로 선택적 매개변수가 많으면 대응하기 어렵다.

선택적 매개변수가 많을 때 사용할 수 있는 3가지 패턴이 있다.

점층적 생성자 패턴

점층적 생성자 패턴은 필수 매개변수만 받는 생성자, 필수 매개변수 + 선택 매개변수 1개 받는 생성자, 필수 매개변수 + 선택 매개변수 2개 받는 생성자, .. 이런식으로 늘려나가는 패턴이다. 

점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 일기 어렵다.

  • 사용자가 원치 않는 매개변수까지 포함하는 경우가 많고 값을 지정해줘야 한다.
  • 클라이언트가 실수로 매개변수 순서를 바꿔 건내줘도 컴파일타임에 잡지 못한다.

자바빈즈 패턴 

자바빈즈 패턴은 setter들로 값을 설정하는 방식이다. 

점층적 생성자 패턴에 비해 인스턴스를 만들기 쉽고, 읽기 쉽다.

  • 객체 하나를 만들려면 메서드를 여러개 호출해야 한다.
  • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다.
  • 매개변수가 유효한지 생성자에서 확인할 수 없다
  • 불변 클래스로 만들 수 없다.

빌더 패턴

점층정 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성 합친 방법이다. 

  • 필수 매개변수만으로 생성자 or 정적 팩터리 메서드를 호출해서 빌더 객체를 얻는다.
  • 빌더 객체를 이용해 선택 매개변수들을 설정한다. 
  • build() 를 호출해 객체를 얻는다. 
public class Robot {
    private final int power;
    private final int age;
    private final int velocity;
    private final int battery;

    public static class Builder {
        // 필수매개변수
        private final int power;
        private final int age;

        // 선택매개변수
        private int velocity = 10;
        private int battery = 0;

        // 빌더 생성자
        public Builder(int power, int age) {
            this.power = power;
            this.age = age;
        }

        // 빌더 세터, 빌더 자기 자신을 반환함
        public Builder velocity(int val) {
            velocity = val;
            return this;
        }

        public Builder battery(int val) {
            battery = val;
            return this;
        }

        public Robot build() {
            return new Robot(this);
        }
    }

    private Robot(Builder builder) {
        power = builder.power;
        age = builder.age;
        velocity = builder.velocity;
        battery = builder.battery;
    }

}

빌더는 클래스 안에 정적 멤버 클래스로 만들어둔다.

정적 멤버 클래스는 다른 클래스 안에 선언되는 클래스다. 외부 클래스의 private 멤버에 접근할 수 있다. 

이외에는 다른 정적 멤버와 같은 규칙을 적용받는다.

 

빌더의 세터는 빌더 자기 자신을 반환해서 연쇄적으로 호출이 가능하므로 flunet api 또는 method chaining이라 한다.

 

        Robot robot = new Robot.Builder(1,2)
                .velocity(2)
                .battery(10)
                .build();

클라이언트는 빌더의 생성자로 빌더를 생성하고, 세터로 선택매개변수 값을 지정하고 build()를 호출해서 외부 객체인 Robot을 얻는다. 이 코드는 읽고 쓰기 쉽다. 

검증은 빌더의 생성자와 세터에서 하면 된다.

그리고 build()에서 호출하는 외부 클래스의 생성자에서 불변식을 검사하면 된다.

불변 : 변경을 허용하지 않음
불변식 : 프로그램이 실행되는 동안, 정해진 기간 동안 반드시 만족해야하는 조건 
ex) 리스트의 크기는 0 이상, start필드의 값은 end 필드의 값보다 앞서야 함

 

빌더패턴은 계층구조를 갖는 클래스와 함께 쓰기 좋다. 

각 계층의 클래스에 빌더를 멤ㅁ버로 정의한다. (추상 클래스- 추상 빌더, 구체 클래스- 구체 빌더)

추상 클래스의 Builder.build()는 추상 메서드로 반환형이 추상 클래스다.

구체 클래스의 Builder.build()는 구체클래스를 반환하게 한다. 

이것을 공변반환타이핑이라 한다. 클라이언트는 형변환에 신경쓰지 않고 빌더를 사용할 수 있다. 

 

빌더의 또다른 장점으로 가변인수를 여러개 사용할 수 있다.

원래는 가변인수가 여러개면 구별할 수가 없어서 사용 불가하다.

 

빌더 패턴은 빌더를 생성하는 비용이 든다는 단점이 있다. 또 매개변수가 4개 이상은 되어야 의미있는 패턴이다.

하지만 api는 매개변수가 적었다가 많아지는 경향이 있으므로 애초에 빌더를 선택하는 것이 나을 때가 많다. 

[아이템 3] private 생성자나 열거 타입으로 싱글턴임을 보장하라

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어렵다. 

싱글턴을 mock으로 대체할 수 없기 때문이다. 

싱글턴을 만드는 방식 3가지가 있다.

 

1. public static final 필드 

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

private 생성자는 INSTANCE를 초기화 할 때 한번만 호출된다. 

다른 생성자가 없기 때문에 싱글턴이 보장된다. 

예외적으로 권한이 있는 클라이언트는 리플렉션 API를 사용해 private 생성자를 호출할 수 있다.

이를 막기 위해서는 생성자에서 두번째 객체를 생성하려고 하면 예외를 던지면 된다.

 

  • api에 싱글턴임이 명백히 드러난다
  • 간결하다

 

2. 정적 팩터리 메서드 

public class Robot {

    public static final Robot INSTANCE = new Robot();
    public static Robot getInstance(){return INSTANCE;}
    private Robot(){
        ..
    }
}

 getInstance는 항상 같은 객체의 참조를 반환하므로 싱글턴이 보장된다. (리플렉션 api 예외는 같다)

  • api를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
    • getInstance()가 다른 객체를 반환하게 하면 된다.
  • 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
  • 정적 팩터리 메서드 참조를 공급자(supplier)로 사용할 수 잇다. 

 

1, 2 방식은 싱글턴 클래스를 직렬화하려면 Serializable을 구현하는 것 만으로 부족하다. 

모든 인스턴스를 transient라고 선언하고 readResolve 메서드를 제공해야 한다. 

이렇게 하지 않으면 직렬화된 객체를 역직렬화 할 때마다 새로운 객체가 만들어진다.

 

직렬화 : 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이
트(byte) 형태로 데이터 변환하는 기술. Serializable 인터페이스를 구현해야 직렬화 할 수 있다. 

 

3. 원소가 하나인 열거 타입 선언 

public enum Robot {
    INSTANCE(10);

    Robot(int battery) {
        this.battery = battery;
    }

    private int battery;

    public void move(){
        
    }
}

열거형 값은 해당 자료형의 인스턴스이다.

원소가 하나인 열거 타입을 선언해서 싱글턴으로 사용한다.

public 방식보다 간결하고, 직렬화하기 쉽다. 

 

대부분 상황에서는 이 방법이 가장 좋다. 다만 다른 클래스를 상속해야 한다면 사용할 수 없는 방법이다.

[아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라

정적 메서드, 정적 필드만으로 구성된 클래스는 인스턴스로 만들어 쓰려고 만든 클래스가 아니다. 

하지만 생성자를 명시하지 않으면 public 생성자가 만들어진다. 

 

추상 클래스로 만드는것으로는 객체화를 막을 수 없다.

추상 클래스를 상속해서 객체화하면 되기 때문이다.

또 사용자는 추상 클래스를 상속해서 사용하라는 의미로 받아들일 수도 있다.

 

    // 객체 생성을 막기 위함 
    private Robot(){
        throw new AssertionError();
    }

따라서 private  생성자를 추가해서 클래스의 인스턴스화를 막을 수 있다. 

컴파일러가 기본 생성자를 만드는 경우는  명시된 생성자가 없을 때 뿐이기 때문이다.

 

  • 클래스 안에서 생성자를 호출하는 경우도 막기 위해 AssertionError를 던진다.
  • 생성자가 존재하는데 호출할 수 없다. 직관적이지 않다
    • 적절한 주석을 달아준다.
  • 상속을 불가능하게 하는 효과도 있다.
    • 모든 생성자는 상위 클래스의 생성자를 호출하게 되는데, private이므로 호출할 수 없다.

[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 

다른 자원에 의존하는 클래스를 정적 유틸리티 클래스나 싱글턴으로 구현하는 경우가 많다.

정적 유틸리티 클래스 : 바로 위에서 다룬 static 멤버, static 함수만 있는 클래스. 객체 생성을 하지 않음

 

하지만 사용하는 자원에 따라 동작이 달라지는 클래스에는 이 두 방법이 적절하지 않다.

여러 자원을 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 하는 경우에는 의존 객체 주입을 사용하자.

public class Robot {
    private final Arm arm;

    public Robot(Arm arm) {
        this.arm = arm;
    }
}

인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 것이다.

  • 의존 객체(자원)의 불변을 보장하여 여러 클라이언트가 안심하고 공유할 수 있다.
    • 싱글턴이나 정적 유틸리티 방식에서 자원을 교체하려면 필드에 final을 해제하고 교체 메서드를 만들어야 한다. 이 방식은 불변을 보장하지 않는다.

이 패턴의 변형으로 생성자에 자원 팩터리를 넘겨줄 수 있다.

팩터리는 호출할 때 마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체다.

팩터리 메서드 패턴을 구현한 것이다. 

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

팩터리는 한정적 와일드카드 타입을 사용해 타입 매개변수를 제한한다.

클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.

즉 위의 예시에서 Supplier는 Tile의 하위 타입이라면 뭐든 생성할 수 있는 팩토리다.

 

의존 객체 주입은 유연성과 테스트 용이성을 개선해준다. 하지만 의존성이 매우 많은 프로젝트에서는 코드를 어지럽게 만들 수도 있다.  Dagger, Guice, Spring 같은 DI 프레임워크를 사용해 해소할 수 있다.

클래스가 내부적으로 하나 이상의 자원에 의존하고, 자원이 클래스 동작에 영향을 준다면 필요한 자원(혹은 자원을 생성하는 팩토리를) 생성자에(혹은 정적 팩터리나 빌더에) 넘겨주자.

 

[아이템 6] 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 것이 좋다. 

 

String

// 1
String s = new String("jidam");
// 2
String s = "jidam";

1번은 실행될 때마다 String 객체를 생성한다. 생성자에 넘겨진 "jidam"과 생성자의 호출 결과로 만드려는 객체가 같다.

이런 코드가 반복문 안에 있으면 쓸데없는 String 객체가 많이 만들어진다.

2번은 하나의 String을 재사용하고 같은 문자열을 사용하는 모든 코드가 같은 객체를 재사용한다.

 

정적 팩터리 메서드

정적 팩터리 메서드를 제공하는 불변 클래스에서 불필요한 객체 생성을 피할 수 있다.

생성자는 호출할 때마다 새로운 객체를 만들지만 정적 팩터리 메서드는 그렇지 않다. 

가변 객체라도 사용중에 변경되지 않는다면 재사용할 수 있다. 

 

생성 비용이 비싼 객체

public class Robot {
    private static final ExpensiveArm ARM = new ExpensiveArm();
    
    static int punch(){
        return ARM.punch();
    }
}

생성 비용이 비싼 객체가 반복해서 필요하다면 캐싱해서 재사용하자.

캐싱한다는 것은 static 멤버로 직접 초기화해놓는 것이다. 

 

어댑터

객체가 불변이라면 재사용해도 안전함이 명백하다.

하지만 덜 명백하거나 직관에 반대되는 상황도 있다.

어댑터는 실제 작업은 뒷단 객체에 위임하고, 본인은 제 2의 인터페이스 역할을 하는 객체다.

어댑터(뷰 라고도 한다)는 뒷단 객체 외에는 관리할 객체가 없기 때문에 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.

 

Map의 keySet 메서드는 Map 객체의 키 전부를 담은 Set 뷰를 반환한다. 

keySet을 호출할 때 마다 새로운 Set이 만들어질 것 같지만 모두가 똑같은 Map을 대변하므로 매번 같은 Set을 반환할지도 모른다. (??)  뒷단 객체인 Map 하나당 하나의 어댑터 Set이 있으면 충분하다. 

 

오토 박싱 

오토박싱은 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.

예컨대 long을 Long에 더할 때마다 새로운 Long 객체가 생성된다.

따라서 되도록이면 박싱된 기본 타입보다 기본 타입을 사용하는 것이 좋다.

의도치 않은 오토박싱이 발생하지 않게 주의하자.

 

[아이템 7] 다 쓴 객체 참조를 해제하라

가비지 컬렉터가 있는 언어에서는 메모리 관리를 신경쓰지 않아도 된다는 오해를 할 수 있다.

다시 쓰지 않을 참조인 다 쓴 참조를 살려두는 경우 문제가 발생한다.

참조를 살려두면 참조당하는 가비지 컬렉터는 객체 뿐만 아니라 그 객체가 참조하는 모든 객체를 회수하지 못한다.

 

elements[size] = null

이를 해결하기 위해 다 쓴 참조를 null처리 해야 한다.

이렇게 null처리 해주면 다 쓴 참조를 사용하려 할 경우 NPE가 발생해 오류를 조기에 발견할 수 있다는 장점도 있다.

 

하지만 모든 경우에 null 처리를 하는 것은 프로그램을 지저분하게 만든다. 

null 처리가 꼭 필요한 예외적인 경우에만 null 처리를 해야 한다.

  • 자기 메모리를 직접 관리하는 클래스
    • 다 쓴 참조가 다 쓴 참조인지는 프로그래머만 알 수 있고 가비지 컬렉터가 알 수 없다. 
    • null처리를 해서 가비지 컬렉터에게 알려줘야 한다.
  • 캐시 
    • 객체 참조를 캐시에 넣고 나서 방치하는 경우가 많다.
    • 외부에서 key를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 경우에 WeakHashMap을 사용하자
    • 캐시 엔트리의 유효 기간을 명확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리 가치를 조금씩 떨어트리는 방법을 사용한다.
      • LinkedHashMap의 removeEldestEntry 메서드 
  • 리스너 or 콜백 
    • callback : 특정 이벤트가 발생하면 콜백함수를 호출하는 것 
    • 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백이 쌓인다.
    • 콜백을 약한참조로 저장하면 가비지 컬렉터가 즉시 수거한다.
      • 약한 참조 : WeakReference 클래스로 생성 가능, 참조하는 객체가 null이 되면 가비지 컬렉터가 수거함
      • WeakHashMap에 키로 저장

[아이템 8] finalizer와 cleaner 사용을 피하라

자바는 객체 소멸자 finalizer와 cleaner를 제공한다. 일반적으로 예측할 수 없고, 불필요하다.

  • 수행 시점을 보장하지 않는다
    • 제때 실행되어야 하는 작업을 할 수 없다
    • 객체에 접근할 수 없게 된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴 지 알 수 없다
    • 가비지 컬렉터 알고리즘에 달렸다
  • 수행 여부를 보장하지 않는다
    • 상태를 영구적으로 수정하는 작업에 절대 finalizer나 cleaner에 의존하면 안된다.
  • finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남아도 그 순간 종료된다.
    • cleaner에서는 이러한 문제가 발생하지 않는다
  • 성능이 떨어진다(시간)
  • finalizer는 finalizer 공격에 노출되어 보안 문제를 일으킬 수 있다.
    • 생성자나 직렬화 과정에서 예외가 발생하면 생성되다 만 객채에서 악의적인 하위 클래스의 finalizer가 실행될 수 있다. 
    • final인 클래스는 하위 클래스를 만들 수 없으니 안전하다 
    • final이 아닌 클래스의 경우 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만드고 final로 선언하자.

파일, 스레드 등 종료해야 할 자원을 담고 있는 클래스에서 finalizer, cleaner를 대체하는 방법은 AutoCloseable을 구현하고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하는 것이다. 이 때, 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋다. close 메서드에서 객체가 닫혔음을 필드에 기록하고, 다른 메서드에서는 이 필드를 검사해서 객체가 닫힌 후에 메서드가 호출되었다면 IllegalStateException을 던진다.

 

finalizer와 cleaner의 쓰임새는 2가지가 있다.

  • 자원이 소유자가 close 메서드를 호출하지 않는 것에 대한 안전망
    • 수행 시간, 수행 여부를 보장하지 않지만, 안하는 것보다는 낫기 때문
  • native peer와 연결된 객체
    • native : java가 아닌 c, cpp같은 다른 프로그래밍 언어
    • native api로 native와 연결된 java 객체를 만든다.
    • 일반 java 객체가 아니므로 gc가 존재를 알지 못한다.
    • 성능 저하를 감당할 수 있고, 네이티브 피어가 심각한 자원을 갖고 있지 않은 경우 사용
      • 아닌 경우 close 메서드를 사용하자
import java.lang.ref.Cleaner;

public class Robot implements AutoCloseable {

    private static final Cleaner cleaner = Cleaner.create();

    private static class Garbage implements Runnable {
        int junkFiles;

        Garbage(int junkFiles) {
            this.junkFiles = junkFiles;
        }

        @Override
        public void run() {
            System.out.println("청소");
            junkFiles = 0;
        }
    }

    private final Garbage garbage;

    private final Cleaner.Cleanable cleanable;

    public Robot(int junkFiles) {
        garbage = new Garbage(junkFiles);
        cleanable = cleaner.register(this, garbage);
    }

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

run은 두가지 경우에 호출된다. 

  • 클라이언트가 Robot의 close 메서드를 호출할 때 close 안에서 cleanble.clean을 호출하고 clean에서 run을 호출한다.
  • gc가 Robot으리 회수할 때 까지 클라이언트가 close를 호출하지 않으면 cleaner가 Garabge의 run을 호출한다. 
    • 시간 보장 x, 여부 보장 x 
    • 안전망 역할

Garbage는 Robot을 참조해서는 안된다. 순환참조가 생겨 가비지 컬렉터가 Robot을 회수할 기회가 오지 않는다.

정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문에 정적 중첩 클래스를 사용한다.

 

cleaner는 안전망으로 쓰였다.

클라이언트가 try-catch-resources 블록으로 Robot 객체 생성을 감싼다면 자동 청소가 필요하지 않다.

하지만 클라이언트가 Robot을 그냥 생성한다면 자동 청소가 될 수도 안될수도 있다.

 

[아이템 9] try-finally보다는 try-with-resources를 사용하라

java에서는 close를 호출해서 닫아줘야 하는 자원이 많다. 클라이언트가 닫지 않을 때를 대비해 안전망을 사용하지만 보장되지 않는다. 전통적으로 try-finally를 사용해 자원이 제대로 닫힘을 보장했다. 하지만 이 방식은 자원이 둘 이상이면 지저분하다. 또한 try문과 finally이 close에서 예외가 발생할 경우 close 예외가 앞의 예외를 집어삼켜 디버깅이 힘들다.

 

try-with-resources로 이러한 문제를 해결할 수 있다.

자원이 AutoCloseable 인터페이스를 구현해야 한다. 

닫아야 하는 자원을 뜻하는 클래스는 AutoCloseable을 구현하도록 하자.

 

    public static void main(String[] args) throws  IOException{
        try (InputStream in = new FileInputStream("path")){
            in.read();
            // ~~ 
        }
    }

try-with-resources 문을 사용하면 짧고 읽기 수월하고, 문제 진단에도 유리하다.

코드에 보이지 않는 close에서 발생한 예외는 숨겨지고 read에서 발생한 예외가 보여진다.

숨겨진 예외도 스택 추적 내역에서 확인할 수 있다.

 

try-with-resources 역시 catch절을 사용할 수 있다. 

 

 

 

 

 

 

 

 

 

 

'Java > 이펙티브 자바' 카테고리의 다른 글

클래스와 인터페이스  (1) 2024.01.05
모든 객체의 공통 메서드  (0) 2024.01.04