개발관련 책

Effetive java 2장 정리

MIN우 2023. 5. 10. 18:07
728x90

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

public staic Boolean valueOf(boolean b) {
  return b ? Boolean.TRUE : Boolean.FALSE;
}

 

  1. 이름을 가질 수 있습니다.
  2. 메서드를 통해 반환될 객체의 특성을 메서드 이름을 통해 명확히 묘사할 수 있는 장점이 있습니다. 예로는 BigInteger 클래스의 BigInteger.probablePrime 메서드가 있습니다.
  3. 호출될 때마다 인스턴스를 새로 생성하지 않아도 됩니다.
  4. 이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다.
  5. 반환 타입의 하위 타입 객체를 반환할 수 있습니다.
  6. 이는 반환할 객체의 클래스를 마음대로 선택할 수 있는 엄청난 유연성을 제공해줍니다.
  7. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있습니다.
  8. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 없어도 됩니다.

그리고 다음과 같은 단점이 있습니다.

  1. 상속하려면 public이나 protected 생성자가 필요하므로 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없습니다.
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵습니다.

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

    @Builder
    public Product(Long number, String name, Integer price, Integer stock, LocalDateTime updatedAt,LocalDateTime createdAt) {
        this.number = number;
        this.name = name;
        this.price = price;
        this.stock = stock;
        this.updatedAt = updatedAt;
        this.createdAt=createdAt;
    }
 Product build = Product.builder()
                .stock(stock)
                .name(name)
                .price(price)
                .createdAt(createdAt)
                .build();
        return build;

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

객체와 인스턴스의 차이: 클래스의 타입으로 선언되었을 때 객체라고 부르고, 그 객체가 메모리에 할당되어 실제 사용될 때 인스턴스라고 함.

 

먼저 인스턴스로 만들어서 쓰지 않는 경우는 어떤 경우가 있을까요? 단순히 static 메서드나 필드를 담은 클래스가 필요할 수 있을 겁니다.

예를 들면 java.util.Arrays의 경우 배열 관련된 메서드들(public static void sort 같은 친구들)이 있겠습니다.

이런 유틸리티 클래스들은 인스턴스를 만들어서 사용하려고 설계한 애들이 아니죠. 하지만 클래스의 생성자를 따로 명시하지 않으면 컴파일러가 자동으로 기본 생성자(`default constructor) 를 만들어 줍니다. 기본 생성자는 public 생성자로, 아무데서나 인스턴스화될 수 있습니다.

그럼 인스턴스화를 막는 방법으론 어떤 것이 있을까요? 간단합니다. private 생성자를 만들어주면 됩니다. private 생성자는 클래스 외부에선 호출될 수 없으니 인스턴스가 만들어 지는 경우를 막을 수 있습니다.

public class UtilityClass {
  // 인스턴스화 방지
  private UtilityClass() {
    ...
  }
}

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

 

싱글턴을 만드는 방식은 두 가지가 있다. 두 방식 모두 생성자는 private으로 감춰두고,
유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해 둔다.

1. public static 멤버가 final 필드인 방식

public static final 필드 방식의 싱글턴

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() { ... }

	public void leaveTheBuilding()
}

private 생성자는 public static final필드인 Elvis.INSTANCE를 초기화할 때 딱 한 번만 호출된다.
public이나 protected 생성자가 없으므로 Elvis클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

@Test
	public void singletonTest(){
		Elvis elvis1 = Elvis.INSTANCE;
		Elvis elvis2 = Elvis.INSTANCE;

		assertSame(elvis1, elvis2); // SUCCESS 
	}

장점

  1. 해당 클래스가 싱글턴임이 API에 명백히 드러난다.
    public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.
  2. 간결하다.

예외

리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
리플렉션 API: java.lang.reflect, class 객체가 주어지면, 해당 클래스의 인스턴스를 생성하거나 메소드를 호출하거나, 필드에 접근할 수 있다.

Constructor<Elvis> constructor = (Constructor<Elvis>) elvis2.getClass().getDeclaredConstructor();
constructor.setAccessible(true);

Elvis elvis3 = constructor.newInstance();
assertNotSame(elvis2, elvis3); // FAIL

해결 방법

생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.

private Elvis() {
	if(INSTANCE != null){
		throw new RuntimeException("생성자를 호출할 수 없습니다!");
	}
}

 

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

쪼끔 친숙한 패턴입니다. 의존성 주입(Dependency Injection) 은 스프링 프레임워크를 접해봤다면 한 번쯤은 들어봤을 단어입니다.

우리가 만드는 클래스들은 대개 하나 이상의 자원에 의존 합니다.

public class minwoo {
  private static final Google google;	// minwoo 가 구글에 의존
}

 

public class minwoo {
  private static final Google google;
  
  public minwoo(Google google) {	
    this.google = Objects.requireNonNull(google);
  }
  ...
}

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면

싱글턴과 정적유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접

만들게 해서도 안된다. 대신 필요한 자원을(혹은 그 자원을 만들어주는 팩터리를)

생성자에 혹은 빌더에 넘겨주자 의존 객체 주입이라 하는 이 기법은 클래스의 유연성,재사용성,테스트

용이성을 기막히게 개선해준다.

 

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

 

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

 

·  생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.

ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

 

·  가변 객체라 해도 수용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

 

·  생성 비용이 아주 비싼 객체가 반복해서 필요하다면, 캐싱하여 재사용하길 권한다.

ex) 아래는 주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드다. 하지만 String.matches 메서드를 사용한다는 데 문제가 있다.

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

 

- String.matches는 성능이 중요한 상황에서 반복해 사용하기에 적합하지 않다.

1. 메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.

2. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.

 

TODO: 유한 상태 머신이란?

 

- 불변인 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();
    }
}

-  isRomanNumeral 메서드가 처음 호출될 때 필드를 초기화하도록 지연 초기화로 불필요한 초기화를 없앨 수는 있지만, 권하지 않는다.

- 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많다(아이템 67).

 

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

· 아래 스택 클래스는 메모리 누수가 발생한다.

- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

· pop 메서드에서 메모리 누수가 발생한다.

- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.

- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.

- 다쓴 참조: 앞으로 다시 쓰지 않을 참조

 

·  객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.

- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.

 

해결 방법

· 참조를 다 썼을 때 null(참조 해제)한다.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

· 다쓴 참조를 null 하면 프로그램 오류를 조기에 발견할 수도 있다.

- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.

- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.

 

but 객체 잠조를 null처리하는 일은 예외적인 경우여야 한다. 가장 좋은 방법은 그 참조를 담은 변수를 유효범위(scope)

밖으로 밀어내는 것 이다.

 

8.  finalizer 와 cleaner 사용을 피하라


· 자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공한다.

· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.

· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

· C++에서의 파괴자(destructor)와 자바의 finalizer, cleaner는 다른 개념이다.

- C++의 파괴자는 비메모리 자원을 회수하는 용도로 쓰이고, 자바에서는 try-with-resources와 try-finally를 사용해 해결한다.

 

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

 

public static void main(String args[]) throws IOException {
    FileInputStream is = null;
    BufferedInputStream bis = null;
    try {
        is = new FileInputStream("file.txt");
        bis = new BufferedInputStream(is);
        int data = -1;
        while((data = bis.read()) != -1){
            System.out.print((char) data);
        }
    } finally {
        // close resources
        if (is != null) is.close();
        if (bis != null) bis.close();
    }
}

 

  • 자원 반납에 의해 코드가 복잡해짐
  • 작업이 번거로움
  • 실수로 자원을 반납하지 못하는 경우 발생
  • 에러로 자원을 반납하지 못하는 경우 발생
  • 에러 스택 트레이스가 누락되어 디버깅이 어려움

 

그래서 이러한 문제를 해결하기 위해 try-with-resources라는 문법이 Java7부터 추가되었다.

 

Java는 이러한 문제점을 해결하고자 Java7부터 자원을 자동으로 반납해주는 try-with-resources 문법을 추가하였다.

Java는 AutoCloseable 인터페이스를 구현하고 있는 자원에 대해 try-with-resources를 적용 가능하도록 하였고, 이를 사용함으로써 코드가 유연해지고, 누락되는 에러없이 모든 에러를 잡을 수 있게 되었다.

 

public static void main(String args[]) throws IOException {
    try (FileInputStream is = new FileInputStream("file.txt"); BufferedInputStream bis = new BufferedInputStream(is)) {
        int data;
        while ((data = bis.read()) != -1) {
            System.out.print((char) data);
        }
    }
}

 

- 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고 ,try-with-resources 를 사용하자!

- 코드는 더 짧아지고 , 만들어지는 예외 정보도 훨씬 유용하다.

 

 

 

  1. Static Memory (Static 영역 또는 Class 영역):
    • 정적 변수(static 변수)와 정적 메서드(static 메서드)가 저장됩니다.
    • 클래스가 처음으로 로드될 때 메모리에 할당되며, 프로그램의 실행 동안 유지됩니다.
    • 모든 인스턴스가 공유하는 데이터를 저장하기에 적합합니다.
  2. Heap Memory (Heap 영역):
    • 객체와 배열이 동적으로 할당되는 영역입니다.
    • new 키워드를 사용하여 객체를 생성할 때 메모리에 할당됩니다.
    • 가비지 컬렉션(Garbage Collection)에 의해 관리되며, 사용하지 않는 객체는 자동으로 해제됩니다.
    • 다수의 스레드가 공유하는 데이터를 저장하기에 적합합니다.
  3. Stack Memory (Stack 영역):
    • 지역 변수와 메서드 호출 시 생성되는 임시 데이터가 저장됩니다.
    • 메서드가 호출될 때 스택 프레임이 생성되며, 메서드의 실행이 완료되면 스택 프레임이 제거됩니다.
    • 각 스레드는 자체 스택을 가지고 있으며, 스택 프레임은 메서드 호출 순서에 따라 쌓이고 해제됩니다.
    • 스택은 메모리 관리가 빠르고 간단하여, 메서드 호출과 로컬 변수 저장에 적합합니다.
728x90