서브넷은 서브 네트워크를 말하며, 기존 네트워크 영역을 분할해 더 작은 크기의 네트워크 영역으로 쪼갠 네트워크이다. (말 그대로 서브 네트워크...) AWS에서의 서브넷은 VPC 내에 생성하는데, VPC 가 가진 CIDR 블록(기존 네트워크 영역) 내에서 더 작은 CIDR 블록(더 작은 크기의 네트워크 영역) 로 쪼갠 네트워크 영역을 말한다.
아래와 같이 VPC를 10.0.0.0/16 으로 CIDR 대역을 설정한 후 10.0.10.0/25, 10.0.20.0/25 CIDR 블럭을 갖는서브넷을 각각 생성하였다. 즉, A 서브넷은 10.0.10.0 ... 10.0.10.127 IP 대역을, B 서브넷은 10.0.20.0 ... 10.0.20.127의 IP 대역을 갖게 되는것이다.
퍼블릭 서브넷, 프라이빗 서브넷??
서브넷 유형을 말하며, 이 두 유형은 서브넷을 생성할 때 지정하는게 아니라 서브넷에 할당된 라우팅 테이블에 의해 지정된다. 퍼블릭의 의미는 인터넷 망, 정확히 말하면 인터넷 게이트웨이와 직접 연결되어 있는 것을 말한다.
다시말하면 퍼블릭 서브넷은 라우터에 의해 인터넷 게이트웨이와 직접 연결되는 서브넷이고, 프라이빗 서브넷은 인터넷 게이트웨이와 직접 연결되지 않은 서브넷이다. 라우팅 테이블 설정에 따라 퍼블릿 서브넷이 프라이빗 서브넷이 될수도, 그 반대가 될수도 있다.
인터넷 게이트웨이가 뭔가요?
인터넷 게이트웨이란 VPC와 인터넷 간에 통신할 수 있게 해주는 VPC 구성요소를 말한다. 줄여서 IGW라고 부른다.
라우팅 테이블에 IGW만 추가하면 외부와 통신이 가능한건가요?
IGW에 대한 라우팅 정보를 추가하기만 하면 해당 서브넷 내 리소스들이 인터넷 망으로 나갈 수 있을까? 그렇지 않다. 서브넷 내에 퍼블릭 IP를 가진 리소스가 존재하지 않기 때문이다.
인터넷 게이트웨이를 설정했다면, NAT 게이트웨이를 설정하거나, 서브넷 상의 리소스에 퍼블릭 IP를 할당해야만이 인터넷 게이트웨이를 통해 인터넷에 연결할 수 있다.
예전 AWS 로 인프라를 구성한 적이 있는데, 구성 당시에는 누군가 만들어놓은 메뉴얼을 따라하기만 했지, 각각의 리소스들이 왜 필요한지, AWS 내에서 어떤 역할을 하는지에대한 고민은 하지 않았다. 그래서 이번 기회에 AWS 구조와 용어에 대해 이해하고 싶었고, 가장 기본인 VPC부터 공부하게 되었다.
Amazon VPC가 뭔가요?
아마존(Amazon) 에서제공하는격리된(Private) 가상(Virtual) 클라우드(Cloud) 서비스를말한다. 조금더풀어말하면, 아마존과같은퍼블릭클라우드환경내일부분을고객전용으로프라이핏하게사용할수있는가상네트워크를말한다. 가상 네트워크라는 말이 알쏭달쏭한데, 글을 읽다보면 이해할 수 있을것이다.
"VPC는 리전의 모든 가용 영역에 적용됩니다." ??
Amazon VPC 사용 설명서의 첫 시작 문구이다. 첫 시작부터 발걸음을 돌리고 싶게 만드는 알쏭달쏭한 문구인데, 첫 문구인만큼 이 의미를 이해하는 것이 중요하다고 생각했다. 먼저 리전과 가용 영역에 대해 알아보자.
리전(Region)과 가용영역(Availability Zone)
리전 (Region)
리전이란 전 세계에서 데이터 센터를 클리스터링하는 지리적 위치를 말한다. 여기서 지리적 위치라고 표현한 이유는 실제 물리적인 위치보다 더 넓은 범위를 표현하는 용어이기 때문이다. 누군가 "너 어디살아?" 라고 물었을때 "나 서울시 xx구 xx아파트 203동 101호에 살아" 라고 하지 않고 "나 서울살아" 라고 하는것과 같다고 생각하면 된다.
데이터 센터 용어만 들으면 데이터가 저장된 센터? DB를 말하는건가? 라고 착각이 들 수 있다. 클라우드 컴퓨팅에서의 데이터 센터란 컴퓨팅 시스템 및 하드웨어 장비가 저장된 물리적 위치를 말한다. 즉, 클라우딩 관련 장비가 저장된 센터를 말한다.
위 그림을 보면 알 수 있듯이 클라우드 서비스를 제공하기 위한 데이터 센터의 지리적 위치는 미국 동부, 서부, 서울, 도쿄 등 지역별로 고루 퍼져있다.
가용영역 (Availability Zone)
리전별로 하나의 데이터 센터만을 운용하고 있을까? 아니다. 최소 2개 이상의 데이터 센터를 가져야 리전으로써의 조건이 성립된다. 즉, 서울 리전에는 실제로 2개 이상의 데이터 센터들이 어딘가에서 운용되고 있는 것이다. 그리고 이렇게 퍼져있는 데이터 센터들을 논리적으로 묶어놓은 것을 가용영역이라 한다.
위 그림은 리전과, 가용영역, 그리고 실제 데이터 센터를 도식화 한것이다.
"VPC는 리전의 모든 가용 영역에 적용됩니다." !!
다시 돌아가서 이 문구의 의미는 뭔지 생각해보자. 해석하면 "VPC라는 가상 네트워크는 리전 내에 있는 모든 가용영역에 위치시킬 수 있다는 뜻이고 이 말은 하나의 VPC 내에 생성되는 리소스들을 여러 데이터 센터에 위치시킬 수 있다는 뜻이다."
아래 그림과 같이 말이다. VPC가 가상의 네트워크이기 때문에 여러 가용영역을 공유 사용할 수 있는것이다.
VPC 에는 자체 IP가 없고 IP 대역(CIDR) 을 설정하던데...
VPC를 생성한다고 해서 VPC 자체에 대한 IP가 할당되지 않는다. 말 그대로 "가상의 네트워크"이기 때문이다. 대신 VPC 내에 실제 리소스를 생성할 때에는 해당 리소스에 IP가 할당되는데, 이때 할당되는 IP의 범위, 즉 CIDR를 VPC 생성 시 설정해야 한다. VPC를 생성할 때 IP 프로토콜과 CIDR를 설정하는 이유가 바로 이것이다.
IPv4 프로토콜을 사용할 경우 CIDR 블록 크기 설정 시 RFC1918 규격과 AWS 자체 VPC CIDR 블럭 규칙에 따라 CIDR를 설정해야 한다. 이를 준수하는 CIDR 블록 크기는 아래로 한정된다.
10.0.0.0/16 ~ /28
172.16.0.0/16 ~ /28
192.168.0.0/16 ~ /28
RFC 1918 프라이빗 IP의 국제 규격으로 아래 대역 범위를 갖는다.
달랑 VPC 만 생성해주지 않아요~
VPC를 생성하면 VPC만 생성되는게 아니라 기본 리소스인 기본 DHCP 옵션 세트, 기본 네트워크 ACL, 기본 보안그룹, 기본 라우팅 테이블도 함께 생성된다. 각각에 대해 알아보자.
첫째, 기본 DHCP 옵션 세트
DHCP가 뭔가요?
Dynamic Host Configuration Protocol의 약자로 네트워크에 위치한 컴퓨터 및 기타 장치에 IP 주소와 같은 네트워크 정보를 자동으로 할당하기 위한 프로토콜을 말한다. 네트워크 설정을 DHCP 서버가 중앙 집중식으로 관리하는 클라이언트/서버 모델인 것이다.
이전에는 네트워크에 위치한 컴퓨터 및 기타 장치의 IP 주소를 수동으로 할당했지만, 오늘날에는 DHCP를 사용해 동적으로 할당하고 있다. 생소하게 느껴질 수 있지만 사실 대부분의 컴퓨터 사용자들이 이 프로토콜을 사용하여 네트워크 설정을 자동화했을 것이다. 필자의 맥북또한 마찬가지인데, 네트워크 탭의 IPv4의 구성 방식이 DHCP를 사용하도록 설정되어 있어 IP, DNS서버, 게이트웨이 주소 등을 따로 설정하지 않아도 자동으로 할당되는 것을 확인할 수 있다.
DHCP를 사용하면 뭐가 좋나요?
네트워크 설정을 자동화할 수 있고, 수동 IP 할당 시 발생할 수 있는 IP 충돌 문제를 예방할 수 있다.
DHCP 옵션 세트가 뭔가요?
EC2 인스턴스가 실행될 때 DHCP를 통해 자동으로 네트워크 설정이 되도록 DHCP 서버로 요청하는데, 이 DHCP 관련 설정이 담긴 세트이다. 각 리전마다 각기 다른 기본 DHCP 옵션 세트를 갖고 있다.
DHCP 옵션 세트가 AWS에서 어떻게 쓰이는지 알려주세요
VPC 내 EC2와 같은 리소스가 실행되면 IP, DNS 서버와 같은 네트워크 설정을 위해 Amazon DHCP 서버로 요청하게 된다. 그럼 DHCP 서버는 VPC 내 설정된 DHCP 옵션 세트를 로드하게 되는데, 이 옵션 세트에 따라 IP 주소와 DNS 서버와 같은 네트워크 설정을 해당 리소스에 할당하게 된다.
참고로 리소스에 네트워크 설정이 정상적으로 할당된 경우 해당 리소스는 자신에게 할당된 IP 정보를 자동으로 라우팅 테이블에 등록하게 되는데, 이러한 과정으로 인해 내부 리소스간의 네트워킹이 가능한 것이다.
그래서 기본 DHCP 옵션 세트는 뭐라고요?
기본 DHCP 옵션 세트란 리소스의 네트워킹 설정을 위해 DHCP 서버가 참조하는 기본옵션들을 말한다. VPC 라는 가상 네트워크 환경에 설정한 CIDR 대역에 맞는 IP로 할당되어야 하지 않겠는가? 이러한 외부 정보가 없다면 어떤 IP를 할당해야하는지에 대한 기준이 잡히지 않을 것이다. (이건 필자의 지극히 주관적인 생각입니다.)
둘째, 기본 네트워크 ACL
네트워크 ACL이 뭔가요?
네트워크 ACL이란 Network Access Control List의 약자로 '서브넷 수준'에서 특정 인바운드 또는 아웃바운드 트래픽에 대한 접근 제어 리스트를 말한다. 아래는 서브넷이 2개인 VPC 내에서 네트워크 ACL의 역할을 알려주는 그림이다.
트래픽이 VPC로 들어오면 라우터에서 라우팅 테이블을 확인해 트래픽을 타겟으로 보낸다. 이때 네트워크 ACL로 하여금 해당 트래픽이 서브넷으로 들어가고 나갈 수 있는지를 제어하는 것이다. 네트워크 레벨에서의 방화벽인 셈이다.
그래서 기본 네트워크 ACL은요?
AWS에서 기본으로 제공하는 네트워크 ACL로, VPC 내 서브넷을 생성할 경우 네트워크 ACL을 설정해야 하는데, 설정하지 않을 경우 자동으로 할당되는 네트워크 ACL이다. 기본 네트워크 ACL의 정책은 모든 인바운드 및 아운바운드에 대한 IPv4, IPv6 트래픽을 허용한다.
셋째, 기본 보안그룹
보안그룹이 뭔가요?
보안 그룹은 VPC 내 리소스에 대한 접근을 제어하는 그룹을 말한다. 예를 들어 특정 IP 에 대한 인바운드를 차단하는 보안 그룹을 만들고, 2개의 EC2 인스턴스의 보안 그룹에 이를 적용한다면, 특정 IP가 두 EC2 인스턴스로 접근하지 못하도록 차단한다.
네트워크 ACL과 같은거 아닌가요?
인바운드, 아운바운드에 대한 접근을 제어한다는 점에서 비슷하지만, 적용 레벨이 다르다. 네트워크 ACL은 서브넷 레벨에서, 보안그룹은 리소스 레벨에 적용된다.
그래서 기본 보안그룹은요?
VPC를 생성할 경우 기본으로 제공되는 보안그룹이다. 모든 트래픽에 대해 인바운드와 아웃바운드를 허용하도록 정책이 설정되어 있다
위 그림은 VPC 내 위치한 두 개의 EC2가 기본 보안 그룹을 적용한 상황이다. 기본 보안그룹 설정했으니 모든 포트 및 IP로부터 오는 트래픽을 받을 수 있게 된다. 단, 기본 보안그룹은 인터넷 게이트웨이 또는 NAT 게이트웨이로부터 오는 트래픽을 거부하도록 설정되어 있다. 만약 NAT 게이트웨이나 인터넷 게이트웨이를 사용한다면 커스텀 시큐리티 그룹을 만들어 각 인스턴스에 적용하면 된다.
기본 라우팅 테이블
라우터부터 알고가자
라우터란네트워크간데이터전송을위해최적경로를 선택한 후 네트워크간통신할수있도록도와주는인터넷장비이다.
그렇다면라우터는 어떻게'최적의경로'를 찾아낼 수 있는걸까? 그건바로라우터가가진 '라우팅테이블'을참고했기 때문이다.
라우팅테이블이 뭐에요?
라우팅 테이블이란 네트워크에서목적지주소를 통해 물리적 목적지에 도달하기위한경로들이 저장된 테이블이다.라우터로하여금최적의경로를선택하도록도와주는역할을 한다. 각라우터가가진라우팅테이블은목적지에도달하기 위해거쳐야할다음라우터의정보를가지고있다.
[Document(metadata={'source': '/{경로}/text.txt'}, page_content='hi hello,\n\nis text file\n\n안녕하세요\n\n텍스트 파일입니다.'),
Document(metadata={'source': '/{경로}/Catalog_v2.csv'}, page_content='\n\n\nlevelType\ncode\ncatalogType\nname\ndescription\nsourceLink\n\n\nCATEGORY\nStreet Lighting\nPRODUCT\nStreet Lighting\nCategory code for Street Lighting\nhttp://lighttree.com/Street Lighting\n\n\nCATEGORY\nPedestrian Lighting\nPRODUCT\nPedestrian Lighting\nCategory code for Pedestrian Lighting\nhttp://lighttree.com/Pedestrian Lighting\n\n\nCATEGORY\nTraffic Signal Poles\nPRODUCT\nTraffic Signal Poles\nCategory code for Traffic Signal Poles\nhttp://lighttree.com/Traffic Signal Poles\n\n\nCATEGORY\nControls\nPRODUCT\nControls\nCategory code for Controls\nhttp://lighttree.com/Controls\n\n\nCATEGORY\nDownlights\nPRODUCT\nDownlights\nCategory code for Downlights\nhttp://lighttree.com/Downlights\n\n\nCATEGORY\nRetrofit Downlights\nPRODUCT\nRetrofit Downlights\nCategory code for Retrofit Downlights\nhttp://lighttree.com/Retrofit Downlights\n\n\nCATEGORY\nAmbient\nPRODUCT\nAmbient\nCategory code for Ambient\nhttp://lighttree.com/Ambient\n\n\nCATEGORY\nBulbs\nPRODUCT\nBulbs\nCategory code for Bulbs\nhttp://lighttree.com/Bulbs\n\n\nCATEGORY\nControllers\nPRODUCT\nControllers\nCategory code for
4) HTML Loader
html 파일을 로드한다. 포스팅에 참고하고 있는 LangChain 페이지를 html 파일로 저장 후 로더를 통해 로드를 해봤다. 결과값을 보면 HTML 내에서 '컨텐츠'로 활용할 수 있는 HTML 태그를 제외한 내용들을 문서형식으로 추출하고 있다.
from langchain_community.document_loaders import UnstructuredHTMLLoader
loader = UnstructuredHTMLLoader("/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/LangChain.html")
data = loader.load()
data
5) JSON Loader
json 형식의 파일을 로드한다.
!pip install jq # 필요한 패키지 설치
from langchain_community.document_loaders import JSONLoader
import json
from pathlib import Path
from pprint import pprint
file_path='/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/sample.json'
data = json.loads(Path(file_path).read_text())
pprint(data)
특정 노드의 값을 추출할 수도 있다. 아래는 IOUnitIDs 키에 대한 노드를 추출했다.
from langchain_community.document_loaders import JSONLoader
import json
from pathlib import Path
from pprint import pprint
loader = JSONLoader(
file_path='/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/sample.json',
jq_schema='.IOUnitIDs',
text_content=False)
data = loader.load()
pprint(data)
[Document(
metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/sample.json', 'seq_num': 1}
, page_content='{"cics:ABEND-1": "EXEC CICS ABEND", "cics:ASKTIME-1": "EXEC CICS ASKTIME", "cics:FORMATTIME-1": "EXEC CICS FORMATTIME", "cics:GET CONTAINER-1": "EXEC CICS GET CONTAINER", "cics:LINK-1": "EXEC CICS LINK [LGSTSQ]", "cics:PUT CONTAINER-1": "EXEC CICS PUT CONTAINER", "cics:RETURN-1": "EXEC CICS RETURN", "pgm:PROCEDURE DIVISION-1": "PROCEDURE DIVISION", "sql:CLOSE-1": "EXEC SQL CLOSE [TESTMULTI,Zip_Cursor,CusClaim_Cursor]", "sql:FETCH-1": "EXEC SQL FETCH [Zip_Cursor]", "sql:FETCH-2": "EXEC SQL FETCH [CusClaim_Cursor]", "sql:FETCH_ROWSET_NEXT-1": "EXEC SQL FETCH ROWSET NEXT [TESTMULTI]", "sql:OPEN-1": "EXEC SQL OPEN [TESTMULTI]", "sql:OPEN-2": "EXEC SQL OPEN [Zip_Cursor,CusClaim_Cursor]", "sql:SELECT_INTO-1": "EXEC SQL SELECT INTO [POLICY,ENDOWMENT,MOTOR]", "sql:SELECT_INTO-2": "EXEC SQL SELECT INTO [POLICY,HOUSE]", "sql:SELECT_INTO-3": "EXEC SQL SELECT INTO [CUSTOMER,MOTOR]", "sql:SELECT_INTO-4": "EXEC SQL SELECT INTO [POLICY,COMMERCIAL]", "sql:SELECT_INTO-5": "EXEC SQL SELECT INTO [POLICY,COMMERCIAL]", "sql:SELECT_INTO-6": "EXEC SQL SELECT INTO [POLICY,CLAIM]"}')]
필자의 이력서 PDF 파일을 INPUT으로 넣어본 결과 오타 없이 잘 출력되는 것을 확인했다. 하지만 PDF 내 이미지 파일 형태로 이력서가 박혀있는 경우에는 해당 내용을 읽어오지 못했다.
예를들어 아래는 PDF는 내 이미지 형태로 이력서가 들어있는데, 이 경우 pageContent에 이력서 내용이 포함되지 않는다.
참고로 PDF Loader는 종류도 많고, 각각에 대한 성능의 장단점이 존재한다고 하니 잘 찾아보고 사용하자.
7) WEB Loader
웹사이트를 로드하여 컨텐츠 정보를 추출한다. 아래 갤럭시 링 관련 기사에 대한 웹사이트 컨텐츠를 추출해보았다. 웹페이지 내 HTML이나 이미지는 모두 제거된, 텍스트 데이터들만 추출된 것을 확인할 수 있다.
from langchain_community.document_loaders import WebBaseLoader # 웹페이지 문서를 로드하는 클래스.
# 리뷰 | 드디어 베일 벗은 삼성 ‘갤럭시 링’, 체험 후 느낀 3가지
loader = WebBaseLoader("https://www.ciokorea.com/news/344012")
data = loader.load()
data
이 외에도 LangChain의 Document Loader는 Markdown, Microsoft Office DOCS, XLSX, PPT 파일 등 다양한 형식의 파일에 대한 Loader를 지원하고 있다. 물론, 모두 무료는 아니다. 자세한 내용은 아래 LangChain 공식문서를 참고하여 사용하면 된다.
검사 예외 (Checked Exception)과 런타임 예외(Unchecked Exception), 에러의 개념과 차이에 대해 숙지하고 있지만, 어떤 상황에서 이러한 예외들을 적용할지에 대한 명확한 기준은 잡혀있지 않았다. 필자의 경우 모든 예외를 런타임 예외로 던졌다. 검사 예외를 사용할 경우 동일 트랜잭션 내 예외 발생 시 롤백하지 않는다는 것이 이유였다. 이번 아이템을 통해 예외처리에 대한 보다 명확한 기준을 잡도록 하자.
검사 예외(Checked Exception)는 언제?
호출하는 쪽에서 복구하리라 여겨지는 상황에 사용한다.
위 내용이 검사 예외와 런타임 예외를 구분하는 기본 규칙이다.
검사 예외를 던지면 호출자는 예외에 대한 처리가 강제된다. 여기서의 예외는 '회복 가능한 예외'이다. 본인이 발생시킨 예외에 대해 호출자가 회복이 가능하다고 판단된다면 검사 예외를 던지면 된다.
* 아마 검사 예외에 트랜잭션에 대해 알아본 독자들이라면 검사 예외일 때는 트랜잭션 롤백이 되지 않는다는 것을 알고 있을 것이다. 검사 예외가 발생했고, 로직에서 복구했는데 트랜잭션이 롤백이 된다면, 복구시킨들 무슨 소용이 있으랴... 트랜잭션과 검사 예외에 대한 관계를 생각해보면 '회복 가능한 예외'일때 검사 예외를 던지는 것을 더 쉽게 이해할 수 있을것이다.
비검사 예외(Unchecked Exception) 는 언제?
호출하는 쪽에서 복구가 불가능하리라 여겨지는 상황에 사용한다.
비검사 예외는 회복 불가능한 예외이다. Runtime Exception 과 Error 이 두가지로 구성되는데 이 둘 모두 회복이 불가능하다고 여겨질 때 사용하며, 통상적으로 잡지(catch) 말아야 한다. 자신만의 커스텀 예외로 예외 전환을 하는 목적으로 사용하는것은 상관 없지만, 이를 잡아 '복구'시킬 필요는 없다. 오히려 복구시키면 문제가 된다. (feat. 트랜잭션)
이처럼 복구가 불가능한 것을 책에서는 '프로그래밍 오류'라고 칭한다.
복구 가능 여부는 어떻게 판단하나?
복구할 수 있는지 아닌지는 명확히 구분되지 않는다. 예를들어 시스템 자원이 고갈된 원인이 엄청난 양의 배열을 생성한 것이라면 프로그래밍 오류라고 할 수 있지만, 폭발적인 요청에 의해 일시적으로 자원이 부족하여 발생했다면 시간을 두고 재요청을 하는 방식으로 복구할 수 있다. 결국, 복구 가능하냐, 불가능하냐에 대한 기준. 검사 예외, 비 검사 예외를 사용하는 것에 대한 기준은 오롯이 API 설계자의 판단에 달렸다. 복구가 가능하다고 믿는다면 검사 예외를, 그렇지 않다면 런타임 예외를 사용할 것이다.
정리
복수할 수 있는 상황이라면 예외 검사를, 프로그래밍 오류라면 비검사 예외를 던지자. 확실하지 않다면 비검사 예외를 던지자.
예외를 예외 상황에서만 사용하지 않는 케이스에 대해 알아보고, 어떤 문제를 야기하는지 이해해보자.
예외 상황에서 사용하지 않는 예
MyClass[] range = new MyClass[5];
range[0] = new MyClass();
range[1] = new MyClass();
range[2] = new MyClass();
range[3] = new MyClass();
range[4] = new MyClass();
try{
int i = 0;
while(true){
range[i++].myMethod();
}
}catch (ArrayIndexOutOfBoundsException e){
}
System.out.println("종료");
try catch 문 안에 반복문이 있고, range 배열을 순회하며 myMethod()를 호출하고 있다. (myMethod는 단순 print 문을 호출함) 반복을 하다 range[1++] 에서 배열의 최대 길이를 넘어갈 경우 ArrayIndexOutOfBoundsException 예외가 발생하는데, 이를 예상하여 예외를 잡아 처리하고 있다. 여기서 발생한 예외는 예외 상황이라고 하기엔 민망하기에 catch 문 안에 아무런 처리를 하지 않고있다. 즉, 일반적인 제어의 흐름에 사용한 것이다.
뭐 어찌됐던간에 range 를 순회하여myMethod를 호출하는 것에는 문제가 없다.
개발자의 의도 파악하기
이를 대체할 수 있는 방법은 여러가지가 있겠지만 위 코드를 작성한 개발자의 의도는 뭐였을까? 바로 성능을 높이기 위해서이다. JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료한다. 어쨌든 배열 경계에 도달하면 예외를 발생시켜 종료하기에 검사 코드를 제거한 것이다. 하지만 이는 잘못된 추론이다. 이에 대한 근거를 알아보자.
잘못된 추론
첫째, 예외는 예외 상황에 쓸 용도로 설계되었다.
JVM 입장에서 예외의 쓰임이 잘못된 것이다. 쓰임이 잘못됐다면, JVM이 지원하는 기능들을 사용하지 못할 확률이 높다.
둘째, 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해 없애준다.
이게 JVM 이 지원하는 기능 중 하나이다.
셋째, 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
JVM의 기능을 사용하지 못하게 되었다.
위 코드에 대한 성능 테스트
테스트를 해보면 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다는 것을 알 수 있다.
// 테스트 데이터 셋팅
MyClass[] range = new MyClass[10000];
for(int i =0; i<10000;i++){
range[i] = new MyClass();
}
// 예외 사용
long start = System.currentTimeMillis();
try{
int i = 0;
while(true){
range[i++].myMethod();
}
}catch (ArrayIndexOutOfBoundsException e){
}
System.out.println("걸린 시간 : "+ (System.currentTimeMillis() - start)); // 40~43
// 표준 반복 관용구 사용
start = System.currentTimeMillis();
for(MyClass a : range){
a.myMethod();
}
System.out.println("걸린 시간 : "+ (System.currentTimeMillis() - start)); // 20~22
System.out.println("종료");
일반적인 반복문이 예외처리를 한 반복문보다 2배는 빠르다. 배열을 순회하는 표준 코드를 JVM이 최적화했다는 것을 간접적으로나마 확인할 수 있었다. 성능이 개선될 줄 알았던 예외를 사용한 반복문은 오히려 느리고, 코드를 헷갈리게 (왜 이렇게 코드를 짰지? 뭐가 있나? 라는 생각이 들지 않는가) 하는데 끝나지 않는다. 버그가 발생하고 디버깅이 어려워진다.
디버깅을 어렵게 하는 코드
아래 코드는 try 문 내에서 호출되는 myMethod() 이며, 보면 arr[10] 에 "lastDance"라는 문자열을 입력한다. 길이가 10이니 arr[10] 에 접근하는 부분에서 ArrayIndexOutOfBoundsException 예외가 발생할 것이고, 예외 로그를 확인한 개발자는 이를 수정할것이다.
static class MyClass{
String[] arr = new String[10];
public void myMethod(){
System.out.println("call my method");
arr[10] = "lastDance";
}
}
그런데 실제로는 예외 로그가 발생하지 않는다. 이를 호출한 main 메서드의 catch에 의해 아무런 처리를 하지 않기 때문이다. 개발자는 예외 로그가 찍히지 않으니 문제가 없다고 생각하고 넘어가버리게 된다. 결국 예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안된다.
정리
예외는 예외 상황에서 쓸 의도로 설계되었다. 제어 흐름에서 사용해서는 안된다. 코드를 최적화하려다 오히려 망가뜨릴 수 있다.
JVM의 구성요소 중 하나인 네이티브 메서드의 사용에 대한 내용이다. 아직까지 네이티브 메서드를 직접 사용하는 것에 대한 필요성을 느끼지 못하고 있지만, 직접 사용하게 될 경우 어떤 주의사항이 있는지 알아보자.
2. 네이티브 메서드와 JNI
자바에서의 네이티브 메서드란 C나 C++ 같은 프로그래밍 언어로 작성된 메서드를 말한다. 그렇다면 자바에서 C와 C++ 과 같은 언어로 작성된 코드를 직접 호출할 수 있을까? 아니다. JVM 내에 두 언어 간 중간다리 역할을 하는 인터페이스가 구성되어 있다. 이를 JNI라고 한다.
참고로 JNI 는 JVM 의 구성 요소 중 하나이다. 이에 대해 알고싶다면 아래 게시글을 읽어보면 좋다.
레지스트리는 윈도우 플랫폼에서 사용하는 기능으로, 윈도우 OS의 설정을 담고 있는 DB를 말한다. 즉, OS 설정을 건드릴 때 네이티브 메서드 사용이 하나의 선택지가 될 수 있다.
2) 네이티브 코드로 작성된 기존 라이브러리를 사용하기 위해
3) 성능 개선을 위해
성능에 결정적인 영향을 주는 부분을 네이티브 언어로 작성하면 성능 개선을 가져올 수도 있다.
하지만 성능 개선을 위한 네이티브 메서드 사용은 권장하지 않고 있다. 자바가 발전함에 따라 대부분의 작업에서 다른 플랫폼에 견줄만한 성능을 내고 있기 때문이다. 예를들어 자바 1.1 시절 BigInteger는 C로 작성한 네이티브 메서드를 JNI를 통해 사용했으나 자바 3 에서 업데이트되며 원래의 네이티브 구현보다 더 빨라졌다.
4. 네이티브 메서드의 단점
네이티브 언어는 OS를 직접 건드리기에 안정성을 보장하지 않는다. 잘못 사용할 경우 애플리케이션의 버그를 유발하거나 메모리를 훼손할 수 있다. 가비지 컬렉터가 네이티브 메모리는 자동으로 회수하지 못하고 추적할 수도 없다.
이식성도 낮다. 자바에서 JNI를 사용하지 않으면 네이티브 메서드를 사용하지 못한다. JNI를 구성한다해도 이 과정에서 접착 코드(glue code)를 작성해야 하는데, 이는 복잡한 작업이고 가독성도 떨어진다.
5. 정리
네이티브 메서드가 성능을 개선해주는 일은 많지 않다. 네이티브 코드 안에 숨은 단 하나의 버그가 애플리케이션 전체를 훼손할 수 있으므로 신중히 고민하고 사용해야한다.
실수란 소수부나 지수부가 있는 수를 가리키며, 정수보다 훨씬 더 넓은 표현 범위를 가진다. float, double 타입이 있다.
실수형 타입
메모리의 크기
데이터의 표현 범위
float
4바이트
3.4 * 10의 -38승 ... 3.4 * 10의 38승
double
8바이트
1.7 * 10의 -308승 ... 1.7 * 10의 308승
문자형 타입 (1종류)
컴퓨터는 2진수밖에 인식하지 못하므로 어떤 문자를 어떤 숫자에 대응시키는 약속을 했다. 대표적으로 C언어는 아스키 코드를 사용해 문자를 표현한다. 아스키 코드는 영문 대소문자 및 몇가지 기호를 사용하는 7비트 문자 인코딩 방식이다. 문자 하나를 7비트로 표현하므로 총 128개의 문자를 표현할 수 있다.
자바에서는 유니코드를 사용하여 문자를 표현한다. 문자 하나를 16비트로 표현하므로, 총 65,536 개의 문자를 표현할 수 있기에 각 나라의 모든 언어를 표현할 수 있다.
문자형 타입
메모리의 크기
데이터의 표현 범위
char
2 바이트
0 ... 65,536
논리형 타입 (1종류)
논리형은 참이나 거짓 중 한 가지 값만을 가질 수 있는 타입을 의미한다. boolean 타입이 있다.
boolean 형의 기본값은 false 이며, 1 바이트의 크기를 가진다.
논리형 타입
메모리의 크기
데이터의 표현 범위
boolean
1바이트
true 또는 false
박싱된 기본 타입이란?
자바에서는 앞서 말한 기본 타입에 대응하는 참조 타입이 하나씩 있는데, 이를 박싱된 기본타입이라고 한다.
예를들어 int, double, boolean 에 대응하는 박싱된 기본 타입은 Integer, Double, Boolean이다.
기본 타입과 박싱된 기본 타입의 차이
첫째, 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 식별성이란 속성을 갖는다.
박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다. 메모리의 주소가 다르기 때문이다.
둘째, 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 null을 가질 수 있다.
null 에 대한 체크가 이루어지지 않는다면 NPE가 발생할 수 있다.
셋째, 기본 타입이 박싱된 기본 타입보다 처리 시간, 메모리 사용면에서 효율적이다.
박싱된 기본 타입이 메모리를 더 잡아먹기 때문이다.
이 세가지를 무시하고 생각없이 사용했다가는 문제가 발생할 수 있다. 이를 무시했을 때 발생하는 문제들을 예제로 알아보자.
함수형 인터페이스인 Comparator 의 구현체를 람다식으로 구현한 후, 호출하는 예제이다. i 보다 j 가 클 경우 -1, 같을경우 0, i 가 클 경우 1을 리턴한다. naturalOrder.compare 메서드의 매개변수로 값을 넣어 테스트했을 때는 문제는 발생하지 않는 것처럼 보인다. new Integer(1), new Integer(3)을 넣었을 땐 -1이, 반대로 넣었을땐 1이 리턴되기 때문이다. 그렇다면 위 코드의 문제가 뭘까?
바로 같은 값에 대해서는 0을 리턴하지 않는 점이다.
첫번째 검사 (i<j) 는 잘 동작한다. i, j가 참조하는 오토박싱된 Integer 인스턴스는 비교를 위해 기본 타입 값으로 변환된 후 비교 연산을 한다. 그런데 두 번째 검사인 (i == j) 에서는 '객체 참조'의 식별성을 검사하게 된다. i와 j 가 같은 값을 갖고있다고 할지라도 인스턴스가 다르기에 false 가 출력되고 결국 1을 반환한다. 즉, 박싱된 기본 타입에 == 연산자를 사용하게 될 경우 문제가 발생하는 것이다.
예제 2. 기이하게 동작하는 프로그램 예제
public class UseCase {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("믿을 수 없군!");
}
}
이 프로그램의 실행 결과는 어떨까?
이 프로그램의 결과는 "믿을 수 없군!"을 출력하지 않지만, 그 전에 기이한 결과를 보여준다. i == 42를 검사할 때 NullPointerException을 던지는 것이다. 원인은 (i == 42) 코드에서 null을 참조하고 있는 참조 타입 i와 42라는 기본 타입을 비교하기 위해 i를 언박싱하게 되는데, 이때 null 참조를 언박싱하므로 NPE 예외가 발생한다. 기본 타입과 박싱된 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 언박싱되기 때문이다.
예제 3.느린 반복문 예제
Long sum = 0L;
for(long i = 0; i <= Integer.MAX_VALUE; i++){
sum += 1;
}
이 프로그램은 지역변수 sum을 박싱된 기본 타입으로 선언하여 느려졌다. sum += 1 부분에서 언박싱과 박싱이 반복해서 일어나기 때문이다. 체감될 정도로 성능이 느려진다.
박싱된 기본 타입은 언제 쓰는게 좋을까?
그렇다면 박싱된 기본 타입은 아래와 같은 상황에서 써야한다.
첫째, 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 없기때문이다.
둘째, 타입 매개변수로 사용한다. 타입 매개변수로 기본 타입을 지원하지 않기 때문이다.
셋째, 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.
정리
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 기본 타입을 사용하자. 간단하고 빠르며, 앞서 말했던 언박싱으로 인한 버그, 성능 이슈를 예방할 수 있기 때문이다. 또한 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다. 박싱된 기본 타입을 꼭 사용해야한다면 위와 같은 문제 상황들을 이해하고 적절히 사용해야 한다.
적합한 인터페이스가 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하자. 실제 클래스를 사용해야 할 상황은 '오직' 생성자로 생성할 때 뿐이다. 선언 타입에 대해서는 클래스를 생각하기 전에 '인터페이스'부터 생각하는 습관을 길러야 겠다.
ArrayList<String> list = new ArrayList<String>();
생성자를 생성할 때는 클래스를 사용했고, 타입도 클래스를 사용했다. 이건 좋지 못한 코드이다.
아래와 같이 ArrayList의 인터페이스인 List를 사용하는 것이 좋다.
List<String> list = new ArrayList<String>();
인터페이스는 유연성을 더한다.
인터페이스를 도입했다는 것은 인터페이스를 사용하는 클래스가 구현부를 알지 못한다는 것이다. 즉, 클래스의 구현체 코드를 알지 못한다는 것이다. 클래스가 구현체 코드를 알지 못한다는 건 안좋은거 아닌가? 완전 바보 클래스 아니야? 라고 생각할 수 있지만, 반대로 생각하면 바보에게도 먹히는게 인터페이스인 것이다. 같은 이름의 과자 포장지 안에 내용물을 다르게 넣어도 되는것이다.
만약 위 예제에서 ArrayList 대신 LinkedList로 구현체를 교체하고싶다면, 생성자 부분만 교체하면 된다. 어차피 List<String> 타입 변수는 애초에 구현체 클래스를 알지 못했으니 이러한 변화에 영향을 받지 않게된다.
List<String> list = new LinkedList<String>();
주의할 점
원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하여, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야한다.
예를들어 LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에서 HashSet으로 바꾸면 문제가 될 수 있다. HashSet은 순회 순서를 보장하지 않기 때문이다.
구현타입을 바꾸려는 동기
기존 사용하던 것보다 성능이 좋거나 좋은 기능을 제공할 수 있기 때문이다. 예를들어 HashMap을 참조하던 변수가 EnumMap으로 바꾸면 속도가 빨라지고 순회 순서도 키의 순서와 같아진다. 단, EnumMap은 키가 열거 타입일 때만 사용 할 수 있다.
또한 LinkedHashMap으로 바꾼다면 성능은 비슷하게 유지하면서 순회 순서를 보장하도록 설정할 수 있다.
선언 타입과 구현 타입을 동시에 바꾸면 안되나
프로그램이 컴파일 되지 않을 수도 있다. 클라이언트에서 기존 타입에서만 제공하는 메서드를 사용했거나, 기존 타입을 사용해야 하는 다른 메서드에 그 인스턴스를 넘겼다면, 타입이 바뀌는 순간 컴파일 에러가 발생할 것이다.
적합한 인터페이스가 없다면?
적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다. String이나 Integer 같은 클래스가 이에 해당한다.
인터페이스에는 없는 특별한 기능을 제공하는 클래스를 사용하는 경우도 있다. 예를들어 PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다. 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 한다.
정리하면
타입으로 클래스 타입을 사용하는 것보다 확장성을 고려하여 인터페이스나 가장 덜 구체적인 상위타입의 클래스 타입을 사용하는 것이 좋다. 클래스를 직접 사용하는 경우는 최소화하자.
Git에는 local, remote 리포지토리와 같은 저장소도 있지만, commit 하기 전 상태를 저장해놓는 저장소도 있다. 이 저장소를 Staging Area라고 한다.
Commit 하기 전이요??
Commit 하기 전은 특정 파일을 add 명령어를 사용해 staged 상태로 변경시켰을때를 말한다. 즉, 어떤 파일을 새로 생성하거나, 수정한 후 add 명령어를 사용하면 이를 "staged 상태로 변경했다" 라고 말한다.
staged 상태가 있다면 다른 상태도 있나요?
그렇다 Staged 말고도 Untracked, Unmodified, Modified 상태도 존재한다. 이 상태들은 크게 두 가지로 구분할 수 있다. 깃이 관리하는 Tracked 상태(관리대상), 깃이 관리하지 않는 Untracked 상태(비관리 상태)이다. Tracked 상태에 Unmodified, Modified, Staged 상태가 포함된다. 아래 이미지를 보며 이해해보자.
1. Untracked 상태
깃이 관리하지 않는, 즉, 파일의 변경에 대해 추적하지 않는 상태이다. 이러한 상태를 갖는 파일들은 어떤 파일일까? 새로 생성한 파일의 경우 COMMIT 하기 전 특정 부분을 수정한다 한들 수정된 라인을 알 수 없다. 아직 깃에 의해 관리되지 않기 때문이다. 깃에 의해 관리되다가 삭제된 파일도 마찬가지이다. 이러한 상태를 Untracked 상태라고 한다.
새로 생성한 파일을 add 하여 Staged 상태를 만들고, 이를 Commit 한다면, 이는 깃에 의해 관리(Tracked)되는Unmodified 상태가 된다.
2. Unmodified 상태
수정되지 않은 상태이다. 파일을 수정하지 않거나, 수정, 생성했던 파일을 commit 한 직후에 해당하는 상태이다.
3. Modified 상태
수정된 상태이다. 단, 깃에 의해 관리되는 Staged 상태의 파일에 대해 수정을 했을 때이며, Untracked 상태의 파일을 수정했을 때는 Modified 상태라고 하지 않는다.
4. Staged 상태
Commit 시 저장소에 기록될 준비를 마친 상태이다. 이를 "stage에 올라갔다" 라고도 표현한다.
사실 어떤 파일을 생성하거나 수정한 후 바로 Commit을 할 수도 있지만, 개발을 하면서 Commit 하고자 하는 파일들을 넣고 빼나가면서 '이번 Commit에는 최종적으로 어떤 파일들이 추가되어야 하는지'를 고민하면서 하나의 Commit 에도 공을 들이시는 경우도 있다. (참고로 필자는 아니다) 이 때 특정 파일들을 Stage 에 저장하며 관리한다면, Commit 예정인 파일들을 관리할 수 있다.
둘째, 충돌을 해결할 때 사용된다.
merge 시 충돌이 발생할 경우 이를 해결해야 한다. 만약 충돌의 범위가 거대하여 많은 시간이 소요된다면 중간 세이브 파일처럼 충돌을 해소한 파일을 디스크 어딘가에 저장해놓는 것이 안전하다. 이처럼 깃에서는 충돌을 해결한 부분에 대해 add 할 경우 해당 파일이 Stage 에 올라가 디스크에 저장된다.