////
Search

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

Created
2022/09/26 11:56
Tags

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

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.
지역변수의 범위를 줄이는 가장 강력한 기법은 ‘가장 처음 쓰일 때 선언하기’다.
지역변수를 생각 없이 선언하다 보면 변수가 쓰이는 범위보다 앞서 선언하거나 다 쓴 뒤에도 여전히 살아있게 되기 쉽다.
그래서 실수로 의도한 범위 앞 혹은 뒤에서 그 변수를 사용하면 끔찍한 결과로 이어질 수 있다.
또한 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
try-catch문은 이 규칙에서 예외다.
변수를 초기화할 때 예외를 던질 가능성이 있으면 try 블록 안에서 초기화해야 한다.
try 블록을 넘어서도 사용할 것 같으면 try 블록 앞에서 선언해야 한다.

반복문

반복문은 독특한 방식으로 변수 범위를 최소화해준다.
예전의 for 형태든 새로운 for-each 형태든, 반복문에서는 반복 변수의 범위가 반복문의 몸체, 그리고 for 키워드와 몸체 사이의 괄호 안으로 제한된다.
for (Element e : c) { ... }
Java
복사
컬렉션이나 배열을 순회하는 권장 관용구
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) { Element e = i.next(); ... }
Java
복사
반복자가 필요할 때의 관용구

정리

지역변수의 범위를 줄이는 가장 강력한 기법은 가장 처음 쓰일 때 선언하기다.
메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다.

주저리

메서드를 작게 유지하는건 별개로 보더라도 요즘 변수를 착각해 사용하는 일은 많이 줄어들었다고 생각한다.
IDE가 발전해서 사용하지 않는 변수나 이상하게 사용되는 변수는 대다수 경고를 주기때문

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

컬렉션 순회하기

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) { Element e = i.next(); ... }
Java
복사
더 나은 방법이 있다.
for (int i = 0; i < a.length; i++) { ... }
Java
복사
더 나은 방법이 있다.
반복자와 인덱스 변수를 코드를 지저분하게하고 오류가 생길 가능성이 높아진다.
1회 반복에서 반복자는 세 번 등장하고 인덱스는 네 번 등장하고 있다.
혹시라도 잘못된 변수를 사용했을 때 컴파일러가 잡아주지 못할 수도 있다. 또한 컬렉션이나 배열이냐에 따라 코드 형태도 달라지고 있다.

향상된 for 문

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

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

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

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

라이브러리의 장점

표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 우리보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
표준 라이브러리를 사용하면 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
따로 노력하지 않아도 성능이 지속해서 개선된다.
기능이 점점 많아진다.
여러분이 작성한 코드가 많은 사람에게 익숙한 코드가 된다.
다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 된다.

장점이 많은데 왜 사람들은 직접 구현할까?

아마도 라이브러리에 그런 기능이 있는지 모르기 때문일 것
메이저 릴리즈마다 주목할 만한 수많은 기능이 라이브러리에 추가된다.
라이브러리가 방대해 모든 API 문서를 공부하기는 힘들다.
하지만 자바 프로그래머라며 적어도 java.util, java.lang, java.io와 그 하위 패키지들에는 익숙해져야 한다.

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

floatdouble은 부동소수점 타입으로 넓은 범위의 수를 빠르고 정밀하게 근사치로 계산하도록 설계되었다.
floatdouble은 특히 금융 관련 계산과는 맞지 않는다.

어설픈 코드

System.out.println(1.03 - 0.42);
Java
복사
이 출력의 결과는 0.61000000000001이다.
잘못된 결괏값을 출력하기 전에 반올림하면 해결되리라 생각할지도 모르지만, 반올림 해도 틀린 답이 나올 수 있다.
정확한 값을 원한다면 BigDecimal, int, long을 이용하자

BigDecimal의 단점

기본타입보다 쓰기고 훨씬 불편하고 느리다.
단발성 계산이라면 느리다는 무시할 수 있지만, 쓰기 불편하다는 점은 못내 아쉬울 것이다.
속도가 중요한 경우 intlong을 이용해 소수점을 직접 관리하는 방법도 있다.

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

자바는 크게 기본타입(int, long, char 등)과 참조타입(모든 Object)으로 나눌 수 있다.
기본타입에 대응하는 참조타입이 하나씩 있고, 이를 박싱된 타입이라고 한다.

박싱 타입과 기본 타입의 차이점

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

박싱 타입 사용시의 주의사항

박싱 타입은 참조타입이기 때문에 == 연산자를 사용하면 의도한 동작과 다르게 나타난다.
박싱 타입을 초기화 하지 않았을 경우 기본 값이 null로 초기화 되기 언박싱시 때문에 NPE이 일어난다.
오토 박싱/언박싱으로 인해서 성능이 저하될 수 있다.

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

문자열은 텍스트를 표현하도록 설계되었지만 편의성 때문에 의도하지 않은 용도로 쓰이는 경향이 있다.

문자열 사용이 적합하지 않은 경우

문자열은 다른 값 타입을 대신하기에 적합하지 않다.
문자열은 열거 타입을 대신하기에 적합하지 않다.
문자열은 혼합 타입을 대신하기에 적합하지 않다.
특정 문자열로 구분시에 파싱을 해야하는데 느리고, 귀찮고, 오류 가능성이 생길 가능성이 높다.
문자열은 권한을 표현하기에 적합하지 않다.
악의적인 사용자가 의도적으로 같은 키값을 위조할 수 있다.

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

문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단이다.
그러나 본격적으로 사용할 경우에는 성능 저하를 감내하기 어렵다.
문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.
문자열은 불변이라서 두 문자열을 연결할 경우 양쪽 내용을 모두 복사해야 한다.

성능을 챙기고 싶다면?

public String statement() { String result = ""; for (int i = 0; i < numItems(); i++) result += lineForItem(i); // 문자열 연결 return result; }
Java
복사
느리다!
public String statement() { StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH); for ( int i = 0; i < numItems(); i++) b.append(lineForItem(i)); return b.toString(); }
Java
복사
빠르다!
성능을 포기하고 싶지 않다면 String 대신 StringBuilder를 사용하자.

여담

어디서 듣기로는 + 연산을 사용해도 컴파일시엔 StringBuilder로 바꿔준다고 본거같은데 추후 찾아봐야겠다.

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

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
객체의 실제 클래스를 사용해야 할 상황은 생성자로 생성할 때 뿐이다.
적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 (상위의) 클래스 타입으로 사용하자.

인터페이스를 타입으로 써야하는 이유

// 좋은 예. 인터페이스를 타입으로 사용했다. Set<Son> sonSet = new LinkedHashSet<>(); // 구현 클래스 변경 Set<Son> sonSet = new HashSet<>(); // 나쁜 예. 클래스를 타입으로 사용했다! LinkedHashSet<Son> sonSet = new LinkedHashSet<Son>();
Java
복사
인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 휠씬 유연해질 것이다.
나중에 구현 클래스를 교체하고자 한다면 그저 새 클래스의 생성자를 호출해주기만 하면 된다.
다른 코드는 전혀 손대지 않고 구현체 변경이 가능하다.

주의점

원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운클래스도 반드시 같은 기능을 제공해야 한다.
예시로, LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에서 이를 HashSet으로 바꾸면 문제가 될 수 있다.
HashSet은 반복자의 순회 순서를 보장하지 않기 때문이다.

예외사항

적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
String, BigInteger과 같은 값 클래스가 그렇다.
클래스 기반으로 작성된 프레임워크가 제공하는 객체들일 경우
이런 경우에도 특정 구현 클래스보다는 기반 클래스를 사용하여 참조하는게 좋다.
OutputStream 등 java.io 패키지의 여러 클래스들이 이 부류에 속한다.
인터페이스에는 없는 특별한 메서드를 제공하는 클래스일 경우
PriorityQueue 클래스는 Queue 인터페이스에 없는 comparator 메서드를 제공한다.
클래스 타입을 직접 사용하는 경우 이런 추가 메서드를 꼭 사용해야하는 경우로 최소화해야한다.

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

리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.
Class 객체가 주어지면 그 클래스의 생성자, 메서드 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있다.
이어서 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다.
Constructor, Method, Field 인스턴스들을 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다.
이 인스턴스들을 통해 해당 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나, 필드에 접근할 수 있다.

리플렉션의 단점

컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
리플렉션 기능을 써서 존재하지 않는/접근할 수 없는 메서드를 호출하려 시도하면 런타임 오류가 발생한다.
리플렉션을 이용하면 코드가 지저분하고 장황해진다.
지루하고, 읽기 어렵다.
성능이 떨어진다.
리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
런타임에 6개나 되는 예외를 던질 수 있다.
리플렉션 없이 생성했다면 컴파일타임에 잡어낼 수 있는 예외들이다.
클래스 이름으로 인스턴스를 생성하기 위해 코드가 25줄이나 된다.
리플렉션이 아니라면 생성자 호출 한줄로 끝날 수 있었다.

리플렉션의 장점

런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다.

리플렉션을 사용한다면?

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점을 취할 수 있다.
리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.

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

네이티브 인터페이스는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다.
네이티브 메서드란 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.

네이티브 메서드의 쓰임새

레지스트리와 같은 플랫폼 특화 기능을 사용
네이티브 코드로 작성된 기존 라이브러리를 사용한다.
성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.
성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 권장하지 않는다.

네이티브 메서드의 단점

네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 안전하지 않다.
네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮다.
디버깅도 더 어렵다.
주의하지 않으면 속도가 오히려 더 느려질 수 있다.
가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
자바 코드와 네이티브 코드 사이의 접착 코드를 작성해야하는데, 이는 가독성이 떨어지고 귀찮은 작업이다.

67. 최적화는 신중히 하라

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