Java8 에서는 기존의 Java 에 비해 다양한 기능들이 접목되었다. 

Optional 과 Lambda, Stream API 와 같은 기능들이 그것인데, 이는 새로이 대세로 떠오르고 있는 함수형 프로그래밍의 메타를 적용한 Java 의 새로운 진화라 할 수 있겠다.


Java8 에 대한 전반적인 소개는 다음 링크를 참조하자.

(http://jins-dev.tistory.com/entry/Java8-%EC%97%90%EC%84%9C-%EC%83%88%EB%A1%9C-%EC%83%9D%EA%B2%A8%EB%82%9C-API-%EB%93%A4?category=760006)


본 포스팅에서 집중해볼 것은 실무에서도 이제 널리 사용되고 있는 Stream API 에 대한 내용과 간단한 사용방법들이다.


 대부분의 이전 Java는 데이터를 처리하기 위해 컬렉션을 사용하고 이는 전통적인 반복문과 조건문 사용에 기인한다. 

컬렉션을 사용하여 할 수 있는 기능은 무한하고 강력하지만, 정작 데이터를 처리하는 패턴은 SQL에서 데이터를 찾는것과 비슷하게 데이터를 찾거나, 묶는것 정도에 국한된다.


 Java8의 Stream API 는 컬렉션을 이용했던 기존의 코드를 좀 더 깔끔하게 구현하고 병렬처리에 있어서도 이점을 가질 수 있도록 하기 위하여 제공된다. 

Stream API의 기본이 되는 원형 stream() 메서드는 모든 컬렉션 타입에 대해 제공되며, 컬렉션 내의 Element 들에 대해 하나씩 구분한 Stream 결과를 나타낸다. 


먼저 Stream API 가 어떻게 구성되는지 알아보자.

Stream API 는 생성연산, 중간연산, 최종연산으로 Flow 가 구성되어 있다. 생성연산은 스트림의 생성을, 중간 연산은 스트림을 통한 데이터 가공 및 변환을 담당하고 최종 연산은 Stream 의 사용을 담당한다. 다음은 연산별로 Stream API 를 정리한 내용이다.



순서대로 스트림 생성연산, 중간연산, 최종연산을 분류하였다.

Stream API 를 사용하기 위해서는 반드시 위의 Pipeline 을 순서대로 따라야 하며, 여러개의 중간연산을 붙이는 것도 가능하다. 다만, 생성연산과 중간연산들은 Stream 을 반환해야만 추가 연산이 가능하다는 점을 잊지 말자.

이를 이용해서 다음 리스트에서 원하는 정보를 추출해보자.


Stream API 를 이용하면 위의 리스트에서 70 이상의 숫자들을 정렬해서 다음과 같이 추출해낼 수 있다.



결과도 다음과 같이 확인해볼 수 있다.



코드 역시 상당히 직관적으로 이해할 수 있다. 코드상에서 우리는 numList 의 Stream 을 생성하여, 70 이상인 요소들만 필터링하여 정렬 후 Array 형태로 반환하고 있다.


이처럼 Stream API 를 사용하면 만들어진 스트림을 별도의 저장이나 가공없이 filter(속성에 맞게 필터링), sorted(정렬), map(정보를 추출), collect(정보를 가공) 할 수 있으며 이 메서드들은 collect에 의해 연속되어 처리된 결과를 리턴하거나 형변환, 또는 Stream 연산의 결과를 취합하여 반환한다. 


다음은 몇가지 자주 사용되는 연산에 대한 설명이다.

(1) map
함수 형태 : Stream<R> map(Function<? super T, ? extends R> mapper)
설명 :  T 타입 객체를 입력받아 R 타입을 반환하는 스트림 생성. 가장 많이 쓰이는 함수 중 하나이며, Stream 을 다른 형태의 Stream 으로 매핑시키는 역할을 한다.

(2) filter
함수 형태 : Stream<T> filter(Predicate<? super T> predicate)
설명 : Predicate(T 를 입력으로 받아 Boolean 을 반환하는 조건 식) 람다식이 true를 반환하는 새로운 스트림 생성

(3) flatMap
함수 형태 : Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
설명 : T 타입을 입력으로 받아, R 타입의 객체를 1:N 으로 매핑하는 스트림을 생성. 말그대로 평평하게(Flat) 만드는 함수이다.

(4) peek
함수 형태 : Stream<T> peek(Consumer<? super T> action)
설명 : T 타입의 매개변수를 입력받아 void 를 반환하는 함수를 실행하고 Stream 을 그대로 반환.

(5) limit
함수 형태 : Stream<T> limit(long maxSize)
설명 : maxSize까지의 Element 들만 반환하는 스트림 생성

(6) sorted
함수 형태 : sorted(), sorted(Comparator<T> comparator)
설명 : 정렬된 스트림을 생성. Integer 라면 자동정렬하지만, T 자료형에 대해 직접 Comparator 구현 필요하다. 
입력으로 주어지는 전체 스트림의 요소를 한꺼번에 정렬하기 때문에, 연속적으로 적용이 불가능하다.

(7) distinct
함수 형태 : distinct()
설명 : 값은 값을 갖는 요소를 중복해서 발생하지 않는 스트림 생성

8번부터는 Terminal Operation 들이다.

(8) forEach
함수 형태 : void forEach(Consumer<? super T> consumer)
설명 : T 타입을 입력으로 받아 void 를 수행하고 void 를 반환한다. for문과 동일하지만, 동작은 동일하지 않다. 
(forEach 는 Stream API 의 일부라는 점을 명심하자. 일반 for 구문과 다르게 PipeLine 을 충실히 따른다.)

(9) count
함수 형태 : count()
설명 : Stream 의 요소의 갯수를 반환한다.

(10) anyMatch
함수 형태 : boolean anyMatch(Predicate<? super T> predicate)
설명 : T를 입력받아 boolean 을 반환하는 predicate 람다식을 만족하는 항목이 Stream 에 하나라도 존재하는지를 반환한다.

(11) noneMatch
함수 형태 : boolean noneMatch(Predicate<? super T> predicate)
설명 : anyMatch 와 동일하지만 람다식을 만족하는 항목이 없는지를 반환한다.

(12) collect
함수 형태 : <R,A> R collect(Collector<? super T,A,R> collector)
설명 : Stream 을 Collector 형태로 치환한다. List 나 Map 등의 Collection 으로 변환하는 함수가 들어간다.


Stream API 를 사용할 때 꼭 알아두어야 할 점은 Stream은 요소들을 보관하지 않으며 요소들은 하부의 컬렉션에 보관되거나 필요할 때 생성된다는 점이다. 또한 원본을 변경하지 않는 대신 결과를 담은 새로운 스트림을 반환한다. 

스트림 연산은 가능하면 지연(Lazy) 처리 되기 때문에 결과가 필요하기 전에는(최종 연산 이전) 실행되지 않는다. 

따라서 최종연산 이전의 연산의 순서는 보장할 수 없다. (가령, Stream 연산에 2개 이상의 sorted 가 있을 경우 어떤 sorted 가 먼저 수행될지는 불분명하다.)


이 말을 다시한번 정리해보자. 스트림은 최대한 연산을 지연하며, 그에 따라 최종연산에 오기 전까지는 연산이 실제로 수행되지 않는다는 것이다.


이를 통해 잘 알아두어야 할 점은, 각 연산별 Pipeline 은 
절차적이지 않다는 점이다. 가령, stream 에 peek 을 통해 연산을 수행한 뒤 collect 로 취합하더라도 루프를 중첩해서 돌지 않는다. 
Stream 은 최종 연산 수행시 내부 파이프라인을 통해 최적화된 연산을 수행한다.

이처럼 Stream 은 각 요소 단위로 연산이 수행되는데 지연(Lazy) 처리 특성에 따라 최종연산에서 한꺼번에 처리가 되므로 최종연산이 선언되지 않은 체인 스트림에서 동작을 수행할 경우 이는 반영되지 않는다. 즉, 다음과 같은 코드는 아무 동작도 하지 않는다.



위의 연산은 peek 이라는 중개 연산을 마지막으로 구문을 마치고 있기 때문에 결과적으로 Stream 연산이 수행되지 않는다. 따라서 출력도 되지 않는다.


이러한 Stream의 특성을 Lazy & ShortCircuit 이라 하며 이는 Stream 이 Collection 과 다르게 그 자체만으로 자료구조가 아닌 연산을 위한 자료구조인 특성을 반영한다 


Stream 을 이용하면 가독성이 뛰어나고, 잘 사용하면 성능적으로도 뛰어난 결과를 가져올 수 있으며 쉽게 병렬 처리 환경으로 이식도 가능하다.

Stream 의 응용은 무궁무진하며 사용하기 위해서는 익숙해지는 것이 중요하다. 많이 사용해보고 적절하게 응용해보도록 하자.






순열탐색은 알고리즘 문제풀이나 실제 코딩에서도 상당히 많이 등장하는 알고리즘의 한 종류이다.

가령 명확한 기준을 갖고 일정한 순서로 전체를 탐색해야 하는 경우, 매우 유용하게 쓰일 수 있으며, 면접에서도 종종 등장하는 알고리즘 구현 문제이다.


순열 탐색을 하는 데 있어서 방법은 많은데, 포스팅에서 다룰 유명한 알고리즘이 있고, 이 방법을 모른다면 일반적으로 DFS나 BFS 를 이용해서도 구현할 수 있다. 하지만 많은 경우에 있어서 구현이 쉽지 않을 것이다.


이번에 다룰 알고리즘은 O(N) 의 시간복잡도로, 무작위 순열의 다음 순열을 구해내는 알고리즘이다.

또한 이를 바탕으로 무작위로 배치된 전체 수열의 모든 순열의 쌍을 구해보고자 한다.


모든 수열의 쌍을 구하는 알고리즘은 무작위 순열의 다음 순열을 구해낸다면 똑같이 적용하면 되므로, 다음 순열을 구하는 Next Permutation Algorithm 부터 정리해보자. 먼저 다음과 같은 수열을 예로 들어보자.



제시된 수열 N은 무작위의 5개 숫자를 갖고 있다. 이 때, 이 순열의 다음 순열을 구하고자 하면 상당히 난감하다. 전체를 탐색하는 알고리즘이 보통 먼저 떠오를텐데, 이는 개별 순열에 대해서는 굉장히 비효율적이다. 

(해당 알고리즘을 모듈화한다고 생각해보자. 함수에서 해당 모듈을 사용할 때마다 전체를 탐색할 수는 없는 일이다!)


Next Permutation 알고리즘은 다음과 같은 Greedy 방법으로 쉽게 다음 순열을 도출해낸다.


먼저, 순열의 전체를 순회하면서(O(n)) N[i] < N[i+1] 인 가장 마지막 i 를 구해낸다. 위의 예시에서 N[i] < N[i+1] 인 i 는 2개가 있지만, 가장 뒷번호의 인덱스를 가진 i 는 2이다. 만약 이때 i가 존재하지 않는다면, 해당 순열이 가장 마지막 순열이 된다.



그 다음에는 배열의 끝부터 좀전에 구해낸 i 까지 오면서 N[j] > N[i] 인 위치, 즉 N[i] 보다 큰 가장 마지막 Element 를 찾아내고, 이 위치를 J 라고 정한다. 이때, Greedy 알고리즘이 사용되는데, 좀 전 단계에서 N[i] < N[i+1] 인 위치를 찾고 i라고 정했기 때문에, J는 무조건 존재한다.  O(n)


다음에는 N[i] 와 N[j] 를 서로 스왑(Swap)해주자. O(1)


이제 남은 단계는 하나다. i+1부터 수열의 마지막까지 Reverse 해주면 된다. Reverse 한다는건, {1, 2, 3} 이라고 저장되어 있을 때 {3, 2, 1} 로 바꾸어 주면 된다는 뜻이다.


이렇게 나온 순열이 초기 순열(2, 5, 1, 9, 7)의 다음 순열이 된다. 간단하지 않은가. 이를 Java 코드로 옮겨보았다.



    /**
     * Find Next Permutation
     *
     * @param nums
     * @return
     */
    private int[] nextPermute(int[] nums) {
        int[] copies = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            copies[i] = nums[i];
        }
        int idx = -1;
        for (int i = 0; i < copies.length - 1; i++) {
            if (copies[i] < copies[i + 1]) {
                idx = i;
            }
        }
        if (idx < 0) {
            //Last Permutation
            return null;
        }
        for (int i = copies.length - 1; i > idx; i--) {
            if (copies[idx] < copies[i]) {
                int tmp = copies[idx];
                copies[idx] = copies[i];
                copies[i] = tmp;
                break;
            }
        }
        for (int i = idx + 1; i < (copies.length + idx + 1) / 2; i++) {
            int tmp = copies[i];
            copies[i] = copies[copies.length - (i - idx)];
            copies[copies.length - (i - idx)] = tmp;
        }
        return copies;
    }


위의 코드에서 idx가 예시의 i를 의미한다. 이를 이용하면 다음 순열을 O(N) 의 복잡도로 구할 수 있다. 이를 전체 순열 탐색에도 적용해보자.



    /**
     * Find Next Permutation
     *
     * @param nums
     * @return
     */
    private int[] nextPermute(int[] nums) {
        int[] copies = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            copies[i] = nums[i];
        }
        int idx = -1;
        for (int i = 0; i < copies.length - 1; i++) {
            if (copies[i] < copies[i + 1]) {
                idx = i;
            }
        }
        if (idx < 0) {
            //Last Permutation
            return null;
        }
        for (int i = copies.length - 1; i > idx; i--) {
            if (copies[idx] < copies[i]) {
                int tmp = copies[idx];
                copies[idx] = copies[i];
                copies[i] = tmp;
                break;
            }
        }
        for (int i = idx + 1; i < (copies.length + idx + 1) / 2; i++) {
            int tmp = copies[i];
            copies[i] = copies[copies.length - (i - idx)];
            copies[copies.length - (i - idx)] = tmp;
        }
        return copies;
    }

    /**
     * Find All Permutation
     *
     * @param nums
     * @return
     */
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> permutations = new ArrayList<>();
        Arrays.sort(nums);
        int[] curArray = nums;
        while (true) {
            List<Integer> integerList = new ArrayList<>();
            for (int i = 0; i < curArray.length; i++) {
                integerList.add(curArray[i]);
            }
            permutations.add(integerList);
            int[] nextPermutation = nextPermute(curArray);
            if (nextPermutation == null) {
                break;
            }
            curArray = nextPermutation;
        }
        return permutations;
    }



이제, permute 함수를 이용하면 무작위로 전달된 수열의 전체 순열을 순서대로 뽑아낼 수 있다. 결과가 많기 때문에 예시의 결과만 확인해보자.



Permute 함수의 결과로 나온 2차원 List 를 출력한 모습이다. 초기에 제시된 순열의 다음 순열을 성공적으로 구해내는 것을 확인할 수 있다.

Permute 함수는 아니겠으나, nextPermute 함수의 Next Permutation 알고리즘이 사용하는 방식은 C++ 의 STL 이 사용하는 수도코드와 같다. 최적화된 Greedy 알고리즘으로 생각된다. 좀 더 연구가 하고 싶다면 다음 자료를 참조하자.

(https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order)


순열의 순차탐색은 많이 사용하는 알고리즘이지만, 면접이나 코딩테스트와 같은 제한된 환경에서 최적화된 시간 복잡도를 요하는 바로 다음 순열을 구하기는 쉽지 않다.

어려운 알고리즘이 아니기 때문에 이해하고 외워두는 것도 좋다.



 시스템을 구성할 때, 특히 서버시스템이라면 장애(Fault) 에 대한 처리는 상당히 중요한 이슈이다. 

오랜시간동안 어떤 종류의 문제가 일어나도 문제가 일어나기 전과 후의 성능이 동일해야하며, 아웃풋 또한 동일해야 한다. 

이런 요건들이 마련되었을 때 해당 시스템을 내구력있는(Tolerant) 시스템이라 한다.


시스템을 구성하는 일부에서 fault / failure 발생해도 정상적 / 부분적으로 기능을 수행할 있는 시스템을 만들기 위해서 수행하는 복구의 종류로는 일반적으로 roll forward 복구, roll back 복구가 존재한다


Role forward 복구는 오류 발생 후의 상태에서 시스템을 복구하고 roll back 복구는 오류 발생 이전 check point 시스템을 복구한다.


Role Forward 복구와 Role Back 복구 모두, 복구 point 에러 발생 지점 간의 멱등성{몇번 실행해도 1 실행과 결과가 동일함이 보장되는 성질} 만족해야 한다.


이를 구현하기 위해서는 다음과 같은 테크닉이 주로 이용된다.



(1) 이중화 시스템

-  Replication :  동일 시스템을 복수로 준비하여 병렬로 실행시켜 다수를 만족한 결과를 올바른 결과로 적용한다.

-  Redundancy : 동일한 시스템을 복수로 준비하여 장애 발생시 보조 시스템으로 전환한다.

-  Diversity : 같은 사양에 다른 H/W 시스템을 복수로 준비하여 운용한다.

( 경우 시스템은 같은 장애를 만들어내지 않는다.)


(2) 동작지속 고장나도 지속적으로 동작해야 하며 복구 작업 성능 간섭이 없어야 한다.


(3) 고장분리 시스템이 고장과 분리되어 정상적인 구성요소에 영향을 주지 말아야 한다.


(4) 고장전염 고장이 전염되지 않게 유의해야 한다.



(1)번과 (3)번 원칙은 주로 설계에 대한 내용들이며, (2)번과 (4)번은 구현에도 직접적인 연관이 있는 편이다. 

서비스마다 차이가 있겠지만, 가능한 발생할 수 있는 "장애"의 단위를 격리시키고, 정상적인 결과를 낼 수 있는 Flow 를 고안하면서(설령 Error 가 Report 되더라도) 동시에 처리할 수 있는 "백신"을 모듈화시키는 것이 중요하다.



실무에서도 자주 사용하는 MySQL 쿼리문들을 정리해보았다. 많이 쓰는 기본 쿼리문 사용법들이므로, 필수적으로 알아두어야 한다.

응용만 한다면 어느정도 복잡한 쿼리문도 손쉽게 만들어낼 수 있다.


- 접속 방법

 : mysql -u root -p (dbname)


- 비밀번호 변경

 : mysqladmin -u root password 새로운 비밀번호


- 테이블의 생성

 : create table 테이블(col int);


- 구조 보기

 : desc 테이블 / explain 테이블


- 이름 변경

 : rename table A to B


- 삭제

 : drop table 테이블


- 레코드 삽입

 : Insert into table values(v1, v2) / Insert into table(col1, col2) values(v1, v2);


- 조회

 : select * from table A

> AS : 칼럼의 이름을 달리 명명해서 출력. (ex) Col1 as 'name'

> Desc : 내림차순, Asc : 오름차순 (ORDER BY)

> LIMIT 10 : 0~10 까지 레코드 수 제한. / LIMIT 100, 10 : 100~110까지 레코드 범위


- 수정

 : Update 테이블 set col1 = 칼럼1 where 조건


- 삭제

 : Delete from 테이블 where 조건


- 칼럼 추가

 : Alter table 테이블명 add col3 varchar(255) not null.


- 칼럼 삭제

 : Alter table 테이블명 drop col3


- 칼럼 수정

 : Alter table 테이블명 modify col3 char(50) not null.


- In : 원하는 필드값만을 선택 추출하는데 사용되는 그룹 조건문



- 조인

(1)   Inner join

 : Select * from tableA inner join tableB on tableA.col1 = tableB.col1

 => tableA의 col1과 tableB의 col1이 일치하는 데이터만을 출력. ON 절의 조건이 일치하는 조인테이블의 결과만을 출력한다.



(2)   Outer join

 : Select * from tableA left outer join tableB on tableA.col1 = tableB.col1

 => tableA.col1이 존재하나 tableB.col1이 존재하지 않으면 tableB.col1 = NULL인 상태로 출력. 

 조인하는 테이블의 ON 절 조건 중 한쪽의 모든 데이터를 가져옴(LEFT JOIN , RIGHT JOIN) 양쪽(FULL JOIN)



- 내장함수 Benchmark

 : Select Benchmark(반복횟수, 실행쿼리)

(ex) Select Benchmark(100, (“select * from table”)); => 해당 쿼리를 100번 반복한 벤치마크 결과를 출력.



- DISTINCT

 : 주로 UNIQUE한 COLUMN이나 TUPLE을 조회할 때 사용되는 키워드. 칼럼을 DISTINCT 를 이용하여 조회한다면 중복을 제거한 값들을 바로 얻을 수 있다. 단 이 때, 여러 개의 칼럼을 지정한다면 칼럼의 조합이 중복되는 것을 제외한다. DISTINCT는 함수처럼 WHERE이 아닌 HAVING 조건식에도 사용이 가능하다. 


(ex) Select DISTINCT email from table;


(ex) SELECT class FROM courses GROUP BY(class) HAVING count(distinct student) >= 5;



- GROUP BY

 : 데이터를 그루핑해서 결과를 가져오는 경우 사용. 내부적으로 중복값을 배제한채 정렬된 결과를 가져온다. 주로 HAVING과 같이 사용되며 그룹으로 묶어서 자체 정렬한다. 좀 더 정확히는 그룹의 대표값을 정렬해서 가져온다. 그렇기 때문에 모든 컬럼에 대해 단순 SELECT 하는 쿼리문에는 쓰기 적절치 않으며 테이블 내에서 데이터를 가공할 때 사용하기가 좋다. 예를 들어 accountType에 따라 해당하는 accountName의 row수를 그루핑 하고 싶다면 다음 쿼리를 사용해보자. 


Select accountType, COUNT(accountName) from accounts group by(accountType);



- HAVING

 : HAVING은 GROUP BY 와 같이 쓰이는 구문으로 GROUP BY의 조건문이라 할 수 있다. 위의 쿼리에서 COUNT가 1개 이상인 내용만 쿼리를 하는데 다음처럼 사용 가능하다. 


SELECT accountType, COUNT(accountName) FROM accounts GROUP BY(accountType) HAVING COUNT(accountName)>1; 


HAVING의 시점은 GROUPING이 끝난 이후이고 WHERE 절과 다르게 HAVING 절은 통계함수를 포함할 수 있다.

HAVING은 () 를 안 싸는 것이 좋다. 버전에 따라 오작동 위험이 있는듯하다 ;;



- SubQuery 사용법

 : 복잡한 쿼리문을 만들 때 많이 사용하게 되는 구문이 서브쿼리문이다. 서브쿼리의 사용은 Nested Loop 를 돌기 때문에 사용에 주의하자.


(ex) SELECT accountInfo from accounts where accountName in (select accountName from accountNames);


위의 쿼리는 accountNames 테이블에 있는 이름에 대해서만 accountInfo를 조회하는 쿼리(Validation)



기본적인 쿼리문들을 정리해보았다. 시간이 나면 Tricky 하게 사용되는 쿼리문들도 정리해볼 생각이다.





MySQL은 조회(SELECT) 에 있어서 최고의 강점을 갖는 종류의 DB이지만, 경우에 따라 막대한 양의 Insert 를 수행해야할 때가 있다.

특히 API 에서 조회하는 DB에 배치 서버가 수시로 데이터를 갱신해주는 경우나, 막대한 양의 메시지 데이터를 처리하게 될 경우가 있는데, DB가 커질 수록, 단순 쿼리로만 작업하다가는 퍼포먼스에 있어 엄청난 디버프를 받게 된다.


대량의 데이터를 삽입하는 것을 Bulk Inserting 이라 하며, 이를 위한 다음과 같은 튜닝 기술들 정도는 숙지해두도록 하자.


(1) 여러 개의 Insert 구문 수행시 Values 리스트를 다중으로 사용하는 것이 성능을 향상시킬 수 있다. 


정말 효과 제대로 본 방법인데, 다수의 동일한 Insert query에 대해서 다음과 같이 최적화가 가능하다.


Insert into T values(a, b, c)

Insert into T values(d, e, f)


위와 같이 쿼리를 날린다고 할때, 이를 다음과 같이 바꾼다.


Insert into T values(a, b, c), (d, e, f) ...


이렇게 되면 성능이 기하급수적으로 향상되며, 이는 JDBC 혹은 클라이언트 등의 연결 설정에서 RewriteBatchedStatements=true 와 같은 속성을 주어 자동으로 최적화시킬 수 있다.

(물론 설정의 경우 실수의 우려도 있고, 개발자가 한눈에 파악하기 쉽지 않기 때문에 협업 시에는 코드 레벨에서 처리하는게 더 좋다.)


또한 이 쿼리 튜닝을 사용할 때에는 비어있지 않은 Table에 Insert 시 my.cnf 파일 내에 있는 bulk_insert_buffer_size를 변경하여 속도 개선이 가능하다. 

(수정 방법은 /etc/mysql/my.cnf 파일을 열어서 [mysqld] 항목 아래에 bulk_insert_buffer_size=256M 과 같이 설정해주면 된다.)


(2) 여러 클라이언트에서 Insert 시 Insert Delayed 를 통해 속도 개선이 가능하다. 이 구문을 이용하면 수행 응답이 큐에 적재되고 테이블이 사용되지 않을 때 삽입한다. 

실시간으로 서비스 중인 경우에 빛을 발하는 성능 향상 법이다.


(3) 파일 스트림으로부터 대량의 데이터를 삽입 시 Load Data Local Infile 구문을 이용해서 필드 구문자로 정리된 File을 MySQL DB로 Redirection 시킬 수 있다. 

각각에 대한 Insert 구문 수행보다 빠른 퍼포먼스를 보인다.


 (ex) Delimeter가 "|" 이고 라인 개행 단위로 레코드가 기록되어 있을 때,


LOAD DATA LOCAL INFILE '경로' [REPLACE | IGNORE] INTO TABLE 테이블명 FIELDS TERMINATED BY '|' LINES TERMINATED BY '\n';


단, 이 경우 5.5 이상 버전에서는 LOCAL을 꼭 붙여야 에러가 나지 않으며, 이를 이용하기 위해 설정 옵션에 [mysql] 아래에 local-infile을, [mysqld] 아래에 local-infile을 추가해주어야 한다.


(4) Index가 많이 사용된 테이블에 대량으로 Insert 시 Index를 비활성화한 후 수행하는 것이 좋다. 


매 Row 마다 계산 후 삽입하는 것보다 전체 키를 비활성화하고 데이터 입력이 종료되고 다시 키를 활성화시키는 것이 빠른 수행을 할 수 있다.


ALTER TABLE 테이블 DISABLE KEYS;

Insert 구문 수행

ALTER TABLE 테이블 ENABLE KEYS;


여기서 Key 란 INDEX 뿐 아니라 FOREIGN KEY 참조도 포함한다. 실제로 Bulk Inserting 이 중요하다면, 체크를 꺼두는 것이 성능 향상에 큰 도움이 된다.

물론 이 경우 단점은, API 로직상 에러가 발생하여 문제가 될 수 있는 데이터가 삽입될 시, 실시간으로 정합성 체크가 불가능하다. 데이터를 신뢰할 경우에만 수행해야 한다.


(5) 트랜잭션을 지원하는 테이블의 경우, Start Transaction과 Commit을, 지원하지 않는 테이블의 경우 테이블 잠금을 실행하면 성능이 향상된다. 

(이유는 버퍼 플러시가 매번 수행되지 않고 작업이 끝난 후 수행되기 때문이다.)


(6) Buffer Size 의 조절을 통해 성능 향상이 가능하다.

테이블에 Insert 시 Index가 정렬되어 들어온다는 보장이 없기 때문에 이는 B트리 구조화시 추가적인 I/O를 수반한다. 

디스크에 데이터를 읽고 쓰고 하는 부가적인 동작이 레코드가 많아질수록, Buffer 크기를 넘어설수록 많이 수반되기 때문이다. 

이를 막기 위해서 InnoDB의 경우 Buffer Size를 크게 잡는다면 이 만큼의 메모리를 추가로 캐싱용 버퍼풀을 위해 사용하는 대신 디스크 I/O의 비용을 높은 비율로 줄일 수 있다. 


잔여 메모리의 50~80% 만큼 조절하는 게 정석이며 그 이상으로 조절 시 오히려 하드디스크를 가상 메모리로 쓰기 위한 스와핑 작업이 발생하기 때문에 성능이 저하된다고 알려져있다. 

(실제로 이부분은 범위 내라면, 조절하는 만큼 성능이 향상된다. 바꾸기 위해서는 my.cnf 설정 내에서 [mysql] 하위 항목으로 innodb_buffer_pool_size=1024M 과 같이 입력하면 된다.)


버퍼 풀 메모리가 충분히 큰 양으로 할당되어 있다면 innodb는 in-memory 데이터베이스처럼 동작한다. 

Access를 위한 select 데이터 뿐 아니라, Insert 및 Update 작업에도 도움이 되는 캐싱을 하기 때문에 적절하 조정하여 사용하는 것이 핵심이다.


버퍼 풀 메모리는 내부적으로 LRU 알고리즘을 사용하는 리스트의 형태로 자세한 내용은 http://dev.mysql.com 레퍼런스를 참조한다.





 MySQL의 TIMESTAMP/DATETIME 자료형과 CURRENT_TIMESTAMP 모듈을 이용하면 손쉽게 레코드의 삽입 시간과 수정 시간을 파일 시스템처럼 관리할 수 있다.

가령 게시판을 만든다고 해보자. 게시판에 유저들이 글을 쓰는데, 등록시간과 수정시간을 기록하는 건 굉장히 중요한 서비스이다.

이 때, 당연히 사용자에게 등록시간과 수정시간을 하나하나 입력받을 수는 없는 노릇이다.

혹은 DB 에 쿼리를 날릴 때 웹에서 직접 현재 시간을 기록하게 해주어도 괜찮지만, DB 단에서 처리할 수 있는 다음과 같은 방법도 있다.



CREATE TABLE blog_text (
`text_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL,
`contents` varchar(255) NOT NULL,
`reg_date` DATETIME DEFAULT CURRENT_TIMESTAMP,
`mod_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(text_id)
);


 CURRENT_TIMESTAMP는 데이터 접근 시의 당시 현재 시간을 나타내는 모듈로, 위처럼 테이블을 생성하면 기본으로 레코드는 생성시 현재 시간을 갖게 된다.(미입력시의 초기값) 

그리고 ON UPDATE 키워드에 의해 갱신 시마다 Updated 칼럼의 Value는 해당 시점의 현재 시간으로 Update 된다.


(1) Insert into blog_text(name, contents) values(‘tester’, ‘안녕하세요. Korean Developers’);

1 | "tester" | "안녕하세요. Korean Developers" | 2018-08-27 09:25:58 | 2018-08-27 09:25:58


(2) Update blog_text set `contents` = "안녕하세요." where text_id = 1;

1 | "jinsp" | "안녕하세요." | 2018-08-27 09:25:58 | 2018-08-27 09:27:51


위와 같이 DB레벨에서 좀 더 쉽고 명확하게 등록시간 / 수정시간을 관리하는 것이 가능하다.

응용하면 다양한 방법으로 Tricky 하게 사용할 수도 있다.

물론 실무에서는 배치 작업이나 자동화된 로그성 데이터들이 아닌 관리해야하는 데이터의 경우 API 단에서 처리하는 경우가 더 많다.


 에버그린 브라우저(Evergreen Browser), 자동적으로 브라우저가 사용자에 대한 별도의 재설치를 요구하지 않고도 업데이트 가 가능한 브라우저를 말한다


 초기 웹브라우저들은 Evergreen 방식을 사용하지 않으나, Web 기술의 발전에 따라 사용자의 편의성과 업데이트를 위해 설계된 방식이다.

Chrome은 대표적인 에버그린 브라우저이며, Firefox나 근래의 IE와 같은 많은 주요 브라우저들이 Evergreen 방식의 업데이트로 브라우저를 제공하고 있다.


 많은 개발자들이 인지하기 힘든 빠른 업데이트 정책에 대해 개발하는 데 있어서 호환의 어려움을 얘기하지만, 많은 브라우저들이 하위 호환성에 대한 고려는 충분히 하고 있기 때문에 우려할 수준은 아닌 것으로 보인다.

 

 웹 개발자들이 개발을 염두해둘 때, Javascript 신기술을 도입하는 데 있어서 체크해야할 사항이 되기도 한다. 

특히 ES6 와 같은 최신 Javascript 가 내장된 브라우저를 찾는 일을 하게 되는데, 주요 에버그린브라우저에서 사용할 목적이라면 브라우저가 알아서 Javascript 를 지원하는 업데이트를 해주기 때문에 이는 큰 걱정거리가 안된다.


두 테이블의 참조 관계에서, A B테이블의 관계가 1:N Relationship을 맺을 때, A 테이블의 해당 키를 참조키(Reference Key)라 하고, B 테이블의 해당 키를 외래키(Foreign Key)라 한다

ON DELETE/UPDATE 시 적용될 Cascade 의 종류는 다음과 같이 분류된다.


(1)  CASCADE : 참조키가 삭제/수정 되면 외래키도 삭제/수정된다.


(2)  RESTRICT : 참조키가 삭제/수정 되는 것을 방지한다.


(3)  SET NULL : 참조키가 삭제/수정 시 NULL로 만든다.


(4)  NO ACTION : 참조키가 삭제/수정 시 변동이 안생긴다.


실무에서는 물리적으로 FK를 적용하는 경우가 드물기 때문에 복잡한 종류의 마스터 데이터이거나 비즈니스 로직보다 데이터의 일관성이 중요한 경우가 아니라면 잘 사용은 안한다. 

하지만 물리적으로 연결하게 된다면 상황에 따라 FK 참조 방식을 설정할 때가 있다. 잘 알아두어서 필요할 때 적절하게 사용할 필요가 있다.



VPC(Virtual Private Cloud)란 AWS 계정 전용 가상네트워크로 AWS클라우드에서 다른 가상 네트워크와 논리적으로 분리되어 있다



위의 그림은 흔하게 볼 수 있는 AWS 를 이용한 웹서비스 아키텍처의 모습으로, 여러대의 EC2 를 각각의 Private Network 로 묶고, 각 VPC 를 Router 를 통해서 인터넷에 연결시키는 모습이다.

위와 같이 VPC의 구성 시 웹서비스 아키텍처에 필요한만큼 서비스를 구성할 수 있으며, IP 주소 범위, 서브넷, 라우팅 테이블, 네트워크 게이트웨이 및 보안 설정과 같은 서비스를 제공한다.

VPC 내부의 각 Instance 들은 Private IP Public IP를 갖으며 이는 main routing table에 의해 게이트웨이를 거쳐 인터넷과 연결하며 이 부분을 EC2 네트워크 엣지라 한다.


 기본 VPC 구축 시, 인터넷 게이트웨이가 포함되며 각각 기본 Subnet Public subnet으로 구성이 된다Internet에 붙어야 한다면 Public, 연결되지 않는다면 Private subnet을 사용해도 무방하다

 이 때, 기본이 아닌 VPC 구축 시 Private IP만 존재하고 Public IP가 없기 때문에 서로는 통신할 수 있으나 Internet에 연결할 수 없다. 이 때 게이트웨이를 추가하고 EIP(Elastic IP)를 연결하여 인터넷 사용이 가능하다.


따라서 위의 그림과 같은 아키텍처를 만들기 위해서는 각 VPC 를 Private으로 구성하고 앞에 NAT 역할을 위해 Public 연결을 지원하는 EIP를 가진 게이트웨이를 두거나 VPC 자체를 Public 으로 구성하는 설계 2가지가 가능하다.


VPC의 Subnet에서는 방화벽과 ACL(네트워크 접근 제어 목록) 을 통한 보안 계층을 지원한다.


VPC에서 Instance 시작시 다음과 같은 장점이 있다.


(1)  인스턴스 시작 및 중지에 상관없이 유지되는 고정 IP


(2)  다수의 IP 할당 가능


(3)  네트워크 인터페이스 및 보안 그룹 설정 가능


(4)  Instance Inbound traffic 제어 (Ingress Filtering) Outbound traffic 제어(Egress Filtering) 가능


(5)  ACL을 통한 접근 제어 가능


실무에서 클라우드를 사용하게 된다면 VPC를 구성하지 않는 경우는 거의 없다고 봐도 무방하다. 간단한 개념이고 설정도 쉽지만 잘 숙지해놓자.

 ACID는 Atomicity, Consistency, Isolation, Durability 의 약자로 Transaction이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.


트랜잭션의 처리는 Index 의 업데이트 와 같은 신경써주어야 하는 많은 변화를 수반하기 때문에 복잡한 문제이다.

ACID 원칙은 복잡하고 에러 발생 가능성이 높은 트랜잭션 상황에서도 반드시 보장해 줄 수 있는 Database 가 트랜잭션을 지원한다면 가져야 하는 성질이다.


Atomicity : 원자성은 Transaction과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 원자성은 중간 단계까지 실행되다가 실패하는 일이 없도록 하는 것이다. All or Nothing. 즉, 모든 작업이 성공하거나 실패한다.


- Consistency : 일관성은 Transaction이 실행을 성공적으로 완료하면 언제나 일관성있는 Valid 한 DB 상태를 유지하는 것을 의미한다. 여기서 Valid 한 상태는 트랜잭션의 결과로 업데이트된 데이터가 각종 Constraints 및 Rule 을 위반하지 않는 것을 의미한다. 무결성 제약에 위반하는 Transaction은 중단된다.


- Isolation : 고립성은 Transaction을 수행 시 다른 Transaction의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이는 Transaction 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 또한 고립성은 Transaction 실행 내역이 연속적이어야함을 의미하고, 유연성있는 제약조건이다.


- Durability : 지속성은 성공적으로 수행된 Transaction은 영원히 반영되어야 함을 의미한다. System 문제나 DB 일관성 체크 등을 하더라도 유지되어야 하고, 모든 Transaction은 로그로 남긴 채로 Rollback도 가능하다. 

Transaction은 로그에 모든 것이 저장된 후에만 Commit 상태로 간주한다.


Database는 트랜잭션에 연관된 모든 연산들을 한번에 실행하기 쉽지 않다. 

서로 강하게 결합되어 있는 연산들의 정렬 기준이 복잡하기 때문이다. 이 문제를 해결하기 위하여 DB는 연산의 처리 시 로깅을 이용하여 모든 작업에 대한 변경사항을 로그로 기록한다. 


ACID 를 보장하기 위한 방법으로 처리하는 데이터에 대해 Lock을 걸어서 관리하거나 MVCC라 하여 데이터의 복사본을 만들어두어 동시 처리 후 충돌을 후처리 하는 방안이 있다.




+ Recent posts