Do You Coding?
[Java] 추상화 (Abstraction) 본문

Java 추상화(Abstraction) 완전 정리
Java OOP의 핵심인 추상화 개념부터 클래스, 변수, 메서드, 생성자까지 핵심만 정리한 글입니다.
1. OOP(객체지향 프로그래밍)의 4대 특성 — A.P.I.E
객체지향 언어의 핵심 특성 네 가지를 A.P.I.E로 기억하면 편하다.
| 특성 | 설명 |
|---|---|
| Abstraction (추상화) | 현실의 객체를 추상화해서 클래스를 구성한다. |
| Polymorphism (다형성) | 하나의 객체를 여러 가지 타입(형)으로 참조할 수 있다. |
| Inheritance (상속) | 부모 클래스의 자산을 물려받아 자식을 정의한다. 코드의 재사용이 가능하다. |
| Encapsulation (데이터 은닉과 보호) | 데이터를 외부에 직접 노출시키지 않고 메서드를 이용해 보호한다. |
2. 객체지향 프로그래밍이란?
2-1. 객체란?
객체(Object) 는 주체가 아닌 것, 주체가 활용하는 것이다.
우리 주변에 있는 모든 것이 프로그래밍의 대상이 될 수 있다.
사물, 개념, 논리 등이 모두 해당된다.
2-2. 객체지향 프로그래밍의 핵심
주변의 많은 것들을 객체화해서 프로그래밍하는 것이다.
블록 형태의 모듈화된 프로그래밍이라고 생각하면 된다.
마치 레고처럼, 비행기 날개를 수정하고 싶으면 해당 블록만 떼고 붙이면 된다.
2-3. 객체지향 프로그래밍의 장점
- 블록 형태의 모듈화로 신뢰성 높은 프로그래밍이 가능하다.
- 추가/수정/삭제가 용이하다.
- 재사용성이 높다.
3. 추상화와 구체화
객체지향의 첫걸음은 클래스다.
현실 객체의 속성과 기능을 추상화시켜 클래스에 정의한다.
클래스는 구체화되어 프로그램의 객체가 된다.
현실 객체 ──추상화──▶ 클래스(설계도) ──구체화──▶ 프로그램 객체(인스턴스)3-1. 현실 객체에서 클래스로
예를 들어 "철수"와 "영희"라는 현실 객체가 있다고 하자.
| 철수 | 영희 | |
|---|---|---|
| 상태(속성) | 이름: 철수, 나이: 27, 배고픔: X | 이름: 영희, 나이: 25, 배고픔: X |
| 기능(행위) | 먹는다, 일한다 | 먹는다, 일한다 |
이 공통된 속성과 행위를 추상화하면 Person 클래스가 된다.
3-2. 클래스에서 객체로
Person 클래스
├── 멤버변수: String name, int age, boolean isHungry
└── 멤버메서드: void eat(), void work()이 클래스를 구체화(인스턴스화)하면 메모리에서 살아있는 객체가 된다.
// 구체화 예시
Person p = new Person();
p.name = "철수";
p.age = 20;
p.isHungry = false;
p.eat();
p.work();
4. 클래스와 객체 — 코드 예제
4-1. Person 클래스 정의
package com.doyoucode.day02.a_person;
public class Person {
String name;
int age;
boolean isHungry;
void eat() {
System.out.println("냠냠");
isHungry = false; // 먹으면 배고프지 않음
}
void work() {
System.out.println("열심");
isHungry = true; // 일하면 배고파짐
}
}
4-2. Person 클래스 테스트
package com.doyoucode.day02.a_person;
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "홍길동";
p1.isHungry = true;
System.out.println(p1.name + " : " + p1.isHungry); // 홍길동 : true
p1.eat();
System.out.println(p1.name + " : " + p1.isHungry); // 홍길동 : false
Person p2 = new Person();
p2.name = "김경호";
p2.age = 10;
p2.isHungry = true;
System.out.println(p2.name + " : " + p2.isHungry); // 김경호 : true
System.out.println(p1.name + " : " + p1.isHungry); // 홍길동 : false
}
}
각 객체는 독립적인 상태값을 가진다.
p2의 상태를 바꿔도p1에는 영향이 없다.
5. JVM 메모리 구조
Java 프로그램 실행 시 메모리는 크게 세 영역으로 나뉜다.
| 영역 | 역할 | 특징 |
|---|---|---|
| meta-space | 클래스 정보 처리 | 타입/Field/Method 정보, 상수 풀 |
| stack | 메서드의 실행 공간 | thread 별로 별도 관리, 메서드 호출 시마다 프레임 적재 |
| heap | 객체를 저장하는 영역 | thread에 의해 공유, G.C에 의해 정리 |
5-1. 메모리 동작 흐름 예시
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "홍길동"; p1.isHungry = true;
p1.eat();
Person p2 = new Person();
p2.name = "임꺽정"; p2.isHungry = true;
}
metaspace stack heap
┌──────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Person │ │ PersonTest │ │ 0x200 (홍길동) │
│ - name │ │ .main │ │ name, age, │
│ - age │ │ ┌──────────┐ │ │ isHungry = false │
│ - isHung │ │ │Person p1 │─┼────▶└─────────────────────┘
│ - eat() │ │ │ = 0x100 │ │
│ - work() │ │ ├──────────┤ │ ┌─────────────────────┐
│ │ │ │Person p2 │─┼────▶│ 0x400 (임꺽정) │
│PersonTest│ │ │ = 0x300 │ │ │ name, age, │
│ - main() │ │ └──────────┘ │ │ isHungry = true │
└──────────┘ └──────────────┘ └─────────────────────┘
stack의 참조변수(p1, p2)가 heap에 있는 실제 객체를 가리키는 구조다.
6. 변수의 종류
6-1. 타입에 따른 분류
| 변수 종류 | 특징 | 비고 |
|---|---|---|
| Primitive Type variable | 기본 8가지 type의 변수 | int i, char c, float f... |
| Reference Type variable | 나머지 모든 것(객체 참조) | String s, int[] points, Person p... |
6-2. 선언 위치에 따른 분류
| 종류 | 선언 위치 | 변수 종류 |
|---|---|---|
| 멤버 변수 | 클래스 영역 (static keyword) | 클래스 멤버 변수 |
| 멤버 변수 | 클래스 영역 | 인스턴스 멤버 변수 |
| 지역 변수 | 함수 내부 | 지역 변수 |
| 지역 변수 | 함수 선언부 | 파라미터 변수 |
public class VariableTypes {
int instanceVariable; // 인스턴스 멤버 변수
static int classVariable; // 클래스 멤버 변수
public static void main(String[] args) { // 파라미터 변수
int localVariable = 10; // 로컬 변수
for (int i = 0; i < 100; i++) { // 로컬 변수
System.out.println(i);
}
}
}
6-3. 인스턴스 멤버 변수의 특징
- 선언 위치: 클래스
{}영역에 선언 - 변수의 생성: 객체가 만들어질 때 heap에 객체 별로 생성됨
- 변수의 초기화: 타입 별로 default 초기화 후 명시적 초기화
- 변수에의 접근: 객체 생성 후 객체 이름(소속)으로 접근
- 소멸 시점: G.C에 의해 객체가 없어질 때 소멸. 프로그래머가 명시적으로 소멸 불가
객체를 만들 때마다 객체 별로 생성된다.
따라서 객체마다 고유한 상태(변수 값)를 유지한다.
public class Person {
String name;
int age = 10;
}
6-4. 클래스 멤버 변수(static)의 특징
- 선언 위치: 클래스
{}영역에 static 키워드를 붙여 선언 - 변수의 생성: 클래스가 로딩될 때 상수 풀에 구성
- 변수의 초기화: 타입 별로 default 초기화 후 명시적 초기화
- 변수에의 접근: 객체 생성과 무관하게 클래스 이름(소속)으로 접근
- 소멸 시점: 클래스가 언로드 될 때 G.C 발생
개별 객체의 생성과 무관하며 모든 객체가 공유하게 된다.
공유 변수라고도 부른다.
public class Person {
static String scientificName = "Homo Sapiens"; // 클래스 변수
String name; // 인스턴스 변수
}
// 접근 방법
Person p = new Person();
p.scientificName = "객체를 통한 변경"; // 가능하지만 비추천 (경고 발생)
Person.scientificName = "클래스를 통한 변경"; // 올바른 접근 방법
static 변수는 반드시 클래스명으로 접근하는 것이 좋다.
그렇지 않으면 공유 변수인지 헷갈리기 때문이다.
static 변수 주의사항 — side effect
public class B {
int a; // 인스턴스 변수
static int b; // 클래스 변수 (공유)
public static void main(String[] args) {
B.b = 1; // 클래스 이름으로 접근 (권장)
B b1 = new B();
B b2 = new B();
B b3 = new B();
b1.a++; // 각각 독립
b2.a++; // 각각 독립
b3.a++; // 서로 영향 X
b1.b++; // 같은 static 변수에 접근!
b2.b++; // side effect 발생
b3.b++; // 세 줄 모두 같은 변수 → B.b는 4가 됨
}
}
static 변수는 상태값이 꼬일 수 있다.
일반적으로 사용을 권장하지 않는다.
다만 알고리즘 풀이 시에는 필요에 따라 많이 쓰는 편이다.
6-5. 지역 변수 & 파라미터 변수
- 선언 위치: 클래스
{}이외의 모든 중괄호 안 (메서드, 생성자, 초기화 블록) - 변수의 생성: 선언된 라인 실행 시 stack의 메서드 프레임 내부에 생성
- 변수의 초기화: 사용 전 명시적 초기화 필수 (default 초기화 없음!)
- 변수에의 접근: 외부 접근 불가, 내부에서 이름으로 바로 접근
- 소멸: 선언된 영역인
{}을 벗어날 때
void call(String to) { // 파라미터 변수
String beep = "띠"; // 로컬 변수
for (int i = 0; i < 3; i++) { // 로컬 변수
System.out.println(beep);
}
}
7. 메서드
7-1. 메서드의 정의와 필요성
메서드란 현실 객체의 동작을 프로그래밍한 것이다.
어떤 작업을 수행하는 명령문의 집합이다.
메서드를 작성하는 이유:
- 반복 코드의 중복 방지 (DRY: Don't Repeat Yourself!)
- 코드의 양을 줄이고 유지 보수가 용이
class Person {
String name;
int age = 0;
// 메서드로 묶으면 한 군데서만 수정해도 됨
void printInfo() {
System.out.println(name + " : " + age);
}
}
// 메서드를 사용하지 않았을 경우 → 중복 발생
Person p = new Person();
System.out.println(p.name + ":" + p.age);
Person p2 = new Person();
System.out.println(p2.name + ":" + p2.age);
// 메서드를 사용했을 경우 → 깔끔
Person p = new Person();
Person p2 = new Person();
p.printInfo();
p2.printInfo();
7-2. 메서드 선언부
리턴 타입
- 메서드를 호출한 곳으로 반환되는 값의 타입
- 아무것도 리턴하지 않을 경우 void
- 리턴 타입은 하나만 적용 가능
public int add(int a, int b) {
return a + b;
}
int result1 = add(100, 200); // OK
double result2 = add(100, 200); // OK (묵시적 형 변환)
- 리턴 타입이 void면 return 문 생략 가능
- 조건문으로 return 할 경우 모든 조건에서 return 필요
파라미터 목록
- 메서드 호출 시 넘겨줘야 하는 변수들이다.
- 넘겨줄 정보가 없으면 생략 가능하다.
- 파라미터 전달 시 묵시적 형변환이 적용된다.
public long add(long a, long b) {
return a + b;
}
add(100, 200); // OK — int → long 묵시적 형변환
add(1.1, 2.2); // 컴파일 에러 — double → long 불가
7-3. Variable Arguments (가변인자)
동일 타입의 인자가 몇 개 들어올지 예상할 수 없을 때 사용한다.... 을 이용해 가변인자를 선언하면 된다.
내부적으로는 배열로 처리된다.
public static void main(String[] args) {
VariableTest vt = new VariableTest();
vt.addAll(1, 2, 3);
vt.addAll(1, 2, 3, 4, 5);
vt.addAll(1, 2);
}
public void addAll(int... params) { // 0개 이상의 int를 받음
int sum = 0;
for (int i : params) {
sum += i;
}
System.out.println(sum);
}
printf도 가변인자를 활용한 대표적인 메서드다.public PrintStream printf(String format, Object... args)
7-4. class 멤버와 instance 멤버 간의 참조와 호출
핵심은 호출하려는 멤버가 메모리에 있는가? 이다.
| 비고 | static member | instance member |
|---|---|---|
| 소속 | 클래스 | 객체 |
| 같은 클래스에서 접근 | 바로 호출 | 바로 호출 |
| 다른 클래스에서 접근 | 클래스_이름.멤버_이름 |
객체 생성 후 객체_이름.멤버_이름 |
- static member → 언제나 메모리에 있음 (클래스 로딩 시 자동 등록)
- instance member → 객체 생성 전에는 메모리에 없음
메모리에 있으면 호출 가능, 없으면 불가.
먼저 메모리에 로딩 후 사용해야 한다.
instance 멤버는 레퍼런스를 통해서 접근한다.
public class First {
static int cv;
int iv;
static void cMethod() {}
void iMethod() {}
void iMethodB() {
// instance 메서드 안에서는 static, instance 모두 접근 가능
cv = 100; // static 변수 접근 OK
iv = 10; // instance 변수 접근 OK
cMethod(); // static 메서드 호출 OK
iMethod(); // instance 메서드 호출 OK
}
}
class Second {
static void cMethod() {}
void iMethod() {}
void iMethodB() {
// 다른 클래스의 static 멤버 → 클래스명으로 접근
First.cv = 100;
First.cMethod();
// 다른 클래스의 instance 멤버 → 객체 생성 후 접근
First f = new First();
f.iv = 10;
f.iMethod();
}
}
7-5. 메서드 호출 스택
스택(stack) 은 FILO(First In Last Out) 구조다.
메서드 호출 시 다음과 같은 과정을 거친다:
- 메서드 호출 시마다 메모리 상자(프레임)를 하나씩 할당
- 상자 내부에 파라미터 변수 등 로컬 변수 구성
- A에서 B를 호출하면 B 실행용 메모리 상자를 위에 쌓음
- 맨 위 상자(B)만 활성화되고, A는 잠시 정지
- B가 리턴하면 B 상자가 제거되며 메모리 반납
- A가 최상위가 되어 다시 동작 재개
동작 중 aMethod 호출 bMethod 호출 bMethod 리턴
┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ aMethod │ │ bMethod │ │ aMethod │ ← 재시작
│ │ │ age | 10 │ │ age | 10 │ │ age | 10 │
│ main │ ├──────────┤ ├──────────┤ ├──────────┤
│args{}│ │ main │ │ aMethod │ │ main │
└──────┘ │ args {} │ │ age | 10 │ │ args {} │
동작 중 └──────────┘ ├──────────┤ └──────────┘
일시정지 │ main │
│ args {} │
└──────────┘8. 메서드 오버로딩 (Method Overloading)
동일한 기능을 수행하는 메서드를 추가 작성하는 것이다.
이름은 같게, 파라미터만 다르게 만든다.
8-1. 왜 필요할까?
println이 대표적인 예시다.
사실 println은 파라미터 타입별로 모두 다른 메서드다.
// 실제 println 오버로딩 (각각 다른 메서드)
public void println(int x)
public void println(char x)
public void println(String x)
만약 오버로딩이 없었다면 이렇게 써야 한다:
// 오버로딩이 없다면...
System.out.printlnInt(1);
System.out.printlnChar('C');
System.out.printlnString("Hi");
오버로딩 덕분에 기억해야 할 메서드가 줄어든다.
중복 코드에 대한 효율적 관리도 가능하다.
8-2. 오버로딩 규칙
- 메서드 이름은 동일
- 파라미터의 개수 또는 순서/타입이 달라야 한다. 같으면 중복 선언 오류.
- 리턴 타입은 오버로딩 판단에 영향 없음. 파라미터가 달라야 한다.
8-3. 오버로딩 예제
void walk() {
walk(100, "cm"); // 기본값으로 호출
}
void walk(double distance) {
walk(distance, "cm"); // 단위 기본값 위임
}
void walk(double distance, String unit) {
// 동일 로직을 여기에 모아서 중복 코드 제거
distance = switch (unit) {
case "cm" -> distance;
case "inch" -> distance * 2.54;
default -> {
System.out.println("unknown");
yield 0;
}
};
System.out.println(distance + "cm 이동");
}
중복 코드가 생기면 파라미터가 가장 많은 메서드에 로직을 모으자.
나머지는 그 메서드를 호출하도록 위임(delegate) 하면 된다.
9. 생성자 (Constructor)
9-1. 생성자란?
Person person1 = new Person(); 에서 new 키워드와 함께 호출하는 것이 생성자다.
- 메서드와 비슷하지만 반환 타입이 없다 (void도 아님!)
- 이름은 클래스 이름과 동일
- 인스턴스 생성 시점에 실행되는 특수 메서드
9-2. 기본 생성자 (Default Constructor)
생성자 코드를 작성하지 않으면 컴파일러가 기본 생성자를 제공해준다.
주의: 매개변수가 있는 생성자를 직접 만들면
컴파일러가 기본 생성자를 넣어주지 않는다!
필요하면 직접 작성해야 한다.
public class C {
int a;
public C() { // 클래스가 public이면 public 붙음
a = 10; // 인스턴스 변수 초기화
init(); // 객체 생성 후 초기 작업 진행
}
void init() {
// 초기 작업용 메소드
}
}
9-3. 생성자 오버로딩과 this()
생성자도 오버로딩이 가능하다.
생성자에서 다른 생성자를 호출할 때는 this() 를 사용한다.
class Data {
int a;
int b;
int c;
Data() {
this(3); // Data(int a) 생성자 호출
}
Data(int a) {
this(a, 3, 3); // Data(int a, int b, int c) 생성자 호출
}
Data(int a, int b, int c) {
this.a = a; // this로 멤버변수와 파라미터 구분
this.b = b;
this.c = c;
System.out.println("생성자3");
}
}
this()는 생성자에서만 사용한다.
일반 메서드에서 오버로딩된 메서드를 호출할 때는 그냥 메서드 이름으로 호출하면 된다.
10. 초기화 블록 (Initializer Block)
10-1. static 블록 vs 인스턴스 블록
| 구분 | static 블록 | 인스턴스 블록 |
|---|---|---|
| 문법 | static { ... } |
{ ... } |
| 실행 시점 | 클래스 로딩 시 (딱 1번) | 생성자 호출 시마다 (생성자보다 먼저) |
| 용도 | 클래스 변수 초기화 | 모든 생성자 공통 실행 코드 |
class GData {
static {
System.out.println("static"); // 1. 클래스 로딩 시 최초 1회
}
{
System.out.println("instance"); // 2. 객체 생성 시마다 생성자보다 먼저
}
GData() {
System.out.println("생성자"); // 3. 마지막으로 생성자 실행
}
static void f1() {
System.out.println("f1");
}
}
public class D {
public static void main(String[] args) {
GData g; // 이 줄만 있으면 static 블록 호출 X
g = new GData(); // → "static", "instance", "생성자" 순서
GData.f1(); // → "f1" (static 블록은 다시 실행 안 함)
}
}
10-2. 초기화 블록 상세 예시
static {
System.out.println("static initializer");
cv = 100; // 클래스 변수 초기화
}
{
System.out.println("instance initializer");
iv = 100; // 인스턴스 변수 초기화
}
public InitializerTest() {
System.out.printf("생성자- iv: %d, cv: %d%n", iv, cv);
this.iv = 300; // 생성자에서 다시 덮어쓰기 가능
}
10-3. 객체 배열 생성 시 주의
int[] arr = new int[3];
System.out.println(arr[0]); // 0 (기본값)
GData[] grr = new GData[3];
System.out.println(grr[2]); // null (객체가 아직 생성되지 않음!)
// 객체 배열은 반복문으로 각각 생성해야 한다
for (int i = 0; i < grr.length; i++) {
grr[i] = new GData(); // 이제야 객체들이 생성됨
}
객체 배열을 선언하면 참조 변수 배열만 만들어진다.
실제 객체는 생성되지 않으며 default 값은 null이다.
11. this 키워드
11-1. this란?
참조 변수로써 객체 자신을 가리킨다.
참조변수로 객체의 멤버에 접근하듯, this를 이용해 자신의 멤버에 접근 가능하다.
11-2. this의 용도
- 로컬 변수와 멤버 변수의 이름이 동일할 경우 멤버 변수임을 명시
- 명시적으로 멤버임을 나타낼 경우 사용
- 자기 자신의 참조를 다른 메서드에 전달할 때 사용
class SB {
void m1(C c) {
// C 객체를 받아서 처리
}
}
public class C {
int age; // 인스턴스 멤버 변수
void fff() {
SB sb = new SB();
sb.m1(this); // 자기 자신(C 객체)을 넘김
}
void setAge(int age) {
this.age = age; // this.age는 멤버변수, age는 파라미터
this.f1(); // this 생략 가능
}
void f1() {}
}
메서드는 객체마다 만들어지지 않는다.
필드는 객체마다 만들어지지만, 메서드는 하나만 존재한다.
내부적으로this가 전달되어 어떤 객체의 호출인지 구분한다.
예:d1.m1()은 보이지 않지만d1.m1(d1)처럼 자기 자신이 전달된다.
핵심 정리
| 개념 | 핵심 요약 |
|---|---|
| 추상화/구체화 | 현실 객체 → 클래스(설계도) → 인스턴스(실체) |
| 인스턴스 변수 | 객체마다 독립, heap에 생성, G.C가 소멸 |
| 클래스(static) 변수 | 모든 객체가 공유, 클래스명으로 접근 권장 |
| 지역 변수 | 명시적 초기화 필수, 블록 벗어나면 소멸 |
| 메서드 | DRY 원칙, 중복 코드 제거 |
| 오버로딩 | 같은 이름 + 다른 파라미터, 리턴 타입은 무관 |
| 생성자 | 클래스명과 동일, 반환 타입 없음, this()로 체이닝 |
| 초기화 블록 | static 블록(1회) → 인스턴스 블록(매번) → 생성자 |
| this | 자기 자신 참조, 멤버변수/파라미터 구분 |
'Backend > Java' 카테고리의 다른 글
| [Java] 상속 (Inheritance) (0) | 2026.03.18 |
|---|---|
| [Java] Java 기본 문법 - 변수, 타입, 배열, 문자열 (0) | 2026.03.17 |