티스토리 뷰

나의 공부방

[개발서적] 이펙티브 자바(Effective Java) 핵심 요약 및 정리

망나니개발자 2021. 3. 5. 17:21
반응형

이펙티브 자바를 읽으면서 제가 제대로 적용하지 못했던 부분 혹은 개념적으로 부족했던 부분을 정리하고, 앞으로의 개발에 적용해보고자 합니다.

 

1. 객체의 생성과 파괴


[ 1. 생성자 대신 정적 팩토리 메서드를 이용하라 ]

  1. 메소드 이름을 가질 수 있다. -> 명확한 이름으로 값을 생성할 수 있다.
  2. 호출 때 마다 인스턴스를 새로 생성하지 않을 수 있다.
  3. 하위 클래스를 반환하는 유연성을 얻을 수 있다.
  4. 매개 변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  5. 정적 팩토리 메소드를 작성하는 시점에는 해당 객체의 클래스가 존재하지 않아도 된다.

정적 팩토리 메소드를 작성하는 시점에는 해당 객체의 클래스가 존재하지 않아도 된다.

하지만 정적 팩토리 메서드 역시 다음과 같은 단점이 있다.

  1. 하위 클래스로 상속하기 위해서는 public 또는 protected 생성자가 필요하다.
  2. 프로그래머가 찾기 어렵다. --> 생성자처럼 명확하지 않으니 from, of 등과 같은 규약을 따라야 한다.

즉, 정적 팩토리 메서드와 생성자 모두 장/단점이 있다. 상황에 맞게 적절한 방법을 선택하도록 하자.

 

[ 2. 생성자의 매개변수가 많다면 빌더를 고려하라 ]

생성자의 매개변수가 많아지면 코드를 직관적으로 이해하기 어렵다. 이에 대한 대안으로 Setter를 사용하면 여러 개의 Setter가 호출되며 객체의 생성 전까지 일관성이 무너지게 되고, Open-Closed 법칙을 위배하게 된다.

이러한 대한으로 빌더를 사용하면 쓰기 쉽고, 읽기 쉬워 가독성을 높일 수 있으며 가변 매개변수를 활용함으로써 유연함을 얻을 수 있다.

 

[ 3. 인스턴스 생성이 불필요한 경우 private 생성자를 사용하라 ]

정적 메서드나 정적 필드만을 갖는 클래스를 생성하는 경우가 있다. 객체 지향적 관점에서 그렇게 좋지는 않은 방식이지만 분명 필요한 경우가 있는데, 예를 들면 유틸성 클래스이다. 이러한 클래스들은 인스턴스를 생성하기 위해 만든 것이 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들 것이고, 다른 사용자는 이것이 자동 생성된 것인지 구분할 수 없을 것이다. 그러므로 이를 방지하기 위해 private 생성자를 추가해주도록 하자.

 

[ 4. 다 쓴 객체를 참조 해제하라 ]

사용이 끝난 객체를 해제해주지 않는다면 에러를 만나게 될 것이다. 하지만 모든 객체를 null로 처리해줄 필요는 없고, 메모리를 직접 관리하여 가비지 컬렉터가 불필요한 객체임을 인지하지 못하는 경우 null로 선언하면 된다.

 

[ 5. try-with-resources를 활용하라 ]

사용한 자원을 close 해주어야 하는 경우가 있다. 하지만 이를 잊기 쉬우며, 중첩되면 코드가 복잡해지는데, 자바 7에서는 이를 해결하기 위해 try-with-resources를 지원하고 있다. try-with-resources를 사용하기 위해서는 자원이 AutoClosable 인터페이스를 구현해야 하는데, 대부분의 자바 라이브러리에서는 이를 구현해두었으므로 손쉽게 이용할 수 있다.

 

 

2. 클래스와 인터페이스


[ 1. 클래스와 멤버의 접근 권한을 최소화하라 ]

잘 설계된 컴포넌트는 클래스 내부 데이터와 구현정보를 숨겨 캡슐화 또는 정보 은닉이 잘 되어있다.

 

[ 2. 변경 가능성을 최소화하라 ]

변경 가능성을 최소화하는 방법 중 하나는 불변의 객체를 생성하는 것이다.

불변의 객체는 단순하다. 생성된 시점에서 파괴되는 시점까지 동일한 값을 유지하기 때문이다. 또한 동기화를 고려할 필요가 없으며 사용에 안전하다. 물론 새로운 값을 지닌 객체가 필요한 경우에는 값의 수정이 불가능하니 새로 생성해주어야 한다는 단점도 있다.

불변 클래스임을 보장하기 위해서는 final 클래스로 선언하는 것이 있다. 하지만 이보다 정적 팩토리 메소드를 제공하면 더욱 유연하게 불변 객체를 생성해줄 수 있다. 이를 활용하면 원하는 값을 지니는 불변 클래스를 생성할 수 있으니 더욱 유연하다. 또한 필요에 따라 캐싱 기능도 제공해줄 수 있다.

변경 가능성을 최소화하는 기법들로는 다음과 같은 것들이 더 있다.

  1. Setter는 필요한 경우에만 만들자
  2. 클래스는 꼭 필요한 경우가 아니라면 불변으로 만들자
  3. 단순한 값 객체는 불변으로 만들자.

모든 클래스를 불변으로 만들 수는 없을 것이다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이면 해당 객체를 예측하기 쉬워지고, 오류가 발생할 가능성을 줄일 수 있을 것이다. 그러므로 변경이 필요한 필드를 제외한 나머지 필드는 final로 선언하자.

즉, 다른 합당한 이유가 없다면 모든 필드는 private final 이여야 하는 것이다.

 

[ 3. 상속보다는 조합을 사용하자 ]

상속은 상당히 강력한 기술이지만 상위 클래스의 구현이 하위 클래스로 노출되어 캡슐화를 깨뜨린다. 또한 상위 클래스와 강결합 되어 있기 때문에 상위 클래스가 변경된다면 하위 클래스의 변경 역시 불가피하다.

그러므로 상위 클래스와 하위 클래스가 순수한 is-a 관계인 경우에만 사용해야 하며, is-a 관계인 경우에도 상위 클래스가 확장을 고려해 설계되었는지 점검 후에 이용하는 것이 좋다.

 

[ 4. 상속을 고려해 설계하고 문서화하라 ]

상속용 클래스는 재정의가능한 메소드들을 내부적으로 어떻게 이용하는지 공개하여 부작용을 방지해야 한다. 또한 중간 과정에 이용되는 hook 메소드 역시 protected로 제공하면 더욱 좋다. 그리고 해당 클래스를 배포하기 전에 최소 3개 이상의 하위 클래스를 만들어 검증해보도록 하자.

또 다른 주의할 점은 상위 클래스의 생성자에서 재정의 가능한 메소드를 호출하면 안된다는 것이다. (p125 예제 참고)

 

[ 5. 추상클래스보다는 인터페이스를 우선하라 ]

추상클래스를 구현했다는 것은 추상클래스가 조상클래스라는 것인데, 이러한 구조는 클래스 계층구조에 큰 혼란을 줄 수 있다. 반대로 인터페이스는 믹스인 타입으로 주된 타입 외에도 특정한 선택적 행위를 제공한다고 선언하는 효과를 주며 유연성을 얻을 수 있다. 예를 들어 Comparable은 자신을 구현한 클래스의 인스턴스들끼리 순서를 정할 수 있다는 것을 의미한다.

인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다. 또한 Java8부터는 디폴트 메소드를 지원하고 있어, 인터페이스에 구현이 명확한 부분은 개발하여 제공해줄 수 있다.

 

[ 6. 태그 달린 클래스보다는 계층 구조를 활용하라 ]

클래스의 타입별로 분기해주어야 하는 태그 달린 클래스를 활용하면 코드에 분기가 생겨 읽기 어려워지고, 불필요하게 정보를 저장해야 되어 메모리도 더 잡아먹는다. 이러한 경우에는 다형성을 이용해 해결하도록 하자. (p144 참고)

 

[ 7. 정적 멤버 클래스와 비정적 멤버 클래스 ]

비정적 멤버 클래스의 인스턴스는 압묵적으로 바깥 클래스의 인스턴스와 연결된다. 그래서 비정적 멤버 클래스에서 클래스명.this 의 형태로 바깥 클래스를 참조할 수 있다. 따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스 없이 생성할 수 없기 때문이다.

비정적 멤버 클래스는 바깥인스턴스.new MemberClass()를 통해서 수동으로 생성해줄 수도 있는데, 이 관계정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 더 차지하고, 생성 시간도 오래 걸린다.

static을 선언하지 않으면 바깥 인스턴스로의 숨은 내부 참조를 갖게 된다. 이는 시간과 자원을 더 사용할 뿐 아니라, 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 발생할 수 있다. 그러므로 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 반드시 static을 붙여 정적 멤버 클래스로 만들도록 하자.

 

 

3. 제네릭


[ 1. Raw 타입은 사용하지 말라 ]

클래스와 인터페이스의 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 한다. 예를 들어 List 인터페이스 List<E>는 원소의 타입을 나타내는 타입 매개변수 E를 받는다. 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라고 한다. 각각의 제네릭 타입은 매개변수화 타입(parameterized type)을 List<String>과 같이 정의해주어야 한다. 하지만 타입 매개변수를 정의하지 않으면 제네릭 타입은 Raw Type으로 정의되는데, 이것이 문제를 일으킬 수 있다.

만약 Integer를 갖는 List에 String이 추가되어도 오류없이 컴파일되고 실행된다. 그리고 나중에 List에 존재하는 값들을 연산할 때(런타임에) 오류(ClassCastException)를 발견할 수 있을 것이다. 오류는 컴파일 시점에 발견하는 것이 가장 좋은데, 만약 Raw Type이 아니라 타입 파라미터를 List<Integer>로 명시해준다면, 컴파일러가 리스트에 Integer만 넣어야 함을 인지하여 컴파일 시점에 오류를 잡아낼 수 있다. 왜냐하면 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장하기 때문이다.

반대로 List<Object> 역시 Raw Type과 마찬가지로 모든 타입을 포용할 수 있다. 하지만 다른 점은 List<Object>는 모든 타입을 허용하겠다는 의사를 컴파일러에게 명확하게 전달했다는 것이다.

 

[ 2. 배열보다는 리스트를 사용하라 ]

1. 배열은 공변이지만 제네릭인 리스트는 불공변이다.

// 런타임 시정에 오류 발생
Object[] array = new Long[1];
array[0] = "efwaf";

// 컴파일 시점에 오류 발생
List<Object> list = new ArrayList<Long>();
list.add("타입이 달라 넣을 수 없다.");

공변이라는 것은 Sub가 Super의 하위 타입이면 Sub[] 역시 Super[] 의 하위 타입이 된다는 것이다. 하지만 제네릭에는 이러한 관계(공변)가 성립하지 않는다. 그렇기 때문에 이를 컴파일 시점에 잡아낼 수 있다.

 

2. 배열은 실체화된다.

배열은 런타임 시점에도 자신이 담기로 한 원소의 타입인지를 확인한다. 반면에 제네릭은 타입 정보가 런타임 시점에는 소거된다. 이를 통해 런타임 시점에 ClassCastException을 만나지 않고, 컴파일 시점에 오류를 잡아낼 수 있다.

 

[ 3. 한정적 와일드카드를 사용해 API의 유연성을 높여라 ]

제네릭은 불공변이기 때문에 하위 타입 객체를 추가하는 경우에 문제가 발생하기 쉽다. 이러한 경우에는 매개변수에 한정적 와일드카드를 이용함으로써 하위 객체 또는 상위 객체까지 연산을 적용할 수 있다. (반환 값에 한정적 와일드카드를 적용하는 것은 오히려 유연성을 떨어뜨린다.)

Iterable<? extends E> src;	    // E의 하위 타입 
Iterable<? super E> src;        // E의 상위 타입

 

물론 와일드 카드를 아무 곳이나 적용하는 곳은 바람직하지 않기 때문에 펙스(Pecs, producer-extends, consumer-super) 공식을 외워두도록 하자. 매개변수화 타입<E> 가 생산자라면 extends를 사용하고, 소비자라면 super 를 이용하라는 공식이다.

이러한 공식을 적용함으로써 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드 카드가 필요하다.

타입 매개변수와 와일드카드는 공통되는 부분이 많아서, 어느 것을 선택해야 할 지 고민이 될 때가 있을 것이다.

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

만약 타입 매개변수가 한번만 나오면 와일드 카드로 대체하라.(2번이 좋다) 이를 통해 유연성을 확보할 수 있을 것이다. 하지만 제네릭 타입에 새로운 값을 추가하는 것이 불가능하므로, 실제 타입으로 바꾸어주는 private 메소드를 추가로 구현하도록 하자.

 

 

4. 열거형


[ 1. 열거 타입을 사용하라 ]

필요한 원소를 컴파일 시점에 다 알 수 있다면 열거 타입을 사용하라.

또한 열거 타입에 Switch문과 같은 분기를 이용하는 것은 새로운 열거 타입이 추가되면 분기를 추가해주어야 해서 유지/보수가 어려워진다. 이러한 경우에는 중첩 열거 타입이나 내부 메소드를 추가(p218)하여 해결하라.

 

 

5. 람다와 스트림


[ 1. 익명 클래스보다는 람다를 사용하라 ]

자바8부터는 추상 메소드 하나짜리 인터페이스는 함수형 인터페이스라는 특별한 대우를 받게 되었다. 그리고 이러한 함수형 인터페이스의 인스턴스를 람다식을 이용해 만들 수 있게 되었다. 람다식을 사용하면 컴파일러의 추론 기능을 통해 매개변수 타입 등의 정보를 생략함으로써 코드를 간결하게 작성할 수 있다.

그렇다고 람다가 무조건 좋은 것은 아니다. 람다는 이름이 없고 문서화도 불가능하기 때문에, 코드 자체가 동작을 명확히 설명하지 못하거나, 줄이 길어지면 더 간단히 줄이거나 람다를 쓰지 않는 쪽으로 리팩토링 하는 것을 고려해야 한다.

 

[ 2. 람다보다는 메소드 참조를 이용하라 ]

람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함이다. 그런데 자바에서는 메소드 참조를 이용하면 람다보다 코드를 간결하게 작성할 수 있다. 메소드 참조는 매개 변수들까지 생략하도록 하여 줄일 수 있는 코드의 양을 높여준다. 또한 메소드 참조는 이름을 지어줄 수 있고, 설명도 문서로 남길 수 있다.

 

[ 3. 스트림은 주의해서 사용하라 ]

Stream을 이용하면 많은 장점을 얻을 수 있다. 하지만 Stream을 과용하면 가독성이 떨어지고 유지보수가 어려워진다. 또한 스트림을 이용하면 람다를 자주 사용하게 되는데, 람다는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 가독성이 유지된다. 또한 세부 구현은 도우미 메소드로 별도로 작성하여 전체적인 가독성을 높여주는 것이 좋다.

만약 Stream을 사용했는데 코드의 가독성이 떨어자고 반복을 사용하여 높아진다면 반복을 선택하는 것이 좋을 수도 있다.

 

[ 4. 스트림에서는 부작용이 없는 함수를 이용하라 ]

스트림 연산에 건네는 함수 객체는 모두 부작용(Side Effect)이 없는 순수 함수여야 한다. (p 277) 예를 들어 forEach에서 값을 set해주는 것과 같은 스트림 코드는 스트림답지 못하다.

 

[ 5. 병렬 스트림은 주의해서 적용하라 ]

데이터 소스가 Stream.iterate거나 중간 연산으로 limit을 사용하면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다. 왜냐하면 파이프라인 병렬화는 limit을 다룰 때 CPU 코어가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다. 그렇기 때문에 주어진 갯수의 결과를 모두 얻어도 남은 갯수의 처리가 끝나도록 기다리게 될 수 있다.

대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 또는 long 형 범위일 때 병렬화의 효과가 가장 좋다. 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기 좋다는 특징이 있다. 또한 이들은 원소의 참조들이 메모리에 연속해서 저장되어 있어서 참조 지역성이 뛰어나다. 참조 지역성이 나쁘면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 멍하니 보내게 되어 벌크 연산을 병렬화할 때 아주 중요한 요소로 작용한다.

그 외에 단말 연산의 작업량이 파이프라인 병렬 수행의 작업량에 상당 비중을 차지하는데, reduce 관련 처리들은 병렬에 적합한 반면 collect 메소드는 컬렉션들을 합치는 비용 때문에 병렬화에 적합하지 않다.

스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되어 예상치 못한 동작이 발생할 수 있으므로 반드시 강도 높게 테스트해보아야 한다. 병렬화 전에 실제로 성능이 향상될 지를 간략히 예측해보는 방법이 있는데, 스트림 안의 원소 수와 원소당 수행되는 줄 수를 곱해보자. 이 값이 최소 수십만은 되어야 성능 향상을 맛볼 수 있다.

 

 

6. 메소드


[ 1. 적시에 방어적 복사본을 만들라 ]

불변의 클래스를 생성하였다 하더라도 해당 객체가 가변적이라면 외부에 의해 불변식이 깨질 수 있다.(p 303)

외부 공격으로부터 내부를 보호하거나 멀티 쓰레딩 환경 등에서 불변성을 보장하기 위해서는 가변 매개변수 각각을 방어적으로 복사해야 한다. 매개변수를 방어적으로 복사하는 목적은 불변 객체를 만들기 위함 뿐만 아니라 제공받은 객체의 참조를 내부에 보관해야 할 때 등에도 그 객체가 잠재적으로 변경가능한지 확인해야 한다.

즉, 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 만약 그 비용이 너무 크거나 수정할 일이 없음을 신뢰한다면 그 책임이 클라이언트에 있음을 문서에 명시하자.

 

[ 2. 메소드 시그니처를 신중히 설계하라 ]

  1. 메소드 이름을 신중히 짓자
  2. 편의 메소드를 너무 많이 만들지 말자
  3. 매개변수 목록은 짧게 유지하자
    1. 여러 메소드로 분리하라
    2. 매개변수 여러 개를 묶어주는 클래스를 생성하라(매개변수 전달용으로만 사용되면 정적 멤버 클래스로 생성)
    3. 빌더 패턴을 메소드 호출에 응용하라
    4. boolean이 매개변수인 경우에는 Enum을 사용하는 것을 고려하라

[ 3. 다중정의(Overloading)는 신중히 사용하라 ]

메소드를 재정의하는 Overriding은 호출할 메소드를 동적으로 선택한다. 즉, 런타임 시점의 상태인 런타임 타입에 의해 결정된다. 그래서 런타임에 가장 하위에서 재정의된 메소드가 호출된다.

하지만 Overloading은 컴파일 시점에 어느 메소드를 호출할 지 정적으로 정해진다. 컴파일 시점에 오직 매개변수의 컴파일 타입에 의해 이루어지기 때문에 오버로딩을 사용하는 경우에는 이를 주의해야 한다.

정확히 어떻게 사용했을 때 다중정의가 혼란을 주느냐는 논란의 여지가 있기 때문에 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 않는 것이 좋다. 또한 가변 인수를 사용하는 경우에는 오버로딩을 하지 않는 것이 좋다. 오히려 다른 메소드 이름을 만드는 것이 좋을 수 있다.

생성자의 경우에는 이름을 다르게 하는 것이 불가능하므로 앞서 배운 정적 팩토리 메소드를 생성하면 된다.

 

[ 4. 가변인수는 신중히 사용하라 ]

메소드를 재정의하는 Overriding은 호출할 메소드를 동적으로 선택한다. 즉, 런타임 시점의 상태인 런타임 타입에 의해 결정된다

 

[ 5. 옵셔널 반환은 신중히 반환하라 ]

옵셔널은 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 클라이언트가 옵셔널을 반환받았을 경우에는 다음과 같은 행동들을 취할 수 있다.

  1. 기본값을 정한다.
  2. 예외를 던진다.
  3. 항상 값이 있다고 가정하고 꺼낸다.
  4. 값의 여부를 boolean으로 받는다.(isPresent는 앞선 메소드들로 대부분 대체가능하다.)

반환값으로 항상 옵셔널을 사용한다고해서 무조건 득이 되는 것은 아니다. 컬렉션, 스트림, 배열, 옵셔널과 같은 컨테이너 타입은 빈 값을 반환하면 처리 코드가 줄어드므로 사용하지 않는 것이 좋다. 또한 Integer나 Boolean은 값을 여러번 감싸므로 특수히 준비된 OptionalInt 등을 사용하는 것이 좋다.

그렇기 때문에 결과가 없을 수 있으며, 클라이언트에서 특별히 이를 처리해야 하는 경우 상황에 옵셔널을 사용하면 좋다. 하지만 옵셔널은 엄연히 새로 객체를 생성하는 것이고, 검사를 위해 메소드가 호출되는 것이므로 성능이 중요한 상황에서는 맞지 않을 수도 있다.

 

 

7. 일반적인 프로그래밍 원칙


[ 1. 라이브러리를 익히고 사용하라 ]

메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가된다. 자바 개발자라면 최소한 java.lang, java.util, java.io 와 그 하위 패키지 및 Collection, Stream 패키지에 대해서는 눈여겨 보도록 하자.

 

[ 2. 정확한 계산을 위해서는 float나 double을 피하라 ]

float와 double은 정확한 계산에 적합하지 않다. 소수점 추적이 필요한 경우 코딩 시의 불편함이나 성능 저하를 신경쓰지 않겠다면 BigDecimal을 이용하라. BigDecimal은 여러가지 반올림모드를 제공하여 반올림을 완벽히 제어할 수 있다. 반면 성능이 중요하고, 소수점을 직접 추적할 수 있으며, 숫자가 너무 크지 않다면 int나 long을 사용해도 된다.

 

[ 3. 박싱된 기본 타입보다는 기본 타입을 사용하라 ]

기본 타입과 박싱된 기본 타입은 다음과 같은 차이점을 지닌다.

  1. 기본 타입은 값만 지니지만, 박싱된 기본 타입은 값에 더해 식별성(동일한 인스턴스인지)을 지닌다.
  2. 기본 타입은 언제나 값을 지니지만 박싱된 기본 타입은 null을 가질 수 있다.
  3. 기본 타입이 시간과 메모리 면에서 보다 효율적이다.

[ 4. 다른 타입이 적절하다면 문자열 사용을 피해라 ]

  1. 문자열은 다른 값 타입을 대신하기에 적합하지 않다.
  2. 문자열은 열거 타입을 대신하기에 적합하지 않다.
  3. 문자열은 혼합 타입을 대신하기에 적합하지 않다.
  4. 문자열은 권한을 표현하기에 적합하지 않다.

[ 5. 문자열 연결은 느리니 주의하라 ]

문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 한다. 문자열 연결 연산자로 n개의 문자열을 잇는 시간은 n*n에 비례한다. 성능을 포기하고 싶지 않다면 StringBuilder를 사용하자. 크기를 초기화하고 이를 사용하면 상당한 성능 이점을 얻을 수 있다.

 

[ 6. 객체는 인터페이스를 사용해 참조하라 ]

적합한 인터페이스만 있다면 매개변수뿐 안이라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 인터페이스 타입으로 선언하면 프로그램이 훨씬 유연해진다.

 

[ 7. 최적화는 신중히 하라 ]

최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽다. 최적화를 하여 성능을 높이기 위해 견고한 구조를 희생하면 오히려 성능이 저하될 수 있다. 또한 빠른 프로그램보다는 좋은 프로그램을 작성하는 것이 좋다. 왜냐하면 구현 상의 문제는 나중에 해결할 수 있지만, 구조 상의 문제는 재구축하지 않고서는 해결이 불가능할 수 있기 때문이다. 그리고 좋은 프로그램이 일반적으로 성능도 좋다.

 

 

8. 예외


[ 1. 예외는 진짜 예외 상황에만 적용하라 ]

가끔 제어 흐름을 위해 예외를 사용하는 경우가 있다. 하지만 다음과 같은 이유로 이러한 상황을 피해야 한다.

  1. 예외는 예외 상황 용도이므로 최적화가 약할 수 있다.
  2. 코드를 try-catch에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
  3. 성능이 떨어질 수 있다.

[ 2. 복구가능한 상황에는 검사 예외를 프로그래밍 오류에는 런타임 예외를 사용하라 ]

호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라. 검사 예외를 던지면 호출자가 그 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제할 수 있다.

반대로 전제 조건을 만족시키지 못하거나 API 명세의 제약을 지키지 못한 경우에는 런타임 예외를 사용하자. 런타임 예외는 에러가 더 이상 전파되지 않는 비검사 예외이다.

 

[ 3. 메소드가 던지는 모든 예외를 문서화하라 ]

메소드가 던질 가능성이 있는 모든 예외를 자바독의 @throws로 문서화하라. 검사 예외든 비검사 예외든, 추상 메소드든 구체 메소드든 모두 마찬가지이다. 대신 비검사 예외는 메소드 선언에는 기입하지 말자. 자바독은 메소드 선언에 있는 throws와 @throws에 명시한 예외를 시각적으로 구분해준다.

 

[ 4. 예외의 상세 메세지에 실패 관련 정보를 담으라 ]

실패 순간을 포착하려면 발생한 예외에 관여된 모든 배개변수와 필드의 값을 실패 메세지에 담아야 한다. 예를 들어 IndexOutOfBoundsException이 발생하였으면 범위의 최솟값, 최댓값, 예외의 인덱스값 모두를 담아야 한다.

 

[ 5. 가능한 실패원자적으로 만들라 ]

  1. 불변의 객체를 사용하라
  2. 실패 가능성이 있는 모든 코드를 객체의 상태를 변경하는 코드 앞에 배치하라
  3. 객체의 임시 복사본에서 작업을 수행하고, 성공적으로 완료되면 원래 객체와 교체하라
  4. 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌려라
  5.  

9. 직렬화


[ 1. 직렬화의 대안을 찾으라 ]

직렬화는 상당히 위험하기 때문에 피해야하며 시스템을 밑바탁부터 설계한다면 Json이나 프로토콜 버퍼와 같은 대안을 사용하는 것이 좋다. 또한 신뢰할 수 없는 데이터는 역직렬화 하지 않는 것이 좋으며 해야한다면 필터를 통해 먼저 검사하도록 하자

 

[ 2. Serializable을 구현할지는 신중히 결정하라 ]

  1. Serializable을 구현하면 릴리즈한 뒤에 수정하기가 어렵다.
  2. 버그와 보안 구멍이 생길 위험이 높아진다.
  3. 해당 클래스의 신버젼을 릴리스할 때 테스트할 것이 늘어난다.

 

 

 

이펙티브 자바는 자바의 고급 내용을 다루며 자바 기반의 클린코드를 작성하기 위한 책입니다. 반드시 최소 한번 쯤은 읽어보시는 것을 추천드리고, 다독하는 것도 매우 추천드립니다!

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함