한 줄 정의
객체지향 프로그래밍(OOP)은 데이터와 그 데이터를 다루는 행동을 객체로 묶고, 객체들이 역할을 나누어 협력하게 만드는 프로그래밍 방식이다.
게임으로 비유하면 클래스는 캐릭터 설계도이고, 객체는 그 설계도로 실제 생성한 용사·마법사다. 비유를 이해한 뒤에는 실제 자바 용어도 함께 기억하자.
먼저 9개 용어 한눈에 보기
클래스(Class): 객체를 만들기 위해 상태와 행동을 정의한 설계
객체(Object): 프로그램 안에서 상태와 행동을 가진 실제 대상
필드(Field): 객체가 기억하는 데이터
메서드(Method): 객체가 수행하는 행동
생성자(Constructor): 객체 생성 시 초기 상태를 설정하는 특별한 구문
캡슐화(Encapsulation): 상태와 행동을 묶고, 잘못된 접근을 제한하는 것
추상화(Abstraction): 복잡한 내부 구현은 숨기고 필요한 사용법만 드러내는 것
다형성(Polymorphism): 같은 호출이 실제 객체에 따라 다르게 동작하는 것
상속(Inheritance): 기존 클래스의 특성을 물려받아 새 클래스를 만드는 것
클래스와 객체
클래스는 설계다
클래스는 “플레이어가 어떤 정보를 가지고 무엇을 할 수 있는가”를 코드로 정의한다. 클래스만 선언한 시점에는 실제 플레이어 객체가 아직 없다.
public class Character { //public :공개
public String name; //String : 문자열
public int hp; //int : 정수
public void attack() {
System.out.println(name + "이(가) 공격합니다.");
}
}
객체는 설계로 만든 실체다
new Player()가 실행되면 메모리에 Player 객체가 생성된다. warrior와 wizard는 객체 자체가 아니라 객체를 가리키는 참조 변수다.
Character warrior = new Character();//전사 만들기
Character wizard = new Character();//마법사 만들기
warrior.name = "전사";
wizard.name = "마법사";
초보 단계에서는 객체와 인스턴스를 거의 같은 뜻으로 사용해도 된다. “
warrior는Player클래스의 인스턴스다”처럼 특정 클래스와의 관계를 강조할 때 인스턴스라는 말을 자주 쓴다.
필드와 메서드
필드는 객체가 기억하는 상태이고, 메서드는 객체가 수행하는 행동이다. 각 객체는 같은 필드 구조를 가지지만 값은 서로 다를 수 있다.
public class Character {
public String name; // 필드
public int hp; // 필드
public void attack() { // 메서드
System.out.println(name + "이(가) 공격합니다.");
}
public void heal(int amount) { // 메서드
hp += amount;
}
}
예를 들어 전사의 hp는 100이고 마법사의 hp는 60일 수 있다. 하지만 두 객체 모두 같은 heal 메서드를 사용할 수 있다.
생성자
생성자는 new로 객체를 만들 때 호출되어 필드의 초기값을 정한다. 이름이 클래스와 같고 반환형을 쓰지 않는다.
public class Character{
public String name;
public int hp;
public void attack() { // 메서드
System.out.println(name + "이(가) 공격합니다.");
}
public void heal(int amount) { // 메서드
hp += amount;
}
//생성자 class Character라서 무조건 Character라고 이름을 지어야 한다.
public Character(String name, int hp) { //이름, 체력 순서대로 받는다.
this.name = name;
this.hp = hp;
}
}
Character hero = new Character("용사", 100); //생성하는 순간에 초기 정보를 입력한다.
생성자를 “특별한 메서드”라고 쉽게 설명하기도 하지만, 자바 언어에서 생성자는 메서드와 구별된다. 반환형이 없고 객체 생성 과정에서만 호출된다.
this.name은 현재 객체의 필드를 뜻한다.
객체지향의 4대 핵심 특성
캡슐화: 올바른 방법으로만 상태 변경하기
체력을 public 필드로 열어 두면 외부 코드가 음수나 비정상적으로 큰 값을 넣을 수 있다. private으로 숨기고 메서드를 통해서만 바꾸면 객체가 스스로 규칙을 지킬 수 있다.
public class Character{
public String name;
private int hp;// private : 비공개 체력을 비공개로 한다.
public void attack() { // 메서드
System.out.println(name + "이(가) 공격합니다.");
}
public void heal(int amount) { // 메서드
hp += amount;
}
//생성자
public Character(String name, int hp) { //이름, 체력 순서대로 받는다.
this.name = name;
this.hp = hp;
}
}
Character hero = new Character("용사", 100); //생성하는 순간에 초기 정보를 입력한다.
//hero.hp = 9999; //에러
//누군가 체력을 9999로 바꾸려 해도 private hp 이기 때문에 오류난다.
캡슐화는 단순히 모든 필드에 getter와 setter를 만드는 일이 아니다. 유효한 변경 규칙을 객체 안에 모으는 것이 핵심이다.
추상화: 사용법과 구현을 분리하기
다른 코드는 attack()을 호출하는 방법만 알면 된다. 데미지 계산, 치명타 판정, 효과음 같은 내부 절차가 바뀌어도 호출하는 쪽은 영향을 덜 받는다. 자바에서는 클래스, 인터페이스, 추상 클래스 등으로 필요한 역할을 표현할 수 있다.
다형성: 같은 호출, 다른 결과
전사와 마법사를 모두 Character 타입으로 다루면서 같은 attack()을 호출할 수 있다. 실행할 메서드는 참조 변수가 아니라 실제 객체의 종류에 따라 결정된다.
Character Warrior = new Character("토르" ,100);
Character Wizard = new Character("멀린" ,80);
Warrior.attack(); // 토르이(가) 검을 휘두릅니다.
Wizard.attack(); // 멀린이(가) 파이어볼을 발사합니다.
토르이(가) 검을 휘두릅니다.
멀린이(가) 파이어볼을 발사합니다.
상속: 공통 특성 물려받기
Warrior extends Character처럼 선언하면 자식 클래스가 부모 클래스의 필드와 메서드를 이어받을 수 있다. 공통 이름과 체력 관리는 부모에 두고, 직업별 공격만 자식이 재정의할 수 있다.
상속은 강한 “~이다(is-a)” 관계일 때 사용한다. 단순히 코드를 재사용하려고 상속 계층을 깊게 만들면 변경이 어려워질 수 있다. “~을 가지고 있다(has-a)” 관계라면 작은 객체를 필드로 넣는 합성(Composition)을 먼저 고려한다.
한 번에 실행하는 게임 예제
아래 예제 하나에 생성자, 캡슐화, 추상화, 상속, 오버라이딩과 다형성이 모두 들어 있다. 파일명을 Game.java로 저장한 뒤 실행할 수 있다.
abstract class Character {
public String name;
private int hp; //비공개
// 생성자
public Character(String name, int hp) {
this.name = name;
this.hp = hp;
}
// 체력 확인
// hp가 비공개 이므로 함수로 받아야한다.
public int getHp() {
return hp;
}
// 데미지 받기
public void takeDamage(int damage) {
hp -= damage;
}
// 추상 메서드 (자식 클래스에서 반드시 구현)
public abstract void attack();
}
// 전사 클래스
class Warrior extends Character {
public Warrior(String name) {
super(name, 120);
}
@Override
public void attack() { //반듯이 구현 해야하는 함수
System.out.println(name + "이(가) 검으로 공격합니다.");
}
}
// 마법사 클래스
class Wizard extends Character {
public Wizard(String name) {
super(name, 80);
}
@Override
public void attack() { //반듯이 구현 해야하는 함수
System.out.println(name + "이(가) 파이어볼을 발사합니다.");
}
}
// 실행 클래스
public class Game {
public static void main(String[] args) {
Character warrior = new Warrior("토르");
Character wizard = new Wizard("멀린");
warrior.attack();
warrior.takeDamage(30);
System.out.println(warrior.name + "의 남은 HP : " + warrior.getHp());
wizard.attack();
wizard.takeDamage(30);
System.out.println(wizard.name + "의 남은 HP : " + wizard.getHp());
}
}
토르이(가) 검을 휘두릅니다.
남은 HP: 90
멀린이(가) 파이어볼을 발사합니다.
남은 HP: 50
이 코드에서 Character는 공통 상태와 규칙을 정의한다. attack()은 구체적인 공격 방식을 자식에게 맡기는 추상 메서드이고, 반복문은 캐릭터 직업을 확인하지 않고 같은 명령을 보낸다.
자주 헷갈리는 부분
클래스와 객체: 클래스는 정의이고, 객체는 런타임에 생성된 값이다.
객체와 참조 변수:
hero는 객체가 저장된 위치를 가리키는 변수다.필드와 지역 변수: 필드는 객체의 상태로 남고, 지역 변수는 메서드 실행 중 잠시 사용된다.
오버라이딩과 오버로딩: 오버라이딩은 상속받은 메서드를 다시 구현하는 것이고, 오버로딩은 같은 이름에 매개변수 구성을 다르게 선언하는 것이다.
캡슐화와 정보 은닉: 접근 제한은 캡슐화를 돕는 수단이다. 객체가 상태와 규칙을 함께 책임지는 것이 더 큰 목적이다.
5분 최종 정리
클래스로 설계하고 객체를 만든다. 객체의 데이터는 필드, 행동은 메서드로 표현하며 생성자가 첫 상태를 정한다. 그 위에 캡슐화로 규칙을 보호하고, 추상화로 사용법을 단순화하고, 다형성으로 같은 역할의 객체를 교체 가능하게 다룬다. 상속은 이 구조를 만드는 방법 중 하나다.
외울 문장: 클래스는 설계도, 객체는 실체, 필드는 상태, 메서드는 행동, 생성자는 초기화.
관련 문서
절차지향 vs 객체지향 프로그래밍: 두 패러다임의 사고방식과 선택 기준을 비교한다.
댓글 0
댓글을 불러오는 중…