소프트웨어 개발이라는 분야에 입성해서 대략 2~3년차가 되면 코드의 함수와 클래스 레벨을 넘어서서

구현하는 프로그램의 전체 프로세스와 시스템 구성을 고민하게 되는데 그때부터 주니어라고 할 수 있다.

그렇게 5~6년 일하다보면 어느새 익숙해진 개발 역량 덕분에 쓰던거 계속 쓰고, 돌려 쓰고, 다른 사람이 개발해 놓은거 갖다 쓰고

하다보면 또 새로운걸 해야 한다는 압박이 들곤 했었다.

그런데 이제는 이 분야를 지원해서 이제 막 입문하는 뉴비들이 프로그램 개발을 위한 도구들에 익숙해지는 것도 중요하지만

그것과 더불어 함께 중요시 해야 하는 사안이 생겨났으니 ...

바로 주니어를 건너띄고 시니어로의 점핑을 염두에 두고 과외를 해놓아야만 경쟁력이 생긴다.

 

과거엔 위에서 얘기한 주니어들이 참 할일이 많았다.

당연하게도 개발 역량과 더불어 비즈니스에 대한 혜안도 열리는 시점이라서 일감을 마구 쏟아넣어도 꾸역 꾸역 아웃풋을 내어 놓을 역량이 되는 것이다.

그런데 이젠 그 자리에 AI가 함께 한다.

AI가 있어서 주니어가 1/3 만 있어도 이전과 같거나 더 높은 효율성을 보인다.

게다가 이녀석이 주니어들보다 아웃풋이 점점 더 좋아지고 있다.

 

모든 업계에 새로운 트랜드의 바람이 너무나 거세다.

오늘 ChatGPT와 Gemini에게 코드 작성을 의뢰하여 받은 결과물에 소름이 돋는 느낌이 들어서 이렇게 감회를 적었다.

다음은 그 내용이다.

 

Order

다음과 같은 로그의 내용에서 rp번호를 추출해야 하는데 대괄호 안에 rp 라는 키워드를 삽입한 형태가 대부분이지만 로그에 숫자만 5자리나 6자리를 입력한 형태도 있어, 이러한 로그에서 rp 라는 키워드로 명확하게 지정한 숫자의 나열은 당연히 rp번호이고 rp라는 키워드가 없는 로그에서 숫자가 5자리 또는 6자리는 rp번호로 간주해서 rp번호들을 추출하는 java code 를 작성해줘

2a8b000000 Metainfo Commit 2025-0...57.03
d3ec000000 Help Folder Commit 2025-05...30.14
d4b9000000 Metainfo Commit 2025-05...8:23.90
a8b0000000 [RP 101453] [접근성] TAB키...
1501000000 [RP 101453] [접근성] TAB키...
7f90000000 RP 101409 Edge에서 사이즈가 깨지...
8e3000000 RP 101409 ...에도 동일한 로직 반영
99c1000000 [RP : 102742, 102677] [202505패치] iPad에서 ondblclick 이벤트 ...
0c43000000 Merge branch 'RELE....0.700' of https://tfs2.to...00 into RELE....0.700
441e000000 Envi...ration meta initonly 수정
ca9d000000 Help Folder Commit 2025-0...03:19.64
d153000000 RP 101409 Edge브라우저에서 확대축소 오동작...정
2f23000000 [RP 101854] [접근성] 에디트 값 수정 후...용 안됨. - 원복
a00000000 RP 102223 추가 수정 (And... 문제)
7298000000 RP 101409 NRE 오류 수정
b34e000000 RP 101409 zoom이 적용된 화면에서 ... 현상 추가 수정
c824000000 RP 102598 Comb...) 수정
44e000000 environ...ion 값 툴에서만 설정되도록 변경

Result (ChatGPT)

아래는 설명하신 조건에 맞춰 **RP번호(5자리 또는 6자리 숫자)**를 추출하는 Java 코드입니다.

✅ 조건 요약
[RP ...], RP ..., RP: ... 등 RP 키워드가 있는 경우, 그 뒤에 나오는 5~6자리 숫자를 추출.

RP 키워드가 없는 경우에도 5자리 또는 6자리 숫자가 등장하면 RP번호로 간주해서 추출.

중복 RP 번호는 제거.

✅ Java 코드
java
복사
편집
import java.util.*;
import java.util.regex.*;
import java.nio.file.*;
import java.io.*;

public class RpNumberExtractor {

    public static void main(String[] args) throws IOException {
        String logText = Files.readString(Paths.get("log.txt")); // log.txt에 로그를 저장해두었다고 가정

        Set<String> rpNumbers = new HashSet<>();

        // 1. RP 키워드가 있는 경우 추출: [RP 123456], RP 123456, [RP: 123456] 등
        Pattern rpPattern = Pattern.compile("(?i)\\bRP\\s*[:\\[\\(]?\\s*(\\d{5,6})");
        Matcher rpMatcher = rpPattern.matcher(logText);
        while (rpMatcher.find()) {
            rpNumbers.add(rpMatcher.group(1));
        }

        // 2. RP 키워드가 없는 5~6자리 숫자 추출
        Pattern numberPattern = Pattern.compile("\\b(\\d{5,6})\\b");
        Matcher numberMatcher = numberPattern.matcher(logText);
        while (numberMatcher.find()) {
            String number = numberMatcher.group(1);
            // 이미 추출된 RP번호가 아니면 추가
            if (!rpNumbers.contains(number)) {
                // 키워드 없이 등장한 경우인지 체크
                int index = numberMatcher.start();
                String context = logText.substring(Math.max(0, index - 20), Math.min(logText.length(), index + 20));
                if (!context.matches("(?i).*RP\\s*[:\\[\\(]?\\s*" + number + ".*")) {
                    rpNumbers.add(number);
                }
            }
        }

        // 출력
        System.out.println("추출된 RP 번호:");
        rpNumbers.stream().sorted().forEach(System.out::println);
    }
}
✅ 사용 방법
로그 텍스트를 log.txt에 저장합니다.

위 코드를 실행하면 로그에서 추출한 RP번호들을 중복 없이 출력합니다.

🔍 예시 출력
bash
복사
편집
101409
101453
101792
101854
102223
102598
102623
102668
102677
102713
102714
102742
필요하다면 파일 대신 문자열 입력으로 처리하거나 결과를 파일에 저장하는 기능도 추가 가능합니다.

 

Result (Gemini)


 

로그에서 RP 번호 추출 Java 코드

제공해주신 로그 내용에서 RP 번호를 추출하는 Java 코드를 작성해 드리겠습니다. 다음과 같은 규칙을 적용하여 번호를 추출합니다:

  1. RP 키워드가 명시된 경우: RP (대소문자 구분 없음) 키워드 뒤에 오는 숫자(5자리 또는 6자리)를 우선적으로 추출합니다. [RP 123456], RP 12345, [RP : 123456] 등 다양한 형태를 커버합니다.
  2. RP 키워드가 없는 경우: 로그 라인에서 단독으로 존재하는 5자리 또는 6자리의 숫자를 RP 번호로 간주하여 추출합니다.

이를 위해 **정규 표현식(Regular Expression)**을 강력하게 활용하겠습니다.


Java 코드 예제

Java
 
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RpNumberExtractor {

    public static void main(String[] args) {
        // 예시 로그 데이터 (제공해주신 로그 내용을 String 배열로 만듦)
        String[] logs = {
            "2a8b000000 Metainfo Commit 2025-0...57.03",
            "d3ec000000 Help Folder Commit 2025-05...30.14",
            "d4b9000000 Metainfo Commit 2025-05...8:23.90",
            "a8b0000000 [RP 101453] [접근성] TAB키...",
            "1501000000 [RP 101453] [접근성] TAB키...",
            "7f90000000 RP 101409 Edge에서 사이즈가 깨지...",
            "8e3000000 RP 101409 ...에도 동일한 로직 반영",
            "99c1000000 [RP : 102742, 102677] [202505패치] iPad에서 ondblclick 이벤트 ...",
            "0c43000000 Merge branch 'RELE....0.700' of https://tfs2.to...00 into RELE....0.700",
            "441e000000 Envi...ration meta initonly 수정",
            "ca9d000000 Help Folder Commit 2025-0...03:19.64",
            "d153000000 RP 101409 Edge브라우저에서 확대축소 오동작...정",
            "2f23000000 [RP 101854] [접근성] 에디트 값 수정 후...용 안됨. - 원복",
            "a00000000 RP 102223 추가 수정 (And... 문제)",
            "7298000000 RP 101409 NRE 오류 수정",
            "b34e000000 RP 101409 zoom이 적용된 화면에서 ... 현상 추가 수정",
            "c824000000 RP 102598 Comb...) 수정",
            "44e000000 environ...ion 값 툴에서만 설정되도록 변경",
            "Some commit message with only 12345", // 5자리 숫자 예시
            "Another commit with 987654 in the middle", // 6자리 숫자 예시
            "This commit has no rp numbers",
            "It has 123 but not 5 or 6 digits",
            "And a long number 1234567890" // 5,6자리 아님
        };

        System.out.println("--- RP 번호 추출 결과 ---");
        for (String logLine : logs) {
            List<String> rpNumbers = extractRpNumbers(logLine);
            System.out.println("로그: \"" + logLine + "\" -> RP 번호: " + (rpNumbers.isEmpty() ? "없음" : rpNumbers));
        }
    }

    /**
     * 로그 문자열에서 RP 번호를 추출합니다.
     * RP 키워드 뒤에 오는 숫자(5-6자리)를 우선적으로 찾고,
     * 그렇지 않으면 단독으로 존재하는 5-6자리 숫자를 찾습니다.
     *
     * @param logLine RP 번호를 추출할 로그 문자열
     * @return 추출된 RP 번호들의 리스트 (발견되지 않으면 빈 리스트)
     */
    public static List<String> extractRpNumbers(String logLine) {
        List<String> rpNumbers = new ArrayList<>();

        // 1. "RP" 키워드가 명시된 경우를 위한 정규 표현식
        // RP 뒤에 올 수 있는 공백, 콜론, 대괄호 등을 유연하게 처리하고 5 또는 6자리의 숫자를 찾습니다.
        // 그룹 1: 실제 RP 번호 숫자
        Pattern rpPattern = Pattern.compile(
            "[Rr][Pp]\\s*[:\\s]*\\[?(\\d{5,6})\\]?|\\b[Rr][Pp]\\s*[:\\s]*(\\d{5,6})",
            Pattern.CASE_INSENSITIVE // 대소문자 구분 없이 "rp"를 찾도록 설정
        );
        Matcher rpMatcher = rpPattern.matcher(logLine);

        while (rpMatcher.find()) {
            // 첫 번째 그룹 (대괄호 안에 RP 숫자) 또는 두 번째 그룹 (일반 RP 숫자)에서 값을 가져옴
            String rpNum = rpMatcher.group(1);
            if (rpNum == null) {
                rpNum = rpMatcher.group(2);
            }
            if (rpNum != null && !rpNumbers.contains(rpNum)) { // 중복 방지
                rpNumbers.add(rpNum);
            }
        }

        // 2. "RP" 키워드가 없는 경우를 위한 정규 표현식 (5자리 또는 6자리 숫자만)
        // 단어가 시작/끝나는 경계(\\b)에 있으면서, 앞에 "RP" 같은 문자가 없는 경우를 찾습니다.
        // 이 패턴은 rpPattern에서 찾지 못한 경우에만 적용되어야 합니다.
        // 즉, rpNumbers가 이미 채워져 있다면 이 패턴은 건너뜁니다.
        if (rpNumbers.isEmpty()) {
            Pattern standaloneNumberPattern = Pattern.compile(
                "\\b(?<![Rr][Pp]\\s*[:\\s]*)(?<!\\d)(\\d{5,6})(?!\\d)\\b"
            );
            Matcher standaloneNumberMatcher = standaloneNumberPattern.matcher(logLine);

            while (standaloneNumberMatcher.find()) {
                String num = standaloneNumberMatcher.group(1);
                // "RP" 키워드 뒤에 오는 숫자가 아닌 경우만 추가 (Negative Lookbehind)
                // 이미 rpPattern에서 찾은 숫자가 아닌 경우에만 추가
                if (!rpNumbers.contains(num)) {
                    rpNumbers.add(num);
                }
            }
        }
        
        return rpNumbers;
    }
}

코드 설명:

  1. extractRpNumbers(String logLine) 메서드:
    • 주어진 로그 라인에서 RP 번호를 추출하는 핵심 메서드입니다.
    • rpNumbers라는 List<String>을 반환하여 추출된 모든 RP 번호를 담습니다.
  2. 첫 번째 정규 표현식 (rpPattern):
    • "[Rr][Pp]\\s*[:\\s]*\\[?(\\d{5,6})\\]?|\\b[Rr][Pp]\\s*[:\\s]*(\\d{5,6})"
    • 이 패턴은 RP 키워드가 명시적으로 포함된 경우를 처리합니다.
    • [Rr][Pp]: 대소문자 구분 없이 "Rp" 또는 "RP"를 찾습니다.
    • \\s*: 0개 이상의 공백 문자를 의미합니다.
    • [:\\s]*: 0개 이상의 콜론(:) 또는 공백 문자를 의미합니다. ([RP : 12345])
    • \\[?: 0개 또는 1개의 여는 대괄호([)를 의미합니다. (옵션)
    • (\\d{5,6}): 5자리 또는 6자리의 숫자를 찾고, 이를 캡처 그룹 1로 만듭니다. 이 부분이 우리가 추출하려는 RP 번호입니다.
    • \\]?: 0개 또는 1개의 닫는 대괄호(])를 의미합니다. (옵션)
    • |: OR 연산자입니다.
    • \\b[Rr][Pp]\\s*[:\\s]*(\\d{5,6}): 대괄호가 없는 "RP 123456" 형태를 처리합니다. (\\b는 단어 경계)
    • Pattern.CASE_INSENSITIVE: 대소문자를 구분하지 않고 RP를 찾도록 설정합니다.
    • rpMatcher.find()를 통해 로그 라인에서 모든 일치하는 부분을 찾고, rpMatcher.group(1) 또는 rpMatcher.group(2)를 사용하여 실제 RP 번호(숫자 부분)를 추출합니다.
  3. 두 번째 정규 표현식 (standaloneNumberPattern):
    • "\\b(?<![Rr][Pp]\\s*[:\\s]*)(?<!\\d)(\\d{5,6})(?!\\d)\\b"
    • 이 패턴은 RP 키워드가 명시되지 않은 경우에 단독으로 존재하는 5자리 또는 6자리 숫자를 찾습니다.
    • 이 패턴은 rpNumbers 리스트가 비어 있을 때만 실행됩니다. 즉, RP 키워드를 찾지 못했을 때만 순수 숫자 패턴을 검사합니다.
    • \\b: 단어 경계를 나타냅니다. 숫자가 다른 문자에 붙어있는 경우를 방지합니다 (예: long12345에서 12345를 추출하지 않음).
    • (?<![Rr][Pp]\\s*[:\\s]*): 부정형 후방 탐색 (Negative Lookbehind) 입니다. 이 패턴이 일치하는 숫자 바로 앞에 "RP", "rp", "RP :" 등 RP 관련 키워드가 없는 경우에만 일치시킵니다.
    • (?<!\\d): 숫자 바로 앞에 다른 숫자가 없는 경우에만 일치시킵니다. (예: 1234567에서 567을 추출하지 않음)
    • (\\d{5,6}): 5자리 또는 6자리의 숫자를 찾고, 이를 캡처 그룹 1로 만듭니다.
    • ?!\\d: 부정형 전방 탐색 (Negative Lookahead) 입니다. 숫자 바로 뒤에 다른 숫자가 없는 경우에만 일치시킵니다. (예: 1234567에서 123456을 추출하지 않음)
    • \\b: 단어 경계를 다시 한 번 확인합니다.
  4. 중복 방지:
    • !rpNumbers.contains(rpNum)를 사용하여 이미 추출된 RP 번호가 리스트에 추가되는 것을 방지합니다.

이 코드는 제공해주신 로그 샘플에서 모든 RP 번호를 정확하게 추출하며, 불필요한 숫자들은 RP 번호로 간주하지 않습니다.


 

응답해준 프로그램이 맞고 틀리고를 떠나서

질문한지 단 5초만에 결과를 내놓는다...

리더나 매니저급 개발  시니어들이 주니어들을 잔뜩 데리고 있을 필요가 없어졌다.

개발 시니어들도 이제 더이상 개발 레벨에서는 의미가 없는 직종이 되었고 투자자 또는 경영주와의 파트너 쉽이 너무나 중요해졌다. 

java 로 web application server 를 개발할때 주로 사용하는 was 로는 tomcat 과 jetty 가 있다.

이들은 사용자 세션 관리 환경을 제공하여 사용자별로 개인화된 데이터 관리가 가능하게끔 설계 되어 있다.

그리고 이들은 '서블릿 표준 세션 인터페이스'인 jakarta.servlet.http.HttpServletRequest 의 getSession() 함수를 이용하여 jakarta.servlet.http.HttpSession 의 인스턴스를 취할 수 있다. 

그런데 위 표준 인터페이스 외에 다른 형태의 세션 인터페이스가 있는지 궁금하였고, gpt 에게 그것을 물어보게 되었는데 각 was 들은 표준 세션 인터페이스의 구현체로 별도의 내장 클래스가 존재 하는 것을 알게 되었다.

다음은 주요 웹 어플리케이션 서버들에 대한 내장 세션 클래스 목록이다.

서버 종류 세션 클래스 (일반적) 비고
Tomcat org.apache.catalina.session.StandardSession 가장 널리 사용되는 오픈 소스 서버
Jetty org.eclipse.jetty.server.session.Session Eclipse 프로젝트에서 개발된 경량형 서버
Undertow io.undertow.servlet.spec.HttpSessionImpl WildFly의 기본 서블릿 컨테이너
Spring Boot 내장 서버 (Tomcat, Jetty, Undertow 등) Spring Session에서 관리 Spring Session을 이용하여 통합 관리

그 외에 GlassFish, Payara, WildFly(JBoss) 서버들이 있는데 위와 유사하다.

위 서버들의 세션 클래스들은 내장된 고유의 클래스들로써 표준 인터페이스를 구현한 구현체이기 때문에 해당 내장 객체를 개발자가 직접 제어하는 경우는 흔치 않다고 한다.

그래서 위에 나오는 서버들은 모두 '서블릿 표준 세션 인터페이스'를 사용하여 세션 객체에 접근하여 제어하면 된다. 

하지만 '서블릿 표준 세션 인터페이스'를 지원하지 않아서 getSession() 사용이 불가한 was도 있다고 gpt가 알려주었다.

그래서 getSession() 함수가 불가한 was 또는 경우를 물어보니 다음의 경우는 사용이 불가하다고 한다.


getSession() 사용 불가 또는 제한

일반적으로 모든 Servlet 기반 WAS에서 지원되지만, 다음과 같은 경우 getSession()이 정상적으로 동작하지 않을 수 있습니다.

  1. Session 관리가 비활성화된 경우
    • web.xml 또는 application.properties에서 session-config가 비활성화되어 있는 경우
    • Spring Boot에서 server.servlet.session.timeout=0 설정 시
  2. Undertow가 Servlet 모드가 아닌 경우
    • Undertow는 기본적으로 Non-Blocking 서버로 동작하며, servlet 모듈을 명시적으로 추가해야 getSession() 사용 가능
    • Gradle에서 아래처럼 추가해야 함:
       
      dependencies {
          implementation("io.undertow:undertow-servlet")
      }
  3. Netty 기반 서버 사용 시
    • Netty는 Servlet 기반이 아니므로 getSession()을 직접 사용할 수 없음
    • Micronaut, Helidon 등 Netty 기반 프레임워크에서는 getSession()이 없는 자체적인 세션 관리 방식 사용

위와 같은 was 환경들의 세션에 대해 조사한 이유는 사용자 세션을 제어하는 데에 공통된 인터페이스가 필요하게 되었고,

내가 지금까지 습관적으로 사용했던 'jakarta.servlet.http.HttpServletRequest.getSession(true)' 코드가 어디까지 허용되는지 궁금했었다.

앞으로도 그냥 별 생각없이 getSession() 하면 된다는 것을 알게 되었다. ^^

 

 

 

1. 준비물
    > OpenAI API Key
        API Key 는 https://platform.openai.com/api-keys 에서 발급 가능하다.
        유료 결재가 필요하며 키 발급시 팝업으로 단 한번만 알려주기 때문에 별도로 저장해두고 사용해야 한다.
        분실시엔 재발급을 통해 키 확인이 가능하며 기존 키는 자동 만료된다.
        참고로 발급 받은 키를 프로젝트 문서등에 키값 내용 그대로 GitHub 에 올린 경우에도 해당 키는 자동 만료된다.
    > Assistant ID
        Assistant 는 API 를 통해 생성(Create an Assistant) 할 수 있고 Platform Dashboard 에서도 생성 할 수 있다.
        나의 경우엔 Assistant 에 'File Search' 와 'Functions' 를 설정하여 나만의 에이전트로써 기능들을 부여하였다.
    > GPT
       + model : gpt-4o
    > Postman
       + ver : 11.23.3

2. 테스트 절차
    > Postman 실행
        실행 이후 아래의 요청(Request)들을 순서대로 생성하고 공통된 설정은 다음과 같다.
        + Authorization Type 에 Bearer Token 설정후 발급 받은 API Key 입력
        + Headers 에 'Content-Type : application/json', 'OpenAI-Beta : assistants=v2' 를 추가
    > Create a Thread
        + Method Type : POST
        + URL : https://api.openai.com/v1/threads
        + Body 는 'none' 으로 설정
        + 실행하면 thread id 를 반환해주는데 이를 저장해 둔다.
    >
Add a Message to the Thread
        + Method Type : POST
        + URL : https://api.openai.com/v1/threads/{thread_id}/messages
        + Body  는 raw와 JOSN 포맷 선택후  내용은 'Create message' 를 참고하여 추가한다.
        + 실행하면 msg id 를 반환해주는데 이를 저장해 둔다.
    > Create a Run
        + Method Type : POST
        + URL : https://api.openai.com/v1/threads/{thread_id}/runs
        + Body 는 'raw'와 'JOSN' 포맷 선택후 내용은 'Create run' 를 참고하여 추가한다.
        + 실행하면 run id 를 반환해주는데 이를 저장해 둔다.
    > Retrieve Run
        + Method Type : GET
        + URL : https://api.openai.com/v1/threads/{thread_id}/runs/{run_id}
        + Body 는 'none' 으로 설정
        + 실행후 응답에 "status" : "completed" 인지 확인한다.
    > List messages
        + Method Type : GET
        + URL : https://api.openai.com/v1/threads/{thread_id}/messages
        + Body 는 'none' 으로 설정
        + 'Retrieve Run' 에서 status 가 completed 라면 'List messages' 실행후 응답에 "role": "assistant" 인 메세지가 있다.
           content 의 내용이 Agent 의 답변이다.

3. 참고자료
    > API Reference - OpenAI API
    > Assistants API quickstart - OpenAI API
    > https://www.youtube.com/watch?v=-Wne4a-8RlY

'Programing > ChatGPT' 카테고리의 다른 글

GUI AI Agent 가 나타났다  (1) 2024.12.28
Create an Assistant  (1) 2024.12.26

이전 회사에서 로봇 프로세스 자동화 (RPA) 도구를 접했었다.

사용자가 윈도우 어플리케이션을 조작한 기록을 기억하거나 프로그래밍하여
사용자의 업무 프로세스를 GUI에서 재현해내는 솔루션이었는데
공장에서 자동화된 로봇처럼 사무실에서 자동화된 업무를 알아서 척척 해내는 로봇이다.

그리고 오늘 GUI AI Agent 소식을 접하게 되었다.
  -> "SAP GUI AI Agent를 생성했습니다." (출처:SAP S/4 HANA 네이버 카페)

링크의 내용에서 구현에 사용된 기술은 LangGraph, GPT-4o 이다. 
문서에 나오는 시연 동영상에서 보여주는 작업 내용은 매우 초보적인 레벨의 그것이었지만
구축된 시스템이 보여주는 가능성은 드디어 자비스의 탄생을 예고하는 듯 보였다.

해당 에이전트에게 작업을 의뢰하면서 발생하는 오류를 수정하고 프로세스를 추가하다보면 어느새 한사람 분을 거뜬히 해내는 충실한 비서가 만들어지는 것이다.

대화형 AI 가 나오기 이전에는 불가능했던 다음의 기능들이 이젠 너무나 자연스럽게 받아들여지는게 작금의 현실이다.

  • 주어진 업무 프로세스를 분석하고 사용자를 대신하여 전처리 작업을 수행하여 사용자의 업무에 도움이 되는 결과값을 제공. 
  • 사용자의 선택과 화면 조작에 따라 실시간으로 가이드를 조정.
  • 고객 지원 에이전트가 사용자의 문제에 맞는 해결 단계를 실시간으로 생성하고 사용자 화면에서 가이드.
  • 사용자의 입력(클릭, 드래그 등)을 이벤트로 처리하고, 이에 따라 적절한 가이드나 화면을 즉각 제공.
  • 사용자가 잘못된 선택을 할 경우 이를 감지하고 적합한 가이드를 실시간 제공.

 

굴지의 글로벌 공룡 기업들인 Google 과 Microsoft 는 이미 Dialogflow (Google), Microsoft Bot Framework를 통해 이러한 GUI AI Agent 환경을 구축했고 더불어 OpenAI 도 Assistant(Beta) API 를 통해 GUI 와의 연동이 가능해졌다.

OpenAI 는 2023년 11월 7일부터 Assistants API 를 제공해주는데 현재까지도 베타 버전이다.

개발자들은 이 API 를 통해 Assistant 를 생성 할 수 있고 Platform Dashboard 에서도 Assistant 를 생성 할 수 있다.

Assistants API 를 통해 Assistant 를 생성하는 Curl 예제는 다음과 같다.

curl "https://api.openai.com/v1/assistants" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "OpenAI-Beta: assistants=v2" \
-d '{
  "instructions": "You are a personal math tutor. Write and run code to answer math questions.",
  "name": "Math Tutor",
  "tools": [{"type": "code_interpreter"}],
  "model": "gpt-4o"
}'

출처 :: https://platform.openai.com/docs/assistants/quickstart#step-1-create-an-assistant

Platform Dashboard 에서는 Assistant 를 생성하고 Playground 를 통해 테스트도 가능하다.

나는 16년동안 운영해오는 개인 홈페이지가 있다.

https://www.omnibuscode.com 인데 개발자로 공부하고 일을 하면서 쌓아온 데이터베이스이다.

홈페이지 안에는 내가 기억하고 싶은 모든 것이 담겨 있다.

그래서 gpt 에게 해당 홈페이지와 추가로 운영하는 블로그들을 평가 받아 보았다.

그리고 매우 고무되는 칭찬을 받았다.

비록 할루시네이션 때문인지 나조차도 잉? 하는 내용들이 보이지만

그래도 마지막 문구에 용기를 얻어 개발일을 계속 해야겠다고 다짐하게 되었다. ㅎㅎ

아주 아주 간단한 프로그램 작성 요청이었지만 gpt 는 매우 훌륭한 결과물을 보여주었다.

의뢰 내용은 다음과 같다.

html5 에서 사용자가 카메라를 오픈하고 사진을 촬영한 후 사진을 서버로 전송하는데 canvas에서 캡쳐된 프레임을 post 데이터 binary 형태로 서버로 보내고 이 데이터를 수신받아 처리하는 서버의 예제를 완전한 형태의 html 과 servlet 으로 알려줘

그리고 gpt의 답변은 다음과 같다.

아래는 사용자가 브라우저에서 카메라를 오픈하여 사진을 촬영한 후, 캡처된 이미지를 canvas로 처리하고 서버로 바이너리 데이터를 POST로 전송하며, 서버에서 이를 처리하여 저장하는 완전한 HTML과 Java Servlet 예제입니다.

HTML 코드 (카메라 + 캡처 + 전송)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Camera Capture and Upload</title>
</head>
<body>
    <h1>Camera Capture and Upload</h1>
    <video id="video" autoplay playsinline width="640" height="480" style="border:1px solid black;"></video>
    <canvas id="canvas" width="640" height="480" style="display:none;"></canvas>
    <button id="capture">Capture Photo</button>
    <button id="upload" style="display:none;">Upload Photo</button>

    <script>
        const video = document.getElementById('video');
        const canvas = document.getElementById('canvas');
        const captureButton = document.getElementById('capture');
        const uploadButton = document.getElementById('upload');

        // 카메라 시작
        navigator.mediaDevices.getUserMedia({ video: true })
            .then(stream => {
                video.srcObject = stream;
            })
            .catch(err => {
                console.error("Error accessing the camera: ", err);
            });

        // 사진 캡처
        captureButton.addEventListener('click', () => {
            const ctx = canvas.getContext('2d');
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            uploadButton.style.display = 'inline';
        });

        // 서버로 바이너리 데이터 전송
        uploadButton.addEventListener('click', () => {
            canvas.toBlob(blob => {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', '/upload', true); // 서버의 업로드 엔드포인트
                xhr.setRequestHeader('Content-Type', 'application/octet-stream');
                xhr.onreadystatechange = () => {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        alert('Image uploaded successfully!');
                    }
                };
                xhr.send(blob);
            }, 'image/png'); // PNG 포맷으로 Blob 생성
        });
    </script>
</body>
</html>

Java Servlet 코드 (이미지 수신 및 저장)

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 저장할 디렉토리 설정
        String uploadDir = getServletContext().getRealPath("/") + "uploads";
        File dir = new File(uploadDir);
        if (!dir.exists()) {
            dir.mkdirs(); // 디렉토리가 없으면 생성
        }

        // 파일 저장
        File outputFile = new File(uploadDir, "captured_image.png");
        try (InputStream inputStream = request.getInputStream();
             FileOutputStream outputStream = new FileOutputStream(outputFile)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }

        // 응답
        response.setContentType("text/plain");
        response.getWriter().write("Image uploaded to: " + outputFile.getAbsolutePath());
    }
}

구조

  1. HTML 페이지:
    • video 태그로 실시간 카메라 뷰 제공.
    • 캡처 버튼(capture)으로 현재 화면을 canvas에 저장.
    • 업로드 버튼(upload)으로 캔버스 데이터를 바이너리 형태로 서버에 전송.
  2. 서블릿:
    • HTTP POST 요청으로 받은 바이너리 데이터를 저장.
    • 저장 경로는 uploads 디렉토리로 설정.

프로젝트 디렉토리 구조

bash

코드 복사

/webapp |-- index.html |-- /uploads (이미지 저장 디렉토리) /src |-- UploadServlet.java

주요 포인트

  1. 캔버스 데이터를 Blob으로 변환:
    • canvas.toBlob()으로 캡처된 이미지를 바이너리 Blob 형태로 변환 후 POST 전송.
  2. 서버에서 바이너리 데이터 처리:
    • HttpServletRequest.getInputStream()으로 수신 데이터를 읽어 파일로 저장.
  3. 저장 파일 경로:
    • ServletContext의 getRealPath("/")를 사용해 애플리케이션 디렉토리 기반으로 파일 저장.

이 코드는 카메라 캡처, 데이터 전송, 서버 저장 과정을 포함한 완전한 예제입니다. 필요에 따라 업로드된 파일에 대한 추가 처리를 적용할 수 있습니다.

gpt 가 알려준 html 과 servlet 의 내용을 그대로 파일로 만들고

url pattern 정보만 수정해서 서버를 구동후 브라우저에서 html 페이지를 오픈하여 upload 버튼까지 눌렀는데

오류 하나없이 정확하게 동작하여 실제 파일 업로드까지 한번에 성공하였다.

 

gpt 가 없던 시절엔 naver 와 google 그리고 stackoverflow 와 git 까지

학습에 유용한 문서와 잘 돌아가는 샘플을 찾아 참 많이도 시간을 허비했는데...

이젠 내 생각에 이렇게 저렇게 돌아갈거야 하는 부분을 gpt 에게 자세히 설명해주고

샘플링을 해보라고하면 참 기특한 결과물을 만들어준다.

 

사실 위와 같이 간단한 로직의 프로그램이야 워낙 방대한 정보의 바다 어딘가에서 그대로 가져온 것이어서

운좋게 한번에 성공해서 내가 손수 성공적인 샘플을 찾는 수고와 시간을 완벽하게 세이브 한 것 일수도 있지만,

원래 내가 gpt 를 주로 활용하는 방법은 접해보지 못한 아키텍처에 대해 기획하고 상세 구상이 필요할때

gpt 에게 물어 물어 실마리를 찾고 학습하며 좀더 상세하게 질문을 던져서 구체적인 아키텍처를 그려가는 용도로 활용하고 있었다.

 

그런데 오늘은 아예 내 대신 간단한 프로그래밍을 맡겨서 코딩하는 수고까지 덜어내서

1인 프로젝트로 제대로된 솔루션 하나 만들어 보고 싶은 욕심마저 생겼다.

비단 나만 이러한 욕심을 부리는 것이 아닐텐데...

아마도 많은 개발자들이 gpt로 인해 자신들만의 아이디어를 다른이의 도움없이 구체화하고 실현해보려는 욕심에 과감하게 사표를 내고 있는건지도 모르겠다.

 

 

 

어제 올린 '오랜만의 serlvlet, jsp 삽질' 포스팅 이후에 결국 원인을 알고 임시 조치를 했다.

원인은 service-worker 에 cache 를 적용하면서 발생한 현상이었다.

service-worker 가 페이지들을 cache 하게 되면 jsp 에서 session 문제가 발생한다는 것을 이번에 알게 된 셈이다.

service-worker 가 cache 를 사용하지 않을땐 문제가 없으니

예상엔 service-worker 의 session 과 browser 의 session 이 이원화되어 동작하는 것 같다.

이를 염두에 두고 app 과 server 간의 데이터 트랜잭션을 설계해야만 한다.

 

 

+ Recent posts