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์ด ์ƒ์„ฑ๋œ ์ดํ›„, ๋Ÿฐํƒ€์ž„์— ํ‘œํ˜„์‹์ด ํ‰๊ฐ€

๊ตฌ๋ถ„
${...} (Property Placeholder)
#{...} (SpEL)

๋ชฉ์ 

์™ธ๋ถ€ ์„ค์ • ๊ฐ’์˜ ์ •์ ์ธ ์ฃผ์ž…

๋™์ ์ธ ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๋ฐ ๋กœ์ง ์ˆ˜ํ–‰

์ฒ˜๋ฆฌ ์‹œ์ 

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๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ ๋‚ด์—์„œ ๋Ÿฐํƒ€์ž„์— ๋ฌธ์ž์—ด ํ˜•ํƒœ์˜ ํ‘œํ˜„์‹์„ ๋™์ ์œผ๋กœ ํ•ด์„ํ•˜๊ณ  ์‹คํ–‰ํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์ œ๊ณตํ•œ๋‹ค.

  1. ExpressionParser: ๋ฌธ์ž์—ด ํ‘œํ˜„์‹์„ ํŒŒ์‹ฑํ•˜์—ฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ Expression ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜

  2. 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์€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์ณ ๋™์ž‘ํ•œ๋‹ค.

  1. ํŒŒ์‹ฑ (Parsing): ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๋ฌธ์ž์—ด ํ‘œํ˜„์‹('Hello' + ' World')์„ ํŒŒ์„œ(SpelExpressionParser)๊ฐ€ ๋ถ„์„ํ•˜์—ฌ ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ(AST)๋กœ ๋ณ€ํ™˜

  2. ํ‰๊ฐ€ (Evaluation): ์ƒ์„ฑ๋œ AST๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ๋…ธ๋“œ์— ํ•ด๋‹นํ•˜๋Š” ์—ฐ์‚ฐ(ํ”„๋กœํผํ‹ฐ ์ ‘๊ทผ, ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ, ์‚ฐ์ˆ  ์—ฐ์‚ฐ ๋“ฑ)์„ ์‹คํ–‰

    • ํ•ด๋‹น ๊ณผ์ •์€ EvaluationContext์— ์ •์˜๋œ ์ƒํƒœ( ๋ฃจํŠธ ๊ฐ์ฒด, ๋ณ€์ˆ˜ ๋“ฑ)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉด์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜

Last updated

Was this helpful?