[Spring Framework - Core] 4. SpEL (Spring Expression Language)

mrcocoball·2023년 9월 4일

Spring Framework

목록 보기
5/20

해당 포스트는 Spring.io의 공식 문서를 포함한 레퍼런스와 코드를 통해 Spring Framework의 구조 / 기술에 대해 확인해보고자 하는 포스트입니다.

1. SpEL (Spring Expression Language)

개요

SpEL (Spring Expression Language)는 스프링 프레임워크에서 사용되는 표현 언어로, 다양한 스프링 구성 요소에서 값을 조회하고 조작하는데에 사용되며, 런타임에 객체 그래프를 쿼리하고 조작하는 것을 지원합니다.

SpEL 은 XML 및 어노테이션 기반 구성에서 사용할 수 있으며 뿐만 아니라 스프링 프레임워크에 직접 연결되지 않고 독립적으로 사용될 수도 있습니다.

주요 활용 목적 / 분야 및 장점

위에서 설명하였듯, SpEL은 다양한 구성 요소에 있는 값을 런타임 시점에서 표현식을 해석하고 처리할 수 있습니다. 뿐만 아니라 메서드 호출과 기본 문자열 템플릿 기능 역시 제공합니다.

이러한 특성을 살려 SpEL은 주로 Spring 구성을 정의하고 관리하는데 사용되며 그 밖에도 다양한 분야에 활용되는데 주요 예시는 다음과 같습니다.

  • 프로퍼티 및 빈 접근
    스프링 빈의 프로퍼티 값을 읽고 설정할 수 있음 (@Value)

  • 빈 참조 / 빈 설정
    스프링 빈을 참조하거나 빈의 메서드를 호출할 수 있으며 의존성 관리 시 SpEL을 사용할 수 있음

  • 조건식 및 논리 연산
    조건식과 논리 연산을 지원하여 표현식을 조건부로 실행하거나 다양한 논리 연산을 수행할 수 있음

  • 리스트 및 맵 조작
    리스트와 맵을 조작하고 필요한 데이터를 추출할 수 있음

  • 배열 및 컬렉션 조작
    배열 및 컬렉션을 조작하여 원소를 필터링하거나 변환할 수 있음

  • 메서드 호출
    메서드를 호출하고 그 결과를 반환할 수 있음

  • 데이터 액세스
    데이터베이스 쿼리나 데이터 조작 로직에서 사용하여 동적으로 데이터를 추출하거나 조작할 수 있음

  • 시큐리티 설정
    스프링 시큐리티와 함께 사용하여 보안 규칙을 정의하고 인증 및 권한 부여를 위한 조건을 지정할 수 있음

  • 템플릿 엔진
    템플릿 엔진(Thymeleaf 등)에서 동적 컨텐츠를 생성하거나 템플릿을 조작할 수 있음

  • 로깅
    로깅 설정에서 로깅 레벨을 동적으로 설정하거나 특정 이벤트에 대한 조건을 정의할 때 활용

SpEL은 스프링 프레임워크에서 다양한 상황에서 코드를 더욱 유지보수하기 쉽게 하고 가독성 있게 만드는데 도움을 줍니다.

2. 주요 표현식

공식 문서에 소개 된 주요 표현식 레퍼런스는 다음과 같습니다.

리터럴 표현식

ExpressionParser parser = new SpelExpressionParser();

// evaluates to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

// evaluates to "Tony's Pizza"
String pizzaParlor = (String) parser.parseExpression("'Tony''s Pizza'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evaluates to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

설정, 배열, 리스트, 맵, 인덱서

// evaluates to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
		context, tesla, String.class);

// Members List

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("members[0].name").getValue(
		context, ieee, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("members[0].inventions[6]").getValue(
		context, ieee, String.class);
        
// Officer's Dictionary
Inventor pupin = parser.parseExpression("officers['president']").getValue(
		societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
		societyContext, String.class);

// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
		societyContext, "Croatia");        

인라인 리스트 / 맵

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

배열 생성

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array -> 현재는 다차원 배열 생성 시 이니셜라이저를 사용할 수 없음
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

메서드

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
		societyContext, Boolean.class);

연산자
관계 연산자 (==, >, <), 정규식, 기호 연산자(lt, gt, le, ge, eq, ne, div, mod, not) 논리 연산자 (&&, ||, !), 수학 연산자, 할당 연산자 (=) 지원

// 관계 연산자
// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

// uses CustomValue:::compareTo
boolean trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean.class);


// 정규식 기반
// evaluates to false
boolean falseValue = parser.parseExpression(
		"'xyz' instanceof T(Integer)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
		"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression(
		"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
        
        
// 논리 연산자
// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);


// 수학 연산자
// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2

String testString = parser.parseExpression(
		"'test' + ' ' + 'string'").getValue(String.class);  // 'test string'

// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000

// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0

// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0

// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21


// 할당 연산자
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
		"name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);

타입

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
		"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
		.getValue(Boolean.class);

생성자

Inventor einstein = p.parseExpression(
		"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
		.getValue(Inventor.class);

// create new Inventor instance within the add() method of List
p.parseExpression(
		"Members.add(new org.spring.samples.spel.inventor.Inventor(
			'Albert Einstein', 'German'))").getValue(societyContext);

변수

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"

함수
EvaluationContext 를 통해 사용자 정의 메서드를 등록하여 사용 가능

// 사용자 정의 메서드 등록
Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

// 유틸리티 메서드 예시
public abstract class StringUtils {

	public static String reverseString(String input) {
		StringBuilder backwards = new StringBuilder(input.length());
		for (int i = 0; i < input.length(); i++) {
			backwards.append(input.charAt(input.length() - 1 - i));
		}
		return backwards.toString();
	}
}

// 사용
ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
		StringUtils.class.getDeclaredMethod("reverseString", String.class));

String helloWorldReversed = parser.parseExpression(
		"#reverseString('hello')").getValue(context, String.class);

빈 참조
@ 를 사용하여 빈 조회 가능 (팩토리 빈 자체 접근 시에는 &)

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

삼항 연산자 (If-Then-Else)

parser.parseExpression("name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
		"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
		.getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

엘비스 연산자
엘비스 연산자는 삼항 연산자 구문을 단축한 것으로 Groovy 언어에서 사용

// 엘비스 연산자 미사용 시
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

// 엘비스 연산자 사용 시
ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'

엘비스 연산자 사용 시 표현식에 기본값을 적용할 수 있음

@Value("#{systemProperties['pop3.port'] ?: 25}")

안전 탐색 연산자(Safe Navigation Operator)
NPE를 피하기 위해 사용되며 객체 참조 시 해당 객체의 속성에 액세스하기 전에 null 여부를 체크, null인 경우 예외 대신 null을 반환

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

컬렉션 셀렉션 / 프로젝션

// 컬렉션 셀렉션
List<Inventor> list = (List<Inventor>) parser.parseExpression(
		"members.?[nationality == 'Serbian']").getValue(societyContext);
        
Map newMap = parser.parseExpression("map.?[value<27]").getValue();

// 컬렉션 프로젝션
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");

표현식 템플릿
표현식 템플릿 사용 시 리터럴 텍스트를 하나 이상의 평가 블록과 혼합 가능

String randomPhrase = parser.parseExpression(
		"random number is #{T(java.lang.Math).random()}",
		new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

public class TemplateParserContext implements ParserContext {

	public String getExpressionPrefix() {
		return "#{";
	}

	public String getExpressionSuffix() {
		return "}";
	}

	public boolean isTemplate() {
		return true;
	}
}

3. 정리

SpEL은 스프링 프레임워크에서 사용되는 표현 언어로 런타임에 객체 그래프를 쿼리하고 조회하는 기능을 지원합니다.
주로 스프링 구성 요소를 정의하고 관리, 구성 요소에서 값을 조회하거나 조작할 때 사용하며 다양한 상황에서 코드를 가독성 있게 하고 유지보수하기 쉽게 만들어줍니다.

Appendix. 레퍼런스

https://docs.spring.io/spring-framework/reference/core/expressions.html
ChatGPT 3.5

profile
Backend Developer

0개의 댓글