프로그래밍 개념 정리

self 인스턴스와 변수의 스코프

Dr.Mlem 2025. 4. 1. 09:47

Q1.

class InsertWithPreCheck(SequentialTaskSet):
    def on_start(self):
        # 아직 저장되지 않은 임시 ID 생성 (verify 실패용)
        raw_id = random_string()
        self.encoded_id = base64.b64encode(raw_id.encode()).decode()  # 이 ID를 전 단계에 계속 사용

    @task
    def verify_before_insert(self):
        # 아직 insert하지 않은 ID로 verify 요청 → 실패 유도
        self.client.post("/api/dfr/verify", json={
            "id": self.encoded_id
        })

    @task
    def insert(self):
        # 위에서 생성한 encoded_id 그대로 사용하여 insert
        data = {
            "key1": f"value_{random.randint(100, 999)}",
            "key2": f"value_{random.randint(100, 999)}"
        }
        encoded_data = base64.b64encode(json.dumps(data).encode()).decode()

        self.client.post("/api/dfr/insert", json={
            "id": self.encoded_id,
            "data": encoded_data
        })

    @task
    def verify_after_insert(self):
        # insert한 ID로 다시 verify → 성공 예상
        self.client.post("/api/dfr/verify", json={
            "id": self.encoded_id
        })
        self.interrupt()  # 해당 SequentialTaskSet 종료

 

이 Locust 코드에서,

@task
    def verify(self):
        self.client.post("/api/dfr/verify", json={
            "id": self.encoded_id
        })
        self.interrupt()  # 순서 종료 후 반복 가능

 

여기의 id 값은 위의 task에서 작성한 것을 사용하는거야? 같은 클래스지만, 다른 메소드니까 사용할 수 없어야 하는거 아니야?

 

A1: 아주 좋은 질문이야! 👏
너가 궁금해한 이 부분은 Python 클래스에서의 인스턴스 변수메서드 간의 공유에 대한 개념이야.


✅ 결론부터 말하자면:

네! verify() 메서드 안에서 self.encoded_id를 쓸 수 있는 이유는,
바로 같은 클래스(인스턴스)의 인스턴스 변수이기 때문이야.
즉, insert()에서 self.encoded_id를 정의하면,
동일한 유저 인스턴스가 실행하는 다른 task에서도 그 값을 공유할 수 있어.


🔍 왜 그럴까?

🔹 Locust의 동작 구조 요약

  • InsertThenVerify 클래스는 SequentialTaskSet을 상속받았고
  • Locust는 이 클래스의 하나의 인스턴스를 만들고,
  • 그 안의 task 메서드들을 순서대로 실행해나가

🔹 Python 클래스에서의 self

class InsertThenVerify(SequentialTaskSet):
    def insert(self):
        self.encoded_id = "abc123"  # <- 여기서 self에 변수 저장

    def verify(self):
        print(self.encoded_id)     # <- 이 값이 위에서 저장한 것 그대로 사용 가능

→ self는 이 클래스의 인스턴스 자신을 가리키고,
→ 인스턴스가 살아있는 동안 self.encoded_id는 메모리에 남아 있음


🎯 Locust 입장에서 다시 보면

  • 한 유저가 insert → verify 순서대로 행동한다면
    • insert에서 self.encoded_id = ... 저장
    • verify에서 self.encoded_id 그대로 꺼내서 사용

따라서 **“다른 메서드라도, 같은 인스턴스 내에서 정의된 값은 공유 가능하다”**는 게 핵심이야.


✅ 한줄 요약

insert()에서 정의된 self.encoded_id는
동일한 SequentialTaskSet 인스턴스 안에 있는 verify()에서도 그대로 사용 가능하다!


필요하다면 클래스/인스턴스/메서드/변수 간 관계를 그림으로 정리해줄 수도 있어 😊

 
 
 
Q2 : 
self가 인스턴스가 아닌 일반 변수였다면, 해당 코딩 스타일은 오류가 나는게 맞는거지?

 

 

 

A2:
네, 정확해요! 👍
self가 없는 일반 변수였다면, 해당 코딩 스타일은 분명히 오류가 발생했을 거예요.


✅ 왜 오류가 나냐면?

파이썬에서 **self.변수명**은 클래스 인스턴스(즉, 객체)의 속성(attribute) 을 말해요.
하지만 **self 없이 그냥 encoded_id**를 쓰면, 그건 지역(local) 변수로 취급돼요.


🔍 예시로 비교해볼게

class TestExample:
    def insert(self):
        encoded_id = "abc123"  # ✅ 지역 변수

    def verify(self):
        print(encoded_id)  # ❌ NameError: name 'encoded_id' is not defined
  • 여기서 encoded_id는 insert() 함수 안에서만 살아 있는 변수야
  • verify()에서는 정의되지 않은 변수라서 오류(NameError)가 발생하지!

✅ 반면 self를 쓰면?

class TestExample:
    def insert(self):
        self.encoded_id = "abc123"  # ✅ 인스턴스 변수

    def verify(self):
        print(self.encoded_id)  # ✅ 정상 출력: abc123
  • 이때는 self.encoded_id가 클래스 인스턴스에 저장된 속성이기 때문에,
  • 다른 메서드에서도 접근 가능!

🧠 용어 정리

종류 선언 방식 유효 범위 공유 가능 여부
지역 변수 encoded_id = ... 해당 함수 내부 ❌ 못 씀
인스턴스 변수 self.encoded_id = ... 클래스 전체 ✅ 가능

✅ 정리하자면:

self 없이 선언한 변수는 그 함수 안에서만 쓸 수 있는 지역 변수이기 때문에,
다른 task 메서드에서 접근하려고 하면 오류가 나는 게 맞아요.

그래서 Locust의 시나리오를 짤 땐,
여러 task 간에 공유할 데이터는 반드시 self.변수명 형태로 인스턴스 속성으로 저장해야 해요.

 

 

 

 

Q3 : 

이 코드에서 self 인스턴스의 경우와 같이, 변수였다면 변수의 스코프때문에 오류가 나는 상황이지만, self같이 스코프 밖에 있는 것처럼 보이지만 문제가 없는 경우들을 정리해서 알려줄 수 있을까?

 

 

 

A3 :

좋은 질문이야!

self를 이용한 인스턴스 변수는 스코프(scope)의 기본 규칙을 '넘어서' 공유되는 대표적인 예시인데,
이처럼 겉보기엔 다른 스코프인 것처럼 보여도 실제론 접근 가능한 경우들은 몇 가지 더 있어.


✅ 스코프를 넘나들 수 있는 대표적인 예시들

구분 예시 설명
1. 인스턴스 변수 (self.변수) self.encoded_id 클래스 내 모든 메서드에서 공유됨
2. 클래스 변수 (클래스명.변수) ClassName.count 모든 인스턴스가 공유함 (static한 성격)
3. 전역 변수 (global) global total_count 함수 내부에서 global 선언 시 전역 접근 가능
4. nonlocal 변수 nonlocal counter 중첩 함수(closure)에서 바깥 함수 변수 접근 가능
5. 모듈 변수 (import) import config; config.value 다른 파일에서 선언된 변수에 접근
6. 파라미터나 리턴값으로 전달된 변수 def func(x): 외부 값이 내부로 전달됨 (스코프를 통과한 느낌)

🔍 1. self.변수 – 인스턴스 변수

class Example:
    def set_value(self):
        self.x = 10  # 인스턴스 속성

    def get_value(self):
        print(self.x)  # 다른 메서드에서도 접근 가능

self는 '이 클래스의 나 자신'을 의미하니까, 어느 메서드에서든 같은 객체 내 값에 접근 가능함.


🔍 2. 클래스 변수 (모든 인스턴스가 공유)

class Example:
    count = 0  # 클래스 변수

    def __init__(self):
        Example.count += 1

self.count로 쓰면 인스턴스 전용으로 덮어써질 수 있음 → 조심


🔍 3. global 키워드

total = 0

def add():
    global total
    total += 1

함수 안에서 전역 변수에 접근하고 싶을 때 global 사용


🔍 4. nonlocal 키워드 (함수 안의 함수)

def outer():
    count = 0
    def inner():
        nonlocal count
        count += 1

내부 함수에서 바깥 함수의 지역 변수에 접근 가능 (closure 활용 시 유용)


🔍 5. 모듈 변수

# config.py
value = 42

# main.py
import config
print(config.value)

 

모듈 전체가 하나의 스코프처럼 작동함.
다만 변경하려면 직접 대입하거나 setter 함수 등 사용 권장


🎯 요약 정리

접근 방식 공유 범위 사용 예
self.변수 클래스 인스턴스 내 모든 메서드 Locust, 클래스 기반 테스트
클래스명.변수 모든 인스턴스에서 공유 카운터 등 공통 속성 관리
global 모듈 전체 설정값 변경 등
nonlocal 중첩 함수 closure 내부 상태 추적
모듈.변수 다른 파일 설정, 전역 상태 관리

필요하면 이 내용, 표로 PPT 정리용으로 만들어줄 수도 있어! 😎
어떤 문맥에 넣을지 알려주면 거기에 맞춰 간결하게 다듬어줄게.

 

 

 

 

Q4 : 

해당 내용을 Go의 경우와 Java의 경우로 알려줘.

 

 

 

A4 :

좋아! Python에서의 스코프를 넘나드는 대표적 예시들과 비슷한 맥락을 GoJava에서 각각 어떻게 처리하는지 정리해줄게.


🟦 Go 언어에서의 스코프와 유사 개념

Go는 Python처럼 클래스는 없지만, 구조체(struct)와 메서드(receiver) 를 통해 비슷한 역할을 수행해.

Python Go 대응 방식 설명
self.변수 receiver.field 메서드 수신자(receiver)로 전달된 struct 인스턴스의 필드 사용
global 패키지 수준 변수 함수 바깥에서 선언된 전역 변수
nonlocal 없음 (지원 X) Go는 중첩 함수 스코프를 공유하지 않음
모듈.변수 패키지.변수 다른 패키지에서 export된 변수 사용 (var X int → pkg.X)

🔍 예시

type Person struct {
    name string
}

func (p *Person) SetName(n string) {
    p.name = n
}

func (p *Person) GetName() string {
    return p.name
}
  • p는 수신자(receiver)
  • p.name은 인스턴스의 필드 접근 (→ Python의 self.name과 같은 개념)

🟨 Java에서의 스코프와 유사 개념

Java는 클래스를 중심으로 구조가 잡혀 있어서 Python과 더 가까운 구조를 가짐.

Python Java 대응 방식 설명
self.변수 this.변수 같은 클래스 내에서 인스턴스 변수 공유
global static 변수 클래스 전체에서 공유하는 정적 변수
nonlocal 익명 클래스 / 람다에서의 final or effectively final 변수 완전 동일한 개념은 없지만 비슷한 패턴 존재
모듈.변수 다른 클래스.변수 public static 변수로 접근 가능 (Config.VALUE)

🔍 예시

public class User {
    private String name;

    public void setName(String n) {
        this.name = n;
    }

    public String getName() {
        return this.name;
    }
}
  • this.name은 Python의 self.name과 같음
  • static 변수는 Python의 클래스 변수처럼 모든 인스턴스가 공유

✅ 요약 비교표 (Python vs Go vs Java)

개념 Python Go Java
인스턴스 변수 self.value receiver.value this.value
클래스 변수 Class.value 없음 (대체 불가) static 변수
전역 변수 global value var value (패키지 수준) public static 변수
비지역 변수 nonlocal ❌ 지원 X 람다 내부의 effectively final 변수
외부 모듈 변수 import config; config.val import pkg; pkg.Val import static Config.VALUE

필요하면 이걸 PPT용 명사형 요약으로도 만들어줄 수 있어 😎
혹은 각각 언어별로 코드 예제만 따로 뽑아줄 수도 있고!

 

 

 

Q5 : 


궁금한게, 이 코드에서

class InsertWithPreCheck(SequentialTaskSet):
    def on_start(self):
        # 아직 저장되지 않은 임시 ID 생성 (verify 실패용)
        raw_id = random_string()
        self.encoded_id = base64.b64encode(raw_id.encode()).decode()  # 이 ID를 전 단계에 계속 사용

    @task
    def verify_before_insert(self):
        # 아직 insert하지 않은 ID로 verify 요청 → 실패 유도
        self.client.post("/api/dfr/verify", json={
            "id": self.encoded_id
        })


이렇게 돌아가는데,

def on_start(self):
        # 아직 저장되지 않은 임시 ID 생성 (verify 실패용)
        raw_id = random_string()
        self.encoded_id = base64.b64encode(raw_id.encode()).decode()  # 이 ID를 전 단계에 계속 사용
 
이렇게 먼저 함수로 사용할 ID값을 생성하면, 그 이후에 계속 사용할 수 있는거야? 내가 너무 어렵게 생각하고 있는 것 같아. 결국, 순서대로 코드는 진행될 뿐이기에, 저 값을 계속 쓸 수 있는 것이지?
 
 
 
A5:
네, 정말 잘 이해하고 계신 거예요! 👍

결론부터 말하면:

on_start()에서 생성한 self.encoded_id는 이후에 정의된 모든 task에서 계속 사용할 수 있습니다.
왜냐하면 그 값은 인스턴스의 속성으로 저장되기 때문이에요.


🔍 왜 그런가?

  • on_start(self)는 Locust의 SequentialTaskSet이 시작될 때 처음 한 번 호출되는 메서드예요.
  • 여기에서 만든 self.encoded_id는 InsertWithPreCheck라는 클래스 인스턴스에 속성(attribute) 으로 저장돼요.
  • 그래서 verify_before_insert(), insert(), verify_after_insert() 같은 다른 메서드들에서 계속 접근 가능한 거죠.

✅ 요약하자면

def on_start(self):
    self.encoded_id = "some value"  # 여기에서 저장하면

@task
def task1(self):
    print(self.encoded_id)  # 여기서 그대로 쓸 수 있어!

→ 이건 전혀 어려운 개념이 아니라,
그냥 **"class 내부의 다른 함수들끼리는 self.변수명으로 값을 공유할 수 있다"**는 걸 이해하면 되는 거예요.


🔁 흐름 상 정리

  1. on_start()가 실행됨 → ID 한 번 생성
  2. 이후 task들이 실행됨 → 생성해둔 ID를 계속 활용
  3. 같은 유저 인스턴스가 이어지는 동안은 값 유지됨

필요하면 이 흐름을 한 장짜리 슬라이드용 설명 or 흐름도 형태로 정리해줄 수도 있어요 😄
혹시 다른 부분도 더 헷갈리는 게 있다면 얼마든지 질문해줘!