Intro

안정 해시는 수평적 확장을 고려할 때 각 서버에 균등하게 데이터를 나누기 위해 사용하는 방법.

해싱(Hashing)이란 무엇인가?

해싱이란 다양한 길이의 입력값들에 대해서 고정된 길이의 출력값을 생성해주는 과정을 뜻한다. 이 출력값을 우리는 해시값(hash value)라고 부른다. 그리고 이 과정을 수행하기 위해 진행하는 과정에 쓰이는 것을 해시 함수(hash function)이라고 한다.

원인

해시 키 재배치(refresh) 문제

- 총 4개의 서버

해시 서버
key0 18358617 1
key1 26143584 0
key2 18131146 2
key3 34085809 1
key4 35863496 0
key5 27581703 3
key6 38164978 2

 

- 총 3개의 서버(1개 서버 다운)

해시 서버
key0 18358617 0
key1 26143584 0
key2 18131146 1
key3 34085809 2
key4 35863496 1
key5 27581703 0
key6 38164978 1
  • 위 예제처럼 해시 값을 서버의 개수로 모듈러 연산을 진행하는 해시 함수를 사용해 키가 저장된 서버의 위치를 특정 짓는다고 가정해보자.
  • 그러면 1개의 서버가 다운될 경우 모듈러 연산을 하는 기준이 4개에서 3개로 변경되면서 각 키에 해당하는 서버 Index는 모두 꼬이게 된다.
  • 결론은 대규모 cache miss가 발생하게 되고 특정 서버에 몰리게 되면 과부하가 걸리며 서비스는 혼돈에 빠질 것이다..

해결 방안

안정 해시(Consistent Hash)

  • 작동 과정
    1. 해시 함수의 결과값에 대한 저장 공간 범위를 정한다. 이를 해시 공간(hash space)라 부른다.
    2. 이 공간을 해시 링으로 만든다.
    3. 모든 서버들을 해싱하고 동일하게 해시 링에 매핑 시킨다.
    4. 모든 키들도 3번과 동일하게 작업한다.
    5. 키들은 시계 방향으로 서버를 탐색한다.

Hash ring

HashRing

회색 글씨로 쓰인 점들을 Server가 매핑된 곳, 파란색 글씨로 쓰인 점들을 key값이라고 가정했을 때,

해시 서버
key-0 15000 1
key-1 25678 2
key-2 29536 2
key-3 33357 2
key-4 36458 3
key-5 39600 3
key-6 58765 0

위 표와 같이 서버에 key 값이 분배되는 것을 확인할 수 있다.

HashRing Server Down

  • 위 그림과 차이점은 S3 서버가 다운되어 없어졌다는 점이다. - hash ring을 사용한 안정 해시에서는 S2부터 S0까지의 키 값들만 재배치를 하면 된다.
해시 서버
key-0 15000 1
key-1 25678 2
key-2 29536 2
key-3 33357 2
key-4 36458 0
key-5 39600 0
key-6 58765 0
  • key-4와 key-5만 재배치가 완료되자 정상적으로 배정이 된 것을 확인할 수 있다.

안정 해시 문제점과 해결 방안

문제점

  1. 파티션 크기의 균등 유지
    • 서버가 삭제되거나 추가됐을 때 키의 재분배를 빠르게 처리가 가능하다는 장점이 있지만 해시 링 내에서 각 서버에 할당되는 해시 공간의 크기가 일정하지 않다는 문제점이 발생한다.
    • 그림을 참고하면 S3 서버가 삭제되면서 S2와 S0 서버 사이의 해시 공간의 범위가 늘어난 것을 확인할 수 있다.
  2. 균등 분포의 문제점
    • 위 그림 예시처럼 하나의 서버에 많은 키값이 저장될 수 있는 것에 더해서 특정 서버에는 어떠한 키값도 저장되지 않는 문제점이 발생할 수도 있다.

해결 방안

  • 가상 노드(virtual node)의 활용
    • 가상 노드를 각 서버마다 n개 생성해서 각 서버를 가르키도록 설정하고 해시 링 내에 매핑 시킨다.
    • 키 값들은 시계 방향 탐색을 진행하며 가상 노드를 마주칠 경우 해당 서버에 키 값을 저장시킨다.
  • 특징
    • 가상 노드의 개수가 많아질 수록 키의 분포는 더욱 균등해진다.
    • 하지만, 가상 노드의 개수가 많아질 수록 해당 가상 노드의 데이터를 저장할 공간 또한 필요하므로 메모리 쪽 효율은 떨어질 수 밖에 없음에 주의하자.

마무리

안정 해시가 필요한 이유는 핫스팟(hotspot) 키 문제를 줄일 수 있다는 점이다. 데이터베이스 내 특정 샤드에 접근이 지나치게 많이 이루어진다면 과부하가 발생할 수 밖에 없기 때문에 해당 문제점을 해결하는데 탁월하다.

어댑터 패턴

호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하기 위한 패턴

어댑터 패턴 특징

예시

  • 예를 들어, 해외여행을 가면 우리가 가진 장비들은 220V로 충전을 해야하지만 국가 별로 지원하는 전압은 다를 수 있다.
  • 이 때, 어댑터를 사용해서 호환되지 않는 것들끼리도 사용할 수 있도록 하는 것과 동일한 방식이다.
  • 동일하게 프로그램을 보면 이미 사용하고 있는 써드파티 라이브러리가 있지만 새로운 기능을 추가하기 위해 새로운 라이브러리를 적용하고 싶다고 가정해보자.
  • 그냥 끼울 수 있다면 최고겠지만 그렇지 못하다면 우리는 노가다를 통해 어거지로 새로운 라이브러리 기능을 우겨넣어야한다.
  • 미련하게 우겨넣지 말고 지성인답게 해결할 수 있는 방법이 바로 어댑터 패턴의 적용이다!

해결책

  1. 어댑터는 기존에 있던 객체와 호환되는 인터페이스를 받는다.
  2. 해당 인터페이스를 사용해서 기존 객체에서 어댑터 내에 새로운 메서드를 가져온다.
  3. 호출을 수신하면 기존 객체에 어댑터가 변환해서 해당 객체에 전달해준다.

어댑터 패턴 종류

  • 어댑터 패턴은 2가지로 나뉜다. 객체와 클래스 어댑터.
  • 주요 차이는 객체를 새롭게 만드느냐 혹은 만들지 않고 상속을 활용해서 메소드를 구현하느냐이다.

    객체


// client 메소드
public class Client {
    public static void main(String[] args) {
        Adapter adapter = new Adapter(new Service());

        adapter.method("https://dev-qhyun.tistory.com/");
    }
}

// client와 작업하기 위해 클래스들이 따라야하는 프로토콜
public interface ClientInterface {
    void method(String data);
}

/*
 클라이언트와 서비스 양쪽에서 동작할 수 있는 코드
 새로운 메서드를 클라이언트에서 사용할 수 있도록 도와준다.
 */
public class Adapter implements ClientInterface{
    Service adaptee; // 사용하고 싶은 객체를 선언

    public Adapter(Service adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void method(String data) {
        adaptee.specialMethod(data);  // adapter를 통해 Service 호출
    }
}

// 기존 시스템을 어댑터를 이용해 들어가려는 쪽
public class Service {
    void specialMethod(String data){
        System.out.print("남의 블로그 홍보 " + data);
    }
}

클래스


public class Client {
    public static void main(String[] args) {
        Adapter adapter = new Adapter();  // 객체는 adapter만 새로 생성한다.

        adapter.method("https://dev-qhyun.tistory.com/");
    }
}

public class Adapter extends Service implements ClientInterface {
    @Override
    public void method(String data) {
        specialMethod(data);
    }
}
  • 클라이언트 호출 부분과 어댑터 코드의 변경만 있었을 뿐이다.
  • 책에서 해당 방식은 다중 상속이 가능한 프로그래밍 언어에서 사용이 가능하다고 설명이 되어있고 클래스로 구현되어있었으나, 비교를 위해서 interface를 활용해서 코드를 작성했다.
  • 객체 생성을 따로 하지 않아도 된다는 점이 메모리 상 약간의 이점이 있는 것 같다.

패턴 적용

시기

  • 기존 클래스를 활용하고싶지만 다른 코드들과 호환되지 않는 경우
  • 부모 클래스에 추가할 수 없는 여러 기존 자식 클래스를 재사용하기 위해 사용

장단점

  1. 단일 책임 원칙 준수
  2. 개방/폐쇄의 원칙 준수

추상 팩토리 패턴

추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있도록 하는 생성패턴이다.

추상 팩토리 패턴 예시 (삼성 & 애플)

문제

  • 비교하기 쉽게 삼성과 애플을 예로 들어보도록 하자
  • 해당 제품들은 노트북, 핸드폰, 패드, 워치, 이어폰 등과 같이 관련 제품들로 형성된 제품군이 존재한다.
  • 맥북 생태계라는 말이 존재하듯 새로운 개별 제품 객체를 생성했을 때, 이 객체들이 기존의 같은 패밀리 내에 있는 다른 제품 객제들과 일치하는 스타일을 가지도록 해야한다.
  • 아이폰을 쓰면서 버즈를 쓰거나, 갤럭시를 쓰면서 애플워치를 찬다면 굉장히 어지러울 것이기 대문이다.

해결책

  1. 개별적인 인터페이스를 명시적으로 선언하는 방법의 사용
  • 위와 같이 같은 객체의 변형을 구현하였다면, 다음은 추상 팩토리 패턴을 적용하는 것이다.
  1. 제품 패밀리 내의 모든 개별 제품들의 생성 메서드들을 목록화시키기
  • 위와 같이 추상 팩토리 인터페이스를 기반으로 별도의 팩토리 클래스를 생성하여 각 팩토리는 특정 종류의 제품을 반환하게만 한다.
  • 삼성 공장에서는 삼성 제품만, 애플 공장에서는 애플 제품만 생성할 수 있듯이 말이다.

주의할 점

  • 클라이언트 코드는 추상 인터페이스를 통해 팩토리들과 제품들 모두와 함께 작동할 수 있도록 해야한다.
  • 그래서 클라이언트 코드는 변형되지 않고 자유자재로 변경할 수 있어야한다.

예시 마무리

  1. 책을 기준으로 추상 제품들은 Watch interface로 개별 연관 제품들의 집합에 대한 인터페이스를 뜻한다.
  2. 구상 제품들은 AppleWatch와 GalaxyWatch와 같이 추상 제품들의 다양한 구현들이다. 각 추상 제품은 주어진 모든 변형에 구현 되어야 한다.
  3. 추상 팩토리 인터페이스는 가각의 추상 제품들을 생성하기 위한 메서드 집합으로 ElectricFactory가 이에 해당한다.
  4. 구상 팩토리들은 AppleFactory와 SamsungFactory가 해당하며 해당 구상 팩토리에 생성 매서드들은 추상 제품을 반환해야한다.(Watch를 반환하는 것 처럼)
    이렇게 설계한다면 클라이언트 코드가 팩토리에서 받은 제품의 특정 변형(구상 제품들)과 결합되지 않고 어떤 팩토리나 제품과 작업할 수 있는 것이다.

간단한 전체 코드

코드는 아무런 로직 없이 껍데기만 대충 만들어봤다.


// 추상 제품
public interface Watch {
    void display();
    void strap();
}

// 애플 워치
public class AppleWatch implements Watch{
  @Override
  public void display() {

  }

  @Override
  public void strap() {

  }
}

// 갤럭시 워치
public class GalaxyWatch implements Watch{
  @Override
  public void display() {

  }

  @Override
  public void strap() {

  }
}

// 추상 팩토리
public interface ElectricFactory {
  void createPhone();
  void createDesktop();
  Watch createWatch();
  void createEarphone();
  void createPad();
}

// 애플 팩토리
public class AppleFactory implements ElectricFactory{
  @Override
  public void createPhone() {

  }

  @Override
  public void createDesktop() {

  }

  @Override
  public Watch createWatch() {
    return new AppleWatch();
  }

  @Override
  public void createEarphone() {

  }

  @Override
  public void createPad() {

  }
}

// 삼성 팩토리
public class SamsungFactory implements ElectricFactory{
  @Override
  public void createPhone() {

  }

  @Override
  public void createDesktop() {

  }

  @Override
  public Watch createWatch() {
    return new GalaxyWatch();
  }

  @Override
  public void createEarphone() {

  }

  @Override
  public void createPad() {

  }
}

+ Recent posts