하나의 PHP 파일에 집중되어 있던 알뜰폰 요금제 검색 API를 SRP(단일 책임 원칙) 기반으로 리팩토링한 과정을 정리했습니다. Service, Repository, Guard 구조 분리와 회귀 테스트 도입을 통해 유지보수성과 확장성을 개선한 실제 사례를 소개합니다.

PHP API 리팩토링 후기: SRP 원칙으로 요금제 검색 API 구조 개선하기
알뜰폰 요금제 비교 서비스인 플랜앱트하우(플하)-plan.apthow.com 가장 많이 수정되는 기능 중 하나가 요금제 검색 API였습니다. 처음 개발할 당시에는 빠르게 기능을 구현하는 것이 목표였기 때문에 대부분의 로직을 하나의 PHP 파일에 작성했습니다. 하지만 서비스가 성장하면서 검색 조건, 정렬 정책, 제휴 요금제 노출, 응답 포맷 등이 계속 추가되었고 결국 유지보수가 어려운 구조가 되었습니다.
이번 리팩토링에서는 단순히 파일을 나누는 것이 아니라 SRP(Single Responsibility Principle, 단일 책임 원칙)를 중심으로 역할과 책임을 분리하는 것을 목표로 진행했습니다.
이 글에서는 실제로 PHP 검색 API를 어떻게 리팩토링했는지, 어떤 기준으로 클래스를 분리했는지, 그리고 리팩토링 이후 어떤 효과를 얻었는지 정리해보겠습니다.
리팩토링을 시작하게 된 이유
초기 구조에서는 api/plans.php 하나가 거의 모든 작업을 담당하고 있었습니다.
// CORS
// API 접근 제어
// Rate Limit
// 검색 필터 처리
// SQL 생성
// 정렬 처리
// 추천 요금제 처리
// 응답 데이터 가공
// JSON 출력
처음에는 문제가 없었지만 기능이 증가하면서 여러 문제가 발생했습니다.
기존 구조의 문제점
| 수정 위치 찾기 어려움 | 기능이 많아질수록 코드 탐색 시간이 증가 |
| 영향 범위 증가 | 작은 수정도 예상치 못한 사이드 이펙트 발생 |
| 테스트 어려움 | 특정 기능만 독립적으로 검증하기 어려움 |
| 코드 중복 증가 | SQL 및 응답 처리 로직 반복 |
| 신규 기능 추가 부담 | 기존 코드 이해가 선행되어야 함 |
특히 정렬 정책 하나를 수정하려고 해도 검색 조건 생성 부분과 SQL 생성 부분을 함께 확인해야 했습니다.
이번 리팩토링의 핵심 목표
이번 작업의 목표는 매우 단순했습니다.
변경 사항이 발생했을 때 수정해야 하는 파일을 명확하게 만든다.
예를 들어 아래와 같은 구조를 만들고 싶었습니다.
정렬 정책 변경
↓
PlanSortService 수정
검색 필터 변경
↓
PlanFilterBuilder 수정
응답 구조 변경
↓
PlanResponseService 수정
제휴 정책 변경
↓
PlanPromotedService 수정
즉,
하나의 변경 이유
=
하나의 수정 위치
를 만드는 것이 목표였습니다.
이 개념이 바로 SRP(단일 책임 원칙)의 핵심입니다.
리팩토링 후 전체 구조
현재 API 진입점은 매우 단순한 형태가 되었습니다.
PlanApiBootstrap::load(__DIR__);
Database::ensureRuntimeSchema();
PlanApiGuard::sendCorsHeaders();
PlanApiGuard::handleOptionsRequest();
PlanApiGuard::isAllowedSiteRequest();
PlanApiGuard::enforceRateLimit();
PlanSearchService::search($_GET);
이제 plans.php 파일만 보더라도 API가 어떤 순서로 동작하는지 바로 이해할 수 있습니다.
Bootstrap
↓
Database
↓
Guard
↓
SearchService
실제 비즈니스 로직은 모두 별도 클래스로 이동했습니다.
PlanResponseService
응답 데이터 관련 책임을 담당합니다.
담당 역할
응답 데이터 정규화
공개 필드 필터링
URL 생성
이벤트 정보 가공
평생 할인 정보 생성
예를 들어 응답 JSON 구조를 변경해야 하는 경우
PlanResponseService
만 수정하면 됩니다.
기존처럼 검색 로직을 건드릴 필요가 없습니다.
PlanSortService
정렬 정책 전용 서비스입니다.
담당 역할
sort 파라미터 해석
ORDER BY 생성
정렬 우선순위 처리
예를 들어
최신순 추가
가격순 변경
데이터 사용량 우선 정렬
같은 요구사항이 발생하면 정렬 서비스만 수정하면 됩니다.
PlanFilterBuilder
검색 조건 생성 전담 클래스입니다.
담당 역할
GET 파라미터 분석
WHERE 절 생성
바인딩 변수 생성
예시
?network=KT
&qos=1Mbps
&lifetime=Y
위 조건을 SQL WHERE 절로 변환합니다.
향후 필터가 추가되어도 이 클래스만 수정하면 됩니다.
예를 들어
평생 할인 필터 추가
QoS 필터 추가
통화 무제한 필터 추가
같은 작업이 가능합니다.
PlanSearchRepository
데이터 조회 전용 클래스입니다.
담당 역할
목록 조회
총 개수 조회
통신사 수 조회
최신 수집 시간 조회
중요한 점은 SQL을 이곳에 집중시켰다는 것입니다.
예를 들어
JOIN 변경
SELECT 컬럼 추가
GROUP BY 수정
같은 작업은 Repository만 수정하면 됩니다.
PlanPromotedService
제휴 요금제 정책을 담당합니다.
일반 검색 로직과 성격이 다르기 때문에 별도로 분리했습니다.
담당 역할
제휴 통신사 판별
추천 요금제 선정
평균 가격 계산
노출 여부 결정
예를 들어
제휴 통신사 추가
추천 개수 변경
노출 정책 변경
등의 요구사항은 이 서비스만 수정하면 됩니다.
PlanApiGuard
보안 및 접근 제어 담당 클래스입니다.
담당 역할
CORS 처리
Origin 검증
API Key 검증
Rate Limit
예를 들어
허용 도메인 추가
요청 제한 변경
같은 작업은 Guard 클래스에서 처리합니다.
PlanApiBootstrap
프로젝트 초기화 담당 클래스입니다.
담당 역할
오토로딩
환경 설정
경로 처리
필수 클래스 검증
배포 환경이 변경되더라도 부트스트랩만 수정하면 됩니다.
PlanSearchService
검색 API의 전체 흐름을 조립하는 오케스트레이션 계층입니다.
현재 구조는 다음과 같습니다.
필터 생성
↓
정렬 생성
↓
Repository 조회
↓
응답 정규화
↓
추천 요금제 계산
↓
JSON 응답
실제 비즈니스 흐름만 담당하며 세부 구현은 각 서비스가 담당합니다.
제휴 요금제 서비스 개선
현재 추천 요금제 기능은 PlanPromotedService에 집중되어 있습니다.
예를 들어 기능 비활성화 시 즉시 종료합니다.
if (!(bool) ($settings['enabled'] ?? false)) {
return [];
}
또한 사용자가 선택한 검색 조건을 그대로 유지합니다.
$promotedWhere = $where;
예를 들어 사용자가
KT망
평생 할인
QoS 1Mbps
조건으로 검색했다면 추천 요금제 역시 동일 조건 내에서만 조회됩니다.
이를 통해 추천 결과가 일반 검색 결과와 일관성을 유지할 수 있습니다.
현재 구조에서 아쉬운 점
리팩토링 이후에도 개선할 부분은 존재합니다.
1. SELECT 구문 중복
현재 일반 검색과 추천 검색이 유사한 SELECT 구문을 사용하고 있습니다.
예시
SELECT
p.id,
c.code,
c.name,
...
향후에는 Repository 내부에 공통 메서드를 둘 예정입니다.
PlanSearchRepository::selectColumns()
이렇게 하면 SQL 중복을 더욱 줄일 수 있습니다.
2. 추천 Repository 분리
현재는 추천 서비스 내부에서 직접 SQL 조회를 수행하고 있습니다.
장기적으로는 아래 구조가 더 적절합니다.
PlanPromotedService
↓
PlanPromotedRepository
이렇게 되면
정책 변경 → Service
SQL 변경 → Repository
로 책임이 더욱 명확해집니다.
회귀 테스트 추가
리팩토링에서 가장 위험한 부분은 기존 동작이 변경되는 것입니다.
이를 방지하기 위해 회귀 테스트를 추가했습니다.
현재 테스트 항목은 다음과 같습니다.
기본 검색
SKT 검색
KT 검색
LGU+ 검색
평생 할인
100원 이하
평생 할인 + QoS 1Mbps
평생 할인 + QoS 5Mbps
실행 방법
php scripts/test_plans_api_regression.php
실행 결과
PASS default_discounted_fee
PASS network_skt
PASS network_kt
PASS network_lgu
PASS lifetime
PASS under_100_won
PASS lifetime_qos_1mbps
PASS lifetime_qos_5mbps
All API regression cases passed.
이제 리팩토링 이후에도 핵심 기능이 정상 동작하는지 빠르게 검증할 수 있습니다.
리팩토링 이후 얻은 변화
과거에는 수정 요청이 들어오면 다음과 같은 흐름이었습니다.
수정 요청
↓
plans.php 열기
↓
수천 줄 탐색
↓
수정
↓
사이드 이펙트 확인
현재는 다음과 같습니다.
정렬 수정
↓
PlanSortService
필터 수정
↓
PlanFilterBuilder
응답 수정
↓
PlanResponseService
추천 정책 수정
↓
PlanPromotedService
수정 위치가 명확해졌고 변경 범위 역시 크게 줄어들었습니다.
결론
이번 PHP API 리팩토링의 핵심은 파일 분리가 아닙니다.
진짜 목적은 변경 이유와 수정 위치를 일치시키는 것입니다.
SRP(단일 책임 원칙)를 기준으로 역할과 책임을 분리한 결과 다음과 같은 효과를 얻을 수 있었습니다.
- 유지보수성 향상
- 코드 가독성 개선
- 테스트 용이성 확보
- 기능 추가 부담 감소
- 사이드 이펙트 최소화
아직 Repository 세분화와 테스트 확대라는 과제가 남아 있지만, 현재 구조만으로도 초기의 거대한 단일 파일 구조와 비교하면 훨씬 관리하기 쉬운 상태가 되었습니다.
PHP API를 운영하면서 파일 하나에 기능이 계속 쌓이고 있다면, 단순히 코드를 정리하는 수준이 아니라 SRP 관점에서 역할과 책임을 다시 나누는 작업을 고려해보는 것도 좋은 선택이 될 수 있습니다.
하나의 변경 --> 하나의 파일
추천 태그
PHP,API리팩토링,SRP,단일책임원칙,PHP아키텍처,Repository패턴,Service패턴,요금제API,알뜰폰개발,백엔드개발,PHP개발,코드리팩토링,유지보수성,회귀테스트,API설계
'실무개발 > BackEnd' 카테고리의 다른 글
| 워드프레스 플러그인 제작하기: 숏코드, 관리자 설정, iframe 위젯까지 직접 구현하는 방법 (0) | 2026.06.21 |
|---|---|
| Docker로 워드프레스 설치하고 알뜰폰 위젯 플러그인 만들기 (0) | 2026.06.20 |
| 스프링부트 Playwright로 HTML을 PDF로 변환할 때 500 에러 해결 방법 (0) | 2026.06.17 |
| PHP7 → PHP8 마이그레이션 오류 모음 및 실무 해결 방법 총정리 (0) | 2026.06.12 |
| Spring Boot WebSocket을 Shared Worker로 운영하는 방법: 여러 탭 연결 최적화 실무 정리 (0) | 2026.06.09 |
