TIL/[TIL]

[TIL]Java API, Wrapper, 오토박싱/오토언박싱, 제네릭

namerong 2026. 5. 8. 15:06

1. 학습 주제

자바에서 자주 사용하는 기본 API와 제네릭의 핵심 개념을 정리했다.
특히 Object, String, StringBuilder, Wrapper, java.time의 역할을 확인했고, Object 기반 코드의 한계를 제네릭이 어떻게 해결하는지도 함께 이해할 수 있었다.

  • Object의 toString(), equals(), hashCode() 이해하기
  • String의 주요 메서드와 불변성 확인하기
  • StringBuilder가 필요한 이유 익히기
  • Wrapper 클래스와 파싱, 문자열 변환 정리하기
  • 오토박싱과 오토언박싱 자세히 이해하기
  • 제네릭의 필요성과 타입 안정성 이해하기
  • 제한된 제네릭과 와일드카드 문법 정리하기

2. Object 클래스

모든 클래스는 Object를 상속받기 때문에, toString(), equals(), hashCode()는 매우 중요하다.

2.1 toString()

System.out.println("book1 =" + book1);
  • 기본 toString()은 클래스명@해시코드 형태를 출력한다.
  • 사람이 보기 좋은 객체 정보 출력을 위해 오버라이딩해서 사용한다.
@Override
public String toString() {
    return "Book{" +
            "number=" + number +
            ", title='" + title + '\'' +
            ", author='" + author + '\'' +
            ", price=" + price +
            '}';
}

2.2 equals()

System.out.println(book2 == book3);
System.out.println(book2.equals(book3));
  • ==는 주소값 비교, 즉 동일성 비교이다.
  • equals()는 내용 비교, 즉 동등성 비교를 위해 오버라이딩한다.

2.3 hashCode()

Set<Book> books = new HashSet<>();
books.add(book2);
System.out.println(books.contains(book3));
  • HashSet, HashMap 같은 해시 기반 컬렉션은 hashCode()와 equals()를 함께 사용한다.
  • equals()가 true라면 hashCode()도 같아야 한다.
  • 그래서 equals()를 오버라이딩했다면 보통 hashCode()도 같이 오버라이딩해야 한다.

3. String

String은 자바에서 가장 많이 쓰는 클래스 중 하나이며, 중요한 특징은 불변성이다.

String rawData = " [PROD-001] Apple iphone 17 Pro | a17 bionic | 2500000 ";

예제에서 사용한 주요 메서드:

  • trim() : 앞뒤 공백 제거
  • toUpperCase() : 대문자 변환
  • indexOf() : 특정 문자 위치 찾기
  • substring() : 문자열 일부 자르기
  • replace() : 특정 문자열 치환
  • split() : 구분자를 기준으로 나누기

3.1 String 비교

String str1 = "java";
String str2 = "java";
String str3 = new String("java");
String str4 = new String("java");
  • 리터럴은 문자열 상수 풀을 재사용할 수 있다.
  • new String()은 새 객체를 만든다.
  • 문자열 값 비교는 항상 .equals()를 사용하는 것이 안전하다.

3.2 String의 불변성

str2 += " mysql";
  • 기존 문자열이 수정되는 것이 아니다.
  • 새로운 String 객체가 만들어진다.
  • 그래서 반복문 안에서 문자열을 계속 더하면 비효율적일 수 있다.

4. StringBuilder

문자열을 반복적으로 수정하거나 이어붙일 때는 StringBuilder가 더 효율적이다.

StringBuilder sb = new StringBuilder();
for(int i=0; i<50000; i++) {
    sb.append(i);
}
  • String은 불변 객체라 수정할 때마다 새 객체가 생긴다.
  • StringBuilder는 같은 객체 내부를 바꾸면서 작업한다.
  • 반복문 안에서 문자열 누적이 많으면 StringBuilder가 훨씬 유리하다.

주요 메서드:

  • append() : 뒤에 붙이기
  • delete() : 일부 삭제
  • insert() : 특정 위치에 삽입
  • reverse() : 순서 뒤집기
  • toString() : 최종적으로 String으로 변환

5. Wrapper 클래스

기본 자료형은 객체가 아니기 때문에, 객체처럼 다뤄야 할 때 Wrapper 클래스를 사용한다.

예:

  • int -> Integer
  • double -> Double
  • char -> Character
  • boolean -> Boolean

예제에서는 문자열 숫자를 실제 숫자로 바꾸는 파싱을 사용했다.

double height = Double.parseDouble(heightStr);
double weight = Double.parseDouble(weightStr);
  • 문자열 "170.5"는 그대로는 계산할 수 없다.
  • parseDouble() 같은 메서드로 기본형 숫자로 바꿔야 한다.

반대로 숫자를 문자열로 바꾸는 것도 가능하다.

String bmiStr1 = String.valueOf(bmi);
String bmiStr2 = bmi + "";

6. 오토박싱과 오토언박싱

이 부분은 정말 중요하다.

6.1 박싱이란?

기본 자료형 값을 Wrapper 객체로 감싸는 것이다.

Integer age = Integer.valueOf(20);

원래는 이렇게 써야 하는데, 자바가 자동으로 바꿔준다.

Integer age = 20;

이것이 오토박싱이다.

  • int 20을 Integer 객체로 자동 변환
  • 개발자가 valueOf()를 직접 쓰지 않아도 됨

6.2 언박싱이란?

Wrapper 객체 안의 값을 다시 기본 자료형으로 꺼내는 것이다.

int unboxedAge = age.intValue();

이것도 자바가 자동으로 처리해준다.

int unboxedAge = age;

이것이 오토언박싱이다.

  • Integer 객체 안의 값을 꺼내서 int로 바꿈
  • 개발자가 intValue()를 직접 쓰지 않아도 됨

6.3 왜 필요한가?

제네릭, 컬렉션, API 대부분은 객체 타입을 다룬다.
그런데 int, double 같은 기본형은 객체가 아니다.

예를 들어:

  • List<int> 는 안 됨
  • List<Integer> 는 가능

즉 객체가 필요한 상황에서 기본형을 편하게 쓸 수 있도록 박싱/언박싱이 지원된다.

6.4 주의할 점

Integer num3 = 128;
Integer num4 = 128;
System.out.println(num3 == num4);
System.out.println(num3.equals(num4));
  • Wrapper 클래스는 객체이므로 ==는 주소 비교가 될 수 있다.
  • 값 비교는 .equals()를 쓰는 것이 안전하다.

한 줄 정리:

  • 오토박싱: 기본형 -> Wrapper 객체
  • 오토언박싱: Wrapper 객체 -> 기본형

7. java.time API

기존 Date보다 더 명확하고 안전한 날짜/시간 API이다.

예제에서 사용한 클래스:

  • LocalDate : 날짜만
  • LocalTime : 시간만
  • LocalDateTime : 날짜와 시간 모두
LocalDate departureDate = LocalDate.of(2026, 12, 25);
LocalTime departureTime = LocalTime.of(13, 8);
LocalDateTime trainDeparture = LocalDateTime.of(departureDate, departureTime);

할 수 있는 작업:

  • 연, 월, 일, 요일 꺼내기
  • minusMinutes() 같은 날짜/시간 계산
  • isBefore()로 비교
  • DateTimeFormatter로 출력 형식 지정
  • 문자열을 날짜 객체로 파싱
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String ticket = trainDeparture.format(formatter);

8. 제네릭이 왜 필요한가

제네릭을 배우기 전에 먼저 Object 기반 코드의 문제를 보는 게 중요하다.

8.1 일반 박스의 문제점

public class NormalBox {
    private Object content;
}

Object는 모든 객체를 담을 수 있다.
그래서 얼핏 보면 유연해 보인다.

normalBox.setContent("안녕하세요");
normalBox.setContent(123);

하지만 문제가 생긴다.

String content = (String) normalBox.getContent();
  • 꺼낼 때마다 강제 형변환이 필요하다
  • 실수로 다른 타입을 넣어도 컴파일 에러가 안 난다
  • 잘못 꺼내면 런타임 에러가 발생한다

즉 Object 기반 방식은 유연하지만 안전하지 않다.


9. 제네릭의 핵심 개념

제네릭은 “타입을 변수처럼 다루는 문법”이다.

public class GenericBox<T> {
    private T content;
}

여기서 T는 타입 파라미터이다.

  • T는 실제 타입이 아니라 타입 자리 표시자
  • 사용할 때 진짜 타입을 넣는다

예:

GenericBox<String> stringBox = new GenericBox<>();
GenericBox<Integer> integerBox = new GenericBox<>();

이렇게 하면:

  • stringBox는 문자열만 담는 박스
  • integerBox는 정수 객체만 담는 박스

9.1 장점

  1. 강제 형변환이 줄어든다
  2. 타입 실수를 컴파일 단계에서 잡을 수 있다
  3. 코드 재사용성이 높아진다
String strContent = stringBox.getContent();
  • 꺼낼 때 형변환이 필요 없다
  • String이 보장되기 때문이다

9.2 왜 더 안전한가

NormalBox는 Object라서 아무거나 들어간다.
GenericBox<String>은 아예 문자열만 받게 제한된다.

즉 제네릭은:

  • “아무거나 넣는 유연함” 대신
  • “정해진 타입만 넣는 안전함”을 준다

10. 제네릭과 기본형

제네릭에는 기본형을 직접 넣을 수 없다.

// GenericBox<int> // 불가능
GenericBox<Integer> integerBox = new GenericBox<>();

왜냐하면 제네릭은 객체 타입을 기준으로 동작하기 때문이다.

그래서 기본형 대신 Wrapper 클래스를 사용한다.

  • int 대신 Integer
  • double 대신 Double

그리고 값을 꺼낼 때는 오토언박싱 덕분에 자연스럽게 기본형처럼 쓸 수 있다.

int intContent = integerBox.getContent();

겉보기에는 Integer를 int에 넣는 것 같지만, 내부적으로 오토언박싱이 일어난다.


11. 제한된 제네릭

제네릭 타입에 아무 타입이나 넣지 못하게 제한할 수도 있다.

public class RabbitFarm<T extends Rabbit> {
    private T animal;
}

이 뜻은:

  • T는 Rabbit 또는 Rabbit의 자식만 가능
  • Snake 같은 전혀 다른 타입은 안 됨

가능한 예:

RabbitFarm<Rabbit> farm1 = new RabbitFarm<>(new Rabbit());
RabbitFarm<Bunny> farm2 = new RabbitFarm<>(new Bunny());
RabbitFarm<DrunkenBunny> farm3 = new RabbitFarm<>(new DrunkenBunny());

불가능한 예:

// RabbitFarm<Snake> rabbitFarm = new RabbitFarm<>();

이런 제한을 두는 이유는 “토끼 농장”에는 토끼 계열만 들어오게 보장하기 위해서다.


12. 와일드카드

와일드카드는 제네릭 타입을 받을 때 범위를 유연하게 표현하는 문법이다.

12.1 <?>

public void manageAnyFarm(RabbitFarm<?> farm)
  • 어떤 타입이든 가능
  • 타입을 정확히 모르지만 받기는 하겠다는 의미

12.2 <? extends Bunny>

public void manageBunnyFarm(RabbitFarm<? extends Bunny> farm)
  • Bunny 또는 Bunny의 자식만 가능
  • Bunny, DrunkenBunny 가능
  • Rabbit은 불가능

의미:

  • “Bunny 계열까지만 받겠다”

12.3 <? super Bunny>

public void manageRabbitOrBunnyFarm(RabbitFarm<? super Bunny> farm)
  • Bunny 또는 Bunny의 부모 타입 가능
  • Bunny, Rabbit 가능

의미:

  • “Bunny를 담을 수 있는 더 넓은 범위까지 받겠다”

13. 제네릭을 이해하는 핵심 감각

제네릭은 문법보다 목적을 먼저 이해하면 편하다.

13.1 제네릭이 없으면

  • Object로 다 담는다
  • 꺼낼 때 형변환한다
  • 실수해도 컴파일러가 잘 못 잡는다

13.2 제네릭이 있으면

  • 애초에 넣을 수 있는 타입을 정한다
  • 꺼낼 때 형변환이 줄어든다
  • 잘못된 타입 사용을 컴파일 단계에서 막는다

즉 제네릭의 본질은:

  • 코드 재사용
  • 타입 안정성
  • 형변환 감소

14. 흐름 정리

14.1 API

  • Object의 toString(), equals(), hashCode() 정리
  • String의 주요 메서드와 불변성 이해
  • 반복 문자열 처리에는 StringBuilder 사용
  • Wrapper 클래스로 파싱, 문자열 변환, 기본형-객체 변환 처리
  • java.time으로 날짜/시간 안전하게 다루기

14.2 제네릭

  • Object 기반 클래스의 한계 확인
  • GenericBox<T>로 타입 안전한 구조 만들기
  • Wrapper 타입과 오토언박싱 연결 이해
  • extends, ?, ? extends, ? super로 범위 제한하기

15. 핵심 정리

  1. Object의 equals()와 hashCode()는 함께 고려해야 한다.
  2. String은 불변 객체라 반복 수정에 비효율적일 수 있다.
  3. 문자열을 많이 붙일 때는 StringBuilder가 유리하다.
  4. Wrapper 클래스는 기본형을 객체처럼 다룰 수 있게 해준다.
  5. 오토박싱은 기본형을 Wrapper 객체로 자동 변환하는 것이다.
  6. 오토언박싱은 Wrapper 객체를 기본형으로 자동 변환하는 것이다.
  7. Wrapper 객체 비교는 ==보다 .equals()가 안전하다.
  8. 제네릭은 타입을 일반화하면서도 타입 안전성을 지켜주는 문법이다.
  9. 제네릭이 없으면 Object와 형변환에 의존하게 되어 런타임 오류 위험이 커진다.
  10. T extends Rabbit은 타입 파라미터를 특정 상속 범위로 제한하는 문법이다.
  11. ? extends는 상한 제한, ? super는 하한 제한이라고 이해하면 정리하기 쉽다.