[중첩 클래스]
클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나, 특정 클래스에만 관계를 맺을 경우에는 중첩 클래스로 선언하는 것이 유지보수에 도움이 되는 경우가 많다.
중첩 클래스 개념
중첩 클래스란 클래스 내부에 선언한 클래스로, 중첩 클래스를 사용하면 클래스의 멤버를 쉽게 사용할 수 있고 외부에는 중첩 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다.
중첩 클래스의 분류
- 멤버 클래스 : 클래스의 멤버로서 선언되는 중첩 클래스
- 로컬 클래스 : 메소드 내부에서 선언되는 중첩 클래스
- 중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트코드 파일(.class)이 별도로 생성
[인스턴스 멤버 클래스]
인스턴스 멤버 클래스는 다음과 같이 A 클래스의 멤버로 선언된 B 클래스
[public] class A {
// 인스턴스 멤버 클래스
[public | private] class B {
}
}
접근 제한자에 따른 인스턴스 멤버 클래스의 접근 범위
- public class B {} : 다른 패키지에서 B 클래스를 사용할 수 있다.
- class B {} : 같은 패키지에서만 B 클래스를 사용할 수 있다.
- private class B {} : A 클래스 내부에서만 B 클래스를 사용할 수 있다.
인스턴스 멤버 클래스 B는 주로 A 클래스 내부에서 사용되므로 private 접근 제한을 갖는 것이 일반적이다.
B 객체는 인스턴스 필드값, 생성자, 인스턴스 메소드에서 생성할 수 O
A 객체가 있어야 B 객체도 생성할 수 있기 때문
public class A {
//인스턴스 멤버 클래스
class B {}
//인스턴스 필드 값으로 B 객체 대입
B field = new B();
//생성자
A() {
B b = new B();
}
//인스턴스 메소드
void method() {
B b = new B();
}
}
B 객체를 A 클래스 외부에 생성하려면 default 또는 public 접근 제한을 가져야 하고, A 객체를 먼저 생성한 다음에 B 객체를 생성해야 한다.
A a = new A();
A.B b = a.new B();
인스턴스 멤버 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드 선언이 올 수 있다. 정적 필드와 정적 메소드는 Java 17부터 선언이 가능하다.
A.java
public class A {
//인스턴스 멤버 클래스
class B {
//인스턴스 필드
int field1 = 1;
//정적 필드(Java17부터 허용)
static int field2 = 2;
//생성자
B() {
System.out.println("B-생성자 실행");
}
//인스턴스 메소드
void method1() {
System.out.println("B-method1 실행");
}
//정적 메소드(Java17부터 허용)
static void method2() {
System.out.println("B-method2 실행");
} //Java17부터 허용
}
//인스턴스 메소드
void useB() {
//B 객체 생성 및 인스턴스 필드 및 메소드 사용
B b = new B();
System.out.println(b.field1);
b.method1();
//B클래스의 정적 필드 및 메소드 사용
System.out.println(B.field2);
B.method2();
}
}
[정적 멤버 클래스]
정적 멤버 클래스 개념
정적 멤버 클래스는 static 키워드와 함께 A 클래스 멤버로 선언된 B 클래스
[public] class A {
// 정적 멤버 클래스
[public | private] static class B {
}
}
접근 제한자에 따른 인스턴스 멤버 클래스의 접근 범위
- public static class B {} : 다른 패키지에서 B 클래스를 사용할 수 있다.
- static class B {} : 같은 패키지에서만 B 클래스를 사용할 수 있다.
- private static class B {} : A 클래스 내부에서만 B 클래스를 사용할 수 있다.
정적 멤버 클래스는 A 클래스 외부에서 A와 함께 사용되는 경우가 많기 때문에 주로 default나 public 접근 제한을 가진다. B 객체는 A 클래스 내부 어디든 객체를 생성할 수 있다.
A.java
public class A {
//인스턴스 멤버 클래스
static class B {}
//인스턴스 필드 값으로 B 객체 대입
B field1 = new B();
//정적 필드 값으로 B 객체 대입
static B field2 = new B();
//생성자
A() {
B b = new B();
}
//인스턴스 메소드
void method1() {
B b = new B();
}
//정적 메소드
static void method2() {
B b = new B();
}
}
A 객체 생성 없이 A 클래스로 접근해서 B 객체를 생성할 수 있다.
A.B b = new A.B();
정적 멤버 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드 선언이 올 수 있다.
A.java
public class A {
//정적 멤버 클래스
static class B {
//인스턴스 필드
int field1 = 1;
//정적 필드(Java17부터 허용)
static int field2 = 2;
//생성자
B() {
System.out.println("B-생성자 실행");
}
//인스턴스 메소드
void method1() {
System.out.println("B-method1 실행");
}
//정적 메소드(Java17부터 허용)
static void method2() {
System.out.println("B-method2 실행");
} //Java17부터 허용
}
}
CustomFilter.java (스프링에서 커스텀 필터 구현한 예)
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class); // 부모 클래스인 AbstractGatewayFilterFactory의 생성자 호출
}
@Override
public GatewayFilter apply(Config config) {
// Custom Pre Filter
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: request id -> {}", request.getId());
// Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom POST filter: response -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// Put the configuration properties
}
}
위에서 Config 클래스를 정적으로 선언한 이유
- 독립적인 사용 가능성
- Config 클래스가 정적으로 선언되었기 때문에, CustomFilter 클래스의 인스턴스가 없이도 Config 클래스를 사용할 수 있다. 이는 Config 객체를 생성하거나 접근할 때 외부 클래스의 인스턴스가 필요하지 않다는 것을 의미한다.
CustomFilter.Config config = new CustomFilter.Config();
2. 논리적 그룹화
- Config 클래스는 CustomFilter 클래스와 논리적으로 관련이 있다. Config 클래스는 CustomFilter의 설정을 정의하는데 사용되며, CustomFilter 외부에서 단독으로 사용되지 않는다. 따라서 CustomFilter 내부에 중첩 클래스 형태로 정의하는 것이 논리적으로 더 명확하다.
3. 캡슐화
- Config 클래스가 CustomFilter 클래스 내부에 중첩됨으로써, Config 클래스가 CustomFilter와 관련이 있음을 명확히 하여, 코드의 가독성을 높이고 유지보수성을 향상시킨다.
4. 네임스페이스 관리
- 클래스가 많아지면 네임스페이스 충돌의 가능성이 높아지는데, 중첩 클래스를 사용하면 이러한 충돌을 피할 수 있다. Config 클래스가 CustomFilter 내부에 있기 때문에, Config라는 이름을 가진 다른 클래스와의 충돌을 피할 수 있다.
[로컬 클래스]
로컬 클래스 개념
생성자 또는 메소드 내부에서 선언된 클래스
[public] class A }
// 생성자
public A() {
class B {}
}
// 메소드
public void method() {
class B {}
}
}
로컬 클래스는 생성자와 메소드가 실행될 동안에만 객체를 생성할 수 O
A.java
public class A {
//생성자
A() {
//로컬 클래스 선언
class B { }
//로컬 객체 생성
B b = new B();
}
//메소드
void method() {
//로컬 클래스 선언
class B { }
//로컬 객체 생성
B b = new B();
}
}
로컬 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드선언이 올 수 있다. 정적 필드와 정적 메소드는 Java 17부터 선언이 가능하다.
A.java
public class A {
//메소드
void useB() {
//로컬 클래스
class B {
//인스턴스 필드
int field1 = 1;
//정적 필드(Java17부터 허용)
static int field2 = 2;
//생성자
B() {
System.out.println("B-생성자 실행");
}
//인스턴스 메소드
void method1() {
System.out.println("B-method1 실행");
}
//정적 메소드(Java17부터 허용)
static void method2() {
System.out.println("B-method2 실행");
}
}
//로컬 객체 생성
B b = new B();
//로컬 객체의 인스턴스 필드와 메소드 사용
System.out.println(b.field1);
b.method1();
//로컬 클래스의 정적 필드와 메소드 사용(Java17부터 허용)
System.out.println(B.field2);
B.method2();
}
}
로컬 변수(생성자 또는 메소드의 매개변수 또는 내부에서 선언된 변수)를 로컬 클래스에서 사용할 경우 로컬 변수는 final 특성을 갖게 되므로 값을 읽을 수만 있고 수정할 수 없게 된다. 이것은 로컬 클래스 내부에서 값을 변경하지 못하도록 제한하기 때문이다.
Java8 이후부터는 명시적으로 final 키워드를 붙이지 않아도 되지만, 로컬 변수에 final 키워드를 추가해서 final 변수임을 명확히 할 수도 있다.
[바깥 멤버 접근]
바깥 클래스의 멤버 접근 제한
정적 멤버 클래스는 바깥 객체가 없어도 사용 가능해야 하므로 바깥 클래스의 인스턴스 필드와 인스턴스 메소드는 사용하지 못한다.
<바깥 클래스의 사용 가능한 멤버>
- 멤버 클래스 : 바깥 클래스의 모든 필드와 메소드
- 정적 멤버 클래스 : 바깥 클래스의 정적 필드와 정적 메소드
A.java
public class A {
//A의 인스턴스 필드와 메소드
int field1;
void method1() { }
//A의 정적 필드와 메소드
static int field2;
static void method2() { }
//인스턴스 멤버 클래스
class B {
void method() {
//A의 인스턴스 필드와 메소드 사용
field1 = 10; //(o)
method1(); //(o)
//A의 정적 필드와 메소드 사용
field2 = 10; //(o)
method2(); //(o)
}
}
//정적 멤버 클래스
static class C {
void method() {
//A의 인스턴스 필드와 메소드 사용
//field1 = 10; //(x)
//method1(); //(x)
//A의 정적 필드와 메소드 사용
field2 = 10; //(o)
method2(); //(o)
}
}
}
바깥 클래스의 객체 접근
중첩 클래스 내부에서 중첩 클래스의 객체 → this로 접근
중첩 클래스 내부에서 바깥 클래스의 객체 → 바깥클래스이름.this
[중첩 인터페이스]
중첩 인터페이스의 개념
중첩 인터페이스는 클래스의 멤버로 선언된 인터페이스이다.
인터페이스를 클래스 내부에 선언하는 이유는 해당 클래스와 긴밀한 관계를 맺는 구현 객체를 만들기 위해서이다.
class A {
[public | private] [static] interface B {
// 상수 필드
// 추상 메소드
// 디폴트 메소드
// 정적 메소드
}
}
중첩 인터페이스는 암시적으로 static이므로 static을 생략해도 항상 A 객체 없이 B 인터페이스를 사용할 수 있다.
중첩 인터페이스는 안드로이드와 같은 UI 프로그램에서 이벤트를 처리할 목적으로 많이 활용된다.
예를 들어, 버튼을 클릭했을 때 이벤트를 처리할 객체는 중첩 인터페이스를 구현해서 만든다.
Button.java
public class Button {
//정적 멤버 인터페이스
public static interface ClickListener {
//추상 메소드
void onClick();
}
}
외부에서 접근이 가능하도록 public이면서 Button 객체 없이 사용할 수 있는 static 중첩 인터페이스 ClickListener를 선언했다.
Button.java
public class Button {
//정적 멤버 인터페이스
public static interface ClickListener {
//추상 메소드
void onClick();
}
//필드
private ClickListener clickListener;
//메소드
public void setClickListener(ClickListener clickListener) {
this.clickListener = clickListener;
}
}
Button 클래스에 ClickListener 타입의 필드와 Setter를 추가해서 외부에서 Setter를 통해 ClickListener 구현 객체를 필드에 저장할 수 있도록 하였다.
Button.java
public class Button {
//정적 멤버 인터페이스
public static interface ClickListener {
//추상 메소드
void onClick();
}
//필드
private ClickListener clickListener;
//메소드
public void setClickListener(ClickListener clickListener) {
this.clickListener = clickListener;
}
public void click() {
this.clickListener.onClick();
}
}
ClickListener 인터페이스 필드를 이용해서 ClickListener의 onClick() 추상 메소드를 호출한다.
ButtonExample.java
public class ButtonExample {
public static void main(String[] args) {
//Ok 버튼 객체 생성
Button btnOk = new Button();
//Ok 버튼 클릭 이벤트를 처리할 ClickListener 구현 클래스(로컬 클래스)
class OkListener implements Button.ClickListener {
@Override
public void onClick() {
System.out.println("Ok 버튼을 클릭했습니다.");
}
}
//Ok 버튼 객체에 ClickListener 구현 객체 주입
btnOk.setClickListener(new OkListener());
//Ok 버튼 클릭하기
btnOk.click();
//-------------------------------------------------------------------------------------------
//Cancel 버튼 객체 생성
Button btnCancel = new Button();
//Cancel 버튼 클릭 이벤트를 처리할 ClickListener 구현 클래스(로컬 클래스)
class CancelListener implements Button.ClickListener {
@Override
public void onClick() {
System.out.println("Cancel 버튼을 클릭했습니다.");
}
}
//Cancel 버튼 객체에 ClickListener 구현 객체 주입
btnCancel.setClickListener(new CancelListener());
//Cancel 버튼 클릭하기
btnCancel.click();
}
}
ButtonExample.java (익명 구현 객체 사용 버전)
public class ButtonExample {
public static void main(String[] args) {
//Ok 버튼 객체 생성
Button btnOk = new Button();
//Ok 버튼 객체에 ClickListener 구현 객체 주입
btnOk.setClickListener(new Button.ClickListener() {
@Override
public void onClick() {
System.out.println("Ok 버튼을 클릭했습니다.");
}
});
//Ok 버튼 클릭하기
btnOk.click();
//-------------------------------------------------------------------------------------------
//Cancel 버튼 객체 생성
Button btnCancel = new Button();
//Cancel 버튼 객체에 ClickListener 구현 객체 주입
btnCancel.setClickListener(new Button.ClickListener() {
@Override
public void onClick() {
System.out.println("Cancel 버튼을 클릭했습니다.");
}
});
//Cancel 버튼 클릭하기
btnCancel.click();
}
}
OKListener와 CancelListener는 버튼 이벤트를 처리할 ClickListener 구현 클래스로, 인터페이스의 onClick() 메소드를 재정의해서 버튼이 클릭되었을 때 해야할 일을 나타낸다.
버튼에 어떤 ClickListener 구현 객체(OkListener / CancelListener)가 설정되었느냐에 따라 실행 결과가 달라지므로 다형성을 실현한다.
[익명 객체]
익명 객체의 개념
익명 객체는 이름이 없는 객체를 의미한다.
명시적으로 클래스를 선언하지 않기 때문에 쉽게 객체를 생성할 수 있다는 장점이 있다.
익명 객체는 필드값, 로컬 변수값, 매개변수값으로 주로 사용된다.
익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다.
- 익명 자식 객체 : 클래스를 상속해서 만들 경우
- 익명 구현 객체 : 인터페이스를 구현해서 만들 경우
익명 자식 객체
익명 자식 객체는 부모 클래스를 상속받아 생성된다. 생성된 익명 자식 객체는 부모 타입의 필드, 로컬 변수, 매개변수의 값으로 대입할 수 있다.
new 부모생성자(매개값, ...) {
// 필드
// 메소드
}
중괄호 블록 안에는 주로 부모 메소드를 재정의하는 코드가 온다.
다음 예시는 Tire 클래스의 익명 자식 객체를 생성해서 roll()을 재정의해 실행 내용을 변경하는 예시이다. (다형성 실현)
Tire.java
public class Tire {
public void roll() {
System.out.println("일반 타이어가 굴러갑니다.");
}
}
Car.java (익명 자식 객체 사용)
public class Car {
//필드에 Tire 객체 대입
private Tire tire1 = new Tire();
//필드에 익명 자식 객체 대입
private Tire tire2 = new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체1이 굴러갑니다.");
};
};
//메소드(필드 이용)
public void run1() {
tire1.roll();
tire2.roll();
}
//메소드(로컬변수 이용)
public void run2() {
//로컬 변수에 익명 자식 객체 대입
Tire tire = new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체2가 굴러갑니다.");
}
};
tire.roll();
}
//메소드(매개변수 이용)
public void run3(Tire tire) {
tire.roll();
}
}
CarExample.java
public class CarExample {
public static void main(String[] args) {
//Car 객체 생성
Car car = new Car();
//익명 자식 객체가 대입된 필드 사용
car.run1();
//익명 자식 객체가 대입된 로컬 변수 사용
car.run2();
//익명 자식 객체가 대입된 매개변수 사용
car.run3(new Tire() {
@Override
public void roll() {
System.out.println("익명 자식 Tire 객체3이 굴러갑니다.");
}
});
}
}
*익명 자식 객체를 사용하지 않는 경우에는 Tire를 상속받는 자식 클래스를 생성한 후 roll()를 재정의하여 사용해햐 한다.
CustomTire1.java
public class CustomTire1 extends Tire {
@Override
public void roll() {
System.out.println("Tire1이 굴러갑니다.")
}
}
CustomTire2.java
public class CustomTire2 extends Tire {
@Override
public void roll() {
System.out.println("Tire이 굴러갑니다.")
}
}
Car.java(익명 자식 객체 사용 X)
public class Car {
private Tire tire = new Tire();
private Tire tire1 = new CustomTire1();
private Tire tire2 = new CustomTire2();
public void run1() {
tire.roll();
tire1.roll();
}
public void run2() {
Tire tire2 = new CustomTire2();
tire2.roll();
}
익명 자식 객체가 부모 타입에 대입되면 부모 메소드 roll()을 호출할 경우, 재정의된 익명 자식 객체의 roll() 메소드가 실행되는 것을 볼 수 있다. (다형성 실현)
익명 구현 객체
익명 구현 객체는 인터페이스 타입의 필드, 로컬변수,매개변수의 값으로 대입할 수 있다.
익명 구현 객체는 안드로이드와 같은 UI 프로그램에서 이벤트를 처리하는 객체로 많이 사용된다.
new 인터페이스() {
// 필드
// 메소드
}
중괄호 안에는 주로 인터페이스의 추상 메소드를 재정의하는 코드가 온다.
다음 예시는 RemoteControl 인터페이스의 익명 구현 객체를 생성해서 roll()을 재정의해 실행 내용을 변경하는 예시이다. (다형성 실현)
RemoteControl.java
public interface RemoteControl {
//추상 메소드
void turnOn();
void turnOff();
}
Home.java
public class Home {
//필드에 익명 구현 객체 대입
private RemoteControl rc = new RemoteControl() {
@Override
public void turnOn() {
System.out.println("TV를 켭니다.");
}
@Override
public void turnOff() {
System.out.println("TV를 끕니다.");
}
};
//메소드(필드 이용)
public void use1() {
rc.turnOn();
rc.turnOff();
}
//메소드(로컬변수 이용)
public void use2() {
//로컬 변수에 익명 구현 객체 대입
RemoteControl rc = new RemoteControl() {
@Override
public void turnOn() {
System.out.println("에어컨을 켭니다.");
}
@Override
public void turnOff() {
System.out.println("에이컨을 끕니다.");
}
};
rc.turnOn();
rc.turnOff();
}
//메소드(매개변수 이용)
public void use3(RemoteControl rc) {
rc.turnOn();
rc.turnOff();
}
}
HomeExample.java
public class HomeExample {
public static void main(String[] args) {
//Home 객체 생성
Home home = new Home();
//익명 구현 객체가 대입된 필드 사용
home.use1();
//익명 구현 객체가 대입된 로컬 변수 사용
home.use2();
//익명 구현 객체가 대입된 매개변수 사용
home.use3(new RemoteControl() {
@Override
public void turnOn() {
System.out.println("난방을 켭니다.");
}
@Override
public void turnOff() {
System.out.println("난방을 끕니다.");
}
});
}
}
참고 문헌
신용권, 임경균. (2024). 이것이 자바다 3판 1권