02. 객체 생성과 파괴

zwundzwzig·2023년 5월 6일
0

이펙티브 자바

목록 보기
1/4
post-thumbnail

객체를 언제 만들어야 하고 만들지 말아야 하는지, 올바른 객체는 무엇이고 불필요한 생성은 무엇인지, 언제 제때 파괴하고 그전에 수행해야 하는 게 무엇인지 알아보자.

생성자 대신 정적 팩터리 메서드를 고려하라

자바에서 public 생성자 함수는 인스턴스를 얻기에 가장 보편적인 방법이다.

하지만 그 언젠가 다양한 인자에 대한 생성자 함수를 만들기 위해 무작정 생성자 함수를 계속 만들던 때가 있더랬다.

그렇기에 정적 팩토리 메서드 static factory method를 사용하자! 이는 객체의 생성을 담당하는 클래스 메서드이며, new 키워드 대신 메서드를 활용해 인스턴스를 만드는 방식이다.

String one = new String("hello");
String two = String.valueOf(”hello”);
// 두 줄은 같은 의미로 String 타입의 인스턴스를 만들 수 있다.

그럼 이제 정적 팩토리 메서드가 좋은 점을 알아보자.

이름을 가질 수 있다.

메서드라는 '행위' 자체에 이름이 있기 때문에 그에 예상되는 행위가 있을 것이고 이를 통해 new 키워드 보다는 직관적으로 소통할 수 있다.

호출될 때마다 새로운 객체를 생성하지는 않아도 된다.

덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 재활용하는 등의 불필요한 객체 생성을 피할 수 있다.

따라서 생성 비용이 큰 객체가 있는 상황에서 성능을 크게 향상 시킬 수 있다. 디자인패턴 중 Flyweight Pattern이 이와 비슷하다.

이러한 인스턴스 통제 클래스를 통해 싱글톤, 인스턴스화 불가, 하나의 인스턴스만 보장(열거 타입, 불변 클래스) 등의 구현이 가능하다.

반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

반환할 객체의 클래스를 자유롭게 설정할 수 있다. 인터페이스 기반 프레임워크 개념의 근간이다.

컬렉션 프레임워크 등에서 이 개념을 활용해 API 외견을 작게 만들었다.

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

정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 하다. 하위 타입이기만 하면 된다.

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

JDBC와 프레임워크를 만들어내는 특징이다.

당장 클래스가 존재하지 않아도 구현체의 위치 정보만으로 객체 생성이 가능하다.

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

즉, 상속 대신 컴포지션을 권장한다.

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

개발자가 임의로 만든 정적 팩터리 메서드 특성 상, 다른 개발자들이 사용시 정퍽 팩토리 메서드를 찾기가 어렵다고 생각 할 수 있다.

하지만 이는 암묵적으로 사용하는 정적 팩터리 메서드 컨벤션으로 해결할 수 있다.

정적 팩토리 메서드 네이밍 컨벤션

from : 하나의 매개 변수를 받아서 객체를 생성
of : 여러개의 매개 변수를 받아서 객체를 생성
getInstance || instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
newInstance || create : 새로운 인스턴스를 생성
get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.

생성자에 매개변수가 많다면 빌더를 고려하라.

정적 팩토리 메소드와 public 생성자에는 똑같은 제약이 하나 있다. 선택적 매개변수가 많은 경우 대응하기 어렵다는 점이다. 이러한 생성자 패턴을 점층적 생성자 패턴이라 한다.

그에 대한 대안으로 매개변수가 없는 생성자로 객체를 만들고 setter로 값을 설정하는 방식인 자바 빈즈 패턴이 있다.

그러나 이 방법 역시 객체 하나 만드는 것에 비해 많은 setter 메서드 호출이 필요하다. 게다가 일관성이 무너져 클래스를 불변으로 만들 수 없으며 스레드 안정성 역시 보장되지 않는다.

점층적 생성자 패턴의 안정성과 자바빈즈의 가독성을 겸비한 빌더 패턴이 그 대안이다.

public class NutritionFacts {
	
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    
    public static class Builder {
    	
        private final int servingSize;
        private final int servings;
        
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        
        public Builder (int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        
        public Builder calories(int val) { calories = val; return this;}
        public Builder fat(int val) { fat = val; return this;}
        public Builder sodium(int val) { sodium = val; return this;}
        public Builder carbohydrate(int val) { carbohydrate = val; return this;}
        
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
        
        public NutritionFacts(Builder builder) {
        	servingSize = builder.servingSize;
            servings = builder.servings;
            calories = builder.calories;
            fat = builder.fat;
            sodium = builder.sodium;
            carbohydrate = builder.carbohydrate
        }
    }
    
}

빌더 패턴을 적용하니 setter가 사라짐으로써 무분별한 데이터 접근을 막음으로써 불변성을 지킬 수 있다. 그리고, 빌더안의 세터 메서드들은 자신을 반환하기때문에 연쇄적으로 호출을 할 수 있는데, 이를 플루언트 API(fluent API) 혹은 메서드 체이닝(method chaining)이라 한다.

이러한 빌더패턴은 명명된 선택적 매개변수(named optional parameter)를 흉내낸 것이다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋은데, 추상 클래스에서는 추상빌더를, 구체 클래스에서는 구체 빌더를 갖게함으로써 계층적으로 빌더를 빌드 할 수도 있다.

이렇게 하위 클래스의 메서드가 상위의 메서드가 정의한 반환 타입이 아니라 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(covariant return typing) 이라 한다.

이 기능을 사용하면 클라이언트가 형변환에 신경쓰지 않아도 된다. 게다가 아이디나, 일련번호와 같은 특정 필드는 빌더가 알아서 채우게 할 수도 있다.

그러나, 코드작성에 비용이 더 커진다는 단점이 있다. 그리고 필드의 수가 적을수록 가치가 떨어지는데 최소 4개 이상은 돼야 쓸만하다.

하지만, 유지 보수 및 기능 추가를 하다보면 변수는 계속해서 늘어나고 그렇기에 뒤늦게 작업을 하는 것보다 처음부터 빌더패턴을 적용하는게 비용을 아낄 수도 있으니 고려해서 사용하자.

생성자나 정적 팩터리가 처리할 매개변수가 많다면 빌더 패턴을 고려하자. 점층적 생성자보다 코드를 읽고 쓰기 편하고, 자바빈즈보다 훨씬 안전하기 때문이다.

private 생성자나 enum 타입으로 싱글턴임을 보증하라.

인스턴스를 하나만 갖는 클래스인 싱글턴 패턴으로 불필요한 인스턴스 생성을 막을 수 있지만, 테스트할 때 어려움이 있다.

만약 해당 싱글톤 클래스가 인터페이스의 구현체라면 해당 인터페이스를 테스트용으로 구현하는 가짜 구현체로 대체 가능하지만 그게 아니라면 싱글톤 인스턴스를 가짜 구현으로 대체할 수 없기 때문이다.

싱글톤을 만드는 방식은 두 가지가 있다.
public static final 방식의 싱글턴

public class Beenzino {
		public static final Beenzino INSTANCE = new Beenzino();
		private Beenzino() {
        	if(Object.nonNull(INSTANCE)) throws new RuntimeException(); 
            // 두 번째 생성자 호출 시 던질게 예외.
        }
		...
        public void doUpAllNight() {...}
}

private 생성자는 public static final 필드인 Beenzino.INSTANCE를 초기화 할 때 단 한 번 호출되고 그 후엔 접근가능한 생성자가 없기에 외부에서 해당 클래스의 인스턴스를 만들 수 없다.

물론, Reflection API인 AccessibleObject.setAccessible를 이용하면 private 생성자도 호출이 가능한데, 이를 막기 위해서는 생성자를 수정해 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.

이 방식의 장점은 간결하고, API에 싱글턴임이 드러나 따로 어떤 메소드를 호출할 필요도 없고, final 필드이기 때문에 다른 객체를 참조할수도 없다는 점이 있다.

정적 팩터리 방식의 싱글턴

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

정적 팩토리 메서드인 getInstance로 인스턴스를 반환하며 Reflection API를 제외하면 다른 인스턴스 빈지노를 만들 수 없다.

이 방식은 getInstance 메소드만 지워 싱글톤이 아닌 클래스로 쉽게 변경 가능하고, 제네릭 싱글턴 팩토리로 만들 수 있고, 마지막으로 정적 팩토리의 참조를 Supplier<> 타입을 이용할 수 있다는 장점이 있다.

결과적으로 정적 팩토리 메소드의 장점이 필요없는 상황이라면 public static final 생성자를 이용한 싱글톤 방식을 사용하면 된다.

이러한 방식들에는 리플렉션 이외에도 직렬화-역직렬화 과정에서 문제가 생길 수도 있다.

단순히 Serializable 인터페이스를 구현한다면 역직렬화할 때마다 새로운 인스턴스가 생기기 때문이다. 이는 readResolve 메서드로 해결할 수 있다.

더 궁극적으로 직렬화나 리플렉션에 대한 공격을 대비하기 위해 원소가 하나인 enum 타입을 활용하자.

단, 만들려는 싱글턴이 Enum 이외의 클래스를 상속해야 한다면 사용할 수 없다.

인스턴스화를 막으려거든 private 생성자를 사용하라.

우리는 내부적으로 인스턴스 변수와 메소드가 필요 없는 유틸리티 클래스를 만들 때가 있다.

대표적으로 java.lang.Math, java.util.Arrays와 같이 수학 연산이나 배열에 관련된 메서드들을 모아놓는 유틸리티 클래스들을 만들거나, 팩토리 패턴에서 정적 메서드를 모아놓거나 final 클래스와 관련된 메서드를 모아놓기도 한다.

이러한 클래스에 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어 의도치 않게 인스턴스화할 수 있는 클래스가 생긴다.

그래서 private 접근 제어자를 활용해 생성자를 만들고 그 안에 에러를 던지는 방식으로 인스턴스화를 막을 수 있다.

자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

싱글톤과 정적 팩토리 메소드를 통한 유틸리티 클래스는 비용소모를 줄여주지만, 해당 자원의 의존성이 커진다면 문제가 생긴다.

대신 이런 자원을 클래스가 자체적으로 만드는게 아니라 인스턴스를 생성할 때 자원을 생성자(혹은 정적 팩터리나 빌더)에 전달해 인스턴스별로 맞는 자원을 갖도록 해주는 의존관계 주입을 사용하자.

public class 마블 {
    private final 가오갤 gog;

    public 마블(가오갤 gog) {
        this.gog = gog;
    }

    public Theater watch(Movie movie, int money) {
        if (!movie.isPayable(money)) 
            throw new IllegalArgumentException("돈이 부족합니다.");
        int result = gog.discount(movie.getPrice());
        
        return Theater.of(movie, result);
    }
}

물론 이런 의존 객체 주입을 통해 유연성 및 테스트 용이성을 개선할 수 있지만, 의존성이 수백, 수천개가 넘으면 역시 비용소모가 상당히 높은데 이런 경우 의존 객체 주입 프레임워크 스프링으로 해결할 수 있다.

불필요한 객체 생성을 피하라.

new String('...') 생성자로 똑같은 문자열 객체가 생성되지만 각각 다른 주소를 바라보는 문제가 생긴다. 그래서? 그냥 '' 안에 하나의 문자열만 만들어 재사용성을 높이자.

또한 정적 팩토리 메서드를 활용해 캐싱 및 재사용성을 높일 수 있다.

불필요한 객체를 만드는 대표적인 예시가 오토박싱(기본 타입과 박싱된 기본 타입을 혼용할 때 자동으로 상호 변환하는 기술)이다. 기본 타입과 박싱된 기본 타입의 구분을 흐리지만 또 구분을 아예 없애진 못하기 때문이다. 그래서 박싱된 기본 타입(Long, Integer) 대신 그냥 기본 타입(long, int)을 사용하자.

다 쓴 객체 참조를 해제하라.

아무리 가비지 컬렉터가 있더라도, 메모리 누수가 늘어나 디스크 페이징, OutOfMemoryError 등의 에러에 직면하지 않도록 직접 메모리 관리하는 습관을 들이자.

책에서는 스택을 사용할 때, 스택이 커졌다 줄어들 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는 문제 예시를 들었다. 스택이 그 객체들의 다 쓴 참조 (obsolete reference)를 갖고 있기 때문에 발생되는 문제이다.

이외에도 캐시, 리스너 혹은 콜백 등에서 메모리 누수 문제가 생긴다.

극단적으로는 null을 사용해 참조 해제하는 방법부터 약한 참조 혹은 remove와 같은 해제 메서드를 활용해 메모리를 직접 관리할 수 있는 인식을 기르자.

finalizer와 cleaner 사용을 피하라.

자바에는 finalizer & cleaner 라는 두 가지 객체 소멸자를 제공한다. 근데 저자는 강한 어조로 쓰지 말라고 한다. 있었는데? 아니, 없어요

이들은 제때 실행되지 않을 수도 있는 등 예측할 수 없고 상황에 따라 위험을 야기시키기 때문에 사용하지 말라고 한다.

대신 파일이나 스레드 등 종료해야 할 자원을 담는 클래스에선 그저 AutoColseable 인스턴스를 구현하고 close 메서드로 닫으면 그만이다.

이 두 객체 소멸자는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 아니, 그마저도 불확실성과 성능 저하에 주의해야 하니까 사용하지 말자.

try-finally보다는 try-with-resources를 사용하라.

자바에선 close 메서드로 직접 닫아야하는 자원이 많고 전통적으로 try-finally가 쓰였다. 그러나 단순 try-finally 사용은 디버깅에 상당히 어려움을 야기한다.

닫아야 하는 모든 자원은 try 문에 넣어두고 실행시키고 catch를 적극적으로 활용해 코드의 중첩을 줄이고 가독성을 높이자.

🧷 참조 교재

  • [프로그래밍 인사이트] 이펙티브 자바 - 조슈아 블로크
profile
개발이란?

0개의 댓글