본문 바로가기

Programming/Java

[Java] 김영한의 자바 중급 1편 #8 - 중첩 클래스, 내부 클래스 - 2

반응형

김영한 선생님의 자바 중급을 듣고 정리한 내용입니다.

 

 


 

1. 지역 클래스

  • 지역 클래스(Local Class)는 내부 클래스의 특별한 종류의 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다. 예를 들어 지역 클래스도 내부 클래스이므로 바깥 클래스이 인스턴스 멤버에 접근할 수 있다.
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.

class Outer {
	
	public void process() {
		
		// 지역 변수
		int localVar = 0;
		
		// 지역 클래스
		class Local {
			...
		}
		
		Local local = new Local();
	}
}

 

1.1. 지역 클래스 예제 1

 

 

지역 클래스의 접근 범위

  • 자신의 인스턴스 변수인 value에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 지역 변수인 localVar에 접근할 수 있다.
  • 자신이 속한 코드 블럭의 매개변수인 paramVar에 접근할 수 있다. → 참고로 매개변수도 지역 변수의 한 종류이다.
  • 바깥 클래스의 인스턴스 멤버인 outInstanceVar에 접근할 수 있다. → 지역 클래스도 내부 클래스의 한 종류이기 때문이다.

 

1.2. 지역 클래스 예제 2

 

 

 


2. 지역 클래스 - 지역 변수 캡처1

변수의 생명 주기에 대해서

클래스 변수 : 프로그램 종료 까지 (메서드 영역)

클래스 변수는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재한다.

인스턴스 변수 : 인스턴스 생존 기간 (힙 영역)
인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기 전까지 존재한다.

지역 변수 : 메서드 호출이 끝나면 사라진다. (스택 영역)
지역 변수는 스택 영역의 스택 프레임 안에 존재한다. 따라서 메서드가 호출되면 생성되고 메서드 호출이 종료되면 스택 프레임이 제
거되면서 그 안에 있는 지역 변수도 모두 제거된다.
생존 주기가 아주 짧다. 참고로 매개변수도 지역 변수의 한 종류이다.

 

 

2.1. 지역 클래스 예제 3

 

 

LocalPrinter 인스턴스 생성 직후 메모리 그림

 

지역 클래스 인스턴스의 생존 범위

  • 지역 클래스로 만든 객체도 인스턴스이기 때문에 힙 영역에 존재한다. 따라서 GC 전까지 생존함
    • LocalPrinter 인스턴스는 process() 메서드 안에서 생성된다. 그리고 process() 메서드에서 main()으로 생성한 LocalPrinter 인스턴스를 반환하고 printer 변수에 참조를 보관한다. 따라서, LocalPrinter 인스턴스는 main()이 종료될 때까지 생존한다.
  • paramVar, localVar와 같은 지역 변수는 process() 메서드를 실행하는 동안에만 스택 영역에서 생존한다. process() 메서드가 종료되면 process() 스택 프레임이 스택 영역에서 제거되면서 함께 제거된다.

 

LocalPrinter.print() 접근 메모리 그림

 

  • LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar에 접근한다. 이 부분은 인스턴스의 필드를 참조하기 때문에 특별한 문제가 없음
  • LocalPrinter 인스턴스는 print 메서드를 통해 스택 영역에 존재하는 지역 변수도 접근하는 것처럼 보인다.. 하지만 스택 영역에 존재하는 지역 변수를 힙 영역에 있는 인스턴스가 접근하는 것은 생각처럼 단순하지 않다.

 

process() 메서드의 종료

 

  • 지역 변수의 생명 주기는 매우 짧다. 반면에 인스턴스 생명주기는 GC 전까지 생존할 수 있다.
  • 지역 변수인 paramVar, localVar는 process() 메서드가 실행되는 동안에만 생존할 수 있다. process() 메서드가 종료되면 process()의 스택 프레임이 제거됨녀서 두 지역 변수도 함께 제거된다.
  • 여기서 문제는 process() 메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다는 점이다.

 

process() 메서드가 종료된 이후에 지역 변수 접근

 

 

  • process() 메서드가 종료된 이후에 main() 메서드 안에서 LocalPrinter.print() 메서드를 호출한다.
  • LocalPrinter 인스턴스에 있는 print() 메서드는 지역 변수인 paramVar, localVar에 접근해야 한다. 하지만 process() 메서드가 이미 종료되었으므로 해당 지역 변수들도 이미 제거된 상태이다.

그런데 실행 결과를 보면 localVar, paramVar 모두 정상적으로 출력된다…!

 

 

 

3. 지역 클래스 - 지역 변수 캡처 2

지역 클래스는 지역 변수에 접근할 수 있다.

지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아있지만 지역 변수는 이미 제거된 상태일 수 있다.

 

지역 변수 캡처

자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다.

→ 변수 캡처(capture)라고 한다. 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처한다.

 

3.1. 지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정 1

 

[1] LocalPrinter 인스턴스 생성 시도 : 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다. → LocalPrinter 클래스는 paramVar, localVar 지역 변수에 접근한다.

 

[2] 사용하는 지역 변수 복사 : 지역 클래스가 사용하는 지역 변수를 복사한다. → paramVar, localVar 지역 변수를 복사한다.

 

3.2. 지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정 2

 

 

[3] 지역 변수 복사 완료 : 복사한 지역 변수를 인스턴스에 포함한다.

 

[4] 인스턴스 생성 완료 : 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다. 이제 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.

 

 

 

3.3. 코드로 캡처 변수 확인하기

    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);
        // printer.print()를 나중에 실행한다.
        // process()의 스택 프레임이 사라진 이후에 실행
        printer.print();

        // 추가하기
        System.out.println("필드 확인");
        for (Field field : printer.getClass().getDeclaredFields()) {
            System.out.println("field : " + field);
        }

    }

실행 결과

필드 확인

// 인스턴스 변수 : LocalPrinter.value
field : int nested.local.LocalOuterV3$1LocalPrinter.value

// 캡처 변수
// localVar, paramVar
field : final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
field : final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar

// 바깥 클래스 참조
field : final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0

 

 

 

4. 지역 클래스 - 지역 변수 캡처 3

지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.

 

따라서 final로 선언하거나 사실상 final이어야 한다.

 

사실상 final ? effictively final이라고 한다.


final 지역 변수는 지역 변수에 final을 사용하지 않았지만 값을 변경하지 않는 지역변수를 뜻한다.


final 키워드를 넣지 않았을 뿐이지, 실제로는 final 키워드를 넣은 것처럼 중간에 값을 변경하지 않은 지역 변수이다.


따라서 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 한다.

 

 

 

캡처한 지역 변수의 값을 다음과 같이 변경하면 어떻게 될 것인가?

Printer printer = new LocalPrinter();

// 컴파일 오류
localVar = 10;
paramVar = 20;

 

스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. → 이것을 동기화 문제라고 한다.

 

캡처 변수의 값을 변경하지 못하는 이유

지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다. - 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 지역 변수의 값도 다시 변경해야 한다.

지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화해야 하는데, 멀티 쓰레드 상황에서 이런 동기화는 매우 어렵고 성능에 나쁜 영향을 줄 수 있다.

 

따라서, 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다..!

 

 

5. 익명 클래스 - 시작

익명 클래스는 클래스의 이름이 없다는 특징이 있다.

 

그리고 익명 클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한번에 처리할 수 있게 된다.

→ 지역 클래스의 선언과 생성을 한번에 처리할 수 있다.

 

Printer printer = new Printer() {
	// body
}

 

5.1. 예제

 

 

new Printer() {body} 익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성한다. {body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. 이 부분이 바로 익명 클래스의 본문이 된다. 쉽게 이야기해서 Printer를 상속(구현)하면서 바로 생성하는 것이다.

 

5.1.1. 익명 클래스의 특징

  • 이름 없는 지역 클래스를 선언 + 생성을 동시에 한다.
  • 익명 클래스는 부모 클래스를 상속받거나, 또는 인터페이스를 구현해야 한다. → 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필히 필요
  • 익명 클래스는 말 그대로 이름이 없기 때문에 생성자를 가질 수 없다.

 

5.1.2. 익명 클래스의 장점

클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어서 코드가 더 간결해진다.

즉, 여러 번 생성이 필요하다면 익명 클래스를 사용할 수 없다. 대신에 지역 클래스를 선언하고 사용하면 된다.

 

 

 

6. 익명 클래스 활용 1

6.1. 리팩토링 전

 

→ 코드를 리팩토링하여 코드의 중복을 제거해보자.

 

6.2. 리팩토링 후

 

변하는 부분과 변하지 않는 부분을 분리하는 것으로 리팩토링을 하였다.

여기서 핵심은 변하는 부분을 메서드(함수) 내부에서 가지고 있는 것이 아니라 외부에서 전달 받는다는 점이다.

 

 

7. 익명 클래스 활용 2

7.1. 리팩토링 전

 

 

이번에는 문자열 같은 데이터가 아니라 코드 조각을 전달해야 한다.

7.2. 리팩토링 후

 

 

코드 조각은 보통 메서드(함수)에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다.

따라서 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 된다.

→ 이 문제를 해결하기 위해 인터페이스를 정의하고 구현 클래스를 만들었다.

→ 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다.

 

8. 익명 클래스 활용3

이번에는 지역 클래스를 사용해서 같은 기능을 구현해본다.

 

8.1. 지역 클래스(Local Class) 활용

 

8.2. 익명 클래스 사용 1

지역 클래스는 한번만 생성하여 사용한다. 이런 경우 익명 클래스로 변경할 수 있다.

게다가 interface로 implement하기 때문에 가능

 

 

 

 

9. 람다(lambda)

Java 8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 2가지였다.

  • int, double과 같은 기본형 타입
  • Process Member와 같은 참조형 타입(인스턴스)

 

결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나, 인스턴스의 참조이다.

 

지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 꼭 생성해서 전달해야 할까?

→ 다음과 같이 클래스나 인스턴스와 관계 없이 메서드만 전달할 수 있다면 더 간단할 것이다.

 

public void runDice() {
	int randomValue = new Random().nextInt(6) + 1;
	System.out.println("주사위 = " + randomValue);
}

hello(메서드 전달: runDice());

 

9.1. 람다

 

코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드의 코드 블럭을 직접 전달하는 것을 확인할 수 있다.

 

반응형