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;
}
}
'Framework > Spring' 카테고리의 다른 글
[Spring] 애플리케이션과 NCP Object Storage 연결하기 (0) | 2024.03.25 |
---|---|
[Spring] 스프링의 캐시 추상화 (0) | 2024.03.17 |
[Spring] 애플리케이션의 초기 응답 속도 개선하기 (0) | 2024.03.07 |
[Spring] Redisson으로 동시성 문제 해결하기 (2) | 2024.02.28 |
[Spring] 스프링의 계층형 아키텍처 (0) | 2024.02.19 |