SpEL(Spring Expression Language)
SpEL์ Spring Framework 3.0๋ถํฐ ์ง์ํ๋ ๊ฐ๋ ฅํ ํํ ์ธ์ด๋ค. ๊ฐ์ฒด ๊ทธ๋ํ๋ฅผ ๋ฐํ์์ ์กฐํํ๊ณ ์กฐ์ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
๊ตฌ๋ฌธ
Spring์์๋ ${...}
์ #{...}
๋ ๊ฐ์ง ํํ์ ํํ์์ ์ฌ์ฉํ๋ค.
${...}
(Property Placeholder)์ฉ๋: ํ๋กํผํฐ ํ๋ ์ด์คํ๋๋ก,
application.yml
๊ฐ์ ํ์ผ์ด๋ ํ๊ฒฝ ๋ณ์ ๋ฑ ์ธ๋ถ ์ค์ ํ์ผ์ ํค(key)์ ํด๋นํ๋ ๊ฐ์ ๊ฐ์ ธ์ค๊ธฐ ์ํด ์ฌ์ฉ๋์: ๋จ์ํ ๊ฐ ์นํ(replacement)์ผ๋ก ๋์
์ฒ๋ฆฌ ์์ : Spring ์ปจํ ์ด๋๊ฐ Bean์ ์์ฑํ๊ธฐ ์ , Bean ์ ์๋ฅผ ์ฝ์ด๋ค์ด๋ ์์ ์ ๋จผ์ ์ฒ๋ฆฌ๋์ด ๊ฐ์ผ๋ก ๋์ฒด
#{...}
(SpEL Expression)์ฉ๋: SpEL ํํ์์ผ๋ก, ๋จ์ํ ๊ฐ ์ฃผ์ ์ ๋์ด ๊ฐ์ฒด์ ๋ฉ์๋ ํธ์ถ, ์ฐ์ฐ, ์กฐ๊ฑด๋ถ ํ๊ฐ, ๋ค๋ฅธ Bean ์ฐธ์กฐ ๋ฑ ๋์ ์ธ ๋ก์ง์ ์ํํ ๋ ์ฌ์ฉ
๋์: ํํ์์ ํ์ฑํ๊ณ ํ๊ฐ(evaluation)ํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ๋์ ์ผ๋ก ์์ฑ
์ฒ๋ฆฌ ์์ : Bean์ด ์์ฑ๋ ์ดํ, ๋ฐํ์์ ํํ์์ด ํ๊ฐ
๋ชฉ์
์ธ๋ถ ์ค์ ๊ฐ์ ์ ์ ์ธ ์ฃผ์
๋์ ์ธ ํํ์ ํ๊ฐ ๋ฐ ๋ก์ง ์ํ
์ฒ๋ฆฌ ์์
Bean ์์ฑ ์ (์ค์ ๋ฉํ๋ฐ์ดํฐ ๋ก๋ฉ ์)
Bean ์์ฑ ํ (๋ฐํ์)
๊ธฐ๋ฅ
๋จ์ ๊ฐ ์นํ
๋ฉ์๋ ํธ์ถ, ์ฐ์ /๋ ผ๋ฆฌ ์ฐ์ฐ, Bean ์ฐธ์กฐ ๋ฑ
์ด ๋์ @Value("#{'${app.name}' + ' - ' + T(java.time.LocalDate).now()}")
์ ๊ฐ์ด ํจ๊ป ์ฌ์ฉํ ์๋ ์๋ค.
1. ๋ฆฌํฐ๋ด (Literals)
๋ฌธ์์ด, ์ซ์, boolean, null ๊ฐ์ ์ง์ ํํํ ์ ์๋ค.
๋ฌธ์์ด:
'Hello World'
์ซ์:
100
,3.14
Boolean:
true
,false
Null:
null
2. ํ๋กํผํฐ ๋ฐ ๋ฉ์๋ ์ ๊ทผ
์๋ฐ์ ์ ํ๊ธฐ๋ฒ(dot notation)์ ์ฌ์ฉํ์ฌ ๊ฐ์ฒด์ ํ๋กํผํฐ๋ ๋ฉ์๋์ ์ ๊ทผํ ์ ์๋ค.
// user.name ํ๋กํผํฐ ์ ๊ทผ
#{user.name}
// name ํ๋กํผํฐ์ toUpperCase() ๋ฉ์๋ ํธ์ถ
#{user.name.toUpperCase()}
// Null-safe ์ ๊ทผ์: user๊ฐ null์ด์ด๋ NullPointerException์ ๋ฐ์์ํค์ง ์๊ณ null์ ๋ฐํ
#{user?.name}
3. ์ฐ์ฐ์
๋ค์ํ ์ข ๋ฅ์ ์ฐ์ฐ์๋ฅผ ์ง์ํ๋ค.
์ฐ์ ์ฐ์ฐ์:
+
,-
,*
,/
,%
,^
๊ด๊ณ ์ฐ์ฐ์:
==
,!=
,<
,>
,<=
,>=
๋ ผ๋ฆฌ ์ฐ์ฐ์:
and
,or
,not
์ผํญ ์ฐ์ฐ์:
#{user.age > 20 ? '์ฑ์ธ' : '๋ฏธ์ฑ๋ ์'}
Elvis ์ฐ์ฐ์:
#{user.name ?: 'Guest'}
(user.name์ด null์ด๋ฉด 'Guest'๋ฅผ ๋ฐํ)
4. ํ์
๋ฐ Bean ์ฐธ์กฐ
T() ์ฐ์ฐ์: ํด๋์ค์ ์ ์ (static) ๋ฉ์๋๋ ํ๋์ ์ ๊ทผํ ๋ ์ฌ์ฉ ๊ฐ๋ฅ
#{T(java.lang.Math).random()}
Bean ์ฐธ์กฐ:
@
๊ธฐํธ๋ฅผ ์ฌ์ฉํ์ฌ Spring ์ปจํ ์ด๋์ ๋ฑ๋ก๋ ๋ค๋ฅธ Bean์ ์ฐธ์กฐ ๊ฐ๋ฅ#{@myBean.getSomeValue()}
ํ์ฉ ์ฌ๋ก
1. ์ ๋
ธํ
์ด์
๊ธฐ๋ฐ ์ค์
@Value
์ ๋
ธํ
์ด์
๊ณผ ํจ๊ป ์ฌ์ฉํ์ฌ ํ๋กํผํฐ ๊ฐ์ ์ฃผ์
ํ๊ฑฐ๋, ๋ค๋ฅธ Bean์ ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ๋์ ์ธ ๊ฐ์ ์ค์ ํ๋ค.
// application.properties์ app.name ๊ฐ์ ์ฃผ์
@Value("${app.name}")
private String appName;
// ์์คํ
ํ๋กํผํฐ(user.home) ๊ฐ์ ์ฃผ์
@Value("#{systemProperties['user.home']}")
private String userHome;
// ๋ค๋ฅธ Bean(someComponent)์ someValue ํ๋ ๊ฐ์ ์ฃผ์
@Value("#{someComponent.someValue}")
private int someValue;
// ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ฃผ์
@Value("#{someComponent.someValue > 100}")
private boolean isValueLarge;
2. ์กฐ๊ฑด๋ถ Bean ์์ฑ
@ConditionalOnExpression
์ ๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ ํน์ ์กฐ๊ฑด์ด ์ฐธ์ผ ๋๋ง Bean์ ์์ฑํ๋๋ก ์ ์ดํ ์ ์๋ค.
// 'feature.enabled' ํ๋กํผํฐ๊ฐ true์ผ ๋๋ง MyFeatureService Bean์ ์์ฑ
@Service
@ConditionalOnExpression("${feature.enabled:false}")
public class MyFeatureService {
// ...
}
3. Spring Security
๋ฉ์๋ ๋ ๋ฒจ์ ๋ณด์ ์ค์ ์์ ์ฌ์ฉ์์ ๊ถํ์ด๋ ์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฒ์ฌํ๋ ๋ฑ ๋ณต์กํ ๋ณด์ ๊ท์น์ ์ ์ฉํ ๋ ๋งค์ฐ ์ ์ฉํ๋ค.
// 'ADMIN' ์ญํ ์ ๊ฐ์ง ์ฌ์ฉ์๋ง ์ ๊ทผ ํ์ฉ
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// ...
}
// ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ username์ด ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ด๋ฆ๊ณผ ๊ฐ์ ๋๋ง ์ ๊ทผ ํ์ฉ
@PreAuthorize("#username == authentication.principal.username")
public UserProfile getUserProfile(String username) {
// ...
}
๋์ ํํ์ ํ๊ฐ
SpEL์ ์ ์ ์ธ ์ค์ ๋ฟ๋ง ์๋๋ผ, SpelExpressionParser
์ EvaluationContext
๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋ ๋ด์์ ๋ฐํ์์ ๋ฌธ์์ด ํํ์ ํํ์์ ๋์ ์ผ๋ก ํด์ํ๊ณ ์คํํ๋ ๊ธฐ๋ฅ๋ ์ ๊ณตํ๋ค.
ExpressionParser: ๋ฌธ์์ด ํํ์์ ํ์ฑํ์ฌ ์คํ ๊ฐ๋ฅํ
Expression
๊ฐ์ฒด๋ก ๋ณํEvaluationContext: ํํ์ ํ๊ฐ์ ํ์ํ ์ปจํ ์คํธ(๋ฃจํธ ๊ฐ์ฒด, ๋ณ์ ๋ฑ)๋ฅผ ์ ๊ณต
๋์ ์์ ์ฝ๋
@Service
public class DiscountService {
private final ExpressionParser parser = new SpelExpressionParser(); // SpEL ํ์๋ ์ค๋ ๋์ ์์ ํ๋ฏ๋ก Bean์ผ๋ก ๊ด๋ฆฌํ๊ฑฐ๋ ๋ฉค๋ฒ ๋ณ์๋ก ์ฌ์ฉ ๊ฐ๋ฅ
private final List<DiscountRule> discountRules; // ์ค์ ๋ก๋ Repository๋ฅผ ํตํด DB์์ ์กฐํ
/**
* ์ฃผ๋ฌธ ์ ๋ณด(OrderContext)๋ฅผ ๋ฐ์ ์ ์ฉ ๊ฐ๋ฅํ ํ ์ธ์จ์ ๊ณ์ฐ
*/
public double calculateDiscountRate(OrderContext context) {
EvaluationContext evalContext = new StandardEvaluationContext(context);
// ์ ์ฅ๋ ๊ท์น๋ค์ ์ํํ๋ฉฐ ์กฐ๊ฑด ํ์ธ
for (DiscountRule rule : discountRules) {
try {
// DB์์ ๊ฐ์ ธ์จ ๋ฌธ์์ด ํํ์์ ํ์ฑํ์ฌ Expression ๊ฐ์ฒด๋ก ๋ณํ
Expression expression = parser.parseExpression(rule.getCondition());
// ํ์ฌ ์ฃผ๋ฌธ ์ ๋ณด(context)๋ฅผ ๊ธฐ์ค์ผ๋ก ํํ์์ ํ๊ฐ
Boolean isMatch = expression.getValue(evalContext, Boolean.class);
if (isMatch != null && isMatch) {
return rule.getDiscountRate(); // ์กฐ๊ฑด์ด ๋ง์ผ๋ฉด ํด๋น ํ ์ธ์จ ๋ฐํ
}
} catch (Exception e) {
// ํํ์ ์ค๋ฅ ์ ๋ก๊ทธ ๊ธฐ๋ก ๋ฑ์ ์์ธ ์ฒ๋ฆฌ
System.err.println("SpEL ํ๊ฐ ์ค๋ฅ ๋ฐ์: " + rule.getCondition());
}
}
return 0.0; // ์ ์ฉ๋ ํ ์ธ ์์
}
private List<DiscountRule> loadDiscountRulesFromDB() {
return List.of(
new DiscountRule(
"user.membershipLevel == 'GOLD' and order.amount >= 100000",
0.15
),
new DiscountRule(
"user.membershipLevel == 'SILVER' and order.amount >= 50000",
0.05
),
new DiscountRule(
"order.productCategory == 'ELECTRONICS'",
0.10
)
);
}
}
// --- SpEL ํ๊ฐ์ ์ฌ์ฉ๋ ๋ฐ์ดํฐ ๋ชจ๋ธ ---
// ๊ท์น ํ๊ฐ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ฌถ์ด์ฃผ๋ DTO
class OrderContext {
private final User user;
private final Order order;
// ...
}
// DB์ ์ ์ฅ๋ ํ ์ธ ๊ท์น ๋ชจ๋ธ
class DiscountRule {
private final String condition; // SpEL ํํ์
private final double discountRate; // ํ ์ธ์จ
// ...
}
๋์ ์๋ฆฌ
SpEL์ ๋ด๋ถ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ ๋์ํ๋ค.
ํ์ฑ (Parsing): ๊ฐ๋ฐ์๊ฐ ์์ฑํ ๋ฌธ์์ด ํํ์(
'Hello' + ' World'
)์ ํ์(SpelExpressionParser
)๊ฐ ๋ถ์ํ์ฌ ์ถ์ ๊ตฌ๋ฌธ ํธ๋ฆฌ(AST)๋ก ๋ณํํ๊ฐ (Evaluation): ์์ฑ๋ AST๋ฅผ ์ํํ๋ฉฐ ๊ฐ ๋ ธ๋์ ํด๋นํ๋ ์ฐ์ฐ(ํ๋กํผํฐ ์ ๊ทผ, ๋ฉ์๋ ํธ์ถ, ์ฐ์ ์ฐ์ฐ ๋ฑ)์ ์คํ
ํด๋น ๊ณผ์ ์
EvaluationContext
์ ์ ์๋ ์ํ( ๋ฃจํธ ๊ฐ์ฒด, ๋ณ์ ๋ฑ)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ฃจ์ด์ง๋ฉด์ ์ต์ข ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ
Last updated
Was this helpful?