본문 바로가기

개인 공부/인프런

[JavaSpring] AOP

728x90

해당글은 김영환 자바스프링 입문을 요약, 정리하면서 개인적인 견해가 들어간 글입니다.

AOP

AOP가 필요한 상황

  • 모든 메소드의 호출 시간을 측정하고 싶다면?
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
  • 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?

시간을 측정하기 위해 코드를 수정해보자.

우선 MemberService코드를 아래와 같이 수정해준다.

package study.studyspring.service;

import ch.qos.logback.core.joran.spi.ConsoleTarget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.studyspring.domain.Member;
import study.studyspring.repository.MemberRepository;
import study.studyspring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Long join(Member member) {

        long start = System.currentTimeMillis();

        try {
            validateDuplicateMember(member);    // 중복 회원 검증
            memberRepository.save(member);
            return member.getId();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("join = " + timeMs + "ms");
        }
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
            .ifPresent(m -> {
                throw new IllegalStateException("이미 존재하는 회원입니다.");
            } );
    }

    public List<Member> findMember() {
        long start = System.currentTimeMillis();
        try {
            return memberRepository.findAll();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;

            System.out.println("join = " + timeMs + "ms");
        }
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

long으로 시작할때 시간과 try finally을 이용해서 에러가 발생하더라도 완료되는 시간을 강제로 잴 수 있게 해줘야 한다.

finally에서는 처음 시간초와 finally안에서 끝나는 초를 빼서 그 값을 출력해주면 메소드의 호출 시간이 나온다.

이런식으로 모든 메소드에 만들어주면 된다….

잘 동작하는지 확인하려면 통합 테스트 코드를 실행하면 된다.

그러면 위 사진과 같이 뜰것이다.

위 코드의 문제점

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.
  • 시간을 측정하는 로직은 공통 관심 사항이다.
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
  • 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.

AOP를 적용한 코드

  • AOP: Aspect Oriented Programming
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리

우선 aop패키지를 하나 만들어준다.

패키지 내부에 TimeTraceAop클래스를 생성한다.

코드는 아래와 같다.

package study.studyspring.aop;

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 TimeTraceAop {
    @Around("execution(* study.studyspring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
        }
    }
}

@Around는 우리가 만든 AOP를 어디서 사용할지를 정해준다고 생각하면 된다.

현재는 studyspring의 하위폴더가 전부 AOP가 적용된다.

aop를 만들었으니 이제 Bean에 등록을 해줘야 한다.

  • AOP를 SpringBean에 등록하는 이유
    1. Spring의 AOP 기능은 프록시를 사용하여 AOP 기능을 수행한다.
      프록시는 Spring 컨테이너에서 Bean으로 등록된 객체에 대해 생성되므로, AOP를 사용하려면 대상 객체가 Spring의 Bean으로 등록되어 있어야 한다.
    2. AOP는 스프링 컨테이너에서 생성되는 프록시 객체를 사용하기 때문에, 대상 객체에 대한 모든 호출은 프록시를 통해 전달된다.
      그래서 대상 객체가 스프링의 Bean으로 등록되어 있지 않으면 AOP 기능이 동작하지 않는다.
    3. AOP를 위해 다양한 어드바이스(Advice) 타입을 지원한다.
      Advice는 AOP 기능이 적용될 시점과 방법을 정의한다.
      Advice를 구현한 클래스는 Spring의 Bean으로 등록하여 AOP를 적용할 수 있다.

때문에 SpringConfig를 들어가서 아래 코드로 바꿔준다.

package study.studyspring.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import study.studyspring.aop.TimeTraceAop;
import study.studyspring.repository.*;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private final MemberRepository memberRepository;

    //    @Autowired 생성자가 1개면 생략 가능
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }

    @Bean
    public TimeTraceAop timeTraceAop() {
        return new TimeTraceAop();
    }
//    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
//        return new JpaMemberRepository(em);
//    }
}

이후 실행시킨 후 회원 목록에 들어가면 아래 사진처럼 뜬다.

  • 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
  • 변경이 필요하면 이 로직만 변경하면 된다.
  • 원하는 적용 대상을 선택할 수 있다

AOP 적용 전

AOP 적용 후

728x90

'개인 공부 > 인프런' 카테고리의 다른 글

[Spring] SOLID 원칙  (0) 2023.04.07
[Spring] 스프링이 만들어진 이유  (0) 2023.04.06
[JavaSpring] JPA & SpringDataJpa  (0) 2023.04.04
[JavaSpring] Spring Integration Test & Spring JdbcTemplate  (0) 2023.04.03
[JavaSpring] Jdbc  (0) 2023.03.31