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

고동현·2024년 7월 19일
0

JAVA

목록 보기
18/22

중첩 클래스: 클래스 안에 클래스를 중첩해서 정의한것

중첩클래스는 4가지가 있다.

  • 정적 중첩 클래스
  • 내부 클래스 종류
    내부 클래스
    지역 클래스
    익명 클래스
  • 정적 중첩 클래스
    static이 붙는다.
    바깥 클래스의 인스턴스에 소속되지 않는다.
  • 내부 클래스
    static이 붙지 않는다.
    바깥 클래스의 인스턴스에 소속된다.

중첩 클래스는 언제 사용해야하나?
특정 클래스가 다른 하나의 클래스 안에서만 사용되거나,
둘이 아주 긴밀하게 연결되어 있는 경우에만 사용한다.

사용하는 이유

  • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스를 포함하는 것이 논리적으로 더 그룹화 된다.
  • 캡슐화: 중첩클래스는 바깥 클래스의 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);

            //바깥 클래스의 static에는 접근 가능,private여도 가능
            System.out.println(outClassValue);
        }
    }
}

정적 중첩 클래스는 static이 붙는다.

  • 자신의 멤버에는 당연히 접근 가능
  • 바깥 클래스의 인스턴스 변수에는 접근 불가 -> static이기 때문에 애초에 인스턴스가 다른영역에 생성됨.
  • 바깥 클래스의 static멤버는 어차피 static이라 접근이 가능하다. 이런식으로 NestedOuter.outClassValue, 그러나 private이면 외부에서는 접근이 불가능하지만, 이건 클래스 내부 중첩이라 접근이 가능하다.
public class NestedOuterMain {
    public static void main(String[] args) {
        NestedOuter nestedOuter = new NestedOuter();//안만들어도됌. 왜냐면 중첩클래스는 static이니까
        NestedOuter.Nested nested = new NestedOuter.Nested();
        nested.print();
    }
}

Nested 객체를 만들기 위해서 static이므로, 굳이 NestedOuter 객체를 만들필요가 없다.

정적 중첩 클래스는 new 바깥클래스.중첩클래스()로 생성할 수 있다.

그림을 통해서 보자.

인스턴스가 생성될때 중첩이라고 내부에서 생성되는게 아니라, 애초에 다른 인스턴스로 각각 생성된다.
당연히 다른 클래스인데, Nested 객체에서 NestedOuter 객체의 필드에 접근 할 수 없다.(바깥 인스턴스 참조가 없다.)

정리하자면, 정적 중첩클래스는 바깥 클래스와 아무런 관계가 없다. 그냥 클래스를 2개 따로 만든 것 뿐이다.
다만, 같은 클래스에 있으니, private static 에 접근 가능하다는 것이다.

중첩클래스 활용

Network 객체 안에서만 NetworkMessage를 사용한다고 치자.

만약 Network와 NetworkMessage를 분리하여 각각 class를 만든다고 가정하면,

NetworkMessage가 다른데서 사용하는건 아닌지?
NetworkMessage에 메시지를 담아서 Network에 전달해야하는지?
여러가지 생각이 들것이다.

그러나 중첩클래스를 사용하고 내부 클래스를 private으로 박아버리면, 아 내부클래스가 오직 외부클래스 안에서만 사용된다고 명확하게 이해할 수 있다.

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

    private class NetworkMessage {
        private String content;

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

        public void print(){
            System.out.println(content);
        }
    }
}
public class NetworkMain {
    public static void main(String[] args) {
        Network network = new Network();
        network.sendMessage("hello");
    }
}

NetworkMessage 클래스의 생성자와 필드가 private으로 되어있으므로 외부에서 생성과 접근이 불가능하다.

외부에서 NetworkMessage net = new Network.NetworkMessage("hello");
불가능하다.

참고: 외부에서 중첩클래스에 접근할때는 바깥클래스.중첩클래스로 접근

내부클래스

정적 중첩 클래스는 바깥 클래스와 아무런 관련이 없다.
그러나, 내부클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다.
즉 내부 클래스는 바깥 클래스의 인스턴스에 소속된다.


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);
        }
    }
}
public class InnerOuterMain {
    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner();
        inner.print();
    }
}

내부클래스에는 정적 중첩 클래스와 달리 static이 붙지 않은 것을 볼 수 있다.
내부클래스는 바깥 클래스에 소속되므로, 바깥클래스의 인스턴스 정보를 알아야 생성할 수 있다.

내부 클래스는 바깥클래스의 인스턴스 참조.new 내부클래스()로 생성가능
-> x001.new Inner();
중첩클래스는 아에 상관이 없으므로, 외부 클래스를 만들지 않고, new 외부클래스.내부클래스() 할 수 있었는데
내부클래스는 외부클래스에 소속되니까 당연히 외부클래스를 반드시 만들어야한다.


내부 인스턴스가 바깥 인스턴스의 참조를 보관한다. 해당 참조를 통해 바깥 인스턴스의 멤버에 접근할 수 있다.

내부클래스 활용
엔진이 Car안에서만 사용된다고 가정하자.

만약 엔진과 Car를 따로 만든다고 가정하면,
엔진에서 Car의 필드를 가져오기 위해서는

  1. Car에서 getChargeLevel, getModel과 같은 메서드를 만들고
  2. engine.getChargeLevel(), engine.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();
    }
    
    public void start(){
        engine.start();
    }
    
    private class Engine{
        public void start(){
            System.out.println("충전레벨 확인: "+chargeLevel);
            System.out.println(model+"의 엔진을 구동합니다.");
        }
    }
}

public class CarMain {
    public static void main(String[] args) {
        Car car = new Car("제네시스",100);
        car.start();
    }
}

Car클래스 내부에 Engine클래스를 만들었다.
내부클래스는 외부클래스의 참조값을 가지고 있으므로 외부클래스 필드에 접근이 가능하다.
고로, Car에서 getChargeLevel,getModel메서드를 만들지 않아도 된다.'

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

맨처음에 설명했던 중첩클래스를 사용하는 이유에서 캡슐화에 쓰인다.
고로, 내부클래스로 만들면, 불필요한 public get메서드를 만들지 않아도 되서 캡슐화가 잘된다.(내부클래스는 외부 클래스 필드 바로 참조 가능) 라고 이해하면 된다.

같은 이름의 바깥 변수

public class A{
	public int value = 1;
    
    class B{
    	public int value =2;
        void tmp(){
        	int value =3;
            
            //value =>3
            //this.value =>2
            //A.this.value=>1
        }
    }
}

우선순위는 더 가깝거나, 더 구체적인것이 우선권을 가진다.
this.value를 통해서 내부클래스의 인스턴스 접근
바깥클래스이름.this는 바깥클래스의 인스턴스에 접근가능하다.(내부클래스는 바깥클래스의 참조를 가지고 있기 때문)

지역 클래스

지역클래스는 내부 클래스중 하나이다.
고로, 내부 클래스의 특징을 그대로 가진다.-> 바깥 클래스의 참조값을 가지므로 바깥 클래스의 인스턴스 멤버에 접근 가능하다.

지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.

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

지역 클래스 Local은

  • 자신 인스턴스 변수 value 접근 가능
  • 코드 블럭의 지역변수 localVar 접근가능
  • 바깥 클래스의 인스턴스 멤버 c 접근가능

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

당연히 지역클래스도 클래스이므로, 일반 클래스처럼 인터페이스를 구현하고, 부모 클래스를 상속 받을 수 있다.

지역클래스 - 지역 변수 캡쳐

변수의 생명주기

  • 클래스(static변수): 메서드 영역에 존재, 자바가 클래스 정보를 읽어들이는 순간부터 프로그램 종료까지 존재
  • 인스턴스 변수: 인스턴스 변수는 본인이 소속된 인스턴스가 GC되기 전까지 존재(힙영역)
  • 지역변수: 메서드 호출이 끝나면 사라짐(스택영역),매개변수도 지역변수임
public interface Printer {
    void print();
}
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);
            }
        }
        Printer 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();
    }
}

우선 LocalPrinter 내부클래스도 일반 클래스처럼 Printer인터페이스를 구현하고 있다.

우선 process메서드 호출후 LocalPrinter인스턴스 생성 직후 메모리 그림을보자.

지역 클래스로 만든 객체도 인스턴스 이므로 힙영역에 존재하게 된다. 따라서 GC전까지 생존한다.

return printer를 하게 되면,
process 프레임은 제거가 되지만,
main메서드에서 반환된 printer를 참조하므로 GC대상이 아니므로 main메서드가 끝날때까지 생존한다.

print메서드 실행

바깥 인스턴스 변수인 outInstanceVar은 바깥 인스턴스의 참조값을 알고 있으므로 가능하다.
그러나, process메서드가 종료후 frame이 날라갔는데, print()메서드는 현재 지역변수인 paramVar,localVar에 접근해야하는 상황이다.

그런데 실행결과를 보면 localVar와 paramVar같은 지역변수도 모두 정상적으로 출력된다 왜그런것일까?

자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역변수를 복사해서 생성해서 인스턴스에 함께 넣어둔다.
물론, 모든 지역 변수를 캡쳐하는것이 아니라 접근이 필요한 지역변수만 캡쳐한다.


지역 클래스의 인스턴스를 생성할때 지역클래스가 사용하는 지역변수를 복사한다.


복사한 지역변수를 인스턴스에 포함시켜 생성한다.


process메서드가 종료되어도, 스택영역의 지역변수에 접근하는 것이 아니라, 인스턴스에 있는 캡쳐한 변수를 사용한다.

어? 그러면 만약에 도중에 process()메서드에서 pramVar나 localVar가 변하면 어떻게 될까?

당연히 안된다.
고로, 한번 초기화 하면 변하지 않게 애초에, pramVar나 localVar를 final로 선언하거나, 한번도 바꾸지 않는 사실살 final로 설정해야한다.

만약 캡쳐한 지역변수의 값을 바꾸게되면, 스택영역에 존재하는 지역변수의 값과 인스턴스에 캡쳐한 캡쳐 변수의 값이 서로 달라지는 동기화 문제가 발생한다.

고로, 애초에 JAVA는 캡쳐한 지역변수의 값을 바꾸지 못하게 컴파일 오류를 발생시킨다.

  • 지역변수의 값을 변경하면 인스턴스에 캡쳐한 변수의 값도 변경해야한다.
  • 반대로 인스턴스에 캡쳐한 변수의 값도 변경하면 해당 지역변수의 값도 다시 변경해야한다.
  • 개발자 입장에서는 예상하지 못한 곳에서 값이 변경 될 수 있으므로, 동기화 문제가 발생하지 않게 애초에 자바는, 캡쳐한 지역변수의 값을 변하지 못학 막아둔다.

익명클래스

  • 익명 클래스는 이름이 없는 지역클래스의 한 종류이다.
  • 특정 부모 클래스(인터페이스)를 상속받고 바로 생성하는 경우 사용한다.
  • 익명 클래스는 단 한번만 인스턴스를 생성할 수 있다. 여러번 생성이 필요하면 지역클래스로 선언하고 사용해야한다.
public class LocalOuterV2 {
    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();
    }
    public static void main(String[] args) {
        LocalOuterV2 localOuter = new LocalOuterV2();
        localOuter.process(2);
    }
}

Printer printer = new Printer(){body}
Printer라는 인터페이스를 구현한 익명클래스를 생성하였다.

익명클래스는 부모클래스를 상속받거나, 또는 인터페이스를 구현해야한다.
익명클래스를 사용하면 장점이 클래스를 별도로 정의하지 않고, 인터페이스나 추상클래스를 즉석에서 구현할수 있어 코드가 간결해진다.

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

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

익명클래스를 쓰는이유

익명클래스를 쓰는이유는 함수를 인자로 던지지 못해서이다.

예를 들어보자.

만약 어떠한 함수 logic()이전에 "메서드시작" logic()이후 "메서드 종료"라고 출력을 해야한다고 하자.

그러면,
print(각자 다른 logic()){
sout("메서드 시작")
logic();
sout("메서드 종료")
}
이런식으로 print()메서드의 파라미터로 각 logic()을 던지고 싶지만, 코드 블럭을 인자로 던지지 못한다.

그래서

public class Logic1{
	public void logic(){
    	sout("this is logic1");
    }
}
public class Logic2{
	public void logic(){
    	sout("this is logic2");
    }
}
public class Main{
	public void print(Object obj){
    	sout("메서드 시작");
        obj.logic();
        sout("메서드 종료");
    }
	public static void main(String[] args){
    	Logic1 logic1 = new Logic1();
        Logic2 logic2 = new Logic2();
        
        print(logic1);
        print(logic2);
    }
}

이처럼 메서드 자체를 print메서드의 인자로 넘기지 못하므로, 객체의 인스턴스를 생성해서 넘겨야하는 불편함이 있었다.

익명클래스 사용후

public interface Process{
	void logic();
}
public class Main{
	public static void process(Process process){
    	sout("메서드 시작");
        process.logic();
        sout("메서드 종료");
    }
    
    public static void main(String[] args){
    	Process logic1 = new Process(){
        	@Override
            public void logic(){
            	sout("this is logic1");
            }
        };
        
        Process logic2 = new Process(){
        	@Override
            public void logic(){
            	sout("this is logic2");
            }
        };
        
        process(logic1);
        process(logic2);
    }
}

이전에는

public static void main(String[] args){
    	Logic1 logic1 = new Logic1();
        Logic2 logic2 = new Logic2();
        
        print(logic1);
        print(logic2);
}

참조값을 변수에 담아서 print메서드에 전달해주었다.

이제는 logic()메서드를 interface로 두고, 내가 원하는 logic()마다 익명클래스를 따로 만들어서 해당 익명클래스의 참조를 전달하면 된다.

그런데 이렇게 클래스나 인스턴스를 정의해서 메서드를 전달하는게 너무 불편하다. 그냥 딱 코드만 전달할 수 있는 방법은 없을까?

Lamda

public class Main{
	public static void process(Process process){
    	sout("메서드 시작");
        process.logic();
        sout("메서드 종료");
    }
    
    public static void main(String[] args){
    	process(() -> {
        	sout("logic1")
        });
        
        process(() -> {
        	sout("logic2")
        });
    }
}

람다를 사용하면 가능하다. 해당 코드블럭을 그대로 전달할 수 있다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글