[Effective Java] 9장. 일반적인 프로그래밍 원칙

kkatal_chae·2022년 10월 30일
0

Effective Java

목록 보기
8/11
post-thumbnail

아이템 57. 지역변수의 범위를 최소화하라

지역변수의 범위를 줄이는 가장 강력한 기법은 역시 ‘가장 처음 쓰일 때 선언하기’ 다.

거의 모든 지역변수는 선언과 동시에 초기화해야 한다.

초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.

반복문은 독특한 방식으로 변수 범위를 최소화해준다.

예전의 for 형태든 새로운 for-each 형태든, 반복문에서는 반복 변수의 범위가 반복문의 몸체, 그리고 for 키워드와 몸체 사이의 괄호 안으로 제한된다.

while 문은 복사해 붙여넣을 때 오류가 발생할 가능성이 있다. 따라서 반복 변수의 값을 반복문이 종료된 뒤에도 써야 하는 상황이 아니라면 while 문보다는 for 문을 쓰는 편이 낫다.

// 컬렉션이나 배열을 순회하는 권장 관용구 
for ( Element e : c ) {
 ... 
}

// 반복자가 필요할 때의 관용구 
for ( Iterator< Element > i = c.iterator(); i.hasNext(); ) {
 Element e = i.next();
 ... 
}

for 문은 변수 유효 범위가 for 문 범위와 일치하여 똑같은 이름의 변수를 여러 반복문에서 써도 서로 아무런 영향을 주지 않는다.

지역변수 범위를 최소화하는 마지막 방법은 메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다.


아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라

향상된 for 문은 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다. 또한, 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지는 신경 쓰지 않아도 된다.

// 컬렉션과 배열을 순회하는 올바른 관용구 
for ( Element e : elements ) {
 ... 
}

for-each 문을 사용할 수 없는 상황

  • 파괴적인 필터링 : 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다. 자바 8 부터는 CollectionremoveIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
  • 변형 : 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
  • 병렬 반복 : 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.

for-each 문은 컬렉션과 배열은 물론 `Iterable` 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.

💡 전통적인 for 문과 비교했을 때 for-each 문은 명료하고, 유연하고, 버그를 예방해준다. 성능 저하도 없다. 가능한 모든 곳에서 for 문이 아닌 for-each 문을 사용하자.


아이템 59. 라이브러리를 익히고 사용하라

표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.

자바 7 부터는 Random 을 더 이상 사용하지 않는 게 좋다. ThreadLocalRandom 으로 대체하면 대부분 잘 작동한다. 한편, 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom 을 사용하라.

표준 라이브러리를 쓰는 두 번째 이점은 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다는 것이다.

세 번째 이점은 따로 노력하지 않아도 성능이 지속해서 개선된다는 점이다.

네 번째 이점은 기능이 점점 많아진다는 것이다.

위의 이점들에 비춰볼 때 표준 라이브러리의 기능을 사용하는 것이 좋아보이지만, 많은 프로그래머가 직접 구현해 쓰고 있다.

💡 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다. 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 크다. 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 훨씬 많이 받으므로 코드 품질도 그만큼 높아진다.

아이템 60. 정확한 답이 필요하다면 float 와 double 은 피하라

floatdouble 타입은 특히 금융 관련 계산과는 맞지 않는다

0.1 혹은 10 의 음의 거듭 제곱 수를 표현할 수 없기 때문이다.

→ 금융 계산에는 BigDecimal , int 혹은 long 을 사용해야 한다.

BigDecimal 을 사용하면 정확한 답을 얻을 수는 있지만, 기본 타입보다 쓰기가 훨씬 불편하고 느리다는 단점이 있다.

대안으로 int 혹은 long 을 사용할 수 있지만 사용할 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 한다.

💡 정확한 답이 필요한 계산에는 floatdouble 을 피하라.
소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경쓰지 않는다면 BigDecimal 을 사용하라
반면, 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 intlong 을 사용하라.
열여덟 자리를 넘어가면 BigDecimal 을 사용해야 한다.


아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용하라

기본 타입과 박싱된 기본 타입의 주된 차이

  • 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다. 달리 말하면 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
  • 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값, 즉 null 을 가질 수 있다.
  • 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.

박싱된 기본 타입에 비교 연산자 ( == ) 를 사용하면 오류가 일어난다

// 기이하게 동작하는 프로그램
public class Unbelievable {
	static Integer i;

	public static void main( String[] args ) {
		if ( i == 42 ) System.out.println(" 결과는 " );
	}
}

결과는 NullPointerException 을 던진다.

원인은 i 가 Integer 라는 것, Integer 도 다른 참조 타입 필드와 마찬가지로 초기값이 null 이라는 데 있다.

기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.

그리고 null 참조를 언박싱하면 NullPointerException 이 발생한다.

박싱된 기본 타입을 쓰이는 적절한 경우

  • 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 한다.
  • 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다.
  • 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.
💡 기본타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다. 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 낳을 수 있다.

아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라

문자열은 다른 값 타입을 대신하기에 적합하지 않다. 열거 타입, 혼합 타입 또한 대신하기에 적합하지 않다.

문자열은 권한을 표현하기에 적합하지 않다

// 잘못된 예 - 문자열을 사용해 권한을 구분하였다
public class ThreadLocal {
	private ThreadLocal() { } // 객체 생성 불가 

	// 현 스레드의 값을 키로 구분해 저장한다.
	public static void set( String key, Object value );

	// 키가 가리키는 현 스레드의 값을 반환한다.
	public static Object get( String key );
}

이 방식의 문제는 스레드 구분용 문자열 키가 전역 이름공간에서 공유된다는 점이다.

이 API 는 문자열 대신 위조할 수 없는 키를 사용하면 해결된다 이 키를 권한( capacity ) 이라고도 한다.

public final class ThreadLocal<T> {
	public ThreadLocal();
	public void set( T value );
	public T get();
}
💡 더 적합한 데이터 타입이 있거나 새로 작성할 수 있다면, 문자열을 쓰고 싶은 유혹을 뿌리쳐라. 문자열은 잘못 사용하면 번거롭고, 덜 유연하고, 느리고, 오류 가능성도 크다.

아이템 63. 문자열 연결은 느리니 주의하라

문자열 연결 연산자 ( + ) 는 여러 문자열을 하나로 합쳐주는 편리한 수단이다. 그러나 본격적으로 사용하면 성능 저하를 감내하기 어렵다.

문자열 연결 연산자로 문자열 n 개를 잇는 시간은 n 의 제곱에 비례한다.

성능을 포기하고 싶지 않다면 String 대신 StringBuilder 를 사용하자

💡 원칙은 간단하다. 성능에 신경 써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산자를 피하자. 대신 Stringbuilderappend 메서드를 사용하라.


아이템 64. 객체는 인터페이스를 사용해 참조하라

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라

// 좋은 예
Set<Son> sonSet = new LinkedHashSet<>();

// 나쁜 예 
LinkedHashSet<Son> sonset = new LinkedHashSet<>();

인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.

💡 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 클래스를 타입으로 사용하자.

아이템 65. 리플렉션보다는 인터페이스를 사용하라

리플렉션( java.lang.reflect ) 을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다. 나아가 Constructor, Method, Field 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다.

리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있는데, 물론 단점이 있다.

  • 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다. 예외 검사도 마찬가지다.
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
  • 성능이 떨어진다. 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.

리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.

// 리플렉션으로 생성하고 인터페이스로 참조해 활용
public static void main(String[] args) {
	// 클래스 이름을 Class 객체로 변환
	Class<? extends Set<String>> cl = null;
	try {
		cl = ( Class<? extends Set<String>> )
						Class.forName( args[0] );
	} catch ( ClassNotFoundException e ) {
		fatalError( "클래스를 찾을 수 없습니다." );
	}

	// 생성자를 얻는다
	Constructor<? extends Set<String>> cons = null;
	try {
		cons = cl.getDeclaredConsturctor();
	} catch ( NoSuchMethodException e ) {
		fatalError( "매개변수 없는 생성자를 찾을 수 없습니다.");
	}

	// 집합의 인스턴스를 만든다
	Set<String> s = null;
	try {
		s = cons.newInstance();
	} catch ( IllegalAccessException e ) {
		fatalError( "생성자에 접근할 수 없습니다." );
	} catch ( InstantiationException e ) {
		fatalError( "클래스를 인스턴스화할 수 없습니다." );
	} catch ( InvocationTargetException e ) {
		fatalError( "생성자가 예외를 던졌습니다" + e.getCause());
	} catch ( ClassCastException e ) {
		fatalError( "Set 을 구현하지 않는 클래스입니다." );
	}

	s.addAll( Arrays.asList( args ).subList( 1, args.length));
	System.out.println( s );
}

private static void fatalError( String msg ) {
	System.err.println( msg );
	System.exit( 1 );
}

💡 간단한 프로그램이지만 여기서 선보인 기법은 강력하다. 이 프로그램은 손쉽게 제네릭 집합 테스터로 변신할 수 있다. 즉, 명시한 Set 구현체를 공격적으로 조작해보며 Set 규약을 잘 지키는지 검사해볼 수 있다. 비슷하게, 제네릭 집합 성능 분석 도구로 활용할 수도 있다. 사실 이 기법은 완벽한 서비스 제공자 프레임워크를 구현할 수 있을 만큼 강력하다. 대부분의 경우 리플렉션 기능은 이 정도만 사용해도 충분하다.

단점도 존재한다.

  1. 런타임에 총 여섯 가지나 되는 예외를 던질 수 있다. 그 모두가 인스턴스를 리플렉션 없이 생성했다면 컴파일타임에 잡아낼 수 있었을 예외들이다.
  2. 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성했다. 리플렉션이 아니라면 생성자 호출 한 줄로 끝났을 일이다.

💡 핵심정리
리플렉션은 복잡한 특수 시스템을 개발할 때 필요한 강력한 기능이지만, 단점도 많다. 컴파일타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용해야 할 것이다.
단, 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위 클래스로 형변환해 사용해야 한다.


아이템 66. 네이티브 메서드는 신중히 사용하라

네이티브 메서드 : C 나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.

전통적인 네이티브 메서드의 주요 쓰임

  • 레지스트리 같은 플랫폼 특화 기능을 사용
  • 네이티브 코드로 작성된 기존 라이브러리를 사용
  • 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성

성능을 개선할 목적으로 네이티브 메서드를 사용하는 것을 거의 권장하지 않는다

네이티브 메서드에는 심각한 단점이 있다.

네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.

네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮다.

디버깅도 더 어렵다. 주의하지 않으면 속도가 오히려 느려질 수도 있다.

가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.

💡 네이티브 메서드를 사용하려거든 한번 더 생각하라. 네이티브 메서드가 성능을 개선해주는 일은 많지 않다. 저수준 자원이나 네이티브 라이브러리를 사용해야만 해서 어쩔 수 없더라도 네이티브 코드는 최소한만 사용하고 철저히 테스트하라. 네이티브 코드 안에 숨은 단 하나의 버그가 여러분 애플리케이션 전체를 훼손할 수도 있다.

아이템 67. 최적화는 신중히 하라

그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다

  • 윌리엄 울프

자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다.

  • 도널드 크누스

최적화 할 때는 다음 두 규칙을 따르라.
첫 번째, 하지 마라.
두 번째, 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.

  • M.A 잭슨

성능 때문에 견고한 구조를 희생하지 말자. 빠른 프로그램보다는 좋은 프로그램을 작성하라

성능을 제한하는 설계를 피하라

완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다.

API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 대표적이다.

API 를 설계할 때 성능에 주는 영향을 고려하라.

프로파일링 도구는 최적화 노력을 어디에 집중해야 할지 찾는 데 도움을 준다. 이런 도구는 개별 메서드의 소비 시간과 호출 횟수 같은 런타임 정보를 제공하여, 집중할 곳은 물론 알고리즘을 변경해야 한다는 사실을 알려주기도 한다. 프로파일러는 아니지만 자바 코드의 상세한 성능을 알기 쉽게 보여주는 마이크로 벤치마킹 프레임워크인 JMH 도 고려해볼만 하다.

💡 핵심정리
빠른 프로그램을 작성하려 안달하지 말자
좋은 프로그램을 작성하다 보면 성능은 따라오게 마련이다.
하지만 시스템을 설계할 때, 특히 API, 네트워크 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두에 두어야 한다.
시스템을 구현을 완료했다면 이제 성능을 측정해보라. 프로파일러를 이용하고 알고리즘을 먼저 살피고 최적화를 반복하고 성능을 측정하라.


아이템 68. 일반적으로 통용되는 명명 규칙을 따르라

식별자 타입
패키지와 모듈org.junit.jupiter.api, com.google.common.collect
클래스와 인터페이스Stream, FutureTask, LinkedHashMap, HttpClient
메서드와 필드remove, groupingBy, getCrc
상수 필드MIN_VALUE, NEGATIVE_INFINITY
지역변수i, denom, houseNum
타입 매개변수T, E, K, V, X, R, U, V, T1, T2

💡 핵심정리
표준 명명 규칙을 체화하여 자연스럽게 베어 나오도록 하자.
오랫동안 따라온 규칙과 충돌한다면 그 규칙을 맹종해서는 안된다. 상식이 이끄는 대로 따르자.

0개의 댓글