<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>너와 나의 스토리</title>
    <link>https://hororolol.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 5 Apr 2026 15:49:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>노는게제일좋아!</managingEditor>
    <item>
      <title>Spring JPA 트랜잭션 관리 - 낙관적 락, 비관적 락</title>
      <link>https://hororolol.tistory.com/768</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring JPA Transaction 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 보통 @Transactional 어노테이션으로 트랜잭션을 선언적으로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Srping은 AOP 방식으로 메서드를 감싸서, 진입 시 begin, 정상 종료 시 commit, 예외 발생 시 rollback을 수행한다.&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;gt; 점유 상태 변경 -&amp;gt; 디바이스 저장&lt;/p&gt;
&lt;pre id=&quot;code_1759029311505&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class DeviceService {

    private final DeviceRepository deviceRepository;

    @Transactional
    public void occupyDevice(Long deviceId, Long userId) {
        Device device = deviceRepository.findById(deviceId)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 디바이스&quot;));

        if (device.isOccupied()) {
            throw new IllegalStateException(&quot;이미 점유 중&quot;);
        }

        device.occupy(userId); // 상태 변경
        deviceRepository.save(device); // flush 시점에 update
    }
}&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JPA에서 제공하는 동시성 제어 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) Optimistic Lock (낙관적 락)&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;엔티티에 버전(@Version)을 두고 업데이트 시점에 버전 비교로 충돌을 감지한다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티의 상태가 변경되어 update가 일어나면 version이 자동으로 증가함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;충돌 발생 시 예외를 던지고 재시도하거나 사용자에게 실패를 알린다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1759030322208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Device {
    @Id
    private Long id;

    private Boolean occupied;
    private Long occupiedBy;

    @Version
    private Long version; // JPA/Hibernate가 자동으로 관리

    public void occupy(Long userId) {
        if (Boolean.TRUE.equals(this.occupied)) {
            throw new IllegalStateException(&quot;이미 점유중&quot;);
        }
        this.occupied = true;
        this.occupiedBy = userId;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1759030427814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class DeviceService {
    private final DeviceRepository repo;

    public void occupyWithRetry(Long deviceId, Long userId) {
        final int maxAttempts = 3;
        for (int attempt = 1; attempt &amp;lt;= maxAttempts; attempt++) {
            try {
                tryOccupy(deviceId, userId); // 트랜잭션 단위 시도
                return;
            } catch (ObjectOptimisticLockingFailureException | OptimisticLockException e) {
                if (attempt == maxAttempts) throw new ConcurrentModificationException(&quot;동시성 충돌 &amp;mdash; 재시도 초과&quot;);
                // 짧은 backoff
                try { Thread.sleep(100L * attempt); } catch (InterruptedException ignored) {}
            }
        }
    }

    @Transactional
    protected void tryOccupy(Long deviceId, Long userId) {
        Device d = repo.findById(deviceId).orElseThrow();
        if (Boolean.TRUE.equals(d.getOccupied())) throw new IllegalStateException(&quot;이미 점유중&quot;);
        d.occupy(userId);
        repo.saveAndFlush(d);
    }
}&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3283&quot; data-start=&quot;3251&quot;&gt;T1과 T2가 동시에 device(버전=1)를 읽음.&lt;/li&gt;
&lt;li data-end=&quot;3321&quot; data-start=&quot;3284&quot;&gt;T1이 점유 후 commit &amp;rarr; DB 업데이트, 버전 = 2.&lt;/li&gt;
&lt;li data-end=&quot;3430&quot; data-start=&quot;3322&quot;&gt;T2가 점유하려고 저장 시도 &amp;rarr; 업데이트문이 WHERE id=? AND version=1로 동작 &amp;rarr; 영향된 row가 0이면 JPA가 OptimisticLockException 발생.&lt;/li&gt;
&lt;li data-end=&quot;3471&quot; data-start=&quot;3431&quot;&gt;T2는 예외를 받아 재시도 로직을 실행하거나 사용자에 실패를 반환.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;단점: 충돌이 잦으면 재시도로 효율 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;2) Pessimistic Lock (비관적 락)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌을 미리 막는다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;DB 수준에서 해당 row를 SELECT ... FOR UPDATE처럼 잠궈서 다른 트랜잭션의 접근을 미리 차단(블락킹)&lt;/li&gt;
&lt;li&gt;충돌 가능성이 높은 경우 사용함.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1759030657591&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface DeviceRepository extends JpaRepository&amp;lt;Device, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT d FROM Device d WHERE d.id = :id&quot;)
    @QueryHints({@QueryHint(name = &quot;javax.persistence.lock.timeout&quot;, value = &quot;3000&quot;)})
    Device findByIdForUpdate(@Param(&quot;id&quot;) Long id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1759030665397&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void occupyPessimistic(Long deviceId, Long userId) {
    try {
        Device d = repo.findByIdForUpdate(deviceId); // DB에서 FOR UPDATE (잠금)
        if (Boolean.TRUE.equals(d.getOccupied())) throw new IllegalStateException(&quot;이미 점유중&quot;);
        d.occupy(userId);
        repo.save(d);
    } catch (LockTimeoutException | PessimisticLockException e) {
        // 잠금 획득 실패 -&amp;gt; 재시도 혹은 사용자에게 알림
        throw new IllegalStateException(&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 data-end=&quot;3538&quot; data-start=&quot;3496&quot;&gt;시나리오
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3657&quot; data-start=&quot;3496&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3538&quot; data-start=&quot;3496&quot;&gt;T1이 SELECT ... FOR UPDATE로 row 잠금 획득.&lt;/li&gt;
&lt;li data-end=&quot;3614&quot; data-start=&quot;3539&quot;&gt;T2가 같은 row를 SELECT ... FOR UPDATE하려 하면 블록되거나(lock timeout 설정 시) 즉시 실패.&lt;/li&gt;
&lt;li data-end=&quot;3657&quot; data-start=&quot;3615&quot;&gt;T1이 commit/rollback을 하면 잠금 해제 &amp;rarr; T2가 진행.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;amp;데드락 위험, 트랜잭션 짧게 유지해야함.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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 Boot</category>
      <category>OptimisticLock</category>
      <category>PessimisticLock</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/768</guid>
      <comments>https://hororolol.tistory.com/768#entry768comment</comments>
      <pubDate>Sun, 28 Sep 2025 12:42:21 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] JPA 양뱡향 매핑은 불필요한 것인가? Fetch 전략과 N+1 문제</title>
      <link>https://hororolol.tistory.com/767</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;i&gt;본 포스팅은 &quot;[NHN Cloud 2019] Spring JPA의 사실과 오해&quot; 강연 내용을 정리한 글입니다.&lt;/i&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;연관관계 매핑 - 단방향 vs 양방향&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사실상 단방향 매핑만으로 연관관계 매핑은 이미 완료&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어차피 연관관계 매핑은 내부적으로 foreign key를 이용하게 되는데, foreign key는 결국 하나이기 때문에&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&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;우선은 단방향 매핑을 사용하고 반대 방향으로의 객체 그래프 탐색이 필요할 때 양방향을 사용&lt;/li&gt;
&lt;/ul&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;다대일(N:1) 단방향 연관관계 매핑&lt;/h4&gt;
&lt;pre id=&quot;code_1758933390943&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @Column(name =&quot;member_id&quot;)
    private Long memberId;
    
    ...   
}


@Entity
public class MemberDetail {
    @Id
    @Column(name =&quot;member_detail_id&quot;)
    private Long memberDetailId;
    
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;
    
    ...
}&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Cascade: 영속성 전이&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;Entity의 영속성 상태 변화를 연관된 Entity에도 함께 적용하는 것&lt;/li&gt;
&lt;li&gt;Cascade Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PERSIST
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 영속 객체로 추가할 때 연관된 Entity도 함께 영속 객체로 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;REMOVE
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 삭제할 때 연관된 Entity도 함께 삭제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DETACH
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 영속성 컨텍스트에서 분리할 때 연관된 Entity도 함께 분리 상태로 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;REFRESH
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 데이터베이스에서 다시 읽어올 때 연관된 Entity도 함께 다시 읽어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MERGE
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 준영속 상태에서 다시 영속 상태로 변경할 때 연관된 Entity도&amp;nbsp; 함께 변경한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ALL
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 상태 변화에 대해 연관된 Entity에 함께 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;Case1: 연속성 전이를 통한 insert&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758933909857&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void createMemberWithDetails() {
    Member member = new Member(&quot;member1&quot;, LocalDateTime.now());
    
    MemberDetail memberDetail1 = new MemberDetail(member, &quot;type1&quot;, &quot;description1&quot;);
    MemberDetail memberDetail2 = new MemberDetail(member, &quot;type2&quot;, &quot;description2&quot;);

    membserDetailRepository.saveall(Arrays.asList(memberDetail1, memberDetail2));
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;members 테이블에 member INSERT&lt;/li&gt;
&lt;li&gt;member_details 테이블에 member_details1 INSERT&lt;/li&gt;
&lt;li&gt;member_details 테이블에 member_details2 INSERT&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;일대다(1:N) 단방향 연관관계 매핑&lt;/h4&gt;
&lt;pre id=&quot;code_1758934076789&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @Column(name =&quot;member_id&quot;)
    private Long memberId;
    
    @OneToMany(casecade = CascadeType.ALL)
    @JoinColumn(name = &quot;member_id&quot;)
    private List&amp;lt;MemberDetail&amp;gt; details;
    
    ...   
}


@Entity
public class MemberDetail {
    @Id
    @Column(name =&quot;member_detail_id&quot;)
    private Long memberDetailId;
 
    ...
}&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case2: 연속성 전이를 통한 insert&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758934532372&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Transactional
public void createMemberWithDetails() {
    Member member = new Member(&quot;member1&quot;, LocalDateTime.now());
    Member savedMember = memberRepository.save(member);
    
    MemberDetail memberDetail1 = new MemberDetail(member, &quot;type1&quot;, &quot;description1&quot;);
    MemberDetail memberDetail2 = new MemberDetail(member, &quot;type2&quot;, &quot;description2&quot;);

    member.getDetails().add(memberDetail1);
    member.getDetails().add(memberDetail2);
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;members 테이블에 member INSERT&lt;/li&gt;
&lt;li&gt;member_details 테이블에 member_details1 INSERT&lt;/li&gt;
&lt;li&gt;member_details 테이블에 member_details2 INSERT&lt;/li&gt;
&lt;li&gt;member_details 테이블에서 member_id UPDATE&lt;/li&gt;
&lt;li&gt;member_details 테이블에서 member_id UPDATE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;추가적으로 업데이트 쿼리가 추가됨&lt;/li&gt;
&lt;li&gt;즉, 일대다(1:N) 단방향 연관관계 매핑에서 연속성 전이를 통해 insert를 하게 되면&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일대다 관계의 외래 키(FK) 지정을 위해 추가적인 update 쿼리가 발생하는 문제가 생김&lt;/li&gt;
&lt;li&gt;이 경우에는 오히려 &lt;b&gt;일대다 양방향 연관관계&lt;/b&gt;로 변경하면 추가적인 update 쿼리가 없어짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;일대다(1:N) 양방향 매핑&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758935088421&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @Column(name =&quot;member_id&quot;)
    private Long memberId;
    
    @OneToMany(casecade = CascadeType.ALL, mappedBy = &quot;member&quot;)
    private List&amp;lt;MemberDetail&amp;gt; details;
    
    ...   
}


@Entity
public class MemberDetail {
    @EmbeddedId
    private PK pk;
    
    private String description;
    
    @ManyToOne
    @MapsId(&quot;memberId&quot;)  // 복합키의 memberId를 member 연관관계와 매핑 -&amp;gt; pk.memberId와 member.memberId는 항상 동일한 값이 됨.
    private Member member;
    
    @Embeddable
    public static class Pk implements Serializable {
    	@Column(name = &quot;member_id&quot;)
        private Long memberId;
        
        private String type;
    }
    ...
}&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;양방향으로 설정하는 경우, 연관관계의 주인을 설정하는 게 중요하다. 내부적으로는 FK를 사용하기 때문이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;결과적으로 FK를 가지고 있는쪽이 연관관계의 주인이 된다.&lt;/li&gt;
&lt;li&gt;예제에서 보면 MemberDetail에서 MapsId
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Member는 연관관계의 주인이 아니기 때문에 join column을 쓰면 안 된다.&lt;/li&gt;
&lt;li&gt;@JoinColumn: 이 필드가 FK임을 명시.&lt;/li&gt;
&lt;li&gt;mappedBy: &quot;나는 주인이 아니다&quot;라는 선언&amp;nbsp;&lt;/li&gt;
&lt;li&gt;@MapsId: 자식 엔티티의 PK와 FK를 공유할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, member_detail의 PK는 (member_id, type) 복합키이고, 그 중 member_id는 member 테이블의 FK 역할을 동시에 한다.&lt;/li&gt;
&lt;li&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;b&gt;그렇다면 항상 ManyToOne 단방향만 쓰는 게 정답일까?&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;추천 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;대부분 MemberDetail 중심으로 접근하고, Member에서 details를 잘 안 쓰는 경우&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;ManyToOne 단방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Member에서 details를 자주 조회해야하는 경우&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;양방향 (mappedBy 설정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;OneToMany 단방향&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;비추 (쿼리 비효율적)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FK는 항상 &quot;다&quot;쪽(@ManyToOne)에서 관리하는 게 정석이고, &quot;일&quot;쪽(@OneToMany)는 필요할 때만 컬렉션으로 접근할 수 있게 양방향을 열어주는 게 가장 실용적이다.&amp;nbsp;&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;b&gt;Fetch 전략&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전략
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;FetchType.EAGER
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 entity를 가져올 때, 연관관계에 있는 entity를 즉시 가져오는 것&lt;/li&gt;
&lt;li&gt;@OneToOne, @ManyToOne&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FetchType.LAZY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 참조가 이뤄졌을 때 연관관계에 있는 entity 값을 가져오는 것&lt;/li&gt;
&lt;li&gt;@OneToMany, @ManyToMany&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;N+1 문제&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연관된 entity를 가지고 올 때, 우리가 의도한 것과 달리 추가적으로 쿼리를 N번 추가적으로 수행하는 문제&lt;/li&gt;
&lt;li&gt;해결 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fetch Join&lt;/li&gt;
&lt;li&gt;Entity Graph&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;오해1: N+1 문제는 EAGER Fetch 전략 때문에 발생하는가?&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;Fetch 전략을 LAZY로 설정했더라도 연관 Entity를 참조하면 그 순간 추가적인 쿼리가 수행됨.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Fetch 전략은 시점 차이지, 똑같이 문제 발생.&lt;/li&gt;
&lt;/ul&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;b&gt;오해2: findAll() 메서드 N+1 문제가 발생하지 않는가?&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;fetch 전략을 적용해서 연관 entity를 가져오는 것은 오직 단일 레코드에서만 적용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;단일 레코드 조회가 아닌 경우 해당 JPQL을 먼저 수행하고 반환된 레코드 하나 하나에 대해 entity에 설정된 fetch 전략을 적용해서 연관 entity 가져옴.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, findAll()로 Member 목록을 가져온 직후, JPA는 단순히 Member 객체들만 영속성 컨텍스트에 올린다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이후, member.getDetails()를 호출하면 그때, fetch 전략을 수행함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉, 연관 entity를 조회하는 시점에서 결국 N+1이 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그렇기 때문에 findAll() 메서드 호출도 역시 이 과정에서 N+1 문제 발생 가능&lt;/li&gt;
&lt;/ul&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;b&gt;N+1 해결을 위해 Fetch Join 사용&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: Pagination + Fetch JOIN
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pagination 쿼리에 Fetch Join을 적용하면 실제로는 모든 레코드르 가져와서 조인한 후에 Pagination처리가 됨.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉, 분리해서 실행해야한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Spring Boot</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/767</guid>
      <comments>https://hororolol.tistory.com/767#entry767comment</comments>
      <pubDate>Sat, 27 Sep 2025 09:48:13 +0900</pubDate>
    </item>
    <item>
      <title>멀티모달 RAG 시스템 - 임베딩/인코딩/fusion</title>
      <link>https://hororolol.tistory.com/765</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;출처: &lt;a href=&quot;https://fastcampus.co.kr/data_red_ragmaster&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fastcampus.co.kr/data_red_ragmaster&lt;/a&gt;&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;멀티 모달 RAG 시스템&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티모달 RAG: 여러 형태의 데이터를 검색해 생성 AI에 연결하여 답변 개선&lt;/li&gt;
&lt;li&gt;예: ChatGPT에 이미지를 보여주거나 음성으로 질문하는 다중모달 인터페이스 등장&lt;/li&gt;
&lt;/ul&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;Embedding&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;텍스트 임베딩 vs 이미지 임베딩 vs 멀티모달 임베딩&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.6512%;&quot;&gt;텍스트 임베딩&lt;/td&gt;
&lt;td style=&quot;width: 85.3488%;&quot;&gt;단어/문장의 언어적 의미를 벡터로 표현 (자연어 의미 공간)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.6512%;&quot;&gt;이미지 임베딩&lt;/td&gt;
&lt;td style=&quot;width: 85.3488%;&quot;&gt;픽셀 데이터의 시각적 패턴을 벡터로 표현(훈련 데이터에 따라 의미적 특징 일부 반영)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.6512%;&quot;&gt;멀티모달 임베딩&lt;/td&gt;
&lt;td style=&quot;width: 85.3488%;&quot;&gt;서로 다른 모달의 데이터(예: 이미지, 텍스트 등)를 공통 공간에 매핑하여 교차검색 가능 (ex: CLIP 모델)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 가지 모달리티를 연결한 bimodal 모델(CLIP, ALIGN 등)이 등장하면서, 서로 다른 형태의 데이터라도 같은 벡터 공간에서 유사도 계산이 가능해짐&lt;/li&gt;
&lt;li&gt;멀티모달 임베딩 예: &quot;캠핑카&quot;라고 입력하면 캠핑카 관련 책(텍스트)도 찾아오고, 관련 이미지도 찾아옴.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트 데이터와 이미지 데이터를 같은 공간에 임베딩했기 때문 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;더 나아가 Meta의 ImageBind처럼 여러 모달(이미지, 오디오, 텍스트, 센서데이터 등)까지 한꺼번에 임베딩하는 다중모달 모델도 연구중&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;b&gt;멀티 모달 모델 정리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CLIP(Contrastive Language-Image Pre-training)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI에서 개발한 몰티모달 모델로, 텍스트와 이미지를 공통 임베딩 공간에서 연결하는 방식으로 학습된 모델이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Contrastive Learning(대조 학습)을 사용하여 이미지-텍스트 쌍을 가까이 또는, 잘못된 쌍을 멀리 두는 형식으로 학습&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Align
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google에서 개발&lt;/li&gt;
&lt;li&gt;CLIP과 유사한 방식으로 이미지-텍스트 정렬&lt;/li&gt;
&lt;li&gt;데이터셋 규모가 훨씬 큼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ImageBind
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Meta에서 개발&lt;/li&gt;
&lt;li&gt;6개 모달리티(텍스트, 이미지, 오디오, 비디오, IMU 센서, 깊이 데이터) 하나의 벡터 공간으로 매핑.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BLIP / BLIP-2
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티모달 rag에서 자주 쓰이는 모델 (텍스트-이미지 질의응답)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;멀티모달 검색 시스템에서는 각 데이터 유형에 맞는 전처리 및 임베딩 기법이 필요
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: OCR, 음성, 영상 데이터 임베딩&amp;nbsp;&lt;/li&gt;
&lt;li&gt;텍스트 기반 이미지의 경우, OCR(광학 문자 인식)로 텍스트를 추출하여 텍스트 임베딩으로 변환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지를 텍스트로 정규화하면 더 효율적&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;음성 오디오의 경우, ASR(자동 음성 인식)로 텍스트로 변환하거나, 음향 자체의 특징 임베딩 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가사가 별로 없는 노래같은 경우는 음성 인식이 의미 없기 때문에, 이런 경우 음향 자체의 특징을 이용해 임베딩해아함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동영상의 경우, 프레임별 이미지 임베딩 + 자막/음성 텍스트 임베딩 등 복합 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;스크린샷 2025-08-03 오후 10.48.19.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xtwz5/btsPDMlb0Fe/0zWa5kKmniwACSw117KOMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xtwz5/btsPDMlb0Fe/0zWa5kKmniwACSw117KOMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xtwz5/btsPDMlb0Fe/0zWa5kKmniwACSw117KOMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxtwz5%2FbtsPDMlb0Fe%2F0zWa5kKmniwACSw117KOMk%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;601&quot; height=&quot;327&quot; data-filename=&quot;스크린샷 2025-08-03 오후 10.48.19.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;804&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;연결 (Linking)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임베딩과 원본 데이터(객체 저장소 등) 및 관련 데이터 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필터링 (Filtering)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메타데이터 기준 사전/사후 필터링으로 검색 정확도 및 효율성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;저장 옵션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터 DB 내, 또는 별도 메타데이터 저장소(RDB, Document DB, Graph DB)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예: 엄마한테 전화로 혼난 적이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메타 데이터로, 전화 정보(통화 시간, 연락처 등)와 원본 음성 녹음, 요약 내용(엄마한테 혼남)을 링킹해서 저장&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-03 오후 10.54.06.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IgKMs/btsPHgxUtlI/0zvrOR4UzoTrxqseHVjyO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IgKMs/btsPHgxUtlI/0zvrOR4UzoTrxqseHVjyO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IgKMs/btsPHgxUtlI/0zvrOR4UzoTrxqseHVjyO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIgKMs%2FbtsPHgxUtlI%2F0zvrOR4UzoTrxqseHVjyO0%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;772&quot; height=&quot;288&quot; data-filename=&quot;스크린샷 2025-08-03 오후 10.54.06.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&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에 모든 걸 저장하는 건 비효율 적임. 그래서 메타데이터는 RDB 등에 저장해서 먼저 필터링한 후, 벡터 db에서 조회하는 게 효율적.&lt;/li&gt;
&lt;li&gt;벡터 db 등을 활용하여 임베딩 벡터와 원본 데이터 식별자를 함께 저장&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-03 오후 11.04.32.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6hwPD/btsPEo5r5VU/u0YSIeXKObidcKpiKUXegk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6hwPD/btsPEo5r5VU/u0YSIeXKObidcKpiKUXegk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6hwPD/btsPEo5r5VU/u0YSIeXKObidcKpiKUXegk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6hwPD%2FbtsPEo5r5VU%2Fu0YSIeXKObidcKpiKUXegk%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;1458&quot; height=&quot;668&quot; data-filename=&quot;스크린샷 2025-08-03 오후 11.04.32.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;듀어 인코더 (Dual-Encoder / Bi-Encoder)&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;/li&gt;
&lt;li&gt;쿼리와 문서의 임베딩을 사전에 계산 가능하다 (특히 문서 쪽)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 소개팅 앱에서 일단 나이, 사는 곳, 종교 등 기본적인 정보로 상대방 필터링 -&amp;gt; 이런 정보들을 미리 계산해둠.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;검색 속도가 빠르고, 데이터 용량이 클 때 스케일링하기 좋음.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;결론: 대규모 데이터에서 빠른 1차 검색 (멀티모달 검색 핵심)&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;결론: Re-ranking 단계에서 정확도 향상 (비용 비쌈)&lt;/li&gt;
&lt;li&gt;모든 쿼리-문서 조합을 모델에 넣어야 해서 대규모 데이터에서는 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;스크린샷 2025-08-03 오후 11.09.30.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n2Dnm/btsPF6JibkU/Hjd7UT8En05zjehALkRubK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n2Dnm/btsPF6JibkU/Hjd7UT8En05zjehALkRubK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n2Dnm/btsPF6JibkU/Hjd7UT8En05zjehALkRubK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn2Dnm%2FbtsPF6JibkU%2FHjd7UT8En05zjehALkRubK%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;653&quot; height=&quot;367&quot; data-filename=&quot;스크린샷 2025-08-03 오후 11.09.30.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;이미지나 오디오를 의미 수준으로 이해해서 사용자의 텍스트 쿼리랑 연결을 해야함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;주로 1차 검색 후 재순위화(re-ranking) 단계에서 rag를 사용함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Re-Ranking 과정&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 data-start=&quot;302&quot; data-end=&quot;354&quot;&gt;&lt;b&gt;쿼리 입력&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; Dual Encoder로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;빠른 검색&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; top N 문서 추출&lt;/li&gt;
&lt;li data-start=&quot;355&quot; data-end=&quot;496&quot;&gt;&lt;b&gt;Re-ranking 단계&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;380&quot; data-end=&quot;496&quot;&gt;
&lt;li data-start=&quot;380&quot; data-end=&quot;458&quot;&gt;&lt;b&gt;Cross Encoder&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용 &amp;rarr; 쿼리와 각 문서를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;하나의 입력으로 합쳐서 모델에 넣고&lt;/b&gt;, relevance score 계산&lt;/li&gt;
&lt;li data-start=&quot;462&quot; data-end=&quot;496&quot;&gt;score 순으로 다시 정렬 &amp;rarr; 최종 top-k 결과 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;데이터 융합(Fusion) 메커니즘&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;초기 융합 (Early Fusion)
&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;저수준(픽셀 값이나 주파수 등 ) 데이터가 한 번에 모델에 들어가서, 모델이 처음부터 숨겨진 패턴을 발견할 수 있게 됨.&lt;/li&gt;
&lt;li&gt;대신, 합치는 과정에서 어떤 부분의 텍스트가 어떤 부분의 이미지나, 어떤 순간의 오디오와 합쳐져야하는지를 알아야함.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 맞추는 과정을 정렬이라고 함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;자동 정렬이 어렵고 까다로운 과정임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;후기 융합 (Late Fusion)
&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;장점:
&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;데이터가 좀 누락되어도 괜찮.&lt;/li&gt;
&lt;li&gt;양식별 최적화 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점:&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터간의 상호작용 손실&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;중간 융합 (intermediate / Hybrid Fusion)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 중간 계층에서 융합하는 것.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;초기 융합과 후기 융합의 장점을 다 가져오는 대신, 설계가 어렵고 비용이 비쌈&lt;/li&gt;
&lt;li&gt;예: 붉은 노을이 있는 해변에 소녀가 있는 사진을 찾으려고 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트로 &quot;소녀가 해변에 앉아있었다&quot;를 입력해서 데이터 찾아옴.&lt;/li&gt;
&lt;li&gt;이미지로 &quot;붉은 노을이 지고 있는 사진&quot;을 입력해서 데이터 찾아옴.&lt;/li&gt;
&lt;li&gt;둘을 따로 처리하다가 중간 지점에서 &quot;소녀랑 해변&quot;이라는 의미를 합쳐서 연관성을 포착한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;중간 어디서 결합할 건지를 결정해야함.&lt;/li&gt;
&lt;li&gt;이미지 한 장이랑 텍스트 한 문장을 비교하려고 해도, 이미지의 모든 픽셀 위치랑 텍스트의 모든 단어 쌍마다 매칭을 해야한다고 함.&lt;/li&gt;
&lt;/ul&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.47.10.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBiEDp/btsPF5KSbYt/3GKkRoXAukNuynrBbk8m80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBiEDp/btsPF5KSbYt/3GKkRoXAukNuynrBbk8m80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBiEDp/btsPF5KSbYt/3GKkRoXAukNuynrBbk8m80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBiEDp%2FbtsPF5KSbYt%2F3GKkRoXAukNuynrBbk8m80%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;577&quot; height=&quot;367&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.47.10.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;그냥 초기 또는 후기 융합 선택하라.&lt;/li&gt;
&lt;/ul&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;b&gt;멀티모달 RAG 구현을 위한 아키텍처 접근 방식&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.48.54.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qoZSA/btsPHDUpH2M/z3lUVlxslZHd4kX8b9xoH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qoZSA/btsPHDUpH2M/z3lUVlxslZHd4kX8b9xoH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qoZSA/btsPHDUpH2M/z3lUVlxslZHd4kX8b9xoH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqoZSA%2FbtsPHDUpH2M%2Fz3lUVlxslZHd4kX8b9xoH1%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;1382&quot; height=&quot;846&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.48.54.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;LLM을 이용해 각각의 데이터를 요약&lt;/li&gt;
&lt;li&gt;원본 데이터 링크랑 같이 각 데이터 타입을 벡터 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.52.23.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4ct9/btsPHiXfSeh/GoD5igefYs0KOwteKnMDo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4ct9/btsPHiXfSeh/GoD5igefYs0KOwteKnMDo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4ct9/btsPHiXfSeh/GoD5igefYs0KOwteKnMDo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4ct9%2FbtsPHiXfSeh%2FGoD5igefYs0KOwteKnMDo1%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;1458&quot; height=&quot;832&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.52.23.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;/ul&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;b&gt;최신 동향 및 미래 전망&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.00.16.png&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bynF4O/btsPF03RI8O/nVfSxAv3HJtP688n4K1VF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bynF4O/btsPF03RI8O/nVfSxAv3HJtP688n4K1VF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bynF4O/btsPF03RI8O/nVfSxAv3HJtP688n4K1VF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbynF4O%2FbtsPF03RI8O%2FnVfSxAv3HJtP688n4K1VF1%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;654&quot; height=&quot;383&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.00.16.png&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gpt가 원래는 텍스트 기반이었다가, 요즘은 이미지도 잘 생산하고, code interpreter도 말하지 따로 않아도 잘 해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;End-to-End 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색기로부터 추출한 데이터와 llm에게 전달하는 간극 해소.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;에이전틱 RAG
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적 쿼리 분해&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&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;b&gt;멀티모달 검색 시스템에서 자주 발생하는 문제 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 1: 모달리티 간 임베딩 불일치 문제&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;해결 1: 임베딩 정합성 향상 방안&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;공통 모델 활용 (ex: CLIP)으로 임베딩 정렬&lt;/li&gt;
&lt;li&gt;도메인 데이터로 파인튜닝&lt;/li&gt;
&lt;li&gt;이미지나 음성도 텍스트 설명으로 변환한 후 텍스트 임베딩으로 통일&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.04.10.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXl4Ad/btsPHGXUZk3/SODQlj5CC7o9G74IweKgZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXl4Ad/btsPHGXUZk3/SODQlj5CC7o9G74IweKgZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXl4Ad/btsPHGXUZk3/SODQlj5CC7o9G74IweKgZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXl4Ad%2FbtsPHGXUZk3%2FSODQlj5CC7o9G74IweKgZ0%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;508&quot; height=&quot;309&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.04.10.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;-&amp;gt; 차원 축소해서 빨리 처리하도록 하면 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.09.37.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yNXGS/btsPGIu76Ih/gHyocQs1S3OdWbpwKhSOj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yNXGS/btsPGIu76Ih/gHyocQs1S3OdWbpwKhSOj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yNXGS/btsPGIu76Ih/gHyocQs1S3OdWbpwKhSOj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyNXGS%2FbtsPGIu76Ih%2FgHyocQs1S3OdWbpwKhSOj0%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;1184&quot; height=&quot;692&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.09.37.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.08.38.png&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Z9c9/btsPESZGe5d/KPo77yamcL1JmhpUiKsHUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Z9c9/btsPESZGe5d/KPo77yamcL1JmhpUiKsHUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Z9c9/btsPESZGe5d/KPo77yamcL1JmhpUiKsHUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Z9c9%2FbtsPESZGe5d%2FKPo77yamcL1JmhpUiKsHUK%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;460&quot; height=&quot;266&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.08.38.png&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;사투리가 너무 심하거나, 글씨체가 안 좋은 경우, 일단 ocr 거친 다음에 언어 모델한테 한 번 더 이 텍스트 읽어보고 정상적인지 물어보고, 고칠부분 있으면 고치라고 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.12.00.png&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJNUKT/btsPF9T3KzB/Kb0JIZtRGeU4aXBLGD8Kn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJNUKT/btsPF9T3KzB/Kb0JIZtRGeU4aXBLGD8Kn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJNUKT/btsPF9T3KzB/Kb0JIZtRGeU4aXBLGD8Kn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJNUKT%2FbtsPF9T3KzB%2FKb0JIZtRGeU4aXBLGD8Kn0%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;602&quot; height=&quot;354&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.12.00.png&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.13.36.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jj89p/btsPEt0tUz9/M7zATSpZtnVEbFnqZjLkMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jj89p/btsPEt0tUz9/M7zATSpZtnVEbFnqZjLkMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jj89p/btsPEt0tUz9/M7zATSpZtnVEbFnqZjLkMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJj89p%2FbtsPEt0tUz9%2FM7zATSpZtnVEbFnqZjLkMk%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;575&quot; height=&quot;333&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.13.36.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ui상 모달별 결과 분리 -&amp;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;/ul&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.15.12.png&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JQItm/btsPHiQwg9q/OBvisGk1HEY0ifGghiP8Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JQItm/btsPHiQwg9q/OBvisGk1HEY0ifGghiP8Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JQItm/btsPHiQwg9q/OBvisGk1HEY0ifGghiP8Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJQItm%2FbtsPHiQwg9q%2FOBvisGk1HEY0ifGghiP8Qk%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;495&quot; height=&quot;286&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.15.12.png&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;새로운 지표를 계속 모니터링하면서 갱신해야함.&lt;/li&gt;
&lt;li&gt;임베딩 모델 교체는 굉장히 조심스럽게 결정해야함.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언어 모델은 쉽게 바꿔도 임베딩 모델 한 번 바꾸면 벡터 부분이 전부 다 바뀌므로 신중해야함.&lt;/li&gt;
&lt;/ul&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.17.08.png&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LGCim/btsPHhKJyqK/8tKUCVKmJeWjf3yBOQznG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LGCim/btsPHhKJyqK/8tKUCVKmJeWjf3yBOQznG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LGCim/btsPHhKJyqK/8tKUCVKmJeWjf3yBOQznG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLGCim%2FbtsPHhKJyqK%2F8tKUCVKmJeWjf3yBOQznG0%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;288&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.17.08.png&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터에 개인 정보가 포함될 수 있으므로 데이터에 접근 제어 및 암호화해야함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;예: 부동산 사진이 사람 얼굴이 들어갈 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.18.00.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t9PG5/btsPGj3bPvb/6YBqNONAx618YEfKYNZzl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t9PG5/btsPGj3bPvb/6YBqNONAx618YEfKYNZzl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t9PG5/btsPGj3bPvb/6YBqNONAx618YEfKYNZzl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft9PG5%2FbtsPGj3bPvb%2F6YBqNONAx618YEfKYNZzl1%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;558&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2025-08-04 오후 3.18.00.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 업계라고해도, 병원만해도 피검사, ct 검사 등을 분석하는 모델이 다르듯, 획일적 솔루션으로는 모든 요구 충족이 어렵기 때문에 모듈식 시스템으로 유연한 커스터마이징을 해야함.&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;Langchain Github
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/langchain-ai/langchain/blob/master/cookbook/Semi_structured_and_multi_modal_RAG.ipynb&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/langchain-ai/langchain/blob/master/cookbook/Semi_structured_and_multi_modal_RAG.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CLIP Sample
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mlfoundations/open_clip&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/mlfoundations/open_clip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://colab.research.google.com/github/mlfoundations/open_clip/blob/master/docs/Interacting_with_open_clip.ipynb&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://colab.research.google.com/github/mlfoundations/open_clip/blob/master/docs/Interacting_with_open_clip.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data Analysis/LLM</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/765</guid>
      <comments>https://hororolol.tistory.com/765#entry765comment</comments>
      <pubDate>Mon, 4 Aug 2025 15:21:52 +0900</pubDate>
    </item>
    <item>
      <title>[요즘 우아한 AI 개발] Part 3 내용 및 용어 정리</title>
      <link>https://hororolol.tistory.com/764</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;LLM 기반 ReAct 방법&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;LLM 기반의 프롬프트 설계 기법.&lt;/li&gt;
&lt;li&gt;모델이 추론(Reasoning)과 행동(Action)을 반복하며 문제를 해결하도록 설계된 방식.&lt;/li&gt;
&lt;li&gt;주로 지식 기반 시스템이나 에이전트 설계에서 사용되며, 모델이 논리적으로 문제를 해결하는 동시에 외부 도구(예: 검색기, 계산기 등)를 활용해 최적의 답을 도출할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;LLM 기반 ReAct 방법은 다양한 벤치마크에서 모방 학습과 강화 학습에 비해 더 높은 답변 성능을 보여준다고 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ReAct 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReAct 방법은 문제 해결 과정을 위한 순차적 추론 단계(chain-of-thought, COT)와 특정 작업 수행을 위한 도구 또는 행동으로 나뉜다.&lt;/li&gt;
&lt;/ul&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;b&gt;라고스(Regas)&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;RAG 파이프라인을 평가하는 프레임워크.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;외부 데이터를 사용해 LLM의 컨텍스트를 확장하는 응용 프로그램을 평가하는 데 필요한 다양한 평가 지표를 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&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;폴라스로 데이터 처리를 더 빠르고 가볍게&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Apache Spark&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;/li&gt;
&lt;li&gt;스파크는 메모리 내에서 데이터를 처리하여 기존의 하둡(MapReduce)보다 훨씬 빠르게 데이터를 처리할 수 있으며, 데이터 스트리밍, 머신러닝, 그래프 분석 등 다양한 기능을 제공한다.&lt;/li&gt;
&lt;/ul&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;b&gt;Trio&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;분산 SQL 쿼리 엔진으로, 여러 데이터 소스에서 데이터를 통합하여 실시간으로 쿼리할 수 있는 기능을 제공한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;원래 PrestoSQL로 알려져 있었으며, 다양한 데이터베이스나 데이터 레이크에 걸쳐 효율적인 분석을 수행할 수 있는 오픈 소스 도구이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;b&gt;S3(아마존 Simple Storage Service)&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;AWS에서 제공하는 확정성 높은 객체 스토리지 서비스.&lt;/li&gt;
&lt;li&gt;S3는 데이터를 파일 형태로 저장하며, 무제한의 데이터 저장을 지원하고 안정적인 백업, 데이터 아카이빙, 웹 애플리케이션 호스팅 등의 용도로 사용된다.&lt;/li&gt;
&lt;/ul&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;gt; 대용량 분산 처리가 필요한 부분은 트리노나 스파크 기반 SQL을 사용해 1차 전처리를 하고, 이를 테이블 형태 또는 S3 내에 파일 형태로 저장한다.&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;비용 효율성이 낮다(비싼 리소스)&lt;/li&gt;
&lt;li&gt;러닝 커브 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉 데이터를 분석하고 처리하는 데에 있어 판다스는 너무 느리고 무겁고, 스파크는 과함.&lt;/li&gt;
&lt;li&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;별다른 인프라 필요 없음&lt;/li&gt;
&lt;li&gt;성능 좋고, 러닝 커브 적은 것&lt;/li&gt;
&lt;li&gt;아파치 에어플로 환경 또는 컨테이너 기반으로 잘 패키징해서 운영 데이터 파이프라인에서 문제없이 돌릴 수 있는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;Polars&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;/li&gt;
&lt;li&gt;특히 대용량 데이터를 처리하는 데 최적화된 성능을 제공한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Rust로 작성된 폴라스는 판다스와 비슷한 기능을 제공하지만, 멀티스레딩을 활용해 더 빠른 연산 속도를 자랑하며, 메모리 효율성을 극대화합니다.&amp;nbsp;
&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;메모리 캐싱과 재사용성 높음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;판다스의 대안으로 주목받고 있으며, 특히 데이터 크기가 클수록 성능 차이가 두드러진다.&lt;/li&gt;
&lt;/ul&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;아파치 애로우(Apache Arrow) 모델&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;대규모 데이터를 효율적으로 처리하기 위한 칼람 기반 인메모리 형식으로, CPU 캐시 효율을 극대화하고 벡터화 연산을 지원한다.&lt;/li&gt;
&lt;li&gt;이를 통해 다양한 시스템과 언어 간 데이터 공유를 빠르게 메모리 복사 없이 할 수 있다.&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;&lt;b&gt;SIMD(Single Instruction, Mltiple Data)&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;/li&gt;
&lt;li&gt;주로 벡터 연산에서 사용되며, 같은 연산을 여러 데이터에 병렬로 수행함으로써 처리 성능을 크게 향상시킨다.&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;&lt;b&gt;Zero-copy&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;/li&gt;
&lt;/ul&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;div style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot;&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;- [요즘&amp;nbsp;우아한&amp;nbsp;AI&amp;nbsp;개발]&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: center;&quot;&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;div id=&quot;aswift_2_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Data Analysis/LLM</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/764</guid>
      <comments>https://hororolol.tistory.com/764#entry764comment</comments>
      <pubDate>Wed, 9 Jul 2025 13:58:54 +0900</pubDate>
    </item>
    <item>
      <title>[요즘 우아한 AI 개발] Part 2 프롬프트 엔지니어링</title>
      <link>https://hororolol.tistory.com/763</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Part&amp;nbsp;2&amp;nbsp;-&amp;nbsp;AI로&amp;nbsp;더&amp;nbsp;편리한&amp;nbsp;서비스&amp;nbsp;만들기&amp;nbsp;&lt;/h2&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;이제 &quot;치킨&quot;같은 음식 이름뿐만 아니라 &quot;금요일&quot;, &quot;매운&quot;, &quot;해장음식&quot; 등 키워드를 넣어도 추천해줌&lt;/li&gt;
&lt;/ul&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;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구체적인 프롬프트 작성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상황과 형식을 명확하게 제시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &quot;첨부한 문서를 요약해주세요&quot; -&amp;gt; &quot;첨부한 회의록을 3가지 핵심 사항으로 요약하고 마크다운 형식으로 정리해주세요.&quot;&lt;/li&gt;
&lt;li&gt;예: &quot;저화질인지 판단하세요&quot; -&amp;gt; &quot;이미지 내부의 주요한 객체가 선명하지 않거나 화질이 깨졌는지 판단하세요.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;프롬프트를 너무 구체화하면 예외 케이스를 놓칠 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &quot;워터마크처럼 합성된 글씨가 있는지 판단하세요&quot;&lt;/li&gt;
&lt;li&gt;워터마크는 잘 잡지만, 다른 합성 텍스트는 놓칠 수 있다.&lt;/li&gt;
&lt;li&gt;-&amp;gt; &quot;워터마크와 같이 인위적으로 합성된 글씨가 있는지 판단하세요.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;구조화된 응답 유도
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답을 JSON 형식으로 포매팅하여 반환하도록 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;생성 지식 프롬프트(Generated Knowledge Prompting)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM이 자체적으로 생성한 지식을 활용하여 특정 질문에 대한 응답을 강화하거나 보완하는 프롬프트 설꼐 기법. 모델이 학습 데이터 외부의 지식을 포함한 질문에 답변할 때 활용된다.&lt;/li&gt;
&lt;li&gt;이는 생각의 사슬(Chain of Thought, CoT)와 유사한 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: GPT가 이미지 내부의 모습 설명을 생성하고, 이 설명을 바탕으로 최종 응답을 도출하도록 하는 방법&lt;/li&gt;
&lt;li&gt;이렇게하면 응답의 일관성이 약 40% 향상됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Latency 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;input 프롬프트 최적화도 필요하지만, 불필요한 응답을 줄이는 것이 훨씬 중요하다.&lt;/li&gt;
&lt;li&gt;예: 응답 사유를 한 문장으로만 반환하도록 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;이미지와 테스트 순서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지와 텍스트를 함께 사용할 때는 이미지를 텍스트 프롬프트보다 먼저 배치하는 것이 도움된다. -&amp;gt; 응답 정확도 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;중요한 내용을 강조하기 위해 헤더(#)를 사용해 섹션을 구분&lt;/li&gt;
&lt;li&gt;여러 정보나 정책 항목을 나열할 때는 목록 형태로 표시&lt;/li&gt;
&lt;li&gt;프롬프트의 전체적인 구조를 코드 스타일로 정리(파이썬의 딕셔너리같은 데이터 구조로)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751431604381&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# instructions = '이미지를 정책(policies)에 따라 분석하고 지정된 JSON 형식으로 결과를 반환하세요.'
# policies = { 
	1: '주요한 음식에 초점이 맞지 않습니다.', 
	2: '해상도가 낮아서 픽셀단위로 질감이 보입니다.'
	# (생략) 
}&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;GPT 한계 극복: 하이브리드 접근의 필요성&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;이미지 속 객체 인식 잘 못 함 -&amp;gt; 정확한 객체 탐지 및 좌표 반환을 못 함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gpt 파인 튜닝: &lt;a href=&quot;https://openai.com/index/introducing-vision-to-the-fine-tuning-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openai.com/index/introducing-vision-to-the-fine-tuning-api/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;저작권/상표권/초상권 등 이미지 속에 포함된 캐릭터나 로고에 저작권 문제 없는지 판단 못 함 -&amp;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;b&gt;실시간 반응형 추천 시스템&lt;/b&gt;&lt;/h4&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;3 가지 컴포넌트를 개발
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실시간 행동 이력 스트리밍&lt;/li&gt;
&lt;li&gt;인코더 모델 학습 및 임베딩 추출&lt;/li&gt;
&lt;li&gt;벡터 유사도 검색 (Vector Similarity Search, VSS)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴포넌트 1: 실시간 행동 이력 스트리밍&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;실시간 행동 이력 조회 API를 통해 몽고디비에 적재된 사용자의 행동 이력을 조회&lt;/li&gt;
&lt;li&gt;Apache Flink + Amazon EKS(Elastic Kubernetes Service)로 실시간 로그 파이프라인 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴포넌트 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;/li&gt;
&lt;li&gt;가게의 메타 정보와 검색 로그 등을 활용해 가게와 검색어가 유사할수록 더 가까운 벡터 공간에 위치시키는 인코더 모델을 학습시킴.&lt;/li&gt;
&lt;li&gt;학습된 인코더 모델로 가게 및 검색어의 임베딩을 추출하고, 이를 VSS 컴포넌트가 사용하는 VectorDB에 업로드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴포넌트 3: 벡터 유사도 검색&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;사용자의 행동 이력과 추천 후보 가게 목록이 주어졌을 때 벡터디비에서 각 임베딩 값을 조회한 후, 이들 사이의 코사인 유사도를 계산해 응답하는 컴포넌트를 개발.&lt;/li&gt;
&lt;li&gt;여기서는 AWS RDS PostgreSQL을 벡터디비로 사용.&lt;/li&gt;
&lt;/ul&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;벡터 유사도 검색&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exact-KNN(K-Nearest Neighbor)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임베딩 벡터 공간 내의 좌표 간 거리를 정확하게 계산하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ANN(Approximate Nearest Neighbor)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;recall(재현율)을 합리적으로 희생하면서도 검색 성능을 올리는 알고리즘&lt;/li&gt;
&lt;li&gt;Exact-KNN보다 많이 쓰임&lt;/li&gt;
&lt;li&gt;IVFFlat이나 HNSW 같은 알고리즘이 이 예다.&lt;/li&gt;
&lt;li&gt;ANN 알고리즘은 미리 인덱스를 빌드해놓고, 검색 시점에서는 인덱스를 통해 성능을 향상시키는 방식의 알고리즘이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HNSW 알고리즘에서 보는 pre filter의 문제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HNSW는 일종의 그래프를 활용하여 가까운 검색 대상을 효율적으로 찾는 알고리즘이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;모든 연결 그래프를 다 탐색하면 시간이 걸리니, 미리 검색에 사용할 밀도를 낮춘 그래프를 레이어 기반으로 인덱스 빌드 시점에 만들어둔다.&lt;/li&gt;
&lt;li&gt;ANN의 성능 개선은 대체로, 검색 전에 데이터를 기준으로 미리 인덱스를 만들고, 검색 시점에 인덱스를 사용하는 데서 나온다.&amp;nbsp; 우리의 프리 필터 문제는 좁혀둔 후보군에 대응하는 인덱스가 없기 때문에 ANN을 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;벡터 유사도 검색에서 pre filter 문제&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- [요즘&amp;nbsp;우아한&amp;nbsp;AI&amp;nbsp;개발]&lt;/p&gt;</description>
      <category>Data Analysis/LLM</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/763</guid>
      <comments>https://hororolol.tistory.com/763#entry763comment</comments>
      <pubDate>Wed, 2 Jul 2025 14:37:40 +0900</pubDate>
    </item>
    <item>
      <title>SSE(Server-Sent Events)란? WebSocket과의 차이</title>
      <link>https://hororolol.tistory.com/760</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹소켓은 단일 TCP 연결로 동시양방향통신 채널을 제공하는 컴퓨터 통신 프로토콜&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SSE(Server-Sent Events)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 클라이언트(주로 브라우저)가 서버로부터 단방향 실시간 데이터를 스트리밍 방식으로 수신할 수 있게 해주는 기술이다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocket과 달리 양방향 통신이 아닌, 서버 -&amp;gt; 클라이언트 방향으로만 데이터를 전송한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;주로 뉴스 속보, 주식 가격, 채팅 알림 등의 실시간 알림 시스템에 사용된다.&lt;/li&gt;
&lt;/ul&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;b&gt;SSE 동작 방식&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;클라이언트가 text/event-stream 헤더로 서버에 요청&lt;/li&gt;
&lt;li&gt;서버는 해당 요청을&lt;b&gt; 끊지 않고&lt;/b&gt;, &lt;b&gt;스트리밍&lt;/b&gt; 방식으로 데이터를 지속적으로 전송 (HTTP 연결을 장시간 유지)&lt;/li&gt;
&lt;li&gt;클라이언트는 전송받은 데이터를 이벤트로 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1749540325781&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /events HTTP/1.1
Accept: text/event-stream&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749540366923&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data: Hello\n\n
data: Another message\n\n&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;JavaScript에서 처리 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749540389662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const evtSource = new EventSource(&quot;/events&quot;);

evtSource.onmessage = function(event) {
  console.log(&quot;New event:&quot;, event.data);
};&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;SSE는 클라이언트가 자동으로 재연결을 시도하며, 이전 이벤트의 ID를 HTTP 헤더로 보낸다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 이전 이벤트의 ID를 담아서 서버로 전달하기 때문에, 서버는 ID별로 이벤트를 저장하거나 복구 전략을 고려해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749540616954&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Last-Event-ID: 12345&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSE는 상태를 서버가 유지하므로, 서버가 여러 대일 경우 Sticky Session 또는 Pub/Sub 전략이 필요함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;SSE vs WebSocket&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 131px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SEE&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;통신 방향&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단방향 (서버 &amp;rarr; 클라이언트)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&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;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복잡도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;낮음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&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;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP 기반&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Yes (단순 GET 요청)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;No (업그레이드 필요)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메시지 포맷&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;텍스트 전용 (text/event-stream)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&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;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자동 재연결&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 제공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&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;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트 수용량&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상대적으로 적음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;더 많은 커넥션 가능 (비동기 IO 필요)&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;</description>
      <category>개발</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/760</guid>
      <comments>https://hororolol.tistory.com/760#entry760comment</comments>
      <pubDate>Tue, 10 Jun 2025 16:35:25 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 예제로 이해하는 인덱싱(Indexing)과 정규와, 비정규화, 반정규화</title>
      <link>https://hororolol.tistory.com/759</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;인덱싱 (Indexing)&lt;/b&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;/li&gt;
&lt;/ul&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;b&gt;인덱스 종류&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 110px;&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: 50%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;인덱스 종류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;Primary Key&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;테이블의 고유 식별자. 자동으로 UNIQUE + NOT NULL 인덱스 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;Unique Key&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;중복 허용 안 됨. 이메일, 주민번호 등에 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;Index (또는 key)&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 19px;&quot;&gt;일반 인덱스. 중복 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 17px;&quot;&gt;FullText Index&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 17px;&quot;&gt;전문 검색용 인덱스 (MyISAM, InnoDB 5.6 이상에서 지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 17px;&quot;&gt;Spatial Index&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center; height: 17px;&quot;&gt;공간 데이터(GIS) 전용 인덱스&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;MySQL의 기존 스토리지 엔진인 InnoDB는 B+Tree 구조로 인덱스를 저장한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Clustered Index: Primary Key 기준으로 실제 데이터도 정렬됨&lt;/li&gt;
&lt;li&gt;Secondary Index: 실제 데이터가 아닌 PK를 참조 (row lookup 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;Clustered Index&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;테이블 자체의 물리적 저장 순서를 결정하는 인덱스 -&amp;gt; 테이블의 데이터 자체가 인덱스 구조에 따라 정렬되어 저장된다.&lt;/li&gt;
&lt;li&gt;테이블에 하나만 존재 가능&lt;/li&gt;
&lt;li&gt;일반적으로 Primary Key가 자동으로 Clustered Index가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748854538802&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  age INT
);&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Secondary Index&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;/li&gt;
&lt;li&gt;여러 개 생성 가능&lt;/li&gt;
&lt;li&gt;secondary 인덱스를 통해 먼저 위치를 찾고, 실제 데이터를 Clustered Index를 통해 다시 읽음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748854607817&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_age ON users(age);

// 키는 age, 값은 해당 row의 id (즉, clustered index의 주소)
[ 22 ] &amp;rarr; id=1 (users의 id)
[ 25 ] &amp;rarr; id=3
[ 30 ] &amp;rarr; id=2
이런식으로 저장됨.&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;즉, Primary key를 만들면 그게 Clustered Index가 되고, 나머지는 모두 Secondary Index다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SELECT * FROM users WHERE age = 25;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 쿼리가 실행되면
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;idx_age(보조 인덱스)에서 25라는 나이를 찾고, id=3을 발견&lt;/li&gt;
&lt;li&gt;id=3으로 Clustered Index를 타고 가서 실제 row 데이터를 읽음&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;언제 인덱스를 걸까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE, JOIN, ORDER BY, GROUP BY에 자주 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;FK, 유니크 값 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fk의 경우 자식 테이블에서 부모 테이블의 값을 찾기 때문에, 참조 대상 칼럼에 인덱스가 있어야 효율적.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;큰 테이블의 범위 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: SELECT&amp;nbsp;*&amp;nbsp;FROM&amp;nbsp;users&amp;nbsp;WHERE&amp;nbsp;age&amp;nbsp;BETWEEN&amp;nbsp;30&amp;nbsp;AND&amp;nbsp;40;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;age에 인덱스가 있다면, 30 이상인 위치로 바로 Jump하고, 거기서부터 40까지 선형 탐색만 함.&lt;/li&gt;
&lt;li&gt;b-tree (균형 이진 트리) 구조라 이진 탐색으로 빠르게 찾음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;과도한 인덱싱은 오히려 성능 저하
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 성능(Insert, Update, Delete) 저하&lt;/li&gt;
&lt;li&gt;인덱스 저장공간 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;예제 1: 블로그 시스템 DB 설계 (1:N 관계)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748152930060&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 사용자
CREATE TABLE users (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  email VARCHAR(100) NOT NULL UNIQUE,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 게시글
CREATE TABLE posts (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id),
  INDEX idx_user_created_at (user_id, created_at)
);

-- 댓글
CREATE TABLE comments (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  post_id BIGINT UNSIGNED NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL,
  content TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts(id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  INDEX idx_post_created_at (post_id, created_at)
);&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;user &amp;lt;-&amp;gt; posts는 1:N, posts &amp;lt;-&amp;gt; comments도 1:N 관계&lt;/li&gt;
&lt;li&gt;posts.user_id, comments.post_id에 인덱스를 추가해 작성자별, 게시글별 조회 속도 향상&lt;/li&gt;
&lt;li&gt;idx_user_created_at: 유저별 글을 시간순으로 정렬할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748153033050&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * 
FROM posts
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 위처럼 &quot;user 42가 작성한 최근 글 10 개 가져오기&quot; 요청할 때,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스가 없다면 mysql은 comments 테이블 전체에서 user_id = 42를 풀스캔(full table scan) 하거나, user_id만 인덱스에 있어도 filesort를 수행하게 된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ORDER BY created_at 정렬 작업은 메모리 또는 디스크에서 수동으로 진행되기 때문에 느림.&lt;/li&gt;
&lt;li&gt;그런데 INDEX (user_id, created_at) 설정을 하게 되면, mysql은 인덱스 자체가 정렬된 자료구조(B+ Tree)이기 때문에 (user_id, created_at) 순서로 인덱스가 잡혀 있으면, 정렬 없이도 빠르게 원하는 결과를 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;MySQL은 복합 인덱스를 왼쪽부터 순차적으로 이용하기 때문에, &lt;b&gt;인덱스에 나열된 컬럼 순서가 쿼리 성능에 결정적인 영향&lt;/b&gt;을 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.07.26.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/euhpVR/btsOcy1f4mV/gmII4TizWU4kuRYBvfTOJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/euhpVR/btsOcy1f4mV/gmII4TizWU4kuRYBvfTOJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/euhpVR/btsOcy1f4mV/gmII4TizWU4kuRYBvfTOJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeuhpVR%2FbtsOcy1f4mV%2FgmII4TizWU4kuRYBvfTOJk%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;214&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.07.26.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;390&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;예제 2: 팔로우 시스템 (자기참조 관계)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748154075453&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE users (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE follows (
  follower_id BIGINT UNSIGNED NOT NULL,
  followee_id BIGINT UNSIGNED NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (follower_id, followee_id),
  FOREIGN KEY (follower_id) REFERENCES users(id),
  FOREIGN KEY (followee_id) REFERENCES users(id),
  INDEX idx_followee (followee_id)
);&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;user &amp;lt;-&amp;gt; user 참조하는 형태&lt;/li&gt;
&lt;li&gt;follower_id, followee_id를 각각 인덱싱 (팔로잉 / 팔로우 조회)&lt;/li&gt;
&lt;li&gt;PRIMARY KEY (follower_id, followee_id): 두 사람의 관계는 중복되지 않도록 보장 + 빠른 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정규화(Normalization), 비정규화(Denormalization), 반정규화(Semi-denormalization)&lt;/h2&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;/ul&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;b&gt;정규화(Normalization)&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;/li&gt;
&lt;li&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;데이터 무결성 유지&lt;/li&gt;
&lt;li&gt;ANOMALY 방지 (삽입/삭제/갱신 시 문제 최소화)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입 이상 (Insertion Anomaly)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제 상황: Physics 과목을 추가하려고 할 때, 수강생이 없다면 course_name과 professor만 입력해야 하는데, student_id가 Not Null 설정되어 있어서 입력 불가.&lt;/li&gt;
&lt;li&gt;문제 해결: 과목 정보를 별도 테이블로 분리 (정규화)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;삭제 이상 (Deletion Anomaly)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제 상황: 학생 Charlie가 수강 취소하면, English 과목 자체가 DB에서 사라진다. (과목이 학생에게 의존된 상황)&lt;/li&gt;
&lt;li&gt;문제 해결: 과목과 학생 정보를 별도 테이블로 분리해서 보존.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;갱신 이상 (Update Anomaly)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제 상황: 수학 교수님이 Dr. Kim -&amp;gt; Dr. Park으로 변경되었는데, 해당 과목을 수강 중인 모든 학생 레코드에 반영해야 함. 일부만 수정되면 데이터 정합성 깨짐 (누군가의 데이터는 과거 교수님 이름으로 기록됨)&lt;/li&gt;
&lt;li&gt;문제 해결: 교수 정보는 과목 테이블에 넣고, 참조로 관리갱신 이상 (Update Anomaly)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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/bWoiic/btsOcPPfWVp/nGAIwLqPnxmt4bJc0LnYTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWoiic/btsOcPPfWVp/nGAIwLqPnxmt4bJc0LnYTk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;244&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.40.46.png&quot; style=&quot;width: 37.2582%; margin-right: 10px;&quot; data-widthpercent=&quot;37.7&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWoiic/btsOcPPfWVp/nGAIwLqPnxmt4bJc0LnYTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWoiic%2FbtsOcPPfWVp%2FnGAIwLqPnxmt4bJc0LnYTk%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;1086&quot; height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Br6yT/btsObLfY67Z/uSIQzDtgHF1t0Szmd9PYJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Br6yT/btsObLfY67Z/uSIQzDtgHF1t0Szmd9PYJ1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;146&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.40.55.png&quot; style=&quot;width: 61.579%;&quot; data-widthpercent=&quot;62.3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Br6yT/btsObLfY67Z/uSIQzDtgHF1t0Szmd9PYJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBr6yT%2FbtsObLfY67Z%2FuSIQzDtgHF1t0Szmd9PYJ1%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;1074&quot; height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/div&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;&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;size18&quot;&gt;&lt;b&gt;예: 학생이 여러 과목을 수강하는 데이터&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.32.12.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lCHLx/btsOb9Orpnz/lA337OrNsvKX8tvFTV0oHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lCHLx/btsOb9Orpnz/lA337OrNsvKX8tvFTV0oHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lCHLx/btsOb9Orpnz/lA337OrNsvKX8tvFTV0oHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlCHLx%2FbtsOb9Orpnz%2FlA337OrNsvKX8tvFTV0oHK%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;1032&quot; height=&quot;224&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.32.12.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;224&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;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;&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 정규화 (Normalization)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.41.21.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s27wE/btsObrIOlwY/knepjPxlnJSStkYsXxhYS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s27wE/btsObrIOlwY/knepjPxlnJSStkYsXxhYS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s27wE/btsObrIOlwY/knepjPxlnJSStkYsXxhYS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs27wE%2FbtsObrIOlwY%2FknepjPxlnJSStkYsXxhYS0%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;770&quot; height=&quot;172&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.41.21.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&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;size18&quot;&gt;&lt;b&gt;2. 비정규화 (Denormalization)&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;/li&gt;
&lt;li&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;읽기 성능 감소&lt;/li&gt;
&lt;li&gt;쿼리 구조 단순화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.35.20.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qhiqk/btsOcxVB75I/hb0xXa3V6981lpu0gkBIN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qhiqk/btsOcxVB75I/hb0xXa3V6981lpu0gkBIN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qhiqk/btsOcxVB75I/hb0xXa3V6981lpu0gkBIN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQhiqk%2FbtsOcxVB75I%2Fhb0xXa3V6981lpu0gkBIN1%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;588&quot; height=&quot;173&quot; data-filename=&quot;스크린샷 2025-05-25 오후 3.35.20.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;210&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;b&gt;3. 반정규화 (Semi-denormalization)&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;/li&gt;
&lt;li&gt;정규화는 유지하되, 자주 쓰이는 값을 중복 컬럼 또는 캐싱 컬럼으로 일부 테이블에 둠.&lt;/li&gt;
&lt;li&gt;예: 게시글의 댓글 수를 coments 테이블에서 매번 COUNT 하지 않고 posts 테이블에 comment_count 컬럼을 둬서 캐싱
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 comments 값이 생길 때마다, posts의 comment_count 값을 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data Analysis/Database</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/759</guid>
      <comments>https://hororolol.tistory.com/759#entry759comment</comments>
      <pubDate>Sun, 25 May 2025 15:48:07 +0900</pubDate>
    </item>
    <item>
      <title>추가 배포 없이 API 케이스 통일하기: 카카오 사례 및 구현 가이드</title>
      <link>https://hororolol.tistory.com/757</link>
      <description>&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;i&gt;카카오 기술 블로그 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://tech.kakao.com/posts/665?utm_source=chatgpt.com&quot;&gt;&amp;ldquo;추가배포 없이 API의 case 통일시키기&amp;rdquo;&lt;/a&gt;를 토대로 작성한 글입니다.&lt;/i&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;문제 정의&lt;/b&gt;&lt;/span&gt;&lt;/h4&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&quot;Admin에서는 Camel case를 써요&quot;, &quot;외부 연동처에서는 Snake case로 내려달라고 하네요&quot; 등 의도치 않게 서비스별로 JSON 케이스가 혼용되어 코드베이스에 여러 Naming 전략이 섞이게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DTO 중복&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여러 대상 서버의 케이스 규칙에 맞추려면 DTO를 여러 벌 정의해야 하는 상황이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1745316525443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(Camel Case) -&amp;gt; SERVER -&amp;gt; (Snake Case)&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동일한 값의 DTO지만 받을 때는 Camel case, 보낼 때는 Snake case가 필요한 상황이 있을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위와 같은 문제는 MSA 환경에서 더욱 빈번하게 발생할 수 있고, 무중단 서비스에서는 쉽게 고치기 어렵다.&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;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;추가 배포 없이 API 케이스 통일하기 3단계&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;957&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmXRde/btsNu4gECJv/8TR8kWTgMQ8fZZzeGeN3k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmXRde/btsNu4gECJv/8TR8kWTgMQ8fZZzeGeN3k0/img.png&quot; data-alt=&quot;출처: https://tech.kakao.com/posts/665&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmXRde/btsNu4gECJv/8TR8kWTgMQ8fZZzeGeN3k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmXRde%2FbtsNu4gECJv%2F8TR8kWTgMQ8fZZzeGeN3k0%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;567&quot; height=&quot;424&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;957&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://tech.kakao.com/posts/665&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Case-Insensitive 파싱 모듈 도입
&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;서버들이 camelCase 또는 snake_case JSON을 받아도 문제 없이 역직렬화할 수 있도록 Jackson의 기능을 활용.&lt;/li&gt;
&lt;li&gt;ObjectMapper 설정에 accept-case-insensitive-properties: true 옵션을 설정하면, 케이스 무관하게 필드를 바인딩할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1745317931389&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jackson:
    mapper:
      accept-case-insensitive-properties: true&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;p data-ke-size=&quot;size16&quot;&gt;2. snake_case 응답/요청으로 일괄 전환&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 목표: API를 외부에 snake_case로 일원화&lt;/li&gt;
&lt;li&gt;요청(Request)은 스프링에서 제공하는&amp;nbsp;&lt;b&gt;RequestBodyAdvice&lt;/b&gt;를 활용하여 AOP 방식으로 camelCase -&amp;gt; snake_case로 변환된 JSON을 강제 파싱&lt;/li&gt;
&lt;li&gt;응답(Response)은 공통 ApiResponse&amp;lt;T&amp;gt; 래퍼를 사용해 내부 데이터를 가공한 뒤 snake_case로 직렬화.&lt;/li&gt;
&lt;li&gt;서버 응답 시 ObjectMapper에 PropertyNamingStarategies.SNAKE_CASE를 적용해 snake_case가 강제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745318069585&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    return mapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745320402687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice
public class SnakeCaseRequestAdvice implements RequestBodyAdvice {

    private final ObjectMapper snakeCaseMapper;

    public SnakeCaseRequestAdvice(ObjectMapper defaultMapper) {
        this.snakeCaseMapper = defaultMapper.copy();
        this.snakeCaseMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType) {

        // 모든 요청에 대해 Advice를 적용할지 여부
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                           Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType) throws IOException {
        // body를 직접 수정할 게 아니라면 그대로 반환
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
                                Type targetType, Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType) {
        // 여기서는 실제 바디 파싱이 끝난 이후라 바꾸는 작업은 거의 안 함
        return body;
    }

    @Override
    public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
                                  Type targetType, Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType) {
        return body;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 모듈 정리 및 경량화&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;1단계에서 도입했던 case-insentive 파싱 모듈(accept-case-insensitive-properties)을 제거 -&amp;gt; 더 이상 필요 없어서&lt;/li&gt;
&lt;li&gt;Jackson 관련 설정(RequestBodyAdvice)도 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2095&quot; data-start=&quot;2079&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이 접근 방식의 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2332&quot; data-start=&quot;2097&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2160&quot; data-start=&quot;2097&quot;&gt;&lt;b&gt;점진적 적용 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2160&quot; data-start=&quot;2097&quot;&gt;기존 API 사용자가 인지하지 못한 채로 변화를 적용할 수 있어 위험이 낮음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2218&quot; data-start=&quot;2161&quot;&gt;&lt;b&gt;코드 수정 최소화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2218&quot; data-start=&quot;2161&quot;&gt;DTO를 복제하거나 대규모 수정 없이도 명명 규칙을 바꿀 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2275&quot; data-start=&quot;2219&quot;&gt;&lt;b&gt;플랫폼 일관성 확보&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2275&quot; data-start=&quot;2219&quot;&gt;외부 연동, 프론트엔드 API 호출 시 명확한 포맷 통일 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2332&quot; data-start=&quot;2276&quot;&gt;&lt;b&gt;테스트/배포 비용 절감&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2332&quot; data-start=&quot;2276&quot;&gt;기존 API의 동작을 유지하면서 새로운 형식으로 전환 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/757</guid>
      <comments>https://hororolol.tistory.com/757#entry757comment</comments>
      <pubDate>Tue, 22 Apr 2025 20:16:52 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 CPU 리소스 관리 cpu.requests cpu.limits 설정 Throttling이란</title>
      <link>https://hororolol.tistory.com/755</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000; text-align: start;&quot; href=&quot;https://techblog.lycorp.co.jp/ko&quot;&gt;LY Corporation Tech Blog&lt;/a&gt;의 &lt;a href=&quot;https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;당신의 CPU는 열심히 일하고 있나요?&lt;/a&gt; 글을 요약 및 기타 정보를 추가하여 작성한 글입니다. 자세한 내용은 링크를 참조해 주세요!&lt;/span&gt;&lt;/i&gt;&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;&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;CPU 요청량(cpu.requests) vs CPU 상한(cpu.limits)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1744464330181&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: &quot;64Mi&quot;
        cpu: &quot;250m&quot;
      limits:
        memory: &quot;128Mi&quot;
        cpu: &quot;500m&quot; # Can we remove this cpu limits field?&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;CPU 요청량 (cpu.request)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod가 사용을 보장받는 최소 CPU 리소스 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CPU 상한 (cpu.limits)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod가 사용할 수 있는 최대 CPU 리소스 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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/bBuSsR/btsNiA0DliR/8L9iSX0K723x9KyI1A9bs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBuSsR/btsNiA0DliR/8L9iSX0K723x9KyI1A9bs0/img.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;802&quot; data-is-animation=&quot;false&quot; width=&quot;465&quot; style=&quot;width: 32.0201%; margin-right: 10px;&quot; data-widthpercent=&quot;32.78&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBuSsR/btsNiA0DliR/8L9iSX0K723x9KyI1A9bs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBuSsR%2FbtsNiA0DliR%2F8L9iSX0K723x9KyI1A9bs0%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;1100&quot; height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPVFPr/btsNjCc4hXU/ItNLHkqQGPdqXpVkKNuwp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPVFPr/btsNjCc4hXU/ItNLHkqQGPdqXpVkKNuwp1/img.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;774&quot; data-is-animation=&quot;false&quot; width=&quot;399&quot; height=&quot;285&quot; style=&quot;width: 32.6959%; margin-right: 10px;&quot; data-widthpercent=&quot;33.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPVFPr/btsNjCc4hXU/ItNLHkqQGPdqXpVkKNuwp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPVFPr%2FbtsNjCc4hXU%2FItNLHkqQGPdqXpVkKNuwp1%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;1084&quot; height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cu1P9g/btsNi6y1YUz/ZO07MIwQKb3M3vux9yCyT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cu1P9g/btsNi6y1YUz/ZO07MIwQKb3M3vux9yCyT1/img.png&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;782&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.9585%;&quot; data-widthpercent=&quot;33.75&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cu1P9g/btsNi6y1YUz/ZO07MIwQKb3M3vux9yCyT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcu1P9g%2FbtsNi6y1YUz%2FZO07MIwQKb3M3vux9yCyT1%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;1104&quot; height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;출처: https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 위 사진처럼 2코어 CPU를 가진 노드가 있다고 하자.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부하가 많이 발생하지 않는 평소에는 각 pod에서 각각 cpu 0.1 코어씩만 사용한다면, 이 노드에서 현재 사용 가능한 여유 코어는 1.8코어가 된다.&lt;/li&gt;
&lt;li&gt;만약, A Pod의 트래픽이 증가하게 된다면, cpu.limits 값인 1.5 코어까지 사용할 수 있게 된다. 그러면 이 노드의 사용 가능한 코어는 0.4 코어만 남게 된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이 상황에서 B Pod의 트래픽이 증가하게 되면 CPU 리소스는 어떻게 배분될까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cpu.requests는 해당 설정값만큼 CPU 리소스를 사용할 수 있도록 보장하는 것이기 때문에 B Pod는 최소 1.0 코어를 사용할 수 있어야 한다. 따라서, A Pod가 CPU 상한값에 맞춰 사용하던 리소스 중 0.5 코어를 반납해 B Pod가 사용할 수 잇도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이러한 CPU 리소스 분배 과정은 쿠버넽티스 내부에서 진행되며, 이때 리소스 할당은 CFS(Completely Fair Scheduler) 알고리즘 기반으로 작동된다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;b&gt;CFS(Completely Fair Scheduler): 리소스 스케줄링 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CFS는 이름처럼 &quot;공정하게 CPU를 나눠 쓰자&quot;라는 철학을 가진 리눅스의 기본 CPU 스케줄러다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;전통적인 라운드로빈이나 우선순위 기반이 아니라, 각 태스크에게 동일한 실행 기회를 제공하는 데 초점을 맞춘다.&lt;/li&gt;
&lt;li&gt;컨테이너에 cpu.limits를 설정하면 CFS는 두 가지 파라미터를 기반으로 CPU 리소스를 사용하도록 설정한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cfs_period_us: CPU 리소스를 할당하는 기준 시간.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값은 100ms이며, 일반적으로 이 값을 조정하는 경우는 흔치 않다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;100ms마다 cpu 리소스를 어떻게 사용할지 새로 스케줄링 한다는 뜻.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;cfs_quota_us: 기준 시간 동안 CPU를 사용할 수 있는 최대 시간.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예를 들어, cpu.limit: 0.5로 설정한다면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100ms 주기 동안 50ms만 CPU 사용 허용 (fs_quota_us = 50000)&lt;/li&gt;
&lt;li&gt;만약 이 걸 넘게되면 남은 시간 동안 CPU 사용이 스로틀링된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 쿠버네티스 자체에 CFS가 있는 건 아니고, 컨테이너의 CPU 사용량을 제한할 때 리눅스 CFS 기능을 활용하는 것!&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;b&gt;스로틀링 (Throttling)&lt;/b&gt;&lt;/h4&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;쿠버네티스에서 CFS 스로틀링이 발생하는 이유&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트래픽이 증가했는데 남아 있는 CPU 리소스가 부족해지는 경우, CPU를 사용하지 못하고 cfs_period_us 시간까지 대기하게 된다.&lt;/li&gt;
&lt;li&gt;여기서 cfs_period_us 시간까지 대기하는 상황, 즉 CPU를 사용하지 못하는 상황을 &quot;&lt;b&gt;CFS Throttling&lt;/b&gt;&quot;이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CPU 코어 수가 많아도 CFS 스로틀링이 발생할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CFS는 코어 수가 아니라 &quot;시간&quot; 기준으로 CPU 사용량을 제한한다.&lt;/li&gt;
&lt;/ul&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;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1EZyN/btsNkjxk0Dr/neC8tYz50xpvrqkrPnbTy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1EZyN/btsNkjxk0Dr/neC8tYz50xpvrqkrPnbTy0/img.png&quot; data-alt=&quot;출처: https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1EZyN/btsNkjxk0Dr/neC8tYz50xpvrqkrPnbTy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1EZyN%2FbtsNkjxk0Dr%2FneC8tYz50xpvrqkrPnbTy0%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;532&quot; height=&quot;312&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 사진처럼, 4코어 노드에서 A Pod가 병렬처리로 각 코어의 25ms씩을 사용하는 상황이 있다고 가정해 보자.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이러면 총 CPU 사용 시간은 100ms이 된다.&lt;/li&gt;
&lt;li&gt;만약 해당 pod의 cpu.limits이 1.0으로 설정되어 있다면, cfs_quota_us 설정 제한(100ms)에 걸리게 된다.&lt;/li&gt;
&lt;li&gt;이러면 남은 75ms에 해당하는 시간 동안 CFS 스로틀링이 발생하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이해하기 쉽게, 비유해보자면 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;당신이 편의점 사장이고 알바 4명을 고용했다고 해보자. 근데 본사에서 &quot;이번 주에 일 시킬 수 있는 총 시간은 100시간까지만 허용&quot;한다라고 했다.&lt;/li&gt;
&lt;li&gt;그러면 4명이 동시에 1시간 일하면 총 4시간을 일한 것이 된다. 즉, 4명이 동시에 25분 일했더니, 총 100분을 다 써서, 이번주는 더 이상 일을 시킬 수 없는 상태가 된다.&lt;/li&gt;
&lt;/ul&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;b&gt;CPU 상한 제거하기&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 자원을 최대한 활용하려면 CFS 스로틀링 발생을 최소화해야 하고, 이를 위해서는 cfs_quota_us로 인한 제한이 발생하지 않아야 하며, 그러려면 CPU 상한을 설정하지 않아야 한다.&lt;/li&gt;
&lt;li&gt;cpu.limits를 제거하면 생길 수 있는 부작용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;limits을 아예 안 주면 CFS는 리미트가 없는 상태로 판단해서, 해당 컨테이너가 CPU를 무한정 사용할 수 있다고 판단한다.&lt;/li&gt;
&lt;li&gt;이런 경우, 멀티 테넌시 환경에서는 noisy neighbor 문제가 생길 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;하나의 파드가 CPU를 독점해서 다른 파드가 CFS 스로틀링을 더 많이 겪게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;따라서, 단일 파드만 사용하는 테스트 환경이나 모놀리식 구조에서는 limits 제거가 유리하지만, 프로덕션 환경이나 멀티 파드/멀티 테넌시 구조에서는 적절한 limits 설정이 중요하다.&lt;/li&gt;
&lt;/ul&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;테넌시 (Tenancy)&lt;/b&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;Single Tenancy: 하나의 쿠버네티스 클러스터를 하나의 팀만 사용
&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;b&gt;각자 자기 공간처럼 분리되어&lt;/b&gt; 서비스를 사용하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Multi Tenancy: 하나의 쿠버네티스 클러스터를 여러 팀이 공유&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-14 오후 6.14.54.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1yySy/btsNk50JncU/30eJCH8nz00zuVAObWY4o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1yySy/btsNk50JncU/30eJCH8nz00zuVAObWY4o0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1yySy/btsNk50JncU/30eJCH8nz00zuVAObWY4o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1yySy%2FbtsNk50JncU%2F30eJCH8nz00zuVAObWY4o0%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;2102&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2025-04-14 오후 6.14.54.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;892&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 방법&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능 지표
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TPS(Transactions Per Second)와 최대 TPS, 평균 응답 시간(Average Response Time)을 선정&lt;/li&gt;
&lt;li&gt;부하를 발생시키면서 그라파나를 통해 지표 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;container_cpu_cfs_throttled_seconds_total: 해당 컨테이너가 CPU CFS에 따라 제한(스로틀링)된 총 시간을 의미.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 값이 증가할수록 스로틀링이 많이 발생한 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;container_cpu_usage_seconds_total: 컨테이너가 사용한 CPU 시간의 총합. 전체적인 CPU 사용량을 측정하는 지표.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/Kubernetes</category>
      <category>k8s</category>
      <category>Kubernetes</category>
      <category>Throttling</category>
      <category>리소스관리</category>
      <category>스로틀링</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/755</guid>
      <comments>https://hororolol.tistory.com/755#entry755comment</comments>
      <pubDate>Sat, 12 Apr 2025 23:51:55 +0900</pubDate>
    </item>
    <item>
      <title>[D2] Elasticsearch 기반 로그 모니터링 시스템의 한계와 Apache Iceberg 요약 및 배경지식 정리</title>
      <link>https://hororolol.tistory.com/754</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Naver D2 &quot;&lt;a href=&quot;https://d2.naver.com/helloworld/8998207&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NELO Alaska: 대용량 로그 데이터 저장을 위한 Apache Iceberg 도입기&lt;/a&gt;&quot; 요약 및 배경 지식들을 정리해 두었습니다.&lt;/p&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Kafka와 Elasticsearch&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 모니터링 시스템 구축에는 인덱스 기반의 빠른 검색을 제공하는 검색 엔진인 Elasticsearch가 널리 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1742170495112&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ 클라이언트 ] 
    │
    ▼
[ Kafka (로그 데이터 수집) ] &amp;rarr; 로그 데이터를 빠르게 수집하고 Elasticsearch로 전송
    │
    ▼
[ Elasticsearch ]
    │
    ├── [ Hot 계층 (SSD) ] &amp;rarr; 최신 데이터 3일간 저장
    │
    ▼
[ Warm 계층 (HDD) ] &amp;rarr; 최대 90일까지 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트로부터 수신된 로그 데이터는 Kafka에 적재된 후 Elasticsearch에 저장된다.&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;b&gt;Kafka&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 메시지 큐(Messafe Queue) 또는 이벤트 스트리밍 플랫폼&lt;/li&gt;
&lt;li&gt;대용량 데이터를 빠르게 수집하고 처리하는 역할을 한다.&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;주요 특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Publisher-Subscriber 모델&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 보내는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Producer(생산자)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 데이터를 소비하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Consumer(소비자)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구조&lt;/li&gt;
&lt;li&gt;Producer가 데이터를 특정&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Topic&lt;/b&gt;(토픽)에 전송하면, Consumer가 해당 토픽을 구독하여 데이터를 가져감&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고성능 &amp;amp; 대용량 처리&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내구성과 장애 허용성(Fault-Tolerance)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;브로커(Broker)&lt;/b&gt; 여러 개로 구성하여 데이터를 &lt;b&gt;복제(replication)&lt;/b&gt; 함으로써 장애에 강함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 아키텍처&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 여러 &lt;b&gt;파티션(Partition)&lt;/b&gt; 으로 나뉘어 저장되므로 &lt;b&gt;확장성이 뛰어남&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka가 하는 역할&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트(앱, 서버) &amp;rarr; &lt;b&gt;Kafka에 로그 데이터 저장&lt;/b&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;rarr; 이후 &lt;/span&gt;&lt;b&gt;Elasticsearch로 전달&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;728&quot; data-start=&quot;639&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;728&quot; data-start=&quot;639&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Hot/Warm 계층&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;Hot/Warm 계층(Hierarchy Storage, Tiered Storage) 개념은 Elasticsearch(ELK 스택)에서 데이터 저장 전략을 설명하는 공식 용어로 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;728&quot; data-start=&quot;639&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;728&quot; data-start=&quot;639&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Hot 계층(Hot Tier)&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 data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;빠른 검색과 실시간 분석이 필요한 데이터를 저장하는 계층&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;SSD를 사용하여 빠른 읽기/쓰기 성능을 제공&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;주로 최근 3일 이내의 최신 데이터가 저장됨&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;검색이 빈번하게 발생하는 데이터&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;Hot 계층의 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;639&quot;&gt;최신 로그 데이터 저장 &amp;rarr; &lt;b&gt;빠르게 검색&lt;/b&gt; &amp;rarr; 일정 기간 후 Warm 계층으로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;Warm 계층 (Warm Tier)&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;/li&gt;
&lt;li&gt;상대적으로 느린 HDD를 사용하여 비용 절감&lt;/li&gt;
&lt;li&gt;최대 90일까지 데이터 저장 가능&lt;/li&gt;
&lt;li&gt;Warm 계층의 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hot 계층에서 &lt;b&gt;3일 지난 데이터 이동&lt;/b&gt; &amp;rarr; 저장 공간 절약 &amp;amp; 비용 절감 &amp;rarr; 필요할 때만 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;비효율적인 Elasticsearch 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 법적 요구 사항 등의 이유로 예외적으로 1년 이상 장기간 로그 데이터를 저장하게 되다 보니 Warm 계층에 저장된 데이터의 크기가 급증하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자들의 검색 요청 로그를 분석한 결과 95% 쿼리가 당일에 발생한 데이터에 대한 것이었고, 99%의 쿼리가 일주일 이내의 데이터를 위한 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch는 일반적으로 데이터 저장과 쿼리 계산을 위한 컴퓨팅을 같은 노드에서 담당하고 있기 때문에 이렇게 거의 검색되지 않는 데이터를 저장하는 것은 효율적인 일이 아니다.&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;b&gt;문제 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하려면 Elasticsearch에는 검색이 자주 일어나는 단기간의 데이터만 저장하고, 장기간 데이터를 저장할 새로운 스토리지가 필요하다는 판단이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch를 대체하는 신규 스토리지에서는 데이터 저장을 위한 스토리지와 검색을 위한 컴퓨팅을 분리한다는 아이디어를 기본으로 설계를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Iceberg를 이용해 로그 데이터 저장을 위한 새로운 타입의 스토리지를 구현한 컴포넌트인 Alaska를 개발해 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Apache Iceberg&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 데이터 레이크에서 테이블 형식으로 데이터를 관리하는 오픈소스 테이블 포맷
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 레이크(Data Lake): 정형(structured), 반정형(semi-structured), 비정형(unstructured) 데이터를 원본 그대로 저장하는 대규모 저장소를 의미&lt;/li&gt;
&lt;li&gt;쉽게 말하면, 기존 db는 정형화된 테이블 구조에 맞춰 데이터를 저장해야 하지만, 데이터 레이크는 모든 형태의 데이터를 가공하지 않고 원본 그대로 저장할 수 있는 저장소이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ACID 트랜잭셔 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iceberg는 스냅샷을 기반으로 트랜잭션을 지원하여 데이터 정합성 유지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Schema Evolution
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블의 스키마를 중단 없이 변경 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hidden partitioning
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 파티셔닝을 통해 사용자가 신경 쓰지 않아도 최적의 데이터 쿼리 성능을 제공함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;신규 로그 모니터링 시스템의 구조&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iceberg를 기반으로 개발한 새로운 로그 모니터링 시스템은 기존 Elasticsearch 기반의 로그 모니터링 시스템을 대체하는 것이 아니다.&lt;/li&gt;
&lt;li&gt;Elasticsearch에는 실시간 모니터링이 필요한 짧은 기간의 로그를 저장하고, 장기간 보관이 필요한 데이터는 새로운 스토리지를 활성화해 저장하도록 설계했다.&lt;/li&gt;
&lt;li&gt;기존 로그 모니터링 시스템에서는 Kafka에 적재된 로그 데이터를 Elasticsearch에 인덱싱하는 방식을 사용했었다. 신규 로그 모니터링 시스템도 동일한 Kafka 토픽으로부터 데이터를 읽어 Iceberg 테이블 포맷으로 저장한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 검색/모니터링이 필요하지 않은 데이터는 Elasticsearch에 저장하지 않고 직접 Iceberg로 저장.&lt;/li&gt;
&lt;/ul&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;b&gt;데이터 프로세싱&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신규 로그 모니터링 시스템은 데이터 프로세싱을 위해서 Kappa Architecture를 따르고 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 실시간으로 저장되고 있는 로그 데이터 테이블에 사용자가 접근해 데이터를 조회할 수 있는 구조이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Iceberg의 오픈 테이블 포맷은 ACID 트랙잭션을 지원하기 때문에 실시간으로 읽고 쓰기 가능.&lt;/li&gt;
&lt;li&gt;이러한 구조로 짧은 지연 시간 안에 데이터 조회가 가능해짐.&lt;/li&gt;
&lt;/ul&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;h3 data-end=&quot;158&quot; data-start=&quot;124&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Kappa Architecture vs Lambda Architecture&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;304&quot; data-start=&quot;159&quot; data-ke-size=&quot;size16&quot;&gt;전통적인 &lt;b&gt;Lambda Architecture&lt;/b&gt;는 배치 처리(batch)와 스트림 처리(stream)를 &lt;b&gt;동시에 사용&lt;/b&gt;하는 구조입니다.&lt;br /&gt;하지만 &lt;b&gt;Kappa Architecture&lt;/b&gt;는 &lt;b&gt;배치 처리를 제거하고 스트림 처리만 사용&lt;/b&gt;하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 항목Lambda ArchitectureKappa Architecture&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;602&quot; data-start=&quot;306&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;602&quot; data-start=&quot;423&quot;&gt;
&lt;tr data-end=&quot;463&quot; data-start=&quot;423&quot;&gt;
&lt;td&gt;데이터 처리 방식&lt;/td&gt;
&lt;td&gt;배치 + 스트림 병행&lt;/td&gt;
&lt;td&gt;스트림 처리만 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;512&quot; data-start=&quot;464&quot;&gt;
&lt;td&gt;데이터 저장소&lt;/td&gt;
&lt;td&gt;배치용(보통 Hadoop) + 실시간용&lt;/td&gt;
&lt;td&gt;단일 스트림 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;553&quot; data-start=&quot;513&quot;&gt;
&lt;td&gt;유지보수&lt;/td&gt;
&lt;td&gt;복잡함 (이중 코드 필요)&lt;/td&gt;
&lt;td&gt;단순함 (하나의 코드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;602&quot; data-start=&quot;554&quot;&gt;
&lt;td&gt;데이터 정합성&lt;/td&gt;
&lt;td&gt;배치와 스트림 간 동기화 필요&lt;/td&gt;
&lt;td&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;&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;* 데이터 적재, 데이터 최적화, Iceberg REST 카탈로그와 데이터 연동, 데이터 쿼리, 적용 결과에 대한 내용은 실제 해당 글을 참고해 주세요.&lt;/p&gt;</description>
      <category>개발</category>
      <author>노는게제일좋아!</author>
      <guid isPermaLink="true">https://hororolol.tistory.com/754</guid>
      <comments>https://hororolol.tistory.com/754#entry754comment</comments>
      <pubDate>Mon, 24 Mar 2025 17:09:15 +0900</pubDate>
    </item>
  </channel>
</rss>