여기서부터 자바의 진짜 심장, 객체지향(OOP)이 시작돼요. 어렵게 느껴질 수 있지만, 붕어빵 틀과 붕어빵 하나면 다 이해됩니다. 천천히, 비유와 함께 따라오세요.
new로 객체를 만들고, 생성자·this로 초기화하는 법을 익혀요.객체지향 프로그래밍(OOP, Object-Oriented Programming)은 프로그램을 "객체(object)"라는 부품 단위로 나눠서 조립하는 방식이에요. 지금까지 우리는 변수와 메서드를 위에서 아래로 늘어놓는 식으로 코드를 짰죠. 그런데 프로그램이 커지면 이 방식은 금방 뒤엉켜요.
그래서 나온 아이디어가 "현실 세계처럼 생각하자"예요. 우리가 사는 세상은 전부 사물(객체)로 이뤄져 있잖아요. 자동차, 사람, 강아지, 계좌 하나하나가 자기만의 데이터와 기능을 가진 독립된 존재죠. 프로그램도 이렇게 객체들을 만들어 서로 협력하게 짜자는 게 OOP입니다.
회원 객체, 상품 객체, 주문 객체로 만들어요. 각 객체는 자기 데이터(회원 이름, 상품 가격…)와 기능(주문하기, 결제하기…)을 스스로 가져요.
💡 핵심: 복잡한 프로그램을 "현실의 사물"에 대응하는 작은 부품들로 쪼개니, 이해하기 쉽고 고치기·재사용하기도 편해져요. 이게 OOP를 쓰는 이유예요.
OOP에서 가장 먼저 만나는 두 단어가 클래스(class)와 객체(object)예요. 둘의 관계는 이렇게 정리돼요.
new Car()로 하나 만드는 걸 "인스턴스를 만든다(인스턴스화)"라고 해요.다른 비유도 많아요. 클래스는 "붕어빵 틀 / 자동차 설계도 / 쿠키 커터", 객체는 "붕어빵 / 실제 자동차 / 찍어낸 쿠키". 공통점은 하나예요 — 클래스는 개념(설계), 객체는 실물.
class Car {...}를 쓰는 순간 "설계도"를, new Car()를 쓰는 순간 "실체 하나"를 만드는 거예요.클래스라는 설계도 안에는 크게 두 가지가 들어가요.
이제 실제 자동차 클래스를 만들어 볼게요. 자동차는 이름과 속도라는 상태를 갖고, 달리는 행동을 할 수 있어요.
// 자동차 "설계도" — 클래스
class Car {
// ① 필드(상태): 이 자동차가 가진 데이터
String name; // 이름
int speed; // 속도
// ② 메서드(행동): 이 자동차가 할 수 있는 일
void run() {
System.out.println(name + "이(가) " + speed + "km/h로 달립니다.");
}
}
위 코드는 아직 설계도일 뿐이에요. 붕어빵 틀을 만든 것이지, 붕어빵을 찍어낸 게 아니죠. 실제로 쓰려면 다음 STEP에서 객체를 생성해야 해요.
객체를 만드는 건 new 키워드가 담당해요. 그리고 만든 객체는 참조변수에 담아 두고, 점(.)으로 필드와 메서드에 접근해요.
public class Main {
public static void main(String[] args) {
// 1) 객체 생성 — 설계도(Car)로 실체 하나를 찍어낸다
Car c = new Car();
// 2) 필드에 값 넣기 — 점(.)으로 접근
c.name = "소나타";
c.speed = 100;
// 3) 메서드 호출 — 이 객체의 행동 실행
c.run(); // 출력: 소나타이(가) 100km/h로 달립니다.
}
}
Car c = new Car(); 한 줄 완전 해부
new Car() — new가 메모리에 새 Car 객체(붕어빵)를 실제로 만들어요. Car()는 뒤에서 배울 생성자 호출이에요.Car c — c는 참조변수(reference variable)예요. 방금 만든 객체가 있는 "위치(주소)"를 가리키는 리모컨이라고 생각하세요.= — new로 만든 객체를 리모컨 c에 연결해요. 이제 c.name, c.run()처럼 c를 통해 그 객체를 조종할 수 있어요.
💡 즉 객체는 메모리 어딘가에 실제로 존재하고, 참조변수 c는 그 객체를 가리키는 리모컨일 뿐이에요. 이 구분이 나중에 아주 중요해져요.
객체는 여러 개 만들 수 있어요. 붕어빵 틀 하나로 붕어빵을 여러 개 찍듯이요. 그리고 각 객체의 필드는 완전히 독립적이에요.
Car c1 = new Car();
c1.name = "소나타";
c1.speed = 100;
Car c2 = new Car();
c2.name = "테슬라";
c2.speed = 200;
c1.run(); // 소나타이(가) 100km/h로 달립니다.
c2.run(); // 테슬라이(가) 200km/h로 달립니다.
// c2.speed를 200으로 바꿔도 c1.speed는 그대로 100! 서로 남남이에요.
c1과 c2는 같은 설계도(Car)에서 나왔지만 각자의 필드 값을 따로 가져요. 붕어빵 하나에 팥을 더 넣어도 옆 붕어빵은 그대로인 것과 같아요. 이걸 "객체마다 상태가 독립적"이라고 해요.앞에서는 객체를 만든 뒤 c.name = "소나타";처럼 필드를 하나씩 채웠어요. 그런데 필드가 많으면 번거롭고 빠뜨리기 쉬워요. 생성자(constructor)는 객체를 만드는 순간(new) 필드를 한 번에 초기화해 주는 특별한 메서드예요.
Car면 생성자도 Car여야 해요.void조차 안 써요! 일반 메서드와 이걸로 구분돼요.new로 객체를 만들 때 자동으로 딱 한 번 호출돼요.
한마디로 생성자는 "객체가 태어날 때 실행되는 초기화 담당자"예요. 붕어빵을 굽는 순간 팥을 미리 넣어 두는 것과 비슷해요.
class Car {
String name;
int speed;
// 생성자 — 이름은 클래스명(Car)과 같고, 반환 타입이 없다!
Car(String name, int speed) {
this.name = name; // this로 필드와 매개변수를 구분
this.speed = speed;
}
void run() {
System.out.println(name + "이(가) " + speed + "km/h로 달립니다.");
}
}
// 사용 — 만들면서 바로 초기화! 한 줄로 끝.
Car c = new Car("소나타", 100);
c.run(); // 소나타이(가) 100km/h로 달립니다.
this는 "지금 이 객체 자신"을 가리키는 키워드예요. 생성자 안을 보면 매개변수 이름(name)과 필드 이름(name)이 똑같아서 헷갈리죠? 이때 this.name은 필드, 그냥 name은 매개변수를 뜻해요.
this.name = name; 뜻풀이
왼쪽 this.name = 이 객체의 필드 name (그 객체에 계속 붙어 있을 데이터).name = 생성자가 받은 매개변수 name (잠깐 전달받은 값).this가 없으면 name = name;이 되어 매개변수가 자기 자신에게 대입되고, 필드는 초기값(예: null) 그대로 남아요. 그래서 이름이 겹칠 땐 this.가 꼭 필요해요.
기본 생성자(default constructor). 생성자를 하나도 안 쓰면, 자바가 매개변수 없는 빈 생성자를 자동으로 넣어줘요. 그래서 STEP 3에서 new Car()가 됐던 거예요. 하지만 생성자를 하나라도 직접 만들면 자바는 기본 생성자를 더 이상 자동으로 안 만들어 줘요.
생성자 오버로딩(overloading). 앞 장에서 메서드 오버로딩을 배웠죠? 생성자도 매개변수 개수·타입만 다르면 여러 개 만들 수 있어요.
class Car {
String name;
int speed;
Car() { // 생성자 ① 매개변수 없음
this.name = "이름없음";
this.speed = 0;
}
Car(String name) { // 생성자 ② 이름만 받음
this.name = name;
this.speed = 0;
}
Car(String name, int speed) {// 생성자 ③ 둘 다 받음
this.name = name;
this.speed = speed;
}
}
Car a = new Car(); // ① 호출
Car b = new Car("소나타"); // ② 호출
Car c = new Car("테슬라", 200); // ③ 호출 → 인자에 맞는 생성자가 자동 선택됨
| 필드 타입 | 기본값 |
|---|---|
int, long, short, byte | 0 |
double, float | 0.0 |
boolean | false |
char | '\u0000' (코드값 0, 널 문자) |
참조형 (String, 배열, 객체…) | null |
Car c = new Car(); 후 아무것도 안 넣으면 c.name은 null, c.speed는 0이에요.
지금까지 우리는 c.speed = 200;처럼 필드를 바깥에서 직접 바꿨어요. 그런데 이렇게 열어두면 문제가 생겨요 — 누군가 c.speed = -9999;처럼 말도 안 되는 값을 넣어도 막을 방법이 없거든요.
그래서 나온 게 캡슐화(encapsulation)예요. 필드는 private으로 숨기고, 값을 읽고 쓰는 건 getter/setter라는 정해진 메서드로만 하도록 만드는 거예요.
class Car {
private String name; // private → 바깥에서 직접 접근 불가!
private int speed;
// getter : 값을 "읽는" 통로
public String getName() {
return name;
}
// setter : 값을 "쓰는" 통로 (여기서 검사도 가능!)
public void setSpeed(int speed) {
if (speed < 0) {
System.out.println("속도는 0보다 작을 수 없어요!");
return; // 잘못된 값이면 막아버림
}
this.speed = speed;
}
public int getSpeed() {
return speed;
}
}
Car c = new Car();
// c.speed = -9999; // ❌ 컴파일 에러! private이라 직접 접근 불가
c.setSpeed(-9999); // setter가 검사해서 막아줌 → "속도는 0보다..."
c.setSpeed(100); // ✅ 정상 값만 통과
getName()만 알면 돼요. 내부에서 필드 이름을 바꿔도 바깥 코드는 안 고쳐도 돼요.
💊 "캡슐"이라는 이름처럼, 알맹이(데이터)를 껍질로 감싸 보호하는 거예요. 약을 캡슐에 넣어 안전하게 먹이는 것과 같은 발상이죠. 지금은 "필드는 private, 접근은 getter/setter로"라는 틀만 기억하면 충분해요.
private은 "이 클래스 안에서만 접근 가능", public은 "어디서든 접근 가능"이라는 뜻이에요. 캡슐화는 필드는 private, getter/setter는 public으로 두는 게 기본 패턴이에요. 접근 제한자는 뒤에서 더 자세히 다룹니다.new로 객체 생성 → 참조변수(리모컨)에 담아 점(.)으로 사용. 객체마다 필드는 독립적.this = "이 객체 자신". this.name = name;은 필드와 매개변수를 구분해요.private + getter/setter로 데이터를 안전하게 보호해요.