Now Loading ...
-
-
-
-
📝[Post] 정적 웹사이트와 동적 웹사이트.
🙋♂️ 정적 웹사이트와 동적 웹사이트.
정적 웹사이트와 동적 웹사이트는 웹페이지를 생성하고 제공하는 방식에서 큰 차이를 보입니다.
각각의 특징을 이해하면 어떤 상황에서 어떤 타입의 웹사이트를 사용해야 하는지 결정하는 데 도움이 됩니다.
1️⃣ 정적 웹사이트.
정적 웹사이트는 미리 만들어진 HTML 파일들을 그대로 웹 서버에서 사용자의 브라우저로 전송하여 보여주는 웹사이트입니다.
이 파일들은 서버에 미리 저장되어 있으며, 사용자의 요청에 따라 변하지 않고 그대로 제공됩니다.
👍 정적 웹사이트의 장점.
단순성과 속도.
복잡한 서버 측 처리 없이 바로 파일을 전송하기 때문에 로딩 시간이 빠릅니다.
호스팅 비용.
낮은 서버 자원 사용으로 인해 비용이 저렴합니다.
보안.
동적 콘텐츠를 처리하는 서버 측 스크립트가 없어 보안 리스크가 상대적으로 낮습니다.
👎 정적 웹사이트의 단점.
유연성 부족.
각 페이지를 수동으로 업데이트해야 하며, 대규모 사이트에서는 유지 관리가 어려울 수 있습니다.
사용자 상호작용 부족.
사용자 입력에 따라 내용이 바뀌지 않으므로, 폼 제출이나 검색과 같은 기능을 직접 구현하기 어렵습니다.
2️⃣ 정적 웹사이트의 예시.
1. 포트폴리오 웹사이트.
웹 개발자, 디자이너, 사진작가 등의 포트폴리오를 위한 웹사이트들은 주로 정적입니다.
이 웹사이트들은 작품을 보여주는 갤러리, 연락처 정보, 이력서 등의 고정된 내용을 포함합니다.
2. 기업 정보 페이지.
소규모 기업이나 스타트업이 회사 정보, 제품 설명, 연락처 정보 등을 제공하는 단순한 웹사이트를 운영할 때, 이는 종종 정적 웹사이트로 구성됩니다.
3. 이벤트 안내 페이지.
특정 이벤트의 일시, 장소, 등록 방법 등을 안내하는 웹페이지로, 주로 내용의 변경이 적고, 정보의 전달이 주 목적일 때 정적 웹사이트로 구현됩니다.
3️⃣ 동적 웹사이트.
동적 웹사이트는 서버 측 프로그래밍 언어를 사용하여 사용자의 요청에 따라 실시간으로 웹페이지를 생성하고 제공합니다.
데이터베이스와의 상호작용을 통해 컨텐츠를 동적으로 생성하고 사용자의 요청에 맞춰 개별적으로 내용을 조정할 수 있습니다.
👍 동적 웹사이트의 장점.
유연성.
사용자의 입력이나 상호작용에 따라 내용을 쉽게 변경할 수 있습니다.
기능성.
데이터베이스에 정보를 저장하고 검색하는 등의 복잡한 기능을 구현할 수 있습니다.
개인화.
사용자의 선호나 행동에 따라 개인화된 경험을 제공할 수 있습니다.
👎 동적 웹사이트의 단점.
비용과 복잡성.
서버 측 처리를 위한 추가적인 자원이 필요하며, 구현과 유지 관리가 복잡해질 수 있습니다.
보안 위험.
데이터베이스와 서버 측 스크립트를 사용함으로써 보안 취약점이 발생할 수 있습니다.
속도.
페이지를 실시간으로 생성하므로 처리 시간이 길어질 수 있습니다.
4️⃣ 동적 웹사이트의 예시.
1. 전자 상거래 플랫폼.
Amazon, eBay 등의 쇼핑 웹사이트는 사용자의 검색, 구매 이력, 상품의 재고 상태 등에 따라 실시간으로 정보를 업데이트하고 표시해야 합니다.
이런 기능은 동적 웹사이트 기술을 필요로 합니다.
2. 소셜 네트워킹 서비스.
Facebook, Twitter와 같은 소셜 미디어 플랫폼은 사용자의 상호 작용에 기반하여 내용이 계속 업데이트 되며, 이러한 동적 상호 작용을 지원합니다.
3. 온라인 교육 플랫폼.
Coursera, Udemy, Inflearn와 같은 교육 플랫폼은 사용자가 선택한 강좌에 따라 개인화된 학습 내용을 제공하고, 퀴즈 점수를 기록하며, 진행 상태를 추적합니다.
🙋♂️ 마무리
정적 웹사이트와 동적 웹사이트 선택은 프로젝트의 요구 사항, 예산, 기대하는 사용자 경험 등에 따라 달라집니다.
간단한 정보 제공 사이트의 경우 정적 웹사이트가 적합할 수 있고, 사용자 상호작용과 데이터 처리가 중요하 서비스는 동적 웹사이트가 더 적합할 수 있습니다.
이러한 예시들을 통해 정적 웹사이트가 주로 고정된 내용을 제공하는 반면, 동적 웹사이트는 사용자의 입력과 상호작용에 따라 콘텐츠가 변경되는 복잡한 기능을 필요로 함을 알 수 있습니다.
각각의 사례에서 요구하는 기능과 특성에 맞춰 웹사이트의 형태를 결정합니다.
-
-
-
📝[Post] 서버와 클라이언트의 개념(1)
🙋♂️ Preview
이번 포스트에서는 컴퓨터 과학에서 말하는 서버와 클라이언트의 개념을 크게 세 가지로 나눠 살펴보겠습니다.
이것은 이해를 돕기 위한 분류로, 서버와 클라이언트라는 개념에 익숙해지고 난 후에 다시 보면 왜 이렇게 나누었는지 이해가 될 것 입니다.
1️⃣ 네트워크에서의 서버와 클라이언트.
서버(Server) : “서비스를 제공하는 쪽”
클라이언트(Client) : “서비스를 제공받는 쪽”
그림에서 서버는 실제 존재하는 물리적인 고성능 컴퓨터이고, 클라이언트는 데스크톱이나 노트북, 스마트폰 등과 같은 사용자들의 단말기를 나타냅니다.
즉, 물리적 장치와 또 다른 물리적 장치 사이의 관계를 의미합니다.
이렇게 물리적인 장치 간에 서로 통신이 이루어지기 위해서는 “통신을 시작하는 쪽”이 “상대방의 네트워크 주소인 IP 주소를 알고 있어야 합니다.”
“클라이언트가 서버의 IP주소를 알고있어야 서버와 클라이언트로서의 관계를 맺을 수 있습니다.”
1️⃣ 트래픽(Traffic) 처리 방법.
우리가 컴퓨터나 스마트폰으로 이용하는 서비스들은 수백만 명 이상의 사용자가 동시에 사용하고 있는 경우가 대부분입니다.
그렇다면 이러한 서비스를 운영하는 서버가 모두 고성능일까요? 🤔
당연히 그렇지 않습니다 ❌
한꺼번에 수백만 명 이상의 사용자로부터 생기는 “트래픽(Traffic)”을 처리하기 위한 방법은 여러가지가 있습니다.
여기서는 가장 범용적이고 직관적인 방법 두 가지, “로드 밸런싱” 과 “캐시”에 대해 간단히 설명하겠습니다.
1️⃣ 로드 밸런싱(Load Balancing).
“로드 밸런싱(Load Balancing)” : 부하 분산.
즉, 서버에 가해지는 부하(Load)를 분산하는 것입니다.
사용자들의 트래픽을 여러 서버가 나눠 받도록 구성하며, 일반적으로 네트워크 장비인 “스위치(Switch)” 를 할당해 “로드 밸런싱”할 수 있습니다.
스위치에서 어떤 서버로 로드 밸런싱이 되도록 할지는 소프트웨어적으로 제어할 수 있습니다.
“로드 밸런싱” 은 “스위치” 라는 장비가 “클라이언트의 트래픽을 먼저 받아” 서 여러 대의 서버로 “분산” 해 주는 방식입니다.
이렇게 하면 부하가 분산되는 효과 외에도 스위치 뒤에 연결된 서버들을 필요에 따라 추가하거나 삭제할 수 있어 편리합니다.
2️⃣ 캐시(Cache).
“캐시(Cache)” : 비용이 큰 작업의 결과를 어딘가에 저장하여 비용이 작은 작업으로 동일한 효과를 내는 것.
캐시를 이용하면 매번 요청이 들어올 때마다 비용이 큰 작업을 다시 수행할 필요 없이 미리 저장된 결과로 응답하면 됩니다.
물론 이렇게 하면 가장 최신의 데이터는 아닐 수 있지만, 성능을 극대화시키고자 하는 캐시의 목적을 생각해 데이터의 실시간성을 조금 포기해도 되는 경우가 많습니다.
✏️ Example
음원 서비스
데이터베이스에 저장된 수많은 음원의 다운로드 수, 스트리밍 수, 추천 수 등으로 인기 점수를 계산하려 100갸의 곡을 오름차순 순위로 제공합니다.
만약 사용자가 한 번 음원을 조회할 때마다 모든 음원의 인기 점수를 계산해 순위를 매긴다면 아마 사용자가 수백 명만 되어도 서버 부하로 응답 시간이 매우 느려질 것입니다.
이렇게 수많은 음원의 인기 점수를 매번 계산하여 순위를 매기는 작업이 바로 ‘비용이 큰 작업’ 입니다.
매시 정각마다 TOP 100을 계산한 결과를 저장했다가 사용자의 요청이 들어왔을 때 응답해주면 ‘비용이 작은 작업’으로 대체할 수 있습니다.
사용자는 16시 30분에 16시에 저장된 TOP 100 결과로도 큰 불편함을 느끼지 않습니다.
이렇게 사용자가 캐시된 과거의 데이터를 보더라도 서비스 시용에 지장이 없다면 캐시 사용을 충분히 고려할 만합니다.
“캐시” 는 다양한 상황에서 비슷한 뜻으로 사용되지만, 공통적으로, ‘비용이 큰 작업을 비용이 작은 작업으로 대신하는 것’이라고 정리할 수 있습니다.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📝[blog post] Java Docs 보는 방법.
📝 Java Docs를 읽는 능력이 필요한 이유. :)
저는 Documentation이 그 어떤 유명 테크 블로거의 글 보다 중요하고 심도있게 읽어야 한다는 개인적인 의견이 있습니다.
그 이유는 Java를 개발한 개발자분들이 직접 만든 설명서나 다름 없기 때문입니다.
우리가 레고를 생각해 봅시다.
내가 좋아하는 레고를 사서 집에서 조립할 때 무엇을 보나요? 🤔
맞습니다!
레고 패키지 안에 들어있는 “설명서”를 기반으로 레고를 조립합니다.
레고를 디자인하고 만드신 분이 직접 “이렇게 순서대로 만들면 당신이 원하는 멋진 레고 완성품을 얻을 수 있습니다!” 라는 것을 직.간접적으로 보여주는 아주 자세한 설명이 들어있죠 📝
설명서는 직접 디자인하고 설계한 사람의 철학과 그들이 왜 그렇게 만들었는지 그리고 어떻게 쓰여야하는지 정확, 명료하게 명시되어 있습니다.
또한 다른 구성품과 맞춰볼 수 있는 것도 제안하거나 보여주기도 합니다.
그래서 Documentation을 보고 제대로 활용할 줄 아는 것이 개발자에게는 중요한 능력 중 하나가 아닐까 하는 생각을 합니다 🙋♂️
1️⃣ Java Documentation 보기.
1. 온라인 문서.
Java SE Documentation은 Oracle 공식 사이트에서 제공됩니다.
Java 버전에 따라 다른 문서가 제공되니, 사용하는 Java 버전에 맞는 문서를 선택해야 합니다.
2. IDE 내장 문서.
많은 통합 개발 환경(IDE)에는 JavaDoc을 쉽게 볼 수 있는 기능이 내장되어 있습니다. InteillJ IDEA, Eclipes, NetBeans 등에서 코드 작성 시 JavaDocs를 볼 수 있습니다.
예를 들어, IntelliJ IDEA에서 클래스나 메소드 이름 위에 커서를 올리면 해당 클래스나 메소드의 JavaDoc이 팝업으로 표시됩니다.
3. 로컬 문서.
Java JDK를 설치할 때, JavaDoc을 로컬에 다운로드할 수 있습니다. 이를 통해 인터넷 연결 없이도 문서를 참조할 수 있습니다.
JDK 설치 경로 아래의 docs 폴더에 HTML 형식의 문서가 저장되어 있습니다.
2️⃣ Java Documentation 활용 방법
Java Documentation을 효과적으로 활용하는 방법을 알아봅시다.🤩
1. 클래스 및 메소드 탐색.
API 문서에서 패키지, 클래스, 메소드, 필드 등의 세부 정보를 탐색할 수 있습니다.
예를 들어, java.util 패키지에 어떤 클래스가 포함되어 있는지, ArrayList 클래스에 어떤 메소드가 있는지 등을 확인할 수 있습니다.
2. 사용 예제 찾기.
각 클래스와 메소드에는 사용 예제가 포함되어 있을 수 있습니다. 이러한 예제는 해당 API를 올바르게 사용하는 방법을 이해하는 데 도움이 됩니다.
3. 메소드 시그니처 및 설명.
메소드의 매개변수, 반환값, 예외 등을 설명하는 시그니처와 설명을 통해 메소드의 사용법을 정확히 알 수 있습니다.
예를 들어, String 클래스의 substring 메소드의 시그니처와 설명을 보면, 매개변수로 전달해야 할 값과 반환되는 값에 대한 정보를 얻을 수 있습니다.
4. 상속 구조 및 인터페이스.
클래스가 구현하는 인터페이스와 상속받는 클래스에 대한 정보를 확인할 수 있습니다. 이를 통해 클래스의 기능을 확장하거나 인터페이스를 구현하는 방법을 이해할 수 있습니다.
3️⃣ 예제
다음은 Java Documentation을 활용하는 몇 가지 예제입니다.
예제 1: ArrayList 클래스의 메소드 사용법 확인 🙋♂️
온라인 문서에서 ArrayList 클래스를 찾습니다.
Java SE Documentation에서 java.util.ArrayList 를 검색합니다.
ArrayList 클래스의 API 문서를 열어 메소드 목록을 확인합니다.
add(E e) 메소드 사용법 확인하기.
add(E e) 메소드는 리스트의 끝에 요소를 추가하는 메소드입니다.
메소드 설명을 읽고, 예제를 확인하여 사용법을 이해합니다.
예제 2. String 클래스의 substring 메소드 사용법 확인 🙋♂️
IDE 내장 문서 활용하기.
IntelliJ IDEA나 Eclipse에서 String 클래스의 substring 메소드를 사용하려고 할 때, 메소드 이름 위에 커서를 올리면 JavaDoc이 표시됩니다.
JavaDoc을 통해 substring(int beingIndex, int endIndex) 메소드의 매개변수와 반환 값에 대한 설명을 읽습니다.
public class Main {
public static void main(String[] args) {
String text = "Hello, World!";
String subText = text.substring(7, 12); // "World"
System.out.println(subText);
}
}
위 예제에서 substring 메소드의 매개변수가 beginIndex 와 endIndex 임을 알 수 있으며, 이는 시작 인덱스부터 종료 인덱스 전까지의 문자열을 반환합니다.
예제 3. 예외 처리 방법 확인 🙋♂️
예외 클래스 문서 확인하기.
java.lang.NullPointerException 클래스의 문서를 확인하여 언제 이 예외가 발생하는지, 그리고 이를 어떻게 처리할 수 있는지에 대한 정보를 얻습니다.
예외 처리 예제
public class Main {
public static void main(String[] args) {
try {
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Caught a NullPointerException");
}
}
}
이 예제는 NullPointException 이 발생할 때 이를 처리하는 방법을 보여줍니다.
📝 요약.
Java Documentation은 Java API를 이해하고 사용하는 데 필수적인 자료입니다.
Java Documentation를 온라인, IDE, 또는 로컬에서 접근할 수 있습니다.
API 문서를 통해 클래스와 메소드의 세부 정보를 확인하고, 예제를 참고하여 올바르게 사용하는 방법을 배울 수 있습니다.
상속 구조와 인터페이스 구현 방법을 이해하여 코드의 재사용성과 확장성을 높일 수 있습니다.
-
-
-
-
💾 [CS] 다양한 입출력 방법
1️⃣ 다양한 입출력 방법.
가장 보편적인 입출력 방법인 프로그램 입출력과 인터럽트 기반 입출력, DMA 입출력에 대해 알아보겠습니다.
1️⃣ 다양한 입출력 방법.
입출력 작업을 수행시 CPU와 장치 컨트롤러가 정보를 주고받아야 합니다.
여기에는 크게 세 가지 방법이 있습니다.
프로그램 입출력.
인터럽트 기반 입출력.
DMA 입출력.
2️⃣ 프로그램 입출력
프로그램 입출력(programmed I/O) 은 기본적으로 프로그램 속 명령어로 입출력 장치를 제어하는 방법입니다.
CPU가 프로그램 속 명령어를 실행하는 과정에서 입출력 명령어를 만나면 CPU는 입출력장치에 연결된 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다.
메모리에 저장된 정보를 하드 디스크에 백업하는 상황을 생각해 봅시다.
CPU는 대략 아래 과정으로 입출력 작업을 합니다.
1. ‘메모리에 저장된 정보를 하드 디스크에 백업한다’는 말은 ‘하드 디스크에 새로운 정보를 쓴다’는 말과 같습니다.
우선 CPU는 하드 디스크 컨트롤러의 제어 레지스터에 쓰기 명령을 보냅니다.
2. 하드 디스크 컨트롤러는 하드 디스크 상태를 확인합니다.
하드 디스크가 준비된 상태라면 하드 디스크 컨트롤러는 상태 레지스터에 준비되었다고 표시합니다.
3. (1) CPU는 상태 레지스터를 주기적으로 읽어 보며 하드 디스크의 준비 여부를 확인합니다.
(2) 하드 디스크가 준비됐음을 CPU가 알게 되면 백업할 메모리의 정보를 데이터 레지스터에 씁니다. 아직 백업 작업(쓰기 작업)이 끝나지 않았다면 (1)번부터 반복하고, 쓰기가 끝났다면 작업을 종료합니다.
이렇듯 프로그램 입출력 방식에서의 입출력 작업은 CPU가 장치 컨트롤러의 레지스터 값을 읽고 씀으로써 이루어집니다.
3️⃣ 메모리 맵 입출력과 고립형 입출력.
CPU 내부에 있는 레지스터들과 달리 CPU는 여러 장치 컨트롤러 속 레지스터들을 모두 알고 있기란 어렵습니다.
그렇다면 아래와 같은 명령어들은 어떻게 명령어로 표현되고, 메모리에 어떻게 저장 되어 있을까요?
프린터 컨트롤러의 상태 레지스터를 읽어라.
프린터 컨트롤러의 데이터 레지스터에 100을 써라.
키보드 컨트롤러의 상태 레지스터를 읽어라.
하드 디스크 컨트롤러의 데이어 레지스터에 ‘a’를 써라.
여기에는 크게 두 가지 방식이 있습니다.
바로 메모리 맵 입출력 과 고립형 입출력 입니다.
1️⃣ 메모리 맵 입출력.
메모리 맵 입출력(memory-mapped I/O) 은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 하나의 주소 공간으로 간주하는 방법입니다.
가령 1,024 개의 주소를 표현할 수 있는 컴퓨터가 있을 때 1,024개 전부 메모리 주소를 표현하는 데 사용하지 않습니다.
512개는 메모리 주소를, 512개는 장치 컨트롤러의 레지스터를 표현하기 위해 사용합니다.
주소 공간 일부를 아래와 같이 약속했다고 가정해 봅시다.
516번지: 프린터 컨트롤러의 데이터 레지스터
517번지: 프린터 컨트롤러의 상태 레지스터
518번지: 하드 디스크 컨트롤러의 데이터 레지스터
519번지: 하드 디스크 컨트롤러의 상태 레지스터
그렇다면 CPU는 ‘517번지를 읽어 들여라’라는 명령어로 키보드 상태를 읽을 수 있습니다.
그리고 ‘518 번지에 a를 써라’ 라는 명령어로 하드 디스크 컨트롤러의 데이터 레지스터로 데이터를 보낼 수 있습니다.
이때 중요한 점은 메모리 맵 입출력 방식에서 CPU는 메모리의 주소들이나 장치 컨트롤러의 레지스터들이나 모두 똑같이 메모리 주소를 대하듯 하면 된다는 점입니다.
그래서 메모리에 접근하는 명령어와 입출력장치에 접근하는 명령어는 굳이 다를 필요가 없습니다.
CPU가 ‘517번지를 읽어라’라는 명령어를 실행했을 때 517번지가 메모리상의 주소를 가리킨다면 CPU는 메모리 517번지에 저장된 정보를 읽어 들일 것이고, 517번지가 프린터 컽츠롤러의 상태 레지스터를 가리킨다면 CPU는 프린터의 상태를 확인할 수 있기 때문입니다.
2️⃣ 고립형 입출력.
고립형 입출력(isolated I/O) 은 메모리를 위한 주소 공간과 입출력장치를 위한 주소 공간을 분리하는 방법입니다.
가령 1,024개의 주소 공간을 가진 컴퓨터가 있다고 가정해 봅시다.
아래 그림처럼 제어 버스에 ‘메모리 읽기/쓰기’ 선 이외에 ‘입출력장치 읽기/쓰기’ 선이 따로 있다면 메모리에도 1,024 개의 주소 공간을 활용하고, 입출력장치도 1,024개의 주소 공간을 활용할 수 있습니다.
CPU가 메모리 읽기/쓰기 선이 활성화되는 명령어를 실행할 때는 메모리에 접근하고, 입출력장치 읽기/쓰기 선이 활성화되는 명령어를 실행할 때는 장치 컨트롤러에 접근하기 때문입니다.
고립형 입출력 방식에서 CPU는 입출력장치에 접근하기 위해 메모리에 접근하는 명령어와는 다른(입출력 읽기/쓰기 선을 활성화시키는) 입출력 명령어를 사용합니다.
메모리에 접근하는 명령어와 입출력장치에 접근하는 명령어는 굳이 다를 필요가 없었던 메모리 맵 입출력과 대조적입니다.
메모리 맵 입출력
고립형 입출력
메모리와 입출력장치는 같은 주소 공간 사용
메모리와 입출력장치는 분리된 주소 공간 사용
메모리 주소 공간이 축소됨
메모리 주소 공간이 축소되지 않음
메모리와 입출력장치에 같은 명령어 사용 가능
입출력 전용 명령어 사용
2️⃣ 인터럽트 기반 입출력
인터럽트는 ‘CPU가 입출력장치에 처리할 내용을 명령하면 입출력장치가 명령어를 수행하는 동안 CPU는 다른 일을 할 수 있다’라고 했습니다.
또한 ‘입출력장치가 CPU에게 인터럽트 요청 신호를 보내면 CPU는 하던 일을 잠시 멈추고 해당 인터럽트를 처리하는 프로그램인 인터럽트 서비스 루틴을 실행한 뒤 다시 하던 일로 되돌아온다’라고 했습니다.
입출력장치에 의한 하드웨어 인터럽트는 정확히 말하자면 입출력장치가 아닌 장치 컨트롤러에 의해 발생합니다.
CPU는 장치 컨트롤러에 입출력 작업을 명령하고, 장치 컨트롤러가 입출력장치를 제어하며 입출력을 수행하는 동안 CPU는 다른 일을 할 수 있습니다.
장치 컨트롤러가 입출력 작업을 끝낸 뒤 CPU에게 인터럽트 요청 신호를 보내면 CPU는 하던 일을 잠시 백업하고 인터럽트 서비스 루틴을 실행합니다.
이렇게 인터럽트를 기반으로 하는 입출력을 인터럽트 기반 입출력(Interrupt-Drive I/O) 이라고 합니다.
폴링
인터럽트와 자주 비교되는 개념 중 폴링(polling) 이라는 개념이 있습니다.
‘CPU는 주기적으로 장치 컨트롤러의 상태 레지스터를 확인하며 입출력장치의 상태를 확인한다’ 라고 했습니다.
이처럼 폴링이란 입출력장치의 상태는 어떤지, 처리할 데이터가 있는지를 주기적으로 확인하는 방식입니다.
폴링 방식은 당연하게도 인터럽트 방식보다 CPU의 부담이 더 큽니다.
인터럽트를 활용하면 CPU가 인터럽트 요청을 받을 때까지 온전히 다른 일에 집중할 수 있기 때문입니다.
이번에는 조금 더 일반적인 입출력장치가 많을 때를 생각해 봅시다.
예를 들어 키보드, 모니터, 스피커, 마우스를 사용하고 있다고 생각해봅시다.
이것은 컴퓨터 속 CPU가 동시다발적으로 발생하는 키보드, 마우스, 모니터, 스피커 인터럽트를 모두 처리해야 한다는 말이기도 합니다.
어떻게 여러 입출력장치에서 인터럽트가 동시에 발생한 경우에는 인터럽트들을 어떻게 처리해야 할까요?
간단하게 생각하면 인터럽트가 발생한 순서대로 인터럽트를 처리하는 방법이 있습니다.
가령 인터럽트 A를 처리하는 도중 발생한 또 다른 인터럽트 B의 요청을 받아들이지 않고, 인터럽트 A 서비스 루틴이 끝나면 그때 비로소 인터럽트 B 서비스 루틴을 실행하는 것이죠.
CPU가 플래그 레지스터 속 인터럽트 비트를 비활성화한 태 인터럽트를 처리하는 경우 다른 입출력장치에 의한 하드웨어 인터럽트를 받아들이지 않기 때문에 CPU는 이렇듯 순차적으로 하드웨어 인터럽트를 처리하게 됩니다.
하지만 현실적으로 모든 인터럽트를 전부 순차적으로만 해결할 수 없습니다.
인터럽트 중에서도 더 빨리 처리해야 하는 인터럽트가 있기 때문입니다.
즉, CPU는 인터럽트 간에 우선순위를 고려하여 우선순위가 높은 인터럽트 순으로 여러 인터럽트를 처리할 수 있습니다.
예를 들어 아래 그림과 같이 현재 CPU가 인터럽트 A를 처리하는 도중에 또 다른 인터럽트 B가 발생했다고 가정해 봅시다.
만약 지금 처리 중인 인터럽트 A보다 B의 우선순위가 낮다면 CPU는 A를 모두 처리한 뒤 B를 처리합니다.
하지만 인터럽트 A보다 B의 우선순위가 높다면 CPU는 인터럽트 A의 실행을 잠시 멈추고 인터럽터 B를 처리한 뒤 다시 A를 처리합니다.
플래그 레지스터 속 인터럽트 비트가 활성화되어 있는 경우, 혹은 인터럽트 비트를 비활성화해도 무시할 수 없는 인터럽트인 NMI(Non-Mashable Interrupt) 가 발생한 경우 CPU는 이렇게 우선순위가 높은 인터럽트부터 처리합니다.
우선순위를 반영하여 다중 인터럽트를 처리하는 방법에는 여러 가지가 있지만, 많은 컴퓨터에서는 프로그래머블 인터럽트 컨트롤러(PIC: Programmable Interrupt Controller) 라는 하드웨어를 사용합니다.
PIC 는 여러 장치 컨트롤러에 연결되어 장치 컨트롤러에서 보낸 하드웨어 인터럽트 요청들의 우선 순위를 판별한 뒤 CPU에 지금 처리해야 할 하드웨어 인터럽트는 무엇인지를 알려주는 장치입니다.
PIC에는 여러 핀이 있는데, 각 핀에는 CPU에 하드웨어 인터럽트 요청을 보낼 수 있는 약속된 하드웨어가 연결되어 있습니다.
가령 첫 번째 핀은 타이머 인터럽트를 받아들이는 핀, 두 번째 핀은 키보드 인터럽트를 받아들이는 핀… 이런 식으로 말이죠.
PIC에 연결된 장치 컨트롤러들이 동시에 하드웨어 인터럽트 요청을 보내면 PIC는 이들의 우선순위를 판단하여 CPU에 가장 먼저 처리할 인터럽트를 알려줍니다.
PIC의 다중 인터럽트 처리 과정을 조금 더 정확히 알아봅시다.
1. PIC가 장치 컨트롤러에서 인터럽트 요청신호(들) 를 받아들입니다.
2. PIC는 인터럽트 우선순위를 판단한 뒤 CPU에 처리해야 할 인터럽트 요청 신호를 보냅니다.
3. CPU는 PIC에 인터럽트 확인 신호를 보냅니다.
4. PIC는 데이터 버스를 통해 CPU에 인터럽트 벡터를 보냅니다.
5. CPU는 인터럽트 벡터를 통해 인터럽트 요청의 주체를 알게 되고, 해당 장치의 인터럽트 서비스 루틴을 실행합니다.
일반적으로 더 많고 복잡한 장치들의 인터럽트를 관리하기 위해 아래와 같이 PIC를 두 개 이상 계층적으로 구성합니다.
이렇게 PIC를 여러 개 사용하면 훨씬 더 많은 하드웨어 인터럽트를 관리할 수 있습니다.
참고로 PIC가 무시할 수 없는 인터럽트인 NMI까지 우선순위를 판별하지 않습니다.
NMI는 우선 순위가 가장 높아 우선순위 판별이 불필요하기 때문입니다.
PIC가 우선순위를 조정해주는 인터럽트는 인터럽트 비트를 통해 막을 수 있는 하드웨어 인터럽트입니다.
3️⃣ DMA 입출력
앞에 설명한 프로그램 기반 입출력과 인터럽트 기반 입출력의 공통점이 있다면 입출력장치와 메모리 간의 데이터 이동은 CPU가 주도하고, 이동하는 데이터도 반드시 CPU를 거친다는 점입니다.
예를 들어 입출력장치 데이터를 메모리에 저장하는 경우 CPU는 (1) 장치 컨트롤러에서 입출력장치 데이터를 하나씩 읽어 레지스터에 적재하고, (2) 적재한 데이터를 메모리에 저장합니다.
메모리 속 데이터를 입출력장치에 내보내는 경우도 마찬가지입니다.
CPU는 (1) 메모리에서 데이터를 하나씩 읽어 레지스터에 적재하고, (2) 적재한 데이터를 하나씩 입출력장치에 내보냅니다.
입출력장치와 메모리 사이에 전송되는 모든 데이터가 반드시 CPU를 거쳐야 한다면 가뜩이나 바쁜 CPU는 입출력장치를 위한 연산 때문에 시간을 뺏기게 됩니다.
하드 디스크 백업과 같이 대용량 데이터를 옮길 때는 CPU 부담이 더욱 커집니다.
그래서 입출력장치와 메모리가 CPU를 거치지 않고도 상호작용할 수 있는 입출력 방식인 DMA(Direct Memory Access) 가 등장하였습니다.
DMA는 이름 그대로 직접 메모리에 접근할 수 있는 입출력 기능입니다.
DMA 입출력을 하기 위해서는 시스템 버스에 연결된 DMA 컨트롤러 라는 하드웨어가 필요합니다.
DMA 입출력 과정.
일반적으로 DMA 입출력은 아래와 같이 이루어집니다.
(1) CPU는 DMA 컨트롤러에 입출력장치의 주소, 수행할 연산(읽기/쓰기), 읽거나 쓸 메모리 주소 등과 같은 정보로 입출력 작업을 명령합니다.
(2) DMA 컨트롤러는 CPU 대신 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다. 이때 DMA 컨트롤러는 필요한 경우 메모리에 직접 접근하여 정보를 읽거나 씁니다.
(3) 입출력 작업이 끝나면 DMA 컨트롤러는 CPU에 인터럽트를 걸어 작업이 끝났음을 알립니다.
이번에는 메모리 내의 정보를 하드 디스크에 백업하는 작업이 DMA 입출력으로 어떻게 이루어지는지도 알아봅시다.
1. CPU는 DMA 컨트롤러에 하드 디스크 주소, 수행할 연산(쓰기), 백업할 내용이 저장된 메모리 주소 등의 정보와 함께 입출력 작업을 명령합니다.
2. (1) DMA 컨트롤러는 CPU를 거치지 않고 메모리와 직접 상호작용하며 백업할 정보를 읽어오고, (2) 이를 하드 디스크의 장치 컨트롤러에 내보냅니다.
3. 백업이 끝나면 DMA 컨트롤러는 CPU에게 인터럽트를 걸어 작업이 끝났음을 알립니다.
위 입출력 과정을 보면 알 수 있듯 입출력장치와 메모리 사이에 주고받을 데이터는 CPU를 거치지 않습니다.
CPU는 DMA 컨트롤러에게 입출력 작업 명령을 내리고, 인터럽트만 받으면 되기 때문에 작업 부담을 훨씬 줄일 수 있습니다.
다시 말해 CPU는 오직 입출력의 시작과 끝에만 관여하면 됩니다.
그런데 여기서 생각해 봐야 할 문제가 있습니다.
DMA 컨트롤러는 시스템 버스로 메모리에 직접 접근이 가능하지만, 시스템 버스는 동시 사용이 불가능합니다.
시트템 버스는 공용 자원이기 때문입니다.
CPU가 시스템 버스를 사용할 떄 DMA 컨트롤러는 시스템 버스를 사용할 수 없고, DMA 컨트롤러가 시스템 버스를 사용할 때는 CPU가 시스템 버스를 사용할 수 없습니다.
그래서 DMA 컨트롤러는 CPU가 시스템 버스를 이용하지 않을 때마다 조금씩 시스템 버스를 이용하거나, CPU가 일시적으로 시스템 버스를 이용하지 않도록 허락을 구하고 시스템 버스를 집중적으로 이용합니다.
CPU 입장에서는 마치 버스에 접근하는 주기를 도둑 맞는 기분이 들 겁니다. 그래서 이러한 DMA의 시스템 버스 이용을 사이클 스틸링(cycle stealing) 이라고 부릅니다.
입출력 버스
마지막으로 DMA 컨트롤러와 장치 컨트롤러의 연결 방식과 입출력 버스에 대해 알아봅시다.
CPU, 메모리, DMA 컨트롤러, 장치 컨트롤러가 모두 같은 버스를 공유하는 구성에서는 DMA를 위해 한 번 메모리에 접근할 때마다 시스템 버스를 두 번 사용하게 되는 부작용이 있습니다.
예로 들었던 메모리 내 정보를 하드 디스크로 백업하는 상황을 다시 생각해 봅시다.
이 경우 (1) 메모리에서 DMA 컨트롤러로 데이터를 가져오기 위해 시스템 버스를 한 번 사용하고, (2) DMA 컨트롤러의 데이터를 장치 컨트롤러로 옮기기 위해 시스템 버스를 또 한 번 사용합니다.
DMA를 위해 시스템 버스를 너무 자주 사용하면 그만큼 CPU가 시스템 버스를 이용하지 못합니다.
이 문제는 DMA 컨트롤러와 장치 컨트롤러들을 입출력 버스(input/output bus) 라는 별도의 버스에 연결하여 해결할 수 있습니다.
아래 그림과 같이 장치 컨트롤러들이 시스템 버스가 아닌 입출력 버스로 DMA 컨트롤러에 연결된다면 DMA 컨트롤러와 장치 컨트롤러가 서로 데이터를 전송할 때는 시스템 버스를 이용할 필요가 없으므로 시스템 버스의 사용 빈도를 줄일 수 있습니다.
현대 대부분 컴퓨터에는 입출력 버스가 있습니다.
다시 말해 대부분의 입출력장치(장치 컨트롤러)는 시스템 버스가 아닌 입출력 버스와 연결됩니다.
이런 점에서 볼 때 입출력 버스는 입출력장치를 컴퓨터 내부와 연결 짓는 통로라고도 볼 수 있습니다.
입출력 버스에는 PIC(Peripheral Component Interconnect) 버스, PCI Express(PCIe) 버스 등 여러 종류가 있습니다.
다음 그림은 여러 입출력 장치들을 PCIe 버스와 연결해 주는 통로인 PCIe 슬롯 입니다.
사용하는 거의 모든 입출력장치들은 이렇게 입출력 버스와 연결되는 통로를 통해 시스템 버스를 타고 CPU와 정보를 주고받습니다.
🙋♂️ 마무리.
키워드로 정리하는 핵심 포인트
프로그램 입출력은 프로그램 속 명령어로 입출력 작업을 하는 방식입니다.
메모리 맵 입출력은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 하나의 주소 공간으로 간주하는 입출력 방식입니다.
고립형 입출력은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 별도로 분리하는 입출력 방식입니다.
인터럽트 기반 입출력은 인터럽트로써 입출력을 수행하는 방법입니다.
DMA 입출력은 CPU를 거치지 않고 메모리와 입출력장치 간의 데이터를 주고받는 입출력 방식입니다.
입출력 버스는 입출력장치와 컴퓨터 내부를 연결 짓는 톨로로, 입출력 작업 과정에서 시스템 버스 사용 횟수를 줄여줍니다.
-
-
-
💾 [CS] 장치 컨트롤러와 장치 드라이버
1️⃣ 장치 컨트롤러와 장치 드라이버.
1️⃣ 장치 컨트롤러.
입출력장치는 CPU, 메모리보다 다루기가 더 까다롭습니다.
여기에는 크게 두 가지 이유가 있습니다.
첫째, 입출력장치에는 종류가 너무나도 많습니다.
키보드, 모니터, USB 메모리, CD-ROM, SSD, 마우스, 스피커, 프린터 등 매우 많습니다.
장치가 이렇게 다양하면 자연스레 장치마다 속도, 데이터 전송 형식 등도 다양합니다.
따라서 다양한 입출력장치와 정보를 주고받는 방식을 규격화하기가 어렵습니다.
이는 마치 CPU와 메모리는 한국어를 사용하는데, 프린터는 영어, 스피커는 일본어, 모니터는 중국어를 사용하는 상황과 같습니다.
둘째, 일반적으로 CPU와 메모리의 데이터 전송률은 높지만 입출력장치의 데이터 전송률은 낮습니다.
여기서 전송률(transfer rate) 이란 데이터를 얼마나 빨리 교환할 수 있는지를 나타내는 지표입니다.
CPU와 메모리처럼 전송률이 높은 장치는 1초에도 수많은 데이터를 주고받을 수 있지만, 키보드나 마우스와 같은 상대적으로 전송률이 낮은 장치는 같은 시간 동안 데이터를 조금씩만 주고받을 수 있습니다.
전송률의 차이는 CPU와 메모리, 입출력 장치간의 통신을 어렵게 합니다.
장치 컨트롤러(Derive Controller)
물론 어떤 입출력장치는 CPU나 메모리보다 전송률이 높은 경우도 있습니다.
하지만 결과적으로 CPU나 메모리와 전송률이 비슷하지 않기 때문에 같은 어려움을 겪게 됩니다.
이와 같은 이유로 입출력장치는 컴퓨터에 직접 연결되지 않고 장치 컨트롤러(Drive Controller) 라는 하드웨어를 통해 연결됩니다.
장치 컨트롤러는 입출력 제어기(I/O Controller), 입출력 모듈(I/O Module) 등으로 다양하게 불립니다.
모든 입출력장치는 각자의 장치 컨트롤러를 통해 컴퓨터 내부와 정보를 주고받고, 장치 컨트롤러는 하나 이상의 입출력장치와 연결되어 있습니다.
예를 들어 하드 디스크 또한 장치 컨트롤러가 있습니다.
2️⃣ 장치 컨트롤러의 역할.
장치 컨트롤러는 대표적으로 다음과 같은 역할을 통해 앞에서 언급한 문제들을 해결합니다.
CPU와 입풀력장치 간의 통신 중개
오류 검출
데이터 버퍼링
입풀력장치 종류가 많이 정보 규격롸가 어려웠던 문제는 장치 컨트롤러가 일종의 번역가 역할을 함으로써 해결할 수 있습니다.
그 과정에서 장치 컨트롤러는 자신과 연결된 입출력장치에 문제는 없는지 오류를 검출하기도 합니다.
장치 컨트롤러의 세 번째 기능인 데이터 버퍼링은 무엇일까요?
버퍼링(buffering) 이란 전송률이 높은 장치와 낮은 장치 사이에 주고받는 데이터를 버퍼(buffer) 라는 임시 저장 공간에 저장하여 전송률을 비슷하게 맞추는 방법입니다.
쉽게 말해 버퍼링은 ‘버퍼에 데이터를 조금씩 모았다가 한꺼번에 내보내거나, 데이터를 한 번에 많이 받아 조금씩 내보내는 방법’이라고 보면 됩니다.
즉, 장치 컨트롤러는 일반적으로 전송률이 높은 CPU와 일반적으로 전송률이 낮은 입출력장치와의 전송률 차이를 데이터 버퍼일으로 완화합니다.
3️⃣ 장치 컨트롤러의 내부 구조.
이번에는 장치 컨트롤러의 간략화된 내부 구조를 살펴봅시다.
장치 컨트롤러 내부는 아래와 같습니다.
실제로는 이보다 복잡하지만, 기억해야 하는 것은 데이터 레지스터(data register) 와 상태 레지스터(status register), 제어 레지스터(control register) 세 가지 입니다.
데이터 레지스터는 CPU와 입출력장치 사이에 주고받을 데이터가 담기는 레지스터입니다.
앞서 장치 컨트롤러는 데이터 버퍼링으로 전송률 차이를 완화한다고 했습니다.
데이터 레지스터가 그 버퍼 역할을 합니다.
최근 주고받은 데이터가 많은 입출력장치에서는 레지스터 대신 RAM을 사용하기도 합니다.
상태 레지스터에는 입출력장치가 입출력 작업을 할 준비가 되었는지, 입출력 작업이 완료되었는지, 입출력장치에 오류는 없는지 등의 상태 정보가 저장됩니다.
제어 레지스터는 입출력장치가 수행할 내용에 대한 제어 정보와 명령을 저장합니다.
이 레지스터들에 담긴 값들은 버스를 타고 CPU나 다른 입출력장치로 전달되기도 하고, 장치 컨트롤러에 연결된 입출력장치로 전달됩니다.
2️⃣ 장치 드라이버
새로운 장치를 컴퓨터에 연결하려면 장치 드라이버를 설치해야 합니다.
1️⃣ 장치 드라이버
장치 드라이버(device driver) 란 장치 컨트롤러의 동작을 감지하고 제어함으로써 장치 컨트롤러가 컴퓨터 내부와 정보를 주고받을 수 있게 하는 프로그램입니다.
프로그램이기에 당연히 실행 과정에서 메모리에 저장됩니다.
장치 컨트롤러가 입출력장치를 연결하기 위한 하드웨어적인 통로라면, 장치 드라이버는 입출력장치를 연결하기 위한 소프트웨어적인 통로입니다.
컴퓨터가 연결된 장치의 드라이버를 인식하고 실행할 수 있다면 그 장치는 어떤 회사에서 만들어진 제품이든, 생김새가 어떻든 상관없이 컴퓨터 내부와 정보를 주고받을 수 있습니다.
반대로 장치 드라이버를 인식하거나 실행할 수 없는 상태라면 그 장치는 컴퓨터 내부와 정보를 주고받을 수 없습니다.
장치 드라이버를 인식하고 실행하는 주체
장치 드라이버를 인식하고 실행하는 주체는 정확히 말하자면 윈도우, macOS와 같은 운영체제입니다.
즉, 운영체제가 장치드라이버를 인식하고 실행할 수 있다면 그 장치는 컴퓨터 내부와 정보를 주고받을 수 있습니다.
장치 드라이버는 운영체제가 기본으로 제공하는 것도 있지만, 장치 제작자가 따로 제공하기도 합니다.
물론 장치 제작자가 장치 드라이버를 따로 제공하는 경우 입출력장치는 해당 드라이버를 직접 설치해야만 사용이 가능합니다.
3️⃣ 키워드로 정리하는 핵심 포인트
입출력장치는 장치 컨트롤러 를 통해 컴퓨터 내부와 정보를 주고받습니다.
장치 드라이버는 장치 컨트롤러가 컴퓨터 내부와 정보를 주고받을 수 있게 하는 프로그램입니다.
-
-
-
-
-
💾 [CS] 다양한 보조기억장치
다양한 보조기억장치
보조기억장치에는 다양한 종류가 있습니다.
그중 가장 태중적인 보조기억장치는 하드 디스크와 플래시 메모리입니다.
우리가 흔히 사용하는 USB 메모리, SD 카드, SSD 같은 저장 장치를 말합니다.
하드 디스크(HDD: Hard Disk Drive)
하드 디스크(HDD: Hard Disk Drive) 는 자기적인 방식으로 데이터를 저장하는 보조기억장치입니다.
이 때문에 하드 디스크를 자기 디스크(magnetic disk) 의 일종으로 지칭하기도 합니다.
대용향 저장 장치가 필요한 작업이나 서버실에 자주 출입하는 작업을 한다면 하드 디스크를 자주 접하게 될 겁니다.
하드 디스크의 생김새.
다음 그림이 바로 하드 디스크입니다.
우리가 아는 CD나 옛날 음향 장치는 LP가 떠오를 겁니다.
실제로도 하드 디스크는 CD나 LP와 비슷하게 동작합니다.
동그란 원판에 데이터를 저장하고, 그것을 회전시켜 뾰족한 리더기로 데이터를 읽는 점에서 비슷합니다.
하드 디스크에서 실질적으로 데이터가 저장되는 곳은 아래 그림 속 동그란 원판입니다.
이를 플래터(platter) 라고 합니다.
하드 디스크는 자기적인 방식으로 데이터를 저장합니다.
플래터는 자기 물질로 덮여 있어 수많은 N극과 S극을 저장합니다.
N극과 S극은 0과 1의 역할을 수행합니다.
그 플래터를 회전시키는 구성 요소를 스핀들(spindle) 이라고 합니다.
스핀들이 플래터를 돌리는 속도는 분당 회전수를 나타내는 RPM(Revolution Per Minute) 이라는 단위로 표현됩니다.
가령 RPM이 15,000인 하드 디스크는 1분에 15,000바퀴를 회전하는 하드 디스크입니다.
플래터를 대상으로 데이터를 읽고 쓰는 구성 요소는 헤드(head) 입니다.
헤드는 플래터 위에서 미세하게 떠 있는 채로 데이터를 읽고 쓰는, 마치 바늘같이 생긴 부품입니다.
그리고 헤드는 원하는 위치로 헤드를 이동시키는 디스크 암(disk arm) 에 부착되어 있습니다.
CD나 LP에 비해 하드 디스크는 훨씬 더 많은 양의 데이터를 저장해야 하므로 일반적인 여러 겹의 플래터로 이루어져 있고 플래터 양면을 모두 사용할 수 있습니다.
양면 플래터를 사용하면 위아래로 플러터당 두 개의 헤드가 사용됩니다.
이 때 일반적으로 모든 헤드는 디스크 암에 부착되어 다같이 이동합니다.
데이터가 저장되는 방법.
그럼 이제 플래터에 데이터가 어떻게 저장되는지 알아봅시다.
플래터는 트랙(track) 과 섹터(sector) 라는 단위로 데이터를 저장합니다.
아래 그림터럼 플래터를 여러 동심원으로 나누었을 때 그중 하나의 원을 트랙이라고 부릅니다.
그리고 트랙은 마치 피자처럼 여러 조각으로 나우어지는데, 이 한 조각을 섹터라고 부릅니다.
섹터는 하드 디스크의 가장 작은 전송 단위입니다.
하나의 섹터는 일반적으로 512바이트 정도의 크기를 가지고 있지만, 정확한 크기는 하드 디스크에 따라 차이가 있습니다.
일부 하드 디스크의 섹터 크기는 4,096바이트에 이르기도 합니다.
여러 겹의 플래터가 사용 될 수 있습니다.
이때 여러 겹의 플래터 상에서 같은 트랙이 위치한 곳을 모아 연결한 논리적 단위를 실린더(cyilnder) 라고 부릅니다.
쉽게 말해 한 플래터를 동심원으로 나눈 공간은 트랙, 같은 트랙끼리 연결한 원통 모양의 공간은 실린더입니다.
연속된 정보는 보통 한 실린더에 기록됩니다.
예를 들어 두 개의 플래터를 사용하는 하드 디스크에서 네 개 섹터에 걸쳐 데이터를 저장할 때는 첫 번째 플래터 윗면, 뒷면과 두 번째 플래터 윗면, 뒷면에 데이터를 저장합니다.
연속된 정보를 하나의 실린더에 기록하는 이유는 디스크 암을 움직이지 않고도 바로 데이터에 접근할 수 있기 때문입니다.
데이터에 접근하는 과정
데이터가 하드 디스크의 섹터, 트랙, 실린더에 저장된다는 것을 알았다면 저장된 데이터에 접근하는 과정을 생각해 봅시다.
하드 디스크가 저장된 데이터에 접근하는 시간은 크게 탐색 시간, 회전 지연, 전송 시간 으로 나뉩니다.
탐색 시간(seek time) : 접근하려는 데이터가 저장된 트랙까지 헤드를 이동시키는 시간을 의미합니다.
회전 지연(rotational latency) : 헤드가 있는 곳으로 플래터를 회전시키는 시간을 의미합니다.
전송 시간(transfer time) : 하드 디스크와 컴퓨터 간에 데이터를 전송하는 시간을 의미합니다.
위 시간들은 별것 아닌 것 같아도 성능에 큰 영향을 끼치는 시간입니다.
일례로 구글의 AI를 주도하고 있는 제프 딘은 과거 ‘프로그래머가 꼭 알아야 할 컴퓨터 시간들’을 공개한 바 있는데, 일부를 발췌하면 다음과 같습니다.
물론 2011년에 자료가 공개된 이후 오늘날 하드 디스크 성능은 많이 향상되었지만, 하드 디스크에서 다량의 데이터를 탐색하고 읽어 들이는 시간은 생각보다 어마어마하다는 사실을 쉽게 짐작할 수 있습니다.
탐색 시간과 회전 지연을 단축시키기 위해서는 플래터를 빨리 돌려 RPM을 높이는 것도 중요하지만, 참조 지역성, 즉 접근하려는 데이터가 플래터 혹은 헤드를 조금만 옮겨도 접근할 수 있는 곳에 위치해 있는 것도 중요합니다.
플래시 메모리
하드 디스크는 최근에 많이 사용하는 보조기억장치이지만, 플래시 메모리(flush memory) 기반의 보조기억장치 또한 많이 사용합니다.
우리가 흔히 사용하는 USB 메모리, SD 카드, SSD가 모두 플래시 메모리 가반의 보조기억장치입니다.
플래시 메모리 내부.
다음 그림에서 붉은 박스로 표기한 부분이 플래시 메모리입니다.
플래시 메모리는 전기적으로 데이터를 읽고 쓸 수 있는 반도체 기반의 저장 장치입니다.
사실 플래시 메모리는 보조기억장치 범주에만 속한다기보다는 다양한 곳에서 널리 사용하는 저장 장치로 보는 것이 옳습니다.
주기억장치 중 하나인 ROM에도 사용되고, 우리가 일상적으로 접하는 거의 모든 전자 제품안에 플래시 메모리가 내장되어 있다고 봐도 무방합니다.
두 종류의 플래시 메모리
플래시 메모리에는 크래 NAND 플래시 메모리 와 NOR 플래시 메모리 가 있습니다.
NAND 플래시와 NOR 플래시는 각각 NAND 연산을 수행하는 회로(NAND 게이트)와 NOR 연산을 수행하는 회로(NOR 게이트)를 기반으로 만들어진 메모리를 뜻합니다.
이 둘 중 대용량 저장 장치로 많이 사용되는 플래시 메모리는 NAND 플래시 메모리 입니다.
플래시 메모리에는 셀(cell) 이라는 단위가 있습니다.
셀이란 플래시 메모리에서 데이터를 저장하는 가장 작은 단위입니다.
이 셀이 모이고 모여 MB, GB, TB 용량을 갖는 저장 장치가 되는 것입니다.
이 때 하나의 셀에 몇 비트를 저장할 수 있느냐에 따라 플래시 메모리 종류가 나뉩니다.
한 셀에 1비트를 저장할 수 있는 플래시 메모리를 SLC(Single Level Cell) 타입,
한 셀에 2비트를 저장할 수 있는 플래시 메모리를 MLC(Multiple Level Cell) 타입,
한 셀에 4비트를 저장할 수 있는 플래시 메모리를 TLC(Triple-Level Cell) 타입이라고 합니다.
큰 차이가 아닌 것처럼 보여도 이는 플래시 메모리의 수명, 속도, 가격에 큰 영향을 끼칩니다.
참고로 한 셀에 4비트를 저장할 수 있는 QLC 타입도 있습니다.
플래시 메모리도 수명이 있나요?
플래시 메모리에는 수명이 있습니다.
플래시 메모리 뿐만 아니라 하드 디스크 또한 수명이 있습니다.
우리가 사용하는 USB 메모리, SSD, SD 카드는 수명이 다하면 더 이상 저장 장치로써 사용이 불가능합니다.
종이에 연필로 쓰고 지우개로 지우고를 반복하다 보면 결국 종이가 찢어지는 것처럼 한 셀에 일정 횟수 이상 데이터를 쓰고 지우면
그 셀은 더 이상 데이터를 저장할 수 없기 때문입니다.
SLC, MLC, TCL 타입의 특징과 차이점.
사람 한 명을 비트, 셀을 집에 비유하면 SLC 타입은 한 집에 한 명, MLC 타입은 한 집에 두 명, TLC 타입은 세 명이 사는 구조로 비유할 수 있습니다.
SLC 타입
SLC 타입은 아래 그림과 같이 한 셀로 두 개의 정보를 표현할 수 있습니다.
홀로 거주하는 집에 제약 없이 출입이 가능하듯 SLC 타입은 MLC나 TLC 타입에 비해 비트의 빠른 입출력이 가능합니다.
수명도 MLC나 TLC 타입보다 길어서 수만에서 수십만 번 가까이 데이터를 쓰고 지우고를 반복할 수 있습니다.
하지만 SLC 타입은 용량 대비 가격이 높습니다.
이는 마치 혼자서 살면 감당해야 할 주거 비용이 커지는 것과 같습니다.
그렇기에 보통 기업에서 데이터를 읽고 쓰기가 매우 많이 반복되며 고성능의 빠른 저장 장치가 필요한 경우에 SLC 타입을 사용합니다.
MLC 타입
MLC 타입은 다음 그림과 같이 한 셀로 네 개의 정보를 표현할 수 있습니다.
SLC 타입보다 일반적으로 속도와 수명은 떨어지지만, 한 셀에 두 비트씩 저장할 수 있다는 점에서 MLC 타입은 SLC 타입도다 대용량화하기 유리합니다.
집의 개수가 같다면 한 집에 한 명씩 사는 것보다 한 집에 두 명씩 사는 것이 훨씬 더 많은 사람을 수용할 수 있는 것과 같은 이치입니다.
두 명이 한 집에서 주거 비용을 나눠 내면 혼자 감당해야 하는 주거 비용보다 저렴해지듯 MLC 타입은 SLC 타입보다 용량 대비 가격이 저렴합니다.
시중에서 사용되는 많은 플래시 메모리 저장 장치들이 MLC 타입(혹은 후술할 TLC 타입)으로 만들어집니다.
TLC 타입
한 셀당 3비트씩 저장할 수 있는 TLC 타입은 한 셀로 여덟 개의 정보를 표현할 수 있습니다.
그렇기에 대용화 하기 유리합니다.
일반적으로 SLC나 MLC 타입보다 수명과 속도가 떨어지지만 용량 대비 가격도 저렴합니다.
정리.
정리하면, 같은 용량의 플래시 메모리 저장 장치라고 할지라도 셀의 타입에 따라 수명, 가격, 성능이 다릅니다.
썻다 지우기를 자주 반복해야 하는 경우 혹은 높은 성능을 원하는 경우에는 고가의 SLC 타입을 선택하는 것이 좋고, 저가의 대용량 저장 장치를 원한다면 TLC 타입, 그 중간을 원한다면 MLC 타입의 저장 장치를 선택하는 것이 좋습니다.
플래시 메모리의 셀보다 더 큰 단위.
이제 플래시 메모리의 가장 작은 단위인 셀보다 더 큰 단위를 알아봅시다.
셀들이 모여 만들어진 단위를 페이지(page), 그리고 페이지가 모여 만들어진 단위를 블록(block) 이라고 합니다.
블록이 모여 플레인(plane), 플레인이 모여 다이(die) 가 됩니다.
플레시 메모리에서 읽기와 쓰기는 페이지 단위로 이루어 집니다.
하지만 삭제는 페이지보다 큰 블록 단위로 이루어집니다.
읽기/쓰기 단위와 삭제 단위가 다르다는 것이 플래시 메모리의 가장 큰 특징 중 하나입니다.
페이지의 상태.
페이지는 세 개의 상태를 가질 수 있습니다.
이는 각각 Free, Valid, Invalid 상태입니다.
Free 상태 : 어떠한 데이터도 저장하고 있지 않아 새로운 데이터를 저장할 수 있는 상태.
Valid 상태 : 이미 유효한 데이터를 저장하고 있는 상태.
Invalid 상태 : 쓰레기값이라 부르는 유효하지 않은 데이터를 저장하고 있는 상태.
플래시 메모리는 하드 디스크와는 달리 덮어쓰기가 불가능하여 Vaild 상태인 페이지에는 새 데이터를 저장할 수 없습니다.
플래시 메모리의 간단한 동작 예시.
플래시 메모리의 간단한 동작을 예시로 알아봅시다.
X라는 블록이 네 개의 페이지로 이루어져 있다고 가정해 보겠습니다.
그리고 그중 두 개의 페이지에는 왼쪽 아래와 같이 A와 B라는 데이터가 저장 되어 있다고 합시다.
여기서 블록 X에 새로운 데이터 C를 저장한다면 아래 그림과 같이 저장됩니다.
플래시 메모리의 읽기 쓰기 단위는 페이지이기 때문입니다.
여기서 새롭게 저장된 C와 기존에 저장되어 있던 B는 그대로 둔 채 기존의 A만을 A’로 수정하고 싶다면 플래시 메모리에서 덮어쓰기는 불가능하기 때문에 기존에 저장된 A는 Invalid 상태가 되어 더 이상 값이 유효하지 않은 쓰레기값이 되고, 새로운 A’ 데이터가 저장됩니다.
결과적으로 블록 X의 Valid 페이지는 B, C, A’가 됩니다.
그런데 여기서 문제가 있습니다.
A와 같이 쓰레기 값을 저장하고 있는 공간은 사용하지 않을 공간인데도 불구하고 용량을 차지하고 있습니다.
이는 엄연히 용량 낭비입니다.
그렇다고 A만 지울 수도 없습니다.
앞서 언급했듯이 플래시 메모리에서 삭제는 블록 단위로 수행되기 때문입니다.
그래서 최근 SSD를 비롯한 플래시 메모리는 이런 쓰레기 값을 정리하기 위해 가비지 컬렉션(Garbege Collection) 기능을 제공합니다.
가비지 컬렉션은 1. 유효한 페이지들만을 새로운 블록으로 복사한 뒤, 2. 기존의 블록을 삭제하는 기능입니다.
즉, 블록 X의 모든 유효한 페이지를 새로운 블록 T로 옮기고 블록 X를 삭제하는 것입니다.
키워드로 정리하는 핵심 포인트
하드 디스크 의 구성요소에는 플래터, 스핀들, 헤드, 디스크 암이 있습니다.
플래터는 트랙과 섹터로 나뉘고, 여러 플래터의 동일한 트랙이 모여 실린더를 이룹니다.
하드 디스크의 데이터 접근 시간은 크게 탐색 시간, 회전 지연, 전송 시간으로 나뉩니다.
플래시 메모리는 한 셀에 몇 비트를 저장할 수 있느냐에 따라 SLC, MLC, TLC로 나뉩니다.
플래시 메모리의 읽기과 쓰기는 페이지 단위로, 삭제는 블록 단위로 이루어 집니다.
-
-
💾 [CS] RAID의 정의와 종류
RAID의 정의와 종류.
1TB 하드 디스크 네 개로 RAID를 구성하면 4TB 하드 디스크 한 개의 성능과 안전성을 능가할 수 있습니다.
RAID의 정의.
‘보조기억장치에도 수명이 있습니다.’ 그래서 ‘하드 디스크와 같은 보조기억장치에 어떻게든 저장만 하면 됩니다’ 와 같은 단순한 답변은 다소 부족한 해법입니다.
이럴 때 사용할 수 있는 방법 중 하나가 RAID입니다.
RAID(Redundant Array of Independent Disks) 는 주로 하드 디스크와 SSD를 사용하는 기술로, 데이트의 안정선 혹은 높은 성능을 위해 여러 개의 물리적 보조기억장치를 마치 하나의 논리적 보조기억장치처럼 사용하는 기술을 의미합니다.
RAID의 종류
RAID 구성 방법을 RAID 레벨 이라고 표현합니다.
RAID 레벨에는 대표적으로 RAID 0, RAID 1, RAID 2, RAID 3, RAID 4, RAID 5, RAID 6 이 있고 그로부터 파생된 RAID 10, RAID 50 등이 있습니다.
RAID 0
RAID 0 은 여러 개의 보조기억장치에 데이터를 잔순히 나누어 저장하는 구성 방식입니다.
가령 1TB 하드 디스크 네 개로 RAID 0을 구성했다고 가정해 봅시다.
이제 어떠한 데이터를 저장할 때 각 하드 디스크는 아래와 같이 번갈아 가며 데이터를 저장합니다,
즉, 저장되는 데이터가 하드 디스크 개수만큼 나위어 자장되는 것입니다.
이때 마치 줄무늬처럼 분산되어 저장된 데이터를 스트라입(Stripe) 이라 하고, 분산하여 저장하는 것을 스트라이핑(Striping) 이라고 합니다.
위와 같이 데이터가 분산되어 저장되면, 다시 말해 스트라이핑되면 저장된 데이터를 읽고 쓰는 속도가 빨라집니다.
하나의 대용량 저장 장치를 이용했더라면 여러 번에 걸쳐 일고 썻을 데이터를 동시에 읽고 쓸 수 있기 때문입니다.
그렇기에 4TB 저장 장치 한 개를 읽고 쓰는 속도보다 RAID 0로 구성된 1TB 저장 장치 네 개의 속도가 이론상 네 배가량 빠릅니다.
RAID 0의 단점
RAID 0에는 단점이 있습니다.
저장된 정보가 안전하지 않습니다.
RAID 0으로 구성된 하드 디스크 중 하나에 문제가 생긴다면 다른 모든 하드 디스크의 정보를 읽는 데 문제가 생길 수 있습니다.
그래서 등장한 것이 RAID 1 입니다.
RAID 1
RAID 1 은 복사본을 만드는 방식입니다.
마치 거울처럼 완전한 복사본을 만드는 구성이기에 미러링(mirroring) 이라고도 부릅니다.
아래 그림은 네 개의 하드 디스크를 RAID 1으로 구성한 모습입니다.
RAID 0처럼 데이터 스트라이핑이 사용되긴 했지만, 오른쪽의 두 하드 디스크는 마치 거울처럼 왼쪽의 두 하드 디스크와 동일한 내용을 저장하고 있습니다.
이처럼 RAID 1에 어떠한 데이터를 쓸 때는 원본과 복사본 두 군데에 씁니다.
그렇기에 쓰기 속도는 RAID 0보다 느립니다.
RAID 1 방식은 복구가 매우 간단하다는 장점이 있습니다.
똑같은 디스크가 두 개 있는 셈이니, 하나에 문제가 발생해도 잃어버린 정보를 금방 되찾을 수 있기 때문입니다.
RAID 1의 단점
RAID 1은 하드 디스크 개수가 한정되었을 때 사용 가능한 용량이 적어지는 단점이 있습니다.
위 그림만 보아도 RAID 0 구성은 4TB의 정보를 저장할 수 있는 반면, RAID 1에서는 2TB의 정보만 저장할 수 있습니다.
즉, RAID 1에서는 복사본이 만들어지는 용량만큼 사용자가 사용하지 못합니다.
결국 많은 양의 하드 디스크가 필요하게 되고, 비용이 증가한다는 단점으로 이어집니다.
RAID 4
RAID 4는 RAID 1처럼 완전한 복사본을 만드는 대신 오류를 검출하고 복구하기 위한 정보를 저장한 장치를 두는 구성 방식입니다.
이때 ‘오류를 검출하고 복구하기 위한 정보’를 패리티 비트(parity bit) 라고 합니다.
RAID 4에서는 패리티를 저장한 장치를 이용해 다른 장치들의 오류를 검출하고, 오류가 있다면 복구합니다.
이로써 RAID 4는 RAID 1보다 적은 하드 디스크로도 데이터를 안전하게 보관할 수 있습니다.
오류를 검출하는 패리트 비트
원래 패리트 비트는 오류 검출만 가능할 뿐 오류 복구는 불가능합니다.
하지만 RAID에서는 패리트 값으로 오류 수정도 가능합니다.
다만 구체적인 방법인 패리티 계산법은 다루지 않을 예정입니다.
여기서 다음 두 가지만 기억하면 됩니다.
RAID 4에서는 패리티 정보를 저장한 장치로서 나머지 장치들의 오루를 검출.복구한다.
패리티 비트는 본래 오류 검출용 정보지만, RAID에서는 오류 복구도 가능하다.
RAID 5
RAID 4에서는 어떤 새로운 데이터가 저장될 때마다 패리티를 저장하는 디스크에도 데이터를 쓰게 되므로 패리티를 저장하는 장치에 병목 현상이 발생한다는 문제가 있습니다.
RAID 5는 아래 그림처럼 패리티 정보를 분산하여 저장하는 방식으로 RAID 4의 문제인 병목 현상을 해소합니다.
RAID 6
RAID 6 의 구성은 기본적으로 RAID 5와 같으나, 다음 그림과 같이 서로 다른 두 개의 패리티를 두는 방식입니다.
이는 오류를 검출하고 복구할 수 있는 수단이 두 개가 생긴 셈입니다.
따라서 RAID 6은 RAID 4나 RAID 5보다 안전한 구성이라 볼 수 있습니다.
다만 새로운 정보를 저장할 때마다 함께 저장할 패리티가 두 개이므로, 쓰기 속도는 RAID 5보다 느립니다.
따라서 RAID 6은 데이터 저장 속도를 조금 희생하더라도 데이터를 더욱 안전하게 보관하고 싶을 때 사용하는 방식입니다.
정리
이 외에도 RAID 0과 RAID 1을 혼합한 RAID 10 방식도 있고, RAID 0과 RAID 5를 혼합한 RAID 5방식도 있습니다.
note: 이렇게 여러 RAID 레벨을 혼합한 방식을 Nested RAID 라고 합니다.
각 RAID 레벨마다 장단점이 있으므로 어떤 상황에서 무엇을 최우선으로 원하는지에 따라 최적의 RAID 레벨은 달라질 수 있습니다.
그렇기에 각 RAID 레벨의 대략적인 구성과 특징을 아는것이 중요합니다.
키워드로 정리하는 핵심 포인트
RAID란 데이터의 안전성 혹은 높은 성능을 위해 여러 하드 디스크나 SSD를 마치 하나의 장치저럼 사용하는 기술입니다.
RAID 0은 데이터를 단순히 병렬로 분산하여 저장하고, RAID 1은 완전한 복사본을 만듭니다.
RAID 4는 패리티를 저장한 장치를 따로 두는 방식이고, RAID 5는 패리티를 분산하여 저장하는 방식입니다.
RAID 6은 서로 다른 두 개의 패리티를 두는 방식입니다.
-
-
-
-
📝[blog post] 연습 문제 풀이 정리(2)
1️⃣ 수열과 재귀.
연습 문제를 풀다보니 수열과 재귀에 대해 많은 수학적 사고력이 필요하겠다는 생각이 들었습니다.
수열 : 수학에서 수의 나열을 의미합니다.
즉, 어떤 규착에 따라 나열된 수들의 집합을 말합니다.
수열은 각 수를 나타내는 일련의 할(terms)으로 구성되며, 각 항은 특정 위치(index)를 가집니다.
수열의 예로는 다음과 같은 것들이 있습니다.
등차수열: 각 항이 일정한 값만큼 증가하거나 감소하는 수열(예: 2, 5, 8, 11…)(각 항이 3씩 증가)
등비수열: 각 항이 일정한 비율로 증가하거나 감소하는 수열(예: 3, 9, 27, 81…)(각 항이 이전 항의 3배)
피보나치 수열: 첫 두 항이 0과 1이고, 그 이후의 각 항이 바로 앞 두항의 합인 수열(예: 0, 1, 1, 2, 3, 5, 8….)
수열은 다양한 수학적 문제를 해결하는 데 사용되며, 특히 함수, 극한, 미적분 등의 주제와 밀접한 관련이 있습니다.
재귀 : 프로그래밍과 수학에서 사용되는 개념으로, 어떤 함수나 알고리즘이 자기 자신을 호출하는 방식울 말합니다.
재귀를 통해 복잡한 문제를 더 작은 하위 문제로 나누어 해결할 수 있습니다.
재귀 함수는 기본적으로 두 가지 부분으로 구성됩니다.
1. 기저 조건(Base Case) : 재귀 호출이 더 이상 필요하지 않은 경우를 정의합니다. 기저 조건이 충족되면 함수는 더 이상 자기 자신을 호출하지 않고 종료됩니다.
2. 재귀 호출(Recursive Call) : 함수가 자기 자신을 호출하여 문제를 더 작은 부분으로 나누어 해결하려고 시도합니다.
재귀는 문제를 단순하고 직관적으로 표현할 수 있는 강력한 도구이지만, 재귀 호출이 과도하면 스택 오버플로(stack overflow)가 발생할 수 있으므로 주의가 필요합니다.
따라서 재귀를 사용할 때는 기저 조건을 잘 정의하고, 필요할 경우 반복(iteration)으로 문제를 해결하는 방법도 고려해야 합니다.
-
-
📝[blog post] 연습 문제 풀이 정리(1)
1️⃣ 이중 for 문.
이중 for 문은 for 문을 중첩해서 사용하는 것을 말합니다.
한 for 문 안에 또 다른 for 문 안에 또 다른 for 문이 들어있는 구조로, 주로 2차원 배열이나 리스트, 행렬을 처리할 때 사용됩니다.
1.1 기본 구조.
for (초기화1; 조건1; 증감1) {
for (초기화2; 조건2; 증감2) {
// 코드 블록
}
}
1.2 예시
예를 들어, 2차원 리스트의 모든 요소를 출력하는 경우를 생각해 봅시다.
public class Main {
public static void main(Stringp[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int[] row : matrix) {
for (int element : row) {
System.out.println(element);
}
}
}
}
위 코드에서 ‘matrix’ 는 2차원 리스트입니다.
첫 번째 for 문은 ‘matrix’ 의 각 행(row)을 순회하고, 두 번째 for 문은 각행의 요소(element)를 순회합니다.
출력 결과는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
2️⃣ 규칙성을 찾는 것이 중요!
어떤 문제를 마주치면 규칙성을 찾는 것이 중요한 것 같습니다.
연습 문제 2-1 중 ‘정수형 숫자를 로마 숫자 표기로 변환하는 프로그램’ 을 작성하는 문제에서 그것을 깨달았습니다.
먼저 어떤 규칙성이 있는지 찾아낸 후 그 규칙성에 따라 문제를 풀고, 문제를 컴퓨터적 사고력을 이용하여 코딩을 하니 문제가 풀리는 것을 알게 되었습니다.
3️⃣ 인덱스를 자유자재로 가지고 놀 줄 알아야 합니다!
연습 문제를 풀면서 느낀 점 중 하나가 “인덱스를 자유자재로 가지고 놀 줄 알아야 한다” 는 부분이었습니다.
“인덱스를 자유자재로 가지고 논다” 라는 말은 문자열이 주어지면 인덱스를 활용하여 문자를 삽입, 삭제, 추출, 변환 등을 자유롭게 할 줄 알아야 한다는 의미입니다.
연습 문제 중 문자열에 대한 문제는 이 부분이 가장 중요시되는 것 같았습니다.
-
-
-
-
-
📝[blog post] 프론트엔드와 백엔드는 무엇이 다를까?(+내가 백엔드 개발자가 되고 싶은 이유)
1️⃣ 프론트엔드와 백엔드?
처음 이 글의 여정을 함께하기에 앞서 프론트엔트가 무엇인가 백엔드가 무엇인지 알아야 할 것 같아요!
제가 아무것도 모르는 당시 저 두 단어 “프론트엔드”, “백엔드”를 듣고 느낀 것은
“프론트엔드”는 뭔가 프론트 데스크 같이 앞에서 누군가가 나를 반겨주는 느낌이였고, “백엔드”는 뒤쪽에서 나를 받쳐주는 든든한 느낌이랄까? 😆
그저 느낌으로는 알쏭달쏭하니 정확한 의미를 알아보는 여행을 떠나봅시다! 🙋♂️
2️⃣ 프론트엔드.
프론트엔드는 웹사이트에서 우리가 볼 수 있는 모든 것들을 만드는 일을 말해요 😆
예를 들어, 컴퓨터나 핸드폰으로 책을 보거나 게임을 할 때, 그 화면에 보이는 모든 것들이 바로 프론트엔드에서 만들어진 거예요.(존경합니다 프론트엔드 개발자님들🙇♂️)
이렇게 생각해 볼까요?
웹사이트를 마치 컬러링북처럼 생각한다면, 프론트엔드 개발자는 그림을 그리고 색칠하는 사람이에요 🧑🎨
프론트엔드 개발자들은 화면에 나타날 모양이나 색상을 정하고, 어디를 누르면 어떤일이 일어날지도 결정합니다.
예를 들어, ‘스타드’ 버튼을 누르면 게임이 시작되거나, 사진을 클릭하면 커지는 것처럼 말이에요.
즉, 프론트엔드는 우리가 웹사이트에서 보고 만지는 모든 것을 아름답고 재미있게 만들어 주는 중요한 일을 한답니다!
3️⃣ 백엔드.
백엔드는 웹사이트에서 우리가 눈에 보이지 않는 부분을 다루는 일을 해요.(그렇다고 뭐.. 해커 이런건 아닙니다.. 완전히 달라요…)
이것은 마치 마술사가 무대 뒤에서 마술을 준비하는 것과 비슷해요! 🪄
우리가 볼 수는 없지만, 마술이 멋기제 보이도록 도와주죠.
예를 들어, 우리가 컴퓨터로 쇼핑을 할 때, 옷이나 장난감을 고르고 주문 버튼을 눌러요. 이떄 백엔드는 주문한 것이 무엇인지 기억하고, 그 물건을 어디로 보내야 할지 알려줘요.
또한, 우리가 어떤 게임을 하거나 질문을 할 때도, 백엔드는 그 대답을 찾아서 화면에 보여주죠.
백엔드는 컴퓨터와 데이터베이스라는 큰 저장소를 사용해서, 우리가 웹사이트에서 필요한 모든 정보를 처리하고 저장하는 곳이에요.
우리가 보지 못하지만, 웹사이트가 잘 작동하도록 도와주는 매우 중요한 부분이랍니다!
4️⃣ 내가 백엔드 개발자가 되고 싶은 이유.
저는 어렸을 때 레고를 참 좋아했어요 :)
그 중에서도 테크닉 레고를 가장 좋아했었어요 :)
그 이유는 완성된 것을 보는 것도 좋았지만 조립해 나가면서 그 안에 중심이 되는 코어, 즉 움직임의 중앙부를 제가 직접 조립하고 움직임이 어디서부터 시작되는지를 직접 이해하는 것이 너무 재미있었거든요.
자동차 레고를 만들다보면 직접 엔진를 만들게 됩니다.
그러면 진짜 엔진이 어떻게 움직이고 이 엔진이 어떻게 동작하느냐에 따라 자동차의 다른 부품들이 맞물려 하나씩 동작하는지 상상되는게 너무 행복했었어요.
이런것들이 어렸을 때부터 너무 좋았답니다.
그리고나서 조금 커서는 루어 낚시를 좋아하게 되었어요.
이 루어 낚시는 “배스” 라는 어종을 대상으로 하는 낚시인데, 이 어종에 대한 여러가지 공부를 해야 했었어요.
먼저, 이 어종이 온도에 민감해 온도에 따라 공격 패턴이 달라요 그래서 그 패턴에 대한 데이터를 수집해야 했었어요.
두 번째, 이 어종은 수중 구조물에 굉장히 예민해요. 자신이 좋아하는 수중 구조물이 따로 있어서 그 수중 구조물을 따로 탐색하고 이해하는 법을 배워야 했었어요.
세 번째, 날씨에 영향을 많이 받는 어종이에요. 햇빛과 그늘 그리고 비가 오는 날과 안오는 날에 따라 먹이 사냥 패턴이 달라져요. 그에 따른 루어 선택과 패턴을 다르게 골라야 합니다.
네 번째, 피딩 타임이라는 이 어종의 먹이 사냥 시간이 있습니다. 이 시간에 따라 어종의 먹이 사냥 패턴이 매우 다양해요.
마지막, 계절에 따라 이 어종이 물 속이 바닥, 중층 또는 상층에 머무는지 이런 데이터가 달라요.
이렇게 이 어종을 낚기 위해서는 수 많은 변수와 데이터들을 조합하여 적절한 위치에 적합한 루어를 선택하여 공격 패턴에 맞는 액션을 주어야 배스가 물어 줍니다.
그럴때 “아 나의 데이터가 맞았구나!” 하는 희열감과 아드레날린 그리고 도파민이 폭발해버리죠.
이런 특성이 저는 백엔드에서도 비슷하게 적용되는 것 같아요.
레고는 백엔드에서의 중심 동작을 알아가는 과정과 직접 동작하는 로직을 만드는 부분에서의 즐거움을 찾아가는 과정에서 재미를 느끼고,
낚시는 백엔드에서 데이터를 찾고 뽑아내어 가공하고 내어주는 부분에서 희열을 느끼는 것 같습니다.
그래서 저의 적성과 맞는 것 같아요.
저는 이러한 부분에서 백엔드 개발자가 제가 즐길 수 있는 부분이 서로 맞기 때문에 백엔드 개발자가 되고 싶습니다 😆
-
-
-
-
-
-
💾 [CS] 메모리의 주소 공간
메모리의 주소 공간.
주소에는 물리 주소와 논리 주소가 있다. 이번 절에서는 이 두 주소의 개념과 차이, 그리고 두 주소 간의 변환 방법을 학습한다.
1. 주소의 종류.
지금까지 ‘메모리에 저장된 정보의 위치는 주소로 나타낼 수 있다’ 정도로만 설명했지만, 사실 주소에는 두 종류가 있습니다.
1. 물리주소 : 메모리 하드웨어가 사용하는 주소.
2. 논리주소 : CPU와 실행 중인 프로그램이 사용하는 주소.
2. 물리 주소와 논리 주소.
CPU와 실행 중인 프로그램은 현재 메모리 몇 번지에 무엇이 저장되어 있는지 다 알고 있지 않습니다.
그 이유는 메모리에 저장된 정보는 시시각각 변하기 때문입니다.
메모리에는 새롭게 실행되는 프로그램이 시시때때로 적재되고, 실행이 끝난 프로그램은 삭제됩니다.
게다가, 같은 프로그램을 실행하더라도 실행할 때마다 적재되는 주소가 달라질 수 있습니다.
예를 들어, 1500번지에 적재되었던 프로그램을 다시 실행하면 3000번지, 또 다시 실행하면 2700번지에 적재될 수 있습니다.
그렇다면 CPU와 실행 중인 프로그램이 이해하는 주소는 무엇일까요?
주소에는 메모리가 사용하는 물리 주소가 있고, CPU와 실행 중인 프로그램이 사용하는 논리 주소가 있습니다.
물리 주소(Physical address) : 정보가 실제로 저장된 하드웨어상의 주소를 의미.
논리 주소(logical address) : CPU와 실행 중인 프로그램이 사용하는 주소, 실행 중인 프로그램 각각에게 부여된 0번지부터 시작되는 주소를 의미함.
예를 들어 현재 메모리에 메모장, 게임, 인터넷 브라우저 프로그램이 적재되어 있다고 가정해 보겠습니다.
메모장, 게임, 인터넷 브라우저 프로그램은 현재 다른 프로그램들이 메모리 몇 번지에 저장되어 있는지,
다시 말해 다른 프로그램들의 물리 주소가 무엇인지 굳이 알 필요가 없습니다.
새로운 프로그램이 언제든 적재될 수 있고, 실행되지 않은 프로그램은 언제든 메모리에서 사라질 수 있기 때문입니다.
그래서 메모장, 게임, 인터넷 브라우저는 모두 물리 주소가 아닌 0번지부터 시작하는 자신만을 위한 주소인 논리 주소를 가지고 있습니다.
예를 들어, ‘10번지’라는 주소는 메모장에도, 게임에도, 인터넷 브라우저에도 논리 주소로써 존재할 수 있습니다.
프로그램마다 같은 논리 주소가 얼마든지 있을 수 있다는 뜻입니다.
그리고 CPU는 이 논리 주소를 받아들이고, 해석하고, 연산합니다.
정리하면, 메모리가 사용하는 주소는 하드웨어상의 실제 주소인 물리 주소이고, CPU와 실행 중인 프로그램이 사용하는 주소는 각각의 프로그램에 부여된 논리 주소입니다.
그런데 CPU가 이해하는 주소가 논리 주소라고는 해도 CPU가 메모리와 상호작용하려면 논리 주소와 물리 주소 간의 변환이 이루어져야 합니다.
논리 주소와 물리 주소 간에 어떠한 변환도 이루어지지 않는다면 CPU와 메모리는 서로 이해할 수 없는 주소 체계를 가지고 각자 다른 이야기만 할 뿐 결코 상호작용할 수 없을 테니까요.
그렇다면 논리 주소는 어떻게 물리 주소로 변환될까요?
논리 주소와 물리 주소 간의 변환은 CPU와 주소 버스 사이에 위치한 메모리 관리 장치(MMU: Memory Management Unit) 라는 하드웨어에 의해 수행됩니다.
MMU는 CPU가 발생시킨 논리 주소에 베이스 레지스터 값을 더하여 논리 주소를 물리 주소로 변환합니다.
예를 들어 현재 베이스 레지스터에 15000이 저장되어 있고 CPU가 발생시킨 논리 주소가 100번지라면 이 논리 주소는 아래 그림처럼 물리 주소 15100번지(100+15000)로 변환됩니다.
물리 주소 15000번지부터 적재된 프로그램 A의 논리 주소 100번지에는 이렇게 접근이 가능한 것 입니다.
베이스 레지스터는 프로그램의 가장 작은 물리 주소, 즉 프로그램의 첫 물리 주소를 저장하는 셈이고,
논리 주소는 프로그램의 시작점으로부터 떨어진 거리인 셈입니다.
3. 메모리 보호 기법.
메모장 프로그램의 물리 주소가 1000번지부터 1999번지, 인터넷 브라우저 프로그램의 물리 주소가 2000번지부터 2999번지, 게임 프로그램의 물리 주소가 3000번지부터 3999번지라고 가정해 보겠습니다.
만약 메모장 프로그램 명령어 중 ‘(논리 주소) 1500번지에 숫자 100을 저장하라’와 같은 명령어가 있다면 숫자 100은 어떤 물리 주소에 저장될까요? 이 명령어는 실행되어도 안전할까요?
혹은 인터넷 브라우저 프로그램 명령어 중 ‘(논리 주소) 1100번지의 데이터를 삭제하라’와 같은 명령어가 있다면 어떤 물리 주소의 데이터가 삭제될까요? 이 명령어는 실행되어도 안전할까요?
위와 같은 명령어들은 실행되어서는 안 됩니다.
프로그램의 논리 주소 영역을 벗어났기 때문입니다.
위 명령어들이 실행된다면 메모장 프로그램 명령어는 애꿏은 인터넷 브라우저 프로그램에 숫자 10을 저장하고, 인터넷 브라우저 프로그램 명령어는 자신과는 전혀 관련 없는 게임 프로그램 정보를 삭제합니다.
이렇게 다른 프로그램의 영역을 침범할 수 있는 명령어는 위험하기 때문에 논리 주소 범위를 벗어나는 명령어 실행을 방지하고 실행 중인 프로그램이 다른 프로그램에 영향을 받지 않도록 보호할 방법이 핑요합니다.
이는 한계 레지스터(limit register) 라는 레지스터가 담당합니다.
베이스 레지스터가 실행 중인 프로그램의 가장 작은 물리 주소를 저장한다면, 한계 레지스터는 논리 주소의 최대 크기를 저장합니다.
즉, 프로그램의 물리 주소 범위는 베이스 레지스터 값 이상, 베이스 레지스터 값 + 한계 레지스터 값 미만이 됩니다.
CPU가 접근하려는 논리 주소는 한계 레지스터가 저장한 값보다 커서는 안 됩니다.
한계 레지스터보다 높은 주소 값에 접근하는 것은 곧 프로그램의 범위에 벗어난 메모리 공간에 접근하는 것과 같디 때문입니다.
베이스 레지스터에 100, 한계 레지스터에 150이 저장되어 있다고 해 봅시다.
이는 물리 주소 시작점이 100번지, 프로그램의 크기(논리 주소의 최대 크기)는 150임을 의미합니다.
따라서 이 프로그램은 150번지를 넘어서는 논리 주소를 가질 수 없습니다.
이번에는 베이스 레지스터에 1500, 한계 레지스터에 1000이 저장되어 있다고 해 봅시다.
이는 물리주소 시작점이 1500번지, 프로그램 크기는 1000임을 의미합니다.
따라서 이 프로그램은 1000번지를 넘어서는 논리 주소를 가질 수 없습니다.
CPU는 메모리에 접근하기 전에 접근하고자 하는 논리 주소가 한계 레지스터보다 작은지를 항상 검사합니다.
만약 CPU가 한계 레지스터보다 높은 논리 주소에 접근하려고 하면 인터럽트(트랩)를 발생시켜 실행을 중단합니다.
이러한 방식으로 실행 중인 프로그램의 독립적인 실행 공간을 확보하고 하나의 프로그램이 다른 프로그램을 침범하지 못하게 보호할 수 있습니다.
5. 키워드로 정리하는 핵심 키워드
물리 주소는 메모리 하드웨어상의 주소이고, 논리 주소는 CPU와 실행 중인 프로그램이 사용하는 주소입니다.
MMU는 논리 주소를 물리 주소로 변환합니다.
베이스 레지스터는 프로그램의 첫 물리 주소를 저장합니다.
한계 레지스터는 실행 중인 프로그램의 논리 주소의 최대 크기를 저장합니다.
컴퓨터 시스템에서 “물리 주소(Physical Address)”와 “논리 주소(Logical Address)”는 메모리 관리의 중요한 개념입니다.
각각은 다음과 같은 의미를 가지며, 시스템의 효율적인 메모리 관리를 위해 사용됩니다.
1.1 논리 주소(Logical Address)
정의 : 논리 주소는 프로그램이 사용하는 주소입니다.
이 주소는 프로그램이 실행되면서 생성되는 주소로, 사용자 또는 프로그램이 접근할 수 있는 주소입니다.
이 주소는 가상 메모리 주소라고도 하며, 실제 메모리의 물리적 위치와는 독립적입니다.
목적 : 논리 주소의 주요 목적은 각 프로세스가 독립된 주소 공간을 갖게 하여, 프로세스간의 메모리 충돌을 방지하고 보안을 강화하는 데 있습니다.
또한, 프로그래밍을 단순화시키고 메모리 관리를 더 유연하게 만듭니다.
1.2 물리 주소(Physical Address)
정의 : 물리 주소는 메모리 장치 내의 실제 위치를 가리키는 주소입니다.
이 주소는 시스템의 메모리 관리 유닛(Memory Management Unit, MMU)에 의해 사용되며, 실제 RAM에서 데이터를 찾는 데 사용됩니다.
목적 : 물리 주소는 시스템의 메모리를 효율적으로 할당하고 관리하는 데 필요합니다.
이를 통해 시스템은 실제 메모리 공간을 최적화하고, 필요한 데이터와 프로그램을 정확한 위치에서 처리할 수 있습니다.
1.3 주소 변환(Address Translation)
논리 주소에서 물리 주소로의 변환은 주로 메모리 관리 유닛(MMU)에 의해 수행됩니다.
이 과정은 다음과 같은 방법으로 이루어 집니다.
1. 페이지 테이블 : 운영체제는 페이지 테이블을 사용하여 논리 주소를 물리 주소로 매핑합니다.
페이지 테이블을 논리 주소를 페이지로 나누고, 각 페이지가 실제 메모리의 어느 부분에 해당하는지를 나타내는 테이블입니다.
2. 변환 조회 버퍼(TLB) : 변환 조회 버퍼는 자주 사용되는 주소 매핑을 캐시하는 작은 메모리로, 주소 변환 과정을 빠르게 만듭니다.
3. 주소 변환 과정
프로세스가 논리 주소를 생성합니다.
MMU는 논리 주소의 페이지 번호를 확인하고, 해당 페이지 번호가 페이지 테이블에 있는지 확인합니다.
페이지 테이블에서 해당 페이지의 물리 주소를 찾아 매핑합니다.
물리 주소를 사용하여 실제 메모리에서 데이터를 엑세스합니다.
📝 정리
이러한 주소 변환 메커니즘은 메모리 보호, 프로세스 격리, 메모리 사용의 효율성 증가 등을 가능하게 하며, 복잡한 현대의 멀티태스킹 환경에서 중요한 역할을 합니다.
Q1. 물리 주소(Physical Address)’와 ‘논리 주소(Logical Address)’에 대해 설명해 주시겠습니까? 이 두 주소의 개념과 차이점을 구체적으로 말씀해 주시고, 어떻게 논리 주소가 물리 주소로 변환되는지 그 과정에 대해서도 설명해 주세요.
논리 주소는 프로그램이 사용하는 주소로, 프로그램 코드에 의해 참조되는 주소입니다. 이는 운영체제에 의해 관리되며, 프로그램이 메모리에 로드되는 위치와 무관하게 일관성을 유지합니다. 즉, 프로그램이 메모리의 어느 위치에 로드되든지 간에 같은 논리 주소를 사용할 수 있습니다. 논리 주소는 가상 메모리 주소라고도 하며, 이를 통해 개발자는 실제 메모리 구조를 신경 쓰지 않고 프로그래밍할 수 있습니다.
물리 주소는 메모리 장치 내의 실제 물리적 위치를 가리킵니다. 즉, 물리 주소는 RAM 내의 실제 데이터나 명령어가 저장된 위치를 나타내며, 메모리 관리 유닛(MMU)에 의해 논리 주소로부터 변환됩니다.
논리 주소에서 물리 주소로의 변환은 주로 메모리 관리 유닛(MMU)을 통해 이루어집니다. 이 과정은 다음과 같습니다:
프로세스가 생성하는 논리 주소는 페이지 번호와 오프셋으로 구성됩니다.
페이지 번호는 페이지 테이블을 참조하여 해당 페이지가 메모리의 어느 물리적 위치에 있는지 결정합니다. 이 페이지 테이블은 운영 체제에 의해 관리되며, 각 페이지의 물리 주소를 저장합니다.
물리 주소는 결정된 페이지 시작 주소에 오프셋을 추가하여 최종적으로 결정됩니다.
변환 조회 버퍼(TLB)는 이러한 변환 과정을 가속화하기 위해 자주 사용되는 주소 변환을 캐시합니다.
이러한 변환 과정을 통해 시스템은 효율적으로 메모리를 관리하며, 프로세스 간 메모리 격리와 보안을 유지할 수 있습니다.
-
-
-
-
-
☕️[Java] Math, Random 클래스
Math, Random 클래스
Math 클래스
Math는 수 많은 수학 문제를 해결해주는 클래스입니다.
너무 많은 기능을 제공하기 때문에 대략 이런 것이 있구나 하는 정도면 충분합니다.
실제 필요할 때 검색하거나 API 문서를 찾아봅시다.
1. 기본 연산 메서드
abs(x) : 절대값
max(a, b) : 최대값
min(a, b) : 최소값
2. 지수 및 로그 연산 메서드
exp(x) : e^x 계산
log(x) : 자연 로그
log10(x) : 로그 10
pow(a, b) : a의 b 제곱
3. 반올림 및 정밀도 메서드
ceil(x) : 올림
floor(x) : 내림
rint(x) : 가장 가까운 정수로 반올림
round(x) : 반올림
4. 삼각 함수 메서드
sin(x) : 사인
cos(x) : 코사인
tan(x) : 탄젠트
5. 기타 유용한 메서드
sqrt(x) : 제곱근
cbrt(x) : 세제곱근
random() : 0.0과 1.0 사이의 무작위 값 생성
Math에서 자주 사용하는 기능들을 예제로 만들어서 실행해봅시다.
package lang.math;
public class MathMain {
public static void main(String[] args) {
System.out.println("max(10, 20): " + Math.max(10, 20)); // 최대값
System.out.println("min(10, 20): " + Math.min(10, 20)); // 최소값
System.out.println("abs(-10): " + Math.abs(-10)); // 절대값
// 반올림 및 정밀도 메서드
System.out.println("ceil(2.1): " + Math.ceil(2.1)); // 올림
System.out.println("floor(2.1): " + Math.floor(2.1)); // 내림
System.out.println("round(2.5): " + Math.round(2.5)); // 반올림
// 기타 유용한 메서드
System.out.println("sqrt(4): " + Math.sqrt(4)); // 제곱근
System.out.println("random(): " + Math.random()); // 0.0 ~ 1.0 사이의 double 값을 반환
}
}
실행 결과
max(10, 20): 20
min(10, 20): 10
abs(-10): 10
ceil(2.1): 3.0
floor(2.1): 2.0
round(2.5): 3
sqrt(4): 2.0
random(): 0.45992290098234856
참고 : 아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해봅시다.
Random 클래스
랜덤의 경우 Math.random()을 사용해도 되지만 Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있습니다.
참고로 Math.random()도 내부에서는 Random 클래스를 사용합니다.
참고로 Random 클래스는 java.util 패키지 소속입니다.
package lang.math;
import java.util.Random;
public class RandomMain {
public static void main(String[] args) {
Random random = new Random();
int randomInt = random.nextInt();
System.out.println("randomIntL " + randomInt);
double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d 사이값이 출력됨
System.out.println("randomDouble: " + randomDouble);
boolean randomBoolean = random.nextBoolean();
System.out.println("randomBoolean: " + randomBoolean);
// 범위 조회
int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력
System.out.println("0 ~ 9: " + randomRange1);
int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력
System.out.println("1 ~ 10: " + randomRange2);
}
}
실행 결과
실행 결과는 항상 다르다.
randomIntL -1662566800
randomDouble: 0.14362030528813963
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 10
random.nextInt(): 랜덤 int 값을 반환합니다.
nextDouble() : 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환합니다.
nextBoolean() : 랜덤 boolean 값을 반환합니다.
nextInt(int bound): 0 ~ bound 미만의 숫자를 랜덤으로 반환합니다. 예를 들어서 3을 입력하면 0, 1, 2를 반환합니다.
1부터 특정 숫자의 int 범위를 구하는 경우 nextInt(int bound)의 결과에 +1을 하면 됩니다.
씨드 - Seed
랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구합니다.
그런데 이 씨드 값이 같으면 항상 같은 결과가 출력됩니다.
// Random random = new Random();
Random random = new Random(1); // seed가 같으면 Random의 결과가 같다.
실행 결과
Seed가 같으면 실행 결과는 반복 실행해도 같습니다.
randomIntL -1155869325
randomDouble: 0.10047321632624884
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 5
new Random(): 생성자를 비워두면 내부에서 System.nanoTime()에 여러가지 복잡한 알고리즘을 섞어서 씨드값을 생성합니다.
따라서 반복 실행해도 결과가 항상 달라집니다.
new Random(int seed): 생성자에 씨드 값을 직접 전달할 수 있습니다. 씨드 값이 같으면 여러번 반복 실행해도 실행 결과가 같습니다.
이렇게 씨드 값을 직접 사용하면 결과 값이 항상 같기 때문에 결과가 달라지는 랜덤 값을 구할 수 없습니다.
하지만 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있습니다.
참고로 마인크래프트 같은 게임은 게임을 시작할 때 지형을 랜덤으로 생성하는데, 같은 씨드값을 설정하면 같은 지형을 생성할 수 있습니다.
-
💾 [CS] CISC와 RISC
CISC와 RISC.
명령어 파이프라이닝과 슈퍼스칼라 기법을 실제로 CPU에 적용하려면 명령어가 파이프라이닝에 최적화되어 있어야 합니다.
쉽게 말해 CPU가 파이프라이닝과 슈퍼스칼라 기법을 효과적으로 사용하려면 CPU가 인출하고 해석하고 실행하는 명령어가 파이프라이닝 하기 쉽게 생겨야 합니다.
‘파이프라이닝 하기 쉬운 명령어’란 무엇일까요?
명령어가 어떻게 생겨야 파이프라이닝에 유리할까요?
이와 관련해 CPU의 언어인 ISA와 각기 다른 성격의 ISA를 기반으로 설계된 CISC와 RISC를 알아봅시다.
명령어 집합
세상에는 수많은 CPU 제조사들이 있고, CPU마다 규격과 기능 만듦새가 다 다릅니다.
그러므로 CPU가 이해하고 실행하는 명령어들이 다 똑같지 않습니다.
물론 명령어의 기본적인 구조와 작동원리는 큰 틀에서 크게 벗어나지 않습니다.
그러나 명령어의 세세한 생김새, 명령어로 할 수 있는 연산, 주소 지정 방식 등은 CPU마다 조금씩 차이가 있습니다.
명령어 집합(instruction set) 또는 명령어 집합 구조(ISA: Instruction Set Architecture) : CPU가 이해할 수 있는 명령어들의 모음.
CPU마다 ISA가 다를 수 있습니다.
명령어 집합에 ‘구조’라는 단어가 붙은 이유는 CPU가 어떤 명령어를 이해하는지에 따라 컴퓨터 구조 및 설계 방식이 달라지기 때문입니다.
가령 인텔의 노트북 속 CPU는 x86 혹은 x86-64 ISA를 이해하고, 애플의 아이폰 속 CPU는 ARM ISA를 이해합니다.
x86(x86-64)과 ARM은 다른 ISA이기 때문에 인텔 CPU를 사용하는 컴퓨터와 아이폰은 서로의 명령어를 이해할 수 없습니다.
실행 파일은 명령어로 이루어져 있고 서로의 컴퓨터가 이해할 수 있는 명령어가 다르기 때문입니다.
x86은 32비트용, x86-64는 64비트용 x86 ISA입니다.
어셈블리어는 명령어를 읽기 편하게 표현한 언어입니다.
ISA가 다르다는 건 CPU가 이해할 수 있는 명령어가 다르다는 뜻입니다.
명령어가 달라지면 어셈블리어도 달라집니다.
다시 말해 같은 소스 코드로 만들어진 같은 프로그램이라 할지라도 ISA가 다르면 CPU가 이해할 수 있는 명령어도 어셈블리어도 달라진다는 것입니다.
예를 들어 보겠습니다.
동일한 소스 코드를 작성하고 ISA가 다른 컴퓨터에서 어셈블리어로 컴파일하면 아래와 같은 결과를 얻을 수 있습니다.
왼쪽은 x86-64 ISA, 오른쪽은 ARM ISA입니다.
똑같은 코드로 만든 프로그램임에도 CPU가 이해하고 실행할 수 있는 명령어가 달라 어셈블리어도 다른 것을 알 수 있습니다.
참고로 사용한 컴파일러에 따라서도 어셈블리어가 달라질 수 있는데, 위 예시에서는 gcc 11.2라는 동일한 컴파일러를 이용했습니다.
ISA가 같은 CPU끼리는 서로의 명령어를 이해할 수 있지만, ISA가 다르면 서로의 명령어를 이해하지 못합니다.
이런 점에서 볼 때 ISA는 일종의 CPU의 언어인 샘입니다.
CPU가 이해하는 명령어들이 달라지면 비단 명령어의 생김새만 달라지는게 아닙니다
ISA가 다르면 그에 따른 나비 효과로 많은 것이 달라집니다.
제어장치가 명령어를 해석하는 방식, 사용되는 레지스터의 종류와 개수, 메모리 관리 방법 등 많은 것이 달라집니다.
그리고 이는 곧 CPU 하드웨어 설계에도 큰 영향을 미칩니다.
ISA는 CPU의 언어임과 동시에 CPU를 비롯한 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속이라고도 볼 수 있습니다.
앞서 명령어 병렬 처리 기법들을 학습했습니다.
이를 적용하기에 용이한 ISA가 있고, 그렇지 못한 ISA도 있습니다.
다시 말해 명령어 파이프라인, 슈퍼스칼라, 비순차적 명령어 처리를 사용하기에 유리한 명령어 집합이 있고, 그렇지 못한 명령어 집합도 있습니다.
그렇다면 명령어 병렬 처리 기법들을 도입하기 유리한 ISA는 무엇일까요?
이와 관련해 현대 ISA의 양대 산맥인 CISC와 RISC에 대해 알아보겠습니다.
CISC
CISC(Complex Instruction Set Computer) : ‘복잡한 명령어 집합을 활용하는 컴퓨터’
여기서 ‘컴퓨터’를 ‘CPU’라고 생각해도 좋습니다.
이름 그대로 복잡하고 다양한 명령어들을 활용하는 CPU 설계 방식입니다.
ISA의 한 종류로 소개한 x86, x86-64는 대표적인 CISC 기반의 ISA입니다.
다양하고 강력한 기능의 명령어 집합을 활용하기 때문에 명령어의 형태와 크기가 다양한 가변 길이 명령어를 활용합니다.
메모리에 접근하는 주소 지정 방식도 다양해서 아주 특별한 상황에서만 사용되는 독특한 주소 지정 방식들도 있습니다.
다양하고 강력한 명령어를 활용한다는 말은 상대적으로 적은 수의 명령어로도 프로그램을 실행할 수 있다는 것을 의미합니다.
프로그램을 실행하는 명령어 수가 적다는 말은 ‘컴파일된 프로그램의 크기가 작다’는 것을 의미합니다.
같은 소스 코드를 컴파일해도 CPU마다 생성되는 실행 파일의 크기가 다를 수 있다는 것입니다.
이런 장점 덕분에 CISC는 메모리를 최대한 아끼며 개발해야 했던 시절에 인기가 높았습니다.
‘적은 수의 명령어만으로도 프로그램을 동작시킬 수 있다’는 점은 메모리 공간을 절약할 수 있다는 장점이기 때문입니다.
하지만 CISC에는 치명적인 단점이 있습니다.
활용하는 명령어가 워낙 복잡하고 다양한 기능을 제공하는 탓에 명령어의 크기와 실행되기까지의 시간이 일정하지 않습니다.
그리고 복잡한 명령어 때문에 명령어 하나를 실행하는 데에 여러 쿨럭 주기를 필요로 합니다.
이는 명령어 파이프라인을 구현하는 데에 큰 걸림돌이 됩니다.
명령어 파이프라인 기법을 위한 이상적인 명령어는 다음 그림과 같이 각 단계에 소요되는 시간이 (가급적 1 클럭으로) 동일해야 합니다.
그래야 파이프라인이 마치 공장의 생산 라인처럼 결과를 내기 때문입니다.
하지만 CISC가 활용하는 명령어는 명령어 수행 시간이 길고 가지각색이기 때문에 파이프라인이 효율적으로 명령어를 처리할 수 없습니다.
한마디로 규격화되지 않은 명령어가 파이프라이닝을 어렵게 만든 셈입니다.
명령어 파이프라인이 제대로 동작하지 않는다는 것은 현대 CPU에서 아주 치명적인 약점입니다.
현대 CPU에서 명령어 파이프라인은 높은 성능을 내기 위해 절대 놓쳐서는 안 되는 핵심 기술이기 때문입니다.
게다가 CISC가 복잡하고 다양한 명령어를 활용할 수 있다고는 하지만, 사실 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.
1974년 IBM 연구소의 존 코크(John Cocke)는 CISC 명령어 집합 중 불과 20% 정도의 명령어가 사용된 전체 명령어의 80%가량을 차지한다는 것을 증명하기도 했습니다.
CISC 명령어 집합이 다양하고 복잡한 기능을 지원하지만 실제로는 자주 사용되는 명령어만 쓰였다는 것입니다.
정리하자면, CISC 명령어 집합은 복잡하고 다양한 기능을 제공하기에 적은 수의 명령으로 프로그램을 동작시키고 메모리를 절약할 수 있지만, 명령어의 규격화가 어려워 파이프라이닝이 어렵습니다.
그리고 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.
이러한 이유로 CISC 기반 CPU는 성장에 한계가 있습니다.
RISC
CISC의 한계가 우리들에게 준 교훈은 크게 아래와 같습니다.
빠른 처리를 위해 명령어 파이프라인을 십분 활용해야 한다. 원활한 파이프라이닝을 위해 ‘명령어 길이와 수행 시간이 짧고 규격화’되어 있어야 한다.
어차피 자주 쓰이는 명령어만 줄곧 사용된다. 복잡한 기능을 지원하는 명령어를 추가하기보다는 ‘자주 쓰이는 기본적인 명령어를 작고 빠르게 만드는 것’이 중요하다.
이런 원칙 하에 등장한 것이 RISC입니다.
RISC(Reduced Instruction Set Computer) : 이름처럼 CISC에 비해 명령어의 종류가 적습니다. 그리고 CISC와는 달리 짧고 규격화된 명령어, 되도록 1클럭 내외로 실행되는 명령어를 지향합니다.
즉, 고정 길이 명령어를 활용합니다.
명령어가 규격화되어 있고, 하나의 명령어가 1클럭 내외로 실행되기 때문에 RISC 명령어 집합은 명령어 파이프라이닝에 최적화되어 있습니다.
그리고 RISC는 메모리에 직접 접근하는 명령어를 load, store 두 개로 제한할 만큼 메모리 접근을 단순화하고 최소화를 추구합니다.
그렇기 때문에 CISC보다 주소 지정 방식의 종류가 적은 경우가 많습니다.
이런 점에서 RISC를 load-store 구조라고 부르기도 합니다.
RISC는 메모리 접근을 단순화, 최소화하는 대신 레지스터를 적극적으로 활용합니다.
그렇기에 CISC보다 레지스터를 이용하는 연산이 많고, 일반적인 경우보다 범용 레지스터 개수도 더 많습니다.
다만 사용 가능한 명령어 개수가 CISC보다 적기 때문에 RISC는 CISC보다 많은 명령으로 프로그램을 작동시킵니다.
키워드로 정리하는 핵심 포인트
ISA는 CPU의 언어이자 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속입니다.
CISC는 복잡하고 다양한 종류의 가변 길이 명령어 집합을 활용합니다.
RISC는 단순하고 적은 종류의 고정 길이 명령어 집합을 활용합니다.
-
-
-
-
💻[Operating System] 리눅스와 우분투의 차이점
리눅스와 우분투의 차이점.
“리눅스” 와 “우분투” 의 차이점을 이해하려면 먼저 리눅스가 무엇인지, 그리고 우분투가 어떻게 이와 관련되어 있는지를 알아야 합니다.
리눅스(Linux).
리눅스는 주로 운영 체제의 커널을 가리키는 용어입니다.
커널은 하드웨어와 소프트웨어 컴포넌트 사이에서 통신을 중재하고, 시스템 리소스를 관리하는 핵심 소프트웨어 컴포넌트입니다.
여기서 컴포넌트란 무엇일까요?
“컴포넌트” 란 일반적으로 더 큰 시스템의 일부로 기능하는 개별적인 부품이나 요소를 의미합니다
이 용어는 다양한 분야에서 사용되며, 각각의 맥락에 따라 다소 다른 의미를 가질 수 있습니다.
이 포스팅에서는 소프트웨어 개발에서의 컴포넌트만 설명하겠습니다.
소프트웨어 개발에서의 컴포넌트
소프트웨어 개발에서는 컴포넌트가 소프트웨어의 모듈이나 라이브러리 형태로 존재할 수 있습니다.
이런 컴포넌트들은 재사용 가능하며, 특정 기능을 수행하기 위해 독립적으로 개발되고 통합될 수 있습니다.
예를 들어, 웹 애플리케이션에서 로그인 모듈, 검색 엔진, 사용자 인터페이스 요소 등이 각각의 컴포넌트로 구성될 수 있습니다.
리눅스 커널은 오픈 소스이며, 1991년 리누스 토발즈에 의해 처음 개발되었습니다.
리눅스 커널 자체는 독립적으로 사용할 수 없으며, 보통 시스템 라이브러리, 유틸리티, 필수 프로그램 등과 함께 배포되어 전체 운영 체제의 기반을 형성합니다.
우분투(Ubuntu).
우분투는 리눅스 커널을 기반으로 한 리눅스 배포판 중 하나입니다.
이는 사용자 친화적인 인터페이스, 풍부한 소프트웨어 저장소, 정기적인 업데이트 및 지원을 제공하며, 개인용 컴퓨터, 서버, 클라우드 등 다양한 완경에서 사용할 수 있습니다.
우분투는 데비안(Debian) 리눅스 배포판을 기반으로 만들어졌으며, 쉽게 접근할 수 있고 설치 및 사용이 간편하다는 점에서 초보자에게 인기가 많습니다.
리눅스와 우분투의 주요 차이점.
1. 정의와 범위 : 리눅스는 커널의 이름이며, 우분투는 리눅스 커널을 포함한 전체 운영 체제의 한 배포판입니다.
2. 사용성 : 커널 자체는 기술적인 측면에서만 다루어지지만, 우분투는 끝 사용자를 대상으로 한 GUI와 사용자 친화적 도구를 제공합니다.
3. 목적 : 리눅스 커널은 다양한 운영 체제의 기반으로 사용됩니다. 반면, 우분투는 개인 사용자와 기업 환경 모두를 겨냥해 특정한 목적과 요구를 충족시키기 위해 개발되었습니다.
요약.
리눅스는 기술적인 컴포넌트(커널)를 지칭하고 우분투는 그 커널을 사용하여 만들어진 하나의 완성된 운영 체제 배포판 입니다.
-
💻[Operating System] 커널(kernel)이란?
커널(kernel)이란?
커널의 역할.
커널(kernel)은 컴퓨터 운영 체제의 핵심 구성 요소로서, 하드웨어와 소프트웨어 리소스 간의 통신을 중재하고 시스템의 모든 주요 기능을 관리하는 역할을 합니다.
커널의 기능.
커널의 기능은 매우 광범위하며, 그 중 몇 가지 주요 기능을 다음과 같이 설명할 수 있습니다.
1. 프로세스 관리.
커널은 시스템에서 실행되는 모든 프로세스(활성 프로그램)의 생성, 실행 및 종료를 관리합니다.
프로세스 관리는 프로세스 스케줄링, 상태 관리, 우선순위 할당 등을 포함합니다.
이를 통해 시스템 리소스가 효율적으로 활용되고, 여러 프로세스 간에 시스템 리소스를 공정하게 분배할 수 있습니다.
2. 메모리 관리.
커널은 시스템의 물리적 메모리(RAM)를 관리하고 각 프로그램에 필요한 메모리 공간을 할당 및 회수합니다.
메모리 관리 기능은 메모리 보호, 메모리 할당, 가상 메모리 시스템을 포함하여, 프로그램이 안정적으로 실행될 수 있도록 지원합니다.
3. 디바이스 드라이버와 I/O 관리.
커널은 하드웨어 디바이스와의 통신을 담당하는 디바이스 드라이버를 관리합니다.
입력 및 출력(I/O)장치(예: 키보드, 마우스, 디스플레이, 저장 장치 등)에 대한 접근과 데이터 전송을 총괄하며, 하드웨어의 올바른 동작을 보장합니다.
4. 파일 시스템 관리.
커널은 파일 시스템을 통해 데이터의 저장 및 검색을 관리합니다.
이는 파일 생성, 삭제, 읽기, 쓰기 등의 작업을 포함하며, 사용자와 응용 프로그램이 파일과 디렉토리를 효율적으로 사용할 수 있도록 합니다.
5. 보안 및 접근 제어.
커널은 시스템 보안을 유지하기 위해 사용자 권한 및 접근 제어를 관리합니다.
이를 통해 사용자의 권한에 따라 리소스 접근을 제한하고, 시스템의 안전성을 유지합니다.
6. 네트워킹
커널을 네트워크 통신을 관리하여, 컴퓨터가 네트워크를 통해 다른 시스템과 데이터를 교환할 수 있도록 지원합니다.
마무리.
커널은 일반적으로 시스템의 가장 낮은 수준에서 실행되며, 운영 체제의 나머지 부분과 사용자 애플리케이션으로부터 분리되어 있습니다.
이는 시스템의 핵심적인 부분을 안정적이고 효율적으로 관리할 수 있도록 하기 위함입니다.
커널의 설계와 구현은 운영 체제의 성능, 안정성 및 확장성에 직접적인 영향을 미칩니다.
-
📦[DataStructure] 삽입 정렬
삽입 정렬.
배열 구조를 어떻게 사용할 수 있는지 이해하는 가장 좋은 방법은 실제 알고리즘을 검토하는 것입니다.
삽입 정렬(insertion sort) 은 배열의 값을 정렬하는 알고리즘으로, 순서를 정할 수 있는 모든 유형의 값에서 작동합니다.
정수, 문자열, 심지어 유통기한에 따라 저장된 창고 안 커피까지 삽입 정렬로 정렬할 수 있습니다.
삽입 정렬은 배열의 일부를 정렬하고, 이 정렬된 범위를 전체 배열이 정렬될 때까지 확장합니다.
알고리즘은 정렬되지 않은 배열의 각 원소를 반복하면서 정렬된 부분의 올바른 위치로 이동합니다.
i의 반복을 시작하는 시점에 i-1 이하의 위치에 있는 원소는 모두 정렬되 있습니다.
알고리즘은 이제 인덱스 i에 있는 원소를 선택하고, 정렬된 접두사에서 이 원소의 올바른 위치를 찾아 나머지 원소를 뒤로 이동시켜서 선택한 원소가 들어갈 공간을 만든 수 삽입합니다.
그러면 정렬된 접두사가 하나 더 커지면서 0에서 i까지 모든 상자가 정렬된 상태가 됩니다.
처음에는 첫 번째 원소를 초기 정렬된 접두사로 선언하고 i = 1부터 반복을 시작할 수 있습니다.
커피 컬렉션을 신선도순으로 정렬하고 싶다고 합시다.
무엇보다 프리미엄 커피가 창고 깊숙이 박혀 있다 상해버리는 비극은 바람직하지 않습니다.
따라서 유통기한이 제일 짧게 남은 커피를 가장 앞쪽에 넣어서 쉽게 접근할 수 있게 해야합니다.
우선 커피백 하나를 정렬된 부분으로 선언하고, 이를 기준으로 정렬 범위를 설정함으로써 커피 정렬을 시작합니다.
그 다음에는 가장 앞쪽에서 두 번째 백부터 날짜를 비교해 정렬된 부분의 백보다 더 앞에 넣어야 할지를 판단합니다.
위치를 바꿀 필요가 있는 경우엔 순서를 바꾸고, 그렇지 않은 경우엔 자리를 유지합니다.
이제 자신 있게 맨 앞의 두 백이 정렬됐다고 말할 수 있습니다.
이렇게 부분적으로 정렬하는 과정을 마지막 백까지 진행하면서 위치를 바꾸는 작업을 반복하면, 커피 컬렉션을 완벽하게 정리할 수 있습니다.
아래 코드와 같이 중첩된 루프를 이용해 삽입 정렬을 구현할 수 있습니다.
InsertionSort(array: A):
Integer: N = length(A)
Integer: i = 1
WHILE i < N: // 1
Type: current = A[i]
Integer: j = i - 1
WHILE j >= 0 AND A[j] > current: // 2
A[j + 1] = A[j]
j = j - 1
A[j + 1] = current
i = i +1
바깥쪽 루프는 최초의 정렬되지 않은 원소인 인덱스 i가 1인 원소부터 시작하고 정렬되지 않은 범위에 있는 각 값을 반복합니다(1)
안쪽 루프는 인덱스 j를 사용해 정렬된 접두사의 원소를 맨 뒤에서부터 하나씩 반복합니다(2)
반복 각 단계에서 현재 값과 정렬된 접두사 안에 있는 인덱스 j의 값을 비교해 확인합니다.
j에 있는 원소가 더 크면 두 값의 순서가 잘못됐으므로 교환해야 합니다.
현재 값을 별도의 변수인 current에 저장했기 때문에 이전 상자에서 데이터를 직접 복사합니다.
즉, i번째와 j번째의 값을 완전히 교환할 필요가 없습니다.
내부 루프는 현재 값을 배열의 맨 앞에 밀어넣거나 현재 값보다 이전 값이 더 작을 때까지만(이 경우가 바로 현재 값이 정렬된 접두사의 올바른 위치에 있음을 나타냅니다.) 계속 진행합니다.
이제 내부 루프의 끝에서 현재 값을 올바른 위치에 쓰기만 하면 됩니다.
바깥쪽 루프는 다음 정렬되지 않은 값으로 진행합니다.
아래 그림은 알고리즘이 어떻게 동작하는지 시각화해 보여줍니다.
각 줄은 반복 시작 시 배열의 상태를 보여줍니다.
빨간색 상자는 현재 위치에 있는 원소를 나타내며, 화살표는 현재 위치의 원소를 삽입하면서 발생하는 이동을 나타냅니다.
삽입 정렬은 그렇게 효율적이지 않습니다.
배열에 원소를 삽입할 때, 상당 부분을 이동해야 할 수도 있습니다.
최악의 경우(worst-case), 알고리즘의 비용은 시퀀스 원소 수의 제곱에 비례합니다.
즉, 최악의 경우 리스트의 모든 원소마다 앞의 모든 원소를 이동해야합니다.
배열의 크기를 2배로 늘리면, 최악의 경우 비용이 4배 증가합니다.
그럼에도 불구하고 삽입 정렬은 배열이 어떻게 작동하는지 중요한 통찰을 제공합니다.
이 간단한 알고리즘은 인덱스를 사용해 원소레 직접 접근할 수 있어야 하며, 새 원소를 삽입할 때 값을 교환할 수 있어야 하며, 모든 원소를 반복(iteration)할 수 있어야 한다는 배열의 여러 특성을 보여줍니다.
-
📦[DataStructure] 문자열
문자열(String) 은 종종 특수한 종류의 배열로 생각할 수 있는, 순서가 지정된 문자의 리스트다.
문자열의 각 칸에는 문자, 숫자, 기호, 공뱁 또는 제한된 특수 기호 중 하나가 포함됩니다.
마지막 칸에 있는 특수 기호 /는 종종 문자열의 끝을 나타냅니다.
인덱스를 사용해 문자열의 문자에 직접 접근할 수 있습니다.
일부 프로그래밍 언어에서는 문자열을 그냥 문자 배열로 직접 구현합니다.
몇몇 다른 언어에서는 문자열이 객체일 수 있으며, 문자열 클래스는 문자를 담고 있는 배열이나 다른 자료 구조를 감싼 래퍼(wrapper) 클래스 역할을 합니다.
문자열 래퍼 크래스는 문자열의 크기를 동적으로 조정하거나 부분 문자열을 탐색하는 등 추가 기능을 제공합니다.
두 경우 모두 일반 배열과 유사한 구조가 문장열에 대한 작업에 어떤 영향을 미칠지 생각해보는 것이 유용합니다.
컴퓨터 화면에 문자열을 표시할 때는 문자열의 각 문자를 반복하면서 하나씩 문자를 표시합니다.
동등성(equality) 검사는 더 흥미롭습니다.
한 번의 연산으로 직접 비교할 수 있는 정수와 달리, 문자열은 각 문자를 반복하면서 비교해야 합니다.
두 문자열을 비교할 때는 서로 일치하지 않는 문자를 발견할 때까지 두 문자열에서 같은 위치에 존재하는 문자를 서로 비교합니다.
아래의 코드는 두 문자열의 동등성을 확인하는 알고리즘을 보여줍니다.
StringEqual(String: str1, String: str2):
IF length(str1) != length(str2):
return False
Integer: N = length(str1)
Integer: i = 0
WHILE i < N AND str1[i] == str2[i]:
i = i + 1
return i == N
알고리즘은 먼저 문자열의 크기를 비교합니다.
길이가 다르면 알고리즘은 해당 시점에 중지됩니다.
길이가 같으면 알고리즘은 각 위치를 반복하면서 해당 위치에 있는 두 문자를 비교합니다.
이때 두 문자가 서로 일치하지 않으면 루프를 중지할 수 있습니다.
문자열을 모두 비교했는데 불일치가 일어나지 않았다면 두 문자열을 같다고 선언할 수 있습니다.
아래의 그림은 이 알고리즘이 두 문자열에 대해 어떻게 작동하는지 보여줍니다. =는 비교할 때 서로 일치한 문자 쌍을 나타냅니다.
X는 최초 불일치로 인해 검사가 종료된 문자쌍을 나타냅니다.
문자열 비교에서 최악의 경우 계산 비용은 문자열의 길잉 비례해 증가합니다.
두 작은 문자열을 비교하는 작업에서는 무시할 수 있지만, 두 긴 문자열을 비교하는 작업에서는 시간이 오래 걸릴 수 있습니다.
예를 들어, 어떤 책의 1판과 2판을 처음부터 한 글자씩 비교하면서 두 책의 본문 문자 배열의 차이를 찾는 지겨운 과정을 상상해볼 수 있습니다.
가장 좋은 경우에는 초기에 일치하지 않는 부분을 찾을 수 있지만, 최악의 경우에는 책의 대부분을 검사해야 합니다.
많은 프로그래밍 언어, 예를 들어 파이썬과 같은 언어는 직접 비교할 수 있는 문자열 클래스를 제공합니다.
따라서 위 코드와 같은 비교 코드를 직접 구현할 필요가 없습니다.
그러나 간단한 비교 함수의 뒤에는 모든 문자를 반복하는 루프가 있습니다.
이 중요한 세부 사항을 이해하지 않으면 문자열 비교 비용을 과소평가할 수 있습니다.
-
📦[DataStructure] 배열
배열.
일반적으로 배열(array) 은 관련된 다수의 값을 저장할 때 사용합니다.
예를 들어, 1년간 매일 마신 커피의 양을 추적하고 싶다고 합시다.
이때 개별 변수(AmountDay1, AmountDay2, AmountDay3 등)를 365개 만들어서 저장할 수 있겠지만, 이 방식은 입력하기도 귀찮고 데이터를 어떤 구조로도 사용할 수 없습니다.
AmountDay2는 단지 텍스트 꼬리표일 뿐이며, AmountDay2 전날의 정보를 AmountDay1이 저장하고 AmountDay2 다음 날의 정보를 AmountDay3가 저장한다는 사실을 프로그램이 알 수 없습니다.
개발자만 이 정보를 알고 있습니다.
배열은 여러 값을 연속적으로 인데스(Index) 가 부여된 상자에 저장하는 간단한 메커니즘을 제공합니다.
아래의 그림처럼 배열은 사실 개별 변수들을 한 줄로 세워둔 것이며, 컴퓨터 메모리에 존재하는 같은 크기의 상자들이 연속적으로 배치된 블록입니다.
개별 변수처럼 배열도 어떤 메모리 덩어리를 차지하며 임의의 다른 정보와 인접할 수 있습니다.
배열의 각 상자에는 숫자, 문자, 포인터 또는 다른(크기가 정해져 있는) 자료 구조와 같은 타입의 값을 저장할 수 있습니다.
일상생활에서도 배열을 매우 많이 사용합니다.
예를 들어, 고등학교 복도에 늘어선 사물함은 학생들의 책과 외투를 저장하는 물리적인 배열입니다.
우리는 개별 사물함을 열어 내부 공간에 쉽게 접근할 수 있습니다.
배열의 구조는 위치(또는 인덱스)를 지정하여 배열 내 개별 값, 즉 원소(element) 에 접근할 수 있게 해줍니다.
배열 내 상자들은 컴퓨터 메모리에서 서로 인접해 있으므로, 첫 번째 원소로부터 오프셋(offset)을 계산해서 해당하는 위치의 메모리를 읽는 방식으로 각 상자에 쉽게 접근할 수 있습니다.
이는 접근하려는 상자의 위치와 관계없이 덧셈 한 번과 메모리 접근만 필요하다는 뜻입니다.
이러한 구조는 우리의 일일 커피 섭취량을 추적하는 것과 같이 순서가 있는 항목을 저장할 때 특히 편리합니다.
형식적으로 배열 A에서 인덱스 i에 있는 값을 A[i]로 참조합니다.
사물함 예제에서 인덱스는 사물함 앞에 표시된 숫자에 해당합니다.
대부분의 프로그래밍 언어는 0부터 시작하는(zero based) 인덱스를 사용합니다.
이 말은 아래의 그림처럼 배열의 첫 번째 값은 인덱스 0, 두 번째 값은 인덱스 1, …에 위치한다는 뜻입니다.
아래 그림은 컴퓨터 메모리 안 배열 모습을 보여줍니다.
여기서 흰 칸이 배열 원소에 해당합니다.
0을 기준으로 인덱싱하면 메모리 내에서 배열의 시작점부터 오프셋을 사용해 위치를 계산할 때 편리합니다.
i번째 원소의 위치는 다음과 같이 계산할 수 있습니다.
위치(인덱스 i의 원소) = 위치(배열 시작) + 각 원소의 크기 x i
인덱스 0의 위치는 배열 시작점과 같습니다.
예를 들어, 위 그림에서 배열 A의 다섯 번째 원소는 A[4]이며 그림 1-4를 찾아보면 그 위치에는 9라는 값이 들어 있습니다.
노트
인덱스를 1부터 시작하는 것도 가능하며, 일부 프로그래밍 언어는 이 규칙을 따릅니다.
1을 기준으로 인덱싱하는 경우 상자의 주소를 계산하는 식은 다음과 같습니다.
위치(인덱스 i의 원소) = 위치(배열 시작) + 각 원소의 크기 x (i-1)
대부분의 프로그래밍 언어에서는 배열 이름과 인덱스를 조합해 값을 가져오거나 설정합니다.
예를 들어, 다음과 같이 인덱스가 5인 상자의 값을 16으로 설정할 수 있습니다.
A[5] = 16
커피 추적 예제에서 하루 동안 섭취한 커피 컵 수를 저장하기 위해 Amount라는 배열을 정의하고, 해당 수량을 Amount[0] 부터 Amount[364]까지 저장할 수 있습니다.
배열을 사용하면 단 하나의 이름으로 365개 다른 값에 순서대로 접근 할 수 있는데, 이름은 비슷하지만 서로 독립적인 변수들을 연속적으로 위치시켰던 것을 수학적인 오프셋으로 전환한 것입니다.
이 개념의 장점을 이해하려면 학교 사물함을 생각하면 됩니다.
개별 사물함을 ‘제레미의 사물함’이나 ‘K로 시작하는 세 번째 학생의 사물함’처럼 이름 붙이면 빠르게 찾기가 거의 불가능합니다.
이런 방식을 사용하면 그냥 인덱스를 사용하는 경우와 달리 모든 사물함에 붙은 꼬리표를 일일이 찾아봐야 합니다.
하지만 배열 인덱스를 사용하면 학생들은 오프셋을 사용해 사물함이 어디 있는지 결정하고 직접 해당 사물함에 접근할 수 있습니다.
종종 배열을 전체 자료 구조로 시각화하고 논의하지만, 각 상자가 개별 변수처럼 작동한다는 사실을 기억하는 것이 중요합니다.
배열을 전체적으로 바꾸려면 모든 상자를 하나하나 바꿔야 합니다.
예를 들어, 원소를 한 칸 앞으로 이동시키고 싶으면 아래 그림처럼 해야 합니다.
배열은 책장에 꽂혀 있는 책들과 다릅니다.
‘커피 애호가를 위한 최고의 공정 무역 커피 가이드’를 끼워넣기 위해 책 컬렉션 전체를 밀어낼 수 있지만, 배열을 그렇지 않습니다.
배열은 오히려 일렬로 늘어선 가게와 같습니다.
서점과 미용실 사이에 커피숍을 끼어넣을 수 없습니다.
커피숍 공간을 확보하려면 인접한 건물로 서점(또는 미용실)을 이전해서 기존 공간을 비우는 방식으로 가게를 하나씩 옮겨야만 합니다.
실제로 배열에서 단순히 두 값을 교환하고 싶은 경우에도 값들을 미묘하게 조정해야 합니다.
예를 들어, 어떤 인덱스 i와 j에 있는 두 값을 교환하려면 먼저 둘 중 하나를 임시 변수에 할당해야 합니다.
Temp = A[i]
A[i] = A[j]
A[j] = Temp
그렇지 않으면 어떤 한 상자 안 값을 덮어쓰게 되어 두 상자가 동일한 값을 가지게 됩니다.
마찬가지로 커피숍과 서점의 위치를 바꾸려고 한다면, 먼저 서점의 가구와 물품 등을 비어 있는 세 번째 임시 위치로 커피숍의 것들을 넣을 수 있는 공간을 확보해야 합니다.
그 후 커피숍을 옮길 수 있고, 서점의 가구와 물품 등을 세 번째 임시 위치에서 커피숍의 이전 위치로 옮길 수 있습니다.
-
-
📦[DataStructure] 변수
변수.
개변 데이터 조각을 종종 변수(variable)에 저장하곤 합니다.
변수(variable) : 컴퓨터 메모리 내 데이터 위치(또는 주소)를 표현하는 이름입니다.
프로그램 실행 중 변경되는 정보를 추적할 수 있게 합니다.
예를 들어 For 루프를 몇 번지나갔는지 세어야 할 경우, 게임에서 플레이어의 점수를 추적해야하는 경우 등
변수가 없으면 프로그램의 내부 상태를 추적, 평가(evaluate), 변경(update)할 수 없습니다.
변수를 생성하면 시스템이 그것을 자동으로 할당하고 위치를 지정합니다.
그리고 나서 원하는 변수 이름을 사용해 자유롭게 해당 위치에 데이터를 쓰고, 데이터를 쓸 때 사용한 변수 이름을 사용해 저장된 데이터를 읽을 수 있습니다.
변수 이름만 알고 있다면 데이터의 메모리 위치를 알 필요가 없습니다.
컴퓨터 메모리를 여러 상자가 일렬로 늘어선 것처럼 생각할 수도 있습니다.
각 변수는 저장한 데이터의 크기에 따라 하나 이상의 인접한 상자를 차지합니다.
아래 그림은 Level, Score, AveScore라는 세 변수를 보여줍니다.
여기서 평균 점수(AveScore)는 메모리 상자를 두 개 사용하는 부동 소수점 수(floating point number, 소수점이 있는 숫자)입니다.
어떤 측면에서 변수는 종이 문서를 담는 폴더에 붙은 종이 라벨과 비슷합니다.
아래 그림처럼 라벨을 붙인 후에는 폴더의 순서나 정확한 위치를 기억할 필요가 없습니다.
그 이유는 라벨로 폴더를 찾으면 되기 때문입니다.
이때 충분한 정보가 포함된 이름을 사용하는 것이 중요합니다.
만약에 파일 캐비닛에 할 일, 중요한 일, 다른 할 일, 그 밖의 일과 같이 이름이 겹치는(이를 오버로드(overload)라고 말합니다) 폴더가 많을 경우 내용을 파악하기 어렵습니다.
마찬가지로, 변수의 이름이 모호하면 변수가 어떤 값을 나타내는지 추측하기 어려워집니다.
많은 프로그래밍 언어에서 변수는 정수(integer), 부동 소수점 값(float), 불린 값(Boolean) 등과 같이 저장된 데이터의 타입과 연관이 있습니다.
타입은 변수가 얼마나 많은 메모리를 차지하고 메모리에 저장된 내용을 어떻게 사용해야 하는지를 프로그램에 알려줍니다.
예를 들어, 불린 변수는 제한된 범위의 값(즉, 참과 거짓)만 저장하며 적은 양의 메모리만 사용하는 경우가 많습니다.
반면, 2배 정밀도(double-precision) 부동 소수점 수는 훨씬 더 크고 정확한 숫자를 저장하므로 여러 상자를 사용합니다.
타입을 정의하는 문법이나 타입을 명시적으로 정의해야만 하는지 여부는 프로그래밍 언어마다 다릅니다.
아래 예제를 봐봅시다.
예제에서는 변수를 명시할 때 언어와 무관한 <타입>: <변수이름>이라는 의사 코드(pseudocode) 형식을 사용합니다.
Integer: coffee_count = 5
Float: percentage_words_spelled_correctly = 21.0
Boolean: had_enough_coffee = False
가씀 Type이라는 타입이 지정된 변수도 있습니다.
이 타입은 어떻게 구현하는지에 따라 다양한 타입이 될 수 있다는 사실을 나타냅니다.
대부분의 프로그래밍 언어에서 일반적으로 사용되는 구문을 사용해 변수를 다룰 것입니다.
예를 들어, 변수에 값을 대입할 때는 =을 사용합니다.
coffee_count = coffee_count + 1
-
-
☕️[Java] System 클래스
System 클래스
System 클래스는 시스템과 관련된 기본 기능들을 제공합니다.
package lang.system;
import java.util.Arrays;
public class SystemMain {
public static void main(String[] args) {
// 현재 시간(밀리초)를 가져옵니다.
long currentTimeMillis = System.currentTimeMillis();
System.out.println("currentTimeMillis = " + currentTimeMillis);
// 현재 시간(나노초)를 가져옵니다.
long currentTimeNano = System.nanoTime();
System.out.println("currentTimeNano = " + currentTimeNano);
// 환경 변수를 읽습니다.
System.out.println("getenv = " + System.getenv());
// 시스템 속성을 읽습니다.
System.out.println("properties = " + System.getProperties());
System.out.println("Java version = " + System.getProperty("java.version"));
// 배열을 고속으로 복사합니다.
char[] originalArray = { 'h', 'e', 'l', 'l', 'o' };
char[] copiedArray = new char[5];
System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length);
// 배열 출력
System.out.println("copiedArray = " + copiedArray);
System.out.println("Arrays.toString = " + Arrays.toString(copiedArray));
// 프로그램 종료
System.exit(0);
}
}
실행 결과
currentTimeMillis = 1713485140558
currentTimeNano = 694481339313708
getenv = {HOMEBREW_PREFIX=/opt/homebrew, MANPATH=/Users/kobe/.nvm/versions/node/v20.10.0/share/man:/opt/local/share/man:/opt/homebrew/share/man::, COMMAND_MODE=unix2003, INFOPATH=/opt/homebrew/share/info:...
properties = {java.specification.version=21, sun.jnu.encoding=UTF-8, java.class.path=/Users/kobe/Desktop/practiceJavaMidPart1/java-mid1/out/production/java-mid1, java.vm.vendor=Oracle Corporation, sun.arch.data.model=64, java.vendor....
Java version = 21.0.2
copiedArray = [C@77459877
Arrays.toString = [h, e, l, l, o]
Process finished with exit code 0
표준 입력, 출력, 오류 스트림: System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타냅니다.
시간 측정: System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공합니다.
환경 변수: System.getProperties()를 사용해 현재 시스템 속성을 얻거나 System.getProperty(Stringkey)로 특정 속성을 얻을 수 있습니다. 시스템 속성은 자바에서 사용하는 설정 값입니다.
시스템 종료: System.exit(int status) 메서드는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달합니다.
상태 코드0 : 정상 종료
상태 코드 0이 아님: 오류나 예외적인 종료
배열 고속 복사: System.arraycopy는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용합니다. 직접 반복문을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공합니다.
-
☕️[Java] Class 클래스
Class 클래스.
자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다.
Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다.
Class 클래스의 주요 기능은 다음과 같습니다.
타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다.
리플렉션: 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다.
동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다.
애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다.
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다.
다음 코드를 실행해봅시다.
package lang.clazz;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
Class clazz = String.class; // 1. 클래스에서 조회
//Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
//Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
// 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interfaces) {
System.out.println("Interface: = " + i.getName());
}
}
}
class vs clazz - class는 자바의 예약어입니다. 따라서 패키지명, 변수명으로 사용할 수 없습니다.
이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용합니다.
clazz는 class와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있습니다.
주의!
main() 옆에 throws Exception이 추가된 부분에 주의합시다. 이 코드가 없으면 컴파일 오류가 발생합니다.
실행 결과
field = class [B value
...
method = byte[] java.lang.String.value()
method = public boolean java.lang.String.equals(java.lang.Object)
...
Superclass: java.lang.Object
Interface: = java.io.Serializable
Interface: = java.lang.Comparable
...
Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다.
Class clazz = String.class; // 1. 클래스에서 조회
Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
Class clazz = Clazz.forName("java.lang.String"); // 3. 문자열로 조회
Class 클래스의 주요 기능
getDeclaredFields(): 클래스의 모든 필드를 조회합니다.
getDeclaredMethods(): 클래스의 모든 메서드를 조회합니다.
getSuperclass(): 클래스의 부모 클래스를 조회합니다.
getInterface(): 클래스의 인터페이스들을 조회합니다.
실행 결과를 보면 String 클래스의 다양한 정보를 확인할 수 있습니다.
클래스 생성하기
Class 클래스에는 클래스의 모든 정보가 들어있습니다. 이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다.
여기서는 간단하게 인스턴스를 생성해봅시다.
package lang.clazz;
public class ClassCreatMain {
public static void main(String[] args) throws Exception {
//Class helloClass = Hello.class;
Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
실행 결과
result = hello!
getDeclaredConstructor().newInstance()
getDeclaredConstructor() : 생성자를 선택합니다.
newInstance() : 선택된 생성자를 기반으로 인스턴스를 생성합니다.
리플랙션 - reflection
Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다.
이런 작업을 리플렉션이라고 합니다.
추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수도 있습니다.
최신 프레임워크들은 이런 기능을 적극 활용합니다.
지금은 Class가 뭔지, 그리고 대략 어떤 기능들을 제공하는지만 알아두면 충분합니다.
지금은 리플랙션을 학습하는 것 보다 훨씬 더 중요한 기본기들을 학습해야 합니다.
-
💾 [CS] 명령어 병렬 처리 기법
명령어 병렬 처리 기법
명령어 병령 처리 기법(ILP: Instruction-Level Parallelism): 명령어를 동시에 처리하여 CPU를 한시도 쉬지 않고 작동시키는 기법.
대표적인 명령어 병렬 처리 기법
명령어 파이프 라이닝
슈퍼스칼라
비순차적 명령어 처리
명령어 파이프라인
명령어 파이프라인을 이해하려면 하나의 명령어가 처리되는 전체 과정을 비슷한 시간 간격으로 나누어 보아야 합니다.
명령어 처리 과정을 클럭 단위로 나누어 보면 일반적으로 다음과 같이 나눌 수 있습니다.
명령어 인출(Instruction Fetch)
명령어 해석(Instruction Decode)
명령어 실행(Execute Instruction)
결과 저장(Write Back)
참고: 이 단계가 정답은 아닙니다.
전공서에 따라 명령어 인출 -> 명령어 실행으로 나누기도 하고,
명령어 인출 -> 명령어 해석 -> 명령어 실행 -> 메모리 접근 -> 결과 저장으로 나누기도 합니다.
여기서 중요한 점은 같은 단계가 겹치지만 않는다면 CPU가 ‘각 단계를 동시에 실행할 수 있다’는 것입니다.
예를 들어 CPU는 한 명령어를 ‘인출’하는 동안에 다른 명령어를 ‘실행’할 수 있고, 한 명령어가 ‘실행’되는 동안에 연산 결과를 ‘저장’할 수 있습니다.
이를 그림으로 표현하면 다음과 같습니다.
t1에는 명령어 1, 2를 동시에 처리할 수 있고 t2에는 명령어 1,2,3을 동시에 처리할 수 있습니다.
이처럼 명령어를 겹처서 수행하면 명령어를 하나하나 실행하는 것보다 훨씬 더 효율적으로 처리할 수 있습니다.
이처럼 마치 공장 생산 라인과 같이 명령어들을 “명령어 파이프라인(instruction pipeline)” 에 넣고 동시에 처리하는 기법을 “명령어 파이프라이닝(instruction pipelining)” 이라고 합니다.
명령어 파이프라인을 사용하지 않고 모든 명령어를 순차적으로만 처리한다면 아래와 같이 처리했을것입니다.
한눈에 봐도 명령어 파이프라이닝을 이용하는 것이 더 효율적임을 알 수 있습니다.
파이프라이닝이 높은 성능을 가져오기는 하지만, 특정 상황에서는 성능 향상에 실패하는 경우도 있습니다.
이러한 상황을 파이프라인 위험(pipeline hazard) 이라고 부릅니다.
파이프라인 위험에는 크게 3가지가 있습니다.
데이터 위험
제어 위험
구조적 위험
데이터 위험
데이터 위험(data hazard) 은 명령어 간 ‘데이터 의존성’에 의해 발생합니다.
모든 명령어를 동시에 처리할 수는 없습니다.
어떤 명령어는 이전 명령어를 끝까지 실행해야만 비로소 실행할 수 있는 경우가 있습니다.
예를 들어 아래 두 명령어를 봅시다.
편의상 레지스터 이름을 R1, R2, R3, R4, R5라 하고 ‘왼쪽 레지스터에 오른쪽 결과를 저장하라’는 기호는 <- 기호로 표기하겠습니다.
명령어 1: R1 <- R2 + R3 // R2 레지스터 값과 R3 레지스터 값을 더한 값을 R1 레지스터에 저장
명령어 2: R4 <- R1 + R5 // R1 레지스터 값과 R5 레지스터 값을 더한 값을 R4 레지스터에 저장
위의 경우 명령어 1을 수행해야만 명령어 2를 수행할 수 있습니다.
즉, R1에 R2 + R3 결괏값이 저장되어야 명령어 2를 수행할 수 있습니다.
만약 명령어 1 실행이 끝나기 전에 명령어 2를 인출하면 R1에 R2 + R3 결괏값이 저장되기 전에 R1 값을 읽어 들이므로 원치 않은 R1 값으로 명령어 2를 수행합니다.
따라서 명령어 2는 명령어 1의 데이터에 의존적입니다.
이처럼 데이터 의존적인 두 명령어를 무작정 동시에 실행하려고 하면 파이프라인이 제대호 작동하지 않는 것을 ‘데이터 위험’이라고 합니다.
제어 위험
제어 위험(control hazard) 은 주로 분기 등으로 인한 ‘프로그램 카운터의 갑작스러운 변화’에 의해 발생합니다.
기본적으로 프로그램 카운터는 ‘현재 실행 중인 명령어의 다음 주소’로 갱신됩니다.
하지만 프로그램 실행 흐름이 바뀌어 명령어가 실행되면서 프로그램 카운터 값에 갑작스러운 변화가 생긴다면 명령어 파이프라인에 미리 가지고 와서 처리 중이었던 명령어들은 아무 쓸모가 없어집니다.
이를 ‘제어 위험’이라고 합니다.
참고: 참고로 이를 위해 사용하는 기술 중 하나가 분기 예측(branch prediction) 입니다.
분기 예측은 프로그램이 어디로 분기할지 미리 예측한 후 그 주소를 인출하는 기술입니다.
구조적 위험
구조적 위험(structural hazard) 은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 ALU, 레지스터 등과 같은 CPU 부품을 사용하려고 할 때 발생합니다.
구조적 위험은 자원 위험(resource hazard) 이라고도 부릅니다.
슈퍼스칼라
파이프라이닝은 단일 파이프라인으로도 구현이 가능하지만, 오늘날 대부분의 CPU에서는 여러 개의 파이프라인을 이용합니다.
이처럼 CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조를 슈퍼스칼라(superscalar) 라고 합니다.
명령어 파이프라인을 하나만 두는 것이 마치 공장 생산 라인을 한 개 두는 것과 같다면, 슈퍼스칼라는 공장 생산 라인을 여러 개 두는 것과 같습니다.
슈퍼스칼라 구조로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서 또는 슈퍼스칼라 CPU라고 합니다.
슈퍼스칼라 프로세서는 매 클럭 주기마다 동시에 여러 명령어를 인출할 수도, 실행할 수도 있어야 합니다.
가령 멀티스레드 프로세서는 한 번에 여러 명령어를 인출하고, 해석하고, 실행할 수 있기 때문에 슈퍼스칼라 구조를 사용할 수 있습니다.
슈퍼스칼라 프로세서는 이론적으로 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라집니다.
하지만 파이프라인 위험 등의 예상치 못한 문제가 있어 실제로는 반드시 파이프라인 개수에 비례하여 빨라지지는 않습니다.
이 때문에 슈퍼스칼라 방식을 차용한 CPU는 파이프라인 위험을 방지하기 위해 고도로 설계되어야 합니다.
여러 개의 파이프라인을 이용하면 하나의 파이프라인을 사용할 때 보다 데이터 위험, 제어 위험, 자원 위험을 피하기가 더욱 까다롭기 때문입니다.
비순차적 명령어 처리
비순차적 명령어 처리(OoOE: Out-of-order execution): 보통 OoOE로 줄여 부릅니다. 이 기법은 많은 전공서에서 다루지 않지만, 오늘날 CPU 성능 향상에 크게 기여한 기법이자 대부분의 CPU가 차용하는 기법입니다.
비순차적 명령어 처리 기법은 이름에서도 알 수 있듯 명령어들을 순차적으로 실행하지 않는 기법입니다. 명령어의 ‘합법적인 새치기’라고 볼 수 있습니다.
지금까지 설명했던 명령어 파이프라이닝, 슈퍼스칼라 기법은 모두 여러 명령어의 순차적인 처리를 상정한 방법이었습니다.
프로그램을 위에서 아래로 차례차례 실행하는 방식이었습니다.
하지만 파이프 라인 위험과 같은 예상치 못한 문제들로 인해 이따금씩 명령어는 곧바로 처리되지 못하기도 합니다.
만약 모든 명령어를 순차적으로만 처리한다면 이런 예상치 못한 상황에서 명령어 파이프라인은 멈춰버리게 됩니다.
예를 들어 아래와 같은 명령어들로 이루어진 소스 코드가 있다고 해봅시다.
편의상 ‘메모리 N번지’는 M(N)으로. ‘메모리 N번지에 M을 저장하라’는 M(N) <- M으로 표기하겠습니다.
1. M(100) <- 1
2. M(101) <- 2
3. M(103) <- M(100) + M(101)
4. M(150) <- 1
5. M(151) <- 2
6. M(152) <- 3
여기서 주목해야 할 점은 3번 명령어를 실행하기 위해서는 M(100) 값은 물론 M(101) 값이 결정되어야 하기에 1번과 2번 명령어 실행이 끝날 때까지 기다려야 한다는 점입니다.
이 명령어들을 순차적으로 실행되는 CPU로 실행하면 다음과 같습니다.
2번 명령어 실행이 끝날 때까지 3, 4, 5, 6번 명령어들은 기다립니다.
앞의 코드를 이루는 명령어들 중에 서로 데이터 의존성이 전혀 없는, 순서를 바꿔 처리해도 수행 결과에 영향을 미치지 않는 명령어들이 있습니다.
가령 3번은 다음과 같이 뒤의 명령어와 순서를 바꾸어 실행해도 크게 문제될 것이 없습니다.
이렇게 순서를 바꿔 실행하면 아래와 같이 수행됩니다.
순차적으로 명령어를 처리할 때보다 더 효율적입니다.
이렇게 명령어를 순차적으로만 실행하지 않고 순서를 바꿔 실행해도 무방한 명령어를 먼저 실행하여 명령어 파이프라인이 멈추는 것을 방지하는 기법을 비순차적 명령어 처리 기법 이라고 합니다.
하지만 아무 명령어나 순서를 바꿔서 수행할 수는 없습니다.
예를 들어서 다음 예시를 봅시다.
1. M(100) <- 1
2. M(101) <- 2
3. M(102) <- M(100) + M(101)
4. M(103) <- M(102) + M(101)
5. M(104) <- M(100)
위 코드에서 3번 명령어와 1번 명령어의 순서를 바꿀 수는 없습니다.
3번 명령어를 수행하려면 반드시 M(100) 값이 결정되어야 하기 때문입니다.
마찬가지로 4번 명령어와 1번 명령어는 순서를 바꿀 수 없습니다.
1번 명령어를 토대로 3번 명령어가 수행되고, 3번 명령어를 토대로 4번이 수행되기 때문입니다.
하지만 위 코드에서 4번 명령어와 5번 명령어는 순서를 바꾸어 실행할 수 있습니다.
다시 말해 이 두 명령어는 어떤 의존성도 없기에 순서를 바꿔도 전체 프로그램의 실행 흐름에는 영향이 없습니다.
이처럼 비순차적 명령어 처리가 가능한 CPU는 명령어들이 어떤 명령어와 데이터 의존성을 가지고 있는지, 순서를 바꿔 실행할 수 있는 명령어에는 어떤 것들이 있는지를 판단할 수 있어야 합니다.
키워드로 정리하는 핵심 포인트
명령어 파이프라이닝은 동시에 여러 개의 명령어를 겹쳐 실행하는 기법입니다.
슈퍼 스칼라는 여러 개의 명령어 파이프라인을 두는 기법입니다.
비순차적 명령어 처리 기법은 파이프라인의 중단을 방지하기 위해 명령어를 순차적으로 처리하지 않는 기법입니다.
-
💾 [CS] 빠른 CPU를 위한 설계 기법
빠른 CPU를 위한 설계 기법.
클럭
조금이라도 더 빠른 CPU를 만들려면 어떻게 CPU를 설계해야 할까요?
이전에 학습한 내용을 상기해봅시다.
컴퓨터 부품들은 ‘클럭 신호’에 맞춰 일사분란하게 움직인다.
CPU는 ‘명령어 사이클’이라는 정해진 흐름에 맞춰 명령어들을 실행한다.
클럭 신호가 빠르게 반복되면 CPU를 비롯한 컴퓨터 부품들은 그만큼 빠른 박자에 맞춰 움직일 것 입니다.
즉, 클럭 속도가 높아지면 CPU는 명령어 사이클을 더 빠르게 반복할 것이고, 다른 부품들도 그에 발맞춰 더 빠르게 작동할 것입니다.
실제로 클럭 속도가 높은 CPU는 일반적으로 성능이 좋습니다.
그래서 클럭 속도는 CPU 속도 단위로 간주되기도 합니다.
클럭 속도: 헤르츠(Hz) 단위로 측정합니다. 이는 1초에 클럭이 몇 번 반복되는지를 나타냅니다.
가령 클럭이 ‘똑-딱-‘하고 1초에 한 번 반복되면 CPU 클럭 속도는 1Hz인 것이고, 클럭이 1초에 100번 반복되면 CPU 클럭 속도는 100Hz인 셈입니다.
실제 CPU 클럭 속도는 위 사진 속 CPU를 보면 알 수 있습니다.
위 사진 속 CPU를 보면 기본 속도(Base)는 2.5GHz, 최대 속도(Max)는 4.9GHz라는 것을 알 수 있습니다.
이는 1초에 클럭이 기본적으로 25억(2.5 x 10⁹)번 반복된다는 것을 나타냅니다.
참고: 1GHz는 1,000,000,000(10⁹)Hz입니다.
“클럭 속도는 일정하지 않다.”
‘클럭’이라는 단어만 보고 시계를 떠올려 클럭 속도가 매번 일정하게 유지된다고 생각할 수도 있지만, 실제로는 그렇지 않습니다.
CPU 사진을 다시 보면 기본 클럭 속도(Base)와 최대 속도(Max)로 나위어 있습니다.
이처럼 CPU는 계속 일정한 클럭 속도를 유지하기보다는 고성능을 요하는 순간에는 순간적으로 쿨럭 속도를 높이고, 그렇지 않을 때는 유연하게 쿨럭 속도를 낮추기도 합니다.
최대 클럭 속도를 강제로 더 끌어올릴 수도 있는데, 이런 기법을 오버클럭킹(overclocking) 이라고 합니다.
클럭 속도를 무지막지하게 높이면 CPU는 무작정 빨라지지 않습니다.
그래픽이 많이 요구되는 게임이나 영상 편집과 같이 CPU에 무리가 가는 작업을 장시간 하면 컴퓨터가 뜨겁게 달아오르는 것을 경험해 본 적이 있을 겁니다.
클럭 속도를 무작정 높이면 이러한 발열 문제가 더 심각해집니다.
이처럼 클럭 속도를 높이는 것은 분명 CPU를 빠르게 만들지만, 클럭 속도만으로 CPU의 성늘을 올리는 것에는 한계가 있습니다.
코어와 멀티 코어
클럭 속도를 높이는 방법 외에 CPU의 성능을 높이는 방법에는 대표적으로 CPU의 코어와 스레드 수를 늘리는 방법이 있습니다.
먼저 코어를 늘리는 방법을 알아봅시다.
코어를 이해하려면 현대적인 관점에서 CPU라는 용어를 재해석해야 합니다.
앞서 CPU를 ‘명령어를 실행하는 부품’이라고 소개했습니다.
많은 전공 서적들의 전통적인 관점에서 ‘명령어를 실행하는 부품’은 원칙적으로 하나만 존재했습니다.
하지만 오늘날 CPU는 많은 기술적 발전을 거듭하였고, 그 결과 CPU 내부에는 ‘명령어를 실행하는 부품’을 얼마든지 만들 수 있게 되었습니다.
우리가 지금까지 CPU의 정의로 알고 있었던 ‘명령어를 실행하는 부품’은 오늘날 코어(core) 라는 용어로 사용됩니다.
다시 말해, 오늘날의 CPU는 단순히 ‘명령어를 실행하는 부품’에서 ‘명령어를 실행하는 부품을 여러 개 포함하는 부품’으로 명칭의 범위가 확장 되었습니다.
예를 들어 8코어(Core) CPU는 ‘명령어를 실행하는 부품’을 여덟 개 포함하고 있다고 보면 됩니다.
코어를 여러 개 포함하고 있는 CPU를 멀티코어(multi-core) CPU 또는 멀티코어 프로세서라고 부릅니다.
이는 CPU 내에 명령어를 처리하는 일꾼이 여러 명 있는 것과 같습니다.
당연히 멀티코어의 처리 속도는 단일코어보다 더 빠릅니다.
다령 클럭 속도가 2.4GHz인 단일 코어 CPU와 클럭 속도가 1.9GHz인 멀티코어 CPU를 비교하면 일반적으로 후자의 성능이 더 좋습니다.
CPU 종류는 CPU 안에 코어가 몇 개 포함되어 있는지에 따라 아래 표와 같이 싱글코어, 듀얼코어, 트리플코어 등으로 나뉩니다.
코어를 늘릴수록 연산 처리 속도도 빨라질까요?
CPU의 연산 속도가 꼭 코어 수에 비례하여 증가하지는 않습니다.
코어마다 처리할 연산이 적절히 분배되지 않는다면 코어 수에 비례하여 연산 속도가 증가하지 않습니다.
또한 처리하고자 하는 작업량보다 코어 수가 지나치게 많아도 성능에는 크게 영향이 없습니다.
중요한 것은 코어마다 처리할 명령어들을 얼마나 적절하게 분배하느냐이고 그에 따라서 연산 속도는 크게 달라집니다.
스레드와 멀티스레드
스레드(thread): 사전적 의미는 ‘실행 흐름의 단위’입니다.
하지만 이 정의를 활자 그대로 받아들이지 말고 더욱 엄밀하게 이해해야 합니다.
CPU에서 사용되는 스레드와 프로그래밍에서 사용되는 스레드는 용례가 다르기 때문입니다.
스레드에는 CPU에서 사용되는 하드웨어적 스레드가 있고, 프로그램에서 사용되는 소프트웨어적 스레드가 있습니다.
하드웨어적 스레드
스레드를 하드웨어적으로 정의하면 ‘하나의 코어가 동시에 처리하는 명령어 단위’를 의미합니다.
CPU에서 사용하는 스레드라는 용어는 보통 CPU 입장에서 정의된 하드웨어적 스레드를 의미합니다.
하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 멀티스레드(multithread) 프로세서 또는 멀티스레드 CPU라고 합니다.
하이퍼스레딩(hyper-threading): 인텔의 멀티스레드 기술을 의미합니다.
인텔이 자신들의 멀티스레드 기술에 하이퍼스레딩이라는 명칭을 부여한 것입니다.
소프트웨어적 스레드
소프트웨어적으로 정의된 스레드는 ‘하나의 프로그램에서 독립적으로 실행되는 단위’를 의미합니다.
프로그래밍 언어나 운영체제를 학습할 때 접하는 스레드는 보통 이렇게 소프트웨어적으로 정의된 스레드를 의미합니다.
하나의 프로그램은 실행되는 과정에서 한 부분만 실행될 수도 있지만, 프로그램의 여러 부분이 동시에 실행될 수도 있습니다.
가령 워드 프로세서 프로그램을 개발한다고 가정해봅시다.
그리고 아래의 기능이 동시에 수행되길 원한다고 해 봅시다.
사용자로부터 입력받은 내용을 화면에 보여 주는 기능
사용자가 입력한 내용이 맞춤법에 맞는지 검사하는 기능
사용자가 입력한 내용을 수시로 저장하는 기능
이 기능들을 작동시키는 코드를 각각의 스레드로 만들면 동시에 실행할 수 있습니다.
정리하면, 스레드의 하드웨어적 정의는 ‘하나의 코어가 동시에 처리하는 명령어의 단위’를 의미하고, 소프트웨어적 정의는 ‘하나의 프로그램에서 독립적으로 실행되는 단위’를 의미합니다.
한 번에 하나씩 명령어를 처리하는 1코어 1스레드 CPU도 소프트웨어적 스레드를 수십 개 실행할 수 있습니다.
1 코어 1 스레드 CPU로도 프로그램의 여러 부분을 동시에 실행할 수 있습니다.
만약 스레드의 사전적 정의(실행 흐름의 단위)만을 암기한다면 ‘1코어 1스레드 CPU가 여러 스레드로 만들어진 프로그램을 실행할 수 있다’라는 말이 어려울 겁니다.
이런 이유로 하드웨어적 스레드와 소프트웨어적 스레드는 구분하여 기억하는 것이 좋습니다.
멀티스레드 프로세서
하나의 코어로 여러 명령어를 동시에 처리하는 기술인 하드웨어적 스레드를 “멀티스레드 프로세서” 라고 합니다.
멀티스레드 프로세서는 하나의 코어로 여러 명령어를 동시에 처리하는 CPU라고 했었습니다. 어떨게 이런 일이 가능할까요?
“멀티스레드 프로세서” 를 실제로 설계하는 일은 매우 복잡하지만, 가장 큰 핵심은 레지스터입니다.
하나의 코어로 여러 명령어를 동시에 처리하도록 만들려면 프로그램 카운터, 스택 포인터, 메모리 버퍼 레지스터, 메모리 주소 레지스터와 같이 하나의 명령어를 처리하기 위해 꼭 필요한 레지스터를 여러개 가지고 있으면 됩니다.
가열 프로그램 카운터가 두 개 있다면 ‘메모리에서 가져올 명령어 주소’를 두 개 지정할 수 있을 것이고, 스택 포인터가 두 개 있다면 두 개의 스택을 관리할 수 있을것 입니다.
아래의 그림을 봅시다.
하나의 명령어를 실행하기 위해 꼭 필요한 레지스터들을 편의상 ‘레지스터 세트’라고 표기했습니다.
레지스터 세트가 한 개인 CPU는 한 개의 명령어를 처리하기 위한 정보들을 기억할 뿐이지만, 레지스터 세트가 두 개인 CPU는 두 개의 명령어를 처리하기 위한 정보들을 기억할 수 있습니다.
여기서 ALU와 제어장치가 두 개의 레지스터 세트에 저장된 명령어를 해석하고 실행하면 하나의 코어에서 두 개의 명령어가 동시에 실행됩니다.
하드웨어 스레드를 이용해 하나의 코어로도 여러 명령어를 동시에 처리할 수 있습니다.
그러나 메모리 속 프로그램 입장에서 봤을 때 하드웨어 스레드는 마치 ‘한 번에 하나의 명령어를 처리하는 CPU’나 다름없습니다.
가령 2코어 4스레드 CPU는 한 번에 네 개의 명령어를 처리할 수 있는데, 프로그램 입장에서 봤을 땐 한 번에 하나의 명령어를 처리하는 CPU가 네 개 있는 것처럼 보입니다.
그래서 하드웨어 스레드를 논리 프로세서(logical processor) 라고 부르기도 합니다.
“코어” 는 명령어를 실행할 수 있는 ‘하드웨어 부품’이고, “스레드” 는 ‘명령어를 실행하는 단위’입니다.
“멀티코어 프로세서” 는 명령어를 실행할 수 있는 하드웨어 부품이 CPU 안에 두 개 이상 있는 CPU를 의미하고, “멀티스레드 프로세서” 는 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 의미합니다.
키워드로 정리하는 핵심 포인트
클럭 속도가 높은 CPU는 빠르게 작동합니다.
코어 란 CPU 내에서 명령어를 실행하는 부품입니다.
멀티코어 프로세서란 여러 개의 코어를 포함하는 CPU를 말합니다.
스레드에는 하드웨어적 스레드와 소프트웨어적 스레드가 있습니다.
멀티스레드 프로세서란 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 말합니다.
-
-
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
래퍼 클래스 - 주요 메서드와 성능.
래퍼 클래스 - 주요 메서드.
래퍼 클래스가 제공하는 주요 메서드를 알아봅시다.
package lang.wrapper;
public class WrapperUtilsMain {
public static void main(String[] args) {
Integer i1 = Integer.valueOf(10); // 숫자, 래퍼 객체 변환.
Integer i2 = Integer.valueOf("10"); // 문자열, 래퍼 객체 변환.
int intValue = Integer.parseInt("10"); // 문자열 전용, 기본형 변환.
// 비교
int compareResult = i1.compareTo(20);
System.out.println("compareResult = " + compareResult);
// 산술 연산
System.out.println("sum: " + Integer.sum(10, 20));
System.out.println("min: " + Integer.min(10, 20));
System.out.println("max: " + Integer.max(10, 20));
}
}
실행 결과
compareResult = -1
sum: 30
min: 10
max: 20
valueOf(): 래퍼 타입을 반환합니다. 숫자, 문자열을 모두 지원합니다.
parseInt(): 문자열을 기본형으로 변환합니다.
compareTo(): 내 값과 인수로 넘어온 값을 비교합니다. 내 값이 크면 1, 같으면 0, 내 값이 작으면 -1을 반환합니다.
Integer.sum(), Integer.min(), Integer.max(): static 메서드 입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다.
pareInt() vs valueOf()
원하는 타입에 맞는 메서드를 사용하면 됩니다.
valueOf("10")는 래퍼 타입을 반환합니다.
parseInt("10")는 기본형을 반환합니다.
Long.parseLong() 처럼 각 타입에 parseXxx()가 존재합니다.
래퍼 클래스와 성능
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.
그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?
다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다.
package lang.wrapper;
public class WrapperVsPrimitive {
public static void main(String[] args) {
int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
long startTime, endTime;
// 기본형 long 사용
long sumPrimitive = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumPrimitive += i;
}
endTime = System.currentTimeMillis();
System.out.println("sumPrimitive = " + sumPrimitive);
System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
// 래퍼 클래스 Long 사용
Long sumWrapper = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumWrapper += i; // 오토 박싱 발생
}
endTime = System.currentTimeMillis();
System.out.println("sumWrapper = " + sumWrapper);
System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");
}
}
단순히 값을 반복해서 10억 번 더합니다.
기본형 long에 더하는 것과 래퍼 클래스 Long에 더하는 부분으로 나누어 테스트 합니다. 결과 값은 같습니다.
실행 결과 - M1 맥북 기준
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 381ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 1640ms
기본형 연산이 래퍼 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다. 참고로 계산 결과는 시스템 마다 다릅니다.
기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다. 예를 들어 int 는 보통 4바이트의 메모리를 사용합니다.
래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다. 자바 버전과 시스템마다 다르지만 대략 8-16바이트의 메모리를 추가로 사용합니다.
기본형, 래퍼 클래스 어떤 것을 사용?
이 연산은 10억 번의 연산을 수행했을 때 0.3초와, 1.5초의 차이입니다.
기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘다 매우 빠르게 연산이 수행됩니다.
0.3초 나누기 10억, 1.5초 나누기 10억이다.
일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다.
CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려합시다.
그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다.
유지보수 vs 최적화
유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다.
특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다.
코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다.
특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다.
권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
-
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 2
래퍼 클래스 - 기본형의 한계 2
기본형과 null
기본형은 항상 값을 가져야 합니다.
하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain0 {
public static void main(String[] args) {
int[] intArr = {-1, 0, 1, 2, 3};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // -1
}
private static int findValue(int[] intArr, int target) {
for (int value : intArr) {
if (value == target) {
return value;
}
}
return -1;
}
}
findValue()는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환합니다.
findValue()는 결과로 int를 반환합니다.
int와 같은 기본형은 항상 값이 있어야 합니다.
여기서도 값을 반환할 때 값을 찾지 못하면 숫자 중 하나를 반환해야 하는데 보통 -1 또는 0을 사용합니다.
실행 결과
-1
0
1
-1
실행 결과를 보면 입력값이 -1일 때 -1을 반환합니다.
그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환합니다.
입력값이 -1일 때를 생각해보면, 배열에 -1 값이 있어서 -1을 반환한 것인지, 아니면 -1 값이 없어서 -1을 반환한 것인지 명확하지 않습니다.
객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain1 {
public static void main(String[] args) {
MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // null
}
private static MyInteger findValue(MyInteger[] intArr, int target) {
for (MyInteger myInteger : intArr) {
if (myInteger.getValue() == target) {
return myInteger;
}
}
return null;
}
}
실행 결과
-1
0
1
null
앞서 만든 MyInteger 래퍼 클래서를 사용했습니다.
실행 결과를 보면 -1을 입력했을 때는 -1을 반환합니다.
100을 입력했을 때는 값이 없다는 null을 반환합니다.
기본형은 항상 값이 존재해야 합니다.
숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.
반면에 객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.
물론 null 값을 반환하는 경우 잘못하면 NullPointerException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
-
-
-
📝 [TIL] 240415 Today I Learned.
1. 리눅스 명령어
pwd(print working directory)
~ 은 Home이라는 경로
ls(list): 내 폴더 안에 있는 폴더 & 파일 내역을 보여줌
la -a(list all): 숨겨진 파일(보통 .으로 시작함)도 모두 볼 수 있음
cd 폴더명(change directory): 폴더 위치 이동
ls 명령어에서 확인된 폴더로 이동 가능
cd .. : 한 단계 위의 폴더라는 뜻
mkdir(make directory): 현재 경로에서 폴더를 생성
touch: 현재 경로에서 파일을 생성하는 명령어
정확히는 파일의 생성과 파일의 날짜, 시간을 변경하는 명령어
2.git
코드 변경점을 기록하는 것
버전 관리 도구(형상 관리 도구)
소프트웨어의 변경사항을 체계적으로 추적하고 통제하는 것
3. github
백업과 공유가 가능한 온라인 코드 저장소
협업이 가능한 온라인 코드 저장소
4. git 필수 명령어
코드 관리를 시작하는 명령어 - git init
초기화하다, 초기 세팅하다의 준말
프로젝트 시작 전 딱 한 번만 입력하면 됨
정확한 프로젝트 폴더(경로)에서 입력해야 함
코드를 저장하는 명령어 - git add & commit
git add 파일명: 저장하기 전 저장할 파일 “지정”
git commit -m “메세지 작성”: 실제로 파일을 “저장”
저장 여부 확인하는 명령어 - git status
내 프로젝트의 변경사항을 한 번에 지정하는 법 - git add .
working directory, staging area, repository에 대해서 알아봅시다.
저장 내역을 확인하는 명령어 - git log
커밋 메시지로 코드 변경점 추측 가능
git diff 코드 변경 확인
git reset 과거로 돌아가기 가능
변경 사항을 원격 저장소(예: github등)에 업로드 하는 명령어 - git push
git push <원격 저장소 이름> <브랜치 이름>
git push origin main
원격 저장소의 내용을 복사하여 새로운 로컬 저장소를 생성하는 데 사용하는 명령어 - git clone
새로운 프로젝트에 참여하거나 기존 프로젝트의 소스 코드를 로컬 컴퓨터로 가져오고 싶을 때 사용.
원격 저장소의 모든 파일, 디렉터리, 버전 기록을 포함합니다. 이를 통해 원격 저장소의 정확한 복사본을 로컬에 생성할 수 있습니다.
원격 저장소에 설정된 브랜치, 원격 추적 정보 등이 자동으로 설정됩니다. 이는 로털에서 작업을 시작하기 위해 필요한 초기 설정을 간소화합니다.
git clone <원격 저장소 URL>
특정 브랜치 클론: git clone -b <브랜치 이름> <원격 저장소 URL>
원격 저장소에서 최신 변경사항을 가져와서 현재 로컬 브랜치와 병합하는 데 사용하는 명령어 - git pull
이 명령은 git fetch와 git merge 두 단계의 작업을 한 번에 수행합니다.
이 명령어의 사용은 특히 팀 환경에서 다른 사람들의 작업을 지속적으로 로컬 환경에 통합할 필요가 있을 때 매우 유용합니다.
자세히 설명
‘git fetch’ 단계 : 이 단계에서는 원격 저장소의 최신 데이터를 로컬 저장소로 가져오지만, 현재 작업 중인 로컬 브랜치에는 자동으로 병합되지 않습니다. 원격 저장소의 변경사항은 로컬의 원격 추적 브랜치에 저장됩니다.
‘git merge’ 단계 : ‘git fetch’ 로 가져온 변경사항을 현재 작업 중인 브랜치와 병합합니다. 이 병합 과정을 통해 로컬 코드베이스에 원격 저장소의 최신 변경사항이 반영됩니다.
git pull <원격 저장소 이름> <브랜치 이름>
예를 들어, 원격 저장소 origin의 main 브랜치에서 최신 변경사항을 가져오고 싶다면 다음 명령어를 사용합니다.
git pull origin main
주의사항 및 활용 팁
자동 병합 충돌 : git pull 을 실행할 때 로컬에서 아직 커밋되지 않은 변경사항이 있다면, 원격의 변경사항과 충돌이 발생할 수 있습니다. 이 경우, Git은 사용자에게 충돌을 해결하고 커밋할 것을 요청합니다.
명시적인 병합 옵션 사용 : 병합 방식을 제어하고 싶을 때는 ’–rebase’ 옵션을 사용하여 기존 커밋 위에 원격 변경사항을 재배치할 수 있습니다. 이는 커밋 히스토리를 더 깔끔하게 유지하는 데 도움을 줍니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 1
래퍼 클래스 - 기본형의 한계 1
기본형의 한계
자바는 객체 지향 언어입니다.
그런데 자바 안에 객체가 아닌 것이 있습니다.
바로 int, double 같은 기본형(Primitive Type)입니다.
기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다.
객체가 아님: 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다. 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없습니다.
추가로 객체 참고가 필요한 컬렉션 프레임워크를 사용할 수 없습니다. 그리고 제네릭도 사용할 수 없습니다.
null 값을 가질 수 없음: 기본형 데이터 타입은 null 값을 가질 수 없습니다. 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다.
기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해봅시다.
왼쪽의 값이 더 작다 -1
두 값이 같다 0
왼쪽의 값이 더 크다 1
package lang.wrapper;
public class MyIntegerMethodMain0 {
public static void main(String[] args) {
int value = 10;
int i1 = compareTo(value, 5);
int i2 = compareTo(value, 10);
int i3 = compareTo(value, 20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
public static int compareTo(int value, int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
여기서는 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교합니다.
그런데 자기 자신인 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용됩니다.
이런 경우 만약 value가 객체라면 value 객체 스스로 가지 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다.
직접 만든 래퍼 클래스.
int를 클래스로 만들어 봅시다.
int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다.
다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다.
이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다.
package lang.wrapper;
public class MyInteger {
private final int value;
public MyInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public int compareTo(int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
@Override
public String toString() {
return String.valueOf(value);
}
}
MyInteger는 int value라는 단순한 기본형 변수를 하나 가지고 있습니다.
그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공합니다.
앞에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했습니다.
이 클래스는 불변으로 설계했습니다.
MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다.
덕분에 데이터 조각에 불과한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다.
package lang.wrapper;
public class MyIntegerMethodMain1 {
public static void main(String[] args) {
MyInteger myInteger = new MyInteger(10);
int i1 = myInteger.compareTo(5);
int i2 = myInteger.compareTo(10);
int i3 = myInteger.compareTo(20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
myInteger.compareTo()는 자기 자신의 값을 외부의 값과 비교합니다.
MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있습니다.
참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
-
💾 [CS] 명령어 사이클과 인터럽트
명령어 사이클과 인터럽트.
명령어 사이클 : CPU가 하나의 명령어를 처리하는 과정에는 어떤 정해진 흐름이 있고, CPU는 그 흐름을 반복하며 명령어를 처리해 나갑니다. 이렇게 하나의 명령어를 처리하는 정형화된 흐름을 “명령어 사이클” 이라고 합니다.
인터럽트 : CPU는 정해진 흐름에 따라 명령어를 처리해 나가지만, 이 흐름이 끊어지는 상황이 발생합니다. 이를 “인터럽트” 라고 합니다.
명령어 사이클
프로그램은 수많은 명령어로 이루어져있고, CPU는 이 명령어들을 하나씩 실행합니다.
이때 프로그램 속 각각의 명령어들은 일정한 주기가 반복되며 실행되는데, 이 주기를 명령어 사이클(instruction cycle) 이라고 합니다.
즉, 프로그램 속 각각의 명령어들은 명령어 사이클이 반복되며 실행됩니다.
메모리에 저장된 명령어 하나를 실행한다고 가정해 봅시다.
가장 먼저 해야할 것은 명령어를 메모리에서 CPU로 가져와야 합니다.
이게 명령어 사이클의 첫 번째 과정입니다.
인출 사이클(fetch cycle) : 메모리에 있는 명령어를 CPU로 가지고 오는 단계.
CPU로 명령어를 인출했다면 이제 명령어를 실행합니다.
이것이 명령어 사이클의 두 번째 과정입니다.
실행 사이클(execution cycle) : CPU로 가져온 명령어를 실행하는 단계, 제어장치가 명령어 레지스터에 담긴 값을 해석하고, 제어 신호를 발생시키는 단계.
프로그램을 이루는 수많은 명령어는 일반적으로 인출과 실행 사이클을 반복하며 실행됩니다.
즉, CPU는 프로그램 속 명령어를 가져오고 실행하고, 또 가져오고 실행하고를 반복하는 것입니다.
하지만 모든 명령어가 이렇게 간단히 실행되는 건 아닙니다.
명령어를 인출하여 CPU로 가져왔다하더라도 곧바로 실행할 수 없는 경우도 있기 때문입니다.
예를 들어 간접 주소 지정 방식을 생각해 봅시다.
간접 주소 지정 방식은 오퍼랜드 필드에 유효 주소의 주소를 명시한다고 했습니다.
이 경우 명령어를 인출하여 CPU로 가져왔다 하더라도 바로 실행 사이클에 돌입할 수 없습니다.
명령어를 실행하기 위해서는 메모리 접근을 한 번 더 해야 하기 때문입니다.
이 단계를 간접 사이클(indirect cycle) 이라고 합니다.
인터럽트.
프로그램을 개발하다 보면 아래 인터럽트라는 단어를 쉽게 접할 수 있습니다.
인터럽트는 영어로 interrupt이며, ‘방해하다, 중단시키다’를 의미합니다.
즉, CPU가 수행 중인 작업은 방해를 받아 잠시 중단될 수 있는데, 이렇게 CPU의 작업을 방해하는 신호를 인터럽트(interrupt) 라고 합니다.
CPU가 작업을 잠시 중단해야 할 정도라면 인터럽트는 ‘CPU가 꼭 주목해야 할 때’ 혹은 ‘CPU가 얼른 처리해야 할 다른 작업이 생겼을 때’ 발생합니다.
인터럽트의 종류에는 크게 동기 인터럽트와 비동기 인터럽트가 있습니다.
동기 인터럽트(synchronous interrupt) : CPU에 의해 발생하는 인터럽트입니다.
CPU가 명령어들을 수행하다가 예상치 못한 상황에 마주쳤을 때, 가령 CPU가 실행하는 프로그래밍상의 오류와 같은 예외적인 상황에 마추쳤을 때 발생하는 인터럽트입니다.
이런 점에서 동기 인터럽트는 예외(execption) 라고 부릅니다.
비동기 인터럽트(asynchronous interrupt) : 주로 입출력장치에 의해 발생하는 인터럽트입니다.
입출력장치에 의한 비동기 인터럽트는 세탁기 완료 알리므 전자레인지 조리 완료 알림과 같은 알림 역할을 합니다.
구체적으로 다음과 같이 사용됩니다.
CPU가 프린터와 같은 입출력장치에 입출력 작업을 부탁하면 작업을 끝낸 입출력장치가 CPU에 완료 알림(인터럽트)을 보냅니다.
키보드, 마우스와 같은 입출력 장치가 어떠한 입력을 받아들였을 때 이를 처리하기 위해 CPU에 입력 알림(인터럽트)을 보냅니다.
하드웨어 인터럽트
하드웨어 인터럽트는 알림과 같은 인터럽트 입니다.
CPU는 입출력 작업 도중에도 효율적으로 명령어를 처리하기 위해 이런 알림과 같은 하드웨어 인터럽트를 사용합니다.
하드웨어 인터럽트를 이용하면 CPU는 주기적으로 하드웨어 완료 여부를 확인할 필요가 없습니다.
CPU는 하드웨어로부터 하드웨어 완료 인터럽트를 받을 때까지 다른 작업을 처리할 수 있습니다.
이렇듯 하드웨어 인터럽트는 입출력 작업 중에도 CPU로 하여금 효율적으로 명령어를 처리할 수 있게 합니다.
하드웨어 인터럽트 처리 순서
입출력장치는 CPU에 인터럽트 요청 신호를 보냅니다.
CPU는 실행 사이클이 끝나고 명령어를 인출하기 전 항상 인터럽트 여부를 확인합니다.
CPU는 인터럽트 요청을 확인하고 인터럽트 플래그를 통해 현재 인터럽트를 받아들일 수 있는지 여부를 확인합니다.
인터럽트를 받아들일 수 있다면 CPU는 지금까지의 작업을 백업합니다.
CPU는 인터럽트 백터를 참조하여 인터럽트 서비스 루틴을 실행합니다.
인터럽트 서비스 루틴이 끝나면 4에서 백업해 둔 작업을 복구하여 실행을 재개합니다.
인터럽트 요청 신호 : 인터럽트는 CPU의 정상적인 실행 흐름을 끊는 것이기에 다른 누군가가 인터럽트하기 전에 “지금끼어들어도 되나요?” 하고 CPU에 물어봐야 합니다. 이를 인터럽트 요청 신호라고 합니다.
이때, CPU가 인터럽트 요청을 수용하기 위해서는 플래그 레지스터의 인터럽트 플래그(interrupt flag) 가 활성화되어 있어야 합니다.
인터럽트 플래그는 말 그래도 하드웨어 인터럽트를 받아들일지, 무시할지를 결정하는 플래그입니다.
CPU가 중요한 작업을 처리해야 하거나 어떤 방해도 받지 않아야 할 때 인터럽트 플래그는 불가능으로 설정됩니다.
만약 인터럽트 플래그가 ‘불가능’으로 설정되어 있다면 CPU는 인터럽트 요청이 오더라도 해당 요청을 무시합니다.
반대로 인터럽트 플래그가 ‘가능’으로 설정되어 있다면 CPU는 인터럽트 요청 신호를 받아들이고 인터럽트를 처리합니다.
다만, 모든 하드웨어 인터럽트를 인터럽트 플래그로 막을 수 있는 것은 아닙니다.
인터럽트 플래그가 불가능으로 설정되어 있을지라도 무시할 수 없는 인터럽트 요청도 있습니다.
무시할 수 없는 하드웨어 인터럽트 가장 우선순위가 높은, 다시 말해 반드시 가장 먼저 처리해야 하는 인터럽트입니다.
정전이나 하드웨어 고장으로 인한 인터럽트가 이에 해당합니다.
CPU가 인터럽트 요청을 받아들이기로 했다면 CPU는 서비스 루틴이라는 프로그램을 실행합니다.
인터럽트 서비스 루틴(ISB: Interrupt Service Routine): 인터럽트를 처리하기 위한 프로그램. 인터럽트 핸들러(Interrupt handler) 라고도 불립니다.
어떤 인터럽트가 발생했을 때 해당 인터럽트를 어떻게 처리하고 작동해야 할지에 대한 정보로 이루어진 프로그램입니다.
요컨태 ‘CPU가 인터럽트를 처리한다’는 말은 ‘인터럽트 서비스 루틴을 실행하고, 본래 수행하던 작업으로 다시 되돌아온다’ 라는 말과 같습니다.
인터럽트를 처리하는 방법은 입출력장치마다 다르므로 각기 다른 인터럽트 서비스 루틴을 가지고 있습니다.
즉, 메모리에는 위 그림처럼 여러 개의 인터럽트 서비스 루틴이 저장되어 있습니다.
이들 하나하나가 ‘인터럽트가 발생하면 어떻게 행동해야 할지를 알려주는 프로그램’이라고 보면 됩니다.
인터럽트 벡터(Interrupt vector) : CPU는 수많은 인터럽트 서비스 루틴을 구분하기 위해 인터럽트 벡터를 이용합니다. 인터럽트 서비스 루틴을 식별하기 위한 정보입니다.
인터럽트 벡터를 알면 인터럽트 서비스 루틴의 시작 주소를 알 수 있기 때문에 CPU는 인터럽트 벡터를 통해 특정 인터럽트 서비스 루틴을 처음부터 실행할 수 있습니다.
CPU는 하드웨어 인터럽트 요청을 보낸 대상으로부터 데이터 버스를 통해 인터럽트 벡터를 전달받습니다.
가령, CPU가 작업을 수행하는 도중 키보드 인터럽트가 발생한 경우라면 CPU는 인터럽트 벡터를 참조하여 키보드 인터럽트 서비스 루틴의 시작 주소를 알아내고, 이 시작 주소부터 실행해 나가며 키보드 인터럽트 서비스 루틴을 실행합니다.
정리하면 ‘CPU가 인터럽트를 처리한다’는 말은 ‘인터럽트 서비스 루틴을 실행하고, 본래 수행하던 작업으로 다시 되돌아온다’는 말과 같습니다.
그리고 CPU가 인터럽트 서비스 루틴을 실행하려면 인터럽트 서비스 루틴의 시작 주소를 알아야 하는데, 이는 인터럽트 벡터를 통해 알 수 있습니다.
인터럽트 서비스 루틴은 여느 프로그램과 마찬가지로 명령어와 데이터로 이루어져 있습니다.
그렇기에 인터럽트 서비스 루틴도 프로그램 카운터를 비롯한 레지스터들을 사용하며 실행됩니다.
그럼, 인터럽트가 발생하기 전까지 레지스터에 저장되어 있던 값들은 어떻게 할까요?
인터럽트 요청을 받기 전까지 CPU가 수행하고 있었던 일은 인터럽트 서비스 루틴이 끝나면 되돌아와서 마저 수행을 해야 하기 때문에 지금까지의 작업 내역들은 어딘가에 백업을 해둬야 합니다.
그렇기에 CPU는 인터럽트 서비스 루틴을 실행하기 전에 프로그램 카운터 값 등 현재 프로그램을 재개하기 위해 필요한 모든 내용을 스택에 백업합니다.
그러고 나서 인터럽트 서비스 루틴의 시작 주소가 위치한 곳으로 프로그램 카운터 값을 갱신하고 인터럽트 서비스 루틴을 실행합니다.
인터럽트 서비스 루틴을 모두 실행하면, 다시 말해 인터럽트를 처리하고 나면 스택에 저장해 둔 값을 다시 불러온 뒤 이전까지 수행하던 작업을 재개합니다.
키워드 정리
인터럽트 요청 신호 : CPU의 작업을 방해하는 인터럽트에 대한 요청
인터럽트 플래그 : 인터럽트 요청 신호를 받아들일지 무시할지를 결정하는 비트
인터럽트 벡터 : 인터럽트 서비스 루틴의 시작 주소를 포함하는 인터럽트 서비스 루틴의 식별 정보
인터럽트 서비스 루틴 : 인터럽트를 처리하는 프로그램
CPU는 이와 같은 과정을 반복해 나가며 프로그램을 실행한다고 볼 수 있습니다.
키워드로 정리하는 핵심 포인트
명령어 사이클은 하나의 명령어가 처리되는 주기로, 인출, 실행, 간접, 인터럽트 사이클로 구성되어 있습니다.
인터럽트 는 CPU의 정상적인 작업을 방해하는 신호입니다.
인터럽트의 종류에는 예외와 하드웨어 인터럽트가 있습니다.
인터럽트 서비스 루틴은 인터럽트를 처리하기 위한 동작들로 이루어진 프로그램입니다.
-
☕️[Java] String 클래스 - 정리
String 클래스 - 정리
1. 자바에서 문자를 다루는 대표적인 타입
char
String
char 배열을 사용하면 문자열을 다룰 수 있으나 불편합니다.
그래서 String이라는 것을 Java에서 제공해줍니다.
2. String 클래스를 통해 문자열을 생성하는 2가지 방법.
String str1 = "hello"; // 방법 1 -> 문자열 리터럴
String str2 = new String("hello"); // 방법 2
쌍따옴표 사용: 방법 1
객체 생성 : 방법 2
문자열 리터럴을 사용하더라도 결국 new String("hello"); 가 되는 것 입니다.
편의상 쌍따옴표로 문자열을 감싸면(문자열 리터럴) 자바 언어에서 new String("hello"); 와 같이 변경해 줍니다.
이 경우 실제로는 성정 최적화를 위해 문자열 풀을 사용합니다.
문자열은 참조형입니다.
3. String 클래스 내부
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
3. String 클래스 비교.
String 클래스를 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야합니다.
동일성(Identity) : == 연산자를 사용해서 두 객체의 참조(Reference) 가 동일한 객체를 가리키고 있는지 확인합니다.
동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인합니다.
4. 문자열 리터럴, 문자열 풀.
String str3 = "hello" 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용합니다.
자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어 둡니다.
이때 같은 문자열이 있으면 만들지 않습니다.
String str3 - "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾습니다.
그리고 찾은 인스턴스의 참조(x003)를 반환합니다.
String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용합니다.
문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있습니다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공합니다.
참고
풀(Pool)은 자원이 모여있는 곳을 의미합니다.
프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻합니다.
여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적입니다.
대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화할 수 있습니다.
참고로 문자열 풀은 힙 영역을 사용합니다.
그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있습니다.
5. String 클래스는 불변객체.
String은 불변 객체입니다.
따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없습니다.
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1.concat(" java");
System.out.println("str1 = " + str1);
System.out.println("str1 = " + str2);
}
String은 불변 객체입니다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환합니다.
실행 결과
str1 = hello
str2 = hello java
String.concat()은 내부에서 새로운 String 객체를 만들어서 반환합니다.
따라서 불변과 기존 객체의 값을 유지합니다.
문자열에서 무언가 변경하는 것이 있으면 그 내부에서 인스턴스를 생성하여 반환합니다.
때문에 반환값을 받아서 사용해야 합니다.
6. String 클래스가 불변으로 설계된 이유.
사이드 이팩트 문제.
문자열 풀을 사용시에 불변으로 설계되어 있어야 안전하게 사용할 수 있기 때문입니다.
7. String 클래스 주요 메서드.
주요 메서드 블로그 글 1
주요 메서드 블로그 글 2
참고. CharSequence
CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
8. StringBuilder - 가변 String
불변인 String 클래스의 단점: 뭔가 값을 더하거나 변경하거나 할 때마다 계속 새로운 객체를 만들어내야 한다는 점 입니다. 때문에 성능도 느려질 수 있습니다. (물론, 불변이기 때문에 안전하기는 합니다.)
이러한 단점들 때문에 StringBuilder 가 있습니다.
StringBuilder: 가변 String 입니다.
값을 쭉 바꾸고 쓰면 되는데, 마지막에는 다시 String으로 바꾸는 toString()을 사용합니다.
즉, 다시 안전한 불변으로 바꾸는 작업입니다.
변경이 필요할 때 가변으로, 마지막에는 불변으로 바꿔서 쓰는것을 권장합니다.
“뭔가 문자열을 변경할 일이 많다” -> StringBuilder 를 사용하면 됩니다.
9. String 최적화.
그러나 생각보다 StringBuilder를 사용할 때가 많이 없습니다.
그 이유는 자바가 String을 최적화하기 때문입니다.
컴파일러에서도 최적화를 하고, 변수로 되어있어도 컴파일러가 최적화를 수행합니다.
예를 들어 Java가 직접 StringBuilder를 사용합니다.
String result = new StringBuilder().append(str1).append(str2).toString();
10. String 최적화가 어려운 경우.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 1000000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
이럴 때는 직접 StringBuilder 를 사용하면 됩니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
String result = sb.toString();
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
11. 메서드 체이닝.
자기 자신의 값을 반환해서 메서드를 쭉 연결해서 사용할 수 있습니다.
자바의 많은 라이브러리들이 메서드 체이닝 기법을 사용하고 있습니다.
메서드 체이닝 블로그 글
-
☕️[Java] String 최적화
String 최적화.
자바의 String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐줍니다.
문자열 리터럴 최적화
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행합니다.(최적화 방식은 자바 버전에 따라 달라집니다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다.
대신에 문자열을 더하기 연산(+)을 사용하면 충분합니다.
String 최적화가 어려운 경우
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
package lang.string.builder;
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대락 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java ...
time = 2528ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.5초가 걸렸습니다.
이럴 때는 직접 StringBuilder를 사용하면 됩니다.
package lang.string.builder;
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java ...
time = 4ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.004초가 걸렸습니다.
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
☕️[Java] 메서드 체이닝 - Method Chaining
메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해서 알아봅시다.
package lang.string.chaining;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다. 이 부분을 유의해서 봅시다.
package lang.string.chaining;
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력합니다.
여기서는 add() 메서드의 반환값은 사용하지 않았습니다.
이번에는 add() 메서드의 반환값을 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같습니다.
adder.add(1)을 호출합니다.
add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다.
adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다.
add()메서드는 자기 자신(this) 메서드의 참조값을 반환합니다.
이 반환값을 adder1, adder2, adder3에 보관했습니다.
따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다.
왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 더 잘 읽히지 않습니다.
이런 방식을 왜 사용하는 것 일까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다.
이 반환된 참조값을 변수에 담아두지 않아도 됩니다.
대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다.
다음과 같은 순서로 실행됩니다.
adder.add(1).add(2).add(3).getValue(); // value = 0
x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다.
x001.getValue(); // value = 6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다.
코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다.
마치 메서드가 체인으로 연결된 것 처럼 보입니다.
이러한 기법을 메서드 체이닝이라고 합니다.
물론 실행 결과도 기존과 동일합니다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다. 예) adder.add(1), adder.add(2)
매서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다. 예) insert(), delete(), reverse()
앞서 StringBuilder를 사용한 코드는 다음과 같이 개선할 수 있습니다.
package lang.string.builder;
public class StringBuilderMain1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4,8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
정리
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리합니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
💾 [CS] 레지스터
레지스터.
프로그램 속 명령어와 데이터는 실행 전후로 반드시 레지스터에 저장됩니다.
따라서 레지스터에 저장된 값만 잘 관찰해도 프로그램의 실행 흐름을 파악할 수 있습니다
다시 말해 레지스터 속 값을 유심히 관찰하면 프로그램을 실행할 때 CPU 내에서 무슨 일이 벌어지고 있는지, 어떤 명령어가 어떻게 수행되는지 알 수 있습니다.
반드시 알아야 할 레지스터.
프로그램 카운터
명령어 레지스터
메모리 주소 레지스터
메모리 버퍼 레지스터
플래스 레지스터
범용 레지스터
스택 포인터
베이스 레지스터
프로그램 카운터.
프로그램 카운터(PC: Program Counter) : 메모리에서 가져올 명령어의 주소, 즉 메모리에서 읽어 들일 명령어의 주소를 저장합니다.
프로그램 카운터를 명령어 포인터(IP: Instruction Pointer) 라고 부르는 CPU도 있습니다.
명령어 레지스터.
명령어 레지스터(IR: Instruction Register) : 해석할 명령어, 즉 방금 메로미에서 읽어 들인 명령어를 저장하는 레지스터입니다.
제어장치는 명령어를 레지스터 속 명령어를 받아들이고 이를 해석한 뒤 제어 신호를 내보냅니다.
메모리 주소 레지스터.
메모리 주소 레지스터(MAR: Memory Address Register) : 메모리 주소를 저장하는 레지스터입니다. CPU가 읽어 들이고자 하는 주소 값을 주소 버스로 보낼 때 메모리 주소 레지스터를 거치게 됩니다.
메모리 버퍼 레지스터.
메모리 버퍼 레지스터(MBR: Memory buffer register) : 메모리와 주고받을 값(데이터와 명령어)을 저장하는 레지스터입니다.
즉, 메모리에 쓰고 싶은 값이나 메모리로부터 전달받은 값은 메모리 버퍼 레지스터를 거칩니다.
CPU가 주소 버스로 내보낼 값이 메모리 주소 레지스터를 거친다면, 데이터 버스로 주고 받을 값은 메모리 버퍼 레지스터를 거칩니다.
메모리 버퍼 레지스터는 메모리 데이터 레지스터(MDR: Memory Data Register)라고도 불립니다.
메모리에 저장된 프로그램을 실행하는 과정에서 프로그램 카운터, 명령어 레지스터, 메모리 주소 레지스터, 메모리 버퍼 레지스터에 어떤 값들이 담기는지 알아봅시다.
1.
CPU로 실행할 프로그램이 1000번지부터 1500번지까지 저장되어 있다고 가정하겠습니다,
그리고 1000번지에는 1101₍₂₎이 저장되어 있다고 가정하겠습니다.
2.
프로그램을 처음부터 실행하기 위해 프로그램 카운터에는 1000이 저장됩니다.
이는 메모리에서 가져올 명령어가 1000번지에 있다는 걸 의미합니다.
3.
1000번지를 읽어 들이기 위해서는 주소 버스로 100번지를 내보내야 합니다.
이를 위해 메모리 주소 레지스터에는 1000이 저장됩니다.
4.
‘메모리 읽기’ 제어 신호와 메모리 주소 레지스터 값이 각각 제어 버스와 주소 버스를 통해 메모리로 보내집니다.
5.
메모리 1000번지에 저장된 값은 데이터 버스를 통해 메모리 버퍼 레지스터로 전달되고, 프로그램 카운터는 증가되어 다음 명령어를 읽어 들일 준비를 합니다.
6.
메모리 버퍼 레지스터에 저장된 값은 명령어 레지스터로 이동합니다.
7.
제어장치는 명령어 레지스터의 명령어를 해석하고 제어 신호를 발생시킵니다.
5단계에서 프로그램 카운터 값이 증가한 것을 확인했습니다.
프로그램 카운터 값이 증가했으니 1000번지 명령어 처리가 끝나면 CPU는 다음 명령어(1001번지)를 읽어 들입니다.
이처럼 프로그램 카운터는 지속적으로 증가하며 계속해서 다음 명령어를 읽어 들일 준비를 합니다.
이 과정이 반복되면서 CPU는 프로그램을 차례대로 실행해 나갑니다.
결국 CPU가 메모리 속 프로그램을 순차적으로 읽어 들이고 실행해 나갈 수 있는 이유는 CPU 속 프로그램 카운터가 꾸준히 증가하기 때문입니다.
범용 레지스터
범용 레지스터(general purpose register) : 다양하고 일반적인 상황에서 자유롭게 사용할 수 있는 레지스터입니다.
메모리 버퍼 레지스터는 테이터 버스로 주고받을 값만 저장하고, 메모리 주소 레지스터는 주소 버스로 내보낼 주소값만 저장하지만, 범용 레지스터는 데이터와 주소를 모두 저장할 수 있습니다.
일반적으로 CPU 안에는 여러 개의 범용 레지스터들이 있고, 현대 대다수 CPU는 모두 범용 레지스터를 가지고 있습니다.
플레그 레지스터
플래그 레지스터(Flag register) : 연산 결과 또는 CPU 상태에 대한 부가적인 정보를 저장하는 레지스터입니다.
특정 레지스터를 이용한 주소 지정 방식(1): 스택 주소 지정 방식.
스택 주소 지정 방식 : 스택과 스택 포인터를 이용한 주소 지정 방식
스택은 한쪽 끝이 막혀 있는 통과 같은 저장 공간입니다.
그래서 스택은 가장 최근에 저장하는 값부터 꺼낼 수 있습니다.
여기서 스택 포인터란 스택의 꼭대기를 가리키는 레지스터입니다.
즉, 스택 포인터는 스택에 마지막으로 저장한 값의 위치를 저장하는 레지스터입니다.
예를 들어 봅시다.
가령 다음과 같이 위에서부터 주소가 매겨져 있고 아래부터 차곡차곡 데이터가 저장되어 있는 스택이 있다고 가정해봅시다.
이때 스택 포인터는 스택의 제일 꼭대기 주소, 즉 4번지를 저장하고 있습니다.
이는 ‘스택 포인터가 스택의 꼭대기를 가리키고 있다’고 볼 수 있겠죠.
쉽게 말해, 스택 포인터는 스택의 어디까지 데이터가 캐워져 있는지에 대한 표시라고 보면 됩니다.
그럼 이 스택에서 데이터를 꺼낼 때는 어떤 데이터부터 꺼내게 될까요?
1 -> 2 -> 3 순서대로 꺼낼 수 있습니다.
여기서 하나의 데이터를 꺼내면 스택에는 2와 3이 남고, 스택의 꼭대기 주소가 달라졌기 때문에 스택 포인터는 5번지를 가리킵니다.
반대로 스택에 데이터를 추가한다면 어떻게 될까요?
현재 스텍세 4라는 데이터를 저장하면 스택의 꼭대기에 4가 저장됩니다.
이때 스택의 꼭대기 주소가 달라졌기 때문에 스택 포인터는 4번지를 가리킵니다.
그런데 스택이라는 것은 도대체 어디에 있는 걸까요?
스택은 메모리 안에 있습니다.
정확히는 메모리 안에 스택처럼 사용할 영역이 정해져 있습니다.
이를 스택 영역이라고 합니다.
이 영역은 다른 주소 공간과는 다르게 스택처럼 사용하기 암묵적으로 약속된 영역입니다.
특정 레지스터를 이용한 주소 지정 방식(2): 변위 주소 지정 방식
변위 주소 지정 방식(displacement addressing mode) : 오퍼랜드 필드의 값(변위)과 특정 레지스터의 값을 더하여 유효 주소를 얻어내는 주소 지정 방식입니다.
그래서 변위 주소 지정방식을 사용하는 명령어는 다음 그림과 같이 연산 코드 필드, 어떤 레지스터의 값과 더할지를 나타내는 레지스터 필드, 그리고 주소를 담고있는 오퍼랜드 필드가 있습니다.
이때, 변위 주소 지정 방식은 오퍼랜드 필드의 주소와 어떤 레지스터를 더하는지에 따라 상대 주소 지정 방식, 베이스 레지스터 주소 지정 방식 등으로 나뉩니다.
상대 주소 지정 방식
상대 주소 지정 방식(relative addressing mode) : 오퍼랜드와 프로그램 카운터와 값을 더하여 유효 주소를 얻는 방식입니다.
프로그램 카운터에는 읽어 들일 명령어의 주소가 저장되어 있습니다.
만약 오퍼랜드가 음수, 가령 -3이였다면 CPU는 읽어 들이기로 한 명령어로부터 ‘세 번째 이전’ 번지로 접근합니다.
한마디로 실행하려는 명령어의 세 칸 이전 번지 명령어를 실행하는 것이지요
반면, 오퍼랜드가 양수, 가열 3이었다면 CPU는 읽어 들이기로 했던 명령어의 ‘세 번째 이후’ 번지로 접근합니다.
즉, 실행하려는 명령어에서 세 칸 건너뛴 번지를 실행하는 겁니다.
상대 주소 지정 방식은 프로그래밍 언어의 if문과 유사하게 모든 코드를 실행하는 것이 아닌, 분기하여 특정 주소의 코드를 실행할 때 사용됩니다.
베이스 레지스터 주소 지정 방식
베이스 레지스터 주소 지정 방식(base-register addressing mode) : 오퍼랜드와 베이스 레지스터의 값을 더하여 유효 주소를 얻는 방식입니다.
여기서 베이스 레지스터는 ‘기준 주소’, 오퍼랜드는 ‘기준 주소로부터 떨어진 거리’로서의 역할을 합니다.
즉, 베이스 레지스터 주소 지정 방식은 베이스 레지스터 속 기준 주소로부터 얼마나 떨어져 있는 주소에 접근할 것인지를 연산하여 유효 주소를 얻어내는 방식입니다.
가령 베이스 레지스터에 200이라는 값이 있고 오퍼랜드가 40이라면 이는 “기준 주소 200번지로부터 40만큼 떨어진 240번지로 접근하라”를 의미합니다.
또 베이스 레지스터에 550이라는 값이 담겨 있고 오퍼랜드가 50이라면 이는 “기준 주소 550번지로부터 50만큼 떨어진 600번지로 접근하라”를 의미하는 명령어 입니다.
키워드로 정리하는 핵심 포인트
프로그램 카운터 는 메모리에서 가져올 명령어의 주소, 명령어 레지스터는 해석할 명령어를 저장합니다.
메모리 주소 레지스터는 메모리의 주소, 메모리 버퍼 레지스터는 메모리와 주고받을 데이터를 저장합니다.
범용 레지스터는 데이터와 주소를 모두 저장하고, 플래그 레지스터는 연산 결과 혹은 CPU 상태에 대한 부가 정보를 저장합니다.
스택 포인터는 스택 최상단의 위치를 저장합니다.
베이스 레지스터에 저장된 주소는 기준 주소로서의 역할을 합니다.
더 알아보기
Jump
Jump 명령어는 프로그램의 실행 흐름을 끊고 지정된 주소로 점프합니다.
Conditional Jump
Conditional Jump 명령어는 특정 조건이 충족될 때에만 주어진 주소로 점프합니다.
Call
Call 명령어는 현재 위치를 저장하고 지정된 주소로 이동합니다.
현재 위치를 저장하기 위해 스택을 사용합니다.
주로 서브루틴(하위 루틴 또는 함수)을 호출할 때 사용됩니다.
Return
Return 명령어는 서브루틴에서 호출자로 복귀합니다.
호출된 서브루틴이 실행을 마치고 호출자로 돌아갈 때 사용됩니다.
-
☕️[Java] String 클래스 - 주요 메서드 2
String 클래스 - 주요 메서드 2
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
package lang.string.method;
public class StringChangeMain2 {
public static void main(String[] args) {
String strWithSpaces = " Java Programming ";
System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase());
System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase());
System.out.println("공백 제거(trim): '" + strWithSpaces.trim() + "'");
System.out.println("공백 제거(strip): '" + strWithSpaces.strip() + "'");
System.out.println("앞 공백 제거(stripLeading): '" + strWithSpaces.stripLeading() + "'");
System.out.println("뒤 공백 제거(stripTrailing): '" + strWithSpaces.stripTrailing() + "'");
}
}
실행 결과
소문자로 변환: java programming
대문자로 변환: JAVA PROGRAMMING
공백 제거(trim): 'Java Programming'
공백 제거(strip): 'Java Programming'
앞 공백 제거(stripLeading): 'Java Programming '
뒤 공백 제거(stripTrailing): ' Java Programming'
문자열 분할 및 조합
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
package lang.string.method;
public class StringSplitJoinMain {
public static void main(String[] args) {
String str = "Apple,Banana,Orange";
// split()
String[] splitStr = str.split(",");
for (String s : splitStr) {
System.out.println(s);
}
String joinStr = "";
for (int i = 0; i < splitStr.length; i++) {
String string = splitStr[i];
joinStr += string;
if (i != splitStr.length-1) {
joinStr += "-";
}
}
System.out.println("joinStr = " + joinStr);
// join()
String joinedStr = String.join("-", "A", "B", "C");
System.out.println("연결된 문자열 = " + joinedStr);
// 문자열 배열 연결
String result = String.join("-", splitStr);
System.out.println("result = " + result);
}
}
실행 결과
Apple
Banana
Orange
joinStr = Apple-Banana-Orange
연결된 문자열 = A-B-C
result = Apple-Banana-Orange
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
package lang.string.method;
public class StringUtilsMain1 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
Object obj = new Object();
String str = "Hello, Java!";
// valueOf 메서드
String numString = String.valueOf(num);
System.out.println("숫자의 문자열 값: " + numString);
String boolString = String.valueOf(bool);
System.out.println("불리언의 문자열 값: " + boolString);
String objString = String.valueOf(obj);
System.out.println("객체의 문자열 값: " + objString);
// 문자 + x -> 문자
String numString2 = "" + num;
System.out.println("빈 문자열 + num: " + numString2);
// toCharArray 메서드
char[] strCharArray = str.toCharArray();
System.out.println("문자열을 문자 배열로 변환 : " + strCharArray);
for (char c : strCharArray) {
System.out.print(c);
}
System.out.println();
}
}
실행 결과
숫자의 문자열 값: 100
불리언의 문자열 값: true
객체의 문자열 값: java.lang.Object@a09ee92
빈 문자열 + num: 100
문자열을 문자 배열로 변환 : [C@30f39991
Hello, Java!
package lang.string.method;
public class StringUtilsMain2 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
String str = "Hello, Java!";
// format 메서드
String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
System.out.println(format1);
String format2 = String.format("숫자: %.2f ", 10.1234);
System.out.println(format2);
//printf
System.out.printf("숫자: %.2f\n", 10.1234);
// matches 메서드
String regex = "Hello, (Java!|World)";
System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
}
}
실행 결과
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true
format 메서드에서 %d는 숫자 %b는 boolean, %s는 문자열을 뜻합니다.
-
☕️[Java] StringBuilder - 가변 String
StringBuilder - 가변 String.
불변인 String 클래스의 단점.
불변인 String 클래스에도 단점이 있습니다.
다음 예를 봅시다.
참고로 실제로 작동하는 코드는 아닙니다.
두 문자를 더하는 경우 다음과 같이 작동합니다.
"A" + "B";
String("A") + String("B"); // 문자는 String 타입.
String("A").concat(String("B")); // 문자의 더하기는 concat을 사용.
new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다.
불변인 String의 내부 값은 변경할 수 없습니다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다.
더 많은 문자를 더하는 경우를 살펴봅시다.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
이 경우 총 3개의 String 클래스가 추가로 생성됩니다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC")는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD")만 사용됩니다.
결과적으로 중간에 만들어진 new String("AB)", new String("ABC")는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점입니다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다.
결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고 : 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
char[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
StringBuilder 사용 예
실제 StringBuilder를 어떻게 사용하는지 확인해 봅시다.
package lang.string.builder;
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4,8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuilder(가변) -> String(불변)으로 바꿀 수 있음.
String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성합니다.
append() 메서드를 사용해 여러 문자열을 추가합니다.
insert() 메서드로 특정 위치에 문자열을 삽입합니다.
delete() 메서드로 특정 범위의 문자열을 삭제합니다.
reverse() 메서드로 문자열을 뒤집습니다.
마지막으로 toString() 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다.
실행 결과
sb = ABCD
sb = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
가변(Mutable) vs 불변(Immutable)
String은 불변합니다. 즉, 한 번 생성되면 그 내용을 변경할 수 없습니다.
따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다.
이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
반면에, StringBuilder는 가변적입니다.
하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않습니다.
이로 인해 메모리 사용을 줄이고 성늘을 향상시킬 수 있습니다. 단 사이드 이펙트를 주의해야 합니다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
-
☕️[Java] String 클래스 - 주요 메서드 1
String 클래스 - 주요 메서드 1
주요 메서드 목록
String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공합니다.
여기서는 자주 사용하는 기능 위주로 나열했습니다.
참고로 기능이 너무 많기 때문에 메서드를 외우기 보다는 주로 사용하는 메서드가 이런 것이 있구나 대략 알아두고, 필요할 때 검색하거나 API 문서를 통해서 원하는 기능을 찾는 것이 좋습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
문자열 분할 및 조합.
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
이제 본격적으로 하나씩 알아봅시다.
참고: CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
package lang.string.method;
public class StringInfoMain {
public static void main(String[] args) {
String str = "Hello, Java!";
System.out.println("문자열의 길이: " + str.length());
System.out.println("문자열이 비어 있는지: " + str.isEmpty());
System.out.println("문자열이 비어 있거나 공백인지 1: " + str.isBlank());
System.out.println("문자열이 비어 있거나 공백인지 2: " + " ".isBlank());
char c = str.charAt(7);
System.out.println("7번째 인덱스의 문자 = " + c);
}
}
실행 결과
문자열의 길이: 12
문자열이 비어 있는지: false
문자열이 비어 있거나 공백인지 1: false
문자열이 비어 있거나 공백인지 2: true
7번째 인덱스의 문자 = J
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
package lang.string.method;
public class StringComparisonMain {
public static void main(String[] args) {
String str1 = "Hello, Java!"; // 대문자 일부 있음
String str2 = "hello, java!";
String str3 = "Hello, World!";
System.out.println("str equals str2: " + str1.equals(str2));
System.out.println("str equalsIgnoreCase str2: " + str1.equalsIgnoreCase(str2));
System.out.println("'a' compareTo 'b': " + "a".compareTo("b"));
System.out.println("'b' compareTo 'a': " + "b".compareTo("a"));
System.out.println("'c' compareTo 'a': " + "c".compareTo("a"));
System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2));
System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello"));
System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
}
}
실행 결과
str equals str2: false
str equalsIgnoreCase str2: true
'a' compareTo 'b': -1
'b' compareTo 'a': 1
'c' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
package lang.string.method;
public class StringSearchMain {
public static void main(String[] args) {
String str = "Hello, Java! Welcome to Java world.";
System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java"));
System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java"));
System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10));
System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java"));
}
}
실행 결과
문자열에 'Java'가 포함되어 있는지: true
'Java'의 첫 번째 인덱스: 7
인덱스 10부터 'Java'의 인덱스: 24
'Java'의 마지막 인덱스: 24
-
-
-
💾 [CS] ALU와 제어장치
ALU와 제어장치.
CPU: 메모리에 저장된 명령어를 읽어 들이고, 해석하고, 실행하는 장치
ALU: CPU 내부에 계산을 담당
레지스터: 명령어를 읽어 들이고 해석하는 제어장치, 작은 임시 저장 장치
ALU
ALU: 레지스터를 통해 피연산자 를 받아들이고, 제어장치로부터 수행할 연산을 알려주는 제어 신호 를 받아 들입니다.
레지스터와 제어장치로부터 받아들인 피연산자와 제어 신호로 산술 연산, 논리 연산 등 다양한 연산을 수행합니다.
ALU가 내보내는 정보.
연산을 수행한 결과는 특정 숫자나 문자가 될 수도 있고, 메모리 주소가 될 수도 있습니다.
그리고 이 결괏값은 바로 메모리에 저장되지 않고 일시적으로 레지스터에 저장됩니다.
CPU가 메모리에 접근하는 속도는 레지스터에 접근하는 속도보다 훨씬 느립니다.
ALU가 연산할 때마다 결과를 메모리에 저장한다면 당연하게도 CPU는 메모리에 자주 접근하게 되고, 이는 CPU가 프로그램 실행 속도를 늦출 수 있습니다.
그래서 ALU의 결괏값을 메모리가 아닌 레지스터에 우선 저장하는 것 입니다.
ALU는 계산 결과와 더불어 플래그를 내보냅니다.
ALU는 결괏값뿐만 아니라 연산 결과에 대한 추가적인 정보를 내보내야 할 때가 있습니다.
연산 결과에 대한 추가적인 상태 정보를 플래그(flag) 라고 합니다.
ALU가 내보내는 대표적인 플래그는 아래와 같습니다.
이러한 플래그는 CPU가 프로그램을 실행하는 도중 반드시 기억해야 하는 일종의 참고 정보입니다.
플래그들은 플래그 레지스터 라는 레지스터에 저장됩니다.
플래그 값들을 저장하는 레지스터입니다.
이 레지스터를 읽으면 연산 결과에 대한 추가적인 정보, 참고 정보를 얻을 수 있습니다.
플레그 레지스터 예시와 설명.
예를 들어 플래그 레지스터가 아래와 같은 구조를 가지고 있고, ALU가 연산을 수행한 직후 부호 플래그가 1이 되었다면 연산 결과는 음수임을 알 수 있습니다.
또한 만약 ALU가 연산을 수행한 직후 플래그 레지스터가 아래와 같다면 제로 플래그가 1이 되었으니 연산 결과는 0임을 알 수 있습니다.
이 밖에도 ALU 내부에는 여러 계산을 위한 회로들이 있습니다.
대표적으로
덧셈을 위한 가산기
뺄셈을 위한 보수기
시프트 연산을 수행해 주는 시프터
오버플로우를 대비한 오버플로우 검출기
등등
제어장치.
제어장치: 제어 신호를 내보내고, 해석하는 부품
제어 신호: 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호
제어장치가 받아들이는 정보.
첫째. 제어장치는 클럭 신호를 받아들입니다.
클럭(Clock): 컴퓨터의 모든 부품을 일사분란하게 움직일 수 있게하는 시간 단위
클럭의 주기에 맞춰 한 레지스터에서 다른 레지스터로 데이터가 이동되거나, ALU에서 연산이 수행되거나, CPU가 메모리에 저장된 명령어를 읽어 들어는 것 입니다.
다만, “컴퓨터의 모든 부품이 클럭 신호에 맞춰 작동한다” 라는 말을 “컴퓨터의 모든 부품이 한 클럭마다 작동한다”라고 이해하면 안됩니다.
컴퓨터 부품들은 클럭이라는 박자에 맞춰 작동할 뿐 한 박자마다 작동하는 건 아닙니다.
가령 다음 그림처럼 하나의 명령어가 여러 클럭에 걸쳐 실행될 수 있습니다.
둘째, 제어장치는 ‘해석해야 할 명령어’를 받아들입니다.
CPU가 해석해야 할 명령어는 명령어 레지스터 라는 특별한 레지스터에 저장됩니다.
제어장치는 이 명령어 레지스터로부터 해석할 명령어를 받아들이고 해석한 뒤, 제어 신호를 발생시켜 컴퓨터 부품들에 수행해야 할 내용을 알려줍니다.
셋째, 제어장치는 플래그 레지스터 속 플래그 값을 받아들입니다.
플래그는 ALU 연산에 대한 추가적인 상태 정보입니다.
제어장치는 플래그 값을 받아들이고 이를 참고하여 제어 신호를 발생 시킵니다.
넷째, 제어장치는 시스템 버스, 그중에서 제어 버스로 전달된 제어 신호를 받아들입니다.
제어 신호는 CPU뿐만 아니라 입출력장치를 비롯한 CPU 외부 장치도 발생시킬 수 있습니다.
제어장치는 제어 버스를 통해 외부로부터 전달된 제어 신호를 받아들이기도 합니다.
제어장치가 내보내는 정보.
여기에는 크게 CPU 외부에 전달하는 제어 신호와 CPU 내부에 전달하는 제어 신호가 있습니다.
제어장치가 CPU 외부에 제어 신호를 전달한다는 말은 곧, 제어 버스로 제어 신호를 내보낸다는 말과 같습니다.
이러한 제어 신호에는 크게 메모리에 전달하는 제어 신호와 입출력장치에 전달하는 제어 신호가 있습니다.
제어장치가 메모리에 저장된 값을 읽거나 메모리에 새로운 값을 쓰고 싶다면 메모리로 제어 신호를 내보냅니다.
그리고 제어장치가 입출력장치의 값을 읽거나 입출력장치에 새로운 값을 쓰고 싶을 때는 입출력장치로 제어 신호를 내보냅니다.
제어장치가 CPU 내부에 전달하는 제어 신호에는 크게 ALU에 전달하는 제어 신호와 레지스터에 전달하는 제어 신호가 있습니다.
ALU에는 수행할 연산을 지시하기 위해, 레지스터에는 레지스터 간에 데이터를 이동시키거나 레지스터에 저장된 명령어를 해석하기 위해 제어 신호를 내보냅니다.
키워드로 정리하는 핵심 포인트
ALU는 레지스터로부터 피연산자를 받아들이고, 제어장치로부터 제어 신호를 받아들입니다.
ALU는 연산 결과와 플래그를 내보냅니다.
제어장치는 클럭, 현재 수행할 명령어, 플래그, 제어 신호를 받아들입니다.
제어장치는 CPU 내부와 외보루 제어 신호 를 내보냅니다.
check point
이진수의 음수표현
2의 보수: 모든 0과 1을 뒤집고, 거기에 1을 더한 값
Q1. ALU가 소프트웨어 개발, 특히 iOS 개발에 어떻게 적용될 수 있는지 설명해 주세요. 예를 들어, 어떻게 ALU가 앱의 성능에 영향을 미칠 수 있는지 구체적인 사례를 들어주세요.
iOS 앱 개발에서 ALU의 역할은 직접적으로 보이지 않지만, 앱의 성능 최적화에 중요합니다. 예를 들어, 이미지 처리나 데이터 암호화 같은 작업은 많은 산술 및 논리 연산을 필요로 하며, 이는 ALU에서 처리됩니다. 따라서, ALU의 효율적인 사용은 앱의 반응 속도와 전반적인 성능에 직접적인 영향을 미칩니다.
Q2. ALU(산술 논리 장치)의 기본적인 기능은 무엇이며, 컴퓨터 프로세서 내에서 어떤 역할을 합니까?
ALU는 컴퓨터의 프로세서 내에 있는 하드웨어 구성 요소로, 기본적인 산술 연산(덧셈, 뺄셈, 곱셈, 나눗셈)과 논리 연산(AND, OR, XOR, NOT)을 수행합니다. 이는 모든 종류의 컴퓨터 프로그램 실행에 기본이 되는 연산이며, 프로세서가 복잡한 계산과 데이터 처리 작업을 수행할 수 있게 해줍니다.
Q3. Java 애플리케이션의 성능 최적화와 관련하여, ALU의 역할과 중요성에 대해 설명해 주세요.
Java 애플리케이션의 성능 최적화에서 ALU의 역할은 중요합니다. ALU는 계산 작업의 실제 수행 장소이므로, ALU의 효율성은 애플리케이션의 처리 속도와 직접적인 관련이 있습니다. 특히, 고성능을 요구하는 애플리케이션에서는 ALU를 통해 수행되는 연산의 최적화가 애플리케이션 전체의 성능을 크게 향상시킬 수 있습니다.
Q4. 멀티 쓰레딩 Java 애플리케이션에서 ALU의 처리 능력이 중요한 이유는 무엇이라고 생각하나요?
멀티 쓰레딩 애플리케이션에서는 여러 쓰레드가 동시에 연산을 수행할 수 있으므로, ALU의 처리 능력이 성능의 병목 현상을 방지하는 데 중요합니다. 효율적인 ALU 설계는 복수의 연산을 동시에 빠르게 처리할 수 있게 해주며, 이는 멀티 쓰레딩 환경에서 애플리케이션의 반응 속도와 처리량을 크게 향상시킬 수 있습니다.
Q5. 현대의 CPU가 여러 ALU를 갖고 있는 경우, 이것이 Java 백엔드 시스템의 성능에 어떤 영향을 미칠 수 있나요?
여러 ALU를 갖는 프로세서는 동시에 여러 연산을 수행할 수 있으므로, Java 백엔드 시스템에서의 병렬 처리 능력을 크게 향상시킵니다. 이는 데이터베이스 쿼리 처리, 대규모 데이터 분석, 실시간 트랜잭션 처리 등 다양한 작업에서 성능 이점을 제공할 수 있습니다.
Q6. Java 애플리케이션에서 복잡한 수학적 연산을 효율적으로 처리하기 위해 개발자가 고려해야 할 ALU와 관련된 측면은 무엇인가요?
개발자는 복잡한 수학적 연산을 효율적으로 처리하기 위해, ALU의 연산 처리 능력을 최대화하는 방법을 고려해야 합니다. 이는 알고리즘의 최적화, 복잡한 연산의 분할 및 정복 전략 적용, 필요한 경우 하드웨어 가속기(예: GPU) 사용 등을 포함할 수 있습니다.
Q7. ALU의 한계를 넘어서서 Java 애플리케이션의 성능을 향상시키기 위해 사용할 수 있는 다른 하드웨어 기반 최적화 기술은 무엇이 있을까요?
ALU의 한계를 넘어서 Java 애플리케이션의 성능을 향상시키기 위해, 다중 코어 프로세싱, 병렬 처리, GPU 가속, FPGA(필드 프로그래밍 게이트 어레이)를 활용한 커스텀 하드웨어 가속 등의 기술을 활용할 수 있습니다. 이러한 기술들은 특정 유형의 작업에 대해 상당한 성능 향상을 제공할 수 있습니다.
-
☕️[Java] String 클래스 - 기본
String 클래스 - 기본.
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
package lang.string;
public class CharArrayMain {
public static void main(String[] args) {
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
System.out.println(charArr);
String str = "hello";
System.out.println("str = " + str);
}
}
실행 결과
hello
str = hello
기본형인 char는 문자 하나를 다룰 때 사용합니다.
char를 사용해서 여러 문자를 나열하려면 char[]을 사용해야 합니다.
하지만 이렇게 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다.
package lang.string;
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
쌍따옴표: "hello"
객체 생성: new String("hello");
String은 클래스입니다.
int, boolean 같은 기본형이 아니라 참조형입니다.
따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다.
따라서 다음 코드는 뭔가 어색합니다.
String str1 = "hello";
문자열은 매우 자주 사용됩니다.
그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해 줍니다.(이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.)
String str1 = "hello"; // 기존
String str1 = new String("hello"); // 변경
String 클래스 구조
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
클래스이므로 속성과 기능을 가집니다.
속성(필드)
private final char[] value;
여기에는 String의 실제 문자열 값이 보관됩니다.
문자 데이터 자체는 char[] 에 보관됩니다.
String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다.
그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다.
참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다.
그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다.
그래서 단순 영어, 숫자로 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용)
그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다.
따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다.
기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다.
기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아봅시다.
주요 메서드는 다음과 같습니다.
length() : 문자열의 길이를 반환합니다.
charAt(inte index) : 특정 인덱스의 문자를 반환합니다.
substring(int beinIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOF(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
String 클래스와 참조형
String은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값들이 들어있는 것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
```java
package lang.string;
public class StringConcatMain {
public static void main(String[] args) {
String a = “hello”;
String b = “ jave”;
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2); } } ``` - 자바에서 문자열을 더할 때는 `String`이 제공하는 `concat()`과 같은 메서드를 사용해야 합니다.
- 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 `+` 연산을 제공합니다.
실행 결과
result1 = hello jave
result2 = hello jave
-
-
-
💉[SQL] REPLACE, SUBSTRING, CONCAT
REPLACE
‘REPLACE’ 함수는 SQL에서 문자열 내의 특정 부분을 다른 문자열로 바꾸고자 할 때 사용됩니다.
이 함수는 데이터 정제나 수정 작업에서 특히 유용하며, 기존 문자열 내의 특정 패턴이나 문자를 찾아 이를 새로운 문자열로 대체하는 기능을 제공합니다.
‘REPLACE’ 는 로그 데이터 정리, 사용자 입력 데이터의 표준화, 데이터 마이그레이션 작업 등 다양한 상황에서 활용될 수 있습니다.
‘REPLACE’ 사용 예
특정 문자열 대체: 고객 데이터에서 전화번호 형식을 변경하고 싶을 때
SELECT REPLACE(phone_number, '-', '') FROM customers;
이 쿼리는 ‘customers’ 테이블의 ‘phone_number’ 열에서 모든 ’-‘ 를 제거합니다.
예를 들어, ‘123-456-7890’ 이라는 전화번호가 있을 경우, ‘1234567890’ 으로 변경됩니다.
데이터 정제: 사용자의 이메일 주소에서 도메인을 변경하고 싶을 때
UPDATE users SET email = REPLACE(email, '@old_domain.com', '@new_domain.com');
이 쿼리는 ‘users’ 테이블의 ‘email’ 열에서 ‘@old_domail.com’ 을 ‘@new_domain.com’ 으로 변경합니다.
텍스트 내용 수정: 상품 설명에서 특정 단어를 새로운 단어로 바꾸고 싶을 때
UPDATE products SET description = REPLACE(description, 'oldword', 'newword');
이 쿼리는 ‘products’ 테이블의 ‘description’ 열에서 ‘oldword’ 를 ‘newword’ 로 변경합니다.
‘REPLACE’ 함수의 특징.
‘REPLACE’ 는 대소문자를 구분하여 작동합니다.
대소문자 구분 없이 대체를 하고자 할 경우, 추가적인 함수나 조건을 사용해야 할 수 있습니다.
문자열 내에서 지정된 패턴이나 문자열을 찾아 모두 대체합니다.
찾고자 하는 문자열이 존재하지 않으면, 원본 문자열이 변경 없이 그대로 반환됩니다.
‘REPLACE’ 함수는 ‘SELECT’, ‘UPDATE’ 등의 쿼리 내에서 사용할 수 있으며, 데이터 조회 또는 수정 작업에 모두 적용할 수 있습니다.
사용 시 고려사항
대량의 데이터를 처리할 때는 ‘REPLACE’ 함수를 사용하는 쿼리의 성능에 주의해야 합니다.
특히 ‘UPDATE’ 작업에서는 대체 작업으로 인해 대량의 데이터가 변경될 수 있으므로, 사전에 작업 범위를 잘 파악하고 필요한 백업을 수행하는 것이 좋습니다.
문자열 대체 작업을 수행할 때는 원치 않는 데이터 변경을 방지하기 위해, 대체할 문자열이 정확히 일치하는지 사전에 확인하는 것이 중요합니다.
‘REPLACE’ 함수는 문자열 데이터를 쉽게 수정하고 정제할 수 있는 강력한 도구로, 데이터베이스 내의 데이터 관리 및 유지보수 작업에 널리 사용됩니다.
SUBSTRING
‘SUBSTRING’ 함수는 SQL에서 문자열의 특정 부분을 추출할 때 사용됩니다.
이 함수는 문자열 데이터 내에서 특정 위치를 기준으로 한 부분 문자열(substring)을 반환하며, 데이터 정제, 특정 형식의 데이터 추출, 또는 문자열 처리 작업에서 매우 유용합니다.
‘SUBSTRING’ 은 로그 분석, 데이터 마이그레이션, 사용자 입력의 특정 부분 처리 등 다양한 상황에서 활용될 수 있습니다.
‘SUBSTRING’ 사용 예
특정 위치의 문자열 추출: 사용자 이메일에서 도메인 부분만을 추출하고 싶을 때
SELECT SUBSTRING(email FROM POSITION ('@' IN email) + 1) FROM users;
이 쿼리는 ‘users’ 테이블의 ‘email’ 열에서 ’@’ 기호 뒤의 도메인 부분을 추출합니다.
고정된 형식의 문자열 처리: 전화번호에서 지역 코드를 추출하고 싶을 때
SELECT SUBSTRING(phone_number, 1, 3) FROM customers;
이 쿼리는 ‘customers’ 테이블의 ‘phone_number’ 열에서 처음 3자리(지역 코드)를 추출합니다.
문자열의 특정 부분 수정 작업에 사용: 주소에서 특정 부분을 다른 형식으로 변경하고 싶을 때
UPDATE addresses SET street = SUBSTRING(street, 1, 10) || '...' WHERE LENGTH(street) > 10;
이 쿼리는 ‘addresses’ 테이블의 ‘street’ 열에서 문자열의 길이가 10자를 초과하는 경우, 처음 10자만을 남기고 그 뒤를 ‘…‘ 으로 대체합니다.
‘SUBSTRING’ 함수의 특징
‘SUBSTRING’ 은 문자열의 특정 섹션을 반환하는 데 사용되며, 시작 위치와 길이(선택적)를 지정하여 원하는 부분 문자열을 추출할 수 있습니다.
다양한 문자열 처리 작업에 활용될 수 있으며, 데이터의 형식을 변경하거나, 특정 패턴에 기반한 정보를 추출하는 등의 목적으로 사용됩니다.
함수의 정확한 구문은 사용하는 SQL 데이터베이스 시스템에 따라 약간씩 다를 수 있으므로, 해당 시스템의 문서를 참조하는 것이 좋습니다.
사용 시 고려사항
‘SUBSTRING’ 함수를 사용할 때는 문자열의 인덱스가 1부터 시작한다는 점을 주의해야 합니다.(대부분의 SQL 시스템에서).
대량의 데이터를 처리할 때는 ‘SUBSTRING’ 함수를 사용하는 쿼리의 성능에 주의해야 합니다. 필요한 경우, 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘SUBSTRING’ 함수는 문자열 데이터를 효과적으로 처리하고 분석하는 데 있어 필수적인 도구로, 데이터베이스 내에서 다양한 문자열 조작 작업을 수행하는 데 널리 사용됩니다.
CONCAT
‘CONCAT’ 함수는 SQL에서 두 개 이상의 문자열을 하나로 결합할 때 사용됩니다.
이 함수는 데이터베이스 내에서 다양한 문자열 정보를 합쳐 새로운 문자열 값을 생성하고자 할 때 유용하며, 보고서 작성, 데이터 형식의 표준화, 사용자 이름이나 주소와 같은 데이터의 결합 등 다양한 상황에서 활용될 수 있습니다.
‘CONCAT’ 사용 예
단순한 문자열 결합: 사용자의 이름과 성을 하나의 문자열로 결합하고 싶을 때
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
이 쿼리는 ‘users’ 테이블의 ‘first_name’ 과 ‘last_name’ 을 공백으로 구분하여 결합한 후, ‘full_name’ 이라는 새로운 열로 결과를 반환합니다.
복수의 열 결합: 고객의 주소 정보를 하나의 문자열로 결합하고 싶을 때
SELECT CONCAT(street_address, ', ', city, state, ' ', postal_code) AS full_address FROM customers;
이 쿼리는 ‘customers’ 테이블에서 여러 주소 관련 열을 콤마와 공백을 사용하여 결합하고, 이를 ‘full_address’ 라는 새로운 열로 결과를 반환합니다.
데이터 형식 표준화: 상품 코드와 상품 이름을 결합하여 표준 형식의 상품 정보를 생성하고 싶을 떄
SELECT CONCAT(product_code, ': ', product_name) AS product_info FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_code’ 와 ‘product_name’ 을 콜론과 공백으로 구분하여 결합한 후,
‘product_info’ 라는 새로운 열로 결과를 반환합니다.
‘CONCAT’ 함수의 특징
‘CONCAT’ 함수는 두 개 이상의 문자열을 매개변수로 받아 이들을 순서대로 결합한 새로운 문자열을 생성합니다.
거의 모든 SQL 데이터베이스 시스템에서 지원되며, 문자열 처리와 데이터 형식의 변환에 널리 사용됩니다.
일부 데이터베이스 시스템에서는 ‘CONCAT’ 대신 연산자(**’
‘** 등)를 사용하여 문자열을 결합할 수도 있습니다.
사용 시 고려사항
결합하려는 문자열 중 하나라도 ‘NULL’ 값을 포함하는 경우, ‘CONCAT’ 의 동작은 데이터베이스 시스템에 따라 다를 수 있습니다.
예를 들어, 일부 시스템은 ‘NULL’ 을 빈 문자열로 취급할 수 있으나, 다른 시스템에서는 전체 결과가 ‘NULL’ 이 될 수 있습니다.
복잡한 문자열 결합을 수행할 때는 성능에 주의해야 하며, 특히 대량의 데이터를 처리할 때는 쿼리 성능을 테스트하고 최적화하는 것이 중요합니다.
‘CONCAT’ 함수는 문자열 데이터를 결합하고 조작하는 과정에서 필수적인 도구로, 데이터베이스 내에서 다양한 문자열 관련 작업을 구행하는 데 활용됩니다.
-
☕️[Java] 불변 객체 - 예제
불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
앞의 Address, ImmutableAddress를 그래로 활용합니다.
변경 클래스 사용
package lang.immutable.address;
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원 B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원A와 회원B는 둘다 서울에 살고 있습니다.
중간에 회원B의 주소를 부산으로 변경해야 합니다.
그런데 회원A와 회원B는 같은 Address 인스턴스를 참조하고 있습니다.
회원B의 주소를 부산으로 변경하는 순간 회원A의 주소도 부산으로 변경됩니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
package lang.immutable.address;
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
//memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원B의 주소를 중간에 부산으로 변경하려고 시도합니다.
하지만 ImmutableAddress에는 값을 변경할 수 있는 메서드가 없습니다.
따라서 컴파일 오류가 발생합니다.
결국 memberB.setAddress(new ImmutableAddress("부산"))와 같이 새로운 주소 객체를 만들어서 전달합니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
사이드 이펙트가 발생하지 않습니다. 회원A는 기존 주소를 그대로 유지합니다.
-
💾 [CS] 명령어의 구조
명령어의 구조
연산코드와 오퍼랜드
아래 그림을 보면 색 배경 필드는 명령의 ‘작동’, 달리 말해 ‘연산’을 담고 있고 흰색 배경 필드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 담고 있습니다.
명령어 : 연산 코드와 오퍼랜드로 구성되어 있습니다.
연산코드(Opreation Code): 색 배경 필드 값, 즉 ‘명령어가 수행할 연산’을 연산코드(Operation Code) 라 합니다.
오퍼랜드(Operand) : 흰색 배경 필드 값, 즉 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 오퍼랜드라고 합니다.
연산 코드는 연산자, 오퍼랜드는 피연산자 라고도 부릅니다.
연산 코드 필드: 연산 코드가 담기는 영역(색칠된 부분)
오퍼랜드 필드: 오퍼랜드가 담기는 영역(색칠되지 않은 부분)
오퍼랜드
오퍼랜드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 의미합니다.
그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있습니다.
다만 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접 명시하기보다는, 많은 경우 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담깁니다.
그래서 오퍼랜드 필드를 주소 필드 라고 부르기도 합니다.
오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러개가 있을 수도 있습니다.
오퍼랜드가 하나도 없는 명령어 0-주소 명령어
오퍼랜드가 하나인 명령어 1-주소 명령어
오퍼랜드가 두 개인 명령어 2-주소 명령어
오퍼랜드가 세 개인 명령어 3-주소 명령어
연산 코드
연산 코드 종류는 매우 많지만, 가장 기본적인 연산 코드 유형은 크게 네 가지로 나눌 수 있습니다.
데이터 전송
산술/논리 연산
제어 흐름 변경
입출력 제어
주소 지정 방식
연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address) 라고 합니다.
오퍼랜드 필드에 데이터가 저장된 위피를 명시 할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식(addressing mode) 이라고 합니다
다시 말해, 주소 지정 방식은 유효 주소를 찾는 방법입니다.
즉시 주소 지정 방식
즉시 주소 지정 방식(immediate addressing mode): 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식입니다.
이런 방식은 표현할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 이하 설명할 주소 지정 방식들보다 빠릅니다.
직접 주소 지정 방식
직접 주소 지정 방식(direct addressing mode): 오퍼랜드 필드에 유효 주소를 직접 명시하는 방식입니다.
오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 더 커졌지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었습니다.
다시 말해 표현할 수 있는 오퍼랜드 필드의 길이가 연산 코드의 길이만큼 짧아져 표현할 수 있는 유효 주소에 제한이 생길 수 있습니다.
간접 주소 지정 방식
간접 주소 지정 방식(indirect addressing mode): 유효 주소의 주소를 오퍼랜드 필드에 명시합니다. 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓습니다.
두 번의 메모리 접근이 필요하기 때문에 앞서 설명한 주소 지정 방식들보다 일반적으로 느린 방식입니다.
레지스터 주소 지정 방식
레지스터 주소 지정 방식(register addressing mode): 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법입니다.
일반적으로 CPU 외부에 있는 메모리에 접근하는 것보다 CPU 내부에 있는 레지스터에 접근하는 것이 더 빠릅니다.
그러므로 레지스터 주소 지정 방식은 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있습니다.
다만, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유합니다. 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다는 점입니다.
레지스터 간접 주소 지정 방식
레지스터 간접 주소 지정 방식(register indirect addressing mode): 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법입니다.
유효 주소를 찾는 과정이 간전 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있습니다.
레지스터 간접 주소 지장 방식은 간접 주소 지정 방식보다 빠릅니다.
정리
연산에 사용할 데이터를 찾는 방법을 주소 지정 방식 이라고 했습니다.
연산에 사용할 데이터가 저장된 위치를 유효 주소 라고 했습니다.
대표적인 주소 지정 방식으로 아래의 다섯 가지 방식을 소개했습니다.
각각의 방식이 오퍼랜드 필드에 명시하는 값을 정리해 보면 아래와 같습니다.
즉시 주소 지정 방식: 연산에 사용할 데이터
직접 주소 지정 방식: 유효 주소(메모리 주소)
간접 주소 지정 방식: 유효 주소의 주소
레지스터 주소 지정 방식: 유효 주소(레지스터 이름)
레지스터 간접 주소 지정 방식: 유효 주소를 저장한 레지스터
키워드로 정리하는 핵심 포인트
명령어 는 연산 코드와 오퍼랜드로 구성됩니다.
연산 코드는 명령어가 수행할 연산을 의미합니다.
오퍼랜드는 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치를 의미합니다.
주소 지정 방식은 연산에 사용할 데이터 위치를 찾는 방법입니다.
Q1. Swift에서 메모리 주소에 접근하기 위해 어떤 타입을 사용할 수 있는지 설명해 주세요. 그리고 왜 이러한 접근 방식이 필요할까요?
Swift에서 메모리 주소에 직접 접근하기 위해 UnsafePointer<T> 타입과 그 변형인 UnsafeMutablePointer<T>를 사용할 수 있습니다. 이러한 포인터들은 C 언어의 포인터와 유사하게 작동하며, 메모리의 특정 위치를 직접 가리키는 데 사용됩니다. 이러한 접근 방식은 일반적으로 Swift의 안전성 및 추상화 원칙에 어긋나지만, 성능 최적화, 기존 C 기반 코드와의 상호 작용, 혹은 저수준 시스템 인터페이스와의 직접적인 상호 작용이 필요한 경우에 필요할 수 있습니다. 예를 들어, 대량의 데이터 처리나 기존 C 라이브러리의 함수를 호출할 때 이러한 방식이 유용할 수 있습니다.
아래는 주니어 Java 백엔드 개발자 면접 질문에 대한 모범 답안 예시입니다. 이 답변들은 Java의 메모리 관리와 관련된 기본적인 지식을 보여주는 데 목적이 있습니다.
Q2. Java에서는 일반적으로 개발자가 직접 메모리 주소를 다루지 않습니다. 이에 대한 이유를 설명해 주세요. 또한, 자동 메모리 관리는 어떤 장점을 제공하나요?
답변: Java에서 개발자가 직접 메모리 주소를 다루지 않는 주된 이유는 Java가 자동 메모리 관리 시스템인 가비지 컬렉션(Garbage Collection, GC)을 제공하기 때문입니다. 이로 인해 메모리 누수와 같은 오류를 방지하고, 개발자가 메모리 관리에 드는 시간과 노력을 줄일 수 있습니다. 자동 메모리 관리의 장점으로는 안정성의 향상, 메모리 관리 오류의 감소, 그리고 개발자의 생산성 향상 등이 있습니다.
Q3. JVM의 메모리 모델을 설명해 주세요. Heap과 Stack 메모리 영역의 차이점은 무엇이며, 각각 어떤 종류의 데이터를 저장하나요?
답변: JVM의 메모리 모델은 크게 Heap 영역과 Stack 영역으로 나뉩니다. Heap 영역은 모든 스레드에 걸쳐 공유되며, 주로 객체와 클래스의 메타데이터가 저장됩니다. 가비지 컬렉션은 이 Heap 영역에서 주로 작동합니다. 반면, Stack 영역은 스레드 별로 별도로 할당되며, 메소드 호출과 관련된 지역 변수와 참조 변수를 저장합니다. Stack은 LIFO(Last In, First Out) 방식으로 데이터를 관리합니다.
Q4. 대규모 데이터 처리 작업을 수행할 때 Java에서 메모리 효율을 최적화하는 방법에는 어떤 것들이 있나요?
답변: 대규모 데이터 처리 시 메모리 효율을 최적화하기 위해, 객체 재사용, 적절한 컬렉션 선택, 스트림 API 사용, 그리고 메모리 캐싱 전략 등을 적용할 수 있습니다. 예를 들어, 객체 풀링을 통해 빈번히 생성 및 파괴되는 객체의 생성 비용을 줄일 수 있습니다. 또한, 데이터 양에 따라 적절한 자료구조를 선택하여 메모리 사용량과 성능을 균형있게 관리할 수 있습니다.
Q5. JNI(Java Native Interface)는 무엇이며, 왜 사용하나요? Java 애플리케이션에서 JNI를 사용하여 네이티브 코드와 상호 작용하는 예를 들 수 있나요?
답변: JNI(Java Native Interface)는 Java 코드 내에서 C나 C++과 같은 네이티브 코드를 호출하거나, 반대로 네이티브 코드에서 Java 코드를 호출할 수 있는 프로그래밍 프레임워크입니다. JNI는 시스템 레벨의 리소스나 레거시 라이브러리를 사용해야 할 때, 또는 성능상의 이유로 직접 하드웨어를 제어해야 할 때 사용됩니다. 예를 들어,
고성능 그래픽 처리나 특정 하드웨어 장치와의 직접적인 상호작용을 구현할 때 JNI를 사용할 수 있습니다.
Q6. 가비지 컬렉션(Garbage Collection)의 기본 원리를 설명해 주세요. Java에서 가비지 컬렉터의 작동 방식에 영향을 미칠 수 있는 프로그래밍 관행에는 어떤 것들이 있나요?
답변: 가비지 컬렉션은 참조되지 않는 객체를 자동으로 검출하고, 이를 메모리에서 제거하여 메모리를 회수하는 프로세스입니다. Java에서 가비지 컬렉터의 효율성에 영향을 미칠 수 있는 프로그래밍 관행으로는, 객체 참조를 적절히 해제하는 것, 대용량 객체의 재사용, 그리고 적절한 컬렉션 사용 등이 있습니다. 불필요한 객체 참조를 남겨두지 않고, 메모리 사용량이 큰 객체는 풀링 기법을 사용하여 관리함으로써, 가비지 컬렉터의 부하를 줄이고 애플리케이션의 성능을 개선할 수 있습니다.
-
☕️[Java] 공유 참조와 사이드 이펙트
공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 b의 값을 부산으로 변경한 코드를 다시 분석해 봅시다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 a, b는 같은 인스턴스를 참조합니다. 따라서 a의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라고 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
사이드 이펙트 해결방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 a와 b가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
코드를 작성해봅시다.
package lang.immutable.address;
public class RefMain1_2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = new Address("서울");
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 b의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
a와 b는 서로 다른 Address 인스턴스를 참조합니다.
a와 b는 서로 다른 인스턴스를 참조합니다.
따라서 b가 참조하는 인스턴스의 값을 변경해도 a에는 영향을 주지 않습니다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 a,b가 서로 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유
Address a = new Address("서울");
Address b = a;
이 경우 a, b 둘 다 같은 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유 하지 않음
Address a = new Address("서울");
Address b = new Address("서울");
이 경우 a, b는 서로 다른 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 가능합니다.
이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다.
쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다.
그런데 여기에 문제가 있습니다.
하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다
다음 예시를 봅시다.
참조값의 공유를 막을 수 있는 방법이 없습니다.
Address a = new Address("서울");
Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다.
b = a와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다.
하지만 Address를 사용하는 개발자 입장에서 실수로 b = a라고 해도 아무런 오류가 발생하지 않습니다.
왜냐하면 자바 문법상 Address b = a와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다.
Address b = new Address("서울"); // 새로운 객체 참조
Address b = a // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
쉽게 이야기해서 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 눈을 크게 잘 뜨고! 집중해서 코드를 잘 작성하면서 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
다음 코드를 봅시다.
package lang.immutable.address;
public class RefMain1_3 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
change(b, "부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
private static void change(Address address, String changeAddress) {
System.out.println("주소 값을 변경합니다 -> " + changeAddress);
address.setValue(changeAddress);
}
}
앞서 작성한 코드와 같은 코드입니다.
단순히 change() 메서드만 하나 추가되었습니다.
그리고 change() 메서드에서 Address 인스턴스에 있는 value 값을 변경합니다.
main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이류를 찾기가 더 어렵습니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}
여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법은 없습니다.
그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까요?
단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까요?
-
☕️[Java] 불변 객체 - 도입
불변 객체 - 도입
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제입니다.
하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없습니다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
앞의 예를 떠올려보면 a, b는 처음 시점에는 둘 다 "서울"이라는 주소를 사용해야 합니다.
그리고 이후에 b의 주소를 "부산"으로 변경해야 합니다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 b = a와 같이 "서울"이라는 Address 인스턴스를 a, b가 함께 사용하는 것이, 다음 코드와 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다.
인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 Address b = a와 같이 공유 참조를 사용해도 아무런 문제가 없습니다. 오히려 더 효율적입니다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다.
b.setValue("부산"); // b의 값을 부산으로 변경해야 합니다.
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다.
기본형과 다르게 참조형인 객체는 처음부터 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다.
따라서 이것은 문제가 아닙니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 더이선가 변경했기 때문입니다.
만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
불변 객체 도입
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어 봅시다.
package lang.immutable.address;
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
내부 값이 변경되면 안됩니다.
따라서 value의 필드를 final로 선언했습니다.
값을 변경할 수 있는 setValue()를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
package lang.immutable.address;
public class RefMain2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다.
System.out.println("a = " + a);
System.out.println("b = " + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
}
}
ImmutableAddress의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었습니다.
이제 ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없습니다.
ImmutableAddress를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫습니다.
예를 들어 b.setValue("부산")을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다.
따라서 어쩔 수 없이 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b에 대입한다.
결과적으로 a, b는 서로 다른 인스턴스를 참조하고, a가 참조하던 ImmutableAddress는 그대로 유지됩니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 a의 값은 그대로 유지되는 것을 확인할 수 있습니다.
자바에서 객체의 공유 참조는 막을 수 없습니다.
ImmutableAddress는 불변 객체입니다. 따라서 값을 변경할 수 없습니다.
ImmutableAddress은 불변 객체이므로 b가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
참고 - 가변(Mutable) 객체 VS 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.)
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.)
Address 는 가변 클래스입니다. 이 클래스로 객체를 생성하면 가변 객체가 됩니다.
ImmutableAddress는 불변 클래스입니다. 이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
💉[SQL] ORDER BY
ORDER BY
‘ORDER BY’ 절은 SQL 쿼리의 결과를 특정 기준에 따라 정렬할 때 사용됩니다.
이를 통해 반환된 데이터를 오름차순(ASC) 또는 내림차순(DESC)으로 정렬할 수 있으며, 숫자, 문자열, 날짜 등 다양한 데이터 타입에 적용할 수 있습니다.
‘ORDER BY’ 는 데이터를 보다 읽기 쉽고 분석하기 용이하게 정렬하여 제공함으로써, 데이터 리포팅, 사용자 인터페이스에서의 데이터 표시, 데이터 분석 등 다양한 상황에서 유용하게 사용됩니다.
‘OREDER BY’ 사용 예
특정 열에 따른 오름차순 정렬: 직원들을 이름순으로 정렬하고 싶을 때
SELECT * FROM emploees ORDER BY name ASC;
이 쿼리는 ‘employees’ 테이블의 모든 행을 ‘name’ 열 기준으로 오름차순으로 정렬하여 반환합니다.
특정 열에 따른 내림차순 정렬: 최신 주문부터 표시하고 싶을 때
SELECT * FROM orders ORDER BY order_by DESC;
이 쿼리는 ‘orders’ 테이블의 모든 행을 ‘order_date’ 열 기준으로 내림차순으로 정렬하여 반환합니다.
여러 열에 따른 정렬: 부서별로 그룹화하고, 각 부서 내에서 급여가 높은 순으로 정렬하고 싶을 때
SELECT * FROM employees ORDER BY department ASC, salary DESC;
이 쿼리는 먼저 ‘department’ 열로 오름차순으로 정렬하고, 같은 부서 내에서는 ‘salary’ 열을 기준으로 내림차순으로 정렬합니다.
‘ORDER BY’ 절의 특징
기본적으로 ‘ORDER BY’ 는 오름차순(ASC)으로 정렬합니다. 내림차순으로 정렬하고 싶다면 각 열 이름 뒤에 ‘DESC’ 키워드를 명시해야 합니다.
여러 열을 기준으로 정렬할 수 있으며, 이 경우 첫 번째 열을 기준으로 정렬한 후 동일한 값에 대해서는 다음 열의 순서에 따라 정렬합니다.
‘SELECT’ 쿼리의 마지막 부분에 위치하며, ‘WHERE’, ‘GROUP BY’, ‘HAVING’ 절 뒤에 명시됩니다.
사용 시 고려사항
‘ORDER BY’ 를 사용할 때는 정렬하고자 하는 열이 인덱싱되어 있는지 확인하는 것이 좋습니다. 특히 대규모 데이터셋을 다룰 때, 인덱스의 유무는 쿼리 성능에 큰 영향을 미칩니다.
복잡한 쿼리에서는 ‘ORDER BY’ 로 인한 추가적인 처리 시간이 필요할 수 있으므로, 성능과 관련하여 적절한 테스트가 필요합니다.
‘ORDER BY’ 절은 SQL 쿼리의 결과를 사용자가 원하는 순서로 쉽게 정렬할 수 있게 해주며, 데이터의 가독성과 분석의 용이성을 크게 향상시킵니다.
-
💉[SQL] GROUP BY
GROUP BY
‘GROUP BY’ 절은 SQL에서 특정 열(들)의 값에 기반하여 행(row)들을 그룹화할 때 사용됩니다.
이 기능은 집계함수(‘SUM’, ‘AVG’, ‘COUNT’, ‘MIN’, ‘MAX’ 등)와 함꼐 사용되어, 각 그룹에 대한 집계된 데이터를 계산하고 반환하는 데 주로 활용됩니다.
‘GROUP BY’ 는 데이터를 요약하고, 특정 기준에 따른 데이터의 통계를 분석할 때 유용하게 사용됩니다.
‘GROUP BY’ 사용 예
그룹별 합계 계산: 각 부서별 총 급여를 계산하고 싶을 때
SELECT department, SUM(salary) FROM employees GROUP BY departmentl
이 쿼리는 ‘employees’ 테이블에서 ‘department’ 별로 그룹화하고, 각 그룹의 ‘salary’ 합계를 계산합니다.
그룹별 평균 계산: 각 제품 카테고리별 평균 가격을 계산하고 싶을 때
SELECT category, AVG(price) FROM products GROUP BY category;
이 쿼리는 ‘product’ 테이블에서 ‘category’ 별로 그룹화하고, 각 그룹의 price 평균을 계산합니다.
그룹별 데이터 수 계산: 각 부서에 속한 직원 수를 세고 싶을 때
SELECT department, COUNT(*) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 ‘department’ 별로 그룹화하고, 각 그룹의 직원 수를 세어 반환합니다.
‘GROUP BY’ 절의 특징
데이터를 그룹화하고 각 그룹에 대한 집계를 수행하여, 데이터의 요약 정보를 제공합니다.
여러 열을 기준으로 그룹화할 수 있으며, 이 경우 선택된 모든 열의 조합에 따라 데이터가 그룹화됩니다.
집계 함수와 함께 사용되어, 각 그룹별로 함계, 평균, 최소값, 최대값 등을 계산할 수 있습니다.
‘HAVING’ 절과 함께 사용하여, 집계 결과에 대한 조건을 설정할 수 있습니다. 이는 ‘WHERE’ 절과 유사하지만, ‘GROUP BY’ 로 그룹화된 결과에 대해 조건을 정용하는 점이 다릅니다.
사용 시 고려사항
‘GROUP BY’ 를 사용할 때는 선택된 열이 ‘SELECT’ 절에 포함되어야 합니다. 그렇지 않은 경우, SQL 쿼리가 예상대로 작동하지 않을 수 있습니다.
대규모 데이터셋에서 ‘GROUP BY’ 를 사용할 때는 쿼리의 성능을 고려해야 합니다. 적절한 인덱스 사용과 데이터 구조의 최적화가 성능에 큰 영햫을 미칠 수 있습니다.
‘GROUP BY’ 절은 데이터를 분석하고 요약 정보를 얻기 위한 강력한 도구로, 데이터베이스 내에서 의미 있는 인사이트를 도출하는 데 크게 기여합니다.
-
-
-
💉[SQL] SUM, AVG, COUNT, MIN, MAX
SUM
‘SUM’ 함수는 SQL에서 특정 열(column)에 포함된 숫자 값들의 합계를 계산할 때 사용됩니다.
이는 집계 함수의 한 종류로, 주로 ‘GROUP BY’ 절과 함꼐 사용되어 여러 그룹의 데이터에 대한 합계를 구하거나, 전체 테이블에서 특정 열의 총합을 계산하는 데 적용됩니다.
‘SUM’ 함수는 보고서 생성, 데이터 분석, 재무 계산 등 다양한 상황에서 유용하게 활용될 수 있습니다.
‘SUM’ 사용 예
전체 합계 계산 : 모든 주문의 총 금액을 계산하고 싶을 때
SELECT SUM(total_price) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘total_price’ 열에 있는 모든 값의 합계를 반환합니다.
그룹별 합계 계산 : 각 부서별 직원들의 총 급여를 계산하고 싶을 떄
SELECT department, SUM(salary) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 각 ‘department’ 별로 ‘salary’ 열의 합계를 계산하여, 각 부서의 총 급여를 보여줍니다.
조건부 합계 계산 : 2023년에 이루어진 모든 판매의 총액을 계산하고 싶을 때
SELECT SUM(sales_amount) FROM sales WHERE year = 2023;
이 쿼리는 ‘sales’ 테이블에서 ‘year’ 열이 2023인 모든 행의 ‘sales_amount’ 열 값의 합계를 반환합니다.
‘SUM’ 함수의 특징
‘SUM’ 함수는 숫자 데이터에 대해서만 사용할 수 있으며, 문자열이나 날짜 등의 데이터 타입에는 사용할 수 없습니다.
‘NULL’ 값을 포함하는 열에 ‘SUM’ 함수를 사용할 때, ‘NULL’ 값은 0으로 간주되지 않고, 단순히 무시됩니다.
즉, ‘NULL’ 값은 합계 계산에 영향을 주지 않습니다.
‘SUM’ 은 다른 집계 함수(‘COUNT’, ‘AVG’, ‘MIN’, ‘MAX’ 등)와 함께 사용될 수 있으며, 복잡한 데이터 집합에 대한 요약 정보를 제공하는 데 유용합니다.
사용 시 고려사항
‘SUM’ 함수를 사용할 때는 대상 열이 숫자 타입임을 확인해야 합니다.
큰 데이터 세트에서 ‘SUM’ 함수를 사용할 때는 쿼리 성능에 주의해야 합니다.
필요한 경우 적절한 인덱스를 사용하여 성능을 최적화할 수 있습니다.
‘GROUP BY’ 절과 함께 ‘SUM’ 을 사용할 때는, 그룹화할 열을 명확히 지정해야 합니다.
‘SUM’ 함수는 데이터베이스에서 숫자 데이터의 합계를 계산하는 데 매우 중요한 도구로, 데이터 분석 및 보고서 작성 등 다양한 상황에서 활용될 수 있습니다.
AVG
‘AVG’ 함수는 SQL에서 특정 열(column)에 포함된 숫자 값들의 평균을 계산할 때 사용됩니다.
이 집계 함수는 특정 데이터 집합의 중간 값을 찾거나, 데이터의 일반적인 경향성을 파악하는 데 유용하며, 데이터 분석, 보고서 작성, 성능 평가 등 다양한 상황에서 활용될 수 있습니다.
‘AVG’ 사용 예
전체 평균 계산 : 모든 직원의 평균 급여를 계산하고 싶을 때
SELECT AVG(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에 있는 값들의 평균을 계산합니다.
그룹별 평균 계산 : 각 부서별 직원들의 평균 급여를 계산하고 싶을 때
SELECT department, AVG(salary) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 각 ‘department’ 별로 ‘salary’ 열의 평균을 계산하여, 각 부서의 직원들에 대한 평균 급여를 보여줍니다.
조건부 평균 계산 : 2023년에 이루어진 모든 판매 건에 대한 평균 판매액을 계산하고 싶을 때
SELECT AVG(sales_amount) FROM sales WHERE year = 2023;
이 쿼리는 ‘salse’ 테이블에서 ‘year’ 열이 2023인 모든 행의 ‘sales_amount’ 열 값들의 평균을 반환합니다.
‘AVG’ 함수의 특징
‘AVG’ 함수는 숫자 데이터에 대해서만 사용할 수 있습니다. 문자열이나 날짜 등 다른 타입의 데이터에는 사용할 수 없습니다.
‘NULL’ 값을 포함하는 열에 ‘AVG’ 함수를 사용할 때, ‘NULL’ 값은 계산에서 제외됩니다.
즉, ‘NULL’ 값은 평균 계산에 영향을 주지 않으며, 실제 값이 있는 데이터만을 기준으로 평균이 계산됩니다.
‘AVG’ 는 다른 집계 함수(‘SUM’, ‘COUNT’, ‘MIN’, ‘MAX’ 등)와 함께 사용될 수 있으며, 데이터의 통계적 분석이나 요약 정보 제공에 유용합니다.
사용 시 고려사항
‘AVG’ 함수를 사용할 때는 대상 열이 숫자 타입인지 확인해야 합니다.
데이터 세트의 크기가 클 때 ‘AVG’ 함수를 사용하면 쿼리 성능에 영향을 줄 수 있으므로, 필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화해야 합니다.
‘GROUP BY’ 절과 함께 ‘AVG’ 를 사용할 때는, 그룹화할 열을 명확하게 지정해야 하며, 그룹별로 평균값을 계산하고자 할 때 특히 유용합니다.
‘AVG’ 함수는 데이터 세트에서 평균값을 계산하여 중요한 인사이트를 제공하는 집계 함수로, 데이터 분석과 의사 결정 과정에서 핵심적인 역할을 합니다.
COUNT
‘COUNT’ 함수는 SQL에서 행(row)의 수를 세는 데 사용됩니다.
이 함수는 특정 조건을 만족하는 행의 수를 찾거나, 테이블의 전체 행 수를 계산할 때 매우 유용합니다. 데이터 분석, 보고서 작성, 데이터 집합의 크기를 파악하는 등의 상황에서 활용됩니다.
‘COUNT’ 는 다양한 형태로 사용될 수 있으며, 가장 일반적인 사용 방법은 ‘COUNT(*)’, COUNT(열 이름), 그리고 ‘COUNT(DISTINCT 열 이름)’ 입니다.
‘COUNT’ 사용 예
테이블의 전체 행 수 계산 : ‘employees’ 테이블의 전체 직원 수를 계산하고 싶을 때
SELECT COUNT(*) FROM employees;
이 쿼리는 ‘employees’ 테이블의 전체 행 수를 반환합니다.
특정 조건을 만족하는 행 수 계산 : 연봉이 $50,000 이상인 직원의 수를 찾고 싶을 때
SELECT COUNT(*) FROM employees WHERE salary >= 50000;
이 쿼리는 ‘salary’가 $50,000 이상인 행의 수를 반환합니다.
고유값의 수 계산 : ‘employees’ 테이블에서 고유한 부서의 수를 계산하고 싶을 때
SELECT COUNT(DISTINCT department) FROM employees;
이 쿼리는 중복을 제거한 ‘department’ 열의 고유값 수를 반환합니다.
‘COUNT’ 함수의 특징.
‘COUNT(*)’ 는 테이블의 전체 행 수를 세며, ‘NULL’ 값을 포함한 모든 행을 계산합니다.
‘COUNT(열 이름)’ 는 특정 열에서 ‘NULL’ 이 아닌 행의 수를 세는 데 사용됩니다.
‘COUNT(DISTINCT 열 이름)’ 는 특정 열의 고유값 수를 계산할 때 사용되며, 중복된 값은 하나로 취급합니다.
‘COUNT’ 함수는 집계 함수로 분류되며, ‘GROUP BY’ 절과 함께 사용하여 특정 조건에 따른 그룹별 행 수를 계산하는 데 유용합니다.
사용 시 고려사항
‘COUNT(*)’ 와 COUNT(열 이름) 사이에는 성능 차이가 있을 수 있으므로, 사용 상황에 따라 적절한 형태를 선택하는 것이 중요합니다.
대규모 데이터베이스에서 ‘COUNT’ 쿼리를 실행할 때는 쿼리 성능에 주의해야 하며, 필요한 경우 적절한 인덱스를 사용하거나 조건을 최적화하여 성능을 개선할 수 있습니다.
‘COUNT’ 함수는 데이터베이스 내 데이터의 양을 측정하고 분석하는 데 필수적인 도구로, 데이터의 크기나 특정 조건을 만족하는 데이터의 수를 파악하는 데 매우 유용합니다.
MIN
MIN 함수는 SQL에서 특정 열(column)의 최소값을 찾을 때 사용됩니다.
이 함수는 숫자, 문자열, 날짜 데이터 타입 등 다양한 종류의 데이터에 대해 작동하며, 테이블 전체 또는 특정 조건을 만족하는 데이터 집합 내에서 가장 작은 값을 찾는 데 유용합니다.
‘MIN’ 은 주로 데이터 분석, 보고서 작성, 데이터의 범위를 이해하고자 할 때 사용됩니다.
‘MIN’ 사용 예
숫자 데이터의 최소값 찾기 : 직원들의 최소 급여를 찾고 싶을 때
SELECT MIN(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에서 가장 낮은 급여를 반환합니다.
날짜 데이터의 최소값 찾기 : 가장 오래된 주문의 날짜를 찾고 싶을 때
SELECT MIN(order_date) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘order_date’ 열에서 가장 이른 날짜를 반환합니다.
문자열 데이터의 최소값 찾기 : 알파벳 순으로 가장 먼저 오는 제품 이름을 찾고 싶을 때
SELECT MIN(product_name) FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_name’ 열에서 알파벳 순으로 가장 앞서는 이름을 반환합니다. 문자열 데이터의 경우, ‘최소값’은 알파벳 순 또는 설정된 정렬 순서에 따라 결정됩니다.
‘MIN’ 함수의 특징
‘MIN’ 함수는 집계 함수의 하나로, 단일 열에서 가장 작은 값을 찾는 데 사용됩니다.
숫자, 문자열, 날짜 등 다양한 타입의 데이터에 대해 최소값을 찾을 수 있습니다.
‘GROUP BY’ 절과 함께 사용하면, 특정 기준(예: 부서별, 카테고리별)으로 그룹화된 데이터 내에서 각 그룹의 최소값을 찾는 데 사용할 수 있습니다.
사용 시 고려사항
‘MIN’ 함수를 사용할 때는 데이터 타입과 해당 필드의 데이터 구조를 이해하는 것이 중요합니다.
특히, 문자열 데이터에 대한 ‘MIN’ 의 사용은 예상치 못한 결과를 가져올 수 있으므로 주의가 필요합니다.
대규모 데이터셋에서 ‘MIN’ 함수를 사용할 때는 쿼리의 성능에 주의해야 합니다.
필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘MIN’ 함수는 데이터 세트에서 최소값을 식별할 때 필수적인 도구로, 데이터의 범위를 파악하고 특정 조건에 따른 최소값을 분석하는 데 유용합니다.
MAX
‘MAX’ 함수는 SQL에서 특정 열(column)의 최대값을 찾을 때 사용됩니다.
숫자, 문자열, 날짜 등 다양한 데이터 타입에 적용할 수 있으며, 테이블 전체 또는 특정 조건을 만족하는 데이터 집합 내에서 가장 큰 값을 찾는 데 유용합니다.
‘MAX’ 는 데이터의 상한을 파악하거나, 가장 최신 또는 가장 오래된 데이터를 식별하는 등의 상황에서 사용됩니다.
‘MAX’ 사용 예
숫자 데이터의 최대값 찾기 : 직원들의 최대 급여를 찾고 싶을 때
SELECT MAX(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에서 가장 높은 급여를 반환합니다.
날짜 데이터의 최대값 찾기 : 가장 최근 주문의 날짜를 찾고 싶을 때
SELECT MAX(order_date) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘order_date’ 열에서 가장 최근의 날짜를 반환합니다.
문자열 데이터의 최대값 찾기 : 알파벳 순으로 가장 마지막에 오는 제품 이름을 찾고 싶을 때
SELECT MAX(product_name) FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_name’ 열에서 알파벳 순으로 가장 뒤에 오는 이름을 반환합니다.
문자열 데이터의 경우, ‘최대값’은 알파벳 순 또는 설정된 정렬 순서에 따라 결정됩니다.
‘MAX’ 함수의 특징
‘MAX’ 함수는 집계 함수의 하나로, 단일 열에서 가장 큰 값을 찾는 데 사용됩니다.
숫자, 문자열, 날짜 등 다양한 타입의 데이터에 대해 최대값을 찾을 수 있습니다.
‘GROUP BY’ 절과 함꼐 사용하면, 특정 기준(예: 부서별, 카테고리별)으로 그룹화된 데이터 내에서 각 그룹의 최대값을 찾는 데 사용할 수 있습니다.
사용 시 고려사항
‘MAX’ 함수를 사용할 때는 데이터 타입과 해당 필드의 데이터 구조를 이해하는 것이 중요합니다. 특히, 문자열 데이터에 대한 ‘MAX’ 의 사용은 예상치 못한 결과를 가져올 수 있으므로 주의가 필요합니다.
대규모 데이터셋에서 ‘MAX’ 함수를 사용할 때는 쿼리의 성능에 주의해야 합니다. 필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘MAX’ 함수는 데이터 세트에서 최대값을 식별할 때 필수적인 도구로, 데이터의 범위를 파악하고 특정 조건에 따른 최대값을 분석하는 데 유용합니다.
-
-
💾 [CS] 소스코드와 명령어
소스코드와 명령어.
‘컴퓨터는 명령어를 처리하는 기계’
명령어는 컴퓨터를 실질적으로 작동시키는 매우 중요한 정보
모든 소스 코드(C, C++, Java, Python 과 같은 프로그래밍 언어로 만든 소스 코드)는 컴퓨터 내부에서 명령어로 변환됩니다.
고급 언어와 저급 언어
프로그램을 만들 때 사용하는 프로그래밍 언어, 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어
이렇게 ‘사람을 위한 언어’를 고급 언어(high-level programming language) 라고 합니다.
컴퓨터가 직접 이해하고 실행할 수 있는 언어
저급 언어(low-level programming language) 하고 합니다.
컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐입니다.
그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 합니다.
저급 언어에는 두 가지 종류가 있습니다.
기계어
0과 1의 명령어 비트로 이루어진 언어입니다.
다시 말해 0과 1로 이루어진 명령어 모음입니다.
어셈블리어
0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어
컴파일 언어와 인터프리터 언어
고급 언어는 저급 언어로 변환되는 방식으로는 크게 두 가지 방식이 있습니다.
컴파일 방식
컴파일 방식으로 작동하는 프로그래밍 언어를 컴파일 언어
인터프리트 방식
인터프리트 방식으로 작동하는 프로그래밍 언어를 인터프리터 언어
컴파일 언어
컴파일 언어
컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어입니다.
컴파일(Compile)
컴파일 언어로 작성된 소스 ㅋ코드는 전체가 저급 언어로 변환되는 과정을 거치는데 이 과정을 “컴파일”이라고 합니다.
컴파일러(Compiler)
컴파일을 수행해 주는 도구
개발자가 작성한 소스 코드 전체를 쭉 훑어보며 소스 코드에 문법적인 오류는 없는지, 실행 가능한 코드인지, 실행 가능한 코드인지, 실행하는 데 불필요한 코드는 없는지 등을 따지며 소스 코드를 처음부터 끝까지 저급 언어로 컴파일합니다.
이때 컴파일러가 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패합니다.
목적 코드(Object Code)
컴파일이 성공적으로 수행되면 개발자가 작성한 소스 코드는 컴퓨터가 이해할 수 있는 저급 언어로 변환됩니다.
이렇게 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드(Object code) 라고 합니다.
인터프리어 언어
인터프리터 언어
인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어입니다
대표적인 인터프리터 언어로 Python이 있습니다.
인터프리터
소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구
인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없습니다.
소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행됩니다.
일반적으로 인터프리터 언어는 컴파일 언어보다 느립니다.
컴파일을 통해 나온 결과물, 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스코드 마지막에 이를 때까지 한 줄 한 줄씩 저급언어로 실행해야 하기 때문입니다.
목적 파일 vs 실행 파일
목적 파일
목적 코드로 이루어진 파일입니다.
실행 파일
윈도우의 .exe 확장자를 가진 파일이 대표적인 실행 파일입니다.
목적 코드가 실행 파일이 되기 위해서는 링킹이라는 작업을 거쳐야 합니다.
링킹
여러 개의 오브젝트 파일이나 라이브러리를 하나의 실행 파일로 결합하는 과정을 의미합니다. 컴파일러가 소스 코드를 기계어로 번역한 후 링커(Linker)가 이러한 기계어 코드들을 모아 실행 가능한 프로그램을 만듭니다.
키워드로 정리하는 핵심 포인트
고급 언어는 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다.
저급 언어는 컴퓨터가 직접 이해하고 실행할 수 있는 언어입니다.
저급 언어는 0과 1로 이루어진 명령어로 구성된 기계어와 기계어를 사람이 읽기 편한 형태로 번역한 어셈블리어가 있습니다.
컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 언어입니다.
인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 저급 언어로 변환되어 실행되는 언어 입니다.
Q1. Swift는 일반적으로 고급 언어로 분류됩니다. Swift의 어떤 특징이 개발자에게 고급 언어의 장점을 제공한다고 생각하나요?
Swift는 고급 언어의 특징으로 높은 수준의 추상화, 강력한 타입 시스템, 메모리 안전성, 그리고 빠른 개발 시간을 제공합니다. Swift의 옵셔널 타입과 같은 기능은 안전한 코드 작성을 돕고, ARC는 메모리 관리를 단순화합니다.
Q2. 고급 언어와 저급 언어의 차이점은 무엇이라고 생각하나요?
고급 언어는 인간이 이해하기 쉬운 형태로 추상화된 언어로, 복잡한 작업을 간단하게 표현할 수 있게 해줍니다. Java와 같은 고급 언어는 메모리 관리, 객체 지향 프로그래밍, 에러 처리 등 복잡한 컴퓨팅 개념을 추상화하여 개발자가 더 쉽게 소프트웨어를 개발할 수 있도록 돕습니다.
저급 언어는 컴퓨터가 직접 이해할 수 있는 더 낮은 수준의 명령어로 구성됩니다. 이에 해당하는 언어는 어셈블리 언어나 기계어로, 이들은 하드웨어와 밀접한 작업을 수행하는 데 사용됩니다. 저급 언어를 사용하면 성능 최적화와 메모리 관리를 더 세밀하게 제어할 수 있지만, 개발과 디버깅 과정이 복잡해집니다.
Q3. Java는 고급 언어 중 하나로 간주됩니다. Java에서 저급 언어의 특성을 활용할 수 있는 방법에는 어떤 것이 있나요?
Java는 기본적으로 고급 언어의 특성을 많이 가지고 있지만, JNI(Java Native Interface)를 통해 저급 언어 코드와 상호 작용할 수 있습니다. JNI는 Java 애플리케이션 내에서 C나 C++과 같은 저급 언어로 작성된 코드를 호출하고 사용할 수 있는 방법을 제공합니다. 이를 통해 개발자는 특정 작업을 위해 시스템 호출이나 하드웨어 직접 제어와 같은 저급 언어의 성능과 효율성을 Java 애플리케이션에 통합할 수 있습니다. 또한, 고성능을 요구하는 애플리케이션의 특정 부분에서 성능을 최적화할 수 있습니다.
Q4. Java에서 고급 언어의 특성이 백엔드 개발에 어떤 장점을 제공하나요?
Java의 고급 언어 특성은 백엔드 개발에서 여러 가지 장점을 제공합니다. 첫째, 강력한 객체 지향 프로그래밍(OOP) 지원으로 코드의 재사용성, 확장성, 유지 보수성이 향상됩니다. 둘째, 자동 메모리 관리와 가비지 컬렉션으로 메모리 누수와 같은 문제를 방지하며 개발자가 메모리 관리에 덜 신경 쓰고 로직 개발에 더 집중할 수 있게 합니다. 셋째, 다양한 라이브러리와 프레임워크, 그리고 강력한 개발 도구와 커뮤니티 지원으로 개발 속도와 효율성이 증가합니다. 마지막으로, Java는 플랫폼 독립적인 특성을 가지고 있어, 다양한 운영 체제에서 실행될 수 있는 애플리케이션을 개발할 수 있습니다.
-
💉[SQL] SQL 문의 기본 구조, SQL
SQL
SQL은 “데이터베이스와 대화를 하기 위한 언어”
SQL(Structured Query Language)은 데이터베이스 관리를 위해 널리 사용되는 쿼리 언어.
Query(쿼리)
데이터베이스에 저장된 데이터에 접근하거나 조직하기 위한 명령어.
SQL 문의 기본 구조
SELECT # '데이터 조회'의 명령어로 필수 구문
FROM # '어디에서 데이터를 조회할까'의 명령어로 필수 구문
WHERE # 조건을 지정해주는 구문
조건을 지정하는 방법.
비교 연산자: <, >, =, <>(같지 않다)
다양한 구문: IN, BETWEEN, LIKE
여러가지 조건의 적용: AND, OR, NOT
참고 자료
테이블과 컬럼, SQL
WHERE란?
AND, OR, NOT
BETWEEN, IN, LIKE
-
-
☕️[Java] Object와 OCP
Object와 OCP.
만약 Object가 없고, 또 Object가 제공하는 toString()이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다.
여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻 입니다.
아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다.
BadObjectPrinter
public class BadObjectPrinter {
public static void print(Car car) { // Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { // Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); // dogInfo() 메서드 만듬
System.out.println(string);
}
}
구체적인 것에 의존
BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog를 사용하는 것을 BadObjectPrinter는 Car, Dog에 의존한다고 표현합니다.
다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있습니다.(물론 직접 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
추상적인 것에 의존
앞서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용합니다.
이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 Object에 클래스에 의존한다고 표현합니다.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
ObjectPrinter는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다.
Animal과 Dog, Cat의 관계를 떠올려봅시다.
Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog, Cat과 같이 하위 타입으로
내려갈 수록 개념은 더 구체적이게 됩니다.
ObjectPrinter와 Object를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
ObjectPrinter의 print() 메서드와 전체 구조를 분석해봅시다.
다형적 참조 : print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용합니다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다.
메서드 오버라이딩 : Object는 모든 클래스의 부모입니다. 따라서 Dog, Car와 같은 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있습니다.
따라서 print(Object obj) 메서드는 Dog, Car와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있습니다.
OCP 원칙
OCP 원칙을 떠올려 봅시다.
Open : 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요가 없습니다.
ObjectPrinter는 모든 타입의 부모인 Object를 사용하고, Object가 제공하는 toString() 메서드만 사용합니다.
따라서 ObjectPrinter를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
System.out.println()
지금까지 설명한 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식을 설명하기 위해 만든 것입니다.
System.out.println() 메서드도 Object 매개변수를 사용하고 내부에서 toString()을 호출합니다.
따라서 System.out.println()를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다.
지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 어버라이딩해서 사용할 수 있도록 설계되어 있습니다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다.
쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다.
동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다. 앞서 ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다.
어떤 경우에는 Car 인스턴스가 넘어오고, 어떤 경우에는 Dog 인스턴스가 넘어옵니다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다.
참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다.
예) ObjectPrinter는 Object에 의존합니다.
-
💉[SQL] BETWEEN, IN, LIKE
BETWEEN, IN, LIKE
BETWEEN
‘BETWEEN’ 연산자는 SQL에서 특정 범위 내의 값을 선택할 때 사용됩니다.
이 연산자는 시작 값과 끝 값 사이에 있는 값을 찾는 데 사용되며, 포함 관계는 양 끝값을 포함합니다.
‘BETWEEN’ 은 숫자, 텍스트, 날짜 등 다양한 데이터 타입에 적용할 수 있어, 매우 유연하게 사용됩니다.
‘BETWEEN’ 사용 예
숫자 범위 : 나이가 20세에서 30세 사이인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age BETWEEN 20 AND 30;
날짜 범위 : 2023년 1월 1일부터 2023넌 12월 31일까지 생성된 모든 주문을 찾고 싶을 때
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';
텍스트 범위 : 알파벳 순으로 ‘apple’과 ‘banana’사이에 오는 모든 항목을 선택할 때
SELECT * FROM products WHERE name BETWEEN 'apple' AND 'banana';
‘BETWEEN’의 특징.
‘BETWEEN’ 연산자는 시작 값과 끝 값 모두를 포함하는 “닫힌 범위(closed range)”를 정의합니다.
범위 검색을 할 때 매우 효과적입니다. 예를 들어, 특정 기간 동안의 데이터 또는 특정 범위의 값을 갖는 데이터를 찾는 경우에 적합합니다.
‘BETWEEN’ 대신 ’>=’ 와 ’<=’ 를 사용해 동일한 조건을 표현할 수도 있지만, ‘BETWEEN’ 을 사용하는 것이 더 직관적이고 간결할 수 있습니다.
주의사항
텍스트 범위를 사용할 때는 데이터베이스가 사용하는 문자열 정렬 규칙(collation)에 주의해야 합니다. 이는 대소문자 구분, 알파벳 순서 등에 영향을 미칠 수 있습니다.
날짜 범위를 다룰 때는 날짜 포맷과 시간대 설정이 예상한 결과에 영향을 줄 수 있으니, 데이터베이스의 날짜 포맷 설정을 확인해야 합니다.
IN
‘IN’ 연산자는 SQL에서 한 번의 쿼리로 여러 값을 조회할 때 사용됩니다.
특히, 하나의 열(column)이 여러 개의 가능한 값 중 하나를 갖고 있는지 확인할 때 유용합니다.
‘IN’ 은 주어진 값 리스트 중 어느 하나라도 일치하는 행을 찾을 때 사용되며, 리스트 내의 값과 정확히 일치하는 행만을 결과로 반환합니다.
이는 복수의 ‘OR’ 조건을 사용하는 것과 동일한 결과를 나타내지만, 훨씬 간결하고 읽기 쉬운 쿼리를 작성할 수 있게 해줍니다.
‘IN’ 사용 예
다수의 명시적 값에 대한 검색 : 이름이 ‘Alice’, ‘Bob’, 또는 ‘Charlie’인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE name IN ('Alice', 'Bob', 'Charlie');
서브쿼리와 함께 사용 : 특정 조건을 만족하는 다른 테이블의 값에 해당하는 행을 찾을 때
SELECT * FROM product WHERE category_id IN (SELECT id FROM categories WHERE type = 'Eletronics')
리스트에 포함된 값들로 필터링 : 특정 지역 코드를 가진 모든 전화번호를 찾고 싶을 때
SELECT * FROM phone_number WHERE area_code IN ('202', '303', '404');
‘IN’ 연산자의 특징
‘IN’ 은 주어진 리스트 안에 있는 값과 일치하는 모든 행을 찾아내는 데 사용됩니다.
복수의 ‘OR’ 조건을 간단하게 표현할 수 있어, 쿼리의 가독성을 높여줍니다.
리스트 내의 각 항목은 정확한 일치(match)를 찾는 데 사용되므로, 부분 일치나 패턴 일치를 위해서는 다른 연산자(예: ‘LIKE’)를 사용해야 합니다.
‘IN’ 은 서브쿼리와 함께 사용될 때 매우 강력하며, 다른 테이블의 결과에 기반한 쿼리를 작성하는 데 유용합니다.
사용 시 고려사항
‘IN’ 리스트에 많은 수의 값이 포함될 경우, 성능이 저하될 수 있습니다. 가능한 한, ‘JOIN’ 이나 다른 방법으로 쿼리를 최적화하는 것을 고려해야 합니다.
‘IN’ 으로 서브 쿼리를 사용할 때는 서브쿼리가 많은 양의 데이터를 반환하지 않도록 주의해야 합니다. 서브쿼리의 결과가 크면 큰 만큼, 전체 쿼리의 성능에 영향을 줄 수 있습니다.
‘IN’ 연산자는 SQL에서 특정한 값들의 집합에 대해 검색할 때 매우 유용하며, 쿼리의 복잡성을 줄이고 읽기 쉽게 만들어 줍니다.
LIKE
‘LIKE’ 연산자는 SQL에서 패턴 매칭을 통해 데이터를 검색할 때 사용됩니다.
이는 주로 텍스트 데이터를 다룰 때 유용하며, 특정 패턴이나 일부 문자열이 포함된 행을 찾고자 할 때 활용됩니다.
‘LIKE’ 연산자는 와일드카드 문자와 함께 사용되며, 더 유연한 검색 조건을 제공합니다.
주로 사용되는 와일드카드에는 ‘%’(어떤 문자열이든지 대체 가능)와 ‘_‘(단인 문자 대체)가 있습니다.
‘LIKE’ 사용 예
특정 문자열로 시작하는 데이터 검색 : 이름이 ‘J’로 시작하는 사람을 찾고 싶을 때
SELECT * FROM people WHERE name LIKE 'J%';
이 경우, ‘J’로 시작하는 모든 이름을 찾습니다. ‘%’는 ‘J’ 이후에 어떤 문자열이 와도 괜찮다는 것을 의미합니다.
특정 문자열을 포함하는 데이터 검색 : 이메일 주소에 ‘gmail.com’을 포함하는 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE email LIKE '%gmail.com';
여기서 ‘%’는 ‘gmail.com’ 앞에 어떤 문자열이 오든지 상관 없다는 것을 의미합니다.
특정 패턴에 맞는 데이터 검색 : 세 자리 코드 중 두 번째 자리가 ‘A’인 모든 코드를 찾고 싶을 때
SELECT * FROM codes WHERE code LIKE '_A%';
이 쿼리에서 ‘_‘는 정확히 하나의 문자를 대체하고, ‘%’는 그 뒤에 어떤 문자열이 오든지 상관 없다는 것을 의미합니다.
‘LIKE’ 연산자의 특징
‘LIKE’ 는 대소문자를 구분하는 데이터베이스에서는 대소문자가 정확히 일치하는 경우에만 결과를 반환합니다. 대소문자 구분 없이 검색하려면, 데이터베이스 또는 컬럼의 설정을 확인하거나, 쿼리에 특정 함수를 사용해야 할 수 있습니다.
패턴 매칭을 통해 유연한 검색이 가능하지만, 와일드카드를 많이 사용할수록 쿼리의 성능은 떨어질 수 있습니다. 특히, 문자열 시작 부분에 ‘%’를 사용하는 경우 인덱스 활용이 어려워 성능 저하의 원인이 될 수 있습니다.
‘LIKE’ 연산자는 문자열 필드 내에서 특정 패턴이나 부분 문자열을 기반으로 데이터를 검색할 때 매우 유용합니다.
패턴 매칭 기능을 통해 복잡한 조건의 문자열 검색을 수행할 수 있으며, 데이터 분석이나 데이터 정제 과정에서 중요한 역할을 합니다.
-
💉[SQL] AND, OR, NOT
AND, OR, NOT
AND
‘AND’ 연산자는 SQL에서 여러 조건을 동시에 만족해야 할 때 사용됩니다.
즉, ‘AND’ 를 사용하는 쿼리는 모든 조건인 참(TRUE)일 때만 결과를 반환합니다.
이는 데이터베이스에서 더 세밀한 필터링을 수행하고자 할 때 유용하며, 특히 복잡한 데이터 집합에서 기준에 부합하는 정확한 데이터를 찾고자 할 때 중요한 역할을 합니다.
‘AND’ 사용 예
여러 기준에 따른 데이터 검색 : 나이가 30세 이상이면서 ‘New York’에 거주하는 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age >= 30 AND city = 'New York' ;
이 쿼리는 ‘age’ 열이 30 이상이면서 동시에 ‘city’ 열이 ‘New York’인 모든 행을 반환합니다.
날짜 범위와 특정 조건을 동시에 만족하는 데이터 검색 : 2023년 1월 1일부터 2023년 3월 31일 사이에 등록되고, 상태가 ‘활성화’인 모든 계정을 찾고 싶을 때
SELECT * FROM account WHERE registration_date BETWEEN '2023-01-01' AND '2023-03-31' AND status = 'Active';
이 쿼리는 ‘registration_date’ 가 지정된 날짜 범위 내에 있으며, ‘status’ 가 ‘Active’ 인 행을 반환합니다.
‘AND’ 연산자의 특징
‘AND’ 연산자를 사용할 때는 각 조건이 서로 어떤 관계에 있는지 고려해야 합니다. 예를 들어, 상호 배타적인 조건을 ‘AND’ 로 연결하면 결과가 항상 비어 있을 것입니다.
성능에 영향을 미칠 수 있는 큰 데이터 세트에서는 인덱스와 조건의 효율적인 사용이 중요합니다. 가능한 한, 성능에 영향을 덜 미치는 조건을 먼저 적용하는 것이 좋습니다.
‘AND’ 연산자는 복수의 조건을 조합하여 데이터를 필터링하고자 할 때 필수적인 도구입니다.
이를 통해 더 정확하고 의미 있는 데이터 집합을 얻을 수 있으며, SQL 쿼리 작성 시 다양한 상황에 맞춰 유연하게 적용할 수 있습니다.
OR
‘OR’ 연산자는 SQL에서 주어진 조건 중 하나 이상이 참(TRUE)일 때 결과를 반환하고자 할 때 사용됩니다.
‘OR’ 을 사용하면 여러 조건 중 하나라도 만족하는 데이터를 선택할 수 있어, 데이터베이스 쿼리의 유연성을 크게 향상시킬 수 있습니다.
‘OR’ 은 다양한 시나리오에서 유용하게 사용되며, 특히 여러 다른 가능성을 모두 포함해야 할 때 중요한 역할을 합니다.
‘OR’ 사용 예
여러 다른 값 중 하나를 만족하는 데이터 검색 : ‘Manager’ 또는 ‘Sales’ 부서에 속한 모든 직원을 찾고 싶을 때
SELECT * FROM employees WHERE department = 'Manager' OR department = 'Sales';
이 쿼리는 ‘department’ 열이 ‘Manager’이거나 ‘Sales’인 모든 행을 반환합니다.
여러 조건 중 하나 이상을 만족하는 데이터 검색 : 나이가 18세 미만이거나 65세 이상인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age < 18 OR age >= 65;
이 쿼리는 ‘age’ 열이 18세 미만이거나 65세 이상인 모든 행을 반환합니다.
‘OR’ 연산자의 특징
‘OR’ 연산자를 사용할 때는 주어진 조건 중 하나라도 참이면 결과 집합에 해당 행이 포함됩니다. 모든 조건이 거짓(FALSE)인 경우에만 결과에서 제외됩니다.
여러 개의 다른 가능성을 허용하는 데 유용하며, 특히 사용자의 입력이나 선택에 따라 다양한 결과를 보여줘야 할 때 자주 사용됩니다.
‘AND’ 연산자와 함께 사용될 수 있으나, 이 경우 우선 순위에 주의해야 하며, 괄호를 사용하여 연산자 간의 우선 순위를 명확하게 구분해야 합니다.
사용 시 고려사항
‘OR’ 연산자를 사용할 때는 쿼리의 성능에 주의해야 합니다. 특히 대규모 데이터 셋에서는 ‘OR’ 조건이 많을 수록 쿼리 성능이 저하될 수 있습니다.
가능한 경우, ‘OR’ 을 사용하는 대신 다른 접근 방식을 고려해보는 것도 좋습니다. 예를 들어, ‘IN’ 연산자를 사용하면 ‘OR’ 과 유사한 결과를 더 효율적으로 얻을 수 있을 때가 많습니다.
복잡한 쿼리에서는 ‘OR’ 과 ‘AND’ 를 혼합하여 사용할 때 괄호를 적절히 사용하여 명확한 논리 구조를 유지하는 것이 중요합니다.
‘OR’ 연산자는 다양한 조건을 유연하게 처리하고자 할 때 매우 유용하며, SQL 쿼리를 작성하는 과정에서 필요한 결과를 얻기 위해 다양한 시나리오를 고려할 수 있게 해줍니다.
NOT
‘NOT’ 연산자는 SQL에서 조건의 논리를 부정할 때 사용됩니다.
즉, ‘NOT’ 은 특정 조건이 거짓(FALSE)일 때 참(TRUE)인 결과를 반환하도록 합니다.
이를 통해 특정 조건을 만족하지 않는 데이터를 검색하고자 할 때 매우 유용하게 활용할 수 있습니다.
‘NOT’ 연산자는 ‘WHERE’ 절 내에서 다른 연산자(예: ‘IN’, ‘BETWEEN’, ‘LIKE’, ‘EXISTS’)와 함께 사용되어, 해당 조건의 반대되는 결과를 얻고자 할 때 사용됩니다.
‘NOT’ 사용 예
특정 조건을 만족하지 않는 데이터 검색 : ‘Sales’ 부서에 속하지 않는 모든 직원을 찾고 싶을 때
SELECT * FROM employees WHERE NOT department = 'Sales';
이는 ‘department’ 가 ‘Sales’ 가 아닌 모든 행을 반환합니다.
특정 범위에 속하지 않는 데이터 검색 : 20세에서 30세 사이가 아닌 사람을 찾고 싶을 때
SELECT * FROM people WHERE NOT age BETWEEN 20 AND 30;
이는 나이가 20세 이상 30세 이하가 아닌 모든 사람을 찾습니다.
지정된 목록에 포함되지 않는 데이터 검색 : ‘Manager’와 ‘Sales’ 부서에 속하지 않은 모든 직원을 찾고 싶을 때
SELECT * FROM employee WHERE department NOT IN ('Manager', 'Sales');
이 쿼리는 ‘department’ 가 ‘Manager’ 또는 ‘Sales’가 아닌 모든 행을 반환합니다.
‘NOT’ 연산자의 특징.
‘NOT’ 연산자는 조건의 논리를 부정하여, 조건이 거짓일 때 참을 반환합니다.
‘NOT’ 은 ‘WHERE’ 절에서 다양한 연산자와 함께 사용될 수 있으며, 특정 조건을 제외한 데이터를 선택하고자 할 때 유용합니다.
복잡한 조건에서는 ‘NOT’ 을 사용하여 예외적인 경우를 쉽게 필터링할 수 있습니다.
사용 시 고려사항.
‘NOT’ 연산자를 사용할 때는 쿼리의 성능에 주의해야 합니다. 특히, ‘NOT’ 이 포함된 조건은 인덱스 활용이 어려워 성능 저하를 일으킬 수 있습니다.
명확하지 않은 논리를 피하기 위해, 가능한 한 ‘NOT’ 의 사용을 최소화하고, 대신 명확한 조건을 사용하여 원하는 결과를 얻는 것이 좋습니다.
‘NOT’ 연산자는 SQL 쿼리에서 특정 조건을 만족하지 않는 데이터를 필터링하고자 할 때 유용하게 사용됩니다.
하지만, 쿼리의 명확성과 성능을 고려하여 신중하게 사용해야 합니다.
-
💉[SQL] WHERE란?
WHERE ?
SQL에서 ‘WHERE’ 절은 데이터베이스에서 특정 조건을 만족하는 행(row)을 검색할 때 사용됩니다.
기본적으로 ‘SELECT’, ‘UPDATE’, ‘DELETE’ 문에서 데이터를 필터링하기 위해 사용되며, 이를 통해 반환되거나 영향을 받는 데이터의 범위를 좁힐 수 있습니다.
WHERE 절의 기본 구조
SELECT column1, column2, ...
FROM table_name
WHERE conditionl
‘SELECT’ 문에서는 특정 조건을 만족하는 행을 선택해 반환합니다.
‘UPDATE’ 문에서는 특정 조건을 만족하는 행에 대해서만 업데이트를 수행합니다.
‘DELETE’ 문에서는 특정 조건을 만족하는 행을 삭제합니다.
조건의 사용
‘WHERE’ 절에서 사용할 수 있는 조건에는 다음과 같은 것들이 있습니다.
비교 연산자(’=’, ‘!=’, ‘<’, ‘>’, ‘<=’, ‘>=’)
논리 연산자(‘AND’, ‘OR’, ‘NOT’)
범위 검색(‘BETWEEN’)
목록에서 선택(‘IN’)
패턴 매칭(‘LIKE’)
NULL 값 검사(‘IS NULL’)
예시
예를 들어, 이름이 ‘Jhon’인 사람의 정보를 찾고 싶다면 다음과 같이 쿼리를 작성할 수 있습니다.
SELECT * FROM users WHERE name = 'Jhon';
또는 나이가 18세 이상인 모든 사용자를 찾고 싶다면 다음과 같이 작성할 수 있습니다.
SELECT * FROM users WHERE age >= 18;
‘WHERE’ 절을 사용함으로써, 큰 데이터베이스 내에서도 필요한 데이터를 효율적으로 찾아낼 수 있습니다.
-
☕️[Java] toString()
toString()
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
코드로 확인해봅시다.
package lang.object.tostring;
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@a09ee92
java.lang.Object@a09ee92
Object.toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다
println()과 toString()
그런데 toString()의 결과를 출력한 코드와 object를 println()에 직접 출력한 코드의 결과가 완전히 같습니다.
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
System.out.println() 메서드는 사실 내부에서 toString()을 호출합니다.
Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력합니다.
public void println(Object x) {
String s = String.valueOf(x);
// ...
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
따라서 println()을 사용할 때, toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
toString() 오버라이딩.
Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 toString()을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package lang.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package lang.object.tostring;
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
package lang.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package lang.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("3. Object 다용성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다용성 활용
객체 정보 출력: lang.object.tostring.Car@452b3a41
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
Car 인스턴스는 toString()을 재정의 하지 않습니다.
따라서 Object가 제공하는 기본 toString() 메서드를 사용합니다.
Dog 인스턴스는 toString()을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
Object obj의 인수로 car(Car)가 전달됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. 재정의된 메서드가 없습니다.
Object.toString()을 실행합니다.
ObjectPrinter.print(dog) // main에서 호출
void print(Object obj = dog(Dog)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
Object obj의 인수로 dog(Dog) 가 전달 됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. Dog에 재정의된 메서드가 있습니다.
Dog.toString()을 실행합니다.
참고 - 객체의 참조값 직접 출력
toString()은 기본으로 객체의 참조값을 출력합니다.
그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue)
실행 결과
refValue = 72ea2f77
-
-
-
-
-
-
💉[SQL] 테이블과 컬럼, SQL
SQL?
SQL은 “데이터베이스와 대화를 하기 위한 언어” 입니다.
옆의 사람에세 필요한 것을 요청시 “A를 주시겠어요?”와 하는 것과 같이 “DB에게도 A를 주시겠어요?” 라고 이야기할 때 사용하는 언어라고 할 수 있습니다.
Query
SQL 이란 언어를 이용하여 데이터베이스에 요청을 하는 질의를 ‘Query’라고 합니다.
테이블과 컬럼?
데이터베이스 : 쉽게 말해 “데이터가 저장되어있는 큰 폴더” 입니다.
체계적으로 조직된 데이터의 집합으로, 데이터의 저장, 검색, 수정, 삭제 등을 효율적으로 처리할 수 있게 해주는 데이터 구조와 관리 시스템을 말합니다.
데이터베이스 관리 시스템(DBMS)은 이러한 데이터베이스를 만들고 관리하는 소프트웨어입니다.
데이터베이스의 주요 구성 요소.
데이터(Data) : 정보의 원시 형태로, 문자, 숫자, 이미지 등 다양한 형태가 있습니다. 데이터베이스에 저장된 데이터는 조직화되어 있어 효율적인 접근과 관리가 가능합니다.
테이블(Table) : 데이터를 구조화하여 저장하는 기본 단위입니다. 테이블은 행(Row)과 열(Column)로 구성되어 있으며, 각 행은 고유한 데이터 레코드를, 열은 특정 데이터 필드를 나타냅니다.
스키마(Schema) : 데이터베이스의 구조를 정의하는 메타데이터의 집합입니다. 테이블 구조, 데이터 타입, 관계 등 데이터베이스의 뼈대를 이룹니다.
쿼리(Query) : 데이터베이스에 저장된 데이터에 접근하거나 조직하기 위한 명령어입니다. SQL(Structured Query Language)은 데이터베이스 관리를 위해 널리 사용되는 쿼리 언어입니다.
데이터베이스의 중요성.
중복성 감소 : 데이터베이스는 데이터 중복을 최소화하여 저장 공간의 효율성을 높이고 데이터 일관성을 유지합니다.
데이터 무결성 : 데이터베이스는 데이터의 정확성과 일관성을 유지하기 위한 규칙(제약 조건)을 적용합니다. 이를 통해 데이터의 신뢰성을 보장합니다.
보안 : 데이터베이스는 사용자의 권한을 관리하여 특정 데이터에 대한 접근을 제어할 수 있습니다. 이는 데이터의 보안을 강화합니다.
백업 및 복구 : 데이터베이스는 데이터의 백업 및 복구 기능을 제공하여, 시스템 장애나 데이터 손실 시 데이터를 복원할 수 있습니다.
데이터베이스의 종류.
관계형 데이터베이스(RDBMS) : 테이블 간의 관계를 기반으로 하는 데이터베이스입니다. Oracle, MySQL, PostgreSQL 등이 있습니다.
비관계형 데이터베이스(NoSQL) : 스키마가 없거나 유연한 데이터 모델을 사용하여 대규모 분산 데이터를 관리하는 데이터베이스입니다. MongoDB, Cassandra, Redis 등이 있습니다.
-
💉[SQL] 데이터베이스 모델링
건물을 짓기 위한 설계도: 데이터베이스 모델링(Database Modeling)
테이블의 구조를 미리 설계하는 개념으로 건출 설계도를 그리는 과정과 비슷합니다.
프로젝트를 진행하기 위해서는 대표적으로 “폭포수 모델(waterfall model)” 을 사용하며, 데이터베이스 모델링은 폭포수 모델의 업무 분석과 시스템 설계 단계에 해당합니다.
이 단계를 거치면 가장 중요한 데이터베이스 개체인 “테이블 구조” 가 결정되는 것 입니다.
프로젝트 진행 단계.
“프로젝트(project)”
현실 세계에서 일어나는 업무를 컴퓨터 시스템으로 옮겨놓는 과정.
대규모 소프트웨어(software) 를 작성하기 위한 전체 과정.
프로그램과 소프트웨어의 구분
프로그래밍 언어(C, 자바, 파이썬 등)를 통해서 만들어진 결과물을 소프트웨어(software)라고 부릅니다.
소프트웨어와 프로그램(program)은 거의 비슷한 용어로 소프트웨어는 좀 더 큰 단위, 프로그램은 좀 더 작은 단위로 부르기도 하지만 대부분의 상황에서 구분 없이 사용하고 있습니다.
“폭포수 모델(waterfall model)”
소프트웨어 개발 절차 중 하나
각 단계가 폭포가 떨어지듯 진행되기 때문에 붙여진 이름
폭포수 모델의 단계
프로젝트 계획
업무 분석
시스템 설계
프로그램 구현
테스트
유지보수
각 단계의 의미를 예를 들어 설명해보겠습니다.
지금 우리가 슈퍼마켓을 운영하고 있다고 가정해봅시다.
이 슈퍼마켓의 물건을 온라인으로도 판매하기 위해 인터넷 쇼핑몰을 구축하려고 합니다.
프로젝트 계획 : 슈퍼마켓의 물건들을 온라인으로 판매하기 위한 계획 단계입니다.
업무 분석 : 슈퍼마켓에서 업무가 어떻게 돌아가는지 파악하는 것입니다. 예로 물건은 어디서 들어오는지, 물건을 어떻게 계산하는지, 재고는 어떻게 관리하는지 등의 업무에 대해서 정리하는 단계입니다.
시스템 설계 : 앞에서 정리한 업무 분석을 컴퓨터에 적용시키기 위해서 알맞은 형태로 다듬는 과정입니다.
프로그램 구현 : 앞에서 완성한 시스템 설계의 결과를 실제 프로그래밍 언어로 코딩하는 단계입니다. 우리가 계획한 내용을 온라인으로 제공하기 위해서는 JavaScript, PHP, JSP 등의 프로그래밍 언어를 사용해야 합니다.
테스트 : 코딩된 프로그램에 오류가 없는지 확인하는 과정입니다.
유지보수 : 실제 온라인 쇼핑몰을 운영하면서 문제점을 보안하고 기능을 추가하는 과정입니다.
폭포수 모델의 장.단점
장점 : 각 단계가 구분되어 프로젝트의 진행 단계가 명확하다는 장점.
단점 : 폭포에서 내려가기는 쉬워도 다시 거슬러 올라가기는 힘든 것처럼 문제가 발생할 경우 다시 앞 단계로 돌아가기가 어렵다는 단점
데이터베이스 모델링.
현실 세계의 슈퍼마켓을 인터넷 쇼핑몰로 만드는 프로젝트를 바탕으로 데이터베이스 모델링 부분을 살펴보겠습니다.
“데이터베이스 모델링(Database modeling)”
우리가 살고 있는 세상에서 사용되는 사물이나 작업을 DBMS의 데이터베이스 개체롤 옮기가 위한 과정
쉽게 이야기하면 현실에서 쓰이는 것을 테이블로 변경하기 위한 작업
슈퍼마켓(현실 세계)의 고객, 물건, 직원 등을 데이터베이스에 각각의 테이블 이라는 개체로 변환합니다.
예를 들어 어떤 사람의 신분을 증명하기 위한 신분증에 이름, 주민등록번호, 주소 등의 정보가 있는 것과 비슷한 개념입니다.
인터넷 쇼핑몰에서 판매할 제품들도 마찬가지입니다. 제품의 이름, 가격, 제조일자, 제조회사, 재고량 등을 데이터베이스에 저장하는 것 입니다.
데이터베이스 모델링에는 정답이 없습니다.
다만, 좋은 모델링과 나쁜 모델링은 분명히 존재합니다. 이는 다양한 학습과 실무 경험에서 우러나옵니다.
전체 데이터베이스 구성도
앞에서 살펴본 데이터베이스 모델링의 결과로 다음과 같은 구성이 완료되었다고 가정하겠습니다.
데이터(data) : 하나하나의 단편적인 정보를 말합니다. 이 그림에서는 tess, 아이유, 바나나와 같은 개별적인 정보를 말합니다.
테이블(table) : 회원이나 제품의 데이터를 입력하기 위해 표 형태로 표현한 것을 말합니다. 지금은 인터넷 쇼핑몰을 구현하기 위해서 회원 정보를 보관할 회원 테이블과 제품 정보를 보관할 제품 테이블, 2개의 테이블을 만들었습니다.
데이터베이스(database) : 테이블이 저장되는 저장소를 말합니다. 데이터를 저장하는 곳이라는 의미로 그림에서는 원통 모양으로 표현했습니다. 그림에 3개의 데이터베이스를 표현했는데요, 각 데이터베이스는 이름이 서로 달라야합니다.
DBMS(Database Management System) : 데이터베이스 관리 시스템 또는 소프트웨어를 말합니다. MySQL 과 같은것이 바로 DBMS입니다. 그림에서 MySQL이 3개의 데이터베이스를 관리하고 있습니다.
열(column) : 테이블의 세로를 말합니다. 각 테이블은 여러 개의 열(컬럼, 필드)로 구성됩니다. 회원 테이블은 3개의 열로, 제품 테이블은 5개의 열로 구성되어 있습니다.
열 이름 : 각 열을 구분하기 위한 이름입니다. 열 이름은 각 테이블 내에서는 서로 달라야 합니다. 회원 테이블의 아이디, 회원 이름, 주소 등이 열 이름입니다.
데이터 형식 : 열에 저장될 데이터의 형식을 말합니다. 회원 테이블의 회원 이름은 열은 ‘1234’와 같은 숫자가 아닌 ‘나훈아’와 같은 문자 형식이어야 합니다. 그리고 제품 테이블의 가격 열은 숫자(정수) 형식이어야 합니다. 데이터 형식은 테이블을 생성할 때 열 이름과 함께 지정해줍니다.
행(row) : 실질적인 진짜 데이터를 말합니다. 예로, ‘tess/나훈아/경기 부천시 중동’이 하나의 행(로우, 레코드)으로 행 데이터라고도 부릅니다. 회원 테이블에서 회원이 몇 명인지는 행 데이터가 몇 개인지로 알 수 있습니다. 즉, 행의 개수가 데이터의 개수입니다. 이 예에서는 4건의 행 데이터가 있으므로 4명의 회원이 가입되어 있는 것입니다.
기본 키(Primary Key, PK) : 기본 키(또는 주키) 열은 각 행을 구분하는 유일한 열을 말합니다. 더 쉽게는 네이버의 회원 아이디, 학번, 주민등록번호 같은 것이라고 생각하면 됩니다. 그래서 기본 키는 중복되어서는 안 되며, 비어 있어서도 안 됩니다.
네이버 아이디, 학번, 주민등록번호 등이 다른 사람과 중복되지 않습니다. 또 네이버 회원인데 네이버 아이디가 없거나, 한국 사람인데 주민등록번호가 없는 것은 불가능합니다.
테이블에는 열이 여러 개 있지만 기본 키는 1개만 지정해야 하며, 일반적으로 1개의 열에 지정합니다.
SQL(Structure Query Language) : DBMS에서 작업을 하고 싶다면 DBMS가 알아듣는 언어(말)로 해야 합니다. 그것이 SQL(구조화된 질의 언어)입니다. 즉, SQL은 사람과 DBMS가 소통하기 위한 언어입니다.
4가지 핵심 키워드, 핵심 포인트
프로젝트란 현실 세계에서 컴퓨터 시스템으로 옮겨놓는 일련의 과정입니다.
폭포수 모델은 소프트웨어 개발 단계 중 하나로, 이름 그대로 폭포가 떨어지듯 개발 단계가 진행됩니다.
데이터베이스 모델링이란 현실 세계에서 사용되는 작업이나 사물들을 DBMS의 테이블(표 형태로 표현한 데이터베이스 개체)로 옮기기 위한 과정입니다.
-
-
-
💾 [CS] 0과 1로 문자를 표현하는 방법
0과 1로 문자를 표현하는 방법.
문자 집합과 인코딩.
반드시 알아야 할 세 가지 용어
문자 집합
인코딩
디코딩
컴퓨터가 인식하교 표현할 수 있는 문자의 모음을 “문자 집합(character set)” 이라고 합니다.
문자를 0과 1로 변환하는 과정을 “문자 인코딩(character encoding)” 이라고 합니다.
0과 1로 이루어진 문자 코드를 사람이 이해할 수 있는 문자로 변환하는 과정을 “문자 디코딩(character decoding)” 이라고 합니다.
아스키 코드.
아스키(ASCII: American Standard Code for Information Interchang)
초창기 문자 집합 중 하나
영어 알파벳과 아라비아 숫자, 그리고 일부 특수 문자를 포함합니다.
아스키 문자
각각 7비트로 표현되는데, 7비트로 표현할 수 있는 정보의 가짓수는 2⁷개로, 총 128개의 문자를 표현할 수 있습니다.
아스키 코드
표를 보면 알 수 있듯 아스키 문자들은 0부터 127까지 총 128개의 숫자 중 하나의 고유한 수에 일대일로 대응됩니다. 아스키 문자에 대응된 고유한 수를 “아스키 코드”라고 합니다.
아스키 코드로 인코딩
아스키 코드를 이진수로 표현함으로써 아스키 문자를 0과 1로 표현할 수 있습니다.
아스키 문자는 이렇게 아스키 코드로 인코딩됩니다.
아스키 코드의 장,단점.
장점
매우 간단하게 인코딩됩니다.
단점
한글을 표현할 수 없습니다.
한글뿐만 아니라 아스키 문자 집합 외의 문자, 특수문자도 표현할 수 없습니다.
그 이유는 근본적으로 아스키 문자 집합에 속한 문자들은 7비트로 표현하기에 128개보다 많은 문자를 표현하지 못하기 때문입니다.
확장 아스키(Extend ASCII).
더 다양한 문자 표현을 위해 아스키 코드에 1비트를 추가한 8비트의 아스키 코드.
그럼에도 표현 가능한 문자 수는 256개여서 턱없이 부족했습니다.
EUC-KR.
한국을 포함한 영어권 외의 나라들은 자신들의 언어를 0과 1로 표현할 수 있는 고유한 문자 집합과 인코딩 방식이 필요하다고 생각했습니다.
이러한 이유로 등장한 한글 인코딩 방식
EUC-KR은 KS X 1001, KS X 1003이라는 문자 집합을 기반으로하는 대표적인 완성형 인코딩 방식입니다.
즉, 초성 중성, 종성이 모두 결합된 한글 단어에 2바이크 크기의 코드를 부여합니다.
EUC-KR로 인코딩된 한글 한 글자를 표현하려면 16비트(한글 한 글자에 2바이트 코드 부여)가 필요합니다.
16비트는 네 자리 십육진수로 표현할 수 있습니다.
즉, EUC-KR로 인코딩된 한글은 네 자리 십육진수로 나타낼 수 있습니다.
한글 인코딩의 두 가지 방식.
완성형 인코딩.
조합형 인코딩.
“완성형 인코딩”
초성, 중성, 종성의 조합으로 이루어진 하나의 글자에 고유한 코드를 부여하는 인코딩 방식입니다.
조합형 인코딩
초성을 위한 비트열, 중성을 위한 비트열, 종성을 위한 비트열을 할당하여 그것들의 조합으로 하나의 글자 코드를 완성하는 인코딩 방식입니다.
다시 말해 초성, 중성, 종성에 해당하는 코드를 합하여 하나의 글자 코드를 만드는 인코딩 방식입니다.
EUC-KR의 문제점.
아스키 코드보다 표현할 수 있는 문자가 많아졌지만(총 2,350여개), 이는 모든 한글 조합을 표현할 수 있을 정도로 많은 양은 아닙니다.
그래서 문자 집합에 정의되지 않은 ‘쀍’, ‘쀓’, ‘믜’같은 글자는 EUC-KR로 표현할 수 없습니다.
“모든 한글을 표현할 수 없다는 사실은 때때로 크고 작은 문제를 유발합니다.”
EUC-KR 인코딩을 사용하는 웹사이트의 한글이 깨지는 현상.
EUC-KR 방식으로는 표현할 수 없는 이름으로 인해 은행, 학교 등에서 피해를 받는 사람이 생김.
이러한 문제를 조금이나마 해결하기 위해 등장한 것이 MS사의 “CP929(Code Page 949)” 입니다.
CP949는 EUC-KR의 확장된 버전
EUC-KR로는 표현할 수 없는 더욱 다양한 문자를 표현 할 수 있습니다.
다만, 이마저도 한글 전체를 표현하기에 넉넉한 양은 아닙니다.
유니코드와 UTF-8.
모든 나라 언어의 문자 집합과 인코딩 방식이 통일되어 있다면, 다시 말해 모든 언어를 아우르는 문자 집합과 통일된 표준 인코딩 방식이 있다면 언어별로 인코딩하는 수고로움을 덜 수 있을 겁니다.
그래서 등장한 것이 “유니코드(Unicode)” 문자 집합입니다.
유니코드.
EUC-KR보다 훨씬 다양한 한글을 포함하며 대부분 나라의 문자, 특수문자, 화살표나 이모티콘까지도 코드로 표현할 수 있는 통일된 문자집합힙니다.
현대 문자를 표현할 때 가장 많이 사용되는 표준 문자 집합이며, 문자 인코딩 세계에서 매우 중요한 역할을 맡고 있습니다.
UTF-8, 16, 32
유니코드는 글자에 부여된 값 자체를 인코딩된 값으로 삼지 않고 이 값을 다양한 방법으로 인코딩합니다.
이런 인코딩 방법에는 크게 UTF-8, 16, 32 등이 있습니다.
요컨데 UTF-8, 16, 32는 유니코드 문자에 부여된 값을 인코딩하는 방식입니다.
UTF-8
통상 1바이트부터 4바이트까지의 인코딩 결과를 만들어 냅니다.
UTF-8로 인코딩한 값의 결과는 1바니크가 될 수도 2바이트, 3바이트, 4바이트가 될 수도 있습니다.
UTF-8로 인코딩한 결과가 몇 바이트가 될지는 유니코드 문자에 부여된 값의 범위에 따라 결정됩니다.
4가지 키워드로 정리하는 핵심 포인트
문자 집합은 컴퓨터가 인식할 수 있는 문자의 모음으로, 문자 집합에 속한 문자를 인코딩하여 0과 1로 표현할 수 있습니다.
아스키 문자 집합에 0부터 127까지의 수가 할당되어 아스키 코드로 인코딩됩니다.
EUC-KR은 한글을 2바이트 크기로 인코딩할 수 있는 완성형 인코딩 방식입니다.
유니코드는 여러 나라의 문자들을 광범위하게 표현할 수 있는 통일된 문자 집합이며, UTF-8, 16, 32는 유니코드 문자의 인코딩 방식입니다.
Q1. iOS 개발에서 문자열을 다루는 것은 매우 흔한 작업입니다. 모든 문자는 컴퓨터 내부에서 0과 1의 이진 코드로 표현됩니다. 예를 들어, 유니코드 인코딩 방식 중 하나인 UTF-8을 사용하여 문자를 이진 코드로 변환할 수 있습니다. ‘안녕하세요’라는 문자열을 UTF-8 인코딩을 사용하여 이진 코드로 어떻게 변환할지 설명해 주세요. 또한, 이 과정에서 iOS 개발에 사용되는 Swift 언어에서 이러한 변환을 수행하는 코드 예시를 작성해 보세요.
UTF-8 인코딩 변환 과정 설명
‘안녕하세요’라는 문자열은 한글 문자로 구성되어 있으며, UTF-8 인코딩에서 한글은 보통 3바이트(24비트)로 인코딩됩니다. UTF-8은 가변 길이 인코딩 방식으로, 각 문자를 1바이트에서 4바이트까지 다양한 길이의 바이트로 인코딩합니다. 예를 들어, ASCII 코드의 경우 1바이트만 사용하지만, 한글과 같은 문자는 더 많은 바이트를 사용합니다.
예시로 ‘안녕하세요’ 중 ‘안’이라는 문자의 유니코드 코드 포인트는 U+548C입니다. 이를 UTF-8로 인코딩하면 다음과 같은 이진수로 표현될 수 있습니다: 1110xxxx 10xxxxxx 10xxxxxx. 실제 이진 코드로 변환하면 특정 이진값을 갖게 됩니다. (‘안’의 경우 실제 이진 변환 결과는 여기서 직접 계산하지 않았으나, 각 문자를 해당 방식으로 변환할 수 있습니다.)
안녕하세요’를 UTF-8로 인코딩하면 다음과 같은 이진 코드로 표현됩니다:
11101100 10010101 10001000 11101011 10000101 10010101 11101101 10010101 10011000 11101100 10000100 10111000 11101100 10011010 10010100
이진 코드는 각 바이트를 8비트 이진수로 표현한 것입니다.
UTF-8 인코딩에서 한글 문자는 대체로 3바이트로 인코딩되므로, 위의 이진 코드는 ‘안녕하세요’의 각 글자를 UTF-8 인코딩으로 변환한 결과를 보여줍니다.
각 부분이 한글 문자 하나를 나타내며, 각 문자는 3개의 바이트(24비트)로 이루어져 있습니다
Swift에서의 구현 예시
Swift에서 문자열을 UTF-8 이진 코드로 변환하는 것은 간단합니다. Swift의 String 타입은 utf8 프로퍼티를 통해 UTF-8 인코딩을 쉽게 접근할 수 있게 해줍니다.
let message = "안녕하세요"
var binaryString = ""
for codeUnit in message.utf8 {
binaryString += String(codeUnit, radix: 2) + " "
}
print(binaryString)
이 코드는 각 문자를 UTF-8 인코딩으로 변환한 후, 각 바이트를 이진수로 변환하여 출력합니다.
출력 결과는 각 UTF-8 인코딩된 바이트를 이진수 형태로 나타낸 것으로, 각 바이트 사이에는 공백이 있습니다.
Q2. Java에서는 문자와 문자열을 다루는 일이 자주 발생합니다. 특히 백엔드 시스템을 개발할 때, 다양한 인코딩 방식을 이해하고 이를 적절히 처리할 수 있는 능력이 중요합니다. UTF-8 인코딩 방식은 국제적으로 널리 사용되며, 다양한 언어와 특수 문자를 지원하는 강력한 인코딩 방식입니다. Java에서 문자열 ‘Java 백엔드 개발자’를 UTF-8 인코딩을 사용하여 이진 코드로 변환하는 과정을 설명해 주세요. 또한, 이 과정을 구현하는 Java 코드를 작성해 보세요.
주어진 질문에 대한 답변은 크게 두 부분으로 나눌 수 있습니다: 첫 번째는 UTF-8 인코딩 방식에 대한 이해와 설명이며, 두 번째는 ‘Java 백엔드 개발자’ 문자열을 UTF-8로 인코딩하여 이진 코드로 변환하는 Java 코드의 구현입니다.
1. UTF-8 인코딩 방식에 대한 이해
UTF-8은 유니코드 문자 집합을 인코딩하는 가장 널리 사용되는 방식 중 하나로, 1바이트에서 4바이트까지 다양한 길이의 바이트를 사용하여 전 세계의 거의 모든 문자를 표현할 수 있습니다. UTF-8은 영문 알파벳과 숫자 같은 기본적인 문자들을 1바이트로 표현하고, 그 외의 문자들은 2바이트 이상을 사용합니다. 예를 들어, 한글은 3바이트를 사용하여 표현됩니다. 이러한 특성 때문에, UTF-8은 다국어 처리가 필요한 웹 및 백엔드 시스템 개발에 널리 사용됩니다.
2. Java 코드 구현
‘Java 백엔드 개발자’ 문자열을 UTF-8로 인코딩하여 이진 코드로 변환하는 과정은 다음 Java 코드를 통해 구현할 수 있습니다:
public class Main {
public static void main(String[] args) {
String text = "Java 백엔드 개발자";
byte[] bytes = text.getBytes(java.nio.charset.StandardCharsets.UTF_8);
StringBuilder binaryString = new StringBuilder();
for (byte b : bytes) {
// 각 바이트를 이진수로 변환하고, 8자리 이진수 형태를 유지하기 위해 앞에 0을 채움
String binary = String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
binaryString.append(binary).append(" ");
}
System.out.println(binaryString.toString().trim());
}
}
이 코드는 다음과 같은 과정을 거칩니다:
문자열 “Java 백엔드 개발자”를 UTF-8 인코딩을 사용하여 바이트 배열로 변환합니다.
변환된 바이트 배열을 순회하면서, 각 바이트를 8비트 이진수로 변환합니다. 이 때, & 0xFF 연산을 사용하여 부호 없는 정수로 처리하고, String.format을 사용하여 이진수를 8자리로 맞춥니다.
변환된 이진수 문자열을 콘솔에 출력합니다.
이 구현을 통해 후보자는 UTF-8 인코딩 방식의 이해, Java에서의 문자열 처리, 그리고 바이트 및 이진수 처리에 대한 자신의 지식과 기술을 면접관에게 보여줄 수 있습니다. 이는 Java 백엔드 개발자로서 갖추어야 할 중요한 기술 중 하나입니다.
-
-
☕️[Java] 좋은 객체 지향 프로그래밍이란?
좋은 객체 지향 프로그래밍이란?
객체 지향 특징
추상화
캡슐화
상속
다형성
객체 지향 프로그래밍?
객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 “객체” 들의 모임으로 파악하고자 하는 것입니다.
각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있습니다.(협력)
객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.
유연하고, 변경에 용이?
레고 블럭 조립하듯이
키보드, 마우스 갈아 끼우듯이
컴퓨터 부품 갈아 끼우듯이
컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
다형성(Polymorphism)
다형성의 실세계 비유
실세계와 객체 지향을 1:1로 매칭 X
그래도 실세계의 비유로 이해하기에는 좋음
역할과 구현으로 세상을 구분
운전자 - 자동차
공연무대
로미오와 줄리렛 공연
예시
운전자 - 자동차
공연 무대
키보드, 마우스, 세상의 표준 인터페이스들
정렬 알고리즘
할인 정책 로직
역할과 구현을 분리
역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해집니다.
장점
클라이언트는 대상의 역할(인터페이스)만 알면 됩니다.
클라이언트는 구현 대상의 내부 구조를 몰라도 됩니다.
클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않습니다.
클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않습니다.
역할과 구현을 분리 2
자바 언어
자바 언어의 다형성을 활용합니다.
역할 = 인터페이스.
구현 = 인터페이스를 구현한 클래스, 구현 객체.
객체를 설계할 때 역할과 구현을 명확히 분리합니다.
객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다.
객체의 협력이라는 관계부터 생각
혼자 있는 객체는 없습니다.
클라이언트: 요청, 서버 응답
수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가집니다.
자바 언어의 다형성
오버라이딩을 떠올려봅시다.
오버라이딩은 자바 기본 문법입니다.
오버라이딩 된 메서드가 실행합니다.
다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있습니다.
물론 클래스 상속 관계도 다형성, 오버라이딩 적용 가능합니다.
다형성의 본질
인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다.
다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야 합니다.
클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있습니다.
역할과 구현을 분리 3
정리
실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있습니다.
유연하고, 변경이 용이합니다.
확장 가능한 설계입니다.
클라이언트에 영향을 주지 않는 변경이 가능합니다.
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
한계
역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생합니다.
자동차를 비행기로 변경해야 한다면?
대본 자체가 변경된다면?
USB 인터페이스가 변경된다면?
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
정리
다형성이 가장 중요합니다!
디자인 패턴 대부분은 다형성을 활용하는 것입니다.
스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것입니다.
다형성을 잘 활용하면 마치 레고 블럭 조립하듯이! 공연 무대의 배우를 선택하듯이! 구현을 편리하게 변경할 수 있습니다.
-
-
-
-
☕️[Java] 인터페이스
인터페이스
“자바는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공합니다.”
순수 추상 클래스
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
“인터페이스는 class 가 아니라 interface 키워드를 사용하면 됩니다.”
인터페이스
public interface InterfaceAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스 - public abstract 키워드 생략 가능
public interface InterfaceAnimal {
void sound();
void move();
}
순수 추상 클래스는 다음과 같은 특징을 갖습니다.
인스턴스를 생성할 수 없습니다.
상속시 모든 메서드를 오버라이딩 해야 합니다.
주로 다형성을 위해 사용됩니다.
인터페이스는 앞서 설명한 순수 추상 클래스와 같습니다. 여기에 약간의 편의 기능이 추가 됩니다.
인터페이스 메서드는 모두 public, abstract입니다.
메서드에 public abstract를 생략할 수 있습니다.(참고로 생략이 권장됩니다.)
인터페이스는 다중 구현(다중 상속)을 지원합니다.
인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final int MY_PI = 3.14;
}
인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주됩니다.
final은 변수의 값을 한 번 설정하면 수정할 수 없다는 뜻입니다.
자바에서 static final을 사용해 정적이면서 고칠 수 없는 변수를 상수라 하고, 관례상 상수는 대문자에 언더스코어(_)로 구분합니다.
해당 키워드는 다음과 같이 생략할 수 있습니다.(생략이 권장됩니다.)
public interface InterfaceAnimal {
int MY_PI = 3.14;
}
예제 5
클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속)관계는 UML에서 점선을 사용합니다.
package poly.ex5;
public class InterfaceMain {
public static void main(String[] args) {
// 인터페이스 생성 불가
// InterfaceAnimal interfaceAnimal = new InterfaceAnimal();
Cat cat = new Cat();
Dog dog = new Dog();
Caw caw = new Caw();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(caw);
}
// 변하지 않는 부분
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
앞서 설명한 순수 추상 클래스 예제와 거의 유사합니다.
순수 추상 클래스가 인터페이스가 되었을 뿐입니다.
클래스, 추상 클래스, 인터페이스는 모두 똑같습니다.
클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같습니다.
모두 자바에서는 .class로 로 다루어집니다.
인터페이스를 작성할 때도 .java에 인터페이스를 정의합니다.
인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됩니다.
상속 vs 구현
부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현합니다.
이렇게 서로 다르게 표현하는 이유를 알아봅시다.
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적입니다.
하지만 인터페이스는 모든 메서드가 추상 메서드입니다.
따라서 물려 받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 합니다.
따라서 구현한다고 표현합니다.
인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 합니다.
따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현합니다.
상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같습니다. 일반 상속 구조와 동일하게 작동합니다.
인터페이스를 사용해야 하는 이유.
모든 메서드가 추상 메서드인 경우 순수 추상 클래스로 만들어도 되고, 인터페이스를 만들어도 됩니다.
그런데 왜 인터페이스를 사용해야 할까요? 단순히 편리하다는 이유를 넘어서 다음과 같은 이유가 있습니다.
제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것입니다.
USB 인터페이스를 생각해봅시다. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 합니다. 그렇지 않으면 작동하지 않습니다.
인터페이스의 규약(제약)은 반드시 구현해야 하는 것입니다.
그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있습니다.
이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 됩니다.
인터페이스는 모든 메서드가 추상 메서드입니다. 따라서 이런 문제를 원천 차단할 수 있습니다.
다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있습니다.
반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능합니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
참고
자바 8에 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있습니다.
하지만 이것은 예외적으로 아주 특별한 경우에만 사용해야 합니다.
자바 9에서 등장한 인터페이스의 private 메서드도 마찬가지입니다.
지금 학습 단계에서는 이 부분들을 고려하지 않는 것이 좋습니다.
이 부분은 추후에 따로 학습하고 정리할 것 입니다.
-
💾 [CS] 컴퓨터 메모리를 16진수로 표시하는 이유
컴퓨터 메모리를 16진수로 표시하는 이유.
이진수와의 호환성 : 컴퓨터는 모든 데이터를 이진수, 즉 0과 1로 처리합니다. 이진수는 매우 기본적이지만, 긴 이진수를 읽고 이해하기는 어렵습니다. 16진수는 이진수를 좀 더 읽기 쉽게 만들어 줍니다. 4비트 이진수 한 덩어리가 16진수 한 자리와 정확히 대응되기 때문에, 이진수를 16진수로 변환하는 것은 자연스럽고 효율적입니다. 예를 들어, 이진수 1111은 16진수 F로 표현됩니다.
효율적인 표현 : 16진수를 사용하면 매우 큰 수나 메모리 주소를 훨씬 짧고, 관리하기 쉬운 형태로 표현할 수 있습니다. 예를 들어, 8비트 이진수인 10011011은 16진수로는 단 두 자리 9B로 표현할 수 있습니다. 이는 프로그래머와 기술자가 메모리 주소나 데이터 값을 빠르게 인식하고 작업하기 용이합니다.
표준화와 호환성 : 16진수는 컴퓨터 과학과 전자공학에서 널리 표준화되어 사용됩니다. 소프트웨어 개발, 디버깅, 하드웨어 설계 등 다양한 분야에서 16진수 사용은 정보를 일관되게 표현하고 전달하는 데 도움을 줍니다. 이는 서로 다른 시스템과 기술 간의 호환성을 증진시키는 역할을 합니다.
디버깅과 분석 용이 : 개발자와 엔지니어가 시스템의 문제를 진단하거나 메모리의 내용을 분석할 때, 16진수 표현은 이진 데이터를 빠르게 읽고 해석할 수 있게 해줍니다. 이는 소프트웨어와 하드웨어의 오류를 찾고 해결하는 과정을 간소화합니다.
이렇게 16진수는 이진수의 복잡성을 줄이면서도 정보를 효과적으로 표현하고 처리할 수 있는 효율적인 방법을 제공합니다.
컴퓨터 공학에서 이러한 방식을 사용함으로써, 우리는 컴퓨터 시스템과 소프트웨어를 보다 쉽게 이해하고, 효율적으로 작업할 수 있게 됩니다.
-
💾 [CS] 0과 1로 숫자를 표현하는 방법
0과 1로 숫자를 표현하는 방법.
정보 단위.
컴퓨터는 0 또는 1밖에 이해하지 못합니다.
0과 1을 나타내는 나타내는 가장 작은 정보 단위를 “비트(bit)” 라고 합니다.
비트는 0 또는 1, 두 가지 정보를 표현할 수 있습니다.
“n비트는 2ⁿ가지 정보를 표현할 수 있습니다.”
바이트(byte) : 여덟 개의 비트를 묶은 단위로, 비트보다 한 단계 큰 단위.
1바이트는 8비트와 같습니다.
2⁸(256)개의 정보를 표현할 수 있습니다.
킬로바이트(kB: kilobyte) : 1바이트 1,000개를 묶은 단위입니다.
메가바이트(MB: megabyte) : 1킬로바이트 1,000개를 묶은 단위입니다.
기가바이트(GB: gigabyte) : 1메가바이트 1,000개를 묶은 단위입니다.
테라바이트(TB: terabyte) : 1기가바이트 1,000개를 묶은 단위입니다.
더 큰 단위도 있습니다.
워드(word) : CPU가 한 번에 처리할 수 있는 데이터 크기를 의미합니다.
만약 CPU가 한 번에 16비트를 처리할 수 있다면 1워드는 16비트가 되고, 한 번에 32비트를 처리할 수 있다면 1워드는 32비트가 되는 것입니다.
워드의 절반 크기를 하프 워드(half word), 1배 크기를 풀 워드(full word), 2배 크기를 더블 워드(double word) 라고 부릅니다.
컴퓨터의 워트 크기는 대부분 32비트 또는 64비트 입니다. 가령 인텔의 x86 CPU는 32비트 워드, x64 CPU는 64비트 워드 CPU입니다.
이진법
0과 1만드로 모든 숫자를 표현하는 방법을 “이진법(binary)” 라고 합니다.
우리가 일상적으로 사용하는 방법은 십진법(decimal) 라고 합니다.
이진법으로 표현한 수를 “이진수”
십진법으로 표현한 수를 “십진수”
숫자만으로 어떤 수가 어떤 진법으로 표현된 수인지 알 수 없습니다.
이런 혼동을 예방하기 위해 이진수 끝에 아래첨자 (2)를 붙이거나 이진수 앞에 0b를 붙입니다.
전자는 주로 이진수를 수학적으로 표기할 때, 후자는 주로 코드 상에서 이진수를 표기할 때 사용합니다.
이진수의 음수 표현
음수를 표현하는 방법 중 가장 널리 사용되는 방법은 2의 보수(two;s complement) 를 구해 이 값을 음수로 간주하는 방법입니다.
2의 보수의 사전적 의미: ‘어떤 수를 그보다 큰 2ⁿ에서 뺀 값’을 의미합니다.
예를 들어 11₍₂₎의 2의 보수는 11₍₂₎보다 큰 2ⁿ, 즉 100₍₂₎에서 11₍₂₎을 뺀 01₍₂₎이 되는 것 입니다.
“굳이 이렇게 사전적 의미로 어렵게 이해할 필요는 없습니다. 2의 보수를 매우 쉽게 표현하자면 다음과 같습니다.”
‘모든 0과 1을 뒤집고, 거기에 1을 더한 값’으로 이해하면 됩니다.
예를 들어 11₍₂₎의 모든 0과 1을 뒤집으면 00₍₂₎이고, 거기에 1을 더한 값은 01₍₂₎입니다.
즉, 11₍₂₎의 2의 보수(음수 표현)는 01₍₂₎이 됩니다.
“실제로 이진수만 봐서는 이게 음수인지 양수인지 구분하기 어렵습니다. 그래서 컴퓨터 내부에서 어떤 수를 다룰 때는 이 수가 양수인지 음수인지를 구분하기 위해 ‘플래그(flag)’를 사용합니다.”
플래그는 쉽게 말해 부가 정보입니다.
십육진법(hexadecimal)
수가 15를 넘어가는 시점에 자리 올림을 하는 숫자 표현 방식입니다.
그리고 십진수 10, 11, 12, 13, 14, 15를 십육진법 체계에서는 각각 A, B, C, D, E, F로 표기합니다.
십육진수도 이진수와 마찬가지로 숫자 뒤에 아래첨자 ₍₁₆₎를 븉아고너 숫자 앞에 0x룰 븉여 구분합니다.
전자는 주로 수학적으로 표기할 때 사용되는 방식
후자는 주로 코드상에서 십육진수를 표기할 때 사용되는 방식
십육진법을 사용하는 주된 이유 중 하나는 이진수를 십육진수로, 십육진수를 이진수로 변환하기 쉽기 때문입니다.
십육진수를 이진수로 변환하기.
십육진수는 한 글자당 열여섯 종류(0~9, A~F)의 숫자를 표현할 수 있습니다.
십육진수를 이루는 숫자 하나를 이진수로 표현할 때는 4비트가 필요합니다.(2⁴ = 16)
십육진수를 이준수로 변환하는 간편한 방법 중 하나는 십육진수 한 글자를 4비트의 이진수로 간주하는 것 입니다.
즉, 십육진수를 이루고 있는 각 글자를 따로따로(4개의 숫자로 구성된) 이진수로 변환하고, 그것을 이어 붙이면 십육진수가 이진수로 변환됩니다.
이진수를 십육진수로 변환하기
이진수를 십육진수로 변환할 때는 이진수 숫자를 네 개씩 끊고, 끊어 준 네 개의 숫자를 하나의 십육진수로 변환한 뒤 그대로 이어 붙이면 됩니다.
키워드로 정리하는 핵심 포인트
비트는 0과 1로 표현할 수 있는 가장 작은 정보 단위입니다.
바이트, 킬로바이트, 메가바이트, 기가바이트, 테라바이트는 비트보다 더 큰 정보 단위입니다.
이진법은 1을 넘어가는 시점에 자리 올림을 하여 0과 1만으로 수를 표현하는 방법입니다.
이진법에서 음수는 2의 보수로 표현할 수 있습니다.
십육진법은 15를 넘어가는 시점에 자리 올림하여 수를 표현하는 방법입니다.
Q1.현대의 컴퓨터와 디지털 기기들은 데이터를 처리하고 저장할 때 기본적으로 0과 1, 즉 이진수를 사용합니다. iOS 개발 과정에서도 이러한 이진수의 원리를 이해하는 것이 중요한데요, 여러분은 이러한 이진수 시스템이 왜 필요하고, 어떻게 우리가 개발하는 앱과 관련이 있는지 설명해주실 수 있나요? 특히, 이진수의 개념이 iOS 앱 개발에서 어떤 실질적인 적용 사례를 가지는지 구체적인 예를 들어 주세요.
이진수 시스템은 컴퓨터와 디지털 기기들이 데이터를 처리하고 저장하는 기본적인 방법입니다. 이 시스템은 0과 1, 두 가지 상태만을 사용하여 정보를 표현하는 방법으로, 컴퓨터 하드웨어는 이러한 이진 상태들을 전기적 신호의 켜짐과 꺼짐으로 해석합니다. 이는 컴퓨터 기술에서 가장 기본이 되는 원리로, 모든 프로그래밍 언어와 운영 체제, 애플리케이션 개발에 깊이 관련되어 있습니다.
iOS 앱 개발에 있어 이진수의 이해는 몇 가지 중요한 측면에서 의미를 가집니다:
데이터 저장과 처리: 앱 내에서 사용자 데이터, 설정, 상태 정보 등을 저장하고 처리할 때, 이진 형식이 기본적으로 사용됩니다. 예를 들어, 사용자가 앱 내에서 사진을 찍거나 파일을 다운로드할 때, 이러한 데이터는 이진 형태로 디바이스에 저장됩니다.
통신: 앱이 서버와 데이터를 주고받을 때, 이진 데이터 형식이 널리 사용됩니다. 예를 들어, REST API를 통해 JSON 형식으로 데이터를 교환하더라도, 실제 네트워크를 통한 전송 과정에서는 이진 데이터로 변환되어 처리됩니다.
성능 최적화: 이진수를 직접 다루는 지식은 앱의 성능 최적화에 큰 도움이 될 수 있습니다. 예를 들어, 이미지나 동영상 처리, 암호화, 데이터 압축 등 고성능을 요구하는 작업에서는 낮은 수준의 이진 처리가 필요할 수 있습니다.
하드웨어 접근과 제어: iOS 앱 개발에서 때로는 하드웨어의 낮은 수준의 기능에 접근하거나 제어해야 할 필요가 있습니다. 이 경우, 이진수 처리 방식을 이해하는 것이 필수적입니다. 예를 들어, Bluetooth 통신이나 기타 특수한 하드웨어 기능을 사용하는 앱을 개발할 때 이진 데이터의 처리가 필요합니다.
이진수 시스템의 이해는 따라서, 기본적인 데이터의 표현부터 앱의 성능 최적화, 하드웨어 제어에 이르기까지 iOS 앱 개발의 여러 단계에 걸쳐 중요한 역할을 합니다. 이러한 지식은 개발자로서 문제 해결 능력을 향상시키고, 더 효율적이고 강력한 앱을 만드는 데 기여합니다.
Q2. 우리가 컴퓨터 과학에서 배우는 가장 기본적인 개념 중 하나는 모든 디지털 데이터가 궁극적으로 0과 1, 즉 이진수로 표현된다는 것입니다. 이러한 이진수 체계를 이해하는 것이 왜 Java 백엔드 개발에 있어 중요한지에 대해 설명해 주세요. 또한, 이 개념이 실제 백엔드 시스템 개발과 운영에 어떻게 적용될 수 있는지 구체적인 예를 들어 설명해주실 수 있나요?
이진수 체계의 이해는 Java 백엔드 개발에 있어 여러 가지 이유로 중요합니다:
데이터 표현 및 처리의 기본: 컴퓨터는 모든 정보를 이진수로 처리하고 저장합니다. Java 백엔드 개발자로서 데이터를 저장, 검색, 변환하는 다양한 작업을 수행할 때 이진 데이터의 이해는 필수적입니다. 예를 들어, 파일 시스템에서 데이터를 읽고 쓰거나, 네트워크 통신을 통해 데이터를 송수신할 때 이진 데이터 형식에 대한 지식이 필요합니다.
성능 최적화: 이진수에 대한 이해는 데이터 압축, 암호화, 데이터 전송 최적화와 같은 고급 개발 작업에서 성능을 향상시키는 데 도움이 됩니다. 예를 들어, 대용량 데이터를 효율적으로 처리하기 위해 비트 연산을 사용할 수 있으며, 이는 이진수의 원리를 이해할 때 가능해집니다.
암호화 및 보안: 현대의 암호화 알고리즘은 대부분 이진수 기반의 복잡한 수학적 연산을 사용합니다. 백엔드 시스템에서 사용자 데이터의 보안을 유지하기 위해 데이터를 암호화하고 해시 함수를 적용할 때, 이진수 원리의 이해는 필수적입니다.
하드웨어 및 시스템 인터페이스: 백엔드 시스템은 때로 특정 하드웨어나 시스템과 직접적으로 상호작용해야 할 수 있습니다. 이러한 상호작용은 종종 낮은 수준의 데이터 표현에 대한 깊은 이해를 요구하며, 이는 이진수 체계의 지식이 있을 때 효율적으로 수행될 수 있습니다.
이진수 체계의 이해는 Java 백엔드 개발자가 효율적이고 안전한 시스템을 설계하고 구현하는 데 필수적인 기초를 제공합니다. 데이터의 기본적인 표현 방식을 이해함으로써 개발자는 보다 깊은 수준에서 시스템을 이해하고, 성능과 보안 문제를 더 잘 해결할 수 있게 됩니다.
-
-
-
☕️[Java] 다형성 활용3
다형성 활용 3
이번에는 배열과 for문을 사용해서 중복을 제거해보겠습니다.
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Animal[] animalArr = {dog, cat, caw};
// 변하지 않는 부분
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
배열은 같은 타입의 데이터를 나열할 수 있습니다.
Dog, Cat, Caw는 모두 Animal의 자식이므로 Animal 타입입니다.
Animal 타입의 배열을 만들고 다형적 참조를 사용하면 됩니다.
// 둘은 같은 코드입니다.
Animal[] animalArr = new Anima[]{dog, cat, caw};
Animal[] animalArr = {dog, cat, caw};
다형적 참조 덕분에 Dog, Cat, Caw의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했습니다.
이제 배열을 for문을 사용해서 반복하면 됩니다.
// 변하지 않는 부분
for (Animal animal: animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
animal.sound()를 호출하지만 배열에는 Dog, Cat, Caw의 인스턴스가 들어있습니다.
메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출됩니다.
조금 더 개선
package poly.ex2;
public class AnimalPolyMain3 {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Cat()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
// 변하지 않는 부분
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
Animal[] animalArr를 통해서 배열을 사용합니다.
soundAnimal(Animal animal)
하나의 동물을 받아서 로직을 처리합니다.
새로운 동물이 추가되어도 soundAnimal(...) 메서드는 코드 변경 없이 유지할 수 있습니다.
이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Caw 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조하기 때문입니다.
따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 병경 없이 유지할 수 있습니다.
여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있습니다.
main()은 코드가 변하는 부분입니다.
새로운 동물을 생성하고 필요한 메서드를 호출합니다.
soundAnimal(...)는 코드가 변하지 않는 부분입니다.
“새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드입니다.”
이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋습니다.
남은 문제
지금까지 설명한 코드에는 사실 2가지 문제가 있습니다.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 클래스입니다.
이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까요?
Animal animal = new Animal();
개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상합니다.
이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없습니다.
하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없습니다.
누군가 실수로 new Animal()을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것 입니다.
이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않습니다.
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
예를들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해봅시다.
우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 “꿀꿀”이라는 소리가 나도록 하는 것입니다.
그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있습니다.
이렇게 되면 부모의 기능을 상속받습니다.
따라서 코드상 아무런 문제가 발생하지 않습니다.
물론 프로그램을 실행하면 기대와 다르게 “꿀꿀”이 아니라 부모 클래스에 있는 Animal.sound()가 호출될 것입니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있습니다.
-
-
☕️[Java] 다형성 활용1
다형성 활용1
지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보겠습니다.
아주 단순하고 전통적인 동물 소리 문제로 접근해보겠습니다.
개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해봅시다.
먼저 다형성을 사용하지 않고 코드를 작성해봅시다.
package poly.ex1;
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex1;
public class Cat {
public void sound() {
System.out.println("야옹");
}
}
package poly.ex1;
public class Caw {
public void sound() {
System.out.println("음메");
}
}
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램입니다.
만약 여기서 새로운 동물이 추가되면 어떻게 될까요?
만약 기존 코드에 소가 없다고 가정해봅시다.
소가 추가된다고 가정하면 Caw 클래스를 만들고 다음 코드도 추가해야 합니다.
// Caw를 생성하는 코드
Caw caw = new Caw();
// Caw를 사용하는 코드
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
Caw를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, Caw를 사용해서 출력하는 부분은 계속 중복이 증가합니다.
중복 코드
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 됩니다.
그런데 Dog, Cat, Caw는 서로 완전히 다른 클래스입니다.
중복 제거 시도
메서드로 중복 제거 시도
메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw, Dog, Cat 중에 하나로 정해야 합니다.
private static void soundCaw(Caw caw) {
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
따라서 이 메서드는 Caw 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없습니다.
Dog, Cat, Caw의 타입(클래스)이 서로 다르기 때문에 soundCaw 메서드를 함께 사용하는 것은 불가능합니다.
배열과 for문을 통한 중복 제거 시도
Caw[] cawArr = { cat, dog, caw }; // 컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound()
}
System.out.println("동물 소리 테스트 종료");
배열과 for문 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Caw 중에 하나로 지정해야 합니다.
같은 Caw들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Caw을 하나의 배열에 담는 것은 불가능합니다.
결과적으로 지금 상황에서는 해결방법이 없습니다.
새로운 동물이 추가될 때 마다 더 많은 중복 코드를 작성해야 합니다.
지금까지 설명한 모든 중복 제거 시도가 Dog, Cat, Caw의 타입이 서로 다르기 때문에 불가능합니다.
“문제의 핵심은 바로 타입이 다르다는 점” 입니다.
반대로 이야기하면 Dog, Cat, Caw가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것입니다.
다형성의 핵심은 다형적 참조와 메서드 오버라이딩입니다.
이 둘을 활용하면 Dog, Cat, Caw 가 모두 같은 타입을 사용하고, 각자 자신의 메서드로 호출할 수 있습니다.
-
💾 [CS] 컴퓨터 구조의 큰 그림
컴퓨터 구조의 큰 그림
우리가 알아야 할 컴퓨터 구조 지식은 크게 두 가지 입니다.
컴퓨터가 이해하는 정보
컴퓨터의 네 가지 핵심 부품
컴퓨터가 이해하는 정보
데이터
컴퓨터가 이해하는 숫자, 문자, 이미지, 동영상과 같은 정적인 정보
명령어
컴퓨터를 실직적으로 작동 시키는 중요한 정보
데이터 없이는 아무것도 할 수 없는 정보 덩어리
“데이터를 움직이고 컴퓨터를 작동 시키는 장보”
“즉, 명령어는 컴퓨터를 작동시키는 정보이고, 데이터는 명령어를 위해 존재하는 일종의 재료입니다.”
컴퓨터 프로그램은 ‘명령어들의 모음’으로 정의되기도 합니다.
그래서 명령어는 컴퓨터 구조를 학습하는 데 있어 데이터보다 더 중요한 개념.
컴퓨터의 4가지 핵심 부품.
중앙처리장치(Central Programming Unit, CPU)
컴퓨터의 두뇌
메모리에 저장된 명령어를 읽어 들이고, 읽어 들인 명령어를 해석하고, 실행하는 부품입니다.
CPU 내부 구성 요소 중 가장 중요한 세 가지는 산술논리연산장치(ALU: Arithmetic Logic Unit), 레지스터(register), 제어장치(CU: Control Unit) 입니다.
ALU: 계산기, 계산만을 위해 존재하는 부품, 컴퓨터 내부에서 수행되는 대부분의 계산은 ALU가 도맡아 수행
레지스터: CPU 내부의 작은 임시 저장 장치, 프로그램을 실행하는 데 필요한 값들을 임시로 저장, CPU 안에는 여러 개의 레지스터가 존재하고 각기 다른 이름과 역할을 가짐
제어장치: 제어 신호(Control Signal)라는 전기 신호를 내보내고 명령어를 해석하는 장치.
제어 신호란 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호
CPU가 메모리에 저장된 값을 읽고 싶을 땐 메모리를 향해 “메모리 읽기”라는 제어 신호를 보낸다.
CPU가 메모리에 어떤 값을 저장하고 싶을 땐 메모리를 향해 “메모리 쓰기”라는 제어 신호를 보낸다.
주기억장치(Main memory, 메모리)
현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품.
즉, 프로그램이 실행되려면 반드시 메모리에 저장되어 있어야 합니다.
메모리에 저장된 값의 위치는 주소로 알 수 있습니다.
보조기억장치(secondary storage)
메모리보다 크기가 크고 전원이 꺼져도 저장된 내용을 잃지 않는 메모리를 보조할 저장 장치
보조기억장치는 ‘보관할’ 프로그램을 저장한다고 생각해도 좋다.
입출력장치(input/output(I/O) device)
마이크, 스피커, 프린터, 마우스, 키보드처럼 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환하는 장치를 의미.
‘컴퓨터 주변에 붙어 있는 장치’라는 의미에서 “주변장치(peripheral device)”라 통칭하기도 함.
“주소”
컴퓨터가 빠르게 작동하기 위해서는 메모리 속 명령어와 데이터가 정돈된 위치에 저장되어 있어야 합니다.
그래서 메모리에는 저장된 값에 빠르게 효율적으로 접근하기 위해 주소(address)라는 개념이 사용됩니다.
주소로 메모리 내 원하는 위치에 접근할 수 있습니다.
메인보드와 시스템 버스
메인보드
마더보드(mother board)라고도 부름
메인보드에는 앞에서 소개한 부품을 비롯한 여러 컴퓨터 부품을 부착할 수 있는 슬록과 연결 단자가 있습니다.
메인 보드에 연력된 부품들은 서로 정보를 주고 받을수 있습니다. 이는 메인보드 내부에 “버스(bus)”라는 통로가 있기 때문입니다.
시스템 버스(system bus)
여러 버스 가운데 컴퓨터의 네 가지 핵심 부품을 연결하는 가장 중요한 버스입니다.
주소 버스, 데이터 버스, 제어 버스로 구성되어 있습니다.
주소 버스(address bus): 주소를 주고받는 통로
데이터 버스(data bus): 명령어롸 데이터를 주고 받는 통로
제어 버스(control bus): 제어 신호를 주고 받는 통로
키워드로 정리하는 핵심 포인트
컴퓨터가 이해하는 정보에는 “데이터” 와 “명령어” 가 있습니다.
“메모리” 는 현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품입니다.
“CPU” 는 메모리에 저장된 명령어를 읽어 들이고, 해석하고, 실행하는 부품입니다.
“보조기억장치” 는 전원이 꺼져도 보관할 프로그램을 저장하는 부품입니다.
“입출력장치” 는 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환할 수 있는 부품입니다.
“시스템 버스” 는 컴퓨터의 네 가지 핵심 부품들이 서로 정보를 주고받는 통로입니다.
Q1. “메모리 주소가 무엇이며, iOS 시스템 내에서 어떤 역할을 수행한다고 생각하나요?”
메모리 주소는 컴퓨터 메모리 내에서 데이터나 명령어의 위치를 식별하는 데 사용되는 고유한 식별자입니다. 각 바이트 또는 워드에는 메모리 내의 위치를 나타내는 고유한 주소가 있으며, 이를 통해 CPU와 다른 시스템 구성 요소가 필요한 데이터를 정확히 찾아 읽고 쓸 수 있습니다.
iOS 시스템 내에서 메모리 주소의 역할은 특히 중요합니다. iOS는 메모리 관리에 자동 참조 카운팅(ARC)를 사용하여 객체의 생명 주기를 관리합니다. ARC는 객체에 대한 참조가 더 이상 필요하지 않게 되면 자동으로 메모리를 해제합니다. 이 과정에서 메모리 주소를 사용하여 각 객체의 위치를 파악하고 관리합니다. 따라서, 개발자로서 메모리 주소의 이해는 메모리 누수를 방지하고 앱의 성능을 최적화하는 데 필수적입니다.
또한, 메모리 주소를 이해하는 것은 포인터를 사용한 프로그래밍, 메모리 접근 최적화, 그리고 다양한 메모리 관리 기법을 적용하는 데 중요합니다. 예를 들어, 효율적인 데이터 구조 설계, 대규모 데이터 처리, 멀티스레딩 환경에서의 데이터 공유와 동기화 문제 해결 등은 메모리 주소와 밀접한 관련이 있습니다.
iOS 시스템 내에서 메모리 주소의 관리와 최적화는 앱의 반응 속도, 안정성, 그리고 사용자 경험에 직접적인 영향을 미치기 때문에, 이를 정확히 이해하고 효과적으로 활용하는 능력은 iOS 개발자에게 매우 중요한 자질입니다.
Q2. “메모리 주소가 무엇이며, Java 시스템 내에서 어떤 역할을 수행한다고 생각하나요?”
“메모리 주소는 컴퓨터 메모리 내의 특정 위치를 식별하는 데 사용되는 고유한 식별자입니다. 이 주소를 통해, 컴퓨터 시스템은 메모리 내에서 데이터나 명령어를 정확히 찾아내어 읽고 쓸 수 있습니다. 간단히 말해, 메모리 주소는 컴퓨터 메모리 내의 ‘우편 주소’와 유사한 역할을 수행합니다.
Java 시스템 내에서, 메모리 주소의 역할은 Java 가상 머신(JVM)에 의해 추상화되어 다루어집니다. Java 개발자들은 직접적으로 메모리 주소를 다루지 않으며, 대신 Java가 제공하는 추상화된 메모리 모델을 사용하여 프로그래밍합니다. Java에서는 객체와 배열 등이 힙 메모리에 할당되며, 개발자는 이러한 객체에 대한 참조를 통해 메모리를 접근하게 됩니다. 여기서 ‘참조’는 실제 메모리 주소를 직접적으로 나타내지는 않지만, 특정 객체를 가리키는 역할을 합니다.
JVM은 가비지 컬렉션(Garbage Collection)을 통해 메모리 관리를 자동화합니다. 가비지 컬렉터는 더 이상 사용되지 않는 객체를 자동으로 검출하고, 그 메모리를 회수하여 재사용 가능하게 만듭니다. 이 과정에서 JVM은 내부적으로 메모리 주소를 관리하여, 효율적인 메모리 할당과 해제를 수행합니다.
따라서, Java 시스템 내에서 메모리 주소는 주로 메모리 할당, 객체 참조, 그리고 가비지 컬렉션과 같은 메모리 관리 작업에 중요한 역할을 수행합니다. Java 개발자로서 우리의 역할은 주로 안전하고 효율적인 코드 작성에 초점을 맞추며, JVM이 메모리 관리의 세부 사항을 추상화하고 처리하도록 합니다. 이렇게 함으로써, 개발자는 메모리 관리의 복잡성으로부터 벗어나 비즈니스 로직 구현에 더 집중할 수 있습니다.”
-
-
-
-
☕️[Java] 다운캐스팅과 주의점
다운캐스팅과 주의점.
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있습니다.
다음 코드를 통해 다운캐스팅에서 발생할 수 있는 문제를 확인해봅시다.
package poly.basic;
// 다운캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가
}
}
실행 결과
Child.childMethod
Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child (poly.basic.Parent and poly.basic.Child are in unnamed module of loader 'app')
at poly.basic.CastingMain4.main(CastingMain4.java:12)
실행 결과를 보면 child1.childMethod()는 잘 호출되었지만, child2.childMethod()는 실행되지 못하고, 그 전에 오류가 발생합니다.
예제의 parent1의 경우 다운캐스팅을 해도 문제가 되지 않습니다.
예제의 parent2를 다운캐스팅하면 ClassCastException 이라는 심각한 런타임 오류가 발생합니다.
이 코드를 자세히 알아봅시다.
Parent parent2 = new Parent()
먼저 new Parent()로 부모 타입으로 객체를 생성합니다.
따라서 메모리 상에 자식 타입은 전혀 존재하지 않습니다.
생성 결과를 parent2에 담아둡니다.
이 경우 같은 타입이므로 여기서는 문제가 발생하지 않습니다.
Child child2 = (Child) parent2
다음으로 parent2를 Child 타입으로 다운캐스팅합니다.
그런데 parent2는 Parent로 생성되었습니다.
따라서 메모리상에 Child 자체가 존재하지 않습니다. Child 자체를 사용할 수 없는 것입니다.
자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastExecption이라는 예외를 발생시킵니다.
예외가 발생하면 다음 동작이 실행되지 않고, 프로그램이 종료됩니다.
따라서 child2.childMethod() 코드 자체가 실행되지 않습니다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅의 경우 이런 문제가 절대로 발생하지 않습니다.
왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성되기 때문입니다!
따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전합니다.
따라서 캐스팅을 생략할 수 있습니다.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있습니다.
왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않습니다.
따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 합니다.
클래스 A, B, C는 상속 관계입니다.
new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성됩니다.
따라서 C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조할 수 있습니다.
“상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.”
A a = new C(): A로 업캐스팅
B b = new C(): B로 업캐스팅
C c = new C(): 자신과 같은 타입
new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성됩니다.
따라서 B의 부모 타입인 A, B 모두 B 인스턴스를 참조 할 수 있습니다.
상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.
하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있습니다.
A a = new B() : A로 업캐스팅
B b = new B() : 자신과 같은 타입
C c = new B() : 하위 타입은 대입할 수 없음, 컴파일 오류
C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생
컴파일 오류 vs 런타임 오류
컴파일 오류는 변수명 오류, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류입니다.
이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류 입니다.
반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류입니다.
런타임 오류는 매우 안좋은 오류입니다.
왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문입니다.
-
-
-
💾 [CS] 패턴 매칭(Pattern Matching)과 표현 매칭(Expression Matching)
패턴 매칭(Pattern Matching)과 표현 매칭(Expression Matching).
패턴 매칭과 표현 매칭은 프로그래밍 언어나 소프트웨어 개발에서 사용되는 두 가지 다른 개념입니다.
둘 다 데이터나 표현식의 구조를 분석하고 일치 여부를 판단하는 방법이지만, 적용되는 맥락과 목적에서 차이가 있습니다.
패턴 매칭(Pattern Matching).
데이터의 구조와 그 내용을 기반으로 한 매칭 방식입니다.
입력된 데이터가 특정 패턴이나 구조와 일치하는지를 검사합니다.
이를 통해 데이터의 타입, 값, 구조 등을 확인하고 , 그에 따른 처리를 분기하는 데 사용됩니다.
표현 매칭(Expression Matching)
특정 표현식이나 문자열이 주어진 패턴이나 규칙과 일치하는지를 확인하는 방법입니다.
주로 문자열 처리, 정규 표현식 사용, 텍스트 분석에서 널리 사용됩니다.
표현 매칭은 특정 패턴(예: 정규 표현식)을 정의하고, 대상 문자열이 이 패턴과 일치하는지 여부를 판단합니다.
이는 검색, 데이터 검증, 파싱 등 다양한 분야에서 활용됩니다.
차이점 요약
적용 분야
패턴 매칭은 주로 데이터의 구조와 타입을 다루는 함수형 프로그래밍에서 사용됩니다.
반면, 표현 매칭은 문자열이나 텍스트 데이터를 처리할 때 사용되는 패턴(예: 정규 표현식)과의 일치 여부를 확인하는 데 쓰입니다.
목적
패턴 매칭은 데이터의 구조를 통해 복잡한 데이터 타입을 효율적으로 분해하고 처리하는 데 중점을 둡니다.
표현 매칭은 문자열 내에서 특정 패턴의 존재 여부를 검사하고, 데이터를 검증하거나 추출하는 데 주로 사용됩니다.
사용 사례
패턴 매칭은 데이터 타입 분해, 조건 분기 처리 등에 사용되며, 함수형 프로그래밍 언어에서 자주 볼 수 있습니다.
표현 매칭은 로그 분석, 웹 페이지 파싱, 사용자 입력 검증 등 문자열 처리에 널리 사용됩니다.
두 방법은 각각의 사용 사례와 목적에 맞게 선택하여 사용되며, 프로그래밍에서의 다양한 문제를 해결하는 데 중요한 역할을 합니다.
-
💾 [CS] 컴퓨터 구조를 알아야 하는 이유
컴퓨터 구조를 알아야 하는 이유.
컴퓨터 구조는 실력 있는 개발자가 되려면 반드시 알아야 할 기본 지식입니다
why?
문제 해결
컴퓨터 구조를 이해하고 있다면 문제 상황을 빠르게 진단할 수 있고, 문제 해결의 실마리를 다양하게 찾을 수 있습니다.
컴퓨터 구조 지식은 다양한 문제를 스스로 해결할 줄 아는 개발자로 만들어 줍니다.
성능, 용량, 비용
“컴퓨터 구조애서 배우는 내용은 결국 성능, 용량, 비용과 직결됩니다.”
즉, 컴퓨터 구조를 이해하면 입력과 출력에만 집중하는 개발을 넘어 성능, 용량, 비용까지 고려하며 개발하는 개발자가 될 수 있습니다,
문제 해결
컴퓨터 구조를 이해하고 있다면 문제 상황을 빠르게 진단할 수 있고, 문제 해결의 실마리를 다양하게 찾을 수 있습니다.
컴퓨터 내부를 거리낌 없이 들여다보면 더 좋은 해결책을 고민할 수 있습니다.
이러한 사고가 가능한 이들에게 컴퓨터란 ‘미지의 대상’이 아닌 ‘분석의 대상’이기 때문입니다.
컴퓨터 구조 지식은 다양한 문제를 스스로 해결할 줄 아는 개발자로 만들어 줍니다.
성능, 용량, 비용
성능, 용량, 비용 문제는 프로그래밍 언어의 문법만 알아서는 해결하기 어렵습니다.
혼자만 사용하는 프로그램을 만들 떄는 이러한 문제를 생각조차 해 본 적이 없을 수도 있습니다.
하지만 유튜브, 워드, 포토샵과 같이 사용자가 많은 프로그램은 필연적으로 성능, 용량, 비용이 고려됩니다.
그래서 컴퓨터 구조를 아는 것은 매우 중요합니다.
“컴퓨터 구조애서 배우는 내용은 결국 성능, 용량, 비용과 직결됩니다.”
즉, 컴퓨터 구조를 이해하면 입력과 출력에만 집중하는 개발을 넘어 성능, 용량, 비용까지 고려하며 개발하는 개발자가 될 수 있습니다,
핵심 포인트
컴퓨터 구조를 이해하면 “문제 해결” 능력이 향상 됩니다.
컴퓨터 구조를 이해하면 문법만으로는 알기 어려운 “성능/용량/비용” 을 고려하며 개발할 수 있습니다.
Q1. iOS 애플리케이션 개발에서 고효율적이고 성능이 우수한 앱을 만들기 위해 컴퓨터 구조에 대한 이해가 왜 중요한지 설명해주세요. 구체적인 예를 들어서 설명해주시기 바랍니다.
답변.
iOS 애플리케이션 개발에서 컴퓨터 구조에 대한 이해는 여러 면에서 중요합니다.
첫째, 성능 최적와레 있어서 핵심적인 역할을 합니다.
예를 들어, CPU의 멀티코어 구조를 이해함으로써, 병렬 처리와 동시성을 통해 애플리케이션의 성능을 효과적으로 향상시킬 수 있습니다.
이는 앱이 사용자의 입력에 빠르게 반응하고, 더 복잡한 작업을 빠른 시간 안에 처리할 수 있게 만들어 줍니다.
둘째, 메모리 관리에 대한 이해를 통해, 애플리케이션의 효율성을 높일 수 있습니다.
예를 들어, RAM과 캐시의 작동 방식을 이해하면, 데이터를 효율적으로 저장하고 접근하는 방법을 개선할 수 있으며, 이는 애플리케이션의 반응 속도와 전반적인 성능에 긍정적인 영향을 미칩니다.
셋째, 하드웨어와 소프트웨어의 상호작용에 대한 이해는 에너지 효율성을 최적화하는 데 도움이 됩니다.
iOS 장치의 배터리 수명은 사용자 경험의 중요한 부분이며, 컴퓨터 구조에 대한 이해는 개발자가 배터리 소모를 최소화하면서 성능을 극대화할 수 있는 애플리케이션을 설계할 수 있게 돕습니다.
마지막으로 컴퓨터 구조에 대한 깊은 이해는 개발자가 효과적인 코드를 작성하고, 시스템 리소스를 효율적으로 관리하며, 최종 사용자에게 더 나은 경험을 제공할 수 있는 애플리케이션을 만들 수 있도록 합니다.
Q2. 서버 개발자가 컴퓨터 구조에 대한 기본적인 지식을 갖추어야 하는 이유에 대해 설명해주시고, 그 지식이 어떻게 서버 애플리케이션의 성능과 안정성에 영향을 미칠 수 있는지 구체적인 예시를 들어 설명해주세요.
답변.
서버 개발자에게 컴퓨터 구조에 대한 이해는 애플리케이션의 성능 최적화와 안정성 보장에 필수적입니다.
첫 번째 이유는 성능 최적화와 관련이 있습니다.
예를 들어, CPU의 멀티코어 아키텍처를 이해함으로써 서버 애플리케이션에서 멀티 스레딩과 병렬 처리를 효율적으로 구현할 수 있습니다.
이는 요청 처리량을 증가시키고 응답 시간을 단축시킬 수 있으며, 고객에게 더 나은 서비스 경험을 제공할 수 있습니다.
두 번째 이유는 자원 관리와 관련이 있습니다.
서버 애플리케이션은 종종 대량의 데이터를 처리하고, 메모리 및 CPU 자원을 집중적으로 사용합니다.
메모리 계층 구조(예: 캐시, 주 메모리)와 이에 대한 이해는 데이터 접근 시간을 최적화하고, 메모리 사용 효율을 극대화하는 방법을 개발자에게 제공합니다.
이를 통해 시스템의 전반적인 효율성을 향상시킬 수 있습니다.
세 번째 이유는 안정성과 가용성에 있습니다.
서버 개발자는 컴퓨터 구조에 대한 이해를 통해 시스템의 잠재적 한계와 병목 현상을 더 잘 파악할 수 있으며, 이를 바탕으로 더 견고하고 오류에 강한 시스템을 설계할 수 있습니다.
예를 들어, 서버 하드웨어의 장애 지점을 이해하고, 이에 대비한 높은 가용성을 보장하는 소프트웨어 설계를 할 수 있습니다.
종합하면, 컴퓨터 구조에 대한 깊은 이해는 서버 개발자가 성능, 자원 관리, 안정성을 고려한 효율적이고 안정적인 서버 애플리케이션을 설계하고 구현하는 데 있어 필수적입니다.
이는 최종적으로 사용자 경험을 개선하고, 비즈니스 목표 달성에 기여합니다.
-
-
-
-
☕️[Java] 상속과 접근 제어
상속과 접근 제어.
상속 관계와 접근 제어에 대해 알아보겠습니다.
참고로 접근 제어를 자세히 설명하기 위해 부모와 자식의 패키지를 따로 분리하였습니다.
접근 제어자를 표현하기 위해 UML 표기법을 일부 사용했습니다.
+: public
#: protected
~: default
-: private
접근 제어자를 잠시 복습해봅시다.
접근 제어자의 종류
private: 모든 외부 호출을 막습니다.
default(package-private): 같은 패키지 안에서 호출은 허용합니다.
protected: 같은 패키지 안에서 호출은 허용합니다.
패키지가 달라도 상속 관계의 호출은 허용합니다.
public: 모든 외부 호출을 허용합니다.
순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용합니다.
private -> default -> protected -> public
그림과 같이 다양한 접근 제어자를 사용하도록 코드를 작성해보겠습니다.
Parent
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
ExtendsAccessMain
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
Parent와 Child의 패키지가 다르다는 부분에 유의합시다.
자식 클래스인 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는지 확인해봅시다.
publicValue = 1;: 부모의 public 필드에 접근합니다. public이므로 접근할 수 있습니다.
protectedValue = 1;: 부모의 protected 필드에 접근합니다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있습니다.
defaultValue = 1;: 부모의 default 필드에 접근합니다. 자식과 부모가 다른 패키지이므로 접근할 수 없습니다.
privateValue = 1;: 부모의 private 필드에 접근합니다. private은 모두 외부 접근을 막으므로 자식이라도 호출할 수 없습니다.
메서드의 경우도 앞서 설명한 필드와 동일합니다.
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
코드를 실행해보면 Child.call() -> Parent.printParent() 순서로 호출합니다.
Child는 부모의 public, protexted 필드나 메서드만 접근할 수 있습니다.
반면에 Parent.printParent()의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있습니다.
접근 제어와 메모리 구조.
본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 줍니다.
왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문입니다.
결국 자식 차입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같습니다.
-
☕️[Java] 상속과 메서드 오버라이딩
상속과 메서드 오버라이딩.
부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있습니다.
예를 들어서 자동차의 경우 Car.move()라는 기능이 있습니다.
이 기능을 사용하면 단순히 “차를 이동합니다.” 라고 출력합니다.
전기차의 경우 보통 더 빠르기 때문에 전기차가 move()를 호출한 경우에는 “전기차를 빠르게 이동합니다.”라고 출력을 변경하고 싶습니다.
이렇게 부모에게서 상속 받은 기능을 자식이 재정의 하는 것을 “메서드 오버라이딩(Overriding)” 이라고 합니다.
package extends1.overriding - Car
package extends1.overriding;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
기존 코드와 같습니다.
package extends1.overriding - GasCar
package extends1.overriding;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.overriding - HydrogenCar
package extends1.overriding;
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
package extends1.overriding - ElectricCar
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않습니다.
메서드 이름은 같지만 새로운 기능을 사용하고 싶습니다.
그래서 ElectricCar의 move() 메서드를 새로 만들었습니다.
이렇게 부모의 기능을 자식이 새로 재정의하는 것을 “매서드 오버라이딩” 이라고 합니다.
이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 호출됩니다.
@Override
@이 붙은 부분을 애노테이션(어노테이션, annotattion)이라 합니다.
애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 됩니다.
이 애노테이션은 상위 클래스의 매서드를 오버라이드하는 것임을 나타냅니다.
이름 그대로 오버라이딩한 매서드 위에 이 애노테이션을 붙여야 합니다.
컴파일러는 이 애노테이션을 보고 매서드가 정확히 오버라이드 되었는지 확인합니다.
오바라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킵니다.
따라서 실수로 오버라이딩을 못하는 경우를 방지해줍니다.
예를 들어서 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생합니다.
참고로 이 기능은 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋습니다.
package extends1.overrding - CarMain
package extends1.overriding;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
GasCar gasCar = new GasCar();
gasCar.move();
}
}
실행 결과
전기차를 빠르게 이동합니다.
차를 이동합니다.
실행 결과를 보면 electricCar.move()를 호출했을 때 오버라이딩한 ElectricCar.move() 메서드가 실행된 것을 확인할 수 있습니다.
오버라이딩과 클래스
Car의 move() 메서드를 ElectricCar에서 오버라이딩 했습니다.
오버라이딩과 메모리 구조
electricCar.move()를 호출합니다.
호출한 electricCar의 타입은 ElectricCar입니다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작합니다.
ElectricCar 타입에 move() 메서드가 있습니다. 해당 메서드를 실행합니다. 이때 실행할 메서드를 이미 찾았으므로 부모 타입을 찾지 않스빈다.
오버로딩(Overloading)과 오버라이딩(Overriding)
메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라고 합니다.
오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻입니다.
따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 됩니다.
메서드 오버라이딩: 메서드 오버라이딩은 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미합니다.
따라서 상속 관계에서 사용합니다. 부모의 기능을 자식이 다시 정의하는 것입니다.
오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것을 말합니다.
자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 됩니다.
오버라이딩을 번역하면 무언가를 다시 정의한다고 해서 재정의라 합니다.
상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 됩니다.
실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용합니다.
메서드 오버라이딩 조건
메서드 오버라이딩은 다음과 같은 까다로운 조건을 가지고 있습니다.
다음 내용은 아직 학습하지 않은 내용들도 있으므로 이해하려고 하기 보다는 참고만 합시다.
지금은 단순히 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분합니다.
메서드 오버라이딩 조건
메서드 이름: 메서드 이름이 같아야 합니다.
메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 합니다.
반환 타입: 반환 타입이 같아야 합니다. 단, 반환 타입이 하위 클래스 타입일 수 있습니다.
접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안됩니다.
예를 들어, 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드 할 수 있지만, private 또는 default로 오버라이드 할 수 없습니다.
예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없습니다.
하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있습니다.
static, final, private: 키워드가 붙은 메서드는 오버라이딩 될 수 없습니다.
static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없습니다.
쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됩니다.
final 메서드는 재정의를 금지합니다.
private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않습니다.
따라서 오버라이딩 할 수 없습니다.
생성자 오버라이딩: 생성자는 오버라이딩 할 수 없습니다.
-
-
-
-
-
-
-
-
-
🆙[Cpp DataStructure] 안정성(stability) 확인
안정성(stability) 확인.
#include <iostream>
#include <cassert>
#include <fstream>
using namespace std;
struct Element
{
int key;
char value;
};
void Print(Element* arr, int size)
{
for (int i = 0; i < size; i++)
cout << arr[i].key << " ";
cout << endl;
for (int i = 0; i < size; i++)
cout << arr[i].value << " ";
cout << endl;
}
int main()
{
// 안정성 확인(unstable)
{
Element arr[] = { {2, 'a'}, {2, 'b'}, {1, 'c' } };
int size = sizeof(arr) / sizeof(arr[0]);
Print(arr, size);
int min_index;
for(int i = 0; i < (size - 1); i++)
{
min_index = i;
for (int j = (i + 1); j < size; j++)
{
if (arr[j].key < arr[min_index].key)
{
min_index = j;
}
}
swap(arr[i], arr[min_index]);
Print(arr, size);
}
}
return 0;
}
실행 결과
2 2 1
a b c
1 2 2
c b a
1 2 2
c b a
정렬의 안정성(stablity)이라는 개념이 있습니다.
이 개념은 stable한지 unstable한지로 구분합니다.
즉, 안정적인지 불안정적인지로 구분합니다.
위 코드에서 보면 Element 배열을 정렬합니다.
이 Èlement를 보면 다음과 같습니다.
struct Element
{
int key;
char value;
};
Element의 key는 정수이고 value는 문자입니다.
그래서 정렬할 때 key를 기준으로 정렬합니다.
그러면 swap시 value(문자)는 함께 따라갑니다. -> 복사를 하니 따라가는 것 입니다.
Element arr[] = { {2, 'a'}, {2, 'b'}, {1, 'c' } };
첫 번째 인덱스의 요소의 key는 2이고 value는 'a' 입니다.
두 번째 인덱스의 요소의 key는 2이고 value는 'b' 입니다.
세 번째 인덱스의 요소의 key는 1이고 value는 'c' 입니다.
“정렬시에는 key가 작은 순서대로 정렬되도록 할 것 입니다.”
즉, key를 기준으로 정렬합니다.
여기서 주목해야 할 점은 “첫 번째 인덱스와 두 번째 인덱스의 요소의 Value”입니다.
두 인덱스의 요소의 key 값은 같으나 value 값은 다릅니다.
비교 로직을 보면 key값을 가지고 비교를 하지만 swap 시에는 key와 value를 모두 가지고 움직입니다.
실행 결과를 보고 value와 안정성의 상관 관계에 대해 알아봅시다.
실행 결과
2 2 1
a b c
1 2 2
c b a
1 2 2
c b a
실행 결과를 보면 (2 a), (2, b), (1, c)에서 “(1, c), (2, b), (2, a)” 순으로 정렬된 것을 볼 수 있습니다.
처음에는 (2 a), (2, b) 순서였지만 정렬 후에는 “(2, b), (2, a)” 로 순서가 바뀌어 정렬되었습니다.
value가 뒤바뀐 것을 알 수 있습니다.
key가 정렬이 되었으므로 정렬은 잘 되었다고 볼 수 있습니다.
그러나 “key가 같은 값일 경우에는 value의 순서가 a, b에서 b, a 로 바뀌었습니다.”
“여기서 stable한 것과 unstable한 것의 차이점이 나타납니다.”
“키 값이 같아도 정렬시 벨류 값이 처음과 같이 유지가 된다면 그것은 stable하다라고 합니다, 그와 반대로 키 값이 같으나 처음과 달리 벨류의 순서가 바뀔 경우에는 unstable하다 라고 합니다.”
이것도 정렬 알고리즘을 분류하는 기준 중 하나입니다.
-
-
☕️[Java] static 변수3
static 변수 3
이번에는 static 변수를 정리해보겠습니다.
용어 정리
public class Data3 {
public String name;
public static int count; // static
}
예제 코드에서 name, count는 둘 다 멤버 변수입니다.
멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류 할 수 있습니다.
멤버 변수(필드)의 종류.
인스턴스 변수: static이 붙지 않은 멤버 변수, 예) name
static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다.
따라서 인스턴스 변수라 합니다.
인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어집니다.
클래스 변수: static이 붙은 멤버 변수, 예) count
클래스 변수, 정적 변수, static 변수등으로 부릅니다. 용어를 모두 사용하니 주의합시다.
static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있습니다.
따라서 클래스 변수라 합니다.
클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어집니다.
인스턴스와는 다른게 보통 여러곳에서 공유하는 목적으로 사용됩니다.
변수와 생명주기
지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관됩니다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됩니다.
따라서 지역 변수는 생존 주기가 짧습니다.
인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 합니다. 인스턴스 변수는 힙 영역을 사용합니다.
힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지 생존하기 때문에 보통 지역 변수보다 생존 주기가 깁니다.
클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관 되는 변수입니다.
메서드 영역은 프로그램 전체에서 사용하는 공용 공간입니다.
클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성됩니다.
그리고 JVM이 종료될 때까지 생명주기가 이어집니다.
따라서 가장 긴 생명주기를 가집니다.
static이 정적이라는 이유는 바로 여기에 있습니다.
힙 역역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거됩니다.
반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다.
정적 변수는 이름 그대로 정적입니다.
정적 변수 접근 법
static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다.
DataCountMain3 마지막 코드에 다음 부분을 추가하고 실행해보겠습니다.
DataCountMain3 - 추가
// 추가
// 인스턴스를 통한 접근
Data3 data4 = new Data3("D");
// 클래스를 통합 접근
System.out.println(Data3.count);
실행 결과 - 추가된 부분
4
4
둘 다 차이는 없습니다. 둘다 결과적으로 정적 변수에 접근합니다.
인스턴스를 통한 접근 data4.count
정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않습니다.
왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문입니다.
클래스를 통한 접근 Data3.count
정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확합니다.
따라서 정적 변수에 접근할 때는 클래스를 통해서 접근합시다.
-
-
☕️[Java] static 메서드 2
static 메서드 2
정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있습니다.
하지만 정적 메서드는 언제나 사용할 수 있는 것이 아닙니다.
정적 메서드 사용법
static 메서드는 static만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없습니다.
반대로 모든 곳에서 static을 호출할 수 있습니다.
정적 메서드는 공용 기능입니다.
따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있습니다.
예제를 통해 정적 메서드의 사용법을 확인해봅시다.
DecoData
package static2;
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
//instanceValue++; // 인스턴스 변수 접근, compile error
//instanceMethod(); // 인스턴스 메서드 접근, compile error
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
public void instanceCall() {
instanceValue++; // 인스턴스 변수 접근
instanceMethod(); // 인스턴스 메서드 접근
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue);
}
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
이번 예제에서는 접근 제어자를 적극 활용해서 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었습니다.
instanceValue는 인스턴스 변수입니다.
staticValue는 정적 변수(클래스 변수)입니다.
instanceMethod()는 인스턴스 메서드입니다.
staticMethod()는 정적 메서드(클래스 메서드)입니다.
staticCall() 메서드를 봐봅시다.
이 메서드는 정적 메서드입니다.
따라서 static 만 사용할 수 있습니다.
정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생합니다.
코드를 보면 staticCall() -> staticMethod()로 static에서 static을 호출하는 것을 확인할 수 있습니다.
instanceCall() 메서드를 봐봅시다.
이 메서드는 인스턴스 메서드입니다.
모든 곳에서 공용인 static을 호출할 수 있습니다.
따라서 정적 변수, 정적 메서드에 접근할 수 있습니다.
물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있습니다.
DecoDataMain
package static2;
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
DecoData.staticCall();
System.out.println("2. 인스턴스 호출1");
DecoData data1 = new DecoData();
data1.instanceCall();
System.out.println("3. 인스턴스 호출2");
DecoData data2 = new DecoData();
data2.instanceCall();
}
}
실행 결과
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출2
instanceValue=1
staticValue=3
정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있습니다.
그래서 인스턴스처럼 참조값의 개념이 없습니다.
특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출합니다.
따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없습니다.
물론 당연한 이야기지만 다음과 같이 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있습니다.
public static void staticCall(DecoData data) {
data.instanceValue++;
data.instanceMethod();
}
-
-
-
-
-
-
-
☕️[Java] 자바 메모리 구조
자바 메모리 구조.
자바 메모리 구조 - 비유
자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역, 3개로 나눌 수 있습니다.
메서드 영역: 클래스 정보를 보관합니다. 이 클래스 정보가 붕어빵 틀입니다.
스택 영역: 실제 프로그램이 실행되는 영역입니다. 메서드를 실행할 때 마다 하나씩 쌓입니다.
힙 영역: 객체(인스턴스)가 생성되는 영역입니다. new 명령어를 사용하면 이 영역을 사용합니다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간입니다.
참고로 배열도 이 영역에 생성됩니다.
위 설명한 내용은 쉽게 비유로 한 것이고 실제는 다음과 같습니다.
메서드 영역(Method Are): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리합니다. 이영역은 프로그램의 모든 영역에서 공유합니다.
클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재합니다.
static영역: static 변수들을 보관합니다.
런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관합니다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리합니다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리합니다.
스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성됩니다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함합니다.
스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임입니다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거됩니다.
힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역입니다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됩니다.
참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성됩니다. 따라서 쓰레드 수 만큼 스택 영역이 생성됩니다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나입니다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있습니다.
메서드 코드는 메서드 영역에
자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생깁니다.
각각의 인스턴스는 내부에 변수와 메서드를 가집니다.
같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유합니다.
따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없습니다.
메서드는 메서드 영역에서 공통으로 관리되고 실행됩니다.
정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행합니다.
-
☕️[Java] 캡슐화
캡슐화
캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다.
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말합니다.
캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있습니다.
캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 너머지는 모두 내부로 숨기는 것입니다.
이전에 객체 지향 프로그래밍을 설명하면서 캡슐화에 대해 알아보았습니다.
이때는 데이터와 데이터를 처리하는 메서드를 하나로 모으는 것에 초점을 맞추었습니다.
여기서 한발짝 더 나아가 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 “접근 제어자” 입니다.
그럼 어떤 것을 숨기고 어떤 것을 노출해야 할까요?
1. 데이터를 숨겨라.
객체에는 속성(데이터)과 기능(메서드)이 있습니다.
캡슐화에서 가장 필수로 숨겨야 하는 것은 “속성(데이터)” 입니다.
package access;
public class Speaker {
private int volume;
Speaker(int volume) {
this.volume = volume;
}
void volumeUp() {
if (volume >= 100) {
System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
} else {
volume += 10;
System.out.println("음량을 증가합니다.");
}
}
void volumeDown() {
volume -= 10;
System.out.println("volumeDown 호출");
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
위 코드에서의 Speaker의 volume을 봐봅시다.
객체 내부의 데이터를 외부에서 함부로 접근하게 둔다면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있습니다.
결국 모든 안전망을 다 빠져나가게 됩니다.
따라서 캡슐화가 깨집니다.
우리가 자동자를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않습니다.
단지 자동차가 제공하는 액셀 기능을 사용해서 액셀을 밟으면 자동차가 나머지는 다 알아서 하는 것입니다.
우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려봅시다.
음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나, 볼륨 상태의 데이터를 직접 수정할 일이 있을까요?
우리는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지는 않습니다.
전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일입니다.
볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일입니다.
쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것입니다.
복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아닙니다.
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 합니다.
2. 기능을 숨겨라.
“객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있습니다.”
이런 기능도 모두 감추는 것이 좋습니다.
우리가 자동차를 운정하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리는 알 필요가 없습니다.
우리는 단지 렉셀과 핸들 정도의 기능만 알면 됩니다.
만약 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 합니다.
“사용자 입장에서 꼭 필요한 기능만 외부에 노출합시다. 나머지 기능은 모두 내부로 숨깁시다.”
“정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화입니다.”
이번에는 잘 캡슐화된 예제를 하나 만들어봅시다.
BankAccount
package access;
public class BankAccount {
private int balance;
public BankAccount() {
balance = 0;
}
// public 메서드: deposit
public void deposit(int amount) {
if (isAmountValid(amount)) {
balance += amount;
System.out.println(amount + "원이 입금되었습니다.");
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
// public 메서드: withdraw
public void withdraw(int amount) {
if (isAmountValid(amount) && (balance - amount >= 0)) {
balance -= amount;
System.out.println(amount + "원이 출금되었습니다.");
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드: getBalance
public int getBalance() {
return balance;
}
private boolean isAmountValid(int amount) {
// 금액이 0보다 커야함.
return amount > 0;
}
}
BankAccountMain
package access;
public class BankAccountMain {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(10000);
account.withdraw(3000);
System.out.println("balance = " + account.getBalance());
}
}
위 코드는 은행 계좌 기능을 다룹니다.
위 코드는 다음과 같은 기능을 가지고 있습니다.
private
balance : 데이터 필드는 외부에 직접 노출하지 않습니다. BankAccount가 제공하는 메서드를 통해서만 접근할 수 있슷빈다.
isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요한 기능입니다. 따라서 private을 사용했습니다.
pulbic
deposit() : 입금
withdraw() : 출금
getBalance() : 잔고
BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 됩니다.
나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있습니다.
“만약 isAmountValid()를 외부에 노출할 경우 어떻게될까요?”
BankAccount를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었습니다.
여러분이 BankAccount를 사용하는 개발자라면 어떤 생각을 할까요?
아마도 입금과 출금 전에 본인이 먼저 isAmountValid()를 사용해서 검증을 해야 하나? 라고 의문을 가질 것입니다.
“만약 balance 필드를 외부에 노출하면 어떻게 될까요?”
BankAccount를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있습니다.
“왜냐하면 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기 때문입니다.”
“결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있습니다.”
“접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있습니다.”
-
💾 [CS] 컴퓨터의 구성
컴퓨터의 구성.
1. 컴퓨터가 시스템은 크게 어떻게 나누어 지나요?
컴퓨터 시스템은 크게 하드웨어와 소프트웨어로 나누어집니다.
하드웨어는 컴퓨터를 구성하는 기계적 장치입니다.
중앙처리장치(CPU)
기억장치: RAM, HDD
입출력 장치: 마우스, 프린터
소프트웨어는 하드웨어의 동작을 지시하고 제어하는 명령어 집합입니다.
시스템 소프트웨어: 운영체제, 컴파일러
응용 소프트웨어: 워드프로세서, 스프레드시트
1.1 명령어란 무엇일까요?
명령어는 컴퓨터에게 무엇을, 어떻게 해야 하는지를 알려주는 지시사항입니다.
콤퓨터는 이러한 명령어들을 해석하고 실행하여 다양한 작업을 수행합니다.
명령어 구성 요소
연산자(Operation Code, Opcode) : 수행해야 할 기본적은 작업의 유형을 나타냅니다. 예를 들어, 데이터를 더하거나 빼거나, 저장하는 등의 작업이 이에 해당합니다.
피연산자(Operand) : 연산자가 작용할 데이터나, 그 데이터가 위치한 메모리 주소를 가리킵니다. 즉, 연산자가 어떤 데이터에 대한 작업을 수행할지를 지정합니다.
결과(Result) : 연산의 결과를 저장할 위치입니다. 이는 명령어에 따라 명시적으로 주어지거나, 특정 규칙에 따라 암시적으로 결정될 수 있습니다.
명령어들은 프로그래밍 언어로 작성되며, 고급 프로그래밍 언어에서 작성된 코드는 컴파일러나 인터프리터를 통해 기계어로 번역되어 컴퓨터가 이해할 수 있는 형태로 변환됩니다.
기계어는 컴퓨터의 프로세서가 직접 실행할 수 있는 매우 기본적이고 낮은 수준의 명령어 집합입니다.
2. 하드웨어란 무엇인가요?
하드웨어는 중앙처리장치(CPU), 기억장치, 입출력장치로 구성되어 있으며 이들은 시스템 버스로 연결되어 있습니다.
시스템 버스는 데이터와 명령 제어 신호를 각 장치로 실어나르는 역할을 합니다.
2.2 중앙처리장치(CPU)란 무엇인가요?
인간으로 따지면 두뇌에 해당하는 부분입니다.
주기억장치에서 프로그램 명령어와 데이터를 읽어와 처리하고 명령어의 수행 순서를 제어합니다.
중앙처리장치는 비교와 연산을 담당하는 산술논리연산장치(ALU)와 명령어의 해석과 실행을 담당하는 제어장치, 속도가 빠른 데이터 기억장소인 레지스터로 구성되어 있습니다.
개인용 컴퓨터와 같은 소형 컴퓨터에서는 CPU를 마이크로프로세서라고도 부릅니다.
2.3 기억장치란 무엇인가요?
프로그램, 데이터, 연산의 중간 결과를 저장하는 장치입니다.
기억장치는 주기억장치와 보조기억 장치로 나누어집니다.
RAM과 ROM도 이곳에 해당합니다.
실행중인 프로그램과 같은 프로그램에 필요한 데이터를 일시적으로 저장합니다.
보조기억장치는 하드디스크 등을 말하며, 주기억장치에 비해 속도는 느리지만 많은 자료를 영구적으로 보관할 수 있는 장점이 있습니다.
2.4 입출력장치란 무엇인가요?
먼저 입출력장치는 입력과 출력 장치로 나뉘어집니다.
입력 장치는 컴퓨터 내부로 자료를 입력하는 장치인 키보드, 마우스등이 이에 속합니다.
출력 장치는 컴퓨터에서 외부로 표현하는 장치인 프린터, 모니터, 스피커등이 이에 속합니다.
3. 시스템 버스란 무엇인가요?
시스템 버스는 하드웨어 구성 요소를 물리적으로 연결하는 선을 말합니다.
시스템 버스는 각 구성요소가 다른 구성요소로 데이터를 보낼 수 있도록 통로가 되어줍니다.
시스템 버스는 용도에 따라 데이터 버스, 주소 버스, 제어 버스로 나뉘어집니다.
3.1 데이터 버스란 무엇인가요?
데이터 버스란 중앙처리장치와 기타 장치 사이에서 데이터를 전달하는 통로를 말합니다.
기억장치와 입출력장치의 명령어와 데이터를 중앙처리장치로 보내거나, 중앙처리장치의 연산 결과를 기억장치와 입출력장치로 보내는 ‘양방향’ 버스입니다.
3.2 주소 버스란 무엇인가요?
주소 버스는 중앙처리장치가 주기억장치나 입출력장치로 기억장치 주소를 전달하는 통로입니다.
주소버스는 그렇기 때문에 ‘단방향’ 버스입니다.
데이터를 정확히 실어나르기 위해서는 기억장치’주소’를 정해주어야 합니다.
3.3 제어 버스
제어 버스는 중앙처리장치가 기억장치나 입출력장치에 제어 신호를 전달하는 통로입니다.
제어 신호의 종류에는 기억장치 읽기 및 쓰기, 버스 요청 및 승인, 인터럽트 요청 및 승인, 클락, 리셋 등이 있습니다.
제어 버스는 읽기 동작과 쓰기 동작을 모두 수행하기 때문에 ‘양방향’ 버스입니다.
제어 버스가 필요한 이유는 주소 버스와 데이터 버스는 모든 장치에 공유되는데 이때 이를 제어할 수단이 필요하기 때문입니다.
4. 컴퓨터의 데이터 처리과정
컴퓨터는 기본적으로 읽고 처리한 뒤 저장하는 과정으로 이루어집니다. (READ -> PROCESS -> WRITE)
이 과정을 진행하면서 끊임없이 주기억장치(RAM)과 소통합니다.
이때 운영체제가 64bit라면, CPU는 RAM으로부터 데이터를 한번에 64bit씩 읽어옵니다.
-
-
-
-
-
-
🆙[Cpp DataStructure] 교환(Swap)과 정렬(Sort)
Swap(교환)
먼저 아래의 코드를 보고 a와 b를 교환해봅시다.
#include <iostream>
using namespace std;
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
// TODO:
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
3 2
“TODO” 에는 어떤 코드가 들어가야 할까요?
“우리가 양 손에 사과🍎와 레몬🍋을 들고 있다고 생각해봅시다.”
그럼 왼손에는 사과🍎와 오른손에는 레몬🍋을 들고 있을 때 사과🍎와 레몬🍋을 바꾸려면 어떻게 해야할까요?
저는 하나의 접시를 가져와 그 접시에 사과 또는 레몬을 잠시 올려두고 비어있는 손으로 과일을 옮긴 뒤 접시에 있는 과일을 집을것 입니다.
그럼 코드도 똑같이 만들 수 있지 않을까요?
#include <iostream>
using namespace std;
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
// TODO:
// 먼저 a를 사과 b를 레몬이라고 생각하고,
// temp라는 접시를 만들어보겠습니다.
// 그 접시에 a라는 사과를 올려보겠습니다.
int temp = a;
// 그럼 비어있는 손으로 레몬을 옮길 수 있게되었네요.
// 비어있는 손으로 레몬을 옮겨보겠습니다.
// a가 있던 손으로 b를 옮깁니다.
a = b;
// 이번에는 b가 있던 손이 비었네요.
// b가 있던 손으로 접시(temp)에 있는 a를 들어보겠습니다.
temp = a;
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
양 손에 있던 사과(3)과 레몬(2)의 자리가 바뀌었습니다
“즉, 교환(Swap)이 이루어졌습니다.”
하지만 항상 이렇게 3줄의 라인인
int temp = a;
a = b;
b = temp;
위 코드처럼 코드를 만들어서 사용할 경우에는 매우 많은 교환(Swap)을 해야할 경우 코드의 양도 늘어나고 가독성도 좋지 않은 것 입니다.
“그럼 이번에는 함수를 이용해서 두 숫자를 교환해봅시다.”
먼저, 위 교환 코드를 그대로 가져다가 사용해볼까요?
void MySwap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
이 경우에는 리턴 값이 없기 때문에 불가능합니다.
만약 리턴값이 int 형이라고 해도 리턴은 1개만 가능하기 때문에 어렵습니다.
cpp의 다른 기능인 구조체나 여러 기능을 사용해야 할 것입니다.
그러면 어떻게 해야할까요?
“C의 포인터와 CPP의 레퍼런스를 활용하면됩니다.”
1. C의 포인터 활용
#include <iostream>
using namespace std;
void MySwapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
MySwapPtr(&a, &b);
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
실행 결과는 올바르게 나왔습니다.
“C 스타일의 포인터 사용은 *(별)을 사용하면 됩니다.”
void MySwapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
temp에 먼저 a의 주소값을 넣어 줍니다.
이후 a의 주소값에 b의 주소값을 넣어 줍니다.
b의 주소값에는 temp(a의 주소값)을 넣어줍니다.
“이렇게 되면 각 주소값이 교환이 됩니다.”
C 스타일의 단점은 선언과 호출시에 나타납니다.
선언시에는 위와 같이 *을 모두 붙여줘야 합니다.
호출시에는 아래와 같이 &를 붙여줘야 합니다.
MySwapPtr(&a, &b);
“하지만 CPP 스타일의 래퍼런스 교환은 단순합니다.”
// 선언시
void MySwapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 호출시
MySwapRef(a, b);
내부 구현은 똑같지만 별다른 어노테이션 없이 일반적인 변수 할당과 같이 해주면 래퍼런스 대입되고 교환이 이루어지는 것을 볼 수 있습니다.
호출시에도 어노테이션을 따로 붙일 필요없이 일반적인 매개변수(parameter)를 넣어주듯이 넣어주면 됩니다.
#include <iostream>
using namespace std;
void MySwapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
MySwapRef(a, b);
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
“이번에는 교환을 활용해서 정렬을 해보겠습니다.”
값과 상관 없이 항상 작은 값이 먼저 출력되게 하려면 어떻게 해야할까요?
즉, 두 값이 같을 때는 순서가 상관이 없지만 큰 값이 먼저 출력되지 않게 해야합니다.
먼저 두 값이 같지 않거나 큰 값이 먼저 출력 되었을 경우에 false를 출력하고 그와는 반대일 경우에는 true를 출력하는 코드를 작성해보겠습니다.
#include <iostream>
using namespace std;
// 정렬(sorting)
int main() {
int arr[2];
// TODO:
for (int j = 0; j < 5; j++) {
for (int i = 0; i < 5; i++) {
arr[0] = i;
arr[1] = j;
cout << boolalpha;
cout << arr[0] << " " << arr[1] << " "
<< (arr[0] <= arr[1]) << endl;
}
}
return 0;
}
먼저 배열을 선언합니다. 배열은 순서가 있기 때문입니다.
그리고 2중 for문을 사용합니다. 첫 번째 for문이 1번 돌 때 두 번째 for문은 5번 돌게됩니다.
그렇게 각각을 i와 j에 라는 변수의 이름으로 arr 배열 인덱스 0번째와 1번째에 넣어줍니다.
실행 결과
0 0 true
1 0 false
2 0 false
3 0 false
4 0 false
0 1 true
1 1 true
2 1 false
3 1 false
4 1 false
0 2 true
1 2 true
2 2 true
3 2 false
4 2 false
0 3 true
1 3 true
2 3 true
3 3 true
4 3 false
0 4 true
1 4 true
2 4 true
3 4 true
4 4 true
실행 결과 값이 작거나 같은 값이 인덱스 0번 즉 오름차순일 경우에는 true 입니다.
그와는 반대로 큰 값이 인덱스 0번 즉 내림차순일 경우에는 false 입니다.
“이제 두 값을 비교하여 오름차순으로 정렬되는 것을 확인하는 함수를 만들었으니 이번에는 실제 정렬을 해보도록하겠습니다.”
#include <iostream>
using namespace std;
bool CheckSorted(int a, int b) {
// TODO: ...
if (a <= b) {
return true;
} else {
return false;
}
}
// 정렬(sorting)
int main() {
int arr[2];
// TODO:
for (int j = 0; j < 5; j++) {
for (int i = 0; i < 5; i++) {
arr[0] = i;
arr[1] = j;
// swap 소개
if (arr[0] > arr[1]) {
swap(arr[0], arr[1]);
}
cout << boolalpha;
cout << arr[0] << " " << arr[1] << " "
<< (CheckSorted(arr[0], arr[1])) << endl;
}
}
return 0;
}
실행 결과
0 0 true
0 1 true
0 2 true
0 3 true
0 4 true
0 1 true
1 1 true
1 2 true
1 3 true
1 4 true
0 2 true
1 2 true
2 2 true
2 3 true
2 4 true
0 3 true
1 3 true
2 3 true
3 3 true
3 4 true
0 4 true
1 4 true
2 4 true
3 4 true
4 4 true
CPP에는 swap이라는 함수가 있습니다 swap을 사용할 경우에는 매개변수로 받은 두 값을 바꿔줍니다.
따라서 위와 같은 실행 결과를 출력합니다.
-
-
-
-
-
☕️[Java] 생성자 - 오버로딩 this()
생성자 - 오버로딩 this()
생성자도 메서드 오버로딩처럼 매개변수만 다르게 해서 여러 생성자를 제공할 수 있습니다.
MemberConstruct - 생성자 추가
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
기존 MemberConstruct에 생성자를 하나 추가해서 생성자가 2개가 되었습니다.
MemberConstruct(String name, int age)
MemberConstruct(String name, int age, int grade)
새로 추가한 생성자는 grade를 받지 않습니다. 대신에 grade는 50점이 됩니다.
package construct;
public class ConstructMain2 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
이름:user1 나이:15성적:90
이름:user2 나이:16성적:50
생성자를 오버로딩 한 덕분에 성적 입력이 꼭 필요한 경우에는 grade가 있는 생성자를 호출하면 되고, 그렇지 않은 경우에는 grade가 없는 생성자를 호출하면 됩니다.
grade가 없는 생성자를 호출하면 성적은 50점이 됩니다.
this()
두 생성자를 비교해 보면 코드가 중복되는 부분이 있습니다.
public MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
public MemberConstruct(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
바로 다음 부분입니다.
this.name = name;
this.age = age;
이때 this()라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있습니다.
참고로 this는 인스턴스 자신의 참조값을 가리킵니다. 그래서 자신의 생성자를 호출한다고 생각하면됩니다.
MemberConstruct - this() 사용
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this(name, age, 50);
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
이 코드는 첫번째 생성자 내부에서 두번째 생성자를 호출합니다.
MemberConstruct(String name, int age) -> MemberConstruct(String name, int age, int grade)
this()를 사용하면 생성자 내부에서 다른 생성자를 호출할 수 있습니다.
이 부분을 잘 활용하면 지금과 같이 중복을 제거할 수 있습니다.
물론 실행 결과는 기존과 같습니다.
this() 규칙
this()는 생성자 코드의 첫줄에만 작성할 수 있습니다.
다음은 규칙 위반입니다.
이 경우 컴파일 오류가 발생합니다.
public MemberConstruct(String name, int age) {
Sytem.out.println("go");
this(name, age, 50);
}
this()가 생성자 코드의 첫줄에 사용되지 않았습니다.
-
-
☕️[Java] this
this
Member - initMember() 추가
package construct;
public class Member {
String name;
int age;
int grade;
// 추가
void initMember( String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}
MethodInitMain3
package construct;
public class MethodInitMain3 {
public static void main(String[] args) {
Member member1 = new Member();
member1.initMember("user1", 15, 90);
Member member2 = new Member();
member2.initMember("user2", 16, 80);
Member[] members = { member1, member2 };
for (Member member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
initMember(...)는 Member에 초기값 설정 기능을 제공하는 메서드입니다.
다음과 같이 메서드를 호출하면 객체의 멤버 변수에 인자로 넘어온 값을 채웁니다.
member1.initMember("user1", 15, 90)
this
Member의 코드를 다시 봐봅시다.
initMember(String name...)의 코드를 보면 메서드의 매개변수에 정의한 String name과 Member의 멤버 변수의 이름이 String name으로 둘다 똑같습니다.
나머지 변수 이름도 name, age, grade로 모두 같습니다.
“멤버 변수와 메스더의 매개변수의 이름이 같으면 둘을 어떨게 구분해야 할까요?”
이 경우 멤버 변수보다 매개변수가 코드 블럭의 안쪽에 있기 때문에 “매개변수가 우선 순위를” 가집니다.
따라서 initMember(String name, ...) 메서드 안에서 name이라고 적으면 매개변수에 접근하게 됩니다.
멤버 변수에 접근하려면 앞에 this.이라고 해주면 됩니다.
여기서 this는 인스턴스 자신의 참조값을 가리킵니다.
진행 과정
this.name = name; // 1. 오른쪽의 name은 매개변수에 접근
this.name = "user"; // 2. name 매개변수의 값 사용
x001.name = "user"; // 3. this.은 인스턴스 자신의 참조값을 뜻함. 따라서 인스턴스의 멤버 변수에 접근.
this 제거
만약 이 예제에서 this를 제거하면 어떻게 될까요?
this.name = name
다음과 같이 수정하면 name은 둘다 매개변수를 뜻하게 됩니다.
따라서 멤버변수의 값이 변경되지 않습니다.
name = name;
정리
매개변수의 이름과 멤버 변수의 이름이 같은 경우 this를 사용해서 둘을 명확하게 구분해야 합니다.
this는 인스턴스 자신을 가리킵니다.
this의 생략
this는 생략할 수 있습니다.
이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수입니다.)를 먼저 찾고 없으면 그 다음으로 멤버 변수를 찾습니다.
멤버 변수도 없으면 오루가 발생합니다.
다음 예제는 필드 이름과 매개변수의 이름이 서로 다릅니다.
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
nameField = nameParameter;
}
}
“예를 들어서 nameField는 앞에 this가 없어도 멤버 변수에 접근합니다.”
nameField는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 없으므로 멤버 변수에서 찾습니다.
nameParametr는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 매개변수가 있으므로 매개변수를 사용합니다.
this와 코딩 스타일
“다음과 같이 멤버 변수에 접근하는 경우에 항상 this를 사용하는 코딩 스타일도 있습니다.”
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
this.nameField = nameParameter;
}
}
this.nameField를 보면 this를 생략할 수 있지만, 생략하지 않고 사용해도 됩니다.
이렇게 this를 사용하면 이 코드가 멤버 변수를 사용한다는 것을 눈으로 쉽게 확인할 수 있습니다.
그래서 과거에 이런 스타일을 많이 사용하기도 했습니다.
쉽게 이야기해서 this를 강제로 사용해서, 지역 변수(매개변수)와 멤버 변수를 눈에 보이도록 구분하는 것입니다.
“하지만 최근에는 IDE가 발전하면서 IDE가 멤버 변수와 지역 변수를 색상으로 구분해줍니다.”
이런 점 때문에 this는 꼭 필요한 경우에만 사용해도 충분하다 생각한다.
예를 들어 이런 경우 this.name = name -> name이 중복되는 것.
-
☕️[Java] 생성자 - 도입
생성자 - 도입
프로그래밍을 하다보면 객체를 생성하고 이후에 바로 초기값을 할당해야 하는 경우가 많습니다.
따라서 앞서 initMember(...)와 같은 메서드를 매번 만들어야 합니다.
“그래서 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 필요한 기능을 좀 더 편리하게 수행할 수 있도록 생성자라는 기능을 제공합니다.”
생성자를 사용하면 객체를 생성하는 시점에 즉시 필요한 기능을 수행할 수 있습니다.
생성자는 앞서 살펴본 initMember(...)와 같이 메서드와 유사하지만 몇가지 다른 특징이 있습니다.
아래 코드를 보면서 이해해봅시다.
MemberConstruct
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
“다음 부분이 바로 생성자입니다.”
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
“생성자는 메서드와 비슷하지만 다음과 같은 차이가 있습니다.”
생성자의 이름은 클래스 이름과 같아야 합니다. 따라서 첫 글자도 대문자로 시작합니다.
생성자는 반환 타입이 없습니다. 비워두워야 합니다.
나머지는 메서드와 같습니다.
ConstructMain1
package construct;
public class ConstructMain1 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16, 80);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
생성자 호출 name=user2,age=16,grade=80
이름:user1 나이:15성적:90
이름:user2 나이:16성적:80
생성자 호출
생성자는 인스턴스를 생성하고 나서 즉시 호출됩니다.
생성자를 호출하는 방법은 다음 코드와 같이 new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 됩니다.
new 생성자이름(생성자에 맞는 인수 목록)
new 클래스이름(생성자에 맞는 인수 목록)
참고로 생성자 이름이 클래스 이름이기 때문에 둘다 맞는 표현입니다.
new MemberConstruct("user1", 15, 90)
이렇게 하면 인스턴스를 생성하고 즉시 해당 생성자를 호출합니다.
여기서는 Memeber 인스턴스를 생성하고 바로 MemberConstruct(String name, int age, int grade) 생성자를 호출합니다.
참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호()도 포함해야 하는 이유가 바로 생성자 때문입니다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함합니다.
생성자 장점
중복 호출 제거
생성자가 없던 시절에는 생성 직후에 어떤 작업을 수행하기 위해 다음과 같이 메서드를 직접 한번 더 호출해야 했습니다.
“생성자 덕분에 객체를 생성하면서 동시에 생성 직후에 필요한 작업을 한번에 처리할 수 있게 되었습니다.”
// 생성자 등장 전
MemberInit member = new MemberInit();
member.initMember("user1", 15, 90);
// 생성자 등장 후
MemberConstruct member = new MemberConstruct("user1", 15, 90);
제약 - 생성자 호출 필수
위 코드에서 생성자 등장 전 코드를 보며 이해해봅시다.
“이 경우 initMember(...)를 실수로 호출하지 않으면 어떻게 될까요?”
이 메서드를 실수로 호출하지 않아도 프로그램은 작동합니다.
하지만 학생의 이름과 나이, 성적 데이터가 없는 상태로 프로그램이 작동하게 됩니다.
“만약에 이 값들을 필수로 반드시 입력해야 한다면, 시스템에 큰 문제가 발생할 수 있습니다.”
결국 아무 정보가 없는 유령 학생이 시스템 내부에 등장하게 됩니다.
생성자의 진짜 장점은 객체를 생성할 때 직접 정의한 생성자가 있다면 “직접 정의한 생성자를 반드시 호출” 해야 한다는 점입니다.
참고로 생성자를 메서드 오버로딩 처럼 여러개 정의할 수 있는데, 이 경우에는 하나만 호출하면 됩니다.
“MemberConstruct 클래스의 경우 다음 생성자를 직접 정의했기 때문에 직접 정의한 생성자를 반드시 호출해야 합니다.”
MemberConstruct(String name, int age, int grade) { ... }
다음과 같이 직접 정의한 생성자를 호출하지 않으면 컴파일 오류가 발생합니다.
MemberConstruct member3 = new MemberConstruct(); // 컴파일 오류 발생
member3.name = "user1";
컴파일 오류 메시지
java: constructor MemberConstruct in class construct.MemberConstruct cannot be applied to given types;
required: java.lang.String,int,int
found: no arguments
reason: actual and formal argument lists differ in length
컴파일 오류는 IDE에서 즉시 확인할 수 있는 좋은 오류입니다.
이 경우 개발자는 객체를 생성할 때, 직접 정의한 생성자를 필수로 호출해야 한다는 것을 바로 알 수 있습니다.
그래서 필요한 생성자를 찾아서 다음과 같이 호출할 것입니다.
MemberConstruct member = new MemberConstruct("user1", 15, 90);
“생성자 덕분에 학생의 이름, 나이, 성적은 항상 필수로 입력하게 됩니다.”
따라서 아무 정보가 없는 유령 회원이 시스템 내부에 등장할 가능성을 원천 차단합니다.
“생성자를 사용하면 필수값 입력을 보장할 수 있습니다.”
참고: 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램입니다.
-
☕️[Java] 생성자 - 필요한 이유
생성자 - 필요한 이유.
객체를 생성하는 시점에 어떤 작업을 하고 싶다면 “생성자(Construct)” 을 이용하면 됩니다.
생성자를 알아보기 전에 먼저 생성자가 왜 필요한지 코드로 간단히 알아봅시다.
MemberInit
package construct;
public class MemberInit {
String name;
int age;
int grade;
}
MethodInitMain1
package construct;
public class MethodInitMain1 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
member1.name = "user1";
member1.age = 15;
member1.grade = 90;
MemberInit member2 = new MemberInit();
member2.name = "user2";
member2.age = 16;
member2.grade = 80;
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
실행 결과
아름:user1 나이:15 성적:90
아름:user2 나이:16 성적:80
회원 객체를 생성하고 나면 name, age, grade 같은 변수에 초기값을 설정합니다.
아마도 회원 객체를 제대로 사용하기 위해서는 객체를 생성하자 마자 이런 초기값을 설정해야 할 것입니다.
이 코드에는 회원의 초기값을 설정하는 부분이 계속 반복됩니다.
메서드를 사용해서 반복을 제거해봅시다.
MethodInitMain2
package construct;
public class MethodInitMain2 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
initMember(member1, "user1", 15, 90);
MemberInit member2 = new MemberInit();
initMember(member2, "user2", 16, 80);
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
static void initMember(MemberInit member, String name, int age, int grade) {
member.name = name;
member.age = age;
member.grade = grade;
}
}
initMember(...) 메서드를 사용해서 반복을 제거했습니다.
그런데 이 메서드는 대부분 MemberInit 객체의 멤버 변수를 사용합니다.
우리는 앞서 객체 지향에 대해서 학습했습니다.
이런 경우 속성과 기능을 한 곳에 두는 것이 더 나은 방법입니다.
쉽게 이야기해서 MemberInit이 자기 자신의 데이터를 변경하는 기능(메서드)을 제공하는 것이 좋습니다.
-
-
-
-
-
☕️[Java] 객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍과 절차 지향 프로그래밍은 서로 대치되는 개념이 아닙니다.
객체 지향이라도 프로그램의 작동 순서는 중요합니다.
다만 어디에 더 초점을 맞추는가에 둘의 차이가 있습니다.
객체 지향의 경우 객체의 설계와 관계를 중시합니다.
반면 절차 지향의 경우 데이터와 기능이 분리되어 있고, 프로그램이 어떻게 작동하는지 그 순서에 초점을 맞춥니다.
절차 지향 프로그래밍
절차 지향 프로그래밍은 이름 그대로 절차를 지향합니다.
쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식입니다.
절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식입니다.
즉, “어떻게”를 중심으로 프로그래밍 합니다.
객체 지향 프로그래밍
객체 지향 프로그래밍은 이름 그대로 객체를 지향합니다.
쉽게 이야기해서 객체를 중요하게 생각하는 방식입니다.
객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객제들 간의 상호작용을 중심으로 프로그래밍하는 방식입니다.
즉 “무엇을” 중심으로 프로그래밍 합니다.
둘의 중요한 차이
절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있습니다.
반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 함께 포함되어 있습니다.
객체란?
세상의 모든 사물을 단순하게 추상화해보면 속성(데이터)과 기능(메서드) 딱 2가지로 설명할 수 있습니다.
자동차
속성: 색상, 속도
기능: 엑셀, 브레이크, 문 열기, 문 닫기
동물
속성: 색상, 키, 온도
기능: 먹는다, 걷는다
게임 캐릭터
속성: 레벨, 경험치, 소유한 아이템들
기능: 이동, 공격, 아이템 획득
“객체 지향 프로그래밍은 모든 사물을 속성과 기능을 가진 객체로 생각하는 것 입니다.”
객체에는 속성과 기능만 존재합니다.
이렇게 단순화하면 세상에 있는 객체들을 컴퓨터 프로그램으로 쉽게 설계할 수 있습니다.
이런 장점들 덕분에 지금은 객체 지향 프로그래밍이 가장 많이 사용됩니다.
참고로 실세계와 객체가 항상 1:1로 매칭되는 것은 아닙니다.
객체 지향의 특징은 속성과 기능을 하나로 묶는 것 뿐만 아니라 캡슐화, 상속, 다형성, 추상화, 메시지 전달 같은 다양한 특징들이 있습니다.
-
-
-
🍃[Spring] Gradle과 Maven
Gradle.
Gradle은 오픈 소스 빌드 자동화 시스템으로, 다양한 프로그래밍 언어와 프로젝트에 대해 유연한 빌드 스크립트를 제공합니다.
Groovy나 Kotlin DSL을 사용하여 빌드 스크립트를 작성하며, 이는 개발자가 읽기 쉽고, 강력하며, 사용자 정의가 가능한 빌드를 구성할 수 있게 합니다.
Gradle은 의존성 관리와 멀티 프로젝트 빌드를 지원하며, 이전에 실행된 작업의 출력을 캐시하여 빌드 시간을 단축시키는 증분 빌드 기능도 제공합니다.
Android 개발을 위한 공식 빌드 시스템으로도 널리 사용됩니다.
Maven.
Maven은 Java 프로젝트의 빌드, 문서화, 보고, 의존성 관리 등을 자동화하기 위한 또 다른 오픈 소스 빌드 도구입니다.
XML 형식의 pom.xml 파일을 사용하여 프로젝트 구성과 의존성을 관리합니다.
Maven은 중앙 저장소에서 필요한 라이브러리와 플러그인을 자동으로 다운로드하고, 프로젝트의 라이프사이클(컴파일, 테스트, 패키징 등)을 관리하는 표준화된 방법을 제공합니다.
이는 프로젝트의 일관성을 유지하고, 빌드 과정을 간소화하는 데 도움이 됩니다.
Gradle과 Maven의 차이점.
빌드 스크립트 구문 : Gradle은 Groovy나 Kotlin으로 작성된 빌드 스크립트를 사용하는 반면, Maven은 XML 기반의 pom.xml 파일을 사용합니다. Gradle의 DSL은 Maven의 XML보다 간결하고, 읽기 쉽습니다.
성능 : Gradle은 증분 빌드와 빌드 캐시 기능을 통해 Maven보다 빌드 시간을 단축시킬 수 있습니다. Maven은 Gradle에 비해 이러한 최적화 기능이 부족합니다.
유연성 : Gradle은 빌드 스크립트에 로직을 추가하여 빌드 프로세스를 매우 세밀하게 제어할 수 있습니다. Maven은 더 엄격한 라이프사이클과 구조를 따르며, 커스터마이징이 제한적입니다.
플러그인 생태계 : Maven은 오랜 기간 동안 사용되어 왔기 때문에 방대한 양의 플러그인이 있지만, Gradle도 활발히 성장하고 있는 플러그인 생태계를 갖추고 있습니다.
프로젝트 구조 : Maven은 규약을 중시하는 구조로, 프로젝트의 디렉토리 구조가 일정합니다. Gradle은 더 많은 구성 가능성을 제공하지만, 이는 동시에 프로젝트 설정이 복잡해질 수 있음을 의미합니다.
결론적으로, Gradler과 Maven은 각각의 장단점을 가지고 있으며, 프로젝트의 요구사항과 개발 팀의 선호도에 따라 적합한 도구를 선택하는 것이 중요합니다.
-
-
☕️[Java] 변수와 초기화
변수와 초기화
변수의 종류
멤버 변수(필드): 클래스에 선언.
지역 변수: 메서드에 선언, 매개변수도 지역 변수의 한 종류입니다.
멤버 변수, 필드 예시
public class Student {
String name;
int age;
int grade;
}
name, age, grade 는 멤버 변수입니다.
지역 변수 예시
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
Student student2 = new Student();
}
}
student1, student2는 지역 변수입니다.
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
a, x(매개변수, Parameter)는 지역변수입니다.
지역 변수는 이름 그대로 “특정 지역에서만 사용되는 변수라는 뜻입니다.”
예를 들어서 변수 x는 changePrimitive() 메서드의 블록에서만 사용됩니다.
changePrimitive() 메서드가 끝나면 제거됩니다.
a 변수도 마찬가지입니다. main() 메서드가 끝나면 제거됩니다.
변수의 값 초기화
멤버 변수: 자동 초기화.
인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화됩니다.
숫자(int) = 0, boolean = false, 참조형 = null(null 값은 참조할 대상이 없다는 뜻으로 사용됩니다.)
개발자가 초기값을 직접 지정할 수 있습니다.
지역 변수: 수동 초기화.
지역 변수는 항상 직접 초기화해야 합니다.
“멤버 변수의 초기화를 살펴봅시다”
InitData
package ref;
public class InitData {
int value1; // 초기화 하지 않음
int value2 = 10; // 10으로 초기화
}
value1은 초기값을 지정하지 않았고, value2는 초기값을 10으로 지정했습니다.
InitMain
package ref;
public class initMain {
public static void main(String[] args) {
InitData data = new InitData();
System.out.println("value1 = " + data.value1);
System.out.println("value2 = " + data.value2);
}
}
실행 결과
value1 = 0
value2 = 10
value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 됩니다.
숫자는 0으로 초기화됩니다.
value2는 10으로 초기값을 지정해두었기 때문에 객체를 생성할 때 10으로 초기화됩니다.
-
☕️[Java] 참조형과 메서드 호출 - 활용
참조형과 메서드 호출 - 활용
아래의 코드 class1.ClassStart3 코드에는 중복되는 부분이 2가지가 있습니다.
name, age, grade에 값을 할당하는 부분.
학생 정보를 출력하는 부분.
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
“이러한 중복은 메서드를 통해 손쉽게 제거할 수 있습니다.”
메서드에 객체 전달
다음과 같이 코드를 작성해봅시다.
Student
package ref;
public class Student {
String name;
int age;
int grade;
}
ref 패키지에도 Student 클래스를 만듭니다.
Method1
package ref;
public class Method1 {
public static void main(String[] args) {
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static void initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
참조형은 메서드를 호출할 때 참조값을 전달합니다.
따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있습니다.
initStudent(Student student, ...) : 전달한 학생 객체의 필드에 값을 설정합니다.
printStudent(Student student, ...) : 전달한 학생 객체의 필드 값을 읽어서 출력합니다.
initStudent() 메서드 호출 분석
initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
student1이 참조하는 Student 인스턴스에 값을 편리하게 할당하고 싶어서 initStudent() 메서드를 만들었습니다.
이 메서드를 호출하면서 student1을 전달합니다.
그러면 student1의 참조값(30f39991)이 매개변수 student에 전달됩니다.
이 참조값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 30f39991 Student 인스턴스에 접근하고 값을 변경할 수 있습니다.
참고: 30f39991은 실제 student1의 참조값입니다.
System.out.println("student1 참조값 : " + student1); 의 결과가 student1 참조값 : ref.Student@30f39991 이였습니다.
주의!
package ref;
import class1 Student;
public class Method1 {
...
}
import class1.Student;이 선언되어 있으면 안됩니다.
이렇게 되면 class1 패키지에서 선언한 Student를 사용하게 됩니다.
이 경우에는 접근 제어자 때문에 컴파일 오류가 발생합니다.
만약 선언되어 있다면 삭제해야 합니다. 삭제하면 같은 패키지에 있는 ref.Student를 사용합니다.
메서드에서 객체 반환
조금 더 코드를 리팩토링 시켜봅시다. 다음 코드에도 중복이 있습니다.
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
바로 객체를 생성하고, 초기값을 설정하는 부분입니다.
이렇게 2번 반복되는 부분을 하나로 합져봅시다.
“다음과 같이 기존 코드를 변경해봅시다.”
Method2
package ref;
public class Method2 {
public static void main(String[] args) {
Student student1 = createStudent("학생1", 15, 90);
Student student2 = createStudent("학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static Student createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
createStudent() 라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드안에 함께 포함했습니다.
“이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리합니다.”
그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 합니다.
그래야 메서드 밖에서 이 객체를 사용할 수 있습니다.
메서드는 호출 결과를 반환(return)을 할 수 있습니다.
메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 됩니다.
createStudent() 메서드 호출 분석
createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했습니다.
이 참조값만 있으면 해당 인스턴스에 접근할 수 있습니다.
여기서는 student1에 참조값을 보관하고 사용합니다.
진행 과정
Student student1 = createStudent("학생1", 15, 90); // 메서드 호출후 결과 반환
Student student1 = student(30f39991); // 참조형인 student를 반환
Student student1 = 30f39991; // student의 참조값 대입
student1 = 30f39991;
createStudent() 는 생성한 Student 인스턴스의 참조값을 반환합니다.
이렇게 반환된 참조값을 student1 변수에 저장합니다.
앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있습니다.
-
☕️[Java] 기본형과 참조형(3) - 메서드 호출
기본형과 참조형(3) - 메서드 호출
대원칙: 자바는 항상 변수의 값을 복사해서 대입합니다.
자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것입니다.
기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입합니다.
기본형이면 변수에 들어 있는 실제 사용하는 값을 복사해서 대입합니다.
참조형이면 변수에 들어 있는 참조값을 복사해서 대입합니다.
메서드 호출도 마찬가지입니다. 메서드를 호출할 때 사용하는 매개변수(parameter)도 결국 변수일 뿐 입니다.
따라서 메서드를 호출할 때 매개변수(parameter)에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달합니다.
다음 예시 코드를 봐봅시다.
기본형과 메서드 호출
package ref;
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
실행 결과
메서드 호출 전: a = 10
메서드 호출 후: a = 10
1. 메서드 호출
메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달합니다.
int x = a
이 코드는 다음과 같이 해석할 수 있습니다.
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다 따라서 변수 a,x 각각 숫자 10을 가지고 있습니다.
2. 메서드 안에서 값을 변경
int a = 10;
changePrimitive(a);
메서드 안에서 x = 20으로 새로운 값을 대입합니다.
결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지됩니다.
3. 메서드 종료
int a = 10;
changePrimitive(a);
메서드 종료후 값을 확인해보면 a는 10이 출력되는 것을 확인할 수 있습니다.
참고로 메서드가 종료되면 매개변수 x는 제거됩니다.
참조형과 메서드 호출
package ref;
public class MethodChange2 {
public static void main(String[] args) {
Data dataA = new Data();
dataA.value = 10;
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
changeReference(dataA);
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
}
static void changeReference(Data dataX) {
dataX.value = 20;
}
}
실행 결과
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
1. 메서드 호출
메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달합니다.
이 코드는 다음과 같이 해석할 수 있습니다.
int dataX = dataA;
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다.
변수 dataA는 참조값 x001을 가지고 있다는 가정하에 dataA는 참조값을 복사해서 전달합니다.
따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 됩니다.
이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있습니다.
2. 메서드 안에서 값을 변경
static void changeReference(Data dataX) {
dataX.value = 20;
}
메서드 안에서 dataX.value = 20으로 새로운 값을 대입합니다.
참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했습니다.
dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataX.value는 둘다 20이라는 값을 가집니다.
3. 메서드 종료
메서드 종료후 dataA.value의 값을 확인해보면 다음과 같이 20으로 변경된 것을 확인할 수 있습니다.
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
기본형과 참조형의 메서드 호출
자바에서 메서드의 매개변서(Parameter)는 항상 값에 의해 전달됩니다.
그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라집니다.
기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)의 값을 변경해도, 호출자의 변수 값에는 영향이 없습니다.
참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경됩니다.
-
-
-
☕️[Java] 기본형과 참조형(1)
기본형과 참조형(1)
자바에서 참조형을 제대호 이해하는 것은 정말 중요합니다.
변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있습니다.
사용하는 값을 변수에 직접 넣을 수 있는 기본형
이전 포스팅의 예시 코드에서 본 Student student1 과 같이 객체가 저장된 메모리의 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있습니다.
기본형(Primitive Type)
Int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 “기본형” 이라고 합니다.
참조형(Reference Type)
Student student1, int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 “참조형” 이라고 합니다.
참조형은 객체 또는 배열에 사용됩니다.
쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)이 들어가 있습니다.
참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 위치로 이동해야 합니다.
기본형 vs 참조형 - 기본
“기본형” 은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있습니다.
때문에 해당 값을 바로 사용할 수 있습니다.
“참조형” 은 실제 사용하는 값을 변수에 담는 것이 아닙니다.
이름 그대로 실제 객체의 위치(참조, 주소)를 저장합니다.
참조형에는 객체와 배열이 있습니다.
“객체” 는 .(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있습니다.
“배열” 은 []를 통해서 메모리 상에 생성된 배열을 찾아야가 사용할 수 있습니다.
기본형 vs 참조형 - 계산
기본형은 들어있는 값을 그대로 계산에 사용할 수 있습니다.
예) 더하고 빼고, 사용하고 등등,(숫자 같은 것들은 바로 계산할 수 있습니다.)
참조형은 들어있는 참조값을 그대로 사용할 수 없습니다.
주소지만 가지고는 할 수 있는게 없습니다.
주소지에 가야 실체가 있기 때문입니다.
예) 더하고 빼고 사용하고를 못합니다. 참조값만 가지고는 계산할 수 있는 것이 없습니다.
“기본형은 연산이 가능하지만 참조형은 연산이 불가능합니다.”
int a = 10, b = 20;
int sum = a + b;
기본형은 변수에 실제 사용하는 값이 담겨있습니다.
따라서 +, -와 같은 연산이 가능합니다.
Student s1 = new Student();
Student s2 = new Student();
s1 + s2 // 오류 발생.
참초형은 변수에 객체의 위치인 참조값이 들어있습니다.
참조값은 계산에 사용할 수 없습니다. 따라서 오류가 발생합니다.
“물론 다음과 같이 .(dot)을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있습니다.”
Student s1 = new Student();
s1.grade = 100;
Student s2 = new Student();
s2 grade 90;
int sum = s1.grade + s2.grade // 연산 가능
쉽게 이해하는 팁
“기본형을 제외한 나머지는 모두 참조형입니다.”
기본형은 소문자로 시작합니다.
int, long, double, boolean 모두 소문자로 시작합니다.
기본형은 자바가 기본으로 제공하는 데이터 타입입니다.
이러한 기본형은 개발자가 새로 정의할 수 없습니다.
개발자는 참조형인 클래스만 직접 정의할 수 있습니다.
클래스는 대문자로 시작합니다.
Student, String, 등…
클래스는 모두 참조형입니다.
“참고 - String”
자바에서 String은 특별합니다.
String은 사실 클래스입니다. 즉, 참조형입니다.
그런데 기본형처럼 문자 값을 바로 대입할 수 있습니다.
문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공합니다.
String에 대한 자세한 내용은 추후에 설명하겠습니다.
-
-
-
-
-
-
🐋[MySQL] 테이블에 데이터 입력 INSERT INTO
INSERT INTO
memberTBL이라는 테이블이 있습니다.
그 테이블에는 아무런 데이터가 입력되지 않은 상태입니다.
열(Column, 필드)은 총 3개입니다.
memberID
char(8)
memberName
char(5)
memberAddress
char(20)
데이터를 한 행(row)을 삽입하려고 할 경우에는 다음과 같이하면 됩니다.
INSERT INTO 테이블이름 VALUES (데이터1, 데이터2, 데이터3...);
만약 memberTBL에 한 행(row)을 삽입하려 할 경우는 다음과 같이하면 됩니다.
INSERT INTO memberTBL VALUES ('Thomas', '토마스', '경기 부천시 중동');
만약 특정 필드에만 값을 입력하고 싶을 경우에는 VALUES 앞에 “입력할 필드를 선언해 줍니다.”
INSERT INTO memberTBL (memberID, memberAddress) VALUES ('Thomas', '경기 부천시 중동');
만약 여러 개의 행(row)을 동시에 입력하고 싶을 경우에는 아래와 같이 여러 개의 행을 동시에 입력이 가능합니다.
INSERT INTO memberTBL (memberID, memberName, memberAddress) VALUES ('Thomas', '토마스', '경기 부천시 중동'), ('Edward', '에드워드', '서울 은평구 증산동'), ('Henrv', '헨리', '인천 남구 주안동'), ('Gorden', '고든', '경기 성남시 분당구');
-
-
-
☕️[Java] 배열 도입
배열 도입
“배열” 을 사용하면 특정 타입을 연속한 데이터 구조로 묶어서 편리하게 관리할 수 있습니다.
Student 클래스를 사용한 변수들도 Student 타입이기 때문에 학생도 “배열” 을 사용해서 “하나의 데이터 구조로 묶어서 관리” 할 수 있습니다.
클래스의 도입 포스팅 예시 코드 참고 -> 학생 클래스.
Student 타입을 사용하는 “배열” 을 도입해봅시다.
package class1;
public class ClassStart4 {
public static void main(String[] args) {
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student[] students = new Student[2];
students[0] = student1;
students[1] = student2;
for (int i = 0; i < students.length; i++) {
System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade);
}
}
}
코드를 분석해 봅시다.
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student 클래스를 기반으로 student1, student2 인스턴스를 생성합니다. 그리고 필요한 값을 채워둡니다.
배열에 참조값 대입
이번에는 Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관해봅시다.
Student[] students = new Student[2];
Student 변수를 2개 보관할 수 있는 사이즈 2의 배열을 만듭니다.
Student 타입의 변수는 Student 인스턴스의 “참조값을 보관” 합니다.
Student 배열의 각각의 항목도 “Student 타입의 변수일 뿐” 입니다. 따라서 “Student 타입의 참조값을 보관” 합니다.
student1, student2 변수를 생각해보면 Student 타입의 참조값을 보관합니다.
배열에는 아직 참조값을 대입하지 않았기 때문에 참조값이 없다는 의미의 null 값으로 초기화 됩니다.
이제 배열에 객체를 보관해보겠습니다.
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
student1, student2에는 참조값이 보관되어 있습니다.
따라서 이 참조값이 배열에 저정됩니다.
또는 student1, student2에 보관된 참조값을 읽고 복사해서 배열에 대입한다고 표현합니다.
이제 배열은 x001, x002의 참조값을 가집니다.
참조값을 가지고 있기 때문에 x001(학생1), x002(학생), Student 인스턴스에 모두 접근할 수 있습니다.
너무 중요해서 한 번더 강조합니다 잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
자바에서 변수의 대입(=)은 모두 변수에 들어있는 값을 복사해서 전달하는 것입니다.
이 경우 오른쪽 변수인 student1, student2에는 참조값이 들어있습니다.
그래서 이 값을 복사해서 왼쪽에 있는 배열에 전달합니다.
따라서 기존 student1, student2에 들어있던 참조값은 당연히 그대로 유지됩니다.
주의!!
변수에는 인스턴스 자체가 들어있는 것이 아닙니다!
“인스턴스의 위치를 가리키는 참조값이 들어있을 뿐입니다!!”
따라서 대입(=)시에 인스턴스가 복사되는 것이 아니라 참조값만 복사됩니다.
배열에 들어있는 객체 사용
배열에 들어있는 객체를 사용하려면 먼저 배열에 접근하고, 그 다음에 객체에 접근하면 됩니다.
이전에 설명한 그림과 코드를 함께 보면 쉽게 이해가 될 것입니다.
학생1 예제
System.out.println(students[0].name); // 배열 접근 시작
System.out.println(x005[0].name); // [0]를 사용해서 x005 배열의 0번 요소에 접근
System.out.println(x001.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생1");
학생2 예제
System.out.println(students[1].name); // 배열 접근 시작
System.out.println(x005[1].name); // [0]를 사용해서 x005 배열의 1번 요소에 접근
System.out.println(x002.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생2");
-
-
-
☕️[Java] 클래스, 객체, 인스턴스 정리
클래스, 객체, 인스턴스 정리.
클래스 - Class
클래스는 객체를 생성하기 위한 “틀” 또는 “설계도” 입니다.
클래스는 객체가 가져야 할 “속성(변수)” 와 “기능(메서드)” 를 정의합니다.
예를 들어 학생이라는 클래스는 속성(변수)으로 name, age, grade를 가집니다.
참고로 기능(메서드)은 추후에 설명합니다. 지금은 속성(변수)에 집중합시다.
“틀” : 붕어빵 틀을 생각해봅시다.
붕어빵 틀은 붕어빵이 아닙니다! 이렇게 생긴 붕어빵이 나왔으면 좋겠다고 만드는 틀일 뿐입니다.
실제 먹을 수 있는 것이 아닙니다. 실제 먹을 수 있는 팥 붕어빵을 객체 또는 인스턴스라고 합니다.
“설계도” : 자동차 설계도를 생각해봅시다.
자동차 설계도는 자동차가 아닙니다! 설계도는 실제 존재하는 것이 아니라 개념으로만 있는 것입니다.
설계도를 통한 생산한 실제 존재하는 흰색 테슬라 모델 Y 자동차를 객체 또는 인스턴스라고 합니다.
객체 - Object
객체는 클래스에서 정의한 속성과 기능을 가진 실체입니다.
객체는 서로 독립적인 상태를 가집니다.
예를 들어 위 코드에서 student1은 학생1의 속성을 가지는 객체이고, student2는 학생2의 속성을 가지는 객체입니다.
student1 과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체입니다.
인스턴스 - Instance
인스턴스는 특정 클래스로부터 생성된 객체를 의미합니다.
그래서 객체와 인스턴스라는 용어는 자주 혼용됩니다.
인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용합니다.
예를 들어서 “student1 객체는 Student 클래스의 인스턴스다.” 라고 표현합니다.
객체 vs 인스턴스
둘 다 클래스에서 나온 실체라는 의미에서 비슷하게 사용되지만, 용어상 인스턴스는 “객체보다 좀 더 관계에 초점을 맞춘 단어입니다.”
보통 “student1은 Student의 객체이다.” 라고 말하는 대신 “student1은 Student의 인스턴스이다.” 라고 특정 클래스와의 “관계를 명확히 할 때 인스턴스” 라는 용어를 주로 사용합니다.
좀 더 쉽게 풀어보자면, 모든 인스턴스는 객체이지만, 우리가 “인스턴스” 라고 부르는 순간 “특정 클래스로부터 그 객체가 생성되었음을 강조하고 싶을 때입니다.”
예를 들어, student1은 객체이지만, 이 객체가 Student 클래스로부터 생성된다는 점을 명확히 하기 위해 student1을 Student의 인스턴스라고 부릅니다.
하지만 둘 다 클래스에서 나온 실체라는 핵심 의미는 같기 때문에 보통 둘을 구분하지 않고 사용합니다.
-
☕️[Java] 클래스 도입
클래스 도입
클래스를 사용해서 “학생”이라는 개념을 만들고, 각 학생 별로 본인의 이름, 나이, 성적을 관리해봅시다.
우선 코드를 봐봅시다.
package class1;
public class Student {
String name;
int age;
int grade;
}
class 키워드를 사용해서 학생 클래스(Student)를 정의합니다.
학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가집니다.
이렇게 “클래스에 정의한 변수들을 멤버 변수, 또는 필드” 라 합니다.
멤버 변수(Member Variable) : 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부릅니다.
필드(Field) : 데이터 항목을 가르키는 전통적인 용어입니다. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라고 합니다.
자바에서 멤버 변수, 필드는 같은 뜻입니다.
클래스에 소속된 변수를 뜻합니다.
클래스는 관례상 대문자로 시작하고 낙타 표기법을 사용합니다.
예) Student, User, MemberService
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
실행 결과
이름:학생1 나이:15 성적:90
이름:학생2 나이:16 성적:80
클래스와 사용자 정의 타입
“타입” 은 데이터의 종류나 형태를 나타냅니다.
int라고 하면 정수 타입, String이라고 하면 문자 타입입니다.
그러면 학생(Student)이라는 타입을 만들면 되지 않을까요?
클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있습니다.
사용자가 직접 정의하는 “사용자 정의 타입” 을 만들려면 설계도가 필요합니다.
이 “이 설계도가 바로 클래스” 입니다.
“설계도”인 “클래스”를 사용해서 “실제 메모리에 만들어진 실체를 객체 또는 인스턴스” 라 합니다.
“클래스”를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할 수 있습니다.
용어: 클래스, 객체, 인스턴스
클래스는 설계도이고, 이 설계도를 기반으로 실제 메모리에 만들어진 실체를 “객체 또는 인스턴스” 라 합니다.
둘다 같은 의미로 사용됩니다.
여기서는 학생(Student) 클래스를 기반으로 학생1(student1), 학생2(student2) 객체 또는 인스턴스를 만들었습니다.
코드 분석
1. 변수 선언.
Student student1 // Student 변수 선언
Student student1
Student “타입” 을 받을 수 있는 변수를 선언합니다.
int는 정수를, String은 문자를 담을 수 있듯이 “Student는 Student 타입의 객체(인스턴스)를 받을 수 있습니다.”
2. 객체 생셩.
student1 = new Student() // Student 인스턴스 생성
student1 = new Student() 코드를 나누어 분석해봅시다.
객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 합니다.
new Student()에서의 new는 새로 생성한다는 뜻 입니다.
new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻입니다.
이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성합니다.
객체를 생성할 때는 new 클래스명()을 사용하면 됩니다. 마지막에 ()도 추가해야합니다.
Student 클래스는 String name, int age, int grade 멤버 변수를 가지고 있습니다.
이 변수를 사용하는데 필요한 메모리 공간도 함께 확보합니다.
3. 참조값 보관
student1 = x001l // Student 인스턴스 참조값 보관
객체를 생성시 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)(x001)을 반환합니다.
여기셔 x001이라고 표현한 것이 참조값입니다.(실제로 x001처럼 표현되는 것은 아니고 이해를 돕기 위한 예시입니다.)
new 키워드를 통해 객체가 생성되고 나면 참조값을 반환합니다.
앞서 선언한 변수인 Student student1에 생성된 객체의 참조값(x001)을 보관합니다.
Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있습니다.
student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있습니다.
따라서 이 변수를 통해서 객체를 접근(참조)할 수 있습니다. 쉽게 이야기해서 student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있습니다.
참조값을 변수에 보관해야 하는 이유
객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없습니다.
“이 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 만드는 것 입니다.”
따라서 생성한 객체에 접근할 수 있는 방법이 필요합니다.
이런 이유로 객체를 생성할 때 반환되는 참조값을 어딘가에 보관해두어야 합니다.
앞서 Student student1 변수에 참조값(x001)을 저장해두었으므로 저장한 참조값(x001)을 통해서 실제 메모리에 존재하는 객체에 접근할 수 있습니다.
지금까지 설명한 내용을 간단히 풀어보면 다음과 같습니다.
Student student1 = new Student(); // 1. Student 객체 생성
Student student1 = x001; // 2. new Student()의 결과로 x001 참조값 변환
student1 = x001; // 3. 최종 결과
이후에 학생2(student2)까지 생성하면 다음과 같이 Student 객체(인스턴스)가 메모리에 2개 생성됩니다.
“각각의 참조값이 다르므로 서로 구분할 수 있습니다.”
참조값을 확인하고 싶다면 다음과 같이 객체를 담고 있는 변수를 출력해보면 됩니다.
System.out.println(student1);
System.out.println(student2);
출력 결과
class1.Student@30f39991
class1.Student@452b3a41
@ 앞은 패키지 + 클래스 정보를 뜻합니다. @ 뒤에 16진수는 참조값을 뜻합니다.
-
💾[Database] SQL의 개요
SQL의 개요
SQL은 관계형 DB에서 사용되는 언어로 ‘에스큐엘’ 또는 ‘시퀄’이라고 읽습니다.
관계형 DBMS(그중에서도 MySQL)를 배우려면 SQL을 익히는 것은 필수입니다.
SQL은 DB를 조작하는 ‘언어’로, 일반적인 프로그래밍 언어(C, C++, Java, C# 등)와 다른 특성을 가지고 있습니다.
SQL은 국제 표준화기관에서 표준화된 내용을 계속 발표했습니다.
SQL의 특징
“DBMS 제작 회사와 독립적이다.”
모든 DBMS 제작 회사에서 표준 SQL이 공개되어 각 회사는 이 표준 SQL에 맞춰 DBMS를 개발합니다.
따라서 SQL은 대부분의 DBMS 제품에서 공통적으로 호환됩니다.
“다른 시스템으로의 이식성이 좋다.”
SQL은 서버용, 개인용, 휴대용 장비 등 운영되는 DBMS마다 상호 호환성이 뛰어납니다.
한 시스템에서 사용하던 SQL을 다른 시스템으로 이식하는 데 큰 문제가 없습니다.
“표준이 계속 발전합니다.”
SQL은 SQL-86, 89, 92, 1999, 2003, 2008, 2011 등으로 개선된 표준안이 계속 발표되었으며, 지금도 개선된 안이 꾸준히 연구되고 있습니다.
“대화식 언어입니다.”
기존 프로그래밍 언어는 프로그램 작성, 컴파일 및 디버깅, 실행 과정을 거쳐야만 그 결과를 확인할 수 있지만 SQL은 바로 질의하고 결과를 얻는 대화식 언어입니다.
“클라이언트/서버 구조를 지원합니다.”
SQL은 분산형 구조인 클라이언트/서버 구조를 지원합니다.
클라이언트에서 질의를 하면 서버에서 그 질의를 받아 처리하여 클라이언트에 전달하는 구조입니다.
SQL을 사용시 주의할 점은, 모든 DBMS 제품의 SQL 문이 완벽하게 동일하지는 않다는 것입니다.
많은 회사가 되도록 표준 SQL을 준수하려고 노력하지만 각 회사의 DBMS마다 특징이 있기 때문에 현실적으로 완전히 통일되기는 어렵습니다.
각 회사는 가급적 표준 SQL을 지키면서도 자신의 제품에 특화된 SQL을 사용합니다.
이를 오라클에서는 PL/SQL, SQL Server에서는 T-SQL이라 부르고 MySQL에서는 그냥 SQL이라 일컫습니다.
아래 그림과 같이 각회사의 제품은 모두 표준 SQL을 공통으로 사용하면서 자기 제품의 특성에 맞춘 호환되지 않는 SQL 문도 사용합니다.
-
☁️[AWS] Route 53에 등록된 서브도메인 github page에 연결하기
AWS Route 53에 등록된 서브도메인 github page에 연결.
고통의 시작.
블로그를 처음 만들고 잘 운영하던 중 jekyll과 Gem Dependency와 Ruby 버전 그리고 Bundle까지 뭔가 엉키고 꼬여서 풀리지 않고 어느새 블로그는 엉망이 되어버렸습니다.
진심으로 복구 시도를 열심해 했으나 내 부족한 지식으로 인하여 밀어버릴 수 밖에…
그리하여 깔끔하게 데이터 백업 후 밀어버리고 더 이쁜 블로그 테마를 찾아버렸습니다(오히려 좋아👍)
그렇게 아주 기분 좋게 블로그를 옮기고 잘 운영되나 싶었는데 ‘얼씨구?’ AWS Route 53에서 등록한 서브도메인을 github page와 연결해 놓았었는데 이 친구가 아주 먹통이 되어버렸습니다.
“DNS가 깃헙 서버와 맞지 않는다는 경고를 내보내주었습니다.”
이상하게 찝찝하게도 그런데 접속은 정상적으로 동작했습니다…
아….. 찝찝한게 제일 싫은 나는 결국 오전 4시 기상해서 하나씩 알아보기 시작했습니다.
DNS
Domain Name System(이후 DNS)은 인터넷의 전화번호부 같은 역할을 합니다.
사람들이 웹사이트에 접속시, 일반적으로 기억하기 쉬운 도메인 이름(예: www.devkobe24.com)을 사용합니다.
그러나 인터넷 자체는 IP 주소(예:192.0.2.1)라는 숫자 주소 체계를 사용하여 컴퓨터들 사이의 통신을 가능하게 합니다.
“DNS는 사용자가 웹 브라우저에 입력한 도메인 이름을 컴퓨터가 이해할 수 있는 IP 주소로 변환하는 시스템입니다.”
이를 통해 사용자는 복잡한 IP 주소를 기억하지 않고도 웹사이트에 쉽게 접근할 수 있습니다.
DNS의 주요 기능과 구성요소
도메인 이름 해석 : 사용자가 웹 브라우저에 URL을 입력하면, DNS 서버는 해당 URL의 도메인 이름을 IP 주소로 변환합니다. 이 과정을 “이름 해석” 또는 “도메인 이름 해석”이라고 합니다.
계층적 구조 : DNS 시스템은 계층적 구조로 되어 있습니다. 맨 위에는 루트 DNS 서버가 있으며, 그 아래에는 최상위 도메인(TLD)서버(예: .com, .net, .org등), 그리고 더 아래에는 권한 있는 이름 서버가 위치합니다. 권한 있는 이름 서버는 특정 도메인(예: devkobe24.com)에 대한 정보를 관리합니다.
캐싱 : DNS 쿼리의 효율성을 높이기 위해, DNS 서버는 해석된 주소 정보를 일정 시간 동안 저장(캐싱)합니다. 이렇게 하면 같은 요청에 대해 반복적으로 최상위 서버에 접근할 칠요가 없어집니다.
재귀적 및 반복 쿼리 : 사용자의 DNS 쿼리는 먼저 로컬 DNS 서버로 전송됩니다. 로컬 DNS 서버는 요청된 도메인의 IP 주소를 알고 있으면 바로 응답합니다. 모르는 경우, 다른 DNS 서버에 요청을 전달하여 답을 찾습니다. 이 과정에서 재귀적 쿼리(사용자를 대신해 답을 찾는 과정)와 반복적 쿼리(요청을 다른 서버로 전달하는 과정)가 사용됩니다.
DNS는 인터넷 사용의 핵심요소로, 사용자가 웹사이트에 접근하고, 이메일을 보내고 받으며, 다양한 온라인 서비스를 이용할 수 있게 해줍니다.
이렇게 DNS에 대하여 알아보고나서 이전에 연결을 어떻게 했었는지 이전에 블로그를 찾아봤습니다.
먼저 연결 전 사전 준비물(?)이 필요합니다.
도메인
Route 53 DNS 호스팅 설정
Github Pages 설정된 Repository
이렇게 준비 한 뒤 Route 53 콘솔을 세팅합니다.
Route 53 콘솔 설정.
Route 53 콘솔로 갑니다 > 호스팅 영역을 클릭 > 보유 도메인을 설정합니다.
레코드 생성
레코드 이름 : 서브(2차) 도메인 명 > www
레코드 유형 : CNAME
값 : (자신의 깃헙 레포 이름) > ex) devKobe24.github.io
Github Repository 설정.
레포지토리 > 셋팅 > 페이지 > 커스텀 도메인
그 안에 Route 53에서 내가 등록한 커스텀 도메인 명을 입력합니다. (예 : www.devkobe24.com)
위와 같이 등록해주고 초록색 글자로 “DNS check successful”이 나오면 성공입니다.
하지만 저는 나오지 않았습니다.
그래서!! 더 알아 본 결과!!
A레코드 추가.
Route 53에서 A레코드를 추가해주었어야 했습니다.
Route 53 > 호스팅 영역 > 등록할 호스팅 영역의 이름 클릭 > 레코드 생성
레코드 이름은 비워둡니다.
레코드 유형은 A
값은 다음 IP 중 하나를 골라 사용합니다.
185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
나머지 옵션은 건들지 않습니다.
이후 레코드 생성을 눌러 생성합니다.
이후 깃헙 레포로 돌아갑니다.
위 그림과 같이 다시 도메인을 넣고 체크해보면 성공적으로 도메인이 적용됩니다, 저는 이렇게 도메인을 성공적으로 적용했습니다!!
참고자료
AWS Route 53에 등록된 서브도메인을 GitHub Pages에 연결하기
-
-
-
☕️[Java] 클래스가 필요한 이유.
클래스가 필요한 이유.
먼저 아래의 학생 정보 출력 프로그램을 만들어 봅시다.
이 프로그램은 두 명의 학생 정보를 출력하는 프로그램입니다.
각 학생은 이름, 나이, 성적을 가지고 있습니다.
요구사항
첫 번째 학생의 이름은 “학생1”, 나이는 15, 성적은 90 입니다.
두 번째 학생의 이름은 “학생2”, 나이는 16, 성적은 80 입니다.
각 학생의 정보를 다음과 같은 형식으로 출력해야 합니다: "이름: [이름] 나이: [나이] 성적: [성적]"
변수를 사용해서 학생 정보를 저장하고 변수를 사용해서 학생 정보를 출력해야 합니다.
예시 출력
이름: 학생1 나이: 15 성적: 90
이름: 학생2 나이: 16 성적: 80
변수를 사용해서 이 문제를 풀어보면 다음과 같이 프로그램을 만들어볼 수 있습니다.
package class1;
public class ClassStart1 {
public static void main(String[] args) {
String firstStudentName = "학생1";
String secondStudentName = "학생2";
int firstStudentAge = 15;
int secondStudentAge = 16;
int firstStudentGrade = 90;
int secondStudentGrade = 80;
System.out.println("이름: " + firstStudentName + " 나이: " + firstStudentAge + " 성적: " + firstStudentGrade);
System.out.println("이름: " + secondStudentName + " 나이: " + secondStudentAge + " 성적: " + secondStudentGrade);
}
}
위 코드에서는 학생 2명을 다루어야 하기 때문에 각각 다른 변수를 사용했습니다.
이 코드의 문제는 학생이 늘어날 때 마다 변수를 추가로 선언해야 하고, 또 출력하는 코드도 추가해야 합니다.
이런 문제를 어떻게 해결할 수 있을까요?
이번에는 위 코드를 “배열을 사용하여 리펙토링” 해봅시다.
아래의 코드는 “배열을 활용한 코드” 입니다.
package class1;
public class ClassStart2 {
public static void main(String[] args) {
String[] studentNames = { "학생1", "학생2" };
int [] studentAges = { 15, 16 };
int [] studentGrades = { 90, 80 };
for (int i = 0; i < studentNames.length; i++) {
System.out.println("이름: " + studentNames[i] + " 나이: " + studentAges[i] + " 성적: " + studentGrades[i]);
}
}
}
이전 코드보다 훨씬 깔끔해졌으며, 위 코드에서 문제가 됐었던 학생이 늘어날 때 마다 변수를 새롭게 추가할 필요도 없어졌습니다.
배열 사용의 한계.
하지만 배열을 사용해서 코드를 최소화하는데 성공했지만 “한 한생의 데이터가 studentNames[], studentAges[], studentGrades라는 3개의 배열에 나누어져 있습니다.”
따라서 “데이터를 변경할 때 매우 조심해서 작업해야 합니다.”
예를 들어서 학생 2의 데이터를 제거하려면 각각의 배열마다 학생2의 요소를 정확하게 찾아서 제거해주어야 합니다.
학생2 제거
String[] studentNames = { "학생1", "학생3", "학생4", "학생5" };
int [] studentAges = { 15, 17, 10, 16 };
int [] studentGrades = { 90, 100, 80, 50 };
한 학생의 데이터가 3개의 배열에 나누어져 있기 때문에 3개의 배열을 각각 변경해야 합니다!!
그리고 한 학생의 데이터를 관리하기 위해 3개의 배열의 인덱스 순서를 항상 정확하게 맞추어야 합니다.(조금이라도 실수하면 😱)
이렇게 하면 특정 학생의 데이터를 변경시 실수할 가능성이 매우 높습니다.
이 코드는 컴퓨터가 볼 때는 아무 문제가 없지만, 사람이 관리하기에는 좋은 코드가 아닙니다. 😵💫
정리
지금처럼 이름, 나이, 성적을 각각 따로 나누어서 관리하는 것은 사람이 관리하기 좋은 방식이 아닙니다.
사람이 관리하기 좋은 방식은 학생이라는 개념을 하나로 묶는 것입니다.
그리고 각각의 학생 별로 본인의 이름, 나이, 성적을 관리하는 것 입니다.
-
☕️[Java] 메서드 파트 정리.
정리
변수명 vs 메서드명
변수 이름은 일반적으로 명사를 사용합니다.
한편 메서드는 무언가 동작하는데 사용하기 때문에 일반적으로 동사로 시작합니다.
이런 차이점 외에는 변수 이름과 메서드 이름에 대한 규칙은 둘다 같습니다.
변수명 예): customerName, totalSum, employeeCount, isAvailable
메서드명 예): printReport(), calculateSum(), addCustomer(), getEmployeeCount(), setEmployeeName()
메서드 사용의 장점
코드 재사용 : 메서드는 특정 기능을 캡슐화하므로, 필요할 때마다 그 기능을 다시 작성할 필요 없이 해당 메서드를 호출함으로써 코드를 재사용할 수 있습니다.
코드의 가독성 : 이름이 부여된 메서드는 코드가 수행하는 작업을 명확하게 나타내므로, 코드를 읽는 사람에게 추가적인 문맥을 제공합니다.
모듈성 : 큰 프로그램을 작은, 관리 가능한 부분으로 나눌 수 있습니다. 이는 코드의 가독성을 향상시키고 디버깅을 쉽게 만듭니다.
코드 유지 관리 : 메서드를 사용하면, 코드의 특정 부분에서 문제가 발생하거나 업데이트가 필요한 경우 해당 메서드만 수정하면 됩니다. 이렇게 하면 전체 코드 베이스에 영향을 주지 않고 변경 사항을 적용할 수 있습니다.
재사용성과 확장성 : 잘 설계된 메서드는 다른 프로그램이나 프로젝트에서도 재사용할 수 있으며, 새로운 기능을 추가하거나 기존 기능을 확장하는 데 유용합니다.
추상화 : 메서드를 사용하는 곳에서는 메서드의 구현을 몰라도 됩니다. 프로그램의 다른 부분에서는 복잡한 내부 작업에 대해 알 필요 없이 메서드를 사용할 수 있습니다.
테스트와 디버깅 용이성 : 개별 메서드는 독립적으로 테스트하고 디버그할 수 있습니다. 이는 코드의 문제를 신속하게 찾고 수정하는데 도움이 됩니다.
따라서, 메서드는 효율적이고 유지 보수가 가능한 코드를 작성하는 데 매우 중요한 도구입니다.
자바에서의 대원칙.
“자바는 항상 변수의 값을 복사해서 대입합니다.”
이 대원칙은 반드시 이해해야 합니다. 그러면 아무리 복잡한 상황에서도 코드를 단순하게 이해할 수 있습니다.
package method;
public class MethodValue0 {
public static void main(String[] args) {
int num1 = 5;
int num2 = num1;
num2 = 10;
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
}
}
실행 결과
num1 = 5
num2 = 10
용어: 메서드 시그니처(method signature)
메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)
메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻합니다.
메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있습니다.
쉽게 이야기해서 메서드를 구분할 수 있는 기준입니다.
자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야 합니다. 그래야 어떤 메서드를 호출 할 지 결정할 . 수있습니다.
따라서 메서드 오버로딩과 같이 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주합니다.
반환 타입은 시그니처에 포함되지 않습니다.
-
-
💾[Database] DBMS의 분류
DBMS의 분류.
“DBMS” 는 크게 계층형(hierarchical), 망형(network), 관계형(relational), 객체지향형(object-oriented), 객체관계형(object-relational) 으로 분류됩니다.
“계층형(Hierachical) DBMS”
1960년대에 처음 등장한 DBMS 개념입니다.
아래 그림에서 보듯이 각 계층이 트리 형태를 띠고 1:N 관계를 갖습니다.
예를 들어 사장 1명에 부서 3개가 연결되어 있는 구조가 계층형 구조입니다.
계층형 DBMS는 구축한 후 구조를 변경하기가 상당히 까다롭습니다.
주어진 상태에서 검색은 빠르나 접근의 유연성이 부족하여 임의 검색 시 어려움이 있는 것이 단점입니다.
“망형(network) DBMS”
계층형(Hierachical) DBMS의 문제점을 개선하기 위해 1970년대에 시작되었습니다.
1:1, 1:N, N:M(다대다) 관계가 지원되어 효과적이고 빠른 데이터 추출이 가능합니다.
그러나 매우 복잡한 내부 포인터를 사용하고 프로그래머가 모든 구조를 이해해야만 프로그램을 작성할 수 있다는 단점이 여전히 존재합니다.
관계형(Relational) DBMS
1969년 에드거 F.코드(Edgar F. Codd)가 수학 모델에 근거하여 고안했습니다.
관계형(Relational) DBMS의 핵심 개념은 ‘데이터베이스는 테이블(table)’이라는 최소 단위로 구성되어 있으며, 이 테이블은 하나 이상의 열로 구성되어 있다는 것입니다.
관계형 DBMS에서는 모든 데이터가 테이블에 저장됩니다.
테이블이라는 구조는 관계형 DBMS의 가장 기본적이고 중요한 구성으로, 테이블을 잘 이해하면 관계형 DBMS의 기본적인 것을 이해했다고 말할 수 있습니다.
테이블은 데이터를 효율적으로 저장하기 위한 구조입니다.
관계형 DBMS에서는 데이터를 하나가 아닌 여러 개의 테이블에 나누어 저장하므로 불필요한 공간의 낭비를 줄이고 데이터 저장의 효율성을 보장합니다.
이렇게 나뉜 테이블의 관계를 “기본키(Primary Key, PK)” 와 “외래키(Foreign Key, FK)” 를 사용하여 맺음으로써 두 테이블을 부모와 자식 관계로 묶습니다.
그리고 부모와 자식 관계로 연결된 테이블을 서로 조합하여 원하는 결과를 얻을 수 있습니다.
이 때 “SQL(Structured Query Language, 구조화된 질의 언어)” 의 조인(join) 기능을 이용합니다.
테이블은 릴레이션(relation), 엔티티(entity) 등으로도 불립니다.
관계형 DBMS는 다른 DBMS에 비해 업무 변화에 따라 바로 순응할 수 있고 유지﹒보수 측면에서도 편리하다는 특징이 있습니다.
또한 대용량 데이터를 체계적으로 관리할 수 있고 데이터의 무결성도 잘 보장됩니다.
따라서 동시에 데이터에 접근하는 여러 응용 프로그래밍을 사용할 때 관계형 DBMS는 적절한 선택이 될 수 있습니다.
관계형 DBMS의 단점으로는 시스템 자원을 많이 차지하여 시스템이 전반적으로 느려진다는 것을 꼽을 수 있습니다.
그러나 최근에는 하드웨어의 급속한 발전으로 이러한 단점이 많이 보완되고 있습니다.
-
-
-
☕️[Java] 메서드.
메서드의 필요성을 알기위해서 아래의 코드를 보고 실제로 느껴보겠습니다.
아래의 코드는 두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을하는 프로그램입니다.
먼저 1+2를 수행하고, 그 다음으로 10 + 20을 수행합니다.
package method;
public class Method1 {
public static void main(String[] args) {
// 계산1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
System.out.println("결과1 출력: " + sum1);
// 계산2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
System.out.println("결과2 출력:" + sum2);
}
}
위 코드는 다음과 같은 특징이 있습니다.
같은 연산을 두 번 수행합니다.
코드를 잘보면 계산 1 부분과, 계산 2 부분이 거의 같습니다.
계산 1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
계산 2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
계산 1과 2 둘 다 변수를 두 개 선언하고, 어떤 연산을 수행하는지 출력하고, 두 변수를 더해서 결과를 구합니다.
만약 프로그램의 여러 곳에서 이와 같은 계산을 반복해야 할 경우에는 같은 코드를 여러번 반복해서 작성해야 할 것입니다.
더 나아가서 어떤 연산을 수행하는지 출력하는 부분을 변경하거나 또는 제거하고 싶다면 해당 코드를 다 찾아다니면서 모두 수정해야 할 것 입니다.
함수(Function)
함수 정의
add(a, b) = a + b
이름이 add이고, a,b라는 두 값을 받는 함수입니다. 그리고 이 함수는 a + b 연산을 수행합니다.
함수 사용
add(1,2) -> 결과: 3
add(5,6) -> 결과: 11
add(3,5) -> 결과: 8
함수에 값을 입력하면, 함수가 가진 연산을 처리한 다음 결과를 출력합니다. 여기서는 단순히 a + b라는 연산을 수행합니다.
여러번 같은 계산을 해야 한다면 지금처럼 함수를 만들어두고(정의), 필요한 입력 값을 넣어서 해당 함수를 호출하면 됩니다. 그러면 계산된 결과가 나옵니다.
함수는 마치 마술상자와 같습니다. 함수를 호출할 때는 외부에서는 필요한 값만 입력하면 됩니다. 그러면 계산된 결과가 출력됩니다.
내부 구조는 어떻게 되어있는지 알 필요가 없습니다.
같은 함수를 다른 입력 값으로 여러번 호출할 수 있습니다.
여기서 핵심은 함수를 한 번 정의해두면 계속해서 재사용할 수 있다는 점입니다!
평균 함수
만약 두 수의 평균을 구해야 한다면 매번 (a + b) / 2라는 공식을 사용해야 할 것입니다.
이것을 함수로 만들어두면 다음과 같이 사용할 수 있습니다.
함수 정의
avg(a, b) = (a + b) / 2
함수 사용
avg(4, 6) -> 결과: 5
avg(10, 20) -> 결과: 15
avg(100, 200) -> 결과: 150
수하의 함수의 개념을 프로그래밍에 가지고 온다면 어떨까요?
필요한 기능을 미리 정의해두고 필요할 때 마다 호출해서 사용할 수 있기 때문에 앞서 고민한 문제들을 해결할 수 있을 것 같습니다.
프로그램 언어들은 오래 전 부터 이런 문제를 해결하기 위해 수학의 함수라는 개념을 차용해서 사용합니다.
-
-
-
☕️[Java] 메서드(2)
메서드 정의
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
위 코드가 바로 메서드입니다.
이것을 함수를 정의하는 것과 같이, 메서드를 정의한다고 표현합니다.
메서드는 수학의 함수와 유사하게 생겼습니다.
함수에 값을 입력하면, 어떤 연산을 처리한 다음에 결과를 반환합니다.
수학에 너무 집중하지 않아도 됩니다, 단순히 무언가 정의해두고 필요할 때 불러서 사용한다는 개념으로 이해하면 충분합니다.
메서드는 크게 “메서드 선언” 과 “매서드 본문” 으로 나눌 수 있습니다.
메서드 선언(Method Declaration)
public static int add(int a, int b)
메서드의 선언 부분으로, 메서드 이름, 반환 타입, 파라미터(매개변수) 목록을 포함합니다.
이름 그대로 이런 메서드가 있다고 선언하는 것입니다.
메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있습니다.
public static
public: 다른 클래스에서 호출할 수 있는 메서드라는 뜻입니다. (접근 제어에서 학습할 예정)
static: 객체를 생성하지 않고 호출할 수 있는 정적 메서드라는 뜻입니다. (자세한 내용은 추후에 정리)
int add(int a, int b)
int: 반환 타입을 정의합니다. 메서드의 실행 결과를 반환할 때 사용할 반환 타입을 지정합니다.
add: 메서드의 이름입니다. 이 이름으로 메서드를 호출할 수 있습니다.
(int a, int b): 메서드를 호출할 때 전달하는 입력 값을 정의합니다. 이 변수들은 해당 메서드 안에서만 사용됩니다. 이렇게 메서드 선언에 사용되는 변수를 영어로 파라미터(parameter), 한글로 매개변수라 합니다.
메서드 본문(Method Body)
{
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드가 수행해야 하는 코드 블록입니다.
메서드를 호출하면 메서드 본문이 순서대로 실행됩니다.
메서드 본문은 마술상자입니다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모르기 때문입니다.
메서드의 실행 결과를 반환하려면 return문을 사용해야 합니다. return문은 다음에 반환할 결과를 적어주면 됩니다.
return sum: sum 변수에 들어있는 값을 반환합니다.
메서드 호출
앞서 정의한 메서드를 호출해서 실행하려면 메서드 이름에 입력 값을 전달하면 됩니다. 보통 메서드를 호출한다고 표현합니다.
int sum1 = add(5, 10);
int sum2 = add(15, 20);
메서드를 호출하면 어떻게 실행되는지 순서대로 확인해봅시다.
int sum1 = add(5, 10); // add라는 메서드를 숫자 5, 10을 전달하면서 호출합니다.
int sum1 = 15; // add(5, 10)이 실행됩니다. 실행 결과 반환 값은 15입니다.
메서드를 호출하면 메서드는 계산을 끝내고 결과를 반환합니다.
쉽게 이야기하자면, 메서드 호출이 끝나면 해당 메서드가 반환한 결과 값으로 치환됩니다.
메서드 호출이 끝나면 더 이상 해당 메서드가 사용한 메모리를 낭비할 이유가 없습니다.
메서드 호출이 끝나면 메서드 정의에 사용한 파라미터 변수인 int a, int b는 물론이고, 그 안에서 정의한 int sum도 모두 제거 되기 때문입니다.
메서드 호출과 용어정리
메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 합니다.
물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 합니다.
호출: call("hello", 20)
메서드 정의: int call(String str, int age)
인수(Argument)
여기서 hello,20 처럼 넘기는 값을 영어로 Argument(아큐먼트), 한글로 인수 또는 인자라 합니다.
실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용합니다.
매개변수(Parameter)
메서드를 정의할 때 선언한 변수인 String str, int age를 매개변수, 파라미터라 합니다.
메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입됩니다.
실무에서는 매개변수, 파라미터 용어를 모두 사용합니다.
용어정리
인수라는 용어는 ‘인’과 ‘수’의 합성어로, ‘들어가는 수’라는 의미를 가집니다. 즉, 메서드 내부로 들어가는 값을 의미합니다. 인자도 같은 의미입니다.
매개변수, parameter는 ‘매개’와 ‘변수’의 합성어로 ‘중간에서 전달하는 변수’라는 의미를 가집니다. 즉, 메서드 호출부와 메서드 내부 사이에서 값을 전달하는 역할을 하는 변수라는 뜻입니다.
-
🍃[Spring Boot] 스프링?
Intro.
스프링 프레임워크(Spring Framework) 는 자바(Java) 가반의 애플리케이션 프레임워크로 엔터프라이즈급 애플리케이션을 개발하기 위한 다양한 기능을 제공합니다.
스프링은 목적에 따라 다양한 프로젝트를 제공하는데, 그중 하나가 스프링 부트(Spring Boot) 입니다.
이번 포스팅에서는 먼저 스프링 부트의 기반인 스프링 프레임워크를 알아보고, 스프링이 제공하는 다양한 프로젝트 중 하나인 스프링 부트의 특징을 설명하겠습니다.
스프링 프레임워크.
스프링 프레임워크(이후 스프링) 는 자바에서 가장 많이 사용하는 프레임워크입니다.
스프링은 자바 언어를 이용해 엔터프라이즈급 개발을 편리하게 만들어주는 ‘오픈소스 경량급 애플리케이션 프레임워크’로 불리고 있습니다.
쉽게 말해서 자바로 애플리케이션을 개발하는 데 필요한 기능을 제공하고 쉽게 사용하도록 돕는 도구입니다.
TIP: ‘엔터프라이즈급 개발?’
‘엔터프라이즈급 개발’은 기업 환경을 대상으로 하는 개발을 뜻합니다.
네이버나 카카오톡 같은 대규모 데이터를 처리하는 환경을 엔터프라이즈 환경이라고 부릅니다.
스프링은 이 환경에 알맞게 설계되어 있어 개발자는 애플리케이션을 개발할 때 많은 요소를 프레임워크에 위임하고 비즈니스 로직을 구현하는 데 집중할 수 있습니다.
스프링의 핵심 가치는 “애플리케이션 개발에 필요한 기반을 제공해서 개발자가 비즈니스 로직 구현에만 집중할 수 있게끔 하는 것” 입니다.
제어 역적(IoC)
일반적인 자바 개발의 경우 객체를 사용하기 위해 아래의 예제 코드와 같은 코드를 사용합니다.
@RestController
public class NoDIController {
private MyService service = new MyServiceImpl();
@GetMapping("/no-di/hello")
public String getHello() {
return service.getHello();
}
}
즉, 사용하려는 객체를 선언하고 해당 객체의 의존성을 생성한 후 객체에서 제공하는 기능을 사용합니다.
객체를 생성하고 사용하는 일련의 작업을 개발자가 직접 제어하는 구조입니다.
하지만 제어 역전(IoC: Inversion of Controller) 을 특징으로 하는 스프링은 기존 자바 개발 방식과 다르게 동작합니다.
IoC를 적용한 환경에서는 사용할 객체를 직접 생성하지 않고 객체의 생명주기 관리를 외부에 위임합니다.
여기서 ‘외부’ 는 스프링 컨테이너(Spring Container) 또는 IoC 컨테이너(IoC Container) 를 의미합니다.
“객체의 관리를 컨테이너에 맡겨 제어권이 넘어간 것”을 제어 역전이라고 부르며, 제어 역전을 통해 의존성 주입(DI: Dependency Injection), 관점 지향 프로그래밍(AOP: Aspect-Oriented Programming) 등이 가능해집니다.
스프링 을 사용하면 객체의 제어권을 컨테이너로 넘기기 때문에 “개발자는 비즈니스 로직을 작성하는 데 더 집중” 할 수 있습니다.
의존성 주입(DI)
의존성 주입(DI: Dependency Injection)이란 “제어 역전의 방법 중 하나”로, 사용할 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용하는 방식을 의미합니다.
스프링에서 의존성을 주입받는 방법은 3가지가 있습니다.
생성자를 통한 의존성 주입
필드 객체 선언을 통한 의존성 주입
setter 메서드를 통한 의존성 주입
스프링에서는 @Autowired라는 어노테이션(annotation)을 통해 의존성을 주입할 수 있습니다.
스프링 4.3 이후 버전은 생성자를 통해 의존성을 주입할 때 @Autowired 어노테이션을 생략할 수도 있습니다.
하지만 스프링을 처음 다룰 때는 가독성을 위해 어노테이션을 명시하기를 권장합니다.
스프링에서 의존성을 주입받는 각 방법에 대한 예시 코드는 아래와 같습니다.
// 생성자를 통한 의존성 주입
@RestController
public class DIController {
// <-- 의존성을 주입 받는 주요부분
MyService myService;
@Autowired
public DIController(MyService myService) {
this.myService = myServicel
}
// -->
@GetMapping("di/hello")
public String getHello() {
return myService.getHello();
}
}
// 필드 객체 선언을 통한 의존성 주입
@RestController
public class FieldInjectionController {
// <-- 의존성을 주입 받는 주요부분
@Autowired
private MyService myService;
// -->
}
// setter 메서드를 통한 의존성 주입
@RestController
public class SetterInjectionController {
// <-- 의존성을 주입 받는 주요부분
MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
// -->
}
스프링 공식 문서에서 권장하는 의존성 주입 방법은 “생성자를 통해 의존성을 주입받는 방식” 입니다.
다른 방식과는 다르게 생성자를 통해 의존성을 주입받는 방식은 “레퍼런스 객체 없이는 객체를 초기화할 수 없게 설계할 수 있기 때문입니다.”
관점 지향 프로그래밍(AOP)
관점 지향 프로그래밍(이후 AOP: Aspect-Oriented Programming) 은 스프링의 아주 중요한 특징입니다.
AOP는 OOP를 더욱 잘 사용하도록 돕는 개념으로 보는 것이 좋습니다.
스터디 가이드
OOP를 요약하자면 각 기능을 재사용 가능한 개별 객체로 구성해 프로그래밍하는 것을 뜻합니다.
다음과 같은 OOP의 핵심키워드를 이해한다면 더 나은 객체지행 프로그래밍이 가능합니다.
추상화(abstraction)
캡슐화(encapsulation)
상속(inheritance)
다형성(polymorphism)
“AOP는 관점을 기준으로 묶어 개발하는 방식을 의미합니다.”
여기서 “관점(aspect)” 이란 “어떤 기능을 구현할 때 그 기능을 ‘핵심 기능’과 ‘부가 기능’으로 구분해 각각을 하나의 관점으로 보는 것을 의미” 합니다.
“핵심기능”
비즈니스로직을 구현하는 과정에서 비즈니스 로직이 처리하려는 목적 기능을 말합니다.
예를 들면, 클라이언트로부터 상품 정보 등록 요청을 받아 데이터베이스에 저장하고, 그 상품 정보를 조회하는 비즈니스 로직을 구현한다면
(1) 상품 정보를 데이터베이스에 저장하고,
(2) 저장된 상품 정보 데이터를 보여주는 코드가 핵심 기능입니다.
그런데 실제 애플리케이션을 개발할 때는 핵심 기능에 부가 기능을 추가할 상황이 생깁니다.
“핵심 기능인 비즈니스 로직 사이에 로깅 처리를 하거나 트랜잭션을 처리하는 코드를 예로 들 수 있습니다.”
일반적인 OOP 형식으로 비즈니스 로직을 작성하면 아래 그림과 같이 비즈니스 동작 흐름이 발생합니다.
OOP 방식의 애플리케이션 로직에서는 위 그림과 같이 객채마다 핵심 기능을 수행하기 위한 “로직” 과 함께 부가 기능인 “로깅”, “트랜잭션” 등의 코드를 작성합니다.
위 그림의 상품정보 등록 기능과 상품정보 조회 기능은 엄연히 다른 기능으로, 각자 로직이 구현돼 있습니다.
하지만 유지보수 목적이나 데이터베이스 접근을 위해 작성된 “로깅” 과 “트랜잭션” 영역은 상품정보를 등록할 때나 상품정보를 조회할 때 동일한 기능을 수행할 확률이 높습니다.
즉, 핵심 기능을 구현한 두 로직에 동일한 코드가 포함된다는 것을 의미합니다.
AOP의 관점에서는 부가 기능은 핵심 기능이 어떤 기능인지에 구관하게 로직이 수행되기 전 또는 후에 수행되기만 하면 됩니다.
그래서 아래 그림과 같은 구성으로 만들 수 있습니다.
이처럼 여러 비즈니스 로직에서 반복되는 부가 기능을 하나의 “공통 로직으로 처리하도록 모듈화해 삽입하는 방식” 을 “AOP” 라고 합니다.
이러한 AOP를 구현하는 방법은 크게 세 가지가 있습니다.
컴파일 과정에 삽입하는 방식
바이트코드를 메모리에 로드하는 과정에 삽입하는 방식
프락시 패턴을 이용한 방식
이 가운데 스프링은 디자인 패턴 중 하나인 “프락시 패턴” 을 통해 “AOP” 기능을 제공하고 있습니다.
스프링 AOP의 목적은 OOP와 마찬가지로 모듈화해서 재사용 가능한 구성을 만드는 것이고, 모듈화된 객체를 편하게 적용할 수 있게 함으로써 개발자가 비즈니스 로직을 구현하는 데만 집중할 수 있게 도와주는 것입니다.
스프링 프레임워크의 다양한 모듈
스프링 프레임워크는 기능별로 구분된 약 20여 개의 모듈로 구성돼 있습니다.
아래 그림은 스프링 공식 문서에서 제공하는 다이어그램입니다.
스프링 프레임워크 공식 문서에서는 스프링 버전별로 다른 다이어그램을 제시하고 있지만 큰 틀은 유사합니다.
그리고 스프링 프레임워크를 사용한다고 해서 모든 모듈을 사용할 필요는 없습니다.
애플리케이션 개발에 필요한 모듈만 선택해서 사용하게끔 설계돼 있으며, 이를 “경량 컨테이너 설계”라고 부릅니다.
-
-
-
-
-
-
-
-
☕️[JAVA] Packaing 옵션.
☕️[JAVA] Packaing 옵션.
Spring Initializr에서 "Packaing" 옵션을 선택할 때 'Jar'와 'War' 중에 선택해야 합니다.
어떤 것을 선택해야 할지는 개발하려는 어플리케이션의 유형과 배포 환경에 따라 달라집니다.
각 포맷에 대한 설명.
1️⃣ Jar (Java Archive)
Jar 파일은 Java 클래스 파일, 메타데이터, 리소스 파일을 하나의 파일로 압축한 포맷입니다.
스탠드얼론(Spring Boot 어플리케이션 권장 포맷): Jar 포맷은 내장된 서버(예: Tomcat, Jetty)를 사용하여 스프링 부트 어플리케이션을 스탠드얼론 어플리케이션으로 실행할 수 있게 합니다. 이는 별도의 웹 서버 설치 없이도 실행 가능하며, 마이크로서비스, 클라우드 어플리케이션 개발에 적합합니다.
간편한 배포와 실행: Jar 파일은 'java -jar' 명령어로 쉽게 실행할 수 있으며, 도커 컨테이너와 같은 환경에 배포하기도 용이합니다.
2️⃣ War (Web Application Archive)
War 파일은 웹 어플리케이션에 필요한 Java 클래스 파일, JSP(JavaServer Pages), 서블릿, 리소스 파일, 메타데이터 등을 포함한 포맷입니다.
전통적인 웹 어플리케이션: War 포맷은 서블릿 컨테이너나 어플리케이션 서버(예: Tomcat, Jetty, WebLogic, WildFly)에 배포될 전통적인 웹 어플리케이션 개발에 사용됩니다. 이 경우, 어플리게이션 서버가 웹 어플리케이션을 실행하는데 필요한 환경을 제공합니다.
엔터프라이즈 환경: 복잡한 엔터프라이즈 환경에서는 여러 어플리케이션을 하나의 서버에 배포해야 할 필요가 있을 수 있으며, War 포맷이 이러한 요구 사항을 충족시킬 수 있습니다.
🙌 선택 기준
스탠드얼론 어플리케이션 개발 및 마이크로 아키텍처를 선호한다면 'Jar'를 선택하세요.
기존의 엔터프라이즈 환경에서 어플리케이션 서버를 사용해야 한다면 'War'를 선택하세요.
Spring Boot는 두 가지 포맷 모두를 지원하므로, 프로젝트 요구 사항과 배포 환경에 맞게 최적의 옵션을 선택할 수 있습니다.
-
📚[Book] The old man and the sea (7).
📚[Book] The old man and the sea (7).
sardines: 정어리
*"So I can get the cast net and go after the sardines"
*"그래서 나는 투망을 가져다가 정어리를 잡으러 갈 수 있어요"
hard: 단단한,견고한
barided: 땋아진, 삼줄로 엮어진
hard-barided line: 단단하게 땋아진
harpoon: (고래나 큰 물고기를 잡는데 사용되는) 창
*The old man carried mast on his shoulder and the boy carried the wooden box with the coiled, hard-barided lines, the gaff and the harpoon with the its shaft.
*노인은 돛대를 어깨에 메고, 소년은 감겨 있고 단단하게 엮인 갈색 줄, 갈고리대, 그리고 창과 그 손잡이가 담긴 나무 상자를 들고 갔다.
stern: 선미
subdue: 제압하다, 통제하다
*The box with the baits was under the stern of the skiff along with the club that was used to subdue the big fish when they wew brought alongside
*미끼 상자는 보트의 선미 아래에 있었고, 큰 물고기를 옆으로 끌어당겼을 때 그것들을 제압하기 위해 사용된 몽둥이도 함께 있었다.
dew: 이슬
through: ~을 통하여, ~동안, 끝까지
temptation: 유혹
*No one would steal from the old man but it was better to take the sail and the heavy lines home as the dew was bad for them and, though he was quite sure no local people would steal from him, the old man thouhjt that a gaff and a harpoon were needless temptations
아무도 그 노인에게서 훔치지 않겠지만, 이슬이 돛과 무거운 줄들에게 해로웠기 때문에 그것들을 집에 가져가는 것이 나았고, 비록 현지 사람들이 자신에게서 훔치지 않을 것이라고 확신하고 있었지만, 노인은 갈고리대와 창은 불필요한 유혹이라고 생각했다.
nearly: 거의, 대략
*The mast was nearly as long as the one room of the shack.
*돛대는 오두막의 한 방만큼이나 거의 길었다.
budshields: 봉오리 껍질
royal plam: (야자수의 한 종류) 로열 팜
*The shack was made of the tough budshields of the royal palm which are called guano and in it there was a bed, a table, one chair, and a place on the dirt floor to cook with charcoal.
오두막은 로열 팜의 튼튼한 봉오리 껍질로 만들어졌으며, 이것을 구아노라고 부릅니다. 그안에는 침대, 탁자, 의자 하나 그리고 숯으로 요리할 수 있는 흙바닥 위의 공간이 있습니다.
-
-
-
-
-
-
-
🌐 [Network, AWS] Subnet이란?
🌐 [Network, AWS] Subnet이란?
서브넷(Subnet 또는 Subnetwork)은 IP 네트워크를 더 작은, 관리 가능한 부분으로 나누는 방법입니다.
서브네팅은 효율적인 IP 주소 관리, 네트워크 트래픽의 분리 및 제어, 보안 강화를 위해 널리 사용됩니다.
네트워크를 서브넷으로 분할하면 네트워크의 복잡성을 줄이고, 네트워크 성능을 최적화하며, 보안을 강화할 수 있습니다.
서브넷의 주요 개념
IP 주소 할당: 네트워크를 서브넷으로 나누면 각 서브넷에 고유한 IP 주소 범위가 할당됩니다. 이를 통해 네트워크 내에서 트래픽을 효과적으로 라우팅할 수 있습니다.
네트워크 마스크: 서브넷을 식별하기 위해 IP 주소와 함꼐 사용되는 네트워크 마스크(또는 서브넷 마스크)가 있습니다. 네트워크 마스크는 IP 주소의 어느 부분이 네트워크 주소에 해당하고 어느 부분이 호스트 주소에 해당하는지를 정의합니다.
브로드캐스트 도메인 분할: 서브네팅을 사용하면 네트워크의 브로드캐스트 도메인을 분할하여 네트워크 트래픽을 줄이고 성능을 향상시킬 수 있습니다. 각 서브넷은 독립된 브로드캐스트 도메인을 형성합니다.
보안과 관리: 서브넷은 네트워크 리소스에 대한 접근을 제어하는 데 사용될 수 있으며, 네트워크 내의 세그먼트를 보다 쉽게 관리하고 모니터링할 수 있게합니다.
서브넷 사용 예시
기업 네트워크: 기업은 다양한 부서나 기능별로 서브넷을 구성하여 네트워크 리소스를 효율적으로 관리하고 보안을 강화할 수 있습니다.
공용 및 프라이빗 클라우드 환경: 클라우드 환경에서는 VPC 내에서 여러 서브넷을 구성하여 고용 서비스와 프라이빗 리소스를 분리할 수 있습니다.
IoT 네트워크: IoT(Internet Of Things) 환경에서는 서브넷을 사용하여 다양한 유형의 장치를 분리하고, 네트워크 트래픽을 관리하며, 보안을 강화할 수 있습니다.
서브넷 구성은 네트워크 설계의 중요한 부분이며, 네트워크의 규모와 복잡성에 따라 다양한 방식으로 구현될 수 있습니다.
-
-
-
-
-
-
-
-
📚[Book] The old man and the sea.
📚[Book] The old man and the sea.
skiff: 작은 보트
Gulf Stream: 맥시코 만류
*He was an old man who fished alone in a skiff in the Gulf Stream and he had gone eighty-four days now without taking a fish.
*그는 맥시코 만류에서 작은 보트를 타고 혼자 낚시를 하는 노인이었는데, 지금까지 84일 동안 물고기를 한 마리도 잡지 못한 채 지내고 있었습니다.
salao: 최악의 불운한 상태를 뜻하는 스페인어
gaff: 갈고릿대
harpoon: 작살
mast: 돛대
furled: 감다, 감겨 오르다, 펄럭이는
permanent: 영구적인
*It made the boy sad to see the old man come in each day with his skiff empty and he always went down to help him carry either the coled lines or the gaff and harpoon and the sail that was furled around the mast.
*노인이 매일 빈 배를 들고 들어오는 것을 보고 소년은 슬펏고, 그는 항상 내려가서 낚싯줄이나 작살, 돛대 주위에 휘감긴 돛을 나르는 것을 도왔습니다.
sail: 돛
sacks: 자루, 마대
*The sail was patched with flour sacks and, furled, it looked like the flag of permanent defeat.
돛에는 밀가루 자루가 덧대어져 있었고, 펼쳐져 있으면 마치 영원한 패배를 알리는 깃발처럼 보였습니다.
이 문장에서 'fruled'는 '휘감긴, 감다'가 아닌 '펄럭이는, 펼펴져 있는'으로 해석되었습니니다.
gaunt: 쓸쓸한, 수척한.
*The old man was thin and gaunt woth deep wrinkles in the back of his neck.
*그 노인은 목덜미에 깊은 주름이 있고 마르고 여위었습니다.
benevolent: 자애로운
blotches: 얼룩
*The brown blotches of the benevolent skin cancer the sun brings from its reflection on the tropic sea were on his cheeks.
*열대 바다의 반사로 태양이 가져다주는 자비로운 피부암의 갈색 얼룩들이 그의 볼에 있었다.
-
-
🌐[Network] HTTP 통신.
🌐[Network] HTTP 통신이란?
HTTP(HyperText Transfer Protocol) 통신은 월드 와이드 웹(World Wide Web)에서 데이터를 주고받는 데 사용되는 주요 프로토콜입니다.
이 프로토콜은 웹 서버와 클라이언트(대게 웹 브라우저)간의 통신을 위해 설계되었습니다.
🌐 HTTP 통신의 주요 특징.
1. 클라이언트-서버 모델 : HTTP는 클라이언트-서버 모델을 따릅니다.
클라이언트(예: 웹 브라우저)는 서버에 요청(Request)을 보내고, 서버는 이에 대한 응답(Response)을 반환합니다.
2. 무상태성(Stateless) : HTTP는 무상태 프로토콜입니다.
즉, 각 요청은 독립적이며, 서버는 이전 요청에 대한 정보를 저장하지 않습니다.
이는 통신을 단순화하지만, 세션 관리를 위해 쿠기와 같은 메커니즘을 사용해야 합니다
3. HTTP 메소드 : HTTP는 다양한 메소드(GET, POST, PUT, DELETE 등)를 사용하여 리소드(웹 페이지, 이미지, 파일 등)에 대한 다양한 작업을 수행할 수 있습니다.
4. 확장 가능 : HTTP 헤더를 통해 프로토콜을 확장할 수 있습니다.이를 통해 메타데이터, 캐싱 정책, 인증 정보 등을 전송할 수 있습니다.
🌐 HTTP 작동 원리.
1. 요청 시작 : 사용자가 웹 브라우저에서 URL을 입력하거나 링크를 클릭하면 HTTP 요청이 시작됩니다.
2. 서버로의 요청 전송 : 웹 브라우저는 해당 서버의 주소를 찾고(도메인 이름 시스템을 통해 IP 주소를 확인)해당 서버에 연결하여 HTTP 요청을 전송합니다.
3. 서버 처리 및 응답 : 웹 서버는 요청을 받고 처리한 뒤, 요청된 리소스(HTML 페이지, 이미지, 파일 등) 또는 오류 메시지, 리디렉션 정보 등을 포함하는 HTTP 응답을 보냅니다.
4. 콘텐츠 렌더링 : 클라이언트(웹 브라우저)는 응답을 받고, 그 내용을 해석하여 사용자에게 표시합니다. 예를 들어, HTML 문서가 반환되면 브라우저는 이를 파싱하여 화면에 웹 페이지로 렌더링합니다.
5. 연결 종료 : 통신이 완료되면 TCP 연결이 종료됩니다.
HTTP/1.1에서는 지속 연결(keep-alive)을 통해 여러 요청과 응답을 같은 연결로 처리할 수 있습니다.
🌐 HTTP 버전.
HTTP/1.x : 가장 널리 사용되는 버전으로, 각 요청/응답마다 별도의 연결을 맺습니다(HTTP/1.0) 또는 지속 연결을 사용합니다(HTTP/1.1)
HTTP/2 : 성능 향상을 위해 도입된 버전으로, 여러 요청을 동시에 하나의 연결로 처리할 수 있는 멀티플렉싱, 헤더 압축 등의 기능을 제공합니다.
HTTP/3 : 최신 버전으로, UDP 기반의 QUIC 프로토콜을 사용하여 연결의 설정 시간을 단축하고, 패킷 손실에 더 강한 성능을 보입니다
HTTP 통신은 웹의 기본적인 동작 방식을 정의하며, 현대 인터넷에서 가장 중요한 프로토콜 중 하나입니다.
-
-
-
🆙 [LeetCode] 88.Merge Sorted Array.
🆙 [LeetCode] 88.Merge Sorted Array.
Difficulty: Easy
Topic: Array, Two Pointer, Sorting
Approach 1: Merge and sort
Intuition(직관)
순진한 접근 방식은 nums2의 값을 그저 nums1의 끝에 쓰고, 그다음 nums1을 정렬하는 것입니다.
우리는 값을 반환할 필요가 없으며, nums1을 직접 수정해야 합니다.
이 방법은 코딩하기는 쉽지만, 이미 정렬된 상태를 활용하지 않기 때문에 높은 시간 복잡도를 가집니다.
Implementation(구현)
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
for i in 0..<n {
nums1[i + m] = nums2[i]
}
nums1.sort()
}
}
Time complexity(시간 복잡도): O((n + m) log(n+m))
내장된 정렬 알고리즘을 사용하여 길이가 x인 리스트를 정렬하는 비용은 O(xlogx)입니다. 이 경우에는 길이가 m+n인 리스트를 정렬하므로 총 시간 복잡도는 O((n + m) log(n + m))가 됩니다.
Space complexity(공간 복잡도): O(n), 하지만 상황에 따라 다를 수 있습니다.
대부분의 프로그래밍 언어는 O(n) 공간을 사용하는 내장 정렬 알고리즘을 가지고 있습니다.
Approach 2: Three Pointers (Start From the Beginning)
Intuition(직관)
각 배열이 이미 정렬되어 있기 때문에, Two pointer 기법을 활용하면 O(n+m)의 시간 복잡도를 달성할 수 있습니다.
Algorithm
nums1의 값을 복사하여 nums1Copy라는 새 배열을 만드는 것이 가장 간단한 구현 방법입니다.
그런 다음 두 개의 읽기(read) 포인터와 하나의 쓰기(write) 포인터를 사용하여 nums1Copy와 nums2에서 값을 읽고 nums1에 씁니다.
nums1Copy를 nums1의 처음 m 값이 포함된 새 배열로 초기화합니다.
읽기 포인터 p1을 nums1Copy의 시작 부분에 초기화합니다.
읽기 포인터 p2를 nums2의 시작 부분에 초기화합니다.
쓰기 포인터 p를 nums1의 시작 부분에 초기화합니다.
p가 여전히 nums1 내에 있는 동안:
nums1Copy[p1]이 존재하고 nums2[p2] 보다 작거나 같으면:
nums1Copy[p1]을 nums1[p]에 쓰고 p1을 1 증가시킵니다.
그렇지 않으면
nums2[p2]를 nums1[p]에 쓰고 p2를 1 증가시킵니다.
p를 1 증가시킵니다.
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
// nums1의 처음 m개 원소의 복사본을 만듭니다.
let nums1Copy = Array(nums1[0..<m])
// nums1Copy와 nums2에 대한 읽기 포인터입니다.
var p1 = 0
var p2 = 0
// nums1Copy와 nums2에서 원소를 비교하여 더 작은 것을 nums1에 씁니다.
for p in 0..<(m + n) {
// p1과 p2가 각각의 배열 범위를 벗어나지 않도록 확인합니다.
if p2 >= n || (p1 < m && nums1Copy[p1] < nums2[p2]) {
nums1[p] = nums1Copy[p1]
p1 += 1
} else {
nums1[p] = nums2[p2]
p2 += 1
}
}
}
}
Complexity Analysis
Time complexity(시간 복잡도) : O(n+m)
우리는 n+2*m 번의 읽기와 n+2*m 번의 쓰기를 수행하고 있습니다. Big O 표기법에서 상수는 무시되므로, 이는 O(n+m)의 시간 복잡도를 의미합니다.
Space complexity(공간 복잡도) : O(m)
우리는 추가적으로 길이가 m인 배열을 할당하고 있습니다.
Approach 3: Three Pointers (Start From the End)
Intuition
인터뷰 팁: 이것은 쉬운 문제에 대한 중간 수준의 솔루션입니다.
쉬운 수준의 문제 중 상당수는 더 어려운 해결책을 갖고 있으며,
좋은 지원자는 이를 찾을것으로 예상됩니다.
Approach 2는 이미 최상의 시간 복잡도인 O(n+m)을 보여주지만, 여전히 추가 공간을 사용합니다.
이는 nums1 배열의 요소들을 어딘가에 저장해야 하기 때문에, 그것들이 덮어쓰여지지 않도록 해야하기 때문입니다
그렇다면 대신 nums1의 끝부터 덮어쓰기 시작하면 어떨까요? 거기에는 아직 정보가 없으니까요.
알고리즘은 이전과 유사하지만, 이번에는 p1을 nums1의 m - 1 인덱스에, p2를 nums2의 n - 1 인덱스에, 그리고 p를 nums1의 m + n - 1 인덱스에 두는 방식입니다.
이 방식으로, nums1의 처음 m 값들을 덮어쓰기 시작할 때, 이미 각각을 새 위치에 써 놓았을 것이라는 것이 보장됩니다.
이런 방식으로, 추가 공간을 없앨 수 있습니다.
인터뷰 팁: 베열 문제를 제자리에서 해결하려고 할 때는 항상 배열을 앞에서 뒤로 순회하는 대신 뒤에서 앞으로 순회하는 가능성을 고려해보세요
이것은 문제를 완전히 바꾸어 놓고, 훨씩 쉽게 만들 수 있습니다.
Implementation
1️⃣
2️⃣
3️⃣
4️⃣
5️⃣
6️⃣
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
// 각 배열의 끝을 가리키는 p1과 p2를 설정합니다.
var p1 = m - 1
var p2 = n - 1
// p를 배열을 통해 뒤로 이동하면서, 매번 p1 또는 p2가 가리키는 더 작은 값을 작성합니다.
for p in stride(from: m + n - 1, through: 0, by: -1) {
if p2 < 0 {
break
}
if p1 >= 0 && nums1[p1] > nums2[p2] {
nums1[p] = nums1[p1]
p1 -= 1
} else {
nums1[p] = nums2[p2]
p2 -= 1
}
}
}
}
Complexity Analysis
Time complexity: O(n + m)
Same as Approach 2.
Space complexity: O(1)
Unlike Approach 2, we’re not using an extra array.
Proof(optional)
이 주장에 대해 조금 회의적일 수도 있습니다.
정말 모든 경우에 작동하나요?
이렇게 대담한 주장을 하는 것이 안전한가요?
이 방식으로, `nums1`의 처음 `m`개 값을 덮어쓰기 시작하면, 각각을 이미 새 위치에 써 놓았을 것입니다
이런 방식으로 우리는 추가 공간을 없앨 수 있습니다.
훌륭한 질문입니다!
그렇다면 왜 이 방법이 작동할까요?
이를 증명하기 위해, p가 nums1에서 p1이 아직 읽지 않은 값을 덮어쓰지 않는 것을 확실히 해야 합니다.
조언 :증명에 겁을 먹고 있나요?
많은 소프트웨어 엔지니어들이 그렇습니다.
좋은 증명은 간단히 각각의 논리적 주장들이 다음 주장 위에 구축되는 것입니다.
이런 방식으로, 우리는 "명백한" 진술로부터 시작하여 증명하고자 하는 것에 이룰 수 있습니다.
각 진술을 하나씩 읽으며, 다음으로 넘어가기 전에 각각을 이해하는 것이 중요합니다.
초기화 시 p는 p1보다 n만큼 앞서 있다는 것을 알 수 있습니다.(다른 말로, p1 + n = p 입니다.)
또한, 이 알고리즘이 수행하는 p의 반복 동안, p는 항상 1 만큼 감소하고, p1 또는 p2 중 하나가 1 만큼 감소한다는 것도 알고 있습니다.
p1이 감소할 때, p와 p1 사이의 간격은 동일하게 유지되므로, 그 경우에 “추월(overtake)”이 발생할 수 없다는 것을 추론할 수 있습니다.
하지만 p2가 감소할 때는, p는 움직이지만 p1은 그렇지 않으므로, p와 p1 사이의 간격이 1만큼 줄어든가는 것을 추론할 수 있습니다.
그리고 이로부터, p2가 감소할 수 있는 최대 횟수는 n번임을 추론할 수 있습니다. 다시 말해, p와 p1 사이의 간격은 최대 n 만큼 1 씩 줄어들 수 있습니다.
결론적으로, 그들이 처음에 n만큼 떨어져 있었기 때문에 추월이 일어날 수 없습니다. 그리고 p = p1일 때, 간격은 n 번 줄어들어야 합니다. 이는 nums2의 모든 것이 병합되었으므로 더 이상 할 일이 없음을 의미합니다.
-
🆙 [LeetCode] 1089.Duplicate Zeros.
🆙 [LeetCode] 1089.Duplicate Zeros
Difficulty: Easy
Topic: Array, Two Pointers
문제는 배열을 제자리에서 수정하도록 요구합니다.
제자리 수정이 제약 조건이 아니라면, 원본 배열에서 대상 배열로 요소를 복사하는 방법을 사용했을 것입니다.
0을 두 번 복사한 것을 주목하세요.
var s = 0
var d = 0
let N = source.count // 여기서 'source'는 Int 타입의 배열이라고 가정합니다.
var destination = [Int]()
// 목적지 배열이 가득 찰 때까지 복사가 수행됩니다.
while s < N {
if source[s] == 0 {
// 0을 두 번 복사합니다.
destination.append(0)
d += 1
destination.append(0)
} else {
destination.append(source[s])
}
d += 1
s += 1
}
문제 설명에는 새 배열을 확장하지 않고 원래 배열의 길이로만 자른다고도 언급되어 있습니다.
이는 배열의 끝에서 몇몇 요소를 버려야 함을 의미합니다.
이러한 요소들은 새로운 인덱스가 원래 배열의 길이를 넘어서는 요소들입니다.
우리에게 주어진 문제 제약 사항에 대해 다시 생각해 봅시다.
추가 공간을 사용할 수 없기 때문에, 우리의 원본 배열과 대상 배열은 본질적으로 동일합니다.
우리는 단순히 원본을 대상 배열로 그대로 복사할 수 없습니다.
그렇게 하면 몇몇 요소를 잃어버릴 것입니다.
왜냐하면, 우리는 배열을 덮어쓰게 될 것이기 때문입니다.
이를 염두에 두고 아래 겁근 방식에서는 배열의 끝 부분에 복사를 시작합니다.
Approach 1: Two pass, O(1) space
Intuition(직관)
만약 우리가 배열의 끝에서 버려질 요소의 수를 안다면, 나머지는 복사할 수 있습니다.
우리는 어떻게 배열의 끝에서 버려질 요소의 수를 알아낼 수 있을까요?
그 수는 배열에 추가될 여분의 0의 수와 같을 것입니다.
여분의 0은 배열의 끝에서 요소 하나를 밀어내면서 자신을 위한 공간을 만듭니다.
일단 우리가 원래 배열에서 최종 배열의 일부가 될 요소의 수를 알게 되면, 우리는 끝에서부터 복사하기 시작할 수 있습니다.
끝에서부터 복사하는 것은, 마지막 몇 개의 불필요한 요소들을 덮어쓸 수 있기 때문에, 어떤 요소도 잃어버리지 않게 해줍니다.
Algorithm
1️⃣. 중복될 제로의 수를 찾습니다. 이를 possible_dups라고 합시다.
최종 배열의 일부가 되지 않을 잘린 제로들을 세지 않도록 주의해야 합니다.
버려진 제로들은 최종 배열의 일부가 되지 않기 때문입니다.
possible_dups의 개수는 원래 배열에서 잘릴 요소의 수를 알려줄 것입니다.
따라서 어느시점에서든, length_ - possible_dups는 최종 배열에 포함될 요소의 수입니다.
참고: 위의 다이어그램에서는 이해를 돕기 위해 원본 배열과 대상 배열을 보여줍니다
우리는 이러한 연산들을 오직 하나의 배열에서만 수행할 것입니다.
2️⃣. 남은 요소들의 경계에 있는 제로에 대한 에지 케이스를 처리합니다.
이 문제의 에지 케이스에 대해 이야기해 봅시다.
남은 배열에서 제로를 복제할 때는 특별한 주의가 필요합니다.
이 주의는 경계에 놓인 Zero에 대해서 취해져야 합니다.
왜냐하면, 이 제로는 가능한 중복으로 간주되거나, 그것의 복제를 수용할 공간이 없을 때 남은 부분에 포함될 수 있기 때문입니다.
만약 그것이 possible_dups의 일부라면 우리는 그것을 복제하고 싶을 것이고, 그렇지 않다면 복제하지 않을 것입니다.
에지 케이스의 예는 - [8,4,5,0,0,0,0,7] 입니다.
이 배열에서 첫 번째와 두 번째로 제로의 중복을 수용할 공간이 있습니다.
하지만 세번째 제로의 중복을 위한 충분한 공간이 없습니다.
따라서 복사할 때 세 번째 제로에 대해선 두 번 복사하지 않도록 주의해야 합니다.
결과 = [8,4,5,0,`0`,0,`0`,0]
3️⃣. 배열의 끝에서부터 순회하여, 0이 아닌 요소는 한 번, 0 요소는 두 번 복사합니다.
우리가 불필요한 요소들을 버린다고 할 때, 이는 단순히 불필요한 요소들의 왼쪽에서 시작하여 새로운 값들로 그것들을 덮어쓰고, 결국 남은 요소들은 오른쪽으로 이동시켜 배열 안에 중복된 요소들을 위한 공간을 만들어낸다는 것을 의미합니다.
class Solution {
func duplicateZeros(_ arr: inout [Int]) {
var possibleDups = 0
let length_ = arr.count - 1
// 복제할 0의 개수를 찾습니다.
// 원래 배열의 마지막 요소를 넘어서면 중지합니다.
// 수정된 배열의 일부가 될 마지막 요소를 넘어서면 중지합니다.
for left in 0...(length_ - possibleDups) {
// 0 숫자 세기
if arr[left] == 0 {
// Edge case: 이 0은 복제할 수 없습니다. 더 이상 공간이 없습니다.
// 왼쪽은 포함될 수 있는 마지막 요소를 가리키고 있습니다.
if left == length_ - possibleDups {
// 이 0의 경우 중복 없이 복사합니다.
arr[length_] = 0
break
}
possibleDups += 1
}
}
// 새 배열의 일부가 될 마지막 요소부터 거꾸로 시작합니다.
var last = length_ - possibleDups
// 0을 두 번 복사하고 0이 아닌 것을 한 번 복사합니다.
while last >= 0 {
if arr[last] == 0 {
arr[last + possibleDups] = 0
possibleDups -= 1
arr[last + possibleDups] = 0
} else {
arr[last + possibleDups] = arr[last]
}
last -= 1
}
}
}
Complexity Analysis
시간 복잡도(Time Complexity): O(N), 여기서 N은 배열의 요소 수입니다. 우리는 배열을 두 번 순회하는데, 하나는 possible_dups의 수를 찾기 위해, 다른 하나는 요소들을 복사하기 위해 사용됩니다. 최악의 경우, 배열에 zero가 적거나 없을 때 배열 전체를 순회할 수도 있습니다.
공간 복잡도(Space Complexity): O(1), 우리는 추가적인 공간을 사용하지 않습니다.
-
📝 배열 삽입 3(배열의 아무 곳에나 삽입하기 - Inserting Anywhere in the Array)
배열 삽입 시리즈
배열 삽입1 (배열의 끝에 삽입하기-Inserting at the End of an Array)
배열 삽입2 (배열의 시작 부분에 삽입하기 - Inserting at the Start of an Array)
마찬가지로, 주어진 인덱스에 삽입하기 위해서는, 해당 인덱스부터 시작하는 모든 요소들을 오른쪽으로 한 자리씩 이동시켜야 합니다.
새 요소를 위한 공간이 생성되면, 삽입을 진행합니다.
생각해보면, 시작 부분에 삽입하는 것은 사실 주어진 인덱스에 요소를 삽입하는 것의 특별한 경우에 해당합니다.
그 경우에 주어진 인덱스는 0이었습니다.
다시 한 번 말씀드리지만, 이것도 비용이 많이 드는 작업입니다.
새 요소를 실제로 삽입하기 전에 거의 모든 다른 요소들을 오른쪽으로 이동시켜야 할 수도 있기 때문입니다.
위에서 보셨듯이, 많은 요소들을 오른쪽으로 한 칸씩 이동시키는 것은 삽입 작업의 시간 복잡도를 증가시킵니다.
다음은 코드의 모습입니다
// 배열 삽입 1,2 코드 참고
var intArray = [Int](repeating: 0, count: 6)
var length = 0
for i in 0..<3 {
intArray[length] = i
length += 1
}
func printArray() {
for i in 0..<intArray.count {
print("Index \(i) contains \(intArray[i])")
}
}
intArray[length] = 10
length += 1
for i in(0...3).reversed() {
intArray[i + 1] = intArray[i]
}
intArray[0] = 20
// 인덱스 2에 요소를 삽입하고 싶다고 가정해봅시다.
// 먼저, 새로운 요소를 위한 공간을 만들어야 합니다.
for i in stride(from: 4, through: 2, by: -1) {
// 각 요소를 오른쪽으로 한 위치씩 이동시킵니다.
intArray[i + 1] = intArray[i]
}
// 이제 새로운 요소를 위한 공간을 만들었으므로,
// 필요한 인덱스에 삽입할 수 있습니다.
intArray[2] = 30
printArray()
다음은 printArray를 실행한 결과입니다.
Index 0 contains 20.
Index 1 contains 0.
Index 2 contains 30.
Index 3 contains 1.
Index 4 contains 2.
Index 5 contains 10.
주의해야 할 주요한 것은 array.capacity가 베열의 전체 용량을 제공한다는 점을 기억하는 것입니다.
마지막으로 사용된 슬롯을 알고 싶다면 count 변수를 사용하여 직접 추적해야합니다.
-
📝 배열 삽입 2(배열의 시작 부분에 삽입하기 - Inserting at the Start of an Array)
배열 삽입 시리즈
배열 삽입1 (배열의 끝에 삽입하기-Inserting at the End of an Array)
배열의 시작 부분에 삽입하기(Inserting at the Start of an Array)
배열의 시작 부분에 요소를 삽입하려면, 새 요소를 위한 공간을 만들기 위해 배열의 다른 모든 요소들을 오른쪽으로 하나의 인덱스만큼 이동시켜야 합니다.
이것은 비용이 매우 많이 드는 작업입니다, 왜냐하면 기존의 요소들을 모두 오른쪽으로 한 단계씩 이동시켜야 하기 때문입니다.
모든 것을 이동시켜야 한다는 것은 이 작업이 상수 시간 작업이 아니라는 것을 의미합니다.
사실, 배열의 시작 부분에 삽입하는 데 걸리는 시간은 배열의 길이에 비례할 것입니다.
시간 복잡도 분석 측면에서 이는 선형 시간 복잡도, 즉 O(N)인데, 여기서 N은 배열의 길이입니다.
다음은 코드의 모습입니다.
// 배열삽입 1 코드 참고
var intArray = [Int](repeating: 0, count: 6)
var length = 0
for i in 0..<3 {
intArray[length] = i
length += 1
}
func printArray() {
for i in 0..<intArray.count {
print("Index \(i) contains \(intArray[i])")
}
}
intArray[length] = 10
length += 1
// 먼저, 새로운 요소를 위한 공간을 만들어야 합니다.
// 이를 위해 각 요소를 오른쪽으로 하나의 인덱스만큼 이동시킵니다.
// 이것은 먼저 인덱스 3의 요소를 이동시키고, 그 다음 2, 그 다음 1, 마지막으로 0을 이동시킵니다.
// 어떤 요소도 덮어쓰지 않기 위해 뒤에서부터 진행해야 합니다.
for i in(0...3).reversed() {
intArray[i + 1] = intArray[i]
}
// 이제 새로운 요소를 위한 공간을 만들었으므로,
// 시작 부분에 삽입할 수 있습니다.
intArray[0] = 20
printArray()
다음은 printArray()를 실행한 결과입니다.
Index 0 contains 20.
Index 1 contains 0.
Index 2 contains 1.
Index 3 contains 2.
Index 4 contains 10.
Index 5 contains 0.
-
-
-
-
-
📝 배열의 용량 vs 배열의 길이
Array Capacity VS Length
만약 누군가가 당신에게 DVD 배열의 길이가 얼마나 되는지 물어본다면, 당신의 대답은 무엇일까요?
당신은 두 가지 다른 대답을 할 수 있습니다.
상자가 가득 차있을 경우, 상자가 담을 수 있는 DVD의 수, 또는
현재 상자에 들어있는 DVD의 수.
이 두 답변은 모두 정확하며, 매우 다른 의미를 가집니다!
이 둘의 차이를 이해하고 올바르게 사용하는 것이 중요합니다.
우리는 첫 번째를 배열의 ‘용량’이라고 부르고, 두번째를 ‘길이’라고 부릅니다.
Array Capacity
DVD[] array = new DVD[6]
array[6]에 요소를 삽입하는 것이 유효한 작업일까요?
array[10]은 어떨까요?
아니요, 이 두 경우 모두 유효하지 않습니다.
배열을 생성할 때, 이 배열이 최대 6 개의 DVD를 담을 수 있다고 지정했습니다.
이것이 배열의 용량입니다.
인덱싱이 0부터 시작한다는 것을 기억한다면, 우리는 오직 array[0], array[1], array[2], array[3], array[4] 그리고 array[5]에만 항목을 삽입할 수 있습니다.
array[-3], array[6], array[100]과 같이 다른 곳에 요소를 넣으려고 하면 ArrayIndexOutOfBoundsExecption으로 코드가 충돌하게 됩니다.
배열의 용량은 배열이 생성될 때 결정되어야 합니다.
용량은 나중에 변경할 수 없습니다.
우리가 사용한 종이 상자에 DVD를 넣는 비유로 돌아가 보면, 배열의 용량을 변경하는 것은 종이 상자를 더 크게 만들려는 것과 같습니다.
고정된 크기의 종이 상자를 더 크게 만드는 것은 비현실적이며, 컴퓨터의 배열에서도 마찬가지입니다!
그렇다면 7번째 DVD를 얻었을 때, 모든 DVD를 같은 배열에 넣고 싶다면 어떻게 할까요?
불행히도 종이 상자의 경우와 마찬가지입니다.
더 큰 상자를 새로 구해서, 기존의 DVD들과 새로운 것을 모두 옮겨야 합니다
자바에서 배열의 용량은 배열의 length 속성값을 확인함으로써 알 수 있습니다.
이는 arr.length라는 코드를 사용하여 확인되는데, 여기서 arr은 배열의 이름입니다.
다른 프로그래밍 언어들은 배열의 길이를 확인하는 데 다른 방법을 사용합니다.
int capacity = array.length;
System.out.println("The Array has a capacity of " + capacity);
이 코드를 실행하면 다음과 같은 출력이 나옵니다:
The Array has a capacity of 6
capacity property of Swift
Instance Property
capacity
배열이 새로운 저장 공간을 할당하지 않고 담을 수 있는 요소의 총 수입니다.
모든 배열은 그 내용을 저장하기 위해 특정 양의 메모리를 예약합니다.
배열에 요소를 추가하고 그 배열이 예약된 용량을 초과하기 시작하면, 배열은 더 큰 메모리 영역을 할당하고 그 요소들을 새로운 저장 공간으로 복사합니다.
새로운 저장 공간은 기존 저장 공간 크기의 배수입니다.
이 지수적 성장 전략은 요소 추가 작업이 평균적으로 상수 시간 내에 이루어지게 하여, 많은 추가 작업의 성능을 평균화합니다.
재할당을 유발하는 추가 작업에는 성능 비용이 들지만, 배열이 커짐에 따라 그런 작업은 점점 덜 자주 발생합니다.
다음 예시는 배열 리터럴로부터 정수 배열을 생성한 다음, 다른 컬렉션의 요소들을 추가합니다.
추가 하기 전에, 배열은 결과 요소들을 저장할 수 있을 만큼 충분히 큰 새로운 저장 공간을 할당합니다.
var numbers = [10, 20, 30, 40, 50]
// numbers.count == 5
// numbers.capacity == 5
numbers.append(contentsOf: stride(from: 60, through: 100, by: 10))
// numbers.count == 10
// numbers.capacity == 10
Array Length
길이(length) 의 또 다른 정의는 배열에 현재 들어 있는 DVD의 수, 또는 다른 항목들의 수입니다
이것은 직접 추적해야 할 것이며, 기존 DVD를 덮어쓰거나 배열에 공백을 남겨두어도 오류는 발생하지 않습니다.
이전 예제에서 length 변수를 사용하여 다음 비어 있는 인덱스를 추적하고 있는 것을 눈치챘을 수 있습니다.
// 용량이 6인 새 배열을 생성합니다.
int[] array = new int[6];
// 현재 길이는 0이며, 요소가 0개 있기 때문입니다.
int length = 0;
// 그 안에 3개의 항목을 추가합니다.
for (int i = 0; i < 3; i++) {
array[i] = i * i;
// 요소를 추가할 때마다 길이가 1씩 증가합니다.
length++;
}
System.out.println("배열의 용량은 " + array.length + "입니다.");
System.out.println("배열의 길이는 " + length + "입니다.");
이 코드를 실행하면 다음과 같은 출력이 나옵니다:
The Array has a capacity of 6
The Array has a length of 3
count property of Swift
Instance Property
count
배열의 요소(elements) 수 입니다.
var count: Int { get }
Handling Array Parameters(배열 매개변수 처리하기)
LeetCode에서의 대부분의 배열 문제는 “길이”나 “용량” 매개변수 없이 매개변수로 배열을 전달합니다.
이게 무슨 뜻일까요?
예를 들어 설명해 보겠습니다.
여기 당신이 풀게 될 첫 번째 문제의 설명이 있습니다.
‘이진 배열이 주어졌을 때, 이 배열에서 연속된 1의 최대 개수를 찾아라.’
그리고 여기 주어진 코드 템플릿이 있습니다.
class Solution {
public int findMaxConsecutiveOnes(int[] nums) {
}
}
유일한 매개변수는 'nums' 인데, 이는 배열입니다. 'nums'의 길이를 모르면 이 문제를 해결할 수 없습니다.
다행히도 이는 간단합니다.
매개변수로 주어진 배열에 대한 추가 정보가 없을때는 길이 == 용량 (length == capacity) 이라고 안전하게 가정할 수 있습니다.
즉, 배열은 그 데이터를 모두 담기에 정확히 적합한 크기입니다.
따라서 .length를 사용할 수 있습니다.
하지만 조심하세요, 배열은 0부터 시작하는 인덱스입니다.
용량(capacity)/길이(length)는 항목의 수이지 최고 인덱스가 아닙니다.
최고 인텍스는 .lenght -1 입니다.
따라서 배열의 모든 항목을 순회하기 위해 다음과 같이 할 수 있습니다.
class Solution {
public int findMaxConsectiveOnes(int[] nums) {
// 힌트: 여기에 변수를 초기화하고 선언하여
// 연속된 1이 몇 개인지 추적합니다.
for (int i = 0; i < nums.length; i++) {
// nums[i] 요소로 무언가를 합니다.
}
}
}
이것이 바로 시작하기 위해 필요한 배열의 기본 사항입니다!
-
📝 스위프트에 왜 변수가 있을까?
변수는 프로그램에서 임시 정보를 저장하는 데 사용되며, 거의 모든 Swift 프로그램의 핵심 부분을 이룹니다.
여러분의 프로그램은 어떤 식으로든 데이터를 변환할 것입니다: 사용자가 할 일 목록 작업을 입력하고 체크하게 하거나, 황량한 섬에서 자본주의적인 너구리를 위해 돌아다니게 하거나, 기기 시간을 읽고 시계에 표시하는 등입니다.
어쨋든, 어떤 종류의 데이터를 받아 어떤 식으로든 변환하고 사용자에게 보여주는 것입니다.
물론, ‘어떤 식으로든 변환하는’ 부분이 진짜 마법이 일어나는 곳입니다, 왜냐하면 그곳이 여러분의 놀라운 아이디어가 실현되는 곳이기 때문입니다.
하지만 데이터를 메모리에 저장하는 과정 - 사용자가 입력한 것이나 인터넷에서 다운로드한 것을 기억하는 것 - 이 곳이 변수가 사용되는 곳 입니다.
var 를 사용하여 변수를 생성하면, var 를 다시 사용하지 않고도 원하는 만큼 변경할 수 있습니다.
예를 들어:
var favoriteSports = "Tennis"
favoriteSports = "MMA"
favoriteSports = "Crossfit"
만약 도움이 된다면, var 를 “새로운 변수 생성(create a new variable)”으로 읽어보세요.
따라서, 위의 첫 번째 줄은 “새로운 변수 favoriteSports 를 생성하고 그것에 Tennis 값을 주세요”로 읽을 수 있습니다.
두 번째와 세 번째 줄에는 var 가 없으므로, 새 변수를 생성하는 것이 아니라 기존 값을 수정합니다.
이제 모든 세 줄에 var 가 있다고 상상해보세요 - 매번 var favoriteSports 를 사용했습니다.
그것은 많은 의미가 없을 것입니다, 왜냐하면 여러분은 “새로운 변수 favoriteSports 를 생성하라”고 세 번 반복하게 되고, 변수는 첫 번째 시도 후에 분명히 새로운 것이 아닙니다.
Swift는 이것을 오류로 표시할 것이고, 여러분이 변수에 다른 이름을 선택할 때까지 코드를 실행하지 못하게 할 것입니다.
그것이 성가신 행동처럼 보일 수 있지만, 신뢰하세요: 그것은 도움이 됩니다!
Swift는 여러분에게 명확해지길 원합니다: 기존 변수를 수정하려고 하고 있다면(그렇다면 두 번째와 그 이후에는 var 를 제거하세요), 아니면 새 변수를 생성하려고 하는 것인가요?(그 경우에는 다른 이름을 지으세요.)
마지막으로: 변수가 많은 Swift 프로그램의 핵심을 이루고 있지만, 때때로 그것들을 피하는 것이 가장 좋다는 것을 배우게 될 것입니다.
이에 대해서는 나중에 더 자세히 학습하겠습니다
-
Touch background to close