반응형

1. 개요

아키텍처는 여러 가지 방식으로 정의되고 이해될 수 있는 용어다. 가장 단순한 정의는 아래와 같다.

 아키텍처란 어떤 경계 안에 있는 내부 구성요소들이 어떤 책임을 갖고 있고, 어떤 방식으로 서로 관계를 맺고 동작하는지를 규정하는 것이다.

 

아키텍처는 관계와 동작의 규정이므로 많고 다양하다. 그 중 웹 개발에 많이 사용하는 계층형 아키텍처와 오브젝트 중심 아키텍처에 대해 알아보았다.

 


2. 계층형 아키텍처

 책임과 성격이 다른 것을 그룹으로 만들어 분리해두는 것을 아키텍처 차원에서는 계층형 아키텍처라고 한다. 보통 웹 기반의 계층은 데이터 액세스 계층, 서비스 계층, 프레젠테이션 계층, 이 세 개의 계층을 갖는다고 해서 3계층 애플리케이션, 3-tire 애플리케이션 이라고도 한다.

 

 

1) 데이터 액세스 계층(DAO)

 데이터 액세스 계층은 DB, ERP, 레거시 시스템 등에 접근하는 역할을 주로 처리하는 계층이다.

 

2) 서비스 계층

  핵심 비지니스 로직을 처리하는 계층이다. 이 계층의 클래스는 POJO로 작성되기에 객체지향적인 설계 기법이 적용된 코드를 통해 쉽게 테스트하고 유연하게 확장할 수 있다.

 

3) 프레젠테이션 계층

 가장 복잡한 계층이며, 매우 다양한 기술과 프레임워크의 조합을 가질 수 있다. 엔터프라이즈 애플리케이션에서는 HTTP 프로토콜을 사용하는 서블릿이 바탕이 된다.

 


3. 계층형 아키텍처 설계 원칙

 

가장 중요한 설계 원칙은 응집도가 높으면서 다른 계층과는 낮은 결합도를 유지하는 것이다. 각 계층은 자신의 계층의 책임에만 충실해야 한다. 예를들어 아래의 코드는 다른 계층과의 결합도를 갖는 코드이다.

 

public ResultSet findUserByName(String name) throws SQLException;

 

 서비스 계층이 DAO를 호출할 때 ResultSet를 처리해야 한다면, 이는 서비스 계층이 JDBC라는 특정 데이터 액세스 계층 기술에 종속되는 것이다.

 예외도 마찬가지이다. SQLException은 Checked Exception이다. 서비스 계층에서는 이를 무시할 수 없기에 예외 상황을 분석하기 처리하는 코드를 만들어야 한다. 이 코드는 다음과 같이 수정돼야 한다.

 

public List<User> findUserByName(String name) throws DataAccessException;

 User는 사용자 정보를 담는 오프젝트이므로 특정 계층에 종속되지 않는다. 결과는 이렇게 계층에 종속되지 않는 오브젝트로 반환해야 한다.

예외 또한 DataAccessException과 같은 런타임 예외로 만들어야 한다. 이를 통해 서비스 계층에서는 이에 대한 예외처리를 하지 않아도 된다.

 

 이 원칙에 위배되는 흔한 실수 중 하나가 프레젠테이션 계층의 오브젝트를 서비스 계층으로 전달하는 것이다. HttpServeltRequest나 HttpSession과 같은 타입을 서비스 계층 인터페이스 메서드의 파라미터 타입으로 사용하면 안된다.

 웹 방식이 아닌 클라이언트가 이 비지니스 로직을 사용해야 할 경우 재사용이 불가능하며, 단위테스트가 복잡해지는 단점이 있다.

 


4. DB/SQL 중심의 로직 구현 방식

 쉽게 말해서 비지니스 로직을 SQL 을 통해 처리하는 방식이다. 로직이 복잡해지면 SQL이 복잡해지고, 하나의 트랜잭션 단위마다 하나 이상의 SQL이 생성되어야 한다. 이러한 방식은 자바를 DB와 연결해주는 단순한 인터페이스 도구로 전락시키는 것이다.

 


5. 거대한 서비스 계층 방식

 복잡한 SQL을 피하면서, 핵심 비지니스 로직은 서비스 계층에서 처리하는 방식이다. DAO가 응답한 정보를 분석, 가공하는 것이 서비스 계층의 핵심 코드가 된다. 이처럼 어플리케이션 코드에 비지니스 로직이 담겨있기 때문에 자바 언어의 장점을 잘 활용하는 것이고, 테스트하기 용이하다.

 하지만 DAO 계층의 SQL은 서비스 계층의 비지니스 로직의 필요에 따라 만들어지기 쉽기 때문에 강한 결합을 여전히 갖고 있다. 서비스 계층의 코드도 업무 단위로 만들어지므로 DAO를 공유할 수 있는 것을 제외하면 코드의 중복이 발생할 수 있다.

 


6. 오브젝트 중심 아키텍처

 오브젝트를 만들고 오브젝트 구조 안에 정보를 담아서 각 계층 사이에 전달하게 하는 아키텍처 방식이다.

 재사용하기 쉽고, 전 계층에서 일관된 구조를 유지한채 사용할 수 있다.

 

 하지만 최적화된 SQL을 사용하지 못하고, 멤버필드가 많아지면 사용하지 않는 필드가 증가하므로 데이터 중심 아키텍처보다 성능이 떨어질 수 있다. 이러한 문제는 지연 로딩(Lazy Loading)을 통해 어느정도 해결할 수 있으며, 실제로 JPA, 하이버네이트와 같은 ORM 기술에서 지연 로딩을 많이 사용한다.

 

 오브젝트에는 단순히 정보 뿐 아니라 기능도 함께 담고있어야 한다. 그래야 서비스 계층의 비지니스 로직에서 재사용할 수 있기 때문이다.

반응형
반응형
반응형

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주차 스터디

  - 1장 오브젝트와 의존관계 (102p ~ 122p)

 


2. 싱글톤 레지스트리

 오브젝트와 관계를 설정하고 리턴하는 순수 자바코드 형태의 '오브젝트 팩토리'와 '어플리케이션 컨텍스트'는 하나의 큰 차이가 있다. 어플리케이션 컨텍스트는 빈을 모두 '싱글톤'으로 만든다는 점이다.

 아래 예제를 보면 알 수 있듯이 애플리케이션 컨텍스트에서 조회한 userDao에 대해 getBean을 두 번 호출할 경우 모두 동일한 객체가 리턴되었고, DaoFactory에서 userDao 메서드를 호출할 경우 다른 객체를 리턴하고 있다. (DaoFactory 관련 코드는 1주차 게시글에 기재되어 있음)

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao1 = applicationContext.getBean("userDao", UserDao.class);
UserDao userDao2 = applicationContext.getBean("userDao", UserDao.class);
System.out.println("동일한 객체인가 : " + (userDao1 == userDao2)); // true

DaoFactory daoFactory = new DaoFactory();
UserDao factoryUserDao1 = daoFactory.userDao();
UserDao factoryUserDao2 = daoFactory.userDao();
System.out.println("동일한 객체인가 : " + (factoryUserDao1 == factoryUserDao2)); // false

 

 그렇다면 스프링 애플리케이션 컨텍스트는 왜 객체를 싱글톤으로 생성할까?


3. 싱글톤으로 빈을 생성하는 이유

 서버환경에서는 객체를 싱글톤으로 만드는게 성능적으로 좋기 때문이다. 예를들어 클라이언트에서 요청이 올 때마다 관련 오브젝트를 새로 생성한다고 생각해보자. 한 요청에 5개의 오브젝트가 생성되어야 한다면, 100번의 요청에는 500개의 오브젝트가 생성된다. 생성 후 사용되지 않는 객체는 자바의 GC에 의해 처리되는데, 처리해야할 객체가 많으니 GC 횟수가 많아져 서버 리소스가 자주 사용될 것이고, 이는 곧 서버 부하를 가져오게 된다.

 만약 싱글톤으로 빈을 생성한다면, 한 요청에 5개의 오브젝트를 생성하는게 아닌 공유하게 될 것이고, GC로 인한 부하도 발생하지 않을 것이다. 이처럼 오브젝트를 싱글톤으로 생성하고 관리하는 것을 '싱글톤 레지스트리'라고 하며, 애플리케이션 컨텍스트는 싱글톤 레지스트리의 역할도 수행한다고 할 수 있다.

 

3.1. 멀티 스레드 환경에서의 싱글톤

 멀티 스레드 환경에서는 하나의 싱글톤 객체에 여러 스레드가 접근하게 된다. 그렇기에 싱글톤 객체의 상태 정보를 수정하는 케이스는 절대 있어서는 안된다. 상태정보는 일반적으로 오브젝트 내 비지니스 로직과 관련되어 사용된다. 상태 정보가 수정된다면 비지니스 로직의 결과 값이 바뀔 수 있다는 뜻인데, 멀티 스레드 환경에서는 상태 값이 덮어 씌워지게 된다. 즉 스레드 각각이 원하지 않는 값을 읽어 올 수 있다는 뜻이다.

 그렇기에 싱글톤은 인스턴스 필드 값을 변경하고 유지하는 상태가 아닌 무상태(stateless) 방식으로 만들어야 한다. 단, 읽기 전용의 상태 값이라면 덮어 씌워질 위험이 없으므로 상태 방식으로 만들어도 상관없다.

 

3.2. 스프링 빈의 스코프

 스프링이 관리하는 빈이 생성되고, 존재하고, 적용하는 범위를 빈의 스코프라고 한다. 빈의 스코프는 일반적으로 싱글톤이다. 하지만 경우에 따라 싱글톤 이외의 스코프를 가질 수 있다. 이에 대해선 추후 10장에서 자세히 다룰 예정이라고 한다.


4. 의존관계 주입(DI)

 IoC는 스프링만의 용어가 아니다. 1주차에서 공부했듯이 IoC라는 개념은 소프트웨어에서 자주 발견할 수 있는 일반적인 개념이다. 객체지향적인 설계를 할 경우 자연스럽게 IoC를 적용할 수 있다. 때문에 스프링을 IoC 컨테이너라고 하기엔 타 소프트웨어와 확실히 구분되지 않았다. 이러한 이유로 스프링 IoC 방식의 핵심 기능을 나타내는 의존 관계 주입(Dependency Injection)이라는 용어가 만들어져 사용되기 시작했다.

 스프링의 동작 원리는 IoC라고 할 수 있지만, 타 프레임워크와 차별화되서 제공되는 핵심 기능은 의존 관계 주입이라고 할 수 있기 때문이다. 그렇다면 의존관계란 뭘까?


5. 의존관계

 두 개의 클래스가 의존관계에 있다는 것을 설명할 때에는 방향성도 같이 설명해야한다. 즉, 누가 누구에게 의존하는 관계에 있다는 식이어야 한다는 말이다. UML 모델에서는 두 클래스의 의존관계를 점선으로 표시하며, 아래 그림을 'A가 B에 의존한다'라고 한다.

 

A가 B에 의존한다.

 

 의존한다는 건 대상이 변하면 자신에게 영향을 미친다는 뜻이다. 여기서는 B가 변하면 A에 영향을 미친다. B의 기능이 추가되거나, 변경되거나, 형식이 바뀌면 그 영향이 A로 전달되기 때문이다.

 예를들어 A가 B를 사용하는 경우 B에 정의된 메소드를 호출해서 사용하게 되는데, B의 메소드 형식이 바뀌면 A도 그에 따라 수정해야하고, B의 형식은 그대로지만 기능이 바뀔경우 A의 기능 수행에도 영향을 미칠 수 있다. 반대로 B는 A에 영향을 받지 않지 않는다. B에서 A를 사용하지 않기 때문이다. 일반적으로 이렇게 사용의 관계에 있는 경우 의존 관계가 맺어진다.

 

5.1. UserDao의 의존관계

 지금까지 작업했던 UserDao는 ConnectionMaker에 의존하고 있다. ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 받을 수 있다. 하지만, ConnectionMaker 인터페이스를 구현한 클래스, 즉 DConnectionMaker가 다른 것으로 바뀌어도 UserDao에 영향을 주지 않는다. 인터페이스를 사용했기 때문에 구현 클래스와의 관계가 느슨해졌기 때문이다.

public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
    
    ...
}

UserDao, ConnectionMaker, DConnectionMaker의 관계

 

 위 그림에서 알 수 있듯이 UserDao 클래스는 ConnectionMaker 인터페이스에게만 의존한다. UserDao 입장에서 DConnectionMaker 클래스의 존재도 알지 못하기에 이 클래스에는 의존하지 않는다.

 이처럼 인터페이스를 통한 의존관계를 갖는 경우에는 UserDao가 본인이 사용할 오브젝트가 어떤 클래스로 만든것인지 미리 알 수 없다. DaoFactory와 같이 DI 기능을 담당하는 클래스에 미리 정의해 놓을 순 있으나, UserDao 클래스나 ConnectionMaker의 코드 내에서는 드러나지 않는다. 이를 알 수 있는 시점은 어플리케이션이 실행된 이후인 런타임 시점이다. 이렇게 런타임 시점에 의존 관계를 맺는 대상을 의존 오브젝트라고 하다. 그리고 이렇게 구체적인 의존 오브젝트와 그것을 사용할 오브젝트를 런타임 시에 연결해주는 작업을 의존관계 주입이라고 정의한다.

 

의존관계 주입
 구체적인 의존 오브젝트와 그것을 사용할 오브젝트런타임 시에 연결해주는 작업을 말하며, 아래 세가지 조건을 충족한다.
 1) 클래스 코드에는 구체적인 의존 오브젝트에 대한 의존관계가 드러나지 않는다. 이를 위해서는 인터페이스에만 의존하고 있어야 한다.
 2) 런타임 시점의 의존관계는 컨테이너나 팩토리같은 제 3의 존재가 결졍한다.
 3) 의존관계는 사용할 오브젝트에 대한 래퍼런스를 외부에서 제공해줌으로써 만들어진다. 

 


6. 의존관계 주입의 응용

6.1. 기능 구현의 교환

 인터페이스 구현체로 기능을 구현하고 있으니, 기능 구현을 교환하려면 인터페이스에 대한 구현체 클래스를 새로 생성 후 DI 하는 부분에서 클래스만 교체하면 된다.

 

6.2. 부가 기능 추가

 DAO가 DB를 얼마나 많이 연결해서 사용하는 지를 파악하기 위해 DB 연결 횟수를 카운팅하는 기능을 추가한다고 가정한다면, DAO 로직마다 Count를 추가하는 게 아닌 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결 횟수를 카운팅하는 오브젝트를 추가하면 된다. 오브젝트 추가는 DI 설정으로 한다.

 

public class CountingConnectionMaker implements ConnectionMaker{

    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    CountingConnectionMaker(ConnectionMaker realConnectionMaker){
        this.realConnectionMaker = realConnectionMaker;
    }
    
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        this.counter++;
        return realConnectionMaker.makeConnection();
    }

    public int getCounter(){
        return this.counter;
    }
}

...

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao(){
        return new UserDao(countingConnectionMaker());
    }

    @Bean
    public ConnectionMaker countingConnectionMaker(){
        return new CountingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker(){
        return new DConnectionMaker();
    }
}


...

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao userDao = applicationContext.getBean("userDao", UserDao.class);

        // DAO 사용 코드

        CountingConnectionMaker ccm = applicationContext.getBean("countingConnectionMaker",
                CountingConnectionMaker.class);

        System.out.println(ccm.getCounter());
    }
}

 

 


7. 회고

 이번 주차에서는 빈의 스코프와 DI에 대해 알아보았다. 이번 기회로 DI에 대한 개념을 재정리 할 수 있게 되었다. DI가 단순히 어떠한 객체를 주입받는다라고 생각했었지만 코드 내에 의존 오브젝트가 드러나서는 안되고, 의존관계를 런타임 시점에 맺어야 한다는 것을 새롭게 알게되었다.

 또한 DI 활용 부분을 보며 AOP, 프록시와 같은 성질을 가진 기능들도 DI로 구현이 가능하겠구나 라는 생각이 들면서 이 기능이 스프링에서 제공하는 다양한 부분에 사용되고 있겠구나 라는 생각도 들었다.

 

반응형

+ Recent posts