토비의 스프링책을 읽던중에 정리해보고자한다

 

싱글톤이뭘까?? 용어가 궁금했다.

 

싱글톤패턴이란 인스턴스를 하나만 생성하여 사용하는 디자인패턴이라한다.

객체의 인스턴스가 JVM상에 하나만 존재한다는 뜻이다.

 

이러한 인스턴스가 왜하나만 존재하는지이유를 살펴보면

스프링 웹서비스를 기반에 둔 프레임워크이다.

서버클라이언트환경에서 여러개의 클라이언트가 동시다발적으로 서버에 객체 생성을 요구한다.

이로인해 서버에 부하가 발생할 가능성이커진다.

이를 방지하기위해 싱글톤 패턴으로 특정객체의 인스턴스를 한나만 생성시키고

이를 공유해 서버 부하 및 메모리 낭비를 방지한다.

 

이렇다면 하나의 인스턴스를 공유함으로서 서버부하를 줄일수있는 효과가 있을수있겠구나 생각이든다.

 

 

하지만 이러한 싱글톤도 문제가있다고한다.

바로 private !!

class suvCar {

   private static final INSTANCE = new suvCar();
   
   private static suvCar getInstance() { return INSTANCE; }
   
   private suvCar(){}
   
}

 

 

이렇게 싱글톤을 구현하게 되면 해당 Class는 private 생성자때문에 상속을 사용할수없게된다.

 

 

 

 

 

 

그래서 생긴게 싱글톤 레지스트리이다.

 

싱글톤패턴의 문제점 private 를 해결하고, 스프링 IoC컨테이너인 ApplicationContext가

Bean을 싱글톤 형태의 Object로 만들고 관리하는 기능을 말한다.

 

즉 스프링 프레임워크는 기본적으로 객체를 싱글톤으로 생성시킨다.

평소에 많이쓰는 예제 코드로 이해해보자

 

@Service
@RequiredArgsConstructor
public class MemberCreateService {

    private final MemberRepository memberRepository;

    public void createMember(MemberCreateRequest memberCreateRequest) {
    	String email = memberCreateRequest.getEmail();
        String pw = memberCreateRequest.getPw();

        if (!memberRepository.existsByEmail(email)) {
            memberRepository.save(new Member(email, pw));
            return;
        }
        throw new EmailDuplicationException();
    }

}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberCreateRequest {

    private String email;
    private String pw;
   
    @Builder
    public MemberCreateRequest(String email, String pw) {
        this.email = email;
        this.pw = pw;
    }
}

 

@Service로인해 MemberCreateService는 싱글톤 Scope를 가진다

즉 스프링에게 빈으로 등록함으로서 Ioc컨테이너가 알아서 관리하고 의존성주입을 해주면서

싱글톤 Scope를 가지게된다

 

 

1. 전역 변수영역에는 불변 객체인 Repository만 가지고있으며 상태정보를 갖지않는다 -> 무상태(stateless)방식, final, 필드내부변경x

2. 데이터의 변경은 메서드의 파라미터(MemberCreateRequest)를 이용하고있다 -> 파라미터로 Bean사이의 데이터주고받기, 데이터변경

 

 

 

지금까지 싱글톤과 싱글톤레지스트리에 대해 알아보았다

평소 쓰이던 코드에서 왜 이애노테이션사용하고 bean을 등록하는지에대해 이해해보는 시간이되었다

 

싱글페이지 애플리케이션이란 하나의 페이지로 이루어진 애플리케이션이라는 의미이다.

 

이를 이해하기위해선 싱글페이지 애플리케이션이란 개념이 생기기전에 사용되던 

멀티 페이지 애플리케이션이 어떻게 작동하는지 살펴봐야한다.

 

멀티페이지 애플리케이션에서는 사용자가 다른페이지로 이동할때마다 새로운 html을 받아오고, 페이지를 로딩할때마다

서버에서 CSS,JS, 이미지 파일등의 리소스를 전달받아 브라우저의 화면에 보여주었다.

각페이지마다 다른 html 파일을 만들어서 제공을 하거나, 데이터에 따라 유동적인 html을 생성해주는 템플릿 엔진을 사용하기도했다.

 

 

페이지마다 그에맞는 html 로딩

 

 

 

 

 

사용자 인터랙션이 별로 없는 정적인 페이지들은 기존의 방식이 적합하지만,

사용자 인터랙션이 많고 다양한 정보를 제공하는 모던 웹 애플리케이션은 이 방식이 적합하지 않는다. 

새로운 페이지를 보여줘야 할 때마다 서버측에서 모든 준비를 한다면 그만큼 서버의 자원을 사용하는것이고, 트래픽도 더 많이 나오게된다.

 

그래서 리액트 같은 라이브러리를 사용해서 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 우선 웹 애플리케이션을 브라우저에 불러와

실행시킨 다음 사용자와의 인터랙션이 발생하면 필요한부분만 자바스크립트를 사용하여 업데이트하는 방식을 사용하게 되었다.

만약 새로운 데이터가 필요하다면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에서 사용 할 수있게 되었다.

 

이렇게 html은 한번만 받아와서 웹 애플리케이션을 실행시킨 후,  이후에는 필요한 데이터만 받아와서 화면에 업데이트하는 것이 싱글 페이지 애플리케이션이다.

 

 

html한번로딩후 필요할때마다 javascript로 upDate

 

 

 

리액트 라우터같은 라우팅 시스템은 사용자의 브라우저 주소창의 경로에 따라 알맞은 페이지를 보여주는데, 이후 링크를 눌러서 다른 페이지로 이동할 때 서버에 다른 페이지의 html을 새로 요청하는 것이 아니라, 브라우저의 History API를 사용하여 브라우저의 주소창의 값만 변경하고 기존에 페이지에 띄었던 웹 애플리케이션을 그대로 유지하면서 라우팅 설정에 따라 또 다른 페이지를 보여주게 된다.

 

 

그래서 Single page application 인 spa라 용칭을 부르게된다.

 

프로젝트를하면서 js로 탭기능이 구현된상태에서  jsp에서 함수의 이름이 같을때

원하는 함수로 태우지않는 경우가있다.

 

function search() {


}

function paging() {

}

이렇게 사용하다보면 함수가 겹치는 경우가있어서 jsp화면에 원하는 함수를 태우지못하는 경우가있다

이유는 브라우저의 스코프는 공용공간으로 쓰이기 때문에 나중에 로딩된 같은 이름의 함수가 먼저 로딩된 함수를 덮어쓰게된다.

 

 

이러한 것을 방지하고자 js의 유효범위를 만들어 사용한다.

jsp화면의 특정 객체를 만들어 해당 객체에서 필요한 모든 function을 선언하는 것이다. 이렇게 되면 겹칠 위험이 사라진다.

 

var boardNew = {
        search : function (type, pageNo){
           

        },
        
        paging : function () {
        
        
        }
}


// 호출시 boardNew.search()
// 호출시 boardNew.paging()

 

이렇게하면 같은 함수를 사용하더라도 원하는 함수를 호출해서 사용할수있다.

다이나믹 프로그래밍 이란? 

주어진 문제를 여러개의 부분 문제들로 나누어 푼다음, 겹치는 문제의 경우 메모이제이션 기법을 사용하여 주어진 문제를 푼다

기억하며 풀기라고 한다.

 

사용하는이유

일반적인 재귀와 DP는 매우 유사하다. 차이점은 일반적인 재귀를 사용할 때는 동일한 작은 문제들이 여러 번 반복되어

비효율적인 계산이 될 수 있다는 것이다. DP는 Memoization 기법을 통해 반복되는 작은 문제들의 결과 값을 저장해 두고 재사용하여

계산 속도를 향상시킨다.

 

 

특징

분할가능 -> 큰문제들을 작은 문제로 나눈다. 작게 나눈 문제는 항상 같은 값을 도출해야한다.

부분 문제반복 -> subproblem 들이 겹칠 때 memoization을 통해 필요한 연산 수를 줄일 수 있다

최적 부분 구조 -> subproblem의 solution으로 더 큰 규모의  proplem의 solution을 구한다.

 

 

 

 

장단점

장점

모든 가능한 경우를 확인하기 때문에 근사치가 아닌 정확한 값을 얻을 수 있다.

 

단점

모든 가능한 경우를 확인하기 때문에 알고리즘에 비해 시간 복잡도가 크다.

 

 

 

코드

 

DP를이용한 피보나치의 Top down 방법

let fiboArr = [0];
let fiboWithMemoization = (n) => {
  if (n < 3) {
    fiboArr[n] = 1;
  }

  if (!fiboArr[n]) { // 내가 저장한 값 중에 없다면 ...
    // 재귀를 이용해 구하고 저장
    fiboArr[n] = fiboWithMemoization(n - 1) + fiboWithMemoization(n - 2);
  }

  return fiboArr[n];
};

해쉬란?

해쉬는 입력 데이터를 고정된 길이의 데이터로 변환된 값을 말한다. 다른 말로는 해시 값이라고한다.

 

 

사용하는경우 

배열의 인덱스위치, 위치, 데이터 값을 저장하거나 검색할때 활용된다.

 

특징 

키(KEY)에 데이터(VALUE)를 매핑할 수 있는 데이터 구조

해쉬 함수를 통해 키의 데이터를 배열에 저장할 수 있는 주소(인덱스 번호)를 계산

 

 

 

 

 

장단점

장점

데이터 저장/ 읽기 속도가 빠름 (검색속도가 빠름)

해시는 키에 대한 데이터가 있는지 확인이 쉬움

 

단점

일반적으로 저장곤간이 많이 필요

여러 키에 해당하는 주소(인덱스)가 동일한 경우 충돌을 해결하기 위한 별도 자료구조 필요

 

 

 

 

 

 

 

코드

class hashTable{
    constructor(size){
        this.storage = [];
        if(size){
            this.size = size;
        }
        else{
            this.size = 100;
        }
    }
    insert = (key,value) => {               
        let index = this.hash(key);
        
        if(this.storage[index] === undefined){
            this.storage[index] = [[key, value]];
        }
        else{
            let storageFlag = false;
            for(let i = 0; i < this.storage[index].length; i++){
                if(this.storage[index][i][0] === key){
                    this.storage[index][i][1] = value;
                    storageFlag = true;
                }
            }
            if(!storageFlag){
                this.storage[index].push([key,value]);
            }
        }
    }
    delete = (key) => {
        let index = this.hash(key);
        if(this.storage[index] === undefined){
            return false;
        }
        else if(this.storage[index].length === 1 && this.storage[index][0][0] === key){
            this.storage.splice(index,1);
            return true;
        }
        else{
            for(let i = 0; i < this.storage[index].length; i++){
                if(this.storage[index][i][0] === key){
                    this.storage[index].splice(i,1)
                    return true;
                }
            }
            return false;
        }
    }
    search = (key) => {
        let index = this.hash(key);
        if(this.storage[index] === undefined){
            return false;
        }
        else if(this.storage[index].length === 1 && this.storage[index][0][0] === key){
            return this.storage[index][0][1];
        }
        else{
            for(let i = 0; i < this.storage[index].length; i++){
                if(this.storage[index][i][0] === key){
                    return this.storage[index][i][1];
                }
            }
            return false;
        } 
    }

    hash = (key) => {
        let hash = 0;
        for(let i = 0; i < key.length; i++){
            hash += key.charCodeAt(i);
        }
        return hash % this.size;
    }

    getTable(){
        return this.storage;
    }

}

let data = new hashTable(100);
data.insert(1,5);
data.insert('asd', 12);
data.insert(213,14);
data.insert('a', 'b');
data.insert('213', '12');
console.log(data.search(1));
console.log(data.search(213));
data.delete(1);
console.log(data.search(1));
data.insert(1,10)
data.delete('a');
console.log(data.search(1));
console.log(data.getTable())

그리디 알고리즘이란? 

최적의 값을 구해야하는 상황에서 사용되는 방법으로

' 매선택에서 지금 이순간 당장 최적인 답을 선택하여 적합한 결과를 도출하자'  가 모토이다.

 

비유하자면 마시멜로 실험을 생각하면된다 -> 그리디 알고리즘을 사용한다는것은 당장 눈앞에있는 마시멜로를 먹는것이다.

하지만 이방법을 사용하는것은 기다렸다가 2개를 먹는다라는 최적해를 보장해주지 못한다

 

 

사용하는이유

항상 최적의 선택을하기때문에 앞서봤던 깊이우선탐색(dp) 보다 빠른 속도를 낸다

 

특징

최적부분 구조이다. -> 한번의 선택이 다음선택에는 전혀 무관한 값이며 매순간의 최적해가 문제에대한 최적해야여한다는의미이다.

 

 

 

 

그리디의 에니메이션 출처 : 위키피디아

 

 

원리

위의 그림을 보면서 아래 설명의 봐보자. 

1. 시작 정점을 7로 설정 (최대값을 구할때)

2. 3과 12중에 하나를 골라! 당장! 12가 크네 선택

3. 5과 6중에 하나를 골라! 당장! 6이 크네

 

이런식으로 탐색을 한다. 당장의 선택에만 선택하므로. 당장의이익만보는 그리디 알고리즘이라 하는것이다.

 

장단점

장점

간단하고 직관적인 방법이다.

또한 원리를 나열할때 3번에 끝난것을보아 계산 시간이 짧아서 대용량 데이터에도 적용이 가능하다

 

 

단점

최적화가 보장되지 않는다는 것이다. 위의 에니메이션을 보면 

맨아래노드의 최대값이 99인 큰값이 존재해서   7 ->3 -> 99 로 진행한다면 더 큰 최대값을 도출할수있는데

당장의 큰값만 선택하다보니 보다 작은 값이 도출되었음을 알 수 있다.

 

 

코드

function getChange(value) {
  let changes = [10000, 5000, 1000, 500, 100, 50, 10];
  let won = Math.floor(value / 10) * 10; // 1의 자리 원화 반내림
  let i = 0;
  let counts = [];

  while (true) {
    if (won >= changes[i]) {
      let count = Math.floor(won / changes[i]);
      won = won - changes[i] * count;
      counts[i] = count;
    } else {
      counts[i] = 0;
    }

    i++;
    if (won === 0) {
      for (let j = 0; j < changes.length - i; j++) {
        counts.push(0);
      }
      break;
    }
  }

  changes.map((change, index) => {
    console.log(`${change.toLocaleString()}원 ${counts[index]}개`);
  });
}

 

 

 

'프로그래밍 > algorithm' 카테고리의 다른 글

[알고리즘] Hash (해쉬)이란  (0) 2024.02.12
[알고리즘] DFS (깊이 우선탐색)이란  (1) 2024.02.11
Codewars 사용  (0) 2022.03.21

+ Recent posts