공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 08 / 10

어노테이션·리플렉션·동적 프록시

Spring 과 MyBatis 가 움직이는 원리
섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

소스: java-src/advanced/08_reflection_proxy (어노테이션 5개, FakeDb, MapperFactory, TxProxy, MiniContainer, Main).

bash
cd java-src/advanced/08_reflection_proxy
javac -encoding UTF-8 *.java && java -Dstdout.encoding=UTF-8 Main

예제 1: Class 객체로 구조 읽기

java
Class<?> c = MemberServiceImpl.class;
for (Field f : c.getDeclaredFields())
    System.out.printf("필드 %s %s @Inject=%b%n",
            f.getType().getSimpleName(), f.getName(), f.isAnnotationPresent(Inject.class));
for (Method m : c.getDeclaredMethods()) {
    Transactional t = m.getAnnotation(Transactional.class);
    System.out.printf("메서드 %s @Transactional=%s%n",
            m.getName(), t == null ? "없음" : "readOnly=" + t.readOnly());
}
text
필드   MemberMapper mapper  @Inject=true
메서드 find(인자 1개)  @Transactional=없음
메서드 transfer(인자 3개)  @Transactional=readOnly=false
메서드 transferAll(인자 2개)  @Transactional=없음
record 구성요소: [long id, java.lang.String name, int point]

getDeclaredMethods() 의 순서는 보장되지 않습니다. 예제는 이름으로 정렬해 출력했습니다. record 는 getRecordComponents() 로 구성요소를 순서대로 얻을 수 있어 매핑에 쓰기 좋습니다.

예제 2: 문자열로 찾기, private 접근, 호출

java
Class<?> c = Class.forName("MemberServiceImpl");
Object svc = c.getDeclaredConstructor().newInstance();
Field f = c.getDeclaredField("mapper");
f.get(svc);                 // IllegalAccessException: private
f.setAccessible(true);
f.get(svc);                 // null (아직 주입 전)

Method find = c.getMethod("find", long.class);
find.invoke(svc, 1L);       // 안에서 NPE → InvocationTargetException
text
private 접근 실패: class Main cannot access a member of class MemberServiceImpl ...
setAccessible 후 mapper = null
find(Long) 없음 → 시그니처는 long.class 로 찾아야 합니다
invoke 예외: InvocationTargetException ← 원본 NullPointerException

예제 3: 가장 작은 동적 프록시

java
List<String> logged = (List<String>) Proxy.newProxyInstance(
        Main.class.getClassLoader(), new Class<?>[]{List.class},
        (proxy, m, args) -> {
            System.out.println("  호출: " + m.getName() + Arrays.toString(args));
            return m.invoke(target, args);
        });
logged.add("a");
logged.size();
text
  호출: add[a]
  호출: add[b]
  호출: size[]
size() = 2
프록시 클래스: jdk.proxy2.$Proxy4, isProxyClass=true, instanceof List=true
클래스는 불가: java.util.ArrayList is not an interface

instanceof List 가 참이므로 호출자는 프록시인지 모릅니다. 인자가 없는 메서드는 args 가 null 로 들어오므로 배열로 다룰 때 주의합니다.

예제 4: 매퍼 인터페이스만으로 SQL 실행

MemberMapper 에는 구현 클래스가 없습니다. MapperFactory.create 가 프록시를 만들고, 핸들러가 SQL 을 조립합니다.

java
public interface MemberMapper {
    @Select("SELECT * FROM member WHERE id = #{id}")
    Member findById(@Param("id") long id);

    @Update("UPDATE member SET point = point + #{delta} WHERE id = #{id}")
    int addPoint(@Param("id") long id, @Param("delta") int delta);
}

핸들러의 바인딩 부분입니다. #{이름} 을 찾을 때마다 ? 로 바꾸고, 그 이름의 값을 순서대로 모읍니다.

java
Matcher mt = PARAM.matcher(sql);                  // #\{(\w+)}
while (mt.find()) {
    ordered.add(named.get(mt.group(1)));
    mt.appendReplacement(prepared, "?");
}
mt.appendTail(prepared);
text
mapper = MapperProxy[MemberMapper] (jdk.proxy2.$Proxy5)
  ==> Preparing: SELECT * FROM member WHERE id = ?
  ==> Parameters: [1]
findById(1) = Member[id=1, name=김철수, point=100]
  ==> Preparing: UPDATE member SET point = point + ? WHERE id = ?
  ==> Parameters: [25, 3]
addPoint(3, 25) = 1건 → Member[id=3, name=박민수, point=75]
findById(99) = null

MyBatis 로그의 ==> Preparing / ==> Parameters 와 같은 모양입니다. 인자 순서가 SQL 에 나온 순서(delta, id) 로 바뀐 것을 보면 이름 바인딩이 왜 필요한지 알 수 있습니다.

행을 객체로 바꾸는 부분은 record 정식 생성자를 리플렉션으로 호출합니다.

java
RecordComponent[] comps = type.getRecordComponents();
for (int i = 0; i < comps.length; i++) {
    types[i] = comps[i].getType();
    vals[i] = row.get(comps[i].getName());     // 컬럼명 = 구성요소 이름
}
return type.getDeclaredConstructor(types).newInstance(vals);

예제 5: @Transactional 프록시와 내부 호출 함정

java
MemberService svc = TxProxy.wrap(new MemberServiceImpl(mapper), MemberService.class, tx);
svc.transfer(1, 2, 30);      // BEGIN → COMMIT
svc.transfer(1, 99, 10);     // BEGIN → 예외 → ROLLBACK, 예외는 그대로 전달
svc.transferAll(1, 99);      // 안에서 this.transfer → 프록시 없음
text
(a) 정상 이체 1→2, 30점
  [TX] BEGIN
  [TX] COMMIT
    결과: Member[id=1, name=김철수, point=70], Member[id=2, name=이영희, point=230]
(b) 없는 회원에게 이체 1→99, 10점
  [TX] BEGIN
  [TX] ROLLBACK ← IllegalArgumentException
    예외 그대로 전달: 받는 회원 없음: 99
    결과: Member[id=1, name=김철수, point=70] (롤백으로 70점 유지)
(c) transferAll(1, 99): 안에서 this.transfer 호출
    예외: 받는 회원 없음: 99
    결과: Member[id=1, name=김철수, point=0] ← BEGIN 없이 실행되어 롤백되지 않음

(b) 는 먼저 70점을 빼고 나서 예외가 났지만 롤백으로 원상복구됐습니다. (c) 는 같은 예외인데 BEGIN 로그가 없고, 빠진 70점이 그대로 사라졌습니다. 이것이 실무에서 "트랜잭션이 안 걸려요" 의 원인 1위입니다.

프록시 핸들러의 핵심은 세 줄입니다. 어노테이션을 구현 클래스 쪽에서 다시 찾는 점에 주의합니다. 인터페이스 Method 객체에는 구현체의 어노테이션이 없습니다.

java
Method impl = cls.getMethod(m.getName(), m.getParameterTypes());
Transactional t = impl.getAnnotation(Transactional.class);
if (t == null) return invoke(m, target, args);

예제 6: 미니 DI 컨테이너

java
MiniContainer container = new MiniContainer(tx)
        .register(new MapperFactory(db).create(MemberMapper.class), new MemberServiceImpl());
container.start();
MemberService svc = container.get(MemberService.class);
text
  등록: 프록시[interface MemberMapper]
  등록: MemberServiceImpl → 프록시로 감쌈
  주입: MemberServiceImpl.mapper ← 프록시[interface MemberMapper]
꺼낸 빈: 프록시[interface MemberService]
  [TX] BEGIN
  [TX] COMMIT

컨테이너는 두 단계로 일합니다. 먼저 @Transactional 이 있는 객체를 프록시로 감싸 공개용 목록에 넣고, 그다음 각 객체의 @Inject 필드에 공개용 빈을 넣습니다. get() 은 항상 공개용을 돌려주므로 호출자는 반드시 프록시를 거칩니다. Spring 의 @Autowired 가 프록시를 주입하는 이유가 이 순서에 있습니다.

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\advanced\08_reflection_proxy
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: Class 객체로 구조 읽기
  • 예제 2: 문자열로 찾기, private 접근, 호출
  • 예제 3: 가장 작은 동적 프록시
  • 예제 4: 매퍼 인터페이스만으로 SQL 실행
  • 예제 5: @Transactional 프록시와 내부 호출 함정
  • 예제 6: 미니 DI 컨테이너
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제