문서 읽는 데 57분 · day17

Day 17 — String과 래퍼 클래스

전체 21강 중 17강 · 자바 기초
난이도 · 입문

지난 시간, 우리는 Phase 2의 마지막 봉우리를 넘었어요. 인스타그램 도메인 모델을 한 장에 펼치고, 회원과 글과 댓글과 팔로우가 서로를 가리키며 맞물려 도는 구조를 직접 설계했죠. 클래스 하나도 어색했던 게 엊그제 같은데, 이제 객체들이 서로 손잡는 그림까지 그리시잖아요. 정말 큰 산을 넘으신 거예요.

그런데 그 마지막 코드에서 슬쩍 지나간 게 하나 있었어요. isFollowing 을 만들 때, 두 회원이 같은 사람인지 비교하면서 equals 를 썼던 거 기억나세요? 작성자 이름을 비교할 때도 마찬가지였고요. 그때 "왜 == 가 아니라 equals 를 쓰지?" 하는 의문이 살짝 스쳤다면 — 오늘이 바로 그 답을 푸는 시간이에요.

오늘부터는 Phase 3예요. 클래스를 직접 설계하던 단계에서 한 걸음 나아가, 자바가 기본으로 쥐여주는 강력한 도구들을 손에 익히기 시작해요. 그 첫 주인공이 바로 우리가 매일 쓰면서도 제대로 들여다본 적 없는 문자열, String 이에요. 그리고 숫자를 객체처럼 다루게 해주는 래퍼(wrapper, 포장) 클래스까지 함께 봐요.

매일 쓰면서도 몰랐던 문자열의 속을 열어보면, 그동안 "그냥 되니까 썼던" 코드들이 "아, 그래서 이렇게 동작했구나" 로 바뀔 거예요. 자, Day 17 시작해봐요!

🎯 학습 목표

  • 문자열을 비교할 때 ==equals() 가 무엇을 비교하는지 정확히 구분하고, 언제 무엇을 써야 하는지 판단할 수 있어요.
  • String 이 "불변(immutable)" 이라는 게 무슨 뜻인지, 왜 그렇게 만들어졌는지 설명할 수 있어요.
  • 문자열을 여러 번 이어붙일 때 StringBuilder 를 쓰는 이유를 이해하고 직접 쓸 수 있어요.
  • 원시 자료형을 객체로 감싸는 래퍼 클래스(Integer·Long·Double·Boolean)의 개념과 용도를 설명할 수 있어요.
  • 오토박싱·언박싱이 자동으로 일어나는 상황을 알아보고, 그 함정(특히 null 언박싱)을 피할 수 있어요.
  • String.format()formatted() 로 숫자와 글자를 깔끔한 문자열로 찍어낼 수 있어요.

Step 1. == vs equals() — 문자열 비교의 비밀

지난 시간 마지막에 남겨둔 그 질문부터 풀어볼게요. "문자열은 왜 == 로 비교하면 안 되고 equals 를 써야 하지?"

먼저 비유로 감을 잡아봐요. 똑같은 라면 한 봉지가 든 택배 상자가 두 개 있다고 해봐요. 안에 든 라면은 완전히 똑같아요. 그런데 상자 자체는 분명히 서로 다른 두 개죠.

        상자 A                    상자 B
   ┌───────────────┐        ┌───────────────┐
   │   "신라면"     │        │   "신라면"     │
   └───────────────┘        └───────────────┘
      주소: 0x100              주소: 0x200

   ==      → "같은 상자냐?"     → A(0x100) 와 B(0x200) 는 다른 상자 → false
   equals  → "내용물이 같냐?"   → 둘 다 "신라면"               → true

== 는 "이 둘이 같은 상자(같은 객체) 냐?" 를 물어요. 상자에 붙은 주소를 비교하는 거예요. 반면 equals 는 "상자 안의 내용 이 같냐?" 를 물어요. 우리가 알고 싶은 건 보통 "글자가 같냐?" 니까, 문자열 비교엔 equals 가 맞는 거죠.

코드로 직접 확인해봐요.

// com/instagram/javabasic/stringbasic/StringComparison.java
public class StringComparison {

    public static void main(String[] args) {
        String literal1 = "instagram";
        String literal2 = "instagram";
        String made1 = new String("instagram");
        String made2 = new String("instagram");

        System.out.println("리터럴끼리 == : " + (literal1 == literal2));        // true
        System.out.println("리터럴끼리 equals : " + literal1.equals(literal2)); // true
        System.out.println("new 끼리 == : " + (made1 == made2));               // false
        System.out.println("new 끼리 equals : " + made1.equals(made2));        // true
    }
}

실행하면 new 로 만든 두 문자열은 ==false, equalstrue 가 나와요. 글자는 똑같은데 상자(객체)는 둘이라서 그래요.

그런데 위에서 이상한 게 보이죠? 따옴표로 직접 쓴 "instagram" 두 개(literal1, literal2)는 ==true 가 나와요. 왜 그럴까요?

따옴표로 쓴 문자열은 "문자열 풀" 에 모여요

자바는 따옴표로 직접 쓴 문자열(이걸 문자열 리터럴 이라고 해요)을 한곳에 모아 보관해요. 이 보관소를 문자열 풀(String Pool) 이라고 불러요. 같은 글자의 리터럴은 이 풀에서 같은 상자 하나 를 함께 가리켜요. 그래서 ==true 가 되는 거죠.

   "instagram"  "instagram"      ← 코드에 두 번 썼지만
        │            │
        └─────┬──────┘
              ▼
     ┌─────────────────┐
     │   "instagram"   │  ← 문자열 풀에 단 하나만 존재
     └─────────────────┘    둘 다 이 상자를 가리킴 → == true

반대로 new String("instagram") 은 "풀에 있든 말든, 새 상자를 하나 더 만들어라" 라는 명령이에요. 그래서 매번 새 객체가 생기고, ==false 가 되죠.

여기서 헷갈리지 않는 비결이 있어요. "리터럴이라 == 가 우연히 true 가 되는 경우" 에 기대지 말고, 문자열 내용 비교는 무조건 equals 를 쓰면 돼요. 그러면 상자가 하나든 둘이든 항상 내용 기준으로 정확하게 비교돼요.

// 내용을 비교하는 두 메서드 — equals 쪽이 항상 안전해요.
public static boolean compareByReference(String a, String b) {
    return a == b;          // 같은 상자냐? (주소 비교)
}

public static boolean compareByContent(String a, String b) {
    return a.equals(b);     // 내용이 같냐? (글자 비교)
}

💡 튜터의 결론

== 는 "같은 객체(상자)냐", equals 는 "내용이 같냐" 를 물어요. 문자열의 글자가 같은지 확인하고 싶다면 항상 equals 예요. 지난 시간 isFollowing 에서 equals 를 썼던 게 바로 이 이유였어요. 만약 == 로 비교했다면, 같은 이름이어도 다른 상자라서 "다른 사람" 으로 잘못 판단했을 거예요.

이제 ==equals 의 차이는 풀렸어요. 그런데 한 가지 더 궁금한 게 생겨요. 문자열은 왜 이렇게 "상자" 단위로 까다롭게 굴까요? 다음 Step에서 문자열의 또 다른 성질, 불변 을 보면 그 답이 보여요.


Step 2. String이 불변이라는 것의 의미

Day 14에서 "불변(immutable)" 이라는 말을 만난 적 있죠. 한 번 만들면 내용을 바꿀 수 없는 객체요. 사실 String 이 바로 그 불변의 대표 주자예요. 한 번 만든 문자열은 절대 바뀌지 않아요.

"네? 저는 맨날 문자열을 바꿨는데요?" 하실 거예요. name = name + "_dev" 같은 코드, 많이 쓰셨죠. 그런데 이건 사실 기존 문자열을 바꾸는 게 아니라, 새 문자열을 만들어서 갈아 끼우는 거예요.

// com/instagram/javabasic/stringbasic/StringImmutability.java
public class StringImmutability {

    public static void main(String[] args) {
        String name = "jaehoon";
        int beforeId = System.identityHashCode(name);

        String greeting = name + "_dev";       // 새 문자열이 생겨요
        int afterId = System.identityHashCode(greeting);

        System.out.println("원본 : " + name);                       // jaehoon (그대로)
        System.out.println("이어붙인 결과 : " + greeting);           // jaehoon_dev
        System.out.println("원본 == 결과 : " + (name == greeting));  // false
        System.out.println("원본 상자 번호 : " + beforeId);
        System.out.println("새 상자 번호 : " + afterId);
    }
}

System.identityHashCode(...) 는 객체마다 부여되는 고유 번호예요. "이 상자가 그 상자가 맞나?" 를 눈으로 확인하는 용도죠. 실행해보면 name 의 상자 번호와 greeting 의 상자 번호가 서로 달라요. name + "_dev" 를 하는 순간 완전히 새로운 상자 가 만들어졌다는 뜻이에요. 원본 name 은 여전히 "jaehoon" 그대로고요.

그림으로 보면 이래요.

   String name = "jaehoon";
        │
        ▼
   ┌───────────┐
   │ "jaehoon" │  ← 원본 (안 바뀜, 그대로 남음)
   └───────────┘

   String greeting = name + "_dev";
        │
        ▼
   ┌─────────────────┐
   │ "jaehoon_dev"   │  ← 새 상자 (새 주소)
   └─────────────────┘

그럼 계속 더하면 어떻게 될까요?

문자열을 반복해서 이어붙이면, 더할 때마다 새 상자가 생기고 이전 상자는 버려져요.

   s = "a"            →  ["a"]
   s = s + "b"        →  ["ab"]        ("a" 는 버려짐)
   s = s + "c"        →  ["abc"]       ("ab" 는 버려짐)
   s = s + "d"        →  ["abcd"]      ("abc" 는 버려짐)
                            ▲
              매번 새 상자, 버려진 상자가 계속 쌓임

짧게 몇 번이면 문제없어요. 그런데 만약 게시물 1만 개의 해시태그를 + 로 계속 이어붙인다면? 버려지는 상자가 산더미처럼 쌓여서 낭비가 커져요. 이 문제의 해결사가 다음 Step의 StringBuilder 예요.

왜 굳이 불변으로 만들었을까요?

"바꿀 수 있으면 편할 텐데 왜 굳이 못 바꾸게 했지?" 싶을 거예요. 이유가 있어요. 문자열은 프로그램 곳곳에서 공유 돼요. Step 1에서 본 문자열 풀처럼, 같은 글자를 여러 곳이 함께 가리키죠. 만약 한 곳에서 내용을 바꿔버리면, 그걸 같이 쓰던 다른 모든 곳이 영문도 모르고 영향을 받아요. 불변으로 잠가두면 "내가 가진 문자열은 누가 건드릴 일이 없다" 는 안전이 보장돼요. 그래서 자바는 String 을 아예 못 바꾸게 설계한 거예요.

💡 튜터의 결론

String 은 한 번 만들면 안 바뀌어요(불변). s = s + "x" 는 기존 문자열을 고치는 게 아니라 새 문자열을 만들어 갈아 끼우는 거예요. 그래서 많이 이어붙이면 버려지는 객체가 쌓여 낭비가 돼요. 불변으로 만든 건 여러 곳이 안심하고 공유하게 하려는 안전장치예요.

그럼 문자열을 많이 이어붙여야 할 땐 어떡하죠? 매번 새 상자를 만들지 않고, 한 상자 안에서 글자를 채워가는 도구가 있어요. 바로 다음에 만날 StringBuilder 예요.


Step 3. StringBuilder로 문자열 효율적으로 쌓기

Step 2에서 본 문제를 다시 떠올려봐요. String 으로 글자를 계속 이어붙이면 매번 새 상자가 생겨요. 이걸 비유하면, 칠판에 문장을 쓰는데 한 글자 더할 때마다 새 칠판을 가져와 처음부터 다시 베껴 쓰는 꼴이에요. 글자가 길어질수록 점점 더 힘들어지죠.

StringBuilder 는 다르게 일해요. 칠판 하나를 두고, 거기에 글자를 계속 이어서 쓰는 방식이에요. 새 칠판을 가져오지 않으니 훨씬 효율적이죠. 다 쓴 다음 toString() 으로 "지금까지 쓴 내용" 을 진짜 문자열로 꺼내요.

// com/instagram/javabasic/stringbasic/StringBuilderBasics.java
// 해시태그 목록을 한 줄 문자열로 누적해 만들어요.
public static String joinTags(String[] tags) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < tags.length; i++) {
        sb.append("#").append(tags[i]);
        if (i < tags.length - 1) {
            sb.append(" ");
        }
    }
    return sb.toString();
}

append(...) 가 핵심이에요. "이 글자를 칠판 끝에 이어 써라" 는 뜻이에요. for 루프 안에서 해시태그를 하나씩 append 로 쌓고, 마지막에 toString() 으로 완성된 한 줄을 꺼내요. {"daily", "instagram", "java"} 를 넣으면 #daily #instagram #java 가 나와요.

append 가 점(.)으로 연달아 붙는 게 보이죠? sb.append("#").append(tags[i]) 처럼요. append 는 자기 자신(같은 칠판)을 다시 돌려주기 때문에, 점을 찍어 계속 이어 쓸 수 있어요. 이걸 메서드 체인(method chain) 이라고 불러요.

append 말고도 쓸 만한 도구들

StringBuilder 에는 글자를 다루는 도구가 더 있어요.

// insert: 원하는 위치에 글자를 끼워 넣어요 (0번 = 맨 앞)
public static String wrapWithBrackets(String inner) {
    StringBuilder sb = new StringBuilder(inner);
    sb.insert(0, "[");
    sb.append("]");
    return sb.toString();        // "post" → "[post]"
}

// delete: 시작 위치부터 끝 위치 직전까지 잘라내요
public static String deleteRange(String text, int start, int end) {
    StringBuilder sb = new StringBuilder(text);
    sb.delete(start, end);
    return sb.toString();        // "instagram", 0, 5 → "gram"
}

// reverse: 글자 순서를 통째로 뒤집어요
public static String reverse(String text) {
    return new StringBuilder(text).reverse().toString();   // "level" → "level"
}

insert 는 원하는 자리에 글자를 끼워 넣고, delete 는 구간을 잘라내고, reverse 는 순서를 뒤집어요. 모두 "한 칠판 안에서" 작업하니 효율적이에요.

🙋 학생 질문

"튜터님, 그럼 문자열은 무조건 StringBuilder 로 다뤄야 하나요?"

아니에요. 짧게 한두 번 이어붙이는 정도(name + "님 안녕하세요")면 그냥 + 가 더 읽기 편하고 충분해요. StringBuilder 가 빛나는 건 루프 안에서 수십·수백 번 이어붙일 때 예요. "반복문 안에서 문자열을 계속 더하고 있다" 싶으면 그때 StringBuilder 를 떠올리면 돼요.

참고로 StringBuilder 에게는 StringBuffer 라는 형제가 하나 있어요. 하는 일은 거의 똑같은데, 여러 작업이 동시에 끼어드는 특수한 상황에서 안전하도록 만들어진 버전이에요. 그런 상황은 한참 뒤에 배우니, 지금은 "평소엔 StringBuilder 면 충분하다" 만 기억해두면 돼요.

💡 튜터의 결론

문자열을 반복문 안에서 여러 번 이어붙일 땐 StringBuilder 가 효율적이에요. append 로 한 칠판에 글자를 쌓고, toString() 으로 꺼내요. insert·delete·reverse 같은 편의 도구도 있고요. 짧은 연결은 그냥 +, 반복적인 연결은 StringBuilder — 이 기준만 잡으면 돼요.

문자열은 이제 충분히 다뤘어요. 이제 시선을 살짝 옮겨볼게요. 우리가 늘 쓰던 숫자 int 를, 객체처럼 다뤄야 하는 순간이 와요. 그때 등장하는 게 래퍼 클래스예요.


Step 4. 래퍼 클래스 — 원시형을 객체처럼 다루기

지금까지 숫자를 담을 때 int, long, double 같은 걸 썼죠. 이런 타입을 원시 자료형(primitive) 이라고 불러요. 값만 담는 가볍고 빠른 타입이에요.

그런데 가끔 이 숫자를 "객체" 로 다뤄야 할 때 가 있어요. 대표적인 게 다음 시간에 배울 컬렉션이에요. 살짝만 귀띔하면, 여러 데이터를 모아 담는 그 도구는 원시형을 직접 못 담아요. 객체만 담을 수 있죠. 그럼 숫자를 어떻게 담을까요?

그래서 자바는 원시형을 객체로 감싸주는 포장(wrapper) 클래스 를 준비해뒀어요. 원시형마다 짝꿍 포장 클래스가 하나씩 있어요.

   원시형(값만)          포장 클래스(객체)
   ─────────           ──────────────
   int        ──감싸기──▶  Integer
   long       ──감싸기──▶  Long
   double     ──감싸기──▶  Double
   boolean    ──감싸기──▶  Boolean

   "맨 알맹이 사탕"  vs  "포장지로 싼 사탕(객체로 다룰 수 있음)"

이름 규칙이 친절해요. intInteger 로 풀어 쓰고(약자가 아니라서), 나머지는 첫 글자만 대문자예요(longLong, doubleDouble, booleanBoolean).

포장 클래스는 단순히 감싸기만 하는 게 아니라, 숫자를 다루는 편리한 도구도 같이 줘요.

// com/instagram/javabasic/stringbasic/WrapperClassBasics.java
public class WrapperClassBasics {

    // 문자열 "123" 을 진짜 숫자 int 123 으로 바꿔요 (parse = 해석하다)
    public static int parseToInt(String text) {
        return Integer.parseInt(text);
    }

    // valueOf 는 Integer "객체" 를 돌려줘요 (parseInt 는 원시형 int 를 돌려줬어요)
    public static Integer toIntegerObject(String text) {
        return Integer.valueOf(text);
    }

    // Integer 객체 안에 든 진짜 int 값을 꺼내요
    public static int extractInt(Integer boxed) {
        return boxed.intValue();
    }

    // int 가 담을 수 있는 가장 큰 값 (상수로 미리 정해져 있어요)
    public static int maxIntValue() {
        return Integer.MAX_VALUE;       // 2147483647
    }
}

Integer.parseInt("1240") 이 특히 자주 써요. 화면에서 입력받은 값은 보통 문자열 "1240" 인데, 이걸 계산에 쓰려면 진짜 숫자 1240 으로 바꿔야 하잖아요. 그 변환을 parseInt 가 해줘요. 반대로 Integer.toString(42) 는 숫자를 문자열 "42" 로 바꿔주고요.

parseIntvalueOf 의 미묘한 차이도 짚고 가요. parseInt원시형 int 를 돌려주고, valueOfInteger 객체 를 돌려줘요. 지금은 "둘 다 문자열을 숫자로 바꾼다, 다만 결과 타입이 다르다" 정도만 알아두면 충분해요.

long·double·boolean 도 똑같은 방식이에요. Double.parseDouble("3.14"), Boolean.parseBoolean("true") 처럼요.

💡 튜터의 결론

원시형(int·long·double·boolean)을 객체로 감싼 게 래퍼(포장) 클래스(Integer·Long·Double·Boolean)예요. 컬렉션처럼 객체만 담는 곳에 숫자를 넣으려면 이 포장이 필요해요. Integer.parseInt(문자열) 로 문자열을 숫자로, Integer.toString(숫자) 로 숫자를 문자열로 바꿀 수 있고요.

그런데 매번 Integer.valueOf 로 감싸고 intValue() 로 꺼내는 게 번거롭지 않나요? 다행히 자바가 이 변환을 거의 자동으로 해줘요. 그 자동 변환을 다음 Step에서 봐요.


Step 5. 오토박싱·언박싱 — 자동 변환의 편리함과 함정

Step 4에서 intInteger 로 감싸고, Integer 에서 int 를 꺼내는 걸 봤어요. 그런데 실제로 코드를 짜보면, 이걸 일일이 손으로 변환하지 않아도 돼요. 자바가 알아서 해주거든요.

// com/instagram/javabasic/stringbasic/AutoboxingUnboxing.java
// int 값을 그냥 Integer 변수에 넣으면 자바가 알아서 포장해요 (오토박싱)
public static Integer box(int value) {
    return value;
}

// Integer 를 int 변수에 넣으면 자바가 알아서 값을 꺼내요 (언박싱)
public static int unbox(Integer boxed) {
    return boxed;
}

box 를 보세요. 돌려줘야 할 타입은 Integer(객체)인데, 원시형 int value 를 그냥 return 해요. 그래도 에러가 안 나요. 자바가 "아, intInteger 로 감싸야 하는구나" 하고 자동으로 포장 해주거든요. 이걸 오토박싱(autoboxing) 이라고 해요(box = 상자에 넣다).

unbox 는 그 반대예요. Integer(객체)를 받아서 원시형 int 로 돌려주는데, 역시 그냥 return boxed 면 돼요. 자바가 알아서 상자에서 값을 꺼내줘요. 이걸 언박싱(unboxing) 이라고 해요.

   int 10  ──(오토박싱: 자바가 자동으로 감쌈)──▶  Integer(10)

   Integer(10)  ──(언박싱: 자바가 자동으로 꺼냄)──▶  int 10

편하죠? 그런데 이 자동 변환에는 조심해야 할 함정 이 두 개 있어요.

함정 1 — 비어 있는(null) 상자를 꺼내면 사고가 나요

Integer 는 객체라서 "비어 있음" 을 뜻하는 null 이 될 수 있어요. 그런데 비어 있는 Integerint 로 꺼내려고 하면?

// 비어 있는(null) Integer 를 int 로 꺼내려 하면 NullPointerException 이 나요.
public static int unboxNull() {
    Integer empty = null;
    return empty;     // 여기서 언박싱하다가 NullPointerException 발생
}

null 은 "상자가 아예 없다" 는 뜻인데, "그 상자 안의 값을 꺼내라" 고 하니 자바가 멈춰버려요. 이때 나는 에러가 그 유명한 NullPointerException(줄여서 NPE)이에요. 원시형 int 는 절대 null 이 될 수 없지만, 포장된 Integernull 이 될 수 있다는 차이가 여기서 사고로 이어져요.

⚠️ 주의

Integer·Long 같은 포장 객체가 null 일 수 있는 상황에서 원시형으로 꺼내 쓰면 NullPointerException 이 터질 수 있어요. "이 객체가 비어 있을 가능성은 없나?" 를 한 번 확인하는 습관이 NPE 를 막아줘요.

함정 2 — 작은 Integer 는 == 가 우연히 true 가 돼요

Step 1에서 "객체는 equals 로 비교해야 한다" 고 했죠. Integer 도 객체라서 똑같아요. 그런데 여기에 헷갈리는 함정이 하나 숨어 있어요.

// -128 ~ 127 범위의 작은 Integer 는 자바가 미리 만들어 재사용해요.
// 그래서 같은 작은 값은 == 도 true 가 돼요 (같은 상자를 가리키니까요).
public static boolean sameCachedObject(int value) {
    Integer a = value;
    Integer b = value;
    return a == b;
}

sameCachedObject(127)true, sameCachedObject(200)false 가 나와요. 똑같은 코드인데 값에 따라 결과가 달라지다니 이상하죠?

이유는 자바가 자주 쓰는 작은 숫자(-128~127)는 미리 만들어두고 재사용 하기 때문이에요. 그래서 127 두 개는 같은 상자를 가리켜 ==true, 범위를 벗어난 200 두 개는 각각 새 상자라 ==false 가 돼요. 이건 Step 1의 문자열 풀과 똑같은 원리예요.

여기서 교훈은 하나예요. 숫자든 문자열이든, 객체(Integer·String)의 값을 비교할 땐 == 가 아니라 equals 를 쓰면 이런 함정에 안 빠져요.

💡 튜터의 결론

오토박싱은 intInteger 자동 포장, 언박싱은 Integerint 자동 꺼내기예요. 편하지만 두 함정을 조심해요. (1) null 인 포장 객체를 언박싱하면 NullPointerException 이 나요. (2) 작은 Integer(-128~127)는 재사용돼서 == 가 우연히 true 가 돼요. 그래서 포장 객체 값 비교도 equals 가 안전해요.

지금까지 문자열과 숫자(포장 클래스)를 다뤘어요. 마지막으로, 이 둘을 합쳐 화면에 깔끔하게 보여주는 도구를 봐요. 인스타 프로필의 "팔로워 1,240명" 같은 문장을 만들 때 쓰는 서식 도구예요.


Step 6. String.format과 formatted로 문자열 꾸미기

화면에 정보를 보여줄 때, 숫자와 글자를 섞어 한 문장으로 만들 일이 많아요. 예를 들어 @jaehoon_dev · 팔로워 1240명 같은 프로필 한 줄이요.

지금까지는 + 로 이어붙였죠. "@" + username + " · 팔로워 " + followers + "명" 처럼요. 틀리진 않지만, 항목이 많아지면 따옴표와 + 가 뒤엉켜 읽기 힘들어져요. 이럴 때 쓰는 게 서식(format) 도구예요.

먼저 "틀" 을 만들고, 빈칸에 값을 채워 넣는 방식이에요.

// com/instagram/javabasic/stringbasic/StringFormatting.java
// 회원 프로필 한 줄을 서식에 맞춰 만들어요.
// 이름은 %s, 팔로워 수는 %d 로 채워요.
public static String profileLine(String username, int followers) {
    return String.format("@%s · 팔로워 %d명", username, followers);
}

String.format 의 첫 번째 값이 "틀" 이에요. 여기서 %s, %d 같은 게 빈칸 이에요. 뒤에 넘긴 값들(username, followers)이 이 빈칸에 순서대로 들어가요. %susername, %dfollowers 가 채워지는 거죠.

빈칸 기호(서식 지정자)는 종류별로 정해져 있어요.

빈칸 무엇을 채우나 예시
%s 문자열(string) "jaehoon"
%d 정수(decimal) 1240
%f 실수(float) 8.567
%x 16진수 255ff

%f 는 소수점 자릿수를 조절할 수 있어요. %.2f 라고 쓰면 소수점 아래 둘째 자리까지만 보여줘요. 참여율 같은 값을 깔끔하게 보여줄 때 좋아요.

// 소수점 둘째 자리까지 — 참여율 같은 값을 깔끔하게 보여줄 때 써요.
public static String engagementRate(double rate) {
    return String.format("참여율 %.2f%%", rate);    // 8.567 → "참여율 8.57%"
}

여기서 %% 가 보이죠? % 기호 자체를 글자로 찍고 싶을 땐 %% 라고 두 번 써요. % 하나는 "빈칸 시작" 신호라서, 진짜 퍼센트 기호를 쓰려면 구분해줘야 하거든요.

formatted — 점 찍어 부르는 또 다른 방법

같은 일을 하는 다른 방법도 있어요. 문자열에 직접 점을 찍어 formatted(...) 를 부르는 방식이에요.

// formatted 는 format 과 같은 일을 해요. 틀 문자열에 점을 찍어 부르는 방식이에요.
public static String profileLineFormatted(String username, int followers) {
    return "@%s · 팔로워 %d명".formatted(username, followers);
}

String.format("틀", 값들)"틀".formatted(값들) 은 결과가 똑같아요. 틀 문자열이 주인공일 때는 formatted 쪽이 좀 더 자연스럽게 읽혀요. 이 formatted 는 비교적 최근에 추가된 기능이라, 요즘 코드에서 점점 자주 보여요.

💡 튜터의 결론

String.format("틀", 값들) 은 틀의 빈칸(%s·%d·%.2f·%x)에 값을 채워 깔끔한 문자열을 만들어요. % 기호 자체는 %% 로 쓰고요. 같은 일을 "틀".formatted(값들) 로도 할 수 있어요. + 로 길게 이어붙이는 것보다 읽기 좋고 자릿수 조절도 쉬워요.

이제 오늘 배운 세 가지 도구 — 문자열 비교·조립, 포장 클래스, 서식 — 가 다 모였어요. 마무리에서 이 셋을 한자리에 합쳐, 작은 종합 예제를 만들어보며 정리할게요.


마무리

오늘 배운 도구를 한자리에 모아, 인스타 프로필 한 줄 요약을 만들어볼게요. 세 가지가 한 메서드 안에서 어떻게 맞물리는지 보세요.

// com/instagram/javabasic/stringbasic/StringAndWrapperSummary.java
// 세 도구를 모두 묶어 프로필 한 줄 요약을 완성해요.
public static String summarize(String username, String followersRaw, String[] tags) {
    int followers = parseFollowers(followersRaw);   // 1) 포장 클래스: 문자열 → 숫자
    String tagLine = buildTagLine(tags);            // 2) StringBuilder: 해시태그 조립
    return "@%s · 팔로워 %d명 · %s".formatted(username, followers, tagLine);  // 3) 서식
}

흐름을 한 단계씩 따라가봐요.

  1. 팔로워 수가 문자열 "1240" 으로 들어와요. 계산이나 비교에 쓰려면 진짜 숫자가 필요하니, Integer.parseIntint 로 바꿔요. (Step 4 — 포장 클래스)
  2. 해시태그 배열을 StringBuilder 로 한 줄(#daily #coding #instagram)로 조립해요. (Step 3 — StringBuilder)
  3. 마지막으로 formatted 로 이름·팔로워 수·해시태그를 깔끔한 한 줄로 찍어내요. (Step 6 — 서식)

실행하면 이렇게 나와요.

@jaehoon_dev · 팔로워 1240명 · #daily #coding #instagram

오늘 배운 세 도구가 한 줄 안에 모두 들어 있죠. 이게 바로 실무에서 화면에 보여줄 문장을 만드는 전형적인 흐름이에요.

오늘 배운 것 — 언제 무엇을 쓸까

도구 언제 쓰나 핵심
equals() 문자열·객체의 내용 을 비교할 때 == 는 같은 상자냐, equals 는 내용이 같냐
StringBuilder 문자열을 반복해서 이어붙일 때 append 로 한 칠판에 쌓고 toString() 으로 꺼냄
래퍼 클래스 숫자를 객체로 다뤄야 할 때 Integer.parseInt 등. 컬렉션에 담으려면 필요
String.format·formatted 숫자·글자를 깔끔한 문장 으로 만들 때 틀의 빈칸(%s·%d·%.2f)에 값 채우기

오늘 배운 것 압축 요약

  • Step 1: == 는 같은 객체냐, equals 는 내용이 같냐를 비교해요. 문자열 내용 비교는 항상 equals.
  • Step 2: String 은 불변이라 s + "x" 마다 새 객체가 생겨요. 여러 곳이 안심하고 공유하게 한 안전장치예요.
  • Step 3: 반복해서 이어붙일 땐 StringBuilderappend 가 효율적이에요.
  • Step 4: 원시형을 객체로 감싼 게 래퍼 클래스(Integer 등). 문자열↔숫자 변환 도구도 줘요.
  • Step 5: 오토박싱·언박싱은 자동 변환이에요. null 언박싱(NPE)과 작은 Integer== 함정을 조심해요.
  • Step 6: String.format·formatted 로 빈칸에 값을 채워 깔끔한 문장을 만들어요.

다음 시간엔 — 드디어 배열을 졸업해요

오늘까지, 그리고 Phase 2 내내 우리를 따라다닌 불편함이 하나 있었어요. 바로 배열 이에요. 회원의 글 목록도, 팔로잉 목록도 전부 배열 + 카운터(Member[] followingfollowingCount)로 담았잖아요. 그런데 이 방식엔 답답한 점이 많았죠.

  • 배열은 만들 때 크기를 미리 정해야 해요. new Member[100] 처럼요. 그런데 팔로잉이 100명을 넘으면? 더는 못 담아요.
  • 지금 몇 개가 들었는지 카운터를 따로 들고 다녀야 했어요. 깜빡하면 버그가 나고요.
  • 중간 항목을 빼면 빈자리를 직접 메워야 하는 번거로움도 있었어요.

다음 시간엔 이 모든 답답함을 한 번에 풀어주는 도구를 배워요. 크기를 미리 정할 필요 없이 알아서 늘어나는 그릇, 카운터 없이 "지금 몇 개" 를 스스로 아는 그릇 — 바로 컬렉션(Collection), 그중에서도 ArrayList 예요. 그리고 오늘 배운 래퍼 클래스가 왜 필요했는지(컬렉션은 원시형을 못 담는다던 그 말!)도 그때 정확히 확인하게 될 거예요.

배열 + 카운터로 끙끙대던 코드가 얼마나 가벼워지는지, 다음 시간에 직접 느껴봐요. 오늘도 정말 수고 많으셨어요!


과제

오늘 배운 문자열 도구와 래퍼 클래스는 머리로만 이해하면 금방 흐려져요. 직접 손으로 짜봐야 진짜 내 것이 돼요. 세 가지 과제를 준비했어요. 모두 오늘까지 배운 문법(클래스·메서드·배열·for·if·String API·StringBuilder·래퍼 클래스·format/formatted)만으로 풀 수 있어요. 컬렉션(List·Map)이나 람다는 아직 안 배웠으니, 여러 개를 담을 땐 배열 로 표현해주세요.

[기초] 과제 1 — 프로필 카드 한 줄 만들기

해야 할 일

회원의 정보를 받아 프로필 카드 한 줄 문자열을 만드는 메서드를 작성해보세요.

상황

인스타 프로필 상단에 @jaehoon_dev · 게시물 42 · 팔로워 1240 같은 한 줄 요약이 뜨죠. 그런데 화면에서 넘어오는 값은 숫자가 아니라 문자열인 경우가 많아요. 게시물 수와 팔로워 수가 "42", "1240" 처럼 문자열로 들어온다고 가정하고, 이걸 진짜 숫자로 바꿔 깔끔한 한 줄로 만들어보세요.

요구사항

  • ProfileCard 클래스를 만들고, public static String render(String username, String postsRaw, String followersRaw) 메서드를 작성하세요.
  • postsRawfollowersRaw 는 문자열로 들어와요. 래퍼 클래스로 진짜 정수(int)로 바꾸세요.
  • String.format 또는 formatted@{username} · 게시물 {posts} · 팔로워 {followers} 형태의 한 줄을 만들어 돌려주세요.
  • main 에서 render("jaehoon_dev", "42", "1240") 을 호출해 결과를 출력해보세요.

힌트

  • 문자열을 숫자로 바꾸는 건 Step 4의 Integer.parseInt(...) 예요.
  • 서식 틀은 Step 6의 "@%s · 게시물 %d · 팔로워 %d".formatted(...) 패턴 그대로예요.

[응용] 과제 2 — 댓글 멘션(@) 영수증 만들기

해야 할 일

댓글 글쓴이들의 이름 배열을 받아, 멘션 한 줄과 통계를 함께 출력하는 메서드를 작성해보세요.

상황

게시물에 댓글을 단 사람들에게 한꺼번에 멘션을 날리는 기능을 떠올려봐요. ["minji", "seungwoo", "jaehoon"] 같은 이름 배열을 받으면, @minji @seungwoo @jaehoon 처럼 멘션 한 줄을 만들고, 맨 끝에 "총 3명에게 멘션" 같은 통계도 붙이는 거예요.

요구사항

  • MentionReceipt 클래스를 만들고, public static String build(String[] usernames) 메서드를 작성하세요.
  • StringBuilder@이름 들을 공백으로 이어 한 줄로 조립하세요(Step 3의 joinTags 패턴 응용).
  • 마지막 줄에 String.format 또는 formatted총 N명에게 멘션 통계를 덧붙이세요. (StringBuilder 에 줄바꿈 \nappend 해서 이어붙여도 좋아요.)
  • main 에서 이름 배열로 호출해 결과를 출력해보세요.

힌트

  • 멘션 한 줄 조립은 Step 3의 joinTags 와 거의 같아요. # 대신 @ 를 붙이면 돼요.
  • 인원수는 배열의 length 예요. 이 정수를 %d 빈칸에 채우면 돼요.

[심화] 과제 3 — == 함정 디버깅 리포트

해야 할 일

== 로 문자열·Integer 를 비교하는 잘못된 코드를 직접 만들어 실행하고, 왜 그런 결과가 나오는지 설명까지 출력하는 작은 진단 프로그램을 작성해보세요.

상황

후배 개발자가 "분명 같은 값인데 == 로 비교하니 어떨 땐 true, 어떨 땐 false 가 나와요. 귀신이 곡할 노릇이에요!" 하고 물어왔어요. 오늘 배운 내용으로 그 현상을 재현하고 원인을 정리해주는 거예요.

요구사항

  • EqualityReport 클래스를 만들고 main 에서 아래 네 가지를 출력하세요.
    • 문자열 리터럴 두 개의 == 결과 (Step 1)
    • new String(...) 두 개의 == 결과 와 equals 결과 (Step 1)
    • Integer 127 두 개의 == 결과 (Step 5 — 캐시 범위 안)
    • Integer 200 두 개의 == 결과 (Step 5 — 캐시 범위 밖)
  • 각 결과 옆에 한 줄짜리 이유를 함께 출력하세요. 예: 200 == 200 : false → 캐시 범위(-128~127) 밖이라 새 객체.
  • 마지막에 결론 한 줄: 내용 비교는 항상 equals 를 쓰면 안전하다.

힌트

  • Integer a = 200; Integer b = 200; 처럼 오토박싱으로 만들면 == 함정을 재현할 수 있어요(Step 5의 sameCachedObject 참고).
  • 문자열은 new String("같은글자") 두 개로 만들면 ==false 가 돼요.
  • 이건 "왜 equals 를 써야 하나" 를 스스로 증명하는 과제예요. 답이 정해진 게 아니라, 현상을 직접 재현하는 게 핵심이에요.

생각해볼 주제

오늘 배운 내용 뒤에는 "왜 자바를 이렇게 설계했을까?" 하는 더 깊은 질문들이 숨어 있어요. 정답을 외우기보다, 한번 곰곰이 생각해보면 문자열과 객체를 보는 눈이 한층 깊어질 거예요.

주제 1 — String을 왜 굳이 "못 바꾸게" 만들었을까?

String 이 불변이라 s + "x" 마다 새 객체가 생기는 건, 어떻게 보면 비효율적으로 느껴져요. "그냥 내용을 바꿀 수 있게 했으면 새 객체를 안 만들어도 됐을 텐데" 싶죠. 그런데도 자바를 만든 사람들은 String 을 불변으로 설계했어요. 불변이라서 얻는 이득(안전·공유·예측 가능성)과, 불변이라서 치르는 비용(매번 새 객체)을 견줘봤을 때, 왜 안전 쪽에 무게를 뒀을지 생각해보세요.

주제 2 — 자바는 왜 intInteger 를 둘 다 두었을까?

다른 관점의 질문이에요. 숫자를 다루는데 굳이 원시형(int)과 포장 클래스(Integer)를 둘 다 만든 이유가 뭘까요? 하나로 통일했으면 오토박싱·언박싱이나 null 언박싱 함정 같은 골치 아픈 것도 없었을 텐데요. 가벼운 int 가 주는 속도와, 객체인 Integer 가 주는 유연함(null 표현, 컬렉션에 담기) 사이의 맞교환을 떠올려보세요.

주제 3 — 문자열을 이어붙일 때, 언제부터 StringBuilder 를 신경 써야 할까?

"문자열 연결은 무조건 StringBuilder 가 좋다" 는 말은 절반만 맞아요. 짧은 연결까지 전부 StringBuilder 로 바꾸면 코드가 오히려 길고 읽기 어려워지죠. 반대로 반복문 안에서 수천 번 이어붙이는데 + 만 쓰면 낭비가 커지고요. "읽기 좋은 코드" 와 "효율적인 코드" 사이에서, 어느 지점부터 StringBuilder 를 꺼내는 게 좋을지 나만의 기준을 세워보세요.

✅ 예시 답안정답 보기

아래 답안은 "정답 하나" 가 아니라 오늘 배운 도구(문자열 비교, StringBuilder 조립, 래퍼 클래스 변환, format/formatted 서식)를 그대로 가져와 푼 모범 사례 중 하나예요. 여러분 코드가 이것과 글자까지 똑같지 않아도 괜찮아요. 핵심 도구를 제대로 짚었는지를 위주로 비교해보세요.

과제 1 예시답안 — 프로필 카드 한 줄 만들기

핵심 접근

문제의 핵심은 두 가지예요. 첫째, 화면에서 넘어온 값은 문자열("42", "1240")이라 계산·표시 전에 진짜 숫자로 바꿔야 한다는 것 — 여기서 래퍼 클래스의 Integer.parseInt 가 등장해요. 둘째, 이름과 숫자를 섞은 한 줄을 + 로 너저분하게 잇지 말고 서식(formatted)으로 깔끔하게 찍어낸다는 것이에요. Step 4(파싱)와 Step 6(서식)을 한 메서드 안에서 잇는 게 전부예요.

예시 구현

// com/instagram/javabasic/solution/day17/ProfileCard.java
public class ProfileCard {

    // 문자열로 들어온 두 숫자를 int 로 바꿔 깔끔한 한 줄로 조립해요.
    public static String render(String username, String postsRaw, String followersRaw) {
        int posts = Integer.parseInt(postsRaw);
        int followers = Integer.parseInt(followersRaw);
        return "@%s · 게시물 %d · 팔로워 %d".formatted(username, posts, followers);
    }

    public static void main(String[] args) {
        System.out.println(render("jaehoon_dev", "42", "1240"));
        // @jaehoon_dev · 게시물 42 · 팔로워 1240
    }
}

Integer.parseInt 로 문자열을 int 로 바꾼 다음, 틀 문자열의 빈칸(%s·%d·%d)에 이름·게시물 수·팔로워 수를 순서대로 채웠어요. String.format("@%s · 게시물 %d · 팔로워 %d", username, posts, followers) 로 써도 결과는 똑같아요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
문자열 → 숫자 변환 Integer.parseIntpostsRaw·followersRawint 로 바꿨는가
서식 사용 + 떡칠 대신 format 또는 formatted 의 빈칸에 값을 채웠는가
빈칸 기호 정확성 이름은 %s, 숫자는 %d 로 맞게 썼는가
main 검증 render("jaehoon_dev", "42", "1240") 을 호출해 결과를 출력했는가

흔한 실수

  • 문자열을 그대로 %d 빈칸에 넣음 → "42"(문자열)를 %d(정수 빈칸)에 넣으면 에러가 나요. %d 앞에는 반드시 진짜 숫자(int)가 와야 해요. 그래서 parseInt 로 먼저 바꾸는 거예요.
  • 변환 없이 %s 로만 다 처리 → 동작은 하지만, 이 과제의 의도는 "문자열을 숫자로 바꾸는 래퍼 클래스 연습" 이에요. parseInt 를 거치는 게 핵심이에요.
  • 빈칸 개수와 값 개수가 안 맞음 → 빈칸은 3개인데 값을 2개만 넘기면 에러가 나요. 빈칸과 값은 항상 1:1 로 맞춰요.

실무 개선 포인트 (심화)

  • 화면 입력이 "42" 처럼 항상 숫자라는 보장이 없어요. 사용자가 "마흔둘" 같은 걸 넣으면 parseInt 가 에러를 던져요. 실무에서는 이런 잘못된 입력을 미리 거르거나, 변환 실패에 대비하는 처리를 해요 — 그 "예외 처리" 가 Phase 3 뒷부분에서 배울 주제예요.
  • 천 단위 콤마(1,240)나 1.2K 같은 표시도 서식으로 다룰 수 있어요. String.format 에는 %,d 처럼 콤마를 넣는 빈칸도 있어서, 큰 숫자를 읽기 좋게 보여줄 때 유용해요.

과제 2 예시답안 — 댓글 멘션(@) 영수증 만들기

핵심 접근

Step 3의 joinTags(해시태그를 #로 잇던 그 패턴)를 거의 그대로 가져와, # 대신 @ 를 붙이면 멘션 한 줄이 돼요. 반복문 안에서 문자열을 여러 번 이어붙이니 StringBuilder 가 딱 맞는 자리예요. 그리고 마지막에 인원수 통계를 format 으로 덧붙이는데, 배열의 length%d 빈칸에 채우면 끝이에요. "반복 연결은 StringBuilder, 마무리 문장은 서식" 이라는 오늘의 두 도구가 한 메서드에서 만나요.

예시 구현

// com/instagram/javabasic/solution/day17/MentionReceipt.java
public class MentionReceipt {

    public static String build(String[] usernames) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < usernames.length; i++) {
            sb.append("@").append(usernames[i]);
            if (i < usernames.length - 1) {
                sb.append(" ");
            }
        }
        // 멘션 한 줄 아래에 통계 한 줄을 줄바꿈으로 이어 붙여요.
        sb.append("\n");
        sb.append(String.format("총 %d명에게 멘션", usernames.length));
        return sb.toString();
    }

    public static void main(String[] args) {
        String[] commenters = {"minji", "seungwoo", "jaehoon"};
        System.out.println(build(commenters));
        // @minji @seungwoo @jaehoon
        // 총 3명에게 멘션
    }
}

append("@").append(usernames[i]) 로 멘션을 쌓고, 마지막 사람이 아닐 때만 공백을 넣어 @minji @seungwoo @jaehoon 처럼 만들었어요. 그다음 줄바꿈(\n)을 append 하고, 인원수 통계를 String.format 으로 이어 붙였죠. 통계까지 같은 StringBuilder 한 상자 안에서 조립한 게 포인트예요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
StringBuilder 반복 조립 반복문 안에서 append 로 멘션을 누적했는가 (+ 누적 아님)
구분 공백 처리 멘션 사이에만 공백을 넣고 끝에는 안 붙였는가
통계 서식 인원수(length)를 %d 빈칸에 채워 통계 줄을 만들었는가
toString 마무리 마지막에 toString() 으로 완성 문자열을 꺼냈는가
main 검증 이름 배열로 호출해 멘션 줄과 통계 줄을 출력했는가

흔한 실수

  • 모든 멘션 뒤에 공백을 붙여 끝에 공백이 남음 → @minji @seungwoo @jaehoon 처럼 끝에 군더더기 공백이 생겨요. if (i < usernames.length - 1) 로 "마지막이 아닐 때만" 공백을 넣어야 깔끔해요. Step 3의 joinTags 가 정확히 이 처리를 했어요.
  • StringBuilder 대신 String+= 로 누적 → 결과는 맞지만 이 과제의 의도(반복 연결엔 StringBuilder)를 비켜가요. 반복문 안에서 이어붙이고 있다는 신호를 알아채는 게 핵심이에요.
  • 인원수를 직접 세려고 별도 카운터 변수를 둠 → 배열은 이미 length 로 개수를 알려줘요. 굳이 따로 세지 않아도 돼요.

실무 개선 포인트 (심화)

  • 멘션할 사람이 한 명도 없을 때(usernames.length == 0)는 멘션 줄이 비고 "총 0명에게 멘션" 만 남아요. 실무에서는 이런 빈 경우에 아예 다른 안내 문구를 보여주는 식으로 갈라 처리하기도 해요. "입력이 비어 있을 때 어떻게 보일까?" 를 늘 한 번 떠올리는 습관이 좋아요.
  • 지금은 이름 배열을 받았지만, 다음 시간에 배울 컬렉션을 쓰면 "댓글 단 사람들" 을 더 유연하게 모아 넘길 수 있어요. 그러면 중복 멘션 제거(같은 사람이 댓글 두 번 달았을 때 한 번만 멘션)도 자연스럽게 다룰 수 있게 돼요.

과제 3 예시답안 — == 함정 디버깅 리포트

핵심 접근

이 과제는 정해진 출력보다 "현상을 직접 재현하는 것" 이 핵심이에요. Step 1의 문자열 비교(리터럴 풀 vs new String)와 Step 5의 Integer 캐시(-128~127) 함정을 한자리에 모아 실행하고, 각 결과 옆에 왜 그런지 한 줄 이유를 붙여요. == 가 "같은 상자냐" 를 묻는다는 사실 하나로 네 가지 결과가 전부 설명된다는 걸 스스로 증명하는 게 목표예요.

예시 구현

// com/instagram/javabasic/solution/day17/EqualityReport.java
public class EqualityReport {

    // 따옴표 리터럴 두 개는 문자열 풀의 같은 상자를 가리켜 == 도 true.
    public static boolean literalReference() {
        String a = "instagram";
        String b = "instagram";
        return a == b;
    }

    // new String 두 개는 각각 새 상자라 == 는 false (내용은 equals 로 true).
    public static boolean newStringReference() {
        String a = new String("instagram");
        String b = new String("instagram");
        return a == b;
    }

    // -128~127 범위의 Integer 는 재사용돼서 == 가 true, 범위 밖은 false.
    public static boolean integerReference(int value) {
        Integer a = value;
        Integer b = value;
        return a == b;
    }
}

각 현상을 메서드로 떼어두면 main 에서 깔끔하게 불러 쓸 수 있어요.

// EqualityReport.java (main 발췌)
System.out.println("리터럴 == : " + literalReference()
        + " → 문자열 풀의 같은 상자라 true");
System.out.println("new String == : " + newStringReference()
        + " → 각각 새 상자라 false (내용 비교는 equals 로 true)");
System.out.println("127 == 127 : " + integerReference(127)
        + " → 캐시 범위(-128~127) 안이라 같은 객체");
System.out.println("200 == 200 : " + integerReference(200)
        + " → 캐시 범위 밖이라 새 객체");
System.out.println("결론: 내용 비교는 항상 equals 를 쓰면 안전하다");

실행하면 리터럴과 127true, new String200false 가 나와요. 똑같이 생긴 비교인데 결과가 갈리는 이유가 한 줄씩 함께 찍히죠.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
문자열 함정 재현 리터럴 ==(true)와 new String ==(false)를 둘 다 보여줬는가
Integer 캐시 재현 127(true)과 200(false)의 == 차이를 보여줬는가
이유 설명 각 결과 옆에 "왜 그런지" 한 줄을 붙였는가
결론 한 줄 "내용 비교는 항상 equals" 결론을 출력했는가
equals 대비 new String 쪽에서 equals 는 true 임을 함께 짚었는가

흔한 실수

  • Integer a = 200; Integer b = 200; 가 아니라 int a = 200; 으로 만듦 → 원시형 int 끼리의 == 는 값 비교라 무조건 true 가 나와요. 캐시 함정은 Integer(객체)일 때만 재현돼요. 오토박싱으로 Integer 를 만들어야 해요.
  • 127200 을 둘 다 캐시 범위 안/밖 한쪽으로만 테스트 → 두 결과가 갈리는 걸 보여줘야 "범위에 따라 달라진다" 가 증명돼요. 경계(127)와 그 바로 바깥(128 이상)을 함께 보여주면 더 선명해요.
  • 결과만 출력하고 이유를 안 적음 → 이 과제는 "왜" 를 설명하는 게 절반이에요. 같은 값인데 결과가 다른 이유를 한 줄로라도 적어야 디버깅 리포트가 돼요.

실무 개선 포인트 (심화)

  • 이 함정은 실무에서 진짜 버그로 자주 나타나요. 특히 화면에서 넘어온 ID 나 수량을 Integer 로 받아 == 로 비교하다가, 값이 127을 넘는 순간 갑자기 비교가 어긋나는 사고가 대표적이에요. 그래서 "객체는 무조건 equals" 가 몸에 배어 있으면 이런 사고를 아예 안 만나요.
  • 자바를 만든 쪽도 이 캐시가 헷갈린다는 걸 알아서, 작은 정수를 미리 만들어두는 범위(-128~127)를 문서로 명시해뒀어요. 다만 우리가 기억할 건 "범위가 몇이더라" 가 아니라 "객체 값 비교는 equals" 라는 한 가지 원칙이에요. 그러면 캐시 범위를 외울 필요조차 없어요.

생각해볼 주제 1 예시답안 — String을 왜 굳이 "못 바꾸게" 만들었을까?

[문제 상황 요약]

String 은 불변이라 s + "x" 마다 새 객체가 생겨요. 매번 새로 만드는 건 분명 비용이죠. "그냥 내용을 바꿀 수 있게 했으면 새 객체를 안 만들어도 됐을 텐데" 싶어요. 그런데도 자바를 만든 사람들은 String 을 불변으로 설계했어요. 안전(공유·예측 가능성)과 비용(매번 새 객체) 사이에서, 왜 안전 쪽에 무게를 뒀을까요?

[튜터의 가이드 및 해설]

이 질문은 "성능 비용" 과 "안전·공유의 이득" 사이의 트레이드오프를 묻는 거예요. 결론부터 말하면, 자바는 String 이 워낙 곳곳에서 공유되고 또 신뢰의 기준이 되기 때문에 안전을 택했어요.

Option A — 불변(못 바꾸게): 장점은 안심하고 공유할 수 있다는 거예요. 같은 문자열을 여러 곳이 가리켜도, 누구도 내용을 못 바꾸니 한쪽 변경이 다른 쪽에 번지는 사고가 없어요. Step 1에서 본 문자열 풀로 같은 글자를 재사용해 메모리도 아낄 수 있고요. 단점은 변경처럼 보이는 작업마다 새 객체가 생겨 비용이 든다는 점이에요.

Option B — 가변(바꿀 수 있게): 장점은 같은 상자 안에서 내용을 고치니 새 객체를 안 만들어도 된다는 거예요. 단점은 공유가 위험해진다는 점이에요. 어떤 회원의 이름 문자열을 다른 곳에서 몰래 바꾸면, 그 문자열을 같이 쓰던 모든 곳이 영문도 모르고 영향을 받아요. 비밀번호나 파일 경로처럼 한 번 정해진 값이 도중에 바뀌면 심각한 보안·동작 사고로 이어지고요.

현업에서는 보통: 이 선택의 실제 이유가 바로 그 안전이에요. 문자열은 회원 이름, 파일 경로, 설정 키처럼 "한 번 정해지면 믿고 쓰는 값" 으로 너무 자주 쓰여요. 이걸 누군가 바꿀 수 있게 두면 공유 자체가 위험해지죠. 그래서 불변으로 잠그고, 대신 "많이 바꿔야 하는 상황" 을 위해 StringBuilder 라는 가변 도구를 따로 준 거예요. 즉 자바는 둘 중 하나를 버린 게 아니라, 기본은 안전한 불변(String), 필요할 때만 꺼내는 가변(StringBuilder) — 이렇게 역할을 갈라 둘 다 제공한 거예요.

🎯 면접관을 홀리는 핵심 멘트

"String 을 불변으로 만든 건 비용을 몰라서가 아니라, 공유의 안전을 택한 결정입니다. 문자열은 이름·경로·키처럼 믿고 쓰는 값으로 곳곳에서 공유되는데, 가변이면 한쪽 변경이 모든 공유 지점에 번져 사고가 됩니다. 그래서 기본은 불변으로 잠가 안전을 보장하고, 많이 바꿔야 하는 상황을 위해 StringBuilder 라는 가변 도구를 따로 제공합니다. 안전을 기본값으로 두고 성능은 전용 도구로 푸는 설계죠."


생각해볼 주제 2 예시답안 — 자바는 왜 intInteger 를 둘 다 두었을까?

[문제 상황 요약]

숫자를 다루는데 자바는 원시형(int)과 포장 클래스(Integer)를 둘 다 만들었어요. 하나로 통일했으면 오토박싱·언박싱이나 null 언박싱 함정 같은 골치 아픈 것도 없었을 텐데요. 그런데도 둘을 나눠 둔 이유가 뭘까요?

[튜터의 가이드 및 해설]

이건 "속도" 와 "유연함" 사이의 트레이드오프예요. intInteger 는 각자 잘하는 게 달라서, 하나로 합치면 둘 중 한쪽의 장점을 잃어버려요.

Option A — 원시형 int: 장점은 가볍고 빠르다는 거예요. 값만 딱 담으니 메모리도 적게 쓰고 계산도 빨라요. 수백만 번 도는 반복문에서 숫자를 셀 때처럼 성능이 중요한 자리에 딱이에요. 단점은 "객체가 아니라서" 할 수 없는 일이 있다는 점이에요. null(값 없음)을 표현 못 하고, 객체만 담는 컬렉션에도 못 들어가요.

Option B — 포장 클래스 Integer: 장점은 객체라서 누리는 유연함이에요. null 로 "아직 값이 없음" 을 표현할 수 있고, 컬렉션에 담을 수 있고, 숫자 관련 편의 메서드(parseInt 등)도 딸려 와요. 단점은 객체라서 더 무겁고, 오토박싱·언박싱 과정과 null 언박싱 같은 함정이 따라온다는 점이에요.

현업에서는 보통: 자리에 맞게 골라 써요. "이 값이 없을 수도 있나(null 이 의미 있나)?", "컬렉션에 담아야 하나?" 면 Integer, 그냥 빠르게 세고 계산하는 단순 숫자면 int 예요. 만약 둘을 Integer 하나로 통일했다면 모든 숫자 계산이 객체를 거쳐 느려졌을 거고, int 하나로 통일했다면 null 표현이나 컬렉션 담기를 포기해야 했을 거예요. 그래서 자바는 둘을 다 두고, 그 사이를 오가는 수고를 덜어주려고 오토박싱·언박싱이라는 자동 변환을 끼워 넣은 거예요. 함정은 그 편의의 대가인 셈이죠.

🎯 면접관을 홀리는 핵심 멘트

"int 와 Integer 는 속도와 유연함의 트레이드오프입니다. int 는 가볍고 빨라 성능이 중요한 자리에 쓰고, Integer 는 null 표현과 컬렉션 담기가 가능해 '값이 없을 수 있는' 자리에 씁니다. 하나로 통일했다면 속도든 유연함이든 한쪽을 잃었을 겁니다. 둘을 다 두고 오토박싱으로 오가는 수고를 덜되, null 언박싱 같은 함정은 그 편의의 대가라는 걸 알고 씁니다."


생각해볼 주제 3 예시답안 — 언제부터 StringBuilder 를 신경 써야 할까?

[문제 상황 요약]

"문자열 연결은 무조건 StringBuilder 가 좋다" 는 말은 절반만 맞아요. 짧은 연결까지 전부 StringBuilder 로 바꾸면 코드가 길고 읽기 어려워지죠. 반대로 반복문 안에서 수천 번 이어붙이는데 + 만 쓰면 낭비가 커지고요. "읽기 좋은 코드" 와 "효율적인 코드" 사이에서, 어느 지점부터 StringBuilder 를 꺼내는 게 좋을까요?

[튜터의 가이드 및 해설]

핵심 기준은 딱 하나예요 — "반복문 안에서 이어붙이고 있는가?" 한 번에 끝나는 연결은 + 가 읽기 좋고, 횟수가 정해지지 않은 반복 연결은 StringBuilder 가 효율적이에요.

Option A — 항상 +: 장점은 읽기 쉽다는 거예요. "@" + name + "님 안녕하세요" 는 한눈에 의미가 들어와요. 단점은 반복문 안에서 쓰면 매번 새 객체가 생겨 낭비가 커진다는 점이에요. 게시물 1만 개의 태그를 + 로 잇는다면 버려지는 문자열이 산더미처럼 쌓여요(Step 2에서 본 그 그림이에요).

Option B — 항상 StringBuilder: 장점은 반복 연결에서 효율적이라는 거예요. 단점은 짧은 연결에도 new StringBuilder().append(...).append(...).toString() 처럼 길게 써야 해서 코드가 너저분해지고 의도가 흐려진다는 점이에요. 한 줄이면 될 걸 네 줄로 만드는 셈이죠.

현업에서는 보통: "반복문 안에서 문자열을 이어붙이고 있다" 가 신호예요. 그 순간이 StringBuilder 를 꺼낼 때예요. 반대로 반복문 밖에서 정해진 개수의 조각을 한 번에 잇는 거라면 + 가 더 낫고요. 사실 요즘 자바는 단순한 + 연결을 내부적으로 효율적으로 처리해주기 때문에, 짧은 연결까지 굳이 StringBuilder 로 바꿀 이유는 거의 없어요. 그래서 실무 기준은 단순해요 — "루프 안 연결이면 StringBuilder, 그 외엔 읽기 좋은 +." 성능 걱정으로 미리 다 바꾸기보다, 반복이라는 신호가 보일 때만 손을 대는 게 깔끔한 코드와 효율을 둘 다 잡는 길이에요.

🎯 면접관을 홀리는 핵심 멘트

"기준은 '반복문 안에서 이어붙이는가' 하나입니다. 루프 안 연결은 매번 새 객체가 쌓이니 StringBuilder 로 가고, 정해진 개수를 한 번에 잇는 건 읽기 좋은 + 로 둡니다. 요즘 자바는 단순 + 연결을 내부적으로 잘 처리해줘서, 짧은 연결까지 StringBuilder 로 바꾸는 건 오히려 가독성을 해치는 과최적화입니다. 반복이라는 신호가 보일 때만 손대는 게 가독성과 효율을 함께 잡는 길입니다."

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.