Spring/SpringCore - advance

스프링 핵심원리( 심화 ) - 동적 프록시(리플렉션)

_Jin_ 2026. 1. 26.

지금까지 프록시를 통한 원본 코드에 대한 변경 없이 부가 기능을 적용하는 방법에 대해 알아보았다.

처음 컨트롤러 - 서비스 - 레포지토리 각 계층 코드의 직접적인 변경을 요구하던 형태에서

본 기능의 코드는 부가 기능으로 인하여 더렵혀지지 않는 방법까지 알아보았다.

보다 유지보수에 수월한 형태와 방법을 적용한 것이다!

 

그러나 여전히 로그 추적기라는 부가 기능을 적용하기 위해서 각 계층의 대상 클래스에 모두 프록시 클래스나 인터페이스들을 만들어야하는 작업은 별도로 필요하다. 

반복을 줄이기 위한 방법은 없을까??

 

이에 대한 방법으로 자바는 기본적으로 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하여 프록시 객체를 동적으로 만들어낼 수 있도록 제공한다.

 

그리고 이 JDK 동적 프록시 기술에 대한 이해를 위해서는 자바의 '리플랙션'에 대한 이해가 필요하다. 

 

리플랙션

리플랙션은 클래스나 메서드 및 멤버에 대한 정보를 런타임에 조사하고 조작을 가능하게 만들어주는 기술이다.

아래의 코드를 통해서 구체적으로 어떤 부분을 가능하게 만드는 기술인지 이해해보자.

 

기본 상황

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;

@Slf4j
public class ReflectionTest {
    @Test
    void reflection0() {
        
        Hello target = new Hello();
		
        //공통 로직1 시작
        log.info("start");
        String result1 = target.callA(); //호출하는 메서드가 다름
        log.info("result={}", result1);
		
        //공통 로직1 종료
		//공통 로직2 시작
        log.info("start");
        String result2 = target.callB(); //호출하는 메서드가 다름
        log.info("result={}", result2);
		//공통 로직2 종료
    }

    @Slf4j
    static class Hello {
        public String callA() {
            log.info("callA");
            return "A";
        }

        public String callB() {
            log.info("callB");
            return "B";
        }
    }
}

 

위의 테스트 코드에서 실행하는 공통 로직1,2는 메서드만 다르고 흐름이 똑같은 코드이다.

여기서 공통 로직1과 공통 로직2를 하나의 메서드로 뽑아서 합칠 수 있는가? ( 반복적인 코드에 대한 중복을 줄이는 방법 )에 대한 질문에 대해서 하나의 해결 방법이 리플랙션을 사용하는 것이다. ( 람다를 사용하는 방법도 있다. )

 

리플렉션 적용

리플렉션을 사용하여, 클래스의 메서드를 사용하는 방법에 대해서 아래의 코드를 통해 파악해보자.

    @Test
    void reflection1() throws Exception {

	//클래스 정보
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
        Hello target = new Hello();

	//callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        Object result1 = methodCallA.invoke(target);
        log.info("result1={}", result1);
		
        //callB 메서드 정보
        Method methodCallB = classHello.getMethod("callB");
        Object result2 = methodCallB.invoke(target);
        log.info("result2={}", result2);
    }

 

[ 각 코드 설명 ]
⭐️Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello") - 클래스의 메타 정보를 획득한다. (내부 클래스는 구분을 위해 $를 사용한다. )

⭐️획득한 클래스 정보를 바탕으로, classHello.getMethod("call") 해당 클래스의 call 메소드 메타 정보를 획득한다.

⭐️획득한 메서드 메토정보로 실제 인스턴스의 메서드를 호출한다.  methodCallA.invoke(target)

 

이처럼 리플랙션을 사용하여 기존의 메서드 호출 방법과 크게 달라지는 부분이 무엇일까?

클래스의 인스턴스를 통해 직접 개별적인 메서드를 호출하던 방식에서 이제 각 메서드의 메타 정보를 취득하여 Method라는 공통 추상화 타입으로 받을 수 있다.

덕분에 공통 로직으로 만들 수 있게 되었다.

 

리플렉션을 활용한 공통 로직

    @Test
    void reflection2() throws Exception {
    
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
        Hello target = new Hello();
        Method methodCallA = classHello.getMethod("callA");
        dynamicCall(methodCallA, target);
        Method methodCallB = classHello.getMethod("callB");
        dynamicCall(methodCallB, target);
    }

    private void dynamicCall(Method method, Object target) throws Exception {
        log.info("start");
        Object result = method.invoke(target);
        log.info("result={}", result);
    }

 

dynamicCall(Method method, Object target) 메서드가 이제 공통 로직을 담당하는 코드이다.

추상화된 메서드 메타 정보가 파라미터로 넘어오고, 실행할 인스턴스 정보를 파라미터로 받는다. 

 

이제 정적인 ` target.callA()` , ` target.callB()` 코드를 리플렉션을 사용해서 ` Method` 라는 메타정보로 추상화하여 공통 로직을 만들 수 있게 된 것이다!

 

단, 주의할 점 

리플렉션은 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
따라서 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
(예를 들어 리플렉션을 사용하여 수 백개의 반복적 작업을 획기적으로 줄일 수 있다면)

 

 

 

 

 

 

댓글