| 용어 | 뜻 | 비유 |
|---|---|---|
| 클래스(class) | 설계도. 어떤 데이터(필드)와 기능(메서드)을 가질지 정의 | 붕어빵 틀, 건축 도면 |
| 객체(object) | 설계도로 만든 실체. 메모리(힙)에 존재하며 고유한 상태를 가짐 | 붕어빵, 실제 건물 |
| 인스턴스(instance) | "어떤 클래스의" 객체임을 강조하는 말. order1은 Order의 인스턴스 |
"이 붕어빵은 저 틀의 인스턴스" |
| 인스턴스화(instantiation) | new로 객체를 만드는 행위 |
붕어빵 굽기 |
객체와 인스턴스는 거의 같은 뜻입니다. "객체"는 그 자체를, "인스턴스"는 클래스와의 관계를 강조할 때 씁니다.
public class Order { // 클래스: 설계도
long id; // 필드(field): 데이터
String customer;
int amount;
void pay() { // 메서드(method): 기능
System.out.println(customer + " 결제 " + amount + "원");
}
}
Order o1 = new Order(); // o1: Order의 인스턴스 (객체 1개)
Order o2 = new Order(); // o2: 또 다른 인스턴스 (객체 2개, 서로 독립)new Order()를 할 때마다 힙에 별도의 메모리가 잡히고, 각 객체는 자기만의 id, customer, amount를 갖습니다. o1.amount = 100은 o2.amount에 영향이 없습니다.
필드(field, 멤버 변수, 속성)는 객체의 상태입니다. 클래스 본문에 선언하며, 메서드 밖에 있습니다. 초기화하지 않으면 기본값(0, null, false)이 자동으로 들어갑니다(지역 변수와 다른 점).
메서드(method)는 객체의 행동입니다.
반환타입 메서드이름(매개변수타입 매개변수이름, ...) {
본문
return 값; // 반환타입이 void가 아니면 필수
}| 용어 | 뜻 |
|---|---|
| 매개변수(parameter) | 메서드 선언에 적힌 변수. void pay(int amount)의 amount |
| 인자(argument) | 호출할 때 실제로 넘긴 값. pay(5000)의 5000 |
| 반환값(return value) | 메서드가 돌려주는 결과. void면 없음 |
| 시그니처(signature) | 메서드 이름 + 매개변수 타입 목록. pay(int) |
메서드를 나누는 기준은 "한 가지 일만 하고, 이름만 보면 무엇을 하는지 알 수 있는가"입니다. calculateTotalWithDiscountAndTaxAndShipping()은 세 개로 쪼개야 합니다.
Order o1 = new Order();
o1.customer = "Kim";
o1.amount = 5000;
Order o2 = o1;스택 힙
┌─────────────┐ ┌─────────────────────┐
│ o1 │ 0x500 │───────►│ Order 객체 │ ← 0x500
│ o2 │ 0x500 │───┐ │ id = 0 │
└─────────────┘ │ │ customer ──► "Kim" │
└───►│ amount = 5000 │
└─────────────────────┘new Order()는 힙에 필드 크기만큼 공간을 잡고 주소를 반환합니다.o1, o2는 스택의 지역 변수이고 주소만 갖습니다. 03 레슨의 배열과 완전히 같습니다.Order o2 = o1;은 주소 복사이므로 o2.amount = 9999를 하면 o1.amount도 9999입니다. 객체는 하나, 이름표만 둘입니다.o1.pay()와 o2.pay()가 어떻게 자기 데이터를 구분할까요? 그것이 this입니다.public class Order {
int amount;
void applyDiscount(int amount) {
amount = amount - 1000; // 매개변수 amount끼리 계산. 필드는 안 바뀜!
}
void applyDiscountFixed(int amount) {
this.amount = amount - 1000; // this.amount = 필드, amount = 매개변수
}
}메서드 안에서 이름이 같으면 가까운 것(매개변수, 지역 변수)이 우선합니다. 필드를 가리키려면 this.를 붙입니다.
this의 정체는 "메서드를 호출한 객체의 주소"입니다. o1.pay()를 호출하면 JVM은 몰래 pay(this = o1)처럼 o1의 주소를 첫 번째 숨은 인자로 넘깁니다. 그래서 하나뿐인 pay 코드가 this.customer로 자기 객체의 데이터를 찾을 수 있습니다.
this의 세 가지 용도:
| 용도 | 예 |
|---|---|
| 필드와 매개변수 구분 | this.name = name; |
| 자기 자신을 반환 (메서드 체이닝) | return this; → builder.setA(1).setB(2) |
| 다른 생성자 호출 | this(0, "unknown"); (2.6) |
static 메서드 안에서는 this를 쓸 수 없습니다. 호출한 객체가 없기 때문입니다(2.8).
public class Order {
long id;
String customer;
Order(long id, String customer) { // 생성자: 클래스 이름과 같고, 반환 타입 없음
this.id = id;
this.customer = customer;
}
}
Order o = new Order(1001L, "Kim"); // new가 생성자를 호출생성자(constructor)의 규칙:
| 규칙 | 설명 |
|---|---|
| 이름 = 클래스 이름 | 대소문자까지 |
| 반환 타입 없음 | void도 쓰지 않음. 쓰면 그냥 일반 메서드가 됨 |
new가 호출 |
직접 o.Order()처럼 부를 수 없음 |
| 오버로딩 가능 | 매개변수가 다른 생성자를 여러 개 |
왜 생성자가 필요한가? 필드를 하나씩 대입하면 빠뜨릴 수 있고, "customer가 없는 Order" 같은 불완전한 객체가 존재하게 됩니다. 생성자는 객체가 만들어지는 순간 반드시 유효한 상태가 되도록 강제합니다. 05 레슨의 캡슐화와 이어지는 개념입니다.
기본 생성자(default constructor): 생성자를 하나도 쓰지 않으면 컴파일러가 매개변수 없는 빈 생성자 Order() {}를 자동으로 넣어 줍니다. 하지만 생성자를 하나라도 직접 쓰면 자동 생성이 사라집니다. 그래서 Order(long, String)을 만든 뒤 new Order()를 하면 컴파일 오류가 납니다. 둘 다 필요하면 둘 다 써야 합니다.
new Order(1001L, "Kim")의 실행 순서:
Order 크기만큼 메모리 확보, 모든 필드를 기본값으로int count = 0; 처럼 선언에 붙은 것)생성자가 여러 개일 때 코드가 중복되면 한 생성자가 다른 생성자를 호출합니다.
public class Order {
long id;
String customer;
String status;
Order(long id, String customer, String status) { // 가장 완전한 생성자
this.id = id;
this.customer = customer;
this.status = status;
}
Order(long id, String customer) {
this(id, customer, "CREATED"); // 위 생성자에 위임. 반드시 첫 줄!
}
Order(long id) {
this(id, "guest"); // 또 위임 → 결국 3개짜리로
}
}this(...)는 생성자 본문의 첫 문장이어야 합니다. 이유: 객체 초기화는 한 번, 한 곳에서 일관되게 끝나야 합니다. this() 앞에 다른 코드가 있으면 "반쯤 초기화된 상태에서 뭔가를 한 뒤 다시 초기화"가 되어 규칙이 무너집니다. 이 패턴을 텔레스코핑(telescoping) 생성자라고 하며, 검증 로직을 가장 완전한 생성자 한 곳에만 두면 된다는 장점이 있습니다.
같은 이름의 메서드를 매개변수만 다르게 여러 개 정의하는 것입니다.
int add(int a, int b) → add(int, int)
double add(double a, double b) → add(double, double)
int add(int a, int b, int c) → add(int, int, int)
String add(String a, String b) → add(String, String)System.out.println이 바로 오버로딩입니다. println(int), println(String), println(double), println(Object)... 10개가 넘고, 그래서 아무거나 넘겨도 동작합니다.
오버로딩 규칙 — 시그니처가 달라야 한다:
| 구분 기준이 되는가? | 항목 | 예 |
|---|---|---|
| ✅ 된다 | 매개변수 개수 | f(int) vs f(int, int) |
| ✅ 된다 | 매개변수 타입 | f(int) vs f(String) |
| ✅ 된다 | 매개변수 순서 | f(int, String) vs f(String, int) |
| ❌ 안 된다 | 반환 타입 | int f(int) vs double f(int) → 컴파일 오류 |
| ❌ 안 된다 | 매개변수 이름 | f(int a) vs f(int b) → 같은 메서드 |
반환 타입이 구분 기준이 안 되는 이유: 호출문 f(5);만 보면 어떤 것을 원하는지 알 수 없습니다. 반환값을 버리는 호출도 가능하므로 컴파일러가 결정할 근거가 없습니다. 시그니처는 "호출문만 보고 결정 가능한 정보"로 이루어져야 합니다.
호출 시 선택 순서: 정확히 일치 → 자동 형변환(widening) → 박싱(int→Integer) → 가변 인자. f(int)와 f(long)이 있을 때 f(5)는 f(int), f(5L)은 f(long). f(long)만 있고 f(5)를 호출하면 int→long 자동 변환으로 f(long)이 호출됩니다.
애매하면(f(int, long)과 f(long, int)에 f(1, 2)) 컴파일 오류가 나므로 캐스팅으로 명시해야 합니다.
| 구분 | 인스턴스 멤버 | static 멤버 (클래스 멤버) |
|---|---|---|
| 소속 | 객체 하나하나 | 클래스 전체 |
| 메모리 | 힙, 객체마다 따로 | 메서드 영역, 클래스당 하나 |
| 생성 시점 | new 할 때 |
클래스가 처음 로드될 때 (프로그램 시작 무렵) |
| 접근 | 객체.멤버 |
클래스.멤버 (객체로도 되지만 비권장) |
this 사용 |
가능 | 불가 (객체가 없음) |
| 인스턴스 멤버 접근 | 가능 | 불가 (어느 객체 것인지 모름) |
public class Order {
static int totalCount = 0; // 모든 Order가 공유하는 카운터
long id; // 객체마다 다름
Order() {
totalCount++; // 객체 생성마다 공유 변수 증가
this.id = totalCount;
}
static boolean isValidAmount(int amount) { // 객체 없이 쓸 수 있는 유틸
return amount > 0;
}
}
Order.isValidAmount(500); // 객체 없이 호출
Order.totalCount; // 객체 없이 접근왜 static 메서드는 인스턴스 필드를 못 쓰는가? Order.isValidAmount(500)을 호출할 때 Order 객체가 0개일 수도, 1만 개일 수도 있습니다. this.id라고 쓰면 그 1만 개 중 누구의 id인지 알 수 없습니다. 그래서 컴파일러가 막습니다. 반대로 인스턴스 메서드에서 static 멤버 접근은 항상 가능합니다(클래스당 하나라 애매하지 않음).
언제 static을 쓰는가:
| 용도 | 예 |
|---|---|
| 객체 상태와 무관한 유틸리티 | Math.max, Integer.parseInt, Arrays.sort |
| 모든 인스턴스가 공유하는 값 | 카운터, 설정, 캐시 |
| 상수 | static final double TAX_RATE = 0.1; |
| 팩토리 메서드 | LocalDate.of(2026, 9, 8), List.of(1, 2) |
main |
JVM이 객체 없이 시작해야 하므로 |
남용 주의: "static을 붙이면 편하니까"로 모든 것을 static으로 만들면 객체 지향이 아니라 절차 지향이 됩니다. 상태를 갖는 것은 인스턴스, 상태 없는 순수 함수는 static이 기본 원칙입니다.
이것은 자바에서 가장 많이 오해되는 주제입니다. 결론부터: 자바는 100% call by value(값 전달)입니다. 단, 참조 타입의 "값"이 주소라서 참조 전달처럼 보일 뿐입니다.
static void changePrimitive(int x) {
x = 100; // 복사본 x만 바뀜
}
static void changeField(Order o) {
o.amount = 100; // 복사된 주소를 따라가 원본 객체의 필드를 바꿈 → 반영됨
}
static void reassign(Order o) {
o = new Order(); // 복사본 o가 새 객체를 가리킴. 원본 변수는 그대로
o.amount = 999; // 새 객체의 필드. 원본과 무관
}
int n = 1;
changePrimitive(n); // n은 여전히 1
Order order = new Order();
order.amount = 5;
changeField(order); // order.amount는 100
reassign(order); // order.amount는 여전히 100 (999 아님)메모리로 보면:
changeField(order) 호출 시:
호출자 스택: order = 0x500 ──┐
메서드 스택: o = 0x500 ──┴──► 힙 0x500: Order{amount}
→ o.amount = 100 은 0x500의 amount를 바꿈. 호출자도 같은 곳을 보므로 반영
reassign(order) 호출 시:
호출자 스택: order = 0x500 ──────► 힙 0x500: Order{amount=100}
메서드 스택: o = 0x500 → 0x700 ──► 힙 0x700: 새 Order{amount=999}
→ o가 다른 주소로 바뀌었을 뿐. order는 여전히 0x500정리:
| 메서드 안에서 | 원본에 반영? | 이유 |
|---|---|---|
| 원시 타입 매개변수 값 변경 | ❌ | 값이 복사됨 |
| 참조 매개변수의 필드/요소 변경 | ✅ | 주소가 복사되어 같은 객체를 봄 |
| 참조 매개변수 자체를 재대입 | ❌ | 복사본 주소만 바뀜 |
"참조 전달(call by reference)"이 진짜로 있는 언어(C++의 &, C#의 ref)에서는 세 번째 경우도 원본에 반영됩니다. 자바는 안 됩니다. 그래서 "자바는 참조를 값으로 전달한다(pass reference by value)"가 정확한 표현입니다.
실무적 의미: 메서드가 넘겨받은 객체를 수정하면 호출자에게 영향이 갑니다. 의도적이면 좋지만(배열 정렬), 아니면 버그입니다. 그래서 05 레슨의 불변 객체가 중요합니다.
Order o = new Order(); // 객체 생성 (힙)
o = null; // 더 이상 가리키는 변수 없음 → GC 대상자바에는 delete가 없습니다. 어떤 변수도 가리키지 않는 객체는 가비지 컬렉터(GC)가 알아서 회수합니다. 메서드가 끝나면 스택의 지역 변수가 사라지고, 그 변수만 가리키던 객체는 자동으로 GC 대상이 됩니다. 개발자는 메모리 해제를 신경 쓰지 않아도 되지만, 대신 필요 없는 객체를 static 변수나 긴 수명의 컬렉션에 계속 담아 두면 GC가 회수하지 못해 메모리 누수가 납니다.
Order o = new Order();
System.out.println(o); // Order@1b6d3586 — 클래스이름@해시코드모든 클래스는 Object를 상속(중급 레슨)하며, Object의 toString은 주소 비슷한 것을 돌려줍니다. 사람이 읽을 수 있게 하려면 재정의(override)합니다.
@Override
public String toString() {
return "Order{id=" + id + ", customer=" + customer + "}";
}@Override는 "부모의 메서드를 재정의한다"는 표시입니다. 오타를 내면 컴파일 오류로 잡아 주므로 항상 붙입니다. 마찬가지로 equals도 기본은 주소 비교이며, "id가 같으면 같은 주문"으로 취급하려면 재정의합니다. 상속을 아직 배우지 않았으므로 여기서는 "필요하면 재정의한다" 정도만 알고, 예제에서 형태를 익힙니다.