본문 바로가기

개발관련 책

Effective java 정복기 1장

728x90

목표

- 취뽀 기념 실무코드컨벤션을 잘 지키기위해 , 더 효율적인 코드를 작성하기위해 Effective java 정복기

목차

· 1장

· 2장

· 3장

· 4장

· 5장

· 6장

· 7장

· 8장

· 9장

· 10장

· 11장

· 12장

 

 

 

1장

 

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

장점 1 : 생성자 자체는 생성되는 객체의 특성을 직관적으로 설명하지 않는다. 하지만 정적팩토리메소드를 사용하게 되면

이름을 가질 수 있다.

public static Position createStartPosition(){
	return new Position(START_POSITION_VALUE);
}
Position position=Position.createStartPosition();

 

장점 2 : Static을 사용하게 되면 클래스레벨에서 변수를 공유하여 클래스가 로딩 될 때 메모리에 올라가며

해당 클래스 내에서 어디서든지 접근이 가능하다.

public static final int MIN_LOTTO_NUMBER = 1;
public static final int MAX_LOTTO_NUMBER = 45;

public static final LottoNumber[] LOTTO_NUMBER_CACHE = new LottoNumber[MAX_LOTTO_NUMBER + 1];

static {
    IntStream.rangeClosed(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
        .forEach(number -> LOTTO_NUMBER_CACHE[number] = new LottoNumber(number));
}

private final int number;


public static LottoNumber valueOf(final int number) {
    if (number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER) {
        throw new IllegalArgumentException("[ERROR]");
    }
    return LOTTO_NUMBER_CACHE[number];
}

Spring 컨테이너는 애플리케이션 구동 시 모든 빈들을 등록하여 객체를 초기화하고 관리한다.

이떄 싱글톤으로 관리를 하게 되는데 static과 관계없이 한번 만 생성이 된다.

@Component
public class MyController {
    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    public void doSomething() {
        myService.performAction();
    }
}

 

인스턴스를 미리 만들어 놓거나 캐싱하여 재활용하게 된다면 불필요한 객체 생성을 피할 수 있다.

 

장점

- 싱글톤 가능

- 인스턴스화 불가로 만들기

- 불변 값 클래스에서 동치인 인스턴스가 하나뿐임을 보장

 

단점

- 상속을 하기위해서는 public이나 protected 생성자가 필요하다, 정적팩터리 메소드만 제공하면 하위

클래스를 만들 수 없다.  즉 컴포지션(아이템 18)을 사용하도록 유도하고 불변타입(아이템 17)로 만들려면 

해당 제약을 지켜야한다는 점에서 장점이다.

 

 

1. 상속을 사용하는 경우

// 상위 클래스: Animal
class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }

    public void sleep() {
        System.out.println("This animal sleeps.");
    }
}

// 하위 클래스: Dog
class Dog extends Animal {
    public void bark() {
        System.out.println("Dog barks: Woof Woof!");
    }
}

public class MainInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();   // 부모 클래스 Animal의 메서드 사용
        dog.sleep(); // 부모 클래스 Animal의 메서드 사용
        dog.bark();  // Dog 고유 메서드 사용
    }
}

 

상속 방식의 문제점

  1. Dog 클래스는 Animal의 모든 기능에 강하게 결합되어 있습니다.
    • 만약 Animal 클래스에 변경(예: 메서드 이름 변경, 추가 기능 등)이 생기면 Dog도 영향을 받을 수 있습니다.
  2. Dog는 "is-a 관계"를 가정합니다.
    • 하지만, 동물의 행동(예: 먹기, 자기)이 모든 동물에게 동일한 방식으로 동작하지 않을 수도 있습니다.

2. 컴포지션을 사용하는 경우

// Animal 클래스
class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }

    public void sleep() {
        System.out.println("This animal sleeps.");
    }
}

// Dog 클래스 (Animal을 포함)
class Dog {
    private final Animal animal; // Animal 객체를 내부 필드로 포함

    public Dog() {
        this.animal = new Animal(); // Animal 객체 초기화
    }

    public void eat() {
        animal.eat(); // Animal의 eat() 동작 위임
    }

    public void sleep() {
        animal.sleep(); // Animal의 sleep() 동작 위임
    }

    public void bark() {
        System.out.println("Dog barks: Woof Woof!");
    }
}

public class MainComposition {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();   // Animal의 eat() 동작
        dog.sleep(); // Animal의 sleep() 동작
        dog.bark();  // Dog 고유 메서드
    }
}

 

  • Dog는 Animal에 강하게 결합되지 않습니다.
    • Animal의 동작을 내부적으로 감싸서 위임합니다.
    • 만약 Animal 클래스가 변경되더라도, Dog의 외부 인터페이스에는 영향을 주지 않습니다.
  • 유연성이 증가합니다.
    • Animal 대신 다른 객체(예: Bird 등)를 조합할 수도 있습니다.

 

 

  • 결합도: 상속은 부모 클래스와 강한 결합을 가지며, 변경 시 자식 클래스에 영향을 줍니다. 컴포지션은 느슨한 결합으로 설계되어 유연하게 객체를 교체하거나 확장할 수 있습니다.
  • 유연성: 상속은 단일 부모 클래스만 상속 가능하지만, 컴포지션은 여러 객체를 조합하여 다양한 동작을 추가하거나 변경할 수 있습니다.
  • 캡슐화: 상속은 부모 클래스의 내부 구현이 자식 클래스에 노출되지만, 컴포지션은 내부 객체의 세부 구현을 감추고 필요한 동작만 외부에 제공할 수 있습니다.

 

단점2

- 프로그래머가 찾기힘들다

- 생성자는 javadoc이 자동으로 상단에 모아서 보여준다. 정적 팩토리 메소드는 그렇지 않다.

 

컨벤션

- from : 하나의 매개 변수를 받아서 객체를 생성

   ex. Date date= Date.from(instant);

 

- of: 여러개의 매개 변수를 받아서 객체를 생성

 

- getInstance | instance : 인스턴스를 생성, 이전에 반환했던 것과 같을 수 있음

- newInstance | create: 새로운 인스턴스를 생성

- get[OtherType]: 다른 타입의 인스턴스를 생성, 이전에 반환헀던 것과 같을 수 있음.

- new[OtherType]: 다른 타입의 새로운 인스턴스를 생성.

 

 

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

 

생성자 방식과의 비교

 

내부적으로 다음처럼생겨난다 @Builder를 생성하게되면

public class User {
    private final String name; // 필수
    private final int age; // 선택
    private final String email; // 선택

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
    }

    public static class Builder {
        private final String name; // 필수
        private int age; // 선택
        private String email; // 선택

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

        public Builder setAge(int age) {
            this.age = age;
            return this;
        }

        public Builder setEmail(String email) {
            this.email = email;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

 

 

생성자방식과 빌더패턴의 차이

매개변수 처리 여러 생성자 오버로드 필요 체이닝 방식으로 유연하게 처리
가독성 파라미터 순서와 타입 혼동 가능 명시적 메서드 사용으로 높은 가독성
확장성 생성자 추가/수정 필요 새로운 메서드만 추가
불변성 객체가 완전히 초기화되지 않을 가능성 있음 완전한 초기화 후 객체 생성 보장
코드 유지보수 생성자 조합이 많아질수록 유지보수 어려움 체계적인 속성 관리로 유지보수 용이

 

 

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

 

싱글톤 만드는 방법

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

 

private 생성자는 public static final 필드인 Yaho.Instance를 초기화 할 때 단 한번만 호출된다.

yaho 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

 

정적 팩터리 방식의 싱글톤

public class Yaho {
	private static final Yaho INSTANCE = new Yaho();
	private Yaho() { ... }
	public static Yaho getInstance() { return INSTANCE; }
}

 

첫 번째 코드         두 번째 코드

접근 방식 Yaho.getInstance() Yaho.INSTANCE
유연성 메서드에 추가 로직을 넣을 수 있음 로직 추가가 어렵고 단순 접근만 가능
가독성 메서드 이름으로 Singleton 의도 명확히 전달 가능 간단하고 직관적
추가 기능 필요 시 Lazy initialization, 동기화 등 구현 가능 불가능

 

아이템_04 인스턴스화를 막기위해서는 private 생성자를 사용해라

❓ 생성자를 명시하지 않으면 되지 않을까?

 

👉 생성자를 명시하지 않으면 컴파일러가 자동으로 public으로 기본 생성자가 생성해준다.
사용자가 코드만 보고 생성자가 없다고 생각하더라도 컴파일 시 자동으로 생성된다.

❓ 추상 클래스로 만들어서 인스턴스화를 막기?

abstract 클래스는 인스턴스로 생성하는 것이 불가능하다.
private 생성자 대신에 쓸 수 있지 않을까? 라는 생각이 들 수 있다.

👉 하위 클래스를 생성하면 인스턴스화가 가능해진다.
👉 abstract 클래스는 보통 클래스들의 공통 필드와 메소드를 정의하는 목적으로 만들기 때문에 상속해서 사용하라는 의미로 오해할 수 있다.

 

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

 

public class AutoLottoNumberGenerator implements LottoNumberGenerator {

    private static final int START_INCLUSIVE = 1;
    private static final int END_INCLUSIVE = 45;
    private static final List<Integer> LOTTO_TOTAL_NUMBERS = IntStream.rangeClosed(START_INCLUSIVE, END_INCLUSIVE)
            .boxed()
            .collect(toList());

    @Override
    public List<Integer> generate(int size) {
        List<Integer> lottoTotalNumbers = new ArrayList<>(LOTTO_TOTAL_NUMBERS);
        Collections.shuffle(lottoTotalNumbers);

        return lottoTotalNumbers.stream()
                .limit(size)
                .collect(toList());
    }
}

 

아이템_06 불필요한 객체생성을 줄여라

 

String pool을 사용해라

정적 팩터리 메소드를 이용하여 불필요한 객체 생성을 줄여라

 

객체 생성이 비싼 경우 캐싱을 통해 객체 생성을 방지

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

String.matches가 가장 쉽게 정규 표현식에 매치가 되는지 확인하는 방법이긴 하지만 성능이 중요한 상황에서 반복적으로 사용하기에 적절하지 않다.

 

그 이유는 내부적으로 Pattern 인스턴스를 만들어 사용하는데 Pattern 인스턴스는 입력받은 정규표현식의 유한 상태 머신을 만들기 때문에 생성 비용이 높다. 이런 생성 비용이 높은 Pattern 인스턴스를 한 번 쓰고 버리는 구조로 만들어 곧바로 GC의 대상이 되게 만들고 있다. 즉, 비싼 객체라고 할 수 있다.

 

public class RomanNumber {
    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 str) {
        return ROMAN.matcher(str).matches();
	}
}

두 방법의 성능을 확인해본 결과 캐싱된 인스턴스를 사용하는 경우 약 10배 정도의 성능 향상이 발생했다는 것을 확인할 수 있다.

 

정규표현식의 유한 상태 머신

유한 상태 머신은 주어지는 모든 시간에서 처해 있을 수 있는 유한 개의 상태를 가지고 주어지는 입력에 따라 어떤 상태에서 다른 상태로 전환시키거나 출력이나 액션이 일어나게 하는 장치를 나타낸 모델이다.

 

이를 쉽게 설명하기 위해 java라는 패턴의 입력 문자열을 포함하고 있는지 확인하기 위해서 다음과 같은 과정으로 동작하게 된다.

 

1. 아직 입력한 아무 문자도 보지 못한 상태에서 시작

2. `j`라는 문자를 보면 `j`를 본 상태로 전이

3. `a`라는 문자를 보면 `ja`까지 본 상태로 전이되지만 `a`가 아닌 다른 문자를 보게 되면 아직 아무 문자도 보지 못한 상태로 전이

4. 입력 문자열을 모두 끝까지 탐색할때 까지 반복수행

 

즉, 간단한 정규표현식이라도 유한 상태 머신으로 변환될 경우 매우 큰 사이즈가 되게 된다.

 

일반적인 탐색과 다르게 모든 상태와 전이를 찾아놓은 상태에서 매칭을 진행하기 때문에 생성비용이 높다는 단점이 있다. 하지만 생성 이후 부터는 매칭이 빠르게 할 수 있어 컴파일러 시점에서 생성하는 것을 권장하고 있다.

 

 

4. 오토 박싱을 사용할 때 주의하라

 


오토 박싱은 기본 타입과 박싱 된 기본 타입을 자동으로 변환해주는 기술이다.

 

하지만 이를 잘못 사용하게 되면 불필요한 메모리 할당과 재할당을 반복하여 성능이 느려질 수 있다.

 

다음 코드를 통해서 한변 살펴보자.

 

그리고 해당 코드들을 테스트를 통해 시간을 확인해보았다.

 

Long 타입에서 long 타입으로 변경해주는 것 만으로도 엄청난 성능 차이가 나는 것을 확인할 수 있다.

 

정리하자면, 꼭 박싱 타입이 필요한 경우가 아니라면 기본 타입을 사용하고, 의도치 않은 오토 박싱이 숨어들지 않게 주의하자.

 

 

아이템 _07 다 쓴 객체 참조를 해제하라

 

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);
    }

}

 

위 코드는 메모리 누수 문제가 있다.

스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다

 

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

 

해법은 간단하다. 해당 참조를 다 썼을 때 null 처리 (참조 해제)하면 된다

 

아이템 _08 finalizer 와 cleaner 사용을 피하라

 

finalizer cleaner 즉시 수행된다는 보장이 없다. 언제 실행될 지 알 수 없으며 시간이 얼마나 걸릴지는 아무도 모른다. 즉 원하는 시점에 실행하게 하는 작업은 절대 할 수 없다.

 

  1. 파일 리소스를 반납하는 작업을 처리한다면 그 파일 리소스가 언제 처리 될지 알 수 없다.
  2. 반납이 되지 않아 새로운 파일을 열지 못 하는 상황이 발생할 수 있다.
  3. 동시에 열 수 있는 파일 개수가 제한되어 있다.

 

3. finalizer와 AutoCloseable 비교

특성finalizerAutoCloseable (try-with-resources)

자원 해제 시점 GC 과정에서 실행 (즉시 해제 보장 X) try 블록 종료 시 즉시 해제
성능 GC 성능 저하, 느림 성능 문제 없음
예외 처리 예외 발생 시 무시, 경고도 없음 예외 발생 시에도 close() 호출 보장
안전성 상태가 불완전하게 남을 위험 있음 안전하게 자원 해제
사용 사례 거의 사용하지 않음 권장되는 자원 관리 방법
Deprecated 여부 Java 9부터 finalizer Deprecated 지속적으로 사용 가능

 

기술적 차이: GC와 직접 호출

특징finalizertry-with-resources

자원 해제 시점 GC가 객체를 회수하는 시점 (비결정적) try 블록 종료 시 (결정적)
실행 주체 GC와 Finalizer Thread 프로그래머가 close() 호출을 명시적으로 정의
실행 우선순위 낮음 즉시 실행
성능 GC 부하 증가, 느림 빠름
예외 처리 보장 보장되지 않음 보장

 

 

아이템 _09 try finally 보다는 try - with -resources를 사용해라

try-finally와 try-with-resources 비교

특징try-finallytry-with-resources

도입 시점 자바 초기부터 지원 자바 7부터 도입
가독성 - 코드가 복잡하고 중첩될 가능성 높음
- 여러 자원을 처리하면 코드가 지저분해짐
- 코드가 간결하고 가독성이 높음
- 자원을 명시적으로 관리하기 쉬움
자원 해제 시점 - finally 블록에서 명시적으로 호출해야 함 - try 블록 종료 시 자동으로 자원 해제
예외 처리 - finally에서 발생한 예외가 원래 예외를 덮어씀
- 디버깅 시 초기 예외를 확인하기 어려움
- try 블록의 예외를 기록하고, close()에서 발생한 예외는 Suppressed로 기록하여 모두 확인 가능
지원 대상 모든 클래스 가능 AutoCloseable 인터페이스를 구현한 클래스만 가능
예외 디버깅 지원 - 초기 예외가 finally의 예외로 덮어지므로 디버깅이 어렵다 - Suppressed 예외로 인해 모든 예외를 확인 가능
자원 관리 - 수동으로 자원을 해제해야 함 - 자동으로 자원 해제
다중 자원 처리 - 중첩된 try-finally 구조 필요 - 단일 try-with-resources 블록으로 간단하게 처리 가능
성능 및 효율성 - 수동 관리로 인한 오류 가능성 있음 - 자원 누수 방지 및 정확한 관리 가능
예외 처리와 병행 - 예외 처리 가능하지만 코드 복잡성 증가 - 예외 처리를 명확히 정의 가능

 

728x90