본문 바로가기
언어/자바의 정석

[자바의 정석] Chapter 6 객체 지향 프로그래밍 요약(3)

by chan10 2021. 2. 3.

JVM 메모리 구조

JVM은 시스템으로부터 프로그램 실행 시 필요한 메모리를 할당 받고 메모리를 용도에 따라 분류한다.

 

1.      메서드 영역(Method area) or 스태틱 영역(Static area)

JVM은 클래스가 실행 시 해당 클래스의 클래스 파일(.class)을 읽어서 이에 대한 정보(클래스 데이터)를 이곳에 저장한다. 클래스의 필드 부분에 선언된 전역 변수와 정적 맴버변수(static이 붙은 자료형)Static 영역에 저장되며 프로그램의 시작부터 종료가 될 때까지 메모리에 남아있다. 그렇기에 전역 변수가 프로그램이 종료될 때까지 어디서든 사용이 가능한 이유이긴 하나 무분별하게 사용하다 보면 메모리가 부족할 우려가 있으니 필요한 변수만 사용한다.

 

2.      Stack area ( or Call stack)

메서드 작업에 필요한 메모리 공간을 제공한다. 메서드가 작업을 수행할 때 stack영역의 메모리를 할당 받으며 이 메모리는 메서드 작업을 수행하는 동안 메서드 정보, 지역변수(매개변수)의 데이터 값이 저장되는 곳이다. 또한 지역변수 이므로 반복문 사용 시 괄호{ }안에 지역변수 또한 stack영역에 저장된다. 기본 타입 변수는 Stack영역에 직접 값을 가지고 있으나 참조 타입의 변수는 Heap영역이나 메서드 영역의 객체 주소를 가진다. Stack영역은 LIFO(Last In First Out) 구조로 마지막에 입력된 값이 제일 먼저 출력된다. 그렇기에 먼저 호출된 메서드가 제일 나중에 종료된다. 메서드가 작업이 종료되면 할당 받은 메모리는 자동으로 반환한다.

 

-       메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.

-       메서드가 수행을 마치고 나면 사용했던 메모리를 반혼하고 스택에서 제거된다.

-       호출스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다.

-       아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.

스택 - LIFO구조

3.      Heap 영역

프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 참조형의 데이터 타입을 갖는 즉, new연산자로 생성된 객체(인스턴스), 배열 등은 Heap영역에 저장된다. Stack영역에 있는 참조 변수는 Stack 영역의 공간에서 실제 데이터가 저장된 Heap영역의 참조값(reference value, 메모리에 저장된 주소를 연결해주는 값)new 연산자를 통해 리턴 받는다.(인스턴스 생성과 사용 참고) 이렇게 리턴 받은 참조 값을 갖고 있는 객체를 통해서만 해당 인스턴스를 다룰 수 있다. 만약 참조하는 변수나 필드가 없다면 의미 없는 객체가 되어 garbage collection의 대상이 되어 자동으로 회수한다.

기본형 매개변수와 참조형 매개변수

기본형 매개변수 : 변수의 값을 읽기만 할 수 있다.(read-only)

참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다.(read & write)

인스턴스 주소가 복사되어 값이 저장된 주소를 알기에 값을 읽어 오는 것은 물론 변경하는 것도 가능하다.

class Data{int x;}
 
class ParameterEx{
    public static void main(String[] args) {
        Data d = new Data();
        d.x = 10;
        System.out.println("main() : x = " + d.x);
        System.out.println();
        
        change(d.x);    // 기본 변수를 인자 값으로 호출
        System.out.println("After change(d.x)");
        System.out.println("main() : x = " + d.x); // 10, 원본 값 유지
        System.out.println();
        
        change(d);    // 참조 변수를 인자 값으로 호출
        System.out.println("After change(d)");
        System.out.println("main() : x = " + d.x); // 1000, 원본 값 변경
        
    }
    // Primitive Parameter (기본형 매개변수)
    static void change(int x) {
        x=1000;
        System.out.println("Primitive Parameter change() : x = " + x);
    }
    // Reference Parameter (참조형 매개변수)
    static void change(Data d) {
        d.x=1000;
        System.out.println("Reference Parameter change() : x = " + d.x);
    }
}
 
 

 

-       매개변수를 기본형으로 선언 시 원본의 값을 복사한 값을 받기에 변경하여도 원본에는 아무런 영향이 없다. , 기본형 매개변수는 변수에 저장된 값만 읽을 뿐 원본 값을 변경할 수는 없다.

-       매개변수를 참조형으로 선언 시 원본의 참조 변수에서 저장하고 있던 주소 값이 매개변수에 복사된다. 그렇기에 원본 참조변수와 매개변수가 같은 객체의 주소를 가리키게 된다. 그래서 매개변수로 값을 변경 시 원본의 값도 변경되는 것이다.

 

o  배열도 배열타입의 참조 변수이기에 배열을 매개변수로 받을 경우 참조형 매개변수와 동일한 동작을 한다.

import java.util.Arrays;
 
class ParameterEx{
    public static void main(String[] args) {
        int[] a = new int[] {1,7,2,9,10};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
    
    static void sort(int[] a){
        for(int i=0;i<a.length;i++) {
            for(int j=1;j<a.length-i;j++) {
                if(a[j]<a[j-1]) {
                    int temp = a[j];
                    a[j] = a[j-1];
                    a[j-1= temp;
                }
            }
        }
    }
}
 
 

-       매개변수 타입이 배열이므로 참조형 매개변수이다. 따라서 새로운 메소드를 호출하여 매개변수 배열을 정렬했으나 원본 배열에서도 영향을 미쳐 정렬이 되었다. 임시적으로 간단히 처리할 때는 별도 클래스 선언보다 이처럼 배열을 이용할 수도 있다.

 

참조형 반환타입

o  반환타입도 참조형이 될 수 있는데 메서드를 종료하면서 반환하는 타입이 참조형이라는 뜻으로 반환하는 객체의 참조된 주소를 반환한다.

class ParameterEx{
public static void main(String[] args) {
    int[] a = new int[] {1,7,2,9,10};
    int[] b = copy(a); // 참조변수 배열 c의 주소 값을 받아 배열 b에 저장
    System.out.println(a[0]);
    System.out.println(b[0]); //전달 받은 주소 값으로 인해 복사된 값 확인
    System.out.println(a[1]);
    System.out.println(b[1]); //b[1]은 복사 하지 않았기에 기본값으로 저장
 
}
 
static int[] copy(int[] a) {
    int[] c = new int[a.length];
    c[0]=a[0];
    return c; // 배열 c의 주소를 반환
}
}
 
 

-       생성한 객체를 main메소드 안에서 사용하기 위해서 새로운 객체의 주소를 반환해 주어야 한다.

 

재귀호출(recursive call)

o  메소드 내부에서 자신을 다시 호출하는 것을 재귀 호출(recursive call)이라고 하며, 재귀 호출을 하는 메소드를 재귀 메소드라고 한다. 호출된 메소드는 값에 의한 호출(call by value)’을 통해 원래의 값이 아닌 복사된 값으로 작업하기에 호출에 의한 메소드와 관계없이 독립적으로 수행이 가능하다.

o  재귀호출은 조건이 없는 경우 무한히 계속 호출을 하기에 반드시 재귀 호출을 멈출 수 있는 조건문이 같이 사용되어야 한다.

o  재귀호출은 매개변수 복사, 종료 후 복귀주소 저장 등의 이유로 반복문보다 수행시간이 더 오래 걸리지만 논리적은 간결함 때문에 사용한다. 효율적이라도 알아보기 힘든 것보다 비효율적이더라도 알아보기 쉬우면 논리적 오류 발생도 적고 수정하기도 용이하다. 그렇기에 재귀호출은 호출에 드는 비용보다 간결함이 주는 이득이 충분히 큰 경우에만 사용해야 한다.

class ParameterEx{
public static void main(String[] args) {
    int result = factorial(4);
    System.out.println(result);
}
static int factorial(int n) {
    // 매개변수의 유효성 검사
    if(n<=0 || n>12return-1;    // 유효 범위 이외의 값 입력 시 -1 반환(재귀 호출X)
    if(n==1return 1;// n의 값이 1로 들어온 경우 1을 반환 후 종료
    
    // 4 * factorial(4-1)로 매개변수 n의 값을 1씩 감소하여 재귀호출
    return n * factorial(n-1);
}
}
 
 

-       매개변수 값이 0이면 if(n==1)문 조건식 참이 아니기에 무한 재귀호출이 일어나고 13!의 값이면 메소드 반환타입인 int의 최대값(20)보다 크기 때문에 if(n<=0 || n>12)조건식을 주어 유효성을 검사한다.

 

// x의 1승부터 x^n승까지 값 구하기 
class ParameterEx{
    public static void main(String[] args) {
        int x=2;
        int n=4;
        long result = 0;
        for(int i=1;i<=n;i++) {
            result+=power(x, n);    //2^1+2^2+2^3+2^4
        }
        System.out.println(result);
        
    }
 
    static long power(int x, int n) {
        if(n==1return x;
        return x * power(x,n-1); 
    }
}
 
 

 

클래스 메소드(static메소드)와 인스턴스 메소드

o  클래스 변수와 같이 메소드 앞에 static이 붙어있으면 클래스 메소드이고 그렇지 않으면 인스턴스 메소드이다.

o  클래스 메소드는 클래스 변수처럼 객체를 생성하지 않고 클래스이름.매소드이름형태로 호출할 수 있다. 인스턴스 메소드는 반드시 객체를 생성해야만 호출할 수 있다.

o  인스턴스 매소드는 인스턴스 변수와 관련된 작업을 하는, 즉 메소드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메소드이다. 인스턴스 변수가 객체를 생성하야 사용할 수 있기에 인스턴스 메소드 또한 객체를 생성해야만 호출할 수 있다.

o  인스턴스와 관계없는(인스턴스 변수나 메소드를 사용하지 않는) 메소드를 클래스 메소드(static 메소드)로 정의하는 것이 일반적이다.

o  클래스 영역에 선언된 변수를 멤버변수라 한다. static이 붙은 변수를 클래스 변수(static 변수), static이 붙지 않은 변수를 인스턴스 변수라고 한다. 멤버 변수는 static변수 인스턴스 변수 모두를 칭하는 말이다.

 

1.     클래스를 설계할 때, 멤버변수 중 모든 인스턴스에 공통으로 사용하는 것에 static을 붙인다.

-       생성된 인스턴스는 서로 독립적이기에 인스턴스 변수 또한 서로 다른 값을 가지며 모든 인스턴스에서 같은 값이 유지되어야 하는 변수는 static을 붙여서 클래스 변수로 정의한다.

 

2.     클래스 변수(static 변수)는 인스턴스를 생성하지 않아도 사용할 수 있다.

-       static이 붙은 변수(클래스 변수)는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성되기 때문이다.

 

3.     클래스 메서드(static 메소드)는 인스턴스 변수를 사용할 수 없다.

-       인스턴스 변수는 인스턴스가 존재해야만 사용할 수 있으나 클래스 메소드는 인스턴스 생성 없이 호출이 가능하므로 클래스 메소드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있다. 그래서 클래스 메소드에서는 인스턴스 변수의 사용을 금지한다.

-       반면 인스턴스 변수가 존재한다는 것인 static변수가 이미 메모리에 존재한다는 것을 의미하기에 인스턴스 변수나 메소드는 static이 붙은 멤버들을 언제든지 사용할 수 있다.

 

4.     메소드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다.

-       메소드 작업내용 중에서 인스턴스 변수를 필요로 하지 않는다면 static을 붙이는 것이 좋다. 인스턴스 메소드는 실행 시 호출 되어야할 메소드를 찾는 과정이 필요하나 static 메소드는 이러한 과정이 없이 이미 메모리에 생성되어 있어 성능이 향상된다.

 

5.     정리

-       클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면 static을 붙여준다.

-       작성한 메소드 중에서 인스턴스 변수나 인스턴스 메소드를 사용하지 않는 메소드에 static을 붙을 것을 고려한다.

-       인스턴스 메소드[add( ),subtract( ) …]는 인스턴스 변수만으로 작업이 가능하기에 매개변수를 선언하지 않았고 add(long a, long b),subtract(long a, long b)… 메소드는 매개변수만으로 작업을 하기에 클래스 메소드(static)로 선언하였다.

-       클래스 메소드는 객체 생성 없이 바로 호출이 가능하고 인스턴스 메소드는 인스턴스 생성 후 호출이 가능하다.

class MyMath2{
long a, b; // 인스턴스 변수 생성
 
// 인스턴스변수 a,b를 이용해서 작업하므로 매개변수가 필요 없다.
// 클래스 변수도 매개변수로 이용할 수 있다.
long add()         { return a+b;}
long subtract() {return a-b;}
long multiply() {return a*b;}
double divide() {return a/b;}
 
// 인스턴스 변수는 사용 할 수 없기에 매개변수로 작업이 가능하다.
// 인스턴스 변수 대신 클래스 변수를 선언 시 매개변수 없이 클래스 변수를 이용해서 작업이 가능하다.
static long add(long a, long b)     { return a+b;}
static long subtract(long a, long b) {return a-b;}
static long multiply(long a, long b) {return a*b;}
static double divide(double a, double b) {return a/b;}
}
 
class MyMathTest2 {
public static void main(String[] args) {
    //클래스 메소드 호출. 인스턴스 생성 없이 호출가능
    System.out.println(MyMath2.add(200L, 100L));
    System.out.println(MyMath2.subtract(200L, 100L));
    System.out.println(MyMath2.multiply(200L, 100L));
    System.out.println(MyMath2.divide(200.0100.0));
    
    MyMath2 mm = new MyMath2();
    mm.a = 200L;
    mm.b = 100L;
    // 인스턴스 메소드는 객체생성 후에만 호출이 가능함
    System.out.println(mm.add());
    System.out.println(mm.subtract());
    System.out.println(mm.multiply());
    System.out.println(mm.divide());
}
}
 
 

클래스 멤버와 인스턴스 멤버간의 참조와 호출

o  같은 클래스에 속한 멤버들 간에는 별도 인스턴스 생성 없이 참조 또는 호출이 가능하나 클래스 멤버가 인스턴스 멤버를 참조 또는 호출할 때에는 인스턴스를 생성해야 한다.

o  이유는 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수도 있기 때문이다.

 

class TestClass{
    void instanceMethod() {}    //인스턴스 메소드
    static void staticMethod() {}    //클래스 메소드
    
    void instanceMehtod2() {    //인스턴스 메소드
        instanceMethod();    //OK, 다른 인스턴스 메소드 호출
        staticMethod();        //OK, static 메소드 호출
    }
    static void staticMethod2() {    //클래스 메소드
        instanceMethod();    //에러, 인스턴스 메소드 호출 불가
        staticMethod();        //static 메소드 호출
    }
}
 
 
class TestClass3 {
    int iv;
    static int cv;
    
    void instanceMethod() {    // 인스턴스 메소드에서
        System.out.println(iv);    // 인스턴스 변수 사용 가능
        System.out.println(cv); // 클래스 변수 사용 가능
    }
    
    static void staticMethod() {    // static 메소드에서
        System.out.println(iv);    // 에러, 인스턴스 변수 사용 불가능
        System.out.println(cv);    // 클래스 변수 사용 가능ㄴ
    }
}
 
 

-       같은 클래스내의 메소드는 서로 객체의 생성이나 참조변수 없이 직접 호출이 가능하지만 static메소드는 인스턴스 메소드를 호출할 수 없다.

-       인스턴스 메소드는 인스턴스 변수를 사용할 수 있으며, static메소드는 인스턴스 변수를 사용할 수 없다.

-       인스턴스 메소드는 인스턴스 멤버(메소드, 변수), static 멤버를 모두 사용할 수 있으나 클래스 메소드는 클래스 멤버만 사용이 가능하다.

 

class MemberCall {
    int iv = 10;
    static int cv = 20;
    
    int iv2 = cv;    //인스턴스 변수는 클래스 변수 사용 가능
//    static int cv2 = iv;    //에러, 클래스 변수는 인스턴스 변수 사용 불가
    static int cv2 = new MemberCall().iv;    // 객체 생성해야 사용 가능
    
    static void staticMethod1() {
        System.out.println(cv);
//        System.out.println(iv);    //에러, 클래스 메소드에서 인스턴스 변수 사용 불가능
        MemberCall c = new MemberCall();
        System.out.println(c.iv);  // 객체를 생성해줘야 인스턴스 변수 참조 가능
    }
    
    //인스턴스 메소드는 인스턴스,클래스 변수 사용 가능
    void instanceMethod1() {    
        System.out.println(cv);
        System.out.println(iv);
    }
    
    static void staticMethod2() {
        staticMethod1();
//        instanceMethod1();    //에러, 클래스 메소드에서 인스턴스 메소드 호출 불가능
        MemberCall c = new MemberCall();
        c.instanceMethod1();    // 객체 생성 후 호출 가능
    }
    // 인스턴스 메소드에서는 인스턴스,클래스 메소드 
    // 모두 인스턴스 생성 없이 사용 가능
    void intanceMethod2() {
        staticMethod1();
        instanceMethod1();
    }
}
 
 

-       클래스 멤버는 언제나 참조, 호출이 가능하기에 인스턴스 멤버가 클래스 멤버를 사용하는 것에 문제가 없으며 클래스 멤버끼리도 참조, 호출에 문제가 없다.

-       그러나 인스턴스 멤버는 인스턴스 생성 후에만 참조, 호출이 가능하기에 클래스 멤버가 인스턴스 멤버를 참조, 호출하기 위해서는 인스턴스 멤버를 생성해야 한다.

-       하나의 인스턴스 멤버가 존재한다는 것은 인스턴스가 이미 생성됨을 의미한다. 그렇기에 다른 인스턴스 멤버들도 모두 존재하기에 인스턴스 멤버간의 호출에는 아무런 문제가 없다.