[Java] 정책변경에 따른 유연한 대응을 위한 전략패턴 도입 방안

Hyo Kyun Lee·2025년 6월 18일
0

Java

목록 보기
98/100

1. 개요

바뀌는 정책에 대해 OCP(수정에는 닫혀있고 확장에는 열려있는) 원칙을 위배하지 않기 위해 전략패턴을 활용하여 구성하였다.

구성하면서 전략패턴의 인터페이스와 구현 객체 간 관계나 내부 동작이 어떻게 이루어지는지 최대한 명확히 짚어보았고, 이에 대해 공부한 내용을 기록한다.

2. ASIS : 함수형 인터페이스를 람다 형태로 구현

@Getter
public class PatternMapping {
    private final Pattern pattern;
    private final Function<T> replacer;

    public PatternMapping(Pattern pattern, KoreanRomanizerEnergyWordsChangingPatternInterface patternStrategy) {
        this.pattern = pattern;
        this.replacer = replacer;
    }
}

기존에는 패턴정보에 따라 matcher를 전달받은 후

String replacement = patternMapping.getReplacer().apply(matcher);
string = string.replace(matcher.group(), replacement);

이처럼 함수형 인터페이스에서 제공하는 함수를 활용하여 치환할 문자열을 추출하였다.

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)
            ),
  

이때 함수형 인터페이스의 함수가 PatternMappig 생성자 시점에서 정의된 람다함수로직을 그대로 실행하게 된다.

하지만 정책변경이 발생할 경우, 람다 자체를 바꾸고 해당 패턴 정보를 가지고 있는 클래스를 찾아서 변경해야 하는 등 OCP 정책을 위반함으로 인한 번거로움이 발생하게 된다.

유지보수성과 정책추가에 따른 확장성을 개선하기 위해 전략패턴을 도입하게 되었다.

3. TOBE : 정책에 따른 매칭함수를 캡슐화

기존 람다형태의 함수식을 정책 변경에 따른 확장성을 확보하기 위해 전략패턴으로 캡슐화하였다.

@Getter
public class PatternMapping {
    private final Pattern pattern;
    private final KoreanRomanizerEnergyWordsChangingPatternInterface patternStrategy;

    public PatternMapping(Pattern pattern, KoreanRomanizerEnergyWordsChangingPatternInterface patternStrategy) {
        this.pattern = pattern;
        this.patternStrategy = patternStrategy;
    }
}

패턴정보에서 치환 문자열을 추출하기 위해 기존 생성자의 함수형 인터페이스를 전략패턴의 인터페이스로 변경해주었다.

@FunctionalInterface
public interface KoreanRomanizerEnergyWordsChangingPatternInterface {
    String replace(Matcher matcher);
}

이때 전략 인터페이스는 FunctionalInterface 어노테이션을 사용해서 하나의 추상 메서드만 가지고 이를 구현할 수 있도록 명시하였다.

public class SolarStrategy implements KoreanRomanizerEnergyWordsChangingPatternInterface {
    @Override
    public String replace(Matcher matcher) {
        return " Solar Power ";
    }
}

그리고 정책정보가 추가될 때마다 해당 전략을 추가해서 관리할 수 있도록 확장성을 개선하였다.

String replacement = patternMapping.getPatternStrategy().replace(matcher);

최종적으로는 기존 patterMapping의 Function에서 제공하는 apply를 사용하는 방식에서, 직접 구현한 인터페이스의 메서드(replace)를 사용하는 방식으로 변경하였다.

4. 전략패턴 동작 원리

getPatternStrategy()는 "도장을 새로 깎는 것"이 아니라,
"이미 만들어진 도장을 가져와서 도장을 찍는 것"이라고 보면 된다.

함수형 인터페이스 구현 로직을 천천히 살펴보면서 든 생각은,

  • 인터페이스의 추상 메서드로 껍데기만 있었던 부분이 생성자 생성 시점에서 껍데기가 채워지는 것인가?
  • patternMapping.getPatternStrategy() 이 시점에서 채워진 껍데기, 즉 "객체"를 생성하여 replace란 구현 메소드를 불러올 수 있는 것인가?

이었다.

그게 아니라 패턴 정보 생성자를 시점하면서 기존 인터페이스를 참조하고 있던 멤버변수는 힙 메모리의 새로 생성된 전략패턴 객체를 바라보게 된다.

new PatternMapping(Pattern.compile("(태양광)"), new SolarPowerReplacer());  //전략패턴 객체 참조

따라서 위에 해당하는 "patternMapping" 객체를 만나게 되면 기존 전략 인터페이스가 아닌, 생성 시점에 생성된 "SolarPowerReplacer"라는 전략패턴 객체를 참조하게 된다.

patternMapping.getPatternStrategy().replace(matcher);  

즉, 이 때 patternMapping이 getPatternStrategy()를 부를때 객체를 생성하거나 부르는 개념이 아니라 "해당 전략 객체를 참조하고 있는 부분을 호출해서" 해당 전략 로직을 불러오는 것 뿐이다.

위에서 말했던 것처럼, "이미 찍어낸 도장"을 가져와서 사용하는 것으로 이해하면 편하다.

5. 결론

람다가 가독성, 간편성 측면에서 좋을 수는 있겠지만 정책변경빈도나 환경에 따라 적절한 캡슐화 및 패턴화가 필요하다.

지금의 경우처럼 정책변경이 빈번하게 일어나거나 유지관리 측면에서 이점을 얻고 싶다면, 패턴을 캡슐화하여 적용하는 것을 고려해보아야 하겠다.

추가적으로 느꼈던 점은 괜히 OOP 5대 원칙, Clean Architecture가 있다는 것이 아니라는 점이다. 이상한 점을 느꼈다면 어떤 점에서 개선사항이 필요할지 고민해보는 것이 좋겠다.

0개의 댓글