다양한 접근 제한자

접근 제한자는 다양한 클래스간에 데이터를 공유하는 과정에서 접근할 수 있는 영역이 어디까지인지를 설정하기 위해 사용됩니다.

 

적절한 접근제한은 보안에 있어서 매우 효과적인 역할을 수행합니다.

 

접근제한자의 종류와 영역은 아래와 같습니다.

 

접근 제한자 적용시킬 수 있는 대상 적용범위
public 필드, 생성자, 메소드, 클래스 모든 접근 가능
protected 필드, 생성자, 메소드 동일 패키지 내 객체와 상속 관계의 객체
default <입력 생략시 default> 필드, 생성자, 메소드, 클래스 동일 패키지 내 객체
private 필드, 생성자, 메소드 현재 객체 내에서만

 

 

다른 패키지의 클래스를 호출하기

유사한 역할을 갖고있는 녀석들을 묶어 우리는 하나의 '패키지'로 관리하는것은 아실겁니다.

 

WorkSpace에서 확인해보면, 실제로도 별도의 폴더를 갖는 것을 볼 수 있습니다.

 

만약 위와같은 형태로 구조가 되어있고, LangMain.java에서 sample.java에 존재하는 클래스를 하나 갖고와 사용하려고 한다고 가정하겠습니다.

 

먼저 sample.java의 코드입니다.

 

간단하게 Sample이라는 클래스에 data라는 변수가 존재하는 것을 볼 수 있습니다.

 

이녀석을 LangMain.java에서 불러오기 위해서는 [ import ]를 통해 먼저 호출을 해줘야합니다.

 

import를 통해 호출하는 방식은 아래와 같습니다.

 

  • 특정 패키지의 하나의 클래스 호출하기

특정 클래스만 호출하기

 

  • 특정 패키지의 모든 클래스 호출하기

 

모든을 의미하는 별(*)을 이용하면 특정 패키지 이하의 모든 클래스를 호출합니다.

 

이렇게 호출된 클래스는 해당 코드에 존재하는 클래스와 동일한 방식으로 사용할 수 있습니다.

 

 

다만, 만약여러개의 클래스가 호출된 상태에서 클래스의 이름이 동일할 경우 import에서 단일 클래스를 호출했던것과 마찬가지로 패키지명을 포함한 모든 경로를 적어줘야합니다.

  1. ds3goj 2020.11.20 20:03

    도움되는 글 매우 잘 배우고 가요

1. Final 필드

Java에서는 Final이라는 통해 한번 정의된 변수의 값이 수정되지 못하도록 만들 수 있습니다.

 

class Math{

    final pi = 3.14;

    //final [타입] [필드명];

}

 

위와같은 형태로 만들어 줄 수 있습니다. 만약 클래스 내에서 필드(변수)를 초기화하지 않았다면, 단 한번 변수를 초기화 할 수 있습니다.

 

일반적으로 이와같은 녀석을 '상수'라고 부르는 경우가 종종있습니다.

 

왜냐하면, 수정이 불가능하기 때문이죠.

 

하지만, C언어에서 '상수'를 사용해본 경험이 있으신 분이라면, 조금 이상하다는 느낌을 받을 수 있습니다.

 

왜냐하면, 일반적으로 '상수'라는 녀석은 '전역변수'처럼 사용되기 때문입니다.

 

 

즉, 위와같이 클래스 내에서 final이라는 녀석을 통해 선언된 필드의 값은 클래스의 객체별로 서로 다른 값을 갖을 수 있다는 문제가 있습니다.

 

 

 

2. Static Final

위의 문제점을 해결하기 위하여 앞에 Static이라는 녀석을 붙여줍니다.

 

이전장에서, 그리고 처음 [JVM 메모리 관리]부분에서 언급했던것처럼 static이라는 녀석은 [메소드 영역]에서 관리되기 때문에 전역변수처럼 사용됩니다.

 

즉, static final로 선언된 값은 코드 전역에서 불편의 값을 갖는 필드, 즉 '상수'라고 불릴만한 자격을 갖게됩니다.

 

 

 

상수를 선언할 때에는 항상 

 

static final 필드 (=초기값);

 

위와같은 형태로 만들어줄 것! 꼭 기억합시다.

1. 인스턴스 멤버

인스턴스 멤버라고 불리는 녀석들은 클래스와 같은 객체를 생성했을 때, 함께 따라오는 '필드'와 '메소드'를 말합니다.

 

때문에 일반적인 '필드'와 '메소드'를 '인스턴스 필드'와 '인스턴스 메소드'라고 불러도 상관없습니다.

 

 

인스턴스 멤버가 갖고있는 특징은, [ 객체가 생성된 후 사용할 수 있다 ]라는 것입니다.

 

즉, Car라는 클래스가 존재하고, 여기에 'name'이라는 필드와 'go()'라는 메소드가 존재한다고 가정하면,

 

Car bmw = new Car(); 

 

라는 코드를 통해 객체를 생성한 후

 

bmw.name 또는 bmw.go() 라는 형태를 통해 접근이 가능하다는 특징을 갖고있습니다.

 

또한 [ Car benz = new Car(); ]라는 녀석을 통해 새로운 객체를 만들었다면, 당연하게도 benz가 참고하고있는 객체의 인스턴스 멤버는 bmw가 참조하는 인스턴스 멤버와 서로 다를수 있습니다.

 

 

 

2. 정적 멤버

일반적으로 'Static'이라는 녀석이 붙은 '필드'와 '메소드'를 정적 멤버라고 부릅니다.

 

public class Car {

    static String name;

    static void go() {...}

}

 

위와같은 클래스가 존재한다면, static으로 선언된 name과 go라는 필드와 메소드는 '정적멤버'입니다.

 

이 녀석들의 특징은 '객체'에 소속된 녀석들이 아닌, 클래스라는 녀석에 소속된 녀석들입니다.

 

즉, 위의 Car라는 클래스에 소속된 녀석이기 때문에 Car라는 클래스를 사용하는 모든 객체들은 동일한 필드와 메소드를 공유한다는 의미입니다.

 

또한, Static으로 선언된 녀석들은 앞서 [ JVM의 메모리 관리 ]부분에서 말씀드린것처럼 '클래스로더'가 코드를 읽는 과정에서 [메소드영역]에 올라가기 때문에 프로그램의 시작지점부터 호출될 수 있으며, 어디서든 호출이 가능하다는 특징을 갖고있습니다.

 

 

 

 

더보기

추가적으로 Class내에서 정적멤버를 초기화하는 방식은 두 가지가 있습니다.

1) 기본

public static Car {

    static String name;

}

 

2) 블록방식

public static Car {

    static String name;

 

    static {

        name = "The Name is BMW!";

    }

}

 

 

아래 블록방식은 클래스 로더가 static으로 선언된 부분을 읽어 수행한다는 것을 응용한 것으로 불 수 있습니다.

따라서 static으로 선언된 name이 먼저 메소드 영역에 올라가고, 이 후 static {...} 부분을 클래스로더가 실행하여 name이라는 값을 수정하게되도록 만들어주는 형태입니다.

생성자 오버로딩

클래스 내에서 사용하는 생성자는 오버로딩이라는 방식을 통해 사용자가 다양한 방식으로 클래스를 초기화 할 수 있도록 지원하고 있습니다.

 

유의할 부분은 생성자 오버로딩시 [매개변수의 타입, 개수, 순서]를 다르게 해줌으로서 생성자를 구분 할 수 있는 근거를 만들어야합니다.

 

몇개의 샘플을 보겠습니다.

 

위와같이 Box 라는 이름의 클래스를 만들어 주었습니다.

 

또한 4개의 생성자를 만들어 주었습니다.

 

하지만, 기본적으로 아래와 같은 녀석이 숨어있습니다.

 

 

기본생성자라는 이름으로 불리는 녀석으로 아무런 매개변수를 갖고있지 않는 녀석으로 단순히 new함수를 통해 클래스를 초기화하는 역할만 수행하는 녀석입니다.

 

 

따라서 위 코드는 총 5개의 생성자(기본생성자 포함)를 갖고있습니다.

 

이 생성자들은 아래와 같은 방식으로 호출되어 클래스 내 필드를 초기화하게됩니다.

 

 

1. For 반복문

Java에서는 두 가지 형태의 for 반복문을 사용할 수 있습니다.

 

하나는 c언어에서 사용했던것과 같은 방식, 다른 하나는 파이썬의 반복문과 같은 배열값을 통으로 넣는 반복문입니다.

 

하나씩 확인해보겠습니다.

 

 

int[] x = {1,2,3};

 

이라는 배열의 값을 하나씩 출력하는 코드를 반복문을 사용하여 만들어보겠습니다.

 

 

먼저 첫번째 방식인 C언어 느낌의 반복문입니다.

 

 

 

 

 

다음은 [향상된 for문]입니다.

 

 

두 가지 방식을 비교해보면 아래의 모습이 훨씬 간단한 것을 볼 수 있습니다.

 

아래 반복문은 x라는 배열의 값을 For문이 실행될 때 마다 하나씩 v에 넣어주는 형태로 작동합니다.

 

때문에 v의 값은 x라는 배열의 값이 들어갈 수 있는 타입으로 선언해 주어야 합니다.

 

간단하네요.

1. 배열의 복사

리스트와 다르게 배열은 처음 초기화된 크기를 변경 할 수 없다는 단점을 갖고있습니다.

때문에 기존에 존재했던 배열의 크기가 작아 추가 공간이 필요하게 된다면, 기존보다 큰 배열을 생성하고 거기에 기존에 데이터를 복사해 넣어줘야 할 필요가 있습니다.

 

이를위해 단순히 반복문을 활용하여 하나씩 데이터를 넣어줄 수도 있지만, 상당히 귀찮은 일이 아닐 수 없습니다.

 

이를 쉽게 해주는 함수가 존재합니다.

 

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

// Ststem.arraycopy( 배열 A, 복사 시작점, 배열 B, 붙여넣을 시작점, 복사할 길이 );

// 배열A를 배열B에 복사

 

실행결과

 

위의 코드를 보게되면 arr1을 arr2의 1번 index부터 복사한것을 볼 수 있습니다.

 

1. Java에서 비교연산자

프로그래밍 언어에서 자주 사용되는 '=='과 '!='이라는 두 연산자는 두 변수가 동일한 값을 갖고있는지 파악하는 용도로 많이 사용됩니다.

 

특히 조건문에서 많이 사용되죠.

 

가볍게 사용하고 있지만, Java에서 이 연산자들은 단순히 동일한 값을 확인하는 용도로 사용되지 않습니다.

 

왜냐하면, 이 연산자의 양옆에 오는 값이 동일한 것인지를 확인하는 것이 아닌, 양옆에 오는 두 객체가 동일한 참조를 하는지 확인하는 용도로 사용되기 때문입니다.

 

 

앞서 JVM가 메모리 영역을 어떻게 관리하는지 생각하며 진행해보겠습니다.

 

JVM Stack 영역에 저장되어있는 '참조 변수'는 아래와 같은 구조로 저장되어있습니다.

 

var1 Heap 3번
var2 Heap 8번
var3 Heap 11번

 

각각의 '참조 변수'는 힙 영역의 특정 위치를 가리키고있고, 이를통해 힙영역에 저장되어있는 실제 데이터에 접근하게됩니다.

 

만약 Heap영역 3번과 8번이 동일한 값을 갖고있다면, var1 == var2라고 했을 때 어떤 값을 반환받을까요?

 

정답은 'False'입니다.

 

힙 영역에는 둘 다 같은 값은 갖고있다해도 참조하는 Heap영역의 주소가 다르기 때문에 False라는 값을 반환하게됩니다.

 

 

 

2. 객체의 생성

위와같이 Java라는 녀석에서 '참조변수'는 참조하는 힙영역이 어디냐에 따라 True나 False를 반환하지만, 실제 코드를 사용할 때에는 가끔 이러한 것들이 이해가 안되는 경우를 볼 수 있습니다.

 

 

만약 아래와 같은 코드를 수행했다고 생각해봅시다.

 

String user1 = "YoungQ";

 

위의 코드를 수행하면 Heap영역 어딘가에 "YoungQ"라는 데이터를 갖고있는 객체가 생성될 것입니다. 여기서는 그곳을 Heap 1번이라고 가정하겠습니다.

 

Heap1 : "YoungQ"

 

위와같이 Heap영역에 1번에 "YoungQ"라는 값이 들어간다면, user1은 Heap1을 참조하게 될 것입니다. 그럼 전체적으로 보면 아래와 같은 형태가 됩니다

 

Stack 영역 Heap 영역
user1 : Heap1 Heap1 : "YoungQ"

 

 

그럼 여기서 아래와 같은 코드를 수행하면 어떻게 될까요?

 

String user2 = "YoungQ";

 

user1과 마찬가지로 새로운 Heap2 영역에 "YoungQ"라는 데이터가 들어갈 것이라고 생각되지만, 그렇게되면 동일한 데이터를 위해 여러 메모리를 할당하는 형태가되기 때문에, 메모리 낭비가 문제가 발생하여 Java에서는 다르게 이를 처리합니다.

 

방식은 간단합니다. 동일한 데이터가 이미 Heap영역에 존재한다면, 그곳을 참조하도록만 만들어주면 되는것이죠.

 

메모리상에는 아래와 같이 들어갑니다.

 

Stack 영역 Heap 영역

user1 : Heap1

user2 : Heap1

Heap1 : "YoungQ"

 

일반적으로 이러한 동시참조의 문제는 user1의 데이터를 변경했을 때, user2의 데이터도 변경되는 문제가 발생한다는 것입니다.

 

그렇게되면 정말 큰 문제가 되죠.

 

하지만, 걱정하지마세요. 하나의 데이터를 변경하게되면, 변경된 데이터로 새로운 객체를 만들어 참조할 힙영역의 주소를 알아서 변경해주니까요.

 

더보기

참고로 원하는 경우 새로운 객체를 만드는 방법도 존재합니다.

 

String user1 = "YoungQ";

String user2 = new String("YoungQ");

 

이와같이 코드를 작성하면 힙 영역에 두 개의 "YoungQ"라는 데이터를 갖은 객체가 생성됩니다.

 

실행결과

 

 

3. NULL 참조

값이 존재하지 않는 경우를 흔히 null로 표시하는 경우가 많이 있습니다.

 

참조변수도 이러한 null값을 갖을 수 있습니다.

 

String user3 = null;

 

위의 코드에서 user3라는 변수는 존재하지만, user3는 어떠한 영역도 참조하지 않는 변수를 의미합니다.

 

이러한 null은 Java 뿐만아니라 다양한 곳에서 사용됩니다. 하지만, 유독 Java에서 null이라는 녀석 문제가 되는데 그 이유는 Java가 대표적인 객체지향 언어이기 때문이죠.

 

많은 것들이 객체 단위로 이뤄지기 때문에 툭하면 NPE(Null Pointer Exception)이라는 애러를 발생시킵니다.

 

Null 값을 참조하려하면 발생하는 녀석이죠.

 

 

실행결과

 

위와같이 말이죠.

1. JVM의 메모리 관리

Java 프로그램을 실행하게되면 운영체제로부터 JVM(Java Vitrual Machine)은 메모리를 할당받고 이를 몇가지 영역으로 세분화하여 관리하게됩니다.

 

JVM은 메모리를 메소드 영역(Method Area), 힙 영역(Heap Area), 스레드 단위(Thread)로 구분하여 관리하게됩니다.

 

각가의 영역에서 어떤 데이터들을 관리하는지 알아보겠습니다.

 

 

2. 메소드 영역(Method Area)

메소드 영역에서는 '.class 파일'들을 읽어들여 각각의 클래스별로 런타임 상수풀, 필드 데이터, 메소드 데이터, 메소드 코드, 생성자 코드 등을 저장하게됩니다.

 

일반적으로 static으로 선언된 값들 역시 이 과정에서 메소드 영역에 저장되게됩니다.

 

이곳에 생성된 데이터는 JVM이 실행되는 순간에 자동으로 생성됩니다. 프로그래밍 과정에서 아직 수행되지 않은 static 영역의 데이터에 접근할 수 있는 이유도 static으로 선언된 값이 JVM이 실행되는 순간에 메모리에 올라가기 때문입니다.

 

여기에 저장된 값들은 모든 쓰레드가 공유하는 값이라는 것을 유의해야합니다.

 

 

 

3. 힙 영역(Heap Area)

힙 영역은 객체(Object)와 배열(Array)이 사용하는 공간이라고 볼 수 있습니다.

 

객체와 배열의 공통점은 이 두가지를 '참조'를 통해 호출한다는 것에 있습니다. C에서 주소값을 통해 배열에 접근하는 방식과 유사하다고 볼 수 있습니다.

 

Java에서 빠질 수 없는 녀석인 gc(Garbage Collector)가 바로 이 힙 영역에 존재하는 객체와 배열들을 정리하는 역할을 수행합니다.

 

어떠한 곳에서도 참조하는 변수나 필드가 없다면 이것을 쓰레기라고 판단하고 gc가 힙 영역에서 이 객체들을 제거하는 역할을 수행하게됩니다.

 

gc의 존재 덕분에 프로그래머가 객체를 제거하기 위한 코드를 작성할 필요가 없습니다. 다만, gc의 경우 항상 실행되고 있는 것이 아니라 필요에 따라 JVM에의해 실행되기 때문에 필요로 하지 않는 객체가 당분간 힙 영역에 존재하는 경우가 있을 수 있습니다.

 

만약 참조하지 않는 객체를 즉시 gc에 의해 정리하고 싶다면, System.gc();를 통해 gc를 실행시킬 수 있습니다.

 

 

 

4. 스레드(Therad)

JVM은 한 프로그램의 작업단위를 의미하는 스레드(쓰레드) 별로 데이터를 나눠 관리합니다.

 

이는 하나의 스레드에서 관리하는 데이터에 다른 스레드가 접근하지 못하는 일이 없도록 데이터의 관리단위를 나눠주기 위함이라고 생각합니다.

 

하나의 스레드 영역에는 JVM 스택(JVM Stack)이라는 영역이 존재합니다.

 

일반적인 데이터 관리형태와 마찬가지로 Stack 구조를 이용하여 스레드에서 사용하는 변수(지역변수 + 참조타입변수)들을 관리하게됩니다.

 

 

+ Recent posts