합격 후기 생각보다 어려웠다..! 그리고 함정이 잔뜩 있는 지뢰밭이었다 시험지를 처음 받아 1단원을 풀 때 까지는 '약간 변별력이 있는 편이네~' 정도로만 생각했습니다. 단순히 개념에 대해 묻는다기보다 살짝 꼬아서 출제했다는 느낌이 있었으나 그렇게 어렵진 않았습니다. 그런데 2단원으로 넘어가면서 잘못하면 불합격할 수도 있겠다는 생각이 들어 침을 꿀꺽 삼켰습니다. CBT에서 봐왔던 복원 문제와 비교했을 때, 문제의 길이가 전체적으로 길었고 문제의 요구사항도 상당히 디테일하고 어려운 편이었습니다. 그뿐 아니라 문제에 함정이 상당히 많이 숨어 있었고, 가채점 후에 알게됐지만 저도 걸려버린 함정이 꽤나 있었습니다 흑흑 시험이 끝난 뒤 카카오톡 SQLD 오픈 단톡방에 들어가보니, 곡소리가 나고있더군요. 서로 문제를 회상하면서 답을 맞춰보는데, 다들 말하는 답이 다르고 서로가 맞다고 팽팽하게 주장하는 그림도 여러 번 있었습니다. 지난 시험인 61회차도 어려웠는데, 이번이 그 때보다 더 어렵다고 하는 목소리가 많았습니다. SQLD는 합격률 발표도 하지 않는 깐깐한 시험이어서 정확히 얼마나 어려웠는지를 수치화 할 수는 없지만, 합격자 발표가 나던 날 오픈 단톡방을 보니 불합격하는 사람이 상당히 많이 보였습니다. 대체로 50점대의 분포가 가장 많이 보였고, 딱 60점을 맞아 가까스로 통과한 사람도 꽤 많이 보였습니다. 합격자들도 80점보다 높은 점수는 볼 수 없었습니다. 공부법 추천 공부법에 대한 이야기에 앞서 제 이야기를 하자면, 저는 관련 전공을 졸업한 전공자이면서 프론트엔드 3년차 개발자입니다. 그래서 완전히 비전공자라고 할 수 는 없지만 데이터베이스는 대학교 1학년 때 A+를 맞아본 뒤로 제대로 다룬적이 없어서 관련 개념을 거의 잊어버렸기 때문에 데이터베이스에 있어서 만큼은 비전공자와 크게 다를게 없다고 봐주시면 될 것 같습니다. 대부분의 수험생들이 인강을 통해서 개념 공부를 시작한다는 것으로 알고 있지만 저는 시간적인 제약이 있었으므로 가급적이면 인강을 듣지 않는 방향을 택했습니다. 금액적인 비용과 시간을 아끼고 싶으신 분들은 제 공부법을 따라해 보시기를 적극 추천드립니다. 공부 기간: 1.5개월 . 교재:『유선배 SQL개발자』,『노랭이』 요약집: 블로그 bruder 요약집 기출: 문어CBT 9회차분 풀기 (52~60회) 1.유선배 1회독 개념 학습을 위해 먼저『유선배 SQL 개발자』를 2회독했습니다. 유선배는 개념이 초심자들도 이해하기 쉽게 설명되어 있어서, 편하게 읽기며 개념의 양도 그렇게 많지 않아 부담스럽지 않게 학습할 수 있습니다. 다만 이때는 몰랐지만, 실제 출제되는 문제에 비해 설명이 부실하거나 빠진 개념도 약간 있을 수 있습니다. 하지만 이건 뒤에 요약집을 통해서 충분히 보완할 수 있으므로, 이 단계에서는 큰 개념만 익히고 간다는 생각을 하는게 좋습니다. 1회독 때에는 주요 개념을 빠르게 숙지하고 수록된 문제들을 풀었습니다. 문제를 풀 때에는 2회독 때를 위해 책에다가 답을 표시하지 않고, 따로 답안지를 만들어 채점했구요. 틀리거나 헷갈린 문제에만 따로 표시를 해두었습니다. 2.유선배 2회독 1회독을 하면서 어느 부분이 쉽고 어려운 부분인지를 느꼈습니다. 그걸 바탕으로 쉬운 부분의 개념은 가볍게 읽고 넘어가고, 어려운 부분은 공들여 개념을 숙지했습니다. 문제 풀이는 1회독에서 표시해두었던 문제 위주로만 진행하며 또 틀린 문제의 경우 해당 개념을 찾아서 좀더 딥하게 공부를 진행했습니다. 3.노랭이 1회독 유선배를 끝낸 후 노랭이로 넘어왔습니다. 확연히 달라진 난이도에 당황을 하게됩니다. 문제도 길고 함정 문제도 있어서 유선배 풀 때에 비해 틀리는 문제가 당연히 많아지게 됩니다. '문제가 이렇게 나온다면 쉽지 않겠는데...?'라는 생각으로 부담감을 가졌으나, 노랭이 책은 SQLP 시험 준비와 겸용으로 사용되는 책이라고 하니, 실제 시험은 이것보다는 조금 더 쉬우니 많이 틀려도 주눅들지 않고 풀어보시는게 중요합니다. 노랭이는 문제만 수록된 문제집입니다. 개념 설명이 없지만 답지에 해설이 꽤나 자세히 작성되어 있으므로 해설을 잘 참고하시면 좋습니다. 3개의 대단원으로 구분되어 있는데 3단원은 SQLP에만 해당되는 내용이므로 2단원 까지만 풀어주시면 됩니다. 책은 두껍지만 3단원이 빠지고, 한 문제당 길이가 길어서 실제로는 몇 문제 되지 않으니 겁먹지 말고 차근차근 풀어보시기 바랍니다. 문제 풀이는 유선배를 풀 때와 같이 답을 직접 표시하지 않고, 틀리거나 헷갈린 문제에만 표시를 해두고 넘어갑니다. 노랭이에 나왔던 문제가 비슷하게 출제되는 경우가 있다고 하니 어렵지만 꼭 풀어보는 것을 권장드립니다. (어려운 문제를 미리 맛봐야 시험이 어렵게 출제되었을 때도 맨탈을 지킬 수 있습니다.) 4.노랭이 2회독 노랭이 1회독에서 표시했던 문제들을 다시 풀어 봅니다. 이해가 되지 않는 문제들은 유튜브에 "노랭이 00번 해설"과 같이 검색하면 미나쌤 등의 해설이 나오니, 교재의 해설만으로 이해가 어려울 때 활용하시면 좋습니다. (미나쌤은 어려운 문제만 올려놓으셨더라구요. 그래서 없을 수도 있습니다.) 5. 블로그 bruder 요약집 암기 구글링을 하여 발견한 블로그 입니다. 얼마나 열심히 공부하신 분인지 본인만의 요약집을 올려놓으셨더라구요. 요약집이라길래 10페이지 정도 될 줄 알았는데, 빽빽하게 작성된 103페이지 분량이었습니다. 블로그 글을 보니 국민대 김남규 교수님 강의, 홍쌤의 데이터랩, SQL 전문가 가이드 등 다양한 개념서의 내용을 집약해 놓은 서적이에서 이것만 보아도 시험을 칠 수 있겠다 싶은 수준입니다. 아마 제가 시간이 정말로 촉박했다면 이거 하나 달달 외워서 들어갔을 것 같네요. 그정도로 퀄리티가 아주 높고 강의를 수강해야만 하는 홍쌤의 데이터랩 개념도 정리되어 있다니 이만한 레버리지가 없습니다. 저는 시험장에 들어가기 직전까지 이 요약집을 열심히 보다가 들어갔습니다. 6. 문어CBT SQLD 등 다양한 시험의 CBT들을 가지고 있는 사이트입니다. 여러 문제 종류들이 있지만 그중에서 꼭 "복원 문제"를 풀어보시길 추천드립니다. SQLD는 시험 문제를 공개하는 시험이 아니므로, 시험을 보고 나온 사람들의 기억을 모아모아 집단지성으로 문제를 복원해냅니다. 그렇기 때문에 문제가 100% 다 똑같지 않을 수 있지만, 해당 문제에서 묻는 개념은 동일하다고 볼 수 있습니다. SQLD 시험은 24년부터 개정되었습니다. 따라서 그 이후인 52회차 부터 학습하시면 충분합니다. SQLD가 문제은행 형식의 시험은 아니지만 과거에 출제 했던 문제를 재출제 하는 경우가 있습니다. 제가 본 62회차 시험에서도 CBT에서 풀었던 문제가 똑같이 나와서 어려운 문제임에도 맞힐 수 있었습니다. 그리니 CBT문제 만큼은 52회부터 전부 꼭 풀어보고 시험장에 들어가시길 바라겠습니다. 이 CBT의 단점은 방심하게 된다는 점입니다. 위 단계를 똑같이 밟아 공부해오셨다면 CBT가 굉장히 쉽게 느껴지실 수 있습니다. 저도 9회의 문제를 푸는 동안 88~98점의 점수가 계속 나와서 '나 공부 그만해도 될지도..?'라는 유혹이 들었으나 혹시 몰라 계획한대로 끝까지 진행했는데, 실제 시험에서는 74점을 맞았으니 방심하지 말아야겠다는 생긱이 드시죠? 요약 인강 없이도 합격할 수 있다. 비전공자 노베이스라도 인강 없이 합격할 수 있다. 개념서 2회독 -> 노랭이 2회독 -> 요약집 암기 -> 문어CBT복원 문제 강추 시험은 CBT보다는 어렵고, 노랭이 보다는 쉽다. 다만, 함정이 엄청 많다. 방심하지 말것.
요즘은 멀리 보며 공부를 하는 중입니다. 지난주를 돌아보며 지난주에는 회귀, 분류, 군집처럼 여러 머신러닝 문제를 살펴보고, 시계열 분석과 자연어 처리까지 학습했다. 마지막에는 퍼셉트론과 TensorFlow를 통해 신경망의 기본 구조도 접했다. 모델마다 사용하는 알고리즘은 달랐지만 데이터를 준비하고, 학습시키고, 새로운 데이터로 평가하는 흐름은 반복된다는 것을 알게 됐다. 이번 주는 CNN을 활용한 이미지 분류와 FastAPI를 이용한 웹 애플리케이션 개발 을 학습했다. 이미지 분류에서는 모델이 이미지의 특징을 어떻게 찾아내는지 살펴봤고, 웹 개발에서는 사용자의 요청이 서버와 데이터베이스를 거쳐 화면으로 돌아오는 과정을 따라갔다. 처음에는 딥러닝과 웹 개발이 서로 다른 내용처럼 느껴졌다. 하지만 두 실습 모두 데이터를 어떤 형태로 받아서 처리하고, 결과를 어떻게 확인하거나 전달하는지 생각해야 한다는 공통점이 있었다. 이번 주의 핵심 이미지 분류 모델의 학습 과정을 이해하고, 웹 애플리케이션에서는 화면·요청 처리·데이터 저장·사용자 권한이 연결되는 구조를 살펴봤다. CNN을 이용한 이미지 분류 이미지를 숫자 배열로 바라보기 먼저 CIFAR-10 데이터를 이용해 이미지 분류 모델을 만들었다. CIFAR-10은 비행기, 자동차, 새, 고양이 등 10개의 범주로 구성된 이미지 데이터셋이다. 훈련 데이터는 50,000장, 테스트 데이터는 10,000장이었고, 이미지 한 장의 크기는 32 × 32 × 3 이었다. 마지막의 3은 RGB 색상 채널을 의미한다. 이미지 한 장: (32, 32, 3) 높이 너비 RGB 훈련 데이터: (50000, 32, 32, 3) 사람은 사진을 보고 바로 고양이나 자동차를 떠올리지만, 모델이 받는 입력은 픽셀 값을 가진 배열이다. 그래서 이미지를 출력해 확인하는 것과 함께 배열의 모양, 정답 레이블의 형태를 확인하는 과정이 먼저 필요했다. 합성곱과 풀링의 역할 CNN에서는 Conv2D 로 이미지의 특징을 추출하고, MaxPool2D 로 특징 맵의 가로·세로 크기를 줄이는 과정을 반복했다. 합성곱은 작은 필터를 이미지의 여러 위치에 적용하면서 패턴을 찾는 과정으로 이해했다. 필터가 학습되면서 이미지 분류에 필요한 특징을 찾아내는 것이다. 풀링은 주변 영역에서 대표 값을 선택해 크기를 줄인다. 이번 실습에서는 최대 값을 선택하는 최대 풀링을 사용했다. from tensorflow.keras.layers import Conv2D, MaxPool2D model.add(Conv2D( filters=32, kernel_size=3, padding="same", activation="relu" )) model.add(MaxPool2D(pool_size=(2, 2), strides=2)) filters=32 는 32개의 필터로 특징 맵을 만들겠다는 의미이고, kernel_size=3 은 3 × 3 크기의 필터를 사용한다는 의미였다. 실습의 기본 보폭에서 padding="same" 은 가장자리를 채워 합성곱 전후의 가로·세로 크기를 유지하도록 했다. 특징 추출이 끝난 뒤에는 Flatten 으로 배열을 펼치고, Dense 층을 거쳐 10개 범주에 대한 예측값을 만들었다. RGB 이미지 ↓ 합성곱 → 풀링 반복 ↓ Flatten ↓ Dense ↓ 10개 범주에 대한 예측 아직 필터가 학습되는 계산을 완전히 설명하기는 어렵다. 그래도 층을 하나씩 추가하는 코드가 단순한 반복이 아니라, 이미지에서 특징을 추출하고 최종 분류로 연결하는 구조라는 점은 조금 이해할 수 있었다. 레이블과 손실 함수의 관계 CIFAR-10 실습에서는 정답을 원-핫 인코딩하지 않고 정수 레이블로 사용했기 때문에 sparse_categorical_crossentropy 를 선택했다. 출력층에는 10개 범주 중 하나를 고르는 문제에 맞게 softmax 를 사용했다. 5번의 epoch를 학습한 노트북의 테스트 정확도는 약 67.26% 였다. 이 값은 해당 실행과 설정에서 나온 결과로, CNN을 사용하면 항상 같은 정확도가 나온다는 뜻은 아니다. 학습 정확도와 검증 정확도를 함께 그래프로 보면서, 학습 데이터에서 성능이 좋아지는 것과 새로운 데이터에서도 잘 예측하는 것은 따로 확인해야 한다는 점을 다시 느꼈다. 다중 클래스와 다중 레이블 다음으로 의류 이미지의 색상과 종류를 함께 예측하는 실습을 진행했다. CIFAR-10은 한 이미지에 하나의 범주를 선택하는 다중 클래스 분류였다. 반면 의류 이미지는 한 장에 green 과 shirt 처럼 여러 레이블이 동시에 붙을 수 있는 다중 레이블 분류였다. 구분 정답의 예 실습의 출력층·손실 함수 다중 클래스 고양이 또는 자동차 softmax · sparse categorical crossentropy 다중 레이블 초록색이면서 셔츠 sigmoid · binary crossentropy 의류 데이터에서는 색상 6개와 의류 종류 5개를 합쳐 총 11개의 레이블을 만들었다. 해당하는 위치는 1, 나머지는 0으로 표시했다. green_shirt ↓ [black, blue, brown, green, red, white, dress, shirt, pants, shorts, shoes] [ 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0] 각 레이블에 해당할 가능성을 별도로 출력해야 하므로 마지막 층에는 sigmoid 를 사용했다. 같은 이미지 분류라도 정답을 어떻게 정의하느냐에 따라 레이블 구성과 출력층, 손실 함수가 달라진다 는 점이 중요하게 느껴졌다. 이미지 데이터 준비와 학습 관리 이미지 경로와 레이블을 함께 관리하기 의류 실습에서는 11,385장의 이미지 파일을 찾아 경로를 모으고, 폴더 이름에서 색상과 의류 종류를 분리해 레이블을 만들었다. 데이터를 훈련·검증·테스트용으로 나눈 뒤 이미지 경로와 11개 레이블을 DataFrame에 담고 CSV로 저장했다. 이미지를 직접 CSV에 넣는 것이 아니라, 이미지를 찾을 경로와 정답 정보 를 표로 관리한 것이다. 이후 ImageDataGenerator 를 이용해 이미지를 배치 단위로 읽었다. 원본 이미지마다 크기가 달랐기 때문에 입력 크기를 112 × 112 로 맞추고, 픽셀 값을 255로 나누어 0~1 범위로 정규화했다. 이미지 파일 + 폴더 이름 ↓ 경로와 레이블을 담은 DataFrame·CSV ↓ 이미지 제너레이터 ↓ 크기 통일·정규화·배치 생성 ↓ 모델 학습 여기서 배치는 한 번에 처리하는 데이터 묶음이고, epoch는 훈련 데이터를 한 바퀴 학습하는 단위였다. 배치 크기가 32이고 훈련 이미지가 5,578장이면, 남는 데이터까지 포함해 한 epoch에 175번의 배치 처리가 필요했다. 테스트 이미지를 예측할 때는 shuffle=False 로 순서를 유지했다. 그래야 예측값과 원래 이미지 경로를 같은 순서로 연결할 수 있다. 과적합과 Dropout 모델에는 Dropout 도 추가했다. 학습 중 일부 출력을 무작위로 0으로 만들어 특정 연결에 지나치게 의존하지 않도록 하는 방법이었다. 처음에는 학습을 많이 하거나 층을 늘리면 성능도 계속 좋아질 것 같았다. 하지만 훈련 손실이 줄어들어도 검증 손실이 다시 커질 수 있다는 것을 보면서, 모델의 복잡도와 반복 횟수를 함께 생각해야 한다는 점을 알게 됐다. 학습 결과를 볼 때 훈련 성능뿐 아니라 검증 성능도 함께 확인해야 한다. 훈련 데이터에 익숙해진 모델이 새로운 데이터에서도 잘 동작하는지는 별도의 문제다. 체크포인트와 조기 종료 검증 손실이 좋아졌을 때 가중치를 저장하는 ModelCheckpoint 와, 개선이 멈추면 학습을 종료하는 EarlyStopping 을 사용했다. checkpoint = tf.keras.callbacks.ModelCheckpoint( "best_performed_model.weights.h5", save_weights_only=True, save_best_only=True, monitor="val_loss" ) early_stop = tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=2 ) patience=2 는 검증 손실이 개선되지 않는 epoch를 2번 허용하는 설정이다. 실습에서는 콜백을 적용한 추가 학습이 예정된 10번을 모두 채우기 전에 종료됐다. 가중치만 저장한 경우에는 동일한 구조의 모델을 다시 만든 뒤 load_weights() 로 읽어야 했다. CIFAR-10 실습에서 모델 전체를 저장하고 불러온 방식과 비교하니, 저장하는 대상이 모델 전체인지 가중치인지 구분할 필요가 있었다. 데이터 증식 마지막으로 고양이 이미지 한 장을 회전하거나 뒤집고, 밝기·크기·위치를 바꾸어 여러 이미지로 만드는 데이터 증식을 살펴봤다. 같은 대상을 다양한 모습으로 보여주어 학습 데이터의 다양성을 높이는 방법으로 이해했다. 다만 어떤 변형이든 적용해도 되는 것은 아니고, 변형 후에도 원래 레이블의 의미가 유지되는지 생각해야 한다. 이번 노트북에서는 변형된 이미지를 출력해 확인했다. 증식 설정을 실제 분류 모델에 적용했을 때 성능이 얼마나 달라지는지는 다음에 직접 비교해 보고 싶다. FastAPI와 웹 요청의 구조 Python 함수가 웹 요청을 처리하기까지 이미지 분류 이후에는 FastAPI를 이용한 웹 개발을 시작했다. Python으로 작성한 함수가 URL과 HTTP 메서드에 연결되어 사용자의 요청을 처리하는 구조였다. from fastapi import FastAPI app = FastAPI() @app.get("/") async def welcome() -> dict: return {"message": "Hello World"} 브라우저에서 해당 주소로 GET 요청을 보내면 함수가 실행되고, 반환한 딕셔너리는 JSON 응답으로 전달됐다. 서버는 Uvicorn으로 실행하고, /docs 에서는 API 목록과 입력 형식을 확인하며 요청을 보내볼 수 있었다. 할 일 API와 CRUD 첫 실습에서는 Python 리스트에 할 일을 저장하고 등록·조회·수정·삭제 기능을 만들었다. 기능 HTTP 메서드 경로 예시 목록 조회 GET /todo 한 건 조회 GET /todo/{todo_id} 등록 POST /todo 수정 PUT /todo/{todo_id} 삭제 DELETE /todo/{todo_id} 같은 경로라도 HTTP 메서드에 따라 다른 기능을 실행할 수 있었다. 또 {todo_id} 처럼 URL 안에 들어가는 값은 경로 매개변수이고, ?page=1 처럼 전달하는 값은 쿼리 매개변수라는 차이도 살펴봤다. Pydantic과 APIRouter 입력 데이터의 모양은 Pydantic의 BaseModel 로 정의했다. 타입 힌트를 통해 필드의 자료형을 지정하면 입력을 검증하고, 변환 가능한 값은 해당 타입으로 변환하는 데 활용할 수 있었다. from pydantic import BaseModel class Todo(BaseModel): id: int item: str 기능별 요청은 APIRouter 로 묶고, 애플리케이션에서 include_router() 로 등록했다. 처음에는 파일을 나누는 것이 번거롭게 느껴졌지만, 기능이 늘어나니 데이터 모델과 요청 처리 코드를 구분하는 이유가 조금 보였다. 요청 처리의 기본 흐름 URL과 HTTP 메서드로 실행할 함수를 찾고, 입력을 검증한 뒤 필요한 작업을 수행해 응답을 반환한다. HTML 화면과 데이터베이스 연결 실습 코드를 실행한 할 일 목록 화면. 화면 설명을 위해 캡처용 예시 데이터를 사용했다. JSON 응답에서 HTML 화면으로 다음 실습에서는 Jinja2 템플릿을 연결해 할 일 목록을 HTML 화면으로 출력했다. 서버에서 조회한 데이터를 템플릿에 전달하고, 템플릿의 반복문으로 목록을 만들었다. {% for todo in todos %} <p>{{ todo.item }}</p> {% endfor %} HTML 폼에서 입력한 값은 Form 으로 받았다. 수정이나 삭제 작업 이후에는 RedirectResponse 와 303 상태 코드를 사용해 목록 페이지로 다시 GET 요청을 보내도록 했다. 폼 입력 → POST 요청 → 데이터 변경 ↓ 303 리다이렉트 ↓ GET으로 목록 조회 이 과정을 따라가면서 화면에 입력한 내용이 바로 저장되는 것이 아니라, 폼의 필드 이름과 서버 매개변수가 연결되고 요청 처리 함수가 실행된다는 점을 이해했다. 메모리 저장에서 SQLite로 처음 사용한 리스트는 서버가 종료되면 저장한 데이터가 사라진다. 이후에는 SQLite와 SQLModel을 연결해 데이터를 파일 기반 데이터베이스에 저장했다. from sqlmodel import Field, SQLModel class Todo(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) item: str table=True 로 테이블에 대응하는 모델을 정의하고, 기본키인 id 는 데이터베이스가 생성하도록 했다. 데이터 등록과 수정에는 session.add() , 변경 확정에는 session.commit() , 삭제에는 session.delete() 를 사용했다. session.add(Todo(item=item)) session.commit() 의존성 주입과 애플리케이션 구조 각 요청 처리 함수에는 Depends(get_session) 으로 데이터베이스 세션을 전달했다. 공통으로 필요한 준비 작업을 별도 함수로 두고, FastAPI가 실행해 그 결과를 전달하는 방식으로 이해했다. 프로젝트에서는 main.py 가 애플리케이션과 라우터를 연결하고, routes 는 요청 처리, models 는 데이터 구조, database 는 연결과 세션, templates 는 화면을 담당했다. 브라우저 요청 ↓ main.py → routes ↓ models + database ↓ templates ↓ HTML 응답 이전에 배운 아키텍처가 아직 추상적으로 느껴졌는데, 이번에는 파일마다 담당하는 역할을 직접 확인할 수 있었다. 특히 기능이 늘어나도 데이터베이스 연결이나 공통 템플릿을 함께 사용할 수 있다는 점이 인상적이었다. 이벤트 기능에서도 같은 구조를 사용해 제목, 설명, 장소와 태그를 저장했다. 쉼표로 입력한 태그를 리스트로 만들고 JSON 형태로 저장하면서, 화면의 입력 형태와 저장 형태가 다를 수 있다는 점도 배웠다. 이벤트 입력 폼과 데이터베이스에서 조회한 목록을 같은 화면에 표시한 모습. 로그인 상태와 사용자 권한 이메일과 비밀번호를 입력하는 회원가입·로그인 화면. 회원가입과 로그인 흐름 회원가입에서는 이메일 형식을 확인하고, 이미 등록된 이메일인지 조회한 뒤 사용자 정보를 저장했다. 로그인에서는 계정을 조회하고 비밀번호를 비교한 뒤 성공 여부에 따라 응답을 달리했다. 실습 코드는 기본 흐름을 확인하기 위해 비밀번호를 직접 저장하고 비교했다. 실제 서비스에서는 비밀번호 해시 등 추가 처리가 필요하다는 점도 구분해서 생각해야 한다. 세션으로 로그인 상태 유지하기 로그인 성공 후에는 세션에 이메일을 저장했다. SessionMiddleware 가 서명된 쿠키의 세션 정보를 읽어 요청에서 사용할 수 있게 했고, 로그아웃할 때는 세션을 비웠다. 로그인 정보 제출 ↓ 계정 확인 ↓ 세션에 사용자 정보 기록 ↓ 다음 요청에서 로그인 상태 확인 처음에는 로그인도 한 번의 함수 실행 정도로 생각했지만, 로그인 이후의 여러 요청에서도 사용자를 확인해야 한다는 점에서 상태 관리가 필요했다. 로그인 확인과 권한 확인 할 일과 이벤트 기능에는 Depends(require_login) 을 연결해 로그인한 사용자만 접근하도록 했다. 게시판은 종류와 작업에 따라 별도로 권한을 확인했다. 기능 실습에서 적용한 규칙 공지사항 읽기 로그인 여부와 관계없이 가능 공지사항 작성 관리자만 가능 자유게시판 읽기·작성 로그인 필요 게시글 수정·삭제 작성자 또는 관리자만 가능 로그인은 사용자가 누구인지 확인하는 과정이고, 권한 검사는 그 사용자가 어떤 작업을 할 수 있는지 확인하는 과정이었다. 화면에 버튼을 보여줄지 결정하는 것과 서버에서 작업을 허용할지 결정하는 것은 각각 필요하다. 서버에서도 요청자의 권한을 확인해야 한다. 또 로그인하지 않은 HTML 요청은 로그인 화면으로 이동시키고, API 요청에는 JSON 오류 응답을 반환했다. 같은 상황에서도 요청한 쪽이 화면인지 API인지에 따라 응답 형태를 고려하는 점이 흥미로웠다. 게시판과 첨부파일 처리 게시판 기능으로 구조 확장하기 게시판에서는 공지사항과 자유게시판을 구분하고, 제목·내용·작성자·등록 시간·조회수·첨부파일 경로를 관리했다. 게시판 종류는 Enum으로 정해진 값만 사용하도록 했고, 제목과 내용은 저장 전에 공백을 정리하고 유효성을 검사했다. 상세 화면에서는 조회수를 증가시키고, 수정 화면에서는 기존 내용을 폼에 채웠다. 할 일 실습에서 배운 CRUD가 게시판에도 반복됐지만, 여기에 권한 확인과 입력 검증이 추가되면서 실제 기능은 여러 단계로 이루어진다는 것을 느꼈다. 페이지네이션 게시글은 한 페이지에 10개씩 조회하고, 페이지 번호는 5개씩 묶어 표시했다. 전체 건수로 페이지 수를 계산하고, offset 과 limit 으로 현재 페이지에 필요한 데이터만 조회했다. 캡처용 게시글 12개 중 첫 페이지의 10개와 페이지 이동 링크를 표시한 화면. posts = session.exec( select(Board) .where(Board.category == category) .order_by(Board.id.desc()) .offset((page - 1) * PAGE_SIZE) .limit(PAGE_SIZE) ).all() 예를 들어 2페이지라면 앞의 10개를 건너뛰고 다음 10개를 가져오는 구조였다. 화면의 페이지 번호와 실제 데이터베이스 조회 범위를 함께 계산해야 한다는 점을 알게 됐다. 파일 업로드와 다운로드 자유게시판에는 첨부파일 기능도 추가했다. HTML 폼에 enctype="multipart/form-data" 를 지정하고, 서버에서는 UploadFile 로 파일을 받았다. 제목·내용 입력과 파일 선택이 가능한 게시글 작성 화면. 입력 내용은 캡처용 예시다. 파일은 무작위 식별자로 만든 폴더에 저장하고, 데이터베이스에는 파일 자체 대신 상대 경로를 기록했다. 실습에서는 파일 크기를 최대 10MB로 제한하고, 1MB씩 나누어 읽으면서 저장했다. 파일 선택과 폼 제출 ↓ 권한·입력값 확인 ↓ 파일 저장 → 저장 경로 확보 ↓ 게시글과 경로를 데이터베이스에 기록 첨부파일을 교체할 때는 새 파일을 저장하고 데이터베이스 변경이 성공한 뒤 기존 파일을 정리했다. 게시글을 삭제할 때도 연결된 파일을 함께 정리했다. 다운로드에서는 게시글의 읽기 권한을 확인하고, 저장 경로가 업로드 폴더 안에 있는지와 실제 파일이 존재하는지 확인한 뒤 FileResponse 를 반환했다. 첨부파일에서 중요한 관계 게시글 정보는 데이터베이스에, 실제 파일은 폴더에 저장된다. 수정·삭제 중 문제가 생기면 두 저장 상태가 어긋날 수 있으므로 파일과 데이터베이스를 함께 생각해야 한다. 처음에는 업로드가 파일을 저장하는 한 단계라고 생각했지만, 기존 파일 교체와 오류 처리, 권한 확인까지 연결되어 있었다. 기능이 많아질수록 정상적인 경우뿐 아니라 중간에 실패한 경우도 고려해야 한다는 점이 기억에 남았다. 6주차 회고 이번 주에는 CNN을 활용한 이미지 분류와 FastAPI 웹 개발을 함께 경험했다. 이미지 분류에서는 합성곱과 풀링으로 특징을 추출하고, 정답의 형태에 따라 출력층과 손실 함수를 다르게 구성했다. 또 검증 손실을 기준으로 학습을 관리하고, 좋은 가중치를 저장하는 과정도 살펴봤다. 웹 개발에서는 간단한 할 일 API가 HTML 화면과 데이터베이스를 갖춘 애플리케이션으로 확장됐다. 이후 로그인과 권한, 게시판과 첨부파일이 추가되면서 각각의 기능이 독립적으로 끝나는 것이 아니라 여러 구성 요소를 거쳐 동작한다는 것을 확인했다. 아직은 예제를 따라가며 각 코드의 역할을 이해하는 단계다. 그래도 파일 이름과 함수만 볼 때보다 요청이나 데이터가 이동하는 순서를 그려보면 전체 구조가 조금 더 잘 보였다. 이번 주 회고: 4L Liked 이미지를 분류한 결과를 원본 이미지와 함께 확인할 수 있어 모델의 출력을 이해하기 쉬웠다. 색상과 의류 종류를 동시에 예측하는 실습에서는 다중 레이블이라는 개념이 구체적으로 느껴졌다
이번 블로그에서는 자바의 클래스와 객체에 대해 공부해보겠습니다. 객체 지향 언어 객체 지향 언어는 실세계의 객체를 프로그램 내에서 표현하기 위해 클래스와 객체를 도입하였다. 객체 지향 언어의 특성 캡슐화 캡슐화란 객체를 캡슐로 싸서 내부를 보호하고 볼 수 없게 하는 것으로 객체의 본질적인 특징이다. 객체는 캡슐화가 기본 원칙이지만 외부와의 접속을 위해 몇 부분만 공개 노출한다. 자바에서 객체는 클래스라는 캡슐을 사용하며, 필드(멤버 변수)와 메소드(멤버 함수)로 구성된다. 클래스 선언: class Animal { String name; int age; void eat() {...} void speak() {...} void love() {...} } 클래스 모양으로 만들어진 객체: String name "Lion" int age 4 void eat(); void speak(); void love(); 상속 자바의 상속은 자식 클래스가 부모 클래스의 속성을 물려받고 기능을 추가하여 확장하는 개념이다. 자바에서 부모 클래스를 슈퍼 클래스라고 부르며 자식 클래스를 서브 클래스라고 부른다. 상속은 슈퍼 클래스의 필드와 메소드를 물려받아 코드를 재사용함으로써, 코드 작성에 드는 시간과 비용을 줄인다. class Animal { String name; int age; void eat() {...} void sleep() {...} void love() {...} } class Human extends Animal { String hobby; String job; void work() {...} void cry() {...} void laugh() {...} } 다형성 다형성은 같은 이름의 메소드가 클래스 혹은 객체에 따라 다르게 동작하도록 구현되는 것을 말한다. 예를 들어, 강아지, 고양이, 닭 클래스는 Animal 클래스를 상속받고, 소리내기 메소드를 각각 다르게 구현하였다. 이것은 슈퍼 클래스에 구현된 메소드를, 서브 클래스에서 동일한 이름으로 자신의 특성에 맞게 다시 구현하는 이른바 메소드 오버라이딩으로 불린다. 다형성의 또 다른 사례는 클래스 내에서 이름이 같지만 서로 다르게 동작하는 메소드를 여러 개 만드는 메소드 오버로딩이 있다. 객체 지향 언어의 목적 객체 지향 언어는 절차 지향 언어의 단점을 보완하고 다음의 목적을 달성하기 위해 탄생하였다. 1. 소프트웨어의 생산성 향상 2. 실세계에 대한 쉬운 모델링 자바 클래스 만들기 클래스의 구성 클래스의 구성 요소를 멤버라고 부르며, 멤버는 필드와 메소드 두 가지다. public class Circle { public int radius; public String name; public Circle() { } public double getArea() { return 3.14*radius*radius; } } 클래스 선언, class Circle 이 코드는 이름이 Circle인 클래스를 선언한다. class 키워드와 클래스 이름으로 선언하고 중괄호 안에 필드와 메소드를 모두 작성한다. 필드와 메소드 객체 내에 값을 저장할 멤버 변수를 필드라고 부른다. radius와 name이 이에 해당한다. 메소드는 함수이며 객체의 행동을 구현한다. getArea() 메소드는 Circle 객체의 반지름 정보를 이용하여 면적을 계산하여 알려준다. 접근 지정자, public Circle이나 필드, 메소드에 붙은 public을 접근 지정자라고 한다. public은 다른 클래스에서 활용하거나 접근할 수 있음을 선언한다. 접근 지정자를 생략할 때 디폴트 접근이라고 부르며, 접근 지정자는 뒤에서 자세히 다룬다. 생성자 클래스의 이름과 동일한 메소드를 특별히 생성자라고 한다. 객체가 생성될 때 자동으로 호출되는 특별한 메소드이다. new 연산자와 객체 생성, 그리고 레퍼런스 변수 앞서 작성한 Circle 클래스의 객체를 생성하고 활용해보자. public static void main(String args[]) { Circle pizza; pizza = new Circle(); pizza.radius = 10; pizza.name = "자바피자"; double area = pizza.getArea(); } 레퍼런스 변수 선언 객체를 생성하기 전, 객체를 가리킬 레퍼런스 변수를 먼저 선언한다. 타입의 객체를 가리킬 레퍼런스 변수 pizza를 선언하는 문장이다. Circle pizza; 이 선언문으로는 Circle 타입의 객체가 생성되지 않는다. 이는 객체에 대한 주소를 가지는 변수일 뿐 객체 자체는 아니다. 객체 생성 반드시 new 연산자를 사용하여 다음과 같이 객체를 생성한다. pizza = new Circle(); 객체 멤버 접근 접근을 위해서는 객체 레퍼런스.멤버 이런식으로 접근한다. pizza.radius = 10; int r = pizza.radius; double area = pizza.getArea(); 생성자 생성자의 개념과 목적 생성자는 객체가 생성될 때 객체의 초기화를 위해 실행되는 메소드이다. 생성자 선언 및 활용 public class Circle { int r; String name; public Circle() { radius = 1; name = ""; } public Circle(int r, String n) { radius = r; name = n; } } 생성자의 이름은 클래스와 동일하다. 생성자는 여러 개 작성할 수 있다. 생성자는 new를 통해 객체를 생성할 때 한 번만 호출된다. 생성자에 리턴 타입을 지정할 수 없다. 생성자의 목적은 객체가 생성될 때, 필요한 초기 작업을 위함이다. 기본 생성자 기본 생성자란 매개변수와 실행 코드가 없어 아무 일도 일어나지 않고 단순 리턴하는 생성자이다. class Circle { public Circle() { } } 기본 생성자가 자동으로 생성되는 경우 생성자가 없는 클래스는 있을 수 없다. 그러므로 생성자가 하나도 없는 경우, 컴파일러는 기본 생성자를 자동으로 생성한다. this 레퍼런스 this는 객체 자신을 가리키는 레퍼런스이다. this의 기초 개념 this는 현재 객체 자신에 대한 레퍼런스이다. 보다 정확히 말하면 현재 실행되는 메소드가 속한 객체에 대한 레퍼런스이다. public class Circle { int radius; public Circle(int r) { this.radius = r; } } this는 현재 객체에 대한 레퍼런스이므로, this.radius는 현재 객체의 멤버 radius에 접근한다. this의 필요성 앞의 Circle 클래스에서 메소드 getRadius()는 다음과 같이 this를 사용하지 않았다. 클래스 내에서 멤버 radius를 접근할 때 굳이 this.radius로 할 필요가 없다. 그렇다면 this는 언제 필요한가? 매개변수의 이름은 그 자체로서 코드를 읽는 사람에게 그 용도를 나타내므로, 적합한 이름을 붙이는 것은 매우 중요하다. 그래서 Circle(int r) 생성자의 매개변수를 r 대신 다음과 같이 radius로 변경하는 것이 좋다. 하지만 이렇게 변경하면 2개의 radius는 모두 매개변수 radius를 접근 하기 때문에, 멤버 radius를 변경하지 못한다. 이럴 때 this를 사용하면 된다. public Circle(int radius) { this.radius = radius; } this()로 다른 생성자 호출 public class Book { String title; String author; void show() { System.out.println(title + " " + author); } public Book() { this("", ""); } public Book(String title) { this(title, "작자미상"); } public Book(String title, String author) { this.title = title; this.author = author; } } this사용 시 주의할 점 반드시 생성자 코드에서만 호출 반드시 같은 클래스 내 다른 생성자 호출할 때 사용 생성자의 첫문장이어야함 객체 배열 자바에서는 객체를 원소로 하는 배열도 만들 수 있다. Circle [] c; c = new Circle[5]; 메소드 활용 메소드는 객체의 동작을 정의한다. 기본적인 형태는 다음과 같다. 반환형 메소드이름(매개변수) { 실행할 코드 } 메소드를 호출하면 매개변수로 값을 전달할 수 있고, return을 통해 결과를 반환할 수 있다. 메소드 오버로딩 자바에서는 같은 이름의 메소드를 여러 개 정의할 수 있다. 단, 매개변수의 개수나 타입이 달라야 한다. int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } 이것을 메소드 오버로딩(Method Overloading)이라고 한다. 반환형만 다른 것은 오버로딩이 될 수 없다. // 불가능 int add(int a, int b) double add(int a, int b) 객체의 소멸과 가비지 컬렉션 자바에서는 객체를 직접 삭제하는 기능을 사용하지 않는다. 객체를 더 이상 참조하는 변수가 없으면 해당 객체는 가비지(Garbage)가 된다. 자바의 가비지 컬렉터(Garbage Collector)는 이러한 객체를 자동으로 찾아 메모리를 회수한다. 예를 들어, Car car = new Car(); car = null; 이후 기존 Car 객체를 가리키는 참조가 없다면 해당 객체는 가비지가 될 수 있다. 따라서 자바에서는 C/C++처럼 개발자가 직접 메모리를 해제할 필요가 없다. 접근 지정자 클래스의 필드나 메소드에 다른 클래스가 접근할 수 있는 범위를 지정할 수 있다. 대표적인 접근 지정자는 다음과 같다. 접근 지정자 접근 범위 private : 같은 클래스 default : 같은 패키지 protected : 같은 패키지 + 상속 관계 public : 모든 클래스 private int speed; private으로 선언하면 해당 클래스 외부에서 직접 접근할 수 없다. 따라서 필요한 경우 메소드를 통해 접근하도록 만든다. public int getSpeed() { return speed; } public void setSpeed(int speed) { this.speed = speed; } 이처럼 데이터를 직접 노출하지 않고 메소드를 통해 접근하는 방식은 객체지향 프로그래밍에서 중요한 개념이다. static 멤버 static이 붙은 필드나 메소드는 객체에 소속되는 것이 아니라 클래스에 소속된다. 일반적인 멤버는 객체마다 각각 존재한다. 반면 static 멤버는 클래스에 하나만 존재하며 여러 객체가 공유한다. class Student { static int count = 0; } 객체를 여러 개 만들어도 count는 하나를 공유한다. 또한 static 메소드는 객체를 생성하지 않고 클래스 이름으로 호출할 수 있다. Student.someMethod(); static 메소드의 특징 static 메소드에서는 일반적인 인스턴스 멤버를 직접 사용할 수 없다. 왜냐하면 static 메소드는 특정 객체에 속하지 않기 때문이다. final final은 변경할 수 없도록 만드는 키워드이다. 변수에 final을 사용하면 값을 변경할 수 없다. final int MAX = 100; 이후 MAX = 200; 처럼 값을 변경할 수 없다. 따라서 final 변수는 상수(constant)를 표현할 때 자주 사용한다. 예제 자바 클래스를 만들어보자. 다음 main() 메소드를 실행하였을 때 예시와 같이 출력되도록 TV 클래스를 작성하라. class TV { private String brand; private int inch; private int price; public TV(String brand, int inch, int price) { this.brand = brand; this.inch = inch; this.price = price; } public void show() { System.out.println(brand + "에서 만든 " + price + "만원짜리 " + inch + "인치 TV"); } } public class TVTest { public static void main(String[] args) { TV tv = new TV("Samsung", 50, 300); tv.show(); } } 3. Grade는 한 학생의 점수를 나타내는 클래스이다. 이름과 3개의 과목 점수를 각각 입력받아 Grade 객체를 생성해서 성적 평균을 출력하는 main()과 실행 예시는 다음과 같다. main()을 포함하는 Grade 클래스를 작성하라. import java.util.Scanner; class Grade { private String name; private int java; private int web; private int os; public Grade(String name, int java, int web, int os) { this.name = name; this.java = java; this.web = web; this.os = os; } public double getAverage() { return (java + web + os) / 3.0; } } public class GradeTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("이름, 자바, 웹프로그래밍, 운영체제 순으로 점수 입력>>"); String name = scanner.next(); int java = scanner.nextInt(); int web = scanner.nextInt(); int os = scanner.nextInt(); Grade st = new Grade(name, java, web, os); System.out.println(name + "의 평균은 " + st.getAverage()); scanner.close(); } } 5. 노래 한 곡을 나타내는 Song 클래스를 작성하라. Song 클래스의 필드는 다음과 같다. 노래의 제목 title 가수 이름 singer 발표 년도 year 가수 나라 lang 또한 Song 클래스에는 다음 메소드들이 있고, main()의 실행 결과는 다음과 같다. 노래 제목, 가수 이름, 발표 년도, 가수 나라의 4개의 매개변수를 받아, 객체의 각 필드를 초기화하는 생성자 노래 정보를 출력하는 show() 메소드 main() 메소드는 "가로수 그늘 아래 서면", "이문세", 1988, "한국"을 매개변수로 하여 Song 객체를 생성하고, 이 객체의 show()를 호출하여 노래 정보를 다음과 같이 출력한다. 1988년 한국의 이문세가 부른 가로수 그늘 아래 서면 class Song { private String title; private String singer; private int year; private String lang; public Song(String title, String singer, int year, String lang) { this.title = title; this.singer = singer; this.year = year; this.lang = lang; } public void show() { System.out.println(year + "년 " + lang + "의 " + singer + "가 부른 " + title); } } public class SongTest { public static void main(String[] args) { Song song = new Song( "가로수 그늘 아래 서면", "이문세", 1988, "한국" ); song.show(); } } 7. 1개의 메모 정보를 담는 Memo 클래스를 작성하라. Memo는 생성자를 비롯하여 다음과 같은 멤버를 갖는다. String 타입의 name, content 필드 // 메모 작성자, 메모 시점, 메모 텍스트 boolean isSameName() // 메모 작성자가 같으면 true 리턴, 아니면 false 리턴 String getName() // 메모 작성자 이름 리턴 void show() // 메모 출력 int length() // 메모 텍스트의 길이 리턴 Memo 객체를 생성하고 다루는 main() 함수와 실행 예시는 다음과 같다. class Memo { private String name; private String time; private String content; public Memo(String name, String time, String content) { this.name = name; this.time = time; this.content = content; } public boolean isSameName(Memo other) { return name.equals(other.name); } public String getName() { return name; } public void show() { System.out.println(name + ", " + time + " " + content); } public int length() { return content.length(); } } public class MemoTest { public static void main(String[] args) { Memo a = new Memo("유승민", "10:10", "자바 과제 있음"); Memo b = new Memo("박재현", "10:15", "시키고도 어려운 연습가요!"); Memo c = new Memo("김정미", "11:30", "사랑하는 사람이 생겼어요."); a.show(); if (a.isSameName(b)) System.out.println("동일한 사람입니다."); else System.out.println("다른 사람입니다."); System.out.println(c.getName() + "가 작성한 메모의 길이는 " + c.length()); } } 9. 숨겨진 숫자에 가장 가까운 수를 제시하는 사람이 이기는 예측 게임을 작성해보자. 1~100 범위의 정수를 랜덤하게 1개 생성하여 게임에 참여한 선수들에게 숫자를 추측하게 한 후 숨겨진 답에 가장 가까운 선수가 승리하며 1점을 부여한다. 게임이 여러 번 반복되어 승점이 많은 사람이 최종 승자가 된다. 게임에 참여하는 사람을 Player 클래를 만들고 이곳에 선수 이름과 누적 점수를 저장한다. main()을 포함하는 게임 프로그램의 클래스는 GuessGame으로 하며 실행 예시는 다음과 같다. import java.util.Scanner; class Player { String name; int score; public Player(String name) { this.name = name; this.score = 0; } } public class GuessGame { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("*** 예측 게임을 시작합니다. ***"); System.out.print("게임에 참여할 선수 수>>"); int n = scanner.nextInt(); Player[] players = new Player[n]; for (int i = 0; i < n; i++) { System.out.print("선수 이름>>"); players[i] = new Player(scanner.next()); } while (true) { int hiddenAnswer = (int)(Math.random() * 100) + 1; System.out.println( "1~100사이의 숫자가 결정되었습니다. 선수들은 맞추어 보세요." ); int[] numbers = new int[n]; for (int i = 0; i < n; i++) { System.out.print(players[i].name + ">>"); numbers[i] = scanner.nextInt(); } int minDiff = Integer.MAX_VALUE; int winner
1. 반복문 1) while / for # while문 초기값 while 조건문: 실행문장 1 실행문장 2 증감치 설정 # 위치 변경 가능 ... # for문 초기값 for 변수 in range(a, b, c): 실행문장 1 실행문장 2 ... range(n): n - 1까지 range(a, b): a부터 b - 1까지 range(a, b, c): a부터 b - 1까지 c만큼 증가 for i in range(5): # 0부터 4까지 print(i, end=" ") print() for i in range(1,10): # 1에서 9까지 print(i, end=" ") print() for i in range(1,15,2): # 1에서 14까지 2씩 증가 print(i,end=" ") 0 1 2 3 4 1 2 3 4 5 6 7 8 9 1 3 5 7 9 11 13 2) 반복문의 제어 break 조건이 참일 경우, 반복문을 탈출함 5개의 데이터를 입력 받아 처리하며, 예외값(0~100사이의 값이 아닌 경우)을 만나면 반복을 멈추기 continue 조건이 참일 경우, continue 다음 반복문을 실행하지 않고 반복을 계속함 예 10개의 값을 입력하여 합을 구하며, 3의 배수는 합을 구하지 않고 제외함 3) break 예제 i = 1 while True: # 무한반복 print(i) if i == 5: break i = i + 1 1 2 3 4 5 sum = 0 while True: score = int(input("Score: ")) if score < 0 or score > 100: break sum = sum + score print("Sum: ", sum) Score: 40 Score: 40 Score: 20 Score: 100 Score: 100 Score: 200 Sum: 300 pw = "0000" i = 1 while True: pw = input("pw: ") if pw == "0000": print("로그인.") break if i == 5: print("5회 입력. 프로그램 종료.") break i = i + 1 pw: 1234 pw: 0000 로그인. 4) continue 예제 # 홀수단 출력 while True: dan = int(input("dan: ")) if dan < 2 or dan > 9: print("Error") break if dan % 2 == 0: print("짝수단 입니다") continue for i in range(1, 10): print(dan, ' * ', i, " = ", dan * i) dan: 2 짝수단 입니다 dan: 3 3 * 1 = 3 3 * 2 = 6 3 * 3 = 9 3 * 4 = 12 3 * 5 = 15 3 * 6 = 18 3 * 7 = 21 3 * 8 = 24 3 * 9 = 27 dan: 10 Error 5) 무한루프 i = 1 while True: print("hi") if i==5: break i=i+1 무한루프는 break로 제어함 2. time 모듈 1) time 시간 관련한 함수들로 구성된 모듈 import time time 함수 time(): 현재 Unix timestamp를 소수로 리턴 sleep(n): 일정시간 동안 실행지연(n초 동안 실행이 지연됨) import time for i in range(5): print("Hello") time.sleep(0.1) # 0.1초간 지연 Hello Hello Hello Hello Hello 학습정리 반복문의 제어 break: 조건이 참일 경우, 반복문을 탈출함 continue: 조건이 참일 경우, continue 다음 반복문을 실행하지 않고 반복을 계속함 time 모듈 import time > time.time() # 현재 시간을 소수로 리턴 time.sleep(n) # 일정시간 동안 실행지연(n초 동안 실행이 지연됨)
파키스탄에서는 매장을 방문하기 전에 구글이나 SNS에서 먼저 검색하는 고객이 늘고 있습니다. 사히왈, 오카라, 파크파탄 같은 지역의 소규모 사업자에게는 큰 기회입니다. 1. 구글에서 검색되도록 하기 구글 비즈니스 프로필을 만들고 가게 이름, 주소, 전화번호, 영업시간을 등록하세요. 근처 고객이 검색할 때 지도에 가게가 나타날 가능성이 높아집니다. 2. 간단한 웹사이트 만들기 복잡한 사이트는 필요 없습니다. 서비스 내용, 가격, 연락처가 보기 쉽게 정리된 페이지만 있어도 새로운 고객의 신뢰를 얻을 수 있습니다. 3. WhatsApp으로 고객 응대하기 파키스탄에서는 이메일보다 채팅을 선호하는 사람이 많습니다. WhatsApp 버튼이나 번호를 넣어 질문과 주문이 쉽게 이루어지도록 하세요. 4. 지역에 도움이 되는 콘텐츠 공유하기 지역 행사, 안내 글, 유용한 팁을 올리면 검색에 잘 노출되고 신뢰도도 높아집니다. 5. 리뷰 요청하기 만족한 고객에게 짧은 리뷰를 부탁해 보세요. 좋은 리뷰는 새로운 고객이 결정을 내리는 데 도움이 됩니다. 마치며 온라인 성장에는 시간이 걸리지만, 작은 실천이 쌓이면 꾸준한 성과로 이어집니다. 지역 정보와 안내 글을 더 보고 싶으시다면 지역 정보 가이드 를 확인해 보세요.
I. 살펴보기 Kotlin에서 클래스를 공부하다 보면 프로퍼티를 선언할 때 Getter와 Setter를 직접 정의하는 경우가 있다. 처음에는 Getter는 값을 가져오고 Setter는 값을 바꾸는 것이라고 이해했지만, Setter 안에서 갑자기 등장하는 field 가 무엇인지 헷갈렸다. 이번 글에서는 Kotlin의 Getter와 Setter가 무엇인지, 그리고 커스텀 Setter에서 field 를 사용하는 이유를 정리해보려고 한다. II. Kotlin의 프로퍼티 다음과 같이 var 로 변수를 선언해보자. var balance: Int = 0 단순히 변수 하나를 선언한 것처럼 보이지만, Kotlin에서는 이를 프로퍼티(Property) 라고 한다. var 로 선언한 프로퍼티에는 기본적으로 값을 읽기 위한 Getter와 값을 변경하기 위한 Setter가 존재한다. 따라서 println(balance) 처럼 값을 읽으면 Getter가 호출되고, balance = 100 처럼 값을 변경하면 Setter가 호출된다. 별도로 Getter와 Setter를 작성하지 않아도 Kotlin이 기본적인 동작을 제공해준다. III. 커스텀 Getter Getter가 호출될 때 단순히 저장된 값을 반환하는 것이 아니라 다른 계산을 수행하도록 만들 수도 있다. 예를 들어 현재 잔액의 절반을 반환하는 프로퍼티를 만들어보자. val halfBalance: Int get() = balance / 2 halfBalance 에 접근하면 저장된 값을 가져오는 것이 아니라 balance / 2 를 계산한 결과를 반환한다. balance = 100 println(halfBalance) 출력 결과는 다음과 같다. 50 여기서 중요한 점은 halfBalance 라는 값을 따로 저장하고 있는 것이 아니라는 것이다. halfBalance 를 요청할 때마다 Getter가 실행되어 현재 balance 를 기준으로 값을 계산한다. IV. 커스텀 Setter Setter도 직접 정의할 수 있다. 예를 들어 은행 계좌의 잔액이 음수가 되지 않도록 만들고 싶다고 해보자. var balance: Int = 0 set(value) { field = if (value < 0) { 0 } else { value } } 이제 다음과 같이 값을 넣으면 balance = 100 Setter의 value 에는 100 이 전달된다. 반대로 balance = -50 을 실행하면 value 에는 -50 이 전달되지만, 조건문에 의해 실제로 저장되는 값은 0 이 된다. 즉, Setter는 프로퍼티에 값이 저장되기 전에 값을 검사하거나 변환할 수 있게 해준다. V. 그런데 field 는 무엇일까? 여기서 가장 헷갈렸던 부분이 바로 이것이었다. field = value 왜 그냥 다음처럼 작성하지 않는 걸까? balance = value Setter 안에서 balance 는 지금 Setter를 정의하고 있는 바로 그 프로퍼티이다. 따라서 Setter 안에서 balance = value 라고 작성하면 balance 에 값을 넣으려고 하면서 Setter가 다시 호출된다. 다시 호출된 Setter 안에서도 또 balance = value 가 실행되고, 다시 Setter가 호출된다. 결국 다음과 같은 과정이 반복된다. balance에 값 저장 → Setter 호출 → balance에 값 저장 → Setter 호출 → balance에 값 저장 → Setter 호출 → ... 즉, 무한 재귀가 발생한다. 이 문제를 피하기 위해 사용하는 것이 field 이다. VI. field 는 실제 저장 공간을 의미한다 Setter 내부의 field = value 에서 field 는 해당 프로퍼티의 값을 실제로 저장하는 Backing Field 를 의미한다. 쉽게 생각하면 balance = 100 은 "balance의 Setter를 호출해서 100을 넣어줘" 라는 의미이고, Setter 내부의 field = 100 은 "Setter를 다시 호출하지 말고 실제 저장 공간에 100을 저장해줘" 라는 의미라고 볼 수 있다. 따라서 Setter 안에서는 자기 자신의 값을 실제로 저장할 때 field 를 사용한다. VII. 전체 예제 Getter와 Setter를 함께 사용하면 다음과 같은 클래스를 만들 수 있다. class BankAccount(initialMoney: Int) { var balance: Int = initialMoney set(value) { field = if (value < 0) { 0 } else { value } } val halfBalance: Int get() = balance / 2 fun info(): String { return "Balance: $balance, Half: $halfBalance" } } 사용해보면 다음과 같다. val account = BankAccount(100) println(account.info()) account.balance = 200 println(account.info()) account.balance = -50 println(account.info()) 마지막에 -50 을 넣더라도 Setter가 값을 확인하기 때문에 balance 에는 0 이 저장된다. VIII. Getter와 Setter 정리 Getter와 Setter의 역할을 정리하면 다음과 같다. > Getter 프로퍼티의 값을 읽을 때 호출된다. 필요하면 값을 계산해서 반환할 수도 있다. > Setter 프로퍼티의 값을 변경할 때 호출된다. 저장하기 전에 값을 검사하거나 변환할 수 있다. > value Setter에 새롭게 들어온 값이다. > field 현재 프로퍼티의 실제 값이 저장되는 공간이다. Setter를 다시 호출하지 않고 값을 직접 저장할 때 사용한다. 특히 value 와 field 를 구분하는 것이 중요하다. 예를 들어 balance = -50 을 실행했을 때 Setter 내부에서는 value = -50 field = 기존 balance가 저장되어 있던 실제 공간 이라고 생각할 수 있다. 그리고 field = 0 을 실행하면 최종적으로 balance 의 실제 저장값이 0 이 된다. IX. 마무리 처음에는 Setter 안에서 왜 굳이 field 라는 새로운 키워드를 사용하는지 이해하기 어려웠다. 하지만 핵심은 간단했다! 프로퍼티 이름을 사용하면 Getter 또는 Setter를 거치고, field 를 사용하면 해당 프로퍼티의 실제 저장 공간에 접근한다. 특히 Setter 안에서 자기 자신의 프로퍼티에 값을 저장하려고 다시 프로퍼티 이름을 사용하면 Setter가 계속 호출될 수 있기 때문에 field 를 사용한다. 앞으로 커스텀 Setter를 볼 때는 다음과 같이 생각하면 될 것 같다. value 는 새로 들어온 값, field 는 실제로 저장할 곳.
Langfuse 공부 1 — Docker와 Ollama로 로컬 실습 환경 만들기 이 글에서 다룰 주제 Langfuse와 로컬 LLM의 역할을 나누고, 실제 요청과 관측 데이터가 이동하는 경로를 이해한다. macOS에는 Ollama와 Python을, Docker에는 Langfuse와 저장소를 준비한다. 한 번의 LLM 호출을 trace로 남기고, 저장된 입력·출력·토큰 사용량을 확인한다. 주요 단어 · Langfuse · Ollama · Trace · Span · Generation · Docker Compose LLM이 답을 돌려줬다는 사실만으로는 애플리케이션의 동작을 충분히 설명하기 어렵다. 어떤 프롬프트가 전달됐는지, 응답에 얼마나 걸렸는지, 여러 단계 중 어디서 문제가 생겼는지를 함께 남겨야 한다. 이번 공부는 외부 유료 LLM API 없이 시작한다. Ollama가 로컬 모델을 실행하고, Python 앱이 그 모델을 호출하며, Langfuse는 그 실행을 관측한다. 첫 글의 목표는 대규모 서비스를 만드는 것이 아니라, 이후 프롬프트 비교와 평가를 반복할 수 있는 작은 실습 환경을 준비하는 것이다. 이전에 정리한 AIOps 에이전트 운영과 Langfuse 에서 운영 관점을 살펴봤다면, 이번 글에서는 직접 데이터를 남기는 출발점을 만든다. 1. 도구의 역할 Langfuse 는 LLM 애플리케이션의 실행, 입력·출력, 모델 호출 등의 관측 데이터를 수집하고 조회하는 도구다. 이 실습에서 답변을 생성하는 모델 서버 역할은 Ollama가 맡는다. Ollama 는 로컬 모델을 내려받아 실행하고 API로 호출할 수 있게 하는 도구다. 이번 모델은 qwen3:8b 이며, Python은 Ollama의 OpenAI 호환 API를 호출한다. Langfuse를 설치했다고 Python 앱의 모든 호출이 저절로 기록되지는 않는다. 앱 코드에 SDK를 붙여 어떤 실행을 기록할지 정해야 한다. 예제는 LLM 호출 전후를 직접 감싸는 방식으로 작성했다. 자동 계측보다 코드가 조금 길지만, 부모 실행과 모델 호출이 어떻게 연결되는지 처음 공부할 때 확인하기 쉽다. 이 글에서 먼저 익힐 용어는 세 가지다. 용어 뜻 이번 예제 Trace 하나의 요청에 속하는 실행 전체 질문 하나를 처리한 실행 Span 실행 안의 일반 작업 구간 local-ollama-study Generation 모델 호출을 표현하는 관측 구간 ollama-chat Generation에는 일반 작업 정보에 더해 모델명과 토큰 사용량 같은 LLM 관련 정보를 기록한다. 이번 trace는 루트 span 하나와 그 아래 generation 하나로 시작한다. 개념과 API는 Langfuse 계측 문서 를 기준으로 확인했다. 2. 전체 구성 전체 구성도. 파란 화살표는 LLM·UI 요청, 초록 화살표는 관측 데이터 전송이다. 보라색 선은 Web/Worker가 사용하는 공통 저장소의 연결 의존성이며, 저장소 사이의 전달 순서를 뜻하지 않는다. Langfuse는 web 컨테이너 하나로 끝나지 않는다. 이번 구성은 web, worker, PostgreSQL, ClickHouse, Redis, MinIO의 여섯 서비스를 사용한다. 구성요소 이 실습의 역할 Langfuse web 브라우저 UI, API, SDK 데이터 수신 Langfuse worker 들어온 이벤트의 비동기 처리 PostgreSQL 사용자·조직·프로젝트 등 트랜잭션 데이터 ClickHouse trace와 observation 등 분석용 관측 데이터 Redis 작업 큐와 캐시 MinIO S3 호환 저장소. 수집 원본 이벤트와 미디어 저장 수집 API는 원본 이벤트를 객체 저장소에 두고 처리를 큐에 넣는다. worker가 비동기로 처리한 뒤 관측 데이터를 ClickHouse에 저장하므로, SDK 호출이 끝나는 순간과 UI에서 검색되는 순간 사이에는 차이가 날 수 있다. 저장소별 역할과 이 흐름은 공식 아키텍처 설명 에 근거한다. 2.1 네이티브 Ollama Apple Silicon에서 Ollama는 Metal을 통한 GPU 가속을 지원한다. 이번에는 이 경로를 사용하도록 Ollama를 macOS에 직접 설치하고, Langfuse 쪽 서비스만 Docker로 실행했다. Python도 호스트에서 실행하므로 모델 주소는 http://127.0.0.1:11434/v1 이다. Ollama 하드웨어 지원 여기서 localhost 는 호출하는 프로세스가 실행 중인 환경 을 가리킨다. 지금 Python 코드를 컨테이너로 옮긴다면 같은 127.0.0.1 주소는 호스트의 Ollama가 아니라 그 컨테이너 자신을 가리키므로 주소 설정을 다시 해야 한다. 2.2 실습 환경 2026년 10월 4일에 준비한 환경은 다음과 같다. 항목 사용 환경 호스트 macOS 26.6.2 · Apple M5 Pro · 메모리 64GB Docker Engine / Compose 29.7.2 / 5.4.0 Docker에 할당된 메모리 약 7.75GiB Langfuse web / worker 4.50.0 Ollama Homebrew 패키지 0.35.1_1 Python 3.14.8 Python 주요 패키지 langfuse 4.16.0 · openai 3.24.0 · httpx 0.28.1 · python-dotenv 1.2.4 모델 qwen3:8b 서버와 SDK의 버전은 서로 다른 제품 버전이다. 둘 다 이름에 Langfuse가 들어간다고 같은 숫자를 맞추는 것은 아니다. 이번 앱은 서버 v4의 observation 조회 API를 사용한다. 버전 업그레이드 시에는 호환성 문서 를 함께 확인한다. qwen3:8b 의 모델 페이지는 다운로드 크기 약 5.2GB와 Q4_K_M 양자화를 표시한다. 이 숫자는 실행에 필요한 전체 메모리 크기가 아니다. 모델 가중치 외에 컨텍스트와 실행 버퍼 등도 메모리를 사용한다. Ollama 모델 정보 Docker 메모리 제한은 이번 단일 요청 실습을 위한 설정이다. 운영 환경의 권장 사양이나 처리량을 검증한 결과로 해석하면 안 된다. Compose 환경 자체도 고가용성과 백업을 갖춘 운영 구성이 아니다. 공식 Compose 설치 문서 3. Ollama 설치 기존 Homebrew 환경에 Ollama를 설치하고 macOS 서비스로 실행했다. brew install ollama brew services start ollama ollama --version curl http://127.0.0.1:11434/api/version ollama pull qwen3:8b ollama list 첫 다운로드에는 인터넷 연결과 모델 파일을 저장할 공간이 필요하다. 내려받은 모델의 로컬 추론과 모델 다운로드는 서로 다른 작업이다. 이번 실습은 로컬 추론에 집중하기 위해 Ollama 설정 파일 ~/.ollama/server.json 에 아래 값을 적용했다. 이미 다른 설정이 있다면 파일 전체를 덮어쓰지 않고 이 항목을 합쳐야 한다. 이번에는 첫 서비스 시작 전에 설정했다. 실행 중인 서비스의 설정을 바꿨다면 brew services restart ollama 로 다시 시작한다. { "disable_ollama_cloud": true } 모델이 로딩된 동안 ollama ps 를 실행하면 메모리에 올라온 모델과 처리 장치를 확인할 수 있다. 공식 문서에서 100% GPU 는 모델이 전부 GPU 메모리에 올라온 상태를 뜻한다. 이번 실행의 ollama ps 에서도 100% GPU 가 표시됐다. 출력의 모델 메모리 크기는 8.8GB, 컨텍스트 설정은 40,960토큰이었다. 이는 실제로 질문에 40,960토큰을 사용했다는 뜻은 아니다. Ollama FAQ 4. Langfuse 실행 4.1 Compose 설정 공식 저장소의 Compose 파일을 출발점으로 사용했다. 실습 파일은 docs/langfuse-study-2026-10-04/lab 에 모았다. lab/ ├── compose.yaml ├── upstream-compose.yaml ├── init_env.py ├── .env # 비밀값, 공개하지 않음 ├── .venv/ └── app/ ├── requirements.txt ├── config.py ├── trace_smoke.py └── verify_trace.py 주요 변경은 브라우저 주소, 비밀값, 저장소 노출 범위다. 설정 이번 값과 이유 web 포트 127.0.0.1:3300:3000 . 호스트 3300에서 컨테이너 3000으로 연결 NEXTAUTH_URL http://localhost:3300 . 실제 브라우저 접속 주소와 일치 MinIO 호스트 포트 127.0.0.1:9190:9000 . 브라우저 미디어 경로에 사용 내부 MinIO 주소 http://minio:9000 . Docker 서비스 이름으로 접근 DB·Redis·worker 포트 호스트에 따로 공개하지 않음 CLICKHOUSE_CLUSTER_ENABLED false . 단일 ClickHouse 컨테이너 사용 서버 익명 사용 통계 TELEMETRY_ENABLED=false 영속 데이터 Compose의 named volume에 보관 TELEMETRY_ENABLED=false 는 Langfuse 서버의 사용 통계 설정이다. 학습 앱에서 보내는 trace 계측을 끄는 의미로 사용한 것이 아니다. 포트를 바꿀 때 ports 만 바꾸면 브라우저 주소와 인증·미디어 URL이 어긋날 수 있다. NEXTAUTH_URL 과 미디어 업로드 URL도 함께 맞춰야 한다. 공식 포트 변경 안내 4.2 비밀값과 초기 프로젝트 init_env.py 는 DB 비밀번호, 암호화 키, API 키, 로컬 사용자 비밀번호를 무작위로 생성해 .env 에 저장한다. 파일 권한은 소유자만 읽고 쓸 수 있는 600 으로 설정했다. 실제 키와 비밀번호는 글과 스크린샷에 싣지 않는다. 초기 조직은 Local Study , 프로젝트는 Langfuse Local Lab 이다. LANGFUSE_INIT_* 환경변수를 이용해 처음 시작할 때 사용자·조직·프로젝트를 만든다. 이는 이 실습이 생성한 계정이며, Langfuse가 제공하는 공통 기본 관리자 비밀번호가 아니다. Headless initialization 다음은 이번에 생성한 로컬 실습 자료를 사용하는 명령이다. 저장소 루트에서 LAB_DIR 를 정한 뒤 해당 프로젝트와 Compose 파일을 명시해 실행한다. LAB_DIR="$PWD/docs/langfuse-study-2026-10-04/lab" cd "$LAB_DIR" /opt/homebrew/bin/python3.14 init_env.py docker compose -p langfuse-study -f "$LAB_DIR/compose.yaml" up -d docker compose -p langfuse-study -f "$LAB_DIR/compose.yaml" ps 컨테이너가 시작됐다는 사실과 서비스가 준비됐다는 사실은 다르다. 첫 실행에는 DB migration과 초기화가 필요하다. ps 에서 의존 저장소의 상태를 확인하고, 문제가 생기면 해당 서비스 로그를 읽는다. docker compose -p langfuse-study -f "$LAB_DIR/compose.yaml" \ logs --tail=100 langfuse-web langfuse-worker curl http://localhost:3300/api/public/health 브라우저의 접속 주소는 http://localhost:3300 이다. DB 비밀번호나 SDK secret key를 로그인 비밀번호로 넣지 않고, 생성된 로컬 사용자 계정을 사용한다. 4.3 공식 파일로 시작하기 앞의 init_env.py , app/trace_smoke.py 등은 이번에 만든 로컬 실습 자료 다. 내 작업 폴더가 없는 독자는 별도의 새 폴더에서 공식 파일로 시작할 수 있다. 아래 경로는 같은 버전의 공식 Compose를 가져오는 시작 절차이며, 이번 실측은 앞에서 설명한 수정본에서 수행했다. mkdir langfuse-reader-lab cd langfuse-reader-lab curl -fL \ https://raw.githubusercontent.com/langfuse/langfuse/v4.50.0/docker-compose.yml \ -o compose.yaml 공식 파일에서 # CHANGEME 로 표시된 비밀값을 바꾸고 다음 항목을 맞춘다. 임의의 예제 비밀번호를 그대로 사용하는 대신 openssl rand -hex 32 등으로 각 값을 생성한다. 같은 저장소에 연결하는 항목에는 동일한 비밀번호를 넣어야 한다. 공식 파일의 위치 변경할 값 web / worker의 이미지 각각 docker.langfuse.com/langfuse/langfuse:4.50.0 , docker.langfuse.com/langfuse/langfuse-worker:4.50.0 web의 ports 127.0.0.1:3300:3000 worker 공통 NEXTAUTH_URL http://localhost:3300 MinIO의 ports 127.0.0.1:9190:9000 web의 미디어 외부 endpoint http://localhost:9190 worker의 미디어 endpoint 내부 주소 http://minio:9000 유지 MinIO·S3 비밀번호 MINIO_ROOT_PASSWORD 와 모든 *_SECRET_ACCESS_KEY 에 같은 값 PostgreSQL 비밀번호 POSTGRES_PASSWORD 와 DATABASE_URL 에 같은 값 ClickHouse 비밀번호 서버와 web/worker의 CLICKHOUSE_PASSWORD 에 같은 값 Redis 비밀번호 Redis 명령의 --requirepass 와 web/worker REDIS_AUTH 에 같은 값 Langfuse 암호화·세션 SALT , NEXTAUTH_SECRET 은 각각 생성, ENCRYPTION_KEY 는 32바이트의 64자리 hex 이 표는 .env 만으로 모든 변경이 적용된다는 뜻이 아니다. 이미지와 ports , web의 미디어 endpoint처럼 파일에 적힌 위치를 직접 수정하는 항목도 있다. Compose 설정이 유효한지 확인한 뒤 실행한다. docker compose -p langfuse-reader-lab -f compose.yaml config --quiet docker compose -p langfuse-reader-lab -f compose.yaml pull docker compose -p langfuse-reader-lab -f compose.yaml up -d docker compose -p langfuse-reader-lab -f compose.yaml ps 처음 접속하면 사용자·조직·프로젝트를 만들고 프로젝트의 API 키를 발급한다. 그 키를 Python 예제용 .env 의 LANGFUSE_PUBLIC_KEY , LANGFUSE_SECRET_KEY 에 넣는다. 뒤의 짧은 Python 예제를 first_trace.py 로 저장하면 직접 만든 app/ 폴더 없이도 같은 계측 구조를 확인할 수 있다. 공식 Compose의 의존 이미지 일부는 범위 태그를 쓴다. 재현할 버전을 정확히 보존하려면 내려받은 이미지의 digest도 기록·고정해야 한다. 실제 로컬 구성은 여섯 이미지 모두 내려받은 arm64 digest로 고정했고, 이미지 식별자를 lab/images.lock.json 에 보관했다. 범위 태그 redis:7 이나 latest 만 기록한 상태와 구분한다. 5. 첫 관측 데이터 빨간 윤곽은 현재 설명하는 계측 영역이다. 모델 요청은 위쪽 Python→Ollama 경로로 나가고, 그 호출을 설명하는 관측 데이터는 아래쪽 Langfuse web으로 전송된다. 5.1 Python 환경 별도 가상환경에 고정한 패키지를 설치한다. 아래 명령은 앞에서 준비한 lab 폴더 기준이다. 이 Mac의 기본 python3 는 3.9였으므로 이번 실습에서는 Homebrew Python 3.14 경로를 명시했다. 다른 환경에서는 Python 3.10 이상의 실행 경로로 바꾼다. /opt/homebrew/bin/python3.14 -m venv .venv .venv/bin/python -m pip install -r app/requirements.txt .venv/bin/python app/trace_smoke.py .venv/bin/python app/verify_trace.py trace_smoke.py 는 다음 질문을 로컬 모델에 전달한다. Langfuse에서 trace, span, generation의 차이를 한국어로 각각 한 문장씩 설명해줘. OpenAI 라는 Python 클라이언트 이름을 사용하지만, 이번 base_url 은 Ollama의 로컬 주소다. 클라이언트 이름과 실제 요청 목적지를 구분해야 한다. api_key="ollama" 는 OpenAI 호환 클라이언트에 넣는 더미 문자열이며, 유료 OpenAI API 키를 발급할 필요가 없다. Ollama OpenAI 호환 API 5.2 계측 코드 전체 스크립트에서 핵심은 모델 호출을 generation으로 감싼 부분이다. 아래는 별도 파일 first_trace.py 로 저장해 실행할 수 있는 짧은 예제다. 현재 폴더의 .env 에 LANGFUSE_PUBLIC_KEY , LANGFUSE_SECRET_KEY 를 준비한다. 키는 프로젝트 설정에서 발급하며, 이 글의 로컬 프로젝트에는 초기화 때 생성한 키가 들어 있다. import os from dotenv import load_dotenv from langfuse import Langfuse from openai import OpenAI load_dotenv(".env") langfuse = Langfuse( public_key=os.environ["LANGFUSE_PUBLIC_KEY"], secret_key=os.environ["LANGFUSE_SECRET_KEY"], base_url="http://localhost:3300", ) assert langfuse.auth_check(), "Langfuse 연결과 프로젝트 키를 확인하세요" prompt = "Langfuse의 trace, span, generation을 각각 한 문장으로 설명해줘." messages = [{"role": "user", "content": prompt}] ollama = OpenAI( base_url="http://127.0.0.1:11434/v1", api_key="ollama", ) with langfuse.start_as_current_observation( as_type="span", name="local-ollama-study", input={"question": prompt}, ) as root: trace_id = langfuse.get_current_trace_id() with langfuse.start_as_current_observation( as_type="generation", name="ollama-chat", model="qwen3:8b", input=messages, ) as generation: response = ollama.chat.completions.create( model="qwen3:8b", messages=messages, temperature=0.2, max_tokens=512, reasoning_effort="none", ) answer = response.choices[0].message.content generation.update( output=answer, usage_details={ "input": response.usage.prompt_tokens, "output": response.usage.completion_tokens, "total": response.usage.total_tokens, }, ) root.update(output={"answer": answer}) langfuse.flush() print(answer) print(langfuse.get_trace_url(trace_id=trace_id)) langfuse.shutdown() ollama.close() 짧은 예제만 실행할 때는 다음처럼 의존성을 설치하고 실행한다. 이 예제는 API 재조회 검증을 포함하지 않으므로 출력된 trace 주소에서도 저장 결과를 확인한다. /opt/homeb
관리자 페이지 노출 취약점 관리자 페이지 노출 취약점이란 관리자페이지가 웹에 공개적으로 노출되어있거나 공격자가 유추하기 쉬운 경로(/admin,/manager)를 통해 관리페이지를 제공하는 경우를 뜻한다. 관리자페이지에 해커가 유추한 아이디와 비밀번호를 무작위로 대입하는 공격하는 브루트포스 공격 (BruteForce Attack)이 일어날 수 있다 관리자페이지를 통해 본격적으로 내부 침입할 수 있는 초기 침투 경로가 될 수 있다. 해당 취약점을 찾는 방법으로는 쇼단을 통한 관리자 페이지 검색 또는 http.title:"admin" 관리자 페이지 제목 기반 검색 또는 intitle:"관리자 로그인" intitle:"Admin Dashboard" intitle:"Administrator Login" 관리자 페이지가 주로 사용하는 파일명이나 url 검색으로 취약점을 가진 웹서버를 찾는다. inurl:/admin/login.php inurl:/manage/login.html inurl:/administrator/ inurl:/webmaster/login inurl:admin_login.aspx 관리자 페이지 취약점 예시 대응방안 접근 제어(ACL) 설정 : 관리자 페이지에 접근할 수 있는 특정 관리자 IP 주소만 허용하도록 방화벽이나 웹 서버 설정을 적용했는지 점검 또는 (/admin) 같은 공격자가 유추하기 쉬운 URL 대신 유추하기 어려운 URL을 통한 관리자 로그인 서비스 제공 디렉토리 리스팅 취약점 웹 서버 설정 시 기본 설정 또는 잘못된 구성 또는 웹 폴더 내부에 웹 페이지가 없을 때 폴더 내부의 상하위 디렉터리와 파일들이 노출되어 내부 구조와 중요 파일에 접근이 가능한 취약점 공격자는 해당 취약점을 통해 웹서버 디렉토리의 주요 자원에 대해서 빠르게 식별가능 중요한 정보를 얻을 수 있는 민감한 파일이 노출될 가능성이 높아진다. 디렉토리 리스팅 취약점 예시 해결방안 1.웹서버의 설정파일에서 디렉토리 리스팅 기능이 활성화 되어있는지 점검 예)Apache 설정파일 httpd.conf 파일 내 모든 디렉터리의 Options 지시자에서 Indexes 옵션 제거 <Directory "/var/www/html"> Options Indexes FollowSymLinks AllowOverride None Require all granted </Directory> 2.웹서버 액세스 제어(Access Control) 기능 설정 비인가자의 민감한 정보 접근을 막기위해 웹 응용 프로그램 자체 인증 메커니즘을 적용 시스템 관리 취약점 시스템 관리 취약점이란 서비스나 프로그램, 시스템 등을 설치나 운영하는 과정에서 잘못된 보안설정이나 민감한 정보 파일의 잘못 된 취급 등 관리 소홀로 인해 발생하는 취약점을 뜻한다. 다음과 같은 취약점은 모두 시스템 관리 취약점의 일종이라 볼 수 있다. 관리자 계정의 취약한 비밀번호 설정 불필요한 관리자 페이지 노출 불필요한 서비스나 포트 활성화 중요 설정 파일이나 백업 파일 외부 노출 불필요한 계정이나 권한 방치 대응방안 백업 파일, 테스트 파일, 임시 파일 등 불필요한 파일 삭제 설정 파일, 로그 등에 포함된 민감정보의 외부 노출 방지 사용자 및 관리자 계정에 필요한 최소한의 권한만 부여 사용하지 않는 계정 및 권한 제거 운영체제, 웹 서버, WAS, 애플리케이션 등의 보안 업데이트를 지속적으로 적용 관리자 기능이나 시스템 관리 페이지에 대한 접근을 제한 중요 시스템에 대한 접근 및 변경 이력을 기록하고 모니터링 불필요한 요청 메소드 허용 취약점 웹 서버가 데이터 조회 및 전송을 위한 HTTP 메소드(GET, POST) 이외 파일 생성·수정·삭제나 시스템 디버깅에 사용되는 고위험 메소드(PUT, DELETE, TRACE,OPTIONS 등)를 제한 없이 허용하도록 설정된 상태 검색 엔진 크롤러는 보통 HTTP GET 요청을 기반으로 콘텐츠를 수집하여 검색어 자체로 특정 URL이 어떤 메소드를 허용하는지 찾는 것은 어렵다. API 엔드포인트나 파일 업로드 디렉터리등 민감한 디렉토리를 검색한 이후에 취약점 자동으로 점검하는 도구를 이용하여 해당 URL에 대해 고위험 메소드 요청하여 취약점 점검한다. HTTP Method가 악용되었을때 예시 PUT :서버의 특정 리소스를 생성하거나 새로 대체하는 메소드. 인증되지 않은 공격자가 임의의 경로에 원하는 파일을 업로드하는데 사용 DELETE :시스템 내부의 중요 리소스의 URI를 지정하여 DELETE 요청을 보내 중요데이터 파괴 가능 CONNECT : 프록시 서버에 터널링을 요청하는 메서드로, 웹 서버가 프록시 기능을 잘못 활성화해 둔 경우, 공격자가 이 서버를 거쳐 제3의 목적지로 CONNECT 요청을 보내 우회 공격의 숙주로 악용가능 TRACE :서버가 받은 HTTP 요청 내용을 그대로 응답 본문(Response Body)에 돌려주는 디버깅 메소드로서, 민감한 인증 정보(Cookie, 세션 토큰, Custom 헤더 등) 노출하는데 사용 OPTIONS :서버가 특정 URI에 대해 어떤 HTTP 메서드를 허용하는지 조사하는 용도 공격자는 실제 공격을 수행하기 전, OPTIONS 요청하여 서버가 특정메소드를 열어두었는지 사전 정찰하는데 사용 대응 방안 1.리소스 별 Method 제한 리소스 별 적절한 요청 Method를 지정하여 Method가 리소스에 부합하지 않는 경우 오류 상태코드로 응답 2.사용자 권한 검증 REST API 사용으로 인해 파일 생성/수정/삭제나 시스템 디버깅에 사용되는 메소드 제한이 어려울경우 사용자 별로 권한을 부여해 인가된 사용자의 Method 요청에 대해서만 응답한다.
Compiler에 대해서 배우는 이유 학습 방법 현재 한양대학교 ERICA에서 오희국 교수님이 강의하시는 Compiler 강의 를 수강하고 있다. 해당 강의는 Stanford University [CS143 Compilers] 내용에 기반하여서 해당 자료를 참고자료로 복습하고 있으며 영어 강의 참고자료는 다음과 같다 [Youtube] Software 개발 AI, Agent, Model, Application, Program 등 다양한 목적에 의해 SW 개발이 이뤄지며 생산성이 향상되어 시장이 과열된 현재 시점에서도 수많은 수요에 의해 수많은 SW개발이 지속적으로 이루어지고 있다. 이 모든 SW는 모두 동일한 Programming Language라는 하나의 통상적으로 정의된 Interface를 통해 개발이 이뤄지고 있다. 이러한 Language들은 Compiler와 Interpreter를 통해 실제 Computer Processor의 명령어 및 레지스터 체계에 기반하여 binary 실행 파일로 변환 가능한 어쎔블리어로 변환된다. 즉, 현재 Programming Language들은 Compiler와 Interpreter를 통해 추상화가 이루어져있으며 이러한 추상화를 통해 객체지향 프로그래밍과 같이 컴퓨터아키텍처 자체에대한 이해를 크게 요구하지 않고 Language 자체 문법만 인지해도 SW 개발이 가능하도록 하였다. Compiler를 이해하는 것의 목적 결국 Compiler를 이해하는 것은 현재 우리가 사용하는 Programming Language들이 실행 코드로 변환되는 과정을 이해한다는 것이다. 그 과정에서 메모리에 올라가 프로세스로 변환되기 위한 요구사항과 프로세서에서 하나의 thread로 실행되기 위한 요구사항을 반영할 것이고 이를 이해한다는 것이다. 이러한 지식이 추후 나에게 데려다줄 기대값을 생각하면 다음과 같다 Programming Language의 문법(Grammar) 가 Compile 과정 에서 적용되는지 Runtime 과정 에서 적용되는지 이해하고 경계를 인지할 수 있게 된다. 이는 코드 분석에 있어서 정적 분석과 동적 분석의 경계의 기반 지식이 될 것이라 생각한다. 2. C와 같이 Compiler만 사용하는 언어와 Java와 같이 Compiler와 Interpreter를 모두 사용하는 언어의 차이를 인지하고 언어 사용시 해당 언어 설계(JVM)를 이해하는 기반이 될 것이라 생각한다. 3. CPU 기반 Architecture에서의 Compiler를 이해하는 것으로 보인다. 하지만 현대 Computer Architecture는 GPGPU, NPU, TPU, PIM, PNM과 같이 수많은 accelerator가 연산을 분담하는 구조이다. 이러한 변화되는 Computer Architecture에서 코드를 어떻게 적용할지 파악할지에 대한 시야 혹은 이를 넘어서 해당 Accelerator에 적합하게 Compiler를 변경할 수 있는 능력을 가질 수 있을지도 모른다 생각한다. Compiler의 강의가 시사하는 점과 강의 접근 방식 Compiler라는 수업은 내게 Programming Language는 결국 하나의 Interface이자 개발자 대상 UI라는 점을 이야기한다. 컴퓨터 사용자의 다양한 요구사항과 컴퓨터 자체에 대한 이해를 기반으로 한 요구사항에 따라 적합한 Programming Language가 개발이 되었다. 지금까지 기술에 대한 이해 뿐만이 아닌 서비스 개발에 있어서도 유저 요구사항 분석만을 기반으로 진행하였고 해당 기술이 요구하는 요구사항에 대해서 명시적으로 명세한적이 없었던거 같다. 따라서 수업에서 이 두가지 관점의 요구사항을 기반으로 접근하는 방식을 이해하고 이에 맞게 정리를 해보려 한다. Compiler Introduction Pre-Notation Code -> Execution Compiler를 이해하기 위해서는 개발자가 작성한 소스코드가 어떠한 절차를 거쳐 실행되는지 이해해야 한다. C/C++ 를 예시로 보면 다음과 같다 Preprocessing - Processor (Source Code -> Expanded Code) gcc -E [소스파일.c] -o [출력파일.i] Compilation - Compiler (Expanded Code -> Assembly Code) gcc -S [소스파일.c] -o [출력파일.s] Assemby - Assembler (Assembly Code -> Object Code) gcc -c [소스파일.c] -o [출력파일.o] Linking - Linker (Object Codes -> Executable Code) gcc [소스파일.c] -o [실행파일] Execution The Role of Compiler Translaction : High Langauge -> Low Language GCC의 경우 Assembly Language는 메모리 및 유한 register공간( L1 I-Cache , L1 D-Caceh 할당 을 하며 정해진 명령어 규격에 맞게 변환 을 한다. Error Detection : Programming Language에서 제공하는 Grammar와 다르게 사용하는 경우 Semantic 해석 과정에서 문제가 되는 Token을 파악 한다. Code Optimization : Translation 과정에서 Language 설계자의 의도된 사항에 따라 변형하여 Low Language로 변환한다. Portability : 여러 OS 혹은 Processor Architecture Type에 대해서 지원하도록 한다. Compiler vs Interpreter 강의에서는 Interpreter에 대해서는 On-Line, Compiler에 대해서는 Off-Line으로 설명한다. Interpreter (On-Line) Interpreter run Program . Interpreter는 해당 Langauge로 만든 Program에 대해서 Program 실행 전까지 어떠한 전처리(Preprocessing) 없이 Program 실행과 함께 실행되며 Data와 Program이 Interpreter를 통해 직접적으로 Output을 만든다. Compiler (Off-Line) Compiler translate Program . Compiler는 실행파일을 만들기 위해 한번만 실행되며 해당 실행 파일을 실행하여 Data를 입력 받아 Output(출력)을 만든다. Classification of Language Compile Language : C, C++, Go, Rust Interpret Language : Python, Java Script Both(Interpreter + JIT(Just In Time) Comiler) : Java, JavaScript, Web Assembly History of Comiler and High Language 1954 - 704 704 : IBM's Mass-produced Computer Hardware All Programming Done in Assembly Software cost exceeded Hardware Suggestion 1) Interpreter - Speed Coding Runtime is 10-20 times slower than Assembly Code This Property is same with present Interpreter. 2) Compiler - FORTRAN ( FOR MULA TRAN SLATE) Translate High-level Language to Assembly Code Performance close to Hand-written Assembly Impact on Computer Science Programming become easier an enormous body of theoritical and practical work The Out Line of Modern Compiler Macro Structure of Compiler (Lexical Analysis, Parsing, Semantic Analysis, Optimization, Code Generation) The Structure of a Compiler Lexical Analysis : Identify Keyword Parsing : Identify Sentence Semantic Analysis : Analyze Sentence Optimization : Editing Code Generation : Translating 1. Lexical Analysis 주체 : Lexer 하는 일 : Partition input string into substrings, where the substrings are called tokens. Output : a Sequence of Tokens 전체 흐름상 뭔가 Parsing이 의미 자체가 직관적으로 String을 단어로 나누는 거 같지만 처리 단위(Unit)이 다르다 String은 A sequence of Characters 즉 character의 연속이다. 이러한 이론상 무작위적인 character들을 구분하는 기준을 통해 Token으로 분리하는 작업이 Lexical Analysis이다. 추후 다음장에서 배우지만 Toekn의 정의는 다음과 같다 Token : A lexeme labeled with its class - a pair <Token class, Lexeme> 2. Parsing 주체 : Parser 하는 일 : Lexer로 부터 받은 A sequence of Token을 통해 Sentence structure를 구성하는 작업 Output : Diagramming Sentence (The Diagram is a tree) 강의에서는 English Analogy를 통해 해당 부분을 설명한다. article, noun, verb, adjective가 token class이며 token class를 종합하여 Grammar에 따른 Component(Subject, Object)를 파악하여 Diagramming Sentence를 구성한다. 실제 코드 if x == y then z = 1 else z = 2 를 살펴보면 다음과 같다. 기타 요구사항에 의한 Language 분류 Scientific - 과학적 계산 목적 대표 예 : Fortran Business - 특정 작업 목적 대표 예 : SQL System - 컴퓨터 리소스 작업 목적 대표 예 : C, Cpp Programming Language Cost Productivity > training cost => switch language
에이전트가 쏟아낸 코드를 사람이 다 읽어야 한다 — ADE가 나온 이유 ADE(Agentic Development Environment)는 코딩 에이전트를 여러 개 동시에 돌리고, 그 결과를 받아 확인하는 작업 환경이다. 에디터가 "코드를 쓰는 창"이라면 ADE는 "일을 나눠 주고 결과를 받는 관리대"에 가깝다. 터미널에서 출발한 Warp, 오픈소스 데스크톱 앱 Orca, 구글의 Antigravity, 에디터에서 넘어온 Cursor가 이 자리를 놓고 경쟁한다. 코드가 나오는 속도는 지난 2년 동안 크게 올랐다. 읽는 속도는 그대로다. Warp가 자사 블로그에 이렇게 적는다. 개발의 가장 큰 병목은 더 이상 코드를 쓰는 것이 아니다. 코드 주변에서 사람이 하는 일 — 무엇을 만들지 정하고, 동작이 맞는지 확인하는 일이다. 에디터는 코드를 쓰는 자리를 다듬어 왔다. 그 자리가 더 이상 병목이 아니라면 도구도 확인하는 쪽으로 옮겨가야 한다. ADE가 그렇게 나왔다. ADE가 무엇인가 Orca는 같은 것을 화면 구성으로 설명한다. 여러 AI 코딩 에이전트를 나란히 돌리는 데스크톱 IDE. 작업마다 자기 git worktree, 자기 에이전트 터미널, 자기 브라우저 탭을 갖는다. 여기서 worktree 는 git이 제공하는 기능이다. 같은 저장소를 디스크의 여러 폴더에 동시에 펼쳐 두고, 각 폴더가 서로 다른 브랜치를 본다. 에이전트 셋이 같은 파일을 동시에 고쳐도 서로 덮어쓰지 않는 이유가 이것이다. 각 폴더의 작업 결과는 diff , 즉 바뀐 줄만 추려 보여주는 비교 화면으로 확인한다. 주의 — 같은 약어가 세 가지를 가리킨다 ADE를 검색하면 서로 다른 것이 섞여 나온다. 풀어 쓰면 쓰는 곳 무엇을 하는 도구인가 Agentic Development Environment Warp 코딩 에이전트 운용 Agent Development Environment Orca 코딩 에이전트 운용 Agent Development Environment Letta 에이전트 자체를 만드는 도구 Warp와 Orca는 약어를 다르게 푸는데 하는 일이 같다. Letta는 약어가 Orca와 똑같은데 하는 일이 다르다. Letta의 ADE는 에이전트의 기억과 도구 설정을 사람이 열어 고치는 화면이다. 코딩 에이전트를 여러 개 돌리는 것과는 상관이 없다. 이 글에서 ADE는 Warp와 Orca 쪽, 코딩 에이전트를 운용하는 도구만 가리킨다. 그리고 ADE는 업계가 합의한 말이 아니다. 제품 이름표로 ADE를 내건 곳은 Warp와 Orca 둘뿐이다. 나머지는 각자 다르게 부른다. Google은 "agentic development platform", Cursor는 "agent-first interface", Zed는 "여러 에이전트를 지휘한다", JetBrains는 "제품들의 묶음"이다. 부르는 말은 제각각인데 만드는 화면은 서로 닮아 있다. 왜 쓰나 확인할 양이 늘었다 에이전트가 하루에 1,000줄을 내놓으면 1,000줄을 읽어야 한다. 읽는 일 자체는 전과 똑같은데 양만 몇 배로 늘어난다. 게다가 직접 쓰지 않은 코드 다. 손으로 쓴 코드는 확인의 상당 부분이 쓰는 동안 끝난다. 왜 그렇게 했는지 쓴 사람이 알고 있기 때문이다. 받은 코드는 의도부터 짐작해야 한다. 그리고 에이전트는 그럴듯하게 틀린다. 문법이 틀리면 컴파일러가 잡는다. 돌아가는데 잘못된 코드는 사람이 읽어야만 걸린다. 숫자가 이 간극을 보여준다. 구글의 개발 성과 연구 프로그램 DORA가 2025년에 기술 전문가 약 5,000명에게 물었다. AI가 생산성을 높였다고 답한 비율은 80%를 넘었다. 같은 조사에서 AI가 만든 코드 를 "상당히" 또는 "많이" 신뢰한다고 답한 비율은 24% 였다. 빨라졌다고는 하는데 그 결과물을 믿지는 않는다. 믿지 않으면 전부 확인해야 한다. 쓰는 사람은 계속 는다. Stack Overflow 개발자 조사에서 AI 도구를 쓴다고 답한 비율은 2023년 44%, 2024년 62%, 2025년 79%였다. 에이전트로 좁히면 2025년 31%에서 2026년 4월 59%로 올랐다. 뒤의 둘은 조사 설계가 서로 달라 같은 집단을 추적한 값이 아니다. 기다리는 시간이 아깝다 에이전트 하나에 일을 시키면 끝날 때까지 사람이 논다. 결제 모듈을 리팩터링하라고 시켜 놓고 5분을 기다린다. 결과가 마음에 안 들면 다시 시키고 또 기다린다. 같은 5분에 셋을 돌리면 결과가 셋 나온다. Cursor는 이 효과를 이렇게 적는다. 여러 모델에 같은 문제를 풀게 하고 가장 좋은 결과를 고르면 최종 산출물이 눈에 띄게 좋아진다. 어려운 과제일수록 그렇다. Orca는 비용까지 들어 설명한다. 에이전트마다 다른 실수를 한다. 같은 과제를 병렬로 돌리는 것이 순차 재시도보다 싸고, 의견이 갈리는 지점이 신호가 된다. 셋이 같은 답을 내면 대개 맞다. 셋이 갈리면 거기가 어려운 지점이다. 에이전트 하나만 돌렸으면 그냥 넘어갔을 자리를 세 결과의 차이가 가리킨다. 읽을 곳이 좁혀진다. 확인하기 좋은 형태로 받는다 에이전트가 무슨 짓을 했는지 터미널 로그로 쫓는 것은 느리다. Google Antigravity는 결과를 Artifacts 라는 형태로 내놓는다. 작업 목록, 구현 계획, 변경 설명, 스크린샷, 브라우저 녹화다. 공식 문서의 표현은 이렇다. 원시 도구 호출보다 개발자가 검증하기 쉽다 ADE가 파는 것이 결국 이것이다. 더 똑똑한 모델이 아니라 확인하기 쉬운 형태 다. 기존 IDE와 무엇이 다른가 세 가지가 다르다. 작업 단위가 파일이 아니라 과제다. IDE는 파일을 열고 함수를 고친다. ADE는 "결제 모듈 리팩터링"이라는 과제를 통째로 넘기고 결과를 받는다. Antigravity는 이것을 "더 높은, 과제 중심의 수준에서 일하게 한다"고 적는다. 창이 하나가 아니라 여럿이다. IDE는 한 프로젝트를 한 창에서 연다. Cursor 3.0은 반대로 간다. 새 인터페이스는 여러 저장소와 여러 환경에 걸쳐 에이전트를 병렬로 돌리게 해준다. 로컬, worktree, 클라우드, 원격 SSH에서. 주인공이 바뀐다. 이 변화를 Antigravity가 가장 선명하게 적었다. 에이전트가 화면 안에 들어가 있던 구도를, 화면이 에이전트 안에 들어가는 구도로 뒤집는다. 지금까지 AI는 에디터에 붙은 기능이었다. 자동완성 패널, 사이드바 챗이다. Antigravity가 화면 둘을 나란히 두는 것이 그래서다. Editor View는 평소 쓰던 AI 에디터다. Manager Surface는 "여러 에이전트를 여러 작업공간에서 띄우고, 지휘하고, 지켜보는 전용 화면"이다. 대표 프로그램 이름 바탕 격리 방식 가격 Warp 터미널 로컬 worktree + 클라우드 Docker 오픈소스(AGPL v3) + 유료 클라우드 Orca 데스크톱 앱 git worktree MIT, 무료 Google Antigravity 에디터 + 관리 화면 작업공간 분리 무료 티어 있음, Ultra 월 $100 Cursor 에디터 worktree · 클라우드 · 원격 SSH 유료 Devin 클라우드 클라우드 샌드박스 무료 · 월 $20–$200 Zed 에디터 worktree(스레드별 선택) 오픈소스 Claude Code CLI --worktree 옵션 구독 격리 방식은 둘로 갈린다. worktree는 디스크 에서 폴더를 나누고, 클라우드 샌드박스는 기계 자체 를 따로 띄운다. Warp — 터미널에서 출발했다 2026년 4월 28일 클라이언트를 AGPL v3로 공개했다. 서버 쪽은 비공개로 남겼다. 로컬에서 여러 세션을 띄우거나, 에이전트마다 worktree를 하나씩 주거나, Oz라는 클라우드 플랫폼으로 넘겨 돌릴 수 있다. Oz는 2026년 2월 10일에 나왔고 Docker 샌드박스에서 에이전트를 돌린다. 기업용 자체 호스팅 선택지도 있다. 지휘 구조에 제한을 뒀다. 공식 문서 표현으로 "정확히 한 단계 깊이 — 부모와 그 직속 자식뿐" 이다. 부모가 자식 N개를 띄워 각자 다른 조각을 맡긴다. 전부 끝나면 결과를 합친다. 자식이 또 자식을 낳지는 않는다. 부모 에이전트 ├── 자식 1 — packages/api ├── 자식 2 — packages/web └── 자식 3 — 테스트·문서 확인 화면은 Interactive Code Review다. 변경된 줄에 인라인으로 의견을 달고, 여러 개를 모아 한 번에 에이전트에게 보낸다. 에이전트는 그 묶음을 한 번에 반영해 새 diff를 돌려준다. Orca — 무료이고 아무 에이전트나 꽂는다 데스크톱(macOS·Windows·Linux)에 모바일 동반 앱이 붙는다. 특징은 에이전트를 가리지 않는다 는 점이다. Claude Code, Codex, Gemini, Cursor, GitHub Copilot, Devin, Cline 등 30여 종의 CLI 에이전트를 꽂아 쓴다. 각 에이전트는 이미 들고 있는 구독을 그대로 쓴다. 대상 독자를 공식 문서가 직접 적어 뒀다. 이미 코드로 먹고살면서 AI를 지렛대로 쓰려는 사람을 위해 만들었다. 대체가 아니다. diff를 읽고, 커밋을 신경 쓰고, worktree를 정돈하는 사람 을 전제한다. 확인은 diff 줄에 마크다운 의견을 달아 묶음으로 되돌리는 방식이다. Google Antigravity — 화면 둘을 나란히 2025년 11월 18일 공개 프리뷰로 나왔다. 2026년 5월 19일 I/O에서 2.0이 나왔다. 독립 데스크톱 앱과 CLI가 붙었다. 무료 티어도 주 단위로 갱신되는 사용량 안에서 CLI까지 전부 쓴다. Cursor — 에디터가 관리대를 흡수했다 2026년 4월 2일 3.0에서 Agents Window가 들어왔다. 여러 저장소와 환경에 걸쳐 에이전트를 병렬로 돌린다. worktree 운용이 명령어로 들어가 있다. /worktree 로 격리된 작업 폴더를 만들고, /apply-worktree 로 본 체크아웃에 반영하고, /delete-worktree 로 치운다. /best-of-n 은 여러 모델에 같은 일을 시켜 결과를 비교한다. worktree마다 필요한 준비 명령은 .cursor/worktrees.json 에 적어 둔다. Devin — 동시 실행 개수가 요금제다 다른 도구와 달리 동시에 띄울 수 있는 세션 수가 요금제로 정해져 있다. 무료와 Pro는 최대 10개, Max와 팀은 무제한이다. Zed, Claude Code, JetBrains Air Zed는 2026년 4월 22일 병렬 에이전트를 넣었다. 한 창 안에서 여러 에이전트가 동시에 돈다. Threads Sidebar에서 에이전트마다 접근 가능한 폴더와 저장소를 지정 하고 진행 상황을 본다. worktree 격리는 스레드별로 켜고 끈다. Claude Code는 CLI 쪽 방식이다. 터미널마다 이름을 달리해 띄우면 그만큼 격리된 작업이 생긴다. claude --worktree feature-auth # 첫 번째 터미널 claude --worktree fix-login # 두 번째 터미널 저장소에 커밋이 최소 하나는 있어야 worktree가 만들어진다. 커밋이 없으면 Failed to resolve base branch "HEAD" 로 실패한다. JetBrains는 2026년 9월 22일 Air를 발표했다. 특정 모델에 묶이지 않는 것을 설계 원칙으로 내세웠고, 목표를 이렇게 적었다. 목표는 더 많은 코드가 아니다. 개발자와 팀과 조직이 이해하고, 검증하고, 책임질 수 있는 소프트웨어다. 쓰는 팁 아래는 전부 각 제품 공식 문서에 적혀 있는 권고다. 공식 권장 숫자는 Orca의 셋뿐이다 구체적인 숫자를 적어 둔 곳은 Orca 하나다. 첫 사용 안내가 worktree를 셋 만들게 하고, 병렬 사용법 문서도 "세 브랜치, 세 diff, 같은 프롬프트"로 시작한다. 규모 감각도 적어 뒀다. worktree 열 개는 감당하기 벅차다. Warp, Cursor, Zed, Antigravity, Claude Code는 권장 개수나 상한을 문서에 적지 않았다. Devin의 10개는 권고가 아니라 요금제 상한이다. "셋"이 두 가지 뜻이다 같은 "에이전트 3개"가 운용 방식에 따라 완전히 다르다. 같은 과제를 셋에 다른 과제를 셋에 하는 일 같은 프롬프트, 결과 셋을 비교해 승자를 고른다 작업을 쪼개 각자 다른 부분을 맡긴다 쓰는 곳 Orca, Cursor /best-of-n Warp 얻는 것 품질, 그리고 의견 차이라는 신호 시간 어려운 과제에는 앞쪽이 맞고, 범위가 넓고 명확한 작업에는 뒤쪽이 맞다. 작업은 파일·패키지·테스트로 쪼갠다 Warp 문서가 구체적이다. 시작하기 전에 파일·패키지·테스트·이슈 중 무엇을 누가 갖는지 정하라고 한다. 예로 든 분할은 이렇다. 한 에이전트에 packages/api , 다른 에이전트에 packages/web , 세 번째에 테스트와 문서. 겹칠 수밖에 없을 때의 규칙도 있다. 두 에이전트가 같은 파일을 건드려야 한다면 한쪽을 주인으로 정하고 , 다른 쪽에는 고치지 말고 검토하거나 제안만 하게 한다. 합칠 때 — 자동으로 합치지 않는다 모든 에이전트의 결과를 자동으로 머지하지 마라. 모으는 단계를 따로 둔다. 각 에이전트의 요약, 바뀐 파일, 검증 결과를 확인하고 한 브랜치씩 머지하거나 체리픽한다. 체리픽은 브랜치를 통째로 합치지 않고 필요한 커밋만 골라 가져오는 것이다. 브랜치 이름에 주인을 드러내라는 조언도 붙는다. agent/claude-auth-refactor , agent/codex-auth-tests 같은 식이다. import·에러 처리·인증부터 본다 Warp는 세 곳을 짚는다. import 구문, 에러 처리, 그리고 보안이나 인증에 닿는 코드 다. 통합 전에는 전체 파일을 가로지르는 합친 diff를 한 번 보라고 한다. Cursor는 확인을 건너뛰지 말라고 못 박는다. AI가 쓴 코드는 리뷰가 필요하다. 정리는 자주 Orca의 조언이다. 머지한 worktree는 과감하게 지워라. 한 번 클릭이면 worktree와 브랜치가 같이 사라진다. 머지된 것을 수십 개씩 쌓아 두면 탐색만 느려진다. 쟁점 공식 문서에서 끝내 찾지 못한 것이 둘 있다. 머지 충돌이 실제로 났을 때 푸는 절차 와 에이전트가 실패한 뒤의 복구 절차 다. 아래 쟁점들도 같은 자리에서 나온다. 승자를 무엇으로 고르나 같은 과제를 셋에 돌려 가장 좋은 것을 고르라는 조언은 Orca에도 Cursor에도 있다. 무엇을 기준으로 고르라는 문서는 어느 쪽에도 없다. 테스트가 다 통과한 결과가 둘이면 거기서부터는 읽어 보는 수밖에 없다. 결과를 셋으로 늘리는 일은 자동화됐는데, 그중 하나를 고르는 일은 자동화되지 않았다. 싸다는 근거가 파는 쪽에서 나온다 "병렬이 순차 재시도보다 싸다"는 문장은 Orca 공식 문서의 주장이다. 토큰을 세 배 쓰는 대신 사람이 기다리는 시간을 줄인다는 계산인데, 어느 선부터 손해인지 알려주는 공식 가이드는 없다. 과제가 쉬우면 셋 다 같은 답을 내고 두 번은 버려진다. 어려울수록 이득이 커진다는 Cursor의 설명이 맞다면, 쉬운 일에 병렬을 쓰는 것이 가장 비싼 선택 이 된다. 격리를 어디까지 해야 하나 지금 두 갈래로 갈려 있다. git worktree 클라우드 샌드박스 나누는 것 디스크의 폴더 기계 자체 강점 가볍고 빠르다 포트 충돌·전역 설정 오염까지 막는다 약점 같은 기계를 쓴다 느리고 돈이 든다 Warp는 둘 다 제공하고, Zed는 스레드마다 켜고 끄게 한다. 어느 쪽이 맞는지는 아직 합의가 없다. 믿지 않으면서 쓴다 앞서 본 24%가 이 도구들이 서 있는 자리다. 믿지 않는데 쓴다. ADE는 이 상태를 전제로 만들어진 도구다. 신뢰를 올리려 하지 않고, 믿지 않아도 쓸 수 있게 확인 화면을 깐다. 신뢰가 올라가면 이 도구들의 생김새도 달라질 것이고, 올라가지 않으면 확인 화면은 계속 두꺼워진다. 정리 ADE가 하는 일은 결국 셋이다. 결과를 diff로 묶어 주고, worktree로 갈라 두고, 의견을 모아 한 번에 되돌린다. 고를 때 볼 것은 세 가지다. 격리를 어디까지 하나 — 디스크에서 나누는 worktree와 머신까지 나누는 클라우드 샌드박스가 다르다. Warp와 Cursor는 둘 다 되고, Orca·Zed·Claude Code는 디스크까지, Devin은 머신 쪽이다 에이전트를 가리나 — Orca·Zed·Cursor는 외부 에이전트나 모델을 꽂게 열어 뒀고, Antigravity와 Devin은 자사 쪽으로 묶인다 확인 화면이 있나 — 인라인 의견을 묶어 보내는 기능이 있는지가 실제 작업 속도를 가른다 다만 이 도구들이 메우는 것은 격차의 한쪽뿐이다. 확인을 쉽게 만들었지 덜 하게 만들지는 않았다. 읽어야 할 양은 그대로이고, 읽는 사람도 그대로다. 에이전트를 셋으로 늘리면 확인할 결과도 셋이 된다. 참고 Warp, Warp is now open source · 문서 · 여러 에이전트 돌리기 · AI 코드 리뷰하기 · Oz Orca, onorca.dev · 문서 · 병렬 에이전트 · GitHub Google, Antigravity 소개 · 개발자 블로그 · I/O 2026 Cursor, 3.0 변경사항 · Agents Window · Worktrees · 에이전트 모범 사례 Devin, 요금제 Zed, 병렬 에이전트 Claude Code, 일반 워크플로 JetBrains, Air 소개 Stack Overflow, 개발자 조사 돌아보기 Google, DORA 2025 리포트
Nefa AirPro là đơn vị uy tín chuyên cung cấp và lắp đặt giải pháp máy cấp khí tươi, hệ thống lọc không khí trung tâm và máy treo tường cao cấp. Tích hợp màng lọc bụi mịn PM2.5 cùng công nghệ thu hồi nhiệt hai chiều, Nefa AirPro mang đến nguồn khí sạch dồi dào, bảo vệ sức khỏe toàn diện. Website: https://nefaairpro.vn/ Hotline: 0975473941 Email: info@nefagroup.vn Showroom HN: A-TT6-4 Khu đô thị Him Lam Vạn Phúc, P. Hà Đông, TP. Hà Nội Kho TP Hồ Chí Minh: 381 Lê Văn Khương, Phường Tân Thới Hiệp, TP. Hồ Chí Minh