[JAVA] 클래스, 필드, 메서드, 생성자, this()
안녕하세요. 이번 글은 객체지향의 기본이자 핵심에 대한 설명입니다.
- 객체지향 프로그래밍이란?
객체지향 프로그래밍이란 객체를 중심으로 하는 프로그래밍이며 순서를 중요시하기보다 객체 간의 상호작용을 중심으로 프로그램을 구현하는 프로그래밍을 의미한다. 이의 반대말은 절차지향 프로그래밍이다.
JAVA에서 객체지향의 핵심 요소는 클래스이다. 이제 클래스를 시작으로 구성 요소에 대한 설명을 이어나가도록 한다.
- 클래스
클래스 (Class)란 프로그램 내 전반적인 구조 및 설계도이며 객체를 생성하기 위한 핵심 도구이다.
내부에는 변수인 Field, 함수인 Method, 생성자와 클래스 안에 또다른 클래스인 Inner Class가 있다. 이때 생성자를 제외한 나머지 3개의 구성요소들을 클래스의 멤버라고 한다.
- 객체
객체란 클래스를 통해 생성된 존재이다. 이때 클래스를 통해 객체를 생성하는 과정을 인스턴스화라고 한다. 이 때 객체와 인스턴스는 전체적으로 비슷하나 인스턴스는 객체의 하위호환이다.
인스턴스화를 통해 중복되는 코드를 불필요하게 입력하지 않아도 되며 목적과 특성에 따라 재사용이 가능하다는 점에서 효율적인 알고리즘 생성이 가능하다.
package classes;
public class NoteBook {
String CPU;
public static void main (String args[]) {
NoteBook MacBook = new NoteBook();
NoteBook XPS15 = new NoteBook();
NoteBook Galaxy_Book = new NoteBook();
NoteBook gram = new NoteBook();
MacBook.CPU = "M1";
XPS15.CPU = "i7-12700H";
Galaxy_Book.CPU = "i5-8265U";
gram.CPU = "i7-7700HQ";
}
}
다음과 같이 하나의 클래스를 기반으로 목적에 맞게 여러 객체를 선언할 수 있으며 객체의 목적에 맞게 필드와 메서드를 알맞게 활용할 수 있다는 점을 확인할 수 있다. 또한 일일이 목적별로 같은 목적인 필드를 생성 내지 선언할 필요가 없다는 점에서 재사용이 가능하며 효율적인 프로그램 구현이 가능하다는 점을 알 수 있다.
클래스를 구성하는 요소는 크게 외부와 내부로 나누면 다음과 같다.
이 글에서는 내부 요소인 필드, 메소드, 생성자 부분을 설명하고 다음 글부터는 외부 요소에 대한 설명을 이어나갈 것이다.
- 필드
필드 (Field)란 어떠한 특정 값을 저장하는 위한 변수이며 클래스의 속성을 나타내는 역할이다.
package classes;
public class NoteBook {
public double display = 15.6;
public String CPU = "Intel 12th Gen i7-12700H";
public String ram_Gen = "DDR5";
public int ram_Volume = 16;
}
다음과 같이 필드는 클래스의 속성이자 정보를 나타내는 라벨과 같다는 점을 알 수 있다. 예를 들어 노트북을 구매하면 사양을 확인할 수 있는데 이를 클래스와 필드로 표현하면 다음과 같다. 내부에는 디스플레이 크기, CPU, RAM의 종류와 크기에 대한 정보가 변수를 통해 명시되어 있다. 이외에도 실제 노트북의 정보는 더 많이 표현해야 되겠지만 이를 기반으로 메소드를 사용해서 기능을 사용하고 필드를 적절히 활용하게 된다.
Field는 위치가 어디에 있느냐에 따라 지역 변수와 전역변수로 나눠진다.
- 지역 변수 : 메서드에 포함된 변수이며 해당 영역에서만 사용가능
- 전역 변수 : 클래스에 포함된 변수이며 클래스 안에서는 메서드에서도 사용가능
package classes;
public class NoteBook {
public String CPU = "Intel 12th Gen i7-12700H"; // 전역변수
void show_PCInfo() {
int CPU_Thread = 20; // 지역변수
System.out.println("======= PC Info =======");
System.out.println("CPU : " + CPU);
}
public static void main (String args[]) {
NoteBook nb = new NoteBook();
nb.show_PCInfo();
//System.out.println(nb.CPU_Thread); // 지역 변수는 해당 메소드같은 영역에서만 사용 가능하다.
}
}
다음과 같이 String_CPU라는 변수는 어느 메서드에도 소속되지 않고 클래스 내에 선언되어 있다. 그래서 메서드 안에서도 CPU를 자유롭게 호출 및 변경이 가능하다는 점을 알 수 있다. 반면 CPU_Thread라는 변수는 메서드 내부에 선언되어있는 만큼 메서드 안에서만 사용 가능하며 해당 영역을 벗어난 곳에서는 호출 및 변경할 수 없다는 점을 확인할 수 있다.
다음 그림을 통해 지역변수와 전역변수의 차이점을 명확하게 확인할 수 있다.
- 메소드
메소드 (Method)는 함수로서 데이터와 API를 활용해서 기능을 구현하기 위한 공간이다.
package classes;
public class NoteBook {
public double display = 15.6;
public String CPU = "Intel 12th Gen i7-12700H";
public String ram_Gen = "DDR5";
public int ram_Volume = 16;
public boolean power = false;
void powerOn() {
power = true;
System.out.println("Power On...");
}
void powerOff() {
power = false;
System.out.println("Power Off...");
}
void show_PCInfo() {
System.out.println("======= PC Info =======");
System.out.println("CPU : " + CPU);
System.out.println("RAM Volume : " + ram_Volume);
System.out.println("RAM Gen : " + ram_Gen);
}
public static void main (String args[]) {
NoteBook nb = new NoteBook();
nb.powerOn();
nb.show_PCInfo();
}
}
상단 소스코드처럼 메소드를 선언하고 내부에 출력문과 변수의 값 변경 등과 같이 기능을 구현하고 Main 함수에 객체를 선언하고 객체를 기반으로 메소드를 호출해서 작동(출력)시킨 것을 확인할 수 있다. 이렇게 메소드가 함수로서 기능을 구현하는 역할을 하고 있다는 점을 알 수 있다.
또한 메서드 앞에 붙는 리턴 타입에 따라 반환 필수 여부가 달라진다.
package classes;
public class NoteBook {
public double display = 15.6;
public String CPU = "Intel 12th Gen i7-12700H";
public String ram_Gen = "DDR5";
public int ram_Volume = 16;
void show_PCInfo() {
System.out.println("======= PC Info =======");
System.out.println("CPU : " + CPU);
System.out.println("RAM Volume : " + ram_Volume);
System.out.println("RAM Gen : " + ram_Gen);
//System.out.println("CPU's Thread : " + CPU_Theard(nb.CPU));
// 다음과 같이 같은 클래스 내부에서 메서드 안에 메서드를 호출가능하다.
// 별도의 객체 생성 및 사용이 불필요하다.
}
int CPU_Theard(String CPU) {
if (CPU.equals("Intel 12th Gen i7-12700H")) {
return 20;
}
else if (CPU.equals("Intel 13th Gen i5-13500H")) {
return 16;
}
else return 0;
}
public static void main (String args[]) {
NoteBook nb = new NoteBook();
nb.show_PCInfo();
System.out.println("CPU's Thread : " + nb.CPU_Theard(nb.CPU));
}
}
다음과 같이 리턴타입이 void면 별도의 return을 통한 반환이 필요없으나 void 외 자료형을 리턴 타입으로 지정할 경우에는 return 키워드를 사용하여 최종적으로 반환할 값을 지정이 필요하다.
상단 소스코드에서도 메서드 show_PCInfo()의 경우에는 void 타입이어서 단순 출력문만 사용해도 오류가 없지만 CPU_Thread()처럼 자료형을 타입으로 지정하면 return이 반드시 필요하다. 없으면 에러가 발생하기 때문이다. 추가로 리턴 타입에는 int 말고도 String, boolean, double ... 어떤 자료형도 들어갈 수 있다. 또한 같은 클래스라면 메서드 안에 메서드를 호출하는 것이 가능하다는 점도 확인할 수 있다.
이때 CPU_Thread() 메서드에서 괄호 ()안에 들어있는 변수 String CPU는 매개변수라고 한다.
매개변수란 메서드 (함수)를 정의할 때 활용되는 변수이며 이러한 메서드를 호출할 때는 괄호 안에 숫자 또는 문자를 입력해야 되는데 이때 입력하는 값 내지 실제값을 인수라고 한다.
- 생성자
생성자 (Constructor)란 클래스명과 같으며 객체를 생성하기 위한 하나의 메소드이다.
package classes;
public class NoteBook {
public String CPU = "Intel 12th Gen i7-12700H";
public String ram_Gen = "DDR5";
public int ram_Volume = 16;
void show_PCInfo() {
System.out.println("======= PC Info =======");
System.out.println("CPU : " + CPU);
System.out.println("RAM Volume : " + ram_Volume);
System.out.println("RAM Gen : " + ram_Gen);
}
public static void main (String args[]) {
NoteBook nb = new NoteBook();
nb.show_PCInfo();
}
}
다음과 같이 NoteBook 클래스를 사용하기 위해서는 객체를 생성해야 하는데 이를 위해 필요한 존재가 생성자다.
객체를 생성하는 소스코드는 NoteBook nb = new NoteBook();인데 생성자를 나타내는 부분이 new NoteBook()이다.
이렇듯 생성자는 객체를 생성하는 데 필요한 부품이며 객체를 생성함으로서 클래스 내부의 필드와 메소드를 사용할 수 있다는 점을 확인할 수 있다.
package classes;
public class NoteBook {
String CPU;
NoteBook() {} // 이렇게 직접 정의하지 않아도 컴파일러가 자동 생성
//NoteBook() {} // 하지만 이런 식으로 기본 생성자가 없는 채 매개변수 생성자를 선언하면 기본 생성자를 사용할 수 없다.
NoteBook(int a, int b) {}
public static void main (String args[]) {
NoteBook MacBook = new NoteBook();
NoteBook XPS15 = new NoteBook();
NoteBook Galaxy_Book = new NoteBook();
NoteBook gram = new NoteBook();
MacBook.CPU = "M1";
XPS15.CPU = "i7-12700H";
Galaxy_Book.CPU = "i5-8265U";
gram.CPU = "i7-7700HQ";
}
}
생성자도 매개변수가 있는 생성자를 정의가능하다. 먼저 기본 생성자는 매개변수가 없는 생성자를 의미하는데 이는 별도로 정의하지 않아도 컴파일러 내부에서 자동으로 생성해준다. 그러나 매개변수가 있는 생성자를 정의한 채 기본 생성자를 별도로 정의하지 않으면 기본 생성자는 만들어지지 않을 뿐만 아니라 사용할 수 없다. 또한 리턴 타입이 없다.
- 객체 생성과정
객체를 생성하는 구문은 클래스명 참조변수명 = new 생성자();이다. 이를 문장으로 풀면 생성자를 통해 생성한 객체를 Heap 영역에 넣고 클래스명 타입의 참조변수명에 (주소값을) 저장하라 로 볼 수 있다.
그리고 참조변수명이 객체의 역할을 하며 new 키워드는 Heap 영역에 저장을 유도하는 역할을 하고 있다.
다음 그림은 객체 생성을 메모리 구조로 표현한 것이다. 객체 생성하는 과정에서 변수에는 Heap 영역에 있는 생성자의 주소값을 저장하고 이를 참조하여 Heap 영역에 접근한다. 그리고 생성자 내부에는 클래스에 있던 필드, 메서드, 이너 클래스가 들어있다. 클래스의 구성요소를 사용하는 데 필요한 존재가 객체이고 객체가 생성자를 통해 만들어지는 만큼 생성자에는 클래스의 구성요소가 들어가있어야 한다는 점을 확인할 수 있다. 그러나 메서드는 클래스 영역 안에 있으면서 Heap 영역의 생성자는 그의 주소값을 참조하는 방식으로 이뤄지고 있다. 이는 아무리 객체의 사용 목적이 달라도 메소드의 근본적인 기능은 변하지 않기 때문이다.
- this, this()
- this : 현재 객체를 의미
- this() : 현재 객체의 생성자이며 문장의 첫 라인에 위치, 생성자가 다른 생성자를 호출할 때 사용
this는 말그대로 자기 자신을 가리키는 객체를 의미한다. 또한 내부 객체 참조 변수명이다.
package classes;
public class NoteBook {
String CPU = "i5-8265U";
int ssd = 512;
int ram = 16;
NoteBook() {} // 이렇게 직접 정의하지 않아도 컴파일러가 자동 생성
NoteBook(int a, int b) {
// 매개변수와 전역변수의 이름이 다르면 이렇게 묵시적으로 사용 처리됨
ssd = a;
ram = b;
}
NoteBook(int ssd, int ram, String CPU) {
// 다음과 같이 매개변수와 전역변수 이름이 같을 시 명확한 구분을 위해 this 키워드 사용
this.CPU = CPU;
this.ssd = ssd;
this.ram = ram;
// 이렇게 하면 명확한 구분이 어렵고 지역변수로 인식하기 때문에 초기값으로 간주한다.
//CPU = CPU;
//ssd = ssd;
//ram = ram;
}
}
다음과 같이 this 키워드는 클래스 내 전역변수와 매개변수를 구분하기 위한 존재인 점을 확인할 수 있다.
만약 둘 다 이름이 같은 상황에서 this 키워드를 생략하면 어느 것이 전역변수인지 매개변수인지를 구분하기 어려워서 해당 이름의 변수를 지역변수로 인식한다. 그러나 지역변수를 별도로 선언한 적이 없는 만큼 컴파일러는 해당 변수의 초기값을 내보낸다. 그러면 전역변수에서 선언했던 값이 제대로 나오지 않는 것을 확인할 수 있다.
이렇듯 this 키워드는 전역변수와 매개변수 이름이 같을 때 이들이 각각의 다른 존재라는 점을 구분해주는 키워드라는 점을 확인할 수 있다.
this()는 메서드로 구분되며 클래스 내 다른 생성자를 호출하는 키워드이다. 반드시 생성자의 첫 줄에 있어야 하며 생성자 내부에서만 사용 가능한 점이 특징이자 유의사항이다.
package classes;
public class NoteBook {
String CPU;
int ssd;
int ram;
NoteBook() {
System.out.println("기본 생성자");
}
NoteBook(int a, int b) {
this();
ssd = a;
ram = b;
}
public static void main (String args[]) {
NoteBook MacBook = new NoteBook(1024, 32); // Console : 기본 생성자
}
}
다음과 같이 생성자 내에서 다른 생성자를 호출할 때는 반드시 this() 키워드를 사용해야 한다.
this() 키워드가 있는 생성자를 바탕으로 메인 메서드에서 객체를 생성했는데 실행 결과로 기본 생성자에 내장되어있는 출력문이 나오는 점을 확인할 수 있다. 이는 곧 this() 키워드의 제기능을 확인할 수 있다고 볼 수 있다.
또한 해당 키워드는 생성자에서만 사용할 수 있다고 했는데 이에 대한 설명을 더하자면 아무리 생성자 안이어도 생성자 내부에 메서드가 있는 상황이라면 메서드 안에서 해당 키워드를 사용하는 것은 불가능하다. 반드시 생성자 내부 맨 첫 줄에 있어야 한다. 이를 통해 생성자 안에 생성자를 호출함으로써 두 개의 생성자를 모두 사용할 수 있다는 점을 확인가능하다.
package classes;
public class NoteBook {
String CPU;
int ssd;
int ram;
NoteBook() {
System.out.println("기본 생성자");
}
NoteBook(int a, int b) {
this();
ssd = a;
ram = b;
System.out.println("두 번째 생성자 호출");
}
NoteBook(int ssd, int ram, String CPU) {
this(512, 16);
this.CPU = CPU;
this.ssd = ssd;
this.ram = ram;
}
public static void main (String args[]) {
NoteBook MacBook = new NoteBook(1024, 32, "M2 Pro"); // Console : 기본 생성자 두 번째 생성자 호출
}
}
다음과 같이 this()안에 매개변수를 기반으로 인수를 넣어서 매개변수가 있는 생성자를 호출하는 것도 가능하다.
소스코드에도 나와있듯이 매개변수가 3개 있는 생성자를 기반으로 메인 메서드에서 객체를 생성했는데 this(512, 16);가 있는 만큼 NoteBook(int ssd, int ram) {} 내부에 있는 출력문을 사용할 수 있다는 점을 확인할 수 있다.
또한 NoteBook(int ssd, int ram) {}에 this()가 있는 상태에서 NoteBook(int ssd, int ram, String CPU) {}를 통한 객체를 선언하면 첫 번째 생성자도 같이 출력할 수 있는 점 또한 확인 가능하다.
확인 후 추가적으로 보완할 수 있으면 보완하겠습니다.
다음 글은 package와 import에 대한 설명입니다.
감사합니다.