본문 바로가기

개발관련 책

Effetive java 2장 정리

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

'개발관련 책' 카테고리의 다른 글

면접을 위한 CS 전공지식 노트 2장  (0) 2024.03.25
면접을 위한 CS 전공지식 노트 1장  (1) 2024.03.24
clean code 3장 요약  (0) 2023.09.24
clean code 2장 요약  (1) 2023.09.24
Effetive java 3장 정리  (0) 2023.07.03