| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- mapping
- 기본형
- 개발자
- 자격증
- s3
- jpa
- 스터디
- serverless
- data
- MSA
- Spring Boot
- Redis
- 자바
- QueryDSL
- jvm
- aws
- bootcamp
- backenddeveloper
- spring
- java
- 오블완
- Cache
- goorm
- 티스토리챌린지
- CodeCommit
- orm
- nosql
- backend
- goorm x kakao
- Docker
- Today
- Total
gony-dev 님의 블로그
JAVA의 꽃, 객체 지향 프로그래밍(OOP)의 클래스 문법 본문
지난 시간에는 배열에 대해 알아보고 코드에 적용하는 시간을 가졌다.
[JAVA] - JAVA 배열을 모르겠다고? 알려드리겠습니다!
이번 시간에는 객체 지향 프로그래밍에서 필수적인 요소인 클래스에 대해 파헤쳐 보자!
객체 지향 프로그래밍(Object Oriented Programming)이란?

"객체" 개념을 기반으로 데이터를 속성과 기능으로 묶어 프로그램을 설계하는 방식으로 줄여서 OOP라고 부른다.
객체는 현실 세계에서의 의미 그대로 '사물'을 의미하며, 클래스라는 '설계도'를 기반으로 만들어진다.
다형성(Polymorphism), 추상화(Abstraction), 상속(Inheritance), 캡슐화(Encapsulation)의 4대 특징을 통해 코드 재사용성 및 유지보수, 확장성을 높히는 것이 목표이다.
우리는 객체 지향 프로그래밍을 설계하기 위해 클래스에 대해 자세히 알아볼 예정이다.
클래스(Class)
클래스 지리구욘~
클래스를 알아보기 전에 클래스가 왜 필요한지에 대해 알아보자.
클래스란 객체를 정의하는 설계도란 의미로 사용된다.
자바에서는 설계도를 통해 여러 객체를 생성하여 사용하는 식으로 프로그래밍을 진행한다.
게임을 예로 들어보자.
게임을 시작하면 모든 플레이어의 체력, 공격력, 스킬 같은 ‘속성의 종류’는 모두 동일한 구조를 가진다.
하지만 플레이가 진행되면서 플레이어마다 체력 수치가 오르고, 새로운 스킬을 배우는 등 값(=데이터)은 달라진다.즉, 변하는 건 “속성 자체”가 아니라 “속성에 들어있는 값”이다.
이때, 플레이어가 생성될 때마다 매번 새로 설계해서 만드는 건 비효율적이다.
그래서 플레이어의 공통 구조를 ‘설계도’로 정의해두고, 공장처럼 필요할 때마다 찍어내듯 생성하는데,
이 설계도가 "클래스"이고, 설계도로 만들어진 실제 플레이어가 "객체"다.
클래스는 객체의 속성을 나타내는 필드와 객체의 함수를 나타내는 메서드로 구성되어 있다.

프로그래밍을 하며 클래스, 인스턴스, 객체 등 많은 용어들이 나온다.
용어들을 정리해보자.
- 클래스 | 객체를 생성하는 설계도
- 객체 | 클래스와 new 연산자를 통해 만든 실제 데이터가 들어있는 변수
- 인스턴스 | 클래스로부터 생성된 특정 개체, 관계적인 표현
- 필드(속성) | 클래스 내에 있는 변수
- 메서드 | 클래스 내에 있는 함수
- 생성자 | 클래스로 객체를 만들 때 각 객체의 변수를 초기 생성해 줄 수 있는 메서드
(객체 == 인스턴스) ? true : false
앞서 언급했듯이 클래스를 통해 객체와 인스턴스가 생성된다.
그런데 객체는 클래스를 통해 만들어지고, 인스턴스도 클래스를 통해서 만들어진다.
과연 이 둘은 같은 걸까?
코드를 통해 알아보자.
class Player { // 클래스(설계도) 선언
int level;
int healthPower;
public Player() {
this.level = 1;
this.healthPower = 10;
}
public String levelUp() {
return "Level Up!";
}
}
public class MyClass {
public static void main(String[] args) {
Player p1 = new Player(); // 객체 생성, p1은 Player의 인스턴스
Player p2 = new Player();
}
}
Player 클래스를 선언하고, p1이라는 객체를 생성하였다.
p1은 힙 영역에 할당된 구체적인 실체를 가지며, 이때부터 "p1은 Player의 인스턴스"라고 부른다.
즉, 객체는 개념적이고, 인스턴스는 실제적인 개념을 갖는 관점이라고 이해하면 된다.
필드(Field)란?
클래스 필드는 클래스에 선언된 변수를 의미한다.
또한 클래스 필드는 선언된 위치와 선언자에 따라 아래와 같이 구분된다.
- 클래스 변수 | static 키워드로 선언된 변수
- 인스턴스 변수 | static 키워드로 선언되지 않은 변수
- 지역 변수 | 메서드나 블록 내에서 선언되고 소멸되는 변수
JVM에서 변수 종류에 대해 학습하였을 때 나온 변수들로 다시 한 번 상기시켜보자.
| 구분 | 생성 시기 | 소멸 시기 | 저장 영역 |
| 클래스 변수 | 클래스가 메모리에 올라갈 때 | 프로그램이 종료될 때 | Method Area |
| 인스턴스 변수 | 인스턴스가 생성될 때 | 인스턴스가 소멸할 때 | Heap Area |
| 지역 변수 | 블록 내 선언문이 실행될 때 | 블록을 벗어날 때 | Stack Area |
메서드(Method)란?
메서드는 우리가 흔히 접하게 될 개념으로 클래스 내의 함수를 의미한다.
특정 작업을 수행하기 위해 선언하며, 클래스의 기능을 정의한다.
자바의 모체 언어인 C에서는 '메서드'가 아닌 '함수'로서 작용하지만, 이름만 다를 뿐이니 크게 신경 쓰지 않아도 된다.
메서드는 생성 및 호출 시 다음과 같은 구성요소들을 필요로 한다.
- 접근 제어자 | 해당 메서드에 접근할 수 있는 외부 범위 명시
- 반환 타입 | 메서드가 코드를 실행하고 최종적으로 반환할 데이터 타입 명시
- 메서드명 | 메서드를 호출하기 위한 이름 명시
- 매개변수 | 메서드를 정의할 때 입력으로 전달된 값을 받는 변수
- 인수 | 메서드 호출 시 넘겨주는 입력값
class Player {
private String nickname = ""; // 인스턴스 변수
public String getNickname() {
return nickname;
}
public Player(String nickname) { // 생성자 생성
this.nickname = nickname;
}
public static String noti(){ // 클래스 메서드 생성
return "캐릭터가 생성되었습니다!";
}
}
public class MyClass {
public static void main(String[] args) {
Player p1 = new Player("John"); // 생성자 호출
System.out.println(Player.noti()); // 클래스 메서드 호출
System.out.println("해당 플레이어 이름은 " +p1.getNickname()); // 인스턴스 메서드 호출
Player p2 = new Player("Jane");
System.out.println(Player.noti());
System.out.println("해당 플레이어 이름은 " + p2.getNickname());
}
}
메서드도 변수와 마찬가지로 클래스 메서드와 인스턴스 메서드가 존재한다.
static 선언 유무에 따라 구분이 가능하며, 클래스 메서드는 메서드 내부에서 인스턴스 변수를 사용할 수 없다는 특징이 있다.
그렇기 때문에 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드를 클래스 메서드로 정의하는 것을 권장한다.
위의 코드를 보면 noti를 통해 플레이어가 생성될 때마다 생성 알림을 출력하였다.
이처럼 클래스 메서드는 모든 인스턴스에 공통으로 적용되는 정보나 로직을 실행할 경우에만 사용하고,
인스턴스 메서드인 getNickname은 유동적으로 변할 수 있는 인스턴스 변수를 다루는 경우에 사용하는 것이 좋다.
생성자(Constructor)
이미 위에서 몇 번의 실습을 통해 경험해 봤지만 다시 정리해보면,
생성자는 객체가 생성될 때 동적으로 인스턴스 변수 초기화를 위해 실행되는 메서드이다.
생성자에는 몇 가지 특징이 존재한다.
- 생성자의 목적은 객체 초기화임.
- 생성자 이름은 클래스명과 반드시 동일해야함.
- new를 통해 객체 생성 시, 생성자는 객체당 한 번만 호출함.
- 생성자는 객체가 생성될 때 반드시 호출됨.
- 생성자는 리턴 타입이 없으며, 지정할 수도 없음.
- 개발자가 생성자를 작성하지 않았으면 컴파일러가 자동으로 생성자를 생성해줌.
- 생성자는 오버로딩을 통해 여러 개 작성이 가능함.
오버로딩(Overloading)이란?
자바의 한 클래스 내에서 생성된 메서드에 대해 같은 이름을 가진 메서드가 있더라도 매개변수의 개수나 타입이 다르면 같은 이름을 사용하여 메서드를 정의하는 개념
메서드명이 같고, 매개변수의 개수나 타입이 달라야 하는 것이 필수 조건이며, 리턴 값만 다른 경우에는 오버로딩을 할 수 없다.
class Player {
int level;
int power;
int job;
public Player(int lv, int po, String job) { // 생성자 생성
this.level = lv;
this.power = po;
this.job = job;
}
}
public class MyClass {
public static void main(String[] args) {
Player p1 = new Player(1, 40, "궁수");
Player p2 = new Player(1, 80, "전사");
}
}
코드를 구현해 간단한 클래스를 작성해 보았다.
이때 생성자 안의 this를 볼 수 있는데, 해당 키워드는 클래스(=설계도) 자신을 나타내는 키워드이다.
'this' 자체가 인스턴스의 주소를 가리키고 있기 때문에 자기 자신에게 접근이 가능하다.
this를 변수가 아닌 메서드로도 적용할 수 있다!
생성자 내부에서만 사용 가능하며, this() 메서드에 인수를 전달하면, 이미 정의되어 있는 다른 생성자를 찾아 호출해 준다!
class Player {
int level;
int power;
int job;
public Player(int lv, int po, String job) { // 생성자 생성
this.level = lv;
this.power = po;
this.job = job;
}
Player() {
this(1, 50, "도적"); // this 메서드 사용
}
}
클래스 상속(Extend)
상속이란 개념은 자바 문법 작성 시 매우 많이 보게 될 요소 중 하나이다.
자바에서의 상속은 상위 클래스와 자식 클래스 간의 관계를 나타내는 단어로,
자식 클래스가 부모 클래스의 특징을 상속받아 사용할 수 있다는 의미이다.

상속을 통해 클래스 재사용 효율을 높혀주고, 코드 시간을 단축시킬 수 있다.
다만 상속에도 몇 가지 제한이 있다.
- 부모 클래스의 private 접근 제어자에 대한 요소들은 자식 클래스가 상속 받을 수 없다.
- 부모와 자식 클래스가 서로 다른 패키지에 있다면, 부모의 default 접근 제한을 갖는 필드나 메서드를 자식이 상속 받을 수 없다.
코드를 통해 이해해보자.
class Player {
int level;
int power;
String nickname;
public Player(String nickname) { // 부모 클래스 생성자
this.level = 1;
this.power = 30;
this.nickname = nickname;
}
}
class Archer extends Player {
public Archer(String nickname) { // 자식 클래스 생성자
super(nickname); // super 메서드로 부모 클래스 생성자 특징 가져가기
this.power = 40;
}
void kingShot(){...}
}
class Warrior extends Player {
public Warrior(String nickname) {
super(nickname);
this.power = 80;
}
void strike() {...}
}
class Thief extends Player {
public Thief(String nickname) {
super(nickname);
this.power = 50;
}
void stealth() {...}
}
public class MyClass {
public static void main(String[] args) {
Archer p1 = new Archer("Jane");
Warrior p2 = new Warrior("타락파워전사");
Thief p3 = new Thief("백엔드 개발자");
}
}
Player를 상속받은 Archer, Warrior, Thief 자식 클래스를 생성했다.
이렇게 각각의 자식 클래스는 부모 클래스의 특성을 물려받아 level, power, nickname 필드를 재생성할 필요가 없으며,
클래스 생성 목적에 맞게 유용하게 다루어 사용하면 된다!
이때 보지 못했던 것이 있는데, 바로 'super()'이다.
this처럼 목적에 따라 키워드 또는 메서드로 사용할 수 있다.
- super 키워드 | 자식 클래스가 부모 클래스의 필드나 메서드를 참조하는데 사용
- super 메서드 | 부모 클래스의 생성자를 호출
상대적으로 super가 this보다 빈번하게 사용되기 때문에 사용법을 확실히 알아두는 것이 좋다!
메서드 재정의(Method Overriding)
메서드 재정의란 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 것을 의미한다.
오버로딩과 다소 어감이 비슷하나,
오버로딩은 기존에 없던 새로운 메서드를 정의하는 것이고,
오버라이딩은 상속받은 클래스의 메서드를 재정의하는 것이다.
class Player {
int level;
int power;
String nickname;
public Player(String nickname) { // 부모 클래스 생성자
this.level = 1;
this.power = 30;
this.nickname = nickname;
}
public void intro() {
System.out.println("Player가 무직 클래스로 생성되었습니다!");
}
}
class Archer extends Player {
public Archer(String nickname) { // 자식 클래스 생성자
super(nickname); // super 메서드로 부모 클래스 생성자 특징 가져가기
this.power = 40;
}
public void intro() { // 메서드 오버라이딩
System.out.println("Player가 궁수 클래스로 생성되었습니다!");
}
void kingShot(){...}
}
public class MyClass {
public static void main(String[] args) {
Archer ar = new Archer();
ar.intro();
}
}
/*
Player가 궁수 클래스로 생성되었습니다!
*/
Import
'import'란 키워드는 클래스 내에서 코드 상단에 패키지를 명시해주면 해당 패키지 내의 클래스 파일을 사용할 수 있게 해준다.
클래스 파일을 가져올 때 특정 파일만 가져올 지, 해당 패키지의 모든 클래스 파일을 가져올지 선택할 수 있다.
바로 패키지 뒤에 '*' 키워드만 붙혀주면 모든 클래스 파일을 사용할 수 있다!
import java.util.Arrays; // java.util에 있는 Arrays 클래스 파일을 가져와 사용한다.
import java.lang.*; // java.lang 패키지 내에 있는 모든 클래스 파일을 가져와 사용한다.
위의 코드처럼 import를 꺼낼 수 있는데, 이때 "java.*"를 하면 그냥 코드 한 줄만 입력할 수 있지 않을까라는 생각을 할 수 있다.
정답은 "안된다"이다.
'*'는 모든 클래스 파일들을 포함하게 하지만, 해당 패키지에 포함된 하위 패키지의 클래스까지 포함시켜 주지는 않는다.
즉, 위에 코드처럼 java.utils와 java.lang에 있는 클래스 파일들을 사용하고 싶다면, 따로따로 명시해 주어야 한다.
static import
"import static" 문을 사용하면 정적 메서드나 필드를 클래스명 없이 사용할 수 있다.
아래 코드를 보자.
import static java.lang.System.out; // static 선언
public class MyClass {
public static void main(String[] args) {
out.println("Hello World!");
}
}
위를 보면 static import를 통해 System.out을 가져왔다.
그렇게 되면 출력 코드 작성 시에는 System.out을 제외한 out부터 사용하면 된다.
제어자(Modifier)
제어자란 클래스와 메서드 등 다양한 곳에서 사용되는 키워드이다.
이 포스트에서 직접적으로 언급했던 제어자는 접근 제어자 밖에 없었다.
하지만 static이나 final 같은 키워드도 제어자라고 부른다.
- 접근 제어자 | private, protected, public, default
- 기타 제어자 | static, final, abstract etc.
1. 접근 제어자
OOP에서는 사용자로부터 숨겨야 하는 정보들이 존재한다.
접근 제어자는 클래스와 클래스의 멤버를 사용할 때, 접근할 수 있는 범위를 지정해 주는 역할을 한다.
접근 제어자는 4가지로 접근 범위 지정이 가능하다.
- private | 같은 클래스 내에서만 접근 가능
- default | 같은 패키지 내에서만 접근 가능
- protected | 같은 패키지나 다른 패키지의 자손 클래스에서 접근이 가능
- public | 접근 제한 X
2. 기타 제어자
2-1. Static
static 멤버는 메모리 영역 내 메서드 영역에 생성된다.
동일한 클래스의 모든 객체에 의해 공유가 가능하며, this 사용이 불가능하다.(인스턴스가 아니기 때문이다.)
2-2. final
final 키워드는 값의 변경이 불가능하다는 것을 명시하는 키워드이다.
final은 상수(=constant) 풀에 저장되며, 오버라이딩을 통한 재정의가 불가능하다.
클래스에 final을 명시할 시, 해당 클래스에 대한 다른 클래스가 상속을 받을 수 없다.
3. 제어자 조합
제어자끼리의 조합은 다양하게 사용될 수 있다.
하지만 사용이 불가능한 제어자 조합이 있으며, 대상에 따라 다르게 적용된다
- 클래스에 final과 abstract는 함께 사용 불가
- 메서드에 static과 abstract는 함께 사용 불가
- 메서드에 private과 abstract는 함께 사용 불가
- 메서드에 private과 final에 함께 사용 불가
출처
1- Inpa Dev
'JAVA' 카테고리의 다른 글
| 추상 클래스? 알려드리겠습니다! (2) | 2026.01.20 |
|---|---|
| JAVA 배열을 모르겠다고? 알려드리겠습니다! (0) | 2026.01.06 |
| JAVA 타입 형변환 (1) | 2026.01.04 |
| JAVA 기본형 데이터 타입 (0) | 2025.12.31 |
| JAVA 변수 종류 (0) | 2025.12.30 |