JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

Java/자바 학습

자바의 클래스(Class)

lamarcK 2025. 5. 1. 15:00

클래스란 무엇인가?

개념 설명

클래스는 객체 지향 프로그래밍의 기본 단위로, 관련된 데이터와 메서드(method)를 하나의 단위로 묶어놓은 설계도다. (자바스크립트의 class와 유사) 실제 프로그램에서 사용되는 객체(Object)를 만들기 위한 템플릿 역할을 한다. 이렇게 만들어진 객체를 인스턴스라고 부른다.

비유 설명

하지만 실제로 클래스가 어떤 것을 만들어야 한다고 강제하는 것은 아니다. 설계도라는 것은 해당 클래스에 대강 그런 기능들을 넣을 것이라 정해두는 것이라 설계도라 부르는 것이다. 클래스는 관련된 함수(메서드)를 담는 콘테이너이자 일종의 카테고리다. 내부에 어떤 속성과 기능이 있을지 정의해두는 틀이자 대강 무엇을 담을지 정해두는 것이지 실제로 클래스 자체가 어떤 역할을 하는 것은 아니다. 일종의 물건들을 넣어두는 서랍 같은 것이다. 서랍 안에 특정 종류의 물건만 넣어둘 수도 있지만 이것저것 섞어 놓을수도 있다. 다만 기왕이면 비슷한 기능을 하거나 같은 분류의 것들을 넣어두는 것이 실제 코드 가독성이나 유지 보수 측면에서 좋기 때문에 클래스를 나누는 것이다. 실제론 엄청 큰 서랍을 만들어서 집을 통째로 넣을 수도 있을 것이다.

 

  • 클래스 : 동물의 '종(種)' 개념
    • 객체/인스턴스 : 실제 개별 동물
  • 클래스 : '개'라는 종족
    • 객체 : 실제 '바둑이', '멍멍이' 등
  • 클래스 : '자동차'
    • 객체 : 엔진, 바퀴, 좌석 등등

사용 목적

  • 코드의 재사용성 향상
    • 비슷한 기능을 가진 것들을 넣어둬서 꺼내서 사용할 수 있도록 하는 것이다.
    • 1개의 클래스에 모든 기능을 다 넣는다면 재사용이 불가능하다.
  • 데이터 캡슐화를 통한 보안 강화
  • 유지보수의 용이성
  • 프로그램의 구조화
    • 비슷한 분류의 기능들을 클래스 별로 나눠서 전체적인 코드를 구조화 하는 것이다.

자주하는 실수

  • 클래스와 객체의 개념 혼동
    • 클래스는 컨테이너, 객체는 실제 아이템이다.
  • 접근 제어자 사용 실수
  • 생성자 오버로딩 미숙
  • static 키워드의 잘못된 사용

대치 개념 비교

구분 클래스 객체(인스턴스)
정의 설계도 실체
메모리 로드 시점 런타임 시점
개수 하나만 존재 여러 개 생성 가능
상태 고정 변경 가능

실무 활용 예시

  • DTO(Data Transfer Object) 클래스 작성
  • 서비스 로직 클래스 구현
  • 엔티티 클래스 정의
  • 유틸리티 클래스 작성
  • 컨트롤러 클래스 구현

클래스 구조 예시:

public class Person {
    // 필드(멤버 변수)
    private String name;
    private int age;

    // 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메소드
    public void sayHello() {
        System.out.println("Hello, I'm " + name);
    }
}

클래스 구성요소 표

구성요소 설명 예시
필드 클래스의 속성/상태 private String name;
생성자 객체 초기화 메소드 public Person(){}
메소드 클래스의 기능/행위 public void sayHello(){}
접근제어자 접근 권한 설정 public, private, protected

클래스의 구조

각 요소의 선언 순서 (일반적인 컨벤션):

  1. 클래스 선언
  2. 상수 (static final)
  3. 클래스 변수 (static)
  4. 인스턴스 변수
  5. 초기화 블록
  6. 생성자
  7. 메서드
  8. 중첩 클래스/인터페이스

클래스 선언

클래스의 이름과 특성을 정의하는 부분이다. 클래스는 기본적으로 접근제어자로 스코프를 정해줘야한다. 클래스 선언 시 접근 제어자는 선택적으로 사용할 수 있으며, 접근 제어자를 명시하지 않으면 자동으로 default(패키지-프라이빗) 접근 제어자가 적용된다.

기본 구조:

[접근제어자] [기타제어자] class 클래스명 [extends 부모클래스] [implements 인터페이스] {
    // 클래스 내용
}
  • 접근제어자 (Access Modifier): public, private, protected,  default(없음)
  • 기타제어자 (Other Modifiers): abstract, final, static
  • extends 부모클래스: 상속 받을 부모클래스를 넣는 부분
    • 부모 클래스의 메서드와 필드를 물려받아 사용
    • 부모 클래스의 메서드를 재정의(오버라이딩)
    • 부모 클래스의 접근 가능한 멤버들을 확장
    • final 클래스는 상속 불가
    • 다중 상속 불가 (하나의 클래스만 상속 가능)
    • private 멤버는 상속은 되지만 접근 불가
  • implements 인터페이스: 인터페이스를 구현 하는 부분
    • 이 클래스에 해당 인터페이스에 정의된 명세를 구현할 것이라고 선언해두는 부분이다
    • 만약에 인터페이스에 정의된 메서드를 클래스에서 구현하지 않으면 오류가 난다

클래스 내부 구조

사실상 자유롭게 코드를 작성할 수 있는 부분이다. 

  • 상수, 변수, 메서드, 생성자, 내부 클래스 등

상수

//상수 (static final) - 보통 클래스 최상단에 위치
    public static final String SPECIES = "Human";
    private static final int MAX_AGE = 150;

상수라는 것은 값이 변하지 않는 변수를 말한다. 변수 앞에 기타제어자 final을 붙여서 선언할 수 있다.

static : static 을 추가로 붙인다면 클래스 레벨에서 공유가 가능하다. 클래스 레벨이라는 것은 클래스끼리 공유가 가능하다는 것으로 상수를 다른 클래스들이 사용할 수 있다는 것을 의미한다.

만약에 static을 사용하지 않는다면 인스턴트 레벨의 상수가 되는데 이 경우엔 해당 클래스의 인스턴스를 통해서만 접근이 가능하다.

인스턴스라는 것은 클래스를 사용해서 생선된 객체다.

하지만 상수를 굳이 인스턴스 레벨에서 사용할 이유가 없기 때문에 보통은 static final을 사용한다.

 

예시를 들자면 아래와 같다.

public class MyClass {
    public static final int CONSTANT = 100;   // 클래스 레벨 상수
    public final int CONSTANT2 = 100;         // 인스턴스 레벨 상수
    
    public static void main(String[] args) {
        // static final 상수 접근 방법
        System.out.println(CONSTANT);         // 직접 접근 가능
        System.out.println(MyClass.CONSTANT); // 클래스로 접근도 가능
        
        MyClass obj = new MyClass();
        // final 상수 접근 방법
        System.out.println(obj.CONSTANT2);    // 인스턴스로만 접근 가능
        // System.out.println(CONSTANT2);     // 에러! 직접 접근 불가
        // System.out.println(MyClass.CONSTANT2); // 에러! 클래스로 접근 불가
    }
}

클래스 변수

    //클래스 변수 (static 변수) 
    private static int count = 0;

상수와 동일한 매커니즘을 가진다. 다만 고정된 값이 아니라서 재할당이 가능하다. 즉, 값을 변경할 수 있다. static을 사용해서 클래스 레벨에서 작동한다.


인스턴스 변수

//인스턴스 변수 (필드)
    private String name;    // private 접근제어자로 캡슐화
    private int age;
    protected String address;  // 같은 패키지와 자식 클래스에서 접근 가능
    public boolean isActive;   // 어디서나 접근 가능

상수와 동일한 매커니즘을 가진다. static을 사용하지 않아 인스턴스 레벨에서만 작동한다. 마찬가지로 상수가 아니라서 값을 변경할 수 있다.

 

클래스 변수와 인스턴스 변수를 분리하는 이유

클래스 변수라는 것은 클래스 단위에서 공유되는 변수라는 뜻이다. 때문에 클래스 내부의 어느 한 부분에서 변수의 값이 바뀌게 된다면 해당 변수를 사용하는 모든 부분에서 값이 변경된다. 

하지만 인스턴스 변수의 경우 개별 인스턴스마다 별개로 작용한다. 즉 서로간의 공유가 되지 않는 변수이기 때문에 동일한 이름을 가진 변수의 값을 변경해도 해당 인스턴스 내부에만 머물지 다른 인스턴스 내부에서 사용되는 변수의 값에는 영향을 끼치지 않는다. 

public class Student {
    // 클래스 변수 (모든 인스턴스가 공유)
    public static int totalCount = 0;
    
    // 인스턴스 변수 (각 인스턴스마다 독립적)
    public String name;
    
    public Student(String name) {
        this.name = name;    // 각 인스턴스마다 다른 값
        totalCount++;        // 모든 인스턴스가 공유하는 값
    }
}

public class Main {
    public static void main(String[] args) {
        Student student1 = new Student("Kim");
        Student student2 = new Student("Lee");
        
        // 인스턴스 변수는 각각 독립적
        System.out.println(studentname);     // "Kim"
        System.out.println(studentname);     // "Lee"
        
        // 클래스 변수는 공유됨
        System.out.println(Student.totalCount); // 2
        System.out.println(studenttotalCount); // 2
        System.out.println(studenttotalCount); // 2
    }
}

 

메모리적 측면의 접근

클래스 변수 (static)

  • Method Area(Static Area)에 저장
  • 프로그램 시작 시 생성되어 프로그램 종료 시까지 유지
  • 하나의 메모리 공간만 할당됨
  • 모든 인스턴스가 이 하나의 메모리 공간을 공유

인스턴스 변수

  • Heap 영역에 저장
  • 인스턴스가 생성될 때마다 새로운 메모리 공간 할당
  • 각 인스턴스마다 별도의 메모리 공간을 가짐
  • 인스턴스가 가비지 컬렉션되면 메모리에서 제거

때문에 static 변수를 사용해야 하는 경우는 다음과 같으며

  • 모든 인스턴스가 공유해야 하는 값
  • 프로그램 전체에서 일관되게 사용되는 상수
  • 전체 개수 카운트 등 공통 데이터

인스턴스 변수를 사용해야 하는 경우는 다음과 같다.

  • 각 객체별로 다른 값을 가져야 하는 경우
  • 객체의 고유한 상태를 표현해야 할 때
  • 임시적으로 사용되는 데이터

역할론적 관점에서 변수을 본다면 인스턴스 내부에서 해당 인스턴스에만 영향을 주는 변수의 경우 인스턴스 변수가 적합하며 다른 인스턴스 모두에게 영향을 주는 경우엔 클래스 변수가 적합하다. 값의 변화 주기가 문제가 아니라 해당 변수의 용도에 따라 정해져야 하는 것이다. 값이 빈번하게 변하더라도 클래스 전체에서 사용되야 한다면 클래스 변수로 선언되야 한다. 반대로 값은 전혀 변하지 않더라도 하나의 인스턴스 내부에서만 사용되야 한다면 인스턴스 변수로 사용해야한다. 

 

메모리적 관점에서 본다면 메모리 소비를 줄일 수 있는 관점에서 접근해야한다. 클래스 변수로 선언하면 딱 한번만 메모리 공간에 할당되기 때문에 각각의 인스턴스에 할당되는 인스턴스 변수에 비해서 메모리 공간적 효율성을 높일 수 있지만 반대로 인스턴스 변수가 금방 사용되고 사라진다면 오히려 해당 클래스를 사용하는 동안 계속해서 메모리를 차지하는 클래스 변수보다 효율적일 수도 있는 것이다.


기본 생성자

  //생성자 - 클래스명과 동일, 리턴타입 없음
    public Person() {  // 기본 생성자
        count++;
    }

클래스와 이름이 동일하고 매개변수가 없는 생성자를 말한다. 클래스 내부에 기본 생성자가 존재한다면 해당 클래스를 사용해서 인스턴스를 만들때 해당 인스턴스에 기본 생성자의 내용이 포함되게 된다.

 

예를 들어

public class Student {
    private String name;
    private static int count = 0;  // 학생 수 카운트
    
    // 기본 생성자
    public Student() {
        this.name = "무명";    // 이름 초기화
        count++;              // 학생 수 증가
        System.out.println("새 학생이 등록됨");
    }
    
    public static void main(String[] args) {
        // 인스턴스 생성시 기본 생성자의 모든 내용이 실행됨
        Student student1 = new Student();
        // 실행결과:
        // - name이 "무명"으로 초기화
        // - count가 1 증가
        // - "새 학생이 등록됨" 출력
        
        Student student2 = new Student();
        // 또 다시 기본 생성자의 모든 내용이 실행됨
    }
}

이렇게 new 생성자를 생성한다면 기본 생성자가 가지고 있던 내용이 인스턴스에 모두 포함되어 작동하게 된다. new로 인스턴스를 생성하는 즉시 실행되기 때문에 만약에 원하는 시점에 실행하고 싶다면 메서드로 만들어서 사용해야한다.

 

때문에 생성자는 객체의 초기상태를 결정하는 용도로만 사용하고 어떤 특정한 동작이 있다면 메서드로 정의해서 사용하는 것이 좋다. 만약에 count++같이 값이 변동하는 코드를 생성자 안에 넣어둔다면 생성자를 생성할 때마다 값이 변해버리기 때문에 일관된 동작을 보장할 수 없다.


 인스턴스 메서드

 //인스턴스 메서드
    public String getName() {  // getter
        return this.name;
    }

    public void setName(String name) {  // setter
        this.name = name;
    }

    protected void walk() {  // 일반 메서드
        System.out.println("Walking...");
    }

 

인스턴스 메서드는 static이 붙지 않은 메서드로, 객체(인스턴스)를 생성해야만 사용할 수 있는 메서드다.

  • 객체를 생성해야 사용 가능
  • 객체의 데이터(인스턴스 변수)를 사용할 수 있음(static이 붙지 않은 변수)
  • this 키워드로 현재 객체 참조 가능(해당 메서드를 호출한 인스턴스를 참조한다)
  • 객체마다 독립적으로 동작
public class Student {
    private String name;
    
    // 인스턴스 메서드들 (static 없음)
    public void study() {
        System.out.println(name + "이(가) 공부합니다.");
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    // 클래스 메서드 (static 있음)
    public static void printSchoolName() {
        System.out.println("OO학교");
    }
    
    public static void main(String[] args) {
        // 인스턴스 메서드 사용하려면
        Student student = new Student();  // 객체 생성 필수!
        student.study();                  // 인스턴스로 호출
        student.setName("홍길동");
        
        // 클래스 메서드는 객체 없이 호출 가능
        Student.printSchoolName();
        
        // Student.study();  // 에러! 인스턴스 메서드는 객체 없이 호출 불가
    }
}

static 메서드 (클래스 메서드)

 //static 메서드 (클래스 메서드)
    public static int getCount() {
        return count;
    }

메서드 앞에 static을 붙이면 클래스 레벨에서 동작하는데 이 경우에는 그냥 메서드의 이름만으로도 호출할 수 있게 된다.

  • 객체 생성 없이 클래스 이름으로 직접 호출 가능
  • static 변수만 직접 접근 가능
  • this 키워드 사용 불가
  • 인스턴스 변수/메서드 직접 사용 불가

내부 클래스 (Inner Class)

//내부 클래스 (Inner Class)
    private class Brain {
        private int iq;

        public Brain(int iq) {
            this.iq = iq;
        }
    }

클래스 내부에 또다른 클래스를 선언하는 경우다. 크게 4종류가 있다.

  • 인스턴스 내부 클래스 : 외부 클래스의 private 멤버 접근 가능
  • 정적(static) 내부 클래스 : 외부 클래스의 static 멤버만 접근 가능
  • 지역(Local) 내부 클래스
  • 익명(Anonymous) 내부 클래스

내부 클래스의 메서드와 변수의 범위

기본적으로 외부 클래스는 내부 클래스의 메서드와 변수에 제한 없이 접근이 가능하다. 내부 클래스 자체가 외부 클래스에 속해있기 때문이다.

단 내부 클래스에서 선언된 메서드 안의 변수에는 접근이 불가능하다. 이 경우 변수의 범위가 메서드 안으로 한정된다. 이는 메서드의 특성으로 메서드 내의 변수는 지역변수로서 범위가 메서드 안으로 한정되기 때문이다.

 

여기서 추가적으로 생각해볼 것은 그렇다면 왜 굳이 내부 클래스에 변수를 사용하냐는 것이다. 어차피 내부 클래스에서 선언한 변수를 외부에서 접근이 가능하면 반대로 내부 클래스가 외부 클래스에 선언한 변수를 가져다 써도 문제가 없지 않은가? 이유는 크게 2가지 정도로 나눌 수 있다.

  1. 내부 클래스의 변수는 접근 제어자를 통해 다른 내부 클래스의 접근을 막고 캡슐화할 수 있다. 이는 관련 데이터와 기능을 논리적으로 그룹화하고 접근을 제어하는데 도움이 된다.
  2. 추가적으로 클래스 내부에 해당 클래스에서 사용할 변수를 선언하는 것이 코드의 논리적 구조화나 관련 기능의 모듈화에 더욱 적합하고 유지보수성 향상 등에도 도움이 된다.

추상 메서드

// 추상 메서드 선언 예시
public abstract void method();  // 구현부가 없음

// 인터페이스
interface Printable {
    void print();  // 추상 메서드
}

구현부가 없는 메서드를 말한다. 선언만 해두고 구현은 다른 곳에서 해야한다.

추상 메서드를 선언할 수 있는 부분은 2곳뿐인데 바로 추상 클래스와 인터페이스다.

abstract class Animal {
    abstract void makeSound();
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("멍멍");
    }
}

// 클래스 선언부 - 접근제어자, class 키워드, 클래스명 순서
public class Person {

    //상수 (static final) - 보통 클래스 최상단에 위치
    public static final String SPECIES = "Human";
    private static final int MAX_AGE = 150;

    //클래스 변수 (static 변수) 
    private static int count = 0;

    //인스턴스 변수 (필드)
    private String name;    // private 접근제어자로 캡슐화
    private int age;
    protected String address;  // 같은 패키지와 자식 클래스에서 접근 가능
    public boolean isActive;   // 어디서나 접근 가능

    //생성자 - 클래스명과 동일, 리턴타입 없음
    public Person() {  // 기본 생성자
        count++;
    }

    public Person(String name, int age) {  // 파라미터가 있는 생성자
        this.name = name;
        this.age = age;
        count++;
    }

    //인스턴스 메서드
    public String getName() {  // getter
        return this.name;
    }

    public void setName(String name) {  // setter
        this.name = name;
    }

    protected void walk() {  // 일반 메서드
        System.out.println("Walking...");
    }

    //static 메서드 (클래스 메서드)
    public static int getCount() {
        return count;
    }

    //내부 클래스 (Inner Class)
    private class Brain {
        private int iq;

        public Brain(int iq) {
            this.iq = iq;
        }
    }

    //추상 메서드 (인터페이스 구현시)
    @Override
    public String toString() {
        return "Person{name=" + name + ", age=" + age + "}";
    }
}

 

주요 구성요소 설명:

  1. 상수 (Constants)
    • static final로 선언
    • 변경 불가능한 값
    • 보통 대문자와 언더스코어로 명명
  2. 클래스 변수 (Static Variables)
    • static 키워드로 선언
    • 모든 인스턴스가 공유
    • 클래스 이름으로 직접 접근 가능
  3. 인스턴스 변수 (Instance Variables)
    • 각 객체마다 별도로 가지는 변수
    • 접근제어자로 캡슐화 구현
    • 객체 생성 시 초기화
  4. 생성자 (Constructors)
    • 클래스명과 동일
    • 리턴타입 없음
    • 객체 초기화 담당
  5. 인스턴스 메서드 (Instance Methods)
    • 객체의 행위 정의
    • 인스턴스 변수 접근 가능
    • this 키워드로 현재 객체 참조
  6. 정적 메서드 (Static Methods)
    • static 키워드로 선언
    • 인스턴스 생성 없이 호출 가능
    • 인스턴스 변수 직접 접근 불가
  7. 내부 클래스 (Inner Classes)
    • 클래스 내부에 정의된 클래스
    • 외부 클래스의 멤버에 접근 가능
    • 캡슐화 강화
  8. 추상 메서드 구현 (Interface Implementation)
    • 인터페이스나 추상클래스 구현
    • @Override 어노테이션 사용
    • 반드시 구현해야 함

자주하는 실수:

  1. 접근제어자 미설정으로 인한 캡슐화 위반
  2. static 멤버와 인스턴스 멤버 혼용
  3. this 키워드 생략으로 인한 변수 참조 오류
  4. 생성자 체이닝 미사용으로 인한 코드 중복

'Java > 자바 학습' 카테고리의 다른 글

메서드 vs 함수  (0) 2025.05.01
클래스의 final  (0) 2025.05.01
자바의 주요 메서드  (0) 2025.04.30
자바와 자바스크립트의 차이점  (0) 2025.04.30
2. 자바의 자료형(Data Type)  (0) 2025.04.28