Do You Coding?

[Java] 추상화 (Abstraction) 본문

Backend/Java

[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) 구조다.
메서드 호출 시 다음과 같은 과정을 거친다:

  1. 메서드 호출 시마다 메모리 상자(프레임)를 하나씩 할당
  2. 상자 내부에 파라미터 변수 등 로컬 변수 구성
  3. A에서 B를 호출하면 B 실행용 메모리 상자를 위에 쌓음
  4. 맨 위 상자(B)만 활성화되고, A는 잠시 정지
  5. B가 리턴하면 B 상자가 제거되며 메모리 반납
  6. 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의 용도

  1. 로컬 변수와 멤버 변수의 이름이 동일할 경우 멤버 변수임을 명시
  2. 명시적으로 멤버임을 나타낼 경우 사용
  3. 자기 자신의 참조를 다른 메서드에 전달할 때 사용
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