Computer Science/프로그래밍언어론

자바 객체 선언의 원리: 배열부터 컬렉션까지

dog-pawwer 2025. 5. 2. 16:58
반응형

객체 선언의 근본: Type 변수 = new Constructor() 구조 분석

자바에서 객체를 생성하는 기본 구조는 Type 변수 = new Constructor()이다. 이 패턴은 자바의 객체 지향적 특성을 잘 보여주는 핵심 문법 요소이다. 각 구성 요소의 의미를 분석해보면 다음과 같다.

Type 변수명 = new Constructor();
  • Type: 변수의 참조 타입을 정의한다. 클래스, 인터페이스, 또는 제네릭 타입이 올 수 있다.
  • 변수명: 메모리에 생성된 객체를 참조하는 이름이다.
  • new: 힙 메모리에 객체를 생성하는 키워드이다.
  • Constructor(): 해당 클래스의 생성자를 호출한다.

여기서 중요한 점은 new의 진정한 의미이다. new는 단순히 메모리에 공간을 할당하는 것이 아니라, 실제로 힙 메모리에 객체를 생성하고 그 참조(주소)를 반환한다. 그리고 이 참조가 변수에 저장되는 것이다.

예를 들어:

String str = new String("Hello");

이 코드에서 실제로 발생하는 일은 다음과 같다:

  1. new String("Hello")이 힙 메모리에 String 객체를 생성한다.
  2. 생성된 객체의 메모리 주소(참조)가 반환된다.
  3. 이 참조값이 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 인터페이스에 정의된 메서드만 사용할 수 있다. 이는 다형성의 또 다른 예시이다.

결론: 자바의 선언은 참조의 관점 정의

자바에서 객체 선언 방식을 이해하는 핵심은 "왼쪽은 관점, 오른쪽은 실체"라는 개념이다. 왼쪽에 선언하는 타입은 그 객체를 어떤 관점에서 볼 것인지 정의하고, 오른쪽의 생성자는 실제로 어떤 객체를 생성할지 결정한다.

이러한 유연성은 코드의 재사용성과 유지보수성을 높이는 핵심 요소이다. 특히 인터페이스를 타입으로 사용하면 구현체를 쉽게 교체할 수 있어 코드의 유연성이 크게 향상된다.

자바의 객체 선언과 생성 방식은 언뜻 복잡해 보일 수 있지만, 그 근본적인 원리를 이해하면 객체 지향 프로그래밍의 강력한 도구가 된다.

반응형