10일차 - 어노테이션, 모듈

은채의 성장통·2025년 6월 11일

KCC정보통신

목록 보기
14/30

노션 정리 링크

3. 어노테이션

어노테이션(Annotations)은 메타데이터를 코드에 추가하여 컴파일러나 런타임 환경에서 활용할 수 있도록 하는 Java의 기능입니다. 주석과는 다르게, 프로그램의 동작에 영향을 줄 수 있습니다.

어노테이션의 주요 역할

  • 컴파일 타임 정보 제공: 컴파일러가 특정 처리를 하도록 지시할 수 있음 (예: @Deprecated).
  • 런타임 처리 지원: 실행 중에 정보를 읽고 활용 가능 (예: @Retention(RetentionPolicy.RUNTIME)).
  • 개발 도구 지원: 프레임워크와 라이브러리에서 코드를 분석하거나 자동 생성하는 데 사용됨 (예: JUnit의 @Test).

3.1 어노테이션 정의(어노테이션 생성하는 사람에게 필요함)

어노테이션을 정의할 때는 @interface 키워드를 사용합니다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 어노테이션 정의
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 유지됨
@Target(ElementType.CLASS) // 메서드에만 적용 가능
public @interface MyAnnotation {
    String value() default "";  // 기본값 설정 가능
}

위 코드에서:

  • @Retention(RetentionPolicy.RUNTIME): 실행 중에도 어노테이션 정보를 유지합니다.
  • @Target(ElementType.METHOD): 메서드에서만 사용할 수 있도록 제한합니다.

어노테이션 사용 예제(우린 주로 사용한다)

어노테이션을 선언한 후, 클래스 또는 메서드에 적용할 수 있습니다.

@MyAnnotation(value = "Hello") //myclass에 어노테이션을 적용한것
public class MyClass {
    @MyAnnotation(value = "Test Method") //myMethod에 어노테이션을 적용한것
    public void myMethod() {
        // 메서드 내용
    }
}

어노테이션을 적용할 때는 @어노테이션이름(요소 값) 형태로 사용하며, 요소 값은 생략할 수도 있습니다.


3.1.1 리플렉션을 활용한 어노테이션 값 읽기(이건 생성하는 사람에게 필요함)

리플렉션(Reflection)을 사용하면 런타임에 어노테이션 정보를 추출할 수 있습니다.

import java.lang.reflect.Method;

public class MyAnnotationExample {
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MyClass.class.getMethod("myMethod");
        MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
        String value = annotation.value();

        System.out.println("@MyAnnotation value: " + value);
    }
}

위 코드에서는:

  1. Method 객체를 통해 myMethod()를 가져옵니다.
  2. getAnnotation(MyAnnotation.class)을 호출하여 어노테이션 정보를 읽습니다.
  3. value() 메서드를 사용해 저장된 값을 출력합니다.

3.2 어노테이션 유지 정책

어노테이션 유지 정책(Retention Policy)은 어노테이션이 코드의 어느 단계까지 유지될지를 결정하는 설정입니다. 유지 범위에 따라 소스 코드, 클래스 파일, 런타임 중 하나를 선택할 수 있습니다.

  • 어노테이션 유지 정책 종류
유지 정책설명
SOURCE소스 코드에서만 유지. 컴파일 후 어노테이션 정보가 제거됨.
CLASS클래스 파일까지 유지되지만, 런타임에서는 제거됨.
RUNTIME런타임까지 유지되며, 실행 중에 리플렉션(reflection)으로 접근 가능.
  • SOURCE 유지 정책 예제
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE) // 소스 코드에서만 유지됨
@interface MyAnnotation1 {
    String value();
}

컴파일 후 어노테이션 정보가 사라지며, 주로 코드 분석 및 문서화에 활용됩니다.

  • CLASS 유지 정책 예제
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.CLASS) // 클래스 파일까지만 유지됨
@interface MyAnnotation2 {
    String value();
}

클래스 파일에는 어노테이션 정보가 포함되지만 런타임에서는 접근할 수 없습니다.

  • RUNTIME 유지 정책 예제
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지됨
@interface MyAnnotation3 {
    String value();
}

리플렉션을 사용해 실행 중에도 어노테이션 정보를 읽을 수 있으며, 프레임워크나 라이브러리에서 활용됩니다.

  • RUNTIME 어노테이션 활용 예제
import java.lang.reflect.Method;

public class AnnotationExample {
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MyClass.class.getMethod("myMethod");
        MyAnnotation3 annotation = method.getAnnotation(MyAnnotation3.class);
        String value = annotation.value();

        System.out.println("@MyAnnotation3 value: " + value);
    }
}

이 코드는 런타임에 getAnnotation()을 사용해 어노테이션 정보를 가져오는 방법을 보여줍니다.

정리

  • SOURCE: 소스 코드에서만 유지 → 컴파일 후 사라짐
  • CLASS: 클래스 파일까지 유지 → 런타임 접근 불가
  • RUNTIME: 런타임까지 유지 → 실행 중 정보 조회 가능

4. 내포된 클래스 및 인터페이스

자바에서는 클래스 내부에 또 다른 클래스를 정의하여 사용할 수 있으며, 이를 내부 클래스라고 합니다. 내부 클래스는 코드의 구조를 명확하게 만들고 특정 클래스와 강하게 연관된 기능을 정의할 때 유용합니다.


4.1. 멤버 내부 클래스 (Member Inner Class)

멤버 내부 클래스는 외부 클래스의 멤버 변수처럼 동작하며, 외부 클래스의 필드와 메서드에 접근할 수 있습니다.

import java.awt.*;
import java.awt.event.*;

public class InnerExample {
    private Frame f;

    public InnerExample() {
        f = new Frame("Inner 클래스 예제");
    }

    public void launchFrame() {
        // 멤버 내부 클래스의 인스턴스를 생성하여 이벤트 핸들러로 등록
        f.addWindowListener(new MyWindowAdapter());
        f.setSize(300, 200);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        InnerExample ie = new InnerExample();
        ie.launchFrame();
    }

    // 멤버 내부 클래스: WindowAdapter를 상속받아 창 닫기 이벤트 처리
    private class MyWindowAdapter extends WindowAdapter {
        public void windowClosing(WindowEvent e) {
            System.exit(0);
        }
    }
}

컴파일 결과: InnerExample$MyWindowAdapter.class 파일이 생성됨.


4.2. 지역 내부 클래스 (Local Inner Class)

지역 내부 클래스는 메서드 또는 블록 내부에서만 선언되고 사용되는 클래스입니다. 접근 제한자가 필요 없으며, 특정 메서드 내부에서만 유효합니다.

import java.awt.*;
import java.awt.event.*;

public class LocalInnerExample {
    private Frame f;

    public LocalInnerExample() {
        f = new Frame("Local Class 예제");
    }

    public void launchFrame() {
        // 메서드 내부에서 정의된 지역 내부 클래스
        class MyWindowAdapter extends WindowAdapter {
            public void windowClosing(WindowEvent we) {
                System.exit(0);
            }
        }

        f.addWindowListener(new MyWindowAdapter());
        f.setSize(300, 200);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        LocalInnerExample obj = new LocalInnerExample();
        obj.launchFrame();
    }
}

컴파일 결과: LocalInnerExample$1MyWindowAdapter.class 파일이 생성됨.

지역 내부 클래스는 메서드 내에서 선언되므로 다른 곳에서는 접근할 수 없음.


4.3. 익명 내부 클래스 (Anonymous Inner Class)

익명 내부 클래스는 이름이 없는 지역 내부 클래스이며, 주로 인터페이스 또는 추상 클래스를 구현할 때 사용됩니다.

import java.awt.*;
import java.awt.event.*;

public class AnonymousExample {
    private Frame f;

    public AnonymousExample() {
        f = new Frame("Anonymous Class 예제");
    }

    public void launchFrame() {
        // 익명 내부 클래스: WindowAdapter를 상속받아 즉시 인스턴스 생성
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                System.exit(0);
            }
        });

        f.setSize(300, 200);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        AnonymousExample ae = new AnonymousExample();
        ae.launchFrame();
    }
}

컴파일 결과: AnonymousExample$1.class 파일이 생성됨.

익명 내부 클래스는 한 번만 사용되므로 별도의 이름이 필요 없음.


4.4. 내부 인터페이스와 익명 클래스

클래스 내부에 인터페이스를 정의하여 특정 목적에 맞게 사용할 수 있습니다. 다음은 버튼 클릭 이벤트를 처리하는 내부 인터페이스와 익명 클래스를 사용하는 예제입니다.

내부 인터페이스 정의

public class Button {
    private String label;

    public Button(String label) {
        this.label = label;
    }

    // 클릭 이벤트 리스너 객체를 저장할 변수
    private OnClickListener onClickListener;

    // 클릭 이벤트 리스너 객체를 설정하는 메서드
    public void setOnClickListener(OnClickListener listener) {
        this.onClickListener = listener;
    }

    // 버튼 클릭 시 실행할 메서드
    public void actionClick() {
        System.out.println(label + " 버튼의 이벤트를 처리합니다.");
        onClickListener.onClick();
    }

    // 내부 인터페이스: 클릭 이벤트 리스너
    public interface OnClickListener {
        void onClick();
    }
}

내부 인터페이스 구현 예제

public class ButtonActionExample {
    public static void main(String[] args) {
        Button btn1 = new Button("Red");

        // 내부 인터페이스 구현 후 객체 생성
        class ButtonClickListener implements Button.OnClickListener {
            @Override
            public void onClick() {
                System.out.println("버튼을 클릭했습니다.");
            }
        }

        ButtonClickListener listener = new ButtonClickListener();
        btn1.setOnClickListener(listener);
        btn1.actionClick();
    }
}

내부 인터페이스를 구현한 클래스를 명시적으로 정의한 후 사용.


익명 클래스로 내부 인터페이스 구현

public class ButtonActionExample2 {
    public static void main(String[] args) {
        Button btn1 = new Button("Red");

        // 익명 내부 클래스로 인터페이스 구현
        Button.OnClickListener listener = new Button.OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("버튼을 클릭했습니다.");
            }
        };

        btn1.setOnClickListener(listener);
        btn1.actionClick();
    }
}

한 번만 사용할 인터페이스 구현체는 익명 내부 클래스로 작성 가능.


익명 클래스를 직접 인수로 전달

public class ButtonActionExample3 {
    public static void main(String[] args) {
        Button btn1 = new Button("Red");

        // setOnClickListener() 메서드 호출 시 익명 클래스 전달
        btn1.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("버튼을 클릭했습니다.");
            }
        });

        btn1.actionClick();
    }
}

익명 내부 클래스를 직접 메서드 호출 인수로 전달 가능.


정리

  1. 멤버 내부 클래스 → 외부 클래스의 멤버처럼 동작하며 접근 가능.
  2. 지역 내부 클래스 → 특정 메서드 내부에서만 유효하며, 블록의 범위를 초과하지 않음.
  3. 익명 내부 클래스 → 한 번만 사용할 클래스를 정의할 때 활용.
  4. 내부 인터페이스 → 특정 기능을 외부에서 설정할 수 있도록 인터페이스를 정의하고 구현 가능.


4. 내부 클래스 사용 및 참조 방법

자바에서 내부 클래스는 외부 클래스의 멤버를 참조할 때 특별한 제한이 없지만, 내부 클래스를 생성하는 방식은 참조 위치에 따라 다릅니다. 내부 클래스의 생성과 참조 방법을 정리하겠습니다.


  • 내부 클래스의 기본 사용

내부 클래스는 외부 클래스 내부에서 정의된 클래스입니다. 내부 클래스에서는 외부 클래스의 멤버에 자유롭게 접근할 수 있습니다.

예제: 내부 클래스의 기본 사용

public class Outer1 {
    private int data;  // 외부 클래스의 멤버 변수

    // 멤버 내부 클래스 정의
    public class Inner1 {
        public void doIt() {
            data++;  // 내부 클래스에서 외부 클래스의 멤버 변수 접근 가능
            System.out.println("Inner 클래스의 메서드 호출됨");
            System.out.println("data 값은 " + data);
        }
    }

    // 내부 클래스 인스턴스를 생성하고 사용하는 메서드
    public void testTheInner() {
        Inner1 in = new Inner1();
        in.doIt();
    }

    // 실행 메서드
    public static void main(String[] args) {
        Outer1 out = new Outer1();
        out.testTheInner();
    }
}

내부 클래스 Inner1은 외부 클래스의 멤버 변수 data에 접근 가능

외부 클래스 내부에서 Inner1의 객체를 생성할 때 일반적인 방식으로 생성 가능

컴파일 결과: Outer1$Inner1.class 파일이 생성됨


  • 2. 다른 클래스에서 내부 클래스 참조

내부 클래스를 외부 클래스의 인스턴스를 통해 생성해야 합니다.

예제: 내부 클래스를 다른 클래스에서 생성

public class Outer2 {
    private int data;

    // 내부 클래스 정의
    public class Inner2 {
        public void doIt() {
            data++;  // 내부 클래스에서 외부 클래스의 멤버 변수 접근 가능
            System.out.println("Inner 클래스의 메서드 호출됨");
            System.out.println("data 값은 " + data);
        }
    }
}

✔ 내부 클래스는 외부 클래스 내부에서 생성하는 것이 일반적이지만, 다른 클래스에서도 생성할 수 있습니다.

public class InnerExample2 {
    public static void main(String[] args) {
        Outer2 out = new Outer2();  // 외부 클래스 객체 생성
        Outer2.Inner2 in = out.new Inner2();  // 외부 클래스 인스턴스를 통해 내부 클래스 객체 생성
        in.doIt();
    }
}

내부 클래스를 외부에서 생성하려면 out.new Inner2(); 형태로 생성해야 함

컴파일 결과: Outer2$Inner2.class 파일이 생성됨


  • 3. static 내부 클래스 (정적 내부 클래스)

내부 클래스가 static으로 선언될 경우 객체 생성 방식이 달라집니다.

public class OuterStatic {
    public static class InnerStatic {
        public void showMessage() {
            System.out.println("Static 내부 클래스 호출됨");
        }
    }

    public static void main(String[] args) {
        OuterStatic.InnerStatic in = new OuterStatic.InnerStatic(); // 정적 내부 클래스 생성
        in.showMessage();
    }
}

static 내부 클래스는 외부 클래스의 인스턴스를 생성하지 않고 직접 호출 가능

OuterStatic.InnerStatic in = new OuterStatic.InnerStatic(); 형태로 객체 생성

컴파일 결과: OuterStatic$InnerStatic.class 파일이 생성됨


  • 4. 내부 클래스에서 변수 참조

내부 클래스의 변수 참조는 우선순위에 따라 결정됩니다.

참조 우선순위설명
지역 변수내부 클래스의 메서드 내에서 선언된 변수
내부 클래스의 멤버 변수내부 클래스의 인스턴스 변수
외부 클래스의 멤버 변수외부 클래스의 인스턴스 변수
  • 예제: 내부 클래스에서 변수 참조
public class Outer3 {
    private int data = 10;  // 외부 클래스의 멤버 변수

    public class Inner3 {
        private int data = 20;  // 내부 클래스의 멤버 변수

        public void doIt(int data) {  // 메서드 내 지역 변수
            System.out.println("지역 변수: " + data);  // 지역 변수 참조
            System.out.println("내부 클래스 멤버 변수: " + this.data);  // 내부 클래스 멤버 변수 참조
            System.out.println("외부 클래스 멤버 변수: " + Outer3.this.data);  // 외부 클래스 멤버 변수 참조
        }
    }
}

this.data → 내부 클래스의 멤버 변수를 참조

Outer3.this.data → 외부 클래스의 멤버 변수를 참조

public class InnerExample3 {
    public static void main(String[] args) {
        Outer3.Inner3 in = new Outer3().new Inner3();  // 내부 클래스 생성
        in.doIt(30);
    }
}

실행 결과: 지역 변수, 내부 클래스 변수, 외부 클래스 변수의 값을 출력


  • 5. 내부 클래스의 접근 제한자 사용

내부 클래스에도 public, private, protected, static 등의 접근 제한자를 적용할 수 있습니다.

public class Outer5 {
    public class Inner1 { // public 접근 가능
        public void doIt() {
            System.out.println("Inner1.doIt 실행");
        }
    }

    protected class Inner2 { // 상속받은 클래스에서 접근 가능
        public void doIt() {
            System.out.println("Inner2.doIt 실행");
        }
    }

    class Inner3 { // 같은 패키지 내에서 접근 가능
        public void doIt() {
            System.out.println("Inner3.doIt 실행");
        }
    }

    private class Inner4 { // 외부에서 접근 불가
        public void doIt() {
            System.out.println("Inner4.doIt 실행");
        }
    }

    public static class Inner5 { // static 내부 클래스
        public static final int MY_NUM = 50;
        public static void doIt() {
            System.out.println("Inner5.doIt 실행");
        }
    }

    public void go() {
        Inner1 in1 = new Inner1();
        in1.doIt();

        Inner2 in2 = new Inner2();
        in2.doIt();

        Inner3 in3 = new Inner3();
        in3.doIt();

        Inner4 in4 = new Inner4();
        in4.doIt();

        Inner5.doIt();
    }

    public static void main(String[] args) {
        Outer5 out = new Outer5();
        out.go();
        Outer5.Inner5.doIt(); // 정적 내부 클래스 호출
        System.out.println("Inner5.MY_NUM : " + Inner5.MY_NUM);
    }
}

내부 클래스의 접근 제한자는 일반적인 클래스와 동일한 방식으로 적용됨

private 내부 클래스는 외부 클래스 내부에서만 접근 가능

static 내부 클래스는 독립적으로 접근 가능


정리

  1. 내부 클래스는 외부 클래스의 멤버 변수와 메서드에 자유롭게 접근 가능
  2. 다른 클래스에서 내부 클래스를 사용하려면 외부 클래스의 인스턴스를 통해 생성
  3. static 내부 클래스는 외부 클래스 없이 직접 생성 가능
  4. 변수 참조 순서: 지역 변수 → 내부 클래스 변수 → 외부 클래스 변수
  5. 내부 클래스에도 접근 제한자(public, private 등) 적용 가능



5. 라이브러리와 모듈(많이 안쓰지만 개념은 알아두자)

자바에서는 코드의 재사용성을 높이고 유지보수를 쉽게 하기 위해 다양한 라이브러리모듈 시스템을 제공합니다. 이를 통해 여러 기능을 손쉽게 활용하고, 프로젝트의 구조를 논리적으로 구성할 수 있습니다.


  • 자바 표준 라이브러리
  • 오라클에서 제공하는 공식 라이브러리로, 자바 개발 환경에 기본 포함됩니다.
  • 입출력, 데이터구조, 알고리즘, 네트워킹, 스레딩, 컬렉션 등의 기능을 제공합니다.
  • 대표적인 패키지:
    • java.io → 파일 입출력 관련 클래스 포함
    • java.util → 자료구조 및 컬렉션 관련 클래스 포함
    • java.net → 네트워크 프로그래밍 관련 클래스 포함
    • java.sql → 데이터베이스 처리 관련 클래스 포함
  • 외부 라이브러리
  • 여러 개발자와 조직에서 제공하는 추가 라이브러리로, 메이븐(Maven)과 그래들(Gradle) 같은 빌드 도구를 통해 프로젝트에 쉽게 추가할 수 있습니다.
  • 대표적인 외부 라이브러리:
    • Apache Commons → 다양한 유틸리티 기능 제공 (파일 처리, 문자열 조작 등)
    • Jackson → JSON 데이터를 처리하는 라이브러리
    • JUnit → 테스트 프레임워크

  • 배포 파일 (JAR, WAR, EAR)

자바에서는 프로그램을 배포할 때 파일 단위로 패키징할 수 있습니다.

배포 파일설명
JAR (Java Archive)자바 클래스, 리소스, 메타데이터 등을 포함하는 기본 패키징 형식
WAR (Web Application Archive)웹 애플리케이션 배포용 파일로, HTML, JSP, XML 등이 포함됨
EAR (Enterprise Archive)여러 모듈을 통합하여 엔터프라이즈 애플리케이션 배포용

JAR 파일 예제:

jar cf myapp.jar -C bin .

위 명령어는 bin 디렉터리 안의 모든 파일을 myapp.jar로 압축하여 패키징합니다.

WAR 파일 예제:

jar cvf myapp.war -C web .

웹 애플리케이션을 WAR 파일로 패키징하여 배포할 수 있습니다.


5.1. 모듈 시스템

자바 9부터 모듈 시스템이 도입되어 프로젝트의 캡슐화 및 의존성 관리가 더욱 쉬워졌습니다.

모듈 구성 요소

  1. 모듈(Module): 클래스, 인터페이스, 리소스 등을 논리적으로 그룹화한 코드 집합
  2. 모듈 디스크립터 (module-info.java): 모듈의 이름, 의존성, 내보낼 패키지 등을 정의하는 파일
  3. 의존성 관리: 프로젝트 내의 모듈 간 관계를 명확하게 설정
  4. 모듈 경로(Module Path): 클래스로더가 모듈을 찾는 경로
  5. 캡슐화(Encapsulation): 필요한 패키지만 공개하고 내부 구현을 숨길 수 있음

모듈 정의 예제 (module-info.java):

module com.example.myproject {
    exports com.example.mymodule;  // 외부에 공개할 패키지
    requires some.other.module;     // 필요로 하는 다른 모듈
}

exports → 외부에서 접근 가능한 패키지를 지정

requires → 다른 모듈이 필요할 경우 명시적으로 선언


5.2. 모듈 export와 import (빌드 패스)

모듈 시스템을 사용할 때는 빌드 패스(Build Path)를 올바르게 설정해야 합니다.

  • 모듈을 내보내고(exports) 다른 모듈에서 가져오기(requires)
// module-info.java (내보내는 모듈)
module com.example.moduleA {
    exports com.example.utils;
}

// module-info.java (가져오는 모듈)
module com.example.moduleB {
    requires com.example.moduleA;
}

moduleAcom.example.utils 패키지를 공개(export)

moduleBmoduleA필요(requires) 하므로 해당 패키지의 클래스를 사용할 수 있음


5.3. 빌드 패스 설정

모듈 시스템을 사용할 때는 클래스 패스 대신 모듈 패스를 사용해야 합니다.

빌드 경로 설정 방법

  • JAR 기반 프로젝트 → 기존 클래스 패스(Classpath) 사용 가능
  • 모듈 기반 프로젝트모듈 패스(Module Path) 사용 필요

클래스 패스(Classpath) 예제

javac -cp external_lib.jar MyClass.java
java -cp external_lib.jar MyClass

-cp 옵션을 사용하여 클래스를 실행

모듈 패스(Module Path) 예제

javac --module-path mods -d out src/moduleA/module-info.java src/moduleA/com/example/utils/*.java
java --module-path out -m com.example.moduleA/com.example.utils.Main

--module-path 옵션을 사용해 모듈을 빌드하고 실행


정리

  1. 라이브러리
    • 자바 표준 라이브러리 (java.util, java.io, java.net 등)
    • 외부 라이브러리 (Apache Commons, JUnit, Jackson 등)
  2. 배포 파일
    • JAR → 일반적인 클래스 패키징
    • WAR → 웹 애플리케이션 패키징
    • EAR → 엔터프라이즈 애플리케이션 패키징
  3. 모듈 시스템
    • module-info.java에서 모듈을 정의 (exports, requires 사용)
    • 캡슐화를 통해 필요한 패키지만 외부에 공개
  4. 빌드 패스 설정
    • 기존 JAR 기반 프로젝트 → 클래스 패스 사용 (cp)
    • 모듈 기반 프로젝트 → 모듈 패스 사용 (-module-path)

5.4 리플렉션 (Reflection) API 정리(평생 1번쓸까 말까 개념만 알아두자)

  • 리플렉션(Reflection)은 실행 중인 프로그램의 구조(Class, Method, Field 등)에 대한 정보를 얻거나 수정할 수 있는 기능입니다. 즉, 컴파일 타임이 아닌 런타임에서 동적으로 클래스나 메서드를 다룰 수 있도록 해줍니다.

  • 1. 리플렉션 API 개요

리플렉션 API는 java.lang.reflect 패키지에 포함되어 있으며, 주요 클래스는 다음과 같습니다:

리플렉션 클래스설명
Class<T>클래스 정보를 가져올 수 있는 클래스
Method클래스 내부의 메서드 정보 조회 및 실행
Field클래스의 필드 정보 조회 및 값 변경
Constructor클래스의 생성자 정보를 조회 및 호출

기본적인 리플렉션 사용 예제

import java.lang.reflect.Method;

class ExampleClass {
    public void showMessage() {
        System.out.println("Hello, Reflection!");
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 클래스 객체 가져오기
        Class<?> clazz = ExampleClass.class;

        // 특정 메서드 가져오기
        Method method = clazz.getMethod("showMessage");

        // 객체 생성 후 메서드 호출
        Object instance = clazz.getDeclaredConstructor().newInstance();
        method.invoke(instance);
    }
}

clazz.getMethod("showMessage") → 클래스 내부의 showMessage() 메서드를 가져옴

method.invoke(instance) → 가져온 메서드를 실행


  • 2. 리플렉션은 언제 사용될까?

리플렉션은 일반적인 개발에서 자주 사용되지 않지만, 다음과 같은 특정 상황에서 유용합니다.

  1. 프레임워크나 라이브러리 개발
    • Spring, Hibernate 같은 프레임워크는 리플렉션을 활용해 객체를 자동으로 생성하거나 메서드를 동적으로 실행합니다.
  2. 애노테이션 처리
    • JUnit 테스트 프레임워크는 @Test 어노테이션을 찾아 자동으로 테스트 메서드를 실행하는데 리플렉션을 사용합니다.
  3. 플러그인 시스템
    • 런타임에 모듈을 로딩하고 실행하는 플러그인 시스템에서 활용됩니다.
  4. 안드로이드 프로그래밍
    • 특정 API 호출이 버전에 따라 다를 경우, 리플렉션을 이용해 런타임에 적절한 메서드를 실행할 수 있습니다.

  • 3. 리플렉션의 단점

리플렉션은 강력한 기능을 제공하지만, 다음과 같은 단점이 있습니다.

  • 성능 저하: 일반적인 메서드 호출보다 느립니다. 런타임에 클래스와 메서드를 찾아 실행하기 때문입니다.
  • 컴파일 타임 오류 검출 불가: 정적 검사가 불가능하므로 런타임 오류 발생 가능성이 높습니다.
  • 보안 문제: private 필드 및 메서드까지 접근할 수 있어 잘못된 사용 시 보안 위험이 있음.

보안 문제 예제

import java.lang.reflect.Field;

class PrivateFieldExample {
    private String secret = "Hidden Data";
}

public class ReflectionSecurityExample {
    public static void main(String[] args) throws Exception {
        PrivateFieldExample obj = new PrivateFieldExample();
        Field field = PrivateFieldExample.class.getDeclaredField("secret");
        field.setAccessible(true); // private 필드 접근 허용
        System.out.println("Secret: " + field.get(obj)); // "Hidden Data" 출력
    }
}

setAccessible(true)를 사용하면 private 변수에도 접근 가능

✔ 이러한 기능이 잘못 사용될 경우 보안 문제가 발생할 수 있음


  • 4. 현업에서 리플렉션을 사용할까?

리플렉션은 강력하지만 일반적인 애플리케이션 개발에서는 잘 사용되지 않습니다.

  • 현업에서는 직접 활용할 일이 거의 없음 → 성능 저하 및 유지보수 문제 때문에 자주 사용되지 않음
  • 프레임워크 내부에서 많이 사용됨 → Spring, Hibernate 같은 프레임워크에서는 자동화 기능을 구현하기 위해 사용됨
  • 안드로이드 개발에서 제한적으로 사용됨 → 특정 API의 버전에 따라 동적으로 메서드를 실행할 때 활용됨

즉, 리플렉션을 직접 활용하는 경우는 드물지만, 프레임워크나 시스템 내부에서 사용하는 경우가 많음.


5.4 전이 모듈(Transitive Module)과 모듈 그룹(Module Group) 정리

자바 모듈 시스템에서 전이 모듈(Transitive Module)과 모듈 그룹(Module Group)은 모듈 간의 관계를 더욱 효율적으로 관리하기 위해 사용됩니다.


5.4.1. 전이 모듈 (Transitive Module)

전이 모듈은 모듈 간의 의존성을 자동으로 연결하여 중간 모듈을 거쳐 간접적인 의존성을 제공하는 개념입니다.

전이 모듈의 동작 방식

예를 들어, 아래와 같은 모듈 관계가 있다고 가정합니다.

전이 의존성을 설정한 경우

module A {
    requires B; // A 모듈은 B 모듈에 의존
}

module B {
    requires transitive C; // B 모듈은 C 모듈을 전이적으로 의존
}

module C {
    // C 모듈 정의
}

A 모듈은 B에 직접 의존하지만, B 모듈이 requires transitive C를 사용했기 때문에 A 모듈은 C 모듈도 자동으로 사용할 수 있음.

requires transitive를 사용하면 직접 명시하지 않아도 A 모듈에서 C 모듈의 클래스를 참조 가능.


전이 의존성을 설정하지 않은 경우

module B {
    requires C; // B 모듈은 C 모듈에 의존하지만 transitive 사용 안함
}

module A {
    requires B; // A 모듈은 B에 의존
    requires C; // A 모듈에서 C도 직접 의존해야 함
}

requires transitive를 사용하지 않으면 A 모듈에서 C 모듈을 직접 지정해야 함.

직접적인 의존 관계를 설정해야 하므로 명시적으로 모든 의존성을 선언해야 함.


전이 모듈이 필요한 이유

  • 코드 간결화: 중간 모듈이 다른 모듈을 포함할 경우, 최종 모듈에서 추가적인 설정 없이 필요한 모듈을 사용할 수 있음.
  • 의존성 자동 관리: 여러 모듈 간의 관계를 일일이 설정할 필요 없이 필요한 모듈이 자동으로 연결됨.
  • 소프트웨어 구조 개선: 관련된 모듈 간의 관계를 명확하게 설정할 수 있어 유지보수가 쉬워짐.

5.4.2. 모듈 그룹 (Module Group)

모듈 그룹은 여러 모듈을 논리적으로 묶어서 함께 관리하는 개념입니다.

이를 통해 특정 기능이나 비즈니스 로직과 관련된 모듈들을 한 번에 의존성으로 추가할 수 있습니다.

모듈 그룹을 활용한 예제

module A {
    requires transitive B; // A 모듈이 B를 전이적으로 의존
    requires transitive C; // A 모듈이 C를 전이적으로 의존
}

module B {
    // B 모듈 정의
}

module C {
    // C 모듈 정의
}

A 모듈이 B와 C를 모두 전이적으로 의존하므로, A 모듈만 requires 설정하면 B와 C 모듈도 자동으로 연결됨.


모듈 그룹을 활용한 프로젝트 구성

예를 들어, 모든 모듈을 포함해야 하는 AppModule 프로젝트가 있을 경우, 다음과 같이 설정할 수 있습니다.

module AppModule {
    requires A; // A 모듈만 의존성을 설정하면 B와 C도 자동 포함됨
}

AppModuleA를 의존하면, A가 전이적으로 BC를 포함하기 때문에 한 번에 모든 모듈을 가져올 수 있음.

모듈 그룹을 사용하면 특정 기능을 담당하는 모듈들을 한 번에 관리할 수 있어 프로젝트 구성이 명확해짐.


5.4.3. 전이 모듈과 모듈 그룹의 장점

개념설명
전이 모듈간접적인 의존성 관리로 불필요한 requires 선언을 줄일 수 있음.
모듈 그룹관련된 모듈을 묶어 의존성을 쉽게 관리하고 모듈 간 관계를 명확하게 정의.

전이 모듈을 활용하면 개발자가 직접 모든 의존성을 추가하지 않아도 필요한 모듈이 자동으로 포함됨.

모듈 그룹은 프로젝트를 논리적으로 나누고 유지보수를 쉽게 할 수 있도록 도와줌.


정리

  1. 전이 모듈 (requires transitive)
    • 중간 모듈이 다른 모듈을 의존할 때, 이를 최종 모듈에서도 자동으로 사용할 수 있도록 연결.
    • 불필요한 직접 requires 선언을 줄일 수 있음.
  2. 모듈 그룹 (Module Group)
    • 여러 모듈을 논리적으로 묶어서 관리하고, 필요할 때 한 번에 포함 가능.
    • 특정 기능 단위의 모듈을 그룹화하여 효율적인 의존성 관리가 가능.
  3. 활용 예제
    • AppModuleA 모듈을 포함하면, A를 통해 BC도 함께 포함됨.
    • 유지보수 및 의존성 관리를 쉽게 할 수 있음.
profile
인생 별거 없어

0개의 댓글