MyStory/Deployment and management

레거시 코드 리팩토링기: 매장 정보 관리 시스템 개선 사례

LupyLaon 2025. 6. 9. 10:16

들어가며

웰스토리 DID(Digital Information Display) 프로젝트에 중간 투입되면서 겪은 흥미로운 경험을 공유하고자 합니다. 처음에는 제 방식대로 코드를 작성했다가, 팀의 기존 코딩 스타일과 아키텍처를 이해하고 이에 맞춰 전면 리팩토링을 진행한 과정입니다.

 

🔍 기존 코드의 문제점들

1. 모델 클래스의 과도한 분산

// 수정 전: 용도별로 분산된 3개의 모델
public class RestaurantSaveModel { /* 저장용 */ }
public class RestaurantQueryModel { /* 조회용 */ }  
public class RestaurantResponseModel { /* 응답용 */ }

 

동일한 매장 정보를 다루는데도 불구하고 3개의 서로 다른 모델 클래스가 존재했습니다. 이는 코드 중복과 유지보수성 저하를 야기했습니다.

 

2. 수동 XSS 체크의 번거로움

// 수정 전: 각 필드마다 수동으로 XSS 체크
model.RESTAURANT_CD = CommonProperties.getXssString(model.RESTAURANT_CD);
model.RESTAURANT_NM = CommonProperties.getXssString(model.RESTAURANT_NM);
model.BRANCH_NAME = CommonProperties.getXssString(model.BRANCH_NAME);
// ... 반복

 

3. 복잡한 유효성 검사 로직

// 수정 전: 컨트롤러에서 하드코딩된 유효성 검사
if (string.IsNullOrEmpty(model.BRANCH_NAME))
{
    resultModel.ERR_CODE = "8001";
    resultModel.ERROR_MSG = "지점명을 입력해주세요.";
    return Content(JsonConvert.SerializeObject(resultModel, Formatting.Indented));
}

if (!RestaurantBiz.IsValidPhoneNumber(model.CONTACT_NUMBER))
{
    resultModel.ERR_CODE = "8001";
    resultModel.ERROR_MSG = "올바른 전화번호 형식이 아닙니다.";
    return Content(JsonConvert.SerializeObject(resultModel, Formatting.Indented));
}

 

4. 인라인 SQL과 비즈니스 로직 혼재

// 수정 전: Biz 레이어에서 직접 SQL 작성
const string sql = @"UPDATE DID.CMS_AUTH_REST SET 
                    STORE_NM = :STORE_NM,
                    TEL_NO = :TEL_NO,
                    OPERATING_HOURS = :OPERATING_HOURS,
                    HOLIDAY_HOURS = :HOLIDAY_HOURS,
                    MOD_ID = :MOD_ID,
                    MOD_DTM = SYSDATE
                    WHERE RESTAURANT_CODE = :RESTAURANT_CODE";

 

🛠️ 리팩토링 과정

1. 모델 통합 및 Data Annotations 적용

// 수정 후: 통합된 모델과 선언적 유효성 검사
public class RestaurantModel
{
    [Display(Name = "매장코드")]
    public string RESTAURANT_CODE { get; set; }
    
    [Required(ErrorMessage = "지점명을 입력해주세요.")]
    [Display(Name = "지점명")]
    public string STORE_NM { get; set; }
    
    [Required(ErrorMessage = "연락처를 입력해주세요.")]
    [RegularExpression(@"^(0\d{1,2}-\d{3,4}-\d{4}|01[016789]-\d{3,4}-\d{4}|070-\d{3,4}-\d{4}|1588-\d{4}|080-\d{3}-\d{4})$",
        ErrorMessage = "올바른 전화번호 형식이 아닙니다. (예: 02-1234-5678, 010-1234-5678)")]
    [Display(Name = "연락처")]
    public string TEL_NO { get; set; }
    
    [Required(ErrorMessage = "평일 운영시간을 입력해주세요.")]
    [Display(Name = "평일 운영시간")]
    public string OPERATING_HOURS { get; set; }
    
    [Required(ErrorMessage = "휴일 운영시간을 입력해주세요.")]
    [Display(Name = "휴일 운영시간")]
    public string HOLIDAY_HOURS { get; set; }
}

 

개선점:

  • 3개 모델을 2개로 통합 (조회용: RestaurantInfoModel, 저장용: RestaurantModel)
  • Data Annotations를 통한 선언적 유효성 검사
  • 정규표현식을 통한 전화번호 형식 검증

2. 저장 프로시저 도입

// 수정 후: 저장 프로시저 사용
public static ResultModel SaveRestaurantInfo(RestaurantModel model)
{
    OracleParameter[] param =
    {
        new OracleParameter("P_RESTAURANT_CODE", model.RESTAURANT_CODE),
        new OracleParameter("P_STORE_NM", model.STORE_NM),
        new OracleParameter("P_TEL_NO", model.TEL_NO),
        new OracleParameter("P_OPERATING_HOURS", model.OPERATING_HOURS),
        new OracleParameter("P_HOLIDAY_HOURS", model.HOLIDAY_HOURS),
        new OracleParameter("P_REG_ID", model.REG_ID),
        new OracleParameter("CUR", OracleDbType.RefCursor) { Direction = ParameterDirection.Output }
    };
    
    var result = OracleHelper.ExecuteDataset(CommonProperties.ConnectionString, 
        CommandType.StoredProcedure, 
        "DID.PKG_CMS_RESTAURANT_MNG.PKG_CMS_RESTAURANT_UPDATE", param);
    
    return Util.ConvertDataTable<ResultModel>(result.Tables[0])[0];
}

 

개선점:

  • 기존 팀의 저장 프로시저 패턴 준수
  • SQL Injection 방지
  • 데이터베이스 로직과 비즈니스 로직 분리

 

3. ModelState를 활용한 유효성 검사

// 수정 후: 간결한 컨트롤러 로직
[HttpPost]
public ActionResult SaveRestaurantInfo(RestaurantModel model)
{
    try
    {
        // ModelState 유효성 검사
        if (!ModelState.IsValid)
        {
            var errorMsg = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => e.ErrorMessage)
                .FirstOrDefault();
            
            return Json(new ResultModel
            {
                ERR_CODE = "8001",
                ERROR_MSG = "입력 데이터가 유효하지 않습니다: " + errorMsg
            });
        }

        // 비즈니스 로직 실행
        ResultModel result = RestaurantBiz.SaveRestaurantInfo(model);
        return Json(result);
    }
    catch (Exception ex)
    {
        return Json(new ResultModel
        {
            ERR_CODE = "8999",
            ERROR_MSG = "매장 정보 저장 중 예기치 못한 오류가 발생했습니다: " + ex.Message
        });
    }
}

 

4. 통합 엔드포인트 설계

// 수정 후: 키오스크용과 관리자용 통합
public static DataSet GetRestaurantInfo(RestaurantInfoModel model)
{
    OracleParameter[] param =
    {
        new OracleParameter("P_RESTAURANT_CODE", model.RESTAURANT_CODE),
        new OracleParameter("CUR", OracleDbType.RefCursor) { Direction = ParameterDirection.Output }
    };
    return OracleHelper.ExecuteDataset(CommonProperties.ConnectionString, 
        CommandType.StoredProcedure, 
        "DID.PKG_CMS_RESTAURANT_MNG.PR_USER_RESTAURANT_SELECT", param);
}

 

📊 개선 결과

정량적 개선

  • 코드 라인 수: 약 40% 감소
  • 모델 클래스: 3개 → 2개로 통합
  • 엔드포인트: 3개 → 2개로 통합
  • 유효성 검사 코드: 90% 감소

정성적 개선

  • 가독성: Data Annotations로 인한 직관적인 유효성 검사 규칙
  • 유지보수성: 중앙화된 모델과 저장 프로시저
  • 확장성: 새로운 필드 추가 시 최소한의 코드 변경
  • 일관성: 기존 팀 코딩 스타일과의 일치

🎯 얻은 교훈

1. 팀 코딩 스타일의 중요성

중간에 프로젝트에 투입될 때는 기존 아키텍처와 코딩 스타일을 먼저 파악하는 것이 중요합니다. 처음에는 제 방식대로 작성했다가, 팀의 기존 패턴을 이해하고 이에 맞춰 전면 수정하게 되었습니다.

2. 점진적 리팩토링의 효과

한 번에 모든 것을 바꾸려 하지 않고, 기존 구조를 이해한 후 단계적으로 개선하는 것이 더 효과적이었습니다.

3. 선언적 프로그래밍의 장점

Data Annotations를 활용한 선언적 유효성 검사는 코드의 가독성과 유지보수성을 크게 향상시켰습니다.