[Java 21] (2) - variable

변수란?

 

수학에서 변수를 '변하는 수'라고 정의하지만 프로그래밍 언어에서의 변수(variable)란, 값을 저장할 수 있는 메모리상의 공간을 의미합니다. 이 공간에 저장된 값은 변경될 수 있기 때문에 변수라는 수학용어의 정의와 상통하는 면이 있어서 이렇게 이름이 붙여졌습니다.

하나의 변수에 단 하나의 값만 저장할 수 있으므로, 새로운 값을 저장하면 기존의 값은 사라집니다.

 

변수의 선언과 초기화

변수를 사용하려면 먼저 변수를 선언(declaration)해야하는데, 변수의 선언 방법은 다음과 같습니다.

변수 타입은 변수의 종류를 지정하는 것입니다. 저장하고자 하는 값의 종류에 맞게 변수의 타입을 선택해서 적어주면 됩니다. 변수는 값을 담기 위한 그릇이므로 어떤 값을 담을 것인지에 따라 그릇의 종류, 즉 변수의 타입이 결정됩니다.

 

변수 이름은 말 그대로 변수에 붙인 이름으로, 값을 저장할 수 있는 메모리 공간에 이름을 붙여주는 것입니다. 저장 공간(변수)의 이름으로 값을 저장하고, 저장된 값을 읽어오기도 합니다.

 

위 예시는 변수 'age'를 선언한 것으로, 나이를 저장하기 위한 공간이고 나이는 정수(Integer)이므로 변수의 타입을 int로 하였습니다.

 

변수를 선언한 이후부터는 변수를 사용할 수 있으나, 그 전에 반드시 변수를 초기화(initialization)해야 합니다. 메모리는 여러 프로그램이 공유하는 자원이므로 전에 다른 프로그램에 의해 저장된 알 수 없는 값(쓰레기 값, garbage value)이 남아있을 수 있기 때문입니다.

 

int a;
a = 10;

int b = 20;

int c = 30, d = 40;

변수에 값을 저장할 때는 대입 연산자(=)를 사용합니다. 변수의 초기화는 선언 이후에 해도 되고, 선언과 동시에 해도 됩니다. 또한 변수는 한 줄에 하나씩 선언하는 것이 보통이지만, 타입이 같은 경우 콤마(,)를 구분자로 여러 변수를 한 줄에 선언할 수도 있습니다.

 

변수의 종류에 따라 변수의 초기화를 생략할 수 있는 경우도 있지만, 변수는 사용되기 전에 적절한 값으로 초기화 하는 것이 좋습니다.

 

변수의 명명규칙

변수의 이름처럼 프로그래밍에서 사용하는 모든 이름을 '식별자(identifier)'라고 하며, 식별자는 같은 영역 내에서 서로 구분(식별)될 수 있어야합니다. 그리고 식별자를 만들 때는 다음과 같은 규칙을 지켜야 합니다.

  1. 대소문자가 구분되며 길이에 제한이 없습니다.
  2. 예약어를 사용해서는 안됩니다.
  3. 숫자로 시작해서는 안됩니다.
  4. 특수문자는 '_' 와 '$' 만을 허용합니다.

규칙 2번에서 예약어는 reserved keyword 라고 하며, 프로그래밍 언어의 구문에서 사용되는 단어를 뜻합니다. 그래서 예약어는 클래스나 변수, 메서드의 이름(identifier)로 사용할 수 없습니다.

- true, false, null은 예약어가 아닌 리터럴이지만, 이름으로 사용할 수 없습니다.
- JDK 9부터 언더스코어(_)가 예약어가 되었습니다.

 

그 외에 필수적인 것은 아니지만 자바 프로그래머들에게 권장하는 규칙들은 다음과 같습니다.

  1. 클래스 이름의 첫 글자는 항상 대문자로 합니다.
  2. 여러 단어로 이루어진 이름은 단어의 첫 글자를 대문자로 합니다. (CamelCase)
  3. 상수의 이름은 모두 대문자로 하고, 여러 단어로 이루어진 경우 '_'로 구분합니다.

 

변수의 이름은 짧을수록 좋지만, 약간 길더라도 용도를 알기 쉽게 의미있는 이름으로 하는 것이 바람직합니다. 또는 선언문에 주석으로 변수에 대한 정보를 주는 것도 좋은 생각입니다.

 

변수의 타입

 

우리가 주로 사용하는 값(data)의 종류(type)는 크게 '문자와 숫자'로 나눌 수 있으며, 숫자는 다시 '정수와 실수'로 나눌 수 있습니다.

이러한 값의 종류에 따라 값이 저장될 공간의 크기와 저장형식을 정의한 것이 자료형(data type)입니다. 자료형에는 문자형(char), 정수형(byte, short, int, long), 실수형(float, double) 등이 있으며, 변수를 선언할 때는 저장하려는 값의 특성을 고려해여 가장 알맞은 자료형을 선택하면 됩니다.

 

기본형과 참조형

자료형은 크게 '기본형'과 '참조형' 두 가지로 나눌 수 있는데, 기본형 변수는 실제 값(data)를 저장하는 반면, 참조형 변수는 어떤 값이 저장되어 있는 주소(memory address)를 값으로 갖습니다. 자바는 C언어와 달리 참조형 변수 간의 연산을 할 수 없으므로 실제 연산에 사용되는 것은 모두 기본형입니다.

메모리에는 1 byte단위로 일련 번호가 붙어 있는데, 이 번호를 메모리 주소(memory address)라고 합니다.

 

  • 기본형(primitive type)
    • 논리형(boolean), 문자형(char), 정수형(byte, short, int ,long), 실수형(float, double)
  • 참조형(reference type)
    • 8개의 기본형을 제외한 나머지 타입

 

참조형 변수(참조 변수)를 선언할 때는 변수의 타입으로 클래스의 이름을 사용하므로 클래스의 이름이 참조 변수의 타입이 됩니다. 그래서 새로운 클래스를 작성한다는 것은 새로운 참조형을 추가하는 셈입니다.

 

다음은 Date클래스 타입의 참조 변수 today를 선언한 것입니다. 참조 변수는 null 또는 객체의 주소를 값으로 갖으며 참조 변수의 초기화는 다음과 같이 합니다.

Date today = new Date();

객체를 생성하는 연산자 new의 결과는 생성된 객체의 주소입니다. 이 주소가 대입 연산자(=)에 의해서 참조 변수 today에 저장되는 것입니다.

참조형 변수는 null 또는 객체의 주소를 갖는데, 주소의 크기는 JVM이 32 bit면 4byte, 64 bit라면 8byte의 크기를 갖습니다.

 

기본형

 

기본형에는 모두 8개의 타입(자료형)이 있으며, 크게 논리형, 문자형, 정수형, 실수형으로 구분됩니다.

 

문자형(char)은 문자를 내부적으로 정수(유니코드)로 저장하기 때문에 정수형과 별반 다르지 않으며, 정수형 또는 실수형과 연산도 가능합니다. 반면에 논리형(boolean)은 다른 기본형과의 연산이 불가능합니다.

 

정수형(byte, short, int, long)은 가장 많이 사용되므로 4가지 타입을 제공합니다. 각 타입마다 저장할 수 있는 값의 범위가 다르므로 저장할 값의 범위에 맞는 타입을 선택하면 되지만, 일반적으로는 CPU가 가장 효율적으로 처리할 수 있는 int형을 많이 사용합니다.

 

각 타입의 변수가 저장할 수 있는 값의 범위와 크기는 다음과 같습니다.

실수형(float, double)은 정수형과 저장 형식이 달라서 같은 크기라도 훨씬 큰 값을 표현할 수 있으나 오차가 발생할 수 있다는 단점이 있습니다. 그래서 정밀도(precision)가 중요한데, 정밀도가 높을수록 발생하는 오차의 범위가 줄어듭니다. float는 정밀도가 7자리 밖에 되지 않으므로 보통 double을 많이 사용합니다.

 

상수와 리터럴

 

'상수(constant)'는 변수와 마찬가지로 값을 저장할 수 있는 공간이지만, 변수와 달리 한 번 값을 저장하면 다른 값으로 변경할 수 없습니다. 상수를 선언하는 방법은 변수와 동일하며, 단지 변수의 타입 앞에 키워드 'final'을 붙이면 됩니다.

final int MAX_SPEED = 10;

그리고 상수는 선언과 동시에 초기화하는 것이 보통이며, 상수의 이름은 모두 대문자로 하는 것이 암묵적인 관례입니다. 또한 상수명이 여러 단어로 이루어져있는 경우 '_'로 구분하는 것이 일반적입니다.

 

리터럴(literal)

원래 원래 12, 345, 'A' 와 같은 값들이 상수인데, 프로그래밍에서는 '값을 한 번 저장하면 변경할 수 없는 저장공간'으로 정의하였기 때문에 이와 구분하기 위하여 리터럴이라는 이름으로 부르게 되었습니다.

 

 

 

  • 변수(variable): 하나의 값을 저장하기 위한 공간
  • 상수(constant): 값을 한 번만 저장할 수 있는 공간
  • 리터럴(literal): 그 자체로 값을 의미

 

 

그렇다면 그냥 그 값 자체인 리터럴을 사용하면 되는데 왜 굳이 상수가 필요한 것일까요?

int rectangleArea = 20 * 10;

위 코드는 리터럴을 직접 사용해 사각형의 면적을 구해서 변수에 저장합니다.

final int WIDTH = 20;
final int HEIGHT = 10;

int rectangleArea = WIDTH * HEIGHT;

위 코드는 상수를 이용해서 기존의 코드를 변경한 것인데, 이전 코드에 비해 면적을 구하는 공식의 의미가 명확해졌습니다. 또한 너비와 높이가 변경될 때도 여러 곳을 수정할 필요없이 상수의 초기화만 수정해주면 됩니다.

 

이처럼 상수는 리터럴에 의미있는 이름을 붙여서 코드의 이해와 수정을 쉽게 만듭니다.

 

접두사, 접미사

변수에 타입이 있는 것처럼 리터럴에도 타입이 있습니다. 그 중 정수형과 실수형에는 여러 타입이 존재하므로, 리터럴에 접미사를 붙여서 타입을 구분합니다.

 

  • 정수형의 경우 long타입의 리터럴에 접미사 l(L)을 붙이고, 접미사가 없으면 int타입 리터럴입니다.
  • 실수형의 경우 float타입의 리터럴에 접미사 f(F)를 붙이고, double타입의 리터럴에 d(D)를 붙입니다.
    (실수형 기본 자료형인 double은 접미사를 생략할 수 있습니다.)
  • 2진수는 리터럴에 접두사 0b, 8진수는 리터럴에 접두사 0, 16진수는 리터럴에 접두사 0x를 붙입니다.

접미사 중 long타입의 접미사 l(L)은 숫자 1과 헷갈리기 쉬우므로 주로 대문자를 사용합니다.

 

그리고 JDK 7부터 정수형 리터럴의 중간에 구분자 '_'를 넣을 수 있어서 다음과 같이 큰 숫자를 편하게 읽을 수 있습니다.

long big = 100_000_000_000L;
long hex = 0xFFFF_FFFF_FFFF_FFFFL;

 

지역 변수 타입 추론 - var

JDK 10부터 지역 변수(local variable)의 경우, 변수를 선언할 때 타입 대신 'var'를 사용할 수 있게 되었습니다. 변수는 값을 담기 위한 것이고, 값의 타입과 변수의 타입이 일치하는 것이 보통이기 때문에 변수의 타입을 생략해도 컴파일러가 값의 타입을 보고 변수의 타입을 추론할 수 있는 것입니다.

int year = 2025;
var year = 2025;

LinkedHashMap map = new LinkedHashMap();
var map = new LinkedHashMap();

만일 변수를 선언할 때 값을 대입하지 않거나 null을 대입하면, 변수의 타입을 추론할 수 없기 때문에 에러가 발생합니다.

 

그리고 다음과 같은 경우 의도했던 것과 다르게 타입이 추론될 수 있어 조심해야 하며, 이럴 때는 var 대신 실제 타입을 적어주면 됩니다.

byte b = 123;
var b = 123;	// int형으로 추론

 

문자 리터럴과 문자열 리터럴

'A'와 같이 작은따옴표로 문자 하나를 감싼 것을 '문자 리터럴'이라고 합니다. 두 문자 이상은 큰따옴표로 감싸야 하며 '문자열 리터럴'이라고 합니다.

char ch = 'J';
String name = "JAVA";

char타입의 변수는 단 하나의 문자만 저장할 수 있으므로, 여러 문자(문자열)를 저장하기 위해서는 String타입을 사용해야 합니다.

 

그리고 덧셈 연산자(+)를 이용하여 문자열을 결합할 수도 있습니다.

String name = "JA" + "VA";	// JAVA
String str = name + 21;		// JAVA21

덧셈 연잔자는 피연산자가 모두 숫자일 때는 두 수를 더하지만, 피연산자 중 어느 한 쪽이 String이면 나머지 한 쪽을 먼저 String으로 변환한 다음 두 String을 결합합니다.

 

텍스트 블럭(text blocks)

JDK 15부터 다중행 문자열 리터럴(multiline string literal)을 작성할 수 있는 기능을 제공하며, '텍스트 블럭'이라고 부릅니다. 이 기능은 여러 줄로 이루어진 문자열을 작성하기 편리하게 도와줍니다. 이전에는 여러 줄로 이루어진 문자열을 작성하려면, 줄바꿈 문자를 사용해야 했습니다.

String str1 = "안녕하세요.\n"
		+ "반갑습니다.\n";

String str2 = """
            안녕하세요.
            반갑습니다.
            """;

그리고 텍스트 블럭을 사용할 때 주의할 점이 있습니다.

  • 텍스트 블럭을 여는 """의 다음 줄 부터 내용이 시작해야 됩니다.
  • 텍스트 블럭을 닫는 """의 위치에 따라 들여쓰기의 기준이 됩니다.

 

printf()와 변환문자

 

printf()는 지시자(specifier)를 통해 변수의 값을 여러 가지 형식으로 변환하여 출력하는 기능을 갖고 있습니다. '지시자'는 값을 어떻게 출력할 것인지를 지정해주는 역할을 합니다. 만약 출력하려는 값이 2개라면, 지시자도 2개를 사용해야하며, 출력될 값과 지시자의 순서는 일치해야 합니다. 

System.out.printf("age:%d%n", age);

println()과 달리 출력 후 자동 줄바꿈을 하지 않기 때문에, 줄바꿈을 하려면 지시자 '%n'을 따로 넣어줘야 합니다.

'%n' 은 포맷 지시자로 실행 중 플랫폼별 줄바꿈 문자('\n', '\r\n')로 치환되기 때문에 이식성을 보장합니다.

 

printf()의 지시자 중에서 자주 사용되는 것들에 대해 살펴보겠습니다.

추가로 지시자에 숫자를 추가하면 원하는 만큼의 출력 공간을 확보하거나 일부만 출력할 수 있습니다.

플래그에는 -(좌측 정렬), +(부호 출려), 공백, 콤마, 0, #(접두어) 등이 있습니다.

실수형 값을 변환문자 %f로 출력하는 경우 기본적으로 소수점 아래 6자리까지 출력됩니다.

 

진법

 

우리가 일상생활에서 주로 사용하는 것은 10진법입니다. 1946년 최초의 컴퓨터 애니악(ENIAC)은 사람에게 익숙한 10진법을 사용하도록 설계되었으나 전기회로는 전압이 불안정해서 전압을 10단계로 나누어 처리하는 데 한계가 있었습니다. 그래서 1950년에 개발된 애드박(EDVAC)은 단 두 가지 단계인, 전기가 흐르면 1, 흐르지 않으면 0 만으로 동작하도록 설계되었고 매우 성공적이었습니다.

그 이후로 지금까지 대부분의 컴퓨터는 2진 체계로 설계되었습니다.

 

앞서 변수에 값을 저장하면 10진수로 저장되었던 것이 아닌, 다음과 같이 2진수로 바뀌어 저장되었습니다.

 

비트(bit)와 바이트(byte)

 

한 자리의 2진수를 '비트(bit, binary digit)'라고 하며, 1 bit는 컴퓨터가 값을 저장할 수 있는 최소 단위입니다. 그러나 1bit는 너무 작은 단위이기 때문에 1bit 8개를 묶어서 '바이트(byte)'라는 단위로 정의해서 데이터의 기본 단위로 사용합니다.

이 외에도 '워드(word)'라는 단위가 있는데, word는 CPU가 한 번에 처리할 수 있는 데이터의 크기를 의미합니다. 이 word는 CPU의 성능에 따라 크기가 달라집니다. 32비트 CPU에서는 4byte를 의미하고, 64비트 CPU에서는 8byte를 의미합니다.

 

8진법과 16진법

 

2진법은 오직 0과 1로만 값을 표현하기 때문에, 2진법으로 값을 표현하면 자릿수가 상당히 길어진다는 단점이 있습니다. 이러한 단점을 보완하기 위해 2진법 대신 8진법이나 16법을 사용합니다. 8진법은 0~7의 숫자를 기호로 사용하고, 16진법은 0~9의 숫자와 추가로 A~F의 문자를 사용합니다.

 

8진수는 2진수 3자리를, 16진수는 2진수 4자리를 각각 한자리로 표현할 수 있기 때문에 자릿수가 짧아져서 알아보기 쉽고 서로 간의 변환 방법도 매우 간단합니다. 2진수를 8진수로 변환할 때는 뒤에서부터 3자리씩 끊어서 바꾸고, 16진수로 변환할 때는 뒤에서부터 4자리씩 끊어서 바꾸면 됩니다.

 

음수의 2진 표현 - 2의 보수법

 

앞서 살펴본 것과 같이 n비트의 2진수로 표현할 수 있는 값의 개수는 모두 2^n개입니다. 예를 들어 4비트의 2진수로는 모두 16개의 값을 표현할 수 있습니다. 이 16개의 값을 모두 부호 없는 정수(양수) 표현에 사용하면 0~15의 정수를 나타낼 수 있습니다. 그렇다면 양수와 음수를 모두 나타내려면 어떻게 해야할까요?

 

바로 4비트 2진수의 절반은 1로 시작하고 절반은 0으로 시작한다는 점을 활용하면 됩니다. 0으로 시작하는 수들을 양수, 1로 시작하는 수들을 음수로 표현할 수 있습니다. 이렇게 2진수로 음수를 표현하기 위해 정한 방법'2의 보수법'입니다.

 

2의 보수법

'2의 보수'란 어떤 2진수에 더했을 때 모든 자릿수가 0이 되고 자리 올림이 발생하게 만드는 값을 말합니다.

예를 들어 2진수 1101의 2의 보수는 0011입니다. 

 

2의 보수는 1의 보수를 사용하면 더 간단하게 구할 수 있습니다.

2의 보수 = 1의 보수 + 1

 

'1의 보수'란 0을 1로, 1을 0으로 바꾼 수로, 어떤 2진수의 1의 보수를 구하고 1을 더하면 2의 보수가 됩니다.

 

이렇게 더했을 때 모든 비트가 0이 되는 2의 보수 표기법을 사용하면 음수를 이진수로 표현할 수 있습니다.

4비트 기준에서 13(1101)과 더해 하위 비트가 모두 0이 되게 만드는 수, 즉 −13은 1101의 2의 보수인 0011로 표현할 수 있습니다.

 

기본형(primitive type)

 

앞서 간단하게 살펴보았던 기본형의 보다 세부적인 내용에 대해 살펴보겠습니다.

 

논리형 - boolean

 

논리형에는 'boolean' 한 가지 밖에 없습니다. boolean형 변수에는 true와 false 중 하나를 저장할 수 있으며 기본값(default)은 false입니다. boolean형 변수는 대답(yes/no), 스위치(on/off) 등의 논리구현에 주로 사용됩니다. boolean형은 true, false 두 가지 값만 표현하면 되므로 1 bit 만으로도 충분하지만, 자바에서는 데이터를 다루는 최소 단위가 byte이기 때문에, boolean의 크기는 1byte입니다.

또한 자바에서는 대소문자가 구별되기 때문에 TRUE와 true는 다른 것으로 간주됩니다.

 

문자형 - char

 

문자형 역시 'char' 한 가지 밖에 없습니다. 문자를 저장하기 위한 변수를 선언할 때 사용되며 char타입의 변수는 단 하나의 문자만을 저장할 수 있습니다. char형 변수에 값을 저장할 때 '문자'가 저장되는 것 같지만, 사실은 문자가 아닌 문자의 유니코드(정수)가 저장됩니다. 컴퓨터는 숫자밖에 모르기 때문에 모든 데이터를 숫자로 변환하여 저장하는 것입니다.

char ch = 'A';
char ch = 65;

위 코드에서 문자 'A'의 유니코드는 65이므로, 실제로 변수 ch에는 65가 저장됩니다.

 

int code = (int)ch;

그래서 문자 리터럴 대신 문자의 유니코드를 직접 저장할 수도 있습니다. 만일 어떤 문자의 유니코드를 알고 싶다면, char형 변수에 저장된 값을 정수형(int)으로 변환하면 됩니다. 어떤 타입(자료형)을 다른 타입으로 변환하는 것을 형변환(casting)이라고 하는데, 형변환에 대해서는 아래에서 자세히 알아보겠습니다.

 

특수 문자

영문자 이외에 tab이나 backspace 등의 특수문자를 저장하려면, \(backslash)를 사용하면 됩니다.

  • tab: \t
  • backspace: \b
  • form feed: \f
  • carriage return: \r
  • 역슬래쉬: \\
  • 작은따옴표: \'
  • 큰따옴표: \"
  • 유니코드: \u

 

char의 저장 형식

char타입의 크기는 2byte(=16bit)이므로, 16자리의 2진수로 표현할 수 있는 정수의 개수인 65526(=2^16)개의 코드를 사용할 수 있으모, char형 변수는 이 범위 내의 코드 중 하나를 저장할 수 있습니다. 예를 들어 문자 'A'를 저장하면, 2진수 '00000000 10000001'로 저장됩니다.

 

char타입은 문자를 저장할 변수를 선언하기 위한 것이지만, 실제로 char타입의 변수에 문자가 아닌 '문자의 유니코드'가 저장되고 저장 형식 역시 정수형과 동일합니다. 다만, 정수형과 달리 음수가 필요없으므로 저장할 수 있는 값의 범위가 다릅니다. 자료형 중 short 또한 2byte의 크기를 갖고 있으나, 절반은 음수에 사용하므로 범위가 다릅니다.

 

char ch = 65;
short s = 65;

예를 들어 위와 같이 변수 ch와 s에 65를 저장하면, 둘 다 2진수로 똑같은 값이 저장됩니다. 그러나 두 변수의 값을 출력해보면 결과는 다르게 나옵니다. 

System.out.println(ch);		// 'A'
System.out.println(s);		// 65

이처럼 값은 어떻게 해석하냐에 따라 결과가 달라지므로 값만으로는 값을 해석할 수 없습니다. 값의 타입까지 알아야 올바르게 해석할 수 있는 것입니다.

 

인코딩과 디코딩

문자를 숫자로 변환할 때 사용되는 기준을 '유니코드(unicode)'라고 합니다.

문자를 코드로 변환하는 것을 '인코딩(encoding)', 그 반대로 코드를 문자로 변환하는 것을 '디코딩(decoding)'이라고 하며, 문자를 저장할 때는 인코딩을 해서 숫자로 변환해서 저장하고, 저장된 문자를 읽어올 때는 디코딩을 해서 숫자를 원래의 문자로 되돌려야 합니다.

 

당연한 얘기지만 어떻게 인코딩을 했는지를 알아야 디코딩이 가능합니다. 만일 인코딩에 사용된 코드표와 디코딩에 사용된 코드표가 다르면 엉뚱한 글자들로 바뀌어 나타날 것입니다.

 

아스키(ASCII)

'ASCII'는 'American Standard Code for Information Interchange'의 약어로 정보 교환을 위한 미국 표준 코드라는 뜻입니다. 아스키는 128(=2^7)개의 문자 집합(character set)을 제공하는 7bit 부호로, 처음 32개의 문자는 인쇄와 전송 제어용으로 사용되는 제어문자로 출력할 수 없고, 마지막 문자(DEL)을 제외한 33번째 이후의 문자들은 출력할 수 있는 문자들로, 기호와 숫자, 영대소문자로 이루어져 있습니다. 아스키는 숫자 '0~9', 영대문자 'A~Z', 영소문자 'a~z'가 연속적으로 배치되어 있다는 특징이 있습니다.

 

다음과 같이 아스키 코드의 특성을 활용할 수 있습니다.

char ch = (char)(1 + '0');		// '1'

int index = 'H' - 'A';			// 7

1행에서 숫자 1과 문자 '0'을 더하면 문자 '1'이 됩니다. 숫자와 문자의 더하기 연산의 경우 int형으로 자동 변환되어 처리되기 때문에, '0'의 아스키 코드인 48과 숫자 1을 더해 문자 '1'의 아스키 코드인 49가 되는데, 이를 char 타입으로 형변환하게 되면 문자 '1'이 됩니다.

또한 2행과 같이알파벳을 'a', 'A'로 빼기 연산을 하게되면 알파벳의 인덱스 값을 얻을 수 있습니다.

 

확장 아스키

일반적으로 데이터는 byte단위로 다뤄지는데 아스키는 7bit이므로 1bit가 남습니다. 이 남는 공간을 활용해서 문자를 추가로 정의한 것이 '확장 아스키'입니다. 확장 아스키에 추가된 128개의 문자는 여러 국가와 기업에서 서로의 필요에 따라 다르게 정의해서 사용합니다. 'ISO(국제표준화기구)'에서 확장 아스키의 표준을 몇 가지 발표했는데, 그 중에서 대표적인 것이 'ISO 8859-1' 입니다. 이 확장 아스키 버전은 'ISO Latin 1'이라고도 하는데 서유럽에서 일반적으로 사용하는 문자들을 포함하고 있습니다.

 

확장 아스키로도 표현할 수 있는 문자의 개수가 255개뿐이므로 한글을 표현하기에는 턱없이 부족합니다. 그래서 생각한 방법이 두 개의 문자코드로 한글을 표현하는 방법이었습니다. 한글을 표현하는 방법은 조합형과 완성형이 있는데, 조합형은 초성, 중성, 종송을 조합하는 방식이고, 완성형은 확장 아스키의 일부 영역에 해당하는 두 문자코드를 조합하여 한글을 표현하는 방식입니다.

 

현재 조합형은 사용되지 않고 완성형에 없는 8822글자를 추가한 '확장 완성형'인 'CP 949'가 사용되는데, 이것이 바로 한글 윈도우에서 사용하는 문자 인코딩입니다.

 

코드 페이지

IBM이 자사의 PC에 '확장 아스키'를 도입해서 사용하기 시작할 때, PC를 사용하는 지역이나 국가에 따라 여러 버전의 '확장 아스키'가 필요했습니다. IBM은 이들을 '코드 페이지(code page)'라 하고, 각 코드 페이지에 'CP xxx'와 같은 형식으로 이름을 붙였습니다. 한글 윈도우는 'CP 949'를, 영문 윈도우는 'CP 437'을 사용합니다.

 

유니코드

예전에는 같은 지역 내에서만 문서 교환이 주를 이뤘지만, 인터넷이 발명되면서 서로 다른 지역의 다른 언어를 사용하는 컴퓨터간의 문서 교환이 활발해지기 시작하자 서로 다른 문자 인코딩을 사용하는 컴퓨터간의 문서 교환에 어려움을 겪게 되었습니다. 이러한 어려움을 해소하고자 전 세계의 모든 문자를 하나의 통일된 문자 집합으로 표현하고자 노력하였고 그 결과가 바로 '유니 코드'입니다.

 

유니코드는 처음엔 16bit 고정폭으로 설계되었지만 범위가 부족해 현재는 U+0000~U+10FFFF(약 21bit)를 사용합니다. 이 중 U+10000~U+10FFFF보충 문자라고 합니다.

 

유니코드는 먼저 유니코드에 포함시키고자 하는 문자들의 집합을 정의하였는데, 이것을 유니코드 문자 집합(character set)이라고 합니다. 그리고 이 문자 집합에 번호를 붙인 것이 유니코드 인코딩입니다. 그리고 유니코드 문자 집합에 순서대로 붙인 일련번호가 코드 포인트(code point)입니다. 유니코드 인코딩에는 UTF-8, UTF-16, UTF-32등 여러가지 종류가 있는데, 자바에서는 UTF-16을 사용해오다가 JDK 18부터 UTF-8로 바뀌었습니다.

 

UTF-16은 모든 문자를 2byte의 고정크기로 표현하고, UTF-8은 하나의 문자를 1~4byte의 가변크기로 표현합니다. 두 인코딩 모두 처음 128문자는 아스키 코드와 동일합니다. 

 

모든 문자의 크기가 동일한 UTF-16이 문자를 다루기는 편리하지만, UTF-8에서는 1 byte로 표현할 수 있는 영어와 숫자도 2 byte로 표현되므로 문서의 크기가 커진다는 단점이 있습니다. 반면 UTF-8은 문서의 크기는 작지만, 문자의 크기가 가변적이므로 다루기 어렵다는 단점이 있습니다. 인터넷에서는 전송 속도가 중요하므로, 문서의 크기가 작을수록 유리해 UTF-8 인코딩으로 작성된 웹문서의 수가 빠르게 늘고 있습니다.

유니코드 인코딩은 파일이나 네트워크 입출력에서 사용되는 개념이고, 메모리에 저장될 때는 사용되지 않습니다.
자바의 경우 char형은 16비트이고, 이 자료형에 저장하기 위해 모든 문자는 UTF-16 코드 유닛으로 저장됩니다.

 

정수형 - byte, short, int, long

 

정수형에는 모두 4개의 자료형이 있으며, 각 자료형이 저장할 수 있는 값이 범위가 서로 다릅니다. 크기 순으로 나열하면 byte(1) < short(2) < int(4) < long(8) 입니다. 이 중에서 정수형 기본 자료형은 int입니다.

 

정수형의 저장 형식

어떤 진법의 리터럴을 변수에 저장해도 실제로는 2진수로 바뀌어 저장됩니다. 이 2진수가 저장되는 형식은 크게 정수형과 실수형이 있으며, 정수형은 다음과 같은 형식으로 저장됩니다.

모든 정수형은 부호 있는 정수이므로 왼쪽의 첫 번째 비트를 '부호 비트(sign bit)'로 사용하고, 나머지는 값을 표현하는데 사용합니다.

 

정수형의 선택 기준

변수에 저장하려는 정수값의 범위에 따라 4개의 정수형 중에서 하나를 선택하면 되겠지만, byte나 short보다 int를 사용하는 것이 좋습니다. byte와 short가 크기는 더 작아서 메모리를 조금 더 절약할 수는 있지만, 저장할 수 있는 값의 범위가 작은 편이라서 연산 시에 범위를 넘어서 잘못된 결과를 얻기 쉽습니다.

 

그리고 JVM의 피연산자 스택(operand stack)이 피연산자를 4 byte단위로 저장하기 때문에 크기가 4 byte보다 작은 자료형의 값을 계산할 때는 4 byte로 변환하여 연산이 수행됩니다. 그래서 오히려 int를 사용하는 것이 더 효율적입니다.

 

정수형의 오버플로우

연산과정에서 해당 타입이 표현할 수 있는 값의 범위를 넘어서는 것을 오버플로우(overflow)라고 합니다. 오버플로우가 발생했다고 에러가 나는 것은 아니지만, 예상치 못한 결과를 얻을 수 있습니다. 따라서 애초에 오버플로우가 발생하지 않도록 충분한 크기의 타입을 선택해서 사용해야 합니다.

 

예를 들어 4 bit 2진수의 최댓값에 덧셈을 하거나, 최솟값에 뺄셈을 하면 다음과 같은 결과 나오게 됩니다.

덧셈의 경우 올림이 발생하지만 4 bit로는 4자리만 표현 가능하기 때문에 올림은 버려집니다. 뺄셈의 경우도 5자리라는 가정하에 연산이 수행되지만 4bit이기 때문에 4자리만 표현됩니다. 즉, 최댓값 + 1 = 최솟값 이고, 최솟값 - 1 = 최댓값입니다.

 

오버플로우는 부호가 없는 정수와, 부호가 있는 정수에서 다르게 발생합니다.

부호 없는 정수의 최댓값은 15, 최솟값은 0이고, 부호 있는 정수의 최댓값은 7, 최솟값은 -8입니다. 따라서 부호 없는 정수의 경우 15와 0에서 오버플로우가 발생하고, 부호 있는 정수의 경우 7과 -8에서 오버플로우가 발생합니다.

 

byte, short, int, long (부호 있는 정수)에서와 char (부호 없는 정수)에서 위 그림과 동일한 방식으로 오버플로우가 발생합니다.

 

실수형 - float, double

 

실수형은 실수를 저장하기 위한 타입으로 float와 double 두 가지가 있으며 각 타입에 저장할 수 있는 값의 범위는 다음과 같습니다.

  • float: 1.4 x 10^45  ~  3.4 x 10^38
  • double: 4.9 x 10^-324  ~  1.8 x 10^308

위 범위는 '양의 범위'만 표현한 것으로, 이 범위에 - 부호를 붙이면 음의 범위가 됩니다. 실수형은 소수점도 표현해야 하므로 얼마나 큰 값을 표현할 수 있는가 뿐만 아니라 얼마나 0에 가깝게 표현할 수 있는가도 중요합니다.

실수형에서는 오버플로우가 발생해 최댓값을 벗어나면 무한대(infinity)가 됩니다.

 

int형과 동일하게 4 byte지만 더 큰 값을 표현할 수 있는 이유는 바로 값을 저장하는 형식이 다르기 때문입니다.

int타입은 부호와 값, 두 부분으로 이루어져 있지만, float타입과 같은 실수형은 부호(S), 지수(E), 가수(M), 세 부분으로 이루어져 있습니다. 즉, 2의 제곱을 곱한 형태(M x 2^E)로 저장하기 때문에 이렇게 큰 범위의 값을 저장하는 것이 가능한 것입니다.

 

그러나 정수형과 달리 실수형은 오차가 발생할 수 있다는 단점이 있습니다. 그래서 실수형에는 표현할 수 있는 값의 범위뿐만 아니라 '정밀도(presicion)'도 중요한 요소입니다. float타입의 정밀도는 7자리이고, double타입의 정밀도는 15자리입니다.

 

실수형의 저장 형식

앞서 본 것과 같이 실수형은 정수형과 달라서, 부동 소수점의 형태로 저장합니다. 부동 소수점은 실수를 '+-Mx2^E'와 같은 형태로 표현하는 것을 말하며, 부호(Sign), 지수(Exponent), 가수(Mantissa)로 이루어져 있습니다.

S는 부호 비트로 0이면 양수, 1이면 음수를 의미합니다. 그러나 정수형과 달리 2의 보수법을 사용하지 않기 때문에 양의 실수를 음의 실수로 변경할 때 그저 부호 비트만 1로 변경하면 됩니다.

 

E는 지수를 저장하는 부분으로 float의 경우, 8 bit의 저장 공간을 갖습니다. 지수는 부호 있는 정수로 -127 ~ 128의 값을 갖지만 이 중 -127과 128은 음의 무한대, 양의 무한대를 의미하기 때문에 실제로 사용하는한 지수의 범위는 -126 ~ 127입니다.

 

M은 실제 값은 가수를 저장하는 부분으로 float의 경우, 2진수 23자리를 저장할 수 있습니다. 2진수 23자리로는 약 7자리의 10진수를 저장할 수 있는데, 이것이 바로 float의 정밀도가 되는 것입니다. double은 가수를 저장할 수 있는 공간이 52자리로 float보다 약 2배이므로, double이 float보다 약 2배의 정밀도를 갖는 것입니다.

 

부동 소수점의 오차

실수 중에는 '파이(π=3.141592...)'와 같은 무한 소수가 존재하므로, 정수와 달리 실수를 저장할 때는 오차가 발생할 수 있습니다. 게다가 10진수가 아닌 2진수로 저장하기 때문에 10진수로는 유한 소수이더라도, 2진수로 변환하면 무한 소수가 되는 경우도 있습니다. 또한 2진수로 유한 소수이더라도, 가수를 저장할 수 있는 자리수가 한정되어 있으므로 저장되지 못하고 버려지는 값들이 있으면 오차가 발생합니다.

 

2진수로 변환된 실수를 저장할 때는 먼저 '1.xxx X 2^n' 의 형태로 변환하는데, 이 과정을 정규화라고 합니다. 정규화된 2진 실수는 항상 '1.'으로 시작하기 때문에 1을 제외한 23자리의 2진수가 가수(M)에 저장되고 그 이후는 잘려나갑니다. 그리고 지수(E)는 기저법으로 저장되기 때문에 지수에 기저인 127을 더한 값이 2진수로 변환되어 저장됩니다.

 

이때 잘려나간 값들에 의해 발생할 수 있는 최대 오차는 약 2^-23인데, 이 값은 가수(M)의 마지막 비트의 단위와 같습니다.

 

형변환

 

프로그램을 작성하다 보면 서로 다른 타입간의 연산을 수행해야 하는 경우가 있는데, 이럴 때는 연산을 수행하기 전에 타입을 일치시켜야 합니다. 이렇게 변수나 리터럴의 타입을 다른 타입으로 변환하는 것을 '형변환(casting)'이라고 합니다.

 

형변환 방법

 

(타입)피연산자

형변환은 위와 같이 형변환하고자 하는 변수나 리터럴의 앞에 변환하고자 하는 타입을 괄호와 함께 붙여주기만 하면 됩니다. 여기에 사용되는 괄호( )는 '캐스트 연산자' 또는 '형변환 연산자'라고 합니다.

 

기본형에서 boolean을 제외한 나머지 타입들은 서로 형변환이 가능합니다. 

 

정수형 간의 형변환

 

큰 타입에서 작은 타입으로 변환하는 경우 크기의 차이만큼 잘려나갑니다. 그래서 경우에 따라 값 손실이 발생할 수 있습니다.

 

반대로 작은 타입에서 큰 타입으로 변환하는 경우 값 손실이 없고, 나머지 빈 공간은 2의 보수법에 의해 양수는 0, 음수는 1로 채워집니다.

 

실수형 간의 형변환

 

실수형에서도 정수형처럼 큰 타입에서 작은 타입으로 변환하는 경우 나머지 자리는 버려집니다.

 

반대로 작은 타입에서 큰 타입으로 변환하는 경우, 빈 공간을 0으로 채웁니다.

 

정수형과 실수형 간의 형변환

 

정수형과 실수형은 저장 형식이 완전히 다르기 때문에 정수형 간의 변환처럼 간단히 값을 채우고 자르는 식으로 할 수 없습니다.

 

정수형 -> 실수형

정수는 소수점이하의 값이 없으므로 비교적 변환이 간단합니다. 그저 정수를 2진수로 변환한 다음 정규화를 거쳐 실수의 저장 형식으로 저장될 뿐입니다. 또한 실수형은 정수형보다 훨씬 큰 저장 범위를 갖기 때문에, 정수형을 실수형으로 변환하는 것은 별 무리가 없습니다.

 

다만 실수형의 정밀도의 제한으로 인한 오차가 발생할 수는 있습니다. 예를 들어 int의 최댓값은 약 21억으로 최대 10자리의 정밀도를 요구합니다. 그러나 float는 10진수로 약 7자리의 정밀도만을 제공하므로, Int를 float로 변환할 때 정밀도 차이에 의한 오차가 발생할 수 있습니다. 따라서 10진수로 8자리 이상의 값을 실수형으로 변환할 때는 double로 형변환해야 오차가 발생하지 않습니다.

 

실수형 -> 정수형

실수형을 정수형으로 변환하면, 실수형의 소수점이하 값은 버려집니다. 그래서 실수형을 정수형으로 형변환할 때 반올림은 발생하지 않습니다. 예를 들어 float타입의 상수 9.1234567f을 int타입으로 형변환하면 9가 됩니다.

 

만일 실수의 소수점을 버리고 남은 정수가 정수형의 저장 범위를 넘는 경우에는 정수의 오버플로우가 발생한 결과를 얻습니다.

 

자동 형변환

 

서로 다른 타입 간의 대입이나 연산을 할 때, 형변환으로 타입을 일치시키는 것이 원칙입니다. 하지만, 경우에 따라 편의상의 이유로 형변환을 생략할 수 있습니다. 그렇다고 해서 형변환되지 않는 것은 아니고, 컴파일러가 생략된 형변환을 자동으로 추가합니다.

다음 두 가지 예시를 살펴보겠습니다.

float f = 1234;			// float f = (float)1234;

int i = 3;
double d = 1.0 + i;		// double d = 1.0 + (double)i;

1행에서 우변은 int타입의 상수이고, 이 값을 저장하려는 변수의 타입은 float입니다. 이 경우 float타입의 변수는 1234라는 값을 저장하는데 아무런 문제가 없기 때문에 형변환 생략이 가능합니다.

 

3행에서 서로 다른 두 타입의 연산이지만 형변환이 생략되었습니다. 이 경우 두 타입중 표현 범위가 더 넓은 타입으로 자동 형변환되어 타입을 일치시킨 다음에 연산을 수행합니다.

 

자동 형변환 규칙

형변환을 생략하면 컴파일러가 알아서 자동으로 형변환을 하는 기준은 기존의 값을 최대한 보존할 수 있는 타입으로 자동 형변환합니다.

표현 범위가 좁은 타입에서 더 넓은 타입으로 형변환하는 경우에는 값 손실이 없으므로 두 타입 중에서 표현 범위가 더 넓은 똑으로 형변환됩니다. 다음은 자동 형변환이 되는 방향을 그림으로 나타낸 것입니다.

그림에서 왼쪽에서 오른쪽으로의 변환은 자동 형변환이 되며, 그 반대 방향으로의 변환은 반드시 형변환 연산자를 사용해야 합니다.

 

보통 자료형의 크기가 큰 것일수록 값의 표현 범위가 크기 마련이지만, 실수형은 정수형과 값을 표현하는 방식이 다르기 때문에 같은 크기일지라도 실수형이 정수형보다 훨씬 더 큰 표현 범위를 갖기 때문에 long보다 float가 오른쪽에 위치합니다.

 

그리고 short와 char는 둘 다 2 byte로 크기가 같지만, char는 부호 없는 정수를 다루고, short는 부호 있는 정수를 다루기 때문에 서로 범위가 달라서 둘 중 어느쪽으로의 형변환도 값 손실이 발생할 수 있으므로 자동 형변환이 수행될 수 없습니다.

'Lang > Java' 카테고리의 다른 글

[Java 21] (6) - Object-oriented Programming 1  (0) 2025.09.29
[Java 21] (5) - array  (1) 2025.09.25
[Java 21] (4) - statement  (0) 2025.09.22
[Java 21] (3) - operator  (0) 2025.09.18
[Java 21] (1) - getting started with Java  (0) 2025.09.11