본문 바로가기
Effective Java

item 1

by irerin07 2023. 9. 9.
728x90

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



장점 1

  • 이름을 가질 수 있다.
public class Order {

	private Product product;
	private boolean prime;
	private boolean urgent;

	**public Order(Product product, boolean prime) {
		this.product = product;
		this.prime = prime;
	}**

	**public Order(Product product, boolean urgent) {
		this.product = product;
		this.urgent= urgent;
	}**

}

위의 예시는 Order에 대한 두개의 생성자를 가지고 있다. 하나는 prime한 order, 또 하나는 urgent한 order용 생성자이다.

단순히 넘어오는 파라미터들의 이름만 보면 두 생성자는 서로 다른 상황에서 Order 인스턴스를 반환하는 것임을 짐작할 수 있다.

하지만 자바에서는 동일한 시그니처의 생성자를 두개 가질 수 없기 때문에 컴파일 에러가 발생하게 된다.

심지어 사용하는 쪽에서는 도대체 어떤 타입의 Order 인스턴스가 반환될 지 알 수가 없다.

이를 회피하는 일종의 꼼수로 다음과 같이 생성자 두개를 선언할 수 있다.

public class Order {

	private Product product;
	private boolean prime;
	private boolean urgent;

	**public Order(Product product, boolean prime) {
		this.product = product;
		this.prime = prime;
	}**

	**public Order(boolean urgent, Product product) { //시그니처의 순서를 바꾸었다.
		this.product = product;
		this.urgent= urgent;
	}**

}

이제 두 생성자의 시그니처는 더 이상 동일하다고 판단되지 않기 때문에 문제없이 넘어갈 수 있다.

그럼 문제가 해결이 된걸까?

생성자는 이름을 가질 수 없다. 생성자의 이름은 반환하는 클래스의 이름으로 고정이 되고 해당 클래스의 인스턴스만 반환하기 때문이다. 정확히는 이름을 바꿀 수 없다는게 맞을 것 같다.

그렇기 때문에 아무리 시그니처의 순서를 바꿔도 사용하는 입장에서는 이게 어느 타입의 Order를 만드는지 알 방법이 없다. 이를 확인하기 위해서는 매번 Order클래스를 열었다 닫았다 해야 할 것이다.

그래서 책의 저자는 이름으로 표현을 할 수 있는 정적 팩터리 메서드의 사용을 권한다.

public class Order {

	private Product product;
	private boolean prime;
	private boolean urgent;

	public static Order primeOrder(Product product) {
		Order order = new Order();
		order.prime = true;
		order.product = product;

		return order;
	}

	public static Order urgentOrder(Product product) {
		Order order = new Order();
		order.urgent= true;
		order.product = product;

		return order;
	}

}

위의 예시처럼 정적 팩토리 메서드를 사용했을때의 장점은 어떤 클래스가 만들어주는 객체의 특징을 정적 팩토리 메서드의 이름으로 수월하게 표현이 가능하다는 것이다.

장점 2.

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

자바의 생성자는 매번 호출될 때마다 새로운 인스턴스를 만든다.

그말인즉 사용하는 쪽에서 언제든 원할때마다 원하는 만큼 매번 새로운 인스턴스를 만들 수 있다는 것이다.

하지만 개발을 하다 보면 이런 무분별한 인스턴스 생성을 통제해야 하는 경우가 있을 수 있는데, 단순히 생성자만으로는 이를 통제하기 어렵다.

이를 통제하기 위한 방법으로 정적 팩토리 메서드를 사용할 수 있다.

public class Settings {

	private boolean useAutoSteering;
	private boolean useABS;
	private Difficulty difficulty;

}

위의 예시대로 작성한 클래스는 해당 클래스를 사용하는 쪽에서 언제는 new 생성자를 활용하여 얼마든지 Settings의 인스턴스를 만들 수 있다.

public class Settings {

	private boolean useAutoSteering;
	private boolean useABS;
	private Difficulty difficulty;

	private Settings() {} //생성자를 private으로 숨긴다.

	private static final Settings SETTINGS = new Settings(); //미리 생성한 Settings객체

	public static Settings newInstance() {
		return SETTINGS;
	}

}

위의 예시처럼 Setting의 기본 생성자를 숨겨버리면 Settings를 사용하는 쪽에서는 이제 더이상 new 생성자를 활용해 새로운 Settings의 인스턴스를 가져올 수 없다.

대신 newInstance() 정적 팩토리 메서드를 사용해야 하여 결코 여러개의 Settings 인스턴스를 만들 수 없게 된다.

생성자로는 절대 할 수 없던 인스턴스 생성의 컨트롤을 적정 팩토리 메서드를 활용해 컨트롤이 가능해진 것이다.

Boolean.valueOf(false) 의 내부 구현 코드를 확인하면 이와 같은 방식으로 작성 되어 있는 것을 확인할 수 있다. 다른점 이라면 valueOf() 메서드는 매개변수에 따라 반환해주는 값이 다른데 이처럼 매개변수에 따라 다른 인스턴스를 반환하는 기능도 정적 팩토리 메서드를 사용하면 구현이 가능해진다.

Flyweight Pattern

  • 자주 사용하는 객체들을 만들어두고 필요로 하는 인스턴스를 꺼내 사용할 수 있게 해주는 패턴. 정적 팩로리 메서드와 통용되는 개념이 있다. → 인스턴스를 통제하는 방법

장점 3.

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

정적 팩토리 메서드에서 반환 타입을 인터페이스나 클래스로 선언을 해두고 실제 반환하는 인스턴스는 반환 타입으로 선언한 인터페이스의 구현체 혹은 클래스의 하위 클래스로 바꿔줄 수 있어 유연성이 늘어난다.

public class HelloServiceFactory {

	public static **HelloService** of(String lang) { //반환타입 HelloService
		if (lang.equals("ko") {
			return new **KoreanHelloService**(); //실제 반환하는 인스턴스는 HelloService의 구현체인 KoreanHelloService
		} else {
			return new **EnglishHelloService**(); //실제 반환하는 인스턴스는 HelloService의 구현체인 EnglishHelloService
		}
	}

}

장점 4.

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

	public static **HelloService** of(String lang) { //입력 매개변수 lang
		if (lang.equals("ko") { //lang에 따라 반환되는 인스턴스가 달라진다
			return new **KoreanHelloService**(); 
		} else {
			return new **EnglishHelloService**();
		}
	}

}

장점 5.

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

자바에서 제공하는 ServiceLoader는 정적 팩토리 메서드가 있다.

해당 클래스의 load(Class class) 메서드를 사용하면 현재 참조할 수 있는 classpath 내에 있는 모든 클래스의 등록되어 있는 구현체를 가지고 온다.

ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class); //HelloService 클래스의 모든 구현체를 가지고 온다. 여러개가 있을 수 있기 때문에 Iterable하다.
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h => {
		System.out.println(h.hello());
});

ServiceLoader를 통해 가져온 HelloService 클래스의 구현체들중 제일 첫번째 구현체를 가지고 와서 해당 클래스의 hello() 메서드를 호출했다.

이 예제에서 말하고자 하는 것은 HelloService의 어떤 구현체가 올지 모르지만 그 구현체가 따르고 있는 인터페이스 기반으로 코딩이 가능해진다는 것이고 굉장히 유연해진다는 것이다.

단점 1.

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

정적 팩토리만을 사용하게 만든 클래스는 private 생성자를 가지게 되는데, 그렇게 되면 해당 클래스는 더이상 상속을 사용할 수 없게 된다.

하지만 우회할 수 있는 방법이 있는데 하위 클래스로 사용하고자 하는 클래스에서 Delegation을 사용해 상위 클래스를 가지고 있으면 굳이 상속을 사용하지 않더라도 상위 클래스의 기능들을 활용할 수 있다.

하지만 List처럼 public 생성자를 가지고, 정적 팩터리 메서드를 제공하는 경우도 있다.

단점 2.

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

Enum을 키로 사용하는 Map을 쓸 때 EnumMap을 쓰는게 더 효율적인 이유

EnumMap과 HashMap의 기반

EnumMap은 enum constants(이늄 상수)로 이루어진 자연스러운 순서에 기반을 두고있는 반면, HashMap은 Hash Table을 기반으로 구현되어 있다.

EnumMap의 생성자

EnumMap은 생성될 때 지정된 이늄상수를 사용하여 비어있는 맵을 만든다. keyUniverse가 이늄상수를 배열로 가지고 있고, keyUniverse의 배열의 길이로 값이 들어갈 비어있는 배열(vals)을 만든다.

// EnumMap.classpublic EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
  }

EnumMap의 put메소드

EnumMap이 구현한 put메소드를 보면, key값에 ordinal이라는 메소드를 통해 index정보를 가져오는데, 이 ordinal은 Enum 타입이 제공하는 메소드로, 이늄상수가 선언된 순서로 매겨진 index정보를 리턴한다.

vals[index]로 key와 매핑되는 value의 위치를 바로 찾아서, value를 업데이트 할 수 있다.

// EnumMap.classpublic V put(K key, V value) {
     typeCheck(key);

     int index = key.ordinal();
     Object oldValue = vals[index];
     vals[index] = maskNull(value);
     if (oldValue == null)
       size++;
     return unmaskNull(oldValue);
   }

Hash Table

HashMap은 Hash Table 기반으로 구현되어있다는데, Hash Table은 뭘까?

유튜브에서 이해가 가는 설명을 찾았다.

"Hash Table은 검색하고자 하는 key값을 입력받아서, 해시함수를 돌려서 반환받은 HashCode를 배열의 Index로 환산을 해서, 데이터에 접근하는 방식의 자료구조입니다. 여기서 사용하는 키값은 문자열, 숫자, 파일도 될 수 있습니다. 해시 함수는 어떤 특정한 규칙을 이용해서, 입력받은 키 값으로 그 키값이 얼마나 큰지에 상관없이 동일한 해시코드를 만들어줍니다." (출처: https://youtu.be/Vi0hauJemxA)

이해한 바로는, key값을 넣으면 hash함수를 사용해서 key값을 숫자로 변환한다. hash함수로 변환한 값을 index로 사용하기 때문에, 동일한 key값을 넣으면 hash함수는 항상 동일한 값을 리턴한다. 배열의 처음부터 끝까지 검사하는 방식이 아니라, 배열의 인덱스값을 찾아서 꺼내오는 방식이기 때문에, 매우 많은 데이터를 넣어도 성능이 일정하다.

버킷, 부하계수와 용량의 resize, Node와 Tree, Linked List 같은 키워드들이 더 있지만, 일단계로는 여기까지만 이해하도록 하겠다.

HashMap의 put메소드

코드로 가서, HashMap의 put메소드를 보면, 받은 key값을 hash()메소드로 해시값으로 변환해서 putVal매소드의 아규먼트로 사용하는 것을 볼 수 있다.

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
  }

putVal 메소드가 복잡해서 더 이상 코드를 보지 않고, 왜 대부분의 경우 EnumMap이 HashMap보다 빠를지에 대해 정리해 보자.

✔ EnumMap이 HashMap보다 성능상 효율적인 이유

  1. EnumMap은 값을 찾을 때, 이늄상수가 index정보를 ordinal메소드로 제공하고 있기 때문에, 바로 찾을 수 있다.
  2. 반면, HashMap은 찾을 때, key값을 hash메소드로 index로 변환하고 찾아야 한다.
  3. 값을 넣을 때, EumMap은 key의 index정보로 값이 저장된 위치를 찾아서 바로 값을 넣는다.
  4. 하지만, HashMap은 key값을 hash로 만들고, 배열의 크기를 조절하는 등의 일이 추가로 필요하다.
  5. 때문에, 배열의 크기가 고정되고 바로 찾을 수 있는 EnumMap이 성능상 훨씬 더 효율적이다.

EnumMap

Javadoc에 따르면

"when the map is created. Enum maps are represented internally as arrays. This representation is extremely compact and efficient."

  • hashmap 은 key를 bucket에 저장하고각 bucket이 linked list를 참조 하고 있음. (linkedlist에는 hash(key)가 같은 element가 들어감) 그런데 enummap 의 경우 key로 사용할 값이 제한되어 있으므로, 그 갯수만큼 길이를 가진 array를 선언하고. 해당 index에 값을 넣으면 됨.

EnumSet

Javadoc에 따르면

"when the set is created. Enum sets are represented internally as bit vectors."

  • hashset은 hashmap 과 같은데 map의 value가 있다 없다를 표현하는 지시자 같은 값이 들어감. enumset은 값이 있다 없다만 표시하면 되니까 enummap 처럼 array로 구현하지 않고 10101011 같은 bitvector로 구현이 가능.

서비스 제공자 프레임워크

서비스 제공자 프레임워크와 같은 것들의 목적은 확장 가능한 애플리케이션을 만드는 것이다.

확장 가능하다는 것은 외부적인 요인으로 인해 애플리케이션의 동작을 다르게 작동하는것.

  • 서비스 제공자 인터페이스 (SPI) : 확장 가능하게끔 만들어주고 싶은 서비스. 확장 가능한 여러가지 구현체들이 만들어질 수 있는 인터페이스
  • 서비스 제공자 (서비스 구현체) : 해당 서비스의 구현체는 서로 다른 클래스, 서로 다른 JAR 파일에 위치할 수 있다.
  • 서비스 제공자 등록 API (서비스 인터페이스의 구현체를 등록하는 방법 : 서비스 구현체를 등록하는 방법을 제공. 스프링의 @Configuration어노테이션을 사용한 설정 파일에서 @Bean 어노테이션을 사용해 등록하는 방법
  • 서비스 접근 API (서비스의 클라이언트가 서비스 인터페이스의 인스턴스를 가져올 때 사용하는 API) : 등록된 서비스를 가져오는 방법. 스프링의 ApplicationContext를 정의하고 정의한 applicationContext의 getBean()을 사용해 Bean을 가져다 사용하는것

브릿지 패턴

구체적인 것과 추상적인 것을 분리하여 그 사이에 다리를 두는 것이 목적이다.

이 두가지를 나누는 이유는 서로에게 영향을 주지 않으면서 독립적으로 개별적인 계층구조로 발전할 수 있게 하기 위함이다.

Delegation이 브릿지 역할을 한다.

리플렉션

클래스로더를 통해 읽어온 클래스 정보(거울에 반사된 정보)를 사용하는 기술

모든 클래스는 jvm의 클래스로더가 클래스들의 정보를 읽어와서 메모리에 저장해둔다.

이 반사된 정보를 바탕으로 여러가지 작업을 할 수 있다.

예를 들어 특정 어노테이션이 붙어있는 필드 또는 메소드를 읽어오거나 특정 이름 패턴에 해당하는 메소드 목록을 가져와 호출할 수 있다.

728x90

'Effective Java' 카테고리의 다른 글

item 4  (0) 2023.10.04
item 3  (0) 2023.09.21
item 2  (0) 2023.09.21