2022-01-31 작성

자바의 데이터 타입(Primitive type, Reference type)

타입(Data type)이란 해당 데이터가 메모리에 어떻게 저장되고, 프로그램에서 어떻게 처리되어야 하는지를 명시적으로 알려주는 것이다. 자바에서 타입은 크게 기본형 타입과 참조형 타입이 있다.

기본형 타입(Primitive type)

기본형 타입은 아래 표와 같이 총 8개의 타입들이 존재한다. 자바에서 미리 형식을 정의하여 제공하고 있으며, 기본값이 존재하기 때문에 Null 개념이 존재하지 않으므로 만약 기본형 타입에 Null을 넣고 싶다면 래퍼 클래스를 활용하면 된다.

기본형 타입은 실제 값을 저장하는 공간으로, 스택(Stack) 메모리에 저장된다. 주로 문법상의 에러(예를 들어 ;을 안붙였다든지)로 빨간 줄이 쳐지는 경우처럼 컴파일 시점에 담을 수 있는 크기를 벗어나면 에러를 발생시키는 컴파일 에러가 발생한다. 

   타입  할당되는 메모리 크기  기본값  데이터의 표현 범위
 논리형  boolean  1 byte  false  true, false
 정수형  byte  1 byte  0  -128 ~ 127
 short  2 byte  0  -32,768 ~ 32,767
 int(기본)  4 byte   0  -2,147,483,648 ~ 2,147,483,647
 long  8 byte  0L  -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
 실수형  float  4 byte  0.0F  (3.4 X 10-38) ~ (3.4 X 1038) 의 근사값
 double(기본)  8 byte  0.0  (1.7 X 10-308) ~ (1.7 X 10308) 의 근사값
 문자형  char  2 byte (유니코드)  '\u0000'   0 ~ 65,535

참조형 타입(Reference type)

참조형 타입은 기본형 타입을 제외한 모든 타입들을 말한다. 참조형 타입은 기본형 타입과 달리 빈 객체를 의미하는 Null 개념이 존재한다. 

참조형 타입은 값이 저장되어 있는 곳의 주소값을 저장하는 공간으로, 힙(Heap) 메모리에 저장된다. 문법상으로 에러가 없지만, 실행시 에러가 나는 런타임 에러가 발생한다. 예를 들어, 객체나 배열을 Null 값으로 받으면 NullPointException 에러가 발생하므로 그에 대한 널처리를 하는 등의 조치가 필요핟다.

 타입  예시  기본값 할당되는 메모리 크기 
 배열(Array)  int[] arr = new int[5];  Null  4 byte (객체의 주소값)
 열거(Enumeration)    Null
 클래스(Class)  String str = "test";
 Student sujin = new Student();
 Null
 인터페이스(Interface)     Null

주의할 점

1) 정수형 타입 사용할 땐 오버플로우/언더플로우 주의

정수형 타입을 사용할 때에는 사용하려는 데이터의 최대/최소 표현 범위를 고려해야 한다. 만약 표현할 수 있는 범위를 벗어난 데이터를 저장하면 오버플로우(overflow) 또는 언더플로우(underflow)가 발생하면서 엉뚱한 값이 저장될 수 있다. 오버플로우와 언더플로우의 뜻은 다음과 같다.

  • 오버플로우 : 해당 타입이 표현할 수 있는 '최대 표현 범위'보다 큰 수를 저장할 때 발생하는 현상
  • 언더플로우 : 해당 타입이 표현할 수 있는 '최소 표현 범위'보다 작은 수를 저장할 때 발생하는 현상

예를 들어보자. 정수형 타입 중 byte의 데이터 표현 범위는 -128 ~ 127 이다. (위의 표 참조) 만약 이 범위를 벗어나는 데이터를 입력하면 오버플로우 또는 언더플로우가 발생하게 되므로 주의하자.

byte max = 127;
byte min = -128;

System.out.println(++max); // 출력 결과는 -128 으로 오버플로우가 발생하여 잘못된 결과가 나온다.
System.out.println(--min); // 출력 결과는 127 으로 언더플로우가 발생하여 잘못된 결과가 나온다.

2) 실수형 타입 사용할 땐 유효한 자릿수를 고려할 것

실수형 타입을 사용할 때에는 표현 범위 말고도 유효 자릿수를 고려해야 한다.보통 컴퓨터에서 실수를 표현할 때에는 부동 소수점 방식을 이용한다.

  • 고정 소수점 방식 : 실수를 정수부와 소수부로 나눈다. 소수부의 자릿수를 미리 정하고 고정된 자릿수의 소수를 표현하기 때문에 표현 가능한 범위가 매우 적다.
  • 부동 소수점 방식 : 실수를 가수부와 지수부로 나눈다. 매우 큰 실수까지도 표현할 수 있지만 10진수를 정확하게 표현할 수는 없게 되므로 오차가 필연적으로 발생한다. 현재 대부분의 시스템에서 실수를 표현할 때 사용하는 방식이며 IEEE 754 표준을 따르고 있다.

실수형 타입인 float과 double의 유효 자릿수는 다음과 같다. 

실수형 타입  지수의 길이  가수의 길이  유효 자릿수
 float  8 bit  23 bit  소수점 약 6자리까지 높은 확률로 정확히 표현
 double  11 bit  52 bit  소수점 약 15자리까지 높은 확률로 정확히 표현

예를 들어보자. 

double num = 0.1;

for (int i = 0; i < 1000; i++) {
    num += 0.1;
}

System.out.println(num);          // 출력 결과는 100.09999999999859 으로 작은 오차가 발생한다.
System.out.println(0.123456789f); // 출력 결과는 0.12345679 으로 약 6자리까지 정확히 표현된다.
System.out.println(0.123456789d); // 출력 결과는 0.123456789 으로 약 15자리까지 정확히 표현된다.

3) 정확한 계산이 필요할 경우

실수형 타입은 큰 범위까지 쓸수는 있지만 오차가 필연적으로 발생한다고 했다. 그렇다면 돈 계산처럼 정확한 계산이 필요할 경우에는 어떻게 해야할까? 결론부터 말하자면  int, long 정수형 타입을 사용하거나 BigDecimal 클래스를 이용하면 된다.

데이터의 표현 범위가 제한되어 있기 때문에 크기가 9자리를 넘지 않으면 int 타입을 사용할 수 있고, 18자리를 넘지 않으면 long 타입을 사용할 수 있고, 18자리를 초과하면 BigDecimal 클래스를 사용해야 한다.

BigDecimal를 사용할 경우, 정확한 연산을 제공하지만 기본형 타입보다 사용하기 불편하고 실행 속도가 느려진다. 사용의 불편함과 기본형 타입을 사용하지 않는데 따른 비용(cost)을 개의치 않는다면 사용하자.

BigDecimal를 사용하지 않을 경우, int, long 타입으로 소숫점을 없앤 형태로 계산해야 한다. 예를 들어 25.35 유로는 2535 센트로 치환해서 계산하고, 보여줄 때만 25.35 로 보여주는 것이다. 따라서 성능이 중요하고 소수점을 우리가 직접 계산하고 유지해도 괜찮으며, 너무 큰 수를 다루는 것이 아니라면 사용하자.

4) 문자형 타입의 기본 : 아스키코드, 유니코드

컴퓨터는 2진수밖에 인식하지 못하므로 문자도 숫자로 표현해야 한다. 따라서 어떤 문자를 어떤 숫자에 대응시킬 것인가에 대한 약속이 필요하다. 문자를 표현시 아스키코드와 유니코드를 사용한다.

C언어는 아스키코드(ASCII)를 사용하여 문자를 표현하고 있다. 아스키코드는 문자 하나를 7비트로 표현하므로, 총 128개의 문자를 표현할 수 있다. 예를 들어 영문자, 숫자, 특수문자 등을 표현할 수 있다.

자바는 유니코드(Unicode)를 사용하여 문자를 표현하고 있다. 아스키코드는 영문자와 숫자밖에 표현 못 하지만, 유니코드는 각 나라의 모든 언어를 표현할 수 있다. 유니코드는 문자 하나를 16비트로 표현하므로, 총 65,536개의 문자를 표현할 수 있으므로 표현범위가 훨씬 넓다. 예를 들어 영문자, 숫자, 특수문자 뿐만 아니라 한글, 한자와 같이 복잡한 언어를 표현한다.

System.out.println((char) 65);         // 출력 결과 : A (아스키코드로 'A'를 의미)
System.out.println((char) 0x41);       // 출력 결과 : A (유니코드로 'A'를 의미)
System.out.println((char) 0xAC00);     // 출력 결과 : 가 (유니코드로 '가'를 의미)
System.out.println((char) 0x91D1);     // 출력 결과 : 金 (유니코드로 '金'를 의미)

5) 매우 큰 범위의 정수형 타입이 필요할 경우 : BigInteger 클래스

BigInteger 클래스는 Big이라는 단어에 걸맞게 거의 무한한 크기의 정수형 숫자를 다룰 수 있다. 앞서 말했듯이 long 타입이 수용할 수 있는 범위보다 더 큰 숫자가 필요하다면 BigInteger 클래스를 이용할 수 있다. 기본적인 사칙연산을 수행할 때 아래와 같은 메서드를 사용하면 된다.

  • add
  • subtract
  • multiply
  • divide
  • mod
BigInteger b1 = new BigInteger("10");
System.out.println(b1); // 출력 결과 : 10

BigInteger b2 = b1.add(BigInteger.valueOf(100));
System.out.println(b2); // 출력 결과 : 10 + 100 = 110

BigInteger b3 = b2.add(b);
System.out.println(b3); // 출력 결과 : 110 + 10 = 120

6) 매우 큰 범위의 실수형 타입이 필요할 경우 : BigDecimal 클래스

BigDecimal 클래스는 무한한 크기의 부동 소수점 숫자를 다룰 수 있다. 기본적인 사칙연산을 수행할 때 아래와 같은 메서드를 사용하면 된다.

  • add
  • subtract
  • multiply
  • divide
  • divide
BigDecimal b1 = new BigDecimal("0.5");
BigDecimal b2 = new BigDecimal("0.4");

System.out.println(b1.add(BigDecimal.valueOf(0.2)));  // 출력 결과 : 0.5 + 0.2 = 0.7
System.out.println(b1.multiply(b2));  // 출력 결과 : 0.5 x 0.4 = 0.20
System.out.println(b1.divide(b2, BigDecimal.ROUND_UP));  // 출력 결과 : 0.5 ÷ 0.4 = 1.25이고 ROUND_UP하여 1.3
System.out.println(b1.divide(b2, 4, BigDecimal.ROUND_UP)); // 출력 결과 : 0.5 ÷ 0.4 = 1.25이고 scale이 4이므로 1.2500