[Java] 클래스와 데이터

Java 기본 — 클래스와 데이터

김영한의 실전 자바 - 기본편 강의 내용을 정리한 글이다.

1. 클래스가 필요한 이유

자바에서 클래스는 서로 관련된 데이터를 하나의 단위로 묶기 위해 존재한다. 이 필요성을 체감하려면 클래스 없이 데이터를 관리했을 때 어떤 문제가 생기는지 직접 확인해보는 것이 좋다.

문제: 학생 정보를 변수로 관리하기

두 명의 학생(이름, 나이, 성적)을 출력하는 프로그램을 변수만 써서 작성하면 다음과 같다.

String student1Name = "학생1"; int student1Age = 15; int student1Grade = 90; String student2Name = "학생2"; int student2Age = 16; int student2Grade = 80; System.out.println("이름: " + student1Name + " 나이: " + student1Age + " 성적: " + student1Grade); System.out.println("이름: " + student2Name + " 나이: " + student2Age + " 성적: " + student2Grade);

학생이 2명일 때는 그럭저럭 관리할 수 있다. 그런데 학생이 10명, 100명으로 늘어나면 변수 선언과 출력 코드를 모두 손으로 추가해야 한다. 배열을 쓰면 이 문제를 어느 정도 해결할 수 있다.

배열로 개선했을 때의 한계

String[] studentNames = {"학생1", "학생2"}; int[] studentAges = {15, 16}; int[] studentGrades = {90, 80};

배열 덕분에 for문을 사용할 수 있게 됐다. 그런데 한 학생의 데이터가 세 개의 배열(studentNames[], studentAges[], studentGrades[])에 분산되어 있다. 학생2를 삭제하려면 세 배열 각각에서 인덱스 1을 찾아 제거해야 한다. 인덱스 순서가 하나라도 틀리면 데이터가 엉킨다.

배열 3개에 분산된 상태 studentNames[] "학생1", "학생2" studentAges[] 15, 16 studentGrades[] 90, 80 인덱스를 항상 맞춰야 한다 클래스로 묶은 상태 Student String name int age int grade 한 학생의 데이터를 하나의 단위로 관리

이것이 클래스가 필요한 핵심 이유다. 이름, 나이, 성적처럼 논리적으로 하나의 개념(학생)에 속하는 데이터를 따로따로 관리하는 것은 인간의 사고방식과 맞지 않는다. 클래스를 사용하면 관련 데이터를 하나의 타입으로 묶어서 응집도 높게 관리할 수 있다.


2. 클래스 도입

클래스를 사용하면 학생이라는 개념을 코드로 직접 표현할 수 있다.

Student 클래스 정의

public class Student { String name; int age; int grade; }

class 키워드로 클래스를 정의한다. 클래스 안에 선언된 변수들을 멤버 변수 또는 필드(Field)라고 부른다. 멤버 변수는 특정 클래스에 소속된 변수이기 때문에 이렇게 부르고, 필드는 데이터 항목을 가리키는 전통적인 용어(데이터베이스, 엑셀에서 각 열을 필드라 부르는 것과 같다)다. 자바에서 두 용어는 같은 뜻으로 사용된다.

클래스 이름은 관례상 대문자로 시작하고 낙타 표기법(PascalCase)을 사용한다. 예: Student, User, MemberService

클래스는 사용자 정의 타입이다

자바의 기본 타입인 int는 정수를 담고, String은 문자열을 담는다. 클래스를 만들면 Student라는 새로운 타입을 직접 정의하는 것이다. 이를 사용자 정의 타입이라고 한다.

클래스는 객체를 만들기 위한 설계도다. 설계도 자체는 실제 물건이 아니듯이, 클래스도 메모리에 실체가 없다. 클래스를 바탕으로 실제 메모리에 만들어진 실체를 객체(Object) 또는 인스턴스(Instance)라 한다.

Student 클래스 (설계도 — 메모리에 없음) String name int age int grade new Student() student1 name="학생1" age=15, grade=90 student2 name="학생2" age=16, grade=80 객체(인스턴스) 실제 메모리에 생성된 실체

3. 객체의 생성과 사용

클래스를 정의했다고 해서 학생 데이터가 메모리에 생기는 것은 아니다. 클래스를 바탕으로 객체를 생성해야 비로소 메모리에 데이터 공간이 만들어진다.

객체 생성의 세 단계

Student student1; // 1단계: Student 타입 변수 선언 student1 = new Student(); // 2단계: 객체 생성, 참조값 반환 // 3단계: 참조값을 변수에 보관 // 위 두 줄을 한 줄로: Student student1 = new Student();
  • 1단계 — 변수 선언: Student student1은 Student 타입의 참조값을 담을 수 있는 변수를 선언한다. 아직 메모리에 객체가 없으며 변수만 스택에 생성된다.
  • 2단계 — 객체 생성: new Student()는 힙(Heap) 메모리에 Student 클래스의 멤버 변수(name, age, grade)를 담을 공간을 실제로 확보한다. 생성이 완료되면 이 객체의 메모리 주소(참조값)가 반환된다.
  • 3단계 — 참조값 보관: 반환된 참조값(예: x001)을 변수 student1에 저장한다. 이후 student1을 통해 언제든지 힙의 객체에 접근할 수 있다.

변수에는 인스턴스 자체가 들어있는 것이 아니라, 인스턴스가 있는 메모리 위치를 가리키는 참조값만 들어있다. 자바에서 대입(=)은 항상 변수에 들어있는 값을 복사해서 전달하므로, 대입 시에 인스턴스가 복사되는 것이 아니라 참조값만 복사된다.

메모리 구조 시각화

스택(Stack) 지역 변수 저장 Student student1 x001 Student student2 x002 힙(Heap) 객체(인스턴스) 저장 x001 — Student name = "학생1" age = 15 / grade = 90 x002 — Student name = "학생2" age = 16 / grade = 80

스택과 힙 메모리의 역할에 대해서는 7장 "자바 메모리 구조와 static"에서 본격적으로 다룬다. 지금은 "변수는 스택에, 객체 실체는 힙에 있다"는 정도로 이해해두면 충분하다.

점(.) 연산자로 객체에 접근하기

객체의 멤버 변수에 접근하려면 점(.) 연산자를 사용한다. student1.name이라고 쓰면 자바는 student1 변수에 들어있는 참조값(x001)을 읽어 힙의 해당 위치로 이동한 뒤, 그곳의 name 필드에 접근한다.

// 값 대입 student1.name = "학생1"; // x001.name = "학생1" student1.age = 15; student1.grade = 90; // 값 읽기 System.out.println("이름: " + student1.name); // x001.name 읽어옴

값을 대입할 때도, 읽을 때도 동일한 방식이다. 점 연산자가 참조값을 경유해서 실제 힙의 객체에 접근하는 것이다.


4. 클래스, 객체, 인스턴스 정리

세 용어는 비슷한 맥락에서 자주 등장하지만 의미가 조금씩 다르다.

용어의미예시
클래스(Class) 객체를 만들기 위한 설계도. 속성(필드)과 기능(메서드)을 정의한다. Student 클래스 선언부
객체(Object) 클래스에서 정의한 속성과 기능을 가진 실체. 서로 독립적인 상태를 가진다. 메모리에 생성된 student1
인스턴스(Instance) 특정 클래스로부터 생성된 객체임을 강조할 때 쓰는 표현. "student1은 Student의 인스턴스다"

객체 vs 인스턴스는 미묘하게 다르다. 모든 인스턴스는 객체이지만, 인스턴스라고 부를 때는 특정 클래스와의 관계를 명확히 하고 싶을 때다. "student1은 객체다"라고 할 수도 있고, "student1은 Student 클래스의 인스턴스다"라고 더 구체적으로 말할 수도 있다. 실무에서는 대부분 구별 없이 혼용한다.


5. 배열과 객체의 결합

클래스를 도입했지만 아직 학생이 늘어나면 출력 코드도 같이 늘어나는 문제가 남아있다. 배열과 조합하면 이 문제까지 해결된다.

Student 타입 배열 만들기

Student[] students = new Student[2]; students[0] = student1; // x001이 복사된다 students[1] = student2; // x002가 복사된다

new Student[2]는 Student 타입 참조값을 2개 담을 수 있는 배열을 힙에 생성한다. 배열 요소는 처음에 null로 초기화된다. 이후 students[0] = student1을 실행하면, student1 변수에 들어있는 참조값(x001)이 배열 칸에 복사된다. 인스턴스 자체가 복사되는 것이 아니다.

Student[] students x005 x005 (배열) x001 [0] x002 [1] x001 — Student name="학생1" age=15, grade=90 x002 — Student name="학생2" age=16, grade=80

배열 선언 최적화와 for문 적용

배열 생성과 동시에 초기값을 채울 수 있고, for문으로 반복 출력까지 가능하다.

Student[] students = {student1, student2}; // 선언과 동시에 초기화 // 일반 for문 for (int i = 0; i < students.length; i++) { System.out.println("이름: " + students[i].name + " 나이: " + students[i].age + " 성적: " + students[i].grade); } // 향상된 for문(Enhanced For Loop) — 더 간결 for (Student s : students) { System.out.println("이름: " + s.name + " 나이: " + s.age + " 성적: " + s.grade); }

향상된 for문은 배열의 각 요소를 순서대로 꺼내어 변수 s에 담아준다. 인덱스 변수가 필요 없고 범위 초과 오류도 발생하지 않는다. 이제 학생이 아무리 많아져도 배열에 추가하기만 하면 출력 코드는 그대로 유지된다.

실무에서는 이보다 더 유연한 ArrayList나 제네릭 컬렉션을 주로 사용한다. 그러나 그 내부 원리는 여기서 다룬 객체 참조와 배열 개념을 기반으로 한다.


6. 정리

개념핵심 내용
클래스 관련 데이터(필드)와 기능(메서드)을 묶는 사용자 정의 타입. 설계도 역할
객체 생성 new 클래스명()으로 힙에 인스턴스를 생성. 반환되는 참조값을 변수에 저장
멤버 접근 점(.) 연산자로 참조값을 경유해 힙의 객체에 접근
배열 + 객체 배열은 객체 자체가 아닌 참조값을 저장. for문과 결합하면 확장에 유연한 코드 작성 가능

클래스는 자바 프로그래밍의 근간이다. 지금 단계에서는 "관련 데이터를 하나의 타입으로 묶는 도구"라는 관점에서 이해하는 것으로 충분하다. 이후 생성자, 접근 제어자, 상속, 다형성을 배우면서 클래스가 단순한 데이터 묶음 이상의 강력한 추상화 도구임을 체감하게 된다.