프로그래밍 언어에는 크게 두가지 타입이 있습니다.

1. 정적인 언어 : 컴파일 때 이미 변수에 대한 데이터 타입이 확정되어 다른 데이터 타입을 할당할 수 없는 언어입니다.

예를들어 C, C++, JAVA와 같은 언어가 여기에 속합니다.

2. 동적인 언어 : 여기 언어들은 변수에 다른 데이터 타입을 할당할 수 있습니다. Ruby, Python 등의 언어가 있죠.

 

자바는 정적인 언어이기 때문에 변수를 사용하려면 데이터 타입을 꼭 지정해줘야 합니다. 자바에서 사용되는 타입은 다음과 같습니다.

https://www.geeksforgeeks.org/data-types-in-java/?ref=lbp

자바의 데이터 타입은 크게 두 가지 Primitive 와 Non-Primitive 타입으로 나눌 수 있습니다. 

- Primitive Data Type : boolean, char, int, short, byte, long, float, double

- Non-Primitive Type : String, Array 등


Primitive Data Type

Primitive 데이터는 오직 한 값만 가지며 다른 특수한 기능이 없습니다. 그 값 자체라고 생각하면 됩니다.

boolean, byte, short, int, long, float, double 이렇게 8가지 데이터 타입이 있으며 하나씩 살펴보겠습니다.

 

먼저 각 자료형마다 표현할 수 있는 범위가 정해져 있습니다. boolean은 1bit, byte는 8bit 처럼 말이죠.

여기서 1bit는 0 또는 1로 표현되는 한자리 값입니다. 즉, 2^1 = 2 가지 값을 표현할 수 있습니다.

마찬가지로 1byte(= 8bit)는 2진수로 00000000 ~ 11111111 사이의 값을 가지며 2^8 = 256 가지 값을 표현합니다.

데이터의 표현 범위는 여기까지 하고 한 자료형씩 보겠습니다.

 

1. boolean : 1bit의 값으로 참/거짓(true/false)만을 나타내는 자료형입니다. 

True는 1, False는 0의 값으로 표현합니다. 두 가지 값만 필요하니 1bit로 모든 값을 표현할 수있습니다.

class Test {
    public static void main(String args[])
    {
        boolean b = true;
        if (b == true)
            System.out.println("Hi boolean"); // Hi boolean 출력
    }
}

 

 

2. byte : 8 bit(1 byte)의 값으로 -128 ~ 127 범위의 값을 표현할 수 있습니다.

정수 자료형 중 가장 작은 범위를 지니고 있습니다. 8bit 이므로 2^8=256 개의 값인 -128~127을 표현합니다. 범위안에 0이 있어 128까지가 아님에 유의합니다.  

class Test {
    public static void main(String args[])
    {
        byte a = 126;
 
        System.out.println(a); // 126 출력
 
        a++;
        System.out.println(a); // 127 출력
 
        // 표현 가능한 범위(-128 ~ 127)를 넘었으므로 -128로 돌아갑니다.
        a++;
        System.out.println(a); // -128 출력
 
        // -128에서 1이 증가합니다.
        a++;
        System.out.println(a); // -127 출력
    }
}

 

3. short : 16 bit(2 byte)의 값으로 -32,768 ~ 32,767 범위의 값을 표현합니다.

2^16= 65,536개의 값을 표현하며, byte와 마찬가지로 중간에 0을 포함하여 양수의 갯수가 하나 더 적습니다. 

 

4. int : 32 bit(4 byte)의 값으로 -2,147,483,648 to 2,147,483,647 범위의 값을 표현합니다.

정수형 중 가장 많이 사용되는 자료형입니다. 범위를 봤을 때 대부분의 변수사용이 이 범위로 커버가 가능하므로 가장 많이 사용됩니다. 만일 사용하는 변수가 21억 이상의 값이 할당될 가능성이 있다면 더 큰 자료형인 long을 사용해야 합니다.

 

5. long : 64 bit(8 byte)의 값으로 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 범위의 값을 표현합니다.

2^64개의 변수를 표현합니다. 값이 엄청나게 커진걸 볼 수 있습니다. 변수를 선언하게 되면 그 공간만큼 메모리가 할당되므로 사용하는 변수에 맞는 자료형을 선택해야 합니다.

물론 요즘 사용하는 하드웨어들이 메모리가 빵빵하기 때문에 자료형 몇개로 부족해지지는 않겠지만 습관을 들이는게 좋습니다.

 

6. float : 32 bit(4 byte)의 값으로 실수를 표현합니다.

위 5개 자료형과는 다르게 실수형 데이터 타입입니다. 실수형이란 소수점을 가진 값으로 이 소수점을 표현하기 위해 가진 bit중 일부를 사용합니다. 소수임을 알리기 위해 값에 f를 붙혀 할당합니다.

public class Main {
    public static void main(String[] args) {
        float number = 0.9f;
        System.out.println(number); // 0.9 출력
    }
}

 

7. double : 64 bit(8 byte)의 값으로 실수를 표현합니다.

float와 마찬가지로 소수점을 표현하기 위해 bit 일부를 사용하지만 f를 붙혀주지 않아도 됩니다.

public class Main {
    public static void main(String[] args) {
        double number = 0.09;
        System.out.println(number); // 0.09 출력
    }
}

 

8. char : 16 bit(2 byte)의 유니코드 문자를 표현합니다.

표현범위로는 '\u0000' (0) to '\uffff' (65535) 입니다. '\u' 는 유니코드라는 의미이고 16진수로 '0000'(0) ~ 'ffff'(65535) 까지 값이 할당된다는 의미입니다.  

자바에서는 문자를 표현할 때 2바이트를 사용하고 있지만 C, C++은 1바이트를 사용합니다. C, C++은 아스키(ASCII)로 문자를 표현하여 일부 문자가 표현이 불가능 합니다. 하지만 자바는 문자(Character)에 Unicode를 사용하므로 대부분 나라의 언어를 포함하여 표현이 가능합니다.

 

아래 테스트를 보면 char형으로 문자 한개를 표현할 수 있고, int 자료형이 4byte로 더 크므로 할당이 가능한 걸 볼 수 있습니다. 

public class Test {
    public static void main(String[] args) {
        char a = 'a';
        char b = 'b';
        System.out.println(a); // a 
        System.out.println(b); // b

        int intFromChar1 = a;
        int intFromChar2 = b;
        System.out.println(intFromChar1); // 97
        System.out.println(intFromChar2); // 98
        
        char x = 'ㄱ';
        char y = 'ㄲ';
        System.out.println(x); // ㄱ
        System.out.println(y); // ㄲ

        int intFromChar3 = x;
        int intFromChar4 = y;
        System.out.println(intFromChar3); // 12593
        System.out.println(intFromChar4); // 12594
    }
}

 

 


 

 

 

템플릿 엔진(Template Engine)

템플릿 엔진이란 템플릿 양식 입력 자료를 합성하여 결과 문서를 출력하는 소프트웨어를 말한다.
그 중 웹 템플릿 엔진은 브라우저에서 출력되는 문서를 위한 소프트웨어이다.

  1. 고정적으로 사용될 부분을 템플릿을 미리 작성해 놓고
  2. 동적으로 변경될 데이터 부분만 필요시 결합하여
  3. 화면(문서)을 완성한다.

어디서 결합하냐에 따라 서버 사이드 템플릿 엔진과 클라이언트 사이드 템플릿 엔진으로 분류할 수 있다.

  • 서버 사이드 템플릿 엔진 서버에서 데이터와 미리 정의된 템플릿으로 HTML을 완성하여 클라이언트에게 전달한다.
    Freemarker, Thymeleaf, Handlebars(Handlebars.java), JSP 등

  • 클라이언트 사이드 템플릿 엔진
    데이터와 템플릿을 합쳐 클라이언트에서 HTML을 완성한다.
    서버는 api 콜에대한 응답데이터만 제공하면서 화면 랜더링은 클라이언트가 담당하는 구조가 가능하다.
    Handlebars(Handlebars.js), EJS(Embedded Javascript Templates) 등

클라이언트 사이드 템플릿 엔진의 필요성

  • 계속해서 페이지를 이동하면서(서버에 데이터를 요청하면서) 화면이 변경되는 것이 아니라, 단일 화면에 특정 이벤트에 따라 화면이 변경되어야 하는 경우 javascript로 html을 변경해야 한다.
  • 조작해야할 코드량이 많아지면 관리가 어려우므로 직접 javascript로 DOM을 제어하는 대신에 클라이언트 사이드 템플릿 엔진을 이용하면 좋다.

람다표현에서는 바깥쪽 스코프의 변수(static 변수, 지역 변수, 인스턴스 변수)를 참조 할 수 있으며 이를 capturing lambda 라고 한다. 

그 중 지역 변수를 참조하는 경우에만 컴파일러는 해당 변수가 final 또는 effectively final 인지 체크하여 아니라면 컴파일 오류를 표출하게 되는데 이에 대해서 알아보고자 한다.

 

아래 메서드는 람다를 리턴한다. 하지만 람다표현식에서 함수의 파라미터(지역변수)를 사용하고 있으며 이를 증가시키고 있으므로 컴파일 오류를 표출한다.

Supplier<Integer> incrementer(int start) {
    return () -> start++; // local variables referenced from a lambda expression must be final or effectively final
}

위 코드의 start 지역변수는 final로 선언되지 않았으며 값을 변경했으므로 effectively final이 아니다.

 > effective final : final은 붙지 않았지만 할당 후 값을 변경하지 않아 final과 같은 효과

 

컴파일 되지 않는 기본적인 이유는 람다는 참조하는 지역변수에 대한 복사본을 가지고 동작하기 때문이다.


람다는 왜 참조하는 외부스코프 지역변수에 대해 복사본을 가지고 동작할까?

위 코드에서 incrementer 함수는 실행되면서 start를 증가시키지 않고 람다를 리턴할 뿐이다.

incrementer 함수의 실행이 끝난 후의 다른 시점에서 리턴한 람다표현식이 실행되어 start 지역변수가 증가할 수 있으며, 그 시점이 오기전에 GC에 의해서 start 지역변수가 정리될 수도 있다.(함수의 실행이 끝나면 지역변수는 GC에 의해 정리대상이 된다.) 그러므로 실행 시점에 지역변수가 사라질 것을 방지하지 위해서 람다는 복사본을 생성하여 동작하게 된다.(또한 지역변수는 Stack영역에 저장되기 때문에 공유자원이 아니므로 다른 thread에서 람다가 실행된다면 변수에 접근할 수 없다.)

 

복사본을 가지고 미래에 동작할 예정인데 복사본에서 값이 바뀌어 버리면(= final or effective final이 아니면, = 최종상태값과 복사본값이 다르면) 오류를 발생할 여지가 생기므로 컴파일 단계에서 막는 것이다.


static 변수와 인스턴스 변수의 경우에는?

private int start = 0;
Supplier<Integer> incrementer() {
    return () -> start++;
}

지역변수가 아닌 인스턴스 변수는 컴파일 에러도 나지 않으며 정상 동작한다. 인스턴스 변수는 Heap영역에 저장되고 static 변수는 Method영역에 저장되며 두 영역모두 공유자원이므로 다른 thread에서 접근이 가능하다. 자유롭게 접근이 가능하므로 실행되는 시점에 최종상태값을 읽어 증가시킬 수 있다. 그러니 변수에 대한 제약조건이 없는 것이다.

 

이에대한 간단한 예제 코드이다.

변수 holder가 참조하는 배열객체는 heap 영역에 저장되어 자유롭게 참조가 가능하다. 그러므로 sums.sum()로 람다가 실행되는 시점에 holder[0]의 최종상태값을 읽어들여 계산하므로 6이 리턴된다.

public int workaroundSingleThread() {
    int[] holder = new int[] { 2 };
    IntStream sums = IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0]);

    holder[0] = 0;

    return sums.sum();
}

 

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

'개발 > JAVA' 카테고리의 다른 글

NullpointerException 과 해결법  (0) 2020.12.09
[Java 기본] Java Primitive 데이터 타입  (0) 2020.12.09
equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01

+ Recent posts