변수(Variable)

프로그래밍에서의 변수(Variable)는 값을 저장하는 공간, 참조하기 위해 쓰이는 공간을 말합니다. 사람이 필요한 데이터를 뇌에 기억시키는 것처럼, 변수도 메모리 공간이라는 곳에 데이터를 기억시킵니다. 쉽게 말하면 메모리 공간의 특정 번지에 이름을 붙인 것으로, 이렇게 붙인 이름을 통해 값을 저장하거나 읽을 수 있습니다.

변수의 선언

만약 우리가 메모리 공간에 어떤 값을 저장하고 싶다면, 다음과 같이 변수를 선언하시면 됩니다. 여기서 선언은 컴파일러에게 이런 이름을 가진 변수가 있고 그 변수의 타입은 이것이라고 알려주는 것입니다. 

int num;

이 문장은 10진수 정수(integer)를 저장하기 위한 메모리 공간을 할당하겠다는 말이 됩니다. 그리고 int 옆에 보이는 num은 우리가 접근하기 위하여 메모리 공간에 붙여진 이름입니다. 이를 변수명이라고 부르며 간혹 식별자(identifier)라고 부르기도 합니다. 이를 메모리 공간에서 살펴보면 다음과 같은 모습일 것입니다.

위에서 확인할 수 있듯이 일반적으로 변수는 아래와 같이 선언합니다.

타입 변수명;

그리고 변수를 선언할 때 타입이 동일하면 여러 개의 변수명을 콤마(,)로 구분해서 한꺼번에 선언할 수도 있습니다.

int num1, num2, num3;

변수명 작성 규칙

변수명을 작성할 때 주의할 점이 있습니다.

  • 변수명은 문자, 언더바(_), 달러 기호($)로만 시작할 수 있습니다.
  • 첫 번째 문자 뒤에는 문자나 0~9까지의 숫자가 들어올 수 있으며, 언더바(_)나 달러 기호($)를 제외한 특수문자나 공백은 올 수 없습니다.
  • 키워드(keyword)를 변수명으로 사용할 수 없습니다.
  • 변수명은 대소문자를 구분합니다. 따라서 Num과 num은 서로 다른 변수입니다.

여기서 키워드(keyword)란 자바에서 미리 정의되어 특별한 의미를 지니는 단어들입니다. 따라서, 아래에 나열된 키워드들은 변수명으로 사용할 수 없습니다. 어떤 것이 키워드인지는 외우려고 하지 않아도 저절로 알게 될 것이니 간단하게 보기만 해 주세요.

abstract, continue, for, new, switch, assert, default, goto, package, synchronized, boolean, do, if, private, this, break, double, implements, protected, throw, byte, else, import, public, throws, case, enum, instanceof, return, transient, catch, extends, int, short, try, char, final, interface, static, void, class, finally, long, strictfp, volatile, const, float, native, super, while

다음과 같은 변수명은 올바른 예입니다.

int num1;
int student;
int _box;

다음은 올바르지 않은 예입니다.

int 1num; // 변수명 앞에 숫자가 올 수 없음.
int char; // 변수명에 키워드가 사용될 수 없음.
int n&um; // 변수명에 특수 문자가 사용될 수 없음.
변수명에 $를 쓰지 마세요. 변수명에 달러 기호를 쓸 수는 있지만 이는 관례에 어긋납니다. 이는 변수명 뿐만이 아니라 다른 명명 규칙에도 모두 적용됩니다. 보통은 컴파일러가 자동으로 생성한 코드에 $를 사용하기 때문에, 우리가 사용한 이름과 컴파일러가 내부적으로 생성하고 사용하는 이름이 서로 충돌하여 예기치 못한 상황을 마주칠 수도 있습니다. 예를 들어서, A 클래스의 내부 클래스인 B는 컴파일을 할 때 A$B.class가 되고, A 클래스 내부의 익명 클래스는 A$1.class가 됩니다. 클래스가 무엇인지는 차차 살펴볼 것입니다.

변수의 초기화

우리가 변수를 선언하면 처음에는 그 변수에 어떤 값이 들어있는지 알 수 없습니다. 변수에 값을 지정하지 않으면 읽어 들여도 쓸모가 없으므로, 변수에 최초로 값을 저장하는 과정이 필요합니다. 이를 '변수를 초기화한다.'라고 말합니다.

public class VariableExamples {

	public static void main(String[] args) {
		int num;
		
		num = 1000;
		
		System.out.println(num); // 1000
	}

}

코드를 살펴보시면, 4행에서 10진수 정수형 변수 num을 선언했습니다. 그리고 6행에서 1000 이란 값으로 변수 num을 초기화했습니다. 여기서 아래와 같이 변수의 선언과 동시에 초기화를 시킬 수도 있습니다.

int num = 1000;

그리고 8행에서 num의 값을 출력하도록 했습니다. 주의하실 점이 있다면, 여기에서 사용된 =는 수학에서 서로 같음을 나타내는 등호 '='가 아닙니다. 자바에서는 오른쪽에 위치한 값을 왼쪽에 있는 변수 num에 대입시키라는 대입 연산자의 의미를 지니고 있습니다.

예제 코드를 살짝 바꿔서 6행에서 1000이 아닌 27.5, 44.76 같은 실수값을 대입시키면 어떻게 될까요? 실수값을 대입시키고 컴파일을 하게되면 아래와 같은 에러가 발생합니다.

에러가 말해주는 대로 필요한 타입(혹은 형)은 정수형(int)인데 실수형(double)이 왔기 때문입니다. 에러를 없애려면 int형이 아닌 double형으로 변수를 선언해야 합니다. 이처럼 int, double 등과 같은 것들을 타입(type)이라 부르며 자바에서는 여러 가지의 타입이 존재합니다.

기본 타입

타입은 변수에 담기는 데이터의 종류를 말하며, 자료형이라고도 합니다. 변수에 값을 저장하기 전에 그 변수가 정수, 실수, 논리형, 문자, 문자열 등 어떤 종류의 값을 지닐 수 있는지 타입을 미리 지정해야 합니다. 자바에서는 크게 정수형(byte, short, int, long), 문자형(char), 실수형(float, double), 논리형(boolean)처럼 8개의 기본 타입을 제공합니다. 하나하나씩 살펴보도록 하겠습니다.

정수형

정수형은 양의 정수, 음의 정수를 저장할 수 있습니다. 타입의 크기에 따라 저장할 수 있는 값의 범위가 정해집니다. 정수형의 값의 범위는 \( -2^{n-1} \sim 2^{n-1}-1 \)이며, n에는 메모리 비트 수가 들어갑니다. 예를 들어서 int형은 크기가 32비트이므로 값의 범위는 \( -2^{31} \sim 2^{31}-1 \)임을 알 수 있습니다.

직접 위의 타입들을 가지고 정수형 변수를 선언해보도록 하겠습니다.

public class PrimitiveTypeExamples {

	public static void main(String[] args) {
		byte num1 = 100;
		short num2 = 30000;
		int num3 = 1000000000;
		long num4 = 9000000000000000000L;
		
		System.out.println(num1); // 100
		System.out.println(num2); // 30000
		System.out.println(num3); // 1000000000
		System.out.println(num4); // 9000000000000000000
	}

}

위의 코드에서 long형 변수 num4의 값을 초기화할 때 뒤에 'L'이 붙은 것을 볼 수 있습니다. 뒤에 붙은 L을 제거하면 아래와 같은 에러가 발생합니다.

정수 리터럴(Integer Literals)

이는 정수 리터럴이 값의 범위를 넘어섰기 때문으로, 컴파일러가 정수 리터럴을 기본적으로 int형으로 간주하기 때문입니다. 여기서 리터럴(literal)은 프로그래머가 소스 코드에 직접 입력한 고정된 값을 말합니다.

// 에러: integer 숫자가 너무 큽니다.
long num4 = 9000000000000000000;

따라서 리터럴이 int형의 범위를 넘어설 경우에는 접미사 L을 붙여서 long형임을 컴파일러에게 알려주어야 합니다. int형의 범위를 넘어서지 않는 경우에는 접미사 L을 붙일 필요는 없습니다. 접미사로 소문자 l을 붙여도 되지만 숫자 1과 비슷해서 혼동이 올 수 있으므로 보통은 대문자 L을 사용합니다.

그리고 자바에는 10진수 리터럴 외에도 2진수, 8진수, 16진수 리터럴이 있습니다. 자바는 앞에 0b 또는 0B로 시작하는 숫자를 2진수(Binary)로 해석하고, 0으로 시작하는 숫자를 8진수(Octal)로, 0x 또는 0X로 시작하는 숫자를 16진수(heXadecimal)로 해석합니다.

0b11011, 0b1000, 0B1000101 // 2진수 리터럴
015, 017, 0233 // 8진수 리터럴
0xC8, 0x70C556, 0XA23 // 16진수 리터럴

마지막으로 숫자 리터럴을 읽기가 힘들다면 언더바(_)를 사용하여 숫자를 끊고 가독성을 높일 수도 있습니다.

// https://docs.oracle.com/javase/8/docs/technotes/guides/language/underscores-literals.html
long socialSecurityNumber1   = 999_99_9999_L;         // X. 접미사 L 앞에 _를 둘 수 없다.

int x1 = _52;              // 숫자 리터럴이 아니라 변수명으로 인식한다.
int x2 = 5_2;              // O (10진수 리터럴)
int x3 = 52_;              // X. 리터럴의 끝에 _를 둘 수 없다.
int x4 = 5_______2;        // O (10진수 리터럴) 

int x5 = 0_x52;            // X. 접두사 0x 사이에 _를 둘 수 없다.
int x6 = 0x_52;            // X. 수가 시작하는 부분에 _를 둘 수 없다.
int x7 = 0x5_2;            // O (16진수 리터럴)
int x8 = 0x52_;            // X. 수의 끝 부분에 _를 둘 수 없다.

int x9 = 0_52;             // O (8진수 리터럴)
int x10 = 05_2;            // O (8진수 리터럴)
int x11 = 052_;            // X. 수의 끝 부분에 _를 둘 수 없다.

문자형

문자형인 char형에는 유니코드 문자(UTF-16) 하나를 저장할 수 있습니다. 유니코드란 전 세계의 모든 언어나 기호에 번호를 할당하여 문자를 정의하는 국제 표준 규약입니다.

아래 예제에서 확인할 수 있듯이, char형 변수에 문자를 저장할 수 있습니다. 여기서 조심할 점은 문자 리터럴은 작은 따옴표(')로 둘러싸야 합니다. 그리고 문자형 변수에는 정수 또한 저장할 수 있는데, 16진수 0xAC00(10진수로 44032)는 유니코드 문자 '가'에 할당된 값으로 직접 출력해보면 둘 다 '가'가 출력되는 것을 확인할 수 있습니다.

public class PrimitiveTypeExamples {

	public static void main(String[] args) {
		char ch1 = '가'; // 44032
		char ch2 = 44032; // '가'
		
		System.out.println(ch1); // 가
		System.out.println(ch2); // 가
	}

}

실수형

실수를 저장할 수 있는 타입에는 float, double이 있습니다. double이란 이름은 float형보다 약 2배의 정밀도를 가진다는 뜻에서 붙여진 것으로, 메모리 공간을 절약하는 것보다 정밀도가 중요한 경우에는 float가 아닌 double을 사용합니다. 다양한 상황에서 float의 정밀도로는 충분하지 않을 때가 많기 때문에 특수한 경우를 제외하고는 double형을 사용할 것을 권장합니다.

실수 리터럴(Floating Point Literals)

컴파일러가 정수 리터럴을 기본적으로 int형으로 간주하는 것과 마찬가지로, 실수 리터럴은 double형으로 간주합니다. 따라서, 아래와 같이 float형 변수를 double형의 값으로 초기화할 때 에러가 발생합니다.

이 경우에는 컴파일러에게 이 값이 float형임을 알려주기 위해서 접미사 f 또는 F를 붙여야 합니다. 참고로, 접미사 d 또는 D를 붙여서 double형임을 알려줄 수도 있습니다.

float a = 12.34f;

그리고 정수 리터럴과 마찬가지로 읽기가 힘들면 언더바(_)를 사용하여 숫자를 끊고 가독성을 높일 수도 있습니다.

// https://docs.oracle.com/javase/8/docs/technotes/guides/language/underscores-literals.html
float pi1 = 3_.1415F;      // X. 소수점 가까이에 _를 둘 수 없다.
float pi2 = 3._1415F;      // X. 소수점 가까이에 _를 둘 수 없다.

과학적 표기법(Scientific notation)

과학적 표기법은 너무 크거나 너무 작은 숫자들을 십진법으로 편하게 작성하여 표현하는 방법을 말합니다. 과학적 표기법을 통해 실수 리터럴은 \( a \times 10^b \)의 형태로 다시 쓸 수 있습니다. 예를 들어서, 3125000을 과학적 표기법을 이용해 나타내면 \( 3.125 \times 10^6 \)이 되고, 0.00096은 \( 9.6 \times 10^{-4} \)가 됩니다. 자바에서는 과학적 표기법을 나타낼 때 \( 3.125 \times 10^6 \)를 3.125e6 또는 3.125e+6로 나타낼 수 있고, \( 9.6 \times 10^{-4} \)은 9.6e-4로 나타낼 수 있습니다. 여기서 소문자 e는 대문자 E로 바꾸어 써도 상관이 없습니다.

public class PrimitiveTypeExamples {

	public static void main(String[] args) {
		double a = 3.125e+6;
		double b = 9.6e-4;
		
		System.out.println(a);
		System.out.println(b);
	}

}

논리형

참(true)과 거짓(false)을 나타낼 수 있는 타입으로 boolean이 있습니다. 아래 예제를 살펴보면, 자바에는 참과 거짓을 나타내는 키워드로 true, false가 있고, boolean형은 두 가지 값만 가질 수 있다는 걸 확인할 수 있습니다.

public class PrimitiveTypeExamples {

	public static void main(String[] args) {
		boolean a;
		
		a = false;
		System.out.println(a); // false
		a = true;
		System.out.println(a); // true
		
		if (a) System.out.println("이 문장은 실행됩니다."); // 이 문장은 실행됩니다.
		
		a = false;
		if (a) System.out.println("이 문장은 실행되지 않습니다.");
	}

}

여기서 if로 시작하는 부분은 자바의 조건문으로, 간단히 살펴보면 소괄호 안에 있는 조건식이 참인가 거짓인가에 따라 실행 여부가 결정됩니다. 이처럼 boolean형은 조건문과 나중에 살펴볼 반복문에서 많이 쓰입니다.

if (a) System.out.println("이 문장은 실행됩니다."); // 이 문장은 실행됩니다.

a = false;
if (a) System.out.println("이 문장은 실행되지 않습니다.");

타입 변환(Type conversion)

어떤 타입의 값을 다른 타입의 값으로 변환하는 과정을 타입 변환이라고 합니다. 이를 형 변환이라고 부르기도 합니다.

확대 변환(Widening conversion)

확대 변환은 표현 범위가 작은 타입에서 표현 범위가 큰 타입으로 변환되는 것을 말합니다. 암시적 변환(implicit conversion)이라고도 하며, 우리가 직접 변환하지 않아도 컴파일러가 자동으로 수행합니다. 이 경우에는 일반적으로 데이터가 손실될 우려가 없기 때문에 에러도 경고도 발생하지 않습니다.

아래의 코드를 보면 자동으로 타입 변환이 이루어지는 것을 볼 수 있습니다.

class TypeConversionExamples {
	public static void main(String[] args) {
		short a = 300;
		// 자동으로 short형에서 int형으로 타입 변환이 이루어진다.
		int b = a;
		// 자동으로 short형에서 float형으로 타입 변환이 이루어진다.
		float c = a;
		
        System.out.println(a); // 300
        System.out.println(b); // 300
        System.out.println(c); // 300.0

        // 그림에는 char이 빠졌지만 char에서 int, long, float, double
        // 로도 확대 변환이 일어나므로 참고하자.
        char d = 'A';
        int e = d;
        float f = d;

        System.out.println(d); // A
        System.out.println(e); // 65
        System.out.println(f); // 65.0
	}
}

참고로, 아래와 같이 타입이 서로 호환되지 않는 경우에는 변환할 수 없습니다. 예를 들어서, 아래와 같이 논리 데이터를 숫자 데이터로 변환하려는 경우 에러가 발생합니다.

boolean a = true;
// 타입 불일치: boolean에서 int로 변환할 수 없습니다.
int b = a;

축소 변환(Narrowing conversion)

반대로 축소 변환은 표현 범위가 큰 타입에서 표현 범위가 작은 타입으로 변환되는 것을 말합니다. 이 경우 표현 범위가 줄어들어서 데이터의 손실이 일어날 수 있으므로, 이것은 컴파일러가 아니라 프로그래머가 직접 수행해야 합니다. 만약 그러지 않았을 때는 타입을 변환할 수 없다는 에러가 발생합니다.

아래의 코드를 보면 (long), (int)와 같이 (타입)이라고 써진 것을 볼 수 있는데 이것은 캐스트 연산자(cast operator)입니다. 아직 연산자는 배우지 않았기 때문에 간단히만 말하면, 캐스트 연산자는 어떤 타입을 다른 타입으로 강제로 변환하는 녀석이라고 할 수 있습니다.

class TypeConversionExamples {
	public static void main(String[] args) {
		double a = 35.42;
        // double형 데이터를 long형으로 강제로 변환한다.
        // 참고로 실수를 정수로 변환하면 소수점 이하는 버려진다.
		long b = (long)a;
        // double형 데이터를 int형으로 강제로 변환한다.
		int c = (int)a;
		
		System.out.println(a); // 35.42
		System.out.println(b); // 35
		System.out.println(c); // 35
        
		// 그림에는 char이 빠졌지만 char에서 short, byte로 축소 변환이 일어난다. 
		char d = 'C';  
		short e = (short)d;  
		byte f = (byte)d;  
		  
		System.out.println(d); // C  
		System.out.println(e); // 67  
		System.out.println(f); // 67
	}
}

위 코드를 컴파일 후 결과를 살펴보면 35.42에서 35로 데이터의 손실이 일어난 것을 확인할 수 있습니다. 이해가 되시나요?

'프로그래밍 관련 > 자바' 카테고리의 다른 글

6편. 연산자 (1)  (19) 2012.07.25
5편. 주석  (14) 2012.07.22
2편. 개발 환경 구축하기  (19) 2012.07.20
3편. 프로그램의 구성  (109) 2012.07.20
1편. 자바의 소개  (63) 2012.07.17