※ 출처 : https://www.nhncloud.com/kr/resource/edu-center 수강 목적 안녕하세요. 저는 클라우드·GPU·AI 인프라 다루는 일을 합니다. 업무를 하면서 클라우드 IaaS, PaaS, SaaS 등 개별 서비스의 스펙이나 특징들은 어느 정도 익숙하지만, 그것들이 늘 머리 속에서 조합되지 않고, 분산되어 흩어져있다는 갈증이 있었습니다. 서비스 하나하나는 알겠는데, 전체 시스템이나 엔드서비스 관점에서 어떻게 설계되어야 하고, 어떤 방식으로 작동되는지, 그 과정에서 발생하는 이슈&리스크와 그 해결방법을 알고 싶었습니다. 그러던 중 NHN Cloud 교육센터의 “NHN Cloud Training: Cloud Architecting” 강의를 알게 되었습니다. 오프라인에서 총 3일 동안 진행되는 교육이라서 여건이 되지 않아 매번 신청과 취소를 반복하다가 최근 유일하게 시간이 생겨서 수강하게 되었습니다. 결론 먼저 말씀드리면 단순히 자격증 취득 목적 뿐만 아니라 실무에서 바로 활용할 수 있는 지식과 스킬을 배울 수 있는 값진 강의였습니다. 교육 개요 교육명칭 : NHN Cloud Training: Cloud Architecting 교육레벨 : 중급(Intermediate) 교육장소 : 서울 서초구 서초대로74길 33(서초동, 비트빌딩) 4층 강의장 교육구성 : 개념이론 + 실습(총 12개 Lab) 혼합 강의 교육일정 : 총 3일간 1일차 : Organization & Project, Network, Compute 2일차 : Storage & Database, Enhanced the Architecture, Journey to Cloud Native 3일차 : Notification & Data Platform, AI Services & ML, Security, Infrastructure as Code, Suggestion for Cloud Architect 비고(링크) : https://edu.nhncloud.com/api/lecture/v1/lectures/eb61f19a-7b25-43c4-8f64-c5624261260d/link 배운 내용(개조식) ※ 본 내용은 교육 중 개인적으로 정리한 메모를 바탕으로 작성되었으며, 실제 강의 내용 및 NHN Cloud 제품 사양과 일부 다르거나 부정확할 수 있습니다. 정확한 내용은 NHN Cloud 공식 문서 또는 교육 자료를 참고해 주시기 바랍니다. 1일차 ※ 실습 시 리전(판교 or 평촌) 설정이 중요하므로 강사님 가이드를 유의하고 숙지해야 합니다. Organization & Project 조직: 최상위 논리 컨테이너, 비용 청구 단위, 만든 사람이 오너(최고 권한), 삭제 시 하위 리소스 전부 사라지고 복구 불가 계정: 클라우드 계정(회원가입/결제 주체) vs IAM 계정(오너/어드민이 발급, 무료, 실제 작업용). 실습은 클라우드 계정 1개 + IAM 계정 5개로 역할별 Task 수행 거버넌스: 특정 IP만 콘솔 접속 허용, 민감 서비스(Key Manager 등) 사용 승인 강제, 2차 인증 강제, 로그인 실패 잠금 등 조직 차원 정책 설정 가능 기타: 클라우드 트레일(활동 로그, 기본 90일 보관, 장기보관은 Object Storage 연동), 리소스 왓처(전체 리소스 통합 조회/태그 필터링) Network VPC: 논리적 가상 사설망. 1개는 무료, 추가 시 과금 라우팅: VPC 생성 시 기본 라우팅 테이블이 암시적 연결됨 — 인터넷/피어링 등 별도 경로 필요 시 명시적 연결로 분리(퍼블릭/프라이빗 분리의 핵심) 보안그룹 vs NACL: 보안그룹(인스턴스 레벨, Stateful, 화이트리스트만) vs NACL(서브넷/VPC 레벨, Stateless, 룰 개수 제한) Compute 인스턴스: 온프레미스 대비 스펙 변경이 콘솔 몇 번 클릭으로 즉시 가능 가용성 존(AZ): 존별 인스턴스 분산 배치 + 로드밸런서로 고가용성 구현 접근 통제: 공인 IP 직접 접속 지양, NHN Bastion 경유 원칙(IP 허용, 명령 통제, 세션 로그 6개월 보관, 웹터미널 지원) 2일차 Storage & Database 문제 확인: LB로 요청이 분산되면 인스턴스마다 DB/이미지가 달라 정상 동작 안 함(스토리지/DB 자체를 분리하는 설계가 필요) 스토리지: 블록 스토리지(인스턴스 마운트형, AZ 제약, 스냅샷 지원) / NAS(공유형, NFS+CIFS, 확장·축소 자유) / 오브젝트 스토리지(HTTP/REST 전용, 무제한 용량, 스탠다드/이코노미 클래스, 버전관리·라이프사이클 기능) 데이터베이스: 직접 설치/DB 인스턴스/RDS 구성 방식 Enhanced the Architecture Tier 분리: 웹/WAS/DB 계층 분리. 운영 환경에서는 보안 인증심사 대응과 계층별 독립 확장을 위해 필수 오토 스케일링: 최소/최대/구동 인스턴스 수 설정, 수평 확장(스케일 아웃) 방식, 증설은 지표 임계치 초과 시 동작(즉각 반응 아님), 감축은 오래된 인스턴스부터(FIFO) CDN & Cache: 클라이언트 앞단(CDN, 글로벌 서비스 필수) vs WAS-DB 사이(인메모리 캐시) Journey to Cloud Native 6R 전략: Rehost(1일차 방식) → Replatform(RDS/이지 캐시 등, 2일차 실습) → Refactor(클라우드 네이티브: MSA/컨테이너/CI-CD/DevOps) MSA: 기능별 독립 배포/장애 격리 장점, 단 조직 구조도 함께 바뀌어야 함(실무에서 자주 실패하는 지점) 관리형 쿠버네티스(NKS/NCS): 어려운 컨트롤 플레인은 NHN이 관리, 고객은 노드+컨테이너만 관리. 노드 그룹 오토스케일링 내장 3일차 ※ NHN Cloud 홈페이지에는 실무에 활용할 수 있는 참고용 아키텍처 레퍼런스와 템플릿이 있다고 합니다.( https://www.nhncloud.com/kr/resource/reference-architecture ) Notification & Data Platform Notification: 푸시/SMS·LMS·MMS/RCS/이메일/카카오 비즈메시지(알림톡·브랜드메시지) 통합 채널 Data 서비스군: Log & Crash Search(로그/크래시 모니터링), Data Flow(ETL, 멀티클라우드 동시 저장 가능), Data Query(Trino 기반, Object Storage+DB 조인 쿼리) AI Services & ML NHN AI 서비스: 인프라(GPU/스토리지) → 플랫폼(AI EasyMaker, Deep Learning Instance) → 응용 서비스(Face Recognition, OCR, Text-to-Speech, Speech-to-Text) Security 책임 공유 모델: NHN이 인증 취득했다고 고객 서비스가 자동으로 인증받는 게 아님(고객도 별도 심사 필요) 서비스 분류: 취약점 점검(Server Security Check), 시스템 보안(Web Shell Detector 등), 보안 관리(Security Compliance, SIEM, Security Advisor), 접근 제어(Cloud Access/SSL VPN), 암호화(Secure Key Manager), 보안 관제(Basic/Security Monitoring), 네트워크 보안(DDoS Guard, WAF, Network Firewall/Hub-and-Spoke 구조) Infrastructure as Code IaC 개념: 반복 자동화·재사용·버전관리·휴먼에러 감소가 핵심 가치 Terraform: 선언적 코드, Plan(사전검토)→Apply(실제 생성) 절차가 핵심(실제로 비용 발생하는 리소스가 생성됨에 유의) Suggestion for Cloud Architect 실무 적용 이번 교육에서 배운 내용 중에서 직접적인 Hands-on 경험이 없었던 스토리지·데이터베이스·오토스케일 서비스와 아키텍처 스킬을 실무에 적용했습니다. 기존에는 웹/WAS 서버 로컬 디스크에 이미지를 저장하던 구조를, 교육에서 배운 대로 Object Storage로 분리해 여러 인스턴스가 동일한 원본 데이터를 공유하도록 개선했습니다. 데이터베이스는 단일 인스턴스로 운영하던 부분을 RDS 고가용성(HA) 구성으로 전환해, 장애 시 예비 마스터로 자동 전환되도록 하여 다운타임 리스크를 줄였습니다. 또한 트래픽이 몰리는 구간을 대비해 세션을 인스턴스에 두지 않고 Easy Cache에 저장하는 Stateless 구조로 바꿔, 오토스케일링 시 세션 유지 문제를 해결했습니다. 오토스케일 정책은 교육에서 강조한 대로 CPU 단일 지표가 아닌 CPU·메모리 두 가지 지표를 함께 사용하도록 설정했습니다. 정적 콘텐츠는 CDN을 적용해 사용자 체감 속도를 개선하고 원본 서버 부하를 줄였습니다. 이러한 개선을 통해 특정 서버 장애나 트래픽 급증 상황에서도 서비스 연속성을 유지할 수 있는 구조를 설계하고 컨설팅할 수 있는 기술을 배우게 되어서 3일 동안 시간일 투자할 만한 실용적이고 유익한 강의였습니다. 수강 소감 "NHN Cloud Training: Cloud Architecting" 교육은 클라우드 사용자에서 설계자 레벨로 업그레이드 되고 싶은 분들에게 상당히 좋은 기회라고 생각합니다. 조직 설계에서 시작해 네트워크·컴퓨트·스토리지·오토스케일·컨테이너·AI·보안·IaC까지, 아키텍트가 알아야 할 지도를 3일 만에 직접 Hands-on으로 테스트하고 스킬로 익힐 수 있었던 가치 있는 교육이었습니다. 무엇보다 이런 교육을 무료로 제공되는 점도 놀랐습니다. 그리고 동종업계에 계신 분들을 만날 수 있는 기회도 더불어 있었습니다. 자격증 공부는 별도로 시간과 노력이 필요하지만, 우선 교육에서 배운 것들을 실무에 바로 활용할 수 있을 것 같아 더 심화 교육도 있다고 해서 추후 시간내서 듣고 싶어졌습니다. ※ 출처 : https://www.nhncloud.com/kr/resource/edu-center/certification/essential 참고 사항(Q&A) Q1. 교육 비용 A1. 무료 교육이었고, 실습 할 때 발생하는 클라우드 서비스 이용료는 교육 Credit이 제공되어서 교육 가이드 내용대로만 수행한다면 추가 비용은 발생하지 않는 것 같습니다. Q2. 교육 방식(이론 중심인지, 실습 중심인지?) A2. Cloud Architecting Intermediate 레벨 교육은 챕터별 이론과 실습이 순차적으로 번갈아 진행되어서 이론에서 익힌 지식을 바로 이어서 체험하는 방식으로 진행되었습니다. 실습의 경우 먼저 "문제 있는 구조"(웹/WAS/DB 통합 서버)를 만들고, 2일차에 개선, 3일차에 DR·보조 리전까지 확장하는 흐름으로 3일간 진행되었습니다. Q3. 실습 난이도 A3. 교육 교재에 상세한 가이드가 있어 내용 그대로 이행하면 NHN Cloud 서비스를 아예 처음 접하시는 분들도 쉽게 따라 할 수 있었습니다. 다만, 가이드 내용을 꼼꼼히 읽지 않고 중간에 놓치거나 잘못 수행하면 바로 잡는데 애를 먹을 수 있습니다. 이 경우에는 실습을 도와주시는 기술지원 강사님께 도움을 요청하시면 됩니다. Q4. 교육 수강 전 준비사항 A4. Essentials 교육 수강자/자격증을 사전에 취득하거나 교육 목차별 NHN Cloud 서비스를 사전에 익혀둔다면 교육 내용과 실습을 진행하는데 훨씬 수월할 것 같습니다. Q5. 자격증 취득 지원 A5. 이론 교육에서 예시 문항을 미리 경험해 볼 수 있어 자격증 취득에 도움이 되고, 중간에 퀴즈 시험 우수자에게는 할인권을 제공해주는 것 같습니다.
실시간 알림 기능을 구현한 뒤, 동시 접속 상황에서 어느 정도까지 안정적으로 동작하는지 확인하기 위해 부하테스트를 진행했습니다. 테스트를 진행하면서 Grafana로 CPU 사용률, 메모리 사용량, 응답 시간을 함께 확인했습니다. 시나리오를 여러 차례 반복하자 메모리 사용량은 계속 증가했고, CPU 사용률과 p99 응답 시간도 함께 올라갔습니다. 처음에는 단순히 부하가 많이 걸린 영향이라고 생각했습니다. 하지만 테스트가 끝난 뒤에도 메모리 사용량은 이전 수준으로 돌아오지 않았고, 같은 시나리오를 반복할수록 CPU와 p99가 이전보다 더 자주 튀었습니다. 또한 테스트 이후 애플리케이션 프로세스를 재시작하자 지표들이 다시 정상 수준으로 돌아왔습니다. 이러한 증상을 바탕으로 몇 가지 가능성을 먼저 확인했습니다. 1. 스레드 풀 고갈 의심 → 가능성 낮음 처음에는 동시 요청이 많아지면서 스레드 풀이 고갈되고, 대기 요청이 증가해 p99 응답 시간이 상승한 것이 아닐까 생각했습니다. 하지만 단순한 스레드 풀 고갈이라면 부하가 종료된 뒤 대기 중인 작업이 처리되면서 응답 시간도 다시 정상 수준으로 회복되는 흐름이 나타나야 합니다. 또, 테스트가 끝난 이후에도 메모리 사용량이 이전 수준으로 돌아오지 않았고, 테스트를 반복할수록 상태가 계속 악화됐습니다. 따라서 단순한 스레드 풀 고갈만으로는 설명하기 어려웠습니다. 2. 호스트 수준 문제 의심 → 가능성 낮음 다음으로 애플리케이션이 실행되는 인스턴스의 CPU, 메모리, 네트워크, 디스크 I/O 등의 자원 문제를 확인했습니다. Grafana에서 호스트 수준의 시스템 지표를 확인했지만 지속적인 CPU 포화나 메모리 부족, 네트워크 병목과 같은 문제는 보이지 않았습니다. 반면 애플리케이션 프로세스를 재시작하면 문제가 사라졌기 때문에, 호스트 자체보다는 애플리케이션 내부 문제일 가능성이 높다고 생각했습니다. 3. GC로 인한 Stop-the-World 문제 의심 Grafana에서는 당시 Heap 세부 지표를 충분히 수집하고 있지 않았습니다. 하지만 부하 테스트를 반복할수록 CPU와 p99가 함께 악화됐고, 메모리 사용량 역시 이전 수준으로 돌아오지 않았습니다. 애플리케이션 프로세스를 재시작하면 정상화되는 점을 보고, 애플리케이션 내부에 문제가 있음을 짐작했고, 특히 메모리가 계속 누적되는 상황에서 GC 가 더 자주 발생하거나 Stop-the-World 시간이 길어지면서 CPU 사용률과 p99 응답 시간에도 영향을 주는 것은 아닌지 의심했습니다. 다만 이 시점의 Grafana 지표만으로 GC 문제라고 확정할 수는 없었습니다. 그래서 GC 문제를 유발할 만한 코드가 있는지 확인하기 위해 AI 코드 리뷰도 함께 진행했습니다. SSE 연결 관리 코드가 의심됐다 리뷰 과정에서 실시간 알림에 사용하던 SSE 연결 관리 코드가 의심 지점으로 나왔습니다. SSE 연결을 생성하면 이후 알림 전송을 위해 SseEmitter 를 애플리케이션 메모리에 유지하고 있었습니다. private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>(); 문제는 SSE 연결이 종료된 이후에도 해당 연결과 관련된 상태가 메모리에서 정리되지 않고 남을 수 있다는 점이었습니다. 이미 사용이 끝난 연결 정보가 계속 참조된 상태로 유지되면 GC가 이를 회수할 수 없고, 연결이 반복될수록 관련 객체가 Heap에 계속 누적될 수 있습니다. SSE 연결 생성 ↓ 연결 상태를 애플리케이션 메모리에 유지 ↓ 클라이언트 연결 종료 ↓ 연결 상태가 정리되지 않고 남음 ↓ 객체에 대한 참조 유지 ↓ GC가 회수할 수 없음 하지만 코드만 보고 이것을 실제 원인이라고 단정할 수는 없었습니다. 그래서 로컬 환경에서 SSE 연결을 반복적으로 생성하고 종료하면서, 연결이 끝난 뒤에도 관련 상태가 실제로 남는지를 중심으로 재현 테스트를 진행했습니다. SSE 재접속으로 문제를 재현했다 로컬 환경에 애플리케이션 두 대를 띄우고, 일반 API 요청은 일정하게 유지한 채 SSE 재접속을 반복했습니다. 여기서 Heap 메모리는 애플리케이션당 320MiB 로 제한했습니다. 테스트에서는 실제 활성 SSE 연결 수와, 연결 종료 후에도 애플리케이션 내부에 남아 있는 연결 상태를 따로 확인했습니다. 정상적으로 정리된다면 연결이 종료된 뒤 두 값 모두 다시 감소해야 합니다. 하지만 재접속을 반복할수록 저장된 연결 상태는 계속 증가했고, 새로운 연결 생성을 중단해 활성 연결이 0 이 된 뒤에도 그대로 남아 있었습니다. 왼쪽 그래프를 보면 SSE 재접속을 중단한 뒤 활성 연결은 0 으로 돌아왔지만, 애플리케이션 내부에 저장된 연결 상태는 그대로 남아 있었습니다. 같은 시간대의 오른쪽 그래프에서는 Heap과 Old 영역의 사용량도 함께 증가하는 모습을 확인할 수 있었습니다. 여러 번의 GC를 거쳐 살아남은 객체는 Old 영역으로 승격됩니다. 따라서 연결은 이미 종료됐는데 Old 영역의 사용량이 계속 증가한다는 점에서, 종료된 연결과 관련된 객체가 제대로 정리되지 않고 계속 살아남고 있을 가능성을 의심했습니다. Full GC가 반복적으로 발생했다 다음으로 JVM 내부 상태를 더 자세히 확인하기 위해 VisualVM 을 사용했습니다. VisualVM에서 Heap 사용량과 GC activity 를 확인해보니, 테스트가 진행될수록 GC 이후에도 Heap 사용량이 이전 수준까지 내려오지 않고 점점 높아지는 모습을 확인할 수 있었습니다. 단순히 객체 생성량이 많아서 Heap 사용량이 증가한 것인지, 아니면 GC 이후에도 객체가 계속 살아남고 있는 것인지 확인할 필요가 있었습니다. 앞서 Grafana에서 Old 영역 사용량도 함께 증가하는 모습을 확인했기 때문에, GC가 발생하는 동안 어떤 Pause가 발생하고 있는지도 확인했습니다. 그래프를 보면 Young GC보다 Full GC가 발생한 구간에서 더 길게 정지한 것을 알 수 있습니다. 다만 이 그래프는 일정 구간에서 가장 길었던 GC 정지 시간을 보여주기 때문에, 선이 유지되는 시간을 하나의 GC 실행 시간으로 볼 수는 없습니다. 개별 Full GC에서 실제로 얼마나 오래 멈췄는지와 GC 전후 Heap 사용량은 GC 로그에서 확인했습니다. Pause Full (G1 Compaction Pause) 318M->260M(320M) 327.310ms Pause Full (G1 Compaction Pause) 318M->286M(320M) 291.926ms Pause Full (G1 Compaction Pause) 318M->291M(320M) 223.679ms 로그에서도 Full GC가 반복적으로 발생하고 있었고, GC 이후에도 Heap 사용량이 충분히 줄어들지 않는 모습을 확인했습니다. 이는 일부 객체가 계속 참조되고 있어 GC가 회수하지 못하고 있다는 것을 의미합니다. 그래서 다음으로는 부하가 끝난 뒤에도 이 상태가 계속 유지되는지 확인했습니다. 연결이 모두 종료된 뒤에도 Heap이 회수되지 않았다 SSE 재접속을 중단하고 활성 연결이 모두 종료될 때까지 기다렸습니다. 그 상태에서 Full GC를 여러 번 수행하면서 Heap 사용량이 얼마나 줄어드는지 확인했습니다. Pause Full (Diagnostic Command) 293M->269M(320M) 250.672ms Pause Full (Diagnostic Command) 269M->269M(320M) 237.300ms Pause Full (Diagnostic Command) 270M->269M(320M) 197.316ms Pause Full (Diagnostic Command) 270M->269M(320M) 197.966ms Pause Full (Diagnostic Command) 269M->269M(320M) 196.685ms 첫 번째 Full GC에서는 일부 메모리가 회수됐습니다. 하지만 이후 네 번의 Full GC에서는 Heap이 약 270MiB 수준에서 더 이상 내려가지 않았습니다. 두 애플리케이션 모두 비슷한 결과를 보였습니다. 애플리케이션 Full GC 이후 사용 Heap app1 약 270MiB app2 약 269MiB 이 시점에는 실제 SSE 활성 연결이 없는 상태였습니다. 그런데 Full GC를 여러 번 수행해도 Heap은 약 270MiB 수준을 유지했습니다. 즉, GC 자체는 실행되고 있었지만 일부 객체는 정리되지 않고 있음을 의미합니다. 따라서 단순한 GC 실행 여부의 문제가 아니라, 애플리케이션 어딘가에서 객체에 대한 참조가 계속 유지되고 있을 가능성이 있다고 생각했습니다. Heap Dump로 남아 있는 객체를 확인하다 Full GC 이후에도 Heap 사용량이 충분히 줄어들지 않았기 때문에 어떤 객체가 계속 살아남아 있는지 확인하기 위해 VisualVM으로 Heap Dump를 분석했습니다. 확인 결과, 종료된 SSE 연결과 관련된 객체들이 메모리에 그대로 남아 있었습니다. 특히 Heap Dump에서 확인한 SSE 연결 관련 객체 수가, 앞서 연결 종료 후에도 남아 있던 연결 상태 수와 거의 동일하게 나타났습니다. 즉, SSE 연결이 종료된 뒤에도 관련 상태가 애플리케이션 내부에 남아 있었고, 이 상태가 연결과 관련된 다른 객체들까지 계속 참조하고 있었습니다. 그 결과 해당 객체들은 더 이상 사용되지 않는데도 GC에 의해 회수되지 못한 채 Heap에 남아 있었습니다. 이를 통해 문제의 원인이 단순히 Heap 크기나 GC 설정 때문이 아니라, 종료된 SSE 연결과 관련된 상태가 계속 참조되면서 발생한 메모리 누수 라는 것을 확인했습니다. 연결 종료 시 참조도 함께 제거하다 원인은 SSE 연결이 종료된 뒤에도 관련 객체에 대한 참조가 메모리에 남아 있던 것이었습니다. 따라서 연결이 종료되는 시점에 저장해둔 SseEmitter 에 대한 참조도 함께 제거하도록 수정했습니다. emitterRepository.delete(userId); 핵심은 연결 자체가 종료되는 것뿐 아니라, 애플리케이션이 해당 연결에 대해 유지하고 있던 참조도 함께 정리하는 것이었습니다. 수정 후 같은 방식으로 SSE 재접속 테스트를 다시 진행했습니다. 이번에는 연결이 종료되자 애플리케이션 내부에 저장돼 있던 연결 상태도 함께 정리됐고, 활성 연결과 저장된 연결 상태 모두 0 으로 돌아왔습니다. Full GC 이후 Heap 사용량도 이전보다 크게 낮아졌습니다. 항목 app1 app2 활성 연결 0 0 저장된 연결 상태 0 0 Full GC 이후 Heap 약 57MiB 약 68MiB 수정 후에는 연결이 종료되면 관련 상태도 함께 정리됐고, Full GC 이후 Heap 사용량도 이전보다 크게 감소했습니다. 이를 통해 종료된 SSE 연결에 대한 참조가 계속 유지되던 것이 메모리 누수의 원인이었다는 것을 확인할 수 있었습니다. 마치며 이번 경험을 통해 여러 가능성을 의심한 뒤 재현 테스트와 JVM 분석 등을 통해 의심점을 좁혀가며 결론을 도출해내는 경험을 할 수 있었습니다. GC와 JVM 메모리 동작을 실제 문제와 연결해서 이해할 수 있었고, VisualVM과 Heap Dump를 활용해 원인을 추적하는 방법도 익혔습니다. GC가 개발자의 메모리 관리 부담을 줄여주지만, 그렇다고 메모리를 완전히 신경 쓰지 않아도 되는 것은 아니라는 점도 느꼈습니다. 무엇보다 문제를 재현하고 가설을 하나씩 좁혀가며 해결하는 과정을 직접 경험했다는 점이 의미 있었습니다.
현재 공시 AI 요약은 당시 무료 api중 가장 괜찮다고 판단했던 Gemini 2.5 Flash를 사용중이었다. 다만 시간이 오래 지나 신버전이 풀렸고, 지난 세션에 최신인 3.8 Flash를 재봤다가 떨어뜨렸는데 글을 쓰다가 그 판정 기준이 2.5의 출력을 정답지로 삼은 것이었음을 알았다. 그래서 이번엔 기준을 공시 원문에서 뽑아 다시 재기로 했다. 계획은 프롬프트 v2를 먼저 넣고 세 모델을 시간대 세 차례에 걸쳐 판정하는 것이었다(F108). 결과는 두 번째서 끝났다. 예상대로 3.5 Flash-Lite는 1차부터 탈락했고, 2차 채점에서 3.8이 2.5를 앞서 3.8을 기본 모델로 올리되 2.5를 fallback으로 남기는 구조로 닫았다. 시작 전에 Gemini 밖 무료 API도 한 번 훑었는데, 조건에 맞는 후보는 없었다. Gemini 밖 무료 API 조사 2.5 Flash의 무료 한도는 하루 20콜이라 리뷰어 한 명이 소진할 수 있는 수치다. 포트폴리오 서비스인만큼 감수하기로 했지만, Gemini 안에서만 고르기 전에 다른 무료 API에 이 문제를 풀 후보가 있는지 확인했다. 조건은 카드 등록 없음, 하루 100콜 이상, JSON 스키마 구조화 출력, 공시 원문 길이의 한국어 입력, Vercel에서 HTTP로 호출 가능, 다섯 개였다. 후보 판정 OpenAI 기각. 데이터 공유 조건의 무료 토큰도 결제 잔액이 있어야 함 Groq · Cerebras 기각. 요청 수는 넉넉하나 분당 토큰·컨텍스트 상한(8K)이 공시 원문 1건에 걸림 OpenRouter free · Cohere 기각. 하루 50콜 · 월 1,000콜 CLOVA Studio 기각. 종량제, 무료 티어 없음 Mistral 무료 플랜 보류. 한도는 충분, 한국어 품질 미확인, 입력이 학습에 쓰임 Upstage Solar Pro 4 보류. 한국어·스키마·컨텍스트 전부 맞으나 무료가 아님(가입 크레딧 3개월 뒤 건당 약 $0.001) NVIDIA NIM 보류. 하루 1만 콜, "trial use only" 약관 다섯 조건을 다 맞추는 후보는 없었다. 근접한 셋도 성능 때문에 붙일 이유는 없고 한도와 장애의 대안일 뿐이라, 어댑터는 Gemini 무료 티어가 깨질 때의 보험으로 백로그에 두었다(F109). 한도 100콜 이상으로 필터링 한 건 현행에서 교체하려면 5배정도 이득은 있어야 한다고 생각해서다. 조사하면서 확인한 것 하나가 이후 계획을 정했는데, Gemini의 무료 일일 한도가 미국 태평양 자정, 한국 시각 16시에 리셋된다는 점이다. 원문 기준 채점 설계 지난 세션의 실수는 기준을 지금 쓰는 모델의 출력에서 뽑은 것이었다. 이번엔 공시 8건(공급계약·전환사채·최대주주 변동·대량보유·주총 소집 2건·감사보고서·거래정지)을 골라, 모델을 한 번도 돌리기 전에 원문만 보고 "독자가 알아야 할 항목"을 건별로 뽑아 고정했다. 채점은 출력에 A·B 같은 라벨만 붙이고 어느 모델인지 가린 채로 했고, 채점이 끝난 뒤에 라벨을 풀었다. 프롬프트는 세 모델 모두 같은 v2를 받았다. 규칙을 한 곳에 모으고 금지어 대신 본문 근거를 묻는 쪽으로 바꾼 것인데, 이 글에서는 모델 비교의 전제로만 둔다. 원문 8건 → 건별 필수 항목 추출·고정 (모델 출력을 보기 전) → 시간대마다 실행 → 라벨 셔플 → 모델명 가린 채점 → 라벨 해제 → 판정 한도가 16시에 리셋되니 새벽과 오전 두 차례로 같은 날의 20콜을 나눠 썼고(8건 × 2창), 3차는 16시 이후로 잡았다. 1차 — 새벽의 503과 자초한 429 모델 결과 2.5 Flash 8/8 성공, 9~22초 3.8 Flash 3건에서 중단. 503(서버 과부하) 다섯 번, 429(한도 초과) 한 번 3.5 Flash-Lite v2 4/8 · v1 7/8. 실패는 전부 30초 무응답 3.8의 429는 내가 만든 것이었다. 503 재시도가 한 요청 안에서 22초에 네 번 나가는데, 스크립트의 호출 간격이 그 재시도를 세지 않아 분당 5회 한도를 넘겼다. 간격을 "최근 60초 송신 4회 이하"로 바꿨다. Lite의 실패는 성공한 건이 2~3초인데 30초 동안 응답 자체가 없는 형태였다. 한도 초과라면 429가 왔을 테고 우리 요청은 한도의 1/4이었으니, 무료 티어가 부하 때 뒤로 밀리는 Google 쪽 큐잉으로 보였다. 2차 — 채점 결과 오전 실측에서는 3.8이 8건 모두 성공했고 503도 한 번뿐이었다(재시도로 회복). Lite는 첫 창에서 v1과 v2 점수가 같아 프롬프트로 올릴 여지가 없음이 확인돼 2차에서 뺐다. 항목 3.8 Flash 2.5 Flash 필수 항목 80/92 (87%) 75/92 (82%) 값 오류 0 1 형식 위반 4 13 응답 시간 4~11초 9~22초 503 1 (회복) 0 숫자만 보면 큰 차이는 아니다. 필수 항목 5개 차이는 8건 중 4건에서 3.8이 앞서고 2건에서 뒤진 결과이고, 형식 위반 13은 대부분 detail이 표의 값을 되풀이한 것이라 독자 피해가 작아 근거로 세게 쓰지 않았다. 결정을 만든 건 값 오류 쪽이다. 2.5는 전환사채 공시에서 발행사의 매도청구권을 조기상환청구로 바꿔 썼고, 이게 1차와 2차에서 똑같이 나왔다. 같은 자리에서 3.8은 맞게 썼다. 요약이 값을 틀리면 다른 항목이 다 맞아도 소용이 없는데, 재현되는 오류 하나와 절반인 응답 시간이 3.8 쪽으로 기울게 했다. 3.8 승격과 2.5 fallback 이쯤에서 "어차피 부하는 두 모델이 비슷할 테니 fallback만 붙이고 넘어가자"는 생각이 들었다. 앞 절반은 틀렸다. 같은 시각에 2.5는 열여섯 번 중 503이 0이고 3.8은 열아홉 번 중 6이었으니, 신모델일수록 무료 티어가 먼저 밀린다. 뒷 절반은 맞되 조건이 붙었다. fallback을 붙이면 코드와 캐시의 모델 기록, 출처 표기가 두 모델로 늘고 최악의 대기가 14초에서 36초가 되니, 3.8이 실제로 더 낫다는 게 확인돼야 낼 비용이었다. 2차 채점이 그걸 보여줬다. 3.8 성공 → 그대로 3.8 429(한도 소진) → 즉시 2.5 3.8 503 재시도 소진 · 30초 timeout → 남은 예산이 15초 이상이면 2.5 그 외 오류(안전 차단·파싱 실패 등) → fallback 없음 fallback이 429에도 걸리는 게 덤으로 큰 효과였다. 두 모델의 하루 20콜 한도가 따로라 실효 한도가 40콜이 되고, 리뷰어가 소진할 수 있다는 첫 문제가 절반으로 준다. 지난 세션에 2.5를 유지한 근거가 "README 전의 안정 상태"였는데, 이 구조가 2.5 단독보다 안정적이라 그 근거는 뒤집혔다. 결정 변수가 셋째 창의 가용성에서 둘째 창의 품질로 옮겨져 셋째 창은 돌리지 않았고, v1 프롬프트로 만든 캐시 8행은 배포 뒤 지워 v2로 다시 만들어지게 했다. 정리 F108 종결 — 프롬프트 v2 반영, 원문 기준 채점 8건 × 2창, 3.8 Flash 기본 + 2.5 Flash fallback, v1 캐시 리셋. 3.5 Flash-Lite 탈락 F109 backlog — 외부 무료 API 조사 완료, 다섯 조건을 충족하는 후보 없음. Gemini 무료 티어가 깨질 때의 보험 F110 backlog — 프롬프트·요약 route 잔손질. 날짜 표기는 프롬프트가 아니라 코드 정규화로, 6월 결산 법인의 headline 예시 등 요약 요청 입력을 서버로 — 제목·회사명을 클라이언트가 보내지 않고 서버가 DART 목록에서 찾도록 바꿔, 조작된 문자열이 프롬프트와 공유 캐시에 닿는 경로를 없앰. 재시도도 요청 전체 55초 예산 안에서 backoff로. 별도 글감 측정의 실수 하나 — 첫 창의 3.8 429는 재시도를 세지 않은 스크립트 간격이 만든 것 판정을 뒤집었지만 3.8이 압도한 건 아니다. 엄밀히는 항목 점수 5%p 차이고, 결정적이었던 건 재현되는 값 오류 하나와 fallback으로 가용성 리스크가 사라진 점이다. 기준을 원문에서 뽑고 모델명을 가리는 채점 인프라는 남았으니 다음 교체는 실행과 채점 두 번이면 된다. 다음 작업 세션 외 — prod에서 요약 한 건을 요청해 3.8이 기록되는지 확인(16시 리셋 뒤), 거래일 실측 항목들 다음 세션 — README 마감(G1) → 신규 상장 일간 편입(F105) → 포트폴리오 정리(G2)
1. 중첩 클래스 : 클래스 안에 클래스를 중첩해서 정의하는 것 이는 클래스를 정의하는 위치에 따라 크게 두 가지로 분류가 가능하고 그 안에서 세부적으로 나눌 수 있다. 내부 클래스(non-static) 는 바깥 클래스를 구성하는 요소이므로 바깥 클래스의 인스턴스에 소속되지만 정적 중첩 클래스(static) 는 바깥 클래스의 인스턴스에 소속되지 않는다. 내부 클래스의 종류 내부 클래스: 바깥 클래스 인스턴스의 멤버에 접근 지역 클래스: 내부 클래스의 특정 + 지역 변수에 접근 익명 클래스: 지역 클래스의 특징 + 클래스의 이름이 없는 특별한 클래스 중첩 클래스를 사용하는 이유 논리적 그룹화 : 특정 클래스에서만 사용하는 클래스를 그 클래스 안에 넣어 관련 있는 코드끼리 묶는 것 캡슐화 : 중첩 클래스는 바깥 클래스의 private 멤버에 접근이 가능하다. 따라서 불필요한 public 메서드를 제거할 수 있다. 1) 정적 중첩 클래스 public class NestedOuter { private static int outClassValue = 3; private int outInstanceValue = 2; static class Nested { private int nestedInstanceValue = 1; public void print() { // 자신의 멤버에 접근 System.out.println(nestedInstanceValue); // 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다. // System.out.println(outInstanceValue); // 바깥 클래스의 클래스 멤버에는 접근할 수 있다. private 역시 System.out.println(NestedOuter.outClassValue); } } } private 접근 제어자는 같은 클래스 안에 있을 때만 접근이 가능하다. 중첩 클래스도 바깥 클래스와 같은 클래스 안에 있기 때문에 바깥 클래스의 private에 접근 가능하다. public class NestedOuterMain { public static void main(String[] args) { NestedOuter outer = new NestedOuter(); NestedOuter.Nested nested = new NestedOuter.Nested(); nested.print(); System.out.println("nestedClass = " + nested.getClass()); } } 정적 중첩 클래스는 new 바깥클래스.중첩클래스()로 생성할 수 있다. -> 중첩 클래스(+ 내부 클래스)는 용도가 자신이 소속된 바깥 클래스에서 사용되도록 하는 것이다. 따라서 외부에서 생성하여 사용하는 것이 가능하지만, 이미 중첩 클래스의 용도에 맞지 않을 수 있기 때문에 이때는 중첩 클래스를 밖으로 빼는 것을 고려하자. 인스턴스가 생성된 상태 바깥 클래스의 멤버에 접근 정적 중첩 클래스는 바깥 클래스의 static 필드에는 접근할 수 있따. 하지만 바깥 클래스가 만든 인스턴스 필드에는 바깥 인스턴스의 참조가 없기 때문에 접근할 수 없다. -> 서로 다른 클래스 두개를 그저 중첩해 놓은 것일 뿐 아무런 관계가 없다. (private 접근 가능 정도만 예외) 2) 내부 클래스 : 정적 중첩 클래스는 바깥 클래스와 서로 관계가 없다. 하지만 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다. 다시말해 내부 클래스는 바깥 클래스의 인스턴스에 소속된다. public class InnerOuter { private static int outClassValue = 3; private int outInstanceValue = 2; class Inner { private int innerInsanceValue = 1; public void print() { // 자신의 멤버에 접근 System.out.println(innerInsanceValue); // 외부 클래스의 인스턴스 멤버에 접근 가능, private 포함 System.out.println(outInstanceValue); // 외부 클래스의 클래스 멤버에는 접근 가능, private 포함 System.out.println(InnerOuter.outClassValue); } } } 내부 클래스의 앞에는 static이 붙지 않아 인스턴스 멤버가 된다. private 접근 제어자는 같은 클래스 안에 있을 때만 접근이 허용된다. 내부 클래스 역시 바깥 클래스와 같은 클래스 안에 있으므로 바깥 클래스의 private 접근 제어자에 접근할 수 있다. public class InnerOuterMain { public static void main(String[] args) { InnerOuter outer = new InnerOuter(); InnerOuter.Inner inner = outer.new Inner(); inner.print(); System.out.println("innerClass = " + inner.getClass()); } } 내부 클래스는 바깥 클래스의 인스턴스에 소속된다. 따라서 바깥 클래스의 인스턴스 정보를 알아야 생성할 수 있다. 내부 클래스는 바깥 클래스의 인스턴스 참조.new 내부클래스() 로 생성할 수 있다. outer.new Inner() 로 생성한 내부 클래스는 개념상 바깥 클래스의 인스턴스 내부에 생성된다. -> 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스를 생성할 수 있다. 개념상 내부 클래스의 생성은 다음과 같다. 같은 이름의 바깥 변수 접근 public class ShadowingMain { public int value = 1; class Inner { public int value = 2; void go() { int value = 3; System.out.println("value = " + value); System.out.println("thi.value = " + this.value); System.out.println("ShadowingMain.value = " + ShadowingMain.this.value); } } public static void main(String[] args) { ShadowingMain main = new ShadowingMain(); Inner inner = main.new Inner(); inner.go(); } } 변수의 이름이 같기 때문에 어떤 변수를 먼저 사용할지 우선순위가 필요하다. 프로그래밍에서 우선순위는 더 가깝고 더 구체적인 것이 갖는다. 메서드 go()에서는 지역변수은 value = 3이 제일 가깝기 때문에 우선순위가 제일 높다. -> 다른 변수들은 가려서 보이지 않음( 섀도잉 ) 3) 내부클래스 - 지역클래스 public class LocalOuterV1 { private int outInstanceVar = 3; public void process(int paramVar) { int localVar = 1; class LocalPrinter { int value = 0; public void printData() { System.out.println("value = " + value); System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } } LocalPrinter printer = new LocalPrinter(); printer.printData(); } public static void main(String[] args) { LocalOuterV1 localOuter = new LocalOuterV1(); localOuter.process(2); } } 지역클래스는 자신이 속한 코드 블럭의 매게변수에 접근할 수 있다.(매개변수도 지역변수임) 지역 클래스는 접근제어자를 사용할 수 없다. (1) 지역 변수와 변수 생명주기 변수의 생명주기 클래스 변수: 프로그램 종료까지(메서드 영역) 인스턴스 변수: 인스턴스의 생존기간, 소속된 인스턴스의 GC 전(힙 영역) 지역 변수: 메서드 호출 종료까지(스택 영역) public class LocalOuterV3 { private int outInstanceVar = 3; public Printer process(int paramVar) { int localVar = 1; class LocalPrinter implements Printer { int value = 0; @Override public void print() { System.out.println("value = " + value); // 인스턴스는 지역 변수보다 더 오래 살아남는다. System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } } Printer printer = new LocalPrinter(); // printer.print()를 여기서 실행하지 않고 Printer 인스턴스만 반환한다. return printer; } public static void main(String[] args) { LocalOuterV3 localOuter = new LocalOuterV3(); Printer printer = localOuter.process(2); // printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행 printer.print(); } } 여기서 LocalPrinter 인스턴스는 process() 메서드에서 생성된다. 또한 process()에서 main()으로 생성한 LocalPrinter 인스턴스를 반환하고 printer 변수에 참조를 보관했으므로 LocalPrinter 인스턴스는 main이 종료될 때까지 살아있는다. paramVar, localVar와 같은 지역변수는 process() 메서드가 실행되는 동안만 스택에서 생존한다. LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역 인스턴스 변수의 outInstanceVar에 접근했다. 또한 print() 메서드를 통해 스택 영역에 존재하는 지역 변수도 접근하는 것처럼 보이지만 그렇지 않다. 지역 변수의 생명주기는 매우 짧은 반면 인스턴스는 GC 전까지 생존이 가능하다. process() 메서드가 종료되면 process()의 스택 프레임이 제거되며 지역변수도 함께 제거되지만 LocalPrinter 인스턴스는 계속 생존할 수 있다. 따라서 print()로 paramVar과 localVar에 접근할 수 없다. 하지만 위 코드를 실행해보면 지역 변수의 값이 문제없이 출력되는 것을 알 수 있다. (2) 지역 변수 캡쳐 : 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어두는 과정 LocalPrinter 인스턴스 생성이 시도됨과 동시에 지역 클래스가 접근하는 지역 변수를 확인하여 paramVar과 localVar 지역 변수에 접근하여 값을 복사한다. 복사한 지역 변수를 인스턴스에 포함시켜 인스턴스 생성을 마친다. (3) 지역 변수 값변경 지역 클래스가 접근하는 지역 변수는 절대 중간에 값이 변하면 안된다. LocalPrinter를 생성하는 시점에 지역 변수인 localVar과 paramVar를 캡쳐하기 때문에 캡쳐한 값을 바꾸면 스택 영역에 있는 값과 인스턴스에 캡쳐된 값이 일치하지 않는 동기화 문제 가 발생하게 된다. 4) 익명 클래스 : 지역 클래스의 일종으로 클래스의 이름이 없다. 익명 클래스를 사용하면 기존 클래스의 선언과 생성을 따로 하던 것에 반해 이 둘을 한꺼번에 처리할 수 있게 된다. import nested.local.Printer; public class AnonymousOuter { private int outInstanceVar = 3; public void process(int paramVar) { int localVar = 1; Printer printer = new Printer() { int value = 0; @Override public void print() { System.out.println("value = " + value); System.out.println("localVar = " + localVar); System.out.println("paramVar = " + paramVar); System.out.println("outInstanceVar = " + outInstanceVar); } }; printer.print(); System.out.println("printer.class = " + printer.getClass()); } public static void main(String[] args) { AnonymousOuter main = new AnonymousOuter(); main.process(2); } } new Printer() {body} 익명 클래스는 클래스의 본문을 정의함과 동시에 생성한다. 익명 클래스를 만들 때는 부모 클래스를 상속받거나 인터페이스를 구현해야 한다. 또한 익명 클래스는 이름을 가지지 않으므로 생성자를 가질 수 없어 기본 생성자만 사용된다. 익명 클래스는 클래스를 별도로 정의하지 않고도 인터페이스를 구현할 수 있어 코드가 간결해진다는 장점이 있다. 익명 클래스를 사용할 수 없는 경우 익명 클래스는 단 한번만 인스턴스를 생성할 수 있다. 따라서 여러번 인스턴스를 생성해야 한다면 익명 클래스를 사용할 수 없다. (지역 클래스 선언)
논문 리뷰: Federated Class-Incremental Learning (GLFC, CVPR 2022) 1. 한 줄 요약 연합학습(FL)에 클래스 증분 학습(CIL)을 처음 결합한 FCIL 문제를 정의하고, 두 종류의 망각을 함께 보정하는 GLFC(Global-Local Forgetting Compensation) 를 제안한 논문이다. (파괴적인 망각) 로컬 망각 : 한 클라이언트 안에서 새 클래스 데이터는 많고 옛 클래스 exemplar는 적어서 생기는 망각 글로벌 망각 : 클라이언트마다 가진 옛 클래스가 달라서(non-i.i.d.) 생기는 이질적 망각 CIL 기법에 FL을 얹은 baseline 대비 평균 정확도가 4.4~15.1%p 높다. 2. 연구 목적과 문제 설정 2.1 동기 기존 FL은 클래스 집합이 고정되어 있다고 가정한다. 현실에서는 클라이언트가 새 클래스를 스트리밍으로 수집하고, 저장 용량이 작아 옛 데이터를 다 못 들고 있으며, 새 클라이언트가 중간에 합류한다. 논문의 예시는 코로나 변종이 새 클래스로 등장하는 병원 간 진단 모델이다. 새로 합류한 병원은 옛 질병 데이터가 거의 없다. (논문에서 설명한 코로나 예제) CIL과 FL을 단순 결합하면 두 가지 문제가 생긴다. 서버가 새 클래스가 언제, 어디서 들어오는지 알아야 하는데, 이는 프라이버시 위반이다. (엔트로피를 통해서 해결 클라이언트마다 망각 양상이 달라서 로컬 CIL만으로는 부족하다. (로컬 CIL, 글로벌 CIL 분리) 2.2 FCIL 정의 태스크 스트림 $\mathcal{T}={\mathcal{T}^t}_{t=1}^{T}$. 첫 번째 학습(태스크 1), 두 번째 학습(태스크 2)-> 지속적으로 학습 태스크 $t$에는 새 클래스 $C^t$개가 있고, 옛 클래스는 $C^p=\sum_{i<t}C^i$개다. 현재가 $t$번째 학습 시점이라면, $C^t$는 지금 새롭게 배워야 할 질병(클래스)의 개수를 뜻합니다. 반면 $C^p$는 기호($\sum$)가 의미하듯, 이전에 학습했던 모든 태스크($i<t$)의 질병 개수를 다 더한 값, 즉 '누적된 옛날 질병 수'를 의미합니다 클라이언트 $l$은 새 데이터 $\mathcal{T}^t_l$과 exemplar memory $\mathcal{M}_l$을 갖고, 클래스 분포 $P_l$은 클라이언트마다 다르다. $l$번째 병원(예: 서울병원)이 이번 단계에서 새로 수집한 데이터가 $T_l^t$이고, 과거의 지식을 잃어버리지 않기 위해 1번 주제에서 다루었던 그 소량의 보관소가 바로 $M_l$ 클라이언트는 매 태스크에서 세 종류로 나뉜다. 종류 새 데이터 옛 exemplar 의미 $S_o$ 없음 있음 이번 태스크 데이터를 못 받은 기존 클라이언트 $S_b$ 있음 있음 새 데이터와 옛 exemplar를 모두 가진 클라이언트 $S_n$ 있음 없음 새로 합류한 클라이언트 태스크 개수, 분포, 새 클래스 도착 시점, 클라이언트 합류 시점은 모두 사전 정보가 없다. 3. 제안 방법 (GLFC) 3.1 로컬 망각 보정 (a) 기본 손실 $$ \mathcal{L} {CE}=\frac{1}{b}\sum {i=1}^{b}\mathcal{D} {CE}\big(P^t_l(x^t {li},\Theta^{r,t}),,y^t_{li}\big) $$ 직관. 표준 분류 손실이다(논문은 sigmoid 확률 위의 binary cross-entropy를 쓴다). 문제는 미니배치 안에 새 클래스 샘플은 많고 옛 클래스 exemplar는 적다는 점이다. 그러면 새 클래스 쪽 gradient가 압도해서 옛 클래스가 밀려난다. 이후 두 손실이 이 불균형을 고친다. 전체 오차를 줄이는 데만 집중합니다. 비유로, 교실에서 90명의 학생이 새로운 질병에 대해 큰 소리로 떠들고 있고, 단 10명의 학생만이 과거 질병에 대해 이야기하는 상황입니다. 모델이 학습하는 방향(그래디언트)은 당연히 목소리가 큰 다수 쪽에 압도당하게 됩니다. 그 결과 모델은 새로운 질병을 맞추는 데만 급급해져서 과거 질병에 대한 기억을 머릿속에서 밀어내 버립니다(파괴적 망각). 그래서 논문은 이 기본 손실($\mathcal{L}_{CE}$)만으로는 한계가 있다고 지적하는 것 교차 엔트로피($\mathcal{D} {CE}$) 앞에 곱해졌던 가중치 비율 $\frac{\vert{}\mathcal{G} {li}^t\vert{}}{\tilde{\mathcal{G}}_i}$ (b) Class-Aware Gradient Compensation Loss ($\mathcal{L}_{GC}$) 먼저 샘플 하나가 정답 뉴런에 주는 gradient를 잰다. $$ \mathcal{G}^t_{li}=\frac{\partial,\mathcal{D} {CE}\big(P^t_l(x^t {li},\Theta^{r,t} l),,y^t {li}\big)}{\partial,\mathcal{N}^t_{y^t_{li}}}=P^t_l(x^t_{li},\Theta^{r,t} l) {y^t_{li}}-1 $$ 직관. 정답 클래스 확률이 1에 가까우면 $|\mathcal{G}|\approx 0$이고, 확률이 낮으면 $|\mathcal{G}|\approx 1$이다. 즉 이 샘플이 얼마나 못 맞히고 있는가를 나타내는 숫자다. 처음 보는 새 클래스 샘플은 크고, 이미 잘 맞히는 샘플은 작다. 다음으로 새 클래스 그룹과 옛 클래스 그룹의 평균 gradient 크기를 각각 구한다. $$ \mathcal{G} n=\frac{\sum {i=1}^{b}|\mathcal{G}^t_{li}|\cdot\mathbb{I} {y^t {li}\in\mathcal{Y}^t_l}}{\sum_{i=1}^{b}\mathbb{I} {y^t {li}\in\mathcal{Y}^t_l}},\qquad \mathcal{G} o=\frac{\sum {i=1}^{b}|\mathcal{G}^t_{li}|\cdot\mathbb{I} {y^t {li}\in\cup_{j<t}\mathcal{Y}^j_l}}{\sum_{i=1}^{b}\mathbb{I} {y^t {li}\in\cup_{j<t}\mathcal{Y}^j_l}} $$ 직관. $\mathcal{G}_n$은 새 클래스 샘플들이 평균적으로 얼마나 크게 gradient를 주는지, $\mathcal{G}_o$는 옛 클래스 샘플들이 평균적으로 얼마나 주는지를 나타낸다. 이 둘의 격차가 곧 학습 속도와 망각 속도의 불균형이다. 이를 이용해 재가중한 손실은 다음과 같다. $$ \mathcal{L} {GC}=\frac{1}{b}\sum {i=1}^{b}\frac{|\mathcal{G}^t_{li}|}{\bar{\mathcal{G}} i}\cdot\mathcal{D} {CE}\big(P^t_l(x^t_{li},\Theta^{r,t} l),,y^t {li}\big),\qquad \bar{\mathcal{G}}_i=\begin{cases}\mathcal{G}_n & x_i\text{가 새 클래스}\ \mathcal{G}_o & x_i\text{가 옛 클래스}\end{cases} $$ 직관. 각 샘플의 가중치가 $|\mathcal{G}_i|/\bar{\mathcal{G}}_i$, 즉 자기 그룹 평균 대비 상대적 난이도다. 새 그룹과 옛 그룹 모두 가중치의 평균이 약 1이 되어, 어느 한쪽 그룹이 gradient를 독점하지 못한다. 그룹 간 스케일 차이가 사라진다. 그룹 안에서는 평균보다 어려운 샘플에 더 큰 가중치가 붙는다(focal loss와 비슷한 효과). 결과적으로 새 클래스는 학습 속도를 늦추고, 옛 클래스는 망각 속도를 늦춘다. 이 아이디어는 저자들의 이전 연구(AAAI'21, FL에서의 class imbalance)를 가져온 것이다. $S_o$는 $\bar{\mathcal{G}}_i$를 항상 $\mathcal{G}_o$로, $S_n$은 항상 $\mathcal{G}_n$으로 고정한다. 각각 옛 클래스만, 새 클래스만 갖고 있기 때문이다. 개별 문제의 오답률 측정 ($\mathcal{G}^t_{li}$): 모델이 데이터를 보고 예측을 했을 때 얼마나 틀렸는지(오차)를 계산합니다. 확률이 낮아 완전히 틀렸으면 1에 가까워지고, 정답을 확신하면 0에 가까워집니다. 당연히 처음 보는 '새로운 질병(클래스)' 데이터는 값이 크게 나오고, 예제 메모리에 있던 '옛 질병' 데이터는 상대적으로 작게 나올 것입니다. 그룹별 평균 목소리 크기 계산 ($\mathcal{G}_n, \mathcal{G}_o$): 새 질병 데이터들이 내는 오차의 평균($\mathcal{G}_n$)과 옛 질병 데이터들이 내는 오차의 평균($\mathcal{G}_o$)을 각각 따로 구합니다. 앞서 이야기한 것처럼 새 질병 그룹의 목소리(오차)가 훨씬 크기 때문에 전체 학습 방향을 독점하려 할 것입니다. 공평한 확성기 달아주기 ($\frac{\vert{}\mathcal{G}^t_{li}\vert{}}{\bar{\mathcal{G}}_i}$): 각 샘플이 낸 오차를 자기가 속한 그룹의 평균치로 나누어 줍니다. 이렇게 하면 새 그룹이든 옛 그룹이든 가중치의 평균이 약 '1'로 동일하게 맞춰집니다. 즉, 새 질병 데이터가 뿜어내는 거대한 오차는 자신들의 큰 평균($\mathcal{G}_n$)으로 나누어져 억제되고, 옛 질병 데이터의 작은 오차는 자신들의 작은 평균($\mathcal{G}_o$)으로 나누어져 뻥튀기(증폭)됩니다 (c) Class-Semantic Relation Distillation Loss ($\mathcal{L}_{RD}$) 옛 모델 $\Theta^{t-1}_l$의 예측 확률로 one-hot 라벨의 앞쪽 $C^p$ 차원을 교체해 soft 라벨을 만든다. $$ \tilde{Y}^t_l=\Big[\underbrace{P^{t-1} l(X^t {lb},\Theta^{t-1} l)} {\text{옛 클래스 }C^p\text{ 차원: 옛 모델의 soft 예측}}\ \Big|\ \underbrace{Y^t_{lb}[C^p{:}]}_{\text{새 클래스 차원: 원래 one-hot}}\Big] $$ $$ \mathcal{L} {RD}=\mathcal{D} {KL}\Big(P^t_l(X^t_{lb},\Theta^{r,t}_l)\ \Big|\ \tilde{Y}^t_l\Big) $$ 직관. 옛 모델이 "이 고양이 사진은 개 0.3, 호랑이 0.2 정도 닮았다"고 알던 클래스 간 유사도 구조를 현재 모델이 그대로 유지하도록 강제한다. 기존 KD(LwF, iCaRL 등)는 옛 클래스 출력만 옛 모델과 맞춘다. 이 논문은 옛 클래스 부분은 옛 모델의 soft 확률, 새 클래스 부분은 정답 one-hot으로 이어 붙인 하나의 타깃을 쓴다. 그래서 옛-새 클래스 간 관계를 한 번에 학습한다. 단, 이 방식은 사실상 옛 모델 확률을 옛 클래스 자리의 라벨로 쓰는 것이라 "새 클래스가 옛 클래스와 얼마나 비슷한가"를 새로 학습하는 것과는 다르다. 옛 모델이 새 클래스를 모른다는 점에 주의해야 한다. 모델이 예측해야 하는 전체 타깃 라벨($\tilde{Y}^t_l$)을 두 조각으로 이어 붙인 일종의 '혼합 라벨'로 만듭니다. 왼쪽 조각 (옛 클래스 부분, $C^p$ 차원): 여기에는 정답(1 또는 0) 대신, 과거의 최적 모델(선생님)이 내놓은 '부드러운 예측(soft label)'을 그대로 넣습니다. 선생님이 "이 사진은 감기일 확률 70%, 폐렴일 확률 20%, 천식일 확률 10%야"라고 말한 그 미묘한 관계성(유사도 구조) 자체를 정답처럼 사용하는 것입니다. 오른쪽 조각 (새 클래스 부분): 여기에는 방금 들어온 새로운 질병 데이터의 '진짜 정답(one-hot label)'을 넣습니다. "이것은 100% 새로운 COVID-19 변이이다"라는 확실한 정보입니다. 그리고 모델은 쿨백-라이블러 발산($\mathcal{D}_{KL}$)을 통해 이 '혼합 라벨'의 분포를 통째로 따라 하도록 학습합니다. 결과적으로 현재 모델은 새로운 질병에 대한 정답을 확실하게 배우면서도, 동시에 과거 질병들 사이의 미묘한 관계성(선생님의 지식 구조)을 잊지 않고 유지하게 됩니다 (d) 로컬 최종 목적함수 $$ \mathcal{L} l=\lambda_1\mathcal{L} {GC}+\lambda_2\mathcal{L}_{RD} $$ 직관. "새 클래스를 잘 배우되(GC, 균형 조정) 옛 지식은 유지한다(RD)"의 균형이다. $t=1$: 옛 모델이 없으므로 $(\lambda_1,\lambda_2)=(1.0,0)$ $t\ge 2$: $(0.5,0.5)$ (e) Task Transition Detection 클라이언트가 스스로 "새 클래스가 들어왔다"를 알아야 exemplar 갱신과 옛 모델 저장 시점을 정할 수 있다. 라벨을 본 적 있는지 확인하는 방법은 다른 클라이언트가 이미 본 클래스를 새 클래스로 오인할 수 있고, 성능 하락을 신호로 쓰는 방법은 클라이언트 샘플링만으로도 성능이 요동쳐서 안 된다. 그래서 전역 모델의 평균 엔트로피를 쓴다. $$ H^{r,t} l=\frac{1}{N^t_l}\sum {i=1}^{N^t_l}\mathcal{I}\big(P^t_l(x^t_{li},\Theta^{r,t})\big),\qquad \mathcal{I}(p)=-\sum_i p_i\log p_i $$ $$ H^{r,t}_l-H^{r-1,t}_l\ \ge\ r_h\ (=1.2)\ \Longrightarrow\ \text{새 태스크 도착},\ t\leftarrow t+1 $$ 직관. 전역 모델이 처음 보는 클래스의 샘플을 만나면 "이게 뭔지 모르겠다"며 예측이 퍼져서 엔트로피가 급등한다. 절대값이 아니라 직전 라운드 대비 변화량을 보므로, 클라이언트 샘플링으로 정확도가 완만하게 흔들리는 것과 구분된다는 논리다. 감지되면 $\mathcal{M}_l$을 갱신하고 옛 모델 $\Theta^{t-1}_l$을 저장한다. 3.2 글로벌 망각 보정: Proxy Server 문제. $\mathcal{L}_{RD}$에 쓰는 옛 모델의 질이 전역 망각을 좌우한다. 각 클라이언트가 자기 데이터로 옛 모델을 고르면 자기가 가진 일부 클래스에만 최적이다. 그래서 별도의 proxy server $S_P$ 가 전역 관점에서 최선의 옛 모델을 골라 배포한다. 다만 클라이언트의 원본 데이터를 보내면 안 되므로 다음의 프라이버시 통신을 쓴다. (a) Prototype Gradient-Based Communication 클라이언트는 새 클래스마다 특징 공간에서 클래스 평균에 가장 가까운 prototype 샘플 하나 를 고른다. 얕은 4층 LeNet $\Gamma={W_i}_{i=1}^{L}$에 넣어 gradient만 계산해 $S_P$에 보낸다. $$ \nabla_{W_i}\Gamma_{lc}=\nabla_{W_i}\mathcal{D} {CE}\big(P^t_l(x^t {lc^ },\Gamma),,y^t_{lc^ }\big) $$ 직관. FL이 원본 데이터 대신 gradient를 공유하는 것과 같은 논리다. 여기서는 클래스당 대표 샘플 하나의 gradient만 보내므로 통신량이 매우 작다. $S_P$는 받은 gradient를 섞어(shuffle) 풀을 만든다. 어느 클라이언트가 보냈는지 추적하지 못하게 하려는 것이다. 라벨은 마지막 층 gradient의 부호로 복원한다(iDLG 방식). gradient inversion으로 샘플을 복원한다. 무작위 dummy $\bar{x}\sim\mathcal{N}(0,1)$에서 시작한다. $$ \mathcal{L} {RT}=\sum {i=1}^{L}\Big|\nabla_{W_i}\mathcal{D} {CE}\big(P^t(\bar{x}^t_n,\Gamma),,y^t_n\big)-\nabla {W_i}\Gamma^t_n\Big|^2 $$ $$ \bar{x}^t_n\leftarrow\bar{x}^t_n-\eta,\nabla_{\bar{x}^t_n}\mathcal{L}_{RT} $$ 직관. "이 gradient를 만들어 낼 만한 입력이 무엇인가"를 거꾸로 찾는 것이다. dummy 입력의 gradient가 받은 gradient와 일치하도록 dummy 이미지를 깎아 나간다(Deep Leakage from Gradients 계열). 정보를 숨긴 것이 아니라 $S_P$가 복원하도록 놔두고, 대신 아래처럼 복원되어도 안전하도록 미리 변형해 둔다. 실제 구현은 L-BFGS를 쓰고, gradient당 200 iteration을 돌린다. (b) Perturbed Prototype 생성 (프라이버시 보호) Prototype을 그대로 보내면 원본이 복원되므로, 보내기 전에 노이즈를 섞은 변형본으로 바꾼다. $$ \mathcal{L} {GP}=\mathcal{D} {CE}\Big(P^t_l\big(\Phi(x^t_{lc^ })+\gamma,\mathcal{N}(0,\sigma^2),\ \Theta^{r,t} l\big),\ y^t {lc^ }\Big) $$ $$ x^t_{lc^ }\leftarrow x^t_{lc^ }-\eta,\nabla_{x^t_{lc^*}}\mathcal{L}_{GP} $$ 직관. latent feature $\Phi(x)$에 노이즈($\gamma=0.1$, $\sigma^2$은 해당 클래스 feature의 분산)를 더한 상태로도 정답을 맞히도록 입력 이미지를 역전파로 수정한다. 그 결과 이미지는 원본과 눈으로 달라 보이지만 모델 입장에서는 "같은 클래스"로 취급되는 샘플이 된다. 공격자가 복원해도 원본 모습이 아니라 변형된 모습만 얻는다는 논리다(Fig. 2). 다만 형식적 프라이버시 보장은 없다. (c) Best Old Model 선택 $S_P$는 복원된 prototype으로 각 라운드의 전역 모델 $\Theta^{r,t}$를 평가해 정확도가 가장 높은 모델을 $\Theta^t$로 저장한다. $t\ge2$부터 $\Theta^{t-1}$과 $\Theta^t$를 선택된 클라이언트에 배포한다. 새 클래스를 감지한 클라이언트: $\Theta^t$를 옛 모델로 사용 감지하지 못한 클라이언트: $\Theta^{t-1}$을 옛 모델로 사용 직관. 여러 클라이언트의 대표 샘플로 만든 "전역 검증 세트"로 모델을 고르는 것이다. 개별 클라이언트는 자기 클래스만 볼 수 있지만, $S_P$는 모든 클라이언트의 prototype을 모아 볼 수 있다는 점이 핵심이다. 📡 통신 최소화 및 역추적 (a): 병원(클라이언트)은 원본 사진을 보내는 대신, 특정 질병을 가장 잘 대표하는 사진 1장(프로토타입)을 고른 뒤 그 사진에 대한 '학습 방향(그래디언트)'만 프록시 서버로 보냅니다. 서버는 이 그래디언트를 역추적(gradient inversion)해서 이미지를 억지로 복원해 냅니다. 🛡️ 프라이버시 씌우기 (b): 서버가 이미지를 복원해 낸다는 점을 역이용하는 아주 영리한 단계입니다. 클라이언트는 애초에 원본 이미지가 복원되지 않도록, 보내기 전에 미리 노이즈를 섞어 이미지를 일그러뜨려 놓습니다(Perturbed Prototype). 🏆 전역 모의고사 실시 (c): 프록시 서버는 각 클라이언트가 보낸 그래디언트들을 이리저리 섞은(shuffle) 뒤, 이 일그러진 이미지들을 복원해 냅니다. 그리고 이 이미지들을 몽땅 모아 전체 네트워크를 아우르는 '전역 검증 세트(모의고사)'로 삼아, 가장 실력이 좋고 덜 편향된 모델을 골라냅니다 3.3 전체 파이프라인 매 라운드 초입에 모든 클라이언트가 엔트로피를 계산하고 iCaRL 방식으로 exemplar를 갱신한다. 서버 $S_G$가 클라이언트를 무작위로 선택하고, 선택된 클라이언트가 $\mathcal{L}_l$로 로컬 학습을 한 뒤 집계한다. 새 클래스를 감지한 클라이언트는 perturbed prototype gradient를 $S_P$에 전송한다. $S_P$는 복원, 평가, best old model 선택 및 배포를 수행한다. 스스로 상태 점검 (엔트로피 계산 및 메모리 갱신): 라운드가 시작되면 로컬 클라이언트(병원)들은 모델의 불확실성(엔트로피)을 계산해 "새로운 질병 데이터가 도착했는지" 파악합니다. 새로운 클래스를 감지하면 예전 모델을 따로 저장해두고, 소중한 과거 데이터(exemplar)를 메모리에 갱신해 둡니다. 균형 잡힌 로컬 학습: 중앙 서버($S_G$)가 무작위로 학습에 참여할 병원들을 선택합니다. 선택된 병원들은 우리가 배운 손실 함수들(기본 손실 + 그래디언트 보상 $\mathcal{L} {GC}$ + 의미론적 증류 $\mathca
2020 TOPCIT 문제풀이 (전 영역) - 기술영역 - M3. 시스템 아키텍처 - 객관식 데스크톱 가상화(VDI, Virtual Desktop Infrastructure) Q. 데스크톱 가상화는 코로나-19 확산으로 인해 재택근무를 위한 환경으로 부각되고 있음 사용자의 데스크톱은 입출력을 위한 장치로만 사용된다. 사용자의 업무 수행을 위한 데이터, 문서등은 데스크톱에 저장된다. 서버 에 저장 서버에서 애플리케이션 실행결과가 데스크톱 환경에 이미지 형태로 전송된다. 클라이언트, 세션관리자, 가상머신, 스토리지 등의 논리적 계층 구조로 구성된다. 해설 : VDI 환경에서 사용자의 업무수행을 위한 애플리케이션, 데이터, 문서 등은 서버에 저장 되고, 서버에서 실행된 애플리케이션 실행결과가 데스크톱 환경에 이미지 형태로 전송되어 사용자의 데스크톱은 입출력을 위한 장치로만 사용된다. M4. 정보보안 - 단답형 SALT A기업은 고객의 비밀번호의 안전한 관리를 일방향 암호화하여 저장하기로 결정 고객의 비밀번호만 해쉬함수를 적용하여 저장/관리하게 되면 레인보우(Rainbow) 공격 등과 같은 취약하기 때문에 비밀번호에 (SALT) 값을 추가하여 해쉬 함수를 적용하고, (SALT) 값은 안전하게 보관하기로 결정 해설 : 고객의 비밀번호 등의 안전한 관리를 위해 SHA-256 이상의 안전한 해쉬 함수를 사용해야 하며, 비밀번호에만 해쉬 함수를 적용할 경우 레인보우 등과 같은 공격에 취약하기 때문에 SALT 값과 같은 임의의 문자열을 추가하여 해쉬함수를 적용하고, 사용된 SALT 값은 안전하게 보관 해야 한다. M2. 데이터 - 서술형 뷰(View)를 생성하기 위한 DDL(Data Definition Language) CREATE VIEW VIP고객(고객아이디, 고객성명, 핸드폰번호) AS SELECT 고객아이디, 고객성명, 핸드폰번호 FROM 고객 WHERE 등급 = 'VIP' WITH CHECK OPTION; (1) 데이터베이스 뷰의 개념에 대해 서술하시오. 기본 테이블인 ‘고객’ 테이블에서 등급이 ‘VIP’인 고객의 고객아이디, 고객성명, 핸드폰번호를 추출하여 ‘VIP고개’이라는 이름의 뷰를 생성한다. 뷰는 데이터베이스에서 하나 이상의 기본 테이블로부터 유도되어 물리적으로는 존재하지 않는 논리적 가상 테이블이다. (2) 밑줄 친 WITH CHECK OPTION 의 의미는? 뷰를 생성할 때 WITH CHECK OPTION을 사용하면, 뷰를 생성할 때의 조건에 해당하는 데이터에 대해서만 추가/변경/삭제가 가능하도록 제한한다. 'VIP고객' 뷰에서 등급이 VIP인 고객정보는 변경할 수 있지만, 등급이 VIP가 아닌 고객정보를 추가/변경/삭제가 불가하도록 제약한다. M1. 소프트웨어 - 수행형 상위 클래스 추출(Extract Superclass) vs 클래스 추출(Extract Class) [보기-A]를 [보기-B]설계 변경에 적용한 리팩토링 기법을 설명하시오. (10점) 상위클래스 추출 은 여러 클래스에 포함된 유사한 기능의 필드나 메서드를 추출하여 상위 클래스를 추출하여 공유하도록 하는 리팩토링 기법 +) 클래스 추출 은 하나의 클래스가 관련 없는 여러 기능을 수행하는 경우 새로운 클래스를 생성하여 일부 기능을 수행하는 필드와 메소드를 새로운 클래스로 이관하는 리팩토링 기법 [보기-B]의 SavingAccount 추상클래스를 JAVA코드로 구현하시오.(30점) 클래스의 abstract 스테레오 타입 메서드의 {abstract} 프로퍼티 접근제한자 (Access Modifier) #: protected +: public -: private 필드 타입(int, double), 메서드의 파라미터와 리턴 타입 abstract class SavingAccount{ protected int period; protected double rate; protected double money; public abstract double calcInterest(); } - 비즈니스영역 - M5&M6. 비즈니스 및 통합 - 객관식 q1. 프로그램 저작권 보호 대상이 아닌 것은? 프로그램 소스코드 프로그램 목적코드 프로그램 구조 및 배열 4. 문제 해결을 위한 절차나 방법으로서의 알고리즘 해설 : 프로그램 작성, 설정된 문제를 해결하기 위해서 사용한 프로그램 언어, 프로토콜, 알고리즘 해법 등은 보호의 대상에서 제외된다. q2. [보기]는 1페이지 보고서의 일부이다. 보고서의 개선 사항으로 적절하지 않은 것은? 1. 제목을 시스템 구축안으로 변경한다. ~ 2. 보고서에 작성 조직과 유관 조직을 추가한다. 3. 현황/이슈 내용 중 훌륭한, 멋있는 등의 수식어구는 제외한다. 4. 구축 절차를 수행 항목과 수행 기간으로 분류하여 표로 전환한다. 해설 : 제목은 문서의 내용을 바로 알 수 있도록 구성 해야 한다. 특히 바쁜 상사나 경영진에게는 제목만 보아도 바로 내용이 상상되게 끔 제목을 잘 잡는 것이 중요하다. 수요자가 제목만 보고도 전체 내용이나 취지, 보고 성격을 알 수 있도록 내용을 최대한 포괄해야 하는데 되도록 20자 이내로 압축해서 표현하는 것이 좋다. q3. [보기]는 SI 업체 A사에서 발생가능한 위험에 대해 대응한 방식이다. A사의 위험 대응 전략은 무엇인지 고르시오. A사는 차세대 프로젝트에서 인공지능 신기술에 대한 기술적 해결이 어려워, 인공지능 솔루션 공급사와 회의를 하여 공급사가 수행하기로 협의하고 프로젝트를 진행하기로 하였다. 회피 (Avoid) 전가 (Transfer) ✅ (고객사나 공급사로 영향력 및 대응의 주체를 제 3자에게 이동) 완화 (Mitigate) 수용 (Accept) 위험 대응 계획 : 프로젝트 목표에 위협적인 요인은 경감시키고 기회는 증대 시킬 수 있는 대안과 조치를 개발하는 프로세스 부정적인 위험(위협)에 대한 전략 : 회피, 전가, 완화, 수용 긍정적인 위험(기회)에 대한 전략 : 활용, 분담, 증대, 수용 M5&M6. 비즈니스 및 통합 - 단답형 q. [보기]의 [프로젝트 현황]에서 프로젝트 원가통제를 위한 CPI(CostPerformance Index)와 SPI(Schedule Performance Index) 각각 계산하여라. 획득 가치 분석(EVM) 범위, 일정, 자원의 측정치를 모두 결합하여, 성과와 진척율을 평가하는 방법론 모든 분석 단위를 화폐로 계산하여 분석하는 방법 PV: 계획된 성과, EV: 획득한 성과, AC: 실제 사용 비용 SPI * = EV / PV, 일정성과지수 = 프로젝트의 일정 효율을 계산 (SPI < 1 이면 일정 지연이 예상된다.) * CPI = EV / AC, 원가성과지수 = 프로젝트의 원가 효율을 계산 (CPI < 1 이면 원가 초과가 예상된다.) 답 : SPI=0.9, CPI=0.75 M5&M6. 비즈니스 및 통합 - 서술형 q. A사는 전년도에 도입한 CRM시스템의 재무적 성과를 평가하기 위하여 ROI와 PP를 활용하려고 한다. [보기]의 내용을 바탕으로 아래 물음에 답하시오. CRM 시스템 적용 결과 시스템 도입 전과 비교하여 40억원의 영업이익(순이익은 동일) 획득 CRM 시스템 도입 위하여 개발 인건비 및 인프라비용으로 150억원을 투자 CRM 시스템 관련 50억원의 기타 비용 사용 1) A사의 CRM 시스템 도입 2개년의 ROI(Return of Investment)를 구하시오. (15점) ROI: 투자수익률로 총 비용 대비 순 효과의 비율을 구하는 재무적 방식 ROI = 영업이익/투자비용x100 = 40억원/(150억원+50억원)x100 = 20% 2) A사의 CRM 시스템 도입 시 PP(Payback Period)를 구하시오. PP: 투자회수기간은 투자비용을 언제 회수할 수 있는지에 초점을 맞추는 투자평가 기법으로 다른 요인들이 동일한 경우 투자회수시간이 짧을수록 좋은 투자로 평가함 PP = 투자비용/연간이익 = (150억원 + 50억원)/40억원 = 5년 M7. 통합 - 통합서술 및 통합수행형 q1. 국내 프라이빗 클라우드 컴퓨팅 서비스 1위인 A기업은 퍼블릭 클라우드 컴퓨팅 영역으로의 사업 확장을 고려하고 있다. A기업은 IT 전략 수립을 위해 비지니스 환경 분석을 수행하고자 한다. [보기1]의 상황을 참고하여 다음 (1), (2)번 문제에 답하시오. [보기1] 한국의 많은 기업들은 핵심 비즈니스모델의 혁신과 IT 비용 절감을 동시에 해결하기 위해 클라우드도입을 고려하고 있다. 또한, 정부는 2015년 9월 '클라우드 발전법'을 제정하고공공분야의 민간 클라우드 이용을 통한 산업 활성화를위한 '클라우드컴퓨팅 발전 기본계획'을 발표하였다. 아마존웹서비스(AWS)는2016년 한국 내 클라우드 컴퓨팅 강화를 위해 데이터센터인 서울 리전(Regjion)가동을 시작하였으며, 특히 한국의 퍼블릭 클라우드 시장은 안정성, 보안 등이 검증되면서 연평균22.6%라는 빠른 성장세를 나타내고있다. (1) IT 전략 수립을 위해 A기업의 SWOT을 분석하시오. 단, S,W,O,T 4가지 관점으로 각각 설명하시오. (20점) * Strength(강점) * : 프라이빗 클라우드 사업 영역에 대한 기술적 우위를 차지하고 있다. Weakness(약점) : 프라이빗 클라우드를 주사업 영역으로 가지고 있어서 퍼블릭 클라우드 운영에 대한 인프라, 서비스 기반이 부족하다. Opportunity(기회) : 2015년에 제정된 정부의 ‘클라우드 발전법’, ‘클라우드 컴퓨팅 발전 기본계획’으로 인해 공공 분야에서 클라우드에 대한 관심이 고조된 상태다. Thread(위협) : AWS가 2016년 한국 내 클라우드 컴퓨팅 영역을 차지하기 위해 ‘서울리전’을 가동하기 시작했다. (2) A기업은 사업 투자를 고려하고 있는 퍼블릭 클라우드와 기존 사업 영역인 프라이빗 클라우드의 개념을 비교하여 설명하시오. 퍼블릭 클라우드 는 불특정 다수에게 IT서비스를 인터넷을 통해 제공하는 형태이다. 프라이빗 클라우드 는 기업이 시스템 자원을 직접 소유하고 자신들의 회사 전용의 클라우드로 사용하는 형태다. [보기2] 개발자 3명으로 구성된 소규모 인적 기업으로, 도큐먼트, 스프레드 시트 등의 문서 앱을 개발하고자 한다. 실제 어플리케이션을 개발하기 위한 도구 및 인프라를 갖추고 있지 않다. 그러나, 현시점에서는 개발용 시스템 구매에 대한 투자는 어려운 상황이다. 향후 개발된 시스템은 사용자 증가 시 Scale-out 등이 자동으로 이루어지도록 하고 싶다. (3) [보기2]는 B기업의 운영 현황을 나타낸 것이다. B기업은 기업 내외부 환경을 고려하여 클라우드 서비스를 받기로 결정하였다. 클라우드 서비스 모델은 크게 SaaS, PaaS, IaaS로 나눌 수 있는데 B기업이 사용하기에 적합한 클라우드 서비스 모델을 추천하고, 해당 모델에 대해 간단히 설명하시오. PaaS(Platform as a Service) 가 적합하다. 애플리케이션을 개발하거나 실행하기 위한 시스템 기능을 서비스로 제공하며 데이터베이스, 개발 프레임워크, 실행 시에 필요한 라이브러리 및 모듈을 제공하는 클라우드 서비스 모델이다. SaaS(Software as a Service) : 특정 소프트웨어를 구매하고 설치하는 대신, 인터넷을 통해 액세스하여 사용, IT 리소스가 제한적인 중소기업이나 스타트업에서 소프트웨어 유지보수와 업그레이드의 부담을 줄이고 싶어할 때 사용 PaaS(Platform as a Service) : 웹 애플리케이션 개발 및 배포가 필요한 개발자들이 사용, 개발자와 개발 팀이 개발 과정을 간소화하고자 할 때 IaaS(Infrastructure as a Service) : 대규모 컴퓨팅 자원이 필요한 경우, 데이터 센터의 유지 관리에 대한 비용과 복잡성을 피하고 싶은 기업에서 사용 q2. 최근 A회사에서는 기존에 내부 직원용으로 사용하던 웹 응용프로그램을 대국민 서비스 용도로 변경하는 프로젝트를 진행 중에 있다. 프로젝트를 맡은 김PM은 '웹 접근성 및 호환성 준수', '모바일 지원', '보안성 강화'라는 3가지 목표를 가지고 업무를 수행하고자 한다. 다음 물음에 답하시오. (1) '웹 접근성(Web Accessicility)'과 '웹 호환성'의 개념을 각각 설명하시오. (30점) 웹 접근성 은 장애인, 고령자 등이 웹 사이트에서 제공하는 정보에 비장애인과 동등하게 접근하고 이해할 수 있도록 보장하는 것이다. (인적, 환경적 용인에 제약 없는 웹 정보 접근) 웹 호환성 은 서비스 이용자 단말기의 하드웨어 및 소프트웨어 환경이 다른 경우에도 동등한 서비스를 제공하는 것 or 웹 브라우저 버전이나 종류에 관계없는 웹 사이트 접근, 크로스 브라우징이 가능 or 웹 표준 준수를 통한 브라우저 호환성을 확보한다. (2) 김PM은 동일한 웹 응용프로그램을 PC뿐만 아니라 스마트폰이나 태블릿과 같은 모바일 기기에서도 동일하게 보여주기 위한 방법으로, '반응형 웹(Responsive Web)'과 '적응형 웹(Adaptive Web)'을 고려하고 있다. '반응형 웹'과 '적응형 웹'의 개념을 각각 설명하시오. (30점) 반응형 웹 은 한 번의 개발로 디스플레이 종류에 따라 화면 크기 및 해상도가 자동으로 조절되어 화면을 보여주는 방식 적응형 웹 은 미리 정해진 몇 가지 화면 크기를 기준으로 두고 비율에 맞춰 페이지를 구성하는 방식 (3) 김PM은 웹 응용프로그램의 보안성 검토를 요청한 결과, [보기]의 '수정 전' 소스코드에서 SQL 삽입공격에 취약점이 있다는 진단을 받았다. 이러한 보안취약점은 시큐어 코딩을 통해 예방할 수 있다. [보기]의 소스코드를 '수정 후'와 같이 보안 취약점이 없도록 빈칸을 작성하시오. (preparedStatement 방식을 사용하도록 작성하며, 4줄 이내로 작성할 것) (40점) String query = "SELECT * FROM ? WHERE Name = ?"; stmt = con.prepareStatement(query); stmt.setString(1, tableName); stmt.setString(2, name); 안전한 코딩기법 preparedStatement 클래스와 하위메소드 executeQuery(), execute(), executeUpdate()를 사용하는 것이 바람직 preparedStatement 클래스를 사용할 수 없는 환경이라면, 입력 값을 필터링 처리한 후 사용한다. 필터링 기준은 SQL 구문제한, 특수문자 제한, 길이제한을 복합적으로 사용한다. 외부로부터 인자를 받는 preparedStatement 객체를 상수 스트림으로 생성하고, 인자부분을 setXXX 메소드로 설정하여, 외부의 입력이 쿼리문의 구조를 바꾸는 것을 방지하는 것이 필요하다.
TL;DR 결론부터 말하면, 입사 전에 독자적으로 만든 개인 프로젝트까지 회사 소유로 한다는 조항에 별다른 검토 없이 그대로 서명할 필요는 없습니다. 입사 전 프로젝트와 입사 후 업무상 만들어지는 결과물은 출발점부터 다릅니다. 특히 프로그램 저작권은 기존 권리가 누구에게 있었는지와 계약을 통해 그 권리를 실제로 회사에 넘기기로 했는지를 따로 봐야 합니다. 따라서 가장 현실적인 방법은 입사 전 프로젝트를 Background IP 또는 Prior Inventions 로 별도 목록화하고, 해당 프로젝트와 기존 소스코드에 관한 권리는 개발자에게 유보된다는 예외조항을 두는 것입니다. 핵심은 간단합니다. 회사에서 일하면서 만든 것과 회사에 들어오기 전에 이미 가지고 있던 것을 같은 디렉터리에 넣으면 안 됩니다. 1. 문제 정의: Problem Statement 개발자 A가 새로운 회사에 입사하면서 근로계약서와 비밀유지계약서, 지식재산권 약정서를 함께 받았다고 가정해보겠습니다. 계약서에 이런 취지의 내용이 들어 있습니다. 근로자가 보유하거나 개발한 프로그램, 소스코드, 아이디어, 발명 및 기타 지식재산권은 회사에 귀속한다. 문장을 조금 더 자세히 읽어보니 입사 후 회사 업무로 개발한 프로그램만 말하는 것이 아닙니다. 입사 전부터 GitHub에서 운영하던 개인 프로젝트나 개인적으로 개발하던 라이브러리까지 회사가 권리를 취득할 수 있는 것처럼 작성되어 있습니다. 이런 경우 개발자가 생각해야 할 질문은 단순히 하나가 아닙니다. 이 계약서가 유효한가? 보다 먼저 다음과 같이 문제를 나누어야 합니다. 1. 프로젝트는 언제 만들어졌는가? 2. 누가 만들었는가? 3. 현재 권리자는 누구인가? 4. 회사의 업무와 관련이 있는가? 5. 회사가 원하는 것은 소유권인가, 사용권인가? 6. 계약서는 기존 권리까지 양도하도록 작성되어 있는가? 7. 입사 후 기존 프로젝트를 계속 개발한다면 그 부분은 어떻게 처리되는가? 개발자식으로 표현하면 이것은 하나의 boolean 문제가 아닙니다. 여러 개의 조건을 확인해야 하는 권리 귀속 로직입니다. 2. 법적 구조 설명: System Architecture 관점 먼저 프로그램은 일반적인 물건과 조금 다릅니다. 노트북을 샀다면 노트북의 소유자는 비교적 쉽게 확인할 수 있습니다. 하지만 프로그램에는 소스코드에 관한 저작권이 존재합니다. 누가 코드를 작성했는지, 회사가 기획하여 업무로 작성된 것인지, 기존 코드가 있었는지, 계약으로 권리를 양도했는지에 따라 결과가 달라집니다. 기본값은 무엇일까? 저작권의 기본 구조에서는 실제로 창작한 사람이 출발점이 됩니다. 다만 회사가 기획하고 회사 업무에 종사하는 사람이 업무상 프로그램을 작성한 경우에는 일정한 요건 아래 회사가 프로그램의 저작자가 될 수 있습니다. 프로그램은 일반적인 업무상저작물과 달리 외부에 공개되었을 것까지 요구되지는 않습니다. 즉, 회사 기획 + 회사 업무 수행 + 업무상 프로그램 작성 = 회사에 권리가 귀속될 가능성이 높은 영역 이라는 구조입니다. 반대로 입사하기 2년 전 개인 노트북으로 혼자 만든 프로그램이라면 이야기가 달라집니다. 회사가 존재하기도 전에 작성했거나, 적어도 그 개발자가 회사에 입사하기 전에 독립적으로 만들어 가지고 있던 프로그램을 두고 단순히 나중에 그 회사에 입사했다는 이유만으로 회사가 처음부터 저작자가 되는 것은 아닙니다. 서버에 입사했다고 해서 과거 커밋의 author가 바뀌는 것은 아닙니다. 그런데 계약서에 서명하면 이야기가 달라질 수 있습니다 여기서 두 번째 레이어가 등장합니다. 저작재산권은 계약을 통해 전부 또는 일부를 다른 사람에게 양도할 수 있습니다. 현행 저작권 제도 역시 저작재산권의 전부 또는 일부 양도를 인정하고 있습니다. 따라서 이런 주장은 위험합니다. “어차피 입사 전에 제가 만든 거니까 계약서에 뭐라고 쓰여 있어도 무조건 제 겁니다.” 항상 그렇지는 않습니다. 원래 개발자에게 있던 권리라고 하더라도 개발자가 이후 계약을 통해 그 권리를 회사에 양도하는 것은 별개의 문제이기 때문입니다. 그래서 계약서를 볼 때는 반드시 두 단계를 나누어야 합니다. Step 1. 현재 이 프로젝트의 권리자는 누구인가? Step 2. 이번 계약을 체결하면서 그 권리를 회사에 넘기는가? 대법원도 프로그램 저작권의 양도 여부가 분명하지 않은 사안에서는 계약 내용과 거래 경위 등을 살펴 권리가 기존 권리자에게 유보되었는지를 판단해 왔습니다. 결국 계약 문구가 중요합니다. 3. 흔히 발생하는 오해: Common Misconceptions 오해 1. 회사 컴퓨터로 만들지 않았으면 무조건 개인 소유다 그렇게 단순하지 않습니다. 장비는 여러 판단 요소 중 하나일 뿐입니다. 회사에서 지급한 맥북을 사용했는지만 가지고 권리 귀속이 결정되는 것은 아닙니다. 누가 개발을 기획했는지, 어떤 업무를 수행하면서 만든 것인지, 근무시간에 개발했는지, 회사의 구체적인 지시가 있었는지 등을 함께 살펴야 합니다. 오해 2. 퇴근 후 집에서 만들었으면 무조건 개인 프로젝트다 이것도 항상 그렇지는 않습니다. 가령 회사가 개발자에게 특정 기능 개발을 맡겼는데 개발자가 집에서 저녁에 코딩했다고 가정해보겠습니다. 저장장소가 개인 PC라는 이유만으로 회사 업무가 개인 프로젝트로 변환되는 것은 아닙니다. location == home 이 곧 personal_project == true 를 의미하지는 않습니다. 오해 3. 입사 전 프로젝트라면 계약서에 서명해도 아무 문제 없다 오히려 이 부분을 가장 조심해야 합니다. 입사 전 프로젝트가 원래 개발자의 것이었다는 것과 그 권리를 계약으로 회사에 넘겼는지는 별개의 문제입니다. 특히 계약서에 다음과 같은 표현이 있다면 범위를 확인할 필요가 있습니다. 과거 및 현재 개발한 모든 프로그램 근로자가 보유하는 일체의 지식재산권 회사 업무와 관련될 수 있는 모든 아이디어 근로기간 전후 개발한 모든 소프트웨어 기존 프로그램 및 그 개량물 일체 범위가 지나치게 넓다면 입사 전 프로젝트까지 포함되는지 반드시 확인해야 합니다. 오해 4. 발명도 프로그램 저작권과 똑같이 보면 된다 그렇지 않습니다. 특허 등이 문제되는 직무발명 은 별도의 구조가 존재합니다. 현행 제도는 회사 업무 범위에 속하고 직원의 현재 또는 과거 직무에 해당하는 발명을 직무발명으로 다루면서, 직무발명에 대해서는 회사가 일정한 절차와 규정에 따라 권리를 승계할 수 있도록 하고 있습니다. 반대로 직무발명이 아닌 발명까지 앞으로 만들어지는 순간 무조건 회사가 가져간다는 식의 사전 포괄승계 조항은 제한됩니다. 대법원도 직무발명 이외의 발명에 대한 사전 승계 부분은 효력이 없다는 취지로 판단하고 있습니다. 다만 이것을 근거로 프로그램 저작권까지 무조건 같은 결론이라고 생각해서는 안 됩니다. 특허·발명 모듈 과 프로그램 저작권 모듈 은 서로 다른 규칙으로 돌아갑니다. 4. 케이스 분기: if / else로 보면 의외로 단순합니다 대략적인 구조를 의사코드로 정리해보겠습니다. if 프로젝트가 입사 전에 이미 완성되어 있었다: 기존 권리자가 누구인지 확인 공동개발자 존재 여부 확인 오픈소스 및 제3자 권리 확인 if 계약서가 기존 프로젝트의 권리까지 회사에 양도한다고 명확히 규정: 수정 또는 제외조항 협의 필요 else: 기존 권리 유지 가능성 검토 else if 입사 후 새롭게 개발했다: if 회사가 기획했고 + 담당업무로 작성했다: 회사 권리 영역인지 검토 else if 완전히 개인적인 프로젝트이고 회사 업무와 무관하고 회사 자원도 사용하지 않았다: 개인 권리 영역인지 검토 else: 경계영역 계약서 + 실제 개발과정 + 업무관련성 함께 검토 문제는 현실의 프로젝트가 이렇게 깔끔하게 분리되지 않는다는 점입니다. 가장 골치 아픈 경우가 있습니다. 입사 전 개인 프로젝트 ↓ 입사 ↓ 기존 코드를 조금 수정 ↓ 회사 프로젝트에서 활용 ↓ 회사 요구사항을 추가 구현 ↓ 개인 프로젝트에도 다시 반영 이렇게 되면 Git history가 갑자기 법적 증거목록처럼 보이기 시작합니다. 어디까지가 기존 코드이고 어디부터가 입사 후 개발된 부분인지가 중요해지기 때문입니다. 5. 특히 위험한 케이스: 개인 라이브러리를 회사 제품에 넣는 경우 개발자들에게 실제로 자주 발생할 수 있는 문제입니다. 개발자 B가 입사하기 전부터 개인적으로 만든 라이브러리 awesome-utils 를 가지고 있다고 가정해보겠습니다. 회사에서도 비슷한 기능이 필요합니다. B는 이렇게 생각합니다. “제가 이미 만들어둔 게 있으니까 그냥 가져다 쓰면 되겠네요.” 개발 효율성 측면에서는 맞습니다. 권리관계 측면에서는 갑자기 dependency가 하나 늘어납니다. 이때는 적어도 다음을 정리해야 합니다. 기존 awesome-utils의 저작권 → 개발자 회사가 사용할 수 있는 범위 → 별도 라이선스 회사 요구로 새로 작성한 코드 → 계약에 따라 판단 기존 프로젝트에 다시 반영할 수 있는지 → 별도 확인 회사 입장에서도 기존 프로젝트 전체를 굳이 소유할 필요가 없는 경우가 많습니다. 회사가 필요한 것은 해당 라이브러리를 자사 제품에서 안정적으로 사용할 권리일 수 있습니다. 그렇다면 소유권 이전보다 사용허락 으로 해결하는 것이 시스템 구조상 훨씬 깔끔합니다. 6. 오픈소스가 섞여 있다면 더 복잡해집니다 개인 프로젝트라고 해서 모든 코드가 개발자 자신의 것은 아닙니다. 오픈소스 라이브러리가 포함될 수도 있고 다른 개발자와 공동으로 작성했을 수도 있습니다. 따라서 계약서에 “해당 프로젝트에 관한 모든 권리를 회사에 양도한다.” 라고 적어 놓는다고 해서 실제로 존재하지 않는 권리까지 회사에 넘길 수 있는 것은 아닙니다. 개발자가 가지고 있는 권리의 범위부터 확인해야 합니다. 내가 작성한 코드 + 공동개발자가 작성한 코드 + 오픈소스 코드 + 외부 API + 상용 SDK 가 하나의 repository에 들어 있다고 해서 모든 권리가 하나의 객체로 합쳐지는 것은 아닙니다. 라이선스 구조를 무시하고 SELECT * FROM intellectual_property 를 실행할 수는 없습니다. 7. 현실적인 대응 전략: Practical Strategy 그렇다면 입사 전에 회사가 이런 계약서를 제시했을 때 어떻게 대응하는 것이 좋을까요. 가장 먼저 계약을 거절할 것인지부터 고민할 필요는 없습니다. 먼저 권리 범위를 정상적으로 설계하면 됩니다. 1단계. 입사 전 프로젝트 목록을 만든다 GitHub repository, 개인 앱, 라이브러리, SaaS, 플러그인, 알고리즘 등 계속 유지할 프로젝트가 있다면 목록을 작성해 두는 것이 좋습니다. 예를 들면 다음과 같습니다. 프로젝트명: ABC Library 최초 개발일: 2024년 Repository: github.com/... 주요 기능: 이미지 처리 라이브러리 권리자: 본인 현재 상태: 개인 프로젝트로 계속 개발 중 이를 계약서 별지에 넣을 수 있습니다. 실무에서는 이런 기존 지식재산을 Background IP , Prior IP , Prior Inventions 등으로 구분하기도 합니다. 2단계. 기존 프로젝트 제외조항을 둔다 계약서는 다음 구조가 훨씬 안전합니다. 입사 전에 독자적으로 개발하거나 보유하고 있던 프로그램, 소스코드 및 기타 지식재산권은 회사에 이전되지 않는다. 해당 기존 지식재산의 목록은 별지와 같다. 그리고 회사에서 기존 프로젝트를 사용할 필요가 있다면 다음 단계를 별도로 설계합니다. Ownership → 개발자에게 유지 License → 필요한 범위에서 회사에 부여 굳이 database ownership까지 migration할 필요 없이 API access만 주면 되는 상황도 있습니다. 3단계. 입사 시점의 상태를 남겨둔다 이것은 개발자에게 특히 쉬운 방법입니다. 입사 전에 다음 자료를 보존해두는 것이 좋습니다. Git commit history repository 생성일 release history package 배포 기록 앱스토어 또는 서비스 출시 기록 개발 관련 문서 기존 소스코드 백업 나중에 분쟁이 생기면 결국 문제가 되는 것은 “이 코드는 언제부터 존재했는가?” 이기 때문입니다. 개발자에게 Git은 버전관리도구이지만, 분쟁이 생기면 훌륭한 타임라인 자료가 되기도 합니다. 4단계. 관련된 모든 것 이라는 문구를 조심한다 계약서에서 가장 위험한 것은 넓고 추상적인 단어입니다. 관련된 파생된 응용된 참고한 활용한 유사한 회사의 사업과 관계있는 예를 들어 회사가 AI 서비스를 한다고 해서 직원이 과거에 개인적으로 만든 모든 AI 프로젝트까지 회사 권리가 된다고 단정할 수는 없습니다. 회사 사업 분야와 비슷하다 와 회사 업무로 개발했다 는 전혀 다른 조건입니다. Boolean 하나 차이 같지만 결과는 상당히 큽니다. 5단계. 입사 후 개인 프로젝트를 계속할 예정이라면 그 부분까지 정한다 입사 전 프로젝트를 제외하는 것만으로 끝나지 않는 경우도 있습니다. 입사 후에도 계속 업데이트할 계획이라면 다음 문제를 미리 정해두는 것이 좋습니다. 개인 시간에 개발 가능 여부 회사 장비 사용 가능 여부 회사 소스코드 사용 금지 회사 영업비밀 사용 금지 회사 프로젝트와 기능이 겹칠 경우 처리방법 기존 프로젝트의 개선물 권리 귀속 처음에는 귀찮아 보입니다. 하지만 장애가 발생한 다음 production에서 hotfix하는 것보다 처음 architecture를 정리하는 것이 훨씬 저렴합니다. 법적 분쟁도 비슷합니다. 8. 그래서 서명해야 할까? 입사 전 개인 프로젝트까지 아무런 구분 없이 회사 소유로 한다는 계약이라면, 일단 그대로 서명하기보다는 어떤 프로젝트와 어떤 권리까지 이전되는지를 먼저 확인하는 것이 좋습니다. 특히 개발자가 계속 운영할 프로젝트가 있다면 계약 단계에서 제외해두는 것이 가장 깔끔합니다. 이미 만들어놓은 프로젝트를 숨길 필요도 없고, 회사와 싸울 필요도 없습니다. 아주 단순하게 말하면 됩니다. 이 프로젝트는 입사 전에 제가 독자적으로 개발한 기존 프로젝트입니다. 회사 업무로 새롭게 작성하는 결과물과는 별도로 기존 프로젝트에 관한 권리는 제게 유보되는 것으로 계약서에 명확히 기재하고 싶습니다. 정상적인 지식재산권 관리라면 이것은 이상한 요구가 아닙니다. 오히려 회사 입장에서도 어떤 코드가 회사 자산이고 어떤 코드가 외부 자산인지 명확해지는 장점이 있습니다. Takeaway 이 문제를 복잡하게 생각할 필요는 없습니다. 권리관계를 세 개의 저장소로 나누면 됩니다. Repository A 입사 전부터 가지고 있던 개인 프로젝트 → 기존 권리 Repository B 회사 기획 + 회사 업무로 새롭게 작성한 결과물 → 회사 권리 여부 검토 Repository C 입사 전 프로젝트를 회사 업무에서 사용하거나 입사 후 계속 수정한 경계영역 → 계약과 라이선스 구조를 별도로 설계 가장 위험한 방식은 이 세 가지를 하나의 폴더에 넣고 계약서에는 모든 지식재산권은 회사 소유 라고 한 줄만 써두는 것입니다. 분쟁이 발생하면 그때부터 사람들은 과거의 commit을 하나씩 열어보기 시작합니다. 좋은 계약서는 분쟁에서 이기기 위한 문서이기 전에, 애초에 어디서 conflict가 발생하는지 미리 정의해놓은 문서입니다. 계약서에 git conflict 표시가 보이기 전에 권리관계부터 merge해두는 편이 낫습니다. 지세훈 변호사 법률상담 안내
어플리케이션 통합 테스트 코드 커버리지 문장 커버리지 - 모든 코드 문장이 1회 이상 실행되었는지 확인 문기 커버리지 - 모든 조건문의 True/False 분기가 수행되었는지 확인한다. 조건 커버리지 - 개별 조건식이 True/False 모두 수행되었는지 확인한다. MC/DC 커버리지 - 각 조건이 결정 결과에 독립적으로 영향을 주는지 확인한다. 테스트 자동화 도구 : 테스트 케이스를 자동으로 실행하고 결과를 기록하는데 사용되는 소프트웨어 chapter 07. 인터페이스 구현 EAI(Enterprise Application Integration) : 기업 내의 가당햔 어플리케이션과 시스템을 통합하는 기술과 방법론 여러 독립적인 시스템 간의 데이터 및 기능을 공유하고, 일관된 비즈니스 프로세스를 지원 가능 Point to Point Hub & Spoke Message Bus Hybrid ESB(Enterprise Service Bus) 다양한 어플리케이션과 서비스를 표준화된 방식으로 통합하는 미들웨어 아키택처 ESB 장애 시 전체 시스템에 영향을 줄 수 있다는 단점이 있다. 1. 인터페이스 형식에 맞춘 데이터 포멧의 종류 포맷 구분 내용 JSON (JavaScript Object Notation) 설명 • 사람이 읽기 쉽고 경량화된 텍스트 포맷이며, (키:값) 구조를 사용하는 데이터 표현 방식이다. • 웹 및 REST API에서 가장 많이 사용되는 데이터 형식이다. 예시 {"name":"hackers","age":25} XML (eXtensible Markup Language) 설명 • 태그 기반으로 데이터의 구조와 의미를 표현하는 마크업 언어이다. • 데이터 구조 표현에 유리하지만 문법이 복잡한 편이다. (SOAP에서 사용) 예시 <name>hackers</name> YAML (YAML Ain't Markup Language) 설명 • 들여쓰기 기반으로 JSON보다 더 간결하게 데이터를 표현하는 포맷이다. • 설정 파일에서 널리 사용되며 가독성이 높은 형식이다. 예시 person: name: hackers age: 25 student: false 2. JSON / XML / YAML 비교 포맷 특징 예시 용도 JSON 경량 텍스트 포맷으로 웹에서 가장 많이 사용된다. REST API 데이터 전송 XML 태그 기반 구조 표현이 뛰어난 포맷이다. 문서 기반 시스템, SOAP YAML 들여쓰기 기반 가독성 높은 설정 파일 포맷이다. Docker, CI/CD 설정 3. 인터페이스 구현 검증 도구의 종류 도구 설명 xUnit • Java(JUnit), C++(CppUnit), .Net(NUnit) 등 다양한 언어를 지원하는 단위 테스트 프레임워크이다. • 각 언어별로 유사한 기능을 제공하여 테스트 케이스 작성, 실행, 결과 보고를 지원한다. FitNesse • 웹 기반 테스트 케이스 설계/실행/결과 확인 등을 지원하는 테스트 프레임워크이다. • 사용자가 이해하기 쉬운 위키 형식의 인터페이스를 제공하여 테스트 케이스를 작성하고 실행할 수 있다. NTAF • Naver 테스트 자동화 프레임워크로, STAF와 FitNesse를 통합하여 테스트 자동화를 지원한다. • 이를 통해 통합적인 테스트 환경을 제공한다. Selenium • 다양한 브라우저 지원 및 개발언어를 지원하는 웹 애플리케이션 테스트 프레임워크이다. • 브라우저 간의 자동화 테스트를 쉽게 수행할 수 있다.
프로그래밍을 처음 배울 때 데이터베이스 관계는 보통 외래 키를 사용해 두 테이블을 연결하는 방법이라고 배운다. CREATE TABLE reservations ( id UUID PRIMARY KEY, customer_id UUID REFERENCES users(id) ); reservations.customer_id 가 users.id 를 참조하므로 예약과 사용자 사이에 관계가 만들어진다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 예약 서비스를 개발하면 테이블을 연결하는 것보다 더 많은 판단이 필요하다. 예약은 반드시 한 명의 사용자에게 속해야 하는가? 회원이 탈퇴하면 예약도 삭제해야 하는가? 한 예약에 여러 참가자가 들어갈 수 있는가? 참가자는 사용자 계정이 없어도 되는가? 예약에 배정된 담당자가 변경되면 과거 기록은 어떻게 남겨야 하는가? 예약과 사용자의 관계를 알면 해당 예약을 수정할 권한도 있다고 볼 수 있는가? 데이터베이스와 코드가 관계의 필수 여부를 동일하게 이해하고 있는가? 관계는 단순히 테이블을 연결하는 기능이 아니다. 관계는 현실 세계의 소유·포함·참조 규칙과 각 데이터의 생명주기를 서비스 안에 표현하는 방법이다. 외래 키를 추가하기 전에 현실의 관계부터 정의해야 한다 크리스가 상담 예약 서비스를 만든다고 생각해 보자. 사용자는 상담 시간을 선택해 예약할 수 있다. 처음에는 예약에 사용자 ID를 추가하는 것만으로 충분해 보인다. type Reservation = { id: string; customerId: string; startsAt: string; }; 이 구조는 예약이 어떤 사용자와 관련되어 있는지 알려준다. 하지만 customerId 라는 필드만으로는 관계의 규칙을 모두 알 수 없다. 예약 하나에는 고객이 정확히 한 명만 존재하는가? 사용자는 여러 예약을 만들 수 있는가? 고객 없이 관리자가 예약을 먼저 만들 수 있는가? 예약이 끝난 후 사용자가 탈퇴해도 예약 기록은 남아야 하는가? 예약을 만든 사람과 실제 참석자가 다를 수 있는가? 코드에 ID 하나를 추가하는 것은 관계의 구현일 뿐이다. 구현 전에 현실에서 두 대상이 어떻게 연결되는지를 정의해야 한다. 현재 서비스의 규칙이 다음과 같다고 가정해 보자. 사용자는 여러 예약을 만들 수 있다. 각 예약은 반드시 한 명의 사용자에게 속한다. 같은 예약이 여러 사용자에게 동시에 소유되지는 않는다. 이는 사용자와 예약 사이의 일대다 관계다. CREATE TABLE users ( id UUID PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE reservations ( id UUID PRIMARY KEY, customer_id UUID NOT NULL REFERENCES users(id), starts_at TIMESTAMPTZ NOT NULL ); NOT NULL 은 예약에 고객이 반드시 필요하다는 규칙을 표현한다. 외래 키는 존재하지 않는 사용자를 참조하지 못하게 한다. 이 관계는 단순히 두 테이블이 연결되었다는 사실보다 더 많은 의미를 가진다. 사용자 한 명은 여러 예약을 가질 수 있다. 예약 하나는 정확히 한 명의 사용자에게 속한다. 예약은 존재하는 사용자만 참조할 수 있다. 고객이 없는 예약은 현재 비즈니스 규칙상 유효하지 않다. 관계의 종류를 고르는 일은 데이터베이스 문법을 선택하는 일이 아니라 현실의 규칙을 명확하게 만드는 일이다. 관계의 수는 현재 화면이 아니라 비즈니스 규칙에서 결정된다 예약 상세 화면에 담당 상담사 한 명만 표시된다고 생각해 보자. type ReservationDetails = { consultantName: string; }; 현재 화면만 보면 예약과 상담사의 관계는 일대일처럼 보인다. 그러나 다음 요구사항이 추가될 수 있다. 담당 상담사가 휴가를 가면 다른 상담사에게 예약을 재배정한다. 한 상담사가 여러 예약을 담당한다. 공동 상담에서는 상담사 두 명이 같은 예약에 참여한다. 과거에 담당했던 상담사도 이력에 남긴다. 화면에 한 명만 보인다는 이유로 데이터 관계를 일대일로 정하면 이후 요구사항을 표현하기 어렵다. 먼저 현재 비즈니스 규칙이 “예약 하나에는 현재 담당 상담사 한 명이 배정되고, 상담사 한 명은 여러 예약을 담당할 수 있다”라고 정의되었다면 다음처럼 표현할 수 있다. ALTER TABLE reservations ADD COLUMN consultant_id UUID REFERENCES consultants(id); consultant_id 에 NOT NULL 이 없으므로 상담사 배정 전에도 예약을 만들 수 있다. 이는 관계의 선택 가능성을 데이터베이스에 표현한 것이다. TypeScript 타입도 같은 규칙을 따라야 한다. type Reservation = { id: string; customerId: string; consultantId: string | null; startsAt: string; }; string | null 은 아직 상담사가 배정되지 않은 상태가 정상적으로 존재할 수 있다는 의미다. 다음처럼 타입과 데이터베이스가 서로 다른 규칙을 표현하면 문제가 생긴다. type Reservation = { consultantId: string; }; 코드는 상담사가 항상 있다고 가정하지만 데이터베이스에는 NULL 이 들어갈 수 있다. 결국 조회 결과를 처리하는 과정에서 예상하지 못한 오류가 발생한다. 관계의 수와 필수 여부는 현재 UI 모양이 아니라 서비스가 허용하는 상태를 기준으로 결정해야 한다. 소유 관계와 참조 관계는 같은 외래 키로 보여도 의미가 다르다 예약은 사용자와 상담사를 모두 참조한다. CREATE TABLE reservations ( id UUID PRIMARY KEY, customer_id UUID NOT NULL REFERENCES users(id), consultant_id UUID REFERENCES consultants(id), starts_at TIMESTAMPTZ NOT NULL ); 형식만 보면 두 필드 모두 외래 키다. 하지만 관계의 의미는 다르다. customer_id 는 예약의 소유 주체를 나타낸다. consultant_id 는 현재 예약에 배정된 담당자를 나타낸다. 소유 관계는 보통 접근 권한, 조회 범위, 데이터 생명주기와 연결된다. 참조 관계는 다른 데이터에 대한 연결만 나타낼 수 있다. 예를 들어 고객이 자신의 예약을 조회할 때는 소유 관계를 사용한다. async function getCustomerReservation( reservationId: string, currentUserId: string ) { return reservationRepository.findOne({ id: reservationId, customerId: currentUserId, }); } 이 코드는 예약 ID뿐 아니라 현재 사용자 ID도 조회 조건에 포함한다. 예약이 존재하더라도 다른 사용자가 소유한 예약이라면 반환하지 않는다. 반면 상담사 정보는 예약 화면에 담당자를 표시하기 위해 참조할 수 있다. const reservation = await reservationRepository.findById(reservationId); const consultant = reservation.consultantId ? await consultantRepository.findById( reservation.consultantId ) : null; 상담사를 참조한다는 사실만으로 현재 사용자가 예약을 수정할 권한까지 얻는 것은 아니다. 같은 외래 키 구조라도 관계가 표현하는 책임은 다르다. 필드 이름과 도메인 로직에서 그 차이가 드러나야 한다. 포함되는 데이터는 부모와 생명주기를 함께할 수 있다 예약에는 고객이 작성한 특별 요청 사항이 여러 개 포함될 수 있다. CREATE TABLE reservation_requests ( id UUID PRIMARY KEY, reservation_id UUID NOT NULL REFERENCES reservations(id), request_type TEXT NOT NULL, details TEXT NOT NULL ); 특별 요청 사항은 독립적으로 존재하기 어렵다. 어떤 예약에도 속하지 않는 요청 사항은 서비스에서 의미가 없을 수 있다. 이때 예약이 삭제되면 요청 사항도 함께 삭제하도록 설정할 수 있다. CREATE TABLE reservation_requests ( id UUID PRIMARY KEY, reservation_id UUID NOT NULL REFERENCES reservations(id) ON DELETE CASCADE, request_type TEXT NOT NULL, details TEXT NOT NULL ); ON DELETE CASCADE 는 편리한 삭제 옵션이기 전에 생명주기 규칙이다. 예약 요청 사항은 예약에 포함되며, 예약이 사라지면 독립적으로 유지되지 않는다. 그러나 같은 설정을 모든 관계에 적용하면 안 된다. customer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE 이 설정은 사용자가 삭제될 때 해당 사용자의 예약까지 모두 삭제할 수 있다. 이미 완료된 상담 기록이나 정산 자료를 보존해야 한다면 위험한 정책이다. 이 경우에는 삭제를 제한하거나, 사용자와 예약 기록을 분리해 보존하는 방법을 고려해야 한다. customer_id UUID REFERENCES users(id) ON DELETE SET NULL 다만 고객 ID를 NULL 로 바꾸는 것만으로 요구사항이 해결되지는 않는다. 예약 당시의 고객 이름이나 연락처를 법적·운영상 보존해야 한다면 별도의 스냅숏 또는 익명화 정책이 필요하다. 삭제 정책은 다음 질문에서 출발해야 한다. 자식 데이터는 부모 없이 의미가 있는가? 부모가 삭제되어도 거래나 운영 기록을 보존해야 하는가? 개인정보를 제거하면서 비식별 기록은 유지할 수 있는가? 연결을 제거할 것인가, 삭제를 막을 것인가, 함께 삭제할 것인가? 관계는 현재 데이터의 연결뿐 아니라 한쪽 데이터가 사라질 때 다른 데이터가 어떻게 되는지도 정의한다. 다대다 관계에는 연결 자체의 의미가 숨어 있다 처음에는 예약 하나에 사용자 한 명만 참석한다고 가정할 수 있다. 이후 단체 예약 기능이 추가되면 한 예약에 여러 참가자가 참여하고, 한 사용자는 여러 예약에 참여할 수 있다. 배열 필드 하나로 참가자를 저장하고 싶을 수 있다. CREATE TABLE reservations ( id UUID PRIMARY KEY, participant_ids UUID[] NOT NULL ); 이 구조는 ID 목록을 저장할 수 있지만 데이터베이스가 각 참가자의 존재 여부를 외래 키로 보장하기 어렵다. 참가자마다 다른 상태나 역할을 저장하기도 어렵다. 연결 테이블을 사용하면 관계를 독립적으로 표현할 수 있다. CREATE TABLE reservation_participants ( reservation_id UUID NOT NULL REFERENCES reservations(id), user_id UUID NOT NULL REFERENCES users(id), role TEXT NOT NULL, status TEXT NOT NULL, joined_at TIMESTAMPTZ, PRIMARY KEY (reservation_id, user_id) ); 이 테이블은 예약과 사용자를 연결할 뿐 아니라 연결 자체의 정보도 저장한다. role 은 예약자, 일반 참가자, 보호자 같은 역할을 나타낸다. status 는 초대됨, 수락함, 거절함 같은 참여 상태를 나타낸다. joined_at 은 실제 참여 시점을 나타낸다. 관계가 자체 속성을 가지기 시작하면 단순한 연결선이 아니라 도메인의 중요한 데이터가 된다. TypeScript에서도 연결 정보를 명시적으로 표현하는 편이 낫다. type ReservationParticipant = { reservationId: string; userId: string; role: "organizer" | "attendee" | "guardian"; status: "invited" | "accepted" | "declined"; joinedAt: string | null; }; 이 타입은 “누가 어떤 예약과 연결되어 있는가”뿐 아니라 “어떤 방식으로 연결되어 있는가”까지 설명한다. 다대다 관계를 발견했을 때는 두 테이블을 어떻게 연결할지만 묻지 않아야 한다. 그 관계에 이름, 상태, 역할, 생성 시점이 필요한지도 함께 확인해야 한다. 중복 관계는 애플리케이션 코드만으로 막기 어렵다 크리스가 참가자 추가 기능을 구현한다고 생각해 보자. const existingParticipant = await participantRepository.findOne({ reservationId, userId, }); if (!existingParticipant) { await participantRepository.create({ reservationId, userId, role: "attendee", status: "invited", }); } 코드는 이미 참가자가 있는지 확인한 뒤 새로운 관계를 만든다. 하지만 거의 동시에 두 요청이 실행되면 두 요청 모두 기존 참가자가 없다고 판단할 수 있다. 관계의 고유성은 데이터베이스에서도 보장해야 한다. CREATE TABLE reservation_participants ( reservation_id UUID NOT NULL REFERENCES reservations(id), user_id UUID NOT NULL REFERENCES users(id), role TEXT NOT NULL, status TEXT NOT NULL, PRIMARY KEY (reservation_id, user_id) ); 복합 기본 키는 같은 사용자가 같은 예약에 두 번 등록되지 못하도록 한다. 만약 한 사용자가 같은 예약에서 여러 역할을 가질 수 있다면 고유성 규칙도 달라진다. UNIQUE (reservation_id, user_id, role) 어떤 제약 조건이 맞는지는 SQL 문법이 아니라 비즈니스 규칙에 따라 결정된다. 한 사용자는 예약당 하나의 참가자 레코드만 가질 수 있는가? 한 사람이 보호자와 예약자를 동시에 맡을 수 있는가? 거절한 초대를 다시 생성할 것인가, 기존 상태를 변경할 것인가? 취소된 관계도 이력으로 보존해야 하는가? 관계의 중복 여부도 “무엇을 같은 관계로 볼 것인가”를 정의해야 판단할 수 있다. 관계를 변경하는 것은 ID 하나를 바꾸는 작업이 아니다 예약의 상담사를 변경하는 코드는 단순해 보인다. await reservationRepository.update(reservationId, { consultantId: newConsultantId, }); 그러나 실제 서비스에서는 관계 변경이 다른 규칙과 연결될 수 있다. 새 상담사가 해당 시간에 근무하는가? 이미 다른 예약을 담당하고 있지 않은가? 해당 상담 종류를 처리할 자격이 있는가? 기존 상담사에게 알림을 보내야 하는가? 재배정 이력을 남겨야 하는가? 따라서 외부에서 받은 ID를 바로 저장해서는 안 된다. 외부 입력은 검증 전까지 신뢰할 수 없다. import { z } from "zod"; const AssignConsultantSchema = z.object({ consultantId: z.string().uuid(), }); const input = AssignConsultantSchema.parse( request.body ); 형식 검증 후에는 실제 관계를 만들 수 있는지도 확인해야 한다. async function assignConsultant( reservationId: string, consultantId: string ) { const reservation = await reservationRepository.findById( reservationId ); if (!reservation) { throw new ReservationNotFoundError(); } const consultant = await consultantRepository.findById( consultantId ); if (!consultant) { throw new ConsultantNotFoundError(); } const canAccept = await scheduleService.canAcceptReservation({ consultantId, startsAt: reservation.startsAt, durationMinutes: reservation.durationMinutes, }); if (!canAccept) { throw new ConsultantUnavailableError(); } await reservationRepository.assignConsultant({ reservationId, consultantId, }); } 이 코드는 두 데이터가 존재하는지만 확인하지 않는다. 현재 시간과 업무 규칙 안에서 유효한 관계인지도 확인한다. 관계 변경이 중요한 이력이라면 현재 외래 키만 저장해서는 과거를 알 수 없다. CREATE TABLE consultant_assignments ( id UUID PRIMARY KEY, reservation_id UUID NOT NULL REFERENCES reservations(id), consultant_id UUID NOT NULL REFERENCES consultants(id), assigned_at TIMESTAMPTZ NOT NULL, unassigned_at TIMESTAMPTZ ); 현재 관계와 관계의 변경 이력은 서로 다른 질문에 답한다. 현재 담당자는 누구인가? 처음 배정된 담당자는 누구였는가? 언제 재배정되었는가? 특정 상담사가 담당했던 예약은 무엇인가? 서비스가 과거의 관계까지 알아야 한다면 단순한 외래 키 변경보다 명시적인 관계 이력이 필요하다. 관계는 데이터 접근 권한을 자동으로 부여하지 않는다 사용자가 예약의 참가자로 등록되어 있다고 해서 모든 행동을 할 수 있는 것은 아니다. const participant = await participantRepository.findOne({ reservationId, userId: currentUser.id, }); 이 관계는 현재 사용자가 예약에 참여한다는 사실을 알려준다. 하지만 예약 취소, 참가자 초대, 상담사 변경 권한까지 자동으로 의미하지는 않는다. 역할과 행동을 함께 확인해야 한다. const canCancelReservation = participant?.role === "organizer" && participant.status === "accepted"; if (!canCancelReservation) { throw new ForbiddenError(); } 관리자나 담당 상담사에게 별도의 권한이 있다면 정책을 한곳에서 관리할 수 있다. const canManageReservation = authorizationService.can({ actor: currentUser, action: "manage", resource: reservation, participant, }); 관계는 권한 판단에 필요한 정보가 될 수 있지만 권한 그 자체는 아니다. 소유자는 어떤 행동을 할 수 있는가? 참가자는 어떤 정보까지 볼 수 있는가? 초대 상태인 사용자가 예약 상세를 조회할 수 있는가? 담당 상담사는 고객 연락처를 볼 수 있는가? 관계가 종료된 뒤에도 과거 기록에 접근할 수 있는가? ID가 존재한다고 접근 권한이 증명되지 않는 것처럼, 두 데이터가 연결되어 있다는 사실만으로 허용되는 행동이 결정되지는 않는다. 관계를 조회하는 방식도 서비스 성능에 영향을 준다 예약 목록에 고객과 상담사 정보를 함께 표시하기 위해 각 예약마다 추가 조회를 실행할 수 있다. const reservations = await reservationRepository.findUpcoming(); for (const reservation of reservations) { reservation.customer = await userRepository.findById( reservation.customerId ); reservation.consultant = reservation.consultantId ? await co