[Java] 중첩 클래스 종류, 정적 중첩 클래스와 내부 클래스, 그리고 지역 클래스와 익명 클래스

벼랑 끝 코딩·2025년 2월 26일
0

Java

목록 보기
22/40

class Clazz {
	// 코드
}

우리가 지겹도록 만들고 사용하는 클래스.
이 클래스를 사용하는 방법에는 또 다른 종류가 있다.
어떻게 사용할 수 있을까?
사실 조금만 생각해보면 하나의 종류밖에 더 찾을 수 없다.

class Clazz {
	
    class InnerClazz {
    	// 코드
    }
}

바로 클래스 안에 클래스를 사용하는 중첩 클래스를 사용하는 것이다!
하지만 중첩 클래스 안에는 여러 종류가 있었으니..
오늘은 중첩 클래스의 종류에 대해 알아보겠다.

정적 중첩 클래스

class Clazz {
	
    static class InnerClazz {
    	// 코드
    }
}

public void createInnerClazz() {
	// ** 정적 중첩 클래스 생성 방법 **
	Clazz.InnerClazz innerClazz = new Clazz.InnerClazz();
}

정적 중첩 클래스는 static 키워드를 이용하여 선언한다.
new 키워드와 함께 외부 클래스.정적 중첩 클래스()로 생성할 수 있다.
정적 중첩 클래스는 어떤 특징을 가지고 있길래 이렇게 사용하는 것일까?

정적 중첩 클래스 특징

class Clazz {

	private static int staticVar;  // 클래스 멤버 접근 가능
    private int privateVar;  // 인스턴스 멤버 접근 불가
	
    static class InnerClazz {
    	private int innerVar;  // 접근 가능
    	// 코드
    }
}
  • 정적 중첩 클래스는 단순히 위치만 내부에 있을 뿐, 다른 클래스의 성격을 가진다.
  • 다른 클래스인 만큼, 외부 클래스의 인스턴스 멤버에 접근할 수 없다.
  • 같은 클래스에서만 사용 가능한 private 클래스 멤버에는 접근이 가능하다.

사실 이러한 특징을 보면, 클래스를 별도로 생성하는 것과 비교했을 때,
정적 중첩 클래스가 외부 클래스의 private 클래스 멤버에 접근이 가능한 것 외에는 별 차이가 없다.
클래스를 별도로 생성하는 것과 다를 바가 없다는 이야기이다.

class Clazz {
	// 코드
}

class InnerClazz {
	// 코드
}

정적 중첩 클래스 사용 이유

클래스를 따로 생성하는 것과 큰 차이가 없다면,
도대체 정적 중첩 클래스는 왜 사용하는 것일까?

논리적 그룹화

분리된 클래스는 어디서 어떻게 사용되는지 잘 구분할 수 없다.
하지만 정적 중첩 클래스를 private 접근 제어자로 생성할 경우,
외부 클래스를 제외한 다른 곳에서의 생성을 제한하기 때문에
정적 중첩 클래스가 외부 클래스에서 밖에 사용되지 않는다는 점을 바로 인지할 수 있다.
이렇게 논리적 그룹화를 위해 정적 중첩 클래스를 사용한다.

class Clazz {
	
    // ** 외부 클래스인 Clazz에서만 사용 가능 **
    private static class InnerClazz {
    	// 코드
    }
}

내부 클래스

class Clazz {
	
    class InnerClazz {
    	// 코드
    }
}

내부 클래스는 static 키워드를 사용하지 않는다.
또한, 정적 중첩 클래스와 달리 단순히 위치만 안에 있는게 아니라 외부 클래스의 구성요소가 된다.

내부 클래스 특징

  • 내부 클래스는 외부 클래스의 인스턴스 멤버가 된다.
  • 같은 클래스에 위치하여 외부 클래스의 클래스 멤버에 접근할 수 있다.
  • 외부 클래스의 인스턴스 멤버에도 접근 가능하다.
class Clazz {
	
    private static int staticVar;  // 접근 가능
    private int privateVar;  // 접근 가능
	
    class InnerClazz {
    	
        private int innerVar;  // 접근 가능
        
    	// 코드
    }
}

public void createInnerClazz() {
	Clazz clazz = new Clazz(); // ** 외부 클래스 먼저 생성 **
    Clazz.InnerClazz innerClazz = clazz.new InnerClazz();
}

내부 클래스는 외부 클래스의 인스턴스 멤버에 소속되기 때문에,
외부 클래스의 인스턴스 정보를 알아야 생성할 수 있다.
따라서, 외부 클래스 인스턴스를 먼저 생성해야 내부 클래스 인스턴스를 생성할 수 있다.
생성된 외부 클래스 인스턴스의 정보(참조값)를 알고있기 때문에,
외부 클래스의 인스턴스 멤버에도 접근이 가능한 것이다.

정적 중첩 클래스와의 차이

정적 중첩 클래스는 외부 클래스와 위치만 같을 뿐 다른 클래스이기 때문에,
정적 중첩 클래스 인스턴스를 생성하기 위해 외부 클래스 인스턴스를 생성할 필요가 없다.
하지만 내부 클래스는 외부 클래스에 소속되는 인스턴스이기 때문에,
내부 클래스 인스턴스 생성을 위해서는 외부 클래스 인스턴스 생성이 선행되어야 한다.
외부 클래스의 인스턴스를 생성한다는 점에서 두 클래스는 메모리 사용에 차이가 있다.

class Clazz {
	static class NestedClazz {  // 정적 중첩 클래스
    	// 코드
    }
    
    class InnerClazz {  // 내부 클래스
    	// 코드
    }
}

public void create() {
	Clazz.NestedClazz nestedClazz = new Clazz.NestedClazz();  // 가능
    
    Clazz.InnerClazz innerClazz = new Clazz.InnerClazz();  // 불가능
    Clazz clazz = new Clazz(); // ** 외부 클래스 인스턴스 먼저 생성 **
    Clazz.InnerClazz innerClazz = clazz.new InnerClazz();

내부 클래스 사용 이유

우리는 논리적 그룹화를 위해 정적 중첩 클래스를 사용한다고 했다.
내부 클래스도 마찬가지로 논리적 그룹화를 위해 사용한다.
하지만 이것만 있었다면 굳이 종류를 나눌 필요가 없었겠지.

캡슐화

class ClazzA {
	// 코드
    
    public void useOnlyClazzB() {
    	// 메서드 바디
    }
}

// ** ClazzA에서만 사용되는 클래스 **
class ClazzB {
	// 코드
}

ClazzB가 ClazzA에서만 사용 된다고 가정하자.
ClazzA에서는 ClazzB만을 위한 메서드가 존재한다.
여기선 두 가지 문제가 발생한다.

사용자는 외부에 노출된 ClazzB가 ClazzA에서만 사용되는지 알 수 없다.
ClazzA와 ClazzB가 논리적으로 그룹화되지 않아 직관적인 사고가 불가능하다.
또 한 가지는, ClazzB만을 위한 useOnlyClazzB() 메서드가 모든 클래스에 노출된다.
지금이야 메서드 이름을 통해 '아, ClazzB에서만 사용되는구나' 라고 판단할 수 있지만,
이름이 서비스와 관련된 경우 메서드가 어디서 또 어떻게 사용되는지 사용자는 알 수 없다.
따라서 ClazzB에서만 사용하는 메서드가 모든 클래스에 그대로 노출된다.

class Clazz {

	// ** private로 선언하여 메서드 캡슐화 **
	private void useOnlyInnerClazz() {
    	// 메서드 바디
    }
	
    // ** 내부 클래스로 논리적 그룹화 **
    private class InnerClazz {
    	useOnlyInnerClazz();
    }
}

위와 같이 내부 클래스로 선언하면 논리적 그룹화가 가능하여,
한 클래스에서만 사용되는 특정 클래스를 직관적으로 이해할 수 있다.
또한, 내부 클래스는 외부 클래스의 정보를 가지고 있어 인스턴스 멤버에 접근 가능하기 때문에
한 클래스에서만 사용되던 public 메서드를 private 메서드로 바꾸어 캡슐화가 가능하다.
더 논리적인 클래스 구조를 설계할 수 있다.

지역 클래스

지역 클래스는 내부 클래스의 종류 중 하나이다.
따라서 내부 클래스와 같은 특징을 가진다.

class Clazz {

	private static int staticVar;  // 접근 가능
    private int privateVar;  // 접근 가능
	
    public void clazzMethod(int paramVar) {  // ** 접근 가능 **
    
    	private int localVar = 0;  // ** 접근 가능 **
    	
        class InnerClazz {
        
        	private int innerVar;  // 접근 가능
        	
        	// 코드
        }
    }
}

지역 클래스는 외부 클래스의 메서드 내부에 지역 변수처럼 선언되어 지역 클래스라고 불린다.
내부 클래스의 특징에 추가로 지역 변수와 메서드 매개 변수에 접근이 가능하다.

지역 클래스 생존 범위

지역 클래스 생존 범위를 알아보기 위해서는
메서드 영역, 스택 영역, 힙 영역에 대해 이해해야 한다.
다음 글을 참고하고 오자.

class Clazz {
	
    public InnerClazz clazzMethod(int paramVar) { // 외부 클래스 메서드 매개 변수
    	
        private int localVar = 1;  // 지역 변수
        
        class InnerClazz {  // 지역 클래스
        	
            public int innerClazzAddMethod() {
            	return paramVar + localVar;  // ** 매개 변수 + 지역 변수 **
            }
        }
        
        return new InnerClazz();
    }
}

public void localClazzMethod() {
	Clazz clazz = new Clazz();
    
    // ** clazzMethod() 호출 및 스택 영역 저장, 제거 **
    InnerClazz innerClazz = clazz.clazzMethod(1);
    
    //  ** 인스턴스 영역 innerClazz의 innerClazzMethod 호출 **
    int result = innerClazz.innerClazzMethod();
}

지역 클래스는 인스턴스이기 때문에 힙 영역에서 관리된다.
지역 클래스와는 달리 clazzMethod()는 메서드 호출과 동시에 스택 영역에서 관리된다.
그리고 메서드가 종료되면 clazzMethod()도 마찬가지로 스택 영역에서 제거된다.

.. 그러면 안되는데?
paramVar, localVar은 clazzMethod()가 제거되면 함께 제거된다.
하지만 인스턴스 영역에서 생성되어 메서드의 스택 영역보다 오래 살아남게 된 InnerClazz는
메서드의 매개 변수와 지역 변수에 접근이 가능하기 때문에,
메서드가 스택 영역에서 삭제된 이후에도 매개 변수와 지역 변수의 합을 계산하려고 한다.

result = 2;

하지만 결과는 정상적으로 출력된다.
어찌된 영문일까..?

캡쳐(Capture)

자바는 지역 변수처럼 생성되는 지역 클래스이지만 인스턴스에 관리되고,
하지만 매개 변수와 지역 변수에 접근이 가능하여
생존 범위에서 차이가 발생할 수 있는 문제점을 위해 캡쳐라는 기능을 제공한다.

캡쳐(Capture)란, 지역 클래스의 인스턴스 생성 시점
지역 클래스 인스턴스가 참고할 수 있는 변수를 복사해서 인스턴스에 함께 저장하는 것이다.

class Clazz {
	
    public InnerClazz clazzMethod(int paramVar) { // ** 매개 변수 캡쳐 **
    	
        private int localVar = 1;  // ** 지역 변수 캡쳐 **
        
        class InnerClazz {  // 지역 클래스
        	
            public int innerClazzAddMethod() {
            	return paramVar + localVar;  // ** 매개 변수와 지역 변수 계산 가능 **
            }
        }
        
        return new InnerClazz();
    }
}

위와 같은 상황에서 InnerClazz는 생성 시 힙 영역에서 관리되지만,
clazzMethod()는 스택 영역에서 관리되어 생존 범위에서 문제가 발생할 수 있다.
그렇기 때문에 InnerClazz 지역 클래스 인스턴스를 생성할 때,
매개 변수인 paramVar과 지역 변수인 localVar을 함께 저장하여 인스턴스를 관리한다.

따라서 메서드 영역의 clazzMethod()가 제거되어
clazzMethod()의 매개 변수와 지역 변수에 접근할 수 없어도,
지역 클래스 인스턴스 자신에 저장한 매개 변수와 지역 변수의 값에 접근하여 문제를 해결할 수 있다.

지역 변수 수정 제한

지역 클래스 인스턴스는 생성 시점에 지역 클래스가 참고하는 변수를 저장한다고 했다.
여기서 눈여겨봐야 할 것은 바로 생성 시점이라는 것이다.

나와 같은 장난 꾸러기들은 생성 시점이라는 단어를 보고 떠오른 게 있을 것이다.
그럼 생성 이후에 바꾼다면?

class Clazz {
	
    public InnerClazz clazzMethod(int paramVar) { // 외부 클래스 메서드 매개 변수
    	
        private int localVar = 1;  // 지역 변수
        
        class InnerClazz {  // 지역 클래스
        	
            public int innerClazzAddMethod() {
            	return paramVar + localVar;  // 매개 변수 + 지역 변수
            }
        }
        
        paramVar = 2 // ** 매개 변수 변경 시도 → 불가, 컴파일 오류 **
        localVar = 2 // ** 지역 변수 변경 시도 → 불가, 컴파일 오류 **
        
        return new InnerClazz();
    }
}

지역 클래스 인스턴스가 참고하는 변수인 매개 변수와 지역 변수의 값을 변경할 경우,
생성 시점에 캡처한 값도 함께 변경해야 하는데
그 과정에서 사이드 이펙트가 발생할 확률이 매우 높기 때문에
자바는 매개 변수와 지역 변수의 변경을 제한하고 있다.

익명 클래스

내부 클래스의 종류 중 하나가 지역 클래스.
지역 클래스의 중류 중 하나는 익명 클래스가 있다.
지역 클래스인데 이름이 없어서 익명 클래스라고 한다.

이름이 없는데 어떻게 사용할 것인가?
그래서 익명 클래스에는 하나의 제약이 있다.
바로 인터페이스를 구현하거나 클래스를 상속하여 확장하는 경우에만 사용이 가능하다.

class Parent {
	// 코드
}

class Clazz {
	
    public void classMethod() {
    	
        // ** 부모 객체를 생성하면서 바로 코드 작성 **
        Parent parent = new Parent() {
        	// 코드
        };
    }
}

차이를 위해 지역 클래스와 비교해보겠다.

class Parent {
	// 코드
}

class Clazz {
	
    public void classMethod() {
    	
        // ** 클래스 이름을 명시하여 생성 **
       	class Child extends Parent {
        	// 코드
        }
    }
}

클래스 이름을 명시하여 생성해야만 하는 지역 클래스와는 달리,
익명 클래스는 지역 클래스(부모 또는 인터페이스)의 생성과 동시에 코드를 구현하여
클래스를 정의하고 객체를 생성하는 과정을 간소화했다.

객체 생성과 동시에 코드를 구현하기 때문에,
객체 생성은 1회만 가능하다.
객체 생성이 2회로 늘어나는 경우, 지역 클래스로 만들어야 한다.

마무리

클래스의 논리적 그룹화를 위한 중첩 클래스에 대해 알아봤다.
정적 중첩 클래스와 내부 클래스의 차이를 이해하고, 무엇을 어떻게 사용해야 할 지 고려해보자.
그리고 지역 클래스의 동작 원리를 이해하고,
익명 클래스의 편리함도 살펴보면서 앞으로 중첩 클래스를 마구 사용해보자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글