[KOSTA 교육 11일차] 객체지향에서의 '객체'란 | 컴파일러 언어와 인터프리터 언어 | 변수, 리터럴, 형변환(★)

junjun·2024년 4월 25일
0

KOSTA

목록 보기
10/48

객체지향에서의 객체란

객체지향이 무엇인가요? '객체'가 무엇인가요?

라고 물어본다면,
이것저것 말만 많아지고 횡설수설하게 되는 것 같다.

"실세계에서 인식할 수 있는 모든 대상의 정적인 속성과 동적인 기능을 묶은 단위"를
객체 라고 한다면,
일단 길고.. 무슨 철학하는 것도 아니고, 내용이 와닿지도 않는다.

개발자에게 딱 필요한 용어, 와닿는 용어로
간단하게 정의하자.

객체는, 변수와 메서드로 나타낼 수 있는 모든 것이다.

변수 ( 인스턴스 변수 )는 객체의 정적인 속성이고,
메서드 ( 인스턴스 메서드 )는 객체의 동적인 기능의 집합이다.

변수

  • 주소값을 기억할 수 없으니 ( 심지어 프로그램이 실행될 때마다 실제 메모리 값은 달라진다 ),
    이 주소에 대한 글자(Symbol)을 매핑한다.

  • 메모리 공간의 이름을 따로 지어놓은 것

객체 vs 클래스 vs 인스턴스

  • 객체는 정적인 값과 동적인 기능을 갖는 모든 것
  • 클래스는 객체를 담는 ( 표현하는 ) 그릇
  • 인스턴스는 객체가 메모리에 올리온 것 ( 메모리에 할당된 객체 )

컴파일러 언어와 인터프리터 언어

  • 컴파일러 언어는 실행(런타임)이전에 잡아낼 수 있는 에러를 다 잡아내고,
    최적화 과정을 거친 코드를 만듬. ( 이 코드는 바이너리 = C,C++ / 바이트코드 = Java 일 수 있음. )

  • 인터프리터 언어는 실행 시 코드의 한줄 한줄 실행한다.
    컴파일 타임이 없어서, 실행까지의 시간은 빨라도, 한 줄 한 줄 실행하는 시간은 느리다.

  • 인터프리터 언어는 중간에 변경사항이 있으면, 바로 적용이 가능하지만
    컴파일러 언어는 다시 빌드 과정 ( 컴파일 + 라이브러리 링크 )을 거쳐야 한다.

컴파일 vs 링크 vs 빌드

  • 컴파일이란, 개발자가 작성한 소스코드를 바이너리 코드 (혹은 바이트 코드)로 변환하는 과정을 말한다. ( 목적파일 .o 생성 )

즉 컴퓨터가 이해할 수 있는 기계어로 변환하는 작업이다.

이러한 작업을 해주는 프로그램이 컴파일러(Compiler)이다.

자바의 경우, JVM에서 실행가능한 바이트코드 형태의 클래스파일이 생성되고, 이것이 컴파일이다.

  • 링크(Link)란,
    소스 파일들간 서로를 호출할 때, 이를 연결해주는 과정이다.
    각 소스 파일들을 컴파일하여, 클래스 파일(.class) 또는 목적 파일(.o)로 만든 뒤
    각 컴파일 결과물 파일들을 연결해주어, 최종 실행가능한 파일을 만들어준다.
    이것이 링크(Link)라 한다.

링크의 종류에는 정적 링크(static link)와 동적 링크(dynamic link)가 있는데

정적링크는 컴파일된 소스파일을 연결하여 실행가능한 파일을 만드는 것이고,
동적링크는 프로그램 실행 도중 프로그램 외부에 존재하는 코드를 찾아서 연결하는 작업을 말한다.
JVM의 클래스 로더는 실행 도중 필요한 클래스에 대해 요청받으면 classpath에서 필요한 클래스를 찾아서 로딩해주는데, 이것을 동적 로딩이라 하고, 동적 링크의 예시이다.

  • 빌드(Build)란,
    소스코드 파일을 실행가능한 소프트웨어 산출물로 만드는 일련의 과정을 말한다.
    빌드의 단계 중 컴파일이 포함되는데, 컴파일은 빌드의 부분집합이다.
    빌드 과정을 도와주는 툴을 빌드 툴이라 한다.

  • 빌드 툴(Build Tool)
    일반적으로 빌드 툴은 다음과 같은 기능을 제공한다.
    전처리(pre-processing) -> 컴파일(compile) -> 패키징(packaging : jar, war) -> 테스팅(testing : CI) -> 배포(distribution : CD)

빌드 툴로는 Ant, Maven, Gradle 등이 있다.

리터럴(literal)과 접미사, 접두사

  • 프로그램에서 직접 표현한 값
  • (★) 리터럴 값 자체도 타입을 갖는다.
    ex> 문자형 리터럴 : "a".repeat(10) 이 가능하다.
    ex> int a = 10L : 컴파일 에러, 리터럴 10L 은 long 타입을 갖는데, 이것을 a에 대입 연산하려 하기 때문이다. (Type Mismatch, 표현 범위가 넓은 범위가 표현 범위가 낮은 범위에 속할 수 없다.)
  • 접두사, 접미사는 리터럴에 의미를 더해주는 글자이다.
  • 자바에서 10 이란 값의 default 타입은 int
  • 자바에서 10.28 이란 값의 default 타입은 double
  • 자바에서 값을 long 타입으로 받고 싶다면, 접미사에 l 또는 L을 추가해주면 된다.
    ex> 12L
  • 자바에서 값을 float 타입으로 받고 싶다면, 10.28f , 10.28F 라고 한다.
  • 16진수로 정수형 리터럴 값을 나타낼 때, 접두사로 0x를 붙인다.
  • 0x16 => 16진수 값, 타입은 int 형
  • 유니코드 문자임을 표현하기 위해 접두사로 \u를 붙인다.
  • \u0000 : 16진수로 16비트 자료를 나타냄. ( char형 )
  • 10의 N승을 표현하기 위해 접미사 e 사용
  • ex> 3.14e3f = 3140.0f
  • ex> 1e1 = 10.0
  • e 접미사 같은 경우, 정수형 데이터에 붙이면 실수형 double이 된다.

형변환과 리터럴

  • 작은 타입 -> 큰 타입에서는 묵시적 형변환이 일어난다.
  • 작다라는 것은 표현의 범위가 작다는 것이고, 크다라는 것은 표현의 범위가 크다는 것이다.
  • float은 4바이트고, long은 8바이트이지만, float표현범위가 더 크므로 float 타입으로 long타입을 담을 때, 명시적으로 형변환해주지 않아도 된다. 묵시적 형변환이 일어난다.
  • 이와 마찬가지로, doublelong보다 표현범위가 넓다. 그렇기에 double -> long 은 명시적 형변환을 해줘야하고, long -> double 이면 묵시적 형변환이 일어난다.
long a = 3; // from int to long ( 안전하니 묵시적 형변환 )

int b = 3L; // 에러

int c = (int) 3L; // 명시적 형변환 OK

long b = 10.28f;
// 불가능 ( float이 long보다 표현할 수 있는 범위가 크기에, 묵시적 형변환 불가! )
// -> long b = (long) 10.28f 로 해주어야 한다.
		
float c = 10L; // 가능. float이 long보다 더 큰 범위를 표현 가능하다.

int f = (int) 10e1;
// 명시적 형변환 ( 10e1 -> double 타입. 
// int로 변환하려면 명시적으로 형변환 해주어야 함 )

형변환이 안되는 케이스 2가지

  • 표현범위가 낮은 것이 표현범위가 큰 것을 담으려고 할 때
  • byte, short형을 char형으로 변환하려하거나, char 형을 short 형으로 변환하려 할 때

static 변수와 지역변수의 차이 ( + new로 선언한 배열의 차이 ) : 선언 시 초기화 필요 여부

  • 변수를 선언만 해놓고, 명시적으로 초기화하지 않으면
    지역변수의 경우 그 값을 사용하려할 때 문제가 있다.

  • static 변수의 경우, 클래스 로더가 데이터 값을 올릴 때 초기화 과정을 거치기에 바로 쓸 수 있다.

  • 배열의 경우 new를 통해 힙 메모리에 할당할 때, default값으로 할당까지 한다.

  • 지역변수는 사용하기 전에 반드시 초기화해주어야 한다.

  • boolean도 사실 default값이 false 이지만, 사실은 내부 비트가 다 0으로 초기화된 것이다. 그냥 기본형의 초기값은 내부 비트가 다 0으로 초기화된다고 생각하면 된다.

신기한(?) 개인적 실험 - 1 ( feat. 오버플로우 )

// 첫번째 실험
byte a = -1;
byte b = 127;
byte c = (byte) (a + 1);
System.out.println("& = " + (cc & dd));
System.out.println("c = " + c);

/* 결과
& = 127
c = -128
*/
  • byte 형의 표현범위는 -2^7 ~ 2^7 -1 이다.

  • 최대 표현값인 127과 음수 byte값의 시작점인 -1에 대해 비트연산 &을 해보니,
    최대 표현값인 127이 나왔다. 이 말은, 127-1은 부호비트를 제외하고 모든 비트값이 같다는 의미이다. 즉, 내부적으로 -11111_1111이다. 그렇다면, -2^71000_000이다.

  • byte 타입의 최댓값 127에 1을 더한 값을 byte 로 치환해보니 -128이 나왔다.
    이 말은, 어떤 타입의 최댓값에서 1을 더하면, 해당 타입의 가장 마지막 값이 된다는 것이다. ( 오버플로우 ). 이는, 0111_111 에 1을 더했을 때, 1000_0000이 되고, 이 값이 byte 타입의 최댓값인 -128을 나타낸다.

신기한(?) 개인적인 실험 - 2 ( 오버플로우 2 / Math.pow는 근사값을 계산한다. )

// 두번째 실험
int x = 1;
for(int i = 0; i < 31; i++) {
	x *= 2;
}
System.out.println("manualPow = " + x);

int libraryPow = (int) Math.pow(2, 31);
System.out.println("libraryPow  = " + libraryPow );
int integerMaxDefault = Integer.MAX_VALUE;
System.out.println("overflowByPowLib = " + ((int) Math.pow(2, 31) + 1));
System.out.println("integerMaxDefault = " + integerMaxDefault);

/* 결과
manualPow = -2147483648
libraryPow = 2147483647
overflowByPowLib = -2147483648
integerMaxDefault = 2147483647
*/
  • int형의 타입은 -2^31 ~ 2^31-1의 범위를 가진다.

  • 첫번째 manualPow의 값은, for문을 통해 2^31이 되도록 하였다.
    - 이 값을 출력해보니, int의 최대값보다 1만큼 증가하여 오버플로우된 값이다.

  • 두번째 libraryPow값은, Math.pow(2,31) 의 값을 int 로 캐스팅하여 출력했다.
    - 당연히 이 값이 오버플로우가 났을 것이라 생각했는데, 의외로 이 값은 int의 최댓값이 나왔다.

    • 왜일까.. 계속 고민을 했는데 바로 Math.pow의 리턴 값이 double이고, 이 값은 내부적으로 근사치의 값으로 nteger.MAX_VALUE+ alpha ( alpha ~= 1, alpha < 1 )인 값을 가졌던 것이었다.
      • 그렇기에, 해당 값을 int로 타입 캐스팅하면, 소수 부분이 잘려나가고, nteger.MAX_VALUE의 값만 남아있던 것이었다!
  • 세번째 Math.pow를 통해 구한 (소수점이 잘려나간) libraryPow값에 1을 더하니, 의도한 대로 오버플로우가 났다.

  • 네번째 integerMaxDefault값을 Integer 클래스의 MAX_VALUE 상수를 가져와서 출력하니, int의 최댓값이 제대로 출력되었다.

  • 이를 통해, Math.pow 가, 즉 double형이 내부적으로 근사치의 값을 표현함을 알 수 있었다. ( 부동 소수점 방식을 사용해서 )

형변환과 이항 연산자(★)

[ 형변환 규칙 정리 ]

  • 표현범위가 넓은 범위 타입에서 표현범위가 낮은 타입으로 변환될 때는 반드시 명시적 형변환을 해주어야 한다. 이 때, 일부 값이 손실될 수 있다.
    - long -> int
    • double -> float
    • float -> long
    • double -> long
    • Parent Type -> Child Type ( Reference Type, 클래스 상속관계에서 )
  • 표현범위가 낮은 범위인 타입에서 표현범위가 넓은 범위의 타입으로 변환할 때는 묵시적 형변환이 일어난다.
    - int -> long
    • float -> double
    • long -> float
    • long -> double
    • Child Type -> Parent Type ( Reference Type, 클래스 상속관계에서 )
    • 그렇기에 Child Type으로 Parent Type을 담을 수 없다.

[묵시적 형변환이 안되는 경우]

  • 표현 범위가 낮은 쪽에서 높은 쪽으로 가려할 때
  • byte, short 형을 char로 변환하는 경우 / char형을 short형으로 변환하는 경우

이항 연산자의 특징(★★★)

  • JVM의 피연산자 스택과 관련있다. ( JVM은 스택 기반 가상머신이다. )

[ 매우 중요한 규칙 2가지 ]
1. int 보다 크기가 작은 타입은 int로 변환하여 계산한다. ( JVM의 피연산자 스택은 연산의 단위를 4바이트를 기준으로 한다. )
( byte, char, short를 이항 연산자를 통해 연산할 때, int로 변환하여 계산한다 )
- char형과 byte형을 연산할 때, 내부적으로 두 변수 모두 int로 캐스팅하여 계산한다. 당연히 묵시적으로 결과값도 int 타입이다.

  1. 피연산자 중 표현범위가 큰 타입으로 형변환하여 계산한다. (묵시적 형변환)
byte a = 10;
byte b = 20;
byte c = a+b; // 에러. a+b의 타입은 int !
// = 왜냐, 해당 리터럴을 JVM 피연산자 스택에 넣을 때,
// 내부적으로 int형 4바이트로 내기 때문이다. 
// 이항연산자 연산 시, int형으로 변환하여 연산하기에
// byte형에 int형을 밀어넣으려면 명시적 형변환을 해야한다.
byte c = (byte) (a+b);

Java 코딩테스트에서 매우 주의해야할 부분 ( JVM의 피연산자 스택 + 이항연산자 )

int a = 1000000;
int b = 2000000;

long c = a*b; // 오버플로우 : a*b 에 담겨있는 값이 묵시적으로 int값임. 그래서 이미 오버플로우.
// a*b의 결과는 내부적으로 int값으로 받는다. ( 리터럴 )
// 이렇게 오염된 값을 long에 넣어주면, 이미 오버플로우 난 값

// 아래는 OK!
int aaa = 1000000;
int bbb = 2000000;
long ccc = (long) aaa*bbb; // 괜찮. aaa 타입을 long으로 변경했고,
// 이항연산자는 표현 범위가 큰 타입에 맞춰 변환해주기에, long으로 결과가 나온다.
// byte, char, short와 같은 타입은 '무조건' int형으로 변경해서 계산.
// 왜냐하면 JVM의 피연산자 스택이 4byte를 기준으로 연산하기 때문이다.
System.out.println(ccc);
  • 이렇게 자바를 처음부터 다시 공부하면서, 자바 코딩테스트를 준비하니 얻는 것이 많다.
    그 중에서도 가장 큰 것은, JVM의 피연산자 스택과 오버플로우의 케이스를 접해보고, 미리 겪어볼 수 있었다는 점이다. 현업에 가면 이런 하나하나 사소한 것들을 신경쓰며 시큐어 코딩을 하는 좋은 신입 개발자로 시작하고 싶다.

0개의 댓글