실무 디자인 패턴 3가지로 중복 코드 끝내기

실무 디자인 패턴은 면접용 암기 지식이 아니라, 매일 마주치는 if-else 폭발과 복붙 코드를 줄이기 위한 현실적인 도구입니다. 결제 수단이 하나 늘 때마다 분기문이 하나씩 붙고, 비슷한 처리 흐름이 클래스마다 복사되는 코드를 한 번이라도 만져봤다면 패턴이 왜 필요한지 몸으로 알고 있는 셈입니다.

이 글에서는 백엔드 실무에서 체감 효과가 가장 큰 세 가지 패턴 — 전략 패턴, 팩토리 패턴, 템플릿 메서드 패턴 — 을 골라, 패턴 적용 전후 코드를 비교하며 정리합니다. 이론보다는 “언제 꺼내 쓰는가”에 초점을 맞췄습니다.


디자인 패턴, 왜 실무에서 필요한가

디자인 패턴은 자주 반복되는 설계 문제에 대한 검증된 해법 모음입니다. 새로 발명하는 것이 아니라, 수많은 개발자가 같은 문제로 데어보고 정착시킨 공통 어휘에 가깝습니다.

실무에서 패턴이 빛나는 순간은 대개 변경이 잦은 지점입니다. 결제 수단, 할인 정책, 알림 채널처럼 “계속 늘어날 게 뻔한” 영역에 분기문을 쌓아 두면, 기능 하나 추가할 때마다 기존 코드를 뜯어야 하고 그때마다 버그 확률이 올라갑니다.

다만 주의할 점도 분명합니다. 패턴은 목적이 아니라 도구입니다. 변경 가능성이 거의 없는 코드에 패턴부터 끼워 넣으면, 단순한 로직이 클래스 여러 개로 흩어져 오히려 읽기 어려워집니다. 변경이 예상되는 곳에만 골라 쓰는 안목이 핵심입니다.


전략 패턴: 분기문을 객체로 바꾸기

전략 패턴(Strategy Pattern)은 알고리즘(행위)을 객체로 캡슐화해, 실행 시점에 갈아 끼울 수 있게 만드는 패턴입니다. 가장 흔한 적용 대상이 바로 “수단에 따라 처리가 갈리는” 분기문입니다.

다음은 결제 수단이 늘어날 때마다 부풀어 오르는 전형적인 코드입니다.

public PaymentResult pay(String method, Money amount) {
    if (method.equals("CARD")) {
        // 카드 결제 로직
    } else if (method.equals("KAKAO")) {
        // 카카오페이 로직
    } else if (method.equals("TOSS")) {
        // 토스 로직
    }
    // 결제 수단이 늘 때마다 여기에 else if 가 계속 붙는다
    throw new IllegalArgumentException("지원하지 않는 결제 수단");
}

전략 패턴을 적용하면, 각 결제 수단을 같은 약속(인터페이스)을 지키는 별도 객체로 분리합니다.

public interface PaymentStrategy {
    PaymentResult pay(Money amount);
}

@Component
public class CardPayment implements PaymentStrategy {
    public PaymentResult pay(Money amount) { /* 카드 결제 */ }
}

@Component
public class KakaoPayment implements PaymentStrategy {
    public PaymentResult pay(Money amount) { /* 카카오페이 */ }
}

이제 새 결제 수단이 생겨도 기존 코드를 건드릴 필요가 없습니다. 클래스 하나를 추가하면 끝입니다. 호출하는 쪽은 어떤 전략이 들어오든 pay()만 부르면 되고, 안에서 무슨 일이 일어나는지 알 필요가 없습니다. 인터페이스로 결합을 끊는 이 사고방식은 인터페이스와 상속, 야근을 줄여준 설계 이야기 에서 다룬 원칙과 정확히 같은 맥락입니다.


팩토리 패턴: 객체 생성 책임을 한곳에 모으기

전략 패턴을 적용하고 나면 곧바로 다음 질문이 따라옵니다. “그래서 어떤 전략 객체를 누가 골라주나?” 이 객체 생성과 선택의 책임을 한곳에 모으는 것이 팩토리 패턴(Factory Pattern)입니다.

생성 로직이 호출부 여기저기에 흩어지면, 결제 수단 매핑을 바꿀 때 또 여러 곳을 찾아다녀야 합니다. 팩토리에 그 책임을 몰아넣으면 됩니다.

@Component
@RequiredArgsConstructor
public class PaymentStrategyFactory {
    private final Map<String, PaymentStrategy> strategies; // 스프링이 자동 주입

    public PaymentStrategy find(String method) {
        PaymentStrategy strategy = strategies.get(method);
        if (strategy == null) {
            throw new IllegalArgumentException("지원하지 않는 결제 수단: " + method);
        }
        return strategy;
    }
}

스프링을 쓴다면 같은 인터페이스를 구현한 빈들을 Map으로 한 번에 주입받을 수 있어, 팩토리 구현이 매우 깔끔해집니다. 빈 이름을 키로 등록해 두면 위처럼 Map 조회만으로 전략 선택이 끝납니다.

구분분기문 직접 작성전략 + 팩토리
수단 추가 시기존 메서드 수정클래스만 추가
생성 책임호출부 곳곳에 분산팩토리 한곳
테스트분기마다 통합전략별 단위 테스트

템플릿 메서드 패턴: 흐름은 고정, 일부만 바꾸기

템플릿 메서드 패턴(Template Method Pattern)은 전체 처리 흐름은 부모가 고정하고, 달라지는 일부 단계만 자식이 채우게 하는 패턴입니다. “큰 골격은 똑같은데 중간 한두 단계만 다른” 작업에 잘 맞습니다.

예를 들어 외부 시스템 연동은 대개 연결 → 요청 → 응답 파싱 → 정리라는 동일한 골격을 가집니다. 연동 대상마다 복붙하면 골격이 N벌로 늘어납니다. 공통 흐름을 부모에 한 번만 정의합니다.

public abstract class ApiClient {
    // 흐름(템플릿)은 부모가 고정한다
    public final Response call(Request req) {
        connect();
        Response res = send(req);   // 이 단계만 자식마다 다르다
        return parse(res);
    }
    protected abstract Response send(Request req);
    protected Response parse(Response res) { return res; } // 기본 구현, 필요시 재정의
    private void connect() { /* 공통 연결 처리 */ }
}

자식 클래스는 달라지는 send()만 구현하면 됩니다. 흐름 자체가 부모에 한 번만 존재하므로, 연결·로깅·정리 로직이 바뀌어도 한 곳만 고치면 모든 연동에 반영됩니다. MyBatis의 공통 SQL 추출이나 스프링의 JdbcTemplate도 이 발상의 연장선이며, 반복을 한곳으로 모으는 관점은 “MyBatis WHERE·SELECT 공통화 실전 패턴 — 휴먼 에러 차단”과도 통합니다.

참고로 스프링 프레임워크 자체가 거대한 패턴의 집합입니다. 트랜잭션·AOP는 프록시 패턴, 빈은 사실상 싱글톤 패턴, ApplicationEvent는 옵저버 패턴으로 동작합니다. 패턴을 알면 프레임워크의 동작 원리까지 함께 읽힙니다. 이벤트 기반 설계가 궁금하다면 스프링 순환참조, 이벤트 리스너로 끊어낸 극복기를 참고하시기 바랍니다.


패턴을 남용하지 않으려면

패턴에 익숙해지면 가장 흔히 빠지는 함정이 과한 적용입니다. 단순한 if 두 줄을 굳이 전략 객체 세 개로 쪼개면, 읽는 사람이 코드를 따라가느라 파일을 여러 개 열어야 합니다.

실무에서 쓰는 간단한 기준은 다음과 같습니다. 같은 형태의 분기나 중복이 세 번 나타나고, 앞으로도 늘어날 가능성이 보일 때 패턴을 도입합니다. 두 번까지는 우연일 수 있으므로 그대로 두는 편이 낫습니다.

또한 상속(템플릿 메서드)보다 인터페이스 기반 조합(전략)을 먼저 고려하는 것이 안전합니다. 상속은 부모와 자식을 단단히 묶어버려서, 정말 “~는 ~다”라는 관계가 분명할 때만 쓰는 것이 좋습니다.


자주 묻는 질문 (FAQ)

Q1. 디자인 패턴을 꼭 외워야 하나요?

이름을 통째로 암기할 필요는 없습니다. 다만 “분기가 늘어나는데 어떻게 정리하지?” 같은 문제 상황과 해법을 연결해 두면, 코드 리뷰나 협업에서 같은 어휘로 빠르게 소통할 수 있습니다. 패턴 이름은 팀의 공통 언어라는 점이 실무에서 더 중요합니다.

Q2. 전략 패턴과 팩토리 패턴은 뭐가 다른가요?

전략 패턴은 행위(알고리즘)를 갈아 끼우는 데 초점이 있고, 팩토리 패턴은 그 객체를 생성·선택하는 책임을 분리하는 데 초점이 있습니다. 둘은 경쟁 관계가 아니라, 전략을 정의하고 팩토리가 골라주는 식으로 함께 쓰일 때가 많습니다.

Q3. 스프링을 쓰면 패턴을 직접 구현할 일이 없지 않나요?

스프링이 프록시·싱글톤·옵저버 같은 패턴을 내부적으로 처리해 주는 것은 맞습니다. 하지만 비즈니스 로직의 분기·중복은 여전히 개발자의 몫입니다. 전략·팩토리·템플릿 메서드는 도메인 코드에서 직접 적용해야 효과를 봅니다.

Q4. 패턴을 적용했더니 클래스가 너무 많아졌습니다.

변경 가능성이 낮은 곳까지 패턴을 적용했을 가능성이 큽니다. 패턴은 “늘어날 것이 예상되는” 지점에만 선택적으로 적용하고, 단순하고 안정적인 로직은 그대로 두는 것이 가독성과 유지보수 모두에 유리합니다.


마무리

실무 디자인 패턴의 핵심은 화려한 구조가 아니라 “변경에 강한 코드”를 만드는 데 있습니다. 전략 패턴으로 분기문을 객체로 바꾸고, 팩토리 패턴으로 생성 책임을 한곳에 모으고, 템플릿 메서드 패턴으로 공통 흐름을 한 번만 정의하면, 새 요구사항이 대개 “기존 코드 수정”이 아니라 “코드 추가”로 끝납니다.

기억할 기준은 단순합니다. 같은 중복이 세 번 보이고 앞으로 늘어날 것 같을 때만 패턴을 꺼내고, 상속보다 인터페이스 기반 조합을 먼저 떠올립니다. 패턴은 목적이 아니라 미래의 나를 위한 보험이라는 점을 잊지 않는 것이 마지막 원칙입니다.

다음 기능을 만들기 전에 30초만 멈춰 물어보시기 바랍니다. “이 분기, 앞으로 계속 늘어날까?” 그 질문 하나가 며칠 뒤의 야근을 막아줍니다.

더 읽어볼 거리로는 Refactoring Guru — 디자인 패턴Spring Framework 공식 문서를 추천합니다.


핵심 요약

  • 디자인 패턴은 변경이 잦은 지점에 선택적으로 적용하는 도구이며, 목적이 아닙니다.
  • 전략 패턴: 수단별 분기문을 같은 인터페이스의 객체로 분리해, 추가 시 기존 코드를 건드리지 않습니다.
  • 팩토리 패턴: 객체 생성·선택 책임을 한곳에 모으며, 스프링에서는 Map 빈 주입으로 깔끔하게 구현됩니다.
  • 템플릿 메서드 패턴: 공통 흐름은 부모가 고정하고 달라지는 단계만 자식이 채워, 흐름 변경을 한곳에서 처리합니다.
  • 같은 중복이 세 번 보이고 늘어날 조짐이 있을 때 도입하고, 상속보다 인터페이스 조합을 우선합니다.

“실무 디자인 패턴 3가지로 중복 코드 끝내기”에 대한 1개의 생각

댓글 남기기