수업 복습

바인드 변수로 핵심관심 정보 뽑아오기

_김영인 2026. 1. 26. 18:00

 

이번 과제의 목표는 비즈니스 코드는 건드리지 않고도 실행 흐름에서 반복되는 공통 관심사를 한 번에 적용하는 게 핵심이었다.

  • 로그 (메서드명 / 인자)
  • 성능 측정 (실행 시간)
  • 반환값 기반 분기 (조회 성공 / 리스트 / 권한 등)
  • 예외 발생 시 통합 로그

이걸 스프링 AOP로 묶어서 서비스 메서드가 어떤 걸 실행하든 일관된 방식으로 출력되게 만드는 것이 과제 흐름이다.

 

 

이 과제의 실행 흐름은 아래 순서로 이해하면 된다.

  1. MemberClient가 스프링 컨테이너를 올린다
  2. 컨테이너에서 MemberService 빈을 꺼내 메서드를 호출한다
  3. 실제로는 원본 객체가 아니라 프록시 (Proxy) 가 호출을 가로챈다
  4. 프록시는 Pointcut 조건에 맞으면 Advice를 실행한다
  5. 그 다음 원본 (ServiceImpl → DAO) 로직이 실행된다
  6. 반환 / 예외 여부에 따라 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번)을 선택하면 로그 흐름이 이렇게 된다.

  1. Before 실행
    → 메서드명 / 인자 출력
  2. Around 실행 (get*이므로)
    → 수행중 메서드 출력
  3. DAO 실행
    → JDBC URL 출력 등
  4. AfterReturning 실행 (get*이므로)
    → 반환값 (List/단건/null) 기준 분기 출력
  5. 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