Spring

[AOP] AOP개념의 이해 : 횡단관심_aop(1)

amoomar 2022. 4. 4. 16:39
반응형

 

목차는 다음과 같다.

1. 복습
   1) Controller수정
   2) 문제해결
   3) 크롤링 접목 순서
2. AOP 사용 예시
   1) 흐름
   2) 사용 예시
   3) 예제
3. 핵심관심의 동작시점
4. Service클래스 존재의 이유
   1) 동작 순서
   2) 존재의 이유

 

 

1. 복습

 

1) Controller수정

이전 포스팅에서 수정 방법에 대해 서술했기 때문에, 해당 목차에서는 변경이 완료된 코드만을 첨부하였다.

 

① BoardController

package com.test.app.controller.board;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.test.app.board.BoardVO;
import com.test.app.board.impl.BoardDAO;

@Controller
// 1. <bean> -> @Controller
// 2. implements Controller 필요없음 -> 제거
// 3. 메서드 강제 사라짐!!!
// 4. 완전한 POJO가 되었다!!!!!
// 		-> req,res,...가 존재하지않음 => 경량의 객체
public class BoardController { // 하나의 컨트롤러파일로 아래의 메서드들을 관리

	@RequestMapping(value="/insertBoard.do") //이런 요청이 오면,
	public String insertBoard(BoardVO vo,BoardDAO boardDAO) { //이 메서드를 사용
		boardDAO.insertBoard(vo);
		return "redirect:main.do";
	}
	@RequestMapping(value="/deleteBoard.do")
	public String deleteBoard(BoardVO vo,BoardDAO boardDAO) {
		boardDAO.deleteBoard(vo);
		return "redirect:main.do";
	}
	@RequestMapping(value="/main.do")
	public ModelAndView getBoardList(BoardVO vo, BoardDAO boardDAO, ModelAndView mav) {
		List<BoardVO> datas=boardDAO.getBoardList(vo);
		mav.addObject("datas", datas); // Model을 이용하여 전달할 정보를 저장!
		mav.setViewName("main.do");
		return mav;
	}
	@RequestMapping(value="/updateBoard.do")
	public String updateBoard (BoardVO vo, BoardDAO boardDAO) {
		boardDAO.updateBoard(vo);
		return "redirect:main.do";
	}
	
}

 


 

 

② MemberController

package com.test.app.controller.member;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.test.app.member.MemberVO;
import com.test.app.member.impl.MemberDAO;

@Controller
public class MemberController {

	@RequestMapping(value = "/deleteMember.do")
	public String deleteMember (MemberVO vo, MemberDAO dao){
		dao.deleteMember(vo);
		return "redirect: login.jsp";
	}

	@RequestMapping(value = "insertMember.do")
	public String insertMember (MemberVO vo, MemberDAO dao){
		dao.insertMember(vo);
		return "redirect:login.jsp";
	}
	
	@RequestMapping(value = "updateMember.do")
	public String updateMember (MemberVO vo, MemberDAO dao){
		dao.updateMember(vo);
		return "redirect:main.do";
	}	
	
	// HttpSession session=request.getSession(); 또한 command 객체가 진행해주므로, 인자로 넣어주면 된다.
	
	@RequestMapping(value = "logout.do")
	public String logout (HttpSession session) {
		session.invalidate();
		return "redirect:login.jsp";
	}
	
	@RequestMapping(value = "login.do")
	public String getMember (MemberVO vo, MemberDAO dao,HttpSession session) {
		vo=dao.getMember(vo); //vo로 검색 결과 반환
		if(vo==null){//검색 결과가 없으면,
			return "redirect:login.jsp"; //현재 페이지 유지
		} else {// 검색 결과가 있으면,
			session.setAttribute("member", vo); //해당 데이터를 세션에 저장
			return "redirect:main.do"; //메인페이지로 이동
		}
	}
	
}

 

 


 

2) 문제해결

만일 설정 정보와 관련하여 문제가 생겼다면, 특정 행동을 통해 문제를 해결할 수 있다. 문제 범위는 filter의 적용이 정상적으로 되지 않는다거나, 혹은 코드를 입력 했음에도 스프링 컨테이너가 클래스를 인식하지 못하는 등의 설정과 관련된 에러상황이 있다. 해결 방법은 아래와 같다.

 

 

1. 프로젝트 우클릭-Properties-Deployment Assembly

Maven부분이 추가가 되어있어야하는데, 추가되지 않은 경우 위와 같은 오류가 발생할 수 있다.

파란 박스인 부분을 확인

 

 

 

2. add버튼 클릭

메이븐과 관련된 설정 사항이 없다면 사진과 동일한 방법을 통해 추가할 수 있다.

add버튼 클릭 후 확인되는 화면,

 

 

maven 추가

 

 


 

3) 크롤링 접목 순서

Spring으로 크롤링을 사용하기 위해 jsp에서와 같이 리스너 클래스를 활용할 수도 있으나, 그 외의 방법으로 크롤링한 데이터를 불러오기 위해 아래와 같은 방법을 사용할 수 있다.

 

내용 설명

 

insert메서드 내부에서 Craw c=new Craw();로 크롤링과 관련된 클래스를 초기화 하여 해당 메서드 내부에서 사용할 수도 있겠으나, 스프링 컨테이너가 new 작업을 지원해주는데 굳이 개발자 스스로 객체화를 진행하여 유지보수를 불리하게 할 이유가 없다.

 

컨테이너가 해당 Craw클래스를 new 해줄 수 있도록 하기 위해 어노테이션을 활용하여 설정을 진행한 메모이고, 만일 xml파일을 통해 설정하고 싶었다면 생성자 주입 방식이나 세터 주입 방식중 하나를 채택하여 사용할 수 있다. 이때, 각 방법에 따라 해당 클래스에 생성자가 있거나, setter가 있어야 할 것이다.

 

 


 

2. AOP 사용 예시

 

1) 흐름

AOP란, 공통 로직(횡단 관심) + 핵심 로직(비즈니스 메서드,CRUD)이다.

즉, 핵심 로직에 공통 로직을 끼워넣는 개념이라고 정리할 수 있다.

 

 

사용 ex) 로깅, 인증, 허가, 트랜잭션, 연결, 해제 등이 있는데, 그 중 로깅을 예시로 들어 AOP개념을 접목해볼 예정이다. 

 

AOP를 간단히 그림으로 설명하자면 아래와 같다.(링크 첨부된 포스팅의 2번 목차 내용 확인)

검정색으로 표시된 박스(계좌이체, 출금, 입금)가 핵심관심(혹은 CRUD, 혹은 비즈니스 메서드)이고, 빨강색으로 표시된 박스가 각 메서드마다 공통적으로 사용되어야할 공통관심(혹은 횡단관심, 공통로직)이라고 할 수 있다. 이때, 각각의 횡단관심은 각각의 핵심관심마다 사용되어야하므로 중복되는 코드가 발생하게 된다. 이 부분을 분리하여 관리하기 위해 AOP개념을 활용한다.

관계도

 

 

 

로그 사용시 공통적으로 사용되는 로직을 클래스로 분리한다. 이때, advice는 횡단관심이다.(=로그는 공통 로직이다.)

파일의 생성 위치

 

 

 

생성자 주입 방식을 통해 DI하며, BoardServiceImpl클래스에서 메서드를 사용하기 전에 로그를 남기게 할 예정이므로, 각 메서드 상단에서 printlog()가 수행될 수 있도록 코딩한다.

package com.test.app.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.test.app.board.BoardService;
import com.test.app.board.BoardVO;
import com.test.app.common.LogAdvice;

@Service("boardService")
// Shape: Circle,Rect,Tri,...
// @Component: @Service,@Repository,@Controller,...
public class BoardServiceImpl implements BoardService {

	@Autowired // DI
	private BoardDAO boardDAO;
	private LogAdvice log; //멤버변수화
	
	public BoardServiceImpl(LogAdvice log) {
		this.log=log;
	}
	
	@Override
	public void insertBoard(BoardVO vo) {
		log.printlog();
		boardDAO.insertBoard(vo);
	}

	@Override
	public BoardVO getBoard(BoardVO vo) {
		log.printlog();
		return boardDAO.getBoard(vo);
	}

	@Override
	public List<BoardVO> getBoardList(BoardVO vo) {
		log.printlog();
		return boardDAO.getBoardList(vo);
	}

	@Override
	public void updateBoard(BoardVO vo) {
		log.printlog();
		boardDAO.updateBoard(vo);
	}

	@Override
	public void deleteBoard(BoardVO vo) {
		log.printlog();
		boardDAO.deleteBoard(vo);
	}

}

 

 

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:p="http://www.springframework.org/schema/p"
	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
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<bean id="BoardServiceImpl" class="com.test.app.board.Impl">
		<constructor-arg ref="log"/>
	</bean>

	<bean id="log" class="com.test.app.common.LogAdvice"/>
	
</beans>

 

 

이때 아래의 내용을 담은 새로운 로깅 메서드로 사용을 변경해야할 상황이 발생하였을때, 상단에서 변경해야 하는 부분이 많아지는 것으로 미루어보아 DI를 했음에도 결합도가 여전히 높다는 것을 예측할 수 있다. 이런 부분을 해결하기 위해 "서블릿 컨테이너를 활용해보면 어떨까?" 하는 의문에서 AOP가 출발하였다.

package com.test.app.common;

public class LogAdvice2 {
	
   public void printlog2() {
      System.out.println("[로깅2] 기존 로그의 결함을 업데이트함!");
   }
}

 

 


 

2) 사용 예시

 

① AOP 라이브러리 추가

pom.xml파일의 설정 정보를 추가하여 라이브러리를 추가한다.

라이브러리 추가

 

사용되는 코드는 아래와 같다.

	<!-- ================ AOP라이브러리 ================== -->
 	<dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjrt</artifactId>
         <version>${org.aspectj-version}</version>
      </dependency>
      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjweaver</artifactId>
         <version>1.8.8</version>
      </dependency>
	<!-- ============================================== -->

 

 

정상적으로 라이브러리가 추가 되었는지 확인할 수 있다.

라이브러리 추가 확인

 

 

 

*이때, 라이브러리가 정상적으로 추가되지 않았다면 아래와 같이 추가할 수 있다.*

프로젝트 우클릭-Maven-Update Project

라이브러리 미추가 이슈 해결

 

 

 


 

② name Space 추가

아래와 같이 name Space에서 aop를 선택, 저장해준다.

name Space 추가

 

 


 

③ AOP 개념 도입

아래의 코드와 같이 태그를 사용하여 설정 정보를 삽입할 수 있다. 아래와 같은 코드인 경우, 새로운 로깅 메서드로 사용을 변경해야할 상황에 대하여 <bean>태그 부분과 <aop:before method="사용할 메서드">의 부분만 수정하는 방식으로 유지보수를 용이하게 할 수 있다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<!-- 설정정보 삽입 -->
	<aop:config>
		<!-- 어떤애랑 결합될지를 정의(expression=어쩌구저쩌구 임플리에 있는 모든 메서드에 대해서 / ) -->
		<aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="aPointCut"/>
		<aop:aspect ref="log"> <!-- 어떤 비스니스 메서드에 대해 설정할 것인지 -->
			<!-- 언제 수행할지 알려주는데, 원하는 시간에 따라 태그 다르게 사용 가능 -->
			<!-- aPointCut사용하기 전에 printlog메서드를 사용할 것이다. -->
			<aop:before method="printlog" pointcut-ref="aPointCut"/>
		</aop:aspect>
	</aop:config>
	
</beans>

 

위에서 나온 태그에 대해 각각 용어를 정리해보았다. 아래의 내용을 이해하면 위의 코드를 해석하는데에 용이하다.

 

1) 조인포인트(Joinpoint): 사용자(브라우저,Client)측에서 사용하는 모든 비즈니스메서드, 포인트컷 후보

 

2) 포인트컷(Pointcut): 횡단관심(공통로직)과 결합될 대상, 즉 내가 join point에서 지정한 핵심관심(핵심로직,비즈니스메서드,CRUD)

 ex) 트랜잭션: selectOne,selectAll 에는 해당 안됨

 

3) 어드바이스(Advice): 횡단 관심,공통 로직,공통 기능,... / 모든 횡단 관심은 동작시점을 가지고 있으며, 공통로직마다 동작 시점은 모두 상이하다.☆

 

4) 위빙(Weaving): 포인트컷이 호출될때, 횡단관심이 수행되는 동작 혹은 과정

=내가 미리 지정해둔 CRUD가 호출될때, 횡단관심이 지정해둔 동작 시험에 수행되는 과정

 

 

*위빙이 안되는 이유*

- 설정이 잘 안된 경우

- 되었으나, Console등에 출력이 안되는 경우

- .xml을 컨테이너가 인식하지 못하는 경우 등

 

 

5) 애스팩트(Aspect) or 어드바이저(Advisor): 관심 결합에 대한 설정 혹은 결합 그 자체 혹은 관점 분리

 

 

 

 


 

 

3) 예제

만족 조건은 아래의 사진과 같다.

조건

 

로깅을 위한 공통로직을 클래스화 하여 메서드 생성한다.

package com.test.app.common;

public class LogAdvice {

	public void printlog1() {
		System.out.println("[로깅] 데이터를 출력합니다.");
	}
	public void printlog2() {
		System.out.println("[로깅] 처리 완료");
	}
}

 

해당 클래스의 메서드를 사용하기 위해 Join PointCut 중 expression을 활용하여 특정 메서드 중 횡단 관심 수행의 기준이 될 메서드(pointCut)를 지정한다. 이후 어떤 PointCut의 동작 시점에 대해 어떤 객체의 어떤 메서드를 사용할 것인지를 설정한다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/><!-- @활용하여 참조한 패키지의 클래스들 new(초기화) -->

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<aop:config>
		<!-- select류 메서드에 대해 aPointCut으로 설정 -->
		<aop:pointcut expression="execution(* com.test.app..*Impl.get*(..))" id="aPointCut"/>
		<!-- 전체 비즈니스 메서드에 대해 bPointCut으로 설정 -->
		<aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="bPointCut" />
		<aop:aspect ref="log"> <!-- 사용할 클래스의 객체 -->
			<!-- 해당 PointCut(지정된 메서드의 사용)을 기준으로 하여 설정한 동작 시점에, method를 수행 할 것이다. -->
			<aop:before method="printlog1" pointcut-ref="aPointCut"/>
			<aop:after method="printlog2" pointcut-ref="bPointCut"/><!--  -->
		</aop:aspect>
	</aop:config>
	
</beans>

 

 

아래와 같은 내용의 main메서드에 대한 결과를 사진으로 첨부하였다.

package com.test.app.board;

import java.util.List;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class Client { // 클라이언트,사용자,브라우저
	public static void main(String[] args) {
		AbstractApplicationContext factory=new GenericXmlApplicationContext("applicationContext.xml");
		
		BoardService bs=(BoardService)factory.getBean("boardService");
		BoardVO boardVO=new BoardVO();
		boardVO.setTitle("어노테이션 실습");
		boardVO.setContent("진짜되나??");
		boardVO.setWriter("작은 티모");
		bs.insertBoard(boardVO);
		
		List<BoardVO> datas=bs.getBoardList(boardVO);
		for(BoardVO v:datas) {
			System.out.println(v);
		}
		
		factory.close();
	}
}

 

 

- bs객체의 insertBoard()사용 후에 "처리 완료" 출력됨(17번 라인)

- bs객체의 getBoardList()사용 전에 "데이터를 출려합니다" 로그 출력됨(19번 라인)

- bs객체의 getBoardList()사용 후에 "처리 완료" 출력됨(19번 라인)

 

 

 


 

3. 핵심관심의 동작시점

before - 수행 전
arter - finally와 유사한 성격으로 에러가 발생하던 안하던 관계 없이 실행됨
After:returning - 반환이 완료된 후에 실행
After:throwing - 반환이 실패했을때 (예외 발생시)

//After-throwing시점 확인을 위해, 일부러 예외 발생시키는 방법
@Override
public void insertBoard(BoardVO vo) {
	if(vo.getBid()==0) { //그냥 하면 언리쳐블 코드 : 죽은 코드라서 if문 사용
		throw new IllegalArgumentException("내가만든예외!");
	}
	boardDAO.insertBoard(vo);
}

내가 만든 예외발생에 대한 로그 확인

 


Around: 필터와 유사하게 동작함 - 사용자의 요청을 "탈취"하여 - 핵심 관심을 가져와서 마음대로 가공할 수 있다.

 

 

로깅을 위한 코드의 추가

package com.test.app.board.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.test.app.board.BoardVO;
import com.test.app.common.JDBCUtil;

@Repository("boardDAO") // 컴포넌트 대신 ! DB와 관련된 작업을 하는 컴포넌트
public class BoardDAO {

	private static final String BOARD_SELECTALL_TITLE = null;
	private Connection conn=null;
	private PreparedStatement pstmt=null;
	private ResultSet rs= null;

	private final String BOARD_INSERT="insert into board (title, writer, content) values(?,?,?)";
	private final String BOARD_SELECTONE="select * from board where bid=?";
	private final String BOARD_SELECTALL="select * from board order by bid desc";
	private final String BOARD_UPDATE="update board set title=?,content=? where bid=?";
	private final String BOARD_DELETE="delete board where bid=?";
	private final String BOARD_SELECTALL_SEARCH_TITLE = "select * from board where title like '%'|| ? ||'%'";
	private final String BOARD_SELECTALL_SEARCH_WRITER = "select * from board where writer like '%'|| ? ||'%'";
	private final String BOARD_SELECTALL_SEARCH_CONTENT = "select * from board where content like '%'|| ? ||'%'";
	
	public void insertBoard(BoardVO vo) {
		System.out.println("insertBoard메서드 호출됨");
		conn=JDBCUtil.connect(); 
		try {
			pstmt=conn.prepareStatement(BOARD_INSERT);
			pstmt.setString(1, vo.getTitle());
			pstmt.setString(2, vo.getWriter());
			pstmt.setString(3, vo.getContent());
			pstmt.executeUpdate();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			JDBCUtil.disconnect(pstmt, conn);
		}
	}
	
	public List<BoardVO> getBoardList(BoardVO vo) {
		System.out.println("getBoardList메서드 호출됨");
		List<BoardVO> datas=new ArrayList<BoardVO>();
		conn=JDBCUtil.connect();
		try {
			pstmt=conn.prepareStatement(BOARD_SELECTALL);
			rs=pstmt.executeQuery();
			while(rs.next()) {
				BoardVO data=new BoardVO();
				data.setBid(rs.getInt("bid"));
				data.setContent(rs.getString("content"));
				data.setTitle(rs.getString("title"));
				data.setWriter(rs.getString("writer"));
				datas.add(data);
			}
			rs.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			JDBCUtil.disconnect(pstmt, conn);
		}
		return datas;
	}

 

 

package com.test.app.common;

// 오토커밋 시키고, 해제하는 등의 동작에 활용할 수 있다.
public class AroundAdvice {

	// 핵심 관심을 잠시 인자로 받아와서 앞, 뒤 작업을 수행한다. 이때, pjp는 필수 조건이다.
	public Object aroundLog(ProceedingJoinPoint pjp) throws Throwble {//pjp=내가 처리할 핵심관심
		System.out.println("[before]");//추가된 횡단 관심
        //proceed의 반환이 object이므로, 이때 어떤 예외가 발생할지 모르므로 최상위 예외로 throws처리
        Object obj=pjp.proceed();
		System.out.println("[After]");//추가된 횡단 관심
        
        return obj;
	}
	
}

 

로그 확인

로그의 확인

 

 

 


 

3. Service클래스 존재의 이유

 

1) 프로그램 동작 순서

현재 : DS-요청정보(*.do)-HM-C-DAO-다음 View정보-VR
개입 후 : DS-요청정보(*.do)-HM-C-Service-DAO-다음 View정보-VR

Service클래스는 "dao야 너 ~메서드 수행해"라는 명령만 하는 친구이다. 즉, service클래스 다음 순서로 동작할 클래스는 dao이다.
사실상 이  service클래스는 모델일 수도 컨트롤러일 수도 있는데, 그래서 구분하지 않고 그냥 service객체라고 한다.

 

 


 

2) service클래스 존재의 이유

없어도 동작에 대해 무리가 없으나, *유지보수 향상을 위해 존재한다.*

 

 

① IoC의 이유! 결합도를 낮춤

If, dao를 바꾸라고 한다면, 모든 DAO 객체(인자부분이나 사용되는 모든 구간)를 전부 다 바꿔야 한다.
즉, controller가 dao를 직접 사용하면, 교체시 코드 수정이 많아진다.

service를 쓴다면, 서비스 클래스의 dao객체만 변경해주면 된다!
또한 dao가 최상위 객체가 된다면(appleWatch가 아니라 그냥 watch), 코드 수정이 없겠지!!!

오류의 파급 효과가 줄어든다. - 오류의 발생 부분을 정확히 짚어내는게 개발자의 능력이다.

 

 


 

② AOP의 이유! 응집도를 높임

 

횡단관심을 끼워넣기 편리해진다. 즉, AOP결합이 용이해진다.

반복되는 공통로직을 분리하여 관리할 목적으로 AOP를 활용할 수 있다는 것, 횡단관심별로 로직을 별도 관리하기 때문에 이런 코드를 응집도가 높다고 표현하며 최종적으로는 유지보수에 유리한 코드가 된다.

 


 

반응형