cheoly's language study blog

📝 C 언어 함수 포인터와 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴

프로그래밍/C언어
반응형
SMALL

C 언어에서 함수를 변수처럼 다루는 '함수 포인터'와 이를 활용한 '콜백 함수'의 모든 것을 파헤칩니다. 코드의 유연성을 극대화하고, 재사용 가능한 모듈을 설계하는 핵심 비법을 실제 예제와 함께 자세히 설명합니다. (키워드: C언어, 함수 포인터, 콜백 함수, 디자인 패턴, 코드 재사용성, 고차 함수)

C 언어 콜백 함수 구조: 함수 포인터를 사용해 메인 로직에서 비교 및 정렬 함수를 동적으로 호출하는 다이어그램

1. 서론: 함수, 그 이상의 활용 - C언어의 유연성을 깨우다

C 언어는 강력하고 효율적인 프로그래밍 언어로 널리 사용되지만, 때로는 그 절차지향적 특성 때문에 코드의 유연성이 부족하다고 느끼는 경우가 있습니다. 하지만 C 언어에도 함수를 마치 변수처럼 다루고, 필요할 때 원하는 함수를 실행시키는 강력한 메커니즘이 존재합니다. 바로 **'함수 포인터(Function Pointer)'**와 이를 활용한 **'콜백 함수(Callback Function)'**입니다.

이번 글에서는 C 언어의 함수 포인터가 무엇인지, 어떻게 선언하고 사용하는지, 그리고 이 함수 포인터가 실제 프로그래밍에서 '콜백 함수'라는 강력한 디자인 패턴으로 어떻게 활용되는지 자세히 살펴보겠습니다. 이를 통해 여러분의 C 언어 코드를 더욱 유연하고 재사용 가능하게 만드는 핵심 비법을 깨우치게 될 것입니다.

2. 함수 포인터: 함수를 가리키는 포인터

우리가 변수의 주소를 저장하기 위해 포인터 변수를 사용하듯이, 함수 포인터는 함수의 메모리 주소를 저장하는 포인터입니다. 이를 통해 우리는 실행 시점에 어떤 함수를 호출할지 동적으로 결정할 수 있게 됩니다.

2.1. 함수 포인터 선언 방법

함수 포인터를 선언하는 문법은 일반 변수 포인터보다 조금 복잡해 보일 수 있습니다. 핵심은 '반환형 (*포인터변수이름)(매개변수 타입, ...)' 형태를 기억하는 것입니다.

// 1. 반환형이 int이고 매개변수가 int 두 개인 함수의 포인터 선언
int (*ptr_to_func)(int, int);

// 예시 함수
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    ptr_to_func = &add; // add 함수의 주소를 함수 포인터에 할당
    // 또는 ptr_to_func = add; (C에서는 &를 생략해도 됩니다)

    int result = ptr_to_func(10, 5); // 함수 포인터를 통해 add 함수 호출
    printf("Add result: %d\n", result); // 출력: Add result: 15

    ptr_to_func = subtract; // subtract 함수의 주소를 할당
    result = ptr_to_func(10, 5); // 함수 포인터를 통해 subtract 함수 호출
    printf("Subtract result: %d\n", result); // 출력: Subtract result: 5

    return 0;
}

2.2. typedef를 이용한 간결한 선언

함수 포인터 선언이 복잡하게 느껴진다면, typedef를 사용하여 별칭을 부여하면 훨씬 간결하고 가독성 높게 코드를 작성할 수 있습니다.

// int를 반환하고 int 두 개를 매개변수로 받는 함수 포인터 타입 정의
typedef int (*OperationFunc)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    OperationFunc op_ptr; // typedef로 정의된 타입 사용

    op_ptr = add;
    printf("Add result: %d\n", op_ptr(20, 10)); // 출력: Add result: 30

    op_ptr = subtract;
    printf("Subtract result: %d\n", op_ptr(20, 10)); // 출력: Subtract result: 10

    return 0;
}

3. 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴

이제 함수 포인터의 진정한 가치를 발휘하는 **'콜백 함수(Callback Function)'**에 대해 알아보겠습니다. 콜백 함수는 특정 이벤트가 발생하거나, 특정 작업이 완료되었을 때 시스템이나 다른 함수가 호출하도록 등록해 둔 함수를 의미합니다. 즉, "나중에 호출해줘!" 하고 맡겨두는 함수입니다.

3.1. 콜백 함수의 개념과 동작 원리

콜백 함수는 다음과 같은 방식으로 작동합니다.

  1. 호출 함수(Caller Function): 어떤 작업을 수행하는 함수가 있습니다. 이 함수는 작업 중간 또는 완료 시점에 특정 기능을 수행해야 하는데, 그 기능이 고정되어 있지 않고 외부에서 주입되기를 기대합니다.
  2. 콜백 함수 (Callback Function): 호출 함수가 나중에 실행할 '특정 기능'을 정의한 함수입니다. 이 함수의 주소를 호출 함수에게 전달합니다.
  3. 함수 포인터 (The Bridge): 호출 함수는 전달받은 콜백 함수의 주소를 함수 포인터에 저장하고 있다가, 필요한 시점에 이 포인터를 통해 콜백 함수를 호출합니다.

이러한 구조를 통해 호출 함수는 **'어떤 기능'을 실행할지 몰라도 '기능을 실행해야 할 시점'**만 알면 됩니다. 이는 코드의 모듈화재사용성을 극대화합니다.

3.2. 콜백 함수 실제 예제: 배열 정렬 (버블 정렬)

가장 대표적인 콜백 함수의 예는 정렬 함수입니다. qsort()와 같은 표준 라이브러리 함수가 대표적이지만, 여기서는 직접 간단한 버블 정렬 함수를 만들면서 콜백의 위력을 보여드리겠습니다.

우리는 **'오름차순'**으로 정렬할 수도 있고, **'내림차순'**으로 정렬할 수도 있습니다. 이 **'정렬 기준'**을 콜백 함수로 전달하여 버블 정렬 함수를 유연하게 만들어 봅시다.

#include <stdio.h>

// int 두 개를 비교하여 int 값을 반환하는 함수 포인터 타입 정의
// 반환값이 음수: a < b
// 반환값이 양수: a > b
// 반환값이 0: a == b
typedef int (*CompareFunc)(int a, int b);

// 1. 오름차순 비교 함수 (a가 b보다 작으면 음수 반환)
int compare_asc(int a, int b) {
    return a - b;
}

// 2. 내림차순 비교 함수 (a가 b보다 크면 음수 반환)
int compare_desc(int a, int b) {
    return b - a; // 순서를 바꿔서 빼면 내림차순
}

// 3. 콜백 함수를 사용하는 범용 버블 정렬 함수
// 배열, 배열 크기, 비교 함수 포인터를 매개변수로 받음
void bubble_sort(int arr[], int size, CompareFunc comparer) {
    int i, j, temp;
    for (i = 0; i < size - 1; i++) {
        for (j = 0; j < size - 1 - i; j++) {
            // 전달받은 comparer 함수를 사용하여 두 요소를 비교
            if (comparer(arr[j], arr[j+1]) > 0) { // arr[j]가 arr[j+1]보다 크다면 (정렬 기준에 따라)
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

// 배열 출력 함수
void print_array(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int size = sizeof(arr) / sizeof(arr[0]);

    printf("Original array: ");
    print_array(arr, size);

    // 오름차순으로 정렬
    bubble_sort(arr, size, compare_asc);
    printf("Sorted (Ascending): ");
    print_array(arr, size); // 출력: 11 12 22 25 34 64 90 

    // 배열을 다시 초기화하고 내림차순으로 정렬
    int arr_desc[] = {64, 34, 25, 12, 22, 11, 90};
    bubble_sort(arr_desc, size, compare_desc);
    printf("Sorted (Descending): ");
    print_array(arr_desc, size); // 출력: 90 64 34 25 22 12 11 

    return 0;
}

위 예제에서 bubble_sort 함수는 compare_asc와 compare_desc 함수를 콜백 함수로 받아서, 동일한 정렬 로직으로 오름차순/내림차순 정렬을 모두 수행합니다. bubble_sort는 **'무엇을 비교할지'**에 대해서는 전혀 모르고 **'비교하는 방법(콜백)'**만 전달받아 유연하게 동작합니다.

4. 콜백 함수의 다양한 활용 분야

콜백 함수는 위 예제 외에도 C 언어 프로그래밍의 다양한 곳에서 활용됩니다.

  • 이벤트 처리: GUI 프로그래밍에서 버튼 클릭, 키보드 입력 등 특정 이벤트 발생 시 호출될 함수를 등록할 때 (예: glutDisplayFunc in OpenGL)
  • 비동기 작업: 네트워크 통신이나 파일 입출력처럼 시간이 오래 걸리는 작업이 완료되었을 때 결과를 처리할 함수를 등록할 때
  • 제네릭 라이브러리: 정렬(qsort), 탐색(bsearch)과 같은 범용 라이브러리에서 사용자가 정의한 비교 함수를 콜백으로 받아 다양한 데이터 타입에 적용할 때
  • 하드웨어 제어: 임베디드 시스템에서 특정 인터럽트 발생 시 처리할 함수를 등록할 때

5. 결론: 함수 포인터와 콜백, C언어의 숨겨진 힘

C 언어의 함수 포인터콜백 함수는 언어가 제공하는 강력한 유연성과 확장성을 보여주는 핵심 개념입니다. 이는 단순한 문법을 넘어, 코드를 더욱 모듈화하고 재사용성을 높이며, 다양한 시나리오에 동적으로 대응할 수 있도록 하는 강력한 디자인 패턴입니다.

처음에는 문법이 다소 어렵게 느껴질 수 있지만, 이번 글의 예제를 통해 그 원리와 활용법을 충분히 익히셨기를 바랍니다. 함수 포인터와 콜백 함수를 자유자재로 다루게 된다면, 여러분의 C 언어 프로그래밍 실력은 한 단계 더 도약할 것입니다.


[🔔 다음 글 예고: C언어 데이터 구조의 꽃, 링크드 리스트 완전 정복!] 다음 글에서는 함수 포인터와 더불어 C 언어의 핵심인 데이터 구조 중 가장 기본적이면서도 강력한 **'링크드 리스트(Linked List)'**에 대해 깊이 파고들 예정입니다. 동적인 데이터 관리가 필요한 여러분에게 필수적인 지식이 될 것입니다. 기대해주세요!

반응형
LIST

이중 포인터로 2차원 배열 다루기 – 개념부터 실전 사용까지

프로그래밍/C언어
반응형
SMALL

C로 코딩하다 보면 2차원 배열을 함수로 넘기거나 동적으로 만들고 싶은데,
int arr[3][4]; 만으로는 뭔가 막히는 느낌이 올 때가 있다.
이때 자주 등장하는 게 바로 이중 포인터(int **) 다.

이 글에서는

  1. 정적 2차원 배열과 이중 포인터의 차이
  2. 왜 int arr[][4]는 되는데 int **는 안 되는지
  3. int **로 2차원 배열처럼 동적 할당하는 방법
  4. free 할 때 주의점, 자주 하는 실수

까지 한 번에 정리해본다.

이중 포인터를 시각적으로 표현한 다이어그램. 깊은 파란 배경 위에 **p , *p₀ , p₀₀ , p₀₁ 같은 요소들이 박스와 화살표로 연결된 구조로 배치되어 있으며, 포인터에서 포인터로 이어지는 참조 관계를 직관적으로 보여주는 프로그래밍 개념 이미지


1. 2차원 배열과 포인터의 기본 관계

먼저 익숙한 2차원 배열부터 보자.

int arr[3][4];
  • 타입: “4개의 int로 이루어진 배열이 3개
  • arr 의 타입은: int [3][4]
  • 포인터로 decay(함수 인자로 전달 등)될 때 타입: int (*)[4]
    → “int 4개짜리 배열을 가리키는 포인터”

즉, arr는 **“배열의 배열”**이고,
arr가 포인터처럼 쓰일 때는 **“한 행을 가리키는 포인터”**가 된다.

int (*p)[4] = arr; // OK
p[0][1] = 10;      // arr[0][1]과 동일

여기서 중요한 포인트:

정적 2차원 배열 int arr[3][4] 는 int **가 아니라
int (*)[4] 타입으로 취급된다.

그래서 함수 인자로 받을 때도 이렇게 해야 한다.

void print_matrix(int arr[][4], int rows);
// 또는
void print_matrix(int (*arr)[4], int rows);

void print_matrix(int **arr, int rows, int cols); 로 받으면
정적 2차원 배열을 그대로 넘겨서는 안 맞는다.


2. 이중 포인터 int ** 는 뭔가?

int ** 는 말 그대로

“int를 가리키는 포인터를 가리키는 포인터”

구조를 그림으로 그리면 이런 느낌이다.

  • int **pp : “행 포인터들을 모아둔 배열(또는 영역)을 가리킨다”
  • pp[i] : i번째 행을 가리키는 int *
  • pp[i][j] : i번째 행, j번째 열의 int

즉, **“포인터들의 배열 + 각 행을 별도로 할당한 구조”**라고 보면 된다.

그래서 int ** 를 제대로 쓰려면
할당도 2단계로 해줘야 한다.


3. int ** 로 2차원 배열처럼 동적 할당하기

예를 들어 rows x cols 크기의 int “2차원 배열처럼” 쓰고 싶다고 하자.

3-1. 할당

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int rows = 3;
    int cols = 4;

    // 1단계: 행 포인터 배열 할당
    int **arr = malloc(rows * sizeof(int *));
    if (arr == NULL) {
        perror("malloc");
        return 1;
    }

    // 2단계: 각 행마다 int 배열 할당
    for (int i = 0; i < rows; i++) {
        arr[i] = malloc(cols * sizeof(int));
        if (arr[i] == NULL) {
            perror("malloc");
            // 이미 할당한 부분 정리 후 종료
            for (int k = 0; k < i; k++) {
                free(arr[k]);
            }
            free(arr);
            return 1;
        }
    }

    // 이제 arr[i][j] 로 접근 가능
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr[i][j] = i * cols + j;
        }
    }

    // 출력 테스트
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%2d ", arr[i][j]);
        }
        printf("\n");
    }

    // 3단계: 해제
    for (int i = 0; i < rows; i++) {
        free(arr[i]);  // 각 행 해제
    }
    free(arr);         // 행 포인터 배열 해제

    return 0;
}

이 구조의 특징:

  • 메모리가 연속적이지 않을 수 있다.
    각 행을 따로 malloc 했기 때문.
  • 대신 행 크기를 서로 다르게 할당하는 것도 가능하다. (jagged array)

예:

arr[0] = malloc(3 * sizeof(int)); // 3열
arr[1] = malloc(10 * sizeof(int)); // 10열
// 이런 식으로 ‘삐뚤빼뚤한’ 2차원 구조도 가능

 

4. 정적 2차원 배열을 함수에 넘길 때 vs int **

정적 2차원 배열을 함수에 넘기고 싶을 땐:

void print_matrix(int rows, int cols, int arr[][4]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main(void) {
    int arr[3][4] = {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    };

    print_matrix(3, 4, arr);
}

또는 C99 이상이라면 가변 길이 배열(VLA) 를 써서

void print_matrix(int rows, int cols, int arr[rows][cols]);

이렇게도 가능하다.

반면, int ** 로 받은 함수는 int ** 로 동적 할당한 데이터만 안전하게 처리해야 한다.

void print_matrix_dyn(int rows, int cols, int **arr);

여기에 int arr[3][4]; 를 그대로 넘기면
타입이 맞지 않고, 심지어 우연히 돌아가도 UB(정의되지 않은 동작) 이 될 수 있다.


5. 이중 포인터로 2차원 배열을 다룰 때 자주 하는 실수

❌ 1) 한 번만 malloc 하고 int ** 로 캐스팅

int **arr = malloc(rows * cols * sizeof(int)); // 잘못된 패턴

이렇게 한 덩어리로 할당해 놓고 arr[i][j] 로 쓰는 건
정상적인 int ** 구조가 아니다.

이 경우 arr 는 사실상 int *로 취급되어야 한다.

int *arr = malloc(rows * cols * sizeof(int));
arr[i * cols + j] = 10;

처럼 1차원 배열 + 인덱스 계산으로 써야 한다.

❌ 2) free 순서를 잘못 처리

int ** 구조에서는

  1. 먼저 각 행(arr[i])을 free
  2. 마지막에 arr 자체를 free

순서가 중요하다.

for (int i = 0; i < rows; i++) {
    free(arr[i]);   // 행 해제
}
free(arr);          // 행 포인터 배열 해제

반대로 먼저 free(arr); 를 해버리면
그 뒤의 arr[i] 접근은 이미 해제된 메모리에 접근하는 셈이라 위험하다.


6. 언제 int ** 를 쓰는 게 좋을까?

  • 행마다 크기가 다른 2차원 구조가 필요할 때
  • 런타임에 행 수, 열 수가 결정되는 2차원-like 데이터를 다룰 때
  • 재사용 가능한 라이브러리 코드에서
    “2차원처럼 보이는 포인터 배열”을 받도록 설계할 때

반면,

  • 크기가 고정이고
  • 메모리가 한 덩어리로 연속되어 있는 게 좋다면

int arr[ROWS][COLS]; 또는

int *arr = malloc(rows * cols * sizeof(int));

처럼 1차원 + 수동 인덱싱 방식이 더 단순할 수 있다.


마무리 정리

  • 정적 2차원 배열 int arr[3][4] 는
    int (*)[4] 로 decay되지, int ** 가 아니다.
  • int ** 는 “포인터들의 배열” 구조이므로
    행 포인터 배열 + 각 행 메모리를 따로 할당해야 한다.
  • int **로 할당한 건 int ** 로만 다루고,
    정적 배열은 int (*)[COL] 또는 int arr[][COL] 로 받자.
  • free 할 때는 행 먼저, 마지막에 상위 포인터를 해제하는 순서를 지키기.
반응형
LIST

C 언어 포인터 완벽 정리 – 이 글 하나로 끝내기

프로그래밍/C언어
반응형
SMALL

파이썬만 쓰다가 다시 C 언어를 보면 제일 먼저 막히는 게 바로 포인터(pointer)다.
개념만 애매하게 잡고 넘어가면, 나중에 구조체·동적 메모리·파일 입출력·함수 포인터에서 계속 발목을 잡는다.

이 글에서는

  • 포인터가 메모리에서 어떻게 동작하는지
  • *, &, 배열, malloc, NULL과의 관계
  • 자주 터지는 실수와 디버깅 포인트

까지 한 번에 정리해본다.

C 언어 포인터를 설명하는 일러스트로, 큰 C 로고와 코드 창에 'int p' 문구가 표시되어 있으며 포인터 구조를 나타내는 다이어그램이 포함된 그래픽


1. 포인터란 무엇인가?

포인터 = “값” 대신 “주소”를 저장하는 변수

일반 변수는 값(value) 을 저장하고,
포인터 변수는 값이 저장된 메모리 주소(address) 를 저장한다.

int x = 10;      // 값 10을 저장하는 int 변수
int *p = &x;     // x가 저장된 "주소"를 저장하는 포인터 변수
  • &x : 변수 x주소
  • p : 그 주소를 저장하는 포인터 변수
  • *p : 포인터 p가 가리키는 주소에 있는

즉,

  • x → 값: 10
  • &x → 주소: 예) 0x1000
  • p → 주소: 0x1000
  • *p → 값: 10

2. 포인터 선언과 기본 문법

2-1. 기본 선언

int *p;      // int 를 가리키는 포인터
char *cp;    // char 를 가리키는 포인터
double *dp;  // double 을 가리키는 포인터
  • *“포인터 타입이다” 라는 의미
  • int *p, q; 처럼 쓰면 p만 포인터고, q는 그냥 int 이다 → 조심!
int* p, q;   // 혼동의 시작: p는 int*, q는 int
int *p, *q;  // 둘 다 포인터

2-2. 포인터에 주소 대입

int x = 42;
int *p = &x;   // x의 주소를 p에 저장
  • 항상 같은 타입끼리 맞춰야 한다.
  • int* 포인터에는 int 변수의 주소만 넣는다.

3. &* 정확히 이해하기

3-1. & 연산자: 주소 연산자

int x = 10;
printf("%p\n", (void*)&x);  // x가 저장된 주소 출력
  • &x : 변수 x메모리에 있는 위치

3-2. * 연산자: 간접 참조(역참조)

int x = 10;
int *p = &x;

printf("%d\n", *p);  // 10
*p = 20;             // x의 값을 20으로 변경
printf("%d\n", x);   // 20
  • p는 “주소”를 저장
  • *p는 그 주소에 있는 “값”에 접근

✔ 포인터를 사용할 때 항상 “지금 이게 주소인지, 인지”를 구분해서 생각하면 헷갈림이 많이 줄어든다.


4. 포인터와 배열의 관계

C에서 배열 이름은 상수 포인터 비슷하게 동작한다.

int arr[3] = {10, 20, 30};

printf("%p\n", (void*)arr);     // arr[0]의 주소
printf("%p\n", (void*)&arr[0]); // 동일한 주소

4-1. 배열 → 포인터 변환

int *p = arr;    // 사실은 &arr[0] 와 같은 의미
printf("%d\n", p[1]);    // 20
printf("%d\n", *(p + 1)); // 20
  • arr[i]*(arr + i)
  • p[i]*(p + i)

4-2. 하지만 배열과 포인터는 완전히 같지는 않다

int arr[3] = {1, 2, 3};
int *p = arr;

sizeof(arr); // 3 * sizeof(int) => 배열 전체 크기
sizeof(p);   // 포인터 변수 크기 (보통 4 또는 8바이트)
  • 배열: 메모리에 연속된 공간 + 크기가 컴파일 시에 결정
  • 포인터: 그 공간을 가리키는 주소만 저장

5. 포인터 연산(pointer arithmetic)

포인터에 +1을 하면 주소가 1 증가하는 게 아니라
타입 크기만큼 이동한다.

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30
  • 만약 sizeof(int) == 4라면,
    p + 1 은 실제 주소로 +4 만큼 이동한 것이다.

✔ 그래서 포인터 연산은 배열 순회에 매우 유용하다.


6. NULL 포인터와 초기화

포인터는 사용 전에 반드시 초기화해야 한다.
초기화되지 않은 포인터는 “쓰레기 주소”를 가리키며,
접근 시 바로 세그멘테이션 폴트(segmentation fault) 로 이어질 수 있다.

int *p = NULL;   // 초기화
if (p == NULL) {
    // 아직 유효한 주소가 없음
}
  • NULL 은 “아무 것도 가리키지 않는다”는 의미의 특별한 주소 값
  • 동적 할당 실패 시에도 보통 NULL을 반환한다.

7. 동적 메모리와 포인터 (malloc, free)

힙(heap) 영역에 메모리를 할당할 때 포인터가 필수로 등장한다.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int) * 3);  // int 3개 공간 할당

    if (p == NULL) {
        // 메모리 할당 실패
        return 1;
    }

    p[0] = 10;
    p[1] = 20;
    p[2] = 30;

    printf("%d %d %d\n", p[0], p[1], p[2]);

    free(p);   // 반드시 해제!
    return 0;
}
  • malloc : 메모리를 할당하고 그 첫 번째 바이트의 주소를 반환
  • free : 더 이상 사용하지 않을 때 반드시 해제해야 함
  • malloc 으로 받은 주소를 잃어버리면 → 메모리 누수(memory leak)

8. 이중 포인터(double pointer) 맛보기

이중 포인터는 “포인터를 가리키는 포인터”다.

int x = 10;
int *p = &x;   // x를 가리키는 포인터
int **pp = &p; // p를 가리키는 포인터

printf("%d\n", **pp);   // 10

언제 쓰냐?

  • 포인터를 함수 인자로 넘겨서 그 포인터 자체를 변경하고 싶을 때
  • 2차원 배열처럼 데이터를 관리할 때
  • 동적 할당된 배열의 배열을 다룰 때

이건 글이 길어지니, 이중 포인터만 따로 글 하나를 파도 될 정도다.


9. 함수 인자와 포인터: call by value vs call by reference 흉내

C는 무조건 call by value지만, 포인터를 사용해서
마치 “참조 호출”처럼 동작하게 만들 수 있다.

#include <stdio.h>

void add_one(int *p) {
    (*p)++;
}

int main(void) {
    int x = 10;
    add_one(&x);    // x의 주소를 넘김
    printf("%d\n", x);  // 11
    return 0;
}
  • 함수는 p라는 포인터의 복사본을 받지만
  • 그 포인터가 가리키는 원래 변수 x를 직접 수정할 수 있다.

파이썬의 “mutable 객체를 함수에서 수정하는 느낌”과 비슷하다고 보면 이해가 편하다.


10. 포인터에서 자주 하는 실수들

10-1. 초기화 안 된 포인터 사용

int *p;       // 초기화 안 됨 (쓰레기 값)
*p = 10;      // 세그폴트 가능성 100%

→ 반드시 NULL 또는 유효한 주소로 초기화.


10-2. 해제 후 포인터를 계속 사용 (use-after-free)

int *p = malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p);   // 정의되지 않은 동작(UB)

free(p); p = NULL; 패턴을 습관처럼 넣는 게 좋다.


10-3. 배열 범위 밖 접근

int arr[3] = {1, 2, 3};
int *p = arr;

printf("%d\n", p[3]);  // 쓰지 말 것! 범위 밖 접근

→ C는 범위 체크를 안 해준다.
디버깅 어려운 버그의 주요 원인.


10-4. 잘못된 캐스팅

int x = 10;
double *dp = (double*)&x;  // 타입이 전혀 다른 포인터

이런 코드는 가능하면 쓰지 않는 게 좋다.
메모리 레이아웃을 완전히 이해하고 쓸 필요가 있을 때만, 조심해서 사용.


11. 포인터를 진짜 이해했는지 스스로 체크해보기

아래 질문들에 스스로 답해볼 수 있다면,
포인터는 “완벽까지는 아니어도 꽤 잘 이해한 상태”다.

  1. int *pint (*p)[3] 의 차이를 설명할 수 있는가?
  2. arr, &arr[0], &arr 의 차이를 말할 수 있는가?
  3. malloc으로 2차원 배열을 만드는 코드를 직접 짤 수 있는가?
  4. NULL 포인터와 초기화되지 않은 포인터의 차이를 구분하는가?
  5. 함수 인자로 포인터를 넘겨서, 원래 변수 값을 바꾸는 코드를 직접 짤 수 있는가?

이 글에서 다룬 것은 그 중에서도 가장 기초이자 필수적인 부분이다.
다음 글에서는 이 내용을 바탕으로

  • 이중 포인터로 2차원 배열 다루기
  • 함수 포인터와 콜백 구조

를 이어서 정리해볼 예정이다.

 

 

반응형
LIST

파이썬 머신러닝 기초: 입문자가 꼭 알아야 할 핵심 개념 정리

프로그래밍/파이썬
반응형
SMALL

AI 열풍이 계속되면서 머신러닝은 더 이상 개발자만의 영역이 아니다.
이제는 기획자, 데이터 담당자, 심지어 일반 직장인도 머신러닝 기초를 배우고 활용하는 시대다.

오늘은 파이썬으로 머신러닝을 시작할 때 반드시 알고 있어야 하는 핵심 개념 5가지를 정리해본다.
코드를 몰라도 이해할 수 있도록 직관적으로 설명했다.

파이썬 머신러닝 기초를 설명하는 썸네일 이미지로, 파이썬 로고와 체크리스트, 상승 그래프, ‘파이썬 머신러닝 기초’라는 한국어 문구가 포함된 교육용 일러스트.


1. 머신러닝이란 무엇인가?

머신러닝(Machine Learning)은
데이터를 통해 스스로 패턴을 학습하고 예측하는 기술이다.

간단히 말하면,

“정답을 직접 알려주는 게 아니라
많은 예시를 보고 스스로 규칙을 찾는 프로그램”

예:

  • 사진을 보고 고양이/강아지를 구분
  • 보험 고객의 이탈 확률 예측
  • 주가 데이터로 상승/하락 예측

파이썬에서는 주로 Scikit-Learn 라이브러리를 이용해 머신러닝을 구현한다.


2. 머신러닝의 3대 구성 요소

머신러닝은 아래 세 가지가 핵심이다.

✔ 1) 데이터 (Data)

모델이 학습할 자료.
엑셀, CSV, DB, API 등 다양한 형태로 존재한다.

✔ 2) 모델(Model)

데이터 패턴을 학습하는 알고리즘.
예: 선형 회귀, 의사결정나무, 랜덤포레스트, SVM 등

✔ 3) 평가(Evaluation)

모델이 얼마나 똑똑한지 측정하는 단계.
예: 정확도, RMSE, F1 Score 등


3. 머신러닝의 종류: 지도학습 vs 비지도학습

① 지도학습(Supervised Learning)

정답(Label)이 있는 데이터를 학습하는 방식.

대표 알고리즘

  • 선형 회귀
  • 로지스틱 회귀
  • 랜덤 포레스트
  • SVM

예:
“고객 정보 → 구매 여부(정답) 예측”

② 비지도학습(Unsupervised Learning)

정답 없이 패턴을 그룹화하는 방식.

대표 알고리즘

  • K-Means
  • PCA

예:
“고객들을 비슷한 그룹끼리 묶어 분석하기(클러스터링)”


4. 파이썬으로 머신러닝 시작하는 기본 코드 구조

Scikit-Learn에서는 대부분 아래 구조를 그대로 따라가면 된다.

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import pandas as pd

# 1. 데이터 불러오기
df = pd.read_csv('data.csv')

X = df.drop('target', axis=1)  # 입력값
y = df['target']               # 정답

# 2. 학습/테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 3. 모델 불러오기
model = LogisticRegression()

# 4. 학습
model.fit(X_train, y_train)

# 5. 예측
pred = model.predict(X_test)

# 6. 평가
print("정확도:", accuracy_score(y_test, pred))

 

이 기본 형태만 이해해도 왠만한 머신러닝 튜토리얼은 80% 이상 읽히기 시작한다.


5. 입문자들이 자주 하는 실수 3가지

❌ 1) 데이터 전처리 없이 바로 모델 돌리기

결측치·이상치가 있으면 정확도는 심각하게 떨어진다.

❌ 2) 평가 데이터(test_set)를 학습에 섞는 실수

이건 진짜 흔한 실수.
평가용 데이터는 절대 모델 학습에 사용하면 안 된다.

❌ 3) 모델 정확도만 보고 “좋다/나쁘다” 판단

정확도가 높아도 불균형 데이터에서는 잘못된 판단이 나오기 때문에
F1 Score, ROC-AUC도 함께 봐야 한다.


마무리: 머신러닝은 ‘코드 암기’보다 ‘흐름 이해’가 먼저다

파이썬 머신러닝은 생각보다 어렵지 않다.
중요한 건 모델의 흐름과 개념을 이해하는 것이다.

  • 데이터 준비
  • 학습/검증 분리
  • 모델 선택
  • 학습
  • 예측
  • 평가

이 구조만 머리에 들어오면,
Scikit-Learn은 마치 “레고 조립하듯” 조립해서 사용할 수 있다.

다음 글에서는
가장 많이 사용하는 머신러닝 모델 5개 + 간단 코드를 정리해줄 예정!

반응형
LIST

AI로 자동 요약 & 이메일 본문 작성 시스템, 하루 만에 MVP

프로그래밍/파이썬
반응형
SMALL

업무 보고서(PDF/DOCX)를 자동으로 읽고 핵심만 요약한 뒤, 수신자/상황에 맞춘 이메일 초안을 자동 생성하는 파이프라인을 Python과 LLM으로 구현합니다. 파일 파싱 → 청크 분할 → 요약 → 이메일 템플릿 생성 → 발송 전 검토까지 한 번에.

PDF 보고서가 AI 로봇을 거쳐 이메일로 변환되는 과정을 나타낸 일러스트. 중앙의 AI 로봇 아이콘을 중심으로 양쪽에 보고서와 이메일 아이콘이 배치되어 있으며, 아래에는 ‘AI로 자동 요약 & 이메일 본문 작성 시스템, 하루 만에 MVP’라는 문구가 적혀 있는 이미지.

1) 목표

  • 보고서(회의록/리포트)를 투입하면 요약본이메일 제목/본문 초안이 자동 생성.
  • 사용자는 검토만 하고 전송.
  • 재현 가능한 CLI 스크립트로 배치 실행(스케줄러 연동).

2) 아키텍처 개요

  1. 수집: input/ 폴더의 PDF, DOCX, TXT 로드
  2. 파싱: PDF → 텍스트, DOCX → 텍스트
  3. 전처리: 문단 기준 분할, 길이 제한(토큰/문자) 맞춰 청킹
  4. 요약: LLM으로 청크 요약 → 메타 요약(최종 TL;DR)
  5. 이메일 생성: 수신자/맥락/톤을 조건으로 제목+본문 작성
  6. 출력: output/yyyymmdd_xxx/summary.md, email_draft.md 저장
input/                  # 원본 보고서 위치
output/
  2025-11-11_0930/      # 실행 시각별 결과 폴더
    summary.md
    email_draft.md
config/
  profile.yaml          # 이메일 톤/수신자/금칙어/서명 등

3) 준비물

  • Python 3.10+
  • 패키지: pypdf2(또는 pdfminer.six), python-docx, tqdm, pyyaml, (선택) tenacity 재시도
  • LLM 제공자 SDK (예: OpenAI/Anthropic 등) 또는 로컬 모델(HuggingFace)
    코드는 모델-중립 인터페이스로 제공 (플러그 교체식)
pip install PyPDF2 python-docx tqdm pyyaml tenacity
# (사용 LLM에 맞춰 SDK 추가 설치)

4) 핵심 로직

  • 청킹 전략: 문단 기준 800~1200자(한글) 또는 400~800 토큰 단위
  • 요약 2단계: (1) 청크별 압축 요약 → (2) 청크 요약들을 다시 합쳐 메타 요약
  • 이메일 생성: 요약 + 수신자 역할 + 톤(격식/친근/임원 보고) + CTA(다음 액션) 반영

5) 예시 코드 (모델-중립)

아래는 “LLM 클라이언트” 인터페이스만 갈아끼우면 동작하는 구조예요.
실제 호출 부분은 YourLLM 클래스에서 LLM SDK에 맞게 구현하세요.

 

# file: ai_report_mail_mvp.py
import os, re, glob, datetime, textwrap, yaml
from dataclasses import dataclass
from typing import List
from tqdm import tqdm

# ---------- LLM 인터페이스(여기만 실제 SDK로 교체) ----------
class YourLLM:
    def __init__(self, model_name: str = "your-model"):
        self.model_name = model_name
    def complete(self, prompt: str, max_tokens: int = 1024) -> str:
        # TODO: 여기에 사용 LLM SDK 호출 코드 작성
        # e.g., OpenAI/Anthropic/HF 텍스트 생성
        # return client.generate(prompt, ...)
        raise NotImplementedError("Connect your LLM provider here.")

# ---------- 파일 파서 ----------
def read_txt(path: str) -> str:
    return open(path, "r", encoding="utf-8", errors="ignore").read()

def read_docx(path: str) -> str:
    from docx import Document
    doc = Document(path)
    return "\n".join([p.text for p in doc.paragraphs])

def read_pdf(path: str) -> str:
    from PyPDF2 import PdfReader
    reader = PdfReader(path)
    texts = []
    for page in reader.pages:
        texts.append(page.extract_text() or "")
    return "\n".join(texts)

def load_text(path: str) -> str:
    ext = os.path.splitext(path)[1].lower()
    if ext == ".txt":
        return read_txt(path)
    if ext == ".docx":
        return read_docx(path)
    if ext == ".pdf":
        return read_pdf(path)
    raise ValueError(f"Unsupported file type: {ext}")

# ---------- 전처리/청킹 ----------
def clean_text(s: str) -> str:
    s = s.replace("\r\n", "\n").replace("\r", "\n")
    s = re.sub(r"\n{3,}", "\n\n", s)
    return s.strip()

def split_paragraphs(s: str) -> List[str]:
    paras = [p.strip() for p in s.split("\n\n") if p.strip()]
    return paras

def chunk_by_chars(paras: List[str], max_chars: int = 1200) -> List[str]:
    chunks, buf = [], []
    size = 0
    for p in paras:
        if size + len(p) + 2 > max_chars and buf:
            chunks.append("\n\n".join(buf))
            buf, size = [], 0
        buf.append(p)
        size += len(p) + 2
    if buf:
        chunks.append("\n\n".join(buf))
    return chunks

# ---------- 요약 프롬프트 ----------
SUMMARY_PROMPT = """당신은 간결하고 정확한 전문 비서입니다.
아래 보고서 청크를 5줄 이내 핵심 bullet로 요약하고, 수치/결정/담당자/일정을 보존하세요.
가능하면 '결정사항/리스크/액션아이템' 세 영역으로 나눠주세요.

[청크]
{chunk}
"""

META_SUMMARY_PROMPT = """아래는 보고서의 부분 요약들입니다.
중복을 제거하고 전사에게 공유할 수 있는 최종 요약으로 8줄 이내로 통합하세요.
'핵심 요지', '결정사항', '리스크', '향후 일정/요청사항'을 소제목으로 구분하세요.

[부분 요약들]
{chunk_summaries}
"""

EMAIL_PROMPT = """당신은 한국어 비즈니스 이메일 작성 전문가입니다.
아래 최종 요약을 바탕으로 수신자({recipient_role})에게 보낼 메일 초안을 작성하세요.

조건:
- 제목: 1줄, 50자 이내, 핵심 키워드 포함
- 본문: 6~12줄, 결론 먼저(핵심/결정/요청 → 근거), 불필요한 수식어 금지
- 톤: {tone}
- CTA: 수신자가 취해야 할 다음 행동 2~3개 bullet
- 금칙어: {banned_words}

[최종 요약]
{final_summary}
"""

# ---------- 메인 파이프라인 ----------
@dataclass
class Profile:
    recipient_role: str = "팀장"
    tone: str = "격식 있고 간결하게"
    banned_words: str = "죄송;부탁;최대한;어쨌든"

def summarize_chunks(llm: YourLLM, chunks: List[str]) -> List[str]:
    results = []
    for c in tqdm(chunks, desc="Summarizing chunks"):
        prompt = SUMMARY_PROMPT.format(chunk=c)
        results.append(llm.complete(prompt))
    return results

def meta_summarize(llm: YourLLM, chunk_summaries: List[str]) -> str:
    joined = "\n\n---\n\n".join(chunk_summaries)
    prompt = META_SUMMARY_PROMPT.format(chunk_summaries=joined)
    return llm.complete(prompt, max_tokens=800)

def generate_email(llm: YourLLM, final_summary: str, profile: Profile) -> str:
    prompt = EMAIL_PROMPT.format(
        recipient_role=profile.recipient_role,
        tone=profile.tone,
        banned_words=profile.banned_words,
        final_summary=final_summary,
    )
    return llm.complete(prompt, max_tokens=800)

def ensure_dir(path: str):
    os.makedirs(path, exist_ok=True)

def save_text(path: str, content: str):
    with open(path, "w", encoding="utf-8") as f:
        f.write(content.strip() + "\n")

def main():
    # 1) 설정 로드
    cfg_path = "config/profile.yaml"
    if os.path.exists(cfg_path):
        cfg = yaml.safe_load(open(cfg_path, "r", encoding="utf-8"))
        profile = Profile(**cfg)
    else:
        profile = Profile()

    # 2) 입력 파일 수집
    files = []
    for ext in ("*.pdf", "*.docx", "*.txt"):
        files.extend(glob.glob(os.path.join("input", ext)))
    if not files:
        print("No input files in ./input")
        return

    # 3) LLM 준비 (여기 구현)
    llm = YourLLM(model_name="your-model")

    # 4) 출력 폴더
    stamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M")
    outdir = os.path.join("output", stamp)
    ensure_dir(outdir)

    # 5) 파일별 처리
    all_chunk_summaries = []
    for fp in files:
        raw = load_text(fp)
        txt = clean_text(raw)
        chunks = chunk_by_chars(split_paragraphs(txt), max_chars=1200)
        chunk_sums = summarize_chunks(llm, chunks)
        # 파일별 요약 저장(옵션)
        save_text(os.path.join(outdir, f"{os.path.basename(fp)}.chunksum.md"),
                  "\n\n---\n\n".join(chunk_sums))
        all_chunk_summaries.extend(chunk_sums)

    # 6) 메타 요약
    final_summary = meta_summarize(llm, all_chunk_summaries)
    save_text(os.path.join(outdir, "summary.md"), final_summary)

    # 7) 이메일 초안 생성
    email_md = generate_email(llm, final_summary, profile)
    save_text(os.path.join(outdir, "email_draft.md"), email_md)

    print(f"Done. See: {outdir}")

if __name__ == "__main__":
    main()

config/profile.yaml 예시

recipient_role: "본부장"
tone: "임원 보고 톤으로 간결하고 단정하게"
banned_words: "죄송;부탁;최대한;아무래도;일단"

6) LLM 연결 힌트

  • 위 YourLLM.complete()에 사용 중인 LLM SDK 호출만 채워 넣으면 됩니다.
  • 토큰 제한이 작은 모델은 max_chars를 낮추세요(예: 800자).

7) 배치/자동화

  • Windows: 작업 스케줄러에서 python ai_report_mail_mvp.py 매일 08:30 실행
  • macOS/Linux: crontab -e
30 8 * * 1-5 /usr/bin/python3 /path/ai_report_mail_mvp.py

8) 품질 팁

  • 금칙어/톤/CTA는 반드시 profile.yaml로 관리(조직별 가이드 반영).
  • 민감정보(인명/금액)는 요약 보존 규칙을 프롬프트에 명시.
  • 요약 정확도 검증을 위해 샘플 원문 → 요약 대조 체크리스트 운용.

9) 확장 아이디어

  • 수신자별 템플릿(영업/개발/임원) 스위치
  • 다국어 이메일(ko→en) 동시 생성
  • 메일 API 연동(초안 자동 업로드까지만, 발송은 사람 확인 후)
반응형
LIST

AI로 자동 보고서 요약 & 이메일 작성 시스템 구상기— 내일은 이걸 실제로 만들어본다!

프로그래밍/파이썬
반응형
SMALL

매일 반복되는 업무 보고서 정리와 이메일 작성.
이 두 가지를 AI가 대신 해준다면 얼마나 편할까?
오늘은 그 아이디어를 정리하고, 내일은 실제 구현에 들어간다.


---

1️⃣ 문제의식

회사나 팀 단위로 일을 하다 보면,
보고서가 쌓이고, 그걸 요약해서 메일로 보내야 할 때가 많다.
그런데 이 과정이 매우 비효율적이다.

같은 내용을 여러 번 복붙

포맷 맞추기, 표현 다듬기 반복

핵심은 한 줄인데 불필요한 내용이 길어짐


결국 시간이 많이 걸리고, 정확성도 떨어진다.


---

2️⃣ 내가 만들려는 시스템

내가 구상한 건 간단하다.

1. PDF나 DOCX 형태의 보고서를 AI가 읽고 요약


2. 그 요약을 바탕으로 이메일 제목과 본문을 자동으로 생성


3. 사용자는 내용 확인 후 보내기만 하면 끝



즉, 사람이 해야 할 건 “확인” 하나뿐이다.
모든 과정은 AI가 자동으로 처리한다.


---

3️⃣ 예상 흐름

input 폴더에 보고서 넣기

AI가 핵심 문장만 추출해 간결하게 요약

“팀장 보고용”, “본부장 보고용” 같은 톤 설정 가능

결과물은 요약문 + 이메일 초안 두 가지 파일로 저장


보고서를 요약하고, 자연스러운 이메일 문체로 정리하는 것까지 한 번에!


---

4️⃣ 기대 효과

보고서 요약 시간 10분 → 10초

메일 작성 스트레스 감소

반복 업무를 AI가 대신하면서 사람은 판단에 집중


특히 팀 단위에서 반복 보고가 많은 곳이라면
이런 자동화 시스템은 업무 효율을 크게 높여줄 것이다.


---

5️⃣ 내일 계획

내일은 실제로 이 시스템을 만들어볼 예정이다.
PDF를 읽고 텍스트로 바꾸는 파트부터,
AI가 문장을 요약하고 이메일 본문을 쓰는 파이프라인까지.

이 블로그에서 단계별 구현기로 나눠서 공유할 예정이니,
관심 있는 분들은 내일 포스트도 꼭 확인해보시길 🚀

반응형
LIST

퇴근 후 30분, 자동화로 하루를 정리하자 — 엑셀·PDF·이메일까지 한 번에!

프로그래밍/파이썬
반응형
SMALL

퇴근 전 반복되는 업무, 예를 들어 파일 정리·보고서 작성·이메일 전송 같은 일들은 생각보다 많은 시간을 잡아먹는다. 하지만 파이썬을 이용하면 이런 일상 업무를 자동화된 루틴으로 바꿀 수 있다. 이번 글에서는 ‘퇴근 후 30분’을 ‘자동화로 3분’으로 단축시키는 실제 방법을 소개한다.

파이썬으로 퇴근 전 자동화 루틴을 완성하는 직장인


매일 퇴근 전, 바탕화면에 흩어진 엑셀 파일을 정리하고
매출 데이터를 PDF로 만들어 보고서를 보내는 일.
이 모든 걸 자동화 루틴으로 전환할 수 있다면 어떨까?

오늘은 ‘퇴근 루틴 자동화’라는 주제로,
실제로 쓸 수 있는 파이썬 코드를 단계별로 다뤄보자.


1️⃣ 폴더 자동 정리 — 날짜별로 파일 분류하기

매일 생성되는 엑셀, PDF 파일을 날짜별로 정리하려면
os, shutil, datetime 모듈을 이용하면 된다.

import os, shutil
from datetime import datetime

base = "C:/Work"
today = datetime.now().strftime("%Y-%m-%d")
target = os.path.join(base, "정리", today)
os.makedirs(target, exist_ok=True)

for file in os.listdir(base):
    if file.endswith((".xlsx", ".pdf")):
        shutil.move(os.path.join(base, file), target)

print("파일 정리 완료:", target)

💡 팁:
이 코드를 daily_cleanup.py로 저장하고
윈도우 “작업 스케줄러”에 등록하면,
퇴근 10분 전에 자동 실행되도록 설정할 수 있다.


2️⃣ 엑셀 데이터에서 자동 보고서 만들기

매일 반복되는 매출 보고서, 수기 편집 대신 코드로 요약하자.

pandas로 데이터를 불러오고,
FPDF로 PDF 보고서를 생성하면 된다.

import pandas as pd
from fpdf import FPDF

# 엑셀 데이터 불러오기
df = pd.read_excel("sales.xlsx")

# 카테고리별 매출 합계
summary = df.groupby("Category")["Amount"].sum().reset_index()

# PDF 생성
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", "B", 16)
pdf.cell(0, 10, "Daily Sales Report", ln=True)
pdf.set_font("Arial", "", 12)

for i, row in summary.iterrows():
    pdf.cell(0, 10, f"{row['Category']}: {row['Amount']:,}", ln=True)

pdf.output("sales_report.pdf")
print("PDF 보고서 생성 완료.")

이제 엑셀 파일을 직접 열지 않아도
매일 자동으로 sales_report.pdf가 폴더에 생성된다.


3️⃣ 이메일 자동 전송으로 마무리

마지막 단계는 이메일 전송이다.
보고서를 메일로 자동 발송하면,
“보내기” 버튼조차 누를 필요가 없다.

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders

sender = "me@company.com"
receiver = "team@company.com"

# 메일 작성
msg = MIMEMultipart()
msg["Subject"] = "오늘의 보고서"
msg["From"] = sender
msg["To"] = receiver

# 본문
body = "팀 여러분, 첨부된 오늘의 보고서를 확인해주세요."
msg.attach(MIMEText(body, "plain"))

# PDF 첨부
filename = "sales_report.pdf"
with open(filename, "rb") as f:
    part = MIMEBase("application", "octet-stream")
    part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename={filename}")
msg.attach(part)

# 메일 전송
with smtplib.SMTP("smtp.company.com", 587) as s:
    s.starttls()
    s.login("me@company.com", "password")
    s.send_message(msg)

print("이메일 발송 완료.")

💡 Tip:
회사 내부망이라 SMTP 접근이 어렵다면,
Outlook 자동화 (win32com.client)나 Google Gmail API를 사용하면 된다.


💡 마무리 — 자동화의 진짜 목적은 “여유”

자동화의 핵심은 ‘코드’가 아니라 ‘습관’이다.
매일 하던 일을 자동화하면,
그 시간에 하루를 정리하고 내일을 준비할 수 있다.

파이썬은 결국 “사람의 시간을 되돌려주는 도구”다.
퇴근 전 30분을 3분으로 줄이는 것,
그게 진짜 효율이고, 진짜 여유다.

다음 예고

다음 글에서는 이번 자동화 루틴을 한 단계 더 확장해
AI가 자동으로 보고서 요약을 작성하고, 이메일 본문을 작성해주는 시스템을 만들어볼 예정이다.

반응형
LIST

🐍 파이썬 자동화, 완전 정복! (4) - 웹 크롤링을 넘어선 브라우저 조작 자동화 (Selenium/Playwright)

프로그래밍/파이썬
반응형
SMALL

단순 크롤링은 이제 그만! 🤯 파이썬 자동화 스크립트를 시스템에 등록했다면, 이제 웹 조작 자동화의 최고봉인 Playwright를 배워보세요. Selenium을 넘어선 압도적인 속도와 안정성으로 로그인, 클릭, 데이터 입력 등 복잡한 웹 작업을 완벽하게 자동화하는 실전 가이드를 공개합니다. (키워드: 파이썬 자동화, 웹 조작, Playwright, 로그인 자동화)

파이썬 IDE 화면 중앙에 Playwright 코드가 실행 중이며, 배경에 흐릿한 Chrome 브라우저 창이 떠 있고, 미래적인 파란 선으로 커서가 'login' 버튼으로 자동 이동하는 모습. 파란색 P 로고와 파이썬 로고가 작게 배치되어 웹 브라우저 자동화를 상징함. 전체적으로 청록색 톤으로 기술적 안정감 강조.


🚀 4편. 웹 크롤링을 넘어선 브라우저 조작 자동화 (Selenium/Playwright)

📝 서론: 크롤링의 한계와 조작 자동화의 필요성

안녕하세요, cheoly입니다! 이전 3편에서는 파이썬 자동화 스크립트를 윈도우와 리눅스에 등록하여 주기적으로 실행하는 실전 가이드를 다뤘습니다. 이제 우리의 코드는 '정해진 시간에 알아서' 실행될 준비가 되었죠.

하지만 웹사이트를 다룰 때, 단순히 정적인 HTML 데이터를 긁어오는 **크롤링(Scraping)**만으로는 부족한 경우가 많습니다.

  • 로그인 페이지를 통과해야 하는 경우
  • 버튼을 클릭해야 새로운 데이터가 로딩되는 경우 (JavaScript 기반 웹사이트)
  • 특정 폼에 정보를 입력하고 '제출'해야 하는 경우

이럴 땐, 실제 사용자가 브라우저를 조작하는 것처럼 파이썬 코드가 대신 행동해야 합니다. 이것이 바로 브라우저 조작 자동화의 영역이며, 주로 Selenium이나 Playwright 같은 라이브러리를 사용합니다.


🛠️ 핵심 도구 소개: Selenium vs. Playwright

특징 Selenium (셀레니움) Playwright (플레이라이트)
등장 시기 2004년 (오래됨, 표준) 2020년 (비교적 최신, MS 주도)
속도 및 성능 상대적으로 느림 (WebDriver 프로토콜 사용) 매우 빠름 (개발자 도구 프로토콜 사용)
지원 브라우저 Chrome, Firefox, Edge, Safari 등 Chrome, Firefox, Safari (단일 API로 모두 제어)
비고 오랜 역사만큼 자료가 많아 입문이 쉽지만, 설정이 복잡할 수 있습니다. 설정이 간편하고 안정성이 높지만, 아직 Selenium만큼 자료가 많지는 않습니다. 최근 강력하게 추천되는 도구입니다.

이 시리즈에서는 최신 트렌드에 맞춰 더 빠르고 안정적인 Playwright를 사용해 보겠습니다.


💡 실전 가이드: Playwright 설치 및 기본 조작

1. 설치 및 브라우저 드라이버 설정

Playwright는 필요한 브라우저 드라이버까지 한 번에 설치해 줍니다.

Bash
 
# Playwright 설치
pip install playwright

# 필요한 브라우저 드라이버 설치 (Chromium, Firefox, WebKit)
playwright install

 

2. 기본 조작 코드: 특정 페이지 접속 후 검색 필드에 입력하기

우리의 목표는 브라우저를 열고, 특정 URL로 이동한 다음, 검색창에 텍스트를 입력하는 것입니다.

Python
 
from playwright.sync_api import sync_playwright

def run(playwright):
    # 1. 브라우저 실행 (headless=False로 설정하면 창이 눈앞에 보입니다)
    browser = playwright.chromium.launch(headless=False)
    page = browser.new_page()

    # 2. 특정 웹사이트 접속
    page.goto("[https://www.google.com](https://www.google.com)")
    print(f"현재 페이지 제목: {page.title()}")
    
    # 3. 입력 필드 찾기 및 텍스트 입력
    # Google 검색창은 일반적으로 name="q" 속성을 가집니다.
    search_box_selector = 'textarea[name="q"]' 
    
    # 입력 대기 (요소가 나타날 때까지 기다림)
    page.wait_for_selector(search_box_selector)
    
    # '파이썬 자동화' 입력
    page.fill(search_box_selector, "파이썬 자동화")
    
    # 4. 엔터 키를 눌러 검색 실행
    page.press(search_box_selector, "Enter")

    # 5. 검색 결과 페이지 확인 (5초 대기)
    page.wait_for_timeout(5000)

    # 6. 브라우저 종료
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

[주의] page.wait_for_timeout(5000)은 디버깅용이며, 실제 운영에서는 특정 요소가 로딩되기를 기다리는 page.wait_for_selector()나 page.wait_for_url() 등을 사용해야 안정적입니다.


💡 심화: 로그인 처리와 Headless 모드

1. 클릭 자동화

특정 버튼을 찾아 클릭하는 것은 자동화의 기본입니다.

Python
 
# '로그인' 버튼을 텍스트로 찾아서 클릭
page.click("text=로그인") 

2. Headless 모드 (가장 중요!)

작업 스케줄러에 등록하여 서버나 백그라운드에서 스크립트를 실행할 때는 브라우저 창이 뜨지 않도록 해야 합니다. 이를 Headless 모드라고 합니다.

위 코드에서 browser = playwright.chromium.launch(headless=False) 부분을 browser = playwright.chromium.launch(**headless=True**)로 변경하면, 브라우저 창이 보이지 않은 채 백그라운드에서 모든 작업이 처리됩니다.


🎁 결론 및 다음 예고

브라우저 조작 자동화는 단순 크롤링을 넘어 실제 업무 환경에서 가장 높은 효율을 낼 수 있는 기술입니다. 로그인, 복잡한 데이터 입력/제출 등 이제 여러분의 파이썬 스크립트가 웹사이트와 완벽하게 상호작용할 수 있게 되었습니다.

다음 5편에서는 이 기술을 활용하여 **"퇴근 후 파일 정리부터 보고서 작성까지 (엑셀/PDF/이메일)"**라는 주제로, 가장 현실적이고 실용적인 업무 문서 자동화를 다뤄보겠습니다. 기대해주세요!

반응형
LIST

💻 파이썬 자동화, 완전 정복! (3) - 윈도우/리눅스 작업 스케줄러 등록 실전 가이드

프로그래밍/파이썬
반응형
SMALL

파이썬 자동화 스크립트를 윈도우 작업 스케줄러 또는 리눅스 크론탭에 등록하여 손이 필요 없는 완전 자동화 시스템을 구축하는 실전 가이드. 스크립트 실행 오류 없이 성공하는 절대 경로 설정 노하우를 지금 확인하세요!

파이썬 로고를 중심으로 톱니바퀴 회로가 펼쳐져 있고, 왼쪽에는 윈도우 작업 스케줄러(Windows Task Scheduler) 화면과 로봇 팔, 오른쪽에는 리눅스 크론탭(crontab -e) 터미널 화면이 보입니다. 도시 배경 위에 '파이썬 자동화, 완전 정복! (3) - 윈도우/리눅스 작업 스케줄러 등록 실전 가이드'라는 제목이 하단에 있습니다.

안녕하세요, Cheoly입니다. 👋 지난 시간까지 우리는 파이썬을 이용해 반복적인 작업을 효율적으로 자동화하는 방법을 배웠습니다. 이제 마지막 단계입니다! 아무리 훌륭한 자동화 스크립트라도, 매번 수동으로 실행해야 한다면 '완전한 자동화'라고 할 수 없겠죠?

이번 글에서는 여러분이 만든 파이썬 자동화 스크립트를 운영체제(OS)의 스케줄러에 등록하여 손이 전혀 가지 않는 완전 자동화 시스템을 구축하는 실전 노하우를 공유하겠습니다.


1. 📂 스크립트 실행 환경 준비: 가장 중요한 첫 단계

작업 스케줄러는 단순히 명령어를 실행해주는 도구일 뿐입니다. 스케줄러가 여러분의 파이썬 스크립트를 올바르게 찾고 실행할 수 있도록 환경을 설정하는 것이 핵심입니다.

A. 스크립트 파일 경로 확인 및 격리

  1. 실행 파일 경로 확인: 스크립트 파일(예: automated_script.py)이 어디에 있는지 정확히 확인하고, 가능한 한 경로에 한글이나 특수 문자가 없는 곳(예: C:\Automation\scripts 또는 ~/automation/scripts)에 두는 것이 좋습니다.
  2. 가상 환경 (Virtual Environment) 사용: 스크립트가 pandasrequests 같은 외부 라이브러리를 사용한다면, 해당 라이브러리들이 설치된 가상 환경을 통해 실행해야 합니다. 스케줄러는 일반적인 터미널 환경과 다르기 때문에 가상 환경의 python 실행 파일을 직접 지정해야 합니다.

B. 절대 경로 사용으로 오류 방지

스크립트 내에서 파일을 읽거나 쓰는 작업이 있다면, 해당 파일 경로를 절대 경로로 지정해야 합니다. 작업 스케줄러가 스크립트를 실행할 때의 '현재 작업 디렉토리'는 여러분이 예상하는 곳과 다를 수 있기 때문입니다.


2. 🛡️ 윈도우 환경: 작업 스케줄러 (Task Scheduler) 활용

윈도우 환경에서는 Windows 작업 스케줄러를 사용하여 파이썬 스크립트를 등록합니다. 설정이 다소 복잡해 보일 수 있지만, 다음 세 가지만 정확히 입력하면 됩니다.

📝 설정 단계

  1. 작업 스케줄러 실행: 윈도우 검색창에서 '작업 스케줄러'를 검색하여 실행합니다.
  2. 기본 작업 만들기: 오른쪽 메뉴에서 '기본 작업 만들기...'를 클릭하고 작업 이름을 지정합니다 (예: Python Daily Reporter).
  3. 트리거 설정: 스크립트를 언제 실행할지 설정합니다 (매일, 매주, 한 번만 등).
  4. 동작 설정 (가장 중요!): '프로그램 시작'을 선택하고 다음 세 항목을 정확히 입력합니다.
항목 입력 내용 예시 (가상환경 사용 시) 설명
프로그램/스크립트 파이썬 인터프리터 실행 파일의 전체 경로 C:\Users\User\venv\Scripts\python.exe 스크립트를 실행할 python.exe 파일의 경로 (가상 환경 내의 Scripts 폴더에 있음)
인수 추가 (선택 사항) 실행할 파이썬 스크립트 파일의 전체 경로 C:\Automation\scripts\automated_script.py 실행할 .py 파일의 전체 경로
시작 위치 (선택 사항) python.exe 파일이 있는 디렉토리 C:\Users\User\venv\Scripts python.exe 파일의 위치를 다시 한번 지정하여 오류 방지

💡 Tip: 만약 스크립트 실행 후 콘솔 창이 깜박이는 것을 원치 않는다면, .py 대신 .vbs 파일을 이용해 pythonw.exe로 실행하거나, 별도의 .bat 파일로 감싸는 방법도 고려할 수 있습니다.


3. 🐧 리눅스/macOS 환경: 크론탭 (Crontab) 활용

리눅스 및 macOS 환경에서는 강력하고 간단한 스케줄링 도구인 크론탭 (crontab)을 사용합니다.

📝 크론탭 등록 단계

  1. 크론 편집기 열기: 터미널에서 다음 명령어를 입력합니다.
  2. crontab -e
  3. 크론 규칙 작성: 파일 맨 아래에 실행 규칙을 추가합니다. 크론 규칙은 5개의 시간 필드실행할 명령어로 구성됩니다.
분 (0-59) 시 (0-23) 일 (1-31) 월 (1-12) 요일 (0-7) 실행 명령어
30 09 * * 1-5 /usr/bin/python3 /home/user/scripts/automated_script.py > /tmp/cron_log.log 2>&1

위 예시의 의미: 월요일부터 금요일까지 오전 9시 30분에 스크립트를 실행합니다.

💡 가상 환경 사용 시 유의사항

크론은 환경 변수가 제한적이므로, 반드시 전체 경로를 사용해야 합니다.

  • 파이썬 실행 경로 확인: which python 또는 which python3 명령어로 시스템 python 경로를 확인합니다.
  • 가상 환경 사용 시: 가상 환경의 python 실행 파일 경로를 직접 지정해야 합니다. /home/user/my_venv/bin/python 형태로 사용하세요.
  • 로그 기록: > /tmp/cron_log.log 2>&1을 추가하여 실행 결과를 로그 파일에 기록하면, 스케줄러가 제대로 작동하는지 확인하고 오류 발생 시 디버깅하는 데 큰 도움이 됩니다.

🛠️ 마무리 및 테스트: 자동화 시스템의 완성

등록을 완료했다면, 반드시 설정한 시간이 오기 전에 수동으로 명령어를 한 번 실행해보거나, 임시로 시간을 현재로부터 몇 분 뒤로 설정하여 작동 여부를 테스트해봐야 합니다.

파이썬 자동화 스크립트OS의 작업 스케줄러에 등록하는 것은 여러분의 자동화 여정의 마침표이자, 진정한 완전 자동화 시스템을 구축하는 성공적인 발판이 될 것입니다!

궁금한 점이 있다면 언제든지 댓글로 남겨주세요! 다음 글에서 또 만나요!

반응형
LIST

🐍 [cheoly의 확장] C++ 엔지니어, 파이썬으로 '개발 업무 자동화' 시작하기 (파일/데이터 처리 실전 가이드)

프로그래밍/파이썬
반응형
SMALL

[cheoly's Insight]

**C++**의 강력한 성능이 필요 없는 단순 반복 작업(로그 분석, 설정 파일 파싱, 데이터 정리)에 귀중한 시간을 낭비해서는 안 됩니다. 파이썬은 C++ 엔지니어의 생산성을 10배 이상 높여주는 최고의 자동화 도구입니다. 이 글은 복잡한 반복 작업을 파이썬 스크립트 한 줄로 끝내는 실전 가이드입니다.

파이썬 로고와 스케줄링 코드 창이 톱니바퀴와 시계 태엽에 연결되어 자동화된 작업을 실행하는 기술 컨셉 이미지.

 

안녕하세요, IT 엔지니어 cheoly입니다.

C++로 시스템의 핵심 로직을 짜는 엔지니어일수록, 부가적인 단순 반복 작업에 드는 시간을 줄여야 합니다. 40대 경력 개발자에게 시간은 곧 생산성이자 가치입니다. 파이썬은 이러한 반복 업무를 단 몇 줄의 코드로 해결하여 C++ 엔지니어의 업무 자동화를 현실로 만들어 줍니다.

이 글에서는 C++ 엔지니어들이 가장 흔히 겪는 파일 및 데이터 처리 작업을 파이썬으로 자동화하는 실전 전략을 제시합니다.


1. ⚙️ C++ 엔지니어가 파이썬 자동화를 시작해야 하는 이유

파이썬은 C++에 비해 속도는 느리지만, 개발 속도와 범용성에서는 압도적인 효율을 제공합니다.

작업 유형 C++ 방식 (High-Performance) Python 방식 (High-Productivity)
로그 파일 파싱 fstream을 이용한 수동 파일 읽기 및 문자열 처리 로직 작성 re 모듈(정규표현식) 또는 **pandas**로 수백 줄의 데이터를 단 몇 줄로 처리
설정 파일 관리 XML/INI 파서 라이브러리 직접 통합 및 빌드 json, yaml 라이브러리로 즉시 데이터 구조화
OS 자동화 WinAPI/POSIX 기반으로 OS 호출 코드 작성 os, shutil 모듈로 파일 복사, 이동, 디렉터리 정리 등 즉시 구현

⭐ 핵심: 파이썬은 **'Glue Language'**로서, 복잡한 C++ 컴포넌트들을 연결하고 데이터를 쉽게 처리하는 경계선 작업에 최적화되어 있습니다.


2. 📁 실전 자동화 1: 로그 파일에서 원하는 데이터 추출하기

시스템 디버깅 및 분석 시 필수적인 로그 파일 처리 작업을 파이썬으로 자동화합니다.

🔑 정규 표현식(re 모듈)을 이용한 키워드 추출

수백만 줄의 로그 파일에서 특정 패턴(예: 에러 코드, 사용자 ID, 타임스탬프)을 가진 줄만 추출해야 할 때 유용합니다.

Python
 
import re

log_file_path = 'system_error.log'
# 'ERROR' 키워드와 함께 타임스탬프([YYYY-MM-DD HH:MM:SS])가 포함된 줄을 찾는 패턴
pattern = r'\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\].*ERROR'

with open(log_file_path, 'r') as f:
    for line in f:
        # 패턴에 매칭되는 줄만 출력
        if re.search(pattern, line):
            print(line.strip())

C++ 대비 효율: C++에서 std::string::find와 복잡한 인덱싱 로직을 구현할 필요 없이, 파이썬의 re 모듈로 즉시 패턴 검색 자동화가 가능합니다.


3. 📊 실전 자동화 2: 설정 및 데이터 파일을 구조화하기

C++ 시스템에서 JSON이나 YAML 같은 파일로 설정값을 관리하는 경우가 많습니다. 파이썬은 이 구조를 즉시 파싱하여 활용할 수 있습니다.

🔑 JSON 파일을 Dictionary 객체로 즉시 변환

API 통신 또는 설정 파일로 흔히 사용되는 JSON 데이터를 파이썬 Dictionary 객체로 쉽게 다룰 수 있습니다.

Python
 
import json

config_path = 'system_config.json'

with open(config_path, 'r') as f:
    config_data = json.load(f) # JSON 파일이 즉시 파이썬 딕셔너리로 변환됨

# 특정 설정값 접근 자동화
timeout = config_data.get('network', {}).get('timeout_sec', 10)
print(f"현재 네트워크 타임아웃 설정값: {timeout}초")

# C++ 코드에 전달할 데이터 구조화 등 후처리 자동화 가능

C++ 대비 효율: JSON 파싱을 위해 무거운 라이브러리를 빌드하거나 복잡한 파서를 구현할 필요 없이, 내장 json 모듈로 단 두 줄만에 데이터 처리가 끝납니다.


4. 🚀 실전 자동화 3: OS 레벨의 파일/디렉터리 정리 및 관리

개발 환경에서 생성된 임시 파일, 빌드 잔여물, 백업 파일 등을 자동으로 정리하고 관리하는 스크립트를 만듭니다.

🔑 shutil과 os 모듈을 이용한 클린업 스크립트

특정 디렉터리의 .bak 파일을 찾아 다른 곳으로 옮기거나 삭제하는 자동화 스크립트입니다.

Python
 
import os
import shutil
from datetime import datetime, timedelta

def cleanup_old_files(target_dir, days_old=30):
    now = datetime.now()
    cutoff_date = now - timedelta(days=days_old)
    
    for filename in os.listdir(target_dir):
        file_path = os.path.join(target_dir, filename)
        
        # 30일 이상 지난 파일만 처리
        if os.path.isfile(file_path) and datetime.fromtimestamp(os.path.getmtime(file_path)) < cutoff_date:
            print(f"30일 경과 파일 삭제: {filename}")
            os.remove(file_path) # 실제 삭제 실행
            
# 예시: 'log_archive' 폴더의 30일 지난 파일 정리 자동화
cleanup_old_files('D:/Project/log_archive', 30)

C++ 대비 효율: 파일 생성 시간, 파일 검색, 경로 조작 등 OS 의존적인 작업들이 파이썬 내장 모듈로 깔끔하게 처리되어 운영 환경의 이식성까지 높일 수 있습니다.


📝 cheoly의 최종 조언: 파이썬은 '생산성 무기'

C++가 시스템의 엔진이라면, 파이썬은 그 엔진 주변의 모든 잡무를 처리하는 도구입니다. 파이썬을 익히는 것은 C++ 경력 엔지니어가 시스템의 핵심 로직에 더 많은 시간을 투자하여 자신의 가치와 연봉을 높이는 가장 효율적인 방법입니다.

다음 콘텐츠에서는 이 파이썬 자동화 스크립트를 **윈도우 작업 스케줄러(혹은 리눅스 크론탭)**에 등록하여 완전 자동화 시스템을 구축하는 실전 가이드를 다루겠습니다

반응형
LIST