국비교육(22-23)

56일차(1)/Spring Boot(2) : Bean 생성하는 방법, AOP 활용 실습

서리/Seori 2022. 12. 26. 20:54

56일차(1)/Spring Boot(2) : Bean 생성하는 방법, AOP 활용 실습

 

 

- 새 Spring Boot 프로젝트 만들기

 

- Spring Boot 프로젝트가 나온 계기는?

  spring은 설정해야 할 값이 많았는데, boot에서는 설정을 대신해주는 것이 많다.

  spring project를 좀더 편리하게 사용하기 위해!

 

File-New-Spring Starter Project (boot 사용)

 

- 이렇게 설정하고 테스트하기! Maven, 8버전으로 생성

 

 

- 설정은 2.7.8로

- 용도에 따라서 의존 dependency를 선택하면 알아서 pom.xml 이 설정된다.

 

 

- 프로젝트 구조가 복잡하다.

- Maven으로 생성하면 Maven의 특징인 pom.xml이 존재한다.

 

pom.xml

- 기본 dependency는 하나만 들어가 있는 상태.

- 하나만으로도 기본 dependency 들이 많이 설치되어 있다.

 

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

- spring boot 프로젝트의 run하는 방법: main 메소드가 들어있는 곳에서 run 버튼 누르기

 

- run할 때에는 둘중 뭘로 해도 상관없으나 보통 Spring Boot App으로 하면 된다.

 

- 실행되었다가 바로 종료된다.

- run 하는 순간만 실행될 뿐 지속적으로 돌아가는 것은 아니다.

 

- run 했을때의 순간 찰나에 spring에 관련한 테스트를 한다.

 

 

- 기본으로 @SpringBootApplication 이라는 Annotation이 붙어있다.

- Run을 하면 이것이 속해있는 패키지를 포함해서 하위 패키지까지 자동으로 component scan이 일어난다.

 

 

- 이전에 쓰던 spring의 경우 이 패키지에 스캔이 일어나고 HomeController 객체가 생성되는 것이었다.

- 이 객체를 Bean 컨테이너에서 관리했다.

 

- 마찬가지로 boot에서도 자동으로 comnponent scan이 일어난다.

- 스캔이 일어날 때 bean으로 만들어질 것들은 알아서 bean으로 만들어진다.

- boot 패키지 클래스의 run이라는 메소드를 실행하면서 뭔가를 전달함

 

- run 이라는 static 메소드를 활용한다.

- 들어가는 인자는 첫번째 타입은 클래스타입(클래스명.class 하면 클래스타입이다)

 두번째 타입은 string 배열이다.

 

 

- ConfigurableApplicationContext 타입을 리턴하는데,

 이것은 ApplicationContext 인터페이스를 구현하는 타입이다.

- 그래서 ApplicationContext 타입으로 변수를 선언하여 받을 수 있다.(ctx)

 

- 이 타입으로 받으면서 getBean() 이라는 메소드를 호출한다.

- 메소드가 다중정의(overloading) 되어 있으므로 용도에 따라서 저 5개중에서 사용하면 된다.

- 첫번째 메소드를 사용함!

 

- spring이 관리하는 컨테이너로부터 원하는 타입의 객체를 가져오는 메소드

- 원하는 타입을 리턴해달라! 는 의미로 작성하는 것

 

- 여기에 클래스 타입을 넣어주면 그 타입이 자동으로 리턴 타입이 된다.

- spring이 관리하는 객체를 만드는 것

 

 

- spring에서는 component scan하면 객체를 만들수 있었다.

- xml문서에다가 bean설정을 통해서 or 특정 패키지를 component scan 하도록 해서!

 

 

** boot에서 객체 생성하기 테스트

 

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

Car 클래스 생성

package com.example.demo.auto;

import org.springframework.stereotype.Component;

//component scan을 통해서 bean이 되도록 한다.
@Component
public class Car {
	public void drive() {
		System.out.println("달려요!");
	}
}

 

- 이 클래스로 spring이 관리하는 객체를 만들려면? 3가지 방법이 있다.

 

1) 제일 쉬운 방법 : @Component 어노테이션 붙이기.

- scan만 일어나면 자동으로 bean이 된다.

 


 

 

2) 다른방식으로  bean만들기

 

MyCar

package com.example.demo.auto;

public class MyCar {
	public void drive() {
		System.out.println("내 차가 달려요!");
	}
}

 

 

 

- demo 패키지(기본)에 새 클래스생성

JavaConfig

package com.example.demo;

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

import com.example.demo.auto.MyCar;

@Configuration
public class JavaConfig {
	
	/*
	 * @Bean 어노테이션과 함께 MyCar type을 리턴하는 메소드를 만들어놓으면
	 * MyCar type 객체가 spring이 관리하는 bean이된다.
	 */
	@Bean
	public MyCar myCar() {
		MyCar c1=new MyCar();
		return c1;
	}
}

 

- JavaConfig에 @Bean 어노테이션과 함께 MyCar type을 리턴하는 메소드를 만들어 놓으면
  MyCar type 객체가 spring이 관리하는 bean이 된다.

- 그 타입을 리턴해주는 메소드를 만든다.

 

- 이것이 boot의 전형적인 방식이다. spring에는 없던 방식!

 

- @Configuration 해서 메소드를 활용해서 bean 을 만든다.

 

 

- 기존의 servlet-context.xml의 기능을 이 설정 클래스에서 대체하는 것이다.

- xml문서에 작성했다면 파란색 텍스트와 같이 작성된다.

- 메소드명이 bean의 이름, 리턴타입이 생성할 객체를 의미하는 것이다.

 

- boot에서 bean을 만드는 방법을 익히기

- configuration 문서에서 @bean과 함께 해당타입을 리턴하는 메소드를 만들어주면 된다.

 

 

 

메인메소드 수정

package com.example.demo;

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

import com.example.demo.auto.Car;
import com.example.demo.auto.LegacyCar;
import com.example.demo.auto.MyCar;

@SpringBootApplication
public class Boot01Hello2Application {
	//Spring Boot Application이 시작되는 main 메소드
	public static void main(String[] args) {
		ApplicationContext ctx=SpringApplication.run(Boot01Hello2Application.class, args);
		//getBean( 원하는 type )
		Car c1=ctx.getBean(Car.class);
		c1.drive();
		
		MyCar c2=ctx.getBean(MyCar.class);
		c2.drive();
		//resources 폴더의 config.xml 문서를 로딩해서 bean을 생성해서 관리하기 (예전 방식)
		ApplicationContext ctx2=new ClassPathXmlApplicationContext("config.xml");
		LegacyCar c3=ctx2.getBean(LegacyCar.class);
		c3.drive();
	}

}

 

- bean으로 만든 후에는 main 클래스의 이 객체를 통해서 얻어낸다.

- bean을 저장되어 있는 컨테이너로부터 가져오는 것이다.

 

- 핵심의존객체를 직접 생성하지 않고, 스프링이 생성하게 하고 받아서 쓴다.

- 받아오려면 getBean하면 된다.

- 메인메소드의 Appplication context 가 바로 container 라고 생각하면 된다.

 

 

- 이렇게 작성하면 빈이 있다면 들어오고, 없다면 에러가 날 것이다.

- 실행해보면 drive 메소드가 잘 실행된다. Bean이 생성되어 있는 것을 알 수 있다!

 


 

3)xml 문서를 통해서 bean으로 만들기(이전과 같은 방식)

 

LegacyCar 클래스 생성

package com.example.demo.auto;

public class LegacyCar {
	public void drive() {
		System.out.println("구형차가 달러요!");
	}
}

 

- Main에 resources 폴더가 있다. 이곳에는 java 클래스가 아닌 자원을 넣는다.

- xml 파일을 만들 것!

 

- others 에서 이것을 선택해서 생성하면 된다.

- 이것이 sevlet context 와 같은 파일이다.

 

config.xml

 

<bean class="com.example.demo.auto.LegacyCar" />

- 자동완성으로 작성하면 된다.

- xml를 spring이 로딩하면 이 객체를 생성해서 자기가 관리한다.

 

메인메소드에 추가

- ClassPathXmlApplicationContext 객체를 생성하고, xml문서를 로딩하라고 하기

- 예전 spring 방식이다. spring boot에서는 이렇게는 잘 하지 않는다.

 

- 위 이미지상으로는 1, 2는 기존의방식, 3은 스프링 boot에서 만드는 방식

 

- 설정을 하는 java클래스(JavaConfig)에서 bean 을 만드는 방식을 새롭게 익혀야 한다.

- bean 을 어떻게 만들고 참조값을 어떻게 얻어내는지 정리하기!

 

 

- bean을 만드는 세가지 방법 익혀두기!

 

- 이렇게 bean을 생성하면 개발자는 이 안에서 원하는 메소드를 생성하고 기능을 추가하거나 필요에따라 변경할 수 있다.

 


 

- 지난 주에 했던 AOP 예제

 

- 메소드의 수행결과는 위와 같다.

 

- write는 쓰는 코드밖에 없는데, 해당메소드가 수행되기 이전/이후에 새로운 내용이 실행되어 들어가 있다.

- 이것은 핵심 비즈니스 로직은 아니다. 상관이 없다.

- java로 프로그래밍하다 보면 핵심 비즈니스 로직과는 상관없는,

여러 메소드를 횡단하고, 여러 메소드에서 필요로 하는 공통적인 작업들이 필요할 때가 있다.

 

- 여기에 AOP 방식을 적용하면, 다른곳에 작성해 두고 설정만으로도 넣을 수 있다.

- 이것이 Aspect Oriented Programming (관점 지향 프로그래밍) 이다. 스프링의 핵심 개념!

 

 

WritingUtil

- demo를 포함한 demo의 하위패키지는 자동으로 component scan이 일어난다.

- write 1,2,3을 실행했을 뿐인데 앞뒤로 새로운 작업이 들어가 있다.

 

- aspect 패키지 안에 writing aspect 클래스가있다.

 

- bean도 되어야 하고 aspect역할도 해야 하기 때문에 @@ 두개!

 

- 이전, 이후에 실행되는 Aspect

- 이런 모양의 메소드를 수행하기 전/후에 이 작업을 수행하라! 라는 뜻

- spring이 관리하는 bean 중에서 write*() 라는 이름으로 시작하는 메소드를 가리킨다.

 

- spring이 관리하는 bean 인데 그 안에 있는 메소드의 수행시에 개입한다.

- 메소드 수행의 이전, 이후에 개입!!

 


 

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();
	}
}

 

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 "열심히 공부 하자~~";
	   }
}

 

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은 void 이고 send로 시작하는 모든 메소드가 point cut이다.
	//@around로 aspect를 적용하면 메소드의 인자로 ProceedingJoinPoint type이 전달된다.
	@Around("execution(void send*(..))")
	public void checkGreeting(ProceedingJoinPoint joinPoint) throws Throwable {
		//메소드에 전달된 인자들 목록을 얻어내기
		Object[] args=joinPoint.getArgs();
		//반복문 돌면서 원하는 type의 값을 찾아서 작업한다.
		for(Object tmp:args) {
			//만일 String type이면
			if(tmp instanceof String) {
				//원래 type으로 casting
				String msg=(String)tmp;
				System.out.println("aspect에서 읽어낸 내용:"+msg);
				if(msg.contains("바보")) {
					System.out.println("바보는 금지된 단어입니다.");
					return; //메소드를 끝내기
				}
			}
		}
		//aspect가 적용된 메소드가 호출되기 직전에 할 작업은 여기서 한다.
		joinPoint.proceed(); //aspect가 적용된 메소드 수행하기
		//aspect가 적용된 메소드가 리턴된 직후에 할 작업은 여기서 한다.
		System.out.println("aspect가 적용된 메소드가 리턴했습니다.");
	}
	
	
   /*
    *  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 "놀자놀자!";
   }
}

 

 

- 메신저를 다른방식으로 bean으로 만들기(JavaConfig 사용 : spring boot의 전형적인 방식)

- messenger 클래스에는 어노테이션이 없다.

- component scan이 아닌 설정 파일에서 bean 만들기

 

- component annotation이 안 붙어있으면 component sca이 아닌 bean설정을 통해서 빈이 된다고 알면 된다.

- Bean 이 되면 aspect 역할을 할 수 있도록 @Aspect가 붙어있지만,

 이것이 붙어있다고 bean이 되는 것은 아니다!!

 

- @Around 는? Before+After 인데 합친 것보다는 기능이 더 많다.

- 개입만 하는 것이 아니고 메소드가 리턴하는 데이터도 직접 조사할 수 있다. Around라는 aspect를 사용하면!

 

@Around("execution(String com.example.demo.util.*.get*())")

- Util 패키지에 있는 모든 get이라는 이름의 메소드에 적용될 aspect 라는 뜻이다.

 

- demo.util 패키지의 모든 클래스의 get으로 시작하는 메소드

- * 은 와일드카드 역할. 이 자리에 아무거나 와도 된다.

- 메소드의 인자는 아무것도 없어야 한다.

- 리턴타입은 반드시 String

→ 이렇게 Aspectj Expression을 해석할 수 있어야 한다.

 

- spring이 관리하는 bean이 이런 메소드를 갖고있어야 한다. bean이 아니면 적용이 안된다!!!

- 만약 bean 중에서 이런 모양의 메소드가 있다면, 메소드 실행 이전과 이후에 개입할 수 있다.

 

- 메소드 수행 이전, 이후란?

 

- joinPoint.proceed() 를 호출하면 이 메소드를 호출하는 것과 같다.

- 이전(초록) 이후(분홍) 위치에서 개입하면 된다.

 

- joinPoint.proceed() 를 호출하지 않으면 실행되지 않는다.

 

 

- 분명히 getMessage를 호출했음에도 불구하고... joinPoint가 실행되지 않으면 호출이 안된다.

- getMessage안에 있는 "getMessage() 메소드가 수행됩니다." 가 수행되지 않는다.

- 하지만 return이 있으므로... 해당 메소드는 호출이 되지 않았는데도 값을 리턴해주기도 한다.

 

- proceed 호출 이전, 이후에 실행값을 넣어주기

 

- proceed를 호출하면 getMessage 메소드가 실행된다.

- object type으로 리턴되므로 캐스팅해서 내용을 검사해볼 수 있다.

- 리턴값은 별개이다!

 

- 원래 리턴하려는 값을 받아서 리턴하면 이렇게 나타난다.(이게 일반적)

- 하지만 다른 값을 리턴해줄 수도 있다!

 

 

Messenger 에 메소드 추가

- 메신저의 sendGreeting() 메소드를 전달하면서 메시지를 전달할 때,

 안에 어떤 값이 들어있는지 조사할 수가 있다.

 

- 조사해서 어떤 경우에는 return에서 끝나고 (1번)

 어떤 경우에는 흐름이 쪽 진행될 것! (2번)

- 두 개의 차이는 aspect가 적용된 메소드에서 joinPoint를 호출했느냐 아니냐이다.

 

- 조사한 내용에 문제가 있으면 → joinPoint 를 호출하지 않는다.

- 조사한 내용에 문제가 없으면 → joinPoint 를 호출한다.

 

 

- 3개의 단어로 테스트

- 2번에는 금지된 단어가 있다. 이 경우 입력받은 값을 리턴해주지 않았다.

→ joinPoint.proceed() 까지 가지 못하므로 메소드를 호출했음에도 불구하고 messenger 메소드의 내용을 실행하지 않을 수도 있다.

 

- 이것을 잘 활용하면 게시물 삭제시 본인이 작성한 글이 아니면 삭제를 요청해도 삭제 기능을 수행하지 않을 수 있다.

 

 

send*(..)

- 인자가 없어도 되고, 여러개 있어도 된다는 의미.

 

ex)

public void sendxxx(){ }

public void sendxxx(int num){ }

public void sendxxx(int num, String name){ }

public void sendxxx(String name, boolean isMan, Car car){ }

 

- 단 조사하는 것이 어려울 수 있다.. 메소드가 이렇게 다양하게 생겼을 수 있으므로...

- send*(..) 라고 작성하면 인자의 개수, 인자의 타입이 다양해도 적용할 수 있다.

 

- joinpoint.getArgs() 는 object 배열로 리턴된다.

 

public void sendxxx(){ }

- 이렇게 작성되어 있으면 이 배열은 방이 0인 빈 배열이다 (인자로 전달된 것이 없으므로)

public void sendxxx(int num){ }

- 이렇게 작성된 경우, 0번 방에 정수가 들어있다.

 

- object 배열에 이렇게 들어가 있으면 반복문을 돌면서 값을 꺼내오면 된다.

 

- instanceof 연산자로 데이터의 타입을 확인할 수 있다.

- tmp가 string이면 찾아서 내보내주기

 

- object가 string이고 0번 방에 있는 것이 확실하면 이렇게만 작성해도 되지만,

 이 메소드 말고 다른메소드에도 다양하게 적용하려면(여러 메소드에 적용할 것이라면)

 반복문을 돌면서 확인하는 작업도 필요하므로 for문 코드가 들어갔다.

 

- AOP를 잘 배워두기! 다양하게 활용 가능하다.

 


 

- spring 프로젝트를 import해서 사용하려면 import는 Maven으로 선택하면 된다.

 

- file-import-maven-Existing Maven Projects

- pom.xml에 의존 dependency들이 모두 명시되어 있기 때문에 하면 된다.

 

- repository 설정하기

 

- 동일하게 stage에 올리고 commit message추가