본문으로 바로가기

[SPRING] AOP 정리 및 실습 (4)

category SPRING/개발 TIP 2021. 4. 14. 18:57

AOP를 공부하기 위해 이동욱님 블로그를 참고하여 실습 하였습니다.

 

작업중인 AOP 깃주소 : github.com/prodo-developer/aop-basic

 

사용법 확장

먼저 아직 AOP가 적용되지 않은 UserService를 진행해보겠습니다.

Perpormance.java (OR 조건)

@Around("execution(* com.blogcode.board.BoardService.getBoards(..)) 
|| execution(* com.blogcode.user.UserService.getUsers(..))")

포인트컷 표현식에 OR 연산자인 ||를 이용하여 UserService를 추가시켰습니다.
이로 인해 알 수 있는 것은 포인트컷 표현식에는 AND, OR, NOT와 같은 관계연산자를 이용할 수 있다는 것입니다.
여기서 만약 표현식이 더 추가가 되면 어떻게 될까요?


저 긴 표현식이 하나씩 추가 될때마다 가독성에 큰 무리가 있습니다.
더불어서 해당 표현식을 재사용하고 싶을땐 어떻게 해야할까요? 변수처럼 표현식을 담을수는 없을까요?
이제 저 표현식을 변수처럼 변경해보겠습니다.

@Aspect
public class Performance {

    @Pointcut("execution(* com.blogcode.board.BoardService.getBoards(..))")
    public void getBoards(){}

    @Pointcut("execution(* com.blogcode.user.UserService.getUsers(..))")
    public void getUsers(){}

    @Around("getBoards() || getUsers()")
    public Object calculatePerformanceTime(ProceedingJoinPoint proceedingJoinPoint) {
        Object result = null;
        try {
            long start = System.currentTimeMillis();
            result = proceedingJoinPoint.proceed();
            long end = System.currentTimeMillis();

            System.out.println("수행 시간 : "+ (end - start));
        } catch (Throwable throwable) {
            System.out.println("exception! ");
        }
        return result;
    }
}

@Pointcut 어노테이션은 애스펙트에서 마치 변수와 같이 재사용 가능한 포인트컷을 정의할 수 있습니다. 그래서 이를 이용하여 각각의 표현식을 getBoards() 메소드와 getUsers() 메소드에 담았습니다.
이렇게 될 경우 다음부터는 동일한 표현식은 미리 지정된 메소드명으로 표현식을 그대로 사용할 수 있는 것입니다.

그럼 좀 더 추가예제를 진행해보겠습니다.
여태 한 것은 파라미터가 없는 애스펙트만 다루었습니다.
만약 타겟 메소드를 어드바이스 하는 애스펙트가 타겟 메소드에 전달된 인자(Arguments)값을 사용하고 싶을땐 어떻게 해야할까요?
이번엔 타겟 메소드의 인자를 사용하는 애스펙트를 만들어 보겠습니다.
신규 기능은 User의 정보가 수정되면 이를 History에서 저장하여 관리하는 수정내역관리 입니다. userService의 update 메소드가 실행될때마다 History Table에 user의 idx가 저장되도록 코드를 추가하겠습니다.

 

History.java

@Entity
public class History {

    @Id
    @GeneratedValue
    private long idx;

    @Column
    private long userIdx;

    @Column
    private Date updateDate;

    public History() {
    }

    public History(long userIdx, Date updateDate) {
        this.userIdx = userIdx;
        this.updateDate = updateDate;
    }

    public long getIdx() {
        return idx;
    }

    public void setIdx(long idx) {
        this.idx = idx;
    }

    public long getUserIdx() {
        return userIdx;
    }

    public void setUserIdx(long userIdx) {
        this.userIdx = userIdx;
    }

    public Date getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }
}

HistoryRepository.java

@Repository
public interface HistoryRepository extends JpaRepository<History, Long>{}

 

UserHistory.java

@Aspect
@Component // @Bean과 동일하게 Spring Bean 등록 어노테이션
public class UserHistory {

    @Autowired
    private HistoryRepository historyRepository;

    @Pointcut("execution(* com.blogcode.user.UserService.update(*)) && args(user)")
    public void updateUser(User user){}

    @AfterReturning("updateUser(user)")
    public void saveHistory(User user){
        historyRepository.save(new History(user.getIdx()));
    }
}

가장 중요한 UserHistory 애스펙트입니다.
이전의 Performance 애스펙트와 다르게 bean등록을 @Bean을 사용하지 않고 @Component를 사용하여 등록하였습니다.

 

그래도 굳이 @Bean으로 등록해서 사용하고 싶다면 Application.java에 @Configuration을 선언하면 됩니다.

@SpringBootApplication
@RestController
@EnableAspectJAutoProxy //오토 프록싱
@Configuration
public class Application implements CommandLineRunner{
    ...
    
    
    @Bean
    public Performance performance() {
    	return new Performance();
    }
    
    @Bean
    public UserHistory userHistory() {
        return new UserHistory();
    }
}

 

(@Bean과 @Component의 차이는 이전의 포스팅을 참고하시면 될것 같습니다.)
애스펙트 역시 Spring이 관리하는 bean이기 때문에 같은 bean인 HistoryRepository를 사용할 수 있습니다.


여기 코드에서 중점으로 보셔야할 것은 args(user) 표현식입니다.
해당 표현식을 통해 타겟 메소드의 인자와 어드바이스의 인자가 매칭이 되는 것입니다.
(저 코드를 제거해보시면 무슨말인지 바로 이해하실 수 있으실 것입니다.)
updateUser라는 포인트컷이 user라는 인자를 사용하도록 args(user) 표현식으로 지정한 것입니다.

@AfterReturning를 통해 정상적으로 타겟 메소드가 실행 후에 DB에 저장되도록 구현을 완성하였습니다.
자 그럼 마지막으로 UserService와 UserServiceImpl 코드를 수정하도록 하겠습니다.

 

UserService.java와 UserServiceImpl.java

public interface UserService {
    List<User> getUsers();

    void update(User user) throws Exception;
}

@Service
public class UserServiceImpl implements UserService{

    @Autowired
    private UserRepository repository;

    @Override
    public List<User> getUsers() {
        return repository.findAll();
    }

    @Override
    public void update(User user) throws Exception{
        repository.save(user);
    }
}

최종적으로 프로젝트 구조는 아래와 같습니다.

 

(최종 프로젝트 구조)

그럼 위 코드가 정상적으로 작동하는지 확인 하기 위해 테스트 코드를 작성하겠습니다.

 

ApplicationTests.java

RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private UserService userService;

    @Autowired
    private HistoryRepository historyRepository;

    @Test
    public void updateUsers() throws Exception {
        List<User> users = userService.getUsers();
        for(int i=0;i<5;i++){
            User user = users.get(i);
            user.setEmail("jojoldu@gmail.com");
            userService.update(user);
        }

        List<History> histories = historyRepository.findAll();
        assertThat(histories.size()).isEqualTo(5);
        assertThat(histories.get(0).getUserIdx()).isEqualTo(1L);
        assertThat(histories.get(1).getUserIdx()).isEqualTo(2L);
    }
}

 

1~5까지의 user들의 메일을 수정하였습니다.
그리고 수정 후, History 테이블에 정상적으로 데이터가 들어간 것인지 확인하도록 코드가 작성되었습니다.
그럼 테스트를 돌려보겠습니다!

 

(테스트 결과)

 

 

참조사이트 : jojoldu.tistory.com/72?category=635883