현재 공시 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