본문으로 건너뛰기
김신건의 로그

[Spring] SpEL: Spring Expression Language

· 수정 · 📖 약 1분 · 288자/단어 #spring #spel #expression
spring spel, expression language, @Value SpEL, spring expression, ExpressionParser

정의

**SpEL (Spring Expression Language)**은 Spring의 강력한 표현 언어. @Value, @PreAuthorize, @Cacheable 등 어노테이션 내에서 동적 표현 평가. property 조회, 메서드 호출, 산술, 컬렉션 projection 등.

기본 문법

#{expression}은 SpEL, ${property}는 properties placeholder.

@Value("#{systemProperties['user.home']}")
private String home;

@Value("#{T(java.lang.Math).PI}")
private double pi;

@Value("#{2 + 3}")
private int five;

@Value("#{'Hello'.length()}")
private int length;

@Value에서

Property 값

@Value("${app.api.key}")              // placeholder
@Value("${app.timeout:30}")            // 기본값
@Value("#{${app.api.urls}}")           // SpEL + property
@Value("#{'${app.tags}'.split(',')}")  // 쉼표 분리

Bean 메서드 호출

@Value("#{userService.findById(1)}")
private User user;

@Value("#{configService.timeout}")
private int timeout;

조건

@Value("#{${app.feature.enabled} ? 'premium' : 'basic'}")
private String tier;

컬렉션

@Value("#{userService.all.![email]}")   // map (각 user의 email)
private List<String> emails;

@Value("#{userService.all.?[active]}")  // select (active true만)
private List<User> activeUsers;

@Value("#{userService.all.^[score > 90]}")    // first match
@Value("#{userService.all.$[score > 90]}")    // last match

어디서 SpEL을 쓰나

@PreAuthorize / @PostAuthorize

@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAuthority('user:write')")
@PreAuthorize("#userId == authentication.principal.id")
@PreAuthorize("@securityService.canAccess(#postId, authentication)")
public void doSomething(Long userId, Long postId) { ... }

@PostAuthorize("returnObject.owner == authentication.name")
public Document load(Long id) { ... }

@Cacheable / @CacheEvict

@Cacheable(value = "users", key = "#id", condition = "#id > 0", unless = "#result == null")
public User findById(Long id) { ... }

@CacheEvict(value = "users", key = "#user.id")
public void update(User user) { ... }

@ConditionalOnExpression

@Bean
@ConditionalOnExpression("${app.feature.enabled} && '${app.env}' != 'test'")
public FeatureService featureService() { ... }

@Scheduled cron (간접)

@Scheduled(cron = "${app.cron.cleanup}")    // property
public void cleanup() { ... }

프로그래밍 사용

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.length()");
int length = (int) exp.getValue();    // 11

// 컨텍스트
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "Alice");
Expression exp2 = parser.parseExpression("'Hello, ' + #name");
String greeting = (String) exp2.getValue(context);    // "Hello, Alice"

// Root object
User user = new User("Alice", 30);
StandardEvaluationContext ctx = new StandardEvaluationContext(user);
Expression exp3 = parser.parseExpression("name.toUpperCase()");
String name = (String) exp3.getValue(ctx);    // "ALICE"

자주 사용 패턴

Type reference

T(java.lang.Math).PI                // Math.PI
T(java.lang.Math).max(1, 2)         // 2
T(java.lang.Integer).MAX_VALUE
T(java.time.LocalDate).now()

List / Map literal

{1, 2, 3, 4}                         // List
{name: 'Alice', age: 30}             // Map
new java.util.ArrayList()

Null-safe

user?.profile?.email                 // user나 profile이 null이면 null

Elvis operator

name ?: 'default'                    // name이 null이면 'default'

정규식

'abc' matches '[a-z]+'    // true

Method security 표현식

@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
@PreAuthorize("hasAuthority('SCOPE_read')")
@PreAuthorize("isAuthenticated()")
@PreAuthorize("isAnonymous()")
@PreAuthorize("permitAll()")
@PreAuthorize("denyAll()")

// 인자 참조
@PreAuthorize("#userId == authentication.principal.id")
public void update(Long userId) { ... }

// Bean 메서드
@PreAuthorize("@authz.canEdit(#postId)")
public void edit(Long postId) { ... }

// 복합
@PreAuthorize("hasRole('ADMIN') and #postId > 0")

// 반환값
@PostAuthorize("returnObject.author == authentication.name")
public Post get(Long id) { ... }

// 필터
@PostFilter("filterObject.public or filterObject.author == authentication.name")
public List<Post> all() { ... }

@PreFilter("filterObject.author == authentication.name")
public void delete(List<Post> posts) { ... }

컬렉션 표현식

// Map (projection): .![property]
users.![name]                 // List<String>의 이름들
users.![{name, email}]        // List<Map>

// Filter: .?[predicate]
users.?[age > 18]
users.?[active and admin]

// First / Last match
users.^[score > 90]
users.$[score > 90]

// Length
users.size()
users.?[active].size()

함정

1. SpEL 인젝션

@Value("#{${user_input}}")    // 위험: user_input이 시스템 명령 실행 가능

사용자 입력을 SpEL에 직접 넣지 말 것. 명시 placeholder만.

2. Bean 메서드 호출 비용

@Cacheable(key = "#user.expensiveCalc()")

매 호출마다 평가. 가능하면 simple 값.

3. null 처리

@PreAuthorize("#user.id == authentication.principal.id")
// user가 null이면 NullPointerException

#user?.id 또는 hasRole + #user 조합.

4. 표현식 컴파일

// 매번 파싱 (느림)
parser.parseExpression("..." + variable)

// 컴파일
SpelParserConfiguration config = new SpelParserConfiguration(
    SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader()
);
ExpressionParser parser = new SpelExpressionParser(config);

핫 코드면 IMMEDIATE 모드로 JIT 컴파일.

5. 디버깅

SpEL 에러는 SpelEvaluationException. 표현식이 길면 디버깅 어려움. 간단히 유지.

SpEL vs JSP EL

SpEL이 더 강력 (메서드 호출, 컬렉션 연산, type reference). JSP EL은 Servlet 컨테이너 표준.

활용 예제

동적 캐시 키

@Cacheable(value = "users", key = "#root.methodName + '_' + #id")
public User get(Long id) { ... }

메서드별 다른 조건

@Cacheable(value = "products", condition = "#product != null && #product.price > 100")
public Product enrich(Product product) { ... }

권한 위임

@PreAuthorize("@accessChecker.check(#root)")
public void doSomething(Long id, String action) { ... }

@Component("accessChecker")
public class AccessChecker {
    public boolean check(MethodSecurityExpressionRoot root) {
        // 복잡한 로직
        return true;
    }
}

조건부 Bean

@Bean
@ConditionalOnExpression("'${db.type}' == 'postgres'")
public DataSource postgresDataSource() { ... }

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기