brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Jan 31. 2021

스프링부트 백엔드 프로그래밍 (7)

7. AOP 

"스프링부트 백엔드 프로그래밍"이라는 주제로 약 8주간 글을 작성할 예정입니다. 4주차를 진행 중이며 w전체 일정의 반은 소화하였네요. 제 스터디가 잘못된 방향으로 가지 않도록, 의견 및 조언을 아낌없이 해주시길 부탁드립니다. 


지난주에는 자바 예외 처리 및 테스트 코드에 대해서 공부하였고, 캐싱 구현 과제가 있었습니다. 이번 주에는 AOP 에 대해서 공부한 후, 스프링 캐시 추상화에 대해서 공부합니다. 


어떻게 설명하면 주니어 개발자들이 쉽게 이해할 수 있을지 많은 고민을 했습니다만, 누군가에게 지식을 전달하는 것은 너무 어려운 일입니다. 처음 시작은 가벼운 마음으로 시작하였습니다만, 글을 쓰고 다시 읽어보니... 너무 이해하기 어려운 내용인 것 같습니다. 


튜터링 방법에 대해서 좀 더 고민이 필요할 것 같네요...ㅠㅠ 



1주 차 - 스프링부트란 무엇인가?, 간단한 API 서버 만들어보기

       1. 스프링부트란 무엇인가?

       2. 간단한 API 서버 만들어보기 (커리큘럼 소개)

2주 차 - 스프링 프레임워크 기본 개념 이해하기

       3. 스프링 프레임워크 IoC, DI(의존성주입)

       4. 스프링부트 AutoConfiguration

3주 차 - Rest API, 테스트 코드 작성하기, 예외 처리하기

       5. [이전글] 예외 처리 및 테스트 코드 작성하기

       [미정, 나중에 시간되면 작성] 6. Rest API (HTTP 기본 개념)    

4주 차 - AOP, 스프링부트 캐시 추상화, Redis 연동하기

       7. [지금글] AOP

       8. [다음글] 스프링 캐시 추상화, Redis 연동하기



[미정]5주 차 - EventListener, MQ, Pub/Sub

      10. EventListener, MQ, Pub/Sub 기본 개념

      11. RabbiMQ, KAFKA 사용해보기

6주차 - 보안(인증)

[미정] 7주 차 - 병렬, 비동기 프로그래밍

[미정] 8주차 - Spring Cloud

[미정] JPA, Spring Data, Spring Session 등



7. AOP


7장에서는, AOP에 대해서 상세하게 설명을 할 예정이었습니다만... 제가 AOP를 제대로 설명할 자신이 없네요.


7.1 AOP 란?

스프링 레퍼런스에서 참고하였습니다. 


Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns (such as transaction management) that cut across multiple types and objects. (Such concerns are often termed “crosscutting” concerns in AOP literature.)


자세한 설명은 생략합니다. 링크를 참고하세요.

https://jojoldu.tistory.com/71

https://www.baeldung.com/spring-aop

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop



7.2 AOP, 그림으로 이해하기

시간 관계상 생략...


7.3 AOP, 코드로 이해하기

스터디에서 과제를 수행하면서, 오픈 API를 연동한 검색 서비스를 구현하고 있습니다. 검색 요청 시 외부 API 서버를 호출해야 하기 때문에, 전체 응답이 빠르지는 않습니다. 그래서, 우리는 비즈니스 로직의 수행 시간을 로그에 남기고 싶습니다. 아래와 같이 3초의 지연시간이 발생하는 메서드가 있다고 가정해봅니다. 

메서드 내부의 수행 시간을 확인하기 위해서는, 아래 샘플 코드처럼 비즈니스 로직이 수행하기 직전, 직후 의 시간 차이를 계산해서 수행 시간을 로그에 남길 수 있습니다.

하지만, 이 방법에는 치명적인 단점이 있습니다. 수행 시간 확인을 하고 싶은 메서드가 있다면,  해당 모든 메서드에 시간을 계산하는 로직이 추가되어야 합니다.


모든 메서드에 수행 시간 계산하는 코드를 추가해야 한다...???... 

좋은 방법이 아닌 것 같습니다. 


스프링 AOP를 구현해보겠습니다. 먼저 의존성을 추가합니다.

@PerformanceTimeRecord라는 어노테이션을 정의해줍니다.

메서드 상단에 어노테이션을 선언해줍니다.

가장 중요한 코딩은, 실제로 수행 시간을 로그에 남길 수 있도록 AOP 구문을 작성해야 합니다. 가장 중요한 구문은 바로 joinPoint.proceed()입니다. 실제 메서드의 내부 로직을 수행합니다. 그리고, proceed() 결과를 반드시 반환해줘야 합니다. return proceed;

메서드를 실행하면, 아래와 같이 로그가 기록됩니다. 

이 글에서는, AOP에 대해서 아주 극히 일부에 대해서만 설명하였습니다. 각자 공부하시길 바랄게요. 

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-introduction-defn


7.4 AOP, 실제 구현해보기

샘플 코드는 아래 링크를 확인해주세요. 이번 샘플 코드는 자세히 안 보셔도 됩니다. 

https://github.com/sieunkr/spring-study-group/tree/master/4-0


지난주 과제는 영화 검색 서비스에 캐싱 기능을 구현하는 조금 어려운 과제였습니다. 해당 과제에 대해서 대부분의 주니어 개발자들이 영화 검색 메서드 내부에 캐싱 기능을 적용하였습니다. 


또는, 응용 서비스 결과를 캐싱 처리하고 싶다면, 서비스를 호출하는 컨트롤러 레이어에서 아래와 같이 구현하면 될 것입니다.

Controller, Service 등 각자 코딩 스타일에 따라서 선택해서 캐싱 로직을 잘 구현하였습니다. 하지만, 영화 외에 쇼핑, 블로그, 뉴스 등 모든 기능에 캐싱 기능을 적용하고 싶다면 어떻게 하면 될까요? 모든 검색 서비스에 캐싱 기능을 전부 적용해줘야 할까요?



AOP 개념을 적용해서, 캐싱 기능을 구현해보겠습니다. 지금부터의 코드는... 이해가 되지 않는다면, 따라 하지 않으셔도 됩니다. 사실, 스프링에서는 이미 캐시 추상화를 제공하고 있고, 다음 시간에 자세히 설명할 예정입니다. 해당 코드는 AOP, 캐싱을 주니어 개발자에게 이해시키기 위해서 임시로 작성한 코드입니다. 자세한 코드가 궁금하시다면 제 github을 확인하시면 됩니다만, 굳이 안 봐도 됩니다. 스프링의 캐시 추상화를 사용하시면 됩니다.



7.4.1 캐시 데이터를 저장하는 자료구조 정의

CurrentMapCustomCache라는 캐시 저장소 역할을 하는 클래스를 정의합니다. 실제 캐시 저장소인 store 필드는 ConcurrentMap의 구현체인 ConcurrentHashMap을 사용합니다. 


1. 캐시 데이터를 저장하는 자료구조는 ConcurrentMap의 구현체입니다. 필자의 샘플 코드에서는 ConcurrentHashMap을 사용하였습니다. key 에는 캐시의 키가 저장되며, value 에는 캐시 데이터가 저장됩니다. 

2. 캐시 데이터를 Key(키)를 사용해서 찾습니다. 없으면 Optional.empty를 반한 합니다.

3. 신규 캐시 데이터를 저장합니다. 


캐시 데이터는 아래와 같이 저장됩니다. 


예)

key : 반지의 제왕, value: 에는 ArrayList <Movie> 데이터가 저장되어있습니다.


위와 같은 캐시 데이터를 용도에 따라서 여러 개의 캐시를 생성 및 관리할 수 있어야 합니다. 전체 캐시 데이터를 관리하는 역할의 CacheManager를 정의합니다. 

필자의 샘플 코드에서의, CacheManager는 캐시를 찾아서 반환해주는 역할 정도만 수행합니다. (예를 들어서, 영화 검색에 대한 캐시를 찾아서 반환해줍니다.) 자세한 구현 소스는 생략합니다. 

CacheManager에서는 전체 캐시 데이터를 cacheMap 필드에 저장해서 관리합니다. 아래와 같이 모든 캐시 데이터를 저장하고 있습니다.


7.4.2 AOP 캐시 추상화 직접 구현해보기

스프링에서는 @Cacheable이라는 캐시 추상화를 이미 제공합니다. 이 글은, 스터디를 위한 샘플 코드이며, 실무에서 필자의 샘플 코드처럼 사용하지 않습니다. 


어노테이션을 선언해줍니다. 


캐싱 기능을 적용할 메서드에 어노테이션을 선언해줍니다. 


- 영화 검색 시 캐싱 기능이 합니다.

- 추천 영화 데이터 제공 시에도 캐싱 기능을 적용합니다.

- 영화 검색 이외에 어떤 기능에도 편하게 어노테이션만 선언해주면 됩니다.


@LookAsideCaching 어노테이션이 붙은 메서드에는, 캐싱 기능이 동작할 것입니다. key에 해당하는 캐시 데이터가 이미 존재한다면, 메서드 내부 로직을 수행하지 않습니다. 왜냐면, 캐시 데이터가 이미 존재하고 있어서, 해당 데이터를 그대로 사용하면 되기 때문입니다. 캐시 데이터가 있는데 굳이 비즈니스 로직을 수행할 필요가 없습니다. 반면에, 캐시 데이터가 없는 경우에는, 캐시 데이터가 없기 때문에 당연히 메서드 내부 로직이 수행되어야 합니다. 즉, 신규 데이터를 생성해서 사용한 후, 나중에 또 사용할 수 있도록 캐시 데이터를 저장해놔야 합니다.


AOP 코드를 작성해보겠습니다.

CachingAspectProvider이라는 클래스에 구현합니다.


코드는, 중요하지 않습니다. 개념만 이해하세요.

1) 캐시 데이터가 있다면, 존재하는 캐시 데이터를 반환합니다. 즉, @LookAsideCaching 어노테이션이 선언된 메서드 내부를 수행하지 않습니다. 

2) 캐시 데이터가 없다면, joinPoint.proceed()를 실행해서, 메서드 내부를 실행합니다. Repository를 사용해서 오픈 API 데이터를 조회합니다. 그리고, 조회한 데이터를 putInCache() 메서드를 호출해서 캐시 저장소에 저장합니다. 




코드와 글로 설명하기에는 너무 지루한 내용이네요. 그림으로 설명해보겠습니다.

캐시 데이터가 존재한다면, joinPoint.proceed()가 실행하지 않습니다. 즉, search 메서드의 내부가 실행이 되지 않습니다. 

반면에, 캐시 데이터가 존재하지 않는다면 어떻게 될까요? 아래 그림과 같이, 메서드 내부 비즈니스 로직이 실행됩니다. 



휴... 자세한 내용은, 스터디 시간에 말로 설명하겠습니다......



7.5 샘플 코드에서 설명하고 싶었던 개념

7.5.1 AOP

관점지향 프로그래밍이라고 부르는 AOP는, 기존의 객체지향 프로그래밍을 보완하기 위해서 사용합니다. 우리가 스터디 중에 진행하는 과제의 핵심 기능은 무엇인가요? 바로 오픈 API를 연동해서 검색 데이터를 제공하는 서비스입니다. 하지만, 캐싱, 로그, 보안, 트랜잭션 같은 기능은 핵심 기능인 검색 서비스와는 개별적인 횡단 관심의 내용들입니다. 이런, 횡단 관심의 기능들은 AOP 개념을 사용해서 적용하면, 중복되는 코드를 효율적으로 관리할 수 있고, 생산성을 높일 수 있습니다. 


역시, 자세한 설명은... 스터디 시간에 하기로...


7.5.2 Cache Pattern

캐싱 전략은 딱히 정답이 없다고 생각합니다. 회사마다 다르고, 상황에 따라서 적절하게 구축해야 합니다. 이것이 항상 정답이다!라는 것은 없습니다. 첫 번째 전략으로는 Inline Cache pattern이라는 전략이 있습니다. 데이터를 캐시 스토리지에 미리 저장하고 필요할 때마다 조회해서 사용하는 방식입니다. 

위와 같이 모든 데이터를 캐시에 저장하는 것이 비효율적인 상황이라면, 대안으로는 Cache Aside Pattern을 고려해보는 것도 좋습니다. 

처음 사용자가 요청했을 때는 캐시 스토리지에는 아무 데이터도 없는 상황입니다.

1. 애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다. 하지만 데이터가 없다.

2. 애플리케이션은 Contents DB에서 데이터를 조회하고 사용자에게 제공한다. 

3. 애플리케이션은 Contents DB에서 가져왔던 데이터를 캐시 저장소에 저장한다. 


다음 사용자가 요청했을 때는 이미 캐시 저장소에 데이터가 있는 상황입니다.

1. 애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다. 캐시 저장소에 저장되어있는 데이터를 제공한다. 


이 글의 샘플 코드는, Cache Aside Pattern에 해당합니다. Aside Pattern에 대해서는, 아래 링크를 참고하길 바랍니다. 

https://docs.microsoft.com/ko-kr/azure/architecture/patterns/cache-aside


7.5.3 PSA (Portable Service Abstraction)


샘플 코드에서는 CustomCacheManager라는 인터페이스를 정의하였고, 해당 구현체인 memoryCustomCacheManager를 사용해서, 메모리 기반의 캐싱을 구현하였습니다. 

만약에, 저장소를 메모리가 아니라... Redis에 저장하도록 변경하고 싶다면 어떻게 하면 될까요? CustomCacheManager의 구현체인 RedisCustomCacheManager를 정의해줍니다. 

그리고, CustomCacheManager를 의존하는 곳에서, 사용하고 싶은 구현체만 변경해주면 됩니다. 

하아... 글로 설명하기 어려우니, 그림으로 설명하겠습니다. 



AspectProvider에서는 CustomCacheManager 인터페이스를 주입받습니다. 인터페이스로 정의하였기 대문에, 실제 구현체에 대한 주입의 변경은 매우 심플하게 구현할 수 있습니다. 이 경우에, 메모리 캐싱에서 레디스 캐싱으로 변경하는 경우, AspectProvider 에는 크게 영향을 받지 않습니다. 


또한, 영화 검색 서비스에 @LookAsideCaching이라는 어노테이션을 선언함으로써 캐싱이 동작하도록 구현하였는데요. 응용 서비스 입장에서는, 캐싱이 실제로 어떻게 동작하는지 전혀 관여하지 않습니다. 어떤 구현체를 사용했는지, 레디스를 사용했는지, 메모리를 사용했는지.. 전혀 알지 못합니다. 

지난주 과제를 보니, 대부분의 주니어 개발자분들께서는, 영화 서비스에 대해서만 캐싱 로직을 구현하였고, ConcurrentMap에 의존하게 과제를 작성하셨습니다. 만약에, ConcurrentMap 기반의 캐싱을, 레디스를 연동하는 방식으로 변경하게 되는 경우에, 영화 검색 서비스 소스 코드에 얼마나 영향을 끼치나요? 영향을 많이 끼칠수록, 특정 구현체에 강하게 의존하고 있다는 증거입니다.. 구현체에 강하게 의존하지 않고, 느슨한 연결을 할 수 있도록 애플리케이션을 개선해보면 좋을 것 같습니다. 


7.6 마무리

7장에서의 샘플 소스는, AOP 개념을 사용해서, 메모리 기반의 캐싱을 구현하였습니다. 사실, 스프링에서는 캐시 추상화라는 좋은 기능을 제공합니다. 아래와 같이, @Cacheable라는 어노테이션을 스프링 프레임워크에서 기본으로 제공합니다. 아래 샘플 코드와 같이 메서드 상단에 선언해주면, 캐싱이 동작할 것입니다.

스프링의 캐시 추상화를 사용하고 싶다면 몇 가지 설정이 더 필요합니다. 자세한 내용은, 8장에서 알아보겠습니다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari