for-each 문은 성가신 코드를 제거하고 반복자나 첨자 변수를 오나전히 제거해서 오류 가능성을 없앤다. 


 보기와 같이 for문 사용에 비해 for-each문을 사용하는 것이 훨씬 간결해 보이고, 첨자변수에 대해 문제를 일으킬 걱정이 없다. for-each문의 장점은 여러 컬렉션이 중첩되는 순환문을 만들어야 할 때 더 빛난다. 


for문으로 작성한 위의 코드를 for-each문을 사용하면 아래와 같이 간결해진다.


요약

for-each문은 전통적인 for문에 비해 명료하고 버그 발생 가능성도 적으며, 성능도 for문에 뒤지지 않는다. 그러니 가능하다면 항상 사용해야 한다. 그러나 아래와 같은 경우에는 for-each문을 사용할 수 없다.
  • 필터링 - 컬렉션을 순회하다가 특정한 우너소를 삭제할 필요가 있을 때
  • 변환 - 리스트나 배열을 순회하다가 그 원소 가운데 일부 또는 전체를 변경해야 할 때
  • 병렬 순회 - 여러 컬렉션을 병렬적으로 순회해야 하고, 모든 반복자나 첨자 변수가 발맞춰 나아가도록 구현해야 할 때 


지역 변수의 유효범위를 최소화하는 가장 강력한 기법은, 처음으로 사용하는 곳에서 선언하는 것이다. 사용하기 전에 선언하면 프로그램의 의도를 알고자 소스 코드를 읽는 사람만 혼란스럽게 할 뿐이다. 또한 거의 모든 지역 변수 선언에는 초기값이 포함되어야 한다. 

 또한 순한문을 잘 쓰면 변수의 유효범위를 최소화할 수 있다. while문보다는 for문을 사용하는 것이 더 좋다. 


 위의 while문을 사용했을 때는 버그가 있다. 복사-붙혀넣기로 넣은 코드가 오류를 발생시키고 있는데,  두번째 while문의 i.hasNext() 부분이 잘못되었다. i의 유효범위안에 있기 때문에 위의 코드는 정상적으로 컴파일 되겠지만 마치 c2가 비어있는 것처럼 동작하고 말것이다. 하지만 for문을 사용하면 위와 같은 문제가 발생하지 않는다.


또 한가지 기법은 메서드의 크기를 줄이고 특정한 기능에 집중하라는 것이다. 두 가지 서로 다른 기능을 한 메서드에서 처리를 하면 한 가지 기능을 수행하는데 필요한 지역 변수의 유효범위가 다른 기능까지 확장되면서 문제가 생길 가능성이 높다. 

추후에 다시 정리

 위와같이 빈 배열이나 컬렉션을 반환하는 대신 null을 반환하는 메서드를 사용하면 클라이언트쪽에서 null에 대한 처리를 아래와 같이 해줘야 한다.

만약 클라이언트에서 null에 대한 처리를 빼먹었다면 오류를 발생시킬 것이다. 그렇다면 빈 배열을 반환하는 것이 null을 리턴하는 것보다 좋은 이유는 무엇일까? "배열할당 비용을 절약할 수 있다"라고 생각할 수 있다.

하지만 해당 메서드가 성능 저하의 주범이라는 것이 밝혀지지 않는 한 그런 수준까지 걱정하는 것은 옳지 않다. 또한 길이가 0인 배열은 변경이 불가능하므로 아무 제약없이 재사용할 수 있다.


즉 위의 getCheeses메서드는 아래와 같이 변경되어야 한다.



 마찬가지로 컬렉션을 반환하는 메서드도 빈 컬렉션을 반환해야 할 때마다 동일한 변경 불가능 빈 컬렉션 객체를 반환하도록 구현할 수 있다.


요약

null대신에 빈 배열이나 빈 컬렉션을 반환하라는 것이다. 


이 메서드는 지정된 자료형의 인자를 0개 이상 받을 수 있다. 동작 원리는 이렇다. 우선, 클라이언트에서 전달한 인자 수에 맞는 배열이 자동 생성되고, 모든 인자가 해당 배열에 대입된다. 그리고 마지막으로 해당 배열이 메서드에 인자로 전달된다. 


 위와 같은 메소드를 이용해 int 인자들을 받아서 합계를 구하는 메소드를 간단하게 구현할 수 있다. 그러나 때로는 하나 이상의 인자를 받을 수 있는 메서드를 구현하고 싶을 때가 있는데, 이런 메소드의 경우 인자를 하나이상의 값 인자와 , 배열인자 총 2개의 인자를 가진 메서드를 정의하면 된다. 
 하지만 다른 메서드가 위와 같은 형태( 인자1 , 배열 )로 마지막 인자가 배열을 받는다고 해서 모두다 가변인자로 바꿀 생각을 하면 안된다. varargs는 정말로 임의 개수의 인자를 처리할 수 있는 메서드를 만들어야 할 때만 사용해야 한다.

 성능이 중요한 곳에서도 varargs의 사용은 조심해야 한다. varargs 메서드를 호출할 때마다 배열이 만들어지고 초기화되기 때문이다.

요약

varargs 메서드는 인자 개수가 가변적인 메서드를 정의할 때 편리하지만, 남용되면 곤란하다. 부적절하게 사용되면 혼란스러운 결과를 초래할 수 있다.  


 위의 클래스는 컬렉션을 종류별로(집합,리스트,아니면 다른 종류의 컬렉션) 분류하는 것이다. 이 프로그램은 Set,List,Unkown Collection을 순서대로 출력하지 않을까 생각하겠지만 실제로는 "Unknown Collection"을 세 번 출력할 뿐이다. classify 메서드가 오버로딩되어 있으며, 오버로딩된 메서드 가운데 어떤 것이 호출될지는 컴파일 시점에 결정되기 때문이다.  위의 반복문 컴파일 시점 자료형은 전부 Collection<?> 으로 동일하다. 따라서 전부 "Unknown Collection"이 출력되는 것이다.


 오버로딩된 메서드는 정적(컴파일 시점)으로 선택되지만, 재정의된 메서드는 동적(실행 시점)으로 선택되기 때문이다.  즉 재정의 메서드 가운데 하나를 선택할 때는 객체의 컴파일 시점 자료형은 아무런 영향을 주지 못하며, 오버로딩에서는 반대로 실행시점 자료형이 아무 영향도 주지 못한다.. 실행될 메서드는 컴파일 시에, 인자의 컴파일 시점 자료형만을 근거로 결정된다. 

 이러한 오버로딩의 문제점을 해결하기 위해서는 아래와 같이 작성하는 것이 최선이다. 오버로딩 된 세 classify 메서드를 하나로 합치고, 그 안에서 instanceof 연산자를 사용해 자료형을 검사하는 것이다.


 
 오버로딩을 사용할 떄는 혼랍스럽지 않게 사용할 수 있도록 주의해야 한다. 처음 예제처럼 결과값이 예측하기 힘든 상황처럼 말이다. 혼란을 피하는 안전하고 보수적인 전략은, 같은 수의 인자를 갖는 두 개의 오버로딩 메서드를 API에 포함시키지 않는 것이다. 
 이러한 중복을 피하기 위해서는 작명 패턴(writeBoolean(boolean) , writeInt(int) ...)과 같은 방법을 사용할 수 있는데, 오버로딩에 비해 각 메서드에 정의되는 read메서드를 정의할 수 있다. 

 같은 수의 인자를 받는 오버로딩 메서드가 많더라도, 어떤 오버로딩 메서드가 주어진 인자 집합을 처리할 것인지가 분명히 결정된다면 혼란스럽지 않을 것이다. 이렇게 확실히 결정된다는 것은 두 자료형을 서로 형변환 할 수 없다는 것이다. 이 조건이 충족되면 주어진 인자 집합에 오버로딩을 적용 했을 때 인자의 실행시점 자료형에 따라 오버로딩 메서드가 결정될 수 있다.

요약

메서드를 오버로딩 할 수 있다고 해서 반드시 그래야 하는 것은 아니다. 인자 개수가 같은 오버로딩 메서드를 추가하는 것은 일반적으로 피해야 한다. 형변환만 추가하면 같은 인자 집합으로 여러 오버로딩 메서드를 호출할 수 있는 상황은 피하는 것이 좋다. 


메서드 이름은 신중하게 고르라.  이해하기 쉬우면서도 같은 패키지 안의 다른 이름들과 일관성이 유지되는 이름을 고르는 것이다.  또한 좀 더 널리 합의된 사항에도 부합하는 이름을 고르는 것. 모르겠다면 자바 라이브러리 API이름들이 어떻게 지어졌는지 참고해 보자.

편의 메서드를 제공하는 데 너무 열 올리지 마라. "모든 메서드는 맡은 일이 명확하고 거기에 충실해야 한다." 클래스에 메서드가 너무 많으면 사용,테스트, 유지보수 등 모든 면에서 활용하기 어렵다. 

인자 리스트를 길게하지 마라. 4개 이하가 되도록 노력하라. 특히나 자료형이 같은 인자를 여러개 나열하는 것은 실수할 가능성이 매우 높으며, 오류없이 실행되기 때문에 위험하다. 
  • 여러 메서드로 나누는 것. 각각의 메서드는 여러 인자의 일부만을 취하며, 메서드가 너무 많이 생성되지 않게 주의하도록 한다.
  • 도움 클래스를 만들어 인자들을 그룹별로 나누는 것이다. 예를들어 카드의 숫자와 모양 등을 인자로 받는 메서드를 만든다고 했을 때, 각각을 인자로 받는 것보다 카드의 숫자와 모양등을 멤버로 가지고 있는 도움클래스를 인자로 전달하여 사용하는 것이 좋다.
  • 빌더패턴을 고쳐서 객체 대신 메서드 호출에 적용하는 것이다. 

인자의 자료형은 클래스보다는 인터페이스가 좋다. 인터페이스 대신에 클래스를 ㅏㅅ용하면 클라이언트는 특정한 구현에 종속되기 때문이다. 

인자의 자료형으로 boolean을 쓰는 것보다는, 우너소가 2개인 enum 자료형을 쓰는 것이 낫다. 그러면 좀 더 읽기 편한 코드가 만들어 진다. 


우리가 만드는 클래스의 클라이언트가 불변식을 망가뜨리기 위해 최선을 다할 것이라는 가정하에, 방어적으로 프로그래밍을 해야한다.  그러니 클라이언트가 이상한 짓을 해도 안정적으로 동작하는 클래스를 만들기 위해 노력해야 한다.


위의 클래스는 일반적으로 생각하는 변경 불가능 클래스를 작성한 것이다. 과연 이 클래스는 변경이 불가능할까? 아래의 코드를 보자.


Date클래스는 변경 가능 클래스라는 것을 이용하면 위와 같이 불변식을 깨뜨릴 수 있다. 따라서 Period 객체의내부를 보호하려면 생성자로 전달되는 변경 가능 객체를 반드시 방어적으로 복사해서 그 복사본을 Period 객체의 컴포넌트로 이용해야 한다. 즉 Period 생성자는 아래와 같이 작성되어야 한다.


위와 같이 생성자를 변경하면 앞서 살펴본 공격법은 먹히지 않는다. 인자의 유효성을 검사하기 전에 방어적 복사본을 만들었다는 것에 유의하자. 유효성 검사는 복사본에 대해서 시행한다. 이러한 방법은 인자를 검사한 직후 복사본이 만들어지기 직전까지의 시간, 즉 취약시간 동안에 다른 스레드가 인자를 변경해버리는 일을 막기 위한 것이다. (TICTOU 공격, time-of-check/time-of-use 공격이라 부른다)


그런데 위와 같은 방법은 생성자 인자를 통한 공격은 막을 수 있지만, 접근자를 통한 공격은 막을 수 없다. 접근자를 호출하여 얻은 객체를 통해 Period 객체 내부를 변경할 수 있기 때문이다.



위와 같은 공격을 막으려면 변경 가능 내부 필드에 대한 방어적 복사본을 반환하도록 접근자를 수정하여야 한다.



 생성자와 접근자를 이렇게 수정하고 나면 Period는 진정한 변경 불가능 클래스가 된다. 


요약

클라이언트로부터 구했거나 클라이언트에게 반환되는 변경 가능 컴포넌트가 있는 경우, 해당 클래스는 그 컴포넌트를 반드시 방어적으로 복사해야 한다. 복사 오버헤드가 너무 크고 클래스 사용자가 그 내부 컴포넌트를 부적절하게 변경하지 않는다는 보장이 잇을 때는, 방어적 복사를 하는 대신 클라이언트 측에서 해당 컴포넌트를 변경해선 안된다. 


+ Recent posts