기존에 간단하게 만들었던 프로젝트에 로그인 기능을 추가해달라는 요구사항이 있으면 여러가지 방법이 있습니다. 대표적으로 Filter를 사용하여 DispatcherServlet이 요청 URL를 인터셉트 하기전에 세션을 관리하는 방식으로 인증을 처리하는 방법입니다. 하지만 이미 스프링에서는 쉬운지는 모르겠지만 더 간편하게 보안을 적용 할 수 있는 스프링 시큐리티라는 기능을 제공하고 있습니다. 오늘은 웹 환경에서 스프링 시큐리티가 어떻게 동작하고 인증 및 권한처리 방법에 대해서 포스팅하였습니다.

스프링 시큐리티란?

Spring Security는 스프링 기반의 어플레케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크 입니다.

스프링 시큐리티 개념

스프링 시큐리티를 개념을 잡기 위해선은 아래 기본적인 용어는 알아두어야 한다고 생각합니다. 평상시에 많이 접하던 용어라 거부감은 딱히 없었습니다...ㅎㅎ

용어 정리

보안: 위험, 손실 , 범죄가 발생하지 않도록 방지하는 상태를 가리킵니다. 일반적으로 보안의 피해발생의 원인이 인간의 행위라는 점에서 안전이라는 개념과 구분됩니다.

접근주체(Principal): 보호된 리소스에 접근하는 대상

인증(Authentication) : 애플리케이션의 작업을 수행할 수 있는 주체(사용자)라고 주장할 수 있는것을 말합니다.

인증의 방식

  • 크리덴셜 기반 인증 : 사용자명과 비밀번호를 이용한 방식
  • 이중인증 : ATM을 이용할 때처럼 물리적인 카드와 사용자가 입력한 개인정보를 조합하는 방식
  • 하드웨어 인증 : 자동차 키와 같은 방식

인증과 인가
일반 사원인 김사원과 재무팀의 박부장은 사내 모든 직원들의 급여명세서를 열람하고 싶어한다. 먼저 시스템에 로그인을 하면 인증관리자(인증을 처리함)는 사용자가 제시한 정보를 통해 사용자가 누구인지를 확인한다. 다음으로, 접근결정 관리자는 사용자가 가지고 있는 역할을 가지고 급여명세서에 접근할 수 있는지 아닌지를 판별하게 된다.

인가(Authorize): 해당 리소스에 대해 접근 가능한 권한을 가지고 있는지 확인하는 과정(After Authentication)

권한: 권한은 인증된 주체가 어플리케이션의 동작을 수행할 수 있도록 허락되있는지를 결정하는것을 말합니다. 따라서 권한 승인이 필요한 부분으로 접근하려면 인증 과정을 통해 주체가 증명 되어야만 한다는 것을 의미합니다.

역할부여란?

인증을 통해서 인증된 주체를 하나 이상의 권한(역할)에 매핑하고 보호된 리소스에 대한 권한을 체크하는 것을 말한다.

필터

스프링 시큐리티에서 웹 애플리케이션에 주로 영향을 주는 방식은 일련의 ServletRequest 필터를 사용한 방식이다. 필터들이 애플리케이션에 대한 모든 요청을 감싸서 처리한다. 스프링 시큐리티에서 여러개의 필터들은 아래 그림과 같이 체인형태를 이루면서 동작한다. 자동 설정 옵션을 사용하면 10개의 스프링 시큐리티 필터가 자동으로 설정된다.

DelegatingFilterProxy

DelegatingFilterProxy는 스프링 시큐리티가 모든 애플리케이션 요청을 감싸게 해서 모든 요청에 보안이 적용되게 하는 서블릿필터이다.(스프링 프레임워크에서 제공) 스프링 프레임워크 기반의 웹 애플리케이션에서 서블릿 필터 라이프 사이클과 연계해 스프링 빈 의존성을 서블릿 필터에 바인딩하는데 사용합니다.
web.xml에 다음과 같은 설정을 해주면 애플리케이션의 모든 요청을 스프링 시큐리티가 감싸서 처리할 수 있게 됩니다.

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

추가로 설명하자면
DelegatinFilterProxy는 filter-name으로 명시된 스프링 컨텍스트에서 빈으로 등록된 springSecurityFilterChain에게 실제 인증처리를 위임하기만 합니다. springSecurityFilterChain은 Filter 인터페이스를 구현한 여러 종류의 필터들을 List 객체에 가지고 있습니다.

스프링 시큐리티를 커스터마이징 하기 위해서는 SpringSecurity에서 제공하는 필터들에 대해 이해하는게 좋습니다.
SpringSecurityFilterChain은 아래 표와 같이 여러 종류의 필터들을 가지고 있습니다.

SpringSecurityFilterChain 종류

12

필터 체인의 제일 마지막에 위치한 FilterSecurityInterceptor는 앞에 지나온 모든 필터들의 정보를 토대로 최종 결정을 내립니다.

SpringSecurityFilterChain

다운로드

스프링 시큐리티 구조

스크린샷 2019-11-12 오후 8 27 23

Spring Security Process

아래 사진은 웹 기반 인증 요청을 처리하는 기본 프로세스를 그림으로 도식화 하였습니다.

스크린샷 2019-11-16 오전 12 01 52

가장 많이 사용하는 폼 기반의 인증방식에 대해서 살펴보겠습니다.
저희 웹 서비스를 가입한 사용자 A가 있다고 가정하겠습니다. 저희 웹 서비스는 회원가입을 통해 가입하여 리소스에 대한 해당 권한을 가진 사용자만이 웹 서버의 리소스에 접근할 수 있습니다.

  1. 먼저, A가 처음에 브라우저를 켜서 저희 웹 서버에서 제공하는 서비스를 이용하기 위해서 폼 기반의 인증요청을 보냅니다. 그러면 위의 표에 나와있듯이 맨 처음에 AbstractAuthenticationProcesFilter를 구현한 클래스(UsernamePasswordAuthenticationFilter)가 인증요청에 해당하는 URL을 감지하여 요청을 가로챕니다.

    public class UsernamePasswordAuthenticationFilter extends
         AbstractAuthenticationProcessingFilter {
  2. Request 객체에 사용자명과 패스워드를 획득 후에 UsernamePasswordAuthenticationTocken 객체에 해당 정보를 바인딩을 하게 됩니다.

UsernamePasswordAuthenticationTocken는 단순 사용자명과 패스워드를 담는 단순 도메인 객체입니다.

  1. UsernamePasswordAuthenticationFilter가 주입 받은 AuthenticationManager의 authenticate 메소드를 호출합니다. 이때 인자로 UsernamePasswordAuthenticationTocken을 반환합니다.
    AuthenticationManager는 스프링 시큐리티의 중요한 요소 중 하나이기 때문에 나중에 자세히 설명하겠습니다.

UsernamePasswordAuthenticationTocken은 Authentication을 상속했기 때문에 자바의 다형성으로 인해 반환이 가능합니다.

  1. AuthenticationManager는 사용자명/패스워드를 실질적으로 인증하는 역할을 맡고 있습니다. AuthenticationManager는 인터페이스이기 때문에 구현체인 ProviderManager를 사용합니다. 이 ProviderManager는 이 권한을 AuthenticationProvider 인터페이스에게 넘깁니다.

뭔가 복잡하지만.. 결국 인증 처리는 AuthenticationProvider를 통해서 처리한다고 생각하면 됩니다. 저희 개발자는 AuthenticationProvider의 구현체를 이용하여 인증 메커니즘을 커스터마이징하여 구현하면 되는 것입니다.

  1. 데이터베이스에서 사용자 정보를 가져올때는 AuthenticationManager는 UserDetailService에게 폼 기반의 인증을 요청한 주체(principal)로 부터 입력받은 사용자명을 파라미터로 받아서 실제 DB에서 정보를 가져와 UserDetails에 정보를 넣어서 반환하는 역할을 합니다.

AuthenticationProvider: 로그인 인증처리를 구현 (비밀 번호 대조)
UserDetailsService: 유저정보를 DB에서 조회하는 역할

위의 순서과 대략적인 스프링 시큐리티 처리 과정입니다. 일반적으로는 인증 관리자는 데이터베이스에 저장된 사용자 정보로 인증을 처리 합니다.

Spring Security 코드 리뷰

이제 간단하게 스프링 시큐리티를 코드를 작성하여 구현해보겠습니다.
가장 먼저 웹 환경에서 스프링 시큐리티를 적용하기 위해서 아래와 같은 클래스 파일을 작성하였습니다.

SpringSecurity config

//웹 보안 활성화 시키는 어노테이션
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // AuthenticationProvider 커스터마이징한 구현체
    private final CustomerAuthenticationProvider customerAuthenticationProvider;

    // DB에서 사용자 정보를 가져오는 UserDetailService 구현체
    private final JpaUserService jpaUserService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
      log.info("security config.......");


      // 웹과 관련된 다양한 보안 설정을 걸어주는 역할을 합니다.
      http.authorizeRequests().antMatchers("/guest/**").permitAll()
              .antMatchers("/board/list").permitAll()
              .antMatchers("/board").hasRole("BASIC");


      // authorizeRequests()는 시큐리티 처리에 HttpServletRequest를 이용합니다.
      http.authorizeRequests().antMatchers("/manager/**").hasRole("MANAGER");

      http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN");

      // customize 한 로그인 페이지 설정
      http.formLogin().loginPage("/login");

      //특정 리소스에 대한 접근 권한이 존재하지 않을때 이동시킬 페이지 설정
      http.exceptionHandling().accessDeniedPage("/accessDenied");

      // 로그아웃시에 세션 무효화, 스프링 시큐리티는 사용자 정보를 HttpSession 방식으로 관리하기 때문에 브라우저가 완전히 종료되면, 로그인한 정보를 잃게 됩니다. 브라우저를 종료하지않을시에.. 로그아웃으로 무효화시킵니다.
      http.logout().logoutUrl("/logout").invalidateHttpSession(true);

    }

    // 패스워드 암호화를 위해 PasswordEncoder Bean으로 등록
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
        // AuthenticationManagerBuilder는 다양한 인증 관리자를 생성해주는 객체입니다. 
        auth.authenticationProvider(customerAuthenticationProvider).userDetailsService(jpaUserService).passwordEncoder(passwordEncoder());
    }
}

@EnableWebSecurity

WebSecurityConfigurerAdapter는 세가지 configure() 메소드를 오버라이딩하고 동작을 설정하는 것으로 전반적인 웹 보안정책을 설정할 수 있습니다.

스크린샷 2019-11-12 오후 7 10 31

이 중에서 가장 많이 사용하는 객체가 HttpSecurity 입니다. 보안에 관련된 역할과 많은 책임을 가지고 있습니다.

AuthenticationManagerBuilder는 다양한 인증관리자를 생성해주는 빌더로 메모리 기반의 인증과 데이터베이스 인증 관리자 등 다양한 인증 관리자를 생성해주는 역할을 맡고 있습니다.

http.formLogin()을 이용하면 기본적으로 AbstractAuthenticationProcessingFilter를 구현한 UsernamePasswordAuthenticationFilter 구현체를 이용하게 됩니다.

http.formLogin().loginPage("/login");

참고로 UsernamePasswordAuthenticationFilter에서는 인증(로그인)요청에 대해서 기본적으로 GET 방식으로 인증 정보가 들어오는 것을 허용하지 않고 Http Method를 POST 방식으로만 지원하고 있습니다.

저 같은 경우에는 아래 인증관리자 빌더를 통해서 AuthenticationProvider와 UserDetailsService를 등록하였습니다. 위에서 언급한것 처럼 DB를 통한 인증처리 방식을 많이 사용하기 때문에 authenticationProvider(customerAuthenticationProvider)를 작성하지 않고 UserDetailsService()만 작성하여도 AuthenticationManager에 DaoAuthenticationProvider가 자동으로 연결됩니다.

auth.authenticationProvider(customerAuthenticationProvider).userDetailsService(jpaUserService).passwordEncoder(passwordEncoder());

AuthenticationProvider 구현

@Component
@RequiredArgsConstructor
public class CustomerAuthenticationProvider implements AuthenticationProvider {
    // UserDetailsService 구현체 
    private final JpaUserService jpaUserService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = authentication.getName();
        UserDetails userDetails = null;

        try {
              userDetails = jpaUserService.loadUserByUsername(username);
        }catch (UserNotFoundException e){
            System.out.println(e.getMessage());
        }catch (BadCredentialsException e){
            System.out.println(e.getMessage());
        }catch (Exception e){
            System.out.println(e.getMessage());
        }

        return new UsernamePasswordAuthenticationToken(userDetails.getUsername(),userDetails.getPassword(),userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return false;
    }
}

위의 코드가 실제로 인증을 처리해주는 AuthenticationProvider 구현체 입니다.
UserDetailsService 인터페이스를 구현한 JpaUserService 객체에게 사용자로 받은 사용자명으로 데이터베이스에 해당 사용자를 조회하여 존재하면 UserDetails에 담아서 AuthenticationProvider에게 제공해주는 역할을 하고 있습니다. 존재하지 않으면 예외를 처리하겠죠

UserDetails

public class JpaSecurityUser extends User {

    // 스프링 시큐리티에서는 권한정보를 ROLE + 권한명으로 리소스 접근승인을 판단합니다. 
    // 따라서 접두사인 PREFIX 변수에 필수로 적어줘야 권한을 확인하고 판별할 수 있습니다.
    private static final String ROLE_PREFIX = "ROLE_";

    private Member member;

    public JpaSecurityUser(Member member){
        // User 클래스를 상속하기 때문에 부모생성자는 필수로 호출해줘야 합니다.
        super(member.getUid(), member.getUpw(),makeGrantedAuthority(member.getRoles()));

        this.member = member;
    }

    public JpaSecurityUser(String username, String password, Collection<? extends GrantedAuthority> authorities){

        super(username, password, authorities);

    }

    // 접근제한자 private로 캡슐화를 통해서 해당 User 클래스에서 생성자로 필요한 사용자의 권한정보를 생성 합니다.  
    private static List<GrantedAuthority> makeGrantedAuthority(List<MemberRole> roles){

        List<GrantedAuthority> list = new ArrayList<>();

        roles.forEach(role -> list.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getRoleName())));

        return list;
    }

    public Member getMember() {
        return member;
    }
}

UserDetails는 인터페이스이기 때문에 실제로 DB에서 조회한 정보를 담기 위해서는 해당 인터페이스의 구현체를 만들어야 합니다. 하지만 우리의 넘사벽 프레임워크인 스프링은 이미 다 준비가 되어있죠.. User라는 클래스가 UserDetails를 구현해놨기 때문에 아래에 UserDetailsService 구현부에서 User를 반환해도 됩니다. 하지만 저는 위의 코드 처럼 나중에 추가적인 맴버변수(이메일, 생년월일)를 사용할 수도 있기 때문에 User 클래스를 확장한 JpaSecurity를 구현하여 Member 도메인을 필드로 가지도록 하였습니다.

public class JpaSecurityUser extends User{

}

사용자명을 파라미터로 받아서 DB에서 해당 유저의 정보를 조회하는 역할을 담당하는 UserDetailsService를 구현한 부분입니다. JPA를 사용하기 때문에 Repository를 이용하여 DB에 접근하였습니다.

UserDetailsService

@Service
@RequiredArgsConstructor
public class JpaUserService implements UserDetailsService {

    @Autowired
    private  final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        JpaSecurityUser user = memberRepository.findById(username).filter(m -> m != null).map(m -> new JpaSecurityUser(m)).get();

        if(user == null){
            throw new UserNotFoundException("해당 계정이 존재하지 않습니다.");
        }

        return user;
    }
}

아래 개발자분들이 올려주신 블로그가 저에게 스프링 시큐리티 개념을 정립하는데 많은 도움을 주셨습니다.

출처: https://blog.naver.com/sipzirala/220979656224,https://javaiyagi.tistory.com/431

IDE는 이클립스만 사용해보다가 최근에 Mac OS를 사용하면서 Intellij로 갈아타게 되었습니다. 물론 무엇을 쓰든 상관없었지만, 트렌드에 쓰잘대 없이 민감한 제가… 큰 마음먹고 한번 사용해보았습니다. 아직은 익숙하지가 않지만 간단하게 Intellij에서 스프링 mvc를 Mybatis 모듈과 연동하여 웹 어플리케이션을 구현해보는 시간을 리뷰해 보았습니다.

먼저, 꽤 저렴하지 않는 금전으로 Intellij Ultimate 1년 구독신청 하였기 때문에 해당 IDE로 실행을 하겠습니다.

Intellij를 사용하여 Spring-Mybatis 연동

스크린샷 2019-10-05 오전 12 41 42

미리 만들어놓은 프로젝트의 구조는 다음과 같습니다. 생각보다 복잡해서… 프로젝트 생성과정은 생략하였습니다. 빌드 도구로는 역시… 이클립스에서 사용해왔던 maven을 이용하여 pom.xml 파일에서 의존성 라이브러리들을 관리합니다.

스크린샷 2019-10-05 오전 12 41 51

위의 프로젝트 구조에서 database.properties 파일과 classes, views디렉토리를 제외하고는 Intellij에서 maven과 스프링 mvc로 프로젝트를 생성하여 보여주는 기본구조입니다.

스프링-Mybatis 모듈 연동을 구현하기 전에 간단하게 프로젝트 구조에 대해서 리뷰해보겠습니다.
웹 어플리케이션이 톰캣에서 동작할때 가장 먼저 web.xml파일을 참조하는데 설정내용은 이클립스와 크게 다르지 않습니다.

스프링 설정파일

web.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

dispatcher-servlet.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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

  <!-- 아래 annotation-driven 태그는 HandlerMapping, HandlerAdapter를 Bean으로 등록하여 요청 url를 controller와 매칭시켜준다.-->
    <mvc:annotation-driven/>

    <mvc:resources mapping="/resources/**" location="/resources/"/>

  <!-- 특정 패키지안의 클래스들을 스캔하고, annotation을 확인 후 bean 인스턴스를 생성한다.-->
    <context:component-scan base-package="com.jun"/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

기존의 이클립스와의 차이점은 context파일 명이 servlet-context.xml, root-context.xml 파일이 아니고 dispatcher-servlet.xml, applicationContext.xml 입니다.

dispatcher-servlet.xml파일에서 view를 검색하는 역할을 하는 ViewResolver를 빈으로 등록하고 js,css같은 정적파일들은 WAS가 처리하지 않고, WEB 서버에게 위임합니다. 스프링 mvc를 사용한다고 하면, 대표적으로 DispatcherServlet를 등록하고, 모든 요청을 받게 설정을 하는데 /resources/** 로 요청이 오면 /resources/로 매핑시켜주어서 바로 처리할 수 있도록 설정하였습니다.

이제 간단하게 요청 url에 특정 id 값을 보내면 Mybatis에서 sqlSession 객체를 이용하여 해당하는 값을 DB 테이블에서 검색하여 view 화면에서 보여주는 예제를 구현해보겠습니다.

이러한 작업을 위해서… 역시 설정들을 간단한?? 설정을 해주면 됩니다.
그전에 pom.xml에서 mybatis, mybatis-spring, mysql-connector-java 의존성 라이브러리들을 pom.xml에 작성하여 해당 dependency들을 받아와야 됩니다.

mybatis는 sql 명령어를 관리해주는 편리한 라이브러리이고, 유지보수성이나 효율성이 기존에 jdbc에서 사용해왔던 방식보다 훨씬 뛰어납니다.

<!-- MyBatis, MyBatis-Spring, Spring-JDBC -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>

            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

mybatis와 mybatis를 연동하는 스프링 그리고 스프링과 jdbc를 연결하는 라이브러리입니다.

mybatis는 필연적으로 데이터베이스를 이용하는 sql문을 설정하는 라이브러리이기에 데이터베이스 개발환경에 꼭 필요하고 스프링 jdbc와 그를 연결하는 mybatis-spring 라이브러리가 필수적입니다.

아래의 그림이 설명이 잘돼어있는거 같아서 참고하였습니다.

스크린샷 2019-10-05 오전 12 42 05

이제 applicationContext.xml파일에서 데이터베이스 관련 설정을 해보겠습니다.

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns\="[http://www.springframework.org/schema/beans](http://www.springframework.org/schema/beans)"  
xmlns:xsi\="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)"  
xmlns:context\="[http://www.springframework.org/schema/context](http://www.springframework.org/schema/context)"  
xsi:schemaLocation\="[http://www.springframework.org/schema/beans](http://www.springframework.org/schema/beans)  
[http://www.springframework.org/schema/beans/spring-beans.xsd](http://www.springframework.org/schema/beans/spring-beans.xsd)  
[http://www.springframework.org/schema/context](http://www.springframework.org/schema/context)  
[http://www.springframework.org/schema/context/spring-context.xsd](http://www.springframework.org/schema/context/spring-context.xsd)"\>  

<context:property-placeholder location\="/WEB-INF/database.properties"/>  
<!--<context:component-scan base-package="com.jun"/>-->  

<!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
<property name="url" value="${jdbc.url}"/>  
<property name="username" value="${jdbc.username}"/>  
<property name="password" value="${jdbc.password}"/>  
</bean>-->  

<bean id\="dataSource" class\="org.springframework.jdbc.datasource.DriverManagerDataSource"\>  
<property name\="driverClassName" value\="com.mysql.jdbc.Driver"/>  
<property name\="url" value\="${jdbc.url}"/>  
<property name\="username" value\="${jdbc.username}"/>  
<property name\="password" value\="${jdbc.password}"/>  
</bean\>  
<!-- Mysql <-> Mybatis를 연결해주는 객체 -->  
<bean id\="sqlSessionFactory" class\="org.mybatis.spring.SqlSessionFactoryBean"\>  
<property name\="dataSource" ref\="dataSource"/>  
<property name\="configLocation" value\="classpath:/mybatis-config.xml"/>  
<property name\="mapperLocations" value\="classpath:/mappers/\*.xml"/>  
</bean\>  

<bean id\="sqlSession" class\="org.mybatis.spring.SqlSessionTemplate"\>  
<constructor-arg ref\="sqlSessionFactory"\></constructor-arg\>  
</bean\>  
</beans\>

DB url, username, password 같은 정보는 보안상 따로 파일을 만들어서 변수를 사용하여 입력하는게 좋은 방법입니다.
<context:property-placeholder location="/WEB-INF/database.properties"/> 태그로 database.properties 파일을 해당 xml파일로 import를 하여 dataSource 빈에 property에 변수를 값으로 넣어줍니다.

sqlSessionFactory 빈에는 mybatis 설정파일과 sql문을 관리하는 mapper xml파일의 경로와 데이터베이스 연결정보들을 가지고 있는 dataSource 객체를 property 값으로 가지고 있습니다.

mybatis-config.xml, member-config.xml 파일은 src/main/resources 디렉토리 밑에 생성하여 만든 설정파일로써 해당경로를 클래스패스로 인식하기 때문에 값을 넣어줄때 classpath:를 써줘야 인식합니다. 안그러면 에러가 발생합니다.

sqlSessionFactory는 데이터베이스 연결과 sql문 실행에 대한 모든 것을 가진 가장 중요한 객체입니다.

여기까지 스프링 웹 어플리케이션에서 DB를 연결하기 위한 설정하는 방법입니다.
이제 자바코드를 이용하여 해당 웹 어플리케이션을 구현해보겠습니다.

  1. Controller
package com.jun.controller;

import com.jun.dto.MemberDto;
import com.jun.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {


    @Autowired
    private MemberService memberService;

    @RequestMapping("/home")
    public String home(){

        return "homepage";
    }

    @RequestMapping("/index")
    public String index(){

        return "index";
    }

    @RequestMapping("/loginForm")
    public String loginForm(){

        return "loginForm";
    }
    //해당 url/id 값으로 요청이 들어오면 loginResultView 메소드 실행
    @RequestMapping("/loginResultView/{id}")
    public String loginTestResult(@PathVariable String id, Model model){

        MemberDto dto = memberService.selectMember(id);

        model.addAttribute("member", dto);

        return "loginResultView";
    }
}
  1. DTO
package com.jun.dto;

public class MemberDto {

    private String id;
    private String name;


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  1. DAO
package com.jun.dao;

import com.jun.dto.MemberDto;
import com.jun.mapper.MemberMapper;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class MemberDao {

    @Autowired
    private SqlSession sqlSession;

    public MemberDto selectMember(final String id){

        MemberMapper mapper = sqlSession.getMapper(MemberMapper.class);

        MemberDto memberDto = mapper.selectMember(id);

        return  memberDto;
    }
}
  1. service
package com.jun.service;

import com.jun.dto.MemberDto;

public interface MemberService {
    public MemberDto selectMember(String id);
}
package com.jun.service;

import com.jun.dao.MemberDao;
import com.jun.dto.MemberDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    public MemberDto selectMember(String id) {

        MemberDto memberDto = memberDao.selectMember(id);

        return memberDto;
    }
}

sql문에서 테이블에서 select로 단일조회하여 저장할 때도 사용하는 용도로 작성한 파일입니다.

  1. mapper
package com.jun.mapper;

import com.jun.dto.MemberDto;

public interface MemberMapper {
    MemberDto selectMember(String id);
}

sqlSession 객체에서 해당 interface를 구현하여 MemberMapper 객체를 이용하여
member-mapper.xml파일에 정의되어 있는 sql파일을 실행하는 책임을 가지고 있습니다.

  1. mapper-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jun.mapper.MemberMapper">
    <select id="selectMember" parameterType="String" resultType="memberDto">
        SELECT name, id from TEST1 WHERE id =#{id}
    </select>
</mapper>

접근하고자 하는 테이블의 sql 명령어들을 정의한 xml 파일입니다.

  1. mapper-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <typeAliases>
        <typeAlias alias="memberDto" type="com.jun.dto.MemberDto"/>
    </typeAliases>
</configuration>

mybatis-config.xml 파일은 Mybatis에 별도의 설정들을 정의합니다.
member-mapper.xml에서 정의된 sql문의 리턴타입을 dto로 사용할 경우에 태그를 사용하면 풀 패키지명 없이 간단하게 memberDto를 리턴타입으로 사용할 수 있습니다.

mysql을 실행하여 임의로 해당 테이블을 dto(Member)와 같은 타입으로 셍상히였습니다.
간단한 spring-mybatis 예제이기 때문에 따로 password는 암호화하여 넣지는 않았습니다.

스크린샷 2019-10-05 오전 12 42 19

위의 코드들을 작성했으면 이제 database에서 해당 값을 가져오는지 실행결과를 보겠습니다.

스크린샷 2019-10-05 오전 12 51 27

대충 작성하였지만 스프링 부트를 배우기 전에 다시한번 복습한다는 생각으로 mybatis-spring 연동예제를 구현해보았습니다. 처음에는 eclipse와 환경도 다르고 library 가져오는 방법도 약간 달라서 어려움이 있었지만… 역시 코딩은 삽질이 답인거 같네요. 제 부족한 점을 다시한번 짚어볼 수 있는 유익한 시간이였습니다.

안녕하세요
이번 포스팅은 Spring Data JPA를 이용하여 동적으로 SQL을 처리하는 Querydsl에 대해 진행하겠습니다.

Querydsl이란?

쿼리를 처리하다 보면 다양한 상황에 맞게 쿼리를 생성하는 경우가 많습니다.
대표적인 케이스가 다양한 검색 조건에 대해서 쿼리를 실행해야 하는 경우라고 할 수 있습니다.
쿼리 메소드나 @Query를 이용하는 경우에 개발 속도는 좋지만 고정적인 쿼리만을 생산합니다.
이러한 이유로 동적인 상황에 대한 처리를 위해서 Querydsl이라는 것을 이용합니다.

단순 문자열(JDBC, Mybatis, JPQL)과 비교해서 Fluent API(Querydsl)를 사용할 때의 장점은 다음과 같습니다.

  1. IDE의 코드 자동 완성 기능 사용
  2. 문법적으로 잘못된 쿼리를 허용하지 않습니다.
  3. 도메인 타입과 property를 안전하게 참조할 수 있습니다.
  4. 도메인 타입의 리팩토링을 더 잘 할 수 있습니다.

요약하면 Querydsl은 타입에 안전한 방식으로 쿼리를 실행하기 위한 목적으로 만들어졌습니다.
즉, Querydsl의 핵심 원칙은 타입 안전성(Type safety)입니다.
그것이 가능한 이유는 문자열이 아닌 메서드 호출로 쿼리가 수행되기 때문입니다.

저는 Maven을 이용해서 Querydsl에 대해 의존성과 플러그인을 설정하겠습니다.
Querydsl을 이용하기 위해서는 다음과 같은 과정을 수행해야 합니다.

  • pom.xml의 라이브러리와 Maven 설정의 변경 및 실행
  • Predicate의 개발
  • Repository를 통한 실행
  1. Querydsl 의존성 라이브러리를 추가합니다.
    <dependency>
     <groupId>com.querydsl</groupId>
     <artifactId>querydsl-apt</artifactId>
     <version>${querydsl.version}</version>
     <scope>provided</scope>
    </dependency>
    <dependency>
     <groupId>com.querydsl</groupId>
     <artifactId>querydsl-jpa</artifactId>
     <version>${querydsl.version}</version>
    </dependency>
    


2. pom.xml에 Querydsl 플러그인을 추가합니다.
~~~xml
 <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <goals>
                <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
 </plugin>
~~~

플러그인에서 outputDirectory 태그에 경로를 적어주었는데요. Querydsl에서는 JPA를 처리하기 위해서 엔터티 클래스를 생성하는 방식을 이용합니다. 이를 'Qdomain'이라고 하는데,
이 도메인 클래스는 위의 해당경로에 생성이 됩니다.

정상적으로 설정이 되었다면 프로젝트 내에 target/generated-sources/java 디렉토리에 Qdomain 클래스가 생성되는 것을 볼수 있습니다.
이때 주의할 사항은 해당 경로는 패스에 존재하지 않기 때문에 Qdomain을 찾지 못한다고 에러가 발생 할 수 있습니다. 따라서 아래와 같이 해당경로를 패스에 추가를 해줘야 합니다.

![스크린샷 2019-06-01 오후 11 53 22](https://user-images.githubusercontent.com/22395934/58750072-85862f00-84c8-11e9-877f-099e106548c2.png)

저는 현재 해당경로를 소스 폴더에 추가한 상태입니다. IntelliJ에서는 File>Project Structure>Modules에 들어가면 위 화면 같이 구성되어있습니다.
추가방법은 좌측 트리에 target/generated-sources/java 디렉토리를 우클릭하여 Sources를 선택하면 우측 Source Folders에 추가가 됩니다.

이제 간단하게 엔터티 클래스를 이용하여 동적으로 쿼리를 생성하는 예제를 보여드리겠습니다.

먼저, Repository 코드에서 QueryDslPredicateExcutor<T> 인터페이스를 상속하도록 아래와 같이 변경해 줍니다.
```java
public interface BoardRepository extends CrudRepository<Board, Long>, QuerydslPredicateExecutor<Board> {

}

QueryDslPredicateExcutor 인터페이스는 다음과 같은 메소드들이 선언되어 있습니다.

스크린샷 2019-10-03 오후 3 58 49

엔터티 클래스

@Getter
@Setter
@ToString
@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long bno;

    private String title;

    private String writer;

    private String content;

    @CreationTimestamp
    private Timestamp regdate;

    @UpdateTimestamp
    private Timestamp updatedate;
}

Predicate를 생성 및 테스트를 수행하는 코드입니다.
Predicate는 '확신한다', '단언하다'라는 뜻으로 이 조건이 맞다고 판단하는 근거를 함수로 제공하는 것입니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTests {

    @Autowired
    private BoardRepository boardRepository;

    @Test
    public void testPredicate(){

        String type = "t";
        String keyword = "17";
        BooleanBuilder builder = new BooleanBuilder();


        QBoard board = QBoard.board;

        //메소드를 통해서 쿼리문을 생성함.
        if(type.equals("t")){
            builder.and(board.title.like("%" + keyword +"%"));
        }

        //bno 값이 0보다 큰 값을 조건에 추가
        builder.and(board.bno.gt(0L));
        //페이징 처리에 정보를 가진 객체 생성
        Pageable pageable = PageRequest.of(0,10);

        Page<Board> result = boardRepository.findAll(builder, pageable);

        System.out.println("PAGE SIZE: " + result.getSize());
        System.out.println("TOTAL PAGES: " + result.getTotalPages());
        System.out.println("TOTAL COUNT: " + result.getTotalElements());
        System.out.println("NEXT: " + result.nextPageable());

        List<Board> list = result.getContent();

        list.forEach(b -> System.out.println(b));
    }

위의 코드를 보면 상황에 따라 조건문이 달라집니다.

  • type이 오면 where type = type
  • keyword가 오면 where keyword = keyword

즉, 파라미터가 어떻게 오는지에 따라 where의 조건이 변경되는 것입니다.
이를 해결하기 위한 방법으로 BooleanBuilder를 자주 사용합니다.

QBoard 객체는 엔터티 클래스인 Board 객체를 참조해서 생성이 됩니다.
만약 위와 같이 설정파일과 패스경로에 추가했음에도 생성이 되지 않는다면 pom.xml을 우클릭하여 아래 화면처럼 Generate Sources and Update Folders를 클릭해줍니다.

스크린샷 2019-06-02 오전 12 23 50

if문에서 필요한 부분만을 BooleanBuilder에 추가하면서 쿼리를 만들었습니다.
메소드로 쿼리를 생성하기 때문에 위에서 언급한것 처럼 안정적이고 문법적인 오류를 허용하지 않습니다. 하지만 where문의 조건을 한눈에 보기 어렵습니다. 지금은 조건문을 많이 추가를 안했지만 조금만 조건이 까다로워지면 추측하기도 힘든 쿼리가 될 것입니다.

and() 메소드를 이용하여 조건을 추가하는 것을 볼 수가 있습니다.
QBoard는 Board의 속성을 이용해서 다양한 SQL에 필요한 구문을 처리할 수 있는 기능이 추가된 형태이므로, like(), get()등을 이용해서 원하는 SQL을 구성하는데 도움을 줍니다.

테스트 코드를 실행하면 다음과 같은 결과를 볼 수 있습니다.
스크린샷 2019-06-02 오전 12 34 55

리턴타입을 Page로 설정했기 때문에 데이터를 추출하는 SQL과 개수를 파악하는 SQL이 실행되고, 이때 필요한 조건들이 지정되는 것을 볼 수 있습니다.

안녕하세요. 이전 포스팅에서 스프링-Mybatis 모듈을 연동하는 간단한 실습을 진행했다면,이번엔 @Aspect를 이용한 스프링 AOP의 개념과 구동원리에 대해서 공부를 하였습니다.
웹 서비스를 운영하면서 다양한 핵심 비즈니스 로직이 존재하게 되는데 그때마다 고객이 해당 비즈니스 서비스를 호출할때마다 보안인증, 트랜잭션 처리, 로깅같은 꼭 필요하지만 중요하지 않는 공통기능의 코드를 작성할 필요성을 느끼게 됩니다. 그때마다 비즈니스 로직을 가진 메소드에 해당 공통기능의 코드를 넣게 된다면 가독성과 유지보수성 측면에서 좋지 않다고 생각하기 때문에 이런경우 스프릥 AOP를 이용하면 좋을거 같다 생각하여 리뷰하게 되었습니다.

스프링 AOP(Aspect Oriented Programming)

AOP는 관점지향 프로그래밍이라는으로 "기능을 핵심 비즈니스 기능과 공통기능으로 '구분'하고,모든 비즈니스 로직에 들어가는 공통기능의 코드를 개발자의 코드 밖에서 필요한 시점에 적용하는 프로그래밍 방식입니다. 이게 무슨 강아지같은 소리인지... 처음에는 이해가 안됬지만.. 역시 코딩은 해보는게 답인거 같아서... 그냥 한번 뭐라도 해보자는 마인드로 간단하게 구글링해본 결과 AOP에 대한 정말 퀄리티가 훌륭한 포스팅들이 넘쳐 흘렀습니다.
컨시브이가 주특기인 저한테는 그중에서 정말 따라하기 쉬운 간단한 코드를 실습하면서 AOP에 대한 기본적인 개념에 대해서 공부하였습니다.

AOP란?

  • 로깅, 예외, 트랜잭션 처리 같은 코드들은 횡단 관심(Crosscutting Concerns)
  • 핵심 비즈니스 로직은 핵심 관심(Core Concerns)

스크린샷 2019-05-25 오후 10 54 53

AOP는 핵심관심과 횡단 관심을 완벽하게 분리할 수 없는 OOP의 한계를 극복하도록 도와줍니다.

AOP 용어

AOP 소스예제를 살펴보기 전에 간단하게 용어정리를 해보았습니다.

조인포인트(Joinpoint): 클라이언트가 호출하는 모든 비즈니스 메소드, 조인포인트 중에서 포인트컷이 되기 때문에 포인트컷의 후보라고 할 수 있습니다.

포인트컷(Pointcut): 특정 조건에 의해 필터링 된 조인포인트, 수많은 조인포인트 중에 특정 메소드에서만 공통기능을 수행시키기 위해 사용됩니다.

어드바이스(Advice): 공통기능의 코드, 독립된 클래스의 메소드로 작성합니다.

위빙(Weaving): 포인트컷으로 지정한 핵심 비즈니스 로직을 가진 메소드가 호출될 때, 어드바이스에 해당하는 공통기능의 메소드가 삽입되는 과정을 의미합니다. 위빙을 통해서 공통기능과 핵심 기능을 가진 새로운 프록시를 생성하게 됩니다.

Aspect: 포인트컷과 어드바이스의 결합입니다. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정합니다.

아래 표는 어드바이스의 동작 시점입니다.

동작시점 설명
Before 메소드 실행 전에 동작
After 메소드 실행 후에 동작
After-returning 메소드가 정상적으로 실행된 후에 동작
After-throwing 예외가 발생한 후에 동작
Around 메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작

포인트컷 표현식

포인트컷을 이용하면 어드바이스 메소드가 적용될 비즈니스 메소드를 정확하게 필터링 할 수 있습니다.

지시자(PCD, AspectJ pointcut designators)의 종류

execution: 가장 정교한 포인트컷을 만들수 있고, 리턴타입 패키지경로 클래스명 메소드명(매개변수)
within: 타입패턴 내에 해당하는 모든 것들을 포인트컷으로 설정
bean: bean이름으로 포인트컷

표현식 설명
* 모든 리턴타입 허용
void 리턴타입이 void인 메소드 선택
!void 리턴타입이 void가 아닌 메소드 선택

패키지 지정

표현식 설명
com.jun.demo.controller 정확하게 com.jun.demo.controller 패키지만 선택
com.jun.demo.controller.. com.jun.demo.controller 패키지로 시작하는 모든 패키지 선택

클래스 지정

스크린샷 2019-10-03 오후 3 22 02

메소드 지정

스크린샷 2019-10-03 오후 3 22 12

매개변수 지정

스크린샷 2019-10-03 오후 3 22 23

JoinPoint 인터페이스

어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요합니다. 예를들면 예외가 발생하였는데, 예외발생한 메소드의 이름이 무엇인지 등을 기록할 필요가 있을 수 있습니다. 이럴때 JoinPoint 인터페이스가 제공하는 유용한 API들이 있습니다.

스크린샷 2019-10-03 오후 3 22 38

Sinature API

스크린샷 2019-10-03 오후 3 22 48

AOP 코드예제

간략하게 스프링 AOP 관련해서 용어정리를 하였습니다. 이제 스프링 AOP가 적용된 간단한 소스코드를 살펴보겠습니다.

해당코드는 간단하게 @RestController를 어노테이션을 적용한 controller에서 클라이언트로부터 요청이오면 해당요청에 매핑되는 메소드를 호출할때마다 얼마나 빠르게 응답하는지 확인하기 위해서 공통모듈로 로깅과 StopWatch 객체를 이용하여 메소드 리턴 시간을 측정하는 것을 구현해보았습니다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
public class LogAspect {

    private static Logger logger = LoggerFactory.getLogger(LogAspect.class);

    //ProceedingJoinPoint 클래스는 JoinPoint 인터페이스를 상속하는 인터페이서.. 인자는 스프링 컨테이너가 넘겨준다.
    @Around("execution(* com.jun.demo.controller.HelloController.*(..))") //포인트컷
    public Object logging(ProceedingJoinPoint pjp) throws Throwable{

        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        logger.info("start -" + pjp.getSignature().getDeclaringTypeName() + " / " + pjp.getSignature().getName());
        Object result = pjp.proceed();
        logger.info("finished -" + pjp.getSignature().getDeclaringTypeName() + " / " + pjp.getSignature().getName());

        stopWatch.stop();
        logger.info("Timer Stop - Elapsed time :" + stopWatch.getTotalTimeMillis());

        return result;
    }
}
@RestController
public class HelloController {

    @GetMapping("/sample")
    public SampleVO makeSample(){

        SampleVO vo = new SampleVO();
        vo.setVal1("v1");
        vo.setVal2("v2");
        vo.setVal3("v3");
        System.out.println(vo);

        return vo;
    }

}

결과화면

스크린샷 2019-05-25 오후 11 45 37

LogAspect는 AOP를 정의하는 클래스로 @Aspect, @Componet로 이클래스가 AOP가 바라보는 관점을 정의하고 스프링 컨테이너가 관리하는 bean으로 등록하는 것을 정의하였습니다.

@Around는 위의 AOP용어 설명처럼 어드바이스 동작 시점을 정의하였습니다.
저는 메소드 실행 전/후에 공통기능을 핵심 비즈니스 로직에 적용하였습니다.

 @Around("execution(* com.jun.demo.controller.HelloController.*(..))")

@Around에 표현식을 사용하였는데 지시자로 execution을 사용하여 정교한 포인트컷을 만들었습니다. 먼저 *은 리턴타입을 의미하는데 모든 리턴타입을 허용한다는 의미이고, 두번째로는 패키지를 지정, 세번째는 클래스 HelloController로 지정하였습니다.
그리고 포인트컷으로 지정할 메소드와 매개변수를 지정하였는데 *은 해당 클래스의 모든 메소드를 포인트컷으로 지정하고, (..)은 매개변수의 개수와 상관없이 모든 매개변수를 지정한다는 의미입니다.

공통기능을 정의한 메소드 logging에 매개변수로 ProceedingJoinPoint 객체는
타켓대상의 핵심 관심에 대한 정보를 제공하는 역할을 하고 있습니다. 그리고 JoinPoint 인터페이스를 상속하는 인터페이스로 스프링 컨테이너에서 제공하고 있습니다.
이때 Around 어드바이스만 다른 어드바이스와 약간 다른데, ProceedingJoinPoint 객체를 인자로 선언해야합니다. 그렇지 않으면 에러가 발생합니다.

위의 코드는 Object 객체를 리턴하도록 하였는데, 그 이유는 스프링 AOP 동작원리에 있습니다. 스프링 AOP는 Proxy(대행자)를 통해서 수행하게 됩니다.
즉 proceed()에서 정상적으로 메서드를 실행한 후 리턴 값을 주는데 가로채서 어떤 action 을 한 후에 기존 리턴 값을 되돌려 주지 않으면 가로챈 프록시가 결과 값을 변경하거나 지워버린것과 다름이 없습니다. 위의 코드는 단순하게 전/후로 시간을 측정하여 로깅을 찍어주고 기존 비즈니스 로직이 실행될 수 있게 pjp.proceed();를 호출하였습니다.

AOP 동작원리

프록시(Proxy)를 이용하여 AOP를 구현

스크린샷 2019-05-25 오후 11 21 28

프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑(Wrapping) 오브젝트입니다.
호출자(클라이언트)에서 타겟을 호출하게 되면 타겟이 아닌 타겟을 감싸고 있는 프록시가 호출되어, 타겟 메소드 실행전에 선처리, 타겟 메소드 실행 후, 후처리를 실행시키도록 구성되어있습니다.

스프링 AOP에서는 런타임시에 Weaving을 통해서 프록시 객체를 생성하게 됩니다.
생성방식으로는 첫번째로 JDK Dynamic Proxy가 있는데 타겟대상이 Interface를 구현하는 클래스면 인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되지 않는 메서드에 대해서는 AOP가 적용되지 않는 단점이 있습니다.

두번째로는 CGLIB가 있는데 타켓대상이 인터페이스를 구현하고 있지 않고 바로 클래스를 사용한다면, 스프링은 CGLIB를 이용하여 클래스에 대한 프록시 객체를 생성합니다. CBLIB는 대상 클래스를 상속받아 구현합니다. 따라서 클래스가 final인 경우에는 프록시를 생성할 수 없습니다.

좀더 구체적으로 설명하기에는 아직 공부를 못했기 때문에 이 부분에 대해서는 다음에 더 준비해서 포스팅하겠습니다.

프록시 객체를 자세하게 까보지 않아서 구체적인 동작원리는 모르겠지만 맴버로 타켓객체와 Aspect로 정의된 공통모듈을 가지는 객체를 가지고 았지 않을까 뇌피셜로 생각만 해봤습니다. 클라이언트의 요청이 오면 포인트컷과 어드바이스가 결합하는 Weaving과정에서 새로운 Proxy객체가 생성되면서 공통기능과 타켓의 핵심 비즈니스로직을 수행하지 않을까 생각도 해봤습니다.

프록시를 이용해서 보조업무를 처리하는 예제 포스팅을 보게 되면서 조금이나마 프록시에 대해서 이해하게 되었고, AOP 개념이 스프링에 한정되지 않는것을 알게 되었습니다.

결론은.. AOP의 장점은 이렇습니다.

  • 단순 복사 붙여넣기 -> 핵심 비즈니스 로직에 공통기능의 코드 중복이 많아져 코드분석과 유지보수를 어렵게 만듭니다.
  • AOP를 통해 부가적인 공통코드를 효율적으로 관리합니다.

참조: https://ooz.co.kr/201
참조: https://jeong-pro.tistory.com/171

+ Recent posts