1. 개요

Java 표준 라이브러리는 String.format ("%s is awesome", "Java") 과 같은 템플릿 기반 문자열의 형식을 지정하는 String.format() 메서드를 제공합니다 .

이 사용방법(예제)에서는 명명된 매개 변수를 지원하는 문자열 형식을 지정하는 방법을 살펴보겠습니다.

2. 문제 소개

String.format () 메서드는 사용하기 매우 간단합니다. 그러나 format() 호출에 인수가 많으면 어떤 값이 어떤 형식 지정자에 오는지 이해하기 어려워집니다. 예를 들면 다음과 같습니다.

Employee e = ...; // get an employee instance
String template = "Firstname: %s, Lastname: %s, Id: %s, Company: %s, Role: %s, Department: %s, Address: %s ...";
String.format(template, e.firstName, e.lastName, e.Id, e.company, e.department, e.role ... )

또한 이러한 인수를 메서드에 전달할 때 오류가 발생하기 쉽습니다. 예를 들어, 위의 예에서 실수로 e.role 앞에  e.department를 넣었습니다.

따라서 템플릿에서 명명된 매개변수와 같은 것을 사용한 다음 모든 매개변수 이름->값 매핑을 보유하는 맵을 통해 서식을 적용할 수 있다면 좋을 것입니다 .

String template = "Firstname: ${firstname}, Lastname: ${lastname}, Id: ${id} ...";
ourFormatMethod.format(template, parameterMap);

이 사용방법(예제)에서는 먼저 이 문제의 대부분의 경우를 해결할 수 있는 인기 있는 외부 라이브러리를 사용하는 솔루션을 살펴보겠습니다. 그런 다음 솔루션을 깨뜨리는 엣지 케이스에 대해 논의합니다.

마지막으로 모든 경우를 처리할 수 있는 자체 format() 메서드를 만듭니다 .

간단하게 하기 위해 단위 테스트 어설션을 사용하여 메서드가 예상 문자열을 반환하는지 확인합니다.

이 예제에서는 간단한 문자열 형식( %s ) 에만 초점을 맞출 것이라는 점도 언급할 가치가 있습니다 . 날짜, 숫자 또는 너비와 정밀도가 정의된 형식과 같은 다른 형식 유형은 지원되지 않습니다.

3. Apache Commons 텍스트에서 StrSubstitutor 사용

Apache Commons Text 라이브러리에는 문자열 작업을 위한 편리한 유틸리티가 많이 포함되어 있습니다. 이름이 지정된 매개변수를 기반으로 문자열 대체를 수행할 수 있는 StrSubstitutor 와 함께 제공됩니다 .

먼저 라이브러리를 Maven 구성 파일에 대한 새 의존성으로 추가해 보겠습니다.

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.9</version>
</dependency>

물론 Maven Central 저장소에서 항상 최신 버전을 찾을 수 있습니다.

StrSubstitutor 클래스를 사용하는 방법을 보기 전에 예제로 템플릿을 만들어 보겠습니다.

String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";

다음으로 StrSubstitutor를 사용하여 위의 템플릿을 기반으로 문자열을 빌드하는 테스트를 만들어 보겠습니다 .

Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");

테스트 코드에서 볼 수 있듯이 매개변수가 모든 이름 -> 값 매핑을 보유하도록 합니다. StrSubstitutor.replace() 메서드를 호출할 때 템플릿매개 변수와 별도로 접두사 및 접미사도 전달하여 StrSubstitutor에 매개 변수가 템플릿에서 무엇으로 구성되어 있는지 알려줍니다 . StrSubstitutor는 매개변수 이름에 대한 접두사 + map.entry.key + 접미사를 검색합니다 .

테스트를 실행하면 통과합니다. 따라서 StrSubstitutor가 문제를 해결하는 것 같습니다.

4. 엣지 케이스: 대체 항목에 자리 표시자가 포함된 경우

StrSubstitutor.replace() 테스트가 기본 사용 사례에 대해 통과하는 것을 확인했습니다 . 그러나 일부 특수한 경우는 테스트에서 다루지 않습니다. 예를 들어 매개변수 값에는 매개변수 이름 패턴 " ${ … } "가 포함될 수 있습니다.

이제 이 경우를 테스트해 보겠습니다.

Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");

assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

위의 테스트에서 " ${text} " 매개변수 값에는 " ${number} " 라는 텍스트가 포함되어 있습니다 . 따라서 “ ${text} ”가 문자 그대로 “ ${number} ” 텍스트로 대체될 것으로 예상합니다 .

그러나 테스트를 실행하면 테스트가 실패합니다.

org.opentest4j.AssertionFailedError: 
expected: "Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]"
 but was: "Text: ['42' is a placeholder.] Number: [42] Text again: ['42' is a placeholder.]"

따라서 StrSubstitutor는 리터럴 ${number} 도 매개변수 자리 표시자로 취급합니다.

실제로 StrSubstitutor 의 Javadoc은 다음과 같은 사례를 언급했습니다.

변수 교체는 재귀 방식으로 작동합니다. 따라서 변수 값에 변수가 포함되어 있으면 해당 변수도 대체됩니다.

이는 각 재귀 단계에서 StrSubstitutor가 추가 교체를 진행하기 위해 마지막 교체 결과를 새 템플릿 으로 사용하기 때문에 발생합니다 .

이 문제를 우회하기 위해 서로 다른 접두사 및 접미사를 선택하여 방해받지 않도록 할 수 있습니다.

String TEMPLATE = "Text: [%{text}] Number: [%{number}] Text again: [%{text}]";
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "%{", "}");

assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

그러나 이론적으로 말하면 값을 예측할 수 없으므로 값에 매개 변수 이름 패턴이 포함되어 대체를 방해할 가능성이 항상 있습니다.

다음으로 문제를 해결하기 위해 자체 format() 메서드를 만들어 보겠습니다  .

5. 직접 포맷터 만들기

StrSubstitutor가 에지 케이스를 잘 처리할 수 없는 이유에 대해 논의했습니다 . 따라서 메서드를 만들면 마지막 단계의 결과를 현재 단계의 새 입력으로 사용하기 위해 루프나 재귀를 사용해서는 안 된다는 어려움이 있습니다 .

5.1. 문제를 해결하기 위한 아이디어

아이디어는 템플릿에서 매개변수 이름 패턴을 검색한다는 것입니다. 그러나 하나를 찾으면 맵의 값으로 즉시 대체하지 않습니다. 대신 표준 String.format() 메서드에 사용할 수 있는 새 템플릿을 빌드합니다. 예를 들면 다음과 같이 변환하려고 합니다.

String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
Map<String, Object> params ...

안으로:

String NEW_TEMPLATE = "Text: [%s] Number: [%s] Text again: [%s]";
List<Object> valueList = List.of("'${number}' is a placeholder.", 42, "'${number}' is a placeholder.");

그런 다음 String.format(NEW_TEMPLATE, valueList.toArray())를 호출할 수 있습니다. 일을 끝내기 위해.

5.2. 방법 만들기

다음으로 아이디어를 구현하는 메서드를 만들어 보겠습니다.

public static String format(String template, Map<String, Object> parameters) {
    StringBuilder newTemplate = new StringBuilder(template);
    List<Object> valueList = new ArrayList<>();

    Matcher matcher = Pattern.compile("[$][{](\\w+)}").matcher(template);

    while (matcher.find()) {
        String key = matcher.group(1);

        String paramName = "${" + key + "}";
        int index = newTemplate.indexOf(paramName);
        if (index != -1) {
            newTemplate.replace(index, index + paramName.length(), "%s");
            valueList.add(parameters.get(key));
        }
    }

    return String.format(newTemplate.toString(), valueList.toArray());
}

위의 코드는 매우 간단합니다. 작동 방식을 이해하기 위해 빠르게 살펴보겠습니다.

먼저 새 템플릿( newTemplate )과 값 List( valueList )을 저장하기 위해 두 개의 새 변수를 선언했습니다. 나중에 String.format()을 호출할 때 필요합니다 .

Regex를 사용하여 템플릿에서 매개변수 이름 패턴을 찾습니다. 그런 다음 매개변수 이름 패턴을 "%s" 로 바꾸고 해당 값을 valueList 변수에 추가합니다.

마지막으로 새로 변환된 템플릿과 valueList의 값을 사용하여 String.format() 을 호출합니다  .

단순화를 위해 메서드에서 접두사 " ${ "와 접미사 " } " 를 하드 코딩했습니다 . 또한 " ${unknown} " 매개변수 값이 제공되지 않으면 " ${unknown} " 매개변수를 " null " 로 간단히 대체합니다 .

5.3. format() 메서드 테스트

다음으로 메서드가 일반적인 경우에 작동하는지 테스트해 보겠습니다.

Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");

다시 한 번 실행하면 테스트가 통과됩니다.

물론 엣지 케이스에서도 작동하는지 확인하고 싶습니다.

params.put("text", "'${number}' is a placeholder.");
result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

이 테스트를 실행하면 역시 통과합니다! 우리는 문제를 해결했습니다.

6. 결론

이 문서에서는 값 집합에서 템플릿 기반 문자열의 매개 변수를 바꾸는 방법을 살펴보았습니다. 기본적으로 Apache Commons Text의 StrSubstitutor.replace() 메서드는 사용하기 매우 간단하며 대부분의 경우를 해결할 수 있습니다. 그러나 값에 매개변수 이름 패턴이 포함되어 있으면 StrSubstitutor가 예기치 않은 결과를 생성할 수 있습니다.

따라서 이 극단적인 경우를 해결하기 위해 format() 메서드를 구현했습니다  .

항상 그렇듯이 예제의 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .

res – REST with Spring (eBook) (everywhere)