<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev.geonwoo_</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 24 Feb 2026 09:14:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev.geonwoo_</title>
            <url>https://velog.velcdn.com/images/dev_geonwoo/profile/617ebf5a-c49e-4b4b-8283-ceaff2fd2189/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev.geonwoo_. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_geonwoo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[스프링 부트와 JPA 활용] 5. 웹 계층 개발]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-5.-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-2ebywa6r</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-5.-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-2ebywa6r</guid>
            <pubDate>Tue, 24 Feb 2026 09:14:47 GMT</pubDate>
            <description><![CDATA[<p>웹 계층 개발 – MVC 분리와 변경 감지의 이해</p>
<p>웹 계층 개발은 단순히 Controller와 HTML을 만드는 작업이 아니다.
도메인과 화면을 어떻게 분리할 것인가,
그리고 변경 감지와 병합의 차이를 어떻게 이해할 것인가에 초점을 둔다.</p>
<hr>
<h3 id="1-홈-화면과-레이아웃--템플릿-구조-설계">1. 홈 화면과 레이아웃 – 템플릿 구조 설계</h3>
<p>홈 컨트롤러는 단순하다.</p>
<pre><code>@Controller
@Slf4j
public class HomeController {

    @RequestMapping(&quot;/&quot;)
    public String home() {
        log.info(&quot;home controller&quot;);
        return &quot;home&quot;;
    }
}</code></pre><p>스프링 부트는 다음 규칙으로 뷰를 찾는다.</p>
<pre><code>classpath:/templates/ + viewName + .html</code></pre><pre><code>return &quot;home&quot;;
→ templates/home.html 렌더링</code></pre><p>추가로, fragments(header, footer)를 사용해 공통 레이아웃을 분리했다.</p>
<p>이 설계의 의미는 명확하다.</p>
<ul>
<li>컨트롤러는 흐름 제어</li>
<li>HTML은 화면 표현</li>
<li>공통 UI는 fragment로 재사용</li>
</ul>
<p>웹 계층도 결국 <strong>관심사 분리(Separation of Concerns)</strong>이다.</p>
<hr>
<h3 id="2-회원-등록--폼-객체를-사용하는-이유">2. 회원 등록 – 폼 객체를 사용하는 이유</h3>
<p>회원 등록에서 가장 중요한 설계 포인트는 엔티티를 직접 화면에 노출하지 않는 것이다.</p>
<pre><code>@Getter @Setter
public class MemberForm {

    @NotEmpty(message = &quot;회원 이름은 필수 입니다&quot;)
    private String name;

    private String city;
    private String street;
    private String zipcode;
}</code></pre><p>왜 폼 객체를 따로 만들었을까?</p>
<p>엔티티를 직접 사용하면:</p>
<ul>
<li>화면 요구사항이 엔티티에 스며든다.</li>
<li>검증 로직이 도메인 모델에 침투한다.</li>
<li>엔티티가 점점 화면 종속적으로 변한다.</li>
</ul>
<p>실무에서 엔티티는 핵심 비즈니스 로직만 가져야 한다.</p>
<p>따라서,</p>
<ul>
<li>화면 → Form 객체</li>
<li>서비스 → 엔티티 변환</li>
<li>도메인 → 순수 상태 유지</li>
</ul>
<p>이 구조가 유지보수를 살린다.</p>
<hr>
<h3 id="3-상품-수정--병합merge의-위험성">3. 상품 수정 – 병합(merge)의 위험성</h3>
<p>상품 수정은 웹 계층에서 가장 중요한 학습 포인트다.</p>
<p>초기 구현은 이런 형태다.</p>
<pre><code>@PostMapping(&quot;/items/{itemId}/edit&quot;)
public String updateItem(@ModelAttribute(&quot;form&quot;) BookForm form) {
    Book book = new Book();
    book.setId(form.getId());
    book.setName(form.getName());
    ...
    itemService.saveItem(book);
}</code></pre><p>문제는 이 Book 객체가 준영속 상태라는 것이다.</p>
<p>그리고 save()는 내부에서 merge()를 호출한다.</p>
<pre><code>public void save(Item item) {
    if (item.getId() == null) {
        em.persist(item);
    } else {
        em.merge(item);
    }
}</code></pre><p>병합의 문제점</p>
<p>merge는:</p>
<ul>
<li>모든 필드를 덮어쓴다.</li>
<li>값이 없으면 null로 업데이트된다.</li>
<li>선택적 변경이 어렵다.</li>
</ul>
<p>즉, 병합은 “전체 교체”이다.</p>
<p>실무에서는 수정 가능한 필드만 노출하는 경우가 많기 때문에
merge는 오히려 위험하다.</p>
<hr>
<h3 id="4-권장-방식--변경-감지-사용">4. 권장 방식 – 변경 감지 사용</h3>
<p>가장 좋은 방법은 컨트롤러에서는 ID와 변경 값만 전달하는 것이다.</p>
<pre><code>@PostMapping(&quot;/items/{itemId}/edit&quot;)
public String updateItem(@PathVariable Long itemId,
                         @ModelAttribute(&quot;form&quot;) BookForm form) {
    itemService.updateItem(itemId, form.getName(),
                           form.getPrice(),
                           form.getStockQuantity());
    return &quot;redirect:/items&quot;;
}</code></pre><p>서비스 계층에서 영속 상태 엔티티를 조회하고 수정한다.</p>
<pre><code>@Transactional
public void updateItem(Long id, String name, int price, int stockQuantity) {
    Item item = itemRepository.findOne(id);
    item.setName(name);
    item.setPrice(price);
    item.setStockQuantity(stockQuantity);
}</code></pre><p>트랜잭션 커밋 시점에 Dirty Checking이 동작한다.</p>
<p>이 설계의 의미는 명확하다.</p>
<ul>
<li>엔티티를 직접 생성하지 않는다.</li>
<li>영속 상태에서 변경한다.</li>
<li>JPA의 변경 감지를 활용한다.</li>
</ul>
<p>이것이 JPA를 제대로 사용하는 방법이다.</p>
<hr>
<h3 id="5-주문-화면--흐름-제어는-컨트롤러의-역할">5. 주문 화면 – 흐름 제어는 컨트롤러의 역할</h3>
<p>주문 컨트롤러는 단순하다.</p>
<pre><code>@GetMapping(&quot;/order&quot;)
public String createForm(Model model) {
    model.addAttribute(&quot;members&quot;, memberService.findMembers());
    model.addAttribute(&quot;items&quot;, itemService.findItems());
    return &quot;order/orderForm&quot;;
}</code></pre><p>POST 요청 시에는:</p>
<pre><code>orderService.order(memberId, itemId, count);</code></pre><p>여기서 중요한 점:</p>
<ul>
<li>컨트롤러는 도메인 로직을 모른다.</li>
<li>서비스가 트랜잭션을 가진다.</li>
<li>도메인이 비즈니스 규칙을 가진다.</li>
</ul>
<p>웹 계층은 오케스트레이터일 뿐이다.</p>
<hr>
<h3 id="6-주문-검색--동적-쿼리의-한계">6. 주문 검색 – 동적 쿼리의 한계</h3>
<p>주문 검색은 OrderSearch 조건 객체를 사용한다.</p>
<pre><code>public class OrderSearch {
    private String memberName;
    private OrderStatus orderStatus;
}</code></pre><p>문자열 JPQL 조립 방식은:</p>
<ul>
<li>가독성 저하</li>
<li>공백 실수 위험</li>
<li>조건 분기 복잡성 증가</li>
</ul>
<p>실제로 JPQL 문자열 연결 시 공백이 누락되면
문법 에러가 발생한다.</p>
<p>실무에서는 Querydsl 같은 타입 안전한 쿼리 도구가 더 적합하다.</p>
<hr>
<h3 id="7-웹-계층-설계에서-배운-핵심">7. 웹 계층 설계에서 배운 핵심</h3>
<ol>
<li>엔티티는 화면에 종속되면 안 된다</li>
</ol>
<p>-&gt; 폼 객체를 사용해 분리하자.</p>
<ol start="2">
<li>병합보다 변경 감지가 안전하다</li>
</ol>
<p>-&gt; 영속 상태에서 수정하자.</p>
<ol start="3">
<li>컨트롤러는 흐름만 제어한다</li>
</ol>
<p>-&gt; 비즈니스 로직은 도메인에 둔다.</p>
<ol start="4">
<li>서비스는 트랜잭션 경계다</li>
</ol>
<p>-&gt; 웹 계층은 트랜잭션을 몰라야 한다.</p>
<ol start="5">
<li>동적 쿼리는 유지보수 비용이 높다</li>
</ol>
<p>-&gt; 타입 안전한 대안이 필요하다.</p>
<hr>
<h3 id="8-정리">8. 정리</h3>
<p>웹 계층 개발은 단순 CRUD 구현이 아니다.</p>
<p>이 단계에서 이해해야 할 것은:</p>
<ul>
<li>MVC 분리 구조</li>
<li>폼 객체와 엔티티의 분리</li>
<li>준영속과 병합의 개념</li>
<li>변경 감지(Dirty Checking)</li>
<li>트랜잭션 경계의 의미</li>
</ul>
<p>결국 웹 계층은
도메인을 보호하기 위한 방어선이다.</p>
<hr>
<p>출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트와 JPA 활용] 4. 주문 도메인 개발]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-4.-%EC%A3%BC%EB%AC%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C-pz6gjs1c</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-4.-%EC%A3%BC%EB%AC%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C-pz6gjs1c</guid>
            <pubDate>Sun, 22 Feb 2026 14:15:56 GMT</pubDate>
            <description><![CDATA[<p>주문 도메인 개발 – 도메인 모델 패턴을 적용하는 이유</p>
<p>도메인 모델 중심 설계 관점에서
주문을 어떻게 설계해야 하는지에 초점을 둔다.</p>
<hr>
<h3 id="1-주문-엔티티-개발--주문은-상태를-가지는-도메인이다">1. 주문 엔티티 개발 – 주문은 상태를 가지는 도메인이다.</h3>
<p>주문(Order)은 단순히 회원과 상품을 연결하는 데이터 객체가 아니다.
주문 상태를 가지며, 취소 로직을 직접 수행하는 도메인 객체다.</p>
<pre><code>@Entity
@Table(name = &quot;orders&quot;)
@Getter
@Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = &quot;order_id&quot;)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member; // 주문 회원

    @OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL)
    private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;&gt;();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;delivery_id&quot;)
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}</code></pre><h4 id="왜-비즈니스-로직을-엔티티에-넣었을까">왜 비즈니스 로직을 엔티티에 넣었을까?</h4>
<p>많이 하는 실수가 있다.</p>
<p>주문 취소 로직을 서비스에서 처리하면</p>
<ul>
<li>배송 상태 검증 로직이 서비스에 흩어지고,</li>
<li>주문 상태 변경 규칙이 중복될 수 있고,</li>
<li>엔티티가 단순 데이터 컨테이너가 된다.</li>
</ul>
<p>하지만 주문 취소는 주문의 핵심 책임이다.</p>
<ul>
<li>배송 완료된 주문은 취소할 수 없다.</li>
<li>취소 시 주문상품도 함께 취소되어야 한다.</li>
<li>상태 변경은 일관되게 관리되어야 한다.</li>
</ul>
<p>즉, 주문은 상태 전이를 관리하는 도메인 객체다.</p>
<hr>
<h3 id="2-생성-메서드--객체-생성-규칙을-강제한다">2. 생성 메서드 – 객체 생성 규칙을 강제한다.</h3>
<pre><code>public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {

    Order order = new Order();
    order.setMember(member);
    order.setDelivery(delivery);

    for (OrderItem orderItem : orderItems) {
        order.addOrderItem(orderItem);
    }

    order.setStatus(OrderStatus.ORDER);
    order.setOrderDate(LocalDateTime.now());
    return order;
}</code></pre><h4 id="왜-정적-팩토리-메서드를-사용할까">왜 정적 팩토리 메서드를 사용할까?</h4>
<ul>
<li>연관관계 세팅을 강제하기 위해</li>
<li>주문 상태를 반드시 ORDER로 초기화하기 위해</li>
<li>불완전한 객체 생성을 막기 위해</li>
</ul>
<p>생성 규칙을 한 곳에 모아두면
도메인의 일관성을 유지할 수 있다.</p>
<hr>
<h3 id="3-주문-취소--상태-전이는-도메인이-책임진다">3. 주문 취소 – 상태 전이는 도메인이 책임진다.</h3>
<pre><code>public void cancel() {
    if (delivery.getStatus() == DeliveryStatus.COMP) {
        throw new IllegalStateException(&quot;이미 배송완료된 상품은 취소가 불가능합니다.&quot;);
    }
    this.setStatus(OrderStatus.CANCEL);
    for (OrderItem orderItem : orderItems) {
        orderItem.cancel();
    }
}</code></pre><p>여기서 중요한 설계 포인트는 다음과 같다.</p>
<ul>
<li>배송 완료 여부를 주문이 직접 검증한다.</li>
<li>주문 상태를 변경한다.</li>
<li>주문상품에 취소를 전파한다.</li>
</ul>
<p>이 설계의 의미는 명확하다.</p>
<p>주문 취소는 단순 UPDATE 쿼리가 아니라
도메인 상태 변경 이벤트다.</p>
<hr>
<h3 id="4-주문상품orderitem--재고-변경의-주체">4. 주문상품(OrderItem) – 재고 변경의 주체</h3>
<pre><code>public static OrderItem createOrderItem(Item item, int orderPrice, int count) {

    OrderItem orderItem = new OrderItem();
    orderItem.setItem(item);
    orderItem.setOrderPrice(orderPrice);
    orderItem.setCount(count);

    item.removeStock(count); // 재고 감소
    return orderItem;
}</code></pre><h4 id="왜-주문-생성-시-재고를-줄일까">왜 주문 생성 시 재고를 줄일까?</h4>
<ul>
<li>주문이 생성되면 재고는 즉시 차감되어야 한다.</li>
<li>재고 부족 검증은 Item이 책임진다.</li>
<li>주문과 재고 정책을 분리한다.</li>
</ul>
<p>주문 취소 시에는 반대로 재고를 복구한다.</p>
<pre><code>public void cancel() {
    getItem().addStock(count);
}</code></pre><p>즉,</p>
<ul>
<li>재고 감소는 Item의 책임</li>
<li>주문 상태 관리는 Order의 책임</li>
</ul>
<p>각 도메인이 자신의 규칙을 스스로 지킨다.</p>
<hr>
<h3 id="5-주문-서비스--왜-로직이-거의-없을까">5. 주문 서비스 – 왜 로직이 거의 없을까?</h3>
<pre><code>@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {

    @Transactional
    public Long order(Long memberId, Long itemId, int count) {
        // 엔티티 조회
        // 배송 생성
        // 주문상품 생성
        // 주문 생성
        // 저장
    }
}</code></pre><h4 id="서비스를-보면-이상할-정도로-로직이-단순하다-왜-그럴까">서비스를 보면 이상할 정도로 로직이 단순하다. 왜 그럴까?</h4>
<p>이 구조의 의미는 명확하다.</p>
<ul>
<li>서비스는 트랜잭션 경계</li>
<li>도메인은 비즈니스 규칙 담당</li>
<li>리포지토리는 영속성 처리</li>
</ul>
<p>즉, 도메인 모델 패턴을 따르고 있기 때문이다.</p>
<p>서비스는 흐름만 제어하고
진짜 비즈니스 로직은 엔티티 내부에 있다.</p>
<hr>
<h3 id="6-주문-검색--동적-쿼리의-한계">6. 주문 검색 – 동적 쿼리의 한계</h3>
<p>주문 검색 기능에서는 OrderSearch 조건 객체를 사용한다.</p>
<pre><code>public class OrderSearch {
    private String memberName;
    private OrderStatus orderStatus;
}</code></pre><p>JPQL 문자열을 직접 이어붙이면 다음과 같은 문제가 발생한다.</p>
<ul>
<li>가독성 저하</li>
<li>오타 위험</li>
<li>조건 분기 복잡성 증가</li>
</ul>
<p>JPA Criteria는 타입은 안전하지만 코드가 지나치게 복잡하다.</p>
<p>결론적으로 실무에서는 Querydsl 같은 대안을 사용한다.</p>
<hr>
<h3 id="7-핵심-설계-포인트">7. 핵심 설계 포인트</h3>
<p>주문은 상태를 가지는 도메인이다.</p>
<ul>
<li>취소 가능 여부를 스스로 판단한다.</li>
<li>상태 전이를 내부에서 관리한다.</li>
</ul>
<p>생성 규칙은 팩토리 메서드로 강제한다.</p>
<ul>
<li>불완전한 객체 생성을 방지한다.</li>
<li>연관관계 세팅을 통제한다.</li>
</ul>
<p>재고 변경은 Item의 책임이다.</p>
<ul>
<li>재고 감소는 주문이 아니라 상품의 책임</li>
<li>응집도 높은 도메인 설계</li>
</ul>
<p>서비스는 트랜잭션 경계다.</p>
<ul>
<li>흐름만 조율한다</li>
<li>비즈니스 로직은 엔티티가 가진다</li>
</ul>
<p>동적 쿼리는 유지보수 관점에서 한계가 있다.</p>
<ul>
<li>JPQL 문자열 조립은 위험하다</li>
<li>실무에서는 Querydsl이 더 적합하다</li>
</ul>
<hr>
<h3 id="8-정리">8. 정리</h3>
<p>주문 도메인 구현은</p>
<ul>
<li>도메인 모델 패턴 적용</li>
<li>상태 전이 로직의 캡슐화</li>
<li>생성 규칙 강제</li>
<li>트랜잭션 경계 설정</li>
<li>동적 쿼리의 한계 이해</li>
</ul>
<p>이 다섯 가지를 이해하는 단계이다.</p>
<hr>
<p>출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트와 JPA 활용] 3. 상품 도메인 개발]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-3.-%EC%83%81%ED%92%88-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-3.-%EC%83%81%ED%92%88-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Sun, 15 Feb 2026 07:04:49 GMT</pubDate>
            <description><![CDATA[<p>상품 도메인 개발 – 엔티티에 비즈니스 로직을 넣는 이유</p>
<p>도메인 모델 중심 설계 관점에서 상품을 어떻게 다뤄야 하는지에 초점을 둔다.</p>
<hr>
<h3 id="1-상품-엔티티-개발--단순-데이터-객체가-아니다">1. 상품 엔티티 개발 – 단순 데이터 객체가 아니다</h3>
<p>상품(Item)은 단순히 name, price, stockQuantity를 가지는 데이터 객체가 아니다.
재고 관리라는 비즈니스 로직을 직접 가지고 있는 도메인 객체다.</p>
<pre><code>@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;dtype&quot;)
@Getter
@Setter
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = &quot;item_id&quot;)
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = &quot;items&quot;)
    private List&lt;Category&gt; categories = new ArrayList&lt;&gt;();

    // == 비즈니스 로직 == //

    public void addStock(int quantity) {
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity) {
        int restStock = this.stockQuantity - quantity;
        if (restStock &lt; 0) {
            throw new NotEnoughStockException(&quot;need more stock&quot;);
        }
        this.stockQuantity = restStock;
    }
}</code></pre><p>왜 재고 로직을 엔티티에 넣었을까?</p>
<p>많이 하는 실수가 있다.</p>
<p>재고 차감 로직을 서비스에서 처리하는 것</p>
<p>그렇게 되면:</p>
<ul>
<li>재고 정책이 여기저기 흩어지고</li>
<li>동일한 로직이 중복될 수 있고</li>
<li>도메인이 단순 DTO처럼 변해버린다.</li>
</ul>
<p>하지만 재고 감소는 상품의 핵심 책임이다.</p>
<ul>
<li>재고는 음수가 되면 안 된다.</li>
<li>재고 증가/감소 규칙은 상품이 알고 있어야 한다.</li>
</ul>
<p>즉, 도메인 모델에 비즈니스 규칙을 캡슐화한 것이다.</p>
<hr>
<h3 id="2-예외-설계--도메인-예외는-도메인에서-던진다">2. 예외 설계 – 도메인 예외는 도메인에서 던진다</h3>
<pre><code>public class NotEnoughStockException extends RuntimeException {
    public NotEnoughStockException(String message) {
        super(message);
    }
}</code></pre><p>재고가 부족하면 removeStock() 내부에서 바로 예외를 던진다.</p>
<p>이 설계의 의미는 명확하다.</p>
<p>재고 부족은 “시스템 오류”가 아니라 “비즈니스 규칙 위반”이다.</p>
<p>따라서:</p>
<ul>
<li>서비스가 if(stock &lt; 0) 검사하지 않는다.</li>
<li>도메인이 스스로 유효성을 보장한다.</li>
<li>서비스는 흐름 제어에만 집중한다.</li>
</ul>
<hr>
<h3 id="3-상품-리포지토리--jpa-기본-패턴">3. 상품 리포지토리 – JPA 기본 패턴</h3>
<pre><code>@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item) {
        if (item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }

    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }

    public List&lt;Item&gt; findAll() {
        return em.createQuery(&quot;select i from Item i&quot;, Item.class)
                .getResultList();
    }
}</code></pre><h4 id="persist-vs-merge">persist vs merge</h4>
<p>여기서 가장 중요한 부분은 merge()다.</p>
<ul>
<li>persist() → 신규 엔티티 저장</li>
<li>merge() → 준영속 엔티티를 다시 병합</li>
</ul>
<p>하지만 실무에서는
변경 감지(dirty checking)를 사용하는 것이 훨씬 안전하다.</p>
<p>merge()는 예측하기 어렵고, 잘못 쓰면 의도하지 않은 업데이트가 발생할 수 있다.</p>
<p>이 강의에서는 구조 이해용으로 쓰지만,
실무에서는 가급적 merge()를 직접 쓰지 않는다.</p>
<hr>
<h3 id="4-상품-서비스--왜-단순-위임만-할까">4. 상품 서비스 – 왜 단순 위임만 할까?</h3>
<pre><code>@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item) {
        itemRepository.save(item);
    }

    public List&lt;Item&gt; findItems() {
        return itemRepository.findAll();
    }

    public Item findOne(Long itemId) {
        return itemRepository.findOne(itemId);
    }
}</code></pre><p>서비스는 거의 리포지토리에 위임만 한다.</p>
<p>이게 이상해 보일 수도 있다.</p>
<p>“서비스에 로직이 없는데 왜 만들지?”</p>
<p>이 구조의 의미는 명확하다.</p>
<ul>
<li>서비스는 트랜잭션 경계</li>
<li>도메인은 비즈니스 규칙 담당</li>
<li>리포지토리는 영속성 처리</li>
</ul>
<p>즉, 계층 책임 분리가 명확하다.</p>
<hr>
<h3 id="5-핵심-설계-포인트">5. 핵심 설계 포인트</h3>
<h4 id="도메인-모델은-데이터-덩어리가-아니다">도메인 모델은 데이터 덩어리가 아니다</h4>
<ul>
<li>재고 감소 로직은 상품 엔티티가 책임진다.</li>
<li>비즈니스 규칙은 도메인 내부에 둔다.</li>
</ul>
<h4 id="예외도-도메인-책임이다">예외도 도메인 책임이다</h4>
<ul>
<li>재고 부족은 도메인 예외로 처리한다.</li>
<li>서비스는 흐름만 제어한다.</li>
</ul>
<h4 id="트랜잭션은-서비스-계층에서-관리한다">트랜잭션은 서비스 계층에서 관리한다</h4>
<ul>
<li>@Transactional(readOnly = true)</li>
<li>쓰기 작업은 별도로 @Transactional</li>
</ul>
<h4 id="jpa-merge는-조심해서-사용해야-한다">JPA merge는 조심해서 사용해야 한다</h4>
<ul>
<li>변경 감지 기반 설계를 선호한다.</li>
<li>실무에서는 DTO → 엔티티 직접 병합 패턴을 주의한다.</li>
</ul>
<hr>
<h3 id="6-정리">6. 정리</h3>
<p>상품 도메인 구현은 </p>
<ul>
<li>도메인에 비즈니스 로직을 두는 이유</li>
<li>계층 책임 분리</li>
<li>트랜잭션 경계 설정</li>
<li>JPA 저장 전략 이해</li>
</ul>
<p>이 네 가지를 이해하는 단계다.</p>
<hr>
<p>출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트와 JPA 활용] 2. 회원 도메인 개발]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-2.-%ED%9A%8C%EC%9B%90-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-2.-%ED%9A%8C%EC%9B%90-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Wed, 07 Jan 2026 07:55:59 GMT</pubDate>
            <description><![CDATA[<p>회원 도메인 개발 (Spring Boot + JPA)</p>
<p>이번 챕터에서는 회원 도메인을 중심으로
Repository → Service → Test 흐름을 직접 구현해본다.</p>
<p>실제로는 트랜잭션, 영속성 컨텍스트, 중복 검증, 테스트 전략까지 JPA 기반 애플리케이션의 기본기를 다룬다.</p>
<hr>
<h3 id="1-요구사항-정리">1. 요구사항 정리</h3>
<h4 id="회원-기능">회원 기능</h4>
<ul>
<li>회원 등록</li>
<li>회원 목록 조회</li>
</ul>
<h4 id="비즈니스-조건">비즈니스 조건</h4>
<ul>
<li>동일한 이름의 회원은 가입할 수 없다. (중복 검증 필요)</li>
</ul>
<p>기능은 단순하지만,
이 안에 도메인 설계와 트랜잭션 전략의 핵심 포인트가 들어있다.</p>
<hr>
<h3 id="2-memberrepository-구현">2. MemberRepository 구현</h3>
<p>JPA를 직접 사용하는 계층이다.</p>
<pre><code>@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List&lt;Member&gt; findAll() {
        return em.createQuery(&quot;select m from Member m&quot;, Member.class)
                 .getResultList();
    }

    public List&lt;Member&gt; findByName(String name) {
        return em.createQuery(
                &quot;select m from Member m where m.name = :name&quot;,
                Member.class)
                .setParameter(&quot;name&quot;, name)
                .getResultList();
    }
}</code></pre><h4 id="repository">@Repository</h4>
<ul>
<li>스프링 빈 등록</li>
<li>JPA 예외 → 스프링 예외로 변환<h4 id="entitymanager">EntityManager</h4>
</li>
<li>PA의 핵심 객체</li>
<li>영속성 컨텍스트를 통해 엔티티를 관리<h4 id="jpql-사용">JPQL 사용</h4>
</li>
<li>테이블이 아니라 엔티티 기준으로 쿼리를 작성한다.</li>
</ul>
<hr>
<h3 id="3-memberservice-구현">3. MemberService 구현</h3>
<p>비즈니스 로직과 트랜잭션을 담당한다.</p>
<pre><code>@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    public List&lt;Member&gt; findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }

    private void validateDuplicateMember(Member member) {
        List&lt;Member&gt; findMembers =
                memberRepository.findByName(member.getName());

        if (!findMembers.isEmpty()) {
            throw new IllegalStateException(&quot;이미 존재하는 회원입니다.&quot;);
        }
    }
}</code></pre><h4 id="트랜잭션-전략">트랜잭션 전략</h4>
<ul>
<li><p>클래스 레벨에서는 @Transactional(readOnly = true)
→ 기본은 읽기 전용</p>
</li>
<li><p>쓰기 메서드에만 @Transactional
→ 변경 감지 + flush 수행</p>
</li>
<li><p>readOnly = true의 의미</p>
<ul>
<li>변경 감지 생략</li>
<li>flush 최소화</li>
<li>DB 드라이버가 지원하면 최적화</li>
</ul>
</li>
</ul>
<p>조회 전용 서비스는 readOnly를 습관처럼 붙이는 게 좋다.</p>
<p>⸻</p>
<h4 id="중복-회원-검증">중복 회원 검증</h4>
<pre><code>validateDuplicateMember()</code></pre><ul>
<li><p>서비스 레벨에서 1차 검증을 한다.
하지만 실무에서는 반드시 UNIQUE 제약 조건을
DB 레벨에도 추가해야 한다.</p>
</li>
<li><p>멀티 쓰레드 환경에서 동시에 가입 요청이 들어오면
서비스 레벨 검증만으로는 완벽하지 않다.</p>
</li>
</ul>
<p>서비스 검증 + DB 제약 조건을 함께 사용해야 안전하다.</p>
<hr>
<h3 id="4-의존성-주입-방식">4. 의존성 주입 방식</h3>
<p>생성자 주입</p>
<pre><code>@RequiredArgsConstructor
private final MemberRepository memberRepository</code></pre><p>왜 생성자 주입을 쓰는가?</p>
<ul>
<li>불변 객체 생성 가능</li>
<li>테스트 용이</li>
<li>final 사용 가능</li>
<li>컴파일 시점 오류 검증 가능</li>
</ul>
<p>스프링 실무에서는 생성자 주입이 표준이다.</p>
<hr>
<h3 id="5-회원-기능-테스트">5. 회원 기능 테스트</h3>
<pre><code>@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        Member member = new Member();
        member.setName(&quot;kim&quot;);

        Long saveId = memberService.join(member);

        assertEquals(member, memberRepository.findOne(saveId));
    }

    @Test
    void 중복_회원_예외() {
        Member member1 = new Member();
        member1.setName(&quot;kim&quot;);

        Member member2 = new Member();
        member2.setName(&quot;kim&quot;);

        memberService.join(member1);

        assertThrows(IllegalStateException.class,
                () -&gt; memberService.join(member2));
    }
}</code></pre><h4 id="테스트-어노테이션-정리">테스트 어노테이션 정리</h4>
<ul>
<li>@SpringBootTest
→ 스프링 컨테이너를 띄워서 테스트</li>
<li>@Transactional
→ 테스트 종료 시 자동 롤백
→ 테스트 격리 보장</li>
</ul>
<p>실무에서 매우 중요하다.</p>
<p>⸻</p>
<h4 id="junit5-변경-사항">JUnit5 변경 사항</h4>
<ul>
<li>@RunWith 제거</li>
<li>@Test(expected=...) 제거</li>
<li>assertThrows() 사용</li>
</ul>
<hr>
<h3 id="6-테스트-환경-설정">6. 테스트 환경 설정</h3>
<p>src/test/resources/application.yml</p>
<p>스프링 부트는 테스트 시:</p>
<ul>
<li>datasource 설정이 없으면</li>
<li>기본적으로 메모리 DB(H2) 사용</li>
<li>ddl-auto: create-drop 자동 적용</li>
</ul>
<p>별도 설정 없이 테스트 격리 환경이 만들어진다.
이 구조는 매우 강력하다.</p>
<hr>
<h3 id="7--정리">7.  정리</h3>
<p>핵심 포인트</p>
<ul>
<li>Repository는 JPA에 집중</li>
<li>Service는 트랜잭션 + 비즈니스 로직 담당</li>
<li>읽기/쓰기 트랜잭션 전략 분리</li>
<li>생성자 주입 사용</li>
<li>중복 검증은 DB 제약 조건까지 고려</li>
<li>테스트는 트랜잭션 롤백으로 격리</li>
</ul>
<hr>
<p>출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트와 JPA 활용] 1. 도메인 분석 설계, 애플리케이션 구현 준비]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-1.-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B6%84%EC%84%9D-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-1.-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B6%84%EC%84%9D-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Fri, 12 Dec 2025 07:49:46 GMT</pubDate>
            <description><![CDATA[<h3 id="1-요구사항-분석--crud가-아니라-도메인을-본다">1. 요구사항 분석 – CRUD가 아니라 도메인을 본다</h3>
<p>단순 기능만 보면 회원/상품/주문 CRUD 시스템이다.
그런데 이걸 그냥 CRUD 테이블 구조로 만들면 이후 확장에 바로 한계가 온다.</p>
<p>핵심은 “주문 도메인”이 중심이라는 것이다.</p>
<ul>
<li>회원은 주문을 한다.</li>
<li>주문은 여러 상품을 가진다.</li>
<li>상품은 재고를 가진다.</li>
<li>주문은 취소될 수 있다.</li>
<li>취소되면 재고가 복구된다.</li>
</ul>
<p>→ 즉, 이 시스템은 단순 CRUD가 아니라
상태 변화와 비즈니스 규칙이 존재하는 도메인 모델이다.</p>
<hr>
<h3 id="2-주문상품을-mn으로-두지-않는-이유">2. 주문–상품을 M:N으로 두지 않는 이유</h3>
<p>처음 보면 주문과 상품은 다대다다.</p>
<p>하지만 JPA에서 @ManyToMany는 거의 사용하지 않는다.</p>
<p>왜?</p>
<ul>
<li>중간 테이블에 추가 컬럼을 넣을 수 없다.</li>
<li>실무에서 주문상품에는 수량, 주문 당시 가격 같은 정보가 반드시 필요하다.</li>
<li>유지보수 난이도가 급격히 올라간다.</li>
</ul>
<p>그래서 우리는 이렇게 푼다.</p>
<pre><code>Order 1 : N OrderItem N : 1 Item</code></pre><p>OrderItem이 단순 중간 테이블이 아니라
비즈니스 개념이 있는 엔티티가 된다.</p>
<p>이 설계 하나로 시스템 확장성이 완전히 달라진다.</p>
<hr>
<h3 id="3-상품-상속-구조--현실적인-타협">3. 상품 상속 구조 – 현실적인 타협</h3>
<p>상품은 도서/음반/영화로 나뉜다.</p>
<p>공통 속성:</p>
<ul>
<li>name</li>
<li>price</li>
<li>stockQuantity</li>
</ul>
<p>→ 공통 부모 Item</p>
<p>JPA 상속 전략을 사용해 하나의 테이블에 DTYPE으로 구분했다.</p>
<p>실무에서는 상속을 무조건 쓰기보다
구조 복잡성 vs 유지보수성을 비교해야 한다.</p>
<hr>
<h3 id="4-연관관계의-주인은-외래키가-있는-쪽">4. 연관관계의 주인은 “외래키가 있는 쪽”</h3>
<p>이건 JPA에서 정말 중요한 개념이다.</p>
<p>연관관계의 주인은</p>
<p>비즈니스 주인이 아니라
외래키를 가진 쪽이다.</p>
<p>Ex.</p>
<ul>
<li>Order가 MEMBER_ID를 가진다 → Order가 주인</li>
<li>OrderItem이 ORDER_ID를 가진다 → OrderItem이 주인</li>
</ul>
<p>왜냐하면
외래키를 가지지 않은 쪽을 주인으로 잡으면
불필요한 UPDATE 쿼리가 추가로 발생한다.</p>
<p>이건 단순 문법 문제가 아니라
SQL 발생 구조와 직결된 설계 문제다.</p>
<hr>
<h3 id="5-엔티티-설계-시-중요한-점">5. 엔티티 설계 시 중요한 점</h3>
<h4 id="setter를-막아야-한다">Setter를 막아야 한다</h4>
<p>Setter가 열려 있으면
엔티티 상태 변경 지점이 무한대로 늘어난다.</p>
<p>→ 도메인 로직이 흩어진다.
→ 유지보수 난이도 급상승</p>
<p>그래서</p>
<ul>
<li>생성 메서드</li>
<li>연관관계 편의 메서드</li>
<li>비즈니스 메서드</li>
</ul>
<p>위주로 설계하는 해야한다.</p>
<p>⸻</p>
<h4 id="모든-연관관계는-lazy">모든 연관관계는 LAZY</h4>
<p>기본 전략:</p>
<pre><code>@ManyToOne(fetch = LAZY)</code></pre><p>왜?
    •    EAGER는 예측이 안 된다.
    •    JPQL 사용 시 N+1 지뢰가 된다.
    •    운영 중 쿼리 튀어나오는 순간 디버깅 난이도 상승</p>
<p>실무에서의 원칙은:
“기본은 LAZY, 필요하면 fetch join”</p>
<p>⸻</p>
<h4 id="컬렉션은-필드에서-초기화">컬렉션은 필드에서 초기화</h4>
<pre><code>private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;&gt;();</code></pre><p>이건 단순 취향 문제가 아니다.</p>
<ul>
<li>null 안정성</li>
<li>하이버네이트 내부 컬렉션 래핑 문제 방지</li>
</ul>
<p>엔티티 설계에서 이런 디테일이 쌓여서
코드 안정성이 만들어진다.</p>
<hr>
<h3 id="6-계층-구조--전형적인-ddd-lite-구조">6. 계층 구조 – 전형적인 DDD-lite 구조</h3>
<pre><code>controller → service → repository → domain</code></pre><ul>
<li>controller: HTTP 처리</li>
<li>service: 트랜잭션 + 비즈니스 흐름</li>
<li>repository: JPA 접근</li>
<li>domain: 엔티티</li>
</ul>
<p>이 구조는 단순하지만
트랜잭션 경계와 도메인 책임 분리가 명확하다.</p>
<hr>
<h3 id="7-정리">7. 정리</h3>
<p>이 예제는 단순 CRUD 예제가 아니라</p>
<ul>
<li>다대다를 어떻게 풀 것인가</li>
<li>연관관계 주인은 왜 FK 쪽인가</li>
<li>왜 LAZY가 기본인가</li>
<li>왜 Setter를 막아야 하는가
를 학습하는 설계 예제다.</li>
</ul>
<hr>
<p>출처
스프링 부트와 JPA 활용 (김영한, 인프런, 2019)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 6. HTTP 헤더2 - 캐시와 조건부 요청]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-6.-HTTP-%ED%97%A4%EB%8D%942-%EC%BA%90%EC%8B%9C%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9A%94%EC%B2%AD</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-6.-HTTP-%ED%97%A4%EB%8D%942-%EC%BA%90%EC%8B%9C%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9A%94%EC%B2%AD</guid>
            <pubDate>Mon, 08 Dec 2025 15:32:35 GMT</pubDate>
            <description><![CDATA[<p>브라우저가 느려지는 이유는 대개 네트워크다. 같은 파일을 매번 내려받는 건 돈(트래픽)과 시간(레이턴시)을 같이 태운다. HTTP 캐시는 이걸 <strong>“이미 받은 건 재사용하자”</strong>로 바꿔서 UX를 확 끌어올린다. 다만 “캐시한다”는 말 안에는 만료(freshness), 검증(validation), 공유(proxy), 무효화(invalidation)까지 꽤 많은 규칙이 숨어 있다.</p>
<hr>
<h3 id="1-캐시가-없을-때-vs-있을-때">1. 캐시가 없을 때 vs 있을 때</h3>
<h4 id="캐시가-없을-때">캐시가 없을 때</h4>
<p>리소스가 변하지 않았더라도 매 요청마다 서버로부터 다시 내려받는다.</p>
<ul>
<li>네트워크 비용 증가 (대역폭/트래픽)</li>
<li>초기 로딩, 재방문 로딩 모두 느림</li>
<li>결과적으로 사용자 경험이 나빠짐</li>
</ul>
<h4 id="캐시가-있을-때">캐시가 있을 때</h4>
<p>응답을 로컬(브라우저 캐시)에 저장하고, 유효 기간 동안은 네트워크를 타지 않는다.</p>
<ul>
<li>트래픽 절감</li>
<li>로딩 속도 개선</li>
<li>서버 부하 감소</li>
</ul>
<p>대표적인 지시어는 이거 하나로 시작한다.</p>
<ul>
<li>Cache-Control: max-age=60
→ “이 응답은 60초 동안 fresh(신선)하다. 그동안은 캐시에서 바로 써라.”</li>
</ul>
<p>중요 포인트</p>
<ul>
<li>max-age가 살아있는 동안은 “재요청” 자체가 안 나간다.</li>
<li>max-age가 끝나면 그때부터 “검증(조건부 요청)”이 개입한다.</li>
</ul>
<hr>
<h3 id="2-캐시-만료-이후의-현실-진짜-바뀌었나를-확인해야-한다">2. 캐시 만료 이후의 현실: “진짜 바뀌었나?”를 확인해야 한다</h3>
<p>max-age가 끝났다고 해서 매번 1MB짜리 파일을 다시 내려받는 건 낭비다.
캐시 만료 이후에도 서버 데이터가 그대로일 수 있기 때문이다.</p>
<p>이때 필요한 게 검증 헤더(validator) 와 조건부 요청(conditional request) 이다.</p>
<p>핵심 목표는 하나다:</p>
<p>“서버 데이터가 그대로라면, 바디(body) 말고 ‘바뀌지 않았다’는 사실만 알려줘.”</p>
<p>그래서 등장하는 응답이:</p>
<ul>
<li>304 Not Modified
→ 바디 없음. 대신 헤더(메타 정보)만 갱신하고, 클라이언트는 자기 캐시를 재사용한다.</li>
</ul>
<hr>
<h3 id="3-검증-헤더와-조건부-요청-last-modified-vs-etag">3. 검증 헤더와 조건부 요청: Last-Modified vs ETag</h3>
<p>캐시 검증은 크게 두 가지 축으로 돌아간다.</p>
<h4 id="last-modified--if-modified-since-시간-기반">Last-Modified / If-Modified-Since (시간 기반)</h4>
<p>서버가 “이 리소스 마지막 수정 시각”을 알려준다.</p>
<ul>
<li>응답: Last-Modified: Tue, 10 Nov 2020 10:00:00 GMT</li>
<li>재요청(조건부): If-Modified-Since: Tue, 10 Nov 2020 10:00:00 GMT</li>
</ul>
<p>동작은 직관적이다.</p>
<ul>
<li>서버가 그 이후로 수정 안 했으면 → 304 Not Modified</li>
<li>수정했으면 → 200 OK + 새 바디</li>
</ul>
<p>장점</p>
<ul>
<li>단순하고 구현이 쉽다.</li>
</ul>
<p>단점(실무에서 걸리는 포인트)</p>
<ul>
<li>1초 미만 단위 제어가 어렵다.</li>
<li>“내용은 동일한데 저장만 다시 해서 수정 시간이 바뀐 경우” 같은 케이스에 취약하다.</li>
<li>서버가 “의도적으로 캐시 정책을 통제”하기 어렵다.
(Ex. 의미 없는 변경은 캐시 유지하고 싶은데 시간은 바뀜)</li>
</ul>
<h4 id="etag--if-none-match-버전-기반">ETag / If-None-Match (버전 기반)</h4>
<p>서버가 “이 표현의 버전(태그)”를 준다.</p>
<ul>
<li>응답: ETag: &quot;a2jiodwjekjl3&quot;</li>
<li>재요청: If-None-Match: &quot;a2jiodwjekjl3&quot;</li>
</ul>
<p>판단은 더 명확해진다.</p>
<ul>
<li>ETag 동일 → 304 Not Modified</li>
<li>ETag 다름 → 200 OK + 새 바디</li>
</ul>
<p>ETag의 본질</p>
<ul>
<li>클라이언트가 캐시 메커니즘을 “이해”할 필요가 없다.</li>
<li>서버가 “이 버전은 유지/갱신”을 완전히 통제한다.</li>
</ul>
<p>예를 들어 서버가 이렇게도 할 수 있다:</p>
<ul>
<li>“베타 오픈 3일간은 파일이 조금 바뀌어도 ETag 유지”</li>
<li>“배포 주기에 맞춰 ETag 전부 갱신”</li>
</ul>
<hr>
<h3 id="4-캐시-제어-헤더-한-장-요약">4. 캐시 제어 헤더 한 장 요약</h3>
<h4 id="cache-control-현대-표준">Cache-Control (현대 표준)</h4>
<ul>
<li>max-age=초 : fresh 기간</li>
<li>no-cache : 저장해도 되지만, 사용 전 항상 원 서버에 검증해야 한다.
(이름이 헷갈리는 대표 케이스)</li>
<li>no-store : 저장 금지 (민감 데이터)</li>
</ul>
<h4 id="하위-호환">하위 호환</h4>
<ul>
<li>Pragma: no-cache : HTTP/1.0 대응</li>
<li>Expires: 날짜 : 만료 시각(HTTP/1.0). 요즘은 max-age 선호</li>
<li>둘 다 있으면 Cache-Control이 우선 (Expires는 무시)</li>
</ul>
<h4 id="validator--conditional-헤더-정리">Validator / Conditional 헤더 정리</h4>
<ul>
<li>Validator: Last-Modified, ETag</li>
<li>Conditional:</li>
<li>시간 기반: If-Modified-Since, If-Unmodified-Since</li>
<li>태그 기반: If-None-Match, If-Match</li>
</ul>
<hr>
<h3 id="5-프록시-캐시-나만-빠른-게-아니라-모두가-빠르게">5. 프록시 캐시: “나만 빠른 게 아니라, 모두가 빠르게”</h3>
<p>브라우저 캐시는 개인(private) 캐시다.
하지만 동일 리소스를 많은 사용자가 요청한다면, 중간에 공용(public) 캐시를 두는 게 더 효율적이다.</p>
<p>Ex.
브라우저(private) → 한국 프록시 캐시(public) → 해외 오리진(origin)</p>
<p>프록시 캐시에서 자주 쓰는 헤더:</p>
<ul>
<li>Cache-Control: public : 공용 캐시에 저장 가능</li>
<li>Cache-Control: private : 개인 캐시에만 저장(기본 성격)</li>
<li>Cache-Control: s-maxage=초 : 프록시 캐시 전용 max-age</li>
<li>Age: 60 : 프록시에 저장된 지 얼마나 됐는지(초)</li>
</ul>
<p>실무에서는 “브라우저는 빨리 갱신해야 하는데 CDN은 좀 더 오래 들고 있어도 됨” 같은 요구가 많아서 s-maxage가 꽤 유용하다.</p>
<hr>
<h3 id="6-캐시-무효화-확실하게-막고-싶을-때">6. 캐시 무효화: “확실하게” 막고 싶을 때</h3>
<p>민감한 데이터(개인정보, 결제, 관리자 화면)나
“절대 오래된 값을 보여주면 안 되는” 응답은 캐시 전략이 달라진다.</p>
<p>자주 쓰는 조합:</p>
<ul>
<li>Cache-Control: no-store
→ 아예 저장 금지 (가장 강함)</li>
<li>Cache-Control: no-cache
→ 저장은 가능, 매번 원 서버 검증 후 사용</li>
<li>Cache-Control: must-revalidate
→ 만료 후 첫 조회는 반드시 원 서버 검증
→ 원 서버 접근 실패 시 반드시 오류 (보통 504 Gateway Timeout)</li>
</ul>
<h4 id="no-cache-vs-must-revalidate-차이-실무-포인트">no-cache vs must-revalidate 차이 (실무 포인트)</h4>
<p>둘 다 “검증하고 써라” 느낌인데, 장애 상황에서 갈린다.</p>
<h4 id="no-cache">no-cache</h4>
<ul>
<li>원 서버가 잠깐 죽었을 때</li>
<li>프록시 캐시 설정에 따라 그냥 오래된 캐시를 200으로 내줄 수도 있음</li>
<li>(운영 철학: “에러보단 구버전이라도 보여주자”)</li>
</ul>
<h4 id="must-revalidate">must-revalidate</h4>
<ul>
<li>원 서버가 죽었으면</li>
<li>오래된 캐시를 주면 안 됨 → 504로 실패해야 함</li>
<li>(운영 철학: “틀린 정보는 못 보여준다”)</li>
</ul>
<hr>
<h3 id="7-정리">7. 정리</h3>
<p>캐시 설계는 “성능”이 아니라 “정합성 정책”이다</p>
<p>캐시는 단순히 빠르게 만드는 기능이 아니라,</p>
<ul>
<li>얼마나 오래 믿을 것인지(max-age)</li>
<li>만료 후엔 어떻게 확인할 것인지(ETag/Last-Modified)</li>
<li>누구와 공유할 것인지(public/private, s-maxage)</li>
<li>장애 시 구버전을 허용할 것인지(no-cache vs must-revalidate)</li>
</ul>
<p>이런 정책 결정에 더 가깝다.</p>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 5. HTTP 헤더1 - 일반 헤더]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-5.-HTTP-%ED%97%A4%EB%8D%941-%EC%9D%BC%EB%B0%98-%ED%97%A4%EB%8D%94-u7ltmwze</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-5.-HTTP-%ED%97%A4%EB%8D%941-%EC%9D%BC%EB%B0%98-%ED%97%A4%EB%8D%94-u7ltmwze</guid>
            <pubDate>Mon, 08 Dec 2025 12:19:10 GMT</pubDate>
            <description><![CDATA[<h3 id="http-헤더-정리-표현협상전송인증쿠키">HTTP 헤더 정리 (표현/협상/전송/인증/쿠키)</h3>
<p>HTTP 헤더는 한마디로 <strong>“전송에 필요한 부가 정보”</strong>다.
바디에 “진짜 데이터”가 들어간다면, 헤더는 그 데이터를 어떻게 해석하고, 어떻게 다룰지를 알려준다.</p>
<h3 id="1-헤더-기본-형태">1. 헤더 기본 형태</h3>
<pre><code>field-name &quot;:&quot; OWS field-value OWS</code></pre><ul>
<li>OWS: 공백 허용</li>
<li>field-name은 대소문자 구분 없음</li>
</ul>
<p>Ex.</p>
<ul>
<li>요청: Host: <a href="http://www.google.com">www.google.com</a></li>
<li>응답: Content-Type: text/html; charset=UTF-8</li>
</ul>
<p>⸻</p>
<h4 id="과거-entity-→-지금-representation표현">(과거) Entity → (지금) Representation(표현)</h4>
<p>예전 RFC2616에서는 “엔티티”라고 불렀는데, 요즘 RFC723x에서는 표현(Representation) 이라는 말을 쓴다.</p>
<ul>
<li>표현 = 표현 메타데이터(헤더) + 표현 데이터(바디/payload)</li>
</ul>
<p>즉, 바디로 데이터를 보내고
헤더로 “이 바디는 JSON이고 UTF-8이고 gzip으로 압축했어” 같은 걸 알려주는 구조이다.</p>
<hr>
<h3 id="2-표현-헤더-representation-headers">2. 표현 헤더 (Representation Headers)</h3>
<h4 id="content-type">Content-Type</h4>
<p>“이 바디 데이터가 무슨 형식인지”</p>
<ul>
<li>text/html; charset=UTF-8</li>
<li>application/json</li>
<li>image/png</li>
</ul>
<h4 id="content-encoding">Content-Encoding</h4>
<p>“이 바디가 어떻게 압축됐는지”</p>
<ul>
<li>gzip, deflate, br(brotli) 등
서버가 압축해서 보내면, 클라이언트는 이걸 보고 풀어서 읽는다.</li>
</ul>
<h4 id="content-language">Content-Language</h4>
<p>“이 바디 내용이 어떤 자연어인지”</p>
<ul>
<li>ko, en, en-US</li>
</ul>
<h4 id="content-length">Content-Length</h4>
<p>“바디 길이(바이트)”</p>
<ul>
<li>단, Transfer-Encoding(chunked) 쓰면 Content-Length 쓰면 안된다.
(분할 전송이면 길이를 미리 알 수 없기 때문이다.)</li>
</ul>
<hr>
<h3 id="3-콘텐츠-협상-content-negotiation">3. 콘텐츠 협상 (Content Negotiation)</h3>
<p>클라이언트가 선호하는 표현을 요청하는 것을 의미한다.</p>
<p>요청에서만 사용:</p>
<ul>
<li>Accept (미디어 타입)</li>
<li>Accept-Language (언어)</li>
<li>Accept-Encoding (압축)</li>
<li>Accept-Charset (문자셋, 요즘은 거의 안쓴다.)</li>
</ul>
<p>우선순위: q 값(quality)</p>
<ul>
<li>0~1 (클수록 우선)</li>
<li>생략하면 1</li>
</ul>
<p>Ex.
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
→ ko-KR &gt; ko &gt; en-US &gt; en 순으로 선호한다.</p>
<p>“구체적인 게 우선”</p>
<ul>
<li>text/* 보다 text/plain이 더 구체적이니까 우선</li>
<li>파라미터 붙은 게 더 구체적 (text/plain;format=flowed)</li>
</ul>
<hr>
<h3 id="4--전송-방식-관련-헤더">4.  전송 방식 관련 헤더</h3>
<h4 id="단순-전송">단순 전송</h4>
<ul>
<li>Content-Length</li>
</ul>
<h4 id="압축-전송">압축 전송</h4>
<ul>
<li>Content-Encoding: gzip</li>
</ul>
<h4 id="분할-전송-chunked">분할 전송 (chunked)</h4>
<ul>
<li>Transfer-Encoding: chunked</li>
<li>이때 Content-Length 금지</li>
</ul>
<h4 id="범위-전송-부분-다운로드">범위 전송 (부분 다운로드)</h4>
<ul>
<li>요청: Range: bytes=1001-2000</li>
<li>응답: Content-Range: bytes 1001-2000/2000</li>
</ul>
<p>→ 영상 스트리밍, 다운로드 재개 같은 데서 핵심이다.</p>
<hr>
<h3 id="5-일반-정보-헤더">5. 일반 정보 헤더</h3>
<h4 id="referer">Referer</h4>
<p>“어디서 넘어왔는지”</p>
<ul>
<li>유입 분석/보안에서 자주 사용한다.
(스펠링이 원래 Referer인 게 함정)</li>
</ul>
<h4 id="user-agent">User-Agent</h4>
<p>클라이언트 앱 정보(브라우저/OS 등)</p>
<ul>
<li>통계/디버깅에서 유용
“특정 브라우저에서만 터지는 버그” 잡을 때 도움된다.</li>
</ul>
<h4 id="server">Server</h4>
<p>응답한 서버 소프트웨어 정보</p>
<ul>
<li>nginx, Apache 등</li>
</ul>
<h4 id="date">Date</h4>
<p>응답 생성 시간 (GMT)</p>
<hr>
<h3 id="6-특별히-중요한-헤더들">6. 특별히 중요한 헤더들</h3>
<h4 id="host-필수">Host (필수)</h4>
<p>요청 대상 호스트(도메인)
한 IP에서 여러 도메인(VHost)을 처리할 때 구분해준다.</p>
<h4 id="location">Location</h4>
<ul>
<li>201 Created: 새로 생성된 리소스 URI</li>
<li>3xx Redirect: 이동할 주소</li>
</ul>
<p>브라우저는 3xx + Location이면 자동 이동.</p>
<h4 id="allow">Allow</h4>
<p>405(Method Not Allowed)에서 “지원하는 메서드가 뭐냐”를 알려준다.</p>
<ul>
<li>Allow: GET, POST</li>
</ul>
<h4 id="retry-after">Retry-After</h4>
<p>503(Service Unavailable)에서
“언제 다시 시도해도 되는지” 알려준다.</p>
<ul>
<li>초 단위 / 날짜 둘 다 가능</li>
</ul>
<hr>
<h3 id="7-인증-헤더">7. 인증 헤더</h3>
<h4 id="authorization">Authorization</h4>
<p>클라이언트 → 서버</p>
<ul>
<li>Authorization: Basic ...</li>
<li>또는 Bearer <token> (실무는 이쪽이 훨씬 흔하다.)</li>
</ul>
<h4 id="www-authenticate">WWW-Authenticate</h4>
<p>서버 → 클라이언트 (보통 401과 함께)</p>
<ul>
<li>“이 리소스 접근하려면 어떤 인증이 필요해”를 설명한다.</li>
</ul>
<hr>
<h3 id="8-쿠키">8. 쿠키</h3>
<h4 id="set-cookie--cookie">Set-Cookie / Cookie</h4>
<ul>
<li>서버 → 클라이언트: Set-Cookie</li>
<li>클라이언트 → 서버: Cookie</li>
</ul>
<p>쿠키는 자동으로 모든 요청에 실려서 서버로 간다.
→ 그래서 최소 정보만 넣는 게 원칙이다. (세션ID, 토큰 정도)</p>
<h4 id="생명주기">생명주기</h4>
<ul>
<li>Expires(날짜) / Max-Age(초)</li>
<li>만료 생략하면 세션 쿠키(브라우저 종료까지)</li>
<li>만료 지정하면 영속 쿠키</li>
</ul>
<h4 id="도메인">도메인</h4>
<ul>
<li>domain=example.org 지정하면 서브도메인까지 포함</li>
<li>생략하면 현재 도메인만</li>
</ul>
<h4 id="path">path</h4>
<p>해당 path 이하에서만 쿠키 전송/접근</p>
<ul>
<li>보통 path=/</li>
</ul>
<h4 id="보안-옵션-실무-핵심">보안 옵션 (실무 핵심)</h4>
<ul>
<li>Secure: HTTPS에서만 전송</li>
<li>HttpOnly: JS(document.cookie) 접근 막음 → XSS 방어</li>
<li>SameSite: 다른 사이트에서 오는 요청에 쿠키 전송 제한 → CSRF 방어</li>
</ul>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 4. HTTP 상태코드]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-4.-HTTP-%EC%83%81%ED%83%9C%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-4.-HTTP-%EC%83%81%ED%83%9C%EC%BD%94%EB%93%9C</guid>
            <pubDate>Sun, 07 Dec 2025 15:53:38 GMT</pubDate>
            <description><![CDATA[<h3 id="1-http-상태-코드-정리">1. HTTP 상태 코드 정리</h3>
<p>HTTP 상태 코드는 서버가 “요청 처리 결과가 지금 어떤 상태인지”를 알려주는 신호다.
프론트/클라이언트 입장에서는 이걸 보고 “다시 요청할지, 로그인 시킬지, 페이지 이동할지” 같은 다음 행동을 정하게 된다.</p>
<p>상태 코드 분류</p>
<ul>
<li>1xx (Informational): 요청 받았고 처리 중 (거의 안 쓴다.)</li>
<li>2xx (Successful): 정상 처리</li>
<li>3xx (Redirection): 추가 행동 필요 (주로 Location 따라 이동)</li>
<li>4xx (Client Error): 클라이언트 문제 → 똑같이 재시도해도 보통 실패한다.</li>
<li>5xx (Server Error): 서버 문제 → 재시도하면 될 수도 있다.</li>
</ul>
<p>모르는 상태 코드가 오면?</p>
<p>클라이언트가 모르는 코드가 와도 보통 앞자리 기준으로 해석한다.</p>
<ul>
<li>299면 “2xx 성공이겠구나”</li>
<li>451이면 “4xx 클라 문제겠구나”</li>
<li>599면 “5xx 서버 문제겠구나”</li>
</ul>
<p>→ 그래서 미래에 상태 코드가 늘어나도 클라이언트를 매번 바꿀 필요가 없다.</p>
<hr>
<h3 id="2-2xx-성공">2. 2xx: 성공</h3>
<ul>
<li><p>200 OK
성공. 조회/수정/삭제 등 대부분에 무난하게 쓰인다.</p>
</li>
<li><p>201 Created
새 리소스 생성
그리고 보통 Location 헤더로 생성된 리소스 URI를 알려준다.</p>
<ul>
<li>Ex. POST /members
응답: 201 Created
헤더: Location: /members/100</li>
</ul>
</li>
<li><p>202 Accepted
요청은 받았는데 아직 처리 끝은 아니다.
배치 같은 “비동기 처리”에 쓰인다.</p>
</li>
<li><p>204 No Content
성공은 했는데 본문으로 내려줄 게 없다.</p>
</li>
</ul>
<hr>
<h3 id="3-3xx-리다이렉션">3. 3xx: 리다이렉션</h3>
<p>3xx가 나오면 브라우저는 보통 Location 헤더를 보고 자동으로 이동한다.</p>
<p>리다이렉션 종류</p>
<ul>
<li>영구 이동: 301, 308</li>
<li>일시 이동: 302, 307, 303</li>
<li>특수 목적: 300, 304</li>
</ul>
<p>⸻</p>
<h4 id="301--308-영구-리다이렉션">301 / 308: 영구 리다이렉션</h4>
<p>“이 URI는 이제 앞으로 계속 여기로 가세요” 느낌.
    •    예: /members → /users</p>
<ul>
<li><p>301 Moved Permanently
리다이렉트되면서 메서드가 GET으로 바뀔 수도 있고, 바디가 사라질 수도 있다. (MAY)</p>
</li>
<li><p>308 Permanent Redirect
301이랑 비슷한데, 메서드/바디를 그대로 유지한다.
(POST로 보냈으면, 리다이렉트도 POST로 유지)</p>
</li>
</ul>
<p>⸻</p>
<h4 id="302--307--303-일시-리다이렉션">302 / 307 / 303: 일시 리다이렉션</h4>
<p>URI가 잠깐 바뀐 것뿐이라서 검색엔진 같은 곳에서 URL을 “영구 변경”으로 취급하면 안 된다.</p>
<ul>
<li><p>PRG(Post/Redirect/Get)
주문 완료: POST /orders
그 상태에서 새로고침하면?
→ 브라우저가 POST를 다시 날릴 수 있어서 중복 주문 터질 수 있다.</p>
<ul>
<li>그래서, POST로 주문 처리</li>
<li>응답을 302로 주고 Location으로 결과 페이지 안내</li>
<li>브라우저는 GET으로 결과 페이지 이동</li>
<li>이렇게 하면 새로고침해도 GET만 다시 날아가서 중복 주문을 막는다.</li>
</ul>
</li>
<li><p>302 Found
리다이렉트 시 GET으로 바뀔 수도 있음(MAY)
현실적으로 라이브러리/브라우저들이 기본값으로 많이 쓴다.</p>
</li>
<li><p>307 Temporary Redirect
302랑 목적은 비슷한데, 메서드가 절대 바뀌면 안된다.(MUST NOT)
즉, POST면 POST 유지.</p>
</li>
<li><p>303 See Other
리다이렉트 시 무조건 GET으로 변경한다.
PRG 패턴에서 의도가 명확해서 깔끔하다.</p>
</li>
</ul>
<p>결론적으로는</p>
<ul>
<li>“GET으로 바뀌어도 상관없다” → 302도 실무에서 흔히 OK</li>
<li>“무조건 GET으로 바꿀 거다(PRG)” → 303</li>
<li>“POST 유지해야 한다” → 307</li>
</ul>
<p>⸻</p>
<h4 id="304-not-modified-캐시용">304 Not Modified (캐시용)</h4>
<p>캐시를 위한 상태 코드를 이미한다.</p>
<ul>
<li>“서버 기준으로 리소스 안 바뀌었으니까, 너 로컬 캐시 써”</li>
<li>그래서 304 응답에는 바디가 있으면 안된다.
(캐시를 써야 하니까)</li>
</ul>
<hr>
<h3 id="4-4xx--5xx-에러">4. 4xx / 5xx: 에러</h3>
<h4 id="4xx-클라이언트">4xx: 클라이언트</h4>
<p>클라이언트 요청 자체가 잘못됐으니, 똑같이 재시도하면 보통 계속 실패한다.</p>
<ul>
<li>400 Bad Request
요청 파라미터/바디가 이상함, 스펙 안 맞음 등</li>
<li>401 Unauthorized
“인증이 필요함”
그리고 401이면 원칙적으로 WWW-Authenticate 헤더로 인증 방법도 알려줘야 한다.</li>
</ul>
<p>인증(Authentication): 너 누구야? (로그인)
인가(Authorization): 너 이거 할 권한 있어? (ADMIN 같은 권한)</p>
<ul>
<li>403 Forbidden
인증은 됐는데 권한이 없다.</li>
<li>404 Not Found
리소스가 없다.
또는 권한이 없다는 걸 숨기고 싶을 때 “없는 척”할 때도 종종 쓴다.</li>
</ul>
<p>⸻</p>
<h4 id="5xx-서버">5xx: 서버</h4>
<p>서버가 터졌거나 처리가 실패했음을 의미한다.
그래서 재시도하면 성공할 수도 있다. (서버가 복구되면)</p>
<ul>
<li>500 Internal Server Error
애매하면 500 (근데 실무에선 “애매하면 500”이 제일 위험한 말이기도 하다.)</li>
<li>503 Service Unavailable
서버 과부하/점검 등으로 잠깐 못 받은 경우이다.
Retry-After 헤더로 “몇 초 뒤에 다시 와” 같은 힌트를 줄 수도 있다.</li>
</ul>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 3. HTTP 메서드, HTTP 메서드 활용]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-3.-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%99%80-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-3.-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%99%80-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sun, 07 Dec 2025 09:00:24 GMT</pubDate>
            <description><![CDATA[<h3 id="1-http-api를-만들어보자">1. HTTP API를 만들어보자</h3>
<h4 id="요구사항">요구사항</h4>
<p>회원 관리 API를 만든다고 가정해보면,</p>
<ul>
<li>회원 목록 조회</li>
<li>회원 조회</li>
<li>회원 등록</li>
<li>회원 수정</li>
<li>회원 삭제</li>
</ul>
<p>이걸 어떻게 URI로 설계할까?</p>
<p>⸻</p>
<h4 id="핵심은-리소스-식별이다">핵심은 “리소스 식별”이다.</h4>
<p>제일 많이 하는 실수가 이거다.</p>
<pre><code>/createMember
/updateMember
/deleteMember</code></pre><p>이건 잘못된 접근이다.</p>
<p>우리가 식별해야 할 건 “행위”가 아니라 “리소스”다.</p>
<p>⸻</p>
<h4 id="리소스란">리소스란?</h4>
<ul>
<li>“회원 등록”이 리소스가 아니다.</li>
<li>“회원 수정”이 리소스가 아니다.</li>
<li>회원(Member)이라는 개념 자체가 리소스다.</li>
</ul>
<p>행위는 나중 문제다.</p>
<p>⸻</p>
<h4 id="uri-설계-기본-구조">URI 설계 기본 구조</h4>
<pre><code>GET    /members          → 회원 목록
GET    /members/{id}     → 회원 조회
POST   /members          → 회원 등록
PATCH  /members/{id}     → 회원 수정
DELETE /members/{id}     → 회원 삭제</code></pre><p>여기서 중요한 포인트:</p>
<ul>
<li>리소스는 명사</li>
<li>행위는 HTTP 메서드</li>
</ul>
<p>리소스와 행위는 반드시 분리하자
이 원칙이 REST의 핵심이다.</p>
<hr>
<h3 id="2-http-메서드---get-post">2. HTTP 메서드 - GET, POST</h3>
<h4 id="get">GET</h4>
<ul>
<li>리소스 조회</li>
<li>쿼리 파라미터로 데이터 전달</li>
</ul>
<p>Ex.</p>
<pre><code>GET /members?age=20</code></pre><p>GET은 바디 사용 가능은 하지만 거의 쓰지 않는다.</p>
<p>⸻</p>
<h4 id="post">POST</h4>
<p>POST는 한 줄로 정의하기 어렵다.</p>
<p>“이 리소스 URI가 정한 방식대로 요청 데이터를 처리해라.”</p>
<p>즉, 정해진 의미가 없다.</p>
<p>대표적인 사용</p>
<ul>
<li>새 리소스 생성</li>
<li>프로세스 처리</li>
<li>다른 메서드로 애매할 때</li>
</ul>
<p>Ex.</p>
<pre><code>POST /members
POST /orders/{id}/start-delivery</code></pre><p>POST vs PUT 차이</p>
<ul>
<li>POST → 서버가 URI 생성</li>
<li>PUT → 클라이언트가 URI 지정</li>
</ul>
<p>이 차이는 실무에서 매우 중요하다.</p>
<hr>
<h3 id="3-http-메서드--put-patch-delete">3. HTTP 메서드 -PUT, PATCH, DELETE</h3>
<h4 id="put">PUT</h4>
<p>리소스를 “완전히 대체”</p>
<ul>
<li>있으면 덮어쓰기</li>
<li>없으면 생성</li>
</ul>
<p>Ex.</p>
<pre><code>PUT /members/10</code></pre><p>클라이언트가 100번 리소스를 직접 지정한다.</p>
<p>⸻</p>
<h4 id="patch">PATCH</h4>
<p>부분 수정</p>
<p>Ex.</p>
<pre><code>PATCH /members/100
{
  &quot;age&quot;: 30
}</code></pre><p>일부 필드만 변경.</p>
<p>⸻</p>
<h4 id="delete">DELETE</h4>
<p>리소스 제거</p>
<p>Ex.</p>
<pre><code>DELETE /members/100</code></pre><hr>
<h3 id="4-http-메서드의-속성">4. HTTP 메서드의 속성</h3>
<h4 id="safe-안전">Safe (안전)</h4>
<p>리소스를 변경하지 않는다.</p>
<ul>
<li>GET</li>
<li>HEAD</li>
<li>OPTIONS</li>
<li>TRACE</li>
</ul>
<p>로그 쌓이는 건 고려하지 않았다. “리소스 변경 여부”만 본다.</p>
<p>⸻</p>
<h4 id="idempotent-멱등">Idempotent (멱등)</h4>
<p>여러 번 호출해도 결과가 같다.</p>
<ul>
<li>GET </li>
<li>PUT</li>
<li>DELETE</li>
</ul>
<p>POST는 아니다.</p>
<p>왜 중요할까?
→ 서버 타임아웃 시 재요청 가능 여부의 판단 기준이 된다.</p>
<p>⸻</p>
<h4 id="cacheable">Cacheable</h4>
<p>응답을 캐시할 수 있는가?</p>
<p>이론상:</p>
<ul>
<li>GET</li>
<li>HEAD</li>
<li>POST</li>
<li>PATCH</li>
</ul>
<p>실무상:</p>
<ul>
<li>거의 GET만 캐시</li>
</ul>
<hr>
<h3 id="5-클라이언트-→-서버-데이터-전송-방식">5. 클라이언트 → 서버 데이터 전송 방식</h3>
<h4 id="쿼리-파라미터-get">쿼리 파라미터 (GET)</h4>
<ul>
<li>정렬</li>
<li>검색</li>
<li>필터링</li>
</ul>
<p>⸻</p>
<h4 id="메시지-바디">메시지 바디</h4>
<ul>
<li>POST</li>
<li>PUT</li>
<li>PATCH</li>
</ul>
<p>회원가입, 주문, 수정 등</p>
<p>⸻</p>
<h4 id="html-form-전송">HTML Form 전송</h4>
<p>Content-Type</p>
<ul>
<li>application/x-www-form-urlencoded</li>
<li>multipart/form-data (파일 업로드)</li>
</ul>
<p>HTML Form은 GET, POST만 지원한다는 점이 중요하다.</p>
<p>⸻</p>
<h4 id="http-api-전송">HTTP API 전송</h4>
<ul>
<li>서버 ↔ 서버</li>
<li>앱 ↔ 서버</li>
<li>SPA ↔ API</li>
</ul>
<p>대부분</p>
<pre><code>Content-Type: application/json</code></pre><p>사실상 JSON이 표준이다.</p>
<hr>
<h3 id="6-http-api-설계-패턴">6. HTTP API 설계 패턴</h3>
<p>이 부분이 실무에서 제일 많이 쓰인다.</p>
<p>⸻</p>
<h4 id="컬렉션collection-방식">컬렉션(Collection) 방식</h4>
<pre><code>POST /members</code></pre><ul>
<li>클라이언트는 ID를 모른다.</li>
<li>서버가 ID를 생성한다.</li>
<li>Location 헤더로 알려준다.<pre><code>201 Created
Location: /members/100</code></pre>서버가 URI를 관리한다.</li>
</ul>
<p>⸻</p>
<h4 id="스토어store-방식">스토어(Store) 방식</h4>
<pre><code>PUT /files/star.jpg</code></pre><ul>
<li>클라이언트가 URI를 직접 지정한다.</li>
<li>파일 업로드 시스템에서 자주 사용한다.</li>
</ul>
<p>클라이언트가 URI를 관리한다.</p>
<p>⸻</p>
<h4 id="컨트롤-uri">컨트롤 URI</h4>
<p>REST스럽지 않지만 현실적인 방식이다.</p>
<pre><code>POST /members/{id}/delete
POST /orders/{id}/start-delivery</code></pre><p>HTML FORM 제약 때문에 자주 등장한다.</p>
<hr>
<h3 id="7-정리">7. 정리</h3>
<ul>
<li>URI는 리소스만 식별한다.</li>
<li>행위는 HTTP 메서드로 표현한다.</li>
<li>POST는 만능이지만 의미가 정해져 있지 않다.</li>
<li>PUT은 전체 대체, PATCH는 부분 변경이다.</li>
<li>멱등성과 안전성은 실무에서 매우 중요하다.</li>
<li>설계는 “일관성”이 가장 중요하다.</li>
</ul>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 2. HTTP 기본]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-2.-HTTP-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-2.-HTTP-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Sat, 06 Dec 2025 15:16:07 GMT</pubDate>
            <description><![CDATA[<h3 id="1-모든-것이-http">1. 모든 것이 HTTP</h3>
<p>요즘 웹에서 오가는 거의 모든 데이터는
결국 HTTP 메시지로 전송된다.</p>
<ul>
<li>HTML</li>
<li>이미지</li>
<li>영상</li>
<li>파일 다운로드</li>
<li>JSON, XML (API 통신)</li>
<li>서버 ↔ 서버 통신</li>
</ul>
<p>결론은 하나다.
: “HTTP 위에서 다 돌아간다.”</p>
<p>⸻</p>
<h4 id="http-역사-간단-정리">HTTP 역사 (간단 정리)</h4>
<ul>
<li>HTTP/0.9 → GET만 있음</li>
<li>HTTP/1.0 → 헤더 등장</li>
<li>HTTP/1.1 → 우리가 가장 중요하게 봐야 할 버전</li>
<li>HTTP/2 → 성능 개선 (멀티플렉싱)</li>
<li>HTTP/3 → TCP 대신 UDP 기반 (더 빠름)</li>
</ul>
<p>현재도 실무에서 기본은 HTTP/1.1 기반 이해다.
그 위에서 2, 3이 성능 개선을 더한 구조라고 보면 된다.</p>
<p>⸻</p>
<h4 id="기반-프로토콜">기반 프로토콜</h4>
<ul>
<li>HTTP/1.1 → TCP</li>
<li>HTTP/2 → TCP</li>
<li>HTTP/3 → UDP</li>
</ul>
<p>즉, HTTP는 전송 계층 위에서 동작하는 애플리케이션 프로토콜이다.</p>
<hr>
<h3 id="2-클라이언트-서버-구조">2. 클라이언트 서버 구조</h3>
<p>HTTP는 기본적으로</p>
<p>요청(Request) → 응답(Response)</p>
<p>클라이언트가 먼저 말 걸고
서버가 답한다.</p>
<p>서버는 “기다리는 입장”이고
클라이언트가 주도권을 가진다.</p>
<hr>
<h3 id="3-stateful-vs-stateless">3. Stateful vs Stateless</h3>
<p>이 부분이 HTTP에서 제일 중요하다고 느꼈다.</p>
<h4 id="stateless-무상태">Stateless (무상태)</h4>
<p>서버가 이전 요청을 기억하지 않는다.
각 요청은 완전히 독립적이다.</p>
<p>장점</p>
<ul>
<li>서버 확장성 ↑</li>
<li>아무 서버나 요청 처리 가능</li>
</ul>
<p>단점</p>
<ul>
<li>요청마다 필요한 정보를 다 담아야 한다</li>
</ul>
<p>⸻</p>
<h4 id="stateful-상태-유지">Stateful (상태 유지)</h4>
<p>서버가 클라이언트 상태를 기억한다.
Ex. 로그인 상태 유지</p>
<p>단점</p>
<ul>
<li>항상 같은 서버가 처리해야 한다.</li>
<li>중간에 서버 죽으면 상태 날아간다.</li>
</ul>
<p>⸻</p>
<h4 id="실무에서는">실무에서는?</h4>
<p>완전한 Stateless는 어렵다.</p>
<p>Ex.</p>
<ul>
<li>로그인 상태</li>
<li>장바구니</li>
</ul>
<p>그래서</p>
<ul>
<li>쿠키</li>
<li>세션</li>
<li>토큰(JWT)</li>
</ul>
<p>같은 방식으로 “최소한만 상태 유지”한다.</p>
<p>핵심은 가능한 Stateless로 설계하라.</p>
<p>트래픽이 폭증할 때 살아남으려면 무조건 Stateless가 유리하다.</p>
<hr>
<h3 id="4-비연결성-connectionless">4. 비연결성 (Connectionless)</h3>
<p>HTTP는 기본적으로
요청 → 응답 → 연결 종료</p>
<p>연결을 계속 유지하지 않는다.</p>
<p>장점</p>
<ul>
<li>서버 자원 절약</li>
<li>대규모 트래픽 대응 가능</li>
</ul>
<p>1시간에 수천 명이 접속해도
실제로 동시에 요청하는 건 훨씬 적다.</p>
<p>한계</p>
<ul>
<li>매번 TCP 연결 (3-way handshake)</li>
<li>HTML + CSS + JS + 이미지 등 여러 요청 발생</li>
</ul>
<p>그래서 등장한 게
HTTP 지속 연결(Persistent Connection)이다.</p>
<pre><code>연결 유지 → 여러 요청 처리 → 종료</code></pre><p>그리고 HTTP/2, HTTP/3에서 더 최적화되었다.</p>
<hr>
<h3 id="5-http-메시지-구조">5. HTTP 메시지 구조</h3>
<p>HTTP 메시지는 생각보다 단순하다.</p>
<pre><code>start-line
header
(empty line)
message body</code></pre><p>⸻</p>
<h4 id="요청-메시지">요청 메시지</h4>
<pre><code>GET /search?q=hello HTTP/1.1
Host: www.google.com</code></pre><p>구성:</p>
<pre><code>METHOD SP PATH SP VERSION</code></pre><p>Ex.</p>
<ul>
<li>GET → 조회</li>
<li>POST → 처리</li>
</ul>
<p>⸻</p>
<h4 id="응답-메시지">응답 메시지</h4>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/html</code></pre><p>구성:</p>
<pre><code>VERSION SP STATUS_CODE SP REASON</code></pre><p>상태코드 Ex.</p>
<ul>
<li>200 → 성공</li>
<li>400 → 클라이언트 오류</li>
<li>500 → 서버 오류</li>
</ul>
<p>⸻</p>
<h4 id="헤더">헤더</h4>
<p>헤더에는 부가 정보가 들어간다.</p>
<p>Ex.</p>
<ul>
<li>Content-Type</li>
<li>Content-Length</li>
<li>Authorization</li>
<li>Cache-Control</li>
</ul>
<p>사실 HTTP 헤더가 거의 모든 걸 담당한다고 봐도 된다.</p>
<p>⸻</p>
<h4 id="바디">바디</h4>
<p>실제 데이터가 들어가는 곳.</p>
<ul>
<li>HTML</li>
<li>JSON</li>
<li>이미지</li>
<li>파일</li>
</ul>
<p>바이트로 표현 가능한 모든 데이터 가능.</p>
<hr>
<h3 id="6-http의-진짜-강점">6. HTTP의 진짜 강점</h3>
<p>HTTP는</p>
<ul>
<li>단순하다.</li>
<li>확장 가능하다.</li>
<li>텍스트 기반이다.</li>
<li>어디서든 사용할 수 있다.</li>
</ul>
<p>그래서 
“HTTP는 웹 프로토콜”이 아니라
“사실상 인터넷 기본 프로토콜”이 되어버렸다.</p>
<hr>
<h3 id="7-정리">7. 정리</h3>
<ul>
<li>HTTP는 모든 데이터 전송의 중심이다.</li>
<li>클라이언트-서버 구조다.</li>
<li>Stateless가 핵심이다.</li>
<li>비연결성 기반이다.</li>
<li>메시지 구조는 단순하다.</li>
<li>단순하지만 확장 가능해서 오래 살아남았다.</li>
</ul>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HTTP 웹 기본 지식] 1. 인터넷 네트워크, URI와 웹 브라우저 요청 흐름]]></title>
            <link>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-1.-%EC%9D%B8%ED%84%B0%EB%84%B7-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-URI%EC%99%80-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@dev_geonwoo/HTTP-%EC%9B%B9-%EA%B8%B0%EB%B3%B8-%EC%A7%80%EC%8B%9D-1.-%EC%9D%B8%ED%84%B0%EB%84%B7-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-URI%EC%99%80-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Fri, 05 Dec 2025 11:18:25 GMT</pubDate>
            <description><![CDATA[<h3 id="1-인터넷-통신">1. 인터넷 통신</h3>
<p>인터넷 통신은 결국
멀리 떨어진 컴퓨터끼리 데이터를 주고받는 과정이다.</p>
<p>구조는 단순하다.</p>
<p>요청(Request) → 응답(Response)</p>
<p>편지처럼 보내고, 전화처럼 묻고 답한다.
웹에서 버튼 하나 눌렀을 때도 이 구조는 그대로 유지된다.</p>
<hr>
<h3 id="2-ip-internet-protocol">2. IP (Internet Protocol)</h3>
<p>IP는 인터넷에서 데이터를 전달하기 위한 기본 규칙이다.</p>
<p>IP의 역할</p>
<ul>
<li>목적지 IP 주소로 데이터 전달</li>
<li>데이터를 패킷(Packet) 단위로 쪼개서 전송</li>
</ul>
<h4 id="패킷이란">패킷이란?</h4>
<p>데이터를 작은 조각으로 나눈 통신 단위
(출발지 IP, 목적지 IP 같은 정보가 같이 들어간다)</p>
<p>⸻</p>
<h4 id="ip의-한계">IP의 한계</h4>
<p>IP는 단순하다. 그래서 한계도 명확하다.</p>
<h5 id="비연결성">비연결성</h5>
<p>상대 서버가 꺼져 있어도 그냥 보낸다.
→ 받는 쪽이 준비됐는지 모른다.</p>
<h5 id="비신뢰성">비신뢰성</h5>
<ul>
<li>중간에 패킷이 사라지면?</li>
<li>순서가 뒤죽박죽이면?</li>
</ul>
<p>IP는 책임지지 않는다.</p>
<h5 id="프로그램-구분-불가">프로그램 구분 불가</h5>
<p>같은 IP에서 여러 프로그램이 동시에 통신하면?
어디로 보내야 할지 구분하지 못한다.</p>
<p>그래서 TCP가 등장한다.</p>
<hr>
<h3 id="3-tcp-vs-udp">3. TCP vs UDP</h3>
<h4 id="인터넷-프로토콜-4계층">인터넷 프로토콜 4계층</h4>
<pre><code>애플리케이션 계층 (HTTP, FTP 등)
전송 계층 (TCP, UDP)
인터넷 계층 (IP)
네트워크 인터페이스 계층</code></pre><p>우리가 개발자로서 주로 다루는 건
-&gt; HTTP (애플리케이션 계층)</p>
<p>그 아래에서 실제 전송을 책임지는 게
-&gt; TCP / UDP</p>
<h4 id="tcp-transmission-control-protocol">TCP (Transmission Control Protocol)</h4>
<p>TCP는 한마디로
“믿을 수 있는 통신”</p>
<h5 id="연결지향-3-way-handshake">연결지향 (3-way handshake)</h5>
<pre><code>SYN → SYN+ACK → ACK</code></pre><p>연결이 성립된 뒤에 데이터 전송 시작.</p>
<h5 id="데이터-전달-보증">데이터 전달 보증</h5>
<p>보냈으면 → 잘 받았는지 확인한다.</p>
<h5 id="순서-보장">순서 보장</h5>
<p>패킷 순서가 꼬이면 재전송 요청한다.</p>
<p>정리하면</p>
<ul>
<li>신뢰성 O</li>
<li>대부분의 웹 통신은 TCP 기반</li>
</ul>
<p>⸻</p>
<h4 id="udp">UDP</h4>
<p>UDP는 TCP의 반대 느낌이다.</p>
<ul>
<li>연결 과정 없음</li>
<li>전달 보증 없음</li>
<li>순서 보장 없음</li>
</ul>
<p>대신</p>
<ul>
<li>단순하다</li>
<li>빠르다</li>
</ul>
<p>그래서</p>
<ul>
<li>실시간 스트리밍</li>
<li>게임</li>
<li>VoIP</li>
</ul>
<p>같은 곳에서 사용된다.
-&gt; 요즘 HTTP/3는 UDP 기반이다.</p>
<hr>
<h3 id="4-port">4. PORT</h3>
<p>IP가 “건물 주소”라면
PORT는 “호수”다.</p>
<p>한 컴퓨터에서 여러 프로그램이 동시에 통신하려면
구분이 필요하다.</p>
<p>포트 범위</p>
<pre><code>0 ~ 65535</code></pre><p>잘 알려진 포트 (0~1023)</p>
<ul>
<li>HTTP → 80</li>
<li>HTTPS → 443</li>
<li>FTP → 21</li>
<li>TELNET → 23</li>
</ul>
<p>보통 개발할 때는 8080, 9090 이런 걸 많이 쓴다.</p>
<hr>
<h3 id="5-dns">5. DNS</h3>
<p>IP 주소는 사람이 기억하기 어렵다.</p>
<pre><code>200.200.200.2</code></pre><p>그래서 등장한 게 DNS.
도메인 → IP 변환 시스템이다.</p>
<p>예:</p>
<pre><code>google.com → 142.xxx.xxx.xxx</code></pre><p>전화번호부 같은 역할이다.</p>
<hr>
<h3 id="6-uri--url--urn">6. URI / URL / URN</h3>
<p>개발하다 보면 헷갈리는 개념이다.</p>
<h4 id="uri-uniform-resource-identifier">URI (Uniform Resource Identifier)</h4>
<p>: 리소스를 식별하는 통합 방식</p>
<p>URI는 큰 개념이고
그 안에 URL, URN이 포함된다.</p>
<p>⸻</p>
<h4 id="url-locator">URL (Locator)</h4>
<p>리소스의 위치</p>
<pre><code>https://example.com:8080/items/1?type=book#detail</code></pre><p>구성요소</p>
<pre><code>scheme://host:port/path?query#fragment</code></pre><ul>
<li>scheme → http, https</li>
<li>host → 도메인</li>
<li>port → 생략 가능</li>
<li>path → 리소스 경로</li>
<li>query → ?key=value</li>
<li>fragment → # (서버로 안 감)</li>
</ul>
<p>⸻</p>
<h4 id="urn-name">URN (Name)</h4>
<p>리소스의 이름</p>
<pre><code>urn:isbn:8060777331</code></pre><p>위치와 무관한 고유 이름.</p>
<p>실무에선 거의 URL만 사용한다고 보면 된다.</p>
<hr>
<h3 id="7-웹-브라우저-요청-흐름">7. 웹 브라우저 요청 흐름</h3>
<p>브라우저에 주소 입력하면 무슨 일이 벌어질까?</p>
<h4 id="http-요청-메시지-생성">HTTP 요청 메시지 생성</h4>
<pre><code>GET /search?q=hello HTTP/1.1
Host: www.google.com</code></pre><h4 id="tcp-연결">TCP 연결</h4>
<ul>
<li>IP + PORT 기반 연결</li>
<li>3-way handshake</li>
</ul>
<h4 id="데이터-전송">데이터 전송</h4>
<p>HTTP 메시지를 TCP/IP 패킷에 담아 보낸다.</p>
<h4 id="서버-응답">서버 응답</h4>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/html</code></pre><p>HTML 데이터를 받아서 브라우저가 렌더링한다.</p>
<h3 id="8-정리">8. 정리</h3>
<ul>
<li>IP는 단순 전송</li>
<li>TCP는 신뢰성 보장</li>
<li>PORT는 프로그램 구분</li>
<li>DNS는 도메인 → IP 변환</li>
<li>URL은 리소스 위치</li>
<li>브라우저는 결국 HTTP 메시지를 TCP 위에서 주고받는다</li>
</ul>
<hr>
<p>출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 9. 빈 스코프]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-9.-%EB%B9%88-%EC%8A%A4%EC%BD%94%ED%94%84</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-9.-%EB%B9%88-%EC%8A%A4%EC%BD%94%ED%94%84</guid>
            <pubDate>Wed, 03 Dec 2025 09:32:42 GMT</pubDate>
            <description><![CDATA[<p>빈 스코프 — “이 빈, 언제까지 살아있냐?”</p>
<p>지금까지는 스프링 빈이 컨테이너 뜰 때 생성돼서, 컨테이너 죽을 때까지 살아있는 걸 기본으로 봤다.
이게 가능한 이유는 스프링 빈 기본이 싱글톤 스코프라서 그렇다.</p>
<p>여기서 <strong>스코프(scope)</strong>는 말 그대로
“이 빈이 존재할 수 있는 범위(수명)”를 뜻한다.</p>
<hr>
<h3 id="1-스프링이-제공하는-대표-스코프들">1. 스프링이 제공하는 대표 스코프들</h3>
<ul>
<li>singleton: 기본값. 컨테이너 시작~종료까지 쭉.</li>
<li>prototype: 요청할 때마다 새로 만들고, 만들고 주입까지만 하고 관리는 안한다.</li>
<li>웹 스코프<ul>
<li>request: HTTP 요청 1개 동안</li>
<li>session: 세션 동안</li>
<li>application: 서블릿 컨텍스트 동안</li>
<li>(websocket 등도 있다.)</li>
</ul>
</li>
</ul>
<p>등록 예시는 이런 느낌:</p>
<ul>
<li>컴포넌트 스캔: @Component + @Scope(&quot;prototype&quot;)</li>
<li>수동 등록: @Bean + @Scope(&quot;prototype&quot;)</li>
</ul>
<hr>
<h3 id="2-프로토타입-스코프--요청할-때마다-새로-뽑는-빈">2. 프로토타입 스코프 — “요청할 때마다 새로 뽑는 빈”</h3>
<p>싱글톤은 getBean()을 여러 번 해도 항상 같은 인스턴스를 준다.
근데 프로토타입은 getBean()할 때마다 항상 새 인스턴스를 만든다.</p>
<p>중요한 포인트</p>
<p>프로토타입 빈은 스프링이 여기까지만 해준다:</p>
<ul>
<li>생성</li>
<li>의존관계 주입</li>
<li>초기화</li>
</ul>
<p>그리고 클라이언트에 넘겨준 다음부터는
“이제 너가 알아서 관리해~”
모드가 된다.</p>
<p>그래서 프로토타입 빈은 @PreDestroy 같은 종료 콜백이 자동으로 안 불린다.
(종료 책임은 빈을 받아간 쪽이 가져감)</p>
<hr>
<h3 id="3-싱글톤--프로토타입-같이-쓰면-터지는-함정">3. 싱글톤 + 프로토타입 같이 쓰면 터지는 함정</h3>
<p>“싱글톤 안에 프로토타입 주입하면, 매번 새로 생성되겠지?”</p>
<p>아니다.</p>
<p>싱글톤 빈은 생성 시점에 딱 한 번 주입을 받는다.
즉, 프로토타입도 주입 시점에 한 번 생성된 그 인스턴스를 싱글톤이 계속 들고 있는다.</p>
<p>그래서 결과적으로는:</p>
<ul>
<li>“프로토타입을 쓴다고 생각했는데”</li>
<li>실제로는 “싱글톤이 프로토타입 하나를 평생 들고 있음”</li>
</ul>
<p>이 상황이 된다.</p>
<hr>
<h3 id="4-해결-쓸-때마다-새로-달라를-구현하는-방법">4. 해결: “쓸 때마다 새로 달라”를 구현하는 방법</h3>
<p>핵심은 단 하나
진짜 프로토타입 조회를 “주입 시점”이 아니라 “사용 시점”으로 미루기 이다.</p>
<p>⸻</p>
<h4 id="컨테이너에-직접-매번-요청acgetbean">컨테이너에 직접 매번 요청(ac.getBean)</h4>
<p>가장 단순한 방법인데, 단점도 확실하다.</p>
<ul>
<li>싱글톤 빈이 ApplicationContext를 들고 있어야 함</li>
<li>컨테이너 종속 + 단위테스트 불편</li>
</ul>
<p>그래서 “진짜 필요한 기능(DL)만” 뽑아 쓰는 방식이 나온다.</p>
<p>⸻</p>
<h4 id="objectprovider-스프링-제공">ObjectProvider (스프링 제공)</h4>
<ul>
<li>objectProvider.getObject() 호출 순간에 빈을 찾아준다.</li>
<li>DL(조회)만 딱 제공한다.</li>
<li>테스트도 상대적으로 편해진다.</li>
</ul>
<p>⸻</p>
<h4 id="jsr-330-provider-자바-표준">JSR-330 Provider (자바 표준)</h4>
<ul>
<li>부트 3 기준 jakarta.inject.Provider</li>
<li>provider.get() 하나로 끝이다. (진짜 단순)</li>
</ul>
<p>⸻</p>
<p>결론적으로</p>
<ul>
<li>스프링 안에서만 살 거면 ObjectProvider가 편하다.</li>
<li>컨테이너 독립성이 필요하면 Provider(자바 표준) 쪽이 깔끔하다.</li>
</ul>
<p>그리고 현실적으로는 프로토타입 자체를 자주 쓰진 않고, “필요할 때 조회(DL) 지연” 용도로 Provider류가 더 많이 쓰인다.</p>
<hr>
<h3 id="5-웹-스코프--웹-요청세션-단위로-살아있는-빈">5. 웹 스코프 — “웹 요청/세션 단위로 살아있는 빈”</h3>
<p>웹 스코프는 특징이 좀 다르다.</p>
<ul>
<li>웹 환경에서만 동작</li>
<li>프로토타입과 달리 스프링이 종료 시점까지 관리
→ 그래서 종료 메서드도 호출된다.</li>
</ul>
<p>대표는 request 스코프가 있다.</p>
<hr>
<h3 id="6-request-스코프-예제에서-왜-에러가-났나">6. request 스코프 예제에서 왜 에러가 났나?</h3>
<p>request 스코프 빈은 요청이 와야 생성될 수 있다.</p>
<p>근데 싱글톤(컨트롤러/서비스)은 앱 뜰 때 만들어지면서 주입이 필요하니까
“아직 요청도 없는데 request 빈을 어떻게 주입해?”</p>
<p>이러면서 터진다.</p>
<p>⸻</p>
<h4 id="해결-1-provider로-지연-생성">해결 1. Provider로 지연 생성</h4>
<p>컨트롤러/서비스에서 ObjectProvider.getObject()를 요청 중에 호출하게 만들면</p>
<ul>
<li>요청이 들어온 상태라 request 빈 생성 가능</li>
<li>같은 요청이면 같은 request 빈 반환</li>
</ul>
<p>⸻</p>
<h4 id="해결-2-프록시proxymode로-가짜를-먼저-주입">해결 2. 프록시(proxyMode)로 “가짜를 먼저 주입”</h4>
<p>@Scope(value=&quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS) 같은 걸 붙이면</p>
<ul>
<li>스프링이 진짜 MyLogger 대신 가짜 프록시를 먼저 주입해둔다.</li>
<li>그리고 실제로 메서드 호출하는 순간에 “지금 요청 기준의 진짜 request 빈”을 찾아서 위임해준다.</li>
</ul>
<p>즉, 코드 입장에서는 그냥 myLogger.log() 똑같이 쓰는데
뒤에서 프록시가 알아서 “요청 시점에 진짜를 연결”해주는 느낌을 받는다.</p>
<hr>
<h3 id="7-정리">7. 정리</h3>
<ul>
<li>기본은 싱글톤 스코프라서 빈이 오래 산다.</li>
<li>프로토타입은 요청할 때마다 새로 만들어주긴 하는데, 스프링이 관리하지 않는다.(그래서 종료 콜백도 없다.)</li>
<li>싱글톤에 프로토타입을 그냥 주입하면 “매번 새로”가 아니라 “한 번 주입된 거 계속”이라는 함정이 있다.<ul>
<li>그래서 Provider / 프록시처럼 조회 시점을 늦추는 방식이 필요하다.</li>
</ul>
</li>
<li>웹 스코프는 request/session 같은 “웹 단위 수명”이고, 스프링이 끝까지 관리해서 종료도 챙겨준다.<ul>
<li>대신 request 스코프는 “요청이 와야 생성 가능”이라 Provider/프록시가 자주 등장한다.</li>
</ul>
</li>
</ul>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 8. 빈 생명주기 콜백]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-8.-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-8.-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1</guid>
            <pubDate>Tue, 23 Sep 2025 07:46:50 GMT</pubDate>
            <description><![CDATA[<p>빈 생명주기 콜백 — “언제 연결하고, 언제 정리하냐” 문제</p>
<p>DB 커넥션 풀이나 외부 API 소켓 같은 건 보통</p>
<ul>
<li>앱 시작할 때 연결 미리 해두고</li>
<li>앱 꺼질 때 연결 깔끔하게 끊는</li>
</ul>
<p>이런 흐름이 필요하다.
그래서 스프링에서도 “초기화 / 종료” 타이밍을 잡는 기능이 따로 있다.</p>
<hr>
<h3 id="1-스프링-빈은-이렇게-산다">1. 스프링 빈은 이렇게 산다</h3>
<p>스프링 빈 라이프사이클은 대충 이렇게 흘러간다.</p>
<ul>
<li>컨테이너 생성</li>
<li>빈 생성</li>
<li>의존관계 주입</li>
<li>초기화 콜백(여기서 연결 같은 거)</li>
<li>사용</li>
<li>소멸 직전 콜백(여기서 정리/해제)</li>
<li>컨테이너 종료</li>
</ul>
<p>여기서 포인트는 하나
: 초기화는 “생성 직후”가 아니라, “주입 완료 후”에 해야 한다.</p>
<p>예를 들어 NetworkClient 같은 애가 있고 url이 setter로 들어오는 구조면
생성자 시점에는 url이 없어서 connect() 때리면 당연히 이상해진다.</p>
<hr>
<h3 id="2-생성자-vs-초기화는-분리하는-게-보통-더-낫다">2. 생성자 vs 초기화는 분리하는 게 보통 더 낫다</h3>
<ul>
<li>생성자: “필수 값 받고 객체 만들기”</li>
<li>초기화: “만들어진 값으로 무거운 작업(연결/초기 세팅)하기”</li>
</ul>
<p>그래서 생성자에서 커넥션 연결 같은 걸 해버리면
테스트/유지보수 쪽에서 피곤해질 수 있다.</p>
<p>(물론 진짜 가벼운 초기화면 생성자에서 한 방에 해도 괜다.)</p>
<hr>
<h3 id="3-스프링이-생명주기-콜백을-지원하는-방식-3개">3. 스프링이 생명주기 콜백을 지원하는 방식 3개</h3>
<p>스프링이 “주입 끝났어<del>” / “이제 종료할게</del>” 알려주는 방법은 크게 3가지가 있다.</p>
<ul>
<li>인터페이스 구현 (InitializingBean, DisposableBean)</li>
<li>@Bean(initMethod=..., destroyMethod=...)로 설정에서 지정</li>
<li>애노테이션 @PostConstruct, @PreDestroy</li>
</ul>
<p>결론부터 말하면 실무 결론은 거의 정해져 있다.</p>
<ul>
<li>기본은 @PostConstruct / @PreDestroy</li>
<li>외부 라이브러리는 @Bean 옵션으로 처리</li>
</ul>
<p>⸻</p>
<h4 id="인터페이스-방식-요즘은-거의-안-씀">인터페이스 방식 (요즘은 거의 안 씀)</h4>
<ul>
<li>InitializingBean.afterPropertiesSet() : 초기화 타이밍</li>
<li>DisposableBean.destroy() : 종료 타이밍</li>
</ul>
<p>동작은 잘 되는데 단점이 좀 크다.</p>
<ul>
<li>스프링 전용 인터페이스라 스프링에 종속</li>
<li>메서드 이름도 고정이라 내 스타일대로 못 짬</li>
<li>외부 라이브러리처럼 “코드를 못 고치는 대상”에는 적용 불가</li>
</ul>
<p>그래서 “스프링 초창기 방식” 정도로만 알고 넘어가면 된다.</p>
<p>⸻</p>
<h4 id="bean으로-initdestroy-메서드-지정-외부-라이브러리용-치트키">@Bean으로 init/destroy 메서드 지정 (외부 라이브러리용 치트키)</h4>
<p>설정 클래스에서 이렇게 지정할 수 있다.</p>
<ul>
<li>@Bean(initMethod=&quot;init&quot;, destroyMethod=&quot;close&quot;)</li>
</ul>
<p>이 방식의 장점은 확실하다.</p>
<ul>
<li>메서드 이름 내 마음대로</li>
<li>빈 코드가 스프링에 의존하지 않음</li>
<li>외부 라이브러리에도 적용 가능 (이게 핵심)</li>
</ul>
<p>그리고 destroyMethod 추론(inferred)에는 재밌는 기본 기능이 있다.</p>
<ul>
<li>destroyMethod 기본값이 (inferred)라서
close()나 shutdown() 같은 이름이 있으면 스프링이 알아서 호출해준다.</li>
<li>그래서 대부분은 destroyMethod를 굳이 안 써도 잘 동작한다.</li>
<li>추론 싫으면 destroyMethod=&quot;&quot;로 끄면 된다.</li>
</ul>
<p>⸻</p>
<h4 id="postconstruct--predestroy-요즘-정석">@PostConstruct / @PreDestroy (요즘 정석)</h4>
<p>이게 제일 깔끔하고 요즘 스프링에서 제일 권장하는 방식이다.</p>
<ul>
<li>@PostConstruct : 주입 끝난 후 초기화</li>
<li>@PreDestroy : 컨테이너 종료 직전 정리</li>
</ul>
<p>좋은 점:</p>
<ul>
<li>애노테이션만 붙이면 끝이다.</li>
<li>스프링 전용이 아니라 jakarta.annotation (JSR-250) 기반이라 표준 느낌이다.</li>
<li>컴포넌트 스캔이랑도 찰떡이라 실무랑 궁합이 좋다.</li>
</ul>
<p>단점 하나:</p>
<ul>
<li>외부 라이브러리는 코드에 애노테이션을 못 붙이니까 적용이 불가능하다.
→ 그때는 @Bean 옵션으로 처리</li>
</ul>
<hr>
<h3 id="4-정리">4. 정리</h3>
<ul>
<li>내 코드(컴포넌트 스캔): @PostConstruct / @PreDestroy</li>
<li>외부 라이브러리: @Bean(initMethod, destroyMethod)</li>
</ul>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 7. 의존관계 자동 주입]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-7.-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-7.-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Tue, 09 Sep 2025 11:08:42 GMT</pubDate>
            <description><![CDATA[<p>다양한 의존관계 주입 방식 정리 (결론: 생성자 주입이 기본값)</p>
<p>스프링에서 의존관계 주입(DI) 방식은 크게 4개가 있다.</p>
<ul>
<li>생성자 주입</li>
<li>수정자(setter) 주입</li>
<li>필드 주입</li>
<li>일반 메서드 주입</li>
</ul>
<p>근데 현실적으로는 “다 알아야 한다”기보단, 왜 생성자 주입을 기본으로 쓰는지가 더 중요했다.</p>
<hr>
<h3 id="1-주입-방식-4종류--느낌만-잡기">1. 주입 방식 4종류 — 느낌만 잡기</h3>
<h4 id="생성자-주입">생성자 주입</h4>
<p>말 그대로 생성자로 주입 받는 방식.</p>
<ul>
<li>객체 생성 시점에 딱 한 번 주입이 끝난다.</li>
<li>그래서 “필수 의존관계 + 불변”에 제일 잘 맞는다.</li>
<li>생성자가 1개면 @Autowired 생략해도 된다(스프링 빈 한정).</li>
</ul>
<p>개인적으로 생성자 주입은 “주입 누락이 발생할 수 없는 구조”라서 마음이 편하다.</p>
<p>⸻</p>
<h4 id="수정자setter-주입">수정자(setter) 주입</h4>
<p>setter로 주입 받는 방식.</p>
<ul>
<li>“선택적으로 주입해도 되는 값”</li>
<li>“나중에 바뀔 수도 있는 값”
에 쓰는게 맞다.</li>
</ul>
<p>다만 setter를 public으로 열어두면,
누가 실수로 바꿔버릴 수 있다는 리스크가 생긴다.</p>
<p>⸻</p>
<h4 id="필드-주입">필드 주입</h4>
<p>처음 보면 진짜 편하다.</p>
<pre><code>@Autowired
private MemberRepository memberRepository;</code></pre><p>근데 문제는 치명적임.</p>
<ul>
<li>외부에서 주입을 바꿀 수가 없음 → 테스트가 너무 힘듦</li>
<li>스프링 없으면 그냥 아무것도 못함</li>
</ul>
<p>그래서 결론은
: “진짜 특별한 케이스 아니면 실무에서는 거의 안 쓰는 게 맞다.”</p>
<p>예외로는</p>
<ul>
<li>스프링 통합 테스트 코드</li>
<li>설정 클래스에서 특수한 목적</li>
</ul>
<p>정도?</p>
<p>⸻</p>
<h4 id="거의-안-씀-일반-메서드-주입">(거의 안 씀) 일반 메서드 주입</h4>
<p>메서드로 여러 파라미터 한 번에 주입 가능하긴 한데,
진짜 거의 못 본다.</p>
<hr>
<h3 id="2-옵션-처리--빈이-없어도-살아야-할-때">2. 옵션 처리 — 빈이 없어도 살아야 할 때</h3>
<p>기본적으로 @Autowired는 대상이 없으면 터진다.
근데 “있으면 쓰고, 없으면 말고”인 경우가 있다.</p>
<p>그럴 땐 옵션 처리를 한다.</p>
<ul>
<li>@Autowired(required=false)
→ 아예 메서드 호출 자체가 안 됨</li>
<li>@Nullable
→ 없으면 null 들어옴</li>
<li>Optional&lt;&gt;
→ 없으면 Optional.empty()</li>
</ul>
<p>나는 개인적으로 Optional이 제일 명확하다고 느꼈다.
(“없을 수도 있음”을 코드로 드러내니까)</p>
<hr>
<h3 id="3-생성자-주입을-기본으로-선택해야-하는-이유">3. 생성자 주입을 기본으로 선택해야 하는 이유</h3>
<p>이 챕터의 결론은 거의 이거다
: “기본은 생성자 주입, 옵션 필요하면 setter 주입”</p>
<p>왜냐면 생성자 주입은 장점이 너무 확실함.</p>
<h4 id="불변immutable-설계-가능">불변(immutable) 설계 가능</h4>
<p>대부분의 의존관계는 애플리케이션 실행 후 바뀌면 안 된다.</p>
<p>setter 주입은 set 메서드를 열어놔야 해서
“바뀔 가능성” 자체를 남겨둔다.</p>
<p>생성자 주입은 한 번 들어오면 끝이라
불변 구조가 자연스럽게 된다.</p>
<p>⸻</p>
<h4 id="주입-누락이-실행-전에-잡힌다">주입 누락이 “실행 전에” 잡힌다</h4>
<p>setter/필드 주입은 스프링이 주입해주니까 괜찮아 보이지만
스프링 없이 단위 테스트 하면…</p>
<ul>
<li>주입 누락 → 런타임 NPE</li>
</ul>
<p>이게 진짜 무섭다.</p>
<p>반면 생성자 주입은
필수 값 누락하면 애초에 생성이 안 되니까
IDE/컴파일 단계에서 바로 걸린다.</p>
<p>컴파일 에러는 세상에서 제일 착한 에러다.</p>
<p>⸻</p>
<h4 id="final-키워드-사용-가능">final 키워드 사용 가능</h4>
<p>생성자 주입은 필드를 final로 만들 수 있다.</p>
<p>그래서 “혹시라도 값이 안 들어온 상태”를
컴파일이 막아준다.</p>
<hr>
<h3 id="4-롬복까지-쓰면-거의-완성">4. 롬복까지 쓰면 거의 완성</h3>
<p>실무에서 제일 흔한 패턴:</p>
<ul>
<li>생성자 1개</li>
<li>@Autowired 생략</li>
<li>@RequiredArgsConstructor로 생성자 자동 생성<pre><code>@RequiredArgsConstructor
@Component
public class OrderServiceImpl implements OrderService {
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
}</code></pre>이게 깔끔하고, 안전하고, 테스트도 편하다.</li>
</ul>
<hr>
<h3 id="5-같은-타입-빈이-2개면-여기서부터-실전">5. 같은 타입 빈이 2개면? (여기서부터 실전)</h3>
<p>@Autowired는 기본이 “타입으로 찾기”라서
같은 타입 빈이 두 개면 터진다.</p>
<p>이때 해결 방법이 3개가 있다.</p>
<p>⸻</p>
<h3 id="6-해결책-3종-세트-이름매칭--qualifier--primary">6. 해결책 3종 세트: 이름매칭 / @Qualifier / @Primary</h3>
<h4 id="필드명파라미터명-매칭">필드명(파라미터명) 매칭</h4>
<p>스프링이 타입으로 먼저 찾다가,
후보가 2개 이상이면 “이름”으로 한 번 더 좁힌다.</p>
<ul>
<li>타입 매칭</li>
<li>후보 여러 개면 → 필드명/파라미터명 = 빈 이름 매칭</li>
</ul>
<p>⸻</p>
<h4 id="qualifier">@Qualifier</h4>
<p>“이거로 주입해”를 명시하는 방식.</p>
<pre><code>@Qualifier(&quot;mainDiscountPolicy&quot;)</code></pre><p>흐름은:</p>
<ul>
<li>Qualifier끼리 먼저 매칭</li>
<li>없으면 빈 이름 매칭</li>
<li>그래도 없으면 예외</li>
</ul>
<p>개인적으로는 @Qualifier는 정말 구분자 용도로만 쓰는 게 깔끔하다고 생각한다.
(“없으면 빈 이름으로도 찾아준다”는 동작까지 기대하면 헷갈릴 수 있다.)</p>
<p>⸻</p>
<h4 id="primary">@Primary</h4>
<p>여러 개면 “기본값”으로 이걸 우선 사용하게 하는 것.</p>
<ul>
<li>자주 쓰는 메인 구현체에 @Primary</li>
<li>가끔 쓰는 예외 구현체는 @Qualifier로 찍어서 가져오기</li>
</ul>
<p>이 패턴이 제일 현실적인 느낌이었다.</p>
<p>그리고 우선순위는
: @Qualifier가 @Primary보다 더 강하다. (더 구체적 지시가 우선)</p>
<hr>
<h3 id="7-qualifier-문자열은-위험하다-→-애노테이션으로-만들기">7. @Qualifier 문자열은 위험하다 → 애노테이션으로 만들기</h3>
<p>@Qualifier(&quot;mainDiscountPolicy&quot;)는 문자열이라 
오타나도 컴파일이 못 잡는다.</p>
<p>그래서 커스텀 애노테이션으로 묶어서
타입 안정성을 챙길 수 있다.</p>
<p>(다만 목적 없이 남발하면 오히려 복잡해질 수 있다.)</p>
<hr>
<h3 id="8-listmap으로-모든-구현체-받기-전략-패턴">8. List/Map으로 “모든 구현체” 받기 (전략 패턴)</h3>
<p>가끔은 “한 개만 고르는 게 아니라 다 필요”한 경우가 있다.</p>
<p>예: 할인 정책을 사용자가 선택</p>
<p>이럴 때는</p>
<ul>
<li>List<DiscountPolicy></li>
<li>Map&lt;String, DiscountPolicy&gt;</li>
</ul>
<p>로 한 번에 주입 가능.</p>
<p>특히 Map은</p>
<ul>
<li>key: 빈 이름</li>
<li>value: 구현체</li>
</ul>
<p>라서 전략 패턴 구현할 때 진짜 편하다.</p>
<hr>
<h3 id="9-자동-vs-수동-빈-등록-실무-기준">9. 자동 vs 수동 빈 등록, 실무 기준</h3>
<p>요약하면 이 느낌:</p>
<ul>
<li>업무 로직(Controller/Service/Repository)
→ 자동 등록이 기본 (컴포넌트 스캔)</li>
<li>기술 지원 로직(AOP, 공통 로그, DB 커넥션 등)
→ 수동 등록 고려 (설정에서 한눈에 보이게)</li>
</ul>
<p>그리고 추가로,
다형성을 많이 쓰는 로직(예: Map으로 전략 주입)은
“어떤 빈들이 들어오는지”가 코드만 보면 잘 안 보일 수 있어서
수동 등록 or 패키지로 모아두는 식으로 “한눈에 보이게” 정리하는 게 좋다.</p>
<hr>
<h3 id="10-한-줄-요약">10. 한 줄 요약</h3>
<ul>
<li>DI는 여러 방식이 있지만 생성자 주입이 기본값</li>
<li>빈이 여러 개면 @Primary(기본값) + @Qualifier(예외 케이스) 조합이 제일 실전적</li>
<li>자동 등록이 기본이고, 기술 지원 로직/전략 패턴 구간은 수동 등록을 고민할 포인트</li>
</ul>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 6. 컴포넌트 스캔]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-6.-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-6.-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</guid>
            <pubDate>Mon, 08 Sep 2025 06:22:59 GMT</pubDate>
            <description><![CDATA[<p>컴포넌트 스캔과 @Autowired — 이제는 내가 안 만들고, 스프링이 만든다</p>
<p>지금까지는 이렇게 했다.</p>
<ul>
<li>@Bean으로 직접 등록</li>
<li>AppConfig에서 객체 생성</li>
<li>생성자 주입 연결</li>
</ul>
<p>솔직히 말해서, 객체가 몇 개 안 될 때는 괜찮다.
근데 수십, 수백 개 되면?</p>
<p>설정 클래스가 지옥이 된다.</p>
<p>그래서 등장한 게 컴포넌트 스캔 + 자동 의존관계 주입이다.</p>
<hr>
<h3 id="1-컴포넌트-스캔이-뭐냐면">1. 컴포넌트 스캔이 뭐냐면</h3>
<p>기존 방식:</p>
<pre><code>@Bean
public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
}</code></pre><p>이제는 이런 설정을 안 써도 된다.</p>
<pre><code>@Component
public class MemberServiceImpl implements MemberService {
}</code></pre><p>그리고 설정 클래스에</p>
<pre><code>@ComponentScan</code></pre><p>만 붙이면 끝이다.</p>
<p>스프링이 알아서:</p>
<ul>
<li>@Component 붙은 클래스 찾고</li>
<li>스프링 빈으로 등록한다.</li>
</ul>
<hr>
<h3 id="2-근데-왜-configuration도-스캔되냐">2. 근데 왜 @Configuration도 스캔되냐?</h3>
<p>이 부분이 재밌다.</p>
<p>@Configuration 안에 들어가보면
실제로 @Component가 붙어있다.</p>
<p>즉, @Configuration도 결국 @Component의 일종이다.</p>
<p>그래서 컴포넌트 스캔을 켜면
이전 AppConfig 같은 설정 클래스도 같이 등록된다.</p>
<p>그래서 예제에서는 excludeFilters로 기존 설정 클래스를 제외했다.</p>
<hr>
<h3 id="3-의존관계는-누가-넣어주냐-→-autowired">3. 의존관계는 누가 넣어주냐? → @Autowired</h3>
<p>이제 AppConfig가 없다.
그럼 의존관계는 어디서 연결하냐?</p>
<p>정답은 생성자에서 @Autowired.</p>
<pre><code>@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
                            DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}</code></pre><p>스프링이:</p>
<ul>
<li>타입 기준으로 빈을 찾고</li>
<li>자동으로 생성자에 주입한다.</li>
</ul>
<p>이건 내부적으로</p>
<pre><code>getBean(MemberRepository.class</code></pre><p>를 자동으로 해주는 거라고 보면 된다.</p>
<hr>
<h3 id="4-스캔-시작-위치는-어디부터">4. 스캔 시작 위치는 어디부터?</h3>
<p>컴포넌트 스캔은 기본적으로
설정 클래스가 위치한 패키지부터 하위 패키지 전체를 스캔한다.</p>
<p>그래서 권장 패턴은:</p>
<ul>
<li>프로젝트 최상단 루트 패키지에 설정 클래스 배치</li>
</ul>
<p>스프링 부트에서는 이게 자동이다.</p>
<pre><code>@SpringBootApplication</code></pre><p>이 안에 이미 @ComponentScan이 들어있다.
실무에서는 거의 설정 위치를 따로 지정 안 한다.</p>
<hr>
<h3 id="5-기본-스캔-대상은-component만이-아니다">5. 기본 스캔 대상은 @Component만이 아니다</h3>
<h4 id="스프링은-이런-것들도-스캔한다">스프링은 이런 것들도 스캔한다.</h4>
<ul>
<li>@Controller</li>
<li>@Service</li>
<li>@Repository</li>
<li>@Configuration</li>
</ul>
<p>이유는?
이 애노테이션들 안에 전부 @Component가 들어있기 때문이다.</p>
<p>⸻</p>
<h4 id="근데-각-애노테이션의-차이점은">근데 각 애노테이션의 차이점은?</h4>
<p>단순히 스캔용이 아니다.</p>
<ul>
<li>@Controller → MVC 컨트롤러로 인식</li>
<li>@Repository → 예외를 스프링 예외로 변환</li>
<li>@Configuration → CGLIB 처리 (싱글톤 보장)</li>
<li>@Service → 사실 특별한 기능은 없음 (의미적 구분)</li>
</ul>
<p>즉, 스프링은 애노테이션을 “의미”로도 사용한다.</p>
<hr>
<h3 id="6-필터는-거의-안-쓴다">6. 필터는 거의 안 쓴다</h3>
<p>includeFilters, excludeFilters로</p>
<ul>
<li>특정 애노테이션만 스캔</li>
<li>특정 타입 제외</li>
</ul>
<p>이런 것도 가능하다.</p>
<p>근데 솔직히 말하면
: 실무에서 거의 안 쓴다.</p>
<p>요즘은</p>
<ul>
<li>기본 스캔 유지</li>
<li>패키지 구조를 잘 잡는 게 더 중요하다.</li>
</ul>
<hr>
<h3 id="7-중복-등록-문제">7. 중복 등록 문제</h3>
<h4 id="자동-vs-자동">자동 vs 자동</h4>
<p>같은 이름이면 바로 예외 발생</p>
<pre><code>ConflictingBeanDefinitionException</code></pre><p>이건 오히려 안전하다.</p>
<p>⸻</p>
<h4 id="수동-vs-자동">수동 vs 자동</h4>
<p>예전 스프링
: 수동 빈이 자동 빈을 덮어씀</p>
<p>이게 진짜 무섭다.</p>
<ul>
<li>개발자가 의도한 건지</li>
<li>실수인지</li>
<li>설정 꼬인 건지</li>
</ul>
<p>구분이 안 된다.</p>
<p>그래서 최근 스프링 부트는
수동 + 자동 충돌 시 기본적으로 오류 발생</p>
<p>이게 훨씬 낫다.</p>
<hr>
<h3 id="8--내가-느낀-핵심">8.  내가 느낀 핵심</h3>
<p>컴포넌트 스캔은 단순히 “자동 등록”이 아니라</p>
<p>객체 생성 책임을 설정 클래스(AppConfig)에서
“애노테이션 기반 구조”로 이동시킨 것</p>
<p>그리고 @Autowired는
“이제 내가 new 안 하고, 스프링이 연결해준다” 선언</p>
<hr>
<h3 id="9-한-줄-정리">9. 한 줄 정리</h3>
<p>컴포넌트 스캔은
설정 코드를 줄이는 기능이 아니라, 구조를 애노테이션 중심으로 바꾸는 전환점이다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 5. 싱글톤 컨테이너]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-5.-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-5.-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</guid>
            <pubDate>Fri, 05 Sep 2025 10:17:18 GMT</pubDate>
            <description><![CDATA[<p>왜 스프링은 기본이 싱글톤일까? (그리고 @Configuration의 진짜 역할)</p>
<p>이 챕터는 단순히 “싱글톤이 좋다”가 아니라
웹 환경에서 왜 싱글톤이 기본 전략인지를 이해하는 파트였다.</p>
<p>그리고 마지막에 나오는 CGLIB 이야기가 은근히 재밌다.</p>
<hr>
<h3 id="1-웹-애플리케이션과-객체-생성-비용">1. 웹 애플리케이션과 객체 생성 비용</h3>
<p>스프링은 기본적으로 웹 애플리케이션을 위한 기술에서 출발했다.</p>
<p>웹 환경의 특징은 이거다.
동시에 여러 사용자가 요청을 보낸다.</p>
<p>만약 우리가 만들었던 순수 DI 컨테이너(AppConfig)가
요청 올 때마다 객체를 새로 만든다면?</p>
<ul>
<li>초당 100 요청
  → 초당 100개 객체 생성
  → 계속 생성 &amp; 소멸</li>
</ul>
<p>이건 그냥 메모리 낭비다.
그래서 해결 방법은 단순하다.</p>
<p>“객체를 한 번만 만들고, 공유하면 되지 않나?”</p>
<p>그게 바로 싱글톤이다.</p>
<hr>
<h3 id="2-싱글톤-패턴이란">2. 싱글톤 패턴이란?</h3>
<p>싱글톤 패턴은</p>
<p>“객체를 딱 1개만 생성하도록 보장하는 디자인 패턴”</p>
<p>전통적인 방식은 이런 느낌이다.</p>
<pre><code>public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    private SingletonService() {}

    public static SingletonService getInstance() {
        return instance;
    }
}</code></pre><ul>
<li>생성자는 private</li>
<li>외부에서 new 못함</li>
<li>getInstance()로만 접근</li>
</ul>
<p>근데 문제 많다
강의에서 강조한 단점이 꽤 많다.</p>
<ul>
<li>구현 코드가 지저분하다</li>
<li>DIP 위반하기 쉽다</li>
<li>테스트 어렵다</li>
<li>유연성 떨어진다</li>
<li>상속 어렵다</li>
</ul>
<p>결론적으로 좋은 객체지향 설계랑 안 맞는다.
그래서 이걸 직접 쓰는 건 점점 안티패턴 취급을 받는다.</p>
<hr>
<h3 id="3-스프링의-싱글톤-컨테이너">3. 스프링의 싱글톤 컨테이너</h3>
<p>스프링은 다르게 접근한다.</p>
<p>“패턴을 직접 쓰지 말고, 컨테이너가 대신 관리해줄게.”</p>
<p>스프링 컨테이너는</p>
<ul>
<li>객체를 한 번만 생성하고</li>
<li>그 인스턴스를 계속 재사용한다</li>
</ul>
<p>이걸 싱글톤 레지스트리라고 한다.</p>
<p>그리고 우리가 지금까지 만든 @Bean 객체들은
기본적으로 전부 싱글톤이다.</p>
<p>여기서 중요한 차이:</p>
<ul>
<li>우리가 직접 싱글톤 코드를 안 짜도 된다</li>
<li>DIP, OCP를 해치지 않는다</li>
<li>테스트하기도 수월하다</li>
</ul>
<p>이게 스프링이 잘 설계된 부분이라고 느꼈다.</p>
<hr>
<h3 id="4-싱글톤의-진짜-주의점-이게-더-중요함">4. 싱글톤의 진짜 주의점 (이게 더 중요함)</h3>
<p>싱글톤은 객체를 공유한다.</p>
<p>즉,
여러 사용자가 하나의 객체를 동시에 사용한다.
그래서 절대 하면 안 되는 게 있다.</p>
<p>상태를 가지면 안 된다 (stateful 금지)</p>
<p>예를 들어 이런 코드가 있다면</p>
<pre><code>private int price; // 공유 필드</code></pre><p>ThreadA가 10000원 넣고
ThreadB가 20000원 넣으면</p>
<p>결과가 꼬일 수 있다.</p>
<p>사용자A 주문금액이
20000원으로 나올 수도 있다.</p>
<p>⸻</p>
<p>그래서 원칙은 이거다.
스프링 빈은 무조건 “무상태(stateless)“로 설계하자.</p>
<ul>
<li>특정 사용자 값 저장하지 말 것</li>
<li>공유 필드 쓰지 말 것</li>
<li>값은 파라미터/지역변수로 처리</li>
</ul>
<p>이건 실무에서도 진짜 중요하다고 느껴진다.</p>
<hr>
<h3 id="5-configuration-없이도-싱글톤이-보장될까">5. @Configuration 없이도 싱글톤이 보장될까?</h3>
<p>이 파트가 제일 흥미로웠다.</p>
<p>AppConfig 코드에서 이런 상황이 있었다.</p>
<pre><code>@Bean
public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
}

@Bean
public OrderService orderService() {
    return new OrderServiceImpl(memberRepository(), discountPolicy());
}</code></pre><p>겉으로 보면 memberRepository()가
    •    컨테이너 등록할 때 1번
    •    memberService에서 1번
    •    orderService에서 1번</p>
<p>총 3번 호출될 것 같지 않나?
그럼 MemoryMemberRepository 인스턴스도 3개 생겨야 맞다.</p>
<p>근데 실제로는 1개만 생성된다.</p>
<p>왜?</p>
<hr>
<h3 id="6-configuration과-cglib의-마법">6. @Configuration과 CGLIB의 마법</h3>
<p>비밀은 여기에 있다.</p>
<pre><code>@Configuration
public class AppConfig { ... }</code></pre><p>스프링은 @Configuration이 붙은 클래스를 그냥 쓰지 않는다.</p>
<p>CGLIB라는 바이트코드 조작 라이브러리를 사용해서
AppConfig를 상속한 가짜 클래스를 하나 만든다.</p>
<p>예를 들어 클래스 이름이 이런 식으로 바뀐다.</p>
<pre><code>AppConfig$$EnhancerBySpringCGLIB</code></pre><p>이 가짜 클래스 안에서 이런 동작을 한다.</p>
<p>@Bean 메서드를 호출하면
이미 빈이 존재하는지 확인하고
있으면 기존 빈 반환
없으면 생성해서 등록 후 반환</p>
<p>그래서 결과적으로 싱글톤이 보장된다.</p>
<hr>
<h3 id="7-bean만-쓰고-configuration-안-쓰면">7. @Bean만 쓰고 @Configuration 안 쓰면?</h3>
<p>여기서 중요한 포인트.</p>
<p>@Configuration을 빼고 
@Bean만 쓰면?</p>
<p>→ CGLIB이 동작하지 않는다.
→ memberRepository()가 실제로 여러 번 호출된다.
→ 인스턴스가 여러 개 생성된다.</p>
<p>즉, @Bean만으로는 싱글톤을 보장하지 않는다.</p>
<p>그래서 실무에서는
스프링 설정 클래스에는 무조건 @Configuration을 붙인다.</p>
<p>라고 이해하면 된다.</p>
<hr>
<h3 id="8-내가-이-챕터에서-얻은-핵심">8. 내가 이 챕터에서 얻은 핵심</h3>
<ol>
<li>스프링은 기본이 싱글톤이다.</li>
<li>싱글톤 객체는 반드시 무상태(stateless)로 설계해야 한다.</li>
<li>@Configuration은 단순 설정 표시가 아니라
싱글톤 보장을 위한 바이트코드 조작 트리거다.</li>
</ol>
<hr>
<h3 id="9-한-줄-정리">9. 한 줄 정리</h3>
<p>스프링은 “싱글톤 패턴을 쓰는 게 아니라”
싱글톤을 안전하게 관리해주는 컨테이너다.</p>
<p>그리고 @Configuration은
그 싱글톤을 보장하는 숨은 핵심 장치다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 4. 스프링 컨테이너와 스프링 빈]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-4.-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-4.-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</guid>
            <pubDate>Wed, 27 Aug 2025 13:40:58 GMT</pubDate>
            <description><![CDATA[<p>스프링 컨테이너는 결국 “객체 공장 + 조립자”다 (Bean 조회, ApplicationContext, BeanDefinition)</p>
<p>이 챕터는 스프링을 처음 볼 때 제일 헷갈리는 부분이었다.</p>
<ul>
<li>ApplicationContext가 뭐지?</li>
<li>빈 등록은 어떻게 되는 거지?</li>
<li>getBean은 왜 이렇게 여러 방식이 있지?</li>
<li>XML, 자바 설정은 뭐가 다른 거야?</li>
</ul>
<p>강의를 듣고 나서 내 결론은 이거였다.</p>
<p>스프링 컨테이너는 “객체를 만들어서(생성) + 연결해주는(조립) 시스템”이다.</p>
<hr>
<h3 id="1-스프링-컨테이너-생성-결국은-applicationcontext">1. 스프링 컨테이너 생성: 결국은 ApplicationContext</h3>
<p>스프링에서 흔히 말하는 “컨테이너”는 보통 ApplicationContext를 말한다.</p>
<pre><code>ApplicationContext ac =
    new AnnotationConfigApplicationContext(AppConfig.class);</code></pre><ul>
<li>ApplicationContext는 인터페이스</li>
<li>AnnotationConfigApplicationContext는 구현체</li>
<li>생성할 때는 “설정 정보(구성 정보)”를 넘겨줘야 한다.
→ 여기서는 AppConfig.class</li>
</ul>
<p>내가 이전 챕터에서 만들었던 AppConfig(조립자)를
스프링이 공식적으로 받아서 컨테이너에 올리는 느낌이다.</p>
<hr>
<h3 id="2-스프링-빈-등록-이름은-기본적으로-메서드명">2. 스프링 빈 등록: 이름은 기본적으로 “메서드명”</h3>
<p>@Configuration + @Bean 기반 설정이면</p>
<ul>
<li>@Bean이 붙은 메서드를 스프링이 호출하고</li>
<li>그 반환 객체를 컨테이너에 등록한다</li>
</ul>
<p>빈 이름은 기본적으로 메서드 이름이다.</p>
<pre><code>@Bean
public MemberService memberService() { ... }
// → 빈 이름: &quot;memberService&quot;</code></pre><p>빈 이름은 중복 나면 위험</p>
<p>같은 이름이면</p>
<ul>
<li>덮어씌워지거나</li>
<li>무시되거나</li>
<li>설정에 따라 오류가 난다</li>
</ul>
<p>(그래서 실무에선 빈 이름 관리도 은근 중요하다고 함)</p>
<hr>
<h3 id="3-의존관계-주입di은-언제-일어나나">3. 의존관계 주입(DI)은 언제 일어나나?</h3>
<p>스프링은 보통</p>
<ol>
<li>빈 생성</li>
<li>의존관계 주입</li>
</ol>
<p>이 두 단계를 나눠서 처리할 수 있는 구조인데,</p>
<p>자바 설정(@Bean) 방식은
생성자 호출 시점에 주입까지 같이 일어나는 경우가 많다.</p>
<p>즉, 내 입장에선 “등록되자마자 조립되는 느낌”으로 보였다.</p>
<hr>
<h3 id="4-컨테이너에-등록된-빈-전체-조회하기">4. 컨테이너에 등록된 빈 전체 조회하기</h3>
<p>컨테이너에 뭐가 들어있는지 보고 싶으면 이게 제일 직관적이다.</p>
<ul>
<li>ac.getBeanDefinitionNames() : 등록된 빈 이름 목록</li>
<li>ac.getBean(name) : 해당 빈 객체 조회</li>
</ul>
<p>그리고 “스프링 내부 빈 vs 내가 만든 빈”은 역할(role)로 구분된다.</p>
<ul>
<li>ROLE_APPLICATION : 내가 만든 빈 (보통 관심 대상)</li>
<li>ROLE_INFRASTRUCTURE : 스프링이 내부에서 쓰는 빈</li>
</ul>
<hr>
<h3 id="5-스프링-빈-조회-가장-기본-패턴">5. 스프링 빈 조회: 가장 기본 패턴</h3>
<p>자주 쓰는 건 거의 이 두 개.</p>
<pre><code>ac.getBean(&quot;memberService&quot;, MemberService.class);
ac.getBean(MemberService.class);</code></pre><p>없는 빈 조회하면 예외가 터진다.</p>
<ul>
<li>NoSuchBeanDefinitionException</li>
</ul>
<hr>
<h3 id="6-같은-타입-빈이-2개면-타입-조회의-함정">6. 같은 타입 빈이 2개면? (타입 조회의 함정)</h3>
<pre><code>ac.getBean(DiscountPolicy.class);</code></pre><p>DiscountPolicy 구현체가 2개면 (Fix, Rate)
스프링은 “뭘 줄지” 모르니까 오류가 난다.</p>
<p>이럴 때는</p>
<ul>
<li>빈 이름을 명시하거나</li>
<li>ac.getBeansOfType()로 전체를 가져온다<pre><code>Map&lt;String, DiscountPolicy&gt; beans = ac.getBeansOfType(DiscountPolicy.class);</code></pre></li>
</ul>
<hr>
<h3 id="7-상속-관계-조회는-부모-타입으로-싹-다-잡힌다">7. 상속 관계 조회는 “부모 타입으로 싹 다 잡힌다”</h3>
<p>부모 타입으로 조회하면 자식 타입까지 같이 딸려온다.</p>
<p>극단적으로</p>
<pre><code>ac.getBeansOfType(Object.class);</code></pre><p>하면 진짜 모든 빈이 나온다. (자바 최상위 타입이니까)</p>
<hr>
<h3 id="8-beanfactory-vs-applicationcontext-뭐-쓰는-게-맞나">8. BeanFactory vs ApplicationContext: 뭐 쓰는 게 맞나?</h3>
<p>간단하게 말하면: </p>
<ul>
<li>BeanFactory = 빈 “관리/조회”의 최소 기능</li>
<li>ApplicationContext = BeanFactory + 부가기능 풀세트</li>
</ul>
<p>ApplicationContext가 제공하는 부가기능 예시:</p>
<ul>
<li>국제화 메시지 (MessageSource)</li>
<li>환경 분리 (profile 같은 거)</li>
<li>이벤트 발행/구독</li>
<li>리소스 조회 등등</li>
</ul>
<p>내 결론
: BeanFactory는 거의 직접 쓸 일이 없고
보통 ApplicationContext 쓰면 된다.</p>
<hr>
<h3 id="9-설정-형식이-달라도-스프링이-유연한-이유-자바xml">9. 설정 형식이 달라도 스프링이 유연한 이유 (자바/Xml)</h3>
<p>스프링은 설정 정보를 여러 형태로 받을 수 있다.</p>
<ul>
<li>자바 설정</li>
<li>XML 설정</li>
<li>Groovy 등…</li>
</ul>
<p>요즘은 부트 쓰면서 XML은 잘 안 보이긴 하는데
레거시에는 많아서 “개념 정도”는 알아두는 게 좋다.</p>
<hr>
<h3 id="10-beandefinition-스프링이-설정을-다루는-공통-포맷">10. BeanDefinition: 스프링이 설정을 다루는 “공통 포맷”</h3>
<p>스프링이 자바 설정이든 XML이든 같은 방식으로 처리할 수 있는 이유는
중간에 BeanDefinition(메타정보) 라는 추상화가 있기 때문이다.</p>
<p>내가 이해한 느낌은 이거다.</p>
<p>설정 파일이 어떤 형식이든
스프링이 결국 “BeanDefinition”이라는 공통 포맷으로 바꿔서 처리한다.</p>
<ul>
<li>자바 설정 → BeanDefinition 생성 (Reader)</li>
<li>XML 설정 → BeanDefinition 생성 (Reader)</li>
<li>그리고 컨테이너는 BeanDefinition을 보고 빈을 만든다</li>
</ul>
<p>실무에서 BeanDefinition을 직접 만질 일은 거의 없지만,
스프링 내부 구조를 볼 때 종종 등장해서 “이 정도 감”만 있으면 충분하다.</p>
<hr>
<h3 id="11-내가-이-챕터에서-가져간-결론">11. 내가 이 챕터에서 가져간 결론</h3>
<ul>
<li>스프링 컨테이너(ApplicationContext)는 빈을 등록하고 조립(DI)하는 곳</li>
<li>빈 조회는 타입/이름 기반으로 가능하지만, “타입 중복”은 주의</li>
<li>ApplicationContext는 BeanFactory + 실전 기능들</li>
<li>설정 형식이 달라도 BeanDefinition으로 추상화돼서 처리된다</li>
</ul>
<hr>
<h3 id="12-한-줄-정리">12. 한 줄 정리</h3>
<p>스프링 컨테이너는 결국
객체를 만들어두고, 필요할 때 꺼내 쓰고, 의존관계까지 연결해주는 시스템이다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 3. 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-3.-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%9D%B4%ED%95%B4-2-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%9B%90%EB%A6%AC-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-3.-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%9D%B4%ED%95%B4-2-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%9B%90%EB%A6%AC-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 25 Aug 2025 08:20:00 GMT</pubDate>
            <description><![CDATA[<p>할인 정책이 바뀌었을 때, 내 설계는 얼마나 버틸까? (AppConfig → IoC/DI → 스프링)</p>
<p>이번 챕터는 한마디로 이 느낌이었다.</p>
<p>“나 유연하게 설계했는데요? …아닌데요?”</p>
<p>다형성도 쓰고, 인터페이스도 뽑아놨는데
정작 정책 하나 바꾸려니까 주문 서비스 코드가 같이 흔들린다.</p>
<p>왜 이런 일이 생겼는지, 그리고 어떻게 해결되는지 흐름으로 정리해본다.</p>
<hr>
<h3 id="1-새로운-할인-정책이-추가됐다-정액-→-정률">1. 새로운 할인 정책이 추가됐다: 정액 → 정률</h3>
<p>기획자 요구사항이 바뀌었다고 치자.</p>
<ul>
<li>기존: VIP면 무조건 1000원 할인 (정액)</li>
<li>변경: VIP면 주문 금액의 10% 할인 (정률)</li>
</ul>
<p>그래서 RateDiscountPolicy를 추가하는 건 어렵지 않다.</p>
<pre><code>public class RateDiscountPolicy implements DiscountPolicy {
    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        }
        return 0;
    }
}</code></pre><p>문제는 <strong>추가가 아니라 “적용”</strong>에서 터진다.</p>
<hr>
<h3 id="2-적용하려는-순간-orderservice가-같이-바뀐다">2. 적용하려는 순간, OrderService가 같이 바뀐다</h3>
<p>기존 코드가 이런 식이면</p>
<pre><code>private final DiscountPolicy discountPolicy = new FixDiscountPolicy();</code></pre><p>정률로 바꾸려면</p>
<pre><code>private final DiscountPolicy discountPolicy = new RateDiscountPolicy();</code></pre><p>결국 OrderServiceImpl 코드 자체를 수정해야 한다.</p>
<p>여기서 깨달음
: “확장(정책 추가)은 쉬운데, 변경(정책 교체)은 클라이언트에 영향을 준다.”</p>
<hr>
<h3 id="3-나는-ocpdip-지킨-줄-알았는데-사실-아니었다">3. 나는 OCP/DIP 지킨 줄 알았는데… 사실 아니었다</h3>
<p>겉으로 보면 이런 구조라서 안전해 보인다.</p>
<pre><code>OrderServiceImpl → DiscountPolicy(interface) ← Fix / Rate</code></pre><p>근데 실제로는 이렇다.</p>
<pre><code>OrderServiceImpl → DiscountPolicy(interface)
OrderServiceImpl → FixDiscountPolicy (구현체)</code></pre><p>즉, OrderServiceImpl은 인터페이스만 의존하는 게 아니라 구현체도 알고 있다.</p>
<p>그래서</p>
<ul>
<li>DIP 위반: “추상”뿐 아니라 “구체”에 의존하고 있음</li>
<li>OCP 위반: 구현체 바꾸려면 OrderServiceImpl 코드 변경 필요</li>
</ul>
<hr>
<h3 id="4-그럼-해결책은">4. 그럼 해결책은?</h3>
<p>해결은 단순하다.</p>
<p>“클라이언트가 구현체를 고르지 않게 만들자.”</p>
<p>그런데 여기서 바로 벽을 만난다.</p>
<p>“그럼 OrderServiceImpl은 인터페이스만 바라보게 바꾸면 되겠네?”</p>
<pre><code>private final DiscountPolicy discountPolicy;</code></pre><p>이렇게만 해놓으면… 실행 시점에 값이 없어서 NPE 난다.</p>
<p>즉, 누군가는 구현 객체를 생성해서 넣어줘야 한다.</p>
<p>그래서 등장하는 게 AppConfig다.</p>
<hr>
<h3 id="5-관심사-분리-공연-기획자가-필요하다-appconfig">5. 관심사 분리: 공연 기획자가 필요하다 (AppConfig)</h3>
<p>강의에서 공연 비유가 진짜 직관적이었다.</p>
<ul>
<li>배우(서비스)는 연기만 해야지</li>
<li>어떤 배우를 캐스팅할지는 기획자가 해야 한다</li>
</ul>
<p>기존 코드 상황은 배우가 “상대 배우 섭외 + 연기”까지 다 하는 느낌이었다.</p>
<p>그래서 역할을 분리한다.</p>
<p>객체 생성/조립은 AppConfig가
실행 로직은 서비스가</p>
<hr>
<h3 id="6-appconfig가-하는-일">6. AppConfig가 하는 일</h3>
<p>AppConfig는 “조립자”다.
    •    필요한 구현 객체를 만들고
    •    어떤 구현을 누구에게 넣을지 결정하고
    •    생성자로 주입한다</p>
<p>예시 느낌은 이런 구조</p>
<pre><code>public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy(); // 여기만 바꾸면 정책 교체 끝
    }
}</code></pre><p>이렇게 되면 서비스 입장에서는</p>
<ul>
<li>“어떤 구현체가 들어오는지” 모른다</li>
<li>그냥 인터페이스만 믿고 실행한다</li>
</ul>
<p>그리고 이게 바로 DI(의존관계 주입) 이다.</p>
<hr>
<h3 id="7-구조가-바뀌면서-생기는-가장-큰-변화">7. 구조가 바뀌면서 생기는 가장 큰 변화</h3>
<p>애플리케이션이 영역이 분리된다.</p>
<ul>
<li>구성 영역(Configuration): AppConfig (조립/선택)</li>
<li>사용 영역(Use): 서비스/도메인 로직 (실행)</li>
</ul>
<p>그래서 할인 정책을 바꾸고 싶으면?</p>
<p>구성 영역만 바꾸면 된다.</p>
<pre><code>public DiscountPolicy discountPolicy() {
    return new RateDiscountPolicy();
}</code></pre><p>OrderServiceImpl 같은 “사용 영역”은 건드릴 필요가 없다.</p>
<p>여기서 진짜로 OCP/DIP가 완성되는 느낌이었다.</p>
<hr>
<h3 id="8-ioc는-뭐였지">8. IoC는 뭐였지?</h3>
<p>여기서 IoC(제어의 역전)가 자연스럽게 나온다.</p>
<p>기존:</p>
<ul>
<li>OrderServiceImpl이 “내가 쓸 객체 내가 정하고 내가 실행 흐름도 만든다.”</li>
</ul>
<p>AppConfig 이후:</p>
<ul>
<li>OrderServiceImpl은 그냥 실행만 한다.</li>
<li>“누가 만들고 어떤 구현을 넣을지”는 AppConfig가 결정한다.</li>
</ul>
<p>즉, 제어 흐름이 서비스에서 설정(AppConfig)으로 넘어갔다.
이게 IoC다.</p>
<hr>
<h3 id="9-이제-스프링으로-넘어가면">9. 이제 스프링으로 넘어가면?</h3>
<p>여기까지는 “내가 만든 AppConfig”였는데
스프링은 이걸 프레임워크 레벨에서 해준다.</p>
<ul>
<li>@Configuration : 설정 클래스</li>
<li>@Bean : 스프링 빈 등록</li>
</ul>
<p>스프링 컨테이너(ApplicationContext)가</p>
<ul>
<li>객체를 생성하고</li>
<li>관리하고</li>
<li>의존관계를 연결해준다</li>
</ul>
<p>그래서 우리는 new로 조립하지 않고</p>
<p>applicationContext.getBean() 같은 방식으로 꺼내 쓰게 된다.</p>
<hr>
<h3 id="10-solid가-실제로-적용되는-지점">10. SOLID가 실제로 적용되는 지점</h3>
<p>이 챕터는 SOLID를 “외우는”게 아니라
왜 필요한지 몸으로 납득하는 파트였다.</p>
<ul>
<li>SRP: “생성/조립”과 “실행” 책임을 분리했다</li>
<li>DIP: 서비스는 추상(인터페이스)만 의존한다</li>
<li>OCP: 정책 교체해도 사용 영역은 안 바뀐다</li>
</ul>
<hr>
<h3 id="11-한-줄-정리">11. 한 줄 정리</h3>
<p>다형성만으로는 설계가 완성되지 않는다.
객체를 생성/조립하는 책임(AppConfig)을 분리해야 OCP/DIP가 진짜로 지켜진다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 2. 스프링 핵심 원리 이해 1 - 예제 만들기]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-2.-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%9D%B4%ED%95%B4-1-%EC%98%88%EC%A0%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-2.-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%9D%B4%ED%95%B4-1-%EC%98%88%EC%A0%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 23 Aug 2025 09:26:13 GMT</pubDate>
            <description><![CDATA[<p>회원/주문 도메인 설계 — 객체지향이 실제로 어떻게 적용되는가</p>
<p>이번 챕터는 단순히 “코드를 짜는 과정”이 아니라
객체지향 설계를 어떻게 시작해야 하는지를 보여주는 단계였다.</p>
<p>여기서 가장 중요했던 건 이것이다.</p>
<p>“요구사항에는 항상 ‘변경 가능성’이 숨어 있다.”</p>
<hr>
<h3 id="1-비즈니스-요구사항-분석">1. 비즈니스 요구사항 분석</h3>
<h4 id="회원">회원</h4>
<ul>
<li>회원 가입 / 조회 가능</li>
<li>등급은 일반 / VIP</li>
<li>저장 방식은 미정 (DB? 외부 시스템?)</li>
</ul>
<p>여기서 이미 중요한 힌트가 나온다.</p>
<p>저장 방식이 확정되지 않았다 = 변경 가능성</p>
<p>⸻</p>
<h4 id="주문--할인-정책">주문 &amp; 할인 정책</h4>
<ul>
<li>회원은 상품 주문 가능</li>
<li>등급에 따라 할인 적용</li>
<li>현재는 VIP 1000원 고정 할인</li>
<li>하지만 할인 정책은 확정 아님</li>
<li>정률 할인으로 바뀔 수도 있음</li>
<li>할인 자체가 없어질 수도 있음</li>
</ul>
<p>여기서 느낀 점
: 할인 정책은 100% 바뀐다.</p>
<p>이건 그냥 “바뀔 수 있다”가 아니라
“무조건 바뀐다”고 가정해야 한다.</p>
<hr>
<h3 id="2-회원-도메인-설계">2. 회원 도메인 설계</h3>
<h4 id="설계-목표">설계 목표</h4>
<ul>
<li>저장 방식이 바뀌어도 서비스는 바뀌지 않게 만들자.</li>
<li>구현이 아니라 역할에 의존하게 만들자.</li>
</ul>
<p>⸻</p>
<h4 id="협력-구조">협력 구조</h4>
<pre><code>클라이언트 → MemberService → MemberRepository</code></pre><p>그리고 저장소는 여러 구현체가 가능하다.</p>
<ul>
<li>MemoryMemberRepository</li>
<li>DbMemberRepository</li>
<li>ExternalMemberRepository</li>
</ul>
<p>여기서 핵심은 이것이다.
MemberService는 MemberRepository “역할”만 안다.</p>
<p>⸻</p>
<h4 id="클래스-구조">클래스 구조</h4>
<pre><code>interface MemberService
class MemberServiceImpl implements MemberService

interface MemberRepository
class MemoryMemberRepository implements MemberRepository</code></pre><p>이미 여기서 다형성이 적용된다.</p>
<hr>
<h3 id="3-회원-도메인-개발">3. 회원 도메인 개발</h3>
<h4 id="member-엔티티">Member 엔티티</h4>
<ul>
<li>id</li>
<li>name</li>
<li>grade (BASIC / VIP)</li>
</ul>
<p>단순하지만, 도메인의 중심 객체다.</p>
<p>⸻</p>
<h4 id="memberrepository">MemberRepository</h4>
<p>인터페이스를 먼저 만든다.</p>
<pre><code>public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}</code></pre><p>그리고 구현체를 만든다.</p>
<pre><code>public class MemoryMemberRepository implements MemberRepository {
    private static Map&lt;Long, Member&gt; store = new HashMap&lt;&gt;();
}</code></pre><p>여기서 중요한 포인트
: HashMap은 동시성 문제 가능 → 실무에서는 ConcurrentHashMap 필요</p>
<p>⸻</p>
<h4 id="memberservice">MemberService</h4>
<pre><code>public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
}</code></pre><p>여기서 문제가 생긴다.</p>
<hr>
<h3 id="4-첫-번째-설계의-문제점">4. 첫 번째 설계의 문제점</h3>
<p>MemberService는 인터페이스에 의존하지만…</p>
<pre><code>new MemoryMemberRepository();</code></pre><p>이 부분 때문에 구현체에도 의존하고 있다.</p>
<p>즉,
    •    DIP 위반
    •    OCP 위반</p>
<p>구현을 바꾸려면 클라이언트 코드도 바꿔야 한다.</p>
<p>다형성은 적용했지만, 아직 완성은 아니다.</p>
<hr>
<h3 id="5-주문--할인-도메인-설계">5. 주문 &amp; 할인 도메인 설계</h3>
<p>이제 진짜 핵심이 시작된다.</p>
<h4 id="주문-흐름">주문 흐름</h4>
<ol>
<li>주문 요청</li>
<li>회원 조회</li>
<li>할인 정책 적용</li>
<li>주문 객체 생성</li>
</ol>
<p>여기서 중요한 점은
: 할인 정책은 주문 서비스가 직접 계산하지 않는다.</p>
<p>“할인 정책”이라는 역할에 위임한다.</p>
<p>⸻</p>
<h4 id="협력-구조-1">협력 구조</h4>
<pre><code>OrderService
 ├─ MemberRepository
 └─ DiscountPolicy</code></pre><p>여기서 또 역할과 구현이 등장한다.</p>
<pre><code>interface DiscountPolicy
class FixDiscountPolicy
class RateDiscountPolicy</code></pre><hr>
<h3 id="6-주문-도메인-개발">6. 주문 도메인 개발</h3>
<h4 id="할인-정책-인터페이스">할인 정책 인터페이스</h4>
<pre><code>public interface DiscountPolicy {
    int discount(Member member, int price);
}</code></pre><p>⸻</p>
<h4 id="고정-할인-정책">고정 할인 정책</h4>
<pre><code>public class FixDiscountPolicy implements DiscountPolicy {

    private final int discountFixAmount = 1000;

    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }
        return 0;
    }
}</code></pre><p>⸻</p>
<p>OrderService</p>
<pre><code>public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}</code></pre><p>또 같은 문제가 발생한다.
구현체를 직접 생성하고 있다.</p>
<hr>
<h3 id="7-설계의-의도">7. 설계의 의도</h3>
<p>이 설계는 일부러 이렇게 만든 것이다.
왜냐하면 지금 우리가 느껴야 할 건 이것이기 때문이다.</p>
<p>“다형성만으로는 OCP, DIP를 지킬 수 없다.”</p>
<p>구현을 바꾸려면 여전히 클라이언트를 수정해야 한다.</p>
<p>이 문제를 해결하기 위해
다음 챕터에서 DI 컨테이너가 등장한다.</p>
<hr>
<h3 id="8-내가-느낀-핵심">8. 내가 느낀 핵심</h3>
<p>이번 챕터에서 가장 인상 깊었던 건
코드를 잘 짜는 게 아니라</p>
<p>“어디가 바뀔지 먼저 생각하는 것”이었다.</p>
<p>할인 정책은 무조건 바뀐다.
저장 방식도 바뀐다.</p>
<p>그래서 역할을 먼저 만들고,
구현은 나중에 붙인다.</p>
<p>이게 객체지향 설계의 출발점이었다.</p>
<hr>
<h3 id="9-한-줄-정리">9. 한 줄 정리</h3>
<p>도메인 설계는 기능 구현이 아니라
변경 가능성을 기준으로 구조를 만드는 과정이다.</p>
<p>또 같은 문제가 발생한다.
구현체를 직접 생성하고 있다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 1. 객체 지향 설계와 스프링]]></title>
            <link>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-1.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81</link>
            <guid>https://velog.io/@dev_geonwoo/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-1.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81</guid>
            <pubDate>Fri, 22 Aug 2025 10:37:55 GMT</pubDate>
            <description><![CDATA[<p>스프링은 왜 탄생했을까? 그리고 왜 객체지향이 핵심일까?</p>
<p>김영한님의 강의를 들으면서 느낀 건, 스프링은 기술이 아니라 철학에 가깝다는 거였다.</p>
<p>단순히 “편한 프레임워크”가 아니라 좋은 객체 지향 설계를 현실에서 가능하게 해주는 도구에 가깝다.</p>
<hr>
<h3 id="1-자바-진영의-겨울-그리고-스프링의-시작">1. 자바 진영의 겨울, 그리고 스프링의 시작</h3>
<h4 id="ejb의-문제">EJB의 문제</h4>
<p>2000년대 초반, 자바 진영의 표준은 EJB였다.</p>
<p>이론적으로는 굉장히 좋아 보였다.
하지만 실제로 쓰면…
    •    복잡하다
    •    느리다
    •    코드가 더러워진다
    •    EJB에 강하게 종속된다</p>
<p>결국 “객체지향을 잘하기 위해 나온 기술”이
오히려 객체지향을 망가뜨리는 상황이 되어버렸다.</p>
<p>그래서 등장한 개념이 POJO (Plain Old Java Object).
“그냥 자바 객체로 개발하자”는 움직임이었다.</p>
<p>⸻</p>
<h4 id="로드-존슨과-스프링의-탄생">로드 존슨과 스프링의 탄생</h4>
<p>로드 존슨은 EJB를 비판하며 책을 썼고,
그 안에는 지금 우리가 아는 스프링의 핵심 개념들이 들어 있었다.
    •    BeanFactory
    •    ApplicationContext
    •    IoC (제어의 역전)
    •    DI (의존관계 주입)
    •    POJO 기반 개발</p>
<p>이 책이 계기가 되어 오픈소스 프로젝트가 시작되었고,
그게 바로 Spring Framework다.</p>
<p>“J2EE라는 겨울을 넘어 새로운 봄(Spring)이 오길 바란다.”</p>
<p>이름이 괜히 Spring이 아니다.</p>
<hr>
<h3 id="2-jpa는-왜-나왔을까">2. JPA는 왜 나왔을까?</h3>
<p>EJB의 엔티티 빈은 거의 쓰이지 않게 되었고,
대신 Hibernate가 엄청난 인기를 끌었다.</p>
<p>Hibernate를 만든 개빈 킹과 함께
Hibernate 기반으로 표준 ORM 기술이 만들어졌는데
그게 바로 <strong>JPA (Java Persistence API)</strong>다.</p>
<p>중요한 포인트:</p>
<p>JPA는 표준(인터페이스)이고
Hibernate는 구현체다.</p>
<p>지금도 대부분 Hibernate를 사용한다.</p>
<p>이 구조 자체가 이미 “역할과 구현의 분리”다.</p>
<hr>
<h3 id="3-스프링이란-무엇인가">3. 스프링이란 무엇인가?</h3>
<p>스프링은 하나의 기술이 아니다.
생태계다.
    •    Spring Framework
    •    Spring Boot
    •    Spring Data
    •    Spring Security
    •    Spring Batch
    •    Spring Cloud
    •    …</p>
<p>하지만 진짜 핵심은 이것이다.
: 스프링은 객체 지향의 장점을 극대화하기 위한 프레임워크다.</p>
<hr>
<h3 id="4-좋은-객체지향이란">4. 좋은 객체지향이란?</h3>
<p>객체지향의 핵심은 네 가지라고 배웠다.</p>
<ul>
<li>추상화</li>
<li>캡슐화</li>
<li>상속</li>
<li>다형성</li>
</ul>
<p>여기서 가장 중요한 건 다형성이다.</p>
<hr>
<h3 id="5-다형성의-진짜-의미">5. 다형성의 진짜 의미</h3>
<p>강의에서 계속 강조한 게 하나 있다.</p>
<p>“역할과 구현을 분리하라.”</p>
<h4 id="운전자와-자동차-예제">운전자와 자동차 예제</h4>
<ul>
<li>운전자 = 클라이언트</li>
<li>자동차 = 역할 (인터페이스)</li>
<li>K5, 아반떼 = 구현체</li>
</ul>
<p>운전자는 “자동차”만 알면 된다.
어떤 모델인지 몰라도 된다.</p>
<p>그래서 자동차를 바꿔도 운전자는 바뀌지 않는다.</p>
<p>이게 유연함이다.</p>
<p>⸻</p>
<h4 id="공연-무대-예제">공연 무대 예제</h4>
<p>로미오와 줄리엣.
    •    로미오 = 클라이언트
    •    줄리엣 = 역할
    •    배우들 = 구현체</p>
<p>배우가 바뀌어도 공연은 계속된다.</p>
<p>이게 역할과 구현을 분리했을 때의 힘이다.</p>
<hr>
<h3 id="6-자바에서-역할과-구현">6. 자바에서 역할과 구현</h3>
<p>자바에서는 이렇게 표현된다.</p>
<pre><code>interface MemberRepository { }
class MemoryMemberRepository implements MemberRepository { }
class JdbcMemberRepository implements MemberRepository { }</code></pre><p>문제는 여기서 끝이 아니다.</p>
<hr>
<h3 id="7-다형성만으로는-부족하다">7. 다형성만으로는 부족하다</h3>
<p>이 코드가 있다고 해보자.</p>
<pre><code>MemberRepository repository = new MemoryMemberRepository();</code></pre><p>구현체를 바꾸려면?</p>
<pre><code>MemberRepository repository = new JdbcMemberRepository();</code></pre><p>클라이언트 코드를 수정해야 한다.</p>
<p>즉,</p>
<ul>
<li>OCP 위반</li>
<li>DIP 위반</li>
</ul>
<p>다형성만으로는 “진짜 유연한 설계”가 완성되지 않는다.</p>
<hr>
<h3 id="8-그래서-스프링이-필요하다">8. 그래서 스프링이 필요하다</h3>
<p>스프링이 해주는 핵심 역할은 이것이다.</p>
<p>객체를 대신 생성해주고
의존관계를 대신 연결해준다.</p>
<p>이게 DI (Dependency Injection)다.</p>
<p>클라이언트는 이제 구현체를 모른다.
그냥 인터페이스만 안다.</p>
<p>이제 진짜로 OCP, DIP가 가능해진다.</p>
<hr>
<h3 id="9-solid-원칙을-다시-보자">9. SOLID 원칙을 다시 보자</h3>
<p>SRP</p>
<p>변경이 생겼을 때 파급효과가 적으면 잘 지킨 것이다.</p>
<p>OCP</p>
<p>확장에는 열려있고, 변경에는 닫혀있어야 한다.
→ 다형성 + DI가 필요하다.</p>
<p>LSP</p>
<p>인터페이스 규약은 반드시 지켜야 한다.
(엑셀을 밟으면 앞으로 가야지 뒤로 가면 안 된다.)</p>
<p>ISP</p>
<p>인터페이스는 작고 명확하게 쪼개는 게 좋다.</p>
<p>DIP</p>
<p>구현이 아니라 추상(인터페이스)에 의존하라.</p>
<hr>
<h3 id="10-결국-스프링의-정체는-이것이다">10. 결국 스프링의 정체는 이것이다</h3>
<ul>
<li>스프링은 객체지향을 잘하기 위해 존재한다.</li>
<li>스프링은 다형성을 극대화한다.</li>
<li>스프링은 OCP, DIP를 현실에서 가능하게 만든다.</li>
</ul>
<p>만약 순수 자바로 OCP, DIP를 제대로 지키면서 개발하려고 하면 결국 우리가 직접 DI 컨테이너를 만들게 된다.</p>
<p>그게 바로 스프링이다.</p>
<hr>
<h3 id="11-내가-느낀-핵심">11. 내가 느낀 핵심</h3>
<p>이번 챕터를 들으면서 느낀 건 하나였다.</p>
<p>스프링을 배우는 게 아니라
객체지향을 다시 배우는 느낌이었다.</p>
<p>스프링의 기술들은 전부 수단이다.
목적은 좋은 객체지향 설계다.</p>
<hr>
<h3 id="12-한-줄-정리">12. 한 줄 정리</h3>
<p>스프링은 기술이 아니라,
좋은 객체지향 설계를 현실에서 가능하게 해주는 도구다.</p>
<hr>
<p>출처
스프링 핵심 원리 - 기본편 (김영한, 인프런, 2020)</p>
]]></description>
        </item>
    </channel>
</rss>