[윤성우의 열혈 Java 프로그래밍] Chapter 19 - 자바의 메모리 모델과 Object 클래스

Update:     Updated:

카테고리:

태그:

19-1. 자바 가상머신의 메모리 모델

- 가상머신은 운영체제 위에서 동작합니다.

자바 가상머신은 운영체제 위에서 실행되는 하나의 프로그램이다.
자바 프로그램은 자바 가상머신에서 실행되는 프로그램이다.

프로그램의 실행에 필요한 메모리 공간을 가리켜 ‘메인 메모리(Main Memory)’라고 한다.
그리고 이 메모리의 효율적인 사용을 위해서 운영체제가 응용프로그램에게 메모리를 할당해준다.

즉, 자바 가상머신은 운영체제가 할당해 주는 메모리 공간을 기반으로 스스로를 실행하면서 더불어 자바 응용 프로그램의 실행도 돕는다.

- 자바 가상머신의 메모리 살림살이

가상머신은 운영체제로부터 할당받은 메모리 공간의 효율적인 사용을 고민해야 한다.
그리고 이 저장공간의 효율적인 사용을 위해 ‘수납장’ 처럼 메모리 공간을 나누고 데이터를 특성에 따라 구분해서 저장한다.
image

각각의 메모리 영역에는 다음의 데이터들을 저장한다.

  • 메소드 영역(Method Area): 메소드의 바이트코드, static 변수
  • 스택 영역(Stack Area): 지역변수, 매개변수
  • 힙 영역(Heap Area): 인스턴스

- 메소드 영역(Method Area)

‘프로그램이 종료될 때까지’ 유지되어야 할 정보가 저장된다. 바이트 코드(Byte Code): 소스파일을 컴파일할 때 생성되는, 자바 가상머신에 의해 실행이 가능한 코드

image

바이트 코드도 메모리 공간에 존재해야 실행이 가능하다.
인스턴스의 생성 및 클래스 변수의 접근을 위해서는 먼저 해당 클래스의 바이트코드가 메모리 공간에 로딩되어야 한다.
즉, 메소드 영역은 특정 클래스의 정보가 메모리 공간에 올려질 때 채워지는 영역이다.

- 스택 영역(Stack Area)

스택은 지역 변수와 매개변수가 저장되는 공간이다. 이 둘은 다음과 같은 공통점이 있다.

중괄호로 구분되는 지역 내에서만 유효한 변수들이다.

image
image
image

즉, 중괄호 내에 할당된 이후에 해당 중괄호를 벗어나면 바로 소멸되는 특성의 데이터 저장을 위한 영역이
‘스택’이다.

- 힙 영역(Heap Area)

인스턴스를 스택이 아닌 힙이라는 별도의 영역에 할당하는 이유는 무엇일까?

인스턴스의 소멸 시점과 소멸 방법이 지역변수와 다르기 때문이다.

image

- 자바 가상머신의 인스턴스 소멸시기

인스턴스의 소멸시기는 더 이상 접근할 수 없어지는 시기,
즉 대상 인스턴스를 참조하는 게 더 이상 없을 때이다.
image

이러한 인스턴스의 소멸 방식을 가리켜 ‘가비지 컬렉션(Garbage Collection)이라고 한다.
정리하자면, 힙 영역은 가상머신에 의한 가비지 컬렉션이 일어나는 메모리 공간이다.
즉 스택 영역과 힙 영역을 구분하여 관리하는 이유는 가비지 컬렉션 때문이다.

(참고) 가비지 컬렉션이 발생하는 시점
인스턴스가 가비지 컬렉션의 대상이 되었다고 바로 소멸되지는 않는다.
가비지 컬렉션의 빈번한 실행은 시스템에 부담이기 때문에 성능에 영향을 미치지 않도록 실행 타이밍은 별도의 알고리즘을 기반으로 계산된다.

19-2. Object 클래스

- 인스턴스의 비교: equals 메소드

== 연산자는 참조변수의 참조 값을 비교한다.
따라서 서로 다른 두 인스턴스의 내용을 비교하려면 별도의 방법을 사용해야 한다.

class INum {
    private int num;

    public INum(int num) {
        this.num = num;
    }

    @Override
    public boolean equals(Object object) {
        if (this.num == ((INum) object).num) {
            return true;
        } else {
            return false;
        }
    }
}

public class ObjectEquality {
    public static void main(String[] args) {
        INum num1 = new INum(10);
        INum num2 = new INum(12);
        INum num3 = new INum(10);

        if (num1.equals(num2)) {
            System.out.println("num1, num2 내용 동일하다.");
        } else {
            System.out.println("num1, num2 내용 다르다.");
        }

        if (num1.equals(num3)) {
            System.out.println("num1, num3 내용 동일하다.");
        } else {
            System.out.println("num1, num3 내용 다르다.");
        }
    }
}

Object 클래스의 equals 메소드는 == 연산자와 마찬가지로 참조변수의 참조 값을 비교하도록 정의되어 있다.
따라서 equals 메소드는 호출을 통해 참조 값을 비교할 필요 없이 내용 비교가 이뤄지도록 오버라이딩 하라고 존재하는 메소드이다.

- 인스턴스 복사(복제): clone 메소드

/**
* Object 클래스에 정의되어있는 인스턴스의 복사를 위한 메소드
* 호출된 메소드가 속한 인스턴스의 복사본이 생성되고, 이렇게 만들어진 복사본의 참조 값이 반환된다.
*/
protected Object clone() throws CloneNotSupportedException

/**
* 이 인터페이스를 구현한 클래스의 인스턴스만 clone 메소드 호출 가능
*/
interface Cloneable

Cloneable 인터페이스의 구현은 무슨 의미일까?

이 클래스의 인스턴스는 복사해도 됩니다. 즉 clone 메소드 호출이 가능합니다.

이 인터페이스는 ‘마커 인터페이스’이다. 따라서 clone이 가능하다는 일종의 표식일 뿐이다.
인스턴스의 복사는 클래스에 따라 허용해서는 안되는 작업일 수 있다.
따라서 인스턴스 복사의 허용 여부는 클래스를 정의하는 과정에서 고민하고 결정해야 하고,
복사 허용이 가능하다는 결론이 나오면 Cloneable 인터페이스를 구현해서 clone 메소드의 호출이 가능하도록 하면 된다.

다음의 예시를 한 번 살펴보자.

class Point implements Cloneable {
    private int xPos;
    private int yPos;

    public Point(int x, int y) {
        xPos = x;
        yPos = y;
    }

    public String showPosition() {
        return "[ " + xPos + ", " + yPos + " ]";
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone(); // 언뜻 보면 별 의미 없어보임..
    }
}

public class InstanceCloning {
    public static void main(String[] args) {
        Point org = new Point(3, 5);
        Point cpy;

        try {
            cpy = (Point) org.clone();
            System.out.println(org.showPosition());
            System.out.println(cpy.showPosition());

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

image

clone() 메소드를 보면 단순히 상위 클래스인 Object 클래스의 메소드를 호출하고 있다.
이것만 보면 별 의미 없을 지 모르겠지만, 여기엔 중요한 의미가 있다.

메소드 오버라이딩을 통해서 접근 범위를 넓히는 것이 가능하다.

// Objcet class에 정의된 clone 메소드
protected Object clone() throws CloneNotSupportedException

// Point class에 정의된 clone 메소드
@Override
public Object clone() throws CloneNotSupportedException {
  return super.clone();
}

다음과 같이 메소드 오버라이딩을 통해 protected로 선언되어있던 상위 클래스의 메소드를 public으로 바꿔주었다.
(참고: 거꾸로 접근 범위를 제한하는 형태의 오버라이딩은 불가능하다.)

- clone 메소드: 얕은 복사

class Point implements Cloneable {
    private int xPos;
    private int yPos;

    public Point(int x, int y) {
        xPos = x;
        yPos = y;
    }

    public String showPosition() {
        return "[ " + xPos + ", " + yPos + " ]";
    }

    public void changePos(int x, int y) {
        xPos = x;
        yPos = y;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Rectangle implements Cloneable {
    private Point upperLeft;    // 좌측 상단 좌표
    private Point lowerRight;   // 우측 하단 좌표

    public Rectangle(int x1, int y1, int x2, int y2) {
        upperLeft = new Point(x1, y1);
        lowerRight = new Point(x2, y2);
    }

    public void changePos(int x1, int y1, int x2, int y2) {
        upperLeft.changePos(x1, y1);
        lowerRight.changePos(x2, y2);
    }

    public void showPosition() {
        System.out.println("좌측 상단: " + upperLeft.showPosition());
        System.out.println("우측 하단: " + lowerRight.showPosition());
        System.out.println();
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class ShallowCopy {
    public static void main(String[] args) {
        Rectangle origin = new Rectangle(1, 1, 9, 9);
        Rectangle copy;

        try {
            copy = (Rectangle) origin.clone();  // 인스턴스 복사

            origin.changePos(2, 2, 7, 7);   // 인스턴스의 좌표 정보 수정

            origin.showPosition();
            copy.showPosition();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

image

Rectangle을 복사하고, 좌표를 변경했다.
하지만 위의 코드의 결과를 보면 기대한 것과는 다르게 복사본도 변경되었음을 확인할 수 있다.

image

Rectangle 인스턴스의 멤버는 upperLeft, lowerRight 두 개의 참조변수로 이뤄져 있다.
따라서 복사 과정에서 참조변수가 지니는 참조 값이 그대로 새 인스턴스에 복사가 된다.
이를 ‘얕은 복사’라 한다.

- clone 메소드: 깊은 복사

image

우리가 위의 코드에서 의도한 것은
upperLeft와 lowerRight가 참조하는 Point 인스턴스까지 복사가 이루어지는 형태의 복사이다.
이러한 형태의 복사를 ‘깊은 복사’라고 한다.

class Point implements Cloneable {
    private int xPos;
    private int yPos;

    public Point(int x, int y) {
        xPos = x;
        yPos = y;
    }

    public String showPosition() {
        return "[ " + xPos + ", " + yPos + " ]";
    }

    public void changePos(int x, int y) {
        xPos = x;
        yPos = y;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Rectangle implements Cloneable {
    private Point upperLeft;    // 좌측 상단 좌표
    private Point lowerRight;   // 우측 하단 좌표

    public Rectangle(int x1, int y1, int x2, int y2) {
        upperLeft = new Point(x1, y1);
        lowerRight = new Point(x2, y2);
    }

    public void changePos(int x1, int y1, int x2, int y2) {
        upperLeft.changePos(x1, y1);
        lowerRight.changePos(x2, y2);
    }

    public void showPosition() {
        System.out.println("좌측 상단: " + upperLeft.showPosition());
        System.out.println("우측 하단: " + lowerRight.showPosition());
        System.out.println();
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        // Object 클래스의 clone 메소드 호출을 통한 복사본 생성
        Rectangle copy = (Rectangle) super.clone();

        // 복사본의 각 멤버에 각각 copy를 적용하여 저장한다.
        copy.upperLeft = (Point) upperLeft.clone();
        copy.lowerRight = (Point) lowerRight.clone();

        return copy;
    }
}

public class DeepCopy {
    public static void main(String[] args) {
        Rectangle origin = new Rectangle(1, 1, 9, 9);
        Rectangle copy;

        try {
            copy = (Rectangle) origin.clone();  // 인스턴스 복사

            origin.changePos(2, 2, 7, 7);   // 인스턴스의 좌표 정보 수정

            origin.showPosition();
            copy.showPosition();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

image

- 인스턴스 변수가 String인 경우의 깊은 복사

결론부터 말하자면 String 클래스는 clone 메소드를 통해서 복사가 불가능하다.
String은 Cloneable 인터페이스를 구현하지 않기 때문이다.

그렇다면 멤버 중 String 인스턴스를 가지고 있을 때, 다음과 같은 형태로 복사를 진행하면 될까..?

@Override
public Object clone() throws CloneNotSupportedException {
  // clone 메소드 호출을 통한 복사본 생성
  Person cpy = (Person)super.clone(); 
  
  // clone 메소드 대신 새로운 String 인스턴스를 new로 생성하는 형태로 깊은 복사를 구현..?
  cpy.name = new String(name);  
  
  return cpy;
}

String 클래스가 Cloneable 인터페이스를 구현하지 않는 이유는 무엇일까?

String은 문자열의 수정이 불가능하므로, 깊은 복사의 대상에서 제외해도 된다.

이렇게 한 번 결정되면 변경이 불가능하므로, 서로 다른 인스턴스가 하나의 String 인스턴스를 공유해도 문제가 되지 않는다.
따라서 clone 메소드를 구현할 때 String 같은 경우에는 단순히 아래처럼 만드어주면 된다.

@Override
public Object clone() throws CloneNotSupportedException {
  return super.clone();
}

- clone 메소드의 반환형 수정: Covariant Return Type

위의 코드에서 clone 메소드를 호출할 때 반환형이 Objcet이기 때문에 동시에 형 변환도 진행해야 했다.

Point org = new Point(1, 2);
Point cpy = (Point)org.clone(); // 형 변환 필요

하지만 자바 5 이후부터는 오버라이딩 과정에서 반환형의 수정을 허용한다.
다만, 모두 가능한 것은 아니고, 다음과 같이 클래스와 메소드의 반환형이 서로 일치해야한다.

image

// 다음과 같이 형 변환 없는 clone 메소드의 호출이 가능하다.
Point org = new Point(1, 2);
Point cpy = org.clone();  // 형 변환 필요 없음

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기