반응형

개요

 RAG 프로세스 중 외부 데이터를 LOAD하는 단계에서 쓰이는 LangChain의 Document Loader에 대해 알아보자.

 


Document Loader의 종류

Document Loader는 다양한 소스의 데이터를 문서의 데이터로 로드하는 클래스이다. txt 파일, 웹페이지, 유튜브 비디오 스크립트, PPT, CSV 등 등 다양한 데이터를 로드할 수 있다.

 

1) Text Loader

텍스트 파일을 읽어온다. 

from langchain_community.document_loaders import TextLoader

loader = TextLoader("/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/text.txt")
loader.load()

 

[Document(metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/text.txt'}, page_content='hi hello,\nis text file\n\n안녕하세요\n텍스트 파일입니다.')]

 

 

2) CSV Loader

CSV 파일을 로드한다. CSV 파일은 IBM에서 제공하는 샘플 CSV 파일을 사용했다. 로우별로 page_content가 분리되고, \n 문자를 구분자로 컬럼에 대한 데이터들이 로더되는 것을 확인할 수 있다.

샘플 CSV 파일

from langchain_community.document_loaders.csv_loader import CSVLoader

loader = CSVLoader(file_path = "/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/Catalog_v2.csv")
loader.load()

 

[Document(metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/Catalog_v2.csv', 'row': 0}, page_content='\ufefflevelType: CATEGORY\ncode: Street Lighting\ncatalogType: PRODUCT\nname: Street Lighting\ndescription: Category code for Street Lighting\nsourceLink: http://lighttree.com/Street Lighting'),
 Document(metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/Catalog_v2.csv', 'row': 1}, page_content='\ufefflevelType: CATEGORY\ncode: Pedestrian Lighting\ncatalogType: PRODUCT\nname: Pedestrian Lighting\ndescription: Category code for Pedestrian Lighting\nsourceLink: http://lighttree.com/Pedestrian Lighting'),
 Document(metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/Catalog_v2.csv', 'row': 2}, page_content='\ufefflevelType: CATEGORY\ncode: Traffic Signal Poles\ncatalogType: PRODUCT\nname: Traffic Signal Poles\ndescription: Category code for Traffic Signal Poles\nsourceLink: http://lighttree.com/Traffic Signal Poles'),
 Document(metadata={'source': '/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/Catalog_v2.csv', 'row': 3}, page_content='\ufefflevelType: CATEGORY\ncode: Controls\ncatalogType: PRODUCT\nname: Controls\ndescription: Category code for Controls\nsourceLink: http://lighttree.com/Controls'),
 ...

 

 

3) Directory Loader

 디렉토리 내 파일들을 로드한다.현재 path 경로에 txt와 csv 파일이 있는데, 이 두가지 파일을 모두 로더하는 것을 확인할 수 있다.

from langchain_community.document_loaders import DirectoryLoader

loader = DirectoryLoader(path = "/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/", glob='*.*')
loader.load()

 

[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 태그를 제외한 내용들을 문서형식으로 추출하고 있다.

 

LangChain.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

 

HTML 태그가 없는 형태

 

5) JSON Loader

json 형식의 파일을 로드한다.

 

sample.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]"}')]

 

 

6) PDF Loader

PDF를 로드한다. 페이지별 배열 형태로 로드되며 메타 데이터로 페이지 번호를 제공한다. 

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("/content/drive/MyDrive/Colab Notebooks/langChain_document_Loader_study_file/sample.pdf")
pages = loader.load_and_split()
pages

 

필자의 이력서 PDF 파일을 INPUT으로 넣어본 결과 오타 없이 잘 출력되는 것을 확인했다. 하지만 PDF 내 이미지 파일 형태로 이력서가 박혀있는 경우에는 해당 내용을 읽어오지 못했다. 

 

예를들어 아래는 PDF는 내 이미지 형태로 이력서가 들어있는데, 이 경우 pageContent에 이력서 내용이 포함되지 않는다.

한국산업인력동단 제공 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

 

 

결과 #1

 

결과 #2

 

 

 

 

이 외에도 LangChain의 Document Loader는 Markdown, Microsoft Office DOCS, XLSX, PPT 파일 등 다양한 형식의 파일에 대한 Loader를 지원하고 있다. 물론, 모두 무료는 아니다. 자세한 내용은 아래 LangChain 공식문서를 참고하여 사용하면 된다.

 


출처

LangChain 공식문서 https://python.langchain.com/v0.1/docs/modules/data_connection/document_loaders/file_directory/

 

File Directory | 🦜️🔗 LangChain

This covers how to load all documents in a directory.

python.langchain.com

 

RAG 위키독스 https://wikidocs.net/231645

 

2-2-3. 디렉토리 폴더 (DirectoryLoader)

### DirectoryLoader 이용하여 특정 폴더의 모든 파일을 가져오기 `DirectoryLoader`를 사용하여 디렉토리 내의 모든 문서를 로드할 수 있습니다. `Di…

wikidocs.net

 

반응형
반응형

개요

 검사 예외 (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에 의해 아무런 처리를 하지 않기 때문이다. 개발자는 예외 로그가 찍히지 않으니 문제가 없다고 생각하고 넘어가버리게 된다. 결국 예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안된다.

오류 없이 실행된 것 같은(?) 로그

 

 


정리

예외는 예외 상황에서 쓸 의도로 설계되었다. 제어 흐름에서 사용해서는 안된다. 코드를 최적화하려다 오히려 망가뜨릴 수 있다.

반응형
반응형

1. 개요

 JVM의 구성요소 중 하나인 네이티브 메서드의 사용에 대한 내용이다. 아직까지 네이티브 메서드를 직접 사용하는 것에 대한 필요성을 느끼지 못하고 있지만, 직접 사용하게 될 경우 어떤 주의사항이 있는지 알아보자.

 


2. 네이티브 메서드와 JNI

 자바에서의 네이티브 메서드란 C나 C++ 같은 프로그래밍 언어로 작성된 메서드를 말한다. 그렇다면 자바에서 C와 C++ 과 같은 언어로 작성된 코드를 직접 호출할 수 있을까? 아니다. JVM 내에 두 언어 간 중간다리 역할을 하는 인터페이스가 구성되어 있다. 이를 JNI라고 한다.

 참고로 JNI 는 JVM 의 구성 요소 중 하나이다. 이에 대해 알고싶다면 아래 게시글을 읽어보면 좋다.

https://tlatmsrud.tistory.com/148#google_vignette

 

[Java] JVM, JDK, JRE / 차이 / JVM 구조

JVM이 뭐야? JVM(Java Virtual Machine)은 바이트코드(.class)를 OS에 특화된 코드(기계어)로 변환하고, 이를 실행하는 '가상의 머신'이다. 특정 OS에 특화된 코드로 변환하기때문에 OS 종속적이다. JVM은 JRE에

tlatmsrud.tistory.com

 


3. 네이티브 메서드의 사용 목적

 

1) 레지스트리와 같은 플랫폼 특화 기능을 사용하기 위해

 레지스트리는 윈도우 플랫폼에서 사용하는 기능으로, 윈도우 OS의 설정을 담고 있는 DB를 말한다. 즉, OS 설정을 건드릴 때 네이티브 메서드 사용이 하나의 선택지가 될 수 있다.

 

2) 네이티브 코드로 작성된 기존 라이브러리를 사용하기 위해

 

3) 성능 개선을 위해

 성능에 결정적인 영향을 주는 부분을 네이티브 언어로 작성하면 성능 개선을 가져올 수도 있다.

 

 하지만 성능 개선을 위한 네이티브 메서드 사용은 권장하지 않고 있다. 자바가 발전함에 따라 대부분의 작업에서 다른 플랫폼에 견줄만한 성능을 내고 있기 때문이다. 예를들어 자바 1.1 시절 BigInteger는 C로 작성한 네이티브 메서드를 JNI를 통해 사용했으나 자바 3 에서 업데이트되며 원래의 네이티브 구현보다 더 빨라졌다. 

 


4. 네이티브 메서드의 단점

 네이티브 언어는 OS를 직접 건드리기에 안정성을 보장하지 않는다. 잘못 사용할 경우 애플리케이션의 버그를 유발하거나 메모리를 훼손할 수 있다. 가비지 컬렉터가 네이티브 메모리는 자동으로 회수하지 못하고 추적할 수도 없다.

 이식성도 낮다. 자바에서 JNI를 사용하지 않으면 네이티브 메서드를 사용하지 못한다. JNI를 구성한다해도 이 과정에서 접착 코드(glue code)를 작성해야 하는데, 이는 복잡한 작업이고 가독성도 떨어진다.

 


5. 정리

 네이티브 메서드가 성능을 개선해주는 일은 많지 않다. 네이티브 코드 안에 숨은 단 하나의 버그가 애플리케이션 전체를 훼손할 수 있으므로 신중히 고민하고 사용해야한다.

반응형
반응형

개요

박싱된 기본 타입과 기본타입의 차이를 알아보고, 왜 후자의 사용을 지향하는지 알아보자.

 


기본 타입이란?

자바에서의 타입은 '데이터 타입'을 말한다. 이는 해당 데이터가 메모리에 어떻게 저장되고, 어떻게 처리되어야 하는지를 명시적으로 알려주는 역할을 한다.

자바에서는 여러 형태의 '타입'을 미리 정의하여 제공하는데, 이것을 자바의 기본타입(==Primitive type) 이라고 한다. 참고로 기본 타입 외에도 String, List 와 같은 참조 타입이 있다.

 기본 타입은 8 종류가 있으며, 크게는 정수형, 실수형, 문자형, 논리형 타입으로 나뉜다.

 


정수형 타입 (4종류)

정수란 부호를 가지고 있으며, 소수 부분이 없는 수를 의미한다. int, long, short, byte 타입이 있다.

정수형 타입 메모리의 크기 데이터의 표현 범위
byte 1바이트 -128 .. 127
short 2바이트 -32,768 ~ 32,767
int 4바이트 -2,147,483,648 ~ 2,147,483,647
long 8바이트 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

 

실수형 타입 (2종류)

실수란 소수부나 지수부가 있는 수를 가리키며, 정수보다 훨씬 더 넓은 표현 범위를 가진다. 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가 발생할 수 있다.

 

셋째, 기본 타입이 박싱된 기본 타입보다 처리 시간, 메모리 사용면에서 효율적이다.

박싱된 기본 타입이 메모리를 더 잡아먹기 때문이다.

 

이 세가지를 무시하고 생각없이 사용했다가는 문제가 발생할 수 있다. 이를 무시했을 때 발생하는 문제들을 예제로 알아보자.

 


예제 1. 오름차순 정렬

Comparator<Integer> naturalOrder = (i, j) -> (i<j) ? -1 : (i == j ? 0 : 1);
System.out.println(naturalOrder.compare(new Integer(1),new Integer(3)));

 

함수형 인터페이스인 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 메서드를 제공한다. 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 한다.

 

정리하면

타입으로 클래스 타입을 사용하는 것보다 확장성을 고려하여 인터페이스나 가장 덜 구체적인 상위타입의 클래스 타입을 사용하는 것이 좋다. 클래스를 직접 사용하는 경우는 최소화하자.

반응형
반응형

개요

 이번 아이템은 라이브러리를 무작정 사용하지 말고, 사용법을 익힌 후에 사용하라는 것인 줄 알았는데, 알고보니 어떤 기능을 구현함에 있어 대체 가능한 라이브러리가 있다면 직접 구현하지 말고 이를 익히고 사용하라는 것이었다.

 

라이브러리를 사용했을 때의 이점

 

1. 코드를 작성한 전문가의 지식과 선배 프로그래머들의 경험을 활용할 수 있다.

 - 라이브러리 자체가 지식과 경험의 집약체인 것이다.

 

2. 비지니스 로직, 핵심 로직에 시간을 집중할 수 있다.

 - 핵심적인 일과 크게 관련없는 문제들은 라이브러리가 대체할 수 있기 때문이다.

 

3. 노력하지 않아도 성능이 개선된다.

 - 라이브러리 사용자가 많다면, 이를 만든 제작자들은 성능 개선을 위해 노력할 수 밖에 없다. 때론 성능이 극적으로 개선되기도 한다.

 

4. 기능이 점점 많아진다.

 - 실제로 커뮤니티에서 얘기가 나오고 논의된 후 다음 릴리스에 기능이 추가되기도 한단다.

 

5. 낯익은 코드가 된다.

 - 범용적으로 사용하는 라이브러리가 우리의 코드에 있다면, 다른 개발자도 읽기 쉽고, 유지보수하기 쉽다.

 

하지만 실상은 직접구현이 많다

대체할 수 있는 표준 라이브러리가 있음에도 직접 구현하는 개발자들이 많다고 한다. 이유는 대체할 수 있는 라이브러리가 있다는 사실을 모르기 때문이다.

메이저 릴리즈마다 주요 기능이 라이브러리에 추가되고 공시된다. 때문에 홈페이지에 직접 들어가 한번씩 읽어보는 것이 좋다.

 

지정한 URL 파싱

예를들어 지정한 URL의 내용을 가져오는 기능을 직접 구현하면 복잡하지만, 자바 9버전에 추가된 InputStream.transferTo 메서드를 사용하면 쉽게 구현할 수 있다.

 

public static void main(String[] args) throws IOException {
    try(InputStream in = new URL("https://www.naver.com/").openStream()){
        in.transferTo(System.out); // 콘솔에 파싱데이터가 출력됨.
    }
}

 

자바 프로그래머라면 적어도...

라이브러리가 너무 방대하여 모든 API 문서를 공부하기는 벅차겠지만, 이 책의 저자는 자바 프로그래머라면 적어도 java.lang, java.util. java.io 패키지들에는 익숙해져야 한다고 말하고 있다.

추가로 컬렉션 프레임워크나 스트림 라이브러리의 경우 유용하게 사용되고 동시성과 관련있는 부분도 있어 알아두는 것을 추천하고 있다.

 

정리

어떤 기능을 개발하기 전 라이브러리가 존재하는지 먼저 살펴보는 습관을 기르자. 그게 프로젝트만의 기능이 아니라면, 누군가는 똑같은 고민을 했을거고, 이를 구현했을 확률이 높다. 누군가 구현했다면 감사히 쓰면된다.

반응형
반응형

개요

 Git에는 local, remote 리포지토리와 같은 저장소도 있지만, commit 하기 전 상태를 저장해놓는 저장소도 있다. 이 저장소를 Staging Area라고 한다.

 

Commit 하기 전이요??

Commit 하기 전은 특정 파일을 add 명령어를 사용해 staged 상태로 변경시켰을때를 말한다. 즉, 어떤 파일을 새로 생성하거나, 수정한 후 add 명령어를 사용하면 이를 "staged 상태로 변경했다" 라고 말한다.

 

staged 상태가 있다면 다른 상태도 있나요?

그렇다 Staged 말고도 Untracked, Unmodified, Modified 상태도 존재한다. 이 상태들은 크게 두 가지로 구분할 수 있다. 깃이 관리하는 Tracked 상태(관리대상), 깃이 관리하지 않는 Untracked 상태(비관리 상태)이다. Tracked 상태에 Unmodified, Modified, Staged 상태가 포함된다. 아래 이미지를 보며 이해해보자.

 

출처 : https://velog.io/@soyi47/GitGithub-staging-commit

 

 

1. Untracked 상태

 깃이 관리하지 않는, 즉, 파일의 변경에 대해 추적하지 않는 상태이다. 이러한 상태를 갖는 파일들은 어떤 파일일까? 새로 생성한 파일의 경우 COMMIT 하기 전 특정 부분을 수정한다 한들 수정된 라인을 알 수 없다. 아직 깃에 의해 관리되지 않기 때문이다. 깃에 의해 관리되다가 삭제된 파일도 마찬가지이다. 이러한 상태를 Untracked 상태라고 한다.

 새로 생성한 파일을 add 하여 Staged 상태를 만들고, 이를 Commit 한다면, 이는 깃에 의해 관리(Tracked)되는 Unmodified 상태가 된다.

 

2. Unmodified 상태

 수정되지 않은 상태이다. 파일을 수정하지 않거나, 수정, 생성했던 파일을 commit 한 직후에 해당하는 상태이다.

 

3. Modified 상태

 수정된 상태이다. 단, 깃에 의해 관리되는 Staged 상태의 파일에 대해 수정을 했을 때이며, Untracked 상태의 파일을 수정했을 때는 Modified 상태라고 하지 않는다.

 

4. Staged 상태

  Commit 시 저장소에 기록될 준비를 마친 상태이다. 이를 "stage에 올라갔다" 라고도 표현한다.

 

Stage 에 올라간 파일들은 어디에 저장되나요?

.git/index 경로에 Stage에 올라간 파일들이 저장된다.

 

Stage 상태가 필요한가요? Commit 까지의 과정을 번거롭게 하는 것 같아요

이를 설명해주는 좋은 글이 있어 공유한다.

https://blog.npcode.com/2012/10/23/git%EC%9D%98-staging-area%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%A0%90%EC%9D%B4-%EC%9C%A0%EC%9A%A9%ED%95%9C%EA%B0%80/

 

Git의 Staging Area는 어떤 점이 유용한가

Git에는 Staging Area라는 공간이 있다. 어떤 변경사항이 저장소에 커밋되기 전에, 반드시 거쳐야만 하는 중간단계이다. 다른 버전관리도구에는 이에 정확히 대응하는 것은 없다. 저장소가 추적하는

blog.npcode.com

 

이해한 내용을 정리하면 아래와 같다.

 

첫째, Commit 예정인 파일들을 관리할 수 있다.

사실 어떤 파일을 생성하거나 수정한 후 바로 Commit을 할 수도 있지만, 개발을 하면서 Commit 하고자 하는 파일들을 넣고 빼나가면서 '이번 Commit에는 최종적으로 어떤 파일들이 추가되어야 하는지'를 고민하면서 하나의 Commit 에도 공을 들이시는 경우도 있다. (참고로 필자는 아니다) 이 때 특정 파일들을 Stage 에 저장하며 관리한다면, Commit 예정인 파일들을 관리할 수 있다.

 

둘째, 충돌을 해결할 때 사용된다.

merge 시 충돌이 발생할 경우 이를 해결해야 한다. 만약 충돌의 범위가 거대하여 많은 시간이 소요된다면 중간 세이브 파일처럼 충돌을 해소한 파일을 디스크 어딘가에 저장해놓는 것이 안전하다. 이처럼 깃에서는 충돌을 해결한 부분에 대해 add 할 경우 해당 파일이 Stage 에 올라가 디스크에 저장된다.

 

참고

https://blog.npcode.com/2012/10/23/git%EC%9D%98-staging-area%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%A0%90%EC%9D%B4-%EC%9C%A0%EC%9A%A9%ED%95%9C%EA%B0%80/

https://velog.io/@soyi47/GitGithub-staging-commit

반응형
반응형

개요

코드 분석중...

 

 누군가 작성한 코드를 분석하기 위해 100줄 남짓한 메서드를 보고있다. 최상위에 지역변수들이 초기화되어 있고, 아래로 비지니스 로직이 주욱 구현되어 있다. 코드를 분석해나가는 도중 어떤 지역변수가 사용되었지만, 이 값이 어떤 값을 갖고있는지를 잊어버려 최상위에 적힌 지역변수를 다시 확인했다. 어떤 변수는 반복문에서도, try 문 안에서도, 다른 변수에 값을 할당하는 부분에도 사용됐다. 변수의 유효 범위가 너무 넓은 것이다. 변수를 체크하다보니 로직의 흐름을 까먹어 다시 분석하기도 한다. 이런 과정을 반복하면서 코드 분석을 마무리했다.

 


지역변수가 최상위에 선언되어 있지 않았다면?

 지역변수들이 최상위에 선언되어 있지 않고, 쓰일 때 초기화되어 있거나, 현재 보이는 코드라인에 초기화 된 값이 보인다면 어땠을까? 다시 확인할 필요도 없고, 코드 분석에 대한 집중력도 유지할 수 있다. 즉, 지역변수의 범위를 최소화한다면 유지보수성과 가독성을 향상시킬 수 있다.

 


지역변수의 범위를 최소화하는 방법

 가장 강력한 방법은 '가장 처음 쓰일 때 선언하는 방법'이다. 앞서 개요에서 말했던 방법이다. 사용하려면 멀었는데 미리 선언부터 해두면 이를 다시 확인하거나 잘못 사용하게 되는 상황이 발생할 수 있다. 사실 거의 모든 지역변수는 선언과 동시에 초기화해야한다. 여기서 거의라고 말한 이유는 그렇지 않은 예외 케이스가 존재하기 때문인데 바로 try-catch 문이다. 예외 처리를 해야하는 경우 선언은 try 문 밖에서, 초기화는 try 문 안에서 해야 catch나 finally 에서 이에 맞게 핸들링을 하거나 리소스를 제거하는 행위를 할 수 있다.

 

아래와 같이 FileInputStream 과 같은 타입의 값을 초기화할 때 예외처리를 하지 않을 경우 아래와 같이 컴파일 타임에 에러가 발생한다. 이를 처리하기 위해 예외를 외부로 던질수도 있지만, 이는 예외에 대한 책임을 전가하기에 메서드 내에서 처리를 하려는 사람도 있을 것이다.

예외 처리를 하지 않아 컴파일 에러 발생

 

 

그 경우 inputStream을 try 외부에서 먼저 선언하고 내부에서 초기화하게 된다.

public void myMethod(){
    File file = new File("");

    FileInputStream inputStream = null;
            
    try{
        inputStream = new FileInputStream(file);
    }catch (FileNotFoundException e){
        e.printStackTrace();
    }finally {
        try{
            inputStream.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

 

물론 InputStream은 autoClose 가 구현되어 있으니 try-resource를 사용한다면 try 괄호 안에서 초기화가 되겠지만, 그게 아닐 경우 위처럼 선언과 동시에 초기화되지 않을 수도 있다. 어쨌든 중요한건 대부분의 지역변수는 사용할때, 선언과 동시에 초기화해야 한다는 것이다.

 


반복문의 반복변수

 지역변수는 반복문에서도 사용된다. 바로 '반복변수'이다. 일반적으로 for나 while 과 같은 반복문에서 사용하는데 만약 반복 변수의 값을 반복문이 종료된 뒤에도 써야 하는 상황이 아니라면 while 보다 for 문을 쓰는 게 낫다.

 

예를들어 컬렉션을 순회하는 코드를 작성할 경우 for문은 아래와 같이 for-each나 전통적인 for문을 사용해서 구현할 수 있다.

List<String> list = new ArrayList<>();
//...


// for-each
for(String e : list){
    //...
}

// 전통 for
for(Iterator<String> i = list.iterator(); i.hasNext();){
	//...
}

 

 

while의 경우는 아래와 같이 구현되는데, while 구문 안에 조건을 넣어야하므로 while 구문 전에 반복 변수를 초기화를 해야한다. 그런데 아래와 같이 while 반복문이 두번 사용될 경우 복붙 과정에서 실수로 조건부의 i를 i2로 수정하지 않아 버그가 발생할 수 있다. 이 경우 두번째 반복문이 실행되지 않을 것이다.

Iterator<String> i = list.iterator();
while(i.hasNext()){
    //...
}

Iterator<String> i2 = list.iterator();
while(i.hasNext()){ // 버그유발
    //...
}

 

 

for문의 경우는 어떨까? 반복 변수의 유효범위가 for문 안으로 한정되어 있기 때문에 위와 같은 버그가 발생하지 않는다. 심지어 변수명을 다르게 설정할 필요도 없다.

for(Iterator<String> i = list.iterator(); i.hasNext();){
    //...
}
for(Iterator<String> i = list.iterator(); i.hasNext();){
    //...
}

 

 

여기서 키포인트는 초기화를 내부에서 했냐, 외부에서 했냐가 아니다. while에 사용되는 반복변수는 for문에 사용되는 반복변수보다 유효 범위가 훨씬 넓다는 것이다. while의 반복변수는 메서드 전체인데 반해, for문의 반복변수는 for문 안으로 한정되어 있다. 즉, 지역변수의 유효범위가 넓을수록 버그를 발생시킬 확률이 높고, 가독성과 유지보수성을 헤친다는 것을 말하고 있다.

 


정리

 지역변수의 범위를 최소화하는 목적은 가독성과 유지보수성을 높이는 것이다. 이를 위해 지역변수를 선언과 동시에 초기화하거나, 실제로 쓰일 때 초기화 하거나, while 보다는 for문을 쓴것인데, 만약 이를 다 지킨다고 하더라도, 코드가 길면 어떨까? 긴 코드 안에는 여러 책임들이 얽혀있고, 여러 기능들을 하고, 여러 예외들을 처리한다면 지역변수의 범위를 최소화한다 한들 가독성과 유지보수성을 높인다는 목적을 이루지 못한다.

 

때문에 지역변수의 범위를 최소화하기에 앞서 선행되어야 할 가장 중요한 것은 단일 책임원칙에 맞게 코드를 구현하여 메서드를 작게 만드는 것이다. 이게 선행됐을 때 비로소 지역변수의 범위를 최소화하는 것이 의미있는 행위가 될것이다.

반응형
반응형

출처 : 위키피디아

 

1. 개요 

 

 앞선 포스팅에서는 Git Merge 에 대해 알아봤다. 코드 충돌이 나는 원인과 과정을 이해하고, Fast Forward Merge , 3-Way-Merge 가 어떤 상황에 발생하는지도 알았다. 다음은 병합과 연관되는 또 다른 명령어인 Rebase에 대해 알아보자. Rebase and Merge 전략이 있을 만큼 연관성있는 명령어이다.

 


2. Rebase 가 뭔가요? 🤔

Re-Base

 

말 그대로 브랜치의 Base Commit(= Base) 를 재설정 (= Re) 하는 명령어이다.

A 브랜치에서 B 브랜치에 대한 Rebase를 할 경우 A 브랜치의 Base Commit이 B 브랜치의 Head Commit으로 변경된다.

 


3. 재설정하면 뭐가 좋은데요? 🤔

 Fast Forward Merge를 통해 깔끔한 커밋 히스토리를 유지할 수 있다. 예를들어 Main 브랜치에서 A 라는 브랜치를 병합하기 전에, A 브랜치가 Main 브랜치에 Rebase 작업을 한다면 어떨까?

A 브랜치의 Base Commit이 Main 브랜치의 Head Commit으로 Re-base 될것이다.

 이후 Main 브랜치에서 A 브랜치를 병합하면 어떻게 될까?

Fast Forward Merge가 되어 깃 히스토리가 직렬로 되어 깔끔하게 관리할 수 있다.

 


4. 자세한 예를들어 설명해주세요. 😖

 

아래의 깃 히스토리를 보자.

개발자는 D 기능 개발을 위해 Main 브랜치로부터 D 브랜치를 생성하고, 작업을 하며 D-C1, D-C2 Commit을 한 상황이다. 이때 D 브랜치의 Base Commit은 M-C2이다.

 

Main 브랜치로부터 D 브랜치를 생성한 상황

 

 

그리고 다른 개발자는 E 기능을 개발하기 위해 마찬가지로 Main 브랜치로부터 E 브랜치를 생성하고, 작업을 하며 E-C1 Commit을 한 상황이다. E 브랜치의 Base Commit은 M-C2이다.

Main 브랜치로부터 E 브랜치를 생성한 상황

 

 

개발이 끝나고 테스트까지 마친 D 기능은 운영 반영을 위해 Main 브랜치에서 Merge 되게 된다. Main 브랜치의 Header Commit과 병합하려는 D 브랜치의 Base Commit이 일치하므로 Fast Forward 방식으로 병합된다.

 

FF 방식으로 병합된 Main, D 브랜치

 

 

곧이어 E 기능도 개발과 테스트가 끝났고, 운영 반영을 위해 Main 브랜치에서 Merge 하게 된다. 이때는 FF 병합이 불가능하므로 3-Way-Merge 방식의 병합이 진행된다.

3-Way-Merge를 통한 Main, E 브랜치 병합

 

 


5. Rebase 설명하랬더니 왜 Merge를...? 🙄

Rebase의 사용 목적을 이해하기 위함이다. 상황을 조금만 복잡하게 만들어보도록 하겠다.

연초에 들어서니 개발 및 수정 요청건이 많아져 F, G, H, I, J, K 기능을 개발해야 하는 상황이 발생했다. 마찬가지로 Main 브랜치로부터 각각의 브랜치를 생성한 후 개발을 진행했으며, 결과적으로 아래와 같은 깃 히스토리가 만들어지게됐다.

6개의 브랜치를 생성했다.

 

 

이후 작업이 완료된 브랜치에 대해 하나씩 운영반영을 할 경우 Main 브랜치에서 브랜치별로 Merge 하게 된다. 몇일에 걸친 운영 반영을 모두 마치고 보니 아래와 같이 병렬 형태의 깃 히스토리가 생성되어 있었다. 예로 든건 6개의 브랜치이나, 작업이 더 추가되어 브랜치가 늘어날 경우 깃 히스토리는 두꺼운 병렬 형태를 띄게 되고, Main 브랜치에 대한 작업 히스토리를 파악하기 어려워진다.

복잡해질 준비를 하는 깃 히스토리

 


6. Rebase를 하면 뭐가 달라지나요? 🤔

 

Rebase를 하면 어떻게 될까? 위 브랜치 중 F, G, H에 대해서만 시뮬레이션을 돌려보겠다.

본격적으로 들어가기 전 Rebase 의 정의를 리마인드 하도록 하겠다.

 

 

말 그대로 브랜치의 Base Commit(= Base) 를 재설정 (= Re) 하는 명령어이다.

A 브랜치에서 B 브랜치에 대한 Rebase를 할 경우 A 브랜치의 Base Commit이 B 브랜치의 Head Commit으로 변경된다.


 

먼저 F 브랜치 운영 반영을 위해 F 브랜치에서 Main 브랜치에 대해 Rebase를 한다.

Main 브랜치의 Head Commit은 Merge Commit이고, F 브랜치의 Base Commit 도 Main 브랜치의 Merge Commit 이므로 아무일도 일어나지 않는다.

F 브랜치 > Main 브랜치 Rebase

 

이제 Main 브랜치에서 F 브랜치를 Merge 하면 Fast Forward Merge 방식으로 병합된다.

 

Main 브랜치 > F 브랜치 Merge

 

 

다음, H 브랜치 작업 운영 반영을 위해 H 브랜치에서 Main 브랜치에 대해 Rebase 한다. 

Main 브랜치의 Head Commit은 F-C3 이고, H 브랜치의 Base Commit 은 Merge Commit 이므로

H 브랜치의 Base Commit은 F-C3로 변경된다.

 

H 브랜치 > Main 브랜치 Rebase

 

 

이제 Main 브랜치에서 H 브랜치를 Merge 하면 Fast Forward Merge 방식으로 병합된다.

Main 브랜치 > H 브랜치 Merge

 

 

 

 

다음, G 브랜치 작업 운영 반영을 위해 G 브랜치에서 Main 브랜치에 대해 Rebase 한다. 

Main 브랜치의 Head Commit은 H-C2 이고, G 브랜치의 Base Commit 은 Merge Commit 이므로

G 브랜치의 Base Commit은 H-C2로 변경된다.

 

G 브랜치 > Main 브랜치 Rebase

 

이제 Main 브랜치에서 G 브랜치를 Merge 하면 Fast Forward Merge 방식으로 병합된다.

Main 브랜치 > G 브랜치 Merge

 

 

직접 테스트를 해보면 아래와 같이 직렬 형태의 깃 히스토리가 남는 것을 알 수 있다. 알아보기도 쉽다.

즉, Rebase를 통한 Merge를 하면 보기 쉬운 형태로 깃 히스토리를 관리하게 된다.

Rebase - Merge 전략 활용

 

 

 

 


7. 위험이 도사리고 있는 Rebase 👺

Rebase를 사용하면 간단하게 Base Commit이 변경된다고 설명했지만, 사실 위험한 작업이다. 왜 위험할까?

Origin 입장에서 생각해보자.

G 브랜치에서 작업 후 PUSH를 했을테니 로컬, 원격 브랜치 모두 아래와 같이 G-C1, G-C2, G-C3 커밋이 있었을 것이다.

G 브랜치

 

그런데 G 브랜치에서 Main 브랜치에 대해 Rebase, 정확히 말하면 로컬 G 브랜치에서 Main 브랜치에 대해 Rebase를 한다. 그럼 로컬 G 브랜치의 깃 히스토리는 아래와 같이 변경된다. 이때 push를 하면 어떻게 될까?

G 브랜치 > Main 브랜치 Rebase

 

 

 

그렇다. 충돌이 발생한다. Rebase 에 의해 로컬 브랜치의 깃 히스토리가 변경되었고 원격 브랜치로 Push 하려 하니 원격 브랜치 입장에서 깃 히스토리가 일치하지 않아 충돌이 발생하는 것이다. 원격 브랜치 입장에서는 로컬 브랜치가 자신과 이름만 같은 브랜치인 것이다.

로컬, 원격 브랜치의 조상 Commit 이 다르니 3-Way-Merge 방식을 통해 아래와 같이 병합된다.

로컬 G 브랜치와 원격 G 브랜치간 충돌 / 3-way-merge

 

결과적으로 Rebase를 했더니 동일 브랜치간 Merge 작업이 추가되고, Main 에서 이를 Merge하면 이러한 히스토리도 남게된다. 이렇게 보니 그냥 Merge를 하는것 보다 못하는 상황이다.

 

앞서 예시에서 보여준 Rebase는 이러한 케이스가 없었다. 즉, 로컬, 원격 브랜치가 충돌이 일어나지 않았다는 건데, 이건 무엇을 의미할까?

 

바로 강제 Push 를 통해 로컬과 원격 브랜치 간 히스토리를 맞춰준 것이다. 즉, Rebase를 하면 강제 Push 작업이 동반된다.

 

이러한 강제 Push는 해당 브랜치에 협업자가 있을 경우 큰 문제를 야기할 수 있다. 특히 이러한 사실을 몰랐을 때 예기치 못한 상황에 당황할 수 있고, 자신이 Push 했던 작업 내역을 잃어버릴 수도 있다.

 

1번 개발자가 G 브랜치를 pull 받은 후 Rebase 한 이후 시점에 2번 개발자가 G 브랜치에 작업 내역을 push 한다면, 1번 개발자의 로컬 G 브랜치에는 2번 개발자가 작업한 내용이 반영되지 않은 상태이다. 이때 1번 개발자가 Rebase 한 내역을 원격 브랜치에 덮어쓰기 위해 강제 push를 하게 된다면 2번 개발자가 작업한 내용과 히스토리 모두 날아가게 된다.

 

다행히도 이 문제는 강제 push 시 force-with-lease 옵션을 사용하면 해결된다. 로컬 저장소와 원격 저장소를 비교했을 때, 원격 저장소에 새로운 커밋이 추가되어 있는 경우 강제 Push 작업을 취소시키는 옵션이다.

 


 

8. 양날의 검 Rebase

Rebase는 깔끔한 깃 히스토리를 관리하고, 매우 단순하게 동작하는 착한(?) 명령인줄 알았으나, 깃 히스토리를 조작하고, 강제 Push 를 동반해야하는 위험한 녀석이었다. 이러한 위험성을 인지하고, 협업자와 충분한 소통과 함께 사용한다면 큰 문제가 없을테지만, Git을 통한 협업이 어색하고, Rebase의 동작과 위험성을 충분히 인지하지 못한 상태에서 사용한다면 코드를 날려먹거나 협업 간 사소하지 않은 문제를 야기할 수 있을 테니 조심히 사용하자.

반응형

+ Recent posts