Sad Puppy 3 개발자 아지트 :: 개발자 아지트

GC;Garbage Collection 이란?

자바의 메모리 관리 방법 중의 하나

 

JVM Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거하는 프로세스

 

*JVM 메모리 영역에서 Heap 영역은 객체가 생성되어 저장되는 공간이다.

(new 키워드로 생성된 객체들이 저장됨)

 

- 다른 영역

    - Method 영역

       

        클래스 정보 저장공간

       

    - Stack 영역

       

        메서드 실행 시 사용되는 공간(지역변수, 호출정보 등)

[장점]

 

Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있음

 

→ 개발자 입장에서 메모리 관리, 메모리 누수(Memory Leak) 문제에 대해 관리하지 않아도 되어 오로지 개발에만 집장할 수 있음

 

*C/C++에서는 이런게 없어서 개발자가 수동으로 메모리 할당과 해제를 해줘야 했었음

 

*파이썬, 자바스크립트, Go 등에도 가비지 컬렉션이 기본 내장 되어있음

 

→ 가비지 컬렉션에 대해 제대로 학습하면, 자바 외의 다른 언어의 가비지 컬렉션 동작에 대해서도 어느정도 통달

 

[단점]

 

자동으로 처리해준다 해도 메모리가 언제 해제되는지 정확하게 알 수 없어서 제어하기 힘듦

 

가비지 컬렉션이 동작하는 동안, 다른 동작을 멈추기 때문에 오버헤드가 발생됨

 

*오버헤드 : 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간이나 메모리 등

 

⇒ 이를 Stop-The-World 라고 함

 

STW;Stop-The-World

GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상

GC가 동작하는 동안 GC관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있음이 시간을 최소화 시키는게 중요함

⇒ 따라서 GC가 너무 자주 실행되는것도 소프트웨어 성능 하락을 초래할 수 있음

 

ex) exploler GC를 너무 자주 실행시켜 성능에 문제를 일으키는 것으로 악명 높았음

 

⇒ 실시간 동작이 중요한 프로그램은 GC사용이 부적절 할 수 있음

 

따라서 개발자가 애플리케이션 사용성을 유지하며 효율적으로 GC를 실행하는 최적화 작업을 해줘야함

 

⇒ 이를 GC 튜닝이라 한다.

 

GC는 어떻게 힙 영역을 관리하나요?

 

모든 객체는 처음에 Young Generation에 생성된다.

 

*Young Generation의 공간은 Old Generation에 비해 상대적으로 좁기 때문에 빠르게 메모리 상의 객체를 찾고 제거할 수 있다. (Young Generation에서 발생되는 GC Minor GC라고 한다)

 

1-1. 정확히는 Young Generation에서 Eden 영역에 위치하게 된다.

 

1-2. 객체가 계속 생성되어 Eden 영역이 꽉 차게 되면 Minor GC가 실행된다.

 

1-2-1. Mark 동작을 통해 reachable 객체를 탐색한다.

 

1-2-2. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동한다.

 

1-2-3. Eden 영역에서 사용되지 않는 객체(unreachable)의 메모리를 해제(sweep)한다.

 

1-3. 살아남은 모든 객체들의 age(Survivor 영역에서 살아남은 횟수)값이 1씩 증가한다.

 

*만약 age값이 임계값에 다다르면 Promotion(Old 영역으로 이동) 여부를 결정한다.

 

1-1, 1-2, 1-3 과정이 반복됨.

 

Old Generation은 길게 살아남(age 임계값 도달)은 메모리들이 존재하는 공간이다.

 

객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 Major GC(Full GC)가 발생한다.

 

Old 영역에 있는 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 삭제함

 

⇒ 하지만 공간이 Young Generation에 비해 크기 때문에 메모리 상의 객체 제거에 많은 시간이 걸림

 

*Young Generation에서는 GC 0.5-1초 사이에 끝나서 애플리케이션에 크게 영향을 안주지만,

Old Generation에서는 Young Generation GC에 비해 10배 이상의 시간을 사용함

 

⇒ 이때 STW 문제가 발생하게된다!!

 

Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 하느라 CPU에 부하를 주기 때문에 멈추거나 버벅이는 현상이 일어난다.

패킷

→ 네트워크를 통해 전송되는 형식화된 데이터 덩어리

→ 예를들자면, 이미지 한장 데이터(파일 또는 데이터)를 잘게 패킷 단위로 쪼개서 인터넷을 통해 컴퓨터 대 컴퓨터로 주고 받고 함

→ 왜 잘게 쪼개는가?

  • 컴퓨터 대 컴퓨터로 통하는데는 여러 길이 있음
  • 하나의 길을 전체 데이터 단위가 차지할 경우
    • 그 길이 큰 데이터를 못버틸 수 있음 = 길마다 수용 가능 범위가 있음 → 길이 통제됨
    • 장애가 한번 나면 끝장임
    • 다른 컴퓨터랑도 통신해야하는 길인데 독점하게 됨
  • 잘게 쪼갠 후 보내면 받는 쪽에서는 각각의 순서를 체크하여 재 조립함
  • 만약 일부 데이터가 누락되면 그 데이터만 다시 요청함

⇒ 빠르고 안전하기 때문임

패킷의 구성요소: 헤더, 페이로드

  • 헤더에는 패킷의 출처, 용도, 처리 방법이 적혀있음(원본 및 대상 IP주소)
  • 페이로드는 실제 데이터임 (쪼개진)

라우터란?

둘 이상의 패킷 교환 네트워크(인터넷)나 서브네트워크를 연결하는 장치(네트워크 하드웨어)임.

(서로 다른 네트워크 간에 데이터를 전달함)

패킷이 인터넷을 이동하다가 길이 막히면 다른 경로로 안내해줌

⇒ 인터넷 길목마다 서 있는 ‘교통 경찰관’ 같은 역할을 함

⇒ 패킷이 어느 길로 가야 가장 빠르고 안전하게 갈 수 있을지 매번 판단해줌

⇒ IP주소를 참고함

라우터의 기능

  • 데이터 패킷을 의도한 IP 주소로 전달해서 네트워크 간의 트래픽을 관리함.
  • 여러 장치가 동일한 인터넷 연결을 사용할 수 있도록 함

라우터에도 여러 유형이 있음.

그러나, 대부분의 라우터는 근거리 통신망(LAN)과 광역 네트워크(WAN) 간에 데이터를 전달함.

*LAN: 특정 지리적 영역으로 제한된, 연결된 장치 그룹임. 일반적으로 단일 라우터가 필요.

*WAN: 넓은 지리적 영역에 분산된 대규모 네트워크.

ex) 전국의 여러 위치에서 운영되는 대규모 조직 및 회사는 각 위치에 대해 별도의 LAN이 필요함.

   이 LAN은 다른 LAN과 연결되어 WAN을 형성함

   WAN은 넓은 영역에 분산되어 있으므로, 여러 라우터와 스위치가 필요한 경우가 많음 

라우터의 작동 방식

: 라우터는 내부 라우팅 테이블을 참조해 네트워크 경로를 따라 패킷을 라우팅 하는 방법을 결정함

과정

  1. 라우터가 패킷 수신
  2. 패킷의 헤더를 읽어 목적지를 확인
  3. 라우팅 테이블의 정보를 기반으로 패킷을 라우팅할 위치를 결정

라우팅 테이블의 종류

  • 정적
    • 변경되지 않음.
    • 네트워크 관리자가 수동으로 라우팅 경로를 설정해야함
  • 동적
    • 자동으로 업데이트됨
    • 다양한 라우팅 프로토콜을 사용해 최단 경로와 가장 빠른 경로를 판단하여 결정함
    • 지도에 최상의 주행 경로를 결정하는 방식과 유사하게 패킷이 목적지에 도달하는 데 걸리는 시간을 기반으로 자동 업데이트함
    • 많은 컴퓨팅 성능이 필요함
      • 소규모 네트워크에서 정적 라우팅에 의존하는 이유임
      • 중간, 대규모 네트워크는 동적 라우팅이 훨씬 효율적

주요 라우팅 프로토콜

:IP, BGP 등..

라우팅이란?

네트워크 라우팅은 하나 이상의 네트워크에서 경로를 선택하는 프로세스임

⇒ 인터넷 경로 결정은 라우터라는 특수한 네트워크 하드웨어를 통해 이루어짐

ex) A에서 B로 전달될 때, 라우터가 1-3-5, 2-4 경로 중에 어디로 가는게 빠른지 판단하여 경로를 결정해 주는것임.

 

스위치란?

 

동일한 네트워크 장치 그룹 간(LAN:근거리 통신망)에 데이터 패킷을 전달함

MAC 주소를 참고하여 딱 해당되는 포트로만 보냄

허브처럼 브로드캐스팅 하지 않음

쓰레드의 등장 배경 및 멀티스레딩과 멀티프로세싱

멀티태스킹(멀티 프로세싱, 멀티 스레딩 포함)
├── 멀티프로세싱
└── 멀티스레딩

멀티 프로세싱

: 여러 개의 프로세스를 동시에 혹은 빠르게 시분할 하여 실행하는 방식

-코어가 여러개인 시스템의 경우, 각 코어가 서로 다른 프로세스를 동시에 독립적으로 실행시킴

-코어가 하나인 시스템에서도, 운영체제가 프로세스 간 컨텍스트 스위칭을 매우 빠르게 수행함으로써 여러 프로세스가 동시에 실행되는 것처럼 동작시킴

멀티 프로세싱의 한계

  1. 하나의 프로세스는 동시에 여러 작업을 수행하지 못한다.
    → 여러 프로세스를 생성해 문제를 해결할 수 있으나, 프로세스가 많아지면 관리나 자원 소모 측면에서 여러 단점이 발생함
  2. 프로세스 간 컨텍스트 스위칭이 무겁다.
    → 컨텍스트 스위칭 작업은 무겁고 비용이 큼
    → 프로세스에서 다른 프로세스로 전환할 때 CPU에서 컨텍스트 스위칭이 발생함.
  3. 프로세스 간 데이터 공유가 어렵다. = IPC를 사용해야함
    → 데이터 공유는 언제 필요한건가?
    ex1) 웹 서버 구조, 프론트 요청을 처리하는 프로세스와 DB에 직접 접근하여 결과를 가공하는 백엔드 프로세스 간 데이터를 공유해야 하는 상황
    ex2) 멀티프로세싱 워커(pool), 대용량 데이터를 병렬 처리하기 위해 여러 워커 프로세스를 띄워놓고, 각 워커가 처리한 결과를 모아 최종 결과를 만들어야 하는 상황
    → 각 프로세스는 독립적인 (가상) 메모리 공간을 사용하기 때문에, 서로 다른 프로세스 간 데이터 공유가 까다로움

    왜 까다로운가?
    : 운영 체제는 한 프로세스가 비정상 종료되거나 엉뚱한 메모리에 접근을 해도 다른 프로세스의 메모리에 직접적인 피해를 주지 않아야함.
    때문에 각 프로세스는 독립적인 메모리 공간을 사용하고, “다른 프로세스 메모리를 막 읽거나 쓰는 행위”를 금지함.

    그렇다면 프로세스 간 데이터 공유는 어떻게?
    : 반드시 운영체제가 제공하는 별도의 통로 IPC를 거쳐야함.
    IPC(Inter-Process Communication)

위와 같은 문제를 해결하기 위해 등장한 것 ⇒ 스레드 (Thread)

스레드(Thread): 한 프로세스 내에서 여러 작업을 동시에 실행하게 해줌

  • 하나의 프로세스 내에 여러 스레드를 보유할 수 있다. ⇒ 멀티 스레딩
    • 각 스레드 당 작업 하나가 할당된다.
  • 요즘 CPU의 실행 단위는 스레드 단위이다.
    • 과거에는 프로세스 단위가 CPU의 실행 단위였다.
  • 컨텍스트 스위칭이 가볍다.
    • 스레드는 같은 프로세스 내에서 메모리 영역(Heap)을 공유함.
    • 따라서 프로세스 간 스위칭 보다 훨씬 가볍다.
    • 다만 모든걸 공유하는건 아님
      • 각 쓰레드는 Stack, 포인터, 프로그램 카운터 등을 고유하게 가지면서 자신의 실행 상태를 유지한다.

'ComputerSicence > 운영체제' 카테고리의 다른 글

멀티쓰레딩  (0) 2025.06.04

멀티쓰레딩은 하나의 프로세스 내에서 여러 작업을 여러 쓰레드를 통해 동시에 실행할 수 있도록 하는 방식.

 

특징

1. 경량화된 실행 단위

- 낮은 오버헤드: 스레드는 같은 프로세스 내에서 실행됨 -> 프로세스 간의 컨텍스트 스위칭에 비해 스레드 간 전환은 훨씬 가볍고 빠름

- 빠른 전환: 각 쓰레드는 자신만의 스택과 레지스터(프로그램 카운터)를 갖지만, 코드나 힙 메모리 등은 공유하기 때문에 전환 시 재설정해야 할 데이터의 양이 적어 전환 속도가 빠름

 

2. 효율적인 데이터 공유

- 공유 메모리: 같은 프로세스 내의 스레드들은 힙 영역 등 주요 메모리 공간을 공유함. -> 데이터 전달이 빠르고 간편

-동기화 관리: 스레드 간의 데이터 공유는 IPC와 같은 복잡한 메커니즘 없이도 이루어지지만, 동시에 접근할 경우 동기화 문제는 여전히 존재함

* IPC란 하나의 컴퓨터 안에서 실행 중인 서로 다른 프로세스 간에 발생하는 통신 임 (Inter-process Communication)

* 프로세스는 각자 독립적인 주소 공간을 가지고 수행됨 

 

3. 응답성 및 처리 성능 향상

- 벙렬 처리: 멀티쓰레딩을 통해 I/O작업과 CPU 집약적 작업을 분리해 동시에 처리할 수 있으므로, 시스템 전체의 응답성이 향상됨

-리소스 활용 최적화: CPU의 멀티코어 환경에서 각 스레드를 개별 코어에 할당하여 병렬 처리가 가능해짐, 시스템 자원을 더욱 효율적으로 사용할 수 있음

 

멀티 쓰레딩의 장점 

- 프로세스 기반의 멀티태스킹보다 낮은 비용의 컨텍스트 스위칭 가능

-효율적인 메모리 사용, 빠른 데이터 공유

-I/O작업이나 대기 작업을 별도의 스레드로 처리하여 주 스레드가 차단되지 않고 사용자 입력이나 다른 중요한 작업에 빠르게 대응할 수 있게됨 

 

멀티 쓰레딩의 단점

- 스레드들이 같은 메모리를 공유하니 경쟁 상태, 교착 상태 같은 동기화 문제 발생 가능성이 있고, 해당 문제 해결 과정이 복잡하거나 디버깅이 어려워질 수 있음

- 스레드 관리를 소홀히 할 경우, 시스템 자원이 과하게 사용될 위험 존재 

 

 

https://www.maeil-mail.kr/question/236

'ComputerSicence > 운영체제' 카테고리의 다른 글

멀티프로세싱의 한계  (2) 2025.06.05

뭐든 회상에 잠기는거보다 직접 부딛히는게 좋음 

[정답코드]

from collections import deque
import copy
def solution(name):
    answer = float("inf")
    name = list(name)
    
    origin=['A']*len(name)
    
    now = 0
    
    controll=0
    controll+=min(ord(name[now])-ord('A'), ord('Z')-ord(name[now])+1)#min(ord(name[now])-65, 90-ord(name[now])+1)
    
    origin[now]=name[now]
    q=deque([])
    q.append((now, controll, origin))
    while q:
        now, controll, origin =q.popleft()
        if origin == name:
            answer=min(answer, controll)
            continue

        check=0
        rcontroll = controll
        rnow=now
        while check<1:
            # 오른쪽으로 가는 경우, 
            rnow=rnow+1
            if rnow>=len(name):
                rnow=0
            # 값을 바꿀 필요가 있는 경우 
            if origin[rnow]!=name[rnow]:
                if 0<=rnow<len(name):
                    check=1
                    rcontroll+=min(ord(name[rnow])-65, 90-ord(name[rnow])+1)+1

                    break
                    #q.append((nnow, controll+1, origin))
            # 값을 바꿀 필요가 없는 경우 
            else:
                rcontroll+=1

        check = 0
        lcontroll = controll
        lnow=now
        while check<1:
            # 왼쪽으로 가는 경우,     
            lnow = lnow -1
            if lnow<0:
                lnow=len(name)-1
            # 값을 바꿀 필요가 있는 경우
            if origin[lnow]!=name[lnow]:
                if 0<=lnow<len(name) :
                    check = 1  
                    lcontroll+=min(ord(name[lnow])-65, 90-ord(name[lnow])+1)+1
                    break
            else:
                lcontroll+=1

        originl=origin[:]
        originr=origin[:]
        originl[lnow]=name[lnow]
        q.append((lnow, lcontroll, originl))
        originr[rnow]=name[rnow]
        q.append((rnow, rcontroll, originr))
            
    
    return answer

 

 

일단 이 문제는 그리디 문제이긴 한데, 그리디 하지 않다. 

 

from collections import deque
import copy
def solution(name):
    answer = float("inf")
    name = list(name)
    origin=['A']*len(name)
    now = 0
    controll=0
    controll+=min(ord(name[now])-65, 90-ord(name[now])+1)
    origin[now]=name[now]
    q=deque([])
    q.append((now, controll, origin))
    while q:
        # print('!', now, controll, origin)
        now, controll, origin =q.popleft()
        
        if origin == name:
            
            answer=min(answer, controll)
            #print('answer', answer)
            continue

        check=0
        rcontroll = controll
        rnow=now
        while check<1:
            # 오른쪽으로 가는 경우, 
            rnow=rnow+1
            if rnow>=len(name):
                rnow=0
            
            # 값을 바꿀 필요가 있는 경우 
            if origin[rnow]!=name[rnow]:
                if 0<=rnow<len(name):
                    check=1
                    # print('now controll', controll, 'plus', min(ord(name[nnow])-65, 90-ord(name[nnow])))
                    rcontroll+=min(ord(name[rnow])-65, 90-ord(name[rnow])+1)+1
                    #origin[rnow]=name[rnow]
                    break
                    #q.append((nnow, controll+1, origin))
            # 값을 바꿀 필요가 없는 경우 
            else:
                rcontroll+=1

        check = 0
        lcontroll = controll
        lnow=now
        # print('lnow1',lnow)
        while check<1:
            # 왼쪽으로 가는 경우,     
            lnow = lnow -1
            if lnow<0:
                lnow=len(name)-1
            # 값을 바꿀 필요가 있는 경우
            if origin[lnow]!=name[lnow]:
                if 0<=lnow<len(name) :
                    check = 1
                    # print('now controll', controll, 'plus', min(ord(name[nnow])-65, 90-ord(name[nnow])))
                    lcontroll+=min(ord(name[lnow])-65, 90-ord(name[lnow])+1)+1
                    #origin[lnow]=name[lnow]
                    break
                    #q.append((nnow, controll+1, origin))
            else:
                lcontroll+=1
        # print('lnow2',lnow)
        #print('lcontroll, rcontroll:', lcontroll-controll, rcontroll-controll, ',', lcontroll, rcontroll,'controll', controll)      
        if lcontroll<rcontroll:
            #print('hi', origin)
            origin[lnow]=name[lnow]
            #print('hi2', origin, 'lcontroll',lcontroll)
            q.append((lnow, lcontroll, copy.deepcopy(origin)))
        elif lcontroll>rcontroll:
            #print('hi', origin)
            origin[rnow]=name[rnow]
            
            #print('hi2', origin, 'lcontroll',lcontroll)
            q.append((rnow, rcontroll, copy.deepcopy(origin)))
        else:
            originl=copy.deepcopy(origin)
            originr=copy.deepcopy(origin)
            originl[lnow]=name[lnow]
            q.append((lnow, lcontroll, copy.deepcopy(originl)))
            originr[rnow]=name[rnow]
            q.append((rnow, rcontroll, copy.deepcopy(originr)))
            
        
    # print(controll)
    
    
    return answer

 

 

이렇게 작성한 코드에서

이런식으로 도저히 넘어가질 않았음.

그런데 내가 하루종일 찾은 테스트케이스는 모두 통과되는 상태임.

 

이건 진짜 근본적인 문제가 하나 있다 싶었다. 

 

내가 이 문제를 풀기위해 사용한 핵심 로직은 

 

"지금 당장 더 싼 쪽(왼쪽 vs. 오른쪽)만 골라서 다른 쪽은 아예 버린다"
(만약 두쪽 다 값이 같으면 둘다 살려줌)

 

이였고, 오히려 그리디한 그리디 분기(pruning) 로직임.



그런데 이게 문제를 해결할 수 없는 결정적인 오류였음. 

이렇게 하면, 현재 상황에서 비용이 더 큰 쪽 경로를 전혀 탐색하지 않는데, 이 경우에서 만약 나중에 더 긴 A 구간이 나와버리면,  “한 번에 건너뛰어서” 전체 이동 횟수를 줄여 줄 수도 있는 상황을 전부 잘라 버림. 

결과적으로, 단편적인 부분에서 싸 보인다고 더 비싼 쪽 경로를 잘라 버리는 순간, 전역 최적해를 놓치게 되는거임;

 

결국 더 싼쪽 골라서 q에 넣기보다는 아예 저 조건 싹 지워버렸음


폭포수 시원하게 흐르는 것 같노

 

 

 

느낀점 : 그리디 문제도 깊게 생각할 부분이 있구나 하는것을 알게되었음.

'CodingTest > solved - Python' 카테고리의 다른 글

[백준11279] 최대 힙  (0) 2024.10.02
[백준14503] 로봇 청소기  (1) 2024.10.01
[백준1620] 나는야 포켓몬 마스터 이다솜  (0) 2024.09.09
[백준1003] 피보나치 함수  (0) 2024.09.09
[백준11723] 집합  (0) 2024.09.09

병목현상(Bottleneck)이란?

 

현실 세계에서 병목은 병의 목 부분이 좁아 액체가 병에서 천천히 나오는 것을 비유한 것이다. 병의 몸통은 넓어 많은 양의 액체를 담을 수 있지만, 목이 좁아져 한 번에 흐를 수 있는 양이 제한되는 것처럼 컴퓨터 시스템에서도 병목현상이 발생하면 처리 능력이 낮아지고 제한된다. 

 

컴퓨터 시스템에서 병목현상은 시스템의 처리 속도를 제한하거나 성능 저하를 유발하는 가장 느린 부분을 뜻한다. 

이 현상은 시스템의 여러 구성 요소 중 특정 부분이 다른 부분에 비해 처리 속도가 느려 전체 시스템의 성능에 부정적인 영향을 미칠 때 발생한다. 

 

 

병목현상의 예시

 

현실세계에서의 병목 현상

  • 고속도로에서 모든 차선이 잘 흐르다가 톨게이트에서 줄을 서서 기다리는 상황에서 줄이 늘어나는 톨게이트가 병목임.

서버에서의 병목현상:

  • DB 부하: 서버 요청이 많아져 DB 조회나 쓰기 작업이 느려지면, 전체 시스템 속도가 느려짐.
  • API 지연: 하나의 API가 처리 속도가 느리면, 다른 API 요청들도 대기하게 되어 응답 시간이 길어짐.

네트워크에서의 병목현상:

  • 데이터가 네트워크를 통해 전달될 때, 특정 라우터나 네트워크 장치의 처리 용량이 한계에 도달하면 병목이 발생함.

개발 프로세스에서의 병목현상:

  • 팀의 특정 작업이 완료되지 않아 다른 작업들이 지연되는 상황.
    • 예시: 백엔드 API 개발이 늦어져 프론트엔드 개발이 멈춰 있는 경우.

 

병목현상의 원인

 

하드웨어 문제:

  • CPU, 메모리, 디스크, 네트워크 등 자원의 제한
  • 예: DB 서버의 CPU가 과부하 상태일 경우

소프트웨어 문제:

  • 비효율적인 코드, 잘못된 알고리즘, 동시성 문제 등
  • 예: 불필요한 중복 요청이나 반복 계산

설계 문제:

  • 시스템 설계 단계에서 특정 구성 요소에 부하가 몰리도록 설계된 경우
    • 예시: 모든 요청이 한 곳으로 집중되는 단일 장애점(Single Point of Failure)

 

병목현상을 해결하는 방법

 

 

분석 및 모니터링:

  • 성능 테스트 도구(JMeter, Locust)를 사용하여 병목 구간을 찾아냄
  • 서버 로깅, APM(Application Performance Monitoring) 도구를 활용함

최적화:

  • 코드 개선: 비효율적인 로직을 최적화함
  • 데이터베이스 최적화: 인덱스 추가, 쿼리 개선 등
  • 캐싱 도입: 자주 사용하는 데이터를 캐시에 저장하여 DB 부하를 감소시킴

리소스 확장:

  • 더 좋은 서버로 업그레이드하거나 서버를 추가함
  • CDN(Content Delivery Network)이나 로드 밸런싱 활용

병목을 해결하면 시스템 전체의 성능이 대폭 향상됨

 

heapq의 원리:
 
heapq는 heapq연산을 해야만 힙의 구조가 깨지지 않는다. 
(만약, heapq를 직접 수정하면(인덱스를 통해 값 변경) 힙의 구조가 깨진다. )
 
heapq연산에는 heappush, heappop 등이 있다. 
 
heapq는 heapq연산을 수행하고 나서 자체적으로 정렬을 할 때, 남은 리스트 전체를 정렬하는 것이 아니라, 필요한 최소한의 연산만 한다. 
결과적으로 해당 heapq에서 최소값만을 맨 앞에 놓는다.
 
이런식으로 heapq는 항상 첫 번째 값이 최소값이 되도록 관리한다. 해당 과정은 이진 트리의 형태를 유지하면서, 최소한의 요소 이동만으로 이루어지기 때문에 효율적이다.

 
이것이 최소힙의 구조이다. 

 

heapq 사용예시:
 
import heapq

n, m = map(int, input().split())
tmp = list(map(int, input().split()))

heapq.heapify(tmp)

for i in range(m):
    # 최소값 두 개 꺼내기
    smallest1 = heapq.heappop(tmp)
    smallest2 = heapq.heappop(tmp)
    
    # 두 값을 합친 후 다시 삽입
    summ = smallest1 + smallest2
    heapq.heappush(tmp, summ)
    heapq.heappush(tmp, summ)
    
print(sum(tmp))

 

 

이를 이해하기 최소힙을 이해하기 좋은 백준 문제 

: 15903 카드 합체 놀이(https://www.acmicpc.net/problem/15903)

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

 

문제 해결 방법

파이썬에서는 힙 자료구조를 heapq 를 통해 제공한다. 이때 최소힙 기능만 사용할 수 있고, 최대힙 기능은 지원하지 않는다. 

최소값일 수록 우선순위를 가지고 맨 앞에 배치되니, 최대 힙을 사용하고 싶으면 값들을 넣을 때 -를 붙여 넣고, 출력할때는 -를 달아서 출력하면 최대힙 처럼 사용할 수 있다. 

 

코드 구현

 

정답 코드 

'''
# 최대 힙
'''
import heapq
import sys
input = sys.stdin.readline
n = int(input())

tmp = []
for i in range(n):
    value = int(input())
    if value !=0:
        heapq.heappush(tmp, -value)
    else:
        if not tmp:
            print(0)
        else:
            print(-heapq.heappop(tmp))

 

 

시간/공간 복잡도

log(n)

 

어려웠던 점

 

알게된 점

  • heapq 모듈 사용방법 

+ Recent posts