서버와 클라이언트가 각각 하나의 프로세스로써 어떻게 데이터를 주고 받는지 이해해보자.

 

IP주소와 포트번호

우선 컴퓨터가 네트워크 상에서 통신을 하기 위해서는 수많은 정보의 바다에서 자신이 누군지 유일하게 식별이 가능한 수단이 있어야한다.

-> 이때 사용되는 것이 IP주소(절대 겹치면 안되고, 고유해야함)

하지만 컴퓨터가 세상에 너무 많아지면서 자연스레 IP주소의 부족 현상이 발생함.

-> 이를 해결 하기 위하여 사설 IP주소를 사용하며 이를 공인 IP주소로 바꾸는 NAT, 서브네팅, IPv6등의 기술들이 나오게됨.

 

컴퓨터가 직접 네트워크에서 통신하는 것이 아니라, 컴퓨터에서 동작하는 프로세스가 또 다른 컴퓨터의 프로세스와 통신하는 것

프로세스간에 통신을 위해 IPC를 하되, 그저 다른 시스템의 프로세스와 IPC를 한다고 생각하는 것이 중요

 

IP주소를 통해 컴퓨터를 식별했다면, 해당 컴퓨터에서 어떤 프로세스에게 데이터를 보내야하는지 알아야하는데 이때 사용되는 식별값이 포트번호임.

클라이언트 a가 서버 b로 데이터를 보낼때 아래와 같은 형태로 데이터를 보낼 대상을 식별함

[서버 프로세스 B가 동작 중인 컴퓨터의 아이피 주소]:[서버 프로세스가 부여받은 포트번호]

즉, [203.230.7.2:80]의 뜻은 [203.230.7.2의 아이피 주소를 가진 컴퓨터의 80번 포트의 프로세스] 를 말한다.

 

 

데이터 송신 과정

  1. Application (데이터를 송신 하려는 서버 프로세스)
  2. Sockets
  3. 네트워크 스택
  4. NIC

- 데이터 송신시

서버 프로세스가 운영체제의 write시스템 콜을 통해 소켓에 데이터를 보내게 되고 이후 tcp/udp계층과 ip계층 그리고 대표적으로 ethernet을 거쳐 흐름제어, 라우팅 등의 작업을 하게 된다.

이후 마지막으로 NIC를 통해 외부로 데이터를 보낸다.

 

- 데이터 수신시

데이터 수신시에는 반대로 NIC에서 데이터를 수신하고, 인터럽트를 통해 Driver로 데이터를 옮기고 이후 네트워크 스택에서 데이터가 이동하며 소켓에 데이터가 담기고, 최종적으로 수신 대상이 되는 프로세스에 데이터가 도달하게 된다.

 

TCP 전용 소켓(=stream 소켓)

tcp는 udp와 달리 신뢰성 있는 데이터 송수신을 하며 tcp 소켓을 활용하는 시스템 콜에서 이런 특징이 드러나게됨.

 

UDP 전용 소켓(=datagram 소켓)

udp는 tcp와 달리 비연결지향임.

 

tcp 소켓을 사용하는 흐름

tcp 소켓의 핵심은 accept() 시스템 콜

 

socket() 시스템 콜

소켓을 만드는 시스템 콜, 미리 형태를 잡아두는 것이라고 이해하면 됨.

socket(domain, type, protocol):

domain: IPv4, IPv6중 무엇을 사용할지 결정

type: stream, datagram소켓 중 선택

protocol: 0,6,17중 0을 넣으면 시스템이 프로토콜을 선택하며, 6이면 tcp, 17이면 udp

 

int socket_descriptor;
socket_descriptor = socket(AF_INET, SOCK_STREAM, 0);

 

socket()의 리턴값은 파일 디스크립터임.

리눅스에서는 모든 것을 파일로 취급하며 소켓 역시 파일로 취급

 

웹서버 프로세스가 데이터를 전송하기 위해 write(), read() 시스템 콜을 사용 할때, 대상 파일의 파일 디스크립터를 파라미터로 전송하여 OS에게 어떤 파일에 데이터를 작성할지, 혹은 어떤 파일의 데이터를 요청할지 결정.

이때, 파일 디스크립터가 소켓의 파일 디스크립터인 경우, 소켓에 데이터를 작성(데이터 송신) 혹은 소켓의 데이터를 읽어들이는(데이터 수신)동작을 하게 되는것.

socket() 시스템 콜은 미리 IPv4통신을 위해 사용할지, IPv6 통신을 위해 사용할지, TCP를 사용할지 아니면 UDP를 사용할지 틀을 만들어두는 것이라 생각하면됨.

 

bind() 시스템 콜

bind(sockfd ,sockaddr, socklen_t)

sockfd : 바인딩을 할 소켓의 파일 디스크립터

sockaddr : 소켓에 바인딩할 아이피 주소, 포트번호를 담은 구조체

socklen_t : 위 구조체의 메모리 크기

 

#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;         // IPv4 주소 체계
    server_address.sin_addr.s_addr = INADDR_ANY; // 모든 가능한 IP 주소
    server_address.sin_port = htons(80);       // 포트 번호 80

    if (bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Bind failed");
        return 1;
    }

    // 바인딩 성공 처리 및 작업 수행

    return 0;
}

 

bind() 시스템 콜은 생성한 소켓에 실제 아이피 주소와 포트번호를 부여하는 시스템 콜이며 OS에게 어떤 소켓에 아이피 주소와 포트번호를 부여할지 알려주기 위해 파라미터에 소켓의 파일 디스크립터를 포함함.

클라이언트는 통신시 포트번호가 자동으로 부여되기에 bind 시스템 콜은 서버에서만 사용됨.

 

 

listen() 시스템 콜 only for TCP

listen(sockfd, backlog)

sockfd : 소켓의 파일 디스크립터

backlog : 연결 요청을 받아줄 크기 = TCP의 백로그 큐의 크기

 

#include <sys/socket.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    // ... 서버 소켓의 주소와 바인딩 설정 ...

    int backlog = 10; // 최대 대기열 크기
    if (listen(sockfd, backlog) == -1) {
        perror("Listen failed");
        return 1;
    }

    // 리스닝 성공 처리 및 연결 요청 처리

    return 0;
}

 

listen() 시스템 콜은 연결지향인 TCP에서만 사용하며 파라미터로 받은 파일 디스크립터에 해당하는 소켓을 클라이언트의 연결 요청을 받아들이도록하며 최대로 받아주는 크기를 backlog로 설정한다

이 listen() 시스템 콜에서 설정하는 backlog의 크기가 TCP에서의 backlog queue의 크기다.

 

연결 요청을 받아들이는 방법

listen() 시스템 콜은 파라미터로 받은 backlog 크기만큼 backlog queue를 만드는 시스템 콜

서버측의 소켓은 listen()이후 대기 상태에서 클라이언트의 연결 요청을 받아주기 위해 backlog queue를 가진 채로 기다림.

실제로는 서버에 셀수 없이 많은 클라이언트가 요청을 보내게 되고 이 요청들은 모두 backlog queue에 저장됨.

client가 클라이언트 소켓을 통해 처음으로 서버에 요청을 하여 백로그 큐에 들어갈때 syn요청을 보내게됨

 

accept() 시스템 콜

int accept(sockfd, sockaddr, socklen_t);

sockfd : 소켓의 파일 디스크립터

sockaddr : 선입선출로 빼온 연결 요청에서 알아낸 클라이언트의 주소

socklen_t : 구조체의 메모리 크기

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80);

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Waiting for client's connection...\n");

    struct sockaddr_in client_address;
    socklen_t client_addrlen = sizeof(client_address);

    int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

    printf("Server: Accepted connection from %s:%d\n",
           inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

    // 3-way handshake의 나머지 두 단계 수행
    char buffer[1024];
    ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0); // 클라이언트의 ACK 받기
    if (bytes_received > 0) {
        printf("Server: Received ACK from client.\n");
    }

 

accept 시스템 콜은 backlog queue에서 syn을 보내와 대기중인 요청을 선입선출로 하나씩 연결에 대한 수립을 해줌.

파라미터를 보면 클라이언트의 아이피 주소, 포트번호를 받는데 이 값은 백로그 큐에서 가장 앞에있는 연결요청 구조체에서 알아내서 가져옴.

 

TCP 3-way handshake

TCP의 특징 : 연결지향, 신뢰성(=3way handshake)

클라이언트와 서버간의 서로 신뢰성 있는 통신을 위해 서로 준비가 되어있다라는걸 확인하는 과정

위 과정중 client가 보내는 SYN이 listen 상태인 서버의 소켓에 연결 요청을 보내는것

이후 과정은 accept 시스템 콜 이후 진행하여 최종적으로 established 상태를 수립하고 본격적인 데이터의 송/수신이 일어남.

 

사실 accept 시스템 콜 이후 곧바로 잔여 3-way handshake 이후 데이터 송수신이 일어나는것은 아님.

서버의 성능을 위해 또 하나의 테크닉이 들어가게됨.(멀티 프로세스 or 멀티 스레드)

 

하나의 프로세스인 서버가 수많은 클라이언트의 요청을 받는 상황에서, 백로그 큐의 가장 앞에 있던 클라이언트의 요청을 받고 응답까지 다 주고 다시 다음 요청을 받아준다면 엄청난 병목이 생김

(따라서 서버는 연결 요청을 받는 부분 따로, 이후 응답 주는 부분 따로 나눔)

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0) { // 자식 프로세스 <- 이 부분에 집중!

            printf("Server: Accepted connection from %s:%d\n",
                   inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

            // 3-way handshake의 나머지 두 단계 수행
            // 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.
            sleep(1); // 실제로는 필요한 로직 수행

            // 서버의 응답 전송
            char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
            send(client_socket, response, strlen(response), 0);
            printf("Server: Sent response to client.\n");

            close(client_socket);
            exit(0);
        }

        close(client_socket);
    }

    close(server_socket);

    return 0;
}

 

위 코드에서 accept 시스템 콜에 대한 리턴을 받음

( == SYN 요청을 보낸 클라이언트가 적어도 하나 있어서 백로그 큐에 있었고 해당 클라이언트의 요청에 대한 이후 응답을 위해 새로운 소켓을 만들었다.)

 

fork() - 자식 프로세스 생성

리턴값 = 0 자식 프로세스, 0이 아님 = 원래 본인(부모) 프로세스

 

부모 프로세스는 연결 요청을 받아주고 자식 프로세스에게 나머지 일을 맡기고 다시 새로운 연결 요청을 받아줌

#include <stdio.h#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0 -> false ) { 
           실행안함
        }

      
    }

    close(server_socket);

    return 0;
	}

 

 

자식 프로세스는 반면 부모 프로세스가 새로 만들어준 소켓을 이어받아 이후 남은 잔여 3-way handshake 수행 후 데이터 통신을 수행

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_address;
        socklen_t client_addrlen = sizeof(client_address);

        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);

        if (fork() == 0 -> true) { // 자식 프로세스
            close(server_socket);

            printf("Server: Accepted connection from %s:%d\n",
                   inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

            // 3-way handshake의 나머지 두 단계 수행
            // 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.
            sleep(1); // 실제로는 필요한 로직 수행

            // 서버의 응답 전송
            char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
            send(client_socket, response, strlen(response), 0);
            printf("Server: Sent response to client.\n");

            close(client_socket);
            exit(0); <-여기서 자식 프로세스가 종료됨
        }

        close(client_socket);
    }

    close(server_socket);

    return 0;
}

 

멀티 스레드:

멀티 프로세스의 단점을 보완하기 위해 리눅스에서 등장한 기술, 멀티 프로세스의 단점을 보완한 것을 제외하고는 크게 다르지 않음.

 

서버란?

OS에 의해 동작하는 프로세스이며, 클라이언트의 역할을 하는 프로세스와 소켓을 통해 IPC를 수행하는 것

 

시스템 콜:

응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스

사용자 프로그램이 디스크 파일을 접근하거나 화면에 결과를 출력하는 등의 작업이 필요한 경우,

사용자 프로그램이 특권 명령의 수행을 필요로 하는 경우, 운영체제에 특권 명령의 대행을 요청하는 것이 시스템 콜

 

시스템 콜은 여러 종류의 기능으로 나누어진다, 각 시스템 콜에는 번호가 할당되고 시스템 콜 인터페이스는 시스템 콜 번호시스템 콜 핸들러 함수주소로 구성되는 시스템 콜 테이블을 유지한다. (시스템 콜 테이블 = 시스템 콜 번호 + 시스템 콜 핸들러 함수주소)?

 

운영체제는 자신의 커널 영역에서 해당 인덱스가 가리키는 주소에 저장되어있는 루틴을 수행한다. > 작업이 완료되면 CPU에게 인터럽트를 발생시켜 수행이 완료되었음을 알린다.

 

 

경우에 따라 시스템 콜이 발생했을때 추가적인 정보가 필요할 수도 있는데 그러한 정보가 담긴 매개변수들은 OS에 어떻게 전달할까?

가장 선호되는 방식(매개변수의 갯수나 길이의 제한이 없기 때문에 선호되는 방식):

매개변수를 메모리에 저장해 해당 메모리의 주소를 레지스터에 전달, 매개변수는 프로그램에 의해 스택에 전달.

 

 

시스템 콜의 예시

in.txt에 있는 파일 내용과 같은 내용을 복사하여 out.txt 파일을 만드는 것

위와같은 명령어를 입력하면 순차적으로 호출되는 시스템 콜은 어떤것이 있을까?

1. 먼저 사용자로부터 입력을 받는데 이때 I/O 시스템 콜 호출이 필요.

2. 이후 'cp' 프로그램을 실행시키면서 in.txt파일이 현재 디렉토리에서 접근 가능한지 확인하기 위한 시스템 콜 호출

3. 이때 접근이 불가능하다면 에러를 발생시키고 프로그램이 종료되는데 이때 시스템 콜 호출.

4. 파일이 존재해 접근이 가능하다면 복사한 파일을 저장하기 위해 out.txt 파일명이 있는지 검사하기 위한 시스템 콜 호출

5. 만약 파일명이 이미 존재한다면(파일명 존재하는지 안하는지 시스템콜 호출을 통해 확인) 덮어 씌워야할지 이어 붙어야할지 User에게 물어볼수있는데 만약 저장하고자 하는 파일 이름이 겹치지않는다면 파일을 저장해야하는데 이때도 시스템 콜 호출.

 

 

시스템 콜이 필요한 이유는?

우리가 일반적으로 사용하는 프로그램은 '응용 프로그램'이다. 유저레벨의 프로그램은 유저레벨의 함수들만으로는 많은 기능을 구현하기 힘들기 때문에 커널의 도움을 반드시 받아야한다. 

이러한 작업은 응용프로그램으로 대표되는 유저 프로세스에서 유저모드에서는 수행할 수 없다.

반드시 커널에 관련된 것은 커널모드로 전환한 후에야 해당 작업을 수행할 권한이 생긴다.

 

그렇다면 권한은 왜 필요한 것일까?

만약 권한이 없을때 해커가 피해를 입히기 위해 악의적으로 시스템 콜을 사용하는 경우나 초보 사용자가 하드웨어 명령어를 잘 몰라서 아무렇게 함수를 호출했을 경우에 시스템 전체를 망가뜨릴 수도 있기 때문이다. 따라서 이러한 명령어들은 특별하게 커널 모드에서만 실행 할 수 있도록 설계되었고, 만약 유저 모드에서 시스템 콜을 호출할 경우에는 운영체제에서 불법적인 접근이라 여기고, 트랩을 발생시킨다.

 

 

유저 모드와 커널 모드

유저 모드

PC register가 사용자 프로그램이 올라가 있는 메모리 위치를 가리키고 있을 때 현재 사용자 프로그램을 수행중이라고 하며 CPU 가 유저 모드에서 수행중이라고 말한다.

 

커널모드

PC register가 운영체제가 존재하는 부분을 가리키고 있다면 현재 운영체제의 코드를 수행중이라고 하며 CPU가 커널모드에서 수행중이라고 말한다.

 

일반 명령과 특권 명령

CPU 내에 모든 비트를 두어서 구분한다.

0 - 커널모드 / 1 - 유저모드

 

일반 명령 (유저모드)

메모리에서 자료를 읽어와서 CPU 에서 계산하고 결과를 메모리에 쓰는 일련의 명령들, 모든 프로그램이 수행 가능함.

 

특권 명령 (커널모드)

보안이 필요한 명령, 입출력 장치, 타이머 등 각종 장치에 접근하는 명령

 

 

[운영체제 / OS] 프로세스와 스레드

프로세스 : 운영체제로부터 자원을 할당 받은 작업의 단위

스레드 : 프로세스가 할당받은 자원을 이용하는 **실행 흐름의 단위

 

멀티스레드의 장단점?

 

스레드는 프로세스내에서 stack 메모리 영역을 제외한 다른 메모리 영역을 같은 프로세스 내 다른 스레드와 공유하기 때문에 메모리 낭비를 줄일 수 있다. 또한, 통신 부담이 적어 응답 속도가 빠르다.

다만, 스레드의 스케줄링은 운영체제가 처리하지 못하기 때문에 동기화 문제에 대응할 수 있어야하고, 한개의 스레드에 문제가 발생할 경우 모든 프로세스가 중단될 수 있다는 단점이 있다.

 

 

프로세스란?

실행중에 있는 프로그램

메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)

스케줄링의 대상이 되는 작업(task)와 같은 의미로 쓰인다.

프로세스 내부에는 최소 하나의 스레드를 가지고 있는데, 실제로는 스레드 단위로 스케줄링을 한다.

하드디스크에 있는 프로그램을 실행하면, 실행을 위해서 메모리 할당이 이루어지고, 할당된 메모리 공간으로 바이너리 코드가 올라가게된다. > 이 순간부터 프로세스라 불린다.

 

 

프로세스의 문맥

CPU 수행 상태를 나타내는 하드웨어 문맥

 

프로세스의 메모리 영역

- Code 영역 : 실행할 프로그램의 코드나 명령어들이 기계어 형태로 저장된 영역. CPU는 코드영역에 저장된 명령어들을 하나씩 처리함.

- Data 영역: 코드에서 선언한 전역 변수와 정적 변수가 저장되는 영역. 프로그램이 실행되면서 할당되고 종료되면서 소멸

- Stack 영역 : 함수 안에서 선언된 지역변수, 매개변수, 리턴값등이 저장된다. 함수 호출시 기록되고 종료되면 제거된다.

- Heap 영역 : 관리가 가능한 데이터 이외의 다른 형태의 데이터를 관리하기 위한 자유공간.

 

프로세스 관련 커널 자료구조

- PCB(Process Control Block)

- Kernel Stack

 

 

프로세스의 상태

프로세스는 상태가 변경되며 수행된다.

Running : CPU를 잡고 instruction을 수행중인 상태

Ready : CPU를 기다리는 상태

Blocked(waiting, sleep) : CPU를 주어도 당장 instruction을 수행할 수 없는 상태, Process 자신이 요청한 event가 즉시 만족되지않아 이를 기다리는 상태, 예) 디스크에서 file을 읽어와야 하는 경우

New : 디스크에서 메모리로 프로그램이 올라가 실행준비를 하는 상태

Terminated : 수행이 끝난 상태

 

 

PCB(Process Control Block)

PCB는 운영체제가 프로세스를 표현한 자료구조이다. 특정 프로세스에 대한 정보를 갖고 있다. 각 프로세스가 생성될때마다 고유의 PCB가 생성되고, 프로세스가 완료되면 PCB는 제거된다. 

프로세스간 문맥 교환이 일어나면서, 프로세스는 진행하던 작업들을 PCB에 저장하고, 이후에 자신의 순서가 왔을때 이어서 처리한다.

 

 

문맥교환(Context Switch)

하나의 프로세스가 이미 CPU를 사용중인 상태에서 다른 프로세스가 CPU를 사용하기 위해 이전 프로세스의 상태를 저장하고 새로운 프로세스의 상태를 적재하는 것.

 

예시) 카카오톡을 켜놓고 유튜브로 노래를 들으면서 웹 서핑을 하는 것은 사용자 입장에서 동시에 일어나는 일처럼 보이지만 실제로는 그렇지않음.

현재 프로세스 A가 CPU를 사용하고 있는 상황에서 CPU 사용시간이 끝나, 다음 프로세스에게 CPU를 넘겨주어야한다. 스케줄링 알고리즘에 의해 다음 CPU를 받을 프로세스 B가 선택되었고, 타이머 인터럽트가 발생해 CPU 제어권이 운영체제 커널에 넘어가게 된다.

이 과정에서 운영체제는 타이머 인터럽트 처리 루틴으로가서 직전까지 수행중이던 프로세스 A의 문맥을 자신의 PCB에 저장하고, 프로세스 B는 예전에 저장했던 자신의 문맥을 PCB로부터 실제 하드웨어로 복원 시키는 과정을 거치게된다.

CPU가 동시에 여러개의 프로세스를 실행시키는 것처럼 보이지만, 사실은 CPU가 재빠르게 여러 프로세스를 번갈아가며 실행하고 관리하고 있는것, 이때 프로세스를 번갈아가면서 처리하는 것을 Context Switching(문맥교환)이라고 한다.

 

오버헤드 : 문맥교환에 필요한 시간, 메모리

 

문맥교환이 아닌 경우?

프로세스가 실행 상태일때, 시스템 콜이나 인터럽트가 발생하면 CPU의 제어권이 운영체제에게로 넘어와 원래 실행중이던 프로세스의 업무를 잠시 멈추고 운영체제의 코드가 실행된다.

이는, 하나의 프로세스가 사용자 모드에서 실행되다가, 커널 모드로 실행 모드만 바뀌는 것일뿐 CPU를 점유하는 프로세스가 다른 사용자 프로세스로 변경되는 과정이 아니기 때문

문맥교환 x
문맥교환 o

 

스레드

프로세스 하나만을 사용해서 프로그램을 실행하기에는 메모리의 낭비가 발생한다.

스레드는 프로세스와 다르게 스레드간 메모리를 공유하며 작동한다.

즉, 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위이다. 스레드는 운영체제의 스케줄러에 의해 독립적으로 관리될 수 있는 프로그래밍된 명령어의 가장 작은 시퀀스이다. 하나의 프로세스는 하나 이상의 스레드를 갖고 있다.

 

 

프로세스와 스레드의 차이점

운영체제는 프로세스마다 독립된 메모리 영역을 Code/Data/Stack/Heap 의 형식으로 할당한다.

각각 독립된 메모리 영역을 할당해주기 때문에 프로세스는 다른 프로세스의 변수나 자료에 접근 할 수 없다.

 

이와 다르게 스레드는 메모리를 서로 공유할 수 있다.

프로세스가 할당받은 메모리 영역 내에서 Stack 형식으로 할당된 메모리 영역은 다로 할당받고, 나머지 Code/Data/Heap 형식으로 할당된 메모리 영역을 공유한다.

따라서, 각각의 스레드는 별도의 스택을 가지고 있지만 힙 메모리는 서로 읽고 쓸 수 있게된다.

 

정리해보면 프로세스는 운영체제로 부터 별도의 메모리 영역을 할당 받고, 스레드는 Stack을 제외한 Code/Data/Heap 부분은 공유해 서로 읽고 쓸 수 있게 된다.(공유 자원을 가짐)

 

이런식으로 메모리는 공유하는 이유?

스레드는 "흐름의 단위"로 CPU의 입장에서 최소 작업 단위가 된다. 

반면 운영체제는 이런 작은 단위까지 직접 작업하지 않기 때문에 운영체제 관점에서는 프로세스가 최소 작업 단위가 된다.

여기서 중요한 점은 하나의 프로세스는 하나 이상의 스레드를 가진다는 점이다. 

따라서, 운영체제 관점에서는 프로세스가 최소 작업 단위인데, 이때문에 같은 프로세스 소속의 스레드끼리 메모리를 공유하지 않을수 없다.

 

멀티태스킹, 멀티스레드

 

멀티태스킹이란 하나의 운영체제 안에서 여러 프로세스가 실행되는 것을 의미한다.

멀티태스킹은 자칫하면 여러 프로세스가 동시에 실행되는 것처럼 보이지만, 자세한 원리를 보면 그렇지 않다.

이는 운영체제의 스케줄링 방식에 의하여 설명될 수 있다.

 

멀티태스킹 : 하나의 운영체제 안에서 여러 프로세스가 실행

멀티스레드 : 하나의 프로세스가 여러 작업을 여러 스레드를 사용해 동시에 처리하는 것

 

멀티스레드의 장점 : 메모리 자원 아낄수있음, Stack 영역을 제외한 모든 메모리를 공유하기에 통신 부담이 적어서 응답 시간 빠름.

단점 : 스레드 하나가 자원 망치면 모든 프로세스가 종료될 수 있음, 동기화 문제(교착 상태가 발생하지 않도록 주의해야됨)

 

동기화 문제란? 

멀티스레드를 사용하면 각각의 스레드중 어떤 것이 어떤 순서로 실행될지 순서를 알 수 없는데 만약 A스레드가 어떤 자원을 사용하다가 B스레드로 제어권이 넘어 간 후 B스레드가 해당 자원을 수정 했을때, 다시 제어권을 받은 A 가 해당 자원에 접근하지 못하거나, 바뀐 자원에 접근하게 되는 오류임

(여러 스레드가 함께 전역 변수를 사용할 경우 발생할 수 있는 충돌을 동기화 문제)

 

 

프로세스 끼리는 정보 공유가 불가능 할까?

사실 프로세스끼리 정보를 공유하는 것은 다음 방법으로 가능함.

다만, 이 경우에는 단순히 CPU 레지스터 교체뿐만 아니라 RAM과 CPU 사이의 캐시 메모리까지 초기화 되기때문에 앞서 말했듯 자원 부담이 크다.

 

 

TCP / IP 계층

 

네트워크 통신이 이루어지는 과정을 살펴보겠다.

우리가 컴퓨터를 켠 뒤, 크롬 브라우저를 열어 네이버 웹사이트를 접속한다고 하면 아래와 같은 흐름으로 통신이 이루어진다.

1. 크롬 브라우저 검색창에서 www.naver.com  을 입력

2. 크롬 브라우저는 이를 네트워크 통신 가능한 형태로 만든다.(보통 패킷이라 부름)

3. 이 패킷을 네트워크에 흘려 보낸다.

4. 네트워크 중간에 있는 기기들이 이 패킷을 읽고 네이버 서버로 전달

5. 네이버 서버는 이 패킷을 다시 풀어, 웹서버가 읽을 수 있는 형태로 만들고 웹서버에 전달

 

중요한 점은 우리는 단순히 사람이 이해할 수 있는 URL만을 입력했지만 내부적으로 패킷과 같은 특정 형태로 데이터를 만든다는 것이다. 

또한 이렇게 만들어진 패킷은 수신받는 쪽에서 다시 사람이 이해할 수 있는 데이터로 만들어진다는 것.

 

 

osi 7계층 모델

위 과정을 OSI 7계층 모델이란 개념 위에서 표현하면 다음 그림과 같이 된다.

이 그림에서 우리는 '송신호스트'가되고 네이버 서버는 '수신 호스트'가 된다.

 

먼저 우리는 브라우저에 url을 입력한다.(응용)

이 데이터는 전송 전에 내부적으로 표현, 세션, 전송, 네트워크, 데이터 링크 계층을 차례대로 지나며 네트워크 통신에 필요한 데이터를 기존 데이터에 추가한다.

각 계층은 순서를 가지며, 응용 > 물리 계층 순으로 전달된다.

각 계층은 이전 계층으로부터 데이터를 전달 받으며, 자신의 계층에서 필요한 데이터들을 기존 데이터에 추가로 붙인다.

데이터를 붙인다는 것은 예를들어 어떤 ip로 전달할지 등의 정보를 더한다는 것이다.

물리 계층에서는 이렇게 계층을 지나며 만든 데이터를 실제로 네트워크에 전송한다.

네이버 서버는 이 데이터를 수신받아 다시 물리 > 응용 계층 순으로 전달한다

각 계층은 마찬가지로 이전 데이터로부터 데이터를 전달 받으며, 필요한 데이터만 분리하여 해석한다.

최종적으로 응용 계층에서는 요청을 웹서버에 전달한 뒤, 웹서버는 송신자에게 필요한 응답(네이버 메인 페이지)를 다시 데이터로 만들게 된다.

 

 

프로토콜

두사람과 편지를 주고받으려면 누가 누구에게 어디로, 어떤 내용인지를 우체부에게 알려줘야한다.

만약 어떤 형태로 남기자고 약속한게 이 형태라면

FROM: 보내는 사람
TO: 받을 사람
ADDRESS: 받는 사람 주소
MESSAGE: 전달할 내용

 

이 형태를 사용하려면 이렇게 써야한다.

FROM: 그랩
TO: 하디
ADDRESS: 서울시 서울숲역 디타워 5층
MESSAGE: 하디, 잘 지내고 있나요?

이제 우체부는 위 내용을 보고 이해 한 후 편지를 전달해줄것이다.

이 형태처럼 통신을 위해 형태 또는 규약을 프로토콜이라고 한다.

여기서는 두 사람과 우체부가 이 프로토콜을 사용했다고 할 수 있다.

네트워크에서는 많은 프로토콜이 있는데, 모두 데이터를 올바르게 전달하고 받기 위해 존재한다.

익히 들어본 HTTP, TCP, UDP가 바로 이러한 프로토콜의 대표적인 예시이다.

 

 

OSI 7계층 모델(국제표준화기구에서 개발한 네트워크 통신 표준 모델)

위의 네트워크 통신의 과정을 7단계의 계층으로 나눈 설계를 의미.

이렇게 계층을 나누어 놓으면 뭐가 좋을까?

전체적으로 필요한 일을 여러 계층으로 나누면 계층별 해야 할 일이 명확해진다.

전체적인 설계 모델이 있음으로써, 설명하기도 이해하기도 쉽다.

각 층을 나누면 각 층별로 필요한 데이터들을 표준화 할 수도 있다.

 

계층 7 : 응용 계층

웹서비스의 UI 부분, 사용자의 입출력 (I/O)을 담당

우리가 보통 개발하는 프론트 엔드, 백엔드 서버가 바로 이 레이어 위에서 동작함.

즉, 대부분의 사람이 사용하는 웹 서비스는 이 레이어 위에서 제공된다고 생각하면 된다.

 

계층 6 : 표현 계층

응용계층과 네트워크 계층을 위해 계층 간 데이터를 적절히 표현하는 부분을 담당

이미지 압축, 데이터 암호화등의 작업이 이 레이어에서 동작

 

계층 5 : 세션 계층

통신은 실제로 세션이라고 하는 단위 위에서 이루어지는데, 이 계층은 이러한 통신 세션을 구성

 

계층 4 : 전송 계층

컴퓨터로 들어온 네트워크 데이터를 어느 포트로 보낼지 담당

신뢰성 있는 데이터를 보장하는 역할을 담당

 

계층 3 : 네트워크 계층

IP주소를 사용하여 네트워크 데이터를 어느 컴퓨터로 보낼지 담당

라우터라는 기계가 담당

 

계층 2 : 데이터 링크 계층

네트워크 카드의 MAC 주소를 사용해 네트워크 데이터를 어느 컴퓨터로 보낼지 담당

 

계층 1 : 물리 계층

디지털 데이터를 아날로그적인 전기적 신호로 변환하여 네트워크 전선에 흘려보낸다.

 

TCP/IP 4계층 모델이란?

실제 대부분의 인터넷 통신은 IP와 TCP에 기반한 일명 TCP/IP 통신을 사용한다.

그리고 이 통신에 특화된 네트워크 통신 계층 모델이 따로 존재하는데, 이걸 TCP/IP 4계층 모델이라고 한다. OSI 7계층과 유사하지만, 아래 그림처럼 4계층으로 간소화 되어있다.

  • 계층 4 : 응용 계층(Application Layer)
    • OSI 7계층 모델의 7,6,5(응용, 표현, 세션) 계층 기능을 담당한다.
    • HTTP, Telent, SSH, FTP와 같은 프로토콜이 여기에서 사용된다.
  • 계층 3 : 전송 계층(Transport Layer)
    • OSI 7 계층 모델의 4(전송) 계층과 같다. 프로세스 간의 신뢰성 있는 데이터 전송을 담당한다.
    • TCP, UDP와 같은 프로토콜이 여기에서 사용된다.
  • 계층 2 : 인터넷 계층 (Internet Layer)
    • OSI 7계층 모델의 3(네트워크) 계층과 같다. 컴퓨터 간 라우팅을 담당한다.
  • 계층 1: 네트워크 인터페이스 계층 (Network Interface Layer)
    • OSI 7계층 모델의 2, 1(데이터 링크, 물리)계층과 같다. 네트워크 통신의 물리적인 부분들을 주로 포함한다.

 

 

HTTP vs HTTPS

HTTP는 보안에 취약하다. 그래서 통신 과정에도 응용 계층의 데이터를 암호화할 필요성이 느껴졌는데, 이로 인해 보안을 위한 레이어 SSL(Secure Sockets Layer, 현재는 TLS라는 명칭으로도 사용)가 생기게 된다. 해당 레이어는 응용 계층과 전송 계층 사이에 존재한다.

이렇게 보안의 역할을 하는 SSL 계층을 기반으로 하는 HTTP 통신을 HTTPS라고 한다.
HTTPS는 통신 보안을 포함하고 있으므로, 당연히 HTTP보다 더 좋다. 다만 내용을 암호화하고 복호화하는 로직이 추가되었으므로, 기존보다 통신 로직이 좀 더 복잡해진다.

 

 

TCP와 UDP의 차이

 

이전 글에서 보았던 것처럼 TCP와 UDP는 TCP/IP 의 전송계층에서 사용되는 프로토콜이다.

전송계층은 IP에 의해 전달되는 패킷의 오류를 검사하고 재전송 요구 등의 제어를 담당하는 계층이다.

 

TCP vs UDP

TCP는 Transmission Control Protocol의 약자이고 UDP는 User Datagram Protocol의 약자이다.

두 프로토콜은 모두 패킷을 한 컴퓨터에서 다른 컴퓨터로 전달해주는 IP 프로토콜을 기반으로 구현되어 있지만, 서로 다른 특징을 가지고있다.

TCP
UDP

TCP에 비해서 UDP가 굉장히 일반적이라는것을 볼 수 있다.

즉, 신뢰성이 요구되는 애플리케이션에서는 TCP를 사용하고 간단한 데이터를 빠른 속도로 전송하고자 하는 애플리케이션에서는 UDP를 사용한다.

 

 

 

TCP

tcp는 네트워크 계층 중 전송 계층에서 사용하는 프로토콜로서, 장치들 사이에서 논리적인 접속을 성립하기 위하여 연결을 설정하여 신뢰성을 보장하는 연결형 서비스이다.

네트워크에 연결된 컴퓨터에서 실행되는 프로그램 간에 일련의 옥텟(데이터, 메세지, 세그먼트라는 블록 단위)를 안정적으로, 순서대로, 에러없이 교환할 수 있게 한다.

 

특징:

 

연결형 서비스로 가상 회선 방식을 제공한다.

3-way-handshaking 과정을 통해 연결 설정

4-way-handshaking 을 통해 연결 해제

 

데이터 처리 속도를 조절하여 수신자의 버퍼 오버플로우 방지(흐름제어)

송신하는 곳에서 감당이 안되게 많은 데이터를 빠르게 보내 수신하는 곳에서 문제가 일어나는 것을 막는다.

수신자가 윈도우 크기 값을 통하여 수신량을 정할 수 있음

 

네트워크 내의 패킷 수가 넘치게 증가하지 않도록 방지(혼잡 제어)

정보의 소통량이 과다하면 패킷을 조금만 전송하여 혼잡 붕괴 현상이 일어나는 것을 막는다.

 

신뢰성이 높은 전송

정상적인 상황에서는 ack값이 연속적으로 전송되어야한다.

그러나 ack값이 중복으로 올 경우 패킷 이상을 감지하고 재전송을 요청한다.

일정시간동안 ack값이 수신을 못할 경우 재전송을 요청한다.

 

전이중, 점대점 방식

전이중 : 전송이 양방향으로 동시에 일어날 수 있다

점대점 : 각 연결이 정확히 2개의 종단점을 가지고 있다.

=> 멀티캐스팅이나 브로드캐스팅을 지원하지 않는다

 

TCP Header

응용 계층으로 부터 데이터를 받은 TCP는 헤더를 추가한 후에 이를 IP로 보낸다. 헤더에는 아래 표와같은 정보가 포함된다.

 

송수신자의 포트 번호 TCP로 연결되는 가상 회선 양단의 송수신 프로세스에 할당되는 포트 주소 16
시퀀스 번호(Sequence Number) 송신자가 지정하는 순서 번호, 전송되는 바이트 수를 기준으로 증가.
SYN = 1 : 초기 시퀀스 번호가 된다. ACK 번호는 이 값에 1을 더한 값.
SYN = 0 : 현재 세션의 이 세그먼트 데이터의 최초 바이트 값의 누적 시퀀스 번호
32
응답 번호(ACK Number) 수신 프로세스가 제대로 수신한 바이트의 수를 응답하기 위해 사용. 32
데이터 오프셋(Data Offset) TCP 세그먼트의 시작 위치를 기준으로 데이터의 시작 위치를 표현(TCP 헤더의 크기) 4
예약 필드(Reserved) 사용을 하지 않지만 나중을 위한 예약 필드이며 0으로 채워져야한다. 6
제어 비트(Flag Bit) SYN, ACK, FIN 등의 제어 번호 -> 아래 추가 설명 참조 6
윈도우 크기(Window) 수신 윈도우의 버퍼 크기를 지정할 때 사용. 0이면 송신 프로세스의 전송 중지 16
체크섬(Checksum) TCP 세그먼트에 포함되는 프로토콜 헤더와 데이터에 대한 오류 검출 용도 16
긴급 위치(Urgent Pointer) 긴급 데이터를 처리하기 위함, URG 플래그 비트가 지정된 경우에만 유효 16

 

제어비트 정보

URG 긴급 위치를 필드가 유효한지 설정
ACK 응답 번호 필드가 유효한지 설정. 클라이언트가 보낸 최초의 SYN 패킷 이후에 전송되는 모든 패킷은 이 플래그가 설정되어야 한다. 자세한 내용은 아래 추가 설명 참조
PSH 수신 애플리케이션에 버퍼링된 데이터를 상위 계층에 즉시 전달할 때
RST 연결의 리셋이나 유효하지 않은 세그먼트에 대한 응답용
SYN 연결 설정 요구. 동기화 시퀀스 번호. 양쪽이 보낸 최초의 패킷에만 이 플래그가 설정되어 있어야 한다.
FIN 더 이상 전송할 데이터가 없을 때 연결 종료 의사 표시

ack 제어비트 : ack는 송신측에 대하여 수신측에서 긍정 응답으로 보내지는 전송 제어용 캐릭터

ack 번호를 사용하여 패킷이 도착했는지 확인한다 > 송신한 패킷이 제대로 도착하지 않았으면 재송신을 요구한다

 

 

TCP의 연결 및 해제

TCP connection(3-way handshake)

먼저 open()을 실행한 클라이언트가 SYN을 보내고 SYN_SENT상태로 대기한다.

서버는 SYN_RCVD 상태로 바꾸고 SYN과 응답 ACK를 보낸다.

SYN과 응답 ACK를 받은 클라이언트는 ESTABLISHED상태로 변경하고 서버에게 응답 ACK를 보낸다.

응답 ACK를 받은 서버는 ESTABLISHED 상태로 변경한다.

 

TCP disconnection(4-way handshake)

먼저 close()를 실행한 클라이언트가 FIN을 보내고 FIN_WAIT1 상태로 대기한다.

서버는 close_wait로 바꾸고 응답 ack를 전달한다. 동시에 해당 포트에 연결되어 있는 어플리케이션에게 close()를 요청한다.

ack를 받은 클라이언트는 상태를 fin_wait2로 변경한다.

close()요청을 받은 서버 어플리케이션은 종료 프로세스를 진행하고 fin을 클라이언트에 보내 last_ack상태로 바꾼다.

fin을 받은 클라이언트 ack를 서버에 다시 전송하고 time_wait로 상태를 바꾼다. time_wait에서 일정 시간이 지나면 closed된다. 

ack를 받은 서버도 포트를 closed로 닫는다.

 

반드시 서버만 close_wait상태를 갖는 것은 아니다.

서버가 먼저 종료하겠다고 fin을 보낼 수 있고, 이런 경우 서버가 fin_wait1상태가 된다

누가먼저 close를 요청하느냐에 따라 상태가 달라질 수 있다.

 

 

UDP Header

송신자의 포트 번호 16 데이터를 보내는 애플리케이션의 포트 번호
수신자의 포트 번호 16 데이터를 받을 애플리케이션의 포트 번호
데이터의 길이 16 UDP 헤더와 데이터의 총 길이
체크섬(Checksum) 16 데이터 오류 검사에 사용

 

tcp헤더와는 차별적으로 udp헤더에는 포함된 정보가 부실한 느낌이다.

udp는 수신자가 데이터를 받는지 마는지 관심이 없기 때문이다. 즉, 신뢰성을 보장해주지 않지만 간단하고 속도가 빠름.

 

udp tcp의 공통점 : 포트번호를 이용하여 주소를 지정, 데이터 오류 검사를 위한 체크섬 존재

차이점 :

TCP(Transfer Control Protocol)                                                   UDP(User Datagram Protocol)

연결이 성공해야 통신 가능(연결형 프로토콜) 비연결형 프로토콜(연결 없이 통신이 가능)
데이터의 경계를 구분하지 않음(Byte-Stream Service) 데이터의 경계를 구분함(Datagram Service)
신뢰성 있는 데이터 전송(데이터의 재전송 존재) 비신뢰성 있는 데이터 전송(데이터의 재전송 없음)
일 대 일(Unicast) 통신 일 대 일, 일 대 다(Broadcast), 다 대 다(Multicast) 통신

 

'Server' 카테고리의 다른 글

Server - 2 AWS (VPC & Internet Gateway & EC2)  (1) 2024.03.26

회원 등록과 회원 목록을 조회할수있는 기능을 구현 해볼것이다.

우선 회원 등록을 위해선 회원 등록 form이 필요하기때문에 이것먼저 구현할 것이다.

(입력받을 값은 많이는 말고 이름, 지역, 거리명, 우편번호를 받을것이다.)

이때 이름 입력을 필수로 받기 위해서 NotEmpty 어노테이션을 사용하여서 필수로 입력되게끔 해줄것이다.

이때 이 어노테이션을 사용하기 위해서는 gradle파일에 

implementation 'org.springframework.boot:spring-boot-starter-validation'

이것을 추가해줘야한다.

그 후

멤버 컨트롤러를 구현해준다.

목록을 조회해줄땐

이렇게 멤버 리스트를 html 파일에 전달을 해주면 된다.

 

다음으로는 상품 등록과 상품 목록 조회를 진행해볼것이다.

우선 책의 form형인 BookForm 클래스를 만들어준다

 

아까 만들어둔 클래스에 대한 것을 model로 리소스 하위파일인 html파일에 데이터를 넘겨줘서 화면을 구성해주고,

postmapping을 통하여 여러 값들을 넣어주고 넣어진 값들이 저장이 됐으면 redirect시켜준다.

 

목록을 조회할땐 저번과 마찬가지로 list를 뽑아서 model을 사용하여서 넘겨준다.

 

JPA 동적 쿼리를 개발할건데 사용자의 이름과 상태를 통해서 검색을 하는 검색 기능을 만들어볼 것이다.

여러 방법들로 해볼건데

우선 1번째 방법(무식한 방법 jpql에 문자열 때려넣기)

 

하지만 이런식으로 문자를 더하면서 하는것은 엄청 힘든 방식임(버그를 찾기가 힘듬)

 

다음으로는 동적쿼리를 Criteria를 사용하여서 해결해보겠다.

하지만 criteria를 사용하여서 동적 쿼리를 처리하였을때의 단점이 있다.

유지보수가 거의 불가능하다.

 

다음으로는 Querydsl로 처리를 해볼것이다.

우선 테스트 코드 작성시에 멤버에 대한 정보를 세팅해주고 book 아이템 객체를 생성하여서 이름, 가격, 재고를 설정해주었다.

그 후 order메서드를 사용하여서 주문을 걸어주었고 추출된 order id로 findOne 메서드를 사용하여서 주문객체를 받아와주었다.

 

 

테스트 목록: 주문시 상태, 주문시 상품의 종류 수, 주문한 상품의 총 가격, 주문 수량만큼 전체 재고에서 빠지는지

 

돌려본 결과 이상 없이 잘 돌아갔다.

 

다음으로는

예외 테스트로 상품 주문 수량이 총 재고보다 많을시에 상황을 테스트 할것이다.

나는 Junit5로 테스트 코드를 작성중이라 Junit4에 있는 test어노테이션에 쓸수있는 expected옵션이 없어서 

대신 assertThrows를 사용하여 람다 형식으로 예외 발생을 검증해주었다.

 

다음은 주문 취소 테스트 케이스를 구현해볼 것이다.

주문 취소 테스트 케이스는 주문 생성 후 주문 취소를 시키고 주문 취소가 된 상태가 CANCLE이 됐는지 검증

추가적으로 재고가 원복되었는지 검증해주었다.

구현 해야할 기능

- 상품 주문 기능

- 주문 내역 조회 기능

- 주문 취소 기능

 

주문 엔티티, 주문 상품 엔티티 개발

 

주문을 생성하는게 많이 복잡함

그래서 이렇게 복잡한건 별도의 생성 메서드가 있으면 좋다. 따라서 order 클래스에 생성 메서드를 만들어준다.

 

다음으로는 비즈니스 로직을 구현할 것이다.

주문 취소 로직 작성시에는 배송이 되어버린 상태라면 취소를 못하는 케이스를 넣었고,

주문 취소시에는 order상태를 cancle로 만들어주고 반복문을 이용하여서 cancle을 처리해주어야한다(소비자가 상품을 2개 주문했으면 2가지 모두에 취소를 해야하기 때문)

그래서 orderItem 클래스에도 cancle 메서드를 만들어줘야 한다.

 

 

다음으로는 비즈니스 로직 이외로 조회로직도 만들어줘야 한다.

우선 전체 가격 조회 로직을 만들어보겠다.

이렇게 for문으로 돌면서 totalprice를 가져와서 더하는 방식으로해야하는 이유는 orderItem 내부에 상품 가격과 주문 수량이 있기때문에 orderItem 클래스 내부에서 그 둘을 곱한 가격을 가져와야한다.

 

 

stream을 사용하여서 코드 축소

 

orderItem에서도 생성 메서드를 만들어줄 것이다.

 

 

주문 리포지토리 개발

 

다음으론 orderservice 클래스를 만들어야하는데

이때 구현해야 할 기능들로는 주문 기능, 취소 기능, 검색 기능 을 구현해야한다.

우선 주문 기능을 구현했다

 

다음으로는 주문 취소 기능을 구현할것이다.

주문 취소 기능은 orderid로 주문을 추출하여서 cancle메서드로 취소하는 방식으로 하였다.

 

다음은 예외 테스트로 상품 주문 갯수 재고 초과 테스트를 해볼것이다.

 

Junit5를 사용하여서 테스트 하고있기때문에 junit4에서 지원하는 test 어노테이션에 expected를 사용할수 없기에

assertThrows를 사용하여 람다 형식으로 검증을 해주었다.

 

상품 엔티티 개발

 

상품을 주문할때 재고가 늘고 줄고 하는 로직을 넣어야한다.

근데 여기서 값을 변경해줄땐 다른곳에서 참조해서 변경하는거보다 그냥 그 엔티티 자체에서 변경하는게 좋다.

비즈니스 로직중 재고 수량 관리 로직을 구현해볼것이다.

Item 클래스에서 값을 바꾸는 메서드를 작성해줌.

 

상품 리포지토리 개발

아이템 저장, 단일조회, 전체조회 기능을 구현할 것이다.

 

상품 서비스 개발

아이템 저장기능, 전체조회, 단일조회 기능을 넣어줄것이다.

Transactional 어노테이션은 기본 디폴트 값이 readOnly=false이므로 맨 최상단에 readOnly= true로 설정해주고 

개발된 기능들중 조회가 아닌 기능들에만 위에 Transactional어노테이션을 붙여주면된다.

(조회가 아닌 저장 기능에 readOnly = true 옵션을 적용해버리면 저장이 안됨)

 애플리케이션 아키텍처

유연하게 컨트롤러에서도 레포지토리 접근 가능하게 할것임.

 

개발순서: 서비스, 리포지토리 계층을 개발하고, 테스트 케이스를 작성해서 검증, 마지막에 웹 계층 적용

 

우선 회원 등록 및 회원 목록 조회 기능을 만들어 볼 것이다.

 

우선 멤버리포지토리를 만들어준다.

(JPQL 문법을 사용하여서 멤버 리스트 호출 및 특정 이름 리스트 호출하는것 잘보기)

 

그 다음으로는 멤버 서비스 클래스를 만들어줄 것이다.

멤버 서비스 클래스에서는 회원가입, 중복 회원 검증, 회원 전체조회, 회원 단일 조회 기능을 넣었다.

@RequiredArgsConstructor 어노테이션을 사용하여서 멤버 리포지토리와 멤버 서비스 클래스의 코드를 줄여주었다.

 

이제 구현한 기능들을 테스트 코드로 검증 할 것이다.(Junit 5로 테스트 케이스 작성)

테스트 요구사항

- 회원가입 성공

- 동일 이름있으면 에러 케이스 발생

 

회원가입 테스트 코드를 작성해주었다.

생성한 멤버객체에 이름을 설정해주고 가입을 시킨후 assertEquals 메서드를 사용하여 가입시킨 멤버객체와 가입시 도출된 id값으로 findOne메서드를 사용하여서 도출된 객체와 비교를 하여서 테스트를 진행하였다.

 

다음은 중복 회원 검증 테스트 코드를 작성하였다.

Junit4에서는 테스트 어노테이션에 expected 설정을 줄수있지만 나는 Junit5로 테스트를 진행중이기에 

assertThrows를 사용하여 람다형식으로 테스트를 진행해주었고, 같은 이름으로 가입시에 예외 케이스가 도출되도록 테스트를 진행하였다.

+ Recent posts