본문 바로가기
  • GDG on campus Ewha Tech Blog
3-2기 스터디/이펙티브 자바 독서

1주차 - 2장 객체 생성과 파괴 (1-6)

by SoohyeonB 2022. 4. 3.
GDSC 이펙티브 자바 스터디 1주차 기록입니다.
2장 객체생성과 파괴, 아이템 1~6를 읽고 스터디원들이 정리한 글입니다. 
아이템 1,2(배수현) 아이템 3,4 (이혜빈) 아이템 5,6 (정수진)

 

2장 객체 생성과 파괴

1. 객체를 만들지 유무에 대한 상황 판단
2. 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법
3. 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령

 

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

클래스는 생성자와 별도로 정적 팩터리 메서드 (static factory method)를 제공한다.

장점

1 이름을 가질 수 있다.

생성자에 제공하는 파라미터가 거기에서 반환하는 객체를 잘 설명하지 못하는 경우에 이름을 가질 수 있는 static 팩토리를 사용하는 것이 알아보기 보다 더 쉽고 읽기 편하다.

예를 들어,

public Foo(String name) {
		this.name = name;
}

public static void main(String[] args){
		Foo foo = new Foo("keesun");
}

이렇게 public 생성자를 사용해서 객체를 생성하는 방식을 사용하면, foo라는 변수가 어떤 것을 의미하는지 정확하게 알기 어렵다.

public static Foo withName(String name) {
		return new Foo(name);
}

public static void main(String[] args){
		Foo foo2 = Foo.withName("keesun");
}

따라서 위 코드와 같이 이름을 가질 수 있는 정적 팩토리 메서드를 사용해주면 “keesun”이라는 string이 어떤 것을 의미하는지 이름으로 바로 알 수 있다.

또한 하나의 시그니처로는 하나의 생성자만 만들 수 있다. 입력 매개변수를 달리해서 생성자를 추가할 수 있지만, 옳지 않은 방법이다.

String name;
String address;

public Foo(String name) {
		this.name = name;
}
public Foo(String address) {
		this.addresss= address;
}

따라서 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸어 특징을 살린 이름을 지어준다.

public static Foo withName(String name) {
		return new Foo(name);
}
public static Foo withAddress(String address) {
		return new Foo(address);
}

 

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

불변(immutable) 클래스(아이템 17)인 경우나 매번 새로운 객체를 만들 필요가 없는 경우에는 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할 지를 철저히 통제할 수 있다.

 

💡 왜 인스턴스를 통제할까?

  1. 인스턴스를 통제하면 클래스를 싱글턴(아이템3)으로도, 인스턴스화 불가(아이템4)로도 만들 수 있다.
  2. 불변 값 클래스(아이템17)에서 동치인 인스턴스가 단 하나뿐임으로 보장할 수 있다.
  3. 플라이웨이트 패턴의 근간이 된다.
  4. 열거타입은 인스턴스가 하나만 만들어짐을 보장한다.

 

뭔소리일까? 이해가 안간다.

public Foo(String name) {
		this.name = name;
}

public static void main(String[] args){
		Foo foo = new Foo("keesun");
}

위에서 봤던 예시를 다시 한번 보자. 현재 foo라는 생성자로 호출을 하게 되면 매번 새로운 객체를 리턴 받게 된다. 즉, 호출할 때마다 새로운 인스턴스가 만들어지는 것이다.

하지만 아래와 같이 정적팩토리 메서드로 선언해주게 되면

private static final Foo GOOD_NIGHT = new Foo();

//정적 팩토리 메서드로 선언
public static Foo getFoo(){
		return GOOD_NIGHT;
}

//foo 호출
public static void main(String[] args){
		Foo foo = Foo.getFoo();
}

몇 번을 호출해도 정적 팩토리 메서드인 GOOD_NIGHT이 리턴된다.

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

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’을 선물한다. 리턴 타입의 하위 타입의 인스턴스를 만들어줘도 되니까, 리턴 타입은 인터페이스로 지정하고 그 인터페이스의 구현체는 API로 노출시키지 않지만 그 구현체의 인스턴스를 만들어 줄 수 있다는 말이다.

  1. API를 작게 유지할 수 있다. 만들 때 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
  2. API를 사용할 때 알아야 할 개념의 개수와 난도 즉, 개념적인 무게 또한 줄일 수 있다.

뭔가 감이 오는데 잘 모르겠다

코드를 통해서 한번 보자

public static Foo withName(String name) {
		return new Foo(name);
}

이러한 인터페이스가 있을 때 밖, 즉 인터페이스를 사용하는 개발자의 입장에서는 Foo라는 인터페이스의 타입만을 알 수 있다. 실제 안에 있는 구현체가 어떤 것인지는 모른다.

이러한 점을 이용하여 java.util.collections는 45개에 달하는 구현체를 갖고 있지만 그 모든 구현체는 non-public이다. 따라서 개발자가 알아야 할 개념의 수가 줄어들게 된다는 것이다.

하지만 자바 9부터는 java.util.collections 대신 인터페이스에 private static method를 추가할 수 있게 되었다.

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

3과 같은 이유로 객체의 타입은 다를 수 있다. Foo로 감싸고 있다고 해서 그 안의 구현체가 반드시 같은 형태의 타입으로 리턴하지 않아도 된다는 뜻이다.

public static Foo withName(String name) {
		return new Foo(name); //얘가 꼭 Foo 타입을 리턴해도 되지 않아도 된다는 뜻
}

만약 foo의 하위타입이 있다면 그 타입을 리턴해도 된다.

그럼 다른 타입을 리턴하는 예시를 한번 보자.

//foo의 하위타입
static class BarFoo extends Foo{
}

public static Foo getFoo(boolean flag){
	return flag ? new Foo() : new BarFoo(); //BarFoo라는 새로운 타입
}

여기에서는 Foo 타입이 아닌 BarFoo 타입을 리턴할 수도 있게 되는 것이다.

이럴 때 3과 마찬가지로 클라이언트는 이 두 클래스의 존재를 모른다. 알 필요도 없는 것이다.

자바에서는 EnumSet 클래스는 생성자 없이 allOf(), of()를 제공하는데, 그 안에서 리턴하는 객체의 타입은 enum 타입의 개수에 따라 다른 함수, RegularEnumSet(), JumboEnumSet()을 제공한다. 몰라도 된다.

  • 밖으로 노출될 필요가 없는 함수들은 private static을 사용하여 감춘다

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

이 또한 유연함을 제공한다. 이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다. 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

어떻게..?

//foo의 하위타입 정의 x
//static class BarFoo extends Foo{
//}

public static Foo getFoo(boolean flag){
	Foo foo = new Foo();
	//TODO 어떤 특정 약속이 되어있는 텍스트 파일에서 FOo의 구현체의 FQCN을 읽어온다. 
	//* FQCN : 클래스가 속한 패키지명을 모두 포함한 이름을 말한다. Fully Qualified Class Name
	//TODO FQCN 에 해당하는 인스턴스를 생성한다. 
	//TODO foo 변수를 해당 인스턴스를 가리키도록 수정한다. 
	return foo;
}

이렇게 코드를 작성할 때, Foo가 호출되는 당시 해당 텍스트 파일에 뭐가 적혀 있느냐에 따라 다른 객체를 리턴하게 된다.

*즉, 해당 메서드를 작성한 후에도 foo를 상속받은 다른 메서드를 나중에도 만들 수 있다는 뜻이다.

*이게 왜 용이할까?

해당 내용은 좀 더 공부해보고 다시 와서 왜 필요한지, 언제 쓰이는지 확인해야 할듯

서비스 로드에 대해서 공부를 좀 더 해보면 이해가 잘 된다고 한다. 의존성 주입과 비슷하다고 한다는데...


단점

1 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

collections 프레임워크에서 제공하는 편의성 구현체 (java.util.Collections)는 상속할 수 없다. 뭔소리일까. 어차피 상속을 할 필요가 없는 클래스이므로 단점도 아니다.

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

생성자는 javadoc 상단에 모아서 보여주지만 static 팩토리 메소드는 API 문서에서 특별히 다뤄주지 않는다. 따라서 클래스나 인터페이스 문서 상단에 중요한 팩토리 메소드에 대한 문서를 제공하는 것이 좋다.

<aside> 💡 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

</aside>

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

static 팩토리 메소드와 public 생성자 모두 매개변수가 많이 필요한 경우에 불편해진다. 선택 매개변수가 많을 때 활용할 수 있는 대안들을 확인해보자.

해결책

1 생성자

설정하고 싶은 매개변수를 최소한으로 사용하는 생성자를 사용해서 인스턴스를 만든다.

NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);

보통의 프로그래머는 점층적 생성자 패턴을 사용하지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 또한 사용자가 설정하기 원치 않는 매개변수까지 포함하기 쉽다.

2 자바빈즈 패턴

매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

//매개변수가 없는 생성자로 객체 생성
NutritionFacts cocaCola = new NutritionFacts(); 
//세터 메서드 호출로 원하는 매개변수 값 설정
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

이 방법의 단점은 최종적인 객체를 하나 만들려면 여러 번의 호출을 거쳐야 하기 때문에 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.

*이러한 일관성의 문제 때문에 자바빈즈 패턴에서는 클래스를 불변(아이템17)으로 만들 수 없다.

  • 어떻게 한다는건지 이해는 안가지만... 당연한거 아님??

이러한 단점을 보완하기 위해서 사용하기 전 freezing을 쓴다는데.... 다루는 방법이 어려워 실전에서는 거의 쓰이지 않는다. 세마포어랑 비슷한 느낌??

<aside> 💡 불변 어떠한 변경도 허용하지 않는다는 뜻으로 주로 변경을 허용하는 가변 객체와 구분하는 용도로 쓰인다. 대표적으로 String 객체는 한번 만들어지면 절대 값을 바꿀 수 없는 불변 객체다. 불변식 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 말한다. 주어진 조건 내에서만 변경이 허용된다. 가변 객체에도 불변식은 존재할 수 있으며, 넓게 보면 불변은 불변식의 극단적인 예라 할 수 있다.

</aside>

설명을 보니 좀 더 이해가 되는 것 같기도 하고... 일단 넘어가자..

3 빌더

앞의 1 점층적 생성자 패턴의 안전성과 2 자바빈즈의 가독성을 겸비한 빌더패턴이다.

  1. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수적인 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
  2. 그 후 빌더 객체가 제공하는 일종의 세터 메서드로 원하는 선택 매개변수들을 설정한다. (부가적 필드)
  3. 매개변수가 없는 build 메서드를 호출해 필요한 객체(보통은 불변)를 얻는다.

불변인 NutritionFacts 클래스

빌더의 세터 메서드는 빌더 자신을 반환한다. 때문에 연쇄적인 호출이 가능하다. 이러한 호출 방식을 따서 플루언트 API 혹은 메서드 연쇄라고 부른다.

그럼 위와 같은 빌더를 이용한 클래스의 클라이언트 코드를 한번 보자.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();

1,2를 합쳐놓은 것으로 보인다! 매개변수의 값을 설정하면서 생성자를 선언할 수 있다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.

왜?

추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서는 추상 클래스를 상속받으며 각 하위 클래스용 빌더도 추상 빌더를 상속받아 만들 수 있다.

예시를 통해 한번 알아보자.

피자의 다양한 종류를 표현한 계층구조의 루트에 놓인 추상 클래스이다.

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
    }

    final Set<Topping> toppings;

//자기자신의 하위타입을 받는 빌더
    abstract static class Builder<T extends  Builder<T>> { // `재귀적인 타입 매개변수`
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); //비어있는 셋

			//addTopping제공
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

			//Pizza의 하위타입을 만들게 됨
        abstract Pizza build(); // `Convariant 리턴 타입`을 위한 준비작업

			// 추상메서드인 `self-type` 개념을 사용해서 형변환 없이도 메소드 체이닝이 가능케 함
        protected abstract T self(); 
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}

이 피자를 상속받는 ny피자를 만들어봅니다.

public class NyPizza extends Pizza {

    public enum Size {
        SMALL, MEDIUM, LARGE
    }

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this); //빌더 자체를 넘겨받음
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

칼조네 피자

public class Calzone extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauseInside = false;

        public Builder sauceInde() {
            sauseInside = true;
            return this;
        }

        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private Calzone(Builder builder) {
        super(builder); 
	//상위 빌더를 호출해도 해당 클래스의 타입을 반환한다. 즉 pizza가 아닌 Calzone를 반환함
        sauceInside = builder.sauseInside;
    }

}

여기서 각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다. 즉, NyPiazza.Builder는 NyPizza를 반환하고, Calzone.Builder는 Calzone를 반환한다는 뜻이다. 다시 말해서, 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환타입이 아닌 그 하위타입을 반환하는 기능을 공변 반환 타이핑이라 한다.

 

 

💡 결론 생성자나 정적 팩터리가 처리해야할 매개변수가 많다면 빌더패턴을 선택하는 것이 더 유연하다.

 

 

 

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

**싱글턴(singleton)** : 인스턴스를 오직 하나만 생성할 수 있는 클래스

 

싱글턴 클래스의 Mocking을 가능하게 하기 위해서는?

Mocking이란 클라이언트가 코드를 테스트 할 때 주로 쓰는 방법으로 테스트할 코드의 가짜 인스턴스를 생성하여 해당 인스턴스로 테스트를 진행하는 방법이다.

만약 인스턴스를 오직 하나만 생성하도록 막아버리면 이러한 Mocking이 어려워지는데, 이를 해결하기 위해서는 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴을 제공하도록 해야한다. 이렇게 하면 클라이언트가 Mocking시 인터페이스를 구현하여 Mock 객체를 만들어낼 수 있다.

싱글턴을 만드는 방법

  1. **public static final 필드 방식의 싱글턴**

클라이언트가 접근할 수 있는 유일한 public 형태는 미리 정의된 인스턴스 하나 뿐이다. 해당 인스턴스에 대한 생성자는 private 접근제한을 걸고 클래스가 초기화될 때 딱 한 번 호출된다.

public class Elvis{
	public static final Elvis Instance = new Elvis();
	private Elvis() {..} // 생성자
}

장점

이 방법의 장점은 해당 클래스가 싱글턴임이 API에 명백히 드러난다는 것 이다. 또한 코드가 간결하다는 장점이 있다.

이렇게 만들면 클라이언트는 인스턴스를 조작할 수 없게 되는데 한 가지 예외가 있다.

예외) 리플렉션 API AccessibleObject.setAccessible

이 메서드는 클라이언트로 하여금 private 생성자를 호출할 수 있도록한다.

public static void checkAndFixAccess(Member paramMember) {
    AccessibleObject localAccessibleObject = (AccessibleObject) paramMember;
    try {
        localAccessibleObject.setAccessible(true);
        return;
    } catch (SecurityException localSecurityException) {
        while (localAccessibleObject.isAccessible())
            ;

이러한 리플렉션을 방지하기 위해서는 생성자에 final인스턴스 외의 인스턴스를 생성하려고 하면 Exception을 발생시키도록 코드를 구현하면 된다. → 생성자에 final 인스턴스가 null 이 아닌 경우에는 Exception을 낸다.

  1. 정적 팩터리 방식의 싱글턴

이 방법은 위의 public final static 인스턴스를 public이 아닌 private 로 접근권한을 변경하고 그 대신 유일한 public접근 허용으로 getInstance()메서드를 통해 private인 인스턴스를 리턴하여 주는 것이다.

이 방법도 마찬가지로 리플렉션을 방어하기 위한 처리는 따로 해주어야 한다.

public class Elvis{
	private static final Elvis Instance = new Elvis();
	private Elvis(){...} // 생성자
	public static Elvis getInstance(){return Instance;}
}

 

장점

이 방법은 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다. 유일한 인스턴스를 반환하던 팩터리 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.

또한 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.

public class GenericFactoryMethod { 
	public static final Set EMPTY_SET = new HashSet(); 

	public static final <T> Set<T> emptySet() { 
		return (Set<T>) EMPTY_SET; } 
	}
}

마지막으로 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.

참고) Supplier는 인자를 받지 않고 Type T 객체를 리턴하는 함수형 인터페이스이다.

public interface Supplier<T> {
    T get();
}

→ Q. 왜 공급자로 사용하는게 장점이 되는가?

이러한 정적 펙토리 메서드를 사용하는 장점이 필요가 없다면 public 필드로 인스턴스를 받는 것이 좋다.

위 두 방법에서의 직렬화

위 두 방법은 직렬화할 때 처리가 까다롭다.

우선 기존의 방법으로 직렬화를 하는 것으로 해결이 안되고 모든 인스턴스 필드를 trainsient 선언해서 Serialize 하는 과정에서 제외시켜야 한다. (그렇지 않으면 private 처리된 필드까지 모두 데이터화 되어서 private의 의미가 없어지는 듯,,?)

또한 readReslove처리도 해주어야 한다. 역직렬화 과정에서 인스턴스가 새로 만들어지는 것을 방지하고 기존에 생성된 싱글톤 인스턴스를 반환하도록 해야 한다.

따라서 직렬화를 위해서는 아래와 같이 코드를 짜야한다.

참고) trainsient는 static과 마찬가지로 접근제어자 다음에 선언해주면 된다.

private Object readReslove(){
	//진짜 Elvis만 반환, 가짜는 가비지 컬렉터로 자동으로 맡겨짐
	return INSTANCE;
}
  1. 열거 타입 방식의 싱글턴 - 바람직한 방법

enum 타입으로 싱글톤을 만들 수 있다.

public enum SingletonEnum {
    INSTANCE;
    int value;
    
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}
public class EnumDemo {
    
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.INSTANCE;
        
        System.out.println(singleton.getValue());
        singleton.setValue(2);
        System.out.println(singleton.getValue());
    }
}

이 경우 아예 생성자 자체가 없기 때문에 싱글톤 형태가 항상 유지되며 추가 노력없이 직렬화할 수 있고 리플렉션 공격도 방어할 수 있다.

따라서 이 방법이 가장 추천되는 방법이다

그러나 만들려는 싱글턴이 Enum 외의 클래스를 상속하게 된다면 이 방법을 사용할 수 없다.

 

 

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

정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한게 아니다. 그러나 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 public 기본 생성자를 생성해버려 의도치 않게 클라이언트가 인스턴스를 만들어버릴 수도 있다.

이를 방지하기 위해서 private형태로 아무 의미 없는 생성자를 만들어주는 것이 좋다.

명시적 생성자가 private이니까 클래스 밖에서 접근할 수도 없고 자동으로 생성자가 생기지도 않는다.

이 때 생성자에 throw new AssertionError()처리를 해주면 좋다. 클래스 안에서 실수라도 생성자를 호출하지 않기 위함이다.

또한 직관적으로 인스턴스 생성용 클래스가 아님을 명시하는 주석을 클래스 코드 상단에 달아두는 것이 좋다.

public class UtilityClass{
//기본 생성자가 만들어지는 것을 막음 (인스턴스 방지)
private UtilityClass(){
throw new AssertionError();}
...
}

이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 상위 클래스의 생성자를 호출하는데 이를 private로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막힌다.

 

 

 

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

하나 이상의 자원에 의존하는 클래스 구현 방법

1. 정적 유틸리티 클래스

public class SpellChecker {
    private static final Lexicon dictionary = ...;
    
    private SpellChecker() {}
    
    public static boolean isValid(String word) {}
    public static List<String> suggestions(String type) {}
}

2. 싱글턴

public class SpellChecker {
    private final Lexicon dictionary = ...;
    
    private SpellChecker() {}
    public nstatic SpellChecker INSTANCE = new SpellChecker(...);
    
    public static boolean isValid(String word) {};
    public static List<String> suggestions(String type) {};
}

1, 2는 유연하지 않고 테스트하기 어렵다.

3. 의존 객체 주입

인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨준다.

public class SpellChecker {
    private final Lexicon dictionary;
    
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
}

불변을 보장하고, 정적 팩터리나 빌더 모두에 똑같이 응용할 수 있다.

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

  • 팩터리는 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체이다. 팩터리로 Supplier<T> 를 사용할 수 있다.
Mosaic create(Supplier<? extends Tile> tileFactory) {
	this.tile = tileFactory.get();
}

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

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

String s1 = new String("java...");
String s2 = "java..."; //인스턴스 재사용

정적 팩터리 메서드를 사용하자

생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 그렇지 않다. 따라서 Boolean(String) 생성자 대신 Boolean.valueOf(String) 메서드를 사용하는 것이 좋다.

생성 비용이 비싼 객체는 재사용하자

//BEFORE
static boolean isKoreanWord(String s) {
    return s.matches("[가-힣]");
}

//AFTER
private static final Pattern KOREAN = Pattern.compile("[가-힣]");
static boolean isKoreanWord(String s) {
    return KOREAN.matcher(s).matches();
}

내부에서 생성되는 정규표현식용 Pattern 인스턴스는 한 번 사용하고 버려진다. -> Pattern 인스턴스를 캐싱해두고 isKoreanWord가 호출될 때마다 이 인스턴스를 재사용한다.

오토박싱이 숨어들지 않도록 주의하자

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

private static long sum() {
 	Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }
}

long 타입인 i가 Long 타입인 sum에 더해질 때마다 Long 인스턴스가 만들어진다. 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

 

 

 

 

 

 

*이펙티브 자바 서적을 참고하여 작성한 글입니다. 

'3-2기 스터디 > 이펙티브 자바 독서' 카테고리의 다른 글

2주차 - 2,3장(item 7-12)  (0) 2022.05.10
3주차 - 3,4장(Item 13-18)  (0) 2022.05.07

댓글