Effective Java 3 을 읽고 공부하자 1

juhyeon·2021년 2월 2일
0

지금부터 Effective Java 3/E 를 읽고 공부한 내용을 정리한다.
정리라기 보다는 그냥 미래의 내가 찾기 편하게 주절주절 써보는거다.

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

클래스는 public 생성자 외에도 그 클래스의 인스턴스를 반환하는 단순 정적 메서드를 제공할 수 있다. 그게 정적 팩터리 메서드 다.
간단히 예를 들면, boolean 에서는 다음과 같은 객체 참조를 뱉는 메서드이다.

public static Boolean valueOf(boolean flag) {
	return flag ? Boolean.TRUE : Boolean.FALSE;
}

위와 같이 작성하는 정적 팩터리 매서드는 5가지 장점이 있다.

  • 반환될 객체를 설명하는 이름을 지어줄 수 있다.
    • 객체의 의미를 설명하기엔 BigInteger.probablePrime 같은 게 좋다~
    • 한 클래스에는 같은 이름으로 파라미터 갯수 다르게해서 생성자 여러개 생성이 가능.
      보통 이런경우 생성자가 여러개라면 일일이 로직을 뜯어봐야 차이를 파악할 수 있음.
      But, 정적 팩터리 메서드를 생성자로 활용하면 각각의 차이를 잘 드러내는 이름을 지어줄 수 있기 때문에 의미를 담기 좋다.
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다. => 성능 upup~
    • 개발자가 불변 클래스에 인스턴스를 미리 만들어놓거나, 새로 생성한 인스턴스를 캐싱해서 재활용하거나... 하면 된다.
  • 반환 타입의 하위 타입객체를 반환할 수 있다!
    • 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성
    • 인터페이스를 정적 팩터리 메서드의 반환타입으로 사용하면 좋을듯
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 반환 타입의 하위 타입이기만 하면
    • 이 매서드를 쓰는 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고, 알 필요도 없다.
  • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • JDBC 를 만드는 근간이 되었음.
    • 각 구현체를 클라이언트가 사용해야 하는데 (결과적으로), 여기서 팩터리 메서드가 없다면 리플렉션을 써야한다. (각 구현체를 인스턴스로 만들 때)

어쨌거나 가장 매력적인 포인트는 이걸 사용해서 불필요한 객체 생성을 피할 수 있다 는 점이다 😆

물론 단점도 있다.

  • 상속을 하려면 어쨌거나 public 이나 protected 생성자가 필요하니, 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • Java Collection Framework 의 Utility 구현 클래스들은 상속할 수 없다. (정적 팩터리 메서드만 제공하고 있으니까.)
    • 불변 타입을 유지하고자 하면 장점이 될 것도 같다..
  • 개발자가 찾기 힘들다.
    • API 문서를 잘 쓰자!

정적 팩터리 메서드를 이름짓는 것도 대세가 있더라.

  • from : 파라미터가 1개일때, 그걸 받아서 해당 타입의 인스턴스 반환할 때
  • of : 파라미터 여러개
  • valueOf : from 과 of 의 더 자세한 버전
  • instance, getInstance : 파라미터로 명시한 인스턴스를 반환하지만, 같은 (equals) 인스턴스는 아니다.
  • create, newInstance : 위에꺼랑 같지만, 매번 새로운 인스턴스를 생성한다는 의미를 내포할때
  • getType : 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때
    FileStore fs = Files.getFileStore(path)
  • newType : 생성할 클래스가 아닌 다른 클래스에 팩터리 매서드를 정의할 때
    FileStore fs = Files.newFileStore(path)
  • type : 위에 두개의 간결 버전
    List<Person> people = Collections.list(oldPerson)

 

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

우리가 정의한 도메인은 시간이 흘러가며 필드가 많아질 수 밖에 없는 운명이다.
이런 관점에서 빌더 패턴은 객체의 확장성을 고려하기에도 좋고, 필드 각각의 유효성 검사 로직을 구현하기에도 편리하다.

또, 빌더 패턴은 계층적으로 설계된 클래스에도 함께 쓰기 좋은데, 개인적으로 추상클래스보다는 인터페이스를 선호하는 편이지만.. 그래도 빌더 패턴의 장점을 돋보이는 것 같아서 코드를 가져와봤다.

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

public abstract class Pizza {
	public enum Topping {
		HAM,
		PEPPER,
		ONION
	}

	private final Set<Topping> toppings;

	abstract static class Builder<T extends Builder<T>> {
		EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

		public T topping(Topping topping) {
			if (topping != null) {
				toppings.add(topping);
			}
			return self();
		}

		abstract Pizza build();

		protected abstract T self();
	}

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

각 계층의 클래스에 관련된 빌더를 멤버로 정의한다. 여기서 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖는다.

위의 코드에서는, abstract method 인 self 를 더해, 하위 클래스에서 형변환 없이도 method chainig in builder pattern 을 지원하게끔 한다. 이건 자바에 self 타입이 없기때문에 사용하는 우회 방법이다.

아래는 Pizza 의 하위 클래스이다.

public class NewYorkPizza extends Pizza {
	public enum Size {
		SMALL,
		MEDIUM,
		LARGE
	}

	private final Size size;

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

		public Builder size(Size size) {
			if (size != null) {
				this.size = size;
			}
			return this;
		}

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

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

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

각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 하위 클래스를 반환하도록 선언한다. 이 덕분에 빌더를 사용하는 쪽에선 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.

위와 같은 패턴에서, 빌더를 사용하는 클라이언트는 다음과 같이 구현할 수 있다.

NewYorkPizza pizza = new NewYorkPizza.Builder()
      .topping(ONION)
      .topping(PEPPER)
      .size(SMALL)
      .build();

 

인스턴스화를 막기 위해선 private 생성자를 사용하라

객체지향을 해치는 길이 될 수 있기 때문에 추천하는 방식은 아니지만,
가끔 static 메서드와 static 필드만을 담은 클래스를 만들고 싶을 때가 있다.

이렇게 static 멤버만 담은 Utility 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다. 매개변수를 받지 않는 public 생성자가 기본적으로 만들어지는데, 이러면 의도치 않게 클래스가 인스턴스화 된다.

단지 추상 클래스로 만들어버리는 것은 인스턴스화를 막을 수 없다. 그냥 상속받아서 하위 클래스를 만들어 버리면 되기 때문이다.

하지만 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때 뿐이므로, private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

public class StringUtils {

	private StringUtils() {
    		throw new AssertionError();
    }
}

위 코드는 다음과 같이 설명된다.

  • 명시적 생성자가 private 이므로 클래스 바깥에서는 여기에 접근할 수 없다.
  • 클래스 안에서 실수로라도 생성자를 호출하면 안되니까 에러를 던져버린다.
  • 하지만 생성자가 존재하는데도 호출할 수 없는.. 직관적이지 못한 코드이므로 주석이 필요하겠다.
  • private 생성자이므로 상속도 불가능하다.

 

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

사용하는 자원에 따라 동작이 달라지는 클래스(DTO) 에는 정적 유틸리티 클래스나 Singleton 방식이 적절하지 않다.

다음과 같은 경우다.

public class SpellChecker {
	private final Dictionary dictionary;
    
    ...
}

사전에 종류에 따라서 맞춤법 검사기는 다르게 동작한다.

여기서 클래스(SpellChecker) 는 여러 Dictionary type 인스턴스를 지원해야 하며, 클라이언트가 원하는 타입의 dictionary 를 사용해야 한다. 이 조건을 만족하기 위해, 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용해야 한다.

다음과 같이, SpellChecker 를 생성할 때 생성자에 넘겨주는 것이다.

public class SpellChecker {
	private final Dictionary dictionary;
    
    public SpellChecker(Dictionary dictionary) {
    	this.dictionary = dictionary
    }
}

이 패턴의 변형으로는 생성자에 Supplier factory 를 넘겨주는 방식이 있다.
Supplier<T> 를 입력받는 메서드는 다음과 같이 쓰면서 타입 매개변수를 제한한다.

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

의존 객체 주입 프레임워크 중 하나는 스프링 프레임워크이다.

 

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

  • String.matches
    String.matches 는 내부적으로 정규표현식을 위해 Pattern 인스턴스를 만든다. 하지만 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern 은 입력받은 정규표현식에 해당하는 유한 state machine 을 만들기 때문에 인스턴스 생성 비용이 높다.

    따라서 성능을 개선하려면 Pattern 인스턴스를 클래스 정적 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 필요할때마다 재사용하는 형태가 가성비 좋다.

    다음과 같이 말이다.

public class RomanNumerals {
  	private static final Pattern ROMAN_PATTERN = Pattern.compile("정규표현식");
    
	static boolean isRomanNumeral(String source) {
		return ROMAN_PATTERN.matcher(source).matches();
	}
}

하지만 이번 솔루션이 "객체 생성은 비싸니까 피해야한다" 를 의미하는건 아니다. JDBC driver 같은, 아주 무겁고 비싼 객체가 아니고서는, 단순히 객체 생성을 피하고자 자기만의 object pool 을 만드는건 위험하다.

불필요한 객체의 생성은 그저 못생긴 코드와 구린 성능을 낳겠지만, 새로운 객체를 만들어야할 상황에서 기존 객체를 재사용하면 버그 폭탄과 보안 구멍을 만드는 꼴이다.

 

profile
Just do it~ 😎

0개의 댓글