# AI 가 사람보다(?) 뛰어난 작곡 실력을 보인 사례가 나왔다

얼마전 (2024.04) 한 교육박람회 주제가 공모전에서 12개의 곡 중 AI 가 작곡한 1개의 곡이 다른 응모작들을 제치고 1등에 당선된 것이 뒤늦게 밝혀진 사례가 뉴스에 나왔습니다.

그리고 며칠 전에는 회사 후배가 장난삼아 AI 로 곡을 만들며 동료들과 "놀이" 를 하는 모습을 보았는데 재미있는 가사를 만들고 AI 작곡 플랫폼을 활용해 간단히 만들어낸 음악이 마치 전문가가 작곡하고 유명 가수가 부른 것 같이 만들어져 나온것을 보고 놀랄 수 밖에 없었네요.

저도 한시간 정도 투자해서 이것저것 만져보니 어느정도는 제가 기대하던 것과 어느정도 유사한 분위기의 음악을 만들 수 있었고, 대략적인 방법을 초등학생 아들에게 간단히 알려주었는데 바로 다음날 잠깐 해보더니 만든 노래를 저에게 들려주더군요. 그만큼 쉽습니다 하지만 강력합니다.

 

# AI 음악 생성 플랫폼 들

AI 로 음악을 만들어주는 플랫폼 서비스들은 상당히 많습니다. AIVA, Soundraw, Musicfy, Beatoven, Stable Audio, ... 등등, 이러한 서비스는 주로 연주 위주의 음악을 생성해주는 AI 서비스 들이라고 보면 되겠습니다.

이번에 사용한 Suno (수노) 와 Udio 와 같은 서비스는 위의 음악 생성 플랫폼과 차별화된 기능으로 "Vocal" 을 생성해줍니다. 즉, 사람 목소리로 노래를 부르게 가수도 고용해준다는 것이죠.

Suno 와 Udio 모두 Vocal 을 포함하여 노래를 생성해주는데요, 

현재 Suno 와 Udio 모두

  • Multi Language 지원
  • 고퀄리티 음질
  • 다양한 장르의 스타일 지원 (재즈, 메탈, 팝, 블루스, 신스 ...)
  • 다양한 프롬프트 지원
  • 사용자 친화 UX

등을 홍보하고 있으며 실제로 사용해봐도 얼마전까지는 Suno 가 사용 편의성이나 부가기능들이 앞서지만 Udio 는 Vocal 의 음질과 Customize 가 더 좋다는 평이 있었으나 Suno 3.5 가 나오면서 그런 것인지 Suno 의 Vocal 품질도 정말 훌륭하다는 느낌이 듭니다.

 

# 두 플랫폼으로 이런게 만들어집니다 한번 들어보세요

아래 음악은 Suno 로 만들어본 샘플곡 입니다. 한번 들어보시죠.

https://suno.com/song/cafbd769-1bd6-4865-90aa-c84f1878174a

 

산산조각 난 마음 by @melodramaticquintet748 | Suno

k-pop acoustic texas blues K-Pop, Acoustic Texas Blues fusion, raw, rhythmic song. Listen and make your own with Suno.

suno.com

어떤가요? 보컬이 약간 Effector 를 쓴 것 같은 느낌이 드는 감도 있지만, 커버 이미지도 가사 내용을 참고해서 아주 잘 만들어주었고 음악도 자연스럽습니다.

 

같은 Prompt 와 가사를 Udio 에 넣어서 만들어진 노래입니다 (Suno 와 prompt 가 호환되지 않아 다른 스타일의 음악이 나온점은 양해하고 들어봐주세요)

https://www.udio.com/songs/4orcymaR47bY3UCp9S3xri

 

goodjoon - Lost Voices | Udio

Listen to Lost Voices by goodjoon on Udio. Discover, create, and share music with the world. Use the latest technology to create AI music in seconds.

www.udio.com

무료라 32초 제한과 350자 이내의 가사만 입력이 가능하지만 Vocal 이 Suno 보다 더 크고 명확한 점 등은 Udio 의 장점이라고 볼 수 있겠습니다. 그런데 그 외의 기능들은 제가 아직 Udio 를 잘 쓰지 못해서 그런가(만약 그렇다면 Suno 가 더 쓰기 쉽다고 봐야겠네요) Suno 만큼의 완성도가 나오지는 못한것 같습니다.

 

# 이제 만드는 과정을 봐보죠, 우선은 가사 부터! ChatGPT 를 활용합니다

실제 음악 프로듀싱은 작사부터다 작곡부터다 말들이 많은 주제이지만(프로듀서 스타일 마다 다르다는 결론을 내봅니다), AI 를 활용한 음악 제작에는 작사부터 하는 방법이 맞습니다 ^^. 생성 시 곡 구성과 함께 가사를 넣어줘야 AI 가 그 구성과 분위기에 좀더 적합한 멜로디를 만들어냅니다.

가사 생성은 현존 최고의 말빨 LLM 인 ChatGPT 를 활용하면 끝판왕 급 가사가 나옵니다. 아 그 전에 가사를 만들어달라고 할 때 곡의 구성 (Song Form 이라고 하죠) 을 ChatGPT 에게 알려주며 그 구성에 맞는 가사를 뽑아내라고 할 것인데요, 보통 대중음악의 Song Form 은 아래가 가장 말그대로 대중적인 구성입니다.

Intro - Verse1 - Pre-Chorus - Chorus - Interlude - Verse2 - Pre-Chorus - Chorus - Bridge - Outro

각 구성의 설명은 아래와 같죠

 

  • Intro (인트로): 곡의 시작 부분으로, 분위기를 설정하고 청중의 관심을 끌기 위한 역할을 합니다. 보통 짧고 간결하며, 종종 곡의 멜로디나 테마를 암시합니다.
  • Verse 1 (벌스1): 곡의 이야기를 전달하는 첫 번째 구절입니다. 가사는 보통 서사적이며, 청중에게 곡의 주제를 소개합니다.
  • Pre-Chorus (프리코러스): 구절과 후렴 사이의 연결 부분으로, 곡의 긴장감을 높이고 후렴으로의 전환을 부드럽게 만듭니다. 멜로디는 구절과 후렴과 다를 수 있으며, 독특한 요소를 추가합니다.
  • Chorus (코러스): 곡의 핵심 메시지를 담고 있으며, 가장 기억에 남는 멜로디와 가사를 포함합니다. 반복적으로 등장하며, 곡의 클라이맥스 역할을 합니다.
  • Interlude (인털루드): 짧은 중간 부분으로, 보통 연주만으로 구성됩니다. 곡의 분위기를 환기시키거나 다음 구절로 자연스럽게 넘어가도록 돕습니다.
  • Verse 2 (벌스2): 두 번째 구절로, 첫 번째 구절의 구조를 따르지만 가사는 다릅니다. 이야기를 계속 이어가며, 곡의 주제를 더욱 발전시킵니다.
  • Pre-Chorus (프리코러스): 첫 번째 프리코러스와 동일한 역할을 하며, 다시 한 번 긴장감을 높이고 후렴으로 전환합니다.
  • Chorus (코러스): 반복적으로 등장하며, 곡의 핵심 메시지를 재확인시킵니다.
  • Bridge (브릿지): 곡의 중간에 새로운 요소를 추가하는 부분입니다. 멜로디와 가사는 구절과 후렴과 다르며, 곡에 변화를 주고 청중의 관심을 유지시킵니다.
  • Outro (아웃트로): 곡의 마무리 부분으로, 곡을 끝맺는 역할을 합니다. 보통 인트로와 대조적이거나 비슷한 분위기를 가지며, 곡의 주제를 마무리합니다.

 

그런데 이렇게 완벽할 필요까지는 없고요, 저는 아래 처럼 Tailoring 하여 구성해보겠습니다

Intro - Verse1 - Chorus1 - Verse2 - Chorus2 - Bridge - Verse3 - Outro

 

자 이제 ChatGPT 에게 가사를 주문해보겠습니다

 

산산조각 난 내 마음

Intro

(음악만 연주, 가사 없음)

Verse 1

핸드폰을 보며 길을 걷다가
횡단보도를 뛰어가던 그 순간
무선 이어폰 속 세상에 묻혀
떨어진 핸드폰, 난 몰랐어

Chorus 1

횡단보도 다 건너고 나서야
떨어진 걸 알게 된 그 순간
다시 줍으려 하니 빨간불
움직이는 차들이 내 폰을 밟아

Verse 2

산산조각 난 내 핸드폰
차 바퀴 아래 부서진 그 모습
마음도 함께 부서져 내려
우울함이 가득한 이 거리

Chorus 2

횡단보도 다 건너고 나서야
떨어진 걸 알게 된 그 순간
다시 줍으려 하니 빨간불
움직이는 차들이 내 폰을 밟아

Bridge

그 순간 돌아갈 수 있다면
핸드폰을 놓치지 않았을 텐데
지금은 그저 슬픔뿐인 나
다시 일어설 용기를 가져야겠지

Verse 3

부서진 화면 속 추억들
이제는 다시 찾을 수 없지만
나의 마음도 다시 새롭게
다시 시작할 수 있기를

Outro

(음악만 연주, 가사 없음)

상황을 설명해주며 노래 가사를 만들어주라 했는데 거창한 가사를 만들어줍니다.

이제 가사가 준비 되었으니 Suno 를 시켜 노래를 만들어달라 할 차례지요.


# 간단히 Suno 에게 노래를 만들어달래자~! (기본 모드)

저는 Suno 를 사용하여 만들어 보겠습니다. 아무리 봐도 저는 Suno 가 Udio 보다는 훨씬 사용하기도 쉽고 자연스럽고 완성도 있는 결과물을 뽑아주는것 같은 느낌이라 이걸로 결정합니다.

 

Suno 의 좌측 메뉴에 Create 로 들어가보면, 입력하는 부분이 

"Song Description" 하나 밖에 안보입니다. 

 

 

 

 

 

위 모드는 기본 모드로, 저 Song Description 에 원하는 곡의 Style 과 곡에 대한 설명을 쓰면 가사와 제목, 심지어 표지까지 한번에 생성해줍니다. 한번 "Create" 를 누르면 2개의 곡을 만들어줍니다.

대단하지 않은가요? 저 짧은 Song Description 을 갖고 어쩜 저렇게 유창하게 가사도 쓰고 정말 Texas Blues 느낌의 곡을 한글로 만들어낼 수 있는지.. 아래는 기본모드로 생성해준 곡 중 한곡 입니다.

https://suno.com/song/e8608aa1-1232-4b8e-b063-68e3016033ec

 

산산조각난 핸드폰 by @melodramaticquintet748 | Suno

raw rhythmic k-pop acoustic texas blues song. Listen and make your own with Suno.

suno.com

 

# ChatGPT 의 가사로 곡을 Suno 에게 만들어달래자 (Custom 모드)

위 기본모드로 곡을 제작했다면 뭔가 아쉬움이 있을 수 있다. ChatGPT 가 생성해준 가사를 사용할 수 없다는게 가장 큰 문제입니다.

생성 모드를 Custom 으로 선택하면, Create 메뉴의 상세화면이 아래 처럼 바뀌고, 가사(Lyrics) 와 음악 스타일(Style of Music) 을 분리해서 넣을 수 있게 된다. 아래부분에 제목(Title) 도 넣을 수 있습니다.

Lyrics 에 ChatGPT 가 생성해준 가사를 붙여넣기 해줍니다.

아마 Prompt 에 음악 스타일을 어떻게 넣어야 할지가 가장 걱정일 듯 하다. 모든 Style Prompt 를 알아내는것은 어렵다 그나마 v2 의 경우는 https://www.suno.wiki/faq/style-and-lyrics/styles-and-genres/ 에 리스트가 있지만, 3.5 에서는 Style Prompt 가 이전 버전들과 동일하게 동작할지 보장할 수 없다고 하고있다고 합니다.

대신 Suno 는 Explore 라는 기능을 3.5 에 와서 새로 추가했습니다.

 방대한 양의 Style Prompt 조합을 갖는 Style 세트가 화면 우측에 보이며 돌아가고, 클릭해보면 (제목을 클릭)

해당 스타일세트를 추출해낸 곡의 상세가 표시되고, 유사한 Style 을 얻고싶으면 "reuse prompt" 를 눌러 나의 새로운 곡을 만들어낼 수 있습니다.

 

아래는 동일한 노래를 두가지 Style 로 생성해 보았어요.

https://suno.com/song/cafbd769-1bd6-4865-90aa-c84f1878174a

 

산산조각 난 마음 by @melodramaticquintet748 | Suno

k-pop acoustic texas blues K-Pop, Acoustic Texas Blues fusion, raw, rhythmic song. Listen and make your own with Suno.

suno.com

 

https://suno.com/song/7d76ee83-366f-4919-b507-8c389ea18faf

 

산산조각난 내폰 (Japanese Dancehall) by @melodramaticquintet748 | Suno

Dancehall, reggae, bass-driven, fast tempo, Jamaican song. Listen and make your own with Suno.

suno.com

 

# 마치며

생성형 AI 의 기술이 나날이 급속도로 발전해 나아가고 있다는게 느껴집니다. 이제 나만의 음악을 만들어 지인들에게 선물도 하고 앨범을 만들어 사람들에게 들려주는 일이 아무나 할 수 있는 일이 되어버린 듯 하네요.

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

ChatGPT 가 HTML 내에서 전체 댓글 수를 가져오는 코드를 만들어줬고, 나는 이를 JS 함수를 만들어 붙여넣기만 했다.

이번에는 실제로 현재 페이지의 전체 댓글 선택 버튼을 클릭하고, 변경메뉴 내의 "휴지통으로 이동" 버튼을 클릭하는 코드를 CoPilot 과 함께 작성해서 완성하려 한다.

 

# 코딩 동반자 CoPilot 

CoPilot 은 왜 이름을 이렇게 지었는지 사용할수록 이해가 된다. 묻고 대답하는 ChatGPT 와 같은 형태 보다는 IDE 의 AutoCompletion 기능을 적극 활용하여 내가 개발하고 있는 코드를 보고 있다가 '아.. 이런 코드 작성하려고 하는거지?' 하며 끊임없이 나의 코드를 자동완성 해주려고 노력해준다.

또는 코드를 개발 시작하기 전에 의사(pseudo)를 코드가 아닌 주석으로 입력해주면 그 의사에 맞는 코드를 뿅~ 하고 만들어주며, 반대로 코딩에 심취한 나머지 주석을 안달고 넘어간 코드에 주석도 달아준다.

CoPilot 도 채팅 기능이 추가되고 지금은 File 내용을 붙여넣지 않아도 파일 자체를 참조할 수 있도록 지정해줄 수도 있어 좀더 광범위한 질답이 가능하며, 출시 예정인 CoPilot Workspace 기능이 나오면 이제 Code Snippet 수준이 아니라 요구사항을 기반으로 Project 자체를 CoPilot 과 함께 개발이 가능해진다.

 

# 코드 개발 시작

아무 이름의 .js 파일을 Visual Studio Code 로 생성 했고, 파일을 열고 일단 ChatGPT 가 만들어준 코드를 내용으로 하는 함수를 위에 붙여 두었다. 그리고 댓글관리 페이지의 HTML 을 크롬 브라우져로 개발자 도구 로 부터 복사해 와 .html 파일로 저장했다.

이제 CoPilot 에게 내가 만들고 싶은 JS 코드를 // 주석으로 설명한다

// ChatGPT 가 만든, 현재 댓글 수를 가져오는 함수
function getCurCount() {
    // Select the h3 element containing the specific txt_count
    const titContElement = document.querySelector('.tit_cont');

    // Select the span element containing the number within the h3 element
    const txtCountElement = titContElement.querySelector('.txt_count');

    // Get the text content from the span element
    const txtCount = txtCountElement.textContent;

    // Remove commas from the string and convert it to a number
    return parseInt(txtCount.replace(/,/g, ''), 10);
}

// list.html 에서 id 가 "checkComments" 인 input 을 클릭하고 "휴지통으로 이동" 이름의 버튼을 누르는 함수
function delComments() {
    // id 가 checkComments 인 element 를 가져와
    var checkComments = document.getElementById("checkComments");
    checkComments.click();

    // type 은 button 이고 class 는 btn_g , value 는 "휴지통으로 이동" 인 input 을 가져옴
    var delButton = document.querySelector("input[type=button].btn_g[value='휴지통으로 이동']");
    delButton.click();
}

// 3초에 1번 씩 getCurCount() 가 100 보다 크면 delComments() 를 실행한다 
setInterval(() => {
    if (getCurCount() > 100) {
        delComments();
    }
}, 3000);

 

코드 에서 getCurCount() 부분은 ChatGPT 가 만들어준 코드이고, 그 아래 delComments() 부터는 // 주석을 이용해 내가 만들고 싶은 코드의 주석을 넣어준 것이다. 처음부터 완전히 깔끔한 코드를 만들어주는것은 아니지만 내가 일부 수정하거나 주석의 내용을 보완/변경 해 나아가면서 좀더 내가 원하는것과 일치한 코드를 만들게할 수 있다.

 

# 브라우져에서 실행 시키기

간단히 저정도의 코드를 만들어서 Chrome 의 개발자 도구 내 console 에 붙여넣고 실행하면 나를 대신하여 댓글 삭제를 진행해준다.

1. 브라우저 콘솔 하단에 위에 만든 코드를 Copy & Paste 해놓고 엔터를 친다!

 

2. 자동으로 화면에 Click 이벤트를 보내며 돌아가는 모습을 구경하고 있으면 된다

 

# 좀더 나은 방향으로..

위의 ChatGPT 와 CoPilot 이 만들어준 코드로 붙여놓고 실행하면 대략 1페이지(15 댓글)을 지우는데에 3~5초 가량의 시간이 소요된다.

1. "8,157 Page * 4초(평균잡아) = 약 9시간" 의 시간이 소요된다. 물론 자동으로 돌고 있을 것이라 그냥 기다려주면 되긴 하지만 왠지 좀더 빠르게 진행되었으면 하는 욕심이 든다..

2. 게다가 현재 방식은 댓글 중 저 요상한 이름의 공격자와는 관계 없는 댓글들 까지도 모두 삭제해버리는 피해를 감수해야 한다.

위 두가지 고민을 해소하기 위해 약간의 시간을 더 투자해 개선을 해보기로 한다! 어떻게~? "영혼의 단짝 코 파일러~ㅅ 과 함께" 

나의 계획은 이렇다

1. 댓글목록 조회는 Ajax 로 XHR GET 요청을 하고 가져온 목록을 화면에 Rendering 하는데, 이때 Query String 으로 page=? 형태로 하여 현재 페이지에 뿌려줄 15개의 항목을 가져온다. 대략 조회는 1~2 초 정도가 소요된다. --> 이 요청을 내가 직접 JS 로 가져오고 병렬로 요청하면 아마도 시간이 대폭 단출 될 것이다!

2. 휴지통으로 이동 버튼 클릭 시에도 Ajax 로 XHR DELETE 요청을 하는데, 이때 form-data 형태로 지울 댓글들의 ID 를 "," separate 하여 보낸다! 15개 기준으로는 2~5초 가량이 소요된다. --> 이 요청에 15개가 아닌 더 많은 수의 item 을 한번에 넣어 보낼 수 있다면? 굿굿~!

3. 1번 + 2번 모두 가능하다면 1 번에서 가져온 item 들 중에서 작성자와 IP 필드가 있을 것이고, 이걸 패턴 매치하여 나쁜놈만 걸러서 삭제할 수 있다! 굿굿굿~!

그래서 일단 1~3 번을 수동으로 테스트 해봤는데~~! 오예~ 모두 가능한 상황이다!

 

# 개선된 코드

CoPilot 과 함께 난 // 주석으로 일을 시키고~ CoPilot 이 열심히 만들고 주인님이 빠꾸 해 가면서 만들어낸 코드는 아래와 같다.
(짧은 시간에 VERSION 3 가 되어버렸는데, VERSION 2는 유사한 방식이나 작업을 fromPage ~ toPage 의 범위로 하게 하였으나 이러면 중간에 삭제 대상이 아닌 댓글들로 목록이 꽉 차게 되면 문제가 생길 수 있어 방식을 변경했다)

아래 코드에서 blockersNamePattern 과 blockersIps 의 내용을 바꾸어서 실행하면 된다.
blockersNamePattern 은 해당 문자열이 author 내용 내에 일부 매치만 되어도 true 가 되므로 잘 생각해서 한다.

실행은 Chrome 개발자도구 Console 에 모든 코드를 붙여넣고 실행한 후,
deleteCommentsForBatchSize(<한번에 delete 요청 보낼 댓글 수>) 를 하면 된다.

////////////////////////////////////////////////////////////
//--- VERSION 3 
// 이거 쓰자.. 브라우져 console 에 다 붙여놓고 deleteCommentsForBatchSize(500) 이런식으로 실행하면 됨   
////////////////////////////////////////////////////////////

let totalCount = 0;
blockersNamePattern = ["Mmz","MjPAz","-1","if(now", "@@"];
blockerIps = ["158.247.***.145"];

function containsBlockerNamePattern(author, ip) {
    for (let i = 0 ; i < blockersNamePattern.length ; i++) {
        if (author.includes(blockersNamePattern[i]))
            return true;
    }

    for (let i = 0 ; i < blockerIps.length ; i++) {
        if (ip.includes(blockerIps[i]))
            return true;
    }

    return false;
}

function collectAllCommentIdsUntilLimit(limit) {
    // Create an array to store all comment IDs
    const allCommentIds = [];
    const fetchPromises = [];
    let page = 1; // Initialize page number

    // Function to delay execution
    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

    // Function to fetch comments and process them
    const fetchAndProcessComments = () => 
        fetch(`https://goodjoon.tistory.com/manage/comments.json?filter=&searchType=writer&searchKeyword=&page=${page}`)
            .then(response => response.json())
            .then(data => {
                // Extract the comment IDs from the response and push them to the allCommentIds array
                const commentIds = data.items
                    .filter(item => containsBlockerNamePattern(item.author, item.ip))
                    .map(item => item.id);
                totalCount = data.totalCount;
                console.log("Page: " + page);
                console.log("commentIds to delete: " + commentIds.length);
                allCommentIds.push(...commentIds);

                // Check if the limit is reached or not
                if (allCommentIds.length < limit ) {
                    page++; // Increment page number for the next fetch
                    return delay(500).then(fetchAndProcessComments); // Continue fetching with delay
                }
            });

    // Start the fetching process
    fetchPromises.push(fetchAndProcessComments());

    // Return a promise that resolves when the limit is reached or all comments are fetched
    return Promise.all(fetchPromises).then(() => allCommentIds);
}

function deleteComments(ids) {
    // Create a FormData object
    const formData = new FormData();

    // Append the IDs to the FormData object
    formData.append('ids', ids.join(','));

    // Make a DELETE request to the comments API
    console.log(ids.length + " 개 삭제 요청 보냄... 기다려..");
    return fetch('<https://goodjoon.tistory.com/manage/comments.json>', {
        method: 'DELETE',
        body: formData
    })
        .then(response => {
            // response 를 출력
            console.log(response);
            return response.json();
        })
        .then(data => console.log(data))
        .then(() => console.log("삭제 완료!"));
}

let timeoutHandle = null;
let itemsPerPage = 15;

function deleteCommentsForBatchSize(batchSize) {
    // Collect all comment IDs from the specified pages
    collectAllCommentIdsUntilLimit(batchSize).then(ids => {
        // Delete the collected comment IDs
        console.log("totalCount: " + totalCount);
        if (ids.length >0 ) { // 총 댓글 수가 (pageTo - pageFrom) * itemsPerPage 보다 크거나 같으면
            console.log("삭제할 댓글 수: " + ids.length);
            deleteComments(ids).then( () =>
                timeoutHandle = setTimeout(() => deleteCommentsForBatchSize(batchSize), 1000)   // 1초 뒤에 deleteIdsOfPage(pageFrom, pageTo) 를 실행
            );
        } else {
            console.log("삭제할 댓글이 없음");
        }
    });
}

테스트 하다보니 제약사항이 있었다!

1. collectAllCommentIdsUntilLimit() 코드 내에 delay 를 위한 Promise 에 전달되는 timeout 값은 500ms 정도가 적당하다. 더 짧게 하다 보니 Tistory 서버가 앞단에서 500번대 에러를 내며 한동안 응답하지 않음을 발견했다

2. batchSize 는 delete 시에 큰 영향이 있는데, 한번에 1,000 개를 넣어보니 Tistory 의 Rev. Proxy 서버 쯤에서 너무 응답이 길어져 Response Timeout 을 낸다. 500 개 정도 부터 시작해봐도 될것 같다

이렇게 작성한 코드를 붙여넣고 위에 설명한대로 deleteCommentsForBatchSize() 를 실행시키면 몇시간 딴짓하고 돌아오면 12만개 쯤이야 삭제를 해놨을 것이다~~

 

# 마치며

이제 Tistory 는 Blog 시장에서 사라져버리는 수순을 기다리고 있는것인지 무언지 정말 실망스러운 운영과 시스템 유지보수가 아쉬움의 선을 넘어선 감정을 갖게 만든다.

무엇이 발전하고 있는지는 모르겠으나 플랫폼이 망가져가고 있는것은 확실히 느낀다.

 

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

지난 글에서는 뭐가 문제인지, 어떻게 할 구상인지에 대해 글을 써봤고, 이번에는 본격적으로 AI 에게 개발을 시켜볼 차례이다.

# ChatGPT (GPT-4o)

 ChatGPT 는 현재 상황을 인식시키고 답을 내놓으라고 할 때 참 착하게 동작해준다. GPT-4o 모델로 image 를 포함한 file 을 upload 하여 일을시킬 수 있어 좀더 여러가지 일을 시킬 수도 있어 좋다.(무료 플랜에서도 제한적으로 사용 가능하다)

 ChatGPT 에게 시킬 일 : ChatGPT 에게는 HTML 코드의 일부를 Image 로 캡춰해서 분석하게 한 후 총 남은 댓글의 개수를 가져오는 Javascript 코드를 만들어 내라고 시켰다. (아래 캡쳐의 122,364 라는 숫자를 가져오고 싶은 것이다)

무한정 일을 반복시킬 수는 없으므로 처음에는 남은 댓글 수가 0이 될 때 까지 돌려버리려 했다. (나중에는 정말 진심으로 감사드릴 댓글까지 지워지면 안되겠다 싶어 좀더 시간을 들여 수정 했지만..^^)

위 처럼 Chrome 의 개발자도구에 있는 HTML 코드 일부를 Image 로 캡춰해서 필요한 부분을 가져오는 JS 를 만들어달라고 하니 아래 처럼 코드를 생성해준다

나는 Javascript 를 Chrome 개발자도구의 console 에서 직접 실행시킬것이기 때문에 "DOMContentLoaded" 이벤트 Listener 를 구현할 필요는 없고, 그 안의 내용만 있으면 된다.

요즘은 Client 쪽 JS 를 개발할 일이 없기때문에 왠만한 ECMAScript 표준함수 들에 대해서는 다 까먹고 있었는데 궂이 찾아볼 필요 없이 잘 코드를 뽑아준다. HTML 에는 숫자에 자릿수 구분자 "," 가 들어가 있는데, 이걸 replace 해주어 int 로 변환해주는 코드도 친절하게 넣어준다.

 

# 좀더 일 주기

잘 해주었다 GPT-4, 그러나 그리 호락한 주인이 아니어서 일을 좀 더 하라고 지시하고 싶은 부분이 있다.

HTML 구조 상 ChatGPT 가 만들어준 코드 처럼 ".txt_count" 클래스로만 가져오면 혹시나 다른데에서도 저 클래스를 쓰는 경우가 생긴다면 잘못된 값을 가져올 수 있으니 (뭐 현재 페이지 코드를 보면 그렇지는 않았지만~), 좀더 querySelector() 의 query 를 specific 하게 쓰거나 다른 코드를 덧붙였으면 하는 주문을 해봤다

💬 const txtCountElement = document.querySelector('.txt_count');
      여기서 .txt_count 클래스는 다른곳에도 사용될 수 있으니 조금 변경해줘

🤖

다른 곳에서도 .txt_count 클래스가 사용될 수 있기 때문에, 특정 부모 요소 내에서 해당 요소를 선택하도록 수정하겠습니다. tit_cont 클래스를 가진 요소 내에서 .txt_count 클래스를 선택하도록 변경하겠습니다.

<!DOCTYPE html>
....
중략
...

    <script>
        // JavaScript to get the number and convert it
        document.addEventListener('DOMContentLoaded', (event) => {
            // Select the h3 element containing the specific txt_count
            const titContElement = document.querySelector('.tit_cont');

            // Select the span element containing the number within the h3 element
            const txtCountElement = titContElement.querySelector('.txt_count');

            // Get the text content from the span element
            const txtCount = txtCountElement.textContent;

            // Remove commas from the string and convert it to a number
            const number = parseInt(txtCount.replace(/,/g, ''), 10);

            // Log the number to the console
            console.log(number);
        });
    </script>
</body>
</html>

이 코드에서는 .tit_cont 클래스를 가진 부모 요소 내에서 .txt_count 클래스를 가진 자식 요소를 선택합니다. 이를 통해 해당 클래스가 다른 곳에 사용되더라도 정확한 요소를 선택할 수 있습니다.


그래서 일단 전체 댓글의 개수를 가져오는 function 하나는 아래와 같이 getCurCount() 라는 이름으로 만들었다

// ChatGPT 가 만든, 현재 댓글 수를 가져오는 함수
function getCurCount() {
    // Select the h3 element containing the specific txt_count
    const titContElement = document.querySelector('.tit_cont');

    // Select the span element containing the number within the h3 element
    const txtCountElement = titContElement.querySelector('.txt_count');

    // Get the text content from the span element
    const txtCount = txtCountElement.textContent;

    // Remove commas from the string and convert it to a number
    return parseInt(txtCount.replace(/,/g, ''), 10);
}

 

이렇게 ChatGPT 에게 HTML 의 Capture Image 를 주고 코드를 만들게 하고 수정까지 요청하여 간단하지만 코드를 하나 만들었으니, 다음에는 CoPilot 을 옆에 앉혀두고 나머지 기능들을 개발 해 나아가자 (다음글 계속)

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

# 댓글 테러 발견

 열정이 식었다기 보다는 다른 것들로 인해 방치되고 있던 나의 티스토리 블로그, 어쩌다 들어와보다 보니 게시물에 댓글들이 엄청 달려있다~! "옷? 사람들이 갑자기 내 블로그에 급 관심 상승할만하게 있었나!?!?!?"

 싶었으나.. 역시나 그럴리는 없고 왠놈이 내 블로그 댓글이 로그인 안한 사람들도 댓글 달 수 있다는 사실을 알고 티스토리를 해킹해보려 시도하다가 잘 안되었는지 (XSS, SQL Injection 을 하려던 흔적이 많이 보였다) 약 12만개 정도의 댓글을 도배 해놨다.

처음에는 원래 티스토리에 있는 기능인 "스팸차단" 기능을 사용하여 작성자와 IP 로 차단을 하면 자동으로 기존 그들이 작성한 댓글들도 삭제된다는 말을 믿고 기능을 실행 해봤다

그러나 이게 뭐지.. 화면에는 아무 반응이 없고 댓글들이 사라지지도 않는다. 왜그런지 크롬 개발자도구 (Option + Cmd + I) 눌러보니 그래도 대형 블로그 플랫폼인 Tistory 에서 서버 에러가 나고 있다

처음에는 임시적인 문제인가 싶었으나 사실 몇달 전에도 이 기능을 실행해봤으나 지금도 결과가 똑같았다

500 에러 나고있는 차단기능

위 처럼 차단 요청에 대해 500 에러가 나고있으며 아마도 오랜동안 쭈욱 저 기능은 안돌고 있는것 같다. 티스토리에 문의를 해봤으나 답변도 없고 답답함의 연속이라 티스토리를 원망하기 시작한다.

혹시나 하여 복수개가 아닌 개별로 선택하고 스팸차단을 실행해봤지만 결과는 아래와 동일하다

 

# 되는건 뭔가

 다행인지 불행인지 차단기능은 안되지만 댓글을 삭제하는 기능은 가능했다. 한땀 한땀 눌러서가 아니라 1 페이지에 15개의 댓글이 뜨는데 그 15개를 전체선택 해서 삭제하는 것(휴지통으로 보내기)은 가능했다.

1 페이지 내의 댓글 전체선택 (15개) 하는 경우,

- 총 122,363 개 댓글이 있으니 8,157 개의 페이지가 있단 얘기이고
- 전체선택 체크박스 클릭 + 변경 버튼 클릭 + 휴지통으로 이동 버튼 클릭 = 3 클릭
- 8,157 x 3 click = 24,471

즉, 24,471번의 클릭을 하면 지울 수 있는 일이다~! 그런데..

- 클릭 1회 당 1초
- 삭제 후 로딩 2초
- 로 계산하면.. 24,471 초 + 8,157페이지 * 2초 로딩 = 40,785 초 = 679.75분 = 11.32 시간 = 약 11시간 20분

즉, 11시간 20분 동안 쉬지도 못하고 화장실도 못가고 클릭을 하고 있어야 한다는 결론이 나왔고, 혹시나 병원비가 나올지도 모르겠다는 생각이 들어서 기계를 시키기로 마음을 먹었다.

 

# 어떻게 할건가

일단 POSTMAN 을 켰고, 티스토리 -> 카카오의 OAuth2 인증을 거쳐 Token 을 받고 인증을 하고 들어가야 하는데.. 이걸 세팅하고 테스트 하고 개발해서 돌리는 시간도 한 11시간은 걸릴것 같다는 판단이 들었고, 최장 2시간 이내에 못끝낼거면 하지 말자는 각오로 20분 정도 진행 하다가 이건 아니다 싶어 방향을 전환했다.

Chrome 의 개발자도구로 돌아가서 페이지를 분석해보니 아래 처럼 현재 페이지의 전체 댓글을 선택하는 체크박스와 변경버튼 눌렀을 때 펼쳐지는 메뉴에서 휴지통으로 이동 버튼의 위치를 찾을 수 있었다.

이제 나의 계획은 이미 OAuth2 인증 다 타고 들어간 저 Session 에서 개발자도구 Console 에서 DOM 으로 올라온 페이지를 보고 Javascript 를 실행시켜 나 대신 클릭을 시키는 것~! (이건 1차 계획이었고, 너무 느려서 방법을 바꿨다.. 이건 추가설명 예정)

자 이제 구상을 했으니 AI 들에게 일감을 던져볼 차례이다 (다음 글에서..)

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,
소프트웨어 개발을 업으로 삼고 오랜동안 일하며 느끼는 것 중의 하나는 빠른 IDE 의 발전속도이다.

처음으로 IDE 라는 개념을 접하게 된 것은 Borland 사의 Turbo-C IDE 였으며 이후 Vim, MS 의 Visual Studio, Eclipse, IntelliJ 와 VSCode 등의 IDE 도구들을 써왔으며, 그 도구들의 기능 중 가장 큰 발전이 있었다고 느끼는 부분은 바로 Code Assist 기능이다.

 
대표적으로 Visual Studio 의 Intelli Sense, JetBrains 의 IDE (IntelliJ IDEA, PyCharm, WebStorm 같은) 의 Code Completion, Eclipse 의 Code Assist, Sublime 같은 Text Editor 의 Auto-Completion 기능들이 이러한 Code Assist 기능이며, 요즘은 이러한 Code Assist 기능 없이는 개발 생산성이 떨어지는것은 물론이고 개발이 불가하다고 느끼는 개발자들도 많을 것이라 생각한다.
 
 
OOP 언어라면 보통 Object 에 "." 을 찍으면 IDE 에서 자동으로 해당 Object 의 Class 및 현재 Code 의 Context 를 분석하여 사용 가능한 Method 및 Attribute 등을 위 화면처럼 Popup 으로 알려준다. 그리고 이렇게 추천된 목록에서 Enter 나 Tab 을 누르는것 만으로 오타 없는 정확한 Keyword, Method, Attribute Name 이 입력된다.
 
만약 이런 Code Assist 기능이 없이 Notepad 에서 수백개의 Class, 수천개의 Method 와 수만 라인의 코드를 개발해 나아가야 한다고 생각하면 정말 Hell 일 것이다.
 
아래는 Code Completion 기능의 예시 이다. 변수, 코드의 일부만 입력 하더라도 현재 Context 에서 입력 가능한 Attribute, Method 나 Keyword 등의 추천이 실시간으로 표시 되며 이 또한 Enter 나 Tab 을 누르는것 만으로 오타 없고 Context 에 맞는 오류 없는(물론 컴파일 오류이지 런타임 오류가 없다는건 아니다) 코드를 입력할 수 있다. 이 얼마나 생산적이고 개발자의 시간과 노력을 아껴주는 일인가!?
 
 
 
요즘 이러한 Code Assist 기능은 기본 기능만으로도 완벽하다고 느꼈었지만 여기에서 만족하지 못한 업체나 개인들이 IDE 의 Plugin 개발을 통해 특정 Framework 을 사용 시 더 생산성있게 코딩할 수 있도록 하거나 심지어 Variable 이나 Attribute, JPA Repository Method 의 Naming 을 자동으로 Assist 해주는 기능, Design Pattern 을 코딩에 쉽게 적용할 수 있는 플러그인 등을 개발하여 배포하고 있으며, 개발자는 점점 이러한 Code Asssist 기능에 의존해 가는 추세이다.
 
 

◼︎ AI Code Assist 가 나왔다!

이러한 Code Assist 기능들의 눈부신 발전에 '이제 더 나올만한 Assist 기능이 있을까?' 하고있던 중 또 지금으로서는 "끝판왕" 이라고 할만한 Assist 기능이 나왔다. 바로 AI Code Assist 이다.
 
기존의 Code Assist 기능이 현재 Code 의 Context 와 전체 프로젝트 내 소스코드 및 리소스의 Indexing 을 통해 사용 가능한 Method 나 Rule 기반의 Code Snippet 을 추천해 주었다면, AI Code Assist 기능(주로 IDE 의 Plugin 형태)은 이러한 보조 외에 현재 코드의 Context 를 이해하고 "앞으로 개발 될 것 같은 코드를 미리 예측" 하여 "코드 조각 또는 장문의 코드를 생성하여 추천" 해준다.
 
위 코드의 경우 나는 mongoTemplate 까지만 쳤는데 나머지 코드들은 AI Code Assist 기능 (Github Copilot) 이 알아서 추천해준 경우이다. Method 의 Name 이나 변수명, 내 프로젝트의 코드들의 패턴, Gihub 의 무수한 코드들을 학습한 AI 가 '응 너 지금 이런 코드를 만들려고 하는거지?' 하며 코드를 추천해주는 것이다. 그런데 이러한 코드는 정말 말도 안되게 내가 하려던 의도의 코드인 경우가 많다.
 
만약 내가 개발하려는 의도의 코드가 아니더라도 내가 생각한 코드를 만들어 가는 도중에 추천 코드는 계속 변경되고 다시 추천된다. 나도 결정하거나 생각하지 못한 부분의 코드까지도 미리 완성하여 실시간으로 추천해주는 기능을 겪어보면 "와.. 이제 개발자 필요없네" 라는 말이 절로 나온다.
 
이러한 코드 추천은 나의 프로젝트와 현재 Code 의 Context , Github 의 코드 학습을 통해서 자동 생성되는것 뿐만이 아니다. 아래 예시를 보자.
 
위 코드에서 나는 // 이후의 주석만 한줄 만들었다. 주석을 심지어 한글로 썼다. 그리고 Enter 를 치자 그 아래로 저런 코드가 만들어진다.
난 처음 이런 기능이 실행 되었을 때 '미쳤다..' 라는 말이 나왔다.
 
반대로 코드를 분석하여 주석을 (그것도 한글로!) 달아주는 기능도 된다
위 코드에서 나는 // account 까지만 입력했다. 그런데 그 뒤 문장인 "컬렉션의 변경사항을 감지하여 로그를 출력한다." 라는 주석을 알아서 생성해준다!
 
정말 감탄스러운 기능이다. 주석을 코드로 만들어주고 코드를 해석하여 주석을 만들어 주며, 내가 개발중인 코드의 의도를 파악하여 코드의 남은 부분을 완성해주는 AI Code Assist 는 정말 충격적이다.
 
 

◼︎ 얼마나 효과적일까?

 
위의 간단한 몇가지 예시 만으로도 그 효과는 굳이 말로 설명하지 않아도 알 수 있을것이라 생각한다. 코드 개발의 진행을 분석하여 의도를 추론하고 깔끔한 형태의 코드로 귀신 같이 내가 만들려고 하던 나머지 부분을 미리 작성하여 추천해주는 기능은 최소 0.5 명 이상의 도움을 나에게 주어 나의 개발 생산성을 높여준다.
 
주석을 미리 작성하고 그 주석에 맞는 코드를 개발해 나아간다면 직접 코딩하는 분량은 매우 적어지고 나머지는 AI 가 코딩 해주도록 가능하므로 요구사항과 설계에 맞는 코드를 작성하는데 도움을 주어 코드의 품질을 높일 수 있다.
 
작성된 코드를 해석하여 주석을 생성해주는 기능을 활용하면 저렴한 비용과 시간을 투자해 코드의 가독성과 유지보수성을 높일 수 있다.
 
이러한 효과 외에도 다양한 오픈소스를 학습한 AI 가 추천해주는 코드로 개발자의 기술 도메인이 약한 부분의 코드를 작성할 때 Learning Curve 를 상당히 단축시킬 수 있다.
 
믿을 수 없겠지만 위의 코드에서 내가 한 것이라고는 파일 이름이 고차함수_테스트.kt 라는 Kotlin 파일을 만들고 "Int 를 받아서 .." 정도 까지를 써주고 shifter 라는 변수를 선언하는 정도의 타이핑만 했을 뿐 나머지는 대부분 추천하는 Code Completion 을 "선택" 한 것 뿐이다.
 

◼︎ Github Copilot

현재 가장 많은 개발자들이 사용하는 AI Code Assist Plugin 은 Github Copilot 이다.
ChatGPT 가 올해 2월 경 부터 급격히 인기를 얻으면서 GPT-3 모델을 활용한 Code 와 주석 Generate 를 해주는 Copilot 이 더욱 인기를 얻고 있는것 같다.
 
이후 글 부터 틈나는대로 개발자를 위한 Copilot 기능 활용 예시와 기회가 된다면 다른 Plugin 들도 사용해가며 글을 이어나아가 보겠다.
또한 개발자를 위한 나른 AI Tool 들이 있다면 사용해보고 글을 올려볼 생각이다.
 
 
 
 
반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

두서 없이 말하는 사람, 일 처리가 매끄럽지 않은 사람, 같은 시간 공부를 해도 머릿속에 남는게 적은 사람, 집중력이 떨어져서 욕먹는 사람 들 대부분의 공통점 중 하나가 주변이 어수선 하다는 것이다.

정리를 하면 좋은것은 안다 단순히 기분이 상쾌해지기 때문일까? 그러한 긍정적인 효과도 있겠지만 더 중요한것들을 이 정리정돈 이라는 행동의 결과로 얻을 수 있다.

당장 내가 자주가는 온라인 서점 사이트에서 “정리” 라는 키워드로 “자기계발” 분야에서 검색되어 나오는 책의 권수만 해도 2023.07.10 기준으로 238 권이나 된다.

시중에 자기계발서에 이렇게 많은 “정리” 와 관련한 책들이 있는 이유가 무엇일까? 이유는 정리는 자기계발에 매우 큰 영향을 끼치며 정리를 하여 무엇인가를 얻기 위한 목적이 있기 때문이다. 단순히 상쾌함 만을 느끼기 위한 기분전환 효과나 정리 자체로 느낄 수 있는 성취감 이상의 것이 있다.

 

정리의 목적

정리라는 단어는 “주변 정리” Arrangement, “문서 정리”, “노트 정리” 등과 같은 Organization, “창고 정리” Clearance, “사업 정리” Liquidation, “채무 정리” Settlement 등과 같이 여러 의미로 쓰인다.

‘우리나라 말은 왜이리 단순해?’ 라고 느낄 수도 있으나 사실 다른 영어단어들 속에서도 공통점이 있다. 바로 “깨끗한 상태로 만든다” 라는 의미를 포함하고 있다는 것이다.

깨끗한 상태로 만드는것이 왜 중요할까? 깨끗한 상태로 만드는 것은 “효율성” 과 관계가 있다. 본질적이지 않은 것들을 치워 깨끗한 상태로 만들면 우리는 본질에 집중할 수 있기 때문이며, 이때 다른것에 에너지를 낭비하지 않기 때문에 같은 에너지로 정리되지 않은 속에 있는 사람들 보다 더 효율적으로 핵심 가치를 얻어낼 수 있기 때문이다.

공부하려고 할 때 주변이 정리되어 있으면 공부하는 것에만 집중할 수 있게 되어 효율적으로 공부가 가능하고, 문서정리가 잘 되어있으면 어떤 문서 내용을 찾으려 하거나 빠르게 내용의 핵심을 파악해야 할 때 관련 없는 문서나 주변 내용을 보느라 시간을 허비하지 않아도 된다.

노트정리 리뷰를 통해 핵심 내용을 중심으로 뇌에 강한 연상력을 불러일으켜 망각곡선을 늦추어 공부한 내용을 오랜 동안 기억할 수 있게도 할 수 있으며, 수익성이 낮은 사업정리를 통해 핵심 사업에 더 집중할 수 있도록 한다.

이렇게 정리라는 행동한정된 노력으로 최대한의 효과를 내기 위한 목적을 이루기 위한 과정의 수단이며, 집중하기 어려운 상황에서 주변을 깨끗이 정리하여 본질을 정확히 대면하기 위한 강력한 도구이다.

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

 Stable Diffusion 이 무엇인지는 대부분 알기 때문에 이 글을 보고 계신다고 생각하니, 일단 Stable Diffusion 에 대한 소개는 건너뛰고 가보겠습니다. 이 부분은 나중에 아주 자세히 글을 한번 올려보겠습니다.

 

Colab 은 무엇인가

 Google Colab은 구글이 제공하는 클라우드 기반의 무료 Jupyter Notebook 환경입니다. 주로 파이썬 라이브러리를 사용하여 데이터 분석 및 머신러닝 모델링 등 다양한 작업을 할 수 있습니다.

사용자가 자신의 브라우저에서 코드를 실행하고 실행 결과를 확인할 있으며 사용자는 Google Drive에 데이터를 업로드하고, 해당 데이터를 Colab에서 로드하여 분석에 활용할 수 있습니다. 또한, Colab은 GPU나 TPU와 같은 하드웨어 가속기를 제공하여, 대용량 데이터셋의 학습을 더 빠르게 처리할 수 있습니다.

또한 Colab 의 Notebook 은 마크다운과 코드 셀 등 다양한 셀 유형을 지원하며, 코드 셀에서는 파이썬 코드를 실행할 수 있고, 다양한 라이브러리와 모듈이 많이 설치되어있습니다.

중요한것은 이러한 환경이 무료로 제공되며, 구글 계정으로 로그인만 하면 바로 사용할 수 있습니다. 이래서 Google Colab 은 저처럼 가난한 GPU 서버 없는 사용자가 AI Model 을 테스트 하거나 하기에 Google신이 내려준 축복같은 선물이라고 보면 되겠습니다.

 

어떻게 Colab 에 설치해야 하나

 네 Colab 에 "설치" 한다는 개념은 사실 딱 맞는 개념은 아니고요, Colab 에 Jupyter 노트북을 만들어서 그 노트북에서 Shell Script 및 Python Script를 실행시켜 필요한 File 들을 VM 에 설치하게 하고 (VM 은 Command Shell 로 접근이 안됩니다) Python 으로 개발된 Web UI 가 실행되어 Browser 로 Web UI 서버를 접근하게 하는 형태입니다.

 일단은 아래 URL 로 접근해봅니다.

https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Online-Services

Web UI 를 개발한 AUTOMATIC1111 이라는 개발자의 Github 내의 Wiki 인데, 여기에 보면 Google Colab, Paperspace, SageMaker 용 Jupyter 노트북용 스크립트들의 Link 가 있습니다

이 중 밑줄의 camenduru 라는 사람의 노트북을 써보기로 하겠습니다. camenduru 의 Github README 아랫쪽으로 스크롤 하다보면 아래와 같은 Link 들이 있습니다.

여러 Model 들이 preset 된 Jupyter script 들이 있는데요,

이중 저걸 설치해보겠습니다. 

링크를 클릭하면 CIVITAI (AI 생성 모델 과 모델로 부터 생성한 이미지들을 공유하는 사이트) 나 Huggin Face 커뮤니티 사이트(딥러닝 모델 및 데이터셋 공유 커뮤니티)로 연결 되니 대략 어떤 모델인지를 확인해볼 수 있습니다.

대략 확인했으면, 파란색 Link 가 아닌 좌측의 "stable" 이라고 되어있는 이미지 버튼을 누릅니다.

그러면, 실제 Colab 용 Jupyter 노트북으로 이동합니다. 저 노트북을 공유한 사람의 노트북인것이죠.

그리고 저 박스 안의 "Drive로 복사" 버튼을 눌러 나의 Google Drive 로 복사를 해줍니다. 

그러면 이제 복사가 완료되었을 것이고 바로 나의 Colab 으로 오게 됩니다.

 

이제 이 노트북을 실행시킬 VM 을 연결해주는 작업을 합니다. 아래 처럼 화면 우측 상단의 "연결" 을 눌러봅니다.

아래 처럼 할당된 리소스가 나오거나, 만약 연결한적이 없다면 런타임 유형을 선택하라고 나오는데 여기에서 "GPU" 를 선택하시면 됩니다.

만약 런타임 유형 선택 화면이 안나오면, 화면 하단의 "런타임 유형 변경" 을 누르시면 됩니다.

이제 Script 의 좌측 Play 버튼을 눌러서 실행시키면 됩니다

 

설치 후 첫 실행

 좀전에 실행시킨 스크립트창 하단에 보면 아래와 같은데 보일겁니다.

저 링크들이 Colab 에서 실행된 Web UI 서버로 접근할 수 있는 URL 입니다. 이중 클릭 해서 접속 되는 URL 로 들어가보시면 됩니다

접속이 잘 되었다면, 

prompt 란에 대략 내가 생성하고 싶은 이미지의 키워드들을 떠올려 넣어보시고, 필요하다면 바로 아래의 Nagative Prompt 에도 포함되면 안되는 요소의 키워드들을 넣어봅니다. 

그리고 저 빨간색 Generate 버튼을 누르면 시간이 흘러 image 가 생성 되고,  

하단의 Save 버튼을 누르면, 파일 이름이 그 밑의 목록에 추가 되고 "Download" 버튼을 통해 해당 이미지를 다운로드 받을 수 있습니다.

만약 Batch 로 생성(다음에 더 설명 예정입니다)하거나 하여 여러 이미지가 나왔다면 "Zip" 을 선택하여 모든 이미지들을 Zip 으로 묶어 다운로드 받을 수도 있습니다.

 

결론

 집에 사양 좋은 Windows Notebook 이 있어서 (NVIDIA 칩 GPU 있는) Local 에서 설치해서 사용하려 했으나 역시나 요구사양대로 4GB 이상의 VRAM 이 아닌 1GB 의 VRAM 노트북이라 실행이 불가했기에 Colab 에서 설치하고 실행하는 방법만 우선 올려봅니다.

 앞으로 여러 모델을 활용하고 다양한 파라미터들을 조작해서 내가 원하는 이미지들에 최대한 근접한 이미지를 만들어가는 방법도 올려보겠습니다. 

 또 다른 기술들과 접목을 하거나 유튜버들이 이야기하는 돈벌기 부업이 정말 되는지도 검증해보고, 다른 Generative AI 서비스나 도구들도 사용해보면서 다양한 컨텐츠를 만들어볼까 생각합니다. 응원 플리즈~

... 

그나저나 camenduru 의 README 를 보다보니 좀 암울한 얘기가 있네요.. 조만간 Stable Diffusion Web UI 사용이 Colab 에서 불가능할수도 있겠네요. Colab 팀이 Stable Diffusion Web UI 를 사람들이 너무 많이 돌려서 GPU 가 터질것 같은데 팀에 예산이 없어.. 조만간 뭔가 조치가 취해질 수 있다는 얘기네요.. 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

Colab 으로 띄우는게 너무 시간도 오래 걸리고 이것저것 만져보고싶은 생각에 Windows PC 에 설치를 하려 했으나 실패했다.

설치는 매우 간단하지만, 설치 후 SD(Stable Diffusion) 실행 시 VRAM 에 Model 을 올리는것 같은데 VRAM 이 1GB 인 내 노트북이 역시나 실행 최소환경 요건에 맞지 않았다.ㅠㅠ

OutOfMemoryError.. 이럴수가.. OTL

어쩔 수 없이 Colab 으로 가야하는가보다

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

 Spring Security 에 OAuth 2 모듈이 있는걸 알고 있지만 OAuth 로 SSO 서버를 구현해보지는 않았기에 chatGPT 에게 물어봤다


Q > Springboot 의 OAuth 를 활용해 SSO 구현 가능하니?

A > ChatGPT 는 당근 가능하다고 하며 TMI 로 아래처럼 얘기를 붙여준다

 

OAuth 정의 

OAuth는 사용자 자격 증명을 공개하지 않고 보호된 서버에서 사용자 자원에 대한 제3자 응용 프로그램의 액세스를 허용하는 인증 프레임 워크입니다. 최신 웹 애플리케이션 및 API에서 인증 및 권한 부여에 자주 사용됩니다.

Spring Boot는 웹 애플리케이션 및 API를 구축하기위한 인기있는 Java 기반 프레임 워크로서 OAuth를 지원하기 위한 다양한 도구 및 라이브러리를 제공합니다.

Spring Boot를 사용하여 OAuth를 이용한 SSO를 구현하려면 Spring Security 프레임 워크를 사용할 수 있습니다. Spring Security는 OAuth 인증 및 권한 부여를 지원하는 내장 기능을 제공합니다. Spring Security를 구성하여 Google이나 Facebook과 같은 OAuth 공급자를 신원 공급자로 사용하도록 설정할 수 있습니다.

다음은 Spring Boot를 사용하여 OAuth를 이용한 SSO를 구현하는 일반적인 단계입니다.

  1. 새로운 Spring Boot 애플리케이션을 만들거나 기존 애플리케이션을 수정하여 Spring Security를 사용합니다.
  2. Google이나 Facebook과 같은 OAuth 공급자를 선택하고 클라이언트 ID 및 클라이언트 비밀번호를 얻기 위해 공급자에 애플리케이션을 등록합니다.
  3. Spring Security를 구성하여 OAuth 공급자를 신원 공급자로 사용하도록 설정합니다.
  4. OAuth 인증 흐름을 처리하는 필요한 컨트롤러 엔드 포인트 및 사용자 인터페이스를 구현합니다.
  5. 사용자가 인증되면 세션 또는 토큰에 사용자 자격 증명을 저장하여 여러 애플리케이션에서 SSO를 활성화합니다.

Spring Boot를 사용하여 OAuth를 이용한 SSO를 구현하는 방법에 대한 자습서, 가이드 및 샘플 코드를 포함한 많은 온라인 자료가 있습니다. Spring Security 문서는 OAuth 인증 및 권한 부여를 구성하는 방법에 대한 자세한 정보를 제공하는 좋은 출발점입니다.


추가질문

이번에도 나의 질문이 조금 부족했음을 느낀다 나는 SSO Server 를 구현하는 방법을 물어보고 싶었는데, SSO 구현방법을 물어봤다.

다시 수정해서 물어보고 Springboot 기반의 Sample Code 도 달라고 요청했다

 

Q >  역시나 친절하게도 아래처럼 대답해준다 

 

First, you would need to add the necessary dependencies to your pom.xml file:

<dependencies>
    <!-- Spring Security OAuth2 dependencies -->
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.4.0</version>
    </dependency>
    
    <!-- Spring Boot dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.6.2</version>
    </dependency>
</dependencies>

Next, you would need to configure the OAuth2 server in your Spring Boot application. Here is an example configuration class:

@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("client1")
            .secret("secret1")
            .authorizedGrantTypes("authorization_code")
            .scopes("read")
            .redirectUris("http://localhost:8081/login/oauth2/code/client1")
            .and()
            .withClient("client2")
            .secret("secret2")
            .authorizedGrantTypes("authorization_code")
            .scopes("read")
            .redirectUris("http://localhost:8082/login/oauth2/code/client2");
    }
 
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
 
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()");
    }
}

In this example configuration class, we are configuring two clients ("client1" and "client2") with different credentials and redirect URIs. We are also specifying that the clients are authorized to use the "authorization_code" grant type with the "read" scope. Finally, we are specifying that the AuthenticationManager should be used to authenticate clients and users.

You would also need to configure your Spring Boot application to use Spring Security for authentication. Here is an example WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/oauth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user1")
            .password("{noop}password1")
            .roles("USER")
            .and()
            .withUser("user2")
            .password("{noop}password2")
            .roles("USER");
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

In this example WebSecurityConfigurerAdapter, we are configuring Spring Security to permit all requests to the "/oauth" endpoint and to require authentication for all other requests. We are also configuring an in-memory UserDetailsService with two users and their passwords,

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

오늘 열심히 새로운 프로젝트를 위한 분석설계 작업을 하던 중 옆 동료들과 잠시 머리도 식힐겸 이야기를 하다가 연동 시스템에서 HTTP 요청 시 Spring Boot 기반의 Multi Instance 서버들이 해당 요청을 Broadcast 받은 것 처럼 각 Instance 의 State 들을 업데이트 해야 하는 요건이 생겨 논의하고 있다는걸 알게 되었다.

머리 식히러 왔어서 자리를 피하고 싶었으나.. 최소한의 Effort 로 구현하고 싶고, Message Broker 나 Redis 같은 IMDB, Zookeper 등을 사용하지 않았으면 좋겠다 하고, NoSQL DB 로 MongoDB 를 사용하고 있으나 DB Polling 으로 인해 부하를 주고싶지 않다고 하고..

잠시 생각하던 중 MongoDB 에서 Collection 의 변경이 일어나면 이 이벤트를 Listen 할 수 있는 기능이 생각나서 해당 기능을 사용해서 구현해보는게 어떤가 이야기 했다.

말이 나온김에 ChatGPT 를 통해 Sample Code 를 만들어 보라 시켜봤다.

GPT write a pub sub example springboot java code using mongodb


ChatGPT 놈의 조언

Here is an example of a basic implementation of the publish-subscribe pattern in a Spring Boot application using MongoDB as the message broker:

@Service
public class PublisherService {
    private final MongoTemplate mongoTemplate;

    @Autowired
    public PublisherService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public void publishMessage(String message) {
        mongoTemplate.save(new Message(message));
    }
}

@Document
class Message {
    private String message;

    public Message(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Service
public class SubscriberService {
    private final MongoTemplate mongoTemplate;

    @Autowired
    public SubscriberService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @PostConstruct
    public void subscribe() {
        mongoTemplate.getDb().getCollection("message").watch().forEach((Consumer<ChangeStreamDocument<Document>>) changeStreamDocument -> {
            Message message = mongoTemplate.getConverter().read(Message.class, changeStreamDocument.getFullDocument());
            System.out.println("Received message: " + message.getMessage());
        });
    }
}

이녀석 초반에는 무늬만 pub sub 같은 코드를 내놓더니만 몇번 심도있는(?) 대화를 해주고 난 이후에는 Collection 의 watch() 로 ChangeStreamDocument 를 사용해 Event 를 Listen 하는 제대로 되어보이는 코드를 내어준다. 한번 테스트 해보자.

 

테스트 코드

package mongo.account;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Getter
@Setter
@Document(collection = "account")
public class Account {
    @Id
    private String id;
    private String userName;
    private String email;
}
@Repository
public interface AccountRepository extends MongoRepository<Account, String> {
    Account findAccountByUserName(String userName);
    List<Account> findAccountsByUserName(String userName);

    @Query(value = "distinct('userName')", fields = "userName")
    List<String> findDistinct();
}
@Service
@RequiredArgsConstructor
@Slf4j
public class AccountSubscriber {
    private final MongoTemplate mongoTemplate;

    @PostConstruct
    public void subscribe() {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("Registering Subscriber");
                mongoTemplate.getDb().getCollection("account").watch()
                        .forEach((Consumer<ChangeStreamDocument<Document>>) changeStreamDocument -> {
                            Account account = mongoTemplate.getConverter().read(Account.class, changeStreamDocument.getFullDocument());
                            log.info("[{}] userName : {}", changeStreamDocument.getOperationType(), account!=null?account.getUserName():null);
                        });
                log.info("Subscriber Registered");
            }
        });
        t.start();
    }
}
@Bean
    @Order(1)
    public ApplicationRunner applicationRunner() {
        return args -> {
            Account account = new Account();
            account.setUserName("joon");
            account.setEmail("abcdef@naver.com");

            accountRepository.insert(account);
            log.info("Done");

            List<Account> result = accountRepository.findAccountsByUserName("joon");
            log.info("result 1 : {}", result.size());


            // .... 생략 ...

            accountRepository.deleteAll();

        };
    }

 

실행결과

 

오늘도 chatGPT 가 나름 참고할만한 코드를 잘 만들어주었다. 

물론 그대로 갖다 붙여넣는다고 원하는 동작을 하지는 못한다 (뭐 이러면 개발자 죽어야지..). watch() 에서 blocking 되기땜에 thread 로 빼주는 코드를 추가해주는 센스까지는 아직 chatGPT 가 못내주기는 하다마는 그래도 이게 어디냐..

구글링 해서 찾을수도 있지만 chatGPT 를 썼을 때에는 좀더 "덜 읽어도" 된다는게 좋은것 같다

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,