[Spring AOP] 레거시 데이터와 하위 호환성을 유지하며 새로운 데이터를 추가해보자

Hocaron·2023년 11월 12일
0

Spring

목록 보기
35/44
post-custom-banner

레거시 프로젝트를 이관하며 레디스에 쌓고 있던 레거시 데이터를 새로운 키값과 자료구조를 가진 새로운 데이터 변경해야했다.
기존에 이미 적재된 레거시 데이터와 하위호환성을 유지하며 새로운 데이터를 추가해보자🫠

데이터의 흐름은 다음과 같아요

데이터 삭제 시에 아래와 같은 형태로 데이터 삭제가 필요하다.

  • 레디스에서 신규 키 삭제
  • 레디스에서 레거시 키 삭제

레디스의 다양한 자료구조가 궁금하다면 [Redis] 레디스의 수많은 자료구조는 어떤 상황에서 쓰일까을 참고하자.

데이터 조회 시에 아래와 같은 형태로 데이터 조회가 필요하다.

  • 레디스 신규 키로 먼저 조회하여 데이터가 있으면 결과 반환
  • 레디스 신규 키에 대한 데이터가 없으면 레디스 레거시 키에서 조회하여 결과 반환

신규 키에 대한 데이터가 없는 경우 레거시 키를 조회하는 이유는 뭘까?

신규 키에 대한 데이터가 없는 경우는 그 데이터가 정말 없는 경우도 있지만, 이관한 신규 서비스에서 추가를 하지 않아 기존 레디스 레거시 키에만 해당 데이터가 있는 경우일 수도 있다. 이 경우에는 레거시 키에는 데이터가 존재하는 상황이므로 해당 데이터를 반환해야 한다.

물론 기존 레디스 레거시 키에도 없다면 정말 없는 데이터이다😎

요구사항을 파악해보자

  1. 신규 키에 대한 삭제 시, 레거시 키에도 삭제가 필요하다.
  2. 신규 키에 대한 조회 시, 조회 결과에 따라 레거시 키에서도 조회해야 한다.
  3. 레거시 데이터에는 TTL 이 최대 30일로 걸려있기 때문에 신규 서비스 배포 후 30일이 후면 레거시 키에 처리를 해주는 코드는 필요가 없다.

즉, 레거시 키에 작업해주는 코드는 30일 이후면 사라질 코드이므로 기존 서비스 로직에 섞이지 않았으면 좋겠고, fade-out 처리할 때 클래스 혹은 메서드만 삭제하면 되도록 구현하고 싶다.

스프링 AOP 가 딱이군, AOP 에 대해 잠깐 알아보자

Advice 종류

Before Advice

@Aspect
public class BeforeExample {

	@Before(value = "execution(com.xyz.CommonPointcuts.dataAccessOperation())")
	public void doAccessCheck() {
		// ...
	}
}

dataAccessOperation 가 수행되기 전에 선행되어야 하는 로직을 작성하면 된다.
예를 들자면, 파라미터에 대한 검증 또는 로깅이나 인터셉터 등이 있다.

After Returning Advice

@Aspect
public class AfterReturningExample {

	@AfterReturning(
		value = "execution(com.xyz.CommonPointcuts.dataAccessOperation())",
		returning="retVal")
	public void doAccessCheck(Object retVal) {
		// ...
	}
}

dataAccessOperation 가 수행된 후, 특정 반환값이 반환되었을 때 수행되어야 하는 로직을 작성하면 된다.

After Throwing Advice

@Aspect
public class AfterThrowingExample {

	@AfterThrowing(
		value = "execution(com.xyz.CommonPointcuts.dataAccessOperation())",
        throwing="ex")
	public void doAccessCheck() {
		// ...
	}
}

dataAccessOperation 가 수행된 후, 특정 예외가 발생했을 때 수행되어야 하는 로직을 작성하면 된다.

After (Finally) Advice

@Aspect
public class AfterFinallyExample {

	@After(value = "execution(com.xyz.CommonPointcuts.dataAccessOperation())")
	public void doReleaseLock() {
		// ...
	}
}

dataAccessOperation 가 수행된 후, 수행되어야 하는 로직을 작성하면 된다.

Around Advice

@Aspect
public class AroundExample {

	@Around(value = "execution(com.xyz.CommonPointcuts.dataAccessOperation())")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// before 로직
		Integer retVal = pjp.proceed(); // dataAccessOperation 반환값
		// after 로직
        retVal++;						// 반환값에 대한 처리
		return retVal;
	}
}

dataAccessOperation 가 수행되기 전 후로 수행되어야 하는 로직을 추가하면 된다. Around 의 경우 다른 Advice 들과 달리 dataAccessOperation 반환값에 추가적인 처리를 해줄 수 있다.

하지만 스프링 공식 문서에서는 다른 advice 가 요구사항에 충족된다면 around 를 쓰지 말것을 권고하고 있다.
Always use the least powerful form of advice that meets your requirements.
For example, do not use around advice if before advice is sufficient for your needs.
출처

매개변수를 Advice에 전달하기

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
	// ...
}

accountDataAccessOperation에서 받는 매개변수를 Advice 내부에서도 사용이 가능하다.

아래 요구사항에는 어떤 Advice가 적절할까?

신규 키에 대한 읽음 처리 시에 레거시 키에도 읽음 처리

이 로직은 신규 키에 대한 삭제 후에 결과값에 상관없이 레거시 키에서도 삭제가 발생해야한다.
@After 가 좋겠군!

    @After(value =
        "execution(* ..Notification.read(String)) "
            +
            "&& args(field)", argNames = "field")
    public void interceptReadMethod(String field) {

        // 레거시키 데이터 삭제
        var key = writeKey();
        redisService.deleteKey(key);
    }

    private String writeKey(String field) {
        return LEGACY_KEY_PREFIX + field;
    }
}
  • public void interceptReadMethod(String field) 메서드
    • 이 메서드는 @After 어노테이션으로 지정된 Pointcut 표현식에 매칭되는 메서드이다.
    • 이 메서드는 하나의 String 매개변수인 field를 받는다.
    • 메서드 내부에서 writeKey 메서드를 호출하여 field 값을 기반으로 key를 생성하고, 그 key를 사용하여 redisService에서 특정 키를 삭제하는 작업을 수행한다.

신규 키에 대한 조회 시, 조회 결과에 따라 레거시 키에서 조회하여 반환결과를 반환

이 로직은 신규 키에 대한 조회 후에 결과값이 없는 경우 레거시 키에서도 조회가 발생해야한다. 그리고 원래 호출한 메서드에서 레거시 키에서 조회한 결과에 대한 반환값을 내려줘야 한다.
@Around 가 좋겠군!

    @Around(value = "execution(* ..Notification.exist(String)) "
            +
            "&& args(field)", argNames = "joinPoint,field")
    public boolean interceptExistMethod(ProceedingJoinPoint joinPoint, String field) throws Throwable {

        // 신규키로 데이터 유무 조회
        var existKey = (boolean) joinPoint.proceed(new Object[]{field});

        if (existKey) {
            return true;
        }

        // 신규키 데이터가 없는 경우
        var key = writeKey(field);
        return Optional.ofNullable(redisService.exist(key))
                .orElse(false);
    }
  • public boolean interceptExistMethod(ProceedingJoinPoint joinPoint, String field) 메서드
    • 이 메서드는 @Around 어노테이션으로 지정된 Pointcut 표현식에 매칭되는 메서드이다.
    • 이 메서드는 두 개의 매개변수를 받는다.
      • joinPoint: ProceedingJoinPoint 타입의 매개변수로, 감싸고 있는 메서드에 대한 정보를 포함하고 있다.
      • field: String 타입의 매개변수로, Notification.exist 메서드에 전달될 값이다.
    • joinPoint.proceed(new Object[]{field})는 감싸고 있는 메서드(Notification.exist)를 호출하고 그 결과를 반환하다.
    • 호출된 메서드의 결과값이 true이면 바로 반환하고, false인 경우 writeKey 메서드를 사용하여 새로운 키를 생성하고 Redis에서 해당 키가 존재하는지 확인하여 결과를 반환한다.

코드 한눈에 보기

레거시 코드에 대한 처리가 필요없어지는 시점에 UserScopeLegacyAspect 클래스만 삭제하면 된다✨

@Aspect
@Component
public record UserScopeLegacyAspect(RedisService redisService) {

    private static final String LEGACY_KEY_PREFIX = "legacy";

    @Around(value = "execution(* ..Notification.exist(String)) "
            +
            "&& args(field)", argNames = "joinPoint,field")
    public boolean interceptExistMethod(ProceedingJoinPoint joinPoint, String field) throws Throwable {

        // 신규키로 데이터 유무 조회하는 메서드 호출
        var existKey = (boolean) joinPoint.proceed(new Object[]{field});

  		// 신규키 데이터가 있는 경우
        if (existKey) {
            return true;
        }

        // 신규키 데이터가 없는 경우
        var key = writeKey(field);
        return Optional.ofNullable(redisService.exist(key))
                .orElse(false);
    }

    @After(value =
        "execution(* ..Notification.read(String)) "
            +
            "&& args(field)", argNames = "field")
    public void interceptReadMethod(String field) {

        // 레거시키 데이터 삭제
        var key = writeKey();
        redisService.deleteKey(key);
    }

    private String writeKey(String field) {
        return LEGACY_KEY_PREFIX + field;
    }
}

정리

  • AOP 를 사용하면 기존 서비스 로직을 건드리지 않으면서 필요한 작업을 할 수 있다.
  • AOP 의 종류는 다양하므로 적절히 사용하자.

References

profile
기록을 통한 성장을
post-custom-banner

0개의 댓글