Java 07
자동/강제 형 변환과 String 숫자 변환: 혼란 끝! 명확한 구별법
작은 타입에서 큰 타입으로는 자동 변환되고 반대는 강제 변환된다는 말이 숫자 손실이나 문자열 변환과 섞여 헷갈린다.
근거 · 교안 p54-p59
1장: 형 변환, 정말 그게 다일까?
솔라는 노트북 화면 한구석에 띄워 둔 메모를 멍하니 바라보고 있었다. 방금 읽은 책의 한 문장이었다.
형 변환은 한 자료형의 값을 다른 자료형으로 바꾸는 것이다.
짧고 명료했다. 하지만 솔라는 어쩐지 개운하지 않은 기분으로 화면을 톡톡 건드렸다. 아는 단어들의 조합인데, 머릿속에서는 안개가 낀 것처럼 그림이 흐릿했다. 분명 자동 형 변환, 강제 형 변환 같은 말들을 들었던 기억이 났다. 문자열을 숫자로 바꾸는 것도 있었다. 그게 다 ‘자료형을 바꾸는 것’이라면, 왜 그렇게 다른 이름으로 불리는 걸까?
“언니.”
거실에서 조용히 코드를 보던 루나가 고개를 들었다.
“형 변환이라는 게 그냥 자료형만 바꾸는 거래. 근데 정말 그게 다야? int를 double로 바꾸든, double을 int로 바꾸든, 문자열 ‘100’을 숫자 100으로 바꾸든 다 똑같은 ‘변환’인 건가?”
솔라의 목소리에는 ‘그럴 리 없다’는 확신과 ‘그런데 왜 아니라고 설명할 수 없지?’라는 답답함이 섞여 있었다. 마치 예전에 int니 double이니 하는 자료형 자체를 처음 골라야 했을 때처럼, 개념은 알겠는데 언제 어떻게 써야 할지 감이 잡히지 않는 기분이었다.
루나는 잠시 생각하더니, 키보드에서 손을 떼고 솔라 쪽으로 몸을 돌렸다.
“솔라, 여기 작은 유리컵이랑 큰 물병이 있다고 생각해 봐.”
루나는 손으로 허공에 작은 컵과 큰 병의 모양을 그렸다.
“작은 컵에 물이 반쯤 차 있어. 이 물을 큰 물병에 옮겨 담는 건 어떨까?”
“그야 쉽지. 그냥 부으면 되잖아. 물이 넘칠 리도 없고, 모자랄 리도 없고.”
솔라는 당연하다는 듯이 대답했다.
“맞아. 그럼 코드로 한번 볼까?”
루나가 자신의 노트북 화면에 짧은 코드를 입력했다.
int smallCup = 100;
double bigBottle = smallCup;
System.out.println(bigBottle);
“여기 int 타입 변수 smallCup은 정수만 담는 작은 컵이야. double 타입 변수 bigBottle은 소수점까지 담을 수 있는 훨씬 큰 물병이고. smallCup의 값을 bigBottle에 그대로 할당했어. 결과가 어떻게 나올 것 같아?”
“음… smallCup에 100이 들어있으니까, bigBottle에도 100이 들어가겠지. double이니까 아마 100.0으로 출력될 것 같은데? 어쨌든 값 자체는 안 변할 거야.”
솔라가 예측하자, 루나는 코드를 실행했다. 화면에 100.0이라는 숫자가 찍혔다. 솔라의 예상 그대로였다.
“정확해. 아무 문제 없이 값이 그대로 옮겨졌어. 데이터가 조금도 사라지지 않았지. 방금 네가 말한 것처럼, 작은 컵의 물을 큰 병에 붓는 것처럼 아주 안전해.”
“응. 이건 그냥 자연스럽게 되네.”
“바로 그 점이야. 왜 이게 이렇게 안전하고 자연스러울까?”
루나의 질문에 솔라는 잠시 생각에 잠겼다. ‘왜?‘라니. 너무 당연해서 이유를 생각해 본 적이 없었다. 그녀는 아까 루나가 그렸던 허공의 컵과 병을 떠올렸다.
”…컵 크기 때문인가?”
솔라가 조심스럽게 입을 열었다.
“int라는 컵이 담을 수 있는 숫자의 범위보다, double이라는 물병이 담을 수 있는 숫자의 범위가 훨씬 넓으니까. 정수만 담던 컵의 내용물은, 소수점까지 담을 수 있는 더 큰 컵으로 옮겨도 아무 문제가 생기지 않는 거지. 들어갈 공간이 충분하니까.”
순간, 머릿속에 흩어져 있던 조각들이 맞춰지는 느낌이 들었다. 안전하다, 자연스럽다, 저절로 된다는 말들이 하나의 그림으로 이어졌다.
“아! 그래서 ‘확장’이라고 하는구나. 작은 쪽에서 큰 쪽으로 안전하게 확장되는 거네.”
솔라는 무릎을 탁 쳤다. 그러자 처음 봤던 그 문장이 다시 떠올랐다.
형 변환은 한 자료형의 값을 다른 자료형으로 바꾸는 것이다.
이제 그 문장은 더 이상 밋밋하게 들리지 않았다. 그 뒤에 숨어 있는 질문이 보이기 시작했다.
“그렇구나… ‘자료형을 바꾼다’는 건, 그냥 이름표를 바꿔 다는 게 아니었어. 어떤 크기의 집에서 어떤 크기의 집으로 이사 가느냐의 문제였네. 작은 집에서 큰 집으로 갈 때는 짐이 남을 걱정이 없으니까 그냥 가도 되는 거고.”
솔라는 자신의 발견에 신이 나서 말을 이었다.
“그러면… 어떤 변환은 이렇게 조용히, 자동으로 일어나는 거구나. 안전하니까. 컴퓨터가 ‘이건 내가 알아서 해도 괜찮겠다’고 판단하는 거네.”
루나는 조용히 고개를 끄덕였다. 솔라가 스스로 길을 찾는 모습을 지켜볼 뿐이었다.
“그럼 반대의 경우는?”
솔라의 눈이 빛났다. 새로운 질문이 꼬리를 물고 떠올랐다.
“큰 물병에 가득 찬 물을 작은 컵에 부으면 어떻게 되지? 분명 넘칠 텐데. 코드에서도 그런 일이 벌어질까? 그리고 넘칠 게 뻔한데, 왜 굳이 그런 변환을 해야 하는 걸까?”
안전한 이사의 원리를 깨닫자, 위험한 이사에 대한 궁금증이 솟아올랐다. 자동으로 되지 않는 것들의 세계, 개발자가 직접 무언가를 결정해야만 하는 세계가 저 너머에 있는 것 같았다.
2장: 자동 형 변환: 보이지 않는 안전한 확장
솔라는 이전 코드를 그대로 띄워놓은 화면을 다시 들여다봤다. 모든 게 명확해졌다고 생각했는데, 보면 볼수록 한 부분이 마음에 걸렸다.
int smallCup = 100;
double bigBottle = smallCup; // <- 이 부분!
솔라는 bigBottle = smallCup; 줄 옆에 주석을 달았다가 지우기를 반복했다. // 자동으로 변환?, // 그냥 되네?. 아무리 봐도 등호(=) 말고는 아무런 명령이 없었다. 컴퓨터가 이 변환이 ‘안전하다’는 것을 어떻게 알고 스스로 처리한 걸까? 마치 보이지 않는 손이 매끄럽게 처리해준 것 같았다.
“언니, 이거 좀 이상해.”
솔라가 화면을 가리켰다.
“작은 컵에서 큰 병으로 옮기는 게 안전하다는 건 알겠어. 그런데 컴퓨터가 그걸 어떻게 알아? 여기 아무런 표시도 없잖아. ‘이거 안전하니까 변환해!’ 같은 명령어가 있는 것도 아니고. 그냥 smallCup을 bigBottle에 넣으라고만 했는데, 알아서 100을 100.0으로 바꿔서 넣었단 말이지. 만약 이게 항상 이렇게 ‘자동’으로, ‘알아서’ 되는 거라면… 개발자는 뭘 믿고 코드를 쓰는 거야?”
솔라의 질문 속에는 ‘자동’이라는 단어에 대한 미묘한 불신이 담겨 있었다. 너무 편리해서 오히려 불안한, 마법 같은 현상에 대한 의구심이었다.
루나는 솔라의 화면을 잠시 들여다보더니, 고개를 끄덕였다. 그리고는 솔라의 코드 아래에 몇 줄을 더 추가했다.
byte tinyBox = 10;
int smallCup = tinyBox; // 이것도 그냥 되네?
long largeContainer = smallCup; // 이것도?
“이건 어때? byte는 int보다 훨씬 작은 정수 타입이고, long은 int보다 훨씬 큰 정수 타입이야. tinyBox를 smallCup에, smallCup을 largeContainer에 넣고 있어.”
솔라는 코드를 가만히 들여다봤다. 컵과 병의 비유가 머릿속에서 숫자의 범위로 바뀌고 있었다.
“음… byte가 제일 작고, 그 다음 int, 그 다음 long… 전부 작은 상자에서 더 큰 상자로 옮겨 담고 있네. 그러니까 이것들도 아무 문제 없이, 아무런 추가 명령 없이 그냥 될 것 같아. 데이터가 잘리거나 넘칠 걱정이 전혀 없으니까.”
“맞아. 지금 네가 한 생각이 바로 컴파일러가 하는 생각이야.”
루나의 말에 솔라는 눈을 동그랗게 떴다.
“컴파일러가 생각을 해?”
“생각이라기보다는, 아주 엄격한 규칙을 따르는 거지.”
루나는 말을 이었다.
“컴파일러는 ‘두 개의 타입이 호환되고, 더 작은 범위를 표현하는 타입에서 더 큰 범위를 표현하는 타입으로 값을 옮기는 경우, 데이터 손실이 없다고 확신할 수 있으므로 내가 알아서 변환하겠다’라는 규칙을 가지고 있어. 이걸 묵시적 형 변환(Implicit Casting) 또는 자동 형 변환이라고 불러.”
묵시적(Implicit). 솔라는 그 단어를 가만히 곱씹었다. ‘겉으로 드러나지 않는다’는 뜻이었다. 개발자가 (double)처럼 명시적인 명령을 내리지 않아도, 규칙에 부합하기 때문에 컴파일러가 조용히 처리해준다는 의미였다.
“아하! 그러니까 ‘자동’이라는 게 그냥 편리 기능이 아니라, ‘안전한 확장’이라는 명확한 규칙 위에서만 작동하는 거구나!”
솔라는 무릎을 탁 쳤다.
“컴퓨터가 똑똑해서 내 마음을 읽고 변환해주는 게 아니었어. 그냥 ‘작은 거에서 큰 거로는 OK’라는, 아주 단순하고 기계적인 규칙을 따를 뿐이었던 거야. 규칙에 맞으니까, 개발자가 귀찮게 뭔가 더 쓸 필요가 없었던 거고.”
이제 솔라에게 double bigBottle = smallCup; 코드는 다르게 보였다. 처음에는 마법처럼 보였던 자동 변환이, 이제는 너무나 당연한 ‘묵시적 확장 규칙’ 의 결과물로 보였다. 보이지 않는 손의 정체는, 똑똑한 인공지능이 아니라 그저 정해진 규칙을 따르는 엄격한 심판이었던 것이다.
“그럼 그 규칙에 어긋나는 건 어떻게 되는 건데?”
새로운 이해는 곧바로 새로운 질문으로 이어졌다.
“고속도로 진입로에서 1차선 도로가 4차선 고속도로로 합쳐지는 건 쉬워. 그냥 들어가면 되니까. 이게 묵시적 변환인 거지. 그런데 반대로, 4차선 고속도로에서 달리던 차들이 전부 1차선 샛길로 빠져나가려고 하면? 분명 병목 현상이 생기고, 못 들어가는 차들이 넘쳐날 거잖아. 컴파일러라는 심판은 이 상황을 그냥 보고만 있진 않겠지? ‘이건 규칙 위반이야!’ 하면서 막아설 것 같은데.”
솔라의 눈이 빛났다. 안전한 규칙의 존재를 확인하자, 그 규칙이 허용하지 않는 위험한 세상이 궁금해졌다. 컴파일러가 막아서는 길을 굳이 가야 할 때, 개발자는 어떤 결단을 내려야 하는 걸까? 그리고 그 결단에는 어떤 대가가 따를까?
3장: 강제 형 변환: 개발자의 결단과 그 대가
솔라는 자신의 비유에 심취해 있었다. ‘4차선 고속도로에서 1차선 샛길로 빠져나가기.’ 컴파일러라는 엄격한 심판이 이런 위험한 길을 그냥 내버려 둘 리 없다고 확신했다. 그녀는 자신의 예상을 코드로 증명해 보이고 싶었다. 노트북을 끌어당겨 새 파일을 열고 망설임 없이 타이핑했다.
double bigBottle = 3.14;
int smallCup = bigBottle;
예상대로, int smallCup = bigBottle; 라인 아래에 즉시 붉은 밑줄이 그어졌다. 컴파일러의 무언의 항의였다. 솔라는 마우스를 밑줄 위로 가져갔다. ‘Type mismatch: cannot convert from double to int.’ 호환되지 않는 타입. double을 int로 변환할 수 없습니다.
“거봐, 내 말이 맞지? 심판이 막아설 거라고 했잖아.”
솔라는 의기양양하게 말했지만, 어딘가 개운치 않았다. 분명 막힐 줄 알았지만, 이렇게 단칼에 거절당하니 오히려 오기가 생겼다.
“그런데 말이야, 언니. 내가 이 물병에 3.14리터가 아니라 정확히 3.0리터가 들어있는 걸 알아. 그래서 소수점 아래는 필요 없고, 정수 3만 꺼내서 쓰고 싶을 수도 있잖아. 넘칠 위험이 없다는 걸 내가 이미 아는 상황인데도, 컴파일러는 무조건 안 된다고만 하네. 답답하게.”
솔라의 불평에 루나는 조용히 화면을 들여다보았다.
“컴파일러는 변수 안의 실제 값을 보지 않아. 오직 타입의 크기, 즉 컵과 물병의 크기만 보고 규칙에 따라 판단할 뿐이야. double 물병은 int 컵보다 크니까, 잠재적으로 물이 넘칠 수 있다고 보고 무조건 막는 거지.”
“그럼 방법이 아예 없는 거야? 개발자가 위험을 감수하고서라도 하고 싶을 땐?”
바로 그 지점이었다. 솔라가 가장 궁금했던 것. 컴파일러의 보호를 뚫고 개발자의 의지를 관철하는 방법. 루나는 솔라의 코드 한가운데를 수정했다.
double bigBottle = 3.14;
int smallCup = (int) bigBottle; // <- 여기!
System.out.println(smallCup);
등호 오른편, 변수명 앞에 괄호와 함께 (int)가 추가되었다. 붉은 밑줄이 마법처럼 사라졌다.
“이게 바로 명시적 형 변환(Explicit Casting), 강제 형 변환이야. 괄호 안에 목표 타입을 적어주는 거지. 이건 컴파일러에게 보내는 서명 같은 거야. ‘네가 왜 걱정하는지 알아. 데이터가 손실될 수 있다는 것도 인지했어. 모든 책임은 내가 질 테니, 그냥 변환해 줘’라고 말하는 거지.”
“책임을 진다는 서명이라… 멋있는데?”
솔라는 (int)라는 작은 코드 조각을 보며 중얼거렸다. 그녀는 코드를 실행했다. 화면에 3이 출력되었다.
“오, 정말 3만 나왔네. 소수점 아래는 그냥 버려지는구나. 반올림도 없이. 이게 언니가 말한 ‘값 손실’의 한 종류인 거네. 소수점 손실.”
솔라는 고개를 끄덕였다. 위험의 첫 번째 얼굴을 확인했다. 예상 가능하고, 어쩌면 의도했을 수도 있는 손실이었다. 하지만 루나는 여기서 멈추지 않았다.
“그럼 이건 어떨까? 그 물병이 상상 이상으로 크다면?”
루나는 bigBottle의 값을 어마어마하게 큰 숫자로 바꿨다. int가 담을 수 있는 최대치(약 21억)를 훌쩍 넘는 숫자였다.
double veryBigBottle = 2200000000.0; // 22억
int smallCup = (int) veryBigBottle;
System.out.println(smallCup);
“이것도… (int) 서명을 했으니까 되긴 되겠지? int 컵이 담을 수 있는 만큼만 담기고 나머지는 버려지려나? 제일 큰 값으로 채워지나?”
솔라의 예측은 더 이상 확신에 차 있지 않았다. 그녀는 조심스럽게 코드를 실행했다. 그리고 출력된 결과를 보고 눈을 의심했다.
2147483647
어라, int의 최댓값이 나왔다. 솔라는 고개를 갸웃하며 숫자를 조금 더 키워봤다. 3000000000.0. 그러자 이번에는 완전히 다른, 기괴한 음수가 나타났다.
-1294967296
“뭐야, 이 숫자? 30억을 넣었는데 왜 음수가 나와? 완전히 다른 값이잖아!”
솔라는 소리쳤다. 이건 소수점이 잘려나가는 것과는 차원이 다른 문제였다. 데이터가 손실되는 것을 넘어, 완전히 망가져 버렸다.
루나가 차분하게 설명했다. “그게 값 손실의 두 번째 얼굴, **오버플로우(Overflow)**야. 컵이 담을 수 있는 용량을 넘어서는 순간, 물이 그냥 넘쳐흐르는 게 아니라 엉뚱한 방향으로 휘어버린 거지. 숫자 체계가 한 바퀴 돌아서 완전히 다른 값이 되어버린 거야.”
순간 솔라는 머리를 한 대 맞은 듯한 충격을 받았다. ‘값 손실이 생길 수 있다’는 책의 문장은 더 이상 막연한 경고가 아니었다. 그것은 때로는 소수점을 잘라내는 정교한 절삭이었고, 때로는 데이터를 쓰레기로 만드는 끔찍한 붕괴였다.
‘강제 형 변환’은 단순히 타입을 바꾸는 기술이 아니었다. 개발자가 데이터 손실의 구체적인 두 가지 유형(소수점 손실과 오버플로우)을 모두 인지하고, 그 결과를 책임지겠다는 명시적인 ‘결단’이었다. 그리고 그 결단에는 때로 예상치 못한 ‘대가’가 따랐다.
“알겠어… 숫자들 사이의 이사는 이제 감이 와. 작은 집에서 큰 집으로 가는 건 안전해서 자동. 큰 집에서 작은 집으로 갈 때는 (이사갈집)이라고 서명하고, 짐이 잘리거나 망가질 각오를 해야 하는 거구나.”
솔라는 자신의 노트북에 적힌 (int)를 보며 생각에 잠겼다. 그러다 문득 새로운 종류의 변환이 머릿속을 스쳤다.
“그런데 언니, 지금까지 우리가 한 건 전부 숫자라는 세상 안에서의 이사였잖아. 만약에… 전혀 다른 세상의 것을 가져오려면 어떻게 해야 해? 예를 들어, 가게 간판에 쓰인 ‘100’이라는 글자를 숫자 100으로 바꾸고 싶을 때. 이건 집을 옮기는 게 아니잖아. 종이에 적힌 글자를 진짜 사과 100개로 바꾸는 것 같은데… 이것도 (int) "100" 이렇게 서명하면 되는 걸까?”
4장: String-숫자 변환: 또 다른 얼굴의 형 변환
솔라는 자신의 깨달음을 곧바로 시험해 보고 싶었다. 숫자들 사이의 이사 규칙, 즉 강제 형 변환이라는 ‘책임 서명’의 원리를 터득했다고 생각했다. 그녀는 노트북에 새로운 코드를 작성하기 시작했다. 가게 간판에 적힌 글자를 진짜 숫자로 바꾸는, 전혀 다른 세상 간의 변환을 시도할 참이었다.
String textNumber = "100";
int realNumber = (int) textNumber;
솔라는 (int)를 입력하며 의기양양한 미소까지 지었다. ‘이것도 변환이니까, 목표 타입으로 서명해주면 되겠지. 글자 “100”을 숫자 100으로 바꿔달라는 내 의도는 명확하잖아?’
하지만 그녀의 예상과 달리, 코드는 붉은 밑줄로 즉시 그어졌다. 컴파일러는 (int) 서명을 받아주기는커녕, 아예 대화 자체를 거부하는 듯했다. 에러 메시지는 이전보다 더 단호했다. ‘Cannot cast from String to int.’ String에서 int로 캐스팅할 수 없습니다.
“어? 왜 안 돼?”
솔라는 당황했다. 이건 ‘데이터 손실’의 위험을 경고하는 수준이 아니었다. 아예 불가능하다는 선언이었다.
“아니, double을 int로 바꿀 때는 책임진다고 서명하니까 봐줬잖아. 그런데 왜 문자열은 서명을 해도 안 되는 건데? 이건 그냥 글자 ‘100’이라서 데이터 손실이 날 것도 없는데. 더 까다롭게 구네.”
솔라의 목소리에는 배신감마저 묻어났다. 일관성 있을 거라 믿었던 규칙이 깨진 순간이었다. 종이에 적힌 글자를 진짜 사과로 바꾸는 비유가 떠올랐다. 단순히 (사과)라고 외친다고 종이가 사과로 변하지 않는 것처럼, 근본적으로 뭔가 다른 과정이 필요한 것일까.
“그건 이사가 아니거든.”
조용히 지켜보던 루나가 나지막이 말했다.
“언니?”
“지금까지 우리가 한 건 숫자라는 같은 마을 안에서 큰 집, 작은 집으로 이사하는 거였어. 집의 구조는 비슷해서 짐을 덜어내거나 그대로 옮기면 됐지. 하지만 지금 솔라 네가 하려는 건, ‘종이에 적힌 집 주소’를 보고 ‘실제 집’을 지으려는 것과 같아. 이건 이사가 아니야. 설계도를 보고 건물을 짓는, 전혀 다른 종류의 작업이지.”
설계도와 건물. 솔라는 그 비유를 잠시 곱씹었다. 이사와 건축. 확실히 다른 차원의 일이었다.
“그럼… 설계도를 건물로 만드는 건축가가 따로 있다는 거야?”
“맞아. 그리고 그 건축가를 부르는 방법은 따로 정해져 있어.”
루나는 솔라의 코드를 수정했다. (int) 서명을 지우고, 완전히 새로운 형태의 코드를 보여주었다.
String textNumber = "100";
int realNumber = Integer.parseInt(textNumber);
System.out.println(realNumber + 1);
Integer.parseInt(). 생소한 구문이었다. (int)처럼 언어 자체에 내장된 기능이 아니라, 마치 누군가의 이름을 부르는 것 같았다. ‘Integer’라는 전문가에게 ‘parseInt(정수로 해석해줘)’라고 일을 시키는 모양새였다.
솔라는 반신반의하며 코드를 실행했다. 화면에 101이 출력되었다. “100”이라는 글자가 완벽하게 숫자 100으로 변환되어 덧셈까지 수행된 것이다.
“된다…! 진짜 되네.”
“그럼 반대는 어떨까? 숫자 100을 다시 글자 “100”으로 바꾸고 싶으면?”
루나의 질문에 솔라는 잠시 망설였다. 하지만 이내 배운 것을 써먹고 싶어졌다.
“설마… 이것도 (String) 100은 아니겠지?”
그녀는 직접 코드를 쳐서 확인했다. 역시나, 컴파일러는 이번에도 단호하게 (String) 서명을 거부했다. 이제 솔라는 더 이상 당황하지 않았다.
“이것도 다른 전문가가 있겠구나. 글자로 바꾸는 전문가.”
그녀는 루나가 보여준 코드를 떠올리며 유추하기 시작했다. String.なんとか(100) 같은 형태가 아닐까? 루나는 기다렸다는 듯이 답을 보여주었다.
int number = 100;
String textAgain = String.valueOf(number);
String.valueOf(). 이번엔 String이라는 전문가가 ‘값으로부터(value of)’ 글자를 만들어내는 모습이었다.
솔라는 두 개의 코드를 나란히 놓고 보았다.
// 글자 -> 숫자 (해석)
int realNumber = Integer.parseInt("100");
// 숫자 -> 글자 (표현)
String textAgain = String.valueOf(100);
순간 머릿속이 환해졌다. 자동 형 변환, 강제 형 변환과는 완전히 다른 세 번째 종류의 변환이었다. 이건 컵과 물병의 크기 문제가 아니었다. 서로 다른 세상의 데이터를 번역하는 문제였다.
“알겠다… 이제 알겠어. 형 변환이라고 다 같은 게 아니었어.”
솔라는 흥분하며 말했다.
“숫자끼리 바꾸는 건 캐스팅(Casting), 즉 하나의 형태를 다른 형태로 억지로 맞추는 거였어. 이사 같은 거지. 하지만 글자와 숫자는 애초에 종류가 다르니까, 그렇게 맞출 수가 없는 거야. 이건 번역이 필요해. Integer.parseInt나 String.valueOf 같은 전문 번역기, 즉 메서드를 써야만 하는 거였어!”
솔라는 방금 자신이 얻은 깨달음에 이름을 붙였다. ‘객체 메서드 변환’. 캐스팅 연산자 ()를 쓰는 게 아니라, 특정 객체(Integer, String)가 제공하는 전용 도구(메서드)를 사용해야만 하는 변환.
이제 ‘형 변환’이라는 단어는 솔라에게 세 가지 다른 얼굴로 보였다. 컴파일러가 조용히 처리해주는 안전한 자동 변환, 개발자가 책임지고 서명하는 위험한 강제 변환, 그리고 전혀 다른 세상 사이를 오가는 특별한 메서드 변환.
“그럼 이제 정리가 좀 되네. 자동, 강제, 그리고 이 메서드 변환. 그런데… 막상 코드를 딱 보면, 이게 어떤 종류의 변환인지, 뭘 조심해야 하는지 한눈에 알아보려면 어떻게 해야 할까? 나만의 진단 가이드 같은 게 있으면 좋겠는데.”
각각의 변환 원리를 이해하자, 이제는 그것들을 종합해서 실제 상황에 적용할 명확한 기준이 필요해졌다.
5장: 헷갈리지 마세요! 형 변환 진단 가이드
솔라는 책상 위에 펼쳐놓은 노트 위에서 펜을 든 채 멈춰 있었다. 노트 중앙에는 ‘형 변환’이라는 단어가 적혀 있었다. 그 주위로 ‘자동’, ‘강제’, ‘메서드’라는 세 개의 가지가 뻗어 나갔지만, 그 아래는 뒤죽박죽이었다. ‘안전’, ‘데이터 손실’, ‘(타입) 서명’, ‘번역 실패’, ‘예외?’ 같은 단어들이 정처 없이 떠다니며 서로 엉겨 붙어 있었다. 각각의 개념은 이해했지만, 한데 모아놓으니 무엇이 어떤 변환의 특징인지, 어떤 위험과 연결되는지 한눈에 들어오지 않았다. 마치 재료는 다 있는데, 어떤 요리에 어떤 양념을 넣어야 할지 모르는 요리사 같았다.
“이게 아닌데….”
솔라는 결국 펜을 내려놓고 의자 깊숙이 몸을 기댔다. 자동 변환은 안전한 이사, 강제 변환은 위험을 감수하는 이사, 메서드 변환은 아예 다른 차원의 건축. 비유는 명확했다. 하지만 실제 코드를 봤을 때, 이 변환이 ‘안전한 이사’인지, 아니면 ‘건축 실패’의 위험이 있는지 순식간에 판단할 수 있을까? 여전히 모든 ‘타입 변경’이 하나의 거대한 문제 덩어리처럼 느껴졌다.
그때, 솔라의 혼란스러운 노트를 본 루나가 조용히 자신의 노트북을 돌려 화면을 보여주었다. 화면에는 설명 하나 없이, 짧은 코드 조각들만 덩그러니 놓여 있었다. 마치 정체를 알 수 없는 약병들을 늘어놓은 것 같았다.
// 케이스 1
double a = 12;
// 케이스 2
String b = "3.14";
int c = (int) Double.parseDouble(b);
// 케이스 3
int d = 200;
byte e = (byte) d;
// 케이스 4
String f = "숫자일까요?";
int g = Integer.parseInt(f);
“진단 시간이야.”
루나가 말했다.
“이론을 정리하는 것보다, 실제 환자를 보고 진단하는 게 더 빠를 때가 있어. 저 코드들이 각각 어떤 종류의 변환이고, 어떤 일이 일어날지, 어떤 위험이 숨어있는지 한번 짚어볼래?”
솔라는 잠시 망설였다. 하지만 뒤죽박죽인 노트를 정리하는 것보다, 구체적인 사례를 마주하는 것이 더 나을 것 같았다. 그녀는 첫 번째 케이스부터 차근차근 살펴보기 시작했다.
“케이스 1. double a = 12;. 이건… 정수 12를 double 타입 변수에 넣고 있네. 작은 집에서 큰 집으로 가는 안전한 이사. ‘자동 형 변환’이야. 데이터 손실 위험은 없고, a에는 12.0이 들어갈 거야. 이건 쉽네.”
자신감이 붙은 솔라는 두 번째 케이스로 넘어갔다. 코드가 약간 더 복잡했다.
“케이스 2… Double.parseDouble(b)가 먼저 보이네. 이건 문자열 “3.14”를 숫자 3.14로 바꾸는 ‘메서드 변환’. 번역가를 부른 거네. 그리고 그 결과를 (int)로 ‘강제 형 변환’하고 있어. 두 가지가 섞여있구나.”
솔라는 잠시 숨을 골랐다. 각 단계에서 일어날 일을 머릿속으로 그려보았다.
“Double.parseDouble("3.14")가 실행되면 숫자 3.14가 돼. 그 다음에 (int) 3.14가 실행되니까, 소수점 아래가 잘려나가는 ‘데이터 손실’이 발생하겠네. 그럼 c에는 3이 들어가겠구나. 이건 위험을 감수하는 이사 과정에서 짐 일부를 버리는 상황이야.”
그녀는 세 번째 케이스를 보았다. 이번에는 숫자의 크기가 문제였다.
“케이스 3. int 타입 d를 byte 타입 e로 바꾸네. int는 큰 집, byte는 아주 작은 집. (byte) 서명으로 ‘강제 형 변환’을 하고 있어. 그런데 byte는 -128에서 127까지밖에 못 담잖아? 200은 그 범위를 넘어서. 이건… 소수점 손실이 아니라, 물이 넘쳐서 이상한 값으로 변하는 ‘오버플로우’가 발생하겠네. e에 200이 아니라 전혀 엉뚱한 숫자가 들어갈 거야.”
진단이 거듭될수록 솔라의 머릿속이 명료해졌다. 마지막 케이스에 다다랐을 때, 그녀는 이미 결과를 예측하고 있었다.
“케이스 4. Integer.parseInt(f). 이것도 ‘메서드 변환’. 그런데 번역하려는 내용이 “숫자일까요?”네. 이건 숫자로 번역할 수가 없잖아. 강제 변환처럼 데이터가 일부 손실되는 게 아니라… 이건 아예 작업 자체가 실패하겠어. 프로그램이 멈추거나 에러를 뿜어낼 거야. ‘변환 실패’ 위험이 있는 거지.”
네 개의 케이스를 모두 진단하고 나자, 솔라는 무언가 깨달은 듯 자신의 엉망진창인 노트를 다시 바라보았다. 그리고는 깨끗한 새 페이지를 펼쳤다. 이번에 그녀의 펜은 망설이지 않았다. 그녀는 질문을 적기 시작했다.
나만의 형 변환 진단 체크리스트
1단계: 변환의 종류부터 확인하자.
- 숫자끼리 바꾸나? (
int->double,double->int등)- Yes -> 2단계로 가기.
- 문자열(
String)과 숫자 사이에 변환이 일어나나?- Yes -> 이건 **‘메서드 변환’**이다. (
Integer.parseInt(),String.valueOf()등) - 🚨 위험 진단: 데이터 손실이 아니라, 변환 실패(Exception) 가능성을 의심해야 한다. (
"Hello"를 숫자로 바꿀 수 없는 것처럼!)
- Yes -> 이건 **‘메서드 변환’**이다. (
2단계: 숫자 변환의 방향을 확인하자.
- 작은 범위 타입에서 큰 범위 타입으로 가나? (
int->double,byte->long등)- Yes -> 이건 **‘자동 형 변환’**이다.
- ✅ 안전 진단: 컴파일러가 알아서 처리한다. 데이터 손실 걱정은 접어두자.
- 큰 범위 타입에서 작은 범위 타입으로 가나? (
double->int,int->byte등)- Yes -> 이건
(타입)서명이 필요한 **‘강제 형 변환’**이다. - 🚨 위험 진단: 데이터 손실을 각오해야 한다. (소수점이 잘리거나, 값이 망가지는 오버플로우가 발생할 수 있다!)
- Yes -> 이건
솔라는 자신이 만든 체크리스트를 만족스럽게 바라보았다. 더 이상 ‘형 변환’은 하나의 거대한 덩어리가 아니었다. 명확한 질문 두 개로 이루어진, 따라가기 쉬운 진단 절차였다. 코드를 볼 때 어떤 질문부터 던져야 할지, 각 경로에서 어떤 위험을 살펴야 할지가 분명해졌다.
이제 ‘데이터 손실’과 ‘변환 실패’는 헷갈리는 개념이 아니라, 각각 강제 변환과 메서드 변환이라는 다른 상황에서 나타나는 명백히 다른 종류의 위험으로 구분되었다. 솔라는 자신이 만든 체크리스트를 노트북 옆에 붙여두었다. 앞으로 어떤 코드 속에서 타입 변환을 만나더라도, 이 나침반이 길을 알려줄 것이라는 확신이 들었다.