스프링의 핵심 IOC & DI & AOP(활용)
스프링 프레임워크의 구성은 20여가지로 구성되어있다!
이러한 구성(모듈)들은
스프링의 핵심 기능인 DI, AOP, etc 등을 제공해준다.
필요한 구성만 골라 사용 가능!
- 웹 서버에 올리기 위한 스프링 부트
- 스프링 데이터
- 마이크로소프트 관련한 스프링 클라우드
- 일정한 데이터를 모아 대용량으로 처리할 때 스프링 배치
- 권한 관련한 스프링 시큐리티
스프링은
테스트가 용이하고,
느슨한 결합을 통해
디자인 패턴이나 유지 보수, 확장이 어렵지 않도록 하게 하는 것
다른 프레임워크와의 가장 큰 차이점은 IoC
IoC (Inversion of Control)
스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리하는 것이 아닌 Spring Container에게 모두 맡긴다.
(객체가 이미 컨테이너 안에 다 들어가 있고, 싱글톤의 형태로 관리된다.)
즉, 개발자에서 프레임워크로 객체 관리의 제어 권한이 넘어갔으니
"제어의 역전" 이라고 한다.
DI (Dependency Injection)
스프링이 객체의 생명주기를 관리해주는데,
우리가 사용하기 위해 외부로부터 객체를 주입받는다
(주입을 해주는 녀석은 Container가 해준다)
- 객체가 다른 객체한테 의존하는 그러한 의존성으로부터 격리시켜 코드 테스트에 용이하다
- DI를 통하여, 불가능한 상황을 Mock(Mock객체를 만들어 주입->응답에 대한 기대값을 넣고 올바른 반응이 나오는지 검사) 와 같은 기술을 통해 안정적으로 테스트 가능
- 내가 필요한 객체를 외부에서 주입받기 때문에, 내 코드를 확장하거나 변경할 때 영향을 최소화한다(추상화)
- 외부에서 주입을 받기 때문에, 객체가 서로 참조를 하는 순환 참조를 막을 수 있다
UrlEncoder
package com.company.ioc;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class UrlEncoder implements IEncoder{
public String encode(String message){
try {
return URLEncoder.encode(message, "UTF-8");
} catch (UnsupportedEncodingException e){
e.printStackTrace();
return null;
}
}
}
Encoder
package com.company.ioc;
import java.util.Base64;
public class Encoder {
public String encode(String message){ //메세지를 하나 받아서
return Base64.getEncoder().encodeToString(message.getBytes()); //메세지를 getBytes로 리턴
}
}
Main
package com.company.ioc;
public class Main {
public static void main(String[] args) {
String url="www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//Base 64로 encoding 해주세요
Encoder encoder=new Encoder();
String result= encoder.encode(url);
System.out.println(result);
UrlEncoder urlEncoder = new UrlEncoder();
String urlResult=urlEncoder.encode(url);
System.out.println(urlResult);
}
}
인터페이스 추가
package com.company.ioc;
public interface IEncoder {
String encode(String message);
}
Base64Encoder
package com.company.ioc;
import java.util.Base64;
public class Base64Encoder implements IEncoder{
public String encode(String message){ //메세지를 하나 받아서
return Base64.getEncoder().encodeToString(message.getBytes()); //메세지를 getBytes로 리턴
}
}
Main에서 클래스타입 변경
package com.company.ioc;
public class Main {
public static void main(String[] args) {
String url="www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//Base 64로 encoding 해주세요
IEncoder encoder=new Base64Encoder();
String result= encoder.encode(url);
System.out.println(result);
IEncoder urlEncoder = new UrlEncoder();
String urlResult=urlEncoder.encode(url);
System.out.println(urlResult);
}
}
Encoder 변경
package com.company.ioc;
public class Encoder {
private IEncoder iEncoder;
public Encoder(){
this.iEncoder=new Base64Encoder();
}
public String encode(String message){
return iEncoder.encode(message); //메세지를 호출
}
}
Main 변경
package com.company.ioc;
public class Main {
public static void main(String[] args) {
String url="www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//Base 64로 encoding 해주세요
Encoder encoder=new Encoder();
String result= encoder.encode(url);
System.out.println(result);
}
}
어느날 URL Encoding도 필요하다고 말한다
그럼 또 Encoder에 들어가서 생성자를 변경해준다....
package com.company.ioc;
public class Encoder {
private IEncoder iEncoder;
public Encoder(){
// this.iEncoder=new Base64Encoder();
this.iEncoder=new UrlEncoder();
}
public String encode(String message){ //메세지를 하나 받아서
return iEncoder.encode(message); //메세지를 호출
}
}
계속 생성자를 바꿔줘야 하고
본질인 Class를 건드리고 있다
굉장히 비효율적이다
외부에서 객체를 주입시키자
Encoder 변경
package com.company.ioc;
public class Encoder {
private IEncoder iEncoder;
public Encoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){ //메세지를 하나 받아서
return iEncoder.encode(message); //메세지를 호출
}
}
Base64Encoder를 써보자~
Main 변경
(Encoder를 건드릴 필요도 없이 Encoder 인자에 new Base64Encoder 써주면 되네!
= 넘겨주는 DI 객체만 바꿔주면 된다!)
package com.company.ioc;
public class Main {
public static void main(String[] args) {
String url="www.navaer.com/books/it?page=10&size=20&name=spring-boot";
Encoder encoder=new Encoder(new Base64Encoder());
String result= encoder.encode(url);
System.out.println(result);
}
}
UrlEncoder를 써보자~
= 외부에서 사용하는 객체를 주입받는걸 DI라고 한다
package com.company.ioc;
public class Main {
public static void main(String[] args) {
String url="www.navaer.com/books/it?page=10&size=20&name=spring-boot";
Encoder encoder=new Encoder(new UrlEncoder());
String result= encoder.encode(url);
System.out.println(result);
}
}
Encoder
package com.company.ioc;
public class Encoder {
private IEncoder iEncoder; //외부에서 주입받음 -> Dependancy Injection을 받은 것이다
public Encoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){
return iEncoder.encode(message);
}
}
만약 Base32Encoder를 쓰고싶다.
그렇다면 클래스 Base32Encoder 만들고
IEncoder 상속 받아서
Main에 파라미터로 주입만 해주면 된다.
그러면 언제든지 내가 가진 Encoder라는 객체는
언제든지 내가 넣은 객체로 동작하게 된다
지금까지
내가 new도 쓰고(객체 만들 때), Base64Encoder() 객체도 내가 직접 넣고 있다.
스프링의 ioc는
스프링 컨테이너가 객체 생성, 생명 주기라던지 관리해준다
Base64Encoder에 @Component 추가
돋보기 달린 콩모양에 손을 대면 스프링 컨테이너가 관리하는 객체들의 목록이 나온다!
스프링이 실행될 때,
컴포넌트 어노테이션이 붙은 클래스들을 찾아서
직접 객체를 싱글톤 패턴으로 만들어서 직접 스프링이 관리한다.
ApplicationContextProvider 클래스 만들기
package com.example.ioc;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
}
}
package com.example.ioc;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
//(1)Web으로부터 주입을 받는데, 스프링이 주입을 해준다
private static ApplicationContext context;
@Override //(2)set 메소드를 만들때 ApplicationContext를 주입해줄 것이며 우리는 그것을 받아 static 변수에 할당할 것이다(위에)
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context=applicationContext;
}
public static ApplicationContext getContext(){ //(3)그럼 우리는 가져다 쓰기만 하면 된다!
return context;
}
}
IocApplication
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
Base64Encoder base64Encoder = context.getBean(Base64Encoder.class); //(5)아마 클래스로 찾은건가...?
Encoder encoder = new Encoder(base64Encoder);
}
}
이전에 했던 URL을 가져와서 DI주입했을 때처럼 실행해보자!
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
Base64Encoder base64Encoder = context.getBean(Base64Encoder.class); //(5)아마 클래스로 찾은건가...?
Encoder encoder = new Encoder(base64Encoder);
//예전에 사용했던 url
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//이전과 똑같이 실행해보자
String result = encoder.encode(url);
System.out.println(result);
}
}
결과 똑같다!
UrlEncoder도 해보자~
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
Base64Encoder base64Encoder = context.getBean(Base64Encoder.class); //(5)아마 클래스로 찾은건가...?
UrlEncoder urlEncoder = context.getBean(UrlEncoder.class); //<====요기 추가함!!!
Encoder encoder = new Encoder(base64Encoder);
//예전에 사용했던 url
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//이전과 똑같이 실행해보자
String result = encoder.encode(url);
System.out.println(result);
}
}
Encoder에 Set 메소드 추가하자
package com.example.ioc;
public class Encoder {
private IEncoder iEncoder; //외부에서 주입받음 -> Dependancy Injection을 받은 것이다
public Encoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
//Bean을 주입받을 수 있는 방법은
//변수 / 생성자 / set 메소드
public void setIEncoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){
return iEncoder.encode(message);
}
}
다시 IocApplication로
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
Base64Encoder base64Encoder = context.getBean(Base64Encoder.class); //(5)아마 클래스로 찾은건가...?
UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
Encoder encoder = new Encoder(base64Encoder);
//예전에 사용했던 url
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
//이전과 똑같이 실행해보자
String result = encoder.encode(url);
System.out.println(result);
// ***새로 추가된 부분***
encoder.setIEncoder(urlEncoder);
result=encoder.encode(url);
System.out.println(result);
}
}
우리가 만들었던
Base64Encoder와
UrlEncoder를
스프링이 관리할 수 있도록 어노테이션을 붙여주고
권한을 넘겨주었다
그럼 Encoder도 만들어보자...(만들기 싫다..)
바로 에러가 떠버린다!
두 가지 타입이 있는데,
Bean은 스프링에서 선택을 할 때
Bean에 대해서 하나만 있으면 바로 맞춰준다. (한 개만 있으면 바로 매칭이 된다)
그런데 지금은
Base64Encoder와 UrlEncoder가 있기 때문에 어떤 것을 매칭해줘야 하는지 모른다.
그럴때,
스프링에게 너가 어떤 것을 매칭하면 돼!
라고 표시해주는 어노테이션
@Qualifier
package com.example.ioc;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class Encoder {
private IEncoder iEncoder; //외부에서 주입받음 -> Dependancy Injection을 받은 것이다
public Encoder(@Qualifier("base64Encoder") IEncoder iEncoder){
this.iEncoder=iEncoder;
}
//Bean을 주입받을 수 있는 방법은
//변수 / 생성자 / set 메소드
public void setIEncoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){
return iEncoder.encode(message);
}
}
컴포넌트의 이름은,
클래스에 아무런 명칭없이 만들면
클래스의 제일 앞글자가 소문자로 변하고 그 이름을 갖게 된다
이름을 따로 주고 싶다면
package com.example.ioc;
import org.springframework.stereotype.Component;
import java.util.Base64;
@Component("base74Encoder") //나는 얘 이름을 base74Encoder로 쓸래
public class Base64Encoder implements IEncoder{
public String encode(String message){ //메세지를 하나 받아서
return Base64.getEncoder().encodeToString(message.getBytes()); //메세지를 getBytes로 리턴
}
}
Encoder에서도 이름 따로 준것으로 바꿔주자
package com.example.ioc;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class Encoder {
private IEncoder iEncoder; //외부에서 주입받음 -> Dependancy Injection을 받은 것이다
public Encoder(@Qualifier("base74Encoder") IEncoder iEncoder){
this.iEncoder=iEncoder;
}
//Bean을 주입받을 수 있는 방법은
//변수 / 생성자 / set 메소드
public void setIEncoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){
return iEncoder.encode(message);
}
}
다시 메인으로 돌아와서 객체 찾아주기
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
//더 이상 불러올 필요 없으니 주석처리한다!
//Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
//UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
Encoder encoder = context.getBean(Encoder.class);
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);
}
}
잘 된다!
이제 코드에서는 new로 된 부분이 전혀 없다.
모든 권한을 스프링에게 넘겨준 것이다.
스프링에서 만든 객체를 우리는 이제 "Bean"이라고 부를 것이다
나는 2개의 Encoder를 사용하고 싶다!
Component로 두지 않고 직접 Bean으로 등록해보자
일단 Encoder 변경
package com.example.ioc;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
public class Encoder {
private IEncoder iEncoder; //외부에서 주입받음 -> Dependancy Injection을 받은 것이다
public Encoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
//Bean을 주입받을 수 있는 방법은
//변수 / 생성자 / set 메소드
public void setIEncoder(IEncoder iEncoder){
this.iEncoder=iEncoder;
}
public String encode(String message){
return iEncoder.encode(message);
}
}
@Configuration
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
//더 이상 불러올 필요 없으니 주석처리한다!
//Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
//UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
Encoder encoder = context.getBean(Encoder.class);
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);
}
}
@Configuration //한 개의 클래스에서 여러 개의 Bean을 등록할 것이라는 의미
class AppConfig{
@Bean //개발자가 new를 사용해서 코드에서 사용하는게 아니라 미리 Bean으로 등록
public Encoder encoder(Base64Encoder base64Encoder){
return new Encoder(base64Encoder);
}
@Bean
public Encoder encoder(UrlEncoder urlEncoder){
return new Encoder(urlEncoder);
}
}
두 개의 Bean이 생겼는데 어떻게 구분해줄 것인가?
이름을 붙여주면 되지요
@Configuration //한 개의 클래스에서 여러 개의 Bean을 등록할 것이라는 의미
class AppConfig{
@Bean("base64Encoder") //아까는 74여서 부딪힐 일 없음
public Encoder encoder(Base64Encoder base64Encoder){
return new Encoder(base64Encoder);
}
@Bean("urlEncode") //UrlEncoder는 이미 쓰고 있기 때문에 Encode
public Encoder encoder(UrlEncoder urlEncoder){
return new Encoder(urlEncoder);
}
}
안나오네?
이름으로 찾아주자
Encoder encoder = context.getBean("base64Encode", Encoder.class); //이름으로 찾아주기
package com.example.ioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
public class IocApplication {
public static void main(String[] args) {
SpringApplication.run(IocApplication.class, args); //(1)스프링 어플리케이션이 실행되고 나면,
ApplicationContext context = ApplicationContextProvider.getContext(); //(2)가져오자
//(3)Bean을 찾는 방법은 이름/클래스 타입 등 여러가지 방법이 있다
//(4)DI는 해줄거지만 IOC 즉 객체관리는 해주지 않을 것이다 (new로 안할 것이다)
//더 이상 불러올 필요 없으니 주석처리한다!
//Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
//UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
Encoder encoder = context.getBean("urlEncode", Encoder.class); //이름으로 찾아주기
String url = "www.navaer.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);
}
}
@Configuration //한 개의 클래스에서 여러 개의 Bean을 등록할 것이라는 의미
class AppConfig{
@Bean("base64Encoder") //아까는 74여서 부딪힐 일 없음
public Encoder encoder(Base64Encoder base64Encoder){
return new Encoder(base64Encoder);
}
@Bean("urlEncode") //UrlEncoder는 이미 쓰고 있기 때문에 Encode
public Encoder encoder(UrlEncoder urlEncoder){
return new Encoder(urlEncoder);
}
}
실제로 코딩할 때는 실질적으로 new를 사용해서 객체를 만들지 않는다
우리의 서비스 로직에서는 스프링 컨텍스트를 통해서 가져올거고
ApplicationContext
를 통해 가져오는 모습을 보여줬지만
앞으로는 다양한 방법으로 가져올 것이다
스프링에서 객체를 관리해주는데 그 객체가 Bean
Bean들이 관리되는 장소가 스프링 컨테이너
스프링 컨테이너가 제어하는 권한을 다 가져갔기 때문에 제어의 역전 -> IOC
나는 주입을 받았으니까 -> DI
이것이 바로 스프링의 핵심 기술
AOP(Aspect Oriented Programming) 관점 지향 프로그래밍
스프링 어플리케이션은 대부분 특별한 경우를 제외하고는
MVC 웹 어플리케이션에서는
제일 앞 Web Layer
가운데 Business Layer
끝 Data Layer로 정의
-Web Layer : REST API를 제공하며, Client 중심의 로직 적용
-Business Layer : 내부 정책에 따른 logic을 개발하며, 주로 해당 부분을 개발
-Data Layer : 데이터 베이스 및 외부와의 연동을 처리
AOP의 개념이 잡히지 않아, 새로비님의 블로그에서 AOP개념 설명 부분을 퍼왔다.
출처: https://engkimbs.tistory.com/746 [새로비:티스토리]
AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.
위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.
| AOP 주요 개념
- Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
- Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
- Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
- JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
- PointCut : JointPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음
출처: https://engkimbs.tistory.com/746 [새로비:티스토리]
스프링에서는 많은 모듈들이 있기때문에,
아무거나 갖다 쓸 수 있는 장점이 있다.
우리는 AOP를 쓰기위해
AOP를 Dependency에 추가해줄 것이다
controller 패키지 만들고 그 안에
RestApiController 만듦
package com.example.aop.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public void get(@PathVariable Long id, @RequestParam String name){
System.out.println("get method");
System.out.println("get method "+id);
System.out.println("get method "+name);
}
@PostMapping("/post")
public void post(@RequestBody){ //여기까지 만들었는데 클래스 객체가 없단다
}
}
클래스 객체가 없기 때문에,
dto 패키지 만들고 User
package com.example.aop.dto;
public class User {
private String id;
private String pw;
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPw() {
return pw;
}
public void setPw(String pw) {
this.pw = pw;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
//toString 오버라이딩하기
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", pw='" + pw + '\'' +
", email='" + email + '\'' +
'}';
}
}
***포트 번호 바꾸기
get
post
aop 패키지 만들고
ParameterAop
package com.example.aop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //AOP로 작동시키기
@Component //스프링이 관리하게 하기
public class ParameterAop {
//포인트컷이란
//내가 어느 부분에 적용시킬건지 적용하는 것(대표적인 것 하나 살펴보자)
@Pointcut("execution(* com.example.aop.controller..*.*(..))") //* com.example.aop 프로젝트에 controller 패키지 하위에 있는 모든 메소드를 aop로 보겠다
private void cut(){ //메소드명 아무거나 가능
}
//메소드 실행되기 전 어떠한 값이 들어가는지 확인
//언제 실행시킬 것이냐?
@Before("cut()") //cut 메소드가 실행되는 시점에 before를 실행시키겠다
public void before(JoinPoint joinPoint){ //들어가는 지점에 대한 정보를 가지고 있는 joinpoint
Object[] args = joinPoint.getArgs(); //메소드에 들어가고 있는 매개변수들의 배열
for(Object obj : args){ //매개변수 값 잘 찍히는지 확인해보기
System.out.println("type "+obj.getClass().getSimpleName()); //타입 가져오기
System.out.println("value "+obj);
}
}
//들어간 후 어떠한 값이 리턴되는지
//반환값이 뭘까?
@AfterReturning(value = "cut()", returning = "returnobj") //returning="" 안에 내가 받고싶은 오브젝트 객체 이름 넣어주기
public void afterReturn(JoinPoint joinPoint, Object returnobj){ //returning값과 매칭되어야함
System.out.println("return obj");
System.out.println(returnobj);
}
}
RestApiController도
리턴 값을 주기 위해 클래스 타입도 바꾸어줌
package com.example.aop.controller;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name){
System.out.println("get method");
System.out.println("get method "+id);
System.out.println("get method "+name);
return id+" "+name;
}
@PostMapping("/post")
public User post(@RequestBody User user){ //dto 만든 후 클래스 객체 불러옴
System.out.println("post method :"+user);
//클래스에서 리턴한 값이 있어야 하니
//user를 리턴해주자
return user;
}
}
type에는 User라는 객체가 들어갔고,
value에는 User라는 클래스가 들어갔고,
post method는 before를 지나서 RestController에 post method에서 찍힌 것
그런 다음에 obj가 찍히고
마지막에 리턴된 값들이 찍힌다.
즉 echo의 형태로 되어있다.
특정한 값들이 어떠한 값이 들어갔고, 어떠한 값이 리턴됐는지 위와 같은 방법으로 보면 된다.
get method로 살펴보자
type은
Long 타입에 100이 들어갔고,
String 타입에 hyerin이 들어갔고,
get method
get method 100
get method hyerin는
RestController에 get method에서 찍힌 것
return obj에는 id + name이 찍힘
내가 특정 부분에 로그를 무조건 남겨야한다고 치면,
이렇게 AOP를 활용해서 로그를 남길 수 있다.
method이름까지 같이 출력해보자
package com.example.aop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect //AOP로 작동시키기
@Component //스프링이 관리하게 하기
public class ParameterAop {
//포인트컷이란
//내가 어느 부분에 적용시킬건지 적용하는 것(대표적인 것 하나 살펴보자)
@Pointcut("execution(* com.example.aop.controller..*.*(..))") //* com.example.aop 프로젝트에 controller 패키지 하위에 있는 모든 메소드를 aop로 보겠다
private void cut(){ //메소드명 아무거나 가능
}
//메소드 실행되기 전 어떠한 값이 들어가는지 확인
//언제 실행시킬 것이냐?
@Before("cut()") //cut 메소드가 실행되는 시점에 before를 실행시키겠다
public void before(JoinPoint joinPoint){ //들어가는 지점에 대한 정보를 가지고 있는 joinpoint
//**새로 추가된 부분
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName());
Object[] args = joinPoint.getArgs(); //메소드에 들어가고 있는 매개변수들의 배열
for(Object obj : args){ //매개변수 값 잘 찍히는지 확인해보기
System.out.println("type "+obj.getClass().getSimpleName()); //타입 가져오기
System.out.println("value "+obj);
}
}
//들어간 후 어떠한 값이 리턴되는지
//반환값이 뭘까?
@AfterReturning(value = "cut()", returning = "returnobj") //returning="" 안에 내가 받고싶은 오브젝트 객체 이름 넣어주기
public void afterReturn(JoinPoint joinPoint, Object returnobj){ //returning값과 매칭되어야함
System.out.println("return obj");
System.out.println(returnobj);
}
}
package com.example.aop.controller;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name){
return id+" "+name;
}
@PostMapping("/post")
public User post(@RequestBody User user){ //dto 만든 후 클래스 객체 불러옴
return user;
}
}
//이제 위와 같은 코드에 sout는 필요없다
//저런 메소드가 많이 생기든 말든 포인트컷으로 외부에서 볼 수 있기 때문에
//이러한 기능들을 aop로 몰아줄 수 있고
//aop로 디버깅할 수 있다
내가 만든 메소드
외부에서 어떤 값이 들어왔는지
내가 어떻게 리턴했는지
디버깅하며 에러를 찾을 수 있다
여기까지 AOP를 통해 들어가는 인자와 리턴값을 알아보았다.
AOP의 활용 사례(2)
Bean같은 경우 클래스에 붙일 수 없으며,
Configuration을 하나의 클래스에 여러 개의 메소드 - Bean을 붙이는 것을 말한다
aop 패키지에
TimerAop 만들어주기
package com.example.aop.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component //Bean같은 경우 클래스에 붙일 수 없다
public class TimerAop {
//포인트컷이란
//내가 어느 부분에 AOP를 적용시킬건지 적용하는 것(대표적인 것 하나 살펴보자)
@Pointcut("execution(* com.example.aop.controller..*.*(..))") //* com.example.aop 프로젝트에 controller 패키지 하위에 있는 모든 메소드를 AOP로 보겠다
private void cut(){ //메소드명 아무거나 가능
}
}
annotation 패키지 만들고 Timer 어노테이션 만들어주기
package com.example.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
RestApiController에 delete 메소드 만들어주기
package com.example.aop.controller;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name){
return id+" "+name;
}
@PostMapping("/post")
public User post(@RequestBody User user){ //dto 만든 후 클래스 객체 불러옴
return user;
}
@Timer //우리가 직접 만든 annotation 붙여주기
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
//DB Logic
Thread.sleep(1000*2); //1000이 1초니 2초정도 걸린다고 한다
}
}
만약 AOP 기능을 넣지 않는다면,
쓸데없이 비즈니스 로직에 맞지 않는 코드들을 아래와 같이 반복해서 넣어야 한다.
그래서 아래와 같이 반복되는 코드들을 빼서 AOP에 넣는 것이다.
package com.example.aop.controller;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name){
StopWatch stopWatch = new StopWatch();
stopWatch.start();
stopWatch.stop(); //타이머 종료
System.out.println("total time : "+stopWatch.getTotalTimeSeconds()); //메소드가 얼마나 걸리는지 확인하기
return id+" "+name;
}
@PostMapping("/post")
public User post(@RequestBody User user){ //dto 만든 후 클래스 객체 불러옴
StopWatch stopWatch = new StopWatch();
stopWatch.start();
stopWatch.stop(); //타이머 종료
System.out.println("total time : "+stopWatch.getTotalTimeSeconds()); //메소드가 얼마나 걸리는지 확인하기
return user;
}
@Timer //우리가 직접 만든 annotation 붙여주기
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//DB Logic
Thread.sleep(1000*2); //1000이 1초니 2초정도 걸린다고 한다
stopWatch.stop(); //타이머 종료
System.out.println("total time : "+stopWatch.getTotalTimeSeconds()); //메소드가 얼마나 걸리는지 확인하기
}
}
일단 다 지워주고
package com.example.aop.controller;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name){
return id+" "+name;
}
@PostMapping("/post")
public User post(@RequestBody User user){ //dto 만든 후 클래스 객체 불러옴
return user;
}
@Timer //우리가 직접 만든 annotation 붙여주기
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
//DB Logic
Thread.sleep(1000*2); //1000이 1초니 2초정도 걸린다고 한다
}
}
서버 실행시켜주고
ParameterAop의 기능을 다 끄고
서버 다시 돌리기
클라이언트 Delete 요청 수행
메소드가 얼마나 시간이 걸리는지,
서버 실행 시간이 얼마나 걸리는 지 측정해서 서버 관리자에게 알림을 보낸다던지..
아니면 반복적으로 들어가 있는 로직들
횡단되어 있는 것들을
꺼내 AOP로 작성하는 것이다
AOP 예제 - 값의 변환
만약 암호화된 코드가 들어오면,
코드 단에서 복호화를 하는게 아니라
AOP에서 이미 복호화를 끝마친 후 보내줄 수 있다
1. 우선 annotation 패키지에 Decode를 만들어주고,
2. RestApiController에 post 메소드 만들어주고,
3. aop 패키지에 DecodeAop 만들어준다
package com.example.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {
}
@Decode
@PutMapping("/put")
public User put(@RequestBody User user){
System.out.println("put");
System.out.println(user);
return user;
}
package com.example.aop.aop;
public class DecodeAop {
}
TimerAop에 있던 내용을 복사해와서
DecodeAop를 만들어준다
package com.example.aop.aop;
import com.example.aop.dto.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
public class DecodeAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut(){}
@Pointcut("@annotation(com.example.aop.annotation.Decode)")
private void enableDecode(){}
//전은 디코딩을 해서 내보낼 것이고,
//후는 엔코딩을 해서 내보낼 것이다.
@Before("cut() && enableDecode()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
Object[] args = joinPoint.getArgs(); //메소드가 돌 때, 메소드의 파라미터들 중에
for(Object arg : args){
if(arg instanceof User) {//argument의 instance가 우리가 만들어 놓은 User라면
//내가 원하는 User라는 객체를 찾았으면 값을 바꿔준다
User user = User.class.cast(arg); //User라는 class로 형변환 시키기
String base64Email=user.getEmail(); //기존에 encoding되어있던 email을 꺼냅니다
String email = new String(Base64.getDecoder().decode(base64Email),"UTF-8"); //decoding을 해준다다 user.setEmail(email); //decoding된 email을 다시 set 해줌
//실질적인 controller 코드에서는 user를 decode할 일은 없어진다
//이러한 기능들을 enableDecode() 어노테이션을 통해 할 것이다
}
}
}
@AfterReturning(value = "cut() && enableDecode()",returning = "returnObj")
public void afterReturn(JoinPoint joinPoint,Object returnObj){ //오브젝트에서
if(returnObj instanceof User) { //User를 찾아서
User user = User.class.cast(returnObj);
String email = user.getEmail(); //평문 email을
String base64Email = Base64.getDecoder().encodeToString(email.getBytes()); //encoding
user.setEmail(base64Email); //base64Email로 encoding해서 내려주기
}
}
}
AopApplication.java
package com.example.aop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Base64;
@SpringBootApplication
public class AopApplication {
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
System.out.println(Base64.getEncoder().encodeToString("steve@gmail.com".getBytes()));
}
}
실행
바뀐 이유는,
처음에 들어왔을 때
before에서 encoding된 base64Email을 decode를 해서 다시 세팅해주었고,
나갈 때
afterReturn에서 평문이었던 email을 encoding해서 넣어주었기 때문이다
그래서 실질적으로 통신을 할 때,
암호화된 base64Email을 보내더라도
Response도 base64로 나온다.
어노테이션으로 구분해서 미리 AOP로 선처리하거나 이러한 방법으로 활용 가능