이번 스프린트에서 사용자 앱의 찜 목록과 쿠폰함·QR을 만들었다. 시안을 화면으로 옮기면서 사용자 입장에서 다시 보고 디자인을 바꾼 부분이 있다. 각 화면에서 어떤 점이 불편했고 어떻게 바꿨는지 적어 둔다. 1. 찜 목록 위치 시안 찜 목록은 단독 화면이고, 헤더 오른쪽에 "쿠폰함" 링크가 있다. 쿠폰함은 사용 가능 / 사용 내역 두 탭이다. 쿠폰함 화면에는 찜 목록으로 가는 길이 없다. 마이페이지의 "찜한 캠페인" 메뉴로만 갈 수 있다. 변경 내용 쿠폰함 탭을 찜 목록 / 사용 가능 / 사용 내역 세 탭으로 나눴다. 탭 주소 찜 목록 /coupons/wishlist 사용 가능 /coupons 사용 내역 /coupons/history 찜 목록은 쿠폰을 받는 곳이다. 찜 → 받기 → 사용이 한 탭 안에서 이어지는 게 자연스럽다. 시안 구조에서는 쿠폰함을 눌러도 찜 목록으로 갈 방법이 없어서, 사용자가 마이페이지까지 가야 한다. 하단 쿠폰함을 누르면 점심때 가장 자주 찾는 사용 가능 (QR) 탭이 열린다. 11:00 선착순 때는 스와이프 헤더의 "찜 목록"이나 10:50 알림으로 들어오므로 찜 목록 탭이 바로 열린다. 탭마다 주소가 있어서 알림이나 다른 화면에서 원하는 탭으로 바로 열 수 있다. 2. 찜 카드 구조 시안 카드(사진, 가게 이름, "메뉴 · N원 할인", 상태) 아래에 사용 시간과 버튼이 카드 밖에 따로 있다. 마감, 하루 한도 상태에도 회색 버튼([마감], [오늘 발급 한도 3개 완료])이 있다. 받는 중 상태는 따로 그려져 있지 않다. 변경 내용 1. 사용 시간과 버튼을 카드 안으로 넣기 버튼이 카드 밖에 있으니 [받기]가 위 카드 것인지 아래 카드 것인지 헷갈렸다. 흰 카드 하나가 가게 하나가 되도록 묶었다. 2. 오른쪽에 원래 가격과 할인가 정렬 쿠폰은 하루 3개까지만 받을 수 있어서, 찜이 여러 개면 그중 어떤 걸 받을지 골라야 한다. "N원 할인"이 문장 중간에 있으면 카드끼리 비교하기 어렵고, 실제로 얼마를 내는지도 계산해야 한다. 가격을 오른쪽 끝에 맞추니 훑어보기만 해도 비교된다. 3. 마감, 하루 한도 상태 버튼 삭제 "마감 · 수량 소진" 상태 줄과 [마감] 버튼이 같은 정보를 두 번 보여줬다. 큰 회색 버튼이 화면을 차지해서 받을 수 있는 카드가 덜 눈에 띄었다. 오픈 전 [11:00 오픈] 버튼은 남겼다. 11:00에 같은 자리가 [받기]로 바뀌어서, 사용자가 미리 손가락을 올려둘 수 있다. 4. 받을 수 있는 카드 맨 위로 정렬 찜한 순서대로 보여주면 11:00 선착순에 마감된 카드를 지나서 [받기]를 찾아야 한다. 받을 수 있음 → 받음 → 마감·한도 순으로 정렬했다. 5. 받는 중에는 버튼 색 유지 처음에는 버튼을 비활성( disabled )으로 막았는데, 비활성 버튼은 회색이라 마감처럼 보였다. 디자인 시스템 버튼에 Loading 속성이 있어서, 색은 그대로 두고 글자 자리에 도는 아이콘을 보여주는 loading 옵션을 만들었다. 수정 전 수정 후 3. 찜 삭제 시안 찜 목록 하단에 "찜 취소는 가게 상세에서 할 수 있어요"라고 적혀 있다. 변경 내용 카드 오른쪽 위 ✕ 버튼으로 바로 삭제하고, "찜 목록에서 삭제했어요 [되돌리기]" 토스트를 4초 보여준다. 받은 쿠폰 카드에는 ✕가 없다. 요구사항에는 "찜한 캠페인 삭제: 사용자가 찜한 캠페인을 목록에서 삭제한다"가 필수로 있었다. 시안과 요구사항이 서로 달라서 요구사항을 따랐다. 확인 모달 대신 되돌리기를 넣어, 실수로 눌러도 바로 되돌릴 수 있게 했다. 4. QR 화면 시안 사용 가능 쿠폰에서 [QR 보기]를 누르면 "쿠폰 사용" 화면으로 넘어간다. 뒤로 가기를 눌러야 쿠폰함으로 돌아온다. 변경 내용 QR을 별도 화면 대신 바텀시트 로 띄웠다. 사용 가능 쿠폰 카드를 누르면 쿠폰함 위로 시트가 올라오고, 그 안에 QR을 보여준다. QR 화면은 매장 계산대 앞에서 직원에게 보여주는 화면이다. 뒤에 사람이 기다리고 있을 수 있어서, 빨리 열고 빨리 닫는 게 가장 중요하다고 봤다. 열기: 작은 [QR 보기] 버튼이 아니라 카드 어디를 눌러도 시트가 올라온다. 급하게 열 때 정확히 버튼을 노리지 않아도 된다. 닫기: 시트를 끌어내리거나 바깥을 누르면 바로 닫힌다. 화면이 바뀌지 않아서 보고 있던 쿠폰함 그대로 돌아온다. 다른 쿠폰 보기: 쿠폰이 여러 장이면 닫고 바로 옆 카드를 누르면 된다. 별도 화면에서는 뒤로 갔다가 다시 들어가야 했다. 뒤로 가기 문제: 별도 화면은 알림이나 링크로 바로 들어왔을 때 이전 페이지가 없어서, 뒤로 가기를 누르면 앱 밖으로 나갈 수 있었다. 시트는 화면 이동이 없어서 이 문제가 없다. 시트 안에서는 직원이 봐야 하는 QR을 가장 크게 두고, 남은 시간은 QR 아래 작은 글자로 내렸다. 시안 (별도 화면) 변경 (바텀시트) 5. 멘토링 반영 멘토링에서 받은 피드백 중 사용자 앱 부분을 반영했다. 첫 탭 이름 첫 탭 이름을 "스와이프"에서 "오늘 점심"으로 바꿨다. "스와이프"는 화면을 쓰는 방법(넘기기)만 설명하고, 이 화면에서 무엇을 하는지는 알려주지 않는다. 이 화면은 10:00~12:59에만 열리고 오늘 점심 쿠폰 포스터를 보고 찜하는 곳이다. 서빙 시간 외에 나오는 "내일 10시에 만나요" 화면과도 자연스럽게 이어진다. 아이콘도 좌우 화살표(⇄)에서 숟가락·포크로 바꿨다. 좌우 화살표는 "교환"처럼 보일 수 있어서, 이름과 같은 뜻의 아이콘을 골랐다. 찜 배지 헤더 찜 배지에서 주황 동그라미를 빼고 주황색 숫자만 남겼다. 멘토링에서 숫자만 있어도 된다는 피드백을 받았다. 스와이프 화면 튐 카드를 넘기기 전과 후의 포스터 iframe을 비교해 보니, 뒤에 있던 카드가 앞으로 올 때 이미 그려진 포스터를 옮기지 않고 새로 그리고 있었다. 넘길 때마다 포스터 두 장을 다시 그리고 사진도 다시 요청했다. 앞 카드와 뒤 카드를 한 목록에서 카드마다 같은 key로 그리도록 바꿨다. 뒤 카드가 앞으로 와도 같은 요소라 크기와 위치만 바뀐다. 세 번째 카드까지 숨겨서 미리 그려 두었다. {cards.slice(currentIndex, currentIndex + 3).map((card, position) => ( <div key={card.serveId} {...(position === 0 ? cardProps : {})}> <PosterCard card={card} /> </div> ))} 수정 후 넘길 때 다시 그리는 포스터는 0장, 사진 재요청도 0번이 됐다. 느낀점 사용자 입장에서 화면을 하나씩 다시 고쳐 나가니 점점 나아지는 게 보여서 작업이 재밌어지고 있다. 오늘 멘토링에서는 생각하지 못했던 부분들을 짚어 주셔서, 앞으로 어떤 부분을 어떻게 개발해 나가야 할지 조금 정리가 됐다.
필자는 학부 졸업을 예상보다 1학기 빨리 하게 되었다. 시간이 남는 관계로 그동안 배운 내용을 완벽하게 흡수해서 기본을 탄탄하게 다지고, 현업에서 바로 투입이 가능한 유능한 엔지니어가 되고자 할 수 있는 노력들을 하고자 한다. 대학교 수업 처럼 1주차, 2주차, 등 8주에 걸쳐서 운영체제 기본기를 탄탄하게 다지고, 기술 면접은 가뿐하게 , 오히려 심사위원이 되어 문제를 출제할 수 있는 수준으로 공부가 되어있는 것을 목표로 하고 싶다 비전공자 혹은 이 과목을 수강하지 않은 분들도 함께 따라오면서 기초를 다질 수 있도록 글을 다듬어서 올리고자 한다. 도움이 되었으면 좋겠다. 할 수 있다! 하면 된다!
우선, 1차적으로는 기능완성을 우선으로 보고 그냥 한 트랜잭션 범위내에서 모든것을 해결했었다. ( openAi 호출 / DB 에서 대상 조회 / openAi 를 통해서 나온 결과를 DB 에 다시 반영 / 검증들 ... ) Transaction BEGIN ↓ PENDING 게시물 10개 조회 ↓ OpenAI API 호출 ↓ 네트워크 응답 대기 ↓ 번역 결과 DB 반영 ↓ Transaction COMMIT 기능은 정상적으로 잘 돌아가는데, 과연 이 트랜잭션 범위가 적절한 범위일까? 왜냐하면 DB 에서 조회하는거야 ... 트랜잭션이 필요하다고 해도, OpenAI API 를 호출하는거까지 트랜잭션이 필요한가? 라는것부터 시작했다. OpenAi API 를 호출하고서, 번역 결과를 받아오는데까지 시간이 오래걸리는 편이었음. 10건을 가지고오는데도 5초~10초 .. 또, 중간에 실패하면 ? 외부 api 호출에 실패하면 ? 등등 ... 하나의 트랜잭션에서 진행하게되면 Connection 자체를 너무 오래 잡고있는 문제가 발생할 가능성이 매우높았음. 그렇기에, 위와같은 생각을 했었고 ... 결국, DB 와는 아무상관없는 Open AI API 를 호출하는데에있어서, 트랜잭션이 계속해서 유지되고있는게 문제. 예를들어서 다음과 같이 걸린다고 하자. DB 조회 20ms OpenAI 호출 3000ms DB 수정 20ms Transaction ≈ 3040ms 즉, DB 와 관련된거는 40ms 뿐인데, DB 와는 상관없는 즉 Transaction 을 물고있지않아도 되는 작업이 3000ms 인 작업시간의 대부분을 차지. 트랜잭션이 유지되는 동안에는 영속성 컨텍스트 / DB 관련된 자원들이 함께 유지가 되고, 쓰기작업 / 락이 개입하게되면 락 유지시간 역시 길어질것이니 ... 문제가 많음 즉, 불필요하게 긴 트랜잭션은 Connection Pool 에 존재하는 Connection / Lock / 영속성 컨텍스트 등등 ... DB 자원을 더 오래 점유할 가능성을 높임. 그래서, 필요한 트랜잭션 범위를 어떻게 설정 ? 결국, 해당 과정에서 Transaction 이 필요한 것에는 어떤 작업이 ? DB 에서 조회 / DB 에 반영하는 이 두개의 작업이 Transaction 이 필요. [Transaction 1] PENDING 10개 조회 DTO 변환 COMMIT ↓ [Transaction 없음] OpenAI API 호출 네트워크 응답 대기 ↓ [Transaction 2] 번역 결과 조회/반영 COMMIT 그래서, DB 작업이 필요한순간에만 잠깐 Transaction 을 여는식으로 선택. 트랜잭션은 Proxy 를 통해서 호출되어야하므로 ... 내부에서 만들지않았음. 즉, 아래와 같은것처럼 만들지않았음 public void translate() { List<TranslationTarget> targets = findPendingTargets(); TranslationBatchResult result = openAiClient.translate(targets); applyTranslations(result); } @Transactional(readOnly = true) public List<TranslationTarget> findPendingTargets() { ... } @Transactional public void applyTranslations( TranslationBatchResult result ) { ... } 이렇게되면, 외부에있는 Proxy 가 applyTranslations / findPendingTargets 를 호출해서 트랜잭션이 적용이 되는게아니라, 내부인 translate -> 위 두개의 메서드를 호출하므로 위 두개의 메서드는 트랜잭션이 적용이 되지 않음. 그렇기에, 별도의 Spring Bean 으로 트랜잭션 담당 분리. 폴더구조는 더 복잡하지만 대략 다음과같이 ... TransferPostTranslationService │ ├── TransferPostTranslationTxService │ └── DB Transaction 담당 │ └── OpenAiClient └── 외부 HTTP 통신 담당 @Service @RequiredArgsConstructor public class TransferPostTranslationServiceImpl { private final TransferPostTranslationTxService txService; private final OpenAiClient openAiClient; public TranslationBatchResult translate() { List<TranslationTarget> targets = txService.findPendingTargets(); if (targets.isEmpty()) { return TranslationBatchResult.empty(); } TranslationBatchResult result = openAiClient.translate(targets); txService.applyTranslations(result); return result; } } @Service @Transactional @RequiredArgsConstructor public class TransferPostTranslationTxService { private final TransferPostRepository transferPostRepository; @Transactional(readOnly = true) public List<TranslationTarget> findPendingTargets() { return transferPostRepository .findTop10ByTranslateStatusOrderByContentCreatedAtDescIdDesc( TranslateStatus.PENDING ) .stream() .map(post -> new TranslationTarget( post.getId(), post.getContent() ) ) .toList(); } public void applyTranslations( TranslationBatchResult result ) { // 번역 결과 DB 반영 } } 그래서, 이전에는 아래와 같은 방식이였다면 ... @Transactional ────────────────────────────────────────── DB 조회 ↓ OpenAI API ──── 네트워크 대기 ──── ↓ DB 수정 ────────────────────────────────────────── COMMIT 이후에는, @Transactional(readOnly = true) ────────────── DB 조회 DTO 변환 ────────────── COMMIT ↓ OpenAI API ────── 네트워크 대기 ────── Transaction 없음 ↓ @Transactional ────────────── DB 수정 ────────────── COMMIT 위처럼, 필요한곳에서만 ... 이번에 해당 과정을 거치면서, 이전에는 그냥 한 범위내에서 모든것을 작성했었는데 실제로 외부 API 를 호출하면서 응답이 느리게오고, 작업이 오래걸리는것은 느끼고 트랜잭션의 범위를 나눠야겠다라고 생각하는게 좋았던거같음. 또한, 트랜잭션이 데이터의 정합성을 보장하는 문제만이 아니라는것을 알게됐음. 그렇기에 앞으로는 DB 작업과 외부 API 호출에 대한것을 분리하고, DB 작업이 필요한 구간에서만 짧게 트랜잭션을 사용하는식으로 변경.
1. Web Web의 기원과 정의 영어 -> 거미줄, 그물처럼 얽힌 구조 인터넷에 흩어진 수많은 문서가 링크를 통해 서로 연결된 모습이 마치 거미줄과 닮음 이처럼 문서들이 서로 연결된 구조를 웹 이라고 부름. 인터넷 위에서 표준규약(공통규칙)으로 자원과 정보를 제공하는 시스템(데이터를 주고 받는) Web Service 의 개념 웹을 통해 제공되는 서비스로, 사용자의 필요를 해결하는 기능을 인터넷을 통해 이용할 수 있도록 구현 한 서비스 사용자는 보통 웹 브라우저나 앱으로 서비스에 접속해 필요한 기능을 이용함. 웹 서비스 흐름 정리 사용자 <-> 클라이언트 <-> 인터넷 <-> 서버 <-> 데이터베이스 클라이언트가 요청을 하고 서버가 응답을 함 (한 쌍) 용어 정리 사용자(user): 웹서비스를 이용하는 '사람' 클라이언트(client): 사용자의 행동을 받아 서버에 요청을 보내는 '프로그램' 서버(server): 클라이언트의 요청을 처리하고 결과를 만들어내는 프로그램 데이터베이스(database): 데이터를 저장,조회,관리하는 소프트웨어 요청(request): 클라이언트가 서버에 처리를 요구하기 위해 전달하는 메시지 응답(response): 서버가 요청 처리 결과로 돌려주는 메시지 UI(User Interface): 사용자가 직접 보고 조작하는 화면과 그 구성요소 ex) 카톡 전송 버튼 API(Application Programming Interface): 프로그램이 다른 프로그램의 기능을 사용하기 위한 약속된 인터페이스 www: 웹 주소 앞에서 자주 보이는 www는 전 세계에 걸쳐 연결된 웹을 뜻하는 World Wide Web의 약자 2. HTTP Client - Server 모델 정리 인터넷으로 연결된 두 컴퓨터는 데이터를 주고받으며, 웹 서비스에서는 두 컴퓨터가 맡은 역할에 따라 클라이언트(Client)와 서버(Server)로 나누어 구분함. 클라이언트와 서버로 역할을 나누어 데이터를 요청하고 응답하는 구조 를 클라이언트-서버 모델이라고 함. 오늘날 대부분의 웹 서비스는 이 구조를 바탕으로 동작하며, 클라이언트-서버 모델에서는 항상 클라이언트가 먼저 서버로 요청을 보내고, 서버는 이에 응답함. <특징> 역할이 명확이 분리된다 요청과 응답의 방향이 정해져 있다 여러 클라이언트가 하나의 서버를 사용할 수 있다 서버는 항상 대기하며 요청을 처리한다 서버에서 데이터와 기능을 중앙에서 관리한다 역할은 통신 상황에 따라 달라질 수 있다. ex)브라우저 -> 웹서버 : 브라우저(클라이언트), 웹서버(서버) 웹서버 -> 데이터베이스 : 웹서버(클라이언트), 데이터베이스(서버) * 역할을 나누는 이유 * 가장 큰 이유는 데이터를 중앙에서 관리하기 위해서임. 서버라는 역할을 지정해 한곳에서 데이터를 관리하면, 나머지 컴퓨터는 서버에 필요한 데이터를 요청하고 항상 최신의 데이터를 받을 수 있음. HTTP(HyperText Transfer Protocol)란? 클라이언트와 서버가 요청과 응답 메시지를 어떻게 주고 받을지 정한 통신규칙 이 메시지는 요청인지, 응답인지 무엇을 요청하는지 (메서드) 요청이 성공했는지 실패했는지 (상태코드) 데이터는 어떤 형식인지 (헤더, 본문) HTTP 메시지의 구성 시작 줄(start line) 요청: 메서드 + URL + HTTP 버전 응답: 상태 코드 + 메시지 헤더(header): 메타데이터(형식, 길이, 인증 정보 등) 본문(body): 실제 전달되는 데이터 상태코드 클라이언트의 요청에 대해 서버가 처리 결과를 숫자로 알려주는 응답코드임. 2XX (성공) 3XX (리다이렉션) 4XX (클라이언트 오류) 5XX (서버 오류) *상태코드에서 클라이언트 코드가 서버 코드 보다 많은 이유? 상태코드는 사용자에게 뭐가 문제인지 알려주기 위한것이기 때문이기도 하고, 서버 코드가 많으면 보안문제가 발생할 수 있기 때문 Curl 을 활용한 HTTP 요청 예제 curl -X POST https://jsonplaceholder.typicode.com/posts -H "Content-Type: application/json" -d '{"title": "hello", "body": "world"}' { "title": "hello", "body": "world", "id": 101 }% 3. RestAPI API 란? 서로 다른 프로그램끼리 데이터나 기능을 사용할 수 있도록 정해 둔 인터페이스 ex) 서버: 로그인하려면 이런 형식으로 요청해 클라이언트: 그 형식에 맞게 요청할게 RestAPI 정의 웹의 기본 원칙을 따르는 API설계 방식 -> 자원을 중심으로 HTTP메소드를 사용해 클라이언트와 서버가 통신하는 API 설계 웹 API를 일관되게 설계하기 위한 대표적인 방식임 REST는 엄격한 규칙은 아니고, API를 일관된 방식으로 설계하자는 일종의 제안에 가까움 URL과 HTTP Method로 요청의 의도를 나타내는 방법 URL(Uniform Resource Locator) : 웹에서 자원의 위치를 식별하는 주소 HTTP Method : 클라이언트가 서버에 어떤 동작을 원하느지 표현하는 방법 GET : 자원 조회 POST : 자원 생성 PUT : 자원 전체 수정 PATCH : 자워 일부 수정 DELETE : 자원 삭제 ex) 클라이언트는 전체 사용자 목록 조회를 원한다 -> GET https://example.com/users 예외 케이스 기본 CRUD를 벗어나는 경우도 있음 의미 있는 행위 자체가 목적이 되는 요청이 존재함 -> 이런 경우 관례적으로 POST 사용 ex) POST/orders/123/cancel -> 주문 취소 (주문 삭제랑은 다름. ) POST/auth/login -> 로그인 POST/payments/1/confirm -> 결제 확정 Query Parameter URL에 붙여서 전달하는 데이터로, 주로 조회 조건이나 옵션을 표현할 때 사용 데이터를 어떻게 보고 싶은지를 서버에 알려주는 역할 경로(path)뒤에 ?key=value 형태로 추가 ex) https://www.google.com/search?q=python 를 검색창에 치면 python을 검색한 구글 창이 뜸!!
Travel plans can change unexpectedly, and knowing the rules before modifying your reservation can make the process easier. United Airlines offers several options for eligible passengers who need to change their flights, although the applicable conditions can depend on the fare type, route, ticket terms, and MileagePlus status. This guide explains the key aspects of united flight change , including fees, eligibility, and the basic process for modifying a reservation. What Is the United Flight Change Policy? The united flight change policy allows eligible travelers to modify details of an existing reservation, such as the travel date or flight time. Depending on the ticket and itinerary, passengers may be able to make changes online through United's website or mobile app. United has eliminated change fees for many standard Economy and premium-cabin tickets for flights within the United States and certain international itineraries. However, Basic Economy tickets and some other restricted fares can have different rules. Because ticket conditions vary, travelers should review their reservation before making a change. United Flight Change Fees One of the most important factors in a united airline flight change is whether a change fee applies. For many eligible tickets, United does not charge a traditional change fee. However, this does not necessarily mean that changing a flight is always free. If the new flight costs more than the original flight, the passenger may need to pay the fare difference. For example, if the original ticket costs $250 and the replacement flight costs $325, the traveler may have to pay the applicable $75 difference. If the new flight is less expensive, the treatment of the remaining value depends on the ticket's terms and applicable United policies. Basic Economy Restrictions Basic Economy tickets generally have more restrictions than standard Economy fares. Depending on the itinerary and ticket conditions, passengers may have limited options for changing their reservations. Before purchasing a Basic Economy ticket, travelers should carefully review the applicable restrictions. If flexibility is important, comparing the available fare options before booking can help identify a ticket with more change flexibility. Same-Day Flight Changes United also provides same-day flight change options for eligible travelers. These options can allow a passenger to request a different flight on the same calendar day, subject to availability and eligibility requirements. Same-day changes are different from changing the travel date entirely. Travelers should check whether their reservation qualifies and whether the desired alternative flight has available space. MileagePlus status and fare type can also affect the options available to a traveler. How to Change a United Flight Online Passengers can generally start the united flight change process through United's website or mobile application. A typical process includes: Visit the United Airlines website or open the United app. Access My Trips. Enter or select the applicable reservation. Choose the option to change or modify the flight. Select a new available flight. Review any fare difference or applicable charges. Confirm the change. Save the updated itinerary. The exact options displayed depend on the reservation and ticket conditions. Eligibility for Flight Changes Eligibility for a united airline flight change can depend on several factors, including: Ticket type Fare conditions Origin and destination Travel date Whether the ticket was purchased directly from United MileagePlus status Whether the requested change is same-day Availability on the replacement flight Tickets purchased through travel agencies or other third-party sellers may require the passenger to contact the original booking provider. What Happens When United Changes Your Flight? If United makes a significant schedule change or cancels a flight, passengers may have different options from those available for a voluntary change. Depending on the circumstances, United may provide rebooking alternatives or other remedies under its applicable policies. Travelers affected by an airline-initiated change should review the notification received from United and the options associated with their reservation. Tips Before Changing Your Flight Before completing a united flight change, consider these tips: Check the fare difference before confirming. Review the ticket's restrictions. Compare multiple replacement flights. Check the new departure and arrival times. Save the updated confirmation. Make changes as early as possible when your plans are known. Contact United if the online system does not provide the option you need. Final Thoughts Understanding the united flight change policy can help travelers make informed decisions when their plans change. While many eligible United tickets no longer have traditional change fees, passengers may still need to pay a fare difference when selecting a more expensive replacement flight. The united airline flight change process and available options can vary according to the ticket, route, fare type, and other circumstances. Travelers should always review the latest United Airlines terms and the specific conditions attached to their reservation before confirming a change.
망했다 미래의 악마 배에 머리 넣었으면 나도 딱히 큰 대가없이 계약 할 수 있었을듯? 회사도 망했고 6년넘게 거의 올인한 관계도 망했고 그랬다 미래 최고 제로부터 시작하는건 이번 분기였는데 리제로를 저번 분기에 써버렸네 제로부터 시작하니까 홀가분한건 맞는데. 그렇다고 좋은건 아니다. 어쩔 수 없이 제로부터 시작하는거다. 하지만? 그 덕분에 객관적으로 현재 상태를 점검하고 계속 이 길로 나아갈지-여러 시도를 더욱 적극적으로 할지~ 어쩔지~ 좀 더 편안한 관점에서 생각해볼 수 있을 것 같다. 내가 좋아했던, 자진해서 꽁꽁 묶고 있던 것들에게서 반강제 탈출했으니까 뭘 하고싶은지 뭘 해야할지 나눠서 생각해보고 이것저것 해보자. 아 그리고 추천인받은 토증 가고싶었는데, 좋은 등가교환이었다. 해낸것 회사망하기 재취업? << 이거 해야하는지가 제일 고민이긴 한데 시도는 하기 베스타 관련 베스타 전업화 전면 무료화(알림톡 bm 부분유료) 알림톡 출시 마케팅 컨택(레뷰 ... etc) 안드로이드 출시 예약페이지 서비스 제공 네이버예약 실패하기 > 링크드인, 스레드 등지에 도움 요청도 해보는 중이긴 함 잔디밭 거의 1년 다 되어가는중 해파랑길 50km 넘게 걸어보기 구마노고도 순례길 찍먹하기(50~60km) 구마노고도 순례길 영상 촬영 + 유튜브화 > 하는중... 서로 갈길 가기 알아간것과 느낀것 많은 것들이 끊어졌다 내가 믿던 것들이 전반적으로 다 끊어졌다 이런 상태일수록 처음부터 다 쌓아나가고 크게 성장하기 좋긴 한데 굳이? 그래야하나? 이런 생각이었지 사실. 큰 상실에서 많은 것들을 얻어갈 수 있는건 맞지만 결국 트레이드오프라서 손에 쥐고 있던 것들을 놓기가 싫었다. 그리고 성장하겠다고 항상 손에 있는걸 던지는 사람보다는 내가 원하는걸 손에 잘 쥐고 있는것도 능력이라고 생각했고, 나는 그런 사람이라고 생각했기에. 하지만 뭐 어쩔수있나 강도가 총들고와서 다 내놓고 가라해서 빤스도 내놓고 나왔다. 구추 덜렁거리고 있는 상태니까 입던 빤스부터 다시 사서 입을지 훈도시 입을지 치마 입을지 노팬티로 다닐지 뭐 등등 어떻게 쌓아나갈지 고민좀 해봐야겠다. 아무튼 지금의 상태가 새롭게 쌓아나가긴 좋긴 하니까- 똑같은 일을 해도 나에게 온전히 집중하는거니까. 뭘 해야할지 생각도 많이 하고 행동도 더 많이 해보자. 간단한 것부터 대단한 것까지 아무렇지않게 해보자. 아 생각해보니까 내가 좀 게을러지고 현재에 안주하려고 했었나? 왜 이런 자극들을 주는거지? 신이시여 도시테... 나 분명 슬슬 이직 준비하려고 포폴 업데이트에 내용도 좀 채워넣고 있었고 직업적으로도 발전하고 있었고 개인 플젝도 시간 써가면서 이것저것 눈에 보이게 발전시키고 있었는데 내가 게을렀나? 분명 라시사처럼 달리지는 않긴 했는데 난 그런 스타일이 아니었는데 그냥 이런 생각 안하고 열심히 한건데 적당히 열심히 해선 안되는건가? 왜 안되는거지 적당히 적당히 중용을 지키면서 살고싶은데 아 모르겠다 적당히가 좋긴 한데 억지로 모든 세상이 달리라고 하는 시점인거같긴 하다. 지원한곳 CJ올리브영(Domestic) > 서탈 토스 > 서탈 토스뱅크 > 서탈 토스플랫폼 > 서탈 토스증권 > 추천인 지원 > 면접 > 사업자 관련 논의 후 탈 토스플레이스 > 서탈 보이저엑스 > 서류합격 > 과제탈락 라프텔 > 서탈 그린카 > 서탈 하이브 > 서탈 누아 > 서탈 빌라모자이크(PO) > 서탈 원프레딕트 > 서탈 고이(PO) > 서탈 헤렌(공비서) > 서탈 빗썸 > 서탈 엔에프타임 > 서류합격 > 면접 > 면탈 비마이프렌즈 > 서탈 DSRV > 서탈 아정당 QA > 서탈 CJ올리브영(Global) > 서탈 해빗팩토리 > 서탈 플래티어 > 서탈 큐피스트 > 서류합격 > 과제탈락 스냅컴퍼니 > 크리마 > 서탈 패스오더 > 서류합격 > 면접 다이렉트클라우드랩 > 서탈 퓨쳐위즈 > 서탈 아정당 AI builder > 서탈 CJ Ment QA > 서탈 윌로그 코코지 > 서탈 팬딩 콜로세움컴퍼니 > 서탈 테슬라 필드 엔지니어 > 서탈 앤서스랩코리아 핀다 취팡 tobe(26년 3분기) 모르겠다. 베스타 네이버 제휴까지 되기 베스타 디자인/UX 전반 개편해서 토스급으로 만들기 베스타 링크 말고도 bm 추가하기(그 신규 bm을 구독 자체에 포함시키기 > 베스타 링크 말고도 여러 유료 기능 해금 시키는 형태로) 4분기 마일스톤대로 잘 살기 안죽고 잘 살기 전체적인 코멘트 몇번 죽을뻔하고(타의적으로 말고) 정신과 약좀 챙겨먹고 걸으러도 다니고 새로운 중심이 좀 잡히니까 좀 살거같다. 그나마 좀 살거같은 상황에서 드는 고민은 다음과 같다. 재취업을 해야할까? 베스타가 막 크게 잘되는것도 아니고, 유튜브도 이제 첫 영상이고, 모든게 사실 다 처음인 상황이다. 되게 안정적이고 원하는것도 되게 단순했던 삶이라서, 단순하게 살고 있었는데 다 개박살나고나니까 뭘 해야할까? 싶어서. 되게 진부한 표현이긴 한데, AI시대다. 유료로 결제하는 사람은 3%고 그 안에서도 이정도로 적극적으로 쓰는 사람은 되게 적겠지. 그렇다고 내가 선두주자라는건 아니고... 딱 롤로치면 처음했는데 골드나오는 정도의 애매한 그런 포지셔닝일듯. 잘쳐주면 다이아? 오버워치 처음할때 그느낌으로다가. 새로운 도구들로 서비스도 만들고 사업도 하고 유튜브도 하고 여행도 다니고 사실상 개인으로서 AI를 참 잘 활용하고 있는 중이긴한데. 회사의 일원으로 안정적인 월급 추구하며 살아야 할까 뭐 SK하이닉스나 삼전 뭐 로펌 이정도 대기업 가면 모르겠는데. 못가잖아(한남톤으로) 갈수있었으면 가서 이제 결혼준비하고있었겠지 근데 나는 그럴 사람이(좋은쪽으로 + 안좋은쪽으로) 아니었던거고 운명이 아니었던거지 뭐 사실 갈 수 있었어도 나중에 후회할 사람이었을까? 요즘 제일 궁금한게 '나라는 사람은 진짜 뭘 하고싶어하는건지?'다. 지금까지는 그냥 내가 어떤 사람인지는 잘 모르는 상태로 내가 좋아하는 사람과 평생을 보내겠다는 일념에만 좀 집중했던 나날들이 아닐까 싶다. 뭐 그게 나쁘다는건 전~~혀 아니지만, 이렇게 된 이상 내 자신에 대해 잘 알아가야 하니까. 이제부터라도 하나씩 해보고 싶은걸 해야지. 다만... 좀 무서운건 이렇게 하고싶은걸 지금 했을 때, 내가 다시 좋아하는 사람이 생기고 뭐 그러면 그때가서 직업을 어떻게 얻지 아 이러나 저러나 고민이니까 남한테 손 안벌리고 살 수 있도록 잘 세팅해서 꼴리는대로 다 해보고 열심히도 해봐야지. 그것말고는 별 수가 없다. 열심히 하자.
이 포스트에서는 이제까지 작성하였던 GEO에 관한 내용을 간단하고 직관적으로 정리해볼 예정이다. 정리할 논문 리스트는 아래와 같다. PoisonedRAG : Knowledge Corruption Attacks to Retrieval-Augmented Generation of Large Language Models GEO : Generative Engine Optimization Ranking Manipulation for Conversational Search Engines (EMNLP2024) What Generative Search Engines Like and How to Optimize Web Content Cooperatively - AutoGEO Agentic GEO From Experience to Skill- Multi-Agent Generative Engine Optimization via Reusable Strategy Learning — MAGEO Structural Feature Engineering for Generative Engine Optimization: How Content Structure Shapes Citation Behavior — GEO-SFE E-GEO : A Testbed for Generative Engine Optimization in E-Commerce SAGEO Arena : A Realistic Environment for Evaluating Search-Augmented Generative Engine Optimization Poisoned RAG 요약 RAG는 LLM이 최신 데이터에 대한 한계를 갖는걸 완화해주는 기법이다. RAG는 Knowledge DB, Retriever, LLM으로 구성된다. 공격자는 공격자가 원하는 답을 얻기 위해 공격 표면으로써 Knowledge DB에 malicious texts을 주입한다. 본 논문에서는 malicious text를 optimization problem으로써 수식화 하였으며, 아래 두 조건으로 나눠진다. retrieval condition: malicious text가 target question에 대해 검색되어질수 있는것을 의미한다. generation condition: malicious text가 LLM이 target question에 대해 target answer을 생성할수 있게 하는 것을 의미한다. 주의할점은 본 논문은 RAG 시스템의 접근 권한을 탈취하거나 악성 문서의 유입 경로를 확보하는 방법을 다루지 않는다. 대신, knowledge DB에 악성 문서를 삽입할 수 있다고 가정하고, 해당 문서가 검색되면서(retrieval condition) LLM이 공격자가 의도한 답변을 생성하도록(generation condition) 악성 문서를 구성하는 방법을 제안한다. Method $$ \max_{\Gamma}\ \frac{1}{M}\cdot \sum_{i=1}^{M} \mathbb{I}\left( \mathrm{LLM}\left(Q_i; E(Q_i; D \cup \Gamma)\right) = R_i \right) \tag{2} $$ 위 수식은 질문 Q로부터 얻은 대답이 우리가 원하는 대답인지를 수치적으로 본다. $\Gamma$: Q에대한 malicious text 5개 $D \cup \Gamma$: corrupted knowledge database (부패된 지식 DB) $E(Q_i; D \cup \Gamma)$: 질문 Q에 대해 부패된 지식 DB로부터 얻은 k개의 검색된 문서 $LLM(*)$: 최종 응답 $\mathbb{I}$: 대답이 target answer에 속하면 1, 아니면 0 $max_{\Gamma}$: 공격 성공률이 최대가 되도록 악성 문서 집합 $\Gamma$를 찾는다는 뜻, 즉 악성 문서 개수는 5개로 고정된 상태로 이 5개의 악성 문서 구성이 공격 성공률이 최대가 되도록 하도록 구성된다는 뜻. 위 공격 성공률이 최대가 되려면 malicious text가 retrieval condition, generation condition을 만족하여야 한다. 그래야 최종 응답에 포함되기 때문이다. 재미있는건 malicious text P의 S을 만드는 방식인데, 블랙박스 방식에서는 $P = S \oplus I$이며, S는 악성 문서의 제목, I는 악성 문서의 본문처럼 생각하면 된다. (실제로는 S는 검색을 유도하는 앞부분, I는 답변을 유도하는 뒷부분이다) 블랙박스 방식에서는 S를 Q로 상정한다. 하지만 화이트박스에서는 retrieval 내부를 알기에 다음과 같은 gradient descent 수식으로 S을 최적화 한다. $$ S = \underset{S'}{\arg\max}; \operatorname{Sim}\left(f_Q(Q),, f_T(S' \oplus I)\right) \tag{5} $$ $f_Q(Q)$: 질문 인코더가 $Q$를 변환한 문서 임베딩 $f_T(S' \oplus I)$: 문서 인코더가 악성 문서 전체를 변환한 문서 임베딩 $Sim$: 두 임베딩의 유사도 $S'$: 검색 유사도를 높이기 위해 최적화하는 후보 텍스트 $\arg\max_{S'}$: 유사도를 가장 크게 만드는 후보 텍스트 $S'$ 자체를 반환 $S$: 최적화 결과로 얻은 텍스트 참고로 I는 GPT4와 프롬프트를 통해 만들어진다. 전체 흐름 1. Knowledge DB 구성 NQ 기본 실험에서는 위키피디아 기반 문서 약 268만 개로 Knowledge DB $D$를 구성. 2. 타깃 질문과 답변 설정 데이터셋에서 타깃 질문 $Q$를 선택하고, 공격자가 유도하려는 오답 $R$을 설정. 3. 수식 (2)로 전체 공격 목표 정의 악성 문서 집합 $\Gamma$ (한 쿼리당 악성 문서 5개)를 삽입했을 때, 타깃 질문들에 대해 LLM이 공격자가 원하는 답변을 생성하는 비율을 최대화하는 것이 목표. 이를 위해 악성 문서는 다음 두 조건을 충족하도록 구성. - Retrieval condition: 타깃 질문에 대한 검색 결과에 악성 문서가 포함되어야 함. - Generation condition: 악성 문서가 컨텍스트로 제공됐을 때 LLM이 타깃 답변을 생성하도록 유도해야 함. 4. 목표를 달성하기 위한 악성 문서 생성 기본 설정에서는 질문마다 $P=S\oplus I$ 형태의 악성 문서를 5개 생성. - 먼저 GPT-4와 프롬프트로 generation condition을 충족하도록 $I$를 생성하고, 공격자 측 LLM에서 타깃 답변이 나오는지 확인하며 정해진 횟수 내에서 재시도. - 이어서 retrieval condition을 충족하도록 $S$를 구성. 블랙박스에서는 $S=Q$로 설정하고, 화이트박스에서는 수식 (5)를 통해 질문 $Q$와 전체 문서 $S\oplus I$의 임베딩 유사도를 최대화하도록 $S$를 최적화. 5. 악성 문서 삽입 및 RAG 실행 생성한 악성 문서 집합 $\Gamma$를 DB에 삽입하여 $D\cup\Gamma$를 구성. 타깃 질문마다 유사도가 높은 상위 5개 문서를 검색하고, 이를 질문과 함께 대상 LLM에 제공해 답변을 생성. 6. 검색 성능 및 최종 공격 성공률 평가 악성 문서가 얼마나 검색되는지를 Precision, Recall, F1으로 평가하고, 최종 답변이 타깃 답변에 해당하는 비율을 공격 성공률(ASR) 로 평가. 이 ASR이 수식 (2)에서 최대화하려는 목적값에 해당
타이타닉 데이터로 PyTorch 모델 만들기 오늘은 타이타닉 승객 데이터를 이용해 생존 여부와 성별을 예측하는 모델 을 만들었다. 아직 세부 코드를 모두 이해한 것은 아니지만, 데이터 준비부터 학습과 검증까지의 전체 흐름을 익혔다. 1. 데이터 확인과 feature 선택 각 열의 의미를 확인하고, 모델에 넣을 입력 정보와 예측할 정답을 구분했다. 입력(X) : 승객의 나이, 객실 등급, 운임, 동반 가족 수 등의 정보 정답(y) : survived 와 sex 정답으로 사용하는 열은 입력에서 제외해야 한다는 점도 배웠다. 2. 데이터 분리와 전처리 데이터를 train, validation, test 로 나눴다. 각각 학습, 모델 선택, 최종 평가에 사용하는 데이터다. 모델이 계산할 수 있도록 boolean과 문자 데이터를 숫자로 변환하고, 나이의 결측값은 train에서 구한 중앙값 으로 채웠다. 결측값이 많은 deck 은 제외하기로 했다. 3. Dataset과 DataLoader 생성 전처리한 데이터를 PyTorch의 Tensor 로 변환했다. TensorDataset 으로 입력과 정답을 묶고, DataLoader 로 한 번에 32명씩 학습하도록 구성했다. 학습 데이터는 순서를 섞어서 사용했다. 4. 모델 구축과 학습 여러 Linear 층과 ReLU 를 연결한 신경망을 만들었다. 생존 여부와 성별을 함께 예측하므로 마지막 출력은 2개로 설정했다. 학습은 다음 과정을 반복한다. 예측 → 정답과 비교해 오차 계산 → 기울기 계산 → 가중치 수정 손실함수는 BCEWithLogitsLoss , 최적화 도구는 Adam 을 사용했다. 5. 결과 확인과 기록 마지막 학습 기록에서 두 예측을 합친 정확도는 약 73.7% 였다. 검증 데이터에서는 다음 결과가 나왔다. 생존 예측 정확도: 65.17% 성별 예측 정확도: 71.91% 학습 정확도만으로 성능을 판단할 수 없고, 별도 데이터에서 검증해야 한다는 점을 배웠다. 또한 epoch마다 Loss와 Accuracy를 저장하고 그래프로 확인하는 방법도 살펴봤다. 오늘 이해한 핵심 흐름은 데이터 확인 → 전처리·분리 → Dataset·DataLoader 생성 → 모델 구축 → 학습 → 검증 이다. 앞으로는 각 코드의 역할을 더 자세히 이해하고, 전처리와 모델 설정에 따라 성능이 어떻게 달라지는지 확인해 보고 싶다.
1. 오늘의 한 줄 요약 Spring Cloud Netflix Eureka를 이용해 Service Discovery 서버를 구성하고, User Service를 여러 인스턴스로 실행해 Eureka에 등록하는 과정 을 실습했다. 어제 MSA의 전체적인 개념을 알아보았다면, 오늘은 여러 서비스의 위치를 관리하는 Service Discovery를 직접 구성해 보았다. 2. 배운 내용 Service Discovery란? MSA에서는 하나의 서비스를 여러 인스턴스로 실행할 수 있다. 예를 들어 User Service가 다음과 같이 서로 다른 포트에서 실행될 수 있다. USER-SERVICE ├─ Instance A : localhost:60000 ├─ Instance B : localhost:60001 └─ Instance C : localhost:60002 인스턴스의 개수가 많아지거나 실행 위치가 변경되면 다른 서비스가 각각의 주소를 직접 관리하기 어려워진다. Service Discovery는 실행 중인 서비스의 이름과 위치를 등록하고, 필요한 서비스가 해당 정보를 검색할 수 있도록 관리하는 역할을 한다. Service Instance │ │ 자신의 이름과 위치 등록 ▼ Eureka Server │ │ 사용 가능한 인스턴스 정보 제공 ▼ 다른 서비스 또는 Load Balancer 오늘은 Service Discovery 구현체로 Spring Cloud Netflix Eureka 를 사용했다. Eureka Server와 Eureka Client Eureka는 크게 Server와 Client로 구분할 수 있다. Eureka Server : 등록된 서비스와 인스턴스 정보를 관리한다. Eureka Client : 자신의 애플리케이션 이름과 주소를 Eureka Server에 등록한다. Eureka Dashboard : 현재 등록된 서비스와 실행 상태를 확인한다. Eureka에 등록된 서비스를 이용하면 호출하는 쪽에서 특정 IP와 포트를 직접 기억하는 대신, USER-SERVICE 와 같은 서비스 이름을 기준으로 인스턴스를 찾을 수 있다. 3. 실습 / 적용 Eureka Server 구성하기 먼저 Eureka Server 역할을 담당할 service-discovery 프로젝트를 생성했다. 메인 클래스에 @EnableEurekaServer 를 추가해 Eureka Server 기능을 활성화했다. @SpringBootApplication @EnableEurekaServer public class ServiceDiscoveryApplication { public static void main(String[] args) { SpringApplication.run( ServiceDiscoveryApplication.class, args ); } } application.yml 에는 서버 포트와 Eureka 설정을 작성했다. server: port: 8761 spring: application: name: service-discovery eureka: client: register-with-eureka: false fetch-registry: false service-discovery 는 다른 서비스가 등록되는 Eureka Server이므로 자기 자신을 다시 Eureka에 등록하거나 서비스 목록을 가져올 필요가 없다. register-with-eureka: false : 자기 자신을 Eureka에 등록하지 않는다. fetch-registry: false : 다른 서비스의 등록 정보를 가져오지 않는다. 애플리케이션 실행 후 다음 주소에서 Eureka Dashboard를 확인했다. http://localhost:8761 User Service 등록하기 다음으로 Eureka Client 역할을 하는 user-service 프로젝트를 생성했다. application.yml 에 서비스 이름과 Eureka Server 주소를 설정했다. server: port: 60000 spring: application: name: user-service eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://127.0.0.1:8761/eureka User Service를 실행하면 Eureka Server에 USER-SERVICE 라는 이름으로 인스턴스가 등록된다. 여기서 실제 등록 기준이 되는 중요한 값은 다음과 같다. spring: application: name: user-service 같은 이름으로 실행한 여러 애플리케이션은 Eureka에서 하나의 서비스에 속한 여러 인스턴스로 관리된다. 같은 서비스를 여러 인스턴스로 실행하기 같은 User Service를 여러 개 실행하려면 각 인스턴스가 서로 다른 포트를 사용해야 한다. 포트를 직접 지정해 실행할 수도 있다. 60000 60001 60002 60003 또는 다음과 같이 포트를 0 으로 지정할 수 있다. server: port: 0 server.port: 0 으로 설정하면 애
1. 한 줄 요약 Service Discovery는 계속 변하는 서비스 인스턴스의 위치를 등록하고 검색하는 기능이며, Spring Cloud Netflix Eureka를 사용하면 마이크로서비스가 자신의 정보를 등록하고 다른 서비스가 논리적인 서비스 이름으로 인스턴스를 찾을 수 있다. 2. 배운 내용 Service Discovery가 필요한 이유 모놀리식 애플리케이션은 대부분 하나의 주소로 요청을 전달하므로 호출할 서버의 위치를 파악하기 어렵지 않다. 반면 MSA에서는 같은 서비스를 여러 인스턴스로 실행할 수 있고, 확장이나 장애 복구 과정에서 인스턴스의 주소와 포트가 계속 바뀔 수 있다. 예를 들어 user-service 가 다음과 같이 실행될 수 있다. user-service:60000 user-service:60001 user-service:60002 호출하는 서비스가 이 주소들을 코드나 설정에 직접 작성하면 인스턴스가 추가되거나 제거될 때마다 설정을 변경해야 한다. Service Discovery를 사용하면 각 인스턴스가 중앙의 Service Registry에 자신의 위치를 등록한다. 다른 서비스나 Load Balancer는 Registry를 조회해 현재 요청을 처리할 수 있는 인스턴스를 찾는다. 서비스 인스턴스 ↓ 등록 Service Registry ↑ 조회 호출 서비스 또는 Load Balancer 내가 이해한 Service Discovery는 전화번호를 모두 외우는 대신 이름과 현재 전화번호를 관리하는 연락처를 조회하는 것과 비슷하다. Eureka Server와 Eureka Client Spring Cloud Netflix Eureka는 Service Discovery를 구현하기 위한 Server와 Client를 제공한다. Eureka Server Eureka Server는 서비스 인스턴스 정보를 저장하는 Service Registry 역할을 한다. 서비스 이름 호스트 주소 포트 상태 정보 서비스의 메타데이터 Eureka Client 각 마이크로서비스는 Eureka Client가 되어 자신의 정보를 Eureka Server에 등록한다. 등록된 서비스는 다른 서비스의 위치를 조회할 수도 있다. 같은 서비스가 여러 인스턴스로 실행되면 Load Balancer는 Eureka에 등록된 인스턴스 목록을 바탕으로 요청을 분산할 수 있다. Eureka Server 프로젝트 생성 강의에서는 다음 환경으로 Service Discovery 프로젝트를 생성했다. Java 21 Spring Boot 3.5.0 Spring Cloud 2025.0.0 Eureka Server Spring Boot와 Spring Cloud는 아무 버전이나 조합할 수 있는 것이 아니다. Spring Cloud의 Release Train마다 호환되는 Spring Boot 세대가 정해져 있으므로 프로젝트를 생성할 때 버전 호환성을 확인해야 한다. Eureka Server를 사용하기 위해 다음 의존성을 추가한다. <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> 메인 클래스에는 @EnableEurekaServer 를 선언한다. @SpringBootApplication @EnableEurekaServer public class ServiceDiscoveryApplication { public static void main(String[] args) { SpringApplication.run( ServiceDiscoveryApplication.class, args ); } } @EnableEurekaServer 를 사용하면 해당 Spring Boot 애플리케이션이 Eureka Server로 동작한다. Eureka Server 설정 Eureka Server는 기본적으로 8761 번 포트를 사용하도록 설정했다. server: port: 8761 spring: application: name: service-discovery eureka: client: register-with-eureka: false fetch-registry: false 각 설정의 의미는 다음과 같다. register-with-eureka: false 현재 애플리케이션은 서비스를 등록받는 Eureka Server이므로 자기 자신을 Eureka Registry에 등록하지 않도록 설정한다. fetch-registry: false Eureka Client가 다른 서비스 목록을 가져오는 기능을 사용하지 않도록 설정한다. 단일 Eureka Server 실습에서는 다른 Eureka Server의 Registry 정보를 가져올 필요가 없으므로 두 값을 모두 false 로 설정했다. 애플리케이션을 실행한 뒤 다음 주소에 접속하면 Eureka 대시보드를 확인할 수 있다. http://localhost:8761 처음에는 등록된 서비스가 없기 때문에 인스턴스 목록이 비어 있다. User Service를 Eureka Client로 등록하기 User Service에는 Eureka Client 의존성을 추가했다. <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> 강의에서는 메인 클래스에 @EnableDiscoveryClient 를 선언했다. @SpringBootApplication @EnableDiscoveryClient public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run( UserServiceApplication.class, args ); } } 현재 Spring Cloud에서는 Eureka Client Starter가 클래스 경로에 있으면 자동 설정을 통해 Eureka Client로 동작할 수 있어 @EnableDiscoveryClient 가 필수는 아니다. 다만 강의에서는 Discovery Client임을 명시적으로 표현하기 위해 사용했다. User Service의 설정은 다음과 같다. server: port: 60000 spring: application: name: user-service eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka fetch-registry: true register-with-eureka: true spring.application.name 은 Eureka에 등록되는 서비스의 논리적인 이름으로 사용된다. defaultZone 에는 Eureka Server의 주소를 지정한다. 현재 공식 문서에서는 defaultZone 의 대소문자가 구분되므로 정확한 이름을 사용해야 한다. User Service를 실행하면 Eureka 대시보드에 다음과 같이 등록된다. Application: USER-SERVICE Status: UP Port: 60000 같은 서비스의 인스턴스 확장하기 MSA에서는 하나의 서비스를 여러 인스턴스로 실행해 트래픽을 분산하거나 가용성을 높일 수 있다. 하지만 동일한 애플리케이션을 그대로 다시 실행하면 이미 사용 중인 포트 때문에 실행에 실패한다. Web server failed to start. Port 60000 was already in use. 하나의 IP와 포트 조합에는 하나의 서버만 바인딩할 수 있기 때문에 추가 인스턴스는 다른 포트를 사용해야 한다. IntelliJ VM Options 사용 두 번째 인스턴스의 실행 설정에 다른 포트를 지정한다. -Dserver.port=60001 Maven으로 실행 mvn spring-boot:run \ -Dspring-boot.run.jvmArguments="-Dserver.port=60002" 빌드된 JAR 파일 실행 mvn clean package java -Dserver.port=60003 -jar ./target/user-service-1.0.jar 각 인스턴스가 실행되면 Eureka에는 하나의 USER-SERVICE 아래 여러 인스턴스가 등록된다. USER-SERVICE ├── user-service:60000 ├── user-service:60001 ├── user-service:60002 └── user-service:60003 서비스를 호출하는 쪽은 물리적인 포트를 직접 선택하는 대신 USER-SERVICE 라는 논리적인 이름을 사용할 수 있다. Random Port 사용하기 인스턴스를 실행할 때마다 포트를 직접 지정하지 않고 운영체제가 사용 가능한 포트를 자동으로 할당하도록 설정할 수도 있다. server: port: 0 spring: application: name: user-service server.port 를 0 으로 설정하면 애플리케이션이 실행될 때 사용 가능한 임의의 포트가 할당된다. Tomcat started on port 53308 Tomcat started on port 53317 이 방식은 여러 인스턴스를 반복해서 실행할 때 포트 충돌을 피할 수 있다는 장점이 있다. Random Port 사용 시 Instance ID 문제 Random Port를 사용해 같은 서비스를 여러 번 실행했지만, Eureka 대시보드에서 인스턴스가 하나만 등록되는 문제가 발생했다. 두 애플리케이션의 서비스 이름과 기본 Instance ID가 같아 Eureka가 서로 다른 인스턴스로 구분하지 못했기 때문이다. 각 인스턴스에 고유한 ID를 설정해 문제를 해결할 수 있다. server: port: 0 spring: application: name: user-service eureka: instance: instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} client: service-url: defaultZone: http://127.0.0.1:8761/eureka 설정값은 다음 순서로 Instance ID를 만든다. spring.application.name 을 사용한다. spring.application.instance_id 가 있으면 해당 값을 사용한다. 값이 없으면 random.value 로 고유한 값을 만든다. 따라서 모든 인스턴스가 같은 user-service 라는 서비스 이름을 사용하면서도 각각 고유한 인스턴스로 등록될 수 있다. user-service:고유한 값 1 user-service:고유한 값 2 서비스 이름은 같은 기능을 제공하는 인스턴스를 묶는 기준이고, Instance ID는 그 안에서 각각의 실행 인스턴스를 구별하는 값이라고 이해했다. Service Discovery와 Load Balancing의 관계 Service Discovery와 Load Balancing은 비슷하게 느껴지지만 역할이 다르다. Service Discovery 현재 실행 중인 서비스 인스턴스가 어디에 있는지 관리하고 검색한다. Load Balancing Service Discovery에서 얻은 여러 인스턴스 중 하나를 선택해 요청을 분산한다. 클라이언트 요청 ↓ Load Balancer ↓ Eureka에서 인스턴스 목록 조회 ┌──────────┬──────────┬──────────┐ │ 60000번 │ 60001번 │ 60002번 │ └──────────┴──────────┴──────────┘ Eureka가 직접 모든 요청을 중계하는 것이 아니라 서비스의 위치 정보를 관리하고, 실제 요청 분산은 Spring Cloud LoadBalancer 같은 별도의 구성 요소가 담당한다. 3. 실습 / 적용 오늘 실습에서는 다음 순서로 Eureka 기반 Service Discovery 환경을 구성했다. Eureka Server 프로젝트를 생성했다. @EnableEurekaServer 로 Eureka Server를 활성화했다. 8761 번 포트에서 Eureka 대시보드를 실행했다. User Service에 Eureka Client 의존성을 추가했다. User Service를 Eureka Server에 등록했다. 서로 다른 포트를 사용해 여러 인스턴스를 실행했다. Eureka 대시보드에서 인스턴스가 함께 등록되는 것을 확인했다. server.port: 0 으로 Random Port를 사용했다. 고유한 instance-id 를 지정해 각 인스턴스를 구분했다. 4. 문제와 해결 문제 1: 추가 인스턴스 실행 시 포트 충돌 발생한 문제 같은 User Service를 추가로 실행했지만 기존 인스턴스가 사용 중인 60000 번 포트와 충돌했다. Port 60000 was already in use. 원인 같은 컴퓨터에서 동일한 IP와 포트를 사용하는 서버를 동시에 실행하려고 했다. 해결 방법 각 인스턴스에 서로 다른 포트를 지정했다. -Dserver.port=60001 -Dserver.port=60002 -Dserver.port=60003 또는 server.port: 0 으로 설정해 실행할 때마다 사용 가능한 포트를 자동으로 할당받았다. 문제 2: Random Port 인스턴스가 하나만 등록됨 발생한 문제 Random Port를 사용해 여러 인스턴스를 실행했지만 Eureka에서는 하나의 인스턴스만 보였다. 원인 포트는 달랐지만 Eureka가 인스턴스를 구분하는 Instance ID가 동일했다. 해결 방법 eureka.instance.instance-id 에 임의의 값을 포함해 각 인스턴스가 고유한 ID를 갖도록 설정했다. eureka: instance: instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} 문제 3: Spring Boot와 Spring Cloud 버전 선택 Spring Boot와 Spring Cloud의 버전을 독립적으로 선택하면 호환성 문제가 발생할 수 있다. 강의에서는 Spring Boot 3.5.0 과 Spring Cloud 2025.0.0 을 함께 사용했다. 공식 호환표에서도 Spring Cloud 2025.0.x 는 Spring Boot 3.5.x 세대와 대응한다. 실제 프로젝트에서는 강의의 초기 버전을 그대로 고정하기보다, 같은 Release Train 안에서 지원되는 최신 서비스 릴리스와 공식 호환표를 확인해야 한다. 5. 다음에 할 일 Eureka에 등록된 서비스 이름으로 다른 서비스를 호출해보기 Spring Cloud LoadBalancer의 요청 분산 방식 알아보기 Eureka Client의 Heartbeat와 인스턴스 제거 과정 공부하기 여러 Eureka Server를 구성하는 고가용성 방식 알아보기 API Gateway와 Eureka를 연결해 동적 라우팅 구현하기 참고 자료 Spring Cloud Netflix 공식 문서 Spring Cloud 공식 페이지 및 버전 호환표 Spring Initializr