이번 자동화 구축 작업을 진행하면서 한글을 영문으로 변환하는 로직을 구현하였는데, 확장성과 변경에 유연하게 대응하기 위한 대응성을 확보하기 위해 어떤 방안을 적용할 수 있을지 고민해보았다.
결론적으로는 함수형 인터페이스와 함수형 프로그래밍을 적용하면서 패턴 개념과 함께 적용해보았고, 이에 대해 공부한 내용들을 기록해보았다.
먼저 전략패턴과 팩토리 패턴에 대해 정확히 짚고 넘어가는 것이 좋겠다.
목적: "어떻게 처리할지(알고리즘)를 바꿀 수 있게 한다"
핵심은 "행위(Behavior)"를 런타임에 바꿀 수 있도록 만드는 것으로, 함수형 인터페이스에서 해당 로직을 처리하는 행동을(Function<Matcher, String>)을 캡슐화해서, 상황에 맞는 걸 골라 실행한다.
Strategy는 "무엇을 할지 정해진 상태"에서 "어떻게 할지 선택"할 수 있도록 하는 것이다.
목적: "어떤 객체를 생성할지를 캡슐화한다"
객체 생성 로직을 위임하는 패턴으로, 클라이언트는 객체를 받는 것에만 관심있고 내부적으로 어떤 객체를 할당받는 지에 대해서는 관심이 없다. 팩토리패턴을 통해 객체 생성 과정을 위임하여 할당된 객체를 받는다.
class PatternMappingFactory {
public static PatternMapping createSolarPatternMapping() {
return new PatternMapping(
Pattern.compile("^(태양광|태양|태양에너지)$"),
matcher -> "Solar Power"
);
}
}
이번에 적용한 "함수형 인터페이스", "함수형 프로그래밍"은 전략패턴을 적용한 것으로 볼 수 있겠다.
Function을 인자로 받아 내부적으로 매칭해주는 로직을 정의한 방식으로 함수형 프로그래밍을 구현해보았다.
이번에 적용하면서 함수형 프로그래밍과 전략 패턴에 대해 정리할 수 있었기에 이 과정을 같이 기록하였다.
기존 한글을 영문으로 변환하는 코드는 나열식으로 되어있어 구조적으로 복잡도가 지나치게 높았고, 한번에 하나의 매핑만 가능하여 정책변경이나 매핑 패턴 추가에 상당한 어려움이 있었다.
private static String normalizeDistrict(String string) {
Matcher matcher = districtPostfixesWithNumbers2.matcher(string);
if (matcher.find()) {
return matcher.group(1) + "-" + matcher.group(2) + " " + matcher.group(3) + "-" + matcher.group(4) + matcher.group(5);
} else {
matcher = districtPostfixesWithNumbers1.matcher(string);
if (matcher.find()) {
return matcher.group(1) + (matcher.group(1).endsWith(" ") ? "" : " ") + matcher.group(2) + "-" + matcher.group(3) + matcher.group(4);
} else {
matcher = districtPostfixes.matcher(string);
return matcher.find() ? matcher.group(1) + "-" + matcher.group(2) + matcher.group(3) : string;
}
}
}
먼저 Arrays.asList를 통해 패턴매핑을 list 상수화한다.
private static final List<PatternMapping> patternMappings = Arrays.asList(
new PatternMapping(
Pattern.compile("^(.{1,20}?)(특별자치도|특별자치시|특별시|광역시|대로|구|군|도|동|리|면|시|읍|가|길|로)(\\s*)$"),
matcher -> matcher.group(1) + "-" + matcher.group(2) + matcher.group(3)
),
new PatternMapping(
Pattern.compile("^(.{0,20}?)(\\d+)(\\s*)(가길|가|번길|로|단지|동)(\\s*)$"),
matcher -> matcher.group(1) + (matcher.group(1).endsWith(" ") ? "" : " ") + matcher.group(2) + "-" + matcher.group(3) + matcher.group(4)
),
new PatternMapping(
Pattern.compile("^(.{0,20}?)(대?로)\\s*(\\d+[가번]?)(길)(\\s*)$"),
matcher -> matcher.group(1) + "-" + matcher.group(2) + " " + matcher.group(3) + "-" + matcher.group(4) + matcher.group(5)
),
new PatternMapping(
Pattern.compile("(태양광|태양|태양에너지|태양열)"),
matcher -> " Solar Power "
),
new PatternMapping(
Pattern.compile("(풍력)"),
matcher -> " Wind Power "
),
new PatternMapping(
Pattern.compile("(바이오|바이오메스|바이오매스|바이오가스|바이오에너지)"),
matcher -> " Bioelectric Power "
),
new PatternMapping(
Pattern.compile("(소수력|수력)"),
matcher -> " Hydroelectric Power "
),
new PatternMapping(
Pattern.compile("(발전소)"),
matcher -> "Plant"
)
);
이처럼 패턴을 적용할 목록을 list로 상수화한다.
이때 PatternMappings는 아래처럼 pattern 정보, pattern을 적용할 matcher를 멤버변수로 하는 생성자로 구성된 클래스로 구성해주었다.
이때 matcher(replacer)는 Function<T, R>로, Type을 받아 String을 반환하는 함수형 인터페이스로 구성해주었다.
@Getter
public class PatternMapping {
private final Pattern pattern;
private final Function<Matcher, String> replacer;
public PatternMapping(Pattern pattern, Function<Matcher, String> replacer) {
this.pattern = pattern;
this.replacer = replacer;
}
}
지금까지의 정보를 종합하면,
PatternMapping에서 정의한 패턴 정보와 matcher는 PatternMappings의 생성자에 의해
- Patterns.compile에서 정의한 패턴정보들이
- 최종적으로 matcher에 전달되어 매핑된 문자열로 반환되어 나온다.
이제 이 함수형 인터페이스에서 matcher와 패턴 정보를 매핑하는 과정만 남았다.
private static String normalizeKea(String string) {
for (PatternMapping patternMapping : patternMappings) {
Matcher matcher = patternMapping.getPattern().matcher(string);
while (matcher.find()) {
String replacement = patternMapping.getReplacer().apply(matcher);
string = string.replace(matcher.group(), replacement);
}
}
return string;
}
이미 list에 patternMapping 정보들이 정의된 상태이므로, 각 패턴 정보들을 불러와 inputValue(=string)과 매칭하는 과정을 순환하도록 하며 이때 매칭한 문자열을 받아오는 과정을 함수형 프로그래밍으로 구현해보았다.
즉,
- patternMapping에서 pattern 정보를 불러온다(list에 정의해놓았다).
- matcher 정보를 불러온다.
- 함수형 인터페이스를 사용하면 매핑된 문자열을 불러올 수 있다는 점을 이용하여, getReplacer().apply(matcher) = matcher -> "matched String Value"; 매핑된 문자열을 불러온다.
- 최종적으로 해당 매핑 문자열로 바꾸는 작업을 진행하며, 매칭된 문자열은 matcher.group을 통해 "해당 패턴이 여러 번 발견되는 지를" 확인하고 발견된 패턴 수 만큼 영문명으로 변환해준다.
함수형 프로그래밍을 너무 어렵게 보지 말자.
matcher.find()
즉, string 안에서 해당 패턴이 여러 번 등장할 수도 있기 때문에, 한 번 찾고 끝나는 게 아니라 여러 번 반복해서 찾아준다.
matcher.group()
따라서 이 패턴과 매칭작업은, 기존 문자열을 특정 패턴에 따라 정규화(normalize)하는 작업이라 할 수 있겠다.