본문

테크
AI와 함께 빠르고 효율적인 개발하기

작성일 2023년 06월 13일
main

개요

프로그래밍 분야에서 인공지능(AI)의 발전은 빠르게 진행되고 있습니다. 코드 작성, 버그 탐지, 코드 최적화 등의 작업을 자동화함으로써, AI는 개발자들이 작업을 더욱 신속하고 효율적으로 수행할 수 있게 돕고 있습니다. 하지만 여전히 어설픈 결과물들이 많이 보이고 있는데요. 때때로 AI는 이해하기 어려운 코드를 생성하거나, 예상치 못한 버그를 만들어내기도 합니다.

AI가 아직 개발자의 핵심 역할을 완전히 대체하진 못하지만, 개발 과정의 보조 역할에서는 매우 효과적이라고 생각합니다. 코드 리뷰, 문서 작성, 테스트 자동화 등의 작업에서 AI는 개발자의 뛰어난 비서 역할을 수행할 수 있습니다.

오늘 이 글에서 ChatGPT와 Copilot을 통해서 AI를 실무에서 적용해 본 경험을 공유하고자 합니다.


AI와 함께 일하기 위한 팁

충분한 자료(데이터)를 제공해라

AI는 사람처럼 생각하는 것이 아니라, 주어진 데이터를 기반으로 계산된 결과를 제공합니다. 그렇기 때문에 사용자의 요청을 정확하게 이해하고 작업을 수행하는 것이 아니라, 때때로 애매하거나 예상치 못한 결과를 제공할 수 있습니다. 이를 방지하기 위해, 사용자는 가능한 한 구체적이고 명확한 명령을 제공해야 합니다.

예를 들어, "이메일을 작성해 줘"라는 일반적인 명령보다는 "내일 오후 2시에 회의에 참석하라는 내용의 이메일을 작성해 줘"라는 구체적인 명령이 AI에게 더 효과적으로 작동합니다.

코드 생성을 요청할 때도 마찬가지입니다. 원하는 기능을 설명하는 글만 제공하는 것보다는, 의사 코드 수준의 코드를 함께 제공하면 AI는 훨씬 더 정확하고 유용한 결과물을 제공할 수 있습니다. 이렇게 함으로써 AI는 사용자의 요구사항을 더 정확하게 이해하고, 그에 따라 더 효과적인 결과를 만들어낼 수 있습니다.

서비스들의 차이와 장점을 활용하라

다양한 AI 서비스들이 출시되어 있으며, 각각은 특정 분야에서 뛰어난 성능을 보입니다. 일반적으로, ChatGPT 4는 가장 우수한 결과물을 제공하지만, 2021년 9월까지의 데이터로 학습되어 있어 최신 정보에 대한 처리는 제한적입니다.

반면에 구글의 Bard는 검색 기반의 결과를 제공하기 때문에 최신 데이터에 대한 처리에 강점을 보입니다.

또한, PPT 제작이나 디자인에 특화된 AI 서비스들도 있습니다. 이러한 다양한 서비스들을 사전에 검색하고 테스트를 통해 자신의 업무에 어떤 서비스가 가장 도움이 될지, 그리고 그 서비스의 한계는 어디인지를 명확하게 파악하는 것이 중요합니다.

저는 미드저니를 구독하여 테스트해 보았는데, 그림 생성 결과물은 상당히 발전한 것을 확인할 수 있었습니다. 그러나, 제가 원하는 맥락을 정확하게 반영하는 데에는 어려움이 있었으며, 생성된 결과물 간의 일관성 또한 아직은 부족한 상태였습니다.

플러그인 활용

ChatGPT 4는 다양한 플러그인을 제공하여 사용자의 편의성을 높이고 있습니다. 플러그인을 사용하려면 다음과 같은 단계를 따르면 됩니다. 먼저, ChatGPT 사용자 계정의 'Settings' 메뉴를 엽니다.

GPT Setting

'Beta features'를 선택한 후 'Plugins' 체크 버튼을 활성화합니다.

Beta features

새로운 채팅을 시작하고 원하는 플러그인을 선택합니다. 처음에는 플러그인을 Plugin store를 클릭하시고 설치를 먼저 해주셔야 합니다. 아래 이미지에서 Link Reader라는 플러그인이 선택되어 있습니다.

Link Reader 플러그인을 선택해주시고 URL 링크와 함께 작업을 요청하시면 해당 URL의 문서를 읽어와서 처리해줍니다. ChatGPT에 코드를 전달할 때, 전체 코드를 올려서 질문하는 것이 번거로울 수 있는데요. 이런 경우, Link Reader 플러그인을 활용하여 특정 링크의 문서에 대한 작업을 요청하면 편리합니다.

plugin

저는 Link Reader, Video Insights, 그리고 Zapier를 자주 사용하고 있습니다. Video Insights는 비디오 링크를 주면 비디오의 내용을 텍스트로 만들어 줍니다. 긴 시간 동안 영상을 볼 수 없을 때 텍스트로 전체 내용을 미리 확인하는 용도로 자주 사용하고 있습니다. Zapier는 자동화에 대한 다양한 기능을 제공합니다. 결과물이 신통치 않을 때가 많지만 가끔 도움이 됩니다. 플러그인을 동시에 선택할 수 있는 것은 세 개까지입니다.


프로그래밍 보조 도구로써 AI의 이점

개인적으로 AI가 일하는데 가장 도움이 되는 순간은 크게 두 가지가 있었습니다.

첫 번째는 “모든 일은 시작이 가장 어렵다”입니다. 막상 시작하고 나면 탄력이 붙어서 일에 집중할 수 있는데도, 다른 방해 요소들로 인해서 집중하지 못하는 경우가 종종 있습니다. 이럴 때는 아주 간단한 작업이라도 시작하면 도움이 되곤 했는데요. 요즘은 AI로 해야할 작업을 미리 물어 보는 것으로 첫 삽을 뜨기도 합니다.

두 번째는 귀찮고 반복적인 작업을 AI를 통해서 시간을 절약하는 것입니다.

아래는 제가 생각하는 프로그래밍 보조 도구로써 AI의 이점들입니다.

  1. 사전조사 지원: AI는 문제 해결에 대한 사전 검색을 용이하게 합니다. 확신이 서지 않는 가설을 AI를 통해 교차 검증하면 시간을 절약할 수 있습니다. 하지만 AI도 자신이 모르는 분야에 대해 자신있게 오답을 제공할 수 있으므로, 다른 AI나 실제 검색을 통한 확인 과정이 필요합니다.
  2. 자동 코드 생성: AI는 품질이 낮거나 오류가 있는 코드를 생성할 수도 있지만, 이미 다양한 코드가 공개된 경우에는 효과적입니다. 반복적인 작업을 대신 수행하는 것만으로도 충분한 가치를 제공합니다.
  3. 문서화 및 주석 작성: AI는 전체 소스 코드에 주석을 추가하거나, JavaDoc과 같은 문서 자동화 스타일을 일괄적으로 적용할 수 있습니다.
  4. 테스트 코드 작성: AI는 실무 수준의 테스트 코드 작성에는 한계가 있지만, 귀찮은 작업을 대신 수행하는 정도의 도움은 제공합니다. 구체적인 조건을 제공하면 더 좋은 결과를 얻을 수 있습니다.
  5. 코드 최적화: AI는 코드를 분석하여 최적화 방안을 제안할 수 있습니다. 이는 코드의 실행 시간을 줄이고, 자원 사용을 최적화하는 데 도움이 됩니다.
  6. 리팩토링: AI는 사람이 쉽게 놓칠 수 있는 부분을 찾아내고, 코드를 자동으로 수정해줍니다. 하지만 품질 문제와 코드 컨벤션, 개발자의 취향을 반영하기 위해 추가적인 마무리 작업이 필요합니다.
  7. 버그 탐지 및 해결: AI는 완벽하지 않지만, 사람이 놓치기 쉬운 부분을 찾아주곤 합니다. 특히 초보 개발자에게는 큰 도움이 될 수 있습니다.
  8. 기타

    - 익숙하지 않은 언어나 개발환경에서 일을할 때 많은 힌트를 제공합니다. 필자의 경우에는 자바스크립트 문법이 항상
    헷갈리는데, 대충 적거나 설명을 달아주면 AI가 완성해주기 때문에 핵심 개발 업무에 보다 집중할 수 있었습니다.

    - 알고리즘 시간 복잡도를 계산하는데 사용할 수 있습니다.

    - 주석 제거에도 도움이 됩니다. 가끔 불필요한 주석이 많이 달린 오픈 소스를 분석할 때 사용하고 있습니다.

    - 콘솔 (또는 리눅스) 명령어가 잘 기억 나지 않을 때 사용할 수 있습니다.


활용 사례들

  1. 리팩토링 및 문서화 작업

소소코드나 소스의 링크를 던져주면 리팩토링이나 문서를 자동으로 생성해줍니다. 링크의 문서와 결과물을 비교해 보시기 바랍니다. 코드를 수정한 부분에 대해서도 설명을 자세하게 달아주는 편입니다.

요청

이 코드를 리팩토링 해주고 JavaDoc 스타일로 한글로 주석을 달아 줘.
https://raw.githubusercontent.com/ryujt/ryulib-android/master/app/src/main/java/ryulib/SuspensionQueue.java

결과

      
package ryulib;
import android.util.Log;
import java.util.LinkedList;
import java.util.Queue;
/**
* 이 클래스는 제네릭 타입 T를 가진 SuspensionQueue를 표현합니다.
*/
public class SuspensionQueue {
    // 큐 인스턴스
    private Queue queue = new LinkedList<>();
    /**
    * 이 메서드는 큐를 비우는 데 사용됩니다.
    */
    public synchronized void clear() {
        queue.clear();
    }
    /**
    * 이 메서드는 아이템을 큐에 넣는 데 사용됩니다.
    * @param item 이것은 큐에 넣을 아이템입니다.
    */
    public synchronized void push(T item) {
        queue.offer(item);
        notifyAll();
    }
    /**
    * 이 메서드는 아이템을 큐에서 꺼내는 데 사용됩니다.
    * @return T 이것은 큐에서 꺼낸 아이템을 반환합니다.
    */
    public synchronized T pop() {
        while (queue.peek() == null) {
            try {
                wait();
            } catch (Exception e) {
                Log.e("SuspensionQueue", e.getMessage());
            }
        }
        return queue.remove();
    }
    /**
    * 이 메서드는 큐의 다음 아이템을 확인하는 데 사용됩니다.
    * @return T 이것은 큐의 다음 아이템을 반환합니다.
    */
    public synchronized T peek() {
        return queue.peek();
    }
    /**
    * 이 메서드는 큐가 비어 있는지 확인하는 데 사용됩니다.
    * @return boolean 큐가 비어 있으면 true를, 그렇지 않으면 false를 반환합니다.
    */
    public synchronized boolean is_empty() {
        return queue.isEmpty();
    }
    /**
    * 이 메서드는 큐의 크기를 얻는 데 사용됩니다.
    * @return int 이것은 큐의 크기를 반환합니다.
    */
    public synchronized int size() {
        return queue.size();
    }
}
                    
      

이 클래스는 제네릭 타입 T를 사용하여 다양한 타입의 객체를 저장할 수 있는 큐를 구현하는 SuspensionQueue 클래스를 정의합니다. 이 클래스는 큐를 초기화하고, 큐에 아이템을 추가하며, 큐에서 아이템을 제거하고, 큐의 다음 아이템을 확인하며, 큐가 비어 있는지 확인하고, 큐의 크기를 반환하는 메서드를 제공합니다. 이 모든 메서드는 동기화되어 있어, 여러 스레드에서 동시에 접근하더라도 안전하게 동작합니다.


아래는 다른 코드의 리팩토링의 결과입니다. 중복된 부분을 메소드로 분리해서 처리하고 있습니다. 메소드 명을 통해서 의도를 확실하게 드러내는 점도 좋아 보입니다.

요청

      
Widget _inputUrl() {
  return Form(
    onChanged: () {
      if (this._blogUrlController.text.length > 0) {
        if(this._blogUrlController.text == UiConfig.blogUrlPrefix) {
          setState(() {
            _blogUrlEnabled = false;
          });
        } else {
          setState(() {
            _blogUrlEnabled = true;
          });
        }
      } else {
        setState(() {
          _blogUrlEnabled = false;
        });
      }
    },
    child: commonInputField(
      controller: this._blogUrlController
    ),
  );
}                        
                    
      

결과

      
void _updateBlogUrlEnabled() {
  setState(() {
    _blogUrlEnabled = _blogUrlController.text.isNotEmpty 
        && _blogUrlController.text != UiConfig.blogUrlPrefix;
  });
}
Widget _inputUrl() {
  return Form(
    onChanged: _updateBlogUrlEnabled,
    child: commonInputField(
      controller: this._blogUrlController
    ),
  );
}                        
                      
      

또 다른 코드의 리랙토링 결과입니다.

요청

      
Widget _urlSettingButton() {
  if (_blogUrlEnabled) {
    return mainButton(
        title: '등록하기',
        onPressed: () async {
          final checkResult = await checkNaverBlog(...);
          if (checkResult.result!) {
            moveSignupForm(...);
          } else {
            CommonUtils.toastMessage(checkResult.message!);
          }
        }
    );
  } else {
    return mainButton(
        title: '등록하기',
        backgroundColor: UiConfig.primaryDisabledColor,
        onPressed: () {
        }
    );
  }
}                        
                    
      

결과

      
Widget _urlSettingButton() {
  return mainButton(
      title: '등록하기',
      backgroundColor: _blogUrlEnabled ? 
          null : UiConfig.primaryDisabledColor,
      onPressed: _blogUrlEnabled ? ()async {
        final checkResult = await checkNaverBlog(...);
        if (checkResult.result!) {
          moveSignupForm(...);
        } else {
          CommonUtils.toastMessage(checkResult.message!);
        }
      } : null
  );
}                        
                      
      

이번 결과물에는 결과물에 조금 불만이 있는데요. 아래와 같은 코드가 개인적으로 더 나아 보입니다.

      
Widget _urlSettingButton() {
  return mainButton(
      title: '등록하기',
      backgroundColor: _blogUrlEnabled ? 
          null : UiConfig.primaryDisabledColor,
      onPressed: () async {
        if (!_blogUrlEnabled) return;
        final checkResult = await checkNaverBlog(...);
        if (checkResult.result!) {
          moveSignupForm(...);
        } else {
          CommonUtils.toastMessage(checkResult.message!);
        }
      }
  );
}                        
                      
      

테스트 코드 작성

방금 작업했던 코드에 대해서 추가작업을 하고 싶을 때는 코드를 다시 올릴 필요가 없습니다.

요청

방금 만든 코드에 대한 테스트 코드를 작성해 줘.

결과

      
package ryulib;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SuspensionQueueTest {
    @Test
    void testQueueOperations() {
        SuspensionQueue queue = new SuspensionQueue<>();
        // Test isEmpty() method
        assertTrue(queue.is_empty());
        // Test push() method
        queue.push(1);
        assertFalse(queue.is_empty());
        // Test peek() method
        assertEquals(1, queue.peek());
        // Test size() method
        assertEquals(1, queue.size());
        // Test pop() method
        assertEquals(1, queue.pop());
        assertTrue(queue.is_empty());
        // Test clear() method
        queue.push(2);
        queue.push(3);
        queue.clear();
        assertTrue(queue.is_empty());
    }
}                        
                      
      

이 테스트 코드는 SuspensionQueue 클래스의 isEmpty(), push(), peek(), size(), pop(), clear() 메서드들을 테스트합니다. 각 메서드가 예상대로 동작하는지 확인하기 위해 JUnit의 assertTrue(), assertFalse(), assertEquals() 메서드들을 사용합니다. 이 테스트 코드를 실행하면 SuspensionQueue 클래스의 메서드들이 제대로 동작하는지 확인할 수 있습니다.


조건을 제시하면 그에 맞는 테스트 코드를 작성해 줍니다.

요청

멀티 스레드 상황에서의 테스트 코드를 작성해 줘.

결과

      
package ryulib;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
class SuspensionQueueTest {
    @Test
    void testQueueOperationsInMultiThreadedEnvironment() throws InterruptedException {
        SuspensionQueue queue = new SuspensionQueue<>();
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        // Pushing items into the queue in one thread
        executorService.submit(() -> {
            for (int i = 0; i < 100; i++) {
                queue.push(i);
            }
        });
        // Popping items from the queue in another thread
        executorService.submit(() -> {
            for (int i = 0; i < 100; i++) {
                assertNotNull(queue.pop());
            }
        });
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        // After all items are pushed and popped, the queue should be empty
        assertTrue(queue.is_empty());
    }
}                        
                      
      

이 테스트 코드는 두 개의 스레드를 생성하여 하나는 큐에 아이템을 넣고, 다른 하나는 큐에서 아이템을 꺼내는 작업을 동시에 수행합니다. 모든 아이템이 큐에 넣어졌다가 꺼내진 후에는 큐가 비어 있어야 합니다. 이를 확인하기 위해 assertTrue(queue.is_empty())를 사용합니다. 이 테스트 코드를 실행하면 SuspensionQueue 클래스가 멀티스레드 환경에서도 제대로 동작하는지 확인할 수 있습니다.


성능 개선 요청

조금이라도 성능을 끌어 내기 위해서 레거시 코드를 보여주고, MemoryStream에 메모리 할당을 미리해서 성능을 개선시킬 수 없냐고 물어봤습니다. 단순히 요청을 하는 것보다, 이와 같이 구체적인 정보를 제공하면 더 정확하고 효과적인 결과를 얻을 수 있습니다.

요청

이 코드에서 MemoryStream에 메모리를 미리 할당해서 성능을 개선할 수 없을까?

      
using System.IO;
using WhaTap.Trace.Utils;
namespace WhaTap.Trace.GoAgent
{
    public sealed class GoPackError : GoPackBase
    {
        string ErrorType;
        string ErrorMessage;
        string Stack;
        public GoPackError(Step step) : base(step)
        {
            ErrorType = step.ErrorType;
            ErrorMessage = step.ErrorMsg;
            Stack = step.ErrorStack;
        }
        public override byte[] GetArray()
        {
            MemoryStream ms = new MemoryStream();
            var intPackArray = GetIntPackArray();
            ms.Write(intPackArray, 0, intPackArray.Length);
            var errorType = PackUtils.GetStringPack(ErrorType);
            ms.Write(errorType, 0, errorType.Length);
            var errorMessage = PackUtils.GetStringPack(ErrorMessage);
            ms.Write(errorMessage, 0, errorMessage.Length);
            var stack = PackUtils.GetStringPack(Stack);
            ms.Write(stack, 0, stack. Length);
            return ms.ToArray();
        }
    }
}                        
                      
      

결과

친절하게 설명과 함께 결과 코드를 보여줍니다.

plugin

줄거리 던져주기

ChatGPT에게 아래와 같은 코드를 던져주고 완성시켜 달라고 요청해 보았습니다.

요청

이 코드를 완성해 줘.

      
namespace WhaTap.Trace
{
    public class ActiveStack
    {
        private ActiveStack()
        {
            var thread = new System.Threading.Thread(() =>
            {
                while (true)
                {
                    // TODO: Get first step from _steps
                    Step step = null;
                    captureCallStack(step);
                    // TODO: get interval from config
                    Thread.Sleep(5000);
                }
            });
        }
        public void AddTransaction(Step step)
        {
            // TODO: add transaction
        }
        public void RemoveTransaction(Step step)
        {
            // TODO: remove step from _steps
        }
        private void captureCallStack(Step step)
        {
            // TODO: capture call stack
        }
        private Object _lock = new Object();
        private List _steps = new List();
    }
}                        
                  
      

결과

      
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace WhaTap.Trace
{
    public class ActiveStack
    {
        private ActiveStack()
        {
            var thread = new System.Threading.Thread(() =>
            {
                while (true)
                {
                    Step step = null;
                    lock (_lock)
                    {
                        if (_steps.Count > 0)
                        {
                            step = _steps[0];
                        }
                    }
                    if (step != null)
                    {
                        captureCallStack(step);
                    }
                    Thread.Sleep(5000);
                }
            });
            thread.Start();
        }
        public void AddTransaction(Step step)
        {
            lock (_lock)
            {
                _steps.Add(step);
            }
        }
        public void RemoveTransaction(Step step)
        {
            lock (_lock)
            {
                _steps.Remove(step);
            }
        }
        private void captureCallStack(Step step)
        {
            StackTrace stackTrace = new StackTrace();
            Console.WriteLine(stackTrace.ToString());
        }
        private Object _lock = new Object();
        private List _steps = new List();
    }
}                        
                  
      

줄거리 작성을 상세하게 제공하고 있어서 결과물이 줄거리에서 크게 벗어나지 않았습니다. 그러나 리스트의 첫 번째 항목을 가져오는 과정에서 오류가 발생했는데요. 이 부분은 아래와 같이 수정해야 했습니다.

list_fix

AI와 작업하다보면 이부분이 상당히 위험합니다. 대충 보면 문제 없을 거 같기에 자칫 찾기 어려운 버그들을 양상할 수 있는 가능성이 많다는 점을 유의해야 합니다.


단순작업 처리하기

이번에는 VisualStudio Code의 경우인데요. 왼쪽의 코드에서 SetTag() 메소드로 처리된 부분을 오른쪽과 같이 멤버 필드로 변경하는 작업이 필요했던 경우입니다.

change_method

문제는 이것이 코드 전반에 너무 많이 산재해 있기 때문에 일일이 수정하는 것이 번거로웠는데요. 아래와 같이 샘플을 하나 작성해 주자 다음 위치에서 수정을 하려는 순간 VisualStudio Code가 알아서 코드를 제시해주는 장면입니다. 샘플을 제공하기 전에는 엉뚱한 코드를 제시해주었지만, 샘플을 제공하자 원하는대로 코드를 생성해 주고 있습니다.

with_sample

이 부분에서도 ChatGPT를 사용하면 훨씬 간편하게 처리할 수 있었습니다. 다만 ChatGPT가 원하는 형식의 질문을 찾기 위해서 시행착오 과정이 필요했습니다.

실패했던 요청 중 하나 (RedisValue에 해당하는 한 건만 수정해 줌)

링크의 코드에서 SetTag를 모두 찾아서 아래 규칙으로 변경해 줘.

         
AS-IS: step.SetTag(Tags.RedisValue, value);
TO-BE: step.RedisValue =  value;
[소스 링크 첨부]  
                  
         

성공한 요청

링크의 코드에서 주석의 코드를 밑에 있는 코드 형태로 변경해 줘.

         
// step.SetTag(Tags.RedisValue, value);
step.RedisValue =  value;
[소스 링크 첨부]  
                  
         

마무리

최근에 다양한 AI 서비스를 탐색하며 그것들을 업무에 어떻게 통합할 수 있는지에 대해 깊이 연구해 보았습니다. AI의 미래 발전 가능성에 대한 기대감이 커지고 있지만, 현재로서는 AI가 개발의 핵심 업무를 완전히 지원하기에는 아직 한계가 있다는 인식을 갖게 되었습니다.

그럼에도 불구하고, 현재의 AI 수준에서도 적절한 접근 방식을 통해 충분히 이점을 얻을 수 있음을 발견했습니다. AI를 활용하면 시간과 비용을 절약하면서도 효율성을 높일 수 있다는 사실을 깨달았습니다.

AI의 장점과 한계를 명확히 이해하고 이를 적절하게 활용하는 것은, 개인이나 조직의 경쟁력을 높이는 중요한 전략이 될 수 있다고 믿습니다. 이 글을 통해 제가 얻은 경험과 인사이트를 공유하고자 합니다.

감사합니다.

 

 

 

류종택[email protected]
Development TeamAPM Agent Developer

지금 바로
와탭을 경험해 보세요.