객체 선언의 근본: Type 변수 = new Constructor() 구조 분석
자바에서 객체를 생성하는 기본 구조는 Type 변수 = new Constructor()이다. 이 패턴은 자바의 객체 지향적 특성을 잘 보여주는 핵심 문법 요소이다. 각 구성 요소의 의미를 분석해보면 다음과 같다.
Type 변수명 = new Constructor();
- Type: 변수의 참조 타입을 정의한다. 클래스, 인터페이스, 또는 제네릭 타입이 올 수 있다.
- 변수명: 메모리에 생성된 객체를 참조하는 이름이다.
- new: 힙 메모리에 객체를 생성하는 키워드이다.
- Constructor(): 해당 클래스의 생성자를 호출한다.
여기서 중요한 점은 new의 진정한 의미이다. new는 단순히 메모리에 공간을 할당하는 것이 아니라, 실제로 힙 메모리에 객체를 생성하고 그 참조(주소)를 반환한다. 그리고 이 참조가 변수에 저장되는 것이다.
예를 들어:
String str = new String("Hello");
이 코드에서 실제로 발생하는 일은 다음과 같다:
- new String("Hello")이 힙 메모리에 String 객체를 생성한다.
- 생성된 객체의 메모리 주소(참조)가 반환된다.
- 이 참조값이 str 변수에 저장된다.
인터페이스와 구현체: 설계도와 실체의 관계
자바에서 컬렉션 프레임워크를 사용할 때 흔히 볼 수 있는 패턴은 다음과 같다:
List<String> list = new ArrayList<>();
이 코드는 인터페이스와 구현체의 관계를 보여준다. List는 인터페이스(설계도)이고, ArrayList는 그 구현체(실제 제품)이다.
여기서 의문이 생긴다. "List는 인터페이스인데 왜 직접 new List()로 객체를 생성할 수 없는가?"
이는 인터페이스의 본질적 특성 때문이다. 인터페이스는 메서드의 시그니처만 정의하고 실제 구현은 포함하지 않는다. 따라서 인터페이스 자체로는 완전한 객체를 생성할 수 없으며, 반드시 그 인터페이스를 구현한 구체 클래스가 필요하다.
// 불가능한 코드
List<String> list = new List<>(); // 컴파일 에러
// 올바른 코드
List<String> list = new ArrayList<>();
이러한 패턴은 다형성(Polymorphism)의 기본이 된다. 상위 타입(인터페이스나 부모 클래스)으로 선언하고 하위 타입(구현체나 자식 클래스)으로 초기화하는 것이다.
예를 들어:
Animal animal = new Dog(); // Animal은 부모 클래스, Dog는 자식 클래스
animal.sound(); // Dog의 sound() 메서드가 실행됨 (동적 바인딩)
여기서 animal 변수는 Animal 타입으로 선언되었지만, 실제로는 Dog 객체를 참조한다. 메서드 호출 시 런타임에 실제 객체의 메서드가 실행되는데, 이를 동적 바인딩이라고 한다.
배열의 특수성: 생성자 없는 객체 생성
앞서 살펴본 일반적인 객체 생성 패턴에 예외가 있다. 바로 배열이다.
int[] arr = new int[5];
이 코드에서 new int[5]는 생성자 호출처럼 보이지만, 실제로는 그렇지 않다. 배열은 특별한 객체로, JVM 수준에서 직접 처리된다. 정확히는 newarray, anewarray 같은 JVM 바이트코드 명령어로 처리된다.
따라서 정확한 객체 생성 패턴은 다음과 같이 수정되어야 한다:
Type 변수 = new 생성_표현식;
여기서 '생성_표현식'은 '생성자 호출' 또는 '배열 생성 표현식'일 수 있다.
기본형과 참조형: 배열의 차이
Java에서 배열은 기본형(primitive)과 참조형(reference) 모두 가능하다. 그러나 두 유형은 메모리 구조, 초기값, 성능에서 차이가 있다.
int[] arr1 = new int[3]; // 기본형 배열: [0, 0, 0]으로 초기화
String[] arr2 = new String[3]; // 참조형 배열: [null, null, null]로 초기화
기본형 배열은 값 자체를 저장하므로 메모리 효율이 좋고 속도가 빠르다. 반면 참조형 배열은 객체의 참조(주소)를 저장하므로 상대적으로 느리다.
배열과 ArrayList: 고정과 가변의 차이
배열과 ArrayList는 비슷해 보이지만 근본적인 차이가 있다.
int[] arr = new int[5]; // 고정 크기, 기본형 가능
ArrayList<Integer> list = new ArrayList<>(); // 가변 크기, 참조형만 가능
배열의 특징:
- 고정된 크기
- 기본형 데이터 저장 가능
- 인덱스 접근 빠름
- 크기 변경 불가
ArrayList의 특징:
- 동적으로 크기 조절 가능
- 참조형 데이터만 저장 가능
- 데이터 추가/삭제가 용이
- 내부적으로 배열을 사용하지만 필요에 따라 크기를 조절
파이썬 사용자라면 자바의 ArrayList가 파이썬의 list와 더 유사하다고 볼 수 있다.
인터페이스 다중 구현의 유연성
Java 클래스는 여러 인터페이스를 동시에 구현할 수 있다. 이는 다중 상속의 문제점을 피하면서도 다양한 행동을 가능하게 한다.
public class LinkedList<E> implements List<E>, Deque<E>, Cloneable, Serializable
이처럼 LinkedList는 List, Deque 등 여러 인터페이스를 구현했기 때문에 다양한 방식으로 사용할 수 있다:
List<Integer> list = new LinkedList<>(); // 리스트로 사용
Queue<Integer> queue = new LinkedList<>(); // 큐로 사용
Deque<Integer> deque = new LinkedList<>(); // 덱으로 사용
각 선언 방식에 따라 사용할 수 있는, 즉 접근 가능한 메서드가 달라진다. 예를 들어 Queue로 선언하면 Queue 인터페이스에 정의된 메서드만 사용할 수 있다. 이는 다형성의 또 다른 예시이다.
결론: 자바의 선언은 참조의 관점 정의
자바에서 객체 선언 방식을 이해하는 핵심은 "왼쪽은 관점, 오른쪽은 실체"라는 개념이다. 왼쪽에 선언하는 타입은 그 객체를 어떤 관점에서 볼 것인지 정의하고, 오른쪽의 생성자는 실제로 어떤 객체를 생성할지 결정한다.
이러한 유연성은 코드의 재사용성과 유지보수성을 높이는 핵심 요소이다. 특히 인터페이스를 타입으로 사용하면 구현체를 쉽게 교체할 수 있어 코드의 유연성이 크게 향상된다.
자바의 객체 선언과 생성 방식은 언뜻 복잡해 보일 수 있지만, 그 근본적인 원리를 이해하면 객체 지향 프로그래밍의 강력한 도구가 된다.