의존성 주입: 의존하는 부분을 외부에서 주입하는 것
AOP: 공통 처리 등의 '횡단적 관심사'를 추출하고 프로그램의 여러 곳에서 호출할 수 있게 설정함으로 개발자는 실현해야 할 기능인 '중심적 관심사'에만 집중해서 작성하면 되는 구조
예시
사용하는 객체 A클래스와 사용되는 객체 B클래스가 있다고 하자
A 클래스에서 B 클래스를 사용하려면 new 키워드를 이용해 B 클래스의 인스턴스를 생성하고 B 클래스의 메서드를 사용한다. 이때 B 클래스에서 구현한 메서드를 변경하면 그 영향으로 A 클래스에서도 해당 메서드를 변경해야 한다. 이런 관계를 'A 클래스는 B 클래스에 의존한다' 라고 한다.
의존에는 두 가지 유형이 있다.
1. 클래스 의존(구현 의존)
2. 인터페이스 의존
예시
사용하는 객체 A 클래스에서 사용되는 객체 B 클래스의 메서드를 호출하는 경우
// A 클래스
xxx(){
B b = new B();
b.methodX();
// B 클래스
methodX() {
...
}
설계가 변경돼 사용되는 객체를 C 클래스로 변경하고 methodY 메서드를 호출하도록 변경해야 하면 어떻게 해야할까
// A 클래스
xxx() {
C c = new C();
c.methodY();
}
// C 클래스
methodY(){
...
}
여기서 문제 -> 이처럼 '사용하는 객체' 클래스에서 '사용되는 객체' 클래스의 타입을 직접 지정하면 '사용되는 객체' 클래스를 변경할 경우 이를 이용하고 있는 곳을 모두 수정해야 한다.
예시
I 인터페이스가 있고 그것을 구현한 '사용되는 객체' B 클래스가 있다. '사용하는 객체' A 클래스에서 B 클래스의 methodX 메서드를 호출한다.
A 클래스에서 인터페이스로 추상화된 I를 이용한다
// A 클래스
xxx(){
I i = new B();
i.methodX();
}
// I 인터페이스
methodX();
// B 클래스 implements I
methodX(){
...
}
설계 변경이 발생해 '사용되는 객체' 클래스를 변경하게 되었다. 어떻게 수정해야 할까
// A 클래스
xxx(){
I i = new C();
i.methodX();
}
클래스 의존과 비교해 수정한 곳이 줄었다
이처럼 인터페이스를 구현한 '사용되는 객체' 클래스를 변경하는 경우 다음과 같은 이점이 있다.
1. 인터페이스는 참조를 받는 유형으로 사용할 수 있으므로 변수의 이름을 변경하지 않아도 된다.
2. 인터페이스가 선언된 메서드를 이용하면 클래스가 바뀌어도 메서드명을 변경하지 않아도 된다.
// Calculator 인터페이스
package chapter03.used;
/**
* 계산 처리
*/
public interface Calculator {
/**
* 계산 처리를 함
* @param x
* @param y
* @return Integer
*/
Integer calc(Integer x, Integer y);
}
// 구현 클래스(AddCalc)
package chapter03.used;
/**
* Calculator 구현 클래스<br>
* 덧셈 처리
*/
public class AddCalc implements Calculator{
@Override
public Integer calc(Integer x, Integer y){
return x + y;
}
}
// 구현 클래스(SubCalc)
package chapter03.used;
/**
* Calculator 구현 클래스<br>
* 뺄셈 처리
*/
public class SubCalc implements Calculator{
@Override
public Integer calc(Integer x, Integer y){
return x - y;
}
}
// Call 클래스
package chapter03.use;
import chapter03.used.AddCalc;
import chapter03.used.Calculator;
import chapter03.used.SubCalc;
/**
* 인터페이스 의존 확인
*/
public class Call {
public static void main(String[] args){
//Calculator calculator = new AddCalc();
Calculator calculator = new SubCalc();
Integer result = calculator.calc(10, 5);
System.out.println("계산 결과는 (" + result + ")입니다.");
}
}
인터페이스 의존으로 작성하면 '사용하는 객체' 클래스에서 수정할 부분이 줄어든다. 하지만 의존성 주입을 사용하면 '사용하는 객체'의 클래스를 수정하지 않아도 된다.
의존성 주입: 의존하는 부분을 외부에서 주입하는 것
지금까지 인스턴스를 생성하는데 new 키워드를 사용했지만 인스턴스 생성과 같은 작업을 프레임워크에 맡길 수 있고 그 역할을 하는 것이 DI 컨테이너다. 스프링 프레임워크는 임의로 구현한 클래스를 인스턴스로 만들어주는 기능을 제공한다.
DI 컨테이너에 인스턴스 생성을 맡기고 다음의 규칙을 지키는 것으로 '사용하는 객체' 클래스를 전혀 수정할 필요가 없게끔 만들 수 있다.
인터페이스를 이용해 의존성을 만든다: 의존하는 부분에 인터페이스를 이용한다
인스턴스를 명시적으로 생성하지 않는다: 인스턴스 생성에 new 키워드를 사용하지 않는다
-> 인스턴스를 생성하려는 클래스에 인스턴스 생성 어노테이션을 부여한다.
스프링 프레임워크는 시작할 때 프로젝트의 모든 패키지를 스캔한다. 이 기능을 컴포넌트 스캔 이라고 한다.
컴포넌트 스캔 후 스프링 프레임워크는 인스턴스 생성 어노테이션이 부여된 클래스를 추출하고 클래스의 인스턴스를 생성한다.
어노테이션 | 개요 |
---|---|
@Controller | 인스턴스 생성 지시, 스프링 MVC를 이용할 때 컨트롤러에 부여 |
@Service | 인스턴스 생성 지시, 트랜잭션 경계가 되는 도메인(서비스) 기능에 부여 |
@Repository | 인스턴스 생성 지시, 데이터베이스 액세스(리포지토리) 기능에 부여 |
@Component | 위 용도 이외에 클래스를 부여 |
인스턴스를 이용하고 싶은 곳에 어노테이션을 부여한다
-> 스프링 프레임워크에 의해 생성된 인스턴스를 사용하는 클래스에 참조를 받는 필드를 선언하고 필드에 @Autowired 어노테이션을 부여한다
package com.example.demo.chapter03.used;
/**
* 인사 인터페이스
*/
public interface Greet {
/**
* 인사하기
*/
void greeting();
}
Greet 인터페이스
package com.example.demo.chapter03.used;
import org.springframework.stereotype.Component;
/**
* Greet 구현 클래스 <br>
* 아침 인사 하기
*/
//@Component
public class MorningGreet implements Greet{
@Override
public void greeting(){
System.out.println("--------------");
System.out.println("좋은 아침입니다");
System.out.println("--------------");
}
}
Greet 인터페이스의 구현 클래스 MorningGreet
package com.example.demo.chapter03.used;
import org.springframework.stereotype.Component;
/**
* Greet 구현 클래스<br>
* 저녁 인사 하기
*/
@Component
public class EveningGreet implements Greet{
@Override
public void greeting(){
System.out.println("-----------------");
System.out.println("좋은 저녁입니다.");
System.out.println("-----------------");
}
}
Greet 인터페이스의 구현 클래스 EveningGreet
package com.example.demo;
import com.example.demo.chapter03.used.Greet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 스프링 부트 시작 클래스
*/
@SpringBootApplication
public class DemoApplication {
/**
* main 메서드
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args)
.getBean(DemoApplication.class).execute();
}
/**
* 주입하는 곳(인터페이스)
*/
@Autowired
Greet greet;
/**
* 실행 메서드
*/
private void execute(){
greet.greeting();
}
}
EveningGreet 클래스에 @Component 어노테이션을 부여해 DemoApplication을 실행하면 @Component가 부여된 EveningGreet 클래스의 greeting 메서드가 호출된다.
이는 스프링 프레임워크 기동 시 컴포넌트 스캔에 의해 EveningGreet 인스턴스가 생성되고 @Autowired 어노테이션에 따라 EveningGreet 클래스의 인스턴스가 클래스의 greet 필드에 주입된다. 이를 실행하면 EveningGreet 클래스의 greeting 메서드가 실행된다.
어노테이션을 간단히 설명하면 다음 세 가지 항목이 된다.
1. 어노테이션(annotaion)은 주석을 의미하는 영어 표현이다.
2. '@xxx'와 같은 형태로 작성한다.
3. 외부 소프트웨어에 필요한 처리 내용을 전달한다.
어노테이션을 사용하면 에러를 출력하거나 프로그램의 동작을 변하는 등 다양한 것을 할 수 있다.
레이어(layer): '층'의 의미, 계층 구조로 되어 있는 각 층을 말한다. 쉽게말해 복잡한 전체 내용을 한 번에 정리해 이해하지 말고 계층화해 각 계층별로 대상의 의미를 이해하는 것이다.
레이어 | 개요 |
---|---|
애플리케이션 레이어(Application Layer) | 애플리케이션 레이어는 클라이언트와의 데이터 입출력을 제어하는 레이어다. |
도메인 레이어 (Domain Layer) | 도메인 레이어는 애플리케이션의 중심이 되는 레이어로서 업무 처리를 수행하는 레이어다. |
인프라스트럭처 레이어(Infrastructure Layer | 인프라스트럭처 레이어는 데이터베이스에 대한 데이터 영속성(Persistence Context) 등을 담당하는 레이어다. |
위 세 레이어는 '도메인 주도 설계(Domain-Driven Design)'에서 설명한 내용이다.
어노테이션 | 개요 |
---|---|
@Controller | 애플리케이션 레이어의 컨트롤러에 부여 |
@Service | 도메인 레이어의 업무 처리에 부여 |
@Repository | 인프라 레이어의 데이터베이스 액세스 처리에 부여 |
@Component | @Controller, @Service, @Repository의 용도 이외의 인스턴스 생성 대상 클래스에 부여 |
레이어별 인스턴스 생성 어노테이션 설명
@Component는 하위 로직을 처리할 때 사용
직접 커스텀 어노테이션을 만들 수도 있다.
커스텀 어노테이션을 만들 때는 java.lang.Annotation 인터페이스를 상속하고 만든다. 또한 커스텀 어노테이션을 정의할 때는 전용 자바 파일을 생성할 필요가 있다.
자세한 내용은 @interface 검색
데이터베이스 액세스 처리에는 예외 발생 시 처리하는 내용이 반드시 포함되어야 한다. 예외 처리를 하지 않으면 프로그램이 중지되고 자바의 경우 예외 처리를 프로그램에 포함하지 않으면 컴파일에 실패한다.
용어 | 내용 |
---|---|
어드바이스(Advice) | 횡단적 관심사의 구현(메서드). 로그 출력 및 트랜잭션 제어 등이다. |
애스펙트(Aspect) | 어드바이스를 정리한 것(클래스)이다. |
조인포인트(JoinPoint) | 어드바이스를 중심적인 관심사에 적용하는 타이밍. 메서드(생성자) 실행 전, 메서드(생성자) 실행 후 등 실행되는 타이밍이다. |
포인트컷(Pointcut) | 어드바이스를 삽입할 수 있는 위치. 예를 들어, 메서드 이름이 get으로 시작할 때만 처리하는 조건을 정의할 수 있다. |
인터셉터(Interceptor) | 처리의 제어를 인터셉트하기 위한 구조 또는 프로그램이다. 스프링 프레임워크에서는 인터셉트라는 메커니즘으로 어드바이스를 중심 관심사에 추가한 것처럼 보이게 한다. |
타깃(Target) | 어드바이스가 도입되는 대상을 말한다. |
스프링 프레임워크에서 '인터셉터'라는 메커니즘을 사용해 횡단적 관심사(어드바이스)를 중심적 관심사(타깃)에 삽입하는 것처럼 보일 수 있다.
어드바이스 | 내용 | 어노테이션 |
---|---|---|
Before Advice | 중심적 관심사가 실행되기 '이전'에 횡단적 관심사를 실행 | @Before |
After Returning Advice | 중심적 관심사가 '정상적으로 종료된 후'에 횡단적 관심사를 실행 | @AfterReturning |
After Throwing Advice | 중심적 관심사로부터 '예외가 던져진 후'로 횡단적 관심사를 실행 | @AfterThrowing |
After Advice | 중심적 관심사의 '실행 후'에 횡단적 관심사를 실행(정상 종료나 예외 종료 등의 결과와 상관없이 실행) | @After |
Around Advice | 중앙적 관심사 호출 전후에 횡단적 관심사를 실행 | @Around |
직접 어드바이스를 만드는 경우 패키지, 클래스, 메서드 등 어드바이스 삽입 대상을 조건으로 지정할 수 있다. 지정하는 조건 방법에는 포인트컷 식을 사용한다. 포인트 컷 표현식은 여러 가지가 있지만 이 문서에서는 'execution' 지시자를 설명한다.
와일드카드 | 내용 |
---|---|
*(애스터리스크) | 임의의 문자열을 나타내고, 패키지를 나타낼 때는 임의의 패키지 한 계층을 나타낸다. 메서드의 인수에서는 한 개의 인수를 나타내 반환값으로도 이용할 수 있다. |
..(점 두 개) | 패키지를 나타내는 경우 0개 이상의 패키지를 나타낸다. 메서드의 인수를 포함하는 경우에는 0개 이상의 임의의 인수를 나타낸다. |
+(플러스) | 클래스명 뒤에 기술해 클래스와 그 서브클래스 및 구현 클래스 모두를 나타낸다. |
구현 예 | 내용 |
---|---|
execution(*com.example.service.DemoService.*(..)) | DemoService 클래스의 메서드에 어드바이스를 적용한다. |
execution(*com.example.service.DemoService.select*(..)) | DemoService 클래스의 select로 시작하는 메서드에 어드바이스를 적용한다. |
execution(String com.example.service.DemoService.*(..)) | DemoService 클래스의 반환값이 String 타입인 메서드에 어드바이스를 적용한다. |
execution(*com.example.service.DemoService.*(String,..) | DemoService 클래스의 첫 번째 인수가 String 타입인 메서드에 어드바이스를 적용한다. |
execution(*com.example.service.*.*(..)) | 지정된 패키지 아래의 모든 클래스의 메서드에 어드바이스를 적용한다(서브 패키지는 포함하지 않는다). |
execution(* com.example.service..*.*(..)) | service 패키지 바로 아래와 하위 패키지의 모든 클래스에 어드바이스를 적용한다. |
execution(* com.example.service.DemoService.*(*)) | DemoService 클래스의 인수가 하나인 메서드에 어드바이스를 적용한다. |
너무 많고 다 비슷하게 생겼다.
직접 어드바이스를 만들고 포인트컷 식에서 어드바이스 삽입 위치를 지정해 AOP의 동작 방식을 확인할 수 있는 프로그램을 만들어보자.
package com.example.dependencyinjectionsample.chapter03.aop;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SampleAspect {
}
어드바이스를 기술하는 클래스에는 @Aspect 어노테이션을 부여한다.
인스턴스를 생성하기 위해 @Component 어노테이션을 부여한다.
package com.example.demo.chapter03.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
@Aspect
@Component
public class SampleAspect {
@Before("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
public void beforeAdvice(JoinPoint joinPoint){
// 시작 부분 표시
System.out.println("==== Before Advice ====");
// 날짜를 출력
System.out.println(new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date()));
// 메서드 이름 출력
System.out.println(String.format("메서드:%s", joinPoint.getSignature().getName()));
}
}
어노테이션의 인수에는 포인트컷 식인 execution(반환형 패키지.클래스.메서드(인수))를 지정한다.
@After("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
public void afterAdvice(JoinPoint joinPoint){
// 시작 부분 표시
System.out.println("==== After Advice ====");
// 날짜를 출력
System.out.println(new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date()));
// 메서드명 출력
System.out.println(String.format("메서드명:%s", joinPoint.getSignature().getName()));
}
메서드를 실행한 후 호출되는 After Advice
@Around("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable{
// 시작 부분 표시
System.out.println("==== Around Advice ====");
System.out.println("처리전");
// 지정한 클래스의 메서드 실행
Object result = joinPoint.proceed();
System.out.println("처리후");
// 반환값을 돌려줄 필요가 있는 경우에는 Object 타입의 반환값을 돌려준다.
return result;
}
메서드 실행 전후에 호출되는 Around Advice
트랜잭션 관리에 @Transactional 어노테이션을 사용한다. @Transactional 어노테이션을 부여하여 데이터베이스 액세스 처리 메서드가 정상 종료하면 트랜잭션을 커밋, 예외가 발생하면 롤백한다.
개발 중 동작 상황 확인을 위해 print 찍는 작업은 매우 피곤
또한 프로그램 작성이 완료됐을 때 모든 '디버깅 로그'를 삭제해야 함.
이런 다수의 클래스에 공통으로 필요한 처리를 '횡단적 관심사'라고 한다.
만일 여러 클래스의 메서드에 print를 자동으로 넣어주는 기능 또는 필요가 없어졌을 때 모두 자동으로 삭제해주는 기능이 있다면 편리할 것이다. 이 같은 생각이 AOP 사고방식이다.
AOP의 주요 사항