<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Typhoon</title><description>Global Engineer @ Metsakuur</description><link>https://typhoon.is-a.dev/</link><language>en</language><item><title>Hello World</title><link>https://typhoon.is-a.dev/posts/en/hello-world/</link><guid isPermaLink="true">https://typhoon.is-a.dev/posts/en/hello-world/</guid><description>First post. What this blog is and what I&apos;ll write about.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I work as a Global Engineer at Metsakuur, a Korean security company specializing in facial recognition. My role covers backend, infrastructure, and ML — whatever the situation calls for.&lt;/p&gt;
&lt;h2&gt;Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine Learning&lt;/td&gt;
&lt;td&gt;PyTorch, TensorFlow, YOLO, Paddle OCR, Tesseract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Spring Boot, Go, FastAPI, Express, Ruby on Rails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Svelte (preferred), React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;Python, Go, Java, TypeScript, Rust, Dart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;On-premise, Proxmox, Ubuntu, Arch Linux, Docker, LXC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;What I&apos;ll write about&lt;/h2&gt;
&lt;p&gt;Posts come from problems I&apos;ve actually run into.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Software Engineering&lt;/strong&gt; — design decisions, tradeoffs, debugging logs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ML / AI&lt;/strong&gt; — practical Vision AI work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — homeserver, on-premise, Linux&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global engineering&lt;/strong&gt; — multilingual systems, working with international clients&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I write in both Korean and English.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/typhoon1217&quot;&gt;GitHub&lt;/a&gt; · &lt;a href=&quot;mailto:jungwooshin.97@gmail.com&quot;&gt;jungwooshin.97@gmail.com&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Hello World — 블로그를 시작하며</title><link>https://typhoon.is-a.dev/posts/ko/hello-world/</link><guid isPermaLink="true">https://typhoon.is-a.dev/posts/ko/hello-world/</guid><description>첫 번째 글 — 나는 누구인지, 이 블로그에서 무엇을 쓸 것인지에 대해.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Metsakuur에서 글로벌 엔지니어로 일하고 있다.&lt;/p&gt;
&lt;p&gt;Metsakuur는 얼굴인식 보안 솔루션을 다루는 한국 소재 회사다. 국내 업무와 해외 고객사 요구사항에 맞는 시스템을 만들고, 사내 개발에서 글로벌 표준 준수도 담당한다. 특정 포지션에 묶이지 않고 백엔드, 인프라, ML까지 상황에 따라 커버한다.&lt;/p&gt;
&lt;h2&gt;Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분야&lt;/th&gt;
&lt;th&gt;기술&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine Learning&lt;/td&gt;
&lt;td&gt;PyTorch, TensorFlow, YOLO, Paddle OCR, Tesseract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Spring Boot, Go, FastAPI, Express, Ruby on Rails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Svelte (선호), React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;Python, Go, Java, TypeScript, Rust, Dart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;On-premise, Proxmox, Ubuntu, Arch Linux, Docker, LXC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;이 블로그에서 다룰 것들&lt;/h2&gt;
&lt;p&gt;주로 실제로 부딪힌 문제들을 쓴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Software Engineering&lt;/strong&gt; — 설계 결정, 트레이드오프, 삽질 기록&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ML / AI&lt;/strong&gt; — 실무 Vision AI 적용 경험&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — 홈서버, 온프레미스, 리눅스&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global engineering&lt;/strong&gt; — 다국어 환경, 해외 고객사 협업&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;포스트는 한국어와 영어 둘 다 올릴 예정이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/typhoon1217&quot;&gt;GitHub&lt;/a&gt; · &lt;a href=&quot;mailto:jungwooshin.97@gmail.com&quot;&gt;jungwooshin.97@gmail.com&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Proxmox에서 LXC로 홈서버 서비스 분리하기</title><link>https://typhoon.is-a.dev/posts/ko/proxmox-lxc-service-isolation/</link><guid isPermaLink="true">https://typhoon.is-a.dev/posts/ko/proxmox-lxc-service-isolation/</guid><description>Proxmox 홈서버에서 VM 대신 LXC 컨테이너로 서비스를 격리하는 방법과 그 이유.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;서비스를 하나씩 VM으로 돌리다 보면 메모리가 금방 바닥난다. 서비스 5개에 VM 5개, RAM은 32GB인데 여유가 없다. LXC로 바꾸고 나서 같은 서버에 서비스를 두 배 이상 올렸다.&lt;/p&gt;
&lt;h2&gt;VM 대신 LXC를 쓰는 이유&lt;/h2&gt;
&lt;p&gt;VM은 게스트 OS 전체를 띄운다. 커널도 따로, 메모리도 따로. Ubuntu VM 하나에 최소 512MB는 잡아야 한다.&lt;/p&gt;
&lt;p&gt;LXC는 호스트 커널을 공유한다. 컨테이너 안에서는 독립된 환경처럼 보이지만 실제로는 호스트의 커널 위에서 돌아간다. RAM 사용량이 훨씬 적다. Nginx만 돌리는 컨테이너라면 50-100MB면 충분하다.&lt;/p&gt;
&lt;p&gt;격리 수준은 VM보다 낮다. 커널을 공유하기 때문에 커널 취약점은 컨테이너를 통해 호스트에 영향을 줄 수 있다. 홈서버에서 내부 서비스만 돌린다면 이 정도 트레이드오프는 감수할 수 있다.&lt;/p&gt;
&lt;h2&gt;Proxmox에서 LXC 만들기&lt;/h2&gt;
&lt;p&gt;먼저 컨테이너 템플릿을 받는다. Proxmox 웹 UI에서 &lt;strong&gt;Datacenter → node → local → CT Templates&lt;/strong&gt;에서 다운로드하거나, 터미널에서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pveam update
pveam available | grep ubuntu
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컨테이너 생성은 웹 UI 우상단 &lt;strong&gt;Create CT&lt;/strong&gt; 버튼. 설정할 것들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CT ID&lt;/strong&gt;: 100번대부터. VM ID와 겹치지 않게.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hostname&lt;/strong&gt;: 서비스 이름으로. &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;gitea&lt;/code&gt;, &lt;code&gt;grafana&lt;/code&gt; 식으로.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template&lt;/strong&gt;: 방금 받은 걸로.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disk&lt;/strong&gt;: 서비스에 따라 8-20GB면 대부분 충분하다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU / Memory&lt;/strong&gt;: 처음엔 넉넉하게 주고 모니터링 보면서 줄인다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;/strong&gt;: &lt;code&gt;vmbr0&lt;/code&gt; 브릿지에 연결하고 고정 IP 할당.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;네트워크 설정&lt;/h2&gt;
&lt;p&gt;컨테이너마다 고정 IP를 주는 게 관리하기 편하다. Ubuntu 컨테이너라면 Netplan으로:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/netplan/10-lxc.yaml
network:
  version: 2
  ethernets:
    eth0:
      addresses:
        - 192.168.1.110/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [1.1.1.1, 8.8.8.8]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서비스별로 IP 대역을 정해두면 나중에 보기 편하다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;192.168.1.100-109&lt;/code&gt; — 인프라 (Nginx, DNS)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.1.110-119&lt;/code&gt; — 모니터링 (Grafana, Prometheus)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.1.120-129&lt;/code&gt; — 앱 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;호스트 디렉토리 마운트&lt;/h2&gt;
&lt;p&gt;데이터는 컨테이너 밖에 두는 게 낫다. 컨테이너를 날려도 데이터는 남는다.&lt;/p&gt;
&lt;p&gt;웹 UI → 컨테이너 → Resources → Add → Mount Point로 bind mount 추가하거나, &lt;code&gt;/etc/pve/lxc/[CTID].conf&lt;/code&gt;에 직접:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mp0: /data/gitea,mp=/opt/gitea/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컨테이너 재시작하면 적용된다. 호스트의 &lt;code&gt;/data/gitea&lt;/code&gt;가 컨테이너 안 &lt;code&gt;/opt/gitea/data&lt;/code&gt;로 연결된다.&lt;/p&gt;
&lt;h2&gt;실제 구성&lt;/h2&gt;
&lt;p&gt;현재 홈서버에서 돌리는 구성:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CT ID&lt;/th&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td&gt;128MB&lt;/td&gt;
&lt;td&gt;리버스 프록시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;512MB&lt;/td&gt;
&lt;td&gt;공용 DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;grafana&lt;/td&gt;
&lt;td&gt;256MB&lt;/td&gt;
&lt;td&gt;모니터링 대시보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;prometheus&lt;/td&gt;
&lt;td&gt;256MB&lt;/td&gt;
&lt;td&gt;메트릭 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;gitea&lt;/td&gt;
&lt;td&gt;256MB&lt;/td&gt;
&lt;td&gt;Git 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;121&lt;/td&gt;
&lt;td&gt;nextcloud&lt;/td&gt;
&lt;td&gt;512MB&lt;/td&gt;
&lt;td&gt;파일 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;전부 합쳐도 RAM 2GB 언저리다. VM으로 했다면 최소 8-10GB는 썼을 거다.&lt;/p&gt;
&lt;p&gt;서비스가 죽거나 업데이트할 때 해당 컨테이너만 재시작하면 된다. 다른 서비스에 영향이 없다. 이게 분리의 핵심이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;LXC는 홈서버에서 서비스를 운영하는 가장 실용적인 방법이다. Docker도 좋지만 Proxmox를 이미 쓰고 있다면 LXC가 더 자연스럽다. 관리 포인트도 적고 오버헤드도 낮다.&lt;/p&gt;
</content:encoded></item><item><title>JPA orphanRemoval — Remove from collection, delete from DB</title><link>https://typhoon.is-a.dev/posts/en/jpa-orphan-removal/</link><guid isPermaLink="true">https://typhoon.is-a.dev/posts/en/jpa-orphan-removal/</guid><description>What orphanRemoval=true actually does, and how it differs from CascadeType.REMOVE.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Add &lt;code&gt;orphanRemoval = true&lt;/code&gt; to a &lt;code&gt;@OneToMany&lt;/code&gt;, and JPA will automatically issue a DELETE when you remove a child entity from the parent&apos;s collection.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&amp;lt;OrderItem&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line — &lt;code&gt;order.getItems().remove(item)&lt;/code&gt; — and the &lt;code&gt;OrderItem&lt;/code&gt; is gone from the database on transaction commit. No explicit &lt;code&gt;em.remove()&lt;/code&gt; needed.&lt;/p&gt;
&lt;h2&gt;orphanRemoval vs CascadeType.REMOVE&lt;/h2&gt;
&lt;p&gt;They sound similar but trigger on different events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CascadeType.REMOVE&lt;/strong&gt; fires when the parent entity is deleted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;em.remove(order); // → also deletes all its items
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;orphanRemoval&lt;/strong&gt; fires when a child is removed from the parent&apos;s collection — even if the parent still exists:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;order.getItems().remove(item); // → item deleted from DB, order stays
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice, most relationships that need one need both. Using &lt;code&gt;cascade = CascadeType.ALL&lt;/code&gt; covers the parent-deletion case while &lt;code&gt;orphanRemoval = true&lt;/code&gt; handles collection removal.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;JPA&apos;s dirty checking tracks changes to your collections within a persistence context. At flush time (before commit), it compares the current state of the collection to the snapshot taken when the entity was loaded. Any entity that disappeared from the collection is treated as an orphan and scheduled for deletion.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;h3&gt;Don&apos;t share orphanRemoval children&lt;/h3&gt;
&lt;p&gt;orphanRemoval implies exclusive ownership. If another entity or relationship also references the same child, you&apos;ll get unexpected deletes. It&apos;s only safe when one parent fully owns the child&apos;s lifecycle.&lt;/p&gt;
&lt;h3&gt;Replacing the whole collection deletes everything&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Dangerous: all existing items become orphans → all deleted
order.setItems(newItems);

// Safe: mutate the existing collection
order.getItems().clear();
order.getItems().addAll(newItems);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swapping the collection reference via setter marks every previous item as orphaned. If that&apos;s not what you want, always mutate in place.&lt;/p&gt;
&lt;h3&gt;Bidirectional relationships need both sides disconnected&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;item.setOrder(null);            // remove child&apos;s FK reference
order.getItems().remove(item);  // remove from parent&apos;s collection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skipping either side can cause dirty checking to miss the change. The clean way is to encapsulate this in helper methods:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Entity
public class Order {
    @OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&amp;lt;OrderItem&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep &lt;code&gt;items&lt;/code&gt; private, expose only &lt;code&gt;addItem&lt;/code&gt; and &lt;code&gt;removeItem&lt;/code&gt;. External code never touches the collection directly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Use &lt;code&gt;orphanRemoval = true&lt;/code&gt; when the child&apos;s lifecycle is fully owned by the parent — if removing it from the collection means it should no longer exist, this is exactly the right tool.&lt;/p&gt;
</content:encoded></item><item><title>JPA orphanRemoval — 컬렉션에서 제거하면 DB에서도 지워진다</title><link>https://typhoon.is-a.dev/posts/ko/jpa-orphan-removal/</link><guid isPermaLink="true">https://typhoon.is-a.dev/posts/ko/jpa-orphan-removal/</guid><description>orphanRemoval=true가 정확히 무엇을 하는지, CascadeType.REMOVE와 어떻게 다른지.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;@OneToMany&lt;/code&gt;에 &lt;code&gt;orphanRemoval = true&lt;/code&gt;를 붙이면, 부모 컬렉션에서 자식 엔티티를 제거할 때 JPA가 자동으로 DELETE 쿼리를 날린다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&amp;lt;OrderItem&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;order.getItems().remove(item)&lt;/code&gt; 한 줄이면 트랜잭션 커밋 시 해당 &lt;code&gt;OrderItem&lt;/code&gt;이 DB에서 삭제된다.&lt;/p&gt;
&lt;h2&gt;CascadeType.REMOVE와 차이&lt;/h2&gt;
&lt;p&gt;헷갈리는 지점이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CascadeType.REMOVE&lt;/strong&gt;: 부모 엔티티가 삭제될 때 자식도 같이 삭제된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// order를 삭제하면 → items도 삭제
em.remove(order);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;orphanRemoval&lt;/strong&gt;: 부모의 컬렉션에서 자식이 빠질 때 삭제된다. 부모가 살아있어도.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// order는 살아있고, items 컬렉션에서만 제거 → DB에서 삭제
order.getItems().remove(item);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;둘을 같이 쓰면 두 경우 모두 처리된다. 대부분 같이 쓴다.&lt;/p&gt;
&lt;h2&gt;동작 원리&lt;/h2&gt;
&lt;p&gt;JPA dirty checking이 핵심이다. 트랜잭션 안에서 영속성 컨텍스트가 컬렉션 변화를 추적한다. 커밋 전 flush 시점에 컬렉션에서 빠진 엔티티를 감지하고 DELETE를 실행한다.&lt;/p&gt;
&lt;p&gt;직접 &lt;code&gt;em.remove(item)&lt;/code&gt;을 호출하지 않아도 된다. 컬렉션 조작만으로 충분하다.&lt;/p&gt;
&lt;h2&gt;주의할 점&lt;/h2&gt;
&lt;h3&gt;자식 엔티티가 다른 곳에서도 참조되면 안 된다&lt;/h3&gt;
&lt;p&gt;orphanRemoval은 &quot;이 자식은 이 부모만 소유한다&quot;는 전제다. 같은 자식 엔티티를 다른 부모나 다른 연관관계가 참조하고 있으면 예상치 못한 삭제가 발생한다.&lt;/p&gt;
&lt;h3&gt;컬렉션 교체 시 주의&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 위험: 기존 컬렉션의 모든 항목이 orphan으로 처리돼 전부 삭제된다
order.setItems(newItems);

// 안전: 기존 컬렉션을 유지하며 수정
order.getItems().clear();
order.getItems().addAll(newItems);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;setter로 컬렉션 통째로 교체하면 기존 항목이 전부 orphan으로 처리돼 삭제된다. 의도한 게 아니라면 컬렉션 내부를 수정하는 방식을 써야 한다.&lt;/p&gt;
&lt;h3&gt;양방향 연관관계에서 양쪽 모두 끊어야 한다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 한쪽만 끊으면 dirty checking이 제대로 작동 안 할 수 있다
item.setOrder(null);            // 자식의 외래키 참조 제거
order.getItems().remove(item);  // 부모 컬렉션에서 제거
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실수하기 쉬운 부분이라 보통 연관관계 편의 메서드로 묶어둔다.&lt;/p&gt;
&lt;h2&gt;실용적인 패턴&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Entity
public class Order {
    @OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&amp;lt;OrderItem&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;addItem&lt;/code&gt;과 &lt;code&gt;removeItem&lt;/code&gt;으로 양방향 연관관계를 한 곳에서 관리한다. 외부에서 직접 컬렉션을 건드리지 않게 한다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;orphanRemoval = true&lt;/code&gt;는 자식 엔티티의 생명주기를 부모에게 완전히 위임할 때 쓴다. 컬렉션에서 빼면 지워져야 하는 관계라면 붙여두면 된다.&lt;/p&gt;
</content:encoded></item></channel></rss>