홈 › 고급 › 08 / 10

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

Spring 과 MyBatis 가 움직이는 원리
섹션 7진행 0 / 10

6. 연습 문제

문제 1

@Retention(RetentionPolicy.RUNTIME) 을 Transactional.java 에서 지우고 실행해 보세요. 어느 절부터 출력이 달라지는지, 그리고 왜 예외 없이 조용히 바뀌는지 설명하세요.

정답 보기

1절에서 @Transactional=없음 이 세 메서드 모두에 찍히고, 5절에서 [TX] BEGIN 이 사라지며 (b) 의 롤백이 되지 않아 1번 회원이 60점이 됩니다. 6절에서 → 프록시로 감쌈 도 사라집니다.

예외가 없는 이유는 getAnnotation 이 "없으면 null" 을 돌려주는 조회 메서드이기 때문입니다. 어노테이션이 컴파일 결과에는 남아 있어도 실행 중 읽을 수 없으면 코드 입장에서는 처음부터 없던 것과 같습니다.

문제 2

TxProxy 에 실행 시간을 재는 기능을 추가하세요. 모든 메서드에 대해 find 0.12ms 처럼 이름과 밀리초를 출력하되, @Transactional 여부와 무관하게 동작해야 합니다.

정답 보기
java
return Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), (proxy, m, args) -> {
    long t0 = System.nanoTime();
    try {
        Method impl = cls.getMethod(m.getName(), m.getParameterTypes());
        Transactional t = impl.getAnnotation(Transactional.class);
        if (t == null) return invoke(m, target, args);
        tx.begin(t.readOnly());
        try { Object r = invoke(m, target, args); tx.commit(); return r; }
        catch (Throwable e) { tx.rollback(e); throw e; }
    } finally {
        System.out.printf("  %s %.2fms%n", m.getName(), (System.nanoTime() - t0) / 1e6);
    }
});

finally 로 감싸면 예외가 나도 시간이 찍힙니다. Spring 에서는 이 역할을 별도 @Aspect 로 분리하고, 어드바이스 순서를 @Order 로 정합니다.

문제 3

MemberMapper 에 @Select("SELECT * FROM member WHERE name = #{name}") Member findByName(@Param("name") String name); 을 추가하고 실행하세요. FakeDb 가 어떤 예외를 내는지 확인한 뒤, FakeDb.select 를 고쳐 동작하게 하세요.

정답 보기

UnsupportedOperationException: 가짜 DB 가 모르는 SQL: SELECT * FROM member WHERE name = ? 이 납니다. 프록시와 바인딩은 새 메서드에 아무 수정 없이 동작했고, 막힌 곳은 실행기뿐입니다.

java
else if (sql.endsWith("WHERE name = ?")) hit = m.name().equals(params.get(0));

이 한 줄이 실제 MyBatis 에서는 JDBC 드라이버의 몫입니다. 인터페이스에 메서드를 추가하기만 하면 되는 구조가 "매퍼는 구현 클래스가 없다" 의 실제 의미입니다.