#loop

자바 반복문 알고 쓰자!

반복문은 어떤 프로그래밍을 하든 기본 중의 기본이다. 우리는 처음 자바 문법 배울 때를 제외하고 반복문에 대해 다시 공부해본 적이 있는가? 있다면 이 글은 넘겨도 좋다. 다만 가장 빈번하게 사용하는 반복문을 너무 가볍게 여겨서는 안 된다. 이 글을 통해 반복문을 조금 더 알고 써보자.


for

for문은 반복문 중에 가장 자주 쓰인다. 나를 포함한 대부분의 초보 개발자들이 가지고 있는 for문을 작성하는 안 좋은 습관이 있다. 아래의 코드에서 한 번 찾아보자.

public void example(List<Integer> numbers) {
    for (int i = 0; i < numbers.size(); i++) {
        ...
    }
}

위 코드는 매번 반복하면서 numbers.size() 메서드를 호출한다. numbers의 size가 10만이라고 가정해보면 10만 번의 필요 없는 size() 메서드를 호출하는 것이다. 아래의 코드처럼 수정한다면 불필요한 반복을 줄일 수 있다.

public void example(List<Integer> numbers) {
    int numbersSize = numbers.size();
    for (int i = 0; i < numbersSize; i++) {
        ...
    }
}

물론 불필요한 size() 메서드를 호출해도 성능에 큰 차이는 없다. 하지만 작은 차이들이 쌓여 1초, 10초가 된다는 점을 기억하자. 간단한 방법으로 조금의 성능 향상이라도 가져올 수 있다면 위에서 한 방식처럼 for문을 작성하는 게 바람직하다고 생각한다.

물론 현실의 코드는 대부분 1000개 이하의 데이터를 순환한다. 극단적인 상황을 고려해서 위처럼 코딩하는 것도 좋지만 for문의 반복횟수가 예상가능하다면 코드의 가독성을 생각해서 for (int i = 0; i < numbers.size(); i++)와 같은 구현도 적절할 수 있다.


for-each(Enhanced for)

JDK 5.0부터 for-each 또는 Enhanced for 라고 불리는 for 루프를 사용할 수 있다.

public void example(List<Integer> numbers) {
    for (int number : numbers) {
        ...
    }
}

위의 코드처럼 for-each문을 사용하면 별도의 형변환이나 get() 메서드를 호출할 필요 없이 순서에 따라서 해당 객체를 사용할 수 있다. for-each문은 for문에 비해 사용하기 편하고 가독성도 좋다. 게다가 Index 관련 Exception들과 위에서 살펴봤던 불필요한 반복도 신경 쓸 필요 없다.

하지만 이 방식은 데이터의 처음부터 끝까지 순서대로 처리해야 할 경우에만 유용하다. 만약 순서를 거꾸로 돌리거나 특정 값부터 데이터를 탐색하는 경우에는 적절하지 않다.

for-each문은 내부적으로 아래와 같이 동작한다.

public static void example(List<String> words) {
    Iterator var1 = words.iterator();

    while(var1.hasNext()) {
        String word = (String)var1.next();
        ...
    }
}

for-each문의 동작 방식에서는 Iterable 인터페이스와 Iterator 인터페이스가 등장한다.

메서드 내부의 첫 번째 라인을 보면 Iterable 인터페이스를 구현한 객체가 iterator() 메서드로 Iterator 인터페이스를 구현한 객체를 반환한다.

그 후 Iterator 인터페이스의 hasNext() 메서드와 next() 메서드로 반복을 실행하는 구조다.

for-each문은 Iterable 인터페이스의 iterator() 메서드가 필요하므로 Iterable 인터페이스를 구현한 객체만이 for-each문을 실행 할 수 있다. Collection 인터페이스도 Iterable 인터페이스를 상속하고 있기 때문에 위의 예제에서 List 인터페이스로 for-each문을 실행할 수 있는 것이다.

for-each문은 내부적으로 메서드를 호출하는 비용이 있기 때문에 for문 보다 느린 것은 당연하다. 물론 차이가 크지는 않다.

for문의 속도를 택할지 for-each문의 가독성, 편리함, 안전함을 택할지는 반복문의 내부 로직과 예상 반복 횟수 그리고 상황에 따라 고민해보면 좋을 것 같다.

for-each문을 실행하려면 Iterable 인터페이스를 구현한 객체여야 한다는 것은 알겠다. 그렇다면 List가 아닌 String Array라면 어떻게 될까? Stirng 객체는 Iterable 인터페이스를 구현하고 있지 않다. 이론대로라면 아래와 같은 코드는 실행될 수 없다.

public void example(String[] words) {
    for (String word : words) {
        ...
    }
}

예상과 다르게 위의 코드는 잘 실행된다. for-each문이 Array type으로 실행된 경우엔 일반 for문으로 적절하게 번역해 주는 것을 확인할 수 있었다. 컴파일한 클래스 파일을 java 파일로 디컴파일한 내부 코드는 다음과 같다.

public void example(String[] words) {
    String[] var1 = words;
    int var2 = words.length;

    for(int var3 = 0; var3 < var2; ++var3) {
        String word = var1[var3];
        System.out.println(word);
    }
}

while

while문은 잘못하면 무한 루프에 빠질 수 있기 때문에 되도록 for문을 사용하기를 권장한다. 아래 코드를 살펴보자.

public void example(List<String> words) {
    boolean flag = true;
    int index = 0;
    while (flag) {
        if (words.get(index++).equals("book")) {
            flag = false;
        }
        ...
    }
}

List에서 “book”이라는 문자열을 발견하면 while문을 빠져나오는 코드이다. 만약 List에 book이라는 단어가 없다면 어떻게 될까? 코드를 작성한 사람 입장에서는 book이라는 단어가 없을 수가 없는 상황이라고 말할 수 있다. 하지만 협업하는 환경에서 누군가 book이 없는 List를 인자로 해당 메서드를 호출할지는 아무도 모르는 일이다.

만에 하나라도 반복문이 계속해서 돌아가는 사태가 발생한다면 해당 애플리케이션은 서버를 재시작하거나 스레드를 강제 종료시킬 때까지 계속 반복문을 수행할 것이다. 그렇게 되면 당연히 서버에 부하도 많이 발생한다.

무조건 while문을 사용하지 말라는 것은 아니다. 상황에 따라 while문이 더 좋을 수도 있다. 다만 문제를 유발할 가능성이 있는 while문인지 아닌지는 항상 점검해 볼 필요가 있다고 말하고 싶다.


forEach 메서드

forEach 메서드는 두 가지가 있다. iterable 인터페이스의 default 메서드 forEach와 java 8부터 추가된 Stream 인터페이스의 forEach 메서드이다.

stream의 forEach메서드는 아래와 같이 사용한다.

public void example(List<String> words) {
    words.stream()
        .forEach(word -> System.out.println(word));
}

일반적인 for문에 비해 람다로 훨씬 간결하게 표현한다. 하지만 단순히 반복을 위해서 Stream forEach를 쓰면 Stream 생성 비용이 낭비된다.

Stream 생성 비용을 낭비하지 않으면서 반복문의 간결함을 유지하고 싶다면 iterable 인터페이스의 default 메서드인 forEach 메서드를 사용하면 된다. iterable 인터페이스는 Collection 인터페이스가 상속하고 있으므로 아래와 같이 Collection 객체들은 전부 사용할 수 있다.

public void example(List<String> words) {
    words.forEach(word -> System.out.println(word));
}

다만 이 forEach 메서드를 사용할 땐 주의해야 하는 점이 있다. 그 내용은 Stream의 foreach 와 for-loop 는 다르다. 이 글을 통해 학습하는 것을 추천한다.


결론

반복문은 어떤 애플리케이션을 개발하더라도 반드시 사용해야 하는 부분이다. 성능에 영향을 미치는 큰 요소인 만큼 불필요한 반복이 있지 않은지, 최선의 반복문인지 항상 고민해야 한다고 생각한다. 성능의 차이가 미미할지라도 어떤 상황에서는 큰 영향을 미칠지 모른다. 이 글의 내용을 참고해서 상황에 따라 어떤 반복문을 사용하면 좋을지 항상 고민해보는 습관을 지니면 좋을 것 같다.


참고 자료