이번 과제의 목표는 비즈니스 코드는 건드리지 않고도 실행 흐름에서 반복되는 공통 관심사를 한 번에 적용하는 게 핵심이었다.
- 로그 (메서드명 / 인자)
- 성능 측정 (실행 시간)
- 반환값 기반 분기 (조회 성공 / 리스트 / 권한 등)
- 예외 발생 시 통합 로그
이걸 스프링 AOP로 묶어서 서비스 메서드가 어떤 걸 실행하든 일관된 방식으로 출력되게 만드는 것이 과제 흐름이다.
이 과제의 실행 흐름은 아래 순서로 이해하면 된다.
- MemberClient가 스프링 컨테이너를 올린다
- 컨테이너에서 MemberService 빈을 꺼내 메서드를 호출한다
- 실제로는 원본 객체가 아니라 프록시 (Proxy) 가 호출을 가로챈다
- 프록시는 Pointcut 조건에 맞으면 Advice를 실행한다
- 그 다음 원본 (ServiceImpl → DAO) 로직이 실행된다
- 반환 / 예외 여부에 따라 AfterReturning / AfterThrowing이 갈린다
AOP는 호출 흐름 (메서드 실행 경로)을 프록시가 가로채서 공통 로직을 수행하는 방식이다.
1) 과제 시작점: 콘솔 Member 프로그램 구조
콘솔 메뉴는 이런 구조다.
1. 전체출력 → getMemberList()
2. 1명출력 → getMember()
3. 회원가입 → insertMember()
4. 회원탈퇴 → deleteMember()
5. 이름변경 → updateMember()
이 중에서 조회 (get)는 반환값이 있고 수정 / 삭제는 보통 void 또는 성공 여부만 반환한다.
이 차이가 AOP에서 Pointcut (적용 대상)과 Advice (실행 타이밍)를 나눌 근거가 된다.
2) 가장 먼저 터진 문제: NoSuchBeanDefinitionException (“ms” 빈이 없음)
실행 초기에 아래 에러가 떴다.
No bean named 'ms' available
이건 “스프링 AOP” 문제가 아니라 스프링 빈 등록 이름 문제다.
MemberClient가 "ms"라는 이름으로 빈을 꺼내는데 @Service에 이름을 안 주면 기본 빈 이름은 보통 memberServiceImpl로 생성된다.
해결
@Service("ms")로 이름 맞추기
@Service("ms")
public class MemberServiceImpl implements MemberService { ... }
3) 과제 핵심: AOP 4종 적용 범위 설계 (Pointcut 2개)
어제 과제는 의도적으로 범위를 나눠서 적용했다.
- aPointcut: 서비스 Impl 전체 메서드
- bPointcut: get* 메서드 (조회 계열)
<aop:pointcut expression="execution(* com.example.biz..*Impl.*(..))" id="aPointcut"/>
<aop:pointcut expression="execution(* com.example.biz..*Impl.get*(..))" id="bPointcut"/>
이렇게 나누면 장점이 있다.
- insert/delete/update 같은 수정 메서드는 전체 공통 로그만 붙이고 (Before, Throwing)
- get* 조회 메서드는 성능 측정 (Around) + 반환 후 처리 (AfterReturning)까지 붙인다
이 방식이 흔하다.
모든 메서드에 측정 / 반환 처리까지 걸면 로그 폭발이 나기 때문이다.
4) 최종 AOP 설정 (XML): 바인딩 (returning/throwing)까지
after-returning / after-throwing은 바인딩 이름 설정이 없으면 제대로 안 돈다.
- returning="returnObj"
- throwing="exceptObj"
그리고 컴파일 옵션에 따라 파라미터 이름이 날아갈 수 있어 arg-names를 추가해 안정화했다.
<aop:config>
<aop:pointcut expression="execution(* com.example.biz..*Impl.*(..))" id="aPointcut"/>
<aop:pointcut expression="execution(* com.example.biz..*Impl.get*(..))" id="bPointcut"/>
<!-- 1) BEFORE: JoinPoint로 메서드/인자 로그 -->
<aop:aspect ref="pla">
<aop:before method="printLog" pointcut-ref="aPointcut"/>
</aop:aspect>
<!-- 2) AROUND: get* 성능 측정 -->
<aop:aspect ref="aa">
<aop:around method="around" pointcut-ref="bPointcut" arg-names="pjp"/>
</aop:aspect>
<!-- 3) AFTER-RETURNING: get* 반환값 바인딩 -->
<aop:aspect ref="ar">
<aop:after-returning method="printLog"
pointcut-ref="bPointcut"
returning="returnObj"
arg-names="jp,returnObj"/>
</aop:aspect>
<!-- 4) AFTER-THROWING: 예외 바인딩 -->
<aop:aspect ref="ata">
<aop:after-throwing method="printLog"
pointcut-ref="aPointcut"
throwing="exceptObj"
arg-names="jp,exceptObj"/>
</aop:aspect>
</aop:config>
5) Advice 4종 코드 (과제에서 쓰인 핵심 코드)
(1) Before (PlusLogAdvice): 메서드명 / 인자 출력
public class PlusLogAdvice {
public void printLog(JoinPoint jp) {
System.out.println("향상된 로그 시작");
String methodName = jp.getSignature().getName();
System.out.println("핵심관심 메서드명 : " + methodName);
Object[] args = jp.getArgs();
System.out.println("메서드 인자들을 받아올수있음");
for(Object arg : args) {
System.out.println(arg);
}
System.out.println("향상된 로그 끝");
}
}
포인트
- 서비스 메서드가 뭐든 상관 없이 “호출된 메서드명 / 인자”를 공통 출력할 수 있다.
- 실무에서는 개인정보 마스킹이 필수다.
(2) Around (AroundAdvice): 성능 측정 + proceed 필수
public class AroundAdvice {
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
System.out.println("현재 수행중인 비즈니스 메서드 : " + methodName);
StopWatch sw = new StopWatch();
sw.start();
Object returnObj = pjp.proceed();
sw.stop();
System.out.println("수행에 걸린시간 : " + sw.getTotalTimeMillis() + "ms");
return returnObj;
}
}
포인트
- proceed()를 호출하지 않으면 핵심 로직이 실행되지 않는다.
- 반환값을 그대로 돌려줘야 호출 흐름이 유지된다.
- 실무에서는 실행 시간 측정이 모니터링 / APM의 기본이다.
(3) AfterReturning (AfterReturningAdvice): 반환값 타입 분기
처음에는 null/List 케이스를 고려하지 않아 “SELECTALL”이 부정확하게 찍혔다.
그래서 null / List / DTO 단건을 분리해 의도대로 출력되게 보강했다.
public class AfterReturningAdvice {
public void printLog(JoinPoint jp, Object returnObj) {
System.out.println("+++티+++");
String methodName = jp.getSignature().getName();
if (returnObj == null) {
System.out.println("조회 결과 없음 (null 반환) - " + methodName);
System.out.println("+++모+++");
return;
}
if (returnObj instanceof java.util.List) {
java.util.List<?> list = (java.util.List<?>) returnObj;
System.out.println("SELECTALL 관련 메서드 - " + methodName + " / 결과 " + list.size() + "건");
System.out.println("+++모+++");
return;
}
if (returnObj instanceof MemberDTO) {
MemberDTO member = (MemberDTO) returnObj;
if ("ADMIN".equals(member.getMrole())) {
System.out.println(">>> 관리자 로그인 <<< - " + methodName);
} else {
System.out.println(">>> 일반 로그인 <<< - " + methodName);
}
}
else if (returnObj instanceof BoardDTO) {
System.out.println("게시글 관련 메서드 - " + methodName);
}
else {
System.out.println("기타 반환 타입(" + returnObj.getClass().getSimpleName() + ") - " + methodName);
}
System.out.println("+++모+++");
}
}
포인트
- instanceof는 null이면 항상 false라서 null 먼저 분기해야 한다.
- List 반환은 단건 DTO와 성격이 다르기 때문에 별도 처리하는 게 자연스럽다.
- "ADMIN".equals(...)로 NPE를 방지하는 게 실무 습관이다.
(4) AfterThrowing (AfterThrowingAdvice): 예외 로그 + 분류
public class AfterThrowingAdvice {
public void printLog(JoinPoint jp, Exception exceptObj) {
System.out.println("예외발생시 출력되는 로그");
System.out.println(exceptObj.getMessage());
if(exceptObj instanceof NullPointerException) {
System.out.println("ㅇㅇ월 ㅇㅇ일 확인된 예외");
System.out.println("ㅇㅇ님이 조치함");
}
else {
System.out.println("미확인 예외 발생!!!");
}
}
}
포인트
- 예외 객체를 바인딩받아 “예외 종류별로” 대응을 달리할 수 있다.
- 실무에서는 예외 타입별로 알림 / 심각도 / 티켓 발행까지 연결한다.
6) 실행 결과 로그를 “흐름대로” 해석하기
예를 들어 전체 출력 (1번)을 선택하면 로그 흐름이 이렇게 된다.
- Before 실행
→ 메서드명 / 인자 출력 - Around 실행 (get*이므로)
→ 수행중 메서드 출력 - DAO 실행
→ JDBC URL 출력 등 - AfterReturning 실행 (get*이므로)
→ 반환값 (List/단건/null) 기준 분기 출력 - Around 종료
→ 수행 시간 출력
로그 순서가 “프록시가 앞뒤로 감싸는 구조”를 그대로 보여준다.
- 공통 로깅을 핵심 코드에서 분리 (유지보수성)
- 조회 (get*)에만 성능 측정 적용 (로그 폭발 방지, 범위 제어)
- 반환값 바인딩으로 “타입 / 권한 기반” 후처리 가능
- 예외 바인딩으로 장애 대응 로그 체계의 시작점 구성
- arg-names로 바인딩 안정화 (환경 / 컴파일 옵션 차이 대응)
'수업 복습' 카테고리의 다른 글
| 스프링에서의 JDBC 정리 (0) | 2026.01.28 |
|---|---|
| Spring AOP를 어노테이션 (@)으로 전환하는 흐름 정리 (0) | 2026.01.27 |
| 바인드 변수 (0) | 2026.01.26 |
| AOP 질의응답 (0) | 2026.01.26 |
| Spring XML 기반 AOP 실습 정리 (0) | 2026.01.23 |