국비교육(22-23)

55일차(4)/Spring Boot(1) : Spring Boot 기초, AOP의 구조 익히기

서리/Seori 2022. 12. 26. 19:57

55일차(4)/Spring Boot(1) : Spring Boot 기초, AOP의 구조 익히기

 

[ Spring 의 특징 ]

1. DI / Dependency Injection : 핵심의존객체를 주입받는 구조로 사용하는것

2. IOC / Inversion Of Control : 객체 생성과 유지관리를 스프링에 맡기는 것(제어의 역전)

3. AOP / Aspect Oriented Programming : 관점 지향 프로그래밍

 

- 원래는 web project가 아니라 일반 java 프로젝트에서 익혀야하는데

 어차피 boot를 배워야하므로... boot환경에서 테스트해보기

 

- Spring Starter 프로젝트를 만들면 Spring boot 프로젝트로 만들어진다.

 

- maven, 8로 세팅

 

- 버전은 2.7.8로 세팅하고 Finish!

 

 

- 이것은 웹프로젝트는 아니고, 일반 자바 프로젝트이다.

- 스프링을 사용할 준비가 되어있는 Maven 프로젝트!

- Maven이므로 pom.xml이 있다.

 

 

- 버전은 1.8로 되어있고 열어보면 기본 dependency들이 들어있다. (2개)

 

Boot01HelloApplication (기본 main메소드)

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Boot01HelloApplication {

	public static void main(String[] args) {
		//SpringApplication 클래스의 static 메소드 run() 을 호출하면 ApplicationContext 객체가 리턴된다.
		ApplicationContext ctx=SpringApplication.run(Boot01HelloApplication.class, args);
		//spring이 관리하는 bean 중에서 car type 을 찾아서 받아오기 (객체는 한번만 생성된다)
		Car car1=ctx.getBean(Car.class);
		Car car2=ctx.getBean(Car.class);
		
		car1.drive();
		car2.drive();
	}

}

 

- 메인메소드가 들어있는 클래스는 이렇게 생겨있다.

 

- 프로젝트에서 우클릭- Run As - Spring boot app 으로 run하면 된다.

 

- java 프로젝트인데 메이븐으로 build하게 되어 있다.

- 일반 java 프로젝트인데 spring에 필요한 기본자원을 끌어오게 되어있는 구조이다..

 

- 실행하면 요런형태로 실행된다.

 

- src/main/resources폴더 

- 안에 있는 파일을 열어보면 비어있다.

 

spring과 관련된 설정 문서들을 작성할 수 있는 곳이다.

- servlet-context와 비슷하다. 다른 형식이지만!

 

- resources안에 파일하나 만들기

 

- banner.txt에 내용 입력

 

- key=value 이런형식으로 작성한다.

- 이렇게 작성하면 spring boot에서 읽어들일 수 있다.

 

 

- 이렇게 설정해두면

 

 

- 상단에 실행되는 배너가 달라진다. 이런 식으로 설정할 수 있다!!

 


 

패키지 안에 JavaConfig 클래스 생성

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/*
 * 어떤 객체를 spring이 생성해서 관리할지 설정( bean 설정 )
 */
@Configuration
public class JavaConfig {

	//이 메소드에서 리턴하는 객체를 spring이 관리하는 bean 이 되도록 한다.
	@Bean
	public Car myCar() {
		System.out.println("myCar 메소드 호출됨");
		Car c1=new Car();
		return c1;
	}
}

 

- 어떤 객체를 spring이 생성해서 관리할지 설정할 수 있다. 클래스에서!

- 예전에는 servlet-context.xml에서 했던 것! 이 bean 설정을 클래스에서한다.

 

 

패키지안에 Car 클래스 생성

package com.example.demo;

public class Car {
	public void drive() {
		System.out.println("달려요!");
	}
}

 

- 이 클래스의 빈을 spring에서 생성해서 관리하도록 할 수 있다.

 

- xml 문서라면 아래와 같이 작성했을 것

 

@Bean

- 스프링 부트에서는 이렇게 한다.

 

- run 하면 myCar메소드가 호출되고, Car 타입 객체를 spring bean container에 가지고 있는다.

(이 어플리케이션은 run 했을때 잠깐 실행되고 프로세스가 잠깐 살아있다가 끝난다.)

 

- 객체를 생성해서 관리하는 방법 :

 @configuration안에 @Bean 으로 만들어서 가지고있으면 된다.

 

 

- Boot01HelloApplication 메인메소드

- 기본으로 SpringApplication 안에 있는 run이라는 메소드를 호출하고 있다.

 

- 이렇게 만든 Bean을 메인메소드에서 받아와서 사용할 수 있다.

- bean을 받아서 사용하는 구조로 test가능

 

- @configuration 안에서 리턴하는 타입의 객체를 관리하겠다는 것!

 

- getBean을 두번 작성해도 한번만 호출된다.

- 싱글톤(Singletone)으로 사용한다.

 

- JavaConfig 안에 메소드를 만들어두면 spring 이 메소드를 관리한다.

- 메소드 이름은 마음대로 지을 수 있는데, 이 메소드명이 bean의 이름이 된다.

- 리턴해주는 타입이 곧 bean이 된다.

 

- 한줄주석은 # 으로 작성한다!

 


 

- 새 프로젝트 생성

 

 

com.example.demo.util 패키지생성

WritingUtil 클래스 생성

package com.example.demo.util;

import org.springframework.stereotype.Component;

//component scan이 일어났을 때 bean이 될 수 있도록 어노테이션 붙여놓기
@Component
public class WritingUtil {
	//생성자
	public WritingUtil() {
		System.out.println("WritingUtil 생성자");
	}
	
	public void write1() {   
		
		System.out.println("편지를 써요");      
	}
	public void write2() {
		
		System.out.println("보고서를 써요");
	}
	public void write3() {
		
		System.out.println("일기를 써요");
	}
	}

 

 

new WritingUtil().write1();

- WritingUtil 안의 메소드를 사용해서 편지를 쓰고 싶다면? 이렇게 작성하면 된다.

- 하지만 spring은 필요한 핵심 의존객체를 직접 만들지않고 interface타입로 받아 쓴다.

→ 메소드를 spring bean container로부터 받아쓰는 구조로 만들기!!

 

- WritingUtil에 @Component 어노테이션을 붙이고, 생성자 만들기

 

 

메인메소드 수정

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

import com.example.demo.util.Messenger;
import com.example.demo.util.WritingUtil;

/*
 * @SpringBootApplication 어노테이션이 붙어있는 main 메소드가 존재하는 이 패키지를 포함해서
 * 하위의 모든 패키지에 component scan이 자동으로 일어난다.
 */
@SpringBootApplication
public class Boot02AopApplication {

	public static void main(String[] args) {
		ApplicationContext ctx=SpringApplication.run(Boot02AopApplication.class, args);
		
		//편지를 쓰고 싶다면?
		//new WritingUtil().write1();
		WritingUtil util=ctx.getBean(WritingUtil.class);
		util.write1();
		util.write2();
		util.write3();
		
		Messenger mgr=ctx.getBean(Messenger.class);
		String msg=mgr.getMessage();
		
		System.out.println("Messenger 객체로부터 받은 메세지:"+msg);
	}

}

 

- 이 패키지를 포함한 모든 패키지에서 component scan이 일어나서 bean이 생성된다.

 

WritingUtil util=ctx.getBean(WritingUtil.class);
util.write1();

- 객체생성은 스프링이 해주고 사용자는 받아서 쓰기만 한다.

 

- 이렇게 객체를 받아서 사용한다.

 

- 하지만 편지를 쓰기전에 뭔가 준비할작업이 있다면 어떻게 할까?

 

- System.out.println("검정색 pen을 준비해요!"); 를 각각의 메소드 위에 추가

 

- 이렇게 실행된다.

 

 

 

- 여기서는 쓰는 것이 핵심 로직이고, 펜을 준비하는 작업은 핵심 로직은 아니다.

- 여러개의 메소드를 횡단하고 있으면서 핵심 로직과는 관계없는 것들을

  횡단 관심사 라고 한다. (Cross Concern)

 

- 검사를 하거나 준비를 하는 작업이 여기에 해당된다.

- 이 작업들은 그때그때 변화할 수 있다.

- 만약 다른 색깔의 펜을 준비한다면? 위와 같이 작성하면 3군데를 각각 다 고쳐야한다...

 

- 준비작업이 달라질 경우에도 클래스를 수정해야한다면 너무 큰일이다...유지보수가 어렵다.

- 이럴 때 AOP를 적용하면 이 횡단 관심사를 따로 작성할 수가 있다.

 

- 따로 작성하고 원하는 곳에 적용시킬 수 있다.

 

- 다른 곳에 작성해서 이 위치에 적용되도록 할 것!!

 

 

package com.example.demo.aspect;

WritingAspect 클래스 생성

package com.example.demo.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class WritingAspect {
	
	@Before("execution(void write*())")
	public void prepare() {
		System.out.println("파란색 pen을 준비해요!");
	}
	
	@After("execution(void write*())")
	public void end() {
		System.out.println("pen을 닫고 마무리해요!");
	}
}
/*
 *    -Aspectj Expression
 * 
 *    1. execution(* *(..)) => 접근 가능한 모든 메소드가 
 *       point cut
 *    2. execution(* test.service.*.*(..)) 
 *       => test.service 패키지의 모든 메소드 point cut
 *    3. execution(void insert*(..))
 *       =>리턴 type 은 void 이고 메소드명이 
 *       insert 로 시작하는 모든 메소드가 point cut
 *    4. execution(* delete*(*))
 *       => 메소드 명이 delete 로 시작하고 인자로 1개 전달받는 
 *      메소드가 point cut (aop 가 적용되는 위치)
 *    5. execution(* delete*(*,*))
 *       => 메소드 명이 delete 로 시작하고 인자로 2개 전달받는 
 *      메소드가 point cut (aop 가 적용되는 위치)
 *    6. execution(String update*(Integer,*))
 *      => 메소드 명이 update 로 시작하고 리턴 type 은 String
 *      메소드의 첫번째 인자는 Integer type, 두번째 인자는 아무 type 다되는 
 *      메소드가 point cut (aop 가 적용되는 위치)
 */

 

- @Aspect를 붙여주면 되는데, AOP를 적용하기 위해서는 추가 라이브러리가 필요하다.

 

<!-- AOP 용 추가 라이브러리 -->
<dependency>
     <groupId>org.aspectj</groupId>
     <artifactId>aspectjweaver</artifactId>
     <version>1.8.0</version>
</dependency>
<dependency>
     <groupId>cglib</groupId>
     <artifactId>cglib</artifactId>
     <version>3.2.0</version>
</dependency>

- pom.xml에 추가 라이브러리를 넣어주기

 

 

- @Aspect, @Component 어노테이션을 추가해주기

 

@Before 

- 이런 모양의 메소드(write) 로 시작하는 모든 메소드를 수행하기 이전에 이 작업을 수행하라는 뜻!

 

 

기본 demo 패키지에 JavaConfig 생성

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.example.demo.aspect.MessengerAspect;
import com.example.demo.util.Messenger;

@Configuration
@EnableAspectJAutoProxy
public class JavaConfig {

	@Bean
	public MessengerAspect msa() {
		return new MessengerAspect();
	}
	
	//Messenger 객체가 spring이 관리하는 bean이 될 수 있도록!
	@Bean
	public Messenger myMessenger() {
		return new Messenger();
	}
}

 

@Configuration
@EnableAspectJAutoProxy

 

- 이렇게 2개의 어노테이션을 붙여준다.

 

- 그리고 메인메소드를 실행해보면 자동으로 이렇게 실행되어 나온다!!

 

 

- 단순히 WritingAspect가 실행된 것이 아니라, 실행시점에 이 위치에 옮겨간 것이다.

 

- 호출하면 여기로 들어오는데, 호출되기 바로 직전(초록색 상자 위치)에

 aspect가 들어온 것이라고 생각하면 된다.

 

- 그럼 파란색 펜이 필요하거나, 빨간색 펜이 필요한 경우에도 사전 작업을 쉽게 바꿀 수 있다.

- 이렇게 횡단관심사를 따로 만들어놓고 적용할 수 있다.

 

- 이것으로 할 수 있는 작업은?

→ 글 작성자가 로그인한 사용자와 다르면 글을 지우는 것을 막아주기

 

- service, controller등에서 작성자와 현재 로그인한 사용자가 같은지 다른지 확인할 일이 많은데,

 이것을 Aspect로 따로 작업해놓고 필요시 원하는곳에 검증하는 작업을 설정하면 들어갈 수 있도록 하면 좋다.

 

- 이런 작업은 핵심 비즈니스 로직과는 거리가 있다.

- 하지만 여기저기에서 이런 기능이 필요하다면?

- 코드를 매번 복사해 사용하지 않고,

 aspect로 작업해놓고 원하는 곳에 설정하는 것만으로도 동작하게 할 수 있다.

 

- 메소드 실행 이전에 aspect를 들어가게 하는데, 이 메소드에 있는 인자들을 얻어와 사용할 수도 있다.

 

WritingAspect에 내용추가

 

 

@After 

- 메소드 수행 이후에 실행한다.

 

- After로 작성하면 이렇게 적용된다.

 

- 이 작성법을 Aspectj Expression이라고 부른다.

- 이 로직을 어떤 패턴의 메소드에다가 적용할지 이 부분에 작성해야 한다.

 

 

[ Aspectj Expression ]
1. execution(* *(..)) => 접근 가능한 모든 메소드가 point cut
2. execution(* test.service.*.*(..)
 => test.service 패키지의 모든 메소드 point cut 
3. execution(void insert*(..))
 => 리턴 type 은 void 이고 메소드명이 insert 로 시작하는 모든 메소드가 point cut
4. execution(* delete*(*))
 => 메소드 명이 delete 로 시작하고 인자를 1개 전달받는 메소드가 point cut (aop 가 적용되는 위치)
5. execution(* delete*(*,*))
 => 메소드 명이 delete 로 시작하고 인자를 2개 전달받는 메소드가 point cut (aop 가 적용되는 위치)
6. execution(String update*(Integer,*))

 => 메소드 명이 update 로 시작하고 리턴 type 은 String,
 메소드의 첫번째 인자는 Integer type, 두번째 인자는 아무 type이나 가능한 메소드가 point cut (aop 가 적용되는 위치)

 

- point cut이란 AOP가 적용되는 그 위치를 말한다.

 

1. 이 자리에는 각각 리턴타입 / 메소드명 /  메소드의 인자가 들어간다.

 

2. test.service 패키지의 모든 메소드가 가능하다.

 

3. insert로 시작하고 리턴타입이 void인

4. delete로 시작하는데 메소드인자는 하나인

5. delete로 시작하는데 메소드인자는 두개인

6. 메소드명이 update로 시작, 메소드의 첫번째 인자로 interger, 2번째 인자는 타입 상관 없다.

 

- 이것을 잘 작성할 수 있어야 원하는 메소드에 적용시킬 수 있다.

 


 

Util 패키지에 Messenger 클래스 생성

package com.example.demo.util;

public class Messenger {
   public void sendGreeting(String msg) {
	      System.out.println("오늘의 인사:"+msg);
	   }

	   public String getMessage() {
	      System.out.println("getMessage() 메소드가 수행됩니다.");
	      return "열심히 공부 하자";
	   }
}

 

 

- 문자열이 끼어들어서 내용을 검증하는 테스트

 

- 리턴한 직후에 리턴된 문자열을 조사할 수도 있다.

 

- 메소드에 전달된 문자, 메소드가 리턴한 값에 개입해서 조사를 하거나 조작을 할 수 있다.

 

- 직접 @Bean을 붙여도되지만 JavaConfig에서 Bean 으로 관리하도록 할 수도 있다.

 

- getBean() 으로 container에서 관리하는 bean으로 만들기.

 

 

Aspect 패키지에 MessengerAspect 클래스 생성

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MessengerAspect {

   /*
    *  return type 은 String 이고
    *  get 으로 시작은 메소드 이고
    *  메소드에 전달되는 인자는 없다 
    *  java.lang 페키지에 있는 type 은 페키지명 생략 가능
    */
   @Around("execution(String com.example.demo.util.*.get*())")
   public Object checkReturn(ProceedingJoinPoint joinPoint) throws Throwable {

      // aspect 가 적용된 메소드를 수행하고 리턴되는 데이터 받아오기 
      Object obj=joinPoint.proceed();
      //원래 type 으로 casting 해서 조사해 볼수가 있다.
      String a = (String)obj;

      //조사후 아예 다른 값을 리턴해 줄수도 있다. 
      return "놀자놀자!";
   }
}

 

- spring이 관리하고있는 메소드 중에서 

리턴타입 string, 메소드명이 get으로 시작하고 , 메소드의 인자는 없는 메소드를 대상으로 적용

 

- 여기에는 Around AOP가 적용된다.

 

- joinpoint라는 인자가 전달되는데, proceed 메소드를 호출하면 object가 리턴된다.

 그것이 위의 (String get*()) 객체이다

 

- 위와 같이 아예 다른 내용을 리턴할 수도 있다.

 

- 이렇게 메소드 앞뒤로 출력되고, Around를 사용해서 어떤 내용도 리턴된다.

 

- messenger를 JavaConfig를 통해 bean으로 만들기

- Aspect에서 그 bean(객체)을 찾아서 사용하기

 

- before/after와 Aspectj Expression 잘 알아두기

- 작성시 패키지명을 명시해야 한다.