이끌든지 따르든지 비키든지

Framework/Spring

[Spring] SpEL으로 더 강력하게 표현식 작성하기

SeongHo5 2024. 3. 13. 19:30

SpEL이란?


 

Spring Expression Language(SpEL)은, 객체 그래프를 조회하고 조작하는 데 사용되는 표현식 언어입니다.

SpEL을 사용해 런타임에 객체의 속성에 접근하거나, 메서드를 호출하거나, 배열, 리스트 및 맵과 같은 컬렉션에 대한 조작을 수행할 수 있습니다. 또한, 논리적 및 산술 연산을 수행하는 데도 사용할 수 있습니다.

 

 

SpEL의 기본 문법


 

1. 리터럴 표현식 - 문자열, 숫자, boolean 등

 

ExpressionParser expressionParser = new SpelExpressionParser();
String helloWorld = (String) expressionParser.parseExpression("'Hello World'").getValue();
int number = (Integer) expressionParser.parseExpression("10").getValue();
boolean trueValue = (Boolean) expressionParser.parseExpression("true").getValue();

 

 

2. 변수 - 변수로 표현식 내에서 참조할 수 있는 값들을 정의

 

EvaluationContext context = new StandardEvaluationContext();
context.setVariable("greeting", "Hello World");
String value = (String) expressionParser.parseExpression("#greeting").getValue(context);

 

 

3. 프로퍼티 - 객체의 프로퍼티에 접근하려는 경우

 

// 'name'이라는 프로퍼티를 가진, 'user' 객체가 있다면
String userName = (String) expressionParser.parseExpression("user.name").getValue(context);

 

 

4. 메서드 호출

 

// 'user'객체의 'getName()' 메서드 호출
String userName = (String) expressionParser.parseExpression("user.getName()").getValue(context);

 

 

5. 연산자 - 산술·논리 연산 등

 

// 산술 연산
int two = (Integer) expressionParser.parseExpression("1 + 1").getValue();

// 논리 연산
boolean trueValue = (Boolean) expressionParser.parseExpression("1 < 2 and 4 > 3").getValue();

// 문자열 연결
String testString = (String) expressionParser.parseExpression("'Hello ' + 'World'").getValue();

 

 

그 외에도, 정규 표현식, 컬렉션 접근에도 SpEL을 사용할 수 있습니다.

 

 

활용할 수 있는 어노테이션


 

@Value

 

필드, 메서드 매개변수, 또는 생성자 매개변수에 적용할 수 있으며, SpEL 표현식을 사용하여 속성 값을 주입할 때 사용합니다. 예를 들어, 환경변수에서 값을 가져오거나, 다른 빈의 속성을 참조할 수 있습니다.

 

가장 익숙한 사용 예시는 application.yml(또는 .properties)에 정의된 프로퍼티 값을 가져올 때입니다.

 

# application.properties
aws.s3.region=#프로퍼티값
aws.s3.endpoint=#프로퍼티값
aws.s3.accessKey=#프로퍼티값
aws.s3.secretKey=#프로퍼티값

 

 

위 프로퍼티 값을 다음과 같이 클래스의 필드에 주입하는데 사용할 수 있습니다.

 

@Configuration
public class S3Config {

    @Value("${aws.s3.region}")
    private String region;

    @Value("${aws.s3.endpoint}")
    private String endPoint;

    @Value("${aws.s3.accessKey}")
    private String accessKey;

    @Value("${aws.s3.secretKey}")
    private String secretKey;

 

 

@Cacheable, @CachePut, @CacheEvict

 

SpEL 표현식을 사용하여 캐시 이름, 캐시 키, 캐시 조건 등을 동적으로 지정할 수 있습니다. 이를 통해 메서드의 실행 결과를 캐싱하거나 캐시에서 제거하는 등의 작업을 조건부로 수행할 수 있습니다.

 

 

@PreAuthorize, @PostAuthorize

 

메서드 실행 전후에 보안 표현식을 평가하는 데 사용됩니다. 

SpEL을 사용하여, 메서드가 실행되기 전이나 실행된 후의 보안 조건을 지정할 수 있습니다.

 

 

@RestController
public class PostController {

    @PutMapping("/posts/{postId}")
    @PreAuthorize("hasRole('ADMIN')") // 관리자 역할을 가진 사용자만 요청 가능
    public String deletePost(@PathVariable String postId) {
        // 게시글 삭제 로직
        return "Post deleted";
    }
    
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public void adminOrOwner(Long userId) {
        // 관리자 또는 소유자만 수행할 수 있는 작업
    }
}

 

 

위의 예시 외에도, @Conditional, @PreFilter, @PostFilter 등 어노테이션에도 활용할 수 있습니다.

 

 

커스텀 어노테이션에 SpEL을 활용하려면?


 

 

위의 어노테이션 외에도, 커스텀 어노테이션이나 다른 경우에도 SpEL을 활용하고 싶다면, SpELExpressionParser와 EvaluationContext를 사용하여 SpEL 표현식을 평가하는 CustomSpelParser를 구현해야 합니다.

 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CustomSpelParser {

    public static Object getDynamicValue(String[] parameterNames, Object[] args, String name) {
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext();

        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }

        return parser.parseExpression(name).getValue(context);
    }

}

 

 

적용 예시

 

분산 락의 동적 Key 생성에 CustomSpelParser을 활용해, 커스텀 어노테이션에도 SpEL을 활용해 동적으로 값을 할당할 수 있습니다.

 

@Slf4j(topic = "distributedLockAspect")
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {

    private static final String REDISSON_KEY_PREFIX = "LOCK::";

    private final RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object aroundLockPointcut(
            ProceedingJoinPoint joinPoint,
            DistributedLock distributedLock
    ) throws Throwable {
        String key = getKeyFromMethodSignature(joinPoint, distributedLock);
        RLock lock = redissonClient.getLock(key);
        try {
            log.info("Acquiring Lock: {}", key);
            if (!acquireLock(lock, distributedLock)) {
                return false;
            }
            return joinPoint.proceed();
        } catch (InterruptedException e) {
            throw new InterruptedException();
        } finally {
            releaseLock(lock);
        }
    }

    private String getKeyFromMethodSignature(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        String key = CustomSpelParser.getDynamicValue(
                ((MethodSignature) joinPoint.getSignature()).getParameterNames(),
                joinPoint.getArgs(),
                distributedLock.keyName()
        ).toString();
        return REDISSON_KEY_PREFIX + key;
    }
}