<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tongmill_g.log</title>
        <link>https://velog.io/</link>
        <description>넘어가 울타리 green green</description>
        <lastBuildDate>Wed, 23 Sep 2026 11:43:08 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tongmill_g.log</title>
            <url>https://velog.velcdn.com/images/tongmill_g/profile/73724a70-336c-477c-ae81-82d816f8eedd/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tongmill_g.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tongmill_g" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AI Challenge 2 회고]]></title>
            <link>https://velog.io/@tongmill_g/AI-Challenge-2-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@tongmill_g/AI-Challenge-2-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 23 Sep 2026 11:43:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>3일간 AI Challenge 2를 진행한 후의 회고</p>
</blockquote>
<p>AI Challenge! 가 뭐냐면.
싸피에서 진행한 AI 챌린지 중 두 번째로, VQA 모델을 만들고 캐글에서 순위를 겨루는 AI 실전이다. 첫 번째 챌린지는 개인으로 하루 동안 진행되었고, 이번 두번째 챌린지는 넷 혹은 다섯이서 팀을 이뤄 함께 참여했다. 총 3일간 진행되었는데 휴일 기간에도 제출을 할 수 있어 사실상 아직 진행 중이다.
나는 알고리즘 스터디원들과 함께 넷이서 참여했다. 짧은 기간이었지만, 이번 챌린지는 이전에 혼자 모델을 학습시켰을 때와는 꽤 다른 경험으로 남았다.</p>
<p>첫 번째 참여했을 때는 성능을 높이기 위해 에폭이나 학습량을 바꾸고, 모델을 교체한 뒤 점수가 어떻게 달라지는지 확인하는 데 집중했다. 물론 그런 실험도 필요하지만, 당시에는 왜 점수가 달라졌는지 설명하거나 다음 실험의 방향을 정하는 일이 막막했다. 하지만 이번에는 달랐다. 팀원이 &quot;파이프 라인&quot;을 제안한 것이다. 모델 학습에 전략이 있다니.</p>
<p><code>이렇게 저렇게해서 요렇게 저렇게 하면 어떨까요. 이거랑 저거 써서... 아님 요거나 그거? 이래요래 해서 안되면 이러쿵 저러쿵......</code>
<br></p>
<p><img src="https://velog.velcdn.com/images/tongmill_g/post/de8ccf70-9735-4d39-96f5-19d83d741eb6/image.png" alt=""></p>
<p>네?</p>
<p>솔직히 9할 정도는 못 알아들었지만 지난 챌린지와 무언가 다르게 흘러가고 있다는 것은 알 수 있었다.</p>
<h2 id="1일차-069에서-시작">1일차, 0.69에서 시작</h2>
<p>우리 팀의 모델은 Qwen2.5-VL-3B-Instruct에 데이터 200개를 학습한 베이스라인에서 출발했다. 첫 정확도는 0.69였다.</p>
<p>우리의 전략은 이랬다. 모델에 이미지를 그대로 넣으면 필요한 정보를 놓칠 수 있으니 질문에 관련된 영역을 바운딩하고, 그 부분을 크롭해 모델에 전달하는 것이다. 처음에는 크롭한 이미지에서 OCR로 글자를 뽑고 선택지와 비교하려 했지만, OCR 모델을 찾는 과정에서 Qwen 자체의 문자 인식 성능이 좋다는 사실을 알게 됐다. 굳이 별도의 OCR 단계를 둘 필요가 없다는 결론이 나와 크롭 이미지를 Qwen이 직접 읽고 답하도록 방향을 바꿨다.</p>
<p>설계가 곧바로 원하는 대로 동작한 것은 아니었다. 모델이 반환한 바운딩 박스 좌표는 정규화된 값이라 실제 이미지에서 엉뚱한 위치를 가리켰다. 좌표를 픽셀 기준으로 보정해야 했다. 원본 이미지는 대부분 720픽셀 이상인데 추론 입력은 384픽셀이라 작은 글자가 뭉개지는 문제도 있었다. 해상도를 높이자 정답률도 함께 올라갔다.</p>
<p>이후에도 크고 작은 문제들이 있었는데 이 과정이 의외로 재미있었다. 계획한 파이프라인이 한 번에 완성되지는 않았지만 문제가 생길 때마다 결과를 확인하고 자료를 찾아보면서 설계를 조금씩 완성해 나갔다. 이론으로만 배울 때는 잘 느끼지 못했던 실전의 감각이 이런 것이 아닐까 싶었다.</p>
<h3 id="크롭은-항상-정답이-아니었다">크롭은 항상 정답이 아니었다</h3>
<p>Qwen3-VL-2B-Instruct로 train 데이터 중 무작위 500개를 추려 모든 문항에 크롭을 적용했을 때 정확도는 0.84였다. </p>
<p>처음에는 크롭을 적용하면 당연히 더 잘 읽을 거라고 생각했다. 결과는 그렇게 단순하지 않았다. 못 맞히던 문제를 맞히게 해주기도 했지만, 원래 맞힐 수 있던 문제를 틀리게 만들기도 했다.</p>
<p>[안내문, 절차, 긴 문장] 유형은 0.775에서 0.875로 올랐고, [고유명사, 상호, 장소명] 유형도 0.904에서 0.923으로 상승했다. 작은 글자나 특정 영역의 정보가 중요한 문제에서는 크롭이 실제로 도움이 된다는 걸 확인한 것이다. 반면 [제외, 부정 비교] 유형은 0.720에서 0.600으로 떨어졌다. 이미지를 잘라내는 과정에서 문제를 푸는 데 필요한 전체 맥락까지 사라졌기 때문이다.</p>
<p>공식 문서를 찾아보면서 Qwen이 여러 이미지를 함께 입력받을 수 있다는 점을 확인했다. 이후 크롭 이미지만 넣던 방식에서 원본 이미지와 크롭 이미지를 함께 넣는 방식으로 바꿨다. 원본으로 전체 맥락을 유지하고, 크롭으로 세부 정보를 보완하려는 선택이었다.</p>
<h2 id="2일차-점수가-곧-기분이-되다">2일차, 점수가 곧 기분이 되다</h2>
<p>모델에 전체 데이터의 절반을 학습했을 때 정확도는 0.866, 전체 데이터를 학습했을 때는 0.888이었다. 여기에 원본과 크롭 이미지를 함께 사용하는 전략을 적용하자 0.941까지 올랐다. 우리가 세운 가설이 실제 점수로 이어진 순간이라 정말 기뻤다. 학습량, 이미지 해상도, 입력 방식이 모두 의미가 있을 수 있다는 것을 결과로 확인했다.</p>
<p>그리고 드디어 모델을 변경해보기로 했다. 좋다는 소문이 파다한 Qwen3.5-9B. 기대가 너무 커서 점수가 낮게 나올 거라는 생각도 안했다. 사실 첫 추론에선 최대 생성 토큰 수 설정이 잘못돼 모든 답을 A로 내놓는 사건이 있었다. 0.2점 대의 대단한 점수도 기록으로 남았다. 이후 설정을 바로잡고 전체 데이터 파인튜닝과 크롭을 적용한 실험에서는 0.960이 나왔다. 기대했던 모델이 기대한 만큼의 점수를 냈을 때 느낀 기쁨은 생각보다 컸다.</p>
<p>반대의 순간도 있었다.</p>
<p><img src="https://velog.velcdn.com/images/tongmill_g/post/b330804c-6248-433f-9d17-480635f718e8/image.PNG" alt=""></p>
<p>위의 실험에 +프롬프팅 을 한 모델이 동시에 추론을 하고 있었다. 0.96을 본 우리는 불을 발견한 원시인 마냥 이 불에 구워질 고기를 기대하고 있었다. 하지만 파인튜닝 없이 프롬프트를 적용한 결과는 0.945였고, 파인튜닝과 프롬프트를 함께 적용한 결과도 0.959였다... 둘 다 파인튜닝과 크롭만 사용한 0.960을 넘지 못했다. 좋아 보이는 기술을 하나 더 얹는다고 성능이 반드시 좋아지지는 않았다. 기대를 많이 한 탓인지 약간 슬프기까지 했다.</p>
<p><img src="https://velog.velcdn.com/images/tongmill_g/post/28749564-4583-4feb-aea7-36d106c6943c/image.png" alt=""></p>
<h2 id="3일차-마지막까지">3일차, 마지막까지</h2>
<p>마지막에는 새로운 분류를 시도했다. 처음의 규칙 기반 분류는 질문에 특정 단어가 있는지를 보는 방식이라 범용성이 부족했다. 오답을 살펴봤을 때에도 기타 분류가 많았다. 그래서 먼저 LLM이 train 데이터의 문제 유형을 분류하게 하고, 텍스트와 숫자를 함께 이해해야 하는 어려운 문제만 Qwen3.8-27B로 보내는 분기 전략을 시도했다.</p>
<p>다만 Colab 자원이 거의 끝나가던 시점이라 충분히 검증하지 못한 채 급하게 추론을 진행해야만 했다. 결과는 0.959였다. 아쉬움은 남았지만, 큰 모델을 쓰는 것만으로 문제가 해결되지는 않는다는 사실을 다시 확인했다. 이 마지막 도전에 새로운 전략을 사용한 만큼 또 다시 좋은 점수를 기대했는데... 아쉬움은 남지만 전략이 유효한지 검증한 건 아니니 아직 다음이 있다.</p>
<p>3일 동안 점수 하나에 이렇게 기뻐하고 실망할 줄은 몰랐다. 점수가 오를 것이라고 확신했던 실험이 잘되면 신이 났고, 좋은 기술을 더했는데 오히려 내려가면 허탈했다. 그래도 그 감정까지 포함해서 재미있었다. 결과를 기다리는 시간이 길어질 수록 더 기대하게 되었고, 회의를 하며 오가는 아이디어를 듣고만 있어도 지식이 쌓이는 것만 같았다. 실제로 이 팀전에 내 기여도는 크지 않지만, 진행 중 가장 많은 것을 얻어간 사람은 나일지도.</p>
<h2 id="마치며">마치며,</h2>
<p><img src="https://velog.velcdn.com/images/tongmill_g/post/3479b11e-1d42-4f4f-9300-9f5f60b8916c/image.PNG" alt=""></p>
<p>이번 챌린지를 거치며 AI 모델에 대한 이해도가 확실히 높아졌다고 느낀다. 모델 구조나 파인튜닝을 이론으로 배운 것과는 달랐다. 이미지 해상도와 입력 구성, 프롬프트, 모델 설정이 서로 영향을 주고, 좋아 보이는 조건끼리도 충돌할 수 있다는 사실을 직접 겪었다. 완벽한 정답이 없는 문제에서 근거를 모아 다음 풀이를 정하는 경험이었다. 마지막에 다른 팀의 발표를 보면서도 많이 배웠다. 같은 데이터와 같은 목표를 두고도 접근 방법은 정말 다양했다. 우리가 떠올리지 못한 기발한 전략이 있었고, 실제로 점수를 높인 방법은 직접 다시 실험해 보고 싶었다. </p>
<p>혼자였다면 어쩌면 아예 시도조차 하지 못했을 거다. 팀원들과 함께였기 때문에 끊임없이 새로운 것을 들을 수 있었다. 서당개가 3년만에 풍월을 읊는데는 이유가 있다. 듣는 건 정말 큰 도움이 된다. 한 편으론 아쉬움도 남는다. 시작할 때 실험 계획과 역할을 조금 더 구체적으로 나눴더라면 시간 자원의 낭비 없이 더 많은 가설을 검증해 볼 수 있었을 것이다. 또 추석 연휴에 시간을 더 쓸 수 있었더라면 좋았을 것 같다. 아직 탐색할 수 있는 덜 밝혀진 맵이 눈 앞에 한참은 더 있는 느낌이다. </p>
<p>사실 최종 성적이 아주 높은 편은 아니었다. 그래도 0.69에서 시작해 처음 0.96 라는 점수를 확인했을 때는 정말 기뻤다. 반에서 박수를 치며 좋아했다. 반 꼴찌로 시작한 우리 모델이 (슬슬 자식으로 여겨졌다) 아주 크게 성장한 것만 같았다. 숫자 자체보다 우리가 세운 가설과 검증이 실제 개선으로 이어졌다는 사실이 좋았다. 물론 숫자도 좋았다.</p>
<p>나는 경험의 가치를 높게 두는 편이다. 새로운 경험일수록 점수를 높게 준다. 이번 3일은 완전히 새로운 스킬을 새로 해금한 즐거운 시간이었다. 연휴 기간 아직 캐글 대회가 열려 있기 때문에 우리팀의 열정이 넘치는 팀원들은 조금 더 시도를 해볼 성싶다. 정말 응원함!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트를 고민하며]]></title>
            <link>https://velog.io/@tongmill_g/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@tongmill_g/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%98%EB%A9%B0</guid>
            <pubDate>Fri, 18 Sep 2026 00:53:25 GMT</pubDate>
            <description><![CDATA[<h3 id="프로젝트에서-무슨-경험을-해보고-싶냐면">프로젝트에서 무슨 경험을 해보고 싶냐면...</h3>
<p>최근 프로젝트 이야기를 하다가 “백엔드 개발자로서 이번 프로젝트에서 어떤 부분을 경험해보고 싶냐”는 질문을 받았다. 바로 대답하기 어려웠다. Spring Boot를 사용해보고 싶다거나 Redis를 적용해보고 싶다거나... 기술 이름을 나열 할 순 있었겠지만 프로젝트에서 내가 정말 경험해 보고 싶은 부분이냐, 하면 그렇지 않은 것 같았다. 어떤 문제를 해결해보고 싶냐고? 새로운 문제요.</p>
<blockquote>
<p><em>새로운 문제가 뭔데?</em></p>
</blockquote>
<br>

<p>음</p>
<br>


<p>ㅎㅎ?</p>
<p><img src="https://velog.velcdn.com/images/tongmill_g/post/a62cfec9-84e6-4920-b626-3cb646c5b575/image.jpg" alt=""></p>
<p>정말 모르겠습니다.</p>
<p>아직 백엔드 프로젝트 경험이 많지 않아서 그런 것 같다. 데이터베이스도 더 깊게 다뤄보고 싶고, 캐시나 비동기 처리도 궁금하다. 직접 서버를 배포하고 로그를 보면서 문제를 찾는 과정도 경험해보고 싶다. 해본 것이 많지 않다 보니 오히려 뭘 해봐도 재미있을 것 같았고, 그래서 하나를 고르기가 더 어려웠다.</p>
<p>이런 고민을 이야기하다가 두 가지 조언을 들었다. 아직 뭘 좋아하는지 모르겠다면 채용공고를 보면서 기업이 백엔드 개발자에게 어떤 기술과 경험을 원하는지 살펴보는 것도 좋다는 것이었다. 또 하나는 막상 프로젝트가 끝난 뒤 기술적으로 무엇을 경험했는지 떠올려보려면 생각보다 어렵다는 이야기였다. (조언 감사합니다)
<br>
<br></p>
<h3 id="그래서-살펴본-채용-공고">그래서 살펴본 채용 공고</h3>
<blockquote>
<h3 id="메가스터디교육-백엔드-개발-공고"><a href="https://www.wanted.co.kr/wd/369313">메가스터디교육 백엔드 개발 공고</a></h3>
</blockquote>
<ul>
<li><strong>MySQL 스키마 설계, 쿼리 최적화 및 인덱스 관리</strong></li>
<li><strong>AWS 클라우드 환경에서 서비스 운영</strong></li>
</ul>
<p>단순히 API를 구현한 경험만이 아니라 데이터 구조를 직접 설계하고, 느린 쿼리를 개선하고, 완성한 서비스를 실제 환경에서 운영한 경험을 요구하고 있었다.
<br></p>
<blockquote>
<h3 id="이너버스-백엔드-개발자-공고"><a href="https://m.jobkorea.co.kr/Recruit/GI_Read/49935755?sc=621">이너버스 백엔드 개발자 공고</a></h3>
</blockquote>
<ul>
<li><strong>Core Java &amp; Framework</strong>
ㆍJava 8+ 실무 경험 필수 (주 개발 언어)
　- 람다, 스트림, CompletableFuture 등 비동기 처리
　- 멀티스레딩, 동시성 제어 및 성능 최적화
　- JVM 튜닝 및 메모리 관리
ㆍMaven/Gradle 빌드 도구 활용</li>
<li><strong>대용량 데이터 처리</strong>
ㆍElasticsearch 실무 경험 (인덱스 설계, 쿼리 최적화, 집계 쿼리)
ㆍ대용량 로그 수집/저장/검색 시스템 개발 경험
ㆍ실시간 데이터 파이프라인 구축 경험
ㆍ배치 처리 및 스트림 처리 아키텍처 설계
ㆍ효율적인 로그 파싱 및 정규화 로직 구현</li>
<li><strong>분산 시스템 &amp; 메시징</strong>
ㆍApache Kafka 또는 유사 메시징 시스템 실무 경험
ㆍ분산 환경에서의 데이터 일관성 및 트랜잭션 처리
ㆍ마이크로서비스 아키텍처 설계 및 구현
ㆍDocker/Kubernetes 기반 컨테이너 환경 경험
ㆍ서비스 간 통신 패턴 (REST, gRPC, 메시지 큐 등)</li>
<li>** 데이터베이스 **
ㆍRDBMS (PostgreSQL, MySQL 등) 설계 및 쿼리 최적화
ㆍNoSQL (Redis, MongoDB 등) 캐싱 및 세션 관리
ㆍ시계열 데이터베이스 경험 우대 (InfluxDB, TimescaleDB 등)
ㆍ대용량 데이터 파티셔닝, 샤딩, 인덱싱 전략</li>
</ul>
<p>다른 공고에 비해 필수 기술 역량이 자세히 적혀있어 전부 가져와 봤다. </p>
<br>

<blockquote>
<h3 id="다이렉트클라우드-채용공고"><a href="https://www.directcloudlab.co.kr/recruit/development">다이렉트클라우드 채용공고</a></h3>
<p><strong>・PHP 또는 Golang을 이용한 백엔드 개발 경험
・웹 서비스의 구조를 이해하고 개발해본 경험
・대용량 트래픽 경험
・서버리스 및 마이크로 아키텍쳐에 대한 이해・PHP 또는 Golang을 이용한 백엔드 개발 경험</strong></p>
</blockquote>
<p>서버를 개발하는 것과 함께 배포된 서비스를 운영하고, 운영 중 발생하는 문제의 원인을 찾는 경험을 중요하게 보고 있었다.
<br><br>
이렇게 공고를 살펴봤는데, 모든 공고가 Kafka나 Elasticsearch 같은 기술을 공통으로 요구하는 것은 아니었다. 이너버스 공고는 대용량 로그를 다루는 서비스의 특성상 데이터 처리와 분산 시스템에 관한 요구가 특히 자세한 편이었다.</p>
<p>그럼에도 여러 공고에서 비슷하게 확인할 수 있었던 것은 다음과 같았다.</p>
<ul>
<li>필요한 데이터를 고려해 <strong>DB 구조를 설계해본 경험</strong></li>
<li>느린 쿼리와 인덱스를 분석해 <strong>성능을 개선해본 경험</strong></li>
<li>많은 요청이나 데이터를 처리하면서 <strong>병목을 찾아본 경험</strong></li>
<li>서버를 배포하는 것에서 끝내지 않고 <strong>실제로 운영해본 경험</strong></li>
<li>오류나 장애가 발생했을 때 <strong>원인을 확인하고 해결해본 경험</strong></li>
<li>캐시, 비동기 처리, 메시지 큐 등을 <strong>문제에 맞게 선택해본 경험</strong></li>
</ul>
<p>Kafka, Elasticsearch, Kubernetes처럼 특정 공고에서 요구하는 기술도 있었지만, 결국 공통적으로 보고 있는 것은 기술 이름보다 그 기술을 이용해 어떤 문제를 해결해봤는지에 가까워 보였다.</p>
<p>그리고 이너버스 공고를 보면서는 또 다른 궁금증이 생겼다.</p>
<blockquote>
<p><em>저 많은 걸 백엔드 개발자 한 명이 다 할 줄 알아야 할까?</em></p>
</blockquote>
<p>찾아보니 회사와 팀의 규모에 따라 달랐다. 작은 팀에서는 한 명의 백엔드 개발자가 API 개발부터 DB 설계, 배포와 운영까지 넓게 담당하기도 한다. 반대로 규모가 큰 회사에서는 데이터 엔지니어, 검색 엔지니어, 인프라 엔지니어, SRE처럼 역할이 나뉘기도 한다.</p>
<p>Kafka 클러스터는 인프라팀에서 운영하고 백엔드 개발자는 메시지를 보내고 처리하는 로직을 만들 수도 있다. Kubernetes 환경은 플랫폼팀에서 관리하지만, 백엔드 개발자가 자신의 서버에 필요한 설정과 상태 확인 방법을 정할 수도 있다.</p>
<p>그러니까 공고에 적힌 기술을 한 사람이 전부 같은 깊이로 다뤄야 한다기보다는, 자신의 영역은 깊게 이해하고 주변 영역의 개발자와 함께 문제를 해결할 수 있어야 한다는 뜻에 가까웠다. 물론 정말로 한 명이 다 하는 회사도 있을 것이다..</p>
<br>

<h3 id="그래서-나는-무슨-경험을-해보고-싶은가">그래서 나는 무슨 경험을 해보고 싶은가</h3>
<p>공고를 살펴보고 나니 처음 질문에도 조금은 구체적으로 답할 수 있을 것 같았다. 아직 “나는 반드시 대용량 로그 처리 전문가가 될 거야!” 같은 목표가 생긴 것은 아니다. 여전히 안 해본 것이 많고, 그래서 이것저것 해보고 싶다.</p>
<p>그래도 단순히 서버를 한 번 만들어보는 것에서 끝내고 싶지는 않다. 오늘 글의 결론은 크게 두 가지 이다.</p>
<h4 id="1-정상-동작보다-장애-상황을-한-번-만들어보고-싶다">1. 정상 동작보다 장애 상황을 한 번 만들어보고 싶다.</h4>
<p>정해진 순서대로 요청을 보내고 예상한 응답을 받는 것만으로는 알 수 없는 문제가 많을 것 같다. 외부 API가 늦게 응답하거나, DB 연결이 끊기거나, 같은 데이터를 여러 사용자가 동시에 수정하는 상황을 직접 만들어보고 싶다.</p>
<ul>
<li>외부 API 응답을 일부러 늦추거나 실패시키기</li>
<li>DB 연결이 끊어진 상황 만들어보기</li>
<li>같은 데이터에 동시에 수정 요청 보내기</li>
<li>서버가 처리할 수 있는 양보다 많은 요청 보내기</li>
<li>작업을 처리하던 서버를 중간에 종료해보기</li>
</ul>
<p>그다음 timeout이나 retry를 적용하고, 트랜잭션과 락을 사용하면서 결과가 어떻게 달라지는지 확인해보고 싶다. 장애가 한 번도 나지 않는 서버를 만들겠다는 것보다 장애가 났을 때 원인을 찾을 수 있는 서버를 만들어보고 싶다.</p>
<h4 id="2-성능-개선을-숫자로-확인해보고-싶다">2. <strong>성능 개선을 숫자로 확인해보고 싶다.</strong></h4>
<p>“Redis를 사용해서 빨라졌다”, “인덱스를 추가해서 성능을 개선했다”라고만 적으면 정말로 얼마나 좋아졌는지 알 수 없다. 개선 전후를 직접 측정하고 비교해보고 싶다.</p>
<ul>
<li>데이터가 1만 건일 때와 100만 건일 때의 조회 시간</li>
<li>인덱스를 적용하기 전후의 응답 시간</li>
<li>캐시를 적용하기 전후의 DB 조회 횟수</li>
<li>동시에 여러 요청이 들어왔을 때의 처리량과 오류율</li>
<li>N+1 문제를 해결하기 전후의 쿼리 실행 횟수</li>
</ul>
<p>조회 API의 평균 응답 시간이 어떤 식으로 변했는지, 초당 처리 가능한 요청 수가 얼마나 늘었는지, 요청 한 번 당 실행되는 쿼리의 수를 줄일 수 있을지를 숫자로 체크하고 싶다.</p>
<br>

<h3 id="결론">결론</h3>
<p>아직 백엔드 경험이 많지 않아서 어떤 분야가 가장 재미있는지는 잘 모르겠다. 오히려 지금은 안 해본 것이 많아서 무엇을 해도 재미있을 것 같다.</p>
<p>그래도 이번 프로젝트에서 해보고 싶은 경험은 이전보다 조금 구체적으로 정리할 수 있게 됐다.</p>
<ul>
<li>정상 동작뿐 아니라 장애 상황도 직접 만들어보기</li>
<li>성능 개선 결과를 숫자로 확인하기</li>
</ul>
<p>또 어떤 기술을 사용할지 먼저 선택하기보단 문제를 먼저 확인하고 필요한 기술을 선택하고 싶다. 먼저 기술을 정해놓고 프로젝트를 진행하면 사용한 기술은 많아질 수 있겠지만 왜 사용했는지를 설명할 수 없다. 프로젝트가 끝난 뒤 Spring Boot와 Redis를 사용했습니다라고만 말하고 싶지는 않다. 어떤 문제가 있었고, 왜 그런 방법을 선택했으며, 적용한 뒤 무엇이 달라졌는지를 설명할 수 있었으면 좋겠다.</p>
<p>프로젝트 기대된당!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 01 | 백엔드 개발자는 무슨 일을 할까?]]></title>
            <link>https://velog.io/@tongmill_g/01-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%84-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@tongmill_g/01-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%84-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 15 Sep 2026 07:37:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>백엔드 개발자는 무슨 일을 할까?</p>
</blockquote>
<h2 id="백엔드-개발자를-향한-항해-01--백엔드-개발자는-무엇을-책임질까">백엔드 개발자를 향한 항해 01 | 백엔드 개발자는 무엇을 책임질까?</h2>
<p>요즘 Java로 알고리즘 문제를 풀고 있다. 문제를 읽고, 조건에 맞는 풀이를 생각하고, 코드를 작성해 결과를 확인하는 과정에는 조금씩 익숙해지고 있다.</p>
<p>이제는 그동안 공부한 내용을 하나의 서비스로 연결해 보고 싶어졌다. 이번에 만들어 보려는 것은 <strong>재고 CSV 파일을 검사하고 정해진 형식으로 바꿔 주는 서비스, DataCheck</strong>다. 아직 구현 전이며, 앞으로 이 프로젝트를 진행하면서 백엔드와 관련된 개념을 함께 공부해 보려고 한다.</p>
<p>파일을 읽고 잘못된 값을 찾는 기능은 Java 프로그램 하나로도 만들 수 있다. 그런데 여러 사용자가 웹에서 파일을 올리고, 이전 결과를 다시 확인하고, 실패한 작업을 재실행하도록 하려면 어떤 일이 더 필요할까?</p>
<p>첫 글에서는 이 질문을 출발점으로 백엔드 개발자의 책임을 정리해 봤다.</p>
<hr>
<p><strong>어떤 서비스를 만들려고 하는가</strong></p>
<p>DataCheck는 정해진 재고 CSV를 업로드하면 잘못된 데이터와 그 이유를 알려주고, 정상 데이터만 표준 형식으로 내려받게 하는 서비스다. CSV는 쉼표로 필드를 구분하는 텍스트 기반 데이터 형식이다. 따옴표로 감싼 값에는 쉼표나 줄바꿈도 들어갈 수 있어서, 단순히 쉼표마다 문자열을 잘라서는 정확하게 읽을 수 없다.</p>
<p>첫 버전은 품목 코드, 품목명, 수량, 창고 코드, 기록일이라는 다섯 열만 지원하려고 한다. 엑셀 파일이나 임의의 양식을 모두 처리하려고 하면 처음부터 범위가 너무 넓어질 것 같아 UTF-8 CSV 한 종류로 정했다.</p>
<p>예를 들어 수량이 음수라면 해당 행을 오류로 분류하고, 날짜가 <code>2026/09/16</code>이라면 실제로 존재하는 날짜인지 확인한 뒤 <code>2026-09-16</code>으로 바꾼다. 품목명에 쉼표가 들어 있어도 하나의 값으로 보존해야 한다. CSV 전용 파서가 파일의 구조를 읽고, 내가 작성할 검증기는 각 값이 업무 규칙에 맞는지 판단하도록 나눌 생각이다. <a href="https://commons.apache.org/proper/commons-csv/apidocs/index.html">Apache Commons CSV의 파싱·출력 설명</a></p>
<p>사용자는 로그인하고 파일을 제출한 뒤 진행 상태를 확인한다. 검사가 끝나면 정상 결과와 오류 보고서를 내려받고, 나중에 자신의 작업 이력을 다시 조회한다.</p>
<p>아직 실제 사용자의 반복 수요를 확인한 것은 아니다. 우선 합성 데이터로 기능과 실패 상황을 검증하는 학습 프로젝트로 시작하려고 한다.</p>
<p><strong>클라이언트와 서버: 파일을 보내는 쪽과 처리하는 쪽</strong></p>
<p>클라이언트(client)는 다른 프로그램의 기능이나 데이터를 요청하는 쪽이고, 서버(server)는 그 요청을 받아 처리하고 결과를 제공하는 쪽이다.</p>
<p>DataCheck에서는 사용자가 접속한 브라우저가 클라이언트 역할을 한다. 브라우저는 선택한 파일을 서버로 보내고, 이후 작업 상태나 결과 파일을 요청한다. 서버는 요청한 사용자를 확인하고, 파일을 보관하고, 검사를 진행하며, 요청에 맞는 응답을 돌려준다.</p>
<p>‘서버’라는 말은 프로그램과 컴퓨터를 모두 가리킬 수 있다. ‘Java로 서버를 만든다’고 할 때는 요청을 처리하는 프로그램을, ‘서버에 파일을 보관한다’고 할 때는 그 프로그램이 사용하는 컴퓨터나 저장 환경을 뜻하는 경우가 많다. 내 노트북에서 서버 프로그램과 브라우저를 함께 실행해도 둘의 역할은 구분된다.</p>
<p>통신에는 요청과 응답의 형식을 정한 약속이 필요하다. 웹에서 사용하는 대표적인 약속이 HTTP다. 파일을 보낸다는 행동도 실제로는 파일 내용을 담은 HTTP 요청으로 전달된다. 다만 모든 요청이 끝날 때까지 파일 검사도 반드시 끝나 있어야 하는 것은 아니다.</p>
<p>이 프로젝트에서 특히 구분해야 할 것이 <strong>‘접수되었다’와 ‘검사가 끝났다’</strong>다.</p>
<p>파일을 오래 검사해야 한다면 서버는 원본을 저장하고 작업 정보를 DB에 남긴 뒤, 우선 작업 ID를 돌려줄 수 있다. 이후 별도의 처리 코드가 검사를 수행하고 브라우저는 작업 ID로 상태를 조회한다. 이런 구조에서 접수 응답을 받았다는 것은 작업이 등록됐다는 뜻이다. 정상 결과를 내려받을 수 있다는 뜻은 아니다.</p>
<p>처음에는 요청이 끝나기 전에 검사까지 수행하는 동기 방식으로 만들어 보고, 기능이 맞게 동작하면 요청 접수와 실제 처리를 분리할 예정이다. 기다리는 흐름을 바꾸는 것이므로, 비동기로 바꿨다고 검사 알고리즘 자체가 빨라지는 것은 아니다.</p>
<p><strong>프런트엔드와 백엔드: 화면에 보이는 상태는 어디에서 결정될까?</strong></p>
<p>프런트엔드(frontend)는 사용자가 서비스를 보고 조작하는 부분을 구현한다. DataCheck에서는 파일 선택창, 업로드 버튼, 작업 목록, 오류 미리보기, 다운로드 버튼이 여기에 해당한다.</p>
<p>겉모양을 만드는 것 외에도 할 일이 있다. 제출 중 버튼이 여러 번 눌리지 않게 하고, 파일을 고르지 않았다면 안내하고, 서버가 알려준 작업 상태에 맞게 화면을 바꾸는 일도 프런트엔드가 담당한다.</p>
<p>백엔드(backend)는 요청을 해석하고, 서비스의 규칙에 맞게 처리하며, 필요한 데이터와 상태를 관리한다. 파일 크기를 검사하고, 사용자와 작업을 연결하고, 검사를 실행하며, 어떤 결과를 다운로드하도록 허용할지 결정하는 일이 여기에 해당한다.</p>
<p>프런트엔드에서 파일 크기를 확인하더라도 서버의 검사는 필요하다. 사용자가 화면 코드를 바꾸거나, 브라우저 대신 다른 도구로 직접 요청할 수도 있기 때문이다. 화면의 검사는 사용자에게 빠르게 안내하기 위한 것이고, 서버의 검사는 실제로 받아들여도 되는 요청인지 판단하기 위한 것이다.</p>
<p>다운로드 버튼을 숨기는 것만으로 다른 사람의 파일 접근을 막을 수도 없다. 버튼이 없어도 다운로드 주소로 직접 요청할 수 있으므로, 파일을 제공하는 순간 서버에서 소유권을 확인해야 한다.</p>
<p>두 영역은 <strong>API(Application Programming Interface)</strong>라는 접점을 통해 협력한다. API는 프로그램이 다른 프로그램의 기능을 어떤 방법으로 요청하고 어떤 결과를 받을지 정한 인터페이스다.</p>
<p>이 서비스에서는 ‘파일을 제출하면 작업 ID를 받는다’, ‘작업 ID로 현재 상태와 집계를 조회한다’, ‘성공한 작업의 결과를 요청하면 파일을 받는다’는 약속이 필요하다. 프런트엔드는 이 약속을 바탕으로 화면을 구성하고, 백엔드는 약속한 의미에 맞는 상태와 결과를 제공한다.</p>
<p>따라서 완료 화면을 보여주는 시점도 단순한 디자인 문제가 아니다. 업로드가 끝난 순간 화면에 ‘검사 완료’를 표시하면 실제 처리 상태와 어긋날 수 있다. 백엔드가 접수·진행·완료를 구분하고, 프런트엔드가 그 의미를 유지해야 한다.</p>
<p>클라이언트·서버가 통신에서의 역할을 설명한다면, 프런트엔드·백엔드는 구현의 관심사와 책임을 설명한다. 서버에서 화면을 만들어 보내는 방식도 있으므로 두 구분을 완전히 같은 뜻으로 외우지는 않으려고 한다.</p>
<p><strong>비즈니스 규칙: 어떤 데이터를 정상으로 볼 것인가</strong></p>
<p>비즈니스 규칙(business rule)은 서비스가 다루는 업무에서 지켜야 하는 조건과 정책이다. 여기서 비즈니스는 돈을 버는 활동만 뜻하지 않는다. DataCheck에서는 재고 데이터를 검사하고 정리하는 일이 서비스의 업무다.</p>
<p>‘수량은 0 이상의 정수여야 한다’, ‘품목 코드는 비어 있으면 안 된다’, ‘날짜는 실제로 존재해야 한다’는 조건이 규칙이다. 잘못된 행이 발견됐을 때 전체 파일을 거절할지, 그 행만 제외할지도 업무 규칙에 해당한다.</p>
<p>이 프로젝트에서는 개별 행의 값이 잘못된 경우 해당 행만 정상 결과에서 제외하고, 오류 보고서에 이유를 남기기로 했다. 하지만 헤더가 다르거나 CSV 구조가 깨져 내용을 신뢰할 수 없다면 파일 전체를 실패로 처리한다. 앞부분에 정상처럼 보이는 행이 있어도 완성된 결과로 공개하지 않는다.</p>
<p>같은 품목·창고·기록일이 반복되면 <strong>첫 번째 정상 행만 채택</strong>한다. 단순히 파일에서 먼저 나타났다는 이유만으로 잘못된 행을 기준으로 삼지는 않는다. 날짜 형식과 정해진 공백 처리를 먼저 적용한 뒤 중복 키를 비교해야 표현만 다른 같은 데이터를 구분할 수 있다.</p>
<p>예를 들어 아래는 네 레코드를 검사할 때 기대하는 결과다. 아직 실행 결과가 아니라, 구현이 맞는지 확인하기 위해 먼저 정한 기준이다.</p>
<table>
<thead>
<tr>
<th>레코드</th>
<th>입력의 특징</th>
<th>기대 결과</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>P-001, 정상 수량, WH-A, 2026/09/16</td>
<td>정상. 날짜를 2026-09-16으로 변환</td>
</tr>
<tr>
<td>2</td>
<td>P-002, 빈 품목명, 음수 수량, WH-A, 2026-09-16</td>
<td>오류 행 1개, 오류 항목 2개</td>
</tr>
<tr>
<td>3</td>
<td>P-001, 정상 수량, WH-A, 2026-09-16</td>
<td>1번과 같은 키이므로 중복 오류</td>
</tr>
<tr>
<td>4</td>
<td>P-002, 정상 품목명·수량, WH-A, 2026-09-16</td>
<td>정상. 2번은 잘못된 행이므로 이 키의 첫 정상 행은 4번</td>
</tr>
</tbody></table>
<p>전체는 4행, 정상은 2행, 오류는 2행이다. 오류 항목은 2번의 두 문제와 3번의 중복 문제를 합쳐 3개다. <strong>오류 행 수와 오류 항목 수는 서로 다르다.</strong></p>
<p>이런 규칙을 실제 판단과 처리 순서로 옮긴 부분을 비즈니스 로직(business logic)이라고 한다. 입력을 정리하고, 필수값과 범위를 검사하고, 정상 후보의 중복을 판단한 뒤 출력 대상을 나누는 과정이 여기에 해당한다.</p>
<p>CSV 라이브러리가 따옴표나 쉼표를 읽어 주더라도, 음수 수량을 허용할지와 중복 중 무엇을 남길지는 대신 결정해 주지 않는다. 코드를 작성하기 전에 정책과 기대 결과를 정리해야 하는 이유다.</p>
<p><strong>영속성: 검사 결과와 작업 이력은 어디에 남을까?</strong></p>
<p>영속성(persistence)은 프로그램 실행이 끝난 뒤에도 데이터를 보존하고 다시 사용할 수 있는 성질이다.</p>
<p>검사 결과를 Java의 <code>ArrayList</code>에만 담아 두면 실행 중에는 사용할 수 있다. 하지만 프로세스를 종료하고 다시 시작했을 때 이전 목록이 자동으로 돌아오지는 않는다. 사용자가 다음 날 작업 이력을 확인하거나 결과를 다시 내려받으려면 메모리 밖에 정보를 남겨야 한다.</p>
<p>이번 프로젝트에서는 파일과 DB가 서로 다른 정보를 보관한다. 원본 CSV, 정상 결과 CSV, 전체 오류 보고서는 파일로 남긴다. DB에는 누가 제출했는지, 파일이 어디에 있는지, 작업 상태가 무엇인지, 언제 어떤 시도로 처리했는지와 같은 정보를 저장한다.</p>
<p>이처럼 데이터에 관한 설명을 <strong>메타데이터(metadata)</strong>라고 한다. 파일의 저장 키, 크기, 소유자, 생성 시각은 파일 내용 자체가 아니라 파일을 관리하기 위한 정보다. 결과 행을 하나씩 검색하는 기능이 지금 필요하지 않으므로 모든 CSV 행을 DB에 저장하지 않을 생각이다.</p>
<p>사용자가 요청한 하나의 검사를 ‘작업’으로, 실제로 처리한 한 번을 ‘시도’로 나누는 것도 중요하다. 서버 오류 때문에 재실행하면 같은 작업 아래 두 번째 시도가 생긴다. 첫 번째 실패 기록을 지워 버리면 무슨 문제가 있었는지 설명하기 어려워진다.</p>
<p>여기서 주의할 점은 <strong>파일 저장과 DB 저장이 자동으로 함께 성공하거나 취소되지는 않는다는 것</strong>이다. 결과 파일을 만드는 데 성공했지만 DB에 완료 상태를 기록하기 전에 서버가 종료될 수도 있다. 반대로 DB에 파일 경로가 있어도 실제 파일이 없어질 수 있다.</p>
<p>DB의 <strong>트랜잭션(transaction)</strong>은 여러 DB 변경을 하나의 처리 단위로 관리한다. 변경을 확정하는 것을 커밋(commit), 취소하는 것을 롤백(rollback)이라고 한다. 하지만 일반적인 DB 트랜잭션을 취소했다고 별도로 작성한 파일까지 자동으로 삭제되지는 않는다. <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-autocommit-commit-rollback.html">MySQL의 커밋·롤백 설명</a></p>
<p>따라서 결과 파일은 임시 위치에 작성하고, 처리가 끝난 뒤 확정 위치로 옮기며, DB에도 결과 참조와 성공 상태를 함께 기록하는 방식으로 설계하려고 한다. 사용자에게는 성공 상태로 확정된 결과만 제공한다. 그 사이에 생긴 불필요한 파일을 어떻게 찾아 정리할지도 별도로 필요하다.</p>
<p>영속성은 모든 파일을 영원히 보관한다는 뜻은 아니다. 이번 서비스에서는 기본 7일 보관 정책을 두고, 실행 중인 작업은 삭제 대상에서 제외할 예정이다. 파일이 만료됐다는 상태와 검사가 실패했다는 상태도 구분해야 한다.</p>
<p><strong>보안: 내 파일을 다른 사람이 볼 수 없어야 한다</strong></p>
<p>보안(security)은 데이터와 기능을 허용되지 않은 접근이나 변경으로부터 보호하는 일이다. 파일 검사 서비스에서는 사용자가 제출한 파일과 결과가 다른 사용자에게 노출되지 않도록 하는 것이 중요한 책임이다.</p>
<p>인증(authentication)은 요청한 주체가 누구인지 확인하는 과정이다. 인가(authorization)는 확인된 주체가 특정 행동을 할 권한이 있는지 판단하는 과정이다. 로그인에 성공했더라도 다른 사용자의 파일을 내려받을 권한까지 얻는 것은 아니다.</p>
<p>사용자 A와 B가 각각 파일을 제출했다고 가정해 보자. B가 A의 작업 ID를 알아냈더라도 상세 정보와 결과 파일을 볼 수 없어야 한다. 재실행 요청 역시 차단해야 한다. 요청에 적힌 사용자 번호를 그대로 믿지 않고, 서버가 검증한 로그인 정보와 저장된 작업의 소유자를 비교해야 한다.</p>
<p>파일 자체도 확인해야 한다. 확장자가 <code>.csv</code>라고 해서 형식이 올바르거나 안전한 것은 아니다. 허용한 크기인지, 지원하는 형식으로 읽을 수 있는지 검사해야 한다. 사용자가 보낸 파일명을 저장 경로로 그대로 사용하지 않고, 서버가 만든 저장 키로 비공개 위치에 보관하는 방식을 사용할 예정이다. <a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html">OWASP의 파일 업로드 보호 지침</a></p>
<p>결과를 CSV로 내려주는 과정에도 살펴볼 점이 있다. 자유 텍스트에 수식으로 해석되는 값이 들어 있으면 스프레드시트에서 파일을 열 때 원래 의도와 다르게 동작할 수 있다. 오류 보고서에 원본 값을 그대로 복사하는 경우도 마찬가지다.</p>
<p>다만 모든 값에 임의의 문자를 붙이면 다른 프로그램이 읽을 데이터까지 달라진다. 기계가 처리할 값과 사람이 볼 표시용 값을 구분하고, 어떤 프로그램에서 안전성을 확인했는지 기록해야 한다. 모든 스프레드시트와 재저장 방식에 통하는 만능 처리라고 가정하지 않으려고 한다. <a href="https://community.owasp.org/attacks/CSV_Injection">OWASP의 CSV Injection 설명</a></p>
<p><strong>신뢰성: 오류가 있어도 결과의 의미는 정확해야 한다</strong></p>
<p>신뢰성(reliability)은 정해진 조건에서 서비스가 기대한 기능을 일관되게 수행하는 성질이다. 이 프로젝트에서는 올바른 파일을 잘 처리하는 것과 함께, 잘못된 데이터나 서버 중단을 만났을 때 어떤 상태를 남기는지도 중요하다.</p>
<p>먼저 <strong>데이터 오류와 서버 장애를 구분</strong>해야 한다. 음수 수량을 찾아 오류 보고서에 남기는 것은 검증기가 해야 할 일을 수행한 결과다. 일부 행이 잘못되었더라도 검사가 끝났다면 ‘완료, 오류 있음’으로 표시할 수 있다. 데이터 행은 있는데 정상 행이 하나도 없는 경우도 여기에 포함한다.</p>
<p>반면 결과를 쓰다가 저장 장치 오류가 나거나 처리 도중 서버가 종료됐다면 검사를 정상적으로 끝내지 못한 것이다. 이런 상태를 완료로 표시하거나 미완성 결과를 내려주면 사용자는 잘못된 결과를 믿게 된다.</p>
<p>작업 상태는 다음처럼 구분할 예정이다.</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>QUEUED</td>
<td>원본과 작업 정보가 접수되어 처리를 기다리는 중</td>
</tr>
<tr>
<td>RUNNING</td>
<td>워커가 작업을 가져가 처리하는 중</td>
</tr>
<tr>
<td>SUCCEEDED</td>
<td>검사 완료, 모든 행 정상, 확정 결과 제공 가능</td>
</tr>
<tr>
<td>SUCCEEDED_WITH_ERRORS</td>
<td>검사 완료, 오류 행이 있으며 정상 결과와 오류 보고서 제공 가능</td>
</tr>
<tr>
<td>FAILED</td>
<td>파일 구조 문제 또는 서버 오류 등으로 전체 처리를 완료하지 못함</td>
</tr>
</tbody></table>
<p>FAILED라는 상태만 보고 모두 재실행해서는 안 된다. 날짜나 파일 구조가 잘못됐다면 같은 입력을 다시 처리해도 해결되지 않는다. 사용자가 파일을 수정해 새 작업으로 제출해야 한다. 서버 오류로 실패한 경우에만 같은 원본으로 다시 실행하도록 하고, 실패 원인 코드를 함께 남길 생각이다.</p>
<p>중복 요청도 신뢰성과 연결된다. 사용자가 업로드를 눌렀지만 응답을 받지 못해 다시 보냈다고 하자. 서버에서는 첫 번째 요청을 이미 접수했을 수 있다. 이때 요청마다 새 작업을 만들면 같은 제출 의도가 여러 작업으로 늘어난다.</p>
<p>이를 구분하기 위해 하나의 제출 의도에는 같은 <strong>요청 키</strong>를 사용한다. 같은 사용자·같은 키·같은 내용이면 기존 작업을 알려주고, 같은 키인데 내용이 바뀌었다면 충돌로 안내한다. 반대로 사용자가 의도적으로 새 작업을 만들었다면 파일이 같더라도 허용한다. 파일 내용이 같은 것과 요청이 중복된 것은 다른 문제다.</p>
<p>서버가 처리 중 종료된 경우에도 기준이 필요하다. 이번에는 한 애플리케이션과 한 워커로 범위를 제한하고, 완전히 종료된 뒤 재시작했을 때 남은 실행 중 작업을 실패로 정리하려고 한다. 이후 사용자가 새로운 시도로 전체 재실행할 수 있게 한다. 중간 레코드부터 이어서 처리하는 기능은 첫 목표에 넣지 않았다.</p>
<p><strong>개발과 운영: 만든 기능을 계속 사용할 수 있게 하기</strong></p>
<p>개발은 요구사항을 정리하고 코드를 작성하며 의도한 동작인지 검증하는 일이다. DataCheck에서는 정상 CSV 하나를 변환하는 것과 함께, 여러 오류가 있는 행이나 잘못된 헤더도 예상한 방식으로 처리하는지 확인해야 한다.</p>
<p>배포(deployment)는 만든 프로그램을 사용자가 접근하는 환경에 반영하는 일이다. 내 컴퓨터에서는 되던 기능도 파일 경로, DB 연결, 메모리 제한, 폴더 권한이 달라지면 동작하지 않을 수 있다. 실행 환경을 정리하고 배포한 뒤 실제 흐름을 확인하는 일도 필요하다.</p>
<p>운영(operations)은 제공 중인 서비스의 상태를 살피고 변경과 장애에 대응하는 일이다. 파일 검사 서비스에서는 대기 작업이 계속 쌓이지 않는지, 실패가 늘지 않는지, 보관 파일이 저장 공간을 모두 차지하지 않는지 살펴야 한다.</p>
<p>로그(log)는 프로그램에서 일어난 사건의 기록이다. 작업 ID, 시도 번호, 상태 변화, 오류 코드와 소요 시간을 남기면 특정 작업이 어디서 멈췄는지 추적할 수 있다. 원본 행 전체나 비밀번호까지 기록할 필요는 없다.</p>
<p>모니터링(monitoring)은 서비스 상태를 지속적으로 확인하는 활동이다. 작업 대기 수, 실패 수, 처리 시간, 사용 중인 저장 공간을 관찰하면 문제가 생겼는지 판단하는 데 도움이 된다. 모니터링할 값도 ‘있으면 좋아 보이는 수치’보다 실제로 어떤 조치를 결정하는 데 쓸지를 생각하며 고르려고 한다.</p>
<p>성능을 설명할 때도 측정이 필요하다. 파일을 접수하는 데 걸린 시간과 검사를 끝내는 데 걸린 시간을 따로 보고, 같은 데이터로 반복 측정해야 한다. 처음 설정한 20 MiB·10만 행 제한은 검증을 위한 초기 상한이며, 지금 그 규모를 충분히 처리한다고 확인한 수치는 아니다.</p>
<p>파일을 한꺼번에 읽지 않고 레코드 단위로 읽으면 메모리 부담을 줄일 수 있다. 그래도 중복 판정용 Set에는 고유 키가 남기 때문에 모든 메모리 사용이 일정해진다고 말할 수는 없다. 개선 결과도 실제로 측정한 조건과 함께 기록할 예정이다.</p>
<p>운영 책임에는 복구 가능한 범위를 설명하는 일도 포함된다. 영속 볼륨을 사용하면 앱을 다시 실행할 때 파일을 유지할 수 있지만, 그것만으로 저장 장치 장애에 대비한 백업까지 갖춘 것은 아니다. 이번 프로젝트에서 확인한 재시작 복구와 아직 보장하지 못하는 상황을 구분해야 한다.</p>
<p>백엔드 개발자 한 사람이 모든 인프라와 보안을 혼자 담당한다는 뜻은 아니다. 팀마다 역할은 다르다. 다만 내가 만든 코드가 어디에 상태를 남기고, 어떤 조건에서 실패하며, 운영 중 무엇을 확인해야 하는지는 이해할 필요가 있다.</p>
<p><strong>이 서비스에서 나눠야 할 책임</strong></p>
<table>
<thead>
<tr>
<th>사용자 행동·상황</th>
<th>프런트엔드의 역할</th>
<th>백엔드의 역할</th>
</tr>
</thead>
<tbody><tr>
<td>파일 제출</td>
<td>파일 선택과 제출 상태를 보여준다.</td>
<td>사용자·허용 크기·입력을 확인하고 파일과 작업을 접수한다.</td>
</tr>
<tr>
<td>작업 상태 확인</td>
<td>접수·대기·처리·완료·실패를 구분해 표시한다.</td>
<td>저장된 상태와 확인된 집계를 제공한다.</td>
</tr>
<tr>
<td>오류 확인</td>
<td>데이터 레코드 번호와 이유를 읽기 쉽게 보여준다.</td>
<td>행 오류와 전체 실패를 구분하고 오류 항목과 행 수를 정확히 집계한다.</td>
</tr>
<tr>
<td>결과 다운로드</td>
<td>제공 가능한 결과와 만료 여부를 안내한다.</td>
<td>소유권·완료 상태·확정 결과·보관 상태를 확인한다.</td>
</tr>
<tr>
<td>같은 요청 재전송</td>
<td>같은 제출에는 같은 요청 키를 사용한다.</td>
<td>DB 제약과 요청 지문으로 중복 접수를 통제한다.</td>
</tr>
<tr>
<td>타인 작업 접근</td>
<td>허용된 메뉴와 결과만 보여준다.</td>
<td>직접 보낸 요청도 검사해 조회·다운로드·재실행을 차단한다.</td>
</tr>
<tr>
<td>처리 중 서버 종료</td>
<td>다음 조회에서 실패 상태와 가능한 행동을 안내한다.</td>
<td>미완성 결과를 공개하지 않고 실패 이력을 보존한다.</td>
</tr>
</tbody></table>
<p>이 표를 정리하면서, 파일을 바꾸는 규칙 외에도 정해야 할 약속이 많다는 것을 알게 됐다. 상태와 응답의 의미를 정하는 일도 구현의 일부다.</p>
<p><strong>알고리즘 공부와 어떻게 이어질까?</strong></p>
<p>알고리즘 문제에서는 정해진 입력 조건 안에서 정확하고 효율적인 결과를 구하는 데 집중한다. DataCheck에서도 자료구조와 시간 복잡도는 필요하다. 중복 키를 찾는 Set, 결과를 순서대로 처리하는 방식, 데이터 크기에 따른 메모리 사용량이 지금까지 공부한 내용과 연결된다.</p>
<p>여기에 서비스에서는 입력 형식이 잘못될 수 있고, 같은 요청이 동시에 들어올 수 있으며, 실행이 끝난 뒤에도 파일과 이력이 남아야 한다는 조건이 붙는다. 결과 계산뿐 아니라 그 결과를 누가 보고, 언제 공개하며, 실패하면 무엇을 남길지도 정해야 한다.</p>
<p>앞으로 기능을 하나 만들 때마다 ‘정답이 나오는가’와 함께 ‘이 요청을 허용해도 되는가’, ‘데이터가 어디에 남는가’, ‘중간에 멈추면 어떤 상태가 되는가’를 확인하려고 한다.</p>
<p><strong>이번 글에서 정리한 것</strong></p>
<table>
<thead>
<tr>
<th>개념</th>
<th>뜻</th>
<th>DataCheck에서의 활용</th>
</tr>
</thead>
<tbody><tr>
<td>클라이언트·서버</td>
<td>요청하는 쪽과 처리·응답하는 쪽</td>
<td>브라우저가 파일을 보내고 서버가 작업을 접수·처리한다.</td>
</tr>
<tr>
<td>프런트엔드·백엔드</td>
<td>사용자 상호작용과 규칙·데이터 처리의 책임 구분</td>
<td>화면은 상태를 표현하고 서버는 실제 상태·권한·결과 공개를 판단한다.</td>
</tr>
<tr>
<td>비즈니스 규칙·로직</td>
<td>지켜야 할 정책과 이를 적용하는 처리 과정</td>
<td>행 검증, 첫 정상 행 채택, 오류 집계, 재실행 허용 조건을 구현한다.</td>
</tr>
<tr>
<td>영속성</td>
<td>실행 이후에도 데이터를 보존하고 재사용하는 성질</td>
<td>파일은 원본·결과를, DB는 소유자·작업·시도 이력을 보관한다.</td>
</tr>
<tr>
<td>보안</td>
<td>허용되지 않은 접근과 변경으로부터 보호</td>
<td>로그인뿐 아니라 작업별 소유권, 저장 위치, 내보내기를 검사한다.</td>
</tr>
<tr>
<td>신뢰성</td>
<td>정해진 조건에서 기대한 기능과 결과의 의미를 유지</td>
<td>접수와 완료를 구분하고, 중복 요청과 중단에도 미완성 결과를 공개하지 않는다.</td>
</tr>
<tr>
<td>개발·운영</td>
<td>기능을 만들고 검증하며 실제 환경에서 유지·복구</td>
<td>테스트·배포·로그·성능 측정·파일 정리와 복구 범위를 관리한다.</td>
</tr>
</tbody></table>
<p>백엔드 개발에는 요청을 처리하는 코드와 함께, <strong>무엇을 정상으로 볼지, 데이터를 어디에 남길지, 실패했을 때 사용자에게 무엇을 알려줄지 정하는 일</strong>도 포함된다. 이번 프로젝트에서는 이 책임을 파일 하나가 들어와 결과로 나가는 흐름 안에서 직접 확인해 보려고 한다.</p>
<p>다음 글에서는 먼저 Spring 없이 Java로 검증기를 만들 예정이다. 한 행의 데이터와 오류를 어떤 객체로 표현할지, 첫 정상 행을 기억하려면 어떤 키를 Set에 넣어야 할지, 오류 항목 수와 오류 행 수를 어떻게 구분할지부터 살펴보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 문제에서 사용하는 입력·출력 완전 정리]]></title>
            <link>https://velog.io/@tongmill_g/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9E%85%EB%A0%A5%EC%B6%9C%EB%A0%A5-%EC%99%84%EC%A0%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tongmill_g/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9E%85%EB%A0%A5%EC%B6%9C%EB%A0%A5-%EC%99%84%EC%A0%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 12 Sep 2026 09:57:29 GMT</pubDate>
            <description><![CDATA[<p>입력으로 다음 숫자가 주어진다.
3
123
456
789</p>
<p>이 입력에서 숫자 5를 불러오려면... 그러니까 입력을 먼저 받아야 하는데......
입력...
입력을 어떻게 받지??????</p>
<p>알고리즘 공부하지 2개월 넘어가는데 입력에서 막혔다. next랑 nextline 차이가 뭐더라?</p>
<p>ㅎㅎ</p>
<p>~ 그래서 시작하는 입력부터 다시 공부하는 알고리즘 세상 ~</p>
<p>이 포스트는 아직도 scanner를 사용해 입력을 받는 나를 위해 쓰였다.</p>
<hr>
<p>자바 알고리즘 입출력, 입력 형태별로 익히기</p>
<p>알고리즘 문제에는 숫자, 문자열, 격자, 명령어처럼 다양한 입력이 나온다. 입력 코드를 쓸 때는 데이터를 어떤 단위로 읽을지부터 정하자.</p>
<ul>
<li>공백으로 구분된 값 하나는 <strong>토큰 단위</strong>로 읽는다.</li>
<li>공백을 포함한 문장은 <strong>줄 단위</strong>로 읽는다.</li>
<li>붙어 있는 숫자나 문자는 <strong>문자열로 읽은 뒤 문자별로 나눈다.</strong></li>
</ul>
<p>이 기준을 바탕으로 <code>Scanner</code>의 기본 동작부터 자주 나오는 입력 유형, <code>BufferedReader</code>와 출력 방법까지 살펴보자.</p>
<h3 id="1-입력-단위와-scanner-기본-설정">1. 입력 단위와 Scanner 기본 설정</h3>
<p>입력 예시를 보면 먼저 공백과 줄바꿈이 어떤 역할을 하는지 살펴보자. 아래는 기본 구분자 설정을 쓰는 <code>Scanner</code> 기준이다.</p>
<table>
<thead>
<tr>
<th>읽을 데이터</th>
<th>메서드 또는 처리 방식</th>
</tr>
</thead>
<tbody><tr>
<td>정수 / 큰 정수 / 실수</td>
<td><code>nextInt()</code> / <code>nextLong()</code> / <code>nextDouble()</code></td>
</tr>
<tr>
<td>공백 없는 문자열</td>
<td><code>next()</code></td>
</tr>
<tr>
<td>공백을 포함한 문장</td>
<td><code>nextLine()</code></td>
</tr>
<tr>
<td>문자 하나</td>
<td><code>next().charAt(0)</code></td>
</tr>
<tr>
<td>붙어 있는 문자나 숫자</td>
<td>문자열로 읽고 <code>charAt()</code> 또는 <code>toCharArray()</code></td>
</tr>
<tr>
<td>공백으로 구분된 숫자</td>
<td><code>nextInt()</code> 반복</td>
</tr>
</tbody></table>
<p>숫자처럼 보여도 <code>00123</code>의 앞자리 <code>0</code>이나 각 자릿수가 필요하다면 문자열로 읽는다. 입력량이 많을 때는 시간 제한도 함께 보고 <code>BufferedReader</code> 사용을 고려한다.</p>
<h4 id="scanner-기본-코드">Scanner 기본 코드</h4>
<pre><code class="language-java">import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        int n = sc.nextInt();

        System.out.println(n);
    }
}</code></pre>
<p>이후의 <code>Scanner</code> 예제는 다음 객체가 생성되어 있다고 가정한다.</p>
<pre><code class="language-java">Scanner sc = new Scanner(System.in);</code></pre>
<p>각 예제는 별개의 입력을 기준으로 작성했다.</p>
<h3 id="2-next-nextint-nextline의-차이">2. next(), nextInt(), nextLine()의 차이</h3>
<p>중요!!</p>
<p><code>next()</code>와 <code>nextInt()</code>는 <strong>토큰 하나</strong>, <code>nextLine()</code>은 <strong>현재 위치부터 줄 끝까지</strong> 읽는다. 토큰은 기본적으로 공백이나 줄바꿈으로 구분되는 값이다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>읽는 단위</th>
<th>앞의 공백·줄바꿈</th>
</tr>
</thead>
<tbody><tr>
<td><code>next()</code></td>
<td>문자열 한 토큰</td>
<td>건너뜀</td>
</tr>
<tr>
<td><code>nextInt()</code></td>
<td>정수 한 토큰</td>
<td>건너뜀</td>
</tr>
<tr>
<td><code>nextLine()</code></td>
<td>현재 위치부터 줄 끝까지</td>
<td>건너뛰지 않음</td>
</tr>
</tbody></table>
<p><code>hello java world</code>를 <code>next()</code>로 읽으면 <code>&quot;hello&quot;</code>만 가져온다. 다음 두 번의 호출에서 <code>&quot;java&quot;</code>, <code>&quot;world&quot;</code>를 읽는다. 같은 입력을 처음부터 <code>nextLine()</code>으로 읽으면 한 줄 전체를 가져온다.</p>
<pre><code class="language-java">String line = sc.nextLine(); // &quot;hello java world&quot;</code></pre>
<p><code>nextLine()</code>은 마지막 줄바꿈도 소비하지만, 반환하는 문자열에는 줄바꿈을 넣지 않는다.</p>
<h4 id="nextint끼리는-줄바꿈을-신경-쓰지-않아도-된다">nextInt()끼리는 줄바꿈을 신경 쓰지 않아도 된다</h4>
<pre><code class="language-java">int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();</code></pre>
<p>이 코드는 <code>5 6 7</code>이 한 줄에 있든, 숫자마다 줄이 바뀌든 똑같이 읽는다. 숫자 뒤의 줄바꿈은 남지만 다음 <code>nextInt()</code>가 건너뛰기 때문이다.</p>
<h4 id="nextline으로-바꿀-때는-현재-위치를-확인하자">nextLine()으로 바꿀 때는 현재 위치를 확인하자</h4>
<pre><code>3
123
456
789</code></pre><pre><code class="language-java">int n = sc.nextInt();       // 3
String row = sc.nextLine(); // &quot;&quot;</code></pre>
<p><code>3</code>을 읽은 직후에는 줄바꿈이 남아 있다. <code>nextLine()</code>은 그 자리에서 바로 줄바꿈을 만나므로 빈 문자열을 반환한다. 다음 줄을 읽으려면 현재 줄의 나머지를 먼저 소비한다.</p>
<pre><code class="language-java">int n = sc.nextInt();
sc.nextLine(); // 첫 줄의 나머지와 줄바꿈 소비
String row = sc.nextLine(); // &quot;123&quot;</code></pre>
<p><strong>숫자 다음에 무조건 <code>nextLine()</code>을 넣는 것은 아니다. 다음에 읽을 데이터가 어느 줄에 있는지 보고 결정한다.</strong></p>
<p>국룰 조합은 </p>
<ol>
<li>nextInt()+nextLine()+nextLine() </li>
<li>nextInt()+next() </li>
</ol>
<h3 id="3-정수-n개-큰-정수와-실수-읽기">3. 정수 N개, 큰 정수와 실수 읽기</h3>
<p>배열이나 정렬 문제에서는 먼저 개수 <code>N</code>을 읽고, 그만큼 반복해서 값을 받으면 된다.</p>
<pre><code>5
10 20 30 40 50</code></pre><pre><code class="language-java">int n = sc.nextInt();
int[] arr = new int[n];

for (int i = 0; i &lt; n; i++) {
    arr[i] = sc.nextInt();
}</code></pre>
<p>숫자가 여러 줄에 나뉘어 있어도 같은 코드로 읽는다. 각 값의 줄 위치보다 정수 <code>N</code>개를 순서대로 읽는 것이 중요한 경우에 쓴다.</p>
<h4 id="큰-정수와-실수">큰 정수와 실수</h4>
<pre><code class="language-java">long value = sc.nextLong();
double ratio = sc.nextDouble();</code></pre>
<p><code>int</code>의 범위를 넘는 정수는 처음부터 <code>long</code>으로 읽어야 한다.</p>
<pre><code class="language-java">long value = sc.nextInt(); // 입력이 int 범위를 넘으면 읽는 단계에서 오류</code></pre>
<p>저장 변수가 <code>long</code>이어도 <code>nextInt()</code>가 읽을 수 있는 범위는 달라지지 않는다.</p>
<h3 id="4-문자-하나와-문자열-읽기">4. 문자 하나와 문자열 읽기</h3>
<h4 id="문자-하나">문자 하나</h4>
<p><code>Scanner</code>에는 <code>nextChar()</code>가 없다. 문자 하나는 문자열로 읽고 첫 문자를 꺼낸다.</p>
<pre><code class="language-java">char ch = sc.next().charAt(0); // 입력이 A라면 &#39;A&#39;</code></pre>
<p>주의할 점은 <code>next()</code>가 토큰 전체를 읽는다는 것이다. 입력이 <code>ABC</code>라면 <code>&quot;ABC&quot;</code>를 모두 읽은 뒤 <code>&#39;A&#39;</code>만 꺼낸다. 다음 <code>next()</code>로 <code>B</code>를 읽을 수는 없다.</p>
<h4 id="붙어-있는-문자">붙어 있는 문자</h4>
<p><code>ABCDE</code>처럼 문자가 붙어 있으면 문자열을 한 번 읽고 각 위치에 접근한다. 문자열 탐색이나 괄호 검사에서 자주 쓰는 방식이다.</p>
<pre><code class="language-java">String word = sc.next();
for (int i = 0; i &lt; word.length(); i++) {
    char ch = word.charAt(i);
    System.out.println(ch);
}</code></pre>
<p>입력이 <code>ABCDE</code>라면 <code>A</code>부터 <code>E</code>까지 한 줄에 하나씩 출력된다. 문자 배열이 필요하면 <code>toCharArray()</code>로 바꾼다.</p>
<pre><code class="language-java">char[] letters = sc.next().toCharArray();</code></pre>
<p>문자열을 그대로 두고 위치별로 확인할 때는 <code>charAt(i)</code>, 배열의 문자를 직접 바꿔야 할 때는 <code>toCharArray()</code>가 편하다.</p>
<p>자바의 <code>char</code>는 UTF-16 코드 단위다. 영문과 숫자 중심의 입력에서는 위처럼 처리하지만, 일부 이모지는 화면의 한 글자가 <code>char</code> 두 개로 표현될 수 있다.</p>
<h4 id="공백으로-구분된-문자">공백으로 구분된 문자</h4>
<pre><code>5
A B C D E</code></pre><pre><code class="language-java">int n = sc.nextInt();
char[] arr = new char[n];

for (int i = 0; i &lt; n; i++) {
    arr[i] = sc.next().charAt(0);
}</code></pre>
<p>각 문자가 별도의 토큰이므로 <code>next()</code>를 반복한다. <strong><code>ABCDE</code>는 한 번 읽고 나누지만, <code>A B C D E</code>는 다섯 번 읽는다.</strong></p>
<h3 id="5-공백을-포함한-문장-읽기">5. 공백을 포함한 문장 읽기</h3>
<p>공백을 포함한 문장은 <code>nextLine()</code>으로 읽는다. <code>hello java world</code>를 <code>next()</code>로 읽으면 <code>&quot;hello&quot;</code>까지만 가져온다.</p>
<h4 id="정수-다음에-문장-여러-줄이-오는-경우">정수 다음에 문장 여러 줄이 오는 경우</h4>
<pre><code>2
hello java
nice to meet you</code></pre><pre><code class="language-java">int n = sc.nextInt();
sc.nextLine(); // n이 있는 줄의 나머지 소비

String[] sentences = new String[n];
for (int i = 0; i &lt; n; i++) {
    sentences[i] = sc.nextLine();
}</code></pre>
<p>현재 줄을 비우는 <code>nextLine()</code>은 반복문 전에 한 번만 호출한다. 문장 안의 공백은 그대로 남으므로 <code>charAt(i)</code>로 접근하면 공백도 하나의 문자로 읽힌다.</p>
<h4 id="숫자와-문장이-같은-줄에-있는-경우">숫자와 문장이 같은 줄에 있는 경우</h4>
<pre><code>20 Kim Jinsol</code></pre><pre><code class="language-java">int age = sc.nextInt();
String name = sc.nextLine().trim(); // &quot;Kim Jinsol&quot;</code></pre>
<p>이때는 현재 줄의 나머지가 이름이다. <code>nextLine()</code>을 호출하고 반환값을 버리면 이름도 버리게 된다. <code>trim()</code>은 양끝 공백이 필요 없을 때만 쓴다.</p>
<h2 id="6-붙어-있는-숫자를-한-자리씩-처리하기">6. 붙어 있는 숫자를 한 자리씩 처리하기</h2>
<p>숫자의 합이나 각 자릿수 검사 문제에서 사용하는 방식이다.</p>
<pre><code>54321</code></pre><pre><code class="language-java">String number = sc.next();
int sum = 0;

for (int i = 0; i &lt; number.length(); i++) {
    sum += number.charAt(i) - &#39;0&#39;;
}

System.out.println(sum); // 15</code></pre>
<p><code>charAt(i)</code>의 반환값은 숫자가 아니라 문자이다.</p>
<pre><code class="language-java">char ch = &#39;5&#39;;

int a = ch;       // 53: 문자 &#39;5&#39;의 코드값
int b = ch - &#39;0&#39;; // 5</code></pre>
<p><code>&#39;0&#39;</code>부터 <code>&#39;9&#39;</code>까지의 코드값이 연속되어 있으므로 <code>&#39;0&#39;</code>을 빼면 해당 정수가 된다.</p>
<p><code>00123</code>을 정수로 읽으면 <code>123</code>이 되어 앞의 <code>0</code>이 사라진다. 앞자리 <code>0</code>이나 원래 자릿수가 필요하면 <code>next()</code>로 문자열을 읽는다. <strong>계산할 숫자 전체가 필요한지, 각 자릿수가 필요한지 구분하자.</strong></p>
<h3 id="7-격자-입력-세-가지">7. 격자 입력 세 가지</h3>
<h4 id="공백-없는-숫자-격자">공백 없는 숫자 격자</h4>
<p>미로, 농작물 수확 등 각 칸이 한 자리 숫자로 주어지는 문제의 입력 형태이다.</p>
<pre><code>3
123
456
789</code></pre><h4 id="next-사용">next() 사용</h4>
<pre><code class="language-java">int n = sc.nextInt();
int[][] map = new int[n][n];

for (int r = 0; r &lt; n; r++) {
    String row = sc.next();

    for (int c = 0; c &lt; n; c++) {
        map[r][c] = row.charAt(c) - &#39;0&#39;;
    }
}</code></pre>
<p>한 행을 문자열로 읽고 각 문자를 숫자로 바꾼다. 첫 행 <code>123</code>은 배열에 <code>1</code>, <code>2</code>, <code>3</code>으로 저장된다.</p>
<p><code>nextLine()</code>으로 행을 읽어도 되지만, 그때는 <code>nextInt()</code> 다음에 첫 줄의 나머지를 소비해야 한다. 이를 빠뜨려 첫 <code>row</code>가 빈 문자열이 되면 <code>row.charAt(0)</code>에서 예외가 발생한다.</p>
<h4 id="공백-있는-숫자-격자">공백 있는 숫자 격자</h4>
<pre><code>2 3
10 20 30
40 50 60</code></pre><pre><code class="language-java">int n = sc.nextInt(); // 행
int m = sc.nextInt(); // 열

int[][] map = new int[n][m];

for (int r = 0; r &lt; n; r++) {
    for (int c = 0; c &lt; m; c++) {
        map[r][c] = sc.nextInt();
    }
}</code></pre>
<p>각 값이 공백으로 구분되어 있으므로 정수 단위로 읽으면 된다.</p>
<p>두 자리 숫자나 음수가 있어도 같은 방식이다.</p>
<pre><code>-10 200 3</code></pre><p><strong><code>charAt(c) - &#39;0&#39;</code>은 한 자리 숫자 문자를 변환하는 방법이므로, 이런 입력에는 사용할 수 없다.</strong></p>
<h4 id="문자-격자">문자 격자</h4>
<p>탐색 문제에서 이동 가능한 칸과 벽을 문자로 구분하는 경우이다.</p>
<pre><code>3 4
S..#
.#..
...E</code></pre><pre><code class="language-java">int n = sc.nextInt();
int m = sc.nextInt();

char[][] map = new char[n][m];

for (int r = 0; r &lt; n; r++) {
    map[r] = sc.next().toCharArray();
}</code></pre>
<p>읽은 뒤에는 문자로 비교한다.</p>
<pre><code class="language-java">if (map[r][c] == &#39;#&#39;) {
    // 벽
}

if (map[r][c] == &#39;E&#39;) {
    // 도착점
}</code></pre>
<p>문자 <code>char</code>는 작은따옴표를 사용한다.</p>
<pre><code class="language-java">&#39;#&#39;   // char
&quot;#&quot;   // String</code></pre>
<p>이 코드는 각 행에 공백 없이 정확히 <code>m</code>개의 문자가 주어진다는 조건을 전제로 한다.</p>
<p>공백 자체가 격자의 한 칸이라면 <code>nextLine()</code>으로 읽고 공백을 보존해야 한다.</p>
<h3 id="8-그래프와-명령어-입력">8. 그래프와 명령어 입력</h3>
<h4 id="그래프의-간선">그래프의 간선</h4>
<pre><code>4 3
1 2
2 3
2 4</code></pre><p>정점 4개, 간선 3개인 무방향 그래프라고 가정한다.</p>
<pre><code class="language-java">int n = sc.nextInt();
int m = sc.nextInt();

List&lt;List&lt;Integer&gt;&gt; graph = new ArrayList&lt;&gt;();

for (int i = 0; i &lt;= n; i++) {
    graph.add(new ArrayList&lt;&gt;());
}

for (int i = 0; i &lt; m; i++) {
    int a = sc.nextInt();
    int b = sc.nextInt();

    graph.get(a).add(b);
    graph.get(b).add(a);
}</code></pre>
<p>필요한 import:</p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;</code></pre>
<p>무방향 그래프이므로 양쪽에 저장한다. 방향 그래프라면 문제에서 지정한 방향으로만 저장한다.</p>
<p>입력에서 확인할 부분은 다음과 같다.</p>
<ul>
<li>정점 번호가 <code>0</code>부터인지 <code>1</code>부터인지</li>
<li>간선이 단방향인지 양방향인지</li>
<li>가중치가 추가로 주어지는지</li>
</ul>
<p>가중치가 있다면 간선 하나당 정수 세 개를 읽는다.</p>
<pre><code class="language-java">int from = sc.nextInt();
int to = sc.nextInt();
int weight = sc.nextInt();</code></pre>
<h4 id="명령어마다-입력-개수가-다른-경우">명령어마다 입력 개수가 다른 경우</h4>
<p>스택이나 큐 구현 문제에서 자주 나오는 형태이다.</p>
<pre><code>4
push 3
push 5
pop
size</code></pre><p>명령어에 따라 필요한 값만 추가로 읽을 수 있다.</p>
<pre><code class="language-java">int n = sc.nextInt();

for (int i = 0; i &lt; n; i++) {
    String command = sc.next();

    if (command.equals(&quot;push&quot;)) {
        int value = sc.nextInt();

        // value 추가
    } else if (command.equals(&quot;pop&quot;)) {
        // 값 꺼내기
    } else if (command.equals(&quot;size&quot;)) {
        // 크기 확인
    }
}</code></pre>
<p>문자열 내용은 <code>equals()</code>로 비교한다. <code>push</code>처럼 값이 뒤따르는 명령만 추가로 정수를 읽으면 된다.</p>
<p>한 줄에 명령 하나가 주어진다면 <code>nextLine()</code>으로 읽고 <code>StringTokenizer</code>로 나눠도 된다. 다만 인수가 공백을 포함한 문장이라면 필요한 공백까지 사라지지 않도록 처리해야 한다.</p>
<h3 id="9-테스트-케이스와-입력-종료-조건">9. 테스트 케이스와 입력 종료 조건</h3>
<h4 id="테스트-케이스가-여러-개인-경우">테스트 케이스가 여러 개인 경우</h4>
<pre><code>2
3
1 2 3
2
10 20</code></pre><p>첫 번째 값이 테스트 케이스 개수 <code>T</code>인 경우이다.</p>
<pre><code class="language-java">int t = sc.nextInt();

for (int tc = 1; tc &lt;= t; tc++) {
    int n = sc.nextInt();
    int sum = 0;

    for (int i = 0; i &lt; n; i++) {
        sum += sc.nextInt();
    }

    System.out.println(&quot;#&quot; + tc + &quot; &quot; + sum);
}</code></pre>
<p>위 코드의 출력:</p>
<pre><code>#1 6
#2 30</code></pre><p><code>#테스트번호 정답</code> 형식은 문제에서 요구할 때만 사용한다.</p>
<h4 id="케이스마다-초기화하고-입력은-끝까지-읽기">케이스마다 초기화하고 입력은 끝까지 읽기</h4>
<p>합계, 정답, 방문 배열처럼 케이스마다 새로 계산할 값은 바깥 반복문 안에서 초기화한다.</p>
<p>판정이 끝났다고 입력 반복문을 <code>break</code>로 빠져나오면 아직 읽지 않은 값이 다음 케이스로 넘어갈 수 있다. 추가 검사는 생략하더라도 정해진 입력은 모두 읽어야 한다.</p>
<pre><code class="language-java">boolean valid = true;

for (int i = 0; i &lt; n; i++) {
    int value = sc.nextInt();

    if (!valid) {
        continue;
    }

    if (value &lt; 0) {
        valid = false;
    }
}</code></pre>
<h3 id="eof-또는-특정-값까지-읽기">EOF 또는 특정 값까지 읽기</h3>
<h4 id="eof까지-정수-읽기">EOF까지 정수 읽기</h4>
<p>EOF는 입력이 끝났다는 의미이다.</p>
<pre><code class="language-java">while (sc.hasNextInt()) {
    int value = sc.nextInt();

    System.out.println(value);
}</code></pre>
<p>정수로만 구성된 입력을 끝까지 읽는 방식이다. 정수가 아닌 토큰을 만나도 반복이 끝나므로 입력 조건을 확인해야 한다.</p>
<h4 id="eof까지-줄-읽기">EOF까지 줄 읽기</h4>
<pre><code class="language-java">while (sc.hasNextLine()) {
    String line = sc.nextLine();

    System.out.println(line);
}</code></pre>
<p>빈 줄도 하나의 줄로 읽는다.</p>
<h4 id="특정-값이-나오면-종료">특정 값이 나오면 종료</h4>
<pre><code>3
5
7
0</code></pre><p><code>0</code>이 종료 조건인 경우:</p>
<pre><code class="language-java">while (sc.hasNextInt()) {
    int value = sc.nextInt();

    if (value == 0) {
        break;
    }

    // value 처리
}</code></pre>
<p>종료 값은 문제에서 정해 준 경우에만 사용한다.</p>
<h3 id="10-bufferedreader와-stringtokenizer">10. BufferedReader와 StringTokenizer</h3>
<p>입력량이 많은 문제에서는 <code>BufferedReader</code>와 <code>StringTokenizer</code>를 함께 사용하는 경우가 많다.</p>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.StringTokenizer;</code></pre>
<p>기본 설정:</p>
<pre><code class="language-java">BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));</code></pre>
<h3 id="정수-하나가-한-줄에-있는-경우">정수 하나가 한 줄에 있는 경우</h3>
<pre><code>5</code></pre><pre><code class="language-java">int n = Integer.parseInt(br.readLine().trim());</code></pre>
<p><code>readLine()</code>은 문자열을 반환하므로 숫자로 사용하려면 변환이 필요하다.</p>
<pre><code class="language-java">long value = Long.parseLong(br.readLine().trim());
double ratio = Double.parseDouble(br.readLine().trim());</code></pre>
<h4 id="한-줄에-정수-여러-개가-있는-경우">한 줄에 정수 여러 개가 있는 경우</h4>
<pre><code>10 20 30</code></pre><pre><code class="language-java">StringTokenizer st = new StringTokenizer(br.readLine());

int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
int c = Integer.parseInt(st.nextToken());</code></pre>
<p><code>readLine()</code>은 한 줄을 읽고, <code>StringTokenizer</code>는 그 문자열을 나눈다. <code>nextToken()</code>의 반환값도 문자열이므로 <code>Integer.parseInt()</code>로 정수로 바꾼다.</p>
<h4 id="stringtokenizer는-다음-줄을-읽지-않는다">StringTokenizer는 다음 줄을 읽지 않는다</h4>
<pre><code>10 20
30</code></pre><pre><code class="language-java">StringTokenizer st = new StringTokenizer(br.readLine());

int a = Integer.parseInt(st.nextToken()); // 10
int b = Integer.parseInt(st.nextToken()); // 20
int c = Integer.parseInt(st.nextToken()); // 예외 발생</code></pre>
<p><code>st</code>에 들어 있는 내용은 첫 줄 <code>&quot;10 20&quot;</code>뿐이다.</p>
<p><strong>다음 줄까지 읽으려면 <code>br.readLine()</code>을 다시 호출해야 한다.</strong></p>
<p><code>Scanner.nextInt()</code>처럼 자동으로 다음 줄의 값을 읽어 주는 것은 아니다.</p>
<h4 id="격자에-적용하기">격자에 적용하기</h4>
<p>공백 있는 정수 격자는 행마다 <code>new StringTokenizer(br.readLine())</code>를 만들고, 열 개수만큼 <code>Integer.parseInt(st.nextToken())</code>을 반복한다. 각 행의 값이 한 줄에 주어진다는 조건에서 쓴다.</p>
<p>공백 없는 숫자 격자는 각 행을 <code>br.readLine()</code>으로 읽고 <code>row.charAt(c) - &#39;0&#39;</code>으로 변환한다. 첫 줄의 크기도 <code>readLine()</code>으로 읽었다면 줄바꿈을 소비하려고 한 번 더 호출할 필요는 없다.</p>
<h4 id="eof까지-읽기">EOF까지 읽기</h4>
<pre><code class="language-java">String line;

while ((line = br.readLine()) != null) {
    // line 처리
}</code></pre>
<table>
<thead>
<tr>
<th>반환값</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;&quot;</code></td>
<td>빈 줄을 읽음</td>
</tr>
<tr>
<td><code>null</code></td>
<td>입력이 끝남</td>
</tr>
</tbody></table>
<p>빈 줄과 EOF는 다르다.</p>
<h4 id="공백-분리와-split">공백 분리와 split()</h4>
<pre><code class="language-java">String line = &quot;10  20   30&quot;;</code></pre>
<h4 id="split--사용-시-주의">split(&quot; &quot;) 사용 시 주의</h4>
<pre><code class="language-java">String[] tokens = line.split(&quot; &quot;);</code></pre>
<p>공백 하나를 기준으로 분리하기 때문에, 연속된 공백 사이에 빈 문자열이 생길 수 있다.</p>
<p>공백 개수가 일정하지 않다면 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-java">String[] tokens = line.trim().split(&quot;\\s+&quot;);</code></pre>
<p><code>\\s+</code>는 하나 이상의 공백 문자에 대응한다.</p>
<p>단, 빈 줄은 별도로 처리해야 한다.</p>
<h4 id="stringtokenizer-사용">StringTokenizer 사용</h4>
<pre><code class="language-java">StringTokenizer st = new StringTokenizer(line);

while (st.hasMoreTokens()) {
    int value = Integer.parseInt(st.nextToken());
}</code></pre>
<p>기본 설정에서는 연속된 공백을 건너뛰고 토큰을 읽는다.</p>
<p><strong>숫자 토큰만 필요하면 <code>StringTokenizer</code>, 문장과 공백 자체가 필요하면 원래 문자열을 유지하는 방식이 적합하다.</strong></p>
<h4 id="입력을-직접-한-문자씩-읽는-경우">입력을 직접 한 문자씩 읽는 경우</h4>
<p><code>BufferedReader.read()</code>로 입력 스트림에서 문자 단위로 읽을 수도 있다. 반환형은 입력 종료 값 <code>-1</code>을 표현하기 위해 <code>int</code>다. 공백과 줄바꿈도 읽히므로 줄바꿈 형식에 따라 <code>&#39;\r&#39;</code>, <code>&#39;\n&#39;</code>을 처리해야 할 수 있다.</p>
<p>단순히 문자열의 각 문자를 확인하려면 <code>charAt()</code>이 간단하다. <code>System.in.read()</code>는 바이트를 읽으므로 한글을 그대로 <code>char</code>로 변환하는 용도로 쓰면 안 된다.</p>
<h3 id="11-출력-형식과-버퍼-사용">11. 출력 형식과 버퍼 사용</h3>
<p><code>print()</code>는 줄바꿈 없이 출력하고, <code>println()</code>은 출력 후 줄을 바꾼다. 공백은 직접 넣어야 한다.</p>
<pre><code class="language-java">System.out.print(10);
System.out.print(20); // 두 호출의 결과: 1020</code></pre>
<p><code>10 20</code>으로 출력하려면 다음처럼 쓴다.</p>
<pre><code class="language-java">System.out.println(10 + &quot; &quot; + 20);</code></pre>
<p>출력 형식을 지정할 때는 <code>printf()</code>를 쓴다.</p>
<pre><code class="language-java">System.out.printf(&quot;%d %s%n&quot;, 10, &quot;java&quot;); // 10 java 출력 후 줄바꿈</code></pre>
<table>
<thead>
<tr>
<th>형식</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>%d</code></td>
<td>정수</td>
</tr>
<tr>
<td><code>%s</code></td>
<td>문자열</td>
</tr>
<tr>
<td><code>%c</code></td>
<td>문자</td>
</tr>
<tr>
<td><code>%.2f</code></td>
<td>소수점 아래 두 자리</td>
</tr>
<tr>
<td><code>%n</code></td>
<td>줄바꿈</td>
</tr>
</tbody></table>
<pre><code class="language-java">System.out.printf(java.util.Locale.US, &quot;%.2f%n&quot;, 3.14159); // 3.14</code></pre>
<p><code>Locale.US</code>를 지정하면 소수점 구분자로 <code>.</code>을 사용한다. 소수 자릿수와 줄바꿈은 문제의 출력 조건에 맞춘다.</p>
<h4 id="배열-출력">배열 출력</h4>
<p>배열을 <code>System.out.println(arr)</code>로 출력하면 원소가 나열되지 않는다. 디버깅할 때는 <code>Arrays.toString()</code>이 편하다.</p>
<pre><code class="language-java">import java.util.Arrays;</code></pre>
<pre><code class="language-java">int[] arr = {1, 2, 3};
System.out.println(Arrays.toString(arr)); // [1, 2, 3]</code></pre>
<p>다만 문제에서 <code>1 2 3</code>을 요구한다면 대괄호와 쉼표를 빼고 공백으로 구분해야 한다. 아래 <code>StringBuilder</code> 예제처럼 구분자를 직접 넣자. <strong>디버깅용 출력과 제출용 출력은 다를 수 있다.</strong></p>
<h4 id="출력이-많다면-stringbuilder">출력이 많다면 StringBuilder</h4>
<p>출력할 내용이 많으면 <code>StringBuilder</code>에 모아서 한 번에 출력할 수 있다. 문자열을 반복해서 <code>+=</code>로 연결하는 것보다 많은 결과를 모으는 데 적합하다.</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
for (int i = 1; i &lt;= 5; i++) {
    sb.append(i).append(&#39;\n&#39;);
}
System.out.print(sb);</code></pre>
<p>위 코드는 <code>1</code>부터 <code>5</code>까지 한 줄에 하나씩 출력한다. 배열을 공백으로 구분해 출력할 때도 같은 방식이다.</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
for (int i = 0; i &lt; arr.length; i++) {
    if (i &gt; 0) {
        sb.append(&#39; &#39;);
    }
    sb.append(arr[i]);
}
sb.append(&#39;\n&#39;);
System.out.print(sb);</code></pre>
<p>첫 원소 앞에는 공백을 넣지 않고, 두 번째 원소부터 앞에 공백을 붙인다. 결과 전체를 메모리에 저장하므로 출력이 매우 크다면 나누어 출력하거나 <code>BufferedWriter</code>를 쓴다.</p>
<h4 id="bufferedwriter">BufferedWriter</h4>
<pre><code class="language-java">import java.io.BufferedWriter;
import java.io.OutputStreamWriter;</code></pre>
<pre><code class="language-java">BufferedWriter bw =
        new BufferedWriter(new OutputStreamWriter(System.out));

bw.write(&quot;hello&quot;);
bw.newLine();

bw.write(String.valueOf(123));
bw.newLine();

bw.flush();</code></pre>
<h4 id="정수-출력-시-주의">정수 출력 시 주의</h4>
<pre><code class="language-java">bw.write(65);</code></pre>
<p>위 코드는 문자열 <code>&quot;65&quot;</code>를 출력하지 않는다. 문자 코드에 해당하는 <code>&#39;A&#39;</code>를 출력한다.</p>
<p>숫자 자체를 출력하려면 문자열로 변환한다.</p>
<pre><code class="language-java">bw.write(String.valueOf(65)); // &quot;65&quot;</code></pre>
<p>마지막에는 <code>flush()</code>로 버퍼에 남은 내용을 출력한다.</p>
<h3 id="12-입력량이-많은-문제의-기본-코드">12. 입력량이 많은 문제의 기본 코드</h3>
<p>아래 코드는 공백이나 줄바꿈으로 구분된 정수를 읽어 합을 출력한다.</p>
<p>입력:</p>
<pre><code>5
10 20 30 40 50</code></pre><pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.StringTokenizer;

public class Main {
    static BufferedReader br =
            new BufferedReader(new InputStreamReader(System.in));

    static StringTokenizer st;

    static String next() throws IOException {
        while (st == null || !st.hasMoreTokens()) {
            String line = br.readLine();

            if (line == null) {
                return null;
            }

            st = new StringTokenizer(line);
        }

        return st.nextToken();
    }

    static int nextInt() throws IOException {
        return Integer.parseInt(next());
    }

    public static void main(String[] args) throws IOException {
        int n = nextInt();
        long sum = 0;

        for (int i = 0; i &lt; n; i++) {
            sum += nextInt();
        }

        System.out.println(sum);
    }
}</code></pre>
<p><code>next()</code>는 현재 줄에 토큰이 남아 있으면 하나를 반환하고, 없으면 다음 줄을 읽는다. 그래서 숫자가 여러 줄에 나뉘어 있어도 처리할 수 있다.</p>
<p>이 코드의 <code>nextInt()</code>는 필요한 정수가 입력에 존재한다고 가정한다. 줄 경계와 빈 줄도 건너뛰므로 문장이나 공백 자체가 중요한 입력에는 맞지 않는다.</p>
<h3 id="13-자주-하는-실수와-제출-전-확인">13. 자주 하는 실수와 제출 전 확인</h3>
<table>
<thead>
<tr>
<th>증상 또는 실수</th>
<th>확인할 부분</th>
</tr>
</thead>
<tbody><tr>
<td><code>nextInt()</code> 다음 <code>nextLine()</code>이 빈 문자열</td>
<td>현재 줄의 나머지와 줄바꿈이 남았는지</td>
</tr>
<tr>
<td><code>nextLine()</code>을 추가했더니 데이터가 사라짐</td>
<td>같은 줄에 필요한 값이 있었는지</td>
</tr>
<tr>
<td>붙어 있는 문자를 한 글자씩 읽지 못함</td>
<td><code>next()</code>가 토큰 전체를 읽는다는 점</td>
</tr>
<tr>
<td>숫자 문자가 코드값으로 저장됨</td>
<td>한 자리 숫자라면 <code>&#39;0&#39;</code>을 빼서 변환했는지</td>
</tr>
<tr>
<td><code>StringTokenizer</code>에서 다음 값이 없음</td>
<td>다음 줄을 직접 읽었는지</td>
</tr>
<tr>
<td><code>split(&quot; &quot;)</code> 뒤 숫자 변환 오류</td>
<td>연속 공백으로 빈 문자열이 생겼는지</td>
</tr>
<tr>
<td>다음 테스트 케이스부터 입력이 꼬임</td>
<td>이전 케이스의 입력을 끝까지 읽었는지</td>
</tr>
<tr>
<td>출력 결과는 맞는데 오답 처리</td>
<td>공백·줄바꿈·배열 구분자와 디버깅 출력</td>
</tr>
<tr>
<td><code>BufferedWriter</code>에서 숫자가 문자로 출력됨</td>
<td><code>String.valueOf()</code>로 변환했는지</td>
</tr>
</tbody></table>
<h4 id="문제를-읽으며-확인할-것">문제를 읽으며 확인할 것</h4>
<p>입력 코드를 쓰기 전에 다음을 확인해 두자.</p>
<ol>
<li>입력 개수나 종료 조건이 <code>N</code>, <code>T</code>, 특정 값, EOF 중 무엇인가?</li>
<li>공백과 줄바꿈은 구분자인가, 데이터의 일부인가?</li>
<li>숫자 전체가 필요한가, 각 자릿수와 원래 형태가 필요한가?</li>
<li>격자 한 칸은 한 문자인가, 여러 자리 정수인가?</li>
<li>입력값이나 계산 결과가 <code>int</code> 범위를 넘을 수 있는가?</li>
<li>입력·출력량과 시간 제한에 맞는 방식을 골랐는가?</li>
<li>공백, 줄바꿈, 테스트 번호, 소수 자릿수 등 출력 조건을 확인했는가?</li>
</ol>
<p>입력이 꼬이면 지금 어디까지 읽었는지부터 확인하자. 토큰을 읽다가 줄 단위로 바꾼 지점이나, 아직 읽지 않은 값이 남은 반복문을 살펴보면 원인을 찾기 쉽다.</p>
<hr>
<h4 id="요약">요약</h4>
<p>이번에 확실히 이해한 건 <strong>숫자를 읽은 뒤 문자열을 읽을 때, <code>next()</code>와 <code>nextLine()</code>의 동작이 다르다</strong>는 점이다.</p>
<p><code>nextInt()</code>로 첫 숫자를 읽고 나면 줄바꿈이 남는다.
이후 <code>next()</code>는 입력 전 앞의 공백을 먹으므로 바로 다음 문자열을 읽지만, <code>nextLine()</code>은 남은 줄바꿈부터 처리한다!</p>
<p>다시 처음으로 돌아가서</p>
<p>입력으로 다음 숫자가 주어진다.
3
123
456
789</p>
<p>이 입력에서 숫자 5를 읽으려면... </p>
<p><strong><code>nextInt()</code> + <code>next()</code></strong>로 읽거나, <strong><code>nextInt()</code> + <code>nextLine()</code>으로 현재 줄을 비운 뒤 + <code>nextLine()</code></strong>으로 다음 줄을 읽어야 한다. </p>
<p>입력이 헷갈릴 때는 어떤 메서드를 쓰는지와 함께 <strong>현재 어디까지 읽었는지</strong>를 확인하기.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기업들이 신입 백엔드 개발자에게 원하는게 뭔데?]]></title>
            <link>https://velog.io/@tongmill_g/9%EC%9B%94-%EC%B2%AB%EC%A7%B8%EC%A3%BC</link>
            <guid>https://velog.io/@tongmill_g/9%EC%9B%94-%EC%B2%AB%EC%A7%B8%EC%A3%BC</guid>
            <pubDate>Sun, 06 Sep 2026 14:04:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="기업에서-요구하는-백엔드-개발자의-여건을-모아보자">기업에서 요구하는 백엔드 개발자의 여건을 모아보자</h4>
</blockquote>
<p><strong><em>짐싸</em></strong>
＜백엔드팀 ​소개＞
• 백엔드팀은 100만 명 이상의 사용자와 수천 명의 파트너가 이용하는 플랫폼 서비스를 안정적으로 운영하며, 기술을 통해 더 나은 사용자 경험을 제공합니다.
• 단순한 기능 구현을 넘어 서비스의 확장성과 안정성을 위한 기반 기술을 설계하고, 서버 아키텍처를 직접 운영하며 성능을 최적화하고 있습니다.
• 고객–파트너 간 복잡한 매칭 로직과 비동기 처리 등 다양한 기술 과제를 해결하며, 디지털 전환이 더딘 3조 원 규모의 이사 시장을 혁신하고 있습니다.
• 백엔드팀은 서비스의 성장을 이끌고 그 속도를 앞서가는 팀입니다.</p>
<p>＜포지션 소개＞
• 짐싸의 비전을 이해하고 서비스의 신뢰성, 안정성, 확장성을 책임질 수 있는 백엔드 시스템을 개발합니다.
• 인프라, DB 관리, API 개발 등 핵심적인 역할을 수행하며, 팀원들과 기술적 이슈에 대한 고민을 함께 해결해나갑니다.
주요업무
• 높은 가용성과 확장성을 가지도록 짐싸 백엔드 시스템 개발 및 개선
• 요구사항 분석 및 테스트를 통한 RESTful API 구축
• 서비스의 성능을 위한 애플리케이션 최적화
• 클라우드 환경에서의 서버 운영
자격요건
• AI 도구·AI 에이전트를 활용해 보신 분
• Spring Boot(Java), JPA를 활용한 프로젝트 경험이 있거나 이에 준하는 개발 역량을 갖추신 분
• DB(RDB, NoSQL) 테이블 설계가 가능하신 분
• 새로운 기술을 두려워하지 않고 도전하며 이를 동료에게 공유하는 것을 즐기시는 분
• 컴퓨터 공학 전공 혹은 이에 준하는 지식 및 활용 역량이 있으신 분
• 해외여행에 결격사유가 없는 분 (남성의 경우, 병역필 또는 면제자)</p>
<hr>
<p><strong><em>여기어때</em></strong></p>
<h4 id="자격요건">자격요건</h4>
<p>• 대용량 트래픽 처리를 위한 시스템 설계 경험이 있는 분
• 논리적이고 체계적인 문제 해결 능력과 커뮤니케이션 능력을 보유한 분
• Java(Kotlin) Spring 기반 대규모 애플리케이션 개발 및 운영 경험이 있는 분
• JPA, Hibernate 등 ORM 사용과 도메인 모델링 경험이 있는 분
• AWS 클라우드 환경(EKS, RDS, ElastiCache) 기반의 인프라 설계 및 운영 경험이 있는 분</p>
<hr>
<p><strong><em>위버스 컴퍼니</em></strong>
주요업무
• 커뮤니티, 멤버십, 커머스 전반을 아우르는 다양한 요구사항을 백엔드 구조로 구현하고 서비스에 반영합니다.
• 유료 멤버십, 구독형 디지털 상품, 쿠폰/투표/설문 등 주요 기능을 개발하고 운영 효율을 개선합니다.
• 전시, 상품, 주문, 클레임, 물류 등 커머스 도메인의 기능을 고도화하고 서비스 간 연계를 설계합니다.
• 서비스 이용 과정에서 발생하는 이슈를 분석하고, 안정적인 운영을 위한 개선 작업을 수행합니다.
• 사용자 데이터 및 VOC를 기반으로 서비스 품질을 점검하고 개선 방향을 도출합니다.</p>
<p>[주요 업무 환경]
• 아티스트의 활동 및 프로젝트 일정에 따른 업무 집중 시기가 있을 수 있습니다.
자격요건
• Java 또는 Kotlin 기반으로 백엔드 개발 경험이 있거나, 이에 대한 기본적인 이해를 갖춘 분
• Spring Framework 기반 웹서비스 구조에 대한 이해 또는 개발 경험이 있는 분
• RESTful API 설계 및 개발에 대한 이해 또는 경험이 있는 분
• 데이터베이스(RDBMS 등)를 활용한 데이터 처리 및 모델링에 대한 이해가 있는 분
• Git 등 형상관리 도구 사용 경험 또는 협업 방식에 대한 이해가 있는 분
• 새로운 기술이나 환경에 대해 스스로 학습하고 적용해 보려는 의지가 있는 분
• 문제를 정의하고 해결해 보고자 하는 태도를 갖춘 분
• 다양한 직군과 협업하며 원활하게 커뮤니케이션할 수 있는 분</p>
<p>[위버스 백엔드 엔지니어가 사용/활용하는 기술]
• 커뮤니티 : Java, Spring, Redis, Kafka (실시간 데이터 및 사용자 인터랙션 처리)
• 프로모션 : Java, Spring, Kafka 기반 이벤트 처리, AWS 등 Cloud 환경
• 멤버십/구독 : Java 또는 Kotlin, Spring, 인증/권한 및 사용자 상태 관리, 외부 시스템 연동(API)
• 커머스 : Java 또는 Kotlin, Spring, JPA, MySQL 등 RDBMS, Redis, Kafka</p>
<hr>
<p><strong><em>드림어스</em></strong>
주요업무
• 미디어 자동 입수 파이프라인 시스템 구축: 미디어 데이터가 유입되는 첫 단계인 대용량 자동 수집(입수) 파이프라인 시스템을 설계하고 개발합니다.
• 미디어 트랜스코딩 고도화: 수집된 다양한 원본 미디어 데이터를 표준 규격으로 통합(Unification)하고, 다양한 디바이스와 네트워크 환경에 맞춰 최적화하는 트랜스코딩 시스템을 고도화 합니다.
• 스트리밍 서비스 연동: 최적화된 미디어 파일이 실제 유저에게 효율적으로 제공될 수 있도록 스트리밍 서비스를 제공 합니다.
• 백오피스 개발 및 운영: 음원/음반 유통망 시스템, 콘텐츠 관리 시스템, 대규모 작업을 위한 운영 포털 시스템을 개발하고 운영합니다.
자격요건
• Java, Kotlin, Golang 중 하나의 언어로 프로젝트를 완성해 본 경험
(학교 과제, 개인/팀 프로젝트, 인턴 등 형태 무관)
• HTTP와 REST의 기본 동작 원리를 이해하고, 간단한 API 서버를 만들어 본 경험
• 관계형 데이터베이스(RDBMS)와 SQL에 대한 기본 이해
• 자료구조, 운영체제, 네트워크 등 CS 기초 지식</p>
<hr>
<p><strong><em>버즈빌</em></strong>
자격요건
&quot;광고 도메인 경험이 없어도 괜찮습니다! 문제를 탐구하고 해결하는 태도가 더 중요합니다.&quot;</p>
<p>• Kotlin, Go, Python 등 하나 이상의 서버 프로그래밍 언어로 백엔드 시스템을 개발해 본 분 (실무 경력이 짧더라도 프로젝트 등으로 역량을 보여줄 수 있다면 충분합니다.)
• MySQL 등 관계형 데이터베이스와 Redis, DynamoDB 등 NoSQL 저장소를 활용해 본 분
• 외부 시스템과의 API 연동을 설계하고 개발해 본 경험이 있는 분
• 백엔드에 한정하지 않고, 제품에 필요하다면 프론트엔드 등 다른 기술 영역으로 확장하는 데 열려 있는 분
• 다양한 직군과 원활하게 소통하고 협업할 수 있는 분
• AI 도구를 적극적으로 도입하고, 이를 통해 생산성을 극대화할 수 있는 역량을 갖추신 분</p>
<hr>
<p>독특했던 채용 설명...</p>
<p><strong><em>쏘카</em></strong></p>
<h4 id="자격요건-1">자격요건</h4>
<p>[백엔드·프론트엔드 어느 한 축은 프로덕션 수준으로 단단한 분]
• 좋은 설계와 나쁜 설계를 자기 기준으로 판단할 수 있는 분
[기능 정의 단계부터 함께 고민해본 경험이 있는 분]
• 요구사항을 받아서 구현하는 사람이 아니라, ‘무엇을 왜 만들어야 하는가’를 PM.디자이너와 함께 결정해본 분
[AI 에이전트를 업무에 깊이 통합해본 분]
• 단순히 &#39;AI를 써봤다&#39; 수준이 아니라, Agentic Coding Agent(Claude Code, Cursor 등)의 작동 방식·강점·한계를 체득하고 자기 워크플로우의 일부로 만든 분
• AI 없이 일하던 방식으로 돌아가는 것이 부자연스럽게 느껴지는 분
[자기 일을 결과로 설명할 수 있는 분]
• &quot;구현했습니다&quot;가 아니라 &quot;이 문제를 해결했고 어떤 결과를 만들었습니다&quot;로 자기 일을 말할 수 있는 분</p>
<h4 id="우대사항">우대사항</h4>
<p>• 제품 개발 경험 3년 이상
• 모빌리티·주차·오프라인·결제·실시간 트랜잭션 등 복잡한 도메인에서 서비스를 개발해본 경험
• 기존 방식을 의심하고 새로 시도해본 경험.</p>
<ul>
<li>팀이 일하는 방식, 시스템이 작동하는 방식, 시장이 굴러가는 방식을 다르게 제안하고 실제로 바꿔본 분
• AI Native 조직 만들기에 동참하고 싶은 분.</li>
<li>SOCAR를 &quot;AI 없이는 작동하지 않는 회사&quot;로 함께 바꿔가는 여정에 흥미를 느끼는 분
• 스타트업 또는 사내 스타트업 환경에서 0 ＞ 1 제품 출시 경험</li>
</ul>
<hr>
<blockquote>
<p>전체적으로는...</p>
</blockquote>
<p><strong>공통적으로 요구하는 역량</strong></p>
<p>Java/Kotlin + Spring 기반 서버 개발
REST API 설계 및 개발
RDBMS·SQL·데이터 모델링, JPA/ORM 활용
자료구조·운영체제·네트워크 등 CS 기초
Redis, Kafka, NoSQL 등 대용량·비동기 처리 기술
AWS 등 클라우드 환경에서의 운영 경험
단순 구현보다 성능·안정성·확장성까지 고려하는 설계
협업, 문제 정의, 자기주도적 학습 및 기술 공유 능력
+) AI 활용 능력에 대한 이야기도 자주 보임</p>
<p><strong>현재 기업들이 원하는 개발자는?</strong></p>
<p>_문제 정의 → 설계 → 개발 → 테스트 → 배포 → 운영 → 성능 개선 _까지 책임지는 <strong>Software Engineer에 가까운 백엔드 개발자</strong>같다는 생각을 했다. 단순히 무엇을 구현했는지보다는 왜 만들었는지, 어떤 문제를 해결했는지, 어떤 결과를 만들었는지를 더 중요하게 보는 듯.
이건 예전에는 없던 새로운 경향이라고 하기엔 무리가 있지만, 기업에서 원하는 것이 단순한 백엔드 업무만 가능한 개발자가 아니라는 것이 더 명확해 보인다.</p>
<blockquote>
<p>GPT의 제안...</p>
</blockquote>
<p>앞으로 백엔드 개발자를 준비한다면 우선순위는
<em>Java → Spring Boot → REST API → DB/SQL/JPA → CS → Redis/Kafka → AWS/Docker</em>
순으로 기본기를 쌓는 것이 좋습니다.
그리고 프로젝트에서도 단순 CRUD에서 끝내지 않고 트래픽, 비동기 처리, 캐시, 장애 대응, 성능 개선, AI 활용까지 한두 가지라도 직접 경험해 보는 것이 중요합니다.</p>
<p>그렇단다</p>
<p>어떤 기술 하나를 진득하게 깊게 아는 것도 중요하지만 여러 경험을 해보는 것도 그만큼 중요하다는 결론
채용공고를 자주 들여다보고 공통적으로 원하는 역량을 잘 캐치해서 기업에서 원하는 인재상이 무엇인지 알고있기, 그리고 그 기술을 사용하는 개발자를 원하는 이유가 무엇인지 체크할 것.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (10) XML과 JSON 데이터 처리]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-10-XML%EA%B3%BC-JSON-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@tongmill_g/JAVA-10-XML%EA%B3%BC-JSON-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 29 Jul 2026 05:35:31 GMT</pubDate>
            <description><![CDATA[<h1 id="xml과-json-데이터-처리">XML과 JSON 데이터 처리</h1>
<h2 id="들어가며">들어가며</h2>
<p>실제 개발에서는 데이터를 직접 코드에 작성하는 경우보다 외부 시스템에서 전달받는 경우가 훨씬 많다.</p>
<p>대표적인 예가 공공데이터포털(Open API), 기상청 API, 지도 API, 금융 API 등이다.</p>
<p>이러한 데이터는 대부분 <strong>CSV</strong>, <strong>XML</strong>, <strong>JSON</strong> 형태로 제공된다.</p>
<p>과거에는 XML이 많이 사용되었지만, 현재는 대부분 JSON을 사용한다. 그렇다고 XML이 사라진 것은 아니다. 공공기관이나 오래된 시스템에서는 XML을 사용하는 경우가 여전히 많으며, Java에서는 XML을 처리하는 SAX, DOM Parser도 자주 등장한다.</p>
<p>이번 글에서는 공공데이터의 형태와 XML의 구조, SAX와 DOM Parser의 차이, 그리고 JSON의 특징까지 정리해보려고 한다.</p>
<hr>
<h1 id="공공데이터와-데이터-형식">공공데이터와 데이터 형식</h1>
<h2 id="공공데이터란">공공데이터란?</h2>
<p>공공데이터란 <strong>국가기관이나 공공기관이 생산하거나 관리하는 공적인 정보</strong>를 의미한다.</p>
<p>대표적인 예는 다음과 같다.</p>
<ul>
<li>날씨 정보</li>
<li>버스 도착 정보</li>
<li>지하철 운행 정보</li>
<li>관광지 정보</li>
<li>미세먼지 정보</li>
<li>병원 및 약국 정보</li>
<li>문화 행사 정보</li>
</ul>
<p>이러한 데이터는 대부분 Open API 형태로 제공되며 프로그램에서 쉽게 사용할 수 있도록 일정한 형식을 가진다.</p>
<p>대표적인 데이터 형식은 다음 세 가지이다.</p>
<ul>
<li>CSV</li>
<li>XML</li>
<li>JSON</li>
</ul>
<hr>
<h2 id="csv">CSV</h2>
<p>CSV(Comma Separated Values)는 가장 단순한 형태의 데이터이다.</p>
<p>이름 그대로 <strong>쉼표(,)</strong> 로 데이터를 구분한다.</p>
<pre><code class="language-csv">id,name,age
1,Kim,20
2,Lee,25
3,Park,30</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>파일 크기가 작다.</li>
<li>사람이 읽기 쉽다.</li>
<li>처리 속도가 빠르다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>데이터 구조를 표현하기 어렵다.</li>
<li>계층 구조를 표현할 수 없다.</li>
<li>데이터 타입 정보가 없다.</li>
</ul>
<p>간단한 테이블 데이터를 저장할 때 많이 사용된다.</p>
<hr>
<h2 id="xml">XML</h2>
<p>XML(Extensible Markup Language)은 태그를 이용하여 데이터를 표현하는 문서 형식이다.</p>
<p>HTML과 비슷하게 생겼지만 목적은 완전히 다르다.</p>
<p>HTML은 화면을 표현하기 위한 언어이고,</p>
<p>XML은 데이터를 저장하고 전달하기 위한 언어이다.</p>
<pre><code class="language-xml">&lt;person&gt;
    &lt;name&gt;Kim&lt;/name&gt;
    &lt;age&gt;20&lt;/age&gt;
&lt;/person&gt;</code></pre>
<p>XML은 구조가 명확하고 확장성이 뛰어나기 때문에 공공데이터에서 많이 사용되었다.</p>
<hr>
<h2 id="json">JSON</h2>
<p>JSON(JavaScript Object Notation)은 현재 가장 많이 사용되는 데이터 교환 형식이다.</p>
<p>객체를 <strong>Key-Value</strong> 형태로 표현한다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Kim&quot;,
  &quot;age&quot;: 20
}</code></pre>
<p>현재 대부분의 REST API는 JSON을 기본 형식으로 사용한다.</p>
<hr>
<h2 id="데이터-형식-비교">데이터 형식 비교</h2>
<table>
<thead>
<tr>
<th>형식</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>CSV</td>
<td>용량이 작고 단순하다.</td>
<td>구조 표현이 어렵다.</td>
</tr>
<tr>
<td>XML</td>
<td>계층 구조 표현이 가능하다.</td>
<td>문법이 복잡하고 용량이 크다.</td>
</tr>
<tr>
<td>JSON</td>
<td>가볍고 사용하기 쉽다.</td>
<td>주석을 사용할 수 없다.</td>
</tr>
</tbody></table>
<p>현재는 JSON이 가장 많이 사용되지만 XML 역시 공공데이터나 레거시 시스템에서는 여전히 중요한 역할을 한다.</p>
<hr>
<h1 id="xml-1">XML</h1>
<h2 id="xml이란">XML이란?</h2>
<p>XML은 <strong>Extensible Markup Language</strong>의 약자이다.</p>
<p>&quot;확장 가능한 마크업 언어&quot;라는 의미이며, 필요한 태그를 개발자가 직접 정의하여 사용할 수 있다.</p>
<p>예를 들어 사람 정보를 저장한다면 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-xml">&lt;person&gt;
    &lt;name&gt;Kim&lt;/name&gt;
    &lt;age&gt;20&lt;/age&gt;
&lt;/person&gt;</code></pre>
<p>학생이라면</p>
<pre><code class="language-xml">&lt;student&gt;
    &lt;name&gt;Hong&lt;/name&gt;
    &lt;grade&gt;3&lt;/grade&gt;
&lt;/student&gt;</code></pre>
<p>처럼 자유롭게 태그를 만들 수 있다.</p>
<p>이것이 HTML과 가장 큰 차이점이다.</p>
<hr>
<h2 id="well-formed-xml">Well-Formed XML</h2>
<p>XML은 반드시 문법을 지켜야 한다.</p>
<p>이를 <strong>Well-Formed XML</strong>이라고 한다.</p>
<p>대표적인 규칙은 다음과 같다.</p>
<h3 id="xml-선언">XML 선언</h3>
<p>문서는 일반적으로 다음과 같이 시작한다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</code></pre>
<ul>
<li>XML 버전</li>
<li>문자 인코딩</li>
</ul>
<p>을 지정한다.</p>
<hr>
<h3 id="root-element는-하나만-존재">Root Element는 하나만 존재</h3>
<p>XML에는 최상위 태그가 반드시 하나 존재해야 한다.</p>
<p>올바른 예</p>
<pre><code class="language-xml">&lt;students&gt;
    ...
&lt;/students&gt;</code></pre>
<p>잘못된 예</p>
<pre><code class="language-xml">&lt;student&gt;&lt;/student&gt;

&lt;teacher&gt;&lt;/teacher&gt;</code></pre>
<p>루트 태그가 두 개이므로 잘못된 XML이다.</p>
<hr>
<h3 id="시작-태그와-종료-태그가-일치해야-한다">시작 태그와 종료 태그가 일치해야 한다.</h3>
<p>올바른 예</p>
<pre><code class="language-xml">&lt;name&gt;Kim&lt;/name&gt;</code></pre>
<p>잘못된 예</p>
<pre><code class="language-xml">&lt;name&gt;Kim&lt;/age&gt;</code></pre>
<hr>
<h3 id="태그는-대소문자를-구분한다">태그는 대소문자를 구분한다.</h3>
<pre><code class="language-xml">&lt;Name&gt;</code></pre>
<p>과</p>
<pre><code class="language-xml">&lt;name&gt;</code></pre>
<p>은 서로 다른 태그이다.</p>
<hr>
<h3 id="속성attribute">속성(Attribute)</h3>
<p>태그에는 속성을 가질 수 있다.</p>
<pre><code class="language-xml">&lt;student id=&quot;1&quot;&gt;</code></pre>
<p>여기서</p>
<pre><code class="language-text">id → 속성 이름
1 → 속성 값</code></pre>
<p>이다.</p>
<hr>
<h1 id="xml-문서-검증">XML 문서 검증</h1>
<p>XML은 개발자가 자유롭게 태그를 만들 수 있다.</p>
<p>그러다 보니 사람마다 태그 이름을 다르게 작성하면 문제가 발생한다.</p>
<p>이를 방지하기 위해 XML 문서의 규칙을 정의하는 방법이 존재한다.</p>
<p>대표적인 방법은</p>
<ul>
<li>DTD(Document Type Definition)</li>
<li>XML Schema(XSD)</li>
</ul>
<p>이다.</p>
<p>이를 이용하면 XML 문서가 정해진 규칙대로 작성되었는지 검사할 수 있다.</p>
<p>이러한 XML을 <strong>Valid XML</strong>이라고 한다.</p>
<hr>
<h1 id="xml-parsing">XML Parsing</h1>
<p>XML은 단순한 문자열이 아니다.</p>
<p>프로그램에서 원하는 데이터를 사용하려면 XML을 분석해야 한다.</p>
<p>이 과정을 <strong>Parsing</strong>이라고 한다.</p>
<pre><code class="language-text">XML 문서

↓

Parser

↓

Java 객체</code></pre>
<p>Java에서는 대표적으로 두 가지 Parser를 제공한다.</p>
<ul>
<li>SAX Parser</li>
<li>DOM Parser</li>
</ul>
<hr>
<h1 id="sax-parser">SAX Parser</h1>
<h2 id="sax란">SAX란?</h2>
<p>SAX(Simple API for XML)는 XML을 처음부터 끝까지 한 줄씩 읽으면서 이벤트 방식으로 처리하는 Parser이다.</p>
<pre><code class="language-text">문서 읽기

↓

시작 태그 발견

↓

이벤트 발생

↓

데이터 처리

↓

다음 태그 읽기</code></pre>
<p>메모리에 XML 전체를 저장하지 않는다.</p>
<p>필요한 순간에만 처리한다.</p>
<hr>
<h2 id="특징">특징</h2>
<h3 id="장점-1">장점</h3>
<ul>
<li>매우 빠르다.</li>
<li>메모리 사용량이 적다.</li>
<li>대용량 XML 처리에 적합하다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>이미 지나간 데이터를 다시 읽을 수 없다.</li>
<li>앞뒤를 자유롭게 탐색하기 어렵다.</li>
<li>구조를 자유롭게 수정하기 어렵다.</li>
</ul>
<p>즉,</p>
<p><strong>속도는 빠르지만 탐색은 불편하다.</strong></p>
<hr>
<h2 id="이벤트-기반-처리">이벤트 기반 처리</h2>
<p>SAX는 태그를 만날 때마다 이벤트가 발생한다.</p>
<p>예를 들어</p>
<pre><code class="language-xml">&lt;name&gt;Kim&lt;/name&gt;</code></pre>
<p>을 읽으면</p>
<pre><code class="language-text">startElement()

↓

characters()

↓

endElement()</code></pre>
<p>순서대로 호출된다.</p>
<p>개발자는 이벤트가 발생했을 때 필요한 작업을 수행하면 된다.</p>
<hr>
<h2 id="handler">Handler</h2>
<p>SAX Parser는 이벤트가 발생했을 때 실행할 코드를 Handler에 작성한다.</p>
<p>대표적인 메서드는 다음과 같다.</p>
<pre><code class="language-java">startElement()

characters()

endElement()</code></pre>
<p>각 메서드가 호출될 때 원하는 데이터를 저장하면 된다.</p>
<hr>
<h2 id="record-클래스">Record 클래스</h2>
<p>Java에서는 XML 데이터를 저장하기 위해 Record를 사용할 수도 있다.</p>
<p>Record는 데이터를 저장하기 위한 불변 객체이다.</p>
<pre><code class="language-java">public record Person(
    String name,
    int age
) {}</code></pre>
<p>Record의 특징은 다음과 같다.</p>
<ul>
<li>모든 필드는 final</li>
<li>Setter가 없다.</li>
<li>생성자, equals(), hashCode(), toString() 등을 자동 생성한다.</li>
</ul>
<p>DTO(Data Transfer Object)를 만들 때 매우 유용하다.</p>
<hr>
<h1 id="dom-parser">DOM Parser</h1>
<h2 id="dom이란">DOM이란?</h2>
<p>DOM(Document Object Model)은 XML 문서를 전부 메모리에 읽은 후 트리 구조를 생성한다.</p>
<pre><code class="language-text">XML

↓

메모리에 전체 저장

↓

트리 생성

↓

원하는 노드 탐색</code></pre>
<hr>
<h2 id="특징-1">특징</h2>
<h3 id="장점-2">장점</h3>
<ul>
<li>원하는 위치를 자유롭게 탐색 가능</li>
<li>수정 가능</li>
<li>부모, 자식 노드 접근 가능</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>메모리 사용량이 크다.</li>
<li>속도가 느리다.</li>
<li>큰 XML에서는 부담이 크다.</li>
</ul>
<p>즉,</p>
<p><strong>탐색은 편하지만 무겁다.</strong></p>
<hr>
<h2 id="node-구조">Node 구조</h2>
<p>DOM에서는 XML을 모두 Node로 관리한다.</p>
<p>예를 들어</p>
<pre><code class="language-xml">&lt;person&gt;
    &lt;name&gt;Kim&lt;/name&gt;
&lt;/person&gt;</code></pre>
<p>은 다음과 같은 트리 구조가 된다.</p>
<pre><code class="language-text">person
 └── name
      └── Kim</code></pre>
<p>태그, 속성, 값 모두 Node로 관리된다.</p>
<hr>
<h1 id="sax와-dom-비교">SAX와 DOM 비교</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>SAX</th>
<th>DOM</th>
</tr>
</thead>
<tbody><tr>
<td>읽는 방식</td>
<td>순차적으로 읽음</td>
<td>전체를 메모리에 로드</td>
</tr>
<tr>
<td>메모리 사용</td>
<td>적음</td>
<td>많음</td>
</tr>
<tr>
<td>속도</td>
<td>빠름</td>
<td>느림</td>
</tr>
<tr>
<td>탐색</td>
<td>어려움</td>
<td>자유로움</td>
</tr>
<tr>
<td>수정</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>대용량 처리</td>
<td>유리</td>
<td>불리</td>
</tr>
</tbody></table>
<p>간단히 기억하면</p>
<blockquote>
<p><strong>SAX는 빠르고 가볍고, DOM은 무겁지만 자유롭다.</strong></p>
</blockquote>
<hr>
<h1 id="json-1">JSON</h1>
<h2 id="json이란">JSON이란?</h2>
<p>JSON(JavaScript Object Notation)은 현재 가장 많이 사용되는 데이터 교환 형식이다.</p>
<p>사람이 읽기 쉽고 용량도 작으며 대부분의 프로그래밍 언어에서 사용할 수 있다.</p>
<p>대표적인 형태는 다음과 같다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Kim&quot;,
  &quot;age&quot;: 20,
  &quot;major&quot;: &quot;Computer&quot;
}</code></pre>
<p>객체는 Key-Value 형태로 구성된다.</p>
<hr>
<h2 id="배열-표현">배열 표현</h2>
<p>JSON은 배열도 표현할 수 있다.</p>
<pre><code class="language-json">{
  &quot;students&quot;: [
    {
      &quot;name&quot;: &quot;Kim&quot;
    },
    {
      &quot;name&quot;: &quot;Lee&quot;
    }
  ]
}</code></pre>
<hr>
<h2 id="json의-장점">JSON의 장점</h2>
<p>JSON이 널리 사용되는 이유는 다음과 같다.</p>
<ul>
<li>문법이 단순하다.</li>
<li>사람이 읽기 쉽다.</li>
<li>용량이 작다.</li>
<li>대부분의 언어에서 지원한다.</li>
<li>웹과 모바일 환경에서 매우 많이 사용된다.</li>
</ul>
<p>현재 REST API에서는 거의 JSON을 사용한다고 생각해도 될 정도이다.</p>
<hr>
<h2 id="xml과-json-비교">XML과 JSON 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>XML</th>
<th>JSON</th>
</tr>
</thead>
<tbody><tr>
<td>문법</td>
<td>태그 기반</td>
<td>Key-Value 기반</td>
</tr>
<tr>
<td>가독성</td>
<td>복잡한 편</td>
<td>간결함</td>
</tr>
<tr>
<td>용량</td>
<td>크다</td>
<td>작다</td>
</tr>
<tr>
<td>속도</td>
<td>느린 편</td>
<td>빠른 편</td>
</tr>
<tr>
<td>계층 구조</td>
<td>가능</td>
<td>가능</td>
</tr>
<tr>
<td>현재 활용도</td>
<td>공공기관, 레거시 시스템</td>
<td>대부분의 REST API</td>
</tr>
</tbody></table>
<p>최근에는 대부분 JSON을 사용하지만,</p>
<p>공공데이터나 오래된 시스템에서는 XML도 자주 사용되므로 두 형식을 모두 이해하고 있어야 한다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 Java에서 사용하는 XML과 JSON의 구조와 XML Parser를 정리했다.</p>
<ul>
<li>공공데이터는 CSV, XML, JSON 형태로 제공된다.</li>
<li>CSV는 단순하지만 구조 표현이 어렵다.</li>
<li>XML은 태그 기반의 구조적인 데이터 형식이다.</li>
<li>XML은 Well-Formed 문법을 반드시 따라야 한다.</li>
<li>DTD와 XML Schema를 이용하면 XML 문서를 검증할 수 있다.</li>
<li>XML Parsing은 XML 데이터를 프로그램에서 사용할 수 있도록 분석하는 과정이다.</li>
<li>SAX Parser는 이벤트 기반으로 동작하며 빠르고 메모리 사용량이 적다.</li>
<li>DOM Parser는 문서를 모두 메모리에 저장한 후 트리 구조로 탐색한다.</li>
<li>Record는 불변 DTO를 간결하게 작성하기 위한 문법이다.</li>
<li>JSON은 Key-Value 구조를 사용하는 데이터 형식이다.</li>
<li>현재 대부분의 REST API는 JSON을 사용한다.</li>
<li>XML은 공공기관과 레거시 시스템에서 여전히 많이 사용된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (9) 예외 처리]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-9-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@tongmill_g/JAVA-9-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 29 Jul 2026 02:27:11 GMT</pubDate>
            <description><![CDATA[<h1 id="예외-처리-완벽-정리">예외 처리 완벽 정리</h1>
<h2 id="들어가며">들어가며</h2>
<p>프로그램을 실행하다 보면 개발자가 예상하지 못한 상황이 발생할 수 있다.</p>
<p>사용자가 숫자를 입력해야 하는 곳에 문자열을 입력하거나, 존재하지 않는 파일을 읽으려고 할 수도 있다. 배열의 범위를 벗어난 인덱스에 접근하거나 <code>null</code>인 객체의 메서드를 호출할 수도 있다.</p>
<pre><code class="language-java">int result = 10 / 0;</code></pre>
<p>위 코드는 실행 중 <code>ArithmeticException</code>을 발생시킨다.</p>
<p>예외를 처리하지 않으면 프로그램은 해당 지점에서 비정상적으로 종료될 수 있다. 그러나 모든 오류 상황에서 프로그램을 즉시 종료하는 것이 적절한 것은 아니다.</p>
<p>잘못된 입력을 다시 요청하거나, 파일이 없다는 메시지를 출력하거나, 작업을 취소한 뒤 안전하게 다음 흐름으로 이동할 수 있어야 한다.</p>
<p>Java는 이러한 상황을 처리하기 위해 <strong>예외 처리(Exception Handling)</strong> 기능을 제공한다.</p>
<p>이번 글에서는 예외 클래스의 계층 구조부터 Checked Exception과 Unchecked Exception의 차이, <code>try-catch-finally</code>, <code>try-with-resources</code>, <code>throw</code>와 <code>throws</code>, 예외 변환, 사용자 정의 예외까지 정리해보려고 한다.</p>
<hr>
<h1 id="예외란-무엇인가">예외란 무엇인가</h1>
<h2 id="프로그램의-오류-상황">프로그램의 오류 상황</h2>
<p>프로그램 실행 중 정상적인 흐름을 방해하는 문제는 크게 Error와 Exception으로 구분할 수 있다.</p>
<pre><code class="language-text">Throwable
├─ Error
└─ Exception</code></pre>
<p><code>Throwable</code>은 Java에서 오류와 예외를 표현하는 최상위 클래스다.</p>
<hr>
<h2 id="error">Error</h2>
<p><code>Error</code>는 일반적으로 애플리케이션 코드에서 복구하기 어려운 심각한 문제를 나타낸다.</p>
<p>대표적인 예는 다음과 같다.</p>
<ul>
<li><code>OutOfMemoryError</code></li>
<li><code>StackOverflowError</code></li>
<li><code>VirtualMachineError</code></li>
</ul>
<pre><code class="language-java">public static void recursive() {
    recursive();
}</code></pre>
<p>위 메서드를 계속 호출하면 호출 스택이 가득 차면서 <code>StackOverflowError</code>가 발생할 수 있다.</p>
<p>Error는 대부분 개발자가 <code>try-catch</code>로 복구하기보다 원인을 찾아 프로그램이나 실행 환경을 수정해야 하는 문제다.</p>
<hr>
<h2 id="exception">Exception</h2>
<p><code>Exception</code>은 프로그램에서 처리하거나 복구할 가능성이 있는 예외 상황을 나타낸다.</p>
<p>대표적인 예는 다음과 같다.</p>
<ul>
<li>잘못된 숫자 변환</li>
<li>존재하지 않는 파일 접근</li>
<li>배열 범위 초과</li>
<li>잘못된 객체 형변환</li>
<li><code>null</code> 객체 사용</li>
<li>데이터베이스 연결 실패</li>
</ul>
<p>예외는 모두 객체이며, 예외가 발생하면 JVM은 해당 상황을 표현하는 예외 객체를 생성한다.</p>
<pre><code class="language-text">예외 상황 발생
→ 예외 객체 생성
→ 예외 객체를 던짐
→ 처리 가능한 catch 탐색</code></pre>
<hr>
<h2 id="예외-클래스의-계층">예외 클래스의 계층</h2>
<p>Java 예외는 다음과 같은 계층 구조를 가진다.</p>
<pre><code class="language-text">Throwable
├─ Error
└─ Exception
   ├─ RuntimeException
   │  ├─ NullPointerException
   │  ├─ ArithmeticException
   │  ├─ ClassCastException
   │  ├─ NumberFormatException
   │  └─ IndexOutOfBoundsException
   │
   ├─ IOException
   ├─ SQLException
   ├─ ClassNotFoundException
   └─ ...</code></pre>
<p><code>RuntimeException</code>을 기준으로 Checked Exception과 Unchecked Exception을 구분한다.</p>
<hr>
<h1 id="checked-exception과-unchecked-exception">Checked Exception과 Unchecked Exception</h1>
<h2 id="checked-exception">Checked Exception</h2>
<p>Checked Exception은 컴파일러가 예외 처리 여부를 확인하는 예외다.</p>
<p>예외가 발생할 수 있는 코드를 작성했다면 다음 중 하나를 반드시 선택해야 한다.</p>
<ul>
<li><code>try-catch</code>로 직접 처리</li>
<li><code>throws</code>로 호출한 곳에 처리 책임 전달</li>
</ul>
<p>처리하지 않으면 컴파일이 되지 않는다.</p>
<p>대표적인 Checked Exception은 다음과 같다.</p>
<ul>
<li><code>IOException</code></li>
<li><code>SQLException</code></li>
<li><code>ClassNotFoundException</code></li>
<li><code>InterruptedException</code></li>
</ul>
<pre><code class="language-java">Class.forName(&quot;com.example.Sample&quot;);</code></pre>
<p><code>Class.forName()</code>은 <code>ClassNotFoundException</code>이 발생할 수 있으므로 반드시 처리해야 한다.</p>
<pre><code class="language-java">public class CheckedExceptionExample {

    public static void main(String[] args) {
        try {
            Class.forName(
                    &quot;com.example.Sample&quot;
            );
        } catch (ClassNotFoundException e) {
            System.out.println(
                    &quot;클래스를 찾을 수 없습니다.&quot;
            );
        }

        System.out.println(&quot;프로그램 정상 종료&quot;);
    }
}</code></pre>
<hr>
<h2 id="unchecked-exception">Unchecked Exception</h2>
<p>Unchecked Exception은 컴파일러가 예외 처리 여부를 강제하지 않는 예외다.</p>
<p><code>RuntimeException</code>과 그 하위 클래스가 해당한다.</p>
<p>대표적인 예는 다음과 같다.</p>
<ul>
<li><code>NullPointerException</code></li>
<li><code>ArithmeticException</code></li>
<li><code>NumberFormatException</code></li>
<li><code>ClassCastException</code></li>
<li><code>IndexOutOfBoundsException</code></li>
<li><code>IllegalArgumentException</code></li>
</ul>
<pre><code class="language-java">int number =
        Integer.parseInt(&quot;Java&quot;);</code></pre>
<p>위 코드는 <code>NumberFormatException</code>을 발생시킬 수 있지만 처리 코드가 없어도 컴파일된다.</p>
<pre><code class="language-java">public class UncheckedExceptionExample {

    public static void main(String[] args) {
        int number =
                Integer.parseInt(&quot;Java&quot;);

        System.out.println(number);
    }
}</code></pre>
<p>실행 시 예외가 발생하면 프로그램이 중단된다.</p>
<hr>
<h2 id="checked와-unchecked-비교">Checked와 Unchecked 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Checked Exception</th>
<th>Unchecked Exception</th>
</tr>
</thead>
<tbody><tr>
<td>상속 계층</td>
<td><code>RuntimeException</code> 이외의 <code>Exception</code> 하위</td>
<td><code>RuntimeException</code> 하위</td>
</tr>
<tr>
<td>컴파일러 검사</td>
<td>처리 여부 검사</td>
<td>처리 여부 강제하지 않음</td>
</tr>
<tr>
<td>처리 방법</td>
<td><code>try-catch</code> 또는 <code>throws</code> 필수</td>
<td>필요에 따라 처리</td>
</tr>
<tr>
<td>대표 사례</td>
<td>파일, DB, 네트워크 등 외부 자원</td>
<td>잘못된 인수, null, 인덱스 오류</td>
</tr>
<tr>
<td>주된 의미</td>
<td>복구 가능한 외부 문제</td>
<td>프로그래밍 오류나 잘못된 상태</td>
</tr>
</tbody></table>
<p>Checked Exception이라고 항상 복구 가능한 것은 아니고, Unchecked Exception이라고 절대 처리하면 안 되는 것도 아니다.</p>
<p>중요한 것은 예외의 종류보다 <strong>현재 계층에서 의미 있게 처리할 수 있는가</strong>다.</p>
<hr>
<h1 id="try-catch로-예외-처리하기">try-catch로 예외 처리하기</h1>
<h2 id="try-catch-구조">try-catch 구조</h2>
<p>예외가 발생할 수 있는 코드를 <code>try</code> 블록에 작성하고, 예외가 발생했을 때 실행할 코드를 <code>catch</code> 블록에 작성한다.</p>
<pre><code class="language-java">try {
    // 예외가 발생할 수 있는 코드
} catch (ExceptionType e) {
    // 예외가 발생했을 때 처리할 코드
}</code></pre>
<p>문자열을 숫자로 변환하는 예제를 살펴보자.</p>
<pre><code class="language-java">public class TryCatchExample {

    public static void main(String[] args) {
        String input = &quot;Java&quot;;

        try {
            int number =
                    Integer.parseInt(input);

            System.out.println(number);
        } catch (NumberFormatException e) {
            System.out.println(
                    &quot;숫자로 변환할 수 없습니다.&quot;
            );
        }

        System.out.println(
                &quot;프로그램을 계속 실행합니다.&quot;
        );
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">숫자로 변환할 수 없습니다.
프로그램을 계속 실행합니다.</code></pre>
<hr>
<h2 id="try-catch의-실행-흐름">try-catch의 실행 흐름</h2>
<p><code>try</code> 블록에서 예외가 발생하지 않으면 <code>catch</code> 블록은 실행되지 않는다.</p>
<pre><code class="language-text">try 시작
→ 정상 실행
→ catch 건너뜀
→ 다음 코드 실행</code></pre>
<p>예외가 발생하면 해당 지점 이후의 <code>try</code> 코드는 실행되지 않는다.</p>
<pre><code class="language-text">try 시작
→ 예외 발생
→ 예외 객체 생성
→ 일치하는 catch로 이동
→ catch 실행
→ 다음 코드 실행</code></pre>
<pre><code class="language-java">public class ExceptionFlowExample {

    public static void main(String[] args) {
        try {
            System.out.println(&quot;A&quot;);

            int result = 10 / 0;

            System.out.println(&quot;B&quot;);
        } catch (ArithmeticException e) {
            System.out.println(&quot;C&quot;);
        }

        System.out.println(&quot;D&quot;);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">A
C
D</code></pre>
<p><code>10 / 0</code>에서 예외가 발생했으므로 <code>&quot;B&quot;</code>는 출력되지 않는다.</p>
<hr>
<h2 id="catch의-예외-변수">catch의 예외 변수</h2>
<pre><code class="language-java">catch (NumberFormatException e)</code></pre>
<p><code>e</code>에는 JVM이 던진 예외 객체가 전달된다.</p>
<p>예외 객체를 통해 어떤 문제가 발생했는지 확인할 수 있다.</p>
<pre><code class="language-java">catch (NumberFormatException e) {
    System.out.println(e.getMessage());
}</code></pre>
<hr>
<h1 id="예외-객체의-정보-확인하기">예외 객체의 정보 확인하기</h1>
<p>모든 예외 클래스는 <code>Throwable</code>을 상속하므로 <code>Throwable</code>이 제공하는 메서드를 사용할 수 있다.</p>
<hr>
<h2 id="getmessage">getMessage()</h2>
<p>예외에 포함된 상세 메시지를 반환한다.</p>
<pre><code class="language-java">try {
    Integer.parseInt(&quot;Java&quot;);
} catch (NumberFormatException e) {
    System.out.println(e.getMessage());
}</code></pre>
<p>출력되는 메시지는 예외가 발생한 원인과 입력값에 대한 정보를 포함할 수 있다.</p>
<hr>
<h2 id="getcause">getCause()</h2>
<p>현재 예외의 원인이 된 다른 예외 객체를 반환한다.</p>
<p>원인이 설정되어 있지 않다면 <code>null</code>을 반환한다.</p>
<pre><code class="language-java">Throwable cause = e.getCause();</code></pre>
<p>예외를 다른 예외로 변환해 던질 때 원래 예외를 연결해두면 <code>getCause()</code>로 확인할 수 있다.</p>
<hr>
<h2 id="printstacktrace">printStackTrace()</h2>
<p>예외가 발생한 위치와 메서드 호출 흐름을 출력한다.</p>
<pre><code class="language-java">catch (Exception e) {
    e.printStackTrace();
}</code></pre>
<p>Stack Trace에는 다음 정보가 포함된다.</p>
<ul>
<li>발생한 예외의 종류</li>
<li>예외 메시지</li>
<li>예외가 발생한 클래스와 메서드</li>
<li>파일명과 코드 줄 번호</li>
<li>해당 메서드가 호출된 과정</li>
</ul>
<pre><code class="language-text">java.lang.NumberFormatException
    at java.lang.Integer.parseInt(...)
    at Example.convert(Example.java:15)
    at Example.main(Example.java:7)</code></pre>
<p>Stack Trace는 디버깅할 때 매우 중요한 정보다.</p>
<p>다만 실제 서버 애플리케이션에서는 <code>printStackTrace()</code>보다 로깅 프레임워크를 통해 예외를 기록하는 경우가 많다.</p>
<hr>
<h1 id="여러-예외-처리하기">여러 예외 처리하기</h1>
<h2 id="여러-catch-블록">여러 catch 블록</h2>
<p>하나의 <code>try</code> 블록에서 여러 종류의 예외가 발생할 수 있다면 여러 <code>catch</code> 블록을 작성할 수 있다.</p>
<pre><code class="language-java">public class MultipleCatchExample {

    public static void main(String[] args) {
        String[] values = {
                &quot;10&quot;,
                &quot;Java&quot;
        };

        try {
            int index = 2;

            int number = Integer.parseInt(
                    values[index]
            );

            System.out.println(number);
        } catch (
                ArrayIndexOutOfBoundsException e
        ) {
            System.out.println(
                    &quot;배열 인덱스를 확인하세요.&quot;
            );
        } catch (
                NumberFormatException e
        ) {
            System.out.println(
                    &quot;숫자 형식을 확인하세요.&quot;
            );
        }
    }
}</code></pre>
<p>발생한 예외와 일치하는 <code>catch</code> 블록 하나만 실행된다.</p>
<hr>
<h2 id="catch-순서">catch 순서</h2>
<p>예외 클래스도 상속 관계를 가진다.</p>
<pre><code class="language-text">Exception
↑
RuntimeException
↑
NumberFormatException</code></pre>
<p><code>catch</code> 블록을 찾을 때 다형성이 적용되므로 자식 예외부터 부모 예외 순서로 작성해야 한다.</p>
<pre><code class="language-java">try {
    Integer.parseInt(&quot;Java&quot;);
} catch (NumberFormatException e) {
    System.out.println(
            &quot;숫자 변환 오류&quot;
    );
} catch (Exception e) {
    System.out.println(
            &quot;그 밖의 예외&quot;
    );
}</code></pre>
<p>상위 예외를 먼저 작성하면 하위 예외의 <code>catch</code> 블록은 실행될 기회가 없다.</p>
<pre><code class="language-java">try {
    Integer.parseInt(&quot;Java&quot;);
} catch (Exception e) {
    System.out.println(&quot;전체 예외 처리&quot;);
}

// 컴파일 오류
// catch (NumberFormatException e) {
// }</code></pre>
<p><code>Exception</code>이 <code>NumberFormatException</code>까지 먼저 처리하기 때문에 뒤의 블록은 도달할 수 없는 코드가 된다.</p>
<hr>
<h2 id="멀티-catch">멀티 catch</h2>
<p>처리 방법이 같은 여러 예외를 하나의 <code>catch</code> 블록에서 처리할 수 있다.</p>
<pre><code class="language-java">try {
    // 예외 발생 가능 코드
} catch (
        NumberFormatException
        | NullPointerException e
) {
    System.out.println(
            &quot;입력값을 확인하세요.&quot;
    );
}</code></pre>
<p><code>|</code>로 연결한 예외는 서로 부모와 자식 관계이면 안 된다.</p>
<pre><code class="language-java">// 사용할 수 없음
// catch (RuntimeException
//        | NumberFormatException e) {
// }</code></pre>
<p><code>NumberFormatException</code>이 이미 <code>RuntimeException</code>에 포함되기 때문이다.</p>
<hr>
<h2 id="구체적인-예외-처리와-통합-처리">구체적인 예외 처리와 통합 처리</h2>
<p>가능하면 예외 상황에 맞는 구체적인 처리를 제공하는 것이 좋다.</p>
<pre><code class="language-java">catch (NumberFormatException e) {
    System.out.println(
            &quot;숫자만 입력할 수 있습니다.&quot;
    );
}</code></pre>
<p>모든 예외를 무조건 <code>Exception</code> 하나로 처리하면 사용자에게 정확한 원인을 설명하기 어렵다.</p>
<pre><code class="language-java">catch (Exception e) {
    System.out.println(
            &quot;오류가 발생했습니다.&quot;
    );
}</code></pre>
<p>그러나 모든 예외를 지나치게 세분화하면 코드가 복잡해질 수 있다.</p>
<p>같은 방식으로 처리할 예외들은 멀티 catch나 공통 상위 타입으로 묶는 것이 적절하다.</p>
<hr>
<h1 id="finally와-자원-정리">finally와 자원 정리</h1>
<h2 id="finally">finally</h2>
<p><code>finally</code> 블록은 예외 발생 여부와 관계없이 <code>try-catch</code>가 끝날 때 실행된다.</p>
<pre><code class="language-java">try {
    System.out.println(&quot;작업 실행&quot;);
} catch (Exception e) {
    System.out.println(&quot;예외 처리&quot;);
} finally {
    System.out.println(&quot;항상 실행&quot;);
}</code></pre>
<p>예외가 발생하지 않아도 실행되고, 예외가 발생해 <code>catch</code>가 실행된 뒤에도 실행된다.</p>
<hr>
<h2 id="return이-있어도-finally가-먼저-실행된다">return이 있어도 finally가 먼저 실행된다</h2>
<pre><code class="language-java">public class FinallyReturnExample {

    public static int test() {
        try {
            System.out.println(&quot;try&quot;);
            return 1;
        } finally {
            System.out.println(&quot;finally&quot;);
        }
    }

    public static void main(String[] args) {
        int result = test();

        System.out.println(result);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">try
finally
1</code></pre>
<p><code>return</code>으로 메서드가 종료되기 전에 <code>finally</code>가 먼저 실행된다.</p>
<hr>
<h2 id="finally가-실행되지-않을-수-있는-경우">finally가 실행되지 않을 수 있는 경우</h2>
<p><code>finally</code>를 일반적으로 항상 실행된다고 설명하지만 절대적인 보장은 아니다.</p>
<p>다음과 같은 경우 실행되지 않을 수 있다.</p>
<ul>
<li><code>System.exit()</code>로 JVM이 종료된 경우</li>
<li>JVM 자체가 비정상 종료된 경우</li>
<li>운영체제나 프로세스가 강제로 종료된 경우</li>
<li>전원이 차단된 경우</li>
</ul>
<pre><code class="language-java">try {
    System.exit(0);
} finally {
    System.out.println(
            &quot;실행되지 않을 수 있음&quot;
    );
}</code></pre>
<hr>
<h2 id="finally의-주요-목적">finally의 주요 목적</h2>
<p><code>finally</code>는 주로 사용한 자원을 정리하기 위해 사용한다.</p>
<p>예를 들어 다음 자원은 사용 후 반납이 필요하다.</p>
<ul>
<li>파일 스트림</li>
<li>네트워크 연결</li>
<li>데이터베이스 연결</li>
<li>입력 스트림</li>
<li>출력 스트림</li>
</ul>
<p>자원을 반납하지 않으면 시스템 자원이 계속 점유되는 Resource Leak이 발생할 수 있다.</p>
<pre><code class="language-java">FileInputStream input = null;

try {
    input = new FileInputStream(
            &quot;data.txt&quot;
    );

    int value = input.read();

    System.out.println(value);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (input != null) {
        try {
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}</code></pre>
<p>하지만 자원을 닫는 <code>close()</code>에서도 예외가 발생할 수 있기 때문에 코드가 복잡해진다.</p>
<p>이를 개선하기 위해 <code>try-with-resources</code>가 도입되었다.</p>
<hr>
<h1 id="try-with-resources">try-with-resources</h1>
<h2 id="자동-자원-반납">자동 자원 반납</h2>
<p><code>try-with-resources</code>는 <code>try</code> 선언부에서 생성한 자원을 자동으로 닫아주는 문법이다.</p>
<pre><code class="language-java">try (
    Resource resource = new Resource()
) {
    // 자원 사용
} catch (Exception e) {
    // 예외 처리
}</code></pre>
<p><code>try</code> 블록을 벗어날 때 자동으로 <code>close()</code>가 호출된다.</p>
<pre><code class="language-java">import java.io.FileInputStream;
import java.io.IOException;

public class ResourceExample {

    public static void main(String[] args) {
        try (
            FileInputStream input =
                    new FileInputStream(
                            &quot;data.txt&quot;
                    )
        ) {
            int value = input.read();

            System.out.println(value);
        } catch (IOException e) {
            System.out.println(
                    &quot;파일 처리 중 오류가 발생했습니다.&quot;
            );

            e.printStackTrace();
        }
    }
}</code></pre>
<p><code>finally</code>에서 직접 <code>close()</code>를 호출하지 않아도 된다.</p>
<hr>
<h2 id="autocloseable">AutoCloseable</h2>
<p><code>try-with-resources</code>에서 사용할 객체는 <code>AutoCloseable</code> 인터페이스를 구현해야 한다.</p>
<pre><code class="language-java">public interface AutoCloseable {

    void close() throws Exception;
}</code></pre>
<p><code>FileInputStream</code>, <code>BufferedReader</code>, JDBC의 <code>Connection</code>과 같은 많은 자원 클래스가 <code>AutoCloseable</code>을 구현한다.</p>
<hr>
<h2 id="여러-자원-사용하기">여러 자원 사용하기</h2>
<p>여러 자원을 함께 선언할 수 있다.</p>
<pre><code class="language-java">try (
    FileInputStream input =
            new FileInputStream(
                    &quot;input.txt&quot;
            );

    FileOutputStream output =
            new FileOutputStream(
                    &quot;output.txt&quot;
            )
) {
    output.write(input.read());
}</code></pre>
<p>자원은 선언한 순서의 반대로 닫힌다.</p>
<pre><code class="language-text">input 생성
output 생성

작업 종료

output close
input close</code></pre>
<hr>
<h2 id="try-with-resources의-변수">try-with-resources의 변수</h2>
<p><code>try</code> 선언부의 자원 변수는 다시 대입할 수 없다.</p>
<pre><code class="language-java">try (
    FileInputStream input =
            new FileInputStream(
                    &quot;data.txt&quot;
            )
) {
    // input = new FileInputStream(&quot;other.txt&quot;);
    // 재할당 불가
}</code></pre>
<p>사실상 <code>final</code>처럼 취급된다.</p>
<hr>
<h2 id="java-9-이후의-개선">Java 9 이후의 개선</h2>
<p>Java 9부터는 외부에서 선언된 <code>final</code> 또는 사실상 final인 변수를 <code>try</code> 선언부에서 사용할 수 있다.</p>
<pre><code class="language-java">FileInputStream input =
        new FileInputStream(&quot;data.txt&quot;);

try (input) {
    System.out.println(input.read());
}</code></pre>
<p>단, 해당 변수는 이후 다른 값으로 재할당되지 않은 상태여야 한다.</p>
<hr>
<h1 id="throw와-throws">throw와 throws</h1>
<h2 id="throw">throw</h2>
<p><code>throw</code>는 예외 객체를 직접 발생시키는 키워드다.</p>
<pre><code class="language-java">throw new IllegalArgumentException(
        &quot;나이는 음수일 수 없습니다.&quot;
);</code></pre>
<p>예외 상황을 개발자가 직접 정의하여 발생시킬 수 있다.</p>
<pre><code class="language-java">public class Member {

    private int age;

    public void setAge(int age) {
        if (age &lt; 0) {
            throw new IllegalArgumentException(
                    &quot;나이는 음수일 수 없습니다.&quot;
            );
        }

        this.age = age;
    }
}</code></pre>
<hr>
<h2 id="throws">throws</h2>
<p><code>throws</code>는 메서드에서 발생한 예외를 직접 처리하지 않고 호출한 곳으로 전달한다고 선언하는 키워드다.</p>
<pre><code class="language-java">public void readFile()
        throws IOException {
}</code></pre>
<p>예외가 사라지는 것이 아니라 처리 책임이 호출자에게 이동한다.</p>
<pre><code class="language-java">public static void readFile()
        throws IOException {

    FileInputStream input =
            new FileInputStream(
                    &quot;data.txt&quot;
            );

    input.close();
}</code></pre>
<p>호출한 메서드는 다시 처리하거나 위임해야 한다.</p>
<pre><code class="language-java">public static void main(String[] args) {
    try {
        readFile();
    } catch (IOException e) {
        System.out.println(
                &quot;파일을 처리할 수 없습니다.&quot;
        );
    }
}</code></pre>
<hr>
<h2 id="throw와-throws-차이">throw와 throws 차이</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>throw</code></th>
<th><code>throws</code></th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>예외 객체 직접 발생</td>
<td>예외 처리 책임 전달</td>
</tr>
<tr>
<td>위치</td>
<td>메서드 실행 블록 내부</td>
<td>메서드 선언부</td>
</tr>
<tr>
<td>뒤에 오는 것</td>
<td>예외 객체</td>
<td>예외 클래스</td>
</tr>
<tr>
<td>개수</td>
<td>한 번에 하나의 객체</td>
<td>여러 예외 클래스 선언 가능</td>
</tr>
</tbody></table>
<pre><code class="language-java">throw new IOException();</code></pre>
<pre><code class="language-java">void read()
        throws IOException,
               SQLException {
}</code></pre>
<hr>
<h2 id="checked-exception과-throws">Checked Exception과 throws</h2>
<p>Checked Exception은 반드시 다음 중 하나로 처리해야 한다.</p>
<pre><code class="language-text">try-catch로 직접 처리
또는
throws로 호출자에게 위임</code></pre>
<pre><code class="language-java">public void loadClass()
        throws ClassNotFoundException {

    Class.forName(
            &quot;com.example.Sample&quot;
    );
}</code></pre>
<hr>
<h2 id="unchecked-exception과-throws">Unchecked Exception과 throws</h2>
<p>Unchecked Exception은 <code>throws</code>를 작성하지 않아도 자동으로 호출 스택을 따라 전달된다.</p>
<pre><code class="language-java">public void validate(int age) {
    if (age &lt; 0) {
        throw new IllegalArgumentException(
                &quot;잘못된 나이&quot;
        );
    }
}</code></pre>
<p>다음처럼 선언할 수도 있지만 컴파일러가 강제하지는 않는다.</p>
<pre><code class="language-java">public void validate(int age)
        throws IllegalArgumentException {
}</code></pre>
<p>Unchecked Exception을 <code>throws</code>에 작성하는 목적은 주로 메서드 사용자가 발생 가능성을 쉽게 알도록 문서화하는 것이다.</p>
<hr>
<h1 id="예외를-처리할지-위임할지-결정하기">예외를 처리할지 위임할지 결정하기</h1>
<p>현재 메서드에서 예외를 의미 있게 처리할 수 있다면 직접 처리한다.</p>
<pre><code class="language-java">try {
    int number =
            Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.out.println(
            &quot;숫자를 다시 입력하세요.&quot;
    );
}</code></pre>
<p>현재 계층에서 처리 방법을 결정할 수 없다면 상위 계층으로 전달한다.</p>
<pre><code class="language-java">public Member findMember(long id) {
    return repository.findById(id)
            .orElseThrow(
                    () -&gt;
                            new MemberNotFoundException(
                                    id
                            )
            );
}</code></pre>
<p>예외를 잡았지만 할 수 있는 일이 없다면 무조건 잡기보다 적절한 계층으로 전달하는 것이 더 좋을 수 있다.</p>
<hr>
<h1 id="메서드-오버라이딩과-throws">메서드 오버라이딩과 throws</h1>
<p>자식 클래스에서 메서드를 오버라이딩할 때 부모 메서드보다 더 넓은 Checked Exception을 선언할 수 없다.</p>
<pre><code class="language-java">public class Parent {

    public void work()
            throws IOException {
    }
}</code></pre>
<p>자식 클래스는 같은 예외나 더 구체적인 하위 예외를 선언할 수 있다.</p>
<pre><code class="language-java">public class Child extends Parent {

    @Override
    public void work()
            throws FileNotFoundException {
    }
}</code></pre>
<p>다음은 사용할 수 없다.</p>
<pre><code class="language-java">public class Child extends Parent {

    // 더 넓은 Exception 선언 불가
    // @Override
    // public void work()
    //         throws Exception {
    // }
}</code></pre>
<p>예외를 선언하지 않는 것도 가능하다.</p>
<pre><code class="language-java">@Override
public void work() {
}</code></pre>
<p>Unchecked Exception은 이 제한을 직접적으로 적용받지 않는다.</p>
<hr>
<h1 id="예외-변환과-exception-chaining">예외 변환과 Exception Chaining</h1>
<h2 id="예외-변환">예외 변환</h2>
<p>하위 계층에서 발생한 기술적인 예외를 상위 계층에서 이해하기 쉬운 업무 예외로 변환할 수 있다.</p>
<p>예를 들어 Repository에서 데이터베이스 관련 예외가 발생했다고 가정해보자.</p>
<pre><code class="language-java">public Member findById(long id) {
    try {
        return loadMemberFromDatabase(id);
    } catch (SQLException e) {
        throw new MemberDataAccessException(
                &quot;회원 데이터 조회 실패&quot;,
                e
        );
    }
}</code></pre>
<p>상위 계층은 <code>SQLException</code> 같은 데이터베이스 구현 세부사항 대신 <code>MemberDataAccessException</code>이라는 의미 있는 예외를 다룰 수 있다.</p>
<hr>
<h2 id="exception-chaining">Exception Chaining</h2>
<p>예외를 변환할 때 원래 예외를 함께 전달하는 것을 Exception Chaining이라고 한다.</p>
<pre><code class="language-java">throw new MemberDataAccessException(
        &quot;회원 데이터 조회 실패&quot;,
        e
);</code></pre>
<p>사용자 정의 예외 생성자에서 원인을 부모 생성자에 전달한다.</p>
<pre><code class="language-java">public class MemberDataAccessException
        extends RuntimeException {

    public MemberDataAccessException(
            String message,
            Throwable cause
    ) {
        super(message, cause);
    }
}</code></pre>
<p>원래 예외 정보를 보존했기 때문에 Stack Trace에서 전체 원인 흐름을 확인할 수 있다.</p>
<pre><code class="language-text">MemberDataAccessException
Caused by: SQLException</code></pre>
<p>예외를 변환하면서 원래 예외를 제거하면 실제 원인을 추적하기 어려워진다.</p>
<hr>
<h1 id="사용자-정의-예외">사용자 정의 예외</h1>
<h2 id="사용자-정의-예외가-필요한-이유">사용자 정의 예외가 필요한 이유</h2>
<p>Java 표준 예외만으로도 많은 상황을 표현할 수 있지만, 업무 의미를 명확하게 전달하기 위해 별도의 예외를 만들 수 있다.</p>
<p>예를 들어 다음 상황을 표현해보자.</p>
<ul>
<li>회원을 찾을 수 없음</li>
<li>주문을 취소할 수 없음</li>
<li>잔액이 부족함</li>
<li>이미 사용 중인 이메일</li>
<li>재고가 부족함</li>
</ul>
<pre><code class="language-java">throw new RuntimeException(
        &quot;회원을 찾을 수 없습니다.&quot;
);</code></pre>
<p>단순 <code>RuntimeException</code>보다 다음 예외가 의미를 더 명확하게 표현한다.</p>
<pre><code class="language-java">throw new MemberNotFoundException(id);</code></pre>
<hr>
<h2 id="unchecked-사용자-정의-예외">Unchecked 사용자 정의 예외</h2>
<p><code>RuntimeException</code>을 상속하면 Unchecked Exception이 된다.</p>
<pre><code class="language-java">public class MemberNotFoundException
        extends RuntimeException {

    public MemberNotFoundException(long id) {
        super(
            &quot;회원을 찾을 수 없습니다. id=&quot; + id
        );
    }
}</code></pre>
<p>사용할 때는 다음과 같다.</p>
<pre><code class="language-java">public Member findMember(long id) {
    if (id != 1L) {
        throw new MemberNotFoundException(id);
    }

    return new Member(id, &quot;김자바&quot;);
}</code></pre>
<p>호출한 쪽에서 반드시 <code>try-catch</code>를 작성할 필요는 없다.</p>
<hr>
<h2 id="checked-사용자-정의-예외">Checked 사용자 정의 예외</h2>
<p><code>Exception</code>을 직접 상속하면 Checked Exception이 된다.</p>
<pre><code class="language-java">public class InsufficientBalanceException
        extends Exception {

    public InsufficientBalanceException(
            long balance,
            long amount
    ) {
        super(
            &quot;잔액이 부족합니다. &quot;
                    + &quot;잔액=&quot;
                    + balance
                    + &quot;, 출금액=&quot;
                    + amount
        );
    }
}</code></pre>
<p>이 예외를 발생시키는 메서드는 처리하거나 <code>throws</code>로 선언해야 한다.</p>
<pre><code class="language-java">public void withdraw(long amount)
        throws InsufficientBalanceException {

    if (amount &gt; balance) {
        throw new InsufficientBalanceException(
                balance,
                amount
        );
    }

    balance -= amount;
}</code></pre>
<hr>
<h2 id="checked와-unchecked-중-무엇을-선택할까">Checked와 Unchecked 중 무엇을 선택할까?</h2>
<p>Checked Exception은 호출자가 반드시 처리하도록 강제할 수 있다.</p>
<p>하지만 호출 단계마다 <code>try-catch</code>나 <code>throws</code>가 추가되어 코드가 복잡해질 수 있다.</p>
<p>Unchecked Exception은 코드가 간결하고 여러 계층을 자연스럽게 전파할 수 있다.</p>
<p>하지만 예외 처리가 누락될 가능성이 있다.</p>
<p>최근 애플리케이션 개발에서는 다음과 같은 업무 예외를 Unchecked Exception으로 설계하는 경우가 많다.</p>
<ul>
<li>유효하지 않은 인수</li>
<li>존재하지 않는 데이터</li>
<li>업무 규칙 위반</li>
<li>처리할 수 없는 상태</li>
</ul>
<p>Checked Exception은 호출자가 실제로 복구하거나 다른 대안을 선택할 수 있을 때 고려하는 것이 좋다.</p>
<hr>
<h1 id="올바른-예외-처리-방법">올바른 예외 처리 방법</h1>
<h2 id="예외를-비워두지-않기">예외를 비워두지 않기</h2>
<p>다음과 같이 예외를 잡고 아무 작업도 하지 않으면 문제가 사라진 것처럼 보인다.</p>
<pre><code class="language-java">try {
    execute();
} catch (Exception e) {
}</code></pre>
<p>실제로는 예외 정보가 완전히 사라져 원인을 찾기 어렵다.</p>
<p>최소한 로그를 남기거나, 의미 있는 처리를 하거나, 다시 던져야 한다.</p>
<pre><code class="language-java">catch (Exception e) {
    logger.error(
            &quot;작업 실행 실패&quot;,
            e
    );

    throw e;
}</code></pre>
<hr>
<h2 id="exception을-무조건-잡지-않기">Exception을 무조건 잡지 않기</h2>
<pre><code class="language-java">catch (Exception e)</code></pre>
<p>는 대부분의 예외를 한 번에 잡을 수 있지만 너무 광범위하다.</p>
<p>복구할 수 없는 프로그래밍 오류까지 잘못 처리하여 문제를 숨길 수 있다.</p>
<p>가능하면 필요한 예외를 구체적으로 처리한다.</p>
<pre><code class="language-java">catch (NumberFormatException e) {
}</code></pre>
<p>최상위 계층에서 예외를 공통 처리해야 할 때는 <code>Exception</code>을 사용할 수 있지만, 내부 로직 곳곳에서 무분별하게 사용하지 않는 것이 좋다.</p>
<hr>
<h2 id="예외를-흐름-제어로-사용하지-않기">예외를 흐름 제어로 사용하지 않기</h2>
<p>다음 코드는 예외를 반복문 종료 조건으로 사용한다.</p>
<pre><code class="language-java">try {
    int index = 0;

    while (true) {
        System.out.println(
                values[index++]
        );
    }
} catch (
        ArrayIndexOutOfBoundsException e
) {
}</code></pre>
<p>예외는 정상적인 분기나 반복 종료를 위한 기능이 아니다.</p>
<p>다음처럼 정상적인 조건식을 사용해야 한다.</p>
<pre><code class="language-java">for (
    int index = 0;
    index &lt; values.length;
    index++
) {
    System.out.println(values[index]);
}</code></pre>
<p>예외 객체 생성과 Stack Trace 구성에는 비용도 발생한다.</p>
<hr>
<h2 id="상태-확인-메서드-활용하기">상태 확인 메서드 활용하기</h2>
<p>예외가 발생하기 전에 상태를 확인할 수 있다면 상태 확인 메서드를 사용할 수 있다.</p>
<pre><code class="language-java">Scanner scanner =
        new Scanner(System.in);

if (scanner.hasNextInt()) {
    int number = scanner.nextInt();

    System.out.println(number);
} else {
    System.out.println(
            &quot;숫자를 입력해야 합니다.&quot;
    );
}</code></pre>
<p><code>hasNextInt()</code>로 상태를 확인한 뒤 <code>nextInt()</code>를 호출하면 잘못된 입력에 대한 예외 발생을 줄일 수 있다.</p>
<p>다만 모든 상황에서 사전 검사만으로 예외를 완전히 막을 수 있는 것은 아니다. 파일이나 네트워크처럼 검사 이후 상태가 변할 수 있는 외부 자원은 실제 작업에서 발생하는 예외도 함께 처리해야 한다.</p>
<hr>
<h2 id="runtimeexception은-원인-수정이-우선일-수-있다">RuntimeException은 원인 수정이 우선일 수 있다</h2>
<p><code>NullPointerException</code>, <code>IndexOutOfBoundsException</code>, <code>ClassCastException</code> 같은 예외는 무조건 <code>try-catch</code>로 감싸기보다 발생 원인을 수정해야 하는 경우가 많다.</p>
<pre><code class="language-java">try {
    String name = null;

    System.out.println(name.length());
} catch (NullPointerException e) {
}</code></pre>
<p>다음처럼 잘못된 상태가 만들어지지 않도록 설계하는 것이 더 좋다.</p>
<pre><code class="language-java">if (name == null) {
    throw new IllegalArgumentException(
            &quot;이름은 null일 수 없습니다.&quot;
    );
}</code></pre>
<hr>
<h1 id="로그를-통해-예외-추적하기">로그를 통해 예외 추적하기</h1>
<p>예외가 발생했을 때 다음 정보를 확인해야 한다.</p>
<pre><code class="language-text">어떤 예외가 발생했는가?
예외 메시지는 무엇인가?
어느 코드에서 발생했는가?
어떤 메서드 호출을 통해 도달했는가?
원인 예외가 있는가?</code></pre>
<p>Stack Trace는 보통 가장 위의 예외만 보는 것이 아니라 아래쪽의 <code>Caused by</code>까지 확인해야 한다.</p>
<pre><code class="language-text">ServiceException
    at MemberService.find(...)

Caused by: SQLException
    at MemberRepository.find(...)</code></pre>
<p>실제 원인은 가장 아래쪽에 가까운 <code>Caused by</code>에 나타나는 경우가 많다.</p>
<hr>
<h2 id="printstacktrace보다-로깅-사용하기">printStackTrace보다 로깅 사용하기</h2>
<p>학습 단계에서는 다음 코드를 자주 사용한다.</p>
<pre><code class="language-java">e.printStackTrace();</code></pre>
<p>실제 프로젝트에서는 로깅 프레임워크를 사용하는 경우가 많다.</p>
<pre><code class="language-java">logger.error(
        &quot;회원 조회 중 예외 발생. id={}&quot;,
        id,
        e
);</code></pre>
<p>로그에는 다음 정보가 함께 포함될 수 있다.</p>
<ul>
<li>발생 시간</li>
<li>로그 레벨</li>
<li>스레드</li>
<li>클래스 이름</li>
<li>업무 식별자</li>
<li>예외 메시지와 Stack Trace</li>
</ul>
<p>예외 객체 <code>e</code>를 문자열로만 연결하면 Stack Trace가 누락될 수 있으므로 로거에 예외 객체 자체를 전달해야 한다.</p>
<hr>
<h1 id="종합-예제-회원-등록-예외-처리">종합 예제: 회원 등록 예외 처리</h1>
<p>회원 등록 과정에서 발생할 수 있는 예외를 사용자 정의 예외로 처리해보자.</p>
<h2 id="duplicateemailexception">DuplicateEmailException</h2>
<pre><code class="language-java">public class DuplicateEmailException
        extends RuntimeException {

    public DuplicateEmailException(
            String email
    ) {
        super(
            &quot;이미 사용 중인 이메일입니다. &quot;
                    + &quot;email=&quot;
                    + email
        );
    }
}</code></pre>
<hr>
<h2 id="invalidmemberexception">InvalidMemberException</h2>
<pre><code class="language-java">public class InvalidMemberException
        extends RuntimeException {

    public InvalidMemberException(
            String message
    ) {
        super(message);
    }
}</code></pre>
<hr>
<h2 id="member">Member</h2>
<pre><code class="language-java">public class Member {

    private final String name;
    private final String email;

    public Member(
            String name,
            String email
    ) {
        if (
            name == null
            || name.isBlank()
        ) {
            throw new InvalidMemberException(
                    &quot;이름은 비어 있을 수 없습니다.&quot;
            );
        }

        if (
            email == null
            || email.isBlank()
        ) {
            throw new InvalidMemberException(
                    &quot;이메일은 비어 있을 수 없습니다.&quot;
            );
        }

        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}</code></pre>
<hr>
<h2 id="memberrepository">MemberRepository</h2>
<pre><code class="language-java">import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class MemberRepository {

    private final Map&lt;String, Member&gt; members =
            new HashMap&lt;&gt;();

    public boolean existsByEmail(
            String email
    ) {
        return members.containsKey(email);
    }

    public void save(Member member) {
        members.put(
                member.getEmail(),
                member
        );
    }

    public Optional&lt;Member&gt; findByEmail(
            String email
    ) {
        return Optional.ofNullable(
                members.get(email)
        );
    }
}</code></pre>
<hr>
<h2 id="memberservice">MemberService</h2>
<pre><code class="language-java">public class MemberService {

    private final MemberRepository repository;

    public MemberService(
            MemberRepository repository
    ) {
        this.repository = repository;
    }

    public Member register(
            String name,
            String email
    ) {
        if (
            repository.existsByEmail(email)
        ) {
            throw new DuplicateEmailException(
                    email
            );
        }

        Member member =
                new Member(name, email);

        repository.save(member);

        return member;
    }
}</code></pre>
<hr>
<h2 id="실행-클래스">실행 클래스</h2>
<pre><code class="language-java">public class ExceptionExample {

    public static void main(String[] args) {
        MemberRepository repository =
                new MemberRepository();

        MemberService service =
                new MemberService(repository);

        try {
            Member first =
                    service.register(
                            &quot;김자바&quot;,
                            &quot;java@example.com&quot;
                    );

            System.out.println(
                    first.getName()
                            + &quot; 등록 완료&quot;
            );

            service.register(
                    &quot;이자바&quot;,
                    &quot;java@example.com&quot;
            );
        } catch (
                DuplicateEmailException e
        ) {
            System.out.println(
                    &quot;회원 등록 실패: &quot;
                            + e.getMessage()
            );
        } catch (
                InvalidMemberException e
        ) {
            System.out.println(
                    &quot;입력값 오류: &quot;
                            + e.getMessage()
            );
        } finally {
            System.out.println(
                    &quot;회원 등록 작업 종료&quot;
            );
        }

        System.out.println(
                &quot;프로그램 정상 종료&quot;
        );
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">김자바 등록 완료
회원 등록 실패: 이미 사용 중인 이메일입니다. email=java@example.com
회원 등록 작업 종료
프로그램 정상 종료</code></pre>
<p>이 예제에는 다음 개념이 포함되어 있다.</p>
<ul>
<li>사용자 정의 예외</li>
<li>Unchecked Exception</li>
<li><code>throw</code>를 이용한 예외 발생</li>
<li>여러 <code>catch</code> 블록</li>
<li><code>finally</code></li>
<li>예외를 이용한 업무 규칙 표현</li>
<li>컬렉션과 <code>Optional</code></li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="error와-exception">Error와 Exception</h2>
<pre><code class="language-text">Error
→ 일반적으로 애플리케이션에서 복구하기 어려운 심각한 문제

Exception
→ 프로그램 코드에서 처리할 수 있는 예외 상황</code></pre>
<p>Error까지 무조건 <code>catch</code>하려고 하는 것은 적절하지 않다.</p>
<hr>
<h2 id="checked와-unchecked">Checked와 Unchecked</h2>
<pre><code class="language-text">Checked Exception
→ 컴파일러가 처리 여부 검사

Unchecked Exception
→ 컴파일러가 처리 여부 강제하지 않음</code></pre>
<p>둘의 차이는 “심각한 예외와 가벼운 예외”가 아니다.</p>
<p>컴파일러가 처리 코드를 강제하는지에 대한 차이다.</p>
<hr>
<h2 id="throw와-throws-1">throw와 throws</h2>
<pre><code class="language-java">throw new Exception();</code></pre>
<p>실제로 예외 객체를 발생시킨다.</p>
<pre><code class="language-java">void method() throws Exception {
}</code></pre>
<p>예외 처리 책임을 호출자에게 전달한다.</p>
<hr>
<h2 id="catch와-throws">catch와 throws</h2>
<pre><code class="language-text">catch
→ 현재 메서드에서 예외 처리

throws
→ 호출한 메서드로 예외 전달</code></pre>
<p><code>throws</code>를 사용한다고 예외가 처리되거나 사라지는 것은 아니다.</p>
<hr>
<h2 id="finally와-try-with-resources">finally와 try-with-resources</h2>
<p><code>finally</code>는 예외 발생 여부와 관계없이 공통 작업을 수행한다.</p>
<p><code>try-with-resources</code>는 <code>AutoCloseable</code> 자원을 자동으로 닫기 위한 전용 문법이다.</p>
<p>파일이나 DB 연결처럼 닫아야 하는 자원에는 가능하면 <code>try-with-resources</code>를 사용하는 것이 좋다.</p>
<hr>
<h2 id="예외-처리와-예외-무시">예외 처리와 예외 무시</h2>
<p>다음은 예외 처리가 아니다.</p>
<pre><code class="language-java">catch (Exception e) {
}</code></pre>
<p>예외를 잡았지만 아무것도 하지 않았기 때문에 문제를 숨긴 것이다.</p>
<hr>
<h2 id="예외-메시지와-stack-trace">예외 메시지와 Stack Trace</h2>
<pre><code class="language-java">e.getMessage()</code></pre>
<p>예외 메시지만 반환한다.</p>
<pre><code class="language-java">e.printStackTrace()</code></pre>
<p>예외 종류, 메시지, 발생 위치, 호출 경로를 함께 출력한다.</p>
<p>디버깅에는 Stack Trace가 훨씬 중요하다.</p>
<hr>
<h1 id="실무에서는-어떻게-사용할까">실무에서는 어떻게 사용할까?</h1>
<p>실무에서는 예외를 잡는 것보다 <strong>어느 계층에서 어떤 의미로 처리할지</strong>가 중요하다.</p>
<p>Repository 계층에서는 데이터베이스나 파일 관련 예외가 발생할 수 있다.</p>
<p>Service 계층에서는 이를 업무 의미가 있는 예외로 변환할 수 있다.</p>
<pre><code class="language-text">SQLException
→ MemberDataAccessException</code></pre>
<p>Controller 계층에서는 예외를 HTTP 응답으로 변환할 수 있다.</p>
<pre><code class="language-text">MemberNotFoundException
→ HTTP 404 Not Found</code></pre>
<p>Spring에서는 <code>@ExceptionHandler</code>나 <code>@RestControllerAdvice</code>를 사용해 예외를 공통 처리할 수 있다.</p>
<pre><code class="language-java">@ExceptionHandler(
        MemberNotFoundException.class
)
public ResponseEntity&lt;?&gt; handle(
        MemberNotFoundException e
) {
    return ResponseEntity
            .notFound()
            .build();
}</code></pre>
<p>예외 메시지에는 문제 해결에 필요한 정보를 포함하되 비밀번호, 인증 토큰, 주민등록번호 같은 민감한 정보는 포함하지 않아야 한다.</p>
<p>예외를 로그로 남길 때도 같은 예외를 여러 계층에서 반복 기록하면 로그가 중복될 수 있다.</p>
<pre><code class="language-text">예외를 처리하는 계층
또는
최종적으로 사용자 응답으로 변환하는 계층</code></pre>
<p>처럼 로깅 위치를 명확히 정하는 것이 좋다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 Java의 예외 처리 구조와 활용 방법을 정리했다.</p>
<ul>
<li>예외는 프로그램의 정상적인 실행 흐름을 방해하는 상황을 표현한다.</li>
<li>Java의 오류와 예외는 모두 <code>Throwable</code>을 상속한다.</li>
<li><code>Error</code>는 일반적으로 애플리케이션에서 복구하기 어려운 심각한 문제다.</li>
<li><code>Exception</code>은 코드에서 처리할 수 있는 예외 상황을 표현한다.</li>
<li>예외는 모두 객체이며 JVM이 예외 객체를 생성해 던진다.</li>
<li>Checked Exception은 처리하지 않으면 컴파일되지 않는다.</li>
<li>Checked Exception은 <code>try-catch</code> 또는 <code>throws</code>로 처리해야 한다.</li>
<li>Unchecked Exception은 컴파일러가 처리 여부를 강제하지 않는다.</li>
<li><code>RuntimeException</code>과 그 하위 예외가 Unchecked Exception에 해당한다.</li>
<li><code>try</code> 블록에는 예외가 발생할 가능성이 있는 코드를 작성한다.</li>
<li><code>catch</code> 블록은 발생한 예외 객체를 받아 처리한다.</li>
<li>예외가 발생하면 해당 지점 이후의 <code>try</code> 코드는 실행되지 않는다.</li>
<li><code>getMessage()</code>는 예외의 상세 메시지를 반환한다.</li>
<li><code>getCause()</code>는 현재 예외의 원인 예외를 반환한다.</li>
<li><code>printStackTrace()</code>는 예외 발생 위치와 호출 흐름을 출력한다.</li>
<li>여러 예외를 처리할 때는 여러 <code>catch</code> 블록을 사용할 수 있다.</li>
<li>자식 예외를 부모 예외보다 먼저 처리해야 한다.</li>
<li>멀티 catch를 이용하면 처리 방식이 같은 여러 예외를 하나로 묶을 수 있다.</li>
<li><code>finally</code>는 예외 발생 여부와 관계없이 일반적으로 실행된다.</li>
<li><code>return</code>이 있더라도 메서드 종료 전에 <code>finally</code>가 실행된다.</li>
<li><code>System.exit()</code>나 JVM 강제 종료 상황에서는 <code>finally</code> 실행이 보장되지 않는다.</li>
<li><code>finally</code>는 주로 자원을 정리하는 데 사용한다.</li>
<li><code>try-with-resources</code>는 <code>AutoCloseable</code> 자원을 자동으로 닫는다.</li>
<li><code>try-with-resources</code>의 자원 변수는 다시 할당할 수 없다.</li>
<li><code>throw</code>는 예외 객체를 직접 발생시킨다.</li>
<li><code>throws</code>는 예외 처리 책임을 호출자에게 전달한다.</li>
<li><code>throws</code>를 사용해도 예외가 사라지는 것은 아니다.</li>
<li>오버라이딩 메서드는 부모보다 더 넓은 Checked Exception을 선언할 수 없다.</li>
<li>예외 변환은 하위 계층의 기술 예외를 상위 계층의 의미 있는 예외로 바꾸는 작업이다.</li>
<li>Exception Chaining은 변환된 예외에 원래 예외를 연결하는 방법이다.</li>
<li>사용자 정의 예외는 업무 상황을 명확하게 표현할 수 있다.</li>
<li>사용자 정의 Checked Exception은 <code>Exception</code>을 상속한다.</li>
<li>사용자 정의 Unchecked Exception은 <code>RuntimeException</code>을 상속한다.</li>
<li>예외를 잡고 아무 작업도 하지 않으면 문제의 원인을 숨기게 된다.</li>
<li>예외는 정상적인 흐름 제어 목적으로 사용하지 않아야 한다.</li>
<li><code>RuntimeException</code>은 무조건 잡기보다 잘못된 코드와 상태를 수정해야 하는 경우가 많다.</li>
<li>실제 프로젝트에서는 <code>printStackTrace()</code>보다 로깅 프레임워크를 사용하는 것이 일반적이다.</li>
<li>Stack Trace를 분석할 때는 예외 종류, 메시지, 발생 위치, <code>Caused by</code>를 확인해야 한다.</li>
</ul>
<hr>
<h1 id="다음-글-예고">다음 글 예고</h1>
<p>다음 글에서는 Java가 파일과 데이터를 읽고 쓰는 방법인 <strong>입출력 스트림</strong>을 다룬다.</p>
<pre><code class="language-text">Java 심화 ⑩ 입출력 스트림

- InputStream과 OutputStream
- Reader와 Writer
- FileInputStream
- FileOutputStream
- BufferedInputStream
- BufferedReader
- File과 Path
- Files 클래스
- 직렬화
- try-with-resources</code></pre>
<p>입출력 API는 Checked Exception을 많이 발생시키므로 이번 글에서 학습한 <code>try-catch</code>, <code>throws</code>, <code>try-with-resources</code>가 실제로 어떻게 사용되는지 자연스럽게 연결해서 이해할 수 있다.</p>
<hr>
<h2 id="면접과-평가에서-자주-나오는-질문">면접과 평가에서 자주 나오는 질문</h2>
<h3 id="checked-exception과-unchecked-exception의-차이는">Checked Exception과 Unchecked Exception의 차이는?</h3>
<p>Checked Exception은 컴파일러가 처리 여부를 검사한다.</p>
<p>Unchecked Exception은 처리 여부를 강제하지 않는다.</p>
<pre><code class="language-text">Checked
→ Exception 하위이지만 RuntimeException 계열이 아님

Unchecked
→ RuntimeException 하위</code></pre>
<h3 id="try에서-return하면-finally는-실행되는가">try에서 return하면 finally는 실행되는가?</h3>
<p>일반적으로 실행된다.</p>
<p><code>finally</code>가 실행된 후 return이 완료된다.</p>
<p>단, <code>System.exit()</code>나 JVM 강제 종료 상황은 예외다.</p>
<h3 id="finally에서-return을-사용하면-어떻게-되는가">finally에서 return을 사용하면 어떻게 되는가?</h3>
<p><code>finally</code>의 return이 기존 return이나 예외를 덮어버릴 수 있다.</p>
<pre><code class="language-java">public int test() {
    try {
        return 1;
    } finally {
        return 2;
    }
}</code></pre>
<p>결과는 <code>2</code>다.</p>
<p>이런 코드는 예외와 원래 반환값을 숨기므로 사용하지 않는 것이 좋다.</p>
<h3 id="throw와-throws의-차이는">throw와 throws의 차이는?</h3>
<pre><code class="language-text">throw
→ 실제 예외 발생

throws
→ 예외 처리 책임 위임</code></pre>
<h3 id="try-with-resources의-조건은">try-with-resources의 조건은?</h3>
<p>자원 객체가 <code>AutoCloseable</code> 또는 그 하위 인터페이스를 구현해야 한다.</p>
<h3 id="try-with-resources에서-자원은-어떤-순서로-닫히는가">try-with-resources에서 자원은 어떤 순서로 닫히는가?</h3>
<p>선언한 순서의 반대로 닫힌다.</p>
<h3 id="예외를-잡아서-다시-던질-때-주의할-점은">예외를 잡아서 다시 던질 때 주의할 점은?</h3>
<p>원래 예외를 cause로 연결해야 한다.</p>
<pre><code class="language-java">throw new ServiceException(
        &quot;처리 실패&quot;,
        e
);</code></pre>
<h3 id="사용자-정의-예외는-checked와-unchecked-중-무엇이-좋은가">사용자 정의 예외는 Checked와 Unchecked 중 무엇이 좋은가?</h3>
<p>호출자가 실제로 복구할 수 있고 처리를 강제할 필요가 있다면 Checked Exception을 고려할 수 있다.</p>
<p>업무 규칙 위반이나 계층 간 전달이 필요한 예외는 RuntimeException으로 설계하는 경우가 많다.</p>
<h3 id="catch-exception-e는-왜-주의해야-하는가"><code>catch (Exception e)</code>는 왜 주의해야 하는가?</h3>
<p>처리할 의도가 없는 예외까지 모두 잡아 문제를 숨길 수 있기 때문이다.</p>
<p>최상위 경계에서 공통 처리하는 경우를 제외하면 구체적인 예외를 처리하는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (8) 컬렉션]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-8-%EC%BB%AC%EB%A0%89%EC%85%98</link>
            <guid>https://velog.io/@tongmill_g/JAVA-8-%EC%BB%AC%EB%A0%89%EC%85%98</guid>
            <pubDate>Wed, 29 Jul 2026 02:26:57 GMT</pubDate>
            <description><![CDATA[<h1 id="컬렉션-프레임워크">컬렉션 프레임워크</h1>
<h2 id="들어가며">들어가며</h2>
<p>프로그램은 대부분 여러 데이터를 저장하고 조회하고 수정하며 삭제하는 작업을 반복한다.</p>
<p>회원 관리 프로그램에서는 여러 회원을 저장해야 하고, 쇼핑몰에서는 상품과 주문 목록을 관리해야 한다. 게시판에서는 여러 게시글을 저장하고 검색해야 한다.</p>
<p>여러 데이터를 저장하는 가장 기본적인 방법은 배열이다.</p>
<pre><code class="language-java">String[] names = new String[3];

names[0] = &quot;김자바&quot;;
names[1] = &quot;이자바&quot;;
names[2] = &quot;박자바&quot;;</code></pre>
<p>하지만 배열은 생성할 때 길이가 결정되고 이후 크기를 변경할 수 없다. 새로운 데이터를 추가하려면 더 큰 배열을 만들고 기존 데이터를 복사해야 한다.</p>
<pre><code class="language-java">String[] oldNames = {
        &quot;김자바&quot;,
        &quot;이자바&quot;,
        &quot;박자바&quot;
};

String[] newNames = new String[5];

System.arraycopy(
        oldNames,
        0,
        newNames,
        0,
        oldNames.length
);</code></pre>
<p>데이터의 개수가 계속 변하는 프로그램에서 배열을 직접 관리하는 것은 번거롭다.</p>
<p>Java는 이러한 문제를 해결하기 위해 여러 데이터를 효율적으로 저장하고 처리할 수 있는 <strong>컬렉션 프레임워크</strong>를 제공한다.</p>
<p>이번 글에서는 컬렉션 프레임워크의 기본 구조와 <code>List</code>, <code>Set</code>, <code>Map</code>의 차이를 살펴본다. 이어서 정렬, 불변 컬렉션, 람다 표현식, 표준 함수형 인터페이스, <code>Optional</code>, Stream API까지 함께 정리해보려고 한다.</p>
<hr>
<h1 id="자료구조와-컬렉션-프레임워크">자료구조와 컬렉션 프레임워크</h1>
<h2 id="자료구조란">자료구조란?</h2>
<p>자료구조는 데이터를 효율적으로 저장하고 접근하고 수정하기 위한 데이터의 조직 방식이다.</p>
<p>단순히 여러 값을 보관하는 것뿐 아니라 다음 내용까지 포함한다.</p>
<ul>
<li>데이터가 어떤 형태로 저장되는가</li>
<li>데이터 사이에 어떤 관계가 있는가</li>
<li>데이터를 어떻게 추가하고 조회하는가</li>
<li>데이터를 어떻게 수정하고 삭제하는가</li>
<li>특정 데이터를 얼마나 빠르게 찾을 수 있는가</li>
</ul>
<p>동일한 데이터를 저장하더라도 자료구조에 따라 작업 성능이 달라질 수 있다.</p>
<p>예를 들어 인덱스로 특정 요소를 빠르게 조회하려면 배열 기반 구조가 유리하다. 중간에 데이터를 자주 추가하거나 삭제한다면 연결 구조가 더 적합할 수 있다.</p>
<p>따라서 무조건 좋은 자료구조가 있는 것이 아니라, <strong>어떤 작업을 자주 수행하는지에 따라 적합한 자료구조를 선택해야 한다.</strong></p>
<hr>
<h2 id="배열과-컬렉션의-차이">배열과 컬렉션의 차이</h2>
<p>배열은 같은 타입의 여러 값을 저장하는 가장 기본적인 자료구조다.</p>
<pre><code class="language-java">int[] numbers = {10, 20, 30};</code></pre>
<p>배열에는 다음과 같은 특징이 있다.</p>
<ul>
<li>생성할 때 길이가 결정된다.</li>
<li>인덱스로 요소에 빠르게 접근할 수 있다.</li>
<li>같은 타입의 값만 저장할 수 있다.</li>
<li>기본 타입과 참조 타입 모두 저장할 수 있다.</li>
</ul>
<p>배열의 길이를 동적으로 변경할 수 없다는 점은 가장 큰 불편함이다.</p>
<p>컬렉션은 내부적으로 배열이나 연결 구조 등을 사용하지만 개발자가 크기 변경과 데이터 관리를 직접 처리하지 않아도 된다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();

names.add(&quot;김자바&quot;);
names.add(&quot;이자바&quot;);
names.add(&quot;박자바&quot;);</code></pre>
<p>컬렉션은 데이터가 추가되면 필요한 저장 공간을 내부적으로 관리한다.</p>
<hr>
<h2 id="object만-사용했을-때의-문제">Object만 사용했을 때의 문제</h2>
<p>모든 객체는 <code>Object</code>를 상속하므로 <code>Object[]</code>를 사용하면 여러 종류의 객체를 함께 저장할 수 있다.</p>
<pre><code class="language-java">Object[] values = new Object[3];

values[0] = &quot;Java&quot;;
values[1] = 100;
values[2] = new Member(&quot;김자바&quot;);</code></pre>
<p>값을 저장할 때는 편리하지만 꺼낼 때는 <code>Object</code>로 반환되기 때문에 강제 타입 변환이 필요하다.</p>
<pre><code class="language-java">String language = (String) values[0];
Integer number = (Integer) values[1];</code></pre>
<p>잘못된 타입으로 변환하면 실행 중 <code>ClassCastException</code>이 발생한다.</p>
<p>제네릭을 사용하면 컬렉션이 저장할 타입을 컴파일 단계에서 제한할 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; languages = new ArrayList&lt;&gt;();

languages.add(&quot;Java&quot;);

// languages.add(100);
// 컴파일 오류</code></pre>
<p>값을 꺼낼 때도 형변환이 필요 없다.</p>
<pre><code class="language-java">String language = languages.get(0);</code></pre>
<hr>
<h2 id="컬렉션-프레임워크의-구조">컬렉션 프레임워크의 구조</h2>
<p>컬렉션 프레임워크는 다수의 데이터를 저장하고 처리하기 위한 인터페이스와 구현 클래스의 집합이다.</p>
<p>주요 클래스와 인터페이스는 <code>java.util</code> 패키지에 포함되어 있다.</p>
<pre><code class="language-text">Collection
├─ List
│  ├─ ArrayList
│  └─ LinkedList
│
└─ Set
   ├─ HashSet
   └─ TreeSet

Map
├─ HashMap
└─ TreeMap</code></pre>
<p><code>List</code>와 <code>Set</code>은 <code>Collection</code> 인터페이스를 상속한다.</p>
<p><code>Map</code>은 Key와 Value의 쌍을 관리하는 구조이기 때문에 <code>Collection</code>과는 별도의 계층으로 존재한다.</p>
<hr>
<h2 id="list-set-map-비교">List, Set, Map 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>데이터 구조</th>
<th>순서</th>
<th>중복</th>
</tr>
</thead>
<tbody><tr>
<td><code>List</code></td>
<td>값을 순서대로 관리</td>
<td>유지됨</td>
<td>허용</td>
</tr>
<tr>
<td><code>Set</code></td>
<td>중복되지 않는 값 관리</td>
<td>일반적으로 보장되지 않음</td>
<td>허용하지 않음</td>
</tr>
<tr>
<td><code>Map</code></td>
<td>Key와 Value의 쌍 관리</td>
<td>구현체에 따라 다름</td>
<td>Key 중복 불가, Value 중복 가능</td>
</tr>
</tbody></table>
<pre><code class="language-java">List&lt;String&gt; list = new ArrayList&lt;&gt;();
Set&lt;String&gt; set = new HashSet&lt;&gt;();
Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();</code></pre>
<hr>
<h2 id="collection의-공통-메서드">Collection의 공통 메서드</h2>
<p><code>List</code>와 <code>Set</code>은 <code>Collection</code> 인터페이스를 상속하므로 공통 메서드를 사용할 수 있다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>대표 메서드</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>추가</td>
<td><code>add(E e)</code></td>
<td>요소 하나 추가</td>
</tr>
<tr>
<td>추가</td>
<td><code>addAll(Collection&lt;? extends E&gt; c)</code></td>
<td>다른 컬렉션의 모든 요소 추가</td>
</tr>
<tr>
<td>조회</td>
<td><code>contains(Object o)</code></td>
<td>특정 요소 포함 여부 확인</td>
</tr>
<tr>
<td>조회</td>
<td><code>isEmpty()</code></td>
<td>비어 있는지 확인</td>
</tr>
<tr>
<td>조회</td>
<td><code>size()</code></td>
<td>요소 개수 반환</td>
</tr>
<tr>
<td>조회</td>
<td><code>iterator()</code></td>
<td>반복자 반환</td>
</tr>
<tr>
<td>삭제</td>
<td><code>remove(Object o)</code></td>
<td>특정 요소 삭제</td>
</tr>
<tr>
<td>삭제</td>
<td><code>removeAll(Collection&lt;?&gt; c)</code></td>
<td>전달받은 요소들을 삭제</td>
</tr>
<tr>
<td>삭제</td>
<td><code>clear()</code></td>
<td>모든 요소 삭제</td>
</tr>
<tr>
<td>변환</td>
<td><code>toArray()</code></td>
<td>배열로 변환</td>
</tr>
</tbody></table>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class CollectionMethodExample {

    public static void main(String[] args) {
        List&lt;String&gt; languages = new ArrayList&lt;&gt;();

        languages.add(&quot;Java&quot;);
        languages.add(&quot;Python&quot;);
        languages.add(&quot;JavaScript&quot;);

        System.out.println(languages.size());
        System.out.println(languages.contains(&quot;Java&quot;));
        System.out.println(languages.isEmpty());

        languages.remove(&quot;Python&quot;);

        System.out.println(languages);

        languages.clear();

        System.out.println(languages.isEmpty());
    }
}</code></pre>
<hr>
<h1 id="list-순서가-있는-데이터-관리하기">List: 순서가 있는 데이터 관리하기</h1>
<h2 id="list의-특징">List의 특징</h2>
<p><code>List</code>는 입력한 순서를 유지하고 중복된 요소를 허용한다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();

names.add(&quot;김자바&quot;);
names.add(&quot;이자바&quot;);
names.add(&quot;김자바&quot;);</code></pre>
<p>결과는 다음과 같다.</p>
<pre><code class="language-text">[김자바, 이자바, 김자바]</code></pre>
<p>동일한 값이 두 번 저장되어도 모두 유지된다.</p>
<p>순서가 있기 때문에 인덱스로 요소에 접근할 수 있다.</p>
<pre><code class="language-java">String first = names.get(0);</code></pre>
<hr>
<h2 id="list의-주요-메서드">List의 주요 메서드</h2>
<table>
<thead>
<tr>
<th>메서드</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td><code>add(E e)</code></td>
<td>마지막에 요소 추가</td>
</tr>
<tr>
<td><code>add(int index, E e)</code></td>
<td>지정된 위치에 요소 추가</td>
</tr>
<tr>
<td><code>get(int index)</code></td>
<td>지정된 위치의 요소 조회</td>
</tr>
<tr>
<td><code>set(int index, E e)</code></td>
<td>지정된 위치의 요소 변경</td>
</tr>
<tr>
<td><code>remove(int index)</code></td>
<td>지정된 위치의 요소 삭제</td>
</tr>
<tr>
<td><code>indexOf(Object o)</code></td>
<td>요소의 첫 번째 인덱스 반환</td>
</tr>
<tr>
<td><code>lastIndexOf(Object o)</code></td>
<td>요소의 마지막 인덱스 반환</td>
</tr>
</tbody></table>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class ListMethodExample {

    public static void main(String[] args) {
        List&lt;String&gt; names = new ArrayList&lt;&gt;();

        names.add(&quot;김자바&quot;);
        names.add(&quot;박자바&quot;);
        names.add(1, &quot;이자바&quot;);

        System.out.println(names);

        String firstName = names.get(0);

        System.out.println(firstName);

        names.set(0, &quot;김Java&quot;);
        names.remove(2);

        System.out.println(names);
    }
}</code></pre>
<hr>
<h2 id="arraylist">ArrayList</h2>
<p><code>ArrayList</code>는 배열을 기반으로 구현된 <code>List</code>다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();</code></pre>
<p>내부적으로 객체 배열을 사용하고, 저장 공간이 부족해지면 더 큰 배열을 생성한 뒤 기존 요소를 복사한다.</p>
<p>개념적으로 다음과 같은 구조다.</p>
<pre><code class="language-text">ArrayList

index    0        1        2
       ┌────────┬────────┬────────┐
value  │ &quot;Java&quot; │ &quot;SQL&quot;  │ &quot;HTML&quot; │
       └────────┴────────┴────────┘</code></pre>
<p>배열 기반이므로 인덱스를 이용한 조회가 빠르다.</p>
<pre><code class="language-java">names.get(1);</code></pre>
<p>반면 배열의 중간에 요소를 추가하거나 삭제하면 뒤에 있는 요소들을 이동해야 한다.</p>
<pre><code class="language-text">삭제 전

[A][B][C][D]

B 삭제

[A][C][D][ ]

C와 D를 앞으로 이동</code></pre>
<hr>
<h2 id="초기-용량-지정하기">초기 용량 지정하기</h2>
<p>저장할 데이터 개수를 어느 정도 알고 있다면 초기 용량을 지정할 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;(100);</code></pre>
<p>이 코드는 리스트의 크기를 100으로 만드는 것이 아니다.</p>
<p>요소를 저장할 내부 공간의 초기 용량을 설정한다.</p>
<pre><code class="language-java">System.out.println(names.size());</code></pre>
<p>결과는 여전히 <code>0</code>이다.</p>
<p>초기 용량을 적절히 지정하면 배열을 반복해서 확장하고 복사하는 비용을 줄일 수 있다. 다만 필요 이상으로 크게 지정하면 메모리를 낭비할 수 있다.</p>
<hr>
<h2 id="linkedlist">LinkedList</h2>
<p><code>LinkedList</code>는 각 요소가 앞뒤 요소의 정보를 가지고 연결된 구조다.</p>
<p>Java의 <code>LinkedList</code>는 이중 연결 리스트로 구현되어 있다.</p>
<pre><code class="language-text">null ← [A] ⇄ [B] ⇄ [C] → null</code></pre>
<p>각 노드는 개념적으로 다음 정보를 가진다.</p>
<pre><code class="language-text">이전 노드 참조
현재 데이터
다음 노드 참조</code></pre>
<pre><code class="language-java">List&lt;String&gt; names = new LinkedList&lt;&gt;();</code></pre>
<p>배열처럼 모든 요소가 연속된 메모리 공간에 저장될 필요가 없다.</p>
<p>요소의 위치를 이미 알고 있다면 주변 링크를 변경하여 추가하거나 삭제할 수 있다.</p>
<pre><code class="language-text">A ⇄ B ⇄ C

B 삭제

A ⇄ C</code></pre>
<p>다만 특정 인덱스의 요소를 찾으려면 처음이나 끝에서부터 연결을 따라 이동해야 하므로 인덱스 조회는 <code>ArrayList</code>보다 느릴 수 있다.</p>
<hr>
<h2 id="arraylist와-linkedlist-비교">ArrayList와 LinkedList 비교</h2>
<table>
<thead>
<tr>
<th>작업</th>
<th>ArrayList</th>
<th>LinkedList</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 조회</td>
<td>빠름</td>
<td>상대적으로 느림</td>
</tr>
<tr>
<td>마지막 요소 추가</td>
<td>일반적으로 빠름</td>
<td>빠름</td>
</tr>
<tr>
<td>중간 요소 추가·삭제</td>
<td>요소 이동 필요</td>
<td>노드 탐색 후 연결 변경</td>
</tr>
<tr>
<td>메모리 사용</td>
<td>요소 배열 중심</td>
<td>노드별 참조 정보 추가</td>
</tr>
<tr>
<td>캐시 효율</td>
<td>좋음</td>
<td>상대적으로 낮음</td>
</tr>
</tbody></table>
<p>이론적으로는 중간 삽입과 삭제가 <code>LinkedList</code>에 유리해 보이지만, 실제 작업에서는 먼저 해당 위치를 찾는 탐색 비용이 발생한다.</p>
<p>따라서 일반적인 Java 애플리케이션에서는 <code>ArrayList</code>를 기본 선택으로 사용하는 경우가 많다.</p>
<p>앞뒤에서 요소를 자주 추가하거나 제거하는 큐나 덱 구조가 필요하다면 <code>LinkedList</code>보다 <code>ArrayDeque</code>가 더 적합한 경우도 많다.</p>
<hr>
<h2 id="주요-시간-복잡도">주요 시간 복잡도</h2>
<table>
<thead>
<tr>
<th>작업</th>
<th align="right">ArrayList</th>
<th align="right">LinkedList</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 조회</td>
<td align="right">O(1)</td>
<td align="right">O(n)</td>
</tr>
<tr>
<td>값 검색</td>
<td align="right">O(n)</td>
<td align="right">O(n)</td>
</tr>
<tr>
<td>마지막 추가</td>
<td align="right">평균 O(1)</td>
<td align="right">O(1)</td>
</tr>
<tr>
<td>중간 추가·삭제</td>
<td align="right">O(n)</td>
<td align="right">탐색 O(n), 연결 변경 O(1)</td>
</tr>
<tr>
<td>첫 요소 추가·삭제</td>
<td align="right">O(n)</td>
<td align="right">O(1)</td>
</tr>
</tbody></table>
<p><code>ArrayList</code>의 마지막 추가는 저장 공간 확장이 필요한 순간에는 배열 복사로 O(n)이 걸리지만, 여러 연산을 평균 내면 상각 O(1)로 본다.</p>
<hr>
<h2 id="반복문에서-요소-삭제-시-주의하기">반복문에서 요소 삭제 시 주의하기</h2>
<p>인덱스를 증가시키면서 요소를 삭제하면 다음 요소의 인덱스가 앞으로 당겨져 검사하지 못하는 요소가 생길 수 있다.</p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class RemoveExample {

    public static void main(String[] args) {
        List&lt;Integer&gt; numbers =
                new ArrayList&lt;&gt;(
                        List.of(1, 3, 5, 2, 4)
                );

        for (int i = 0; i &lt; numbers.size(); i++) {
            if (numbers.get(i) % 2 == 1) {
                numbers.remove(i);
            }
        }

        System.out.println(numbers);
    }
}</code></pre>
<p><code>1</code>을 삭제하면 뒤의 <code>3</code>이 인덱스 0으로 이동하지만 <code>i</code>는 1로 증가하므로 <code>3</code>을 확인하지 못할 수 있다.</p>
<p>뒤에서부터 순회하면 인덱스가 밀리는 문제를 피할 수 있다.</p>
<pre><code class="language-java">for (int i = numbers.size() - 1; i &gt;= 0; i--) {
    if (numbers.get(i) % 2 == 1) {
        numbers.remove(i);
    }
}</code></pre>
<p>메모에 적힌 조건은 다음처럼 수정해야 한다.</p>
<pre><code class="language-java">i &gt;= 0</code></pre>
<p><code>i &lt;= 0</code>을 사용하면 마지막 인덱스가 양수인 경우 반복문이 시작되지 않는다.</p>
<hr>
<h2 id="iterator로-안전하게-삭제하기">Iterator로 안전하게 삭제하기</h2>
<p>컬렉션을 순회하면서 요소를 삭제해야 한다면 <code>Iterator</code>를 사용할 수 있다.</p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorRemoveExample {

    public static void main(String[] args) {
        List&lt;Integer&gt; numbers =
                new ArrayList&lt;&gt;(
                        List.of(1, 2, 3, 4, 5)
                );

        Iterator&lt;Integer&gt; iterator =
                numbers.iterator();

        while (iterator.hasNext()) {
            int number = iterator.next();

            if (number % 2 == 1) {
                iterator.remove();
            }
        }

        System.out.println(numbers);
    }
}</code></pre>
<p><code>Iterator</code>로 순회 중일 때는 컬렉션의 <code>remove()</code>가 아니라 <code>iterator.remove()</code>를 사용해야 한다.</p>
<hr>
<h2 id="removeif">removeIf()</h2>
<p>조건에 맞는 요소를 삭제할 때는 <code>removeIf()</code>가 더 간결하다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers =
        new ArrayList&lt;&gt;(
                List.of(1, 2, 3, 4, 5)
        );

numbers.removeIf(
        number -&gt; number % 2 == 1
);

System.out.println(numbers);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[2, 4]</code></pre>
<hr>
<h2 id="향상된-for문에서-수정하면-안-되는-이유">향상된 for문에서 수정하면 안 되는 이유</h2>
<p>다음 코드는 실행 중 <code>ConcurrentModificationException</code>을 발생시킬 수 있다.</p>
<pre><code class="language-java">for (Integer number : numbers) {
    if (number % 2 == 1) {
        numbers.remove(number);
    }
}</code></pre>
<p>향상된 <code>for</code>문은 내부적으로 <code>Iterator</code>를 사용한다. 순회 중 컬렉션의 구조가 예상하지 못하게 변경되면 반복자가 안전하게 동작할 수 없기 때문에 예외가 발생한다.</p>
<p>요소를 삭제하려면 다음 방법을 사용한다.</p>
<ul>
<li><code>Iterator.remove()</code></li>
<li><code>removeIf()</code></li>
<li>뒤에서부터 인덱스 순회</li>
<li>삭제할 요소를 별도로 수집한 뒤 <code>removeAll()</code></li>
</ul>
<hr>
<h2 id="removeint와-removeobject">remove(int)와 remove(Object)</h2>
<p><code>List&lt;Integer&gt;</code>에서 <code>remove()</code>를 사용할 때 특히 주의해야 한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers =
        new ArrayList&lt;&gt;(
                List.of(10, 20, 30)
        );

numbers.remove(1);</code></pre>
<p><code>1</code>은 삭제할 값이 아니라 인덱스로 해석된다.</p>
<p>결과는 다음과 같다.</p>
<pre><code class="language-text">[10, 30]</code></pre>
<p>값 <code>10</code>을 삭제하고 싶다면 <code>Integer</code> 객체로 전달해야 한다.</p>
<pre><code class="language-java">numbers.remove(Integer.valueOf(10));</code></pre>
<hr>
<h2 id="제네릭은-불공변이다">제네릭은 불공변이다</h2>
<p><code>SpiderMan</code>이 <code>Person</code>의 자식이라고 가정해보자.</p>
<pre><code class="language-java">public class Person {
}

public class SpiderMan extends Person {
}</code></pre>
<p>배열은 다음 대입이 가능하다.</p>
<pre><code class="language-java">SpiderMan[] spiderMen =
        new SpiderMan[3];

Person[] people = spiderMen;</code></pre>
<p>배열은 공변이기 때문이다.</p>
<p>하지만 제네릭은 불공변이므로 다음 대입은 불가능하다.</p>
<pre><code class="language-java">List&lt;SpiderMan&gt; spiderMen =
        new ArrayList&lt;&gt;();

// List&lt;Person&gt; people = spiderMen;
// 컴파일 오류</code></pre>
<p><code>List&lt;SpiderMan&gt;</code>은 <code>List&lt;Person&gt;</code>의 하위 타입이 아니다.</p>
<p>여러 자식 타입을 읽기 위한 목적이라면 와일드카드를 사용할 수 있다.</p>
<pre><code class="language-java">List&lt;? extends Person&gt; people = spiderMen;</code></pre>
<hr>
<h2 id="arraysaslist">Arrays.asList()</h2>
<p><code>Arrays.asList()</code>는 배열을 기반으로 고정 크기의 리스트를 만든다.</p>
<pre><code class="language-java">import java.util.Arrays;
import java.util.List;

public class ArraysAsListExample {

    public static void main(String[] args) {
        List&lt;String&gt; names =
                Arrays.asList(
                        &quot;김자바&quot;,
                        &quot;이자바&quot;,
                        &quot;박자바&quot;
                );

        names.set(0, &quot;김Java&quot;);

        System.out.println(names);
    }
}</code></pre>
<p>요소 변경은 가능하다.</p>
<pre><code class="language-java">names.set(0, &quot;김Java&quot;);</code></pre>
<p>그러나 크기를 변경하는 작업은 불가능하다.</p>
<pre><code class="language-java">// names.add(&quot;최자바&quot;);
// UnsupportedOperationException

// names.remove(&quot;이자바&quot;);
// UnsupportedOperationException</code></pre>
<p>원본 배열과 리스트가 연결되어 있다는 점도 주의해야 한다.</p>
<pre><code class="language-java">String[] values = {&quot;A&quot;, &quot;B&quot;};

List&lt;String&gt; list =
        Arrays.asList(values);

values[0] = &quot;Changed&quot;;

System.out.println(list);</code></pre>
<p>결과는 다음과 같다.</p>
<pre><code class="language-text">[Changed, B]</code></pre>
<p>독립적으로 수정 가능한 <code>ArrayList</code>가 필요하다면 새 리스트로 복사한다.</p>
<pre><code class="language-java">List&lt;String&gt; names =
        new ArrayList&lt;&gt;(
                Arrays.asList(
                        &quot;김자바&quot;,
                        &quot;이자바&quot;
                )
        );</code></pre>
<hr>
<h1 id="set-중복되지-않는-데이터-관리하기">Set: 중복되지 않는 데이터 관리하기</h1>
<h2 id="set의-특징">Set의 특징</h2>
<p><code>Set</code>은 중복된 요소를 허용하지 않는 컬렉션이다.</p>
<pre><code class="language-java">Set&lt;String&gt; languages =
        new HashSet&lt;&gt;();

languages.add(&quot;Java&quot;);
languages.add(&quot;Python&quot;);
languages.add(&quot;Java&quot;);</code></pre>
<p><code>&quot;Java&quot;</code>를 두 번 추가했지만 한 번만 저장된다.</p>
<pre><code class="language-text">[Java, Python]</code></pre>
<p><code>HashSet</code>은 입력 순서를 보장하지 않는다.</p>
<pre><code class="language-java">System.out.println(languages);</code></pre>
<p>출력 순서는 저장 순서와 다를 수 있다.</p>
<p>순서가 필요하면 목적에 따라 다음 구현체를 고려할 수 있다.</p>
<ul>
<li><code>LinkedHashSet</code>: 입력 순서 유지</li>
<li><code>TreeSet</code>: 정렬된 순서 유지</li>
</ul>
<hr>
<h2 id="hashset">HashSet</h2>
<p><code>HashSet</code>은 해시값을 이용해 데이터를 저장하고 검색한다.</p>
<pre><code class="language-java">Set&lt;Member&gt; members =
        new HashSet&lt;&gt;();</code></pre>
<p>내부적으로는 <code>HashMap</code>을 사용하며, 저장할 객체를 Key처럼 관리한다.</p>
<hr>
<h2 id="해시란">해시란?</h2>
<p>해시는 임의 길이의 데이터를 일정한 규칙에 따라 고정된 범위의 값으로 변환하는 방식이다.</p>
<p>Java 객체는 <code>hashCode()</code> 메서드로 정수 형태의 해시 코드를 반환한다.</p>
<pre><code class="language-java">String value = &quot;Java&quot;;

System.out.println(value.hashCode());</code></pre>
<p>해시 기반 자료구조는 해시 코드를 이용해 데이터가 저장될 위치를 빠르게 찾는다.</p>
<p>개념적인 과정은 다음과 같다.</p>
<pre><code class="language-text">객체
→ hashCode()
→ 저장 위치 계산
→ 해당 위치에서 equals()로 최종 비교</code></pre>
<hr>
<h2 id="해시-충돌">해시 충돌</h2>
<p>서로 다른 객체가 같은 해시값이나 같은 저장 위치를 가질 수 있다. 이를 해시 충돌이라고 한다.</p>
<p>해시 코드는 <code>int</code> 범위의 값이지만 실제로 수십억 개의 배열을 생성할 수는 없다. 내부 배열의 크기에 맞게 저장 위치를 계산하기 때문에 서로 다른 해시 코드도 같은 위치에 배정될 수 있다.</p>
<p>Java의 <code>HashMap</code>은 같은 위치에 여러 요소가 배정되면 연결 구조로 관리한다. 충돌이 많이 발생하고 일정 조건을 만족하면 Java 8부터 일부 구간을 트리 구조로 전환해 검색 성능이 지나치게 떨어지는 것을 줄인다.</p>
<p>따라서 <code>HashSet</code>과 <code>HashMap</code>이 항상 단순한 연결 리스트만 사용하는 것은 아니다.</p>
<hr>
<h2 id="중복-객체를-판단하는-기준">중복 객체를 판단하는 기준</h2>
<p><code>HashSet</code>은 일반적으로 다음 두 조건을 이용해 논리적으로 같은 객체인지 판단한다.</p>
<pre><code class="language-text">hashCode()가 같음
그리고
equals()가 true</code></pre>
<p>회원 전화번호가 같으면 같은 회원으로 판단하는 예제를 만들어보자.</p>
<pre><code class="language-java">import java.util.Objects;

public class SmartPhone {

    private final String number;
    private final String model;

    public SmartPhone(
            String number,
            String model
    ) {
        this.number = number;
        this.model = model;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof SmartPhone phone)) {
            return false;
        }

        return Objects.equals(
                number,
                phone.number
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(number);
    }

    @Override
    public String toString() {
        return &quot;SmartPhone{&quot;
                + &quot;number=&#39;&quot; + number + &#39;\&#39;&#39;
                + &quot;, model=&#39;&quot; + model + &#39;\&#39;&#39;
                + &#39;}&#39;;
    }
}</code></pre>
<pre><code class="language-java">import java.util.HashSet;
import java.util.Set;

public class HashSetExample {

    public static void main(String[] args) {
        Set&lt;SmartPhone&gt; phones =
                new HashSet&lt;&gt;();

        phones.add(
                new SmartPhone(
                        &quot;010-1111-2222&quot;,
                        &quot;Model A&quot;
                )
        );

        phones.add(
                new SmartPhone(
                        &quot;010-1111-2222&quot;,
                        &quot;Model B&quot;
                )
        );

        System.out.println(phones.size());
        System.out.println(phones);
    }
}</code></pre>
<p>전화번호를 기준으로 <code>equals()</code>와 <code>hashCode()</code>를 재정의했으므로 두 객체를 같은 데이터로 판단한다.</p>
<pre><code class="language-text">1</code></pre>
<p><code>equals()</code>를 재정의할 때는 반드시 동일한 필드 기준으로 <code>hashCode()</code>도 함께 재정의해야 한다.</p>
<hr>
<h1 id="map-key와-value를-함께-관리하기">Map: Key와 Value를 함께 관리하기</h1>
<h2 id="map의-특징">Map의 특징</h2>
<p><code>Map</code>은 Key와 Value의 쌍으로 데이터를 관리한다.</p>
<pre><code class="language-text">Key          Value

&quot;apple&quot;   → &quot;사과&quot;
&quot;banana&quot;  → &quot;바나나&quot;
&quot;grape&quot;   → &quot;포도&quot;</code></pre>
<p>하나의 Key와 Value 쌍을 Entry라고 부른다.</p>
<p>Key는 중복될 수 없지만 Value는 중복될 수 있다.</p>
<pre><code class="language-java">Map&lt;String, Integer&gt; scores =
        new HashMap&lt;&gt;();

scores.put(&quot;김자바&quot;, 90);
scores.put(&quot;이자바&quot;, 90);</code></pre>
<p>점수 <code>90</code>은 중복될 수 있다.</p>
<p>같은 Key로 값을 다시 저장하면 기존 값이 교체된다.</p>
<pre><code class="language-java">scores.put(&quot;김자바&quot;, 100);</code></pre>
<hr>
<h2 id="map의-주요-메서드">Map의 주요 메서드</h2>
<table>
<thead>
<tr>
<th>메서드</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td><code>put(K key, V value)</code></td>
<td>Key와 Value 저장</td>
</tr>
<tr>
<td><code>get(Object key)</code></td>
<td>Key에 해당하는 Value 조회</td>
</tr>
<tr>
<td><code>getOrDefault(Object key, V defaultValue)</code></td>
<td>값이 없으면 기본값 반환</td>
</tr>
<tr>
<td><code>containsKey(Object key)</code></td>
<td>Key 존재 여부 확인</td>
</tr>
<tr>
<td><code>containsValue(Object value)</code></td>
<td>Value 존재 여부 확인</td>
</tr>
<tr>
<td><code>remove(Object key)</code></td>
<td>Key에 해당하는 Entry 삭제</td>
</tr>
<tr>
<td><code>keySet()</code></td>
<td>모든 Key를 Set으로 반환</td>
</tr>
<tr>
<td><code>values()</code></td>
<td>모든 Value를 Collection으로 반환</td>
</tr>
<tr>
<td><code>entrySet()</code></td>
<td>모든 Entry를 Set으로 반환</td>
</tr>
<tr>
<td><code>size()</code></td>
<td>Entry 개수 반환</td>
</tr>
<tr>
<td><code>clear()</code></td>
<td>모든 Entry 삭제</td>
</tr>
</tbody></table>
<hr>
<h2 id="hashmap-사용하기">HashMap 사용하기</h2>
<pre><code class="language-java">import java.util.HashMap;
import java.util.Map;

public class HashMapExample {

    public static void main(String[] args) {
        Map&lt;String, Integer&gt; scores =
                new HashMap&lt;&gt;();

        scores.put(&quot;김자바&quot;, 90);
        scores.put(&quot;이자바&quot;, 85);
        scores.put(&quot;박자바&quot;, 95);

        int kimScore = scores.get(&quot;김자바&quot;);

        System.out.println(kimScore);

        scores.put(&quot;김자바&quot;, 100);

        System.out.println(scores);

        scores.remove(&quot;이자바&quot;);

        System.out.println(scores);
    }
}</code></pre>
<hr>
<h2 id="존재하지-않는-key-조회하기">존재하지 않는 Key 조회하기</h2>
<p><code>get()</code>으로 존재하지 않는 Key를 조회하면 <code>null</code>이 반환된다.</p>
<pre><code class="language-java">Integer score = scores.get(&quot;없는 회원&quot;);</code></pre>
<p>기본값이 필요하다면 <code>getOrDefault()</code>를 사용할 수 있다.</p>
<pre><code class="language-java">int score =
        scores.getOrDefault(
                &quot;없는 회원&quot;,
                0
        );</code></pre>
<hr>
<h2 id="map-순회하기">Map 순회하기</h2>
<p>Key와 Value를 함께 사용할 때는 <code>entrySet()</code>이 편리하다.</p>
<pre><code class="language-java">for (
        Map.Entry&lt;String, Integer&gt; entry
                : scores.entrySet()
) {
    System.out.println(
            entry.getKey()
                    + &quot;: &quot;
                    + entry.getValue()
    );
}</code></pre>
<p>람다를 사용하면 더 간결하게 작성할 수 있다.</p>
<pre><code class="language-java">scores.forEach(
        (name, score) -&gt;
                System.out.println(
                        name + &quot;: &quot; + score
                )
);</code></pre>
<hr>
<h2 id="hashmap의-내부-구조">HashMap의 내부 구조</h2>
<p><code>HashMap</code>은 내부적으로 Key의 <code>hashCode()</code>를 이용해 저장 위치를 계산한다.</p>
<p>개념적으로 다음과 같은 구조를 가진다.</p>
<pre><code class="language-text">내부 배열

index 0 → Entry
index 1 → Entry → Entry
index 2 → 비어 있음
index 3 → Entry</code></pre>
<p>Entry에는 Key, Value, 해시 정보, 다음 Entry를 가리키는 정보 등이 포함된다.</p>
<p>Key의 중복을 판단할 때도 <code>hashCode()</code>와 <code>equals()</code>가 사용된다.</p>
<p>따라서 사용자 정의 객체를 <code>HashMap</code>의 Key로 사용할 때는 논리적인 동등성 기준에 맞게 두 메서드를 재정의해야 한다.</p>
<p>또한 Key로 사용하는 객체의 비교 기준 필드는 저장 후 변경하지 않는 것이 안전하다. Key의 해시값이 변경되면 저장된 데이터를 다시 찾지 못할 수 있다.</p>
<hr>
<h2 id="set과-map의-관계">Set과 Map의 관계</h2>
<p><code>HashSet</code>은 내부적으로 <code>HashMap</code>을 이용한다.</p>
<p>Set에 저장할 요소를 <code>HashMap</code>의 Key로 사용하고, Value에는 의미 없는 공통 객체를 저장하는 방식이다.</p>
<pre><code class="language-text">HashSet 요소
→ HashMap의 Key

내부 Value
→ 공통 더미 객체</code></pre>
<p>이 때문에 <code>HashSet</code>도 <code>HashMap</code>과 마찬가지로 <code>hashCode()</code>와 <code>equals()</code>를 이용해 중복을 판단한다.</p>
<hr>
<h1 id="컬렉션-생성과-정렬">컬렉션 생성과 정렬</h1>
<h2 id="불변-컬렉션">불변 컬렉션</h2>
<p>Java 9부터 <code>List.of()</code>, <code>Set.of()</code>, <code>Map.of()</code>를 이용해 변경할 수 없는 컬렉션을 만들 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; languages =
        List.of(
                &quot;Java&quot;,
                &quot;Python&quot;,
                &quot;JavaScript&quot;
        );</code></pre>
<p>다음 작업은 모두 불가능하다.</p>
<pre><code class="language-java">// languages.add(&quot;C++&quot;);
// languages.remove(&quot;Java&quot;);
// languages.set(0, &quot;Kotlin&quot;);</code></pre>
<p>실행하면 <code>UnsupportedOperationException</code>이 발생한다.</p>
<hr>
<h2 id="listof-setof-mapof">List.of(), Set.of(), Map.of()</h2>
<pre><code class="language-java">List&lt;String&gt; list =
        List.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;);

Set&lt;String&gt; set =
        Set.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;);

Map&lt;String, Integer&gt; map =
        Map.of(
                &quot;A&quot;, 10,
                &quot;B&quot;, 20
        );</code></pre>
<p>이 컬렉션들은 <code>null</code>을 허용하지 않는다.</p>
<pre><code class="language-java">// List.of(&quot;A&quot;, null);
// NullPointerException</code></pre>
<p><code>Set.of()</code>에는 중복값을 전달할 수 없다.</p>
<pre><code class="language-java">// Set.of(&quot;A&quot;, &quot;A&quot;);
// IllegalArgumentException</code></pre>
<p><code>Map.of()</code>도 중복된 Key를 허용하지 않는다.</p>
<hr>
<h2 id="수정-가능한-컬렉션으로-복사하기">수정 가능한 컬렉션으로 복사하기</h2>
<p><code>List.of()</code>로 만든 데이터를 수정해야 한다면 새 컬렉션으로 복사한다.</p>
<pre><code class="language-java">List&lt;String&gt; languages =
        new ArrayList&lt;&gt;(
                List.of(
                        &quot;Java&quot;,
                        &quot;Python&quot;
                )
        );

languages.add(&quot;JavaScript&quot;);</code></pre>
<hr>
<h2 id="arraysaslist와-listof-비교">Arrays.asList()와 List.of() 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>Arrays.asList()</code></th>
<th><code>List.of()</code></th>
</tr>
</thead>
<tbody><tr>
<td>크기 변경</td>
<td>불가능</td>
<td>불가능</td>
</tr>
<tr>
<td>요소 변경 <code>set()</code></td>
<td>가능</td>
<td>불가능</td>
</tr>
<tr>
<td><code>null</code> 요소</td>
<td>허용</td>
<td>허용하지 않음</td>
</tr>
<tr>
<td>원본 배열 연결</td>
<td>연결됨</td>
<td>해당 없음</td>
</tr>
<tr>
<td>도입 버전</td>
<td>초기 Java부터</td>
<td>Java 9</td>
</tr>
</tbody></table>
<hr>
<h2 id="list-정렬하기">List 정렬하기</h2>
<p><code>Collections.sort()</code>를 이용해 리스트를 오름차순으로 정렬할 수 있다.</p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SortExample {

    public static void main(String[] args) {
        List&lt;Integer&gt; numbers =
                new ArrayList&lt;&gt;(
                        List.of(5, 2, 4, 1, 3)
                );

        Collections.sort(numbers);

        System.out.println(numbers);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[1, 2, 3, 4, 5]</code></pre>
<p><code>List</code>의 <code>sort()</code> 메서드를 사용할 수도 있다.</p>
<pre><code class="language-java">numbers.sort(null);</code></pre>
<hr>
<h2 id="내림차순-정렬">내림차순 정렬</h2>
<pre><code class="language-java">numbers.sort(
        Collections.reverseOrder()
);</code></pre>
<p>Comparator를 사용하면 다음과 같이 작성할 수도 있다.</p>
<pre><code class="language-java">numbers.sort(
        (left, right) -&gt;
                Integer.compare(
                        right,
                        left
                )
);</code></pre>
<hr>
<h2 id="사용자-정의-객체-정렬">사용자 정의 객체 정렬</h2>
<p>회원 객체를 나이순으로 정렬해보자.</p>
<pre><code class="language-java">public class Member {

    private final String name;
    private final int age;

    public Member(
            String name,
            int age
    ) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + &quot;(&quot; + age + &quot;)&quot;;
    }
}</code></pre>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class MemberSortExample {

    public static void main(String[] args) {
        List&lt;Member&gt; members =
                new ArrayList&lt;&gt;(
                        List.of(
                                new Member(&quot;김자바&quot;, 25),
                                new Member(&quot;이자바&quot;, 20),
                                new Member(&quot;박자바&quot;, 30)
                        )
                );

        members.sort(
                Comparator.comparingInt(
                        Member::getAge
                )
        );

        System.out.println(members);
    }
}</code></pre>
<p>나이가 같은 경우 이름으로 추가 정렬할 수 있다.</p>
<pre><code class="language-java">members.sort(
        Comparator
                .comparingInt(Member::getAge)
                .thenComparing(Member::getName)
);</code></pre>
<hr>
<h2 id="comparable과-comparator">Comparable과 Comparator</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Comparable</th>
<th>Comparator</th>
</tr>
</thead>
<tbody><tr>
<td>메서드</td>
<td><code>compareTo()</code></td>
<td><code>compare()</code></td>
</tr>
<tr>
<td>정렬 기준</td>
<td>클래스 내부의 기본 정렬</td>
<td>외부에서 다양한 정렬 기준 정의</td>
</tr>
<tr>
<td>패키지</td>
<td><code>java.lang</code></td>
<td><code>java.util</code></td>
</tr>
<tr>
<td>사용 예</td>
<td>이름을 기본 정렬 기준으로 지정</td>
<td>나이순, 점수순 등 상황별 정렬</td>
</tr>
</tbody></table>
<p>클래스 자체에 자연스러운 기본 정렬 기준이 있다면 <code>Comparable</code>을 구현할 수 있다.</p>
<p>다양한 정렬 기준이 필요하다면 <code>Comparator</code>가 더 유연하다.</p>
<hr>
<h1 id="람다-표현식으로-동작-전달하기">람다 표현식으로 동작 전달하기</h1>
<h2 id="람다-표현식이란">람다 표현식이란?</h2>
<p>람다 표현식은 하나의 동작을 간결한 코드 블록으로 표현하는 문법이다.</p>
<p>기존에는 간단한 동작 하나를 전달하기 위해 익명 구현 객체를 만들어야 했다.</p>
<pre><code class="language-java">Comparator&lt;Integer&gt; comparator =
        new Comparator&lt;Integer&gt;() {

            @Override
            public int compare(
                    Integer left,
                    Integer right
            ) {
                return Integer.compare(
                        left,
                        right
                );
            }
        };</code></pre>
<p>람다 표현식을 사용하면 다음처럼 줄일 수 있다.</p>
<pre><code class="language-java">Comparator&lt;Integer&gt; comparator =
        (left, right) -&gt;
                Integer.compare(
                        left,
                        right
                );</code></pre>
<p>람다는 메서드 자체가 아니라 함수형 인터페이스의 구현 객체로 해석된다.</p>
<hr>
<h2 id="함수형-인터페이스">함수형 인터페이스</h2>
<p>추상 메서드가 하나만 존재하는 인터페이스를 함수형 인터페이스라고 한다.</p>
<pre><code class="language-java">@FunctionalInterface
public interface Calculator {

    int calculate(int left, int right);
}</code></pre>
<p>람다 표현식을 구현 객체로 대입할 수 있다.</p>
<pre><code class="language-java">Calculator add =
        (left, right) -&gt;
                left + right;

int result = add.calculate(10, 20);</code></pre>
<p><code>@FunctionalInterface</code>는 해당 인터페이스가 함수형 인터페이스의 조건을 만족하는지 컴파일러가 검사하도록 한다.</p>
<p><code>default</code> 메서드와 <code>static</code> 메서드는 여러 개 존재해도 상관없다. 추상 메서드만 하나여야 한다.</p>
<hr>
<h2 id="람다-문법">람다 문법</h2>
<p>매개변수가 하나라면 괄호를 생략할 수 있다.</p>
<pre><code class="language-java">value -&gt; System.out.println(value)</code></pre>
<p>실행문이 하나라면 중괄호를 생략할 수 있다.</p>
<pre><code class="language-java">(left, right) -&gt; left + right</code></pre>
<p>실행문이 여러 개라면 중괄호를 사용한다.</p>
<pre><code class="language-java">(left, right) -&gt; {
    int result = left + right;

    return result;
}</code></pre>
<p>중괄호를 사용하면서 값을 반환하려면 <code>return</code>이 필요하다.</p>
<hr>
<h2 id="컬렉션과-람다">컬렉션과 람다</h2>
<p>컬렉션 API에는 람다 표현식을 사용할 수 있는 메서드가 많다.</p>
<pre><code class="language-java">List&lt;String&gt; names =
        new ArrayList&lt;&gt;(
                List.of(
                        &quot;김자바&quot;,
                        &quot;이자바&quot;,
                        &quot;박자바&quot;
                )
        );

names.forEach(
        name -&gt;
                System.out.println(name)
);</code></pre>
<p>조건에 맞는 요소를 삭제할 수도 있다.</p>
<pre><code class="language-java">names.removeIf(
        name -&gt; name.startsWith(&quot;김&quot;)
);</code></pre>
<p>모든 요소를 변경할 수도 있다.</p>
<pre><code class="language-java">names.replaceAll(
        String::toUpperCase
);</code></pre>
<hr>
<h2 id="메서드-참조">메서드 참조</h2>
<p>람다 표현식이 기존 메서드 하나만 호출한다면 <code>::</code> 연산자를 이용한 메서드 참조로 표현할 수 있다.</p>
<pre><code class="language-java">names.forEach(
        name -&gt;
                System.out.println(name)
);</code></pre>
<p>다음처럼 변경할 수 있다.</p>
<pre><code class="language-java">names.forEach(
        System.out::println
);</code></pre>
<hr>
<h2 id="정적-메서드-참조">정적 메서드 참조</h2>
<pre><code class="language-java">Function&lt;String, Integer&gt; parser =
        Integer::parseInt;</code></pre>
<p>다음 람다와 같다.</p>
<pre><code class="language-java">Function&lt;String, Integer&gt; parser =
        value -&gt; Integer.parseInt(value);</code></pre>
<hr>
<h2 id="특정-객체의-인스턴스-메서드-참조">특정 객체의 인스턴스 메서드 참조</h2>
<pre><code class="language-java">String prefix = &quot;Java&quot;;

Predicate&lt;String&gt; checker =
        prefix::equals;</code></pre>
<p>다음 람다와 같다.</p>
<pre><code class="language-java">Predicate&lt;String&gt; checker =
        value -&gt; prefix.equals(value);</code></pre>
<hr>
<h2 id="임의-객체의-인스턴스-메서드-참조">임의 객체의 인스턴스 메서드 참조</h2>
<pre><code class="language-java">Function&lt;String, String&gt; converter =
        String::toUpperCase;</code></pre>
<p>다음 람다와 같다.</p>
<pre><code class="language-java">Function&lt;String, String&gt; converter =
        value -&gt; value.toUpperCase();</code></pre>
<hr>
<h2 id="생성자-참조">생성자 참조</h2>
<pre><code class="language-java">Supplier&lt;StringBuilder&gt; supplier =
        StringBuilder::new;</code></pre>
<p>다음 람다와 같다.</p>
<pre><code class="language-java">Supplier&lt;StringBuilder&gt; supplier =
        () -&gt; new StringBuilder();</code></pre>
<p>매개변수가 있는 생성자도 참조할 수 있다.</p>
<pre><code class="language-java">Function&lt;String, StringBuilder&gt; creator =
        StringBuilder::new;</code></pre>
<hr>
<h2 id="순회-중-같은-컬렉션을-수정하지-않기">순회 중 같은 컬렉션을 수정하지 않기</h2>
<p>다음 코드는 잘못된 예제다.</p>
<pre><code class="language-java">// names.forEach(names::add);</code></pre>
<p><code>names</code>를 순회하면서 같은 <code>names</code>에 다시 데이터를 추가하기 때문에 구조적 변경이 발생한다. <code>ConcurrentModificationException</code>이 발생하거나 의도하지 않은 동작으로 이어질 수 있다.</p>
<p>출력하려면 다음처럼 사용해야 한다.</p>
<pre><code class="language-java">names.forEach(System.out::println);</code></pre>
<p>다른 컬렉션에 복사하려면 목적지 컬렉션을 별도로 둔다.</p>
<pre><code class="language-java">List&lt;String&gt; copied =
        new ArrayList&lt;&gt;();

names.forEach(copied::add);</code></pre>
<hr>
<h1 id="표준-함수형-인터페이스">표준 함수형 인터페이스</h1>
<p>Java는 자주 사용하는 함수 형태를 <code>java.util.function</code> 패키지의 표준 인터페이스로 제공한다.</p>
<pre><code class="language-text">Consumer
Supplier
Function
Operator
Predicate</code></pre>
<p>직접 함수형 인터페이스를 만들 필요 없이 목적에 맞는 표준 인터페이스를 사용할 수 있다.</p>
<hr>
<h2 id="consumer">Consumer</h2>
<p>매개값을 받고 반환값이 없는 형태다.</p>
<pre><code class="language-text">T → 반환값 없음</code></pre>
<pre><code class="language-java">Consumer&lt;String&gt; printer =
        value -&gt;
                System.out.println(value);

printer.accept(&quot;Java&quot;);</code></pre>
<p>메서드 참조로 표현할 수 있다.</p>
<pre><code class="language-java">Consumer&lt;String&gt; printer =
        System.out::println;</code></pre>
<p>대표 메서드는 <code>accept()</code>다.</p>
<hr>
<h2 id="supplier">Supplier</h2>
<p>매개값은 없고 값을 반환하는 형태다.</p>
<pre><code class="language-text">매개값 없음 → T</code></pre>
<pre><code class="language-java">Supplier&lt;StringBuilder&gt; supplier =
        StringBuilder::new;

StringBuilder builder = supplier.get();</code></pre>
<p>대표 메서드는 <code>get()</code>이다.</p>
<hr>
<h2 id="function">Function</h2>
<p>매개값을 받아 다른 값을 반환한다.</p>
<pre><code class="language-text">T → R</code></pre>
<pre><code class="language-java">Function&lt;String, Integer&gt; lengthFunction =
        String::length;

int length =
        lengthFunction.apply(&quot;Java&quot;);</code></pre>
<p>대표 메서드는 <code>apply()</code>다.</p>
<hr>
<h2 id="operator">Operator</h2>
<p>매개값과 반환값의 타입이 같은 함수다.</p>
<pre><code class="language-text">T → T</code></pre>
<p>하나의 값을 받아 같은 타입으로 반환하는 경우 <code>UnaryOperator</code>를 사용한다.</p>
<pre><code class="language-java">UnaryOperator&lt;Integer&gt; doubleValue =
        number -&gt; number * 2;

System.out.println(
        doubleValue.apply(10)
);</code></pre>
<p>두 값을 받아 같은 타입으로 반환하는 경우 <code>BinaryOperator</code>를 사용한다.</p>
<pre><code class="language-java">BinaryOperator&lt;Integer&gt; add =
        Integer::sum;

System.out.println(
        add.apply(10, 20)
);</code></pre>
<hr>
<h2 id="predicate">Predicate</h2>
<p>매개값을 받아 <code>boolean</code>을 반환한다.</p>
<pre><code class="language-text">T → boolean</code></pre>
<pre><code class="language-java">Predicate&lt;Integer&gt; isEven =
        number -&gt; number % 2 == 0;

System.out.println(
        isEven.test(10)
);</code></pre>
<p>대표 메서드는 <code>test()</code>다.</p>
<p>필터링과 조건 검사에 많이 사용된다.</p>
<hr>
<h2 id="표준-함수형-인터페이스-비교">표준 함수형 인터페이스 비교</h2>
<table>
<thead>
<tr>
<th>인터페이스</th>
<th>매개변수</th>
<th>반환값</th>
<th>대표 메서드</th>
</tr>
</thead>
<tbody><tr>
<td><code>Consumer&lt;T&gt;</code></td>
<td>있음</td>
<td>없음</td>
<td><code>accept()</code></td>
</tr>
<tr>
<td><code>Supplier&lt;T&gt;</code></td>
<td>없음</td>
<td>있음</td>
<td><code>get()</code></td>
</tr>
<tr>
<td><code>Function&lt;T, R&gt;</code></td>
<td>있음</td>
<td>있음</td>
<td><code>apply()</code></td>
</tr>
<tr>
<td><code>UnaryOperator&lt;T&gt;</code></td>
<td>있음</td>
<td>동일 타입</td>
<td><code>apply()</code></td>
</tr>
<tr>
<td><code>BinaryOperator&lt;T&gt;</code></td>
<td>2개</td>
<td>동일 타입</td>
<td><code>apply()</code></td>
</tr>
<tr>
<td><code>Predicate&lt;T&gt;</code></td>
<td>있음</td>
<td><code>boolean</code></td>
<td><code>test()</code></td>
</tr>
</tbody></table>
<hr>
<h1 id="optional과-stream-api">Optional과 Stream API</h1>
<h2 id="optional이란">Optional이란?</h2>
<p><code>Optional&lt;T&gt;</code>은 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 표현하는 래퍼 클래스다.</p>
<pre><code class="language-java">Optional&lt;Member&gt;</code></pre>
<p>메서드가 <code>null</code>을 직접 반환하는 대신 값의 존재 여부를 명시적으로 표현할 수 있다.</p>
<pre><code class="language-java">import java.util.Optional;

public class MemberRepository {

    public Optional&lt;String&gt; findNameById(
            long id
    ) {
        if (id == 1L) {
            return Optional.of(&quot;김자바&quot;);
        }

        return Optional.empty();
    }
}</code></pre>
<hr>
<h2 id="optional-사용하기">Optional 사용하기</h2>
<pre><code class="language-java">Optional&lt;String&gt; name =
        repository.findNameById(1L);

if (name.isPresent()) {
    System.out.println(name.get());
}</code></pre>
<p>다만 <code>isPresent()</code>와 <code>get()</code>을 반복해서 사용하는 것보다는 Optional 전용 메서드를 활용하는 것이 좋다.</p>
<pre><code class="language-java">name.ifPresent(
        System.out::println
);</code></pre>
<p>값이 없을 때 기본값을 반환할 수 있다.</p>
<pre><code class="language-java">String result =
        name.orElse(&quot;이름 없음&quot;);</code></pre>
<p>기본값 생성 비용이 크다면 <code>orElseGet()</code>을 사용할 수 있다.</p>
<pre><code class="language-java">String result =
        name.orElseGet(
                () -&gt; createDefaultName()
        );</code></pre>
<p>값이 없을 때 예외를 발생시킬 수도 있다.</p>
<pre><code class="language-java">String result =
        name.orElseThrow(
                () -&gt; new IllegalArgumentException(
                        &quot;회원을 찾을 수 없습니다.&quot;
                )
        );</code></pre>
<hr>
<h2 id="orelse와-orelseget의-차이">orElse와 orElseGet의 차이</h2>
<pre><code class="language-java">optional.orElse(
        createDefaultValue()
);</code></pre>
<p><code>orElse()</code>에 전달한 코드는 Optional에 값이 있어도 먼저 실행될 수 있다.</p>
<pre><code class="language-java">optional.orElseGet(
        () -&gt; createDefaultValue()
);</code></pre>
<p><code>orElseGet()</code>은 값이 없을 때만 Supplier를 실행한다.</p>
<p>기본값 생성에 비용이 들거나 부수 효과가 있다면 <code>orElseGet()</code>이 적합하다.</p>
<hr>
<h2 id="optional-사용-시-주의할-점">Optional 사용 시 주의할 점</h2>
<p><code>Optional</code>은 주로 메서드 반환값에서 값이 없을 가능성을 표현하기 위해 사용한다.</p>
<p>다음과 같은 사용은 일반적으로 신중해야 한다.</p>
<ul>
<li>엔티티나 DTO의 필드 타입으로 사용</li>
<li>메서드 매개변수로 사용</li>
<li>컬렉션 자체를 Optional로 감싸기</li>
<li><code>Optional&lt;List&lt;T&gt;&gt;</code>처럼 불필요하게 중첩하기</li>
<li>무조건 <code>get()</code> 호출하기</li>
</ul>
<p>컬렉션은 결과가 없으면 빈 컬렉션을 반환하는 것이 자연스럽다.</p>
<pre><code class="language-java">return List.of();</code></pre>
<hr>
<h2 id="stream-api란">Stream API란?</h2>
<p>Stream API는 배열과 컬렉션의 요소를 선언적인 방식으로 처리하기 위한 API다.</p>
<p>기존 반복문에서는 처리 절차를 직접 작성한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers =
        List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; evenNumbers =
        new ArrayList&lt;&gt;();

for (Integer number : numbers) {
    if (number % 2 == 0) {
        evenNumbers.add(number);
    }
}</code></pre>
<p>Stream을 사용하면 어떤 작업을 할 것인지 중심으로 작성할 수 있다.</p>
<pre><code class="language-java">List&lt;Integer&gt; evenNumbers =
        numbers.stream()
                .filter(
                        number -&gt;
                                number % 2 == 0
                )
                .toList();</code></pre>
<hr>
<h2 id="stream의-처리-흐름">Stream의 처리 흐름</h2>
<p>Stream 연산은 크게 세 단계로 볼 수 있다.</p>
<pre><code class="language-text">데이터 소스
→ 중간 연산
→ 최종 연산</code></pre>
<pre><code class="language-java">numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .forEach(System.out::println);</code></pre>
<ul>
<li><code>stream()</code>: 스트림 생성</li>
<li><code>filter()</code>: 조건에 맞는 요소 선택</li>
<li><code>map()</code>: 요소 변환</li>
<li><code>forEach()</code>: 결과 소비</li>
</ul>
<hr>
<h2 id="중간-연산과-최종-연산">중간 연산과 최종 연산</h2>
<p>중간 연산은 새로운 Stream을 반환하며 여러 개 연결할 수 있다.</p>
<p>대표적인 중간 연산은 다음과 같다.</p>
<ul>
<li><code>filter()</code></li>
<li><code>map()</code></li>
<li><code>sorted()</code></li>
<li><code>distinct()</code></li>
<li><code>limit()</code></li>
<li><code>skip()</code></li>
<li><code>peek()</code></li>
</ul>
<p>최종 연산이 호출되어야 실제 처리가 시작된다.</p>
<p>대표적인 최종 연산은 다음과 같다.</p>
<ul>
<li><code>forEach()</code></li>
<li><code>toList()</code></li>
<li><code>collect()</code></li>
<li><code>count()</code></li>
<li><code>reduce()</code></li>
<li><code>findFirst()</code></li>
<li><code>anyMatch()</code></li>
<li><code>allMatch()</code></li>
</ul>
<hr>
<h2 id="stream은-원본-컬렉션을-변경하지-않는다">Stream은 원본 컬렉션을 변경하지 않는다</h2>
<pre><code class="language-java">List&lt;Integer&gt; numbers =
        new ArrayList&lt;&gt;(
                List.of(5, 2, 4, 1, 3)
        );

List&lt;Integer&gt; sortedNumbers =
        numbers.stream()
                .sorted()
                .toList();

System.out.println(numbers);
System.out.println(sortedNumbers);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[5, 2, 4, 1, 3]
[1, 2, 3, 4, 5]</code></pre>
<p>Stream 연산은 원본 컬렉션 자체를 정렬하지 않고 새로운 결과를 만든다.</p>
<hr>
<h2 id="map-연산">map 연산</h2>
<p><code>map()</code>은 각 요소를 다른 값으로 변환한다.</p>
<pre><code class="language-java">List&lt;String&gt; names =
        List.of(
                &quot;java&quot;,
                &quot;spring&quot;,
                &quot;vue&quot;
        );

List&lt;String&gt; upperNames =
        names.stream()
                .map(String::toUpperCase)
                .toList();</code></pre>
<hr>
<h2 id="reduce-연산">reduce 연산</h2>
<p><code>reduce()</code>는 여러 요소를 하나의 결과로 합친다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers =
        List.of(1, 2, 3, 4, 5);

int sum =
        numbers.stream()
                .reduce(
                        0,
                        Integer::sum
                );

System.out.println(sum);</code></pre>
<p><code>0</code>은 초기값이며 <code>Integer::sum</code>은 두 값을 합치는 함수다.</p>
<hr>
<h2 id="맵과-리듀스-모델">맵과 리듀스 모델</h2>
<p>Map-Reduce 모델은 데이터를 변환한 뒤 결과를 하나로 합치는 처리 방식이다.</p>
<pre><code class="language-text">원본 데이터

[1, 2, 3, 4]

map
각 값을 제곱

[1, 4, 9, 16]

reduce
모든 값을 합산

30</code></pre>
<pre><code class="language-java">int result =
        List.of(1, 2, 3, 4)
                .stream()
                .map(
                        number -&gt;
                                number * number
                )
                .reduce(
                        0,
                        Integer::sum
                );</code></pre>
<hr>
<h1 id="종합-예제-회원-목록-처리하기">종합 예제: 회원 목록 처리하기</h1>
<p>회원 데이터를 컬렉션에 저장하고 Stream으로 처리해보자.</p>
<h2 id="member-클래스">Member 클래스</h2>
<pre><code class="language-java">import java.util.Objects;

public class Member {

    private final long id;
    private final String name;
    private final int age;
    private final Grade grade;

    public Member(
            long id,
            String name,
            int age,
            Grade grade
    ) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Grade getGrade() {
        return grade;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof Member member)) {
            return false;
        }

        return id == member.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return name
                + &quot;(&quot;
                + age
                + &quot;, &quot;
                + grade
                + &quot;)&quot;;
    }
}</code></pre>
<h2 id="grade-enum">Grade Enum</h2>
<pre><code class="language-java">public enum Grade {
    BASIC,
    SILVER,
    GOLD
}</code></pre>
<h2 id="실행-클래스">실행 클래스</h2>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CollectionExample {

    public static void main(String[] args) {
        List&lt;Member&gt; members =
                new ArrayList&lt;&gt;();

        members.add(
                new Member(
                        1L,
                        &quot;김자바&quot;,
                        25,
                        Grade.GOLD
                )
        );

        members.add(
                new Member(
                        2L,
                        &quot;이자바&quot;,
                        20,
                        Grade.BASIC
                )
        );

        members.add(
                new Member(
                        3L,
                        &quot;박자바&quot;,
                        30,
                        Grade.SILVER
                )
        );

        members.add(
                new Member(
                        4L,
                        &quot;최자바&quot;,
                        22,
                        Grade.GOLD
                )
        );

        List&lt;String&gt; adultGoldMembers =
                members.stream()
                        .filter(
                                member -&gt;
                                        member.getAge() &gt;= 20
                        )
                        .filter(
                                member -&gt;
                                        member.getGrade()
                                                == Grade.GOLD
                        )
                        .sorted(
                                Comparator.comparingInt(
                                        Member::getAge
                                )
                        )
                        .map(Member::getName)
                        .toList();

        System.out.println(adultGoldMembers);

        Set&lt;Member&gt; memberSet =
                new HashSet&lt;&gt;(members);

        System.out.println(
                &quot;중복 제거 후 회원 수: &quot;
                        + memberSet.size()
        );

        Map&lt;Long, Member&gt; memberMap =
                new HashMap&lt;&gt;();

        for (Member member : members) {
            memberMap.put(
                    member.getId(),
                    member
            );
        }

        Member found =
                memberMap.get(1L);

        System.out.println(
                &quot;조회 결과: &quot; + found
        );
    }
}</code></pre>
<p>이 예제에서는 다음 개념이 사용되었다.</p>
<ul>
<li><code>ArrayList</code>에 회원 목록 저장</li>
<li><code>HashSet</code>으로 중복 관리</li>
<li><code>HashMap</code>으로 ID 기반 조회</li>
<li>Enum으로 회원 등급 제한</li>
<li>람다와 메서드 참조</li>
<li>Stream의 <code>filter()</code>, <code>sorted()</code>, <code>map()</code></li>
<li><code>equals()</code>와 <code>hashCode()</code>를 이용한 동등성 판단</li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="collection과-collections">Collection과 Collections</h2>
<pre><code class="language-java">Collection</code></pre>
<p><code>List</code>와 <code>Set</code>의 최상위 인터페이스다.</p>
<pre><code class="language-java">Collections</code></pre>
<p>컬렉션을 정렬하거나 변환하는 정적 유틸리티 메서드를 제공하는 클래스다.</p>
<pre><code class="language-java">Collections.sort(list);
Collections.reverse(list);
Collections.shuffle(list);</code></pre>
<hr>
<h2 id="list와-arraylist">List와 ArrayList</h2>
<pre><code class="language-java">List&lt;String&gt; names =
        new ArrayList&lt;&gt;();</code></pre>
<p><code>List</code>는 인터페이스이고 <code>ArrayList</code>는 구현 클래스다.</p>
<p>변수는 인터페이스 타입으로 선언하는 것이 구현체 교체에 유리하다.</p>
<pre><code class="language-java">List&lt;String&gt; names =
        new LinkedList&lt;&gt;();</code></pre>
<p>다만 <code>ArrayList</code>에만 존재하는 기능이 반드시 필요하다면 구체 타입을 사용할 수도 있다.</p>
<hr>
<h2 id="크기와-용량">크기와 용량</h2>
<pre><code class="language-java">List&lt;String&gt; names =
        new ArrayList&lt;&gt;(100);</code></pre>
<p>초기 용량은 100이지만 실제 요소 개수는 0이다.</p>
<pre><code class="language-java">names.size();</code></pre>
<p>결과는 <code>0</code>이다.</p>
<ul>
<li>크기: 현재 저장된 요소 개수</li>
<li>용량: 내부 배열이 요소를 저장할 수 있는 공간</li>
</ul>
<hr>
<h2 id="hashset의-순서">HashSet의 순서</h2>
<p><code>HashSet</code>은 입력 순서를 보장하지 않는다.</p>
<p>입력 순서가 필요하면 <code>LinkedHashSet</code>, 정렬된 순서가 필요하면 <code>TreeSet</code>을 고려한다.</p>
<hr>
<h2 id="map은-collection이-아니다">Map은 Collection이 아니다</h2>
<p><code>Map</code>은 여러 데이터를 관리하지만 <code>Collection</code> 인터페이스를 상속하지 않는다.</p>
<p>Key와 Value의 쌍을 관리하는 구조가 <code>List</code>와 <code>Set</code>의 단일 요소 구조와 다르기 때문이다.</p>
<hr>
<h2 id="hashmap의-key와-변경-가능한-객체">HashMap의 Key와 변경 가능한 객체</h2>
<p>객체를 <code>HashMap</code>의 Key로 사용한 뒤 <code>hashCode()</code> 계산에 사용된 필드 값을 변경하면 해당 Key를 다시 찾지 못할 수 있다.</p>
<p>따라서 Map의 Key는 <code>String</code>, <code>Integer</code>, Enum, 불변 값 객체처럼 변경되지 않는 타입을 사용하는 것이 안전하다.</p>
<hr>
<h2 id="stream과-컬렉션">Stream과 컬렉션</h2>
<p>컬렉션은 데이터를 저장한다.</p>
<p>Stream은 데이터를 저장하지 않고 데이터 처리 과정을 표현한다.</p>
<pre><code class="language-text">Collection
→ 데이터 저장

Stream
→ 데이터 처리</code></pre>
<p>Stream은 한 번 최종 연산을 수행하면 다시 사용할 수 없다.</p>
<pre><code class="language-java">Stream&lt;String&gt; stream =
        names.stream();

stream.forEach(System.out::println);

// 같은 stream 재사용 불가</code></pre>
<hr>
<h1 id="실무에서는-어떻게-사용할까">실무에서는 어떻게 사용할까?</h1>
<p>일반적인 목록 관리에는 <code>ArrayList</code>가 가장 자주 사용된다.</p>
<pre><code class="language-java">List&lt;Member&gt; members =
        new ArrayList&lt;&gt;();</code></pre>
<p>중복을 허용하지 않는 태그, 권한, 식별자 집합에는 <code>HashSet</code>이 적합하다.</p>
<pre><code class="language-java">Set&lt;String&gt; tags =
        new HashSet&lt;&gt;();</code></pre>
<p>ID나 코드로 특정 객체를 빠르게 조회해야 한다면 <code>HashMap</code>을 사용할 수 있다.</p>
<pre><code class="language-java">Map&lt;Long, Member&gt; memberById =
        new HashMap&lt;&gt;();</code></pre>
<p>하지만 데이터가 데이터베이스에 저장되어 있다면 애플리케이션에서 모든 데이터를 Map으로 복사하기보다 데이터베이스의 인덱스와 조회 기능을 활용해야 한다.</p>
<p>컬렉션 타입은 가능하면 인터페이스로 선언한다.</p>
<pre><code class="language-java">List&lt;Member&gt; members =
        new ArrayList&lt;&gt;();

Map&lt;Long, Member&gt; memberMap =
        new HashMap&lt;&gt;();</code></pre>
<p>외부에 컬렉션을 반환할 때 내부 컬렉션이 직접 변경되면 안 된다면 불변 복사본을 반환할 수 있다.</p>
<pre><code class="language-java">public List&lt;Member&gt; getMembers() {
    return List.copyOf(members);
}</code></pre>
<p><code>Stream</code>은 필터링, 변환, 집계가 연속적으로 이어질 때 가독성이 좋다.</p>
<pre><code class="language-java">members.stream()
        .filter(Member::isActive)
        .map(Member::getName)
        .sorted()
        .toList();</code></pre>
<p>반면 복잡한 상태 변경, 예외 처리, 여러 단계의 분기가 포함되면 일반 반복문이 더 명확할 수 있다. Stream을 사용한다고 항상 더 좋은 코드는 아니다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 Java 컬렉션 프레임워크와 함수형 데이터 처리 방법을 정리했다.</p>
<ul>
<li>자료구조는 데이터를 효율적으로 저장하고 접근하고 수정하기 위한 조직 방식이다.</li>
<li>배열은 길이가 고정되어 있어 데이터 개수가 자주 변하는 상황에서 불편할 수 있다.</li>
<li>컬렉션 프레임워크는 여러 데이터를 쉽게 저장하고 처리하기 위한 인터페이스와 클래스의 집합이다.</li>
<li><code>List</code>는 입력 순서를 유지하고 중복을 허용한다.</li>
<li><code>Set</code>은 중복을 허용하지 않는다.</li>
<li><code>Map</code>은 Key와 Value의 쌍으로 데이터를 관리한다.</li>
<li><code>Map</code>의 Key는 중복될 수 없지만 Value는 중복될 수 있다.</li>
<li><code>ArrayList</code>는 배열 기반이므로 인덱스 조회가 빠르다.</li>
<li><code>ArrayList</code>의 중간 삽입과 삭제에는 요소 이동이 필요하다.</li>
<li><code>LinkedList</code>는 각 요소가 앞뒤 노드의 참조를 가지는 이중 연결 구조다.</li>
<li><code>LinkedList</code>는 인덱스 조회를 위해 연결을 따라 이동해야 한다.</li>
<li>일반적인 목록에는 <code>ArrayList</code>가 기본 선택으로 자주 사용된다.</li>
<li>컬렉션 순회 중 직접 요소를 삭제하면 <code>ConcurrentModificationException</code>이 발생할 수 있다.</li>
<li>순회 중 삭제에는 <code>Iterator.remove()</code> 또는 <code>removeIf()</code>를 사용할 수 있다.</li>
<li>인덱스로 삭제할 때는 뒤에서부터 순회하면 인덱스 이동 문제를 피할 수 있다.</li>
<li><code>List&lt;Integer&gt;</code>의 <code>remove(int)</code>는 값이 아니라 인덱스를 삭제한다.</li>
<li>제네릭은 불공변이므로 <code>List&lt;Dog&gt;</code>는 <code>List&lt;Animal&gt;</code>의 하위 타입이 아니다.</li>
<li><code>Arrays.asList()</code>는 크기를 변경할 수 없지만 요소 교체는 가능하다.</li>
<li><code>HashSet</code>과 <code>HashMap</code>은 <code>hashCode()</code>와 <code>equals()</code>를 이용해 객체를 구분한다.</li>
<li><code>equals()</code>를 재정의하면 <code>hashCode()</code>도 같은 기준으로 재정의해야 한다.</li>
<li><code>HashSet</code>은 내부적으로 <code>HashMap</code>을 사용한다.</li>
<li>사용자 정의 객체를 Map의 Key로 사용할 때는 비교 기준 필드를 변경하지 않는 것이 좋다.</li>
<li><code>List.of()</code>, <code>Set.of()</code>, <code>Map.of()</code>는 변경할 수 없는 컬렉션을 생성한다.</li>
<li><code>List.of()</code> 계열은 <code>null</code>을 허용하지 않는다.</li>
<li><code>Collections.sort()</code>나 <code>List.sort()</code>를 이용해 리스트를 정렬할 수 있다.</li>
<li><code>Comparable</code>은 클래스의 기본 정렬 기준을 정의한다.</li>
<li><code>Comparator</code>는 외부에서 다양한 정렬 기준을 제공한다.</li>
<li>람다 표현식은 함수형 인터페이스의 구현을 간결하게 표현한다.</li>
<li>함수형 인터페이스에는 추상 메서드가 하나만 존재해야 한다.</li>
<li>메서드 참조는 기존 메서드 하나를 호출하는 람다를 간결하게 표현한다.</li>
<li><code>Consumer</code>는 매개값을 받고 반환하지 않는다.</li>
<li><code>Supplier</code>는 매개값 없이 값을 반환한다.</li>
<li><code>Function</code>은 값을 받아 다른 값으로 변환한다.</li>
<li><code>Operator</code>는 동일한 타입을 받아 동일한 타입으로 반환한다.</li>
<li><code>Predicate</code>는 값을 받아 <code>boolean</code>을 반환한다.</li>
<li><code>Optional</code>은 값이 존재할 수도 있고 없을 수도 있는 상황을 표현한다.</li>
<li>Optional의 값을 꺼낼 때 무조건 <code>get()</code>을 사용하는 것은 피하는 것이 좋다.</li>
<li>Stream API는 배열과 컬렉션의 요소를 선언적인 방식으로 처리한다.</li>
<li>Stream은 데이터를 저장하지 않으며 원본 컬렉션을 직접 변경하지 않는다.</li>
<li><code>filter()</code>는 조건에 맞는 요소를 선택한다.</li>
<li><code>map()</code>은 요소를 다른 값으로 변환한다.</li>
<li><code>reduce()</code>는 여러 요소를 하나의 결과로 합친다.</li>
<li>Stream의 중간 연산은 최종 연산이 호출될 때 실행된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (7) 제네릭, Enum, Annotation]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-7-%EC%A0%9C%EB%84%A4%EB%A6%AD-Enum-Annotation</link>
            <guid>https://velog.io/@tongmill_g/JAVA-7-%EC%A0%9C%EB%84%A4%EB%A6%AD-Enum-Annotation</guid>
            <pubDate>Mon, 27 Jul 2026 23:54:07 GMT</pubDate>
            <description><![CDATA[<h1 id="제네릭-enum-annotation">제네릭, Enum, Annotation</h1>
<h2 id="들어가며">들어가며</h2>
<p>Java로 프로그램을 작성하다 보면 다음과 같은 코드를 자주 만나게 된다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();
Map&lt;String, Integer&gt; scores = new HashMap&lt;&gt;();</code></pre>
<p>여기서 <code>&lt;String&gt;</code>, <code>&lt;Integer&gt;</code>처럼 사용할 타입을 지정하는 문법이 <strong>제네릭(Generic)</strong> 이다.</p>
<p>또한 주문 상태나 회원 등급처럼 사용할 수 있는 값이 제한되어 있다면 다음과 같이 <code>enum</code>을 사용한다.</p>
<pre><code class="language-java">OrderStatus status = OrderStatus.PAID;</code></pre>
<p>메서드 위에 붙는 <code>@Override</code>, <code>@Deprecated</code>와 같은 표현도 자주 볼 수 있다.</p>
<pre><code class="language-java">@Override
public String toString() {
    return &quot;Member&quot;;
}</code></pre>
<p>이처럼 제네릭, 열거 타입, 어노테이션은 서로 완전히 다른 문법처럼 보이지만 모두 프로그램의 타입 안정성을 높이고 코드의 의미를 명확하게 만드는 데 사용된다.</p>
<p>이번 글에서는 제네릭이 필요한 이유부터 제네릭 클래스와 메서드, 타입 제한, 와일드카드, 타입 소거를 살펴본다. 이어서 Enum을 이용해 제한된 값을 안전하게 관리하는 방법과 Annotation으로 코드에 메타데이터를 추가하는 방법까지 정리해보려고 한다.</p>
<hr>
<h1 id="제네릭으로-타입을-안전하게-관리하기">제네릭으로 타입을 안전하게 관리하기</h1>
<h2 id="제네릭이란">제네릭이란?</h2>
<p>제네릭은 클래스, 인터페이스, 메서드에서 사용할 타입을 외부에서 지정할 수 있도록 만드는 문법이다.</p>
<p>제네릭을 사용하면 다양한 타입의 객체를 하나의 코드로 처리하면서도 컴파일 단계에서 타입을 검사할 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();</code></pre>
<p>위 코드는 <code>names</code> 리스트가 <code>String</code> 객체만 저장하도록 제한한다.</p>
<pre><code class="language-java">names.add(&quot;Java&quot;);

// names.add(100);
// 컴파일 오류</code></pre>
<p>잘못된 타입을 저장하려고 하면 프로그램 실행 전 컴파일 단계에서 오류를 발견할 수 있다.</p>
<hr>
<h2 id="object만-사용했을-때의-문제">Object만 사용했을 때의 문제</h2>
<p>제네릭이 도입되기 전에는 다양한 객체를 저장하기 위해 <code>Object</code> 타입을 많이 사용했다.</p>
<pre><code class="language-java">public class ObjectBox {

    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}</code></pre>
<p>모든 클래스는 <code>Object</code>를 상속하므로 어떤 객체든 저장할 수 있다.</p>
<pre><code class="language-java">ObjectBox box = new ObjectBox();

box.set(&quot;Java&quot;);</code></pre>
<p>하지만 값을 꺼낼 때마다 강제 타입 변환이 필요하다.</p>
<pre><code class="language-java">String value = (String) box.get();</code></pre>
<p>실수로 잘못된 타입으로 변환하면 실행 중 <code>ClassCastException</code>이 발생한다.</p>
<pre><code class="language-java">box.set(100);

String value = (String) box.get();</code></pre>
<p>이 코드는 컴파일될 수 있지만 실행 중 오류가 발생한다.</p>
<p>제네릭을 사용하면 이러한 문제를 컴파일 단계에서 방지할 수 있다.</p>
<hr>
<h2 id="제네릭-클래스-선언">제네릭 클래스 선언</h2>
<p>클래스 이름 뒤에 <code>&lt;T&gt;</code>와 같이 타입 파라미터를 선언하면 제네릭 클래스를 만들 수 있다.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}</code></pre>
<p><code>T</code>는 실제 타입이 아니라 나중에 사용할 타입이 들어갈 자리다.</p>
<p>문자열을 저장하려면 다음과 같이 사용한다.</p>
<pre><code class="language-java">public class BoxExample {

    public static void main(String[] args) {
        Box&lt;String&gt; stringBox = new Box&lt;&gt;();

        stringBox.set(&quot;Java&quot;);

        String value = stringBox.get();

        System.out.println(value);
    }
}</code></pre>
<p>정수를 저장하는 객체도 같은 클래스로 만들 수 있다.</p>
<pre><code class="language-java">Box&lt;Integer&gt; integerBox = new Box&lt;&gt;();

integerBox.set(100);

Integer number = integerBox.get();</code></pre>
<p>하나의 <code>Box</code> 클래스를 여러 타입에 재사용하면서도 각 객체에는 지정된 타입만 저장할 수 있다.</p>
<hr>
<h2 id="타입-파라미터-이름">타입 파라미터 이름</h2>
<p>타입 파라미터의 이름은 자유롭게 작성할 수 있지만 일반적으로 한 글자의 대문자를 사용한다.</p>
<table>
<thead>
<tr>
<th>이름</th>
<th>주로 사용하는 의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>T</code></td>
<td>Type</td>
</tr>
<tr>
<td><code>E</code></td>
<td>Element</td>
</tr>
<tr>
<td><code>K</code></td>
<td>Key</td>
</tr>
<tr>
<td><code>V</code></td>
<td>Value</td>
</tr>
<tr>
<td><code>N</code></td>
<td>Number</td>
</tr>
<tr>
<td><code>R</code></td>
<td>Return 또는 Result</td>
</tr>
</tbody></table>
<p>예를 들어 <code>Map</code>은 키와 값을 사용하므로 다음과 같이 표현한다.</p>
<pre><code class="language-java">Map&lt;K, V&gt;</code></pre>
<p>실제 사용 시에는 구체적인 타입을 전달한다.</p>
<pre><code class="language-java">Map&lt;String, Integer&gt; scores = new HashMap&lt;&gt;();</code></pre>
<hr>
<h2 id="여러-개의-타입-파라미터">여러 개의 타입 파라미터</h2>
<p>하나의 제네릭 클래스에 여러 타입 파라미터를 선언할 수도 있다.</p>
<pre><code class="language-java">public class Pair&lt;K, V&gt; {

    private final K key;
    private final V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}</code></pre>
<p>사용할 때는 각 타입을 지정한다.</p>
<pre><code class="language-java">public class PairExample {

    public static void main(String[] args) {
        Pair&lt;String, Integer&gt; score =
                new Pair&lt;&gt;(&quot;김자바&quot;, 95);

        System.out.println(score.getKey());
        System.out.println(score.getValue());
    }
}</code></pre>
<hr>
<h2 id="제네릭-인터페이스">제네릭 인터페이스</h2>
<p>인터페이스도 제네릭으로 선언할 수 있다.</p>
<pre><code class="language-java">public interface Repository&lt;T&gt; {

    void save(T value);

    T findById(long id);
}</code></pre>
<p>구현 클래스에서 실제 타입을 지정한다.</p>
<pre><code class="language-java">public class MemberRepository
        implements Repository&lt;Member&gt; {

    @Override
    public void save(Member value) {
        System.out.println(
                value.getName() + &quot; 저장&quot;
        );
    }

    @Override
    public Member findById(long id) {
        return new Member(id, &quot;김자바&quot;);
    }
}</code></pre>
<p><code>Repository&lt;Member&gt;</code>로 구현했기 때문에 모든 메서드가 <code>Member</code> 타입을 기준으로 동작한다.</p>
<hr>
<h2 id="제네릭의-장점">제네릭의 장점</h2>
<p>제네릭을 사용하는 가장 큰 이유는 타입 안정성과 코드 재사용성이다.</p>
<pre><code class="language-text">제네릭을 사용하지 않는 경우

Object로 저장
→ 값을 꺼냄
→ 강제 타입 변환
→ 실행 중 오류 가능</code></pre>
<pre><code class="language-text">제네릭을 사용하는 경우

사용할 타입 지정
→ 컴파일러가 타입 검사
→ 형변환 없이 사용</code></pre>
<p>제네릭을 사용하면 다음과 같은 장점이 있다.</p>
<ul>
<li>잘못된 타입 사용을 컴파일 단계에서 차단한다.</li>
<li>값을 꺼낼 때 강제 타입 변환이 필요 없다.</li>
<li>하나의 클래스나 메서드를 여러 타입에 재사용할 수 있다.</li>
<li>코드의 의도가 더 명확해진다.</li>
</ul>
<hr>
<h1 id="제네릭-사용-시-알아야-할-규칙">제네릭 사용 시 알아야 할 규칙</h1>
<h2 id="raw-type">Raw Type</h2>
<p>제네릭 클래스에서 타입 인수를 생략한 형태를 Raw Type이라고 한다.</p>
<pre><code class="language-java">Box box = new Box();</code></pre>
<p>컴파일은 가능하지만 제네릭의 타입 검사가 제대로 적용되지 않는다.</p>
<pre><code class="language-java">box.set(&quot;Java&quot;);
box.set(100);</code></pre>
<p>서로 다른 타입의 값을 자유롭게 저장할 수 있기 때문에 타입 안정성을 잃는다.</p>
<pre><code class="language-java">String value = (String) box.get();</code></pre>
<p>저장된 값이 <code>Integer</code>라면 실행 중 오류가 발생할 수 있다.</p>
<p>따라서 특별한 이유가 없다면 Raw Type을 사용하지 않아야 한다.</p>
<pre><code class="language-java">Box&lt;String&gt; box = new Box&lt;&gt;();</code></pre>
<hr>
<h2 id="suppresswarnings">@SuppressWarnings</h2>
<p>Raw Type이나 확인되지 않은 형변환을 사용하면 컴파일러가 경고를 표시한다.</p>
<pre><code class="language-java">@SuppressWarnings(&quot;rawtypes&quot;)</code></pre>
<p>여러 경고를 동시에 억제할 수도 있다.</p>
<pre><code class="language-java">@SuppressWarnings({
        &quot;rawtypes&quot;,
        &quot;unchecked&quot;,
        &quot;unused&quot;
})</code></pre>
<p>다만 경고를 숨긴다고 코드의 문제가 해결되는 것은 아니다.</p>
<p><code>@SuppressWarnings</code>는 경고가 발생하는 이유를 정확하게 이해하고, 해당 코드가 안전하다는 것을 보장할 수 있을 때만 제한적으로 사용해야 한다.</p>
<hr>
<h2 id="기본-타입은-제네릭-타입으로-사용할-수-없다">기본 타입은 제네릭 타입으로 사용할 수 없다</h2>
<p>제네릭의 타입 인수에는 기본 타입을 직접 사용할 수 없다.</p>
<pre><code class="language-java">// Box&lt;int&gt; box;
// 사용할 수 없음</code></pre>
<p>기본 타입 대신 래퍼 클래스를 사용해야 한다.</p>
<pre><code class="language-java">Box&lt;Integer&gt; box = new Box&lt;&gt;();
Box&lt;Double&gt; doubleBox = new Box&lt;&gt;();</code></pre>
<p>컬렉션에서도 동일하다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = new ArrayList&lt;&gt;();</code></pre>
<p><code>int</code> 값은 오토박싱을 통해 <code>Integer</code> 객체로 변환되어 저장된다.</p>
<pre><code class="language-java">numbers.add(10);</code></pre>
<p>개념적으로 다음과 같은 변환이 일어난다.</p>
<pre><code class="language-java">numbers.add(Integer.valueOf(10));</code></pre>
<hr>
<h2 id="static-멤버에서-클래스-타입-파라미터를-사용할-수-없다">static 멤버에서 클래스 타입 파라미터를 사용할 수 없다</h2>
<p>클래스의 타입 파라미터는 객체를 생성할 때 결정된다.</p>
<pre><code class="language-java">Box&lt;String&gt; stringBox = new Box&lt;&gt;();
Box&lt;Integer&gt; integerBox = new Box&lt;&gt;();</code></pre>
<p>반면 <code>static</code> 멤버는 객체가 아니라 클래스에 소속된다.</p>
<p>따라서 클래스 타입 파라미터 <code>T</code>를 정적 필드나 정적 메서드에서 직접 사용할 수 없다.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {

    // 사용할 수 없음
    // private static T value;
}</code></pre>
<p>각 객체마다 <code>T</code>가 다를 수 있는데 정적 필드는 모든 객체가 하나를 공유하기 때문이다.</p>
<p>정적 메서드에서 제네릭이 필요하다면 메서드 자체에 별도의 타입 파라미터를 선언해야 한다.</p>
<pre><code class="language-java">public static &lt;T&gt; void print(T value) {
    System.out.println(value);
}</code></pre>
<hr>
<h2 id="제네릭-메서드">제네릭 메서드</h2>
<p>메서드의 반환 타입 앞에 타입 파라미터를 선언하면 제네릭 메서드를 만들 수 있다.</p>
<pre><code class="language-java">public class GenericMethodExample {

    public static &lt;T&gt; void print(T value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        print(&quot;Java&quot;);
        print(100);
        print(3.14);
    }
}</code></pre>
<p>호출할 때 전달한 인수를 기준으로 타입이 추론된다.</p>
<pre><code class="language-java">print(&quot;Java&quot;);</code></pre>
<p>위 호출에서는 <code>T</code>가 <code>String</code>으로 결정된다.</p>
<p>명시적으로 타입을 지정할 수도 있다.</p>
<pre><code class="language-java">GenericMethodExample.&lt;String&gt;print(&quot;Java&quot;);</code></pre>
<p>대부분의 경우 컴파일러가 타입을 추론하므로 직접 작성할 필요는 없다.</p>
<hr>
<h2 id="제네릭-메서드의-반환값">제네릭 메서드의 반환값</h2>
<p>제네릭 메서드는 전달받은 타입과 동일한 타입의 값을 반환할 수 있다.</p>
<pre><code class="language-java">public class GenericMethodExample {

    public static &lt;T&gt; T getFirst(T[] values) {
        if (values == null || values.length == 0) {
            return null;
        }

        return values[0];
    }

    public static void main(String[] args) {
        String[] names = {&quot;Java&quot;, &quot;Spring&quot;};
        Integer[] numbers = {10, 20, 30};

        String firstName = getFirst(names);
        Integer firstNumber = getFirst(numbers);

        System.out.println(firstName);
        System.out.println(firstNumber);
    }
}</code></pre>
<hr>
<h1 id="타입의-범위를-제한하기">타입의 범위를 제한하기</h1>
<h2 id="한정-타입-파라미터">한정 타입 파라미터</h2>
<p>모든 타입을 허용하지 않고 특정 타입과 그 하위 타입만 사용하도록 제한할 수 있다.</p>
<pre><code class="language-java">&lt;T extends Number&gt;</code></pre>
<p>위 선언은 <code>T</code>가 <code>Number</code>이거나 <code>Number</code>의 하위 타입이어야 한다는 뜻이다.</p>
<pre><code class="language-java">public class NumberCalculator {

    public static &lt;T extends Number&gt;
    double doubleValue(T number) {
        return number.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println(doubleValue(10));
        System.out.println(doubleValue(3.14));

        // doubleValue(&quot;Java&quot;);
        // 컴파일 오류
    }
}</code></pre>
<p><code>Integer</code>, <code>Double</code>, <code>Long</code> 등은 <code>Number</code>를 상속하므로 사용할 수 있다.</p>
<hr>
<h2 id="인터페이스로-타입-제한하기">인터페이스로 타입 제한하기</h2>
<p>클래스뿐 아니라 인터페이스를 기준으로도 제한할 수 있다.</p>
<pre><code class="language-java">&lt;T extends Comparable&lt;T&gt;&gt;</code></pre>
<p>여기서 <code>extends</code>는 클래스 상속뿐 아니라 인터페이스 구현까지 포함하는 의미로 사용된다.</p>
<pre><code class="language-java">public static &lt;T extends Comparable&lt;T&gt;&gt;
T max(T first, T second) {
    if (first.compareTo(second) &gt;= 0) {
        return first;
    }

    return second;
}</code></pre>
<pre><code class="language-java">public class MaxExample {

    public static &lt;T extends Comparable&lt;T&gt;&gt;
    T max(T first, T second) {
        return first.compareTo(second) &gt;= 0
                ? first
                : second;
    }

    public static void main(String[] args) {
        System.out.println(max(10, 20));
        System.out.println(max(&quot;Java&quot;, &quot;Spring&quot;));
    }
}</code></pre>
<hr>
<h2 id="여러-제한-조건">여러 제한 조건</h2>
<p>여러 타입 제한을 동시에 적용할 수 있다.</p>
<pre><code class="language-java">&lt;T extends Parent &amp; InterfaceA &amp; InterfaceB&gt;</code></pre>
<p>클래스 제한이 있다면 가장 앞에 한 번만 작성해야 한다.</p>
<pre><code class="language-java">&lt;T extends Number &amp; Comparable&lt;T&gt;&gt;</code></pre>
<p>클래스는 하나만 사용할 수 있지만 인터페이스는 여러 개 지정할 수 있다.</p>
<hr>
<h1 id="타입-소거와-제네릭의-한계">타입 소거와 제네릭의 한계</h1>
<h2 id="타입-소거란">타입 소거란?</h2>
<p>Java의 제네릭 타입 정보는 주로 컴파일 단계에서 타입 검사를 위해 사용된다.</p>
<p>컴파일러는 제네릭 코드를 검사하고 필요한 형변환 등을 추가한 뒤 일부 타입 정보를 제거한다. 이를 <strong>타입 소거(Type Erasure)</strong> 라고 한다.</p>
<p>예를 들어 다음 코드가 있다고 가정해보자.</p>
<pre><code class="language-java">Box&lt;String&gt; stringBox = new Box&lt;&gt;();
Box&lt;Integer&gt; integerBox = new Box&lt;&gt;();</code></pre>
<p>런타임에는 <code>Box&lt;String&gt;</code>과 <code>Box&lt;Integer&gt;</code>가 완전히 별개의 클래스 파일로 생성되는 것이 아니다.</p>
<p>기본적으로 같은 <code>Box</code> 클래스를 공유한다.</p>
<pre><code class="language-java">System.out.println(
        stringBox.getClass()
                == integerBox.getClass()
);</code></pre>
<p>결과는 <code>true</code>다.</p>
<hr>
<h2 id="타입-소거-후의-형태">타입 소거 후의 형태</h2>
<p>다음과 같은 제네릭 클래스가 있다고 가정해보자.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {

    private T value;

    public T get() {
        return value;
    }
}</code></pre>
<p>타입 파라미터에 제한이 없다면 컴파일 이후 개념적으로 <code>Object</code>를 기준으로 처리된다.</p>
<pre><code class="language-java">public class Box {

    private Object value;

    public Object get() {
        return value;
    }
}</code></pre>
<p>한정 타입이 있다면 해당 상한 타입이 기준이 된다.</p>
<pre><code class="language-java">public class NumberBox&lt;T extends Number&gt; {
}</code></pre>
<p>이 경우 <code>T</code>는 소거 후 <code>Number</code>를 기준으로 처리된다.</p>
<hr>
<h2 id="new-t를-사용할-수-없는-이유">new T()를 사용할 수 없는 이유</h2>
<p>다음 코드는 사용할 수 없다.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {

    public T create() {
        // return new T();
        // 컴파일 오류
    }
}</code></pre>
<p>런타임에는 <code>T</code>가 어떤 구체적인 타입인지 알 수 없기 때문이다.</p>
<p>객체 생성이 필요하다면 생성자를 외부에서 전달받는 방법을 사용할 수 있다.</p>
<pre><code class="language-java">import java.util.function.Supplier;

public class Factory&lt;T&gt; {

    private final Supplier&lt;T&gt; supplier;

    public Factory(Supplier&lt;T&gt; supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}</code></pre>
<pre><code class="language-java">public class FactoryExample {

    public static void main(String[] args) {
        Factory&lt;Member&gt; factory =
                new Factory&lt;&gt;(Member::new);

        Member member = factory.create();

        System.out.println(member);
    }
}</code></pre>
<hr>
<h2 id="instanceof-t를-사용할-수-없는-이유">instanceof T를 사용할 수 없는 이유</h2>
<p>다음 코드 역시 사용할 수 없다.</p>
<pre><code class="language-java">public boolean check(Object value) {
    // return value instanceof T;
}</code></pre>
<p><code>instanceof</code>는 런타임에 실제 타입을 검사하지만, 런타임에는 <code>T</code>의 구체적인 타입 정보가 소거되기 때문이다.</p>
<p>필요하다면 <code>Class&lt;T&gt;</code> 객체를 전달받을 수 있다.</p>
<pre><code class="language-java">public class TypeChecker&lt;T&gt; {

    private final Class&lt;T&gt; type;

    public TypeChecker(Class&lt;T&gt; type) {
        this.type = type;
    }

    public boolean check(Object value) {
        return type.isInstance(value);
    }
}</code></pre>
<pre><code class="language-java">TypeChecker&lt;String&gt; checker =
        new TypeChecker&lt;&gt;(String.class);

System.out.println(checker.check(&quot;Java&quot;));
System.out.println(checker.check(100));</code></pre>
<hr>
<h2 id="제네릭-배열-생성">제네릭 배열 생성</h2>
<p>제네릭 타입의 배열을 직접 생성할 수 없다.</p>
<pre><code class="language-java">// T[] values = new T[10];
// 컴파일 오류</code></pre>
<p>다음과 같은 배열도 생성할 수 없다.</p>
<pre><code class="language-java">// List&lt;String&gt;[] lists = new List&lt;String&gt;[10];</code></pre>
<p>배열은 런타임에 자신의 요소 타입을 확인하지만 제네릭 타입 정보는 타입 소거로 인해 충분히 유지되지 않기 때문이다.</p>
<p>일반적으로 제네릭 데이터는 배열보다 <code>List&lt;T&gt;</code>와 같은 컬렉션으로 관리한다.</p>
<pre><code class="language-java">List&lt;T&gt; values = new ArrayList&lt;&gt;();</code></pre>
<hr>
<h1 id="와일드카드로-제네릭의-범위-넓히기">와일드카드로 제네릭의 범위 넓히기</h1>
<h2 id="제네릭은-서로-다른-타입이다">제네릭은 서로 다른 타입이다</h2>
<p>다음과 같은 클래스 관계가 있다고 가정해보자.</p>
<pre><code class="language-java">public class Animal {
}

public class Dog extends Animal {
}</code></pre>
<p><code>Dog</code>는 <code>Animal</code>의 자식이지만 다음 두 리스트는 서로 상속 관계가 아니다.</p>
<pre><code class="language-java">List&lt;Dog&gt;
List&lt;Animal&gt;</code></pre>
<p>따라서 다음 대입은 불가능하다.</p>
<pre><code class="language-java">List&lt;Dog&gt; dogs = new ArrayList&lt;&gt;();

// List&lt;Animal&gt; animals = dogs;
// 컴파일 오류</code></pre>
<p>만약 허용된다면 <code>animals</code>를 통해 <code>Cat</code>을 추가할 수 있게 되고, 원래 <code>List&lt;Dog&gt;</code>였던 리스트의 타입 안정성이 깨질 수 있기 때문이다.</p>
<p>여러 제네릭 타입을 유연하게 처리하려면 와일드카드를 사용한다.</p>
<hr>
<h2 id="제한-없는-와일드카드">제한 없는 와일드카드</h2>
<p><code>&lt;?&gt;</code>는 구체적인 타입을 알 수 없는 제네릭 타입을 의미한다.</p>
<pre><code class="language-java">List&lt;?&gt; values;</code></pre>
<p><code>List&lt;String&gt;</code>, <code>List&lt;Integer&gt;</code>, <code>List&lt;Member&gt;</code> 등을 모두 참조할 수 있다.</p>
<pre><code class="language-java">public static void printAll(List&lt;?&gt; values) {
    for (Object value : values) {
        System.out.println(value);
    }
}</code></pre>
<pre><code class="language-java">printAll(List.of(&quot;Java&quot;, &quot;Spring&quot;));
printAll(List.of(10, 20, 30));</code></pre>
<p>어떤 타입인지 알 수 없기 때문에 값을 꺼낼 때는 <code>Object</code>로만 안전하게 받을 수 있다.</p>
<pre><code class="language-java">Object value = values.get(0);</code></pre>
<p>새로운 값을 추가하는 것은 거의 불가능하다.</p>
<pre><code class="language-java">// values.add(&quot;Java&quot;);
// values.add(100);</code></pre>
<p>실제 리스트가 어떤 타입인지 모르기 때문이다.</p>
<p><code>null</code>은 모든 참조 타입에 저장할 수 있으므로 추가할 수 있다.</p>
<pre><code class="language-java">values.add(null);</code></pre>
<p>다만 <code>List.of()</code>처럼 수정 불가능한 리스트라면 <code>null</code> 추가 여부와 관계없이 변경 자체가 불가능하다.</p>
<hr>
<h2 id="상한-제한-와일드카드">상한 제한 와일드카드</h2>
<p><code>&lt;? extends T&gt;</code>는 <code>T</code> 또는 <code>T</code>의 하위 타입을 의미한다.</p>
<pre><code class="language-java">List&lt;? extends Animal&gt;</code></pre>
<p>다음 타입들을 참조할 수 있다.</p>
<pre><code class="language-text">List&lt;Animal&gt;
List&lt;Dog&gt;
List&lt;Cat&gt;</code></pre>
<p>값을 꺼낼 때는 최소한 <code>Animal</code>이라는 사실이 보장된다.</p>
<pre><code class="language-java">public static void makeSounds(
        List&lt;? extends Animal&gt; animals
) {
    for (Animal animal : animals) {
        animal.sound();
    }
}</code></pre>
<p>하지만 구체적인 리스트 타입을 알 수 없기 때문에 새로운 객체를 추가할 수 없다.</p>
<pre><code class="language-java">// animals.add(new Dog());
// 컴파일 오류</code></pre>
<p>실제 리스트가 <code>List&lt;Cat&gt;</code>일 수도 있기 때문이다.</p>
<p>상한 제한 와일드카드는 데이터를 주로 읽는 경우에 적합하다.</p>
<hr>
<h2 id="하한-제한-와일드카드">하한 제한 와일드카드</h2>
<p><code>&lt;? super T&gt;</code>는 <code>T</code> 또는 <code>T</code>의 부모 타입을 의미한다.</p>
<pre><code class="language-java">List&lt;? super Dog&gt;</code></pre>
<p>다음 타입들을 참조할 수 있다.</p>
<pre><code class="language-text">List&lt;Dog&gt;
List&lt;Animal&gt;
List&lt;Object&gt;</code></pre>
<p><code>Dog</code> 객체는 안전하게 추가할 수 있다.</p>
<pre><code class="language-java">public static void addDog(
        List&lt;? super Dog&gt; values
) {
    values.add(new Dog());
}</code></pre>
<p><code>Dog</code>의 자식 객체가 있다면 그 객체도 추가할 수 있다.</p>
<p>하지만 꺼낸 값의 정확한 타입은 알 수 없다.</p>
<pre><code class="language-java">Object value = values.get(0);</code></pre>
<p>실제 리스트가 <code>List&lt;Object&gt;</code>일 수도 있기 때문에 <code>Dog</code> 타입이라고 보장할 수 없다.</p>
<hr>
<h2 id="pecs-원칙">PECS 원칙</h2>
<p>와일드카드를 기억할 때 자주 사용하는 원칙이 <strong>PECS</strong>다.</p>
<pre><code class="language-text">Producer Extends
Consumer Super</code></pre>
<p>데이터를 제공하고 꺼내는 생산자라면 <code>extends</code>를 사용한다.</p>
<pre><code class="language-java">List&lt;? extends Animal&gt;</code></pre>
<p>데이터를 전달받아 저장하는 소비자라면 <code>super</code>를 사용한다.</p>
<pre><code class="language-java">List&lt;? super Dog&gt;</code></pre>
<p>다음 복사 메서드를 보면 PECS를 이해하기 쉽다.</p>
<pre><code class="language-java">public static &lt;T&gt; void copy(
        List&lt;? extends T&gt; source,
        List&lt;? super T&gt; destination
) {
    for (T value : source) {
        destination.add(value);
    }
}</code></pre>
<p><code>source</code>는 값을 꺼내는 생산자이므로 <code>extends</code>를 사용하고, <code>destination</code>은 값을 넣는 소비자이므로 <code>super</code>를 사용한다.</p>
<pre><code class="language-java">public class CopyExample {

    public static void main(String[] args) {
        List&lt;Integer&gt; source =
                List.of(10, 20, 30);

        List&lt;Number&gt; destination =
                new ArrayList&lt;&gt;();

        copy(source, destination);

        System.out.println(destination);
    }

    public static &lt;T&gt; void copy(
            List&lt;? extends T&gt; source,
            List&lt;? super T&gt; destination
    ) {
        for (T value : source) {
            destination.add(value);
        }
    }
}</code></pre>
<hr>
<h2 id="와일드카드-비교">와일드카드 비교</h2>
<table>
<thead>
<tr>
<th>문법</th>
<th>의미</th>
<th>안전하게 꺼내는 타입</th>
<th>추가 가능한 값</th>
</tr>
</thead>
<tbody><tr>
<td><code>List&lt;?&gt;</code></td>
<td>알 수 없는 타입</td>
<td><code>Object</code></td>
<td><code>null</code> 정도</td>
</tr>
<tr>
<td><code>List&lt;? extends Animal&gt;</code></td>
<td>Animal 또는 하위 타입</td>
<td><code>Animal</code></td>
<td><code>null</code> 정도</td>
</tr>
<tr>
<td><code>List&lt;? super Dog&gt;</code></td>
<td>Dog 또는 상위 타입</td>
<td><code>Object</code></td>
<td><code>Dog</code>와 그 하위 타입</td>
</tr>
</tbody></table>
<hr>
<h1 id="enum으로-제한된-값을-안전하게-관리하기">Enum으로 제한된 값을 안전하게 관리하기</h1>
<h2 id="열거-타입이란">열거 타입이란?</h2>
<p>열거 타입은 변수가 가질 수 있는 값을 미리 정해 놓는 타입이다.</p>
<p>회원 등급이 다음 세 가지로만 구성된다고 생각해보자.</p>
<pre><code class="language-text">GOLD
SILVER
BRONZE</code></pre>
<p>문자열로 관리하면 다음과 같은 문제가 생길 수 있다.</p>
<pre><code class="language-java">String grade = &quot;GLOD&quot;;</code></pre>
<p>오타가 있어도 단순 문자열이기 때문에 컴파일러가 문제를 발견하지 못한다.</p>
<p>상수로 관리할 수도 있다.</p>
<pre><code class="language-java">public static final String GOLD = &quot;GOLD&quot;;
public static final String SILVER = &quot;SILVER&quot;;</code></pre>
<p>하지만 문자열이기 때문에 다른 의미의 문자열도 저장할 수 있다.</p>
<pre><code class="language-java">String grade = &quot;ADMIN&quot;;</code></pre>
<p>Enum을 사용하면 허용된 값만 저장하도록 제한할 수 있다.</p>
<hr>
<h2 id="enum-선언">Enum 선언</h2>
<p>열거 타입은 <code>enum</code> 키워드를 사용한다.</p>
<pre><code class="language-java">public enum Grade {
    GOLD,
    SILVER,
    BRONZE
}</code></pre>
<p>Enum 내부에 선언된 각각의 값을 열거 상수라고 한다.</p>
<p>열거 상수는 일반적인 상수 이름 규칙에 따라 대문자로 작성하고, 여러 단어로 구성되면 언더바를 사용한다.</p>
<pre><code class="language-java">public enum OrderStatus {
    PAYMENT_PENDING,
    PAID,
    SHIPPING,
    DELIVERED,
    CANCELED
}</code></pre>
<hr>
<h2 id="enum-사용">Enum 사용</h2>
<p>열거 타입 변수에는 해당 Enum에 선언된 상수와 <code>null</code>만 저장할 수 있다.</p>
<pre><code class="language-java">Grade grade = Grade.GOLD;</code></pre>
<p>다른 Enum 타입이나 임의의 문자열을 저장할 수 없다.</p>
<pre><code class="language-java">// Grade grade = &quot;GOLD&quot;;
// 컴파일 오류</code></pre>
<p>비교할 때는 <code>==</code>를 사용할 수 있다.</p>
<pre><code class="language-java">if (grade == Grade.GOLD) {
    System.out.println(&quot;골드 회원입니다.&quot;);
}</code></pre>
<p>각 열거 상수는 하나의 고정된 객체이므로 <code>==</code> 비교가 안전하다.</p>
<hr>
<h2 id="switch와-enum">switch와 Enum</h2>
<p>Enum은 <code>switch</code>문과 잘 어울린다.</p>
<pre><code class="language-java">public class GradeExample {

    public static void main(String[] args) {
        Grade grade = Grade.GOLD;

        String benefit = switch (grade) {
            case GOLD -&gt; &quot;10% 할인&quot;;
            case SILVER -&gt; &quot;5% 할인&quot;;
            case BRONZE -&gt; &quot;적립금 제공&quot;;
        };

        System.out.println(benefit);
    }
}</code></pre>
<p>모든 열거 상수를 처리하면 <code>default</code>를 생략할 수 있다.</p>
<p>새로운 Enum 상수가 추가되었을 때 처리하지 않은 부분을 컴파일러가 알려줄 수 있다는 장점도 있다.</p>
<hr>
<h2 id="enum은-클래스다">Enum은 클래스다</h2>
<p>Enum은 단순한 상수 모음이 아니다.</p>
<p>내부적으로 <code>java.lang.Enum</code>을 상속하는 특별한 클래스다.</p>
<p>따라서 다음 요소를 가질 수 있다.</p>
<ul>
<li>필드</li>
<li>생성자</li>
<li>메서드</li>
<li>추상 메서드</li>
<li>인터페이스 구현</li>
</ul>
<p>회원 등급마다 할인율을 가지도록 만들 수 있다.</p>
<pre><code class="language-java">public enum Grade {

    GOLD(0.10),
    SILVER(0.05),
    BRONZE(0.01);

    private final double discountRate;

    Grade(double discountRate) {
        this.discountRate = discountRate;
    }

    public int calculateDiscount(int price) {
        return (int) (price * discountRate);
    }

    public double getDiscountRate() {
        return discountRate;
    }
}</code></pre>
<pre><code class="language-java">public class GradeExample {

    public static void main(String[] args) {
        Grade grade = Grade.GOLD;

        int discount =
                grade.calculateDiscount(100_000);

        System.out.println(discount);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">10000</code></pre>
<p>Enum이 자신의 데이터와 동작을 직접 가지기 때문에 등급별 조건문을 외부에 반복해서 작성하지 않아도 된다.</p>
<hr>
<h2 id="enum-생성자">Enum 생성자</h2>
<p>Enum 생성자는 외부에서 직접 호출할 수 없다.</p>
<pre><code class="language-java">// Grade grade = new Grade();
// 사용할 수 없음</code></pre>
<p>열거 상수를 선언할 때 내부적으로 생성자가 호출된다.</p>
<pre><code class="language-java">GOLD(0.10)</code></pre>
<p>Enum 생성자는 명시적으로 <code>public</code>이나 <code>protected</code>로 선언할 수 없으며 외부에서 객체를 추가로 생성할 수 없다.</p>
<hr>
<h2 id="values">values()</h2>
<p><code>values()</code>는 Enum에 선언된 모든 상수를 배열로 반환한다.</p>
<pre><code class="language-java">public class EnumMethodExample {

    public static void main(String[] args) {
        for (Grade grade : Grade.values()) {
            System.out.println(grade);
        }
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">GOLD
SILVER
BRONZE</code></pre>
<hr>
<h2 id="valueof">valueOf()</h2>
<p><code>valueOf()</code>는 문자열과 이름이 일치하는 열거 상수를 반환한다.</p>
<pre><code class="language-java">Grade grade = Grade.valueOf(&quot;GOLD&quot;);

System.out.println(grade);</code></pre>
<p>이름이 일치하지 않으면 <code>IllegalArgumentException</code>이 발생한다.</p>
<pre><code class="language-java">// Grade.valueOf(&quot;gold&quot;);
// 대소문자가 달라 오류 발생</code></pre>
<p>외부 입력을 Enum으로 변환할 때는 예외 처리가 필요할 수 있다.</p>
<hr>
<h2 id="name과-ordinal">name()과 ordinal()</h2>
<p><code>name()</code>은 열거 상수의 이름을 문자열로 반환한다.</p>
<pre><code class="language-java">System.out.println(Grade.GOLD.name());</code></pre>
<p>결과는 <code>&quot;GOLD&quot;</code>다.</p>
<p><code>ordinal()</code>은 열거 상수가 선언된 순서를 반환한다.</p>
<pre><code class="language-java">System.out.println(Grade.GOLD.ordinal());
System.out.println(Grade.SILVER.ordinal());</code></pre>
<p>결과는 각각 <code>0</code>, <code>1</code>이다.</p>
<p>하지만 <code>ordinal()</code> 값을 데이터베이스 저장값이나 업무 로직에 사용하는 것은 피해야 한다.</p>
<p>Enum 선언 순서가 바뀌면 값도 함께 달라지기 때문이다.</p>
<pre><code class="language-text">GOLD = 0
SILVER = 1</code></pre>
<p>중간에 새로운 상수가 추가되면 기존 값의 의미가 달라질 수 있다.</p>
<p>저장용 코드가 필요하다면 별도의 필드를 선언하는 것이 안전하다.</p>
<pre><code class="language-java">public enum Grade {

    GOLD(&quot;G&quot;),
    SILVER(&quot;S&quot;),
    BRONZE(&quot;B&quot;);

    private final String code;

    Grade(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}</code></pre>
<hr>
<h2 id="enum에-추상-메서드-선언하기">Enum에 추상 메서드 선언하기</h2>
<p>열거 상수마다 서로 다른 동작을 구현할 수도 있다.</p>
<pre><code class="language-java">public enum Operation {

    PLUS {
        @Override
        public int calculate(int left, int right) {
            return left + right;
        }
    },

    MINUS {
        @Override
        public int calculate(int left, int right) {
            return left - right;
        }
    },

    MULTIPLY {
        @Override
        public int calculate(int left, int right) {
            return left * right;
        }
    };

    public abstract int calculate(
            int left,
            int right
    );
}</code></pre>
<pre><code class="language-java">public class OperationExample {

    public static void main(String[] args) {
        int result =
                Operation.PLUS.calculate(10, 20);

        System.out.println(result);
    }
}</code></pre>
<p>이 방식은 Enum 상수별로 동작이 확실히 구분될 때 사용할 수 있다.</p>
<hr>
<h2 id="enum이-문자열보다-좋은-이유">Enum이 문자열보다 좋은 이유</h2>
<table>
<thead>
<tr>
<th>문자열</th>
<th>Enum</th>
</tr>
</thead>
<tbody><tr>
<td>임의의 값 저장 가능</td>
<td>선언된 값만 저장 가능</td>
</tr>
<tr>
<td>오타를 컴파일러가 찾기 어려움</td>
<td>잘못된 상수는 컴파일 오류</td>
</tr>
<tr>
<td>관련 기능을 외부에 작성</td>
<td>필드와 메서드를 Enum 내부에 작성 가능</td>
</tr>
<tr>
<td>타입 구분이 약함</td>
<td>서로 다른 Enum 타입을 구분</td>
</tr>
<tr>
<td>리팩터링이 어려울 수 있음</td>
<td>IDE 리팩터링 지원이 좋음</td>
</tr>
</tbody></table>
<hr>
<h1 id="annotation으로-코드에-정보-추가하기">Annotation으로 코드에 정보 추가하기</h1>
<h2 id="annotation이란">Annotation이란?</h2>
<p>어노테이션은 소스 코드에 추가하는 메타데이터다.</p>
<p>일반 주석은 사람이 읽기 위한 설명이지만, 어노테이션은 컴파일러, JVM, 라이브러리, 프레임워크 등이 읽고 활용할 수 있다.</p>
<pre><code class="language-java">@Override
public String toString() {
    return &quot;Member&quot;;
}</code></pre>
<p><code>@Override</code>는 해당 메서드가 부모 클래스의 메서드를 재정의한 것임을 컴파일러에게 알려준다.</p>
<p>어노테이션은 코드의 실제 실행 내용을 직접 작성하기보다 코드에 대한 추가 정보나 처리 규칙을 전달하는 역할을 한다.</p>
<hr>
<h2 id="java의-기본-어노테이션">Java의 기본 어노테이션</h2>
<p>Java에서 자주 사용하는 기본 어노테이션은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Override</code></td>
<td>오버라이딩 메서드임을 컴파일러에 알림</td>
</tr>
<tr>
<td><code>@Deprecated</code></td>
<td>사용을 권장하지 않는 요소 표시</td>
</tr>
<tr>
<td><code>@SuppressWarnings</code></td>
<td>특정 컴파일 경고 억제</td>
</tr>
<tr>
<td><code>@FunctionalInterface</code></td>
<td>함수형 인터페이스임을 검증</td>
</tr>
<tr>
<td><code>@SafeVarargs</code></td>
<td>가변 인자와 제네릭 관련 경고가 안전함을 표시</td>
</tr>
</tbody></table>
<hr>
<h2 id="override">@Override</h2>
<p>부모 클래스나 인터페이스의 메서드를 재정의했음을 나타낸다.</p>
<pre><code class="language-java">public class Member {

    @Override
    public String toString() {
        return &quot;Member&quot;;
    }
}</code></pre>
<p>메서드 이름이나 매개변수를 잘못 작성하면 컴파일러가 오류를 알려준다.</p>
<pre><code class="language-java">@Override
public String toStrings() {
    return &quot;Member&quot;;
}</code></pre>
<p><code>Object</code>에 <code>toStrings()</code>라는 메서드가 없으므로 컴파일 오류가 발생한다.</p>
<p>오버라이딩할 때는 항상 <code>@Override</code>를 작성하는 것이 좋다.</p>
<hr>
<h2 id="deprecated">@Deprecated</h2>
<p>더 이상 사용을 권장하지 않는 클래스, 생성자, 메서드 등에 표시한다.</p>
<pre><code class="language-java">public class MemberService {

    @Deprecated
    public void oldRegister() {
        System.out.println(&quot;이전 등록 방식&quot;);
    }

    public void register() {
        System.out.println(&quot;새로운 등록 방식&quot;);
    }
}</code></pre>
<p>기존 코드와의 호환성을 위해 기능을 바로 삭제할 수 없지만 새로운 코드에서는 사용하지 않도록 안내할 때 사용한다.</p>
<p>문서화를 더 명확하게 하기 위해 Javadoc의 <code>@deprecated</code> 태그와 함께 사용할 수 있다.</p>
<pre><code class="language-java">/**
 * @deprecated register()를 사용하세요.
 */
@Deprecated
public void oldRegister() {
}</code></pre>
<p>Java 9부터는 추가 속성도 지정할 수 있다.</p>
<pre><code class="language-java">@Deprecated(
        since = &quot;2.0&quot;,
        forRemoval = true
)
public void oldRegister() {
}</code></pre>
<p><code>since</code>는 사용 중단이 시작된 버전을 나타내고, <code>forRemoval</code>은 향후 제거 예정 여부를 나타낸다.</p>
<hr>
<h2 id="suppresswarnings-1">@SuppressWarnings</h2>
<p>컴파일러가 발생시키는 특정 경고를 억제한다.</p>
<pre><code class="language-java">@SuppressWarnings(&quot;unused&quot;)
private void test() {
    int number = 10;
}</code></pre>
<p>여러 경고를 함께 억제할 수 있다.</p>
<pre><code class="language-java">@SuppressWarnings({
        &quot;rawtypes&quot;,
        &quot;unchecked&quot;
})
public void legacyCode() {
}</code></pre>
<p>경고를 단순히 숨기기 위한 용도로 남발하면 실제 문제를 놓칠 수 있다. 가능한 한 경고의 원인을 해결하고, 의도적으로 안전하다고 판단한 범위에만 적용해야 한다.</p>
<hr>
<h2 id="functionalinterface">@FunctionalInterface</h2>
<p>추상 메서드를 하나만 가지는 함수형 인터페이스에 사용한다.</p>
<pre><code class="language-java">@FunctionalInterface
public interface Calculator {

    int calculate(int left, int right);
}</code></pre>
<p>함수형 인터페이스는 람다 표현식의 대상이 될 수 있다.</p>
<pre><code class="language-java">Calculator add =
        (left, right) -&gt; left + right;

System.out.println(add.calculate(10, 20));</code></pre>
<p><code>@FunctionalInterface</code>를 작성한 인터페이스에 추상 메서드를 두 개 이상 선언하면 컴파일 오류가 발생한다.</p>
<pre><code class="language-java">@FunctionalInterface
public interface Calculator {

    int calculate(int left, int right);

    // void print();
    // 추가하면 컴파일 오류
}</code></pre>
<hr>
<h1 id="사용자-정의-annotation-만들기">사용자 정의 Annotation 만들기</h1>
<h2 id="어노테이션-선언">어노테이션 선언</h2>
<p>사용자 정의 어노테이션은 <code>@interface</code>를 사용해 선언한다.</p>
<pre><code class="language-java">public @interface Author {
}</code></pre>
<p>인터페이스 선언과 비슷하지만 앞에 <code>@</code>가 붙는다.</p>
<p>사용할 때는 다음과 같이 작성한다.</p>
<pre><code class="language-java">@Author
public class MemberService {
}</code></pre>
<hr>
<h2 id="어노테이션-속성">어노테이션 속성</h2>
<p>어노테이션 내부에는 추상 메서드와 비슷한 형태로 속성을 선언한다.</p>
<pre><code class="language-java">public @interface Author {

    String name();

    String date();
}</code></pre>
<p>사용할 때는 <code>속성이름 = 값</code> 형태로 작성한다.</p>
<pre><code class="language-java">@Author(
        name = &quot;김자바&quot;,
        date = &quot;2026-07-28&quot;
)
public class MemberService {
}</code></pre>
<p>어노테이션 속성에는 반환 타입만 작성하며 매개변수는 선언할 수 없다.</p>
<hr>
<h2 id="기본값-지정">기본값 지정</h2>
<p><code>default</code> 키워드로 속성의 기본값을 지정할 수 있다.</p>
<pre><code class="language-java">public @interface Author {

    String name();

    String date() default &quot;미작성&quot;;
}</code></pre>
<p>기본값이 있는 속성은 사용 시 생략할 수 있다.</p>
<pre><code class="language-java">@Author(name = &quot;김자바&quot;)
public class MemberService {
}</code></pre>
<hr>
<h2 id="value-속성">value 속성</h2>
<p>속성 이름이 <code>value</code>이고 다른 필수 속성이 없다면 이름을 생략할 수 있다.</p>
<pre><code class="language-java">public @interface Description {

    String value();
}</code></pre>
<p>다음 두 사용법은 같다.</p>
<pre><code class="language-java">@Description(value = &quot;회원 서비스&quot;)</code></pre>
<pre><code class="language-java">@Description(&quot;회원 서비스&quot;)</code></pre>
<p>Spring의 많은 어노테이션에서 <code>value</code> 속성 이름을 생략하는 형태를 볼 수 있다.</p>
<pre><code class="language-java">@RequestMapping(&quot;/members&quot;)</code></pre>
<hr>
<h2 id="어노테이션-속성에서-사용할-수-있는-타입">어노테이션 속성에서 사용할 수 있는 타입</h2>
<p>어노테이션 속성에는 제한된 타입만 사용할 수 있다.</p>
<p>대표적으로 다음 타입을 사용할 수 있다.</p>
<ul>
<li>기본 타입</li>
<li><code>String</code></li>
<li><code>Class</code></li>
<li>Enum</li>
<li>다른 Annotation</li>
<li>위 타입들의 배열</li>
</ul>
<pre><code class="language-java">public @interface ApiInfo {

    String name();

    int version();

    Class&lt;?&gt; responseType();

    HttpMethod method();

    String[] tags();
}</code></pre>
<p>다음과 같은 일반 객체 타입은 속성으로 사용할 수 없다.</p>
<pre><code class="language-java">// Member member();
// 사용할 수 없음</code></pre>
<hr>
<h1 id="메타-어노테이션">메타 어노테이션</h1>
<h2 id="메타-어노테이션이란">메타 어노테이션이란?</h2>
<p>메타 어노테이션은 다른 어노테이션의 적용 방법을 설정하는 어노테이션이다.</p>
<p>대표적인 메타 어노테이션은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>메타 어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Target</code></td>
<td>어노테이션을 적용할 수 있는 위치 지정</td>
</tr>
<tr>
<td><code>@Retention</code></td>
<td>어노테이션 정보를 유지할 기간 지정</td>
</tr>
<tr>
<td><code>@Documented</code></td>
<td>Javadoc에 어노테이션 정보 포함</td>
</tr>
<tr>
<td><code>@Inherited</code></td>
<td>자식 클래스에 어노테이션 상속</td>
</tr>
<tr>
<td><code>@Repeatable</code></td>
<td>같은 어노테이션 반복 사용 허용</td>
</tr>
</tbody></table>
<hr>
<h2 id="target">@Target</h2>
<p>어노테이션을 적용할 수 있는 위치를 지정한다.</p>
<pre><code class="language-java">import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
public @interface LogExecution {
}</code></pre>
<p>위 어노테이션은 메서드에만 사용할 수 있다.</p>
<pre><code class="language-java">@LogExecution
public void save() {
}</code></pre>
<p>여러 위치를 허용할 수도 있다.</p>
<pre><code class="language-java">@Target({
        ElementType.TYPE,
        ElementType.METHOD
})
public @interface Audit {
}</code></pre>
<p>자주 사용하는 <code>ElementType</code>은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>값</th>
<th>적용 대상</th>
</tr>
</thead>
<tbody><tr>
<td><code>TYPE</code></td>
<td>클래스, 인터페이스, Enum</td>
</tr>
<tr>
<td><code>FIELD</code></td>
<td>필드</td>
</tr>
<tr>
<td><code>METHOD</code></td>
<td>메서드</td>
</tr>
<tr>
<td><code>PARAMETER</code></td>
<td>매개변수</td>
</tr>
<tr>
<td><code>CONSTRUCTOR</code></td>
<td>생성자</td>
</tr>
<tr>
<td><code>LOCAL_VARIABLE</code></td>
<td>지역 변수</td>
</tr>
<tr>
<td><code>ANNOTATION_TYPE</code></td>
<td>어노테이션</td>
</tr>
<tr>
<td><code>TYPE_USE</code></td>
<td>타입이 사용되는 위치</td>
</tr>
</tbody></table>
<hr>
<h2 id="retention">@Retention</h2>
<p>어노테이션 정보를 언제까지 유지할지 지정한다.</p>
<pre><code class="language-java">import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
}</code></pre>
<p>유지 정책은 다음 세 가지다.</p>
<table>
<thead>
<tr>
<th>정책</th>
<th>유지 범위</th>
</tr>
</thead>
<tbody><tr>
<td><code>SOURCE</code></td>
<td>소스 코드까지만 유지</td>
</tr>
<tr>
<td><code>CLASS</code></td>
<td>클래스 파일까지 유지, 런타임에는 보장되지 않음</td>
</tr>
<tr>
<td><code>RUNTIME</code></td>
<td>런타임까지 유지하여 Reflection으로 조회 가능</td>
</tr>
</tbody></table>
<p>컴파일러만 사용하는 어노테이션은 <code>SOURCE</code>가 적절할 수 있다.</p>
<p>프레임워크가 런타임에 어노테이션을 읽어야 한다면 <code>RUNTIME</code>을 사용한다.</p>
<p>Spring의 많은 어노테이션은 런타임에 Reflection으로 분석되므로 <code>RUNTIME</code> 정책을 사용한다.</p>
<hr>
<h2 id="documented">@Documented</h2>
<p><code>@Documented</code>가 적용된 어노테이션은 Javadoc 생성 시 문서에 포함될 수 있다.</p>
<pre><code class="language-java">import java.lang.annotation.Documented;

@Documented
public @interface ApiDescription {
}</code></pre>
<hr>
<h2 id="inherited">@Inherited</h2>
<p><code>@Inherited</code>가 적용된 클래스 수준 어노테이션은 자식 클래스가 상속받은 것처럼 조회할 수 있다.</p>
<pre><code class="language-java">import java.lang.annotation.Inherited;

@Inherited
public @interface Role {
}</code></pre>
<p>다만 인터페이스 구현이나 메서드 어노테이션까지 자동으로 상속되는 것은 아니다.</p>
<hr>
<h2 id="repeatable">@Repeatable</h2>
<p>같은 어노테이션을 한 대상에 여러 번 작성할 수 있도록 한다.</p>
<pre><code class="language-java">import java.lang.annotation.Repeatable;

@Repeatable(Roles.class)
public @interface Role {

    String value();
}</code></pre>
<p>컨테이너 어노테이션도 필요하다.</p>
<pre><code class="language-java">public @interface Roles {

    Role[] value();
}</code></pre>
<p>사용할 때는 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-java">@Role(&quot;ADMIN&quot;)
@Role(&quot;MANAGER&quot;)
public class MemberService {
}</code></pre>
<hr>
<h1 id="reflection으로-annotation-읽기">Reflection으로 Annotation 읽기</h1>
<p>런타임 유지 정책을 가진 어노테이션은 Reflection을 통해 읽을 수 있다.</p>
<h2 id="사용자-정의-어노테이션">사용자 정의 어노테이션</h2>
<pre><code class="language-java">import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceInfo {

    String name();

    int version() default 1;
}</code></pre>
<h2 id="어노테이션-적용">어노테이션 적용</h2>
<pre><code class="language-java">@ServiceInfo(
        name = &quot;회원 서비스&quot;,
        version = 2
)
public class MemberService {
}</code></pre>
<h2 id="어노테이션-조회">어노테이션 조회</h2>
<pre><code class="language-java">public class AnnotationExample {

    public static void main(String[] args) {
        Class&lt;MemberService&gt; type =
                MemberService.class;

        ServiceInfo info =
                type.getAnnotation(
                        ServiceInfo.class
                );

        if (info != null) {
            System.out.println(info.name());
            System.out.println(info.version());
        }
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">회원 서비스
2</code></pre>
<p>프레임워크는 이와 유사한 방식으로 클래스와 메서드에 붙은 어노테이션을 분석한 뒤 객체 생성, 요청 매핑, 유효성 검사 같은 기능을 수행한다.</p>
<hr>
<h1 id="실무에서-제네릭-enum-annotation은-어떻게-사용할까">실무에서 제네릭, Enum, Annotation은 어떻게 사용할까?</h1>
<h2 id="제네릭">제네릭</h2>
<p>Java 컬렉션과 여러 API는 대부분 제네릭으로 설계되어 있다.</p>
<pre><code class="language-java">List&lt;Member&gt;
Set&lt;String&gt;
Map&lt;Long, Product&gt;
Optional&lt;Member&gt;
ResponseEntity&lt;UserResponse&gt;
Page&lt;ProductResponse&gt;</code></pre>
<p>제네릭 덕분에 하나의 API가 다양한 타입을 처리하면서도 컴파일 단계에서 타입 안정성을 제공할 수 있다.</p>
<p>메서드의 매개변수와 반환 타입을 설계할 때는 지나치게 광범위한 <code>Object</code>보다 구체적인 제네릭 타입을 사용하는 것이 좋다.</p>
<pre><code class="language-java">List&lt;Member&gt; findAll();</code></pre>
<p>또한 컬렉션을 읽기만 하거나 채워 넣는 메서드를 작성할 때 와일드카드와 PECS 원칙을 활용할 수 있다.</p>
<hr>
<h2 id="enum">Enum</h2>
<p>실무에서는 상태값, 권한, 등급, 결제 방식처럼 가능한 값이 명확히 제한된 데이터에 Enum을 사용한다.</p>
<pre><code class="language-java">public enum OrderStatus {
    CREATED,
    PAID,
    SHIPPING,
    DELIVERED,
    CANCELED
}</code></pre>
<p>문자열 상수 대신 Enum을 사용하면 오타를 줄이고 타입 안정성을 높일 수 있다.</p>
<p>상태별 동작이 있다면 조건문을 외부에 늘어놓기보다 Enum이 직접 책임지도록 설계할 수도 있다.</p>
<pre><code class="language-java">status.canCancel();
status.getDescription();</code></pre>
<p>다만 데이터베이스에 Enum을 저장할 때 <code>ordinal()</code> 순서를 사용하는 것은 피하고 문자열 이름이나 별도의 코드를 사용하는 것이 안전하다.</p>
<hr>
<h2 id="annotation">Annotation</h2>
<p>Spring에서는 어노테이션이 매우 광범위하게 사용된다.</p>
<pre><code class="language-java">@Controller
@Service
@Repository
@Component</code></pre>
<pre><code class="language-java">@GetMapping
@PostMapping
@RequestBody
@PathVariable</code></pre>
<pre><code class="language-java">@Transactional
@Valid
@NotNull</code></pre>
<p>개발자는 어노테이션으로 의도를 표현하고, 프레임워크가 해당 정보를 읽어 필요한 기능을 수행한다.</p>
<p>하지만 어노테이션을 사용한다고 해서 실제 동작이 자동으로 생기는 것은 아니다. 어노테이션을 분석하고 처리하는 컴파일러, 라이브러리 또는 프레임워크 코드가 있어야 한다.</p>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="listobject와-list"><code>List&lt;Object&gt;</code>와 <code>List&lt;?&gt;</code></h2>
<pre><code class="language-java">List&lt;Object&gt;</code></pre>
<p><code>Object</code> 또는 모든 자식 객체를 저장할 수 있는 리스트다.</p>
<pre><code class="language-java">List&lt;Object&gt; values =
        new ArrayList&lt;&gt;();

values.add(&quot;Java&quot;);
values.add(100);</code></pre>
<p>반면</p>
<pre><code class="language-java">List&lt;?&gt;</code></pre>
<p>구체적인 요소 타입을 알 수 없는 리스트를 참조한다.</p>
<pre><code class="language-java">List&lt;?&gt; values = List.of(&quot;Java&quot;);</code></pre>
<p><code>List&lt;String&gt;</code>일 수도 있고 <code>List&lt;Integer&gt;</code>일 수도 있으므로 새로운 값을 추가할 수 없다.</p>
<pre><code class="language-text">List&lt;Object&gt;
→ 요소 타입이 Object라고 확정

List&lt;?&gt;
→ 요소 타입을 알 수 없음</code></pre>
<hr>
<h2 id="t-extends-animal과--extends-animal"><code>&lt;T extends Animal&gt;</code>과 <code>&lt;? extends Animal&gt;</code></h2>
<pre><code class="language-java">&lt;T extends Animal&gt;</code></pre>
<p>메서드나 클래스가 사용하는 타입 파라미터를 선언한다.</p>
<pre><code class="language-java">public static &lt;T extends Animal&gt;
void print(T value) {
}</code></pre>
<pre><code class="language-java">&lt;? extends Animal&gt;</code></pre>
<p>이미 선언된 제네릭 타입의 범위를 표현하는 와일드카드다.</p>
<pre><code class="language-java">List&lt;? extends Animal&gt; animals;</code></pre>
<p>타입 파라미터는 타입 사이의 관계를 유지해야 할 때 유용하고, 와일드카드는 다양한 제네릭 타입을 유연하게 받을 때 유용하다.</p>
<hr>
<h2 id="enum과-상수-클래스">Enum과 상수 클래스</h2>
<pre><code class="language-java">public static final String GOLD = &quot;GOLD&quot;;</code></pre>
<p>문자열 상수는 값 자체만 제한적으로 제공한다.</p>
<pre><code class="language-java">public enum Grade {
    GOLD,
    SILVER
}</code></pre>
<p>Enum은 독립적인 타입이며 필드, 메서드, 생성자를 가질 수 있다.</p>
<p>서로 다른 Enum 타입의 상수는 이름이 같아도 호환되지 않는다.</p>
<hr>
<h2 id="annotation과-일반-주석">Annotation과 일반 주석</h2>
<pre><code class="language-java">// 회원을 저장한다.</code></pre>
<p>일반 주석은 사람이 읽는다.</p>
<pre><code class="language-java">@Override</code></pre>
<p>어노테이션은 컴파일러나 프레임워크가 읽을 수 있다.</p>
<p>다만 어노테이션 자체가 실행 로직을 가진 것은 아니다. 어노테이션을 처리하는 별도의 코드가 있어야 한다.</p>
<hr>
<h2 id="interface와-interface">@interface와 interface</h2>
<pre><code class="language-java">public interface Payment {
}</code></pre>
<p>객체가 구현해야 하는 기능의 규격을 정의한다.</p>
<pre><code class="language-java">public @interface Audit {
}</code></pre>
<p>코드에 부착할 메타데이터 타입을 정의한다.</p>
<p>문법이 비슷하지만 목적은 완전히 다르다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 Java 제네릭, Enum, Annotation을 함께 정리했다.</p>
<ul>
<li>제네릭은 클래스, 인터페이스, 메서드에서 사용할 타입을 외부에서 지정하는 문법이다.</li>
<li>제네릭은 컴파일 단계에서 타입을 검사하여 타입 안정성을 높인다.</li>
<li>제네릭을 사용하면 값을 꺼낼 때 불필요한 강제 타입 변환을 줄일 수 있다.</li>
<li>타입 파라미터는 일반적으로 <code>T</code>, <code>E</code>, <code>K</code>, <code>V</code> 등의 이름을 사용한다.</li>
<li>하나의 제네릭 클래스에 여러 타입 파라미터를 선언할 수 있다.</li>
<li>인터페이스와 메서드에도 제네릭을 적용할 수 있다.</li>
<li>Raw Type은 타입 검사를 우회하므로 사용하지 않는 것이 좋다.</li>
<li>제네릭에는 기본 타입 대신 래퍼 클래스를 사용해야 한다.</li>
<li>클래스의 타입 파라미터는 정적 필드나 정적 메서드에서 직접 사용할 수 없다.</li>
<li>정적 메서드에 제네릭이 필요하면 메서드 자체의 타입 파라미터를 선언해야 한다.</li>
<li><code>&lt;T extends Number&gt;</code>와 같이 타입의 범위를 제한할 수 있다.</li>
<li>Java 제네릭은 타입 소거 방식으로 구현된다.</li>
<li>타입 소거로 인해 <code>new T()</code>, <code>instanceof T</code>, 제네릭 배열 생성 등이 제한된다.</li>
<li><code>&lt;?&gt;</code>는 요소의 구체적인 타입을 알 수 없는 제네릭 타입이다.</li>
<li><code>&lt;? extends T&gt;</code>는 데이터를 읽는 생산자에 적합하다.</li>
<li><code>&lt;? super T&gt;</code>는 데이터를 저장하는 소비자에 적합하다.</li>
<li>PECS는 Producer Extends, Consumer Super를 의미한다.</li>
<li>Enum은 변수가 가질 수 있는 값을 미리 제한하는 특별한 클래스다.</li>
<li>열거 상수는 하나의 고정된 Enum 객체다.</li>
<li>Enum은 필드, 생성자, 메서드를 가질 수 있다.</li>
<li>Enum은 <code>switch</code>와 함께 사용하기 좋다.</li>
<li><code>values()</code>는 모든 열거 상수를 반환한다.</li>
<li><code>valueOf()</code>는 이름과 일치하는 열거 상수를 반환한다.</li>
<li><code>ordinal()</code>은 선언 순서를 반환하지만 저장용 업무 값으로 사용하지 않는 것이 좋다.</li>
<li>Annotation은 코드에 추가하는 메타데이터다.</li>
<li>어노테이션은 컴파일러, JVM, 라이브러리, 프레임워크 등이 활용할 수 있다.</li>
<li>사용자 정의 어노테이션은 <code>@interface</code>로 선언한다.</li>
<li>어노테이션의 속성은 추상 메서드와 비슷한 형태로 선언한다.</li>
<li>속성에는 <code>default</code>로 기본값을 지정할 수 있다.</li>
<li><code>value</code> 속성은 조건에 따라 이름을 생략할 수 있다.</li>
<li><code>@Target</code>은 어노테이션의 적용 위치를 제한한다.</li>
<li><code>@Retention</code>은 어노테이션 정보가 유지되는 기간을 지정한다.</li>
<li>런타임 어노테이션은 Reflection으로 조회할 수 있다.</li>
<li>Spring은 어노테이션을 분석하여 객체 생성, 요청 처리, 트랜잭션 등의 기능을 제공한다.</li>
</ul>
<hr>
<h2 id="면접과-평가에서-자주-나오는-질문">면접과 평가에서 자주 나오는 질문</h2>
<h3 id="제네릭을-사용하는-이유는">제네릭을 사용하는 이유는?</h3>
<p>컴파일 단계에서 타입 안정성을 확보하고 불필요한 형변환을 줄이며 하나의 코드를 여러 타입에 재사용하기 위해 사용한다.</p>
<h3 id="raw-type이란">Raw Type이란?</h3>
<p>제네릭 타입에서 타입 인수를 생략한 형태다.</p>
<pre><code class="language-java">List list = new ArrayList();</code></pre>
<p>하위 호환성을 위해 남아 있지만 타입 안정성을 잃으므로 새로운 코드에서는 사용하지 않는 것이 좋다.</p>
<h3 id="listobject와-list의-차이는"><code>List&lt;Object&gt;</code>와 <code>List&lt;?&gt;</code>의 차이는?</h3>
<p><code>List&lt;Object&gt;</code>는 요소 타입이 <code>Object</code>로 확정된 리스트다. 다양한 객체를 추가할 수 있다.</p>
<p><code>List&lt;?&gt;</code>는 요소 타입을 알 수 없는 리스트다. 구체적인 타입을 모르기 때문에 <code>null</code> 이외의 값을 안전하게 추가할 수 없다.</p>
<h3 id="pecs란">PECS란?</h3>
<pre><code class="language-text">Producer Extends
Consumer Super</code></pre>
<p>데이터를 꺼내는 생산자에는 <code>extends</code>, 데이터를 넣는 소비자에는 <code>super</code>를 사용하는 원칙이다.</p>
<h3 id="java의-제네릭이-공변인가">Java의 제네릭이 공변인가?</h3>
<p>기본적으로 불공변이다.</p>
<pre><code class="language-java">List&lt;Dog&gt;</code></pre>
<p>는 <code>List&lt;Animal&gt;</code>의 하위 타입이 아니다.</p>
<p>와일드카드를 통해 제한적인 공변성과 반공변성을 표현할 수 있다.</p>
<pre><code class="language-java">List&lt;? extends Animal&gt;
List&lt;? super Dog&gt;</code></pre>
<h3 id="enum과-문자열-상수의-차이는">Enum과 문자열 상수의 차이는?</h3>
<p>Enum은 독립적인 타입이므로 허용된 값만 저장할 수 있고, 필드와 메서드를 포함할 수 있다. 문자열 상수는 임의의 문자열과 구분되지 않아 타입 안정성이 약하다.</p>
<h3 id="equals로-enum을-비교해도-되는가">equals로 Enum을 비교해도 되는가?</h3>
<p>가능하지만 Enum은 각 상수가 하나의 고정 객체이므로 일반적으로 <code>==</code>를 사용하는 것이 명확하고 안전하다.</p>
<h3 id="annotation과-interface의-차이는">Annotation과 Interface의 차이는?</h3>
<p>인터페이스는 객체가 구현해야 하는 기능의 규격이다.</p>
<p>어노테이션은 코드에 부착하는 메타데이터 타입이다.</p>
<h3 id="retentionpolicy의-차이는">RetentionPolicy의 차이는?</h3>
<pre><code class="language-text">SOURCE
→ 소스 코드에서만 유지

CLASS
→ 클래스 파일까지 유지

RUNTIME
→ 실행 중에도 유지, Reflection 조회 가능</code></pre>
<h3 id="spring은-어노테이션을-어떻게-처리하는가">Spring은 어노테이션을 어떻게 처리하는가?</h3>
<p>클래스 패스 스캔, Reflection, 프록시, Bean 후처리기 등의 기능을 이용해 어노테이션이 부착된 클래스와 메서드를 분석하고 추가 동작을 적용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (6) 인터페이스와 추상 클래스의 활용]]></title>
            <link>https://velog.io/@tongmill_g/Java-6-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@tongmill_g/Java-6-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Mon, 27 Jul 2026 23:46:01 GMT</pubDate>
            <description><![CDATA[<h1 id="인터페이스와-추상-클래스의-활용">인터페이스와 추상 클래스의 활용</h1>
<h2 id="들어가며">들어가며</h2>
<p>이전 글에서는 다형성과 추상 클래스, 캡슐화를 공부했다.</p>
<p>추상 클래스를 이용하면 공통 기능을 부모 클래스에 모아 코드의 중복을 줄일 수 있었고, 다형성을 이용하면 부모 타입 하나로 다양한 자식 객체를 사용할 수 있었다.</p>
<p>하지만 객체지향 프로그래밍에서는 <strong>기능만 정의하고 구현은 각 객체에게 맡기고 싶은 경우</strong>도 자주 발생한다.</p>
<p>예를 들어</p>
<ul>
<li>결제할 수 있는 객체</li>
<li>출력할 수 있는 객체</li>
<li>이동할 수 있는 객체</li>
<li>저장할 수 있는 객체</li>
</ul>
<p>처럼 <strong>&quot;무엇을 할 수 있는가&quot;</strong> 만 정의하고 싶은 경우가 있다.</p>
<p>이럴 때 사용하는 것이 <strong>인터페이스(Interface)</strong> 이다.</p>
<p>인터페이스는 객체가 가져야 하는 기능의 규격을 정의하며, Java에서 다형성을 가장 많이 활용하는 방법이기도 하다.</p>
<p>이번 글에서는 인터페이스의 개념부터 구현 클래스, 다형성, default 메서드, static 메서드, private 메서드, 그리고 추상 클래스와의 차이까지 정리해보려고 한다.</p>
<hr>
<h1 id="인터페이스란-무엇인가">인터페이스란 무엇인가</h1>
<h2 id="인터페이스의-개념">인터페이스의 개념</h2>
<p>인터페이스는 <strong>객체의 사용 방법을 정의한 타입</strong>이다.</p>
<p>객체 내부가 어떻게 구현되어 있는지는 알 필요 없이,</p>
<p>어떤 기능을 사용할 수 있는지만 정의한다.</p>
<p>예를 들어 TV와 에어컨의 리모컨을 생각해보자.</p>
<p>사용자는</p>
<ul>
<li>전원 켜기</li>
<li>전원 끄기</li>
<li>볼륨 조절</li>
</ul>
<p>버튼만 알고 있으면 된다.</p>
<p>리모컨 내부 회로가 어떻게 동작하는지는 몰라도 사용할 수 있다.</p>
<p>인터페이스도 이와 동일하다.</p>
<p>개발자는 구현 방법을 몰라도 인터페이스만 알면 객체를 사용할 수 있다.</p>
<hr>
<h2 id="인터페이스를-사용하는-이유">인터페이스를 사용하는 이유</h2>
<p>인터페이스를 사용하면</p>
<p><strong>개발 코드와 실제 객체를 분리할 수 있다.</strong></p>
<pre><code class="language-text">개발 코드
      ↓
 Interface
      ↓
 실제 객체</code></pre>
<p>개발 코드는 인터페이스만 바라본다.</p>
<p>따라서 구현 객체가 변경되어도 개발 코드는 수정하지 않아도 된다.</p>
<p>예를 들어</p>
<pre><code class="language-java">Payment payment = new CardPayment();</code></pre>
<p>이 코드를</p>
<pre><code class="language-java">Payment payment = new KakaoPayment();</code></pre>
<p>으로 변경해도</p>
<p>사용하는 코드는 그대로 유지된다.</p>
<p>이것이 인터페이스가 제공하는 가장 큰 장점이다.</p>
<hr>
<h1 id="인터페이스-선언하기">인터페이스 선언하기</h1>
<p>인터페이스는 클래스와 동일하게 <code>.java</code> 파일로 작성한다.</p>
<pre><code class="language-java">public interface RemoteControl {

}</code></pre>
<p>클래스와 비슷하지만 중요한 차이가 있다.</p>
<p>인터페이스는 객체를 생성할 수 없다.</p>
<p>따라서 생성자를 가질 수 없다.</p>
<hr>
<h2 id="인터페이스의-구성-요소">인터페이스의 구성 요소</h2>
<p>인터페이스에는 대표적으로 다음 요소를 선언할 수 있다.</p>
<ul>
<li>상수</li>
<li>추상 메서드</li>
<li>default 메서드</li>
<li>static 메서드</li>
<li>private 메서드(Java9 이상)</li>
</ul>
<hr>
<h2 id="상수-필드">상수 필드</h2>
<p>인터페이스에는 일반 필드를 만들 수 없다.</p>
<p>오직 상수만 선언할 수 있다.</p>
<pre><code class="language-java">public interface RemoteControl {

    int MAX_VOLUME = 10;

}</code></pre>
<p>위 코드는 실제로 컴파일되면 다음과 동일하다.</p>
<pre><code class="language-java">public interface RemoteControl {

    public static final int MAX_VOLUME = 10;

}</code></pre>
<p>즉,</p>
<pre><code class="language-text">public
static
final</code></pre>
<p>은 자동으로 추가된다.</p>
<p>상수 이름은 관례적으로 모두 대문자를 사용하며</p>
<p>단어 사이는 <code>_</code>를 사용한다.</p>
<hr>
<h2 id="추상-메서드">추상 메서드</h2>
<p>인터페이스는 객체의 기능을 정의하는 것이 목적이다.</p>
<p>따라서 대부분 메서드는 구현부 없이 선언만 존재한다.</p>
<pre><code class="language-java">public interface RemoteControl {

    void turnOn();

    void turnOff();

}</code></pre>
<p>실제로는</p>
<pre><code class="language-java">public abstract void turnOn();</code></pre>
<p>과 같다.</p>
<p><code>public abstract</code>는 생략 가능하다.</p>
<hr>
<h1 id="인터페이스-구현하기">인터페이스 구현하기</h1>
<p>인터페이스는 객체를 만들 수 없기 때문에</p>
<p>반드시 구현 클래스가 필요하다.</p>
<pre><code class="language-java">public interface RemoteControl {

    void turnOn();

}</code></pre>
<p>구현 클래스는</p>
<p><code>implements</code></p>
<p>키워드를 사용한다.</p>
<pre><code class="language-java">public class Television implements RemoteControl {

    @Override
    public void turnOn() {
        System.out.println(&quot;TV 전원 ON&quot;);
    }

}</code></pre>
<p>인터페이스의 모든 추상 메서드를 반드시 구현해야 한다.</p>
<hr>
<h2 id="구현-객체-사용하기">구현 객체 사용하기</h2>
<p>객체는 인터페이스 타입으로 참조한다.</p>
<pre><code class="language-java">RemoteControl rc = new Television();

rc.turnOn();</code></pre>
<p>실제 객체는 Television이지만</p>
<p>개발 코드는 RemoteControl만 알고 있다.</p>
<p>이처럼 인터페이스를 이용하면 구현 객체를 쉽게 교체할 수 있다.</p>
<hr>
<h1 id="인터페이스와-다형성">인터페이스와 다형성</h1>
<p>인터페이스도 부모 클래스처럼 다형성을 구현할 수 있다.</p>
<pre><code class="language-java">RemoteControl rc;

rc = new Television();

rc = new Audio();</code></pre>
<p>같은 변수지만</p>
<p>저장되는 객체에 따라 실행 결과가 달라진다.</p>
<hr>
<h2 id="자동-타입-변환">자동 타입 변환</h2>
<p>인터페이스를 구현한 객체는</p>
<p>자동으로 인터페이스 타입으로 변환된다.</p>
<pre><code class="language-java">RemoteControl rc = new Television();</code></pre>
<p>이는 다음과 같은 의미이다.</p>
<pre><code class="language-java">Television tv = new Television();

RemoteControl rc = tv;</code></pre>
<p>별도의 형변환 없이 자동으로 변환된다.</p>
<hr>
<h2 id="매개변수의-다형성">매개변수의 다형성</h2>
<p>인터페이스는 매개변수에서도 매우 많이 사용된다.</p>
<pre><code class="language-java">public class User {

    public void use(RemoteControl rc) {
        rc.turnOn();
    }

}</code></pre>
<p>호출할 때는</p>
<pre><code class="language-java">user.use(new Television());

user.use(new Audio());</code></pre>
<p>처럼 다양한 구현 객체를 전달할 수 있다.</p>
<p>실제 실행되는 메서드는</p>
<p>각 객체가 구현한 메서드이다.</p>
<hr>
<h1 id="다중-인터페이스-구현">다중 인터페이스 구현</h1>
<p>Java는 클래스의 다중 상속은 지원하지 않는다.</p>
<p>하지만</p>
<p>인터페이스는 여러 개를 동시에 구현할 수 있다.</p>
<pre><code class="language-java">public interface Printer {

    void print();

}</code></pre>
<pre><code class="language-java">public interface Scanner {

    void scan();

}</code></pre>
<pre><code class="language-java">public class MultiFunctionPrinter
        implements Printer, Scanner {

    @Override
    public void print() {
        System.out.println(&quot;출력&quot;);
    }

    @Override
    public void scan() {
        System.out.println(&quot;스캔&quot;);
    }

}</code></pre>
<p>하나의 객체가 여러 인터페이스 타입으로 사용될 수 있다.</p>
<pre><code class="language-java">Printer printer = new MultiFunctionPrinter();

Scanner scanner = new MultiFunctionPrinter();</code></pre>
<hr>
<h1 id="인터페이스의-새로운-기능java8-이후">인터페이스의 새로운 기능(Java8 이후)</h1>
<p>Java8 이전에는 인터페이스에는</p>
<p>추상 메서드만 존재했다.</p>
<p>하지만 Java8부터 여러 기능이 추가되었다.</p>
<hr>
<h2 id="default-메서드">default 메서드</h2>
<p>인터페이스에서도 구현부를 가진 메서드를 작성할 수 있다.</p>
<pre><code class="language-java">public interface RemoteControl {

    void turnOn();

    default void setMute() {
        System.out.println(&quot;무음 처리&quot;);
    }

}</code></pre>
<p>구현 클래스는</p>
<p>필요하면 그대로 사용하고</p>
<p>원한다면 오버라이딩할 수도 있다.</p>
<hr>
<h2 id="default-메서드가-필요한-이유">default 메서드가 필요한 이유</h2>
<p>이미 배포된 인터페이스에 새로운 메서드를 추가하면</p>
<p>모든 구현 클래스가 컴파일 오류가 발생한다.</p>
<p>이를 해결하기 위해</p>
<p>default 메서드가 만들어졌다.</p>
<p>기존 구현 클래스는 수정하지 않아도 된다.</p>
<hr>
<h2 id="static-메서드">static 메서드</h2>
<p>인터페이스도 static 메서드를 가질 수 있다.</p>
<pre><code class="language-java">public interface RemoteControl {

    static void changeBattery() {
        System.out.println(&quot;배터리 교체&quot;);
    }

}</code></pre>
<p>객체를 생성하지 않고 사용할 수 있다.</p>
<pre><code class="language-java">RemoteControl.changeBattery();</code></pre>
<hr>
<h2 id="private-메서드">private 메서드</h2>
<p>Java9부터는</p>
<p>default 메서드 내부에서 사용할 private 메서드도 만들 수 있다.</p>
<pre><code class="language-java">public interface RemoteControl {

    default void turnOn() {
        checkBattery();
    }

    private void checkBattery() {
        System.out.println(&quot;배터리 확인&quot;);
    }

}</code></pre>
<p>외부에서는 호출할 수 없다.</p>
<p>인터페이스 내부의 코드 중복을 줄이기 위해 사용된다.</p>
<hr>
<h1 id="추상-클래스와-인터페이스">추상 클래스와 인터페이스</h1>
<p>추상 클래스와 인터페이스는 매우 비슷해 보인다.</p>
<p>하지만 목적이 다르다.</p>
<p>추상 클래스는</p>
<p>공통 기능과 상태를 공유하기 위한 부모 클래스이다.</p>
<p>인터페이스는</p>
<p>객체가 반드시 가져야 하는 기능의 규격이다.</p>
<p>예를 들어</p>
<p>동물은 모두 이름과 나이가 있다.</p>
<p>이 경우</p>
<pre><code class="language-java">abstract class Animal</code></pre>
<p>이 적절하다.</p>
<p>반면</p>
<p>날 수 있는 기능은</p>
<p>새도 가능하고</p>
<p>비행기도 가능하다.</p>
<p>공통 부모가 존재하지 않는다.</p>
<p>이 경우</p>
<pre><code class="language-java">interface Flyable</code></pre>
<p>이 적절하다.</p>
<hr>
<h2 id="추상-클래스와-인터페이스-비교">추상 클래스와 인터페이스 비교</h2>
<table>
<thead>
<tr>
<th>추상 클래스</th>
<th>인터페이스</th>
</tr>
</thead>
<tbody><tr>
<td>상속은 extends</td>
<td>구현은 implements</td>
</tr>
<tr>
<td>객체 생성 불가</td>
<td>객체 생성 불가</td>
</tr>
<tr>
<td>필드 가능</td>
<td>상수만 가능</td>
</tr>
<tr>
<td>생성자 가능</td>
<td>생성자 없음</td>
</tr>
<tr>
<td>일반 메서드 가능</td>
<td>default 이전에는 추상 메서드만 가능</td>
</tr>
<tr>
<td>단일 상속</td>
<td>다중 구현 가능</td>
</tr>
</tbody></table>
<p>간단히 정리하면</p>
<p><strong>&quot;공통 기능은 추상 클래스&quot;</strong></p>
<p><strong>&quot;공통 규격은 인터페이스&quot;</strong></p>
<p>라고 이해하면 된다.</p>
<hr>
<h1 id="인터페이스가-많이-사용되는-이유">인터페이스가 많이 사용되는 이유</h1>
<p>Java의 대표적인 API들도 대부분 인터페이스를 중심으로 설계되어 있다.</p>
<p>예를 들어</p>
<pre><code class="language-java">List&lt;String&gt; list = new ArrayList&lt;&gt;();</code></pre>
<p>변수는 List 인터페이스 타입이다.</p>
<p>실제 객체는 ArrayList이다.</p>
<p>필요하면</p>
<pre><code class="language-java">list = new LinkedList&lt;&gt;();</code></pre>
<p>로 변경해도 사용하는 코드는 거의 수정하지 않는다.</p>
<p>Spring Framework에서도</p>
<p>의존성 주입(DI)은 대부분 인터페이스를 기반으로 이루어진다.</p>
<p>즉,</p>
<p>인터페이스는 객체를 유연하게 교체할 수 있도록 만드는 핵심 기술이다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 인터페이스의 개념과 활용 방법을 정리했다.</p>
<ul>
<li>인터페이스는 객체의 사용 방법을 정의한 타입이다.</li>
<li>객체의 구현과 개발 코드를 분리하여 결합도를 낮춘다.</li>
<li>인터페이스는 객체를 생성할 수 없으며 생성자를 가질 수 없다.</li>
<li>인터페이스에는 상수와 추상 메서드를 선언할 수 있다.</li>
<li>인터페이스를 구현하는 클래스는 <code>implements</code>를 사용한다.</li>
<li>구현 클래스는 모든 추상 메서드를 반드시 오버라이딩해야 한다.</li>
<li>구현 객체는 인터페이스 타입으로 자동 타입 변환된다.</li>
<li>인터페이스 역시 다형성을 구현하는 핵심 요소이다.</li>
<li>매개변수와 필드에서도 인터페이스 타입을 사용할 수 있다.</li>
<li>하나의 클래스는 여러 인터페이스를 동시에 구현할 수 있다.</li>
<li>Java8부터는 default 메서드와 static 메서드를 지원한다.</li>
<li>Java9부터는 private 메서드도 작성할 수 있다.</li>
<li>추상 클래스는 공통 기능을 위한 부모 클래스이고, 인터페이스는 기능의 규격을 정의하기 위한 타입이다.</li>
<li>Java API와 Spring Framework는 대부분 인터페이스 중심으로 설계되어 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (5) 다형성, 추상 클래스, 캡슐화와 Object 메서드 정리]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-5-%EB%8B%A4%ED%98%95%EC%84%B1-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BA%A1%EC%8A%90%ED%99%94%EC%99%80-Object-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tongmill_g/JAVA-5-%EB%8B%A4%ED%98%95%EC%84%B1-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BA%A1%EC%8A%90%ED%99%94%EC%99%80-Object-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 22 Jul 2026 05:30:56 GMT</pubDate>
            <description><![CDATA[<h1 id="다형성-추상-클래스-캡슐화와-object-메서드-정리">다형성, 추상 클래스, 캡슐화와 Object 메서드 정리</h1>
<h2 id="들어가며">들어가며</h2>
<p>앞선 글에서는 상속을 통해 부모 클래스의 필드와 메서드를 자식 클래스가 재사용하고, 오버라이딩을 통해 부모의 기능을 자식 클래스에 맞게 다시 정의하는 방법을 살펴보았다.</p>
<p>상속 관계가 만들어지면 자식 객체는 자신의 타입뿐 아니라 부모 타입으로도 다룰 수 있다.</p>
<pre><code class="language-java">Dog dog = new Dog();
Animal animal = dog;</code></pre>
<p>실제 객체는 <code>Dog</code>이지만 이를 <code>Animal</code> 타입의 변수로 참조할 수 있다.</p>
<p>이처럼 하나의 객체를 여러 타입으로 다룰 수 있는 성질을 <strong>다형성</strong>이라고 한다.</p>
<p>다형성을 사용하면 코드 하나로 여러 종류의 객체를 처리할 수 있다. 새로운 자식 클래스가 추가되더라도 기존 코드를 크게 수정하지 않고 기능을 확장할 수 있기 때문에 객체지향 프로그래밍에서 매우 중요한 개념이다.</p>
<p>이번 글에서는 자동 타입 변환과 강제 타입 변환, <code>instanceof</code>, 필드와 매개변수의 다형성, 동적 바인딩, 추상 클래스, 캡슐화, 싱글톤 패턴, 그리고 <code>Object</code> 클래스의 주요 메서드까지 정리해보려고 한다.</p>
<hr>
<h1 id="부모-타입으로-자식-객체-사용하기">부모 타입으로 자식 객체 사용하기</h1>
<h2 id="참조-타입에도-타입-변환이-있다">참조 타입에도 타입 변환이 있다</h2>
<p>기본 타입의 값이 다른 기본 타입으로 변환될 수 있었던 것처럼 클래스와 클래스 사이에도 타입 변환이 존재한다.</p>
<p>다만 참조 타입의 형변환은 아무 클래스 사이에서나 가능한 것은 아니다. 기본적으로 상속 관계에 있는 클래스 사이에서만 가능하다.</p>
<pre><code class="language-java">public class Animal {

    public void eat() {
        System.out.println(&quot;먹이를 먹습니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat extends Animal {

    public void meow() {
        System.out.println(&quot;야옹&quot;);
    }
}</code></pre>
<p><code>Cat</code>은 <code>Animal</code>을 상속하므로 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-java">Cat cat = new Cat();
Animal animal = cat;</code></pre>
<p>한 문장으로 작성할 수도 있다.</p>
<pre><code class="language-java">Animal animal = new Cat();</code></pre>
<p>자식 타입의 객체를 부모 타입 변수에 저장하는 변환은 자동으로 이루어지므로 캐스팅 연산자를 작성하지 않아도 된다.</p>
<p>이를 <strong>자동 타입 변환</strong>, <strong>업캐스팅</strong>, 또는 <strong>묵시적 형변환</strong>이라고 한다.</p>
<hr>
<h2 id="바로-위-부모가-아니어도-변환할-수-있다">바로 위 부모가 아니어도 변환할 수 있다</h2>
<p>상속 계층에서 상위에 있는 타입이라면 바로 위의 부모가 아니어도 자동 타입 변환이 가능하다.</p>
<pre><code class="language-text">Object
  ↑
Animal
  ↑
Cat</code></pre>
<pre><code class="language-java">Cat cat = new Cat();

Animal animal = cat;
Object object = cat;</code></pre>
<p><code>Cat</code> 객체는 <code>Animal</code>이면서 동시에 <code>Object</code>이기도 하므로 두 타입 모두로 참조할 수 있다.</p>
<pre><code class="language-java">Object object = new Cat();</code></pre>
<p>모든 Java 클래스가 <code>Object</code>를 최상위 부모로 가지므로 모든 객체는 <code>Object</code> 타입 변수에 저장할 수 있다.</p>
<hr>
<h2 id="참조-변수-타입에-따라-사용할-수-있는-멤버가-달라진다">참조 변수 타입에 따라 사용할 수 있는 멤버가 달라진다</h2>
<p>다음 코드에서 실제로 Heap 영역에 생성된 객체는 <code>Cat</code>이다.</p>
<pre><code class="language-java">Animal animal = new Cat();</code></pre>
<p>하지만 참조 변수의 타입은 <code>Animal</code>이다.</p>
<p>따라서 컴파일러는 <code>Animal</code> 클래스에 선언된 멤버만 직접 사용할 수 있도록 제한한다.</p>
<pre><code class="language-java">animal.eat();

// animal.meow();
// 컴파일 오류</code></pre>
<p>실제 객체에는 <code>meow()</code>가 존재하지만 <code>Animal</code> 타입만 보고 있는 변수에서는 해당 메서드를 알 수 없다.</p>
<pre><code class="language-text">실제 객체: Cat

Cat 객체가 가진 기능
- eat()
- meow()

Animal 타입 변수로 보이는 기능
- eat()</code></pre>
<blockquote>
<p>객체가 실제로 가지고 있는 것과 현재 참조 변수를 통해 사용할 수 있는 것은 다를 수 있다.</p>
</blockquote>
<hr>
<h2 id="오버라이딩된-메서드는-실제-객체-기준으로-호출된다">오버라이딩된 메서드는 실제 객체 기준으로 호출된다</h2>
<p>부모 타입 변수에서는 부모 클래스에 선언된 메서드만 호출할 수 있다.</p>
<p>그러나 그 메서드가 자식 클래스에서 오버라이딩되었다면 부모 메서드가 아니라 자식 클래스에서 재정의한 메서드가 호출된다.</p>
<pre><code class="language-java">public class Animal {

    public void sound() {
        System.out.println(&quot;동물이 소리를 냅니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat extends Animal {

    @Override
    public void sound() {
        System.out.println(&quot;야옹&quot;);
    }

    public void meow() {
        System.out.println(&quot;고양이가 웁니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class PolymorphismExample {

    public static void main(String[] args) {
        Animal animal = new Cat();

        animal.sound();

        // animal.meow();
        // Animal 타입에 선언되지 않아 호출 불가
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">야옹</code></pre>
<p>참조 변수는 <code>Animal</code> 타입이지만 실제 객체가 <code>Cat</code>이므로 <code>Cat</code>에서 오버라이딩한 <code>sound()</code>가 실행된다.</p>
<p>이것이 다형성의 핵심이다.</p>
<pre><code class="language-text">호출 가능한 메서드
→ 참조 변수 타입을 기준으로 결정

실제로 실행되는 오버라이딩 메서드
→ 실제 객체 타입을 기준으로 결정</code></pre>
<hr>
<h1 id="다형성으로-유연한-코드-만들기">다형성으로 유연한 코드 만들기</h1>
<h2 id="다형성이란">다형성이란?</h2>
<p>다형성은 같은 사용 방법으로 여러 종류의 객체를 다룰 수 있고, 실제 객체에 따라 서로 다른 실행 결과를 만들 수 있는 성질이다.</p>
<p>다형성은 주로 다음 두 가지를 통해 구현된다.</p>
<ul>
<li>부모 타입으로 자식 객체를 참조하는 타입 변환</li>
<li>자식 클래스의 메서드 오버라이딩</li>
</ul>
<p>다음과 같은 동물 클래스가 있다고 가정해보자.</p>
<pre><code class="language-java">public class Animal {

    public void sound() {
        System.out.println(&quot;동물이 소리를 냅니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    public void sound() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat extends Animal {

    @Override
    public void sound() {
        System.out.println(&quot;야옹&quot;);
    }
}</code></pre>
<p>같은 <code>Animal</code> 타입 변수에 서로 다른 자식 객체를 저장할 수 있다.</p>
<pre><code class="language-java">Animal first = new Dog();
Animal second = new Cat();

first.sound();
second.sound();</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">멍멍
야옹</code></pre>
<p>호출 방법은 모두 <code>sound()</code>로 같지만 실제 객체에 따라 서로 다른 동작이 실행된다.</p>
<hr>
<h2 id="필드의-다형성">필드의 다형성</h2>
<p>필드를 부모 타입으로 선언하면 여러 종류의 자식 객체를 저장할 수 있다.</p>
<p>자동차에 장착되는 타이어를 예로 들어보자.</p>
<pre><code class="language-java">public class Tire {

    public void roll() {
        System.out.println(&quot;일반 타이어가 회전합니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class WinterTire extends Tire {

    @Override
    public void roll() {
        System.out.println(&quot;겨울용 타이어가 눈길에서 회전합니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class SportsTire extends Tire {

    @Override
    public void roll() {
        System.out.println(&quot;스포츠 타이어가 빠르게 회전합니다.&quot;);
    }
}</code></pre>
<p>자동차 클래스의 필드를 부모 타입으로 선언한다.</p>
<pre><code class="language-java">public class Car {

    private Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    public void run() {
        tire.roll();
    }

    public void changeTire(Tire tire) {
        this.tire = tire;
    }
}</code></pre>
<p>실행 클래스는 다음과 같다.</p>
<pre><code class="language-java">public class CarExample {

    public static void main(String[] args) {
        Car car = new Car(new WinterTire());

        car.run();

        car.changeTire(new SportsTire());

        car.run();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">겨울용 타이어가 눈길에서 회전합니다.
스포츠 타이어가 빠르게 회전합니다.</code></pre>
<p><code>Car</code> 클래스는 구체적인 타이어 종류를 알 필요가 없다. <code>Tire</code> 타입만 알고 있어도 여러 자식 객체를 사용할 수 있다.</p>
<hr>
<h2 id="매개변수의-다형성">매개변수의 다형성</h2>
<p>메서드의 매개변수를 부모 타입으로 선언하면 부모 객체뿐 아니라 모든 자식 객체를 인수로 받을 수 있다.</p>
<pre><code class="language-java">public class AnimalHospital {

    public void checkup(Animal animal) {
        System.out.println(&quot;진료를 시작합니다.&quot;);
        animal.sound();
    }
}</code></pre>
<pre><code class="language-java">public class HospitalExample {

    public static void main(String[] args) {
        AnimalHospital hospital = new AnimalHospital();

        hospital.checkup(new Dog());
        hospital.checkup(new Cat());
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">진료를 시작합니다.
멍멍
진료를 시작합니다.
야옹</code></pre>
<p>다형성을 사용하지 않았다면 각 타입마다 별도의 메서드를 만들어야 할 수도 있다.</p>
<pre><code class="language-java">void checkupDog(Dog dog) {
}

void checkupCat(Cat cat) {
}</code></pre>
<p>부모 타입 하나로 받으면 자식 클래스가 추가되더라도 기존 메서드를 재사용할 수 있다.</p>
<hr>
<h2 id="println도-다형성을-사용한다">println()도 다형성을 사용한다</h2>
<p><code>System.out.println()</code>에는 여러 오버로딩이 존재하며 객체를 출력하는 메서드는 <code>Object</code> 타입을 받을 수 있다.</p>
<pre><code class="language-java">System.out.println(new Dog());
System.out.println(new Cat());
System.out.println(new Person());</code></pre>
<p>모든 클래스는 <code>Object</code>를 상속하므로 어떤 객체든 <code>Object</code> 타입 매개변수로 전달할 수 있다.</p>
<pre><code class="language-java">public void printObject(Object object) {
    System.out.println(object);
}</code></pre>
<p>다만 <code>Object</code>처럼 너무 상위 타입을 사용하면 받을 수 있는 객체의 범위는 넓어지지만, 해당 타입에서 사용할 수 있는 기능은 줄어든다.</p>
<p>따라서 실제 비즈니스 로직에서는 처리하려는 객체들의 공통 부모나 인터페이스를 적절하게 선택하는 것이 중요하다.</p>
<hr>
<h2 id="배열의-다형성">배열의 다형성</h2>
<p>배열은 같은 타입의 데이터만 저장할 수 있다.</p>
<pre><code class="language-java">Dog[] dogs = new Dog[3];</code></pre>
<p><code>Dog[]</code>에는 <code>Dog</code> 객체만 저장할 수 있다.</p>
<p>부모 타입 배열을 사용하면 여러 자식 타입 객체를 하나의 배열에서 관리할 수 있다.</p>
<pre><code class="language-java">public class AnimalArrayExample {

    public static void main(String[] args) {
        Animal[] animals = new Animal[3];

        animals[0] = new Animal();
        animals[1] = new Dog();
        animals[2] = new Cat();

        for (Animal animal : animals) {
            animal.sound();
        }
    }
}</code></pre>
<p>실제 객체에 따라 오버라이딩된 메서드가 실행된다.</p>
<pre><code class="language-text">동물이 소리를 냅니다.
멍멍
야옹</code></pre>
<hr>
<h2 id="object-배열에는-모든-객체를-저장할-수-있다">Object 배열에는 모든 객체를 저장할 수 있다</h2>
<p><code>Object</code>는 모든 클래스의 최상위 부모이므로 <code>Object[]</code>에는 어떤 참조 타입 객체든 저장할 수 있다.</p>
<pre><code class="language-java">public class ObjectArrayExample {

    public static void main(String[] args) {
        Object[] values = new Object[4];

        values[0] = &quot;Java&quot;;
        values[1] = new Dog();
        values[2] = new Cat();
        values[3] = Integer.valueOf(100);

        for (Object value : values) {
            System.out.println(value);
        }
    }
}</code></pre>
<p>기본 타입 값도 오토박싱을 통해 래퍼 객체로 변환되므로 저장할 수 있다.</p>
<pre><code class="language-java">values[3] = 100;</code></pre>
<p>실제로는 다음과 비슷한 변환이 일어난다.</p>
<pre><code class="language-java">values[3] = Integer.valueOf(100);</code></pre>
<p>이러한 구조는 Java 컬렉션이 여러 객체를 저장할 수 있는 기반이 되었다.</p>
<p>다만 현대 Java의 컬렉션은 제네릭을 사용하므로 일반적으로 <code>Object</code>로 모든 값을 섞어 저장하기보다 저장할 타입을 명시한다.</p>
<pre><code class="language-java">List&lt;Animal&gt; animals = new ArrayList&lt;&gt;();</code></pre>
<hr>
<h1 id="부모-타입을-다시-자식-타입으로-변환하기">부모 타입을 다시 자식 타입으로 변환하기</h1>
<h2 id="강제-타입-변환">강제 타입 변환</h2>
<p>부모 타입 변수에서는 자식 클래스에만 선언된 멤버를 직접 사용할 수 없다.</p>
<pre><code class="language-java">Animal animal = new Cat();

// animal.meow();
// 호출 불가</code></pre>
<p>실제 객체가 <code>Cat</code>이라는 사실이 확실하다면 부모 타입 참조를 다시 자식 타입으로 변환할 수 있다.</p>
<pre><code class="language-java">Cat cat = (Cat) animal;

cat.meow();</code></pre>
<p>부모 타입을 자식 타입으로 변환하는 것을 <strong>강제 타입 변환</strong>, <strong>다운캐스팅</strong>, 또는 <strong>명시적 형변환</strong>이라고 한다.</p>
<pre><code class="language-text">자동 타입 변환

Cat → Animal
자식 → 부모

강제 타입 변환

Animal → Cat
부모 → 자식</code></pre>
<hr>
<h2 id="강제-타입-변환이-가능한-조건">강제 타입 변환이 가능한 조건</h2>
<p>부모 타입 변수라고 해서 무조건 자식 타입으로 변환할 수 있는 것은 아니다.</p>
<p>원래 자식 객체가 부모 타입으로 자동 변환되어 저장된 경우에만 다시 해당 자식 타입으로 변환할 수 있다.</p>
<pre><code class="language-java">Animal animal = new Cat();

Cat cat = (Cat) animal;</code></pre>
<p>위 코드는 실제 객체가 <code>Cat</code>이므로 정상적으로 동작한다.</p>
<p>반면 다음 코드는 실제 객체가 <code>Animal</code>이다.</p>
<pre><code class="language-java">Animal animal = new Animal();

Cat cat = (Cat) animal;</code></pre>
<p>컴파일은 될 수 있지만 실행 중 <code>ClassCastException</code>이 발생한다.</p>
<pre><code class="language-text">java.lang.ClassCastException</code></pre>
<p>참조 변수 타입만 보는 것이 아니라 Heap에 실제로 어떤 객체가 생성되어 있는지가 중요하다.</p>
<hr>
<h2 id="instanceof로-객체-타입-확인하기">instanceof로 객체 타입 확인하기</h2>
<p>안전하게 강제 타입 변환하려면 먼저 <code>instanceof</code> 연산자로 실제 객체 타입을 확인할 수 있다.</p>
<pre><code class="language-java">객체 instanceof 타입</code></pre>
<p>검사 결과는 <code>boolean</code>으로 반환된다.</p>
<pre><code class="language-java">Animal animal = new Cat();

boolean result = animal instanceof Cat;

System.out.println(result);</code></pre>
<p>결과는 <code>true</code>다.</p>
<p>안전한 변환 코드는 다음과 같다.</p>
<pre><code class="language-java">public class TypeCheckExample {

    public static void main(String[] args) {
        Animal animal = new Cat();

        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;

            cat.meow();
        }
    }
}</code></pre>
<hr>
<h2 id="instanceof-패턴-매칭">instanceof 패턴 매칭</h2>
<p>Java 16부터는 <code>instanceof</code> 검사와 타입 변환을 한 문장으로 처리하는 패턴 매칭을 사용할 수 있다.</p>
<pre><code class="language-java">if (animal instanceof Cat cat) {
    cat.meow();
}</code></pre>
<p>기존 코드와 비교하면 다음과 같다.</p>
<pre><code class="language-java">if (animal instanceof Cat) {
    Cat cat = (Cat) animal;

    cat.meow();
}</code></pre>
<p>패턴 매칭을 사용하면 중복된 캐스팅 코드를 줄이고 타입 검사가 성공한 영역에서 바로 변수를 사용할 수 있다.</p>
<hr>
<h2 id="메서드에서-타입에-따라-자식-기능-사용하기">메서드에서 타입에 따라 자식 기능 사용하기</h2>
<pre><code class="language-java">public class AnimalService {

    public void handle(Animal animal) {
        animal.sound();

        if (animal instanceof Cat cat) {
            cat.meow();
        }

        if (animal instanceof Dog dog) {
            dog.guard();
        }
    }
}</code></pre>
<p>다만 <code>instanceof</code>가 너무 많이 등장한다면 다형성을 충분히 활용하지 못한 설계일 가능성도 있다.</p>
<p>자식마다 다른 동작이 필요하다면 부모 클래스나 인터페이스에 공통 메서드를 선언하고 오버라이딩하는 방식이 더 적절할 수 있다.</p>
<pre><code class="language-java">public abstract class Animal {

    public abstract void performSpecialAction();
}</code></pre>
<p>각 자식 객체가 자신의 행동을 직접 책임지도록 만들면 외부 코드에서 타입을 계속 검사할 필요가 줄어든다.</p>
<hr>
<h1 id="참조-타입과-실제-객체의-메서드-연결">참조 타입과 실제 객체의 메서드 연결</h1>
<h2 id="정적-바인딩">정적 바인딩</h2>
<p>바인딩은 메서드나 필드 호출이 실제 코드와 연결되는 것을 의미한다.</p>
<p>컴파일 시점에 참조 변수의 타입을 기준으로 연결이 결정되는 것을 정적 바인딩이라고 한다.</p>
<p>대표적인 경우는 다음과 같다.</p>
<ul>
<li>필드</li>
<li><code>static</code> 메서드</li>
<li>오버로딩된 메서드</li>
<li><code>private</code> 메서드</li>
<li><code>final</code> 메서드</li>
</ul>
<p>부모와 자식 클래스에 같은 이름의 필드가 존재하는 예제를 살펴보자.</p>
<pre><code class="language-java">public class Parent {

    String name = &quot;부모 필드&quot;;

    static void printType() {
        System.out.println(&quot;Parent static method&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Child extends Parent {

    String name = &quot;자식 필드&quot;;

    static void printType() {
        System.out.println(&quot;Child static method&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class StaticBindingExample {

    public static void main(String[] args) {
        Parent value = new Child();

        System.out.println(value.name);
        value.printType();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">부모 필드
Parent static method</code></pre>
<p>실제 객체는 <code>Child</code>지만 필드와 정적 메서드는 참조 변수 타입인 <code>Parent</code>를 기준으로 연결된다.</p>
<hr>
<h2 id="동적-바인딩">동적 바인딩</h2>
<p>오버라이딩된 인스턴스 메서드는 프로그램 실행 시 실제 객체 타입에 따라 연결된다.</p>
<p>이를 동적 바인딩이라고 한다.</p>
<pre><code class="language-java">public class Parent {

    public void print() {
        System.out.println(&quot;Parent&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Child extends Parent {

    @Override
    public void print() {
        System.out.println(&quot;Child&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class DynamicBindingExample {

    public static void main(String[] args) {
        Parent value = new Child();

        value.print();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">Child</code></pre>
<p>참조 타입은 <code>Parent</code>지만 실제 객체가 <code>Child</code>이므로 가장 마지막에 오버라이딩된 <code>Child.print()</code>가 실행된다.</p>
<pre><code class="language-text">필드와 static 메서드
→ 참조 변수 타입 기준

오버라이딩된 인스턴스 메서드
→ 실제 객체 타입 기준</code></pre>
<p>이 차이가 다형성을 이해할 때 매우 중요하다.</p>
<hr>
<h1 id="추상-클래스로-공통-설계-규격-만들기">추상 클래스로 공통 설계 규격 만들기</h1>
<h2 id="추상-클래스란">추상 클래스란?</h2>
<p>여러 실체 클래스가 공통으로 가져야 하는 필드와 메서드를 추출해 선언한 클래스를 추상 클래스라고 한다.</p>
<p>추상 클래스는 직접 객체를 생성하기 위한 클래스가 아니라 자식 클래스의 공통 부모와 설계 규격 역할을 한다.</p>
<pre><code class="language-java">public abstract class Animal {
}</code></pre>
<p><code>abstract</code> 키워드를 사용해 선언한다.</p>
<p>추상 클래스는 <code>new</code> 연산자로 직접 객체를 생성할 수 없다.</p>
<pre><code class="language-java">// Animal animal = new Animal();
// 컴파일 오류</code></pre>
<p>상속을 통해 자식 클래스를 만든 뒤 자식 객체를 생성해야 한다.</p>
<pre><code class="language-java">Animal animal = new Dog();</code></pre>
<hr>
<h2 id="추상-클래스의-용도">추상 클래스의 용도</h2>
<p>추상 클래스는 크게 두 가지 목적으로 사용한다.</p>
<p>첫째, 여러 자식 클래스에 공통으로 필요한 필드와 메서드를 제공한다.</p>
<p>둘째, 자식 클래스가 반드시 구현해야 하는 기능을 추상 메서드로 선언하여 설계 규격을 통일한다.</p>
<pre><code class="language-java">public abstract class Animal {

    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + &quot;이(가) 먹이를 먹습니다.&quot;);
    }

    public abstract void sound();
}</code></pre>
<p><code>name</code>, 생성자, <code>eat()</code>은 모든 동물이 공통으로 사용한다.</p>
<p><code>sound()</code>는 모든 동물에게 필요하지만 구체적인 실행 내용은 자식마다 다르므로 추상 메서드로 선언한다.</p>
<hr>
<h2 id="추상-클래스도-생성자를-가진다">추상 클래스도 생성자를 가진다</h2>
<p>추상 클래스는 직접 객체를 만들 수 없지만 필드와 생성자를 선언할 수 있다.</p>
<pre><code class="language-java">public abstract class Animal {

    protected String name;

    public Animal(String name) {
        this.name = name;
    }
}</code></pre>
<p>자식 객체가 생성될 때 <code>super()</code>를 통해 추상 클래스의 생성자가 실행된다.</p>
<pre><code class="language-java">public class Dog extends Animal {

    public Dog(String name) {
        super(name);
    }

    @Override
    public void sound() {
        System.out.println(name + &quot;: 멍멍&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat extends Animal {

    public Cat(String name) {
        super(name);
    }

    @Override
    public void sound() {
        System.out.println(name + &quot;: 야옹&quot;);
    }
}</code></pre>
<hr>
<h2 id="추상-메서드">추상 메서드</h2>
<p>추상 메서드는 메서드 선언부만 있고 실행 블록이 없는 메서드다.</p>
<pre><code class="language-java">public abstract void sound();</code></pre>
<p>중괄호가 없고 세미콜론으로 끝난다.</p>
<p>추상 메서드를 하나라도 가진 클래스는 반드시 추상 클래스로 선언해야 한다.</p>
<pre><code class="language-java">public abstract class Animal {

    public abstract void sound();
}</code></pre>
<p>추상 클래스를 상속한 일반 자식 클래스는 모든 추상 메서드를 반드시 오버라이딩해야 한다.</p>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    public void sound() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<p>재정의하지 않으려면 자식 클래스 역시 추상 클래스로 선언해야 한다.</p>
<pre><code class="language-java">public abstract class Mammal extends Animal {
}</code></pre>
<hr>
<h2 id="추상-클래스를-활용한-다형성">추상 클래스를 활용한 다형성</h2>
<pre><code class="language-java">public class AbstractExample {

    public static void main(String[] args) {
        Animal[] animals = {
                new Dog(&quot;초코&quot;),
                new Cat(&quot;나비&quot;)
        };

        for (Animal animal : animals) {
            animal.eat();
            animal.sound();
        }
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">초코이(가) 먹이를 먹습니다.
초코: 멍멍
나비이(가) 먹이를 먹습니다.
나비: 야옹</code></pre>
<p><code>Animal</code>은 직접 생성할 수 없지만 자식 객체를 부모 타입으로 관리하는 기준 역할을 한다.</p>
<hr>
<h1 id="캡슐화로-객체의-데이터-보호하기">캡슐화로 객체의 데이터 보호하기</h1>
<h2 id="캡슐화란">캡슐화란?</h2>
<p>캡슐화는 객체의 데이터와 기능을 하나로 묶고 내부 상태를 외부에서 함부로 변경하지 못하도록 보호하는 객체지향 특성이다.</p>
<p>필드를 <code>public</code>으로 공개하면 외부에서 어떤 값이든 직접 저장할 수 있다.</p>
<pre><code class="language-java">public class BankAccount {

    public long balance;
}</code></pre>
<pre><code class="language-java">BankAccount account = new BankAccount();

account.balance = -100_000;</code></pre>
<p>잔액에 음수가 직접 저장되면서 객체가 유효하지 않은 상태가 될 수 있다.</p>
<p>필드를 <code>private</code>으로 숨기고 공개 메서드를 통해서만 변경하도록 제한할 수 있다.</p>
<pre><code class="language-java">public class BankAccount {

    private long balance;

    public void deposit(long amount) {
        if (amount &lt;= 0) {
            throw new IllegalArgumentException(
                    &quot;입금액은 0보다 커야 합니다.&quot;
            );
        }

        balance += amount;
    }

    public boolean withdraw(long amount) {
        if (amount &lt;= 0 || amount &gt; balance) {
            return false;
        }

        balance -= amount;

        return true;
    }

    public long getBalance() {
        return balance;
    }
}</code></pre>
<p>객체 내부에서 유효성을 검사하기 때문에 잘못된 상태를 방지할 수 있다.</p>
<hr>
<h2 id="getter와-setter">Getter와 Setter</h2>
<p><code>private</code> 필드의 값을 외부에서 읽고 변경할 필요가 있다면 Getter와 Setter 메서드를 제공할 수 있다.</p>
<pre><code class="language-java">public class Member {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(
                    &quot;이름은 비어 있을 수 없습니다.&quot;
            );
        }

        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age &lt; 0) {
            throw new IllegalArgumentException(
                    &quot;나이는 음수일 수 없습니다.&quot;
            );
        }

        this.age = age;
    }
}</code></pre>
<p>Getter는 값을 반환하는 메서드이고, Setter는 값을 변경하는 메서드다.</p>
<pre><code class="language-text">getName()
→ name 값을 읽음

setName(...)
→ name 값을 변경</code></pre>
<p>다만 모든 필드에 무조건 Getter와 Setter를 만드는 것이 캡슐화는 아니다.</p>
<p>객체의 상태를 어떤 방식으로 변경해야 하는지에 맞는 의미 있는 메서드를 제공하는 것이 더 중요하다.</p>
<pre><code class="language-java">account.setBalance(1000);</code></pre>
<p>보다 다음 방식이 객체의 의도를 더 잘 표현한다.</p>
<pre><code class="language-java">account.deposit(1000);</code></pre>
<hr>
<h1 id="객체-생성을-제한하는-싱글톤-패턴">객체 생성을 제한하는 싱글톤 패턴</h1>
<h2 id="객체를-하나만-생성해야-하는-경우">객체를 하나만 생성해야 하는 경우</h2>
<p>프로그램에 따라 특정 클래스의 객체가 여러 개 필요하지 않을 수 있다.</p>
<p>예를 들어 다음과 같은 경우가 있다.</p>
<ul>
<li>프로그램 전체에서 동일한 설정 객체를 사용할 때</li>
<li>공유 상태를 하나의 객체에서 관리해야 할 때</li>
<li>객체 생성 비용이 매우 커서 재사용해야 할 때</li>
<li>객체의 정체성을 하나로 유지해야 할 때</li>
</ul>
<p>이때 클래스의 객체를 하나만 생성하고 프로그램 전체에서 공유하도록 만드는 설계 방식을 싱글톤 패턴이라고 한다.</p>
<hr>
<h2 id="싱글톤-패턴-구현">싱글톤 패턴 구현</h2>
<pre><code class="language-java">public final class Singleton {

    private static final Singleton INSTANCE =
            new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public void sayHello() {
        System.out.println(&quot;Hello&quot;);
    }
}</code></pre>
<p>구현 원리는 다음과 같다.</p>
<p>외부에서 생성자를 호출하지 못하도록 <code>private</code> 생성자를 선언한다.</p>
<pre><code class="language-java">private Singleton() {
}</code></pre>
<p>클래스 내부에서 자신의 객체를 하나만 생성한다.</p>
<pre><code class="language-java">private static final Singleton INSTANCE =
        new Singleton();</code></pre>
<p>객체를 외부에 제공하는 정적 메서드를 선언한다.</p>
<pre><code class="language-java">public static Singleton getInstance() {
    return INSTANCE;
}</code></pre>
<hr>
<h2 id="싱글톤-객체-사용">싱글톤 객체 사용</h2>
<pre><code class="language-java">public class SingletonExample {

    public static void main(String[] args) {
        Singleton first = Singleton.getInstance();
        Singleton second = Singleton.getInstance();

        System.out.println(first == second);

        first.sayHello();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">true
Hello</code></pre>
<p>두 변수는 같은 객체를 참조한다.</p>
<pre><code class="language-text">first  ──┐
         ├──→ Singleton 객체
second ──┘</code></pre>
<hr>
<h2 id="싱글톤-사용-시-주의점">싱글톤 사용 시 주의점</h2>
<p>싱글톤은 객체를 하나만 유지할 수 있다는 장점이 있지만 전역 상태처럼 사용되기 쉬워 클래스 간 결합도를 높이고 테스트를 어렵게 만들 수 있다.</p>
<p>특히 변경 가능한 상태를 싱글톤이 가지고 있다면 여러 코드가 같은 값을 공유하면서 예상하지 못한 문제가 생길 수 있다.</p>
<p>Spring에서는 직접 싱글톤 코드를 작성하기보다 IoC 컨테이너가 Bean 객체의 생성과 생명주기를 관리한다. 기본적으로 Spring Bean은 싱글톤 스코프로 관리되지만 전통적인 싱글톤 패턴과 구현 방식은 다르다.</p>
<hr>
<h1 id="object-클래스의-주요-메서드-재정의하기">Object 클래스의 주요 메서드 재정의하기</h1>
<h2 id="object-클래스">Object 클래스</h2>
<p>Java의 모든 클래스는 최상위 부모인 <code>Object</code>를 상속한다.</p>
<p>따라서 모든 객체는 다음과 같은 메서드를 사용할 수 있다.</p>
<pre><code class="language-text">toString()
equals()
hashCode()
getClass()</code></pre>
<p>그중 <code>toString()</code>, <code>equals()</code>, <code>hashCode()</code>는 객체의 목적에 맞게 자주 재정의한다.</p>
<hr>
<h2 id="tostring">toString()</h2>
<p><code>toString()</code>은 객체를 문자열로 표현하는 메서드다.</p>
<p>재정의하지 않고 객체를 출력하면 클래스 이름과 해시 코드 기반 문자열이 출력된다.</p>
<pre><code class="language-java">public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}</code></pre>
<pre><code class="language-java">Person person = new Person(&quot;김자바&quot;, 20);

System.out.println(person);
System.out.println(person.toString());</code></pre>
<p><code>println()</code>에 객체를 전달하면 내부적으로 객체의 <code>toString()</code> 결과를 출력한다.</p>
<p>의미 있는 내용이 나오도록 재정의할 수 있다.</p>
<pre><code class="language-java">public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return &quot;Person{&quot;
                + &quot;name=&#39;&quot; + name + &#39;\&#39;&#39;
                + &quot;, age=&quot; + age
                + &#39;}&#39;;
    }
}</code></pre>
<p>출력 결과는 다음과 같다.</p>
<pre><code class="language-text">Person{name=&#39;김자바&#39;, age=20}</code></pre>
<hr>
<h2 id="equals">equals()</h2>
<p>참조 타입에 <code>==</code>를 사용하면 두 변수가 같은 객체를 참조하는지 비교한다.</p>
<pre><code class="language-java">Person first = new Person(&quot;김자바&quot;, 20);
Person second = new Person(&quot;김자바&quot;, 20);

System.out.println(first == second);</code></pre>
<p>두 객체는 별도로 생성되었으므로 결과는 <code>false</code>다.</p>
<p>객체의 필드 값을 기준으로 논리적인 동등성을 비교하려면 <code>equals()</code>를 재정의해야 한다.</p>
<pre><code class="language-java">import java.util.Objects;

public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof Person person)) {
            return false;
        }

        return age == person.age
                &amp;&amp; Objects.equals(name, person.name);
    }
}</code></pre>
<pre><code class="language-java">Person first = new Person(&quot;김자바&quot;, 20);
Person second = new Person(&quot;김자바&quot;, 20);

System.out.println(first.equals(second));</code></pre>
<p>결과는 <code>true</code>다.</p>
<hr>
<h2 id="hashcode">hashCode()</h2>
<p><code>hashCode()</code>는 객체를 구분하기 위해 사용하는 정수 형태의 해시값을 반환한다.</p>
<p><code>HashSet</code>, <code>HashMap</code>, <code>Hashtable</code> 같은 해시 기반 자료구조는 객체를 저장하고 검색할 때 <code>hashCode()</code>와 <code>equals()</code>를 함께 사용한다.</p>
<p><code>equals()</code>가 <code>true</code>인 두 객체는 반드시 같은 <code>hashCode()</code>를 반환해야 한다.</p>
<p>따라서 <code>equals()</code>를 재정의할 때는 <code>hashCode()</code>도 함께 재정의해야 한다.</p>
<pre><code class="language-java">@Override
public int hashCode() {
    return Objects.hash(name, age);
}</code></pre>
<p>완성된 클래스는 다음과 같다.</p>
<pre><code class="language-java">import java.util.Objects;

public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof Person person)) {
            return false;
        }

        return age == person.age
                &amp;&amp; Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return &quot;Person{&quot;
                + &quot;name=&#39;&quot; + name + &#39;\&#39;&#39;
                + &quot;, age=&quot; + age
                + &#39;}&#39;;
    }
}</code></pre>
<hr>
<h2 id="hashset에서-동등성-확인하기">HashSet에서 동등성 확인하기</h2>
<pre><code class="language-java">import java.util.HashSet;
import java.util.Set;

public class HashSetExample {

    public static void main(String[] args) {
        Set&lt;Person&gt; people = new HashSet&lt;&gt;();

        people.add(new Person(&quot;김자바&quot;, 20));
        people.add(new Person(&quot;김자바&quot;, 20));

        System.out.println(people.size());
    }
}</code></pre>
<p><code>equals()</code>와 <code>hashCode()</code>가 올바르게 재정의되어 있다면 논리적으로 같은 두 객체는 중복으로 저장되지 않는다.</p>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">1</code></pre>
<hr>
<h1 id="종합-예제-결제-방식-다형성">종합 예제: 결제 방식 다형성</h1>
<p>추상 클래스와 다형성을 이용해 여러 결제 방식을 처리해보자.</p>
<h2 id="payment-추상-클래스">Payment 추상 클래스</h2>
<pre><code class="language-java">public abstract class Payment {

    private final long amount;

    protected Payment(long amount) {
        if (amount &lt;= 0) {
            throw new IllegalArgumentException(
                    &quot;결제 금액은 0보다 커야 합니다.&quot;
            );
        }

        this.amount = amount;
    }

    public long getAmount() {
        return amount;
    }

    public abstract void pay();
}</code></pre>
<p>결제 금액은 모든 결제 방식이 공통으로 가지지만 실제 결제 방식은 자식 클래스마다 다르므로 <code>pay()</code>를 추상 메서드로 선언한다.</p>
<hr>
<h2 id="cardpayment">CardPayment</h2>
<pre><code class="language-java">public class CardPayment extends Payment {

    private final String cardNumber;

    public CardPayment(long amount, String cardNumber) {
        super(amount);
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay() {
        System.out.printf(
                &quot;카드 %s로 %,d원을 결제합니다.%n&quot;,
                cardNumber,
                getAmount()
        );
    }
}</code></pre>
<hr>
<h2 id="cashpayment">CashPayment</h2>
<pre><code class="language-java">public class CashPayment extends Payment {

    public CashPayment(long amount) {
        super(amount);
    }

    @Override
    public void pay() {
        System.out.printf(
                &quot;현금으로 %,d원을 결제합니다.%n&quot;,
                getAmount()
        );
    }
}</code></pre>
<hr>
<h2 id="paymentservice">PaymentService</h2>
<pre><code class="language-java">public class PaymentService {

    public void process(Payment payment) {
        System.out.println(&quot;결제 처리를 시작합니다.&quot;);

        payment.pay();

        System.out.println(&quot;결제 처리가 완료되었습니다.&quot;);
    }
}</code></pre>
<p>매개변수를 <code>Payment</code> 타입으로 선언했기 때문에 모든 자식 결제 객체를 받을 수 있다.</p>
<hr>
<h2 id="실행-클래스">실행 클래스</h2>
<pre><code class="language-java">public class PaymentExample {

    public static void main(String[] args) {
        PaymentService service = new PaymentService();

        Payment cardPayment = new CardPayment(
                50_000,
                &quot;1234-5678&quot;
        );

        Payment cashPayment = new CashPayment(30_000);

        service.process(cardPayment);

        System.out.println();

        service.process(cashPayment);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">결제 처리를 시작합니다.
카드 1234-5678로 50,000원을 결제합니다.
결제 처리가 완료되었습니다.

결제 처리를 시작합니다.
현금으로 30,000원을 결제합니다.
결제 처리가 완료되었습니다.</code></pre>
<p><code>PaymentService</code>는 카드 결제인지 현금 결제인지 직접 확인하지 않는다.</p>
<p>단지 <code>Payment</code> 타입의 <code>pay()</code>를 호출할 뿐이며, 실제 객체에 따라 적절한 메서드가 실행된다.</p>
<p>이러한 구조를 사용하면 새로운 결제 방식이 추가되어도 <code>PaymentService</code>를 수정하지 않고 확장할 수 있다.</p>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="자동-타입-변환과-강제-타입-변환">자동 타입 변환과 강제 타입 변환</h2>
<pre><code class="language-java">Animal animal = new Cat();</code></pre>
<p>자식 객체를 부모 타입으로 저장하므로 자동 타입 변환이다.</p>
<pre><code class="language-java">Cat cat = (Cat) animal;</code></pre>
<p>부모 타입 참조를 다시 자식 타입으로 바꾸므로 강제 타입 변환이다.</p>
<hr>
<h2 id="참조-타입과-실제-객체-타입">참조 타입과 실제 객체 타입</h2>
<pre><code class="language-java">Animal animal = new Cat();</code></pre>
<ul>
<li>참조 변수 타입: <code>Animal</code></li>
<li>실제 객체 타입: <code>Cat</code></li>
</ul>
<p>직접 호출할 수 있는 멤버는 <code>Animal</code>을 기준으로 결정되지만, 오버라이딩된 인스턴스 메서드는 <code>Cat</code>의 메서드가 실행된다.</p>
<hr>
<h2 id="오버로딩과-동적-바인딩">오버로딩과 동적 바인딩</h2>
<p>오버로딩된 메서드는 컴파일 시 전달한 인수와 참조 타입을 기준으로 선택된다.</p>
<p>오버라이딩된 메서드는 실행 시 실제 객체 타입을 기준으로 선택된다.</p>
<pre><code class="language-text">오버로딩
→ 컴파일 시점

오버라이딩
→ 실행 시점</code></pre>
<hr>
<h2 id="추상-클래스와-일반-클래스">추상 클래스와 일반 클래스</h2>
<p>추상 클래스도 필드, 생성자, 일반 메서드를 가질 수 있다.</p>
<p>차이점은 직접 객체를 생성할 수 없고 추상 메서드를 선언할 수 있다는 것이다.</p>
<pre><code class="language-java">public abstract class Animal {
}</code></pre>
<hr>
<h2 id="추상-메서드와-빈-메서드">추상 메서드와 빈 메서드</h2>
<p>다음은 추상 메서드다.</p>
<pre><code class="language-java">public abstract void sound();</code></pre>
<p>실행 내용이 없으며 자식 클래스가 구현해야 한다.</p>
<p>다음은 빈 일반 메서드다.</p>
<pre><code class="language-java">public void sound() {
}</code></pre>
<p>이미 구현이 완료된 일반 메서드이므로 자식 클래스가 반드시 오버라이딩할 필요가 없다.</p>
<hr>
<h2 id="final-참조-변수와-불변-객체">final 참조 변수와 불변 객체</h2>
<pre><code class="language-java">final List&lt;String&gt; names = new ArrayList&lt;&gt;();</code></pre>
<p><code>names</code>가 다른 리스트를 참조하도록 바꿀 수는 없지만 리스트 안의 요소는 추가하거나 삭제할 수 있다.</p>
<pre><code class="language-java">names.add(&quot;Java&quot;);</code></pre>
<p><code>final</code>은 참조 변경을 막는 것이지 객체 내부 상태를 자동으로 불변으로 만드는 것은 아니다.</p>
<hr>
<h2 id="equals와-">equals()와 ==</h2>
<pre><code class="language-java">first == second</code></pre>
<p>같은 객체를 참조하는지 비교한다.</p>
<pre><code class="language-java">first.equals(second)</code></pre>
<p>클래스에서 정의한 논리적인 동등성 기준으로 비교한다.</p>
<p><code>equals()</code>를 재정의하지 않으면 <code>Object.equals()</code>는 기본적으로 참조 동일성을 비교한다.</p>
<hr>
<h1 id="실무에서는-어떻게-사용할까">실무에서는 어떻게 사용할까?</h1>
<p>다형성은 객체지향 설계에서 조건문을 줄이고 기능 확장을 쉽게 만드는 데 사용된다.</p>
<p>다음과 같은 코드는 타입이 추가될 때마다 조건문을 수정해야 한다.</p>
<pre><code class="language-java">if (payment instanceof CardPayment) {
    // 카드 결제
} else if (payment instanceof CashPayment) {
    // 현금 결제
}</code></pre>
<p>다형성을 활용하면 각 객체가 자신의 동작을 책임질 수 있다.</p>
<pre><code class="language-java">payment.pay();</code></pre>
<p>새로운 결제 방식이 추가되더라도 <code>Payment</code>를 구현하거나 상속하고 <code>pay()</code>만 재정의하면 된다.</p>
<p>추상 클래스는 여러 구현 클래스가 상태와 공통 로직을 공유해야 할 때 적합하다. 단순히 동작의 규격만 필요하다면 이후 학습할 인터페이스가 더 적절할 수 있다.</p>
<p>객체의 필드는 기본적으로 <code>private</code>으로 숨기고, 객체가 유효한 상태를 유지할 수 있도록 의미 있는 메서드를 제공하는 것이 좋다.</p>
<pre><code class="language-java">order.changeStatus(OrderStatus.PAID);</code></pre>
<p>단순 Setter보다 객체의 업무 의미가 드러나는 메서드를 사용하는 것이 유지보수에 유리하다.</p>
<p>싱글톤은 필요할 때만 제한적으로 사용해야 한다. Spring 환경에서는 객체 생성을 직접 통제하는 싱글톤 코드보다 컨테이너에 객체 관리를 맡기는 경우가 많다.</p>
<p><code>equals()</code>와 <code>hashCode()</code>는 DTO, 값 객체, 컬렉션의 키처럼 논리적 동등성이 중요한 클래스에서 함께 재정의해야 한다. Java의 <code>record</code>를 사용하면 컴파일러가 두 메서드와 <code>toString()</code>을 자동으로 생성해주므로 값 전달 객체를 표현할 때 유용하다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 상속 관계의 객체를 유연하게 사용하는 다형성과 객체 형변환을 정리했다.</p>
<ul>
<li>참조 타입도 상속 관계에서 타입 변환이 가능하다.</li>
<li>자식 객체는 부모 타입 변수에 자동으로 저장할 수 있다.</li>
<li>자식 타입에서 부모 타입으로의 변환을 업캐스팅이라고 한다.</li>
<li>바로 위 부모뿐 아니라 상속 계층의 더 상위 타입으로도 변환할 수 있다.</li>
<li>모든 객체는 <code>Object</code> 타입으로 참조할 수 있다.</li>
<li>부모 타입 변수에서는 부모 클래스에 선언된 멤버만 직접 사용할 수 있다.</li>
<li>부모 메서드가 자식 클래스에서 오버라이딩되었다면 실제 자식 객체의 메서드가 실행된다.</li>
<li>같은 호출 방식으로 객체마다 다른 결과가 나오는 성질을 다형성이라고 한다.</li>
<li>부모 타입 필드에는 여러 자식 객체를 저장할 수 있다.</li>
<li>부모 타입 매개변수에는 부모 객체와 모든 자식 객체를 전달할 수 있다.</li>
<li>부모 타입 배열을 사용하면 서로 다른 자식 객체를 하나의 배열에서 관리할 수 있다.</li>
<li><code>Object[]</code>에는 모든 참조 타입 객체를 저장할 수 있다.</li>
<li>기본 타입 값은 오토박싱을 통해 래퍼 객체로 변환되어 <code>Object</code>에 저장될 수 있다.</li>
<li>부모 타입을 다시 자식 타입으로 변환하는 것을 다운캐스팅이라고 한다.</li>
<li>강제 타입 변환은 실제 객체가 해당 자식 타입인 경우에만 안전하다.</li>
<li>잘못된 강제 타입 변환은 <code>ClassCastException</code>을 발생시킨다.</li>
<li><code>instanceof</code>로 객체의 실제 타입을 확인할 수 있다.</li>
<li>Java 16부터 <code>instanceof</code> 패턴 매칭을 사용할 수 있다.</li>
<li>필드와 <code>static</code> 메서드는 참조 변수 타입을 기준으로 연결된다.</li>
<li>오버라이딩된 인스턴스 메서드는 실제 객체 타입을 기준으로 실행된다.</li>
<li>이를 동적 바인딩이라고 한다.</li>
<li>추상 클래스는 실체 클래스의 공통 상태와 기능을 추출한 부모 클래스다.</li>
<li>추상 클래스는 직접 객체를 생성할 수 없다.</li>
<li>추상 클래스도 필드, 생성자, 일반 메서드를 가질 수 있다.</li>
<li>추상 메서드는 실행 블록 없이 선언만 존재한다.</li>
<li>일반 자식 클래스는 부모의 추상 메서드를 반드시 오버라이딩해야 한다.</li>
<li>캡슐화는 객체의 데이터를 숨기고 공개 메서드로 접근을 통제하는 것이다.</li>
<li>필드를 <code>private</code>으로 선언하면 외부에서 직접 변경할 수 없다.</li>
<li>Getter와 Setter는 필드 접근 통로가 될 수 있지만 무조건 만들어야 하는 것은 아니다.</li>
<li>객체의 의도를 나타내는 메서드를 제공하는 것이 더 좋은 캡슐화일 수 있다.</li>
<li>싱글톤 패턴은 객체를 하나만 생성해 공유하는 방식이다.</li>
<li>싱글톤은 <code>private</code> 생성자와 <code>static</code> 인스턴스 및 접근 메서드로 구현할 수 있다.</li>
<li><code>toString()</code>은 객체를 의미 있는 문자열로 표현하도록 재정의할 수 있다.</li>
<li><code>equals()</code>는 객체의 논리적인 동등성을 비교하도록 재정의할 수 있다.</li>
<li><code>equals()</code>를 재정의하면 <code>hashCode()</code>도 함께 재정의해야 한다.</li>
<li>해시 기반 컬렉션은 객체 비교에 <code>hashCode()</code>와 <code>equals()</code>를 함께 사용한다.</li>
</ul>
<hr>
<h2 id="면접과-평가에서-자주-나오는-질문">면접과 평가에서 자주 나오는 질문</h2>
<h3 id="업캐스팅과-다운캐스팅의-차이">업캐스팅과 다운캐스팅의 차이</h3>
<pre><code class="language-text">업캐스팅
- 자식 타입 → 부모 타입
- 자동 변환
- 안전한 변환
- 사용할 수 있는 멤버 범위가 부모 기준으로 제한

다운캐스팅
- 부모 타입 → 자식 타입
- 명시적 캐스팅 필요
- 실제 객체 타입이 맞지 않으면 ClassCastException</code></pre>
<h3 id="부모-타입인데-왜-자식-메서드가-실행되는가">부모 타입인데 왜 자식 메서드가 실행되는가?</h3>
<p>부모 타입 변수로 호출 가능한 메서드는 컴파일 단계에서 결정되지만, 오버라이딩된 인스턴스 메서드의 실제 구현은 런타임에 실제 객체 타입을 기준으로 선택되기 때문이다. 이를 동적 바인딩이라고 한다.</p>
<h3 id="필드는-다형성이-적용되는가">필드는 다형성이 적용되는가?</h3>
<p>메서드 오버라이딩과 같은 방식의 다형성이 적용되지 않는다. 중복된 필드는 참조 변수 타입을 기준으로 선택된다.</p>
<h3 id="instanceof를-많이-쓰면-왜-좋지-않을-수-있는가">instanceof를 많이 쓰면 왜 좋지 않을 수 있는가?</h3>
<p>객체별 동작을 외부 코드가 타입별로 분기하고 있다는 뜻일 수 있다. 자식 클래스가 자신의 동작을 오버라이딩하도록 설계하면 타입 검사 코드를 줄일 수 있다.</p>
<h3 id="추상-클래스는-생성자가-왜-필요한가">추상 클래스는 생성자가 왜 필요한가?</h3>
<p>자식 객체가 생성될 때 부모 클래스에서 선언한 필드를 초기화하고 공통 초기화 작업을 수행하기 위해 필요하다.</p>
<h3 id="추상-클래스에-일반-메서드를-작성할-수-있는가">추상 클래스에 일반 메서드를 작성할 수 있는가?</h3>
<p>가능하다. 추상 클래스는 일반 클래스의 기능을 모두 가지면서 추상 메서드를 추가로 선언할 수 있다.</p>
<h3 id="private-생성자는-상속을-막는가">private 생성자는 상속을 막는가?</h3>
<p>직접적인 자식 클래스의 생성자가 부모 생성자를 호출할 수 없기 때문에 일반적으로 상속이 불가능해진다. 다만 같은 최상위 클래스 내부에 선언된 중첩 클래스처럼 접근 가능한 특별한 위치는 예외가 될 수 있다.</p>
<h3 id="equals만-재정의하면-어떤-문제가-생기는가">equals만 재정의하면 어떤 문제가 생기는가?</h3>
<p><code>HashSet</code>이나 <code>HashMap</code>이 먼저 해시 코드를 기준으로 객체 위치를 찾기 때문에, 논리적으로 같은 객체가 서로 다른 버킷에 저장되어 중복 처리나 검색 실패가 발생할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (4) 상속과 오버라이딩]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-4-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9</link>
            <guid>https://velog.io/@tongmill_g/JAVA-4-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9</guid>
            <pubDate>Tue, 21 Jul 2026 13:59:49 GMT</pubDate>
            <description><![CDATA[<h1 id="상속과-오버라이딩-클래스-관계부터-접근-제한자까지">상속과 오버라이딩: 클래스 관계부터 접근 제한자까지</h1>
<h2 id="들어가며">들어가며</h2>
<p>앞선 글에서는 객체와 클래스, 필드, 메서드, 생성자를 이용해 하나의 객체를 설계하는 방법을 살펴보았다.</p>
<p>하지만 실제 프로그램에서는 여러 클래스가 완전히 독립적으로 존재하지 않는다. 자동차와 전기 자동차처럼 공통된 속성과 기능을 가지는 클래스도 있고, 자동차와 엔진처럼 하나의 객체가 다른 객체를 부품으로 포함하는 경우도 있다.</p>
<p>예를 들어 <code>Dog</code>와 <code>Cat</code> 클래스가 모두 이름과 나이를 가지고 있으며 먹는 기능을 제공한다고 생각해보자.</p>
<pre><code class="language-java">public class Dog {

    String name;
    int age;

    void eat() {
        System.out.println(&quot;먹이를 먹습니다.&quot;);
    }

    void bark() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat {

    String name;
    int age;

    void eat() {
        System.out.println(&quot;먹이를 먹습니다.&quot;);
    }

    void meow() {
        System.out.println(&quot;야옹&quot;);
    }
}</code></pre>
<p>두 클래스에는 <code>name</code>, <code>age</code>, <code>eat()</code>이라는 코드가 중복되어 있다. 이런 공통 기능을 상위 클래스에 정의하고 하위 클래스가 물려받도록 만드는 기능이 <strong>상속</strong>이다.</p>
<p>이번 글에서는 Java의 상속과 포함 관계, 메서드 오버라이딩, <code>super</code>, <code>Object</code>, 패키지와 import, 접근 제한자와 <code>final</code>, 그리고 상속 범위를 제한하는 봉인 클래스까지 정리해보려고 한다.</p>
<hr>
<h1 id="상속으로-클래스의-공통-기능-재사용하기">상속으로 클래스의 공통 기능 재사용하기</h1>
<h2 id="상속이란">상속이란?</h2>
<p>상속은 기존 클래스가 가지고 있는 필드와 메서드를 새로운 클래스가 물려받아 재사용하는 기능이다.</p>
<p>기존 클래스를 부모 클래스 또는 상위 클래스라고 하며, 부모 클래스를 상속받는 클래스를 자식 클래스 또는 하위 클래스라고 한다.</p>
<pre><code class="language-text">부모 클래스
- 공통 속성
- 공통 기능

       ↓ 상속

자식 클래스
- 부모의 속성과 기능 사용
- 자신만의 속성과 기능 추가</code></pre>
<p>Java에서는 <code>extends</code> 키워드를 이용해 상속 관계를 만든다.</p>
<pre><code class="language-java">public class Animal {

    String name;
    int age;

    void eat() {
        System.out.println(name + &quot;이(가) 먹이를 먹습니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    void bark() {
        System.out.println(name + &quot;이(가) 멍멍 짖습니다.&quot;);
    }
}</code></pre>
<p><code>Dog</code> 클래스에는 <code>name</code>, <code>age</code>, <code>eat()</code>이 직접 선언되어 있지 않지만 <code>Animal</code> 클래스로부터 상속받았기 때문에 사용할 수 있다.</p>
<pre><code class="language-java">public class AnimalExample {

    public static void main(String[] args) {
        Dog dog = new Dog();

        dog.name = &quot;초코&quot;;
        dog.age = 3;

        dog.eat();
        dog.bark();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">초코이(가) 먹이를 먹습니다.
초코이(가) 멍멍 짖습니다.</code></pre>
<p>상속을 사용하면 여러 클래스에 반복되는 코드를 공통 부모 클래스로 옮길 수 있다.</p>
<pre><code class="language-text">Animal
- name
- age
- eat()

Dog extends Animal
- bark()

Cat extends Animal
- meow()</code></pre>
<p>공통 기능을 부모 클래스에서 관리하기 때문에 부모 클래스의 코드를 수정하면 이를 상속받는 자식 클래스에도 변경 사항이 반영된다.</p>
<hr>
<h2 id="상속되지-않는-것">상속되지 않는 것</h2>
<p>자식 클래스가 부모 클래스의 모든 코드를 그대로 물려받는 것은 아니다.</p>
<p>부모 클래스의 다음 요소는 자식 클래스에 직접 상속되지 않는다.</p>
<ul>
<li>부모 클래스의 생성자</li>
<li>부모 클래스의 초기화 블록</li>
<li>접근할 수 없는 <code>private</code> 멤버</li>
</ul>
<p>생성자는 객체를 초기화하기 위해 해당 클래스가 직접 실행하는 코드이므로 상속되지 않는다. 다만 자식 객체가 만들어질 때 부모 생성자는 반드시 호출된다.</p>
<pre><code class="language-java">public class Parent {

    Parent() {
        System.out.println(&quot;Parent 생성자 실행&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Child extends Parent {

    Child() {
        System.out.println(&quot;Child 생성자 실행&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class ConstructorExample {

    public static void main(String[] args) {
        Child child = new Child();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">Parent 생성자 실행
Child 생성자 실행</code></pre>
<p>자식 객체를 만들 때 부모 부분이 먼저 초기화되고, 이후 자식 부분이 초기화된다.</p>
<hr>
<h2 id="상속은-is-a-관계다">상속은 is-a 관계다</h2>
<p>상속 관계는 흔히 <strong>is-a 관계</strong>라고 표현한다.</p>
<pre><code class="language-text">Dog is an Animal.
강아지는 동물이다.

ElectricCar is a Car.
전기 자동차는 자동차다.</code></pre>
<p>자식 객체를 부모 타입의 한 종류라고 자연스럽게 말할 수 있다면 상속 관계를 고려할 수 있다.</p>
<pre><code class="language-java">public class ElectricCar extends Car {
}</code></pre>
<p>반대로 다음 관계는 자연스럽지 않다.</p>
<pre><code class="language-text">Engine is a Car.
엔진은 자동차다.</code></pre>
<p>엔진은 자동차의 한 종류가 아니라 자동차가 가지고 있는 부품이다. 이런 관계는 상속이 아니라 포함 관계로 설계해야 한다.</p>
<hr>
<h2 id="java가-단일-상속만-지원하는-이유">Java가 단일 상속만 지원하는 이유</h2>
<p>Java 클래스는 하나의 부모 클래스만 직접 상속할 수 있다.</p>
<pre><code class="language-java">public class Child extends Parent {
}</code></pre>
<p>다음처럼 두 클래스를 동시에 상속할 수는 없다.</p>
<pre><code class="language-java">// 사용할 수 없는 문법
// public class Child extends ParentA, ParentB {
// }</code></pre>
<p>만약 다중 상속을 허용했는데 두 부모 클래스에 동일한 이름과 형태의 메서드가 존재한다면 어떤 메서드를 사용해야 하는지 모호해질 수 있다.</p>
<pre><code class="language-text">ParentA
└─ print()

ParentB
└─ print()

Child
└─ 어느 print()를 상속해야 하는가?</code></pre>
<p>Java는 이러한 복잡성을 줄이기 위해 클래스의 단일 상속만 허용한다.</p>
<p>다만 하나의 클래스가 여러 인터페이스를 구현하는 것은 가능하다.</p>
<pre><code class="language-java">public class SmartPhone
        implements Camera, MusicPlayer, Phone {
}</code></pre>
<p>여러 객체의 기능이 필요하다면 인터페이스와 포함 관계를 함께 활용할 수 있다.</p>
<hr>
<h2 id="모든-클래스의-부모인-object">모든 클래스의 부모인 Object</h2>
<p>Java의 모든 클래스는 최종적으로 <code>Object</code> 클래스를 상속한다.</p>
<p>다음 클래스에 <code>extends</code>가 작성되어 있지 않더라도</p>
<pre><code class="language-java">public class Person {
}</code></pre>
<p>개념적으로는 다음과 같은 구조를 가진다.</p>
<pre><code class="language-java">public class Person extends Object {
}</code></pre>
<p>따라서 모든 객체는 <code>Object</code> 클래스가 제공하는 메서드를 사용할 수 있다.</p>
<p>대표적인 메서드는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>toString()</code></td>
<td>객체를 문자열로 표현</td>
</tr>
<tr>
<td><code>equals()</code></td>
<td>객체의 동등성 비교</td>
</tr>
<tr>
<td><code>hashCode()</code></td>
<td>객체의 해시 코드 반환</td>
</tr>
<tr>
<td><code>getClass()</code></td>
<td>객체의 클래스 정보 반환</td>
</tr>
</tbody></table>
<pre><code class="language-java">public class Person {

    String name;
}</code></pre>
<pre><code class="language-java">public class ObjectExample {

    public static void main(String[] args) {
        Person person = new Person();

        System.out.println(person.toString());
        System.out.println(person.getClass());
        System.out.println(person.hashCode());
    }
}</code></pre>
<p>이 메서드들은 <code>Person</code>에 직접 선언하지 않았지만 <code>Object</code>로부터 상속받았기 때문에 사용할 수 있다.</p>
<hr>
<h1 id="상속과-포함-관계를-구분하기">상속과 포함 관계를 구분하기</h1>
<h2 id="포함-관계란">포함 관계란?</h2>
<p>클래스를 재사용하는 방법에는 상속만 있는 것이 아니다.</p>
<p>하나의 클래스가 다른 클래스의 객체를 필드로 가지도록 설계할 수도 있다. 이를 포함 관계 또는 구성 관계라고 한다.</p>
<p>포함 관계는 보통 <strong>has-a 관계</strong>라고 표현한다.</p>
<pre><code class="language-text">Car has an Engine.
자동차는 엔진을 가지고 있다.

Computer has a CPU.
컴퓨터는 CPU를 가지고 있다.</code></pre>
<pre><code class="language-java">public class Engine {

    void start() {
        System.out.println(&quot;엔진이 작동합니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Car {

    private final Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    void start() {
        engine.start();
        System.out.println(&quot;자동차가 출발할 준비를 마쳤습니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class CarExample {

    public static void main(String[] args) {
        Car car = new Car();

        car.start();
    }
}</code></pre>
<p><code>Car</code>는 <code>Engine</code>을 상속하지 않고 필드로 포함한다.</p>
<hr>
<h2 id="상속과-포함-중-무엇을-선택할까">상속과 포함 중 무엇을 선택할까?</h2>
<p>상속과 포함은 단순히 문법의 문제가 아니라 클래스 설계의 문제다.</p>
<table>
<thead>
<tr>
<th>관계</th>
<th>판단 기준</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>상속</td>
<td>A는 B의 한 종류인가?</td>
<td>강아지는 동물이다</td>
</tr>
<tr>
<td>포함</td>
<td>A는 B를 가지고 있는가?</td>
<td>자동차는 엔진을 가지고 있다</td>
</tr>
</tbody></table>
<pre><code class="language-text">Dog is an Animal
→ 상속

Car has an Engine
→ 포함</code></pre>
<p>상속을 잘못 사용하면 부모 클래스와 자식 클래스가 지나치게 강하게 결합될 수 있다. 부모 클래스의 변경이 모든 자식 클래스에 영향을 주기 때문이다.</p>
<p>따라서 단순히 코드를 재사용하고 싶다는 이유만으로 상속을 사용하기보다, 실제로 자연스러운 <code>is-a</code> 관계가 성립하는지 먼저 생각해야 한다.</p>
<blockquote>
<p>💡 상속보다 포함을 우선해서 고려하라는 설계 원칙을 “상속보다는 컴포지션을 선호하라”라고 표현하기도 한다.</p>
</blockquote>
<hr>
<h1 id="메서드-오버라이딩과-super">메서드 오버라이딩과 super</h1>
<h2 id="메서드-오버라이딩이란">메서드 오버라이딩이란?</h2>
<p>부모 클래스에서 정의한 메서드가 자식 클래스의 상황에 맞지 않을 수 있다.</p>
<p>예를 들어 모든 동물이 소리를 낸다는 공통 기능은 있지만, 강아지와 고양이는 서로 다른 소리를 낸다.</p>
<pre><code class="language-java">public class Animal {

    void sound() {
        System.out.println(&quot;동물이 소리를 냅니다.&quot;);
    }
}</code></pre>
<p>자식 클래스에서 부모 메서드를 자신의 동작에 맞게 다시 정의하는 것을 <strong>메서드 오버라이딩</strong>이라고 한다.</p>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    void sound() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cat extends Animal {

    @Override
    void sound() {
        System.out.println(&quot;야옹&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class OverrideExample {

    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.sound();
        cat.sound();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">멍멍
야옹</code></pre>
<hr>
<h2 id="오버라이딩의-조건">오버라이딩의 조건</h2>
<p>부모 메서드를 오버라이딩하려면 기본적으로 다음 조건을 만족해야 한다.</p>
<ul>
<li>메서드 이름이 같아야 한다.</li>
<li>매개변수의 타입, 개수, 순서가 같아야 한다.</li>
<li>반환 타입이 같거나 부모 반환 타입의 하위 타입이어야 한다.</li>
<li>접근 제한 범위는 부모 메서드보다 좁아질 수 없다.</li>
<li>부모 메서드보다 더 넓은 범위의 검사 예외를 선언할 수 없다.</li>
<li><code>final</code>, <code>private</code>, <code>static</code> 메서드는 일반적인 인스턴스 메서드 오버라이딩 대상이 아니다.</li>
</ul>
<p>반환 타입은 반드시 완전히 같아야 하는 것은 아니다. 참조 타입이라면 부모 반환 타입의 하위 타입으로 변경할 수 있으며, 이를 공변 반환 타입이라고 한다.</p>
<pre><code class="language-java">public class Animal {

    Animal create() {
        return new Animal();
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    Dog create() {
        return new Dog();
    }
}</code></pre>
<p><code>Dog</code>는 <code>Animal</code>의 하위 타입이기 때문에 허용된다.</p>
<hr>
<h2 id="override-어노테이션">@Override 어노테이션</h2>
<p>오버라이딩한 메서드 위에는 <code>@Override</code>를 작성하는 것이 좋다.</p>
<pre><code class="language-java">@Override
void sound() {
    System.out.println(&quot;멍멍&quot;);
}</code></pre>
<p><code>@Override</code>는 컴파일러에게 해당 메서드가 부모 클래스나 인터페이스의 메서드를 재정의한 것임을 알려준다.</p>
<p>메서드 이름이나 매개변수를 잘못 작성하면 컴파일러가 오류를 알려준다.</p>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    void sounds() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<p>부모 클래스에 <code>sounds()</code>라는 메서드가 없다면 컴파일 오류가 발생한다.</p>
<p><code>@Override</code>가 없다면 새로운 메서드를 선언한 것으로 처리될 수 있기 때문에 오버라이딩할 때는 항상 작성하는 것이 안전하다.</p>
<hr>
<h2 id="오버로딩과-오버라이딩">오버로딩과 오버라이딩</h2>
<p>이름이 비슷하지만 완전히 다른 개념이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>오버로딩</th>
<th>오버라이딩</th>
</tr>
</thead>
<tbody><tr>
<td>위치</td>
<td>주로 같은 클래스</td>
<td>부모와 자식 클래스</td>
</tr>
<tr>
<td>메서드 이름</td>
<td>같음</td>
<td>같음</td>
</tr>
<tr>
<td>매개변수</td>
<td>달라야 함</td>
<td>같아야 함</td>
</tr>
<tr>
<td>목적</td>
<td>같은 기능을 다양한 인수로 제공</td>
<td>부모 기능을 자식에게 맞게 재정의</td>
</tr>
</tbody></table>
<pre><code class="language-java">// 오버로딩
int add(int left, int right) {
    return left + right;
}

double add(double left, double right) {
    return left + right;
}</code></pre>
<pre><code class="language-java">// 오버라이딩
@Override
void sound() {
    System.out.println(&quot;멍멍&quot;);
}</code></pre>
<hr>
<h2 id="super로-부모-멤버-사용하기">super로 부모 멤버 사용하기</h2>
<p><code>this</code>가 현재 객체를 가리킨다면 <code>super</code>는 현재 객체의 부모 클래스 부분을 가리킨다.</p>
<p>부모 메서드를 오버라이딩한 뒤 기존 기능도 함께 사용하고 싶다면 <code>super.메서드()</code>를 호출할 수 있다.</p>
<pre><code class="language-java">public class Animal {

    void introduce() {
        System.out.println(&quot;동물입니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    @Override
    void introduce() {
        super.introduce();
        System.out.println(&quot;그중에서도 강아지입니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class SuperExample {

    public static void main(String[] args) {
        Dog dog = new Dog();

        dog.introduce();
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">동물입니다.
그중에서도 강아지입니다.</code></pre>
<p>부모 메서드의 기존 기능을 재사용하면서 자식 기능을 추가할 수 있다.</p>
<hr>
<h2 id="this와-super">this와 super</h2>
<pre><code class="language-java">this.name</code></pre>
<p>현재 객체의 멤버에 접근한다.</p>
<pre><code class="language-java">super.name</code></pre>
<p>부모 클래스에 선언된 멤버에 접근한다.</p>
<p>부모와 자식 클래스에 같은 이름의 필드가 있다면 <code>this</code>와 <code>super</code>를 통해 구분할 수 있다.</p>
<pre><code class="language-java">public class Parent {

    String name = &quot;부모 필드&quot;;
}</code></pre>
<pre><code class="language-java">public class Child extends Parent {

    String name = &quot;자식 필드&quot;;

    void printNames() {
        System.out.println(this.name);
        System.out.println(super.name);
    }
}</code></pre>
<p>다만 부모와 자식 클래스에 같은 이름의 필드를 중복해서 선언하는 것은 혼란을 만들 수 있으므로 특별한 이유가 없다면 피하는 것이 좋다.</p>
<hr>
<h2 id="super와-부모-생성자">super()와 부모 생성자</h2>
<p><code>this()</code>가 같은 클래스의 다른 생성자를 호출한다면 <code>super()</code>는 부모 클래스의 생성자를 호출한다.</p>
<pre><code class="language-java">public class Animal {

    String name;

    Animal(String name) {
        this.name = name;
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    String breed;

    Dog(String name, String breed) {
        super(name);
        this.breed = breed;
    }
}</code></pre>
<pre><code class="language-java">public class ConstructorExample {

    public static void main(String[] args) {
        Dog dog = new Dog(&quot;초코&quot;, &quot;푸들&quot;);

        System.out.println(dog.name);
        System.out.println(dog.breed);
    }
}</code></pre>
<p>부모 클래스에 선언된 필드는 부모 생성자에서 초기화하고, 자식 클래스에 추가된 필드는 자식 생성자에서 초기화하는 구조다.</p>
<hr>
<h2 id="super-사용-규칙">super() 사용 규칙</h2>
<p><code>super()</code>는 자식 생성자의 첫 번째 실행문으로만 작성할 수 있다.</p>
<pre><code class="language-java">Dog(String name, String breed) {
    super(name);
    this.breed = breed;
}</code></pre>
<p>생성자의 첫 번째 줄에는 <code>this()</code> 또는 <code>super()</code> 중 하나만 올 수 있다.</p>
<pre><code class="language-text">this()
→ 같은 클래스의 다른 생성자 호출

super()
→ 부모 클래스의 생성자 호출</code></pre>
<p>명시적으로 작성하지 않으면 컴파일러가 매개변수가 없는 <code>super()</code>를 자동으로 삽입한다.</p>
<pre><code class="language-java">Dog() {
    super();
}</code></pre>
<p>따라서 부모 클래스에 매개변수가 없는 생성자가 없으면 자식 클래스에서 부모 생성자를 직접 호출해야 한다.</p>
<pre><code class="language-java">public class Animal {

    Animal(String name) {
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    Dog() {
        super(&quot;이름 없음&quot;);
    }
}</code></pre>
<p>자식 객체를 생성하면 부모 클래스부터 <code>Object</code>까지 생성자가 위에서부터 순서대로 실행된다.</p>
<pre><code class="language-text">Object 생성자
→ 부모 생성자
→ 자식 생성자</code></pre>
<p>정확히는 별도의 부모 객체와 자식 객체가 각각 따로 생성되는 것이 아니라, 하나의 자식 객체 내부에서 부모 클래스가 담당하는 부분부터 순서대로 초기화된다고 이해하는 것이 좋다.</p>
<hr>
<h1 id="클래스의-상속-범위와-구조-제한하기">클래스의 상속 범위와 구조 제한하기</h1>
<h2 id="final">final</h2>
<p><code>final</code>은 대상에 따라 “더 이상 변경하거나 확장할 수 없다”는 의미를 부여한다.</p>
<hr>
<h3 id="final-변수">final 변수</h3>
<p><code>final</code> 변수는 한 번 값을 저장한 뒤 다시 변경할 수 없다.</p>
<pre><code class="language-java">final int maxCount = 100;

// maxCount = 200;
// 컴파일 오류</code></pre>
<p>상수는 일반적으로 <code>static final</code>과 함께 선언한다.</p>
<pre><code class="language-java">public class MathConstants {

    public static final double PI = 3.141592;
}</code></pre>
<pre><code class="language-java">System.out.println(MathConstants.PI);</code></pre>
<p><code>final</code> 필드가 객체마다 다른 값을 가져야 한다면 생성자에서 초기화할 수 있다. 이를 빈 final 필드 또는 blank final field라고 한다.</p>
<pre><code class="language-java">public class Member {

    private final String id;

    public Member(String id) {
        this.id = id;
    }
}</code></pre>
<p>각 객체는 생성될 때 서로 다른 <code>id</code>를 한 번만 저장할 수 있다.</p>
<hr>
<h3 id="final-메서드">final 메서드</h3>
<p><code>final</code> 메서드는 자식 클래스에서 오버라이딩할 수 없다.</p>
<pre><code class="language-java">public class Account {

    final void printSecurityMessage() {
        System.out.println(&quot;보안 기능을 실행합니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class SavingsAccount extends Account {

    // 오버라이딩할 수 없음
    // @Override
    // void printSecurityMessage() {
    // }
}</code></pre>
<p>핵심 동작을 자식 클래스가 변경하지 못하도록 막을 때 사용할 수 있다.</p>
<hr>
<h3 id="final-클래스">final 클래스</h3>
<p><code>final</code> 클래스는 다른 클래스가 상속할 수 없다.</p>
<pre><code class="language-java">public final class Utility {
}</code></pre>
<pre><code class="language-java">// 상속할 수 없음
// public class ChildUtility extends Utility {
// }</code></pre>
<p>대표적인 <code>final</code> 클래스에는 <code>String</code>과 여러 불변 클래스가 있다.</p>
<p>다만 <code>Math</code> 클래스는 <code>final</code> 클래스이기도 하지만 생성자도 <code>private</code>으로 선언되어 객체 생성을 막고 정적 기능만 제공한다.</p>
<hr>
<h2 id="sealed-class">sealed class</h2>
<p><code>final</code> 클래스는 모든 상속을 금지한다.</p>
<p>하지만 특정 클래스에게만 상속을 허용하고 그 외 클래스의 상속은 막고 싶을 수 있다. 이때 봉인 클래스인 <code>sealed class</code>를 사용할 수 있다.</p>
<pre><code class="language-java">public sealed class Payment
        permits CardPayment, CashPayment {
}</code></pre>
<p><code>Payment</code>는 <code>CardPayment</code>와 <code>CashPayment</code>만 직접 상속할 수 있다.</p>
<p>허용된 자식 클래스는 반드시 다음 중 하나로 선언해야 한다.</p>
<ul>
<li><code>final</code></li>
<li><code>sealed</code></li>
<li><code>non-sealed</code></li>
</ul>
<hr>
<h3 id="final-자식-클래스">final 자식 클래스</h3>
<p>더 이상의 상속을 허용하지 않는다.</p>
<pre><code class="language-java">public final class CashPayment extends Payment {
}</code></pre>
<hr>
<h3 id="sealed-자식-클래스">sealed 자식 클래스</h3>
<p>자신의 자식 클래스도 제한한다.</p>
<pre><code class="language-java">public sealed class CardPayment extends Payment
        permits CreditCardPayment {
}</code></pre>
<pre><code class="language-java">public final class CreditCardPayment
        extends CardPayment {
}</code></pre>
<hr>
<h3 id="non-sealed-자식-클래스">non-sealed 자식 클래스</h3>
<p>부모 클래스에서 적용받은 봉인을 해제한다.</p>
<pre><code class="language-java">public non-sealed class CardPayment extends Payment {
}</code></pre>
<p>이제 다른 클래스가 <code>CardPayment</code>를 자유롭게 상속할 수 있다.</p>
<pre><code class="language-java">public class CreditCardPayment extends CardPayment {
}</code></pre>
<hr>
<h2 id="sealed-class를-사용하는-이유">sealed class를 사용하는 이유</h2>
<p>봉인 클래스는 상속 계층을 명확하게 제한할 수 있다.</p>
<pre><code class="language-text">Payment
├─ CardPayment
└─ CashPayment</code></pre>
<p>결제 방식, 사용자 권한, 명령 종류처럼 가능한 하위 타입이 명확하게 제한되어야 하는 모델에 적합하다.</p>
<pre><code class="language-java">public sealed interface UserRole
        permits Admin, Member, Guest {
}</code></pre>
<p>의도하지 않은 클래스가 상속 구조에 들어오는 것을 막을 수 있으며, 클래스 계층을 분석하기도 쉬워진다.</p>
<blockquote>
<p>💡 <code>sealed class</code>는 Java 17에서 정식 기능으로 도입되었다.</p>
</blockquote>
<p><code>permits</code>는 항상 반드시 직접 작성해야 하는 것은 아니다. 허용할 자식 클래스가 같은 소스 파일에 선언되어 있다면 컴파일러가 이를 추론할 수 있어 생략할 수 있다.</p>
<pre><code class="language-java">public sealed class Shape {
}

final class Circle extends Shape {
}

final class Rectangle extends Shape {
}</code></pre>
<hr>
<h1 id="패키지와-접근-범위-관리하기">패키지와 접근 범위 관리하기</h1>
<h2 id="패키지란">패키지란?</h2>
<p>프로그램의 클래스가 많아지면 관련된 클래스를 목적에 따라 분류할 필요가 있다.</p>
<p>컴퓨터에서 파일을 폴더로 나누어 관리하는 것처럼 Java에서는 클래스를 패키지로 나누어 관리한다.</p>
<pre><code class="language-text">com.example
├─ member
│  ├─ Member.java
│  └─ MemberService.java
│
├─ order
│  ├─ Order.java
│  └─ OrderService.java
│
└─ product
   ├─ Product.java
   └─ ProductService.java</code></pre>
<p>패키지는 클래스를 분류하는 역할뿐만 아니라 클래스의 전체 이름을 구성한다.</p>
<pre><code class="language-java">com.example.member.Member</code></pre>
<p>이처럼 패키지 이름과 클래스 이름을 모두 포함한 이름을 Fully Qualified Name, 즉 완전한 클래스 이름이라고 한다.</p>
<hr>
<h2 id="패키지-선언">패키지 선언</h2>
<p>패키지는 소스 파일의 주석과 공백을 제외한 첫 번째 문장에 선언한다.</p>
<pre><code class="language-java">package com.example.member;

public class Member {
}</code></pre>
<p>하나의 소스 파일에는 하나의 패키지만 선언할 수 있다.</p>
<p>패키지를 선언하지 않은 클래스는 이름 없는 기본 패키지에 속하지만, 실제 프로젝트에서는 기본 패키지를 사용하지 않는 것이 좋다.</p>
<hr>
<h2 id="패키지-이름-작성-규칙">패키지 이름 작성 규칙</h2>
<p>패키지 이름은 일반적으로 모두 소문자로 작성한다.</p>
<p>회사나 조직의 도메인을 역순으로 사용하고 프로젝트와 기능을 이어 붙이는 방식이 널리 사용된다.</p>
<pre><code class="language-text">com.company.project.member
com.company.project.order
org.example.application.product</code></pre>
<p>예를 들어 다음과 같이 구성할 수 있다.</p>
<pre><code class="language-text">com.ssafy.project.member
com.ssafy.project.order
com.ssafy.project.common</code></pre>
<hr>
<h2 id="import">import</h2>
<p>현재 클래스와 다른 패키지에 있는 클래스를 짧은 이름으로 사용하려면 <code>import</code>를 작성한다.</p>
<pre><code class="language-java">package com.example.order;

import com.example.member.Member;

public class Order {

    Member member;
}</code></pre>
<p><code>import</code>를 사용하지 않으면 완전한 클래스 이름을 작성해야 한다.</p>
<pre><code class="language-java">public class Order {

    com.example.member.Member member;
}</code></pre>
<hr>
<h2 id="import-작성-위치">import 작성 위치</h2>
<p><code>import</code>는 패키지 선언과 클래스 선언 사이에 작성한다.</p>
<pre><code class="language-java">package com.example.order;

import com.example.member.Member;
import java.util.ArrayList;
import java.util.List;

public class Order {
}</code></pre>
<p>패키지 선언은 하나만 가능하지만 <code>import</code>는 여러 개 작성할 수 있다.</p>
<hr>
<h2 id="와일드카드-import">와일드카드 import</h2>
<p>특정 패키지의 여러 클래스를 가져오려면 <code>*</code>를 사용할 수 있다.</p>
<pre><code class="language-java">import java.util.*;</code></pre>
<p>다만 <code>*</code>는 해당 패키지에 직접 포함된 클래스만 가져온다. 하위 패키지까지 가져오지는 않는다.</p>
<pre><code class="language-java">import java.util.*;</code></pre>
<p>위 코드는 다음 하위 패키지의 클래스까지 가져오지 않는다.</p>
<pre><code class="language-text">java.util.concurrent
java.util.function
java.util.stream</code></pre>
<p>또한 와일드카드 import는 어떤 클래스를 사용하는지 한눈에 파악하기 어려울 수 있어 IDE가 생성하는 개별 import를 유지하는 경우가 많다.</p>
<hr>
<h2 id="이름이-같은-클래스-사용하기">이름이 같은 클래스 사용하기</h2>
<p>서로 다른 패키지에 이름이 같은 클래스가 존재할 수 있다.</p>
<pre><code class="language-text">java.util.Date
java.sql.Date</code></pre>
<p>두 클래스를 동시에 사용해야 한다면 하나는 완전한 클래스 이름으로 작성해야 한다.</p>
<pre><code class="language-java">import java.util.Date;

public class DateExample {

    Date utilDate;
    java.sql.Date sqlDate;
}</code></pre>
<hr>
<h2 id="기본으로-import되는-패키지">기본으로 import되는 패키지</h2>
<p><code>java.lang</code> 패키지는 별도로 import하지 않아도 자동으로 사용할 수 있다.</p>
<pre><code class="language-java">String
System
Math
Object
Integer</code></pre>
<p>다음 import는 명시적으로 작성하지 않아도 된다.</p>
<pre><code class="language-java">import java.lang.*;</code></pre>
<p>Eclipse 계열 IDE에서는 일반적으로 <code>Ctrl + Shift + O</code>를 이용해 import를 자동으로 정리할 수 있다.</p>
<hr>
<h1 id="접근-제한자로-객체-보호하기">접근 제한자로 객체 보호하기</h1>
<h2 id="제한자란">제한자란?</h2>
<p>제한자는 클래스, 필드, 생성자, 메서드 선언부에 추가적인 의미를 부여하는 키워드다.</p>
<p>제한자는 크게 접근 제한자와 그 외 제한자로 나눌 수 있다.</p>
<pre><code class="language-text">접근 제한자
- public
- protected
- default
- private

그 외 제한자
- static
- final
- abstract
- synchronized
- ...</code></pre>
<p>하나의 선언에 여러 제한자를 함께 사용할 수 있다.</p>
<pre><code class="language-java">public static final int MAX_COUNT = 100;</code></pre>
<p>순서는 문법적으로 크게 중요하지 않지만 일반적으로 접근 제한자를 가장 앞에 작성한다.</p>
<hr>
<h2 id="접근-제한자-비교">접근 제한자 비교</h2>
<p>접근 제한자는 해당 클래스나 멤버를 어느 범위까지 사용할 수 있는지 결정한다.</p>
<table>
<thead>
<tr>
<th>접근 제한자</th>
<th align="right">같은 클래스</th>
<th align="right">같은 패키지</th>
<th align="right">다른 패키지의 자식 클래스</th>
<th align="right">그 외</th>
</tr>
</thead>
<tbody><tr>
<td><code>public</code></td>
<td align="right">가능</td>
<td align="right">가능</td>
<td align="right">가능</td>
<td align="right">가능</td>
</tr>
<tr>
<td><code>protected</code></td>
<td align="right">가능</td>
<td align="right">가능</td>
<td align="right">조건부 가능</td>
<td align="right">불가능</td>
</tr>
<tr>
<td>default</td>
<td align="right">가능</td>
<td align="right">가능</td>
<td align="right">불가능</td>
<td align="right">불가능</td>
</tr>
<tr>
<td><code>private</code></td>
<td align="right">가능</td>
<td align="right">불가능</td>
<td align="right">불가능</td>
<td align="right">불가능</td>
</tr>
</tbody></table>
<p>default는 실제 키워드를 작성하는 것이 아니라 접근 제한자를 생략한 상태다.</p>
<pre><code class="language-java">class Member {
}</code></pre>
<pre><code class="language-java">void print() {
}</code></pre>
<hr>
<h2 id="public">public</h2>
<p><code>public</code>은 어디에서든 접근할 수 있다.</p>
<pre><code class="language-java">public class Member {

    public void introduce() {
        System.out.println(&quot;회원입니다.&quot;);
    }
}</code></pre>
<p>다른 패키지에서도 import한 뒤 사용할 수 있다.</p>
<hr>
<h2 id="private">private</h2>
<p><code>private</code>은 해당 클래스 내부에서만 접근할 수 있다.</p>
<pre><code class="language-java">public class BankAccount {

    private long balance;

    public void deposit(long amount) {
        if (amount &gt; 0) {
            balance += amount;
        }
    }

    public long getBalance() {
        return balance;
    }
}</code></pre>
<p>외부 코드가 <code>balance</code>를 직접 음수로 변경하지 못하도록 보호할 수 있다.</p>
<pre><code class="language-java">BankAccount account = new BankAccount();

// account.balance = -100_000;
// 접근 불가</code></pre>
<p>이처럼 데이터를 외부에 직접 노출하지 않고 메서드를 통해 제어하는 것을 캡슐화라고 한다.</p>
<hr>
<h2 id="default">default</h2>
<p>접근 제한자를 생략하면 같은 패키지 내부에서만 접근할 수 있다.</p>
<pre><code class="language-java">class PackageMember {

    void print() {
        System.out.println(&quot;같은 패키지에서 사용 가능&quot;);
    }
}</code></pre>
<p>패키지 내부에서만 사용하는 클래스나 기능에 적합하다.</p>
<hr>
<h2 id="protected">protected</h2>
<p><code>protected</code>는 같은 패키지에서는 default와 비슷하게 접근할 수 있다.</p>
<p>다른 패키지에서는 해당 클래스를 상속한 자식 클래스가 상속 관계를 통해 접근할 수 있다.</p>
<pre><code class="language-java">package parent;

public class Parent {

    protected int value = 10;
}</code></pre>
<pre><code class="language-java">package child;

import parent.Parent;

public class Child extends Parent {

    void printValue() {
        System.out.println(value);
    }
}</code></pre>
<p>다른 패키지의 일반 클래스에서는 사용할 수 없다.</p>
<p><code>protected</code>는 “어디서나 자식 객체를 통해 접근 가능하다”는 단순한 의미가 아니며, 다른 패키지에서는 상속받은 자식 클래스 내부에서 상속 관계를 통해 사용해야 한다.</p>
<hr>
<h2 id="오버라이딩과-접근-제한자">오버라이딩과 접근 제한자</h2>
<p>자식 클래스에서 메서드를 오버라이딩할 때 접근 범위를 부모보다 좁게 만들 수 없다.</p>
<pre><code class="language-java">public class Parent {

    protected void print() {
    }
}</code></pre>
<p>다음 오버라이딩은 가능하다.</p>
<pre><code class="language-java">public class Child extends Parent {

    @Override
    public void print() {
    }
}</code></pre>
<p><code>protected</code>보다 넓은 <code>public</code>으로 변경했기 때문이다.</p>
<p>다음 코드는 사용할 수 없다.</p>
<pre><code class="language-java">public class Child extends Parent {

    // 접근 범위를 좁힐 수 없음
    // @Override
    // private void print() {
    // }
}</code></pre>
<p>부모 타입으로 사용할 수 있었던 기능이 자식 객체에서는 갑자기 사용할 수 없게 되면 다형성의 일관성이 깨질 수 있기 때문이다.</p>
<hr>
<h1 id="어노테이션으로-코드에-정보-추가하기">어노테이션으로 코드에 정보 추가하기</h1>
<h2 id="어노테이션이란">어노테이션이란?</h2>
<p>어노테이션은 소스 코드에 추가하는 메타데이터다.</p>
<p>사전적으로는 주석이라는 의미지만 일반 주석과 달리 컴파일러, JVM, 프레임워크 등이 읽고 활용할 수 있다.</p>
<pre><code class="language-java">@Override
public String toString() {
    return &quot;Member&quot;;
}</code></pre>
<p>어노테이션은 코드 자체의 동작을 직접 작성하기보다 코드에 대한 정보나 처리 방법을 추가한다.</p>
<p>대표적인 기본 어노테이션은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Override</code></td>
<td>오버라이딩한 메서드임을 표시</td>
</tr>
<tr>
<td><code>@Deprecated</code></td>
<td>더 이상 사용을 권장하지 않는 요소 표시</td>
</tr>
<tr>
<td><code>@SuppressWarnings</code></td>
<td>특정 컴파일 경고 억제</td>
</tr>
<tr>
<td><code>@FunctionalInterface</code></td>
<td>함수형 인터페이스임을 검증</td>
</tr>
</tbody></table>
<hr>
<h2 id="deprecated">@Deprecated</h2>
<p>더 이상 사용을 권장하지 않는 클래스나 메서드에 사용한다.</p>
<pre><code class="language-java">public class LegacyService {

    @Deprecated
    public void oldMethod() {
        System.out.println(&quot;이전 방식&quot;);
    }

    public void newMethod() {
        System.out.println(&quot;새로운 방식&quot;);
    }
}</code></pre>
<p>기존 코드와의 호환성을 위해 메서드는 남겨두되 새로운 코드에서는 사용하지 않도록 안내할 수 있다.</p>
<hr>
<h2 id="suppresswarnings">@SuppressWarnings</h2>
<p>특정 경고를 의도적으로 무시할 때 사용한다.</p>
<pre><code class="language-java">@SuppressWarnings(&quot;deprecation&quot;)
public void useLegacyMethod() {
    LegacyService service = new LegacyService();
    service.oldMethod();
}</code></pre>
<p>경고가 불편하다는 이유로 무조건 사용하는 것이 아니라, 경고가 발생하는 원인을 이해하고 의도적으로 무시해도 안전할 때만 사용해야 한다.</p>
<hr>
<h1 id="종합-예제-회원-등급-상속-구조">종합 예제: 회원 등급 상속 구조</h1>
<p>지금까지 배운 내용을 이용해 회원 등급을 표현해보자.</p>
<h2 id="member-클래스">Member 클래스</h2>
<pre><code class="language-java">package member;

public class Member {

    private final String id;
    private String name;

    public Member(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public void printInfo() {
        System.out.println(&quot;아이디: &quot; + id);
        System.out.println(&quot;이름: &quot; + name);
    }

    public int calculatePoint(int price) {
        return price / 100;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    protected void setName(String name) {
        this.name = name;
    }
}</code></pre>
<p><code>id</code>는 객체 생성 후 변경되어서는 안 되므로 <code>final</code>로 선언했다.</p>
<p>외부에서 필드에 직접 접근하지 못하도록 <code>private</code>으로 보호하고 필요한 기능만 메서드로 제공한다.</p>
<hr>
<h2 id="vipmember-클래스">VipMember 클래스</h2>
<pre><code class="language-java">package member;

public final class VipMember extends Member {

    private final double pointRate;

    public VipMember(
            String id,
            String name,
            double pointRate
    ) {
        super(id, name);
        this.pointRate = pointRate;
    }

    @Override
    public int calculatePoint(int price) {
        return (int) (price * pointRate);
    }

    @Override
    public void printInfo() {
        super.printInfo();
        System.out.println(&quot;회원 등급: VIP&quot;);
        System.out.printf(
                &quot;적립률: %.1f%%%n&quot;,
                pointRate * 100
        );
    }
}</code></pre>
<p><code>VipMember</code>는 <code>Member</code>를 상속한다.</p>
<p>부모의 생성자는 <code>super()</code>를 이용해 호출하고, 포인트 계산 방식은 VIP 회원에게 맞게 오버라이딩한다.</p>
<p><code>printInfo()</code>에서는 부모 메서드를 <code>super.printInfo()</code>로 재사용한 뒤 VIP 정보를 추가로 출력한다.</p>
<hr>
<h2 id="실행-클래스">실행 클래스</h2>
<pre><code class="language-java">package app;

import member.Member;
import member.VipMember;

public class MemberExample {

    public static void main(String[] args) {
        Member normalMember = new Member(
                &quot;member01&quot;,
                &quot;김자바&quot;
        );

        VipMember vipMember = new VipMember(
                &quot;vip01&quot;,
                &quot;이자바&quot;,
                0.05
        );

        normalMember.printInfo();
        System.out.println(
                &quot;적립 포인트: &quot;
                        + normalMember.calculatePoint(100_000)
        );

        System.out.println();

        vipMember.printInfo();
        System.out.println(
                &quot;적립 포인트: &quot;
                        + vipMember.calculatePoint(100_000)
        );
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">아이디: member01
이름: 김자바
적립 포인트: 1000

아이디: vip01
이름: 이자바
회원 등급: VIP
적립률: 5.0%
적립 포인트: 5000</code></pre>
<p>이 예제에는 다음 개념이 사용되었다.</p>
<ul>
<li><code>extends</code>를 이용한 상속</li>
<li><code>super()</code>를 이용한 부모 생성자 호출</li>
<li><code>@Override</code>를 이용한 메서드 재정의</li>
<li><code>super.메서드()</code>를 이용한 부모 기능 재사용</li>
<li><code>private</code>을 이용한 데이터 은닉</li>
<li><code>protected</code> 멤버</li>
<li><code>final</code> 필드와 클래스</li>
<li>패키지와 import</li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="상속과-객체-생성">상속과 객체 생성</h2>
<p>자식 객체를 생성한다고 해서 부모 객체와 자식 객체가 완전히 별개로 두 개 생성되는 것은 아니다.</p>
<pre><code class="language-java">Dog dog = new Dog();</code></pre>
<p>하나의 <code>Dog</code> 객체 안에 <code>Animal</code>로부터 물려받은 부분과 <code>Dog</code>가 직접 선언한 부분이 함께 존재한다.</p>
<p>다만 초기화 순서는 부모 클래스부터 자식 클래스 순서로 진행된다.</p>
<hr>
<h2 id="상속과-접근">상속과 접근</h2>
<p>상속받았다고 해서 부모 클래스의 모든 멤버에 직접 접근할 수 있는 것은 아니다.</p>
<pre><code class="language-java">private String name;</code></pre>
<p><code>private</code> 멤버는 부모 클래스 내부에서만 접근할 수 있다.</p>
<p>자식 클래스가 사용해야 한다면 부모 클래스가 <code>protected</code> 또는 <code>public</code> 메서드를 제공해야 한다.</p>
<hr>
<h2 id="this와-super-1">this()와 super()</h2>
<pre><code class="language-java">this(...)</code></pre>
<p>같은 클래스의 다른 생성자를 호출한다.</p>
<pre><code class="language-java">super(...)</code></pre>
<p>부모 클래스의 생성자를 호출한다.</p>
<p>둘 다 생성자의 첫 번째 실행문에만 작성할 수 있으므로 동시에 직접 호출할 수 없다.</p>
<hr>
<h2 id="super와-부모-객체">super와 부모 객체</h2>
<p><code>super</code>는 별도로 생성된 부모 객체를 가리키는 변수가 아니다.</p>
<p>현재 자식 객체 안에 포함된 부모 클래스 부분의 멤버에 접근하기 위한 키워드다.</p>
<hr>
<h2 id="final과-불변-객체">final과 불변 객체</h2>
<p>필드에 <code>final</code>을 붙였다고 해서 그 객체의 내부 상태까지 모두 변경할 수 없는 것은 아니다.</p>
<pre><code class="language-java">final StringBuilder builder = new StringBuilder(&quot;Java&quot;);

builder.append(&quot; Programming&quot;);</code></pre>
<p><code>builder</code>가 다른 객체를 참조하도록 변경할 수는 없지만, 참조 중인 <code>StringBuilder</code> 객체의 내용은 변경할 수 있다.</p>
<pre><code class="language-java">// builder = new StringBuilder(&quot;Spring&quot;);
// 참조 변경은 불가능</code></pre>
<hr>
<h1 id="실무에서는-어떻게-사용할까">실무에서는 어떻게 사용할까?</h1>
<p>상속은 강력하지만 부모와 자식 클래스 사이의 결합을 강하게 만든다.</p>
<p>부모 클래스의 내부 구조가 바뀌면 여러 자식 클래스가 영향을 받을 수 있기 때문에, 실제 개발에서는 단순한 코드 재사용만을 목적으로 상속을 사용하지 않는다.</p>
<p>다음 조건을 만족할 때 상속을 고려하는 것이 좋다.</p>
<ul>
<li>자식 클래스가 부모 클래스의 진짜 하위 종류다.</li>
<li>부모 타입으로 자식 객체를 사용해도 의미가 자연스럽다.</li>
<li>부모 클래스의 계약을 자식 클래스가 유지할 수 있다.</li>
<li>오버라이딩을 통해 행위를 확장하거나 구체화할 필요가 있다.</li>
</ul>
<p>단순히 다른 클래스의 기능을 사용하고 싶다면 포함 관계가 더 적절할 수 있다.</p>
<pre><code class="language-java">public class OrderService {

    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}</code></pre>
<p>Spring에서도 서비스 클래스가 다른 서비스나 저장소 객체를 필드로 가지고 사용하는 포함 관계가 매우 많이 사용된다.</p>
<pre><code class="language-text">OrderService has a PaymentService</code></pre>
<p>또한 필드는 가능한 한 <code>private</code>으로 보호하고, 외부에는 필요한 메서드만 공개하는 것이 일반적인 설계 방식이다.</p>
<pre><code class="language-java">private long balance;

public void deposit(long amount) {
}</code></pre>
<p>상속 구조가 명확하게 제한되어야 하는 도메인에서는 <code>sealed class</code>를 활용할 수 있지만, 모든 프로젝트에서 반드시 사용할 필요는 없다. 일반적인 클래스 설계에서는 인터페이스, 조합, 의존성 주입도 함께 고려해야 한다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>이번 글에서는 클래스의 기능을 재사용하고 확장하는 Java 상속을 정리했다.</p>
<ul>
<li>상속은 부모 클래스의 필드와 메서드를 자식 클래스가 재사용하는 기능이다.</li>
<li>Java에서는 <code>extends</code> 키워드로 클래스 상속 관계를 만든다.</li>
<li>부모 클래스의 생성자와 초기화 블록은 자식 클래스에 상속되지 않는다.</li>
<li>자식 객체를 생성할 때는 부모 클래스 부분부터 먼저 초기화된다.</li>
<li>상속은 일반적으로 <code>is-a</code> 관계가 성립할 때 사용한다.</li>
<li>포함 관계는 한 객체가 다른 객체를 필드로 가지는 <code>has-a</code> 관계다.</li>
<li>Java 클래스는 하나의 부모 클래스만 직접 상속할 수 있다.</li>
<li>여러 기능이 필요할 때는 인터페이스와 포함 관계를 함께 활용할 수 있다.</li>
<li>모든 Java 클래스는 최종적으로 <code>Object</code> 클래스를 상속한다.</li>
<li>오버라이딩은 부모 메서드를 자식 클래스에 맞게 다시 정의하는 것이다.</li>
<li>오버라이딩한 메서드에는 <code>@Override</code>를 작성하는 것이 좋다.</li>
<li>오버라이딩 시 메서드 이름과 매개변수 목록이 같아야 한다.</li>
<li>참조 타입 반환값은 부모 반환 타입의 하위 타입으로 변경할 수 있다.</li>
<li>오버라이딩하는 메서드의 접근 범위는 부모보다 좁아질 수 없다.</li>
<li><code>super</code>는 부모 클래스의 멤버에 접근할 때 사용한다.</li>
<li><code>super()</code>는 부모 생성자를 호출하며 자식 생성자의 첫 번째 줄에 작성한다.</li>
<li>부모 생성자를 명시적으로 호출하지 않으면 컴파일러가 <code>super()</code>를 삽입한다.</li>
<li><code>final</code> 변수는 값을 다시 변경할 수 없다.</li>
<li><code>final</code> 메서드는 오버라이딩할 수 없다.</li>
<li><code>final</code> 클래스는 상속할 수 없다.</li>
<li><code>static final</code>은 모든 객체가 공유하는 상수를 표현할 때 사용한다.</li>
<li>봉인 클래스는 허용된 클래스만 상속하도록 제한한다.</li>
<li>봉인 클래스의 자식은 <code>final</code>, <code>sealed</code>, <code>non-sealed</code> 중 하나로 선언해야 한다.</li>
<li>패키지는 관련된 클래스를 계층적으로 분류하고 이름 충돌을 방지한다.</li>
<li>다른 패키지의 클래스를 사용할 때는 <code>import</code>를 사용할 수 있다.</li>
<li><code>java.lang</code> 패키지는 자동으로 import된다.</li>
<li>접근 제한자는 클래스와 멤버를 사용할 수 있는 범위를 결정한다.</li>
<li><code>private</code>은 클래스 내부, default는 같은 패키지, <code>protected</code>는 같은 패키지와 상속 관계, <code>public</code>은 모든 위치에서 접근할 수 있다.</li>
<li>필드를 <code>private</code>으로 보호하고 메서드를 통해 사용하도록 만드는 것이 캡슐화의 기본이다.</li>
<li>어노테이션은 컴파일러, JVM, 프레임워크가 활용할 수 있는 메타데이터다.</li>
</ul>
<hr>
<h1 id="다음-글-예고">다음 글 예고</h1>
<p>다음 글에서는 상속 관계를 활용하는 핵심 개념인 <strong>다형성</strong>을 다룬다.</p>
<pre><code class="language-text">Java 객체지향 ⑤ 다형성

- 자동 타입 변환
- 부모 타입과 자식 객체
- 메서드 오버라이딩과 동적 바인딩
- 강제 타입 변환
- instanceof
- instanceof 패턴 매칭
- 매개변수의 다형성
- 배열과 다형성</code></pre>
<p>상속이 클래스 사이의 관계를 만드는 기능이라면, 다형성은 그 관계를 실제 프로그램에서 유연하게 사용하는 방법이라고 볼 수 있다.</p>
<hr>
<h1 id="📌-작성자-참고-메모--블로그에-넣지-않아도-되는-내용">📌 작성자 참고 메모 — 블로그에 넣지 않아도 되는 내용</h1>
<h2 id="이번-글의-시리즈-위치">이번 글의 시리즈 위치</h2>
<p>이번 글은 사용자가 지정한 기준에 따라 <strong>Java 시리즈의 네 번째 글</strong>로 구성했다.</p>
<pre><code class="language-text">Java 객체지향 ① 변수와 타입 변환
Java 객체지향 ② 제어문과 배열
Java 객체지향 ③ 객체와 클래스
Java 객체지향 ④ 상속과 오버라이딩</code></pre>
<p>다만 앞선 글 제목에 이미 다른 번호가 붙어 있다면 실제 게시 시리즈 이름에 맞춰 숫자만 조정하면 된다.</p>
<hr>
<h2 id="메모에서-수정한-내용">메모에서 수정한 내용</h2>
<h3 id="object의-자식-클래스">Object의 자식 클래스</h3>
<p>메모의 “별도의 extends 선언이 없는 클래스는 <code>extends Object</code>가 생략된다”는 설명은 학습 관점에서 맞다. 다만 인터페이스는 클래스를 상속하지 않으며 <code>Object</code>를 직접 상속한다고 표현하지 않는다.</p>
<h3 id="오버라이딩-반환-타입">오버라이딩 반환 타입</h3>
<p>메모에는 “리턴 타입이 같아야 한다”고 적혀 있었지만, 정확히는 다음과 같다.</p>
<ul>
<li>기본 타입 반환값은 같아야 한다.</li>
<li>참조 타입 반환값은 부모 반환 타입과 같거나 그 하위 타입일 수 있다.</li>
</ul>
<h3 id="sealed-클래스의-permits">sealed 클래스의 permits</h3>
<p>메모에는 <code>sealed</code>가 선언된 클래스는 <code>permits</code>가 반드시 필요하다고 되어 있지만 예외가 있다.</p>
<p>허용된 자식 클래스가 같은 컴파일 단위, 즉 같은 <code>.java</code> 파일에 함께 선언되어 있으면 <code>permits</code>를 생략할 수 있다.</p>
<h3 id="sealed-철자">sealed 철자</h3>
<p>메모의 <code>seald</code>는 <code>sealed</code>로 수정했다.</p>
<h3 id="final-클래스-예시">final 클래스 예시</h3>
<p><code>String</code>은 <code>final</code> 클래스가 맞다. <code>Math</code>도 <code>final</code> 클래스지만 생성자까지 <code>private</code>이라 객체 생성을 막고 정적 유틸리티 기능을 제공한다.</p>
<h3 id="object까지-객체가-모두-만들어진다는-표현">Object까지 객체가 모두 만들어진다는 표현</h3>
<p>부모 클래스마다 독립적인 객체가 여러 개 생성되는 것은 아니다.</p>
<p>하나의 자식 객체 내부에서 부모 클래스가 담당하는 상태가 포함되고, 생성자를 통해 부모 부분부터 자식 부분까지 순서대로 초기화된다.</p>
<h3 id="모든-클래스는-패키지에-속한다는-표현">모든 클래스는 패키지에 속한다는 표현</h3>
<p>패키지 선언이 없는 클래스도 이름 없는 기본 패키지에 속한다고 볼 수 있다. 다만 실제 프로젝트에서는 기본 패키지를 권장하지 않는다.</p>
<hr>
<h2 id="면접이나-평가에서-자주-나오는-질문">면접이나 평가에서 자주 나오는 질문</h2>
<h3 id="오버로딩과-오버라이딩의-차이">오버로딩과 오버라이딩의 차이</h3>
<pre><code class="language-text">오버로딩
- 같은 이름
- 다른 매개변수
- 컴파일 시점에 선택

오버라이딩
- 상속 관계
- 같은 메서드 선언
- 자식 클래스에서 재정의
- 실행 시 실제 객체 기준으로 선택</code></pre>
<h3 id="java가-다중-상속을-지원하지-않는-이유">Java가 다중 상속을 지원하지 않는 이유</h3>
<p>동일한 메서드를 가진 여러 부모 클래스를 동시에 상속할 때 어떤 구현을 사용할지 모호해질 수 있기 때문이다. Java는 클래스 단일 상속과 인터페이스 다중 구현을 채택했다.</p>
<h3 id="private-메서드는-오버라이딩되는가">private 메서드는 오버라이딩되는가?</h3>
<p>아니다. <code>private</code> 메서드는 자식 클래스에서 보이지 않으므로 상속된 메서드를 재정의하는 오버라이딩이 성립하지 않는다. 자식 클래스에 같은 형태의 메서드를 선언해도 서로 별개의 메서드다.</p>
<h3 id="static-메서드는-오버라이딩되는가">static 메서드는 오버라이딩되는가?</h3>
<p>인스턴스 메서드처럼 오버라이딩되지 않는다. 자식 클래스가 같은 형태의 정적 메서드를 선언하면 메서드 숨김이 발생한다.</p>
<h3 id="생성자는-상속되는가">생성자는 상속되는가?</h3>
<p>상속되지 않는다. 다만 자식 객체 생성 시 <code>super()</code>를 통해 부모 생성자가 호출된다.</p>
<h3 id="final-참조-변수는-불변-객체인가">final 참조 변수는 불변 객체인가?</h3>
<p>아니다. 참조 변수가 가리키는 대상을 바꿀 수 없다는 의미일 뿐, 참조 중인 객체의 내부 상태가 변경되지 않는다는 뜻은 아니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (3) 객체, 클래스, 필드, 메서드, 생성자]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-3-%EA%B0%9D%EC%B2%B4-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%95%84%EB%93%9C-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90</link>
            <guid>https://velog.io/@tongmill_g/JAVA-3-%EA%B0%9D%EC%B2%B4-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%95%84%EB%93%9C-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90</guid>
            <pubDate>Tue, 21 Jul 2026 02:55:16 GMT</pubDate>
            <description><![CDATA[<h1 id="java-객체는-어떻게-만들어질까-클래스부터-생성자까지">Java 객체는 어떻게 만들어질까? 클래스부터 생성자까지</h1>
<h2 id="들어가며">들어가며</h2>
<p>지금까지 변수, 조건문, 반복문, 배열을 이용하면 비교적 간단한 프로그램을 만들 수 있었다.</p>
<p>하지만 프로그램의 규모가 커지면 수많은 변수와 메서드가 서로 뒤섞이기 시작한다. 회원의 이름, 나이, 이메일을 각각 다른 변수로 관리하고, 자동차의 속도나 색상도 별도의 변수로 흩어놓는다면 데이터와 기능의 관계를 파악하기 어려워진다.</p>
<p>객체지향 프로그래밍은 이 문제를 해결하기 위해 <strong>서로 관련된 데이터와 기능을 하나의 객체로 묶어 관리하는 방식</strong>이다.</p>
<p>예를 들어 자동차를 프로그램으로 표현한다면 자동차가 가진 데이터와 기능을 다음처럼 나눌 수 있다.</p>
<pre><code class="language-text">자동차 객체

속성
- 모델명
- 색상
- 현재 속도

동작
- 시동 걸기
- 가속하기
- 정지하기</code></pre>
<p>Java에서는 객체가 가져야 하는 속성과 동작을 클래스에 정의하고, 그 클래스를 기반으로 실제 객체를 생성한다.</p>
<p>이번 글에서는 객체와 클래스의 관계부터 필드, 메서드, 생성자, <code>this</code>, <code>static</code>, 메서드 오버로딩, 초기화 블록까지 정리해보려고 한다.</p>
<hr>
<h1 id="1-객체란">1. 객체란?</h1>
<p>객체는 <strong>물리적으로 존재하거나 추상적으로 생각할 수 있는 대상 중에서 자신의 속성을 가지며 다른 대상과 구분할 수 있는 것</strong>을 의미한다.</p>
<p>물리적으로 존재하지 않더라도 프로그램에서 하나의 대상으로 구분할 수 있다면 객체로 표현할 수 있다.</p>
<p>예를 들어 주문은 손으로 만질 수 있는 물건은 아니지만 다음과 같은 속성과 동작을 가진다.</p>
<pre><code class="language-text">주문

속성
- 주문 번호
- 주문자
- 주문 금액
- 주문 상태

동작
- 결제하기
- 취소하기
- 배송 상태 변경하기</code></pre>
<hr>
<h2 id="객체는-속성과-동작으로-구성된다">객체는 속성과 동작으로 구성된다</h2>
<p>객체는 크게 속성과 동작으로 구성된다.</p>
<pre><code class="language-text">객체 = 속성 + 동작</code></pre>
<p>Java에서는 속성을 <strong>필드</strong>, 동작을 <strong>메서드</strong>로 표현한다.</p>
<pre><code class="language-java">public class Car {

    // 속성
    String model;
    String color;
    int speed;

    // 동작
    void accelerate() {
        speed += 10;
    }

    void stop() {
        speed = 0;
    }
}</code></pre>
<p>위 클래스에서 <code>model</code>, <code>color</code>, <code>speed</code>는 자동차 객체의 데이터를 나타내는 필드이고, <code>accelerate()</code>와 <code>stop()</code>은 자동차 객체가 수행할 수 있는 동작을 나타내는 메서드다.</p>
<hr>
<h1 id="2-객체의-상호작용">2. 객체의 상호작용</h1>
<p>실제 프로그램은 하나의 객체만으로 구성되지 않는다.</p>
<p>회원 객체가 주문 객체를 생성하고, 주문 객체가 결제 객체의 기능을 호출하며, 결제 객체는 은행 시스템과 통신할 수 있다.</p>
<p>객체들은 주로 메서드를 호출하면서 서로 상호작용한다.</p>
<pre><code class="language-text">회원 객체
    ↓ 주문 요청
주문 객체
    ↓ 결제 요청
결제 객체</code></pre>
<p>객체가 다른 객체의 기능을 이용하는 것을 메서드 호출이라고 한다.</p>
<pre><code class="language-java">payment.pay();</code></pre>
<p>여기서 <code>payment</code>는 결제 객체를 참조하는 변수이고, <code>pay()</code>는 결제 객체가 제공하는 기능이다.</p>
<hr>
<h2 id="객체-간의-관계">객체 간의 관계</h2>
<p>객체는 여러 형태의 관계를 맺을 수 있다.</p>
<h3 id="집합-관계">집합 관계</h3>
<p>집합 관계는 완성품과 부품 사이의 관계다.</p>
<pre><code class="language-text">자동차 - 엔진
컴퓨터 - CPU
주문 - 주문 상품</code></pre>
<p>자동차 객체가 엔진 객체를 필드로 가지고 있다면 집합 관계로 볼 수 있다.</p>
<pre><code class="language-java">public class Engine {

    void start() {
        System.out.println(&quot;엔진이 작동합니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Car {

    Engine engine = new Engine();

    void start() {
        engine.start();
    }
}</code></pre>
<hr>
<h3 id="사용-관계">사용 관계</h3>
<p>사용 관계는 하나의 객체가 다른 객체의 기능을 일시적으로 이용하는 관계다.</p>
<pre><code class="language-java">public class Calculator {

    int add(int left, int right) {
        return left + right;
    }
}</code></pre>
<pre><code class="language-java">public class CalculatorExample {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        int result = calculator.add(10, 20);

        System.out.println(result);
    }
}</code></pre>
<p><code>CalculatorExample</code>은 <code>Calculator</code> 객체의 <code>add()</code> 메서드를 사용한다.</p>
<hr>
<h3 id="상속-관계">상속 관계</h3>
<p>상속 관계는 부모 객체가 가진 속성과 기능을 자식 객체가 물려받는 관계다.</p>
<pre><code class="language-text">동물
├─ 강아지
├─ 고양이
└─ 새</code></pre>
<p>Java에서는 <code>extends</code> 키워드로 상속 관계를 표현한다.</p>
<pre><code class="language-java">public class Animal {

    void eat() {
        System.out.println(&quot;먹이를 먹습니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Dog extends Animal {

    void bark() {
        System.out.println(&quot;멍멍&quot;);
    }
}</code></pre>
<p>상속은 이후 글에서 더 자세하게 다루는 것이 좋다.</p>
<hr>
<h1 id="3-객체지향-프로그래밍이란">3. 객체지향 프로그래밍이란?</h1>
<p>객체지향 프로그래밍은 프로그램에서 필요한 대상들을 객체로 정의하고, 객체들이 서로 상호작용하도록 프로그램을 구성하는 방식이다.</p>
<p>예를 들어 쇼핑몰 프로그램을 만든다면 다음과 같은 객체를 설계할 수 있다.</p>
<pre><code class="language-text">회원
상품
장바구니
주문
결제
배송</code></pre>
<p>각 객체는 자신의 데이터와 기능을 책임진다.</p>
<pre><code class="language-text">회원
- 이름
- 이메일
- 로그인()

상품
- 상품명
- 가격
- 재고
- 재고감소()

주문
- 주문번호
- 주문금액
- 주문생성()
- 주문취소()</code></pre>
<p>객체지향 프로그래밍은 각각의 객체를 설계한 뒤 조립하여 전체 프로그램을 완성한다.</p>
<hr>
<h2 id="객체지향-프로그래밍의-장점">객체지향 프로그래밍의 장점</h2>
<p>객체지향 프로그래밍은 관련된 데이터와 기능을 하나의 단위로 묶기 때문에 다음과 같은 장점을 가진다.</p>
<h3 id="모듈화">모듈화</h3>
<p>프로그램을 독립적인 객체 단위로 나눌 수 있다.</p>
<p>회원 기능을 수정하더라도 주문이나 상품 코드에 미치는 영향을 줄일 수 있다.</p>
<h3 id="재사용성">재사용성</h3>
<p>이미 작성한 클래스를 여러 곳에서 다시 사용할 수 있다.</p>
<pre><code class="language-java">Calculator calculator1 = new Calculator();
Calculator calculator2 = new Calculator();</code></pre>
<p>같은 클래스를 기반으로 여러 객체를 생성할 수 있다.</p>
<h3 id="유지보수성">유지보수성</h3>
<p>특정 기능이 어디에 정의되어 있는지 찾기 쉬워지고, 코드 수정 범위를 줄일 수 있다.</p>
<h3 id="확장성">확장성</h3>
<p>기존 코드를 최대한 유지하면서 새로운 기능을 추가하기 쉬워진다.</p>
<hr>
<h1 id="4-객체지향의-핵심-특징">4. 객체지향의 핵심 특징</h1>
<p>객체지향 프로그래밍의 대표적인 특징은 다음 네 가지다.</p>
<pre><code class="language-text">추상화
캡슐화
상속
다형성</code></pre>
<p>메모에 적힌 “OOP is a PIE”는 객체지향의 특징을 기억하기 위한 표현으로 볼 수 있다.</p>
<ul>
<li>Polymorphism: 다형성</li>
<li>Inheritance: 상속</li>
<li>Encapsulation: 캡슐화</li>
</ul>
<p>여기에 Abstraction, 즉 추상화를 함께 객체지향의 핵심 특징으로 다룬다.</p>
<hr>
<h2 id="추상화">추상화</h2>
<p>추상화는 현실의 객체가 가진 수많은 특징 중에서 프로그램에 필요한 것만 선택해 표현하는 과정이다.</p>
<p>자동차에는 수많은 부품과 정보가 있지만 자동차 관리 프로그램에서 다음 정보만 필요할 수 있다.</p>
<pre><code class="language-text">모델명
색상
속도</code></pre>
<p>필요한 특징만 선택하여 클래스로 표현하는 것이 추상화다.</p>
<hr>
<h2 id="캡슐화와-데이터-보호">캡슐화와 데이터 보호</h2>
<p>캡슐화는 객체의 데이터와 기능을 하나로 묶고, 외부에서 내부 데이터를 함부로 변경하지 못하도록 보호하는 것이다.</p>
<pre><code class="language-java">public class BankAccount {

    private int balance;

    public void deposit(int amount) {
        if (amount &lt;= 0) {
            return;
        }

        balance += amount;
    }

    public int getBalance() {
        return balance;
    }
}</code></pre>
<p><code>balance</code>를 <code>private</code>으로 선언하면 외부에서 직접 변경할 수 없다.</p>
<pre><code class="language-java">BankAccount account = new BankAccount();

// account.balance = -10000;
// 직접 접근 불가</code></pre>
<p>반드시 <code>deposit()</code> 같은 메서드를 통해 정상적인 방식으로 변경해야 한다.</p>
<hr>
<h2 id="상속">상속</h2>
<p>상속은 부모 클래스가 가진 필드와 메서드를 자식 클래스가 물려받는 기능이다.</p>
<p>공통 코드를 재사용할 수 있다.</p>
<hr>
<h2 id="다형성">다형성</h2>
<p>다형성은 하나의 객체를 여러 타입으로 참조할 수 있는 특성이다.</p>
<pre><code class="language-java">Animal animal = new Dog();</code></pre>
<p>실제 객체는 <code>Dog</code>지만 부모 타입인 <code>Animal</code>로 참조할 수 있다.</p>
<p>다형성 역시 상속과 인터페이스를 학습한 뒤 자세히 다루는 것이 자연스럽다.</p>
<hr>
<h1 id="5-클래스와-객체">5. 클래스와 객체</h1>
<p>클래스는 객체를 생성하기 위한 설계도다.</p>
<p>객체는 클래스를 기반으로 실제 메모리에 생성된 실체다.</p>
<pre><code class="language-text">클래스
→ 객체를 만들기 위한 설계도

객체
→ 클래스를 바탕으로 만들어진 실제 대상</code></pre>
<p>자동차 설계도 하나로 여러 자동차를 만들 수 있는 것처럼 클래스 하나로 여러 객체를 생성할 수 있다.</p>
<pre><code class="language-java">Car firstCar = new Car();
Car secondCar = new Car();</code></pre>
<p><code>firstCar</code>와 <code>secondCar</code>는 같은 <code>Car</code> 클래스로부터 만들어졌지만 서로 다른 객체다.</p>
<hr>
<h2 id="인스턴스란">인스턴스란?</h2>
<p>인스턴스는 특정 클래스로부터 생성된 객체를 의미한다.</p>
<pre><code class="language-java">Car car = new Car();</code></pre>
<p>위에서 <code>car</code>가 참조하는 객체는 다음처럼 표현할 수 있다.</p>
<blockquote>
<p><code>car</code>는 <code>Car</code> 클래스의 인스턴스다.</p>
</blockquote>
<p>객체와 인스턴스는 비슷한 의미로 사용되지만, 인스턴스는 어떤 클래스로부터 만들어졌는지를 강조하는 표현이다.</p>
<hr>
<h1 id="6-객체지향-프로그래밍의-기본-단계">6. 객체지향 프로그래밍의 기본 단계</h1>
<p>Java에서 객체를 이용하는 과정은 다음과 같다.</p>
<pre><code class="language-text">1. 클래스 설계
2. 클래스로 객체 생성
3. 객체의 필드와 메서드 사용</code></pre>
<p>예제를 살펴보자.</p>
<hr>
<h2 id="클래스-설계">클래스 설계</h2>
<pre><code class="language-java">public class Student {

    String name;
    int age;

    void study() {
        System.out.println(name + &quot;이(가) 공부합니다.&quot;);
    }
}</code></pre>
<hr>
<h2 id="객체-생성">객체 생성</h2>
<pre><code class="language-java">Student student = new Student();</code></pre>
<hr>
<h2 id="객체-사용">객체 사용</h2>
<pre><code class="language-java">student.name = &quot;김자바&quot;;
student.age = 20;

student.study();</code></pre>
<p>전체 코드는 다음과 같다.</p>
<pre><code class="language-java">public class StudentExample {

    public static void main(String[] args) {
        Student student = new Student();

        student.name = &quot;김자바&quot;;
        student.age = 20;

        System.out.println(student.name);
        System.out.println(student.age);

        student.study();
    }
}</code></pre>
<hr>
<h1 id="7-클래스-선언">7. 클래스 선언</h1>
<p>클래스는 다음 형식으로 선언한다.</p>
<pre><code class="language-java">public class 클래스이름 {
}</code></pre>
<pre><code class="language-java">public class Student {
}</code></pre>
<p>클래스 이름은 일반적으로 첫 글자를 대문자로 작성하는 PascalCase를 사용한다.</p>
<pre><code class="language-text">Student
BankAccount
OrderItem
UserService</code></pre>
<hr>
<h2 id="클래스-이름-작성-규칙">클래스 이름 작성 규칙</h2>
<p>클래스 이름은 다음 규칙을 따라야 한다.</p>
<ul>
<li>하나 이상의 문자로 구성한다.</li>
<li>첫 글자에 숫자를 사용할 수 없다.</li>
<li><code>$</code>와 <code>_</code>를 제외한 특수문자를 사용할 수 없다.</li>
<li>공백을 사용할 수 없다.</li>
<li>Java 예약어를 사용할 수 없다.</li>
<li>대문자와 소문자를 구분한다.</li>
</ul>
<p>다음은 사용할 수 없는 클래스 이름이다.</p>
<pre><code class="language-java">// public class 1Student {}
// public class User-Info {}
// public class class {}</code></pre>
<p>문법적으로 <code>$</code>와 <code>_</code>를 사용할 수 있지만 일반적인 클래스 이름에는 사용하지 않는 것이 좋다.</p>
<hr>
<h1 id="8-new-연산자를-이용한-객체-생성">8. new 연산자를 이용한 객체 생성</h1>
<p>객체는 <code>new</code> 연산자를 사용해 생성한다.</p>
<pre><code class="language-java">new Student();</code></pre>
<p><code>new</code> 연산자는 Heap 영역에 새로운 객체를 생성하고 그 객체를 참조할 수 있는 값을 반환한다.</p>
<p>보통 객체의 참조값을 변수에 저장한다.</p>
<pre><code class="language-java">Student student;

student = new Student();</code></pre>
<p>선언과 생성을 한 문장으로 작성할 수도 있다.</p>
<pre><code class="language-java">Student student = new Student();</code></pre>
<hr>
<h2 id="객체-생성-과정">객체 생성 과정</h2>
<pre><code class="language-java">Student student = new Student();</code></pre>
<p>이 문장은 개념적으로 다음 과정을 거친다.</p>
<pre><code class="language-text">1. Student 타입의 참조 변수 student가 생성된다.
2. new Student()가 Heap 영역에 객체를 생성한다.
3. 생성자가 실행되어 객체를 초기화한다.
4. 생성된 객체의 참조값이 반환된다.
5. 참조값이 student 변수에 저장된다.</code></pre>
<p>메모리 구조는 다음과 같이 이해할 수 있다.</p>
<pre><code class="language-text">JVM Stack                    Heap

student ───────────────→ Student 객체
                          ┌───────────┐
                          │ name=null │
                          │ age=0     │
                          └───────────┘</code></pre>
<hr>
<h1 id="9-클래스의-두-가지-용도">9. 클래스의 두 가지 용도</h1>
<p>입문 단계의 Java 프로그램에서는 클래스를 두 용도로 나누어 작성하는 경우가 많다.</p>
<hr>
<h2 id="라이브러리-클래스">라이브러리 클래스</h2>
<p>라이브러리 클래스는 객체를 생성하고 사용할 수 있도록 필드, 생성자, 메서드를 제공한다.</p>
<pre><code class="language-java">public class Student {

    String name;
    int age;

    void study() {
        System.out.println(name + &quot;이(가) 공부합니다.&quot;);
    }
}</code></pre>
<hr>
<h2 id="실행-클래스">실행 클래스</h2>
<p>실행 클래스는 <code>main()</code> 메서드를 제공하여 프로그램의 시작점을 담당한다.</p>
<pre><code class="language-java">public class StudentExample {

    public static void main(String[] args) {
        Student student = new Student();

        student.name = &quot;김자바&quot;;
        student.study();
    }
}</code></pre>
<p>실제 프로젝트에서는 하나의 실행 클래스가 여러 라이브러리 클래스를 조합하여 프로그램을 실행한다.</p>
<hr>
<h1 id="10-클래스의-구성-요소">10. 클래스의 구성 요소</h1>
<p>클래스는 대표적으로 필드, 생성자, 메서드로 구성된다.</p>
<pre><code class="language-java">public class ClassName {

    // 필드
    int fieldName;

    // 생성자
    ClassName() {
    }

    // 메서드
    void methodName() {
    }
}</code></pre>
<p>각 구성 요소의 역할은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>필드</td>
<td>객체의 데이터 저장</td>
</tr>
<tr>
<td>생성자</td>
<td>객체 생성 시 초기화</td>
</tr>
<tr>
<td>메서드</td>
<td>객체의 기능과 동작 정의</td>
</tr>
</tbody></table>
<hr>
<h1 id="11-필드란">11. 필드란?</h1>
<p>필드는 객체가 가져야 하는 데이터를 저장한다.</p>
<p>대표적으로 다음과 같은 정보를 필드로 선언할 수 있다.</p>
<ul>
<li>객체의 고유 데이터</li>
<li>현재 상태</li>
<li>다른 부품 객체에 대한 참조</li>
</ul>
<pre><code class="language-java">public class Car {

    String model;
    String color;
    int speed;
    Engine engine;
}</code></pre>
<p><code>model</code>, <code>color</code>, <code>speed</code>는 자동차의 상태를 저장하고, <code>engine</code>은 자동차가 포함하는 다른 객체를 참조한다.</p>
<hr>
<h2 id="필드-선언-위치">필드 선언 위치</h2>
<p>필드는 클래스의 중괄호 내부에 선언하되, 생성자나 메서드 블록 바깥에 선언한다.</p>
<pre><code class="language-java">public class Person {

    String name; // 필드

    Person() {
        int age = 20; // 로컬 변수
    }

    void introduce() {
        String message = &quot;안녕하세요.&quot;; // 로컬 변수
    }
}</code></pre>
<p>생성자나 메서드 내부에 선언된 변수는 필드가 아니라 로컬 변수다.</p>
<hr>
<h2 id="필드와-로컬-변수의-차이">필드와 로컬 변수의 차이</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>필드</th>
<th>로컬 변수</th>
</tr>
</thead>
<tbody><tr>
<td>선언 위치</td>
<td>클래스 블록 내부</td>
<td>메서드, 생성자, 제어문 내부</td>
</tr>
<tr>
<td>소속</td>
<td>객체 또는 클래스</td>
<td>실행 중인 블록</td>
</tr>
<tr>
<td>기본값</td>
<td>자동 초기화</td>
<td>자동 초기화되지 않음</td>
</tr>
<tr>
<td>생명주기</td>
<td>객체 또는 클래스와 함께 유지</td>
<td>블록 실행 중에만 유지</td>
</tr>
</tbody></table>
<pre><code class="language-java">public class Person {

    String name; // 자동으로 null
    int age;     // 자동으로 0

    void introduce() {
        int count;

        // System.out.println(count);
        // 로컬 변수는 초기화 전 사용 불가
    }
}</code></pre>
<hr>
<h2 id="필드의-기본값">필드의 기본값</h2>
<p>필드는 명시적으로 초기화하지 않아도 타입에 맞는 기본값으로 초기화된다.</p>
<table>
<thead>
<tr>
<th>타입</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>정수 타입</td>
<td><code>0</code></td>
</tr>
<tr>
<td>실수 타입</td>
<td><code>0.0</code></td>
</tr>
<tr>
<td><code>char</code></td>
<td><code>&#39;\u0000&#39;</code></td>
</tr>
<tr>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td>참조 타입</td>
<td><code>null</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">public class Person {

    String name;
    int age;
    boolean active;
}</code></pre>
<pre><code class="language-java">public class PersonExample {

    public static void main(String[] args) {
        Person person = new Person();

        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.active);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">null
0
false</code></pre>
<hr>
<h1 id="12-필드-사용">12. 필드 사용</h1>
<p>클래스 내부에서는 필드 이름으로 바로 접근할 수 있다.</p>
<pre><code class="language-java">public class Car {

    int speed;

    void accelerate() {
        speed += 10;
    }
}</code></pre>
<p>클래스 외부에서는 객체를 생성한 뒤 점 연산자 <code>.</code>를 이용한다.</p>
<pre><code class="language-java">Car car = new Car();

car.speed = 50;

System.out.println(car.speed);</code></pre>
<p>다만 실무에서는 필드를 외부에 직접 공개하기보다 <code>private</code>으로 보호하고 메서드를 통해 접근하는 방식을 권장한다.</p>
<pre><code class="language-java">public class Car {

    private int speed;

    public void accelerate() {
        speed += 10;
    }

    public int getSpeed() {
        return speed;
    }
}</code></pre>
<hr>
<h1 id="13-변수의-종류">13. 변수의 종류</h1>
<p>Java의 변수는 선언 위치와 <code>static</code> 여부에 따라 나눌 수 있다.</p>
<pre><code class="language-text">변수
├─ 멤버 변수
│  ├─ 인스턴스 멤버 변수
│  └─ 클래스 멤버 변수
│
└─ 지역 변수
   ├─ 로컬 변수
   └─ 매개변수</code></pre>
<hr>
<h1 id="14-인스턴스-멤버-변수">14. 인스턴스 멤버 변수</h1>
<p>클래스 영역에 선언되고 <code>static</code>이 붙지 않은 필드를 인스턴스 멤버 변수라고 한다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;
}</code></pre>
<p>인스턴스 필드는 객체가 생성될 때 객체마다 별도로 만들어진다.</p>
<pre><code class="language-java">Person first = new Person();
Person second = new Person();

first.name = &quot;김자바&quot;;
second.name = &quot;이자바&quot;;

System.out.println(first.name);
System.out.println(second.name);</code></pre>
<p>두 객체는 각각 고유한 <code>name</code> 값을 가진다.</p>
<pre><code class="language-text">first  → Person 객체 {name=&quot;김자바&quot;}
second → Person 객체 {name=&quot;이자바&quot;}</code></pre>
<p>인스턴스 필드는 객체가 Garbage Collector에 의해 제거될 때 함께 사라진다.</p>
<hr>
<h1 id="15-클래스-멤버-변수">15. 클래스 멤버 변수</h1>
<p>필드에 <code>static</code>을 붙이면 클래스 멤버 변수가 된다.</p>
<pre><code class="language-java">public class Person {

    static String scientificName = &quot;Homo sapiens&quot;;

    String name;
}</code></pre>
<p>클래스 변수는 특정 객체에 속하지 않고 클래스 자체에 속한다.</p>
<p>모든 객체가 하나의 값을 공유한다.</p>
<pre><code class="language-java">Person first = new Person();
Person second = new Person();

Person.scientificName = &quot;Homo sapiens sapiens&quot;;

System.out.println(first.scientificName);
System.out.println(second.scientificName);</code></pre>
<p>두 객체에서 동일한 값이 출력된다.</p>
<hr>
<h2 id="클래스-이름으로-접근하기">클래스 이름으로 접근하기</h2>
<p><code>static</code> 멤버는 객체가 아니라 클래스 이름으로 접근하는 것이 권장된다.</p>
<pre><code class="language-java">Person.scientificName = &quot;Homo sapiens&quot;;</code></pre>
<p>객체를 통해서도 접근은 가능하지만 권장되지 않는다.</p>
<pre><code class="language-java">Person person = new Person();

person.scientificName = &quot;변경된 값&quot;;</code></pre>
<p>IDE에서는 다음과 비슷한 경고가 나타날 수 있다.</p>
<pre><code class="language-text">The static field Person.scientificName
should be accessed in a static way</code></pre>
<p><code>static</code> 값은 객체마다 고유한 값이 아니라 모든 객체가 공유하는 값이므로 클래스 이름을 사용하는 것이 의도를 명확하게 보여준다.</p>
<hr>
<h2 id="static은-고정값이-아니다">static은 고정값이 아니다</h2>
<p><code>static</code>을 값이 변하지 않는다는 의미로 오해하기 쉽다.</p>
<p>그러나 <code>static</code>은 객체가 아니라 클래스에 소속된다는 뜻이다.</p>
<pre><code class="language-java">public class Counter {

    static int count;
}</code></pre>
<pre><code class="language-java">Counter.count = 10;
Counter.count = 20;</code></pre>
<p>값은 변경할 수 있다.</p>
<p>변경할 수 없는 상수를 만들려면 <code>static final</code>을 함께 사용한다.</p>
<pre><code class="language-java">public class MathConstants {

    public static final double PI = 3.141592;
}</code></pre>
<hr>
<h1 id="16-지역-변수와-매개변수">16. 지역 변수와 매개변수</h1>
<p>메서드, 생성자, 초기화 블록 내부에서 선언된 변수는 지역 변수다.</p>
<pre><code class="language-java">public class Calculator {

    int add(int left, int right) {
        int result = left + right;

        return result;
    }
}</code></pre>
<p>위 코드에서 다음 변수는 모두 지역 변수 계열이다.</p>
<ul>
<li><code>left</code>: 매개변수</li>
<li><code>right</code>: 매개변수</li>
<li><code>result</code>: 로컬 변수</li>
</ul>
<p>지역 변수는 해당 코드가 실행될 때 Stack 프레임에 생성되고, 블록을 벗어나거나 메서드가 종료되면 사라진다.</p>
<p>또한 사용하기 전에 반드시 초기화해야 한다.</p>
<hr>
<h1 id="17-메서드란">17. 메서드란?</h1>
<p>메서드는 객체의 동작이나 기능을 코드로 표현한 것이다.</p>
<pre><code class="language-java">public class Calculator {

    int add(int left, int right) {
        return left + right;
    }
}</code></pre>
<p>메서드를 작성하면 반복되는 코드를 하나로 묶어 재사용할 수 있다.</p>
<pre><code class="language-java">int firstResult = calculator.add(10, 20);
int secondResult = calculator.add(100, 200);</code></pre>
<hr>
<h2 id="메서드가-필요한-이유">메서드가 필요한 이유</h2>
<p>메서드를 사용하지 않으면 동일한 계산 코드를 여러 번 작성해야 한다.</p>
<pre><code class="language-java">int firstResult = 10 + 20;
int secondResult = 30 + 40;
int thirdResult = 50 + 60;</code></pre>
<p>기능을 메서드로 분리하면 중복을 줄이고 의미를 명확하게 표현할 수 있다.</p>
<pre><code class="language-java">int firstResult = add(10, 20);
int secondResult = add(30, 40);
int thirdResult = add(50, 60);</code></pre>
<hr>
<h1 id="18-메서드-선언-구조">18. 메서드 선언 구조</h1>
<p>메서드는 다음 구조를 가진다.</p>
<pre><code class="language-java">리턴타입 메서드이름(매개변수 목록) {
    실행문

    return 반환값;
}</code></pre>
<pre><code class="language-java">int add(int left, int right) {
    int result = left + right;

    return result;
}</code></pre>
<hr>
<h2 id="리턴-타입">리턴 타입</h2>
<p>리턴 타입은 메서드를 호출한 곳으로 돌려주는 값의 타입이다.</p>
<pre><code class="language-java">int add(int left, int right) {
    return left + right;
}</code></pre>
<p><code>add()</code>는 <code>int</code> 값을 반환한다.</p>
<p>반환할 값이 없다면 <code>void</code>를 사용한다.</p>
<pre><code class="language-java">void printMessage() {
    System.out.println(&quot;안녕하세요.&quot;);
}</code></pre>
<p>메서드는 직접적으로 하나의 값만 반환할 수 있다.</p>
<p>여러 데이터를 반환해야 한다면 배열이나 객체를 반환할 수 있다.</p>
<pre><code class="language-java">int[] calculate(int left, int right) {
    return new int[]{
            left + right,
            left - right
    };
}</code></pre>
<p>실무에서는 관련된 여러 값을 별도의 결과 객체로 묶는 방법도 많이 사용한다.</p>
<hr>
<h2 id="메서드-이름">메서드 이름</h2>
<p>메서드 이름은 기능을 쉽게 파악할 수 있도록 동사 형태로 작성하는 경우가 많다.</p>
<pre><code class="language-java">calculateTotalPrice()
findUser()
saveOrder()
deleteReview()
printResult()</code></pre>
<p>다음처럼 의미가 불분명한 이름은 피하는 것이 좋다.</p>
<pre><code class="language-java">doIt()
work()
process1()</code></pre>
<hr>
<h2 id="매개변수">매개변수</h2>
<p>매개변수는 메서드를 호출할 때 전달받을 값을 선언한다.</p>
<pre><code class="language-java">int add(int left, int right) {
    return left + right;
}</code></pre>
<p>호출할 때 전달하는 실제 값을 인수 또는 인자라고 부른다.</p>
<pre><code class="language-java">calculator.add(10, 20);</code></pre>
<ul>
<li><code>left</code>, <code>right</code>: 매개변수</li>
<li><code>10</code>, <code>20</code>: 인수</li>
</ul>
<p>메서드 호출 과정에서 자동 타입 변환이 적용될 수 있다.</p>
<pre><code class="language-java">void printDouble(double value) {
    System.out.println(value);
}</code></pre>
<pre><code class="language-java">printDouble(10);</code></pre>
<p><code>int</code> 값 <code>10</code>이 <code>double</code>로 자동 변환된다.</p>
<hr>
<h1 id="19-메서드의-return">19. 메서드의 return</h1>
<p>반환 타입이 <code>void</code>가 아니라면 메서드는 반드시 해당 타입의 값을 반환해야 한다.</p>
<pre><code class="language-java">int add(int left, int right) {
    return left + right;
}</code></pre>
<p>조건문에 따라 값을 반환한다면 가능한 모든 실행 경로에서 값이 반환되어야 한다.</p>
<pre><code class="language-java">String getGrade(int score) {
    if (score &gt;= 90) {
        return &quot;A&quot;;
    }

    if (score &gt;= 80) {
        return &quot;B&quot;;
    }

    return &quot;C&quot;;
}</code></pre>
<p>다음 코드는 일부 상황에서 반환값이 없으므로 컴파일 오류가 발생한다.</p>
<pre><code class="language-java">int getNumber(boolean condition) {
    if (condition) {
        return 10;
    }

    // condition이 false일 때 반환값 없음
}</code></pre>
<hr>
<h2 id="void-메서드의-return">void 메서드의 return</h2>
<p><code>void</code> 메서드는 반환값이 없으므로 <code>return</code>을 생략할 수 있다.</p>
<p>다만 메서드를 즉시 종료할 목적으로 사용할 수 있다.</p>
<pre><code class="language-java">void register(String name) {
    if (name == null || name.isBlank()) {
        return;
    }

    System.out.println(name + &quot; 등록 완료&quot;);
}</code></pre>
<hr>
<h1 id="20-가변-인자">20. 가변 인자</h1>
<p>동일한 타입의 인수가 몇 개 전달될지 알 수 없다면 가변 인자를 사용할 수 있다.</p>
<pre><code class="language-java">int addAll(int... numbers) {
    int sum = 0;

    for (int number : numbers) {
        sum += number;
    }

    return sum;
}</code></pre>
<p>다양한 개수의 인수를 전달할 수 있다.</p>
<pre><code class="language-java">public class VariableArgumentsExample {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        System.out.println(calculator.addAll(1, 2, 3));
        System.out.println(calculator.addAll(1, 2, 3, 4, 5));
    }
}</code></pre>
<pre><code class="language-java">public class Calculator {

    int addAll(int... numbers) {
        int sum = 0;

        for (int number : numbers) {
            sum += number;
        }

        return sum;
    }
}</code></pre>
<p>가변 인자는 내부적으로 배열로 처리된다.</p>
<pre><code class="language-java">int... numbers</code></pre>
<p>호출 시 전달한 값의 개수에 맞춰 배열이 생성된다.</p>
<hr>
<h2 id="가변-인자-주의사항">가변 인자 주의사항</h2>
<p>가변 인자는 매개변수 목록의 마지막에만 선언할 수 있다.</p>
<pre><code class="language-java">void print(String title, int... values) {
}</code></pre>
<p>다음 형태는 사용할 수 없다.</p>
<pre><code class="language-java">// void print(int... values, String title) {
// }</code></pre>
<p>가변 인자는 한 메서드에 하나만 선언할 수 있다.</p>
<hr>
<h1 id="21-메서드-오버로딩">21. 메서드 오버로딩</h1>
<p>메서드 오버로딩은 같은 클래스 내부에 이름이 같은 메서드를 여러 개 선언하는 기능이다.</p>
<p>단, 매개변수의 타입, 개수 또는 순서가 달라야 한다.</p>
<pre><code class="language-java">public class Calculator {

    int add(int left, int right) {
        return left + right;
    }

    double add(double left, double right) {
        return left + right;
    }

    int add(int first, int second, int third) {
        return first + second + third;
    }
}</code></pre>
<p>호출할 때 전달한 인수에 맞는 메서드가 선택된다.</p>
<pre><code class="language-java">Calculator calculator = new Calculator();

System.out.println(calculator.add(10, 20));
System.out.println(calculator.add(1.5, 2.5));
System.out.println(calculator.add(10, 20, 30));</code></pre>
<hr>
<h2 id="오버로딩의-조건">오버로딩의 조건</h2>
<p>다음 중 하나 이상이 달라야 한다.</p>
<ul>
<li>매개변수의 개수</li>
<li>매개변수의 타입</li>
<li>매개변수의 선언 순서</li>
</ul>
<pre><code class="language-java">void print(int value) {
}

void print(String value) {
}

void print(int count, String text) {
}

void print(String text, int count) {
}</code></pre>
<p>매개변수 이름만 바꾸는 것은 오버로딩이 아니다.</p>
<pre><code class="language-java">void print(int value) {
}

// void print(int number) {
//     중복 선언 오류
// }</code></pre>
<p>리턴 타입만 다른 것도 오버로딩이 아니다.</p>
<pre><code class="language-java">int calculate(int value) {
    return value;
}

// double calculate(int value) {
//     중복 선언 오류
// }</code></pre>
<p>메서드를 호출할 때는 반환 타입이 아니라 메서드 이름과 인수 목록으로 구분하기 때문이다.</p>
<hr>
<h1 id="22-클래스-멤버와-인스턴스-멤버의-호출">22. 클래스 멤버와 인스턴스 멤버의 호출</h1>
<p>가장 중요한 기준은 호출하려는 멤버가 현재 메모리에 존재하는지다.</p>
<p><code>static</code> 멤버는 클래스가 로딩될 때 사용할 수 있지만, 인스턴스 멤버는 객체가 생성되어야 사용할 수 있다.</p>
<pre><code class="language-java">public class MemberExample {

    static int classValue = 10;

    int instanceValue = 20;

    static void classMethod() {
        System.out.println(classValue);

        // System.out.println(instanceValue);
        // 객체가 없으므로 직접 접근 불가
    }

    void instanceMethod() {
        System.out.println(classValue);
        System.out.println(instanceValue);
    }
}</code></pre>
<hr>
<h2 id="인스턴스-메서드에서-static-멤버-접근">인스턴스 메서드에서 static 멤버 접근</h2>
<p>인스턴스 메서드는 객체가 생성된 이후 호출된다.</p>
<p>이 시점에는 클래스 멤버와 인스턴스 멤버가 모두 존재하므로 둘 다 접근할 수 있다.</p>
<pre><code class="language-java">void instanceMethod() {
    System.out.println(classValue);
    System.out.println(instanceValue);
}</code></pre>
<hr>
<h2 id="static-메서드에서-인스턴스-멤버-접근">static 메서드에서 인스턴스 멤버 접근</h2>
<p><code>static</code> 메서드는 객체가 생성되지 않아도 호출할 수 있다.</p>
<p>따라서 어떤 객체의 인스턴스 필드를 사용해야 하는지 알 수 없다.</p>
<pre><code class="language-java">static void classMethod() {
    // System.out.println(instanceValue);
}</code></pre>
<p>객체를 명시적으로 생성하거나 전달받으면 접근할 수 있다.</p>
<pre><code class="language-java">static void printInstanceValue(MemberExample example) {
    System.out.println(example.instanceValue);
}</code></pre>
<hr>
<h1 id="23-메서드-호출-스택">23. 메서드 호출 스택</h1>
<p>Java에서 메서드가 호출되면 JVM Stack에 해당 메서드를 위한 프레임이 생성된다.</p>
<p>Stack은 LIFO 구조다.</p>
<pre><code class="language-text">Last In, First Out
마지막에 들어온 것이 먼저 나간다.</code></pre>
<p>다음 코드를 살펴보자.</p>
<pre><code class="language-java">public class CallStackExample {

    public static void main(String[] args) {
        methodA();
    }

    static void methodA() {
        methodB();
    }

    static void methodB() {
        System.out.println(&quot;methodB 실행&quot;);
    }
}</code></pre>
<p>호출 과정은 다음과 같다.</p>
<pre><code class="language-text">1. main() 프레임 생성
2. methodA() 프레임 생성
3. methodB() 프레임 생성
4. methodB() 종료 및 프레임 제거
5. methodA() 종료 및 프레임 제거
6. main() 종료</code></pre>
<p>메모리 구조를 개념적으로 표현하면 다음과 같다.</p>
<pre><code class="language-text">┌───────────────┐
│ methodB()     │ ← 현재 실행
├───────────────┤
│ methodA()     │ ← 대기
├───────────────┤
│ main()        │ ← 대기
└───────────────┘</code></pre>
<p>항상 Stack 맨 위에 있는 프레임이 현재 실행 중인 메서드다.</p>
<hr>
<h1 id="24-java는-call-by-value다">24. Java는 Call by Value다</h1>
<p>Java는 메서드를 호출할 때 변수에 저장된 값을 복사해 전달한다.</p>
<p>따라서 Java의 매개변수 전달 방식은 항상 <strong>Call by Value</strong>다.</p>
<p>기본 타입과 참조 타입 모두 동일하게 값이 복사된다. 다만 참조 타입 변수에 저장된 값이 객체를 가리키는 참조값이라는 차이가 있다.</p>
<hr>
<h2 id="기본-타입-전달">기본 타입 전달</h2>
<pre><code class="language-java">public class PrimitiveParameterExample {

    public static void main(String[] args) {
        int number = 10;

        change(number);

        System.out.println(number);
    }

    static void change(int value) {
        value = 100;
    }
}</code></pre>
<p>실행 결과는 <code>10</code>이다.</p>
<p><code>number</code>의 값 <code>10</code>이 <code>value</code>에 복사되었기 때문에 <code>value</code>를 변경해도 원본 변수에는 영향을 주지 않는다.</p>
<pre><code class="language-text">number = 10
     ↓ 값 복사
value = 10</code></pre>
<hr>
<h2 id="참조-타입-전달">참조 타입 전달</h2>
<pre><code class="language-java">public class Person {

    String name;
}</code></pre>
<pre><code class="language-java">public class ReferenceParameterExample {

    public static void main(String[] args) {
        Person person = new Person();
        person.name = &quot;김자바&quot;;

        changeName(person);

        System.out.println(person.name);
    }

    static void changeName(Person target) {
        target.name = &quot;이자바&quot;;
    }
}</code></pre>
<p>결과는 <code>&quot;이자바&quot;</code>다.</p>
<p><code>person</code>과 <code>target</code>은 서로 다른 변수지만 같은 객체를 가리키는 참조값을 가지고 있다.</p>
<pre><code class="language-text">person ──┐
         ├──→ Person 객체
target ──┘</code></pre>
<p>따라서 <code>target</code>을 통해 객체의 필드를 변경하면 <code>person</code>으로도 변경된 값을 확인할 수 있다.</p>
<hr>
<h2 id="참조-변수-자체를-바꾸는-경우">참조 변수 자체를 바꾸는 경우</h2>
<pre><code class="language-java">public class ReferenceParameterExample {

    public static void main(String[] args) {
        Person person = new Person();
        person.name = &quot;김자바&quot;;

        replace(person);

        System.out.println(person.name);
    }

    static void replace(Person target) {
        target = new Person();
        target.name = &quot;새로운 객체&quot;;
    }
}</code></pre>
<p>결과는 여전히 <code>&quot;김자바&quot;</code>다.</p>
<p>메서드 내부의 <code>target</code> 변수에 새로운 참조값을 저장했을 뿐, 호출한 쪽의 <code>person</code> 변수는 변경되지 않는다.</p>
<blockquote>
<p>Java는 객체를 참조로 전달하는 언어가 아니라, 객체를 가리키는 참조값을 값으로 복사해 전달하는 언어다.</p>
</blockquote>
<hr>
<h1 id="25-생성자란">25. 생성자란?</h1>
<p>생성자는 <code>new</code> 연산자로 객체를 생성할 때 호출되는 특별한 코드 블록이다.</p>
<p>생성자는 객체의 필드를 초기화하거나 객체를 사용할 준비를 하는 역할을 한다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;

    Person() {
        name = &quot;이름 없음&quot;;
        age = 0;
    }
}</code></pre>
<p>객체를 생성하면 생성자가 실행된다.</p>
<pre><code class="language-java">Person person = new Person();</code></pre>
<hr>
<h2 id="생성자는-메서드와-비슷하지만-다르다">생성자는 메서드와 비슷하지만 다르다</h2>
<p>생성자는 메서드와 비슷한 형태를 가지지만 다음 차이가 있다.</p>
<ul>
<li>이름이 클래스 이름과 같다.</li>
<li>반환 타입을 작성하지 않는다.</li>
<li><code>new</code> 연산자로 객체를 생성할 때 호출된다.</li>
<li>객체 초기화를 담당한다.</li>
</ul>
<pre><code class="language-java">public class Person {

    Person() {
    }
}</code></pre>
<p>다음처럼 <code>void</code>를 작성하면 생성자가 아니라 일반 메서드가 된다.</p>
<pre><code class="language-java">public class Person {

    void Person() {
        System.out.println(&quot;일반 메서드&quot;);
    }
}</code></pre>
<hr>
<h1 id="26-기본-생성자">26. 기본 생성자</h1>
<p>클래스에 생성자를 하나도 선언하지 않으면 컴파일러가 매개변수가 없는 기본 생성자를 자동으로 추가한다.</p>
<p>소스 코드가 다음과 같다고 가정해보자.</p>
<pre><code class="language-java">public class Car {
}</code></pre>
<p>개념적으로 컴파일러는 다음 생성자를 제공한다.</p>
<pre><code class="language-java">public class Car {

    public Car() {
    }
}</code></pre>
<p>따라서 생성자를 직접 작성하지 않아도 객체를 생성할 수 있다.</p>
<pre><code class="language-java">Car car = new Car();</code></pre>
<hr>
<h2 id="기본-생성자가-자동-생성되지-않는-경우">기본 생성자가 자동 생성되지 않는 경우</h2>
<p>클래스에 생성자를 하나라도 직접 선언하면 컴파일러는 기본 생성자를 추가하지 않는다.</p>
<pre><code class="language-java">public class Car {

    Car(String model) {
    }
}</code></pre>
<p>따라서 다음 코드는 컴파일 오류가 발생한다.</p>
<pre><code class="language-java">// Car car = new Car();</code></pre>
<p>직접 선언한 생성자를 사용해야 한다.</p>
<pre><code class="language-java">Car car = new Car(&quot;아반떼&quot;);</code></pre>
<p>매개변수가 없는 생성자도 필요하다면 직접 선언해야 한다.</p>
<pre><code class="language-java">public class Car {

    Car() {
    }

    Car(String model) {
    }
}</code></pre>
<hr>
<h1 id="27-매개변수가-있는-생성자">27. 매개변수가 있는 생성자</h1>
<p>생성자에 매개변수를 선언하면 객체를 생성할 때 초기값을 전달할 수 있다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;

    Person(String personName, int personAge) {
        name = personName;
        age = personAge;
    }
}</code></pre>
<p>객체 생성 시 인수를 전달한다.</p>
<pre><code class="language-java">Person person = new Person(&quot;김자바&quot;, 20);</code></pre>
<p>이 방식은 객체 생성 직후 반드시 필요한 값을 설정할 수 있다는 장점이 있다.</p>
<hr>
<h1 id="28-this">28. this</h1>
<p><code>this</code>는 현재 객체 자신을 참조하는 키워드다.</p>
<p>인스턴스 메서드나 생성자 내부에서 현재 객체의 필드와 메서드에 접근할 때 사용할 수 있다.</p>
<pre><code class="language-java">public class Person {

    String name;

    void printName() {
        System.out.println(this.name);
    }
}</code></pre>
<p><code>this.name</code>은 현재 메서드를 호출한 객체의 <code>name</code> 필드를 의미한다.</p>
<hr>
<h2 id="필드와-매개변수-이름이-같은-경우">필드와 매개변수 이름이 같은 경우</h2>
<p>생성자의 매개변수 이름은 필드 이름과 같게 작성하는 경우가 많다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}</code></pre>
<p>왼쪽의 <code>this.name</code>은 필드이고, 오른쪽의 <code>name</code>은 매개변수다.</p>
<pre><code class="language-text">this.name = name;
필드          매개변수</code></pre>
<p><code>this</code>를 생략하면 매개변수 자신에게 값을 다시 대입하는 코드가 된다.</p>
<pre><code class="language-java">Person(String name) {
    name = name;
}</code></pre>
<p>이 코드는 객체의 필드를 초기화하지 못한다.</p>
<hr>
<h1 id="29-생성자-오버로딩">29. 생성자 오버로딩</h1>
<p>생성자도 메서드처럼 오버로딩할 수 있다.</p>
<p>매개변수의 타입, 개수 또는 순서가 다른 생성자를 여러 개 선언할 수 있다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;

    Person() {
    }

    Person(String name) {
        this.name = name;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}</code></pre>
<p>객체를 다양한 방식으로 생성할 수 있다.</p>
<pre><code class="language-java">Person first = new Person();
Person second = new Person(&quot;김자바&quot;);
Person third = new Person(&quot;이자바&quot;, 25);</code></pre>
<hr>
<h2 id="생성자-오버로딩이-아닌-경우">생성자 오버로딩이 아닌 경우</h2>
<p>매개변수 이름만 다르고 타입과 개수가 같다면 오버로딩이 아니다.</p>
<pre><code class="language-java">Person(String name) {
}

// Person(String userName) {
//     중복 선언 오류
// }</code></pre>
<hr>
<h1 id="30-this를-이용한-다른-생성자-호출">30. this()를 이용한 다른 생성자 호출</h1>
<p>생성자 오버로딩이 많아지면 필드 초기화 코드가 중복될 수 있다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;
    String address;

    Person(String name) {
        this.name = name;
        this.age = 0;
        this.address = &quot;주소 없음&quot;;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
        this.address = &quot;주소 없음&quot;;
    }

    Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}</code></pre>
<p><code>this()</code>를 사용하면 다른 생성자를 호출하여 중복을 줄일 수 있다.</p>
<pre><code class="language-java">public class Person {

    String name;
    int age;
    String address;

    Person() {
        this(&quot;이름 없음&quot;, 0, &quot;주소 없음&quot;);
    }

    Person(String name) {
        this(name, 0, &quot;주소 없음&quot;);
    }

    Person(String name, int age) {
        this(name, age, &quot;주소 없음&quot;);
    }

    Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}</code></pre>
<p>필드 초기화는 마지막 생성자에 집중되어 있다.</p>
<hr>
<h2 id="this-사용-규칙">this() 사용 규칙</h2>
<p><code>this()</code>는 생성자 내부의 첫 번째 실행문으로만 사용할 수 있다.</p>
<pre><code class="language-java">Person(String name) {
    this(name, 0);
}</code></pre>
<p>다음처럼 다른 실행문 뒤에 작성할 수 없다.</p>
<pre><code class="language-java">Person(String name) {
    // System.out.println(&quot;생성 시작&quot;);
    // this(name, 0);
}</code></pre>
<p>한 생성자 안에서 <code>this()</code>와 <code>super()</code>를 함께 호출할 수도 없다. 둘 다 생성자의 첫 번째 줄에만 올 수 있기 때문이다.</p>
<hr>
<h1 id="31-필드-초기화-순서">31. 필드 초기화 순서</h1>
<p>인스턴스 필드는 객체가 생성될 때 대략 다음 순서로 초기화된다.</p>
<pre><code class="language-text">1. 타입의 기본값
2. 명시적 필드 초기화
3. 인스턴스 초기화 블록
4. 생성자</code></pre>
<p>예제를 살펴보자.</p>
<pre><code class="language-java">public class InitializationOrder {

    int number = printAndReturn(&quot;명시적 필드 초기화&quot;, 10);

    {
        System.out.println(&quot;인스턴스 초기화 블록&quot;);
        number = 20;
    }

    InitializationOrder() {
        System.out.println(&quot;생성자&quot;);
        number = 30;
    }

    int printAndReturn(String message, int value) {
        System.out.println(message);

        return value;
    }
}</code></pre>
<p>객체 생성 시 출력 순서는 다음과 같다.</p>
<pre><code class="language-text">명시적 필드 초기화
인스턴스 초기화 블록
생성자</code></pre>
<p>마지막 <code>number</code> 값은 <code>30</code>이다.</p>
<hr>
<h1 id="32-초기화-블록">32. 초기화 블록</h1>
<p>초기화 블록은 생성자처럼 멤버 변수를 초기화하거나 객체 생성 시 공통으로 실행할 코드를 작성하는 데 사용할 수 있다.</p>
<p>초기화 블록은 이름이 없기 때문에 직접 호출할 수 없고 매개변수도 받을 수 없다.</p>
<hr>
<h2 id="인스턴스-초기화-블록">인스턴스 초기화 블록</h2>
<p>중괄호만 작성한 블록은 인스턴스 초기화 블록이다.</p>
<pre><code class="language-java">public class Person {

    String name;

    {
        System.out.println(&quot;객체 생성 전 공통 초기화&quot;);
        name = &quot;이름 없음&quot;;
    }

    Person() {
        System.out.println(&quot;생성자 실행&quot;);
    }
}</code></pre>
<p>객체가 생성될 때마다 생성자보다 먼저 실행된다.</p>
<pre><code class="language-text">new
→ 명시적 필드 초기화
→ 인스턴스 초기화 블록
→ 생성자</code></pre>
<p>여러 생성자에서 공통으로 실행해야 하는 코드가 있을 때 사용할 수 있지만, 일반적으로는 생성자나 별도 메서드로 초기화 과정을 표현하는 편이 더 읽기 쉬운 경우가 많다.</p>
<hr>
<h2 id="클래스-초기화-블록">클래스 초기화 블록</h2>
<p><code>static</code>이 붙은 블록은 클래스 초기화 블록이다.</p>
<pre><code class="language-java">public class ApplicationConfig {

    static String environment;

    static {
        System.out.println(&quot;클래스 초기화&quot;);
        environment = &quot;development&quot;;
    }
}</code></pre>
<p>클래스가 처음 로딩될 때 한 번 실행된다.</p>
<pre><code class="language-text">클래스 로딩
→ static 필드 초기화
→ static 초기화 블록</code></pre>
<p>객체 생성 여부와 관계없이 클래스 단위로 한 번 실행된다.</p>
<hr>
<h1 id="33-객체-생성과-jvm-메모리">33. 객체 생성과 JVM 메모리</h1>
<p>객체 생성과 메서드 호출을 메모리 관점에서 정리해보자.</p>
<pre><code class="language-java">public class Person {

    static String species = &quot;Human&quot;;

    String name;

    Person(String name) {
        this.name = name;
    }

    void introduce() {
        String message = &quot;안녕하세요.&quot;;

        System.out.println(message + &quot; &quot; + name);
    }
}</code></pre>
<pre><code class="language-java">public class PersonExample {

    public static void main(String[] args) {
        Person person = new Person(&quot;김자바&quot;);

        person.introduce();
    }
}</code></pre>
<p>개념적인 메모리 구조는 다음과 같다.</p>
<pre><code class="language-text">Method Area
┌────────────────────────┐
│ Person 클래스 정보      │
│ 필드 정보               │
│ 생성자 코드             │
│ introduce() 코드        │
│ static species          │
└────────────────────────┘

JVM Stack
┌────────────────────────┐
│ introduce() 프레임      │
│ message = &quot;안녕하세요.&quot; │
├────────────────────────┤
│ main() 프레임           │
│ person = 참조값          │
└────────────────────────┘

Heap
┌────────────────────────┐
│ Person 객체             │
│ name = &quot;김자바&quot;         │
└────────────────────────┘</code></pre>
<p>실제 JVM의 내부 구현은 더 복잡하지만, 입문 단계에서는 클래스 정보, 메서드 실행 공간, 객체 저장 공간을 구분해서 이해하는 것이 중요하다.</p>
<hr>
<h1 id="34-종합-예제-은행-계좌-클래스">34. 종합 예제: 은행 계좌 클래스</h1>
<p>지금까지 배운 내용을 활용해 은행 계좌 클래스를 만들어보자.</p>
<pre><code class="language-java">public class BankAccount {

    static int accountCount;

    String owner;
    String accountNumber;
    private long balance;

    BankAccount(String owner, String accountNumber) {
        this(owner, accountNumber, 0);
    }

    BankAccount(
            String owner,
            String accountNumber,
            long initialBalance
    ) {
        this.owner = owner;
        this.accountNumber = accountNumber;
        this.balance = initialBalance;

        accountCount++;
    }

    void deposit(long amount) {
        if (amount &lt;= 0) {
            System.out.println(&quot;입금액은 0보다 커야 합니다.&quot;);
            return;
        }

        balance += amount;

        System.out.printf(
                &quot;%,d원이 입금되었습니다.%n&quot;,
                amount
        );
    }

    boolean withdraw(long amount) {
        if (amount &lt;= 0) {
            System.out.println(&quot;출금액은 0보다 커야 합니다.&quot;);
            return false;
        }

        if (amount &gt; balance) {
            System.out.println(&quot;잔액이 부족합니다.&quot;);
            return false;
        }

        balance -= amount;

        System.out.printf(
                &quot;%,d원이 출금되었습니다.%n&quot;,
                amount
        );

        return true;
    }

    long getBalance() {
        return balance;
    }

    void printAccountInfo() {
        System.out.println(&quot;예금주: &quot; + owner);
        System.out.println(&quot;계좌번호: &quot; + accountNumber);
        System.out.printf(&quot;잔액: %,d원%n&quot;, balance);
    }

    static int getAccountCount() {
        return accountCount;
    }
}</code></pre>
<p>실행 클래스는 다음과 같다.</p>
<pre><code class="language-java">public class BankAccountExample {

    public static void main(String[] args) {
        BankAccount firstAccount = new BankAccount(
                &quot;김자바&quot;,
                &quot;100-123-456789&quot;
        );

        BankAccount secondAccount = new BankAccount(
                &quot;이자바&quot;,
                &quot;200-456-789012&quot;,
                100_000
        );

        firstAccount.deposit(50_000);
        firstAccount.withdraw(10_000);

        System.out.println();
        firstAccount.printAccountInfo();

        System.out.println();
        secondAccount.printAccountInfo();

        System.out.println();
        System.out.println(
                &quot;생성된 계좌 수: &quot;
                        + BankAccount.getAccountCount()
        );
    }
}</code></pre>
<p>이 예제에는 다음 개념이 포함되어 있다.</p>
<ul>
<li>인스턴스 필드</li>
<li><code>static</code> 클래스 필드</li>
<li>필드 캡슐화</li>
<li>생성자 오버로딩</li>
<li><code>this()</code></li>
<li><code>this.필드</code></li>
<li>반환값이 있는 메서드</li>
<li><code>void</code> 메서드</li>
<li>가드 클로즈</li>
<li>객체마다 다른 상태</li>
<li>모든 객체가 공유하는 클래스 변수</li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념-정리">헷갈리기 쉬운 개념 정리</h1>
<h2 id="클래스와-객체">클래스와 객체</h2>
<pre><code class="language-text">클래스
→ 객체를 만들기 위한 설계도

객체
→ 설계도를 기반으로 메모리에 생성된 실체</code></pre>
<pre><code class="language-java">public class Person {
}</code></pre>
<p><code>Person</code>은 클래스다.</p>
<pre><code class="language-java">Person person = new Person();</code></pre>
<p><code>new Person()</code>으로 생성된 대상이 객체다.</p>
<hr>
<h2 id="객체와-인스턴스">객체와 인스턴스</h2>
<p>객체는 독립적으로 존재하는 대상을 의미하고, 인스턴스는 어떤 클래스로부터 생성되었는지를 강조한다.</p>
<pre><code class="language-text">person은 객체다.
person은 Person 클래스의 인스턴스다.</code></pre>
<hr>
<h2 id="필드와-로컬-변수">필드와 로컬 변수</h2>
<pre><code class="language-java">public class Person {

    String name; // 필드

    void introduce() {
        String message = &quot;안녕하세요.&quot;; // 로컬 변수
    }
}</code></pre>
<p>필드는 객체나 클래스에 소속되며 자동 초기화된다. 로컬 변수는 실행 블록에 소속되며 직접 초기화해야 한다.</p>
<hr>
<h2 id="인스턴스-멤버와-클래스-멤버">인스턴스 멤버와 클래스 멤버</h2>
<pre><code class="language-java">String name;
static int count;</code></pre>
<ul>
<li><code>name</code>: 객체마다 따로 존재</li>
<li><code>count</code>: 모든 객체가 공유</li>
</ul>
<hr>
<h2 id="static과-final">static과 final</h2>
<pre><code class="language-java">static int count;</code></pre>
<p>클래스에 소속되지만 값은 변경 가능하다.</p>
<pre><code class="language-java">static final int MAX_COUNT = 100;</code></pre>
<p>클래스에 소속되며 한 번 초기화한 후 값을 변경할 수 없다.</p>
<hr>
<h2 id="생성자와-메서드">생성자와 메서드</h2>
<pre><code class="language-java">Person() {
}</code></pre>
<p>생성자는 클래스 이름과 같고 반환 타입이 없다.</p>
<pre><code class="language-java">void introduce() {
}</code></pre>
<p>메서드는 이름을 자유롭게 정할 수 있고 반환 타입을 작성한다.</p>
<hr>
<h2 id="this와-this">this와 this()</h2>
<pre><code class="language-java">this.name</code></pre>
<p>현재 객체의 필드를 의미한다.</p>
<pre><code class="language-java">this(&quot;김자바&quot;, 20);</code></pre>
<p>현재 클래스의 다른 생성자를 호출한다.</p>
<hr>
<h2 id="오버로딩과-오버라이딩">오버로딩과 오버라이딩</h2>
<p>이번 글에서 다룬 것은 오버로딩이다.</p>
<pre><code class="language-text">오버로딩
→ 같은 클래스에서 같은 이름을 사용하되 매개변수를 다르게 선언

오버라이딩
→ 상속받은 부모 메서드를 자식 클래스에서 다시 정의</code></pre>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>오늘은 Java 객체지향 프로그래밍의 시작점이 되는 객체, 클래스, 필드, 메서드, 생성자를 정리했다.</p>
<ul>
<li>객체는 자신의 속성과 동작을 가지며 다른 대상과 식별할 수 있는 프로그래밍 대상이다.</li>
<li>객체의 속성은 필드로, 동작은 메서드로 표현한다.</li>
<li>객체들은 메서드를 호출하며 서로 상호작용한다.</li>
<li>객체 간에는 집합 관계, 사용 관계, 상속 관계 등이 존재할 수 있다.</li>
<li>객체지향 프로그래밍은 필요한 객체를 설계하고 서로 조립하여 프로그램을 만드는 방식이다.</li>
<li>객체지향의 대표적인 특징은 추상화, 캡슐화, 상속, 다형성이다.</li>
<li>클래스는 객체를 만들기 위한 설계도이고, 인스턴스는 클래스로부터 생성된 객체다.</li>
<li><code>new</code> 연산자는 Heap 영역에 객체를 생성하고 참조값을 반환한다.</li>
<li>클래스는 필드, 생성자, 메서드로 구성된다.</li>
<li>필드는 객체의 고유 데이터와 상태를 저장한다.</li>
<li>필드는 자동으로 기본값이 설정되지만 로컬 변수는 직접 초기화해야 한다.</li>
<li>인스턴스 필드는 객체마다 따로 생성된다.</li>
<li>클래스 필드는 <code>static</code>으로 선언하며 모든 객체가 공유한다.</li>
<li><code>static</code>은 값이 고정된다는 의미가 아니며, 상수는 <code>static final</code>로 표현할 수 있다.</li>
<li>메서드는 객체의 동작을 정의하고 코드 중복을 줄인다.</li>
<li>메서드는 반환 타입, 이름, 매개변수 목록, 구현부로 구성된다.</li>
<li>가변 인자는 전달된 값의 개수에 따라 내부적으로 배열을 생성한다.</li>
<li>메서드 오버로딩은 같은 이름의 메서드를 매개변수 목록을 다르게 하여 여러 개 선언하는 것이다.</li>
<li>Java는 기본 타입과 참조 타입 모두 Call by Value 방식으로 값을 전달한다.</li>
<li>참조 타입을 매개변수로 전달할 때는 객체의 참조값이 복사된다.</li>
<li>생성자는 객체 생성 시 호출되어 필드 초기화와 준비 작업을 담당한다.</li>
<li>생성자를 선언하지 않으면 기본 생성자가 자동으로 추가된다.</li>
<li>생성자를 하나라도 직접 작성하면 기본 생성자는 자동 생성되지 않는다.</li>
<li><code>this</code>는 현재 객체 자신을 가리킨다.</li>
<li><code>this()</code>는 같은 클래스의 다른 생성자를 호출하며 반드시 생성자의 첫 줄에 작성해야 한다.</li>
<li>인스턴스 초기화 블록은 객체 생성 때마다 실행되고, 클래스 초기화 블록은 클래스 로딩 시 한 번 실행된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (2) 제어문과 배열]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-2-%EC%A0%9C%EC%96%B4%EB%AC%B8%EA%B3%BC-%EB%B0%B0%EC%97%B4</link>
            <guid>https://velog.io/@tongmill_g/JAVA-2-%EC%A0%9C%EC%96%B4%EB%AC%B8%EA%B3%BC-%EB%B0%B0%EC%97%B4</guid>
            <pubDate>Mon, 20 Jul 2026 23:59:29 GMT</pubDate>
            <description><![CDATA[<h1 id="java--조건문과-반복문">Java : 조건문과 반복문</h1>
<h2 id="들어가며">들어가며</h2>
<p>Java 프로그램은 기본적으로 <code>main()</code> 메서드의 시작 중괄호부터 끝 중괄호까지 위에서 아래로 한 줄씩 실행된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        System.out.println(&quot;첫 번째 실행&quot;);
        System.out.println(&quot;두 번째 실행&quot;);
        System.out.println(&quot;세 번째 실행&quot;);
    }
}</code></pre>
<p>하지만 실제 프로그램은 언제나 모든 코드를 같은 순서로 실행하지 않는다.</p>
<p>사용자의 나이에 따라 다른 문장을 출력하거나, 로그인 여부에 따라 접근을 허용해야 할 수 있다. 상품 목록을 모두 출력하려면 같은 작업을 여러 번 반복해야 한다.</p>
<p>이처럼 프로그램의 실행 순서를 조건에 따라 나누거나 특정 코드를 반복하도록 만드는 문법을 <strong>제어문</strong>이라고 한다.</p>
<p>Java의 제어문은 크게 다음과 같이 구분할 수 있다.</p>
<pre><code class="language-text">제어문
├─ 조건문
│  ├─ if
│  └─ switch
│
└─ 반복문
   ├─ for
   ├─ while
   └─ do-while</code></pre>
<p>반복문이나 <code>switch</code>의 실행을 제어하기 위해 <code>break</code>와 <code>continue</code>도 함께 사용한다.</p>
<hr>
<h1 id="1-조건문이란">1. 조건문이란?</h1>
<p>조건문은 조건식의 결과에 따라 서로 다른 코드를 실행한다.</p>
<p>Java의 조건식에는 반드시 <code>true</code> 또는 <code>false</code>를 결과로 만드는 표현식이나 <code>boolean</code> 타입 변수가 와야 한다.</p>
<pre><code class="language-java">int age = 20;

boolean isAdult = age &gt;= 19;

if (isAdult) {
    System.out.println(&quot;성인입니다.&quot;);
}</code></pre>
<p>Java에서는 숫자 <code>0</code>이나 <code>1</code>을 조건식으로 사용할 수 없다.</p>
<pre><code class="language-java">int number = 1;

// if (number) {
//     컴파일 오류
// }</code></pre>
<p>C나 JavaScript와 달리 Java의 <code>if</code> 조건에는 반드시 <code>boolean</code> 결과가 필요하다.</p>
<hr>
<h1 id="2-if문">2. if문</h1>
<p><code>if</code>문은 조건식이 <code>true</code>일 때 중괄호 내부의 실행문을 실행한다.</p>
<pre><code class="language-java">if (조건식) {
    조건식이 true일 때 실행할 코드
}</code></pre>
<p>예를 들어 사용자의 점수가 60점 이상일 때 합격을 출력할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int score = 75;

        if (score &gt;= 60) {
            System.out.println(&quot;합격입니다.&quot;);
        }

        System.out.println(&quot;프로그램을 종료합니다.&quot;);
    }
}</code></pre>
<p><code>score &gt;= 60</code>이 <code>true</code>이므로 <code>&quot;합격입니다.&quot;</code>가 출력된다.</p>
<hr>
<h2 id="중괄호는-생략하지-않는-것이-좋다">중괄호는 생략하지 않는 것이 좋다</h2>
<p>실행문이 한 줄뿐이라면 중괄호를 생략할 수 있다.</p>
<pre><code class="language-java">if (score &gt;= 60)
    System.out.println(&quot;합격입니다.&quot;);</code></pre>
<p>그러나 코드가 추가될 때 실수하기 쉽기 때문에 중괄호를 항상 작성하는 것이 안전하다.</p>
<pre><code class="language-java">if (score &gt;= 60) {
    System.out.println(&quot;합격입니다.&quot;);
}</code></pre>
<p>다음 코드는 들여쓰기만 보면 두 문장이 모두 조건에 포함된 것 같지만, 실제로는 첫 번째 문장만 <code>if</code>문의 영향을 받는다.</p>
<pre><code class="language-java">if (score &gt;= 60)
    System.out.println(&quot;합격입니다.&quot;);

    System.out.println(&quot;축하합니다.&quot;);</code></pre>
<p><code>&quot;축하합니다.&quot;</code>는 조건과 관계없이 항상 실행된다.</p>
<blockquote>
<p>들여쓰기는 사람이 코드를 읽기 쉽게 만들 뿐, Java의 실행 범위를 결정하지 않는다. 실행 범위는 중괄호가 결정한다.</p>
</blockquote>
<hr>
<h1 id="3-if-else문">3. if-else문</h1>
<p>조건식이 <code>true</code>인 경우와 <code>false</code>인 경우에 각각 다른 코드를 실행하려면 <code>else</code>를 사용한다.</p>
<pre><code class="language-java">if (조건식) {
    조건식이 true일 때 실행
} else {
    조건식이 false일 때 실행
}</code></pre>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int score = 55;

        if (score &gt;= 60) {
            System.out.println(&quot;합격입니다.&quot;);
        } else {
            System.out.println(&quot;불합격입니다.&quot;);
        }
    }
}</code></pre>
<p>두 블록 중 하나만 실행된다.</p>
<hr>
<h1 id="4-if-else-if-else문">4. if-else if-else문</h1>
<p>조건이 여러 개라면 <code>else if</code>를 사용한다.</p>
<pre><code class="language-java">if (조건식1) {
    실행문 A
} else if (조건식2) {
    실행문 B
} else {
    실행문 C
}</code></pre>
<p>점수에 따라 등급을 출력하는 프로그램을 만들어보자.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int score = 85;
        char grade;

        if (score &gt;= 90) {
            grade = &#39;A&#39;;
        } else if (score &gt;= 80) {
            grade = &#39;B&#39;;
        } else if (score &gt;= 70) {
            grade = &#39;C&#39;;
        } else if (score &gt;= 60) {
            grade = &#39;D&#39;;
        } else {
            grade = &#39;F&#39;;
        }

        System.out.println(&quot;학점: &quot; + grade);
    }
}</code></pre>
<p>조건은 위에서부터 순서대로 검사하며, 처음으로 <code>true</code>가 된 블록만 실행한다.</p>
<p>따라서 조건의 순서가 매우 중요하다.</p>
<hr>
<h2 id="조건-순서를-잘못-작성한-경우">조건 순서를 잘못 작성한 경우</h2>
<pre><code class="language-java">int score = 95;

if (score &gt;= 60) {
    System.out.println(&quot;D 이상&quot;);
} else if (score &gt;= 90) {
    System.out.println(&quot;A&quot;);
}</code></pre>
<p><code>95</code>는 첫 번째 조건인 <code>score &gt;= 60</code>을 만족하므로 두 번째 조건은 검사하지 않는다.</p>
<p>범위를 판별하는 조건은 일반적으로 큰 값부터 작은 값 순서로 작성한다.</p>
<pre><code class="language-java">if (score &gt;= 90) {
    System.out.println(&quot;A&quot;);
} else if (score &gt;= 80) {
    System.out.println(&quot;B&quot;);
} else if (score &gt;= 70) {
    System.out.println(&quot;C&quot;);
}</code></pre>
<hr>
<h1 id="5-중첩-if문">5. 중첩 if문</h1>
<p><code>if</code>문 내부에 또 다른 <code>if</code>문을 작성할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int age = 20;
        boolean hasTicket = true;

        if (age &gt;= 19) {
            if (hasTicket) {
                System.out.println(&quot;입장할 수 있습니다.&quot;);
            } else {
                System.out.println(&quot;티켓이 필요합니다.&quot;);
            }
        } else {
            System.out.println(&quot;성인만 입장할 수 있습니다.&quot;);
        }
    }
}</code></pre>
<p>다만 중첩이 너무 깊어지면 코드를 이해하기 어려워진다.</p>
<p>다음과 같이 논리 연산자를 활용해 조건을 합칠 수 있다.</p>
<pre><code class="language-java">if (age &gt;= 19 &amp;&amp; hasTicket) {
    System.out.println(&quot;입장할 수 있습니다.&quot;);
}</code></pre>
<hr>
<h1 id="6-switch문">6. switch문</h1>
<p><code>switch</code>문은 하나의 변수나 표현식의 결과를 여러 값과 비교할 때 사용한다.</p>
<pre><code class="language-java">switch (변수 또는 표현식) {
    case 값1:
        실행문 A;
        break;

    case 값2:
        실행문 B;
        break;

    default:
        실행문 C;
}</code></pre>
<p>요일 번호를 입력받아 요일을 출력하는 예제를 살펴보자.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int day = 3;

        switch (day) {
            case 1:
                System.out.println(&quot;월요일&quot;);
                break;

            case 2:
                System.out.println(&quot;화요일&quot;);
                break;

            case 3:
                System.out.println(&quot;수요일&quot;);
                break;

            case 4:
                System.out.println(&quot;목요일&quot;);
                break;

            case 5:
                System.out.println(&quot;금요일&quot;);
                break;

            default:
                System.out.println(&quot;주말입니다.&quot;);
        }
    }
}</code></pre>
<hr>
<h2 id="switch문의-break">switch문의 break</h2>
<p>기존 형태의 <code>switch</code>문에서 <code>break</code>는 현재 <code>switch</code>문을 종료한다.</p>
<p><code>break</code>를 작성하지 않으면 일치하는 <code>case</code> 이후의 코드까지 계속 실행된다. 이를 <strong>fall-through</strong>라고 한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int number = 1;

        switch (number) {
            case 1:
                System.out.println(&quot;첫 번째&quot;);

            case 2:
                System.out.println(&quot;두 번째&quot;);

            case 3:
                System.out.println(&quot;세 번째&quot;);
        }
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">첫 번째
두 번째
세 번째</code></pre>
<p>의도적으로 여러 <code>case</code>를 함께 처리하는 경우가 아니라면 <code>break</code>를 빠뜨리지 않아야 한다.</p>
<hr>
<h2 id="여러-case를-함께-처리하기">여러 case를 함께 처리하기</h2>
<p>기존 <code>switch</code>문에서는 여러 <code>case</code>를 이어 작성할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int month = 2;

        switch (month) {
            case 12:
            case 1:
            case 2:
                System.out.println(&quot;겨울&quot;);
                break;

            case 3:
            case 4:
            case 5:
                System.out.println(&quot;봄&quot;);
                break;

            case 6:
            case 7:
            case 8:
                System.out.println(&quot;여름&quot;);
                break;

            default:
                System.out.println(&quot;가을&quot;);
        }
    }
}</code></pre>
<hr>
<h1 id="7-switch-표현식">7. switch 표현식</h1>
<p>최근 Java에서는 화살표 <code>-&gt;</code>를 사용하는 switch 표현식을 사용할 수 있다.</p>
<p>switch 표현식은 다음과 같은 장점이 있다.</p>
<ul>
<li><code>switch</code> 자체가 값을 반환할 수 있다.</li>
<li>자동으로 해당 <code>case</code>만 실행하므로 <code>break</code>가 필요 없다.</li>
<li>여러 조건을 쉼표로 묶을 수 있다.</li>
<li>코드가 간결해진다.</li>
</ul>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int score = 85;

        char grade = switch (score / 10) {
            case 10, 9 -&gt; &#39;A&#39;;
            case 8 -&gt; &#39;B&#39;;
            case 7 -&gt; &#39;C&#39;;
            case 6 -&gt; &#39;D&#39;;
            default -&gt; &#39;F&#39;;
        };

        System.out.println(&quot;학점: &quot; + grade);
    }
}</code></pre>
<blockquote>
<p>화살표 형태의 switch는 Java 14부터 정식 문법으로 사용할 수 있다.</p>
</blockquote>
<hr>
<h2 id="switch-표현식에서-여러-문장-실행하기">switch 표현식에서 여러 문장 실행하기</h2>
<p>한 <code>case</code>에서 여러 문장을 실행하고 값을 반환하려면 중괄호와 <code>yield</code>를 사용한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String membership = &quot;GOLD&quot;;

        int discountRate = switch (membership) {
            case &quot;VIP&quot; -&gt; {
                System.out.println(&quot;VIP 회원입니다.&quot;);
                yield 20;
            }

            case &quot;GOLD&quot; -&gt; {
                System.out.println(&quot;GOLD 회원입니다.&quot;);
                yield 10;
            }

            default -&gt; {
                System.out.println(&quot;일반 회원입니다.&quot;);
                yield 0;
            }
        };

        System.out.println(&quot;할인율: &quot; + discountRate + &quot;%&quot;);
    }
}</code></pre>
<p><code>yield</code>는 switch 표현식의 결과값을 반환한다.</p>
<p>메서드에서 사용하는 <code>return</code>과 비슷해 보이지만, <code>yield</code>는 현재 switch 표현식의 값을 결정한다.</p>
<hr>
<h1 id="8-if문과-switch문의-차이">8. if문과 switch문의 차이</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>if문</th>
<th>switch문</th>
</tr>
</thead>
<tbody><tr>
<td>주요 용도</td>
<td>범위와 복합 조건 검사</td>
<td>하나의 값에 따른 분기</td>
</tr>
<tr>
<td>조건 표현</td>
<td>비교·논리 연산 가능</td>
<td>일치하는 값 중심</td>
</tr>
<tr>
<td>범위 조건</td>
<td>적합</td>
<td>일반적으로 부적합</td>
</tr>
<tr>
<td>복합 조건</td>
<td>적합</td>
<td>상대적으로 제한적</td>
</tr>
<tr>
<td>가독성</td>
<td>조건이 적으면 좋음</td>
<td>경우의 수가 많을 때 좋음</td>
</tr>
</tbody></table>
<p>다음과 같은 범위 조건에는 <code>if</code>가 적절하다.</p>
<pre><code class="language-java">if (score &gt;= 90) {
    System.out.println(&quot;A&quot;);
}</code></pre>
<p>다음과 같이 값이 명확히 나뉘는 경우에는 <code>switch</code>가 적절하다.</p>
<pre><code class="language-java">switch (menuNumber) {
    case 1:
        System.out.println(&quot;회원 조회&quot;);
        break;

    case 2:
        System.out.println(&quot;회원 등록&quot;);
        break;
}</code></pre>
<hr>
<h1 id="9-반복문이란">9. 반복문이란?</h1>
<p>반복문은 조건을 만족하는 동안 특정 코드를 반복 실행한다.</p>
<p>Java의 대표적인 반복문은 다음과 같다.</p>
<ul>
<li><code>for</code></li>
<li><code>while</code></li>
<li><code>do-while</code></li>
</ul>
<p>일반적으로 반복 횟수를 알고 있다면 <code>for</code>, 특정 조건이 만족될 때까지 반복한다면 <code>while</code>을 사용한다.</p>
<hr>
<h1 id="10-for문">10. for문</h1>
<p><code>for</code>문은 초기화식, 조건식, 증감식으로 구성된다.</p>
<pre><code class="language-java">for (초기화식; 조건식; 증감식) {
    반복할 실행문
}</code></pre>
<p>실행 순서는 다음과 같다.</p>
<pre><code class="language-text">1. 초기화식
2. 조건식 검사
3. 실행문 실행
4. 증감식 실행
5. 다시 조건식 검사</code></pre>
<p>1부터 5까지 출력하는 예제를 살펴보자.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        for (int i = 1; i &lt;= 5; i++) {
            System.out.println(i);
        }
    }
}</code></pre>
<p><code>int i = 1</code>은 반복이 시작될 때 한 번만 실행된다.</p>
<p><code>i &lt;= 5</code>가 <code>true</code>인 동안 반복하며, 한 번 반복할 때마다 <code>i++</code>가 실행된다.</p>
<hr>
<h2 id="1부터-100까지-합-구하기">1부터 100까지 합 구하기</h2>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int sum = 0;

        for (int i = 1; i &lt;= 100; i++) {
            sum += i;
        }

        System.out.println(&quot;합계: &quot; + sum);
    }
}</code></pre>
<hr>
<h2 id="for문의-변수-범위">for문의 변수 범위</h2>
<p><code>for</code>문의 초기화식에서 선언한 변수는 <code>for</code>문 내부에서만 사용할 수 있다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; 5; i++) {
    System.out.println(i);
}

// System.out.println(i);
// 컴파일 오류</code></pre>
<p>반복이 끝난 뒤에도 변수가 필요하다면 반복문 밖에서 선언해야 한다.</p>
<pre><code class="language-java">int i;

for (i = 0; i &lt; 5; i++) {
    System.out.println(i);
}

System.out.println(&quot;반복 종료 후 i: &quot; + i);</code></pre>
<hr>
<h1 id="11-중첩-for문">11. 중첩 for문</h1>
<p>반복문 내부에 또 다른 반복문을 작성할 수 있다.</p>
<p>구구단을 출력하는 예제를 살펴보자.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        for (int dan = 2; dan &lt;= 9; dan++) {
            System.out.println(&quot;[&quot; + dan + &quot;단]&quot;);

            for (int number = 1; number &lt;= 9; number++) {
                System.out.printf(
                        &quot;%d × %d = %d%n&quot;,
                        dan,
                        number,
                        dan * number
                );
            }

            System.out.println();
        }
    }
}</code></pre>
<p>바깥쪽 반복문이 한 번 실행될 때마다 안쪽 반복문은 처음부터 끝까지 실행된다.</p>
<hr>
<h1 id="12-while문">12. while문</h1>
<p><code>while</code>문은 조건식이 <code>true</code>인 동안 반복한다.</p>
<pre><code class="language-java">while (조건식) {
    실행문
}</code></pre>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int number = 1;

        while (number &lt;= 5) {
            System.out.println(number);
            number++;
        }
    }
}</code></pre>
<p><code>while</code>문에서는 증감식을 직접 작성해야 한다.</p>
<pre><code class="language-java">number++;</code></pre>
<p>증감이나 조건을 변경하는 코드를 빠뜨리면 무한 반복이 발생할 수 있다.</p>
<pre><code class="language-java">int number = 1;

while (number &lt;= 5) {
    System.out.println(number);

    // number가 변하지 않으므로 무한 반복
}</code></pre>
<hr>
<h2 id="while문은-언제-사용할까">while문은 언제 사용할까?</h2>
<p>반복 횟수를 정확히 모르고 특정 조건이 만족될 때까지 반복해야 할 때 유용하다.</p>
<p>사용자가 <code>0</code>을 입력할 때까지 숫자를 계속 더하는 예제다.</p>
<pre><code class="language-java">import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int sum = 0;

        while (true) {
            System.out.print(&quot;숫자를 입력하세요. 종료는 0: &quot;);
            int number = Integer.parseInt(scanner.nextLine());

            if (number == 0) {
                break;
            }

            sum += number;
        }

        System.out.println(&quot;입력한 숫자의 합: &quot; + sum);

        scanner.close();
    }
}</code></pre>
<hr>
<h1 id="13-do-while문">13. do-while문</h1>
<p><code>do-while</code>문은 실행문을 먼저 한 번 실행한 뒤 조건을 검사한다.</p>
<pre><code class="language-java">do {
    실행문
} while (조건식);</code></pre>
<p>조건이 처음부터 <code>false</code>여도 실행문이 최소 한 번 실행된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int number = 10;

        do {
            System.out.println(number);
            number++;
        } while (number &lt; 5);
    }
}</code></pre>
<p>조건 <code>number &lt; 5</code>는 처음부터 <code>false</code>지만 <code>10</code>이 한 번 출력된다.</p>
<hr>
<h2 id="사용자-입력-검증-예제">사용자 입력 검증 예제</h2>
<pre><code class="language-java">import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int menu;

        do {
            System.out.println(&quot;1. 조회&quot;);
            System.out.println(&quot;2. 등록&quot;);
            System.out.println(&quot;0. 종료&quot;);
            System.out.print(&quot;메뉴 선택: &quot;);

            menu = Integer.parseInt(scanner.nextLine());

            if (menu &lt; 0 || menu &gt; 2) {
                System.out.println(&quot;올바른 메뉴를 입력하세요.&quot;);
            }
        } while (menu &lt; 0 || menu &gt; 2);

        System.out.println(&quot;선택한 메뉴: &quot; + menu);

        scanner.close();
    }
}</code></pre>
<p>입력을 반드시 한 번 받아야 하므로 <code>do-while</code>문이 잘 어울린다.</p>
<hr>
<h1 id="14-for-while-do-while-비교">14. for, while, do-while 비교</h1>
<table>
<thead>
<tr>
<th>반복문</th>
<th>특징</th>
<th>적합한 상황</th>
</tr>
</thead>
<tbody><tr>
<td><code>for</code></td>
<td>초기화·조건·증감이 한곳에 있음</td>
<td>반복 횟수를 알고 있을 때</td>
</tr>
<tr>
<td><code>while</code></td>
<td>조건을 먼저 검사</td>
<td>종료 시점을 조건으로 판단할 때</td>
</tr>
<tr>
<td><code>do-while</code></td>
<td>실행 후 조건 검사</td>
<td>최소 한 번 실행해야 할 때</td>
</tr>
</tbody></table>
<p>동일한 작업을 어떤 반복문으로도 구현할 수 있지만, 상황에 가장 자연스러운 문법을 선택하는 것이 좋다.</p>
<hr>
<h1 id="15-break문">15. break문</h1>
<p><code>break</code>는 반복문이나 <code>switch</code>문의 실행을 즉시 종료한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        for (int i = 1; i &lt;= 10; i++) {
            if (i == 5) {
                break;
            }

            System.out.println(i);
        }
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">1
2
3
4</code></pre>
<p><code>i</code>가 5가 되는 순간 반복문이 종료된다.</p>
<hr>
<h1 id="16-continue문">16. continue문</h1>
<p><code>continue</code>는 현재 반복에서 남은 코드를 건너뛰고 다음 반복으로 이동한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        for (int i = 1; i &lt;= 10; i++) {
            if (i % 2 == 0) {
                continue;
            }

            System.out.println(i);
        }
    }
}</code></pre>
<p>짝수일 때 출력을 건너뛰므로 홀수만 출력된다.</p>
<pre><code class="language-text">1
3
5
7
9</code></pre>
<hr>
<h2 id="break와-continue-차이">break와 continue 차이</h2>
<pre><code class="language-text">break
→ 반복문 전체를 종료

continue
→ 현재 반복만 건너뛰고 다음 반복 진행</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>break</th>
<th>continue</th>
</tr>
</thead>
<tbody><tr>
<td>현재 반복</td>
<td>중단</td>
<td>중단</td>
</tr>
<tr>
<td>다음 반복</td>
<td>실행하지 않음</td>
<td>계속 실행</td>
</tr>
<tr>
<td>반복문 전체</td>
<td>종료</td>
<td>유지</td>
</tr>
</tbody></table>
<hr>
<h1 id="17-라벨을-이용한-반복문-종료">17. 라벨을 이용한 반복문 종료</h1>
<p>반복문이 중첩된 상태에서 일반적인 <code>break</code>를 사용하면 가장 가까운 반복문 하나만 종료한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        for (int i = 1; i &lt;= 3; i++) {
            for (int j = 1; j &lt;= 3; j++) {
                if (j == 2) {
                    break;
                }

                System.out.println(&quot;i = &quot; + i + &quot;, j = &quot; + j);
            }
        }
    }
}</code></pre>
<p>바깥쪽 반복문까지 한 번에 종료하려면 라벨을 사용할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        outer:
        for (int i = 1; i &lt;= 3; i++) {
            for (int j = 1; j &lt;= 3; j++) {
                if (i == 2 &amp;&amp; j == 2) {
                    break outer;
                }

                System.out.println(&quot;i = &quot; + i + &quot;, j = &quot; + j);
            }
        }

        System.out.println(&quot;반복 종료&quot;);
    }
}</code></pre>
<p>라벨은 메모에 적힌 다음 형태처럼 작성한다.</p>
<pre><code class="language-java">outer:
for (...) {
    for (...) {
        break outer;
    }
}</code></pre>
<blockquote>
<p>라벨을 과도하게 사용하면 코드 흐름을 추적하기 어려워질 수 있다. 가능하면 메서드 분리나 조건 재구성을 먼저 고려하는 것이 좋다.</p>
</blockquote>
<hr>
<h1 id="18-종합-예제-간단한-메뉴-프로그램">18. 종합 예제: 간단한 메뉴 프로그램</h1>
<p>조건문과 반복문을 함께 활용해 간단한 회원 관리 메뉴를 만들어보자.</p>
<pre><code class="language-java">import java.util.Scanner;

public class MemberMenu {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println();
            System.out.println(&quot;===== 회원 관리 =====&quot;);
            System.out.println(&quot;1. 회원 조회&quot;);
            System.out.println(&quot;2. 회원 등록&quot;);
            System.out.println(&quot;3. 회원 삭제&quot;);
            System.out.println(&quot;0. 종료&quot;);
            System.out.print(&quot;메뉴를 선택하세요: &quot;);

            int menu = Integer.parseInt(scanner.nextLine());

            switch (menu) {
                case 1:
                    System.out.println(&quot;회원 정보를 조회합니다.&quot;);
                    break;

                case 2:
                    System.out.println(&quot;새 회원을 등록합니다.&quot;);
                    break;

                case 3:
                    System.out.println(&quot;회원을 삭제합니다.&quot;);
                    break;

                case 0:
                    System.out.println(&quot;프로그램을 종료합니다.&quot;);
                    scanner.close();
                    return;

                default:
                    System.out.println(&quot;올바른 메뉴 번호를 입력하세요.&quot;);
            }
        }
    }
}</code></pre>
<p>이 예제에서는 다음 개념이 사용되었다.</p>
<ul>
<li><code>while (true)</code>를 이용한 반복 메뉴</li>
<li><code>switch</code>를 이용한 메뉴 분기</li>
<li><code>return</code>을 이용한 프로그램 종료</li>
<li><code>Scanner</code>를 이용한 입력</li>
<li><code>Integer.parseInt()</code>를 이용한 문자열 변환</li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념">헷갈리기 쉬운 개념</h1>
<h2 id="if문의-조건은-boolean이어야-한다">if문의 조건은 boolean이어야 한다</h2>
<pre><code class="language-java">int number = 1;

// if (number) {
//     사용할 수 없음
// }

if (number == 1) {
    System.out.println(&quot;1입니다.&quot;);
}</code></pre>
<hr>
<h2 id="과-는-다르다"><code>=</code>과 <code>==</code>는 다르다</h2>
<pre><code class="language-java">int number = 10;        // 대입
boolean result = number == 10; // 비교</code></pre>
<hr>
<h2 id="switch문의-break와-반복문의-break">switch문의 <code>break</code>와 반복문의 <code>break</code></h2>
<p>두 경우 모두 현재 제어 구조를 종료하지만 종료 대상이 다르다.</p>
<ul>
<li><code>switch</code> 안의 <code>break</code>: 해당 <code>switch</code> 종료</li>
<li>반복문 안의 <code>break</code>: 가장 가까운 반복문 종료</li>
</ul>
<hr>
<h2 id="while과-do-while"><code>while</code>과 <code>do-while</code></h2>
<pre><code class="language-java">while (조건식) {
    실행문
}</code></pre>
<p><code>while</code>은 조건을 먼저 검사한다.</p>
<pre><code class="language-java">do {
    실행문
} while (조건식);</code></pre>
<p><code>do-while</code>은 실행문을 먼저 실행하므로 최소 한 번 실행된다.</p>
<hr>
<h1 id="실무에서-알아두면-좋은-내용">실무에서 알아두면 좋은 내용</h1>
<p>조건문이 지나치게 중첩되면 읽기 어렵기 때문에, 실패 조건을 먼저 처리하고 메서드를 종료하는 <strong>가드 클로즈</strong> 방식을 활용할 수 있다.</p>
<pre><code class="language-java">public static void registerUser(String name, int age) {
    if (name == null || name.isBlank()) {
        System.out.println(&quot;이름이 올바르지 않습니다.&quot;);
        return;
    }

    if (age &lt; 0) {
        System.out.println(&quot;나이가 올바르지 않습니다.&quot;);
        return;
    }

    System.out.println(&quot;회원을 등록합니다.&quot;);
}</code></pre>
<p>다음과 같이 여러 단계로 중첩하는 것보다 읽기 쉽다.</p>
<pre><code class="language-java">if (name != null) {
    if (!name.isBlank()) {
        if (age &gt;= 0) {
            System.out.println(&quot;회원을 등록합니다.&quot;);
        }
    }
}</code></pre>
<p>반복문 내부에서 컬렉션을 순회할 때 요소를 직접 삭제하면 오류가 발생할 수 있으므로, 이후 컬렉션을 학습할 때 <code>Iterator</code>나 <code>removeIf()</code> 같은 방법도 함께 알아두는 것이 좋다.</p>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<ul>
<li>Java 프로그램은 기본적으로 위에서 아래로 실행된다.</li>
<li>제어문을 사용하면 실행 흐름을 조건에 따라 나누거나 반복할 수 있다.</li>
<li>조건문에는 <code>if</code>문과 <code>switch</code>문이 있다.</li>
<li><code>if</code>문의 조건식은 반드시 <code>boolean</code> 결과를 만들어야 한다.</li>
<li><code>else if</code> 조건은 위에서부터 순서대로 검사한다.</li>
<li>하나의 값을 여러 경우와 비교할 때는 <code>switch</code>문이 유용하다.</li>
<li>화살표를 사용하는 switch 표현식은 값을 반환하며 <code>break</code>가 필요 없다.</li>
<li>여러 문장을 실행하는 switch 표현식에서는 <code>yield</code>로 값을 반환한다.</li>
<li>반복 횟수를 알고 있다면 <code>for</code>문이 적합하다.</li>
<li>조건에 따라 반복한다면 <code>while</code>문이 적합하다.</li>
<li><code>do-while</code>문은 조건과 관계없이 최소 한 번 실행된다.</li>
<li><code>break</code>는 반복문 전체를 종료한다.</li>
<li><code>continue</code>는 현재 반복만 건너뛰고 다음 반복을 진행한다.</li>
<li>라벨을 이용하면 중첩 반복문 바깥까지 종료할 수 있지만 남용하지 않는 것이 좋다.</li>
</ul>
<hr>
<h1 id="배열">배열</h1>
<h2 id="들어가며-1">들어가며</h2>
<p>학생 30명의 점수를 저장해야 한다고 가정해보자.</p>
<p>배열을 사용하지 않는다면 다음과 같이 변수를 여러 개 선언해야 한다.</p>
<pre><code class="language-java">int score1 = 90;
int score2 = 80;
int score3 = 100;</code></pre>
<p>데이터가 30개, 100개, 1,000개로 늘어나면 각각의 변수를 직접 선언하고 처리하는 방식은 현실적이지 않다.</p>
<p>이 문제를 해결하기 위해 같은 타입의 데이터를 하나로 묶어 관리하는 자료구조가 배열이다.</p>
<pre><code class="language-java">int[] scores = {90, 80, 100};</code></pre>
<p>하지만 배열을 제대로 이해하려면 먼저 Java의 기본 타입과 참조 타입, 그리고 Stack과 Heap 메모리의 관계를 알아야 한다.</p>
<hr>
<h1 id="1-기본-타입과-참조-타입">1. 기본 타입과 참조 타입</h1>
<p>Java의 데이터 타입은 기본 타입과 참조 타입으로 구분된다.</p>
<h2 id="기본-타입">기본 타입</h2>
<p>기본 타입 변수는 정수, 실수, 문자, 논리값을 직접 저장한다.</p>
<pre><code class="language-java">int age = 20;
double height = 175.5;
char grade = &#39;A&#39;;
boolean isStudent = true;</code></pre>
<p>Java의 기본 타입은 다음 8개다.</p>
<pre><code class="language-text">byte
short
int
long
float
double
char
boolean</code></pre>
<hr>
<h2 id="참조-타입">참조 타입</h2>
<p>참조 타입 변수는 객체 자체가 아니라 객체를 참조하는 값을 저장한다.</p>
<p>대표적인 참조 타입은 다음과 같다.</p>
<ul>
<li>배열</li>
<li>열거 타입</li>
<li>클래스</li>
<li>인터페이스</li>
<li><code>String</code></li>
</ul>
<pre><code class="language-java">String name = &quot;김자바&quot;;
int[] scores = {90, 80, 100};</code></pre>
<p>개념적으로 다음과 같이 이해할 수 있다.</p>
<pre><code class="language-text">Stack 영역              Heap 영역

name     ───────────→   &quot;김자바&quot;

scores   ───────────→   [90][80][100]</code></pre>
<p>참조 타입 변수에는 객체를 찾아갈 수 있는 참조값이 저장된다.</p>
<blockquote>
<p>실제 JVM 구현에서 참조값이 반드시 사람이 볼 수 있는 메모리 주소 그 자체라고 단정할 수는 없다. 입문 단계에서는 객체의 위치를 찾아가기 위한 값이라고 이해하면 된다.</p>
</blockquote>
<hr>
<h1 id="2-java의-주요-메모리-영역">2. Java의 주요 메모리 영역</h1>
<p>Java 프로그램이 실행되면 JVM은 목적에 따라 메모리를 여러 영역으로 나누어 사용한다.</p>
<p>입문 단계에서 중요한 영역은 다음 세 가지다.</p>
<pre><code class="language-text">JVM 메모리
├─ Method Area
├─ Heap
└─ JVM Stack</code></pre>
<hr>
<h2 id="method-area">Method Area</h2>
<p>메서드 영역에는 JVM이 읽은 클래스 관련 정보가 저장된다.</p>
<p>대표적으로 다음과 같은 정보가 포함된다.</p>
<ul>
<li>클래스의 바이트 코드</li>
<li>메서드 코드</li>
<li>필드 정보</li>
<li>생성자 정보</li>
<li>정적 필드</li>
<li>런타임 상수 풀</li>
</ul>
<p>정확한 내부 구현은 JVM 버전과 구현체에 따라 달라질 수 있지만, 클래스 단위 정보가 관리되는 영역이라고 이해할 수 있다.</p>
<hr>
<h2 id="heap-영역">Heap 영역</h2>
<p>Heap에는 <code>new</code> 연산자 등을 통해 생성된 객체와 배열이 저장된다.</p>
<pre><code class="language-java">int[] scores = new int[3];
String name = new String(&quot;Java&quot;);</code></pre>
<p>위 코드에서 배열과 <code>String</code> 객체는 Heap에 생성된다.</p>
<p>더 이상 어떤 변수에서도 참조하지 않는 객체는 Garbage Collector의 정리 대상이 된다.</p>
<hr>
<h2 id="jvm-stack-영역">JVM Stack 영역</h2>
<p>메서드가 호출될 때마다 Stack에 프레임이 생성된다.</p>
<p>프레임에는 해당 메서드에서 사용하는 로컬 변수와 연산 중간값 등이 저장된다.</p>
<pre><code class="language-java">public static void main(String[] args) {
    int age = 20;
    int[] scores = new int[3];
}</code></pre>
<p>개념적으로 다음과 같이 표현할 수 있다.</p>
<pre><code class="language-text">JVM Stack                        Heap

main() 프레임
┌────────────────┐
│ age = 20       │
│ scores = 참조값 │ ───────────→ [0][0][0]
└────────────────┘</code></pre>
<p>메서드 실행이 끝나면 해당 프레임도 Stack에서 제거된다.</p>
<hr>
<h1 id="3-참조-타입의--비교">3. 참조 타입의 == 비교</h1>
<p>기본 타입에 <code>==</code>를 사용하면 값을 비교한다.</p>
<pre><code class="language-java">int first = 10;
int second = 10;

System.out.println(first == second);</code></pre>
<p>참조 타입에 <code>==</code>를 사용하면 두 변수가 같은 객체를 참조하는지 비교한다.</p>
<pre><code class="language-java">int[] first = {1, 2, 3};
int[] second = first;
int[] third = {1, 2, 3};

System.out.println(first == second);
System.out.println(first == third);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">true
false</code></pre>
<p><code>first</code>와 <code>second</code>는 같은 배열을 참조하지만, <code>third</code>는 내용이 같더라도 별도로 생성된 배열을 참조한다.</p>
<pre><code class="language-text">first  ──┐
         ├──→ [1][2][3]
second ──┘

third ──────→ [1][2][3]</code></pre>
<hr>
<h1 id="4-null과-nullpointerexception">4. null과 NullPointerException</h1>
<p>참조 타입 변수는 객체를 참조하지 않는 상태를 나타내기 위해 <code>null</code>을 가질 수 있다.</p>
<pre><code class="language-java">String name = null;
int[] scores = null;</code></pre>
<p>변수가 <code>null</code>인지 <code>==</code> 또는 <code>!=</code>로 확인할 수 있다.</p>
<pre><code class="language-java">if (name == null) {
    System.out.println(&quot;이름이 없습니다.&quot;);
}</code></pre>
<hr>
<h2 id="nullpointerexception">NullPointerException</h2>
<p><code>null</code> 상태인 참조 변수를 통해 객체의 필드, 메서드 또는 배열 요소에 접근하면 <code>NullPointerException</code>이 발생한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String text = null;

        System.out.println(text.length());
    }
}</code></pre>
<p><code>text</code>가 참조하는 <code>String</code> 객체가 없기 때문에 <code>length()</code>를 호출할 수 없다.</p>
<p>배열도 마찬가지다.</p>
<pre><code class="language-java">int[] numbers = null;

numbers[0] = 10;</code></pre>
<p>배열 객체가 생성되지 않았기 때문에 요소에 접근할 수 없다.</p>
<p>다음처럼 객체가 존재하는지 먼저 확인해야 한다.</p>
<pre><code class="language-java">if (text != null) {
    System.out.println(text.length());
}</code></pre>
<hr>
<h1 id="5-string-객체와-문자열-비교">5. String 객체와 문자열 비교</h1>
<p>문자열 리터럴이 동일하면 Java는 문자열 상수 풀에 있는 같은 객체를 재사용할 수 있다.</p>
<pre><code class="language-java">String first = &quot;Java&quot;;
String second = &quot;Java&quot;;

System.out.println(first == second);</code></pre>
<p>이 경우 <code>true</code>가 나올 가능성이 아니라, 동일한 문자열 리터럴은 일반적으로 같은 문자열 풀 객체를 참조하므로 <code>true</code>가 나온다.</p>
<p>반면 <code>new</code> 연산자로 생성하면 새로운 객체가 만들어진다.</p>
<pre><code class="language-java">String first = new String(&quot;Java&quot;);
String second = new String(&quot;Java&quot;);

System.out.println(first == second);</code></pre>
<p>실행 결과는 <code>false</code>다.</p>
<p>문자열의 내용은 <code>equals()</code>로 비교해야 한다.</p>
<pre><code class="language-java">System.out.println(first.equals(second));</code></pre>
<p>결과는 <code>true</code>다.</p>
<blockquote>
<p>문자열 비교에 <code>==</code>를 사용하면 생성 방식에 따라 결과가 달라질 수 있으므로 내용 비교에는 항상 <code>equals()</code>를 사용해야 한다.</p>
</blockquote>
<hr>
<h1 id="6-배열이란">6. 배열이란?</h1>
<p>배열은 같은 타입의 여러 데이터를 하나의 연속된 구조로 관리하는 자료구조다.</p>
<p>각 데이터에는 0부터 시작하는 인덱스가 부여된다.</p>
<pre><code class="language-text">scores

index    0     1      2
       ┌────┬────┬─────┐
value  │ 90 │ 80 │ 100 │
       └────┴────┴─────┘</code></pre>
<p>Java 배열의 특징은 다음과 같다.</p>
<ul>
<li>같은 타입의 값만 저장할 수 있다.</li>
<li>인덱스는 0부터 시작한다.</li>
<li>생성된 이후 길이를 변경할 수 없다.</li>
<li>배열 자체는 참조 타입이다.</li>
<li>배열의 요소는 기본 타입 값이나 객체 참조값을 저장한다.</li>
</ul>
<hr>
<h1 id="7-배열-변수-선언">7. 배열 변수 선언</h1>
<p>배열 변수는 다음 두 방식으로 선언할 수 있다.</p>
<pre><code class="language-java">int[] scores;
int scores2[];</code></pre>
<p>두 문법 모두 가능하지만 Java에서는 타입 뒤에 대괄호를 작성하는 첫 번째 방식을 권장한다.</p>
<pre><code class="language-java">int[] scores;</code></pre>
<p>이 방식은 변수의 타입이 <code>int 배열</code>이라는 점을 더 분명히 보여준다.</p>
<pre><code class="language-java">int number;       // int 타입
int[] numbers;    // int[] 타입</code></pre>
<p>배열 변수를 선언하는 것만으로 배열 객체가 생성되는 것은 아니다.</p>
<pre><code class="language-java">int[] numbers = null;</code></pre>
<p>이 상태에서는 사용할 배열이 존재하지 않는다.</p>
<hr>
<h1 id="8-값-목록으로-배열-생성하기">8. 값 목록으로 배열 생성하기</h1>
<p>배열을 선언하면서 값 목록을 제공할 수 있다.</p>
<pre><code class="language-java">int[] scores = {90, 80, 100};
String[] names = {&quot;김자바&quot;, &quot;이자바&quot;, &quot;박자바&quot;};</code></pre>
<p>배열 길이는 입력한 값의 개수에 따라 결정된다.</p>
<pre><code class="language-java">System.out.println(scores.length);</code></pre>
<p>결과는 <code>3</code>이다.</p>
<hr>
<h2 id="선언과-값-목록을-분리할-수-없는-이유">선언과 값 목록을 분리할 수 없는 이유</h2>
<p>다음 코드는 컴파일 오류가 발생한다.</p>
<pre><code class="language-java">int[] scores;

// scores = {90, 80, 100};
// 컴파일 오류</code></pre>
<p>중괄호만 사용하는 값 목록 방식은 배열 변수를 선언하는 문장에서만 사용할 수 있다.</p>
<p>나중에 값을 결정해야 한다면 <code>new</code>를 명시해야 한다.</p>
<pre><code class="language-java">int[] scores;

scores = new int[]{90, 80, 100};</code></pre>
<hr>
<h1 id="9-new-연산자로-배열-생성하기">9. new 연산자로 배열 생성하기</h1>
<p>배열의 길이만 알고 있고 값은 나중에 저장하려면 <code>new</code> 연산자를 사용한다.</p>
<pre><code class="language-java">int[] scores = new int[3];</code></pre>
<p>기본 형식은 다음과 같다.</p>
<pre><code class="language-text">타입[] 변수 = new 타입[길이];</code></pre>
<p>이미 변수를 선언했다면 생성문을 분리할 수 있다.</p>
<pre><code class="language-java">int[] scores;

scores = new int[3];</code></pre>
<hr>
<h1 id="10-배열의-기본값">10. 배열의 기본값</h1>
<p><code>new</code>로 배열을 생성하면 각 요소는 타입에 맞는 기본값으로 자동 초기화된다.</p>
<table>
<thead>
<tr>
<th>요소 타입</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td><code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code></td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>float</code>, <code>double</code></td>
<td><code>0.0</code></td>
</tr>
<tr>
<td><code>char</code></td>
<td><code>&#39;\u0000&#39;</code></td>
</tr>
<tr>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td>참조 타입</td>
<td><code>null</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] numbers = new int[3];
        boolean[] results = new boolean[3];
        String[] names = new String[3];

        System.out.println(Arrays.toString(numbers));
        System.out.println(Arrays.toString(results));
        System.out.println(Arrays.toString(names));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[0, 0, 0]
[false, false, false]
[null, null, null]</code></pre>
<hr>
<h1 id="11-배열-요소에-접근하기">11. 배열 요소에 접근하기</h1>
<p>배열 요소는 인덱스를 이용해 읽거나 변경한다.</p>
<pre><code class="language-java">int[] scores = new int[3];

scores[0] = 90;
scores[1] = 80;
scores[2] = 100;

System.out.println(scores[0]);</code></pre>
<p>배열의 첫 번째 요소는 인덱스 <code>0</code>이다.</p>
<pre><code class="language-text">첫 번째 요소 → 0
두 번째 요소 → 1
세 번째 요소 → 2</code></pre>
<hr>
<h2 id="arrayindexoutofboundsexception">ArrayIndexOutOfBoundsException</h2>
<p>배열의 범위를 벗어난 인덱스에 접근하면 <code>ArrayIndexOutOfBoundsException</code>이 발생한다.</p>
<pre><code class="language-java">int[] scores = new int[3];

scores[3] = 100;</code></pre>
<p>길이가 3인 배열에서 사용할 수 있는 인덱스는 <code>0</code>, <code>1</code>, <code>2</code>다.</p>
<p>마지막 인덱스는 다음과 같이 구할 수 있다.</p>
<pre><code class="language-java">int lastIndex = scores.length - 1;</code></pre>
<hr>
<h1 id="12-배열의-length">12. 배열의 length</h1>
<p>배열의 길이는 <code>length</code> 필드로 확인한다.</p>
<pre><code class="language-java">int[] scores = {90, 80, 100};

System.out.println(scores.length);</code></pre>
<p><code>String</code>의 길이는 메서드이므로 괄호를 사용하지만, 배열의 <code>length</code>는 필드이므로 괄호가 없다.</p>
<pre><code class="language-java">String text = &quot;Java&quot;;

System.out.println(text.length());
System.out.println(scores.length);</code></pre>
<ul>
<li>문자열: <code>length()</code></li>
<li>배열: <code>length</code></li>
</ul>
<hr>
<h1 id="13-for문으로-배열-순회하기">13. for문으로 배열 순회하기</h1>
<p>배열의 모든 요소를 처리할 때는 <code>for</code>문과 <code>length</code>를 함께 사용한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100};

        for (int i = 0; i &lt; scores.length; i++) {
            System.out.printf(
                    &quot;%d번째 점수: %d%n&quot;,
                    i,
                    scores[i]
            );
        }
    }
}</code></pre>
<p>조건식에는 배열의 마지막 인덱스가 아니라 길이를 사용한다.</p>
<pre><code class="language-java">i &lt; scores.length</code></pre>
<p><code>&lt;=</code>를 사용하면 범위를 벗어난 인덱스에 접근하게 된다.</p>
<pre><code class="language-java">// i &lt;= scores.length 사용 금지</code></pre>
<hr>
<h2 id="배열의-합계와-평균">배열의 합계와 평균</h2>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100, 70, 85};

        int sum = 0;

        for (int i = 0; i &lt; scores.length; i++) {
            sum += scores[i];
        }

        double average = (double) sum / scores.length;

        System.out.println(&quot;합계: &quot; + sum);
        System.out.println(&quot;평균: &quot; + average);
    }
}</code></pre>
<p>배열 길이를 <code>5</code>처럼 직접 작성하지 않고 <code>scores.length</code>를 사용하면 배열 크기가 변경되어도 코드를 수정할 필요가 없다.</p>
<hr>
<h1 id="14-향상된-for문">14. 향상된 for문</h1>
<p>향상된 <code>for</code>문은 배열이나 컬렉션의 요소를 처음부터 끝까지 순서대로 읽을 때 사용한다.</p>
<pre><code class="language-java">for (요소타입 변수 : 배열) {
    실행문
}</code></pre>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100};

        for (int score : scores) {
            System.out.println(score);
        }
    }
}</code></pre>
<p>각 반복마다 배열 요소가 <code>score</code> 변수에 대입된다.</p>
<hr>
<h2 id="향상된-for문의-한계">향상된 for문의 한계</h2>
<p>향상된 <code>for</code>문에서는 현재 요소의 인덱스를 직접 알 수 없다.</p>
<pre><code class="language-java">for (int score : scores) {
    System.out.println(score);
}</code></pre>
<p>인덱스가 필요하다면 일반 <code>for</code>문을 사용한다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; scores.length; i++) {
    System.out.println(i + &quot;: &quot; + scores[i]);
}</code></pre>
<p>또한 기본 타입 배열을 향상된 <code>for</code>문으로 순회하며 반복 변수 값을 변경해도 원본 배열은 바뀌지 않는다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        for (int number : numbers) {
            number = number * 10;
        }

        for (int number : numbers) {
            System.out.println(number);
        }
    }
}</code></pre>
<p>출력은 여전히 <code>1</code>, <code>2</code>, <code>3</code>이다.</p>
<p>원본 요소를 변경하려면 인덱스로 접근해야 한다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; numbers.length; i++) {
    numbers[i] *= 10;
}</code></pre>
<hr>
<h1 id="15-arraystostring">15. Arrays.toString()</h1>
<p>배열 변수를 그대로 출력하면 사람이 읽기 어려운 문자열이 나온다.</p>
<pre><code class="language-java">int[] numbers = {1, 2, 3};

System.out.println(numbers);</code></pre>
<p>배열 내용을 간단히 확인하려면 <code>Arrays.toString()</code>을 사용한다.</p>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        System.out.println(Arrays.toString(numbers));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[1, 2, 3]</code></pre>
<p><code>Arrays</code> 클래스를 사용하려면 import가 필요하다.</p>
<pre><code class="language-java">import java.util.Arrays;</code></pre>
<hr>
<h1 id="16-문자열을-char-배열과-int-배열로-변환하기">16. 문자열을 char 배열과 int 배열로 변환하기</h1>
<p>메모에 있는 <code>&quot;1234567890&quot;</code> 문자열 예제를 완성해보자.</p>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String original = &quot;1234567890&quot;;

        char[] characters = new char[original.length()];
        int[] numbers = new int[original.length()];

        for (int i = 0; i &lt; original.length(); i++) {
            characters[i] = original.charAt(i);
            numbers[i] = characters[i] - &#39;0&#39;;
        }

        System.out.println(Arrays.toString(characters));
        System.out.println(Arrays.toString(numbers));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]</code></pre>
<p><code>characters</code>의 요소는 문자이고 <code>numbers</code>의 요소는 숫자다.</p>
<pre><code class="language-java">numbers[i] = characters[i] - &#39;0&#39;;</code></pre>
<p>문자 <code>&#39;0&#39;</code>부터 <code>&#39;9&#39;</code>까지는 유니코드상 연속된 값을 가지므로 <code>&#39;0&#39;</code>을 빼면 실제 숫자를 얻을 수 있다.</p>
<pre><code class="language-text">&#39;3&#39; - &#39;0&#39;
→ 51 - 48
→ 3</code></pre>
<p>문자열을 문자 배열로 바꾸는 작업은 API를 사용해 간단히 처리할 수도 있다.</p>
<pre><code class="language-java">char[] characters = original.toCharArray();</code></pre>
<hr>
<h1 id="17-배열의-길이는-변경할-수-없다">17. 배열의 길이는 변경할 수 없다</h1>
<p>Java 배열은 한 번 생성되면 길이를 늘리거나 줄일 수 없다.</p>
<pre><code class="language-java">int[] numbers = new int[3];</code></pre>
<p>각 요소의 값은 변경할 수 있다.</p>
<pre><code class="language-java">numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;</code></pre>
<p>그러나 네 번째 요소를 추가할 수는 없다.</p>
<pre><code class="language-java">// numbers[3] = 40;
// 범위 초과</code></pre>
<p>더 큰 배열이 필요하다면 새로운 배열을 만들고 기존 값을 복사해야 한다.</p>
<pre><code class="language-java">int[] oldNumbers = {10, 20, 30};
int[] newNumbers = new int[5];

for (int i = 0; i &lt; oldNumbers.length; i++) {
    newNumbers[i] = oldNumbers[i];
}</code></pre>
<p>크기가 자주 변해야 한다면 배열보다 <code>ArrayList</code>가 더 적합하다.</p>
<hr>
<h1 id="18-for문을-이용한-배열-복사">18. for문을 이용한 배열 복사</h1>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] original = {10, 20, 30};
        int[] copied = new int[5];

        for (int i = 0; i &lt; original.length; i++) {
            copied[i] = original[i];
        }

        System.out.println(Arrays.toString(copied));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[10, 20, 30, 0, 0]</code></pre>
<hr>
<h1 id="19-systemarraycopy">19. System.arraycopy()</h1>
<p>Java가 제공하는 <code>System.arraycopy()</code>를 사용하면 배열의 일부 또는 전체를 복사할 수 있다.</p>
<pre><code class="language-java">System.arraycopy(
        원본배열,
        원본시작인덱스,
        대상배열,
        대상시작인덱스,
        복사할요소개수
);</code></pre>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String[] original = {&quot;Java&quot;, &quot;Spring&quot;, &quot;Vue&quot;};
        String[] copied = new String[5];

        System.arraycopy(
                original,
                0,
                copied,
                0,
                original.length
        );

        System.out.println(Arrays.toString(copied));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[Java, Spring, Vue, null, null]</code></pre>
<p>메서드 이름은 <code>arrayCopy</code>가 아니라 소문자 <code>c</code>를 사용한 다음 형태다.</p>
<pre><code class="language-java">System.arraycopy()</code></pre>
<hr>
<h1 id="20-arrayscopyof">20. Arrays.copyOf()</h1>
<p>배열 전체를 새로운 길이로 복사할 때는 <code>Arrays.copyOf()</code>가 간결하다.</p>
<pre><code class="language-java">import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] original = {10, 20, 30};

        int[] copied = Arrays.copyOf(original, 5);

        System.out.println(Arrays.toString(copied));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">[10, 20, 30, 0, 0]</code></pre>
<p>두 번째 인수는 새 배열의 길이다.</p>
<pre><code class="language-java">Arrays.copyOf(원본배열, 새로운길이);</code></pre>
<hr>
<h1 id="21-얕은-복사와-참조값-복사">21. 얕은 복사와 참조값 복사</h1>
<p>참조 타입 배열을 복사할 때는 배열 요소가 참조하는 객체를 새로 만드는 것이 아니라 참조값이 복사된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        StringBuilder[] original = {
                new StringBuilder(&quot;Java&quot;)
        };

        StringBuilder[] copied = new StringBuilder[1];

        System.arraycopy(original, 0, copied, 0, 1);

        copied[0].append(&quot; Programming&quot;);

        System.out.println(original[0]);
        System.out.println(copied[0]);
    }
}</code></pre>
<p>실행 결과는 두 배열 모두 다음 문자열을 출력한다.</p>
<pre><code class="language-text">Java Programming</code></pre>
<p>배열 객체는 서로 다르지만 내부 요소가 같은 <code>StringBuilder</code> 객체를 참조하기 때문이다.</p>
<pre><code class="language-text">original ──→ [참조값] ──┐
                         ├─→ StringBuilder 객체
copied   ──→ [참조값] ──┘</code></pre>
<p>이와 같이 객체 자체가 아닌 참조값만 복사하는 방식을 얕은 복사라고 한다.</p>
<hr>
<h1 id="22-다차원-배열">22. 다차원 배열</h1>
<p>Java의 다차원 배열은 배열이 다른 배열을 참조하는 구조다.</p>
<p>2차원 배열은 다음과 같이 선언하고 생성한다.</p>
<pre><code class="language-java">int[][] scores = new int[2][3];</code></pre>
<p>첫 번째 길이는 행, 두 번째 길이는 열로 이해할 수 있다.</p>
<pre><code class="language-text">scores
├─ scores[0] → [0][0][0]
└─ scores[1] → [0][0][0]</code></pre>
<p>값을 저장할 때는 두 개의 인덱스를 사용한다.</p>
<pre><code class="language-java">scores[0][0] = 90;
scores[0][1] = 80;
scores[0][2] = 100;

scores[1][0] = 70;
scores[1][1] = 85;
scores[1][2] = 95;</code></pre>
<hr>
<h2 id="2차원-배열-생성과-초기화">2차원 배열 생성과 초기화</h2>
<pre><code class="language-java">int[][] matrix = {
        {1, 2, 3},
        {4, 5, 6}
};</code></pre>
<p>중첩 반복문으로 모든 요소를 출력할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int[][] matrix = {
                {1, 2, 3},
                {4, 5, 6}
        };

        for (int row = 0; row &lt; matrix.length; row++) {
            for (int column = 0;
                 column &lt; matrix[row].length;
                 column++) {

                System.out.print(matrix[row][column] + &quot; &quot;);
            }

            System.out.println();
        }
    }
}</code></pre>
<p><code>matrix.length</code>는 행 배열의 길이고, <code>matrix[row].length</code>는 해당 행의 열 길이다.</p>
<hr>
<h1 id="23-계단식-배열">23. 계단식 배열</h1>
<p>Java의 2차원 배열은 각 행의 길이가 서로 다를 수 있다.</p>
<pre><code class="language-java">int[][] scores = new int[2][];

scores[0] = new int[2];
scores[1] = new int[3];</code></pre>
<p>메모리 구조는 다음과 같다.</p>
<pre><code class="language-text">scores
├─ scores[0] → [0][0]
└─ scores[1] → [0][0][0]</code></pre>
<p>값 목록으로도 만들 수 있다.</p>
<pre><code class="language-java">int[][] numbers = {
        {1, 2},
        {3, 4, 5},
        {6}
};</code></pre>
<p>이를 계단식 배열 또는 가변 길이 배열이라고 부르기도 한다.</p>
<hr>
<h1 id="24-객체를-참조하는-배열">24. 객체를 참조하는 배열</h1>
<p>참조 타입 배열의 요소에는 객체의 참조값이 저장된다.</p>
<pre><code class="language-java">String[] languages = new String[3];

languages[0] = &quot;Java&quot;;
languages[1] = &quot;Java&quot;;
languages[2] = new String(&quot;Java&quot;);</code></pre>
<p>관계는 다음과 같이 볼 수 있다.</p>
<pre><code class="language-text">languages[0] ──┐
               ├──→ 문자열 풀의 &quot;Java&quot;
languages[1] ──┘

languages[2] ─────→ new String(&quot;Java&quot;)</code></pre>
<p>따라서 다음 결과가 나온다.</p>
<pre><code class="language-java">System.out.println(languages[0] == languages[1]);
System.out.println(languages[0] == languages[2]);
System.out.println(languages[0].equals(languages[2]));</code></pre>
<pre><code class="language-text">true
false
true</code></pre>
<hr>
<h1 id="25-명령행-인수와-string-args">25. 명령행 인수와 String[] args</h1>
<p><code>main()</code> 메서드의 매개변수는 문자열 배열이다.</p>
<pre><code class="language-java">public static void main(String[] args)</code></pre>
<p>프로그램을 실행할 때 전달한 값들이 <code>args</code> 배열에 저장된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        System.out.println(&quot;전달된 값의 개수: &quot; + args.length);

        for (String argument : args) {
            System.out.println(argument);
        }
    }
}</code></pre>
<p>예를 들어 다음과 같이 실행했다고 가정하자.</p>
<pre><code class="language-bash">java Main Java Spring 100</code></pre>
<p><code>args</code> 배열은 다음 값을 가진다.</p>
<pre><code class="language-text">args[0] = &quot;Java&quot;
args[1] = &quot;Spring&quot;
args[2] = &quot;100&quot;</code></pre>
<p>명령행 인수를 전달하지 않아도 일반적으로 <code>args</code>는 <code>null</code>이 아니라 길이가 0인 배열이다.</p>
<hr>
<h1 id="26-열거-타입">26. 열거 타입</h1>
<p>데이터가 몇 가지 한정된 값만 가질 수 있다면 열거 타입을 사용할 수 있다.</p>
<p>요일은 다음 일곱 값 중 하나만 가진다.</p>
<pre><code class="language-text">MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY</code></pre>
<p>문자열로 관리하면 오타가 발생할 수 있다.</p>
<pre><code class="language-java">String today = &quot;SUNDEY&quot;;</code></pre>
<p>열거 타입을 사용하면 허용된 값만 저장할 수 있다.</p>
<hr>
<h2 id="열거-타입-선언">열거 타입 선언</h2>
<p><code>Week.java</code> 파일을 생성한다.</p>
<pre><code class="language-java">public enum Week {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}</code></pre>
<p>열거 상수는 관례적으로 모두 대문자와 언더바를 사용한다.</p>
<hr>
<h2 id="열거-타입-사용">열거 타입 사용</h2>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        Week today = Week.FRIDAY;

        if (today == Week.FRIDAY) {
            System.out.println(&quot;오늘은 금요일입니다.&quot;);
        }
    }
}</code></pre>
<p>열거 상수는 정해진 하나의 객체이므로 <code>==</code>를 사용해 안전하게 비교할 수 있다.</p>
<p>switch와도 잘 어울린다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        Week today = Week.SATURDAY;

        String message = switch (today) {
            case MONDAY, TUESDAY, WEDNESDAY,
                 THURSDAY, FRIDAY -&gt; &quot;평일입니다.&quot;;

            case SATURDAY, SUNDAY -&gt; &quot;주말입니다.&quot;;
        };

        System.out.println(message);
    }
}</code></pre>
<hr>
<h1 id="27-종합-예제-학생-점수-관리">27. 종합 예제: 학생 점수 관리</h1>
<p>배열, 반복문, 조건문을 함께 활용해 점수 통계를 계산해보자.</p>
<pre><code class="language-java">import java.util.Arrays;

public class ScoreManager {

    public static void main(String[] args) {
        int[] scores = {85, 92, 78, 100, 66};

        int sum = 0;
        int maximum = scores[0];
        int minimum = scores[0];

        for (int score : scores) {
            sum += score;

            if (score &gt; maximum) {
                maximum = score;
            }

            if (score &lt; minimum) {
                minimum = score;
            }
        }

        double average = (double) sum / scores.length;

        System.out.println(&quot;점수: &quot;
                + Arrays.toString(scores));

        System.out.println(&quot;합계: &quot; + sum);
        System.out.printf(&quot;평균: %.2f%n&quot;, average);
        System.out.println(&quot;최고 점수: &quot; + maximum);
        System.out.println(&quot;최저 점수: &quot; + minimum);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">점수: [85, 92, 78, 100, 66]
합계: 421
평균: 84.20
최고 점수: 100
최저 점수: 66</code></pre>
<hr>
<h1 id="헷갈리기-쉬운-개념-1">헷갈리기 쉬운 개념</h1>
<h2 id="배열-변수와-배열-객체">배열 변수와 배열 객체</h2>
<pre><code class="language-java">int[] numbers;</code></pre>
<p>배열 변수만 선언한 상태다.</p>
<pre><code class="language-java">numbers = new int[3];</code></pre>
<p>실제 배열 객체를 생성하고 참조값을 변수에 저장한 상태다.</p>
<hr>
<h2 id="배열-길이와-마지막-인덱스">배열 길이와 마지막 인덱스</h2>
<p>길이가 3인 배열의 인덱스는 <code>0</code>, <code>1</code>, <code>2</code>다.</p>
<pre><code class="language-java">int length = numbers.length;
int lastIndex = numbers.length - 1;</code></pre>
<hr>
<h2 id="배열의-length와-string의-length">배열의 length와 String의 length()</h2>
<pre><code class="language-java">numbers.length
text.length()</code></pre>
<p>배열은 필드, 문자열은 메서드다.</p>
<hr>
<h2 id="배열-변수-복사와-배열-요소-복사">배열 변수 복사와 배열 요소 복사</h2>
<pre><code class="language-java">int[] first = {1, 2, 3};
int[] second = first;</code></pre>
<p>새로운 배열이 만들어진 것이 아니다. 두 변수가 같은 배열을 참조한다.</p>
<pre><code class="language-java">second[0] = 100;

System.out.println(first[0]);</code></pre>
<p>결과도 <code>100</code>이다.</p>
<p>독립된 배열을 만들려면 실제 복사가 필요하다.</p>
<pre><code class="language-java">int[] second = Arrays.copyOf(first, first.length);</code></pre>
<hr>
<h1 id="실무에서-알아두면-좋은-내용-1">실무에서 알아두면 좋은 내용</h1>
<p>배열은 크기가 고정되어 있고 같은 타입의 값을 빠르게 순차 처리하는 데 적합하다.</p>
<p>다음과 같은 경우 배열이 잘 어울린다.</p>
<ul>
<li>크기가 고정된 좌표값</li>
<li>월별 데이터 12개</li>
<li>RGB 값</li>
<li>알고리즘 문제의 입력 데이터</li>
<li>행렬 계산</li>
</ul>
<p>데이터의 추가와 삭제가 자주 발생한다면 <code>ArrayList</code> 같은 컬렉션이 더 적합하다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();</code></pre>
<p>배열의 내용을 비교할 때도 <code>==</code>를 사용하면 안 된다.</p>
<pre><code class="language-java">int[] first = {1, 2, 3};
int[] second = {1, 2, 3};

System.out.println(first == second);</code></pre>
<p>결과는 <code>false</code>다.</p>
<p>요소의 내용을 비교하려면 <code>Arrays.equals()</code>를 사용한다.</p>
<pre><code class="language-java">import java.util.Arrays;

System.out.println(Arrays.equals(first, second));</code></pre>
<p>2차원 이상 배열의 내용을 비교할 때는 <code>Arrays.deepEquals()</code>를 사용한다.</p>
<pre><code class="language-java">Arrays.deepEquals(firstMatrix, secondMatrix);</code></pre>
<p>2차원 배열을 출력할 때는 <code>Arrays.deepToString()</code>을 사용할 수 있다.</p>
<pre><code class="language-java">System.out.println(Arrays.deepToString(matrix));</code></pre>
<hr>
<h1 id="오늘-배운-내용-정리-1">오늘 배운 내용 정리</h1>
<ul>
<li>Java의 데이터 타입은 기본 타입과 참조 타입으로 구분된다.</li>
<li>기본 타입 변수는 값을 직접 저장하고, 참조 타입 변수는 객체를 참조하는 값을 저장한다.</li>
<li>배열은 참조 타입이며 배열 객체는 Heap 영역에 생성된다.</li>
<li>메서드가 호출될 때 JVM Stack에 프레임이 생성되고, 로컬 변수가 프레임에서 관리된다.</li>
<li>참조 타입에서 <code>==</code>는 같은 객체를 참조하는지 비교한다.</li>
<li>객체의 내용을 비교하려면 타입에 맞는 <code>equals()</code> 또는 배열 비교 API를 사용해야 한다.</li>
<li>참조 타입 변수는 <code>null</code>을 가질 수 있다.</li>
<li><code>null</code>인 변수로 객체를 사용하면 <code>NullPointerException</code>이 발생한다.</li>
<li>배열은 같은 타입의 여러 데이터를 인덱스로 관리한다.</li>
<li>배열 인덱스는 0부터 시작하며 마지막 인덱스는 <code>length - 1</code>이다.</li>
<li>배열은 생성된 이후 길이를 변경할 수 없다.</li>
<li><code>new</code>로 생성한 배열 요소는 타입별 기본값으로 초기화된다.</li>
<li>일반 <code>for</code>문은 인덱스가 필요할 때 사용하고, 향상된 <code>for</code>문은 요소를 순서대로 읽을 때 편리하다.</li>
<li><code>Arrays.toString()</code>으로 배열 내용을 쉽게 출력할 수 있다.</li>
<li>배열은 반복문, <code>System.arraycopy()</code>, <code>Arrays.copyOf()</code> 등으로 복사할 수 있다.</li>
<li>참조 타입 배열을 복사하면 기본적으로 요소가 가진 참조값이 복사된다.</li>
<li>Java의 2차원 배열은 각 행이 다른 배열을 참조하는 구조다.</li>
<li>각 행의 길이가 다른 계단식 배열도 만들 수 있다.</li>
<li><code>String[] args</code>에는 프로그램 실행 시 전달한 명령행 인수가 저장된다.</li>
<li>한정된 값만 사용하는 데이터는 열거 타입으로 표현할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA (1) 기본 타입 ~ 형변환]]></title>
            <link>https://velog.io/@tongmill_g/JAVA-1-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85-%ED%98%95%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@tongmill_g/JAVA-1-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85-%ED%98%95%EB%B3%80%ED%99%98</guid>
            <pubDate>Mon, 20 Jul 2026 23:54:51 GMT</pubDate>
            <description><![CDATA[<h1 id="java-변수에는-무엇이-저장될까-기본-타입부터-형-변환까지-완벽-정리">Java 변수에는 무엇이 저장될까? 기본 타입부터 형 변환까지 완벽 정리</h1>
<h2 id="들어가며">들어가며</h2>
<p>Java 프로그램을 작성할 때 가장 먼저 접하는 개념은 변수와 데이터 타입이다.</p>
<p>처음에는 변수를 단순히 “값을 담는 상자”라고 배우지만, 실제로는 데이터의 종류와 크기, 메모리 저장 방식, 연산 결과까지 모두 데이터 타입의 영향을 받는다.</p>
<p>예를 들어 다음 코드는 비슷해 보이지만 서로 다른 결과를 만든다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        System.out.println(1 / 2);
        System.out.println(1.0 / 2);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">0
0.5</code></pre>
<p>첫 번째 연산은 정수끼리 계산했기 때문에 결과도 정수가 되고, 두 번째 연산은 실수가 포함되어 있어 실수 연산이 수행된다.</p>
<p>이번 글에서는 Java 소스 코드의 기본 구조부터 변수, 기본 타입, 문자열, 타입 변환, 콘솔 입출력까지 차근차근 정리해보려고 한다.</p>
<hr>
<h1 id="1-java-프로그램의-기본-구조">1. Java 프로그램의 기본 구조</h1>
<p>Java 소스 코드는 일반적으로 패키지 선언, 클래스 선언, 메서드 선언, 실행문으로 구성된다.</p>
<pre><code class="language-java">package sec03.exam01;

public class Hello {

    public static void main(String[] args) {
        System.out.println(&quot;Hello, Java&quot;);
    }
}</code></pre>
<p>각 부분을 나누어 살펴보자.</p>
<hr>
<h2 id="패키지-선언">패키지 선언</h2>
<pre><code class="language-java">package sec03.exam01;</code></pre>
<p><code>package</code>는 현재 클래스가 속한 패키지를 선언한다.</p>
<p>패키지는 관련된 클래스들을 그룹으로 관리하기 위한 구조이며, 일반적으로 디렉터리 구조와 일치한다.</p>
<p>위 코드가 정상적으로 작성되었다면 파일은 다음과 같은 위치에 존재해야 한다.</p>
<pre><code class="language-text">src
└─ sec03
   └─ exam01
      └─ Hello.java</code></pre>
<blockquote>
<p>패키지 이름은 일반적으로 모두 소문자로 작성한다.</p>
</blockquote>
<hr>
<h2 id="클래스-선언">클래스 선언</h2>
<pre><code class="language-java">public class Hello {
}</code></pre>
<p>Java 프로그램의 코드는 기본적으로 클래스 내부에 작성된다.</p>
<p><code>public class Hello</code>는 <code>Hello</code>라는 이름의 공개 클래스를 선언한다는 의미다.</p>
<p><code>public</code> 클래스가 존재하는 소스 파일의 이름은 클래스 이름과 같아야 한다.</p>
<pre><code class="language-text">클래스 이름: Hello
파일 이름: Hello.java</code></pre>
<hr>
<h2 id="main-메서드">main 메서드</h2>
<pre><code class="language-java">public static void main(String[] args) {
}</code></pre>
<p><code>main()</code> 메서드는 일반 Java 애플리케이션의 시작점이다. JVM은 프로그램을 실행할 때 이 메서드를 찾아 내부의 코드를 위에서부터 실행한다.</p>
<p>각 키워드는 다음과 같은 의미를 가진다.</p>
<ul>
<li><code>public</code>: JVM이 클래스 외부에서 메서드에 접근할 수 있도록 한다.</li>
<li><code>static</code>: 객체를 생성하지 않아도 메서드를 실행할 수 있도록 한다.</li>
<li><code>void</code>: 메서드가 값을 반환하지 않는다는 뜻이다.</li>
<li><code>main</code>: JVM이 프로그램의 시작점으로 인식하는 메서드 이름이다.</li>
<li><code>String[] args</code>: 프로그램 실행 시 전달받은 명령행 인수를 저장한다.</li>
</ul>
<p><code>String[] args</code>는 메서드가 받는 값을 정의하는 <strong>매개변수</strong>다.</p>
<hr>
<h2 id="실행문">실행문</h2>
<pre><code class="language-java">System.out.println(&quot;Hello, Java&quot;);</code></pre>
<p>실행문은 실제 동작을 수행하는 코드다.</p>
<p>main 메서드 블록 내부에는 다음과 같은 실행문을 작성할 수 있다.</p>
<pre><code class="language-java">int age = 20;                     // 변수 선언과 초기화
age = 21;                         // 변수에 값 저장
System.out.println(age);          // 메서드 호출</code></pre>
<p>대부분의 Java 실행문 끝에는 세미콜론을 작성한다.</p>
<pre><code class="language-java">;</code></pre>
<p>세미콜론은 하나의 실행문이 끝났다는 것을 의미한다.</p>
<hr>
<h1 id="2-java의-주석">2. Java의 주석</h1>
<p>주석은 소스 코드에 설명을 작성할 때 사용한다.</p>
<p>주석은 컴파일 과정에서 제거되기 때문에 생성된 바이트 코드에는 포함되지 않는다.</p>
<hr>
<h2 id="한-줄-주석">한 줄 주석</h2>
<p><code>//</code> 뒤에 작성한 내용은 해당 줄이 끝날 때까지 주석으로 처리된다.</p>
<pre><code class="language-java">int age = 20; // 사용자의 나이</code></pre>
<hr>
<h2 id="범위-주석">범위 주석</h2>
<p><code>/*</code>와 <code>*/</code> 사이의 모든 내용을 주석으로 처리한다.</p>
<pre><code class="language-java">/*
 * 회원의 이름과 나이를 저장한다.
 * 테스트용 코드다.
 */
String name = &quot;김자바&quot;;
int age = 20;</code></pre>
<hr>
<h2 id="문서화-주석">문서화 주석</h2>
<p><code>/**</code>와 <code>*/</code> 사이에 작성하며, 클래스나 메서드의 API 문서를 만들 때 사용한다.</p>
<pre><code class="language-java">/**
 * 전달받은 두 정수를 더한다.
 *
 * @param left 첫 번째 정수
 * @param right 두 번째 정수
 * @return 두 정수의 합
 */
public static int add(int left, int right) {
    return left + right;
}</code></pre>
<p>문서화 주석은 <code>javadoc</code> 도구를 이용해 HTML 형식의 API 문서를 생성할 때 활용할 수 있다.</p>
<blockquote>
<p>주석은 코드가 무엇을 하는지 반복해서 적기보다는, 코드만으로 알기 어려운 이유나 의도를 설명하는 데 사용하는 것이 좋다.</p>
</blockquote>
<hr>
<h1 id="3-변수란">3. 변수란?</h1>
<p>컴퓨터의 메모리는 값을 저장할 수 있는 수많은 공간으로 구성되어 있다.</p>
<p>프로그램이 메모리 주소를 직접 사용해야 한다면 특정 값이 어디에 저장되었는지 계속 기억해야 하므로 프로그램을 작성하기 매우 어려워진다.</p>
<p>이 문제를 해결하기 위해 사용하는 것이 변수다.</p>
<blockquote>
<p>변수는 값을 저장할 수 있는 메모리 공간에 붙인 이름이다.</p>
</blockquote>
<p>개발자는 실제 메모리 주소 대신 변수 이름을 사용해 값을 저장하거나 읽을 수 있다.</p>
<pre><code class="language-java">int age = 20;

System.out.println(age);</code></pre>
<p>위 코드에서 <code>age</code>는 변수 이름이며, <code>20</code>은 변수에 저장된 값이다.</p>
<hr>
<h2 id="변수-선언">변수 선언</h2>
<p>변수를 사용하려면 먼저 데이터 타입과 이름을 작성해야 한다.</p>
<pre><code class="language-java">int age;</code></pre>
<p>기본 형식은 다음과 같다.</p>
<pre><code class="language-text">데이터타입 변수이름;</code></pre>
<p>변수 선언은 Java에 다음 내용을 알려주는 과정이다.</p>
<ul>
<li>어떤 종류의 값을 저장할 것인지</li>
<li>해당 값을 어떤 이름으로 사용할 것인지</li>
<li>필요한 메모리 크기가 얼마인지</li>
</ul>
<hr>
<h2 id="값-대입">값 대입</h2>
<p>변수에 값을 저장할 때는 대입 연산자 <code>=</code>를 사용한다.</p>
<pre><code class="language-java">int age;
age = 20;</code></pre>
<p>대입 연산자는 오른쪽 값을 왼쪽 변수에 저장한다.</p>
<p>수학에서의 등호와는 의미가 다르다.</p>
<pre><code class="language-java">age = age + 1;</code></pre>
<p>위 코드는 현재 <code>age</code> 값에 1을 더한 후 다시 <code>age</code>에 저장한다는 의미다.</p>
<hr>
<h2 id="변수-초기화">변수 초기화</h2>
<p>변수를 선언하면서 최초의 값을 저장하는 것을 초기화라고 한다.</p>
<pre><code class="language-java">int age = 20;</code></pre>
<p>이때 <code>20</code>을 초기값이라고 부른다.</p>
<p>Java의 로컬 변수는 초기화하지 않은 상태로 사용할 수 없다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int age;

        // System.out.println(age);
        // 컴파일 오류: age가 초기화되지 않음
    }
}</code></pre>
<hr>
<h2 id="변수의-사용-범위">변수의 사용 범위</h2>
<p>메서드나 제어문의 중괄호 내부에서 선언한 변수를 로컬 변수라고 한다.</p>
<p>로컬 변수는 자신이 선언된 중괄호 블록 내부에서만 사용할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int outer = 10;

        if (outer &gt; 0) {
            int inner = 20;

            System.out.println(outer);
            System.out.println(inner);
        }

        System.out.println(outer);

        // System.out.println(inner);
        // 컴파일 오류: inner는 if 블록 밖에서 사용할 수 없음
    }
}</code></pre>
<hr>
<h1 id="4-변수-이름을-작성하는-규칙">4. 변수 이름을 작성하는 규칙</h1>
<p>Java의 변수 이름은 다음 규칙을 따라야 한다.</p>
<ul>
<li>첫 번째 문자는 문자, <code>$</code>, <code>_</code> 중 하나여야 한다.</li>
<li>숫자로 시작할 수 없다.</li>
<li>공백을 포함할 수 없다.</li>
<li>Java 예약어를 사용할 수 없다.</li>
<li>영어 대문자와 소문자를 구분한다.</li>
</ul>
<pre><code class="language-java">int age;
int userAge;
int _count;
int $price;</code></pre>
<p>다음 이름은 사용할 수 없다.</p>
<pre><code class="language-java">// int 1age;
// int user age;
// int class;</code></pre>
<p>규칙상 <code>$</code>와 <code>_</code>로 시작할 수 있지만, 일반적인 변수 이름은 영어 소문자로 시작하는 <strong>lowerCamelCase</strong> 방식을 사용한다.</p>
<pre><code class="language-java">int studentAge;
String userName;
double totalPrice;</code></pre>
<p>상수는 대문자와 언더바를 사용한다.</p>
<pre><code class="language-java">final int MAX_COUNT = 100;</code></pre>
<hr>
<h1 id="5-java의-데이터-타입">5. Java의 데이터 타입</h1>
<p>Java의 데이터 타입은 크게 기본 타입과 참조 타입으로 나뉜다.</p>
<h2 id="기본-타입">기본 타입</h2>
<p>기본 타입 변수에는 실제 값이 저장된다.</p>
<p>Java의 기본 타입은 총 8개다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>타입</th>
</tr>
</thead>
<tbody><tr>
<td>정수</td>
<td><code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code></td>
</tr>
<tr>
<td>문자</td>
<td><code>char</code></td>
</tr>
<tr>
<td>실수</td>
<td><code>float</code>, <code>double</code></td>
</tr>
<tr>
<td>논리</td>
<td><code>boolean</code></td>
</tr>
</tbody></table>
<h2 id="참조-타입">참조 타입</h2>
<p>참조 타입 변수에는 객체가 존재하는 위치를 참조하는 값이 저장된다.</p>
<p>대표적인 참조 타입은 다음과 같다.</p>
<ul>
<li>배열</li>
<li>클래스</li>
<li>인터페이스</li>
<li>열거 타입</li>
<li><code>String</code></li>
</ul>
<pre><code class="language-java">String name = &quot;김자바&quot;;
int[] scores = {90, 80, 100};</code></pre>
<p><code>String</code>은 기본 타입처럼 자주 사용되지만 실제로는 클래스이며, 참조 타입이다.</p>
<hr>
<h1 id="6-정수-타입">6. 정수 타입</h1>
<p>Java의 정수 타입은 <code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code>이 있다.</p>
<table>
<thead>
<tr>
<th>타입</th>
<th align="right">크기</th>
<th>대략적인 범위</th>
</tr>
</thead>
<tbody><tr>
<td><code>byte</code></td>
<td align="right">1바이트</td>
<td>-128 ~ 127</td>
</tr>
<tr>
<td><code>short</code></td>
<td align="right">2바이트</td>
<td>-32,768 ~ 32,767</td>
</tr>
<tr>
<td><code>int</code></td>
<td align="right">4바이트</td>
<td>약 -21억 ~ 21억</td>
</tr>
<tr>
<td><code>long</code></td>
<td align="right">8바이트</td>
<td>매우 큰 정수 범위</td>
</tr>
</tbody></table>
<p>일반적인 정수는 <code>int</code>를 가장 많이 사용한다.</p>
<pre><code class="language-java">int age = 20;
int count = 100;</code></pre>
<p><code>int</code> 범위를 넘어가는 숫자는 <code>long</code>을 사용한다.</p>
<pre><code class="language-java">long population = 8_000_000_000L;</code></pre>
<p><code>long</code> 리터럴은 숫자 뒤에 <code>L</code> 또는 <code>l</code>을 작성한다.</p>
<p>소문자 <code>l</code>은 숫자 <code>1</code>과 헷갈릴 수 있으므로 대문자 <code>L</code>을 권장한다.</p>
<pre><code class="language-java">long value = 10_000_000_000L;</code></pre>
<p>숫자 사이의 언더바는 가독성을 위한 표현이며 값에는 영향을 주지 않는다.</p>
<hr>
<h2 id="정수-리터럴">정수 리터럴</h2>
<p>소스 코드에서 프로그래머가 직접 작성한 값을 리터럴이라고 한다.</p>
<p>Java에서는 정수를 여러 진법으로 표현할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int binary = 0b1010;     // 2진수
        int octal = 012;         // 8진수
        int decimal = 10;        // 10진수
        int hexadecimal = 0xA;   // 16진수

        System.out.println(binary);
        System.out.println(octal);
        System.out.println(decimal);
        System.out.println(hexadecimal);
    }
}</code></pre>
<p>모두 10이 출력된다.</p>
<hr>
<h2 id="정수-오버플로">정수 오버플로</h2>
<p>정수 타입이 표현할 수 있는 최대 범위를 넘으면 오버플로가 발생한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int max = Integer.MAX_VALUE;

        System.out.println(max);
        System.out.println(max + 1);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">2147483647
-2147483648</code></pre>
<p>최댓값에 1을 더했지만 더 큰 값이 되는 것이 아니라 최솟값으로 되돌아간다.</p>
<blockquote>
<p><code>Integer</code>는 <code>int</code> 타입과 관련된 기능을 제공하는 래퍼 클래스다. <code>Integer.MAX_VALUE</code>와 <code>Integer.MIN_VALUE</code>를 이용해 <code>int</code>의 범위를 확인할 수 있다.</p>
</blockquote>
<pre><code class="language-java">System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);
System.out.println(Long.MAX_VALUE);
System.out.println(Long.MIN_VALUE);</code></pre>
<p>금액, 사용자 수, 시간처럼 값이 커질 가능성이 있다면 저장 범위를 고려하여 <code>long</code>을 사용해야 한다.</p>
<hr>
<h1 id="7-char-타입">7. char 타입</h1>
<p><code>char</code>는 하나의 문자를 저장하는 타입이다.</p>
<p>문자 리터럴은 작은따옴표로 감싼다.</p>
<pre><code class="language-java">char grade = &#39;A&#39;;
char korean = &#39;가&#39;;</code></pre>
<p>큰따옴표를 사용하면 문자열이 되기 때문에 <code>char</code> 변수에 저장할 수 없다.</p>
<pre><code class="language-java">char letter = &#39;A&#39;;

// char invalid = &quot;A&quot;;
// 컴파일 오류: &quot;A&quot;는 String</code></pre>
<hr>
<h2 id="char는-정수-타입이다">char는 정수 타입이다</h2>
<p><code>char</code>는 내부적으로 유니코드 값을 저장하는 정수 타입이다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        char letter = &#39;A&#39;;
        int unicode = letter;

        System.out.println(letter);
        System.out.println(unicode);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">A
65</code></pre>
<p>유니코드 값을 직접 저장할 수도 있다.</p>
<pre><code class="language-java">char letter1 = 65;
char letter2 = &#39;\u0041&#39;;

System.out.println(letter1);
System.out.println(letter2);</code></pre>
<p>두 변수 모두 <code>A</code>를 나타낸다.</p>
<blockquote>
<p><code>char</code>가 문자 자체를 저장한다고 표현하기도 하지만, 정확히는 UTF-16 코드 단위 하나를 저장한다. 따라서 모든 유니코드 문자를 하나의 <code>char</code>만으로 표현할 수 있는 것은 아니다. 일부 이모지처럼 보조 문자가 필요한 문자는 두 개의 <code>char</code>를 사용한다.</p>
</blockquote>
<hr>
<h1 id="8-string-타입">8. String 타입</h1>
<p>여러 문자의 모음을 문자열이라고 한다.</p>
<p>Java에서 문자열은 큰따옴표로 감싼다.</p>
<pre><code class="language-java">String name = &quot;김자바&quot;;
String message = &quot;Hello, Java&quot;;</code></pre>
<p><code>String</code>은 기본 타입이 아니라 클래스이므로 참조 타입이다.</p>
<pre><code class="language-java">char letter = &#39;A&#39;;
String text = &quot;A&quot;;</code></pre>
<p><code>&#39;A&#39;</code>와 <code>&quot;A&quot;</code>는 서로 다른 타입이다.</p>
<ul>
<li><code>&#39;A&#39;</code>: <code>char</code></li>
<li><code>&quot;A&quot;</code>: <code>String</code></li>
</ul>
<hr>
<h2 id="이스케이프-문자">이스케이프 문자</h2>
<p>문자열 내부에서 역슬래시 <code>\</code>는 특별한 문자를 표현하는 이스케이프 문자로 사용된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        System.out.println(&quot;나는 \&quot;Java\&quot;를 좋아합니다.&quot;);
        System.out.println(&quot;첫 번째 줄\n두 번째 줄&quot;);
        System.out.println(&quot;이름\t나이&quot;);
        System.out.println(&quot;C:\\java\\workspace&quot;);
    }
}</code></pre>
<p>대표적인 이스케이프 문자는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>표현</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>\n</code></td>
<td>줄바꿈</td>
</tr>
<tr>
<td><code>\t</code></td>
<td>탭</td>
</tr>
<tr>
<td><code>\&quot;</code></td>
<td>큰따옴표</td>
</tr>
<tr>
<td><code>\&#39;</code></td>
<td>작은따옴표</td>
</tr>
<tr>
<td><code>\\</code></td>
<td>역슬래시</td>
</tr>
</tbody></table>
<hr>
<h2 id="텍스트-블록">텍스트 블록</h2>
<p>Java 15부터 텍스트 블록을 정식으로 사용할 수 있다.</p>
<p>텍스트 블록은 여러 줄의 문자열을 가독성 좋게 작성할 때 사용한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String message = &quot;&quot;&quot;
                내 이름은 %s,
                나이는 %d살이고,
                사는 곳은 %s입니다.
                &quot;&quot;&quot;.formatted(&quot;홍길동&quot;, 20, &quot;율도국&quot;);

        System.out.println(message);
    }
}</code></pre>
<p>텍스트 블록은 큰따옴표 두 개가 아니라 <strong>큰따옴표 세 개</strong>를 사용한다.</p>
<pre><code class="language-java">&quot;&quot;&quot;
여러 줄 문자열
&quot;&quot;&quot;</code></pre>
<hr>
<h1 id="9-실수-타입">9. 실수 타입</h1>
<p>Java의 실수 타입은 <code>float</code>와 <code>double</code>이 있다.</p>
<table>
<thead>
<tr>
<th>타입</th>
<th align="right">크기</th>
<th align="right">대략적인 정밀도</th>
</tr>
</thead>
<tbody><tr>
<td><code>float</code></td>
<td align="right">4바이트</td>
<td align="right">약 7자리</td>
</tr>
<tr>
<td><code>double</code></td>
<td align="right">8바이트</td>
<td align="right">약 15자리</td>
</tr>
</tbody></table>
<p>Java는 실수 리터럴을 기본적으로 <code>double</code>로 해석한다.</p>
<pre><code class="language-java">double pi = 3.14;</code></pre>
<p><code>float</code> 변수에 실수 리터럴을 저장하려면 뒤에 <code>F</code> 또는 <code>f</code>를 붙여야 한다.</p>
<pre><code class="language-java">float pi = 3.14F;</code></pre>
<p>다음 코드는 컴파일 오류가 발생한다.</p>
<pre><code class="language-java">// float pi = 3.14;</code></pre>
<p><code>3.14</code>가 <code>double</code>로 해석되는데, 더 큰 범위를 가진 <code>double</code> 값을 <code>float</code>에 자동으로 저장할 수 없기 때문이다.</p>
<hr>
<h2 id="실수-연산은-정확하지-않을-수-있다">실수 연산은 정확하지 않을 수 있다</h2>
<p>컴퓨터는 실수를 2진수 기반으로 저장한다.</p>
<p>일부 10진수 실수는 2진수로 정확하게 표현할 수 없으므로 근삿값이 저장된다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        double first = 2.0;
        double second = 1.1;

        System.out.println(first - second);
    }
}</code></pre>
<p>실행 결과는 다음과 같이 나올 수 있다.</p>
<pre><code class="language-text">0.8999999999999999</code></pre>
<p>이는 Java의 오류가 아니라 IEEE 754 부동소수점 표현 방식에서 발생하는 일반적인 현상이다.</p>
<hr>
<h2 id="정확한-소수-계산과-bigdecimal">정확한 소수 계산과 BigDecimal</h2>
<p>금액처럼 정확한 소수 계산이 필요한 경우 <code>double</code> 대신 <code>BigDecimal</code>을 고려해야 한다.</p>
<pre><code class="language-java">import java.math.BigDecimal;

public class Main {

    public static void main(String[] args) {
        BigDecimal first = new BigDecimal(&quot;2.0&quot;);
        BigDecimal second = new BigDecimal(&quot;1.1&quot;);

        BigDecimal result = first.subtract(second);

        System.out.println(result);
    }
}</code></pre>
<p>실행 결과는 정확히 다음과 같다.</p>
<pre><code class="language-text">0.9</code></pre>
<p><code>BigDecimal</code>은 실수형이 아니라 객체이므로 산술 연산자를 직접 사용할 수 없다.</p>
<pre><code class="language-java">BigDecimal addResult = first.add(second);
BigDecimal subtractResult = first.subtract(second);
BigDecimal multiplyResult = first.multiply(second);</code></pre>
<blockquote>
<p><code>new BigDecimal(0.1)</code>보다 <code>new BigDecimal(&quot;0.1&quot;)</code> 또는 <code>BigDecimal.valueOf(0.1)</code>을 사용하는 것이 안전하다. <code>double</code>의 근삿값이 그대로 전달될 수 있기 때문이다.</p>
</blockquote>
<hr>
<h1 id="10-boolean-타입">10. boolean 타입</h1>
<p><code>boolean</code>은 참과 거짓을 저장한다.</p>
<pre><code class="language-java">boolean isLoggedIn = true;
boolean isDeleted = false;</code></pre>
<p><code>boolean</code>에는 <code>true</code>와 <code>false</code>만 저장할 수 있다.</p>
<pre><code class="language-java">int age = 20;
boolean isAdult = age &gt;= 19;

System.out.println(isAdult);</code></pre>
<p>C 언어처럼 숫자 <code>0</code>이나 <code>1</code>을 논리값으로 사용할 수 없다.</p>
<pre><code class="language-java">// boolean result = 1;
// 컴파일 오류</code></pre>
<p><code>boolean</code>은 다른 기본 타입과 타입 변환되지 않는다.</p>
<hr>
<h1 id="11-타입-변환이란">11. 타입 변환이란?</h1>
<p>타입 변환은 하나의 데이터 타입을 다른 데이터 타입으로 바꾸는 것을 의미한다.</p>
<pre><code class="language-java">int intValue = 10;
double doubleValue = intValue;</code></pre>
<p>변수 값을 다른 타입의 변수에 저장하거나 서로 다른 타입끼리 연산할 때 타입 변환이 발생한다.</p>
<p>타입 변환은 크게 두 가지로 나뉜다.</p>
<ul>
<li>자동 타입 변환</li>
<li>강제 타입 변환</li>
</ul>
<hr>
<h1 id="12-자동-타입-변환">12. 자동 타입 변환</h1>
<p>자동 타입 변환은 상대적으로 표현 범위가 작은 타입의 값을 더 큰 타입에 저장할 때 발생한다.</p>
<pre><code class="language-text">byte → short → int → long → float → double</code></pre>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        byte byteValue = 10;
        int intValue = byteValue;
        long longValue = intValue;
        double doubleValue = longValue;

        System.out.println(doubleValue);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">10.0</code></pre>
<p>값이 더 넓은 표현 범위를 가진 타입으로 이동하므로 일반적으로 데이터 손실 가능성이 낮다.</p>
<hr>
<h2 id="타입의-크기보다-표현-범위가-중요하다">타입의 크기보다 표현 범위가 중요하다</h2>
<p>자동 타입 변환은 단순히 메모리 크기만 비교하는 것이 아니다.</p>
<p>예를 들어 <code>long</code>은 8바이트이고 <code>float</code>은 4바이트지만 다음 변환이 허용된다.</p>
<pre><code class="language-java">long longValue = 100L;
float floatValue = longValue;</code></pre>
<p>그 이유는 <code>float</code>이 <code>long</code>보다 더 넓은 크기의 값을 표현할 수 있기 때문이다.</p>
<p>다만 <code>float</code>은 정밀도가 제한되어 있으므로 큰 정수의 일부 자릿수가 손실될 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        long longValue = 123456789123456789L;
        float floatValue = longValue;

        System.out.println(longValue);
        System.out.println(floatValue);
    }
}</code></pre>
<p>따라서 “자동 변환이 가능하다”와 “값이 완벽하게 보존된다”는 항상 같은 의미가 아니다.</p>
<hr>
<h2 id="char에서-int로-변환">char에서 int로 변환</h2>
<p><code>char</code>는 정수 타입이므로 <code>int</code>로 자동 변환할 수 있다.</p>
<pre><code class="language-java">char charValue = &#39;A&#39;;
int intValue = charValue;

System.out.println(intValue);</code></pre>
<p>출력 결과는 <code>65</code>다.</p>
<p>반대로 <code>byte</code>에서 <code>char</code>로는 자동 변환할 수 없다.</p>
<pre><code class="language-java">byte byteValue = 65;

// char charValue = byteValue;
// 컴파일 오류</code></pre>
<p><code>byte</code>는 음수를 저장할 수 있지만 <code>char</code>는 <code>0</code> 이상의 값만 저장하므로 자동 변환이 허용되지 않는다.</p>
<hr>
<h1 id="13-강제-타입-변환">13. 강제 타입 변환</h1>
<p>표현 범위가 큰 타입의 값을 작은 타입에 저장하려면 강제 타입 변환이 필요하다.</p>
<p>강제 타입 변환은 캐스팅 연산자 <code>()</code>를 사용한다.</p>
<pre><code class="language-java">double doubleValue = 3.14;
int intValue = (int) doubleValue;

System.out.println(intValue);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">3</code></pre>
<p>소수점 이하 값은 버려진다.</p>
<p>원본 변수의 값은 변경되지 않는다.</p>
<pre><code class="language-java">double doubleValue = 3.14;
int intValue = (int) doubleValue;

System.out.println(doubleValue); // 3.14
System.out.println(intValue);    // 3</code></pre>
<hr>
<h2 id="정수-범위를-벗어나는-강제-변환">정수 범위를 벗어나는 강제 변환</h2>
<p>강제 변환은 데이터 손실이 발생할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int intValue = 300;
        byte byteValue = (byte) intValue;

        System.out.println(byteValue);
    }
}</code></pre>
<p><code>byte</code>는 300을 저장할 수 없으므로 기대한 값과 다른 결과가 나온다.</p>
<blockquote>
<p>강제 타입 변환은 컴파일 오류를 제거하는 수단이 아니라, 데이터 손실 가능성을 개발자가 책임지고 변환하겠다는 의미다.</p>
</blockquote>
<hr>
<h2 id="int를-char로-변환">int를 char로 변환</h2>
<pre><code class="language-java">int intValue = 65;
char charValue = (char) intValue;

System.out.println(charValue);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">A</code></pre>
<hr>
<h1 id="14-정수-연산에서의-자동-타입-변환">14. 정수 연산에서의 자동 타입 변환</h1>
<p><code>byte</code>, <code>short</code>, <code>char</code> 타입이 산술 연산에 사용되면 연산 전에 <code>int</code>로 자동 변환된다.</p>
<pre><code class="language-java">byte left = 10;
byte right = 20;

// byte result = left + right;
// 컴파일 오류

int result = left + right;</code></pre>
<p>두 피연산자가 모두 <code>byte</code>여도 결과는 <code>int</code>가 된다.</p>
<p>따라서 특별한 이유가 없다면 정수 연산에는 <code>int</code>를 사용하는 것이 편리하다.</p>
<hr>
<h2 id="상수식의-예외">상수식의 예외</h2>
<p>컴파일 시점에 결과를 확정할 수 있고 값이 타입 범위 안에 있다면 일부 경우에는 대입이 허용된다.</p>
<pre><code class="language-java">byte result = 10 + 20;</code></pre>
<p><code>10 + 20</code>은 컴파일 시점에 <code>30</code>이라는 값으로 계산할 수 있고 <code>byte</code> 범위 안에 있으므로 저장할 수 있다.</p>
<p>반면 변수가 포함되면 실행 시점에 계산되므로 <code>int</code> 결과를 그대로 <code>byte</code>에 넣을 수 없다.</p>
<pre><code class="language-java">byte x = 10;
byte y = 20;

// byte result = x + y;</code></pre>
<hr>
<h2 id="long이-포함된-정수-연산">long이 포함된 정수 연산</h2>
<p>피연산자 중 하나가 <code>long</code>이면 다른 정수 피연산자도 <code>long</code>으로 변환된다.</p>
<pre><code class="language-java">int intValue = 10;
long longValue = 20L;

long result = intValue + longValue;</code></pre>
<hr>
<h1 id="15-실수-연산에서의-자동-타입-변환">15. 실수 연산에서의 자동 타입 변환</h1>
<p>피연산자 중 하나가 <code>double</code>이면 다른 피연산자도 <code>double</code>로 변환된다.</p>
<pre><code class="language-java">int intValue = 10;
double doubleValue = 5.5;

double result = intValue + doubleValue;

System.out.println(result);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">15.5</code></pre>
<p>정수 결과가 필요하다면 명시적으로 강제 변환해야 한다.</p>
<pre><code class="language-java">int result = intValue + (int) doubleValue;</code></pre>
<p>이 경우 <code>5.5</code>가 <code>5</code>로 변환된 후 연산되므로 결과는 <code>15</code>다.</p>
<hr>
<h2 id="float-리터럴-연산">float 리터럴 연산</h2>
<p>실수 리터럴은 기본적으로 <code>double</code>이다.</p>
<pre><code class="language-java">double result = 1.5 + 2.3;</code></pre>
<p><code>float</code> 결과를 얻으려면 각 리터럴에 <code>F</code>를 붙여야 한다.</p>
<pre><code class="language-java">float result = 1.5F + 2.3F;</code></pre>
<hr>
<h1 id="16-정수-나눗셈의-주의점">16. 정수 나눗셈의 주의점</h1>
<p>정수끼리 나누면 결과도 정수다.</p>
<pre><code class="language-java">int result = 1 / 2;

System.out.println(result);</code></pre>
<p>결과는 <code>0</code>이다.</p>
<p>정수 연산에서는 소수점 이하 값이 버려지기 때문이다.</p>
<p>실수 결과를 얻으려면 연산 전에 피연산자 중 하나를 실수로 변환해야 한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        int x = 1;
        int y = 2;

        double result1 = (double) x / y;
        double result2 = x / (double) y;
        double result3 = (double) x / (double) y;

        System.out.println(result1);
        System.out.println(result2);
        System.out.println(result3);
    }
}</code></pre>
<p>세 결과 모두 <code>0.5</code>다.</p>
<p>다음 코드는 의도한 결과를 만들지 못한다.</p>
<pre><code class="language-java">double result = (double) (x / y);</code></pre>
<p><code>x / y</code>가 먼저 정수 연산되어 <code>0</code>이 된 후 <code>double</code>로 변환되므로 결과는 <code>0.0</code>이다.</p>
<blockquote>
<p>타입 변환은 연산이 수행되기 전에 적용해야 한다.</p>
</blockquote>
<hr>
<h1 id="17-문자열-결합-연산">17. 문자열 결합 연산</h1>
<p><code>+</code> 연산자는 숫자끼리 사용하면 덧셈을 수행한다.</p>
<pre><code class="language-java">int result = 3 + 7;</code></pre>
<p>피연산자 중 하나가 문자열이면 문자열 결합 연산이 수행된다.</p>
<pre><code class="language-java">String result = 3 + &quot;7&quot;;

System.out.println(result);</code></pre>
<p>출력 결과는 <code>&quot;37&quot;</code>이다.</p>
<hr>
<h2 id="연산은-왼쪽부터-수행된다">연산은 왼쪽부터 수행된다</h2>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        System.out.println(1 + 2 + 3);
        System.out.println(1 + 2 + &quot;3&quot;);
        System.out.println(1 + &quot;2&quot; + 3);
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">6
33
123</code></pre>
<p>첫 번째 식은 모두 숫자이므로 덧셈이 수행된다.</p>
<pre><code class="language-text">1 + 2 + 3
→ 3 + 3
→ 6</code></pre>
<p>두 번째 식은 <code>1 + 2</code>가 먼저 계산된 후 문자열과 결합된다.</p>
<pre><code class="language-text">1 + 2 + &quot;3&quot;
→ 3 + &quot;3&quot;
→ &quot;33&quot;</code></pre>
<p>세 번째 식은 처음부터 문자열 결합이 시작된다.</p>
<pre><code class="language-text">1 + &quot;2&quot; + 3
→ &quot;12&quot; + 3
→ &quot;123&quot;</code></pre>
<p>원하는 계산 순서를 명확히 하려면 괄호를 사용한다.</p>
<pre><code class="language-java">String result = &quot;결과: &quot; + (1 + 2 + 3);</code></pre>
<hr>
<h1 id="18-문자열을-기본-타입으로-변환하기">18. 문자열을 기본 타입으로 변환하기</h1>
<p>사용자가 입력한 값이나 외부에서 받은 데이터는 문자열인 경우가 많다.</p>
<p>문자열을 숫자나 논리값으로 바꾸려면 각 래퍼 클래스의 <code>parse</code> 메서드를 사용한다.</p>
<pre><code class="language-java">byte byteValue = Byte.parseByte(&quot;10&quot;);
short shortValue = Short.parseShort(&quot;100&quot;);
int intValue = Integer.parseInt(&quot;1000&quot;);
long longValue = Long.parseLong(&quot;10000&quot;);
float floatValue = Float.parseFloat(&quot;3.14&quot;);
double doubleValue = Double.parseDouble(&quot;3.141592&quot;);
boolean booleanValue = Boolean.parseBoolean(&quot;true&quot;);</code></pre>
<p><code>Integer</code>의 철자를 주의해야 한다.</p>
<pre><code class="language-text">Integer</code></pre>
<p><code>Interger</code>가 아니다.</p>
<hr>
<h2 id="numberformatexception">NumberFormatException</h2>
<p>숫자로 변환할 수 없는 문자열을 변환하려 하면 <code>NumberFormatException</code>이 발생한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String value = &quot;100원&quot;;

        int number = Integer.parseInt(value);

        System.out.println(number);
    }
}</code></pre>
<p><code>&quot;100원&quot;</code>에는 숫자가 아닌 문자가 포함되어 있으므로 예외가 발생한다.</p>
<p>안전하게 처리하려면 예외 처리를 사용할 수 있다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String value = &quot;100원&quot;;

        try {
            int number = Integer.parseInt(value);
            System.out.println(number);
        } catch (NumberFormatException exception) {
            System.out.println(&quot;숫자로 변환할 수 없는 값입니다.&quot;);
        }
    }
}</code></pre>
<hr>
<h1 id="19-기본-타입을-문자열로-변환하기">19. 기본 타입을 문자열로 변환하기</h1>
<p>기본 타입 값을 문자열로 바꾸는 대표적인 방법은 <code>String.valueOf()</code>다.</p>
<pre><code class="language-java">int number = 3;
String text = String.valueOf(number);

System.out.println(text);</code></pre>
<p>빈 문자열과 결합하는 방법도 있다.</p>
<pre><code class="language-java">String text = &quot;&quot; + 3;</code></pre>
<p>간단한 출력에는 사용할 수 있지만, 변환 의도를 명확하게 나타내기 위해서는 <code>String.valueOf()</code>가 더 적절하다.</p>
<pre><code class="language-java">String text = String.valueOf(3);</code></pre>
<hr>
<h1 id="20-기본-타입과-래퍼-클래스">20. 기본 타입과 래퍼 클래스</h1>
<p>각 기본 타입에는 대응되는 래퍼 클래스가 있다.</p>
<table>
<thead>
<tr>
<th>기본 타입</th>
<th>래퍼 클래스</th>
</tr>
</thead>
<tbody><tr>
<td><code>byte</code></td>
<td><code>Byte</code></td>
</tr>
<tr>
<td><code>short</code></td>
<td><code>Short</code></td>
</tr>
<tr>
<td><code>int</code></td>
<td><code>Integer</code></td>
</tr>
<tr>
<td><code>long</code></td>
<td><code>Long</code></td>
</tr>
<tr>
<td><code>float</code></td>
<td><code>Float</code></td>
</tr>
<tr>
<td><code>double</code></td>
<td><code>Double</code></td>
</tr>
<tr>
<td><code>char</code></td>
<td><code>Character</code></td>
</tr>
<tr>
<td><code>boolean</code></td>
<td><code>Boolean</code></td>
</tr>
</tbody></table>
<p>래퍼 클래스는 기본 타입 값을 객체 형태로 다룰 수 있게 한다.</p>
<pre><code class="language-java">int primitive = 10;
Integer wrapper = primitive;</code></pre>
<p>기본 타입이 래퍼 객체로 변환되는 것을 오토박싱이라고 한다.</p>
<pre><code class="language-java">Integer wrapper = 10;</code></pre>
<p>반대로 래퍼 객체가 기본 타입으로 변환되는 것을 언박싱이라고 한다.</p>
<pre><code class="language-java">Integer wrapper = 10;
int primitive = wrapper;</code></pre>
<blockquote>
<p>기본 타입과 참조 타입 사이의 모든 변환이 래퍼 클래스로 해결되는 것은 아니다. 기본 타입을 객체처럼 다루거나 문자열과 기본 타입 사이를 변환할 때 래퍼 클래스가 주로 사용된다.</p>
</blockquote>
<hr>
<h1 id="21-콘솔-출력">21. 콘솔 출력</h1>
<p>Java에서 표준 출력 장치는 일반적으로 모니터 또는 콘솔이다.</p>
<pre><code class="language-java">System.out</code></pre>
<p><code>System.out</code>을 통해 값을 출력할 수 있다.</p>
<hr>
<h2 id="println">println()</h2>
<p>값을 출력한 후 줄을 바꾼다.</p>
<pre><code class="language-java">System.out.println(&quot;Java&quot;);
System.out.println(&quot;Spring&quot;);</code></pre>
<p>출력 결과는 다음과 같다.</p>
<pre><code class="language-text">Java
Spring</code></pre>
<hr>
<h2 id="print">print()</h2>
<p>값을 출력한 후 줄을 바꾸지 않는다.</p>
<pre><code class="language-java">System.out.print(&quot;Java &quot;);
System.out.print(&quot;Spring&quot;);</code></pre>
<p>출력 결과는 다음과 같다.</p>
<pre><code class="language-text">Java Spring</code></pre>
<hr>
<h2 id="printf">printf()</h2>
<p>형식 문자열에 맞춰 값을 출력한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String name = &quot;김자바&quot;;
        int age = 20;
        double height = 175.456;

        System.out.printf(&quot;이름: %s%n&quot;, name);
        System.out.printf(&quot;나이: %d세%n&quot;, age);
        System.out.printf(&quot;키: %.1fcm%n&quot;, height);
    }
}</code></pre>
<p>대표적인 변환 문자는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>문자</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>%d</code></td>
<td>정수</td>
</tr>
<tr>
<td><code>%f</code></td>
<td>실수</td>
</tr>
<tr>
<td><code>%s</code></td>
<td>문자열</td>
</tr>
<tr>
<td><code>%c</code></td>
<td>문자</td>
</tr>
<tr>
<td><code>%b</code></td>
<td>논리값</td>
</tr>
<tr>
<td><code>%n</code></td>
<td>운영체제에 맞는 줄바꿈</td>
</tr>
</tbody></table>
<hr>
<h2 id="전체-자릿수와-소수-자릿수">전체 자릿수와 소수 자릿수</h2>
<pre><code class="language-java">double value = 123.456789;

System.out.printf(&quot;%10.2f%n&quot;, value);</code></pre>
<p><code>%10.2f</code>는 다음을 의미한다.</p>
<ul>
<li><code>10</code>: 전체 출력 폭</li>
<li><code>.2</code>: 소수점 이하 두 자리</li>
<li><code>f</code>: 실수 출력</li>
</ul>
<hr>
<h2 id="argument-index">argument index</h2>
<p>출력할 값의 순서를 지정할 수도 있다.</p>
<pre><code class="language-java">String name = &quot;김자바&quot;;
int age = 20;

System.out.printf(&quot;이름은 %1$s이고, 나이는 %2$d세입니다.%n&quot;, name, age);</code></pre>
<p>일반적으로 값을 작성한 순서대로 사용한다면 인덱스는 생략해도 된다.</p>
<pre><code class="language-java">System.out.printf(&quot;이름은 %s이고, 나이는 %d세입니다.%n&quot;, name, age);</code></pre>
<hr>
<h1 id="22-키보드-입력">22. 키보드 입력</h1>
<p>Java의 표준 입력 장치는 일반적으로 키보드다.</p>
<pre><code class="language-java">System.in</code></pre>
<hr>
<h2 id="systeminread">System.in.read()</h2>
<p><code>System.in.read()</code>는 입력된 데이터를 바이트 단위로 읽어 정수 형태의 값을 반환한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) throws Exception {
        int keyCode = System.in.read();

        System.out.println(keyCode);
    }
}</code></pre>
<p>사용자가 <code>A</code>를 입력하면 <code>65</code>가 출력된다.</p>
<p>다만 <code>System.in.read()</code>는 다음과 같은 이유로 일반적인 콘솔 입력에 불편하다.</p>
<ul>
<li>한 바이트 또는 바이트 단위로 읽는다.</li>
<li>문자열 전체를 편리하게 처리하기 어렵다.</li>
<li>입력 예외 처리가 필요하다.</li>
<li>한글처럼 여러 바이트로 표현되는 문자를 직접 처리하기 복잡하다.</li>
</ul>
<p>따라서 입문 단계에서는 주로 <code>Scanner</code>를 사용한다.</p>
<hr>
<h1 id="23-scanner를-이용한-입력">23. Scanner를 이용한 입력</h1>
<p><code>Scanner</code>를 사용하려면 먼저 클래스를 import해야 한다.</p>
<pre><code class="language-java">import java.util.Scanner;</code></pre>
<p>전체 예제는 다음과 같다.</p>
<pre><code class="language-java">import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print(&quot;이름을 입력하세요: &quot;);
        String name = scanner.nextLine();

        System.out.print(&quot;나이를 입력하세요: &quot;);
        int age = Integer.parseInt(scanner.nextLine());

        System.out.printf(&quot;%s님의 나이는 %d세입니다.%n&quot;, name, age);

        scanner.close();
    }
}</code></pre>
<p><code>nextLine()</code>은 사용자가 입력한 한 줄을 문자열로 반환한다.</p>
<p>숫자가 필요하다면 문자열을 기본 타입으로 변환할 수 있다.</p>
<pre><code class="language-java">int age = Integer.parseInt(scanner.nextLine());</code></pre>
<hr>
<h2 id="nextint와-nextline의-주의점">nextInt()와 nextLine()의 주의점</h2>
<p>다음과 같이 <code>nextInt()</code> 뒤에 <code>nextLine()</code>을 사용하면 예상과 다른 결과가 나올 수 있다.</p>
<pre><code class="language-java">int age = scanner.nextInt();
String name = scanner.nextLine();</code></pre>
<p><code>nextInt()</code>는 숫자만 읽고 사용자가 입력한 줄바꿈 문자는 남겨둔다. 이후 <code>nextLine()</code>이 남아 있는 줄바꿈을 읽어 빈 문자열을 반환할 수 있다.</p>
<p>이를 해결하는 방법은 두 가지다.</p>
<h3 id="남은-줄바꿈을-한-번-읽기">남은 줄바꿈을 한 번 읽기</h3>
<pre><code class="language-java">int age = scanner.nextInt();
scanner.nextLine();

String name = scanner.nextLine();</code></pre>
<h3 id="모든-입력을-nextline으로-받고-변환하기">모든 입력을 nextLine()으로 받고 변환하기</h3>
<pre><code class="language-java">int age = Integer.parseInt(scanner.nextLine());
String name = scanner.nextLine();</code></pre>
<p>입력 방식을 통일할 수 있기 때문에 두 번째 방법이 실수하기 적다.</p>
<hr>
<h1 id="24-기본-타입-비교와-문자열-비교">24. 기본 타입 비교와 문자열 비교</h1>
<p>기본 타입의 값은 <code>==</code> 연산자로 비교한다.</p>
<pre><code class="language-java">int first = 10;
int second = 10;

boolean result = first == second;

System.out.println(result);</code></pre>
<p>문자열의 내용은 <code>equals()</code> 메서드로 비교해야 한다.</p>
<pre><code class="language-java">String input = &quot;Java&quot;;

boolean result = input.equals(&quot;Java&quot;);

System.out.println(result);</code></pre>
<hr>
<h2 id="문자열에-를-사용하면-안-되는-이유">문자열에 ==를 사용하면 안 되는 이유</h2>
<p><code>String</code>은 참조 타입이다.</p>
<p>참조 타입에 <code>==</code>를 사용하면 문자열 내용이 아니라 동일한 객체를 참조하는지 비교한다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        String first = new String(&quot;Java&quot;);
        String second = new String(&quot;Java&quot;);

        System.out.println(first == second);
        System.out.println(first.equals(second));
    }
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-text">false
true</code></pre>
<p>두 객체의 내용은 같지만 서로 다른 객체이기 때문에 <code>==</code> 결과는 <code>false</code>다.</p>
<p>문자열의 내용을 비교하려면 <code>equals()</code>를 사용해야 한다.</p>
<hr>
<h2 id="nullpointerexception을-피하는-문자열-비교">NullPointerException을 피하는 문자열 비교</h2>
<p>다음 코드에서 <code>input</code>이 <code>null</code>이면 예외가 발생한다.</p>
<pre><code class="language-java">input.equals(&quot;Java&quot;);</code></pre>
<p>비교 대상이 고정 문자열이라면 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-java">&quot;Java&quot;.equals(input);</code></pre>
<p>고정 문자열 <code>&quot;Java&quot;</code>는 <code>null</code>이 아니므로 <code>input</code>이 <code>null</code>이어도 <code>false</code>를 반환한다.</p>
<p>Java 7 이상에서는 <code>Objects.equals()</code>도 사용할 수 있다.</p>
<pre><code class="language-java">import java.util.Objects;

boolean result = Objects.equals(input, &quot;Java&quot;);</code></pre>
<p>두 값이 모두 <code>null</code>인 경우에도 안전하게 비교할 수 있다.</p>
<hr>
<h1 id="25-종합-예제">25. 종합 예제</h1>
<p>사용자에게 상품명, 가격, 수량을 입력받아 총금액을 계산하는 프로그램을 작성해보자.</p>
<pre><code class="language-java">import java.util.Scanner;

public class OrderCalculator {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print(&quot;상품명을 입력하세요: &quot;);
        String productName = scanner.nextLine();

        System.out.print(&quot;상품 가격을 입력하세요: &quot;);
        int price = Integer.parseInt(scanner.nextLine());

        System.out.print(&quot;구매 수량을 입력하세요: &quot;);
        int quantity = Integer.parseInt(scanner.nextLine());

        int totalPrice = price * quantity;

        System.out.println();
        System.out.println(&quot;===== 주문 정보 =====&quot;);
        System.out.printf(&quot;상품명: %s%n&quot;, productName);
        System.out.printf(&quot;가격: %,d원%n&quot;, price);
        System.out.printf(&quot;수량: %d개%n&quot;, quantity);
        System.out.printf(&quot;총금액: %,d원%n&quot;, totalPrice);

        scanner.close();
    }
}</code></pre>
<p>입력 예시는 다음과 같다.</p>
<pre><code class="language-text">상품명을 입력하세요: 키보드
상품 가격을 입력하세요: 50000
구매 수량을 입력하세요: 2</code></pre>
<p>출력 결과는 다음과 같다.</p>
<pre><code class="language-text">===== 주문 정보 =====
상품명: 키보드
가격: 50,000원
수량: 2개
총금액: 100,000원</code></pre>
<p>이 예제에는 다음 개념이 사용되었다.</p>
<ul>
<li>변수 선언과 초기화</li>
<li><code>String</code>과 <code>int</code></li>
<li><code>Scanner</code> 입력</li>
<li><code>Integer.parseInt()</code></li>
<li>산술 연산</li>
<li><code>printf()</code> 형식 출력</li>
</ul>
<hr>
<h1 id="헷갈리기-쉬운-개념-정리">헷갈리기 쉬운 개념 정리</h1>
<h2 id="선언-대입-초기화">선언, 대입, 초기화</h2>
<pre><code class="language-java">int number;       // 선언
number = 10;      // 대입
int age = 20;     // 선언과 동시에 초기화</code></pre>
<hr>
<h2 id="문자와-문자열">문자와 문자열</h2>
<pre><code class="language-java">char letter = &#39;A&#39;;
String text = &quot;A&quot;;</code></pre>
<p>작은따옴표는 문자, 큰따옴표는 문자열이다.</p>
<hr>
<h2 id="자동-타입-변환과-강제-타입-변환">자동 타입 변환과 강제 타입 변환</h2>
<pre><code class="language-java">int intValue = 10;
double doubleValue = intValue;       // 자동 타입 변환

double pi = 3.14;
int number = (int) pi;               // 강제 타입 변환</code></pre>
<p>자동 타입 변환은 일반적으로 표현 범위가 넓어지는 방향이고, 강제 타입 변환은 데이터 손실 가능성이 있다.</p>
<hr>
<h2 id="parseint와-valueof">parseInt()와 valueOf()</h2>
<pre><code class="language-java">int number = Integer.parseInt(&quot;100&quot;);
Integer wrapper = Integer.valueOf(&quot;100&quot;);</code></pre>
<ul>
<li><code>Integer.parseInt()</code>는 기본 타입 <code>int</code>를 반환한다.</li>
<li><code>Integer.valueOf()</code>는 래퍼 객체 <code>Integer</code>를 반환한다.</li>
</ul>
<p>오토 언박싱 때문에 비슷하게 사용할 수 있지만 반환 타입은 다르다.</p>
<hr>
<h2 id="print-println-printf">print(), println(), printf()</h2>
<pre><code class="language-java">System.out.print(&quot;A&quot;);
System.out.println(&quot;B&quot;);
System.out.printf(&quot;%d%n&quot;, 100);</code></pre>
<ul>
<li><code>print()</code>: 출력 후 줄바꿈하지 않는다.</li>
<li><code>println()</code>: 출력 후 줄바꿈한다.</li>
<li><code>printf()</code>: 형식을 지정해 출력한다.</li>
</ul>
<hr>
<h1 id="실무에서-알아두면-좋은-내용">실무에서 알아두면 좋은 내용</h1>
<h2 id="금액은-double로-계산하지-않는다">금액은 double로 계산하지 않는다</h2>
<p>실수 계산에는 오차가 발생할 수 있으므로 정확한 금액 계산에 <code>double</code>을 사용하는 것은 위험하다.</p>
<p>소수점이 필요 없다면 최소 단위의 정수로 관리할 수 있다.</p>
<pre><code class="language-java">long priceInWon = 10_000L;</code></pre>
<p>소수점 계산이 필요하면 <code>BigDecimal</code>을 고려한다.</p>
<hr>
<h2 id="입력값은-항상-검증해야-한다">입력값은 항상 검증해야 한다</h2>
<p>사용자가 반드시 숫자를 입력한다고 보장할 수 없다.</p>
<pre><code class="language-java">try {
    int age = Integer.parseInt(input);
} catch (NumberFormatException exception) {
    System.out.println(&quot;숫자만 입력해야 합니다.&quot;);
}</code></pre>
<hr>
<h2 id="문자열-비교는-equals">문자열 비교는 equals()</h2>
<p>문자열 내용을 비교할 때는 <code>==</code>가 아니라 <code>equals()</code>를 사용한다.</p>
<pre><code class="language-java">&quot;admin&quot;.equals(role);</code></pre>
<hr>
<h2 id="변수-이름에는-의미를-담는다">변수 이름에는 의미를 담는다</h2>
<p>다음과 같이 의미를 알 수 없는 이름은 피하는 것이 좋다.</p>
<pre><code class="language-java">int a;
int b;
int c;</code></pre>
<p>역할을 알 수 있도록 작성한다.</p>
<pre><code class="language-java">int productPrice;
int quantity;
int totalPrice;</code></pre>
<hr>
<h1 id="오늘-배운-내용-정리">오늘 배운 내용 정리</h1>
<p>오늘은 Java 프로그램의 기본 구조부터 변수, 데이터 타입, 타입 변환, 콘솔 입출력까지 정리했다.</p>
<ul>
<li>Java 애플리케이션은 일반적으로 <code>main()</code> 메서드에서 실행을 시작한다.</li>
<li>변수는 값을 저장할 메모리 공간에 붙인 이름이다.</li>
<li>로컬 변수는 사용하기 전에 반드시 초기화해야 한다.</li>
<li>Java의 기본 타입은 정수, 문자, 실수, 논리 타입으로 구분된다.</li>
<li><code>String</code>은 기본 타입이 아니라 문자열 객체를 다루는 참조 타입이다.</li>
<li><code>char</code>는 내부적으로 UTF-16 코드 단위에 해당하는 정숫값을 저장한다.</li>
<li>Java의 실수 리터럴은 기본적으로 <code>double</code>로 해석된다.</li>
<li>부동소수점 연산은 오차가 발생할 수 있으며 정확한 소수 계산에는 <code>BigDecimal</code>을 고려해야 한다.</li>
<li>작은 표현 범위에서 큰 표현 범위로 이동할 때는 자동 타입 변환이 발생한다.</li>
<li>큰 표현 범위에서 작은 표현 범위로 변환하려면 캐스팅 연산자를 사용해야 한다.</li>
<li>강제 타입 변환 과정에서는 값이나 정밀도가 손실될 수 있다.</li>
<li><code>byte</code>, <code>short</code>, <code>char</code>는 산술 연산 시 <code>int</code>로 변환된다.</li>
<li>정수끼리 나누면 결과도 정수이므로 실수 결과가 필요하면 연산 전에 타입을 변환해야 한다.</li>
<li>문자열을 기본 타입으로 변환할 때는 <code>Integer.parseInt()</code>, <code>Double.parseDouble()</code> 등의 메서드를 사용한다.</li>
<li>기본 타입을 문자열로 변환할 때는 <code>String.valueOf()</code>를 사용할 수 있다.</li>
<li><code>println()</code>은 줄바꿈을 포함하고, <code>print()</code>는 줄바꿈하지 않으며, <code>printf()</code>는 출력 형식을 지정한다.</li>
<li>콘솔 입력은 <code>Scanner</code>를 통해 편리하게 받을 수 있다.</li>
<li>기본 타입의 값은 <code>==</code>로 비교하지만 문자열의 내용은 <code>equals()</code>로 비교해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git 처음으로 공부하기]]></title>
            <link>https://velog.io/@tongmill_g/Git-%EC%B2%98%EC%9D%8C%EC%9C%BC%EB%A1%9C-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tongmill_g/Git-%EC%B2%98%EC%9D%8C%EC%9C%BC%EB%A1%9C-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 19 Jul 2026 09:00:47 GMT</pubDate>
            <description><![CDATA[<h1 id="git-기초부터-branch와-merge까지">Git 기초부터 Branch와 Merge까지</h1>
<p>Git을 처음 배우면 가장 많이 헷갈리는 것이 <strong>파일의 경로(Path)</strong> 와 <strong>Git의 동작 방식</strong>, 그리고 <strong>Branch와 Merge</strong>이다.</p>
<p>이번 글에서는 Git을 사용하기 전에 반드시 알아야 하는 경로(Path)의 개념부터 Git의 기본 명령어, Branch와 Merge의 원리까지 한 번에 정리해보려고 한다.</p>
<hr>
<h1 id="경로path란">경로(Path)란?</h1>
<p>컴퓨터는 파일을 찾기 위해 <strong>경로(Path)</strong> 를 사용한다.</p>
<p>예를 들어 아래와 같은 폴더 구조가 있다고 가정해보자.</p>
<pre><code>C:\
 ├── Users
 │     └── kim
 │          └── Desktop
 │                └── Git
 │                     └── sample.txt</code></pre><p>sample.txt를 찾으려면 컴퓨터에게 위치를 알려줘야 하는데, 이때 사용하는 것이 <strong>경로(Path)</strong> 이다.</p>
<p>경로는 크게 <strong>절대경로</strong>와 <strong>상대경로</strong>로 나뉜다.</p>
<hr>
<h1 id="절대경로absolute-path">절대경로(Absolute Path)</h1>
<p>절대경로는 <strong>최상위 디렉터리(루트)</strong> 부터 시작하는 경로이다.</p>
<p>예를 들어</p>
<pre><code>C:\Users\kim\Desktop\Git\sample.txt</code></pre><p>또는 Linux/macOS에서는</p>
<pre><code>/home/kim/Git/sample.txt</code></pre><p>처럼 시작부터 끝까지 모든 위치를 작성하는 것이 절대경로이다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>어디서 실행하든 항상 동일하다.</li>
<li>위치가 명확하다.</li>
<li>폴더 이름이 변경되면 사용할 수 없다.</li>
</ul>
<hr>
<h1 id="상대경로relative-path">상대경로(Relative Path)</h1>
<p>상대경로는 <strong>현재 내가 있는 위치를 기준</strong> 으로 파일을 찾는다.</p>
<p>예를 들어 현재 위치가</p>
<pre><code>Desktop/Git</code></pre><p>이라면</p>
<pre><code>sample.txt</code></pre><p>만 입력해도 된다.</p>
<p>상위 폴더로 이동하려면</p>
<pre><code>..</code></pre><p>를 사용한다.</p>
<p>예를 들어</p>
<pre><code>../project</code></pre><p>는</p>
<blockquote>
<p>현재 위치에서 한 단계 위로 올라간 뒤 project 폴더</p>
</blockquote>
<p>를 의미한다.</p>
<hr>
<h1 id="💡-왜-git에서는-상대경로를-사용하는가">💡 왜 Git에서는 상대경로를 사용하는가?</h1>
<p>Git 프로젝트는 여러 사람이 함께 개발한다.</p>
<p>만약 절대경로를 사용하면</p>
<pre><code>C:\Users\kim\Desktop\project</code></pre><p>처럼 사용자 이름이 포함된다.</p>
<p>하지만 팀원의 컴퓨터는</p>
<pre><code>C:\Users\park\Desktop\project</code></pre><p>일 수도 있다.</p>
<p>즉,</p>
<p><strong>절대경로는 사람마다 달라진다.</strong></p>
<p>반면 상대경로는</p>
<pre><code>src/main/java</code></pre><p>처럼 프로젝트 내부 기준으로 작성하기 때문에</p>
<p>누구의 컴퓨터에서도 동일하게 동작한다.</p>
<p>그래서 프로젝트에서는 항상 <strong>상대경로를 사용하는 것이 원칙</strong>이다.</p>
<hr>
<h1 id="루트-디렉터리root-directory">루트 디렉터리(Root Directory)</h1>
<p>루트 디렉터리는 파일 시스템의 <strong>가장 최상위 폴더</strong>이다.</p>
<p>Windows</p>
<pre><code>C:\</code></pre><p>Linux/macOS</p>
<pre><code>/</code></pre><p>모든 폴더는 루트 디렉터리 아래에 존재한다.</p>
<hr>
<h1 id="홈-디렉터리home-directory">홈 디렉터리(Home Directory)</h1>
<p>홈 디렉터리는 <strong>사용자 개인 폴더</strong>이다.</p>
<p>Windows</p>
<pre><code>C:\Users\사용자이름</code></pre><p>Linux</p>
<pre><code>/home/사용자이름</code></pre><p>macOS</p>
<pre><code>/Users/사용자이름</code></pre><p>보통 터미널을 실행하면 처음 위치가 홈 디렉터리이다.</p>
<hr>
<h1 id="git의-동작-원리">Git의 동작 원리</h1>
<p>Git은 파일을 바로 저장하지 않는다.</p>
<p>파일은 아래와 같은 과정을 거쳐 저장된다.</p>
<pre><code class="language-text">Working Directory
        │
 git add
        ▼
 Staging Area
        │
 git commit
        ▼
 Repository</code></pre>
<p>※ 강의에서 사용한 그림을 함께 보면 이해하기 쉽다.</p>
<h3 id="1-working-directory">1. Working Directory</h3>
<p>현재 작업 중인 공간이다.</p>
<p>파일을 수정하거나 새로 만들면 여기에 존재한다.</p>
<p>상태</p>
<ul>
<li>Untracked</li>
<li>Modified</li>
</ul>
<hr>
<h3 id="2-staging-area">2. Staging Area</h3>
<p>Commit하기 전에 저장할 파일을 임시로 모아두는 공간이다.</p>
<pre><code>git add</code></pre><p>명령어를 실행하면 파일이 Staging Area로 이동한다.</p>
<p>Git은 이 영역에 올라온 파일만 Commit한다.</p>
<hr>
<h3 id="3-repository">3. Repository</h3>
<p>최종적으로 Commit이 저장되는 공간이다.</p>
<p>Commit 이후에는 Git이 변경 이력을 관리한다.</p>
<hr>
<h1 id="💡-왜-staging-area가-필요한가">💡 왜 Staging Area가 필요한가?</h1>
<p>예를 들어</p>
<pre><code>A.java
B.java
README.md</code></pre><p>세 개를 수정했다고 가정하자.</p>
<p>이번 Commit에는</p>
<pre><code>A.java
README.md</code></pre><p>만 포함하고 싶다면</p>
<pre><code class="language-bash">git add A.java
git add README.md</code></pre>
<p>만 실행하면 된다.</p>
<p>Staging Area 덕분에 <strong>Commit할 파일을 선택할 수 있다.</strong></p>
<hr>
<h1 id="git-기본-명령어">Git 기본 명령어</h1>
<h2 id="git-status">git status</h2>
<p>현재 Git의 상태를 확인한다.</p>
<pre><code class="language-bash">git status</code></pre>
<p>출력 예시</p>
<pre><code class="language-text">On branch master

Changes not staged for commit:

modified: index.html</code></pre>
<p>가장 자주 사용하는 명령어이다.</p>
<hr>
<h2 id="git-log">git log</h2>
<p>Commit 기록을 확인한다.</p>
<pre><code class="language-bash">git log</code></pre>
<p>출력 예시</p>
<pre><code class="language-text">commit 52ac...

Author : kim

Date : ...</code></pre>
<hr>
<h2 id="git-log---oneline">git log --oneline</h2>
<p>Commit을 한 줄씩 간단하게 출력한다.</p>
<pre><code class="language-bash">git log --oneline</code></pre>
<p>출력 예시</p>
<pre><code class="language-text">5a1f3d1 Update README
a71ce12 Initial Commit</code></pre>
<p>실무에서도 매우 자주 사용한다.</p>
<hr>
<h2 id="git-config---global--l">git config --global -l</h2>
<p>현재 Git 설정을 확인한다.</p>
<pre><code class="language-bash">git config --global -l</code></pre>
<p>확인 가능한 정보</p>
<ul>
<li>username</li>
<li>email</li>
<li>editor</li>
<li>기본 설정</li>
</ul>
<hr>
<h1 id="linux-기본-명령어">Linux 기본 명령어</h1>
<h2 id="cat">cat</h2>
<p>파일 내용을 출력한다.</p>
<pre><code class="language-bash">cat README.md</code></pre>
<p>여러 파일도 가능하다.</p>
<pre><code class="language-bash">cat file1.txt file2.txt</code></pre>
<hr>
<h2 id="echo">echo</h2>
<p>문자를 출력한다.</p>
<pre><code class="language-bash">echo Hello</code></pre>
<p>파일 생성에도 많이 사용된다.</p>
<pre><code class="language-bash">echo Hello &gt; test.txt</code></pre>
<hr>
<h1 id="git-저장-과정">Git 저장 과정</h1>
<p>가장 많이 사용하는 Git 명령어 순서는 다음과 같다.</p>
<pre><code class="language-bash">git add .
git commit -m &quot;커밋 메시지&quot;
git push</code></pre>
<p>각 명령어의 역할은 다음과 같다.</p>
<h3 id="git-add-">git add .</h3>
<p>현재 폴더의 변경 사항을 모두 Staging Area에 올린다.</p>
<pre><code class="language-bash">git add .</code></pre>
<hr>
<h3 id="git-commit">git commit</h3>
<p>Staging Area의 내용을 하나의 버전으로 저장한다.</p>
<pre><code class="language-bash">git commit -m &quot;회원가입 기능 구현&quot;</code></pre>
<p>Commit 메시지는 변경 내용을 한눈에 알 수 있도록 작성하는 것이 좋다.</p>
<hr>
<h3 id="git-push">git push</h3>
<p>로컬 저장소의 Commit을 원격 저장소(GitHub 등)로 업로드한다.</p>
<pre><code class="language-bash">git push</code></pre>
<hr>
<h1 id="gitignore란">.gitignore란?</h1>
<p>프로젝트에는 Git으로 관리하면 안 되는 파일들이 존재한다.</p>
<p>예를 들어</p>
<ul>
<li>build 파일</li>
<li>IDE 설정</li>
<li>로그 파일</li>
<li>API Key</li>
<li>환경 변수(.env)</li>
</ul>
<p>이런 파일들은 <code>.gitignore</code>에 등록한다.</p>
<p>예시</p>
<pre><code class="language-gitignore">.env

node_modules/

.idea/

*.log

*.class</code></pre>
<p>.gitignore를 쉽게 생성해주는 사이트</p>
<blockquote>
<p><a href="https://www.toptal.com/developers/gitignore/">https://www.toptal.com/developers/gitignore/</a></p>
</blockquote>
<p>프로젝트에서 사용하는 IDE나 언어를 선택하면 자동으로 생성해준다.</p>
<hr>
<h1 id="branch란">Branch란?</h1>
<p>Branch는 <strong>독립적으로 작업할 수 있는 개발 공간</strong>이다.</p>
<p>보통 <strong>기준점에서 새로운 작업 공간을 만든다</strong>고 이해하면 쉽다.</p>
<p>예를 들어</p>
<pre><code>main

↓

회원가입 기능

↓

로그인 기능

↓

검색 기능</code></pre><p>을 동시에 개발하고 싶다면</p>
<p>각 기능별 Branch를 만든다.</p>
<p>대표 명령어는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>git branch</code></td>
<td>브랜치 목록 확인</td>
</tr>
<tr>
<td><code>git branch -r</code></td>
<td>원격 브랜치 확인</td>
</tr>
<tr>
<td><code>git branch 브랜치명</code></td>
<td>브랜치 생성</td>
</tr>
<tr>
<td><code>git branch -d 브랜치명</code></td>
<td>병합된 브랜치 삭제</td>
</tr>
<tr>
<td><code>git branch -D 브랜치명</code></td>
<td>강제 삭제</td>
</tr>
</tbody></table>
<p>(강의 슬라이드의 표를 함께 참고하면 이해하기 쉽다.)</p>
<hr>
<h1 id="fast-forward-merge">Fast-Forward Merge</h1>
<p>Fast-Forward는 가장 단순한 Merge 방식이다.</p>
<p>브랜치를 만든 뒤</p>
<p><strong>기존 브랜치에 새로운 Commit이 없다면</strong></p>
<p>Merge할 때 포인터만 앞으로 이동한다.</p>
<p>예를 들어</p>
<pre><code>main

A

↓

feature

A → B → C</code></pre><p>Merge하면</p>
<pre><code>main

A → B → C</code></pre><p>가 된다.</p>
<p>새로운 Merge Commit은 생성되지 않는다.</p>
<h3 id="특징-1">특징</h3>
<ul>
<li>Merge Commit 없음</li>
<li>Commit 기록이 깔끔함</li>
<li>가장 빠른 Merge</li>
</ul>
<hr>
<h1 id="3-way-merge">3-Way Merge</h1>
<p>Fast-Forward가 불가능하면 3-Way Merge가 발생한다.</p>
<p>예를 들어</p>
<pre><code>A

├── B(main)

└── C(feature)</code></pre><p>둘 다 Commit이 생겼기 때문에</p>
<p>Git은</p>
<pre><code>A

├── B

└── C

     ↓

Merge Commit</code></pre><p>을 새롭게 생성한다.</p>
<p>즉</p>
<p>공통 조상(Common Ancestor)을 기준으로</p>
<p>세 개의 Commit을 비교하여 Merge한다.</p>
<p>그래서 이름이</p>
<p><strong>3-Way Merge</strong>이다.</p>
<h3 id="특징-2">특징</h3>
<ul>
<li>Merge Commit 생성</li>
<li>Branch의 작업 기록 유지</li>
<li>협업에서 가장 많이 사용</li>
</ul>
<hr>
<h1 id="💡-fast-forward와-3-way-merge-차이">💡 Fast-Forward와 3-Way Merge 차이</h1>
<table>
<thead>
<tr>
<th>Fast-Forward</th>
<th>3-Way Merge</th>
</tr>
</thead>
<tbody><tr>
<td>Merge Commit 없음</td>
<td>Merge Commit 생성</td>
</tr>
<tr>
<td>브랜치 이동만 수행</td>
<td>새 Commit 생성</td>
</tr>
<tr>
<td>기록이 단순함</td>
<td>작업 이력 보존</td>
</tr>
<tr>
<td>충돌 가능성 낮음</td>
<td>충돌 발생 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="⚠️-헷갈리기-쉬운-개념">⚠️ 헷갈리기 쉬운 개념</h1>
<h3 id="commit과-push는-다르다">Commit과 Push는 다르다.</h3>
<p>많은 초보자가 Commit과 Push를 같은 것으로 생각하지만 둘은 전혀 다른 작업이다.</p>
<ul>
<li><strong>Commit</strong>은 내 컴퓨터(Local Repository)에 변경 사항을 저장하는 작업이다.</li>
<li><strong>Push</strong>는 Local Repository의 Commit을 원격 저장소(Remote Repository, GitHub 등)로 업로드하는 작업이다.</li>
</ul>
<p>즉, Commit을 여러 번 수행한 뒤 한 번에 Push할 수도 있다.</p>
<hr>
<h3 id="add와-commit의-차이">add와 commit의 차이</h3>
<ul>
<li><code>git add</code>는 변경 사항을 <strong>Staging Area</strong>에 올리는 작업이다.</li>
<li><code>git commit</code>은 Staging Area에 있는 변경 사항을 <strong>하나의 버전으로 저장</strong>하는 작업이다.</li>
</ul>
<p>이 두 단계가 분리되어 있기 때문에 필요한 파일만 선택해서 Commit할 수 있다.</p>
<hr>
<h1 id="📌-오늘-배운-내용-정리">📌 오늘 배운 내용 정리</h1>
<p>오늘은 Git을 사용하기 전에 반드시 알아야 하는 기본 개념과 Git의 핵심 동작 방식을 정리했다.</p>
<ul>
<li>경로(Path)는 절대경로와 상대경로로 나뉘며, 협업에서는 <strong>상대경로를 사용하는 것이 원칙</strong>이다.</li>
<li>루트 디렉터리는 파일 시스템의 최상위 위치이고, 홈 디렉터리는 사용자의 개인 작업 공간이다.</li>
<li>Git은 <strong>Working Directory → Staging Area → Repository</strong>의 순서로 파일을 관리하며, <code>git add</code>와 <code>git commit</code>은 각각 다른 역할을 수행한다.</li>
<li><code>git status</code>, <code>git log</code>, <code>git log --oneline</code>, <code>git config --global -l</code> 등의 명령어를 통해 저장소의 상태와 이력을 확인할 수 있다.</li>
<li><code>cat</code>과 <code>echo</code>는 터미널에서 파일을 확인하거나 생성할 때 자주 사용하는 기본 명령어이다.</li>
<li><code>.gitignore</code>를 활용하면 불필요하거나 민감한 파일을 Git의 관리 대상에서 제외할 수 있다.</li>
<li>Branch는 독립적인 작업 공간을 제공하며, Merge는 브랜치의 작업 내용을 합치는 과정이다.</li>
<li>Fast-Forward Merge는 브랜치 포인터만 이동하는 단순한 병합 방식이고, 3-Way Merge는 공통 조상을 기준으로 새로운 Merge Commit을 생성하는 병합 방식이다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>