반응형
반응형

1. 개요

 스프링 AOP를 이해하고, 빈 후처리기를 활용해 AOP를 구현해보자.

 


2. AOP

2.1. AOP가 뭔가요?

 AOP는 말 그대로 Aspect(관점) Oriented(지향) Programming(프로그래밍). 관점 지향적인 프로그래밍이다. 풀어 말하면 어떤 로직에 대해 핵심 기능과 부가 기능이라는 관점으로 나누어 모듈화하는 프로그래밍 기법을 말한다. 토비의 스프링에도 비슷하게 정의되어 있어 가져와봤다.

 

AOP란 어플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법이다.

 

 예를들어 @Transactional을 사용하지 않고 DB 데이터 처리를 하는 특정 메서드에 대한 트랜잭션 기능을 부여한다면, 아래와 같은 코드를 구현할 수 있다.

    public void upgradeLevels() {
        TransactionStatus status = transactionManager.getTransaction(
                new DefaultTransactionDefinition()
        );

        try{
            userService.upgradeLevels(); // 핵심기능
            transactionManager.commit(status);
        }catch(Exception e){
            transactionManager.rollback(status);
        }
    }

 

 

 현재는 upgradeLevels() 메서드에 대해서만 트랜잭션을 적용하였으나 다른 메서드에도 적용해야한다면 아래와 같이 많은 중복코드가 생길것이다.

    public void upgradeLevels() {
        TransactionStatus status = transactionManager.getTransaction(
                new DefaultTransactionDefinition()
        );

        try{
            userService.upgradeLevels();
            transactionManager.commit(status);
        }catch(Exception e){
            transactionManager.rollback(status);
        }
    }
    
    public void saveUserInfo() {
        TransactionStatus status = transactionManager.getTransaction(
                new DefaultTransactionDefinition()
        );

        try{
            userService.saveUserInfo();
            transactionManager.commit(status);
        }catch(Exception e){
            transactionManager.rollback(status);
        }
    }
    
    ...
    
    public void update() {
        TransactionStatus status = transactionManager.getTransaction(
                new DefaultTransactionDefinition()
        );

        try{
            userService.update();
            transactionManager.commit(status);
        }catch(Exception e){
            transactionManager.rollback(status);
        }
    }
    
    ...

 

 중복을 없애려면 어떻게 해야할까? 중복되는 트랜잭션 로직을 분리하고, 다른 클래스에서도 재사용할 수 있도록 모듈화 해야한다. 이 말은 UserService의 메서드와 트랜잭션 로직을 각각 핵심기능과 부가기능으로 분리하고, 부가기능을 모듈화 해야 한다는 것인데. 이렇게 접근하는 프로그래밍 방식이 AOP, 관점 지향 프로그래밍이라고 할 수 있다.

 

2.2. AOP 용어

 AOP 사용 시 자주 사용되는 용어들이 있다. 이 중에서도 스프링 AOP에서 주로 사용되는 Advice, Pointcut, Advisor는 꼭 숙지하자.

 

Target 부가 기능을 부여할 대상이다. 핵심기능을 담은 클래스일 수도 있지만 경우에 따라 다른 부가기능을 제공하는 프록시 오브젝트일 수도 있다.
Advice 타겟에게 제공할 부가 기능을 담은 모듈이다. 어드바이스는 오브젝트로 정의하기도 하지만 메서드 레벨에서 정의할 수도 있다
JoinPoint 어드바이스가 적용될 수 있는 위치를 말한다. 스프링 AOP에서 조인포인트는 메서드의 실행단계 뿐이다. 타깃 오브젝트가 구현한 인터페이스의 모든 메서드는 조인 포인트가 된다.
Pointcut  어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다. 스프링 AOP의 조인포인트는 메서드의 실행이므로 스프링의 포인트컷은 메서드를 선정하는 기능을 갖고 있다. 그래서 포인트컷 표현식에서도 메서드의 시그니처를 비교하는 방법을 주로 사용한다. 메서드는 클래스 안에 존재하는 것이기 때문에 메서드 선정이란 결국 클래스를 선정하고 그 안의 메서드를 선정하는 과정을 거치게 된다.
Advisor  어드바이저와 포인트컷을 하나씩 갖고 있는 오브젝트이다. 어드바이저는 어떤 기능(어드바이스)을 어디에(포인트컷) 전달할 것인가를 알고 있는 AOP의 가장 기본이 되는 모듈이다. 어드바이저는 스프링 AOP에서만 사용되는 용어이고, 일반적인 AOP에서는 사용되지 않는다.

 

* JoinPoint와 Pointcut이 헷갈려요

더보기

예를 들어서 살펴보면 좀 더 쉬운데요. MemberService의 hello()라는 메소드 실행 전,후에 hello랑 bye를 출력하는 일을 한다고 가정해보죠. 이때 MemberService 빈이 타겟, "hello() 메소드 실행 전,후"가 포인트컷, "메소드 실행 전,후"라는게 조인포인트, "hello랑 bye를 출력하는 일"이 Advice입니다. 포인트컷과 조인포인트가 많이 햇갈릴텐데 조인포인트가 메타적인 정보라고 생각하시면 되고 포인트컷이 좀 더 구체적인 적용 지점이라고 생각하시면 됩니다.

- 인프런 문의사항 답변 내용 (답변자 : 백기선님)

 


3. 빈 후처리기를 통한 AOP

스프링에서는 AOP를 위한 다양한 모듈을 제공한다. 일단 빈 후처리기를 활용하여 AOP를 적용해보도록 하겠다.

 

3.1. 빈 후처리기가 뭔가요?

 BeanPostProcessor 인터페이스를 구현한 클래스로 빈을 생성한 후 후처리 기능을 하는 클래스이다.

 스프링의 대표적인 빈 후처리기는 DefaultAdvisorAutoProxyCreator로 등록된 빈 중에서 Advisor 인터페이스를 구현한 클래스에 대한 자동 프록시 생성 후처리기이다.

 이를 활용하면 스프링이 생성하는 빈 오브젝트 중 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 그림과 함께 동작과정을 이해해보자.

 

3.2. DefaultAdvisorAutoProxyCreator 동작과정

DefaultAdvisorAutoProxyCreator 동작과정

 

1) 어플리케이션이 시작되면 빈 설정파일을 읽어 빈 오브젝트를 생성한다.

 

2) BeanPostProcessor 인터페이스를 구현한 클래스(DefaultAdvisorAutoProxyCreator)가 빈으로 등록되어 있다면 생성된 빈을 여기로 전달한다.

 

3) DefaultAdvisorAutoProxyCreator는 생성된 빈 중에서 Advisor 인터페이스를 구현한 클래스가 있는지 스캔한다.

 

4) Advisor 인터페이스를 구현한 클래스가 있다면 Advisor의 포인트 컷을 통해 프록시를 적용할지 선별한다.

 

5) Advisor가 없거나 포인트 컷 선별이 되지 않았다면 전달받은 빈을 그대로 스프링 컨테이너에게 전달하고, 선별됐다면 프록시 생성기 역할을 하는 객체에서 프록시 생성 요청을 한다.

 

6) 프록시 생성기는 프록시를 생성하고 프록시에 어드바이저를 연결한다.

 

7) 완성된 프록시를 스프링 컨테이너에게 전달한다.

 

8) 스프링 컨테이너는 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용한다.

 

3.3. DefaultAdvisorAutoProxyCreator 예제

  UserService 인터페이스에 대한 구현체 클래스는 UserServiceImpl은 DB 데이터 처리를 하는 IUserDao와 비지니스 로직을 담당하는 UserLevelUpgradePolicy를 DI받는다. 

 UserServiceImpl 메서드 실행 도중 예외가 발생할 경우 모든 트랜잭션을 rollback하기 위해 트랜잭션 처리를 하는 부가기능을 빈 후처리기를 통해 구현해보도록 하자. 매커니즘을 이해하는 것에 초점을 맞췄기 때문에 부가적인 코드는 첨부하지 않도록 하겠다.

 

1) UserService

public interface UserService {

    void upgradeLevels();
    void add(User user);
}

 

2) UserServiceImpl

public class UserServiceImpl implements UserService {

    private IUserDao userDao;

    private UserLevelUpgradePolicy userLevelUpgradePolicy;
    
    public void setUserDao(IUserDao userDao){
        this.userDao = userDao;
    }

    public void setUserLevelUpgradePolicy(UserLevelUpgradePolicy userLevelUpgradePolicy){
        this.userLevelUpgradePolicy = userLevelUpgradePolicy;
    }

    public void upgradeLevels() {
        List<User> users = userDao.getAll(); // DB /
        for(User user : users) {
            if (userLevelUpgradePolicy.canUpgradeLevel(user)) {
                userLevelUpgradePolicy.upgradeLevel(user);
            }
        }
    }

    public void add(User user) {
        if(user.getLevel() == null){
            user.setLevel(Level.BASIC);
        }
        userDao.add(user);
    }
}

 

3.3.1. 빈 후처리기 등록

 DefaultAdvisorAutoProxyCreator를 빈으로 등록하면 된다. xml 설정을 통해 빈을 등록하였다.

 

<bean class ="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>

 

 

3.3.2. 포인트컷 정의

 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 클래스를 모두 찾는다. 구현 클래스를 생성하기 전, Advisor에 필요한 포인트컷과 어드바이스를 준비해야 한다. 먼저 포인트컷을 생성하였다.

 

public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {

    public void setMappedClassName(String mappedClassName){
        this.setClassFilter(new SimpleClassFilter(mappedClassName));
    }

    static class SimpleClassFilter implements ClassFilter {

        String mappedName;

        private SimpleClassFilter(String mappedName){
            this.mappedName = mappedName;
        }

        public boolean matches(Class<?> clazz){
            return PatternMatchUtils.simpleMatch(mappedName,
                    clazz.getSimpleName());
        }
    }
}

 

  포인트컷은 NameCatchMethodPointcut내부 익명 클래스 방식으로 확장해서 만들었다. 이름에서 알 수 있듯이 메서드 선별 기능을 가진 포인트컷인데, 클래스에 대해서는 필터링 기능이 없는게 아닌 모든 클래스를 다 허용한는 기본 클래스 필터가 적용되어 있다. 때문이 이 클래스 필터를 재정의 하였다.

 

StaticMethodMatcherPointcut 의 기본 classFilter
TrueClassFilter.INSTANCE

Canonical instance of a ClassFilter that matches all classes
 : 모든 클래스와 일치하는 ClassFilter의 정식 인스턴스

 

주석을 보면 알 수 있듯이 기본 클래스 필터인 TrueClassFilter.INSTANCE는 모든 클래스와 일치시킨다.

 

 

3.3.3. 어드바이스 정의

 이제 부가기능을 포함하는 어드바이스를 정의해보겠다. MethodInterceptor 인터페이스를 구현하면 된다. invoke 메서드에 부가기능 및 타겟 오브젝트 호출 로직을 넣어준다.

public class TransactionAdvice implements MethodInterceptor {

    private PlatformTransactionManager transactionManager;

    public void setTransactionManager(PlatformTransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }


    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TransactionStatus status = transactionManager
                .getTransaction(new DefaultTransactionDefinition());

        try{
            Object ret = invocation.proceed(); //타겟 메서드 실행
            transactionManager.commit(status);
            return ret;
        } catch (RuntimeException e){
            transactionManager.rollback(status);
            throw e;
        }
    }
}

 

3.3.4. 어드바이저 등록

 어드바이저는 스프링에서 제공하는 DefaultPointcutAdvisor를 사용한다.

앞서 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 클래스를 모두 찾는다고 했는데 DefaultPointcutAdvisor 클래스가 Advisor 인터페이스의 구현체 클래스 중 하나이다. 

 어드바이저 등록은 'XML을 통한 빈 설정' 부분에 기재하였다.

 

3.3.5. XML을 통한 빈 설정

 이제 작업한 내용을 바탕으로 스프링 빈 설정을 한다. 어드바이저에 대한 자동 프록시 생성 후처리기인 DefaultAdvisorAutoProxyCreator 빈을 등록하고, 스캔할 어드바이저로 DefaultPointcutAdvisor 타입의 transactionAdvisor 빈을 등록한다. 생성 시 필요한 어드바이스와 포인트컷도 마찬가지로 빈으로 등록해줬다.

 필자는 클래스 이름의 suffix가 ServiceImpl인 클래스, 메서드 이름의 prefix가 upgrade 인 메서드에 대해 포인트컷을 설정하기 위해 mappedClassName엔 "*ServiceImpl"를, mappedName엔 "upgrade*" 를 설정해주었다.

	...
    
    <bean id = "userService" class = "org.example.user.service.UserServiceImpl">
        <property name="userDao" ref = "userDao"></property>
        <property name="userLevelUpgradePolicy" ref = "defaultUserLevelUpgradePolicy"></property>
    </bean>
    
    ...
    
    <!-- 빈 후처리기 등록 -->
    <bean class ="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

    <!-- 어드바이스 설정 -->
    <bean id = "transactionAdvice" class = "org.example.proxy.TransactionAdvice">
        <property name="transactionManager" ref = "transactionManager"></property>
    </bean>
    
    <!-- 포인트컷 설정 -->
    <bean id = "transactionPointcut" class = "org.example.proxy.NameMatchClassMethodPointcut">
        <property name="mappedClassName" value = "*ServiceImpl"/>
        <property name="mappedName" value = "upgrade*"/>
    </bean>

    <!-- 어드바이저 (어드바이스 + 포인트컷) 설정 -->
    <bean id = "transactionAdvisor" class = "org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref = "transactionAdvice"></property>
        <property name="pointcut" ref ="transactionPointcut"></property>
    </bean>
    
    ...

 

3.3.6. 테스트

 게시글에 누락시킨 클래스들이 많아 테스트 케이스를 이해하기 힘든 관계로 간단히 설명하자면, 모든 유저들에 대해 정해진 조건을 만족할 경우 다음 레벨로 업그레이드하는 UserService의 upgradeLevels()를 메서드를 테스트하며, 메서드 실행 도중 예외발생 시 트랜잭션이 적용되는지 확인하기 위함이다.

 

 테스트를 위해 upgradeLevels 내부에서 실행되는 UserLevelUpgradePolicy의 upgradeLevel() 메서드에 대해 예외가 발생하도록 Stubbing 처리하였다.

 

 test2 유저의 경우 SIVER로 업그레이드 됐었으나, 커밋 전 예외 발생으로 인해 BASIC 레벨로 롤백되는지 확인하는 케이스를 진행하였고, 트랜잭션이 적용되어 테스트가 성공함을 확인하였다.

public class UserServiceTest {

    @Autowired
    private UserService userService;

    @SpyBean
    private IUserDao userDao;

    @SpyBean
    private DefaultUserLevelUpgradePolicy userLevelUpgradePolicy;

    private final List<User> users = Arrays.asList(
            new User("test1","테스터1","pw1", Level.BASIC, 49, 0, "tlatmsrud@naver.com"),
            new User("test2","테스터2","pw2", Level.BASIC, 50, 0, "tlatmsrud@naver.com"),
            new User("test3","테스터3","pw3", Level.SILVER, 60, 29, "tlatmsrud@naver.com"),
            new User("test4","테스터4","pw4", Level.SILVER, 60, 30, "tlatmsrud@naver.com"),
            new User("test5","테스터5","pw5", Level.GOLD, 100, 100, "tlatmsrud@naver.com")
    );
    @BeforeEach
    void setUp(){

        // 모든 유저 조회 시 미리 정의한 유저 텍스처 조회
        given(userDao.getAll()).willReturn(users);

        // 4번째 유저에 대한 업그레이드 메서드 실행 시 예외 발생
        willThrow(new RuntimeException()).given(userLevelUpgradePolicy).upgradeLevel(users.get(3));
    }


    @Test
    void upgradeAllOrNothing(){
        // 테이블 데이터 초기화
        userDao.deleteAll();

        // 테이블에 유저 정보 insert
        users.forEach(user -> userDao.add(user));

        // 유저 레벨 업그레이드 메서드 실행 및 예외 발생 여부 확인 (setUp 메서드에 4번째 유저 업그레이드 처리 시 예외 발생하도록 스터빙 추가)
        assertThatThrownBy(() -> userService.upgradeLevels())
                .isInstanceOf(RuntimeException.class);

        // DB
        assertThat(userDao.get("test1").getLevel()).isEqualTo(Level.BASIC);
        assertThat(userDao.get("test2").getLevel()).isEqualTo(Level.BASIC);
        assertThat(userDao.get("test3").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test4").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test5").getLevel()).isEqualTo(Level.GOLD);

        System.out.println(userService.getClass().getName()); //com.sun.proxy.$Proxy50

    }
}

 

추가적으로 Autowired한 userService의 클래스 타입은 자동 프록시 생성 빈 후처리기에 의해 Proxy 객체가 생성된 관계로 UserServiceImpl가 아닌 Proxy임을 확인할 수 있었다. 

포인트컷과 일치하여 생성된 프록시 빈

 

만약 포인트컷에 선별되지 않도록 mappedName 혹은 mappedClassName을 변경한다면 프록시를 생성하지 않고 아래와 같이 UserServiceImpl 타입으로 출력되는 것도 확인할 수 있었다.

포인트컷과 일치하지 않아 그대로 리턴된 빈

 


4. 세밀한 포인트컷

 

4.1. 리플렉션 API 활용?!

 예제에서는 단순히 클래스나 메서드의 이름으로만 포인트컷을 지정했는데, 더 세밀하고 복잡한 기준을 적용해 포인트컷을 지정할 수도 있다. 바로 리플렉션 API를 활용하는 것이다. 어차피 TransactionAdvice의 invoke 메서드 파라미터인 MethodInvocation도 리플렉션이 적용된 파라미터이기 때문에 메서드나 클래스, 리턴 값 등 대부분의 정보를 얻을 수 있기 때문이다.

invoke 메서드의 invocation 파라미터

 

 하지만 리플렉션 API를 사용하면 코드가 지저분해지고 포인트컷 비교 정보가 달라질때마다 해당 로직을 수정해야한다. 이에 스프링은 표현식을 통해 간단하게 포인트컷의 클래스와 메서드를 선별할 수 있도록 하는 방법을 제공하는데 이를 포인트컷 표현식이라고 한다.

 

4.2. 포인트컷 표현식

 포인트컷 표현식을 지원하는 포인트컷을 적용하려면 포인트컷 빈으로 AspectExpressionPointcut 클래스를 사용하면 된다. 

 

4.3. 포인트컷 표현식 문법

 포인트컷 표현식은 포인트컷 지시자를 이용하여 작성하며 대표적으로 execution()이 있다. 메서드의 풀 시그니처를 문자열로 비교하는 개념이며, 문법은 아래와 같다. 참고로 괄호([ ])안은 생략 가능하다.

 

포인트컷 표현식

 

 예를들어 execution("* org.test.service.*ServiceImpl.upgrade*(..)) 는 모든 접근제한자 및 리턴타입을 갖고, ServiceImpl로 끝나는 클래스 명을 갖고, 메서드명이 upgrade로 시작하는 모든 메서드 시그니처를 의미한다.

 

4.4. AspectExpressionPointcut 적용해보기

 포인트컷 표현식 사용을 위한 의존성을 추가하고, xml에 설정했던 포인트컷 빈을 수정해보자.

 

1) 의존성 추가

implementation 'org.aspectj:aspectjtools:1.9.19'

 

2) xml 설정 변경

<bean id = "transactionPointcut" class = "org.springframework.aop.aspectj.AspectJExpressionPointcut">
    <property name="expression" value = "execution(* org.example.user.service.*ServiceImpl.upgrade*(..))"/>
</bean>

 기존엔 포인트컷에 대한 클래스를 생성해주었지만, AspectJExpressionPointcut을 사용하니 그럴 필요가 없게 되었다. 테스트 코드를 실행해보면 테스트가 성공하는 것을 확인할 수 있다.

 

4.5. 스프링 AOP 정리

 스프링의 AOP를 적용하려면 최소한 네 가지 빈을 등록해야 한다.

 

* 자동 프록시 생성기

 스프링의 DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록한다. 빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성하는 기능을 담당한다.

 

* 어드바이스

 부가기능을 구현할 클래스를 빈으로 등록한다. TransactionAdvice는 AOP 관련 빈 중 유일하게 직접 구현한 클래스이다.

 

* 포인트컷

 스프링의 AspectJExpressionPointcut을 빈으로 등록하고 포인트컷 표현식을 넣어주면 된다. 

 

* 어드바이저

 스프링의 DefaultPointcutAdvisor를 빈으로 등록한다. 어드바이스와 포인트컷을 참조하는 것 외에는 기능이 없다. 자동 프록시 생성기에 의해 검색되어 사용된다.

 


5. AOP 네임스페이스

 포인트컷 표현식을 사용하니 어드바이스를 제외하고는 모두 스프링에서 제공하는 클래스를 사용하고 있다. 스프링에서는 이렇게 AOP를 위해 기계적으로 적용해야하는 빈들을 간편하게 등록하는 방법을 제공한다. 바로 aop 스키마를 이용하는 것이다.

 aop 스키마를 사용하려면 bean xml 설정파일에 aop 네임 스페이스 선언을 추가해줘야 한다.

 

aop 네임스페이스 추가

 

 이를 추가하면 빈 후처리기, 포인트컷, 어드바이저가 자동으로 등록되므로 xml 설정에서 제거할 수 있다. 이제 aop 네임 스페이스를 사용하여 bean 설정 xml을 아래와 같이 작성할 수 있다. 단위 테스트를 통해 트랜잭션이 적용됨을 확인할 수 있었다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
>

   ...
   
    <!-- 빈 후처리기, 포인트컷, 어드바이저 빈 생성 코드 제거
    <bean class ="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
    
    <bean id = "transactionPointcut" class = "org.example.proxy.NameMatchClassMethodPointcut">
        <property name="mappedClassName" value = "*ServiceImpl"/>
        <property name="mappedName" value = "upgrade*"/>
    </bean>

    <bean id = "transactionAdvisor" class = "org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref = "transactionAdvice"></property>
        <property name="pointcut" ref ="transactionPointcut"></property>
    </bean>
    -->

    <bean id = "transactionAdvice" class = "org.example.proxy.TransactionAdvice">
        <property name="transactionManager" ref = "transactionManager"></property>
    </bean>

    <aop:config>
        <aop:advisor advice-ref="transactionAdvice"
                     pointcut="execution(* org.example.user.service.*ServiceImpl.upgrade*(..))"></aop:advisor>
    </aop:config>
</beans>

 

반응형
반응형
반응형

1. 개요

 다이내믹 프록시에 대해 이해한다.

 


2. 프록시란?

 먼저 프록시가 뭘까? 프록시에 대해 알아보기 전 이전 스터디때 배웠던 UserService의 트랜잭션 기능을 회고해보았다.

 

2.1. UserService의 트랜잭션 기능 회고

 비지니스 로직에 대해 얼마의 처리 시간이 걸렸는지에 대한 로깅처리, 트랜잭션 처리와 같은 부가 기능을 추가하기 위해 인터페이스를 통한 추상화 방법을 사용할 수 있다. 앞선 스터디에서 UserService에 트랜잭션 기능을 추가하기 위해 인터페이스를 통한 추상화 방법을 적용하였고, 아래와 같이 부가기능과 핵심기능을 분리하는 구조가 되었다.

UserService에 대한 Transaction 기능 추가 추상화

 

 

2.2. '핵심인 척' 하는 '부가기능'

 이러한 구조가 가지는 중요한 특징이 있는데, 부가 기능 수행 후 핵심 기능을 가진 클래스로 요청을 위임해줘야 한다는 것과, 핵심기능은 부가기능을 가진 클래스의 존재 자체를 몰라야 한다는 것이다. 존재 자체를 모르는 것은 런타임시 DI를 통한 추상화 때문이다.

 클라이언트도 인터페이스를 통해 서비스를 호출하기 때문에 실제로 어떤 구현체를 사용하는지 모른다. 핵심기능을 호출한다고 생각할 뿐이다. 부가기능(UserServiceTx)을 통해 핵심기능(UserServiceImpl)을 사용하고 있는데 말이다. 즉, 부가기능 클래스(UserServiceTx)는 의도치않게 클라이언트를 속여 '핵심인 척' 하고있다.

 

2.3. 그래서 프록시가 뭔가요?

 이렇듯 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 요청을 받아주는 오브젝트를 프록시라고 한다. 추가로 프록시를 통해 최종적으로 요청을 위임받아 핵심 기능을 처리하는 오브젝트를 타겟이라고 한다. 앞서 구성했던 Client, UserServiceTx, UserServiceImpl 구조를 아래와 같이 프록시와 타겟으로 표현할 수 있다.

프록시 관점으로 본 UserService 구조

 

2.4. 프록시의 목적

 프록시의 목적은 크게 두가지이다. 첫째는 클라이언트가 타겟에 접근하는 방법을 제어하기 위해서, 두 번째는 타겟에 부가적인 기능을 부여하기 위해서이다. 각각의 목적에 따라 디자인 패턴에서는 다른 패턴으로 구분한다.

 

2.5. 데코레이터 패턴

 데코레이터 패턴은 타겟에 부가적인 기능을 런타임 시 다이나믹하게 부여하기 위해 프록시를 사용하는 패턴이다. 핵심 기능은 그대로 두고 부가 기능, 즉 데코레이션만 추가하는 것이다.

 부가기능은 하나일수도, 여러개일수도 있다. 때문에 이 패턴에서는 프록시가 한 개로 제한되지 않는다.

 UserService 인터페이스를 구현한 타겟인 UserServiceImpl에 트랜잭션 부가기능을 제공하는 UserServiceTx를 추가한 것도 데코레이터 패턴을 적용한 것이다.

 데코레이터 패턴은 인터페이스를 통해 위임하는 방식이기 때문에 어느 데코레이터에서 타겟으로 연결될지는 코드 레벨에선 알 수 없다. DI 설정에 따라 다이나믹하게 구성되게 때문이다.

 이 패턴은 타겟 코드의 수정도 없고, 클라이언트 호출 방법도 변경하지 않은 채 새로운 기능을 추가할 때 유용한 방법이다.

 

2.5. 프록시 패턴

 프록시 패턴의 프록시는 타겟에 대한 접근 방법을 제어하는 의미의 프록시이다. 즉, 클라이언트가 타겟에 접근하는 방식을 변경해주는 패턴이다.

 클라이언트에게 타겟에 대한 래퍼런스를 넘길 때 실제 타겟이 아닌 프록시를 넘겨주는 것이다. 그리고 프록시의 메서드를 통해 타겟을 사용하려고 시도하면, 그때 프록시가 타겟 오브젝트를 생성하고 요청을 위임해준다.

 이 방식의 장점은 해당 객체가 메모리에 존재하지 않아도 프록시를 통해 정보를 참조할 수 있고, 타겟이 반드시 필요한 시점까지 타겟 객체의 생성을 미뤄 메모리를 사용 시점을 늦출 수 있다.

 

2.6. 프록시는 어디에 쓰나요?

 프록시는 기존 코드에 영향을 주지 않으면서 기능을 확장하거나 접근 방법을 제어한다. 그럼에도 불구하고 많은 개발자는 타겟 코드를 고치고 말지 번거롭게 프록시를 만들지는 않는다고 한다.

 그 이유는 프록시를 만드려면 부가 기능을 추가하고자 할때마다 새로운 클래스를 정의해야 하고, 인터페이스의 구현 메서드가 많다면 모든 메서드에 일일히 구현하고 위임하는 코드를 넣어야 한다.

 UserService가 아닌 ProductService가 있다고 가정하고, 여기에 트랜잭션 부가기능을 처리하는 프록시를 통해 구성한다고 하자. ProductServiceTx 클래스를 만들고, 구현해야할 메서드마다 트랜잭션 로직과 핵심 기능을 담당하는ProductServiceImpl를 만들어 위임하는 로직을 넣어야 한다. 메서드 양이 많아지면 작업량도 많아지고, 중복코드도 많아진다.

 이러한 문제점들을 해결하여 좀더 간단하게 프록시를 구성하는 방법이 있을까? 있다! 그게 바로 다이나믹 프록시이다.

 


3. 다이나믹 프록시

3.1. 다이나믹 프록시란?

 다이니믹 프록시는 런타임 시점프록시를 자동으로 만들어서 적용해주는 기술이다. 자바에서는 리플렉션 기능을 사용해서 프록시를 만드는 JDK 다이나믹 프록시를 사용한다.

 

 

3.2. 리플렉션이란?

 리플렉션에 대한 내용은 따로 포스팅하였다. 다이나믹 프록시의 기반 기술이기도 하지만 스프링 프레임워크의 기반 기술이기도 하기에 이 개념에 대해 잘 모른다면 이해해보길 권장한다.

 

https://tlatmsrud.tistory.com/112

 

String name = "Spring";

 

 위 문자열의 길이를 알고 싶다면 어떻게 할까? 단순히 length 메서드를 호출해도 되지만 리플렉션 기능을 사용해도 된다.

 

Method lengthMethod = String.class.getMethod("length");

 

 리플렉션을 통해 String 클래스의 length라는 메서드 정보를 가져온다. 메서드를 실행시키려면 Method.invoke() 메서드를 사용하면 된다.

int length = lengthMethod.invoke(name); // == name.length()

 

 아래는 간단한 리플렉션 테스트이다.

@Test
public void invokeMethod() throws Exception{
    String name = "Spring";

    assertThat(name.length()).isEqualTo(6);

    Method lengthMethod = String.class.getMethod("length");
    assertThat((Integer)lengthMethod.invoke(name)).isEqualTo(6);

    assertThat(name.charAt(0)).isEqualTo('S');
    Method charAtMethod = String.class.getMethod("charAt", int.class); // method name, parameterType ...
    assertThat((Character) charAtMethod.invoke(name,0)).isEqualTo('S');
}

 

결론은 리플렉션을 사용하면 런타임시에 클래스에 대한 정보를 가져올 수 있고, 클래스의 메서드를 호출할 수 있는 것이다. 그럼 이 클래스의 메서드를 호출하기 전, 후에 부가기능을 추가한다면 앞서 배웠던 프록시 클래스를 만들어 적용했던 것과 동일하게 동작시킬수도 있는 것이다.

 

 

3.3. 프록시 클래스

 프록시 객체를 이용한 방법과 다이나믹 프록시를 이용한 방법의 차이를 느끼기 위해 먼저 데코레이터 패턴을 적용해보자. 패턴을 만들기 위해 타겟 클래스와 인터페이스, 부가기능 클래스를 정의했다.

 

3.2.1. Hello.java

 프록시 인터페이스 클래스이다.

public interface Hello {
    String sayHello(String name);
    String sayHi(String name);
    String sayThankYou(String name);
}

 

3.2.2. HelloTarget.java

 핵심 기능을 처리하는 타겟 오브젝트이다.

public class HelloTarget implements Hello{

    @Override
    public String sayHello(String name) {
        return "Hello "+name;
    }

    @Override
    public String sayHi(String name) {
        return "Hi "+name;
    }

    @Override
    public String sayThankYou(String name) {
        return "Thank You "+name;
    }
}

 

3.2.3. Test.java

 간단한 테스트 코드이다.

@Test
public void simpleProxy(){
    Hello hello = new HelloTarget(); // 타깃은 인터페이스를 통해 접근
    assertThat(hello.sayHello("Sim")).isEqualTo("Hello Sim");
    assertThat(hello.sayHi("Sim")).isEqualTo("Hi Sim");
    assertThat(hello.sayThankYou("Sim")).isEqualTo("Thank You Sim");
}

 

일단은 부가기능은 넣지 않고 타겟클래스와 인터페이스만 사용하였다. 이제 문자열을 대문자로 치환하는 부가기능을 추가한 프록시를 만들어보자.

 

3.2.4. HelloUppercase.java

public class HelloUppercase implements Hello{

    private final Hello hello;
    public HelloUppercase(Hello hello){
        this.hello = hello;
    }

    @Override
    public String sayHello(String name) {
        return hello.sayHello(name).toUpperCase();
    }

    @Override
    public String sayHi(String name) {
        return hello.sayHi(name).toUpperCase();
    }

    @Override
    public String sayThankYou(String name) {
        return hello.sayThankYou(name).toUpperCase();
    }
}

 멤버 필드로 정의된 Hello는 타겟 클래스로 HelloTarget을 참조하도록 해야한다.

 

3.2.5. Test.java

@Test
public void simpleProxy(){
    Hello hello = new HelloTarget(); // 타겟 오브젝트 생성
    Hello proxyHello = new HelloUppercase(hello); // 프록시 오브젝트 생성 및 의존성 주입
    assertThat(proxyHello.sayHello("Sim")).isEqualTo("HELLO SIM");
    assertThat(proxyHello.sayHi("Sim")).isEqualTo("HI SIM");
    assertThat(proxyHello.sayThankYou("Sim")).isEqualTo("THANK YOU SIM");
}

 이로써 데코레이터 패턴을 적용한 프록시 구조가 만들어졌다. 부가 기능을 하는 HelloUppercase 코드를 보면 프록시의 문제점인 부가기능(대문자로 치환)과 위임(hello.method) 코드가 중복되는 것을 알 수 있다.

 이제 다이나믹 프록시를 적용하여 이 문제를 해결해보자.

 

3.3. 다이나믹 프록시 적용

 다이나믹 프록시는 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트이다. 다이나믹 프록시의 오브젝트는 타겟의 인터페이스와 같은 타입으로 자동으로 만들어진다. 다이나믹 프록시는 오브젝트를 타겟 인터페이스를 통해 사용할 수 있다.

 부가기능 제공 코드는 InvocationHandler를 구현한 오브젝트로 생성해야 한다.

 

3.3.1. UppercaseHandler.java

 타겟 메서드를 실행한 결과 값이 String이거나 해당 메서드명이 say로 시작할 경우 대문자로 변환하는 부가기능을 추가하였다.

public class UppercaseHandler implements InvocationHandler {

    private Object target;

    public UppercaseHandler(Object target){
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = method.invoke(target, args); // invoke를 통해 target 오브젝트의 method 실행

        if(ret instanceof String && method.getName().startsWith("say")){ // 문자열 타입이거나 say로 시작하는 메서드일 경우
            return ((String)ret).toUpperCase();
        }
        return ret;
    }
}

 

3.3.2. Test.java

 Hello 인터페이스에 대한 부가기능을 UppercaseHandler로 설정함과 동시에 타겟 오브젝트를 HelloTarget으로 설정하였다. 데코레이터 패턴에서 부가기능을 처리하기 위해 구현했던 HelloUppercase 클래스가 필요하지 않게 되었고, 이 클래스가 안고 있던 부가기능 로직과 위임 로직의 중복이 모두 해결되었다.

@Test
public void dynamicProxy(){
    Hello proxyHello = (Hello) Proxy.newProxyInstance(
            getClass().getClassLoader(), // 다이나믹 프록시를 정의하는 클래스 로더
            new Class[] {Hello.class}, // 다이나믹 프록시가 구현해야할 인터페이스
            new UppercaseHandler(new HelloTarget())); // 부가기능 및 위임 코드를 담는 InvocationHandler 구현 클래스

    assertThat(proxyHello.sayThankYou("Sim")).isEqualTo("THANK YOU SIM");
}

 


4. 트랜잭션 기능 리팩토링

 이제 트랜잭션 기능을 넣기 위해 사용했던 전략 패턴 대신 다이나믹 프록시를 사용하도록 변경해보자. 먼저 부가기능을 처리하는 InvocationHandler 구현체 클래스를 구현하자.

 

4.1. TransactionHandler.java

public class TransactionHandler implements InvocationHandler {
    private Object target;
    private PlatformTransactionManager transactionManager;
    private String pattern;

    public void setTarget(Object target){
        this.target = target;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }

    public void setPattern(String pattern){
        this.pattern = pattern;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 메서드 명이 pattern 으로 시작할 경우
        if(method.getName().startsWith(pattern)){
            return invokeInTransaction(method, args); // 트랜잭션 기능과 함께 메서드 실행
        }
        return method.invoke(target, args); // 트랜잭션 기능 없이 메서드 실행
    }

    public Object invokeInTransaction(Method method, Object[] args) throws Throwable {

        // 트랜잭션 생성
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition());

        try{
            // 메서드 실행
            Object ret = method.invoke(target, args);

            // 트랜잭션 commit
            this.transactionManager.commit(status);
            return ret;
        } catch (InvocationTargetException e) {

            // 타겟 메서드 실행 중 예외 발생 시 트랜잭션 rollback
            this.transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

 메서드 명이 pattern으로 시작할 경우 트랜잭션 기능 사이에 타겟 메서드를 호출하는 invokeInTransaction() 메서드를 호출한다. 내부에서는 리플렉션을 사용하여 타겟 오브젝트에 대한 메서드를 실행한다. 예외가 발생할 경우 롤백이, 발생하지 않을 경우 커밋이 된다.

 그리고 다이나믹 프록시를 통한 타겟 메서드 호출 시 예외가 발생할 경우 InvocationTargetException 예외 안에 타겟 메서드에서 발생한 예외가 포장되므로 이에 맞게 예외 처리 로직을 수정한다.

 

4.2. 테스트

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class DynamicProxyUserServiceTest {

    @Autowired
    private UserServiceImpl userService;

    @Autowired
    private PlatformTransactionManager transactionManager;

    @SpyBean
    private IUserDao userDao;

    @SpyBean
    private DefaultUserLevelUpgradePolicy userLevelUpgradePolicy;

    private final List<User> users = Arrays.asList(
            new User("test1","테스터1","pw1", Level.BASIC, 49, 0, "tlatmsrud@naver.com"),
            new User("test2","테스터2","pw2", Level.BASIC, 50, 0, "tlatmsrud@naver.com"),
            new User("test3","테스터3","pw3", Level.SILVER, 60, 29, "tlatmsrud@naver.com"),
            new User("test4","테스터4","pw4", Level.SILVER, 60, 30, "tlatmsrud@naver.com"),
            new User("test5","테스터5","pw5", Level.GOLD, 100, 100, "tlatmsrud@naver.com")
    );
    @BeforeEach
    void setUp(){
        userService.setUserDao(userDao);
        userService.setUserLevelUpgradePolicy(userLevelUpgradePolicy);

        given(userDao.getAll()).willReturn(users);

        willThrow(new RuntimeException()).given(userLevelUpgradePolicy).upgradeLevel(users.get(3));
    }

    @Test
    void upgradeAllOrNothingWithNoProxy(){

        // 테이블 데이터 초기화
        userDao.deleteAll();

        // 테이블에 유저 정보 insert
        users.forEach(user -> userDao.add(user));

        // 유저 레벨 업그레이드 메서드 실행 및 예외 발생 여부 확인 (setUp 메서드에 4번째 유저 업그레이드 처리 시 예외 발생하도록 스터빙 추가)
        assertThatThrownBy(() -> userService.upgradeLevels())
                .isInstanceOf(RuntimeException.class);

        // DB
        assertThat(userDao.get("test1").getLevel()).isEqualTo(Level.BASIC);
        assertThat(userDao.get("test2").getLevel()).isEqualTo(Level.SILVER); // 트랜잭션이 적용되지 않아 BASIC 레벨로 롤백되지 않음.
        assertThat(userDao.get("test3").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test4").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test5").getLevel()).isEqualTo(Level.GOLD);

    }

    @Test
    void upgradeAllOrNothingWithProxy(){

        // 부가기능 핸들러 객체 생성
        TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTarget(userService);
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern("upgradeLevels");

        // 다이나믹 프록시 생성
        UserService proxyUserService = (UserService) Proxy.newProxyInstance(
                getClass().getClassLoader() // 다이나믹 프록시 클래스의 로딩에 사용할 클래스 로더
                ,new Class[] {UserService.class} // 구현할 인터페이스
                ,txHandler // 부가 기능과 위임 코드를 담은 InvocationHandler
        );

        // 테이블 데이터 초기화
        userDao.deleteAll();

        // 테이블에 유저 정보 insert
        users.forEach(user -> userDao.add(user));

        assertThatThrownBy(() -> proxyUserService.upgradeLevels())
                .isInstanceOf(RuntimeException.class);

        assertThat(userDao.get("test1").getLevel()).isEqualTo(Level.BASIC);
        assertThat(userDao.get("test2").getLevel()).isEqualTo(Level.BASIC); // 트랜잭션이 적용되지 않아 BASIC 레벨로 롤백됨.
        assertThat(userDao.get("test3").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test4").getLevel()).isEqualTo(Level.SILVER);
        assertThat(userDao.get("test5").getLevel()).isEqualTo(Level.GOLD);
    }
}

 users 필드에 설정한 테스트 픽스처에 대해 두가지 테스트를 진행하도록 하였다.

upgradeAllOrNothingWithNoProxy 테스트는 다이나믹 프록시를 적용하지 않은 케이스로 UserService의 구현체 클래스를 직접 호출하고 있어 트랜잭션이 적용되지 않는다. upgradeAllOrNothingWithProxy는 다이나믹 프록시를 적용한 케이스로 트랜잭션이 적용되어 있다.

 

  setUp 메서드에서  4번째 유저의 업그레이드 시 예외가 발생하도록 스터빙 처리하였기에, 첫번째 테스트 케이스에서는 예외가 발생해도 이전 업그레이드 처리 된 유저에 대해서 롤백이 되지 않는지를 체크했고, 두번째 테스트 케이스에서는 예외가 발생할 경우 롤백이 되는지를 체크했다. 테스트 결과는 성공적이었다.

테스트 결과

 

반응형
반응형
반응형

1. 개요

 - 업무를 하던 도중 특정 로직에서 Enhancer, MethodInterceptor이라는 객체를 사용하고 있었다. 로직을 분석해봐도 이해가 잘 가지 않았던 구조였기 때문에 공부의 필요성을 느껴 공부 후 내용을 정리한다.


2. Enhancer란?

 - Enhancer의 사전적 의미는 기능을 높이는 것, 증진시키는 것을 의미한다. 이를 프록시에 대입하여 생각해보니 프록시 객체는 다양한 객체를 호출할 수 있기때문에 기능이 증진된다는 것과 비슷한 맥락을 갖는 것 같다.

 - 로직적으로 이 객체는 프록시 객체를 생성하는 역할을 한다.


3. Enhancer를 사용한 프록시 객체 생성

 - Enhancer 객체를 사용하여 프록시 객체를 생성하는 예제이다.

 

3.1) EnhancerTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ssk.simpany.test.methodInterceptorTest;
 
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.NoOp;
 
public class EnhancerTest {
 
    public static void main(String[] args) {
        
        Enhancer enhancer = new Enhancer(); //Enhancer 객체 생성
        
        enhancer.setSuperclass(MyService.class); // 타켓 클래스 지정
        enhancer.setCallback(NoOp.INSTANCE);      // 옵션  (NoOp, MethodIntetceptor 등)
        Object targetObj = enhancer.create();     // 프록시 객체 생성
        
        if(targetObj instanceof MyService){           // targetObj 객체가 MyService로 형 변환이 가능한지
            //형변환 가능.
            MyService myService = (MyService)targetObj; //형변환
            myService.myServiceMethod("test");
        }
    }
}
 
cs

 

3.2) MyService.java

1
2
3
4
5
6
7
8
9
package com.ssk.simpany.test.methodInterceptorTest;
 
public class MyService {
 
    public String myServiceMethod(String a){
        System.out.println("call my ServiceMethod is "+a);
        return a;
    }
}
cs

 

- EnhancerTest 클래스에서 프록시 객체를 생성할 수 있는 Enhancer 객체를 생성하고 setSuperclass 메서드를 사용하여 타겟 클래스를 지정한다.

- setCallback 메서드에는 NoOp.INSTANCE(NoOption)을 입력했는데, 말 그대로 옵션이 없는 프록시객체로 설정하기 위함이다.

- NoOp로 설정하면 단순히 프록시 객체를 통해 타겟 클래스만 호출하는 것이고, MethodInterceptor로 설정하면 타겟 클래스 호출 전 후로 로직을 넣거나 매개변수를 변경하는 등의 작업이 가능하다. 후자의 경우도 다뤄보도록 할 예정이다.

- enhancer.create() 메서드를 호출하면 마침내 프록시 객체를 생성하게 된다.

- Object 형으로 반환되게 되지만 내부적으로는 앞서 설정한 타켓 클래스이기 때문에 instanceof를 사용하여 형 변환 또한 가능함을 확인할 수 있다. 결과적으로 enhance 에서 생성한 프록시 객체를 통해 MyService객체를 호출한다.

 

3.3) 결과

프록시 객체를 통한 MyService 호출 결과

 

  사실 위 예제를 직접 작성했을땐 '그냥 객체 생성하고 호출하면 될것이지 왜 프록시까지 굳이 생성해서 호출하는걸까?' 라는 생각과 함께 비지니스 로직에 의해 동적으로 호출되어야 할 객체가 있다면 이를 사용했을 때 어느정도 효과를 볼 수 있지 않을까라는 생각도 들었다. 타겟 객체를 생성하는 게 아닌 호출하는 것이므로 메모리적으로도 좋지않을까? 혹시 누군가 알고계시다면 댓글로 달아주시길 부탁드리와요..


4. MethodInterceptor란?

 프록시 객체의 콜백 기능 중 하나로, 프록시 객체에서 타겟 객체를 호출하기 전, 후로 비지니스 로직을 추가하거나, 타겟 클래스가 아닌 다른 객체의 메소드를 호출하거나, 인자 값을 변경할 수 있다.

 구현 방법은 커스텀한 클래스에 MethodInterceptor 인터페이스를 상속받은 후 intercept 메서드를 오버라이드 한다.

 

4.1) MethodInterceptorTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ssk.simpany.test.methodInterceptorTest;
 
import java.lang.reflect.Method;
 
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
 
public class MethodInterceptorTest implements MethodInterceptor{
 
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("before target Proxy");
        Object returnValue = methodProxy.invokeSuper(object, args);
        System.out.println("after target Proxy");
        return returnValue;
    }
}
cs

 

4.2) Main.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ssk.simpany.test.methodInterceptorTest;
 
import org.springframework.cglib.proxy.Enhancer;
 
public class Main {
 
    public static void main(String[] args) {
        
        Enhancer enhancer = new Enhancer();     // enhancer 객체 생성
        enhancer.setSuperclass(MyService.class);// 타겟 클래스 지정
        enhancer.setCallback(new MethodInterceptorTest()); //콜백으로 MethodInterceptorTest 객체 설정
        Object targetObj = enhancer.create();    //프록시 생성
        MyService myService = (MyService)targetObj; //형변환
        myService.myServiceMethod("test"); //프록시 객체로 메서드 호출
    }
}
 
cs

- MethodInterceptor를 사용하여 아주 간단한 프록시 객체를 구현했다. 기존과 다른 건 MethodInterceptorTest 클래스를 구현한 후 callback 으로 설정했다는 점이다.

- 이 설정으로 인해 프록시 객체로 메서드를 호출하면 MethodInterceptorTest 클래스의 intercept 메서드가 호출되게 되어 before target Proxy 문자열 호출 후 타겟 클래스가 실행됨을 확인할 수 있다.

참고로 methodProxy.invokeSuper의 리턴 값은 타겟 객체의 리턴 값이다.

 

4.3) 결과

MethodInterceptor를 적용한 프록시 객체 호출 결과


5. 마치며

 지금은 단순히 enhancer와 MethodInterceptor를 이용해 프록시를 구현하는 것만 해보아서 그런지 느낌만 알 뿐, 아직도 머릿속에서 정리가 되지 않은 느낌이다. 또한 어떻게 활용되는지도 크게 와닿지 않는다. 다음 게시글로 이 방식을 활용하여 다양한 예제를 만들어보도록 하겠다.

반응형
반응형
반응형

1. 개요

 maven 프로젝트에 스프링 시큐리티를 적용하던 도중, 특정 bean이 주입되지 않아 NullPointException 에러가 발생했다. 참고로 해당 Bean은 xml 설정파일을 통해 등록된 상태였다.

 결론부터 말하면 문제가 발생한 Bean(SqlSession)은 WebApplicationContext로 설정되어있었고, 해당 Bean을 호출하는 Bean(SpringSecurity)은 RootApplicationContext로 설정되어 있어서 발생한 문제였다.

그렇다면 RootApplicationContext, WebApplicationContext 는 무엇이며, 어떻게 이와같이 설정이 나뉘게된 것일까?


2. RootApplicationContext와 WebApplicationContext 

2.1. RootApplicationContext

말 그대로 최상위 ApplicationContext이다.WebApplicationContext의 부모 Context이며 자식에게 자신의 설정을 공유한다. 단, 자신은 자식인 WebApplicationContext의 설정에 접근하지 못한다.

 

2.2. WebApplicationContext

Servlet 단위의 ApplicationContext이다. RootApplicationContext의 자식 Context이며, 부모인 RootApplicationContext의 설정에 접근할 수 있다.

 

아래 이미지가 이를 잘 설명해주고 있다.

출처 : https://howtodoinjava.com/spring-mvc/contextloaderlistener-vs-dispatcherservlet/

 

나의 경우 스프링 시큐리티 관련 객체에서 SqlSession을 Autowired 받아 DB 조회를 하는 로직을 구현하였는데, 스프링 시큐리티 설정은 RootApplicationContext으로, SqlSession은 WebApplicationContext로 설정했었다.

 

즉, RootApplicationContext에서 WebApplicationContext에 있는 Bean인 SqlSession을 주입하려고 했던 것이다. 당연히 접근이 되지 않아 주입이 되지 않았고, NullPointException이 발생했던 것이었다.

 

그렇다면 어떤 설정이 RootApplicationContext와 WebApplicationContext로 나누게 한 것일까? 그게 바로 ContextLoaderListener이다.


3. ContextLoaderListener

ContextLoaderListener는 RootApplicationContext를 생성하는 클래스이다.

빨간 블록안에 contextConfigLocation 설정파일을 읽어 RootApplicationContext를 생성한다.

 

참고로 파란색 블록안에 contextConfigLocation 설정파일을 읽어 생성되는 것이 WebApplicationContext이다.

web.xml

 즉, applicationContext.xml과 security-context.xml을 로드하여 RootApplicationContext를 생성하고,

 servlet-context.xml과 mybatis-context.xml을 로드하여 WebApplicationContext를 생성했다.

 결과적으로 스프링 시큐리티 관련 bean은 rootApplicationContext에, sqlSession bean은 WebApplicationContext에 설정되었으며, 스프링 시큐리티 서비스 객체(rootApplicationContext)에서 sqlSession(WebApplicationContext)를 Autowired 하지 못해 발생한 문제였다.

 

 

ContextLoaderListener 설정으로 인해 의존 주입이 되지 않아 발생한 에러임을 확인하고 ContextLoaderListener에 대해 포스팅하려고 했으나, 오히려 이 부분에 대한 설명이 적은 것 같아 아쉽다. ㅠㅠ

 

반응형
반응형
반응형

1. 개요

 교재를 보며 스프링 부트 환경에서 MVC 패턴을 사용하여 통신 테스트를 하던 도중 예기치 못한 상황이 발생했다.

Controller에서 String 형태로 View의 이름을 반환하려 했으나, 실제 반환된 것은 뷰가 아닌 스트링 자체였다.

test 를 리턴했을 때 test.html 리소스가 반환되는 것이 아닌 test 문자열 자체가 반환된 것이다. 코드를 확인해 보니 이 두 어노테이션을 적절히 사용하지 못해 발생한 문제였다.

 

2. @Controller

 Controller 어노테이션을 사용 시 일반적으로 View Resolver에 설정한 값 기준으로 return 하는 값과 일치하는 View 를 찾아 반환한다. @ResponseBody 어노테이션을 사용할 시 데이터 자체를 반환할 수 있으며, JSON 또는 String 형태로 값을 반환할 수 있다.

 

3. @RestController

 @Controller + @ResponseBody 이다. 해당 컨트롤러에서 View를 리턴하지 않고, REST API를 사용한다면 이 설정이 적절하다.

 

만약 Controller 통신 후 view 페이지를 예상했는데 view 이름만 달랑 있는 페이지가 나오거나, 반대 상황이 나온다면 컨트롤러의 어노테이션을 확인해보자.

 

반응형
반응형
반응형

1. 개요

 JUnit 을 사용하는 테스트 클래스에는 RunWith, ContextConfiguration 어노테이션이 붙는다. 책에서 RunWith는 스프링와 JUnit 간 인터페이스 역할을, ContextConfiguration은 스프링 컨텍스트 설정파일을 읽는 역할을 한다고 하나 크게 와닿지 않았다.

 이런 마음을 갖고 공부를 하던 중 저 의미를 이해하게 되어 글을 남긴다.

 

2. @RunWith

 RunWith(SpringJUnit4ClassRunner.class)는 말 그대로 SpringJUnit4ClassRunner.class를 실행한다는 것이고, 이 클래스는 내부적으로 스프링 컨테이너를 생성해준다.

 

3. @ContextConfiguration

 생성된 스프링 컨테이너에 스프링 빈을 추가하기 위해서는 application-context.xml 파일과 같은 설정 파일을 읽어야 하는데, 이런 설정파일을 로드하는 어노테이션이 ContextConfiguration이다.

 만약 스프링 컨테이너가 필요 없다면, 즉, 스프링 빈 팩토리에서 빈을 로드하는 것이 아닌, 직접 new로 객체를 생성해가며 테스트 코드를 작성할 것이라면 위의 어노테이션을 제거해도 된다.

 

4. 결론

 JUnit 테스트에 스프링 컨테이너를 사용할거면 위의 어노테이션을 넣어주자.

 

반응형
반응형

1. 개요

실무 투입한지 거의 1년이 다 되어갈 무렵, web.xml 코드를 보던 중 문득 이런 생각이 들었다.

'웹 애플리케이션의 첫 단추인 web.xml에 대해 누군가에게 설명할 수 있을까?'  

1년간 일은 열심히 했는데 가장 기본적인것에 대한 공부가 전혀 안되있음을 느껴, 공부 후 포스팅을 한다.

참고로 web.xml에 대한 예제는 MVC 패턴 기준으로 작성했다.


2. 정의

web.xml은 DD (Deployment Descriptor : 배포 설명자)라고 불리며, Web Application의 설정파일이다.

DD는 Web Application 실행 시 메모리에 로드된다.

즉, web.xml이란 웹 어플리케이션을 실행시킬 때 함께 올라가야할 설정(설명)들을 정의해놓은 것이다.

그렇다면 web.xml에는 어떤 설정을 할까?


3. 설정

Web.xml 에서는 크게 DispatcherServlet, ContextLoaderListener, Filter 설정을 한다.

 

클라이언트의 요청을 처리하는 DispatcherServlet.

웹 어플리케이션 컨텍스트 단위의 설정을 로드하는 ContextLoaderListener,

이건 꼭 거쳤으면 좋겠네. Filter.

 

3.1) DispatcherServlet

DispatcherServlet은 클라이언트의 요청을 전달받는 객체이다. 하는 일은? 당연히 클라이언트의 요청을 처리하는 일이다. 그럼 어떻게 처리할까?

 

클라이언트의 요청을 처리하려면 크게 4가지 일이 진행되어야 한다.

 

첫째, 클라이언트의 요청을 처리해줄 컨트롤러를 찾는다.

둘째, 컨트롤러를 실행시킨다. (비지니스 로직 처리)

셋째, 클라이언트에게 보여질 View를 찾는다.

넷째, 응답 데이터와 View를 클라이언트에게 전달한다.

 

요청을 처리할 컨트롤러를 찾는 일은 Handler Mapping이라는 객체가 처리한다. 이 객체는 클라이언트의 요청 경로를 이용해서 컨트롤러를 검색하고, 검색된 객체를 DispatcherServlet에게 리턴한다.

만약 클라이언트가 'http://~~/test' 를 요청할 경우 /test를 처리할 컨트롤러 객체를 리턴하는 것이다. 

 

컨트롤러를 실행시키는 일은 Handler Adapter라는 객체가 처리한다. 이 객체는 @Controller 어노테이션을 이용해 구현한 컨트롤러 뿐만 아니라, Controller 인터페이스를 구현한 컨트롤러, 특수 목적으로 사용되는 HttpRequestHandler 인터페이스를 구현한 클래스를 동일한 방식으로 실행할 수 있도록 만들어졌다. (출처 : 스프링5 입문. 최범균)

즉, 컨트롤러 실행 업무에 특화된 객체로 Controller를 실행하는 것이다.

Controller가 실행되면 개발자가 구현한 비지니스 로직을 거쳐 응답 데이터가 추출된다. 실행된 Controller는 리턴할 데이터와 View를 Handler Adapter에게 리턴한다.

Handler Adapter는 데이터와 view를 ModelAndView 형태로 DispatcherServlet에게 리턴한다.

 

여기서 view는 단순히 view의 이름이다. 뭔 소리냐면 이 이름에 해당하는 실제 view를 찾아야한다는 것이다.

클라이언트에게 보여질 view를 찾는 일은 ViewResolver 객체가 처리한다.

ViewReolver bean 객체 설정

위는 viewResolver 객체를 설정하는 부분인데 설정된 prefix(접두사), suffix(접미사)를 참조하여 처리한다.

만약 Controller에서 리턴한 view 이름이 hello였다면 /WEB-INF/view/hello.jsp를 찾아 dispatcher Servlet에게 리턴한다.

 

최종적으로 viewResolver가 리턴한 View 객체에 응답 데이터를 넣어 클라이언트에게 리턴한다.

 

이처럼 클라이언트의 요청은 DispatcherServlet라는 감독관(?)이 처리한다. 다만 직접 처리하지 않고 적절한 객체들에게 일을 위임하여 처리하고 있다.

 

3.2) ContextLoaderListener

앞서 Dispatcher Servlet은 클라이언트의 요청을 처리하는 객체라고 설명했다. 웹 어플리케이션의 규모가 커진다면, 클라이언트의 요청또한 다양해질 것이고, 이를 처리할 Dispatcher Servlet도 늘어날 가능성이 있다. 다른 성격을 가진 서블릿이 생성될 것이고, 설정 또한 서블릿의 성격에 맞게 각각 적용시켜야 한다.

반면에, 모든 서블릿이 공통으로 가져야할 설정들도 있다. 즉 Servlet Context 단위가 아닌 Application Context 단위의 설정이 필요한데 이를 ContextLoaderListener 객체가 처리한다.

이 객체는 Application Context 단위의 설정을 생성한다.

참고로 Application Context 는 Web Application 의 Context이며, 모든 Servlet들이 참조가 가능한 부모 Context이다.

 

3.3) Filter

클라이언트에서 온 요청을 Dispatcher Servlet이 받기 전 거치는 부분이 있다. 바로 이 Filter 객체이다.

만약 스프링 시큐리티 필터가 적용되어 있다면, 인가 및 인증 처리를 먼저 처리하고, 인코딩 필터가 적용되어 있다면 클라이언트의 요청데이터를 인코딩하는 작업이 선 처리된 후 Dispatcher Servlet에게 필터링 된 데이터가 전달된다.


4. 예제

이제 나같은 코린이가 흔히 봤던... 그저 작성만 했던... web.xml 코드를 분석해보자

 

4.1) web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
 
 <!-- Dispatcher Servlet 생성 -->
 <servlet>
     <servlet-name>myDispatcherServlet</servlet-name>
     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
     <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>classpath:/config/servlet-config.xml</param-value>
     </init-param>
     
     <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
     <servlet-name>myDispatcherServlet</servlet-name>
     <url-pattern>/</url-pattern>
 </servlet-mapping>
 
 <!-- web application context -->
 <listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 <context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>
         /WEB-INF/config/application-context.xml
     </param-value>
 </context-param>
 
 <!-- Encoding Filter 생성 -->
 <filter>
     <filter-name>encodingFilter</filter-name>
     <filter-class>
         org.springframework.web.filter.CharacterEncodingFilter
     </filter-class>
     <init-param>
         <param-name>encoding</param-name>
         <param-value>UTF-8</param-value>
     </init-param>
     <init-param>
         <param-name>forceEncoding</param-name>
         <param-value>true</param-value>
     </init-param>
 </filter>
 <filter-mapping>
     <filter-name>encodingFilter</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>
</web-app>
cs

분석 전 다시 되새겨보겠다.

첫째, web.xml은 DD(Deploy Descriptor, 배포 설명자)이다.

둘째, DD는 Web Application 실행 시 메모리에 로드된다.

셋째, web.xml에는 크게 dispatcherServlet, contextLoaderListener, filter 를 설정한다.

 

만약 tomcat이라는 WAS(Web Application Server)를 통해 이 web Application을 실행시킨다고 가정하면, web.xml 파일에 설정한 내용들이 메모리에 로드될 것이다.

 

본격적으로 코드를 분석해보자.

 

7 ~ 16 line - 클라이언트의 요청을 처리하는 Dispatcher Servlet을 myDispatcherServlet이란 이름으로 생성하고, 이 서블릿에 대한 설정파일로 servlet-config.xml을 지정한다. (controller 스캔용, 코드는 글의 최하단에 첨부)

 

17 ~ 20 line - 설정한 Dispatcher Servlet이 처리할 url-pattern을 설정한다. '/' 경로로 들어오는 모든 요청에 대해서 myDispatcherServlet이 처리를 담당한다.

 

23 ~ 31 line - web application context 단위의 설정파일로 application-context.xml을 설정한다. 이 설정은 servelt으로 생성한 myDispatcherServlet에게 공유된다. (view-resolver 설정용, 코드는 글의 최하단에 첨부)

 

34 ~ 47 line - 스프링에서 지원하는 encoding Filter를 filter에 추가한다.

 

48 ~ 51 line - encoding Filter가 처리할 url-pattern을 설정한다. '/*' 모든 경로에 대해 인코딩 필터를 적용한다.

 

정리하면 이 예제 web.xml은 클라이언트의 요청을 처리할 인코딩 필터와 Dispatcher Servelt을 생성하고, view Resolver를 web application context 단위로 설정하였다.

 

> servlet-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
 
 
    <context:component-scan base-package="controller"/>
    
</beans>
 
cs

 

> application-context.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>
 
cs

 

반응형
반응형
반응형

목차

1. 개요

2. 환경

3. 라이브러리

4. 예제

5. 실행결과


1. 개요

 log4j2 = log lib

 log를 남기는 이유는 여러 가지가 있는데 대표적으로 [에러 추적, 디버깅] 또는 [통계]나 [기록]을 목적으로 해!

 어쨌든간에 어렵지 않은 내용이니 한번 따라 해 보자고~

 So~~~~~~~ Ez.


2. 환경

이클립스 MARS
JDK 1.7
comcat 7 (그저갓)

3. 라이브러리

 이거 다 받아서 프로젝트의 lib폴더에 넣자. 프로젝트를 컴파일하면 lib폴더에 있는 jar파일을 class파일로 만들어 사용하기 때문에 여기다 넣어도 된단다! 예외가 몇 개 있지만 요 녀석들은 해당되지 않아! 어서 다운로드하으렴

log4j-api-2.0.2.jar
0.12MB
log4j-core-2.0.2.jar
0.75MB
log4j-over-slf4j-1.7.7.jar
0.02MB
log4j-slf4j-impl-2.0.2.jar
0.02MB
log4j-web-2.0.2.jar
0.02MB
slf4j-api-1.7.7.jar
0.03MB
slf4j-ext-1.7.7.jar
0.04MB


4. 예제

 다운로드한 파일을 프로젝트 폴더의 lib(경로는 WEB-INF/lib) 폴더에 넣었다면 본격적인 예제를 작성해볼 건데, 그냥 로그를 뿌리는 것보다 뭔가 상황이 주어지면 맛깔날 것 같아서 대충 시나리오를 준비했어.

 ※ 시나리오

  1) 나는 서버 개발자

  2) x초보 서버 개발자

  3) controller가 아닌! Servlet을 이용.

  4) 클라이언트가 Servlet에 설정된 Url로 요청

  5) log출력.

 

웹 프로젝트는 생성했다고 가정하고 post, get 통신을 받는 서블릿 패키지 및 클래스를 만들어보자.

 

  4.1) 서블릿 생성

  File -> New -> Servlet 클릭

  나는 패키지 이름을 one, 클래스 이름을 andTwo로 설정했어.

서블릿 패키지 및 클래스 생성

   4.2) andTwo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package one;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
 
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
 
/**
 * Servlet implementation class andTwo
 */
@WebServlet("/andTwo")
public class andTwo extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private Logger logger = LogManager.getLogger(andTwo.class);
    /**
     * Default constructor. 
     */
    public andTwo() {
        // TODO Auto-generated constructor stub
    }
 
    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        response.getWriter().append("Served at: ").append(request.getContextPath());
        logger.error("error message");
        logger.warn("warn message");
        logger.info("info message");
        logger.debug("debug message");
        logger.trace("trace message");
    }
 
    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        response.getWriter().append("Served at: ").append(request.getContextPath());
        logger.error("error message");
        logger.warn("warn message");
        logger.info("info message");
        logger.debug("debug message");
        logger.trace("trace message");
    }
 
}
 
 

코드 설명 대충 하자면 

18번째 줄은 요청 url이야. 그림으로 설명해줄게

포트정보
context path

서버 탭을 자세히 살펴보면 자신의 포트와 contextPath 정보를 확인할 수 있어.

서버를 기동시 키면 http://localhost:port/contextPath 가 루트 url이 되는 거야.

요청 url인 '/andTwo'는 루트 url을 포함한 형태라고 생각해.

정리하면 http://localhost:8080/log/andTwo 경로로 요청이 들어오면 해당 경로와 mapping 된 서블릿에 정의된 코드를 처리한다는 뜻이야.

 

21번째 줄에 logger 객체 LogManger.getLogger() 메서드를 통해 생성해주었어. getLogger의 파라미터 값에 현재 클래스명. class를 넣어줘

 

32번째 줄은 Get방식으로 온 요청에 대한 처리.

 

45번째 줄은 Post방식으로 온 요청에 대한 처리이고, 각각 error, warn, info, debug, trace 로그를 남기게 돼. 이들을 로그 레벨이라고 하고 자세한 건 구글링을 통해 알아보면 좋을 것 같아.

 

간단하게 몇 개만 설명하면

 error는 시스템에 문제를 발생시킬만한 로그정보

 warn는 시스템에 문제가 생기진 않지만 에러를 야기할 수 로그정보

 debug는 디버깅을 위해 남기는 로그정보

 

 상황에 맞게 이 로그들을 사용해야 한다는데.. 나는 아직도 어려워..

 어쨌든 이 서블릿으로 요청이 들어오면 무조건 error ~ tarce까지 로그를 출력하도록 했어.

 이제 log4j2의 설정 파일을 생성해야하는데 생성하기 전에 log4j2의 설정파일 경로를 지정해놓자. 이 작업은 web.xml파일에서 해.

 

 4.3) web.xml 파일 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>log</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  <context-param>
    <param-name>log4j2ConfigLocation</param-name>
    <param-value>/WEB-INF/log4j2.xml</param-value>
  </context-param>
</web-app>
 

display-name은 프로젝트명.

welcome-file-list는 프로젝트가 컴파일되면 최초로 보이는 파일 list.

만약 WEB-INF에 이 파일이 없다면? 404 에러가 뜰 거야. 하지만 우리는 저런 html 파일이 필요 없기 때문에 만들지 않아도 돼

 

context-param은 서버의 parameter를 설정해주는 부분이야.

log4 jConfigLocation이라는 변수에 /WEB-INF/log4j2.xml이라는 값을 넣었다고 생각하면 편해.

 

log4j2는 기본적으로 로그에 대한 설정 파일인 log4j2.xml이 필요한데 이 녀석의 경로야.

 

(사실 이 부분이 없어도 되는데 이유는 log4j2는 로드가 될때 설정 파일인 log4j2.xml을 classpath: 경로에서 알아서 찾는다고 하네..)

 

이제 log4j2 설정파일 세팅을 해보자.

 

 4.4) log4j2.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <!-- 로그 출력 방식 -->
    <Appenders>
        <!-- 콘솔 출력 방식 -->
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
        </Console>
        
        <!-- 파일 저장 방식 -->
        <File name="file" fileName="C:/Users/mcnc/Desktop/20191203.log">
              <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
        </File>
    </Appenders>
    
    <Loggers>
        <!-- one 패키지의 'andTwo 클래스의 로그 레벨은 info로 올리고', file로 저장할래 -->
        <!-- additivity는 같은 로그가 있다면 additivity가 설정되어있는 로거에서만 출력하도로 설정하는거야.
        참고로 모든 로거에는 기본 로그가 남는데 아래처럼 따로 설정한 로그 방식을 AppenderRef 해준다면 기본 로그+Append 로그. 총 2개의 로그가 남아.
        기본로그는 최소의 정보만 들어있기때문에 이를 막아야겠지? 로그 자체는 동일하기때문에 additivity를 false로 해 주면 기본로그는 뜨지 않을거야. -->
        <Logger name = "one.andTwo"  level="info" additivity = 'false'>
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="file"/>
        </Logger>
    </Loggers>
</Configuration>
 

3번째 줄의 Appenders는 로그 방식에 대한 정의 부분이야.

 

6번째 줄은 Console로 출력하기 위한 설정으로 target="SYSTEM_OUT"으로 설정했어.  name은 이 방식에 대한 이름이야. 맘대로 정해.

 

7번째 줄은 로그 내용에 대한 형식이야. 날짜, 시간, 클래스.. 대충 요런 형식으로 로그 정보가 남는다는 뜻이야. 자세한 건 구글링..

 

11번째 줄부터는 로그를 파일에 저장시키는 방식을 정의한 부분이야.

 

16번째 줄부터는 Appenders에 정의해놓은 로그 방식을 사용하는 부분이야.

 

21번째 줄의 name은 패키지 경로를 포함한 클래스 이름이야. andTwo 클래스에서 발생되는 로그를 콘솔과 파일로 남기겠다는 뜻인데, level이 info로 설정되어 있으니 info레벨 이상의 로그만 남긴다는 의미야.

현재 andTwo 클래스에는 상위부터 error ~ trace까지의 로그를 남긴다고 되어있는데 level속성에 의해 trace와 debug를 제외한 로그만이 console과 파일에 저장되게 되지!

 

이제 서버를 켜고 크롬 창에서 요청 url을 입력해봐. 만약 아래와 같은 화면이 출력된다면 Servlet 호출에 성공한 거고, 콘솔 창, 파일에 로그까지 남는다면 log4j2를 이용한 로그 남기기도 성공한 거야!


5. 실행결과

 

이클립스 콘솔

 

 

파일

 

 

설명이 너무 장황하고 두서없어서 이해하긴 어려울 거야... 공부한다고 생각하고 나 자신과 대화를 한다는 컨셉으로 쓴 거니까 이해해주길..

반응형
반응형
반응형

목차

1. 개요

2. logging이란

3. log4j란

4. log4j 설치 및 설정

5. 예제


1. 개요

 서버는 클라이언트와 정보를 주고받는다. 다양한 이유로 그 과정에서 발생하는 여러 사건이나 정보들을 기록으로 남기는데 이 기록을 log라고 한다. 그리고 log를 남기는 행위를 '로깅'이라고 하는데, 모든 서버에 기본적으로 들어가는 개념이기 때문에 오늘은 이 logging에 대해 공부하는 시간을 가져보도록 하자. 구동 환경은 이클립스와 tomcat 7.0이다.


2. logging이란

 로깅이란 시스템 동작 시 시스템 상태/작동 정보를 시간의 경과에 따라 기록하는 것이다. 그 기록을 '로그'라고 한다.

 한마디로 '로깅 = 로그 기록'이다.

 로깅은 많은 부분에서 사용되는데, 사용자의 패턴이나 시스템 동작 분석에 사용되거나 해킹 사고가 발생할 경우 비정상 동작의 기록을 통해 추적하는 데 사용한다. 백엔드 개발자들의 경우 개발 과정에 있어 디버깅에 활용할 수도 있다. 이 외에도 다방면으로 쓰인다.

 그렇다면 Spring과 같은 Java환경에서 로깅을 하려면 어떻게 해야 할까? 시스템 동작에 대한 기록을 남기는 것이기 때문에 흔히 알고 있는 system.out.println()과 같은 메소드를 사용할 수 있겠다. 하지만 이는 메모리면에서 비효율적인 데다 클라이언트의 접속량이 많아질수록 안정적으로 실행되지 못하는 등 여러 문제를 안고 있다. (문제에 대해 궁금하다면 직접 구글링을 추천)

 이 문제에 대한 솔루션이자 Java 환경의 logging 시스템이 바로 log4j이다. log4j에 대해 본격적으로 알아보도록 하자.


3. log4j란 

 Java 환경의 로깅 시스템을 제공하는 라이브러리.

 Logger, Appender, Layout 등 다양한 컴포넌트가 사용되며, 로그 레벨을 분류하여 로그 정보를 출력시킬 수 있다.

 

 3.1) 컴포넌트

컴포넌트 설명
Logger 로그의 주체, 로그 파일을 작성하는 클래스
Appender 로그를 출력하는 위치
Layout Appender의 출력포맷(일자, 시간 등)을 설정하여 로그 내용으로 지정하는 속성. 

  Layout의 종류는 여러 가지가 있지만 일반적으로 디버깅에 가장 적합한 PattenLayout을 사용한다.

  PattenLayout이란 출력 포맷을 정해진 일련의 패턴을 사용하여 설정하는 것인데 패턴 정보는 아래와 같다.

패턴 설명
C 클래스명 출력
d 로그 시간 출력
F 파일명 출력. 수행한 메소드와 라인번호가 함께 출력.
L 라인 번호 출력
m 로그로 전달된 메시지 출력
M 로그를 수행한 메소드명 출력
n 개행
p 로그 이벤트명 (DEBUG 등)
r 로그 처리시간(milliseconds)
t 로그 이벤트가 발생된 쓰레드 출력

 위의 패턴을 잘 조합하여 로그에 대한 출력 포맷을 설정한다.

 

 3.2) 로그 레벨

 로그는 기본적으로 6개의 레벨을 갖는다. 아래로 갈수록 낮은 레벨이다.

로그 레벨 설명
fatal 시스템 문제와 같은 아주 심각한 에러가 발생한 상태를 나타냄.
error 요청을 처리하는중 문제가 발생한 상태를 나타냄.
warn 처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 경고성 메시지를 나타냄.
info 로그인, 상태변경과 같은 정보성 메시지를 나타냄.
debug 개발시 디버그 용도로 사용한 메시지를 나타냄.
trace 디버그 레벨이 너무 광범위한것을 해결하기위해서 좀더 상세한 상태를 나타냄

 debug(), warn(), error()와 같이 메소드를 사용해서 로그 정보를 얻을 수 있으며, 로그 레벨 설정을 통해 로그를 통제할 수 있다. 이는 Logger의 Level 메소드를 통해 이루어지며, 지정한 로그 레벨보다 낮은 로깅 이벤트는 무시된다이는 뒷부분의 예제를 보면 이해할 수 있을 것이다.


4. log4j 설치 및 설정

 1) http://logging.apache.org/log4j/2.x/download.html 에서 최신버전.zip 파일 다운.

log4j 홈페이지

 2) 압축을 풀면 많은 파일이 있는데 그중 log4j-api-2.x.jar 와 log4j-core-2.x.jar 파일을 복사.

 

 3) 이클립스를 실행시켜 Dynamic Web Project를 생성하고 프로젝트 폴더에 lib 폴더를 생성.

 

 4) lib 폴더에 복사한 두 파일을 붙여넣기.

lib에 파일 붙여넣기

  5) 라이브러리를 추가는 '프로젝트 우클릭 → properties → Java Build Path → Libraries → Add JARs' 클릭

  

  6) lib에 있는 두 파일을 선택 후 Apply 클릭하면 라이브러리 설정이 완료.

프로젝트에 log4j 라이브러리 추가


5. 예제

  1) project  Java Resources → src 경로에 log4j2.xml 파일 생성 후 아래의 소스코드 추가.

     (log4j2.xml은 로깅에 대한 기본 환경 설정 파일)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="STDOUT" />
        </Root>
    </Loggers>
</Configuration>

  4번 라인 - 로그를 출력하는 위치(Appenders)를 Console로 설정. 로깅 이름은 STDOUT으로 설정.

  5번 라인 - patten을 시간, 로그 이벤트명, 쓰레드, 클래스명, 파일명, 라인 번호, 메시지, 개행으로 설정

  9~10번 라인 - STDOUT에 대한 Root level을 debug로 설정(서블릿에서 호출될 로그 중 debug보다 낮은 로그 레벨은 무시)

  

  2) proejct에 servlet 클래스를 생성하고 아래의 코드를 입력. 

  (servlet 생성 및 기본 개념을 모르겠다면 클릭)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package servlet;
 
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
/**
 * Servlet implementation class log
 */
@WebServlet("/log")
public class log extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private Logger logger = LogManager.getLogger(log.class);
    /**
     * @see HttpServlet#HttpServlet()
     */
    public log() {
        super();
        // TODO Auto-generated constructor stub
    }
 
    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        response.getWriter().append("Served at: ").append(request.getContextPath());
        logger.error("error message");
        logger.warn("warn message");
        logger.info("info message");
        logger.debug("debug message");
        logger.trace("trace message");
    }
 
    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
 
}
 

  18번 라인 - log 클래스에 대한 Logger객체 생성

  33~37번 라인 - 5가지의 로그 메소드 호출

 

  3) Servlet 파일 Run

실행 결과

  현재 servlet 파일에서 logger를 error, warn, info , debug, trace 총 5개 레벨에 대한 출력을 시도했으나 제일 낮은 레벨인 trace를 제외한 4개만 console로 출력된 상태이다. 이유는 앞서 언급했던 로그 레벨에 따른 통제와 관련이 있다.   log4j2.xml 파일에서 STDOUT 로깅 데이터에 대한 level이 debug로 설정되었기 때문이다. trace를 출력시키고 싶다면 xml 파일 9번째 줄의 debug 대신 trace를 넣어주면 된다.

 

  4) tomcat 서버 에러 발생 시

   tomcat 서버 에러가 발생할 경우 tomcat에 서버에 대해서도 log4j 라이브러리를 추가시켜줘야 한다. log4j를 사용하여 얻은 로그가 기본적으로 WAS(tomcat)에 남기 때문이다.

   추가 방법은 tomcat Server Overview → Open launch configuration → Classpath → UserEntries → Add JARs를 클릭하여 lib 폴더에 있는 두 라이브러리를 클릭하면 된다.

tomcat Overview
log4j 라이브러리 추가

 

시간이 늦은 관계로 log4j을 통한 롤링은 다음에 포스팅하도록 하겠다.

반응형
반응형

목차

1. 개요

2. Servlet 이란?

3. Servlet 동작 구조

4. 예제 및 실습

5. 실행화면


1. 개요

 저번 게시물에서 Spring MVC 모델에 대해 공부하던 중 Dispacher Servlet이란 개념이 애매모호한것 같아 이를 구조적으로 이해하기위해 선행 학습 개념인 Servlet에 대한 공부를 진행하였다.

 eclipse EE와 tomcat을 연동하여 WAS 구조를 만들고, Servlet을 이용하여 Client - Web Server - Web Container간 통신 구현 및 구조를 이해해보도록 하자.


2. Servlet 이란?

 Servlet이란 JAVA를 이용하여 동적 페이지를 생성하는 서버측 프로그램이다.

 CGI(Common Gateway Interface)라고도 하는데 CGI란 사용자의 입력을 받아 동적 페이지를 만드는 것이다.

 사용자의 입력에 따라 결과가 달라지는 것, 예를들어 쇼핑몰 로그인 후 나오는 자신의 닉네임같은 것이다.

 즉, JAVA로 구현된 CGI라고 생각하면 된다.

 

 개념은 알았으니 Servlet 동작 구조에 대해 살펴보도록 하자. 


3. Servlet 동작 구조

Servlet 동작 구조

 1) 클라이언트의 요청이 있으면 Web Server에게 요청이 전달된다.

 

 2) Web Server는 정적인 데이터(HTML, 이미지, css 등)만을 처리하고, 동적 데이터(Servlet, DB, 로직 등)는 Web Container에게 전달한다.

* Web Container : Servlet 클래스 또는 JSP 파일을 실행하기 위한 실행 환경을 제공하는 컨테이너

 

 3) Web Container는 web.xml파일을 참조하여 해당 Servlet에 대한 쓰레드를 생성한다. 그리고 httpServletRequest와  httpServletResponse 객체를 생성하여 이 쓰레드에게 전달한다.

 쓰레드를 생성하는 이유는 클라이언트에게 효율적으로 웹 페이지를 제공하고, 서버의 부하를 막기 위함이다.

 이로써 통신 객체를 가진 쓰레드가 만들어진다.

* 쓰레드 : 여러가지 작업을 동시에 수행할 수 있도록 복제(나눈)한 것

 

 4) Container가 Servlet을 호출한다. 

 

 5) 호출된 Servlet의 작업을 담당하는 쓰레드(3에서 생성된 쓰레드)는 로직이 정의된 doPost()doGet() 메소드를 호출한다. 이 두 메소드는 Servlet class에 정의되어있다.

 

 6) 호출한 메소드의 로직을 컴파일한 후 생성된 동적 페이지를 (3)번 에서 생성했던 httpServletResponse객체에 담아 Web Container에게 넘겨준다.

 

 7) Web Container는 전달받은 response 객체를 HTTPResponse 형태로 바꿔 웹 서버로 전송함과 동시에 생성했던 쓰레드와 httpServletRequest, httpServletResponse 객체를 종료 및 소멸시킨다.

 (HTTPResponse는 Web Server에서 Client 로의 응답 객체이다.)

 

 8) Web Server는 전송받은 HTTPResponse 객체를 HTTP 통신을 통해 클라이언트에게 전송하여 화면을 출력시킨다.


4. 예제 및 실습 (eclipes EE, tomcat 7.0)

 4.1) Dynamic Web Project 생성(web.xml 체크)

New - Dynamic Web Project
web.xml Check

 4.2) Servlet 생성

  - Package, class 이름을 입력.

  - class이름의 첫글자는 대문자로 입력.

create Servlet

 

 4.3) Servlet 코드 입력(test1.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package one;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
/**
 * Servlet implementation class Two
 */
@WebServlet("/Two")
public class Two extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public Two() {
        super();
        // TODO Auto-generated constructor stub
    }
    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        response.getWriter().append("Served at: ").append(request.getContextPath());
        PrintWriter out = response.getWriter();
        out.println("<html>"+"<body>"+"<h2>Hello World</h2>"+"</body>"+"</html>");
        
    }
    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
}
 
 
 

 

4.4) web.xml 코드 입력

 <servlet-name> 태그에는 프로젝트의 이름을, servlet-class에는 클래스의 주소(Package.class형식)를 입력.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
  <display-name>test1</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  
  <servlet>
      <servlet-name>test1</servlet-name>
      <servlet-class>one.Two</servlet-class>
  </servlet>
</web-app>
 

5. 실행화면

Servlet에 들어있던 HTML 코드

 URL의 끝이 test1이 아니라 Servlet 명인 Two를 추가적으로 입력해야한다.

 URL 설정은 해당 Servlet 파일 또는 web.xml 파일 내에서 가능하다.

 

 페이지에 Servlet에서 정의한 로직이 출력되면 성공한 것이며, 이렇게 eclipse, tomcat을 사용한 Servlet 실습을 마치도록 하겠다.

반응형

+ Recent posts