OOP (Object Oriented Programming) : 객체 지향 프로그래핑
AOP (Aspect Oriented Programming) : 관점 지향 프로그래밍
2가지에 대해서 알아야 한다.
Java와 같은 OOP의 특징과 Spring의 AOP의 특징을 함께 알아야 한다.
AOP는 OOP의 확장 개념이다.
AOP : 중복되는 공통 코드를 분리하고 코드 실행 전이나 후의 시점에 해당 코드를 삽입함으로써 소스 코드의 중복을 줄이고, 필요할 때마다 가져다 쓸 수 있게 객체화하는 기술
| 용어 | 설명 |
|---|---|
| Aspect | 핵심 비즈니스 로직과는 별도로 수행되는 횡단 관심사를 말한다. |
| Advice | Aspect의 기능 자체를 말한다. |
| Join point | Advice가 적용될 수 있는 위치를 말한다. |
| Point cut | Join point 중에서 Advice가 적용될 가능성이 있는 부분을 선별한 것을 말한다. |
| Weaving | Advice를 핵심 비즈니스 로직에 적용하는 것을 말한다. |

Aspect : 어느 지점에 어떤 기능을 사용할 것인가
어느 지점 = Join point
Join point에 사용 할 기능을 선정하는 것 = Point cut
위 2가지 과정에 속하는 것이 Aspect 이고, 만든 것을 Advice, 그것을 실제 코드에 적용하는 것을 Weaving이라고 한다.
| 종류 | 설명 |
|---|---|
| Before | 대상 메소드가 실행되기 이전에 실행되는 어드바이스 |
| After-returning | 대상 메소드가 정상적으로 실행된 이후에 실행되는 어드바이스 |
| After-throwing | 예외가 발생했을 때 실행되는 어드바이스 |
| After | 대상 메소드가 실행된 이후에(정상, 예외 관계없이) 실행되는 어드바이스 |
| Around | 대상 메소드 실행 전/후에 적용되는 어드바이스 |
Proxy 기반의 AOP 구현체 : 대상 객체(Target Object)에 대한 프록시를 만들어 제공하며, 타겟을 감싸는 프록시는 서버 Runtime 시에 생성된다.
메서드 조인 포인트만 제공 : 핵심기능(대상 객체)의 메소드가 호출되는 런타임 시점에만 부가기능(어드바이스)을 적용할 수 있다.

Proxy : '대리자' 라는 개념으로 알고 있겠지만, 원래 코드를 건드리지 않고 부가 코드를 덧씌우는 기술로 알고 있는게 좋다.
MemberDTO, MemberDAO, MemberService 를 활용해서 출력해보자.
MemberDTO
public class MemberDTO {
private Long id;
private String name;
public MemberDTO(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "MemberDTO{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
Lombok 을 활용해서
@AllArgsConstructor,@ToString도 가능하다.
MemberService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MemberService {
private final MemberDAO memberDAO;
@Autowired
public MemberService(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
public List<MemberDTO> findAllMembers() {
System.out.println("Target -> findAllMembers()");
return memberDAO.selectAllMembers();
}
}
MemberDAO
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class MemberDAO {
private final List<MemberDTO> memberList;
public MemberDAO() {
memberList = new ArrayList<>();
memberList.add(new MemberDTO(1L, "홍길동"));
memberList.add(new MemberDTO(2L, "유관순"));
}
public List<MemberDTO> selectAllMembers() {
return memberList;
}
}
Application
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext("com.ohgiraffers.section01.aop");
MemberService memberService = context.getBean("memberService", MemberService.class);
System.out.println("===== Select All Members =====");
List<MemberDTO> members = memberService.findAllMembers();
members.forEach(System.out::println);
}
}
실행결과

이제 AOP 기능을 동작하기 위해서 build.gradle에 추가할 라이브러리가 있다.
build.gradle
// https://mvnrepository.com/artifact/one.gfw/aspectjweaver
implementation 'one.gfw:aspectjweaver:1.9.19'
// https://mvnrepository.com/artifact/org.aspectj/aspectjrt
implementation("org.aspectj:aspectjrt:1.9.19")
dependency 에 추가하자.
LoggingAspect 클래스를 생성하고 빈 스캐닝을 통해 빈 등록을 한다.
우선 Before 어드바이스를 간단하게 알아보자.
Before 어드바이스는 대상 메소드가 실행되기 이전에 실행되는 어드바이스이다. 미리 작성한 포인트 컷을 설정한다.
LoggingAspect
package com.jehun.section01.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.jehun.section01.aop.*Service.*(..))")
public void logBefore (JoinPoint joinPoint) {
System.out.println("Before joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("Before joinPoint.getSignature() : " + joinPoint.getSignature());
if (joinPoint.getArgs().length > 0) { // 타겟으로 하는 매개변수가 하나라도 있다면
System.out.println("Before joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]); // 배열 형태로도 받아올 수 있다.
}
}
}
@Aspect : pointcut과 advice를 하나의 클래스 단위로 정의하기 위한 어노테이션이다.
위의 LoggingAspect 를 적용하기 위해서는 빈 설정파일이 필요하다.
aspectj의 autoProxy 사용에 관한 설정을 해 주어야 advice가 동작한다.
ContextConfiguration
package com.jehun.section01.aop;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ContextConfiguration {
}
proxyTargetClass=true 설정은 cglib를 이용한 프록시를 생성하는 방식으로, Spring 3.2부터 스프링 프레임워크에 포함되어 별도 라이브러리 설정을 하지 않고 사용할 수 있다.
성능 면에서 더 우수하다.
다시 Application 클래스의 main을 실행하면
Before joinPoint.getTarget() : com.jehun.section01.aop.MemberService@c667f46
Before joinPoint.getSignature() : List com.jehun.section01.aop.MemberService.findAllMembers()
Target -> findAllMembers()
MemberDTO{id=1, name='홍길동'}
MemberDTO{id=2, name='유관순'}
위와 같이 출력된다. 즉, MemberService 부분을 호출할 때 LoggingAspect 클래스에서 중간에 채가서 @Before 어노테이션에서 정의한 포인트 컷 속 jointPoint.getTarget() 을 실행해준다.
조인포인트(Join Point)라고 한다.포인트컷(Point Cut)은 여러 조인포인트들에 어드바이스를 적용할 곳을 지정한 것이다.<포인트컷 표현식>
execution([수식어] 리턴타입 [클래스이름], 이름(파라미터))
1. 수식어 : public, private 등 수식어를 명시 (생략 가능하다)
2. 리턴 타입 : 리턴 타입을 명시
3. 클래스 이름(패키지명 포함) 및 메소드 이름 : 클래스 이름과 메소드 이름을 명시
4. 파마리터(매개변수) : 메소드의 파라미터를 명시
5. " * " : 1개이면서 모든 값이 올 수 있음
6. " .. " : 0개 이상의 모든 값이 올 수 있음
포인트컷의 예시
excution(public Integer com.jehun.section01.advice.*.*(*))
=>com.jehun.section01.advice패키지에 속해 있는 바로 다음 하위 클래스에 파라미터가 1개인 모든 메소드이자 접근 제어자가 public 이고 반환형이 Integer인 경우
excution(* com.jehun.section01.advice.annotation..stu*(..))
=>com.jehun.section01.advice패키지 및 하위 패키지에 속해 있고 이름이 stu로 시작하는 파라미터가 0개 이상인 모든 메소드이며 접근제어자와 반환형은 상관없음
LoggingAspect에 logPointcut() 메소드 추가하기
@Pointcut("execution(* com.jehun.section01.aop.*Service.*(..))")
public void logPointcut(){
}
그럼 이제 Before advice에서도 바꿔줄 수 있다.
@Before("LoggingAspect.logPointcut()")
public void logBefore (JoinPoint joinPoint) {
System.out.println("Before joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("Before joinPoint.getSignature() : " + joinPoint.getSignature());
if (joinPoint.getArgs().length > 0) { // 타겟으로 하는 매개변수가 하나라도 있다면
System.out.println("Before joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]); // 배열 형태로도 받아올 수 있다.
}
}
이번엔 After 어드바이스에 대해 알아보자.
After 어드바이스는 대상 메소드가 실행된 이후에(정상, 예외 관계없이) 실행되는 어드바이스이다. 미리 작성한 포인트 컷을 설정한다.
포인트 컷을 동일한 클래스 내에서 사용한다면, 클래스명 생략 가능
패키지가 다르다면, 패키지를 포함한 클래스명을 기술해야한다.
Before 어드바이스와 동일하게 매개변수로 JoinPoint 객체 전달 가능
@After("logPointcut()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After joinPoint.getTarget() " + joinPoint.getTarget());
System.out.println("After joinPoint.getSignature() " + joinPoint.getSignature());
if(joinPoint.getArgs().length > 0){
System.out.println("After joinPoint.getArgs()[0] " + joinPoint.getArgs()[0]);
}
}
위의 코드 추가 후 실행 결과
===== Select All Members =====
Before joinPoint.getTarget() : com.jehun.section01.aop.MemberService@26adfd2d
Before joinPoint.getSignature() : List com.jehun.section01.aop.MemberService.findAllMembers()
Target -> findAllMembers()
After joinPoint.getTarget() com.jehun.section01.aop.MemberService@26adfd2d
After joinPoint.getSignature() List com.jehun.section01.aop.MemberService.findAllMembers()
MemberDTO{id=1, name='홍길동'}
MemberDTO{id=2, name='유관순'}
Application 에 추가
System.out.println("===== Select One Members =====");
System.out.println(memberService.findByMember(1));
MemberService 에 추가
public MemberDTO findByMember(int index) {
System.out.println("target => findByMember 실행");
return memberDAO.selectByMember(index);
}
MemberDAO에 추가
public MemberDTO selectByMember(int index) {
return memberList.get(index);
}
실행 결과
===== Select All Members =====
Before joinPoint.getTarget() : com.jehun.section01.aop.MemberService@26adfd2d
Before joinPoint.getSignature() : List com.jehun.section01.aop.MemberService.findAllMembers()
Target -> findAllMembers()
After joinPoint.getTarget() com.jehun.section01.aop.MemberService@26adfd2d
After joinPoint.getSignature() List com.jehun.section01.aop.MemberService.findAllMembers()
MemberDTO{id=1, name='홍길동'}
MemberDTO{id=2, name='유관순'}
===== Select One Members =====
Before joinPoint.getTarget() : com.jehun.section01.aop.MemberService@26adfd2d
Before joinPoint.getSignature() : MemberDTO com.jehun.section01.aop.MemberService.findByMember(int)
Before joinPoint.getArgs()[0] : 1
target => findByMember 실행
After joinPoint.getTarget() com.jehun.section01.aop.MemberService@26adfd2d
After joinPoint.getSignature() MemberDTO com.jehun.section01.aop.MemberService.findByMember(int)
After joinPoint.getArgs()[0] 1
MemberDTO{id=2, name='유관순'}
잘 보면 이전에 jointPoint.getArgs()[0] 부분에서 출력이 없다가, 회원 1명 조회를 하니 생긴 것이 보인다.
다시 해당 코드가 작성된 곳으로 가면 주석으로 매개변수가 1개 이상이 있는 경우 라고 하여 회원 1명을 조회하기 위해 인덱스에 1을 넣었기 때문에 그 때 출력된 것이다.
이번엔 @AfterReturning 어노테이션에 대해서 알아보자.
LoggingAspect에 추가
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("After Returning result: " + result);
if (result != null && result instanceof List) {
((List<MemberDTO>) result).add(new MemberDTO(3L, "반환 값 가공"));
}
}
AfterReturning 어드바이스는 대상 메소드가 정상적으로 실행된 이후에 실행되는 어드바이스이다. 미리 작성한 포인트 컷을 설정한다.
returning 속성은 리턴값으로 받아올 오브젝트의 매개변수 이름과 동일해야 한다. 또한 joinPoint는 반드시 첫 번째 매개변수로 선언해야 한다.
이 어드바이스에서는 반환 값을 가공할 수도 있다.
AfterReturning 내용의 실행결과
Target -> findAllMembers()
After Returning result: [MemberDTO{id=1, name='홍길동'}, MemberDTO{id=2, name='유관순'}]
After joinPoint.getTarget() com.jehun.section01.aop.MemberService@72ade7e3
After joinPoint.getSignature() List com.jehun.section01.aop.MemberService.findAllMembers()
MemberDTO{id=1, name='홍길동'}
MemberDTO{id=2, name='유관순'}
MemberDTO{id=3, name='반환 값 가공'}
MemberService 클래스의 findAllMembers 메소드가 실행된 후에 AfterReturning 어드바이스의 실행 내용이 삽입돼 동작하는 것을 확인할 수 있다.
이번에는 @AfterThrowing 에 대해 알아보자.
AfterThrowing 어드바이스는 예외가 발생했을 때 실행되는 어드바이스이다. 미리 작성한 포인트 컷을 설정한다.
throwing 속성의 이름과 매개변수의 이름이 동일해야 한다. 이 어드바이스에서는 Exception 에 따른 처리를 작성할 수 있다.
LoggingAspect에 추가
@AfterThrowing(pointcut = "logPointcut()", throwing = "exception")
public void logAfterThrowing(Throwable exception) {
System.out.println("AfterThrowing exception = " + exception);
}
Application 에 추가
System.out.println("===== Select One Members (예외 발생시키기) =====");
System.out.println(memberService.findByMember(3));
3을 넣었을 때 IndexOutOfBounds가 발생하게 될 것이고, exception을 통해 출력되는 것을 볼 수 있을 것이다.
실행 결과
===== Select One Members =====
Before joinPoint.getTarget() : com.jehun.section01.aop.MemberService@72ade7e3
Before joinPoint.getSignature() : MemberDTO com.jehun.section01.aop.MemberService.findByMember(int)
Before joinPoint.getArgs()[0] : 3
target => findByMember 실행
AfterThrowing exception = java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
After joinPoint.getTarget() com.jehun.section01.aop.MemberService@72ade7e3
After joinPoint.getSignature() MemberDTO com.jehun.section01.aop.MemberService.findByMember(int)
After joinPoint.getArgs()[0] 3
Around 어드바이스 알아보기
Around 어드바이스는 대상 메소드 실행 전/후에 적용되는 어드바이스이다. 미리 작성한 포인트 컷을 설정한다.
Around Advice는 가장 강력한 어드바이스이다. 이 어드바이스는 조인포인트를 완전히 장악하기 때문에 앞에 살펴 본 어드바이스 모두 Around 어드바이스로 조합할 수 있다.
AroundAdvice의 조인포인트 매개변수는 ProceedingJoinPoint로 고정되어 있다. JoinPoint의 하위 인터페이스로 원본 조인포인트의 진행 시점을 제어할 수 있다.
조인포인트 진행하는 호출을 잊는 경우가 자주 발생하기 때문에 주의해야 하며 최소한의 요건을 충족하면서도 가장 기능이 약한 어드바이스를 쓰는게 바람직하다.
LoggingAspect 에 추가
@Around("logPointcout()")
public Object logArount(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around Before : " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("Around After : " + joinPoint.getSignature().getName());
return result;
}
실행 결과 (AfterThrowing과 관련된 인덱스 3을 넣는 코드는 주석처리했다.)
===== Select All Members =====
Around Before : findAllMembers
(중략)
Around After : findAllMembers
MemberDTO{id=1, name='홍길동'}
MemberDTO{id=2, name='유관순'}
MemberDTO{id=3, name='반환 값 가공'}
===== Select One Members =====
Around Before : findByMember
(중략)
Around After : findByMember
MemberDTO{id=2, name='유관순'}
Reflection : 컴파일 된 자바 코드에서 필드 및 메소드의 정보를 구해오는 방법이다.
이를 통해 프로그램의 동적인 특성 구현 가능
예를 들어, 리플렉션을 이용하면 실행 중인 객체의 클래스 정보를 얻어오거나, 클래스 내부의 필드나 메소드에 접근할 수 있다.
스프링에서는 런타임 시 개발자가 등록한 빈을 애플리케이션 내부에서 다루기 위한 기술이기도 한다.
쓰이는 곳 : 스프링 프레임워크, 마이바티스, 하이버네이트, jackson 등의 라이브러리
리플렉션 테스트의 대상이 될 Account 클래스를 생성한다.
Account
package com.ohgiraffers.section02.reflection;
public class Account {
private String backCode;
private String accNo;
private String accPwd;
private int balance;
public Account() {}
public Account(String bankCode, String accNo, String accPwd) {
this.backCode = bankCode;
this.accNo = accNo;
this.accPwd = accPwd;
}
public Account(String bankCode, String accNo, String accPwd, int balance) {
this(bankCode, accNo, accPwd);
this.balance = balance;
}
public String getBalance() {
return this.accNo + " 계좌의 현재 잔액은 " + this.balance + "원 입니다.";
}
public String deposit(int money) {
String str = "";
if(money >= 0) {
this.balance += money;
str = money + "원이 입급되었습니다.";
}else {
str = "금액을 잘못 입력하셨습니다.";
}
return str;
}
public String withDraw(int money) {
String str = "";
if(this.balance >= money) {
this.balance -= money;
str = money + "원이 출금되었습니다.";
}else {
str = "잔액이 부족합니다. 잔액을 확인해주세요.";
}
return str;
}
}
Class 타입의 인스턴스 -> 해당 클래스의 메타 정보를 가지고 있는 클래스
/* .class 문법을 이용하여 Class 타입의 인스턴스를 생성할 수 있다. */
Class class1 = Account.class;
System.out.println("class1 = " + class1);
Class class2 = new Account().getClass();
System.out.println("class2 = " + class2);
try {
Class class3 = Class.forName("com.ohgiraffers.section02.reflection.Account");
System.out.println("class3 = " + class3);
Class class4 = Class.forName("[D");
Class class5 = double[].class;
System.out.println("class4 = " + class4);
System.out.println("class5 = " + class5);
Class class6 = Class.forName("[Ljava.lang.String;");
Class class7 = String[].class;
System.out.println("class6 = " + class6);
System.out.println("class7 = " + class7);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
실행 결과
class1 = class com.jehun.section02.reflection.Account
class2 = class com.jehun.section02.reflection.Account
class3 = class com.jehun.section02.reflection.Account
class4 = class [D
class5 = class [D
class6 = class [Ljava.lang.String;
class7 = class [Ljava.lang.String;
field 정보에 접근 가능하다.
Application에 추가
Field[] fields = Account.class.getDeclaredFields();
for (Field field : fields) {
System.out.println("modifiers = " + Modifier.toString(field.getModifiers())
+ ", type = " + field.getType()
+ ", name = " + field.getName());
}
실행 결과

생성자 정보에 접근 가능하다.
Application 에 추가
Constructor[] constructors = Account.class.getConstructors();
for (Constructor constructor : constructors) {
System.out.println("name : " + constructor.getName());
Class[] params = constructor.getParameterTypes();
for (Class param : params) {
System.out.println("paramType : " + param.getTypeName());
}
}
실행 결과
name : com.jehun.section02.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
paramType : int
name : com.jehun.section02.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
name : com.jehun.section02.reflection.Account
인스턴스도 생성 가능하다.
try {
Account acc = (Account) constructors[0].newInstance("20", "110-223-123456", "1234", 10000);
System.out.println(acc.getBalance());
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
실행 결과
110-223-123456 계좌의 현재 잔액은 10000원 입니다.
메소드 정보에 접근 가능하다.
Application에 추가
Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for (Method method : methods) {
System.out.println(Modifier.toString(method.getModifiers()) + " "
+ method.getReturnType().getSimpleName() + " "
+ method.getName());
if ("getBalance".equals(method.getName())) {
getBalanceMethod = method;
}
}
실행 결과

invoke 메소드로 메소드 호출이 가능하다.
Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for (Method method : methods) {
System.out.println(Modifier.toString(method.getModifiers()) + " "
+ method.getReturnType().getSimpleName() + " "
+ method.getName());
if ("getBalance".equals(method.getName())) {
getBalanceMethod = method;
}
}
try {
System.out.println(getBalanceMethod.invoke(((Account)constructors[2].newInstance())));
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
실행 결과
