<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>siio's blog</title>
    <link>https://545aa7.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 04:52:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>siio</managingEditor>
    <image>
      <title>siio's blog</title>
      <url>https://tistory1.daumcdn.net/tistory/5419092/attach/4561e94d3aaf48bf9be7e17687af924c</url>
      <link>https://545aa7.tistory.com</link>
    </image>
    <item>
      <title>@Transactional 안에서 SSE 전송하면 안 되는 이유</title>
      <link>https://545aa7.tistory.com/120</link>
      <description>&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot + JPA&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;HikariCP(Connection Pool)&lt;/li&gt;
&lt;li&gt;SSE(Server-Sent Events)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 id=&quot;1-&quot; data-ke-size=&quot;size23&quot;&gt;1. 문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class NotificationService {

    private final NotificationRepository notificationRepository;
    private final SseEmitterService sseEmitterService;

    @Transactional
    public void sendNotification(Long userId, String message) {
        // 1. 알림 데이터를 DB에 저장
        Notification notification = Notification.builder()
            .userId(userId)
            .message(message)
            .createdAt(LocalDateTime.now())
            .build();

        notificationRepository.save(notification);

        // 2. SSE를 통해 실시간 전송
        sseEmitterService.send(userId, notification);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 개발 후 프론트 팀원이 연동을 진행하던 중, 처음에는 잘 동작하지만 시간이 지날 수록 응답이 느려지고 결국 테스트 서버가 먹통이 되는 문제가 발생했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에는 정상 동작하다가 점진적으로 느려짐&lt;/li&gt;
&lt;li&gt;DB Connection Timeout 발생&lt;/li&gt;
&lt;li&gt;서버는 정상이지만 DB 접근에서 병목&lt;/li&gt;
&lt;li&gt;테스트 서버 재시작 후 동일 패턴 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 id=&quot;2-&quot; data-ke-size=&quot;size23&quot;&gt;2. 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 DB Connection Pool 설정 문제인 줄 알고 maximumPoolSize, connectionTimeout 등을 조정해 봤지만 문제는 계속 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 로그를 확인해보니 Active Connection이 계속 증가하다가 Pool이 고갈되는 패턴이 반복되고 있었습니다. 이상한 점은 DB 작업 자체는 빠른데(ms) Connection이 오래 점유되고 있다는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 문득 관성으로 붙이는 &lt;code&gt;@Transactional&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;Spring의 &lt;code&gt;@Transactional&lt;/code&gt;은 다음과 같이 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 메서드 시작 &amp;rarr; 트랜잭션 시작, DB 커넥션 획득&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 메서드 내부 로직 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 메서드 정상 종료 &amp;rarr; COMMIT, DB 커넥션 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 예외 발생 &amp;rarr; ROLLBACK, DB 커넥션 반환&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;DB 커넥션은 트랜잭션이 살아있는 동안 계속 점유&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;초기 코드에서 &lt;code&gt;@Transactional&lt;/code&gt;이 메서드 전체에 걸려 있었고, 프론트에서 api 연동 작업을 하며 계속 타임아웃이 일어나고 있었기 때문에 DB 커넥션이 점유된 채로 유지되고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Transactional  // &amp;larr; 여기서 DB 커넥션 획득
public void sendNotification(Long userId, String message) {
    notificationRepository.save(notification);  // DB 작업 (0.01초)

    sseEmitterService.send(userId, notification);  // 네트워크 I/O (1~2초)
    // &amp;uarr; 이 작업이 끝날 때까지 DB 커넥션 계속 점유
}
// &amp;larr; 여기서야 비로소 DB 커넥션 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션과 네트워크 I/O를 같은 범위에서 처리하고 있어 트랜잭션이 불필요하게 길어진 것이 원인이었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 id=&quot;3-&quot; data-ke-size=&quot;size23&quot;&gt;3. 해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션과 네트워크 I/O를 분리하는 것이 필요했습니다. DB 작업은 트랜잭션 안에서 처리하되, SSE 전송은 &lt;b&gt;트랜잭션 커밋 후&lt;/b&gt;에 실행되도록 로직을 개선했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class NotificationService {

    private final NotificationRepository notificationRepository;
    private final SseEmitterService sseEmitterService;

    @Transactional
    public void createReactionNotification(Post post, User actor, ReactionType newType) {
        // 1. DB 작업: 알림 저장
        Notification notification = Notification.of(receiver, post, newType, count);
        Notification savedNoti = notificationRepository.save(notification);

        // 2. 트랜잭션 커밋 후 SSE 전송되도록 등록
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    sendNotificationAfterCommit(savedNoti);
                }
            }
        );
        // 여기서 트랜잭션 종료 &amp;rarr; DB 커넥션 반환
    }

    // 트랜잭션 밖에서 실행됨
    private void sendNotificationAfterCommit(Notification notification) {
        sseEmitterService.send(notification.getReceiver().getUserId(), notification);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt; 메서드 안에서 모든 로직 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransactionSynchronizationManager&lt;/code&gt;를 사용해 커밋 후 실행할 작업 등록&lt;/li&gt;
&lt;li&gt;SSE 전송은 &lt;code&gt;afterCommit()&lt;/code&gt; 시점에 실행되어 트랜잭션과 분리&lt;/li&gt;
&lt;li&gt;DB 커넥션은 트랜잭션 커밋 즉시 반환됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 흐름 비교&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;개선 전:
@Transactional 시작
  &amp;rarr; DB 커넥션 획득
  &amp;rarr; 알림 저장 (0.01초)
  &amp;rarr; SSE 전송 (1~2초) &amp;larr; 여기서도 계속 커넥션 점유
  &amp;rarr; 트랜잭션 종료, 커넥션 반환
총 커넥션 점유 시간: 1~2초

개선 후:
@Transactional 시작
  &amp;rarr; DB 커넥션 획득
  &amp;rarr; 알림 저장 (0.01초)
  &amp;rarr; afterCommit 콜백 등록
  &amp;rarr; 트랜잭션 종료, 커넥션 반환 &amp;larr; 즉시 반환!

afterCommit 실행 (트랜잭션 밖)
  &amp;rarr; SSE 전송 (1~2초) &amp;larr; 커넥션과 무관
총 커넥션 점유 시간: 0.01초
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 id=&quot;4-&quot; data-ke-size=&quot;size23&quot;&gt;4. 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 발생 시 전체 시스템에 영향을 주지 않으면서 동시 접속자가 증가해도 안정적으로 운영할 수 있도록 만들었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB Active Connection이 정상 범위(5~10개)로 유지&lt;/li&gt;
&lt;li&gt;Connection Timeout 문제 완전히 해결&lt;/li&gt;
&lt;li&gt;동시 접속자 증가 시에도 안정적 운영&lt;/li&gt;
&lt;li&gt;서버 재시작 없이 장시간 안정 운영 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 팀원 테스트 결과 더 이상 느려지거나 타임아웃이 발생하지 않는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;5-&quot; data-ke-size=&quot;size23&quot;&gt;5. 정리&lt;/h3&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 호출&lt;/li&gt;
&lt;li&gt;이메일 전송&lt;/li&gt;
&lt;li&gt;파일 업로드&lt;/li&gt;
&lt;li&gt;SSE 전송&lt;/li&gt;
&lt;li&gt;Websocket 전송&lt;/li&gt;
&lt;li&gt;메시지 큐 전송&lt;/li&gt;
&lt;li&gt;긴 대기(Thread.sleep())&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 작업들은 모두 네트워크 I/O이거나 시간이 오래 걸리는 작업입니다. 트랜잭션은 DB 커넥션을 점유하므로 트랜잭션 안에서 시간이 오래 걸리는 작업을 하면 여러 사용자가 서비스를 사용할 때 Connection Pool 고갈로 인해 전체 시스템 장애가 발생할 수 있습니다.&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;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 메서드 분리 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;DB 작업 메서드에만 &lt;/span&gt;&lt;code style=&quot;letter-spacing: 0px;&quot;&gt;@Transactional&lt;/code&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 적용하여 네트워크 I/O 메서드와 완전히 분리&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;2. &lt;code&gt;TransactionalSynchronization&lt;/code&gt; 방식 (이번에 적용한 방법)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt; 메서드 안에서 &lt;code&gt;afterCommit()&lt;/code&gt; 콜백 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 커밋 성공 후에만 네트워크 I/O 실행(트랜잭션 일관성 보장)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring의 TransactionSynchronizationManager&amp;nbsp;:&amp;nbsp;&lt;br /&gt;트랜잭션 생명주기의 특정 시점에 콜백을 실행할 수 있게&amp;nbsp;해줍니다.&amp;nbsp;&lt;br /&gt;비동기가 아닌 지연 실행의 개념이며 같은 스레드에서 순차 실행합니다.&lt;/blockquote&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;3. 비동기 처리(&lt;code&gt;@Async&lt;/code&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 I/O를 별도 스레드에서 비동기로 처리(단, 에러 처리와 재시도 로직 필요)&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 범위는 DB 작업으로만 최소화&lt;/li&gt;
&lt;li&gt;네트워크 I/O는 트랜잭션 밖에서 처리&lt;/li&gt;
&lt;li&gt;외부 시스템 호출 실패가 트랜잭션에 영향 없도록 설계&lt;/li&gt;
&lt;li&gt;필요시 보상 트랜잭션(Compensation) 패턴 고려&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-&quot; data-ke-size=&quot;size23&quot;&gt;느낀 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 관성으로 붙였던 &lt;code&gt;@Transactional&lt;/code&gt;에 대해 깊이 있게 공부할 수 있었던 경험이었습니다. 이 경험을 통해 트랜잭션 범위를 항상 최소화하는 습관의 중요성, 네트워크 I/O와 DB 작업은 항상 분리해야 한다는 점, 문제 발생 시 로그와 모니터링을 통한 체계적 분석의 필요성을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &amp;ldquo;왜 이 어노테이션을 붙이는가?&amp;ldquo;에 대한 명확한 이유 없이 관성적으로 코드를 작성하는 것의 위험성을 깨달았습니다. 앞으로는 항상 이유가 있는 코드 작성을 습관화하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &amp;ldquo;Pool 크기를 늘리면 되겠지&amp;rdquo;라고 쉽게 생각했지만, 근본 원인을 파악하고 해결하는 과정에서 더 많이 배웠습니다. SSE 알림 기능 개발 시 같은 문제로 고민하시는 분들께 도움이 되었으면 좋겠습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@Transactional</category>
      <category>connection pool</category>
      <category>SSE</category>
      <category>트러블슈팅</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/120</guid>
      <comments>https://545aa7.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 4 Mar 2026 11:19:37 +0900</pubDate>
    </item>
    <item>
      <title>[스위프 웹 11기]NCP 활용 프로젝트 소개 - 써봄</title>
      <link>https://545aa7.tistory.com/119</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q1. 프로젝트를 소개해 주세요.&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;써봄&lt;/b&gt;은 AI 시대에 나만의 언어와 사고력을 키울 수 있는 글쓰기 루틴 서비스입니다. 생성형 AI 활용이 일상화되면서 대학생의 60.2%가 &quot;AI에 의존하며 사고력이 낮아질까 봐 두렵다&quot;고 응답하는 등, AI 의존도 증가에 따른 사고력 저하 우려가 커지고 있습니다. 이러한 문제를 해결하기 위해 매일 논리적/확장적 사고로 구분된 5가지 주제 중 하나를 선택하여 글을 쓰고, AI 코치의 구체적인 피드백을 받아 사고력을 키울 수 있는 서비스를 기획했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 가치&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실효성&lt;/b&gt;: 동국대 국어학 교수 2인의 자문을 받아 학문적으로 검증된 사고력 훈련 구조&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지속성&lt;/b&gt;: 캘린더, 캐릭터, 피드 기능 등을 통해 루틴을 유지할 수 있는 UX 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 기간&lt;/b&gt;: 2024.10.04 ~ 2024.11.28 (8주)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀 구성&lt;/b&gt;: PM(1), PD(2), FE(2), BE(3)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 링크&lt;/b&gt;: &lt;a href=&quot;https://www.seobom.site&quot;&gt;https://www.seobom.site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub&lt;/b&gt;: &lt;a href=&quot;https://github.com/SWYP-SUBOM/SWYP-SUBOM-BACKEND&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/SWYP-SUBOM/SWYP-SUBOM-BACKEND&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q2. Ncloud에서 어떤 서비스를 활용하셨나요?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Server (Compute)&lt;/b&gt;: 백엔드 애플리케이션 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CLOVA Studio&lt;/b&gt;: AI 피드백 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q3. Ncloud 서비스를 어떻게 적용하였나요?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;architecture.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuhaYR/dJMcag48oVT/p6v6iUVWbw2CjFjJviHC9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuhaYR/dJMcag48oVT/p6v6iUVWbw2CjFjJviHC9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuhaYR/dJMcag48oVT/p6v6iUVWbw2CjFjJviHC9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuhaYR%2FdJMcag48oVT%2Fp6v6iUVWbw2CjFjJviHC9K%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;717&quot; data-filename=&quot;architecture.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NCP Server 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 스펙&lt;/b&gt;: Standard s2-g3a (vCPU 2EA, Memory 8GB)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS&lt;/b&gt;: Ubuntu 24.04&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크&lt;/b&gt;: VPC 구성으로 보안 강화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 환경&lt;/b&gt;: Docker 컨테이너 기반 애플리케이션 배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI/CD&lt;/b&gt;: Jenkins를 통한 자동 배포 파이프라인 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLOVA Studio 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 비동기 처리 아키텍처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLOVA Studio API의 응답 시간(평균 2초 이상)을 고려하여 비동기 처리 방식을 도입했습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;요청 API&lt;/b&gt;: POST /api/posts/{postId}/ai-feedback
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉시 AiFeedback 엔티티 생성 (status: PROCESSING)&lt;/li&gt;
&lt;li&gt;202 Accepted와 함께 피드백 ID 반환&lt;/li&gt;
&lt;li&gt;백그라운드에서 @Async를 통해 CLOVA API 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조회 API&lt;/b&gt;: GET /api/posts/{postId}/ai-feedback/{id}
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 1-2초 간격으로 폴링&lt;/li&gt;
&lt;li&gt;처리 완료 시 status: COMPLETED와 함께 결과 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. AI 피드백 생성 프로세스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 작성한 글을 CLOVA Studio의 HyperCLOVA X 모델에 전송하여 다음 항목을 평가받습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글의 강점 분석&lt;/li&gt;
&lt;li&gt;논리&amp;middot;표현&amp;middot;구조 측면의 개선 포인트&lt;/li&gt;
&lt;li&gt;완성도 점수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Prompt Engineering&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 JSON 응답 형식을 요청하여 파싱 오류 없이 안정적으로 데이터를 처리합니다. 한국어 고맥락 언어 특성을 고려하여 조사 하나하나까지 신경 써서 프롬프트를 작성했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q4. Ncloud 사용 중 특히 만족했던 점과, 아쉬웠던 점은 무엇인가요?&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만족했던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. NCP Server&lt;/b&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;b&gt;한글 문서화&lt;/b&gt;: 공식 문서가 한글로 잘 정리되어 있어 초기 설정과 트러블슈팅이 수월했습니다. 특히 VPC, 서브넷, 보안 그룹 설정 등 네트워크 구성 시 한글 가이드가 큰 도움이 되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직관적인 설정&lt;/b&gt;: 콘솔 UI가 직관적이어서 서버 생성, 공인 IP 할당, 스토리지 구성 등을 빠르게 진행할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정적인 운영&lt;/b&gt;: 한 달 넘게 서버를 운영하면서 다운타임 없이 안정적으로 서비스를 제공할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. CLOVA Studio&lt;/b&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;b&gt;구조화된 응답&lt;/b&gt;: JSON 형태의 구조화된 응답이 일관되게 제공되어 파싱 오류 없이 안정적으로 처리할 수 있었습니다. 이 덕분에 예외 처리보다 프롬프트 품질 개선에 집중할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한국어 특화&lt;/b&gt;: 한국어로 자연스러운 피드백을 생성할 수 있어 서비스의 품질을 높일 수 있었습니다. 특히 고맥락 언어의 특성을 살린 섬세한 표현이 가능했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NCP 통합&lt;/b&gt;: NCP 생태계 내에서 Server와 CLOVA Studio를 함께 사용하여 인프라 관리가 용이했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아쉬웠던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. NCP Server&lt;/b&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;b&gt;사이드 프로젝트용 스펙 부족&lt;/b&gt;: Micro 서버는 Jenkins와 Docker를 함께 올리기에 너무 작았고, Standard 서버는 월 6~8만원으로 사이드 프로젝트에는 부담스러운 비용입니다. vCPU 1EA, Memory 4GB 정도의 중간 스펙이 있다면 더 많은 개발자들이 부담 없이 사용할 수 있을 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. CLOVA Studio&lt;/b&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;b&gt;제한된 토큰 수&lt;/b&gt;: 최대 토큰 제약으로 원하는 만큼 상세한 피드백을 제공하기 어려웠습니다. 긴 글에 대한 포괄적인 분석에 한계가 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 속도&lt;/b&gt;: 평균 2초 이상의 응답 시간으로 실시간성이 요구되는 UX 구현에 제약이 있어, 비동기 처리와 폴링 방식을 필수적으로 도입해야 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q5. Green Developers 프로그램 참여 소감 말씀 부탁드립니다. (50자 이상)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SI 회사에 입사하고 거의 일 년이 다 되어가는데 성장하고 있다고 느끼지 못했습니다. 사이드 프로젝트의 필요성을 절실히 느끼던 중 스위프 웹 11기에서 NCP 크레딧이 지원된다는 것을 알게 되었고, 프로젝트 비용 절감에 큰 매력을 느껴 참가하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 30만원의 크레딧 지원(초기 10만원, 스위프 연계 20만원)을 받아 Jenkins와 Docker를 올린 Standard 서버를 한 달 넘게 안정적으로 운영할 수 있었고, 덕분에 스위프 웹 11기에서 우수상을 받는 성과를 거둘 수 있었습니다. 크레딧 지원이 없었다면 비용 부담으로 프로젝트를 완성하기 어려웠을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Green Developers 프로그램은 단순히 크레딧을 지원하는 것을 넘어, 개발자들이 아이디어를 실제 서비스로 구현할 수 있는 기회를 제공한다는 점에서 큰 의미가 있다고 생각합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q6. 마지막 한 말씀 부탁드립니다.&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 진행 중 흥미로웠던 경험&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 비동기 처리 아키텍처 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLOVA Studio API의 긴 응답 시간을 어떻게 처리할지 고민하면서, 동기/비동기, 폴링/웹소켓/SSE 등 다양한 방식을 검토했습니다. 최종적으로 @Async 기반의 비동기 처리와 폴링 방식을 선택했는데, 구현이 간단하면서도 안정적이어서 만족스러웠습니다. 추후 Message Queue나 SSE로 개선할 계획도 세울 수 있었습니다.&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;2. Prompt Engineering의 재미&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLOVA Studio를 사용하면서 한국어 프롬프트 엔지니어링의 재미를 발견했습니다. 영어 기반 LLM과 달리 조사 하나, 어순 하나가 응답 품질에 큰 영향을 주는 것을 보며, 고맥락 언어의 특성을 실감할 수 있었습니다. 구조화된 JSON 응답의 안정성 덕분에 프롬프트 튜닝에만 집중할 수 있었던 것도 좋았습니다.&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;3. 비용에 따른 인프라 선택 경험&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 기능을 구현하는 것을 넘어, 실제 서비스 운영을 위한 인프라를 설계하면서 비용과 성능 사이의 균형점을 찾는 경험을 할 수 있었습니다. Micro 서버로는 Jenkins와 Docker를 함께 운영하기에 부족했고, Standard 서버는 사이드 프로젝트에 부담스러운 비용이었지만, 안정적인 CI/CD 파이프라인 구축을 위해 Standard를 선택했습니다. 이 과정에서 서버 스펙, 배포 전략, 모니터링 등을 종합적으로 고려하는 법을 배웠습니다. 앞으로 Test 환경과 Production 환경을 분리할 계획이며 이에 걸맞는 인프라 전략을 세우기 위해 또다시 많이 배우는 시간을 가질 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;향후 NCP 활용 계획&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;써봄 서비스를 앞으로도 계속 운영하면서 실제 사용자들의 피드백을 받고 개선해나갈 예정입니다. 3개월간 운영하며 루틴 유지율, 사고력 향상 체감도 등을 분석하고, 이를 바탕으로 개인 맞춤형 성장 리포트, 난이도별 주제 확장 등의 기능을 추가할 계획입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 사용자가 늘어나면 NCP의 Load Balancer, Auto Scaling 등 관리형 서비스를 도입하여 안정성과 확장성을 높일 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Green Developers 프로그램 피드백&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 클라우드 크레딧 지원이 우리 팀에게 너무나 큰 도움이 되고 있습니다. 다만 사이드 프로젝트를 진행하는 개발자들을 위해 Micro와 Standard 사이의 중간 스펙 서버가 추가된다면, 더 많은 개발자들이 부담 없이 NCP를 경험하고 좋은 서비스를 만들어낼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, Green Developers 프로그램을 통해 단순히 서비스를 만드는 것을 넘어 성장할 수 있는 기회를 얻었습니다. 감사합니다!&lt;/p&gt;</description>
      <category>Projects</category>
      <category>ncp</category>
      <category>SWYP</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/119</guid>
      <comments>https://545aa7.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 16 Dec 2025 01:14:45 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 7579. 앱</title>
      <link>https://545aa7.tistory.com/118</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7579&quot;&gt;[BOJ]7579.앱&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;우리는 스마트폰을 사용하면서 여러 가지 앱(App)을 실행하게 된다. 대개의 경우 화면에 보이는 ‘실행 중’인 앱은 하나뿐이지만 보이지 않는 상태로 많은 앱이 &amp;#39;활성화&amp;#39;되어 있다. 앱들이 활성화 되어 있다는 것은 화면에 보이지 않더라도 메인 메모리에 직전의 상태가 기록되어 있는 것을 말한다. 현재 실행 중이 아니더라도 이렇게 메모리에 남겨두는 이유는 사용자가 이전에 실행하던 앱을 다시 불러올 때에 직전의 상태를 메인 메모리로부터 읽어 들여 실행 준비를 빠르게 마치기 위해서이다.&lt;/p&gt;
&lt;p&gt;하지만 스마트폰의 메모리는 제한적이기 때문에 한번이라도 실행했던 모든 앱을 활성화된 채로 메인 메모리에 남겨두다 보면 메모리 부족 상태가 오기 쉽다. 새로운 앱을 실행시키기 위해 필요한 메모리가 부족해지면 스마트폰의 운영체제는 활성화 되어 있는 앱들 중 몇 개를 선택하여 메모리로부터 삭제하는 수밖에 없다. 이러한 과정을 앱의 ‘비활성화’라고 한다.&lt;/p&gt;
&lt;p&gt;메모리 부족 상황에서 활성화 되어 있는 앱들을 무작위로 필요한 메모리만큼 비활성화 하는 것은 좋은 방법이 아니다. 비활성화된 앱들을 재실행할 경우 그만큼 시간이 더 필요하기 때문이다. 여러분은 이러한 앱의 비활성화 문제를 스마트하게 해결하기 위한 프로그램을 작성해야 한다&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;현재 N개의 앱, A1, ..., AN이 활성화 되어 있다&lt;/strong&gt; 고 가정하자. 이들 &lt;strong&gt;앱 Ai는 각각 mi 바이트만큼의 메모리를 사용&lt;/strong&gt; 하고 있다. 또한, 앱 Ai를 비활성화한 후에 다시 실행하고자 할 경우, 추가적으로 들어가는 &lt;strong&gt;비용(시간 등)을 수치화 한 것을 ci&lt;/strong&gt; 라고 하자. 이러한 상황에서 사용자가 새로운 앱 B를 실행하고자 하여, 추가로 M 바이트의 메모리가 필요하다고 하자. 즉, 현재 활성화 되어 있는 앱 A1, ..., AN 중에서 몇 개를 비활성화 하여 M 바이트 이상의 메모리를 추가로 확보해야 하는 것이다. 여러분은 그 중에서 &lt;strong&gt;비활성화 했을 경우의 비용 ci의 합을 최소화하여 필요한 메모리 M 바이트를 확보하는 방법&lt;/strong&gt; 을 찾아야 한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;입력은 3줄로 이루어져 있다. 첫 줄에는 정수 N과 M이 공백문자로 구분되어 주어지며, 둘째 줄과 셋째 줄에는 각각 N개의 정수가 공백문자로 구분되어 주어진다. 둘째 줄의 N개의 정수는 현재 활성화 되어 있는 앱 A1, ..., AN이 사용 중인 메모리의 바이트 수인 m1, ..., mN을 의미하며, 셋째 줄의 정수는 각 앱을 비활성화 했을 경우의 비용 c1, ..., cN을 의미한다&lt;/p&gt;
&lt;p&gt;단, 1 ≤ N ≤ 100, 1 ≤ M ≤ 10,000,000이며, 1 ≤ m1, ..., mN ≤ 10,000,000을 만족한다. 또한, 0 ≤ c1, ..., cN ≤ 100이고, M ≤ m1 + m2 + ... + mN이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;필요한 메모리 M 바이트를 확보하기 위한 앱 비활성화의 최소의 비용을 계산하여 한 줄에 출력해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;예제 입력&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 60
30 10 20 35 40
3 0 3 5 4&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;h3&gt;예제 출력&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;접근 방법&lt;/h2&gt;
&lt;p&gt;제한된 메모리에서 필요한 메모리를 확보하기 위해 앱을 비활성화할 때, &lt;strong&gt;필요한 메모리를 확보하면서 비활성화 비용을 최소화&lt;/strong&gt; 하는 방법을 찾아야 하는 문제입니다. 특정 용량 내에서 최대 가치를 찾는 문제인 배낭 문제와 비슷한 유형입니다. N의 최대값이 100이므로 각 앱을 비활성화할지 말지를 &lt;strong&gt;완전탐색으로 푼다면 시간 복잡도는 O(2^N)&lt;/strong&gt; 이 됩니다. 따라서 완전탐색으로 풀 수 없는 문제입니다.&lt;br&gt;문제를 분석해보면, &lt;strong&gt;최적 부분 구조&lt;/strong&gt; 가 있다는 것을 알 수 있습니다. 최적 부분 구조란 큰 문제의 해답이 작은 문제들의 해답으로 이루어진다는 성질입니다. 예를 들어, 앱 1,3을 비활성화했을 때 필요한 최소 비용을 구한다고 합시다. 이제 앱 4를 추가해서 비활성화할 때 앱 1,3을 비활성화한 상태에서 비용과 메모리를 그대로 활용할 수 있습니다. 즉, 이전 계산 결과를 재활용할 수 있습니다.&lt;br&gt;같은 문제를 반복해서 계산할 필요가 없다는 점에서 &lt;strong&gt;DP&lt;/strong&gt; 를 사용하여 문제를 풀 수 있고, DP 테이블은 배낭 문제에서 &lt;code&gt;dp[무게]&lt;/code&gt;를 정의하는 것처럼 &lt;code&gt;dp[비용]&lt;/code&gt;으로 정의할 수 있겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;DP 테이블 정의&lt;/h3&gt;
&lt;p&gt;dp[c]를 비용 c를 사용하여 확보할 수 있는 최대 메모리라고 정의합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;초기화&lt;/h3&gt;
&lt;p&gt;비용이 0일 때 확보할 수 있는 메모리는 0입니다. dp 배열의 초기값을 0으로 만들어줍니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;점화식&lt;/h3&gt;
&lt;p&gt;각 앱 i에 대해 비용이 c일 때, 현재 i번 앱을 비활성화하면 이전 상태에서 추가로 메모리를 확보할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = Math.max(dp[j], dp[j - costs[i]] + bytes[i]);&lt;/code&gt;&lt;/pre&gt;&lt;ol&gt;
&lt;li&gt;현재 앱을 비활성화 하지 않는 경우&lt;br&gt;이전 단계에서 얻은 비용 &lt;code&gt;dp[j]&lt;/code&gt;는 그대로 유지됩니다.&lt;/li&gt;
&lt;li&gt;현재 앱 i를 비활성화 하는 경우&lt;br&gt;비용 j 중에서 비활성화 비용 costs[i] 만큼 사용하게 됩니다. 즉, 비용 j에서 costs[i]를 뺀 비용으로 확보한 메모리에 현재 앱이 사용하고 있는 메모리 bytes[i]를 추가로 확보하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;같은 비용이라면 더 큰 메모리를 확보할 수 있는 경우를 선택합니다. 즉, 이 두 값 중 더 큰 값을 선택합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;시간 복잡도&lt;/h3&gt;
&lt;p&gt;N개의 앱에 대해 최대 sum(costs) 만큼의 비용에 대해 계산합니다. 각 앱에 대해 비용을 갱신하므로 시간 복잡도는 &lt;strong&gt;O(N * sumCosts)&lt;/strong&gt; 가 됩니다. sumCosts는 각 앱의 비활성화 비용의 합입니다. N은 최대 100까지 가능하고 각 앱의 비활성화 비용은 최대 100이므로 &lt;code&gt;최악의 경우 sumCosts는 10,000&lt;/code&gt;이 될 수 있습니다.&lt;br&gt;따라서, &lt;code&gt;N * sumCosts = 100 * 10,000 = 1,000,000&lt;/code&gt;이 되어 제한 시간 1초 안에 해결할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;전체 로직&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;각 앱의 메모리 사용량과 비활성화 비용을 입력 받습니다.&lt;/li&gt;
&lt;li&gt;dp 배열을 초기화하고 비용을 0에서 시작합니다.&lt;/li&gt;
&lt;li&gt;각 앱에 대해 비용을 역순으로 처리하면서 현재 앱을 비활성화하는 경우와 그렇지 않은 경우를 고려해 DP 테이블을 갱신합니다.&lt;/li&gt;
&lt;li&gt;dp 배열에서 확보한 메모리가 M 이상인 최소 비용을 찾습니다.&lt;/li&gt;
&lt;li&gt;최소 비용을 출력합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;h3&gt;역순 탐색 이유&lt;/h3&gt;
&lt;p&gt;한 번의 반복에서 갱신된 dp 값을 다시 참조하는 것을 방지하기 위함입니다. 예를 들어, 순차적으로 탐색할 경우 dp[j]를 갱신한 후, 그 값이 다음 &lt;code&gt;dp[j+1]&lt;/code&gt;을 갱신하는 데 사용되면 &lt;strong&gt;같은 앱을 여러 번 비활성화 하는 것&lt;/strong&gt; 과 같은 문제가 발생합니다. 역순 탐색을 하면 이전 상태에서만 계산이 이루어지기 때문에 같은 앱을 중복으로 비활성화하는 일이 생기지 않습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어, 다음과 같은 입력이 주어진다고 가정합니다.&lt;br&gt;앱의 개수 N = 3&lt;br&gt;확보해야 하는 메모리 M = 6&lt;br&gt;각 앱이 사용하는 메모리 : 4MB, 2MB, 3MB&lt;br&gt;각 앱의 비활성화 비용 : 3, 1, 2  &lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;1번 앱(4MB, 비용 3) 처리 (&lt;strong&gt;순차 탐색&lt;/strong&gt;):&lt;br&gt;dp[3] = max(dp[3], dp[0] + 4) → dp[3] = max(0, 0 + 4) = 4&lt;br&gt;dp[4] = max(dp[4], dp[1] + 4) → dp[4] = max(0, 0 + 4) = 4&lt;br&gt;dp[5] = max(dp[5], dp[2] + 4) → dp[5] = max(0, 0 + 4) = 4&lt;br&gt;dp[6] = max(dp[6], dp[3] + 4) → dp[6] = max(0, 4 + 4) = 8&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;dp[6]을 갱신할 때 이미 갱신된 dp[3]의 값을 참조하고 있습니다. 즉, 1번 앱을 비활성화해서 4MB를 확보한 상태에서 또다시 d[3]을 참조해 중복으로 앱 1을 비활성화한 것처럼 계산됩니다.&lt;br&gt;역순 탐색은 이미 갱신된 값을 다음 계산에 사용하지 않도록 보장합니다. &lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;1번 앱(4MB, 비용 3) 처리 (&lt;strong&gt;역순 탐색&lt;/strong&gt;):&lt;br&gt;dp[6] = max(dp[6], dp[6 - 3] + 4) → dp[6] = max(0, 0 + 4) = 4&lt;br&gt;dp[5] = max(dp[5], dp[5 - 3] + 4) → dp[5] = max(0, 0 + 4) = 4&lt;br&gt;dp[4] = max(dp[4], dp[4 - 3] + 4) → dp[4] = max(0, 0 + 4) = 4&lt;br&gt;dp[3] = max(dp[3], dp[3 - 3] + 4) → dp[3] = max(0, 0 + 4) = 4&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;예시&lt;/h3&gt;
&lt;p&gt;다음과 같은 입력이 주어진다고 가정합니다.&lt;/p&gt;
&lt;p&gt;앱의 개수 N = 3&lt;br&gt;확보해야 하는 메모리 M = 6&lt;br&gt;각 앱이 사용하는 메모리 : 4MB, 2MB, 3MB&lt;br&gt;각 앱의 비활성화 비용 : 3, 1, 2&lt;br&gt;이때 모든 앱을 비활성화할 때 발생할 수 있는 총 비용을 계산합니다. 여기서는 &lt;code&gt;sumCosts = 3 + 1 + 2 = 6&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;각 앱을 비활성화할지 말지 결정하면서 DP 테이블을 갱신합니다.&lt;br&gt;이때, 각 앱을 순차적으로 처리하고 비용을 역순으로 처리하여 중복 계산을 방지합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;1번 앱&lt;/strong&gt; 처리&lt;br&gt;&lt;code&gt;dp[3]&lt;/code&gt; 부터 &lt;code&gt;dp[sumCosts]&lt;/code&gt; 까지 갱신합니다.&lt;br&gt;&lt;code&gt;j = 6&lt;/code&gt;일 때: dp[6] = max(dp[6], dp[6 - 3] + 4) = max(0, 0 + 4) = 4&lt;br&gt;&lt;code&gt;j = 5&lt;/code&gt;일 때: dp[5] = max(dp[5], dp[5 - 3] + 4) = max(0, 0 + 4) = 4&lt;br&gt;&lt;code&gt;j = 4&lt;/code&gt;일 때: dp[4] = max(dp[4], dp[4 - 3] + 4) = max(0, 0 + 4) = 4&lt;br&gt;&lt;code&gt;j = 3&lt;/code&gt;일 때: dp[3] = max(dp[3], dp[3 - 3] + 4) = max(0, 0 + 4) = 4&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: &lt;code&gt;dp = [0, 0, 0, 4, 4, 4, 4]&lt;/code&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;2번 앱&lt;/strong&gt; 처리&lt;br&gt;&lt;code&gt;dp[6]&lt;/code&gt;부터 &lt;code&gt;dp[1]&lt;/code&gt;까지 역순으로 갱신합니다.&lt;br&gt;&lt;code&gt;j = 6&lt;/code&gt;일 때: dp[6] = max(dp[6], dp[6 - 1] + 2) = max(4, 4 + 2) = 6&lt;br&gt;&lt;code&gt;j = 5&lt;/code&gt;일 때: dp[5] = max(dp[5], dp[5 - 1] + 2) = max(4, 4 + 2) = 6&lt;br&gt;&lt;code&gt;j = 4&lt;/code&gt;일 때: dp[4] = max(dp[4], dp[4 - 1] + 2) = max(4, 4 + 2) = 6&lt;br&gt;&lt;code&gt;j = 3&lt;/code&gt;일 때: dp[3] = max(dp[3], dp[3 - 1] + 2) = max(4, 0 + 2) = 4&lt;br&gt;&lt;code&gt;j = 2&lt;/code&gt;일 때: dp[2] = max(dp[2], dp[2 - 1] + 2) = max(0, 0 + 2) = 2&lt;br&gt;&lt;code&gt;j = 1&lt;/code&gt;일 때: dp[1] = max(dp[1], dp[1 - 1] + 2) = max(0, 0 + 2) = 2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: &lt;code&gt;dp = [0, 2, 2, 4, 6, 6, 6]&lt;/code&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;3번 앱&lt;/strong&gt; 처리&lt;br&gt;&lt;code&gt;dp[6]&lt;/code&gt;부터 &lt;code&gt;dp[2]&lt;/code&gt;까지 역순으로 갱신합니다.&lt;br&gt;&lt;code&gt;j = 6&lt;/code&gt;일 때: dp[6] = max(dp[6], dp[6 - 2] + 3) = max(6, 6 + 3) = 9&lt;br&gt;&lt;code&gt;j = 5&lt;/code&gt;일 때: dp[5] = max(dp[5], dp[5 - 2] + 3) = max(6, 4 + 3) = 7&lt;br&gt;&lt;code&gt;j = 4&lt;/code&gt;일 때: dp[4] = max(dp[4], dp[4 - 2] + 3) = max(6, 2 + 3) = 6&lt;br&gt;&lt;code&gt;j = 3&lt;/code&gt;일 때: dp[3] = max(dp[3], dp[3 - 2] + 3) = max(4, 2 + 3) = 5&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: &lt;code&gt;dp = [0, 2, 2, 5, 6, 7, 9]&lt;/code&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;확보해야 하는 메모리 6 이상인 최소 비용은 dp[4]입니다. 이 예시의 출력은 4가 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;

public class BJ_G3_7579 {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static int N; // 앱의 개수
    static int M; // 필요한 메모리
    static int[] bytes; // 앱이 사용하고 있는 메모리
    static int[] costs; // 해당 앱을 비활성화 할 경우의 비용
    static int sumCosts;
    static int[] dp; // i만큼의 비용으로 확보할 수 있는 최대 메모리 크기

    public static void main(String[] args) throws Exception {
        init();
        solve();
        output();
    }

    public static void init() throws Exception {
        String[] inputs = br.readLine().split(&amp;quot; &amp;quot;);
        N = Integer.parseInt(inputs[0]);
        M = Integer.parseInt(inputs[1]);

        bytes = new int[N + 1];
        costs = new int[N + 1];

        String[] byteInputs = br.readLine().split(&amp;quot; &amp;quot;);
        String[] costInputs = br.readLine().split(&amp;quot; &amp;quot;);
        for (int i = 1; i &amp;lt;= N; i++) {
            bytes[i] = Integer.parseInt(byteInputs[i - 1]);
            costs[i] = Integer.parseInt(costInputs[i - 1]);
            sumCosts += costs[i];
        }

        dp = new int[sumCosts + 1];
    }

    public static void solve() {
        for (int i = 1; i &amp;lt;= N; i++) {
            for (int j = sumCosts; j &amp;gt;= costs[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - costs[i]] + bytes[i]);
            }
        }
    }

    public static void output() {
        for (int i = 0; i &amp;lt; dp.length; i++) {
            if (dp[i] &amp;gt;= M) {
                System.out.println(i);
                break;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm</category>
      <category>DP</category>
      <category>Knapsack</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/118</guid>
      <comments>https://545aa7.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 2 Oct 2024 00:25:50 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 10971. 외판원 순회2</title>
      <link>https://545aa7.tistory.com/117</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/10971&quot;&gt;[BOJ]10971. 외판원 순회2&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;외판원 순회 문제는 영어로 Traveling Salesman problem (TSP) 라고 불리는 문제로 computer science 분야에서 가장 중요하게 취급되는 문제 중 하나이다. 여러 가지 변종 문제가 있으나, 여기서는 가장 일반적인 형태의 문제를 살펴보자.&lt;/p&gt;
&lt;p&gt;1번부터 N번까지 번호가 매겨져 있는 도시들이 있고, 도시들 사이에는 길이 있다. (길이 없을 수도 있다) 이제 한 외판원이 &lt;code&gt;어느 한 도시에서 출발해 N개의 도시를 모두 거쳐 다시 원래의 도시로 돌아오는 순회 여행 경로&lt;/code&gt;를 계획하려고 한다. 단, 한 번 갔던 도시로는 다시 갈 수 없다. (맨 마지막에 여행을 출발했던 도시로 돌아오는 것은 예외) 이런 여행 경로는 여러 가지가 있을 수 있는데, &lt;code&gt;가장 적은 비용&lt;/code&gt;을 들이는 여행 계획을 세우고자 한다.&lt;/p&gt;
&lt;p&gt;각 도시간에 이동하는데 드는 비용은 행렬 W[i][j]형태로 주어진다. W[i][j]는 도시 i에서 도시 j로 가기 위한 비용을 나타낸다. 비용은 대칭적이지 않다. 즉, W[i][j] 는 W[j][i]와 다를 수 있다. 모든 도시간의 비용은 양의 정수이다. W[i][i]는 항상 0이다. 경우에 따라서 도시 i에서 도시 j로 갈 수 없는 경우도 있으며 이럴 경우 W[i][j]=0이라고 하자.&lt;/p&gt;
&lt;p&gt;N과 비용 행렬이 주어졌을 때, 가장 적은 비용을 들이는 외판원의 순회 여행 경로를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 도시의 수 N이 주어진다. (2 ≤ N ≤ 10) 다음 N개의 줄에는 비용 행렬이 주어진다. 각 행렬의 성분은 1,000,000 이하의 양의 정수이며, 갈 수 없는 경우는 0이 주어진다. W[i][j]는 도시 i에서 j로 가기 위한 비용을 나타낸다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 외판원의 순회에 필요한 최소 비용을 출력한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;예제 입력&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
0 10 15 20
5 0 9 10
6 13 0 12
8 8 9 0&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;h3&gt;예제 출력&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;35&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;hr&gt;
&lt;h2&gt;접근 방법&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;양의 가중치를 가지는 그래프에서 최소 비용으로 여행을 간다는 점을 보고 &lt;code&gt;다익스트라&lt;/code&gt;를 먼저 떠올렸습니다.&lt;/li&gt;
&lt;li&gt;그러나 다익스트라는 하나의 출발지에서 다른 모든 도시까지의 최단 경로를 찾을 때에는 적합하지만, 모든 도시를 반드시 순회해야 하는 TSP 문제에는 적합하지 않습니다.&lt;/li&gt;
&lt;li&gt;따라서 모든 도시를 거치는 &lt;code&gt;dfs&lt;/code&gt;와 모든 경로를 탐색해야 하므로 &lt;code&gt;백트래킹&lt;/code&gt;을 사용하는 방법을 생각했습니다.&lt;/li&gt;
&lt;li&gt;N의 크기가 최대 10까지이므로 &lt;code&gt;완전탐색&lt;/code&gt;으로 문제를 풀 수 있을 것이라 판단했습니다.&lt;br&gt;인접 행렬로 표현된 그래프의 DFS 시간복잡도 : O(V^2) (V: 정점의 개수)&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;h2&gt;주요 로직&lt;/h2&gt;
&lt;h3&gt;1. 인접행렬로 그래프 표현&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public static void init() throws Exception {
    N = Integer.parseInt(br.readLine());

    costs = new int[N][N];
    visit = new boolean[N];
    result = Integer.MAX_VALUE;

    for (int i = 0; i &amp;lt; N; i++) {
        String[] inputs = br.readLine().split(&amp;quot; &amp;quot;);
        for (int j = 0; j &amp;lt; N; j++) {
            costs[i][j] = Integer.parseInt(inputs[j]);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;모든 도시를 순회하며 최소 비용 경로를 찾아야 하므로 이동 비용이 자주 조회될 것이라 생각하여 인접행렬로 그래프를 표현했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;2. dfs + 백트래킹&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public static void dfs(int node, int cnt, int sum, int start) {
    if(cnt == N) {

        // 순회가 되지 않는다면 패스
        if(costs[node][start] == 0) {
            return;
        }

        result = Math.min(result, sum + costs[node][start]);
        return;
    }

    visit[node] = true;

    for (int i = 0; i &amp;lt; costs[node].length; i++) {
        if (visit[i] || costs[node][i] == 0) {
            continue;
        }

        dfs(i, cnt + 1, sum + costs[node][i], start);
    }
    visit[node] = false;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;도시를 DFS로 순회하며 경로를 탐색하고, 각 경로의 비용을 계산합니다. 경로가 완료되면 출발 도시로 돌아가는 비용을 더해 최소 비용을 갱신하고 백트래킹으로 모든 경로를 탐색합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 0, 1, 2, 3 노드가 모두 각각의 노드에게 간선이 연결되어 있다고 가정해봅시다. 처음에는 0 → 1 → 2 → 3 -&amp;gt; 0 순으로 방문할 것이고 다음에는 0 → 1→ 3→ 2 -&amp;gt; 0 순으로 방문할 것입니다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;cnt&lt;/code&gt;가 도시의 총 개수(N)와 같다면, 이는 모든 도시를 방문한 것이므로, 마지막 노드에서 출발 노드로 돌아가는 비용을 더해 결과를 갱신합니다. 이때 순회 경로가 만들어지지 않는다면 해당 경로는 무시합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;hr&gt;
&lt;h2&gt;최종 코드&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;메모리 : 14996 kb&lt;/li&gt;
&lt;li&gt;시간: 280 ms&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;

public class BJ_S1_10971 {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static int N;
    static int[][] costs;
    static boolean[] visit;
    static int result;

    public static void main(String[] args) throws Exception {
        init();
        solve();
        output();
    }

    public static void output() {
        System.out.println(result);
    }

    public static void solve() {
        for (int i = 0; i &amp;lt; N; i++) {
            dfs(i, 1, 0, i);
        }
    }

    public static void dfs(int node, int cnt, int sum, int start) {
        if(cnt == N) {

            // 순회가 되지 않는다면 패스
            if(costs[node][start] == 0) {
                return;
            }

            result = Math.min(result, sum + costs[node][start]);
            return;
        }

        visit[node] = true;

        for (int i = 0; i &amp;lt; costs[node].length; i++) {
            if (visit[i] || costs[node][i] == 0) {
                continue;
            }

            dfs(i, cnt + 1, sum + costs[node][i], start);
        }
        visit[node] = false;
    }

    public static void init() throws Exception {
        N = Integer.parseInt(br.readLine());

        costs = new int[N][N];
        visit = new boolean[N];
        result = Integer.MAX_VALUE;

        for (int i = 0; i &amp;lt; N; i++) {
            String[] inputs = br.readLine().split(&amp;quot; &amp;quot;);
            for (int j = 0; j &amp;lt; N; j++) {
                costs[i][j] = Integer.parseInt(inputs[j]);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm</category>
      <category>10971</category>
      <category>dfs</category>
      <category>외판원순회</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/117</guid>
      <comments>https://545aa7.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 3 Sep 2024 01:08:40 +0900</pubDate>
    </item>
    <item>
      <title>JDK, JRE, JVM</title>
      <link>https://545aa7.tistory.com/116</link>
      <description>&lt;p&gt;&lt;img width=&quot;70%&quot; src=&quot;https://github.com/user-attachments/assets/b6e59661-1258-4a42-87bd-c2790ff5da56&quot;&gt;&lt;/img&gt;&lt;/p&gt;
&lt;h2&gt;JDK(Java Development Kit)&lt;/h2&gt;
&lt;p&gt;JDK는 Java Development Kit의 약자로 자바 개발자들이 개발할 때 필요한 도구 모임입니다. JDK에는 대표적으로  javac(자바 컴파일러, 자바 소스 코드를 바이트 코드로 컴파일), JRE(Java Runtime Environment, jdb(Java 디버거), javadoc(문서 생성기, 자바 소스 코드에서 API문서를 생성), jar(Java 아카이브 도구)가 있습니다.&lt;/p&gt;
&lt;p&gt;즉, JDK는 자바 애플리케이션을 개발하고 실행하는 데 필요한 모든 도구를 제공합니다. Oracle, OpenJDK, Amazon Corretto 등 여러 배포판이 존재합니다.&lt;/p&gt;
&lt;h2&gt;JRE(Java Runtime Environment)&lt;/h2&gt;
&lt;p&gt;JRE는 Java Runtime Environment의 약자로 자바 런타임 환경을 의미하며 자바 애플리케이션을 실행하기 위한 환경을 제공합니다. JRE에는 JVM, 클래스 라이브러리(자바 기본 클래스 라이브러리), 구성 파일이 포함됩니다.&lt;br&gt;자바 애플리케이션을 개발하기 위해서는 JDK가 필요하지만, 이미 개발된 자바 애플리케이션을 실행하려면 JRE만으로 충분히 가능합니다. JRE는 Oracle, OpenJDK 등 여러 배포판이 존재합니다.&lt;/p&gt;
&lt;h2&gt;JVM(Java Virtual Machine)&lt;/h2&gt;
&lt;p&gt;JVM은 Java virtual Machine의 약자로 자바 가상 머신을 의미합니다. JVM은 자바 프로그램을 실행하기 위한 가상화된 컴퓨터 시스템으로 OS와 프로그램 사이에 하나의 추상 계층이 추가된 것입니다.  기존에는 OS 또는 하드웨어에 종속적으로 프로그래밍해야 했는데, JVM을 통해 한 번의 프로그래밍으로도 여러 환경에 독립적으로 실행할 수 있게 만들었습니다. 즉, 프로그래머가 더 중요한 것에 집중할 수 있는 환경을 제공해 준 가상 머신입니다.&lt;br&gt;JVM에는 Class Loader(자바 바이트 코드를 Runtime data area에 로딩하는 스레드), 실행 엔진(인터프리터, JIT 컴파일러), GC 등이 포함되어 있습니다.&lt;br&gt;JVM에 관해서는 다음 글에서 더 자세히 설명하겠습니다.&lt;/p&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/JAVA-&quot;&gt;☕-JDK-JRE-JVM-개념-구성-원리- -완벽-총정리&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <category>JDK</category>
      <category>jre</category>
      <category>jvm</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/116</guid>
      <comments>https://545aa7.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 16 Jul 2024 18:45:27 +0900</pubDate>
    </item>
    <item>
      <title>Git &amp;amp; Github를 활용한 협업 프로세스</title>
      <link>https://545aa7.tistory.com/114</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Commit, Branch, PR 등 컨벤션을 정합니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양식이 통일되어야 원활하게 확인할 수 있기 때문에 컨벤션을 미리 정하고 협업을 하는 것이 권장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Commit Message Convention&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 사용하는 commit convention은 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;type: subject

body

footer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;code&gt;type&lt;/code&gt; : 변경 사항의 유형, 소문자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subject&lt;/code&gt; : 간결한 변경 사항 설명, 첫 글자는 소문자로 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;code&gt;body&lt;/code&gt; : 변경 사항에 대한 자세한 설명&lt;/li&gt;
&lt;li&gt;&lt;code&gt;footer&lt;/code&gt; : 추가적인 메타데이터, 관련 이슈 참조 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;code&gt;feat&lt;/code&gt; : 새로운 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix&lt;/code&gt; : 버그 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs&lt;/code&gt; : 문서 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style&lt;/code&gt; : 코드의 의미에 영향을 미치지 않는 변경사항&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor&lt;/code&gt; : 코드 리팩토링(기능은 그대로지만 코드 재구성)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; : 테스트 추가 및 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore&lt;/code&gt; : 빌드, 보조 도구, 라이브러리 등의 변경사항&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Branch Strategy&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 사용하는 Git 브랜치 전략에는 대표적으로 Git Flow 가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/jude0124/post/1757743e-9d52-4d62-aaf3-75135fa70bad/image.png&quot; alt=&quot;git_flow&quot; /&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;code&gt;main&lt;/code&gt; : 안정적인 프로덕션 코드 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;develop&lt;/code&gt; : 기능을 통합하는 브랜치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feature&lt;/code&gt; : 새로운 기능 개발, dev 브랜치에서 파생
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명명 예 : &lt;code&gt;feature/sign-up&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release&lt;/code&gt; : 배포 준비를 위해 버그 수정 및 최종 조정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명명 예: &lt;code&gt;release/1.0.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotfix&lt;/code&gt; : 프로덕션에서 발생한 긴급 버그 수정, main에서 파생
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명명 예: hotfix/login-bug&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Pull Request(PR) 템플릿&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 명확하게 작성하고 팀의 코드 리뷰 프로세스를 체계적으로 유지하기 위해 사용합니다. 팀마다 다르지만 일반적으로 다음과 같은 섹션을 포함합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 제목
2. 설명
3. 체크리스트
4. 관련 이슈
5. 스크린 샷(선택)
6. 주석&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;## 제목
&amp;lt;!-- 
간결하고 명확한 PR 제목을 작성하세요. 
예시: &quot;Add OAuth 2.0 support for user authentication&quot; 
--&amp;gt;

## 설명
&amp;lt;!-- 
변경 사항에 대한 자세한 설명을 작성하세요.
무엇이 변경되었는지, 왜 변경되었는지, 어떻게 변경되었는지를 설명합니다.
--&amp;gt;

## 체크리스트
&amp;lt;!-- 
PR을 제출하기 전에 다음 항목들을 확인하세요.
--&amp;gt;
- [ ] PR 제목은 적절하고 명확합니다.
- [ ] 코드가 의도한 대로 동작합니다.
- [ ] 새로운 기능이나 수정된 기능에 대한 테스트를 추가했습니다.
- [ ] 문서(README 등)를 업데이트했습니다.
- [ ] 로컬 환경에서 모든 테스트를 통과했습니다.

## 관련 이슈
&amp;lt;!-- 
관련된 이슈 번호를 참조합니다.
예시: &quot;Closes #123&quot; 
--&amp;gt;
Closes #

## 스크린샷
&amp;lt;!-- 
UI 변경 사항이 있는 경우 스크린샷을 추가하세요.
--&amp;gt;
![스크린샷 설명](URL)

## 주석
&amp;lt;!-- 
리뷰어에게 도움이 될 추가 정보나 주석을 작성하세요.
--&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PR 템플릿 사용 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Repository에서 PR 템플릿을 추가하려면 &lt;code&gt;.github&lt;/code&gt; 디렉토리에 &lt;code&gt;PULL_REQUEST_TEMPATE.md&lt;/code&gt; 파일을 생성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 PR을 만들 때 자동으로 템플릿 내용이 표준화되기 때문에 코드 리뷰가 체계적으로 이루어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초기 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀장이 Repository를 만들고(Organization으로 만들면 팀의 관심사만 분리되기 때문에 사용하는 것이 좋습니다) 팀원들을 초대합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Repository의 &lt;code&gt;main&lt;/code&gt; 브랜치에서 &lt;code&gt;develop&lt;/code&gt; 브랜치를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기능 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 팀원들은 Github Repository를 로컬로 clone 합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone {Github Repository Https 주소}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 가져온 저장소에서 &lt;code&gt;develop&lt;/code&gt; 브랜치로 이동하고 새로운 기능 브랜치를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// 현재 브랜치(dev)에서 새로운 기능 브랜치 생성
git switch -c {feature/기능}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때, 만약 &lt;code&gt;develop&lt;/code&gt; 브랜치에 갱신된 코드가 있다면 &lt;code&gt;develop&lt;/code&gt;에서 pull을 받습니다.&lt;/li&gt;
&lt;li&gt;작업하는 중인 코드가 존재한다면 &lt;code&gt;develop&lt;/code&gt;로 브랜치를 이동할 수 없기 때문에 잡시 stash 영역(임시 저장 영)에 넣어둡니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git stash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;dev 브랜치에서 pull 받은 코드를 현재 기능 구현하고 있는 브랜치에 병합합니다. 그러기 위해서는 작업하고 있는 브랜치로 이동해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;// 브랜치 이동 git switch {작업하고 있는 브랜치} // dev 브랜치 -&amp;gt; 작업하고 있는 브랜치 병합 git merge develop // 작업하던 기능 stash 영역에서 가져오기 git stash pop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 기능을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 완료한 기능을 저장소에 반영합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;add&lt;/li&gt;
&lt;li&gt;&lt;code&gt;  git add {파일} // 파일명이 아닌 . 을 입력하면 변경된 모든 파일이 add 됩니다.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;git의 staging 영역에 파일을 추가합니다.&lt;/li&gt;
&lt;li&gt;commit&lt;code&gt;-m&lt;/code&gt; : commit message 를 인라인으로 작성하라는 옵션입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;  git commit -m {commit message}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;스테이징 영역에 추가된 변경 사항을 영구적으로 로컬에 저장하는 명령어입니다.&lt;/li&gt;
&lt;li&gt;pushremote에 로컬 브랜치와 같은 이름의 브랜치를 만들면서 코드를 push하라는 명령어입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push origin {로컬 브랜치와 같은 브랜치명}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) Github에 push한 브랜치에서 &lt;code&gt;develop&lt;/code&gt; 브랜치로 Pull Request를 보냅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀원들은 해당 PR에 코멘트를 작성합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 출돌이 없으면 merge 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 충돌이 있으면 PR을 close 하고 코드를 수정합니다.&lt;/li&gt;
&lt;li&gt;같은 PR에서 reopen 한 후 &amp;lsquo;수정완료&amp;rsquo; 와 같은 코멘트를 작성합니다.&lt;/li&gt;
&lt;li&gt;팀원들의 확인 후에 아무 문제가 없으면 merge 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 개발 환경에 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;develop&lt;/code&gt; 브랜치에 각 기능 개발이 완료된 코드를 병합 완료하면 release 브랜치에 pr 및 merge를 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 배포 환경에서 문제가 있다면 hotfix 브랜치를 새로 만들어 버그를 고칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 운영 환경에 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;release 브랜치의 코드가 정상 작동한다면 안정적으로 운영하는 main 브랜치에 release 브랜치의 코드를 머지합니다.&lt;/p&gt;</description>
      <category>Projects</category>
      <category>git #github #협업프로세스</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/114</guid>
      <comments>https://545aa7.tistory.com/114#entry114comment</comments>
      <pubDate>Fri, 24 May 2024 17:54:13 +0900</pubDate>
    </item>
    <item>
      <title>[JazzMeet]Cookie &amp;amp; Session vs. JWT</title>
      <link>https://545aa7.tistory.com/113</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Jazz Meet 프로젝트를 진행하면서 관리자 계정을 어떻게 구현해야 할지 고민한 내용입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;⭐ 첫 번째 고민: Cookie &amp;amp; Session vs. JWT&lt;/h2&gt;
&lt;h3&gt; ️ Cookie &amp;amp; Session 기반 인증&lt;/h3&gt;
&lt;p&gt;Cookie와 Session 기반 인증 로직은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자 로그인 요청&lt;/li&gt;
&lt;li&gt;서버에서 인증 처리&lt;br&gt;1) 받은 아이디와 비밀번호 검증&lt;br&gt;2) 인증이 성공하면, 서버는 이 사용자에 대한 세션 생성&lt;/li&gt;
&lt;li&gt;서버는 생성된 세션 ID를 사용자의 웹 브라우저에 쿠키 형태로 전송&lt;/li&gt;
&lt;li&gt;브라우저가 서버에 요청을 보낼 때마다 세션 ID를 쿠키로 같이 전송&lt;/li&gt;
&lt;li&gt;사용자가 로그아웃을 요청하면, 서버는 해당 사용자의 세션을 종료(삭제)하고, 사용자의 브라우저에 저장된 쿠키(세션ID)를 무효화&lt;br&gt;1) 이후 사용자가 다시 인증이 필요한 페이지에 접근하려고 하면, 서버는 유효한 세션 ID가 쿠키에 없기 때문에 사용자를 비인증 상태로 판단&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Sesstion &amp;amp; Cookie 기반 인증의 장단점&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;br&gt;서버가 사용자의 로그인 상태를 쉽게 관리할 수 있고, 사용자는 여러 페이지를 이동하면서도 로그인 상태를 유지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;쿠키를 사용하기 때문에 보안상 주의가 필요&lt;/li&gt;
&lt;li&gt;서버에서 세션 저장소를 사용하므로 요청이 많아질 경우 서버에 부하가 심해짐&lt;br&gt; 세션 데이터를 저장할 때 기본적으로 서버의 메모리에 저장된다. 각 사용자별로 고유한 세션을 생성하고 유지해야 하기 때문에 사용자의 수가 많아질 수록 서버 메모리 사용량도 증가한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;보완&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTTPS를 통해 암호화되어 전송해야 하며, 쿠키에 저장된 정보는 민감한 정보를 직접 포함하지 않아야 합니다. 세션 하이재킹 공격에 대비하기 위해 &lt;strong&gt;쿠키에 플래그를 설정&lt;/strong&gt; 하여 보호 조취를 취하는 것이 좋습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;세션 하이재킹&lt;/strong&gt;: 공격자가 사용자의 세션 토큰이나 세션 쿠키를 가로채 그 사용자로서 서버에 접근하는 보안 공격&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;방어 플래그&lt;/strong&gt;: Secure 플래그, HttpOnly 플래그, SameSite 플래그&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 부하 방지&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;세션 스토리지 외부화&lt;/strong&gt;&lt;br&gt;세션 데이터를 서버 메모리 대신 외부 데이터 스토리지 시스템(Redis 등)에 저장하여, 서버의 메모리 부담을 줄이고, 서버 간에 세션 정보를 공유할 수 있는 기능을 제공&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스테이트리스 설계 고려&lt;/strong&gt;&lt;br&gt;JWT와 같은 토큰 기반 인증 방식으로 서버 측에서 사용자 상태를 저장할 필요가 없어 서버 부하를 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt; ️ JWT 기반 인증&lt;/h3&gt;
&lt;p&gt;JSON Web Token, 인증에 필요한 정보들을 암호화 시킨 토큰을 의미합니다. JWT 토큰을 HTTP 헤더에 실어 서버가 클라이언트를 식별합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JWT의 구조&lt;/strong&gt;&lt;br&gt;&lt;code&gt;{Header} . {Payload} . {Signature}&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Header&lt;/strong&gt; : 토큰의 유형(typ, 보통 JWT)과 해싱 알고리즘(alg, 예: HMAC SHA256 또는 RSA)이 포함된 JSON 객체, 헤더는 Base64Url 방식으로 인코딩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload&lt;/strong&gt; : 토큰에 담을 클레임(claim)이 포함된 JSON 객체, Base64Url 방식으로 인코딩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature&lt;/strong&gt; : 헤더의 인코딩된 값, 페이로드의 인코딩된 값, 비밀 키를 합친 후 헤더에 명시된 알고리즘을 사용하여 생성, 메시지가 중간에 변경되지 않았음을 검증하는 데 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JWT 기반 인증 로직은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;클라이언트 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유ID 등의 정보를 Payload 에 저장&lt;/li&gt;
&lt;li&gt;암호화할 비밀키를 사용해 토큰 발급&lt;/li&gt;
&lt;li&gt;클라이언트는 전달받은 토큰을 저장해두고, 서버에 요청할 때마다 토큰을 요청 헤더 &lt;code&gt;Authorization&lt;/code&gt; 에 포함시켜 함께 전달&lt;/li&gt;
&lt;li&gt;서버는 토큰의 Signature을 비밀키로 복호화한 다음, 위변조 여부 및 유효 기간 등을 확인&lt;/li&gt;
&lt;li&gt;유효한 토큰이라면 요청에 응답&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;JWT 기반 인증의 장단점&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Header와 Payload를 가지고 Signature를 생성하므로 데이터 위변조를 막을 수 있습니다.&lt;/li&gt;
&lt;li&gt;인증 정보에 대한 별도의 저장소가 필요 없습니다.&lt;/li&gt;
&lt;li&gt;확장성이 우수합니다.&lt;br&gt;  상태 비저장&lt;br&gt;  &lt;strong&gt;분산 시스템이나 MSA에서 서비스 간 인증 용이&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT는 토큰의 길이가 길어, 인증 요청이 많아질 수록 네트워크 부하가 심해집니다.&lt;/li&gt;
&lt;li&gt;Payload 자체는 암호화되지 않기 때문에 유저의 중요한 정보는 담을 수 없습니다.&lt;/li&gt;
&lt;li&gt;토큰을 탈취당하면 대처하기 어렵습니다.&lt;br&gt;  유효기간이 만료될 때까지 계속 사용이 가능하기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;보완&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;짧은 만료 기한 설정&lt;/strong&gt;&lt;br&gt; 토큰이 탈취되더라도 빠르게 만료되기 때문에 피해를 최소화할 수 있습니다.&lt;br&gt; → 그러나 사용자가 자주 로그인해야 하는 불편함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;&lt;br&gt; 1) 클라이언트가 로그인 요청을 보내면 서버는 Access Token 및 그보다 긴 만료 시간을 가진 Refresh Token을 발급&lt;br&gt; 2) 클라이언트는 Access Token이 만료되었을 때 Refresh Token을 사용하여 Access Token의 재발급을 요청&lt;br&gt; 3) 서버는 DB에 저장된 Refresh Token과 비교하여 유효한 경우 새로운 Access Token을 발급하고, 만료된 경우 사용자에게 로그인을 요구&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;→ Access Token의 만료 기한을 짧게 설정할 수 있으며, 사용자가 자주 로그인할 필요가 없습니다.&lt;br&gt;→ 검증을 위해 서버는 Refresh Token을 별도의 storage에 저장해야 합니다. 추가적인 I/O 작업이 일어나기 때문에 JWT의 장점을 완벽하게 누릴 수 없습니다. 클라이언트도 탈취 방지를 통해 Refresh Token을 보안이 유지되는 공간에 저장해야 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt; ️ 선택&lt;/h3&gt;
&lt;p&gt;세션보다 확장성이 우수한 JWT 방식을 도입하고, 토큰을 Access Token과 Refresh Token으로 나눠서 보안을 강화하기로 결정했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;⭐ 두 번째 고민: Refresh Token을 서버에서 어떻게 관리해야 하는가?&lt;/h2&gt;
&lt;p&gt;처음에는 MySQL의 유저 테이블에 그대로 저장했으나, Refresh Token이 만료되었는지 주기적으로 확인해야 하는 로직(ex. 스케줄러 사용)이 별도로 필요했습니다. Access Token의 만료 시간이 짧은 만큼 Refresh Token의 db 접근이 많아지기 때문에 MySQL보다 더 빠르고 값에 만료 시간을 줄 수 있는 Redis를 사용하는 것이 좋을 것이라 판단했습니다.&lt;/p&gt;
&lt;p&gt;따라서 Refresh Token의 저장소를 MySQL에서 Redis로 변경했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;⭐ 세 번째 고민: Refresh Token 은 브라우저에서 어떻게 관리해야 하는가?&lt;/h2&gt;
&lt;p&gt;Refresh Token을 탈취 당할 경우 Access Token을 발급하여 사용자인 척 서비스에 접근을 할 수 있기 때문에 접근하지 못하게 쿠키에 다음 플래그를 적용하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;secure(true)&lt;/strong&gt; : HTTPS 환경에서만 사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;httpOnly(true)&lt;/strong&gt; : 자바스크립트로 접근 불가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sameSite(lax)&lt;/strong&gt; : 쿠키가 동일한 사이트의 요청 또는 일부 안전한 크로스 사이트에서 쿠키를 받을 수 있음&lt;br&gt;특히 완전히 다른 도메인으로 등록해두었던 프론트 어드민을 백엔드 어드민의 하위로 변경하여 same site 정책에서 동일한 도메인으로 인식되도록 만들었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;⭐ 네번째 고민: Redis를 사용하여 로그아웃한 유저를 매번 식별한다면 세션 저장소로 Redis를 사용하는 것과 동일하지 않나?&lt;/h2&gt;
&lt;p&gt;기존 로직은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;로그아웃 시 redis 에 access token을 저장(블랙리스트)하고 다른 요청이 들어올 경우 이 redis에 저장된 토큰인지 확인&lt;br&gt; 만약 redis에 저장된 토큰이라면 예외처리&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그런데 서버 부하를 줄이기 위해 JWT를 사용했는데, JWT의 단점인 탈취 시의 취약점을 상쇄하고자 Redis를 사용한다면, 기존 세션과 다를 바가 있는가? 라는 의문이 들었습니다.&lt;/p&gt;
&lt;p&gt;이렇게 블랙리스트를 도입한다면 가볍다는 JWT의 장점도 제대로 활용 못하고 세션을 사용하는 것보다 보안이 떨어지는 것 같다고 생각했습니다.&lt;/p&gt;
&lt;p&gt;따라서 블랙리스트를 삭제하기로 결정했습니다. Access Token은 1시간의 짧은 유효기간을 가지고 있고 이를 재발급하기 위한 Refresh Token은 httpOnly 쿠키로 보호되어 있기 때문에, Access Token을 자주 재발급해도 Refresh Token을 탈취당할 위험도 적으면서 Access Token을 탈취당해도 시간상 한계가 있어 공격 당할 위험이 적다고 판단했기 때문입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;⭐ 느낀 점&lt;/h2&gt;
&lt;p&gt;서버 안전성과 사용자 경험은 Trade-Off 관계를 가집니다. 개발/운영 중인 서비스의 주어진 환경에 맞춰 개발자의 적절한 선택이 필요함을 깨달았습니다. 최근 많은 기업이 MSA 방식으로 서비스를 만들기 때문에 확장성이 좋은 JWT 방식을 도입했지만 정작 저희 서비스는 단일 서버를 사용하기 때문에 JWT의 장점을 온전히 느끼지 못한 것 같습니다. 이후 프로젝트에서는 두 개 이상의 서버를 사용해봐야 겠다는 생각이 들었습니다. &lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;참고&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2021-05-22-cookie-session-jwt/&quot;&gt;인증 방식 : Cookie &amp;amp; Session vs JWT&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://mgyo.tistory.com/832?category=1031443&quot;&gt;Redis를 이용한 토큰 탈취 대응 시나리오(feat. Refresh Token Rotation)&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://hudi.blog/refresh-token/&quot;&gt;Access Token의 문제점과 Refresh Token&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://junior-datalist.tistory.com/352&quot;&gt;Refresh Token Rotation 과 Redis로 토큰 탈취 시나리오 대응&lt;/a&gt;&lt;/p&gt;</description>
      <category>Projects</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/113</guid>
      <comments>https://545aa7.tistory.com/113#entry113comment</comments>
      <pubDate>Thu, 16 May 2024 09:49:07 +0900</pubDate>
    </item>
    <item>
      <title>[JazzMeet] 도메인 간 쿠키 공유 되지 않는 문제 해결</title>
      <link>https://545aa7.tistory.com/112</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;❓문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token을 http only 쿠키로 사용하려고 다음과 같이 서버 코드를 작성해서 배포했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
 * 관리자 로그인 API
 */
@PostMapping(&quot;/api/admins/login&quot;)
public ResponseEntity&amp;lt;LoginAdminResponse&amp;gt; login(@RequestBody @Valid LoginAdminRequest loginAdminRequest) {
    Jwt jwt = adminService.login(loginAdminRequest);

    return ResponseEntity.ok()
        .header(HttpHeaders.SET_COOKIE, getRefreshToken(jwt).toString())
        .body(AdminMapper.INSTANCE.toLoginAdminResponse(jwt));
}

private ResponseCookie getRefreshToken(Jwt jwt) {
    return ResponseCookie.from(&quot;refreshToken&quot;, jwt.getRefreshToken())
        .maxAge(jwtProperties.getRefreshTokenExpiration())
        .path(&quot;/&quot;)
        .secure(true)
        .httpOnly(true)
        .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 어드민 'jazzmeet-admin.site'에서 서버 'jazzmeet.site'로 로그인 API 요청을 보낼 때 HTTP Response 에는 쿠키가 포함돼서 나가지만 브라우저의 쿠키에는 저장되지 않는 문제가 발생했습니다.&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프론트의 요청 코드에 설정 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트의 로그인 요청 코드에 &lt;code&gt;credentials:include&lt;/code&gt; 옵션을 추가했습니다. 이때 CORS 문제가 발생해서 백엔드에서도 cors 설정에 프론트 어드민을 allowedOrigins로 설정하고 &lt;code&gt;allowCredentials(true)&lt;/code&gt; 옵션을 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Override
public void addCorsMappings(CorsRegistry registry) {
  registry.addMapping(&quot;/api/**&quot;)
      .allowedOrigins(&quot;https://www.jazzmeet-admin.site&quot;, &quot;https://jazzmeet.site&quot;)
      .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;)
      .allowCredentials(true)
      .allowedHeaders(&quot;*&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;addMapping : /api로 시작하는 모든 URL에 대해 CORS 정책이 적용됩니다.&lt;/li&gt;
&lt;li&gt;allowedOrigins : 지정된 출처(origin)의 요청만 서버가 수락하도록 허용합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에 구체적인 출처를 명시하면 스프링은 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 헤더를 자동으로 만들어줍니다. 브라우저의 보안 정책에 따라, &lt;code&gt;allowCredentials(true)&lt;/code&gt; 설정을 사용하면 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 헤더에 구체적인 출처(와일드카드 &lt;code&gt;*&lt;/code&gt; X)를 요구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;allowedMethods : 허용할 HTTP 메서드를 지정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OPTIONS: 서버가 지원하는 메서드를 조회할 때 사용되며, CORS preflight 요청에 대응하기 위해 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;allowCredentials : 크로스 오리진 요청 시 쿠키와 같은 인증 정보를 포함할 수 있도록 허용합니다.&lt;/li&gt;
&lt;li&gt;allowedHeaders : 설정한 HTTP 헤더를 요청에서 허용하겠다는 것을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 문제는 해결됐지만 여전히 쿠키가 저장되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿠키 생성 시 옵션 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키 생성 시 &lt;code&gt;sameSite(&quot;None&quot;)&lt;/code&gt;, &lt;code&gt;secure(true)&lt;/code&gt; 옵션을 추가해주었습니다.&lt;br /&gt;크롬의 20년도 업데이트에서 same site 속성의 기본 값이 &lt;code&gt;None&lt;/code&gt;에서 &lt;code&gt;Lax&lt;/code&gt;로 변경되었을 알 수 있었습니다. 이 속성은 웹 사이트 간 요청 위조(CSRF, Cross-Site Request Forgery) 공격을 방지하는 데 도움을 줍니다. 이는 다른 도메인 간 요청에서 사용자의 데이터 보호를 강화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;same site 속성에는 세 가지 설정이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Strict&lt;/b&gt;: 쿠키는 오직 같은 사이트에서 발생한 요청에만 전송됩니다. 이는 가장 제한적인 옵션입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lax&lt;/b&gt;: 더 유연하며, 사용자가 다른 사이트에서 링크를 클릭해 사이트에 접근했을 때 쿠키를 전송할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;None&lt;/b&gt;: 모든 크로스 사이트 요청에 쿠키를 전송하도록 합니다. 이 옵션을 사용하려면 반드시 쿠키를 &lt;b&gt;&lt;code&gt;Secure&lt;/code&gt;&lt;/b&gt;로도 설정해야 합니다, 즉 HTTPS를 통해서만 쿠키가 전송됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 어드민 &lt;code&gt;jazzmeet-admin.site&lt;/code&gt; 와 서버 어드민 &lt;code&gt;jazzmeet.site&lt;/code&gt;는 아무런 연관이 없는 cross domain 이기 때문에 크로스 사이트 요청에도 쿠키를 요청할 수 있는 sameSite(&quot;None&quot;) 과 secure(true) 옵션을 추가해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 여전히 브라우저의 쿠키에 저장이 되지 않고 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기서부터 조금 멘탈이 흔들리기 시작했습니다. 무슨 문제인지 알 수가 없어 몇 번 시도를 했지만 결국 문제는 2번과 5번의 시도로 해결할 수 있었습니다. 3, 4번은 이 문제 해결에 직접적인 영향을 주지 않았지만 일단 기록으로 남겼습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 쿠키 생성 시 프론트 도메인을 명시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 옵션이 들어가는 도메인이 프론트 도메인과 달라 쿠키가 저장이 안 되는가 싶어서 쿠키 생성 시 &lt;code&gt;domain(&quot;jazzmeet-admin.site&quot;)&lt;/code&gt; 옵션을 추가해주었습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private ResponseCookie getRefreshToken(Jwt jwt) {
    return ResponseCookie.from(&quot;refreshToken&quot;, jwt.getRefreshToken())
        .maxAge(jwtProperties.getRefreshTokenExpiration())
        .path(&quot;/&quot;)
        .secure(true)
        .httpOnly(true)
        .domain(&quot;jazzmeet-admin.site&quot;)
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 여전히 프론트 어드민 브라우저에는 쿠키가 저장되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 프론트 어드민을 서버의 서브 도메인으로 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 도메인과 프론트 어드민 도메인이 완전히 다른 cross domain이라 쿠키가 저장되는 않는 것일 수도 있겠다고 생각하여 프론트 어드민 도메인을 서버의 서브 도메인 &lt;code&gt;admin.jazzmeet.site&lt;/code&gt;로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 우리의 프론트 어드민은 Amplify에 배포되어 있었기 때문에 Amplify의 도메인 관리에서 &lt;code&gt;jazzmeet.site&lt;/code&gt;를 루트 도메인으로 설정하고, 서브 도메인으로 &lt;code&gt;admin.jazzmeet.site&lt;/code&gt;를 추가한 후 www로 redirect 되는 옵션을 껐습니다. Amlify에서 발급된 키를 가비아의 jazzmeet.site DNS 관리에서 추가해주었고, admin.jazzmeet.site에 배포된 프론트 어드민 주소를 저장해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더해서 &lt;code&gt;jazzmeet.site&lt;/code&gt;를 포함한 서브 도메인들에서도 쿠키를 공유할 수 있도록 하기 위해 &lt;code&gt;domain(&quot;.jazzmeet.site&quot;)&lt;/code&gt; 으로 옵션을 수정해주었습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private ResponseCookie getRefreshToken(Jwt jwt) {
  return ResponseCookie.from(&quot;refreshToken&quot;, jwt.getRefreshToken())
    .maxAge(jwtProperties.getRefreshTokenExpiration())
    .path(&quot;/&quot;)
    .secure(true)
    .httpOnly(true)
    .domain(&quot;.jazzmeet.site&quot;)
    .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 쿠키는 브라우저에 저장되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 프론트의 로그인 요청에 옵션 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cross domain 상태라도 sameSite(&quot;None&quot;)과 secure(true) 옵션을 설정해주었다면 프론트 어드민을 서버의 서브 도메인으로 변경하지 않아도 정상적으로 쿠키가 공유되어야 했습니다. 그럼에도 계속해서 쿠키 저장이 되지 않아 프론트 코드를 살펴보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, 토큰 재발급 로직에만 &lt;code&gt;credentials:'include'&lt;/code&gt; 옵션이 설정되어 있었고 로그인 요청 로직에는 설정되지 않았습니다. 이 옵션이 프론트에서 서버로 쿠키를 보낼 때에만 사용되는 줄 알았는데 서버에서 보낸 쿠키를 브라우저에 저장하기 위해서도 필요한 옵션이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프론트 로그인 요청에 &lt;code&gt;credentials:'include'&lt;/code&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;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;cross domain 이라면 &lt;code&gt;sameSite(&quot;None&quot;)&lt;/code&gt;과 &lt;code&gt;secure(true)&lt;/code&gt; 옵션을 주고 프론트에서 쿠키를 보내거나 받는 요청에 'credentials:'include'`옵션을 설정하면 쿠키 공유가 가능합니다.&lt;/li&gt;
&lt;li&gt;같은 도메인이라면 same site는 기본 설정(Lax)으로 두고 프론트 코드에 'credentials:'indluce'` 옵션을 추가하면 쿠키 공유가 가능합니다.&lt;/li&gt;
&lt;/ol&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure?hl=ko&quot;&gt;새로운 SameSite=None; Secure 쿠키 설정에 대비&lt;/a&gt;&lt;/p&gt;</description>
      <category>Projects</category>
      <category>Cross Domain</category>
      <category>http only cookie</category>
      <category>same site</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/112</guid>
      <comments>https://545aa7.tistory.com/112#entry112comment</comments>
      <pubDate>Sun, 12 May 2024 19:59:19 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 22252. 정보 상인 호석</title>
      <link>https://545aa7.tistory.com/108</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암흑가의 권력은 주먹과 정보에서 나온다. 주먹은 한 명에게 강하고, 정보는 세계를 가지고 놀 수 있기 때문에 호석이는 세상 모든 정보를 모으는 &quot;정보 상인&quot;이 되고 싶다. 정보 상인은 정보를 사고파는 사람을 의미한다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호석이는 아직 상인계의 새싹이기 때문에, 초기 투자를 통해서 여러 명의 &quot;정보 고릴라&quot;들로부터 정보를 모으려고 한다. 정보 고릴라란 여기저기서 정보를 수집하는 사람들을 의미한다. 일단 정보를 긁어모으기 위해서 호석이는 여러 정보 고릴라들에게 정보를 구매하려고 한다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암흑가의 연락망은 빼곡하기 때문에 누가 어떤 정보를 얻었는지에 대한 찌라시들이 수시로 퍼진다. 찌라시로 알 수 있는 것은, 어떤 이름을 가진 고릴라가 C1, C2, ..., Ck 만큼의 가치가 있는 정보 k 개를 얻었다는 점이다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호석이는 이를 바탕으로 임의의 시점에 특정 고릴라에게 정보를 몇 개 살 것인지를 정할 수 있다. 이때 가치 순으로 가장 비싼 정보들을 구매한다. 예를 들어 고릴라가 가진 정보가 10개이고, 호석이가 사고 싶은 정보 개수가 4개라면, 고릴라는 10개 중에서 가치 순으로 가장 비싼 4개를 팔 것이다. 한 번 거래한 정보는 호석이에게 더 이상 가치가 없기 때문에 고릴라도 그 정보를 파기한다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 암흑가의 주먹이며 양대 산맥이 될 가능성이 있는 호석이를 주시하고 있다. 관찰하면서 얻은 정보는 총 Q 개이다. 각 정보는 다음의 2가지 중 하나이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1 Name k C1, C2, ..., Ck : 이름이 [Name]인 고릴라가 k 개의 정보를 얻었으며, 각 가치는 C1 부터 Ck 이다.&lt;/li&gt;
&lt;li&gt;2 Name b : 호석이가 이름이 [Name]인 고릴라에게 b 개의 정보를 구매한다. 이때 고릴라가 가진 정보들 중 가장 비싼 b 개를 구매하며, 고릴라가 가진 정보가 b개 이하이면 가진 모든 정보를 구매한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;견제를 위해서 호석이가 가진 정보들의 가치 총합, 즉 호석이가 정보들을 구매하는 데에 쓴 돈의 총합을 구하자.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입력&lt;/b&gt;&lt;br /&gt;고릴라들이 정보를 얻는 사건과 호석이가 거래하는 정보가 시간순으로 주어진다. 첫 번째 줄에는 쿼리의 개수 Q 가 주어진다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 Q 개의 줄에 걸쳐서 각 줄에 쿼리가 주어진다. 쿼리는 1이나 2로 시작한다. 1로 시작하는 경우에는 정보를 얻은 정보 고릴라의 이름과 k 가 주어지며 이어서 k 개의 정보 가치 C1, ..., Ck가 자연수로 주어진다. 모든 Ci는 1 이상 100,000 이하이다. 2로 시작하는 경우에는 호석이가 거래하려는 정보 고릴라의 이름과 구매하려는 정보의 개수 b가 주어진다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출력&lt;/b&gt;&lt;br /&gt;모든 쿼리가 종료되었을 때에 호석이가 얻게 되는 정보 가치의 총합을 출력하라.&lt;/p&gt;
&lt;br /&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1 &amp;le; Q &amp;le; 100,000, Q 는 자연수&lt;/li&gt;
&lt;li&gt;모든 Name은 알파벳 소문자 혹은 대문자로 이루어져 있고 공백이 없으며 길이는 1 이상 15 이하이다.&lt;/li&gt;
&lt;li&gt;1 &amp;le; k &amp;le; 100,000, k 는 자연수&lt;/li&gt;
&lt;li&gt;1 &amp;le; C &amp;le; 100,000, C 는 자연수&lt;/li&gt;
&lt;li&gt;1 &amp;le; b &amp;le; 100,000, b 는 자연수&lt;/li&gt;
&lt;li&gt;모든 쿼리에 대한 k 의 합은 1,000,000 을 넘지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호석이가 정보를 구매하면 총합에 들어가고 고릴라의 정보가 사라져야 하므로 queue.poll을 써야 한다.&lt;/li&gt;
&lt;li&gt;고릴라가 얻은 정보를 순서대로 저장하기 위해서는 PriorityQueue를 사용하여 정보를 가치의 내림차순으로 저장하면 된다.&lt;/li&gt;
&lt;li&gt;1을 입력 받으면 고릴라가 정보를 얻는 것이므로 map에 고릴라의 이름과 정보들을 담는다.&lt;/li&gt;
&lt;li&gt;이미 있는 고릴라라면 queue에 add만 한다.&lt;/li&gt;
&lt;li&gt;2를 입력 받으면 호석이가 정보를 사는 것이므로 해당 고릴라의 이름으로 정보를 찾아 count 수 만큼 정보를 poll로 빼내 총합에 누적한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고릴라의 이름이 map에 없으면 아무것도 하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;총합을 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 정보 상인 호석
public class Week09_22252 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        long q = Integer.parseInt(br.readLine());
        long sum = 0;
        HashMap&amp;lt;String, PriorityQueue&amp;lt;Integer&amp;gt;&amp;gt; monkey = new HashMap&amp;lt;&amp;gt;();

        for (int i = 0; i &amp;lt; q; i++) {
            st = new StringTokenizer(br.readLine());
            int type = Integer.parseInt(st.nextToken());
            String name = st.nextToken();
            int count = Integer.parseInt(st.nextToken());

            if (type == 1) {
                for (int j = 0; j &amp;lt; count; j++) {
                    if (!monkey.containsKey(name)) {
                        PriorityQueue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;(Collections.reverseOrder());
                        pq.add(Integer.valueOf(st.nextToken()));
                        monkey.put(name, pq);
                    } else {
                        monkey.get(name).add(Integer.parseInt(st.nextToken()));
                    }
                }
            } else {
                if (monkey.get(name) == null) {
                    continue;
                }
                while(!monkey.get(name).isEmpty() &amp;amp;&amp;amp; count &amp;gt; 0) {
                    sum += monkey.get(name).poll();
                    count--;
                }

            }
        }
        System.out.println(sum);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>algorithm</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/108</guid>
      <comments>https://545aa7.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 11 Dec 2022 18:38:31 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 13915. 현수의 열기구 교실</title>
      <link>https://545aa7.tistory.com/107</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현수는 열기구 여름특강의 강사다. 현수는 매우 성실해서 모든 수강생들의 열기구 비행을 기록하고있다. 매 비행 이후, 현수는 그 비행에 참석한 수강생들의 기록을 리스트에 추가한다. 리스트에는 각 수강생마다 띄웠던 기구의 인식번호만이 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 시즌이 끝난 후, 현수는 얼마나 많은 종류의 열기구들을 비행해봤는지에 따른 수강생들의 숙련도를 분류해 나열하려고한다. 만약 두 수강생이 비행했던 열기구의 종류들이 같다면 두 수강생은 같은 숙련도를 가진것으로 분류된다. (이 경우, 비행을 한 횟수는 관계가 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현수는 총 9 종류의 열기구를 관리하며, 수강생들의 기록은 각 열기구의 번호들로써 이루어진다. 모든 수강생들 중 비행 횟수가 9번을 넘는 수강생은 없다. (1번 열기구를 세번 2번과 3번을 각각 한번씩 운용했던 수강생의 번호는 11123이 되겠다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 수강생 234423과 수강생 342는 같은 숙련도를 가진 것으로 분류된다. 하지만 수강생 118821과 수강생 1189821 같은 경우는 9번 열기구 비행 유무의 차이로 다른 숙련도를 가진 것으로 분류된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현수의 리스트에 있는 수강생들이 총 몇개의 숙련도로 분류되는지 구하라.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입력&lt;/b&gt;&lt;br /&gt;각 테스트케이스마다 첫 줄에는 총 수강생의 수인 정수 N(1 &amp;le; N &amp;le; 1 000) 이 주어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어지는 N줄은 열기구 비행 기록을 나타내는 각 수강생들의 번호들이 주어진다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출력&lt;/b&gt;&lt;br /&gt;매 테스트케이스마다 각 줄에 현수의 리스트에 있는 수강생들의 숙련도가 몇 개로 분류되어지는지 출력하라.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력이 있는 동안 while문을 돌려 테스트케이스를 입력받는다.&lt;/li&gt;
&lt;li&gt;HashSet을 사용하여 숙련도에서 중복을 없앤다.&lt;/li&gt;
&lt;li&gt;중복을 없앤 set을 String으로 변환해 다른 HashSet에 넣어 중복을 없앤다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;set을 그냥 HashSet에 넣으면 다른 객체라고 판단하여 중복이 없어지지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 수강생의 정보가 담겨있는 set의 size를 구해 StringBuilder에 추가한다.&lt;/li&gt;
&lt;li&gt;while문이 종료되면 sb를 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 현수의 열기구 교실
public class Week09_13915 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String testcaseStr = &quot;&quot;;
        StringBuilder sb;
        StringBuilder result = new StringBuilder();

        while ((testcaseStr = br.readLine()) != null) { // 이거 없으면 50%에서 틀렸습니다 나온다
            HashSet&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();
            int testcase = Integer.parseInt(testcaseStr);
            for (int i = 0; i &amp;lt; testcase; i++) {
                Set&amp;lt;Integer&amp;gt; number = new HashSet&amp;lt;&amp;gt;();
                sb = new StringBuilder();
                String[] temp = br.readLine().split(&quot;&quot;);
                for (String s : temp) {
                    number.add(Integer.valueOf(s));
                }
                for (int n : number) {
                    sb.append(String.valueOf(n));
                }
                set.add(String.valueOf(sb));
            }
            result.append(set.size() + &quot;\n&quot;);
        }
        System.out.println(result.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>algorithm</category>
      <author>siio</author>
      <guid isPermaLink="true">https://545aa7.tistory.com/107</guid>
      <comments>https://545aa7.tistory.com/107#entry107comment</comments>
      <pubDate>Sun, 11 Dec 2022 18:24:54 +0900</pubDate>
    </item>
  </channel>
</rss>