Java/Effective Java

[Effective Java] Item57~67. 일반적인 프로그래밍 원칙

jiniya22 2024. 5. 19. 21:28
반응형

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

지역변수를 사용하기 직전에, 지역변수를 선언하는 것이 좋으며
가능한 선언과 동시에 초기화하는 것이 좋습니다.

while문 같이, 변수를 외부에 선언하는 문법보다는
for문을 이용하여, for문 블럭 내에서 지역변수를 선언하고, 그 블럭 내에서만 해당 지역변수를 사용하게 만드는 것이 bug를 유발하지 않기에 더 좋은 코드입니다.

메서드는 작게 유지하고, 각 메서드당 하나의 기능에 집중하게 만들자


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

전통적으로 for문을 사용하는 아래와 같은 방식이 있습니다.

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
  Element e = i.next();
  ...
}
for (int i = 0; i < a.length; i++) {
  ...
}

이러한 for문은 while문보다는 낫지만, for문 내에서 정의한 지역변수 요소들을 사용해야할 것이 아니라면 아래와 같은 이유의 단점들이 존재합니다.

  1. for 선언에 쓰이는 요소들이 많아지는 만큼, 그에 의한 오류가 발생시킬 수 있는 가능성도 높아집니다.
  2. for를 적용할 대상이 달라짐에 따라 작성법이 달라지는 불편함이 존재합니다. (컬렉션, 배열)

인덱스나 반복자를 사용하지 않아도 된다면, 향상된 for문을 사용하는 것이 가독성 측면에서도 좋고,
for 적용대상이 컬렉션이든 배열이든 관계없이 동일한 문법 구조로 작성하는 것이 가능합니다.

또, 전통적인 for문과 비교했을 때 성능적인 차이점도 없습니다.

for(Element e: elements) {
  ...
}

다만, 아래의 상황에서는 향상된 for문을 사용할 수 없기 때문에 전통적인 for문을 사용해야 합니다.

  1. 컬렉션을 순회하면서 선택된 원소를 제거해야하는 경우.
    • Java8 이상부터 지원하는 Colleciton 클래스의 removeIf 메서드로 컬렉션을 명시적으로 순회하는 것을 피할 수 있습니다.
  2. 리스트나 배열을 순회하면서 그 원소의 값의 일부나 전체를 교체해야하는 경우
    • 이 경우에는 리스트의 반복자나 배열의 인덱스를 사용해야합니다.
  3. 여러 컬렉션을 병렬로 순회해야하는 경우

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

float와 double은 과학/공학 계산용으로 설계된 것으로, 이진 부동소수점 연산에 사용됩니다.
넓은 범위의 수를 빠르고 정밀한 근사치로 계산하기 때문에, 정확한 계산 결과가 필요할 때 사용하면 안됩니다.

특히, 금융 관련 계산에서는 사용하지 않아야 합니다.

float/double은 0.1 이나 음의 거듭제곱(10110^{-1}, 10210^{-2})을 표현할 수 없습니다.

예를들어, 1.01 - 0.33 = 0.68 이 나와야하지만
java에서는 0.6799999999999999를 출력합니다.

System.out.println(1.01 - 0.33);
0.6799999999999999

아래의 계산 결과도 0.2가 아닌 근사치가 출력됩니다

System.out.println(1.00 - 8 * 0.10);
0.19999999999999996

double 자료형을 정확한 계산이 필요한 금융 계산에 적용할 경우, 잘못된 결과가 발생될 수 있습니다.

double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
3 items bought.
Change: $0.3999999999999999

이 경우, BigDecimal로 교체할 경우 정확한 결과를 도출할 수 있습니다.
다만, BigDecimal은 기본 타입에 비해 속도가 느리고 사용 방법이 직관적이지 않다는 단점이 있습니다

final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
    funds = funds.subtract(price);
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
4 items bought.
Change: $0.00

만약 계산법을 십진수로 표현이 가능하면서, 계산에 필요한 수가 너무 크지 않다면 int 나 long 타입으로 변경하여 계산하는 것도 방법입니다.
숫자를 9자리 십진수로 표현할 수 있다면 int 자료형을, 18자리 십진수로 표현할 수 있다면 long 타입을 사용하면 됩니다.

만일 18자리수롤 넘어간다면 BigDecimal을 사용해야 합니다.

위의 예제 코드에서는 달러 체계에서 센트 단위로 변경한 후, 자료형을 int로 변경하여 계산을 할 수 있습니다.

int funds = 100;
int itemsBought = 0;
for (int price = 10; funds >= price; price += 10) {
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: " + funds + " cents");
4 items bought.
Change: 0 cents

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

java에서 데이터 타입은 primitive(기본) 타입 (int, long, float, double) 과 reference 타입(String, Map, List, Double, Integer) 이 있습니다.

primitive 타입을 List 나 Map에서 활용하기 위해 사용되는 것이 boxing 된 기본타입들이 있는데, Integer, Long, Float, Double이 이에 해당됩니다.

java에서는 auto boxing, auto unboxing을 제공하고 있어 기본타입과 박싱된 기본 타입을 구분하지 않고 혼용해서 사용할 수 있지만, 두 타입에는 차이점이 있습니다.
따라서, 두 타입의 차이점을 명확히 알고 타입을 적절하게 사용하는 것이 좋습니다.

61.1. 차이점

  • 기본 타입은 값만 가지고 있으나, 박싱된 기본타입은 값과 식별성(identity) 속성을 가지고 있습니다
    • 식별성은 값이 동일하더라도 서로 다른 값임을 판단하는 속성입니다. 박싱된 기본타입도 reference 타입의 일종이므로 값으로 동일성을 판단하는 것이 아니라 주소값으로 동일값임으로 판단합니다.
  • 기본타입은 유효한 값만을 담을 수 있지만, 박싱된 기본타입의 기본값은 유효하지 않은 값도 담을 수 있습니다.
    • 박싱된 기본값은 null 도 담을 수 있습니다.
  • 기본타입이 시간과 메모리 사용면에서 더 효율적입니다.

61.2. 박싱된 기본타입 사용시 주의점

기본타입과 박싱된 기본타입을 혼용해서 사용할 경우, 자동으로 unboxing 되기 때문에, null이 들어올 수 있는 상황에 대한 예외처리를 해야합니다.

예를 들어 아래와 같은 코드가 있다고 할때,
i는 초기화가 되어있지 않기 때문에 null이 들어있는 상태입니다.
null값은 10과 같지 않기 때문에 출력문이 출력될 것이라고 생각할 수 있으나

이 값을 기본타입 10과 비교하려 할때, 자동 unboxing 되면서 null값을 기본타입으로 변환하려 시도하게 되고
이 때, NullPointerException이 발생되게 됩니다.

static Integer i;

public static void main(String[] args) {
	if(i != 10)
		System.out.println("이게 출력될까요?");
}

비슷한 이유로, 아래의 코드도 오류가 발생될 수 있습니다.

정수형 수를 오름차순으로 정렬하고자 아래의 크드를 만들었으나
얼핏보기에는 코드상으로 이상이 없어보이지만, 만일 비교대상이 null이 들어왔을 경우에 대한 예외처리가 빠져있어, 적용 대상에 null이 들어있을 경우 NullPointerException을 발생시킵니다.

Comparator<Integer> naturalOrder = (i, j) -> i < j ? -1 : (i == j ? 0 : 1);

또, 박싱된 기본타입을 사용하는 것은 객체를 생성하는 것이기에 이에의한 연산적 측면에서의 오류를 범할 수 있습니다.

참고로, Java9 버전부터는 박싱된 기본타입을 생성자를 통해 생성하는 것을 막고 있기 때문에
동일한 수를 담고 있는 두 숫자 객체의 비교에 대한 예상치 못한 부작용은 해소되었습니다.

@Deprecated(since="9", forRemoval = true)
public Integer(int value) {
	this.value = value;
}

63. 문자열 합성이 빈번할 경우 StringBuilder를 사용하자

여러개의 문자열을 합치는 것은 + 연산자를 사용하는 것으로도 손쉽게 가능합니다.

String s1 = "apple"
String s2 = "tree"
String result = s1 + s2;

다만, 문자열을 + 연산자로 잇는 행위는 성능이 좋지 않습니다.
n개의 문자열을 + 연산자로 연결하는데에 드는 시간복잡도는 O(n2)O(n^2) 입니다.

문자열은 불변객체 일종이기 때문에 잇는 연산이 발생할 때마다 매번 새로운 객체에 복사하는 행위를 하기 때문입니다.

그렇기 때문에, 이어야하는 항목이 많을 경우라면 StringBuilder 를 사용하는 것이 좋습니다.

static String ex01(List<String> list) {
	StringBuilder builder = new StringBuilder();
	for (String s : list) {
		builder.append(s);
	}
	return builder.toString();
}

67. 최적화는 신중히 하라

성능때문에 견고한 구조를 희생해서는 안됩니다.
빠른 프로그램보다는 좋은 프로그램을 작성하는 것이 더 좋습니다.

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

좋은 프로그램은 정보 은닉 원칙을 따르고, 그렇기 떄문에 개별 구성요소를 각각 독립적으로 설계할 수 있게 됩니다.
따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있게 합니다.

속도적 최적화 문제는 추후 얼마든지 해결할 수 있는 문제이지만
아키텍쳐적 결함은 시스템 전체를 재작성하지 않고서는 해결하기 어려운 상황이 많습니다.
성능을 제한하는 설계는 피해야합니다.

외부 시스템과 소통하는 방식은 추후 변경하기가 어렵습니다.
따라서 초반에 설계하는 단계에서 이 부분을 유의하여 설계해야합니다.

728x90
반응형