본문 바로가기
개발/Java

자바 기본과 객체 지향 (4)

by 컴쏘 2024. 10. 31.

자바를 이해하기 위해 필요한 기본 개념들을 몇가지 정리해보자.

 

main 메서드의 static

자바의 main 메서드는 왜 static 일까?

 

간단하게 정리하면, 다음과 같다. 

main 메서드는 프로그램의 시작점으로, 프로그램이 실행되면 가장 먼저 호출되는 메서드이다. 

따라서, main 메서드는 누군가 호출하기 전에 미리 메모리에 있어야 하기 때문에 static을 붙인다. 

만약, main이 메모리에 없다면, 시작점인 main() 메소드를 호출하려고 하는데 메모리에 main()이 없어 실행할 수 없게 된다.

 

static이 뭐길래?
static은 java 프로그램이 실행하기 전에, static 함수나 static 변수를 첫 단계로 메모리에 올려 프로그램을 실행시킨다. (static이 실행 시 1순위)

또한, static은 종료될 때까지 사라지지 않는다. (이는 Garbage Collector에 의해 메모리에서 정리되지 않기 때문이다.)

 

 

상수 vs. 리터럴 

상수와 리터럴은 뭐가 다를까? 

 

상수(Constant) : 변수처럼 값을 저장할 수 있는 공간이지만, 변수와 달리 한번 값을 저장하면 다른 값으로 변경할 수 없다. 

  • 한번 선언하면 고정된다. (선언과 동시에 초기화)
  • 변수처럼 선언하되, 변수 타입 앞에 final을 붙여주면 된다. 
  • 상수의 이름은 모두 대문자로 한다는 규칙이 있다. 

리터럴(Literal) : 변하지 않는 데이터 그 자체를 의미한다.

  • 데이터 그 자체 값을 의미한다. 

변수, 상수, 리터럴 ❘ 출처 : https://hstory0208.tistory.com/entry/Java-상수와-리터럴-constant-와-literal-이란

 

그렇다면, 왜 구분할까? 

 

상수는 리터럴에 의미 있는 이름을 붙여서, 코드의 이해와 수정을 돕는다. 

따라서, 상수를 두게되면, 다른 값으로 변경한다고 해도 여러 곳을 변경할 필요 없이, 상수만 변경하면 되므로 수정의 번거로움을 줄여준다. 

 

Primitive Type vs. Reference Type

간단히 설명하면, Primitive Type은 변수에 값 자체를 저장하고, Reference Type은 메모리 상에 객체가 있는 위치를 저장한다. 

 

좀 더 자세히 알아보자. 

 

Primitive Type : 가장 기본적인 데이터 타입으로, java에서는 총 8가지의 Primitive Type이 있다.

  • boolean, byte, short, int, long, float, double, char 
  • 반드시, 사용하기 전에 선언되어야 한다. 
  • 비 객체 타입이기 때문에, null을 가질 수 없다. (default 값이 있기 때문) 
  • 실제 값을 저장하기 때문에 Stack 메모리에 저장된다. 
  • 컴파일 시점에 데이터의 표현 범위를 벗어나면 컴파일 에러가 발생한다. 
  • Generic Type에 사용 불가하다. 

Reference Type : Primitive Type을 제외한 타입들 

  • 빈 객체를 의미하는 null이 존재
  • 값이 저장되어 있는 주소 값을 저장하는 공간으로 heap 메모리에 저장된다. 
  • 런타임 에러가 발생한다. 
  • Generic Type에 사용 가능하다. 

 

Java는 Call by Value이다.

Java는 오직 Call by Value로만 동작한다.

 

더보기

Call by Value?

 

함수의 매개 변수 전달 방법에는 Call by Value와 Call by Reference가 있다.

 

Call by Value : 함수가 인수로 전달받은 값을 복사하여 처리하는 방식 

  • 값을 복사하여 전달하기 때문에 함수 내에서 값을 변경해도 원본 값은 변경되지 않는다. 

Call by Reference : 함수가 인수로 전달되는 변수의 참조 값을 함수 내부로 전달하는 방식

  • 인자로 전달되는 값이 변수의 주소이다. 
  • 함수 내에서 인자로 전달된 값을 변경하면, 호출한 쪽에서도 해당 변수의 값이 변경된다. 

 

왜 Call by Value로만 동작할까? 

 

Java의 변수 생성 시 메모리에 저장되는 방식을 살펴보자.

JVM 메모리 변수 저장 위치 ❘ 출처 : https://bcp0109.tistory.com/360

  • Primitive Type : Stack 영역에 변수와 함께 저장 
  • Reference Type : 객체는 Heap에, 변수는 Stack에 저장, 변수가 객체의 주소값을 가지고 있다. 

 

위의 저장 방식을 기억하면서, 좀 더 자세히 알아보자. 

더 자세한 예시를 보고 싶으시다면, 다음의 글을 확인해주세요.

Primitive Type 인 경우

메서드(A)에서 Primitive Type을 파라미터로 가지고 있는 메서드(B) 를 호출하게 되면, Stack에 메서드(B) 영역(B-Stack)이 생기고, 해당 영역(B-Stack)에 동일한 이름을 가진 변수가 존재하게 된다. 따라서, (B-Stack)영역의 값을 바꿔도, 기존의 메서드(A)의 변수에는 영향이 없다. 

 

Reference Type인 경우

메서드(A)에서 Reference Type을 파라미터로 가지고 있는 메서드(B) 를 호출하게 되면, Stack에 메서드(B) 영역(B-Stack)이 생기고, 해당 영역(B-Stack)에 동일한 이름을 가진 변수가 존재하게 된다. 그리고 (B-Stack)에 존재하는 변수가 Heap 영역에 있는 객체를 바라보게 된다. ((A-Stack)에 있는 변수가 바라보는 객체와 동일)

 

만약, 이상태에서 수정을 하면 객체의 값이 수정된다.

하지만, (B-Stack) 존재하는 변수에 새로운 객체(newO)를 할당해주면 (B-Stack)에 존재하는 변수는 새로운 객체를 바라보게 된다. (이때의 새로운 객체(newO)를 수정하면 메서드(A)의 객체에는 영향이 없다.)

 

여기서 위의 상황이 Call by Reference와 뭐가 다른가? 

 

Call by Reference는 실제 주소를 전달하지만, Java의 경우는 객체의 주소를 가지는 변수를 복사해서 전달한다. 

따라서, Java에서의 전달 값은 객체의 주소를 가지고 있는 변수의 복사본일 뿐이다. 

 

Java 직렬화

직렬화는 Java의 Obejct 또는 Data를 다른 컴퓨터의 Java 시스템에서도 사용할 수 있도록 바이트 스트림 형태로 연속적인 데이터로 변환하는 포맷 변환 기술이다. 

 

바이트 스트림 ?
스트림은 클라이언트나 서버 간에 출발지 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다.

Java는 스트림의 기본 단위를 바이트로 두고 있기 때문에, 네트워크, 데이터 베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리해야 한다.

 

 

그럼 반대인 역직렬화는? 

바이트로 변환된 데이터를 원래 Java 시스템의 Object 또는 Data로 변환하는 기술이다.

 

시스템적으로 본다면, JVM의 Heap 혹은 Stack 메모리에 상주하고 있는 객체 데이터를 직렬화를 통해 바이트 형태로 변환하여 데이터베이스나 파일과 같은 외부 저장소에 저장해두고, 다른 컴퓨터에서 이 파일을 가져와 역직렬화를 통해 Java 객체로 변환하여 JVM 메모리에 적재하는 것이다. 

 

왜 필요할까?

직렬화를 응용한다면, 휘발성이 있는 캐싱 데이터를 영구 저장이 필요할 때 사용할 수 있다. 

 

예를 들어, JVM의 메모리에서만 상주되어있는 객체 데이터가 시스템이 종료되더라도 나중에 다시 재사용될 수 있을 때 영속화를 하면 좋다. 다음과 같은 곳에 사용될 수 있다. 

 

서블릿 세션 (Servlet Session) 

  • 세션 데이터를 저장하거나 서버 간 공유가 필요한 경우 : 사용자의 세션 정보를 데이터베이스나 외부 저장소에 저장하거나 여러 서버에서 공유해야 할 때는 직렬화가 필요하다. 
  • 톰캣 세션 클러스터링은 여러 톰캣 서버 간에 세션을 공유하는 방식입니다. 이를 통해 사용자가 서버 간 이동(로드 밸런싱 등)을 하더라도 동일한 세션 데이터를 사용할 수 있다. 

캐시 (Cache) 

  • 데이터베이스로부터 조회한 객체 데이터가 다시 필요할 때 DB 조회가 아닌, 직렬화를 통해 메모리나 외부 파일에 저장해두었다가, 역직렬화하여 사용하는 캐시 데이터로서 이용 가능하다. 
  • 요즘에는 Redis와 같은 캐시 DB를 많이 사용하는 편이다. 

직렬화의 단점

  1. 직렬화는 용량이 크다. : 객체에 저장된 데이터 값 뿐만 아니라 타입 정보, 클래스 메타 정보를 가지고 있으므로 용량을 은근히 많이 차지한다. 따라서. 장기간 저장해야 하는 정보는 직렬화를 지양해야 한다.
  2. 역직렬화는 위험하다. : 역직렬화는 가젯(gadget, 잠재적으로 위험한 동작을 수행하는 메서드)에 의해 프로그램 코드가 전체 공격 범위에 들어가게 되거나, 객체를 직렬화하여 외부로 전송하는 과정에서 중간에 누가 가로채 파일 바이트 내용을 조작해 공격할 수 있다. 

따라서, 신뢰할 수 없는 데이터는 절대 역직렬화하면 안되고, 꼭 해야만 한다면, 역직렬화필터링과 같은 방어 기법들을 사용해야 한다. 

 

참조 1 
참조 2 
참조 3
참조 4
참조 5