{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[Java] 자바로 프로그래밍 입문하기: 1.3. 조건문과 반복문 (1)

  이전까지의 프로그램은 각각의 구문이 프로그램 내에서 위에서 아래로 순서에 맞게 단 한 번만 실행되었습니다. 하지만 복잡한 프로그램들은 똑같이 실행하더라도 다양한 방식으로 결과가 달라집니다. 이는 구문의 흐름이나 순서에 따라 달라지는 것입니다. 우리는 이런 흐름을 제어 흐름(Control flow)이라고 합니다.

  이번 절에서는 프로그램 변수값을 이용한 로직으로 제어 흐름을 바꿀 수 있는 구문들에 대해 배워볼 것입니다. 이것은 프로그래밍에서 굉장히 중요한 부분입니다.

  특히 여러분은 자바 구문 중, 특정 조건을 만족해야만 실행이 되는 조건문이라는 것과 특정 조건에 따라서 여러 번 실행될 수 있는 반복문이라는 것을 배울 것입니다.


if문

  대부분의 프로그램은 서로 다른 입력에 대해 서로 다른 행동을 취해야 합니다. 이런 것을 자바에서 구현하는 방법 중 하나는 if문입니다.


if (<boolean expression>) { <statements> }

  이 설명은 템플릿(template)이라 불리는 정형화된 표기법이며, 자바 구조의 형식을 설명하는 데 사용할 것입니다. 홑화살괄호(<>, angle bracket)에는 우리가 이미 정의했던 구조가 들어가고, 이는 해당 구조의 용례(instance)를 사용할 수 있음을 나타냅니다.

  <boolean expression> 은 불리언 값으로 표현되는 식을 의미하며, 보통 비교 연산으로 이루어진 것입니다. <statements>는 구문 블록, 즉 세미콜론으로 끝나는 문장들로 이루어진 것입니다. 만약 <statements>가 단 하나의 구문이라면, 중괄호는 쓰지 않아도 상관 없습니다. 이런 중괄호로 묶인 구문들을 시퀀스(sequence)라고 합니다.

  if문의 의미는 간단합니다. 시퀀스 내의 구문들은 식이 참일 때에만 실행 됩니다. 다음 예시를 참고해보세요.


if (x < 0) x = -x;

  위 코드는 x에 절대값을 취하는 코드입니다.

if (x > y)
{
	int t = x;
	x = y;
	y = t;
}

  위 코드는 x값이 y값보다 크다면 서로의 값을 바꾸는 코드입니다. 이런 if문은 해당 연산이 참일 때에만 실행할 수 있습니다. else문을 추가해서 식이 거짓일 때에도 특정 구문을 실행하도록 할 수도 있습니다. 템플릿은 다음과 같습니다.


if (<boolean expression>) 	<statements T>
else				<statements F>

  예시는 다음 코드를 참고하세요.


if (x > y) 	max = x;
else		max = y;


  이런 제어 흐름을 이해하는 방법 중 하나는, 플로우 차트(flow chart)라는 다이어 그램을 통해 시각화해보는 것입니다.




  초기에, 로우 레벨 언어를 사용하는 프로그래머들은 제어 흐름을 이해하기 힘들 때 플로우 차트를 필수적으로 사용했었습니다. 위의 표에는 if와 if-else문의 용례가 기술되어 있습니다.

  조건문은 프로그래밍의 중요한 요소입니다. <프로그램 1.3.1>은 if-else문의 다른 예시입니다. 해당 프로그램의 바디에는 앞서 보여준 표의 용례처럼 한 문장밖에 없지만, 이는 생각해볼 가치가 있는 흥미롭고 철학적인 이야기를 보여줍니다: 컴퓨터가 무작위의 값을 산출할 수 있을까요? 물론, 아닙니다. 우선 프로그램은 난수의 성질과 아주 유사한 숫자들을 산출할 수 있다는 점만 알아두세요.


프로그램 1.3.1: Flip

public class Flip 
{
    public static void main(String[] args) 
    {
	//Simulate a coin flip.
	if (Math.random() < 0.5) System.out.println("Heads");
	else			 System.out.println("Tails");
    }
}
% java Flip
Heads
% java Flip
Tails
% java Flip
Tails


while문

  많은 프로그램들의 계산은 필연적으로 반복됩니다. 기초 자바 구조에서는 다음처럼 제어흐름을 조절할 수 있게 합니다.


while (<boolean expression>) { <statements> }

  while문은 while과 if라는 서로 다른 키워드를 사용한다는 점을 빼면, if문과 같은 구조를 갖습니다. 하지만 그 의미는 꽤 다릅니다. 이는 컴퓨터에게 다음의 행위를 하도록 하게 합니다: 첫째, 식이 거짓이면 아무것도 하지 않습니다. 둘째, 식이 참이면 시퀀스를 실행하며, 실행 후에 다시 식을 확인합니다. 식이 거짓이 될 때까지 반복합니다.

   반복되는 문장 블록을 보통 루프의 바디라고 합니다. 역시 마찬가지로, 바디가 단 하나의 구문이라면 중괄호는 쓰지 않아도 됩니다. 쉽게 생각해보면, while문은 동일한 if문의 무한한 연속과 같습니다.

if (<boolean expression>) { <statements> }
if (<boolean expression>) { <statements> }
if (<boolean expression>) { <statements> }
...

  따라서 어느 지점에서는 반드시 무언가를 바꾸어서, 불리언 식을 거짓으로 만들어야만 이 시퀀스를 탈출할 수 있을 것입니다. 흔한 프로그래밍의 기법 중 하나는, 루프가 반복된 횟수를 표시하는 정수값을 이용하는 것입니다. 처음 값에서 시작해서, 루프를 통과할 때마다 1씩 증가하며, 루프를 계속할 지 결정하는 시점에 미리 정해놓은 값을 넘었는지 확인하는 것입니다. <프로그램 1.3.2, TenHellos>는 while문의 전형적인 용례를 보여줍니다.


프로그램 1.3.2: Your first while loop

 
public class TenHellos 
{
    public static void main(String[] args) 
    {	// Print 10 Hellos.
	System.out.println("1st Hel1o");
	System.out.println("2nd Hello");
	System.out.println("3rd Hello");
	int i = 4;
	while (i <= 10)
	{   // Print the ith Hello.
	    System.out.println(i + "th Hello");
	    i = i + 1;
	}
    }
}
% java TenHellos
1st Hello
2nd Hello
3rd Hello
4th Hello
5th Hello
6th Hello
7th Hello
8th Hello
9th Hello
10th Hello

 

  이 프로그램에서 핵심은 다음 구문입니다.


i = i + 1;

  수학적 등호로 이 문장을 바라보면 말도 안 되는 문장이므로, 자바 대입문의 입장에서 바라보아야 합니다. 이는 변수 i에 i + 1의 결과를 대입한다는 의미입니다. 만약 i의 값이 4였다면, 이 구문을 실행하고 난 뒤에는 i의 값이 5가 될 것이며, 그런 식으로 계속 증가할 것입니다.

  <TenHellos>에서 i의 값은 4로 시작해, i의 값이 11이 되어 시퀀스를 탈출할 대까지 총 7번 실행됩니다. while 반복문을 이용하는 것이 지금은 별로 가치가 없어보일 수 있지만, 곧 반복문 없이는 도저히 해결하기 힘들 정도로 많이 반복되는 과제들을 다루게 될 것입니다. while문을 이용하는 프로그램과 그렇지 않은 프로그램 사이에는 엄청난 차이가 있습니다. while문은 잠재적으로 무한한 횟수의 문장을 반복할 수 있게 해주기 때문입니다.

  하지만 이에는 단점도 있습니다. 프로그램이 세련되면 세련될 수록, 아마 누군가가 여러분의 코드를 봤을 때, 단숨에 이해하기는 어려울 것입니다. <프로그램 1.3.3, PowersOfTwo>는 while문을 이용해서 2의 제곱수를 계산해냅니다. 

public class PowersOfTwo {
    public static void main(String[] args) 
    { // Print the first N powers of 2.
	int N = Integer.parseInt(args[0]);
	int v = 1;
	int i = 0;
	while (i <= N) 
	{ // Print ith power of 2.
	    System.out.println(i + " " + v);
	    v = 2 * v;
	    i = i + 1;
	}
    }
}
% java PowersOfTwo 5
0 1
1 2
2 4
3 8
4 16
5 32


  반복되는 프로그램의 행위를 추적 테이블로 꼼꼼히 살펴보는 것은 좋은 습관입니다. <PowersOfTwo>의 경우, 반복문을 순회하는 변수의 값과, 반복문을 제어하는 조건식을 보여줘야 합니다. <PowersOfTwo>는 매 루프마다 변수의 값을 출력하므로, 사실 상 self-tracing 프로그램이라고 할 수 있습니다. 이는 다시 말하면, 어떤 프로그램이든 System.out.println을 적절히 활용하면 여러분들도 추적 테이블을 만들어볼 수 있다는 의미입니다.

  <PowersOfTwo>에는 숨겨진 문제가 있는데, 자바의 int 자료형에서 가장 큰 정수는 2^31-1 이고, 프로그램은 그런 숫자에 대한 가능성을 테스트하지 않았다는 사실입니다. 만약 <PowersOfTwo>를 31 이상의 인자로 실행시킨다면, 다음과 같은 출력을 볼 수 있을 것입니다.

...
30 1073741824
31 -2147483648
32 0

  변수 v는 너무 커지게 되어 음수로 바뀌게 되는데, 이는 자바가 정수를 표현하는 방법 때문입니다. 이전에도 배웠지만, 이를 overflow라고 말합니다. int의 가장 큰 값은 Integer.MAX_VALUE를 이용해 사용해볼 수 있습니다. <프로그램 1.3.3>을 개선하는 방법은, overflow를 일으킬 지 미리 테스트해서, 너무 큰 숫자라면 오류 메시지를 출력하는 것입니다. 하지만 이런 것처럼 프로그램이 모든 입력에 대해 대처하도록 하는 것은 생각보다 어려운 일입니다.

  좀 더 복잡한 예로, 주어진 양의 정수 N 이하의 가장 큰 2의 제곱수를 계산하는 걸 생각해봅시다.


int v = 1;
while (v <= N / 2)
    v = 2 * v;  

  아주 간단해 보이는 코드 조각이지만, 이 프로그램이 정말 올바른 값을 출력한다는 사실을 납득하려면 여러분은 꽤나 깊게 생각을 해보아야 될 것입니다.


  1. v는 언제나 2의 제곱수이다.
  2. v는 N보다 커지지 않는다.
  3. v의 값은 반복될 때마다 증가한다. 고로 언젠가는 반복문을 탈출할 것이다.
  4. 탈출한 이후에, 2*v는 반드시 N보다 클 것이다.

  이런 식의 추론은 while문이 어떻게 동작하는지 이해하는데 중요한 역할을 합니다.


for 반복문

  앞서 본 것처럼, while문은 모든 방식의 응용 프로그램을 작성할 수 있게 해줍니다. 더 많은 예시들을 보기 이전에, 반복문을 좀 더 유연하게 작성할 수 있는 자바 문법을 알아볼까 합니다. 이는 while문의 대체 표기법이며, 근본적으로 while문과 다른 점은 없지만 보편적으로 사용됩니다. 좀 더 간편하고 읽기 쉽게 만들어주기 때문입니다.

  많은 반복문은 다음과 같은 전략을 사용합니다: 인덱스 변수를 하나 초기화(initialize) 하고, 반복문의 조건(boolean expression)에 인덱스 변수를 포함시키며, 반복문의 끝에서 해당 인덱스 변수를 증가(increment)시킨다. 

  for문의 템플릿은 다음과 같습니다.


for (<initialize>; <boolean expression>; <increment>)
{
    <statements>
}

  이는 사실 다음의 형태를 보기 좋게 표기한 것입니다.

<initialize>;
while (<boolean expression>)
{
    <statements>
    <increment>;
}

  사실 <initialize>나 <increment>부분에 초기화나 증감이 아닌 다른 구문을 사용해도 상관은 없습니다. 하지만 우리는 for문을 사용할 때 거의 항상 초기화와 증감을 하는 관용구처럼 사용합니다. 예를 들어, 앞서 보았던 <프로그램 1.3.2, TenHellos>의 반복문을 다음과 같이 바꿀 수 있습니다.


for (int i = 4; i <= 10; i = i + 1)
    System.out.println(i + "th Hello");

  간편하죠. 심지어 우리는 이것보다 조금 더 간편화된 방법을 사용할 수 있습니다. 간편화된 표기법에 대해 배워보겠습니다.


복합 대입 연산자(Compound assignment idioms)

  변수의 값을 변경하는 것은 프로그래밍에서 자주 행해지는 것입니다. 따라서 자바는 다양한 속기법을 목적에 맞게 제공해줍니다. 예를 들어, 변수 i의 값을 1 증가시키는 방법은 다음 4가지 방법이 있습니다.


i = i + 1;
i++;
++i;
i += 1;

  물론 i--나 --i, i -= 1, i = i-1처럼 1을 감소시키는데에도 사용할 수 있습니다. 사실 어떤 것도 상관 없지만, 대부분의 프로그래머들은 i++나 i--을 for문에서 사용합니다.

  ++와 --는 보통 정수에 사용되지만, 복합 대입 연산자는 어떤 숫자 기본 자료형이든, 또 어떤 수학 연산자이든 사용할 수 있습니다. 예를 들어, v *= 2 또는 v += v를 v = 2*v 대신 사용할 수 있습니다. 이 관용구들은 편의를 위한 것 그 이상 그 이하도 아닙니다.


스코프(Scope, 범위)

  변수의 스코프는, 그 변수가 정의된 부분을 의미합니다. 일반적으로 변수의 스코프는 해당 변수가 선언된 중괄호 안에 있는 구문들입니다. 

  특히, for 반복문의 첫 부분은 for 반복문의 바디 블록에 있는 것으로 간주합니다. 이는 while문과 for문의 차이를 드러내죠. 전형적인 for문에서 인덱스 변수(i)는 for문 바깥 구문에서 사용될 수 없습니다. 반면 while문에서는 가능합니다. 이러한 차이는 while문 대신 for문을 사용하는 이유가 되기도 합니다. 

  같은 결과를 이끄는 공식들 중 어떤 하나를 선택하는 것은 순전히 프로그래머들의 취향입니다. 이는 마치 작가가 글을 쓸 때 많은 동의어들 중에 가장 괜찮은 단어를 고르는 것과 같습니다.

  다음 표는 자바에서 전형적인 반복문의 코드 조각들입니다. 자바의 반복문을 이해하기 위해서는 이런 코드 조각들을 작성해보고, 컴파일 후 실행해보세요. 직접 작성해본 코드를 실행하는 것만큼 좋은 경험은 없습니다. 또한 자바에서 반복문의 사용에 대한 이해를 높이기 위해 필수적인 절차입니다.


중첩(Nesting)

  if, while, for문은 여타 배웠던 대입문들과 별 다를 바 없습니다. 즉, 구문이 들어갈 수 있는 곳이라면 언제든지 사용할 수 있습니다. 특히, 우리는 복합적인 문장을 위해 반복문의 바디 안에도 반복문을 넣을 수 있다는 것입니다. 첫번째 예로, <DivisorPattern>은 중첩된 for문과 print문으로 구성되어 있습니다. 이 프로그램은 i번째 행에, i의 약수 자리에 별표가 있는 별표 패턴을 출력합니다.


프로그램 1.3.4: Your first nested loops


public class DivisorPattern {
    public static void main(String[] args) 
    {	// Print a square that visualizes divisors.
	int N = Integer.parseInt(args[0]);
	for (int i = 1; i <= N; i++) 
	{   // Print the ith line.
	    for (int j = 1; j <= N; j++) 
	    {	// Print the jth entry in the ith line.
		if ((i % j == 0) || (j % i == 0))
		    System.out.print("* ");
		else
		    System.out.print("  ");
	    }
	    System.out.println(i);
	}
    }
}
% java DivisorPattern 5
* * * * * 1
* *   *   2
*   *     3
* *   *   4
*       * 5

  <DivisorPattern>은 복잡한 제어 구조를 갖고 있고, 이런 구조는 플로우 차트로 표현할 수 있습니다.


  이런 도표는 제어 구조 사용의 중요성을 보여줍니다. 중첩(nesting)을 통해, 프로그램이 복잡한 제어 구조를 갖고 있더라도 이해하기 쉽게 반복문과 조건문으로 프로그램을 작성할 수 있습니다. 대부분의 유용한 계산들은 단일이나 이중 중첩만으로도 충분히 만들어낼 수 있습니다.


  두 번째 중첩의 예는, 과세율을 계산하는 세금 예상 프로그램의 코드 조각입니다. 


if      (income < 0) 		rate = 0.0;
else if (income < 47450) 	rate = .22;
else if (income < 114650) 	rate = .25;
else if (income < 174700) 	rate = .28;
else if (income < 311950) 	rate = .33;
else				rate = .35;

  이 경우, 여러 개의 if문들이 서로 배반되는 가능성의 문제를 확인하기 위해 중첩되어 있습니다. 이런 구조는 우리가 자주 사용하게 될 구조입니다.

  루프를 이용한 프로그래밍은 컴퓨팅의 온 세상에 발을 딛게 해줍니다. 다음 예제들은 1.2절에서 배웠던 자료형들로만 작업하는 것들이지만, 다른 어떤 응용프로그램이든 간에 그 메커니즘은 동일하다고 장담합니다.


(계속)

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기