<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 알다가도 모르겠네요</title>
    <link>https://2jaebbang.tistory.com/</link>
    <description>https://github.com/2jaebbang</description>
    <language>ko</language>
    <pubDate>Mon, 27 Jul 2026 19:23:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>이재빵</managingEditor>
    <image>
      <title>개발 알다가도 모르겠네요</title>
      <url>https://tistory1.daumcdn.net/tistory/4373060/attach/cf90262ca4c247d1b13e48127770d34e</url>
      <link>https://2jaebbang.tistory.com</link>
    </image>
    <item>
      <title>[Spring] AWS S3 + CloudFront를 활용해서 음악 공유 URL을 만들어보자</title>
      <link>https://2jaebbang.tistory.com/205</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인생네컷에는 QR코드를 통해 촬영 과정을 담은 비디오, 사진 이미지를 다운받을 수 있는 유효시간 72시간의 URL을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 개발중인 Daytune 앱 내에도 사용자가 자신의 음악을 주위 친구들에게 자랑함으로써&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt; 참여도를 높이고, 유입자 수를 증가시키&lt;/span&gt;기 위해 공유 URL 기능을 추가하기로 했다. 현재는&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;사용자가 일기를 작성하면, 그 일기의 감정에 맞는 음악 2개를 생성해주고 사용자는 2개 중 하나를 골라 소유하게 된다. 그 다음 과정으로&lt;/span&gt;&amp;nbsp;&lt;b&gt;72시간동안 접근 가능한 음악공유 URL&lt;/b&gt; 을 생성하여 제공하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면 유효시간이 설정된 url은 어떻게 만들어야 할까? 답은 AWS S3의 Presigned Url 기능에 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Pre-Signed URL 이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Pre-Signed URL은 AWS S3의 객체에 대한 &lt;b&gt;제한된 시간 동안의 접근 권한을 부여하는 URL&lt;/b&gt;이다. 이 URL을 통해, S3 버킷에 저장된 객체에 대해 제한된 시간 동안 읽기 또는 쓰기 권한을 부여할 수 있다. Pre-Signed URL을 생성하는 주체는 해당 객체에 접근할 권한을 가진 사용자여야 하며, URL을 생성할 때 특정 기간 동안만 유효하도록 설정할 수 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AWS CloudFront 란? &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudFront는 &lt;b&gt;글로벌 콘텐츠 배포 네트워크(CDN)&lt;/b&gt; 서비스이다. 웹 콘텐츠, 동영상, 애플리케이션 데이터 등의 정적 및 동적 콘텐츠를 보다 빠르게 제공할 수 있고, 지리적으로 분산된 엣지 로케이션을 활용하여 사용자에게 가장 가까운 서버에서 콘텐츠를 제공함으로써 성능을 최적화시킨다고 한다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;특히 SSL/TLS 암호화를 통해 데이터 전송의 보안을 보장하고, Origin Access Identity(OAI)를 사용하면 S3 버킷을 CloudFront를 통해서만 접근 가능하도록 설정할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구현과정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS S3과 Cloud Front를 활용하여 유효시간이 있는 공유 URL을 생성해 보자.&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;build.gradle 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1716821767355&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;yml파일 내 AWS S3 관련 정보 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1716823894727&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloud:
  aws:
    credentials:
      accessKey: 액세스키
      secretKey: 비밀키
    region:
      static: 지역
    stack:
      auto: false
    s3:
      bucket: S3 버킷명
    cloudfront:
      domain: 설정한 cloudfront 도메인명&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;File Util 구현&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1716823714575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 파일 다운로드 유틸리티
 * 주어진 URL에서 파일을 다운로드하여 임시 파일로 저장하는 기능
 */
public class FileUtil {
    /**
     * 주어진 URL에서 파일을 다운로드하여 임시 파일로 저장
     * @param fileUrl 다운로드할 파일의 URL
     * @return 다운로드한 임시 파일
     * @throws IOException 파일 다운로드 중 오류가 발생한 경우
     */
    public static File downloadFile(String fileUrl) throws IOException {
        // URL 객체 생성
        URL url = new URL(fileUrl);

        // URL에 대한 HttpURLConnection 객체 생성
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // HTTP 요청 메서드를 GET으로 설정
        connection.setRequestMethod(&quot;GET&quot;);

        // 연결 시도
        connection.connect();

        // 서버 응답 코드가 HTTP OK(200)인지 확인
        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            // 응답 코드가 HTTP OK가 아닌 경우 예외 발생
            throw new IOException(&quot;파일 다운로드 실패: &quot; + connection.getResponseMessage());
        }

        // 서버로부터 파일 데이터를 읽기 위한 입력 스트림 열기
        InputStream inputStream = connection.getInputStream();

        // 임시 파일 생성 (접두사 &quot;temp&quot;, 접미사 &quot;.mp3&quot; 사용)
        File tempFile = File.createTempFile(&quot;temp&quot;, &quot;.mp3&quot;);

        // 임시 파일에 데이터를 쓰기 위한 파일 출력 스트림 생성
        FileOutputStream outputStream = new FileOutputStream(tempFile);

        // 데이터 읽기 및 쓰기를 위한 버퍼 생성 (버퍼 크기: 4096 바이트)
        byte[] buffer = new byte[4096];
        int bytesRead;

        // 입력 스트림으로부터 데이터를 읽어와 버퍼에 저장하고, 출력 스트림으로 씀
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }

        // 출력 스트림 닫기 (자원 해제)
        outputStream.close();

        // 입력 스트림 닫기 (자원 해제)
        inputStream.close();

        // 다운로드한 임시 파일 반환
        return tempFile;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;S3 Config 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1716823793256&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * S3 연동을 위한 환경 설정
 */
@Slf4j
@Configuration
public class S3Config {
    @Value(&quot;${cloud.aws.credentials.accessKey}&quot;)
    private String accessKey;

    @Value(&quot;${cloud.aws.credentials.secretKey}&quot;)
    private String secretKey;

    @Value(&quot;${cloud.aws.region.static}&quot;)
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(Regions.fromName(region))
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;S3Service 구현&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS S3에 파일을 업로드하고 CloudFront URL을 생성하는 서비스가 필요하다. 이 때 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;CloudFront URL를 이용하는 이유는 s3에서 생성된 url을 그대로 사용할 경우, s3의 주소가 그대로 노출된 뿐 아니라 url길이가 아래처럼 너무 길어지기 때문이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://daytune-share-url-s3.s3.ap-northeast-2.amazonaws.com/pages/9662c047-e9b2-4358-8d60-c3643df50a3f.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Date=20240523T151412Z&amp;amp;X-Amz-SignedHeaders=host&amp;amp;X-Amz-Expires=259199&amp;amp;X-Amz-Credential=AKIA6GBMES42FVAAMYOK%2F20240523%2Fap-northeast-2%2Fs3%2Faws4_request&amp;amp;X-Amz-Signature=af0d750532e5501c06c2bf13310a7c624ed9ff2275ce2ab9e02bfb0b559dba62&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://daytune-share-url-s3.s3.ap-northeast-2.amazonaws.com/pages/9662c047-e9b2-4358-8d60-c3643df50a3f.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Date=20240523T151412Z&amp;amp;X-Amz-SignedHeaders=host&amp;amp;X-Amz-Expires=259199&amp;amp;X-Amz-Credential=AKIA6GBMES42FVAAMYOK%2F20240523%2Fap-northeast-2%2Fs3%2Faws4_request&amp;amp;X-Amz-Signature=af0d750532e5501c06c2bf13310a7c624ed9ff2275ce2ab9e02bfb0b559dba62&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudFront 를 통해 Url을 생성할 경우 아래처럼 설명한 도메인명, 짧은 길이의 주소가 생성된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://cdn.chewthecud.site/pages/ab9b9130-e520-44db-8d65-8e7b74e173c6.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cdn.chewthecud.site/pages/ab9b9130-e520-44db-8d65-8e7b74e173c6.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3Service 클래스는 파일을 S3에 업로드하고, 업로드된 파일의 URL을 반환하는 역할을 한다. 이를 통해 사용자들이 업로드한 파일을 클라우드에서 직접 접근할 수 있게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1716822036256&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class S3Service {
    @Autowired
    private AmazonS3 amazonS3;

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucketName;

    @Value(&quot;${cloud.aws.cloudfront.domain}&quot;)
    private String cloudFrontDomain;

    /**
     * File 객체를 S3에 업로드
     * @param fileName 업로드할 파일의 이름
     * @param file 업로드할 파일 객체
     * @return 업로드된 파일의 CloudFront URL
     */
    public String uploadFile(String fileName, File file) {
        try (InputStream inputStream = new FileInputStream(file)) {
            // 파일 메타데이터 설정
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.length());
            metadata.setContentType(&quot;audio/mpeg&quot;);

            // S3에 파일 업로드
            amazonS3.putObject(new PutObjectRequest(bucketName, fileName, inputStream, metadata));
        } catch (Exception e) {
            // 업로드 중 예외 발생 시 RuntimeException 발생
            throw new RuntimeException(e.getMessage());
        }

        // 업로드된 파일의 CloudFront URL 반환
        return &quot;https://&quot; + cloudFrontDomain + &quot;/&quot; + fileName;
    }

    /**
     * 바이트 배열을 S3에 업로드
     * @param fileName 업로드할 파일의 이름
     * @param content 업로드할 파일 내용 (바이트 배열)
     * @param contentType 파일의 콘텐츠 타입
     * @return 업로드된 파일의 CloudFront URL
     */
    public String uploadFile(String fileName, byte[] content, String contentType) {
        // 바이트 배열을 입력 스트림으로 변환
        InputStream inputStream = new ByteArrayInputStream(content);

        // 파일 메타데이터 설정
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(content.length);
        metadata.setContentType(contentType);

        // S3에 파일 업로드
        amazonS3.putObject(new PutObjectRequest(bucketName, fileName, inputStream, metadata));

        // 업로드된 파일의 CloudFront URL 반환
        return &quot;https://&quot; + cloudFrontDomain + &quot;/&quot; + fileName;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;공유 HTML 페이지 생성 및 업로드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 AI음악 2개 중 최종 선택한 음악을 72시간동안 공유할 수 있는 url을 S3를 통해 생성하는 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;공유 HTML 페이지 생성 및 업로드&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3에서 생성한 Pre-Signed URL을 사용하여 HTML 페이지를 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성된 HTML 페이지를 S3에 업로드하고 해당 URL을 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;공유 URL 저장 및 반환&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공유 URL을 생성하고 유효기간(3일)을 설정한 후 데이터베이스에 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최종적으로 생성된 공유 URL을 포함한 응답 객체를 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1716822150789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional
    public MusicSelectionResponseDto selectMusic(Long diaryId, Long musicId) {
        // 일기 엔티티 조회
        ...
        // 선택된 음악 엔티티를 조회
        ...
        // 이미 선택된 음악인지 확인 (이미 선택된 음악일 경우 fileUrl이 https://cdn1.suno.ai/~ -&amp;gt; https://storage.googleapis.com/~ 로 변경되어있음)
        ...

        // 선택된 음악을 제외한 나머지 음악 목록을 조회
        ...
        // 나머지 음악들을 삭제
        ...
        try {
            // 원격 파일을 다운로드
            File tempFile = FileUtil.downloadFile(selectedMusic.getFileUrl());

            // 선택된 음악을 Firebase Storage와 AWS S3에 저장
            String firebaseUrl = firebaseService.uploadFileToFirebaseStorage(tempFile, diary.getId().toString());
            String s3Url = s3Service.uploadFile(&quot;music/diaryId_&quot; + diary.getId() + &quot;.mp3&quot;, tempFile);

            // 임시 파일 삭제
            tempFile.delete();

            // 선택된 음악의 URL을 업데이트
            selectedMusic.setFileUrl(firebaseUrl);
            musicRepository.save(selectedMusic);

            String thumbnailUrl = &quot;https://via.placeholder.com/300&quot;;  // 예시 썸네일 URL
            String htmlUrl = generateMusicSharePage(s3Url, thumbnailUrl);

            // 공유 URL 생성 및 저장
            ShareUrl shareUrl = new ShareUrl();
            shareUrl.setMusic(selectedMusic);
            shareUrl.setUrl(s3Url);
            shareUrl.setExpirationDate(LocalDateTime.now().plusDays(3)); // 유효 기간 3일 설정
            shareUrlRepository.save(shareUrl);

            return MusicSelectionResponseDto.builder().shareUrl(htmlUrl).build();
        } catch (IOException e) {
            throw new RuntimeException(&quot;파일 업로드 중 오류가 발생했습니다.&quot;, e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;아래 generateMusicSharePage 메서드는 음악 파일의 Pre-Signed URL을 사용하여 HTML 페이지를 생성하고, S3에 업로드하는 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HTML 페이지 생성&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제공된 Pre-Signed URL과 썸네일 이미지를 사용하여 HTML 페이지 콘텐츠를 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTML내 오디오 플레이어를 포함하여 사용자가 음악을 재생할 수 있도록 함 (추후 앱스토어 링크, 음악 다운로드 버튼 추가 예정)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HTML 페이지 업로드&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성된 HTML 페이지를 S3에 업로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;업로드된 HTML 페이지의 S3 URL을 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1716822677467&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    /**
     * 음악 파일의 Presigned URL을 사용하여 HTML 페이지를 생성하고, S3에 업로드
     * @param presignedUrl 음악 파일의 Presigned URL
     * @return HTML 페이지의 Presigned URL
     */
    private String generateMusicSharePage(String presignedUrl, String thumbnailUrl) {
        String htmlContent = &quot;&amp;lt;!DOCTYPE html&amp;gt;\n&quot;
                + &quot;&amp;lt;html lang=\&quot;en\&quot;&amp;gt;\n&quot;
                + &quot;&amp;lt;head&amp;gt;\n&quot;
                + &quot;    &amp;lt;meta charset=\&quot;UTF-8\&quot;&amp;gt;\n&quot;
                + &quot;    &amp;lt;meta name=\&quot;viewport\&quot; content=\&quot;width=device-width, initial-scale=1.0\&quot;&amp;gt;\n&quot;
                + &quot;    &amp;lt;title&amp;gt;Daytune&amp;lt;/title&amp;gt;\n&quot;
                + &quot;    &amp;lt;meta property=\&quot;og:image\&quot; content=\&quot;&quot; + thumbnailUrl + &quot;\&quot; /&amp;gt;\n&quot;
                + &quot;    &amp;lt;meta property=\&quot;og:title\&quot; content=\&quot;Daytune\&quot; /&amp;gt;\n&quot;
                + &quot;    &amp;lt;meta property=\&quot;og:description\&quot; content=\&quot;오늘의 감정을 음악으로\&quot; /&amp;gt;\n&quot;
                + &quot;&amp;lt;/head&amp;gt;\n&quot;
                + &quot;&amp;lt;body&amp;gt;\n&quot;
                + &quot;    &amp;lt;audio controls&amp;gt;\n&quot;
                + &quot;        &amp;lt;source src=\&quot;&quot; + presignedUrl + &quot;\&quot; type=\&quot;audio/mpeg\&quot;&amp;gt;\n&quot;
                + &quot;        Your browser does not support the audio element.\n&quot;
                + &quot;    &amp;lt;/audio&amp;gt;\n&quot;
                + &quot;&amp;lt;/body&amp;gt;\n&quot;
                + &quot;&amp;lt;/html&amp;gt;&quot;;

        // S3에 HTML 페이지 업로드
        String pageFileName = &quot;pages/&quot; + UUID.randomUUID().toString() + &quot;.html&quot;;
        return s3Service.uploadFile(pageFileName, htmlContent.getBytes(StandardCharsets.UTF_8), &quot;text/html&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과물&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.19.18.png&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buwrOM/btsHEqvKPgr/kFHIebxOxxyFzQERl5HTSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buwrOM/btsHEqvKPgr/kFHIebxOxxyFzQERl5HTSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buwrOM/btsHEqvKPgr/kFHIebxOxxyFzQERl5HTSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuwrOM%2FbtsHEqvKPgr%2FkFHIebxOxxyFzQERl5HTSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;117&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.19.18.png&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.19.00.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I0Dq4/btsHElux3Vk/UbKKgiIuz8ebJva3IVK4g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I0Dq4/btsHElux3Vk/UbKKgiIuz8ebJva3IVK4g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I0Dq4/btsHElux3Vk/UbKKgiIuz8ebJva3IVK4g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI0Dq4%2FbtsHElux3Vk%2FUbKKgiIuz8ebJva3IVK4g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;506&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.19.00.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cloud Front와 S3 버킷, Rout53 설정이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.55.25.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQTvMI/btsHEnlCaJW/bfsIoiGjct0qiI0tAN7juk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQTvMI/btsHEnlCaJW/bfsIoiGjct0qiI0tAN7juk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQTvMI/btsHEnlCaJW/bfsIoiGjct0qiI0tAN7juk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQTvMI%2FbtsHEnlCaJW%2FbfsIoiGjct0qiI0tAN7juk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3086&quot; height=&quot;1450&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.55.25.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.45.49.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b91kVX/btsHEn6PeHL/kfKtjBEPD9G2MomXwZa0mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b91kVX/btsHEn6PeHL/kfKtjBEPD9G2MomXwZa0mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b91kVX/btsHEn6PeHL/kfKtjBEPD9G2MomXwZa0mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb91kVX%2FbtsHEn6PeHL%2FkfKtjBEPD9G2MomXwZa0mK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;846&quot; height=&quot;243&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.45.49.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.21.46.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGgrll/btsHD74fQTZ/6aJKqq8SvoRoSWPHA98YE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGgrll/btsHD74fQTZ/6aJKqq8SvoRoSWPHA98YE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGgrll/btsHD74fQTZ/6aJKqq8SvoRoSWPHA98YE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGgrll%2FbtsHD74fQTZ%2F6aJKqq8SvoRoSWPHA98YE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;842&quot; height=&quot;414&quot; data-filename=&quot;스크린샷 2024-05-28 오전 12.21.46.png&quot; data-origin-width=&quot;3086&quot; data-origin-height=&quot;1516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1716824914507&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Boot &amp;amp; AWS] AWS S3 bucket을 활용한 이미지 업로드&quot; data-og-description=&quot;프로젝트를 진행하면서 부가적인 회원 서비스를 깔끔하게 마무리짓기 위해 프로필 사진 업로드를 진행하고자 하였습니다.로컬 환경의 상대 경로를 활용하는 것은 이전에 시도해보아서 익숙했&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@jjinwo0/Spring-Boot-AWS-AWS-S3-bucket%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C&quot; data-og-url=&quot;https://velog.io/@jjinwo0/Spring-Boot-AWS-AWS-S3-bucket을-활용한-이미지-업로드&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dJRxQ9/hyWdpO4lYF/XsItjYj5tcxUcV5UGn8WF0/img.jpg?width=1374&amp;amp;height=773&amp;amp;face=0_0_1374_773,https://scrap.kakaocdn.net/dn/vOibQ/hyWdn4LLFq/06khWsb32hdmmJkYDJiMYK/img.jpg?width=1374&amp;amp;height=773&amp;amp;face=0_0_1374_773,https://scrap.kakaocdn.net/dn/mjl6P/hyWdjuxuLD/AEoi8RGog2m2i6feVoSVp1/img.png?width=1777&amp;amp;height=807&amp;amp;face=0_0_1777_807&quot;&gt;&lt;a href=&quot;https://velog.io/@jjinwo0/Spring-Boot-AWS-AWS-S3-bucket%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@jjinwo0/Spring-Boot-AWS-AWS-S3-bucket%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dJRxQ9/hyWdpO4lYF/XsItjYj5tcxUcV5UGn8WF0/img.jpg?width=1374&amp;amp;height=773&amp;amp;face=0_0_1374_773,https://scrap.kakaocdn.net/dn/vOibQ/hyWdn4LLFq/06khWsb32hdmmJkYDJiMYK/img.jpg?width=1374&amp;amp;height=773&amp;amp;face=0_0_1374_773,https://scrap.kakaocdn.net/dn/mjl6P/hyWdjuxuLD/AEoi8RGog2m2i6feVoSVp1/img.png?width=1777&amp;amp;height=807&amp;amp;face=0_0_1777_807');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot &amp;amp; AWS] AWS S3 bucket을 활용한 이미지 업로드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 부가적인 회원 서비스를 깔끔하게 마무리짓기 위해 프로필 사진 업로드를 진행하고자 하였습니다.로컬 환경의 상대 경로를 활용하는 것은 이전에 시도해보아서 익숙했&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1716824925292&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;aws S3, CloudFront, Route53 연동하기&quot; data-og-description=&quot;자체 개발한 플랫폼을 실제 도메인과 서버와 연결하기 위해 S3를 처음 사용해 보았다. 대략적 흐름은 이렇다. - 로컬에서 파일 build (npm build / yarn build) - 생성된 build 폴더 안의 파일들을 S3에 업로&quot; data-og-host=&quot;whoyoung90.tistory.com&quot; data-og-source-url=&quot;https://whoyoung90.tistory.com/19&quot; data-og-url=&quot;https://whoyoung90.tistory.com/19&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/JNWcI/hyV9TEg0AE/sTNbWiRHlW10br3cS3xawk/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/H7T7b/hyWdgq1S77/4hFdowbMkurbyJerOOvly0/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/dRKY63/hyV91PMI4P/AKSD1yUgviLXYWLTBUkg2K/img.png?width=877&amp;amp;height=878&amp;amp;face=0_0_877_878&quot;&gt;&lt;a href=&quot;https://whoyoung90.tistory.com/19&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://whoyoung90.tistory.com/19&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/JNWcI/hyV9TEg0AE/sTNbWiRHlW10br3cS3xawk/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/H7T7b/hyWdgq1S77/4hFdowbMkurbyJerOOvly0/img.jpg?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/dRKY63/hyV91PMI4P/AKSD1yUgviLXYWLTBUkg2K/img.png?width=877&amp;amp;height=878&amp;amp;face=0_0_877_878');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;aws S3, CloudFront, Route53 연동하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자체 개발한 플랫폼을 실제 도메인과 서버와 연결하기 위해 S3를 처음 사용해 보았다. 대략적 흐름은 이렇다. - 로컬에서 파일 build (npm build / yarn build) - 생성된 build 폴더 안의 파일들을 S3에 업로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;whoyoung90.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS S3를 활용하여 설정한 시간동안만 유효한 URL을 생성하는 과정을 설명했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Pre-Signed URL을 사용하면, S3 버킷에 저장된 파일에 대한 접근 권한을 시간 설정과 함께 부여할 수 있다는 점이 매력적이었다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물론 공유페이지 자체는 리액트와 같은 프론트엔드 프레임워크를 사용한게 아니고, 백단에서 한땀한땀 html 코드를 한줄씩 쳐서 구현하는 방식이라 유지보수, 확장성 측면에서는 많이 부족한 부분이 있다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 단순 공유 목적의 페이지를 사용자에게 제공하는 것이었기 때문에 공수가 가장 덜 드는 방법이었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>웹/Spring</category>
      <category>CloudFront</category>
      <category>presigned url</category>
      <category>s3</category>
      <category>URL 공유</category>
      <category>스프링부트</category>
      <category>유효시간 설정 url</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/205</guid>
      <comments>https://2jaebbang.tistory.com/205#entry205comment</comments>
      <pubDate>Mon, 27 May 2024 23:42:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Async를 이용해 비동기 처리를 해보자</title>
      <link>https://2jaebbang.tistory.com/203</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;기존에는 일기를 작성하면 내부적으로는 '일기 생성 -&amp;gt; GPT로 일기 감정 추출 -&amp;gt; Suno AI로 음악생성 -&amp;gt; 일기 생성 완료' 의 로직으로 돌아가고 있어 클라이언트가 응답을 받는데까지 7~8초가 소요됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;따라서 &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;비동기 처리를 활용하면 장기 실행 작업이 메인 스레드를 차단하지 않도록 하여 사용자 응답 시간을 줄이고 시스템 자원을 효율적으로 사용할 수 있도록 구현했다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lhscI/btsHptG9F3V/jwvb65RiPbdBFNdHI5JOO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lhscI/btsHptG9F3V/jwvb65RiPbdBFNdHI5JOO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lhscI/btsHptG9F3V/jwvb65RiPbdBFNdHI5JOO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlhscI%2FbtsHptG9F3V%2Fjwvb65RiPbdBFNdHI5JOO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;474&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비동기 처리를 위한 AsyncConfig 클래스&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비동기 처리를 위한 설정 클래스를 작성한다. AsyncConfig 클래스는 Spring의 비동기 기능을 활성화하고, ThreadPoolTaskExecutor를 설정하여 비동기 작업을 처리할 때 사용할 스레드 풀을 구성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715780782091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = &quot;taskExecutor&quot;)
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 코어 스레드 풀 크기 설정
        executor.setMaxPoolSize(50); // 최대 스레드 풀 크기 설정
        executor.setQueueCapacity(100); // 대기열 크기 설정
        executor.setThreadNamePrefix(&quot;Async-&quot;); // 스레드 이름 접두사 설정
        executor.initialize();
        return executor;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DiaryService 클래스&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DiaryService 클래스는 일기와 관련된 비즈니스 로직을 처리한다. 일기를 생성하고 비동기로 GPT 프롬프트 생성&amp;amp;음악 생성을 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715780973436&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class DiaryService {

    private final UserRepository userRepository;
    private final DiaryRepository diaryRepository;
    private final ChatGPTService chatGPTService;
    private final MusicService musicService;

    public DiaryService(UserRepository userRepository, DiaryRepository diaryRepository, ChatGPTService chatGPTService, MusicService musicService) {
        this.userRepository = userRepository;
        this.diaryRepository = diaryRepository;
        this.chatGPTService = chatGPTService;
        this.musicService = musicService;
    }

    @Transactional
    public DiaryCreateResponseDto createDiary(Long userId, DiaryCreateRequestDto diaryCreateRequestDto) {
        try {
            User user = userRepository.findById(userId)
                    .orElseThrow(() -&amp;gt; new UserException(&quot;해당하는 사용자를 찾을 수 없습니다.&quot;));

            Diary diary = diaryCreateRequestDto.toEntity(diaryCreateRequestDto, user);
            Diary savedDiary = diaryRepository.save(diary);
            return DiaryCreateResponseDto.of(savedDiary);
        } catch (Exception e) {
            throw new DiaryException(&quot;Diary 생성 중 오류가 발생했습니다. &quot; + e.getMessage());
        }
    }

    @Async
    public void handleDiaryCreationAsync(Long diaryId) {
        try {
            generatePromptAsync(diaryId);
            generateMusicAsync(diaryId);
        } catch (Exception e) {
            log.error(&quot;Diary 추가 작업 중 오류가 발생했습니다. &quot; + e.getMessage());
        }
    }

    @Async
    public void generatePromptAsync(Long diaryId) {
        try {
            Diary diary = diaryRepository.findById(diaryId)
                    .orElseThrow(() -&amp;gt; new DiaryException(&quot;Diary not found with id: &quot; + diaryId));

            PromptRequestDto promptRequestDto = new PromptRequestDto(diary.getContent());
            PromptResponseDto promptResponseDto = chatGPTService.sendPrompt(promptRequestDto, PromptType.CONTENT_EMOTION_ANALYSIS);

            String updatedPrompt = diary.getPrompt() + &quot; , &quot; + promptResponseDto.getContent();
            diary.setPrompt(updatedPrompt);

            diaryRepository.save(diary);
        } catch (Exception e) {
            log.error(&quot;GPT 프롬프트 처리 중 오류가 발생했습니다. &quot; + e.getMessage());
        }
    }

    @Async
    public void generateMusicAsync(Long diaryId) {
        try {
            Diary diary = diaryRepository.findById(diaryId)
                    .orElseThrow(() -&amp;gt; new DiaryException(&quot;Diary not found with id: &quot; + diaryId));

            MusicCreateRequestDto musicCreateRequestDto = new MusicCreateRequestDto();
            musicCreateRequestDto.setDiaryId(diary.getId());
            musicCreateRequestDto.setGenerationPrompt(diary.getPrompt());

            musicService.createMusic(musicCreateRequestDto);
        } catch (Exception e) {
            log.error(&quot;음악 생성 중 오류가 발생했습니다. &quot; + e.getMessage());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기술적 인사이트&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비동기 처리의 장점&lt;/b&gt;: 비동기 처리를 통해 여러 작업을 동시에 처리할 수 있으므로, 사용자 요청에 대한 응답 시간을 단축할 수 있었다. 일기 작성 후 감정 분석 및 음악 생성과 같은 시간이 오래 걸리는 작업을 비동기로 처리했기 때문에 UX 측면에서 개선됐다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ThreadPoolTaskExecutor 설정&lt;/b&gt;: 비동기 작업의 효율적인 처리를 위해 ThreadPoolTaskExecutor를 사용하여 스레드 풀을 설정하였다. 코어 스레드 풀 크기, 최대 스레드 풀 크기, 대기열 크기 등을 설정하여 다양한 작업 부하를 처리할 수 있도록 구성했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비동기 메서드 구현&lt;/b&gt;: Spring의 @Async 애노테이션을 사용하여 비동기 메서드를 구현하였다. 일기 생성 후, GPT 프롬프트 생성 및 음악 생성을 비동기 메서드로 처리하여 메인 프로세스의 성능을 저하시키지 않도록 했다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1715781098825&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Async
public void handleDiaryCreationAsync(Long diaryId) {
    try {
        generatePromptAsync(diaryId);
        generateMusicAsync(diaryId);
    } catch (Exception e) {
        log.error(&quot;Diary 추가 작업 중 오류가 발생했습니다. &quot; + e.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹/Spring</category>
      <category>api 비동기처리</category>
      <category>async</category>
      <category>비동기처리</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/203</guid>
      <comments>https://2jaebbang.tistory.com/203#entry203comment</comments>
      <pubDate>Thu, 16 May 2024 21:28:12 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] SunoAI와 Spring Boot를 연동해보자</title>
      <link>https://2jaebbang.tistory.com/202</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이번 글에서는 SunoAI와 Spring Boot를 연동해 사용자 감정에 맞는 음악을 생성하는 기능을 구축하는 방법을 설명하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;SunoAI는 AI를 활용해 음악을 생성하는 서비스로, 사용자의 감정에 맞춘 음악을 제공하는 데 적합하다. 그러나 SunoAI는 공식 API를 제공하지 않기 때문에 SunoAI의 쿠키값을 이용한 GitHub 오픈소스 API를 사용하였다. (EC2 서버에 따로 AI서버 구축한 상태)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이 기능은 일기를 작성한 후 ChatGPT를 이용해 일기의 핵심 단어를 추출하고, 이를 바탕으로 SunoAI가 감정에 맞는 음악을 생성하도록 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1715778931563&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - gcui-art/suno-api: Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs.&quot; data-og-description=&quot;Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs. - gcui-art/suno-api&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gcui-art/suno-api&quot; data-og-url=&quot;https://github.com/gcui-art/suno-api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hhrbq/hyV6ef0LY1/1eRRztQk3j56nKl0VYUcH0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/gcui-art/suno-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gcui-art/suno-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hhrbq/hyV6ef0LY1/1eRRztQk3j56nKl0VYUcH0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - gcui-art/suno-api: Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs. - gcui-art/suno-api&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SunoAIConfig 클래스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SunoAI API와의 연동을 위해 설정 클래스를 작성한다. SunoAIConfig 클래스는 SunoAI API 호출에 필요한 RestTemplate과 HttpHeaders를 설정하고, SunoAI API의 URL을 설정 파일에서 읽어오기 위해 SunoAIProperties 클래스를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715778749510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class SunoAIConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public HttpHeaders httpHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }

    @Getter
    @Setter
    @Component
    @ConfigurationProperties(prefix = &quot;sunoai&quot;)
    public static class SunoAIProperties {
        private String url;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;MusicService 클래스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MusicService 클래스는 SunoAI API를 호출하여 음악을 생성하고, 데이터베이스에 저장하는 비즈니스 로직을 포함하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;음악을 생성하는 createMusic 메서드는 Diary 엔티티를 조회하고, SunoAI API를 호출하여 음악 ID 목록을 받아와 Music 엔티티를 생성하여 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715778804190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MusicService {
    private final SunoAIConfig sunoAIConfig;
    private final SunoAIConfig.SunoAIProperties sunoAIProperties;
    private final MusicRepository musicRepository;
    private final DiaryRepository diaryRepository;

    @Transactional
    public void createMusic(MusicCreateRequestDto musicCreateRequestDto) {
        Diary diary = diaryRepository.findById(musicCreateRequestDto.getDiaryId())
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Diary not found with id: &quot; + musicCreateRequestDto.getDiaryId()));

        SunoAIRequestDto sunoAIRequestDto = new SunoAIRequestDto();
        sunoAIRequestDto.setPrompt(musicCreateRequestDto.getGenerationPrompt());

        List&amp;lt;String&amp;gt; sunoAIMusicIds = generateSunoAIMusic(sunoAIRequestDto);

        if (sunoAIMusicIds.size() &amp;gt;= 2) {
            Music music1 = new Music(diary, musicCreateRequestDto.getGenerationPrompt(), sunoAIMusicIds.get(0));
            Music music2 = new Music(diary, musicCreateRequestDto.getGenerationPrompt(), sunoAIMusicIds.get(1));

            musicRepository.save(music1);
            musicRepository.save(music2);
        } else {
            throw new MusicException(&quot;SunoAI에서 생성된 음악 ID의 수가 부족합니다.&quot;);
        }
    }

    public List&amp;lt;String&amp;gt; generateSunoAIMusic(SunoAIRequestDto sunoAIRequestDto) {
        HttpEntity&amp;lt;SunoAIRequestDto&amp;gt; requestEntity = new HttpEntity&amp;lt;&amp;gt;(sunoAIRequestDto, sunoAIConfig.httpHeaders());
        ResponseEntity&amp;lt;List&amp;lt;SunoAIResponseDto&amp;gt;&amp;gt; response = sunoAIConfig.restTemplate().exchange(
                sunoAIProperties.getUrl() + &quot;/generate&quot;,
                HttpMethod.POST,
                requestEntity,
                new ParameterizedTypeReference&amp;lt;&amp;gt;() {});

        return response.getBody().stream()
                .filter(musicData -&amp;gt; &quot;submitted&quot;.equals(musicData.getStatus()))
                .map(SunoAIResponseDto::getId)
                .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;MusicController 클래스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MusicController 클래스는 API 엔드포인트를 제공한다. 이 컨트롤러는 음악 생성을 위한 generateMusic 메서드를 포함한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715778849373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/music&quot;)
public class MusicController {
    private final MusicService musicService;

    public MusicController(MusicService musicService) {
        this.musicService = musicService;
    }

    @PostMapping(&quot;/generate&quot;)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;SunoAIGenerationResultDto&amp;gt;&amp;gt; generateMusic(@RequestBody SunoAIRequestDto sunoAIRequestDto) {
        try {
            List&amp;lt;String&amp;gt; songIds = musicService.generateSunoAIMusic(sunoAIRequestDto);
            SunoAIGenerationResultDto result = new SunoAIGenerationResultDto(songIds);
            return ResponseEntity.ok(ApiResponse.success(&quot;음악 생성 완료&quot;, result));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(ApiResponse.error(&quot;음악 생성 실패: &quot; + e.getMessage()));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기술적 인사이트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;API 연동의 중요성&lt;/b&gt;: SunoAI는 공식 API를 제공하지 않으므로 SunoAI의 쿠키값을 이용한 GitHub 오픈소스 API를 사용해야 했다. 이 과정에서 API 연동의 중요성을 깨달았다. 다양한 예외 상황을 처리하는 방법을 학습하였고, API 연동이 서비스의 핵심 기능을 수행하는 데 얼마나 중요한지 실감했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;트랜잭션 관리의 필요성&lt;/b&gt;: 음악 생성 과정에서 여러 엔티티를 생성하고 저장하는 부분에서 트랜잭션 관리는 필수적이었다. 이를 통해 데이터 일관성을 유지하고, 예외 발생 시 롤백 처리를 통해 안정성을 확보할 수 있었다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비동기 처리의 가능성&lt;/b&gt;: 음악 생성과 같은 시간이 오래 걸리는 작업을 비동기로 처리하면 시스템 성능을 크게 향상시킬 수 있을 것으로 보인다. 현재는 동기 방식으로 구현돼 있어 일기 요약(GPT4) 3초 + 음악생성(Suno AI) 3초~4초, 약 7~8초 정도 걸리는 상황이다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;향후 비동기(@Async) 처리를 도입하여 사용자 응답 시간을 줄이고, 시스템 자원을 효율적으로 사용할 계획이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>웹/Spring</category>
      <category>API</category>
      <category>Suno AI</category>
      <category>음악생성</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/202</guid>
      <comments>https://2jaebbang.tistory.com/202#entry202comment</comments>
      <pubDate>Wed, 15 May 2024 22:19:53 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Immutable Object(불변객체)를 알아보자</title>
      <link>https://2jaebbang.tistory.com/201</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Immutable 객체란 무엇인가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바에서 객체가 Immutable이라는 것은 해당 객체가 생성된 후 그 상태가 변하지 않는다는 것을 의미한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 객체의 상태(데이터)를 변경할 수 없다는 것이다. 이는 데이터의 무결성을 유지하고, 멀티스레딩 환경에서의 동기화 문제를 해결하는데&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도움이 된다고 한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;string-example.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UbjK8/btsG8ASrWPD/DkR4bSrWwfDpfxFOuBFOzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UbjK8/btsG8ASrWPD/DkR4bSrWwfDpfxFOuBFOzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UbjK8/btsG8ASrWPD/DkR4bSrWwfDpfxFOuBFOzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUbjK8%2FbtsG8ASrWPD%2FDkR4bSrWwfDpfxFOuBFOzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;351&quot; data-filename=&quot;string-example.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Immutable 객체의 이점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 데이터 무결성 보장&lt;/b&gt;: Immutable 객체는 생성 후 상태가 변경되지 않으므로, 예기치 않은 side effect로부터 데이터를 보호할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 스레드 안전&lt;/b&gt;: 여러 스레드에서 동시에 해당 객체를 참조해도 객체가 변경될 수 없으므로 동기화를 신경 쓸 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. 캐싱과 재사용 용이&lt;/b&gt;: Immutable 객체는 그 자체로 고유한 식별자 역할을 할 수 있으며, 캐시에 저장하고 재사용하기에 안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. 디버깅 용이&lt;/b&gt;: 객체의 상태가 변경되지 않기 때문에, 디버깅 시 객체의 상태를 추적하기가 용이하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;자바에서의 Immutable 객체 사용 예&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가장 대표적인 예는 한 번 생성되면 그 값을 변경할 수 없는 &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;String 객체이다&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;String 객체의 불변성 때문에 문자열을 변경할 때마다 실제로는 새로운 String 객체가 생성되기 때문에,&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 반복적인 문자열 연산에서 비효율적일 수 있다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어, s가 &quot;abc&quot;라고 할 때, 여기에 &quot;de&quot;를 더하려면&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새로운 문자열 객체가 생성된다.&lt;/li&gt;
&lt;li&gt;기존 문자열 &quot;abc&quot;의 각 문자 'a', 'b', 'c'가 새로운 문자열 객체로 복사된다.&lt;/li&gt;
&lt;li&gt;새로 추가할 문자열 &quot;de&quot;의 각 문자 'd', 'e'가 복사된 후에 이어서 추가된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 전체 문자열 &quot;abcde&quot;가 새로운 문자열 객체에 저장되며, 이 과정에서 &lt;u&gt;각 문자가 개별적으로 복사된&lt;/u&gt;다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 문자열의 길이가 길어질수록 이 복사하는 작업도 더 많은 시간과 메모리를 요구하게 되며, &lt;span style=&quot;color: #000000;&quot;&gt;큰 반복문 내에서 문자열을 결합하는 작업은 시간 복잡도 &lt;span aria-hidden=&quot;true&quot;&gt;O(n&lt;span style=&quot;text-align: left;&quot;&gt;2&lt;/span&gt;)&lt;/span&gt;를 가지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 해결하기 위해 StringBuffer와 StringBuilder 클래스를 사용하게 되는데, 두 클래스 모두 내부적으로 가변적인 문자 배열을 사용하여 문자열을 조작한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;StringBuffer는&lt;/b&gt; 스레드 안전(thread-safe)을 보장하는 반면, &lt;b&gt;StringBuilder&lt;/b&gt;는 단일 스레드 환경에서 더 빠른 성능을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;직접 Immutable 클래스 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 Immutable 클래스를 만드는 방법은 간단한데, 모든 필드를 final로 선언하고 setter 메소드를 제외, 생성자에서만 수정 가능하도록 구현한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714633602866&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>웹/Java</category>
      <category>immutable object</category>
      <category>string</category>
      <category>불변객체</category>
      <category>시간복잡도</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/201</guid>
      <comments>https://2jaebbang.tistory.com/201#entry201comment</comments>
      <pubDate>Thu, 2 May 2024 16:11:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 애플 소셜 로그인을 구현해보자</title>
      <link>https://2jaebbang.tistory.com/200</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플은 2019년 애플 로그인 기능을 발표한 동시에, App Store에 등록할 때 소셜 로그인이 하나라도 있다면 애플 로그인이 필수로 제공되어야 한다는 심사정책을 내놓았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5월에 MVP 출시 예정인 앱에는 소셜로그인 기능이 들어가기 때문에 애플로그인을 필수로 적용시켜야 하는 상황이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제는 애플로그인이 다른 소셜로그인들과 동작방식이 좀 다르다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-30 오후 11.52.54.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIxr5L/btsG483BGra/rpvZBYW1vjF9dersUZtRa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIxr5L/btsG483BGra/rpvZBYW1vjF9dersUZtRa1/img.png&quot; data-alt=&quot;카카오 소셜로그인 연동 로직&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIxr5L/btsG483BGra/rpvZBYW1vjF9dersUZtRa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIxr5L%2FbtsG483BGra%2FrpvZBYW1vjF9dersUZtRa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;793&quot; height=&quot;484&quot; data-filename=&quot;스크린샷 2024-04-30 오후 11.52.54.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카카오 소셜로그인 연동 로직&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플의 경우에는 아래처럼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 서버 내부에서 별도로 &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;client_secret라는 값을 생성해준 뒤에&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;2. 애플서버에 여러 설정정보를 함께 전달해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;3. 여기서 끝이 아니라 애플서버로부터 전달받은 id_token 값을 파싱해야 비로소 이메일과 같은 사용자 정보를 얻을 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-01 오전 12.25.00.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvmuP2/btsG4zApfKS/mZpQugtLFgB5GnkstT2jgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvmuP2/btsG4zApfKS/mZpQugtLFgB5GnkstT2jgK/img.png&quot; data-alt=&quot;애플 소셜로그인 연동 로직&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvmuP2/btsG4zApfKS/mZpQugtLFgB5GnkstT2jgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvmuP2%2FbtsG4zApfKS%2FmZpQugtLFgB5GnkstT2jgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;545&quot; data-filename=&quot;스크린샷 2024-05-01 오전 12.25.00.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;애플 소셜로그인 연동 로직&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 레퍼런스를 살펴보니까, Identity Token값을 활용해서 구현하는 방법도 있었는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Authorization Code만 클라이언트로부터 전달받아서 구현하는 방법이 그나마 공수가 덜 들 것 같아서 이 방법으로 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[구현코드]&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1714491303242&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    /**
     * Apple 토큰 인증 API 호출 -&amp;gt; 응답받은 ID 토큰을 JWT Decoding 처리 -&amp;gt; AppleUserInfoResponseDto로 반환
     * @param authorizationCode 사용자로부터 받은 인증 코드
     * @return 디코딩된 사용자 정보를 담고 있는 AppleUserInfoResponseDto 객체
     */
    public AppleUserInfoResponseDto getAppleUserProfile(String authorizationCode) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE));
        HttpEntity&amp;lt;String&amp;gt; request = new HttpEntity&amp;lt;&amp;gt;(&quot;client_id=&quot; + appleProperties.getClientId() +
                &quot;&amp;amp;client_secret=&quot; + generateClientSecret() +
                &quot;&amp;amp;grant_type=&quot; + appleProperties.getGrantType() +
                &quot;&amp;amp;code=&quot; + authorizationCode, headers);

        ResponseEntity&amp;lt;AppleSocialTokenInfoResponseDto&amp;gt; response = socialConfig.restTemplate().exchange(
                appleProperties.getAudience() + &quot;/auth/token&quot;, HttpMethod.POST, request, AppleSocialTokenInfoResponseDto.class);

        DecodedJWT decodedJWT = JWT.decode(Objects.requireNonNull(response.getBody()).getIdToken());

        AppleUserInfoResponseDto appleUserInfoResponseDto = new AppleUserInfoResponseDto();

        appleUserInfoResponseDto.setSubject(decodedJWT.getClaim(&quot;sub&quot;).asString());
        appleUserInfoResponseDto.setEmail(decodedJWT.getClaim(&quot;email&quot;).asString());

        return appleUserInfoResponseDto;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714491319700&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   /**
     * Apple의 인증 서버와의 통신에 사용될 JWT을 생성하기 위해 사용되는 ClientSecret
     * ClientSecret은 토큰 요청 시 서명 목적으로 사용되며, 공개키/비공개키 인증 메커니즘이 포함됨
     * @return 생성된 JWT ClientSecret
     */
    private String generateClientSecret() {
        LocalDateTime expiration = LocalDateTime.now().plusMinutes(5);

        return Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId())
                .setIssuer(appleProperties.getTeamId())
                .setAudience(appleProperties.getAudience())
                .setSubject(appleProperties.getClientId())
                .setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
                .setIssuedAt(new Date())
                .signWith(getPrivateKey(), SignatureAlgorithm.ES256)
                .compact();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714491330244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    /**
     * 애플의 JWT 클라이언트 시크릿 생성을 위한 비공개 키 로드
     * @return 로드된 RSA 비공개 키
     */
    private PrivateKey getPrivateKey() {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(&quot;BC&quot;);

        try {
            byte[] privateKeyBytes = Base64.getDecoder().decode(appleProperties.getPrivateKey());
            PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes);
            return converter.getPrivateKey(privateKeyInfo);
        } catch (Exception e) {
            throw new RuntimeException(&quot;Error converting private key from String&quot;, e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714491555444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 애플 유저 정보 DTO
 */
@Builder
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@AllArgsConstructor
@Data
public class AppleUserInfoResponseDto {
    // 고유ID
    @JsonProperty(&quot;sub&quot;)
    private String subject;

    @JsonProperty(&quot;email&quot;)
    private String email;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1714491567133&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 애플 소셜 토큰 응답 정보 DTO
 */
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Data
public class AppleSocialTokenInfoResponseDto {
    @JsonProperty(&quot;access_token&quot;)
    private String accessToken;

    @JsonProperty(&quot;token_type&quot;)
    private String tokenType;

    @JsonProperty(&quot;expires_in&quot;)
    private Long expiresIn;

    @JsonProperty(&quot;refresh_token&quot;)
    private String refreshToken;

    @JsonProperty(&quot;id_token&quot;)
    private String idToken;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[트러블슈팅]&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;애플 서버 /auth/token로 토큰 발급 요청시 Invalid_grant 에러 발생&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. &lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Authorization Code의 유효시간은 5분, 일회용으로 제한되기 때문에 생긴 문제였다. &lt;b&gt;새로 발급받은 코드로 요청&lt;/b&gt;해서 해결했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;2.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;토큰 발급 시 파라미터로 들어가는 client_id값이 App ID가 아닌 Service ID를 사용했기 때문에 발생한 문제였다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;client_id&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;는 애플 개발자 계정에서&amp;nbsp;&lt;/span&gt;Certificates, Identifiers &amp;amp; Profiles&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;-&amp;nbsp;&lt;/span&gt;Identifiers&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;에서 설정한 id인데, &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;아래의 그림을 보면 APP IDs값과 Service IDs값을 사용하는 경우가 나눠져 있었다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-01 오전 12.54.21.png&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cufRTI/btsG35M6hAC/vJd3HgZidymMqoZS0cyamk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cufRTI/btsG35M6hAC/vJd3HgZidymMqoZS0cyamk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cufRTI/btsG35M6hAC/vJd3HgZidymMqoZS0cyamk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcufRTI%2FbtsG35M6hAC%2FvJd3HgZidymMqoZS0cyamk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;208&quot; data-filename=&quot;스크린샷 2024-05-01 오전 12.54.21.png&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단하게 설명하자면 소셜로그인을 ios앱에서 연동한다면 client_id값에 App IDs가 들어가게 되고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;웹사이트나 웹뷰 형식을 통해 연동하면 client_id값에 Service IDs가 들어가게 되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;client_id값에 애플 디벨로퍼에서 설정한 App IDs 값으로 수정&lt;/b&gt;하니까 해결됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[레퍼런스]&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1714490973553&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Sign in with Apple REST API | Apple Developer Documentation&quot; data-og-description=&quot;Communicate between your app servers and Apple&amp;rsquo;s authentication servers.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cDlMT7/hyVZsLTl2y/0VKJeNa7cuy83gRNfkdQ0k/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/wqjZc/hyVVMkWKLZ/LiVeIu1ZSyef1FR2Okrd70/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cDlMT7/hyVZsLTl2y/0VKJeNa7cuy83gRNfkdQ0k/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/wqjZc/hyVVMkWKLZ/LiVeIu1ZSyef1FR2Okrd70/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Sign in with Apple REST API | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Communicate between your app servers and Apple&amp;rsquo;s authentication servers.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714491206344&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정&quot; data-og-description=&quot;[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정 - (현재 글) [2] 스프링 프로젝트에 애플 로그인 API 연동하기 Spring API Server에서 Apple Login API를 연동하여 앱(App)에 제공하기 위&quot; data-og-host=&quot;whitepaek.tistory.com&quot; data-og-source-url=&quot;https://whitepaek.tistory.com/60&quot; data-og-url=&quot;https://whitepaek.tistory.com/60&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Da1ov/hyVZre9xf3/V3AHpB9rdXlIkMEmOvZeP0/img.png?width=800&amp;amp;height=469&amp;amp;face=0_0_800_469,https://scrap.kakaocdn.net/dn/sQ9Kv/hyVVHKH5k3/mGslQciKYkgqyC28KVesPk/img.png?width=800&amp;amp;height=469&amp;amp;face=0_0_800_469,https://scrap.kakaocdn.net/dn/QhIrI/hyVVzTsvu5/gwhdadrtYL878rKjY4PWhK/img.png?width=2463&amp;amp;height=2175&amp;amp;face=0_0_2463_2175&quot;&gt;&lt;a href=&quot;https://whitepaek.tistory.com/60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://whitepaek.tistory.com/60&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Da1ov/hyVZre9xf3/V3AHpB9rdXlIkMEmOvZeP0/img.png?width=800&amp;amp;height=469&amp;amp;face=0_0_800_469,https://scrap.kakaocdn.net/dn/sQ9Kv/hyVVHKH5k3/mGslQciKYkgqyC28KVesPk/img.png?width=800&amp;amp;height=469&amp;amp;face=0_0_800_469,https://scrap.kakaocdn.net/dn/QhIrI/hyVVzTsvu5/gwhdadrtYL878rKjY4PWhK/img.png?width=2463&amp;amp;height=2175&amp;amp;face=0_0_2463_2175');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정 - (현재 글) [2] 스프링 프로젝트에 애플 로그인 API 연동하기 Spring API Server에서 Apple Login API를 연동하여 앱(App)에 제공하기 위&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;whitepaek.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714490939080&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring API서버에서 Apple 인증(로그인 , 회원가입) 처리하기&quot; data-og-description=&quot;들어가며 사이드 프로젝트를 진행하던 도중 APP에서 Apple 로그인을 적용해야 했다. https://apps.apple.com/kr/app/%EA%B8%80%EC%9D%84%EB%8B%B4%EB%8B%A4/id1517289762 &amp;lrm;글을담다 &amp;lrm;마음 속 와 닿은 글을 손쉽게 담는, '&quot; data-og-host=&quot;hwannny.tistory.com&quot; data-og-source-url=&quot;https://hwannny.tistory.com/71&quot; data-og-url=&quot;https://hwannny.tistory.com/71&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VxQfo/hyVVGyhPUK/HyZJMdRQGA8nOZWtkKd8KK/img.png?width=800&amp;amp;height=635&amp;amp;face=0_0_800_635,https://scrap.kakaocdn.net/dn/XzViq/hyVVBX1Q22/K8ipYrC90TwcR9Dd6NEK71/img.png?width=800&amp;amp;height=635&amp;amp;face=0_0_800_635,https://scrap.kakaocdn.net/dn/epWxmn/hyVVFMSyAk/D6c4nKlEBMLTuk9aBj9T91/img.png?width=1274&amp;amp;height=962&amp;amp;face=0_0_1274_962&quot;&gt;&lt;a href=&quot;https://hwannny.tistory.com/71&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hwannny.tistory.com/71&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VxQfo/hyVVGyhPUK/HyZJMdRQGA8nOZWtkKd8KK/img.png?width=800&amp;amp;height=635&amp;amp;face=0_0_800_635,https://scrap.kakaocdn.net/dn/XzViq/hyVVBX1Q22/K8ipYrC90TwcR9Dd6NEK71/img.png?width=800&amp;amp;height=635&amp;amp;face=0_0_800_635,https://scrap.kakaocdn.net/dn/epWxmn/hyVVFMSyAk/D6c4nKlEBMLTuk9aBj9T91/img.png?width=1274&amp;amp;height=962&amp;amp;face=0_0_1274_962');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring API서버에서 Apple 인증(로그인 , 회원가입) 처리하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 사이드 프로젝트를 진행하던 도중 APP에서 Apple 로그인을 적용해야 했다. https://apps.apple.com/kr/app/%EA%B8%80%EC%9D%84%EB%8B%B4%EB%8B%A4/id1517289762 &amp;lrm;글을담다 &amp;lrm;마음 속 와 닿은 글을 손쉽게 담는, '&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hwannny.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714491020040&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[ Spring Boot] OAuth 2.0, JWT를  활용한 애플 로그인 구현 (2-구현편)&quot; data-og-description=&quot;Spring Boot로 애플 로그인 구현하기&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@komment/Spring-Boot-OAuth-2.0-JWT%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-2-%EC%95%A0%ED%94%8C%ED%8E%B8&quot; data-og-url=&quot;https://velog.io/@komment/Spring-Boot-OAuth-2.0-JWT를-활용한-소셜-로그인-구현-2-애플편&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b2Tpzq/hyVZrTKcMC/4l8UEwtaw6xAIz9yIanxfk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/deUcuw/hyVVDBzRfQ/vt5wyMu8ZAYljkYtHeatOK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/iJoor/hyVVMedoSN/DfOYLguxAkKK2FquACsYt0/img.png?width=1358&amp;amp;height=1780&amp;amp;face=0_0_1358_1780&quot;&gt;&lt;a href=&quot;https://velog.io/@komment/Spring-Boot-OAuth-2.0-JWT%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-2-%EC%95%A0%ED%94%8C%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@komment/Spring-Boot-OAuth-2.0-JWT%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-2-%EC%95%A0%ED%94%8C%ED%8E%B8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b2Tpzq/hyVZrTKcMC/4l8UEwtaw6xAIz9yIanxfk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/deUcuw/hyVVDBzRfQ/vt5wyMu8ZAYljkYtHeatOK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/iJoor/hyVVMedoSN/DfOYLguxAkKK2FquACsYt0/img.png?width=1358&amp;amp;height=1780&amp;amp;face=0_0_1358_1780');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[ Spring Boot] OAuth 2.0, JWT를  활용한 애플 로그인 구현 (2-구현편)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot로 애플 로그인 구현하기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714491182290&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Apple 로그인 구현하기&quot; data-og-description=&quot;Spring Boot와 java security로 Apple 로그인 구현하기&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@byeongju/Apple-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@byeongju/Apple-로그인-구현하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxqpMl/hyVZd2g57Z/X02ohEEkx0FQcpEEgokkjK/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502,https://scrap.kakaocdn.net/dn/4DlBA/hyVVDO5gWK/FkTithlmEptKhSKhTNCke0/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502,https://scrap.kakaocdn.net/dn/cmu4gA/hyVVIJDV2i/KeYcJLgbk5WZKelQ0wX7l1/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502&quot;&gt;&lt;a href=&quot;https://velog.io/@byeongju/Apple-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@byeongju/Apple-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxqpMl/hyVZd2g57Z/X02ohEEkx0FQcpEEgokkjK/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502,https://scrap.kakaocdn.net/dn/4DlBA/hyVVDO5gWK/FkTithlmEptKhSKhTNCke0/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502,https://scrap.kakaocdn.net/dn/cmu4gA/hyVVIJDV2i/KeYcJLgbk5WZKelQ0wX7l1/img.png?width=912&amp;amp;height=502&amp;amp;face=0_0_912_502');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apple 로그인 구현하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot와 java security로 Apple 로그인 구현하기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714491086838&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] 애플 로그인을 구현해보자&quot; data-og-description=&quot;번거로운 회원가입 절차없이 버튼 클릭 하나만으로 서비스를 이용할 수 있는 소셜 로그인은 사용자 유입에 도움이 된다.하지만 iOS 앱은 카카오, 구글 로그인과 같은 소셜 로그인을 추가하려면 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@haron/Spring-%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90&quot; data-og-url=&quot;https://velog.io/@haron/Spring-애플-로그인을-구현해보자&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZqR45/hyVVEAs92k/g2mC0v9BrOtnLaUKn9Th1k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/hNYTg/hyVZmdP5GE/0qujWwZs3WoRxirfawbE11/img.png?width=1912&amp;amp;height=956&amp;amp;face=0_0_1912_956,https://scrap.kakaocdn.net/dn/V1WfU/hyVVE8iIHu/bQ5YXqGNjxgNDKVW4C0hF1/img.png?width=1888&amp;amp;height=888&amp;amp;face=0_0_1888_888&quot;&gt;&lt;a href=&quot;https://velog.io/@haron/Spring-%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@haron/Spring-%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZqR45/hyVVEAs92k/g2mC0v9BrOtnLaUKn9Th1k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/hNYTg/hyVZmdP5GE/0qujWwZs3WoRxirfawbE11/img.png?width=1912&amp;amp;height=956&amp;amp;face=0_0_1912_956,https://scrap.kakaocdn.net/dn/V1WfU/hyVVE8iIHu/bQ5YXqGNjxgNDKVW4C0hF1/img.png?width=1888&amp;amp;height=888&amp;amp;face=0_0_1888_888');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] 애플 로그인을 구현해보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;번거로운 회원가입 절차없이 버튼 클릭 하나만으로 서비스를 이용할 수 있는 소셜 로그인은 사용자 유입에 도움이 된다.하지만 iOS 앱은 카카오, 구글 로그인과 같은 소셜 로그인을 추가하려면&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714492920208&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AEbI3/hyVZq8nusX/Dit7mVpPxpMl3O2RyxEDk0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/uQAhl/hyVVKtTAG1/nt823wGhOKnRTeMMWxLkX0/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors#Possible-reasons-for-invalid-client-errors&quot; data-og-host=&quot;developer.apple.com&quot; data-og-description=&quot;Diagnose errors received by the Sign in with Apple client, or its server infrastructure, by identifying the underlying causes of common error codes and explore their potential solutions.&quot; data-og-title=&quot;TN3107: Resolving Sign in with Apple response errors | Apple Developer Documentation&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors#Possible-reasons-for-invalid-client-errors&quot; data-source-url=&quot;https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors#Possible-reasons-for-invalid-client-errors&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AEbI3/hyVZq8nusX/Dit7mVpPxpMl3O2RyxEDk0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/uQAhl/hyVVKtTAG1/nt823wGhOKnRTeMMWxLkX0/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;TN3107: Resolving Sign in with Apple response errors | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Diagnose errors received by the Sign in with Apple client, or its server infrastructure, by identifying the underlying causes of common error codes and explore their potential solutions.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714492924840&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b1KW6e/hyVZnw255E/pKWVvPc0MvRd9UPGGm2Z8K/img.png?width=1069&amp;amp;height=187&amp;amp;face=0_0_1069_187,https://scrap.kakaocdn.net/dn/eoxQjv/hyVVEtGZ6P/EWrwZYnLZUdJwqILKXreUk/img.png?width=1069&amp;amp;height=187&amp;amp;face=0_0_1069_187,https://scrap.kakaocdn.net/dn/bWLHFS/hyVVBwXONA/Ka86cCL50Iy1ILPkqT4brk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot; data-og-url=&quot;https://velog.io/@ncookie/애플-로그인-Invalidgrant-에러&quot; data-og-source-url=&quot;https://velog.io/@ncookie/%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-Invalidgrant-%EC%97%90%EB%9F%AC&quot; data-og-host=&quot;velog.io&quot; data-og-description=&quot;기존 iOS 네이티브 앱에서 애플 로그인을 웹뷰에서 구현했었는데, 이번에 공식 라이브러리를 사용해 다시 구현하기로 했다. 이를 위해 앱 측에서 애플 인가 서버로부터 발급받은 사용자 정보와 a&quot; data-og-title=&quot;[IMAD] 애플 로그인 Invalid_grant 에러&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@ncookie/%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-Invalidgrant-%EC%97%90%EB%9F%AC&quot; data-source-url=&quot;https://velog.io/@ncookie/%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-Invalidgrant-%EC%97%90%EB%9F%AC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b1KW6e/hyVZnw255E/pKWVvPc0MvRd9UPGGm2Z8K/img.png?width=1069&amp;amp;height=187&amp;amp;face=0_0_1069_187,https://scrap.kakaocdn.net/dn/eoxQjv/hyVVEtGZ6P/EWrwZYnLZUdJwqILKXreUk/img.png?width=1069&amp;amp;height=187&amp;amp;face=0_0_1069_187,https://scrap.kakaocdn.net/dn/bWLHFS/hyVVBwXONA/Ka86cCL50Iy1ILPkqT4brk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[IMAD] 애플 로그인 Invalid_grant 에러&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;기존 iOS 네이티브 앱에서 애플 로그인을 웹뷰에서 구현했었는데, 이번에 공식 라이브러리를 사용해 다시 구현하기로 했다. 이를 위해 앱 측에서 애플 인가 서버로부터 발급받은 사용자 정보와 a&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1714492928491&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://forums.developer.apple.com/forums/thread/690273&quot; data-og-source-url=&quot;https://forums.developer.apple.com/forums/thread/690273&quot; data-og-host=&quot;forums.developer.apple.com&quot; data-og-description=&quot;Hi, I have implemented login/signup using apple login in my website and everything is working fine. I have created a service id, created the key, configured the redirect uri and generated the client secret. Now I'm trying to implement the login on my iOS a&quot; data-og-title=&quot;Apple SignIn invalid grant from iOS | Apple Developer Forums&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://forums.developer.apple.com/forums/thread/690273&quot; data-source-url=&quot;https://forums.developer.apple.com/forums/thread/690273&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Apple SignIn invalid grant from iOS | Apple Developer Forums&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Hi, I have implemented login/signup using apple login in my website and everything is working fine. I have created a service id, created the key, configured the redirect uri and generated the client secret. Now I'm trying to implement the login on my iOS a&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;forums.developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>웹/Spring</category>
      <category>invalid_grant</category>
      <category>oauth</category>
      <category>소셜 연동</category>
      <category>스프링부트</category>
      <category>애플로그인</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/200</guid>
      <comments>https://2jaebbang.tistory.com/200#entry200comment</comments>
      <pubDate>Wed, 1 May 2024 00:59:26 +0900</pubDate>
    </item>
    <item>
      <title>[Java] compareTo와 compare 메서드의 차이</title>
      <link>https://2jaebbang.tistory.com/196</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Comparable (compareTo())&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;자기 자신과 매개변수 객체를 비교&quot;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718591978991&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public int compareTo(Cls cls) {
    if (this.number &amp;gt; cls.number) 
        return 1; // 비교주체가 더 크면 양수
    else if (this.number == cls.number) 
        return 0; // 같으면 0
    else 
        return -1; // 비교값이 더 크면 음수
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Comparator (compare())&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&quot;두 매개변수 객체를 비교&quot;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718591987257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public int compare(Cls o1, Cls o2) {
    if (o1.number &amp;gt; o2.number) 
        return 1; // 첫번째 인자가 두번째 인자보다 크면 양수
    else if (o1.number == o2.number)
        return 0; // 같으면 0
    else (o1.number &amp;lt; o2.number) 
        return -1; // 두번째 인자가 첫번째 인자보다 크면 음수
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹/Java</category>
      <category>arrays</category>
      <category>compare</category>
      <category>compateto</category>
      <category>정렬</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/196</guid>
      <comments>https://2jaebbang.tistory.com/196#entry196comment</comments>
      <pubDate>Thu, 28 Dec 2023 07:35:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Concurrency/Rate Limiter를 적용하여 API 요청을 제어해보자</title>
      <link>https://2jaebbang.tistory.com/190</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최근 프로젝트에서 API 요청 수를 제어하기 위해 Rate Limiter를 적용하는 작업을 수행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 과정에서 겪은 문제점과 해결하기 위해 학습한 내용을 정리해보고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;* 업무 환경은 Laravel 프레임워크를 쓰고 있기 때문에, 지금 설명하려는 Spring에서의 Rate Limiter 구현과는 적용 알고리즘 정도의 차이가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Problem&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 사용자 계정별 글 작성 수 제한 정책 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자마다 시간 당 글 작성 수를 제한해야 하는 기획팀의 요구사항이 있었다. (아래처럼 자동화 공격이나 도배성 뻘글 방지 목적도 있었다.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1DA52AD2-3756-4282-B1B1-5782D280006A_1_105_c.jpeg&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zrvaD/btsH8i5jKnd/TTSCbKYwjEKfnmQn8pDlj1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zrvaD/btsH8i5jKnd/TTSCbKYwjEKfnmQn8pDlj1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zrvaD/btsH8i5jKnd/TTSCbKYwjEKfnmQn8pDlj1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzrvaD%2FbtsH8i5jKnd%2FTTSCbKYwjEKfnmQn8pDlj1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;144&quot; height=&quot;312&quot; data-filename=&quot;1DA52AD2-3756-4282-B1B1-5782D280006A_1_105_c.jpeg&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. AI 이미지 생성 API 다중 호출 시 서버 과부하로 인한 타임아웃 이슈&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 운용 중인 단일 AI서버에서 이미지를 하나 생성하는데는 약 5~6초가 걸린다.&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;다수의&amp;nbsp;&lt;/span&gt;AI 이미지 생성 요청이 들어올 경우,&amp;nbsp;응답 지연으로 인해&amp;nbsp;커넥션 타임아웃되는 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Solution&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 문제들을 모두 해결할 수 있는 방법을 찾던 중, '&lt;b&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초'&amp;nbsp;&lt;/b&gt;책에서&lt;b&gt;&amp;nbsp;'&lt;/b&gt;&lt;b&gt;Rate Limiter'&lt;/b&gt;&amp;nbsp;라는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결론부터 말하자면, 애플리케이션 단에서 &lt;b&gt;Redis + Rate Limit을 적용하여 1번 문제를 해결&lt;/b&gt;했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2번 문제는 Concurrency Limit 방식으로 해결&lt;/b&gt;했다. 현재 실행 중인 요청 수를 실시간으로 관리함으로써, 특정 시점에 동시에 처리할 수 있는 최대 요청 수를 제한하도록 하는 것이다. 요청이 들어오면 Redis에 카운트를 증가시키고, 응답을 보내면 카운트를 감소시키는 방식으로 구현 자체는 상당히 쉬웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Rate Limit 이란?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;API 또는 웹 서비스에서 특정 시간 동안 허용되는 최대 요청 횟수를 제한하는 기능&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 서비스 제공자가 서비스의 과부하를 방지하고, 무제한적인 요청을 방지하기 위해 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-21 오후 9.41.22.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpvLuu/btsH82HNIUx/t0mGwyiSbLORtwn8Az00eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpvLuu/btsH82HNIUx/t0mGwyiSbLORtwn8Az00eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpvLuu/btsH82HNIUx/t0mGwyiSbLORtwn8Az00eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpvLuu%2FbtsH82HNIUx%2Ft0mGwyiSbLORtwn8Az00eK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;471&quot; height=&quot;1054&quot; data-filename=&quot;스크린샷 2024-06-21 오후 9.41.22.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;제한 조건을 정해야한다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유저의 요청 수가 임계치를 넘었을때 제한 하는 방식은 크게 3가지로 나눠볼 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 공통적으로는 유저의 요청수가 초과하면 &lt;b&gt;HTTP Status 429 - Too many requests &lt;/b&gt;를 리턴하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. Hard Throttling :&lt;/b&gt; rate limit을 엄격하게 관리 (1개라도 넘으면 안됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. Soft Throttling :&lt;/b&gt; 설정 %까지는 rate limit을 넘기는 것을 허용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. Elastic or Dynamic Throttling :&lt;/b&gt; 서버 자원에 여유가 있을 때는 rate limit을 넘기더라도 요청을 허용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 팀은 보수적인 정책으로 1번을 채택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기능 / 운영 측면에서의 조건&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Rate limit을 구현할 때 요구 조건은 크게 구현과 운영의 측면으로 나누어 고려할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기능 측면&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 클라이언트가 서버에 요청할 수 있는 횟수를 특정 임계치까지만 허용한다. (예: 1분당 40회)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 클라이언트의 요청횟수가 정책적으로 정해둔 임계치를 넘어설 경우에는 에러를 리턴해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;운영 측면&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Rate Limit이 동작하는 서버는 안정적으로 동작해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Rate Limit서버로 인해 서버의 처리량이 과도하게 늘어나거나 응답시간이 지연되지 않고 빠르게 Rate Limit을 처리할 수 있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Rate Limit 알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. Token Bucket&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 버켓 안에 토큰을 담아두고, 요청마다 토큰을 소비하여 버켓 안의 토큰이 모두 소진되면 요청 제한&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 다음 refill 시간까지 요청 불가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;*메모리는 효율적이지만, 분산 환경에서 동시에 들어오는 요청에 대해 Race condition 발생 가능성 있음.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7K6ud/btsH8MZo7Q7/RbvIMQUXcgUxNXg81YaCDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7K6ud/btsH8MZo7Q7/RbvIMQUXcgUxNXg81YaCDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7K6ud/btsH8MZo7Q7/RbvIMQUXcgUxNXg81YaCDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7K6ud%2FbtsH8MZo7Q7%2FRbvIMQUXcgUxNXg81YaCDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;462&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2.&amp;nbsp; Leaky Bucket&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- FIFO 형식의 Bucket Queue에 요청을 순서대로 담아 크기가 다 차면 요청 제한&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 다음 refill 시간이 되면 가장 먼저 들어온 요청 제거 후 신규 요청을 담음 (다 찬 상태에서 신규 요청은 버려짐)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfaqnW/btsH9K7oF4u/vtkC9IsrGOnHAWFFyacSE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfaqnW/btsH9K7oF4u/vtkC9IsrGOnHAWFFyacSE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfaqnW/btsH9K7oF4u/vtkC9IsrGOnHAWFFyacSE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfaqnW%2FbtsH9K7oF4u%2FvtkC9IsrGOnHAWFFyacSE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;287&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. Fixed Window Counter&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 고정된 크기 내에 일정 요청 개수만을 허용 (Twitter API에 &lt;a style=&quot;color: #000000;&quot; title=&quot;rate limits&quot; href=&quot;https://developer.x.com/en/docs/twitter-api/rate-limits#v2-limits%20%EC%B6%9C%EC%B2%98:%20https://gngsn.tistory.com/224%20[ENFJ.dev:%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC]&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;rate limits&lt;/a&gt; 기능이 있다고 함)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 특정 API를 1분 동안 5개의 요청만 받는다고 설정하면, 해당 시간동안 5개의 요청을 받은 뒤 접근 제한 처리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;* 특정 간격 사이에 부하가 올 가능성 있음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(11:00:58 에 5개의 요청 &amp;amp; 11:01:02 에 5개의 요청 들어올 경우 4초 동안 10개의 요청이 들어오는 편향 케이스 발생)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (2).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5u3t3/btsH8FzlyhL/OKiKGlF2oPPYFa2KxnHFnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5u3t3/btsH8FzlyhL/OKiKGlF2oPPYFa2KxnHFnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5u3t3/btsH8FzlyhL/OKiKGlF2oPPYFa2KxnHFnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5u3t3%2FbtsH8FzlyhL%2FOKiKGlF2oPPYFa2KxnHFnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;170&quot; data-filename=&quot;다운로드 (2).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. Sliding Logs&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 모든 요청을 타임스탬프로 기록하고 현재 시점을 기준으로 시간 범위 내의 요청 개수에 따라 요청 제한&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 1분에 5개의 요청을 받는다고 할때, 현재 시간 ~ 1분 전의 요청 확인 (Log: Key-요청자, Value-타임스탬프)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 즉, 1분 사이에 2개의 요청만 인입돼 있으면 3개의 요청 추가 가능&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;* 유저 각각의 타임스탬프를 모두 로깅하기 때문에 메모리 비효율적&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (4).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzaXC5/btsH71JQrV3/IPJnQXcKxI7kZKXYT5GSfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzaXC5/btsH71JQrV3/IPJnQXcKxI7kZKXYT5GSfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzaXC5/btsH71JQrV3/IPJnQXcKxI7kZKXYT5GSfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzaXC5%2FbtsH71JQrV3%2FIPJnQXcKxI7kZKXYT5GSfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;205&quot; data-filename=&quot;다운로드 (4).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5. Sliding Window Counter&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Fixed Window + Sliding Log. 타임스탬프를 모두 저장하지는 않고 카운터로 요청 개수에 따라 요청 제한&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Fixed Window의 경계점 부하 문제를 Sliding Log와 함께 범위를 조금씩 '움직이는 시간' 단위로 측정.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;* 요청자 별 모든 타임스탬프 대신 카운트만 저장하기 때문에 메모리 효율성 개선&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (5).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bey4kd/btsH9uDMBhd/RPNN7HthoXAc3RBnc5qaYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bey4kd/btsH9uDMBhd/RPNN7HthoXAc3RBnc5qaYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bey4kd/btsH9uDMBhd/RPNN7HthoXAc3RBnc5qaYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbey4kd%2FbtsH9uDMBhd%2FRPNN7HthoXAc3RBnc5qaYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;566&quot; height=&quot;171&quot; data-filename=&quot;다운로드 (5).png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어, &lt;b&gt;1분에 10개의 요청&lt;/b&gt;을 받는다고 가정해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;11:00:00 ~ 11:01:00 사이에 9건&lt;/u&gt;, &lt;u&gt;11:01:00 ~ 11:02:00 사이에 5건&lt;/u&gt;의 요청이 들어왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때, &lt;b&gt;11:01:15 에 새로운 요청이 인입&lt;/b&gt;된다고 하면 &lt;u&gt;시간 범위로 따졌을 때 25% 지점에 위치&lt;/u&gt;하게 되고, 그 전 &lt;u&gt;11:00:00 ~ 11:01:00의 시간 범위는 75%의 비중을 차지&lt;/u&gt;하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;계산하게 되면 &lt;b&gt;9 X 0.75 + 5 = 14.75 가 되고, 10건의 제한을 넘었기 때문에 11:01:15 의 요청은 거부&lt;/b&gt;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 케이스로, &lt;b&gt;11:01:30 에 새로운 요청이 인입&lt;/b&gt;된다고 하면 &lt;u&gt;시간 범위로 50% 지점에 위치&lt;/u&gt;하고, 그 전 &lt;u&gt;11:00:00 ~ 11:01:00의 비중은 50%를 차지&lt;/u&gt;하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;9 X 0.5 + 5 = 9.5 가 되므로 11:01:30 의 요청은 승인&lt;/b&gt;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;일시적으로 많은 트래픽이 와도 토큰이 있다면 처리가 가능하면서 토큰 손실 처리를 통해 평균 처리 속도를 제한할 수 있다. 즉, 평균 유입 속도를 제한하고 처리 패킷 손실없이 특정 수준의 버스트 요청 허용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 처음 문제점에 어떤 알고리즘을 적용할지 선택해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[사용자 계정별 글 작성 수 제한 정책 추가  문제]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;토큰 버킷 알고리즘&lt;/b&gt;을 적용하여 일정한 속도로 토큰을 추가하고, 사용자가 글을 작성할 때 토큰을 소모하게 한다. 이를 통해 사용자 계정별로 글 작성 수를 제어할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;글 작성 자체는 burst 트래픽(일시적인 트래픽 폭주)가 없었기 때문에, 허용하면서도 일정 기간 동안의 최대 요청 수를 제한하는 데 효과적이라고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*Laravel에서 지원하는 RateLimiter 기능 (ThrottleRequests) 은 &lt;b&gt;Fixed Window Counter 알고리즘&lt;/b&gt;&amp;nbsp;기반.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Race Condition에 대응하자&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그런데 아까 보면 토큰 버킷 알고리즘을 사용할 경우 &lt;b&gt;Race Condition&lt;/b&gt;에 빠질 수 있다고 했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #343a40; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스에서 카운터의 값을 읽음 (counter)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;counter +1의 값이 임계치를 넘는지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;넘지 않는다면 레디스에 보관된 카운터 값 +1&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by9Zjp/btsH8IbUbB2/2avQzG8MDbN8rHqvvoHOQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by9Zjp/btsH8IbUbB2/2avQzG8MDbN8rHqvvoHOQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by9Zjp/btsH8IbUbB2/2avQzG8MDbN8rHqvvoHOQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby9Zjp%2FbtsH8IbUbB2%2F2avQzG8MDbN8rHqvvoHOQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;275&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Concurrency Limit 케이스이긴 하지만, 2번 문제에서 요청 수를 초과했지만 그대로 AI이미지 생성 API에 인입되는 것을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;경쟁 조건 문제를 해결하는 가장 널리 알려진 해결책은 Lock이지만, 싱글스레드로 작동하게 될 수 있어 시스템의 성능을 떨어뜨릴 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #343a40; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해결책은 두가지가 있는데 그중에서 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;루아 스크립트&lt;/span&gt;를 적용해보자.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #343a40; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;루아 스크립트(Lua script)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 자료구조의 정렬집합(sorted set)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;루아 스크립트&lt;/span&gt;를 쓰면 어떤 일이 일어날까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;루아 스크립트가 실행되는 동안, 레디스는 blocked 상태가 된다고 한다. 즉 레디스는 루아 스크립트가 Atomic하게 실행되는 걸 보장하기 때문에, 분산 서버 환경 등에서 용이하게 쓰인다고 한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*루아 스크립트는 '원자적' 처리 목적이기 때문에, 동시성 제어를 우선시하는 분산 락과는 차이가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원자성 처리&lt;/b&gt;: 트랜잭션 내의 모든 연산이 전부 수행되거나 전혀 수행되지 않도록 보장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동시성 제어&lt;/b&gt;: 여러 트랜잭션이 동시에 수행될 때 데이터베이스의 일관성과 무결성을 유지.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1718985538913&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Lua 스크립트를 이용한 토큰 버킷 알고리즘 구현

-- Redis 키
local key = KEYS[1]
-- 버킷의 최대 용량
local capacity = tonumber(ARGV[1])
-- 리필되는 토큰 수
local refill_tokens = tonumber(ARGV[2])
-- 토큰이 리필되는 간격 (초 단위)
local refill_interval = tonumber(ARGV[3])
-- 현재 시간 (초 단위)
local current_time = tonumber(ARGV[4])

-- 현재 토큰 수와 마지막 리필 시간을 Redis에서 가져옴
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill_time')
local tokens = tonumber(bucket[1])
local last_refill_time = tonumber(bucket[2])

-- 버킷이 존재하지 않는 경우 초기화
if tokens == nil then
    tokens = capacity
    last_refill_time = current_time
end

-- 토큰 리필
local elapsed = current_time - last_refill_time
if elapsed &amp;gt;= refill_interval then
    local new_tokens = math.min(capacity, tokens + math.floor(elapsed / refill_interval) * refill_tokens)
    tokens = new_tokens
    last_refill_time = current_time
end

-- 토큰이 충분한지 확인
if tokens &amp;gt; 0 then
    tokens = tokens - 1
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill_time', last_refill_time)
    return 1 -- 요청 허용
else
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill_time', last_refill_time)
    return 0 -- 요청 거부
end&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Rate Limiter 라이브러리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Bucket4j&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;lock-free한 구현으로&amp;nbsp;&lt;/span&gt;&lt;b&gt;멀티 스레딩 환경에서의 확장성이 우수&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;하며, 추가적인 동시성 전략도 제공&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;고성능과 높은 동시성이 요구되는 시스템에 적합&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Guava&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;1a37&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰 버킷 알고리즘 기반 구현&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;guava 자체는 처리율 제한 목적은 아님 (기능은 존재)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;단일 인스턴스에서 속도 제한을 적용할 경우, 간단한 프로젝트에 적합&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;동시성 제어가 약하고, 분산 시스템에 부적합&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;7ec0&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. RateLimitJ&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;a2ce&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Sliding Window 알고리즘 기반 구현&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;더 이상 지원하지 않음 (EOL)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;97df&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Resilience4j&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;3543&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MSA 구축 목적 라이브러리&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java17 이상 (Spring Boot3) 부터만 사용 가능&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;ff40&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;번외. Spring Cloud Gateway (API Gateway)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;ec11&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API 게이트웨이로서 레이트 리미팅, 로드 밸런싱, 회로 차단 등의 기능을 제공&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Boot 기반의 프로젝트에서 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Custom Redis Rate Limiter (Token Bucket)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1718963591177&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class TokenBucket extends RateLimiter {
    private int tokens; // 현재 토큰 수
    private final int capacity; // 버킷의 최대 용량
    private long lastRefillTime; // 마지막으로 토큰이 리필된 시간
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate; // Redis 템플릿
    private final String redisKey; // Redis에서 사용할 키

    @Autowired
    public TokenBucket(int maxRequestPerSec, RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate, String key) {
        super(maxRequestPerSec);
        this.tokens = maxRequestPerSec;
        this.capacity = maxRequestPerSec;
        this.lastRefillTime = scaledTime();
        this.redisTemplate = redisTemplate;
        this.redisKey = key + &quot;:tokens&quot;;
        // Redis에 초기 토큰 수 설정 (설정되어 있지 않은 경우)
        if (redisTemplate.opsForValue().get(this.redisKey) == null) {
            redisTemplate.opsForValue().set(this.redisKey, this.tokens, 1, TimeUnit.HOURS); // 예시로 1시간 TTL 설정
        }
    }

    @Override
    public boolean allow() {
        synchronized (this) {
            refillTokens(); // 토큰 리필
            // Redis에서 현재 토큰 수를 가져옴
            Integer currentTokens = (Integer) redisTemplate.opsForValue().get(this.redisKey);
            if (currentTokens == null || currentTokens == 0) {
                return false; // 토큰이 없으면 요청 거부
            }
            // Redis에서 토큰 수를 감소
            redisTemplate.opsForValue().decrement(this.redisKey);
            return true; // 요청 허용
        }
    }

    private void refillTokens() {
        final long now = scaledTime(); // 현재 시간을 가져옴
        if (now &amp;gt; this.lastRefillTime) {
            final double elapsedTime = (now - this.lastRefillTime);
            int refill = (int) (elapsedTime * this.maxRequestPerSec);
            // Redis에서 현재 토큰 수를 가져옴
            Integer currentTokens = (Integer) redisTemplate.opsForValue().get(this.redisKey);
            if (currentTokens == null) {
                currentTokens = this.tokens;
            }
            // 새로운 토큰 수를 계산
            int newTokenCount = Math.min(currentTokens + refill, this.capacity);
            // Redis에 토큰 수를 업데이트
            redisTemplate.opsForValue().set(this.redisKey, newTokenCount, 1, TimeUnit.HOURS); // 예시로 1시간 TTL 설정
            this.lastRefillTime = now; // 마지막 리필 시간을 업데이트
        }
    }

    private long scaledTime() {
        return System.currentTimeMillis() / 1000; // 현재 시간을 초 단위로 반환
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5가지 라이브러리 중, 확장성이 가장 훌륭한 Bucket4J를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 토큰 버킷 알고리즘을 쓰기 때문에, 일정한 속도로 토큰을 추가하고 요청 시 토큰을 소모하는 방식으로 작동한다.&lt;br /&gt;lock-free 상태에서 필요한 경우에 사용할 수 있는 동시성 전략을 제공하고, 레퍼런스 수가 가장 많았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.baeldung.com/spring-bucket4j&quot;&gt;https://www.baeldung.com/spring-bucket4j&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Bucket4J Class&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Refill : &lt;/b&gt;일정시간마다 몇개의 Token을 충전할지 지정하는 클래스&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Bandwidth : &lt;/b&gt;Bucket의 총 크기를 지정하는 클래스 (Token 충전 주기 및 개수를 지정한 Refill 클래스 사용하여 만듦)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Bucket&amp;nbsp; : &lt;/b&gt;최종적으로 트래픽 제어를 할 클래스(Bandwidth 클래스를 사용하여 빌드)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718962538293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.github.bucket4j.Bandwidth;
import com.github.bucket4j.Bucket;
import com.github.bucket4j.Bucket4j;
import com.github.bucket4j.Refill;
import com.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager;

@Service
public class RateLimiterService {
    private RedisClient redisClient;
    private StatefulRedisConnection&amp;lt;String, String&amp;gt; connection;
    private LettuceBasedProxyManager&amp;lt;String&amp;gt; buckets;
    private final Map&amp;lt;String, Bucket&amp;gt; cache = new ConcurrentHashMap&amp;lt;&amp;gt;();

    @PostConstruct
    public void init() {
        redisClient = RedisClient.create(&quot;redis://localhost:6379&quot;);
        connection = redisClient.connect();
        buckets = new LettuceBasedProxyManager&amp;lt;&amp;gt;(connection.sync());
    }

    // API별로 다른 버킷을 생성하는 메서드
    private Bucket createBucketForApi(String api) {
        switch (api) {
            case &quot;api1&quot;:
                return Bucket4j.builder()
                        .addLimit(Bandwidth.classic(10, Refill.greedy(10, Duration.ofMinutes(1))))
                        .build();
            case &quot;api2&quot;:
                return Bucket4j.builder()
                        .addLimit(Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(5))))
                        .build();
            default:
                return Bucket4j.builder()
                        .addLimit(Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1))))
                        .build();
        }
    }

    // API별로 요청이 허용되는지 확인하는 메서드
    public boolean isAllowed(String api, String key) {
        String bucketKey = api + &quot;:&quot; + key;
        Bucket bucket = cache.computeIfAbsent(bucketKey, k -&amp;gt; buckets.builder().build(bucketKey, () -&amp;gt; createBucketForApi(api)));
        return bucket.tryConsume(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1718962563735&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api&quot;)
public class ApiController {
    private final RateLimiterService rateLimiterService;

    @Autowired
    public ApiController(RateLimiterService rateLimiterService) {
        this.rateLimiterService = rateLimiterService;
    }

    // API1 엔드포인트
    @GetMapping(&quot;/resource1&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; getResource1(@RequestParam String clientId) {
        boolean allowed = rateLimiterService.isAllowed(&quot;api1&quot;, clientId);

        if (!allowed)
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(&quot;Too many requests for API1&quot;);

        return ResponseEntity.ok(&quot;Request accepted for API1&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-21 오후 7.11.10.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C0ay8/btsH8gzXSC6/ChkWX3IKS7PJZKWIhTlGpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C0ay8/btsH8gzXSC6/ChkWX3IKS7PJZKWIhTlGpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C0ay8/btsH8gzXSC6/ChkWX3IKS7PJZKWIhTlGpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC0ay8%2FbtsH8gzXSC6%2FChkWX3IKS7PJZKWIhTlGpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;72&quot; data-filename=&quot;스크린샷 2024-06-21 오후 7.11.10.png&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;클라이언트에게 자신의 요청이 제한에 걸리고 있는지 알려주자&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. Http response header를 활용한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #303030; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;X-Ratelimit-Remaining: 윈도 내 남은 처리가능 요청 수&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;X-Ratelimit-Limit: 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;X-Ratelimit-Retry-After: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 보내야하는 지 알림&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. API Response body로 알려주자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Result&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일정 기간 동안의 최대 요청 수 제어가 가능해졌다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API 요청 대기로 인한 타임아웃 문제를 사전 방지할 수 있게 되었다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-21 오후 11.54.43.png&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUqXNk/btsH8xhmttw/MZTdkGJvZqWIakB7kZSGak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUqXNk/btsH8xhmttw/MZTdkGJvZqWIakB7kZSGak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUqXNk/btsH8xhmttw/MZTdkGJvZqWIakB7kZSGak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUqXNk%2FbtsH8xhmttw%2FMZTdkGJvZqWIakB7kZSGak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;578&quot; data-filename=&quot;스크린샷 2024-06-21 오후 11.54.43.png&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;왜 애플리케이션 단에서 Rate Limit 처리를 했는가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위치&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.8604%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3489%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단점&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.8604%; height: 19px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25.3489%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 위변조 쉬움&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 요청 통제 쉽지 않음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;미들웨어&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.8604%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center;&quot;&gt;- API Gateway 지원되기 때문에 구현 용이&lt;br /&gt;- MSA 환경 추천&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3489%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;- 커스텀의 한계&lt;br /&gt;- API Gateway에 이슈 발생시 전체 처리율 제한에 영향 가능성&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.8604%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;- Rate Limit 알고리즘 선택 가능&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3489%; height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Rate Limit 로직 구현 필요&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;- 사용 중인 PGL이 서버측 구현을 지원할 만큼 효율이 높은지 검토가 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주도적으로 애플리케이션 내에서 주도적으로 Rate Limit 관리가 가능하다는점이 가장 컸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 상황에 맞는 Rate Limit의 알고리즘을 직접 선택할 수 있고, 커스텀이 용이(ex. 사용자의 요청 수를 보고 계정을 제제시킨다던지, 적절한 에러 메시지 처리) 하다는 점이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;s&gt; 라이브러리도 워낙 잘 나와있어서 적용하기 너무 쉬움&lt;/s&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;애플리케이션 앞단에서 Rate Limit 처리 할 경우&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. API Gateway&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API Gateway는 여러 서비스나 API에 대한 단일 진입점 역할을 하는 서버이다. API Gateway는 클라이언트 요청을 수신하고, 이를 적절한 서비스로 라우팅하는 기능을 제공한다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;라우팅&lt;/b&gt;: 클라이언트 요청을 적절한 서비스나 API로 라우팅&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인증 및 권한 부여&lt;/b&gt;: 클라이언트 요청을 인증하고, 권한을 부여&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: 여러 서버에 요청을 분산시켜 부하를 고르게 분산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;속도 제한&lt;/b&gt;: 요청 수를 제한하여 서버 과부하를 방지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;데이터 변환&lt;/b&gt;: 요청과 응답 데이터를 변환하여 클라이언트와 서비스 간의 통신을 용이하게 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로깅 및 모니터링&lt;/b&gt;: 요청과 응답을 기록하고, 시스템 상태를 모니터링&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. nginx Rate Limit&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;nginx Rate Limit은 Leaky Bucket 알고리즘과 유사하게 동작된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;nginx에는 요청 수를 제한하는 limit_req, 커넥션 수를 제한하는 limit_conn 모듈을 제공하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. limit_req (요청 제한)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718971714224&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http { 
	# 클라이언트 IP 주소를 기반으로 요청 속도를 제한하기 위한 영역 정의
	limit_req_zone $binary_remote_addr zone=request_limit_per_ip:10m rate=10r/s;

	server {
    	location / {
        	# 정의된 영역을 사용하여 요청 속도 제한 적용
        	limit_req zone=request_limit_per_ip burst=10 nodelay;
    	}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. limit_conn (커넥션 제한)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1718971827827&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http { 
	# 서버 이름을 기반으로 연결 수를 제한하기 위한 영역 정의
	limit_conn_zone $server_name zone=request_limit_per_server:10m;

	server {
    	location / {
        	# 정의된 영역을 사용하여 각 서버의 최대 연결 수를 10으로 제한
        	limit_conn request_limit_per_server 10;
    	}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;왜 메시지 큐는 고려 대상에서 제외되었는가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음에 나온 아이디어는 RabbitMQ와 같은 Message Queue였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;* 메시지 큐는 대용량 데이터를 처리하기 위한 배치 작업, 채팅 서비스, 비동기 데이터를 처리할 때 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 많아지거나 데이터가 많아지면 응답 지연으로 서비스가 정상적으로 작동하지 못하는 상황이 올 수 있기 때문에, 기존에 분산되어 있던 데이터 처리를 한 곳에 집중하면서 메시지 브로커를 통해 필요한 프로그램에 작업을 분산시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메시지 큐는 비동기 작업 처리와 분산 시스템에서의 트래픽 제어에 효과적이지만, 이번 문제를 해결하는 데 적합하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;b&gt;. 실시간 요청 제어의 필요성&lt;/b&gt;: 가장 큰 원인이었다. 메시지 큐를 사용하면 요청을 큐에 넣고 비동기적으로 처리하게 되어, 사용자에게 즉각적인 응답을 제공하기 어렵고, 실시간 API 요청 수 관리가 불가하기 때문에 기존 기획 의도와는 방향성이 틀어질 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 복잡성 증가&lt;/b&gt;: 프로모션이 얼마 남지 않은 상태였기 때문에, RabbitMQ를 탑재해서 큐에 메시지를 넣고 꺼내는 작업, 메시지 처리의 신뢰성 보장, 큐 관리 등 Rate Limiter에 비해 공수가 많이 든다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1719043019047&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;라라벨 8.x - 속도 제한&quot; data-og-description=&quot;라라벨 한글 메뉴얼 8.x - 속도 제한&quot; data-og-host=&quot;laravel.kr&quot; data-og-source-url=&quot;https://laravel.kr/docs/8.x/rate-limiting&quot; data-og-url=&quot;http://laravel.kr/docs/8.x/rate-limiting&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dqdNPH/hyWoKs7Dts/QG1A6AxuTvdhcnOGkqjDN0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://laravel.kr/docs/8.x/rate-limiting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://laravel.kr/docs/8.x/rate-limiting&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dqdNPH/hyWoKs7Dts/QG1A6AxuTvdhcnOGkqjDN0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;라라벨 8.x - 속도 제한&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;라라벨 한글 메뉴얼 8.x - 속도 제한&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;laravel.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719043244478&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;라라벨 8.x - 라우팅&quot; data-og-description=&quot;라라벨 한글 메뉴얼 8.x - 라우팅&quot; data-og-host=&quot;laravel.kr&quot; data-og-source-url=&quot;https://laravel.kr/docs/8.x/routing&quot; data-og-url=&quot;http://laravel.kr/docs/8.x/routing&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chaUvt/hyWoPBabRz/77BiRmJyG9Js958A5jgroK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://laravel.kr/docs/8.x/routing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://laravel.kr/docs/8.x/routing&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chaUvt/hyWoPBabRz/77BiRmJyG9Js958A5jgroK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;라라벨 8.x - 라우팅&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;라라벨 한글 메뉴얼 8.x - 라우팅&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;laravel.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1718965490221&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[개념원리]Message Queue&quot; data-og-description=&quot;Message Queue에 대한 개념 및 여러 오픈소스에 대해 설명합니다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@power0080/Message-Queue-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC&quot; data-og-url=&quot;https://velog.io/@power0080/Message-Queue-개념-정리&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zesbL/hyWoNQFumw/Jw7wwwDMHy6Lu49ofPWA0k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/dmZJLv/hyWoCuQN0j/fuLkCxWKmzufvFvax3CyN1/img.png?width=651&amp;amp;height=301&amp;amp;face=0_0_651_301,https://scrap.kakaocdn.net/dn/bkqenc/hyWoPHIgen/NfywKh7uPGzsmLLl19l2Yk/img.jpg?width=482&amp;amp;height=317&amp;amp;face=0_0_482_317&quot;&gt;&lt;a href=&quot;https://velog.io/@power0080/Message-Queue-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@power0080/Message-Queue-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zesbL/hyWoNQFumw/Jw7wwwDMHy6Lu49ofPWA0k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/dmZJLv/hyWoCuQN0j/fuLkCxWKmzufvFvax3CyN1/img.png?width=651&amp;amp;height=301&amp;amp;face=0_0_651_301,https://scrap.kakaocdn.net/dn/bkqenc/hyWoPHIgen/NfywKh7uPGzsmLLl19l2Yk/img.jpg?width=482&amp;amp;height=317&amp;amp;face=0_0_482_317');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[개념원리]Message Queue&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Message Queue에 대한 개념 및 여러 오픈소스에 대해 설명합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1718971266443&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Boot] Bucket4j를 이용해 서버 측 처리율 제한 장치 구축하기&quot; data-og-description=&quot;잦은 제 3자의 서비스를 호출하여 과금이 나올까봐 무섭다면? 처리율 제한 장치를 구축할 것!&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@dongvelop/Spring-Boot-Bucket4j%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B2%84-%EC%B8%A1-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@dongvelop/Spring-Boot-Bucket4j를-이용해-서버-측-처리율-제한-장치-구축하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfhlgx/hyWoCInUkF/8KtBA9VbfdZtXhlOMTaRK0/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263,https://scrap.kakaocdn.net/dn/R7RYy/hyWoMjVXzy/rodKrdscLkcdjcLKiJKFDk/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263,https://scrap.kakaocdn.net/dn/fy1s3/hyWoBvW1zQ/yOIRUkhl0D5MZi0ROj1de1/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263&quot;&gt;&lt;a href=&quot;https://velog.io/@dongvelop/Spring-Boot-Bucket4j%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B2%84-%EC%B8%A1-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@dongvelop/Spring-Boot-Bucket4j%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B2%84-%EC%B8%A1-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfhlgx/hyWoCInUkF/8KtBA9VbfdZtXhlOMTaRK0/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263,https://scrap.kakaocdn.net/dn/R7RYy/hyWoMjVXzy/rodKrdscLkcdjcLKiJKFDk/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263,https://scrap.kakaocdn.net/dn/fy1s3/hyWoBvW1zQ/yOIRUkhl0D5MZi0ROj1de1/img.png?width=622&amp;amp;height=346&amp;amp;face=239_214_284_263');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot] Bucket4j를 이용해 서버 측 처리율 제한 장치 구축하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;잦은 제 3자의 서비스를 호출하여 과금이 나올까봐 무섭다면? 처리율 제한 장치를 구축할 것!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1718972008887&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Nginx RateLimit] Nginx에 RateLimit 적용해보기&quot; data-og-description=&quot;Rate Limiting주어진 특정한 시간 동안 제한된 HTTP 요청량을 제한할 수 있도록 하는 기능.Rate Limiting을 하는 이유보안 측면의 목적 (Brute&amp;nbsp;force&amp;nbsp;attack 방지 등)비정상적인 요청 방지 (DDoS 공격 등 방지)&quot; data-og-host=&quot;willseungh0.tistory.com&quot; data-og-source-url=&quot;https://willseungh0.tistory.com/191&quot; data-og-url=&quot;https://willseungh0.tistory.com/191&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CN1zZ/hyWoLeiyFf/4fsIS51NsyqJlL5TktQ4s0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UlZZY/hyWoOIPRBD/PGN6Wk6vLoMekKKX3HBK1k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bpoNVq/hyWoNXtrRF/0vyYBtfKkYKzysMflxrYz0/img.png?width=523&amp;amp;height=459&amp;amp;face=0_0_523_459&quot;&gt;&lt;a href=&quot;https://willseungh0.tistory.com/191&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://willseungh0.tistory.com/191&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CN1zZ/hyWoLeiyFf/4fsIS51NsyqJlL5TktQ4s0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UlZZY/hyWoOIPRBD/PGN6Wk6vLoMekKKX3HBK1k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bpoNVq/hyWoNXtrRF/0vyYBtfKkYKzysMflxrYz0/img.png?width=523&amp;amp;height=459&amp;amp;face=0_0_523_459');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Nginx RateLimit] Nginx에 RateLimit 적용해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Rate Limiting주어진 특정한 시간 동안 제한된 HTTP 요청량을 제한할 수 있도록 하는 기능.Rate Limiting을 하는 이유보안 측면의 목적 (Brute&amp;nbsp;force&amp;nbsp;attack 방지 등)비정상적인 요청 방지 (DDoS 공격 등 방지)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;willseungh0.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1718973523789&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Boot] 자바 스프링에서 처리율 제한 기능을 구현하는 4가지 방법&quot; data-og-description=&quot;개요 스마트폰 어플에서 우다다다 버튼을 누르다보면 가끔 아래와 같은 메세지를 마주할 수 있다. 이런 기능들은 대부분의 사이트에서 만나볼 수 있는데, 보통 서버 안정을 위해 필수적으로 탑&quot; data-og-host=&quot;hogwart-scholars.tistory.com&quot; data-og-source-url=&quot;https://hogwart-scholars.tistory.com/entry/Spring-Boot-%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95&quot; data-og-url=&quot;https://hogwart-scholars.tistory.com/entry/Spring-Boot-%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btQuZl/hyWoGD3fW7/KWizPAbTbTWkiK7cT5V3GK/img.gif?width=182&amp;amp;height=208&amp;amp;face=0_0_182_208,https://scrap.kakaocdn.net/dn/gfH8p/hyWoEe8xlJ/cVIxMwQzRVqpgqOn5qNbmK/img.gif?width=182&amp;amp;height=208&amp;amp;face=0_0_182_208,https://scrap.kakaocdn.net/dn/HeUe1/hyWoDm0Kwt/Zk05iSV8utM3KRvkK7rtfk/img.png?width=1154&amp;amp;height=1610&amp;amp;face=0_0_1154_1610&quot;&gt;&lt;a href=&quot;https://hogwart-scholars.tistory.com/entry/Spring-Boot-%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hogwart-scholars.tistory.com/entry/Spring-Boot-%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btQuZl/hyWoGD3fW7/KWizPAbTbTWkiK7cT5V3GK/img.gif?width=182&amp;amp;height=208&amp;amp;face=0_0_182_208,https://scrap.kakaocdn.net/dn/gfH8p/hyWoEe8xlJ/cVIxMwQzRVqpgqOn5qNbmK/img.gif?width=182&amp;amp;height=208&amp;amp;face=0_0_182_208,https://scrap.kakaocdn.net/dn/HeUe1/hyWoDm0Kwt/Zk05iSV8utM3KRvkK7rtfk/img.png?width=1154&amp;amp;height=1610&amp;amp;face=0_0_1154_1610');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot] 자바 스프링에서 처리율 제한 기능을 구현하는 4가지 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 스마트폰 어플에서 우다다다 버튼을 누르다보면 가끔 아래와 같은 메세지를 마주할 수 있다. 이런 기능들은 대부분의 사이트에서 만나볼 수 있는데, 보통 서버 안정을 위해 필수적으로 탑&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hogwart-scholars.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1718973598636&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Rate Limit - step 1] Rate Limit이란? (소개, Throttling, 구현시 주의사항 등)&quot; data-og-description=&quot;필자는 현재 API서버를 열심히 개발 하고 있다. 개발한 서비스가 트래픽을 많이 받다보니, 트래픽을 적절히 제한하지 않았을 때 장애가 발생했고, 그에 따라 API의 Rate Limit을 구현해야 했다. 공부&quot; data-og-host=&quot;etloveguitar.tistory.com&quot; data-og-source-url=&quot;https://etloveguitar.tistory.com/126&quot; data-og-url=&quot;https://etloveguitar.tistory.com/126&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bb9PcA/hyWoBCJMRs/OLkAykR5L07QUFM2UmPX51/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/Y2iF9/hyWoFLTDIH/RvoKhPzVR7HpsYK5l9Mj0K/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bZV8yC/hyWoE7hxUi/ZAUBanjBasWtJz3CrHhg40/img.jpg?width=798&amp;amp;height=984&amp;amp;face=0_0_798_984&quot;&gt;&lt;a href=&quot;https://etloveguitar.tistory.com/126&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://etloveguitar.tistory.com/126&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bb9PcA/hyWoBCJMRs/OLkAykR5L07QUFM2UmPX51/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/Y2iF9/hyWoFLTDIH/RvoKhPzVR7HpsYK5l9Mj0K/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bZV8yC/hyWoE7hxUi/ZAUBanjBasWtJz3CrHhg40/img.jpg?width=798&amp;amp;height=984&amp;amp;face=0_0_798_984');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Rate Limit - step 1] Rate Limit이란? (소개, Throttling, 구현시 주의사항 등)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;필자는 현재 API서버를 열심히 개발 하고 있다. 개발한 서비스가 트래픽을 많이 받다보니, 트래픽을 적절히 제한하지 않았을 때 장애가 발생했고, 그에 따라 API의 Rate Limit을 구현해야 했다. 공부&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;etloveguitar.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1718975147530&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;서비스 가용성 확보에 필요한 Rate Limiting Algorithm에 대해 | Mimul Tech log&quot; data-og-description=&quot;Rate Limit 알고리즘(Leaky bucket, Token bucket, Fixed window counter, Sliding window log, Sliding window counter)과 주요 서비스들의 Rate Limit 정책 등에 정리한 글.&quot; data-og-host=&quot;www.mimul.com&quot; data-og-source-url=&quot;https://www.mimul.com/blog/about-rate-limit-algorithm/&quot; data-og-url=&quot;https://www.mimul.com/blog/about-rate-limit-algorithm/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/enjdYp/hyWoCuSUB2/OEejEZLm11xZG1hqkdnyf0/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260,https://scrap.kakaocdn.net/dn/Uz8dy/hyWoOB3XYY/knyPJVH8k8AEWc2etbPtUk/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260,https://scrap.kakaocdn.net/dn/dKp9uZ/hyWoE0yELR/CkwV0A4GBv04IXOFSDvVq0/img.jpg?width=640&amp;amp;height=640&amp;amp;face=177_165_437_449&quot;&gt;&lt;a href=&quot;https://www.mimul.com/blog/about-rate-limit-algorithm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.mimul.com/blog/about-rate-limit-algorithm/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/enjdYp/hyWoCuSUB2/OEejEZLm11xZG1hqkdnyf0/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260,https://scrap.kakaocdn.net/dn/Uz8dy/hyWoOB3XYY/knyPJVH8k8AEWc2etbPtUk/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260,https://scrap.kakaocdn.net/dn/dKp9uZ/hyWoE0yELR/CkwV0A4GBv04IXOFSDvVq0/img.jpg?width=640&amp;amp;height=640&amp;amp;face=177_165_437_449');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;서비스 가용성 확보에 필요한 Rate Limiting Algorithm에 대해 | Mimul Tech log&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Rate Limit 알고리즘(Leaky bucket, Token bucket, Fixed window counter, Sliding window log, Sliding window counter)과 주요 서비스들의 Rate Limit 정책 등에 정리한 글.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.mimul.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹/Spring</category>
      <category>api 요청 수 제한</category>
      <category>bucket4j</category>
      <category>concurrency limit</category>
      <category>Lua Script</category>
      <category>race condition</category>
      <category>rate limit</category>
      <category>redis</category>
      <category>동시성 제어</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/190</guid>
      <comments>https://2jaebbang.tistory.com/190#entry190comment</comments>
      <pubDate>Fri, 25 Aug 2023 08:45:25 +0900</pubDate>
    </item>
    <item>
      <title>금칙어 필터링 하기 (Feat. Redis 적용)</title>
      <link>https://2jaebbang.tistory.com/187</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Situation&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;맡고 있는 앱 내 기능 중, 텍스트를 입력하면 AI이미지로 출력해주는 기능이 있다. 이때, 사용자가 비속어 등의 특정 단어를 쓸 경우 선정적이거나 폭력적인 이미지가 나올 수 있어, 입력값에 비속어가 있는지 판별해 필터링하는 로직을 구현해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기획팀에게 받은 단어는 약 2500개였고, 받은 데이터들을 어디에 저장하는게 좋을지 고민을 하게 되었다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 생각한 것이 &lt;b&gt;로컬 파일&lt;/b&gt;, &lt;b&gt;db,&lt;/b&gt;&amp;nbsp;&lt;b&gt;redis서버&amp;nbsp;&lt;/b&gt;이었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 중에서도 &lt;span style=&quot;text-align: left;&quot;&gt;캐싱 및 공유 데이터 관리가 가장 용이하다고 들은 &lt;b&gt;redis&lt;/b&gt;를&amp;nbsp;통해 데이터를 저장해보고자 했다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;지금까지 redis서버를 한번도 구축해본 적이 없었고, 사실 redis에 대한 개념도 제대로 잡혀 있지 않았다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;그래서 먼저 redis가 무엇인지부터 알아보았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;결론적으로는 속도 등의 문제로 (아래에 설명), redis가 아닌 local에서 불러오는 식으로 구현하게 됐지만 redis를 직접 연동해본 귀한 경험이 되었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis란?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Key, Value&lt;/b&gt;&amp;nbsp;구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템 (DBMS).&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터베이스, 캐시, 메세지 브로커로 사용, 인메모리 데이터 구조를 가진 저장소.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Redis를 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;데이터베이스는 물리 디스크에 데이터를 저장해 서버 문제가 생겨도 데이터가 손실되지 않으나, 사용자가 많아질수록 부하가 생기고 느려질 수 있다. &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기나 소규모 서비스에서는 WEB-WAS-DB 구조로 충분하지만, 사용자가 늘어나면 &lt;b&gt;캐시 서버&lt;/b&gt;가 필요하게 되며, &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이때 Redis를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;캐시는 한번 읽은 데이터를 임의의 공간에 저장하기에,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;같은 요청이 여러 번 들어오는 경우 매번 DB를 거치는 것이 아닌, 캐시 서버에서 저장된 결괏값을 바로 내려주기 때문에 서비스의 속도가 느려지지 않는 장점이 있다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Key, Value 구조.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;String, Lists, Sets, Sorted Sets, Hashes 자료 구조 지원.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Single Threaded.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Look aside cache&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 클라이언트가 데이터를 요청 -&amp;gt; &amp;nbsp; 웹서버는 &lt;span style=&quot;text-align: start;&quot;&gt;Cache 서버에 먼저 확인&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Cache 서버에 데이터가 있으면 Cache 서버에 있는 값을 클라이언트에게 바로 반환 (Cache Hit)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Cache 서버에 데이터가 없으면 DB에 데이터를 조회 -&amp;gt; Cache 서버에 저장하고 값을 클라이언트에게 반환 (Cache Miss)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Write Back&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 웹서버는 모든 데이터를 Cache 서버에 저장&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Cache 서버에 특정 시간 동안 데이터가 저장됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Cache 서버에 있는 데이터를 DB에 저장 -&amp;gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB에 저장된 Cache 서버의 데이터 삭제&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;*&lt;span style=&quot;text-align: start;&quot;&gt;서버 장애 시 데이터 손실의 위험&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis를 구축해보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생각보다 redis를 실행하는건 간단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ubuntu 환경을 기준으로 하게 되면,&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1691040961728&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt-get update
sudo apt-get upgrade
sudo apt-get install redis-server

//설치 후 버전확인
redis-server --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mac 기준으로는 `brew install redis`를 해준 후, 아래의 명령어 두개 중 하나를 골라서 하면 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690881684185&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew services start redis
redis-server --damonize yes //백그라운드 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행을 한 뒤, `redis-cli ping` 명령어를 실행했을 때, 아래와 같이 응답하면 제대로 실행이 된 상태다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-01 오후 6.22.21.png&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;34&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JWXn0/btsppIoxPrM/RFZsvbiwC0auSP4q1zG7Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JWXn0/btsppIoxPrM/RFZsvbiwC0auSP4q1zG7Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JWXn0/btsppIoxPrM/RFZsvbiwC0auSP4q1zG7Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJWXn0%2FbtsppIoxPrM%2FRFZsvbiwC0auSP4q1zG7Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;248&quot; height=&quot;34&quot; data-filename=&quot;스크린샷 2023-08-01 오후 6.22.21.png&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;34&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음으로 redis.conf 파일에서, Redis가 사용할 수 있는 최대 사용 메모리양을 정한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 메모리를 초과하게 됐을 때, 데이터를 어떻게 삭제할지 또한 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690949512170&quot; class=&quot;awk&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo nano /etc/redis/redis.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 파일에서 maxmemory와 maxmemory-policy를 찾아서 최대 사용 메모리는 1gb, 초과할 경우 가장 오래된 데이터를 지우고 가장 최근에 저장된 데이터를 사용하는 것으로 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690949512171&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;maxmemory 1g
maxmemory-policy allkeys-lru&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정이 적용되도록 Redis를 재시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690949512171&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo systemctl restart redis-server.service&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Redis의 기본포트는 6379이다. Redis가 6379 포트를 쓰고 있는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1690949512172&quot; class=&quot;1c&quot; style=&quot;background-color: #f6f7f8; color: #555555; text-align: start;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ netstat -nlpt | grep 6379&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Solution&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문자열에서 금칙어가 있는지 판별하는 방법 두가지 중에 고민을 하게 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫번째 방법은 redis에 금칙어를 모두 집어넣고, 문자열을 순회하며 해당 문자열을 redis의 exists메서드를 통해 판별하는 법,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두번째 방법은 금칙어 array를 순회하며, KMP알고리즘을 적용한 메서드 안에 매개변수 값으로 해당 금칙어를 넣어 존재유무를 판별하는 법이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;위에서 설명한 것 처럼, redis는 캐시를 사용하기 때문에 속도 측면에서 두번째 방법보다 빠를지도 모르겠다는 생각이 들었고 두 가지 방법의 성능을 측정해봤다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;결과는 놀라웠다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 54px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 36px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 36px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;redis 적용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 36px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;KMP알고리즘 적용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 18px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시간 (1000자 기준) /ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 18px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;750 ~ 850&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.333333333333336%; height: 18px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;30&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생각했던 것과는 달리, 1000자 기준으로 KMP알고리즘을 적용한 코드가 약 25배 차이 나는 퍼포먼스를 보여줬다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;두 방법의 코드를 분석한 결과는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 90px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Redis 적용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;KMP알고리즘 적용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;중첩 루프를 사용한 브루트포스 방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;KMP (Knuth-Morris-Pratt) 알고리즘&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;모든 부분 문자열을 생성하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;각 부분 문자열이 Redis에 존재하는지 확인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;주어진 문장에서 금칙어 목록과 일치하는 부분 문자열 찾음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;중복된 서브스트링 검사를 피하기 위해&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;checkedSubStrings 배열을 사용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;LPS (Longest Prefix Suffix) 배열을 사용하여 패턴 내의 부분 일치를 효율적으로 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시간복잡도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;O(n^2)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;O(n+m)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Redis 적용 (브루트포스)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1691113011255&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ForbiddenWordsFilter {
    private Jedis redis;
    
    public ForbiddenWordsChecker() {
        redis = new Jedis(&quot;localhost&quot;);
    }

    public boolean checkForbiddenWordsRedis(String sentence) {
        int length = sentence.length();
        for (int i = 0; i &amp;lt; length; i++) {
            for (int j = i + 1; j &amp;lt;= length; j++) {
                String subString = sentence.substring(i, j);
                if (redis.exists(subString))
                    return false; // 금칙어가 문장에 포함되어 있을 경우 false 반환
            }
        }
        return true; // 금칙어가 없으면 정상 리턴
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;KMP 알고리즘 적용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1691113116855&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ForbiddenWordsFilter {
    private static final List&amp;lt;String&amp;gt; forbiddenWords = List.of(&quot;word1&quot;, &quot;word2&quot;); // 금칙어 목록

    public boolean checkForbiddenWords(String sentence) {
        for(String word : forbiddenWords) {
            if(kmpSearch(word, sentence))
                return false; // 금칙어가 문장에 포함되어 있을 경우 false 반환
        }
        return true; // 금칙어가 문장에 포함되어 있지 않을 경우 true 반환
    }

    private boolean kmpSearch(String pattern, String text) {
        int[] lps = computeLPSArray(pattern);
        int i = 0, j = 0;
        while (i &amp;lt; text.length()) {
            if(pattern.charAt(j) == text.charAt(i)) {
                i++;
                j++;
            }
            if(j == pattern.length())
                return true; // 패턴이 일치
            if(i &amp;lt; text.length() &amp;amp;&amp;amp; pattern.charAt(j) != text.charAt(i))
               j = (j != 0) ? lps[j - 1] : 0;
        }
        return false;
    }

    private int[] computeLPSArray(String pattern) {
        int length = 0, i = 1;
        int[] lps = new int[pattern.length()];
        while(i &amp;lt; pattern.length()) {
            if(pattern.charAt(i) == pattern.charAt(length)) lps[i++] = ++length;
            if (length != 0) length = lps[length - 1];
            else lps[i++] = length;
        }
        return lps;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과적으로&amp;nbsp;KMP 알고리즘은 패턴의 부분 일치를 활용하여 불필요한 비교를 줄이므로, 브루트포스 방식에 비해 효율적이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;브루트포스 방식&amp;nbsp;코드는 모든 부분 문자열을 생성하고 Redis에서 확인하는 작업을 수행하므로, KMP 코드보다 더 많은 연산을 필요로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물론 브루트포스 방식이 직관적이고 구현이 간단했지만, 긴 문자열에 대해 훨씬 효율적인 KMP 알고리즘을 최종적으로 적용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1690881391740&quot; style=&quot;color: #333333; text-align: start;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;문자열 필터(채팅필터)&quot; data-og-description=&quot;문자열을 필터링 하기 위해 여러가지를 찾아 보았다. 요즘 나오는 게임들은 글로벌! 하게 출시를 하기 때문...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=jjadoll&amp;amp;logNo=221028089902&quot; data-og-url=&quot;https://blog.naver.com/jjadoll/221028089902&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5oVHn/hyTwqJydjp/7as12w2qx9RM9QOOknOkC0/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=jjadoll&amp;amp;logNo=221028089902&quot; data-source-url=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=jjadoll&amp;amp;logNo=221028089902&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5oVHn/hyTwqJydjp/7as12w2qx9RM9QOOknOkC0/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;문자열 필터(채팅필터)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;문자열을 필터링 하기 위해 여러가지를 찾아 보았다. 요즘 나오는 게임들은 글로벌! 하게 출시를 하기 때문...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1691115122103&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;알고리즘 - KMP 알고리즘 : 문자열 검색을 위한 알고리즘&quot; data-og-description=&quot;＠ 문자열을 검색하려면? : 패턴 매칭&quot; data-og-host=&quot;chanhuiseok.github.io&quot; data-og-source-url=&quot;https://chanhuiseok.github.io/posts/algo-14/&quot; data-og-url=&quot;https://chanhuiseok.github.io/posts/algo-14/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://chanhuiseok.github.io/posts/algo-14/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chanhuiseok.github.io/posts/algo-14/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;알고리즘 - KMP 알고리즘 : 문자열 검색을 위한 알고리즘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;＠ 문자열을 검색하려면? : 패턴 매칭&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chanhuiseok.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>KMP 알고리즘</category>
      <category>redis</category>
      <category>금칙어</category>
      <category>문자열 검색</category>
      <category>비속어 필터링</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/187</guid>
      <comments>https://2jaebbang.tistory.com/187#entry187comment</comments>
      <pubDate>Tue, 25 Jul 2023 17:16:15 +0900</pubDate>
    </item>
    <item>
      <title>Stable Diffusion 을 서버와 연동해보자</title>
      <link>https://2jaebbang.tistory.com/185</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Stable Diffusion&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;자연어 설명으로부터 이미지를 생성해내는 딥러닝 생성형 AI로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Text to Image 모델, 즉 텍스트로부터 이미지를 뽑아내는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;아무튼 회사에서 운영중인 앱에 Stable Diffusion 기술을 탑재시켜야 했다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;그림을 잘 못 그리는&amp;nbsp;&lt;/span&gt;사용자들을 위해, 텍스트를 입력하면 그 텍스트를 stable diffusion을 이용해 이미지를 출력해서 그림에 흥미를 갖도록 하기 위한 의도였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;stable-diffusion-text-to-image.png&quot; data-origin-width=&quot;1393&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWyd1x/btsnIJP3hQH/DRRs53iXHjcrwgqJg9dk00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWyd1x/btsnIJP3hQH/DRRs53iXHjcrwgqJg9dk00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWyd1x/btsnIJP3hQH/DRRs53iXHjcrwgqJg9dk00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWyd1x%2FbtsnIJP3hQH%2FDRRs53iXHjcrwgqJg9dk00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;581&quot; height=&quot;249&quot; data-filename=&quot;stable-diffusion-text-to-image.png&quot; data-origin-width=&quot;1393&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;stable-diffusion-img2img-image-to-image.png&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl6od4/btsnOfgvdJ4/h7ukREM7496YAS3eshjg2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl6od4/btsnOfgvdJ4/h7ukREM7496YAS3eshjg2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl6od4/btsnOfgvdJ4/h7ukREM7496YAS3eshjg2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl6od4%2FbtsnOfgvdJ4%2Fh7ukREM7496YAS3eshjg2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;200&quot; data-filename=&quot;stable-diffusion-img2img-image-to-image.png&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Situation&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;: 외부서버에서 접근 가능하도록 만들자&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동작 구조는&amp;nbsp;&lt;b&gt;[클라이언트]&lt;/b&gt; &lt;b&gt;&amp;lt;---&lt;/b&gt;이미지 생성 요청/응답&lt;b&gt;---&amp;gt;&lt;/b&gt; &lt;b&gt;[WAS서버]&lt;/b&gt; &lt;b&gt;&amp;lt;---&lt;/b&gt;이미지 생성&lt;b&gt;---&amp;gt;&lt;/b&gt; &lt;b&gt;[Stable Diffusion서버]&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로세스가 이뤄져야 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;아래의 깃헙은 Stable Diffusion을 web ui형태로 구현해낸 소스코드다. &lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689574646868&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - AUTOMATIC1111/stable-diffusion-webui: Stable Diffusion web UI&quot; data-og-description=&quot;Stable Diffusion web UI. Contribute to AUTOMATIC1111/stable-diffusion-webui development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/AUTOMATIC1111/stable-diffusion-webui&quot; data-og-url=&quot;https://github.com/AUTOMATIC1111/stable-diffusion-webui&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dGjeLN/hyTk2KiRna/yZMUjFYrE0JwzaOeFDmym0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_129_1057_219&quot;&gt;&lt;a href=&quot;https://github.com/AUTOMATIC1111/stable-diffusion-webui&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/AUTOMATIC1111/stable-diffusion-webui&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dGjeLN/hyTk2KiRna/yZMUjFYrE0JwzaOeFDmym0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_129_1057_219');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - AUTOMATIC1111/stable-diffusion-webui: Stable Diffusion web UI&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Stable Diffusion web UI. Contribute to AUTOMATIC1111/stable-diffusion-webui development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 web ui 형태의 stable diffusion 이미지 생성 페이지가 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpHDBu/btsoMG4cTJI/JoudPdxvE8JD5ESXDW28P1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpHDBu/btsoMG4cTJI/JoudPdxvE8JD5ESXDW28P1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;807&quot; data-filename=&quot;Image.jpeg&quot; data-widthpercent=&quot;52.45&quot; style=&quot;width: 51.83566074193858%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpHDBu/btsoMG4cTJI/JoudPdxvE8JD5ESXDW28P1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpHDBu%2FbtsoMG4cTJI%2FJoudPdxvE8JD5ESXDW28P1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfLIqp/btsoz7IPGEq/AREaHgnlNl0K1uk5U2dea1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfLIqp/btsoz7IPGEq/AREaHgnlNl0K1uk5U2dea1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;890&quot; data-filename=&quot;Image.jpeg&quot; data-widthpercent=&quot;47.55&quot; style=&quot;width: 47.00154856038699%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfLIqp/btsoz7IPGEq/AREaHgnlNl0K1uk5U2dea1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfLIqp%2Fbtsoz7IPGEq%2FAREaHgnlNl0K1uk5U2dea1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;저 코드를&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Stable Diffusion 전용 서버 안에 탑재하는 것이 필요했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전용 서버는 Ncloud 플랫폼을 통해 구축 및 호스팅이 되고 있었고, Window Server 환경이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;깃헙의 공식문서를 보면 윈도우 환경에서 실행하는 법은 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 3.30.12.png&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKieRX/btsnSZxOf0t/00uoLDlywKR5n2EXXEkHZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKieRX/btsnSZxOf0t/00uoLDlywKR5n2EXXEkHZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKieRX/btsnSZxOf0t/00uoLDlywKR5n2EXXEkHZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKieRX%2FbtsnSZxOf0t%2F00uoLDlywKR5n2EXXEkHZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;172&quot; data-filename=&quot;스크린샷 2023-07-17 오후 3.30.12.png&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드를 살펴보니 REST API를 구축한 프레임워크는 FAST API였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버를 실행시키면 아래와 같이 &lt;u&gt;http://127.0.0.1:7861&lt;/u&gt;&amp;nbsp;주소로 실행이 되었다는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rh8lZ/btsoDH3OjSD/IpRHMvR320kRYKHxsDlxdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rh8lZ/btsoDH3OjSD/IpRHMvR320kRYKHxsDlxdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rh8lZ/btsoDH3OjSD/IpRHMvR320kRYKHxsDlxdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRh8lZ%2FbtsoDH3OjSD%2FIpRHMvR320kRYKHxsDlxdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;955&quot; height=&quot;463&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Problem1: 로컬 주소에다가 api호출을 할 순 없다&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메인서버와 Stable Diffusion서버가 REST api 통신을 하기 위해서는 로컬주소가 아닌 외부주소가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 8080포트를 쓰기로 했고, cmd를 통해 현재 쓰이고 있는 8080포트를 살펴보니 아래와 같이 아무것도 뜨지 않는다. 즉 닫혀 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;53&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfawpL/btsoz7BXrpB/kD2uZY3ssRVRreRRSJpovk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfawpL/btsoz7BXrpB/kD2uZY3ssRVRreRRSJpovk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfawpL/btsoz7BXrpB/kD2uZY3ssRVRreRRSJpovk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfawpL%2Fbtsoz7BXrpB%2FkD2uZY3ssRVRreRRSJpovk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;53&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;53&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생각보다 해결법은 무척 간단했는데, 소스코드 내에서&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689581226041&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server_name=server_name,
server_port=cmd_opts.port,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 부분을 아래와 같이 바꿔주면 바로 해결이 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1689581252026&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server_name='0.0.0.0',
server_port=8080,&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;55&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clEL0T/btsoyp3ZVkc/AzCs0ZW786CZLz3CS52fb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clEL0T/btsoyp3ZVkc/AzCs0ZW786CZLz3CS52fb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clEL0T/btsoyp3ZVkc/AzCs0ZW786CZLz3CS52fb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclEL0T%2Fbtsoyp3ZVkc%2FAzCs0ZW786CZLz3CS52fb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;625&quot; height=&quot;55&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;55&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCa3FH/btsoxV9U7wT/errqKKbXUowKHnRgRcZ8rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCa3FH/btsoxV9U7wT/errqKKbXUowKHnRgRcZ8rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCa3FH/btsoxV9U7wT/errqKKbXUowKHnRgRcZ8rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCa3FH%2FbtsoxV9U7wT%2FerrqKKbXUowKHnRgRcZ8rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;950&quot; height=&quot;448&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 '0.0.0.0' 이 하는 역할은,&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;0.0.0.0로 접근하면 로컬 호스트의 모든 IPv4로 되어있는 호스트에 접근이 가능하다는 것을 뜻한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Problem2: 방화벽 정책을 설정하자&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 포트만 뚫었다고 외부서버와 연결 가능한 것이 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엄연히 방화벽이라는게 존재하고, 특정 ip&amp;amp;port는 통과 가능하게 방화벽 정책을 설정하는 것이 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;방화벽&lt;/b&gt;은 전달되는 패킷의 정보(프로토콜, 포트, IP 주소 정보)를 바탕으로 전송을 허용 또는 거부하는 시스템이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;이때 외부 네트워크에서 발생해 내부 네트워크로 패킷이 전달되는 것을 &lt;b&gt;Inbound&lt;/b&gt; 라고 하며, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;내부 네트워크에서 발생해 외부 네트워크로 패킷이 전달되는 것을 &lt;b&gt;Outbound&lt;/b&gt; 트래픽이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;이때 방화벽의 &lt;b&gt;Inbound, Outbound 규칙&lt;/b&gt;은 패킷의 전달 방향(내부, 외부)과 통신 프로토콜, 포트, IP 주소를 기준으로 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;트래픽을 허용하거나 거부하는 규칙을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689581678903&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;가비아: 국내 1위&quot; data-og-description=&quot; &quot; data-og-host=&quot;customer.gabia.com&quot; data-og-source-url=&quot;https://customer.gabia.com/manual/cloud/6751/13320&quot; data-og-url=&quot;https://customer.gabia.com/manual/cloud/6751/www.gabia.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b5qyll/hyTkZ7X57D/aivr5sTBU49uSmFlTO0DPk/img.jpg?width=1200&amp;amp;height=1000&amp;amp;face=0_0_1200_1000&quot;&gt;&lt;a href=&quot;https://customer.gabia.com/manual/cloud/6751/13320&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://customer.gabia.com/manual/cloud/6751/13320&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b5qyll/hyTkZ7X57D/aivr5sTBU49uSmFlTO0DPk/img.jpg?width=1200&amp;amp;height=1000&amp;amp;face=0_0_1200_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;가비아: 국내 1위&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;customer.gabia.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 Stable Diffusion 서버에서 먼저 작업을 해줬다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dt1peg/btsoCkOzLMA/kKg9QL7dLZ2bQpbOLOsDN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dt1peg/btsoCkOzLMA/kKg9QL7dLZ2bQpbOLOsDN1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;577&quot; data-filename=&quot;Image.png&quot; data-widthpercent=&quot;50.23&quot; style=&quot;width: 49.64253034598189%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dt1peg/btsoCkOzLMA/kKg9QL7dLZ2bQpbOLOsDN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdt1peg%2FbtsoCkOzLMA%2FkKg9QL7dLZ2bQpbOLOsDN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZfG2C/btsoGg5UHpE/d8ndwohn3FFiqvKQY5Q6rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZfG2C/btsoGg5UHpE/d8ndwohn3FFiqvKQY5Q6rK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;579&quot; data-filename=&quot;Image.png&quot; data-widthpercent=&quot;49.77&quot; style=&quot;width: 49.194678956343694%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZfG2C/btsoGg5UHpE/d8ndwohn3FFiqvKQY5Q6rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZfG2C%2FbtsoGg5UHpE%2Fd8ndwohn3FFiqvKQY5Q6rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HiBjN/btsoNVUrPJX/a6ZraXY9ngSuRfGN2ovkG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HiBjN/btsoNVUrPJX/a6ZraXY9ngSuRfGN2ovkG0/img.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;579&quot; data-is-animation=&quot;false&quot; data-filename=&quot;Image.png&quot; data-widthpercent=&quot;49.89&quot; style=&quot;width: 49.3064839253751%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HiBjN/btsoNVUrPJX/a6ZraXY9ngSuRfGN2ovkG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHiBjN%2FbtsoNVUrPJX%2Fa6ZraXY9ngSuRfGN2ovkG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgb4Xm/btsoQtpSrmE/sNPuNDdRv7wIczIYaKo2H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgb4Xm/btsoQtpSrmE/sNPuNDdRv7wIczIYaKo2H0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;578&quot; data-filename=&quot;Image.png&quot; data-widthpercent=&quot;50.11&quot; style=&quot;width: 49.5307253769505%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgb4Xm/btsoQtpSrmE/sNPuNDdRv7wIczIYaKo2H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcgb4Xm%2FbtsoQtpSrmE%2FsNPuNDdRv7wIczIYaKo2H0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIeOxO/btsoQs5ylgA/FF8yr7NibtBiRPzgsCH780/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIeOxO/btsoQs5ylgA/FF8yr7NibtBiRPzgsCH780/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIeOxO/btsoQs5ylgA/FF8yr7NibtBiRPzgsCH780/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIeOxO%2FbtsoQs5ylgA%2FFF8yr7NibtBiRPzgsCH780%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;329&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인바운드 규칙으로 아래와 같이 8080번 포트가 열렸음을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSRe29/btsoGfzaYE9/ngTnH8lctSQyFHTwt9yA8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSRe29/btsoGfzaYE9/ngTnH8lctSQyFHTwt9yA8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSRe29/btsoGfzaYE9/ngTnH8lctSQyFHTwt9yA8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSRe29%2FbtsoGfzaYE9%2FngTnH8lctSQyFHTwt9yA8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1157&quot; height=&quot;71&quot; data-filename=&quot;Image.png&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 다음으로 Ncloud Platform Console 에서 ACG 라는 것을 설정해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;&lt;b&gt;ACG(Access Control Group)&lt;/b&gt;는&amp;nbsp;&lt;/span&gt;서버 간 네트워크 접근 제어 및 관리를 할 수 있는 IP/Port 기반 필터링 방화벽 서비스 를 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689581819143&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;ACG&quot; data-og-description=&quot; &quot; data-og-host=&quot;guide.ncloud-docs.com&quot; data-og-source-url=&quot;https://guide.ncloud-docs.com/docs/server-acg-classic&quot; data-og-url=&quot;https://guide.ncloud-docs.com/docs/server-acg-classic&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://guide.ncloud-docs.com/docs/server-acg-classic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://guide.ncloud-docs.com/docs/server-acg-classic&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ACG&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;guide.ncloud-docs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안상 캡처는 안했지만&amp;nbsp;&lt;b&gt;프로토콜(TCP), 접근소스(접근할 WAS ip), 허용포트(port)&lt;/b&gt; 를 설정해주면 됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Result: curl로 api를 쏴보자&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1690163923584&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X POST -H &quot;Content-Type: application/json&quot; \
-d '{&quot;prompt&quot;: &quot;cute snake cartoon style&quot;, &quot;negative_prompt&quot;: &quot;cute&quot;, &quot;steps&quot;: 25, &quot;sampler_index&quot;: &quot;Euler&quot;, &quot;width&quot;: 512, &quot;height&quot;: 512, &quot;cfg_scale&quot;: 7, &quot;seed&quot;: -1}' \
http://{stable diffusion server ip}/sdapi/v1/txt2img&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 코드를 was서버 내부에서 curl 호출하니&amp;nbsp;성공적으로 base64형태의 response를 받는 것을 알 수 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-24 오전 10.56.47.png&quot; data-origin-width=&quot;2376&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvNqcL/btsozk2zx8l/agRUJkVk2cxtvo7dlRFGx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvNqcL/btsozk2zx8l/agRUJkVk2cxtvo7dlRFGx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvNqcL/btsozk2zx8l/agRUJkVk2cxtvo7dlRFGx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvNqcL%2Fbtsozk2zx8l%2FagRUJkVk2cxtvo7dlRFGx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;784&quot; height=&quot;237&quot; data-filename=&quot;스크린샷 2023-07-24 오전 10.56.47.png&quot; data-origin-width=&quot;2376&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Insight&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;방화벽 정책을 다루게 되면서, 서로 다른 서버 간 통신을 수립할 때 보안 측면이 얼마나 중요한지 깨달았다. 이번 경험을 통해, ACG의 개념과 인바운드와 아웃바운드 규칙을 설정하고 방법을 배웠고&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;네트워크 통신을 이해하는 것이 정말 중요함을 다시 한번 느꼈다. &lt;br /&gt;외부와 통신이 가능하도록 로컬 주소를 외부 주소로 변환 가능하게 하기 위해서는, 당연히 IIS와 같은 웹서버 프로그램을 통해 구축해야 한다고 생각했지만&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;적절한 방화벽 설정 등으로도 외부와 통신이 가능하다는 것을 알게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;stable diffusion이라는 기술을 서버에 탑재해서, 외부 서버와 http통신 가능하도록 해 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;curl을 통해 base64 형태의 응답을 성공적으로 받았을 때는&lt;/span&gt;&amp;nbsp;기울인 노력이 확인되는 뜻 깊은 순간이었다.&lt;/p&gt;</description>
      <category>Stable Diffusion</category>
      <category>ACG</category>
      <category>Stable Diffusion</category>
      <category>서버 구축</category>
      <category>인바운드</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/185</guid>
      <comments>https://2jaebbang.tistory.com/185#entry185comment</comments>
      <pubDate>Mon, 17 Jul 2023 14:41:13 +0900</pubDate>
    </item>
    <item>
      <title>env파일 인식시키는 법</title>
      <link>https://2jaebbang.tistory.com/184</link>
      <description>&lt;pre id=&quot;code_1686707805154&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i --save @nestjs/config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후, 아래와 같이 ConfigModule.foorRoot 안에 envFilePath를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1686707769687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.development.env',
      isGlobal: true,
    }),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT),
      username: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_DATABASE,
      entities: ['dist/**/*.entity{.ts,.js}'],
      synchronize: false,
    }),
    TypeOrmModule.forFeature([]),
  ],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹/Nestjs</category>
      <author>이재빵</author>
      <guid isPermaLink="true">https://2jaebbang.tistory.com/184</guid>
      <comments>https://2jaebbang.tistory.com/184#entry184comment</comments>
      <pubDate>Wed, 14 Jun 2023 10:58:37 +0900</pubDate>
    </item>
  </channel>
</rss>