[Java 회고 03] 클래스와 객체 생성 : 생성자, 참조와 얕은/깊은 복사, 임시 객체와 String
2025-12-18 |
글 정보
| 카테고리 | java/study/basic |
| 작성일 | 2025-12-18 |
| 게시 여부 | true |
| series | Java 회고 |
| series-order | 3 |
| 제목 | [Java 회고 03] 클래스와 객체 생성 : 생성자, 참조와 얕은/깊은 복사, 임시 객체와 String |
클래스, 객체, 인스턴스
- 클래스 : 객체를 구현하기 위한 문법, 형식
- 객체 : 단위이고, 추상적임
- 인스턴스 : 메모리(Heap)에 실체화된 것
- 변수(참조자)를 사용하는 이유는 메모리 사용을 위한 인스턴스가 존재하는 메모리 주소를 가리키는 리모컨 같은 역할
- 변수는 Stack에 존재함
클래스 문법
- 구성요소(변수, 함수)를 멤버라 지칭
- 멤버
- 필드 = 변수
- 메서드 = 함수
- 선언과 정의가 공존한다.
- C언어와 같은 대부분 언어에선 구분하나, 자바는 선언과 정의가 동시에 되어야함
- 클래스 위에는 항상 패키지가 존재한다.
클래스 생성자
// 1. 반환 타입(void 등)을 적지 않습니다.
// 2. 메서드 이름은 반드시 '클래스 이름'과 같아야 합니다.
public Car() {
// new Car(); 를 할 때 자동으로 실행되는 코드
System.out.println("자동차가 생성되었습니다.");
}
- 사용할 수 있는 객체를 생성(
new)할 때 초기화 하는 역할
- 클래스 파일은 설계도이고, 객체(또는 인스턴스)는 해당 설계도로 만든 제품이라고 볼 수 있다.
- 일반 메서드와 비슷하지만, 리턴 타입이 아예 없다는 것이 큰 차이
class Box {
int size;
// 1. 매개변수가 없는 생성자 (기본값 설정)
Box() { this.size = 10; }
// 2. 매개변수가 있는 생성자 (값 지정)
Box(int size) { this.size = size; }
}
// 사용: new Box() 또는 new Box(50) 가능
- 일반 메서드처럼 매개변수 구성이 다른 생성자를 여러개 정의 가능
- 여러개가 정의 되어있더라도 new 연산 시 호출되는 생성자는 한개임
- 생성자에서 다른 생성자도 호출 가능
- 생성자 내에서 다른 생성자를 호출 할 때 맨 위에 위치해야함(가장 먼저 호출)
생성자
- 명시적 호출이 없으며 new 연산자를 사용함
- 객체의 초기화를 위해 문법적으로 들어가 있는 함수임
- 주의사항 : 필드 선언시 멤버에 대입한 초깃값과 생성자 초기화 값이 충돌할 경우, 생성자에 기술한 코드가 우선임
public class InitTest {
// 1. 필드 명시적 초기화
int value = 10;
public InitTest() {
// 2. 생성자에서 초기화 (이것이 덮어씌움)
this.value = 20;
}
public static void main(String[] args) {
InitTest t = new InitTest();
System.out.println(t.value); // 결과: 20
}
}
- 객체가 생성되는 시점(new)에 자동으로 호출된다.
- 또한 어떠한 생성자도 클래스에 없는경우, 컴파일러가 자동으로 빈 생성자를 추가해준다.
- 생성자를 작성할 때, 항상 클래스 자체와 관련있는 코드만 기술할것을 권장함
- 하나의 인스턴스에 여러 참조자가 존재할 수 있다.
- 이 경우 사이드 이펙트를 고려하라.
- 생성자를 private로 지정하는 경우, 생성자 사용을 제한하는 것이다.
DeepCopy VS Swallow Copy
public class ShallowCopyExample {
public static void main(String[] args) {
// 원본 객체 생성
Person original = new Person("홍길동", new Address("서울"));
// 얕은 복사 (생성자에서 주소값만 그대로 전달)
Person shallowCopied = new Person(original);
// 복사본의 주소를 "부산"으로 변경
shallowCopied.address.city = "부산";
// [Side Effect 발생]
// 복사본을 바꿨는데 원본인 '홍길동'의 주소도 "부산"으로 바뀜!
System.out.println("원본 주소: " + original.address.city); // 부산 (의도치 않은 변경)
}
}
class Address {
String city;
Address(String city) { this.city = city; }
}
class Person {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
// 얕은 복사 생성자
Person(Person other) {
this.name = other.name;
this.address = other.address; // [위험] 주소값(참조)만 복사함
}
}
- 내용 자체를 카피한다면 Deep Copy
- 참조자만 카피하는 것은 Swallow Copy
- 얕은 복사는 사이드 이펙트의 원인이 된다.
- 하나의 인스턴스를 바라보는 여러개의 참조자가 있는 경우, 원본 인스턴스의 변경에 따른 부수효과를 예측하기 힘들다.
- 특히 자바에선 참조자가 파라미터로 넘어갈 때 이런 위험에 쉽게 노출된다.
- 그래서 원본 인스턴스 자체를 복사하여 새롭게 할당하는 Deep Copy를 하는 법이 중요
- Swallow Copy와 달리 각각의 인스턴스를 만들어 참조가 존재
public class DeepCopyExample {
public static void main(String[] args) {
Person original = new Person("홍길동", new Address("서울"));
// 깊은 복사 (내부 Address도 새로 생성)
Person deepCopied = new Person(original);
// 복사본의 주소 변경
deepCopied.address.city = "부산";
// [안전함]
// 원본은 여전히 "서울"을 유지함. 서로 다른 주소 객체를 가짐.
System.out.println("원본 주소: " + original.address.city); // 서울
}
}
class Person {
String name;
Address address;
// 깊은 복사 생성자
Person(Person other) {
this.name = other.name;
// [핵심] new 키워드로 아예 새로운 Address 인스턴스를 생성해서 할당
this.address = new Address(other.address.city);
}
// 일반 생성자 생략 (위 예제와 동일) ...
}
참조자
TestClass testClass = new TestClass();라는 구문이 있을때, testClass를 참조자 라고 한다.
- 클래스 형식에 대한 변수 선언은 모두가 참조자이다.
- 참조자는 인스턴스를 가리키는 레퍼런스이다. 인스턴스 자체가 아니다.
참조자
public class ReferenceIdentity {
public static void main(String[] args) {
// 1. 인스턴스 생성
// 힙 메모리에 "익명의 Member 인스턴스"가 생성되고, 그 주소를 p1이 가짐
Member p1 = new Member("Alice");
// 2. 참조자 복사 (포인터 복사)
// 인스턴스가 복사되는 것이 아니라, '주소값'만 p2에 복사됨
Member p2 = p1;
// 3. 동일성 확인
// p2를 통해 내용을 바꾸면, p1이 가리키는 대상도 바뀜 (같은 곳을 보니까)
p2.name = "Bob";
System.out.println(p1.name); // Bob 출력
// p1과 p2는 서로 다른 변수지만, 같은 주소(인스턴스)를 가리킴
System.out.println(p1 == p2); // true (주소값이 같음)
}
}
class Member {
String name;
Member(String name) { this.name = name; }
}
- 자바는 참조와 인스턴스의 구분이 표면적으로 되어있지 않아, 초보자들의 경우 객체 복사에 문제가 생길 수 있다.
- 자바에게 이름이 부여되는 것은 오직 참조자뿐임 = 식별자
- 인스턴스는 이름이 없다. 오직 주소만 존재한다.
- 그렇다고 그 인스턴스가 메모리상에 존재하는 주소가 고정이냐? 그것도 아니다.
- 그러니 인스턴스를 주소로 식별하는 것은 쉽지 않다.
- 참조자는 단순한 포인터다.
- 대부분 이것을 인스턴스로 착각한다.
// 1. Cloneable 인터페이스를 반드시 구현해야 함 (마커 인터페이스)
class Item implements Cloneable {
int value;
// 2. protected인 clone()을 public으로 오버라이딩 해야 함
// 3. 예외(CloneNotSupportedException) 처리가 강제됨
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 기본적으로 얕은 복사(Shallow Copy) 수행
}
}
public class CloneProblem {
public static void main(String[] args) {
Item original = new Item();
try {
// 4. 리턴 타입이 Object이므로 매번 형변환(Casting) 필요
Item copy = (Item) original.clone();
} catch (CloneNotSupportedException e) {
// 5. 불필요해 보이는 예외 처리 로직 작성 필요
e.printStackTrace();
}
}
}
- 자바의 모든 클래스들은 Object를 상속받는다.
- Object에는 clone() 메서드를 사용하여 깊은 복사를 할 수 있지만
- 이는 규약이 모호하고 예외처리를 복잡하게 만든다.
깊은 복사를 하는 법
public class DeepCopyConstructor {
public static void main(String[] args) {
// 1. RHS (Right Hand Side) : 원본 객체 생성
Member rhs = new Member("홍길동", new Address("서울"));
// 2. LHS (Left Hand Side) : 복사 생성자를 통해 생성
// rhs를 넘겼지만, 내부적으로는 새로운 Address를 만들어 가짐
Member lhs = new Member(rhs);
// 3. 독립성 확인
// lhs의 주소를 바꿔도 rhs는 영향받지 않음
lhs.addr.city = "부산";
System.out.println("RHS(원본): " + rhs.addr.city); // 서울 (유지됨!)
System.out.println("LHS(복사): " + lhs.addr.city); // 부산
}
}
// 참조 타입 필드 (이 녀석도 복사가 필요함)
class Address {
String city;
Address(String city) { this.city = city; }
// Address용 복사 생성자 (재귀적인 깊은 복사를 위해 권장)
Address(Address rhs) {
this.city = rhs.city; // 값만 베껴옴
}
}
class Member {
String name;
Address addr; // 참조형 변수 (Deep Copy 대상)
// 일반 생성자
Member(String name, Address addr) {
this.name = name;
this.addr = addr;
}
// ★ 복사 생성자 (Copy Constructor) 구현 ★
// 파라미터로 넘어온 rhs(원본)의 데이터를 읽어 this(LHS)에 채운다.
Member(Member rhs) {
this.name = rhs.name; // String은 불변객체라 참조 복사도 무관
// [중요] 참조형 필드는 반드시 'new'를 사용해 새로 만들어야 함!
// rhs.addr의 주소를 넣는게 아니라, rhs.addr의 '값'을 가진 새 객체를 연결
this.addr = new Address(rhs.addr);
}
}
- 권장 방법은 복사 생성자를 만드는 것이다.
- 복사 생성자는 항상 Deep Copy와 붙어다니는 개념
- 애초에 복사 생성자는 Deep Copy를 위해 존재한다.
- 복사의 원본을 오른쪽에 둔다 하여 RHS라는 변수명을 사용한다
- Right Hand Side
- 그러면 복사의 결과물은 LHS라고 하며, 복사 생성자는 RHS의 Value를 읽어들여 LHS로 카피
- 이는 C++ 스타일의 문법이며, 자바엔 없다.
- 즉, 직접 만들어야 한다!
- 초보때 실수를 줄이고 싶다면 객체 참조자가 멤버라면 DeepCopy 메서드를 만들고 사용하라
- 적어도 참조로 인한 문제는 방지할 수 있다.
임시 객체
public class Main {
public static void main(String[] args) {
// 1. 일반적인 방식 (참조자 O)
// 'p'라는 참조 변수가 힙 영역의 실제 Point 인스턴스를 가리킵니다.
Point p = new Point(10, 20);
p.print();
// 2. 임시 객체 사용 (참조자 X)
// new Point(30, 40)으로 인스턴스는 생성되지만, 이름이 없습니다.
// print() 실행 직후, 더 이상 접근할 수 없어 가비지 컬렉션(GC) 대상이 됩니다.
new Point(30, 40).print();
}
}
// 간단한 Point 클래스 (설명용)
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
void print() { System.out.println("좌표: " + x + ", " + y); }
}
- 객체를 만들 때 항상 참조자와 실제 인스턴스가 구분됨을 생각해라
- 인스턴스가 생성되는 경우 접근할 참조자가 없는 경우 해당 인스턴스를 임시객체라고 한다.
- 또는 코드에서 명백히 드러나지 않는 인스턴스를 의미한다.
- 보통의 경우 클래스 형식이라 하면, new 연산을 통해 객체를 만든다.
- 하지만 String은 명백한 클래스이지만 리터럴을 통해 생성이 가능하다.
String
public class StringMemory {
public static void main(String[] args) {
// 1. new 연산자 사용 (Heap 메모리)
// 매번 새로운 객체가 힙 영역에 생성됩니다. 같은 문자열이라도 주소값이 다릅니다.
String strHeap = new String("Java");
// 2. 리터럴 사용 (Runtime Constant Pool)
// JVM이 관리하는 풀(Pool)에 저장됩니다. 이미 같은 문자열이 있다면 재사용합니다.
String strPool = "Java";
// 주소값 비교 (==)
// strHeap은 힙, strPool은 상수 풀에 있으므로 서로 다른 주소를 가집니다.
System.out.println(strHeap == strPool); // false 출력
}
}
public class StringConcatProblem {
public static void main(String[] args) {
// 코드상으로는 한 줄이지만, 내부적으로는 여러 임시 객체가 생성됩니다.
String result = "Hello" + "World" + "!!";
// [내부 동작 과정]
// 1. "Hello"와 "World"가 리터럴로 존재
// 2. 둘을 합친 "HelloWorld"라는 **임시 객체(Instance)**가 힙에 생성됨 (사용자가 볼 수 없음)
// 3. 생성된 "HelloWorld"와 "!!"를 합쳐서 최종 결과 "HelloWorld!!" 생성
// 4. 중간에 생겼던 "HelloWorld"는 참조자가 없어 가비지(Garbage)가 됨
}
}
- String을 new 연산으로 만드는 경우 해당 String은 Heap메모리에 적재된다.
- 하지만 리터럴(
")로 만드는 경우 JVM이 Runtime constant pool에 적재된다.
- 문제는? 덧셈연산이 된다는 것!
- "Hello" + "World" + "!!" 를 한다면 순서대로 처리되면서 "HelloWorld"가 임시객체가 된다!
- "HelloWorld" + "!!" 처럼 순서대로 처리되기에, JVM이 내부에서 만든 임시객체가 된다.
- 임시객체는 메모리상 비효율의 직접적인 원인이 된다.
- 특히 String은 이를 자주 유발함으로 주의해서 사용해야한다.
연관 문서
- 이전: [[Java 회고 02 객체의 본질 - 메모리 구조와 설계의 관점]]