Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 9회차 본문

Java

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 9회차

개발곰발 2024. 12. 18.
728x90

 

인프런 강의 <김영한의 실전 자바 - 중급 1편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


 

섹션 8, 섹션 9는 모두 중첩 클래스, 내부 클래스에 대한 내용이다. 우선 중첩(Nested)과 내부(Inner)라는 단어의 뜻과 그 차이에 대해 알아보자.

  • 중첩(Nested) : 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계. 나의 안에 있지만 내 것이 아닌 것.
  • 내부(Inner) : 나의 내부에 있는 나를 구성하는 요소. 나의 내부에서 나를 구성하는 요소

강의에 나온 예시를 chatGPT에게 그려달라고 해보았다. 큰 상자 안에 전혀 다른 상자를 넣은 것은 중첩(Nested)이라고 한다. 사람의 심장은 내부(Inner)에서 사람을 구성하는 요소이다.

중첩(왼) / 내부(오)

📘중첩 클래스

중첩 클래스는 static과 non-static으로 나눌 수 있다. static은 정적 중첩 클래스, non-static은 내부 클래스(내부 클래스, 지역 클래스, 익명 클래스)를 의미한다. 정리하자면 다음과 같이 분류된다.

  • 중첩 클래스
    • 정적 중첩 클래스 : 정적 변수(클래스 변수, static 변수)와 같은 위치에 선언, static 키워드 사용
    • 내부 클래스
      • 내부 클래스 : 인스턴스 변수와 같은 위치에 선언, static 키워드 사용하지 않음, 바깥 클래스의 인스턴스 멤버에 접근
      • 지역 클래스 : 지역 변수와 같은 위치에 선언, 코드 블럭 안에서 클래스 정의, 내부 클래스의 특징 + 지역 변수에 접근
      • 익명 클래스 : 이름이 없는 클래스로, 지역 클래스의 특별한 버전

✔️ 중첩 클래스의 선언 위치

  • 정적 중첩 클래스  →  정적 변수(=static 변수, 클래스 변수)와 같은 위치
  • 내부 클래스  →  인스턴스 변수와 같은 위치
  • 지역 클래스  →  지역 변수와 같은 위치
더보기

⭐ 지역 변수, 인스턴스 변수, 클래스 변수 복습!

 

지역 변수(매개변수 포함) : 스택 영역의 스택 프레임 안에 보관되는 변수. 클래스에 바로 접근해 사용 가능, 자바 프로그램을 시작할 때 딱 1개가 만들어지며 여러곳에서 공유하는 목적으로 사용된다. 메서드 종료 시 스택 프레임이 제거되면서 그 안의 지역 변수도 함께 제거되므로 생존 주기가 짧다.
인스턴스 변수 : 힙 영역의 인스턴스에 있는 멤버 변수. 인스턴스를 생성해야 사용 가능, 인스턴스를 새로 만들 때 마다 새로 만들어진다. GC(가비지 컬렉션)가 발생하면 제거되므로 보통 지역 변수보다는 생존 주기가 길다.
- 클래스 변수 : 메서드 영역의 static 영역에 보관되는 변수. 해당 클래스가 JMV에 로딩되는 순간 생성되고 JVM이 종료될 때 제거되므로 생명 주기가 가장 길다.

✔️ 정적 중첩 클래스와 내부 클래스의 차이?

  • 맨 처음 설명한 중첩과 내부의 단어 차이에서 보이듯, 정적 중첩 클래스는 바깥 클래스의 안에 있지만 바깥 클래스와 관계 없는 전혀 다른 클래스를 말한다. 내부 클래스는 바깥 클래스의 내부에 있으면서 바깥 클래스를 구성하는 요소를 말한다.

즉, 바깥 클래스 입장에서 볼 때 안에 있는 클래스가 나의 인스턴스에 소속된다면 내부 클래스, 소속되지 않는다면 정적 중첩 클래스로 분류할 수 있다!

 

✔️ 중첩 클래스는 언제 사용할까?

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

✔️ 중첩 클래스를 사용하는 이유

  • 논리적 그룹화 : 어떤 클래스 안에서만 사용되는 클래스라면 그 클래스 안에 위치시키는 것이 논리적으로 더 그룹화 된다.
  • 캡슐화 : 중첩 클래스는 바깥 클래스의 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(NestedOuter.outClassValue);
        }
    }
}

 

나의 클래스에 포함된 중첩 클래스가 아니라 다른 곳에 있는 중첩 클래스에 접근할 때 바깥 클래스 인스턴스를 생성할 필요는 없다. new 바깥클래스.중첩클래스() 로 생성한다. 아래 코드는 예시라서 이렇게 적었지만, 중첩 클래스의 용도는 원래 자신이 소속된 바깥 클래스 안에서 사용되는 것이기 때문에 외부에서 사용하고 있다면 중첩 클래스의 용도에 맞지 않다. 그럴 때는 중첩 클래스를 밖으로 빼는 것이 좋다.

public class NestedOuterMain {
    public static void main(String[] args) {
        NestedOuter outer = new NestedOuter(); // 필요X
        NestedOuter.Nested nested = new NestedOuter.Nested();
        nested.print();
        
        System.out.println("nestedClass = " + nested.getClass());
    }
}

 

✔️ 정적 중첩 클래스의 특징

  • static이 붙는다.
  • 바깥 클래스의 인스턴스에 소속되지 않는다.
  • 바깥 클래스의 private 접근제어자에 접근할 수 있다.

📘내부 클래스

내부 클래스란?

내부 클래스란 바깥 클래스의 인스턴스와 연결되어 있고, 바깥 클래스의 인스턴스 상태에 의존하거나 강하게 연관된 작업을 수행할 때 사용한다. 개념상 바깥 클래스(InnerOuter)의 인스턴스 내부에서 내부 클래스(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);
        }
    }
}

 

내부 클래스는 바깥 클래스의 인스턴스에 소속되기 때문에 바깥 클래스의 인스턴스 정보를 알아야 한다. 따라서 바깥 클래스의 인스턴스를 먼저 생성한 뒤, [바깥클래스의 인스턴스 참조.new 내부클래스()]로 조금 특이한 방식으로 내부 클래스를 생성한다.

public class InnerOuterMain {

    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter(); 
        InnerOuter.Inner inner = outer.new Inner(); 
        inner.print();
    }
}

 

✔️ 내부 클래스의 특징

  • static이 붙지 않는다.
  • 바깥 클래스의 인스턴스에 소속된다.
  • 자신의 멤버뿐만 아니라 바깥 클래스의 인스턴스 멤버, 바깥 클래스의 클래스 멤버에 접근할 수 있다.
  • 바깥 클래스의 private 접근 제어자에 접근할 수 있다.

같은 이름의 바깥 변수 접근

바깥 클래스의 인스턴스 변수 이름과 내부 클래스의 인스턴스 변수 이름이 같다면, 더 가깝거나 구체적인 것이 우선권을 가진다. 다음과 같이 go() 메서드에서 지역변수인 value가 가장 가깝기 때문에 우선순위가 높다. 이렇게 다른 변수들을 가려서 보이지 않게 하는 것을 섀도잉(Shadowing)이라고 한다.

public class ShadowingMain {

    public int vlaue = 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.value = " + ShadowingMain.this.vlaue);
        }
    }

    public static void main(String[] args) {
        ShadowingMain main = new ShadowingMain();
        Inner inner = main.new Inner();
        inner.go();
    }
}

 

value, this.value, ShadowingMain.value로 각각 접근할 수 있지만, 처음부터 이름을 서로 다르게 지어 구분하는 것이 낫다.

value = 3
this.value = 2
ShadowingMain.value = 1

📘지역 클래스

지역 클래스란?

지역 클래스란 코드 블럭 안에서 정의되는 클래스로, 내부 클래스의 특별한 종류 중 하나이다.

 

✔️ 지역 클래스의 특징

  • 지역 변수처럼 코드 블럭 안에 클래스를 선언한다.
  • 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
  • 지역 변수에 접근할 수 있다.
  • 매개 변수에 접근할 수 있다.(매개 변수도 지역 변수의 한 종류)
  • 지역 변수처럼 접근 제어자를 사용할 수 없다.

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

지역 변수 캡쳐 내용에서 기억해야 할 것은 "지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다." 라는 것이다.

 

✔️ 지역 변수 캡쳐란?

지역 변수의 생명 주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다. 만약 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근하려 할 때, 인스턴스는 살아있지만 지역 변수는 이미 제거된 상태일 수 있다. 따라서 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사한다. 이런 과정을 변수 캡처(Capture)라 한다.

 

아래 코드를 보자. localVar, paramVar와 같은 지역 변수는 process() 메서드를 실행하는 동안에만 스택 영역에서 생존하고, 스택 프레임이 제거되는 순간 제거된다. 그러나 process() 메서드가 종료된 이후에도 localVar , paramVar 와 같은 지역 변수의 값들이 모두 정상적으로 출력된다. 이렇게 출력되는 것이 조금 이상해 보이지만, 이건 지역 변수 캡쳐로 인한 결과이다.

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

        LocalPrinter 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();
    }
}
value=0
localVar=1
paramVar=2
outInstanceVar=3

 

✔️ 지역 클래스가 접근하는 지역 변수는 사실상 final

사실상 final(effectively final) 지역 변수는 final 키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻한다.  지역 변순은 중간에 값이 변하면 안되고, final 혹은 사실상 final이어야 한다. 만약 값이 바뀌게 되면스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡쳐한 캡쳐변수의 값이 서로 달라지는 문제가 발생한다. 이는 여러 복잡한 문제들을 발생시킬 수 있기 때문에 자바는 이런 문제들을 근본적으로 차단하기 위해 캡쳐 변수의 값을 변경하지 못하게 한다.

📘익명 클래스

익명 클래스란?

익명 클래스란 이름이 없는 지역 클래스로, 상위 타입을 상속 또는 구현하면서 바로 생성된다. 주로 특정 상위 타입을 간단히 구현해서 일회성으로 사용할 때 유용하다.

 

✔️ 익명 클래스의 특징

  • 이름 없는 클래스를 선언하는 동시에 생성한다.
  • 부모 클래스를 상속받거나 인터페이스를 구현해야 한다.
  • 이름이 없으므로 기본 생성자만 사용된다.
  • 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의된다.
  • 클래스를 별도로 정의하지 않기 때문에 코드가 간결해진다.
  • 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.

 

익명 클래스가 사용되는 예시를 살펴보자. 인수로 Process를 받는 hello() 메서드는 다형성을 활용해 외부에서 어떤 인스턴스가 들어오느냐에 따라 결과가 달라진다. Process 인터페이스를 구현한 Dice, Sum 클래스를 직접 만들어 인스턴스를 생성해 전달할 수도 있겠지만, 익명 클래스를 사용해 더 간결하게 전달할 수 있다.

public interface Process {
    void run();
}

 

익명 클래스의 참조값을 변수에 담아두지 않고 인수로 바로 전달할 수 있는데, 자바 8부터는 람다(Lambda)를 이용해 메서드(함수)를 인수로 전달할 수 있게 되었다.

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 = 0; i < 3; i++) {
                    System.out.println("i = " + i);
                }
            }
        });
    }
}

 

다음과 같이 람다를 사용한다.

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 = 0; i < 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}

 

강의에서는 "람다가 메서드(더 정확히는 함수)의 코드 블럭을 직접 전달한다"고 나와있는데, <더 정확히는 함수>라고 설명한 이유가 궁금해 더 찾아보았다. 함수와 메서드에 대한 개념은 다음과 같다.

  • 메서드 : 자바에서 클래스나 객체에 속해 있는 "동작"
  • 함수 : 특정 입력을 받아 출력을 반환하는 독립적인 "코드 조각"

자바는 객체 지향 언어이기 때문에 클래스 안에 모든 코드를 넣어야 했고, 원래 순순한 함수라는 개념이 없었다. 하지만 자바 8부터 람다(Lambda)가 등장하면서 독립적인 함수를 코드에 전달할 수 있게 되었다 즉 람다는 클래스나 객체 없이도 독립적인 동작을 표현할 수 있는 익명 함수라고 보면 되는 것!

 

예를 들면 이렇게 클래스에 속하지 않은 독립적인 함수(x -> x * x)를 변수에 넣거나 전달할 수 있다.

public class Main {
    public static void main(String[] args) {
        // 람다 표현식: 입력 x를 받아 x * x를 반환하는 함수
        Function<Integer, Integer> square = x -> x * x;

        System.out.println(square.apply(5)); // 출력: 25
    }
}

 

따라서, 강의에서 설명한 코드 조각을 함수라고 보면 되는구나!라고 이해했다.

📘 출처 - 김영한의 실전 자바 - 중급 1편