Overview

한 프로젝트를 여러 사람이 작업하면 각자의 feature 브랜치에서 수정한 내용을 하나의 통합 브랜치 (main) 에 합치는 방식으로 진행합니다.

이렇게 브랜치를 통합할 때 사용하는 명령어가 git merge 입니다.

이 merge 는 각 브랜치의 상황에 따라 다르게 동작하고 방법도 다양하기 때문에 어떤 방법들이 있는지 알아봅니다.


1. Git Merge

CLI 또는 GUI 에서 사용하는 경우입니다.

크게 Merge, Fast-Forward, Squash, Rebase 가 있습니다.


1.1. Merge

$ git switch main
$ git merge feature

main 브랜치에 추가 작업 내역이 있다면 새로운 Merge Commit 을 만들게 됩니다.

가장 일반적인 Merge 방법입니다.

feature 의 모든 커밋 로그와 하나로 합친 Merge Commit 로그가 전부 남습니다.


1.2. Fast-Forward

$ git switch main
$ git merge feature # main 에 추가 작업 내역 없음

feature 브랜치를 딴 이후로 main 브랜치에 아무런 커밋이 없다면 merge 할 때 Fast-Forward 방식으로 합쳐집니다.

Fast-Forward 를 그대로 직역하면 "빨리감기" 라는 뜻입니다.

이 말 그대로 별도의 Merge 기록 없이 원래 main 에서 작업한 것처럼 로그가 남습니다.

병합하려는 main 브랜치에 커밋이 존재한다면 Fast-Forward Merge 가 되지 않습니다.


1.3. No Fast-Forward (--no-ff)

$ git switch main
$ git merge --no-ff feature # main 에 추가 작업 내역 없지만 머지 커밋 생성

만약 main 에 추가 작업 내역이 없어도 새로운 Merge Commit 을 만들고 싶다면 --no-ff 옵션을 추가합니다.

feature 브랜치의 존재를 남기고 싶을 때 사용할 수 있습니다.


1.4. Squash (--squash)

$ git switch main
$ git merge --squash feature
$ git commit -m "Merge Squash feature/squash"

Squash Merge 는 feature 의 모든 커밋을 하나의 커밋으로 만들어 main 에 머지합니다.

feature 에서 리뷰 반영, 버그 수정 등으로 쓸데없는 커밋이 많아진 경우 이를 다 기록하지 않고 하나의 새로운 커밋으로 남길 수 있습니다.

대신 feature 브랜치의 수정사항이 큰 경우 하나의 커밋으로 전부 표현하기 보다 커밋을 잘개 쪼개는게 알아보기 더 편할 수 있기 때문에 신중히 사용해야 합니다.


1.5. Rebase

$ git switch main
$ git rebase feature

main 에 아무런 추가 커밋이 없다면 Fast-Forward 와 동일하게 HEAD 만 이동합니다.

하지만 다른 커밋이 있다면 이름 그대로 커밋을 재배치 합니다.

재배치 하고 나면 현재 브랜치의 커밋이 rebase 하려는 브랜치의 뒤로 이동합니다.

main 브랜치에서 git rebase feature 를 했다면 feature 의 커밋 내역이 먼저 찍히고 이후 main 브랜치의 커밋이 찍힙니다.

만약 같은 범위를 수정해서 rebase 과정에서 충돌이 발생한다면 각 커밋 별로 충돌을 해결해야 합니다.

별도의 Merge Commit 이 남지 않는다는 점은 Fast-Forward 와 동일하지만 Rebase 는 각 브랜치에 다른 커밋이 있어도 하나의 줄기로 합쳐줄 수 있다는 장점이 있습니다.


위 사진과 같이 어디 브랜치에서 시작하냐에 따라 커밋의 순서가 바뀝니다.

현재 checkout 한 브랜치의 커밋이 가장 최신에 위치하는 걸 볼 수 있습니다.


2. Github Merge

Github 에서도 Merge 를 할 수 있습니다.

아마 Github 에서 Pull Request 로 코드 리뷰를 받은 후에 Merge 하는 경우가 더 많을 것 같습니다.

Github 에서 지원하는 Merge 는 크게 세종류입니다.


2.1. Create a merge commit

Create a merge commit 을 사용하면 main 브랜치에 커밋이 있건말건 무조건 --no-ff 옵션으로 머지됩니다.

커밋 로그가 전부 남으며 새로운 Merge Commit 이 함께 만들어집니다.


2.2. Squash and merge

Git 과 마찬가지로 feature 의 커밋 이력들 대신 새로운 Merge Commit 하나만 남깁니다.

저는 리뷰 수정사항이 많아서 기능에 비해 커밋 수가 너무 많을 때 사용하는 방법입니다.


2.3. Rebase and merge

Rebase 는 feature 의 커밋 로그를 main 브랜치 커밋 로그 뒤에 붙여줍니다.

위에서 설명할 때 rebase 를 하면 현재 브랜치의 커밋이 대상 브랜치의 커밋 뒤로 이동한다고 했습니다.

이 특성을 이용해서 우선 feature 브랜치에서 main 브랜치를 rebase 한 뒤, feature -> main 으로 Fast-Forward Merge 한 셈입니다.

Git 명령어로 나타내면 아래와 같습니다.


# feature 브랜치로 이동
$ git switch feature

# main 브랜치의 커밋내역을 feature 브랜치 이전으로 끼워넣기
# 이렇게 하면 최신 main 브랜치에서 feature 를 딴 뒤 수정한 것처럼 됨
$ git rebase main 

# 다시 main 브랜치로 이동
$ git switch main

# main 에 feature 브랜치 머지
# rebase 를 했기 때문에 HEAD 만 이동하는 fast-forward merge 실행
$ git merge feature

이렇게 하면 깔끔하게 main 브랜치 뒤에 feature 브랜치 커밋 로그를 남길 수 있습니다.

다만, feature 브랜치의 커밋이 엄청 많은 경우 전부 rebase merge 해버리면 트리만 봤을 때 작업의 큰 줄기가 잘 보이지 않는다는 단점이 있습니다.


Conclusion

그래서 어떤걸 사용하는게 좋냐? 라고 한다면 정해진 답은 없습니다.

여러 개의 커밋을 남기고 싶다면 --no-ff 머지를 사용하고 하나만 깔끔하게 남긴다면 Squash 또는 Rebase 를 사용합니다.

현재 제가 사용하는 방식을 그림으로 나타내면 아래와 같습니다.


저는 대부분의 머지를 PR 에서 하기 때문에 PR 을 최대한 작은 단위로 나눈 후 전부 Squash Merge 하는 걸 선호했습니다.

코드리뷰를 받다보면 추가 수정 사항이 발생하고 자잘한 커밋들을 전부 남기면 지저분하게 느껴서 없애고 싶었어요.

회사에 와서 git rebase main 으로 feature 브랜치를 최신화 하고 --no-ff 옵션으로 Merge Commit 을 남기는 방법을 알게 되었는데 굉장히 깔끔한 것 같습니다.

여러 회사들의 기술 블로그를 보면 각 팀마다 사용하는 Merge 전략이 있는데 여러 가지를 찾아보고 본인이 느끼기에 가장 좋아보이는 전략을 선택하면 됩니다.

'공부 > Git' 카테고리의 다른 글

Gitmoji (for Git Commit Convention)  (0) 2022.06.18
Git Directory 이름 변경  (0) 2020.04.26

Overview

Gitmoji 란 Git + Emoji 입니다.

여러 사람이 커밋 메시지를 작성하다보면 일관성이 없고 나중에는 히스토리를 알아보기 힘들어집니다.

Gitmoji 는 이모지를 사용하여 커밋 메시지를 일정하게 작성하도록 도와주는 툴입니다.

커밋 메시지 타이틀 앞에 특정 이모지를 넣으면서 이모지만 보고도 어떤 목적으로 한 커밋인지 알아볼 수 있습니다.

gitmoji 에 대한 설명은 gitmoji 공식에서 볼 수 있지만 사용하기 위해서는 gitmoji-cli 를 참고해야 합니다.


1. Install

# use brew
$ brew install gitmoji

# use npm
$ npm i -g gitmoji-cli

brew 또는 npm 을 사용해서 설치할 수 있습니다.


2. Configuration

gitmoji 를 사용하기 전에 미리 설정을 해두는게 좋습니다.

gitmoji -g 명령어를 사용해서 여러가지 옵션을 설정할 수 있습니다.

저는 대부분 기본값을 사용하는데 Select how emojis should be used in commits 옵션만 text 대신 emoji 를 선택했습니다.


Github 에서 볼 때는 별 문제 없으나 이렇게 이모지를 지원하지 않는 외부 툴에서는 Text 그대로 노출됩니다.

그래서 이모지 자체를 커밋 메시지에 넣을 수 있도록 설정했습니다.


3. Usage

gitmoji 의 사용법은 크게 어렵지 않습니다.

그냥 평범한 사용법에서 git commit 대신 gitmoji -c 를 입력해주면 됩니다.


3.1. Choose Gitmoji

gitmoji -c 를 입력하면 위 그림처럼 이모지 선택이 나옵니다.

위아래 방향키를 사용해서 다른 이모지들을 찾아볼 수도 있고, 직접 원하는 기능을 검색할 수도 있습니다.


위 그림처럼 refactor 를 검색하면 그에 맞는 이모지를 띄워줍니다.


3.2. Input Commit Message

원하는 이모지를 선택했다면 Commit Title, Message 를 입력합니다.

Message 는 생략해도 됩니다.

전부 입력 후 엔터를 누르면 커밋이 완료됩니다.

이후에는 똑같이 git push 를 사용해서 원격 저장소에 반영할 수 있습니다.


3.3. Repository 확인

이제 커밋 로그를 확인하면 이렇게 이모지가 잘 들어간 것을 볼 수 있습니다.

제가 실제로 두번 커밋한거고 중복해서 로그가 쌓인건 아닙니다.


4. Plugin 및 GUI

IntelliJ 는 Plugin, VSCode 는 Extension 으로 지원하기도 합니다.

IDE 에서 직접 커밋 메시지를 작성하는 스타일이었다면 이용해보는 게 좋습니다.

Source Tree, GitKraken, Git Fork 등등 별도 GUI 를 사용할 때는 아쉽게도 지원되지 않는 것 같아요.

개인적으로 CLI 대신 GUI 를 많이 사용하기 때문에 이 부분이 참 아쉬웠습니다.

그래서 그냥 add, push, pull 등은 전부 GUI 를 이용하고 오로지 커밋만 gitmoji -c 를 사용합니다.

만약 이게 싫다면 이모지를 직접 복사해서 GUI 커밋 메시지에 붙여넣는 방법도 가능합니다.

Gitmoji Dev 사이트에서 원하는 이모지를 검색할 수 있고 그림을 누르면 자동으로 복사되기 때문에 GUI 에서 작성하는 경우 편리하게 이용할 수 있습니다.


5. 장단점

가장 큰 장점은 커밋 로그를 시각적으로 확인할 수 있다 입니다.

단점은 이모지가 뭘 의미하는지 알고 있어야 하고 GUI 와 같이 쓰기 번거롭다는 점인것 같아요.

Gitmoji 를 처음 봤을 때 생각난건 feat:, fix:, refactor: 등을 사용하는 Angular Git Commit Guidelines 였는데요.

시각적으로는 그림인 이모지가 더 잘들어오긴 하지만 가독성은 개인에 따라 다르기 때문에 뭐가 더 낫다고 단언할 수는 없을 것 같아요.

Angular Convention 과 비교했을 때 가장 큰 장점을 뽑자면 Search 기능이라고 생각합니다.

파일을 수정하고 어떤 prefix 를 붙여야할 까 고민될 때 gitmoji 는 대충 이런 목적이다~ 검색을 하면 그에 맞는 이모지를 추천해줍니다.


Conclusion

사실 Gitmoji 가 뭔지 잘 모르다가 회사에서 사용하면서 알게 되었는데요.

처음에는 CLI 와 GUI 를 왔다갔다 하는게 불편했지만 적응해보니 생각보다 쓸만한 것 같습니다.

특히 Search 기능이 강력해서 적당히 입력해도 알아서 추천해주니 큰 고민 없이 넣을 수 있다는게 좋았습니다.


Reference

'공부 > Git' 카테고리의 다른 글

Git Merge (feat. Github)  (0) 2022.06.20
Git Directory 이름 변경  (0) 2020.04.26

1. Overview

전략 패턴은 여러 알고리즘 (로직) 을 캡슐화 하여 상호 교환 가능하게 하는 패턴입니다.

단순히 패턴만 보면 어떤 목적인지 이해하기 힘들지만 템플릿 메서드 (Template Method) 패턴과 비교해서 생각해보면 됩니다.

템플릿 메서드 패턴은 추상 클래스에 공통된 로직을 놓고 변경되는 로직은 상속을 통해 구현했습니다.

이 패턴의 단점은 부모 클래스에 의존도가 생긴다는 점이었습니다.

전략 패턴은 변하지 않는 부분을 Context 에 두고 변하는 부분을 Strategy 인터페이스의 구현체에 작성합니다.

전략에 해당하는 Strategy 인터페이스와 구현체에는 비즈니스 로직 외에 아무런 로직이 없기 때문에 공통된 로직이 변경되어도 아무런 영향이 없습니다.


2. Strategy Example

간단한 예시 코드를 작성해봅니다.

  • 비즈니스 로직 1 존재
  • 비즈니스 로직 2 존재
  • 각 비즈니스 로직의 실행 시간을 측정하는 공통된 로직 존재

2.1. Before

public class BeforeStrategyApp {

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

    private static void logic1() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 1 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }

    private static void logic2() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 2 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

요구사항을 단순하게 구현하면 이렇게 공통된 로직이 존재합니다.

위 코드에서 다른 부분은 "비즈니스 로직 실행" 하나뿐이고 나머지는 전부 중복된 코드입니다.

전략 패턴을 적용해서 리팩토링 해봅시다.


2.2. Strategy (변경되는 부분)

public interface Strategy {
    void call();
}

public class StrategyLogic1 implements Strategy {

    @Override
    public void call() {
        System.out.println("비즈니스 로직 1 실행");
    }
}

public class StrategyLogic2 implements Strategy {

    @Override
    public void call() {
        System.out.println("비즈니스 로직 2 실행");
    }
}

Strategy 인터페이스와 각 비즈니스 로직을 담당하는 하위 구현체들을 선언합니다.

나중에 비즈니스 로직 3 이 추가된다면 인터페이스나 다른 구현체 변경 없이 새로 추가하기만 하면 됩니다.


2.3. Context (공통된 부분)

public class Context {

    private final Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        strategy.call();
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

공통된 로직이 작성되어 있는 Context 클래스입니다.

Strategy 를 생성자로 받기 때문에 어떤 구현체를 받느냐에 따라 비즈니스 로직이 달라집니다.

비즈니스 로직을 위임 한다고도 표현합니다.


2.4. Application (Client)

public class AfterStrategyApp {

    public static void main(String[] args) {
        Strategy strategy1 = new StrategyLogic1();
        Context context1 = new Context(strategy1);
        context1.execute();

        Strategy strategy2 = new StrategyLogic1();
        Context context2 = new Context(strategy2);
        context2.execute();
    }
}

구현을 원하는 로직에 따라 다른 Strategy 를 생성자로 넘겨줍니다.

Spring 에서는 Bean 주입 설정만 바꾸면 쉽게 로직을 변경할 수 있습니다.


3. 장단점

  • 장점
    • 공통 로직이 부모 클래스에 있지 않고 Context 라는 별도의 클래스에 존재하기 때문에 구현체들에 대한 영향도가 적음
    • ContextStrategy 라는 인터페이스를 의존하고 있기 때문에 구현체를 갈아끼우기 쉬움
  • 단점
    • 로직이 늘어날 때마다 구현체 클래스가 늘어남
    • ContextStrategy 를 한번 조립하면 전략을 변경하기 힘듬

4. Template Callback 패턴

전략 패턴은 생성자 파라미터로 한번 주입하고 나면 동적으로 변경할 수 없다는 단점이 있습니다.

이러한 단점을 극복하기 위해 Context 의 생성자가 아닌 execute() 메서드의 파라미터로 Strategy 를 넘겨주기도 합니다.

이런 패턴을 Template Callback (템플릿 콜백) 패턴이라고도 부릅니다.

템플릿 콜백 패턴도 전략 패턴과 동일하지만 동적으로 비즈니스 로직을 설정할 수 있다는 장점이 있습니다.


5. Template Callback Example

위에서 사용했던 예제를 템플릿 패턴으로 다시 구현해봅니다.


5.1. Callback (Strategy)

public interface Callback {
    void call();
}

전략 패턴과 동일하게 인터페이스 하나를 정의합니다.

실제 비즈니스 로직은 런타임에 넘겨줄거라서 별다른 구현체를 만들지 않아도 됩니다.


5.2. Template (Context)

public class TimeLogTemplate {

    public void execute(Callback callback) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        callback.call();
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

전략 패턴의 Context 에 해당하는 부분이지만 생성자로 받는 대신 execute() 의 파라미터로 전략을 넘겨받습니다.


5.3. Application (Client)

public class AfterTemplateCallbackApp {

    public static void main(String[] args) {
        TimeLogTemplate timeLogTemplate = new TimeLogTemplate();
        timeLogTemplate.execute(() -> System.out.println("비즈니스 로직 1 실행"));
        timeLogTemplate.execute(() -> System.out.println("비즈니스 로직 2 실행"));
    }
}

Callback 이 함수형 인터페이스라서 람다식으로 간단하게 표현할 수 있습니다.

이제 새로운 로직이 추가되어도 클래스를 만들지 않고 파라미터의 값만 변경해주면 됩니다.

Spring 에서 JdbcTemplate, RestTemplatexxxTemplate 의 형태를 하면 대부분 템플릿 콜백 패턴을 사용한 거라고 생각하시면 됩니다.


Reference

1. Overview

코드를 작성하다보면 로깅, 예외 처리 등등 반복되어 작성하는 코드가 발생합니다.

이런 경우 코드의 중복을 없애기 위한 패턴 중 하나가 템플릿 메서드 패턴입니다.

AbstractClass (추상 클래스) 는 템플릿을 제공하고 이를 상속 받는 하위 클래스가 구체적인 로직을 작성합니다.

추상 클래스가 전체적인 골격을 정의하고 일부 로직은 하위 상속 클래스에서 구현합니다.

중복된 로직은 추상 클래스에 정의하고 달라지는 비즈니스 로직만 상속 클래스에서 재정의 (오버라이딩) 합니다.

여기서 중복된 로직은 일반적으로 "변하지 않는 부분" 이고 비즈니스 로직은 "변하는 부분" 이라고 할 수 있습니다.


2. Example

간단한 예시 코드를 작성해봅니다.

  • 비즈니스 로직 1 존재
  • 비즈니스 로직 2 존재
  • 각 비즈니스 로직의 실행 시간을 측정하는 공통된 로직 존재

2.1. Before

public class BeforeTemplateMethodApp {

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

    private static void logic1() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 1 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }

    private static void logic2() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 2 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

요구사항을 단순하게 구현하면 이렇게 공통된 로직이 존재합니다.

위 코드에서 다른 부분은 "비즈니스 로직 실행" 하나뿐이고 나머지는 전부 중복된 코드입니다.

템플릿 메서드 패턴을 적용해서 리팩토링 해봅시다.


2.2. Abstract Class

public abstract class AbstractTemplate {

    public void execute() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        call();
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }

    protected abstract void call();
}

공통된 로직을 갖고 있는 추상 클래스 입니다.

execute() 메서드 내부에서 비즈니스 로직 부분은 call() 메서드 호출로 대체합니다.

call() 메서드는 이 추상 클래스를 상속하는 자식 클래스에서 오버라이딩 합니다.


2.3. Sub Class

public class SubClassLogic1 extends AbstractTemplate {

    @Override
    protected void call() {
        System.out.println("비즈니스 로직 1 실행");
    }
}

public class SubClassLogic2 extends AbstractTemplate {

    @Override
    protected void call() {
        System.out.println("비즈니스 로직 2 실행");
    }
}

추상 클래스를 상식하는 SubClass 입니다.

비즈니스 로직만 정의해둔 간단한 메서드입니다.

이후에 또다른 비즈니스 로직 3 이 필요하다면 SubClassLogic3 을 정의해서 사용하면 됩니다.


2.4. Application (Client)

public class AfterTemplateMethodApp {

    public static void main(String[] args) {
        AbstractTemplate template1 = new SubClassLogic1();
        template1.execute();

        AbstractTemplate template2 = new SubClassLogic2();
        template2.execute();
    }
}

실제로 사용하는 부분입니다.

execute() 메서드를 호출하는 건 동일하지만 어떤 객체를 만드냐에 따라 로직이 달라집니다.


3. 장단점

  • 장점
    • 중복된 코드를 없애고 SubClass 에서는 비즈니스 로직에만 집중할 수 있음 (SRP)
    • 나중에 새로운 비즈니스 로직이 추가되어도 기존 코드를 수정하지 않아도 됨 (OCP)
  • 단점
    • 클래스 파일을 계속 만들어야 함
    • 자식 클래스는 실제로 부모 클래스를 사용하지 않는데 단순히 패턴 구현을 위한 상속 때문에 의존 관계를 갖고 있음

템플릿 메서드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴으로 Strategy (전략) 패턴이 있습니다.


Reference

1. Overview

Factory 패턴 시리즈의 마지막인 추상 팩토리 패턴입니다.

추상 팩토리는 얼핏 보면 팩토리 메서드 패턴과 비슷하다고 느낄 수도 있습니다.

가장 큰 차이점은 팩토리 메서드 패턴은 어떤 객체를 생성 할지에 집중하고 추상 팩토리 패턴은 연관된 객체들을 모아둔다는 것에 집중합니다.


2. Abstract Method

추상 팩토리 패턴은 연관된 객체들의 생성을 하나의 팩토리에서 담당합니다.


3. Example

스포츠 팀을 만든다고 가정합니다.

스포츠 팀에는 플레이어와 매니저가 필수로 존재하기 때문에 팀의 구성요소라고 볼 수 있습니다.


3.1. ProductA (Manager)

public interface Manager {
}

public class SoccerManager implements Manager {
}

public class TennisManager implements Manager {
}
  • Manager 인터페이스와 클래스를 정의합니다.
  • 축구팀과 테니스팀이 존재하기 때문에 두 개의 Manager 구현 클래스를 정의합니다.

3.2. ProductB (Player)

public interface Player {
}

public class SoccerPlayer implements Player {
}

public class TennisPlayer implements Player {
}
  • Player 인터페이스와 클래스를 정의합니다.
  • 매니저와 마찬가지로 축구 선수와 테니스 선수를 정의합니다.

3.3. Factory (StaffFactory)

public interface StaffFactory {
    Manager createManager();
    Player createPlayer();
}

public class SoccerStaffFactory implements StaffFactory {

    @Override
    public Manager createManager() {
        return new SoccerManager();
    }

    @Override
    public Player createPlayer() {
        return new SoccerPlayer();
    }
}

public class TennisStaffFactory implements StaffFactory {

    @Override
    public Manager createManager() {
        return new TennisManager();
    }

    @Override
    public Player createPlayer() {
        return new TennisPlayer();
    }
}
  • Product 를 생성하는 Factory 클래스를 정의합니다.
  • Manager, Player 는 축구라는 하나의 공통점으로 묶을 수 있습니다.
  • 그래서 SoccerManager, SoccerPlayer 를 생산하는 SoccerStaffFactory 와 반대로 테니스 객체들을 생성하는 TennisStaffFactory 를 정의합니다.
  • 단순하게 생각하면 팩토리 메서드 패턴과 동일하지만 공통된 집합을 모아둔다는 점이 특징입니다.

3.4. Client

public class AbstractFactoryApp {
    public static void main(String[] args) {
        use(new SoccerStaffFactory());
        use(new TennisStaffFactory());
    }

    private static void use(StaffFactory factory) {
        Manager manager = factory.createManager();
        Player player = factory.createPlayer();
    }
}
  • 구체 클래스가 아닌 인터페이스에 의존하게 작성할 수 있습니다.
  • 어떤 Factory 를 넘겨받는지에 관계 없이 클라이언트는 Manager, Player 를 생성해서 사용할 수 있습니다.

3.5. 의존 관계 다이어그램

의존 관계 그림을 보면 맨 처음에 봤던 다이어그램과 동일한 것을 볼 수 있습니다.


4. 장단점

  • 장점
    • 팩토리 메서드 패턴과 마찬가지로 수정에는 닫혀 있고 확장에는 열려 있습니다.
    • 여러 개의 비슷한 집합 객체 생성을 하나의 팩토리에 모아둘 수 있습니다. (위 예시 뿐만 아니라 자동차의 부품 등)
  • 단점
    • 팩토리 메서드 패턴과 마찬가지로 클래스 갯수가 늘어납니다.

Reference

1. Overview

Factory 패턴은 객체 생성과 관련된 디자인 패턴입니다.

1편에서 봤던 Simple Factory 는 객체 생성 역할을 담당하면서 각 클라이언트에서 구현 클래스에 직접 의존하지 않도록 분리했습니다.

하지만 새로운 클래스가 추가되었을 때 Factory 클래스를 수정해야 한다는 한계가 있었습니다.

기존 코드의 변경 없이 확장하기 위한 디자인 패턴이 Factory Method 패턴입니다.


2. Factory Method

Factory Method Pattern (팩토리 메소드 패턴) 은 생성 패턴 중 하나로 객체를 생성할 때 어떤 클래스의 인스턴스를 만들 지 서브 클래스에서 결정하게 합니다.

즉, 인스턴스 생성을 서브 클래스에게 위임합니다.

부모 추상 클래스는 인터페이스에만 의존하고 실제로 어떤 구현 클래스를 호출할 지는 서브 클래스에서 구현합니다.

이렇게 하면 새로운 구현 클래스가 추가되어도 기존 Factory 코드의 수정 없이 새로운 Factory 를 추가하면 됩니다.


3. Example

사용자 관리 프로그램이 있고 네이버 계정으로 가입할 수 있다고 가정합니다.


2.1. Product (User)

public interface User {
    void signup();
}
  • User 인터페이스 정의

public class NaverUser implements User {
    @Override
    public void signup() {
        System.out.println("네이버 아이디로 가입");
    }
}
  • User 인터페이스를 구현하는 NaverUser 클래스 정의
  • 오버라이드한 메서드에는 네이버 유저 전용 로직 추가

2.2. Creator (UserFactory)

public abstract class UserFactory {

    public User newInstance() {
        User user = createUser();
        user.signup();
        return user;
    }

    protected abstract User createUser();
}
  • 추상 클래스로 UserFactory 를 정의
  • 외부에서 User 객체를 생성할 때는 newInstance() 메서드를 호출하면 되고, 실제로 어떤 객체를 생성할 지는 추상 메서드로 정의해서 하위 클래스에서 정의
  • Java 8 부터는 인터페이스에서 default 메서드를 사용할 수 있기 때문에 인터페이스로 정의할 수도 있지만 protected 키워드를 사용해 접근을 제한하고 싶어서 추상 클래스를 사용

public class NaverUserFactory extends UserFactory {
    @Override
    protected User createUser() {
        return new NaverUser();
    }
}
  • UserFactory 를 상속받는 NaverUserFactory 를 정의
  • NaverUser 를 반환하도록 오버라이드

2.3. Client

UserFactory userFactory = new NaverUserFactory();
User user = userFactory.newInstance();
  • 클라이언트 코드에서 NaverUser 클래스에 대한 의존성 없이 사용 가능
  • 의존성 주입을 사용해서 외부에서 Factory 클래스를 받아온다면 NaverUserFactory 에 대한 의존성도 제거 가능

3. 스펙 확장

팩토리 메서드 패턴의 장점은 확장할 때 기존 코드의 변경이 없어도 된다는 점이라고 했습니다.

카카오 서비스가 오픈되고 사용자가 많아져서 카카오 계정으로도 가입할 수 있게 확장되었다고 가정합니다.

Simple Factory 에서는 UserFactory 코드를 수정해야 했습니다.

깜빡하고 switch-case 문을 추가하지 않으면 코드에 오류가 생기며 Enum 으로 어느정도 방어할 수는 있지만 수정에도 열려있다는 단점은 변하지 않습니다.

하지만 우리는 Factory Method 패턴을 적용했기 때문에 기존 코드 (User, NaverUser, UserFactory, NaverUserFactory) 의 수정 없이 새로운 코드를 추가만 하면 됩니다.


3.1. 새로운 Product 와 Creator

public class KakaoUser implements User {
    @Override
    public void signup() {
        System.out.println("카카오 아이디로 가입");
    }
}
  • NaverUser 클래스와 동일하게 User 인터페이스를 구현하는 KakaoUser 클래스 추가

public class KakaoUserFactory extends UserFactory {
    @Override
    protected User createUser() {
        return new KakaoUser();
    }
}
  • NaverUserFactory 클래스와 동일하게 KakaoUserFactory 정의

UserFactory userFactory = new NaverUserFactory();
User user = userFactory.newInstance();

// 위 클라이언트 코드 수정 없이 다른 곳에서 사용 가능
UserFactory kakaoUserFactory = new KakaoUserFactory();
User kakaoUser = kakaoUserFactory.newInstance();
  • 기존 코드의 변경 없이 새로 선언한 클래스만 사용하여 확장 가능

4. 장단점

  • 장점: Factory Method 패턴의 가장 큰 장점은 지금까지 본 것처럼 수정에 닫혀있고 확장에는 열려있는 OCP 원칙을 지킬 수 있다는 점입니다.
  • 단점: 간단한 기능을 사용할 때보다 많은 클래스를 정의해야 하기 때문에 코드량이 증가합니다.

Reference

1. Overview

Factory 패턴은 객체 생성 역할을 별도의 클래스 (Factory) 에게 위임하는 것이 가장 궁극적인 목표입니다.

디자인 패턴 중 Facotry 와 관련된 패턴은 크게 두 가지가 있습니다.

팩토리 메서드 패턴과 추상 팩토리 패턴인데요.

이 두 가지 패턴의 베이스가 되는 가장 단순한 형태의 Factory 패턴이 존재합니다.

엄밀히 따지면 디자인 패턴이 아니라 객체 지향 프로그래밍에서의 자주 쓰이는 관용구 느낌이라 별도의 이름은 없지만 Simple Factory 라는 이름으로 많이 불립니다.

이 글에서는 Simple Factory 에 대해 알아보고 이후 나머지 두 패턴에 대해 알아볼 예정입니다.


2. Simple Factory

Simple Factory 는 굉장히 단순합니다.

객체는 여러 곳에서 생성될 수 있는데, 호출하는 쪽이 객체의 생성자에 직접 의존하고 있으면 나중에 변경되었을 때 수정되어야 하는 코드가 많이 발생합니다.

그래서 생성자 호출 (new) 을 별도의 클래스 (Factory) 에서 담당하고 클라이언트 코드에서는 팩토리를 통해 객체를 생성합니다.


3. Example

public interface Pet {
}

public class Cat implements Pet {
}

public class Dog implements Pet {
}

애완 동물을 한번 예시로 들어봅니다.

공통 인터페이스인 Pet 을 정의하고 이를 구현하는 Cat, Dog 클래스를 만들었습니다.

이제 클라이언트 코드에서 Cat, Dog 을 사용하기 위해 생성할 수 있습니다.


3.1. Before

Pet cat = new Cat();
Pet dog = new Dog();

일반적인 사용법은 new 를 사용해 구현 클래스를 생성한 후 호출하는 겁니다.

하지만 이렇게 하면 Client 와 클래스들 사이에 다음과 같은 의존관계가 생깁니다.


이렇게 구현 클래스를 직접 의존하고 있으면 해당 클래스의 생성자나 전처리 코드가 변경되었을 때 사용하는 모든 Client 코드를 변경해야 합니다.

그래서 객체의 생성만을 담당하는 별도의 Factory 클래스를 만들어 생성 역할을 넘겨봅니다.


3.2. After

public interface Pet {
    enum Type {
        CAT, DOG
    }
}

우선 Pet 인터페이스에 enum 으로 타입을 선언합니다.


public class PetFactory {

    public Pet createPet(Pet.Type petType) {
        switch (petType) {
            case CAT:
                return new Cat();
            case DOG:
                return new Dog();
            default:
                throw new IllegalArgumentException("Pet 타입이 아닙니다");
        }
    }
}

PetFactory 를 만든 후 Pet.Type 에 따라 다른 객체를 생성해서 반환합니다.


PetFactory petFactory = new PetFactory();
Pet cat = petFactory.createPet(Pet.Type.CAT);
Pet dog = petFactory.createPet(Pet.Type.DOG);

PetFactory 를 선언한 후 생성 메서드만 호출하면 실제 구현 클래스인 Cat, Dog 에 의존하지 않은 코드를 작성할 수 있습니다.


의존 관계를 그림으로 표현하면 위와 같이 변경됩니다.

Client 에서 구현 클래스를 직접 의존하지 않기 때문에 나중에 클래스 이름이 변경되거나, 생성자가 변경되는 경우에도 PetFactory 내부만 수정하면 됩니다.


4. Simple Factory 의 한계

Simple Factory 는 앞서 말했듯이 디자인 패턴으로 분류되지는 않습니다.

이 패턴은 객체의 생성 역할을 담당하며 확장이 용이하다는 장점이 있지만 변경에 닫혀 있어야 한다는 OCP 원칙에 위배됩니다.

만약 새로운 애완 동물 구현 클래스로 Bird 가 추가 되었다고 가정합니다.

그럼 PetFactory 내부에 존재하는 switch 문에 해당 클래스를 추가해줘야 합니다.

객체지향 원칙은 확장을 할 때 기존 코드에 영향을 주지 않는 것을 지향합니다.

팩토리 메서드나 추상 팩토리 패턴을 활용한다면 기존 클래스에 영향을 주지 않고 확장이 가능합니다.

인덱스에 대해 알고 있는 정보를 정리합니다.

잘못된 정보가 있으면 지적 부탁드립니다.

Overview

인덱스는 "목차" 라고 할 수 있습니다.

예를 들어 책에서 어떤 단어의 위치를 찾으려면 책을 전부 다 봐야하지만 가나다순으로 목차를 만들어두면 쉽게 찾을 수 있습니다.

목차는 순서대로 정렬되어 있기 때문에 데이터를 더 빠르게 찾을 수 있게 도와줍니다.

하지만 목차를 넣으면 별도 페이지를 만들어야 하기 때문에 책이 조금 더 두꺼워집니다.

그리고 새로운 단어가 추가되면 목차에도 추가해줘야 하고 중간 단어를 넣는다면 순서를 조정해서 페이지도 다시 만들어야 합니다.


1. 인덱스란

인덱스는 위에서 설명한 목차와 비슷합니다.

어떤 데이터를 찾을 때 DB 테이블을 찾는 대신 미리 만들어둔 인덱스를 먼저 탐색합니다.

인덱스를 설정하면 특정 컬럼들을 키 값으로 메모리 영역에 트리 구조로 저장해둡니다.

그리고 디스크 저장소에 바로 접근하는 대신 메모리 저장소에 있는 인덱스를 먼저 조회해서 빠르게 데이터를 가져올 수 있습니다.

인덱스가 데이터를 빠르게 가져올 수 있는 이유는 항상 정렬된 상태를 유지하기 때문입니다.

이를 위해 데이터가 추가/삭제 될 때마다 자료구조를 정렬하기 때문에 인덱스는 SELECT 성능을 향상시키는 대신 INSERT, UPDATE, DELETE 의 성능이 떨어지게 됩니다.


2. 인덱스 종류

인덱스 종류는 어떤 자료구조를 사용하냐에 따라 나눌수도 있고, 데이터 저장 방식에 따라 클러스터드 (Clustered) 인덱스와 넌클러스티드 (Non Clustered) 인덱스로 나누기도 합니다.


2.1. Clustered Index vs Non-Clustered Index

  • Clustered Index
    • 이름 그대로 인접한 데이터들을 한곳으로 모았다는 뜻
    • PK 설정 시 자동으로 클러스터드 인덱스로 만들어짐
    • 테이블당 1개씩만 허용
    • 물리적인 데이터를 갖고 있음
    • 항상 정렬된 상태를 유지하고 노드 내에서도 정렬되어 있음
    • Non Clustered 인덱스에 비해 조회 속도가 빠르지만 삽입/수정/삭제는 더 느림
  • Non Clustered Index
    • UNIQUE 로 설정된 컬럼에 자동으로 생성됨
    • 인덱스 페이지는 로그 파일에 저장됨
    • 레코드의 원본은 정렬되지 않고 인덱스 페이지만 정렬됨

2.2. Primary Index vs Secondary Index

  • Primary Index
    • PK (기본키) 를 기반으로 만들어진 인덱스
    • PK 는 하나만 존재할 수 있기 때문에 Primary Index 도 단 하나만 존재
  • Secondary Index
    • 기본키는 아니지만 성능 향상을 위해 임의의 컬럼을 지정해서 만든 인덱스
    • 여러 개의 Secondary Index 가 존재할 수 있음

2.3. 자료구조에 따른 분류

  • B-Tree 인덱스
    • 가장 많이 사용되는 구조
  • Hash 인덱스
    • 컬럼 값을 해싱해서 사용하며 매우 빠른 검색 가능
    • 컬럼 값과 인덱스 키 값이 일치하지 않기 때문에 문자열 검색과 같이 일부 일치에 대해 검색 불가능
  • Fractal-Tree 인덱스
    • B-Tree 인덱스의 단점을 보완하기 위해 만듬
    • 컬럼 값을 변형하지 않으며 데이터의 저장/삭제 처리 비용을 많이 줄임
    • 아직 많이 사용되지는 않음

3. B-Tree 인덱스

Balanced Tree 의 약자로서 데이터베이스 인덱싱 알고리즘 가운데 가장 일반적으로 사용되는 알고리즘 입니다.

B-Tree 인덱스는 컬럼의 값을 변경시키지 않고 구조체 내에서 항상 정렬된 상태를 유지합니다.

B-Tree 는 최상위에 루트 (Root) 노드가 존재하고 하위에 브랜치 (Branch) 노드, 마지막에 리프 (Leaf) 노드로 되어 있습니다.

부모 노드를 기준으로 왼쪽 자식 노드는 더 작은 값 오른쪽 자식 노드는 더 큰값을 갖고 있습니다.


3.1. 왜 B-Tree 인가?

인덱스로 사용할 수 있는 자료구조는 여러 개가 있을 겁니다.

Tree 구조의 Worst 시간복잡도는 한쪽으로 모든 자식 노드가 쏠려있는 형태인 O(n) 입니다.

그래서 우리는 자식 노드가 양쪽에 골고루 퍼져있는 Balanced Tree 를 사용합니다.

Balanced Tree 중에는 RedBlack Tree 도 있는데 왜 사용하지 않을까요?

해시 테이블은 O(1) 인데 왜 사용하지 않을까요?


3.2. 다른 트리 구조를 사용하지 않는 이유

위에서 잠시 언급한 RedBlack Tree 는 B-Tree 와 마찬가지로 정렬된 상태와 밸런스를 유지합니다.

그럼 B-Tree 와 차이가 없을 것 같은데 왜 사용하지 않을까요?

가장 큰 차이점은 B-Tree 는 노드 하나에 여러 개의 데이터를 저장할 수 있다는 겁니다.

노드에서 배열 형태로 여러 데이터를 저장할 수 있기 때문에 트리 포인터를 참조해서 계속 depth 를 타고 들어가는 것보다 효율적이고 이는 데이터가 많아질수록 차이가 두드러집니다.


3.3. Hash 테이블을 사용하지 않는 이유

해시 테이블은 Hash 함수를 사용해서 키 값을 해싱한 후에 테이블에 저장합니다.

해시 테이블은 분명 한 가지 키에 대한 탐색은 효율적입니다.

하지만 데이터가 정렬되어 있지 않기 때문에 부등호 (<, >) 를 사용하지 못한다는 단점이 있습니다.


3.4. B-Tree 인덱스의 쿼리별 특징

  • SELECT
    • 특정 키 값을 찾기 위해 자식 노드를 계속 타고 들어가는 방식
    • 마지막 리프 노드에는 레코드의 주소가 존재하고 이 값으로 테이블 레코드를 찾을 수 있음
  • INSERT
    • B-Tree 에 새로운 키값을 저장할 때는 우선 적절한 위치를 찾아야함
    • 새로운 키값과 레코드 정보는 리프 노드에 저장
    • 만약 리프 노드가 꽉찼다면 트리를 재구성하여 리프 노드를 분리
    • 분리 과정에서 해당 리프 노드의 부모 노드까지 영향이 갈 수 있음
    • 이러한 이유로 INSERT 작업은 상대적으로 비용이 많이 듬
    • 인덱스가 많으면 많을수록 이런 비용이 추가로 들기 때문에 너무 많은 인덱스를 추가하는 건 성능에 영향을 줌
  • DELETE
    • B-Tree 에서 키 값 삭제는 간단
    • 해당 키를 찾아서 삭제 마크만 하면 작업이 완료
    • 삭제 마킹된 인덱스 키 공간은 그대로 두거나 재활용 가능
  • UPDATE
    • 인덱스는 항상 정렬된 상태로 유지됨
    • 단순히 인덱스 키 값을 수정한다면 트리의 전체 구조를 바꿔야 할 수도 있음
    • 그래서 B-Tree 에선 키 변경이 아닌 기존 키 삭제 (DELETE) 후 새로운 키 추가 (INSERT) 방식을 사용
    • 따라서 키 값의 잦은 수정은 성능에 큰 영향을 미침

4. 인덱스 설정 시 고려사항

인덱스는 조회 속도를 향상시키지만 어느정도 오버헤드가 존재하기 때문에 아무렇게나 추가해버리면 안됩니다.


4.1. 인덱스의 갯수

인덱스의 갯수는 3 ~ 4 개가 적당합니다.

인덱스의 갯수가 너무 많으면 다음과 같은 이슈가 존재합니다.

  • 데이터 삽입/수정/삭제 시마다 인덱스도 같이 추가하거나 수정/삭제 해주어야 해서 성능상 이슈가 존재
  • 데이터 삽입시마다 인덱스도 같이 추가하기 때문에 인덱스가 늘어날수록 더 많은 메모리를 차지함
  • 인덱스가 많아지면 옵티마이저가 잘못된 인덱스를 선택할 확률이 높아짐 (인덱스 힌트로 원하는 인덱스를 지정할 순 있음)

4.2. 인덱스를 걸기에 적절한 컬럼

인덱스의 갯수에 한계가 있다면 적절한 인덱스 컬럼을 정하는 것도 중요합니다.

인덱스는 카디널리티 (Cardinality) 가 높은 컬럼에 지정하는 게 좋습니다.

카디널리티가 높다는 말은 데이터의 중복이 적다는 뜻인데 대표적으로 ID, 주민번호 등이 있습니다.

반대로 성별 같은 중복된 데이터가 많은 경우 카디널리티가 낮다고 표현합니다.

성별에 인덱스를 거는 경우 인덱스를 타더라도 남/녀 두가지만 존재하기 때문에 결국 나머지 조건에 맞는 데이터는 직접 풀스캔을 해서 찾아야 합니다.

하지만 ID 같이 중복된 값이 없는 경우 해당하는 데이터를 빠르게 찾을 수 있습니다.


4.3. 읽어야 하는 레코드 갯수

인덱스는 일반적으로 단 하나의 데이터를 구할 때 가장 효율적입니다.

여러 개의 데이터를 구한다면 인덱스를 통해 레코드의 주소를 찾아 데이터의 레코드를 읽는 작업을 반복해야 합니다.

그래서 만약 많은 레코드를 한번에 조회한다면 오히려 인덱스를 사용하지 않고 직접 테이블을 읽는 것이 더 효율적일 수 있습니다.

예를 들어 테이블에 10 개의 데이터가 존재하고, 5건의 데이터를 읽는다고 가정한다면 인덱스를 통하는 것보다 테이블을 직접 읽는 게 효율적일 수도 있습니다.

일반적으로 DBMS 의 옵티마이저는 인덱스를 사용해 레코드 1건을 읽는 것이 테이블에서 직접 읽는 것보다 4 ~ 5배 정도 비용이 더 많이 든다고 예측합니다.

그러므로 인덱스를 통해 읽어야 할 레코드가 전체 테이블의 20 ~ 25% 이상이라면 직접 테이블을 읽는 것이 효율적입니다.

사실 인덱스가 걸려있어도 옵티마이저가 판단하여 더 효율적이라고 생각되는 방법을 선택하기 때문에 개발자가 직접 고려할 일은 없지만 기본적으로 알 고 있으면 좋습니다.


4.4. 복합 인덱스를 구성할 때

인덱스는 여러 개의 컬럼을 동시에 지정할 수도 있는데 어떤 순서로 구성하느냐에 따라 성능이 달라집니다.

위에서 인덱스는 트리 구조로 되어있다고 했는데, 여러 개의 컬럼을 함께 키 값으로 지정하는 경우 먼저 첫 번재 컬럼을 기준으로 정렬된 뒤에 두번째 컬럼이 정렬되어 있습니다.

이 말은 즉 첫 번째 컬럼 없이 두 번째 컬럼만 갖고 인덱스를 조회하면 제대로 된 위치를 찾을 수 없다는 뜻입니다.

그러므로 복합 인덱스를 구성했다면 조회할 때 앞 순서의 조건을 반드시 포함해야 인덱스를 태울 수 있습니다.

예를 들어 A, B, C 컬럼 순으로 인덱스를 구성했다면 A 라는 컬럼을 조회 조건에 포함해야 최소한의 인덱스를 태울 수 있고 B, C 컬럼만 조회 조건에 있다면 인덱스를 타지 못합니다. (정확히 말하면 인덱스 풀 스캔을 하는데 이런 경우 일반적으로 인덱스를 타지 않는다고 표현)

그리고 여러 개의 컬럼이 있다면 카디널리티가 높은 순에서 낮은 순으로 지정하는게 인덱스의 효율을 이끌어낼 수 있습니다.

그리고 과거에는 인덱스의 컬럼 순서와 조회 컬럼 순서를 맞춰야 인덱스를 탔지만 최근에는 옵티마이저가 알아서 인덱스 순서에 맞춰주기 때문에 거의 차이가 없습니다.

그래도 재배열하는 과정을 생략하기 위해 최대한 맞추는게 좋긴 합니다.


5. 인덱스 사용 시 주의사항

지금까지도 조금씩 언급했지만 인덱스는 사용할 대 주의할 점이 몇개 존재합니다.

  • 다중 인덱스를 사용할 때 범위 조건은 인덱스를 타지만 이후 컬럼들은 인덱스를 타지 않음
    • 범위 조건으로 사용할 때 주의
  • 인덱스로 지정한 컬럼은 그대로 사용해야 함
    • WHERE age * 10 > 20 처럼 조회조건에 있는 컬럼을 변경하면 안됨
    • WHERE age > 20 / 10 처럼 컬럼을 그대로 사용해야 함

6. InnoDB Adaptive Hash Index

MySQL 은 기본으로 InnoDB 를 사용하고 InnoDB 는 B-Tree 를 사용합니다.

PK 가 아닌 컬럼으로 인덱스를 지정하면 Secondary Index 가 생성됩니다.

그래서 인덱스로 컬럼을 조회하면 Secondary 인덱스를 기반으로 PK 를 찾은 뒤 다시 Primary Index 로 데이터를 찾아냅니다.

인덱스를 두번 타기 때문에 2 * O(log n) 비용이 듭니다.

그래서 자주 사용되는 컬럼을 해시로 정의해서 B-Tree 를 타지 않고 바로 데이터에 접근할 수 있게 하는 걸 Adaptive Hash Index 라고 합니다.

미리 캐싱한 해시값으로 조회하기 때문애 O(1) 의 속도를 보여주지만 어떤 값을 해싱할지는 옵티마이저가 판단하기 때문에 제어할 수 없다는 약점이 있습니다.


7. Covering Index

일반적으로 인덱스를 설계할 때는 WHERE 절에 대해서만 이야기하지만 사실 쿼리 전체에 대해 인덱스 설계가 필요합니다.

인덱스를 사용하면 특정 컬럼 값을 키로 하여 데이터의 주소값을 구한 뒤 해당 주소값으로 다시 테이블에 접근해서 최종 데이터를 구합니다.

커버링 인덱스란 인덱스에 이미 필요한 데이터가 전부 존재해서 테이블에 접근할 필요가 없는 인덱스를 의미합니다.

예를 들어 (A, B) 컬럼으로 인덱스를 구성한 경우 SELECT * FROM table WHERE A = ? 와 같은 형식으로 쿼리를 사용하면 인덱스를 탄 후에 전체 컬럼값을 모두 구하기 위해 테이블에 접근합니다.

하지만 SELECT A, B FROM table WHERE A = ? 와 같이 구하려는 컬럼값이 모두 인덱스에 이미 존재한다면 테이블에 다시 접근할 필요가 없습니다.

인덱스는 기본적으로 Non Clustered Index 에서 먼저 값을 구하고 Clustered Index 에서 다시 데이터를 구합니다.

여기서 커버링 인덱스가 사용되었다는건 Clustered Index 까지 통하지 않고 Non Clustered Index 만으로도 데이터를 구할 수 있다는 뜻입니다.

커버링 인덱스가 적용되면 EXPLAIN 실행 시 Extra 필드에 Using index 라고 표시됩니다.


Reference

'공부 > Database' 카테고리의 다른 글

MySQL Optimizer 와 USE INDEX vs FORCE INDEX  (0) 2022.06.21
Cache 전략  (0) 2022.05.20
Redis 설치 및 명령어  (0) 2021.08.07

+ Recent posts