개발

switch와 if else 중 어떤 것을 써야하는가?

aahcbird 2019. 2. 23. 02:03

switch와 if else 중 어떤 것을 써야하는가?


switch구문은 변수를 입력 받아 미리 정해놓은 여러 값들과의 일치여부를 판단하여 switch문 내의 control flow를 결정한다.


if else구문은 boolean의 결과 값을 내놓는 조건문에 따라 true, false에 해당하는 각각 두 개의 흐름으로 갈라진다. 

if else문을 중첩되게 배치하면, 두 개의 흐름뿐만 아니라 세 개, 네 개 등등.. 그 이상의 control flow을 가질 수 있게된다.

(if / else if / else 와 같은 방식)


if else구문을 쓸 수 있는 모든 상황에 switch문을 쓸 수 있는 건 아니지만그와 반대로 모든 switch 구문은 if else문으로 대체될 수 있다.


즉, 하나의 변수를 입력받아 그 변수의 값에 따라 다른 흐름으로 이동할 수 있는 코드를 짜야할 때에 switch문과 if else구문이 둘 다 사용될 수 있음을 알 수 있다.

그렇다면 둘 중 어느 것을 사용해야 하는가?


결론부터 말하자면, switch문을 사용하자.


if else 구문을 사용한 코드와 switch 구문을 사용한 코드를 예를 들어 비교해보자.



1
2
3
4
5
6
7
8
9
10
11
int num = 5;
int ret;
 
if (num == 0)      ret = num;
else if (num == 1) ret = num;
else if (num == 3) ret = num;
else if (num == 5) ret = num;
else if (num == 7) ret = num;
else               ret = num;
 
System.out.println(ret);
cs


출력

5


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int num = 5;
int ret;
 
switch (num) {
case 0: ret = num; break;
case 1: ret = num; break;
case 3: ret = num; break;
case 5: ret = num; break;
case 7: ret = num; break;
default: ret = num; break;
}
System.out.println(ret);
cs


출력

5


구조적인 부분 빼고는 동일한 기능을 하는 두 개의 코드이다.


if else구문 각각의 조건문을 iterate하며 control flow를 결정한다.

N개의 if else구문이 있으면 N개의 조건문의 진위여부를 판단한다.


하지만 switch구문의 동작방식은 if else문과 다르다.


JVM에서는 switch구문안의 case 값들의 분포에 따라 내부적으로 각각의 상황에 최적화된 2개의 자바 바이트코드를 생성하는데, 공통적으로 두 경우 모두 HashTable이 연상되는 구조를 지니고 있다.


위의 예와 같이 case 값들이 서로 큰 차이가 없이 붙어있을 경우 TableSwitch형식의 컴파일을 수행하고, case의 값들이 서로 차이가 크게 날 경우 LookupSwitch형식을 사용한다. (예를 들어 case의 값이 1, 6, 34일 때)


컴파일된 자바 바이트코드를 통해 각각의 상황을 비교해보자.



  <case가 0, 1, 3, 5, 7일 때>


     0  iconst_5

     1  istore_1 [num]

     2  iload_1 [num]

     3  tableswitch default: 73

          case 0: 48

          case 1: 53

          case 2: 73

          case 3: 58

          case 4: 73

          case 5: 63

          case 6: 73

          case 7: 68

    48  iload_1 [num]

    49  istore_2 [ret]

    50  goto 75

    53  iload_1 [num]

    54  istore_2 [ret]

    55  goto 75

    58  iload_1 [num]

    59  istore_2 [ret]

    60  goto 75

    63  iload_1 [num]

    64  istore_2 [ret]

    65  goto 75

    68  iload_1 [num]

    69  istore_2 [ret]

    70  goto 75

    73  iload_1 [num]

    74  istore_2 [ret]

    75  getstatic java.lang.System.out : java.io.PrintStream [16]

    78  iload_2 [ret]

    79  invokevirtual java.io.PrintStream.println(int) : void [22]

    82  return


  <case값이 1, 6, 34인 경우>


    0  iconst_5

     1  istore_1 [num]

     2  iload_1 [num]

     3  lookupswitch default: 51

          case 1: 36

          case 6: 41

          case 34: 46

    36  iload_1 [num]

    37  istore_2 [ret]

    38  goto 53

    41  iload_1 [num]

    42  istore_2 [ret]

    43  goto 53

    46  iload_1 [num]

    47  istore_2 [ret]

    48  goto 53

    51  iload_1 [num]

    52  istore_2 [ret]

    53  getstatic java.lang.System.out : java.io.PrintStream [16]

    56  iload_2 [ret]

    57  invokevirtual java.io.PrintStream.println(int) : void [22]

    60  return



이름에서 어느정도 유추할 수 있듯이, TableSwitch는 case의 값이 서로 큰 차이가 나지 않는 경우 case로 주어진 값case들 사이의 값들에 해당하는 case까지 전부 계산하여 바이트코드로 생성한다.


0, 1, 3, 5, 7에 해당하는 case값만 주어졌지만 실제 컴파일 된 바이트코드를 보니 case들 사이의 값인 2, 4, 6 또한 default에 해당하는 control flow를 따르도록 컴파일 된 것을 확인할 수 있다.


밑의 경우는 case에 1, 6, 34값을 정했을 때 나온 LookupSwitch형식의 자바 바이트코드이다.

TableSwitch 방식의 코드와는 다르게, case간의 값들이 서로 많이 차이나게되면 그 사이의 값들을 해시 형태로 계산해두지 않는다는 것을 알 수 있다.


이 두 개의 switch 컴파일 방식들은 구조가 HashTable, List와 비슷하지만 비슷한건 단지 생김새 뿐만이 아니다.

자바 바이트코드에서 TableSwitchlabel만을 사용하고, LookupSwitchKey와 함께 label을 사용한다.

TableSwitch는 스택의 꼭대기에서 얻은 값을 전달하여 label을 찾아 바로 jump하는 방식으로 동작하고, LookupSwitch는 이진 탐색 트리 형식으로 저장된 구조에서 key값을 찾아 key값과 연결된 label을 통해 jump하는 형태로 동작한다.

그러므로 위의 코드는 case문의 개수에 따른 변수의 동일성을 계산하는 시간복잡도가 List, HashTable과 같이 각각 O(lgN), O(1)이다.


최종적으로 정리하자면, switch문은 item의 개수(N)에 따라 worst case ㅡ O(lgN) 에 실행된다는 것을 알 수 있다.

if else문은 item의 개수에 따라 O(N) 시간에 실행될 것이다.


for문을 충분히 중첩시켜 실제 수행시간을 측정해보록 하자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
long a = System.currentTimeMillis();
int ret = -1;
 
for (int i=0; i<1000000000++i)
    for (int num=0; num<5++num)
        if (num == 0)      ret = 10;
        else if (num == 1) ret = 20;
        else if (num == 2) ret = 30;
        else if (num == 3) ret = 40;
        else if (num == 4) ret = 50;
        else               ret = 12;
long b = System.currentTimeMillis();
System.out.println(ret);
System.out.println(b-a+"ms");
cs


출력

50

8718ms



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long a = System.currentTimeMillis();
int ret = -1;
 
for (int i=0; i<1000000000++i)
    for (int num=0; num<5++num)
        switch (num) {
        case 0: ret = 10break;
        case 1: ret = 20break;
        case 2: ret = 30break;
        case 3: ret = 40break;
        case 4: ret = 50break;
        default: ret = 12break;
        }
long b = System.currentTimeMillis();
System.out.println(ret);
System.out.println(b-a+"ms");
cs
 

출력

50
2820ms



결론

실제 수행시간은 N의 크기가 5라는 작은 수치임에도 switch문이 3배 정도 빠르다는 결과를 얻었다.

if else의 분기와 case의 개수를 각각 3개로 줄여 측정해도 switch문이 대략 1.5 ~ 2배 가까이 빠르게 동작했다.

N의 크기가 증가할 수록 차이는 더욱 벌어질 것으로 예상된다.


많은 코더들이 '하나의 variable을 받아 그 값을 비교해 control flow를 정하는 코드'를 짤 때 switch구문 대신 if else문을 쓰는 경향을 보인다.

혹여 구현시간이 빨라서 생산성이 높다는 이점이 있으면 if else문을 써도 되겠지만, 대부분의 언어에는 그러한 차이가 없다고 생각한다.


특정 코드의 최적화를 위해선 if else문보다 switch구문을 쓰는게 더 적절하다.