Servlet기반 MVC 구조 및 동작 순서

image

  1. 클라이언트로부터 요청이 Dispatcher Servlet으로 들어온다.
  2. Dispatcher Servlet은 DAO를 통해서 데이터를 가져온다.
  3. Http Session에 DB에서 가져온 데이터를 저장한다.(굳이 이렇게 할 필요는 없음)
  4. Dispatcher Servlet이 해당 뷰로 매칭시킨다.
  5. JSP 뷰에서 Session 객체에 저장되어진 데이터를 가져와 뷰를 구성한다
  6. 구성되어진 뷰는 클라이언트로 다시 보내어진다.

Spring MVC 구조 및 동작 순서

image

  1. 클라이언트에서 서버로 요청을 보내면 제일 먼저 Dispatcher Servlet이 요청을 받는다.
  2. Dispatcher Servlet은 받은 요청 URI를 HandlerMapping으로 보내면 HandlerMapping은 해당 URI와 매핑되는 Controller객체를 받는다.
  3. 매핑된 Controller에서 DAO로 부터 데이터를 받아 business logic을 처리하고 View Name을 리턴한다
  4. View Name은 ViewResolver를 통해서 Prefix나 Suffix등의 붙어지고 해당 뷰는 Client로 보내어진다.

Spring MVC 구조 직접 만들기

  1. web.xml 설정

    1
    2
    3
    4
    5
    6
    7
    8
    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    • 모든 요청이 Dispatcher Servlet을 거치도록 설정한다.
    • Dispatcher Serlvet이 모든 클라이언트 요청을 첫 번째로 받는다.
  2. Dispatcher 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
    49
    public class DispatcherServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private HandlerMapping handlerMapping;
    private ViewResolver viewResolver;


    public DispatcherServlet() {
    System.out.println("===> DispatcherServlet 생성");
    }

    @Override
    public void init() throws ServletException {
    handlerMapping = new HandlerMapping();
    viewResolver = new ViewResolver();

    // ViewResolver의 접두사와 접미사를 결정한다
    viewResolver.setPrefix("./");
    viewResolver.setSuffix(".jsp");
    }

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 1. 사용자 요청 path 정보를 추출한다.
    String uri = request.getRequestURI();
    String path = uri.substring(uri.lastIndexOf("/"));
    System.out.println(path);

    // 2. HandlerMapping을 통해 path에 해당하는 Controller를 검색한다.
    Controller ctrl = handlerMapping.getController(path);

    // 3. 검색된 Controller를 실행한다
    String viewName = ctrl.handleRequest(request, response);

    // 4. ViewResolver를 통해 viewName에 해당하는 화면을 검색한다
    String view = null;
    if (!viewName.contains(".do")) {
    if (viewName.contains(".html")){
    view = viewName;
    } else {
    view = viewResolver.getView(viewName);
    }
    } else {
    view = viewName;
    }

    // 5. ViewResolver가 검색해준 화면으로 이동한다.
    response.sendRedirect(view);
    }
    }
    • 요청이 들어오면 Dispatcher Serlvet에서 URI에 해당하는 Controller를 HandlerMapping 객체를 통해 받아온다
    • Controller에서 logic을 처리하고 View Name을 Return해준다
    • Return받은 View Name을 ViewResolver로 다시 재 조합한다
    • 재조합된 View Name으로 응답을 보낸다
  3. Handler Mapping 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class HandlerMapping {
    private Map<String, Controller> mappings;

    public HandlerMapping() {
    // 모든 컨트롤러 객체를 Map에 등록한다
    mappings = new HashMap<String, Controller>();
    // USERS관련 컨트롤러 등록
    mappings.put("/login.do", new LoginController());
    mappings.put("/logout.do", new LogoutController());

    // BOARD 관련 컨트롤러 등록
    mappings.put("/getBoardList.do", new GetBoardListController());
    mappings.put("/insertBoard.do", new InsertBoardController());
    mappings.put("/updateBoard.do", new UpdateBoardController());
    mappings.put("/deleteBoard.do", new DeleteBoardController());
    mappings.put("/getBoard.do", new GetBoardController());
    }

    public Controller getController(String path) {
    // 매개변수로 받은 path에 해당하는 컨트롤러 객체를 검색하여 리턴한다.
    return mappings.get(path);
    }
    }
    • 각 요청에 대한 Controller 객체를 HashMap에 저장한다
    • 요청이 들어오면 HashMap에서 해당 Controller 객체를 가져온다
  4. 해당 Controller 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class InsertBoardController implements Controller {
    @Override
    public String handleRequest(HttpServletRequest request, HttpServletResponse response) {
    String title = request.getParameter("title");
    String writer = request.getParameter("writer");
    String content = request.getParameter("content");

    BoardVO vo = new BoardVO();
    vo.setTitle(title);
    vo.setWriter(writer);
    vo.setContent(content);

    BoardDAO boardDAO = new BoardDAOJDBC();
    boardDAO.insertBoard(vo);

    return "getBoardList.do";
    }
    }
    • DAO에서 요청한 데이터를 가져오는 등의 Business Logic을 진행한다.
    • 응답으로 보내져야할 View Name을 Return한다.
  5. ViewResolver 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ViewResolver {
    //접두사
    private String prefix;

    //접미사
    public String suffix;

    public void setPrefix(String prefix) {
    this.prefix = prefix;
    }

    public void setSuffix(String suffix) {
    this.suffix = suffix;
    }

    public String getView(String viewName) {
    return prefix + viewName + suffix;
    }
    }
    • Controller에서 받아온 View Name에 경로설정과 파일 확장자와 같은 부분을 Prefix나 Suffix로 붙혀서 Return한다

Spring MVC (XML을 통한 설정)

Servlet Container와 Spring Container와의 관계

  • Web이 아닌 Spring Project에서 Spring Container를 구성할 때에는 GenericXmlApplicationContext나 AnnotationApplicationContext와 같은 Class를 통해서 xml이나 java파일을 설정파일로 등록하고 만들었다.

  • 하지만 Spring Web Project는 Spring Container를 구성할 때 XmlWebApplicationContext같은 WebApplicationContext Class로 형성되어진다.

  • Spring Web Project의 Spring Container 형성과정은 Web이 아닌 Spring Project와는 형성과정에 차이가 있다

  • Spring Web Project Container 형성과정

    image

    1. Tomcat과 같은 WAS가 처음 로딩될 때 Servlet Container를 생성한다

    2. Servlet Container를 통해서 DispatcherServlet에 요청이 들어왔을 때 DispatcherServlet이 메모리에 형성된다

    3. DispatcherServlet이 처음 생성되어 질 때 init() 메소드가 호출되어 지는데 그 메소드에 Spring Container 생성 코드가 포함되어져 있다.

    4. XmlWebAppolication과 같은 객체에 Spring Bean 정보가 들어있는 action-servlet.xml을 받아서 Spring Container를 생성한다

      Servlet Container는 Servlet, Filter, Listener와 같은 클래스 밖에 관리를 못하지만, Spring Container는 일반 클래스들도 Bean으로 등록하여 관리할 수 있다.

  1. web.xml에 Spring DispatcherServlet 등록

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/presentation-layer.xml</param-value>
    </init-param>
    </servlet>
     
    <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    • 이전에는 직접 구현한 DispatcherServlet을 web.xml에 등록했다
    • 지금은 Spring Framework에서 만들어진 Dispatcher Servlet을 모든 요청에 매핑되도록 등록했다
    • Spring Container 빈 설정파일은 action-servlet.xml와 같은 이름 기본 등록되어있다
    • 다른 이름의 xml로 바꾸고싶다면 param에 contextConfigLocation을 변경해주면 된다
  2. presentation-layer.xml 설정 (Spring Bean 설정파일)

    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
    <?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-3.0.xsd

    <!-- 모든 컨트롤러 클래스들을 bean으로 등록한다 -->
    <bean id="getBoardList" class="com.rubypaper.web.controller.board.GetBoardListController">
    </bean>
    <bean id="login" class="com.rubypaper.web.controller.user.LoginController">
    </bean>
    <bean id="logout" class="com.rubypaper.web.controller.user.LogoutController">
    </bean>
    <bean id="getBoard" class="com.rubypaper.web.controller.board.GetBoardController">
    </bean>
    <bean id="insertBoard" class="com.rubypaper.web.controller.board.InsertBoardController">
    </bean>
    <bean id="updateBoard" class="com.rubypaper.web.controller.board.UpdateBoardController">
    </bean>
    <bean id="deleteBoard" class="com.rubypaper.web.controller.board.DeleteBoardController">
    </bean>

    <!-- 클라이언트의 요청을 어떤 컨트롤러가 처리할지 HandlerMapping으로 매핑한다 -->
    <!-- handleMapping id 고정 (spring에서 등록한 bean은 마지막 단어 두개 중 첫 단어만 소문자로 변경해서 id로 등록)-->
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
    <props>
    <prop key="/login.do">login</prop>
    <prop key="/logout.do">logout</prop>
    <prop key="/getBoardList.do">getBoardList</prop>
    <prop key="/getBoard.do">getBoard</prop>
    <prop key="/insertBoard.do">insertBoard</prop>
    <prop key="/updateBoard.do">updateBoard</prop>
    <prop key="/deleteBoard.do">deleteBoard</prop>
    </props>
    </property>
    </bean>

    <!-- View Resolver를 등록한다
    브라우저는 절대 서버가 관리하는 프로젝트의 WEB-INF폴더에 접근할 수 없다.
    따라서 브라우저가 직접 접근해서는 안되는 파일은 WEB-INF폴더에 은닉한다.
    -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/board/"/>
    <property name="suffix" value=".jsp"/>
    </bean>

    </beans>
    • 모든 컨트롤러 클래스를 bean으로 등록한다
    • 클라이언트 각 요청의 URI마다 어떤 Controller가 처리할 지 Spring HandlerMapping으로 매핑한다
    • Spring ViewResolver을 등록한다
    • 브라우저는 WEB-INF 폴더에 접근할 수 없으므로 브라우저가 접근하면 안되는 파일은 WEB-INF 디렉토리에 은닉한다
  3. Spring Controller Interface를 구현한 Controller 클래스 생성

    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
    public class LoginController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    String id = request.getParameter("id");
    String password = request.getParameter("password");

    UserVO vo = new UserVO();
    vo.setId(id);
    vo.setPassword(password);

    UserDAO userDAO = new UserDAOJDBC();
    UserVO user = userDAO.getUser(vo);

    ModelAndView mav = new ModelAndView();
    if (user != null) {
    HttpSession session = request.getSession();
    session.setAttribute("user", user);
    // forward: 이나 redirect: 을 뷰 이름 앞에 붙이면 ViewResolver를 무시한다
    mav.setViewName("forward:getBoardList.do");
    } else {
    mav.setViewName("redirect:login.jsp");
    }
    return mav;
    }
    }
    • org.springframework.web.servlet.mvc.Controller의 구현체 클래스를 만든다
    • 이전에는 View Name을 String 값으로 return했다면 지금은 ModelAndView 객체로 Return한다
    • Spring ViewResolver를 거치지 않고 ViewName을 설정하려면 redirect: 나 forward: 를 ViewName에 추가한다

Spring MVC (Annotation을 통한 설정)

  1. ComponentScan 설정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <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.2.xsd">
     
    <!-- 컴포넌트 스캔 설정 -->
    <context:component-scan base-package="com.rubypaper"/>>
    </beans>
    • @Controller를 Bean으로 인식하기 위해서 ComponentScan을 할 package를 지정한다
    • xml로 component scan을 설정하지 않고 @ComponentScan Annotation을 시작 패키지안의 클래스에 붙여줘도 된다
  2. Controller 설정

    1
    2
    3
    4
    5
    6
    7
    8
    @Controller
    public class InsertBoardController {
     
    @RequestMapping(value="/insertBoard.do")
    public void insertBoard(BoardVO vo) {

    }
    }
    • Controller Class에 @Controller Annotation을 추가해서 Spring Container에게 Controller클래스로 Bean을 등록한다

    • Controller Class안의 메소드에 매핑할 URI값을 @RequsetMapping(value = “URI”)로 설정한다

    • Controller 메소드의 Parameter 매핑
      image

      • Client에서 input 태그의 name값으로 요청이 들어오면 매핑되는 Method의 파라미터값에서 name과 매핑되는 setter메소드를 통해 값이 바인딩된다
    • 요청 방식에 따른 @RequestMapping 사용

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Controller
      public class LoginController {
      @RequestMapping(value="/login.do", method=RequestMethod.GET)
      public String loginView(UserVO vo) {
      vo.setId("test");
      vo.setPassword("test123");
      return "login.jsp";
      }
       
      @RequestMapping(value="/login.do", method=RequestMethod.POST)
      public String login(UserVO vo, UserDAO userDAO) {
      if(userDAO.getUser(vo) != null) return "getBoardList.do";
      else return "login.jsp";
      }
      }
    • @RequestMapping에서 매핑될 URI를 value로 정하고 Http Method를 method값에 지정한다

    • @RequestMapping은 Http Method에 따라서 @GetMapping, @PostMapping등으로 사용될 수 있다

Layer 통합하기 (Presentation-Layer, Business-Layer)

DispatcherServlet를 통해 만들어진 Spring Container만 사용

image

  • 요청이 들어오면 DispatcherServlet이 생성되고 생성될 때 presentation-layer.xml을 가져와서 xml에 설정되어진 Bean들을 가지고 Spring Container가 생성된다
  • 해당 Spring Container에는 Controller Bean들만 등록되어지고 직접 DAO에 접근해서 로직을 처리한다

Service를 통해서 Controller가 접근 시 문제점

image

  • Controller를 직접 DAO로 접근시키지 않고 Service Class를 통해서 접근한다
  • 하지만 presentaion-layer.xml에 등록되어진 Bean은 Controller Bean 밖에 없다
  • 따라서 해당 Spring Container는 Service Bean들에 접근할 수 없다

Layer를 분리하여 연결

  • Presentation Layer와 Business Layer를 분리한다
    image

    • Controller에 해당하는 부분은 Presentation Layer로 분리한다
    • DAO, Service에 해당하는 부분은 Business Layer로 분리한다
    • Controller는 분리된 두개의 개층을 Service Class를 통해서 접근한다
  • Business Layer용 Spring Container 생성

    • ContextLoaderListener 등록
      1
      2
      3
      4
      5
      6
      7
      8
      9
      <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:business-*.xml</param-value>
      </context-param>
      <listener>
      <listener-class>
      org.springframework.web.context.ContextLoaderListener
      </listener-class>
      </listener>
      • Servlet 설정 파일인 web.xml에 ContextLoaderListener를 등록한다
      • 해당 Listener는 business layer 설정파일 xml을 가지고 있다
      • ContextLoaderListener는 ServletContextListener를 구현하고 있다.
      • ServletContextListener는 Servlet Container가 시작하거나 종료 될 때 동작하는 Listenr이다.
      • 따라서 ContextLoaderListener는 Servlet Container가 시작할 때 contextLoader로 business layer xml 정보를 가지고 WebApplicationContext를 만들어서 Spring Container를 생성한다
  • Container간의 관계
    image

    • Root Spring Container가 ContextLoaderListener를 통해서 생성된 Container로 Service와 DAO Bean들을 가지고 있다

    • 하위 Spring Contaienr를 DispatcherServlet을 통해서 생성된 Container로 Controller를 Bean으로 가지고 있다

    • 하위 Container는 Root Container의 Bean들을 상속받아서 사용한다

    • 이제 Controller는 Root Spring Container에 생성되어진 Service Bean에 접근이 가능해진다

  • Container 생성과정

    1. Tomcat Server를 가동하면 Servlet Container가 생성된다

    2. Servlet Container가 생성될 때 ContextLoadListener가 동작해여 businees layer xml 설정정보를 가지고 Spring Container가 생성된다

    3. Server가 가동된 이후에 Client를 통해서 요청이 들어오면
      DispatcherServlet이 메모리에 생성되어진다

    4. DispatcherServlet이 생성되어 초기화 될 때 presentation layer xml 설정정보를 가지고 또 다른 Spring Container를 생성한다

      Web Application에서 생성되어진 Container는 Servlet Container, Businees Spring Container, Presentation Layer Container 총 3개이다

Comment and share


스프링 PSA(Portable Service Abstraction)

환경의 변화에 상관없이 일관된 방식의 기술 접근 환경을 제공하려는 추상화 구조

Spring은 Spring Web MVC, Spring Transaction, Spring Cache등의 다양한 기술에 Portable Service Abstraction을 제공하고 있다


Web MVC와 관련된 Service Abstraction

Spring MVC는 서블릿 어플리케이션임에도 불구하고 서블릿이 전혀 겉으로 들어나지 않았다.

단지 @Controller 어노테이션이 붙어잇는 클래스에서 @GetMapping, @PostMapping과 같은 @RequestMapping 애노테이션을 사용해서 요청을 매핑한다

실제롤는 내부적으로 서블릿 기반으로 코드가 동작하고 있지만 서블릿 기술은 추상화 계층에 숨겨져 있는 것이다.

이렇게 추상화 계층을 사용하여 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공해주는 것을 Service Abstraction이라 한다.

더하여 Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것이 Portable Service Abstraction이다.


Spring Web MVC

Service Abstraction

일반 클래스에 @Controller 애노테이션을 사용하면 요청을 매핑할 수 있는 컨트롤러 역할을 수행하는 클래스가 된다.

그 클래스에서는 @GetMapping과 @PostMapping 애노테이션을 사용해서 요청을 매핑할 수 있다

요청을 처리하는 메소드들은 뷰를 리턴하며 각 메소드에서 model에 담은 객체가 모델에 해당한다

Spring Web MVC를 사용하면 이렇게 서블릿을 간편하게 개발할 수 있는데, 뒷단에 spring이 제공해주는 여러 기능들이 숨겨져 있기 때문이다.

즉 서블릿의 HttpServlet을 상속받고 doGet(), doPost() 작성하는 등의 작업을 직접하지 않아도 된다

Service Abstarction의 목적 중 하나가 이러한 편의성을 제공하는 것이다.

Portable

Spring Web MVC가 Tomcat기반을 동작하고 있을 때 코드를 거의 그대로 둔 상태에서 톰캣이 아닌 완전히 다른 기술 스택으로 실행하는 것이 가능하다.

spring-boot-starter-web 의존성 대신 프로젝트를 spring-boot-starter-webflux로 변경하기만 하면 톰캣이 아닌 netty기반을 실행되게 할 수 도 있다.

원래 기존에 톰캣으로 실행하던 프로젝트를 netty기반으로 실행하게 하려면 더 복잡한 과정이 필요하지만 spring이 제공해주는 Spring Web MVC 추상화 계층을 사용해서 간단히 netty로 실행할 수 있는 것이다.

이렇게 Spring Web MVC는 @Controller, @RequestMapping과 같은 애노테이션과 뒷단의 여러가지 복잡한 인터페이스들 그리고 기술들을 기반으로 하여 사용자가 웹 기술 스택을 간편하게 바꿀 수 있도록 해준다

중요한 것은 이런 것들이 기존 코드를 거의 변경하지 않고도 가능하다는 것이다.


Spring Transaction

트랜잭션을 처리하려면 setAutoCommit()과 commit(), rollback()을 명시적을 호출해야 한다

그러나 Spring이 제공하는 @Transactional 애노테이션을 사용하면 단순히 메소드에 애노테이션을 붙여줌으로써 트랜잭션 처리가 이루어진다.

이 또한 PSA로써 다양한 기술 스택으로 구현체를 바꿀 수 있다.

예를 들어 JDBC를 사용하면 DatasourceTransactionManager, JPA를 사용하는 JpaTransactionManager, Hibernate를 사용하는 HibernateTransactionManager를 유연하게 바꿔서 사용할 수 있다.

즉 기존 코드는 변경하지 않은 채로 트랜잭션을 실제로 처리하는 구현체를 사용 기술에 따라 바꿔 끼울 수 있다


Spring Cache

Cache도 마찬기지로 JCacheManagerm ConcurrentMapCacheManager, EhCacheManager와 같은 여러가지 구현체를 사용할 수 있다.

사용자는 @Cacheable 애노테이션을 붙여줌으로써 구현체를 크게 신경쓰지 않아도 필요에ㄷ 따라 바꿔 쓸 수 있는 것이다.

이렇게 spring이 제공해주는 다양한 PSA 기술 덕분에 코드는 더 견고해지고 기술이 바뀌어도 유연하게 대처할 수 있게 된다.

Comment and share


스프링 AOP(Aspect Oriented Programming)

AOP

AOP는 Asepect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.

관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고

부가적인 관점을 각각 따로 분리해서 모듈화하겠다는 것이다.

예로들어 핵심적인 관점은 우리가 적용하고자 하는 핵심 비지니스 로직이 된다.

또한 부가적인 관점은 핵심 로직외에 행해지는 데이터베이스 연결, 로깅, 파일 입출력등이 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화하겠다는 의미다.

이때, 소스 코드상에서 핵심 비지니스 로직(Core Concerns)외에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.

스크린샷 2020-11-27 오후 12 59 42

위와 같이 흩어진 관심사(Crosscutting Concerns)를 Aspect로 모듈화하고 핵심적인 비지니스 로직(Core concerns)에서 분리하여 재사용하겠다는 것이 AOP이다.

AOP 관련 용어

  • 조인 포인트(JoinPoint) : 클라이언트가 호출하는 모든 비지니스 메소드, 조인포인트 중에서 포인트컷이 되기때문에 포인트 컷의 후보로 생각할 수 있다.
  • 포인트 컷(Pointcut) : 특정 조건에 의해 필터링된 조인 포인트, 수 많은 조인 포인트 중에 특정 메소드에서만 Advice를 수행시키기 위해서 사용된다
  • 어드바이스(Advice) : Crosscutting Concern에 해당하는 공통 기능의 코드
  • 애스펙트(Aspect) : 포인트 컷과 어드바이스의 결합이다. 어떤 포인트 컷 메소드에 대해 어떤 어드바이스 메소드가 실행할 지 결정한다.
  • 위빙(Weaving) : 핵심 비지니스 로직과 Aspect를 연결하여 Advised Object를 만드는 과정
    1. complie time
    2. load time
    3. runtime

AOP 구현 방법

  1. 컴파일

    • A.java —— (AOP) —–> A.class
    • 컴파일을 할 때 중간에 공통 로직을 끼워 넣는다
    • 컴파일하기 전 코드에는 해당 로직이 없지만, 컴파일이 완료되면 해당 로직을 가지고
      있는 상태이다
    • AspectJ를 이용해서 만들어진다
  2. 바이트코드 조작

    • A.java —-> A.class —-> (AOP) —-> 메모리
    • load time에서 ClassLoader가 A.class를 읽어온다
    • 읽어와서 메모리에 올릴 때 공통 로직이 들어가도록 조작한다
    • AspectJ를 이용해서 만들어진다
  3. 프록시 패턴

    • Runtime에서 구현하는 방식
    • 프록시 패턴을 적용하여 실제 객체가 실행되는 것이 아닌 실제 객체를 감싸고 있는 프록시 객체를 Runtime에 생성
    • 프록시 객체는 Aspect가 적용되어있고 Core Concern이 들어있는 실제 객체를 reference하고 있다
    • AOP가 적용된 객체를 요청할 때 실제 객체가 아닌 프록시 객체가 실행된다.
    • AspectJ로 만들어져 있으며 SPRING AOP는 이 방식으로 구현되어있다.

Spring AOP

스프링 AOP 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있다
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, Spring IoC와 연동하여 어플리케이션의 문제에 대한 해결책을 제공하는 것이 목적

프록시 패턴

  • 접근 제어 또는 부가기능 추가

    image

    • 인터페이스에 대해서 핵심 비지니스 로직만으로 구성되어진 실제 객체를 만든다
    • 인터페이스에 대해서 또 하나의 Proxy 구현체를 만든다
    • 이 구현체에는 부가기능이 추가있으며 실제 객체를 참조하여 실제 객체의 비지니스 로직을 실행 시킬 수 있다
    • 클라이언트는 객체를 인터페이스를 통해서 접근하게 되며, 실제객체가 직접 실행되는 것이 아닌 Proxy객체를 통해서 부가기능을 수행하고 간접적으로 실행된다.

프록시 패턴을 적용 (Spring AOP를 사용하지 않음)

  • EventService Interface

    1
    2
    3
    4
    5
    6
    7
    public interface EventService {
    void createEvent();

    void publishEvent();

    void deleteEvent();
    }
  • EventService 구현체 (핵심 비지니스 로직 수행)

    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
    @Service
    public class SimpleEventService implements EventService{

    @Override
    public void createEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }
  • EventService의 Proxy 구현체 (부가기능 + 핵심 로직 객체 참조)

    @Primary로 SimpleEventService가 아닌 ProxySimpleEventService이 실행됨

    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
    @Primary
    @Service
    public class ProxySimpleEventService implements EventService{

    @Autowired
    SimpleEventService simpleEventService;

    @Override
    public void createEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.createEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.publishEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
    simpleEventService.deleteEvent();
    }
    }

문제점

  • 매번 프록시 패턴을 적용해서 프록시 클래스를 작성해야한다
  • 여러 클래스 여러 메소드들에 적용할려면 반복적인 작업이 증가한다
  • 객체들관의 관계도 복잡하다

스프링 AOP 등장

  • 스프링 IoC컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제 해결
  • 동적 프록시 : 동적으로 프록시 객체를 생성하는 방법
    • 자바가 제공하는 방법은 인터페이스 기반 프록시 생성
    • CGlib은 클래스 기반 프록시도 지원
  • 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록해준다
    • 클라이언트 코드에 변경이 없다

스프링 AOP 적용해서 개선

스프링 AOP는 Spring Container에 등록된 Bean들만 적용된다

  • Pointcut을 Annotation 기반으로 작성

    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.METHOD)
    public @interface PerfLogging {
    }
  • Aspect 작성 (Around 방식을 적용한 Advice 메소드 작성)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class PerfAspect {

    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
    }

    @Before("bean(simpleEventService)")
    public void hello(){
    System.out.println("hello");
    }
    }
    • Around방식은 핵심 비지니스로직 실행 권한을 ProceedingJoinPoint로 받아온다 (Spring Container가 받아옴)
    • 받아온 객체를 실행하기 전 후에 작업을 진행한다
    • 받아온 객체가 return값을 가지고 있으면 Advice메소드에서 그 값을 return해 주어야한다
  • AOP를 적용할 Pointcut메소드를 지정한다(메소드 위에 어노테이션 설정)

    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
    @Service
    public class SimpleEventService implements EventService{
    @PerfLogging
    @Override
    public void createEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Created an event");
    }

    @PerfLogging
    @Override
    public void publishEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    @Override
    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }

Pointcut 정의 방식

  1. execution 설정 방식

    • 사용 예

      1
      @Around("execution(* com.example..*.EventService.*(..))")
    • 적용 규칙

      스크린샷 2020-11-27 오후 2 51 06
  2. bean 설정 방식

    • 사용 예

      적용할 빈 이름을 직접 입력

      1
      @Around("bean(simpleEventService)")
  3. annotation 설정 방식 (위에서 설명)

Advice 실행 시점

  1. Before

    • 핵심 비지니스 로직 실행 전에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @Before("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사전 처리> " + method + "비지니스 로직 수행 전 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  2. After

    • 핵심 비지니스 로직 실행 후에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @After("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사후 처리> " + method + "비지니스 로직 수행 후 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  3. After Returning

    • 핵심 비지니스 로직 실행한 후 반환된 객체를 Advice에서 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @AfterReturning(pointcut = "!void com.rubypaper.biz..*Impl.*(..))", returning = "returnObj")
      public void afterLog(Object returnObj) {
      System.out.println("<사후 처리> 비지니스 로직 리턴 값 : " + returnObj.toString());

      if (returnObj instanceof UserVO) {
      UserVO user = (UserVO) returnObj;
      if(user.getRole().equals("ADMIN")) {
      System.out.println(user.getName() + "님은 관리자 화면으로 바로 이동........");
      }
      }
      }
    • 핵심 비지니스 로직에서 반환된 객체를 returnObj로 받아온다
  4. After Throwing

    • 핵심 비지니스 로직 실행헌 후 예외가 발생하면 예외객체를 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
       @AfterThrowing(pointcut = "execution(* com.rubypaper.biz..*Impl.*(..))", throwing = "exceptionObj")
      public void exceptionLog(JoinPoint jp, Exception exceptionObj) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름

      System.out.println("[ 예외처리 ]"+ method +" 메소드 수행 중 예외 발생");

      if(exceptionObj instanceof IllegalArgumentException){
      System.out.println("0 번 게시 글을 등록할 수 없습니다");
      } else if (exceptionObj instanceof ArithmeticException) {
      System.out.println("0 으로 숫자를 나눌 수 없습니다");
      } else if (exceptionObj instanceof SQLException) {
      System.out.println("SQL 구문에 오류가 있습니다");
      } else {
      System.out.println("문제 발생 !! 시스템을 잠시 종료합니다");
      }
      }
      • 예외객체를 exceptionObj로 받아온다
  5. Around

    • 위에서 설명

Advice에서 Pointcut에 해당되는 메소드에 대한 정보를 얻기 위해 JoinPoint라는 객체를 받아올 수 있다. Around만 예외적으로 ProceedJoinPoint라는 객체를 통해서 받아온다

Comment and share


백준 1759 : 문제링크

  • 문제유형 :

    • 백 트레킹
  • 설명

    • C개의 문자들중에서 L개를 선택하는 모든 조합을 고려한다
    • 모음의 개수와 자음의 개수 조건을 확인한다
  • 풀이

    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
    import copy

    result = []
    string = []

    def combination(array, length, index):
    if len(string) == length:
    result.append(copy.deepcopy(string))
    return
    for i in range(index, len(array)):
    string.append(array[i])
    combination(array, length, index + 1)
    string.pop()

    vowels = ('a', 'e', 'i', 'o', 'u')
    l, c = map(int, input().split(' '))

    array = input().split(' ')
    array.sort()

    combination(array, l, 0)

    for password in result:
    count = 0:
    for i in password:
    if i in vowels:
    count += 1

    if count >= 1 and count <= l - 2:
    print(''.join(password))

Comment and share


백준 1987 : 문제링크

  • 문제유형 :

    • 백 트레킹
  • 설명

    • 말을 상하좌우 네 가지 방향으로 이동시킨다
    • 지금까지 지나온 모든 칸에 적힌 알파벳과 다른 알파벳이 적인 칸으로 이동
    • 백 트레킹을 이용하여 다른 알파벳이 적힌 칸으로 이동하도록 한다
  • 풀이

    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
    dx = [-1, 1, 0, 0]
    dy = [0, 0, 1, -1]

    def bfs(x, y):
    global result
    q = set()
    q.add((x, y, array[x][y]))

    while q:
    x, y, step = q.pop()
    result = max(result, len(step))

    for i in range(4):
    nx = x + dx[i]
    ny = y + dy[i]

    if (0 <= nx and nx < r and 0 <= ny and ny < c
    and array[nx][ny] not in step)
    q.add(nx, ny, step + array[nx][ny])

    r, c = map(int, input().split())
    array = []
    for _ in range(r):
    array.append(input())

    result = 0
    bfs(0, 0)
    print(result)

Comment and share


백준 9663 : 문제링크

  • 문제유형 :

    • 백 트레킹
  • 설명

    • DFS를 이용하여 백트래킹 알고리즘을 구현한다
    • 각 행을 차례대로 확인하면서, 각 열에 퀸을 놓는 경우를 고려한다
    • 이 때 위쪽 행을 모두 확인하며, 현재 위치에 놓을 수 있는지 확인한다
  • 풀이

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def check(x):
    for i in range(x):
    if row[x] == row[i]:
    return False
    if abs(row[x] - row[i]) == x - i:
    return False
    return True

    def dfs(x):
    global result
    if x == n:
    result += 1
    else:
    for i in range(n):
    row[x] = i
    if check(x):
    dfs(x+1)

    n = int(input())
    row = [0] * n
    result = 0
    dfs(0)
    print(result)

Comment and share


백준 1781 : 문제링크

  • 문제유형 :

    • 탐욕 알고리즘
  • 설명

    • 정렬 및 우선순위 큐를 이용하여 O(NlogN)의 시간에 해결할 수 있다
    • 데드라인 기준으로 오름차순 정렬을 수행한다
    • 각 문제의 컵라면 수를 우선순위 큐에 넣으면서, 데드라이을 초과할 결우 최소원소를 제거한다
  • 풀이

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import heapq

    n = int(input())
    array = []
    q = []

    for i in range(n):
    a, b = map(int, input().split(' '))
    array.append((a, b))
    array.sort()

    for i in array:
    a = i[0]
    heapq.heappush(q, i[1])
    if a < len(q):
    heapq.heappop(q)

Comment and share


백준 1461 : 문제링크

  • 문제유형 :

    • 탐욕 알고리즘
  • 설명

    • 일직선상에 책들을 순서대로 위치시킨다
    • 0보다 큰 책들과 0보다 작은 책들을 나누어서 처리한다
    • 음수와 양수 좌표를 위해 두 개의 우선순위 큐를 이용한다
    • 마지막 책을 놓을 때는 다시 0으로 돌아올 필요가 없기 때문에, 가장 먼 책을 마지막으로 놓는다
  • 풀이

    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
    import heapq

    n, m = map(int, input().split(' '))
    array = list(map(int, input().split(' ')))
    positive = []
    negative = []

    largest = max(max(array), - min(array))

    for i in array:
    if i > 0:
    heapq.heappush(positive, -i)
    else:
    heapq.heappush(negative, i)

    result = 0

    while positive:
    result += heapq.heappop(positive)
    for _ in range(m - 1):
    if positive:
    heapq.heappop(positive)

    while negative:
    result += heapq.heappop(negative)
    for _ in range(m - 1):
    if negative:
    heapq.heappop(negative)

    print(-result * 2 - largest)

Comment and share


백준 1092 : 문제링크

  • 문제유형 :

    • 탐욕 알고리즘
  • 설명

    • 정렬만 수행하면 되므로 시간복잡도는 O(NlogN)
    • 각 센서를 오름차순으로 정렬한다
    • 각 센서 사이의 거리를 계산한다
    • 가장 거리가 먼 순서대로 k - 1개의 연결 고리르 제거한다
  • 풀이

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import sys

    n = int(input())
    k = int(input())

    if k >= n:
    print(0)
    sys.exit()

    array = list(map(int, input().split(' ')))
    array.sort()

    distance = []
    for i in range(1, n):
    distances.append(array[i] - array[i - 1])
    distance.sort(reverse=True)

    for i in range(k - 1):
    distances[i] = 0
    print(sum(distances))

Comment and share

Yechan Kim

author.bio


author.job