[WS/2주차] 자바 데이터 타입, 변수 그리고 배열

자바 스터디 https://github.com/whiteship/live-study/issues/2
목표 : 자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.

이것이 자바다 책을 보며 같이 공부했습니다.

1. 프리미티브 타입 종류와 값의 범위 그리고 기본 값

  • 프리미티브(기본) 타입은 정수, 실수, 문자, 논리, 리터럴을 저장하는 타입이다. 정수 타입엔 byte, char, short, int, long이 있고 실수 타입엔 float, double, 논리 타입엔 boolean이 있다.
값의 종류 기본 타입 메모리 사용 크기 저장되는 값의 범위
정수 byte 1 byte 8 bit -27~(27-1) (-128~127)
char 2 byte 16 bit 0~216-1 (유니코드:\u0000~\uFFFF, 0~65535)
short 2 byte 16 bit -215~(215-1) (-32,768~32,767)
int 4 byte 32 bit -231~(231-1) (-2,147,483,648~2,147,483,647)
long 8 byte 64 bit -263~(263-1)
실수 float 4 byte 32 bit (+/-)1.4E-45 ~ (+/-)3.4028235E38
double 8 byte 64 bit (+/-)4.9E-324 ~ (+/-)1.7976931348623157E308
논리 boolean 1 byte 8 bit true, false
  • 1 byte = 8 bit. 바이트 크기가 클수록 표현하는 값의 범위가 크다. 정수 타입일 경우 -2n-1 ~ 2n-1-1 의 값을 저장할 수 있는데, 여기서 n이 메모리 사용 크기(bit 수) 이다. 실수 타입은 가수와 지수 부분에 사용되는 bit 크기에 따라 값의 범위가 저장 된다.

정수 타입(byte, char, short, int, long)

  • 정수 타입엔 모두 다섯 개의 타입이 있으며 저장할 수 있는 값의 범위가 서로 다르다.
  • 자바는 기본적으로 정수 연산을 int 타입으로 한다. byte와 short는 값의 범위가 작아 연산 시 범위가 초과 되어 계산 오류가 날 수 있어, 보통 정수 리터럴은 int 타입 변수에 저장한다.
  • 연산 시 저장할 수 있는 값의 범위를 넘으면 다시 최소값부터 값이 시작한다. byte의 경우 -128(최소값)부터 127(최대값)을 넘으면 다시 -128부터 시작된다. 이렇게 값의 범위를 초과해 엉터리 값이 저장되는 걸 쓰레기값이라고 한다.

    정수타입 byte char short int long
    바이트 수 1 2 2 4 8
  1. byte 타입 : 색상 정보 및 파일 or 이미지 등의 이진(바이너리) 데이터를 처리할 때 주로 사용된다. 표현 값 범위는 -27~(27-1) 인데 양수가 27-1 인 이유는 0이 포함되기 때문이다.
  2. char 타입 : 자바는 모든 코드를 유니코드로 처리한다. 자바는 하나의 유니코드를 저장하기 위해 2byte 크기의 char 타입을 제공하고, char는 유니코드기에 음수가 들어갈 수 없다. 작은 따옴표로 감싼 문자를 대입하면 해당 문자의 유니코드가 저장된다. 직접 유니코드 정수값을 10진수 char c = 65; 또는 16진수(‘\u + 16진수값) char c = '\u0041'; 으로 넣을 수 있다. char 타입 변수에 단순 초기화를 위해 ‘’ 처럼 빈 문자열을 넣었을 경우 컴파일 에러가 나기 때문에 공백을 넣어 초기화해야 한다 char c = ' ';.
  3. short 타입 : 2byte(16bit)로 표현되는 정수값을 저장할 수 있는 데이터 타입. C언어와의 호환을 위해 사용되며 자바에선 잘 사용되지 않는다.
  4. int 타입 : 4byte(32bit)로 표현되는 정수값을 저장할 수 있는 데이터 타입. 자바에서 정수 연산을 위한 기본 타입이다(byte 타입과 short 타입 변수를 더하면 int 타입으로 변환되어 연산 되고 결과 역시 int 타입이 된다). 자바에선 정수 연산을 4byte로 처리하기 때문에 int와 byte, short 타입간의 성능 차이는 거의 없다. 직접 코드로 값을 저장할 경우 8진수, 10진수, 16진수로 표현할 수 있다. 하지만 어떤 진수로 입력하든 동일한 값이 2진수로 변환해 저장된다.
  5. long 타입 : 8byte(64bit)로 표현되는 정수값을 저장할 수 있는 데이터 타입. 수치가 큰 데이터를 다루는 프로그램에선 long 타입이 필수다(ex. 은행, 우주 관련 프로그램). long타입 변수 초기화는 정수 뒤에 l이나 L을 붙여준다(보통 L사용). 4byte가 아닌 8byte 정수 데이터임을 컴파일러에게 알려주기 위함이다. int의 저장 범위를 넘는 수는 반드시 l이나 L을 붙여줘야 한다.

실수 타입(float, double)

  • 소수점이 있는 실수 데이터를 저장할 수 있는 타입으로 메모리 사용 크기에 따라 float, double이 있다.

    실수타입 float double
    바이트 수 4 8
  • 메모리 사용 크기는 각각 int와 long과 같지만 저장 방식의 차이로 정수 타입보다 훨씬 큰 범위의 수를 저장할 수 있다. 부동 소수점 방식(floating-point)을 사용한다.
  • 부동 소수점 방식 : +(부호) m(가수) X 10ⁿ(n은 지수)
    • m은 0≤m<1 범위의 실수여야 한다.
    • ex) 1.2345 = 0.12345 x 10¹ = 가수 0.12345 지수 1
  • float : 부호(1bit) + 지수(8bit) + 가수(23bit) = 32bit = 4byte
  • double : 부호(1bit) + wltn(11bit) + 가수(52bit) = 64bit = 8byte
  • 자바 실수 리터럴의 기본 타입은 double이다. 즉 실수 리터럴을 float에 그냥 저장할 수 없다. 실수 리터럴을 float에 저장하기 위해선 f나 F를 뒤에 붙여줘야 한다. float a = 3.14F;
  • 정수 리터럴에 10 지수를 나타내는 E 혹은 e를 포함하고 있으면 실수 타입 변수에 저장해야 한다. double b = 3e6; float c = 3e6f; double d = 2e-3;
  • double 타입의 가수 bit 수가 float 가수 bit 수보다 약 두 배 더 크기 때문에 double 타입이 정밀도가 훨씬 높다.

논리 타입(boolean)

  • 1byte(8bit)로 표현되는 논리값(true/false)를 저장할 수 있는 데이터 타입이다.
  • 두 가지 상태값을 저장할 필요성이 있을 때 사용된다. 상태값에 따라 조건문, 제어문의 실행 흐름을 변경하기 위해 주로 이용된다.

2. 프리미티브 타입과 레퍼런스

자바는 크게 프리미티브 타입과 레퍼런스 타입으로 나뉜다. 프리미티브 타입(기본 타입)은 정수, 실수, 문자, 논리 리터럴을 저장하는 타입이다. 레퍼런스 타입객체(Object)의 주소를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스 타입을 말한다.

02

프리미티브 타입은 실제 값을 변수에 저장하지만, 레퍼런스 타입은 메모리의 주소를 값으로 갖는다. 변수는 스택에 생성되고 객체는 힙 영역에 생성되는데 레퍼런스 타입은 다음 그림과 같은 원리이다.

03

int와 double 변수 age, price는 직접 값을 갖고있지만, String 변수 name, hobby는 힙 영역의 객체 주소값을 저장하고 있다. 즉 주소로 객체를 참조한다는 의미로 String 클래스 변수는 레퍼런스 타입이다.

기본 타입 변수의 ==, != 는 값이 동일한지 묻는 거지만, 참조 변수는 같은 객체 주소값을 참고하고 있는지 아닌지를 본다.

String

자바에서 String은 문자열이 직접 변수에 저장되는 것이 아니라 String 문자열이 저장된 객체가 생성되고 변수는 그 주소값을 참조한다. 문자열 리터럴이 같다면 같은 주소를 참조하게 된다. 만약 같은 문자열 리터럴String name1 = "스트링";을 갖고 있더라도, String name2 = new String("스트링"); new 연산자로 직접 객체를 생성했다면 name1과 name2 변수의 == 연산 결과는 false가 나온다. 저장된 주소가 다르기 때문이다.
그래서 String은 문자열이 동일한지 비교할 땐 == 연산자가 아니라 equals 메소드를 사용한다. equals 메소드는 원본 문자열과 매개값으로 주어진 비교 문자열이 동일한지 비교해주는 메소드이다.

String은 참조 변수이기 때문에 초기값으로 null 값을 사용할 수 있다. String 변수가 참고하는 String 객체가 없다는 뜻이다.

3. 리터럴

소스 코드 내에서 직접 입력된 값을 리터럴(literal) 이라고 한다. 리터럴은 정해진 표기법대로 작성되어야 한다. 상수와 같은 의미지만, 프로그램에선 상수를 “값을 한번 저장하면 변경할 수 없는 변수”로 정의하기에 구분하기 위해 리터럴이라고 부른다.

정수 리터럴

  • 소수점이 없는 정수 리터럴은 10진수 0, 75, -100
  • 0으로 시작되는 리터럴은 8진수 02, -04
  • 0x 또는 0X로 시작하고 0~9 숫자나 A,B,C,D,E,F 또는 a,b,c,d,e,f로 구성된 리터럴은 16진수 0x5, 0xA, 0xB3, 0xAC08
  • 정수 리터럴을 저장할 수 있는 타입은 byte, char, short, int, long

실수 리터럴

  • 소수점이 있는 리터럴은 10진수 실수 0.25, -3.14
  • 대문자 E 또는 소문자 e가 있는 리터럴은 10진수 지수와 가수
    • 5E7 = 5 x 107
    • 0.12E-5 = 0.12 x 10-5
  • 10진수를 저장할 수 있는 타입은 float, double

문자 리터럴

  • 작은 따옴표(‘)로 묶은 텍스트는 하나의 문자 리터럴 'A', '한', '\t', '\n'
  • 역슬래쉬()가 붙은 문자 리터럴은 이스케이프 문자라고도 한다.

    이스케이프 문자 용도 유니코드
    ‘\t’ 수평 탭 0x0009
    ‘\n’ 줄 바꿈 0x000a
    ‘\r’ 리턴 0x000d
    ’" “(큰 따옴표) 0x0022
    ’'’ ‘(작은 따옴표) 0x0027
    ’\’ &bsol; 0x005c
    ‘\u16진수’ 16진수에 해당하는 유니코드 0x0000~0xffff
  • 문자 리터럴을 저장할 수 있는 타입은 char

문자열 리터럴

  • 큰따옴표(“)로 묶은 텍스트는 문자열 리터럴, 큰따옴표 안에 텍스트가 없어도 문자열 리터럴로 간주함. 문자열 리터럴 내에서도 이스케이프 문자 사용 가능.
    "대한민국", "탭 만큼 이동 \t 한다."
  • 문자열 리터럴 저장할 수 있는 타입은 String

논리 리터럴

  • true와 false는 논리 리터럴
  • 논리 리터럴 저장할 수 있는 타입은 boolean

4. 변수 선언 및 초기화하는 방법

변수 선언

  • 변수란 하나의 값을 저장할 수 있는 메모리 공간이다. 프로그램으로 값이 계속 변할 수 있기 때문에 변수라고 한다.
  • 변수엔 한 가지 타입의 값만 설정할 수 있다.
  • 변수를 사용하기 위해선 먼저 변수를 선언해야 한다. 변수 선언은 변수타입, 변수이름으로 구성된다.

    int age; // 정수(int) 값을 저장할 수 있는 age 변수 선언
    String name; //문자(String)를 저장할 수 있는 name 변수 선언
    double x,y,z; //같은 타입은 콤마로 한꺼번에 선언 가능하다
    
  • 변수 이름은 메모리 주소에 붙여진 이름이다. 변수이름으로 메모리 주소에 접근해서 값을 저장하거나 읽는다. 변수명은 자바의 명명규칙을 따라야하며 예약어를 사용할 수 없다.

변수 초기화

  • 변수에 값을 저장할 때 대입 연산자(=) 를 사용한다. 자바에선 우측의 값을 좌측의 변수에 저장한다는 의미이다.
  • 변수를 선언하고, 처음 값을 저장해주는 걸 변수의 초기화라고 한다.

    int a; //변수 선언
    a = 10; // 변수 초기화
    int b = 10; //변수 선언과 초기화를 동시에 할 수 있다
    
  • 변수는 초기화 되어야 읽을 수 있다.
    int a; // 변수 a를 선언만 함
    int b = a+30; //a를 초기화하지 않았기 때문에 컴파일 에러 발생
    int c = 10; // 변수 c를 선언과 동시에 초기화
    int d = c + 10; //변수 c값을 읽고 10을 더한 값(20)이 변수 d에 저장됨
    

5. 변수의 스코프와 라이프타임

  • 변수의 사용 범위를 변수의 스코프라고 한다. 변수는 중괄호 {} 블록 내에서 선언되고 사용된다. 중괄호 블록을 사용하는 건 클래스, 생성자, 메소드이다. 변수가 선언된 블럭이 곧 변수의 스코프(사용범위)이다.
  • 메소드 블록 내에서 생성된 변수는 지역 변수(local variable) 라고 한다. 로컬 변수는 메소드 실행이 끝나면 메모리에서 소멸된다.
  • 변수는 선언된 블록 내에서만 사용 가능하다. 변수가 어떤 범위에서 사용될지 생각하고 선언 위치를 정하자. 메소드 내 어디서든 사용 가능하게 하려면, 메소드 첫머리에 선언하고 제어문(if, for, while…) 안에서만 사용 가능하게 하려면 제어문 안에서 선언.

    01

6. 타입 변환, 캐스팅 그리고 타입 프로모션

타입 변환은 데이터를 다른 데이터 타입으로 변환하는 것이다. 자동(묵시적) 타입 변환(Promotion)과 강제(명시적) 타입 변환(Casting)이 있다.

자동 타입 변환(Promotion)

프로그램 실행 중에 자동으로 타입 변환이 일어나는 것이다. 작은 크기를 가지는 타입이 큰 크기의 타입에 대입될 때 발생한다. 크기 타입의 구분은 사용하는 메모리의 크기이다.

byte(1) < short(2) < int(4) < long(8) < float(4) < double(8)
float가 int보다 큰 이유는 표현 값 범위가 int보다 더 크기 때문이다.

byte a = 10;
int b = a; //자동타입 변환이 일어난다.

변환 이전의 값과 변환 후의 값은 동일하다(작은 그릇의 물을 큰 그릇으로 옮기는 걸 생각하자). 단 정수 타입 -> 실수 타입은 무조건 변환이 일어나는데, 실수 타입으로 변환 된 이후의 값은 정수값이 아닌 .0이 붙은 실수값이 된다.
char 타입은 int로 변환 시 유니코드 값이 int에 저장된다. 단 char는 음수를 저장하지 못하기 때문에 음수가 저장될 수 있는 byte 타입은 char로 자동 변환시킬 수 없다.

int intval = 200;
double doubleval = intval; //200.0
char charval = 'A';
int intval2 = charval; // intval2 = 65
byte a = 65;
char b = a; //컴파일 에러
char c = (char)a; //강제 타입 변환으론 가능

강제 타입 변환(Casting)

큰 크기의 타입은 작은 타입으로 자동 변환할 수 없기에, 큰 크기 타입을 작은 타입으로 쪼개어 저장하는 것이다. 캐스팅 연산자 ()를 사용하며 ()엔 쪼개는 단위가 들어간다.

//작은 크기 타입 = (작은 크기 타입) 큰 크기 타입
int intval = 103029770;
byte byteval = (byte)intval; // 강제 타입 변환

위 예제를 그림으로 보면 다음과 같다.

05

끝 1byte만 byte 타입 변수에 저장되므로 원래 int 값이 보존되지 않는다. 하지만 int 값이 끝 1byte로만 표현 가능하다면 byte 타입으로 변환해도 원래 값이 보존된다.
ex) int 변수에 10을 저장할 경우, 4byte 중 1byte로 충분히 10을 표현할 수 있으므로 앞의 3byte는 모두 0으로 채워진다. 강제 타입 변환을 하면 앞의 3byte는 버려지고 끝 1byte만 byte 타입 변수에 저장되기 때문에 원본 값이 보존된다.

int 타입은 char 타입으로 자동 변환되지 않기에 강제 타입변환을 사용해야 한다. int 타입에 저장 값이 유니코드 범위(0~65535) 라면 (char) 캐스팅 연산자를 사용해 char로 변환할 수 있다.

int intval = 'A';
System.out.println(intval); // 65
char charval = (char)intval;
System.out.println(charval); // 유니코드에 해당하는 문자(A)가 출력된다.

실수 타입(float, double)도 정수타입으로 변환하기 위해선 강제타입변환을 사용해야 한다. 소수점 이하 부분은 버려지고 정수 부분만 저장된다.

강제 타입 변환에서 주의할 점

  1. 사용자에게 입력받은 값을 변환할 때 값 손실이 일어나선 안 된다. 변환 전 안전하게 값이 보존될 수 있는지 검사하는 게 좋다. 자바에선 데이터 값을 검사하기 위해 boolean, char를 제외하고 최대값, 최소값을 상수로 제공하고 있다. (ex. Integer.MAX_VALUE / Integer.MIN_VALUE)
  2. 정수 타입을 실수 타입으로 바꿀 때 정밀도 손실을 피해야 한다. int 값을 손실 없이 float로(부호 1비트 + 지수 8비트 + 가수 23비트) 변환하려면 가수 23비트로 표현 가능한 값이여야 한다. int 값이 123456780이라면 23비트로 표현할 수 없기 때문에 근사치로 변환되고 정밀도 손실이 발생한다. int 값을 정밀도 손상 없이 실수 타입으로 안전하게 변환하기 위해선 double 타입(부호 1비트 + 지수 11비트 + 가수 52비트)를 사용해야 한다. int는 32비트고 double의 가수 52비트보다 작기 때문에 정밀도 손실 없이 double로 변환 가능하다.

연산식에서의 자동 타입 변환

연산은 같은 타입의 피연산자 간에만 수행되기 때문에 타입이 다를 경우 피연산자 중 크기가 큰 타입으로 자동 타입 변환 후 연산을 실행한다.

int intVal = 10;
double doubleVal = 5.5;
double res = intVal + doubleVal;
//intVal가 double로 자동 타입 변환되고 연산을 수행. 당연히 연산 결과는 double이다.

int로 꼭 연산을 해야 한다면 double을 int로 강제타입변환 시킨 후 덧셈 연산을 수행하자. int res = intVal + (int)doubleVal;

자바는 정수 연산일 경우 int 타입을 기본으로 한다. 피연산자를 4byte 단위로 저장하기 때문이다. 따라서 4byte보다 작은 byte, short, char 은 4byte인 int로 변환된 후 연산이 수행되고 연산 결과도 int 타입이 된다. char 연산 결과를 문자로 저장하기 위해선 int로 결과값을 받은 후 char 타입으로 강제 타입 변환 해야한다.

char + int = int
long + int = long
float + double = double float + 실수리터럴 = double

7. 1차 및 2차 배열 선언하기

배열은 같은 타입의 변수를 연속 된 공간에 나열하고 각 공간에 인덱스를 지정해놓은 자료구조이다. 같은 타입의 변수만을 저장할 수 있기 때문에, 선언과 동시에 저장 가능한 데이터 타입이 결정 된다. 또한 한 번 생성된 배열의 길이는 변경될 수 없다.

1차원 배열

배열을 사용하기 위해선 먼저 배열 변수를 선언해야 한다. 두 가지 형태로 사용 가능하다.

타입[ ] 변수;
타입 변수[ ];

대괄호 []는 배열 변수를 선언하는 기호이다. 타입은 배열에 저장될 변수 타입이다.

배열은 참조변수이기 때문에 참조할 객체를 생성해줘야 한다. 참조할 배열 객체가 없다면 null로 초기화 될 수 있다. 참조할 객체가 없는 상태에서 배열[인덱스]를 호출하면 NullPointerException이 발생한다.
배열에 저장될 목록이 있다면 다음과 같이 배열 객체를 만들 수 있다.

데이터타입[] 변수 = {값0, 값1, 값2…};

{}는 주어진 값들을 갖는 객체 배열을 힙 영역에 생성하고 그 주소값을 리턴해 준다. 배열 변수는 리턴 된 주소값을 저장하여 참조가 이루어진다. 하지만 위 같은 경우는 배열변수를 선언하면서 가능한 방법이고, 선언한 후에 변수 = {값0, 값1} 처럼 작성하면 컴파일 오류가 발생한다. 값 목록이 선언 후 나중에 결정된다면 다음과 같이 작성한다.

타입[] 변수; 변수 = new 타입[] {값0, 값1, 값2…};

메소드 매개값이 배열인 경우도 마찬가지다. 매개 변수로 배열이 선언된 메소드가 있을 경우, 값 목록으로 배열을 생성하며 메소드 매개값으로 사용할 때는 반드시 new 연산자와 함께 사용해야 한다.

int aaa(int[] array);
int result = aaa({95,85,90}); //컴파일 에러
int result = aaa(new int[] {95, 85, 90});

값 목록을 미리 갖고 있지 않지만, 배열을 먼저 만들고 싶을 땐 다음과 같이 생성시킨다.

타입[] 변수 = null;
변수 = new 타입[배열길이];
or
타입[] 변수 = new 타입[배열길이];

new로 배열을 처음 생성하면, 배열은 자동적으로 기본값으로 초기화된다. int 배열이라면 기본값 0으로 초기화 되고 String 배열이라면 null로 초기화 된다.

참조타입(클래스, 인터페이스) 배열 같은 경우엔 각 항목에 객체의 주소를 저장한다. 그래서 String[] 배열 항목 간 문자열을 비교하기 위해선 ==가 아닌 equals()를 사용해야 한다. ==는 객체의 번지 비교가 된다.

2차원 배열

1차원 배열은 값 목록으로 구성되었다면, 2차원 배열은 행과 열로 구성된 배열이다. 수학의 행렬처럼 가로 인덱스와 세로 인덱스를 사용한다. 예를 들어 int[][] a = new int[2][3] 으로 생성했다면 가로 2행, 세로 3열인 2차원 배열이 생성 된다.

자바는 2차원 배열을 중첩 방식으로 생성한다. 위에서 예로 든 코드를 생성했다면, 세 개의 배열 객체가 생성된다. 배열 변수 a는 길이 2인 배열 A를 참조하고, 배열 A의 a[0]은 길이 3인 배열 B를, a[1]은 길이 3인 배열 C를 참조한다.

06

자바는 일차원 배열이 서로 연결된 구조로 다차원 배열을 구현하기 때문에, 계단식 구조를 가질 수 있다.

int[][] a = new int[2][3];
a[0] = new int[2]; // 0, 1
a[1] = new int[3]; // 0, 1, 2

07

이 때 정확한 배열의 길이로 인덱스를 사용해야 한다. a[0][2]는 ArrayIndexOutOfBoundsException 을 발생시킨다. 배열 B 객체의 마지막 인덱스가 1이기 때문이다. 하지만 a[1][2] 는 가능하다. 배열 C 객체의 마지막 인덱스가 2이기 때문이다.

그룹화 된 값 목록을 갖고 있다면 다음과 같이 선언할 수도 있다.

타입[][] 변수 = { {값1, 값2, …}, {값1, 값2,…}, … }

8. 타입 추론, var

참고 : https://catch-me-java.tistory.com/19
자바 10부터 타입 추론을 지원한다. 타입추론은 개발자가 변수의 타입을 명시적으로 적어주지 않아도, 컴파일러가 타입을 대입된 리터럴로 추론하는 것이다.

  • var는 초기화값이 있는 지역변수로만 선언이 가능하다.
  • var는 멤버변수, 메소드의 파라미터, 리턴 타입으로 사용할 수 없다.
  • var는 키워드가 아니다(어떠한 타입도 예약어도 아니다. 변수명으로도 사용할 수 있다.)
  • var는 런타임 오버헤드가 없다.
    • 컴파일 시점에 초기화 값을 통해 이미 var의 타입이 결정되어 있기 때문에, var를 읽을 때마다 타입을 알아내기 위한 연산을 수행하지 않는다. 그래서 자바에선 var로 선언된 변수는 중간에 타입이 절대 변하지 않는다.
  • var엔 null 값이 들어갈 수 없다.
  • 람다식에선 사용 불가능. 배열 선언에도 var 사용 불가능함.

-> 여러 글을 읽어봤는데 아직까진 자바에서 var 사용이 편한진 모르겠다… 이걸 어떻게 잘 쓸 수 있을진 의견도 분분한 것 같고? 아무튼 자바에서도 var를 사용할 수 있다니 신기하네

태그:

카테고리:

업데이트:

댓글남기기