Singleton 패턴

|

Singleton 패턴

  • 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.
  • 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.

자바에서 싱글턴 패턴을 구현할 때 고려해야 할 사항은 크게 2가지다. - 1. thread-safe. - 2. 메모리 낭비.

이를 위해 synchronized(동기화)와 lazy loading 기법을 사용하기도 하지만 개발자가 작성하는 코드에는 성능 및 정확성으로 인한 문제가 발생할 여지가 있다. (Double Checked Locking, lazy initialation 등등)

어떻게 하면 이러한 문제 발생의 여지를 없앨 수 있을까? 바로 클래스 로더와 lazy initialation을 이용하는 것이다.


public class Something {

    private Something(){
        System.out.println("Something Constructor is called");
    }

    static{
        System.out.println("Something static load");
    }

    private static class LazyHolder {

        public static final Something INSTANCE = new Something();

        static {
            System.out.println("Holder static load");
        }
    }

    public static Something getInstance(){
        return LazyHolder.INSTANCE;
    }

    public static void main(String[] args) {
        System.out.println("main() started");
        Something a = Something.getInstance();
    }
}

--- 실행결과 ---
Something static load
main() started
Something Constructor is called
Holder static load
--------------

클래스 로더는 크게 2가지 방식으로 클래스를 로딩한다.

- 1) 런타임 로딩 : 특정 클래스의 코드를 실행할 떄 클래스를 로딩한다.
- 2) 로드 타임 로딩 : 런타임 로딩에 의해서 클래스(A)가 로딩 될때, 해당 클래스(A) 내부에서 참조(사용)하는 클래스(B,C...)가 있다면 그 클래스도 로드하는 방법

Something 클래스로 예를 들어 설명해 보자면

  1. 해당 코드를 실행하면 main() 메서드가 존재하는 Something 클래스를 로딩한다. (런타임 로딩)
  2. main() 메서드가 실행된다.
  3. Something.getInstance() 메서드 호출에 의해 LazyHolder 클래스가 로딩된다. (런타임 로딩)
  4. Something() 생성자가 호출된다. (lazy loading)
  5. LazyHolder의 static 블록이 실행된다.

클래스가 로딩되는 시점은 ‘thread-safe’ 함을 JVM이 보장해주며 사용시점에 클래스가 로딩되므로 미리 생성하여 발생하는 메모리 낭비도 없다는 장점이 있어 Singleton 구현 시 가장 많이 사용되는 기법 중 하나이다.

해당 구현 기법은 ‘Bill Pugh Solution’ 이며 Enum을 이용한 싱글턴 구현 기법 또한 많이 사용된다고 하니 궁금한 사람은 찾아보면 좋을 것 같다.

ref. https://blog.seotory.com/post/2016/03/java-singleton-pattern
ref. https://javacan.tistory.com/entry/1
ref. https://jeong-pro.tistory.com/86
ref. https://limkydev.tistory.com/67
ref. https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4

Builder 패턴

|

빌더 패턴

‘생성자 인자가 많을 때에는 빌더 패턴을 고려해라’ - Effective Java

자바에서 객체를 생성하는 방법은 여러가지가 있다.

  • 점층적 생성자 패턴

public class Person {

    private String name; // 필수사항
    private String juminBunho; // 필수사항
    private int phone; // 선택사항
    private String address; // 선택사항

    public Person (String name, String juminBunho){
        this.name = name;
        this.juminBunho = juminBunho;
    }

    public Person(String name, String juminBunho, int phone) {
        this.name = name;
        this.juminBunho = juminBunho;
        this.phone = phone;
    }

    public Person(String name, String juminBunho, String address){
        this.name = name;
        this.juminBunho = juminBunho;
        this.address = address;
    }

    public Person(String name, String juminBunho, int phone, String address) {
        this.name = name;
        this.juminBunho = juminBunho;
        this.phone = phone;
        this.address = address;
    }
}


public static void main(String[] args){
    Person person = new Person("jay","920101-1111111"); // 원하는 생성자로 객체를 생성한다.
}


  • 자바빈 패턴 (getter와 setter를 이용한 패턴)

public static void main(String[] args){
    Person person = new Person();
    person.setName("jay");
    person.setJuminBunho("920101-1111111");
}

점층적 생성자 패턴의 경우 1) 멤버변수의 갯수에 비례하여 생성자가 많아져 추후에 인자가 추가될 경우 코드를 수정하는 데 많은 비용이 든다. 2) 인자가 많을 경우 코드 가독성이 떨어진다는 단점이 존재하고

자바빈 패턴의 경우에는 1) 점층적 생성자 패턴처럼 생성자를 여러개 만들지 않고 가독성이 높아졌으나 2) 생성자 1회 호출로 객체 생성을 끝낼 수 없어 객체 일관성이 일시적으로 꺠질수 있고 3) setter 메서드로 인하여 immuatble한 객체를 만들 수 없다.

이러한 단점들을 보완하기 위해 Builder 패턴이 존재한다.

  • Builder 패턴

public class Person {

    private String name; // 필수사항
    private String juminBunho; // 필수사항
    private int phone; // 선택사항
    private String address; // 선택사항

    // 내부 클래스인 'Builder'에서만 생성할 수 있도록 접근제어자를 private로 설정.
    private Person(Person.Builder builder){
        name = builder.name;
        juminBunho = builder.juminBunho;
        phone = builder.phone;
        address = builder.address;
    }

    public static class Builder{

        private String name; // 필수사항
        private String juminBunho; // 필수사항

        private int phone; // 선택사항
        private String address; // 선택사항

        public Builder(String name, String juminBunho){
            this.name = name;
            this.juminBunho = juminBunho;
        }

        public Builder Phone(int phone){
            this.phone = phone;
            return this;
        }

        public Builder Address(String address){
            this.address = address;
            return this;
        }

        public Person build(){
            return new Person(this);
        }
    }


    public static void main(String[] args) {

        // 1. chaining을 이용하지 않고 객체를 생성하는 법
        Person.Builder builder = new Person.Builder("jay","920101-1234567");
        builder.Address("Seoul");
        builder.Phone(12345678);
        Person jay = builder.build();

        // 2. chaining을 이용하여 객체를 생성하는 방법
        Person nora = new Builder("Nora","920101-1111111")
                .Address("Seoul")
                .Phone(12345678)
                .build();

    }

빌더 패턴은 1) 가독성이 다른 패턴에 비해 높고 (어떤 인자에 어떤 값을 있는지 한 눈에 알 수 있다) 2) immutable한 객체를 만들 수 있으며 3) 한 번에 객체를 생성하므로 객체 일관성이 꺠지지 않는다. 4) 또한 생성 전 build() 메서드에서 검증로직을 추가하여 유효성검사를 할 수도 있다.

ref. https://johngrib.github.io/wiki/builder-pattern/
ref. 자바 프로그래밍 면접 이렇게 준비한다

점근적 분석과 시간복잡도

|

알고리즘의 분석

  • 알고리즘의 자원사용량 분석
  • 자원이란 실행시간, 메모리, 저장장치, 통신 등을 의미한다.

포스팅에서는 공간(메모리)은 실행시간에 비해 상대적으로 저비용이므로 실행시간의 분석에 대해서만 다루겠다.

  • 시간복잡도

    1) 실행시간은 실행환경에 따라. 달라진다.

      - 하드웨어, 운영체제, 언어, 컴파일러 등등
    

    2) 실행시간을 측정하는 대신 연산의 실행 횟수를 카운트 하는게 더 합리적이다.

    3) 연산의 실행 횟수는 입력 데이터의 크기에 대한 함수로 표현한다.

    4) 데이터의 크기가 같더라도 실제 데이터에 따라 달라진다.

    • 예를 들어 정렬 알고리즘을 실행한다고 가정하면
      입력 데이터의 정렬 상태에 따라 연산 횟수가 달라진다.

      • 이 문제로 인해 ‘최악의 경우 시간복잡도’와 ‘평균 시간복잡도’를 사용한다.
      • ‘평균 시간복잡도’는 구하기가 쉽지 않은 경우가 많으므로 자주 사용하지 않는다.
      • ‘최악의 경우 시간복잡도’가 알고리즘 성능의 하한선을 보장 해주므로 주로 사용한다.
  • 점근적 분석

    1) 점근적 표기법을 사용한다.

      - 데이터의 개수 n->∞ 일때, 수행시간이 증가하는 growth rate로 시간복잡도를 표현하는 기법
          ex) 4n^2+5n+3 -> O(n^2), Θ(n^2)
    

    2) 유일한 분석법도 아니고 가장 좋은 분석법도 아니다. 다만 간단하며, 알고리즘의 실행환경에 비의존적이다.

  • 상수 시간복잡도


int sample(int data[], int n)
{
    int k = n/2;
    return data[k];
}


n에 상관없이 상수 시간이 소요된다. 시간 복잡도 : O(1)

  • 선형 시간복잡도

int sum(int data[],int n)
{
    int sum = 0;
    for (int i=0; i<n; i++)
    {
        // 이 알고리즘에서 가장 자주 실행되는 문장이며, 실행 횟수는 항상 n번이다.
        // 가장 자주 실행되는 문장의 실행 횟수가 n번이라면 모든 문장의 실행 횟수의 합은 n에 선형적으로 비례한다.
        sum = sum + data[i]; 
    }
    return sum;
}

시간 복잡도 : O(n)

  • 신형 시간복잡도(순차탐색)

int search(int n, int data[], int target)
{
    for(int i=0; i<n; i++)
    {
        if(data[i] == target)
            return i;
    }
    return -1;
}

이 경우 target과 n에 따라 시간복잡도가 달라진다. 최악의 경우는. data 배열의 마지막 인덱스가 target이 존재하는 경우이므로 최악의 경우 시간복잡도 : O(n)

  • Quadratic (2차함수)

boolean is_distinct(int n, int data[])
{
    for(int i=0; i<n-1; i++)
        for(int j=i+1; j<n; j++)
        {
            if(data[i] == data[j])
                return false;
        }
    return true;
}

최악의 경우 배열에 저장된 모든 원소쌍을 비교하므로 연산의 횟수는 n(n-1)/2. 최악의 경우 시간복잡도 : O(n^2)

HTTP,HTTPS와 SSL/TLS

|


HTTPS 란?

  • HyperText Transfer Protocol over Secure Socket Layer의 약자로서 HyperText인 HTML을 전송하기 위한 통신규약인 ‘HTTP’의 보안이 강화된 프로토콜을 의미한다. SSL/TLS 프로토콜을 통해 세션 데이터를 암호화하여 전송한다.

  • SSL/TLS 프로토콜이란?

    • 넷스케이프 사에서 웹서버와 클라이언트(브라우저)간의 보안을 위해 만들었으며, 공캐키/개인키, 대칭키 기반으로 사용한다.


SSL/TLS 프로토콜을 통해 어떻게 데이터가 암호화되고 전송될까?

서버와 클라이언트가 통신을 할 때는 크게 3단계로 나눠진다.

    1. handshake
    1. 전송
    1. 세션종료

과정을 설명하기 전에 알아야할 용어 몇 가지를 소개하겠다.

CA/Root Certificate

  • SSL/TLS 인증서를 발급하는 기관 (신뢰할 수 있는 제3자).
  • 브라우저들은 기본적인 CA 리스트를 내장하고 있다.


SSL/TLS 인증서

  • SSL/TLS 프로토콜의 핵심.
  • 클라이언트와 서버간의 통신을 제3자가 보증해주는 전자화된 문서이다.
  • 클라이언트가 서버에 접속한 직후에 서버는 클라이언트에게 이 인증서 정보를 전달한다.
  • 클라이언트가 접속한 서버가 신뢰할 수 있는 서버임을 보장한다.
  • 어떻게?
    1. 브라우저에 내장된 CA 리스트 확인
    2. 서버에서 받은 인증서의 발급자가 CA 리스트에 존재한다면 해당 서버는 신뢰할 수 있는 사이트이다. 신뢰할 수 있는 기관인 CA가 발급한 인증서이기 때문이다.


대칭 키

  • 서버와 클라이언트가 데이터를 암호화/복호화 할때 사용하는 키.
  • 키가 하나여서 상대방에서 전달해줘야 한다. 전달 시 키 자체는 암호화 되지 않으므로 노출될 가능성이 있다.


공개키/개인키

  • private key (개인키)와 public key(공개키) 쌍으로 존재한다.
  • 개인키로 암호화 -> 공개키로 복호화 또는 공개키로 암호화 -> 개인키로 복호화 만 가능하다.
  • 주로 서버에서 개인키를 가지고 클라이언트에게 공개키를 전달하는 방식으로 보안을 유지한다.
  • 공개키가 유출되더라도 개인키는 서버가 소유하고 있으므로 복호화 할 수 없다.


서버와 클라이언트간의 통신과정은 3단계로 나누어진다고 했다.

handshake -> 전송 -> 세션종료

    1. handshake

    첫 번째 과정인 handshake에서 데이터가 암호화 되어 통신할 준비가 끝이 난다.
    handshake를 해석하면 악수다.
    사람 간에도 처음에 만나 악수를 하며 서로를 살피는 것 처럼
    서버와 클라이언트도 처음에 handshake를 하는 과정을 하며
    상대방이 존재하는 지, 어떤 방식으로 데이터를 주고 받을지를 파악한다.

    1. 전송
    • handshake가 끝난 서버와 클라이언트는 자유롭게 데이터를 암호화하여 전송한다.
    1. 세션 종료
  • 데이터의 전송이 끝나면 SSL 통신이 끝났음을 서로에게 알려준다. 이 때 통신에서 사용한 대칭키인 세션키를 폐기한다.


ref. https://sites.google.com/site/tlsssloverview/ssl-tls-protocol-layers

ref. https://opentutorials.org/course/228/4894#signiture

ref. https://ko.wikipedia.org/wiki/HTTPS

Call By Value vs Call By Reference

|

Call by value (값에 의한 호출) 메서드를 호출하면 파라미터로 값을 복사해서 전달하는 방식을 의미한다.

Call by reference (참조에 의한 호출) 메서드를 호출하면 데이터 값이 아닌 주소값를 전달하는 호출 방법이다.

‘Call by value’와 ‘Call by reference’의 차이는 결국 값이냐 주소값이냐의 차이인데 말로 설명하면 이해가 쉽지 않으니 예제를 살펴보자.


typedef struct node{
    char *data;
    node *next;
} Node ;

void add_first(char *item, Node *head)
{
    Node *temp = (Node *)malloc(sizeof(Node*));
    temp->data = item;
    temp->next = head;
    head = temp;
}

int main(){

    Node * head;
    head = (Node *)malloc(sizeof(Node*));
    head->data="jay";
    head->next=NULL;

    add_first("Ann",head);    

    printf("%s",head->data);

    return 0;
}

add_first(“Ann”,head);가 호출된 후

printf(“%s”,head->data); 에 의해 출력되는 결과는

jay’ 일까 ? ‘Ann’ 일까?

정답은 ‘jay’ 이다.

그 이유는 C 언어는 기본적으로 ‘Call by value’로 동작하기 때문이다.

add_first()가 호출되는 과정을 좀 더 자세히 살펴보자.


int main(){

    Node * head;
    head = (Node *)malloc(sizeof(Node*));
    head->data="jay";
    head->next=NULL;
    
    // 1. add_first("Ann",head); 호출
    add_first("Ann",head); // 1000번지를 argument로 넘겨줌. (그림 1)

    // 2. add_first() 파라미터 'head'는 1000번지를 값으로 하는 포인터 변수로
    // add_first("Ann",head)에서 넘긴 argument 'head'와는 별개의 변수다.

    printf("%s",head->data);

    return 0;
}

void add_first(char *item, Node *head)
{
    Node *temp = (Node *)malloc(sizeof(Node*));
    temp->data = item;
    temp->next = head; // 그림 2
    head = temp; // 그림 3
}

< 그림 1 >

< 그림 2 >

< 그림 3 >

그럼 ‘Call by reference’는 어떻게 구현하고 동작할까?

add_first() 함수와 호출하는 부분을 조금 고쳐보자.


int main(){

    Node * head;
    head = (Node *)malloc(sizeof(Node*));
    head->data="jay";
    head->next=NULL;
    
    // head의 주소값 2000번지를 argument로 넘겨준다.
    // 그림 4
    add_first("Ann",&head); 

    printf("%s",head->data);

    return 0;
}
   
void add_first(char *item, Node **pre_head)
{
    Node *temp = (Node *)malloc(sizeof(Node*));
    temp->data = item;
    temp->next = *pre_head; // 그림 5
    *pre_head = temp; // 그림 6
}

< 그림 4 >

< 그림 5 >

< 그림 6 >

끝으로 각각의 장,단점을 정리해보자면

Call by value

장점 : 복사하여 처리하기 때문에 안전하다. 원래의 값이 보존이 된다.
단점 : 복사를 하기 때문에 메모리가 사용량이 늘어난다.

Call by reference

장점 : 복사하지 않고 직접 참조를 하기에 빠르다.
단점 : 직접 참조를 하기에 원래 값이 영향을 받는다.