Spring DI, 컴포넌트 스캔, AOP

tabi·2023년 6월 22일
0

Spring

목록 보기
15/15
post-thumbnail

Spring의 DI, 컴포넌트 스캔을 이용한 Bean 등록, AOP에 대해 알아보자.

1. Spring FrameWork?

  • 자바 표준 프레임워크
  • 스프링을 이용해 어플리케이션을 개발할 때 기반이 되는 프레임워크
  • 스프링의 핵심기능인 DI와 AOP기능을 제공함
  • 웹 어플리케이션 개발 시 MVC, ORM 등을 사용
  • 빌드 도구: 프로젝트 생성부터 개발 후 배포까지를 할 수 있는 도구(ex. 메이븐, 그래들)
  • 스프링 프레임워크는 기능에 따라 여러가지 모듈을 가지고 있으며, 모든 모듈은 각각 의존관계를 지닌다. 이 때, Spring-core는 모든 모듈이 의존하는 모듈이다.
    • Spring-tx : 트랜젝션관 관련된 기능 제공
    • Spring-webmvc: 웹 개발과 관련된 기능 제공
    • Spring-context: 객체 생성, 라이프 사이클 처리, 스키마 확장
    • Spring-beans: 스프링 컨테이너를 이용해 객체를 생성하는 기본 기능 제공

Maven?

  • MVN Repository
  • 빌드도구들의 주요 특징 중 하나는 의존모듈(jar 파일) 관리에 있다.
  • 메이븐은 중앙 리파지터리라는 서버에서 필요한 jar 파일을 다운로드 받아 의존 모듈을 관리한다.(스프링 팀은 스프링 프레임워크에 포함된 jar 파일(모듈)을 중앙 리파지터리를 통해 배포하고 있다.)
  • pom.xml에서 중앙 리파지터리를 통해 다운받은 것들은 C드라이브의 .m2 파일(로컬 리파지터리)에 들어간다.

2. Spring DI(Dependency Injection)

1. Spring DI를 이용한 객체 생성 실습

  1. org.doit.ik.di 패키지 생성하고 하위에 파일 생성
  • Record.java 인터페이스, RecordImpl.java 클래스, RecordView.java 인터페이스, RecordViewImpl.java 클래스 생성
  • 결합력이 낮은 코딩

① 생성자를 통한 의존성 주입

  • ex01.java
package org.doit.ik.di.test;

import org.doit.ik.di.Record;
import org.doit.ik.di.RecordImpl;
import org.doit.ik.di.RecordViewImpl;

public class ex01 {

	public static void main(String[] args) {
		System.out.println("hello world");
		
		RecordImpl record = new RecordImpl(); //객체 생성 후
		
		//생성자 DI
		RecordViewImpl rvi = new RecordViewImpl(record); //여기에 이렇게 record(생성자) 넣어주는게 의존성 주입
		rvi.input(); //성적 정보 입력
		rvi.output(); //성적 정보 출력
		
		System.out.println("END");

	}//main
}//class

② setter를 통한 의존성 주입

package org.doit.ik.di.test;

import org.doit.ik.di.Record;
import org.doit.ik.di.RecordImpl;
import org.doit.ik.di.RecordViewImpl;

public class ex01 {

	public static void main(String[] args) {
		System.out.println("hello world");
		
		//setter를 통한 DI주입
		RecordImpl record = new RecordImpl();
		
		//생성자 DI
		RecordViewImpl rvi = new RecordViewImpl(); //여기에 이렇게 record(생성자) 넣어주는게 의존성 주입
		rvi.setRecord(record);
		rvi.input(); //성적 정보 입력
		rvi.output(); //성적 정보 출력
		
		System.out.println("END");

	}//main
}//class
  • Record.java
package org.doit.ik.di;

public interface Record {

	//1. 총점을 구해서 반환하는 메서드
	int total();
	
	//2. 평균을 구해서 반환하는 메서드
	double avg();
	
}
  • RecordImpl.java
package org.doit.ik.di;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter //@Getter 쓰면 get 함수만 만들어진다.
@Setter //@Setter 쓰면 set 함수만 만들어진다.
@AllArgsConstructor //@AllArgsConstructor 쓰면 필드 생성자 전부 만들어짐
@NoArgsConstructor //@NoArgsConstructor 쓰면 디폴트 생성자 전부 만들어짐
public class RecordImpl implements Record {
	
	private int kor;
	private int eng;
	private int mat;

	@Override
	public int total() {
		return kor + eng + mat;
	}

	@Override
	public double avg() {
		return total()/3.0;
	}

}
  • RecordView.java
package org.doit.ik.di;

public interface RecordView {
	
	//1. 한 사람의 성적 정보를 입력받는 메서드
	void input();
	//2. 한 사람의 성적 정보를 출력하는 메서드 
	void output();
}
  • RecordViewImpl.java
package org.doit.ik.di;

import java.util.Scanner;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data //equals, hashCode, to String 등 만들어짐
@AllArgsConstructor
@NoArgsConstructor
public class RecordViewImpl implements RecordView{
	
	//private RecordImpl record = new RecordImpl(); 이건 결합력이 높아 좋은 코드가 아니다.
	private RecordImpl record = null;

	@Override
	public void input() {
		try( Scanner scanner = new Scanner(System.in)) {
			System.out.println("국어, 영어, 수학 점수 입력?");
			int kor = scanner.nextInt();
			int eng = scanner.nextInt();
			int mat = scanner.nextInt();
			
			this.record.setKor(kor);
			this.record.setEng(eng);
			this.record.setMat(mat);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void output() {
		System.out.printf(
				"국어 = %d, 영어 = %d, 수학 = %d, 총합 = %d, 평균 = %.2f\n",
				this.record.getKor(),
				this.record.getEng(),
				this.record.getMat(),
				this.record.total(),
				this.record.avg()
				);
	}

}

2. Spring Container

  • BeanFactory와 ApplicationContext, 두가지 컨테이너가 있으며 그 밑에 5가지의 클래스가 있다.
    • GenericXmlApplicationContext
    • AnnotationConfigApplicationContext
    • GenericGroovyApplicationContext
    • XmlWebApplicationContext
    • AnnotationConfigWebApplicationContext
  1. 스프링에서는 객체를 생성 + 조립 해서 스프링 컨테이너에 담는다.

  2. 이렇게 컨테이너를 만드는 방법에는 두 가지가 있다.

  • xml, java 두가지 방법을 동시에 쓸 수도 있다.

① xml 파일로 만드는 방법 : 유지보수가 쉽다

src/main/resources/application-context.xml 파일추가
  • Spring Bean Configuration file 생성 -> 아무것도 클릭 안 하고 Finish

  • Beans : 객체를 생성하기 위한 부모태그

  • application-context.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"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <!-- 객체 생성 --> 
<!-- RecordImpl record = new RecordImpl(); 객체 생성하겠다는 의미, 아래와 같다. -->
<bean id="record" class="org.doit.ik.di.RecordImpl"></bean> <!-- 이렇게 하면 위 코드와 동일한 의미이다 -->
<!-- The fully qualified name of the bean's class : RecordImpl 무조건 경로 다 입력해야 한다. -->

  
<!-- 객체 안에 setter 주입하기 -->
<!-- RecordViewImpl rvi = new RecordViewImpl(); 객체 생성해 setter 주입, 아래와 같다.
rvi.setRecord(record); -->
<bean id="rvi" class="org.doit.ik.di.RecordViewImpl">
<constructor-arg ref="record"/>
<!-- name: SetRecord()라는 setter 함수의 Set은 떼고 R은 소문자로 바꿔서 name 에 넣음 -->
	
	
<!--  Setter 통해 DI함 -->
<!-- <property name = "record" ref="record"></property> -->
<property name = "record">
	<ref bean = "record"/>
</property>
	</bean>
</beans>
  • ex02.java
package org.doit.ik.di.test;

import org.doit.ik.di.RecordViewImpl;
import org.springframework.context.support.GenericXmlApplicationContext;

public class Ex02 {
	public static void main(String[] args) {
	String resourceLocations="application-context.xml";
	//xml에서 얻어오는 GenericXmlApplicationContext
	GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(resourceLocations);

	
	//생성된 공장으로부터 얻어오겠다는 메소드
	RecordViewImpl rvi = (RecordViewImpl)ctx.getBean("rvi");
	rvi.input(); //성적 정보 입력
	rvi.output(); //성적 정보 출력
	
	System.out.println("END");

}//main
}

② java(클래스) 파일로 만드는 방법 : 유지보수가 어렵다

  • Config.java
package org.doit.ik.di2;

import org.doit.ik.di.RecordImpl;
import org.doit.ik.di.RecordViewImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//자바 설정 파일에서 Bean 객체 만들기

@Configuration //이게 클래스 위에 붙으면 자바클래스 설정파일이라는 뜻(application-context.xml 파일과 같은 설정파일)
public class Config {
		//RecordImpl record = new RecordImpl();
		//xml에서는 <bean id="record" class="org.doit.ik.di.RecordImpl"/>이렇게 함
		@Bean
		public RecordImpl record() {
			return new RecordImpl();
		}
	
		/*
		 * <bean id="rvi" class="org.doit.ik.di.RecordViewImpl"> <constructor-arg
		 * ref="record"/> <!-- <property name = "record" ref="record"></property> -->
		 * <!-- setter DI --> <property name = "record"> <ref bean = "record"/>
		 * </property> </bean>
		 */
		@Bean(name="rvi")
		public RecordViewImpl getRecordViewImpl() {
			RecordViewImpl rvi = new RecordViewImpl();
			rvi.setRecord(record());
			return rvi;
		}
}
  • ex03.java
package org.doit.ik.di2.test;

import java.io.ObjectInputFilter.Config;

import org.doit.ik.di.RecordViewImpl;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class ex03 {
	public static void main(String[] args) {
		//java에서 얻어오는 AnnotationConfigApplicationContext
		//Config.class: Config라고 하는 자바클래스 파일에서 얻어오겠다는 의미
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);

		//생성된 공장으로부터 얻어오겠다는 메소드
		RecordViewImpl rvi = (RecordViewImpl)ctx.getBean("rvi");
		rvi.input(); //성적 정보 입력
		rvi.output(); //성적 정보 출력
		
		System.out.println("END");
	}//main
}

NoSuchBeanDefinitionException : No bean named 'rvi' available ?

  • 생성된 Bean을 찾아오지 못한다는 오류 발생 시
  • @Bean 해 준 자바 클래스 파일로 이동해 @Bean(name="rvi")를 해주면 오류를 해결할 수 있다.

3. DI 주입

  1. @Autowired
  • 생성된 객체가 있다면 주입하라는 의미로 주입 객체가 없거나 여러 개라면 오류가 난다.
  • 의존 자동 주입 어노테이션을 사용하기 위해서는 다음과 같은 코드로 빈을 등록해야 한다.
    - 다수의 스프링 전처리기 빈을 등록해줌
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

//아래 코드와 함께 위의 context 관련 url도 등록되어야 한다.
<context:annotation-config/>
  • context 관련 url 같은 경우 namespaces 를 통해 등록할 수 있다.
  1. Bean이 여러개인 경우

    NoUniqueBeanDefinitionException

  • 아래 예시는 record1, record2 로 Bean이 여러개이다. 이런 경우 NoUniqueBeanDefinitionException이 발생한다.
  • application-context3.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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	 <!-- @Autowired 어노테이션을 사용하기 위해 빈을 등록하는 코드 -->
	 <context:annotation-config/>

<bean id="record1" class="org.doit.ik.di3.RecordImpl3"></bean>
<bean id="record2" class="org.doit.ik.di3.RecordImpl3"></bean>
<!-- rvi를 사용하려면 record가 주입되어야 한다. -->
<bean id="rvi" class="org.doit.ik.di3.RecordViewImpl3"> 
	 <!--  <constructor-arg ref="record"/>이것이 DI 주입, 우리는 어노테이션 쓰기로 했으니 주석처리 -->
	</bean>
</beans>
  • 이런 경우 RecordViewImpl3.java에 Inject, Named를 지정해주면 해결된다.
package org.doit.ik.di3;

public class RecordViewImpl3 implements RecordView3{
	//@Setter()
	//생성된 객체가 있다면 주입하라는 의미, 주입 객체가 없거나 여러개라면 오류가 난다.
	
	//@Resource(name="record1") java9부터 사용 X
	
	@Inject
	@Named(value="record2")

@Override
public void input() {
		//생략
}
  1. 찾을 수 있는 Bean이 없는 경우

    NoSuchBeanDefinitionException

  • 아래 예시는 Bean에 주석처리를 해 찾을 수 있는 Bean이 없는 경우이다. 이런 경우 NoSuchBeanDefinitionException이 발생한다.
  • application-context3.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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	 <!-- @Autowired 어노테이션을 사용하기 위해 빈을 등록하는 코드 -->
	 <context:annotation-config/>

<!-- <bean id="record1" class="org.doit.ik.di3.RecordImpl3"></bean>
<bean id="record2" class="org.doit.ik.di3.RecordImpl3"></bean> -->
<!-- rvi를 사용하려면 record가 주입되어야 한다. -->
<bean id="rvi" class="org.doit.ik.di3.RecordViewImpl3"> 
	 <!--  <constructor-arg ref="record"/>이것이 DI 주입, 우리는 어노테이션 쓰기로 했으니 주석처리 -->
	</bean>
</beans>
  • 이런 경우 RecordViewImpl3.java에 (required = false) 속성을 지정해주면 해결된다. 이는 필수는 아니니 있으면 주입하고 없으면 주입하지 말자는 의미이다.
package org.doit.ik.di3;

public class RecordViewImpl3 implements RecordView3{
	//@Setter()
	@Autowired(required = false)//필수는 아니니 있으면 주입하고 없으면 주입하지 말자는 의미
	
	//@Resource(name="record1") java9부터 사용 X
	
@Override
public void input() {
		//생략
}

BeanDefinitionParsingException

3. Spring Bean

컴포넌트 스캔을 이용한 Bean 자동등록★(자주 사용)

기억하기!

  • servlet-context의 코드에 의해 @애노테이션이 붙은 코드들을 스캔하게 된다.
<context:component-scan base-package="org.doit.ik" />

@Controller = 컨트롤러 역할을 하는 bean 객체 생성과 같다.
@Repository = DAO 역할을 하는 bean 객체
@Service = 서비스 역할을 하는 bean 객체
@Component = 그 외 스캔 대상
@Autowired = 주입 시켜주는 것

  • 특정 패키지에 위치한 클래스를 스프링 빈으로 자동등록하고 의존 자동 설정을 통해서 각 빈 간의 의존을 처리할 수 있다면, 설정 코드를 만드는 수고를 덜 수 있다. 이것이 바로 컴포넌트 스캔 기능이다.
  • 특정 패키지 및 그 하위 패키지에서 클래스를 찾아 스프링 빈으로 등록해주는 기능으로 자동으로 Bean 객체 생성 및 주입까지 완료해준다.
  • 검색 대상은 @Component 애노테이션을 적용하고, 아래와 같은 코드를 적용한 경우에 해당한다.
  • application-context4.xml 파일 내에 다음과 같이 설정
    • 여기서 org.doit.ik는 가장 상위 패키지로 여기에 적용을 해줘야 base로 하위 패키지도 모두 scan 범위가 된다.
	<!-- @Component 어노테이션을 사용하기 위해 scan 범위 적용하는 코드 -->
	<context:component-scan base-package="org.doit.ik"></context:component-scan>

어떤 것들을 서치할 수 있을까?

  • @Component, @Repository, @Service, @Controller, @RestController, @ControllerAdvice, and @Configuration stereotypes will be detected.
  • Note: This tag implies the effects of the 'annotation-config' tag, activating @Required, @Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit

java.lang.UnsatisfiedDependencyException

  • 주의: @Component는 이름을 부여하지 않으면 class의 첫번째 글자를 '소문자'로 한 Bean 객체가 생성된다.
  • 따라서 이름을 부여하거나, 혹은 name 태그 value를 줄 때 자동으로 설정되는 Bean 객체의 이름으로 맞추어 주어야 한다.
이름 부여
@Component("record")
Bean 객체 이름 다르게 하기
// @Named(value="recordImpl4")

1. @Component 사용 시 이름 주지 않으려면?

java.lang.ClassCastException

  • 자바빈이 생성되고 이를 가져오는 과정에서 classcast가 맞지 않으면 발생하는 문제로 Component 이름을 클래스명(앞글자는 소문자)을 맞춰주는 것으로 해결할 수 있다.
  • Ex05.java 에서 RecordViewImpl4 rvi = (RecordViewImpl4)ctx.getBean("recordViewImpl4"); 로 맞춰주기
package org.doit.ik.di4.test;

import org.doit.ik.di4.RecordViewImpl4;
import org.springframework.context.support.GenericXmlApplicationContext;

public class Ex05 {
	public static void main(String[] args) {
		String resourceLocations="application-context4.xml";
		//xml에서 얻어오는 GenericXmlApplicationContext
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(resourceLocations);

		//생성된 공장으로부터 얻어오겠다는 메소드
		RecordViewImpl4 rvi = (RecordViewImpl4)ctx.getBean("recordViewImpl4"); //recordViewImpl4 로 준다
		rvi.input(); //성적 정보 입력
		rvi.output(); //성적 정보 출력
		
		System.out.println("END");
	}
}
  • RecordImpl4.java
package org.doit.ik.di4;

import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter //@Getter 쓰면 get 함수만 만들어진다.
@Setter //@Setter 쓰면 set 함수만 만들어진다.
@AllArgsConstructor //@AllArgsConstructor 쓰면 필드 생성자 전부 만들어짐
@NoArgsConstructor //@NoArgsConstructor 쓰면 디폴트 생성자 전부 만들어짐
@Component
public class RecordImpl4 implements Record4 {
	
	private int kor;
	private int eng;
	private int mat;

	@Override
	public int total() {
		return kor + eng + mat;
	}

	@Override
	public double avg() {
		return total()/3.0;
	}

}
  • RecordViewImpl4.java
package org.doit.ik.di4;

import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter //@Getter 쓰면 get 함수만 만들어진다.
@Setter //@Setter 쓰면 set 함수만 만들어진다.
@AllArgsConstructor //@AllArgsConstructor 쓰면 필드 생성자 전부 만들어짐
@NoArgsConstructor //@NoArgsConstructor 쓰면 디폴트 생성자 전부 만들어짐
@Component //이름 안 주고 그냥 어노테이션만 준다. 자동으로 생성되는 이름은 recordImpl4 이다.	
public class RecordImpl4 implements Record4 {
	
	private int kor;
	private int eng;
	private int mat;

	@Override
	public int total() {
		return kor + eng + mat;
	}

	@Override
	public double avg() {
		return total()/3.0;
	}
}

2. @Component 사용 시 특정한 이름을 부여하려면?

@Component("원하는이름")

컴포넌트 애노테이션의 용도별 하위 타입

  • 스프링 컨테이너가 관리 컴포넌트로 식별할 수 있게 해주는 마커
  • o.s.stereotype.Component: 스프링 빈임을 의미(@Component)
  • o.s.stereotype.Service: 서비스임을 의미(@Service)
  • o.s.stereotype.Repository: DAO임을 의미(@Repository)
  • o.s.stereotype.Controller: 웹 MVC의 컨트롤러임을 의미(@Controller)

4. Spring AOP(관점 지향적 프로그래밍 기법)

1. AOP란? (Aspect Oriented Programming)

  • 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법(관점: 핵심관심사항/공통관심사항)
  • 공통기능: 공통관심사항(cross-cutting concern)
  • 핵심로직: 핵심관심사항(core concern)
  • 고유한 기능 앞에 해야하는 애플리케이션 전반에 걸쳐 적용되는 공통적인 기능들을 AOP라고 한다.(ex. 트랜젝션, 보안 등)
  • 스프링에서 AOP를 구현하는 세가지 방식
    • XML 스키마 확장을 이용한 POJO 클래스 기반
    • AspectJ에서 정의한 @Aspect(관점) 애노테이션 기반 구현
    • 스프링 API를 이용한 AOP 구현
  • 다양한 구현 방법이 존재하지만 기본적으로는 공통관심사항을 구현한 코드를 핵심로직을 구현한 코드 안에 집어넣는다.
  • AOP는 메서드를 호출할 때 보조기능을 장착할 수 있다.

AOP 용어

  • Aspect: 공통 기능(Advice+Pointcut)
  • Advice: 공통 관심 기능을 언제 핵심 로직에 적용해야 할지를 정의하는 시점, Joinpoint에서 실행되어야 하는 프로그램 코드(BEFORE, AROUND, AFTER)
  • Weaving: Advice를 핵심 로직 코드에 적용하는 것(컴파일 시, 클래스 로딩 시, 런타임 시 Weaving 가능)
  • Joinpoint: Advice를 적용가능한 지점(ex.메서드 호출, 필드값 변경 등), 구현한 코드를 끼워넣을 수 있는 프로그램의 이벤트
  • Pointcut: Joinpoint의 부분 집합으로 실제로 Advice가 적용되는 지점인 Joinpoint의 상세한 스펙을 정의한 것. 포인트컷이 에스펙트를 나눠주는 역할을 한다. 스프링에서는 정규 표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의할 수 있다.

2. AOP 실습 - proxy 직접 만들어보기

Around advice

  • AOP를 활용해 계산에 걸리는 시간을 계산 전/후로 출력해보자.
  • org.doit.ik.aop 패키지 추가
  • Ex02.java 파일을 만들고 Calculator 인터페이스 안에 사칙연산 추상메서드 선언
  • CalculatorImpl 클래스 구현
  • application-context.xml 파일에 빈 생성
  • org.doit.ik.aop2.advice 패키지 생성 후 LogPrintAroundAdvice 클래스 생성
  • Around advice 구현을 위해서는 MethodInterceptor 메소드를 implement 해야 한다.
  1. Ex02.java
package org.doit.ik.aop2;

import org.doit.ik.aop.Calculator;
import org.springframework.context.support.GenericXmlApplicationContext;

public class Ex02 {

	public static void main(String[] args) {
		//org.doit.ik.aop2.advice 패키지 생성(aspect 적용 시점(전, 후) 정하기)
		//전, 후 : LogPrintAroundAdvice 클래스 생성(처리 시간을 계산하고 출력)
		
		//자동으로 빈 생성
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("application-context.xml");
		//핵심기능만 출력
		//Calculator calc = ctx.getBean("calc",Calculator.class);
		//보조기능+핵심기능까지 출력
		Calculator calc = ctx.getBean("calcProxy",Calculator.class);
		System.out.println(calc.add(4, 2));
		
		System.out.println("END");

	}//main
}//class
  1. Calculator.java
package org.doit.ik.aop;

public interface Calculator {
	
	int add(int x, int y);
	int sub(int x, int y);
	int mult(int x, int y);
	int div(int x, int y);
}
  1. CalculatorImpl.java
  • 일반적인 계산기 구현 메소드가 아래와 같다면, AOP를 사용하는 경우에는 다르게 구현해야 한다.
public class CalculatorImpl2 implements Calculator{
	@Override
	public int add(int x, int y) {
		long start = System.nanoTime();
		int result = x + y ; //핵심 관심 사항(core concern)
		long end = System.nanoTime();
		System.out.printf("덧셈 처리 시간: %d ns\n",(end-start));
		return result;
	}
}
  • AOP로 구현하기 위한 코드
public class CalculatorImpl2 implements Calculator{
	@Override
	public int add(int x, int y) {
		//전			공통 관심 사항 제거
		int result = x + y ; //핵심 관심 사항(core concern)
		//후
		return result;
	}
}
  1. application-context.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"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 실제기능을 하는 객체 -->
	<bean id="calc" class="org.doit.ik.aop2.CalculatorImpl2"/>
	
	<!-- 보조기능을 하는 객체 -->
	<bean id="logPrintAroundAdvice" class="org.doit.ik.aop2.advice.LogPrintAroundAdvice"/>
	<bean id="logPrintBeforeAdvice" class="org.doit.ik.aop2.advice.LogPrintBeforeAdvice"/>
	<bean id="logPrintAfterReturningAdvice" class="org.doit.ik.aop.LogPrintAfterReturningAdvice"/>

	
	<!-- 스프링 AOP: 프록시(proxy) 기반 으로 실제기능과 보조기능을 합해보자-->
		<!-- 1. 가짜 bean을 만들어주고 -->
	<bean id="calcProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
		<!-- 2. target: 핵심기능을 하는 실제 기능 객체를 ref로 등록해준다.-->
		<property name = "target" ref="calc"></property>
		<!-- 3. pointcut: Interface, 즉 추상메서드가 호출될 때 보조 기능 객체를 장착하겠다.-->
		<property name = "proxyInterfaces">
		<!-- setProxyInterfaces(Class[] 인터페이스) 라서 밑에 list로 받는다. -->
			<list>
			<!-- Calculator라는 인터페이스를 호출할 때 -->
				<value>org.doit.ik.aop.Calculator</value>
			</list>
		</property>
			<!-- 4. advice 등록 -->
		<property name="interceptorNames">
			<list>
				<value>logPrintAroundAdvice</value>
				<value>logPrintBeforeAdvice</value>
				<value>logPrintAfterReturningAdvice</value>
			</list>
		</property>
	</bean>
</beans>
  1. LogPrintAroundAdvice.java
package org.doit.ik.aop2.advice;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StopWatch;

public class LogPrintAroundAdvice implements MethodInterceptor {
	//처리된 시간을 나타내는 보조기능을 하는 Advice
	//add()를 해 메서드를 호출할 때 보조기능 장착 + 로그 기록
	@Override
	public Object invoke(MethodInvocation method) throws Throwable {
		
		String methodName = method.getMethod().getName();
		Log log = LogFactory.getLog(this.getClass());
		
		log.info(methodName + "start");
		StopWatch sw = new StopWatch(); //스탑워치 메소드 활용
		sw.start();
		
		Object result = method.proceed(); //calc.add() -> 핵심관심사항
		
		sw.stop();
		log.info(methodName + "end");
		log.info(methodName + "처리시간:" + sw.getTotalTimeMillis() + "ms");
		
		return result;
	}
}

Before Advice

  • Before Advice 구현을 위해서는 MethodBeforeAdvice 메소드를 implement 해야 한다.
  • LogPrintBeforeAdvice.java
package org.doit.ik.aop2.advice;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.MethodBeforeAdvice;

public class LogPrintBeforeAdvice implements MethodBeforeAdvice {

	@Override
	public void before(
			Method method //add() 메소드
			, Object[] args //매개변수 4, 2
			, Object target //대상객체(실제 객체 calc)
			) throws Throwable {
		String methodName = method.getName();
		Log log = LogFactory.getLog(this.getClass());
		log.info( methodName + "() : LogPrintBeforeAdvice 호출됨...");
	}//before
}

After Returning Advice

  • After Returning Advice 구현을 위해서는 AfterReturningAdvice 메소드를 implement 해야 한다.

  • After Returning Advice는 대상 객체가 핵심기능의 메서드를 예외없이 처리한 경우 호출되는 advice이다.

  • LogPrintAfterReturningAdvice.java

package org.doit.ik.aop;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;

public class LogPrintAfterReturningAdvice implements AfterReturningAdvice {

	@Override
	public void afterReturning(
			Object returnValue //결과값
			, Method method //메서드
			, Object[] args //매개변수
			, Object target //대상객체
			) throws Throwable {
		String methodName = method.getName();
		Log log = LogFactory.getLog(this.getClass());
		log.info( methodName + "() : LogPrintAfterReturningAdvice 호출됨...");
	}
}

3. XML 기반 AOP 구현

  • <aop:config> 태그 이용해 Aspect 설정하고 Advice를 어떤 Pointcut에 적용할지 지정한다.
  • namespaces에서 AOP 체크하면 <aop:config> 태그 사용가능
  1. application-context4.xml(pg.250 aspect 문법)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	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.3.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

	<!-- 컴포넌트로 적용 -->
	<context:component-scan base-package="org.doit.ik" />

	
	<aop:config>
		<aop:aspect id="traceAspect" ref="logPrintProfiler4">
		<aop:pointcut expression="execution(* org.doit.ik.aop..*.*(*,*))" id="publicMethod"/>
			<aop:around method="trace" pointcut-ref="publicMethod"/> <!-- around advice의 메소드명이 trace -->
		<!-- pointcut에는 aspectJ 문법이 들어간다. -->
			<aop:before method ="before" pointcut-ref="publicMethod"/>
			<aop:after method ="afterFinally" pointcut-ref="publicMethod"/>
		
		</aop:aspect>
		
	</aop:config>
	
</beans>
  • Ex04.java
package org.doit.ik.aop4;

import org.doit.ik.aop.Calculator;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class Ex04 {

	public static void main(String[] args) {

		//자동으로 빈 생성
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("application-context4.xml");

		//스프링 aop가 내부적으로 프록시를 사용한다.
		Calculator calc = ctx.getBean("calc4",Calculator.class);
		System.out.println(calc.add(4, 2));

		System.out.println("END");

	}//main
}//class
  • LogPrintProfiler5.java
package org.doit.ik.aop2.advice;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StopWatch;

public class LogPrintAroundAdvice implements MethodInterceptor {
	//처리된 시간을 나타내는 보조기능을 하는 Advice
	//add()를 해 메서드를 호출할 때 보조기능 장착 + 로그 기록
	@Override
	public Object invoke(MethodInvocation method) throws Throwable {
		
		String methodName = method.getMethod().getName();
		Log log = LogFactory.getLog(this.getClass());
		
		log.info(methodName + "start");
		StopWatch sw = new StopWatch(); //스탑워치 메소드 활용
		sw.start();
		
		Object result = method.proceed(); //calc.add() -> 핵심관심사항
		
		sw.stop();
		
		log.info(methodName + "end");
		log.info(methodName + "처리시간:" + sw.getTotalTimeMillis() + "ms");
		
		return result;
	}

}
  • LogPrintProfiler4.java
package org.doit.ik.aop4.advice;

import org.aopalliance.intercept.Joinpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Component("logPrintProfiler4")
public class LogPrintProfiler4 {

	//1. AroundAdvice pg222
	public Object trace(ProceedingJoinPoint joinPoint) throws Throwable{
		String methodName = joinPoint.getSignature().toShortString(); //메소드 이름 가져온다.
		Log log = LogFactory.getLog(this.getClass());
		log.info(methodName + "start");
		StopWatch sw = new StopWatch(); //스탑워치 메소드 활용
		sw.start();
		
		//핵심 관심 사항
		Object result = joinPoint.proceed(); //calc.add
		sw.stop();
	       log.info("> " + methodName +"() end.");
	       log.info("> " + methodName +"() 처리 시간 :  " + sw.getTotalTimeMillis() +"ms");
	      return result;
	   }
		
	
	//2. BeforeAdvice
		   public void before(JoinPoint joinPoint) {
			      String methodName = joinPoint.getSignature().getName();
			      Log log = LogFactory.getLog(this.getClass());
			      log.info(">>> " + methodName + "() : LogPrintProfiler4.before 가 호출됨... ");      
			   }
			   
			   // 3.  AfterAdvice  p 221
			   public void afterFinally(JoinPoint joinPoint) {
			      String methodName = joinPoint.getSignature().getName();
			      Log log = LogFactory.getLog(this.getClass());
			      log.info(">>> " + methodName + "() : LogPrintProfiler4.afterFinally 가 호출됨... ");      
			   }

			}

4. @Aspect 애노테이션 기반 AOP

  • 반드시 xml 파일에 <aop:aspectj-autoproxy/>를 설정해준다.

  • CalculatorImpl5.java

package org.doit.ik.aop5;

import org.doit.ik.aop.Calculator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("calc5")
public class CalculatorImpl5 implements Calculator{

	@Override
	public int add(int x, int y) {
		//전			공통 관심 사항 제거
		int result = x + y ; //핵심 관심 사항(core concern)
		//후
		return result;
	}
}
  • LogPrintProfiler5.java
package org.doit.ik.aop5.advice;

import org.aopalliance.intercept.Joinpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Component("logPrintProfiler5")
@Aspect
public class LogPrintProfiler5 {

	//<aop:pointcut expression="execution(* org.doit.ik.aop..*.*(*,*))" id="publicMethod"/>
	@Pointcut("execution(* org.doit.ik.aop..*.*(*,*))") //이제 pointcut도 이렇게 지정해주면 된다.
	private void publicMethod(){}
	
	@Around("publicMethod()")
	//1. AroundAdvice pg222
	public Object trace(ProceedingJoinPoint joinPoint) throws Throwable{
		String methodName = joinPoint.getSignature().toShortString(); //메소드 이름 가져온다.
		Log log = LogFactory.getLog(this.getClass());
		log.info(methodName + "start");
		StopWatch sw = new StopWatch(); //스탑워치 메소드 활용
		sw.start();
		
		//핵심 관심 사항
		Object result = joinPoint.proceed(); //calc.add
		sw.stop();
	       log.info("> " + methodName +"() end.");
	       log.info("> " + methodName +"() 처리 시간 :  " + sw.getTotalTimeMillis() +"ms");
	      return result;
	   }
		
	@Before("publicMethod()")
	//2. BeforeAdvice
		   public void before(JoinPoint joinPoint) {
			      String methodName = joinPoint.getSignature().getName();
			      Log log = LogFactory.getLog(this.getClass());
			      log.info(">>> " + methodName + "() : LogPrintProfiler5.before 가 호출됨... ");      
			   }
		
	@After("publicMethod()")
	// 3.  AfterAdvice  p 221
			public void afterFinally(JoinPoint joinPoint) {
			      String methodName = joinPoint.getSignature().getName();
			      Log log = LogFactory.getLog(this.getClass());
			      log.info(">>> " + methodName + "() : LogPrintProfiler5.afterFinally 가 호출됨... ");      
			   }
			}
  • Ex05.java
package org.doit.ik.aop5;

import org.doit.ik.aop.Calculator;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class Ex05 {

	public static void main(String[] args) {

		//자동으로 빈 생성
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("application-context5.xml");

		//스프링 aop가 내부적으로 프록시를 사용한다.
		Calculator calc = ctx.getBean("calc5",Calculator.class);
		System.out.println(calc.add(4, 2));

		System.out.println("END");

	}//main
}//class
  • application-context5.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: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.3.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

	<!-- 컴포넌트로 적용 -->
	<context:component-scan base-package="org.doit.ik" />

	<aop:aspectj-autoproxy/>
	
</beans>
profile
개발 공부중

0개의 댓글