프로그래밍 핸드북: 사이드 이펙트 (Side Effect)

프로그래밍에서 ‘사이드 이펙트’는 함수의 본래 목적인 ‘입력을 받아 출력을 계산하는 것’ 외에 프로그램의 상태나 외부 세계에 미치는 모든 변화를 의미. 이는 마치 약을 복용했을 때 주된 치료 효과 외에 나타나는 부수적인 효과(부작용)와 비슷함.

1. 만들어진 이유: 왜 ‘사이드 이펙트’라는 개념이 중요한가?

소프트웨어의 복잡성이 증가하면서 코드의 동작을 예측하고 테스트하는 것이 매우 중요해짐. 사이드 이펙트는 코드의 예측 가능성을 떨어뜨리는 주된 원인.

  • 비유로 이해하기: 자판기

    • 순수한 자판기 (Pure Function): 1000원 버튼을 누르면 항상 콜라가 나옴. 입력(1000원)이 같으면 출력(콜라)도 항상 같음. 이 자판기는 다른 어떤 일도 하지 않음.

    • 사이드 이펙트가 있는 자판기 (Function with Side Effects): 1000원 버튼을 눌러 콜라가 나오는 것은 같지만, 동시에 본사에 ‘콜라 재고 -1’ 신호를 보내고, 화면의 추천 메뉴를 바꾸며, 구매 기록을 로그 파일에 저장함. 콜라를 내주는 것 외의 모든 부수적인 행동이 바로 사이드 이펙트.

이처럼 사이드 이펙트가 있는 코드는 눈에 보이지 않는 곳에서 상태를 바꾸기 때문에, 같은 입력을 주더라도 프로그램의 전체적인 상태나 다른 부분의 실행 결과에 따라 다른 결과를 낳을 수 있음. 이는 버그의 온상이 되기 쉬움. 따라서 프로그래머들은 사이드 이펙트를 명확히 인식하고 관리하여 코드의 안정성을 높이고자 함.

2. 구조: 사이드 이펙트란 정확히 무엇인가?

사이드 이펙트를 이해하기 위해 먼저 **순수 함수(Pure Function)**의 개념을 알아야 함.

순수 함수 (Pure Function)

다음 두 가지 조건을 만족하는 함수.

  1. 동일한 입력에 대해 항상 동일한 출력을 반환. (No surprises)

  2. 함수 외부의 어떤 상태도 변경하지 않음. (No side effects)

// 순수 함수 예시
function add(a, b) {
  return a + b; // 입력 a, b에만 의존하며 외부 상태를 바꾸지 않음
}

사이드 이펙트의 종류

순수 함수의 조건을 깨뜨리는 모든 행위가 사이드 이펙트에 해당함.

  • 외부 상태 변경 (Modifying External State)

    • 전역 변수나 함수 외부의 변수를 수정하는 행위.

    • 객체나 배열을 인자로 받아 그 내용을 직접 수정하는 행위.

    let globalValue = 10;
    
    function multiplyBy(num) {
      globalValue = globalValue * num; // 사이드 이펙트: 전역 변수를 수정함
      return globalValue;
    }
    
    
  • 외부 세계와의 상호작용 (Interacting with the Outside World)

    • 데이터베이스에 데이터를 읽거나 쓰는 행위.

    • 네트워크 API를 호출하는 행위.

    • 콘솔(console)에 로그를 출력하거나 사용자 입력을 받는 행위.

    • 파일 시스템에서 파일을 읽거나 쓰는 행위.

  • 비결정적(Non-deterministic) 동작

    • 함수 호출 시점에 따라 결과가 달라지는 경우.

    • new Date()Math.random()처럼 실행할 때마다 다른 값을 반환하는 함수를 사용하는 것.

3. 사용법: 사이드 이펙트를 어떻게 다룰 것인가?

사이드 이펙트는 무조건 나쁜가?

아님. 사실상 모든 유용한 프로그램은 사이드 이펙트를 가짐. 화면에 무언가를 표시하고(사이드 이펙트), 사용자 입력을 받고(사이드 이펙트), 서버에 데이터를 저장하는(사이드 이펙트) 행위 없이는 프로그램이 의미가 없음.

문제는 사이드 이펙트 그 자체가 아니라, 통제되지 않는 사이드 이펙트임. 핵심은 사이드 이펙트를 코드의 나머지 부분과 명확히 분리하고 격리하여 관리하는 것.

관리 전략

  1. 사이드 이펙트 분리 (Separation)

    • 프로그램의 핵심 로직(계산, 비즈니스 규칙)은 가능한 순수 함수로 작성.

    • 데이터베이스 접근, 네트워크 요청 등 사이드 이펙트를 일으키는 코드는 프로그램의 가장자리(entry/exit points)로 밀어내어 별도로 관리.

    • 예시: 계산 로직과 출력 로직을 분리.

    // 1. 계산 로직 (순수 함수)
    function createGreetingMessage(name) {
      return `Hello, ${name}!`;
    }
     
    // 2. 사이드 이펙트가 발생하는 부분 (출력)
    function showGreeting(name) {
      const message = createGreetingMessage(name);
      console.log(message); // 사이드 이펙트 발생 지점
    }
     
  2. 참조 투명성 (Referential Transparency) 활용

    • 순수 함수는 ‘참조 투명성’을 가짐. 이는 함수 호출 부분을 그 함수의 반환 값으로 대체해도 프로그램의 동작이 변하지 않음을 의미.

    • add(2, 3)은 항상 5이므로, 코드에서 add(2, 3)5로 바꿔도 아무 문제가 없음.

    • 코드가 참조 투명성을 가질수록 이해하고 테스트하기 쉬워짐. 사이드 이펙트는 이 참조 투명성을 깨뜨림.

  3. 멱등성 (Idempotency) 확보

    • ‘멱등성’은 연산을 여러 번 수행해도 결과가 한 번 수행한 것과 같은 상태를 의미.

    • 예를 들어, ‘사용자 ID 123 삭제’ 요청은 여러 번 보내도 첫 번째 요청에서 사용자가 삭제된 후에는 더 이상 상태가 변하지 않으므로 멱등성을 가짐.

    • 네트워크 오류 등으로 재시도가 필요한 사이드 이펙트를 다룰 때 멱등성은 시스템을 안정적으로 만드는 중요한 속성.

4. 심화: 함수형 프로그래밍의 관점

함수형 프로그래밍에서는 사이드 이펙트를 ‘불순함(impurity)‘으로 간주하고 이를 다루기 위한 정교한 기법(모나드, 펑터 등)을 사용. 이는 사이드 이펙트 자체를 값으로 다루어, 언제 어떻게 발생시킬지 프로그래머가 완벽하게 통제하도록 돕는 방식. 이 접근법은 코드 전체의 예측 가능성을 극대화함.

결론적으로, 사이드 이펙트는 유용한 프로그램을 만들기 위한 필수악. 성공적인 프로그래머는 사이드 이펙트를 피하는 것이 아니라, 그것을 명확히 식별하고 격리하여 코드베이스 전체를 깨끗하고 예측 가능하게 유지하는 사람.

이제 순수 함수와 사이드 이펙트의 차이가 명확해졌나요? 다음으로 어떤 프로그래밍 개념에 대해 알아보고 싶으신가요?

프로그래밍 사이드 이펙트 핸드북

1. 만들어진 이유: 왜 사이드 이펙트를 이해해야 하는가?

컴퓨터 프로그램은 본질적으로 상태(State) 를 변경하는 작업을 수행. 변수 값을 바꾸거나, 파일을 쓰거나, 화면에 무언가를 표시하는 모든 행위가 상태 변경에 해당.

사이드 이펙트는 이처럼 함수의 주된 목적 외에 프로그램의 상태를 변경시키는 모든 부수적인 효과를 의미. 마치 약을 먹었을 때 치료 효과 외에 예상치 못한 다른 반응이 나타나는 것과 유사.

초기 프로그래밍에서는 이러한 사이드 이펙트를 명확히 구분하지 않았지만, 프로그램의 규모가 커지고 복잡해지면서 문제가 발생. 예상치 못한 곳에서 상태가 변경되면 버그를 찾기 어렵고, 코드의 동작을 예측하기 힘들어짐.

따라서 사이드 이펙트를 명확히 이해하고 관리하는 것은 안정적이고 예측 가능한 소프트웨어를 만들기 위한 핵심적인 과제. 특히 여러 작업이 동시에 실행되는 병렬 처리 환경에서는 사이드 이펙트 관리가 더욱 중요.

2. 구조: 사이드 이펙트는 어떻게 구성되는가?

사이드 이펙트는 크게 ‘순수함(Purity)’ 이라는 개념과 반대되는 지점에서 발생.

  • 순수 함수 (Pure Function)

    • 동일한 입력에 대해 항상 동일한 출력을 반환.

    • 사이드 이펙트가 없음. 즉, 함수 외부의 어떤 상태도 변경하지 않음.

    • 예시: 두 숫자를 더하는 함수 add(a, b)는 입력된 a와 b 값에만 의존하며, 외부 변수나 상태를 바꾸지 않음.

  • 비순수 함수 (Impure Function)

    • 입력이 같아도 다른 출력을 반환할 수 있음.

    • 사이드 이펙트를 발생시킴.

    • 예시: 현재 시간을 반환하는 함수, 파일을 읽거나 쓰는 함수, 데이터베이스에 데이터를 저장하는 함수 등.

사이드 이펙트의 주요 발생 원인:

  1. 외부 상태 변경: 함수 외부의 변수나 객체의 값을 수정.

  2. I/O 작업: 파일 시스템, 네트워크, 데이터베이스 등 외부 시스템과 상호작용.

  3. 화면 출력: 콘솔이나 UI에 데이터를 표시.

  4. 예외(Exception) 발생: 프로그램의 정상적인 흐름을 중단시킴.

3. 사용법: 사이드 이펙트 다루기

사이드 이펙트는 필요악과 같음. 프로그램이 의미 있는 작업을 하려면 외부 세계와 소통해야 하므로 완전히 없앨 수는 없음. 중요한 것은 ‘관리’ 하는 것.

관리 전략:

  1. 격리 (Isolation): 사이드 이펙트를 일으키는 코드를 프로그램의 특정 영역으로 분리. 프로그램의 대부분은 순수 함수로 구성하고, 사이드 이펙트가 필요한 부분은 명확히 구분하여 관리.

    • 비유: 주방에서 요리할 때 재료 손질, 조리, 설거지 공간을 분리하여 효율을 높이는 것과 같음.
  2. 명시적 처리 (Explicitness): 함수 이름이나 타입을 통해 해당 함수가 사이드 이펙트를 일으킬 수 있음을 명확히 표시.

    • 예시: 함수 이름에 saveUserToDatabase처럼 구체적인 동작을 명시.
  3. 함수형 프로그래밍 패러다임 활용:

    • 불변성(Immutability): 데이터가 생성된 후에는 변경하지 않는 원칙. 원본 데이터를 바꾸는 대신 새로운 데이터를 생성하여 반환. 이는 예상치 못한 상태 변경을 원천적으로 차단.

    • 모나드(Monad): 사이드 이펙트를 포함하는 계산을 순차적으로 연결하고 조합할 수 있는 디자인 패턴. 복잡한 사이드 이펙트를 안전하게 다루는 구조를 제공. (예: Promise, Optional)

코드 예시 (JavaScript):

// 사이드 이펙트가 있는 경우 (전역 변수 변경)
let total = 0;
function addToTotal(num) {
  total += num; // 사이드 이펙트: 함수 외부의 'total' 변수를 변경
  return total;
}
 
// 사이드 이펙트를 제거한 경우 (순수 함수)
function add(currentTotal, num) {
  return currentTotal + num; // 외부 상태를 변경하지 않고, 계산 결과만 반환
}
 
let newTotal = add(total, 5); // 함수 호출자는 반환된 값을 사용해 상태를 직접 업데이트

4. 심화: 사이드 이펙트와 아키텍처

현대 소프트웨어 아키텍처는 사이드 이펙트를 효과적으로 관리하기 위해 설계됨.

  • 계층형 아키텍처 (Layered Architecture):

    • 표현 계층 (Presentation Layer): UI 렌더링 등 화면과 관련된 사이드 이펙트 처리.

    • 비즈니스 계층 (Business Layer): 대부분 순수 함수로 구성된 핵심 로직.

    • 데이터 접근 계층 (Data Access Layer): 데이터베이스, 파일 시스템 등 I/O 관련 사이드 이펙트 처리.

  • CQRS (Command Query Responsibility Segregation):

    • 명령 (Command): 시스템의 상태를 변경하는 작업 (사이드 이펙트 발생).

    • 조회 (Query): 시스템의 상태를 읽는 작업 (사이드 이펙트 없음).

    • 상태를 변경하는 책임과 조회하는 책임을 명확히 분리하여 시스템의 복잡성을 낮추고 예측 가능성을 높임.

이처럼 사이드 이펙트를 단순히 피해야 할 대상으로 보는 것을 넘어, 어떻게 잘 구조화하고 관리할 것인가의 관점에서 접근하는 것이 중요.

이 핸드북이 사이드 이펙트에 대한 이해를 넓히는 데 도움이 되었기를 바랍니다.

혹시 사이드 이펙트 관리와 관련하여 특정 프로그래밍 언어나 프레임워크에서 사용되는 기법에 대해 더 궁금한 점이 있으신가요?

사이드 이펙트(Side Effect) 핸드북

1. 만들어진 이유: 왜 ‘부수 효과’라는 개념이 생겨났을까?

컴퓨터 과학의 초기, 프로그램은 주로 명령형 프로그래밍(Imperative Programming) 방식으로 작성되었습니다. 이는 컴퓨터에게 “무엇을” 할지보다 “어떻게” 할지를 순서대로 명령하는 방식입니다. 예를 들어, “변수 a에 5를 더하고, 그 결과를 변수 b에 저장한 후, b의 값을 화면에 출력하라”와 같이 구체적인 절차를 나열합니다.

이 방식은 직관적이지만, 프로그램의 규모가 커지면서 문제가 발생하기 시작했습니다. 여러 함수가 공유된 상태(Shared State), 즉 전역 변수나 같은 메모리 공간을 동시에 수정하면서 예측 불가능한 결과들이 나타나기 시작한 것입니다.

함수를 호출했는데, 예상했던 반환값 외에 프로그램의 다른 부분에 예상치 못한 변경이 일어나는 현상. 이것이 바로 사이드 이펙트입니다. 마치 약을 먹었는데, 병을 고치는 주된 효과 외에 두통이나 소화불량 같은 부수적인 효과가 나타나는 것과 같습니다.

이러한 문제를 해결하고 프로그램의 동작을 더 쉽게 예측하고 제어하기 위해, 함수형 프로그래밍과 같은 패러다임에서는 사이드 이펙트를 최소화하고 **순수 함수(Pure Function)**를 지향하는 개념이 중요해졌습니다.

2. 구조: 사이드 이펙트는 어떻게 구성되는가?

사이드 이펙트는 함수가 자신의 스코프(Scope) 바깥에 있는 무언가와 상호작용할 때 발생합니다. 함수의 주된 임무는 입력값을 받아 계산 후 결과값을 반환하는 것이지만, 이 과정 외에 다른 일을 할 때 사이드 이펙트가 생깁니다.

주요 사이드 이펙트 유형

  • 전역 상태 변경 (Modifying Global State): 함수 바깥에 선언된 변수나 객체의 값을 직접 수정하는 경우.

    let globalValue = 10;
    
    function addAndModify(num) {
      // 사이드 이펙트: 함수 외부의 globalValue를 직접 변경
      globalValue += num;
      return num + 5;
    }
    
  • 입출력 (I/O - Input/Output): 파일 시스템을 읽거나 쓰고, 데이터베이스에 쿼리를 보내고, 네트워크를 통해 API를 호출하거나, 콘솔에 로그를 출력하는 모든 행위.

    def save_user_to_db(user_data):
        # 사이드 이펙트: 데이터베이스에 데이터를 저장 (외부 시스템과 상호작용)
        db.save(user_data)
        print("User saved!") # 이것 또한 콘솔 출력이라는 사이드 이펙트
    
  • DOM 조작 (DOM Manipulation): 웹 개발에서 JavaScript 함수가 HTML 문서의 구조나 스타일을 직접 변경하는 경우.

  • 인자로 받은 객체 수정 (Mutating Arguments): 함수에 참조(Reference)로 전달된 객체나 배열의 원본을 직접 수정하는 경우.

    function addToArray(arr, item) {
      // 사이드 이펙트: 인자로 받은 배열 원본을 직접 수정
      arr.push(item);
      return arr;
    }
    

이와 반대되는 개념이 바로 **순수 함수(Pure Function)**입니다. 순수 함수는 다음과 같은 두 가지 조건을 만족합니다.

  1. 동일한 입력에 대해 항상 동일한 출력을 반환한다.

  2. 사이드 이펙트가 없다. (함수 외부의 어떤 상태도 변경하지 않는다.)

3. 사용법: 사이드 이펙트, 어떻게 다뤄야 할까?

사이드 이펙트는 버그의 온상이 될 수 있지만, 현실적으로 모든 사이드 이펙트를 없앨 수는 없습니다. 화면에 무언가를 보여주고, 사용자 입력을 받고, 데이터를 저장하는 등 프로그램이 세상과 소통하려면 반드시 필요하기 때문입니다.

중요한 것은 사이드 이펙트를 **“관리”하고 “격리”**하는 것입니다.

관리 전략

  1. 최대한 순수 함수로 만들기: 핵심적인 비즈니스 로직은 순수 함수로 작성하여 테스트와 예측이 쉽게 만듭니다.

  2. 사이드 이펙트 격리하기: 사이드 이펙트가 꼭 필요한 코드는 프로그램의 특정 계층이나 모듈로 분리합니다. 예를 들어, 데이터베이스와 통신하는 코드는 ‘Repository’ 계층에, API 호출 코드는 ‘ApiService’ 모듈에 모아두는 식입니다. 이렇게 하면 프로그램의 나머지 부분은 사이드 이펙트로부터 안전하게 유지됩니다.

  3. 불변성(Immutability) 활용: 원본 데이터를 직접 수정하는 대신, 데이터의 복사본을 만들어 변경하고 새로운 결과물을 반환합니다. 이는 예기치 않은 상태 변경을 막는 강력한 방법입니다.

    // 나쁜 예: 원본 배열을 수정 (사이드 이펙트 발생)
    const originalArray = [1, 2, 3];
    function addItem_bad(arr, item) {
      arr.push(item);
      return arr;
    }
    
    // 좋은 예: 새로운 배열을 반환 (사이드 이펙트 없음)
    const originalArray2 = [1, 2, 3];
    function addItem_good(arr, item) {
      return [...arr, item]; // 스프레드 문법으로 복사본 생성
    }
    

4. 심화 내용: 더 깊이 알아보기

  • 참조 투명성 (Referential Transparency): 어떤 표현식(Expression)을 그 표현식의 결과값으로 대체해도 프로그램의 동작에 아무런 영향이 없다면 ‘참조에 투명하다’고 말합니다. 순수 함수는 항상 참조 투명성을 만족합니다. 이는 코드의 추론을 매우 쉽게 만들어 줍니다.

  • 멱등성 (Idempotence): 같은 연산을 여러 번 수행해도 결과가 한 번 수행한 것과 같은 특성. 예를 들어, HTTP PUT이나 DELETE 요청은 멱등성을 가집니다. 사이드 이펙트를 다룰 때, 특히 네트워크 요청과 같은 비동기 작업에서 멱등성을 보장하면 시스템의 안정성이 크게 향상됩니다.

  • 동시성 문제 (Concurrency Issues): 여러 스레드나 프로세스가 공유된 자원을 동시에 수정하려고 할 때 사이드 이펙트는 ‘경쟁 상태(Race Condition)‘와 같은 심각한 문제를 일으킵니다. 사이드 이펙트를 최소화하고 상태 변경을 신중하게 관리하는 것이 동시성 프로그래밍의 핵심입니다.

사이드 이펙트를 이해하고 제어하는 능력은 깨끗하고, 유지보수하기 쉽고, 확장 가능한 소프트웨어를 만드는 데 필수적인 기술입니다.

이 핸드북이 사이드 이펙트라는 개념을 이해하는 데 도움이 되셨나요? 혹시 특정 프로그래밍 언어에서 사이드 이펙트를 관리하는 구체적인 방법에 대해 더 궁금한 점이 있으신가요?