본문 바로가기

Java

[Java] 중첩 클래스, 내부 클래스

중첩 클래스, 내부 클래스란?

다음과 같이 for문 안에 for문을 중첩하는 것을 중첩(Nested) for문이라 합니다.

for (...) {
 //중첩 for문
 for (...) {
 }
}

 

마찬가지로 클래스 안에 클래스를 중첩해서 정의할 수 있는데, 이것을 중첩 클래스(Nested Class)라 합니다. 

class Outer {
 ...
 //중첩 클래스
 class Nested {
 ...
 }
}


중첩 클래스는 클래스를 정의하는 위치에 따라 분류됩니다.

 

 

중첩 클래스는 총 4가지가 있고, 크게 2가지로 분류할 수 있습니다.

 

  • 정적 중첩 클래스
  • 내부 클래스 종류
    • 내부 클래스
    • 지역 클래스
    • 익명 클래스

중첩 클래스를 정의하는 위치는 변수의 선언 위치와 같습니다.

 

변수의 선언 위치

  • 정적 변수(클래스 변수)
  • 인스턴스 변수
  • 지역 변수

 

중첩 클래스의 선언 위치

  • 정적 중첩 클래스 => 정적 변수와 같은 위치
  • 내부 클래스 => 인스턴스 변수와 같은 위치
  • 지역 클래스 => 지역 변수와 같은 위치
class Outer {
 ...
 //정적 중첩 클래스
 static class StaticNested {
 ...
 }
 //내부 클래스
 class Inner {
 ...
 }
}
  • 정적 중첩 클래스는 정적 변수와 같이 앞에 static 이 붙어있다.
  • 내부 클래스는 인스턴스 변수와 같이 앞에 static 이 붙어있지 않다.
class Outer {
    public void process() {
    //지역 변수
    int lcoalVar = 0;
    //지역 클래스
    class Local {...}
    Local local = new Local();
    }
}
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 클래스를 정의한다.
  • 참고로 익명 클래스는 지역 클래스의 특별한 버전이다.

 

"그럼 중첩이라는 단어와 내부라는 단어는 무슨 차이가 있는 것일까요?"

  • 중첩(Nested): 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계
  • 내부(Inner): 나의 내부에 있는 나를 구성하는 요소

 

여기서 의미하는 중첩( Nested )과 내부( Inner )를 분류하는 핵심은 바로 

바깥 클래스 입장에서 볼 때 안에 있는 클래스가 나의 인스턴스에 소속이 되는가 되지 않는가의 차이입니다.

  • 정적 중첩 클래스는 바깥 클래스와 전혀 다른 클래스이다. 따라서 바깥 클래스의 인스턴스에 소속되지 않는다.
  • 내부 클래스는 바깥 클래스를 구성하는 요소이다. 따라서 바깥 클래스의 인스턴스에 소속된다.

정리

  • 정적 중첩 클래스는 바깥 클래스의 안에 있지만 바깥 클래스와 관계 없는 전혀 다른 클래스를 말한다.
  • 내부 클래스는 바깥 클래스의 내부에 있으면서 바깥 클래스를 구성하는 요소를 말한다.
  • 내부 클래스들은 바깥 클래스의 인스턴스에 소속된다. 정적 중첩 클래스는 그렇지 않다.

 

정적 중첩 클래스

  • static 이 붙는다.
  • 바깥 클래스의 인스턴스에 소속되지 않는다.

내부 클래스

  • static 이 붙지 않는다.
  • 바깥 클래스의 인스턴스에 소속된다.

내부 클래스의 종류

  • 내부 클래스(inner class): 바깥 클래스의 인스턴스의 멤버에 접근
  • 지역 클래스(local class): 내부 클래스의 특징 + 지역 변수에 접근
  • 익명 클래스(anonymous class): 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스

 

중첩 클래스는 언제 사용해야 하나?

 

내부 클래스를 포함한 모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴
밀하게 연결되어 있는 특별한 경우에만 사용해야 한다. 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중
첩 클래스로 만들면 안된다.

 

 

중첩 클래스를 사용하는 이유

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리
    적으로 더 그룹화 된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는
    장점도 있다.
  • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불
    필요한 public 메서드를 제거할 수 있다. 

 


정적 중첩 클래스

public class NestedOuter {

    private static int outClassValue = 3;
    private int outInstanceValue = 2;

    static class Nested {

        private  int nestedInstanceValue = 1;

        public void print() {

            // 자신의 멤버에 접근
            System.out.println(nestedInstanceValue);

            // 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.
            //System.out.println(outInstanceValue);

            // 바깥 클래스의 클래스 멤버에는 접근할 수 있다. private도 접근 가능
            System.out.println(outClassValue);
        }
    }
}
  • 정적 중첩 클래스는 앞에 static이 붙는다.
  • 정적 중첩 클래스는
    • 자신의 멤버에는 당연히 접근할 수 있다.
    • 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.
    • 바깥 클래스의 클래스 멤버에는 접근할 수 있다.

 

private 접근 제어자

  • private 접근 제어자는 같은 클래스 안에 있을 때만 접근할 수 있다.
  • 중첩 클래스도 바깥 클래스와 같은 클래스 안에 있다. 
  • 따라서 중첩 클래스는 바깥 클래스의 private 접근 제어자에 접근할 수 있다.
public class NestedOuterMain {

    public static void main(String[] args) {

        NestedOuter outer = new NestedOuter();
        NestedOuter.Nested nested = new NestedOuter.Nested();
        nested.print();
        
        System.out.println("nestedClass = " + nested.getClass());
    }
}
  • 정적 중첩 클래스는 new 바깥클래스.중첩클래스() 로 생성할 수 있다.
  • 중첩 클래스는 NestedOuter.Nested 와 같이 바깥 클래스.중첩클래스 로 접근할 수 있다.
  • 여기서 new NestedOuter() 로 만든 바깥 클래스의 인스턴스와 new NestedOuter.Nested() 로 만든 정적 중첩 클래스의 인스턴스는 서로 아무 관계가 없는 인스턴스이다. 
  • 단지 클래스 구조상 중첩해 두었을 뿐이다.
    • 참고로 둘이 아무런 관련이 없으므로 정적 중첩 클래스의 인스턴스만 따로 생성해도 된다.
1
3
nestedClass = class nested.nested.NestedOuter$Nested

 

중첩 클래스를 출력해보면 중첩 클래스의 이름은 NestedOuter$Nested 와 같이 바깥 클래스, $ , 중첩 클래스의 조합으로 만들어진다.

 

정리

  • 정적 중첩 클래스는 사실 다른 클래스를 그냥 중첩해 둔 것일 뿐이다. 쉽게 이야기해서 둘은 아무런 관계가 없다. 
  • NestedOuter.outClassValue 와 같은 정적 필드에 접근하는 것은 중첩 클래스가 아니어도 어차피 클래스명.정적
    필드명 으로 접근할 수 있다.
  • 쉽게 이야기해서 다음과 같이 정적 중첩 클래스를 만들지 않고, 그냥 클래스2개를 따로 만든것과 같다.
class NestedOuter {
}
class Nested {
}
  • 이 코드와 정적 중첩 클래스의 유일한 차이는 같은 클래스에 있으니 private 접근 제어자에 접근할 수 있다는 정도이다.
// Network 객체 안에서만 사용
public class NetworkMessage {

    private String content;

    public NetworkMessage(String content) {
        this.content = content;
    }

    public void print() {
        System.out.print(content);
    }
}
  • NetworkMessage 는 Network 객체 안에서만 사용되는 객체이다.
public class Network {

    public void sendMessage(String text) {
        NetworkMessage networkMessage = new NetworkMessage(text);
        networkMessage.print();
    }
}
  • text 를 입력 받아서 NetworkMessage 를 생성하고 출력하는 단순한 기능을 제공한다.
public class NetworkMain {

    public static void main(String[] args) {
        Network network = new Network();
        network.sendMessage("안녕 쌈뽕 코딩");
    }
}
  • Network 를 생성하고 network.sendMessage() 를 통해 메시지를 전달한다.
  • NetworkMain 은 오직 Network 클래스만 사용한다. NetworkMessage 클래스는 전혀 사용하지 않는다. 
  • NetworkMessage 는 오직 Network 내부에서만 사용된다.

패키지를 열어보면 다음 두 클래스가 보일 것입니다. (main 제외)

  • Network
  • NetworkMessage

Network 관련 라이브러리를 사용하기 위해서 패키지를 열어본 개발자는 아마도 두 클래스를 모두 확인해볼 것 입니다.

그리고 해당 패키지를 처음 확인한 개발자는 Network 와 NetworkMessage 를 둘다 사용해야 하나? 라고 생각할 것 입니다.

NetworkMessage 에 메시지를 담아서 Network 에 전달해야 하나? 와 같은 여러가지 생각을 할 것입니다.

아니면 NetworkMessage 가 다른 여러 클래스에서 사용되겠구나 라고 생각할 수도 있습니다.
두 클래스의 코드를 모두 확인하고 나서야 Network 클래스만 사용하면 되는구나,

NetworkMessage 는 단순히 Network 안에서만 사용되는구나 라고 이해할 수 있습니다.

 

정적 중첩 클래스로 리팩토링 후

public class Network {

    public void sendMessage(String text) {
        NetworkMessage networkMessage = new NetworkMessage(text);
        networkMessage.print();
    }

    private static class NetworkMessage {

        private String content;

        public NetworkMessage(String content) {
            this.content = content;
        }

        public void print() {
            System.out.print(content);
        }
    }
}

 

  • NetworkMessage 클래스를 Network 클래스 안에 중첩해서 만들었다.
  • NetworkMessage 의 접근 제어자를 private 설정했다. 따라서 외부에서 NetworkMessage 에 접근할 수없다.
    • 예) new Network.NetworkMessage() 처럼 접근할 수 없다.
public class NetworkMain {
    
    public static void main(String[] args) {
        Network network = new Network();
        network.sendMessage("안녕 쌈뽕 코딩");
    }
}

 

Network 관련 라이브러리를 사용하기 위해서 패키지를 열어본 개발자는 해당 클래스만 확인할 것 입니다.

추가로 NetworkMessage 가 중첩 클래스에 private 접근 제어자로 되어 있는 것을 보고, Network 내부에서만 단독으로 

사용하는 클래스라고 바로 인지할 수 있습니다

 

중첩 클래스의 접근


나의 클래스에 포함된 중첩 클래스가 아니라 다른 곳에 있는 중첩 클래스에 접근할 때는

바깥클래스.중첩클래스 로 접근해야 합니다. 

NestedOuter.Nested nested = new NestedOuter.Nested();

 

나의 클래스에 포함된 중첩 클래스에 접근할 때는 바깥 클래스 이름을 적지 않아도 됩니다.

public class Network {
    public void sendMessage(String text) {
    NetworkMessage networkMessage = new NetworkMessage(text);
    }
    private static class NetworkMessage {...}
}
  • 중첩 클래스(내부 클래스 포함)는 그 용도가 자신이 소속된 바깥 클래스 안에서 사용되는 것이다. 
  • 따라서 자신이 소속된 바깥 클래스가 아닌 외부에서 생성하고 사용하고 있다면, 이미 중첩 클래스의 용도에 맞지 않을 수 있다. 
  • 이때는 중첩 클래스를 밖으로 빼는 것이 더 나은 선택이다.

 

내부 클래스


정적 중첩 클래스는 바깥 클래스와 서로 관계가 없습니다.

하지만 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 됩니다.

쉽게 이야기해서 내부 클래스는 바깥 클래스의 인스턴스에 소속 됩니다.

 

정적 중첩 클래스

  • static 이 붙는다.
  • 바깥 클래스의 인스턴스에 소속되지 않는다.

내부 클래스

  • static 이 붙지 않는다.
  • 바깥 클래스의 인스턴스에 소속된다.
public class InnerOuter {

    private static int outClassValue = 3;
    private int outInstanceValue = 2;

    class Inner {
        
        private int innerInstanceValue = 1;

        public void print () {
            // 자기 자신에 접근
            System.out.println(innerInstanceValue);

            // 외부 클래스의 인스턴스 멤버에 접근 가능, private도 접근 가능
            System.out.println(outInstanceValue);
            
            // 외부 클래스의 클래스 멤버에 접근 가능, private도 접근 가능
            System.out.println(outClassValue);
        }
    }
}

 

  • 내부 클래스는 앞에 static 이 붙지 않는다. 쉽게 이야기해서 인스턴스 멤버가 된다.
  • 내부 클래스는
    • 자신의 멤버에는 당연히 접근할 수 있다.
    • 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
    • 바깥 클래스의 클래스 멤버에 접근할 수 있다.

private 접근 제어자

  • private 접근 제어자는 같은 클래스 안에 있을 때만 접근할 수 있다.
  • 내부 클래스도 바깥 클래스와 같은 클래스 안에 있다. 
  • 따라서 내부 클래스는 바깥 클래스의 private 접근 제어자에 접근할 수 있다.
public class InnerOuterMain {

    public static void main(String[] args) {

        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner();
        inner.print();

        System.out.println("innerClass " + inner.getClass());
    }
}
1
2
3
innerClass class nested.inner.InnerOuter$Inner

 

  • 내부 클래스는 바깥 클래스의 인스턴스에 소속된다. 따라서 바깥 클래스의 인스턴스 정보를 알아야 생성할 수 있다.
  • 내부 클래스는 new 바깥클래스의 인스턴스 참조.내부클래스( ) 로 생성할 수 있다.
    • 내부 클래스는 바깥 클래스의 인스턴스에 소속되어야 한다. 따라서 내부 클래스를 생성할 때, 바깥 클래스의 인스턴스 참조가 필요하다.
  • outer.new Inner( ) 에서 outer 는 바깥 클래스의 인스턴스 참조를 가진다.
    • outer.new Inner() 로 생성한 내부 클래스는 개념상 바깥 클래스의 인스턴스 내부에 생성된다.
    • 따라서 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스를 생성할 수 있다

 

 

내부 클래스로 리팩토링 전

public class Engine {

    private Car car;

    public Engine(Car car) {
        this.car = car;
    }

    public void start() {
        System.out.println("충전 레벨 확인: " + car.getChargeLevel());
        System.out.println(car.getModel() + "의 엔진을 구동합니다.");
    }
}
  • 엔진은 Car 클래스에서만 사용된다.
  • 엔진을 시작하기 위해서는 차의 충전 레벨과 차량의 이름이 필요하다.
  • Car 인스턴스의 참조를 생성자에서 보관한다.
  • 엔진은 충전 레벨을 확인하기 위해 Car.getChargeLevel() 이 필요하다.
  • 엔진은 차량의 이름을 확인하기 위해 Car.getModel() 이 필요하다.
public class Car {

    private String model;
    private int chargeLevel;
    private Engine engine;

    public Car(String model, int chargeLevel) {
        this.model = model;
        this.chargeLevel = chargeLevel;
        this.engine = new Engine(this);
    }

    // Engine 에서만 사용하는 메서드
    public String getModel() {
        return model;
    }

    // Engine 에서만 사용하는 메서드
    public int getChargeLevel() {
        return chargeLevel;
    }

    public void start() {
        engine.start();
        System.out.println(model + " 시작 완료");
    }
}
  • Car 클래스는 엔진에 필요한 메서드들을 제공해야 한다. 다음 메서드는 엔진에서만 사용하고, 다른 곳에서는 사용하지 않는다.
    • getModel()
    • getChargeLevel()
  • 결과적으로 Car 클래스는 엔진에서만 사용하는 기능을 위해 메서드를 추가해서, 모델 이름과 충전 레벨을 외부에 노출해야 한다
public class CarMain {

    public static void main(String[] args) {
        Car myCar = new Car("Model Y", 100);
        myCar.start();
    }
}
충전 레벨 확인: 100
Model Y의 엔진을 구동합니다.
Model Y 시작 완료

 

 

내부 클래스로 리팩토링 후

public class Car {

    private String model;
    private int chargeLevel;
    private Engine engine;

    public Car(String model, int chargeLevel) {
        this.model = model;
        this.chargeLevel = chargeLevel;
        this.engine = new Engine();
    }

    public void start() {
        engine.start();
        System.out.println(model + " 시작 완료");
    }

    private class Engine {

        public void start() {
            System.out.println("충전 레벨 확인: " + chargeLevel);
            System.out.println(model + "의 엔진을 구동합니다.");
        }
    }
}
public class CarMain {

    public static void main(String[] args) {
        Car myCar = new Car("Model Y", 100);
        myCar.start();
    }
}
  • 엔진을 내부 클래스로 만들었다.
  • Engine.start() 를 기존과 비교해보자.
    • Car 의 인스턴스 변수인 chargeLevel 에 직접 접근할 수 있다.
    • Car 의 인스턴스 변수인 model 에 직접 접근할 수 있다.

내부 클래스의 생성

  • 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때는 바깥 클래스 이름을 생략할 수 있다.
    • 예) new Engine() 
  • 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 인스턴스를 자동으로 참조한다. 여기서 new Engine() 로 생성된 Engine 인스턴스는 자신을 생성한 바깥의 Car 인스턴스를 자동으로 참조한다.

리팩토링 전의 문제

  • Car 클래스는 엔진에 필요한 메서들을 제공해야 합니다. 다음 메서드는 엔진에서만 사용하고, 다른 곳에서는 사용하지 않습니다.
    • getModel()
    • getChargeLevel()
  • 결과적으로 엔진에서만 사용하는 기능을 위해 메서드를 추가해서, 모델 이름과 충전 레벨을 외부에 노출해야 합니다.


리팩토링 전에는 결과적으로 모델 이름과 충전 레벨을 외부에 노출했습니다. 

이것은 불필요한 Car 클래스의 정보들이 추가로 외부에 노출되는 것이기 때문에 캡슐화를 떨어뜨린다.
리팩토링 후에는 getModel() , getChargeLevel() 과 같은 메서드를 모두 제거했습니다. 

결과적으로 꼭 필요한 메서드만 외부에 노출함으로써 Car 의 캡슐화를 더 높일 수 있었습니다.

 


중첩 클래스는 언제 사용해야 하나?


중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별
한 경우에만 사용해야 한다. 외부 여러곳에서 특정 클래스를 사용한다면 중첩 클래스로 사용하면 안된다.

 


중첩 클래스를 사용하는 이유

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화가 된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다.
  • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다.

 

 

같은 이름의 바깥 변수 접근

바깥 클래스의 인스턴스 변수 이름과 내부 클래스의 인스턴스 변수 이름이 같으면 어떻게 될까요?

public class ShadowingMain {

    public int value = 1;

    class Inner {

        public int value = 2;

        void go() {
            int value = 3;
            System.out.println("value = " + value);
            System.out.println("this.value = " + this.value);
            System.out.println("ShadowingMain.this.value = " + ShadowingMain.this.value);
        }
    }

    public static void main(String[] args) {

        ShadowingMain main = new ShadowingMain();
        Inner inner = main.new Inner();
        inner.go();
    }
}
value = 3
this.value = 2
ShadowingMain.this.value = 1

 

변수의 이름이 같기 때문에 어떤 변수를 먼저 사용할지 우선순위가 필요하다.
프로그래밍에서 우선순위는 대부분 더 가깝꺼나, 더 구체적인 것이 우선권을 가진다. 쉽게 이야기해서 사람이 

직관적으로 이해하기 쉬운 방향으로 우선순위를 설계한다.


메서드 go() 의 경우 지역 변수인 value 가 가장 가깝다. 따라서 우선순위가 가장 높다.
이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(Shadowing)이라 한다.
다른 변수를 가리더라도 인스턴스의 참조를 사용하면 외부 변수에 접근할 수 있다.


this.value 는 내부 클래스의 인스턴스에 접근하고, 바깥클래스이름.this 는 바깥 클래스의 인스턴스에 접근할 수있다.
프로그래밍에서 가장 중요한 것은 명확성이다. 이렇게 이름이 같은 경우 처음부터 이름을 서로 다르게 지어서 명확하게
구분하는 것이 더 나은 방법이다

 

 

지역 클래스

  • 지역 클래스(Local class)는 내부 클래스의 특별한 종류의 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다. 예를 들어서 지역 클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.
class Outer {
    public void process() {
        //지역 변수
        int localVar = 0;
        //지역 클래스
        class Local {...}
        
        Local local = new Local();
    }
}

 

지역 클래스의 특징

  • 지역 클래스는 지역 변수처럼 코드 블럭 안에 클래스를 선언한다.
  • 지역 클래스는 지역 변수에 접근할 수 있다.
public class LocalOuterV1 {

    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;

        class LocalPrinter {
            int value = 0;

            public void printDate() {
                System.out.println("value = " + value);
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceVar = " + outInstanceVar);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        printer.printDate();
    }

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3

 

지역 클래스의 접근 범위

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

지역 클래스는 지역 변수 처럼 접근 제어자를 사용할 수 없다.

 

 

내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속할 수 있습니다

public interface Printer {
    void print();
}
public class LocalOuterV2 {

    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;

        class LocalPrinter implements Printer{
            int value = 0;

            @Override
            public void print() {
                System.out.println("value = " + value);
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceVar = " + outInstanceVar);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        printer.print();
    }

    public static void main(String[] args) {
        LocalOuterV2 localOuter = new LocalOuterV2();
        localOuter.process(2);
    }
}

 

 

 

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

public class LocalOuterV3 {

    private int outInstanceVar = 3;

    public Printer process(int paramVar) {

        int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.

        class LocalPrinter implements Printer{
            int value = 0;

            @Override
            public void print() {
                System.out.println("value = " + value);
                // 인스턴스는 지역 변수보다 더 오래 살아남는다.
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceVar = " + outInstanceVar);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        //printer.print();를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
        return printer;
    }

    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);
        //printer.print();를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
        printer.print();
    }
}
  • process() 는 Printer 타입을 반환한다. 여기서는 LocalPrinter 인스턴스를 반환한다.
  • 여기서는 LocalPrinter.print() 메서드를 process() 안에서 실행하는 것이 아니라 process() 메서드가 종료된 이후에 main() 메서드에서 실행한다.
value=0
localVar=1
paramVar=2
outInstanceVar=3

 

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

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

참고 - 어떻게 제거된 지역 변수들에 접근할 수 있는 것일까?
여기서는 이해를 돕기 위해 설명을 단순화 했지만, 더 정확히 이야기 하면 LocalPrinter.print() 메서드를 실행하면 이 메서드도 당연히 스택 프레임에 올라가서 실행된다. main() 에서 print() 를 실행했으므로 main() 스택 프레임 위에 print() 스택 프레임이 올라간다. 물론 process() 스택 프레임은 이미 제거된 상태이므로 지역 변수인 localVar , paramVar 도 함께 제거되어서 접근할 수 없다

 

 

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

지역 클래스는 지역 변수에 접근할 수 있다.
그런데 앞서 본 것 처럼 지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다.
지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는

살아있지만, 지역 변수는 이미 제거된 상태일 수 있다.

 

지역 변수 캡처
자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인
스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture)라 한다.
캡처라는 단어는 스크린 캡처를 떠올려 보면 바로 이해가 될 것이다. 인스턴스를 생성할 때 필요한 지역 변수를 복사해
서 보관해 두는 것이다. 물론 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처한다

 

1. LocalPrinter 인스턴스 생성 시도: 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다.
LocalPrinter 클래스는 paramVar , localVar 지역 변수에 접근한다.
2. 사용하는 지역 변수 복사: 지역 클래스가 사용하는 지역 변수를 복사한다. (매개변수도 지역 변수의 한 종류이다)
여기서는 paramVar , localVar 지역 변수를 복사한다.

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

 

  • LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar , localVar 에 접근하면 사실은 스택영역에 있는 지역 변수에 접근하는 것이 아니다. 대신에 인스턴스에 있는 캡처한 변수에 접근한다.
  • 캡처한 paramVar , localVar 의 생명주기는 LocalPrinter 인스턴스의 생명주기와 같다. 따라서 LocalPrinter 인스턴스는 지역 변수의 생명주기와 무관하게 언제든지 paramVar , localVar 캡처 변수에 접근할 수 있다.
  • 이렇게 해서 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명주기가 다른 문제를 해결한다.

 

코드로 캡처 변수 확인

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

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

// 캡처 번수
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

 

실행 결과를 통해 LocalPrinter 클래스의 캡처 변수를 확인할 수 있다. 추가로 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다. 참고로 이런 필드들은 자바가 내부에서 만들어 사용하는 필드들 입니다.

 

정리
지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관한다. 그리고 지역 클래스의 인
스턴스를 통해 지역 변수에 접근하면, 실제로는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처 변수에
접근한다.

 

 

 

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

지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안됩니다.
따라서 final 로 선언하거나 또는 사실상 final 이어야 한다. 이것은 자바 문법이고 규칙입니다.

 

사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻합니다.

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

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


지역 클래스가 접근하는 지역 변수는 왜 final 또는 사실상 final 이어야 할까? 왜 중간에 값이 변하면 안될까?

import java.lang.reflect.Field;

public class LocalOuterV4 {

    private int outInstanceVar = 3;

    public Printer process(int paramVar) { // 사실상 파이널

        int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.

        class LocalPrinter implements Printer{
            int value = 0;

            @Override
            public void print() {
                System.out.println("value = " + value);
                // 인스턴스는 지역 변수보다 더 오래 살아남는다.
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceVar = " + outInstanceVar);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        // 만약 localVar의 값을 변경한다면? 다시 캡쳐를 해야하나?
        // localVar = 10;
        // paramVar = 20;
        return printer;
    }

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

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

 

Printer printer = new LocalPrinter();
  • LocalPrinter 를 생성하는 시점에 지역 변수인 localVar , paramVar 를 캡처한다.

그런데 이후에 캡처한 지역 변수의 값을 다음과 같이 변경하면 어떻게 될까요?

Printer printer = new LocalPrinter()
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나??
localVar = 10; // 컴파일 오류
paramVar = 20; // 컴파일 오류

 

이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라 한다.물론 자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함께 변경하도록 설계하면 된다. 하지만 이로 인해 수 많은 문제들이 파생될 수 있다.

 

 

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

  • 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다.
  • 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다.
  • 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다. 이는 디버깅을 어렵게 한다.
  • 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다. 이 부분은 멀티쓰레드를 학습하면 이해할 수 있다.

이 모든 문제는 캡처한 지역 변수의 값이 변하기 때문에 발생합니다. 

자바는 캡처한 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제들을 근본적으로 차단하고 있습니다.

 

 

익명 클래스

 

익명 클래스(anonymous class)는 지역 클래스의 특별한 종류의 하나 입니다.
익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있습니다.

 

지역 클래스 예제 코드인 LocalOuterV2 코드를 다시 한번 살펴봅시다.

 

여기서는 지역 클래스를 사용하기 위해 선언과 생성이라는 2가지 단계를 거친다.

1. 선언: 지역 클래스를 LocalPrinter 라는 이름으로 선언한다. 이때 Printer 인터페이스도 함께 구현한다.
2. 생성: new LocalPrinter() 를 사용해서 앞서 선언한 지역 클래스의 인스턴스를 생성한다.

//선언
class LocalPrinter implements Printer{
 //body
}
//생성
Printer printer = new LocalPrinter();
  • 익명 클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한번에 처리할 수 있다.
Printer printer = new Printer(){
 //body
}

 

import nested.local.Printer;

public class AnonymousOuter {

    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;

        Printer printer = new Printer() {
            int value = 0;

            @Override
            public void print() {
                System.out.println("value = " + value);
                System.out.println("localVar = " + localVar);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outInstanceVar = " + outInstanceVar);
            }
        };

        printer.print();
        System.out.println("printer.class = " + printer.getClass());
    }

    public static void main(String[] args) {
        AnonymousOuter main = new AnonymousOuter();
        main.process(2);
    }
}
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
printer.class = class nested.anonymous.AnonymousOuter$1

 

 

new Printer() {
// body
}


익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성한다. new 다음에 바로 상속 받으면서 구현 할 부모 타입을 입력하면 된다. 이 코드는 마치 인터페이스 Printer 를 생성하는 것 처럼 보인다. 하지만 자바에서 인터페이스를 생성하는 것을 불가능하다. 이 코드는 인터페이스를 생성하는 것이 아니고, Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다. {body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. 이 부분이 바로 익명 클래스의 본문이 된다. 쉽게 이야기해서 Printer 를 상속(구현) 하면서 바로 생성하는 것이다.

 

 

익명 클래스 특징

  • 익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
  • 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다.
  • 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다.
  • 익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다. (기본 생성자만 사용됨)
  • 익명 클래스는 AnonymousOuter$1 과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의된다. 익명 클래스가 여러개면 $1 , $2 , $3 으로 숫자가 증가하면서 구분된다.

 

익명 클래스의 장점

익명 클래스를 사용하면 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 더 간결해진다. 하지만, 복잡하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋다.

 

익명 클래스를 사용할 수 없을 때
익명 클래스는 단 한 번만 인스턴스를 생생할 수 있다. 다음과 같이 여러 번 생성이 필요하다면 익명 클래스를 사용할 수
없다. 대신에 지역 클래스를 선언하고 사용하면 된다.

Printer printer1 = new LocalPrinter();
printer1.print();

Printer printer2 = new LocalPrinter();
printer2.print();

 

정리

  • 익명 클래스는 이름이 없는 지역 클래스이다.
  • 특정 부모 클래스(인터페이스)를 상속 받고 바로 생성하는 경우 사용한다.
  • 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.

 

익명 클래스 활용

 

리팩토링 전

import java.util.Random;

public class Ex1Main {
    
    public static void helloDice() {
        System.out.println("프로그램 시작");
        
        // 코드 조각 시작
        int randomValue = new Random().nextInt(6) + 1;
        System.out.println("주사위 = " + randomValue);
        // 코드 조각 종료

        System.out.println("프로그램 종료");
    }
    public static void helloSum() {
        System.out.println("프로그램 시작");

        // 코드 조각 시작
        for (int i = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        // 코드 조각 종료

        System.out.println("프로그램 종료");
    }

    public static void main(String[] args) {
        helloDice();
        helloSum();
    }
}
프로그램 시작
주사위 = 5
프로그램 종료
프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료

 

이 코드를 하나의 메서드에서 실행할 수 있도록 리팩토링 해봅시다.

 

 

리팩토링 후

public interface Process {
    void run();
}
import java.util.Random;

public class Ex1RefMain {

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }
    static class Dice implements Process {
        @Override
        public void run() {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        }
    }
    static class Sum implements Process {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        }
    }

   public static void main(String[] args) {
        Dice dice = new Dice();
        Sum sum = new Sum();
        
        hello(dice);
        hello(sum);
        
        //hello(new Dice());
        //hello(new Sum());
    }
}
  • Process process 매개변수를 통해 인스턴스를 전달할 수 있다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다.
  • 이때 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행된다.
  • hello() 를 호출할 때 어떤 인스턴스를 전달하는 가에 따라서 다른 결과가 실행된다.
  • hello(dice) 를 호출하면 주사위 로직이, hello(sum) 을 호출하면 계산 로직이 수행된다.

 

어떻게 외부에서 코드 조각을 전달할 수 있을까?


코드 조각은 보통 메서드(함수)에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다.
그런데 지금까지 학습한 내용으로는 메서드를 전달할 수 있는 방법이 없다. 대신에 인스턴스를 전달하고, 인스턴스에 있
는 메서드를 호출하면 된다.

 

 

지역 클래스 사용

public class Ex1RefMainV2 {

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }


    public static void main(String[] args) {
        class Dice implements Process {

            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("주사위 = " + randomValue);
            }
        }
        class Sum implements Process {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        }

        Dice dice = new Dice();
        Sum sum = new Sum();

        hello(dice);
        hello(sum);

        //hello(new Dice());
        //hello(new Sum());
    }
}

 

 

 

익명 클래스 사용1

public class Ex1RefMainV3 {

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }


    public static void main(String[] args) {

        Process dice = new Process() {

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

        Process sum = new Process() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        };
        hello(dice);
        hello(sum);
    }
}

 

 

익명 클래스 사용2 - 참조값 직접 전달

public class Ex1RefMainV4 {

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }


    public static void main(String[] args) {

        hello(new Process() {

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

        hello(new Process() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }
}

 

 

람다(lamba)

 

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

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

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


지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 꼭 생성해서 전달해야 할까?
생각해보면 클래스나 인스턴스와 관계 없이 다음과 같이 메서드만 전달할 수 있다면 더 간단하지 않을까?

public void runDice() {
    int randomValue = new Random().nextInt(6) + 1;
    System.out.println("주사위 = " + randomValue);
}
public void runSum() {
    for (int i = 1; i <= 3; i++) {
    System.out.println("i = " + i);
    }
}
hello(메서드 전달: runDice())
hello(메서드 전달: runRun())

 

자바8에 들어서면서 큰 변화가 있었는데 바로 메서드(더 정확히는 함수)를 인수로 전달할 수 있게 되었다. 이것을 간단히 람다(Lambda)라 한다.

 

 

리팩토링 - 람다

public class Ex1RefMainV5 {

    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        process.run();
        System.out.println("프로그램 종료");
    }


    public static void main(String[] args) {

        hello(() -> {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        });

        hello(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}
  • 코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드(더 정확히는 함수)의 코드 블럭을 직접 전달하는 것을 확인할 수 있다.