8. 중첩 클래스, 내부 클래스2
지역 클래스 - 시작
지역 클래스(Local Class) 는 내부 클래스의 특별한 종류 중 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다.
예를 들어, 지역 클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.
지역 클래스의 특징
지역 클래스는 지역 변수처럼 코드 블럭 안에 클래스를 선언한다.
지역 클래스는 지역 변수에 접근할 수 있다.
지역 클래스 예제1
지역 클래스의 접근 범위
자신의 인스턴스 변수인
value
에 접근할 수 있다.자신이 속한 코드 블럭의 지역 변수인
localVar
에 접근할 수 있다.자신이 속한 코드 블럭의 매개변수인
paramVar
에 접근할 수 있다.바깥 클래스의 인스턴스 멤버인
outInstanceVar
에 접근할 수 있다.
지역 클래스 예제2
당연한 이야기지만 내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속할 수 있다.
지역 클래스 - 지역 변수 캡처1
지역 클래스를 더 자세히 알아보기 전에 잠시 변수들의 생명 주기에 대해서 정리해보자.
클래스 변수 : 프로그램 종료 까지, 가장 길다(메서드 영역)
클래스 변수(
static
변수) 는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재한다.
인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)
인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기 전까지 존재한다. 생명주기가 긴 편이다.
지역 변수 : 메서드 호출이 끝나면 사라짐(스택 영역)
지역 변수는 스택 영역의 스택 프레임 안에 존재한다. 따라서 메서드가 호출 되면 생성되고, 메서드 호출이 종료 되면 스택 프레임이 제거되면서 그 안에 있는 지역 변수도 모두 제거된다. 생존 주기가 아주 짧다.
지역 클래스 예제3
지역 클래스 인스턴스를 외부로 리턴해서 사용할 수 있다.
LocalPrinter 인스턴스 생성 직후 메모리 그림
지역 클래스 인스턴스의 생존 범위
지역 클래스로 만든 객체도 인스턴스이기 때문에 힙 영역에 존재한다. 따라서 GC 전까지 생존한다.
LocalPrinter
인스턴스는process()
메서드 안에서 생성된다.그리고
process()
에서main()
으로 생성한LocalPrinter
인스턴스를 반환하고printer
변수에 참조를 보관한다.,
LocalPrinter
인스턴스는main()
이 종료될 때까지 생존한다.
paramVar
,localVar
와 같은 지역 변수는process()
메서드를 실행하는 동안에만 스택 영역에서 생존한다.process()
메서드가 종료되면process()
스택 프레임이 스택 영역에서 제거 되면서 함께 제거된다.
process() 메서드의 종료
지역 변수의 생명주기는 매우 짧다. 반면에 인스턴스의 생명주기는 GC 전까지 생존할 수 있다.
지역 변수인
paramVar
,LocalVar
는process()
메서드가 실행되는 동안만 생존할 수 있다.process()
메서드가 종료되면process()
메서드의 스택프레임이 제거되면서 두 지역 변수도 함께 제거.
여기서 문제는
process()
메서드가 종료되어도LocalPrinter
인스턴스는 계속 생존할 수 있다는 점이다.
process() 메서드가 종료된 이후에 지역 변수 접근
예제를 보자. 여기서는
process()
메서드가 종료된 이후에main()
메서드 안에서LocalPrinter.print()
메서드를 호출한다.LocalPrinter
인스턴스에 있는print()
메서드는 지역 변수인paramVar
,localVar
에 접근해야 한다. 하지만process()
메서드가 이미 종료되었으므로 해당 지역 변수들도 이미 제거된 상태이다.그런데 실행 결과를 보면
paramVar
,localVar
와 같은 지역 변수의 값들이 모두 정상적으로 출력되는 것을 확인할 수 있다..어떻게 제거된 지역 변수들에 접근할 수 있을까?
지역 클래스 - 지역 변수 캡처2
지역 변수 캡처
자바는 이런 문제를 해결하기 위해서 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture) 라고 한다.
캡처라는 단어는 스크린 캡처를 떠올려 보면 바로 이해가 될 것이다. 인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 두는 것이다.
물론 모든 지역 변수를 캡처하는 것이 아니라, 접근이 필요한 지역 변수만 캡처한다.
지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정1
LocalPrinter
인스턴스 생성 시도 : 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다.LocalPrinter
클래스는paramVar
,localVar
지역 변수에 접근한다.
사용하는 지역 변수 복사 : 지역 클래스가 사용하는 지역 변수를 복사한다.(매개변수도 지역변수의 한 종류이다)
여기서는
paramVar
,localVar
지역 변수를 복사한다.
지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정2
지역 변수 복사 완료 : 복사한 지역 변수를 인스턴스에 포함한다.
인스턴스 생성 완료 : 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다. 이제 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.
캡처 변수 접근
때문에, 외부에서 지역변수에 접근하면 실제 지역변수에 접근하는 것이 아니라, 인스턴스 내 캡처한 지역변수에 접근한다.
이로 인해 인스턴스 내 지역변수는 인스턴스와 생명주기가 같다. 따라서 위에서 언급한 생명주기가 다른 문제점을 해결한다.
코드로 캡처 변수 확인
다음 코드를 통해 캡처 변수를 직접 확인해보자.
LocalOuterV3 - 추가
실행 결과를 통해 LocalPrinter
클래스의 캡처 변수를 확인할 수 있다. 추가로 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다. 참고로 이런 필드들은 자바가 내부에서 만들어 사용하는 필드들이다.
지역 클래스 - 지역 변수 캡처3
지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다. 따라서 final
로 선언하거나 또는 사실상 final
로 보아야 한다. 이것은 문법이기에 지켜야한다.
용어 - 사실상 final영어로 effectively final이라 한다. 사실상
final
지역 변수는 지역 변수에final
키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻한다.final
키워드를 넣지 않았을 뿐이지, 실제로는final
키워드를 넣은 것 처럼 중간에 값을 변경하지 않은 지역 변수이다. 따라서 사실상final
지역 변수는final
키워드를 넣어도 동일하게 작동해야 한다.
지역 클래스가 접근하는 지역 변수는 왜 final
또는 사실상 final
이어야 할까? 왜 중간에 값이 변하면 안될까?
코드를 통해서 확인하보자.
Printer printer = new LocalPrinter();
LocalPrinter
를 생성하는 시점에 지역 변수인 localVar
, paramVar
를 캡처한다.
그런데 이후에 캡처한 지역 변수의 값을 다음과 같이 변경하면 어떻게 될까?
이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라고 한다. 때문에, 사실상 final
변수는 위와 같이 변경할 경우 컴파일 오류가 발생한다.
캡처 변수의 값을 변경하지 못하는 이유
지역 변수의 값을 변경하면 인스턴스의 캡처한 변수의 값도 변경해야 한다.
반대로 인스턴스에 있는 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다.
개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다. 이는 디버깅을 어렵게 한다.
지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화해야 하는데, 멀티쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다.
이 모든 문제는 캡처한 지역 변수의 값을 변경하기 때문에 발생한다. 자바는 캡처한 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제들을 근본적으로 차단한다.
익명 클래스 - 시작
익명 클래스(Anonymous Class) 는 지역 클래스의 특별한 종류의 하나이다.
익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.
익명 클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한번에 처리할 수 있다.
new Printer() {...}
이 코드는 마치 인터페이스 Printer
를 생성하는 것 처럼 보인다. 하지만 자바에서 인터페이스를 생성하는 것은 불가능하다. 이 코드는 인터페이스를 생성하는 것은 아니고, Printer
라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다.
쉽게 이야기해서 Printer
를 상속(구현) 하면서 바로 생성하는 것이다.
익명 클래스 특징
익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 상속받아야 한다.
익명 클래스를 사용할 때는, 상위 클래스나 인터페이스가 필요하다.
익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다.(기본 생성자만 사용됨)
익명 클래스는
AnonymousOuter$1
과 같이 자바 내부에서 바깥 클래스 이름 +$
+ 숫자로 정의된다. 익명 클 래스가 여러개면$1
,$2
,$3
으로 숫자가 증가하면서 구분된다.
익명 클래스의 장점
익명 클래스를 사용하면 클래스를 별도로 정의하지 않고 인터페이스, 추상 클래스를 즉석에서 구현할 수 있어, 코드가 더 간결해진다. 하지만 복잡하거나 재사용이 필요한 경우에는 별도의 클래스를 정의해주는 것이 좋다.
익명 클래스는 외부에서 코드 조각을 전달 받을 수 있다.
이전에는 외부에서는 매개변수를 통해서 간단한 데이터나, 인스턴스의 참조만을 전달 받을 수 있었다.
익명 클래스를 사용할 수 없을 때
익명 클래스는 단 한번만 인스턴스를 생성할 수 있다. 여러번 생성이 필요하다면 익명 클래스를 사용할 수 없다. 대신에 지역 클래스를 선언하고 사용하면 된다.
Last updated