본문 바로가기
자바/김영한의 실전 자바 - 기본편

김영한의 실전 자바 - 기본편(feat. 전체 정리)

by limdae94 2024. 12. 23.

1. 클래스와 데이터

1. 클래스 도입

사람이 관리하기 좋은 방식으로 어떠한 개념을 하나로 묶는 것이다.

  • 예) Student 클래스
public class Student {
	String name;
   	int age;
 	int grade;
}

클래스에 정의된 변수들(name, age, grede)을 변수, 또는 필드라 한다.

  • 멤버 변수(Member Variable) : 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부른다.
  • 필드(Field) : 데이터 항목을 가리키는 전통적인 용어이다.
  • 자바에서 멤버 변수, 필드는 같은 뜻이다. 클래스에 소속된 변수를 뜻한다.

클래스란 설계도이다.

클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있다.(Student)

사용자가 직접 정의하는 사용자 정의 타입을 만들려면 설계도가 필요하다.

이 설계도가 바로 클래스이다.

설계도인 클래스를 사용해서 실제 메모리에 만들어진 실체를 객체 또는 인스턴스라 한다.

 

2. 클래스 생성

public class ClassStart3 {
	public static void main(String[] args) {
    	Student student1;
      	student1 = new Student();
     	student1.name = "학생1";
     	student1.age = 15;
     	student1.grade = 90;
     
     	Student student2 = new Student();
     	student2.name = "학생2";
     	student2.age = 16;
     	student2.grade = 80;
     
     	System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
     	System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
    }
}

 

 

변수 선언

  • Student student1
    • Student 타입을 받을 수 있는 변수를 선언한다.
    • int는 정수를, String은 문자를 담을 수 있듯이 Student는 Student 타입의 객체(인스턴스)를 받을 수 있다.

객체 생성

  • student = new Student()
    • 객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 한다.
    • new Student() : new는 새로 생성한다는 뜻이다. new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻이다. 이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성한다.
    • 객체를 생성할 때는 new 클래스명()을 사용하면 된다. 마지막에 ()도 추가해야 한다.
    • Sudent 클래스는 String name, int age, int grade 멤버 변수를 가지고 있다. 이 변수를 사용하는데 필요한 메모리 공간도 함께 확보한다.

참조값 보관

  • 객체를 생성하면 자바는 메모리에 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)(x001)을 반환한다.
  • new 키워드를 통해 객체를 생성되고 나면 참조값을 반환한다. 앞서 선언한 변수인 Student student1에 생성된 객체의 참조값(x001)을 보관한다.
  • Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있다.
    • student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있다. 따라서 이 변수를 통해 객체를 접근(참조)할 수 있다. 쉽게 이야기해서 student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있다.

참조값을 변수에 보관해야 하는 이유

객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없다. 이 코드는 단순히 Student 클래스(설계도)를 기반으로 메모리에 실제 객체를 만드는 것이다. 따라서 생성한 객체에 접근할 수 있는 방법이 필요하다. 이런 이유로 객체를 생성할 때 반환되는 참조값을 어딘가에 보관해두어야 한다. 앞서 Student student1 변수에 참조값(x001)을 저장해두었으므로 저장한 참조값을 통해서 실제 메모리에 존재하는 객체에 접근할 수 있다.

 

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

클래스 - Class

클래스는 객체를 생성하기 위한 틀 또는 설계도이다.
클래스는 객체가 가져야 할 속성(변수)과 기능(메서드)를 정의한다.
예를 들어 학생이라는 클래스는 속성으로 name, age, grade를 가진다.

 

객체 - Object

객체는 클래스에서 정의한 속성과 기능을 가진 실체이다.
객체는 서로 독립적인 상태를 가진다.

예를 들어 위 코드에서 student1은 학생1의 속성을 가지는 객체이고, student2는 학생2의 속성을 가지는 객체다.
student1과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체이다.

 

인스턴스 - Instance

인스턴스는 특정 클래스로부터 생성된 객체를 의미한다. 그래서 객체와 인스턴스라는 용어는 자주 혼용된다.
인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용한다.
예를 들어서 student1 객체는 Student 클래스의 인스턴스다. 라고 표현한다.

 

객체 vs 인스턴스

둘다 클래스에서 나온 실체라는 의미에서 비슷하게 사용되지만, 용어상 인스턴스는 객체보다 좀 더 관계에 초점을 맞춘 단어이다. 보통 student은 Student의 객체이다. 라고 말하는 대신 student은 Student의 인스턴스이다. 라고 특정 클래스와의 관계를 명확히 할 때 인스턴스라는 용어를 주로 사용한다.

4. 자바에서 대입은 항상 변수에 들어 있는 값을 복사해서 전달한다.

students[0] = student1;
students[1] = student2;
//자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.
students[0] = x001;
students[1] = x002;

자바에서 변수의 대입(=)은 모두 변수에 들어있는 값을 복사해서 전달하는 것이다. 이 경우 오른쪽 변수인 student1, student2에는 참조값이 들어있다. 그래서 이 값을 복사해서 왼쪽에 있는 배열에 전달한다. 따라서 기존 student1, student2에 들어있던 참조값은 당연히 그대로 유지된다.

주의!

변수에는 인스턴스 자체가 들어있는 것이 아니다! 인스턴스의 위치를 가리키는 참조값이 들어있을 뿐이다!
따라서 대입(=)시에 인스턴스가 복사되는 것이 아니라 참조값만 복사된다.


2. 기본형과 참조형

1. 기본형 vs 참조형

자바에서 참조형을 제대로 이해하는 것은 정말 중요하다.

사용하는 값을 변수에 직접 넣을 수 있는 기본형.
객체가 지정된 메모리의 위치를 가리키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있다.

  • 기본형(Primitive Type) : int, long, double, boolean처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 기본형이라 한다.
  • 참조형(Reference Type) : Student student, int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 참조형이라 한다. 참조형은 객체 또는 배열에 사용된다.

쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)가 들어가 있다.
참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 해당 위치로 이동해야 한다.

 

쉽게 이해하는 팁

기본형을 제외한 나머지는 모두 참조형이다.

  • 기본형은 소문자로 시작한다. int, long, double, boolean 모두 소문자로 시작한다.
    • 기본형은 자바가 기본으로 제공하는 데이터 타입이다. 이러한 기본형은 개발자가 새로 정의할 수 없다.
      개발자는 참조형인 클래스만 직접 정의할 수 있다.
  • 클래스는 대문자로 시작한다. Student
    • 클래스는 모두 참조형이다.

참고 - String

자바에서 String은 특별하다. String은 사실은 클래스다. 따라서 참조형이다. 그런데 기본형처럼 문자 값을 바로 대입할 수 있다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공한다.

 

대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다.

자바에서 변수에 값을 대입하는 것은 변수에 들어있는 값을 복사해서 대입하는 것이다.

기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다. 기본형이면 변수에 들어있는 실제 사용하는 값을 복사해서 대입하고, 참조형이면 변수에 들어있는 참조값을 복사해서 대입한다.

 

참조형의 경우 실제 사용하는 객체가 아니라 객체의 위치를 가리키는 참조값만 복사된다. 쉽게 이야기해서 실제 건물이 복사가 되는 것이 아니라 건물의 위치인 주소만 복사되는 것이다. 따라서 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐이다.

 

2. 참조형과 메서드 호출

public class Method2 {
 		public static void main(String[] args) {
 		Student student1 = createStudent("학생1", 15, 90);
 		Student student2 = createStudent("학생2", 16, 80);
 		printStudent(student1);
 		printStudent(student2);
 	}
    
 	static Student createStudent(String name, int age, int grade) {
 		Student student = new Student();
 		student.name = name;
 		student.age = age;
 		student.grade = grade;
 		return student;
 	}
    
 	static void printStudent(Student student1) {
 		System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
 	}
}

메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했다.
이 참조값만 있으면 해당 인스턴스에 접근할 수 있다. 여기서는 student1에 참조값을 보관하고 사용한다.

참조형은 메서드를 호출할 때 참조값을 전달한다. 따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있다.

3. null

참조형 변수에는 항상 객체가 있는 위치를 가리키는 참조값이 들어간다. 그런데 아직 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶다면 어떻게 해야할까?

참조형 변수에는 아직 가리키는 대상이 없다면 null이라는 특별한 값을 넣어둘 수 있다. null은 값이 존재하지 않는, 없다는 뜻이다.

멤버 변수에서의 참조형의 값 초기화는 참조형 = null로 자동 초기화해준다.

data에 null을 할당했다. 따라서 앞서 생성한 x001 Data 인스턴스를 더는 아무도 참조하지 않는다. 이렇게 아무도 참조하지 않게 되면 x001이라는 참조값을 다시 구할 방법이 없다. 따라서 해당 인스턴스에 다시 접근할 방법이 없다.

이렇게 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐이다.

C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했다. 만약 실수로 인스턴스 삭제를 누락하면 메모리에 사용하지 않는 객체가 가득해져서 메모리 부족 오류가 발생하게 된다.

자바는 이런 과정을 자동으로 처리해준다. 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다.

객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료할 때 까지 계속 생존한다. 그런데 중간에 해당 객체를 참조하는 곳이 모두 사라지면 그때 JVM은 필요 없는 객체로 판단하고 GC를 사용해서 제거한다.

4. NullPointerException

만약 참조값이 없이 객체를 찾아가면 어떤 문제가 발생할까?

이 경우 NullPointerException이라는 예외가 발생하는데, 개발자를 가장 많이 괴롭히는 예외이다.

NullPointerException은 이름 그도 null을 가리키나(Pointer)인데, 이때 발생하는 예외(Exception)다.

null은 없다는 뜻이므로 결국 주소가 없는 곳을 찾아갈 때 발생하는 예외이다.

객체를 참조할 때는.(dot)을 사용한다. 이렇게 하면 참조값을 사용해서 해당 객체를 찾아갈 수 있다. 그런데 참조값이 null 이라면 값이 없다는 뜻이므로, 찾아갈 수 있는 객체(인스턴스)가 없다. NullPointerException은 이처럼 null에 .(dot)을 찍었을 때 발생한다.

NullPointerException이 발생하면 null 값에 . (dot)을 찍었따고 생각하면 문제를 쉽게 찾을 수 있다.

5. 기본형과 참조형 정리

대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다.

자바에서 변수에 값을 대입하는 것은 변수에 들어있는 값을 복사해서 대입하는 것이다.

기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다. 기본형이면 변수에 들어있는 실제 사용하는 값을 복사해서 대입하고, 참조형이면 변수에 들어있는 참조값을 복사해서 대입한다.

기본형이든 참조형이든 변수의 값을 대입하는 방식은 같다. 하지만 기본형과 참조형에 따라 동작하는 방식이 달라진다.

 

기본형 vs 참조형 - 기본

  • 자바의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 나눌 수 있다.
  • 기본형을 제외한 나머지 변수는 모두 참조형이다. 클래스와 배열을 다루는 변수는 참조형이다.
  • 기본형 변수는 값을 직접 저장하지만, 참조형 변수는 참조(주소)를 저장한다.
  • 기본형 변수는 산술 연산을 수행할 수 있지만, 참조형 변수는 산술 연산을 수행할 수 없다.
  • 기본형 변수는 null을 할당할 수 없지만, 참조형 변수는 null을 할당할 수 있다.

기본형 vs 참조형 - 대입

  • 기본형과 참조형 모두 대입시 변수 안에 있는 값을 읽고 복사해서 전달한다.
  • 기본형은 사용하는 값을 복사해서 전달하고, 참조형은 참조값을 복사해서 전달한다! 이것이 중요하다. 실제 인스터스가 복사되는 것이 아니다. 인스턴스를 가리키는 참조값을 복사해서 전달하는 것이다! 따라서 하나의 인스턴스를 여러곳에서 참조할 수 있다.
  • 헷갈리면 그냥 변수 안에 들어간 값을 떠올려보자. 기본형은 사용하는 값이, 참조형은 참조값이 들어있다!
    변수에 어떤 값이 들어있든간에 그 값을 그대로 복사해서 전달한다.

기본형 vs 참조형 - 메서드 호출

  • 메서드 호출시 기본형은 메서드 내부에서 매개변수(파라미터)의 값을 변경해도 호출자의 변수 값에는 영향이 없다.
  • 메서드 호출시 참조형은 메서드 내부에서 매개변수(파라미터)로 전달된 객체의 멤버 변수를 번경하면, 호출자의 객체도 변경된다.

 

3. 객체 지향 프로그래밍

1. 객체 지향 프로그래밍 vs 절차 지향 프로그래밍

객체 지향 프로그래밍과 절차 지향 프로그래밍은 서로 대치되는 개념이 아니다. 객체 지향이라도 프로그램의 작동 순서는 중요하다. 다만 어디에 더 초점을 맞추는가에 둘의 차이가 있다. 객체 지향의 경우 객체의 설계와 관계를 중시한다. 반면 절차 지향의 경우 데이터와 기능이 분리되어 있고, 프로그램이 어떻게 작동하는지 그 순서에 초점을 맞춘다.

절차 지향 프로그래밍

  • 절차 지향 프로그래밍은 이름 그대로 절차를 지향한다. 쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식이다.
  • 절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식이다. 즉, 어떻게 를 중심으로 프로그래밍 한다.

객체 지향 프로그래밍

  • 객체 지향 프로그래밍은 이름 그대로 객체를 지향한다. 쉽게 이야기해서 객체를 중요하게 생각하는 방식이다.
  • 객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식이다. 즉 무엇을 중심으로 프로그래밍 한다.

둘의 중요한 차이

  • 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있다. 반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 객체 안에 함께 포함되어 있다.

2. 객체란?

세상의 모든 사물을 단순하게 추상화해보면 속성(데이터)과 기능 딱 2가지로 설명할 수 있다.


자동차

  • 속성 : 차량 색상, 현재 속도
  • 기능 : 액셀, 브레이크, 문 열기, 문 닫기

동물

  • 속성 : 색상, 키, 온도
  • 기능 : 먹는다, 걷는다

게임 케릭터

  • 속성 : 레벨, 경험치, 소유한 아이템들
  • 기능 : 이동, 공격, 아이템 획득

객체 지향 프로그래밍은 모든 사물을 속성과 기능을 가진 객체로 생각하는 것이다. 객체에는 속성과 기능만 존재한다.

이렇게 단순화하면 세상에 있는 객체들을 컴퓨터 프로그램으로 쉽게 설계할 수 있다.

이런 장점 덕분에 지금은 객체 지향 프로그래밍이 가장 많이 사용된다. 참고로 실세계와 객체가 항상 1:1로 매칭되는 것은 아니다.

모듈화
객체의 각각의 기능을 메서드로 만들면 각각의 기능을 모듈화하였다고 한다.
쉽게 이야기해서 레고 블럭을 생각하면 된다. 필요한 블럭을 가져다 꼽아서 사용할 수 있다.
어떤 객체의 기능이 필요하면 해당 기능을 메서드 호출만으로 손쉽게 사용할 수 있다.

3. 객체 지향 프로그래밍

객체 지향 프로그래밍은 어떠한 개념을 객체로 온전히 만드는 것에 집중해야 한다. 그러기 위해서는 프로그램의 실행 순서보다는 객체로 표현하려는 클래스를 만드는 것 자체에 집중해야 한다. 객체로 표현하려는 개념이 어떤 속성(데이터)을 가지고 어떤 기능(메서드)을 제공하는지 이 부분에 초점을 맞추어야 한다. 쉽게 이야기 해서 어떠한 개념을 객체로 만들어서 제공하는 개발자와 그 객체를 사용하는 사용자가 분리되어 있다고 생각하면 된다.

public class MusicPlayer {
    int volume = 0;
    boolean isOn = false;
    void on() {
        isOn = true;
        System.out.println("음악 플레이어를 시작합니다");
    }
    void off() {
        isOn = false;
        System.out.println("음악 플레이어를 종료합니다");
    }
    void volumeUp() {
        volume++;
        System.out.println("음악 플레이어 볼륨:" + volume);
    }
    void volumeDown() {
        volume--;
        System.out.println("음악 플레이어 볼륨:" + volume);
    }
    void showStatus() {
        System.out.println("음악 플레이어 상태 확인");
        if (isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + volume);
        } else {
 			System.out.println("음악 플레이어 OFF");
        }
    }
 }

MusicPlayer 클래스에 음악 플레이어에 필요한 속성과 기능을 모두 정의했다. 이제 음악 플레이어가 필요한 곳에서 이 클래스만 있으면 온전한 음악 플레이어를 생성해서 사용할 수 있다. 음악 플레이어를 사용하는데 필요한 모든 속성과 기능이 하나의 클래스에 포함되어 있다.

 public class MusicPlayerMain4 {
     public static void main(String[] args) {
     	MusicPlayer player = new MusicPlayer();
        //음악 플레이어 켜기
        player.on();
        //볼륨 증가
        player.volumeUp();
        //볼륨 증가
        player.volumeUp();
        //볼륨 감소
        player.volumeDown();
        //음악 플레이어 상태
        player.showStatus();
        //음악 플레이어 끄기
        player.off();
    }
 }

MusicPlayer 를 사용하는 코드를 보자. MusicPlayer 객체를 생성하고 필요한 기능(메서드)을 호출하기만 하면 된다. 필요한 모든 것은 MusicPlayer 안에 들어있다.

  • MusicPlayer 를 사용하는 입장에서는 MusicPlayer 의 데이터인 volume, iOn 같은 데이터는 전혀 사용하지 않는다.
  • MusicPlayer 를 사용하는 입장에서는 이제 MusicPlayer 내부에 어떤 속성(데이터)이 있는지 전혀 몰라도 된다. MusicPlayer 를 사용하는 입장에서는 단순하게 MusicPlayer 가 제공하는 기능 중에 필요한 기능을 호출해서 사용하기만 하면 된다.

캡슐화

MusicPlayer 를 보면 음악 플레이어를 구성하기 위한 속성과 기능이 마치 하나의 캡슐에 쌓여있는 것 같다. 이렇게 속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것을 캡술화라 한다.

객체 지향 프로그래밍 덕분에 음악 플레이어 객체를 사용하는 입장에서 진짜 음악 플레이어를 만들고 사용하는 것 처럼 친숙하게 느껴진다. 그래서 코드가 더 읽기 쉬운 것은 물론이고, 속성과 기능이 한 곳에 있기 때문에 변경도 더 쉬워진다. 예를 들어서 MusicPlayer 내부 코드가 변하는 경우에 다른 코드는 변경하지 않아도 된다. MusicPlayer 의 volume 이라는 필드 이름이 다른 이름으로 변한다고 할 때 MusicPlayer 내부만 변경하면 된다. 또 음악 플레이어가 내부에서 출력하는 메시지를 변경할 때도 MusicPlayer 내부만 변경하면 된다. 이 경우 MusicPlayer 를 사용하는 사용자 입장에서는 코드를 전혀 변경하지 않아도 된다. 물론 외부에서 호출하는 MusicPlayer 의 메서드 이름을 변경한다면 MusicPlayer 를 사용하는 곳의 코드도 변경해야 한다.

4. 생성자

생성자는 객체 생성 직후 객체를 초기화 하기 위한 특별한 메서드로 생각할 수 있다.

1. 생성자 - 필요한 이유

객체를 생성하는 시점에 어떤 작업을 하고 싶다면 생성자(Constructor)를 이용하면 된다. 어떠한 객체를 생성하고 나면 초기값을 설정해야 하는 경우가 있다. 객체 지향 프로그래밍에서는 속성과 기능을 한 곳에 두는 것이 더 나은 방법이다. 그러므로 객체 안에 객체의 초기값을 설정하는 생성자를 제공하는 것이 좋다.

2. this

this

this는 인스턴스 자신의 참조값을 가리킨다. 멤버 변수에 접근하려면 앞에 this. 붙여주면 된다.

this의 생략

this는 생략할 수 있다. 이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수다)를 먼저 찾고 없으면 그 다음으로 멤버 변수를 찾는다. 멤버 변수도 없으면 오류가 발생한다.

3. 생성자 - 도입

생성자와 메서드의 차이점

  • 생성자의 이름은 클래스 이름과 같아야 한다. 따라서 첫 글자도 대문자로 시작한다.
  • 생성자는 반환 타입이 없다. 비워두어야 한다.
  • 나머지는 메서드와 같다

생성자 호출

생성자는 인스턴스를 생성하고 나서 즉시 호출된다. 생성자를 호출하는 방법은 다음 코드와 같이 new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 된다.

new 생성자이름(생성자에 맞는 인수 목록)
new 클래스이름(생성자에 맞는 인수 목록)

참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호 () 도 포함해야 하는 이유가 바로 생성자 때문이다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함한다.

생성자 장점

중복 호출 제거

생성자가 없던 시절에는 생성 직후에 어떤 작업을 수행하기 위해 메서드를 직접 한번 더 호출해야 했다. 생성자 덕분에 객체를 생성하면서 동시에 생성 직후에 필요한 작업을 한번에 처리할 수 있게 되었다.

제약 - 생성자 호출 필수

객체를 생성할 때 직접 정의한 생성자가 있다면 직접 정의한 생성자를 반드시 호출해야 한다는 점이다. 참고로 생성자를 메서드 오버로딩처럼 여러개 정의할 수 있는데, 이 경우에는 하나만 호출하면 된다.

생성자를 사용하면 필수값 입력을 보장할 수 있다.

좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램이다.

4. 기본 생성자

기본 생성자

  • 매개변수가 없는 생성자를 기본 생성자라 한다.
  • 클래스에 생성자가 하나도 없으면 자바 컴파일러는 매개변수가 없고, 작동하는 코드가 없는 기본 생성자를 자동으로 만들어준다.
  • 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않는다.

기본 생성자를 왜 자동으로 만들어줄까?

만약 자바에서 기본 생성자를 만들어주지 않는다면 생성자 기능이 필요하지 않는 경우에도 모든 클래스에 개발자가 직접 기본 생성자를 정의해야 한다. 생성자 기능을 사용하지 않는 경우도 많기 때문에 이런 편의 기능을 제공한다.

  • 생성자는 반드시 호출되어야 한다.
  • 생성자가 없으면 기본 생성자가 제공된다.
  • 생성자가 하나라도 있으면 자바는 기본 생성자가 제공되지 않는다. 이 경우 개발자가 정의한 생성자를 직접 호출해야 한다.

5. 생성자 - 오버로딩과 this()

생성자도 메서드 오버로딩처럼 매개변수만 다르게 해서 여러 생성자를 제공할 수 있다.

public MemberConstruct(String name, int age) {
	this.name = name;
 	this.age = age;
 	this.grade = 50;
}
public MemberConstruct(String name, int age, int grade) {
   	this.name = name;
   	this.age = age;
   	this.grade = grade;
}

this()

위의 두 생성자를 비교해 보면 코드가 중복 되는 부분이 있다.

이때 this() 라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있다. 참고로 this 는 인스턴스 자신의 참조값을 가리킨다. 그래서 자신의 생성자를 호출한다고 생각하면 된다.

MemberConstruct(String name, int age) {
 	this(name, age, 50); //변경
 }
 MemberConstruct(String name, int age, int grade) {
 	System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" +
grade);
 	this.name = name;
 	this.age = age;
 	this.grade = grade;
 }

이 코드는 첫번째 생성자 내부에서 두번쨰 생성자를 호출한다.

this() 규칙

this() 는 생성자 코드의 첫줄에만 작성할 수 있다.

5. 패키지

패키지(package)는 이름 그대로 물건을 운송하기 위한 포장 용기나 그 포장 묶음을 뜻한다. 해당 패키지 안에 관련된 자바 클래스들을 넣으면 된다.

1. 패키지와 계층 구조

패키지는 보통 다음과 같이 계층 구조를 이룬다.

  • a
    • b
    • c

이렇게 하면 다음과 같이 총 3개의 패키지가 존재한다.

a, a.b, a.c

계층 구조상 a 패키지 하위에 a.b 패키지와 a.c 패키지가 있다.

그런데 이것은 우리 눈에 보기에 계층 구졸르 이룰 뿐이다. a 패키지와 a.b, a.c 패키지는 서로 완전회 다른 패키지이다.

따라서 a 패키지의 클래스에서 a.b 패키지의 클래스가 필요하면 import 해서 사용해야 한다. 반대도 물론 마찬가지이다.

정리하면 패키지가 계층 구조를 이루더라도 모든 패키지는 서로 다른 패키지이다.
물론 사람이 이해하기 쉽게 계층 구조를 잘 활용해서 패키지를 분류하는 것은 좋다. 참고로 카테고리는 보통 큰 분류에서 세세한 분류로 점점 나누어진다. 패키지도 마찬가지이다.

6. 접근 제어자

1. 접근 제어자 이해

자바는 public, private 같은 접근 제어자(access modifier)를 제공한다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.

이런 접근 제어자가 왜 필요할까?

스피커에 들어가는 소프트웨어를 개발하는 개발자라고 생각해보자. 스피커의 음량은 절대로 100을 넘으면 안된다는 요구사항이 있다.(100을 넘어가면 스피커가 고장난다고 가정하자)

  • Speaker 객체
package access;

public class Speaker {
	int volume;
    
    Speaker(int volume) {
 		this.volume = volume;
    }
    
	void volumeUp() {
 		if (volume >= 100) {
 			System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
        } 
		else {
            volume += 10;
 			System.out.println("음량을 10 증가합니다.");
		}
    }
    
 	void volumeDown() {
    	volume -= 10;
 		System.out.println("volumeDown 호출");
    }
    
 	void showVolume() {
 		System.out.println("현재 음량:" + volume);
    }
 }

위의 Speaker 객체에서 volumeUp을 통해 볼륨을 올린다. 이 메서드를 통해 볼륨이 100이상 넘어가지 않도록 제어하고 있다.

새로운 개발자가 이 Speaker 객체를 사용한다고 해보자. 이때 새로운 개발자는 스피커가 100이상 넘어가면 스피커가 망가지는 것을 모른다. 새로운 개발자는 볼륨이 100이상 올라가지 않는 것을 보고 Speaker 클래스를 살펴본다. 이때 volume 필드를 직접 사용할 수 있기 때문에 새로운 개발자는 volume 필드에 직접 접근해 값을 200으로 설정하고 이 코드를 실행한 순간 스피커는 고장난다.

volume 필드

Speaker 객체를 사용하는 사용자는 Speaker 의 volume 필드와 메서드에 모두 접근할 수 있다.

앞서 volumeUp() 과 같은 메서드를 만들어서 음량이 100을 넘지 못하도록 제약을 걸어놨지만 소용이 없다. 왜냐하면 Speaker 를 사용하는 입장에서는 volume 필드에 직접 접근해서 원하는 값을 설정할 수 있기 때문이다.

이런 문제를 근본적으로 해결하기 위해서는 volume 필드의 외부 접근을 막을 수 있는 방법이 필요하다.

만약 Speaker 클래스를 개발하는 개발자가 처음부터 private을 사용해서 volume 필드의 외부 접근을 막아두었다면 어떠했을까? 새로운 개발자도 volume 필드에 직접 접근하지 않고, volumeUp() 과 같은 메서드를 통해서 접근했을 것이다. 결과적으로 Speaker 가 망가지는 문제는 발생하지 않았을 것이다.

참고 : 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약을 제공하는 프로그램이다.

2. 접근 제어자 종류

접근 제어자 종류

  • private : 모든 외부 호출을 막는다.
  • default(pacakage-private) : 같은 패키지안에서 호출은 허용한다.
  • protected : 같은 패키지안에서 호출을 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
  • public : 모든 외부 호출을 허용한다.

package-private

접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용된다.

default 라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만, 실제로는 package-private 이 더 정확환 표현이다. 왜냐하면 해당 접근 제어자를 사용하는 멤버는 동일한 패키지 내의 다른 클래스에서만 접근이 가능하기 때문이다.

접근 제어자 사용 위치

접근 제어자는 필드와 메서드, 생성자에 사용된다.
추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있다.

접근 제어자 핵심은 속성과 기능을 외부로부터 숨기는 것이다.

  • private 은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없다.
  • default 는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없다.
  • protected 는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
  • public 은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.

3.접근 제어자 사용 - 클래스 레벨

클래스 레벨의 접근 제어자 규칙

  • 클래스 레벨의 접근 제어자는 public, default 만 사용할 수 있따.
    • private, protected 는 사용할 수 없다.
  • public 클래스는 반드시 파일명과 이름이 같아야 한다.
    • 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
    • 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.

7. 캡슐화

캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나다. 캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말한다. 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있다.

캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것이다.

1. 데이터를 숨겨라

객체에는 속성(데이터)과 기능(메서드)이 있다. 캡슐화에서 가장 필수로 숨겨야 하는 것은 속성(데이터)이다. Speaker의 volume을 떠올려 보자. 객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다. 결국 모든 안전망을 다 빠져나가게 된다. 따라서 캡슐화가 깨진다.

우리가 자동차를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않는다. 단지 자동차가 제공하는 액셀 기능을 사용해서 액셀을 밟으면 자동차가 나머지는 다 알아서 하는 것이다.

우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려보자. 음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나, 볼륨 상태의 데이털르 직접 수정할 일이 있을까? 우리는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누룰 뿐이다. 그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지 않는다. 전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일이다. 불륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 불륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일이다. 쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것이다. 복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아다.

객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.

2. 기능을 숨겨라

객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있다. 이런 기능도 모두 감추는 것이 좋다. 우리가 자동차를 운전하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리가 알 필요는 없다. 우리는 단지 액셀과 핸들 정도의 기능만 알면 된다.

만약 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 한다.

사용자 입장에서 꼭 필요한 기능만 외부에 노출하자. 나머지 기능은 모두 내부로 숨기자

정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화이다.