본문 바로가기
프로그래밍/개발 팁

[좋코vs나코] 제3편: "함수 하나가 백과사전?" - 단일 책임 원칙 (SRP)과 함수 설계 📜

by 다다면체 2025. 5. 16.
728x90
반응형

안녕하세요, 코딩 여정에 함께하는 동료 개발자 여러분! 😊 혹시 코드를 읽다가 거대한 함수를 마주하고 길을 잃은 듯한 느낌, 받아보신 적 있으신가요? 마치 한 권의 백과사전처럼 온갖 내용이 다 들어있는 함수 말이에요. 📜 오늘은 이런 '만능 함수'의 함정에서 벗어나, 코드를 훨씬 깔끔하고 관리하기 쉽게 만들어주는 마법 같은 원칙, 바로 **'단일 책임 원칙(Single Responsibility Principle, SRP)'**에 대해 이야기 나눠보려고 합니다. 특히 함수를 설계할 때 이 원칙을 어떻게 적용하는지 함께 알아보시죠! 🚀

반응형

❌ 이런 함수는 피해주세요! (SRP 위반 사례)

함수가 너무 많은 일을 하려고 하면 여러 문제가 발생할 수 있습니다. 마치 한 사람이 너무 많은 역할을 떠안으면 지치고 실수하는 것처럼 말이죠.

  1. 수백 줄짜리 '백과사전' 함수 함수 하나가 화면을 가득 채울 정도로 길다면 어떨까요? 📜 이름만 봐서는 정확히 어떤 작업을 하는지 짐작하기 어렵고, 전체 로직을 이해하려면 한참을 들여다봐야 합니다. 이런 함수는 그 자체로 "나중에 문제 생길 거예요!"라고 외치고 있는 것과 같습니다.
    • 가독성 최악: 코드를 이해하는 데 너무 많은 시간이 소요됩니다.
    • 디버깅 지옥: 어디서 버그가 발생했는지 찾기가 매우 어렵습니다. 😭
    • 수정의 두려움: 작은 부분을 수정해도 얘기치 않은 다른 부분에서 문제가 터질 수 있습니다. (사이드 이펙트!)
  2. 여러 기능을 한 번에! '만능 맥가이버칼' 함수 (God Function) 사용자 정보 조회부터 주문 처리, 이메일 발송, 로그 기록까지... 함수 하나가 너무 많은 책임을 지고 있는 경우입니다. 🛠️🤔 어떤 점이 문제일까요?
    • 높은 결합도: 하나의 기능 변경이 연관 없는 다른 기능에 영향을 줄 가능성이 큽니다.
    • 재사용성 저하: 이 함수 전체를 다른 곳에서 재사용하기는 거의 불가능합니다.
    • 테스트의 어려움: 너무 많은 것을 한 번에 테스트해야 해서 복잡하고 시간이 오래 걸립니다. 특정 부분만 테스트하기도 어렵고요.
  3. // 나쁜 예시: 만능 함수
    function handleOrder(orderId, userId) {
      // 1. 주문 정보 가져오기
      const order = db.getOrder(orderId);
      if (!order) {
        console.error("주문 없음");
        return; // 💣 여기서 끝? 에러 처리는?
      }
    
      // 2. 사용자 정보 가져오기
      const user = db.getUser(userId);
      if (!user) {
        console.error("사용자 없음");
        return; // 💣
      }
    
      // 3. 재고 확인 및 업데이트
      if (!inventory.checkAndUpdate(order.items)) {
        console.error("재고 부족");
        // 롤백 로직 필요 (생략)
        return; // 💣
      }
    
      // 4. 결제 처리
      const paymentSuccess = payment.process(user, order.total);
      if (!paymentSuccess) {
        console.error("결제 실패");
        // 롤백 로직 필요 (생략)
        return; // 💣
      }
    
      // 5. 이메일 알림 발송
      email.sendOrderConfirmation(user.email, order);
    
      // 6. 로그 기록
      logger.log("주문 성공: " + orderId);
      console.log("주문 처리 완료!");
    }
    
  4. 끝없이 들어가는 '인셉션' 함수 (깊은 들여쓰기) if-else 문이나 반복문이 여러 겹으로 중첩되어 코드의 논리 흐름을 파악하기 매우 어려운 함수입니다. 겹겹이 쌓인 코드 안으로 빠져드는 느낌이죠. 🌀🤔 어떤 점이 문제일까요?
    • 가독성 저하: 로직을 한눈에 따라가기가 너무 힘듭니다.
    • 실수 유발: 조건을 하나라도 잘못 이해하면 예상치 못한 결과가 발생할 수 있습니다.
    • 유지보수 어려움: 새로운 조건을 추가하거나 기존 로직을 수정하기가 매우 까다롭습니다.
      // 나쁜 예시: 깊은 들여쓰기
      function processData(data, userType, config) {
        if (data) {
          if (data.isValid) {
            if (userType === "admin") {
              // ... 로직 1 ...
              if (config.isProduction) {
                // ... 로직 2 ...
                return "어드민 프로덕션 처리 완료";
              } else {
                // ... 로직 3 ...
                return "어드민 개발 처리 완료";
              }
            } else if (userType === "member") {
              // ... 로직 4 ...
              if (config.sendNotification) {
                // ... 로직 5 ...
                // ... 알림 발송 ...
                return "멤버 알림 발송 완료";
              } else {
                return "멤버 처리 완료 (알림 없음)";
              }
            } else {
              return "알 수 없는 사용자 타입"; // 😖
            }
          } else {
            return "유효하지 않은 데이터"; // 😖
          }
        } else {
          return "데이터 없음"; // 😖
        }
      }
      

✅ 이렇게 함수를 설계해보세요! (SRP 준수 사례)

그렇다면 어떻게 함수를 잘 만들 수 있을까요? 해답은 '단일 책임 원칙'을 따르는 것입니다!

  1. 한 가지 일만 똑 부러지게! (함수 분리) 위에서 봤던 '만능 맥가이버칼' 함수 handleOrder를 SRP에 따라 여러 개의 작고 명확한 함수로 분리해봅시다.💡 어떤 점이 좋을까요?
    • 명확성: 각 함수는 자신의 책임만 다하므로 코드를 이해하기 훨씬 쉽습니다.
    • 유지보수 용이성: 수정이 필요할 때 해당 함수만 집중해서 보면 되므로 다른 부분에 미치는 영향을 최소화할 수 있습니다.
    • 재사용성 향상: getUserDetails 같은 함수는 주문 처리 외 다른 곳에서도 유용하게 쓰일 수 있습니다.
      // 좋은 예시: 기능별로 분리된 함수들
      
      // 각 함수는 한 가지 책임만 가집니다.
      function getOrderDetails(orderId) {
        const order = db.getOrder(orderId);
        if (!order) throw new Error(`주문 정보(${orderId})를 찾을 수 없습니다.`); // ✅ 명확한 에러
        return order;
      }
      
      function getUserDetails(userId) {
        const user = db.getUser(userId);
        if (!user) throw new Error(`사용자 정보(${userId})를 찾을 수 없습니다.`); // ✅
        return user;
      }
      
      function checkAndUpdateStock(items) {
        if (!inventory.checkAndUpdate(items)) {
          // 필요시, 여기에서 관련 롤백 로직을 수행하거나 구체적인 예외를 던질 수 있습니다.
          throw new Error("재고가 부족하거나 업데이트에 실패했습니다."); // ✅
        }
        // 성공 시 명시적으로 반환하거나, 반환 없이 정상 종료
      }
      
      function processPaymentForOrder(user, totalAmount) {
        const paymentSuccess = payment.process(user, totalAmount);
        if (!paymentSuccess) throw new Error("결제에 실패했습니다."); // ✅
      }
      
      function sendOrderConfirmationEmail(emailAddress, orderDetails) {
        // 이메일 발송 실패 시 어떻게 처리할지 정책에 따라 구현 (예: 로깅, 재시도 큐)
        email.sendOrderConfirmation(emailAddress, orderDetails);
      }
      
      function logOrderSuccess(orderId) {
        logger.log("주문 성공: " + orderId);
      }
      
      // 개선된 메인 처리 로직: 이제 각 단계를 명확히 호출합니다.
      function handleOrder_SRP(orderId, userId) {
        try {
          const order = getOrderDetails(orderId);
          // 주문 정보에 userId가 포함되어 있다고 가정하거나, 별도 파라미터로 받습니다.
          const user = getUserDetails(order.userId || userId); 
      
          checkAndUpdateStock(order.items);
          processPaymentForOrder(user, order.total);
          sendOrderConfirmationEmail(user.email, order);
          logOrderSuccess(orderId);
      
          console.log("주문 처리 완료!");
          return { success: true, orderId: orderId }; // ✅ 명확한 결과 반환
        } catch (error) {
          console.error(`주문 처리 중 오류 발생 (주문 ID: ${orderId}): ${error.message}`);
          // 여기서 공통 에러 처리 또는 구체적인 롤백 로직 호출
          // 예: initiateOrderRollback(orderId, error);
          return { success: false, error: error.message }; // ✅ 실패 시에도 명확한 결과
        }
      }
      
  2. 명확한 입력(매개변수)과 출력(반환 값) 함수가 무엇을 받고(매개변수), 그 결과로 무엇을 반환하는지 명확해야 합니다. 마치 잘 설계된 자판기처럼요! 🪙🥤
    • 예측 가능성: 함수의 사용법을 쉽게 알 수 있고, 어떻게 동작할지 예측하기 쉬워집니다.
    • 부수 효과(Side Effect) 최소화: 함수가 외부 상태를 직접 변경하기보다, 입력값을 받아 처리 후 결과를 반환하는 순수 함수(Pure Function)에 가깝게 만들면 좋습니다.
  3. 테스트는 식은 죽 먹기! 🥣 (테스트 용이성) 함수가 작고 한 가지 일만 한다면, 마치 레고 블록 하나하나를 살펴보듯 테스트 코드를 작성하기 매우 쉬워집니다.
    • 단위 테스트 용이: 각 기능을 독립적으로, 그리고 쉽게 테스트할 수 있어 버그를 조기에 발견하고 코드의 안정성을 크게 높일 수 있습니다.
    • 신뢰성 향상: 테스트가 잘 된 코드는 변경에 대한 자신감을 줍니다.

🎯 핵심은 이겁니다, 여러분!

"함수는 작게, 한 놈만 팬다! 🥊"

함수가 하나의 책임만 갖도록 명확하게 정의하고, 그 책임에만 집중하도록 만들어야 합니다. 이것이 바로 단일 책임 원칙의 핵심입니다.

단일 책임 원칙을 함수 설계에 적용하는 것은 처음에는 조금 어색하고 일이 더 많아지는 것처럼 느껴질 수 있습니다. 🤔 하지만 장기적으로는 코드의 가독성, 유지보수성, 테스트 용이성을 크게 향상시켜 결국 우리를 행복하게 만들어 줄 거예요. 😊 오늘부터 여러분의 함수를 한번 SRP의 눈으로 살펴보시는 건 어떨까요? 작은 변화가 코드 전체의 품질을 높이는 마법을 경험하게 되실 겁니다! ✨

728x90
반응형