Java의 Object
클래스는 Java에서 모든 클래스의 조상 클래스입니다. 즉, Java에서 작성된 모든 클래스는 기본적으로 Object
클래스를 상속받으며, Object
클래스에서 제공하는 메서드를 사용할 수 있습니다. 여기서는 Object
클래스에서 중요한 5가지 메서드인 toString()
, equals()
, hashCode()
, finalize()
, clone()
에 대해 구체적으로 설명하겠습니다.
1. toString()
메서드
역할:toString()
메서드는 객체를 사람이 이해할 수 있는 문자열로 변환해줍니다. 이 메서드는 객체가 어떤 값을 가지고 있는지 쉽게 확인할 수 있도록 돕습니다.
왜 사용해야 하는가?기본적으로 객체를 출력하거나 디버깅할 때, 객체의 상태를 쉽게 확인할 수 있는 방법이 필요합니다. toString()
메서드는 그 역할을 수행하며, 이를 통해 객체의 속성값을 확인하거나 로그를 남길 때 유용합니다.
설계 이유:
기본적으로 Object
클래스의 toString()
메서드는 클래스의 이름과 메모리 주소(해시값)를 반환합니다. 하지만 우리가 필요로 하는 것은 객체가 담고 있는 유의미한 데이터이기 때문에, 많은 경우 toString()
메서드를 오버라이딩하여 우리가 원하는 정보를 반환하도록 커스터마이징하는 것이 일반적입니다.
예시:
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 25);
System.out.println(person.toString()); // Person{name='Alice', age=25}
}
}
여기서 toString()
메서드를 오버라이딩하여, 객체가 가지고 있는 속성(name
과 age
)을 문자열로 반환하도록 수정했습니다. 이를 통해 객체의 내용을 한눈에 파악할 수 있습니다.
2. equals()
메서드
역할:equals()
메서드는 두 객체가 같은지 비교하는 메서드입니다. 기본적으로는 객체의 메모리 주소(즉, 참조값)를 비교하지만, 우리가 두 객체가 같은 속성을 가지고 있는지 확인하고 싶을 때는 equals()
메서드를 오버라이딩하여 속성을 비교할 수 있습니다.
왜 사용해야 하는가?
기본적으로 두 객체의 실제 내용을 비교하고 싶을 때 사용합니다. 예를 들어, 두 명의 사람(Person) 객체가 있을 때, 단순히 그들이 같은 객체를 참조하는지(메모리 주소) 확인하는 것이 아니라, 그들의 이름과 나이가 같은지를 비교할 수 있습니다.
설계 이유:Object
클래스의 equals()
는 기본적으로 두 객체의 참조 주소를 비교합니다. 그러나 실생활에서 객체의 속성이 같으면 같은 객체라고 보고 싶기 때문에, 클래스에 맞게 equals()
를 오버라이딩하는 것이 일반적입니다.
예시:
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Alice", 25);
System.out.println(person1.equals(person2)); // true
}
}
위 예시에서는 equals()
메서드를 오버라이딩하여 두 Person
객체가 이름과 나이가 같으면 true
를 반환하도록 설정했습니다.
3. hashCode()
메서드
역할:hashCode()
메서드는 객체의 고유한 해시 코드를 반환합니다. 이 해시 코드는 주로 해시 테이블(예: HashMap
, HashSet
)에서 객체를 빠르게 검색할 때 사용됩니다.
왜 사용해야 하는가?equals()
와 함께 주로 사용됩니다. 두 객체가 같다면(equals()
메서드가 true
를 반환하는 경우), 그들의 hashCode()
도 같아야 합니다. 이는 해시 기반 컬렉션에서 객체를 저장하고 검색하는 데 중요한 역할을 합니다.
설계 이유:hashCode()
메서드는 객체를 해시 테이블에 저장할 때 효율적인 검색을 위해 필요합니다. 해시 값이 고유하면 충돌을 줄일 수 있어 검색 성능이 향상됩니다. 따라서 equals()
를 오버라이딩하면 hashCode()
도 함께 오버라이딩하는 것이 일반적입니다.
예시:
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Main {
public static void main(String[] args) {
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Alice", 25);
System.out.println(person1.hashCode()); // 동일한 해시 코드
System.out.println(person2.hashCode()); // 동일한 해시 코드
}
}
이 예시에서는 hashCode()
를 오버라이딩하여 이름과 나이를 기반으로 해시 코드를 생성합니다. 이렇게 하면 equals()
메서드가 true
를 반환하는 객체는 동일한 해시 코드를 가집니다.
4. finalize()
메서드
역할:finalize()
메서드는 객체가 더 이상 사용되지 않을 때 가비지 컬렉터에 의해 호출됩니다. 이 메서드를 통해 객체가 제거되기 전에 리소스를 정리하거나 중요한 작업을 할 수 있습니다.
왜 사용해야 하는가?
주로 객체가 제거되기 전에 시스템 자원(예: 파일, 데이터베이스 연결 등)을 해제해야 할 때 사용됩니다. 그러나 Java에서는 finalize()
메서드 대신 try-with-resources
나 명시적인 자원 관리를 권장합니다.
설계 이유:finalize()
메서드는 자바 초기 설계에서 리소스 정리 목적으로 제공되었지만, 불확실한 실행 타이밍 때문에 현재는 권장되지 않습니다.
예시:
class Resource {
@Override
protected void finalize() throws Throwable {
System.out.println("Resource is being cleaned up.");
super.finalize();
}
}
public class Main {
public static void main(String[] args) {
Resource resource = new Resource();
resource = null; // 가비지 컬렉터 대상
System.gc(); // 가비지 컬렉터 실행 요청
}
}
위 코드는 Resource
객체가 가비지 컬렉터에 의해 제거되기 전에 finalize()
가 호출되는 상황을 보여줍니다. 하지만 이 방법은 불확실성이 있으므로 대체 방법이 필요합니다.
5. clone()
메서드
역할:clone()
메서드는 객체의 복사본을 만드는 데 사용됩니다. 객체를 복제하면 원본 객체와는 독립된 새로운 객체가 생성됩니다.
왜 사용해야 하는가?
객체를 복사하고 싶은 상황이 있을 수 있습니다. 예를 들어, 원본 객체를 변경하지 않고 복제한 객체에서만 수정 작업을 할 수 있습니다.
설계 이유:
Java에서 객체 복제는 매우 유용한 기능입니다. clone()
메서드는 얕은 복사를 수행하지만, 깊은 복사가 필요한 경우 해당 메서드를 오버라이딩할 수 있습니다.
예시:
class Person implements Cloneable {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) {
try {
Person person1 = new Person("Alice", 25);
Person person2 = (Person) person1.clone();
System.out.println(person1 == person2); // false (다른 객체)
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
위 예시에서는 Person
클래스에서 clone()
메서드를 오버라이딩하여 복제된 객체를 생성합니다. 복제된 객체는 원본과는 독립적으로 동작합니다.
결론
이 다섯 가지 메서드는 Java에서 객체를 다루는 데 필수적입니다. 각 메서드는 객체의 동작을 정의하고, 효율적인 비교, 복사, 해시 테이블 사용, 가비지 수집 등의 기능을 제공합니다. toString()
과 equals()
는 객체의 의미 있는 표현과 비교를 위해 주로 오버라이딩하며, hashCode()
는 equals()
와 함께 해시 기반 컬렉션에서 유용하게 사용됩니다. finalize()
는 리소스 정리를 위한 메서드지만, 현재는 그보다 더 나은 방법들이 존재하며, clone()
은 객체의 복사 기능을 제공합니다. 각 메서드를 이해하고 올바르게 사용하는 것이 Java 개발의 중요한 부분입니다.
========================================================================================
String str = "Hello world!"; // String constant pool에 저장됨
String doNotUse = new String("Hello World!"); // heap 영역에 저장됨
String constant Pool - 코드 클래스 파일의 상수 풀(아래에서 나올 constant pool과 다름)에 `CONSTANT_String`타입으로 저장되며 런타임시 스캔 대상이 되어 string constant pool로 저장하는 구조입니다. String constant pool에 저장된 문자열은 모두 같은 객체를 재사용하게 됩니다. new 연산자를 사용하여 문자열 생성시 string constant pool을 사용하여 최적화가 가능할 수 있던 문자열을 heap영역으로 신규 저장하게 됩니다. 그러므로 성능 및 메모리 최적화를 위하여 문자열 선언 시 new 연산자 사용은 지양하는 것이 좋습니다.
Constant Pool (Class file) - 컴파일시 클래스파일 내부에 존재하는 영역으로, 클래스로더에 의해 JVM에 로드될 때 메모리에 로드합니다. 주로 클래스의 구성요소(상수, 문자열, 클래스/인터페이스 참조) 데이터를 저장하고 있습니다.
Runtime Constant Pool (JVM)
상수 풀, Runtime Constant Pool이라고도 부르며 Java 8 이전에는 JVM - Perm영역에 저장되었고, Java8 출시 이후부터는 JVM - Metaspace 영역에 저장됩니다. 앞서 설명한 Class file constant pool이 런타임시 이 영역으로 저장됩니다.
주로 클래스와 관련된 메타데이터를 저장하고 클래스 구조, 필드, 메서드와 같은 데이터를 저장합니다. 클래스 상수 풀 또한 클래스로더에 의해 클래스를 로딩할 때 Runtime Constant Pool에 저장됩니다.
Metaspace 영역으로 이관하게 된 이유
Perm영역에 이 데이터가 저장되던 시점(JDK 7 이하)에는 Class, Metadata 로딩 과정에서 메모리 릭이 발생하였고, Perm 영역의 크기를 고정적으로 설정해야 했기 때문에 메모리 부족으로 OOM이 터지는 일이 있었습니다. 이 이슈를 개선하기 위해 Constant Pool을 Metaspace영역으로 이관하였고 OOM을 피할 수 있게 되었습니다. 조금 더 설명하자면 Metaspace영역은 JVM의 Native Memory를 사용하며 JVM이 관리합니다. Perm영역과의 결정적인 차이는 메모리가 동적으로 관리되며 필요할 경우 OS에게 요청하여 메모리를 추가 할당할 수 있습니다. 이를 통해 OOM을 개선할 수 있었습니다.
정리
- JDK에 있는 자바 컴파일러를 통해 java 파일을 바이트 코드(class 파일)로 만들고, JRE에서 바이트 코드를 실행시키면 JVM이 시작되면서 JVM 위에서 바이트 코드가 기계어로 해석되어 실행된다.
- JVM의 명세를 따르는 가상 머신은 모두 JVM이다. 대표적으로 Oracle의 Hotspot이 존재한다.
- JVM의 런타임 데이터 영역에는 모든 스레드들이 공유하는 Heap, Method 영역, 각 스레드 마다 존재하는 Stack, PC Register, Native Method Stack이 존재한다.
- HotSpot의 JAVA 실행 엔진은 일반적으로 한줄 씩 바이트 코드를 읽어 인터프리터를 통해 기계어로 번역하며 자주 사용되는 바이트 코드는 JIT 컴파일러를 통해 캐시에 미리 컴파일 하는 방식으로 실행 엔진을 최적화 시킨다.
- 클래스 로더에는 BootStrap, Platform, System 클래스 로더가 존재하며 각 클래스 로더들은 위임 모델과 계층 구조를 지닌다.
- JVM은 동적으로 로딩,링크,초기화 과정을 진행하며 java 어플리케이션의 실행은 특정 클래스를 로딩, 링크, 초기화 과정을 거친 후 해당 클래스의 main method를 실행하는 것을 의미한다.
- Java의 동적 바인딩은 실행 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Referenc를 고정된 주소 값으로 바꾸는 것이며 이 때 고정된 주소 값을 선택하는 기준은 스택 위에 올라와 있는 객체의 타입이다.
클래스를 메모리에 올리는 Loading 기능은 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다는 점이다. 착각하는 점이 위의 3가지 과정이 거의 동시에 이루어져서 같이 묶어 생각하는데, 엄연히 클래스 로드(Loading)와 초기화(Initialization)은 다른 작업이다. 그리고 클래스나 클래스 내의 static 멤버들을 소스를 실행하자마자 한번에 메모리에 모두 올라가는줄 착각하는데, 곰곰히 생각해보면 언제 어디서 사용될지 모르는 static 멤버들을 처음에 전부 메모리에 올린다는건 비효율적이기 때문에, 클래스 내의 멤버를 호출하게 되면 그때서야 클래스가 동적으로 메모리에 로드된다. 즉, JVM은 실행될때 모든 클래스를 메모리에 올려놓지 않고, 필요한 클래스를 메모리에 올려 효율적으로 관리하는 것이다.
* inner 클래스를 선언할때 static 키워드를 붙여주지 않으면 '외부 참조' 현상 때문에, 내부 클래스 인스턴스를 생성하기 위해 우선적으로 만들었던 외부 클래스 인스턴스가 정상적으로 GC 수거가 안되 메모리에 잔존하게 되어 문제점을 일으키게 된다. 따라서 내부 클래스가 외부 클래스의 멤버를 가져와 사용하는 경우가 아닌 경우 반드시 내부 클래스를 선언 할 때는 static 키워드를 붙여주어야 한다.
* Outer.class 클래스 객체만 가져올 경우 클래스가 loading만 되며, 클래스 객체를 이용해 인스턴스화 하면 그제서야 클래스가 initialization이 되는걸, 점선 구분선을 통해 클래스 로딩 과정이 분리되었음을 볼 수 있다. 클래스 초기화 작업은 오직 한번만 이행된다. 이 의미는 멀티 스레드 환경에서 클래스 초기화 동작 자체는 스레드 세이프함을 의미한다.
* 싱글톤 패턴의 코드 예제를 보면, 다음과 같이 static inner 클래스를 이용해 지연 초기화를 실현한다. 내부 클래스도 결국은 클래스이기 때문에 클래스가 로드될때 딱 한번만 초기화되는 특성을 이용하여 static final 상수에 싱글톤 객체를 할당하는 기법이다. 거기다 바로 위에서 살펴봤듯이 클래스 로딩 및 초기화 과정이 스레드 세이프함을 이용하여 멀티 스레드 환경에서도 문제없이 싱글톤 인스턴스를 만들 수 있는 것이다.
'Spring & Java > Programming' 카테고리의 다른 글
8. Spring 3대 요소 (0) | 2023.11.23 |
---|---|
7. JVM (0) | 2023.11.23 |
6. 핵심원리 기본편 (0) | 2022.08.02 |
5. 개념 (0) | 2022.07.18 |
4. 스프링 웹 개발 기초 (0) | 2022.07.14 |