본문 바로가기

국비 교육

2020.10.26일자 수업 : stateless logIn 기능 구현, SQL문법

 stateless 통신에서 LogIn/Out 

 

git/eomcs-java-basic/src/main/java com.eomcs.net.stateful

클라이언트가 여러번 작업을 요청했을 때, 이전에 요청했던 작업을 수행하는 과정에서 발생한 정보를 토대로 작업을 수행하기 위해서는 그 정보를 서버 혹은 클라이언트 어딘가에 기록해야 한다. stateful 방식에서는 한번 연결되면, 연결이 끊어지기 전까지 저장한 정보를 유지하는 것이 용이하다. 그러나 stateless는 한번의 요청과 응답마다 연결이 끊어지기 때문에 정보를 유지하는 것이 까다롭다.

 

예를 들어보자. 계산기 서비스를 stateful 방식으로 제공하는 서버가 있다. 클라이언트에게서 기존 계산 결과에서 원하는 계산을 여러번 수행하기 위해 계산 결과를 그때 그때 저장하는 로컬 변수 하나가 필요하다.

 

stateful 통신 방식에서는 클라이언트가 원할 때까지 계속해서 연결을 끊지 않고, 작업을 수행하기 떄문에 한번의 processRequest 메서드 호출을 통해서만 작업이 수행된다. 따라서 processRequest 메서드 안에 이전 계산 결과를 저장하는 로컬 변수를 두면 클라이언트가 연결을 끊어 processRequest가 리턴될 때까지는 변수 값이 유지된다.

import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalcServer {
  public static void main(String[] args) throws Exception {
    System.out.println("서버 실행 중...");

    ServerSocket ss = new ServerSocket(8888);

    while (true) {
      Socket socket = ss.accept();
      try {
        processRequest(socket);
      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
        System.out.println("다음 클라이언트의 요청을 처리합니다.");
      }
    }
  }

  static void processRequest(Socket socket) throws Exception {
    try (Socket socket2 = socket;
        DataInputStream in = new DataInputStream(socket.getInputStream());
        PrintStream out = new PrintStream(socket.getOutputStream());) {

      int result = 0;

      loop: while (true) {
        String op = in.readUTF();
        int a = in.readInt();

        switch (op) {
          case "+":
            result += a;
            break;
          case "-":
            result -= a;
            break;
          case "*":
            result *= a;
            break;
          case "/":
            result /= a;
            break;
          case "quit":
            break loop;
        }

        out.printf("계산 결과: %d\n", result);
      }
      out.println("quit");
    }
  }
}

그러나 stateless 방식은 다르다. stateless는 한명의 클라이언트와 여러번의 단절된 연결을 통해 작업을 수행하므로 여러번의 processRequest 메서드 호출을 한다. 따라서 processRequest 메서드 안에 로컬 변수를 두어도 호출할 때마다 다시 초기화되므로 stateful 방식에서 사용하던 로컬 변수는 쓸모가 없어진다. 심지어는 여러 개의 스레드를 통해서 각 스레드 하나당 processRequest 메서드를 한번씩 호출하는 방식을 취할 때, 여러개의 스레드가 관리하는 여러 개의 스택에서 하나의 로컬 변수를 유지할 수 있을리가 없다.

 

따라서 기존의 stateless 방식으로 제공되는 계산기 서비스는 기존의 계산 결과를 갖고 계산을 하지 않고, 늘 새로운 계산을 수행했다.

import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class CalcServer {
  public static void main(String[] args) throws Exception {
    System.out.println("서버 실행 중...");

    ServerSocket ss = new ServerSocket(8888);

    while (true) {
      Socket socket = ss.accept();
      System.out.println("클라이언트 요청 처리!");
      try {
        processRequest(socket);
      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
        System.out.println("다음 클라이언트의 요청을 처리합니다.");
      }
    }
  }

  static void processRequest(Socket socket) throws Exception {
    try (Socket socket2 = socket;
        DataInputStream in = new DataInputStream(socket.getInputStream());
        PrintStream out = new PrintStream(socket.getOutputStream());) {

      int a = in.readInt();
      String op = in.readUTF();
      int b = in.readInt();
      int result = 0;

      switch (op) {
        case "+":
          result = a + b;
          break;
        case "-":
          result = a - b;
          break;
        case "*":
          result = a * b;
          break;
        case "/":
          result = a / b;
          break;
      }
      out.printf("%d %s %d = %d\n", a, op, b, result);
    }
  }
}

그러나 stateless 방식으로 각 클라이언트의 이전 수행 결과를 갖고 새로운 계산을 하려고 하려면, 동시에 수많은 클라이언트를 대응하는 서버에서  클라이언트를 구분하기 위한 정보가 필요하다. 결과적으로는 클라이언트의 인증 정보와 여태까지의 수행 결과 값을 서버에서 저장하고, 클라이언트도 이 인증정보를 받아서 늘 서버에게 작업을 요청할 때마다 스스로를 인증하는 방식을 취해야 한다. 구체적이 과정은 다음과 같다.

  • 클라이언트가 서버에 처음으로 작업을 요청하면 ID의 디폴트 값을 전송한다.
  • 서버는 클라이언트와 연결할 때마다 Map에 클라이언트의 ID를 key로 계산 결과를 저장하고, 클라이언트는 서버에게 요청할 때마다 자신의 ID를 보낸다. 서버는 Map에서 아이디를 갖고 수행 결과를 검색하며, 일치하는 아이디로 저장된 결과가 있다면 꺼내서 이어 계산한다. 만약 Map에 그러한 아이디로 저장된 것이 없다면 일단 작업을 수행한 후 Map에 새로 <id, 수행 결과>를 저장한다. 그리고 서버는 클라이언트 측으로 저장한 id를 보내준다. 클라이언트는 서버에게서 받은 id를 내부에서 저장하고, 서버에 요청할 때마다 그 id 정보를 보낸다. 서버는 id 정보를 통해 클라이언트를 구별하여 그 클라이언트 전용 정보를 갖고 작업을 수행할 수 있는 것이다.
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class CalcServer {
  static Map<Long, Integer> resultMap = new HashMap<>();

  static class RequestHandler extends Thread {

    Socket socket;

    public RequestHandler(Socket socket) {
      this.socket = socket;
    }

    @Override
    public void run() {
      try {
        processRequest(socket);
      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
      } finally {
        System.out.println("클라이언트 연결 종료!");
      }
    }
  }


  public static void main(String[] args) throws Exception {
    System.out.println("서버 실행 중...");

    ServerSocket ss = new ServerSocket(8888);

    while (true) {
      System.out.println("클라이언트의 연결을 기다림!");
      Socket socket = ss.accept();
      InetSocketAddress remoteAddr = (InetSocketAddress) socket.getRemoteSocketAddress();
      System.out.printf("클라이언트(%s:%d)가 연결되었음!\n", 
          remoteAddr.getAddress(), remoteAddr.getPort());

      RequestHandler requestHandler = new RequestHandler(socket);

      requestHandler.start();
      System.out.printf("%s 클라이언트 요청을 스레드에게 맡겼습니다!\n",
          remoteAddr.getAddress());

    }
  }

  static void processRequest(Socket socket) throws Exception {
    try (Socket socket2 = socket;
        DataInputStream in = new DataInputStream(socket.getInputStream());
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

      long clientId = in.readLong();

      String op = in.readUTF();
      int value = in.readInt();

      Integer obj = resultMap.get(clientId);
      int result = 0;

      if (obj != null) {
        System.out.printf("%d 기존 고객 요청 처리!\n", clientId);
        result = obj; 
      } else {
        clientId = System.currentTimeMillis();
        System.out.printf("%d 신규 고객 요청 처리!\n", clientId);
      }

      switch (op) {
        case "+":
          result += value;
          break;
        case "-":
          result -= value;
          break;
        case "*":
          result *= value;
          Thread.currentThread().sleep(10000);
          break;
        case "/":
          result /= value;
          break;
      }
      resultMap.put(clientId, result);
      out.writeLong(clientId);

      out.writeInt(result);

      out.flush();
    }
  }
}

네이버와 같은 포털도 마찬가지로 같은 방식으로 사용자 인증과정을 거쳐 작업을 수행한다.

클라이언트가 로그인을 하면, 서버는 사용자 정보를 DBMS에서 검색하고, 발견하면, 그에 대한 쿠키 정보를 Map에 저장 후, 클라이언트에게 그대로 전송한다. 클라이언트는 이 쿠키 정보를 저장하고 있다가 다시 서버에 요청할 때 그 쿠키 정보를 보내는데, 서버는 이 쿠키 정보를 갖고 로그인 정보를 꺼내서 응답한다. 여러개의 웹 브라우저 창들은 쿠키 정보들을 공유하므로, 다른 브라우저 창으로 접속해도 같은 쿠키 정보를 서버에 보내므로 로그인 상태를 유지할 수 있다. 이 쿠키 정보를 sessionId라고 한다.

 

다만 서버에서는 브라우저쪽으로 쿠키정보를 보낼때 만료시간을 함께 보내어 설정된 만료시점이 지나면 더이상 그 쿠키 정보를 보내지 않는다. 또한 서버쪽에서도 만료 시간이 지정되어 브라우저쪽에서 쿠키 정보를 보내도 그 쿠키 정보를 무효하게 취급할 수도 있다. 마지막으로 응답한 이후에 클라이언트가 10분동안 요청을 안하면 서버는 그 이후에 들어온 쿠키정보를 무시하고 자동 로그아웃을 처리한다. 이럴 경우에는 로그인 정보가 무효화되었으니, 다시 로그인해달라는 메시지가 뜨기도 한다. 


 SQL 문법 - DDL 

git/eomcs-docs/sql/exam01.sql

index

검색 조건으로 사용되는 컬럼을 인덱스로 지정할 수가 있으며 이를 이용하면 데이터를 빨리 찾을 수 있다. 인덱스는 특정 컬럼의 값을 A-Z 또는 Z-A로 정렬시키는 문법이다. 알파벳 순으로 정렬이 되어있으면 항목을 처음부터 끝까지 검사할 필요가 없어 검색 속도가 빨라질 수 밖에 없다. 

 

DBMS는 해당 컬럼의 값으로 정렬한 데이터 정보를 별도로 생성하므로 차지하는 데이터 용량이 두배가 된다. 그러나 요즘은 데이터를 저장하는데 들어가는 비용(자원의 비용이 점점 더 낮아지고 있으므로)보다 데이터를 더 빠르게 찾는 것의 이점이 훨씬 크다. 데이터에 변경사항이 생기면 이 인덱스 정보도 함께 갱신되므로, 조회속도는 빠르지만 데이터 입력/변경/삭제 속도는 느려진다는 단점도 있다.

 

table을 추가할 때 특정 칼럼을 인덱스로 설정하고 싶다면, 다음과 같은 명령어를 사용하면 된다.

fulltext index 인덱스 이름 (칼럼명)

예를 들면 name 칼럼을 test1_name_idx 인덱스로 설정하는 명령은 다음과 같다.

create table test1(
  no int primary key,
  name varchar(20),
  age int,
  kor int,
  eng int,
  math int,
  constraint test1_uk unique (name, age),
  fulltext index test1_name_idx (name)
);

이렇게 인덱스를 설정하면 데이터를 추가, 삭제, 변경할 때마다 인덱스를 갱신하므로 속도가 느려진다.

insert into test1(no,name,age,kor,eng,math) values(1,'aaa',20,80,80,80);
insert into test1(no,name,age,kor,eng,math) values(2,'bbb',21,90,80,80);
insert into test1(no,name,age,kor,eng,math) values(3,'ccc',20,80,80,80);
insert into test1(no,name,age,kor,eng,math) values(4,'ddd',22,90,80,80);
insert into test1(no,name,age,kor,eng,math) values(5,'eee',20,80,80,80); 

반면 데이터를 검색할 때에는 속도가 더 빨라진다. 이 속도의 차이는 데이터의 양이 기하급수적으로 늘어날 수록 뚜렷해진다.

select * from test1 where name = 'bbb';

테이블 변경

여기서는 기존에 이미 있는 테이블을 변경하는 방법을 소개한다.

테이블에 칼럼 추가

보통 테이블의 구조를 수정하고 싶을 때 alter라는 명령어를 사용한다.

첫 줄에 특정 테이블을 수정한다는 명령어를 작성하고, 그 다음 줄에 아래와 같이 특정 칼럼을 추가한다

alter table 테이블명
  add column 컬럼명 데이터 타입;

.

PK, Unique, Index 컬럼 지정

alter 명령어를 첫줄에 작성한 다음 PK와 UK는 add constraint 명령어를, index는 add fulltext 명령어를 위와 같이 작성한다.

alter table 테이블명
  add constraint PK명 primary key (컬럼명),
  add constraint UK명 unique (컬럼명, 컬럼명, ...),
  add fulltext index 인덱스명 (컬럼명);

칼럼에 옵션 추가

컬럼에 not null 등의 옵션을 추가하고자 하면, 테이블의 구조를 바꾸는 alter table 명령어를 첫줄에 쓰고, 컬럼의 특성을 수정한다는 modify column 명령어와 함께 컬럼명과 데이터 타입, not null이라는 특성을 써주면 된다.

alter table 테이블명
  modify column 컬럼명 데이터타입 not null;

컬럼 값 자동 증가

숫자 타입의 컬럼인 경우, auto_increment를 통해 값을 1씩 자동 증가시킬 수 있다. 단, 해당 컬럼이 key(PK / UK)여야 한다.

alter table 테이블명
  modify column 컬럼명 int not null auto_increment;

특정 컬럼을 auto_increment로 지정하면 데이터를 입력할 때 해당 컬럼의 값을 넣지 않아도 자동으로 증가된다. 단 삭제를 통해 중간에 비어있는 번호는 다시 채우지 않는다. 즉 증가된 번호는 계속 앞으로 증가할 뿐이다. auto_increment는 테이블에서 하나만 있어야한다.

 

Oracle에는 비슷한 역할을 하는 문법으로 auto_increment 대신 sequence 명령어가 있다.

 

데이터를 입력할 때 auto_increment 컬럼의 값을 직접 지정할 수가 있다. (auto_increment = no)

insert into test1(no, name) values(1, 'xxx');

auto_increment 컬럼의 값을 지정하지 않으면 해당 컬럼의 마지막 값을 하나 증가시켜서 입력한다.

insert into test1(name) values('aaa');

값을 삭제해도 auto_increment는 계속 하나씩 증가한다. 단, MariaDB에서는 값을 입력하다가 오류가 발생해서 입력에 실패하면 값이 증가되지 않는다.


View

조회 결과를 테이블처럼 사용하는 문법이다. select 명령어가 복잡할 때, 사용하면 편리하다.

create view 가상 테이블명
  as select 컬럼명, 컬럼명,... from 테이블명 where 컬럼명 = 값;

 

이렇게 한뒤에 show tables 하면 worker라는 테이블이 하나 더생기는데 이것은 진짜 테이블이 아니라 가상 테이블일 뿐이다.

 

가상 테이블의 

실무에서는 select 명령어가 2,30문장이 넘어가므로 가상 테이블이 있으면 훨씬 명령어가 간단해진다.

아주 긴 sql을 가상 테이블로 만든 것을 뷰라고 한다. view가 참조하는 테이블에 데이터를 입력한 후 view를 조회하면

새로 추가된 컬럼이 함께 조회된다.

따라서 이것은 별도의 table이 아니므로, 원래의 table에 변경사항이 생기면 가상 테이블에 바로 반영된다. 즉, 이것은 그냥 뷰를 조회할 떄마다 select 문장을 실행하는 것으로 이해해야 한다.

 

테이블을 만들고 데이터를 추가 변경 삭제하고 view 를 추가, 삭제, 변경 하는 등,데이터를 조작하는 것 이아닌 데이터를 어디에 어떻게 담을 것인가에 대한, 즉 데이터를 어떻게 정의할 것인가에 대한 문법을 DDL이라고 한다.


 SQL문 - DML 

 

git/eomcs-docs/sql/exam02.sql

create table test1 (
  no int not null,
  name varchar(20) not null,
  tel varchar(20) not null,
  fax varchar(20),
  pstno varchar(5),
  addr varchar(200)
);

 

no 컬럼을 pk로 설정

alter table test1
  add constraint test1_pk primary key (no);

 

자동으로 하나씩 증가하는 컬럼으로 만든다.

alter table test1alter table test1
  modify column no int not null auto_increment;

 

insert 문법

어떤 컬럼을 지정하지 않으면 테이블을 생성할 떄 선언한 컬럼 순서대로 값을 지정해야 한다.

insert into 테이블명 value(값,....);

insert into test1 values(null, 'aaa', '111', '222', '10101', 'seoul');

 

컬럼을 명시하면 컬럼의 순서를 바꿀 수는 있으나 value의 순서들은 앞에서 지정된 순서와 완전히 일치해야한다. 명시한 컬럼의 개수와 value의 개수가 다르면 안된다.

insert into 테이블명(컬럼,컬럼,...) values(값,값,...);
insert into test1(name,fax,tel,no,pstno,addr) 
    values('bbb','222','111',null,'10101','seoul');

 

no 컬럼은 필수 입력 컬럼이지만, 
  자동 증가 컬럼이기 때문에 값을 입력하지 않아도 된다.*/
insert into test1(name,tel) values('ccc','333');

 

한번에 값을 여러개 입력할 수도 있다.

insert into test1(name,tel) values
('aaa', '1111'),
('bbb', '2222'),
('ccc', '3333');

 

select 결과를 테이블의 insert 하기

create

primary key는 not null 굳이 안해줘도 어차피 not null이다.

 

select name, tel from test1 where addr='seoul'; - 집이 서울인 사람의 이름과 전화번호 조회

명령어의 순서

먼저 테이블에서 이 조건에 맞는 사람을 모두 찾고,(record 먼저 검색)

그 사람들의 이름과 tel값을 찾는 것이다.(column 검색)

 

insert into test2(name,tel)
  select name, tel from test1 where addr='seoul'; 

집이 서울인 사람들의 이름과 tel을 test2에 추가한다.

 

 

select 결과의 컬럼명과 insert 테이블의 컬럼명이 같을 필요는 없다. 그러나 결과의 컬럼 갯수와 insert하려는 컬럼 갯수가 같아야아 한다.

결과닁 컬럼 타입과 insert하려면 컬럼의 타입이 같거나 입력을 할 수 있는 타입이어야 한다.

 

insert into test2(name,tel)
  select name, tel, fax from test1 where addr='seoul'; 개수가 맞지 않는다.

 

update 문법

등록된 데이터를 변경할 때 사용하는 명령어

update 테이븖명 set 컬럼명=값, 컬럼명=값, ... where 조건...;

update test1 set pstno='1111', fax='222', where no=3;

yodate test1

 

update test1 set fax='333'; 이렇게 조건을 지정하지 않으면 모든 데이터에 대해 변경한다.

 

commit 을 직접 할 때까지 commit 하지 않게 하려면 DBMS auto commit 을 false로 해야 한다.

set autocommit=false;

 

이렇게 하면 변경사항을 원하는 시점에 직접 커밋할 수 있다.

commit;

 

이렇게 하면 변경사항들을 취소하고 다시 rollback 할 수 있다.

rollback;

rollback은 마지막 커밋 상태로만 돌릴 수 있다.

 

DBMS에 사요자가 delete update, insert 등 변경사항들이 임시테이블이 놓여진다. 그리고 사용자가 commit 을 보낼때 비로소 이 변경사항들이 실제 테이블에 적용된다. select 하더라도 임시테이블에서 꺼내 조회하는 것이다. rollback을 서버에 보낸다면 임시 테이블을 모두 삭제한다.

 

autocommit 이 false인 상태에서 항목을 지운 상태로 조회하면 항목이 지워진 임시테이블을 보여주지만, 실제로는 아직 지워지지 않은 상태이다.이 상태에서 다른 클라이언트가 select를 하면 아직 지워지지 않은 상태가 보여지게 된다.

commit 을 하게 되면 변경사항이 진짜 데이터 베이스에서 적용되어서 다른 클라이언트(autocommit= true인 경우)가 접근하면 변경된 상태가 반영된 것이 보인다. 

임시테이블은 클라이언트 당 하나씩 관리되고 있기 때문에 서로의 임시테이블을 확인할 수는 없다.

 

auto commit이 false가 되는 시점의 진짜 데이터 베이스를 복제하여 임시테이블을 사용한다. 따라서 어떤 클라이언트가 오래전에 autocommit = false로 설정된 상태에서 다른 클라이언트로 변경사항을 커밋하더라도 변경사항이 적용되지 않는다. 이미 임시 테이블이기 때문이다. maria db는 그럴 가능성이 있다. 그러나 데이터 베이스의 양이전체를 복제하여 임시 테이블로 만들기에는 너무 크기가 커서 무겁다.

 

조건을 지정하지 않으면 모든 데이터가 삭제되므로 delete from test1;

엄청난 양의 테이블을 갖고와서 다른 곳에 insert하는 경우, 갑자기 중간쯤 insert되다가 메모리 부족으로 에러가 나는 경우가 있다. autocommit = true 인경우, 한개씩 바로바로 실제 테이블로 옮기지만, autocommit = false인 경우에는 원래 테이블에서 임시 DB로 먼저 한꺼번에 옮기게 되는데, 이 임시 DB가 실제 DB보다 작다. 한꺼번에 임시 DB로 옮기고싶다면 임시 DB의 크기를 늘리는 수 밖에 없다. 차라리 그냥 중간중간 적당한 데이터를 가져오는 중간에 commit 해주면서 임시 DB를 비우는 것이 좋다. 

 

한 요청에 대한 작업이 끝났으면 즉시 commit 을 수행하는 것이 좋다. 간혹가다가 데이터 베이스를 이관하는 작업을 수행할 때 특히 자주 실수할 수 있는 부분이다.

 

*데이터 베이스 관련 용어

이전 버전의 테이블 구조 (학생 성적) Data 

-이름 -전화 -주소 -국어 -영어 -수학 -합계 -평균 -시험일

 

||   기존 데이터를 신규 테이블 구조에 맞춰 두 데이터 베이스로 옮기는 작업을 migration 이라고 한다.

V

신규 버전의 테이블 구조(학생)   /  (성적)

 -번호 -이름 -전화 -주소               -국어 -영어 -수학 -합계 -평균 -시험일 -학생번호

 

회사에서 운영되는 프로그램의 버전이 업그레이드 되었을때 업그레이드된 데이터 구조에 맞춰서 데이터들을 이전 버전에서 신규버전으로 옮기는 과정을 migration이라고 한다. 데이터를 이관, 이주