Programming/JAVA

이펙티브 자바 - 2장 객체 주입 , 관리 , 참조

긍정왕웹서퍼 2022. 3. 15. 23:36
728x90

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

여러 클래스가 하나 이상의 자원에 의존합니다. 예를 들면 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스로 구현한 모습을 드물지 않게 볼 수 있습니다. 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않습니다. 

// 정적 유틸리티를 잘못 사용
public class SpellChecker {
	private static final Lexicon dictionary = ...;
    private SpellChecker(){}
    
    public static boolean isValid(String word) {}
    public static List<String> suggestions(String typo){}
}

// 싱글턴을 잘못 사용
public class SpellChecker{
	private final Lexicon dictionary = ...;
    private SpellChecker(){}
    
    public static SpellChecker INSTANCE = new SpellChecker();
    public boolean isValid(String word){}
    public List<String> suggestions(String typo){}
}

 

대신 클래스가 여러 자원 인스턴스를 지원해야 하며 클라이언트가 원하는 자원을 사용해야 합니다. 이 조건을 간단히 충족하는 패턴이 있습니다. 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식입니다. 

// 의존 객체 주입은 유연성과 테스트 용이성을 높인다.
public class SpellChecker {
	private final Lexicon dictionary;
    
    public SpellChecker(Lexicon dictionary) {
    	this.dictionary = Objects.requireNonNull(dictionary);
    }
    public boolean isValid(String word){}
    public List<String> suggestions(String typo){}
}

위 처럼 자원이 몇개이든 의존 관계가 어떻든 상관없이 잘 동작하게 만들며, 불변성을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있고 의존 객체 주입은 생성자, 정적 팩터리, 빌더에 모두 똑같이 응용할 수 있을 정도로 유연성이 높고 재사용성, 테스트의 용이성을 개선할 수 있게 된다.

 

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

똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되며 특히 불변 객체는 언제든 재사용할 수 있다. 

최악의 코드
String s = new String("hello");

위 같은 문장은 실행될 때마다 인스턴스를 새로 생성한다. 이처럼 문장을 구성하는 코드가 자주 사용하는 메서드에 있다면 불필요한 String 인스턴스가 몇번이고 생성되어 자원을 잡아먹게 된다.

이를 개선하자면 
String s = "hello";

인스턴스를 매번 만드는 대신 하나의 인스턴스를 재선언하여 재사용성을 보장할 수 있게 된다.

그리고 생성 비용이 비싼 객체도 더러 있다. JAVA에서 String.matches 정규식은 문자열 형태를 확인하는 가장 쉬운방법 이지만, 성능면에 있어서는 적합하지 않다. 이런 정규식의 성능을 개선하려면 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 메서드가 호출될 때마다 이 인스턴스를 재사용한다. 

public class RomanNumerals {
	private static final Pattern ROMAN = 
    Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    
    static boolean isRomanNumeral(String s) {
    	return ROMAN.matcher(s).matches();
    }
}

객체가 불변이라면 재사용해도 되는 안전성이 보장된다. 하지만 덜 명확하거나 직관에 반대되는 상황도 있다.

그리고 불필요한 객체를 만들어내는 예로 오토박싱(auto boxing)이 있다. 이는 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애는것은 아니다. 개념으로는 비슷하더라도 성능면에서는 확실히 다르다. 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 있진 않는지 주의하자. 다만 요즘 JVM의 가비지 컬렉터는 최적화가 잘되어 있어서 가벼운 객체들을 생성하고 파괴하는 비용은 크게 부담되지 않지만, 객체 풀(pool)을 만드는 것에는 주의하는 것이 좋다. 예를 들어 데이터베이스의 연결같은 경우는 생성 비용이 워낙 비싸서 재사용하는 편이 낫다. 

 

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

C언어 같은 메모리를 프로그래머가 직접 관리해야 하는 언어를 사용하다 JVM과 같은 가비지 컬렉터를 갖춘 언어로 넘어오면 마법과도 같이 편리해진다. 다 쓴 객체를 알아서 회수하기 때문에 메모리 관리에 신경을 덜 쓰이게 된다. 하지만 완전히 그런것은 또 아니다.

메모리 누수는 존재할 수 있으며 여전히 유의해야 한다. 

가비지 컬렉션 언어에서는 의도치않게 발생하는 메모리 누수를 찾기가 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 그래서 몇개의 객체가 많은 객체를 회수하지 못하게 하고 성능에 악영향을 줄 수 있다. 이런 경우 해법은 간단하다. 해당 참조를 다 썼을 때 null처리를 하면 된다. 

다쓴 참조를 null 처리하면 다른 이점도 따라온다. 만약 null처리한 참조를 실수로 사용하면 NullPointerException 을 던지며 종료된다. 

하지만 이런 경우는 객체 참조를 null 처리하는 일이 예외적인 경우여야 한다. 캐시나 스택처럼 자기 메모리를 직접 관리하는 클래스들은 메모리 누수에 주의해야 한다. 또한 리스너(Listener)와 콜백(callback)또한 주범이다. 

 

메모리 누수는 겉으로는 잘 들어나지 않아 시스템에 오랫동안 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나, 힙 프로파일러와 같은 디버깅 도구를 이용해야만 발견되기도 하기 때문에 예방하는 것이 가장 바람직하다.