자바 세상은 “클래스” 와 “객체” 로 이루어져 있다. 그럼 클래스와 객체가 왜 필요한지, 클래스가 어떤 방식으로 발전하면서 만들어졌는지 차근차근 알아보자.
먼저 학생 2명의 이름, 나이, 점수를 출력하는 코드를 작성해보자.
package class1;
public class ClassStart1 {
public static void main(String[] args) {
String student1 = "학생1";
int student1_age = 15;
int student1_score = 80;
String student2 = "학생2";
int student2_age = 20;
int student2_score = 100;
System.out.println("이름: " + student1 + " 나이: " + student1_age + " 점수: " + student1_score);
System.out.println("이름: " + student2 + " 나이: " + student2_age + " 점수: " + student2_score);
}
}
학생 2명을 컨트롤해야 하기 때문에 각각 다른 변수를 사용했다. 근데 이런 방식으로는 학생이 더 늘어날 때마다 변수를 추가로 선언해줘야 하고, 출력하는 코드도 추가해야 한다.
그럼 이 문제를 어떻게 더 간단하게 해결할 수 있을까? 반복문을 돌려서 할 수 있을 것 같지만, 변수의 이름이 전부 다르기 때문에 어쩔 수가 없다. 변수 이름을 통일 시켜야 하는데, 이때 같은 데이터 타입을 하나로 묶을 수 있는 “배열” 이 떠올랐다.
package class1;
public class ClassStart1 {
public static void main(String[] args) {
String[] students = {"학생1", "학생2"};
int[] ages = {15, 20};
int[] scores = {80, 100};
for (int i = 0; i < students.length; i++) {
System.out.println("이름: " + students[i] + " 나이: " + ages[i] + " 점수: " + scores[i]);
}
}
}
실행해보면, 기존 코드와 동일한 결과를 얻을 수 있다. 여기서 학생이 더 늘어나면 변수를 더 선언하고, 루프를 더 돌릴 필요도 없이 배열에 요소를 추가해주면 그만이다.
근데… 한 학생의 데이터가 지금 students[], ages[], scores[] 총 3개의 배열에 나누어져 있다. 그렇기 때문에 데이터를 변경할 때 매우 조심해서 작업해야 한다. 만약 특정 학생의 데이터를 삭제하고 싶다면, 정확하게 그 학생의 인덱스에 해당하는 데이터를 각각 제거해야 한다. 간단히 말해, 한 학생의 데이터를 관리하기 위해서는 3개 배열의 인덱스 순서를 항상 정확하게 맞춰야 한다는 것이다.
“학생” 이라는 개념을 하나로 묶는 방법은 없을까? 학생들 본인의 이름, 나이, 성적을 각각 관리하면 너무 편리할 것 같다.
클래스를 사용해서 학생이라는 개념을 만들고, 각각의 학생 별로 본인의 이름, 나이, 성적을 관리하도록 해보자.
<Student 클래스>
package class1;
public class Student {
String name;
int age;
int grade;
}
위와 같이, class 키워드를 사용해서 학생 클래스(Student)를 정의하는 것이다. 학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가진다. 이런 식으로 클래스에 정의한 변수들을 “멤버 변수”, 혹은 “필드” 라고 부른다.
💭 자바에서 멤버 변수, 필드는 같은 뜻이다. 모두 클래스에 소속된 변수를 뜻한다. 그리고 클래스는 관례상 대문자로 시작하고 CamelCase(ex. Student, MemberService, MemberRepository 등)를 사용한다.
그럼 이제 저 Student클래스를 사용해보자.
package class1;
public class ClassStart2 {
public static void main(String[] args) {
Student student1 = new Student(); // student1 객체 생성
// student1 = x001(참조값);
student1.name = "학생1";
student1.age = 15;
student1.grade = 80;
Student student2 = new Student(); // student2 객체 생성
// student2 = x002(참조값);
student2.name = "학생2";
student2.age = 20;
student2.grade = 100;
// 실제 참조값 출력
System.out.println(student1);
System.out.println(student2);
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 점수:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 점수:" + student2.grade);
}
}
타입은 데이터의 종류나 형태를 나타낸다. 아까 클래스를 만든 행위는, 학생(Student)이라는 타입을 직접 만든 것이다. 클래스는 “설계도” 다. 이 설계도를 사용해서 실제 메모리에 만들어진 실체를 “객체” 혹은 “인스턴스” 라고 하는 것이다.
쉽게 말하자면, 붕어빵 틀만 덩그러니 갖다 놓으면 붕어빵을 먹을 수 없다. 그 틀에 반죽이랑 팥이나 슈크림을 넣고 구워야 붕어빵이 되는 것이다. 이 만들어진 붕어빵이 바로 객체(인스턴스)라는 소리다.
위의 코드를 하나씩 분석해보자.

일단 Student 타입을 받을 수 있는 변수(student1)를 선언했기 때문에 변수의 공간이 할당되고, new Student() 를 통해 인스턴스를 만들었다. 실제 메모리에 Student 클래스 정보를 기반으로 새로운 인스턴스를 생성하라는 뜻이다. 만들어진 Student 클래스는 name, age, grade 멤버 변수를 가지고 있고, 이 변수들을 사용하는데 필요한 메모리 공간도 함께 확보한 것이다.

객체를 생성하기만 하면 뭐하나, 어디에 만들었는지 알아야 접근해서 사용할 수 있을 것이다. 그래서 new 키워드를 통해 객체가 생성되고 메모리 어딘가에 있는 해당 객체에 접근할 수 있는 참조값이 반환하는데, 앞서 선언한 변수 student1에 생성된 인스턴스의 참조값(x001)을 보관하는 것이다. 따라서 이제 student1 변수를 통해서 메모리에 있는 실제 객체에 접근(참조)할 수 있는 것이다.
<실제 출력된 참조값 결과>
객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없다. 해당 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 만드는 역할까지만 하는 거다. 따라서 생성한 객체에 접근할 수 있는 방법이 필요하기 때문에, 객체를 생성할 때 반환되는 참조값을 어딘가에 보관할 필요가 있다. 앞서 student1 변수에 참조값(x001)을 저장해뒀으므로 이를 통해 실제 메모리에 존재하는 객체에 접근할 수 있는 것이다.
객체를 사용하려면 일단 메모리에 존재하는 객체에 접근해야 한다. 객체에 접근하려면 온점(.)을 사용하면 된다.
객체가 가지고 있는 멤버 변수(name, age, grade)에 값을 대입하기 위해서 일단 온점(.)을 사용해 student1에 들어있는 참조값(x001)을 읽어서 메모리에 존재하는 객체에 접근한다.
student1.name = "학생1"에서 온점을 통해 student1의 참조값을 읽었다. 즉, x001.name = "학생1"과 똑같은 것이다. 메모리 주소 x001에 있는 객체의 name 필드에 가서 학생1을 집어 넣는 것이다. 나중에 객체의 값을 읽는 것도 참조값을 사용해서 객체에 접근하면 된다.
그림으로 살펴보면 아래와 같다.

객체를 사용해서 학생 데이터를 구조적으로 더욱 이해하기 쉽게 변경할 수 있었지만, 출력하는 부분을 보면, 여전히 새로운 학생이 추가되면 해당 부분도 같이 추가해줘야 한다는 문제가 있다.
이런 문제는 배열을 사용한다면, 특정 타입을 연속한 데이터 구조로 묶어 편리하게 관리할 수 있다. Student 클래스를 사용한 변수들도 Student 타입이기 때문에 역시 배열을 사용해서 하나의 데이터 구조로 묶을 수 있다.
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1 = new Student();
Student student2 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 80;
student2.name = "학생2";
student2.age = 20;
student2.grade = 100;
Student[] students = new Student[2];
students[0] = student1;
students[1] = student2;
// Student[] students = new Student[]{student1, student2};
// 인텔리제이 단축키 : iter
for (Student s : students) {
System.out.println("이름:" + s.name + " 나이:" + s.age + " 점수:" + s.grade);
}
}
}
위 코드처럼 Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관하는 것이다.

“학생들” 을 보관할 수 있는 사이즈가 2인 배열 students인 것이다. Student 타입의 변수는 Student 인스턴스의 참조값을 담을 수 있었다. 이와 같이, Student 배열의 각각의 항목도 Student 타입의 변수일 뿐이고, 그 변수들도 참조값을 보관하는 것이 역할일 것이다. 하지만, Student[] students = new Student[2] 이 부분만으로는 아직 배열에 참조값을 대입하지는 않았기 때문에 각각 null 값으로 초기화된다.

“자바에서 대입은 항상 변수에 들어있는 값을 복사하는 것이다!”
student1에는 참조값 x001이, student2에는 참조값 x002가 들어있었다. 그 값을 복사해서 students 배열의 각각의 인덱스에 전달하는 것이다. 이제 배열은 x001, x002의 참조값을 가지고 있기 때문에 학생1과 학생2 인스턴스에 모두 접근할 수 있다.

student1, student2의 참조값을 복사해서 students[0], students[1]에 전달해도 기존 student1, student2에 들어 있던 참조값은 당연히 그대로 유지된다. 원한다면, student1을 통해서 직접 접근할 수도 있고, 배열 변수를 통해서도 접근할 수 있다는 말이다. 다만, 배열에 들어있는 객체를 사용하려면 먼저 배열에 접근하고, 그 다음에 객체에 접근하는 차이만 있을 뿐이다.
❗주의점
변수에는 인스턴스 자체가 들어있는 것이 아니다. 인스턴스의 위치를 가리키는 참조값이 들어있을 뿐이다. 따라서 대입 시에 인스턴스가 복사되는 것이 아니라 참조값만 복사되는 것이다.
MovieReview 클래스를 만들자. 그 안에는 영화 제목, 리뷰 내용이 포함되어 있다. 영화 리뷰 정보를 선언하고 출력해보도록 하자.
package class1;
public class MovieReview {
String title;
String review;
}
// 내가 푼 풀이
package class1;
public class MovieReviewMain {
public static void main(String[] args) {
MovieReview movieReview1 = new MovieReview();
MovieReview movieReview2 = new MovieReview();
MovieReview movieReview3 = new MovieReview();
movieReview1.title = "해리포터";
movieReview1.review = "희대의 대작";
movieReview2.title = "존윅";
movieReview2.review = "화려한 액션";
movieReview3.title = "귀멸의 칼날";
movieReview3.review = "인생 애니";
MovieReview[] movieReviews = new MovieReview[3];
movieReviews[0] = movieReview1;
movieReviews[1] = movieReview2;
movieReviews[2] = movieReview3;
for (MovieReview movie : movieReviews) {
System.out.println("영화 제목: " + movie.title + ", 리뷰: " + movie.review);
}
}
}
/*
영화 제목: 해리포터, 리뷰: 희대의 대작
영화 제목: 존윅, 리뷰: 화려한 액션
영화 제목: 귀멸의 칼날, 리뷰: 인생 애니
*/
상품 주문 정보를 담을 수 있는 ProductOrder 클래스를 만들자. 클래스 안에는 상품명, 가격, 주문 사량이 포함되어 있어야 한다. 여러 상품의 주문 정보를 배열로 관리하고, 그 정보들을 출력하고, 최종 결제 금액을 계산하여 출력하자.
package class1;
public class ProductOrder {
String productName;
int price;
int quantity;
}
// 내가 푼 풀이
package class1;
public class ProductOrderMain {
public static void main(String[] args) {
ProductOrder order1 = new ProductOrder();
ProductOrder order2 = new ProductOrder();
ProductOrder order3 = new ProductOrder();
order1.productName = "마라탕";
order1.price = 20000;
order1.quantity = 1;
order2.productName = "떡볶이";
order2.price = 10000;
order2.quantity = 2;
order3.productName = "요거트 아이스크림";
order3.price = 15000;
order3.quantity = 3;
ProductOrder[] orders = new ProductOrder[] {order1, order2, order3};
int totalPrice = 0;
for (ProductOrder o : orders) {
System.out.println("상품명: " + o.productName + ", 가격: " + o.price + ", 수량: " + o.quantity);
totalPrice += o.price * o.quantity;
}
System.out.println("총 결제 금액: " + totalPrice);
}
}
/*
상품명: 마라탕, 가격: 20000, 수량: 1
상품명: 떡볶이, 가격: 10000, 수량: 2
상품명: 요거트 아이스크림, 가격: 15000, 수량: 3
총 결제 금액: 85000
*/