<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Engineering Note</title>
    <link>https://techbook11.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Jul 2026 18:47:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Software Engineer Kim</managingEditor>
    <item>
      <title>[JPA] fetch join을 사용하는 이유와 한계</title>
      <link>https://techbook11.tistory.com/853</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;fetch join을 사용하는 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch join은 JPA에서 지연로딩으로 설정되어 있는 연결된 엔티티를 지연로딩으로 조회하지 않고 한 번에 조회하기 위해 사용.&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;fetch join의 한계&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;size16&quot;&gt;왜 페이지네이션이 적용이 안되나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS는 두 개의 테이블을 조인할 때, Cartesian Product(카테시안 곱)이 일어나는데 이때 데이터 중복이 발생한다. 예를 들면, 하나의 게시글에 3개의 댓글이 달렸다고 할 때, Post(게시글 테이블)과 Comment(댓글 테이블)을 조인하면 3개의 Row가 생긴다.&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;Post 테이블&lt;/p&gt;
&lt;table style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; border-collapse: collapse; width: 100%; height: 36px;&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;width: 50%; height: 17px;&quot;&gt;post_id&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;title&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;JPA 공부중&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;Comment 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&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;width: 33.3333%; height: 17px;&quot;&gt;comment_id&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;post_id&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;재밌어요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;어렵네요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&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;두 개의 테이블을 조인한 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터 중복이 발생. post_id는 하나지만 게시글과 조인을 하면서 3개의 게시글이 존재하는 것처럼 결과가 나타났다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 51px;&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;width: 25%; height: 17px;&quot;&gt;post_id&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;comment_id&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;title&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;JPA 공부중&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;재밌어요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;JPA 공부중&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;어렵네요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;JPA 공부중&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&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;페이지네이션이 적용이 안된다는 의미는 논리적 오류가 발생한다는 의미다. 페이지네이션은 조인 후에 offset과 limit으로 데이터를 분류하는데 이때 요청한 결과와 다른 결과가 반환될 수 있다. 그리고 JPA는 객체로 데이터를 다루기 때문에 만약 하나의 게시글만 가져오기 위해 limit 1를 적용하면 DB SQL문에서 limit 1이 수행되지 않고 메모리에서 직접 페이지네이션을 적용하면서 성능이슈가 발생할 수 있다.&lt;/p&gt;</description>
      <category>Server/JPA ORM</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/853</guid>
      <comments>https://techbook11.tistory.com/853#entry853comment</comments>
      <pubDate>Wed, 8 Apr 2026 14:22:32 +0900</pubDate>
    </item>
    <item>
      <title>[분산 트랜잭션] 분산트랜잭션 처리 패턴</title>
      <link>https://techbook11.tistory.com/852</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;분산 트랜잭션 처리 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 여러 개읜 분산된 서비스를 하나의 일관된 트랜잭션으로 묶는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;- 2단계 커밋&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 원자성을 유지하기 위해 분산 데이터베이스 트랜잭션에 포함돼 있는 모든 노드가 커밋(commit)되거나 롤백(rollback)하는 메커니즘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 단점 : 각 서비스에 잠금(lock in)이 걸려 발생하는 성능 문제 탓에 효율적인 방법이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사가 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 일관성 유지가 필요한 트랜잭션을 모두 묶어 하나의 트랜잭션으로 처리하는 것이 아니라, 각 로컬 트랜잭션으로 분리해 순차적으로 처리하는 방법.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 롤백이 필요한 경우 보상 트랜잭션으로 데이터의 일관성을 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/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;cf) 원자성(Acidity) : 트랜잭션의 작업이 모든 작업을 완전히 수행하거나 완전히 수행하지 않은 상태로 유지되어야 함을 의미&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;참고 자료 : 도메인 주도 설계로 시작하는 마이크로 서비스 개발(p68)&lt;/p&gt;</description>
      <category>Server/분산트랜잭션</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/852</guid>
      <comments>https://techbook11.tistory.com/852#entry852comment</comments>
      <pubDate>Mon, 16 Feb 2026 00:43:28 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 고아객체</title>
      <link>https://techbook11.tistory.com/851</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;고아 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 부모 엔티티와 연관관계가 끊어진 자식 엔티티.&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;&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;size16&quot;&gt;또는 부모가 제거되면 자식이 제거되는 기능(부모를 제거하면 자식은 참조가 끊기기 때문. CascadeType.REMOVE 설정과 같은 역할을 함.)&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;gt; @OneToOne, @OneToMany에만 사용할 수 있다.&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;pre id=&quot;code_1770372875865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//예제 8.19 고아 객체 제거 기능 설정

@Entity
public class Parent {

 @Id @GeneratedValue
 private Long id;
 
 @OneToMany(mappedBy = &quot;parent&quot;, orphanRemoval = true)
 private List&amp;lt;Child&amp;gt; children = new ArrayList&amp;lt;Child&amp;gt;();

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계 제거를 통해 고아객체 제거 기능 수행&lt;/p&gt;
&lt;pre id=&quot;code_1770372935564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거


//실행된 SQL 결과
DELETE FROM CHILD WHERE ID=?&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;컬렉션에서 첫 번째 자식을 제거했고, orphanRemoval = true 옵션으로 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행된다.&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;pre id=&quot;code_1770373072161&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;parent1.getChildren().clear();&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;참고자료 : JPA 프로그래밍(김영한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Server/JPA ORM</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/851</guid>
      <comments>https://techbook11.tistory.com/851#entry851comment</comments>
      <pubDate>Fri, 6 Feb 2026 19:21:45 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 영속성 전이 CASCADE</title>
      <link>https://techbook11.tistory.com/850</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;8.4 영속성 전이 CASCADE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모의 엔티티를 저장할 때 자식 엔티티도 저장할 것인지 설정하게 해주는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;8.4.1 영속성 전이: 저장&lt;/b&gt;&lt;/h4&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;pre id=&quot;code_1770371302748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//예제 8.14 부모 엔티티

@Entity
public class Parent {

 @Id @GeneratedValue
 private Long id;
 
 @OneToMany(mappedBy = &quot;parent&quot;)
 private List&amp;lt;Child&amp;gt; children = new ArrayList&amp;lt;Child&amp;gt;();

}


//예제 8.15 자식 엔티티
@Entity
public class Child {
 @Id @GeneratedValue
 private Long id;
 
 @ManyToOne
 private Parent parent;
}&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;영속성 전이 설정을 하지 않고 Entity를 저장하는 예제&lt;/p&gt;
&lt;pre id=&quot;code_1770371444553&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static void saveNoCasecade(EntityManager em){
 //부모 저장
 Parent parent = new Parent();
 
 //1번 자식 저장
 Child child1 = new Child();
 child1.setParent(parent); //자식 -&amp;gt; 부모 연관관계 설정
 parent.getChildren(child1); //부모 -&amp;gt; 자식


//2번 자식 저장
Child child2 = new Child();
child2.setParent(parent); //자식 -&amp;gt; 부모 연관관계 설정
parent.getChildren(child2); //부모 -&amp;gt; 자식
em.persist(child2);
}&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;JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 따라서 예제를 보면 부모 엔티티와 자식 엔티티 모두 개별적으로 영속 상태로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;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;pre id=&quot;code_1770371683746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Parent {

 @Id @GeneratedValue
 private Long id;
 
 @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST)
 private List&amp;lt;Child&amp;gt; children = new ArrayList&amp;lt;Child&amp;gt;();

}

private static void saveWithCascade(EntityManager em) {

 Child child1 = new Child();
 Child child2 = new Child();
 
 Parent parent = new Parent();
 child1.setParent(parent);
 child2.setParent(parent);
 parent.getChildren().add(child1);
 parent.getChildren().add(child2);


//부모 저장, 연관된 자식들 함께 저장
em.persist(parent);
}&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;CasecadeType.PERSIST 옵션 덕분에 부모만 영속화하면, 자식 엔티티까지 함께 영속화해서 저장한다.&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-origin-width=&quot;1390&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2G5lE/dJMcac2156a/JE0MMJxwfEjgI3s1fyVPDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2G5lE/dJMcac2156a/JE0MMJxwfEjgI3s1fyVPDK/img.png&quot; data-alt=&quot;8.11 CASCADE 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2G5lE/dJMcac2156a/JE0MMJxwfEjgI3s1fyVPDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2G5lE%2FdJMcac2156a%2FJE0MMJxwfEjgI3s1fyVPDK%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;611&quot; height=&quot;436&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;8.11 CASCADE 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8.4.2 영속성 전이 : 삭제&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h4&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;pre id=&quot;code_1770372411885&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.REMOVE)
 private List&amp;lt;Child&amp;gt; children = new ArrayList&amp;lt;Child&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1770372395392&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);&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;만약 CasecadeType.REMOVE를 설정하지 않고,&amp;nbsp; em.remove(findParent);코드를 실행하면 부모 엔티티만 삭제된다. 하지만 데이터베이스의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스에서 외래키 무결성 예외가 발생한다.&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;참고자료 : JPA 프로그래밍(김영한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Server/JPA ORM</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/850</guid>
      <comments>https://techbook11.tistory.com/850#entry850comment</comments>
      <pubDate>Fri, 6 Feb 2026 19:09:55 +0900</pubDate>
    </item>
    <item>
      <title>[SW Engineering]동시성 제어를 위한 Lock 비교</title>
      <link>https://techbook11.tistory.com/849</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;**조회 시점에 실제로 데이터베이스 수준의 락을 거는 방식은 '비관적 락(Pessimistic Lock)'**입니다. 낙관적 락은 그 이름과 달리 실제로는 '락'을 걸지 않는다는 점이 가장 큰 차이점이에요.&lt;br /&gt;혼동하기 쉬운 두 개념을 명확하게 정리해 드릴게요.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;1. 비관적 락 (Pessimistic Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&quot;데이터 수정이 빈번할 거야. 충돌이 날 게 뻔하니 미리 문을 잠가두자!&quot;&lt;br /&gt;* 작동 방식: 데이터를 조회할 때 SELECT ... FOR UPDATE 같은 구문을 사용하여 데이터베이스 로우(Row)에 직접 락을 겁니다.&lt;br /&gt;* 특징: 다른 트랜잭션은 해당 락이 풀릴 때까지 대기해야 합니다.&lt;br /&gt;* 장점: 데이터 무결성을 강력하게 보장하며, 충돌이 잦은 환경에서 재시도 로직이 필요 없습니다.&lt;br /&gt;* 단점: 성능(동시성)이 저하될 수 있고, 자칫하면 데드락(Deadlock)에 빠질 위험이 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 낙관적 락 (Optimistic Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&quot;충돌이 자주 안 날 거야. 일단 그냥 두고, 나중에 저장할 때 확인만 하자!&quot;&lt;br /&gt;* 작동 방식: 조회 시점에 락을 걸지 않습니다. 대신 버전(Version) 번호나 타임스탬프를 활용합니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp; * 데이터를 읽을 때 버전 번호(예: 1)도 함께 읽습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp; * 수정 후 업데이트할 때 WHERE version = 1 조건을 붙여 쿼리를 날립니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp; * 그사이 다른 사용자가 업데이트해서 버전이 2가 되었다면, 내 쿼리는 실패하게 됩니다.&lt;br /&gt;* 특징: DB 레벨의 락이 아니라 애플리케이션 레벨에서 처리하는 논리적 격리 방식입니다.&lt;br /&gt;* 장점: 실제로 락을 잡지 않으므로 비관적 락보다 성능상 이점이 큽니다.&lt;br /&gt;* 단점: 충돌이 발생하면 개발자가 직접 예외 처리를 하거나 재시도 로직을 구현해야 합니다.&lt;br /&gt;핵심 비교 요약&lt;br /&gt;| 구분 | 비관적 락 (Pessimistic) | 낙관적 락 (Optimistic) |&lt;br /&gt;|---|---|---|&lt;br /&gt;| 락 시점 | 데이터 조회 시 즉시 | 업데이트(Commit) 시점에 확인 |&lt;br /&gt;| 자원 점유 | 트랜잭션 동안 점유 (DB 락) | 점유하지 않음 |&lt;br /&gt;| 충돌 대응 | 발생 전 예방 (대기 발생) | 발생 후 처리 (예외 발생) |&lt;br /&gt;| 적합한 상황 | 충돌이 잦고 데이터 정합성이 최우선일 때 | 충돌이 적고 성능이 중요할 때 |&lt;br /&gt;따라서 질문하신 **&quot;조회 시점에 락을 걸어 충돌을 방지하는 것&quot;**은 비관적 락의 설명이며, 낙관적 락은 조회 시점에는 누구나 접근 가능하게 열어두되 수정 시점에 버전 일치 여부를 따지는 방식이라고 이해하시면 완벽합니다.&lt;br /&gt;혹시 지금 개발 중인 서비스에서 어떤 락을 적용할지 고민 중이신가요? 상황을 말씀해 주시면 적절한 방식을 추천해 드릴 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;선착순 주문 이벤트라면 재고 정합성이 생명이죠! 수량이 한정되어 있고 짧은 시간에 트래픽이 몰리기 때문에, 락의 선택이 서비스의 생사(?)를 결정할 수도 있습니다.&lt;br /&gt;앞서 살펴본 두 가지 방식 중 선착순 이벤트에는 보통 어떤 것이 유리할까요?&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;1. 선착순 이벤트에서 '낙관적 락'의 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;낙관적 락은 충돌이 발생하면 애플리케이션에서 예외를 던지고 재시도를 해야 합니다.&lt;br /&gt;* 상황: 재고가 100개인데 1,000명이 동시에 주문 버튼을 누름.&lt;br /&gt;* 결과: 1명만 성공하고 나머지 999명은 버전이 맞지 않아 오류가 발생합니다. 이들이 다시 재시도를 하면 또 충돌이 발생하죠.&lt;br /&gt;* 결론: 트래픽이 몰리는 선착순 이벤트에서는 DB 부하만 가중시키고 사용자 경험은 최악이 될 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 선착순 이벤트에서 '비관적 락'의 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;비관적 락은 순례자가 줄을 서듯 데이터베이스 로우에 락을 획득할 때까지 대기시킵니다.&lt;br /&gt;* 장점: 재고 수량이 딱 떨어지는 것을 보장하기 가장 쉽습니다.&lt;br /&gt;* 단점: 대기열이 길어지면 DB 커넥션이 부족해져 시스템 전체가 느려질 수 있습니다.&lt;br /&gt;  실무에서 권장하는 '선착순' 구현 전략&lt;br /&gt;사실 데이터베이스 락만으로는 수만 명의 동시 접속을 버티기 어렵습니다. 그래서 실무에서는 보통 다음과 같은 단계로 발전시킵니다.&lt;br /&gt;* Level 1: 비관적 락 (Pessimistic Lock)&lt;br /&gt;&amp;nbsp;&amp;nbsp; * 트래픽이 아주 많지 않을 때 가장 확실하고 단순한 방법입니다. SELECT ... FOR UPDATE로 재고를 하나씩 차감합니다.&lt;br /&gt;* Level 2: Redis를 활용한 분산 락 (Distributed Lock)&lt;br /&gt;&amp;nbsp;&amp;nbsp; * DB까지 요청이 가기 전에 Redis 같은 고성능 저장소에서 락을 획득하게 합니다. DB 부하를 획기적으로 줄일 수 있습니다. (Redisson 라이브러리 등이 주로 사용됩니다.)&lt;br /&gt;* Level 3: Redis 원자적 연산 (Incr/Decr) + 메시지 큐&lt;br /&gt;&amp;nbsp;&amp;nbsp; * 락 자체를 걸지 않고 Redis의 decr 명령어로 재고를 먼저 깎은 뒤, 성공한 요청만 Kafka나 RabbitMQ에 담아 비동기로 DB에 저장합니다. 가장 고성능을 내는 방식입니다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비관적락 분산락 낙관적락 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;비관적락과 Redis의 분산락은 기본적으로 대기열 큐를 사용하여 동작합니다. 한 번에 하나의 트랜잭션(쓰레드)만 데이터를 읽을 수 있는데, 큐에 대기중인 트랜잭션들은 락이 해제되면 차례대로 데이터를 조회하고 쓸 수 있습니다. 반면 낙관적락은 큐잉 모델을 따르지 않고 실질적으로 락도 사용하지 않는다. 동시 요청이 오게 되면 여러 트랜잭션들이 자유롭게 데이터를 읽어가고, 애플리케이션 로직을 거친후 쓰기시점에 처음 읽어간 버전과 다르면, 재시도 로직을 구현하거나 실패처리를 통해 동시성 문제를 해결합니다.&lt;br /&gt;선착순 이벤트에서 데이터 정합성을 위해 재시도 로직이 추가 되는 낙관적락은 일반적인 동시성 문제 해결에서 낙관적락을 사용하는 때와 달리 성능이 저하 될 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;최초 구현 방법&lt;br /&gt;낙관적락 비관적락 비교&lt;br /&gt;&lt;br /&gt;재고 정합성이 깨질 경우 비즈니스 손실이 크다고 판단했습니다. 낙관적 락은 충돌 시 재시도 비용이 발생해 초고밀도 트래픽에선 성능 저하가 우려되었고, 따라서 데이터 수준에서 원자성을 보장하는 비관적 락을 선택해 안정성을 확보했습니다&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;최종 &lt;br /&gt;비관적락에서 Redis 분산락으로 마이그레이션&lt;br /&gt;&lt;br /&gt;단일 DB 락은 커넥션 점유 문제로 인해 전체 시스템 병목이 될 수 있다는 점을 경계했습니다. 시스템의 확장성(Scalability)을 고려했을 때, 애플리케이션 외부에서 락을 관리하는 분산 락 방식이 DB 부하를 줄이면서도 동시성을 제어할 수 있는 최적의 아키텍처라고 판단했습니다.&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 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;동시성 제어를 위해 여러가지 락의 트레이드 오프를 고려하며 알게된 점&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;낙관적 락이 일반적으로 성능상 유리해서 선택한다고 하지만, 선착순 이벤트처럼 특별한 비즈니스 상황에서는 오히려 성능이 저하될 수 있습니다. 사실상 낙관적 락이 성능 상 유리한 조건은 동시에 트래픽이 몰리지 않는 상황에서만 성능이 유리하다. 이유는 실질적으로 락을 걸지 않기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 선착순 이벤트 상황에서는 충돌시 실패처리를 하면 사용자에게 다시 시도하라는 메세지를 보내고 실제 사용자가 다시 시도해야하는 상황으로 UX가 저하 될 수 있고, 시스템상에서 재시도로직을 추가하면 오히려 CPU 점유시간이 길어지고, DB에 다시 SELECT 쿼리를 날려야 하므로 커넥션이 낭비되고, 불필요한 네트워크 통신으로 인한 네트워크 부하가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 CS 관점에서 트레이드 오프를 고려하고 적절한 기술을 선택할 수 있는 능력이 Software Engineer에게 필요한 능력이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SW Engineering/선착순 이벤트</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/849</guid>
      <comments>https://techbook11.tistory.com/849#entry849comment</comments>
      <pubDate>Wed, 4 Feb 2026 17:16:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] AOP</title>
      <link>https://techbook11.tistory.com/848</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP(Aspect-Oriented Promgramming)&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;size16&quot;&gt;AOP가 사용되는 예&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-Invocation 이슈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 클래스 내부에서 메서드가 자기 자신의 다른 메서드를 호출하면서 AOP가 적용되지 않는 문제&lt;/p&gt;</description>
      <category>Server</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/848</guid>
      <comments>https://techbook11.tistory.com/848#entry848comment</comments>
      <pubDate>Tue, 27 Jan 2026 20:14:52 +0900</pubDate>
    </item>
    <item>
      <title>[Computer Architecture] 캐시와 버퍼의 차이</title>
      <link>https://techbook11.tistory.com/846</link>
      <description>&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;h4 data-ke-size=&quot;size20&quot;&gt;1. 캐시(Cache)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터에 더 빠르게 접근하기 위해 사용하는 임시 저장소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적&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;/li&gt;
&lt;li&gt;한 번 읽은 데이터나 자주 쓰일 것 같은 데이터를 미리 복사해둡니다.&lt;/li&gt;
&lt;li&gt;예시 : 브라우저 웹 캐시, CPU 캐시(L1, L2, L3)&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; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. 버퍼(Buffer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 장치 사이에서 데이터를 주고 받을 때, 전송 속도의 차이를 완충하기 위해 사용하는 임시 공간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적&lt;/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;b data-index-in-node=&quot;4&quot; data-path-to-node=&quot;1&quot;&gt;비동기적(Asynchronous) 처리&lt;/b&gt;를 가능하게 만드는 일종의 &lt;b data-index-in-node=&quot;40&quot; data-path-to-node=&quot;1&quot;&gt;'포스트잇'이나 '창고'&lt;/b&gt; 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;원리 : 데이터를 모았다가 한꺼번에 전송하거나 전송 속도에 맞춰 조금씩 꺼내 쓰는 방식&lt;/li&gt;
&lt;li&gt;예시 : 동영상 스트리밍 버퍼(네트워크 속도가 느려도 동영상이 끊기지 않게 미리 데이터를 모음), 메세지 큐, 프린터 스풀링&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Computer Science/Computer Architecture</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/846</guid>
      <comments>https://techbook11.tistory.com/846#entry846comment</comments>
      <pubDate>Sun, 25 Jan 2026 11:27:04 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 동시성 제어테스트를 위한 멀티쓰레드 코드 흐름 파악하기</title>
      <link>https://techbook11.tistory.com/845</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;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private int 주문_동시요청(int 요청개수) throws InterruptedException {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    CountDownLatch latch = new CountDownLatch(요청개수);
    AtomicInteger exceptionCount = new AtomicInteger(0);

    for (int i = 0; i &amp;lt; 요청개수; i++) {
        executor.submit(() -&amp;gt; {
            try {
                orderService.placeOrder(userId, productId, 1);
            } catch (OutOfStockException e) {
                e.printStackTrace();
                exceptionCount.incrementAndGet(); // 재고 부족 예외 카운트
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();
    executor.shutdown();
    return exceptionCount.get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;Main&amp;nbsp;Thread&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;submit()&amp;nbsp;&amp;rarr;&amp;nbsp;Worker1&amp;nbsp;생성&amp;nbsp;(작업&amp;nbsp;던짐)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;submit()&amp;nbsp;&amp;rarr;&amp;nbsp;Worker2&amp;nbsp;생성&amp;nbsp;(작업&amp;nbsp;던짐)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;submit()&amp;nbsp;&amp;rarr;&amp;nbsp;Worker3&amp;nbsp;생성&amp;nbsp;(작업&amp;nbsp;던짐)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;submit()&amp;nbsp;&amp;rarr;&amp;nbsp;Worker4&amp;nbsp;생성&amp;nbsp;(작업&amp;nbsp;던짐)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;submit()&amp;nbsp;&amp;rarr;&amp;nbsp;Worker5&amp;nbsp;생성&amp;nbsp;(작업&amp;nbsp;던짐)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;latch.await()&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;여기서&amp;nbsp;멈춤&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;Worker들이&amp;nbsp;각자&amp;nbsp;placeOrder()&amp;nbsp;실행&amp;nbsp;중...&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;완료될&amp;nbsp;때마다&amp;nbsp;countDown()&amp;nbsp;호출&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─►&amp;nbsp;카운터&amp;nbsp;0&amp;nbsp;되면&amp;nbsp;해제&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─►&amp;nbsp;shutdown()&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─►&amp;nbsp;return&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 클래스 요약&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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&gt;클래스&lt;/td&gt;
&lt;td&gt;패키지&lt;/td&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecutorService&lt;/td&gt;
&lt;td&gt;java.util.concurrent&lt;/td&gt;
&lt;td&gt;스레드 풀 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CountDownLatch&lt;/td&gt;
&lt;td&gt;java.util.concurrent&lt;/td&gt;
&lt;td&gt;동기화 (완료 대기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AtomicInteger&lt;/td&gt;
&lt;td&gt;java.util.concurrent.atomic&lt;/td&gt;
&lt;td&gt;Thread-safe 카운터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>Programming Language/Java</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/845</guid>
      <comments>https://techbook11.tistory.com/845#entry845comment</comments>
      <pubDate>Sat, 24 Jan 2026 23:11:52 +0900</pubDate>
    </item>
    <item>
      <title>[Database] MySQL 격리수준과 격리성 문제 용어정리</title>
      <link>https://techbook11.tistory.com/844</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;/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&gt;용어&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirty Read&lt;/td&gt;
&lt;td&gt;어떤 트랜잭션에서 처리한 작업이 완료(commit)되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-Repeatble Read&lt;/td&gt;
&lt;td&gt;Repeatable Read가 불가능한 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phantom Read&lt;/td&gt;
&lt;td&gt;트랜잭션 내부에서 같은 쿼리를 두 번 실행할 때 첫 번째 실행시에 없던(phantom)레코드가 조회되는 현상.&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;격리수준&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 70px;&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%; height: 19px;&quot;&gt;용어&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Dirty Read가 허용되는 격리수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;READ COMMITTED&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Dirty Read가 발생하지 않고, 어떤 트랜잭션에서 변경한 내용이 커밋되기 전까지는 다른 트랜잭션에서 조회할 수 없는 격리 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;REAPEATABLE READ&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;MVCC(Multi Version Concurrency Control)를 통해 언두 영역에서 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서 동일한 결과를 보장하는 격리 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;SELIAIZABLE&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&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;참고자료 : Real MySQL(백은빈, 이성욱)&lt;/p&gt;</description>
      <category>Computer Science/Database</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/844</guid>
      <comments>https://techbook11.tistory.com/844#entry844comment</comments>
      <pubDate>Sat, 24 Jan 2026 17:38:33 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] N+1문제가 발생하는 예(주문내역 조회)</title>
      <link>https://techbook11.tistory.com/843</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;주문내력 조회 기능 구현 과정 중 발생한 N+1문제 해결과정을 정리한 글입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;문제 정의&amp;nbsp;&lt;br&gt;JPA 사용시 연관관계가 복잡한 Entity N+1문제&lt;br&gt;네트워크 오버헤드 및, 과도한 데이터베이스 쿼리로 인한 데이터베이스 부하 증가&lt;br&gt;페이지네이션 문제, 다수의 데이터 조회시 필요한 페이징처리에서 일대다 조인으로 발생하는 Cartesian Product 문제.&lt;br&gt;Order 기준 페이징 처리와 OrderItem 기준 페이징 처리 방식이 사용자에게 보여지는 데이터의 신뢰성 문제가 발생&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;주문 내역 조회는 여러 정보를 한 번에 보여주어야 하는 기능으로 Order Entity, Item Entity, OrderItem Entity, ItemImg Entity의 정보를 조합해서 응답해주어야 합니다. 이 과정에서 발생한 N+1문제를 해결한 과정입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;N+1 문제가 발생하면 &lt;b&gt;데이터베이스 성능이 심각하게 저하&lt;/b&gt;됩니다. 구체적인 이유는 다음과 같습니다.&lt;br&gt;&lt;b&gt;1. 과도한 데이터베이스 쿼리&lt;/b&gt; 연관된 Entity에 대한 추가쿼리가 발생합니다. 예를 들어,&amp;nbsp;&lt;br&gt;&lt;b&gt;2. 네트워크 오버헤드&lt;/b&gt; 데이터베이스와 애플리케이션 서버 사이의 네트워크 통신이 반복적으로 일어나면서 각 쿼리마다 네트워크 지연(latency)이 누적됩니다. 쿼리 실행 시간 자체보다 이 왕복 시간이 더 큰 병목이 될 수 있습니다.&lt;br&gt;&lt;b&gt;3. 응답 속도 저하&lt;/b&gt; 사용자 입장에서는 페이지 로딩이 느려지고, API 응답 시간이 길어집니다. 데이터가 많아질수록 이 문제는 기하급수적으로 악화됩니다.&lt;br&gt;&lt;b&gt;4. 데이터베이스 부하 증가&lt;/b&gt; 동시 접속자가 많은 경우, 모든 사용자가 이런 비효율적인 쿼리를 발생시키면 데이터베이스 서버에 과부하가 걸려 전체 시스템이 느려지거나 다운될 수 있습니다.&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;N+1 문제 해결을 위해 고민 과정&lt;br&gt;프록시로 초기화된 데이터를 실제 데이터로 세팅하기 위한 문제를 해결하기 위해 모든 데이터를 한 번에 Join하는 방식을 고려.&lt;br&gt;QueryProjection 방식과 Entity를 활용라는 방식이 있는데 현재 기능에서만 사용하는 재사용성이 떨어지는 단점이 있어서 채택하지 않았습니다.&lt;br&gt;fetch join 으로 주문 내역 조회에 필요한 데이터를 지연로딩이 아니라 즉시로딩처럼 연관된 데이터는 한 번에 조회하는 기능입니다. &lt;br&gt;하지만 일대다 컬렉션 fetch join은 JPA에서 페이지네이션을 메모리에서 수행하는 문제가 있습니다. 극단적으로 Out Of Memory 문제가 발생할 수 있습니다. 또, 데이터의 신뢰성이 떨어질 수 있습니다. 일대다 조인 테이블을 페이징처리를 하면 ‘다’를 기준으로 페이징이 수행되기 때문에 원하는 조건으로 데이터를 페이지별로 구분할 수 없습니다. &lt;br&gt;마지지막으로 일대다 컬렉션이 2개 이상 섞인 엔티티는 fetch join을 수행할 수 없습니다.&lt;br&gt;최종 결정은 Order Entity만 페이지네이션을 적용한 후 조회한 후 나머지 엔티티는 batch size로 조건에 맞는 데이터를 where in 절에 포함시켜 한 번에 조회하도록 했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;구체적인 문제 분석과정&lt;br&gt;&amp;nbsp;&lt;br&gt;주문 내역 기능을 개발하고 로깅 설정과 테스트를 통해 개발한 코드의 쿼리가 예상보다 많이 실행되는 문제를 발견하였습니다.&amp;nbsp;&lt;br&gt;N+1문제 발생 코드&lt;/p&gt;&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt; 
 &lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public Page&amp;lt;OrderHistDto&amp;gt; getOrderList(String email, Pageable pageable) {
    log.info(&quot;===== 주문 목록 조회 시작 =====&quot;);

    log.info(&quot;1. findOrdersByEmail 호출&quot;);
    List&amp;lt;Order&amp;gt; orders = orderRepository.findOrdersByEmail(email, pageable);
    log.info(&quot;2. countOrdersByEmail 호출&quot;);
    Long totalCount = orderRepository.countOrdersByEmail(email);
    List&amp;lt;OrderHistDto&amp;gt; orderHistDtoList = new ArrayList&amp;lt;&amp;gt;();

    for(Order order : orders){
        log.info(&quot;주문별 상품 목록 조회&quot;);
        OrderHistDto orderHistDto = new OrderHistDto(order);
        log.info(&quot;3. getOrderItems 호출&quot;);
        List&amp;lt;OrderItem&amp;gt; orderItems = order.getOrderItems();
        for(OrderItem orderItem : orderItems){
            log.info(&quot;4.findByItemIdAndRepImgYn 호출&quot;);
            ItemImg itemImg = itemImgRepository.findByItemIdAndRepImgYn(orderItem.getItem().getId(), &quot;Y&quot;);
            OrderItemDto orderItemDto = new OrderItemDto(orderItem, itemImg.getImgUrl());
            orderHistDto.addOrderItemDto(orderItemDto);
        }
        orderHistDtoList.add(orderHistDto);
    }

    return new PageImpl&amp;lt;&amp;gt;(orderHistDtoList, pageable, totalCount);
}
&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;테스트 환경&lt;/p&gt;&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt; 
 &lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;주문 목록 조회 테스트&quot;)
void getOrderListTest() {
    // given
    Member member = saveMember();

    Item item1 = saveItem();
    Item item2 = saveItem();
    Item item3 = saveItem();

    createItemImg(item1, &quot;/images/item1.jpg&quot;);
    createItemImg(item2, &quot;/images/item2.jpg&quot;);
    createItemImg(item3, &quot;/images/item3.jpg&quot;);

    createOrder(member, item1, 2);
    createOrder(member, item2, 1);
    createOrder(member, item3, 3);

    em.flush();
    em.clear();&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;3개의 상품을 생성하고, 상품별 이미지를 연결하는 코드를 만들고 Member가 주문하는 코드를 작성했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;실행 쿼리 분석&lt;br&gt;사용자를 기준으로 Order Entity를 조회하는 메서드, 'findOrdersByEmail(email, peageable); 에서 1개의 쿼리가 수행되었습니다.&lt;/p&gt;&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt; 
 &lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;List&amp;lt;Order&amp;gt; orders = orderRepository.findOrdersByEmail(email, pageable);&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;select&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.order_id,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.created_by,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.member_id,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.modified_by,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.order_date,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.order_status,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.reg_time,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.update_time&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orders&amp;nbsp;o1_0&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;join&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;member&amp;nbsp;m1_0&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;on&amp;nbsp;m1_0.member_id=o1_0.member_id&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;where&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;m1_0.email=?&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;order&amp;nbsp;by&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o1_0.order_date&amp;nbsp;desc&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;offset&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;?&amp;nbsp;rows&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fetch&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;first&amp;nbsp;?&amp;nbsp;rows&amp;nbsp;only&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이후에 반복문을 살펴보면, orderId가 '3'인 OrderItem 목록을 조회하기 위해 또 하나의 쿼리가 수행되었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;select&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.order_id,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.order_item_id,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.created_by,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.item_id,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.modified_by,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.order_price,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.quantity,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.reg_time,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.update_time&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;order_item&amp;nbsp;oi1_0&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;where&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oi1_0.order_id=?&lt;br&gt;&amp;nbsp;&lt;br&gt;이렇게 조회한 OrderItem 리스트에서 대표 이미지를 조회하기 위해&lt;br&gt;&amp;nbsp;&lt;br&gt;최종 해결&lt;br&gt;Order Entity에 대해서만 offset, limit으로 페이지네이션을 적용하고 orderItem과 item, itemImg는 Batch Size로 조건절에 맞는 데이터만 조회할 수 있게 해결&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Server/JPA ORM</category>
      <author>Software Engineer Kim</author>
      <guid isPermaLink="true">https://techbook11.tistory.com/843</guid>
      <comments>https://techbook11.tistory.com/843#entry843comment</comments>
      <pubDate>Fri, 23 Jan 2026 16:52:57 +0900</pubDate>
    </item>
  </channel>
</rss>