코드의 중복을 없애라거나 적절한 API를 쓴다거나 등의 바람직한 코드를 위한 지침에는 항상 다음과 같은 이유들이 붙어다닙니다.
-
코드의 가독성이 좋아진다.
-
유지보수가 편해진다.
-
오류의 발생 가능성이 줄어든다.
지역변수의 유효범위를 최소화하라는 지침에도 위의 이유들이 역시나 인용되고 있지요. (참고자료1 Effective Java, 참고자료 3 Code completed 2nd Edition). 변수의 선언과 초기화, 사용 사이에 있는 코드가 많을 수록 취약성 있는 코드가 들어갈 여지는 커집니다. 이른 시점부터 쓰이지도 않는 객체가 초기화되어서 메모리를 낭비하고 있을 가능성도 있고, 선언되고 한번도 쓰이지 않는 변수들도 눈에 잘 들어오지 않게 됩니다. (뭐 쓰이지 않는 변수는 Eclipse의 warning으로 잡아낼수 있기는 합니다만)
지역변수를 쓰기 바로 전에 초기화하고, 선언과 함께 초기화 하는 좋습니다. 즉 가능한 변수가 처음 사용되는 곳의 가장 근접한 위치에서 선언되고 초기화되어야 한다는 말이죠. 변수의 수명을 가능한 짧게 유지하라는 말로도 표현됩니다. 하지만 많은 개발자들은 메소드의 처음에서 C언어처럼 쭉 변수를 선언하고 시작하고 있습니다.
반복문에서는 while보다는 for를 쓰는 것이 변수의 범위관리에 유리합니다.
보통 iterator와 while이 아래와 같이 많이 쓰이고 있습니다.
Iterator i = c.iterator();
while (i.hasNext())\{
doSomething(i.next());
}
이 것을 for문으로 쓴다면 다음과 같습니다.
for (Iterator i = c.iterator() ; i.hasNext(); ) {
doSomething(i.next());
}
작은 차이지만 Iterator i는 for block을 벗어나는 순간 잊어버려도 되는 것이니 block 밖에서 개발자의 머리는 조금이나마 가벼워 질 수 있습니다. 이것은 캡슐화의 원칙인 class의 맴버 중 밖에서 볼 필요 없는 것들은 private으로 선언해야 하는 이유와 일맥상통합니다.
1.루프밖에서 list에서 꺼내서 담을 변수 선언
static int countOfIncluded(List list, String str){
int count = 0;
String element;
for (int i=0,n=list.size();i<n;i++)\{
_*element = (String) list.get(i);*_
if (element.indexOf(str)!= -1 ) count++;
}
return count;
}
2.루프안에서 list에서 꺼내서 담을 변수 선언
static int countOfIncluded(List list, String str){
int count = 0;
for (int i=0,n=list.size();i<n;i++){
String element = (String) list.get(i);
if (element.indexOf(str)!= -1 ) count++;
}
return count;
}
많은 분들이 1번과 같이 코드를 작성하고 있고, 1번이 성능이 좋다는 '믿음’을 가지고 계실 것입니다. 실제로 한 번 테스트 해볼까요? 크기가 10000개인 리스트를 생성해서 100번씩 반복해서 실행시간을 찍어보는 코드를 만들어보았습니다. 1번 방법은 ListReader1.java, 2번 방법은 ListReader2.java, 시간을 찍어보는 코드는 LoopTester.java 로 첨부하였습니다. 이런 비교 시에는 실행순서에 따라서도 실행시간이 영향을 받으므로 1,2,2,2,1,1,2의 순서로 몇번씩 사이에 걸린 시간을 밀리세컨드로 출력하게했습니다. 실행결과는 다음과 같았습니다.
test1:밖에 선언 2243
test2:안에 선언 2153
test2:안에 선언 2184
test1:밖에 선언 2253
test1:밖에 선언 2213
test2:안에 선언 2123
거의 차이가 없거나 오히려 안에 선언한 쪽이 미묘하게 빠르기도 합니다.
이번에는 javap -c
를 이용해서 컴파일된 byte 코드를 분석해 보겠습니다.
> javap -c ListReader1
Compiled from "ListReader1.java"
public class looptest.ListReader1 extends java.lang.Object{
public looptest.ListReader1();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
static int countOfIncluded(java.util.List,java.lang.String);
Code:
0: iconst_0
1: istore_2
2: iconst_0
3: istore 4
5: aload_0
6: invokeinterface #2, 1; //InterfaceMethod java/util/List.size:()I
11: istore 5
13: iload 4
15: iload 5
17: if_icmpge 50
20: aload_0
21: iload 4
23: invokeinterface #3, 2; //InterfaceMethod java/util/List.get:(I)Ljava/la
ng/Object;
28: checkcast #4; //class String
31: astore_3
32: aload_3
33: aload_1
34: invokevirtual #5; //Method java/lang/String.indexOf:(Ljava/lang/String
;)I
37: iconst_m1
38: if_icmpeq 44
41: iinc 2, 1
44: iinc 4, 1
47: goto 13
50: iload_2
51: ireturn
}
javap -c ListReader2
Compiled from "ListReader2.java"
public class looptest.ListReader2 extends java.lang.Object{
public looptest.ListReader2();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
static int countOfIncluded(java.util.List,java.lang.String);
Code:
0: iconst_0
1: istore_2
2: iconst_0
3: istore_3
4: aload_0
5: invokeinterface #2, 1; //InterfaceMethod java/util/List.size:()I
10: istore 4
12: iload_3
13: iload 4
15: if_icmpge 49
18: aload_0
19: iload_3
20: invokeinterface #3, 2; //InterfaceMethod java/util/List.get:(I)Ljava/la
ng/Object;
25: checkcast #4; //class String
28: astore 5
30: aload 5
32: aload_1
33: invokevirtual #5; //Method java/lang/String.indexOf:(Ljava/lang/String
;)I
36: iconst_m1
37: if_icmpeq 43
40: iinc 2, 1
43: iinc 3, 1
46: goto 12
49: iload_2
50: ireturn
}
노란 줄이 많은 것은 라인수가 1라인 차이가 나고, local variable이 저장되는 공간의 index번호가 달라서입니다. 내용을 보시면 거의 똑같이 실행되고 있는 것을 알 수 있으실 것입니다. 루프가 도는 goto문장을 봐도 (왼쪽의 47라인과 오른쪽의 46라인) 같은 곳으로 (13라인과 12라인)으로 이동을 하기 때문에 특별히 오른쪽 예제2의 경우가 더 일을 하는 것은 없습니다. 다만 3번째 라인 istore n 이였던 것이 istore_n 으로 바뀌는 등 언더바(_)가 들어간 부분이 있습니다.
각각의 명령어의 의미는 다음 링크를 확인해 보시면 나와있습니다.
내용을 보면 istore과 istore_n은 전자가 암시적이라는 것만 빼고는 같다고 나옵니다. (Each of the istore_<n> instructions is the same as istore with an index of <n>, except that the operand <n> is implicit. ) iload의 경우도 마찬가고요. 어떤 차이가 있을까 해서 검색해 봤더니 아래와 내용을 발견했습니다.
'istore_<n>' is functionally equivalent to 'istore <n>', although it is typically more efficient and also takes fewer bytes in the bytecode
istore_n 쪽이 오히려 효율적이라는 말이 나와 있습니다. 만약 iload_n도 마찬가지라면 goto가 찾아가는 라인에서는 iload_n이 있는 예제2가 더 효율적인 코드일 수도 있다는 것입니다. 어쨓든 이런 작은 차이를 접어둔다면 코드가 하는 일의 절차는 차이가 없습니다.
결국 "루프 안에서 반복되는 변수 선언을 밖으로 빼는 것은 성능상에 아무런 이점이 없고 소스에서 변수의 유효범위만 늘어나게 한다. " 는 것입니다.
여기서 이런 말을 하실 분이 계실 것 같습니다.
"원래는 변수를 loop안에 생성하는 루프가 돌 때마다 String 객체의 참조를 저장하기 위한 공간이 따로 할당되는 것인데 위의 경우는 JVM에서 최적화를 해 준 것 아니냐? JVM에 따라서 이런 최적화가 안 되는 버전도 있을 수가 있는데 개발자는 어떤 JVM에서도 한번만 선언이 되도록 루프 밖에 변수를 선언해야 하지 않겠냐? "
The sizes of the local variable array and the operand stack are determined at compile time and are supplied along with the code for the method associated with the frame .
풀이하면 메소드 안의 local variable들의 값들은 고정된 크기의 배열에 저장되고 그 배열의 크기는 compile시에 결정된다는 내용이 있습니다. 즉 한 메소드 안에서 사용할 local variable이 저장될 공간의 갯수는 이미 compile 시에 정해져 있는 것이지 동적으로 변하는 것이 아니라는 말입니다. 만약 위의 소스에서 list의 갯수에 따라서 local variable의 값이 저장되는 공간(객체가 저장되는 공간을 가리키는 말이 아닙니다.)이 달라진다면 위의 설명과 모순되는 일입니다.
비슷한 설명과 논쟁이 인터넷의 여러 곳에서 이미 벌어졌으니 아래의 링크를 읽어보셔도 재밌을 것입니다.
이런 오해들은 전에 Java의 호출은 pass by value 에서 말했던 객체선언에 대한 오해와 어느 정도 관련이 있다고 봅니다. 객체를 생성이나 사용을 안 하더라도 선언 자체만으로 저장을 위한 큰 공간이 할당된다고 믿는 경우가 많은 것 같습니다.
전에 이런 코드를 본적이 있습니다. 클래스의 멤버 변수로 임시 객체를 생성해 놓고 그것을 여러개의 메소드에서 같이 쓰는 코드였었습니다.
Class MyClass {
String temp;
void work(List list){
for (int i=0; i = list.size(); i++)\{
temp = (String) list.get(i);
//기타...
}
}
void play(List list)\{
for (int i=0; i = list.size(); i++)\{
temp = (String) list.get(i);
//기타...
}
}
}
이런 코드를 짰던 사람도 경력도 어느 정도 되고, 실력도 있어보이는 사람이였죠. 아마 이 개발자는 이런 구조가 temp 객체가 저장될 공간을 상당히 아꼈다는 뿌듯함을 가지고 있었을 지도 모릅니다. 그러나 객체는 Heap 메모리에 있는 것이기에 지역변수가 선언이 하나 덜 되었다고 해서 객체가 줄어든 것은 아닙니다. 그리고 여기서는 list에서 받아오지 새로 생성되는 객체는 없습니다. 더군다나 설계상 의미로도 temp는 Class의 멤버로써 아무런 의미가 없습니다. 성능측면에서 봐도 클래스 멤버 변수는 지역변수에 비해서 더 비용이 큽니다. (참고자료 2: 자바 퍼포먼스 튜닝)
한편으로는 변수의 범위를 줄인다고 해서 반복적으로 Loop 안에서 실행해줄 필요가 없는 작업까지 loop안으로 끌고 들어가서는 안되겠죠. 다음과 같은 코드를 본적이 있습니다.
for (int i=0, n=userList.size(); i<n;i++)\{
UserBiz biz = (UserBiz) lookup(UserBiz.ROLE);#*_ //비지니스 컴포넌트 객체를 얻어온다.
User user = (User)userList.get(i);
biz.add(user;
}
위의 코드에서 비지니스 컴포넌트를 얻어오는 부분은 반복적으로 수행될 필요가 없으므로 loop 밖으로 나가는 것이 맞습니다. 위의 biz 객체를 loop안에서만 쓴다고 하여도 위와 같이 해당 코드가 안으로 들어가 있다면 한번만 얻어오는 되는 객체를 얻어오는 작업이 계속 반복될 것 입니다. 사실 위의 경우는 비지니스 객체에서 List를 받아서 처리하는 메소드를 하나 더 만드는 것이 나을 것 같네요. 아키텍쳐에 따라 다르지만 비지니스 객체의 호출은 비싼 작업일 경우가 많고 그런 호출의 횟수는 최대한 줄일수록 좋겠죠.
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email