동기적 실행 ( syschronous execution )

  • 서브루틴 간에 명확한 실행 순서 존재
  • A,B,C의 세 가지 서브루틴이 존재하고 A,B,C 순으로 실행되기를 기대한다면 반드시 A->B->C 순서로 실행되어야 
    한다. 즉, B는 A가 실행 완료되기를 기다리고, C는 A와 B가 실행 완료되기를 기다린다.

동기적 실행은 프로그래밍을 처음 배울 때부터 익숙한 개념이다. 일반적으로 코드는 1번째 줄 -> 2번째 줄 ... 식으로
line by line으로 실행되며, 이 순서가 뒤바뀌지 않는다. 

 

비동기적 실행 ( asynchronous execution )

  • 서브루틴 간에 명확한 실행 순서 존재 X
  • A->B->C의 순서로 실행될 수도 있고, B->C->A의 순서로 실행될 수도 있다. 
  • 이런 경우 서브루틴이 완료되었다는 것을 전달하기 위해 callback 패턴을 사용하기도 한다.

 

비동기적 실행은 실행이 뒤바뀔 수 있다. ( 물론 line 단위가 아니라 함수 단위로 비동기화를 한다. )
비동기적 실행은 프로그래밍 시에 구조를 잘 짜 놓지 않으면 예기치 못한 버그가 발생하게 된다. 예를 들어 서로 다른
서브루틴에서 동시에 data write를 시도한다면, 실행 순서에 따라 동작이 달라질 수 있다. 

비동기 기법은 특히 웹 프로그래밍에 매우 유용하다. 웹 앱이 브라우저에서 특정 코드를 실행하느라 브라우저에게
제어권을 돌려주지 않으면 브라우저는 마치 정지된 것처럼 보일 수 있다. 이러한 현상을 blocking이라고 한다.

 

멀티 스레드/코어가 일반화된 현 시점에서는 적절한 비동기화 구조는 상당한 성능 향상을 누릴 수 있다.

 

 

혼동하지 말아야 할 것

  • 동기/비동기적 실행은 멀티 스레딩과 전혀 관련이 없다.
    => 즉 하나의 스레드에서도 비동기적 실행을 할 수 있고, 멀티 스레드에서도 동기적 실행을 할 수 있다.
  • 스레드 or 코어의 개수가 중요한 것이 아니라 서브루틴 간의 실행 순서가 정해져 있는지가 중요하다.
  • 멀티쓰레드 프로그래밍에서 스레드간 크리티컬 섹션 문제 방지를 위한 동기화랑 여기서의 동기/비동기는 다르다!!

 

 

동기와 비동기의 동작 방식

 

동기와 비동기 예시

이렇게 해도 이해가 안 갈 수 있기 때문에 간단한 예시를 들겠다.

 

직원 - 서버, 손님 - 클라이언트

 

1. single thread - 동기 

  •    은행에서 직원 1명이 여러 손님들의 처리가 끝날 때까지 손님은 자기 순서가 올 때까지 줄서서 대기

2. multi thread - 동기

  •    은행에서 직원을 손님 수만큼 증가시킨다. 손님이 없을 때는? -> 비용 낭비

3. single thread - 비동기

  •    은행에서 직원 한명이 일을 보고 손님은 번호표를 뽑고 다른 일 처리

4. multi thread - 비동기

  •    손님의 수 만큼은 아니고 3~4명의 직원이 번호표 처리

 

솔직히 성능으로만 본다면 4번이 최고이나 무조건 4번만 하진 않는다.
왜냐하면 코드 구현이나 성능에 들어가는 비용이 많이 들고 복잡해 지기 때문이다.
그래서 적절하게 상황에 맞게 써야 한다.

 

실제로 웹의 경우는 언제 어디서 누가 몇 명이 어떤 순서로 들어올지 모르는 환경이다. 비동기 통신이기 때문이다.
그렇기 때문에 비용과 성능 측면에서 비동기 방식을 택하게 되는 것이다.

1 : 1 관계 (일대일 관계)

  • 어느 엔티티 쪽에서 상대 엔티티와 단 하나의 관계를 가지는 것

 

예를 들어 우리나라는 일부일처제로 한 남자는 한 여자와, 한 여자는 한 남자와 밖에 결혼을 할 수 없다.

남편 또는 부인을 2명 이상 둘 수 없는데, 이러한 관계가 1 : 1관계다.

 

1 : N 관계 (일대다 관계)

  • 한 쪽 엔티티가 관계를 맺은 엔티티 쪽의 여러 객체를 가질 수 있는 것

 

현실에서는 1:N 관계가 많은데, 실제 DB를 설계할 때 자주 쓰이는 방식이다.

1:N 관계는 M:N처럼 새로운 테이블을 만들지 않는다.

예를 들어 부모와 자식 관계를 생각해보면, 부모는 자식을 1명, 2명, 3명 , 그 이상도 가질 수 있다. 이를 부모가
자식을 소유한다고( has a 관계 ) 표현한다.

 

반대로 자식 입장에서는 부모를 하나만 가질 수 밖에 없다.
이러한 관계를 1:N 관계라고 한다.


부모 테이블에서는 내 자식들이 누구인지 정보를 넣을 필요가 없고, 자식 테이블에서만 각각의 자식들이
자신의 부모 정보를 FK로 넣음으로써 관계를 표현한다.

 

 

 

M : N 관계 (다대다 관계)

  • 관계를 가진 양쪽 엔티티 모두에서 서로가 서로를 1:N 관계로 보고 있는 것

 

예를 들어, 학원과 학생의 관계를 생각해보면, 한 학원에는 여러명의 학생이 수강할 수 있으므로 1:N 관계를 가진다.
반대로 학생도 여러개의 학원을 수강할 수 있으므로, 이 사이에서도 1:M 관계를 가진다.
그러므로 학원과 학생은 M:N관계를 가진다고 할 수 있다.

 

M:N 관계는 서로가 서로를 1:N 관계로 갖고 있기 때문에 서로의 PK를 자신의 FK로 갖고 있어야 한다.

일반적으로 M:N관계는 두 테이블의 PK를 복합키로 갖는 또 다른 테이블을 생성해서 관리한다.

 

'DB' 카테고리의 다른 글

SQL INNER JOIN과 OUTER JOIN  (0) 2022.02.24

JOIN

  • 여러 테이블에 흩어져 있는 정보 중 사용자가 필요한 정보만 가져와서 가상의 테이블처럼 만들어서 결과를
    보여주는 것
  • 2개의 테이블을 조합하여 하나의 열로 표현한다.

 

예시)

EMP 테이블   DEPT 테이블
EMPNO ENAME JOB DEPTNO   DEPTNO DNAME LOC
7839 KING PRESIDENT 90 10 ACCOUNT N_YORK
7566 JONES MANAGER 20 20 RESEARCH DALLAS
7788 SCOTT ANALYST 10 30 SALES CHICAGE
7654 MARTIN SALESMAN 30 40 OPERATE BOSTON
7900 JAMES CLERK        

 

 

 

JOIN 종류

1. INNER JOIN

  • 조건에 해당하는 값만 출력
  • 서로 연관된 내용만 검색하는 조인 방법

명시적 조인 표현 암시적 조인 표현
SELECT a.empno, a.ename, a.job, a.deptno, b.dname
FROM EMP a JOIN DEPT b
ON a.deptno = b.deptno;
SELECT a.empno, a.ename, a.job, a.deptno, b.dname
FROM EMP a, DEPT b             
WHERE a.deptno= b.deptno; 

 

조인 결과

"KING"과 "JAMES"의 DEPTNO가 DEPT 테이블에 존재하지 않는다. 이 상태에서 INNER JOIN을 하면 
"KING", "HAMES"의 데이터는 조회되지 않는다.

 

2. OUTER JOIN

  • 조인하는 여러 테이블에서 한 쪽에는 데이터가 있고 한 쪽에는 데이터가 없는 경우, 데이터가 있는 쪽 테이블의 
    내용을 전부 출력하는 방법
  • 조인 조건에 만족하지 않아도 해당 행을 출력하고 싶을 때 사용

 

OUTER JOIN 종류

  LEFT OUTER JOIN 

  • 왼쪽 테이블      =>  값이 있는 행은 전부 출력
    오른쪽 테이블   =>  조건에 맞는 것만 출력

명시적 조인 표현 암시적 조인 표현
SELECT a.empno, a.ename, a.job, a.deptno, a.dname
FROM EMP a LEFT OUTER JOIN  DEPT b
ON a.deptno = b.deptno;
SELECT a.empno, a.ename, a.job, a.deptno, a.dname
FROM EMP a, DEPT b
WHERE a.deptno = b.deptno(+);

 

 조인 결과 

 조인 조건에 만족하지 않는 "KING"과 "JAMES"도 출력됨.

'DB' 카테고리의 다른 글

관계형 데이터베이스 1:1, 1:N, M:N 관계  (0) 2022.02.24

절차지향(Procedural Programming)이란?

물이 위에서 아래로 흐르는 것처럼 순차적인 처리가 중요시 되며 프로그램 전체가 유기적으로 연결되도록 만드는 프로그래밍 기법이다. 대표적인 절차지향 언어에는 C언어가 있다. 이는 컴퓨터의 작업 처리 방식과 유사하기 때문에 객체지향 언어를 사용하는 것에 비해 더 빨리 처리되어 시간적으로 유리하다. 옛날에는 하드웨어와 소프트웨어의 개발 속도차이가 크지 않았다. 하지만 하드웨어의 빠른 발전을 통해 컴퓨팅 환경은 급속도로 증가했지만 소프트웨어 개발 시간이 따라가지 못하게 되고 이런 상황에 소프트웨어의 개발시간을 단축하되 하드웨어에 기본적인 사양을 잡아먹어도 더 이상 큰 단점이 아니기에 모듈화, 캡슐화해서 개념적으로 접근하는 형태를 갖는 객체지향 프로그래밍이 탄생했다. 객체지향 프로그래밍은 개발하려는 것을 기능별로 묶어 모듈화를 함으로써 같은 기능을 중복으로 연산하지 않거나 모듈을 재활용하기 때문에 유지보수에 유리하다.

 

장점

- 컴퓨터의 처리구조와 유사해 실행속도 빠름

 

단점

- 유지보수가 어려움

- 실행 순서가 정해져 있으므로 코드의 순서가 바뀌면 동일한 결과를 보장하기 어려움

- 디버깅이 어려움

 

 

객체지향(Object Oriented Programming)이란?

객체지향이란 실제 세계를 모델링하여 소프트웨어를 개발하는 방법이다. 객체지향 프로그래밍에서는 데이터와 절차를 하나의 덩어리로 묶어서 생각한다. 이는 마치 컴퓨터 부품을 하나씩 사다가 컴퓨터를 조립하는 것과 같은 방법이다.

 

객체지향의 4대 특성

1. 추상화

  • 객체 지향적 관점에서 클래스를 정의하는 것. 예를 들어 사자, 고양이, 강아지가 있을 때 우리는 이것을
    각각 객체라고 하며, 이 객체들의 공통점인 동물이라고 표현할 수 있는데 이때 동물로 묶는 행위를
    추상화라고 한다.

2. 캡슐화

  • 특정 객체가 독립적으로 역할을 수행하기 위해 필요한 데이터와 기능을 하나로 묶는 것.
    ☞ 쉽게 말해 모듈화를 의미한다. 이러한 캡슐화를 통해 정보를 객체 안에 포함시키고, 그 정보에 대한 직접
        접근은 허용하지 않는 대신, 필요에 따라 확인할 수 있는 인터페이스를 외부에 공개함으로써 정보 은닉
        효과도 자연스럽게 따라온다.

3. 상속

  • 상위 개념의 특징을 하위 개념이 물려받는 것.

    상속에서 주의할 점
    ☞ 계층도, 조직도 관점(가족 관계도와 같은)에서 이해하면 안된다.
    분류도 관점에서 이해해야 한다.
    하위 클래스는 상위 클래스의 역할을 대신할 수 있으면서 고유의 역할도 수행할 수 있어야 한다.
          아버지와 아들을 예로 들면 아들은 아버지의 역할을 할 수 있으면서 아들 고유의 역할도 수행할 수 있어야
          하지만 그렇지 못하다. 이런 부분에서 상속을 사용할 때 주의해야 한다는 것이다.
    상속은 코드의 재사용성을 높이고 확장성을 높여준다.

4. 다형성

  • 다형성이란 하나의 이름으로 많은 상황에 대처하는 기법이다. 오버로딩, 오버라이딩이 그 방법이다.
    다형성을 이요하면 코드가 더 간단해지는 효과가 있다.

 

장점

- 코드의 재활용성이 높음

- 코딩이 절차지향보다 간편함

- 디버깅이 쉬움

 

단점

- 처리속도가 절차지향보다 느림

- 설계에 많은 시간소요가 들어감

 

이론적으로만 본다면 객체지향이 절차지향 언어에 비해 장점이 많다. 하지만 프로그래밍을 할 떼 항상 객체지향 언어만 사용하는 것은 아니다. 객체지향 언어는 어떤 모듈에 있는 하나의 기능만 필요하더라도 모듈 전체를 가져와야 하기 때문에 절차지향 프로그래밍보다 프로그램 사이즈가 더 커질 수도 있다. 또한 데이터에 대한 접근도 상대적으로 절차지향식보다 느려질 가능성이 많다. 

 

객체지향과 절차지향의 차이점

객체지향의 반대는 절차지향이 아니고 절차지향의 반대는 객체지향이 아니다. 

절차지향  ->  순차적으로 실행에 초점이 되어 있음 , 데이터 중심

객체지향  ->  객체간의 관계/조직에 초점을 두고 있음, 기능 중심

 

https://www.acmicpc.net/problem/11726

 

11726번: 2×n 타일링

2×n 크기의 직사각형을 1×2, 2×1 타일로 채우는 방법의 수를 구하는 프로그램을 작성하시오. 아래 그림은 2×5 크기의 직사각형을 채운 한 가지 방법의 예이다.

www.acmicpc.net

DP를 이용해 풀어야 하는 문제이다.


DP를 사용하기 위한 조건
   1. 큰 문제를 작은 문제로 나눌 수 있다.
   2. 작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 동일하다.

위 조건에 부합하기 때문에 DP를 사용한다.
DP는 규칙을 찾아서 점화식을 세우는 것이 가장 중요하기 때문에 우선 규칙을 생각한다.

n-1, n-2 까지는 채워진 경우의 수가 구해진 상태라고 가정하자. 그 후 맨 뒤에 길이를 추가하면 될 수 있는 경우의
수는 2가지이다.


위와 같이 타일의 세로 길이가 n일 때 타일을 채우는 경우는 "2x1" 타일을 쓰는 경우, "1x2" 타일을 쓰는 경우
두가지이다. 그렇기 때문에 점화식은 다음과 같다.

 

D[i] = D[i - 1] + D[i - 2]

 

틀린 코드 ( overflow 문제 발생 )

#include <stdio.h>
#include <iostream>

using namespace std;

int d[1001];

int tile(int n){
  if(n == 1) return 1;
  if(n == 2) return 2;
  if(d[n] != 0) return d[n];
  
  d[n] = tile(n-1) + tile(n-2);
  return d[n];
}

int main(){
  int n;
  scanf("%d",&n);
  printf("%d",tile(n) % 10007);
}

처음에 위와 같이 코드를 구현했더니 계속 틀렸습니다라고 나와서 당황스러웠다.
사실 틀렸습니다가 나온 이유는 재귀 호출해서 값을 계속 더하는 문제는 값이 매우 커지는 경우가 많기 때문에
OverFlow문제를 야기할 수가 있다.


=> 즉, 문제에서 10007로 나누라던 이유도 값이 매우 커져서 int값을 넘어가 overflow가 발생하는 경우를 방지하기
     위함이다. 그러므로 tile 함수 안에서 10007을 나눈 나머지를 배열에 저장하도록 구현해야 overflow가 나지 않고
     문제를 해결할 수 있다.

다이나믹 프로그래밍 문제는 종류가 굉장히 많으며 컴퓨터적인 사고력을 물어보기에 적합하다는 점에서 자주
출제되는 문제이다. 

다이나믹 프로그래밍

  • 하나의 문제는 단 한 번만 풀도록 하는 알고리즘
    => 한 번 푼 것을 여러 번 다시 푸는 비효율적인 알고리즘을 개선시키는 방법
  • dp같은 경우 문제에서 규칙성을 찾아서 점화식을 세우는 게 가장 중요하다.

 

다이나믹 프로그래밍을 쓰는 이유

  • 일반적으로 상당수의 분할 정복 기법은 동일한 문제를 다시 푼다는 단점을 가지고 있다.
    ( 단, 퀵 정렬이나 병합 정렬은 동일한 문제를 다시 푸는 단점이 없다. 그러므로 매우 빠르다. )

 

단순 분할 정복으로 풀게 되면 심각한 비효율성을 낳는 대표적인 예시

   ' 피보나치 수열 '

   피보나치 수열은 특정한 숫자를 구하기 위해 그 앞에 있는 숫자와 두 칸 앞에 있는 숫자의 합을 구해야 한다.

     피보나치 수열의 점화식 : D[i] = D[i - 1] + D[i - 2]

 

  위 공식에 따라서 1, 1, 2, 3, 5, 8, 13, 21, 34 ,55, ... 와 같이 나아간다. 만약 단순하게 분할 정복 기법을 이용해
  15번째 피보나치 수열을 구한다고 가정해보자. 그러면 아래와 같이 그래프의 형태로 진행 상황을 확인할 수 있다.
  D[15]를 구하려면 D[14]와 D[13]을 알아야 한다. 다만 D[14]를 알려면 D[13]과 D[12]를 알아야만 한다. 이런 식으로
  진행이 된다.


 

  따라서 이런 경우 병합 정렬을 할 때처럼 단순한 분할 정복 기법을 사용하면 안 된다. 왜냐하면 이미 해결한 문제를
  다시 반복적으로 해결하여 비효율적이기 때문이다. 위 예제를 보면  D[12]가 벌써 3번이나 반복적으로 계산되었다.
  => 이럴 경우에 동적 프로그래밍 기법을 사용해야 한다!!

 

다이나믹 프로그래밍의 사용 조건

  1.  큰 문제를 작은 문제로 나눌 수 있다.
  2.  작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 동일하다.

   쉽게 말해 크고 어려운 문제가 있으면 그것을 먼저 잘게 나누어서 해결한 후에 처리하여 나중에 전체의 답을 
   구하는 것이다. 다만 이 과정에서 '메모이제이션(Memoization)'이 사용된다는 점이 분할 정복과 다르다.
   메모이제이션이란 이미 계산한 결과를 배열에 저장함으로써 나중에 동일한 계산을 해야 할 때는 저장된 값을
   단순히 반환하기만 하면 되도록 해주는 것이다. 

 

   피보나치 수열을 예로 들어 문제를 확인해보자.

#include <stdio.h>

int d(int x){
    if(x == 1) return 1;
    if(x == 2) return 1;
    return d(x - 1) + d(x - 2);
}

int main(){
	printf("%d", d(10));
}

  위 경우 답이 55로 잘 출력된다. 하지만 50번째 피보나치 수열을 구하려고 하면 어떻게 될까?

#include <stdio.h>

int d(int x){
    if(x == 1) return 1;
    if(x == 2) return 1;
    return d(x - 1) + d(x - 2);
}

int main(){
	printf("%d", d(50));
}

  실행해보면 아래와 같이 실행은 되지 않고 계속 CPU만 돌아가는 것을 알 수 있다.

 

  그래서 위와 같은 문제를 해결하기 위해 메모이제이션 기법을 활용하면 된다.

#include <stdio.h>

int d[100];

int fibonacci(int x){
	if(x == 1) return 1;
	if(x == 2) return 1;
	if(d[x] != 0) return d[x];
	return d[x] = fibonacci(x - 1) + fibonacci(x - 2);
}

int main(){
	printf("%d", fibonacci(30));
}

위와 같이 해주면 이미 계산된 결과는 배열 d에 저장되기 때문에 한 번 구한 값을 다시 구하는 일은 발생하지 않는다.
이렇게 작업하면 순식간에 계산된 결과가 출력되는 것을 확인할 수 있다. 다이나믹 프로그래밍의 개념을 이해한 뒤에는
많은 문제를 접하여 감을 잡는게 중요하다.

깊이 우선 탐색 ( DFS )

  • 탐색을 함에 있어서 보다 깊은 것을 우선적으로 탐색하는 알고리즘
  • 미로를 탐색할 때 한 방향으로 갈 수 있을 때까지 계속 가다가 더 이상 갈 수 없게 되면 다시 가장 가까운
    갈림길로 돌아와서 이곳으로부터 다른 방향으로 다시 탐색을 진행하는 방법과 유사하다.
  • 깊이 우선 탐색(DFS)이 너비 우선 탐색(BFS)보다 좀 더 간단
  • 단순 검색 속도 자체는 너비 우선 탐색(BFS)에 비해 느리다.

 

깊이 우선 탐색 특징

  • 넓게(wide) 탐색하기 전에 깊게(deep) 탐색하는 것
  • 스택(Stack)을 사용
    - 사실 스택을 사용하지 않아도 구현이 가능하다. 왜냐하면 컴퓨터는 구조적으로 항상 스택의 원리를
      사용하기 때문이다.
  • 자기 자신을 호출하는 재귀 알고리즘의 형태이다.
  • 탐색 과정이 시작 노드에서 한없이 깊게 진행되는 걸 막기 위해 깊이 제한(depth bound)을 사용
  • 깊이 제한에 도달할 때까지 목표노드가 발견되지 않으면 최근에 첨가된 노드의 부모노드로 돌아온다.
    -> 이 과정을 백트래킹(backtracking)이라고 한다.

깊이 우선 탐색 장점과 단점

< 장점 > 

  • 단지 현 경로상의 노드들만을 기억하면 되므로 저장공간의 수요가 비교적 적음
  • 목표노드가 깊은 단계에 있을 경우 해를 빨리 구할 수 있음

< 단점 >

  • 해가 없는 경로에 깊이 빠질 가능성 존재.
    -> 실제의 경우 미리 지정한 임의의 깊이까지만 탐색하고 목표노드를 발견하지 못하면 다음 경로를 따라 탐색하는 방법
         이 유용할 수 있음
  • 얻어진 해가 최단 경로가 된다는 보장이 없음.
    -> 목표에 이르는 경로가 다수인 문제에 대해 깊이우선 탐색은 해에 다다르면 탐색을 끝내버리므로, 
         이때 얻어진 해는 최적이 아닐 수 있다.

 

깊이 우선 탐색을 사용하는 경우

  • 모든 노드를 방문하고자 하는 경우 사용

 

동작 방식

DFS는 다음과 같은 간단한 알고리즘에 따라서 동작한다.

  1. 스택의 최상단 노드를 확인합니다.
  2. 최상단 노드에게 방문하지 않은 인접 노드가 있으면    =>   그 노드를 스택에 넣고 방문처리합니다.
                                        방문하지 않은 인접 노드가 없으면   =>   스택에서 최상단 노드를 뺍니다.

초기 상태

- 스택그래프 준비

 

1. 

DFS는 맨 처음에 시작 노드(Start Node)를 스택에 삽입하면서 시작한다. 또한 그와 동시에 시작 노드를 
방문했다고 알리는 '방문 처리' 를 해주도록 한다. ( 방문 처리 = 빨간색 )

 

 

위의 1,2번을 반복 수행한다.

 

2.

스택에 있던 최상단 노드가 1번 노드이므로 1번과 인접한 노드 중에서 방문하지 않은 2번 노드를 스택에 넣는다.

 

3.

최상단 노드가 2번 노드이므로 2번과 인접한 노드 중에서 방문하지 않은 노드인 3번 노드를 스택에 넣는다.

 

4.

최상단 노드가 3번 노드이므로 3번의 인접 노드 중 방문하지 않은 노드인 6번 노드를 스택에 넣는다.

 

5.

이어서 6번 노드가 최상단 노드이므로 6번의 인접 노드 중 방문하지 않은 7번 노드를 스택에 넣는다.

 

6.

7번 노드, 6번 노드, 3번 노드는 스택에서 빠져나온다. => 인접 노드를 전부 방문했기 때문이다.

이후에 스택에서 최상위 노드가 2번이므로 2번의 인접 노드 중 방문하지 않은 4번 노드를 스택에 넣는다.

 

7.

4번 노드의 인접 노드 중 방문하지 않은 5번 노드가 스택에 들어가고, 이후에 스택에서 하나씩 다 노드들이
빠져나오게 된다. 

 

최종 상태

따라서 방문 경로는 1 - 2 - 3 - 6 - 7 - 4 - 5 이다.

 

DFS 구현 코드

#include<iostream>
#include<vector>

using namespace std;

int number = 7;
int c[7];
vector<int> a[8];

void dfs(int x){
  if(c[x]) return; // 현재 그 노드가 방문한 노드라면 바로 함수 종료
  c[x] = true;
  cout << x << ' ';
  for(int i = 0; i < a[x].size(); i++){
    int y = a[x][i];
    dfs(y);
  }
}

int main(){
  //1과 2를 연결
  a[1].push_back(2);
  a[2].push_back(1);
  
  //1과 3을 연결
  a[1].push_back(3);
  a[3].push_back(1);

  //2와 3을 연결
  a[2].push_back(3);
  a[3].push_back(2);

  //2와 4를 연결
  a[2].push_back(4);
  a[4].push_back(2);
  
  //2와 5를 연결
  a[2].push_back(5);
  a[5].push_back(2);

  //3과 6을 연결
  a[3].push_back(6);
  a[6].push_back(3);

  //3과 7을 연결
  a[3].push_back(7);
  a[7].push_back(3);

  //4와 5를 연결
  a[4].push_back(5);
  a[5].push_back(4);

  //6과 7을 연결
  a[6].push_back(7);
  a[7].push_back(6);

  dfs(1);
}
기본적으로 재귀 호출 자체가 컴퓨터의 내부적인 구조인 스택 구조에 차곡차곡 쌓이는 형태이기 때문에
간단히 재귀함수로 구현을 하는 것 만으로도 사실 스택에 담고 빼는 것과 같은 효과가 나온다. 그렇기 때문에
스택을 따로 쓰지 않고 재귀만 구현해서 DFS를 구현하는게 알고리즘 대회에서 자주 쓰인다.

프로세스와 스레드의 차이

프로세스(Process)

  • 실행 중인 프로그램. 즉 디스크로부터 메모리에 적재되어 CPU의 할당을 받을 수 있는 것을 말한다.
  • 운영체제로부터 주소 공간, 파일, 메모리 등을 할당받으며 이것들을 총칭하여 프로세스라고 한다.

프로세스 제어 블록 (Process Control Blcok, PCB)

  PCB는 특정 프로세스에 대한 중요한 정보를 저장하고 있는 운영체제의 자료구조이다.
운영체제는 프로세스를 관리하  기 위해 프로세스의 생성과 동시에 고유한 PCB를 생성한다.
프로세스는 CPU를 할당받아 작업을 처리하다가도 프로세스 전환이 발생하면 진행하던 작업을 저장하고
CPU를 반환해야 하는데, 이때 작업의 진행 상황을 모두 PCB에 저장하게 된다.
그리고 다시 CPU를 할당받게 되면
PCB에 저장되어 있던 내용을 불러와 이전에 종료되었던 시점부터 다시 작업을 수행한다.

 

PCB에 저장되는 정보

  • 프로세스 식별자 ( Process ID, PID )
  • 프로세스 상태 ( new, ready, running, waiting, terminated 등의 상태를 저장 )
  • 프로그램 카운터 ( 프로세스가 다음에 실행할 명령어의 주소 )
  • CPU 레지스터
  • CPU 스케줄링 정보 ( 프로세스의 우선순위, 스케줄 큐에 대한 포인터 등 )
  • 메모리 관리 정보 ( 페이지 테이블 or 세그먼트 테이블 등과 같은 정보를 포함 )
  • 입출력 상태 정보
  • 어카운팅 정보 ( 사용된 cpu 시간, 시간제한 계정본호 등 )

스레드 ( Thread )

  • 프로세스의 실행 단위
  • 한 프로세스 내에서 동작되는 여러 실행 흐름
  • 같은 프로세스에 속한 스레드들끼리는 코드 영역, 데이터 영역, 힙 영역, 그리고 열린 파일이나 신호와 같은
    운영체제 자원들을 공유한다.
  • 하나의 프로세스를 다수의 실행 단위로 구분하여 자원을 공유하고 자원의 생성과 관리의 중복성을 최소화하여
    수행 능력을 향상시키는 것을 멀티스레딩이라고 한다. 이 경우 각각의 스레드는 독립적인 작업을 수행해야 하기
    때문에 각자의 스택과 pc 레지스터 값을 가지고 있다.

쓰레드 자원 공유 형태

스택을 스레드마다 독립적으로 할당하는 이유

  스택 메모리 공간이 독립적이라는 것은 독립적인 함수 호출이 가능하다는 것이고 이는 독립적인 실행 흐름이 추가되는 것이다. 따라서 스레드의 정의에 따라 독립적인 실행 흐름을 추가하기 위한 최소 조건으로 독립된 스택을 할당한다.

 

PC register를 스레드마다 독립적으로 할당하는 이유

  PC값은 스레드가 명령어의 어디까지 수행하였는지를 나타내게 된다. 스레드는 CPU를 할당받았다가 스케줄러에 의해 다시 선점당한다. 그렇기 때문에 명령어가 연속적으로 수행되지 못하고 어느 부분까지 수행했는지 기억할 필요가 있다. 따라서 PC 레지스터를 독립적으로 할당해야 한다.

 

멀티 스레드

멀티 스레딩의 장점

  프로세스를 이용하여 동시에 처리하던 일을 스레드로 구현할 경우 메모리 공간과 시스템 자원 소모가 줄어들게 된다. 스레드간의 통신이 필요한 경우에도 별도의 자원을 이용하는 것이 아니라 전역 변수의 공간 or 동적할당된 공간인 Heap 영역을 이용하여 데이터를 주고받을 수 있다. 그렇기 때문에 프로세스 간 통신 방법에 비해 스레드 간의 통신 방법이 훨씬 간단하다. 심지어 스레드의 context switching은 프로세스의 context switching과 달리 캐시 메모리를 비울 필요가 없기 때문에 더 빠르다. 따라서 시스템의 자원 소모가 줄어들며 자연스럽게 프로그램의 응답 시간이 단축된다. 이러한 장점 때문에 여러 프로세스로 할 수 있는 작업들을 하나의 프로세스에서 스레드로 나눠 수행하는 것이다.

 

멀티 스레딩의 문제점

  멀티 프로세스 기반으로 프로그래밍 할 때는 프로세스 간 공유하는 자원이 없기 때문에 동일한 자원에 동시에 접근하는 일이 없었지만 멀트 스레딩을 기반으로 프로그래밍 할 때는 이 부분을 신경써줘야 한다. 서로 다른 스레드가 데이터와 힙 영역을 공유하기 때문에 어떤 스레드가 다른 스레드에서 사용중인 변수나 자료구조에 접근하여 엉뚱한 값을 읽어오거나 수정할 수가 있다.

 

  그렇기 때문에 멀티스레딩 환경에서는 동기화 작업이 필요하다. 동기화를 통해 작업 처리 순서를 컨트롤하고 공유 자원에 대한 접근을 컨트롤 하는 것이다. 하지만 이로 인해 병목현상이 발생하여 성능이 저하될 가능성이 높다. 그러므로 과도한 락으로 인한 병목현상을 줄여야 한다. 그리고 디버깅이 어렵다.

 

멀티 스레드 vs 멀티 프로세스

  멀티 스레드는 멀티 프로세스보다 적은 메모리 공간을 차지하고 문맥 전환이 빠르다는 장점이 있지만, 오류로 인해 하나의 스레드가 종료되면 전체 스레드가 종료될 수 있다는 점과 동기화 문제를 안고 있다. 반면 멀티 프로세스 방식은 하나의 프로세스가 죽더라도 다른 프로세스에는 영향을 끼치지 않고 정상적으로 수행된다는 장점이 있지만, 멀티 스레드보다 많은 메모리 공간과 CPU 시간을 차지한다는 단점이 존재한다. 이 두 가지는 동시에 여러 작업을 수행한다는 점에서 같지만 적용해야 하는 시스템에 따라 적합/부적합으로 구분된다. 따라서 대상 시스템의 특징에 따라 적합한 동작 방식을 선택하고 적용해야 한다.

 

프로세스 동기화

Critical Section(임계영역)

  • 멀티 스레딩에 문제점에서 나오듯, 동일한 자원을 동시에 접근하는 작업을 실행하는 코드 영역을 Critical Section
    이라 칭한다.

Critical Section Problem(임계영역 문제)

프로세스들이 Critical Section을 함께 사용할 수 있는 프로토콜을 설계해야 한다.

 

Requirements( 해결을 위한 기본 조건 )

  • Mutual Exclusion ( 상호 배제 )
    - 프로세스 p1이 Critical Section에서 실행 중이라면 다른 프로세스들은 그들이 가진 Critical Section에서 실행될 수 
      없다.
  • Progress(진행)
    - Critical Section에서 실행중인 프로세스가 없고, 별도의 동작이 없는 프로세스들만 Critical Section 진입 후부로서
       참여될 수 있다.
  • Bounded Waiting ( 한정된 대기 )
    - p1이 Critical Section 에 진입 신청 후 받아들여질 때까지, 다른 프로세스들이 Critical Section에 진입하는 횟수는 
      제한이 있어야 한다.

Critical Section Problem 해결책

Lock

   - 하드웨어 기반 해결책으로써, 동시에 공유 자원에 접근하는 것을 막기 위해 Critical Section 에 진입하는 프로세스는
      Lock 을 획득하고 Critical Section 을 빠져나올 때, Lock 을 방출함으로써 동시에 접근이 되지 않도록 한다.

   - lock은 다중처리기 환경에서는 시간적인 효율성 측면에서 적용할 수 없다.

 

Semaphores(세마포)

  • 소프트웨어상에서 Critical Section 문제를 해결하기 위한 동기화 도구

Semaphores 종류

  • 카운팅 세마포
    가용한 개수를 가진 자원 에 대한 접근 제어용으로 사용되며, 세마포는 그 가용한 자원의 개수 로 초기화 된다.
    자원을 사용하면 세마포가 감소, 방출하면 세마포가 증가 한다.

  • 이진 세마포
    MUTEX 라고도 부르며, 상호배제의 (Mutual Exclusion)의 머릿글자를 따서 만들어졌다.
    이름 그대로 0 과 1 사이의 값만 가능하며, 다중 프로세스들 사이의 Critical Section 문제를 해결하기 위해 사용한다.

Semaphores 단점

  • Busy Waiting(바쁜 대기)
    - Semaphore 초기 버전에서 Critical Section 에 진입해야하는 프로세스는 진입 코드를
      계속 반복 실행해야 하며, CPU 시간을 낭비했었다. 이를 Busy Waiting이라고 부르며 특수한 상황이
      아니면 비효율적이다.

    - 일반적으로는 Semaphore에서 Critical Section에 진입을 시도했지만 실패한 프로세스에 대해 Block시킨 뒤,
      Critical Section에 자리가 날 때 다시 깨우는 방식을 사용한다. 이 경우 Busy waiting으로 인한
      시간낭비 문제가 해결된다.

Deadlock(교착상태)

  • 세마포가 Ready Queue 를 가지고 있고, 둘 이상의 프로세스가 Critical Section 진입을
    무한정 기다리고 있고, Critical Section 에서 실행되는 프로세스는 진입 대기 중인 프로세스가 실행되어야만
    빠져나올 수 있는 상황을 지칭한다.

 

스케줄러

프로세스를 스케줄링하기 위한 Queue에는 세 가지 종류가 존재한다.

  • Job Queue : 현재 시스템 내에 있는 모든 프로세스의 집합
  • Ready Queue : 현재 메모리 내에 있으면서 CPU를 잡아서 실행되기를 기다리는 프로세스의 집합
  • Device Queue : Device I/O 작업을 대기하고 있는 프로세스의 집합

각각의  Queue에 프로세스들을 넣고 빼주는 스케줄러에도 크게 세 가지 종류가 존재한다.

  • 장기 스케줄러 ( Long-term scheduler or job scheduler )
    메모리는 한정되어 있는데 많은 프로세스들이 한꺼번에 메모리에 올라올 경우, 대용량 메모리(일반적으로 디스크)에
    임시로 저장된다. 이 pool에 저장되어 있는 프로세스 중 어 떤 프로세스에 메모리를 할당하여 ready queue로 보낼지 결정하는 역할을 한다.
      -  메모리와 디스크 사이의 스케줄링을 담당
      -  프로세스에 memory를 할당 ( admit )
      -  degree of Multiprogramming 제어 ( 실행중인 프로세스의 수 제어 )
      -  프로세스의 상태 ( new -> ready )

  • 단기 스케줄러 ( Short-term scheduler or CPU scheduler )
      -  CPU와 메모리 사이의 스케줄링을 담당
      -  Ready Queue 에 존재하는 프로세스 중 어떤 프로세스를 running 시킬지 결정
      -  프로세스에 CPU를 할당 ( scheduler dispatch )
      -  프로세스의 상태 ( read -> running -> waiting -> ready )

  • 중기 스케줄러 ( Medium-term scheduler or Swapper )
      -  여유 공간 마련을 위해 프로세스를 통째로 메모리에서 디스크로 쫓아냄 (swapping)
      -  프로세스에게서 memory를 deallocate
      -  degree of Multiprogramming 제어 ( 동시에 처리 가능한 수 작업이나 프로그램의 수 )
      -  현 시스템에서 메모리에 너무 많은 프로그램이 동시에 올라가는 것을 조절하는 스케줄러
      -  프로세스의 상태 ( ready -> suspended )

CPU 스케줄러

스케줄링 대상은 Ready Queue에 있는 프로세스들이다.

 

1. FIFO(First In First Out)

  • 먼저 온 고객을 먼저 서비스 해주는 방식, 즉 먼저 온 순서대로 처리
  • 비선점형(Non-Preemptive) 스케줄링
    일단 CPU를 잡으면 CPU burst가 완료될 때까지 CPU를 반환하지 않는다. 할당되었던 CPU가 반환될 때만
    스케줄링이 이루어진다.

문제점

  • convoy effect
    - 소요시간이 긴 프로세스가 먼저 도달하여 효율성을 낮추는 현상 발생

 

2. SJF(Shortest - Job - First)

  • 다른 프로세스가 먼저 도착했어도 CPU burst time이 짧은 프로세스에게 선 할당
  • 비선점형(Non-Preemptive) 스케줄링

문제점

  • starvation
    -  효율성을 추구하는게 가장 중요하지만 특정 프로세스가 지나치게 차별받으면 안된다. 이 스케줄링 기법은
       극단적으로 CPU 사용이 짧은 job을 선호한다. 그래서 사용 시간이 긴 프로세스는 거의 영원히 CPU를
       할당받을 수 없다.

 

3. SRTF(Shortest Remaining Time First)

  • 현재 수행중인 프로세스의 남은 burst time보다 더 짧은 CPU burst time을 가지는 새로운 프로세스가 도착하면
    CPU를 뺏긴다.
  • 선점형(Preemptive) 스케줄링

문제점

  • starvation
    -  새로운 프로세스가 도달할 때마다 스케줄링을 다시하기 때문에 CPU burst time(CPU 사용시간)을 측정할 수가 없다.

 

4. Priority Scheduling

  • 우선순위가 가장 높은 프로세스에게 CPU를 할당하는 스케줄링이다.
    ※우선순위 : 정수로 표현하고 작은 숫자가 우선순위가 높다.
  • 선점형(Preemptive) 스케줄링
    - 더 높은 우선순위의 프로세스가 도착하면 Ready Queue의 Head에 넣는다.

문제점

  • starvation
  • 무기한 봉쇄(Indefinite blocking)
    - 실행 준비는 되어있으나 우선순위가 낮아 CPU를 사용 못하는 프로세스가 무기한 대기하는 상태

해결 방법

  • aging
    - 아무리 우선순위가 낮은 프로세스라도 오래 기다리면 우선순위를 높여준다.

 

5. Round Robin

  • 현대적인 CPU 스케줄링
  • 각 프로세스는 동일한 크기의 할당 시간(time quantum)을 갖게 된다.
  • 할당 시간이 지나면 프로세스는 선점당하고 ready queue의 제일 뒤에 가서 다시 줄을 선다.
  • CPU 사용시간이 랜덤한 프로세스들이 섞여있을 경우 효율적
  • RR이 가능한 이유는 프로세스의 context를 save할 수 있기 때문이다.

장점

  • Response time이 빨라진다.
  • 프로세스가 기다리는 시간이 CPU를 사용할 만큼 증가한다. => 공정한 스케줄링이다. 

주의할 점

  • 설정한 time quantu이 너무 커지면 FIFO와 같아진다. 또 너무 작아지면 스케줄링 알고리즘의 목적에는
    이상적이지만 잦은 context switch로 overload가 발생한다. 그렇기 때문에 적당한 time quantum을 설정
    하는 것이 중요하다.

 

     

좋은 코드란?

  • 읽기 쉬운 코드
  • 중복이 없는 코드
  • 테스트가 용이한 코드

Object Oriented Programming ( 객체지향 프로그래밍 )

객체 지향 프로그래밍 이전의 프로그래밍 패러다임을 살펴보면, 중심이 컴퓨터에 있었다. 컴퓨터가 사고하는대로 프로그래밍을 하는 것이다. 하지만 객체지향 프로그래밍은 인간 중심적 프로그래밍 패러다임이라고 할 수 있다. 즉, 현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 것을 말한다. 현실 세계의 사물들을 객체라고 보고 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍 하는 것이다. 이것을 추상화라고 한다.

 

OOP로 코드를 작성하면 이미 작성한 코드에 대한 재사용성이 높다. 자주 사용되는 로직을 라이브러리로 만들어두면 계속해서 사용할 수 있으며 그 신뢰성을 확보할 수 있다. 또한 라이브러리를 각종 예외상황에 맞게 잘 만들어두면 개발자가 사소한 실수를 하더라도 그 에러를 컴파일 단계에서 잡아낼 수 있으므로 버그 발생이 줄어든다. 또한 내부적으로 어떻게 동작하는지 몰라도 개발자는 라이브러리가 제공하는 기능들을 사용할 수 있기 때문에 생산성이 높아지게 된다. 객체 단위로 코드가 나눠져 작성되기 때문에 디버깅이 쉽고 유지보수에 용이하다. 또한 데이터 모델링을 할 때 객체와 매핑하는 것이 수월하기 때문에 요구사항을 보다 명확하게 파악하여 프로그래밍 할 수 있다.

 

객체 간의 정보 교환이 모두 메시지 교환을 통해 일어나므로 실행 시스템에 많은 오버헤드가 발생하게 된다. 하지만 이것은 하드웨어의 발전으로 많은 부분 보안되었다. 객체 지향 프로그래밍의 치명적인 단점은 함수형 프로그래밍 패러다임의 등장 배경을 통해서 알 수 있다. 바로 객체가 상태를 갖는다는 것이다. 변수가 존재하고 이 변수를 통해 객체가 예측할 수 없는 상태를 갖게 되어 애플리케이션 내부에서 버그를 발생시킨다는 것이다. 이러한 이유로 함수형 패러다임이 주목받고 있다.

 

객체 지향적 설계 원칙 ( SOLID ) ★

  1.  SRP(Single Responsibility Principle)
    - 단일 책임 원칙
    - 클래스는 단 하나의 책임을 가져야 하며 클래스를 변경하는 이유는 단 하나의 이유이어야 한다.
  2. OCP(Open-Closed Principle)
    - 개방-폐쇄 원칙
    - 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
  3. LSP(Liskov Substitution Principle)
    - 리스코프 치환 원칙
    - 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
  4. ISP(Interface Segregation Principle)
    - 인터페이스 분리 원칙
    - 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
  5. DIP(Dependency Inversion Principle)
    - 의존 역전 원칙
    - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

 

RESTful API

월드 와이드 웹(World Wide Web a.k.a WWW)과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 아키텍처의 한 형식으로 자원을 정의하고 자원에 대한 주소를 지정하는 방법 전반에 대한 패턴

REST는 하나의 아키텍처로 볼 수 있다. 좀 더 정확한 표현으로 말하자면, REST는 API 설계의 중심에 자원이 있고 
HTTP Method를 통해 자원을 처리하도록 설계하는 것이다.

 

REST의 특징

  • Uniform Interface
  • Stateless ( 무상태성 )
  • Cacheable ( 캐시 가능 )
  • Client-Server 구조
  • Hierarchical system ( 계층형 구조 )
  • Self-descriptiveness ( 자체 표현 구조 )

RESTful하게 API를 디자인 한다는 것은 무엇을 의미하는가

  1.  리소스와 행위를 명시적이고 직관적으로 분리한다.
    - 리소스  =>  URI로 표현되는데 리소스가 가리키는 것은 명사로 표현되어야 한다.
    - 행위     =>  HTTP Method로 표현하고, GET(조회), POST(생성), PUT(기존 entity 전체 수정),
                            PATCH(기존 entity 일부 수정), DELETE(삭제) 를 분명한 목적으로 사용한다.

  2.  Message는 Header와 Body를 명확하게 분리해서 사용한다.
    - Entity에 대한 내용은 body에 담는다.
    - 애플리케이션 서버가 행동할 판단의 근거가 되는 컨트롤 정보인 API 버전 정보, 응답받고자 하는 MIME 타입 등은
       header에 담는다.
    - header와 body는 http header와 http body로 나눌 수도 있고, http body에 들어가는 json 구조로 분리할 수도 있다.
     
  3.  API 버전을 관리한다.
    - 환경은 항상 변하기 때문에 API의 signature가 변경될 수도 있음에 유의하자.
    - 특정 API를 변경할 때는 반드시 하위호환성을 보장해야 한다.

  4.  서버와 클라이언트가 같은 방식을 사용해서 요청하도록 한다.
    - 브라우저는 form-data 형식의 submit으로 보내고 서버에서는 json 형태로 보내는 식의 분리보다는
       json으로 보내든, 둘 다 form-data 형식으로 보내든 하나로 통일한다.
    - 다른 말로 표현하자면 URI가 플랫폼 중립적이어야 한다.

 

장점

  • Open API를 제공하기 쉽다.
  • 멀티플랫폼 지원 및 연동이 용이하다.
  • 원하는 타입으로 데이터를 주고 받을 수 있다.
  • 기존 웹 인프라(HTTP)를 그대로 사용할 수 있다.

단점

  • 사용할 수 있는 메소드가 4가지 밖에 없다.
  • 분산환경에서는 부적합하다.
  • HTTP 통신 모델에 대해서만 지원한다.

 

자세한 REST API에 대한 정보 : https://meetup.toast.com/posts/92

 

TDD

TDD란?

 Test-Driven Development(TDD)는 매우 짧은 개발 사이클의 반복에 의존하는 소프트웨어 개발 프로세스이다. 우선 개발자는 요구되는 새로운 기능에 대한 자동화된 테스트케이스를 작성하고 해당 테스트를 통과하는 가장 간단한 코드를 작성한다. 일단 테스트 통과하는 코드를 작성하고 상황에 맞게 리팩토링하는 과정을 거치는 것이다. 말 그대로 테스트가 코드 작성을 주도하는 개발방식인 것이다.

 

Add a test

 테스트 주도형 개발에선, 새로운 기능을 추가하기 전 테스트를 먼저 작성한다. 테스트를 작성하기 위해서, 개발자는 해당 기능의 요구사항과 명세를 분명히 이해하고 있어야 한다. 이는 사용자 케이스와 사용자 스토리 등으로 이해할 수 있으며, 이는 개발자가 코드를 작성하기 전에 요구사항에 집중할 수 있도록 도와준다. 이는 정말 중요한 부분이자 테스트 주도 개발이 주는 이점이라고 볼 수 있다.

 

Run all tests and see if new one fails

 어떤 새로운 기능을 추가하면 잘 작동하던 기능이 제대로 작동하지 않는 경우가 발생할 수 있다. 더 위험한 경우는 개발자가 이를 미처 인지하지 못하는 경우이다. 이러한 경우를 방지하기 위해 테스트 코드를 작성하는 것이다. 새로운 기능을 추가할 때 테스트 코드를 작성함으로써, 새로운 기능이 제대로 작동함과 동시에 기존의 기능들이 잘 작동하는지 테스트를 통해 확인할 수 있는 것이다.

 

Refactor code

 '좋은 코드'를 작성하기란 쉽지 않다. 코드를 작성할 때 고려해야 할 요소가 한 두 가지가 아니기 때문이다. 가독성이 좋게 coding convention을 맞춰야 하며, 네이밍 규칙을 적용하여 메소드명, 변수명, 클래스명에 일관성을 줘야 하며, 앞으로의 확장성 또한 고려해야 한다. 이와 동시에 비즈니스 로직에 대한 고려도 반드시 필요하며, 예외처리 부분 역시 빠뜨릴 수 없다. 물론 코드량이 적을 때는 이런 저런 것들을 모두 신경쓰면서 코드를 작성할 수 있지만 끊임없이 발견되는 버그들을 디버깅하는 과정에서 코드가 더럽혀지기 마련이다.

 

 이러한 이유로 코드량이 방대해지면 리팩토링을 하게 된다. 이 때 테스트 주도 개발을 통해 개발을 해 왔다면, 테스트 코드가 그 중심을 잡아줄 수 있다. 뚱뚱해진 함수를 여러 함수로 나누는 과정에서 해당 기능이 오작동을 일으킬 수 있지만 간단히 테스트를 돌려봄으로써 이에 대한 안심을 하고 계속해서 리팩토링을 진행할 수 있다. 결과적으로 리팩토링 속도도 빨라지고 코드의 퀄리티도 그만큼 향상하게 되는 것이다. 코드 퀄리티 부분을 조금 상세히 들어가보면, 보다 객체지향적이고 확장 기능이 용이한 코드, 재설계의 시간을 단축시킬 수 있는 코드, 디버깅 시간이 단축되는 코드가 TDD와 함께 탄생하는 것이다.

 

 어차피 코드를 작성하고나서 제대로 작동하는지 판단해야하는 시점이 온다. 물론 중간 중간 수동으로 확인도 할 것이다. 또 테스트에 대한 문서도 만들어야 한다. 그 부분을 자동으로 해주면서, 코드 작성에 도움을 주는 것이 TDD인 것이다. 끊임없이 TDD 찬양에 대한 말만 했다. TDD를 처음 들어보는 사람은 이 좋은 것을 왜 안하는가에 대한 의문이 들 수도 있다.

 

의문점

Q. 코드 생산성에 문제가 있지는 않나?

  두 배는 아니더라도 분명 코드량이 늘어난다. 비즈니스 로직, 각종 코드 디자인에도 시간이 많이 소요되는데, 거기에다가 테스트 코드까지 작성하기란 여간 벅찬 일이 아닐 것이다. 코드 퀄리티보다는 빠른 생산성이 요구되는 시점에서 TDD 는 큰 걸림돌이 될 수 있다.

 

Q. 테스트 코드를 작성하기가 쉬운가?

  이 또한 TDD 라는 개발 방식을 적용하기에 큰 걸림돌이 된다. 진입 장벽이 존재한다는 것이다. 어떠한 부분을 테스트해야할 지, 어떻게 테스트해야할 지, 여러 테스트 프레임워크 중 어떤 것이 우리의 서비스와 맞는지 둥 여러 부분들에 대한 학습이 필요하고 익숙해지는데에도 시간이 걸린다. 팀에서 한 명만 익숙해진다고 해결될 일이 아니다. 개발은 팀 단위로 수행되기 때문에 팀원 전체의 동의가 필요하고 팀원 전체가 익숙해져야 비로소 테스트 코드가 빛을 발하게 되는 것이다.

 

Q. 모든 상황에 대해서 테스트 코드를 작성할 수 있는가? 작성해야 하는가?

  세상에는 다양한 사용자가 존재하며, 생각지도 못한 예외 케이스가 존재할 수 있다. 만약 테스트를 반드시 해봐야 하는 부분에 있어서 테스트 코드를 작성하는데 어려움이 발생한다면? 이러한 상황에서 주객이 전도하는 상황이 발생할 수 있다. 분명 실제 코드가 더 중심이 되어야 하는데 테스트를 위해서 코드의 구조를 바꿔야 하나하는 고민이 생긴다. 또한 발생할 수 있는 상황에 대한 테스트 코드를 작성하기 위해 배보다 배꼽이 더 커지는 경우가 허다하다. 실제 구현 코드보다 방대해진 코드를 관리하는 것도 쉽지만은 않은 일이 된 것이다.

모든 코드에 대해서 테스트 코드를 작성할 수 없으며 작성할 필요도 없다. 또한 테스트 코드를 작성한다고 해서 버그가 발생하지 않는 것도 아니다. 애초에 TDD 는 100% coverage 와 100% 무결성을 주장하지 않았다.

 

MVC 아키텍쳐

MVC의 각 컴포넌트의 역할

 

Model( 모델 )
- 컨트롤러가 호출할 때 요청에 맞는 역할을 수행한다.
- 비즈니스 로직( = 업무에 필요한 데이터 처리 수행 )을 구현하는 영역으로 응용프로그램에서 데이터를 처리하는 부분이다.
- DB에 연결하고 데이터를 추출하거나 저장, 삭제, 업데이트, 변환 등의 작업을 수행한다.
- 상태의 변화가 있을 때 컨트롤러와 뷰에 통보해 후속 조치 명령을 받을 수 있게 한다.

 

View( 뷰 )

- 컨트롤러로부터 받은 모델의 결과값을 가지고 사용자에게 출력할 화면을 만드는 일을 한다.
- 만들어진 화면을 웹브라우저에 전송하여 웹브라우저가 출력하게 한다.
- 사용자와의 상호작용을 위한 인터페이스를 표시하는 영역

 

Controller ( 컨트롤러 )

- 일종의 조정자라고 할 수 있다. 클라이언트의 요청을 받았을 때, 그 요청에 대해 실제 업무를 수행하는 모델 컴포넌트를 호출한다.
- 클라이언트가 보낸 데이터가 있다면, 모델에 전달하기 쉽게 데이터를 가공한다. 모델이 업무를 마치면 그 결과를 뷰에 전달한다.

 

 

MVC 구동 원리

C/S(Client - Server) 구조로 요청을 하면 그에 맞는 응답을 하는 구조를 기본으로 하고 있다.

1. 웹 브라우저가 웹 서버에 웹 애플리케이션 실행을 요청 ( MVC 구조가 WAS라고 보면 된다. )
2. 웹 서버는 들어온 요청을 처리할 수 있는 서블릿을 찾아서 요청을 전달한다. ( Matching )

3. 서블릿은 모델 자바 객체의 메서드를 호출한다.

4. 데이터를 가공하여 값 객체를 생성하거나, JDBC를 사용하여 데이터베이스와의 인터랙션을 통해 값 객체를 생성한다.

5. 업무 수행을 마친 결과값을 컨트롤러에게 반환한다.

6. 컨트롤러는 모델로부터 받은 결과값을 View에게 전달한다.

7. JSP는 전달받은 값을 참조하여 출력할 결과 화면을 만들고 컨트롤러에게 전달한다.

8. 뷰로부터 받은 화면을 웹 서버에게 전달한다.

9. 웹 브라우저는 웹 서버로부터 요청한 결과값을 응답받으면 그 값을 화면에 출력한다.

 

 

Ethernet

  • 컴퓨터 네트워크 기술의 하나로, 물리 계층와 데이터 링크 계층으로 나뉜다.
  • 물리 계층           =>  신호와 배선 정의
    데이터링크 계층   =>  MAC 패킷과 프로토콜의 형식 정의 

Ethernet 헤더 구조

Destination MAC( 6 bytes ) Source MAC ( 6 bytes ) Type ( 2 bytes )
목적지 주소     출발지 주소    상위 계층 Protocol 종류 구분
( IP : 0x0800, ARP : 0x0806 등 ) 

                                      

IP ( Internet Protocol )

  • 컴퓨터가 갖게 되는 주소
  • Wifi가 달라질 때마다 얻는 주소가 달라진다.
  • 비신뢰성, 비연결성 통신이다.
    비신뢰성 => 흐름에 관여하지 않기 때문에 보낸 정보가 제대로 갔는지 보장 X
                  ex) 전송 과정에서 패킷이 손상되거나, 전송한 패킷의 순서가 뒤죽박죽이거나, 같은 패킷이
                         두 번 전송될 수도 있다.
  • 신뢰성을 보장하기 위해선 IP의 상위 프로토콜인 TCP를 사용해야 한다.
                  

IP 헤더 구조

Version : IP의 버전을 나타냄 ( IPv4 / IPv6 )

 

IHL      : IP헤더의 길이

 

TOS     : 서비스의 우선 순위를 제공

 

Total Length : ethernet패킷과 패킷 끝에 필요없는 부분을 제외한 순수 IP 패킷의 길이

 

Identification : 분열이 발생했을 때 원래의 데이터를 식별하기 위해서 사용  

 

Fragment Flags ( IP Flags )
     - x : 항상 0으로 설정
     - D : 분열 여부를 나타냄 ( 0 => 분열 가능, 1 => 분열 방지 )
     - M : 분열될 조각이 더 있는지 판단 ( 0 => 마지막 조각, 1 => 분열될 조각 더 있음 )

 

Fragment Offset : 8바이트 offset으로 조각에 저장된 원래 데이터의 바이트 범위 나타냄


TTL : 데이터를 전달할 수 없는 것으로 판단되어 소멸되기 이전에 데이터가 이동할 수 있는 단계의 수


Protocol : 상위계층 프로토콜 표시 ( TCP : 6, UDP : 17 )

 

Header Checksum : IP header의 체크섬을 저장

 

Source Address : 출발지 IP 주소

 

Destination Address : 목적지 IP 주소

 

TCP

  • 데이터를 송수신하기 위해 IP를 사용하는 프로토콜
  • 데이터를 세그먼트 단위로 잘게 쪼개서 전송한 후 데이터를 받은 도착지에서
    다시 재조립하는 과정을 거친다.

 

TCP 헤더 구조

Source Port : 데이터를 전송하는 곳의 포트번호

 

Destination Port : 목적지의 포트번호 ( 80 : HTTP, 443 : HTTPS )

 

Sequence Number : 데이터 byte의 고유한 일련번호. 이를 이용하여 순서를 올바르게 배열한다.

 

Acknowldegement Number
      - 다음 세그먼트 수신 준비가 되었다는 여부와 수신이 완료되었다는 확인 메시지 전달

 

Offset ( = Header Length )
      - 필드값 * 4 = TCP Header 길이

 

Reserved : 차후 사용을 위한 예약 필드

 

TCP Flags
  - TCP flag는 비트로 되어 있기 때문에 flag를 체크해 줄 때 비트 연산자를 써야 한다.
    ex) if(tcphdr->th_flags & TH_RST ) 이런 식으로 비트연산자(&,|,^ 등등) 을 써서 확인해야 한다.


  SYN : 연결 요청 플래그 ( 통신 시작시 세션을 연결하기 위한 플래그 )


  ACK : 응답 플래그 ( 송신측으로부터 패킷을 잘 받았다는 걸 알려주기 위한 플래그 )


  FIN  : 연결 종료 플래그 ( 세션 종료 )


  RST  : 연결 재설정 플래그 ( 강제 연결 종료 )


  PSH : 넣기 플래그 ( 버퍼가 채워지기를 기다리지 않고 받는 즉시 전달 )
          -> 버퍼링 없이 7계층의 응용프로그램에게 바로 전달한다.


  URG : 긴급 데이터 플래그   

 

Window Size : 송신 시스템의 수신 버퍼의 크기를 바이트 단위로 나타냄

 

Checksum : TCP 세그먼트 내용이 유효한지 확인 및 손상 여부 검사

 

 

※ 데이터 수신 측 (receiver) 이 필요한 정보 => window size, Acknowledgement Number

                                                                                        

'TCP, IP 네트워크 프로그래밍' 카테고리의 다른 글

TCP 통신  (0) 2022.02.03

+ Recent posts