홈 › 고급 › 08 / 10

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

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

4. 응용 변형 예제

변형 1: 호출 비용 재기

text
10,000,000회: 직접 0 ms, Method.invoke 154 ms, Proxy 80 ms

직접 호출은 JIT 가 인라인해서 측정이 안 될 만큼 빠릅니다. 리플렉션 호출은 1회에 약 15ns, 프록시는 약 8ns 입니다. DB 한 번이 1ms 이므로 서비스 계층의 프록시 비용은 신경 쓸 수준이 아닙니다. 다만 초당 수천만 번 도는 내부 루프에서는 리플렉션을 피하고 Method 객체를 캐시합니다.

변형 2: 내부 호출 함정 해결

transferAll 이 트랜잭션을 타게 하려면 프록시를 통해 불러야 합니다. 세 가지 방법이 있습니다.

방법 코드 비고
자기 주입 @Autowired MemberService self; 순환 주입, @Lazy 필요
클래스 분리 호출부를 다른 빈으로 옮김 가장 권장
바깥 메서드에도 선언 transferAll 에 @Transactional 경계가 넓어짐

실무에서는 클래스 분리가 가장 깔끔합니다. 컨트롤러가 transfer 를 직접 여러 번 부르거나, TransferFacade 같은 상위 빈에서 MemberService.transfer 를 부르면 매 호출이 프록시를 지납니다.

변형 3: readOnly 힌트

@Transactional(readOnly = true) 는 우리 프록시에서 로그 한 줄 차이입니다. 실제 Spring 은 이 값을 커넥션의 setReadOnly(true) 로 넘기고, JPA 는 변경 감지를 끕니다. MyBatis 만 쓰는 프로젝트에서는 Oracle 드라이버가 힌트로만 쓰므로 효과가 작습니다. 그래도 "이 메서드는 조회" 라는 문서로서 붙이는 가치가 있습니다.

변형 4: 어노테이션으로 검증기 만들기

@NotBlank, @Max(100) 을 필드에 붙이고 리플렉션으로 검사하면 Bean Validation 의 뼈대가 됩니다.

java
for (Field f : dto.getClass().getDeclaredFields()) {
    f.setAccessible(true);
    Max max = f.getAnnotation(Max.class);
    if (max != null && ((Number) f.get(dto)).longValue() > max.value())
        errors.add(f.getName() + " 은 " + max.value() + " 이하");
}

Spring 의 @Valid 도 결국 이 루프입니다. 어노테이션은 규칙을 적어 두는 자리이고, 읽고 판단하는 코드는 따로 있습니다.