스프링 핵심원리 고급편

김파란·2024년 5월 2일

Spring

목록 보기
2/10

1. ThreadLocal

  • 스프링은 기본적으로 싱글톤인데 traceHolder필드를 공유자원으로 쓰기 때문에 동시성 문제가 생긴다
  • 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소가 있다
  • 쓰레드풀을 사용할 경우 ThreadLocal.remove를 하지 않으면 쓰레드의 데이터도 남아있다

2. 템플릿 메서드 패턴

  • 템플릿이라는 틀에 변하지 않는 부분을 몰아둔다 그리고 일부 변하는 부분을 별도로 호출해서 해결
  • 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의
    • 자식 클래스가 알고리즘의 전체구조를 변경하지 않고, 특정 부분만 재정의할 수 있다
    • 상속과 오버라이딩을 통한 다형성으로 문제를 해결한다
  • 상속으로 해결한다

1). 사용

public abstract class AbstractTemplate {
    private static final Logger log = LoggerFactory.getLogger(AbstractTemplate.class);

    public void execute(){
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        call(); // 상속
        // 비즈니스 로직 종료
        long endtime = System.currentTimeMillis();
        long resultTime = endtime - startTime;
        log.info("resultTime = {}", resultTime);

    }

    protected abstract void call();
}
// 사용예시
public class OrderControllerV4 {

    private final OrderServiceV4 orderService;
    private final LogTrace trace;

    @GetMapping("/v4/request")
    public String request(String itemId){
        AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };
        return template.execute("OrderController.request()");
    }

}

3. 전략패턴

  • 상속이 아닌 위임으로 해결한다
  • 필드와 파라미터 방식이 있다
    • 필드는 생성자 방식으로 주입받는다
    • 파라미터는 메서드를 실행할 때마다 주입받는다
// 정의
public interface Strategy {
    void call();

}
// 사용
public class ContextV1 {
    private Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public void execute(){
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        strategy.call();
        // 비즈니스 로직 끝
        long endTime = System.currentTimeMillis();
        long resultTime = startTime - endTime;
        System.out.println(resultTime);
    }
}

// 실사용
new ContextV1(() -> log.info("테스트"); // 이런식으로 사용

상속과 위임

  • 상속
    • 메서드 안에서 자식으로 필요한 로직을 실행한다
    • 문제점: 비즈니스 로직이 템플릿에 강하게 종속된다
  • 위임
    • 비즈니스 로직을 다른곳에서 작성한뒤에 Context에 넣는다

4. 템플릿 콜백 패턴

  • 콜백
    • 다른 코드의 인수로서 넘겨주는 실행 가능한 코드
    • 코드가 호출(call)은 되는데 코드를 넘겨준 곳의 뒤(back)에서 실행 된다는 뜻
  • 스프링에서는 JdbcTemplate 같은 Template에는 콜백패턴으로 만들어져 있다
  • 전략 패턴 중 일부
    • 파라미터 형식으로 받는걸 템플릿 콜백 패턴이라고 한다
// 콜백 패턴
public interface TraceCallback<T> {
    T call();
}

// 템플릿
public class TraceTemplate {
    private final LogTrace trace;

    public TraceTemplate(LogTrace trace) {
        this.trace = trace;
    }

    public <T> T execute(String message, TraceCallback<T> callback) {
        TraceStatus status = null;
        try {
            status = trace.begin(message);
            // 로직 호출
            T result = callback.call();
            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

// 사용
@RestController
public class OrderControllerV5 {
    private final OrderServiceV5 orderService;
    private final TraceTemplate traceTemplate;

    public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
        this.orderService = orderService;
        this.traceTemplate = new TraceTemplate(trace);
    }

    @GetMapping("/v5/request")
    public String request(String itemId) {

        return traceTemplate.execute("orderController", () -> {
            orderService.orderItem(itemId);
            return "OK";
        });

    }
}

5. 프록시, 프록시 패턴, 데코레이터 패턴

  • 인터페이스 기반 프록시
    • 인터페이스 기반 프록시는 상속이라는 제약에서 자유롭다
    • 프로그램 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 좋다
  • 클래스 기반 프록시
    • 해당 클래스에만 적용할 수 있다
    • 상속을 사용하기 때문에 몇가지 제약이 있다
      • 부모 클래스의 생성자를 호출해야 한다
      • 클래스에 final 키워드가 붙으면 상속이 불가능하다
      • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다
  • 문제는 클래스마다 프록시를 만들어야 한다

1). 프록시

  • 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 대신 간접적으로 서버에 요청
  • 대리자를 프록시라고 한다
  • 프록시 패턴이나 데코레이터 패턴은 프록시를 이용하지만 의도에 따라서 구분한다
    • 프록시 패턴
      • 접근 제어가 목적
    • 데코레이터 패턴
      • 새로운 기능 추가가 목적
  • 장점
    • 간접호출을 하면 대리자가 중간에서 여러가지 일을 할 수 있다
    • 접근 제어, 캐싱, 지연로딩

2). 프록시 패턴

  • 접근 제어하기 위해 대리자를 제공
    • 캐싱
// 인터페이스
public interface Subject {
    String operation();
}

// 프록시
@Slf4j
public class CacheProxy implements Subject {

    private Subject target;
    private String cacheValue;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("프록시 호출");
        if (cacheValue == null) {
            cacheValue = target.operation();
        }
        return cacheValue;
    }
}

// 실제 객체
@Slf4j
public class RealSubject implements Subject {
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 프록시 패턴 시작
public class ProxyPatternClient {

    private Subject subject;

    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.operation();
    }
}

// 실행
public class ProxyPatternTest {

    @Test
    void cacheProxyTest() {
        RealSubject realSubject = new RealSubject();
        CacheProxy cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
        client.execute();
        client.execute();
        client.execute();
    }
}

3). 데코레이터 패턴

  • 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
// 인터페이스
public interface Component {
    String operation();
}

// 데코레이터 패턴
@Slf4j
public class MessageDecorator implements Component {

    private Component component;

    public MessageDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info("MessageDecorator 실행");

        //data -> *****data*****
        String result = component.operation();
        String decoResult = "*****" + result + "*****";
        log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
        return decoResult;
    }
}

// 실제 객체
@Slf4j
public class RealComponent implements Component {
    @Override
    public String operation() {
        log.info("RealComponent 실행");
        return "data";
    }
}

// 실행
@Slf4j
public class DecoratorPatternClient {

    private Component component;

    public DecoratorPatternClient(Component component) {
        this.component = component;
    }

    public void execute() {
        String result = component.operation();
        log.info("result={}", result);
    }
}

// 시작
@Slf4j
public class DecoratorPatternTest {

    @Test
    void decorator1() {
        Component realComponent = new RealComponent();
        Component messageDecorator = new MessageDecorator(realComponent);
        DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
        client.execute();
    }
}

6. 동적프록시

  • 프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법
  • 자바는 기본적으로 JDK 동적 프록시랑 CGLIB 같은 프록시 기술을 제공한다

1). JDK 동적 프록시

  • 인터페이스 기반 프록시
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
    private final Object target; // 동적 프록시가 호출할 대상

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        long endTime = System.currentTimeMillis();
        log.info("TimeProxy 종료 resultTime={}", endTime - startTime);
        return result;
    }
}

// 사용
@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA(){
        AInterface target = new AImpl();

        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        // 어디에 생성될지 클래스 로더 지정, 어떤 인터페이스 기반인지, handler
        AInterface proxy = (AInterface)Proxy.newProxyInstance(
                AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("ProxyClass={}", proxy.getClass());
    }
}

2). CGLIB

  • Code Generator Libaray
  • 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
  • 구체 클래스로 동적 프록시
public class TimeMethodInterceptor implements MethodInterceptor {
    private final Object target;

    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        Object result = methodProxy.invoke(target, args); // method.invoke보다 더 빠름 똑같은 기능

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("timeProxy 종료  resultTIme={}", resultTime);
        return result;
    }
}
 @Test
    void cglib(){
        ConcreteService target = new ConcreteService();
        // CGLIB를 만드는 코드
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ConcreteService.class);
        enhancer.setCallback(new TimeMethodInterceptor(target));
        ConcreteService proxy = (ConcreteService) enhancer.create();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }

7. 프록시 팩토리

  • JDK, CGLIB를 동적으로 적용
    • 프록시가 제공하는 기능을 Advice라고 한다
    • Advice를 통해 호출하면 된다
    • Advice로 target을 호출한다
  • 조건에 맞을때만 프록시 로직을 적용
    • PointCut으로 적용여부 선택
  • 프록시 만드는건 프록시 팩토리
  • 부가기능 적용은 Advice

스프링 기본 설정

  • 스프링은 기본적으로 proxyTargetClass=true로 설정해서 사용한다
  • 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체클래스 기반으로 프록시를 생성한다
// Advice 생성
@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        // 타겟을 호출하고 그 결과를 받는다
        // 프록시 팩토리를 생성하는 단계에서 타겟을 넘긴다
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        log.info("TimeProxy 종료 resultTime={}", endTime - startTime);
        return result;
    }
}

// 사용
@Slf4j
public class ProxyFactoryTest {
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() throws Exception {
        // given
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());
        // 인터페이스여도 CGLIB로 하고 싶을때
        proxyFactory.setProxyTargetClass(true);

        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        proxy.save();

        // 프록시 팩토리로 만든 프록시에만 적용
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
}

1). 포인트컷, 어드바이스, 어드바이저

  • 포인트컷
    • 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터링 로직
    • 대상 여부를 확인하는 필터 역할만 담당한다
  • 어드바이스
    • 프록시가 호출하는 부가 기능
    • 부가 기능 로직만 담당한다
  • 어드바이저
    • 하나의 포인트컷과 하나의 어드바이스

정리

  • 조언(Advice)를 어디(PointCut)에 할것인가
  • 조언자(Advisor)는 어디(PointCut)에 조언(Advice)을 해야할지 알고 있다
  • 프록시에 맞춰서 적었다. AOP랑은 조금 다름

2). 직접 만든 포인트컷

public class AdvisorTest {
    @Test
    void advisorTest1(){
        ServiceImpl target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        // Advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());

        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }

    @Test
    @DisplayName("직접 만든 포인트컷")
    void advisorTest2(){
        ServiceImpl target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        // Advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointCut(), new TimeAdvice());

        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }

    static class MyPointCut implements Pointcut{
        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE;
        }

        @Override
        public MethodMatcher getMethodMatcher() {
            return new MyMethodMatcher();
        }
    }

    @Slf4j
    static class MyMethodMatcher implements MethodMatcher{

        private String matchName = "save";

        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            boolean result = method.getName().equals(matchName);
            log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
            log.info("포인트컷 결과 result={}",result);
            return result;
        }

        // 이게 참이면 밑에 있는 matches가 불려진다
        // args가 있으면 캐싱이 안돼서 false로 하는편
        @Override
        public boolean isRuntime() {
            return false;
        }

        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args) {
            return false;
        }
    }
}

3). 스프링에서 제공하는 포인트컷

  • NameMatchMethodPointCut
    • 메서드 기반 매칭
    • PatternMatchUtils를 사용한다. *사용 가능
  • JdkRegexpMethodPointcut
    • JDK 정규 표현식 기반으로 포인트컷 매칭
  • TruePointcut
    • 항상 참을 반환
  • AnnotationMatchingPointcut
    • 애노테이션으로 매칭한다
  • AspectJExpressionPointcut
    • aspectJ 표현식으로 매칭한다
@Test
    @DisplayName("스프링에서 제공하는 포인트컷")
    void advisorTest3(){
        ServiceImpl target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);

        // PointCut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("save");
        // Advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());

        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }

0개의 댓글