7 minute read

9장 일반적인 프로그래밍 원칙

지역변수의 범위를 최소화하라.

이번 아이템은 기본적으로 ‘클래스와 멤버의 접근 권한을 최소하하라’고 한 아이템 15와 취지가 비슷하다.

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.

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

지역변수를 생각 없이 선언하다 보면 변수가 쓰이는 범위보다 너무 앞서 선언하거나, 다 쓴 뒤에도 여전히 살아 있게 되기 쉽다.

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

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

반복문은 독특한 방식응로 변수 범위를 최소화해준다. 에전의 for 형태든 새로우 for-each 형태든, 반복문에서는 반복 변수의 범위가 반복문의 몸체, 그리고 for 키워드와 몸체 사이의 괄호 안으로 제한된다. 따라서 반복 변수의 값을 반복문이 종료된 뒤에도 써야 하는 상황이 아니라면 while 문보다는 for 문을 쓰는 편이 낫다.

지역변수 범위를 최소화하는 마지막 방법은

메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다.

한 메서드에서 여러 가지 기능을 처리한다면 그중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다. 해결책은 간단하다. 단순히 메서드를 기능별로 쪼개면 된다.

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

for(Element e : elements) {}

컬렉션을 중첩해 순회해야 한다면 for-each 문의 이점이 더욱 커진다.

for (Suit suit : suits) { 
	for(Rank rank : ranks) { }
} 

하지만 안타깝게도 for-each 문을 사용할 수 없는 상황이 세 가지 존재한다.

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

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

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

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

  1. 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
  2. 핵심적인 일과 크게 관련없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
  3. 따로 노력하지 않아도 성능이 지속해서 개선된다는 점이다.
  4. 기능이 점점 많이진다는 것이다.
  5. 작성한 코드가 많은 사람에게 낯익은 코드가 된다.

바퀴를 다시 발명하지 말자. 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태고 구현해놓았을 가능성이 크다. 그런 라이브러리가 있다면, 쓰면 된다. 있는지 잘 모르겠다면 찾아보라. 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋다. 점차 개선될 가능성이 크다. 여러분의 실력을 폄하하는 게 아니다. 코드 품질에도 규모의 경제가 적용된다. 즉, 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 훨씬 많이 받으므로 코드 품질도 그만큼 높아진다.

정확한 답이 필요하다면 float과 double은 피하라.

  • float과 double 타입은 특히 금융 관련 계산과는 맞지 않는다.

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

정확한 답이 필요한 계산에는 float나 double을 피하라. 소수점 추적은 시스템이 맡기고, 코딩 시의 불편함이나 성능 저하를 신경 쓰지 않는다면 BigDecimal을 사용하라. BigDecimal이 제공하는 여덟 가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있다. 법으로 정해진 반올림을 수행해야 하는 비즈니스 계산에서 아주 편리한 기능이다. 반면, 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int나 long을 사용하라. 숫자를 아홉 자리 십진수로 표현할 수 있다면 int를 사용하고, 열여덟 자리 십진수로 표현할 수 있다면 long을 사용하라. 열여덟 자리를 넘어가면 BigDecimal을 사용해야 한다.

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

기본 타입과 방식된 기본 타입의 주된 차이는 크게 세 가지다.

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

박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다. 실무에서 이와 같이 기본 타입을 다루는 비교자가 필요하면 Comparator.naturalOrder()를 사용하자.

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

그렇다면 박싱된 기본 타입은 언제 써야할까? 적절히 쓰이는 경우가 몇 가지가 있다.

첫 번째 컬렉션의 원소, 키 값으로 쓴다. 일반화해 말하면 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다.

마지막으로 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.

기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라. 기본 타입은 간단하고 빠르다. 박싱된 기본 타입을 써야 한다면 주의를 기울이자. 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다. 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데, 이는 여러분이 원한 게 아닐 가능성이 크다. 같은 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 nullPointerexception을 던질 수 있다. 마지막으로, 기본 타입을 박싱하는 작없은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.

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

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

많은 사람이 파일, 네트워크, 키보드 입력으로부터 데이터를 받을 때 주로 문자열을 사용한다. 사뭇 자연스러워 보이지만, 입력받을 데이터가 진짜 문자열일 때만 그렇게 하는 게 좋다.

기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고, 없다면 새로 하나 작성하라.

  • 문자열은 열거 타입을 대신하기에 적합하지 않다.
  • 문자열은 혼합 타입을 대신하기에 적합하지 않다.
  • 문자열은 권한을 표현하기에 적합하지 않다.

더 적합한 데이터 타입이 있거나 새로 작성할 수 있다면, 문자열을 쓰고 싶은 유혹을 뿌리쳐라. 문자열은 잘못 사용하면 번거롭고, 덜 유연하고, 느리고, 오류 가능성도 크다. 문자열을 잘못 사용하는 흔한 예로는 기본 타입, 열거 타입, 혼합 타입이 있다.

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

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

문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없는 결과다.

성능을 포기하고 싶지 않다면 Stinrg 대신 StringBuilder 혹은 StringBuffer를 사용하자.

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

아이템 51에서 매개변수 타입으로 클래스가 아니라 인터페이스를 사용하라고 했다.

이 조언을 ‘객체는 클래스가 아닌 인터페이스로 참조하라’고까지 확장할 수 있다.

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

인터페이스를 타입으로 사용한느 습관을 길러두면 프로그램이 훨씬 유연해질 것이다. → 나중에 구현 클래스를 교체하고자 한다면 그저 새 클래스의 생정자를 호출해주기만 하면 된다.

→ 단, 주의할 점이 있다. 원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 한다.

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

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

리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.

Class 객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있고, 이어서 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다.

심지어 Constructor, Method, Field 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수 있다.

예를 들어 Method.invoke는 어떤 클래스의 어떤 객체가 가진 어떤 메서드라도 호출할 수 있게 해준다.

단점

  1. 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
  2. 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
  3. 성능이 떨어진다.

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

컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을 것이다ㅏ.

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

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

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

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

최적화는 신중히 하라.

모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 개를 소개한다.

  • (맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다(심지어 효율을 높이지도 못하면서).
  • (전체의 97% 정도인) 자그마한 효율성을 모두 잊자. 섣부른 최적화가 만악의 근원이다.
  • 최적화를 할 때는 다음 두 규칙을 따르라. 첫 번째, 하지 마라. 두 번째, (전문가 한정) 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.

최적화는 좋으 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다. 빠르지도 않고 제대로 작동하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시키는 것이다.

성능 때문에 견고한 구조를 희생하지 말자.

빠른 프로그램보다는 좋은 프로그램을 작성하라.

좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍처 자체가 최적화할 수 있는 길을 안내해줄 것이다.

설계 단계에서 성능을 반드시 염두에 주어야 한다.

  • 성능을 제한하는 설계를 피하라.
    • 완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트끼리 혹은 외부 시스템과의 소통 방식이다.
  • API를 설계할 때 성능에 주는 영향을 고려하라.
    • 성능을 위해 API를 왜곡하는 건 매우 안 좋은 생각이다.

프로파일링 도구는 최적화 노력을 어디에 집중해야 할지 찾는 데 도움을 준다. 이런 도구는 개별 메서드의 소비 시간과 호출 횟수 같은 런타임 정보를 제공하여, 집중할 곳은 물론 알고리즘을 변경해야 한다는 사실을 알려주기도 한다.

빠른 프로그램을 작성하려 안달하지 말자. 좋은 프로그램을 작성하다 보면 성능은 따라오게 마련이다. 하지만 시스템을 설계할 때, 특히, API , 네트워크 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두에 두어야 한다. 시스템 구현을 완료했다면 이제 성능을 측정해보라. 충분히 빠르면 그것으로 끝이다. 그렇지 않다면 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라. 가장 먼저 어떤 알고리즘을 사용 했는지를 살펴보자. 알고리즘을 잘못 골랐다면 다른 저수준 최적화는 아무리 해봐야 소용이 없다. 만족할 때까지 이 과정을 반복하고, 모든 변경 후에는 성능을 측정하라.

일반적으로 통용되는 명명 규칙을 따르다.

표준 명명 규칙을 체화하여 자연스럽게 베어 나오도록 하자. 철자 규칙은 직관적이라 모호한 부분이 적은 데 반해, 문법 규칙은 더 복잡하고 느슨하다.