이전, DAY5 에서는 다음과 같은 내용까지 봤었다. CPU가 직접 계속 확인 Polling vs 장치에 일이 생기면 CPU에게 알림 Interrupt 오늘은, CPU 가 작업도중 인터럽트를 받았을때 무슨일이 일어나는지 알아보자. CPU 가 JAVA 프로그램을 실행하고 있던도중, 키보드 인터럽트가 발생했다. CPU 가 해당 이벤트를 처리하고나서, CPU 는 어떻게 원래 프로그램으로 정확하게 돌아오는걸까 ? CPU 가 다음 프로그램을 실행하고 있다고 하자. int a = 10; int b = 20; int c = a + b; System.out.println(c); 물론, CPU 가 JAVA 소스를 그대로 실행하는거는 아니고, 이전에 배웠었던 여러가지 과정들을 거쳐서, CPU 가 명령어를 실행한다. ( Bus, Fetch , Decode, Execute ... ) 단순화해서 생각해보면, CPU 는 아래와 같은 방식으로 실행하고있다고하자. 명령어 100 ↓ 명령어 104 ↓ 명령어 108 ↓ 명령어 112 ↓ ... 이에 따라, Program Counter(다음 실행할 명령어 주소) 도 계속해서 바뀔것이다. PC = 100 ↓ PC = 104 ↓ PC = 108 ↓ PC = 112 그런데, 108 근처를 실행하던 도중에 사용자가 키보드 입력을 했다고해보자. CPU 명령어 실행 ↓ 명령어 실행 ↓ 키보드 입력 발생 -> ─────── INTERRUPT! ─────── CPU 가, 이를 인지하고 해당 이벤트를 처리해야한다. 그런데, CPU 입장에서는 실행하고있던 즉, 원래 하던 작업은 어떻게 해놓고 가야할까? 만약, CPU 가 인터럽트를 처리하면서 기존 상태를 잊어버린다고 하자. 예를들어, CPU Register 가 아래와 같다고 하자. R1 = 10 R2 = 20 PC = 108 인터럽트를 처리하면서 Register를 사용해서, 해당 인터럽트를 처리했다. ( 이벤트 처리 ) 이후, CPU 는 무엇을 해야하느니 모르는 상태인것이다. 즉, CPU -> "어디까지 실행했었지?" 이렇게된다면, 프로그램은 정상적으로 이어갈 수 없다. 그렇기에, Interrupt 에는 매우 중요한 원칙이 있다. 원래 실행하던 프로그램의 상태를 보존했다가, 인터럽트 처리가 끝나면 복원해야만 한다. CPU 가 프로그램을 실행중이라고 하자. R1 = 10 R2 = 20 PC = 108 이러한 정보들은 프로그램을 계속해서 실행하는데에 매우 중요하다. 특히, PC ( Program Counter 는 다음에 어떤 명령어를 실행할 것인가와 매우 관련이있다. 다음과같은 CPU 의 실행 상태를 큰 의미에서 Context 라고 생각할수있다. Context R1 = 10 R2 = 20 PC = 108 즉, Context -> CPU 가 현재 작업을 계속하기 위해, 기억해야 하는 실행 상태. 다시, Interrupt 에 대해서 살펴보자. 프로그램 실행 ↓ Interrupt 발생 ↓ 현재 실행 상태 보존 ↓ Interrupt Handler 실행 ↓ Interrupt 처리 완료 ↓ 기존 상태 복원 ↓ 원래 프로그램 실행 계속 전체적인 흐름은 다음과 같다. CPU 가 평소처럼 Fetch -> Decode -> Execute 를 반복하면서, 명령어들을 처리하고있다. 예를들어, PC = 100 ↓ Fetch ↓ Execute PC = 104 ↓ Fetch ↓ Execute PC = 108 ↓ Fetch ↓ Execute 이러한 상황에서 키보드 입력이 발생했다고 하자. 키보드 장치가 Interrupt 를 요청한다. 사용자 ↓ Keyboard ↓ Controller ↓ Interrupt 요청 ↓ CPU CPU 에게, 처리해야 하는 이벤트가 발생했다고 알려주는것이다. CPU 가 이 요청을 받아들이면 원래 실행하고 있던 흐름에서, 이벤트 처리를 하기 위해 잠시동안 벗어나야 한다. 현재 CPU 의 상태가 다음과 같다고 하자. PC = 108 R1 = 10 R2 = 20 R3 = 30 Interrupt 처리가 끝난뒤에는, 원래 작업으로 돌아가려면 해당 상태를 기억하고 있어야만한다. 즉, CPU는 Interrupt 처리를 하기전에 해당 상태 ( Context) 를 기억하고있는다. CPU는, Interrupt 를 처리하기위해 Interrupt Handler 로 이동한다. Interrupt 가 발생했다고해서, CPU 가 알아서 키보드 입력 처리를 하는게아니라 Interrupt 종류에 따라 실행해야할 코드가 존재한다. ( 키보드 / 마우스 / ... ) 이를 일반적으로 Interrupt Handler 라고 한다. (인터럽트 처리 루틴 ) Interrupt 발생 ↓ 어떤 Interrupt인가? ↓ 해당 Handler 찾음 ↓ Handler 실행 이렇게해서, Interrupt 를 처리한다. 만약 키보드 입력이라면 ... Keyboard Interrupt ↓ Keyboard 관련 Handler 위와 같을것이다. Handler 를 실행한다. CPU 입장에서는, Handler 또한 결국 명령어의 집합인것이다. 그래서 뭔가 마법처럼 Interrupt 를 처리하는것이 아니라, 이전에 봤었던 흐름들이 계속해서 이어진다. CPU -> Handler Fetch ↓ Decode ↓ Execute 즉, Handler 의 명령어들을 가지고와서 실행한다. 필요한 작업들을 실행했고, Handler 를 통해서 명령어들을 가지고와서 해당하는 Interrupt 를 처리했다. 이제, CPU는 원래 진행하고있던 프로그램으로 돌아가야만 한다. 이때, 위에서 말했던 Context 를 활용한다. 저장된 상태 PC Register CPU 상태 Interrupt 를 처리하기전에, 상태 ( Context ) 를 저장한다고 했었다. 이제, Interrupt 를 다 처리했으니 해당 Context 를 가지고와서, CPU 에 다시 복원한다. 이후, CPU 는 복원된 Context 를 통해서 실행중이던 프로그램을 실행한다. 즉, 전체적인 흐름은 다음과 같다. Java 프로그램 실행 ↓ CPU 명령어 실행 PC = 100 PC = 104 PC = 108 ↓ !!! Interrupt !!! ↓ 현재 상태 저장 ( Context ) PC / Register / ... ↓ Interrupt Handler ↓ 처리 완료 ↓ 기존 상태 복원 ↓ 원래 프로그램 계속 실행 프로그램 입장에서는 잠시 멈췄다가 다시 실행되는것과 같다. Interrupt 가 발생했다고해서, CPU 가 즉시 멈추는걸까? CPU는 보통 현재 명령어 실행과 인터럽트 인식 시점 등의 규칙에 따라 안전한 경계에서 인터럽트를 처리한다. 즉, 즉시 멈추는것이아니라 판단하기에 적당한 시점에 Interrupt 를 처리한다. 명령어 실행 중 ↓ Interrupt 요청 인식 ↓ 현재 실행 흐름을 안전하게 중단할 수 있는 시점 ↓ Handler 이동 DAY5 에서 봤었던 Polling 방식과 Interrupt 방식을 다시한번 살펴보자. Polling 이라면 아래와 같이 실행된다. CPU ↓ Keyboard 확인 ↓ 없음 ↓ 프로그램 조금 실행 ↓ Keyboard 확인 ↓ 없음 ↓ 프로그램 조금 실행 ↓ Keyboard 확인 반면, Interrupt 라면 다음과 같이 실행된다. CPU 프로그램 실행 ↓ 프로그램 실행 ↓ 프로그램 실행 ↓ Keyboard │ │ Interrupt ↓ CPU 현재 상태 저장 ↓ Handler ↓ 상태 복원 ↓ 프로그램 계속 즉, CPU 가 장치를 계속 확인할 필요가 줄어든다. 예를들어, 다음과 같은 상황을 상상해보자. 나는 택배를 시켰고 택배는 언젠가는 온다는것을 알고있다. Polling 방식과 같다면, 내가 무언가를 하다가 ( 밥을 먹던가 / 샤워를 하던가 / 컴퓨터를 하던가 ... ) 택배가 왔는지 계속해서 문앞으로 나가서 확인해본다. 이는, 당연히 비효율적이다. 내가 하고있던 것들을 끊고 나가야하기도 하므로. 반면에, Interrupt 와 같은 방식을 생각해보자. 나는 그냥 내가 해야할것을 하고있는다. 그러다가, 벨이 울렸고 나는 문을 열어볼것이다. ( 벨이울린다면, 택배가 왔다는 상황이라고 가정. ) 이와 같은 방식은 계속해서 확인하는 Polling 방식에 비해 효율적이다. Timer Interrupt Interrupt 는 키보드나 SSD 와 같은 것들에서만 오는것이 아니다. 앞으로 운영체제를 배우기 위해서는 중요한 Interrupt 가 존재하는데, 이것이 Timer Interrupt 이다. 컴퓨터에는 일정 시간마다 CPU 에게 인터럽트를 발생시킬 수 있는 타이머가 존재한다. 예를들어, 아래와 같은 자바코드를 실행했다고 하자. while (true) { } 무한 루프에 빠진 상황이다. 이와같다면, 다른 명령어들은 실행되지 못할것이다. 위 코드에 있는 Context 가 다음과 같다고 하자. CPU PC = 100 R1 = 10 R2 = 20 해당하는 프로그램(Program A 라고하자.) 만 계속해서 실행중인 상황이다. Timer Interrupt 가 발생하고, 운영체제가 판단한다. Program A 그만하고, Program B 실행하자. 그러면, Program A 의 상태를 저장해야 한다. CPU PC = 100 R1 = 10 R2 = 20 이후, Program B 의 상태를 가지고온다 Program B Context PC Register ... 이제, CPU 는 위에있는 정보를 바탕으로 Program B 를 실행한다. CPU Program A ↓ 상태 저장 Program B ↓ 상태 복원 CPU ↓ Program B 실행 이것이, 다음에 운영체제쪽에서 배울 Context Switching 의 핵심이다. 오늘은, Interrupt 발생했을때, 처리하는 전체적인 과정을 봤다. CPU 가 다른 작업으로 갔다가 돌아오기위해서는, 진행중이던 상태를 저장해야 한다. 라는것이 중요하다. Interrupt 가 발생하면, CPU 는 원래 실행중이던 곳으로 돌아오기 위해 현재 실행중이던 상태를 저장해두고, Interrupt Handler 를 통해 해당 Interrupt 를 처리한다. 이후, 저장해뒀던 상태 ( Context) 를 통해 원래 실행중이던 프로그램으로 돌아올수있다. 이렇게, 기존 실행을 이어갈수있는것이다.
Excel 스프레드시트를 배포하거나 보관할 때 원본 편집이 가능한 형식보다 고정된 레이아웃의 문서가 필요한 경우가 있다. XPS(XML Paper Specification)는 Microsoft가 개발한 페이지 기술 언어로, 문서의 레이아웃과 각 페이지의 시각적 모양을 정의하는 고정 문서 형식이다. PDF와 유사한 용도로 사용되며, Windows 환경에서 기본적으로 지원된다. 이 글에서는 C#에서 Excel 파일을 XPS 형식으로 변환하는 방법을 살펴본다. Spire.XLS for .NET 라이브러리를 사용하며, 변환 과정과 고려 사항을 정리한다. XPS 형식의 특징 XPS는 XML 기반의 고정 문서 형식으로, 문서의 레이아웃과 인쇄 정보를 XML로 표현한다. Microsoft는 Windows 7부터 'Microsoft XPS Document Writer'를 기본 제공하여 XPS 파일을 생성할 수 있도록 지원하고 있다. Excel 데이터를 XPS로 변환하면 원본 스프레드시트의 서식, 차트, 이미지 등이 고정된 형태로 보존되므로, 다양한 환경에서 일관된 출력을 얻을 수 있다. 라이브러리 설치 이 글에서는 Spire.XLS for .NET을 사용한다. NuGet 패키지 관리자 또는 .NET CLI를 통해 설치할 수 있다. 패키지 관리자 콘솔: Install-Package Spire.XLS 설치가 완료되면 C# 코드에서 Spire.Xls 네임스페이스를 추가한다. 기본 변환 코드 Excel을 XPS로 변환하는 절차는 세 단계로 이루어진다. Workbook 객체를 생성한다. LoadFromFile() 메서드로 Excel 파일을 로드한다. SaveToFile() 메서드에 FileFormat.XPS 를 지정하여 저장한다. using Spire.Xls; namespace ExcelToXPS { class Program { static void Main(string[] args) { // Workbook 객체 생성 Workbook workbook = new Workbook(); // Excel 파일 로드 workbook.LoadFromFile(@"C:\path\to\sample.xlsx", ExcelVersion.Version2010); // XPS 형식으로 저장 workbook.SaveToFile(@"C:\path\to\result.xps", FileFormat.XPS); // 리소스 해제 workbook.Dispose(); } } } LoadFromFile 메서드의 두 번째 매개변수는 Excel 파일의 버전을 지정한다. ExcelVersion.Version2010 은 .xlsx 형식에 해당하며, 구버전 .xls 파일의 경우 다른 버전 상수를 사용할 수 있다. FileFormat 열거형 SaveToFile 메서드의 두 번째 매개변수로 전달되는 FileFormat 열거형에는 다양한 출력 형식이 정의되어 있다. XPS 형식은 FileFormat.XPS 로 지정한다. 동일한 코드 구조에서 출력 형식만 변경하여 다양한 변환 작업을 수행할 수 있다. 고려 사항 변환 코드를 실제 프로젝트에 적용할 때 검토해야 할 사항이 몇 가지 있다. 리소스 관리 : Workbook 객체는 IDisposable 을 구현하므로, 사용 후 Dispose() 를 호출하거나 using 문으로 감싸는 것이 권장된다. 위 예제에서는 명시적으로 Dispose() 를 호출했다. 파일 경로 : LoadFromFile 과 SaveToFile 메서드에 전달하는 경로는 절대 경로와 상대 경로 모두 사용 가능하다. 서버 환경에서는 절대 경로를 사용하는 것이 안전하다. Excel 버전 : LoadFromFile 메서드에서 지정하는 ExcelVersion 은 파일 형식과 일치해야 한다. .xlsx 파일에 ExcelVersion.Version97to2003 을 지정하면 오류가 발생할 수 있다. 폰트 및 레이아웃 : Excel에서 사용된 폰트가 시스템에 설치되어 있지 않으면 XPS 변환 시 레이아웃이 달라질 수 있다. 서버 환경에서 변환을 수행하는 경우 필요한 폰트가 설치되어 있는지 확인해야 한다. 마무리 C#에서 Excel을 XPS로 변환하는 작업은 Workbook 객체를 생성하고, 파일을 로드한 뒤, SaveToFile 메서드에 FileFormat.XPS 를 지정하는 것으로 완료된다. 코드 구조가 단순하고 변환 절차가 명확하므로, 배포용 문서 생성이나 아카이빙 자동화에 활용할 수 있다. 다만 실제 적용 전에 폰트 설치 여부와 파일 경로 처리 방식을 검토하는 것이 좋다.
개발 용어 탐구 10편 - AI 시대 개발 용어: LLM, RAG, AI Agent, MCP, Vibe Coding 들어가며 최근 개발 환경은 빠르게 변화하고 있다. 과거에는: 개발자 ↓ 직접 코드 작성 ↓ 테스트 ↓ 배포 과정이 중심이었다. 하지만 최근에는: 개발자 ↓ AI 활용 ↓ 코드 생성 ↓ 검토 및 개선 ↓ 배포 형태의 개발 방식이 점점 늘어나고 있다. 이 과정에서 새로운 개발 용어들이 등장했다. 대표적으로: LLM RAG AI Agent MCP Vibe Coding Prompt Engineering 등이 있다. 이번 글에서는 AI 시대 개발자가 알아야 할 핵심 용어를 정리한다. 1. LLM(Large Language Model) LLM은: 대규모 언어 모델(Large Language Model) 을 의미한다. 쉽게 말하면: "언어와 코드를 이해하고 생성하는 AI 모델" 이다. 대표적인 LLM: GPT 계열 Claude Gemini Llama 등이 있다. LLM은 학습한 데이터를 기반으로: 글 생성 질문 답변 코드 생성 문서 분석 등을 수행한다. 예: 개발자가: 로그인 기능 만들어줘 라고 요청하면: LLM이: Controller Service Database 처리 코드 같은 코드를 생성할 수 있다. 2. AI Coding Assistant AI Coding Assistant는: 개발 작업을 도와주는 AI 도구 를 의미한다. 대표 기능: 코드 자동 완성 코드 설명 오류 분석 리팩토링 제안 테스트 코드 생성 예: 개발자가: public class User { 작성하면: AI가: 필요한 메서드 생성자 Getter/Setter 등을 제안한다. 대표 도구: GitHub Copilot Claude Code ChatGPT 기반 개발 도구 3. Prompt Engineering 프롬프트 엔지니어링은: AI에게 원하는 결과를 얻기 위해 질문과 지시를 설계하는 기술 이다. 단순 요청: 회원 기능 만들어줘 좋은 요청: Spring Boot 기반으로 작성하고, Controller와 Service를 분리하며, JWT 인증 구조를 적용해줘. 차이: AI에게 필요한 정보를 많이 제공할수록 더 정확한 결과를 얻을 수 있다. 개발에서는: 요구사항 작성 코드 생성 디버깅 문서 작성 등에 활용된다. 4. Vibe Coding Vibe Coding은: AI와 대화하며 원하는 결과물을 만들어가는 개발 방식 을 의미한다. 기존 개발: 개발자 ↓ 직접 설계 ↓ 직접 코드 작성 ↓ 테스트 Vibe Coding: 개발자 ↓ AI에게 요구사항 설명 ↓ AI 코드 생성 ↓ 개발자가 검토 ↓ 수정 중요한 점: Vibe Coding은: "개발자가 코드를 전혀 몰라도 된다" 라는 의미는 아니다. 오히려 중요해지는 능력: 요구사항 정의 설계 능력 코드 검토 능력 문제 해결 능력 이다. 5. RAG(Retrieval-Augmented Generation) RAG는: 검색 기반 생성 방식 이다. LLM의 한계: AI는 학습하지 않은 최신 정보나 개인 데이터를 모를 수 있다. 예: 회사 내부 문서: 제품 설계 문서 API 문서 업무 규칙 이 있다고 하자. 일반 LLM: 모름 RAG 적용: 사용자 질문 ↓ 관련 문서 검색 ↓ 검색 결과를 LLM에게 전달 ↓ 답변 생성 구조: 사용자 ↓ 검색 시스템 ↓ 문서 데이터 ↓ LLM ↓ 답변 활용: 기업 내부 챗봇 문서 검색 고객 지원 개발 문서 분석 6. AI Agent AI Agent는: 목표를 받아 스스로 계획하고 여러 작업을 수행하는 AI 시스템 이다. 일반적인 LLM: 질문 ↓ 답변 Agent: 목표 입력 ↓ 계획 수립 ↓ 도구 사용 ↓ 결과 확인 ↓ 작업 완료 예: "서버 오류 해결해줘" 라는 요청. Agent: 로그 확인 ↓ 문제 분석 ↓ 코드 확인 ↓ 수정 제안 ↓ 테스트 같은 작업 흐름을 수행할 수 있다. 7. Tool Calling AI Agent에서 중요한 개념이다. Tool Calling은: AI가 외부 도구를 사용할 수 있도록 연결하는 방식 이다. 예: AI 자체: 답변 생성 만 가능. Tool 연결: AI ↓ DB 조회 ↓ 파일 읽기 ↓ API 호출 ↓ 결과 분석 가능. 개발 환경에서는: 코드 실행 Git 조작 문서 검색 서버 확인 등에 활용된다. 8. MCP(Model Context Protocol) MCP는: AI 모델과 외부 도구 및 데이터 사이의 연결 규칙 이다. 쉽게 말하면: "AI가 개발 환경을 이해하고 사용할 수 있게 하는 통신 방식" 이다. 예: AI에게: 내 프로젝트 구조 분석해줘 라고 요청. MCP 환경에서는: AI가: 파일 구조 확인 코드 읽기 함수 관계 분석 변경 제안 같은 작업을 수행할 수 있다. 9. AI 시대 개발자의 역할 변화 과거: 개발자 ↓ 코드 작성자 현재: 개발자 ↓ 설계자 검토자 문제 해결자 역할이 강조되고 있다. AI가 담당: 반복 코드 작성 기본 구현 문서 작성 보조 개발자가 담당: 시스템 설계 요구사항 분석 기술 선택 품질 판단 10. AI 시대에도 필요한 개발 기본기 AI 도구가 발전해도 기본 지식은 중요하다. 필요한 영역: 프로그래밍 언어 자료구조 알고리즘 운영체제 네트워크 데이터베이스 소프트웨어 설계 이유: AI가 만든 결과가: 올바른지 판단 성능 문제가 없는지 확인 구조적으로 좋은지 평가 해야 하기 때문이다. 11. AI 개발 흐름 현대적인 개발 흐름: 요구사항 분석 ↓ AI 활용 ↓ 코드 생성 ↓ 개발자 검토 ↓ 테스트 ↓ 배포 ↓ 운영 AI는 개발자를 대체하는 도구라기보다: 개발자의 생산성을 높이는 도구로 활용되고 있다. 마무리 AI 시대에는 개발 방식이 변화하고 있다. 정리하면: LLM = 코드와 언어를 이해하는 AI 모델 RAG = 외부 데이터를 검색해 답변 강화 AI Agent = 목표를 수행하는 AI 시스템 MCP = AI와 도구를 연결하는 규칙 Vibe Coding = AI와 협업하는 개발 방식 Prompt Engineering = AI 요청을 설계하는 기술 중요한 것은 AI 도구를 사용하는 능력뿐 아니라, AI가 만든 결과를 이해하고 검증할 수 있는 개발 기본기 이다.
SNS를 정리하면서 하나의 메시지를 여러 구독자에게 동시에 전달하는 팬아웃(fan-out) 구조를 다뤘는데 구독자가 여러 개로 늘어났을 때 그중 하나가 계속 메시지를 받지 못하는 상황은 어떻게 처리되는지는 짚지 않았다. 이번 글에서는 SNS 구독자 중 일부가 실패했을 때 발생하는 문제와 DLQ(Dead Letter Queue)가 왜 필요한지 정리한다. SNS는 구독자마다 독립적으로 메시지를 전달한다 SNS는 하나의 주제(Topic)에 메시지를 발행하면 그 주제를 구독하고 있는 모든 엔드포인트(Lambda, SQS, 이메일 등)로 각각 전달한다. 이때 구독자 각각에 대한 전달은 서로 독립적으로 이루어진다. 즉 구독자 A에게 전달이 실패했다고 해서 구독자 B, C로의 전달이 함께 실패하는 것은 아니다. Publisher ──▶ SNS Topic ──▶ 구독자 A (성공) ├─▶ 구독자 B (실패 → 재시도) └─▶ 구독자 C (성공) 실패한 구독자는 어떻게 되는가 전달에 실패한 구독자에 대해서는 SNS가 일정 정책에 따라 재시도를 수행한다. 하지만 재시도 횟수에는 한계가 있고, 그 한계를 넘어서도 계속 실패하는 경우 해당 메시지는 결국 폐기될 수 있다. 예를 들어 구독하고 있는 Lambda 함수에 일시적인 오류가 있거나 처리량 한도를 초과해 지속적으로 실패를 반환하는 상황이라면 그 메시지는 재시도가 끝난 뒤 아무 기록 없이 사라질 수 있다는 뜻이다. 저번에 정리했던 CloudWatch → SNS → Lambda → Slack 흐름을 생각해보면 만약 Slack 웹훅 호출을 담당하는 Lambda 함수가 일시적으로 오류를 내고 있었다면 그 사이에 발생한 알림 메시지들이 재시도 끝에 조용히 유실될 수 있었다는 것이 된다. 알림이 오지 않았다는 사실 자체도 알아차리기 어려운 상황이 되는 것이다. DLQ가 필요한 이유 이런 메시지 유실을 막기 위해 사용하는 것이 DLQ(Dead Letter Queue)다. 재시도가 모두 실패한 메시지를 그냥 폐기하는 대신에 별도로 지정한 큐(DLQ)로 보내서 보관하는 방식이다. SNS ──▶ 구독자(Lambda) ──재시도 모두 실패──▶ DLQ(SQS 큐)에 보관 └─▶ 이후 별도 확인·재처리 가능 DLQ에 쌓인 메시지를 통해 어떤 메시지가, 언제, 왜 전달에 실패했는지 사후에 확인할 수 있고 필요하다면 원인을 해결한 뒤 재처리할 수도 있다. 즉 DLQ는 실패를 막아주는 장치가 아니라 실패했다는 사실 자체를 조용히 사라지지 않게 남겨두는 장치 에 가깝다. 정리 SNS의 팬아웃 구조는 구독자마다 독립적으로 전달이 이루어지기 때문에 특정 구독자 하나의 장애가 전체 알림 흐름을 막지는 않는다. 다만 그 구독자로의 전달이 재시도 끝에 실패하면 메시지가 유실될 수 있고 알림 시스템에서는 이 유실 자체를 인지하기 어렵다는 문제가 있다. DLQ를 구성해두면 이런 실패 메시지를 남겨서 사후에 확인하고 대응할 수 있다.
개발 용어 탐구 6편 - 배포와 운영 용어: CI/CD, 빌드, 배포, 모니터링 들어가며 개발자가 코드를 작성했다고 해서 바로 사용자가 서비스를 이용할 수 있는 것은 아니다. 일반적인 서비스 개발 과정은: 코드 작성 ↓ 테스트 ↓ 빌드 ↓ 배포 ↓ 운영 ↓ 모니터링 과정을 거친다. 이 과정에서 등장하는 용어가: 빌드(Build) 배포(Deploy) CI/CD 파이프라인(Pipeline) 테스트 자동화 모니터링(Monitoring) 로그(Log) 장애 대응 이다. 이번 글에서는 실제 개발팀에서 자주 사용하는 운영 관련 용어를 알아본다. 1. 빌드(Build) 빌드는: 작성한 소스 코드를 실행 가능한 형태로 만드는 과정 이다. 쉽게 말하면: "프로그램을 실행할 수 있도록 준비하는 과정" 이다. 예: 개발자가 작성: Java 코드 Kotlin 코드 Python 코드 ↓ 빌드: 실행 가능한 프로그램 생성 빌드 과정에는 보통: 코드 컴파일 라이브러리 연결 파일 패키징 테스트 실행 등이 포함된다. 2. 컴파일(Compile)과 빌드(Build)의 차이 둘은 비슷하지만 다르다. 컴파일 소스 코드를 다른 형태의 코드로 변환하는 과정 예: Java 코드 ↓ Bytecode 빌드 실행 가능한 프로그램을 만들기 위한 전체 과정 포함: 컴파일 + 라이브러리 연결 + 테스트 + 패키징 관계: 빌드 └── 컴파일 포함 3. 배포(Deployment) 배포는: 완성된 프로그램을 실제 사용자가 사용할 수 있는 환경에 올리는 과정 이다. 예: 개발자의 컴퓨터: 완성된 서비스 ↓ 배포: 서버에 업로드 ↓ 사용자가 접속 가능 웹 서비스라면: 코드 ↓ 서버 ↓ 도메인 접속 ↓ 사용자 이용 과정이다. 4. 배포 방식 수동 배포 개발자가 직접 실행: 파일 이동 ↓ 서버 접속 ↓ 프로그램 실행 문제: 실수 가능 시간이 오래 걸림 반복 작업 어려움 자동 배포 도구가 자동으로 처리: 코드 변경 ↓ 자동 테스트 ↓ 자동 빌드 ↓ 자동 배포 현재 많은 기업이 자동화를 사용한다. 5. CI(Continuous Integration) CI는: 지속적 통합 이라는 의미이다. 개발자가 코드를 작성하면: 코드 작성 ↓ Git 저장소 업로드 ↓ 자동 테스트 ↓ 문제 확인 과정을 수행한다. 목적: 코드 오류 빠른 발견 여러 개발자의 코드 통합 품질 유지 예: 개발자 A: 로그인 기능 추가 개발자 B: 결제 기능 추가 CI가: 두 코드가 함께 동작하는지 확인 한다. 6. CD(Continuous Delivery / Deployment) CD는: 지속적 제공 또는 지속적 배포 를 의미한다. CI가: 코드 검사 라면, CD는: 검사 완료 ↓ 자동 배포 까지 담당한다. 전체 흐름: 개발자 코드 작성 ↓ Git Push ↓ CI 테스트 ↓ 빌드 ↓ CD 배포 ↓ 서비스 반영 7. CI/CD Pipeline 파이프라인은: 코드가 서비스로 전달되는 자동화 과정 이다. 예: Code ↓ Build ↓ Test ↓ Deploy ↓ Production 각 단계를 연결한 흐름을: CI/CD Pipeline 이라고 한다. 대표 도구: GitHub Actions Jenkins GitLab CI/CD CircleCI 8. 테스트 자동화(Test Automation) 테스트 자동화는: 사람이 직접 확인하지 않고 프로그램으로 테스트하는 것 이다. 예: 회원 가입 기능 테스트: 자동: 회원가입 요청 ↓ 결과 확인 ↓ 성공 여부 판단 장점: 반복 테스트 가능 오류 빠른 발견 배포 안정성 증가 9. 운영 환경(Production) 개발 환경에서는 정상 작동해도 실제 서비스에서는 문제가 발생할 수 있다. 그래서 환경을 구분한다. Development 환경 개발자가 작업하는 환경. 개발 PC 테스트 데이터 Test 환경 테스트 목적. Production 환경 실제 사용자가 사용하는 환경. 실제 서비스 실제 사용자 실제 데이터 현업에서는 Production을 줄여서: Prod 라고 많이 부른다. 10. 모니터링(Monitoring) 모니터링은: 서비스 상태를 지속적으로 확인하는 것 이다. 확인하는 정보: CPU 사용량 메모리 사용량 응답 시간 오류 발생률 트래픽 예: 사용자 증가 ↓ CPU 90% ↓ 서버 증설 필요 같은 판단을 한다. 11. 로그(Log) 로그는: 프로그램 실행 과정에서 남기는 기록 이다. 예: 로그: 2026-09-29 10:00 User Login Success User ID: 123 로그를 통해: 오류 원인 확인 사용자 행동 분석 장애 조사 를 한다. 12. 장애(Incident) 장애는: 서비스가 정상적으로 동작하지 않는 상황 이다. 예: 서버 다운 DB 연결 실패 응답 속도 저하 오류 증가 장애 발생: 사용자 영향 ↓ 원인 분석 ↓ 복구 ↓ 재발 방지 과정을 거친다. 13. 롤백(Rollback) 롤백은: 문제가 발생한 변경 사항을 이전 상태로 되돌리는 것 이다. 예: 새로운 버전 배포: Version 2 배포 ↓ 문제 발생: 로그인 오류 ↓ 롤백: Version 1 복구 14. 개발자와 운영의 관계 현대 개발에서는 개발자가 운영 과정도 이해해야 한다. 흐름: 개발 ↓ Git 관리 ↓ CI/CD ↓ 배포 ↓ 모니터링 ↓ 장애 대응 좋은 개발자는: "코드를 작성했다" 에서 끝나는 것이 아니라: "서비스가 안정적으로 운영된다" 까지 생각한다. 마무리 배포와 운영은 소프트웨어 개발 과정에서 매우 중요한 부분이다. 정리하면: Build = 실행 가능한 프로그램 생성 Deploy = 서비스 환경에 배치 CI = 코드 통합 자동화 CD = 배포 자동화 Pipeline = 자동화된 작업 흐름 Monitoring = 서비스 상태 확인 Log = 실행 기록 Rollback = 이전 버전 복구 현업 개발자는 코드를 작성하는 것뿐 아니라, 자신의 코드가 실제 서비스에서 어떻게 배포되고 운영되는지 이해해야 한다. 다음 글에서는 대규모 서비스를 만들 때 등장하는 시스템 설계 용어(모놀리식, MSA, 캐시, 로드밸런서, 메시지 큐) 를 알아본다.
신규주에서 가장 중요한 요소는 무엇일까? .펀더멘털 (이익) ? .업종 및 그 업종의 강세 ? .상장 당일 유통 시가 총액 ? 물론 중요하지만 같은 조건이라면 다른 요소가 크게 작용한다. 연속된 상장 일정으로 트랜드가 형성될 수 있다는 기대감 연속된 일정 속 기존 신규주의 공통된 흐름 (트랜드) 이라는 거시적인 관점 안에서 실전 매매 관점에서는 '시가 갭' 이다 유통시가 총액이 1000억 미만이면 비슷한거고 업황과 펀더는 의외로 크게 작용되지 않을 수 있다. 결국 기회가 있어야 되고, 그 기회는 상방 Room이고 그 것을 결정하는 것은 시초 가격이다. 9시 동시호가에서 무엇이 더 낮게 많은 물량이 몰리는지 보자. 두 종목 이상이라면 상호비교, 한 종목 이라면 절대치로 가늠하자
개발 용어 탐구 8편 - 개발 문화 용어: 리팩토링, 레거시, 기술 부채, 코드 리뷰, 애자일 들어가며 개발자는 단순히 코드를 작성하는 사람만을 의미하지 않는다. 실제 회사에서는: 기존 코드를 개선하고 팀원과 협업하고 프로젝트를 관리하고 지속적으로 서비스를 발전시키는 과정이 중요하다. 이 과정에서 자주 사용하는 용어들이 있다. 대표적으로: 리팩토링(Refactoring) 레거시(Legacy) 기술 부채(Technical Debt) 코드 리뷰(Code Review) 애자일(Agile) 스프린트(Sprint) 등이다. 이번 글에서는 개발 조직에서 실제로 사용하는 업무 용어를 알아본다. 1. 리팩토링(Refactoring) 리팩토링은: 프로그램의 동작은 유지하면서 코드 구조를 개선하는 작업 이다. 쉽게 말하면: "기능은 그대로 두고 코드를 더 좋게 만드는 것" 이다. 예: 처음 작성한 코드: 하나의 함수에 회원 조회 검증 DB 저장 메일 발송 모두 작성 문제: 코드가 길다 수정하기 어렵다 테스트하기 어렵다 리팩토링: 회원 조회 함수 검증 함수 메일 함수 분리 결과: 읽기 쉬움 유지보수 쉬움 오류 수정 쉬움 2. 리팩토링과 기능 개발의 차이 기능 개발: 새로운 기능 추가 예: 검색 기능 추가 리팩토링: 기존 기능 유지 코드 구조 개선 예: 검색 코드는 그대로 내부 구조 개선 중요한 점: 리팩토링은 사용자에게 보이는 기능 변화가 없다. 하지만 장기적으로 개발 속도를 높인다. 3. 레거시(Legacy) 레거시는: 오래된 시스템이나 코드를 의미하는 개발 용어 이다. 예: 10년 전 만든 서비스: 오래된 코드 낡은 기술 부족한 문서 이런 시스템을: 레거시 시스템 이라고 부른다. 4. 레거시는 무조건 나쁜 것인가? 아니다. 레거시는 상황에 따라 의미가 다르다. 좋은 의미: 오랫동안 운영된 안정적인 시스템 어려운 의미: 수정하기 어려운 오래된 코드 예: 은행 시스템: 20년 운영 검증 완료 중요 서비스 같은 경우도 있다. 5. 기술 부채(Technical Debt) 기술 부채는: 빠른 개발을 위해 나중에 해결해야 할 문제를 남겨두는 것 이다. 비유: 빚과 같다. 예: 빠르게 기능 출시: 일단 동작하게 개발 ↓ 출시 성공 하지만: 코드 중복 테스트 부족 복잡한 구조 가 남는다. 이것이 기술 부채이다. 6. 기술 부채가 발생하는 이유 1. 빠른 출시 시장 경쟁 때문에 빠르게 개발해야 하는 경우. 2. 요구사항 변경 처음 설계와 현재 요구가 달라지는 경우. 3. 유지보수 부족 시간이 지나면서 코드 품질이 떨어지는 경우. 7. 기술 부채 관리 좋은 개발팀은 기술 부채를 계속 관리한다. 방법: 리팩토링 테스트 추가 구조 개선 문서화 목표: 현재 개발 속도 + 미래 유지보수성 의 균형을 맞추는 것이다. 8. 코드 리뷰(Code Review) 코드 리뷰는: 다른 개발자가 작성한 코드를 검토하는 과정 이다. 흐름: 개발자 A 코드 작성 ↓ Pull Request 생성 ↓ 개발자 B 검토 ↓ 수정 ↓ 병합 검토 내용: 코드 오류 설계 문제 가독성 성능 보안 문제 9. 코드 리뷰의 목적 코드 리뷰는 단순히 잘못을 찾는 과정이 아니다. 목적: 코드 품질 향상 더 좋은 방법을 찾는다. 지식 공유 팀원들이 서로의 코드를 이해한다. 문제 예방 배포 전에 오류를 발견한다. 10. Pull Request(PR) PR은: 변경한 코드를 팀에 검토 요청하는 과정 이다. 예: 개발자가: 로그인 기능 개발 완료. ↓ GitHub에: Pull Request 생성 ↓ 팀원 리뷰. 현업에서: "PR 올렸습니다." "PR 리뷰 부탁드립니다." 같은 표현을 사용한다. 11. 애자일(Agile) 애자일은: 빠른 피드백과 반복적인 개선을 중요하게 생각하는 개발 방식 이다. 기존 방식: 폭포수(Waterfall): 기획 ↓ 설계 ↓ 개발 ↓ 테스트 ↓ 배포 애자일: 계획 ↓ 개발 ↓ 테스트 ↓ 피드백 ↓ 개선 (반복) 12. 스프린트(Sprint) 스프린트는: 애자일에서 일정 기간 동안 목표를 가지고 진행하는 개발 단위 이다. 예: 2주 스프린트: 목표: 로그인 기능 완성 기간: 2주 스프린트 동안: 개발 테스트 리뷰 를 진행한다. 13. 스크럼(Scrum) 스크럼은: 애자일을 실제 프로젝트 운영 방식으로 적용하는 방법 중 하나이다. 대표 활동: 데일리 스크럼(Daily Scrum) 매일 짧은 회의. 공유: 어제 한 일 오늘 할 일 문제점 스프린트 리뷰 완료한 기능 확인. 회고(Retrospective) 프로젝트 진행 방식을 개선. 14. 문서화(Documentation) 개발에서 문서화는 중요하다. 예: API 문서 설계 문서 장애 기록 개발 가이드 문서가 없으면: 작성자 퇴사 ↓ 새 개발자 이해 어려움 문제가 발생한다. 15. 개발팀에서 자주 사용하는 표현 현업에서는 이런 표현을 많이 사용한다. "리팩토링이 필요합니다." 의미: 현재 구조 개선 필요 "레거시 코드입니다." 의미: 오래되었거나 개선이 필요한 기존 코드 "기술 부채가 쌓였습니다." 의미: 빠른 개발 과정에서 해결하지 못한 문제가 증가 "PR 올렸습니다." 의미: 코드 검토 요청 마무리 개발자는 혼자 코드를 작성하는 직업이 아니다. 실제 개발 현장에서는: 기존 코드 개선 팀 협업 품질 관리 프로젝트 운영 이 중요하다. 정리하면: 리팩토링 = 코드 구조 개선 레거시 = 기존 오래된 시스템 기술 부채 = 나중에 해결해야 할 개발 비용 코드 리뷰 = 코드 검토 과정 PR = 변경 코드 검토 요청 애자일 = 반복 개선 방식 스프린트 = 짧은 개발 기간 단위 좋은 개발자는 단순히 코드를 작성하는 사람이 아니라, 팀과 함께 지속 가능한 소프트웨어를 만들어가는 사람 이다. 다음 글에서는 개발 성능과 확장성을 이해하기 위한 성능 관련 용어(병목, 캐시, 스케일 업, 스케일 아웃, 부하 테스트) 를 알아본다.
개발 용어 탐구 9편 - 성능과 확장성 용어: 병목, 캐시, 스케일 업, 스케일 아웃, 부하 테스트 들어가며 서비스가 성장하면 단순히 기능이 동작하는 것만으로는 부족하다. 사용자가 증가하면 개발자는 이런 질문을 고민해야 한다. 사용자가 10배 증가하면 버틸 수 있는가? 응답 속도가 느려지는 이유는 무엇인가? 서버를 어떻게 확장해야 하는가? 이때 등장하는 용어가: 병목(Bottleneck) 성능 최적화(Performance Optimization) 캐시(Cache) 스케일 업(Scale Up) 스케일 아웃(Scale Out) 부하 테스트(Load Test) 처리량(Throughput) 지연 시간(Latency) 이다. 이번 글에서는 실제 서비스 운영에서 자주 사용하는 성능 관련 용어를 알아본다. 1. 성능(Performance) 성능은: 시스템이 얼마나 빠르고 효율적으로 작업을 처리하는지 나타내는 기준 이다. 서비스 성능을 판단하는 요소: 응답 속도 처리 가능한 요청 수 자원 사용량 안정성 예: 사용자가 검색 버튼 클릭: 좋은 서비스: 요청 ↓ 0.1초 후 결과 느린 서비스: 요청 ↓ 5초 후 결과 2. 지연 시간(Latency) Latency는: 요청을 보낸 후 결과를 받을 때까지 걸리는 시간 이다. 예: 사용자가 API 요청: 10:00:00.000 요청 ↓ 응답: 10:00:00.200 응답 Latency: 0.2초 이다. 낮은 Latency: 빠른 응답 좋은 사용자 경험 과 연결된다. 3. 처리량(Throughput) 처리량은: 일정 시간 동안 처리할 수 있는 작업의 양 이다. 예: 서버 A: 초당 100개 요청 처리 서버 B: 초당 10000개 요청 처리 B가 처리량이 높다. 웹 서비스에서는 보통: TPS (Transaction Per Second) 초당 처리 요청 수 같은 지표를 사용한다. 4. 병목(Bottleneck) 병목은: 전체 시스템 성능을 제한하는 가장 느린 부분 이다. 비유: 도로가 넓다가 한 구간만 좁으면: 넓은 도로 ↓ 좁은 도로 ↓ 차량 정체 발생. 시스템에서도: 사용자 요청 ↓ 서버 ↓ DB 조회 (느림) ↓ 응답 지연 이면 DB가 병목이다. 5. 병목의 주요 원인 1. 데이터베이스 예: 비효율적인 SQL 인덱스 없음 느린 조회 2. 서버 처리 예: 복잡한 계산 CPU 과부하 3. 네트워크 예: 큰 파일 전송 느린 통신 6. 성능 최적화(Performance Optimization) 성능 최적화는: 시스템의 속도와 효율을 개선하는 작업 이다. 방법: 알고리즘 개선 DB 쿼리 최적화 캐시 사용 서버 구조 개선 예: 기존: 매번 DB 조회 ↓ 개선: 캐시 사용 7. 캐시(Cache) 캐시는 이전에도 나온 개념이지만 성능 관점에서 다시 살펴본다. 캐시는: 자주 사용하는 데이터를 빠른 저장 공간에 저장하는 기술 이다. 일반 흐름: 사용자 요청 ↓ DB 조회 ↓ 결과 반환 캐시 사용: 사용자 요청 ↓ Cache 확인 ↓ 있으면 바로 반환 ↓ 없으면 DB 조회 8. 캐시의 종류 클라이언트 캐시 사용자 브라우저에 저장. 예: 이미지 CSS JavaScript 서버 캐시 서버에서 관리. 대표: Redis Memcached CDN 캐시 전 세계 여러 서버에 데이터를 저장. 예: 이미지 영상 정적 파일 9. 스케일 업(Scale Up) 스케일 업은: 하나의 서버 성능을 높이는 방법 이다. 예: 기존 서버: CPU 4코어 메모리 16GB ↓ 업그레이드: CPU 32코어 메모리 128GB 장점: 구조 변경 적음 구현 쉬움 단점: 하드웨어 한계 존재 비용 증가 10. 스케일 아웃(Scale Out) 스케일 아웃은: 서버 개수를 늘리는 방법 이다. 기존: 사용자 ↓ 서버 1 확장: 사용자 ↓ 로드 밸런서 ↓ 서버 1 서버 2 서버 3 장점: 큰 트래픽 대응 가능 장애 대응 향상 단점: 구조 복잡 서버 관리 필요 11. 스케일 업 vs 스케일 아웃 구분 Scale Up Scale Out 방법 서버 성능 증가 서버 추가 구조 변경 적음 필요 한계 하드웨어 한계 확장 가능 관리 쉬움 어려움 예: 작은 서비스: Scale Up 으로 충분할 수 있다. 대규모 서비스: Scale Out 을 많이 사용한다. 12. 부하 테스트(Load Test) 부하 테스트는: 많은 사용자가 동시에 접근하는 상황을 시뮬레이션하는 테스트 이다. 목적: 서버가 얼마나 버티는가? 어디서 문제가 발생하는가? 확인. 예: 실제 사용자: 100명 테스트: 10000명 요청 생성 확인: 응답 시간 오류율 CPU 사용량 메모리 사용량 13. 스트레스 테스트(Stress Test) 스트레스 테스트는: 시스템의 한계를 확인하는 테스트 이다. 예: 점점 사용자 증가: 100명 ↓ 1000명 ↓ 10000명 ↓ 장애 발생 지점 확인 목적: "어디까지 버틸 수 있는가" 를 확인한다. 14. 성능 모니터링 운영 중에는 계속 확인해야 한다. 확인 항목: CPU Memory Disk Network Response Time Error Rate 문제가 발견되면: 측정 ↓ 원인 분석 ↓ 개선 ↓ 다시 측정 과정을 반복한다. 15. 개발자가 성능을 알아야 하는 이유 초급 개발자: 기능이 동작한다 에 집중. 경험 있는 개발자: 사용자가 많아지면? DB는 버틸까? 장애가 나면? 어떻게 확장할까? 까지 고려한다. 마무리 성능과 확장성은 서비스가 성장하기 위해 반드시 고려해야 하는 요소이다. 정리하면: Latency = 응답 시간 Throughput = 처리량 Bottleneck = 성능을 제한하는 지점 Cache = 빠른 데이터 저장 Scale Up = 서버 성능 증가 Scale Out = 서버 개수 증가 Load Test = 부하 상황 테스트 좋은 개발자는 단순히 기능을 만드는 것이 아니라, 많은 사용자가 안정적으로 사용할 수 있는 시스템을 만드는 것까지 고려한다. 다음 글에서는 개발자가 실제 회사에서 사용하는 AI 시대 개발 용어(LLM, RAG, AI Agent, MCP, Vibe Coding) 를 알아본다.
문제 괄호 회전하기 문제 설명 다음 규칙을 지키는 문자열을 올바른 괄호 문자열이라고 정의합니다. () , [] , {} 는 모두 올바른 괄호 문자열입니다. 만약 A 가 올바른 괄호 문자열이라면, (A) , [A] , {A} 도 올바른 괄호 문자열입니다. 예를 들어 [] 가 올바른 괄호 문자열이므로, ([]) 도 올바른 괄호 문자열입니다. 대괄호, 중괄호, 그리고 소괄호로 이루어진 문자열 s 가 매개변수로 주어집니다. 이 s 를 왼쪽으로 $x()$ 칸 만큼 회전시켰을 때 s 가 올바른 괄호 문자열이 되게 하는 x 의 개수를 return 하도록 solution 함수를 완성해주세요. 제한사항 s 의 길이는 1 이상 1,000 이하입니다. 입출력 예시 s result " {}" 3 "}]()[{" 2 "[)(]" 0 "}}}" 0 문제 풀이 " {}" 0 1 2 3 4 5 [ ] ( ) { } 6바퀴를 돌려야 함. (0 ~ 5) 1 2 3 4 5 0 ] ( ) { } [ i만큼 반복하고 j가 인덱스가 되어야 함. i + j i가 0일 때, 0, 1, 2, 3, 4, 5 i가 1일 때, 1, 2, 3, 4, 5, 6 (6 % 6 = 0) i가 2일 때, 2, 3, 4, 5, 6(6 % 6 = 0), 7(7 % 6 = 1) ... i가 5일 때, 5, 6(6 % 6 = 0), 7(7 % 6 = 1), 8(8 % 6 = 2), 9(9 % 6 = 3), 10(10 % 6 = 4) 초기 문제 풀이는 다음과 같이 했다. def solution(s): if len(s) == 1: return 0 answer = 0 length = len(s) for i in range(length): pairs = [0] * 3 for j in range(length): index = (i + j) % length if s[index] == '(': pairs[0] += 1 elif s[index] == ')': pairs[0] -= 1 elif s[index] == '{': pairs[1] += 1 elif s[index] == '}': pairs[1] -= 1 elif s[index] == '[': pairs[2] += 1 elif s[index] == ']': pairs[2] -= 1 if pairs[0] < 0 or pairs[1] < 0 or pairs[2] < 0: break if pairs[0] == 0 and pairs[1] == 0 and pairs[2] == 0: answer += 1 return answer 그러나 틀렸다. 왜 틀렸나 문제를 다시 보니 올바른 괄호는 열리고 바로 닫혀야 하는 것이다. {(}) 이런 거는 올바른 괄호로 보지 않는데 현재 로직에서는 올바른 괄호로 보고 있다. 스택을 활용해서 쌓고 빼고를 해야 할 것 같다. 그리고 {() 와 같은 홀수는 항상 짝이 맞지 않아 아무리 돌려도 올바르지 않은 괄호이다. 따라서 홀수이면 0을 조기 반환한다. if len(s) % 2 != 0: return 0 괄호들의 짝을 딕셔너리로 관리하여 스택에 넣고 빼도록 했다. pairs = {')': '(', '}': '{', ']': '['} 따라서 최종적으로 문제 풀이는 다음과 같다. def solution(s): # 홀수는 항상 올바르지 않는 괄호를 반환하므로 조기 반환 if len(s) % 2 != 0: return 0 answer = 0 length = len(s) # 괄호들의 짝 관리 딕셔너리 pairs = {')': '(', '}': '{', ']': '['} # 회전하기 위한 for 문 for i in range(length): stack = [] is_valid = True # 올바른 괄호인지를 확인하는 for문 for j in range(length): # 인덱스가 넘어가지 않도록 조정 char = s[(i + j) % length] # 여는 괄호라면 stack에 추가 if char in "({[": stack.append(char) # 닫는 괄호라면 else: # 만약 stack이 비어있거나 내 짝이 아닌 괄호라면 올바르지 않은 괄호이므로 for문 빠져나가기 if not stack or stack[-1] != pairs[char]: is_valid = False break # 위 if문을 통과했다면 올바른 괄호이므로 pop 수행 stack.pop() # 올바른 괄호 for문을 빠져 나와 올바른 괄호이면 결과 값에 +1 해주기 if is_valid and not stack: answer += 1 return answer 위 풀이를 리팩터링 해본 풀이 def is_valid(s): stack = [] pairs = {')': '(', '}': '{', ']': '['} for char in s: if char in '({[': stack.append(char) else: if not stack or stack[-1] != pairs[char]: return False stack.pop() return len(stack) == 0 def solution(s): if len(s) % 2 != 0: return 0 answer = 0 for i in range(len(s)): if is_valid(s[i:] + s[:i]): answer += 1 return answer 느낀점 우선 문제를 제대로, 잘 읽고 이해하는 것이 중요하다고 생각했다. 이전에 올바른 괄호 문제에 회전시키면 다라고 생각했는데 그게 아니었던 것이었다. 조기 반환의 경우도 2개씩 짝이 되는 것을 보고 짝수만 올바른 괄호가 되는구나라는 생각을 했어야 하는데 "하나면 안되네?" 라는 생각만 했다. 큰 차이는 아닐지 모르겠으나 메모리, 시간을 절약할 수 있는 조기 반환을 위해 조건을 생각해보는 연습을 해야겠다. 슬라이싱에 대해서도 좀 더 배운 것 같다. 문자열을 회전시킬 때는 항상 % 연산으로만 했었는데 슬라이싱이라는 것이 있다는 것을 보고 놀랐다. 앞으로는 자주 써먹지 않을까 싶다.