Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바의 정석 기초편 ch2
- 자바 기본편 - 다형성
- 스프링 mvc2 - 검증
- 스프링 입문(무료)
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch9
- 2024 정보처리기사 시나공 필기
- 스프링 mvc1 - 스프링 mvc
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch8
- 2024 정보처리기사 수제비 실기
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch14
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 타임리프
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch5
- @Aspect
- 스프링 mvc2 - 로그인 처리
- 코드로 시작하는 자바 첫걸음
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch7
- 스프링 db1 - 스프링과 문제 해결
- 게시글 목록 api
- 자바의 정석 기초편 ch1
Archives
- Today
- Total
나구리의 개발공부기록
중첩 클래스와 내부 클래스, 중첩클래스, 내부 클래스란?, 정적 중첩 클래스, 정적 중첩 클래스의 활용, 내부 클래스, 내부 클래스의 활용, 같은 이름의 바깥 변수 접근 본문
인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편
중첩 클래스와 내부 클래스, 중첩클래스, 내부 클래스란?, 정적 중첩 클래스, 정적 중첩 클래스의 활용, 내부 클래스, 내부 클래스의 활용, 같은 이름의 바깥 변수 접근
소소한나구리 2025. 1. 22. 16:05출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 중첩클래스, 내부 클래스란?
1) 중첩 클래스와 내부 클래스
(1) 중첩 클래스
- for문 안에 for문을 중첩하는 것을 중첩(Nested) for문이라고 하는데, 클래스도 클래스 안에 클래스를 중첩해서 정의할 수 있으며 이를 중첩 클래스라고 함
(2) 중첩 클래스 분류
- 중첩 클래스는 총 4가지가 있고 크게 2가지로 분류 할 수 있음
- 정적 중첩 클래스(static)
- 내부 클래스 종류
- 내부 클래스
- 지역 클래스
- 익명 클래스
** 참고
- 익명 클래스는 지역클래스의 특별한 버전으로 이후에 설명함
(3) 중첩 클래스의 선언 위치와 변수의 선언위치를 비교
- 중첩 클래스들의 선언 위치는 각 변수들의 선언위치와 같음
- 정적 중첩 클래스 -> 정적 변수와 같은 위치(static)
- 내부 클래스 -> 인스턴스 변수와 같은 위치
- 지역 클래스 -> 지역 변수와 같은 위치(메서드 안)
- 정적 중첩 클래스는 정적 변수와 같이 앞에 static이 붙어있고 내부 클래스는 인스턴스 변수와 같이 static이 없음
- 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 클래스를 정의함
class Outer {
static class StaticNested { // 정적 중첩 클래스
...
}
class Inner { // 내부 클래스
...
}
public void process() {
int localvar = 0; // 지역 변수
class Local { ... } // 지역 클래스
Local local = new Local(); // 지역 클래스 생성
}
}
(4) 중첩이라는 단어와 내부라는 단어의 차이
- 중첩(Nested): 어떤 다른 것이 내부에 위치하거나 포함되는 구조적 관계
- 내부(Inner): 나의 내부에 있는 나를 구성하는 요소
- 즉, 여기에서 의미하는 중첩은 나의 안에 있지만 내것이 아닌 것을 말하며 단순히 위치만 안에 있는 반면 내부는 나의 내부에서 나를 구성하는 요소를 뜻함
- 정적 중첩 클래스는 바깥 클래스의 안에 있지만 바깥 클래스와 관계 없는 전혀 다른 클래스를 말하며 내부 클래스는 바깥 클래스의 내부에 있으면서 바깥 클래스를 구성하는 요소를 말함
- 여기서의 중첩과 내부를 분류하는 핵심은 바깥 클래스 입장에서 볼 때 안에있는 클래스가 나의 인스턴스에 소속되는가 되지 않는가의 차이임
- 정적 중첩 클래스는 static으로 바깥 클래스와 전혀 다른 클래스이므로 인스턴스에 소속되지 않고 내부 클래스는 바깥 클래스를 구성하는 요소이므로 바깥 클래스의 인스턴스에 소속이 됨
2) 정리
(1) 중첩 클래스 종류 정리
- 정적 중첩 클래스: static이 붙고 바깥 클래스의 인스턴스에 소속되지 않음
- 내부 클래스: static이 붙지 않고 바깥 클래스의 인스턴스에 소속됨
(2) 내부클래스의 종류 정리
- 내부 클래스(inner class): 바깥 클래스의 인스턴스의 멤버에 접근
- 지역 클래스(local class): 내부 클래스의 특징 + 지역 변수에 접근
- 익명 클래스(anonymous class): 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스
(3) 용어 정리
- 중첩 클래스: 정적 중첩 클래스 + 내부 클래스 종류 모두 포함
- 정적 중첩 클래스: 정적 중첩 클래스만을 말함
- 내부 클래스: 내부 클래스, 지역 클래스, 익명 클래스를 포함하여 말함
(4) 중첩 클래스는 언제 사용?
- 내부 클래스를 포함한 모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 함
- 외부의 여러 클래스가 특정 중첩 클래스를 사용한다면 중첩 클래스로 만들면 안됨
(5) 중첩 클래스를 사용하는 이유
- 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화가 됨, 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있음
- 캡슐화: 중첩 클래스는 바깥 클래스의 private멤버에 접근할 수 있으므로 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있음, 말로는 이해가 어려워서 예제를 통해서 이해하는 것을 권장함
** 참고 - 실무 용어
- 실무에서는 중첩, 내부라는 단어를 명확하게 구분하지 않고 중첩 클래스 혹은 내부 클래스라고 이야기함
- 엄밀하게 구분하자면 static이 붙어있는 정적 중첩 클래스는 내부 클래스라고하면 안되지만 대부분의 개발자들이 둘을 구분해서 말하지 않기 때문에 내부 또는 중첩 클래스라고하면 상황과 문맥에 따라서 이해하면됨
2. 정적 중첩 클래스
1) 정적 중첩 클래스
(1) NestedOuter
- 정적 중첩 클래스는 앞에 static이 붙음
- 정적 중첩 클래스는 자신의 멤버와 바깥 클래스의 클래스 멤버에는 접근할 수 있지만 인스턴스 멤버에는 접근할 수 없음
- private 접근 제어자: static 변수는 클래스이름.변수이름으로 원래 어디서든 접근할 수 있음, 그러나 정적 중첩 클래스는 바깥 클래스와 같은 클래스 안에 있으므로 private static 변수에도 접근할 수 있다는 것이 특징임
** 참고
- NestedOuter.outclassValue으로 접근하는 것이 원래의 static 변수의 접근법이지만 outclassValue만 사용해도 바깥 클래스에 있는 필드를 찾아서 접근함
package nested.nested;
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);
}
}
}
(2) NestedOuterMain
- 정적 중첩 클래스는 new 바깥 클래스.중첩클래스()로 생성할 수 있으며 NestedOuter.Nested와 같이 바깥 클래스.중첩클래스로 접근할 수 있음
- 여기서 new NestedOuter()로 만든 바깥 클래스의 인스턴스와 new NestedOuter.Nested()로 만든 정적 중첩 클래스의 인스턴스는 서로 아무 관계가 없는 인스턴스임
- 단지 구조상 중첩 해 두었을 뿐이고 아무런 관련이 없으므로 정적 중첩 클래스의 인스턴스만 따로 생성해도 됨
- 중첩 클래스를 출력해보면 중첩 클래스의 이름은 바깥 클래스$중첩클래스의 조합으로 만들어지는 것을 확인할 수 있음
package nested.nested;
public class NestedOuterMain {
public static void main(String[] args) {
NestedOuter outer = new NestedOuter();
NestedOuter.Nested nested = new NestedOuter.Nested();
nested.print();
// 클래스 정보
System.out.println("outer.getClass() = " + outer.getClass());
}
}
/* 실행 결과
1
3
outer.getClass() = class nested.nested.NestedOuter
*/
(3) 인스턴스가 생성된 상태
- NestedOuter와 Nested의 변수와 메서드들이 생성된 상태를 그림으로 표현하면 서로가 아무런 관련이 없음
(4) 바깥 클래스의 멤버에 접근
- Nested.print()를 보면 정적 중첩 클래스는 바깥 클래스의 정적 필드에는 접근할 수 있지만 바깥 클래스가 만든 인스턴스 필드에는 참조값이 없기때문에 접근할 수가 없음
(5) 정리
- 정적 중첩 클래스는 사실 다른 클래스를 그냥 중첩해둔 것 일뿐 아무런 관계가 없음
- NestedOuter에 선언된 static 변수에 접근하는 것은 중첩 클래스가 아니여도 어차피 클래스명.정적필드명으로 접근이 가능함
- 즉, 정적 중첩 클래스를 만들지 않고 그냥 클래스를 2개 따로 만든것과 같음
- 하나의 차이는 같은 클래스에 있으므로 정적 중첩 클래스는 바깥 클래스의 private 접근 제어자에 접근할 수 있다는 정도임
3. 정적 중첩 클래스의 활용
1) 정적 중첩 클래스로 리팩토링 전
(1) NetworkMessage
- Network객체에서만 사용되는 객체
package nested.nested.ex1;
// Network 객체 안에서만 사용
public class NetworkMessage {
private String content;
public NetworkMessage(String content) {
this.content = content;
}
public void print() {
System.out.println(content);
}
}
(2) Network
- NetworkMessage 인스턴스를 생성하고 해당 인스턴스의 print()메서드를 호출
package nested.nested.ex1;
public class Network {
public void sendMessage(String text) {
NetworkMessage networkMessage = new NetworkMessage(text);
networkMessage.print();
}
}
(3) NetworkMain
- Network 인스턴스를 생성하고 해당 인스턴스의 sendMessage()메서드에 인자를 입력하여 메시지를 출력
- NetworkMain은 Network 클래스만 사용하고 NetworkMessage 클래스는 전혀 사용하지않으며 NetworkMessage클래스는 Network 내부에서만 사용됨
package nested.nested.ex1;
public class NetworkMain {
public static void main(String[] args) {
Network network = new Network();
network.sendMessage("hello java");
}
}
(4) ex1 패키지의 구조
- main을 제외하면 ex1 패키지에는 Network와 NetworkMessage 두개의 클래스가 있음
- Network 관련 라이브러리를 사용하기 위해서 ex1 패키지를 열어본 개발자는 Network와 NetworkMessage 둘다 사용해야하는지, NetworkMessage에 담아서 Network에 전달해야 하는지에 대한 생각을 할 것이며 NetworkMessage가 다른 여러 클래스에서 사용 될 것이라고 생각할 수 있음
- 그리고 두 클래스의 코드를 모두 확인하고 나서야 Network 클래스만 사용하면 되는 것이라고 이해하게 될 것임
2) 정적 중첩 클래스로 리팩토링 전
(1) Network
- NetworkMessage 클래스를 Network 클래스 안에 중첩해서 만들고 접근제어자를 private으로 설정했으므로 외부에서 NetworkMessage에 접근할 수 없음
- 즉 new Network.NetworkMessage()로 접근할 수 없음
package nested.nested.ex2;
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.println(content);
}
}
}
(2) NetworkMain
- ex1 패키지에 작성한 코드와 완전 동일한 코드
- ex2에 선언된 Network클래스를 사용하도록 변경만하고 프로그램을 실행하면 정상적으로 코드가 출력됨
(3) ex2 패키지의 구조
- 실행하는 main 클래스를 제외하면 ex2 패키지에는 Network 클래스 하나만 있으므로 Network 관련 라이브러리를 사용하기 위해 ex2 패키지를 열어본 개발자는 해당 클래스만 확인할 것임
- 추가로 NetworkMessage가 중첩 클래스에 private 접근 제어자로 되어있는 것을 보고 Network 내부에서만 단독으로 사용하는 클래스라고 인지할 수 있음
(4) 중첩 클래스의 접근
- 다른 중첩 클래스에 접근: 나의 클래스에 포함된 중첩 클래스가 아니라 다른 곳에 있는 중첩 클래스에 접근할 때는 바깥클래스.중첩클래스로 접근해야함
- 나의 중첩 클래스에 접근: 나의 클래스에 포함된 중첩 클래스에 접근할 때는 바깥 클래스 이름을 적지 않아도 됨
- 중첩 클래스(내부 클래스 포함)는 그 용도가 자신이 소속된 바깥 클래스 안에서 사용되는 것임
- 자신이 소속된 바깥 클래스가 아닌 외부에서 생성하고 사용하고 있다면 이미 중첩 클래스의 용도에 맞지 않을 수 있으므로 이럴 때는 중첩 클래스를 밖으로 빼는 것이 더 나은 선택일 수 있음
4. 내부 클래스
1) 내부 클래스
(1) InnerOuter
- 정적 중첩 클래스는 바깥 클래스와 서로 관계가 없지만 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가됨
- 내부 클래스 앞에는 static이 붙지 않으며 자신의 멤버는 물론 바깥 클래스의 인스턴스 멤버, 클래스 멤버 모두 접근이 가능함
- 내부 클래스도 바깥 클래스와 같은 클래스 안에 있으므로 바깥 클래스의 private접근 제어자에 접근할 수 있음
package nested.inner;
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);
}
}
}
(2) InnerOuterMain
- 내부 클래스는 바깥 클래스의 인스턴스에 소속되므로 바깥 클래스의 인스턴스 정보를 알아야 생성할 수 있으므로 바깥클래스의 인스턴스 참조.new 내부 클래스()로 생성할 수 있음
- outer.new Inner()로 생성한 내부 클래스는 개념상 바깥 클래스의 인스턴스 내부에 생성되며 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스를 생성할 수 있음
package nested.inner;
public class InnerOuterMain {
public static void main(String[] args) {
InnerOuter outer = new InnerOuter();
InnerOuter.Inner inner = outer.new Inner();
inner.print();
System.out.println("inner.getClass() = " + inner.getClass());
}
}
(3) 내부 클래스의 생성 개념(좌), 실제(우)
- 개념적으로는 왼쪽 그림처럼 내부 클래스는 바깥 클래스의 인스턴스 내부에서 내부 클래스의 인스턴스가 생성되는 것처럼 이해할 수 있음
- 즉, 내부 인스턴스는 바깥 인스턴스를 알고있기 때문에 인스턴스의 멤버에 접근할 수 있음
- 그러나 실제로는 오른쪽 그림처럼 내부 인스턴스가 바깥 인스턴스 안에 생성되는 것은 아니고 개념상 인스턴스 안에 생성된다고 이해하면 됨
- 즉, 내부 인스턴스는 바깥 인스턴스의 참조를 보관하고 이 참조를 통해 바깥 인스턴스의 멤버에 접근할 수 있는 것임
(4) 정리
- 정적 중첩 클래스와는 다르게 내부 클래스는 바깥 인스턴스에 소속됨
- 정적 중첩 클래스는 다른 클래스를 그냥 중첩해 둔 것이고 전혀 관계가 없지만 내부 클래스는 바깥 클래스의 인스턴스 내부에서 구성요소로 사용됨
- 내부 클래스는 바깥 클래스의 인스턴스를 먼저 생성하고 그 참조를 통해 생성할 수 있음
5. 내부 클래스의 활용
1) 내부 클래스로 리팩토링 전
(1) Engine
- Car 클래스에서만 사용되는 클래스
- Engine을 시작하기 위해서는 차의 충전 레벨과 차의 이름이 필요하다고 가정하고 작성
package nested.inner.ex1;
// Car에서만 사용
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() + "의 엔진을 구동합니다.");
}
}
(2) Car
- Car클래스는 엔진에 필요한 메서드들을 제공해야므로 getModel(), getChargeLevel() 메서드를 생성
- 해당 메서드들은 엔진에서만 사용되고 다른곳에서 사용되지 않지만 결과적으로 외부에 노출되어버림
package nested.inner.ex1;
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 + " 시작 완료");
}
}
(3) CarMain
- Car()인스턴스를 생성하여 start()메서드를 호출하면 정상적으로 출력되는 것을 확인할 수 있음
package nested.inner.ex1;
public class CarMain {
public static void main(String[] args) {
Car myCar = new Car("BMW i7", 100);
myCar.start();
}
}
/* 실행 결과
충전 레벨 확인: 100
BMW i7의 엔진을 구동합니다.
BMW i7 시작 완료
*/
2) 내부 클래스로 리팩토링 후
(1) Car
- 엔진을 Car의 내부 클래스로 생성
- 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 대는 바깥 클래스의 이름을 생략할 수 있음
- 바깥 클래스에서 내부 클래스의 인스턴스를 생성할 때 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 이스턴스를 자동으로 참조함
- 내부 클래스인 Engine은 이미 Car인스턴스를 알고 있기 때문에 기존에 Car을 생성하던 코드는 모두 지워도됨
- 심지어 Car의 인스턴스 변수들에 직접 접근할 수 있으므로 public으로 Engine에 제공했던 메서드들을 모두 삭제할 수 있음
package nested.inner.ex2;
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 + " 시작 완료");
}
// Car에서만 사용
private class Engine {
public void start() {
System.out.println("충전 레벨 확인: " + chargeLevel);
System.out.println(model + "의 엔진을 구동합니다.");
}
}
}
(2) CarMain
- ex1 패키지에서 만든 동일한 코드를 ex2 패키지에 작성 후 프로그램을 실행해보면 정상적으로 출력이 되는 것을 확인할 수 있음
(3) 리팩토링 전의 문제
- 결과적으로 외부에 불필요한 Car 클래스의 정보들인 모델 이름과 충전 레벨이 외부에 노출되어 캡슐화를 떨어뜨림
- 리팩토링 후에는 getModel(), getChargeLevel()과 같은 메서드를 모두 제거할 수 있었고 결과적으로 꼭 필요한 메서드만 외부에 노출함으로써 Car의 캡슐화를 더 높일 수 있었음
6. 같은 이름의 바깥 변수 접근
1) 같은 이름의 바깥 변수 접근
(1) ShadowingMain
- 바깥 클래스의 멤버 변수와 내부 클래스의 멤버 변수, 내부 클래스의 메서드의 지역 변수의 이름이 모두 같을때에는 어떤 변수를 먼저 사용할지 우선순위가 필요함
- 프로그래밍에서 우선순위는 대부분 더 가깝거나, 더 구체적인 것이 우선권을 가지므로 go() 메서드의 경우 지역 변수인 value가 가장 가깝기 때문에 우선순위가 가장 높음
- 이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(shadowing)이라 함
- 다른 변수를 가리더라도 인스턴스의 참조를 사용하면 외부 변수에 접근할 수 있으므로 this.value로 내부 클래스 인스턴스 변수에 접근할 수 있고 바깥클래스이름.this.value는 바깥 클래스 인스턴스 변수에 접근할 수 있음
- 그러나 프로그래밍에서 가장 중요한 것은 명확성이므로 이렇게 이름이 같은 경우 자체를 만들지 않도록 처음부터 이름을 다르게 지어서 명확하게 구분하는 것이 제일 좋음
package nested;
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
*/