어노테이션(Annotations)은 메타데이터를 코드에 추가하여 컴파일러나 런타임 환경에서 활용할 수 있도록 하는 Java의 기능입니다. 주석과는 다르게, 프로그램의 동작에 영향을 줄 수 있습니다.
@Deprecated).@Retention(RetentionPolicy.RUNTIME)).@Test).어노테이션을 정의할 때는 @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() {
// 메서드 내용
}
}
어노테이션을 적용할 때는 @어노테이션이름(요소 값) 형태로 사용하며, 요소 값은 생략할 수도 있습니다.
리플렉션(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);
}
}
위 코드에서는:
Method 객체를 통해 myMethod()를 가져옵니다.getAnnotation(MyAnnotation.class)을 호출하여 어노테이션 정보를 읽습니다.value() 메서드를 사용해 저장된 값을 출력합니다.어노테이션 유지 정책(Retention Policy)은 어노테이션이 코드의 어느 단계까지 유지될지를 결정하는 설정입니다. 유지 범위에 따라 소스 코드, 클래스 파일, 런타임 중 하나를 선택할 수 있습니다.
| 유지 정책 | 설명 |
|---|---|
| SOURCE | 소스 코드에서만 유지. 컴파일 후 어노테이션 정보가 제거됨. |
| CLASS | 클래스 파일까지 유지되지만, 런타임에서는 제거됨. |
| RUNTIME | 런타임까지 유지되며, 실행 중에 리플렉션(reflection)으로 접근 가능. |
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE) // 소스 코드에서만 유지됨
@interface MyAnnotation1 {
String value();
}
컴파일 후 어노테이션 정보가 사라지며, 주로 코드 분석 및 문서화에 활용됩니다.
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.CLASS) // 클래스 파일까지만 유지됨
@interface MyAnnotation2 {
String value();
}
클래스 파일에는 어노테이션 정보가 포함되지만 런타임에서는 접근할 수 없습니다.
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지됨
@interface MyAnnotation3 {
String value();
}
리플렉션을 사용해 실행 중에도 어노테이션 정보를 읽을 수 있으며, 프레임워크나 라이브러리에서 활용됩니다.
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()을 사용해 어노테이션 정보를 가져오는 방법을 보여줍니다.
자바에서는 클래스 내부에 또 다른 클래스를 정의하여 사용할 수 있으며, 이를 내부 클래스라고 합니다. 내부 클래스는 코드의 구조를 명확하게 만들고 특정 클래스와 강하게 연관된 기능을 정의할 때 유용합니다.
멤버 내부 클래스는 외부 클래스의 멤버 변수처럼 동작하며, 외부 클래스의 필드와 메서드에 접근할 수 있습니다.
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 파일이 생성됨.
지역 내부 클래스는 메서드 또는 블록 내부에서만 선언되고 사용되는 클래스입니다. 접근 제한자가 필요 없으며, 특정 메서드 내부에서만 유효합니다.
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 파일이 생성됨.
✔ 지역 내부 클래스는 메서드 내에서 선언되므로 다른 곳에서는 접근할 수 없음.
익명 내부 클래스는 이름이 없는 지역 내부 클래스이며, 주로 인터페이스 또는 추상 클래스를 구현할 때 사용됩니다.
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 파일이 생성됨.
✔ 익명 내부 클래스는 한 번만 사용되므로 별도의 이름이 필요 없음.
클래스 내부에 인터페이스를 정의하여 특정 목적에 맞게 사용할 수 있습니다. 다음은 버튼 클릭 이벤트를 처리하는 내부 인터페이스와 익명 클래스를 사용하는 예제입니다.
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();
}
}
✔ 익명 내부 클래스를 직접 메서드 호출 인수로 전달 가능.
자바에서 내부 클래스는 외부 클래스의 멤버를 참조할 때 특별한 제한이 없지만, 내부 클래스를 생성하는 방식은 참조 위치에 따라 다릅니다. 내부 클래스의 생성과 참조 방법을 정리하겠습니다.
내부 클래스는 외부 클래스 내부에서 정의된 클래스입니다. 내부 클래스에서는 외부 클래스의 멤버에 자유롭게 접근할 수 있습니다.
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 파일이 생성됨
내부 클래스를 외부 클래스의 인스턴스를 통해 생성해야 합니다.
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 파일이 생성됨
내부 클래스가 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 파일이 생성됨
내부 클래스의 변수 참조는 우선순위에 따라 결정됩니다.
| 참조 우선순위 | 설명 |
|---|---|
| 지역 변수 | 내부 클래스의 메서드 내에서 선언된 변수 |
| 내부 클래스의 멤버 변수 | 내부 클래스의 인스턴스 변수 |
| 외부 클래스의 멤버 변수 | 외부 클래스의 인스턴스 변수 |
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);
}
}
✔ 실행 결과: 지역 변수, 내부 클래스 변수, 외부 클래스 변수의 값을 출력
내부 클래스에도 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 내부 클래스는 독립적으로 접근 가능
자바에서는 코드의 재사용성을 높이고 유지보수를 쉽게 하기 위해 다양한 라이브러리와 모듈 시스템을 제공합니다. 이를 통해 여러 기능을 손쉽게 활용하고, 프로젝트의 구조를 논리적으로 구성할 수 있습니다.
java.io → 파일 입출력 관련 클래스 포함java.util → 자료구조 및 컬렉션 관련 클래스 포함java.net → 네트워크 프로그래밍 관련 클래스 포함java.sql → 데이터베이스 처리 관련 클래스 포함자바에서는 프로그램을 배포할 때 파일 단위로 패키징할 수 있습니다.
| 배포 파일 | 설명 |
|---|---|
| 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 파일로 패키징하여 배포할 수 있습니다.
자바 9부터 모듈 시스템이 도입되어 프로젝트의 캡슐화 및 의존성 관리가 더욱 쉬워졌습니다.
✔ 모듈 정의 예제 (module-info.java):
module com.example.myproject {
exports com.example.mymodule; // 외부에 공개할 패키지
requires some.other.module; // 필요로 하는 다른 모듈
}
✔ exports → 외부에서 접근 가능한 패키지를 지정
✔ requires → 다른 모듈이 필요할 경우 명시적으로 선언
모듈 시스템을 사용할 때는 빌드 패스(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;
}
✔ moduleA는 com.example.utils 패키지를 공개(export)
✔ moduleB는 moduleA를 필요(requires) 하므로 해당 패키지의 클래스를 사용할 수 있음
모듈 시스템을 사용할 때는 클래스 패스 대신 모듈 패스를 사용해야 합니다.
javac -cp external_lib.jar MyClass.java
java -cp external_lib.jar MyClass
✔ -cp 옵션을 사용하여 클래스를 실행
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 옵션을 사용해 모듈을 빌드하고 실행
java.util, java.io, java.net 등)Apache Commons, JUnit, Jackson 등)JAR → 일반적인 클래스 패키징WAR → 웹 애플리케이션 패키징EAR → 엔터프라이즈 애플리케이션 패키징module-info.java에서 모듈을 정의 (exports, requires 사용)cp)-module-path)리플렉션 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) → 가져온 메서드를 실행
리플렉션은 일반적인 개발에서 자주 사용되지 않지만, 다음과 같은 특정 상황에서 유용합니다.
@Test 어노테이션을 찾아 자동으로 테스트 메서드를 실행하는데 리플렉션을 사용합니다.리플렉션은 강력한 기능을 제공하지만, 다음과 같은 단점이 있습니다.
✔ 보안 문제 예제
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 변수에도 접근 가능
✔ 이러한 기능이 잘못 사용될 경우 보안 문제가 발생할 수 있음
리플렉션은 강력하지만 일반적인 애플리케이션 개발에서는 잘 사용되지 않습니다.
✔ 즉, 리플렉션을 직접 활용하는 경우는 드물지만, 프레임워크나 시스템 내부에서 사용하는 경우가 많음.
자바 모듈 시스템에서 전이 모듈(Transitive Module)과 모듈 그룹(Module Group)은 모듈 간의 관계를 더욱 효율적으로 관리하기 위해 사용됩니다.
전이 모듈은 모듈 간의 의존성을 자동으로 연결하여 중간 모듈을 거쳐 간접적인 의존성을 제공하는 개념입니다.
예를 들어, 아래와 같은 모듈 관계가 있다고 가정합니다.
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 모듈을 직접 지정해야 함.
✔ 직접적인 의존 관계를 설정해야 하므로 명시적으로 모든 의존성을 선언해야 함.
모듈 그룹은 여러 모듈을 논리적으로 묶어서 함께 관리하는 개념입니다.
이를 통해 특정 기능이나 비즈니스 로직과 관련된 모듈들을 한 번에 의존성으로 추가할 수 있습니다.
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도 자동 포함됨
}
✔ AppModule이 A를 의존하면, A가 전이적으로 B와 C를 포함하기 때문에 한 번에 모든 모듈을 가져올 수 있음.
✔ 모듈 그룹을 사용하면 특정 기능을 담당하는 모듈들을 한 번에 관리할 수 있어 프로젝트 구성이 명확해짐.
| 개념 | 설명 |
|---|---|
| 전이 모듈 | 간접적인 의존성 관리로 불필요한 requires 선언을 줄일 수 있음. |
| 모듈 그룹 | 관련된 모듈을 묶어 의존성을 쉽게 관리하고 모듈 간 관계를 명확하게 정의. |
✔ 전이 모듈을 활용하면 개발자가 직접 모든 의존성을 추가하지 않아도 필요한 모듈이 자동으로 포함됨.
✔ 모듈 그룹은 프로젝트를 논리적으로 나누고 유지보수를 쉽게 할 수 있도록 도와줌.
requires transitive)requires 선언을 줄일 수 있음.Module Group)AppModule이 A 모듈을 포함하면, A를 통해 B와 C도 함께 포함됨.