[개발 일기] 2025.02.25 - 전략 패턴

2025. 2. 25. 22:43·개발 일기

💡개요

오늘은 자바에서 사용되는 디자인 패턴인 전략 패턴에 대해 정리해 보자.

 

 

 

📕 전략 패턴

자바에서 전략 패턴이란, 실행 시점에 적절한 전략을 선택하여 유연하게 변경하거나 추가할 수 있도록 하는 디자인 패턴이다.

 

 

전략 패턴에서 각 기능을 담당하는 개별 전략은 단순히 전략이라기보다는 하나의 알고리즘으로 이해하는 것이 더 직관적이다.

 

 

다음은 전략 패턴을 도입했을 때, 클라이언트가 의존하는 전략과 관련된 다이어그램이다.

 

 

 

전략 패턴을 적용하면, 클라이언트는 Context만을 의존하게 되어 구체적인 전략(알고리즘) 구현에 대한 결합도를 낮출 수 있다.

 

 

이를 통해 새로운 전략을 쉽게 추가하거나 변경할 수 있으며, 코드의 유연성과 확장성이 향상된다.

 

 

 

🚀 전략 패턴 적용

 

다음은 전략 패턴 예시 코드이다.

 

 

첫 번째로 사용할 전략을 인터페이스로 추상화해야 한다.

 

 

interface Strategy {
    void runStrategy();
}

 

 

그다음, 위에서 선언한 전략 인터페이스를 구현한 구현체 클래스를 만든다.

 

 

이 구현체는 우리가 적용할 알고리즘이 포함된다.

 

 

public class StrategyA implements Strategy {

    @Override
    public void runStrategy() {
        System.out.println("A 전략 실행");
    }
}

public class StrategyB implements Strategy {
    
    @Override
    public void runStrategy() {
        System.out.println("B 전략 실행");
    }
}

 

 

그리고 위 전략 구현체를 보관하고 실행할 Context 클래스를 생성한다.

 

 

public class Context {
    Strategy strategy;

    void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    void runStrategy() {
        strategy.runStrategy();
    }
}

 

 

이제부턴 Context에 원하는 전략을 동적으로 설정하고 실행할 수 있다.

 

 

Context context = new Context();
context.setStrategy(new StrategyA()); // Context에 A 전략 적용
context.runStrategy();
context.setStrategy(new StrategyB()); // Context에 B 전략 적용
context.runStrategy();

 

 

클라이언트 코드 실행 결과는 다음과 같다.

 

[결과]
A 전략 실행
B 전략 실행

 

 

이처럼 전략 패턴을 사용하면 실행 시점에 원하는 전략을 동적으로 변경할 수 있어 기능을 더욱 유연하게 관리할 수 있다.

 

 

 

🚀 여러 개의 전략

 

위 예시처럼 낱개의 전략을 적용할 뿐만 아니라, 여러 전략을 사용할 수도 있다.

 

 

다음은 사용자가 회원가입 시, 입력한 비밀번호의 정합성을 판별하는 로직을 전략 패턴을 사용지 않고! 구현한 코드이다.

 

 

public class Password {
    private static final int PASSWORD_MIN_LENGTH = 8;
    private static final int PASSWORD_MAX_LENGTH = 20;
    private static final Pattern alphabetPattern = Pattern.compile("[a-zA-Z]");
    private static final Pattern numberPattern = Pattern.compile("\\\\d");
    private static final Pattern specialCharPattern = Pattern.compile("[!@?]");

    private void validate(final String password) {
        // 비밀번호가 null인지 검증
        if (password == null) {
            throw new RestApiException(PASSWORD_CANNOT_BE_NULL);
        }

        // 비밀번호에 공백이 포함되었는지 검증
        if (password.contains(" ")) {
            throw new RestApiException(PASSWORD_CANNOT_CONTAINS_BLANK);
        }

        // 비밀번호 길이 검증
        if (password.length() < PASSWORD_MIN_LENGTH || password.length() > PASSWORD_MAX_LENGTH) {
            throw new RestApiException(INVALID_PASSWORD_LENGTH);
        }

        // 비밀번호 형식 검증 (알파벳, 숫자, 특수문자 포함 여부)
        if (!(alphabetPattern.matcher(password).find()
                && numberPattern.matcher(password).find()
                && specialCharPattern.matcher(password).find())) {
            throw new RestApiException(INVALID_PASSWORD_FORMAT);
        }
    }
}

 

 

대부분의 플랫폼에서는 회원가입 시 비밀번호에 대한 여러 가지 제한 사항을 두고 있다.

 

 

예를 들어, 비밀번호의 최소/최대 길이나 필수로 포함해야 하는 문자(알파벳, 숫자, 특수문자 등) 같은 조건들이 있다.

 

 

위 Password 클래스는 이러한 검증 로직을 포함하고 있지만, 비밀번호 객체 자체에 너무 많은 역할이 부여되어 있다는 문제점이 있다.

 

 

물론 비즈니스적으로 볼 때 비밀번호의 정합성을 판별하는 것은 Password 클래스 내부에서 진행하는 것이 맞다.

 

 

하지만 위 코드는 지저분해도 너무 지저분하다.

 

 

특히 저 validate() 라는 메서드에 너무 많은 역할이 집중되어 있다.

 

 

저런 메서드가 유지보수를 어렵게 만드는 악마가 되곤 한다..

 

 

다음은 위 문제를 해결하기 위해 비밀번호 정합성을 전략 패턴을 사용해 구현한 코드이다.

 

 

interface PasswordValidationStrategy {
    void validate(final String password);
}

 

 

사용할 전략 메서드는 정합성 검증이기 때문에 validate() 라는 메서드명을 사용했다.

 

 

 

PasswordNullStrategy

 

비밀번호에 null 입력 여부를 판별하는 전략이다.

 

 

class PasswordNullStrategy implements PasswordValidationStrategy {

    /**
     * 비밀번호가 null 인지 검증한다.
     * @param password null 여부를 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (password == null)
            throw new RestApiException(PASSWORD_CANNOT_BE_NULL);
    }
}

 

 

 

PasswordBlankStrategy

 

비밀번호에 공백이 포함되었는지 확인하는 전략이다.

 

 

class PasswordBlankStrategy implements PasswordValidationStrategy {

    /**
     * 비밀번호에 공백이 포함되어 있는지 검증한다.
     * @param password 공백 포함 여부를 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (password.contains(" "))
            throw new RestApiException(PASSWORD_CANNOT_CONTAINS_BLANK);
    }
}

 

 

 

PasswordFormatStrategy

 

비밀번호에 최소 1개의 알파벳, 숫자, 특수문자(!, ?)가 포함되어 있는지 확인하는 전략이다.

 

 

class PasswordFormatStrategy implements PasswordValidationStrategy {
    private static final Pattern alphabetPattern = Pattern.compile("[a-zA-Z]"),
            numberPattern = Pattern.compile("\\\\d"),
            specialCharPattern = Pattern.compile("[!@?]");

    /**
     * 비밀번호에 알파벳, 숫자, 특수문자가 모두 포함되어 있는지 검증한다.
     * @param password 포맷을 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (!(alphabetPattern.matcher(password).find()
                && numberPattern.matcher(password).find()
                && specialCharPattern.matcher(password).find()))
            throw new RestApiException(INVALID_PASSWORD_FORMAT);
    }
}

 

 

 

PasswordLengthStrategy

 

비밀번호 길이가 기준에 맞는지 판별하는 전략이다.

 

 

class PasswordLengthStrategy implements PasswordValidationStrategy {
    private static final int PASSWORD_MIN_LENGTH = 8, PASSWORD_MAX_LENGTH = 20;

    /**
     * 비밀번호의 길이가 PASSWORD_MIN_LENGTH 이상 PASSWORD_MAX_LENGTH 이하인지 검증한다.
     * @param password 길이 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (password.length() < PASSWORD_MIN_LENGTH || password.length() > PASSWORD_MAX_LENGTH)
            throw new RestApiException(INVALID_PASSWORD_LENGTH);
    }
}

 

 

 

위의 전략 구현체를 적용하기 위해 Password 클래스에 List<PasswordValidationStrategy> 타입의 리스트를 생성하여 전략을 보관한다. (참고로 여기선 위 코드처럼 Password에서 직접 관리하는 것 보다 Context 객체를 따로 생성하여 관리하는 것이 전략 패턴의 정석이다..!)

 

 

모든 전략 구현체는 PasswordValidationStrategy 인터페이스를 상속받았기 때문에, 다형성을 활용하여 해당 리스트에 저장할 수 있다.

 

 

이제 리스트에 저장된 구현체들을 stream().forEach() 문법을 사용해 validate() 메서드를 훨씬 간결하게 표현할 수 있게 되었다.

 

@Embeddable
@NoArgsConstructor
public class Password {
    private static final List<PasswordValidationStrategy> passwordValidationStrategies = List.of(
            new PasswordNullStrategy(),
            new PasswordBlankStrategy(),
            new PasswordLengthStrategy(),
            new PasswordFormatStrategy()
    );

    ...

    @Column(name = "PASSWORD")
    private String password;

     public Password(final String password) {
        validate(password);
        this.password = password;
    }

    private void validate(String password) {
        passwordValidationStrategies.stream().forEach(strategy -> strategy.validate(password));
    }
}

 

 

뿐만 아니라, 이후에 비밀번호 최대 길이를 25자로 늘리거나, 사용 가능한 특수 문자에 !, @, ?뿐만 아니라 #, $까지 추가해야 한다는 요구사항이 생길 수도 있다.

 

 

이 경우, 기존에 전략 패턴을 적용하지 않았다면, 클라이언트 코드에 해당하는 Password 클래스를 직접 수정해야 한다.

 

public class Password {
    ...
    // 20 -> 25
    private static final int PASSWORD_MAX_LENGTH = 25;
    ...
    // !@? -> !@?#$
    private static final Pattern specialCharPattern = Pattern.compile("[!@?#$]");

    private void validate(final String password) {
        ...
    }
}

 

 

아니면 비밀번호 길이 제한 판별이 필요 없기 때문에 제거하라는 요구사항이 전달된 경우, 이 경우도 Password 클래스 코드 수정이 불가피하다.

 

 

public class Password {
    // 길이 제한 관련 상수 제거
    ...

    private void validate(final String password) {
        ...

        // 비밀번호에 공백이 포함되었는지 검증
        if (password.contains(" ")) {
            throw new RestApiException(PASSWORD_CANNOT_CONTAINS_BLANK);
        }

        // 비밀번호 길이 검증 코드 제거

        // 비밀번호 형식 검증 (알파벳, 숫자, 특수문자 포함 여부)
        if (!(alphabetPattern.matcher(password).find()
                && numberPattern.matcher(password).find()
                && specialCharPattern.matcher(password).find())) {
            throw new RestApiException(INVALID_PASSWORD_FORMAT);
        }
    }
}

 

 

하지만 전략 패턴을 사용할 경우 Password 코드의 변경을 최소화할 수 있다.

 

 

비밀번호 최대 제한 길이를 25로 변경할 경우 PasswordLengthStrategy.PASSWORD_MAX_LENGTH 만 수정하면 된다.

 

class PasswordLengthStrategy implements PasswordValidationStrategy {
    private static final int PASSWORD_MIN_LENGTH = 8, PASSWORD_MAX_LENGTH = 25; // 20 -> 25

    /**
     * 비밀번호의 길이가 PASSWORD_MIN_LENGTH 이상 PASSWORD_MAX_LENGTH 이하인지 검증한다.
     * @param password 길이 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (password.length() < PASSWORD_MIN_LENGTH || password.length() > PASSWORD_MAX_LENGTH)
            throw new RestApiException(INVALID_PASSWORD_LENGTH);
    }
}

 

 

사용 가능한 특수문자 추가도 PasswordFormatStrategy.specialCharPattern 만 수정하면 된다.

 

 

class PasswordFormatStrategy implements PasswordValidationStrategy {
    private static final Pattern alphabetPattern = Pattern.compile("[a-zA-Z]"),
            numberPattern = Pattern.compile("\\\\d"),
            specialCharPattern = Pattern.compile("[!@?#$]"); // !@? -> !@?#$

    /**
     * 비밀번호에 알파벳, 숫자, 특수문자가 모두 포함되어 있는지 검증한다.
     * @param password 포맷을 검증할 비밀번호
     */
    @Override
    public void validate(final String password) {
        if (!(alphabetPattern.matcher(password).find()
                && numberPattern.matcher(password).find()
                && specialCharPattern.matcher(password).find()))
            throw new RestApiException(INVALID_PASSWORD_FORMAT);
    }
}

 

 

이처럼 전략 패턴을 도입하면 코드 수정 범위를 최소화할 수 있기 때문에, 유지보수 측면에서 많은 장점을 얻을 수 있다.

 

 

하지만 그 만큼 전략을 수행하는 클래스가 많아지기 때문에 관리가 어렵다는 단점 또한 존재한다..

'개발 일기' 카테고리의 다른 글

[개발 일기] 2025.02.28 - @Profiles, @ActiveProfiles  (1) 2025.02.28
[개발 일기] 2025.02.26 - Dangling quantifier '+’ 에러  (0) 2025.02.26
[개발 일기] 2025.02.24 - == 연산자 vs Objects.isNull()  (0) 2025.02.24
[개발 일기] 2025.02.23 - @Builder를 사용하면 생성자가 private이라도 외부에서 접근 가능한 이유?  (0) 2025.02.23
[개발 일기] 2025.02.22 - JPA Entity 기본생성자  (0) 2025.02.22
'개발 일기' 카테고리의 다른 글
  • [개발 일기] 2025.02.28 - @Profiles, @ActiveProfiles
  • [개발 일기] 2025.02.26 - Dangling quantifier '+’ 에러
  • [개발 일기] 2025.02.24 - == 연산자 vs Objects.isNull()
  • [개발 일기] 2025.02.23 - @Builder를 사용하면 생성자가 private이라도 외부에서 접근 가능한 이유?
오도형석
오도형석
  • 오도형석
    형석이의 성장일기
    오도형석
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • MSA 모니터링 서비스
        • DB
      • 스파르타 코딩클럽
        • SQL
        • Spring
      • 백엔드
        • Internet
        • Java
        • DB
      • 캡스톤
        • Django
        • 자연어처리
      • Spring
        • JPA
        • MSA
      • ETC
        • ERROR
      • 개발 일기 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
오도형석
[개발 일기] 2025.02.25 - 전략 패턴
상단으로

티스토리툴바