Java 공부 56일차(중첩 클래스, 내부 클래스란2?)2편

임선구·2025년 8월 1일

몸 비틀며 Java

목록 보기
57/58

오늘의 잔디


익명 클래스 - 시작

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

앞서 만들었던 지역 클래스 예제 코드인 LocalOuterV2 코드를 다시 한번 살펴보자.

package nested.local;
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);
 }
 }
 Printer printer = new LocalPrinter();
 printer.print();
 }
 public static void main(String[] args) {
 LocalOuterV2 localOuter = new LocalOuterV2();
 localOuter.process(2);
 }
}

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

지역 클래스의 선언과 생성

//선언
class LocalPrinter implements Printer{
 //body
}
//생성
Printer printer = new LocalPrinter();

익명 클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한번에 처리할 수 있다.

익명 클래스 - 지역 클래스의 선언과 생성을 한번에

Printer printer = new Printer(){
 //body
}

예제 코드를 통해 익명 클래스를 만들어보자.

package nested.anonymous;
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);
 }}
  • 이 코드는 앞서 설명한 LocalOuterV2 와 완전히 같은 코드이다. 여기서는 익명 클래스를 사용했다.

실행 결과

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();

정리

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

익명 클래스 활용1

익명 클래스가 어떻게 활용되는지 알아보자.
익명 클래스에 들어가기 전에 먼저 간단한 문제를 하나 풀어보자.

리팩토링 전

package nested.anonymous.ex;
public class Ex0Main {
 public static void helloJava() {
 System.out.println("프로그램 시작");
 System.out.println("Hello Java");
 System.out.println("프로그램 종료");
 }
 public static void helloSpring() {
 System.out.println("프로그램 시작");
 System.out.println("Hello Spring");
 System.out.println("프로그램 종료");
 }
 public static void main(String[] args) {
 helloJava(); helloSpring();
 }
}

실행 결과

프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료

이 코드의 중복이 보일 것이다. 코드를 리팩토링해서 코드의 중복을 제거해보자.
helloJava() , helloSpring() 메서드를 하나로 통합하면 된다.

리팩토링 후

package nested.anonymous.ex;
public class Ex0RefMain {
 public static void hello(String str) {
 System.out.println("프로그램 시작");
 System.out.println(str);
 System.out.println("프로그램 종료");
 }
 public static void main(String[] args) {
 hello("hello Java");
 hello("hello Spring");
 }
}

실행 결과

hello Java
프로그램 종료
프로그램 시작
hello Spring
프로그램 종료

코드를 분석해보자.

기존 코드에서 변하는 부분과 변하지 않는 부분을 분리해야 한다.

public static void helloJava() {
 System.out.println("프로그램 시작"); //변하지 않는 부분
 System.out.println("Hello Java"); //변하는 부분
 System.out.println("프로그램 종료"); //변하지 않는 부분
}
public static void helloSpring() {
 System.out.println("프로그램 시작"); //변하지 않는 부분
 System.out.println("Hello Spring"); //변하는 부분
 System.out.println("프로그램 종료"); //변하지 않는 부분
}

여기서 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것이다.
변하는 부분은 그대로 유지하고 변하지 않는 부분을 어떻게 해결할 것 인가에 집중하면 된다.

이해를 돕기 위해 다음과 같은 코드를 먼저 작성해보자.

public static void hello() {
 System.out.println("프로그램 시작"); //변하지 않는 부분
 //변하는 부분 시작
 System.out.println("Hello Java");
 System.out.println("Hello Spring");
 //변하는 부분 종료
 System.out.println("프로그램 종료"); //변하지 않는 부분
}

여기서 "Hello Java" , "Hello Spring" 와 같은 문자열은 상황에 따라서 변한다.

여기서는 상황에 따라 변하는 문자열 데이터를 다음과 같이 외부에서 전달 받아서 출력하면 된다.

public static void hello(String str) {
 System.out.println("프로그램 시작"); //변하지 않는 부분
 System.out.println(str); //str: 변하는 부분
 System.out.println("프로그램 종료"); //변하지 않는 부분
}
  • 변하지 않는 부분은 그대로 유지하고, 변하는 문자열은 외부에서 전달 받아서 처리한다

단순한 문제였지만 프로그래밍에서 중복을 제거하고, 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을
분리하는 것이다. 여기서는 변하지 않는 "프로그램 시작", "프로그램 종료"를 출력하는 부분은 그대로 유지하고, 상황에 따라 변화가 필요한 문자열은 외부에서 전달 받아서 처리했다.
이렇게 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 외부에서 전달 받으면, 메서드(함수)의 재사용성을 높일 수 있다.
리팩토링 전과 후를 비교해보자. hello(String str) 함수의 재사용성은 매우 높아졌다.
여기서 핵심은 변하는 부분을 메서드(함수) 내부에서 가지고 있는 것이 아니라, 외부에서 전달 받는다는 점이다.

익명 클래스 활용2

이번에는 비슷한 다른 문제를 한번 풀어보자.

리팩토링 전

package nested.anonymous.ex;
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 //코드 조각 종료
 System.out.println("프로그램 종료");
 }
 public static void main(String[] args) {
 helloDice();
 helloSum();
 }
}

실행 결과

프로그램 시작
주사위 = 5 //랜덤
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료

이 코드를 앞에서 리팩토링 한 예와 같이 하나의 메서드에서 실행할 수 있도록 리팩토링 해보자.
참고로 앞의 문제는 변하는 문자열을 외부에서 전달하면 되었다. 이번에는 문자열 같은 데이터가 아니라 코드 조각을 전달해야 한다.

리팩토링 후

package nested.anonymous.ex;
public interface Process {
 void run();
}
package nested.anonymous.ex;
import java.util.Random;
//정적 중첩 클래스 사용
public class Ex1RefMainV1 {
 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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 } }
 }
 public static void main(String[] args) {
 Process dice = new Dice();
 Process sum = new Sum();
 System.out.println("Hello 실행");
 hello(dice);
 hello(sum);
 }
}

코드를 분석해보자.

여기서는 단순히 데이터를 전달하는 수준을 넘어서, 코드 조각을 전달해야 한다.

리펙토링 전

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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 //코드 조각 종료
 System.out.println("프로그램 종료"); //변하지 않는 부분
}

리펙토링 진행 단계

public static void hello() {
 System.out.println("프로그램 시작"); //변하지 않는 부분
 //코드 조각 시작
 int randomValue = new Random().nextInt(6) + 1;
 System.out.println("주사위 = " + randomValue);
 //코드 조각 종료
 //코드 조각 시작
 for (int i = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 //코드 조각 종료
 System.out.println("프로그램 종료"); //변하지 않는 부분
}
  • 프로그램 시작, 프로그램 종료를 출력하는 부분은 변하지 않는 부분이다.
  • 코드 조각을 시작하고 종료하는 부분은 변하는 부분이다.
  • 결국 코드 조각을 시작하고 종료하는 부분을 외부에서 전달 받아야 한다. 이것은 단순히 문자열 같은 데이터를 전달 받는 것과는 차원이 다른 문제이다.

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

이 문제를 해결하기 위해 인터페이스를 정의하고 구현 클래스를 만들었다.

public interface Process {
 void run();
}
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 }
}
  • Dice , Sum 각각의 클래스는 Process 인터페이스를 구현하고 run() 메서드에 필요한 코드 조각을 구현한다.
  • 여기서는 정적 중첩 클래스를 사용했다. 물론 정적 중첩 클래스가 아니라 외부에 클래스를 직접 만들어도 된다.

리팩토링 완료

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

실행 결과

Hello 실행
프로그램 시작
주사위 = 5 //랜덤
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료

정리

  • 문자열 같은 데이터를 메서드에 전달할 때는 String , int 와 같은 각 데이터에 맞는 타입을 전달하면 된다.
  • 코드 조각을 메서드에 전달할 때는 인스턴스를 전달하고 해당 인스턴스에 있는 메서드를 호출하면 된다.

익명 클래스 활용3

이번에는 지역 클래스를 사용해서 같은 기능을 구현해보자

지역 클래스 사용

package nested.anonymous.ex;
import java.util.Random;
//지역 클래스 사용
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 }
 }
 Process dice = new Dice();
 Process sum = new Sum();
 System.out.println("Hello 실행");
 hello(dice);
 hello(sum);
 }
}

실행 결과는 기존과 같다. 이해하는데 어려움은 없을 것이다.

익명 클래스 사용1

앞의 지역 클래스는 간단히 한번만 생성해서 사용한다. 이런 경우 익명 클래스로 변경할 수 있다.

package nested.anonymous.ex;import java.util.Random;
//익명 클래스 사용
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 }
 };
 System.out.println("Hello 실행");
 hello(dice);
 hello(sum);
 }
}

실행 결과는 기존과 같다.

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

이 경우 익명 클래스의 참조값을 변수에 담아둘 필요 없이, 인수로 바로 전달할 수 있다.

import java.util.Random;
//익명 클래스 참조 바로 전달
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 }
 });
 }
}

실행 결과는 기존과 같다.

람다(lambda)

자바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)라 한다.

리팩토링 - 람다

package nested.anonymous.ex;
import java.util.Random;
//람다 사용
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 = 1; i <= 3; i++) {
 System.out.println("i = " + i);
 }
 });
 }
}
  • 코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드(더 정확히는 함수)의 코드 블럭을 직접 전달하는 것을 확
    인할 수 있다.

람다에 대한 자세한 내용은 이후에 별도로 다룬다. 지금은 이런 것이 있구나 정도만 알아두자.

정리

  • 정적 중첩 클래스: 바깥 클래스와 밀접한 관련이 있지만, 인스턴스 간에 데이터 공유가 필요 없을 때 사용한다.
  • 내부 클래스: 바깥 클래스의 인스턴스와 연결되어 있고, 바깥 클래스의 인스턴스 상태에 의존하거나 강하게 연관
    된 작업을 수행할 때 사용한다.
  • 지역 클래스:
    • 내부 클래스의 특징을 가진다.
    • 지역 변수에 접근할 수 있다. 접근하는 지역 변수는 final 이거나 사실상 final
      업로드중..
      이어야 한다.
    • 주로 특정 메서 드 내에서만 간단히 사용할 목적으로 사용한다.
  • 익명 클래스:
    • 지역 클래스인데, 이름이 없다.
    • 상위 타입을 상속 또는 구현하면서 바로 생성된다.
    • 주로 특정 상위 타입을 간단히 구현해서 일회성으로 사용할 때 유용하다.
profile
끝까지 가면 내가 다 이겨

2개의 댓글

comment-user-thumbnail
2025년 9월 22일

이분 코딩 접으셨나요? 코딩 접으신거죠?

1개의 답글