CodeGym /행동 /JAVA 25 SELF /리플렉션과 의존성 주입 (Dependency Injection)

리플렉션과 의존성 주입 (Dependency Injection)

JAVA 25 SELF
레벨 62 , 레슨 4
사용 가능

1. 리플렉션을 통한 직렬화

이미 알고 있듯이 직렬화는 객체를 바이트 스트림이나 텍스트 표현으로 변환하는 과정입니다. Java에는 표준 직렬화(Serializable)가 있지만, 실제로는 JacksonGson 같은 라이브러리를 통해 JSON/XML 형식을 더 자주 사용합니다.

왜 여기서 리플렉션이 필요할까요?
객체를 직렬화하려면 그 객체의 필드와 값들을 알아야 합니다. 필드는 private일 수 있고, 일반적인 코드로는 접근할 수 없습니다 — 하지만 리플렉션이라면 가능합니다. 그래서 JSON 라이브러리는 런타임에 객체 구조를 동적으로 순회하면서 FieldMethod를 통해 필드를 읽고/쓸 수 있습니다.

예시: 객체를 문자열로 단순 직렬화

데이터 클래스:

public class Person {
    private String name;
    private int age;
    private boolean active;

    public Person(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

리플렉션 기반의 가장 단순한 직렬화기:

import java.lang.reflect.Field;

public class SimpleSerializer {
    public static String serialize(Object obj) {
        StringBuilder sb = new StringBuilder();
        Class<?> clazz = obj.getClass();
        sb.append(clazz.getSimpleName()).append("{");
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            field.setAccessible(true); // private 필드 접근 허용
            try {
                sb.append(field.getName()).append("=")
                  .append(field.get(obj));
            } catch (IllegalAccessException e) {
                sb.append(field.getName()).append("=<?>");
            }
            if (i < fields.length - 1) sb.append(", ");
        }
        sb.append("}");
        return sb.toString();
    }
}

사용 예:

Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}

작동 방식

  • getDeclaredFields()로 선언된 필드를 가져옵니다.
  • setAccessible(true)로 접근 가능하게 만듭니다.
  • 필드 이름과 값을 읽어 문자열을 만듭니다.

예제의 한계: 중첩 객체, 컬렉션, 배열, 순환 참조를 처리하지 않습니다 — 원리를 보여주기 위한 데모일 뿐입니다.

실제 라이브러리는 어떻게 하나요?

Jackson/Gson은 중첩 객체와 컬렉션을 처리하고, 애노테이션(@JsonIgnore, @SerializedName), 날짜 포맷 등도 고려합니다 — 이 모든 것이 리플렉션을 기반으로 합니다.

Jackson 예시:

import com.fasterxml.jackson.databind.ObjectMapper;

Person p = new Person("Bob", 25, false);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
// {"name":"Bob","age":25,"active":false}

2. Dependency Injection (DI)과 리플렉션

Dependency Injection은 의존성을 클래스 내부에서 생성하지 않고 외부에서 ‘주입’하는 패턴입니다. 이는 코드를 더 유연하고, 테스트 가능하며, 확장 가능하게 만듭니다. Java에서는 Spring, Guice, Dagger 같은 프레임워크가 @Autowired, @Inject 같은 애노테이션으로 주입 지점을 표시합니다.

여기서 리플렉션이 필요한 이유
DI 컨테이너는 필드/생성자를 찾고 애노테이션을 읽은 뒤 런타임에 인스턴스를 생성해야 합니다 — 이는 Class/Constructor/Field API로 수행됩니다.

예시: 리플렉션으로 구현한 미니 DI

주입 애노테이션:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}

의존성을 가지는 클래스:

public class Service {
    public void doWork() {
        System.out.println("Service is working!");
    }
}

public class Client {
    @Inject
    private Service service;

    public void useService() {
        service.doWork();
    }
}

미니 컨테이너:

import java.lang.reflect.*;

public class MiniDIContainer {
    // 클래스에 대해 객체를 생성하고 @Inject가 붙은 필드에 의존성을 주입합니다
    public static Object createObject(Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();

        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                Object dependency = createObject(field.getType()); // 재귀적으로
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

사용 예:

public class Main {
    public static void main(String[] args) throws Exception {
        Client client = (Client) MiniDIContainer.createObject(Client.class);
        client.useService(); // Service is working!
    }
}

작동 방식 컨테이너는 @Inject가 붙은 필드를 찾아, 해당 타입으로 의존성을 생성하고 리플렉션으로 private 필드에 설정합니다.

중요: 이는 단순화된 그림입니다. 실제 DI 컨테이너는 스코프, 싱글톤, 구성, 프록시, 순환 의존성 처리 등도 지원합니다.

3. 동적 프록시

프록시는 호출을 가로채 로깅, 보안, 트랜잭션 등과 같은 동작을 추가하는 ‘대리’ 객체입니다. Java에서는 java.lang.reflect.ProxyInvocationHandler가 이를 제공합니다. 이는 많은 Spring AOP 기능과 Mockito의 목 생성 등에 기반이 됩니다.

예시: 로깅 프록시

public interface HelloService {
    void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
}
import java.lang.reflect.*;

public class LoggingProxy {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class<?>[]{iface},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("메서드 호출: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

사용 예:

HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);

proxy.sayHello("World");
메서드 호출: sayHello
Hello, World!

작동 방식 Proxy.newProxyInstance는 인터페이스를 구현하는 객체를 만들고, 모든 메서드 호출은 InvocationHandler.invoke로 전달됩니다. 여기서 위임 전/후에 원하는 ‘횡단 관심사’ 코드를 실행할 수 있습니다.

4. 실전 시나리오와 제한사항

리플렉션은 ‘실전’에서 어디에 쓰이나?

  • JUnit@Test가 붙은 메서드를 찾아 리플렉션으로 호출합니다.
  • Spring — 빈을 생성하고 의존성을 주입하며, 애노테이션을 스캔하고 프록시를 생성합니다.
  • Jackson/Gson — private 필드를 포함해 읽어 직렬화/역직렬화합니다.
  • Hibernate — 클래스 구조로 ORM 모델을 만들고, 필드와 프록시 객체를 관리합니다.
  • Mockito — 목을 생성하고 프록시를 통해 호출을 가로챕니다.

리플렉션을 항상 사용하면 안 되는 이유

  • 성능. 일반 호출보다 느린 경향이 있습니다(캐싱/바이트코드 생성으로 완화 가능).
  • 보안. 캡슐화를 깨뜨려 private 데이터에 접근할 수 있습니다.
  • Java 9+ 모듈성. 패키지/모듈을 명시적으로 열지 않으면 InaccessibleObjectException이 발생할 수 있습니다.

5. 자주 하는 실수

오류 1: checked 예외 무시. 리플렉션 메서드는 NoSuchFieldException, IllegalAccessException, InvocationTargetException 등을 던집니다. 이들을 처리하거나 여러분의 예외로 래핑하세요.

오류 2: setAccessible(true)가 항상 동작하는 것은 아님. Java 9+ 모듈식 애플리케이션에서는 InaccessibleObjectException이 발생할 수 있습니다. JVM/모듈 파라미터(--add-opens)를 사용하거나 공개 API를 사용해야 합니다.

오류 3: DI에서 순환 의존성. 순진한 재귀(AB에 의존하고, BA에 의존)를 사용하면 StackOverflowError가 발생합니다. 실제 컨테이너는 의존성 그래프를 추적하고 특수 기법으로 순환을 해소합니다.

오류 4: 객체 직렬화가 불완전함. 재귀 순회가 없으면 참조 필드는 ClassName@hash처럼 직렬화됩니다. 올바른 직렬화를 위해서는 중첩 객체/컬렉션 처리와 순환에 대한 보호가 필요합니다.

오류 5: 성능 저하. 빈번한 리플렉션 연산(루프나 핫 패스)은 병목이 됩니다. Field/Method 캐싱, 바이트코드 생성, MethodHandle 또는 애노테이션 프로세싱을 사용하세요.

오류 6: 캡슐화 위반. 리플렉션으로 private 필드를 변경하면 코드가 취약해지고 디버깅하기 어려운 버그로 이어질 수 있습니다. 가능하면 공개된 계약을 우선하세요.

1
설문조사/퀴즈
리플렉션, 레벨 62, 레슨 4
사용 불가능
리플렉션
리플렉션 и 다이내믹한 가능성
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION