self 인스턴스와 변수의 스코프
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()에서도 그대로 사용 가능하다!
필요하다면 클래스/인스턴스/메서드/변수 간 관계를 그림으로 정리해줄 수도 있어 😊
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에서의 스코프를 넘나드는 대표적 예시들과 비슷한 맥락을 Go와 Java에서 각각 어떻게 처리하는지 정리해줄게.
🟦 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를 전 단계에 계속 사용
결론부터 말하면:
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.변수명으로 값을 공유할 수 있다"**는 걸 이해하면 되는 거예요.
🔁 흐름 상 정리
- on_start()가 실행됨 → ID 한 번 생성
- 이후 task들이 실행됨 → 생성해둔 ID를 계속 활용
- 같은 유저 인스턴스가 이어지는 동안은 값 유지됨
필요하면 이 흐름을 한 장짜리 슬라이드용 설명 or 흐름도 형태로 정리해줄 수도 있어요 😄
혹시 다른 부분도 더 헷갈리는 게 있다면 얼마든지 질문해줘!