문법만 알고 프로그램을 만들면 곧 세 가지 현실적인 문제를 만나게 된다. 예상하지 못한 입력이나 오류가 발생한다. 여러 작업을 동시에 처리해야 한다. 콘솔이나 파일처럼 프로그램 외부와 데이터를 주고받아야 한다. Java에서는 각각 예외 처리 , 스레드 , 입출력 API 로 대응한다. 1. 예외는 프로그램 실행 중 발생하는 문제 상황이다 예를 들어 존재하지 않는 배열 인덱스에 접근하면 예외가 발생한다. int[] values = {10, 20, 30}; System.out.println(values[5]); 프로그램에서 예상 가능한 문제를 처리하려면 try-catch 를 사용한다. try { int[] values = {10, 20, 30}; System.out.println(values[5]); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("잘못된 인덱스입니다."); } 가능하면 무조건 Exception 하나로 잡기보다 처리할 수 있는 구체적인 예외 를 잡는 편이 의도가 명확하다. 2. 여러 catch 로 예외 종류를 나누기 try { int page = Integer.parseInt(input); System.out.println(values[page]); } catch (NumberFormatException e) { System.out.println("숫자 형식이 아닙니다."); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("범위를 벗어났습니다."); } 예외 종류에 따라 다른 복구 전략을 적용할 수 있다. 3. throw : 의도적으로 예외 발생시키기 잘못된 상태를 그냥 통과시키지 않고 예외로 표현할 수 있다. public static void validateAmount(int amount) { if (amount < 0) { throw new IllegalArgumentException("금액은 0 이상이어야 합니다."); } } 이렇게 하면 호출하는 쪽에서 오류를 명확하게 인식할 수 있다. 4. finally : 성공해도 실패해도 실행 try { System.out.println("작업 시작"); } catch (Exception e) { System.out.println("문제 발생"); } finally { System.out.println("작업 종료"); } finally 는 예외 발생 여부와 관계없이 실행된다. 과거에는 파일이나 네트워크 자원을 닫는 데 많이 사용했지만, 닫을 수 있는 자원은 try-with-resources 가 더 안전한 경우가 많다. 5. try-with-resources AutoCloseable 을 구현한 자원은 블록이 끝날 때 자동으로 닫힌다. import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; try (BufferedWriter writer = new BufferedWriter(new FileWriter("app.log"))) { writer.write("application started"); } catch (IOException e) { System.out.println("파일 저장 실패"); } 정상 종료뿐 아니라 예외가 발생한 경우에도 close() 가 호출되므로 자원 누수를 줄일 수 있다. 6. 사용자 정의 예외 도메인에 맞는 오류 의미를 표현하고 싶다면 직접 예외 클래스를 만들 수 있다. class InsufficientStockException extends Exception { public InsufficientStockException(String message) { super(message); } } public static void validateStock(int stock, int quantity) throws InsufficientStockException { if (stock < quantity) { throw new InsufficientStockException("재고가 부족합니다."); } } 7. throws : 예외 처리를 호출한 곳으로 넘기기 public static String loadConfig() throws IOException { // 파일 읽기 return "config"; } 메소드가 직접 해결하기 어려운 예외를 상위 호출자에게 맡길 수 있다. throw 는 예외를 실제로 발생시키는 것 , throws 는 이 메소드가 예외를 바깥으로 전달할 수 있음을 선언하는 것 이다. 8. 스레드란? 스레드는 하나의 프로세스 안에서 실행되는 작업 흐름이다. 여러 스레드를 사용하면 여러 작업이 동시성(concurrency) 을 가지게 할 수 있다. 여러 CPU 코어에서 실제로 동시에 실행될 수도 있지만, 스레드를 만들었다고 항상 물리적으로 병렬 실행되는 것은 아니다. 스케줄러와 실행 환경에 따라 달라진다. Thread 상속 class LogThread extends Thread { @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println("log " + i); } } } Thread thread = new LogThread(); thread.start(); run() 을 직접 호출하는 것과 start() 는 다르다. 새 스레드에서 실행하려면 start() 를 호출해야 한다. 9. Runnable 구현 동작을 객체로 분리하려면 Runnable 을 사용할 수 있다. Runnable task = () -> { for (int i = 1; i <= 3; i++) { System.out.println("task " + i); } }; Thread thread = new Thread(task); thread.start(); Thread 를 직접 상속하지 않으므로 다른 클래스를 상속해야 하는 경우에도 사용할 수 있다. 실무에서는 낮은 수준의 Thread 를 직접 많이 만들기보다 Executor 계열 API나 가상 스레드 같은 더 높은 수준의 동시성 도구를 사용하는 경우가 많지만, Thread 와 Runnable 의 기본 원리를 이해하면 이후 개념을 익히기 쉽다. 10. join() : 다른 스레드가 끝날 때까지 기다리기 Thread worker = new Thread(() -> { System.out.println("백그라운드 작업"); }); worker.start(); worker.join(); System.out.println("작업 완료 후 실행"); join() 은 대상 스레드가 종료될 때까지 현재 스레드를 기다리게 한다. InterruptedException 처리가 필요할 수 있다. 11. 공유 데이터와 경쟁 상태 여러 스레드가 같은 값을 동시에 수정하면 예상하지 못한 결과가 나올 수 있다. class Counter { int value = 0; void increment() { value++; } } value++ 는 겉보기와 달리 읽기 → 증가 → 쓰기 과정이므로 여러 스레드가 동시에 실행하면 증가가 유실될 수 있다. 12. synchronized 로 임계 구역 보호하기 class Counter { private int value = 0; public synchronized void increment() { value++; } public int getValue() { return value; } } 한 번에 하나의 스레드만 increment() 의 임계 구역에 들어가게 해 경쟁 상태를 줄인다. 블록 단위로 특정 객체를 락으로 사용할 수도 있다. synchronized (lock) { // 공유 자원 접근 } 동기화는 안전성을 높이지만 락 경쟁이 많으면 성능에 영향을 줄 수 있다. 필요한 범위를 작게 잡는 것이 중요하다. 13. Scanner 로 콘솔 입력 받기 import java.util.Scanner; Scanner scanner = new Scanner(System.in); System.out.print("페이지 번호: "); int page = scanner.nextInt(); System.out.println("입력된 페이지: " + page); 대표 메소드는 다음과 같다. next() : 공백 전까지 한 단어 nextInt() : 정수 nextDouble() : 실수 nextLine() : 한 줄 전체 nextInt() 뒤에 바로 nextLine() 을 사용하면 남아 있던 줄바꿈 문자를 읽어 빈 문자열처럼 보일 수 있다. 이 동작은 콘솔 입력을 처음 다룰 때 자주 헷갈린다. 14. print , println , printf System.out.print("A"); System.out.println("B"); System.out.printf("status=%d, path=%s%n", 200, "/api/users"); printf 에서 자주 사용하는 형식 지정자는 다음과 같다. %d : 정수 %f : 실수 %s : 문자열 %n : 플랫폼에 맞는 줄바꿈 System.out.printf("%,d%n", 1234567); // 1,234,567 System.out.printf("%.2f%n", 3.14159); // 3.14 System.out.printf("%04d%n", 12); // 0012 15. 파일과 폴더 다루기 전통적인 File API로 존재 여부와 경로 정보 등을 다룰 수 있다. import java.io.File; File file = new File("app.log"); System.out.println(file.exists()); System.out.println(file.getName()); System.out.println(file.getAbsolutePath()); System.out.println(file.length()); File folder = new File("logs"); folder.mkdir(); 현대 Java에서는 java.nio.file.Path 와 Files API도 많이 사용한다. import java.nio.file.Files; import java.nio.file.Path; Path path = Path.of("logs", "app.log"); Files.createDirectories(path.getParent()); 16. 파일 쓰기와 읽기 쓰기 import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; try (BufferedWriter writer = new BufferedWriter(new FileWriter("app.log"))) { writer.write("server started"); writer.newLine(); writer.write("port=8080"); } catch (IOException e) { System.out.println("파일 쓰기 실패"); } 읽기 import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; try (BufferedReader reader = new BufferedReader(new FileReader("app.log"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.out.println("파일 읽기 실패"); } readLine() 이 더 읽을 내용이 없으면 null 을 반환하므로 while 문으로 파일 끝까지 읽을 수 있다. 핵심 정리 try-catch 로 실행 중 발생 가능한 예외를 처리한다. throw 는 예외를 발생시키고, throws 는 예외를 호출자에게 전달할 수 있음을 선언한다. 닫아야 하는 자원은 try-with-resources 를 사용하면 안전하다. Thread.start() 는 새 스레드에서 run() 을 실행한다. join() 은 다른 스레드가 끝날 때까지 기다린다. 공유 데이터를 여러 스레드가 수정하면 경쟁 상태가 발생할 수 있다. synchronized 는 임계 구역의 동시 접근을 제어한다. Scanner , printf , 파일 API를 이용해 프로그램 외부와 데이터를 주고받을 수 있다.
프로그램이 조금만 커져도 변수를 하나씩 만들고 같은 코드를 반복해서 작성하는 방식은 금방 한계가 온다. Java에서는 배열 로 같은 타입의 데이터를 묶고, 메소드 로 반복되는 동작을 하나의 기능 단위로 나눌 수 있다. 1. 배열이 필요한 이유 상품 가격이 다섯 개 있다고 해보자. int price1 = 12000; int price2 = 18000; int price3 = 9500; int price4 = 22000; int price5 = 15000; 값이 늘어날수록 관리가 어려워진다. 같은 자료형의 값을 하나로 묶으면 훨씬 다루기 쉽다. int[] prices = {12000, 18000, 9500, 22000, 15000}; 2. 배열 만들기 크기를 먼저 정할 수 있다. int[] statusCodes = new int[3]; 각 요소는 인덱스로 접근한다. 인덱스는 0부터 시작한다. statusCodes[0] = 200; statusCodes[1] = 404; statusCodes[2] = 500; 배열의 길이는 length 로 확인한다. System.out.println(statusCodes.length); // 3 범위를 벗어난 인덱스에 접근하면 ArrayIndexOutOfBoundsException 이 발생한다. System.out.println(statusCodes[3]); // 예외 3. 배열 초기화 방법 String[] environments = new String[] { "dev", "staging", "production" }; 선언과 동시에 값을 넣는다면 new String[] 을 생략할 수 있다. String[] environments = {"dev", "staging", "production"}; 일반적으로 String environments[] 보다 String[] environments 형태가 자료형을 더 명확하게 보여주므로 많이 사용한다. 4. 배열 순회 인덱스가 필요할 때 int[] latencies = {120, 85, 210}; for (int i = 0; i < latencies.length; i++) { System.out.println(i + ": " + latencies[i]); } 값만 필요할 때 향상된 for 문을 사용할 수 있다. for (int latency : latencies) { System.out.println(latency); } 인덱스를 직접 수정하거나 위치 정보가 필요한 작업이 아니라면 향상된 for 문이 간결하다. 5. 2차원 배열 행과 열처럼 데이터를 구성할 때 사용할 수 있다. int[][] traffic = { {120, 130, 140}, {200, 210, 220} }; System.out.println(traffic[0][1]); // 130 중첩 반복문으로 순회할 수 있다. for (int row = 0; row < traffic.length; row++) { for (int col = 0; col < traffic[row].length; col++) { System.out.println(traffic[row][col]); } } 각 행마다 길이가 다를 수도 있으므로 안쪽 반복문의 조건은 traffic[row].length 처럼 작성하는 것이 안전하다. 6. 문자와 숫자의 관계 Java의 char 는 숫자 값으로도 표현할 수 있다. char upperA = 'A'; int code = upperA; System.out.println(code); // 65 ASCII 기준으로 숫자 '0' 은 48, 대문자 'A' 는 65, 소문자 'a' 는 97부터 시작한다. 하지만 Java 문자열 전체를 ASCII로만 이해하면 안 된다. Java는 유니코드를 다루며 char 는 UTF-16 코드 단위를 표현한다. 7. 메소드는 기능을 이름 붙인 코드 묶음이다 같은 코드를 여러 번 사용해야 한다면 메소드로 분리할 수 있다. public static void printSeparator() { System.out.println("----------------"); } public static void main(String[] args) { printSeparator(); System.out.println("사용자 목록"); printSeparator(); } 메소드를 사용하면 중복이 줄고 코드의 의도가 이름으로 드러난다. 8. 매개변수와 인자 메소드가 외부 값을 받도록 만들 수 있다. public static void printUser(String name) { System.out.println("user=" + name); } printUser("kim"); 엄밀히 구분하면 메소드 선언부의 name 은 매개변수(parameter) , 호출할 때 전달한 "kim" 은 인자(argument) 다. 여러 값을 받을 수도 있다. public static void printOrder(String orderId, int amount) { System.out.println(orderId + ": " + amount); } 9. 반환값 계산 결과를 호출한 곳으로 돌려주려면 반환형과 return 을 사용한다. public static int calculateTotal(int price, int quantity) { return price * quantity; } int total = calculateTotal(15000, 3); System.out.println(total); // 45000 반환값이 없는 메소드는 void 를 사용한다. public static void log(String message) { System.out.println(message); } 10. 메소드 오버로딩 같은 이름이지만 매개변수의 개수나 타입이 다른 메소드를 여러 개 정의할 수 있다. public static int add(int a, int b) { return a + b; } public static int add(int a, int b, int c) { return a + b + c; } public static double add(double a, double b) { return a + b; } 반환형만 다르게 만드는 것은 오버로딩이 아니다. // 컴파일 불가 int parse(String value) { ... } double parse(String value) { ... } 호출 시 어떤 메소드를 사용할지 매개변수 목록으로 구분할 수 있어야 한다. 11. 메소드를 쓰면 무엇이 좋아질까? 예를 들어 주문 금액 검증이 여러 위치에서 반복된다고 해보자. if (amount < 0) { System.out.println("잘못된 금액"); } 이를 메소드로 분리하면 규칙을 한곳에서 관리할 수 있다. public static boolean isValidAmount(int amount) { return amount >= 0; } if (!isValidAmount(amount)) { System.out.println("잘못된 금액"); } 이렇게 하면 코드가 무엇을 검사하는지 더 잘 보이고, 규칙이 바뀌어도 메소드 하나만 수정하면 된다. 12. 변수의 범위: Scope 메소드나 블록 안에서 선언한 지역 변수는 해당 범위를 벗어나면 사용할 수 없다. public static void main(String[] args) { int count = 10; if (count > 0) { String message = "데이터 있음"; System.out.println(message); } // System.out.println(message); // 사용 불가 } 지역 변수의 범위를 필요 이상으로 넓게 잡지 않으면 코드의 상태를 추적하기 쉬워진다. 13. main 메소드의 역할 전통적인 Java 애플리케이션의 시작점은 다음 메소드다. public static void main(String[] args) { // 프로그램 시작 } public : JVM이 외부에서 호출할 수 있음 static : 객체를 만들지 않고 실행 가능 void : 반환값 없음 String[] args : 실행 시 전달받는 문자열 인자 명령행 인자를 사용할 수도 있다. public static void main(String[] args) { if (args.length > 0) { System.out.println("mode=" + args[0]); } } 핵심 정리 배열은 같은 자료형의 여러 값을 하나의 변수로 관리한다. 배열 인덱스는 0부터 시작하고 길이는 array.length 로 확인한다. 인덱스가 필요하면 일반 for , 값만 필요하면 향상된 for 가 편리하다. 2차원 배열은 array[row][col] 형태로 접근한다. 메소드는 특정 기능을 수행하는 코드 묶음이다. 매개변수로 값을 받고 return 으로 결과를 반환할 수 있다. 오버로딩은 같은 이름과 서로 다른 매개변수 목록을 가진 메소드를 정의하는 것이다. 지역 변수는 선언된 블록 안에서만 사용할 수 있다.
8월 8일, 베이징 부동산 시장에 또 하나의 주목할 만한 조정이 시행되었습니다. 새로운 규정에 따르면, 베이징 호적이 아닌 가족이 상품 주택을 구매할 때 필요한 사회보험 또는 개인소득세 납부 기간이 단축되었습니다. 5환 이내 주택의 경우 기존 2년에서 1년으로 줄어들었고, 현재 베이징 전역에서 이 기간은 통일적으로 1년입니다. 베이징에 비교적 최근에 이주한 많은 젊은이들에게 이 변화는 꽤 구체적인 의미를 가집니다. 과거에는 안정적인 직장과 정기적인 납부 기록이 있어도 필요한 기간이 채워질 때까지 기다려야 했습니다. 이제는 그중 일부가 훨씬 일찍 구매 자격을 얻게 되었습니다. 가격이 아니라 접근성 실질적으로 바뀐 것은 집값 수준이 아니라, 시장에 진입할 수 있는 가능성입니다. 자격 요건은 오랫동안 외지인에게 가장 큰 장벽 중 하나였습니다. 이 장벽이 낮아진 것은 주로 실수요층—주택을 투자가 아닌 거주지로 보는 사람들—에게 영향을 미칩니다. 그러나 자격은 조건 중 하나일 뿐입니다. 계약금 규모, 월 상환액, 소득의 안정성, 그리고 앞으로 몇 년간의 계획 등이 여전히 실제로 계약을 체결할지 여부를 결정합니다. 많은 사람들이 고용과 전반적인 경제 상황과 관련된 위험을 계속 저울질하고 있습니다.详情请点选91情报局入口: 링크텍스트 주택공적금을 통한 지원 구매 규정 변경과 함께 주택공적금 메커니즘도 조정되었습니다. 대출 한도가 늘어나고, 리모델링을 위한 인출 절차가 일부 간소화되었으며, 대출 인정 규칙이 명확해졌습니다. 부부 모두 안정적인 납부 기록이 있는 가정의 경우, 이용 가능한 자금 규모가 눈에 띄게 커졌습니다. 详情请点选反职业差入口 이 때문에 온라인 논의가 “1년 만에 살 수 있나?”라는 질문에만 머물지 않고, 가능한 대출 규모, 월 부담, 이사 비용 계산으로 빠르게 확장되었습니다. 시장은 어떻게 반응할까 진입 장벽이 낮아졌다고 해서 수요가 자동으로 증가하는 것은 아닙니다. 결정적인 요인은 여전히 구매자의 자신감입니다. 자격을 더 일찍 얻은 사람이 실제로 더 적극적으로 매물을 보기 시작할 수도 있습니다. 조건을 충족하더라도 가격과 고용 상황에 대한 더 명확한 신호를 기다리며 관망하는 사람도 있을 것입니다. 앞으로 몇 주 동안 주목해야 할 두 가지 지표는 거래 건수의 변화와 구매 상담 건수입니다. 바로 이 수치들이 새로운 정책이 시장 행동을 얼마나 바꾸었는지를 보여줄 것입니다. 详情请点选吃瓜文章入口 정책은 이미 시행 중입니다. 그 영향이 얼마나 뚜렷하게 나타날지는 앞으로 한 달간의 데이터가 알려줄 것입니다.
📝오늘 학습 키워드 Spring ,Spring Boot 클래스, 객체 static 은 꼭 필요한 곳에 알고 써야함. static 은 "모두가 같이 쓰는 공용 물건"입니다. 인터페이스 프로그래밍 명명 규칙 라이브러리 Spring initializr(스프링 이니셜라이저) Servlet API 명세서 용어 및 메서드 정리 1️⃣ 용어 단위 테스트(Unit Test) : 프로그램의 가장 작은 단위인 메서드(기능) 하나만 떼어내서 제대로 동작하는지 독립적으로 검증하는 테스트 불변성 (Immutability) : 객체를 처음 만든 순간 이후에는 내부 값을 절대로 변경할 수 없게 막아두는 성질 ( private final ) Spring : 자바로 서버를 만들 때 필요한 기능을 미리 만들어둔 도구 모음 Spring Boot : Spring을 바로 쓸 수 있게 준비해주는 도구 M(Model) 데이터 V(View) 보여주는 화면* C(Controller) 요청 받고 연결* 2️⃣ 메서드 Lombok 라이브러리 Getter : 조회 Setter : 수정 toString : 객체 String 변환 @NoArgsConstructor : 기본 생성자를 생성한다. @AllArgsConstructor : 모든 필드를 매개변수로 하는 생성자를 생성 생성자 없이 객체 생성가능. (실무에서 지양함.) @RequiredArgsConstructor : 필수(final) 필드만을 매개변수로 하는 생성자를 자동으로 생성 @slf4J : 로그를 남기기 위한 Logger 자동 생성 학습키워드 정리 클래스 : 객체를 만들기 위한 설계도 객체 : 객체를 만들 땐 new 사용 생성자 : 틀(클래스)로 어떻게 결과물(객체)을 만들 것인가에 대한 방법을 강제(⬇️) 한다. 최소한의 데이터나 규칙을 반드시 채워야만 객체가 만들어지도록 규칙을 강제 적용한다. 인터페이스 : 약속이나 규칙을 정의하는 것 (규격) 1️⃣ 프로그래밍 명명규칙 kebab-case : 문자와 문자 사이를 - 로 이어준다. PascalCase : 문자 시작 대문자, 문자와 문자 사이 대문자 camelCase : 문자와 문자 사이 대문자 ex) Java, JavaScript, TypeScript에서 변수, 함수, 메세드 이름 만들 때 사용. snake_case : 문자와 문자 사이 언더바 사용 ✅ 자바에서 사용하는 명명 규칙 PascalCase : 클래스 이름을 지정할 때 사용 camelCase : 변수, 함수, 메서드 명 때 사용 2️⃣ Json 클라이언트와 서버가 통신할 때 사용하는 데이터 형식 Json은 Key 와 Value 로 이루어진 데이터 방식 Value에 또다른 Key/Value 가 될 수 있음. { "이름" : "홍길동", "나이" : 30 "사는 곳" : { ⬅️ Value 안에 Key/Value "시" : "원주시", "동" : "단계동" } } 3️⃣ 라이브러리 애플리케이션 개발에 필요한 클래스, 함수 등을 모아놓은 코드 모음을 의미함. 필요한 기능을 위해 구현하기 위해 사용 4️⃣ 프레임워크 소프트웨어 개발을 간편하게 만들기 위한 소프트웨어 개발 환경 개발자는 해당 틀 안에서 개발 해야함 ex) 붕어빵 틀로 붕어빵 만들기. 프레임워크는 라이브러리들의 모음 전체적인 구조를 잡기 위해 사용 Spring 프레임워크는 자바 플랫폼을 위한 오픈소스 애플리케이션 프레임워크 5️⃣ HTTP (HyperText Transfer Protocol) 웹에서 데이터를 주고 받기 위한 통신 규악 클라이언트는 Request (요청) , 응답 대기 서버는 요청에 대한 처리를 수행 후 결과 Response (응답) 무상태성(Stateless)을 가지고 있음. 이전 요청이나 상태를 기억하지 않는 것. 📚 HTTP 요청 메세지(Request Message) start Line HTTP Method GET 요청의 의도를 가진 GET, POST, PUT, PATCH, DELETE 등이 있다. Create - POST (리소스 생성) Read - GET (리소스 조회) Update - PUT (전체, 덮어쓰기) , PATCH (일부 수정) Delete - DELETE (리소스 삭제) Request Target path* 경로: /event Query String(= Query Parameter) 에 해당하는 값도 포함한다. ex) /search?keyword=sparta HTTP Version : 1.1 HTTP Version을 나타낸다. Header Host: spartacodingclub.kr 임의의 Header를 추가할 수 있다. (단, 서버가 값을 알고 있어야 함) 요청의 추가 정보들을 가지고 있다. ex) Message Body 내용, 크기, 인증, 브라우저 정보, 서버 정보 등 Empty Line 공백 한 줄 필수 값 Message Body 실제 전송하는 데이터가 담겨 있는 부분 HTML, 이미지, JSON 등 byte로 표현되는 모든 데이터 전송 가능. 요청 시 GET의 경우 Message Body가 지원되지 않는 경우가 많다. 📚 HTTP 응답 메시지(Response Message) start Line Http verision status Code (200,300,400,500) status text Header Response 사용된s Header 값 존재 Empty Line 공백 한 줄 필수 값 Message Body 실제 전송하는 데이터 담겨 있는 부분 없으면 공백 📚 HTTP 속성 안전성 : GET 빼고 안전하지않음 멱등성 : 한번 호출하거나 수천번 호출해도 항상 결과는 같음 (POST는 멱등성 보장하지않음) 6️⃣ API / RESTful API API(Application Proramming Interface )는 인터페이스임 프로그램끼리 대화하는 방법, 어떤 요청과 어떤 응답 받을 지 정의 RESTful 잘 설계된 API 디자인 원칙 동사보단 명사, 단수보단 복수 /get/items 마지막에 / 넣지 않기 언더바 대신 - 사용하고 대문자 사용하지 않음 확장자 포함하지 않기 (svg,png,exe 등) 계층화 하기 ❌ /items/{memberId}/members/{itemId} (x) ✅ /members/{memberId}/items/{itemId} 7️⃣ Annotation Annotation implement 의 Override 어노테이션을 활용하면 복잡한 기능도 쉽게 사용 가능하다 Lombook 라이브러리 클래스의 모든 필드에 대한 getther 와 setter 메서드를 자동으로 생성하는 기능 @Getter @Setter public class User { private String name; private int age; /** 아래 코드를 @Getter, @Setter 어노테이션이 생성해준다. public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } **/ } toString 메서드는 객체를 String으로 변환해주는 역할을 수행 8️⃣ Spring Initializr 스프링 부트 기반의 프로젝트를 매우 쉽게 빠르게 생성할 수 있도록 도와주는 웹 기반 도구 서비스 의존성 다른 사람들이 만들어 놓은 편리한 기능(부품)을 가져와 조립하는데 사용 (외부 라이브러리, 외부 코드를 의미) Dependencies로 lombok , Spring Web 사용 의존성 관리도구 사용하는 타입 Gradle - Groovy build.gradle 의존성 추가하는 방법⬇️ Maven Repository 링크 https://mvnrepository.com/ 9️⃣ Servlet (서블릿) 서버에서 실행되어 클라이언트의 요청을 처리하고, 그 결과를 동적으로 생성하여 응답하는 자바 프로그램 서블릿을 실행하고 관리하며 웹 서버와 통신하게 해주는 전문적인 환경을 "서블릿 컨테이너" 라고 부름 서블릿 사용 시 클라이언트와 서버 간 요청 & 흐름도 🔟 API 명세서(메뉴판) 궁금증 || 트러블슈팅 `/Solution.java:7: error: cannot find symbol List<Integer> list = new ArrayList<>(); ^ symbol: class List location: class Solution` cannot find symbol 은 "내가 모르는 단어가 나왔어!"라는 자바의 오류이고 코드 맨 위에 import java.util.List;라는 단어장(주문서)을 적어주지 않아서 생긴 오류입니다. 패키지 명이 일치하지않고 extends 예약을 사용하여 발생한 오류 해결 방법 import 사용할 것 예약명으로 패키지명을 하지않을 것 잡고 넘어가야할 개념 ✅ AI에게 질문하기 전 "나는 어떻게 문재 해결 하려고 했는지 한줄" 적어보고 방향성 잡기 코드를 왜 이렇게 짰는 지 스스로에게 질문하면 코드를 구현하는 습관을 들일 것. Spring 공식 문서 잘 활용하기 (영문 천천히 읽으며 학습) Srping Boot 버전을 잘 확인하면 디버깅 시간을 줄일 수 있으니 항상 버전을 확인하는 습관을 들이기 핵심 정리 튜터님에게 질문 많이 할 것
"초기 반응은 그렇게 뜨거웠는데, 왜 시장은 더 커지지 않을까?" 모든 스타트업이 넘어야하는 깊은 골짜기, 캐즘(Chasm)에 대해 알아보자. 기술수용주기 신제품은 한꺼번에 팔리지 않는다 신제품이 나오면 바로 써 보는 사람이 있고, 주변에서 충분히 써 본 뒤에야 움직이는 사람이 있다. 제프리 무어 가 제시하는 기술수용주기는 이 차이를 단순히 빠르고 느린 성격 차이로 보지 않는다. 고객이 구매를 결정할 때 무엇을 확인하는지 가 다르다. 이렇게 제품을 수용하는 순서에 따라 고객 집단은 5개로 나눠지며, 순서대로 제품에 접근한다. 고객 집단 신제품을 보는 관점 혁신수용자 기술 자체가 흥미롭다. 미완성이어도 먼저 써 본다. 얼리어답터 새로운 기술로 남보다 앞서갈 기회를 찾는다. 전기다수 위험을 줄이고 싶다. 비슷한 처지의 고객이 이미 쓰는지 확인한다. 후기다수 제품이 표준이 되고 가격이 내려간 뒤에 움직인다. 지각수용자 기존 방식으로 버티다가 더는 피하기 어려울 때 받아들인다. 주기 사이에는 자연스럽게 넘지 못하는 금이 있다 같은 제품을 보고도 앞의 고객은 가능성을 사고, 다음 고객은 검증된 해결책을 산다. 다음 수용 집단을 받아드리기 위해서는 제품과 기업에 확실한 변화가 필요하며, 그 중 가장 큰 골짜기인 캐즘(Chasm)을 넘는 것은 스타트업이 살아남기 위한 필수 조건이다. 캐즘 무어가 말한 캐즘 은 얼리어답터와 전기다수 사이에 생기는 깊은 단절이다. 초기 고객에게 잘 팔렸다는 사실만으로 주류 고객이 안심하지 않는다. 오히려 “그 회사는 원래 새로운 기술을 먼저 써 보는 곳이잖아요”라고 생각할 수 있다. 전기다수는 실용주의자들이다. 이들은 매우 보수적이다. 쉽게 신제품에 마음을 열지 않는다. 캐즘의 4가지 증상 초기 고객층이 소진되어 신규 매출이 멈춘다 잠재 고객들이 "다른 도입 사례를 보여 달라"며 구매를 미룬다 얼리어답터들의 서로 다른 맞춤 요구에 개발 자원이 쪼개져 제품의 정체성이 흐려진다 성장을 전제로 늘려 둔 조직과 비용 때문에 현금이 마른다 모든 매출 정체를 캐즘이라고 부를 수는 없다. 기존 고객도 떠나고 있다면 제품 자체의 문제일 수 있고, 업계 전체가 같은 시기에 예산을 줄였다면 경기 영향일 수 있다. 반면 기존 고객은 계속 사용하지만 신규 고객이 도입 사례와 안정적인 지원을 요구하며 구매를 미룬다 면, 고객 집단이 바뀌고 있는지 살펴볼 만하다. 쓰던 사람은 계속 쓰는데 새 고객은 안 오는 것 — 그것이 캐즘이다. 캐즘을 만난 기업에게는 조금 답답한 역설이 있다. 전기다수 고객은 자기와 비슷한 회사의 도입 사례 를 보고 싶어 한다. 하지만 첫 번째 전기다수 고객이 없으니 보여줄 사례도 없다. 초기시장의 성공이 주류시장에 들어갈 근거로 자동 변환되지 않는 것이다. 하지만, 실용주의자 한명만 움직인다면 다른 실용주의자들도 따라들어올 것이다. 캐즘을 건너는 방법 제프리 무어는 캐즘의 첫번째 해결책으로 틈새시장 집중 을 제시한다. 실용주의자 고객 한명만 무너뜨리면 도미노처럼 다른 실용주의자들도 무너져서 쏟아진다는 것이다. 틈새 시장을 노리기 위해서는 다음을 따져본다. 고객에게 지금 해결해야 할 문제 가 있는가? 우리가 그 시장에서 눈에 띄는 선택지가 될 만큼 범위가 좁은가? 이후 옆 시장으로 확장할 수 있을 만큼 의미 있는 시장인가? 제품뿐 아니라 도입과 사용에 필요한 지원까지 제공할 수 있는가? 기업은 더 많은 기능, 더 많은 영업이 필요한게 아닌 틈새시장을 향해 좁혀야한다 캐즘의 두번째 해결책은 완전완비제품 이다. 완전완비제품 은 목표 고객이 구매와 사용 충동을 느끼는 데 필요한 모든 가치를 완벽하게 갖춘 상태의 제품이다. 예를 들어, 전기차만으로는 완전완비제품이라고 할 수 없는 것이다. 충전 인프라, 보험, 잔존가치 등 모든게 준비된 상태가 완전완비제품의 완성이라고 볼 수 있다. 실용주의자에게 적당한 제품은 없다 완전한 제품을 판매해야한다. 캐즘의 마지막 해결책으로는 볼링엘리 를 노린다. 진입할 시장은 볼링장, 기업은 볼링핀이라고 생각하면 하나의 기업만 무너뜨리면 나머지 기업도 한번에 무너진다는 것이다. 처음 말한 틈새시장을 노리라는 말과 같은 말을 반복한다. 처음부터 여러 핀을 동시에 노리면 어느 곳에서도 충분한 사례를 만들기 어렵다. 틈새시장에서 1등이 되면 협력업체들이 알아서 붙고, 완전완비제품이 강해지고, 매출이 빨라지는 선순환에 올라탈 수 있다. 정리 하이테크 기업, 스타트업의 제품이 출시되고 이를 받아드리는 고객은 5가지로 나누어지며, 이는 기술수용주기를 따른다. 그 중, 얼리어답터에서 전기다수 고객에게 넘어가기위해서는 캐즘이라는 깊은 골짜기를 넘어가야한다. 이를 넘지못하면 당연하게도 스타트업은 실패한다. 주류 시장으로 넘어가기 직전, 큰 장애물인** Chasm**에 대해 알아봤다. 다음은 캐즘을 넘어간 이후, 주류시장에 도달했을 때 마주치는 돌풍과 같은 수요에 대해 알아보고자 한다.
프로세스의 개념 한 줄 요약 프로세스는 실행 중인 프로그램 에 대한 운영체제의 추상화다. OS는 프로세스들을 번갈아 실행(시분할)해서, CPU가 여러 개 있는 듯한 환상을 만든다. 0. 프로그램 vs 프로세스 디스크에 가만히 있는 프로그램을 읽어서 실행해 생명을 불어넣는 것 이 운영체제의 역할이다. 1. CPU 가상화와 시분할 시분할 (Time Sharing) 하나의 프로세스를 잠깐 실행하고 → 중단하고 → 다른 프로세스를 실행하는 것을 빠르게 반복하는 기법이다. 효과: CPU는 1개(또는 소수)인데, 여러 개의 가상 CPU가 있는 것처럼 보인다. 장점: 원하는 만큼 많은 프로세스를 동시에 실행할 수 있다. 대가: CPU를 나눠 쓰니까 각 프로세스 입장에서는 더 느려진다. 메커니즘 vs 정책 CPU 가상화를 잘 구현하려면 도구 와 지능 이 둘 다 필요하다. 메커니즘 (도구) 정책 (지능) 질문 어떻게 할까? 무엇을/누구를 고를까? 정의 필요한 기능을 구현하는 방법 OS가 결정을 내릴 때 쓰는 알고리즘 예시 문맥 전환 (실행 중인 프로세스를 멈추고 다른 것으로 바꾸는 방법) 스케줄링 정책 (실행 가능한 프로세스가 여럿일 때 누구를 먼저 실행할지) 2. 프로세스의 구성 요소 (4.1) 프로세스를 이해하려면 "실행 중에 읽거나 바꿀 수 있는 하드웨어 상태" 가 뭔지 알아야 한다. 구성 요소 내용 메모리 (주소 공간) 명령어와 프로그램이 읽고 쓰는 데이터가 저장되는 곳. 가장 중요한 구성 요소 레지스터 많은 명령어가 레지스터를 직접 읽고 쓴다 입출력 정보 프로세스가 현재 열어둔 파일 목록 특별한 레지스터들 프로그램 카운터(PC) : 지금 어느 명령어 를 실행 중인지 가리킨다. 명령어 포인터(IP)라고도 부른다. 스택 포인터 / 프레임 포인터 : 함수의 지역 변수, 인자, 리턴 주소가 저장되는 스택 을 관리한다. 💡 프로세스를 멈췄다가 다시 이어서 실행하려면, 이 상태들을 통째로 저장해뒀다가 복원해야 한다. 3. 프로세스 API (4.2) 운영체제가 제공해야 하는 기본 인터페이스는 다섯 가지다. API 설명 생성 새 프로세스를 만든다 (쉘에 명령어 입력, 아이콘 더블클릭) 제거 필요 없는 프로세스를 강제로 종료한다 대기 어떤 프로세스가 끝날 때까지 기다린다 각종 제어 일시정지, 재개 등 상태 얼마나 실행됐는지, 지금 어떤 상태인지 등의 정보를 조회한다 4. 프로세스 생성 과정 (4.3) 1 코드·정적 데이터 탑재 → 2 스택 할당 → 3 힙 할당 → 4 입출력 초기화 → 5 main()으로 점프 단계 하는 일 1 탑재 디스크의 실행 파일에서 코드와 정적 데이터를 읽어 프로세스의 주소 공간에 올린다 2 스택 스택용 메모리를 할당하고, main(argc, argv) 의 인자로 초기화한다 3 힙 동적 할당용 영역. malloc() 으로 요청하고 free() 로 반환한다. 연결 리스트, 해시 테이블, 트리처럼 크기가 변하는 자료구조에 쓰인다 4 입출력 유닉스에서는 표준 입력 / 표준 출력 / 표준 에러 3개의 파일 디스크립터를 기본으로 열어준다 5 실행 시작 main() 으로 분기하면서 CPU 제어권을 새 프로세스에게 넘긴다 📌 늦은 탑재 (Lazy Loading) 요즘 OS는 프로그램 전체를 한 번에 올리지 않고, 실행 중 필요한 부분만 그때그때 메모리에 올린다. 자세한 동작은 페이징·스와핑을 배워야 이해할 수 있다. 지금은 "코드와 데이터를 디스크에서 메모리로 올려야 실행된다"만 기억하자. 5. 프로세스 상태 (4.4) ⭐ 핵심 스케줄됨 준비(Ready) ─────────▶ 실행(Running) ▲ ◀───────── │ │ 중단됨 │ 입출력 요청 │ ▼ └───── 입출력 완료 ── 대기(Blocked) 상태 의미 실행 (Running) CPU에서 명령어를 실행하고 있다 준비 (Ready) 실행할 준비는 끝났지만, 다른 프로세스가 CPU를 쓰고 있어서 차례를 기다리는 중이다 대기 (Blocked) 어떤 사건(예: 디스크 입출력 완료)이 일어날 때까지 실행할 수 없는 상태다. CPU를 줘도 할 일이 없다 상태 전이 규칙 준비 ↔ 실행 : OS의 스케줄링 정책 에 따라 이동한다. 스케줄되면 준비 → 실행 중단되면 실행 → 준비 (나중에 다시 스케줄될 수 있는 상태) 실행 → 대기 : 입출력 요청 등으로 기다려야 할 때 대기 → 준비 : 기다리던 사건이 끝나면 준비 상태로 돌아간다 ⚠️ 대기 → 실행으로 바로 가지 않는다! 깨어나면 먼저 준비 상태 로 가서 줄을 서야 한다. 실제로 바로 실행될지는 OS가 결정한다. 책 예시 💡 4~6 동안 CPU가 한 순간도 놀지 않는다. 대기(Blocked) 상태가 존재하는 이유가 바로 이것이다. 만약 P0가 대기 상태로 가지 않고 계속 "입출력 끝났나?"만 확인했다면, 그 시간 동안 CPU는 낭비된다. 6. 운영체제의 자료구조 (4.5) 운영체제도 결국 하나의 프로그램이라서, 프로세스를 관리하기 위한 자료구조를 가진다. 자료구조 역할 프로세스 리스트 준비 상태인 프로세스들의 목록 현재 실행 중인 프로세스 정보 지금 CPU를 쓰는 프로세스가 누구인지 대기 중인 프로세스 정보 입출력이 완료되면 해당 프로세스를 깨워서 준비 상태로 옮겨야 하므로 따로 관리한다 레지스터 문맥 (Context) 프로세스가 중단될 때 레지스터 값들을 저장해둔다. 나중에 이 값을 복원 해서 실행을 재개한다 → 이것이 문맥 전환 이런 프로세스 정보를 담은 구조체를 PCB(Process Control Block, 프로세스 제어 블록) 라고 부른다. 7. 🔗 Pintos와 연결 OSTEP 4장 Pintos PCB struct thread 실행 / 준비 / 대기 THREAD_RUNNING / THREAD_READY / THREAD_BLOCKED 준비 프로세스 리스트 ready_list 입출력 대기 → 완료 시 깨우기 Alarm Clock : 잠든 스레드를 BLOCKED로 두고, 깰 시각이 되면 READY로 스택을 인자로 초기화 Project 2 Argument Passing ( argc , argv 를 스택에 쌓기) ✍️ 정리 한 문장 프로세스는 실행 중인 프로그램에 대한 OS의 추상화로, 주소 공간·레지스터·입출력 정보 로 이루어진 실행 상태를 가진다. OS는 이를 PCB 로 관리하며, 프로세스는 실행·준비·대기 상태를 오가며 CPU를 시분할 로 공유한다
선착순 이벤트 동시성 제어: 락 4종을 같은 조건에서 비교했습니다 정원 30명짜리 선착순 신청에 비관적 락, 낙관적 락, MySQL Named Lock, Redis 분산 락을 모두 구현했습니다. 그리고 같은 조건에서 k6로 측정했습니다. 결과부터 적으면 이렇습니다. 전략 p95 비관적 락 310.8ms 낙관적 락 383.7ms Named Lock 872.4ms Redis 분산 락 1,647.1ms 가장 느린 것이 Redis 분산 락이었습니다. 왜 그렇게 됐는지가 이 글의 내용입니다. 동시에 신청하면 정원이 넘칩니다 이벤트 참가 신청 기능입니다. 로직은 단순합니다. event.increaseParticipants(); participantRepository.save(participant); 혼자 테스트할 때는 잘 됩니다. 문제는 동시 요청입니다. 여러 트랜잭션이 같은 시점에 참가자 수를 읽으면 전부 같은 값을 봅니다. 그리고 각자 +1 해서 저장합니다. 갱신 손실입니다. 성공 응답은 여러 번 나갔는데 DB의 참가자 수는 그보다 작습니다. JUnit으로 100개 스레드를 동시에 던져 먼저 재현했습니다. 트랜잭션만으로는 동시성이 제어되지 않는다는 걸 확인하고 시작했습니다. 네 가지를 구현했습니다 락을 거는 위치가 다릅니다. DB 행, DB 세션, 그리고 Redis입니다. 비관적 락 조회 시점에 배타 락을 겁니다. @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select e from EventWithLock e where e.id = :id") Optional<EventWithLock> findByIdWithPessimisticLock(Long id); PESSIMISTIC_WRITE 는 SELECT ... FOR UPDATE 로 나갑니다. 락이 트랜잭션 종료까지 유지되므로 쓰기 순서를 따로 강제할 필요가 없습니다. 낙관적 락 락을 걸지 않습니다. 버전이 바뀌었으면 실패시킨 뒤 재시도합니다. @Lock(LockModeType.OPTIMISTIC) @Query("select e from EventWithLock e where e.id = :id") Optional<EventWithLock> findByIdWithOptimisticLock(Long id); 재시도는 파사드에서 처리했습니다. 여기서 한 가지를 구분했습니다. } catch (IllegalStateException e) { throw e; // (1) } catch (Exception e) { Thread.sleep(RETRY_DELAY_MS); // (2) } (1) 정원 초과는 재시도해도 결과가 같습니다. 바로 실패시켜야 합니다. (2) 버전 충돌만 재시도합니다. 둘을 뭉뚱그리면 이미 마감된 이벤트에 무한정 재시도가 돕니다. 이 구현에서 데드락을 만났는데, 원인이 JPA의 쓰기 순서였습니다. 그 이야기는 따로 적었습니다. Named Lock MySQL의 GET_LOCK() 은 레코드가 아니라 이름으로 락을 겁니다. @Query(value = "SELECT GET_LOCK(:lockName, :timeoutSeconds)", nativeQuery = true) Integer getLock(@Param("lockName") String lockName, @Param("timeoutSeconds") int timeoutSeconds); 이쪽에서도 사고가 하나 있었습니다. 실패율이 84.77%까지 올라갔는데, 원인은 락이 아니라 제가 얹은 트랜잭션 구조였습니다. 이 글의 측정은 그 문제를 고친 뒤의 코드로 했습니다. Redis 분산 락 락을 DB 밖으로 뺍니다. String lockKey = "event-lock:" + eventId; for (int retry = 0; retry < MAX_RETRY; retry++) { if (!distributedLockProvider.tryLock(lockKey, LOCK_TIMEOUT_MS)) { Thread.sleep(RETRY_DELAY_MS); // (1) continue; } try { eventWithLockService.joinEventWithNamedLock(eventId, memberId); // (2) return; } finally { distributedLockProvider.releaseLock(lockKey); } } (1) 락을 못 얻으면 50ms 쉬고 다시 시도합니다. 최대 100회입니다. (2) 락을 얻은 뒤에야 DB에 접근합니다. 이 순서가 뒤에서 중요해집니다. 측정 측정 환경 측정 일시 : 2026.03.20 16:19 ~ 16:31 (12분 내) 스크립트 : scenario3-concurrency.js 동일 부하 패턴 : 5s → 10 VU, 5s → 100 VU, 30s 유지, 10s 감소 요청 간 sleep : 0.05초 정원 : 30명 인프라 : AWS EC2 + RDS(MySQL 8) + Redis HikariCP 풀 : 32 측정 구간 중 코드 커밋 없음 결과 전략 avg p95 처리량 비관적 락 165.1ms 310.8ms 288.7 req/s 낙관적 락 198.9ms 383.7ms 275.8 req/s Named Lock 515.2ms 872.4ms 129.7 req/s Redis 분산 락 600.2ms 1,647.1ms 114.1 req/s 네 가지 모두 등록 성공 30건, 정원을 정확히 지켰습니다. 비관적 락 p95 310.8ms 낙관적 락 p95 383.7ms MySQL Named Lock p95 872.4ms Redis 분산 락 p95 1,647.1ms Redis가 가장 느렸던 이유 락을 못 얻으면 50ms 쉬고 다시 시도합니다. 100 VU가 하나의 락을 두고 경쟁하면 대기 줄이 길어집니다. 뒤쪽 요청은 50ms 단위로 계속 깨어나며 기다립니다. 여기에 네트워크 왕복이 더해집니다. 비관적 락은 DB 안에서 끝나지만, Redis 락은 요청마다 Redis를 다녀와야 합니다. 락 대기 비용을 커넥션에서 시간으로 옮긴 것이지, 없앤 게 아니었습니다. 비관적 락이 빨랐던 이유 DB가 락 대기를 직접 관리합니다. 애플리케이션이 50ms마다 깨어나 폴링할 필요가 없고, 트랜잭션도 하나만 씁니다. Named Lock도 마찬가지로 DB가 대기를 관리하지만, 락을 잡는 쿼리와 작업 쿼리가 따로 나가서 왕복이 한 번 더 생깁니다. 그래서 무엇을 쓸 것인가 처음에는 "어느 락이 제일 좋은가"를 찾고 있었습니다. 그런 건 없었습니다. 상황 선택 단일 DB, 경합 높음 비관적 락 경합 낮음 낙관적 락 서버 여러 대, DB를 공유하지 않음 Redis 분산 락 커넥션 풀이 빡빡함 Redis 분산 락 (지연은 감수) Redis 분산 락이 느린 건 나쁜 선택이어서가 아닙니다. 이 조건에서 필요하지 않은 선택 이었을 뿐입니다. 여러 인스턴스가 붙는 환경이면 결론이 달라집니다. 이 측정의 한계 실패율 0%는 커스텀 지표 기준입니다. k6 기본 지표 http_req_failed 로 보면 99%가 나옵니다. 정원이 찬 뒤의 400 응답까지 실패로 세기 때문입니다. 어느 지표를 보느냐에 따라 0%와 99%가 갈립니다. 측정 순서가 균일하지 않습니다. 낙관적, Named, Redis는 12분 안에 연달아 쟀고 비관적 락만 8분 뒤에 쟀습니다. JVM과 DB가 더 데워진 상태였을 수 있습니다. 가장 좋은 수치가 나온 쪽이 마지막에 측정됐다는 점은 밝혀둡니다. 조건이 극단적입니다. 정원 30명에 100 VU는 경합률이 매우 높습니다. 충돌이 드문 워크로드라면 결과가 달라집니다. 단일 인스턴스입니다. EC2 한 대, Redis 단일 노드입니다. 서버를 늘리면 순위가 바뀝니다. 그리고 순수한 락 4종의 비교가 아닙니다. 네 전략이 같은 엔티티를 쓰는데 거기에 @Version 이 붙어 있습니다. 이건 따로 다룰 만한 이야기라 별도 글로 적었습니다. 마치며 락을 네 가지 구현해놓고 보니, 성능을 가른 건 락 자체가 아니라 대기를 누가 관리하느냐 였습니다. DB가 관리하면 빠르고, 애플리케이션이 폴링하면 느립니다. 전체 코드와 k6 스크립트는 backend-pass-portfolio 에 있습니다.
Choosing an ERP system is a major step for businesses that want to organize their operations and improve productivity. An ERP platform can bring finance, inventory, sales, purchasing, HR, production, and reporting into one connected environment. However, selecting the wrong system can create unnecessary costs and implementation challenges. Understanding How to Choose ERP Software India helps businesses evaluate their actual requirements, compare suitable solutions, and choose technology that can support both present operations and future expansion. What Should You Check Before Selecting ERP Software? Before looking at different ERP providers, businesses should first understand their current processes. Every organization has different workflows, challenges, employees, and technology requirements. A system that works well for one company may not be suitable for another. Begin by identifying repetitive tasks, reporting problems, data duplication, inventory issues, and communication gaps between departments. Make a list of the processes that need improvement and decide what you expect the ERP to achieve. It is also useful to involve employees from different departments during the initial planning stage. Their practical experience can help management identify requirements that may otherwise be overlooked. This creates a stronger foundation for software evaluation. ERP Software Selection India: How Can You Evaluate Your Requirements? ERP software selection India should be based on business needs rather than choosing software simply because it has many features. The selection process becomes easier when companies clearly separate essential requirements from optional ones. Start by deciding which departments will use the ERP. A manufacturing company may need production planning, warehouse management, purchasing, quality control, and accounting. A distribution company may place greater importance on inventory, sales, purchasing, logistics, and financial management. Businesses should also evaluate the number of users, locations, expected transaction volumes, integration requirements, and reporting needs. Customization is another important factor, particularly when existing workflows are different from standard ERP processes. Creating a detailed checklist allows decision-makers to compare different platforms fairly. It also prevents businesses from investing in unnecessary modules that employees may never use. How Can You Identify the Best ERP Software in India? The Best ERP software in India is not necessarily the one with the longest feature list. The right platform should solve specific operational problems while remaining practical for employees to use. Businesses should examine software functionality, user experience, flexibility, scalability, security, integration options, implementation support, and pricing. A product demonstration can help decision-makers understand whether the software actually fits their workflows. Vendor reliability is equally important. Research the provider's implementation experience, technical support, training services, upgrade policies, and ability to customize the system when necessary. Businesses should also consider the long-term relationship with the vendor. ERP software is generally not a short-term investment. A dependable provider should be available to assist with future updates, new requirements, troubleshooting, and system improvements. ERP Software for Indian Businesses: What Features Are Essential? ERP software for Indian businesses should provide functionality that supports everyday business activities while allowing room for customization. Instead of selecting every available module, organizations should identify the features that directly contribute to their operational goals. Common modules include finance and accounting, inventory, procurement, sales, CRM, HR, manufacturing, project management, and analytics. Businesses can select modules according to their industry and operational structure. Integration capabilities are also important. The ERP should be able to exchange information with existing applications where required. This reduces duplicate data entry and provides employees with more consistent information. Ease of use should never be ignored. If employees find the interface confusing, adoption may become difficult. A simple and intuitive ERP can make training easier and encourage users to work within the new system. How Should Businesses Compare ERP Solutions in India? When reviewing ERP solutions in India, businesses should consider more than the initial purchase price. The total investment can include subscriptions, licenses, implementation, customization, training, data migration, integrations, maintenance, and support. Cloud ERP may be attractive for organizations that want remote access and reduced infrastructure management. On-premise deployment can provide greater control for companies with particular infrastructure or security requirements. Scalability is another major consideration. As the business grows, it may need more users, locations, transactions, products, or integrations. The ERP should be able to support that growth without requiring a complete replacement. Ask every vendor for a clear cost structure and implementation plan. Comparing the total cost of ownership provides a more realistic view of which solution offers the best long-term value. ERP System Selection: What Is the Best Comparison Method? A structured ERP system selection process can make decision-making easier. Businesses can create a scorecard that compares shortlisted ERP providers against predefined requirements. Consider factors such as: Required modules and functionality Ease of use Customization options Integration capabilities Security controls Scalability Reporting and analytics Implementation support Training Customer service Total cost of ownership Assigning scores to these areas can make comparisons more objective. It also helps management understand why one ERP is more suitable than another. Businesses should arrange demonstrations and provide vendors with realistic examples of their workflows. This gives users an opportunity to evaluate how the software performs in situations they encounter every day. How Important Is ERP Implementation Planning? Selecting an ERP is only the beginning. Once the software is chosen, implementation needs careful planning. Businesses should establish a project team, define responsibilities, set milestones, and communicate expectations to employees. Data migration should receive particular attention. Existing customer, supplier, product, inventory, and financial records should be reviewed and cleaned before being transferred. Employee training is equally important. Users should understand the new workflows and receive training that matches their specific roles. This can reduce confusion and improve adoption after launch. Businesses should also establish a process for handling issues during the early stages of implementation. Regular communication between the internal team and ERP provider can help resolve problems quickly. What Common ERP Selection Mistakes Should You Avoid? Choosing software only because it is inexpensive is one common mistake. The cheapest solution may not provide the functionality or scalability needed by the business. Another problem is focusing too heavily on features. An ERP with hundreds of features may still be unsuitable if employees cannot use it comfortably. Ignoring future growth can also create problems. Businesses should consider expected expansion before selecting a platform. Finally, do not overlook vendor support. Even good software may require technical assistance, updates, training, or customization. Strong after-sales service can make a significant difference to the long-term success of the ERP. How Can Businesses Make a Confident Final Decision? After demonstrations and evaluations, narrow the options to the ERP platforms that satisfy the most important requirements. Ask shortlisted vendors detailed questions about implementation, security, integration, customization, support, and future upgrades. The final decision should consider both technical functionality and practical business value. When learning How to Choose ERP Software India, businesses should focus on selecting software that employees can use effectively and that management can rely on for accurate information. A good ERP should simplify operations rather than make them more complicated. It should also provide enough flexibility to adapt as the organization changes. Conclusion ERP selection requires planning, research, and realistic evaluation. Businesses should understand their processes, define requirements, compare providers, evaluate total costs, and involve employees before making a final decision. The right ERP can improve efficiency, centralize information, reduce manual work, and provide better visibility across departments. With a structured approach, businesses can make their investment more valuable and avoid many common implementation challenges. Ultimately, How to Choose ERP Software India depends on finding a practical balance between features, usability, cost, scalability, security, and vendor support. A well-selected ERP can become a long-term technology foundation for business growth. FAQs What should I consider before buying ERP software? Consider your business processes, required modules, budget, number of users, integrations, scalability, security, and vendor support. Is ERP useful for small businesses? Yes. Small businesses can benefit from ERP when they select a solution with essential features and flexible pricing. Should I choose cloud or on-premise ERP? It depends on your infrastructure, security requirements, budget, accessibility needs, and IT resources. Why is vendor support important? Reliable support helps with implementation, training, troubleshooting, updates, and future customization. How can I compare ERP pro
Java를 처음 시작하면 문법보다 먼저 헷갈리는 것이 JDK가 무엇인지 , int 와 String 은 왜 다른지, 값을 변수에 어떻게 담는지 같은 기본 개념이다. 이 글에서는 Java 코드를 실행하기 위한 최소 환경부터 자료형, 변수, 상수, 주석, 형 변환까지 한 번에 정리한다. 이후 조건문, 반복문, 객체지향을 공부하기 위한 가장 앞단의 내용이라고 보면 된다. 1. Java 코드는 어떻게 실행될까? Java 개발을 위해서는 JDK(Java Development Kit) 가 필요하다. JDK에는 Java 소스 코드를 바이트코드로 컴파일하는 javac , 프로그램을 실행하는 java 같은 개발 도구가 포함되어 있다. 2026년 기준 최신 Java SE 릴리스는 더 새 버전까지 나와 있지만, 입문 학습에서는 JDK 25 LTS 처럼 장기 지원 버전을 선택하면 버전 변화에 덜 흔들리며 공부하기 좋다. 개발 도구는 IntelliJ IDEA, VS Code 등 무엇을 사용해도 괜찮다. 중요한 것은 IDE 자체보다 다음 흐름을 이해하는 것이다. Hello.java ↓ javac Hello.class ↓ JVM에서 실행 프로그램 결과 가장 기본적인 Java 프로그램은 다음과 같다. public class Hello { public static void main(String[] args) { System.out.println("Hello, Java!"); } } main 은 프로그램이 시작되는 진입점이고, System.out.println() 은 값을 콘솔에 출력한다. Java 25에는 더 간결한 소스 파일 형태도 추가되었지만, 클래스와 public static void main(String[] args) 구조를 먼저 익혀두면 기존 Java 코드와 프레임워크를 이해하기 쉽다. 2. 자료형은 왜 필요할까? 프로그램은 숫자, 문자, 참/거짓 등 서로 다른 종류의 데이터를 다룬다. Java는 변수를 만들 때 어떤 종류의 값을 저장할지 자료형을 명확하게 지정 하는 정적 타입 언어다. int retryCount = 3; double responseTime = 1.25; boolean success = true; char grade = 'A'; String endpoint = "/api/users"; 기본 자료형 Java의 대표적인 기본 자료형은 다음과 같다. 자료형 용도 예시 int 일반적인 정수 100 long 더 큰 정수 3_000_000_000L float 실수 3.14F double 더 정밀한 실수 3.141592 boolean 참/거짓 true , false char UTF-16 코드 단위 하나 'A' int 는 32비트, long 은 64비트 정수이고, float 와 double 은 부동소수점 수를 표현한다. 일반적인 실수 계산에는 double 을 더 자주 사용한다. 초보 단계에서 자주 보는 설명 중 boolean 을 무조건 1바이트라고 외우는 경우가 있는데, Java 언어 명세는 boolean 의 저장 크기를 고정된 바이트 수로 정의하지 않는다. true 와 false 두 값만 가진다 고 이해하는 것이 정확하다. 또한 char 는 2바이트이지만 모든 유니코드 문자를 하나의 char 로 표현할 수 있는 것은 아니다. 이모지처럼 보조 문자가 필요한 경우에는 두 개의 char 가 사용될 수 있다. 참조 자료형 String , 배열, 클래스의 객체 등은 참조 자료형이다. String serviceName = "order-api"; int[] statusCodes = {200, 201, 404}; User user = new User(); 초보 설명에서는 참조 변수가 "메모리 주소를 저장한다"고 단순화하기도 한다. 더 정확하게는 객체를 가리키는 참조값을 가진다 고 이해하면 된다. 그 참조값이 실제 물리 메모리 주소와 동일하다고 가정할 필요는 없다. 3. 변수 선언과 초기화 변수는 값을 저장하고 다시 사용하기 위한 이름 있는 공간이다. int requestCount = 10; 구조를 나누면 다음과 같다. int requestCount = 10; 자료형 변수명 값 선언과 대입을 나누는 것도 가능하다. int requestCount; requestCount = 10; 하지만 값이 준비되어 있다면 선언과 동시에 초기화하는 편이 읽기 쉽다. String environment = "production"; boolean cacheEnabled = true; 4. 변수 이름은 코드의 설명서다 변수명에는 문자, 숫자, _ , $ 등을 사용할 수 있지만 숫자로 시작할 수 없고 Java 예약어도 사용할 수 없다. int userCount = 10; // 좋음 int activeUserCount = 8; // 의미가 구체적 Java에서는 일반적으로 변수와 메소드 이름에 lowerCamelCase 를 사용한다. String apiBaseUrl = "https://example.com"; int maxRetryCount = 3; 아래처럼 의미를 알기 어려운 이름은 피하는 편이 좋다. int a = 3; int x1 = 5; 코드는 작성하는 시간보다 다시 읽는 시간이 길다. 변수명만 보고도 값의 역할을 추측할 수 있게 만드는 것이 중요하다. 5. 주석은 필요한 곳에만 사용한다 한 줄 주석은 // , 여러 줄 주석은 /* ... */ 을 사용한다. int timeoutSeconds = 30; // 외부 API 요청 제한 시간 /* 개발 환경에서는 테스트 서버를 사용한다. 운영 환경에서는 실제 API 주소를 사용한다. */ 다만 다음처럼 코드가 이미 설명하는 내용을 주석으로 반복할 필요는 없다. int userCount = 10; // userCount에 10을 넣는다 좋은 주석은 무엇을 하는지보다 왜 이렇게 했는지 설명한다. 6. 바뀌면 안 되는 값은 final 한 번 값이 정해진 뒤 다시 대입하지 못하게 하려면 final 을 사용한다. final int MAX_RETRY_COUNT = 3; MAX_RETRY_COUNT = 5; // 컴파일 오류 상수처럼 사용할 때는 UPPER_SNAKE_CASE 를 많이 사용한다. 주의할 점은 final 변수가 반드시 선언과 동시에 초기화되어야 하는 것은 아니라는 것이다. 인스턴스의 final 필드는 생성자에서 한 번 초기화할 수도 있다. class ApiClient { private final String baseUrl; ApiClient(String baseUrl) { this.baseUrl = baseUrl; } } 7. 형 변환: 작은 범위에서 큰 범위로, 큰 범위에서 작은 범위로 자동 형 변환 표현 범위가 더 넓은 타입으로 옮길 때는 Java가 자동으로 변환할 수 있다. int requestCount = 120; long totalRequestCount = requestCount; double average = totalRequestCount; 단, int -> float -> double 처럼 변환 자체는 가능해도 부동소수점 표현 특성 때문에 매우 큰 정수에서는 정밀도 손실이 생길 수 있다. "큰 자료형이므로 언제나 값이 완벽하게 보존된다"고 생각하면 안 된다. 명시적 형 변환 반대로 더 좁은 타입으로 바꾸려면 직접 캐스팅해야 한다. double latency = 42.9; int roundedDownLatency = (int) latency; System.out.println(roundedDownLatency); // 42 실수에서 정수로 변환하면 소수점 아래가 버려진다. 문자열과 숫자 변환 실무에서 API 파라미터나 입력값을 다루다 보면 문자열과 숫자 사이의 변환이 자주 필요하다. String pageText = "12"; int page = Integer.parseInt(pageText); int statusCode = 200; String statusText = String.valueOf(statusCode); 잘못된 숫자 문자열을 변환하면 예외가 발생한다. Integer.parseInt("12px"); // NumberFormatException 따라서 사용자 입력이나 외부 데이터는 이후에 배울 예외 처리와 함께 사용하는 경우가 많다. 8. 처음 공부할 때 자주 헷갈리는 부분 String 은 기본 자료형이 아니라 참조 자료형 이다. 문자열 리터럴에는 큰따옴표( " ), char 에는 작은따옴표( ' )를 사용한다. String language = "Java"; char firstLetter = 'J'; 또한 double 을 int 로 바꿀 때 반올림되는 것이 아니라 소수 부분이 잘린다. System.out.println((int) 9.99); // 9 핵심 정리 Java 프로그램을 개발하려면 JDK가 필요하고, 입문 단계에서는 LTS 버전을 선택하기 편하다. Java는 변수 선언 시 자료형을 지정하는 정적 타입 언어다. int , long , float , double , boolean , char 는 기본 자료형이다. String , 배열, 객체는 참조 자료형이다. 변수는 의미를 드러내는 lowerCamelCase 이름을 사용하는 것이 좋다. 값 재대입을 막으려면 final 을 사용한다. 넓은 타입으로는 자동 변환될 수 있지만, 좁은 타입으로 바꿀 때는 명시적 캐스팅이 필요하다. 문자열과 숫자 변환은 Integer.parseInt() , String.valueOf() 등을 활용한다.
📅 Date : 2026.10.06 🏷️ Tags : #Python #DataVisualization #EDA # Plotnine # Matplotlib #MachineLearning 🎯 이번 주 목표 이번 기술블로그에서는 데이터 시각화 세션에서 학습한 시각적 속성과 스케일의 개념을 복습하고, 데이터의 특성 및 분석 목적에 따라 적절한 그래프를 선택하는 방법을 정리하고자 한다. 특히 Python의 plotnine을 활용한 실습 과제를 다시 분석하면서, 단순히 그래프를 생성하는 것이 아니라 어떤 정보를 전달하고 싶은지에 따라 그래프의 구조와 표현방식을 설계하는 과정에 집중한다. 강의에서 배운 시각화 내용을 실습에서 진행한 코드와 연결짓고, 추가적인 개념 학습과 이를 활용할 수 있는 방안에 대해 더 생각해보는 것을 목표로 한다. 📌 Contents 1. 세션 복습 2. 과제 복습 3. 관련 논문 리뷰 링크 4. 이번 주 한 줄 정리 1️⃣ 세션 복습 ✨ 핵심 개념 및 내용 정리 📖 핵심 개념 이번 세션에서 중요하게 다룬 내용은 크게 아래와 같이 정리해볼 수 있다. 시각적 속성과 스케일: 데이터의 값을 위치, 모양, 크기, 색상 등의 시각적 속성에 연결하는 방법 위치 및 색상 스케일: 데이터의 분포와 분석목적에 따라 적합한 좌표계와 색상을 선택하는 방법 수량 데이터의 시각화: 막대도표, 묶은 막대, 누적 막대, 점 도표 및 히트맵의 활용 분포 데이터의 시각화: 히스토그램, 밀도 도표 및 여러 집단의 분포를 비교하는 방법 효과적인 시각화 설계: 그래프에서 발생하는 왜곡을 방지하고 전달하려는 정보를 명확하게 표현하는 방법 이번 세션에서는 데이터 시각화가 단순히 보기 좋은 그래프를 만드는 작업이 아니라, 동일한 데이터라도 축의 범위나 사용된 색상, 그래프의 종류, 범주 정렬 방식에 따라 전달되는 정보나 강조하고 싶은 부분이 달라질 수 있다는 것이다. 따라서 시각화를 시작하기 전 단계에서 데이터의 특성과 그 분석 목적을 먼저 명확히 파악해야할 필요가 있다. 📝 내용 정리 1. 데이터 시각화의 기본 원칙 강의에서 데이터 시각화를 미학과 과학의 결합이라 설명한다. 미학적 측면: 색상, 그래프 배치, 가독성 등 시각적 표현 과학적 측면: 데이터의 특성을 정확하게 반영하고 수치적 관계를 왜곡하지 않는 것 이 두 가지 측면 모두 중요하지만 우선적으로 고려해야 할 요소는 데이터의 정확한 전달이다. 시각화의 문제를 크게 다음과 같이 구분할 수 있다. 구분 의미 대표적인 사례 Ugly 정보를 전달하지만 시각적으로 조잡 불필요하게 많은 색상 사용 Bad 정보를 전달하는 방식이 부적절 혹은 이해하기 어려움 범주의 순서를 임의로 배치 Wrong 데이터를 잘못 표현하거나 왜곡 축의 설정 때문에 실제 차이를 과장 여기서 중요한 것은 그래프를 예쁘게 만드는 것보다 잘못된 해석을 유발하지 않도록 설계하는 것이다. 실제 실습과제에서도 의도적으로 잘못 작성된 그래프를 수정하는 문제가 여러차례 있었다. 이를 통해 코드가 오류없이 실행된다는 사실과 올바른 데이터 시각화라는 사실은 별개라는 점을 알 수 있었다. 2. 시각적 속성과 스케일 시각적 속성 이란 데이터의 값을 그래프로 표현하는 시각적 요소를 뜻한다. 시각적 속성은 다음과 같다. 시각적 속성 표현 방법 활용 사례 위치 x축과 y축의 좌표 산점도에서 두 변수의 관계 크기 점 크기 또는 도형 면적 자동차 무게 색상 색조, 명도, 채도 데이터의 그룹 모양 원, 삼각형, 사각형 서로 다른 범주의 구분 선 굵기 선의 두께 데이터의 중요도 또는 가중치 선 종류 실선, 점선 서로 다른 계열 구분 스케일 이란 데이터의 값과 시각적 속성을 연결하는 규칙이다. 예를 들어 자동차 데이터의 배기량을 x축에, 연비를 y축에, 마력을 색상에 연결한다면 각각의 변수에 대응하는 스케일이 필요하다. 하나의 그래프에서 여러 개의 스케일을 동시에 사용할 수도 있다. 강의에서 제시된 자동차 산점도에는 총 다섯가지의 스케일이 사용되었다. x축의 배기량: 위치 스케일 y축의 연비: 위치 스케일 마력: 색상 스케일 자동차 무게: 크기 스케일 실린더 수: 모양 스케일 특히 동일한 데이터라도 어떤 시각적 속성에 연결하느냐에 따라 전달할 수 있는 정보가 달라진다는 점을 이해할 수 있었다. 3. 위치 스케일 위치스케일 은 데이터의 값을 그래프의 좌표에 대응시키는 방법이다. 강의에서는 선형 스케일, 로그 스케일, 제곱근 스케일과 극좌표계를 다루었다. 3-1. 선형 스케일 선형 스케일 은 데이터에서 동일한 수치 차이가 그래프에서도 동일한 거리로 표현되는 방식이다. 예를 들어 10과 20의 차이, 20과 30의 차이는 모두 10이므로 그래프에서도 같은 간격으로 표시된다. 가장 직관적인 스케일이며, 일반적인 수량 데이터나 변수 사이의 관계를 표현할 때 사용한다. 그러나 값의 범위가 지나치게 넓은 데이터에서는 문제가 발생할 수 있다. 예를 들어 인구가 매우 많은 일부 도시와 상대적으로 인구가 적은 여러 도시를 하나의 선형 스케일로 표현하면, 값이 작은 도시들이 그래프 하단에 밀집하여 서로 구분하기 어려워질 수 있다. 3-2. 로그 스케일 로그 스케일 은 값의 절대적인 차이보다 비율의 차이를 표현할 때 유용하다. 선형 스케일이 동일한 차이를 같은 거리로 표현한다면, 로그 스케일은 동일한 배율을 같은 거리로 표현한다. 예를 들어 밑이 10인 로그 스케일에서 1, 10, 100, 1000은 동일한 간격으로 나타난다. 로그 스케일의 가장 큰 장점은 값의 범위가 넓은 데이터의 구조를 보다 쉽게 관찰할 수 있다는 점이다. 강의에서는 인구를 전체 인구 중간값으로 나눈 비율을 이용하여 선형 스케일과 로그 스케일을 비교했다. 주의할 점은 로그 스케일에서 0을 표현할 수 없다는 것이다. log0은 정의되지 않기 때문에 데이터에 0이 포함되어 있다면 로그 스케일을 그대로 적용하기 어렵다.' 3-3. 제곱근 스케일 제곱근 스케일 은 로그 스케일과 마찬가지로 큰 값 사이의 차이를 압축하지만, 0을 표현할 수 있다는 장점이 있다. 따라서 사고 발생 건수처럼 0이 존재하고 값의 편차가 큰 음수가 아닌 데이터를 표현할 때 고려할 수 있다. 다만 제곱근 스케일은 로그 스케일처럼 일정한 배율에 대응하는 자연스러운 눈금 체계를 갖지 않는다. 데이터의 의미에 맞는 눈금을 선택하는 것이 중요하다. 또한 음수에 제곱근 변환을 그대로 적용할 수 없기 때문에 데이터의 범위를 함께 확인하여야 한다. 3-4. 극좌표계 극좌표계 는 위치를 직교좌표계의 x, y 대신 거리와 방향으로 표현한다. 반지름: 중심으로부터 떨어진 거리 각도: 중심을 기준으로 한 방향 시간이나 계철처럼 주기적으로 반복되는 데이터를 표현할 때 활용할 수 있다. 다만 주기성을 표현할 수 있다는 장점과는 별개로, 정확한 수치 비교가 목적이라면 극좌표계 대신 일반적인 직교좌표계(x, y축)가 더 적합할 것이다. 4. 색상 스케일 색상 스케일 은 데이터의 값을 색상에 대응시키는 방법이다. 강의에서는 색상을 사용하는 목적을 세 가지로 구분했다. 데이터의 그룹을 구분할 떄 데이터의 값을 표현할 때 특정 정보를 강조할 때 이를 실제 시각화에 적용하면 네 가지 색상 스케일로 구분할 수 있다. 색상 스케일 특징 활용 사례 정성적 서로 다른 범주를 구분 제조사별 제조국 순차적 값이 작거나 큰 정도를 표현 주택 가격 중위값 발산형 특정 기준보다 크거나 작은 정도를 표현 평균 대비 거래량 증감률 강조 원, 삼각형, 사각형 최고 연비 차종 4-1. 정성적 스케일 정성적 스케일 은 데이터에 크기나 순서의 의미가 없고 서로 다른 집단을 구분해야 할 때 사용한다. 예를 들어 자동차의 제조국을 독일, 미국, 일본 ,한국 등으로 구분한다면 국가 사이에 수치적인 크기 관계가 존재하지 않는다. 이 경우에는 각 국가를 구별할 수 있도록 서로 다른 색상을 사용하되, 특정 국가의 색상만 과도하게 두드러지지 않도록 주의해야 한다. 강의에서는 색각 이상이 있는 사람도 비교적 쉽게 구분할 수 있도록 설계된 Okabe-Ito 팔레트를 소개했다. -> Tableau에서 기본적으로 제공되는 그래프에서도 그룹별로 색상이 다르게 그래프에 표현되는 것을 확인할 수 있다. 물론 여기서는 데이터의 크기가 의미가 있지만 서로다른 3가지의 범주로 구분할때에 색상에 차이를 두면 알아보기가 쉽다. 4-2. 순차적 스케일 순차적 스케일 은 데이터의 값이 작거나 큰 정도를 연속적인 색상 변화로 표현하는 방식이다. 대표적으로 명도나 채도가 점진적으로 변화하는 팔레트를 사용한다. 주택 가격, 온도, 생산량 등 크기가 중요한 데이터를 히트맵이나 지도에 표현할 때 활용할 수 있다. 이때 색상이 불규칙하게 변화하면 실제 값의 순서가 왜곡되어 보일 수 있으므로, 색상이 일정한 방향으로 변화하는 팔레트를 선택하는 것이 중요하다. 4-3. ✨발산형 스케일 발산형 스케일 은 특정 기준점을 중심으로 값이 양방향으로 변화할 때 사용한다. 예를 들어 평균 대비 거래량 증감률이 다음과 같다고 가정하자. -20%: 평균보다 거래량이 적음 0%: 평균과 동일 +20%: 평균보다 거래량이 많음 이때 0을 중립적인 색상으로 설정하고 음수와 양수에 서로 다른 색상 계열을 부여하면 데이터의 방향과 크기를 동시에 표현할 수 있다. 발산형 스케일에서 가장 중요한 요소는 기준점의 의미이다. 기준이 평균인지, 0인지, 특정 목표값인지에 따라 그래프의 해석이 달라질 수 있다. 4-4. 강조 스케일 강조 스케일 은 여러 데이터 중에서 특정값이나 범주를 강조할 때 사용한다. 예를 들어 자동차 차종별 평균 연비 그래프에서 가장 높은 연비를 기록한 차종에만 주황색을 사용하고 나머지는 회색으로 표현하여 강조할 수 있다. 이를 통해 모든 데이터를 하나씩 확인하지 않아도 그래프의 핵심 정보를 빠르게 파악할 수 있다. 5. 수량 데이터의 시각화 수량 데이터의 시각화에서는 범주별 수량값을 어떻게 효과적으로 비교할 것인지가 중요하다. 막대도표: 범주별 수량값을 비교할 때 사용한다. 막대도표에서느 막대의 길이 자체가 값의 크기를 표현하므로 수량 축은 원칙적으로 0에서 시작해야 한다. 또한 범주의 이름이 길거나 범주가 많다면 가로 막대그래프를 사용한느 것이 효과적이다. 순서가 없는 범주라면 크기순으로 정렬할 수 있지만, 요일이나 연령대처럼 본래의 순서가 존재하는 범주의 경우에는 해당 순서를 유지하여 표현해야 한다. 묶은 막대: 하나의 범주 안에서 두 개 이상의 집단을 비교할 때 사용한다. 서로 다른 집단의 막대가 같은 기준선에서 시작하기 때문에 집단 간 값의 차이를 쉽게 비교할 수 있다. 누적 막대: 각 집단의 값을 하나의 막대에 누적하여 표현한다. 막대의 전체 높이가 합계를 나타내므로 범주별 전체 규모를 비교할 때 유용하다. 반면 각 집단이 서로 다른 위치에서 시작하므로 개별 집단 사이의 차이를 정밀하게 비교하기에는 불리할 수 있다. 점 도표: 데이터의 값을 막대 길이가 아닌 위치로 표현한다. 따라서 수량 축을 반드시 0에서 시작하지 않아도 되며, 비교하려는 값들이 비슷한 범위에 밀집해 있을 때 효과적이다. 히트맵: 데이터의 값을 색상으로 표현한다. 여러 범주와 시간에 걸쳐 관측된 데이터처럼 값의 개수가 많은 경우 전반적인 변화 패턴을 파악하는 데 유용하다. 그러나 색상을 통한 정확한 수치 비교에는 한계가 있다. 따라서 정확한 수치보다는 패턴을 파악하려는 목적에 적합하다. 6. 분포 데이터의 시각화 분포 데이터의 시각화에서는 개별적인 수치보다 데이터가 전체적으로 어떤 형태로 분포되어 있는지를 파악하는 것이 중요하다. 히스토그램: 연속형 데이터를 일정한 구간으로 나누고 각 구간에 포함된 데이터의 빈도를 막대 그래프로 표현한다. 여기서 가장 중요한 설정 중 하나는 구간 폭인 binwidth이다. 구간 폭이 지나치게 작으면 작은 변동까지 드러나 전체적인 구조를 파악하기가 어려워지고, 구간 폭이 지나치게 크면 여러 값이 하나의 구간으로 뭉쳐져 중요한 세부적인 특징을 파악하기 어렵다. 밀도 도표: 커널 밀도 추정(KDE)을 활용하여 데이터의 분포를 부드러운 곡선으로 표현한다. 히스토그램과 달리 연속적인 곡선으로 전체 분포를 확인할 수 있다. 여기서 대역폭은 밀도 곡선이 얼마나 부드럽게 표현되는지를 결정한다. 대역폭이 작으면 세부적인 변화가 강조되고, 대역폭이 크면 곡선이 더 부드러워진다. 하지만 대역폭을 과도하게 크게 설정하면 실제 데이터에 존재하는 여러 봉우리나 국소적인 특징이 사라질 수 있다. 또한 밀도 도표는 추정 결과이므로 원래 데이터가 존재하지 않는 영역에도 곡선이 나타날 수 있다. 따라서 분포의 세부 구조나 경계가 중요한 경우 히스토그램과 함께 확인하는 것이 좋다. 여러 집단의 분포 비교: 여러 집단의 분포를 비교할 때는 누적 히스토그램, 중첩 히스토그램, 중첩 밀도 도표 등을 사용할 수 있다. 특히 누적 히스토그램은 집단마다 막대의 시작 위치가 달라 개별 집단의 분포를 비교하기 어렵다. 이 문제를 해결하기 위해 반투명한 중첩 밀도 도표를 활용하거나 집단별로 패널을 분리할 수 있다. 이번 과제에서도 펭귄 데이터를 이용하여 여러 종과 성별의 분포를 비교하면서 각 시각화 방식의 특징을 확인했다. 7. 시각화 도구 강의 마지막에는 Tableau와 Power BI도 소개되어 있다. 구분 Tableau Power BI 중심 기능 데이터 탐색 및 시각화 데이터 모델링과 리포팅 특징 복잡한 차트 및 세밀한 시각화 정형화된 리포트와 데이터 관리 활용 환경 다양한 데이터 소스와 시각화 작업 Microsoft 생태계와의 연계 이번 실습에서는 BI 도구 대신 Python의 plotnine을 사용했다. plotnine은 R의 ggplot2를 기반으로 만들어진 Python 시각화 라이브러리이다. ggplot()을 이용해 그래프의 기본 구조를 정의하고 aes()로 시각적 속성을 지정한 후, geom_ * ()함수로 그래프의 종류를 선택한다. 여기에 스케일, 좌표계, 레이블 및 테마를 추가하는 방식으로 그래프를 구성할 수 있다. 🔍 추가적인 공부 및 조사 강의에서 다룬 내용을 바탕으로 실습 과정에서 추가적으로 이해하게 된 내용을 정리했다. 아래 내용에는 강의에서 직접 다루지 않은 개념에 대한 보충 설명도 포함하였다. 새롭게 알게 된 내용 1. 데이터 변환과 축 변환은 구분하여야 한다. 이번 과제에서 가장 주의깊게 살펴본 개념 중 하나는 로그 변환이었다. 일반적으로 로그 스케일을 적용한다는 말을 들으면 데이터 자체에 로그를 취하는 방식과 축에 로그 스케일을 적용하는 방식을 혼동하기 쉽다. #데이터를 직접 변환 county["log_ratio"] = np.log10(county["ratio"]) ggplot(county, aes(x="rank", y="log_ratio")) #원본 데이터에 로그 축 적용 ( ggplot(county, aes(x="rank", y="ratio")) + scale_y_log10() ) 두 그래프는 같은 로그 변환을 사용하여 점을 배치하므로 적절한 조건에서 위치가 동일하게 나타날 수 있다. 하지만 축의 의미에는 차이가 있다. 첫 번째 방법은 데이터 자체가 로그 변환되어 있으므로 축에 나타나는 수치도 로그값이다. 두 번째 방법은 원본 데이터에 로그 스케일을 적용하므로 축 눈금을 원래 데이터의 단위로 표시할 수 있다. 즉, 중요한 것은 단순히 로그 변환을 적용하는 것이 아니라 그래프의 축과 레이블이 실제 데이터의 의미를 올바르게 전달하는지 확인하는 것이다. 2. 히스토그램과 밀도 도표는 모두 설정값에 영향을 받는다 히스토그램의 구간 폭과 밀도 도표의 대역폭은 서로 다른 개념이지만, 전체적인 분포의 세부 구조를 얼마나 드러낼 것인지 결정한다는 공통점이 있다. 히스토그램에서는 구간 폭을 변경하면 각 구간에 포함되는 관측값이 달라진다. 밀도 도표에서는 대역폭을 변경하면 주변 관측값의 영향을 반영하는 범위가 달라진다. 따라서 그래프에서 봉우리가 하나 나타났다고 해서 실제 데이터가 반드시 단봉 분포라고 단정할 수 없다. 3. 분포의 모양을 비교하는 것과 집단의 규모를 비교하는 것은 다르다 두 집단의 분포를 비교할 때는 비교 목적에 따라 그래프의 정규화 방식도 달라져야 한다. 예를 들어 두 집단으 밀도 도표를 각각 정규화하면 각 곡선 아래의 면적은 1이 된다. 이는 각 집단의 분포 형태를 비교하는 데 적합하지만, 집단의 전체 데이터 개수가 얼마나 다른지는 직접적으로 표현하지 않는다. 반대로 각 집단의 관측값 개수를 반영하면 집단 간 규모의 차이까지 함께 표현할 수 있다. 기존 내용과의 비교 또는 심화 내용 수업 내용 추가 조사 내용 로그 스케일 원본 데이터에 로그 축을 적용하는 방법과 데이터를 직접 로그 변환하는 방법으 ᅵ차이 정성적 색상 스케일 색각 이상을 고려한 Okabe-Ito 팔레트 활용 순차적 색상 스케일 데이터 값의 순서를 자연스럽게 인지할 수 있는 지각적으로 균일한 팔레트 히스토그램 구간 폭 뿐만 아니라 구간의 시작점도 분포 표현에 영향을 미칠 수 있음 밀도 도표 대역폭에 따라 과소평활 또는 과대평활이 발생할 수 있음 여러 분포 비교 개별 분포의 형태와 전체 데이터에서 차지하는 규모를 구분해야 함 강조 스케일 강조 대상을 선택하는 기준까지 설명해야 해석의 편향을 줄일 수 있음 💡 아이디어 및 활용 방안 🚀 활용 아이디어 1. 제조 공정 이상탐지 데이터의 시각화 제조 공정에서는 온도, 압력, 진동 등 여러 종류의 센서 데이터가 시간에 따라 수집된다. 이때 센서별로 데이터의 단위와 값의 범위가 다르기 때문에 모든 변수를 동일한 방식으로 표현하면 중요한 변화를 파악하기 어려울 수 있다. 특히 이상탐지 결과를 시각화할 때는 정상 구간을 평이한 색상으로 표현하고 이상이 탐지된 시점을 강조하면 효과적일 것이다. 2. 배터리 성능 저하 및 이상탐지 결과의 비교 배터리의 사이클에 따른 용량 감소를 시각화한다면 사이클을 x축, 용량을 y축으로 설정한 선 그래프를 사용할 수 있다. 또한 정상 셀과 이상 셀의 궤적을 구분할 때 정성적 색상 스케일이나 강조 스케일을 적용할 수 있다. 여러 이상탐지 방법의 탐지 시점을 비교할 때는 각 방법의 탐지 시점을 그래프에 직접 표시하면 결과를 전달하는 데 도움이 될 것이다. 3. 여러 센서 및 공정 조건의 분포 비교 정상 제품과 불량 제품의 센서값 분포를 비교할 때 밀도 도표를 활용할 수 있다. 두 집단의 분포를 반투명하게 중첩하면 서로 겹치는 구간을 파악할 수 있고, 패널을 분리하면 각 집단의 분포를 명확하게 비교할 수 있다. 또한 공정 조건별 평균값이 기준보다 얼마나 높은지 or 낮은지를 히트맵으로 표현한다면 발산형 색상 스케일을 적용해볼 수 있다. 2️⃣ 과제 복습 💻 코드 해설 1. 하나의 산점도에 다섯 가지 변수 표현 ( ggplot( mtcars, aes( x="disp", y="mpg", color="hp", size="wt", shape="factor(cyl)" ) ) + geom_point() + scale_color_cmap("viridis") + labs( x="displacement (cu. in.)", y="fuel efficiency (mpg)", color="power (hp)", size="weight (1000 lbs)", shape="cylinders" ) ) aes() 함수는 데이터의 각 변수를 시각적 속성에 연결한다. x="disp": 배기량을 x축에 표현 y="mpg": 연비를 y축에 표현 color="hp": 마력을 색상으로 표현 size="wt": 자동차 무게를 점의 크기로 표현 shape="factor(cyl)": 실린더 수를 점의 모양으로 구분 여기서 geon_point()는 그래프의 형태를 산점도로 지정하고, scale_color_cmap("viridis")는 마력의 크기를 연속적인 색상으로 표현하도록 설정한다. 가장 중요하게 학습한 부분은 factor(cyl)을 사용한 이유이다. cyl은 실린더 수이므로 숫자로 기록되어 있지만, 이번 그래프에서는 서로 다른 실린더 수를 가진 자동차를 그룹으로 구분해야 한다. 모양이라는 시각적 속성은 연속적인 크기보다는 서로 다른 범주를 구분하는데에 적합하다. 따라서 factor(cyl)을 사용하여 실린더 수를 범주형 변수로 변환했다. 이를 통해 하나의 그래프에 여러 변수를 표현하는 것이 가능하더라도, 각각의 변수를 데이터의 특성에 적합한 시각적 속성에 연결해야 한다는 점을 이해할 수 있었다. 2. 로그 스케일로 인구 데이터의 큰 편차를 표현하기 county = midwest[ ["count
배경 전자계약서 시스템에는 업무 목적에 따라 여러 종류의 대용향 엑셀 파일을 생성하고 있었다. 데이터 조건에 따라 파일 생성에 오랜 시간이 걸리기 때문에 사용자가 결과가 나올 때까지 계속 기다리게 하는 대신, 작업이 끝나면 이메일로 결과를 받도록 구성했다. 이 과정에서 비동기 처리는 필요했지만, 이 비동기 처리를 과연 kafka로 구현해야 하는지에 대해서는 의문이 든 상태이다. 기존 처리 흐름은 이랬다. 사용자가 엑셀 생성을 요청한다. API 서버가 요청 정보를 DB에 저장한다. kafka에 이벤트를 발행한다. 워커가 이벤트를 소비한다 이벤트 속 데이터를 조회하고 엑셀 파일을 생성한다. 파일을 저장하고 메일을 발송하는 후속 처리를 진행한다. 완료 여부를 기록하고 kafka에 처리 완료를 확인한다. 변화 초기에는 10분 이내로 엑셀 생성이 끝났다. 그러나, 신규 엑셀 유형을 추가하면서 엑셀의 각 행마다 외부 API를 호출해야 하는 까다로운 작업이 추가되었다. 수만 건의 데이터를 대상으로 반복되는 API 호출이 계속되다보니 외부 API 응답 대기 네트워크의 지연 다른 팀 서버의 응답까지 지연 행 수 증가에 따른 호출 횟수 증가 이러한 문제들이 발생하였다. 결과적으로 데이터 조회만 30분 이상 걸리거나, 전체 작업이 짧게는 30분, 길게는 1시간 이상씩 걸리는 상황이 발생하였다. 작업 시간이 길어짐과 동시에, 외부 시스템에 의존하기 때문에 작업 시간을 일정하게 예측하기 어렵다는 점이 주요 포인트였다. 기존 아키텍처 AS-IS: 같은 엑셀 파일이 여러 번 발송되었다. 동일한 kafka 이벤트를 서로 다른 워커가 처리하는 경우가 발생하였다. 로그 추적 결과 컨슈머가 같은 메시지를 두 번 처리한 흔적을 발견했다. 워커 A가 kafka에서 엑셀 생성 요청 이벤트를 가져온다. 워커 A는 오랜 시간 동안 엑셀 생성 작업을 수행하였다. 생성 작업이 오래 걸려 poll()을 호출하지 못했다. poll()을 호출하지 못하니 max.poll.interval.ms로 정한 시간을 초과해버렸다. a. 아무리 바빠도 5분 안에는 생존 신고해라. 안 그러면 쓰러진 걸로 간주하고, 너에게 맡긴 일 다른 애한테 다시 시킬거다. b. 하지만, 이 일꾼은 너무 바쁜 나머지 poll()을 하지 못해 kafka는 이 일꾼이 쓰러진 것으로 판단하고 리밸런싱을 감행한다. Consumer Group의 리밸런싱으로 담당 파티션이 다른 워커로 재할당되었다. a. 쓰러졌다고 판단된 일꾼이 하고 있던 이벤트를 다른 일꾼한테 재할당한다. 완료 처리가 확정되지 않은 이벤트를 워커 B가 다시 소비했다. 워커 A도 실제로는 계속 작업 중이었으므로, 두 워커가 같은 파일을 생성하고 메일을 발송했다. poll()이 뭐하는 아이인가요? poll()은 구독하고 있는 메시지 큐에서 메시지를 가져오는 명령이 맞다. 하지만 부분적으로는 생존 신고 역할도 한다. kafka에서 컨슈머의 생존은 두 가지 단계로 나뉜다. 우선 네트워크 생존이다. 이는 별도의 백그라운드 쓰레드가 브로커와 주기적으로 짤막한 신호를 주고받으며 증명한다. 이를 heartbeat라고 한다. 또 한 가지는 애플리케이션 생존인데, 프로세스가 켜져 있지만 코드가 무한 루프에 빠졌거나 너무 무거운 작업을 하게 되면 다음 메시지를 가져오지 못 하는 상태가 될 수 있다. 바로 이 상황이 poll()을 못 부르는 상태이다. kafka 브로커는 poll()이 일정 시간 동안 안 들어오면, 이 워커는 살아만 있지 일은 못 하는 바보 상태이구나~ 라고 판단하게 된다. 즉, 기능적 정의로는 메시지 큐에서 데이터를 당겨오는 명령어지만, 실무적 의미로는 kafka 브로커에게 나 이전 작업 무사히 마치고 다음 일 받을 준비가 됐다는 생존 및 작업 가능 신고인 셈이다. 단순한 해결책: interval.ms를 늘리면 되는가? 타임아웃을 1시간으로 늘리면 문제를 해결할 수 있지만, 1시간 30분 걸리는 작업 발생 시 똑같은 문제가 발생하게 된다. 또한 진짜 장애가 발생하면 1시간 뒤에 알아채게 된다. Hotfix: 작업을 coroutine으로 실행, 리스너에서 먼저 처리 확인하기 일반 함수(서브루틴)는 한 번 호출되면 return문을 만나 끝날 때까지 멈추지 않고 끝까지 달리지만, 코루틴은 실행 중간에 잠시 멈추고 제어권을 다른 곳에 넘겼다가, 나중에 멈췄던 그 지점부터 다시 이어서 실행할 수 있다. 스레드와 코루틴의 차이? 백그라운드에서 실행되는 거라면 멀티스레드와 뭐가 다른가? 할 수 있다. 스레드는 OS가 관리하는 실제 작업의 단위이고 스레드 하나당 1MB 내외의 스택 메모리를 차지한다. OS가 개입하기 때문에 스위칭 비용이 커서 속도가 비교적 느리다. 반면 코루틴은 소프트웨어가 관리하는 가상의 스레드고, 하나당 수 KB 수준의 아주 작은 크기만 차지한다. 하나의 스레드 안에서 코드 객체만 바뀌므로 비용이 거의 들지 않아 값이 싸다. 그래서! 코루틴을 경량 스레드 라고 부르기도 하는 것이다. 여담으로, Java에서는 경량 스레드를 표현할 방법이 없어 골머리를 썩히다가 JDK 21에 가상 스레드가 도입되면서 완화되었다. 즉, 코루틴을 사용해서 백그라운드 작업으로 전환한 뒤 즉시 ACK를 보내는 방식으로 생존신고를 했다. 하지만 핫픽스답게 이것 역시 근본적인 해결책은 아니다. 외부 API 오류로 진짜 엑셀 생성이 실패해도 정상적인 메시지 재소비로 복구되지 않을 수 있다. 서버 문제로 프로세스가 종료되면 백그라운드 작업도 사라지기 때문에 작업이 유실된다. kafka가 꼭 필요한가? kafka를 선택했던 이유는 요청을 받는 API와 실제 엑셀 생성 처리를 분리하고 싶었던 목적이 크다. 순간적인 트래픽 변화에도 비동기 처리로 탄력적 대응을 노렸고, 필요한 경우 컨슈머 그룹을 늘려 유동적 대처가 가능할 것으로 예상했기 때문이다. 하지만, 엑셀 발송 작업 업무 특성상 트래픽이 튀는 상황이 발생할 일이 거의 없었고, 생산자와 소비자가 모두 같은 서비스 내부에 있어 처리 주체를 분리하는 것이 의미가 없었다. 여기서 중요한 건 워커는 여러 대가 필요했지만, 컨슈머 그룹이 여러 개가 필요한 건 아니라는 것에 주의해야 한다. kafka의 강점은 하나의 이벤트를 여러 그룹이 동시에 복사해서 읽어갈 수 있는 기능을 제공한다는 것이다. 하지만 지금 상황은 똑같은 엑셀을 만들라는 명령을 여러 팀이 가져갈 필요가 없었다. 오직 엑셀 생성이라는 단 하나의 목적만 있었기 때문이다. 단지 요청이 많아서 엑셀 생성팀 안에 일꾼만 여러 명 있으면 된다는 의미였다. 즉! 우리가 kafka를 썼던 이유는 일이 많아서 여러 워커가 나눠 처리하기 위함이었지, 하나의 이벤트를 여러 컨슈머 그룹이 각자 가져가는 복잡한 구조는 우리에게 필요 없어. → kafka의 최고 장점을 우리는 안 써도 돼. → 어 그래? 그러면 굳이 kafka 쓸 필요가 있나? 버리자. TO-BE: RDB 기반의 Task 큐로 전환하자. 새 구조의 업무 단계는 이렇다. API아 DB에 작업을 등록한다. 워커가 대기 중인 작업을 조회한다. Redis 분산 락으로 작업을 선점한다. DB 상태를 실행 중으로 변경한다. Heartbeat(생존신고)를 갱신하며 엑셀을 생성한다. 성공하면 완료 처리하고 실패하면 재시도 또는 최종 실패 처리한다. 여기에 별도의 복구 스케쥴러가 실행 중인 작업을 감시한다. 실행 중인데 Heartbeat가 오래 갱신되지 않은 작업을 발견한다. 작업을 대기 상태로 복귀시킨다. 그 작업은 기다리고 있다가 다른 워커가 다시 처리하게 된다. kafka를 걷어낸 아키텍처 모든 작업은 상태와 마지막 Heartbeat 정보를 가진다. 워커는 일을 받으면 주기적으로 heartbeat를 갱신하고, 2분 동안 갱신을 하지 못하면 (서버가 죽으면) fallback 스케쥴러가 2분마다 주기적으로 보내는 select 쿼리에 의해 잡히게 되어 상태를 PENDING으로 변경당하게 된다. 여기서 알 수 있는 건, fallback 스케쥴러는 유실될 가능성이 있는 작업을 PENDING으로 되돌려 다시 워커가 그 작업을 재시도 할 수 있도록 돕는다는 것이다. 참고로 fallback 스케쥴러는 모든 워커에 하나씩 있다. 다만, 2분에 한 번씩 SELECT하는 구문은 한 스케쥴러가 랜덤으로 선정된다. 선정 기준은 ShedLock의 락을 먼저 선점한 스케쥴러가 담당한다. 왜 굳이 여러 개의 스케쥴러를 만드느냐? 한다면, 하나의 워커가 장애로 인해 뻗어도, 다른 스케쥴러가 살아남아 남겨진 이벤트들을 회수해 갈 수 있기 때문이다. 워커는 3초 단위로 내가 가져올 작업이 있는지 없는지 DB를 조회한다. 조회한 id중 선점하고 싶은 작업을 Redis에게 나 이거 선점할게 라고 부탁한다. Redis가 오케이 하면 그걸 DB에 쓰고, 안돼 라고 하면 다른 id를 가지고 부탁한다. 일을 할당받은 워커는 주기적으로 DB에 Heartbeat를 갱신하는 프로세스를 활성화한다. 이때, Heartbeat 갱신은 코루틴으로 구현한다. 만약 외부 API 실패나 네트워크 오류로 실패한다면, 단순히 실패로 끝내지 않고 재시도 횟수 값을 늘린 뒤 다시 PENDING 상태로 되돌린다. 3회까지 재시도 기회를 주고, 그래도 안 되면 FAILED로 변경시킨다. 여기서 생긴 궁금증 만약 Heartbeat를 갱신하는 쿼리문이 일시적인 오류로 DB에 보내지지 않았을 때, 그게 2분이 경과된다면 fallback 스케쥴러가 정상적인 작업의 상태를 강제로 PENDING으로 바꿔버릴 수 있는 문제가 생기지는 않을까? 라는 궁금증이 들었다. 그렇게 된다면 워커는 일을 잘 하고 있고 단지 Heartbeat 갱신에 문제가 생긴 것인데 스케쥴러가 PENDING 처리 해 버리니 그 일을 다른 워커가 빼앗아 진행하게 되면(Redis 락은 워커가 가져가는 즉시 풀린다.) 처음에 생겼던 문제가 또 다시 반복되진 않을까? 생각했다. 사실 Heartbeat 갱신은 10초에 한 번, fallback 스케쥴러가 잡는 기준은 2분이니 이론상 갱신이 12번 연속 실패해야 해서 확률이 희박하긴 하지만, 우리가 서킷브레이커를 설정하는 이유도 이러한 희박한 확률까지 빠짐없이 방어하기 위해 하는 것 아닌가? 배민에서는 이러한 상황을 고려했으나 트레이드오프를 고려해 과감히 배제하였는지 궁금하다. 무엇을 얻었나? 이렇게 kafka를 걷어냄으로써 디버깅에 용이해졌고, 단일 데이터 소스로 상태 관리가 단순해졌다. 메시지 재발행 및 유실 문제 비율이 크게 하락했고, 자동 복구 메커니즘까지 완비했다. 단순 쿼리로 실시간 모니터링까지 구축했다. 카프카, 쿠버네티스 등등, 요새 취준생들이 많이 쓰는 기술 스택이다. 뭔가 최신 기술같고, 고급 기술같아 보인다. (특히 카프카) 하지만, 이런 기술을 쓴다고 너는 어려운 프로젝트를 해낸적이 있구나! 라고 생각할지는 잘 모르겠다. 'kafka를 쓴다.' 라는 사실에 집중하는 것이 아니라, 이 기술 스택을 왜 써야 하고, 우리에게 꼭 필요한 서비스인지, 그렇지 않다면 더 간단한 방법은 없는지를 고민하는 과정 자체가 더 중요하다고 생각한다. 기술을 위한 프로젝트에 급급하지 말고, 프로젝트를 위한 기술을 잘 골라내는 것이 AI 시대 개발자들이 지녀야 할 능력 중 하나라고 생각한다.