1. Wireless Network(무선 통신)

  • Wireless Link
    • 신호의 매질(medium)이 대기
    • 채널 내에서 신호들간의 충돌만이 이슈였던 유선연결과 달리 여러 문제가 발생
    • 충돌의 회피/감지가 어려움
      • 멀리서 보낸 시그널은 세기가 매우 약해지며 이더넷이 사용하는 CSMA/CD 방식으로는
        신호를 감지하지 못하고 신호를 보내 충돌이 더 쉽게 발생 

      • 자신이 보내는 신호의 세기가 다른 신호보다 훨씬 크기때문에 충돌의 감지도 어려움
  • IEEE 802.11 Wireless LAN (WI-FI)
    • 무선 연결 네트워크 기술 표준

    • 구조
      • 유선 네트워크에 연결되어있는 AP(Access Point) 존재 
      • 무선 네트워크 인터페이스(무선랜카드)를 가진 단말은 AP와 연결
      • AP와 AP는 스위치로 서로 연결되어있음
      • AP는 주기적으로 자신의 정보를 무선링크상에 broadcast
      • 같은 AP에 연결된 호스트의 집합을 BSS(Basic Service Set)라 한다(하나의 AP와 다수의 유저로 구성)

    • MAC 프로토콜
      • CSMA/CA(Collision Avoid) 사용
      • 송식측은 Carrier Sense 로 채널의 상태를 확인, 표준에 정의된 DIFS 만큼 대기 후 프레임을 전송
      • 수신측은 프레임을 수신한 뒤 표준에 정의된 SIFS 만큼 대기 후 ACK를 전송
      • 이더넷과 달리 충돌을 100퍼센트 감지할 수 없기 때문에 ACK를 통해 전달 여부를 확인해야함
      • 충돌을 감지할 수 없기 때문에 보내기 시작한 프레임은 충돌이 발생하더라도 상관없이 끝까지 전송

    • RTS-CTS
      • 충돌 회피를 위한 컨트롤 프레임 교환방식
      • RTS(Ready To Send) : 송신측이 수신측에게 데이터를 보내려 함을 알리는 컨트롤 프레임
      • CTS(Clear To Send) : 수신측이 송식측에게 현재 데이터를 보내도 된다는 것을 알리는 컨트롤 프레임
      • AP는 CTS를 브로드캐스트하여 데이터를 전송할 동안 다른 호스트들이 데이터를 전송하지 않도록 함
      • 이후 송신측은 수신측에 안전하게 데이터를 전송 가능

    • IEEE 802.11 프레임 헤더
      출처: https://kr.mathworks.com/help/wlan/ug/802-11-mac-frame-generation.html
      • Frame Control
      • Duration
      • Address
        • 4 개의 6바이트(48비트) MAC 주소 필드를 가짐
        • Address 4 는 거의 사용되지 않음(Ad-Hoc 네트워크에 사용)
        • Address 1 : 프레임을 전달받을 목적지 MAC 주소
        • Address 2 : 프레임을 전송하는 출발지 MAC 주소
        • Address 3 : AP가 연결된 라우터의 MAC 주소

        • 왜 AP가 연결된 라우터의 MAC 주소를 WIFI의 프레임이 가지고있어야하는가?
          => AP는 링크 계층의 장비이기 때문에 네트워크 계층의 기능인 포워딩테이블 작성이 불가능

        •  AP는 MAC 주소를 가지는가?
          => AP의 무선 인터페이스쪽은 MAC 주소를 가지지만 유선 인터페이스는 스위치와 마찬가지로
               MAC 주소를 가지지 않음(보이지않음)

        • 가정용 공유기 등의 장비의 경우 링크레이어뿐 아니라 어플리케이션 레이어까지 구현된 장비

 

 

2. Mobile Network(이동 통신)

  • TCP 연결의 유지 여부
    • WI-FI는 다른 AP로의 이동이 발생할 경우 IP주소가 변경, TCP 연결을 유지할 수 없음(이동통신 X)

    • 이동 통신은 네트워크의 범위가 굉장히 넓어 기지국이 커버하는 범위 내에서라면
      어디로 이동하더라도 연결이 끊기지 않음

  • 구조
    • 전국에 다수의 기지국(Base Station)이 설치되어있으며 사용자는 기지국과 무선연결을 수립하고
      기지국간의 계층화된 네트워크를 통해 통신이 이뤄진다.

'CS > 네트워크' 카테고리의 다른 글

#11 네트워크 보안 2 - HTTPS  (0) 2021.12.07
#10 네트워크 보안 1 - 암호화  (0) 2021.12.06
#8 Link Layer(Data Link Layer + Physical Layer)  (0) 2021.11.25
#7 Network Layer  (0) 2021.11.18
#6 Transport Layer  (0) 2021.11.16

1. 데이터 관리방식 변경

  • 기존의 테이블 모델은 DB의 쿼리 결과로 얻은 객체를 리스트형태로 가져와 2차원 리스트 형태로 관리
  • 인덱싱은 편하지만 모델을 사용하는 의미가 희박해지고 수정사항을 DB에 반영하는 것도 비효율적

  • 모델에 getter, setter 추가
    # 화주별 일일정산 데이터 모델
    class DayCalOwnerValues(Base):
        __tablename__ = 'daycal_owner_values'
        date = Column(Date, primary_key=True)
        owner_id = Column(Integer, primary_key=True)
        owner_name = Column(String, nullable=False)
        kd_total = Column(Integer, nullable=False)
        kd_fare = Column(Integer, nullable=False)
        kd_drop = Column(Integer, nullable=False)
        kd_fee4 = Column(Integer, nullable=False)
        after_deduction = Column(Integer, nullable=False)
        match_fee5 = Column(Integer, nullable=False)
        owner_fare = Column(Integer, nullable=False)
        owner_drop = Column(Integer, nullable=False)
        listing_fee4 = Column(Integer, nullable=False)
        kd_pre = Column(Integer, nullable=False)
        deduction_total = Column(Integer, nullable=False)
        total_include_pre = Column(Integer, nullable=False)
    
        def __init__(self, date, owner_id, owner_name):
            self.date = date
            self.owner_id = owner_id
            self.owner_name = owner_name
            self.kd_total = 0
            self.kd_fare = 0
            self.kd_drop = 0
            self.kd_fee4 = 0
            self.after_deduction = 0
            self.match_fee5 = 0
            self.owner_fare = 0
            self.owner_drop = 0
            self.listing_fee4 = 0
            self.kd_pre = 0
            self.deduction_total = 0
            self.total_include_pre = 0
    
        def get(self, idx):
            if idx == 0:
                return self.kd_total
            elif idx == 1:
                return self.kd_fare
            elif idx == 2:
                return self.kd_drop
            elif idx == 3:
                return self.kd_fee4
            elif idx == 4:
                return self.after_deduction
            elif idx == 5:
                return self.match_fee5
            elif idx == 6:
                return self.owner_fare
            elif idx == 7:
                return self.owner_drop
            elif idx == 8:
                return self.listing_fee4
            elif idx == 9:
                return self.kd_pre
            elif idx == 10:
                return self.deduction_total
            elif idx == 11:
                return self.total_include_pre
    
        def set(self, idx, val):
            if idx == 0:
                self.kd_total = val
            elif idx == 1:
                self.kd_fare = val
            elif idx == 2:
                self.kd_drop = val
            elif idx == 3:
                self.kd_fee4 = val
            elif idx == 4:
                self.after_deduction = val
            elif idx == 5:
                self.match_fee5 = val
            elif idx == 6:
                self.owner_fare = val
            elif idx == 7:
                self.owner_drop = val
            elif idx == 8:
                self.listing_fee4 = val
            elif idx == 9:
                self.kd_pre = val
            elif idx == 10:
                self.deduction_total = val
            elif idx == 11:
                self.total_include_pre = val​
     
    • 모든 모델 클래스에 get, set 메소드를 위와같이 추가. 
    • 테이블 모델에서의 참조를 용이하게 하기 위해서 위 처럼 인덱스를 사용한 get, set을 구현

  • db액션 수정
    # 화주 명단 가져오기
    def get_daycal_owner_list():
        return session.query(DayCalOwner).order_by(DayCalOwner.id).all()
    
    
    # 화주별 데이터 가져오기
    def get_daycal_owner_values(today = None):
        if today:
            return session.query(DayCalOwnerValues).filter(DayCalOwnerValues.date == today).order_by(DayCalOwnerValues.owner_id).all()
    
        today = date.today()
        values = []
        owner_list = get_daycal_owner_list()
        for owner in owner_list:
            id = owner.get(0)
            name = owner.get(1)
            value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
            if not value:
                value = DayCalOwnerValues(today, id, name)
                session.add(value)
                session.commit()
            values.append(value)
        return values
    
    
    # 기타 데이터 가져오기
    def get_daycal_other_values(today = date.today()):
        value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
        if not value:
            value = DayCalOtherValues(today)
            session.add(value)
            session.commit()
        return value
    
    
    # 결과 데이터 가져오기
    def get_daycal_result(today = date.today()):
        value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
        if not value:
            value = DayCalResult(today)
            session.add(value)
            session.commit()
        return value​
    • DB에서 쿼리를 통해 가져온 객체를 to_list를 호출하여 컬럼값이 들어있는 리스트의 형태로 반환하는
      기존의 방식에서 객체 자체를 반환하는 방식으로 변경.

  • 테이블 모델 수정
        def data(self, index, role):
            if index.isValid():
                if role == Qt.DisplayRole:
                    value = self.table_data[index.column()].get(index.row())
                    return format(value, ',')
                elif role == Qt.EditRole:
                    value = self.table_data[index.column()].get(index.row())
                    return value
                elif role == Qt.TextAlignmentRole:
                    return int(Qt.AlignRight | Qt.AlignVCenter)
    
        def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
            if role == Qt.DisplayRole:
                if orientation == Qt.Horizontal:
                    return self.owner_list[section].get(1)
                else:
                    return self.vertical_header[section]
            return None
            
        def setData(self, index: QModelIndex, value: int, role):
            if role == Qt.EditRole:
                r, c = index.row(), index.column()
                self.changed(r, c, self.table_data[c].get(r), value)
                self.table_data[c].set(r, value)
                return True
            return False​
     
    • 기존에 2차원 리스트 형태였던 데이터들이 객체의 배열로 변경되면서 참조 방식도 getter를 사용하도록 수정
    • 이렇게 객체를 직접 수정하는 방식을 사용하는 것으로 모델을 사용하는 의미도 퇴색되지 않으며
      저장기능의 구현도 훨씬 편해진다.

  • 저장 액션 구현
    def save():
        session.commit()​
    • 테이블에서 쿼리로 가져온 객체를 직접 수정하도록 변경한 덕분에 단순히 session.commit()을 호출하는 것으로
      모든 변경사항이 DB에 반영된다.

  • 저장 액션 메뉴와 툴바에 추가
            # 저장 액션 추가
            save_data = QAction(QIcon('src/img/save_icon.png'), '저장하기', self)
            save_data.setShortcut('Ctrl+S')
            save_data.setStatusTip('화주 이름 변경')
            save_data.triggered.connect(actions.save)
            file_menu.addAction(save_data)
            tool_bar.addAction(save_data)​
     
    • 액션을 메뉴와 툴바에 추가하고 단축키를 Ctrl+S 로 지정, 아이콘파일을 구하여 설정해준다.

 

이제 데이터를 입력하고 저장기능을 사용하면 애플리케이션을 종료하고 다시 실행해도 작성한 오늘의 데이터가 
표시되는 것을 볼 수 있다.

1. Broadcast Medium(공용 채널)

  • 네트워크 계층에서 봤던 추상화된 형태와는 달리 실제로 라우터와 호스트,
    라우터와 라우터의 사이에는 전용 회선이 존재하는 것이 아닌 경우가 많음

  • 실제로는 사이에 다른 호스트나 라우터와 함께 사용하는 공용 채널이 존재하는 경우가 대부분이며
    이러한 매체를 Broadcast Medium 이라 한다.

 

 

2. MAC(Medium Access Control) 프로토콜

  • 송신한 데이터가 공용 채널에서서로 충돌하여 쓰레기 데이터가 되는 것을 막을 방법이 필요
    => Medium Access (Control) Protocol - MAC

    => WIFI 도 공기를 Broadcast Medium으로 하는 MAC 프로토콜의 일종(802.11)


  • 이상적인 MAC 프로토콜의 지향점(링크 대역폭이 R일 때)
    • 하나의 노드만이 통신을 수행할 경우 대역폭 전체(R)를 사용할 수 있다.
    • N 개의 노드가 통신을 수행할 경우 대역폭 전체를 N으로 나눈 R/N 만큼의 대역폭을 사용할 수 있다.
    • 통신을 관리하는 특별한 노드가 존재하지 않는다(모든 노드가 동등한 입장)
    • 단순하게 동작해야한다.

  • 접근법
    • Partitioning
      • TDMA(Time Division Multiple Access)
        • 각 노드에게 회선을 사용할 수 있는 시간 슬롯을 배정하는 방식
        • 주어진 시간동안은 모든 주파수 대역을 사용할 수 있음
        • 동시 사용자가 적을 경우 시간 슬롯이 낭비될 수 있음

      • FDMA(Frequency Division Multiple Access)
        • 각 노드에게 주파수 대역을 나누어 배정하는 방식
        • 아날로그 방식으로 구현이 간단함
        • 인접채널간 간섭문제를 해결하기 위해 주파수 대역사이에 가드밴드가 필요해진다
          => 가드밴드로 인해 낭비되는 주파수대역 생김
      • 주파수 대역이나 시간 슬롯이 낭비되지 않는 경우(사용자가 많은 경우)에 유리

    • Random Access
      • Partition 방식의 경우 충돌방지는 확실하게 되지만 주파수대역 낭비나 시간슬롯 낭비의 문제가 있음
      • 노드들이 원하는 타이밍에 통신을 수행하는 방식
      • 사용자가 적을 경우 Partitioning에 비해 효율적

      • CSMA(Carrier Sense Multiple Access)
        • 공용 채널이 사용되고 있지 않다고 판단되면 통신을 실시하는 방식
        • 충돌(Collision)
          • 한 노드가 다른 노드가 통신을 먼저 시작한것을 인식하기 전에 통신을 시작한 경우
            발생하는 충돌을 미연에 방지하는 것은 불가능.

          • 충돌이 발생하는 것을 전제로 충돌로 인한 피해를 최소화하는 방식을 사용
            => CSMA/CD(Collision Detection)
            => 한쪽이 충돌발생을 탐지하고 바로 전송을 멈춘다
            => 랜덤 타이머를 설정하고 타임아웃시 재전송
            => 충돌이 발생할 때마다 타이머의 시간을 두 배씩 늘려간다

          • 유선통신의 경우 충돌 감지 자체는 쉽게 가능

    • Taking Turns
      • polling
        • 하나의 중심 호스트(master)가 연결된 다른 호스트(server)들에게 전송할 데이터가 있는지
          확인하여 전송하도록 하는 방식

      • token passing
        • token을 가진 호스트만이 전송을 할 수 있도록 하는 방식

      • 두 방법 모두 한가지 문제가 발생하면(master의 고장, token의 유실 등) 치명적인 문제가
        발생하는 single point failure 문제로 실제로는 잘 사용되지 않음

 

 

3. 이더넷(Ethernet)

  • 유선 케이블 상황에서의 MAC 프로토콜
  • Random Access 방식을 사용(CSMA/CD)
  • 이더넷 프레임
    • Preamble / SFD
      • 비트동기, 프레임동기를 위한 부분
      • Physical Layer의 헤더부분

    • DA(Desination Address) / SA(Source Address)
      • 목적지와 출발지의 MAC 주소

    • Len/Type
      • 값이 0x0600 미만이면 프레임의 데이터 영역(Data + Padding)의 길이로 해석
      • 0x0600 이상이면 Data부분에 담겨있는 상위 프로토콜의 타입으로 해석
        => ARP 쿼리, ARP 리스폰스 등

    • Data
      • IP 패킷이 들어있는 부분
      • 뒤에 따라오는 Padding 부분은 0으로 채워진다.

    • FCS(Frame Check Sequence)
      • 수납된 데이터의 에러 검출을 위한 CRC

  • 신뢰성있는 전송
    • 전송에 대한 피드백이 없는데 전송이 정상적으로 완료됐는지 확인하는방법
      => 외부 간섭이 없는 유선연결이기에 회선(채널)내에서의 충돌만 없다면 전송된것으로 볼 수 있음

    • 충돌을 탐지하기 전에 전송이 종료되면 전송에 실패한 데이터를 재전송하지 못함
      => 전송측이 충돌을 탐지하기 위한 충분한 시간을 기다려줄 필요가 있음
      => 전송할 프레임의 길이가 충분하면 충돌을 감지할 수 있음
      => 이더넷은 이를 위한 프레임의 최소길이를 지정

    • 보내려는 데이터의 크기가 프레임 최소길이보다 작다면?
      => 0으로 채워진 padding 부분을 붙여 최소길이를 만족시킴

  • MAC 주소와 ARP
    • MAC 주소
      • 네트워크 랜카드가 가지고있는 고유 주소
      • 랜카드가 제조된 시점부터 바뀌지 않는 값

    • ARP(Address Resolution Protocol)
      • IP 주소를 물리적 네트워크 주소에 바인딩하기 위한 프로토콜

      • 모든 호스트는 포워딩 테이블을 통해 IP패킷을 보낼 다음 목적지(next hop)를 알 수 있지만
        그 IP에 해당하는 MAC 주소를 알지 못해 링크 레이어 레벨에서의 전송이 불가능하다.
        이를 해결하기 위해서 ARP 프로토콜을 통해 IP주소에 해당하는 MAC 주소를 알아내야한다.

      • 절차
        • 전송측은 프레임의 데이터/타입 영역에 IP패킷 대신 목적지 IP주소에 해당하는
          MAC 주소를 요청하는 ARP 쿼리를 담아 네트워크상에 존재하는 모든 호스트들에게
          브로드캐스트

        • 전송측이 보낸 ARP 쿼리를 받은 호스트중 자신의 IP가 쿼리에서 원하는 IP주소와 일치하는
          호스트는 자신의 프레임의 데이터/타입 영역에 ARP 응답(리스폰스)을 담아 전송하는 것으로
          자신의 MAC 주소를 알림

        • 전송측은 얻은 MAC 주소를 ARP 캐시(테이블)에 저장하고 이를 참조하여 목적지 IP에 해당하는
          MAC 주소로 IP패킷을 담은 이더넷 프레임을 전송

 

 

4. 이더넷 네트워크의 물리적 형태

  • 버스(Bus)
    • 90년대 중반까지 주로 사용되었던 형태

    • 모든 노드가 하나의 충돌 도메인을 가지기에 호스트들간에 충돌이 발생할 수 있음

  • 스타(Star)
    • 현재 주로 사용되고있는 형태
    • 여러 호스트들의 중심에 스위치가 존재하며 스위치와 각 호스트들 사이에는 전용 회선이 존재

    • 스위치는 연결된 호스트들로부터 전송된 프레임을 버퍼에 담아두고 처리해나간다.

    • 스위치의 각 인터페이스가 어느 호스트와 연결되어있는지를 학습해나가며 
      스위치 포워딩 테이블을 생성하여 이를 토대로 프레임들의 전송을 처리한다.

    • 학습의 절차
      • 프레임을 전송받은 순간 전송한 호스트의 위치를 파악, 테이블에 저장
      • 목적지의 위치를 모를 경우 flooding(브로드캐스트) 을 통해 위치를 알아내고 테이블에 저장
      • 테이블을 참조하여 해당하는 목적지로 프레임을 전송
      • 처음에는 테이블을 채워나가기 위해 flooding 이 발생하지만 시간이 조금 지나고나면
        테이블이 완성되어 바로 포워딩을 할 수 있다. 
    • 스위치를 사용하는 것으로 보다 쉽게 이더넷 네트워크의 규모를 확장할 수 있다.

    • 스위치가 행하는 포워딩을 스위칭이라고도 한다.
    • 스위치는 MAC 주소를 가지지 않는다(사용자입장에선 보이지 않는다)

 

'CS > 네트워크' 카테고리의 다른 글

#10 네트워크 보안 1 - 암호화  (0) 2021.12.06
#9 Wireless Network & Mobile Network  (0) 2021.11.30
#7 Network Layer  (0) 2021.11.18
#6 Transport Layer  (0) 2021.11.16
#5 Application Layer 2 - HTTP  (0) 2021.11.01

처음에는 QTableWidget을 사용하여 표를 구성하였지만 조금 알아보니 QTableView를 사용하는 것이 성능면에서
더 낫다는 듯 해서 교체작업을 진행했다.

 

1. 테이블 뷰 생성

from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView

from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel


# 일일 정산서 계산서 위젯
class DayCal(QWidget):
    # 생성자
    def __init__(self):
        super().__init__()
        self.input_table = QTableView()
        self.other_table = QTableView()
        self.result_table = QTableView()
        self.init_ui()

	''' 생략 '''

 화주별 데이터를 입력받고 일부 결과를 표시하기 위한 input_table, 화주와 관계없는 별도의 데이터를 입력하기
위한 other_table, 입력데이터를 기반으로 결과를 나타낼 result_table 로 총 세개의 QTableView를 생성하였다.

 

 

2. input_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *


class DayCalTableModel(QAbstractTableModel):
    def __init__(self, parent, horizontal_header, data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.table_data = data
        self.horizontal_header = horizontal_header
        self.vertical_header = ['강동총금액', '강동운임', '강동하차비', '강동수수료 4%', '공제후 금액', '중매수수료 5%', '화주운임', '화주하차비', '상장수수료 4%', '강동선지급금', '공제합계', '선지급금포함 공제합계']
        self.row_count = len(self.vertical_header)
        self.column_count = len(self.horizontal_header)

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return self.horizontal_header[section]
            else:
                return self.vertical_header[section]
        return None

    def sort(self, col, order):
        self.emit(SIGNAL("layoutAboutToBeChanged()"))
        self.table_data = sorted(self.table_data, key=operator.itemgetter(col))
        if order == Qt.DescendingOrder:
            self.table_data.reverse()
        self.emit(SIGNAL("layoutChanged()"))

    def flags(self, index):
        if index.row() in [3, 4, 10, 11]:
            return Qt.ItemIsSelectable | Qt.ItemIsEnabled
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.table_data[index.column()][index.row()] = value
            return True
        return False

    def owner_added(self, name):
        self.beginInsertColumns(QModelIndex(), self.column_count, self.column_count)
        self.horizontal_header.append(name)
        self.table_data.append([0]*12)
        self.column_count += 1
        self.endInsertColumns()

    def owner_removed(self, name):
        idx = self.horizontal_header.index(name)
        self.beginRemoveColumns(QModelIndex(), idx, idx)
        self.table_data.pop(idx)
        self.horizontal_header.pop(idx)
        self.column_count -= 1
        self.endRemoveColumns()

    def owner_modified(self, org, chg):
        idx = self.horizontal_header.index(org)
        self.horizontal_header[idx] = chg
        
    ''' 생략 '''

여기서는 QAbstractTableModel 을 상속한 테이블 모델을 작성하였다. (이 외에도 다른 여러가지 방법들이 있는 듯 했다.)

 

  • __init__(self, parent, horizontal_header, data, *args)
    • parent 위젯과 헤더, 그리고 표시할 데이터를 입력받는 초기화 메소드
    • 해당 모델의 parent를 입력받은 parent 위젯으로 설정
    • 해당 모델의 table_data 를 입력받은 데이터로 설정한다.  
    • horizontal_header를 입력받은 헤더 데이터로 설정한다. 이는 화주 명단과 같다.
    • vertical_header를 설정한다. 이 항목들은 변경되지 않기 때문에 하드코딩으로 설정해주었다.
    • row_count, column_count(행과 열의 길이) 값을 각각 vertical, horizontal 헤더의 길이로 설정한다.

 

  • rowCount(self, parent) / columnCount(self, parent)
    • QAbstractModel에 정의된 rowCount, columnCount 메소드를 오버라이딩
    • 테이블의 열 수와 행 수를 반환한다.

 

  • data(self, index, role)
    • QAbstractModel에 정의된 data 메소드를 오버라이딩

    • role은 이 메소드를 어떤 상태(역할)에서 호출하는지를 나타내며 index는 참조하려는 데이터의
      위치(행과 열)를 나타낸다. role 과 index에 따라 적절히 데이터를 반환하도록 구현한다.

    • 여기서는 Qt.DisplayRole(표시), Qt.EditRole(수정) 상태에서 호출할 경우 index에 해당하는 데이터를
      반환하도록 구현하였다. 만약 role == Qt.EditRole을 조건에서 뺄 경우 수정상태에 들어간 셀에서는
      데이터가 표시되지 않는다.

 

  • headerData(self, section, orientation, role)
    • QAbstractModel에 정의된 headerData 메소드를 오버라이딩

    • orientation은 방향(행방향 / 열방향)을 나타내며 section은 인덱스를 나타낸다.

    • horizontal, vertical 의 헤더를 반환하기 위해 사용한다.
    • orientation 이 horizontal인지 vertical인지에 따라 각각 해당하는 헤더값을 반환하도록 구현하였다.

 

  • flags(self, index)
    • QAbstractModel에 정의된 flags 메소드를 오버라이딩

    • index 에 해당하는 셀에 대해 flag를 설정하기 위해 사용한다

    • 플래그를 통해 특정 셀의 편집가능 여부를 수정하는 등의 작업이 가능하다.

    • 사용자가 값을 입력하는 셀이 아닌 부분을 편집 불가능하게 설정하였다.

 

  • setData(self, index, value, role)
    • QAbstractModel에 정의된 setData 메소드를 오버라이딩

    • 셀의 값 변경을 반영하기 위해 구현해야한다.

    • edit role 일 경우에 수정사항이 반영되도록 구현하였다.

    • 반영되었을 경우 True, 아닐 경우 False를 반환한다.

 

  • owner_added(self, name) / owner_removed(self, name) / owner_modified(self, org, chg)
    • 화주의 추가, 삭제, 이름변경을 반영하기 위해 직접 구현한 메소드이다.

    • 화주가 추가되고 그에따라 화주별 데이터도 추가되어 데이터베이스에 반영되지만
      데이터베이스에서 데이터를 읽어오는 것은 프로그램 실행시에만 수행하는 작업이며
      반영된 데이터를 굳이 데이터베이스에서 다시 읽어오는것도 비효율적이기에
      모델의 메소드를 호출하여 즉각 반영하도록 하였다. 

    • added 와 removed 의 경우 테이블 자체가 변경되는 작업이기에 (열의 추가/삭제)
      beginInsertColumns, beginRemoveColumns 등 테이블의 변경작업이 시작됨을 알리는
      메소드를 호출한 뒤 작업을 수행하고 마지막에 endInsertColumns, endRemoveColumns 등
      테이블의 변경작업이 끝났음을 알리는 메소드를 호출해줘야 변경사항이 정상적으로 반영된다.

    • modified 의 경우 단순히 값을 수정하는 것 뿐이기에 header 리스트를 수정해주는 것만으로
      간단하게 반영된다.

 

 

3. other_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *

''' 생략 '''

class DayCalOthersTableModel(QAbstractTableModel):
    def __init__(self, parent, data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.table_data = [data]
        self.vertical_header = ['경매 사무실 입금', '가라경매 강동 입금', '직접 지출', '우리 경매', '강동 사입']
        self.row_count = len(self.vertical_header)
        self.column_count = 1

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Vertical:
                return self.vertical_header[section]
        return None

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.table_data[index.column()][index.row()] = value
            return True
        return False

''' 생략 '''
  • other_table 의 테이블 모델도 기본적으로 input_table의 그것과 거의 동일하게 구현

  • horizontal header가 필요하지 않기 때문에 vertical header만을 설정

  • 모두 사용자가 입력하는 데이터로만 이루어졌기에 특정 셀의 수정 불가등의 설정을 하지 않음

 

 

4. result_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *

''' 생략 '''

class DayCalResultTableModel(QAbstractTableModel):
    def __init__(self, parent, result_data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.horizontal_header = ['계']
        self.vertical_header = ['강동총금액 합계', '강동운임 합계', '강동하차비 합계', '강동수수료 4% 합계', '공제후금액 합계',
                                '중매수수료 5% 합계', '화주운임 합계', '화주하차비 합계', '상장수수료 4% 합계', '경매확인',
                                '경매 차액', '중개수수료 5%', '경매 차익']
        self.row_count = len(self.vertical_header)
        self.column_count = 1

        self.table_data = [result_data]

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Vertical:
                return self.vertical_header[section]
            else:
                return self.horizontal_header[section]
        return None

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled
  • 마찬가지로 input_table과 거의 동일하게 구현

  • 해당 테이블은 모든 셀을 수정 불가능하도록 설정

 

 

5. 테이블 뷰에 모델 세팅

from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView

from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel


# 일일 정산서 계산서 위젯
class DayCal(QWidget):
    # 생성자
    def __init__(self):
        super().__init__()
        self.input_table = QTableView()
        self.other_table = QTableView()
        self.result_table = QTableView()
        self.init_ui()

    # ui 초기화
    def init_ui(self):
        # 화주별 데이터 입력테이블
        self.data_model = DayCalTableModel(self, actions.get_daycal_owner_list(), actions.get_daycal_owner_values())
        self.input_table.setModel(self.data_model)

        # 기타 데이터 입력 테이블
        self.other_data_model = DayCalOthersTableModel(self, actions.get_daycal_other_values())
        self.other_table.setModel(self.other_data_model)

        # 결과 테이블
        self.result_data_model = DayCalResultTableModel(self, actions.get_daycal_result())
        self.result_table.setModel(self.result_data_model)

        # 그리드 레이아웃
        grid = QGridLayout()

        # 테이블위젯 추가
        grid.addWidget(self.input_table, 0, 0)
        grid.addWidget(self.other_table, 1, 0)
        grid.addWidget(self.result_table, 0, 1, 2, 1)
        grid.setRowStretch(0, 5)
        grid.setColumnStretch(0, 5)

        # 레이아웃 세팅
        self.setLayout(grid)
        
    # 화주 추가 반영
    def owner_added(self, added_user):
        self.data_model.owner_added(added_user)

    # 화주 삭제 반영
    def owner_removed(self, removed_name):
        self.data_model.owner_removed(removed_name)

    # 화주 이름 변경 반영
    def owner_modified(self, org_name, chg_name):
        self.data_model.owner_modified(org_name, chg_name)
        
''' 생략 '''
  • 작성한 모델의 인스턴스를 생성(이 때, DB에서 데이터를 읽어와서 인자로 넘겨준다.)

  • 테이블 뷰에 각각 해당하는 모델의 인스턴스를 세팅

  • 레이아웃에 추가한 뒤 위젯에 레이아웃을 세팅한다.

  • DayCal 위젯은 owner_added, owner_removed, owner_modified 메소드를 각각 구현하여
    화주 상태변경을 인식하고 각 테이블의 데이터모델의 메소드를 호출하여 이를 반영한다.

 

 

6. DB 작업을 위한 함수 작성

# 화주 명단 가져오기
def get_daycal_owner_list():
    return [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]


# 화주별 데이터 가져오기
def get_daycal_owner_values():
    values = []
    today = date.today()
    for owner_id in session.query(DayCalOwner.id).order_by(DayCalOwner.id):
        id = owner_id[0]
        value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
        if not value:
            value = DayCalOwnerValues(today, id)
            session.add(value)
            session.commit()
        values.append(value.to_list())
    return values


# 기타 데이터 가져오기
def get_daycal_other_values():
    today = date.today()
    value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
    if not value:
        value = DayCalOtherValues(today)
        session.add(value)
        session.commit()
    return value.to_list()


# 결과 데이터 가져오기
def get_daycal_result():
    today = date.today()
    value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
    if not value:
        value = DayCalResult(today)
        session.add(value)
        session.commit()
    return value.to_list()
  • 데이터 모델의 인스턴스 생성에 필요한 데이터를 DB에서 가져오기 위한 함수 구현

  • get_daycal_owner_list
    • 화주 이름 명단을 반환

    • 화주 id를 기준으로 오름차순 정렬

 

  • get_daycal_owner_values
    • 화주별 오늘의 데이터를 리스트 형태로 반환

    • 화주 id를 기준으로 오름차순 정렬

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

  • get_daycal_other_values
    • 오늘의 기타 데이터를 리스트 형태로 반환

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

  • get_daycal_result
    • 오늘의 결과 데이터를 리스트 형태로 반환

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

 

7. to_list 메소드 추가

# 화주별 일일정산 데이터 모델
class DayCalOwnerValues(Base):
    __tablename__ = 'daycal_owner_values'
    date = Column(Date, primary_key=True)
    owner_id = Column(Integer, primary_key=True)
    kd_total = Column(Integer, nullable=False)
    kd_fare = Column(Integer, nullable=False)
    kd_drop = Column(Integer, nullable=False)
    kd_fee4 = Column(Integer, nullable=False)
    after_deduction = Column(Integer, nullable=False)
    match_fee5 = Column(Integer, nullable=False)
    owner_fare = Column(Integer, nullable=False)
    owner_drop = Column(Integer, nullable=False)
    listing_fee4 = Column(Integer, nullable=False)
    kd_pre = Column(Integer, nullable=False)
    deduction_total = Column(Integer, nullable=False)
    total_include_pre = Column(Integer, nullable=False)

    def __init__(self, date, owner_id):
        self.date = date
        self.owner_id = owner_id
        self.kd_total = 0
        self.kd_fare = 0
        self.kd_drop = 0
        self.kd_fee4 = 0
        self.after_deduction = 0
        self.match_fee5 = 0
        self.owner_fare = 0
        self.owner_drop = 0
        self.listing_fee4 = 0
        self.kd_pre = 0
        self.deduction_total = 0
        self.total_include_pre = 0

    def to_list(self):
        return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
                self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.kd_pre,
                self.deduction_total, self.total_include_pre]


# 일일정산서에 기타 데이터 모델
class DayCalOtherValues(Base):
    __tablename__ = 'daycal_other_values'
    date = Column(Date, primary_key=True)
    office_deposit = Column(Integer)
    kd_deposit = Column(Integer)
    direct_exp = Column(Integer)
    our_auc = Column(Integer)
    kd_buy = Column(Integer)

    def __init__(self, date):
        self.date = date
        self.office_deposit = 0
        self.kd_deposit = 0
        self.direct_exp = 0
        self.our_auc = 0
        self.kd_buy = 0

    def to_list(self):
        return [self.office_deposit, self.kd_deposit, self.direct_exp, self.our_auc, self.kd_buy]


# 일일정산서 결과 모델
class DayCalResult(Base):
    __tablename__ = 'daycal_result'
    date = Column(Date, primary_key=True)
    kd_total = Column(Integer, nullable=False)
    kd_fare = Column(Integer, nullable=False)
    kd_drop = Column(Integer, nullable=False)
    kd_fee4 = Column(Integer, nullable=False)
    after_deduction = Column(Integer, nullable=False)
    match_fee5 = Column(Integer, nullable=False)
    owner_fare = Column(Integer, nullable=False)
    owner_drop = Column(Integer, nullable=False)
    listing_fee4 = Column(Integer, nullable=False)
    auc_check = Column(Integer, nullable=False)
    auc_diff = Column(Integer, nullable=False)
    match_fee5_final = Column(Integer, nullable=False)
    auc_profit = Column(Integer, nullable=False)

    def __init__(self, date):
        self.date = date
        self.kd_total = 0
        self.kd_fare = 0
        self.kd_drop = 0
        self.kd_fee4 = 0
        self.after_deduction = 0
        self.match_fee5 = 0
        self.owner_fare = 0
        self.owner_drop = 0
        self.listing_fee4 = 0
        self.auc_check = 0
        self.auc_diff = 0
        self.match_fee5_final = 0
        self.auc_profit = 0

    def to_list(self):
        return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
                self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.auc_check,
                self.auc_diff, self.match_fee5_final, self.auc_profit]
  • DB에서 모델단위로 읽어온 데이터를 보다 쉽게 리스트 형태로 전달하기 위해 to_list 메소드를 추가

  • 단순히 테이블에 표현하기 위해 필요한 column 값들을 리스트형태로 반환하도록 구현

 

 

이제 날짜가 바뀌면 자동으로 그날의 디폴트 데이터를 DB에 삽입하고 테이블에 표시해준다. 

또한 QTableWidget을 사용했을 때 보다 좀더 유연하게 테이블을 사용할 수 있게 되었다.

1. IP(Internet Protocol)

  • 네트워크 계층의 핵심 프로토콜
  • 전송계층에서 TCP 혹은 UDP 헤더를 붙여 만들어진 패킷에 IP 헤더를 붙여 IP 패킷을 생성
  • 패킷은 헤더에 명시된 IP 주소를 기반으로 목적지로 전송된다.
  • IP 자체는 비연결형 프로토콜이며 신뢰성있는 전송을 보장하지 않는다.(이는 전송계층에서 담당)

 

 

2. IP 헤더(IPv6)

출처:https://en.wikipedia.org/wiki/IPv4

  • Version
    • 현재 사용하는 IP의 버전

 

  • IHL(IP Header Length)
    • IP 헤더의 길이(데이터 포함 X)

 

  • TOS(Type of Service)
    • 서비스의 우선순위를 표시하기 위한 영역

 

  • Total Length
    • 데이터를 포함한 IP패킷 전체의 길이

 

  • Identification / Flags / Fragment Offset
    • 라우터의 전송가능한 최대 패킷 크기(MTU)가 IP 패킷보다 작아 패킷이 쪼개질 경우
      쪼개진 패킷을 다시 재조립하기 위해 필요한 정보를 저장하기 위한 영역

    • Identification(16bit) : 어느 패킷의 프래그먼트인지 나타내기 위한 영역

    • Flags(3bit) : 패킷 분할 정보를 나타내기 위한 영역. 예를들어 마지막 비트는 뒤에 다른 프래그먼트가
                       더 있는지 표시되기 위해 사용된다.

    • Offset(15bit) : 각 프래그먼트의 원본 데이터에서의 인덱스를 나타내는 영역

 

  • TTL(Time To Live)
    • 초기 설정한 값에서 포워딩을 거칠 때마다 1씩 감소
    • 패킷에 수명을 주어 무한루프에 빠진 패킷 등이 일정 횟수의 포워딩을 거치면 사라지도록 함

 

  • Protocol(Upper Layer)
    • 상위 계층에서 사용될 프로토콜을 명시(TCP/UDP 등)

 

  • Header Checksum
    • ip 헤더의 체크섬을 저장 - 최소한의 에러검출

 

  • Source/Destination IP Address
    • 출발지, 목적지 IP주소

 

  • Options
    • 추가적인 처리 옵션을 정의하기 위한 영역
    • 없을 수도 있으며 최대 40바이트까지 사용 가능

 

 

3. IP 주소

  • 8비트로 표현되는 정수 4개(32비트)로 표현되는 주소체계
    => IPv6의 경우 총 128비트로 주소를 표현한다. 즉, 2 ^ 128 개의 독립적인 주소를 표현할 수 있다.

 

  • IP 주소는 엄밀히 말해 특정 호스트를 가리키는 주소가 아닌 네트워크 인터페이스를 가리키는 주소
    => 라우터와 같이 다수의 인터페이스를 가지는(다수의 IP주소를 가지는) 장비도 존재한다.

 

  • IP의 분배
    • 단순히 IP 하나하나를 호스트에게 배분하는 방식을 사용할 경우 포워딩 테이블의 크기가 너무 커지며
      포워딩에 걸리는 시간도 너무 길어진다.
    • IP 계층화
      • Network ID 부분과 Host ID 부분을 분리
      • ex) 12.34.158.0/24
        • 상위 24비트(12.34.158) 까지는 네트워크 ID
        • 나머지 8비트(0) 까지는 호스트 ID
        • 같은 네트워크에 속하는 호스트들을 동일한 IP prefix로 묶을 수 있게 된다
          => 포워딩 테이블을 보다 간단하게 표현하여 효율적인 포워딩을 가능하게 한다.
        • 새 호스트에 IP주소를 할당하는 작업도 간단해짐
      • 과거에는 네트워크 ID를 부여받는 기관의 규모에 따라 네트워크 ID 비트수를 조정
        • A클래스 - 상위 8비트를 네트워크 ID로 사용
        • B클래스 - 상위 16비트를 네트워크 ID로 사용
        • C클래스 - 상위 24비트를 네트워크 ID로 사용
      • 현재는 서브네팅을 사용하여 유동적으로 prefix를 조절

    • 서브넷 마스크(Subnet Mask)
      • 네트워크 ID가 어디까지인지 컴퓨터가 이해할 수 있도록 표현하는 방식

      • ex) 125.23.87.0 이라는 네트워크 ID를 부여받았을 때, 각각 30개의 호스트를 가지는
        8개의 서브넷으로 분리하려면 네트워크 ID인 24비트까지의 서브넷마스크는 모두 1,
        그 이하로는 8개의 서브넷을 사용하기 위해 3개의 비트를 1로, 나머지를 0으로 하면
        0인 부분이 5비트가 되어 32개의 주소를 나타낼 수 있게된다. 

      • 만약 125.23.87.0 에 서브넷마스크가 255.255.255.192라면
        • 서브넷의 갯수는 최대 4개이다.
        • 각 서브넷의 호스트 범위는 0~63, 64~127, 128~191, 192~255 가 된다.
        • 이 중 세 번째 서브넷인 128~191 에서 대표주소는 125.23.87.128이며
          브로드캐스트 주소는 125.23.87.191이 된다.

  • NAT(Network Address Translation)
    • 20년 전에도 이미 IPv4가 지원하는 2 ^ 32 개의 주소만으론 부족할 것으로 예상하여
      IPv6를 만들었지만 정작 현재까지도 사용중인것은 여전히 IPv4 이다

    • IPv6로의 이전을 위해서는 기존에 사용하던 모든 네트워크 장비를 교체해야하는데,
      이는 아직까지는 현실적으로 어려움이 따름 

    • 결과적으로 IPv4를 그대로 사용하면서도 표현 가능한 주소의 갯수를 늘리기 위해
      NAT(Network Address Translation)를 사용

    • NAT의 방식
      • 로컬 네트워크에서 그 네트워크 내부에서만 유일성을 가지는 IP 주소를 분배

      • 네트워크 내부 호스트가 외부 호스트에게 데이터를 전송할 경우 내부 호스트의 IP주소를
        전 세계적으로도 유일한 게이트웨이 주소로 변경해주고 외부에서 내부의 호스트에게
        데이터를 전송할 경우 내부 IP주소로 변환하여 내부 호스트에게 전달해준다.

      • 기본적으로는 출발지 IP주소(게이트웨이 주소)와 도착지 IP주소를 통해 내부 IP를 특정하지만
        이 방식으로는 다수의 내부 호스트가 같은 목적지와 통신할 경우 구분이 불가능해진다.
        이를 해결하기 위해 추가적으로 포트를 구성하여 패킷을 구분하는 PAT 혹은 NAPT 방식을 사용한다.
      • NAT 테이블에 기록되는 것은 내부에서 패킷을 전송할 때이기 때문에 클라이언트로써는
        문제가 없지만 상대의 요청을 기다려야하는 서버로서는 정상적으로 동작하기 어렵다.
        NAT 테이블에 직접 변환정보를 기재하여 서버로서 동작할 수 있게 하는 방법도 있지만
        일반 사용자가 할 수 있는 작업은 아니다.

      • NAT의 또다른 문제는 계층화된 네트워크 구조를 어기고있다는 점이다. 네트워크 계층에서 동작하는
        장비인 NAT 라우터가 IP 패킷의 데이터부분(TCP헤더의 포트)을 인지하고 수정까지 하는 규칙위반을
        하고있다. (대부분의 사용자는 서버 역할을 할 경우가 없기 때문에 문제를 크게 인지하지 못한다.)

  • DHCP(Dynamic Host Configuration Protocol)
    • DHCP 서버가 네트워크에 연결된 호스트에게 자동으로 IP주소를 대여해주는 방식
    • 절차
      • DHCP 클라이언트는 네트워크에 연결하기 위해 네트워크 내에서 브로드캐스트(255.255.255.255)로
        DHCP 서버에게 IP 주소를 요청하는 패킷을 전송(discover)

      • DHCP 서버는 요청에 대한 응답으로 대여해줄 IP 주소를 담은 패킷을 전송(offer)

      • DHCP 클라이언트는 요청에 응답해준 DHCP 서버중 하나를 선택하여 해당 서버를 선택했음을 알리는
        패킷을 브로드캐스트로 전송한다.(request) 이 때, 브로드캐스트를 사용하는 이유는 선택받지 못한
        DHCP 서버가 자신의 요청을 더이상 신경 쓸 필요가 없도록 해주기 위해서이다.

      • 선택받은 DHCP 서버는 request 패킷에 대해 ack 패킷을 보내고 프로토콜이 완료된다.
        이후부터 클라이언트측은 대여받은 IP를 통해 통신이 가능해진다.
    • 이 과정을 통해 네트워크에 연결된 호스트는 자신과 연결된 DNS 서버와
      게이트웨이 라우터의 IP주소를 알게되고 이를 기반으로 포워딩 테이블을 생성하게 된다.

 

 

3. 라우터(Router)

  • 네트워크 계층의 핵심 장비
  • IP 패킷들은 출발지와 목적지 사이를 연결하는 다수의 라우터를 거쳐 전송된다.
  • 포워딩(Forwarding)
    • 라우터의 가장 기본적인 기능
    • 라우터가 가진 포워딩 테이블에 따라 목적지 주소에 매칭되는 방향으로 패킷을 전송
    • 목적지 주소 하나하나에 매칭되는 방향을 따로 관리하는 것을 비효율적
      => 엔트리를 주소의 범위 형태로 구성하여 목적지 주소와 가장 많이 일치하는 엔트리와 매칭

 

  • 라우팅(Routing)
    • 포워딩을 위한 포워딩 테이블을 생성하는 작업
    • 목적지 주소까지의 모든 경로 중 최적의 경로를 탐색
    • 라우팅 결과 만들어진 포워딩 테이블은 라우터의 입력 포트에 각각 저장되며 라우터에
      도착한 패킷들의 포워딩은 이를 이용하여 각 포트에서 병렬적으로 수행된다.

 

 

4. 라우팅 알고리즘

  • 연결 상태(Link State) 알고리즘
    • 라우터간의 연결 상태정보를 모든 라우터에 전달하여 최단경로를 탐색하는 알고리즘
    • 데이크스트라(Dijkstra)의 최단경로 알고리즘 사용
    • 라우팅 정보 변경될 경우에만 변경정보를 브로드캐스팅으로 전달
    • 최단경로를 구하기 위해 전체 링크의 상태를 알고있어야하기 때문에 추가적인 메모리가 필요

    • OSPF, IS-IS 등의 프로토콜이 이 알고리즘을 사용

    • 대규모 네트워크에 적합

  • 거리 벡터(Distance Vector) 알고리즘
    • 최단경로 탐색을 재귀적으로 수행
      => 브로드캐스팅이 아닌 이웃한 라우터와의 통신만으로 네트워크의 상태를 파악

    • 벨만 포드(Bellman-Ford Algorithm)의 최단경로 알고리즘 사용
    • 라우팅 정보 변경의 전달이 주기적으로 이루어짐
      • 즉각적으로 발생하지 않기 때문에 잘못된 정보로 경로를 계산하게 될 수 있음
      • 변화가 없어도 주기적으로 통신이 발생하기에 트래픽을 낭비

    • count to infinity 문제
      • 인접한 노드의 목적지까지의 거리를 토대로 목적지까지의 최단경로를 계산
      • 그런데 인접한 노드의 최단경로에 자기자신이 포함될경우 문제가 발생(역류)
      • 인접한 노드 x에 의존하는 최단경로값을 노드 x에 넘겨줄 때는 무한값을
        넘겨주는 것으로 역류를 미연에 방지할 수 있음

    • RIP, IGRP 등의 프로토콜이 이 알고리즘을 사용

    • 소규모 네트워크에 적합

 

 

5. AS(Autonomous System) - 자율시스템

  • 통일된 라우팅 프로토콜을 사용하고 동일한 관리자에 의해 관리되는 독립된 네트워크 영역
  • 해당 네트워크에 속한 라우터와 서브 네트워크들의 집합
  • 넓게 보면 ISP(Internet Service Provider)를 말하기도 한다.

  • 필요성
    • 전 세계의 라우터를 대상으로 라우팅 알고리즘을 실행하기에는 그 수가 너무 많음
    • 계층화를 통해 라우팅의 규모를 분할 - 보다 효율적인 라우팅
    • 라우팅 알고리즘을 AS별로 독립적으로 적용
    • AS 단위의 보안 유지
    • 고장 및 오류를 AS단위로 관리 가능
  • AS 라우팅 프로토콜
    • IGP(Interior Gateway Protocol)
      • AS 내부의 라우터간의 라우팅
      • RIP, IGRP, EIGRP, OSPF, IS-IS 등 일반적으로 말하는 라우팅 알고리즘은 IGP를 의미

    • EGP(Exterior Gateway Protocol)
      • 서로 다른 AS 간의 라우팅 
      • BGP(Border Gateway Protocol)
        • 서로 다른 AS간에 라우팅 경로를 설정하기 위해 AS 외곽의 라우터끼리 IP주소 정보를 교환
        • AS 내부의 독립정 정책을 사용하는 IGP와 달리 조직간에 합의된 정책을 따른다.

'CS > 네트워크' 카테고리의 다른 글

#9 Wireless Network & Mobile Network  (0) 2021.11.30
#8 Link Layer(Data Link Layer + Physical Layer)  (0) 2021.11.25
#6 Transport Layer  (0) 2021.11.16
#5 Application Layer 2 - HTTP  (0) 2021.11.01
#4 Socket Programming  (0) 2021.10.29

1. TCP(Transmission Control Protocol)

  • 전송계층(Transport Layer)의 연결 지향형 프로토콜
  • 두 호스트간에 통신을 위한 논리적 연결(가상회선)을 수립하여 통신
  • 3-way handshaking 으로 연결을 수립, 4-way handshaking으로 연결을 해제
  • 신뢰성있는 데이터 전송(Reliable Data Transfer)을 지원
  • 데이터의 순서유지 보장 
  • 흐름제어(수신측의 처리속도에 맞춰 전송속도 조절) 지원
  • 혼잡제어(망의 혼잡도에 따라 전송 패킷량 조절) 지원
  • 양방향(Full Duplex), 1:1(Point to Poin) 통신

 

 

2. UDP(User Datagram Protocol)와의 비교

  TCP UDP
신뢰성 보장 O X
순서 유지 O X
에러 검출 O O
흐름 제어 O X
혼잡 제어 O X
연결 과정 O X
통신 형태 1:1(Point to Point), 양방향(Full Duplex) 1:1, 1:N(Broadcast), N:M, 단방향(Simplex)
통신 단위 바이트 스트림(데이터 유실이 없기 때문) 데이터그램

 

 

3. 헤더

 ① UDP

  • Port(32bit)  : source(송신측) 포트와 dest(수신측) 포트 두 종류가 있으며 각각 16비트씩 차지

  • Segment Length(16bit) : 헤더를 포함한 UDP 세그먼트의 길이 정보

  • Checksum(16bit) : 전송 도중에 에러가 발생했는지 판단하기 위한 값.  UDP는 에러가 발생한 segment를 drop

 

  • Port(32bit) : source(송신측) 포트와 dest(수신측) 포트 두 종류가 있으며 각각 16비트씩 차지
  • Sequence Number(32bit) : 전송되는 데이터의 순서. 데이터의 순서 유지를 보장하기 위한 값
  • Acknowledgement Number(32bit) : 수신자가 받을 것으로 기대하는 다음 데이터의 시퀀스번호
  • Data Offset(= Header Length)(4bit) : 헤더가 아닌 데이터(본문)가 시작되는 위치. 단위는 word(=32bit)
    => 맨 마지막 필드인 Option의 경우 길이가 정해져있지 않기 때문에 이 필드가 필요하다.

  • Reserved(3bit) : 나중을 위해 예약된 필드. 현재는 그냥 0으로만 채워지는 상태
  • Flags(9bit) : 세그먼트의 속성을 표현하는 플래그
    • URG : Urgent Pointer 필드에 값이 채워져있음을 표시
    • ACK : Acknowledgement 필드에 값이 채워져있음을 표시. 이 플래그가 0이라면 해당 필드를 무시
    • PSH(Push) : 수신측에게 이 데이터를 최대한 빠르게 애플리케이션에 전달할 것을 요청.  
                       이 플래그가 0이라면 수신측은 버퍼가 채워질 때 까지 대기
    • RST(Reset) : 이미 연결이 확립된 상대에게 연결을 강제로 리셋해달라고 요청
    • SYN(Synchronized) : 상대방과 연결을 확립할 때 시퀀스 번호의 동기화를 맞추기 위한 세그먼트임을 표시
    • FIN(Finish) : 연결 종료 요청을 위한 세그먼트임을 표시
    • NS, CWR, ECE : 명시적 혼잡 통보(ECN)를 위한 플래그.  혼잡 제어 기능의 향상을 위한 플래그
  • Window Size(16bit) : 한번에 전송 가능한 데이터의 양을 의미. 즉, 윈도우의 최대 크기는 (2^16 - 1) 바이트
  • Checksum(16bit) : 데이터 송신중 에러가 발생했는지 검출하기 위한 값. 에러 발생시 재전송을 요청하기 위함
  • Urgent Pointer(16bit) : 긴급 포인터. URG 플래그가 1이라면 해당 포인터가 가리키는 데이터를 우선처리
  • Option(~40 bytes) : TCP의 기능 확장을 위해 사용되는 필드. TCP 헤더의 최대 길이가 60 bytes 이고 고정 필드의
                               길이가 도합 20 bytes 이기 때문에 Option 필드의 최대 길이는 40bytes가 된다.

 

 

4. 신뢰성 있는 데이터 전송(Reliable Data Transfer) / 데이터의 순서유지 보장

  • 송신측은 데이터 패킷을 전송한 뒤 타이머를 설정
  • 수신측이 데이터를 받으면 응답으로 다음에 받아야할 패킷의 시퀀스 넘버를 명시한 ACK을 전송
  • 이때, ACK를 보낼 때 응답 데이터를 보낼 가능성을 생각하여 수신 후 ACK을 보낼 때 까지 딜레이를 둔다.
  • 송신측이 ACK를 받으면 데이터가 정상적으로 전달된 것을 확인, ACK에 명시된 다음 패킷을 전송
  • ACK를 받지 못하고 타임아웃이 발생하면 데이터가 전달되지 않은것으로 간주, 데이터 패킷을 재전송

 

 

5. 흐름 제어(Flow Control)

 ① Stop and Wait

  • 하나의 패킷을 보낸 뒤 해당 패킷에 대한 ACK가 돌아오기까지 대기
  • 패킷 전송마다 ACK를 기다리는 방식으로는 패킷 전송 이후 응답이 돌아오기 까지의
    RTT(Round Trip Time)만큼 시간을 낭비

 

 ② Sliding Window

  • 지정된 갯수(윈도우 크기)만큼의 패킷은 하나하나 ACK를 기다리지 않고 전송하는 Pipeline 방식을
    사용하는 것으로 성능향상 가능 

 

 

6. Pipeline 방식을 사용할 때의 신뢰성 보장

 Pipelined 방식을 실제로 구현하려면 한번에 얼마만큼의 패킷을 연속적으로 보낼 것인지

오류가 발생한 패킷을 어떻게 재전송할 것인지를 생각해야한다.  이에 대한 두 가지 접근법에 대해 알아보자.

 ① Go Back N (GBN)

  • 사전에 윈도우(Window)의 크기를 임의로 정하여 윈도우의 크기만큼의 패킷을 연속으로 전송

  • 수신측은 대기하고있던 순서에 해당하는 패킷이 아니면 discard, 마지막으로 받은 패킷에 대한 ack를 전송
    => 여기서 ACK은 마지막으로 전달받은 패킷의 시퀀스넘버이다. 
         실제 TCP에서의 ACK는 GBN과는 달리 다음에 받아야할 패킷의 시퀀스넘버를 명시한다.

  • 유실된 패킷에 대한 ack가 오지 않아 timeout이 발생하면 윈도우의 시작 위치를 timeout이 발생한
    패킷으로 옮겨 윈도우의 크기만큼 재전송
  • 수신은 정상적으로 됐지만 ACK만 유실된 경우 다음 ACK만으로도 이전 패킷이 정상적으로 전송됐음을
    유추가능하기에 문제가 발생하지 않음

  • 중간에 하나의 패킷만 유실되도 해당 패킷부터 다시 윈도우의 크기만큼 패킷들을 재송신하는
    방식이기 때문에 그다지 효율적이지 못함

 

 ② Selective Repeat

  • 수신측이 유실된 현재 기다리는중인 패킷이 아니어도 정상적으로 수신한 패킷을 버퍼에 저장하고
    해당 패킷에 대한 ack를 전송

  • 송신측은 timeout이 발생하면 해당 패킷을 재송신하고 이후 정상적으로 전송된 패킷에 대한 ack를
    받더라도 timeout이 발생한 패킷들에 대한 ack가 도착하기 전까지는 윈도우를 이동하지 않으며
    새로운 패킷을 보내지도 않음

  • 수신측은 유실되었던 패킷을 수신하면 그동안 버퍼에 저장해두었던 다른 패킷들과 함께 정상적으로 패킷을
    처리하고 유실되었던 패킷에 대한 ack를 전송

  • 송신측은 timeout이 발생한 패킷에 대한 ack를 수신하면 윈도우를 이동하고 새로운 패킷을 전송
    이때, 미리 ack를 받아두었던 패킷들도 모두 스킵하여 윈도우를 이동한다.
  • GBN에 비해 효율적이지만 구현이 복잡함, 수신측에도 버퍼 필요

  • Selective Repeat 프로토콜의 딜레마
    • Sequence Number 의 범위가 윈도우의 크기만큼일 경우 재전송한 데이터와 정상적으로 보낸
      데이터를 수신측이 구분할 수 없게 되는 경우가 발생할 수 있음

    • Sequence Number 의 범위를 더 넓혀야함 - 얼마만큼 넓혀야하는가
      => 윈도우 크기의 2배가량이 Sequence Number의 범위를 최소화하면서 문제를 해결할 수 있다.
  • 실제 TCP는 윈도우 크기가 매우 크기때문에 모든 패킷에 일일히 타이머를 설정하는 것은 비현실적
    => GBN을 기반으로 조금 더 개선된 방식을 사용

 

 

7. 혼잡 제어(Congestion Control)

  • 패킷이 전송되는 과정에서 네트워크가 혼잡상태에 빠질 경우 이를 감지, 패킷 전송량을 조절
  • TCP의 경우 패킷의 유실을 통해 혼잡을 감지하고 혼잡 제어에 들어간다.

 ① AIMD(Additive Increase Multicative Decrease)

  • 합증가/곱감소 알고리즘
  • 처음에는 패킷을 주기마다 하나씩 보내는 것으로 시작
  • 패킷이 정상적으로 보내질 경우 주기마다 1씩 전송량을 증가
  • 윈도우 크기 증가여부는 이전에 윈도우 크기를 증가시킨 뒤 처음으로 보내진 패킷의 ACK로 판단한다.
  • 패킷전송에 실패하거나 timeout이 발생할 경우 윈도우 크기를 절반으로 감소
    => 단순 패킷 유실의 경우 일부 패킷을 제외한 다른 패킷의 ACK이 도달하기에 구분 가능
  • 초반에 패킷 전송량이 늘어나는데 너무 많은 시간이 걸림
  • 네트워크의 혼잡을 미리 감지하지 못하고 혼잡이 발생하고나서 비로소
    윈도우 크기를 절반으로 줄이는 방식

 

 ② Slow Start(Multicative Increase)

  • AIMD와 마찬가지로 처음에는 패킷을 하나 보내는 것으로 시작
  • 패킷이 정상적으로 보내질 경우 "모든 ACK마다" 1씩 전송량을 증가. 즉, 주기마다 윈도우 크기는 2배씩 증가
  • 혼잡이 감지될 경우 해당 시점에서의 윈도우 크기를 기억하고 윈도우 크기를 1로 초기화
  • 이후 혼잡이 감지된 시점의 윈도우 크기의 절반까지는 기존의 방식대로 주기마다 2배씩 증가
  • 절반 이후부터는 선형적으로 주기마다 1씩 증가시키는 방식을 사용
  • 초기 패킷 전송량을 빠르게 높여갈 수 있으며 한번 혼잡을 감지하고나면 AIMD보다 효율적인 혼잡제어가 가능 

 ③ Fast Retransmit - 패킷유실에 대한 대처

  • 수신측이 순서에 맞지 않는 패킷을 받았을 때도 ACK를 전송
  • 이 때, ACK 값으로 마지막으로 순서를 지켜 들어온 패킷의 다음 패킷의 시퀀스 넘버를 담아
    전송하는 것으로 송신측이 빠진 패킷만을 재전송하도록 할 수 있음
  • 또한 재전송이 발생한 것으로 네트워크 혼잡을 감지하여 윈도우 사이즈를 조절할 수 있음

 

 ④ Fast Recovery - Slow Start의 윈도우 크기 복구에 걸리는 시간 개선

  • 혼잡을 감지할 경우 윈도우 사이즈를 1로 줄이지 않고 반으로 줄이는 방식
  • 이 방식에서는 혼잡이 발생하기 전까지는 Slow Start 방식을, 발생한 후에는 AIMD방식을 사용하게된다.

 

7. Nagle 알고리즘 

if there is new data to send then
    if the window size ≥ MSS and available data is ≥ MSS then
        send complete MSS segment now
    else
        if there is unconfirmed data still in the pipe then
            enqueue data in the buffer until an acknowledge is received
        else
            send data immediately
        end if
    end if
end if

 

  • Pipelined Protocol에서 패킷 하나로 전송되는 데이터의 크기가 너무 작을 경우 데이터크기 대비 패킷량이
    너무 많아지는 오버헤드가 발생
  • 지정한 사이즈의 버퍼를 사용하여 이전에 보낸 패킷의 ack가 돌아오기 전까지 전송해야할 데이터는 버퍼에 저장

  • ack가 돌아오면 버퍼에 넣어둔 데이터를 하나의 패킷으로 전송
  • 이 방식을 사용할 경우 전송 속도는 느려지지만 전송되는 패킷의 수가 줄어들어 효율적인 통신이 가능하다.

  • 빠른 응답속도가 요구되는 시스템에서는 nagle을 사용하지 않는 것이 좋을 수 있다.

  • WAN 과 같이 규모가 크고 느린 네트워크 통신에서는 nagle을 사용하는 것이 효과적이다.

 

'CS > 네트워크' 카테고리의 다른 글

#8 Link Layer(Data Link Layer + Physical Layer)  (0) 2021.11.25
#7 Network Layer  (0) 2021.11.18
#5 Application Layer 2 - HTTP  (0) 2021.11.01
#4 Socket Programming  (0) 2021.10.29
#3 Application Layer 1 - 소켓(Socket)  (0) 2021.10.28

1. 액션 추가

from PySide6.QtCore import QObject
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QMainWindow, QWidget, QGridLayout

from controller.config_manager import set_geometry, get_geometry
from controller import actions
from widgets.docs_window import DocTab
from widgets.simple import TimeLabel


class MainWindow(QMainWindow, QObject):

        ``` 생략 ```

        # 화주삭제 액션 추가
        delete_owner = QAction(QIcon('src/img/delete_owner_icon.png'), '화주 삭제', self)
        delete_owner.setShortcut('Ctrl+Shift+R')
        delete_owner.setStatusTip('화주 추가')
        delete_owner.triggered.connect(lambda: actions.delete_owner(self, self.central_widget.doc_tab.tab1))
        file_menu.addAction(delete_owner)
        tool_bar.addAction(delete_owner)

        # 화주 이름 변경 액션 추가
        modify_owner = QAction(QIcon('src/img/modify_owner_icon.png'), '화주 이름 변경', self)
        modify_owner.setShortcut('Ctrl+Shift+C')
        modify_owner.setStatusTip('화주 이름 변경')
        modify_owner.triggered.connect(lambda: actions.modify_owner(self, self.central_widget.doc_tab.tab1))
        file_menu.addAction(modify_owner)
        tool_bar.addAction(modify_owner)
        
        
        ``` 생략 ```
  • 화주 삭제(delete_owner), 화주 이름 변경(modify_owner) 액션을 생성하여 메뉴바와 툴바에 추가

  • 각 액션의 triggered 시그널과 controller.actions 의 슬롯함수를 연결

 

 

2. 화주 삭제

# 화주 삭제
def delete_owner(main_window, table_widget, name_data=None):
    # 입력받은 이름이 이미 있는 경우
    if name_data:
        name = name_data

    # 이름을 입력받아야 하는 경우
    else:
        # 다이얼로그 위젯 생성
        delete_owner_dialog = Dialog()
        delete_owner_dialog.setWindowTitle('화주 삭제')
        delete_owner_dialog.setGeometry(500, 500, 300, 50)

        # 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
        grid = QGridLayout()
        input_name = QLineEdit()
        input_name.setPlaceholderText('화주 이름')
        input_name.returnPressed.connect(delete_owner_dialog.success)
        submit = QPushButton('삭제')
        submit.clicked.connect(delete_owner_dialog.success)
        grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
        grid.addWidget(input_name, 0, 1, 1, 2)
        grid.addWidget(submit, 1, 0, 1, 3)
        delete_owner_dialog.setLayout(grid)

        # 다이얼로그를 modal 하게 표시
        delete_owner_dialog.show_modal()

        # 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 삭제절차 종료
        if delete_owner_dialog.canceled:
            return

        # 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
        name = input_name.text()

    # 화주 삭제
    try:
        target = session.query(DayCalOwner).filter(DayCalOwner.name == name).first()
        session.delete(target)
        session.commit()
        table_widget.owner_removed(target.name)
    except UnmappedInstanceError:
        main_window.statusBar().showMessage('>> 등록되지 않은 화주입니다.')
  • 기본적인 흐름은 화주 추가와 동일

  • 쿼리를 통해 입력받은 이름에 해당하는 데이터를 찾아 삭제

  • 삭제를 테이블에 즉각 반영하기 위해 table_widget.owner_removed 실행
  • 해당하는 데이터가 존재하지 않을 경우 status bar에 등록되지 않은 화주임을 표시

 

 

3. 화주 삭제 반영

    # 화주 삭제 반영
    def owner_removed(self, removed_name):
        idx = self.owners.index(removed_name)
        self.input_table.removeColumn(idx)
        del self.owners[idx]
  • 화주 이름 리스트에서 화주의 인덱스를 찾아 해당 인덱스의 열을 삭제하고 화주 이름 리스트에서도 삭제

 

 

4. 화주 이름 변경

# 화주 이름 변경
def modify_owner(main_window, table_widget, name_data=None):
    # 다이얼로그 위젯 생성
    modify_owner_dialog = Dialog()
    modify_owner_dialog.setWindowTitle('화주 이름 변경')
    modify_owner_dialog.setGeometry(500, 500, 300, 50)
    grid = QGridLayout()

    # 화주 이름을 미리 입력받지 않은 경우(메뉴바나 툴바에서 변경액션에 접근한 경우)
    org_name = QLineEdit()
    if not name_data:
        # 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
        org_name.setPlaceholderText('화주 이름')
        grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
        grid.addWidget(org_name, 0, 1, 1, 2)

    chg_name = QLineEdit()
    chg_name.setPlaceholderText('바꿀 이름')
    chg_name.returnPressed.connect(modify_owner_dialog.success)
    submit = QPushButton('변경')
    submit.clicked.connect(modify_owner_dialog.success)

    grid.addWidget(QLabel('바꿀 이름: '), 1, 0, 1, 1)
    grid.addWidget(chg_name, 1, 1, 1, 2)
    grid.addWidget(submit, 2, 0, 1, 3)
    modify_owner_dialog.setLayout(grid)

    # 다이얼로그를 modal 하게 표시
    modify_owner_dialog.show_modal()

    # 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 삭제절차 종료
    if modify_owner_dialog.canceled:
        return

    # 대상 화주이름 name 과 바꿀 이름 changed 를 설정
    name = name_data if name_data else org_name.text()
    changed = chg_name.text()

    # 화주 이름 변경
    try:
        target = session.query(DayCalOwner).filter(DayCalOwner.name == name).first()
        target.name = changed
        session.commit()
        table_widget.owner_modified(name, changed)
    except AttributeError:
        main_window.statusBar().showMessage('>> 등록되지 않은 화주입니다.')
    except DatabaseError:
        main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
        session.rollback()
  • Dialog를 통해 변경할 화주의 이름(name)과 바꿀 이름(changed)을 입력받음

  • 데이터베이스에서 name에 해당하는 데이터를 가져와 이름을 변경한 뒤 commit

  • 이름 변경을 즉각 반영하기 위해 table_widget.owner_modified 실행
  • AttributeError 발생시 status bar에 이름을 바꾸려는 화주가 등록되지 않은 화주임을 표시
  • DatabaseError 발생시 status bar에 바꿀 이름이 이미 존재하는 다른 화주의 이름임을 표시

 

 

5. 화주 이름 변경 반영

    # 화주 이름 변경 반영
    def owner_modified(self, org_name, chg_name):
        idx = self.owners.index(org_name)
        self.owners[idx] = chg_name
        self.input_table.setHorizontalHeaderItem(idx, QTableWidgetItem(chg_name))
  • 기존 이름의 인덱스를 찾아 바꿀 이름으로 변경하고 해당 열의 헤더에도 반영

 

 

6. 테스트

 

  • 등록되지 않은 화주 삭제 시도

 

 

 

  • 등록되지 않은 화주의 이름 변경 시도

 

 

 

  • 바꿀 이름이 이미 존재하는 화주의 이름인 경우 

 

 

 

  • 정상적인 삭제

 

 

 

  • 정상적인 이름 변경

'개인 프로젝트 > Accounting Program' 카테고리의 다른 글

#18 저장기능 구현  (0) 2021.11.25
#17 QTableView  (0) 2021.11.22
#15 모델/구조 변경, 화주 추가 기능 수정  (0) 2021.11.02
#14 화주 추가  (0) 2021.11.01
#13 데이터베이스 연결  (0) 2021.10.30

1. DayCalOwner 모델 변경

# 화주 모델
class DayCalOwner(Base):
    __tablename__ = 'daycal_owner'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, unique=True)

    def __init__(self, name):
        self.name = name
  • autoincrement 가 설정된 id 속성을 추가

  • primary key를 name에서 id로 변경
  • name을 primaryk 중복 불가능한 값으로 하기 위해 unique 설정

 

 

2. actions.py

from PySide6.QtWidgets import QLabel, QGridLayout, QPushButton, QLineEdit
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm.exc import UnmappedInstanceError

from controller.db_manager import session
from models.models import DayCalOwner
from widgets.simple import Dialog


# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
    # 입력받은 이름이 이미 있는 경우
    if name_data:
        name = name_data

    # 이름을 입력받아야 하는 경우
    else:
        # 다이얼로그 위젯 생성
        create_owner_dialog = Dialog()
        create_owner_dialog.setWindowTitle('화주 추가')
        create_owner_dialog.setGeometry(500, 500, 300, 50)

        # 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
        grid = QGridLayout()
        input_name = QLineEdit()
        input_name.setPlaceholderText('화주 이름')
        input_name.returnPressed.connect(create_owner_dialog.success)
        submit = QPushButton('추가')
        submit.clicked.connect(create_owner_dialog.success)
        grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
        grid.addWidget(input_name, 0, 1, 1, 2)
        grid.addWidget(submit, 1, 0, 1, 3)
        create_owner_dialog.setLayout(grid)

        # 다이얼로그를 modal 하게 표시
        create_owner_dialog.show_modal()

        # 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
        if create_owner_dialog.canceled:
            return

        # 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
        name = input_name.text()

    # 새 화주 추가
    new_owner = DayCalOwner(name)
    try:
        session.add(new_owner)
        session.commit()
        table_widget.owner_added(name)
    except DatabaseError:
        main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
        session.rollback()
  • main_window 에 구현했던 화주 추가 액션을 controller 디렉토리의 actions.py 로 이동

  • self 대신 메인윈도우의 인스턴스와 화주 추가 결과를 반영할 table이 위치한 위젯의 인스턴스를 인자로 받음

  • dialog를 통하지않고 이름을 입력받을 상황을 대비하여 name_data 인자를 추가

  • name_data가 입력되었다면 다이얼로그로 이름을 입력받지 않고 name_data를 name으로 설정

  • 중복된 이름일 경우 status bar에 이미 등록된 화주임을 표시

  • name이 더이상 primary key가 아닌 unique key이기 때문에 이름이 중복될 경우 add 가 아닌 commit단계에서
    예외가 발생한다. 그러므로 예외 발생시 이미 적용된 add 를 취소하기 위해 rollback을 호출해줘야한다.

 

from controller import actions

    ``` 생략 ```
 
        # 화주추가 액션 추가
        create_owner = QAction(QIcon('src/img/create_owner_icon.png'), '화주 추가', self)
        create_owner.setShortcut('Ctrl+Shift+A')
        create_owner.setStatusTip('화주 추가')
        create_owner.triggered.connect(lambda: actions.create_owner(self, self.central_widget.doc_tab.tab1))
        file_menu.addAction(create_owner)
        tool_bar.addAction(create_owner)
        
    ``` 생략 ```
  • contoller.actions를 import 한 뒤 create_owner 액션의 triggered 시그널에 lambda 함수의 형태로 연결해준다.

 

 

3. simple.py

from PySide6.QtCore import QDateTime, QTimer
from PySide6.QtWidgets import QLabel, QDialog


# 시간레이블 클래스(QLabel 상속)
# 1초마다 현재 날짜/시간을 갱신하여 표시하는 레이블
class TimeLabel(QLabel):
    def __init__(self):
        super().__init__()
        self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.timeout)
        self.timer.start(100)

    def timeout(self):
        self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))


# 다이얼로그(QDialog 상속)
class Dialog(QDialog):
    def __init__(self):
        super().__init__()
        self.canceled = True

    def show_modal(self):
        return super().exec_()

    def success(self):
        self.canceled = False
        self.close()
  • Dialog, TimeLabel 등의 구조가 간단한 위젯들을 widgets/simple.py 에 분리

 

 

4. DayCal 위젯

# 일일 정산서 계산서 위젯
class DayCal(QWidget):
    # 생성자
    def __init__(self):
        super().__init__()
        self.owners = [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
        self.num_of_owners = len(self.owners)
        self.input_table = QTableWidget()
        self.input_table.setParent(self)
        self.init_ui()

    ``` 생략 ```
  • 화주 데이터의 id를 기반으로 순서를 유지하도록 쿼리시에 order_by 를 통해 id를 기준으로 오름차순 정렬

 

 

5. create_owner 수정

# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
    # 입력받은 이름이 이미 있는 경우
    if name_data:
        name = name_data

    # 이름을 입력받아야 하는 경우
    else:
        # 다이얼로그 위젯 생성
        create_owner_dialog = Dialog()
        create_owner_dialog.setWindowTitle('화주 추가')
        create_owner_dialog.setGeometry(500, 500, 300, 50)

        # 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
        grid = QGridLayout()
        input_name = QLineEdit()
        input_name.setPlaceholderText('화주 이름')
        input_name.returnPressed.connect(create_owner_dialog.success)
        submit = QPushButton('추가')
        submit.clicked.connect(create_owner_dialog.success)
        grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
        grid.addWidget(input_name, 0, 1, 1, 2)
        grid.addWidget(submit, 1, 0, 1, 3)
        create_owner_dialog.setLayout(grid)

        # 다이얼로그를 modal 하게 표시
        create_owner_dialog.show_modal()

        # 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
        if create_owner_dialog.canceled:
            return

        # 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
        name = input_name.text()

    # 새 화주 추가
    new_owner = DayCalOwner(name)
    try:
        session.add(new_owner)
        session.commit()
        table_widget.owner_added(name)
    except DatabaseError:
        main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
        session.rollback()
  • 이름을 입력하기 위한 위젯을 QPlainText 에서 QLineEdit 으로 변경

  • returnPressed 시그널을 사용하여 엔터키에도 반응하도록 수정
  • 다이얼로그에 canceled 속성을 추가하여 단순 종료와 데이터를 정상적으로 제출한 종료를 구분

  • 데이터를 정상적으로 제출했을 때만 기능이 동작하도록 수정

 

'개인 프로젝트 > Accounting Program' 카테고리의 다른 글

#17 QTableView  (0) 2021.11.22
#16 화주 삭제, 화주 이름 변경  (0) 2021.11.02
#14 화주 추가  (0) 2021.11.01
#13 데이터베이스 연결  (0) 2021.10.30
#12 메인 화면 구성  (0) 2021.10.28

지난번에는 로컬 데이터베이스를 설치하고 연결 세션을 만들어 데이터베이스에 등록된 화주 명단에 따라 테이블이

구성되도록 하였다.  이번에는 화주 추가 기능을 구현하여 실제로 데이터베이스에 화주를 등록하고 정상적으로

반영되는지 확인해본다.

 

1. 화주 추가 액션

        # 화주추가 액션 추가
        create_owner = QAction(QIcon('src/img/create_owner_icon.png'), '화주 추가', self)
        create_owner.setShortcut('Ctrl+Shift+A')
        create_owner.setStatusTip('화주 추가')
        create_owner.triggered.connect(self.create_owner)
        file_menu.addAction(create_owner)
        tool_bar.addAction(create_owner)
  • widgets/main_window.py 에서  MainWindow 위젯의 init_ui 메소드에 위와 같은 내용을 추가
  • create_owner 액션을 만들어 메뉴와 툴바에 추가하고 self.create_owner 메소드에 연결

 

 

2. 화주 추가 메소드

# 화주 추가
    def create_owner(self):
        # 다이얼로그 위젯 생성
        create_owner_dialog = Dialog()
        create_owner_dialog.setWindowTitle('화주 추가')
        create_owner_dialog.setGeometry(500, 500, 300, 50)

        # 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
        grid = QGridLayout()
        input_name = QPlainTextEdit()
        input_name.setPlaceholderText('화주 이름')
        submit = QPushButton('추가')
        submit.clicked.connect(create_owner_dialog.close)
        grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
        grid.addWidget(input_name, 0, 1, 1, 2)
        grid.addWidget(submit, 1, 0, 1, 3)
        create_owner_dialog.setLayout(grid)

        # 다이얼로그를 modal 하게 표시
        create_owner_dialog.show_modal()

        # 다이얼로그에서 입력이 완료되면 입력받은 이름으로 화주 추가
        name = input_name.toPlainText()
        new_owner = DayCalOwner(name)
        try:
            session.add(new_owner)
            session.commit()
            self.central_widget.doc_tab.tab1.owner_added(name)
        except IntegrityError:
            self.statusBar().showMessage('>> 이미 등록된 화주입니다.')
  • widgets/main_window.py 에서  MainWindow 위젯에 create_owner 메소드를 위와 같이 정의

  • 추가할 화주의 이름을 입력받기 위한 다이얼로그를 생성하여 Modal 하게 표시한다.
    => Modal : 다이얼로그가 떠있는 동안 메인 프로세스와 상호작용할 수 없도록 하는 방식

  • 입력받은 이름을 key 값으로 하여 화주 모델인 DayCalOwner 클래스의 인스턴스를 생성

  • 데이터베이스 session에 인스턴스를 add, commit

  • central_widget의 doc_tab의 첫번째 탭인 DayCalWidget에서 화주가 추가됐을 때 이를 즉각 테이블에
    반영하기 위한 메소드인 owner_added를 실행

 

 

3. CentralWidget 

class CentralWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.doc_tab = DocTab()
        self.init_ui()

    def init_ui(self):
        self.setStyleSheet("background-color: #FFFFFF")

        # 그리드 레이아웃
        grid = QGridLayout()

        # 탭 위젯 추가
        grid.addWidget(self.doc_tab, 0, 0)

        # 중앙 위젯에 그리드 레이아웃 적용
        self.setLayout(grid)
  • DocTab 을 레이아웃을 통하지 않고 바로 참조하기 위해 CentralWidget을 QWidget을 상속하여 별도로 정의

 

 

4. Dialog

# 다이얼로그(QDialog 상속)
class Dialog(QDialog):
    def __init__(self):
        super().__init__()

    def show_modal(self):
        return super().exec_()
  • show_modal 기능을 갖춘 다이얼로그 위젯을 정의

 

 

5. 테이블에 추가된 화주 반영

    # 화주 추가 반영
    def owner_added(self, added_user):
        self.owners.append(added_user)
        self.input_table.insertColumn(self.input_table.columnCount())
        self.input_table.setHorizontalHeaderItem(self.input_table.columnCount()-1, QTableWidgetItem(added_user))
  • widgets/docs_window.py 에서 DayCal 위젯에 create_owner 메소드를 위와 같이 정의
  • 화주명 리스트에 추가된 화주이름을 추가하고 테이블에 열을 하나 추가하여 추가된 화주의 이름을 헤더로 설정

 

 

 

 

정상적으로 화주가 추가되는 것을 확인할 수 있다.

 

※ 간단하게 구현을 해봤지만 화주 이름을 추가할 때 추가 버튼을 엔터키로 대체할 수 없는 부분이나 
   아무것도 버튼을 누르지 않고 창을 닫기만 해도 입력된 것으로 처리되는 등의 문제가 있어 수정이 필요한 상태이다.

=> 현재는 수정되었다.

 

1. HTTP(HyperText Transfer Protocol)

  • HTML 등의 하이퍼미디어 문서를 전송하기 위한 프로토콜

  • 일반적으로 웹 브라우저와 웹 서버간의 통신을 위해 설계되었지만 다른 용도로 사용할 수도 있음

  • 주로 TCP를 사용(HTTP/3 부터는 UDP를 사용한다고 한다.)

 

 

2. HTTP의 특성

 ① 비연결성(Connectionsless)

  • HTTP 프로토콜은 서버가 클라이언트의 요청을받아 처리하고나면 연결을 유지하지 않고 해제해버림
  • 더 많은 클라이언트로부터 요청을 받기 위해 연결 유지에 드는 리소스를 절약하기 위함
  • 클라이언트의 요청에 대해 매번 새로운 연결을 수립하고 해제하는 오버헤드가 발생

  • KeepAlive 속성을 부여하여 주기적으로 상대의 상태를 묻는 패킷을 보내 반응이 있는동안은 연결을
    유지하도록 할 수 있지만 연결을 유지하기 위해 리소스를 사용하므로 주의해야함

 

 ② 무상태(Stateless)

  • HTTP 프로토콜의 비연결성으로 인해 서버는 클라이언트를 식별할 수 없는 상태

  • 서버가 클라이언트를 식별하려면 어딘가에 클라이언트의 정보를 저장해둘 필요가 있음
    1. 쿠키(Cookie)
      • 클라이언트측에 클라이언트의 정보를 저장
      • 정보가 클라이언트측에 저장되기 때문에 서버에 부담이 가진 않지만 보안이 취약
    2. 세션(Session)
      • 서버측에 클라이언트의 정보를 저장
      • 정보가 서버측에 저장되기 때문에 서버의 리소스를 차지하게되지만 보안면에서 쿠키보다 나음
      • 동시 접속자 수가 많아지면 서버 과부하의 원인이 될 수 있음
      • 세션 정보 또한 중간에 탈취당할 수 있기 때문에 보안면에서 여전히 불안요소가 있음
    3. 토큰(token)을 사용한 인증
      • 보호할 데이터를 토큰으로 치환(암호화)하여 원본데이터대신 토큰을 사용하는 방식
      • OAuth, JWT 등이 대표적

 

 

3. HTTP 응답 상태코드

 클라이언트의 요청에 대한 서버의 응답에 포함되는 요청의 처리 상태를 나타내는 메시지

 

  • 1XX : 요청 접수중, 처리중 등 서버의 상태를 나타내는 코드. 특수한 경우를 제외하면 거의 사용되지 않음

  • 2XX : 성공 코드. 요청이 성공적으로 접수/처리된 경우

  • 3XX : 리다이렉션 코드. 요청한 페이지가 새 위치로 이동했거나 마지막 요청 이후 변경이 없는 경우

  • 4XX : 요청 오류 코드. 클라이언트측의 문제(잘못된 주소, 허용되지 않은 방식 등)로 처리에 문제가 생긴 경우

  • 5XX : 서버 오류 코드. 서버측의 문제(미구현 기능, 불량 게이트웨이 등)로 처리에 문제가 생긴 경우

 

 

4. HTTP Method

 HTTP 프로토콜상에 정의된 요청의 방식

 

  • GET : 서버에게 리소스를 요청(조회)하기 위해 사용

  • HEAD : GET과 유사하지만 본문 없이 헤더만을 통해 정보를 획득하기 위해 사용

  • PUT : 서버가 요청에 따라 리소스를 생성하거나 수정해야하는 경우 사용

  • POST : 입력 데이터를 요청 본문에 넣어 서버에 전송하기 위해 사용

  • DELETE : 서버에 리소스 삭제를 요청하기 위해 사용
  • TRACE : 요청 패킷이 서버에 도착하는 사이 변조되는 등의 문제가 발생하는지 여부를 확인하기 위해 사용
  • OPTIONS : 서버에게 특정 리소스가 지원하는 메소드를 알아보기 위해 사용

 

 

5. RESTful API

 ① REST(Representational State Transfer)

  • HTTP 프로토콜을 개선해나가는 과정에서 이전 버전의 HTTP로 구축된 웹과의 호환성문제가 대두
  • 로이 필딩(Roy Fielding)은 이를 해결하기 위해 웹과 같은 분산 하이퍼미디어 시스템을 위한
    아키텍쳐 스타일인 REST를 만들어냄
  • 여기서 말하는 아키텍쳐 스타일이란 제약 조건의 집합과도 같은 의미로, REST 가 내세우는 모든
    제약 조건을 만족하여 시스템을 설계했을 때 비로소 REST 하다고 말할 수 있음

 ② REST 아키텍쳐의 6가지 제약조건

  1. 클라이언트-서버 구조
    • 유저 인터페이스를 데이터 저장소와 분리하는 것으로 여러 플랫폼에 대해 인터페이스의 이식성을 높임
    • 서버측의 컴포넌트를 단순화하는 것으로 확장성을 높임
    • 서버, 클라이언트측의 컴포넌트가 독립적으로 발전 가능해짐

  2. 무상태(Stateless)
    • 클라이언트의 어떤 컨텍스트도 서버에 저장되지 않으며 세션 상태 등은 전적으로 클라이언트측에 저장
    • 시스템의 가시성, 신뢰성, 확장성을 증가시키기 위함

  3. 캐시(Cache)
    • 서버의 응답 데이터는 암시적으로든 명시적으로든 캐싱 가능 여부가 정해져있어야 함
    • 클라이언트측은 캐싱 가능하다고 정해진 응답에 대해서는 그 데이터를 재사용할 권리를 가짐
    • 네트워크 효율성을 증가시키기 위함
  4. 통일된 인터페이스(Uniform Interface)
    • 컴포넌트간의 인터페이스를 통일하여 시스템의 전반적인 구조를 단순화하고 상호작용의 가시성 향상
    • 데이터가 어플리케이션이 원하는 상태가 아닌 표준화된 형식으로 전달되기 때문에 효율성이 떨어질 수 있음
    • 웹과 같은 대규모 하이퍼 미디어 데이터 전송을 위한 시스템에 최적화

  5. 계층화된 시스템(Layered System)
    • 시스템을 계층화하여 시스템 전체의 복잡성을 낮추고 각 계층의 서비스를 독립적으로 관리할 수 있도록 함
    • 중개자(Intermediaries)는 로드 밸런싱을 통해 서버의 부담을 낮추고 서버의 확장성을 높여줄 수 있음
    • 데이터 처리에 오버헤드와 지연이 발생하여 사용자의 체감 성능이 떨어질 수 있지만 공유 캐싱을 통해
      이를 상쇄할 수 있음

  6. Code-On-Demand(Optional)
    • 클라이언트가 서버로부터 애플릿이나 스크립트 등의 코드를 제공받아 기능을 확장할 수 있도록 함
    • 클라이언트측에 기본적으로 구현되어있어야할 기능의 수가 줄어들어 클라이언트가 단순화됨
    • 서버측에서 새로운 기능을 추가적으로 지원하기 편해져 확장성이 증가
    • 시스템의 가시성을 감소시킬 수 있는 제약이기에 필수적인 제약은 아님
    • 현재 웹 기준 자바 스크립트가 이에 해당

 

 ③ REST API - REST 규칙을 완벽하게 지키는 API

  • HTTP의 규칙에만 따라도 클라이언트-서버 구조, 무상태, 캐시, 계층화된 시스템, Code-On-Demand 등
    대부분의 REST 규칙은 잘 지켜짐

  • 단일 인터페이스(Uniform Interface)의 네 가지 제약조건 중 두 가지가 문제가 됨
    • 리소스의 식별(identification of resources)
      • URI를 사용함으로서 만족

    • 메시지를 통한 리소스의 조작(manipulation of resources through representations)
      • 단순히 메시지를 통해 자원을 조작할 수 있다면 만족
    • 자기 서술적 메시지(self-descriptive messages)
      • 메시지만으로 해당 요청을 어떻게 처리해야할지에 대한 충분한 정보를 포함해야함
      • 오늘날 대부분의 API가 지키지 못하는 규칙
      • 웹에서는 기본적으로 만족

    • 하이퍼링크를 통한 상태전이(hypermedia as the engine of application state - HATEOAS)
      • 애플리케이션의 상태 전이가 하이퍼미디어를 통해 이뤄짐
      • 웹에서는 기본적으로 만족

 

 ④ RESTful API - REST 스러운 API

  • 완벽한 REST API를 만들기는 현실적으로 어려움이 따름
  • 완벽하진 않더라도 가능한 선에서는 REST 규칙을 지키는 RESTful API를 구현할 수는 있음
  • URI 매핑이 깔끔해지고 메시지가 의미하는 바가 명확해진다는 장점을 가짐

 

 

6. HTTP 헤더

 ① 요청 라인(Request Line)

  • {HTTP 메소드} {요청 uri} HTTP/{HTTP 버전}  의 형태로 구성

 

 ② 공통 헤더(General Headers)

  • Date : 메시지가 발행된 날짜와 시간

  • Connection : 현재 전송 이후 연결의 유지여부(Keep-Alive / close)
    => HTTP /1.1 에서 주로 사용 (HTTP /2 이후로는 사용불가)

  • Content-Length : 본문의 크기를 바이트단위로 표시

  • Content-Type : 컨텐츠 타입(MIME)과 인코딩을 명시할 수 있음

  • Content-Language : 사용자의 언어

  • Content-Encoding : 컨텐츠의 압축 인코딩(br, gzip, deflate, compress, identity 등).

  • Cache-Control : 요청과 응답 내의 캐싱 매커니즘을 위한 디렉티브를 정하는데 사용

 

 ③ 요청 헤더(Request Headers)

  • Host : 서버의 도메인 네임
  • User-Agent : 사용자가 요청을 보내는데 사용한 클라이언트(운영체제, 애플리케이션, 브라우저)
  • Accept : 클라이언트가 수용 가능한 미디어 타입(MIME)
  • Cookie : 웹 서버가 클라이언트에 저장해둔 쿠키 정보
  • Origin : 요청이 시작된 주소
  • If-Modified-since : 주어진 날짜 이후에 리소스가 변경되지 않았다면 요청을 제한
  • Authorization : 서버에게 인증 토큰을 보내기 위한 헤더. JWT 등을 사용한 인증에서 사용

 

 ④ 응답 헤더(Response Headers)

  • Server : 웹 서버의 정보

  • Access-Control-Allow-Origin : 서버가 허용하는 요청 시작 주소

  • Access-Control-Allow-Headers : 서버가 허용하는 헤더

  • Access-Control-Allow-Methods : 서버가 허용하는 메소드

  • Allow : 리소스가 지원하는 메소드 집합

  • Content-Diposition : 브라우저가 응답 본문을 표시할 방식(inline: 본문표시 / attachment : 다운로드)
    => attachment 의 경우 filename 옵션으로 파일명 지정도 가능

  • Location : 300번대 응답(리다이렉션)이나 201 Created 응답(리소스 생성)시 이동할 페이지 주소 명시

  • Content-Security-Policy : 컨텐츠 보안정책 명시(외부 컨텐츠를 가져올 경우)

 

 

'CS > 네트워크' 카테고리의 다른 글

#7 Network Layer  (0) 2021.11.18
#6 Transport Layer  (0) 2021.11.16
#4 Socket Programming  (0) 2021.10.29
#3 Application Layer 1 - 소켓(Socket)  (0) 2021.10.28
#2 OSI 7계층  (0) 2021.10.28

+ Recent posts