<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>PSG-00님의 블로그</title>
    <link>https://memo50984.tistory.com/</link>
    <description>개발자의 작고 소중한 공간</description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 05:44:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>PSG-00</managingEditor>
    <item>
      <title>[코드잇][위클리페이퍼][12주차]</title>
      <link>https://memo50984.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;12주차 위클리페이퍼 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션의&amp;nbsp;각&amp;nbsp;계층에서&amp;nbsp;수행되는&amp;nbsp;입력값&amp;nbsp;검증의&amp;nbsp;범위와&amp;nbsp;책임을&amp;nbsp;어떻게&amp;nbsp;나눌&amp;nbsp;것인지에&amp;nbsp;대해&amp;nbsp;설명해주세요.&amp;nbsp;특히&amp;nbsp;중복&amp;nbsp;검증을&amp;nbsp;피하면서도&amp;nbsp;안정성을&amp;nbsp;확보하는&amp;nbsp;방안과,&amp;nbsp;이와&amp;nbsp;관련된&amp;nbsp;트레이드오프에&amp;nbsp;대해&amp;nbsp;설명해주세요.&lt;/li&gt;
&lt;li&gt;테스트에서 사용되는 Mockito의 Mock, Stub, Spy 개념을 각각 설명하고, 어떤 상황에서 어떤 방식을 선택해야 하는지 구체적인 예시와 함께 설명하세요.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 각 계층 별 입력값 검증의 범위와 책임&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션의 각 계층이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 애플리케이션은 복잡성을 제어하기 위해 &lt;b&gt;MVC 패턴&lt;/b&gt;과 &lt;b&gt;계층형 아키텍처&lt;/b&gt;라는 기술을 사용한다. 이후 모바일 시장이 등장하면서 클라이언트의 역할이 중요해지고, 서버와 클라이언트를 분리하는 &lt;b&gt;클라이언트(SPA) - 서버(API) 아키텍처&lt;/b&gt;로 변화한다. 따라서, 입력값 검증의 범위와 책임도 각 계층의 관심사에 따라 분리되어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2uQEu/dJMcabjszve/EB6kBrUq7Liw2kkyrrMV3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2uQEu/dJMcabjszve/EB6kBrUq7Liw2kkyrrMV3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2uQEu/dJMcabjszve/EB6kBrUq7Liw2kkyrrMV3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2uQEu%2FdJMcabjszve%2FEB6kBrUq7Liw2kkyrrMV3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;400&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 서버의 계층형 아키텍처를 5Layer로 표현하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presentaiton Layer(Model, View, Controller)&lt;/li&gt;
&lt;li&gt;Business Layer(Service&lt;/li&gt;
&lt;li&gt;Persistence layer(Data Access Layer, Repository)&lt;/li&gt;
&lt;li&gt;Database Layer(DB)&lt;/li&gt;
&lt;li&gt;Domain Layer(Entity)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계층 별 입력값 검증 목적에 따른 범위와 책임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 살펴본 것 처럼 애플리케이션은 관심사 분리를 위해 계층형 아키텍처를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 입력값의 검증 범위와 책임도 각 계층의 관심사에 맞게 설계해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 각 계층의 입력값 검증의 목적에 따른 범위와 책임을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;클라이언트 계층&lt;/i&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목적 및 책임: 사용자 경험 향상 및 불필요한 서버 요청 방지&lt;/li&gt;
&lt;li&gt;검증 범위: 필수 파라미터 누락, 비밀번호/이메일 등 형식, 파라미터 길이 등&lt;/li&gt;
&lt;li&gt;주의점: 클라이언트 검증은 개발자 도구 등으로 우회할 수 있으므로 보안 목적으로 신뢰할 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프레젠테이션 계층&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목적 및 책임: 서버의 첫 번째 계층으로, 들어온 데이터의 요청이 문법적으로 올바른지 확인함&lt;/li&gt;
&lt;li&gt;검증 범위: 데이터 타입 검증, 페이로드의 구조적 유효성(JSON, UUID 등), NULL, 공백 등&lt;/li&gt;
&lt;li&gt;구현: 주로 DTO에 @Valid와 관련된 어노테이션을 사용&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775378753379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserRequestDto.java
public record UserRequestDto(
    @NotBlank(message = &quot;이메일은 필수 입력값입니다.&quot;)
    @Email(message = &quot;올바른 이메일 형식이 아닙니다.&quot;)
    String email,

    @NotBlank(message = &quot;비밀번호는 필수 입력값입니다.&quot;)
    @Size(min = 8, message = &quot;비밀번호는 최소 8자 이상이어야 합니다.&quot;)
    String password
) {}

// UserController.java
public class UserController {
    @PostMapping(&quot;/signup&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; signUp(@Valid @RequestBody UserRequestDto request) {
        userService.register(request);
        return ResponseEntity.ok(&quot;회원가입 성공&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 계층&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목적 및 책임: 데이터가 실제 비즈니스 규칙과 시스템에 부합하는지 확인함&lt;/li&gt;
&lt;li&gt;검증 범위: 중복 데이터 검증(이메일, Username 등), 권한에 따른 비즈니스 로직 적용&lt;/li&gt;
&lt;li&gt;구현: 실질적인 비즈니스 규칙에 맞는 로직 직접 구현&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775378805773&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserService.java
public class UserService {
    private final UserRepository userRepository;

    public void register(UserRequestDto request) {
        // 의미론적 검증: &quot;이미 가입된 이메일인가?&quot;
        if (userRepository.existsByEmail(request.email())) {
            throw new AlreadyExistsException(&quot;이미 사용 중인 이메일입니다.&quot;);
        }

        // 비즈니스 룰 검증: &quot;특정 정책에 따라 가입이 제한되는가?&quot;
        if (isBlacklisted(request.email())) {
            throw new PolicyViolationException(&quot;가입이 제한된 사용자입니다.&quot;);
        }

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인 계층&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목적 및 책임: 엔티티 스스로가 생성부터 소멸까지 항상 도메인 규칙 및 시스템 상 유효한 상태임을 유지&lt;/li&gt;
&lt;li&gt;검증 범위: 객체 생성 및 수정 시 논리적 무결성 검증&lt;/li&gt;
&lt;li&gt;구현: 생성자에서 검증 로직 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775395981002&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// User.java (Entity)
public class User {
    private String email;
    private String password;

    private User(String email, String password) {
        Assert.hasText(email, &quot;이메일은 비어있을 수 없습니다.&quot;);
        Assert.hasText(password, &quot;비밀번호는 비어있을 수 없습니다.&quot;);
        
        this.email = email;
        this.password = password;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영속성 계층(Repository, DB)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목적 및 책임: 서버 내 영구적으로 저장되는 데이터의 무결성을 보장하는 것&lt;/li&gt;
&lt;li&gt;검증 범위: 저장되는 엔티티의 테이블에 해당하는 제약 조건(NOT NULL, UNIQUE 등)&lt;/li&gt;
&lt;li&gt;구현: 테이블 작성 시 제약 조건에 해당하는 SQL문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775378837947&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE, -- 물리적인 UNIQUE, NOT NULL 보장
    password VARCHAR(255) NOT NULL,
    age INT CHECK (age &amp;gt; 0)             -- DB 수준의 값 범위 검증
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;Layer&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;주요 역할 및 책임&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;검증 내용&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;검증 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;클라이언트&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;불필요한 서버 요청 방지&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;필수값 누락, &lt;br /&gt;이메일 및 비밀번호 형식,&lt;br /&gt;글자 수 제한 등&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;JS의 required 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;프레젠테이션&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;데이터 형식 검사&lt;br /&gt;(구문 검증)&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;데이터 타입, Null 여부, &lt;br /&gt;정규식 매칭, 숫자 범위 등&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;DTO의 @Valid 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;비즈니스&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;비즈니스 규칙 검사&lt;br /&gt;(의미적 검증)&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;중복 데이터 확인, 권한 확인,&lt;br /&gt;비즈니스 규칙 준수 확인&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;비즈니스 규칙에 맞는 쿼리 작성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;도메인&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;객체 무결성 보장&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;객체 생성 시 필드값의 무결성&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;생성자 내 Assert 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;영속성&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;DB 데이터 무결성 보장&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;제약 조건 준수 여부&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;SQL의 제약 조건&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 각 계층별 관심사에 맞게 검증의 범위와 책임을 가지는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복 검증 회피 및 안정성 확보 방안 + 트레이드오프&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;계층별로 책임을 나누었지만, &quot;앞단에서 검증한 데이터를 뒷단에서 또 검증해야 하는가?&quot;에 대한 딜레마가 발생할 수 있다..&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;이는 시스템의 안정성과 코드의 유지보수성 사이의 트레이드오프이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;두 가지 대표적인 예외 상황을 통해 이를 구분할 수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상황 A: 알 수 없는 장애로 인한 데이터 손실&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,0,0&quot;&gt;상황:&lt;/b&gt; 프레젠테이션 계층(DTO)에서 Null 검증을 완벽히 통과했는데, 네트워크 패킷 손실이나 프레임워크 오류 등 알 수 없는 이유로 서비스 계층에 Null이 전달된 경우.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,1,0&quot;&gt;대응:&lt;/b&gt; 서비스 계층에서 DTO에서 했던 Null 검증을 중복으로 수행하지 않음. 이러한 상황은 정상적인 비즈니스 흐름이 아닌 서버 오류임. 엔티티 생성 시점의 Assert나 500 에러(Internal Server Error)로 처리하여 시스템을 빠르게 실패(Fail-fast)시켜야 함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,2,0&quot;&gt;트레이드오프:&lt;/b&gt; 극단적인 예외 상황까지 서비스 로직에서 방어하려 들면 코드가 비대해지고 유지보수가 어려워지므로(DRY 원칙 위배), 발생 확률이 희박한 시스템 결함은 인프라 계층의 예외로 넘겨야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상황 B: 동시성 이슈로 인한 논리적 예외&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,0,0&quot;&gt;상황:&lt;/b&gt; 서비스 계층에서 existsByEmail로 이메일 중복을 확인(통과)하고 DB에 저장하려는 찰나, 레이스 컨디션(Race Condition)으로 인해 동시에 같은 이메일이 가입되어 DB 수준에서 중복 예외가 발생하는 경우.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,1,0&quot;&gt;대응:&lt;/b&gt; 이는 분산 환경에서 발생하는 흔하고 논리적인 예외이므로 반드시 처리해야 함&lt;b data-index-in-node=&quot;35&quot; data-path-to-node=&quot;29,1,0&quot;&gt;.&lt;/b&gt; DB 계층의 UNIQUE 제약조건을 최후의 방어선으로 삼고, 서비스 계층에서 DB 예외(DataIntegrityViolationException)를 잡아 비즈니스 예외(409 Conflict)로 전환해서 사용자에게 전달해야함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,2,0&quot;&gt;트레이드오프:&lt;/b&gt; 앞선 계층의 검증을 통과했더라도 동시성 문제 앞에서는 무력할 수 있음. 따라서 성능상 비용이 들더라도 핵심 데이터 무결성에 대해서는 여러 계층에 걸쳐 방어적으로 검증해야함.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Mockito의 Mock, Stub, Spy&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mockito란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 애플리케이션 코드를 작성하다보면 복잡한 로직을 마주하게 된다.&amp;nbsp; 이때 작성한 로직이 의도한대로 잘 작동하는지 확인하기 위해서는 실제로 환경을 구축해서 API 요청을 하면서 테스트를 해볼 수도 있다. 하지만 이는 환경 구축의 오버헤드와 부분적인 문제를 알기 어렵다는 문제점이 있다. 이를 해결하기 위해 부분적으로 기능을 테스트하는 단위테스트가 있다. 단위 테스트에서는 의존성을 격리하기 위해 Mockito를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mockito는 Java에서 테스트를 작성할 때 사용되는 대표적인 Mock(가짜) 객체 생성 프레임워크이다. 테스트하고자 하는 계층이 다른 계층을 강하게 의존하고 있을 때, 해당 의존성을 Mock 객체로 대체하여 오직 테스트 계층의 로직에만 집중할 수 있게 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트의 종류&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;통합 테스트
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@SpringBootTest를 사용하여 애플리케이션의 모든 빈(Bean)과 실제 데이터베이스까지 전부 연결하여 테스트함.&lt;/li&gt;
&lt;li&gt;실제 운영 환경과 유사하게 검증할 수 있지만, 실행 속도가 느리고 오류 발생 시 원인을 파악하기 어렵다는 단점이 있음.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;슬라이스 테스트
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@WebMvcTest, @DataJpaTest 등을 사용하여 애플리케이션의 특정 계층(Layer)만 얇게 잘라내어 테스트함.&lt;/li&gt;
&lt;li&gt;컨트롤러 계층의 API 요청/응답 형식이나, 리포지토리 계층의 쿼리가 잘 작동하는지 확인함.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;단위 테스트
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스프링 컨테이너(IoC)를 아예 띄우지 않고, 순수한 Java 코드만 메모리에 올려 테스트하며 속도가 매우 빠름.&lt;/li&gt;
&lt;li&gt;단위 테스트의 주 대상은 '비즈니스 계층(Service)'임. 애플리케이션의 핵심 로직과 정책이 모여있는 서비스 계층은 외부 인프라(DB 등)에 구애받지 않고 언제든 빠르고 독립적으로 검증되어야 하기 때문임.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단위 테스트에서 의존성을 해소하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 스프링 없이 서비스 계층을 단위 테스트하려면 어떻게 해야 할까?&lt;br /&gt;실제 코드를 보면서 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 작성된 서비스 로직이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1775405041499&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserService
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository; // 의존성 1
    private final UserMapper userMapper;         // 의존성 2

    public UserResponseDto registerUser(UserRequestDto request) {
        // 1. 중복 이메일 검증
        if (userRepository.existsByEmail(request.email())) {
            throw new IllegalArgumentException(&quot;이미 사용 중인 이메일입니다.&quot;);
        }
        
        // 2. DTO -&amp;gt; Entity 변환
        User user = userMapper.toEntity(request);
        
        // 3. DB 저장
        User savedUser = userRepository.save(user);
        
        // 4. Entity -&amp;gt; DTO 변환 후 반환
        return userMapper.toDto(savedUser);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 UserService의 registerUser는 회원가입 기능이고 이를 테스트 하기 위해서는 UserRepository와 UserMapper라는 외부 인프라에 의존성을 해결해야 한다. 이때 등장하는 것이 Mockito이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mockito에는 @Mock, @Stub, @Spy 등의 어노테이션이 있고 단위테스트에서는 서술한 3가지를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Mock 객체이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념: 실제 객체와 겉모습(메서드 시그니처)는 동일하되, 내부는 텅 비어있는 가짜 객체&lt;/li&gt;
&lt;li&gt;사용법: Mock 객체로 만들 계층에 선언적 어노테이션을 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775405352668&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository; // DB 통신을 차단하기 위한 완전한 가짜 객체

    @Spy
    private UserMapper userMapper = new UserMapperImpl(); // (Spy는 아래에서 설명)

    @InjectMocks
    private UserService userService; // 가짜 객체들을 진짜 Service에 조립(주입)해줌
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 Stub이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념: Mock으로 만든 텅 빈 가짜 객체가 특정 상황(주로 검증할 상황)에서 어떤 값을 반환할지 미리 약속하는 행위이다.&lt;/li&gt;
&lt;li&gt;사용법: Mock객체의 특정 상황(메서드와 파라미터)의 리턴값을 설정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775405585964&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;이미 존재하는 이메일이면 예외가 발생한다&quot;)
void duplicateEmailTest() {
    // Given: Stubbing (대본 작성)
    // 가짜 userRepository에게 &quot;이 이메일로 중복 검사 들어오면 무조건 true라고 대답해!&quot; 라고 지시합니다.
    UserRequestDto request = new UserRequestDto(&quot;test@test.com&quot;, &quot;password&quot;);
    BDDMockito.given(userRepository.existsByEmail(request.email())).willReturn(true);

    // When &amp;amp; Then: 실제 서비스 로직을 실행하면, 위에서 작성한 대본(true)에 의해 예외가 터져야 함
    assertThrows(IllegalArgumentException.class, () -&amp;gt; userService.registerUser(request));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로는 Spy이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념: 기존 로직은 그대로 수정하면서, 개발자가 Stub을 작성해 원하는대로 작동하게 하거나 호출 여부를 감시할 수 있다.&lt;/li&gt;
&lt;li&gt;사용법: 사용할 대상에 선언적 어노테이션을 작성하고 구현체를 직접 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775405818855&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;정상적인 정보가 주어지면 회원가입에 성공한다&quot;)
void registerSuccessTest() {
    // Given
    UserRequestDto request = new UserRequestDto(&quot;test@test.com&quot;, &quot;password&quot;);
    User savedUser = new User(1L, &quot;test@test.com&quot;, &quot;password&quot;); // DB에서 반환될 가짜 엔티티
    
    // DB 조회는 가짜(Stub)로 막아줍니다.
    BDDMockito.given(userRepository.existsByEmail(request.email())).willReturn(false);
    BDDMockito.given(userRepository.save(any(User.class))).willReturn(savedUser);

    // When
    // 이때 userMapper.toEntity() 와 toDto() 는 @Spy 객체이므로 &quot;실제 변환 로직&quot;이 실행됩니다.
    UserResponseDto response = userService.registerUser(request);

    // Then
    assertNotNull(response);
    assertEquals(&quot;test@test.com&quot;, response.email());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 상황에서 어떤 방법을 선택해야 하는지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이에 대한 대답은 위에서 전부 했다. 의존성이 커서 그 의존성을 끊어내야 실질적인 단위 테스트가 되는 대상은 Mock 객체로 만들고 반환값은 Stub으로 작성한다. 그 외에 의존성이 작은 단순 매퍼나 유틸리티 클래스는 반환값을 매번 직접 작성하기 어려우므로 이때는 Spy 객체로 만들고, 의존성을 끊어내야 할 때 Stub으로 의존성을 끊어내면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/19</guid>
      <comments>https://memo50984.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 30 Mar 2026 10:47:46 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][11주차]</title>
      <link>https://memo50984.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;초급 프로젝트가 끝나고 오랫만에 돌아온 위클리페이퍼이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안에 대해 설명하세요.&lt;/li&gt;
&lt;li&gt;트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 발생할 수 있는 문제점들을 설명하고, 이를 해결하기 위한 트랜잭션 격리 수준들을 설명하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JPA에서 발생하는 N+1 문제의 발생 원인과 해결방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 N+1은 실제로 많은 개발자들이 JPA를 사용할 때 마주치는 문제이다. 그렇다면 N+1이 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 1+N이라고 보는게 조금 더 이해하기 쉬울 것이다. 하나의 쿼리를 실행하면 그 쿼리에서 추가적으로 N개의 쿼리가 발생하기 때문에 1+N, 즉 N+1이라고 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로만 하면 별로 와닿지 않을 것 같으니 N+1의 예시를 보여주겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;N+1이 발생하는 상황 예시&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4097&quot; data-origin-height=&quot;875&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YSC8P/dJMcajaBbMz/tFZzub0eucJBBEWVYC3sa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YSC8P/dJMcajaBbMz/tFZzub0eucJBBEWVYC3sa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YSC8P/dJMcajaBbMz/tFZzub0eucJBBEWVYC3sa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYSC8P%2FdJMcajaBbMz%2FtFZzub0eucJBBEWVYC3sa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4097&quot; height=&quot;875&quot; data-origin-width=&quot;4097&quot; data-origin-height=&quot;875&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 테이블 구조가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 단방향으로 매핑한다면 아래와 같은 구조일 것이다.(Team은 Member를 모르니까 생략)&lt;/p&gt;
&lt;pre id=&quot;code_1774981211560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team; // Member는 Team을 알지만, Team은 Member 리스트가 없음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향으로 매핑한다면 아래와 같이 작성될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774981265827&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Team {
    @OneToMany(mappedBy = &quot;team&quot;) 
    private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
}

@Entity
public class Member {
    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 작성되는 비즈니스 로직은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774981382741&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public List&amp;lt;String&amp;gt; getMemberTeamNames() {
    // 1. 모든 멤버를 조회 (쿼리 1번 실행)
    List&amp;lt;Member&amp;gt; members = memberRepository.findAll(); 

    // 2. 각 멤버의 팀 이름을 추출 (N+1 발생)
    return members.stream()
        .map(member -&amp;gt; member.getTeam().getName()) //   여기서 N번의 추가 쿼리 발생!
        .collect(Collectors.toList());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 발생하는 쿼리는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774981489289&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- [1번] findAll() 실행 시
SELECT * FROM member;

-- [N번] 루프 내부에서 member.getTeam().getName() 호출 시마다 실행
SELECT * FROM team WHERE id = 1; -- 첫 번째 멤버의 팀 조회
SELECT * FROM team WHERE id = 2; -- 두 번째 멤버의 팀 조회
SELECT * FROM team WHERE id = 5; -- 세 번째 멤버의 팀 조회
... (조회된 멤버 수 N만큼 반복)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 매핑일 경우 다음과 같은 비즈니스 로직도 존재할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774981435128&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public void printTeamMemberCount() {
    // 1. 모든 팀을 조회 (쿼리 1번 실행)
    List&amp;lt;Team&amp;gt; teams = teamRepository.findAll(); 

    // 2. 각 팀의 멤버 수를 출력
    for (Team team : teams) {
        // team.getMembers()는 프록시 컬렉션이며, .size() 호출 시 DB를 조회함
        System.out.println(team.getName() + &quot;의 멤버 수: &quot; + team.getMembers().size());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 발생하는 쿼리는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774981522280&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- [1번] findAll() 실행 시
SELECT * FROM team;

-- [N번] team.getMembers().size() 호출 시마다 실행
SELECT * FROM member WHERE team_id = 1; -- 1번 팀의 멤버들 조회
SELECT * FROM member WHERE team_id = 2; -- 2번 팀의 멤버들 조회
SELECT * FROM member WHERE team_id = 3; -- 3번 팀의 멤버들 조회
... (조회된 팀 수 N만큼 반복)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 N+1 문제가 뭔지는 이해했을 것이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;N+1이 발생하는 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 도대체 왜 이 문제가 발생하는지 알아보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JPA와 DB의 차이에 대해서 이해할 필요가 있다. JPA는 엔티티 간 연관관계를 객체 그래프로 다루고, 관계형 DB에서는 이를 테이블과 조인으로 표현한다. 그 과정에서 JPA는 연관된 객체는 그래프를 타고 자유롭게 호출할 수 있지만, DB에서는 JOIN이 필요하다. 즉, 우리가 JPA에서 OneToMany로 연관관계를 만들어놓았으면 team.getMembers() 이런식으로 팀의 멤버에는 아주 쉽게 접근할 수 있지만, DB에서는 Team의 멤버를 얻기 위해서는 외래키를 이용해서 JOIN 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 JPA에서 find 메서드로 team을 DB에서 가져올 때, team과 연관된 엔티티인 member를 가져오지 않는다. 이것이 바로 JPA의 꽃인 지연로딩(Lazy)이다. 지금 당장은 team의 멤버를 사용할지 안할지 모르니 일단은 proxy 객체로 채워 넣는 것이다. 그리고 실제로 team의 멤버를 어떤식으로든 접근할 때 그 때 멤버를 조회하는 것이다. 그러므로 team을 가져오는 쿼리 1번 + team의 각 멤버 조회 N번의 쿼리가 발생하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이런 의문이 생길 수도 있다. 어? 그러면 그냥 지연 로딩(Lazy)하지 말고 즉시 로딩(Eager) 하면 해결되는 거 아닌가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이는 해결방법이 아니다. 그 이유는 하이버네이트가 내부적으로 QueryTranslator로 JPA를 SQL로 변환할 때 Eager인지 Lazy인지 전혀 신경 쓰지 않기 때문이다. 그래서 결국 즉시 로딩은 일단 team을 가져오는 쿼리를 작성하고 eager는 엔티티를 반환하는 시점에서 데이터가 채워져 있어야 하므로 프록시 객체를 채워 넣는 대신 즉시 team의 각 멤버를 가져오는 쿼리 N번을 실행해서 데이터를 모두 채운 후 반환한다. 즉 시점의 차이일 뿐 결국 N+1이 발생하는 것은 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;N+1 해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 N+1이 발생하는 원인을 알았으니 해결 방법을 알아 낼 차례이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 크게 4가지이다.&lt;br /&gt;1. Fetch Join&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. @EntityGraph&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. @Batchsize&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. DTO Protection&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 1번과 2번은 JPQL 쿼리 튜닝을 통해 JOIN을 수행함으로서 연관된 엔티티를 전부 가져오는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join은 기본적으로 명시하지 않으면 INNER JOIN을 수행하며 연관된 데이터가 없는 경우에는 누락한다.(즉 멤버가 없는 팀은 누락한다는 것이다.)&lt;/p&gt;
&lt;pre id=&quot;code_1774983371418&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;select t from Team t join fetch t.members&quot;)
    List&amp;lt;Team&amp;gt; findAllJoinFetch();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774983603574&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT t.*, m.* FROM team t INNER JOIN member m ON t.id = m.team_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;@EntityGraph는 기본적으로 LEFT OUTER JOIN을 사용하며 연관된 데이터가 없는 경우에도 가져온다.&lt;/p&gt;
&lt;pre id=&quot;code_1774983406054&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;members&quot;})
    @Query(&quot;select t from Team t&quot;)
    List&amp;lt;Team&amp;gt; findAllEntityGraph();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774983613666&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT t.*, m.* FROM team t LEFT OUTER JOIN member m ON t.id = m.team_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 배치사이즈는 IN절로 해당하는 것들을 전부 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 사이즈로 나눠서 가져오므로 실제 쿼리 개수는 1 + ceil(N / size)가 된다. 사용방법은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774983476568&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 글로벌 설정 방법(모두 적용됨)
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

// 특정 엔티티의 필드에 적용
@Entity
public class Team {
    @BatchSize(size = 100)
    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY)
    private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774983628987&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM member WHERE team_id IN (1, 2, 3, ..., 100)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 DTO Projection은 엔티티 필드의 전체가 아닌 일부만 조회해서 연관관계를 해소하는 방법이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774984020729&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record TeamMemberCountDto(String teamName, Long memberCount) {}

public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {

    @Query(&quot;select new com.example.dto.TeamMemberCountDto(t.name, count(m)) &quot; +
           &quot;from Team t left join t.members m group by t.name&quot;)
    List&amp;lt;TeamMemberCountDto&amp;gt; findAllTeamSummary();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1을 해결하기 위한 방법을 언제 사용하면 좋을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연관된 엔티티를 수정해야 하는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes ➡️ Fetch Join 또는 @EntityGraph (엔티티 상태 유지 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 양이 많아서 페이징 처리가 필요한가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes ➡️ @BatchSize (OneTo~ FetchJoin은 페이징 처리 어려움)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;수정 없이 단순 통계나 목록 조회인가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes ➡️ DTO Projection (가장 가볍고 빠름)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 트랜잭션의 격리성이 보장되지 않을 때 발생할 수 있는 문제점 및 해결법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 여러 작업을 하나의 논리적 단위로 묶는 최소 단위를 트랜잭션(Transaction)이라고 하며, 이 트랜잭션이 안전하게 수행되기 위해서는 반드시 지켜야 하는 4가지 철칙이 있는데, 이를 ACID 속성이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션의 4대 원칙, ACID 속성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;원자성 (Atomicity):&lt;/b&gt; 트랜잭션 내의 작업들은 &quot;모두 성공(Commit)하거나 모두 실패(Rollback)해야 한다&quot; (All or Nothing)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;일관성 (Consistency):&lt;/b&gt; 트랜잭션이 성공적으로 완료되면, 데이터베이스는 항상 일관된 상태를 유지해야 한다. (예: 계좌 이체 전후의 총금액은 같아야 함)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;격리성 (Isolation):&lt;/b&gt; 둘 이상의 트랜잭션이 동시에 실행될 때, 서로의 작업에 끼어들거나 중간 상태를 볼 수 없도록 격리해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,3,0&quot;&gt;지속성 (Durability):&lt;/b&gt; 성공적으로 커밋된 트랜잭션의 결과는 시스템이 고장 나더라도 영구적으로 보존되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리성이 완벽하게 보장되지 않는 이유&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;격리성을 보장하기 위해서는 한 번에 하나의 트랜잭션만 처리하면 사실 매우 간단하게 보장할 수 있다. 우리가 화장실을 사용하는 것을 예로 들자. 화장실에서는 세면대에서 손을 씻을 수 있고, 샤워기로 목욕을 할 수 있고, 대변기와 소변기에서 각각 볼 일을 볼 수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;그런데 화장실에 단 한 명씩 들어가게 되면 화장실에 들어가는 사람이 많을 경우 굉장히 밀릴 것이다. 그래서 각각 다른 일을 수행한다면 들여보내면 화장실을 들어가는 사람들이 밀리지 않을 것이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;마찬가지로 수만 명의 유저가 동시에 접속하는 서비스에서 트랜잭션을 줄 세워 하나씩 처리한다면, 엄청난 성능 저하와 병목 현상이 발생할 것이다. 즉, 데이터베이스는 &quot;데이터의 정합성(정확성)&quot;과 &quot;동시 처리 성능&quot; 사이에서 딜레마에 빠지게 된다. 결국 성능을 위해 격리성을 조금씩 느슨하게 풀어주게 되는데, 이 과정에서 완벽히 격리되지 않은 트랜잭션들이 서로 간섭하며 치명적인 3가지 문제점이 발생한다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;격리성이 보장되지 않을 때 발생하는 3가지 문제점&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① Dirty Read (더티 리드)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;다른 트랜잭션이 아직 커밋하지 않은(작업 중인) 데이터를 읽어버리는 현상&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,0,0&quot;&gt;상황:&lt;/b&gt; 트랜잭션 A가 특정 회원의 포인트를 100에서 200으로 수정함. (아직 Commit 전)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,0&quot;&gt;문제 발생:&lt;/b&gt; 이때 트랜잭션 B가 해당 회원의 포인트를 조회하니 200이 조회됩니다. 트랜잭션 B는 이 200이라는 값을 기준으로 후속 로직을 실행함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,2,0&quot;&gt;치명적 결과:&lt;/b&gt; 만약 트랜잭션 A에서 에러가 발생해 작업을 100으로 &lt;b data-index-in-node=&quot;38&quot; data-path-to-node=&quot;17,2,0&quot;&gt;Rollback&lt;/b&gt;해버린다면? 트랜잭션 B는 실제 DB에 존재하지도 않는 '200'이라는 더티(Dirty) 데이터를 가지고 비즈니스 로직을 처리한 셈이 되어 심각한 데이터 오염이 발생함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② Non-Repeatable Read (논-리피터블 리드)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&quot;한 트랜잭션 내에서 같은 조건으로 데이터를 두 번 읽었는데, 그 값이 달라지는 현상&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0,0&quot;&gt;상황:&lt;/b&gt; 트랜잭션 A가 1번 상품의 재고를 조회하니 10개임.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,1,0&quot;&gt;문제 발생:&lt;/b&gt; 그사이 트랜잭션 B가 1번 상품의 재고를 5개로 **수정(UPDATE)**하고 Commit 함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,2,0&quot;&gt;치명적 결과:&lt;/b&gt; 트랜잭션 A가 다시 한번 1번 상품의 재고를 조회하면 이번에는 5개가 나옴. 하나의 트랜잭션 안에서는 데이터가 일관되어야 한다는 원칙이 깨진 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ Phantom Read (팬텀 리드)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;한 트랜잭션 내에서 같은 조건으로 레코드를 두 번 읽었는데, 처음에는 없던 '유령(Phantom)' 레코드가 나타나는 현상&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,0,0&quot;&gt;상황:&lt;/b&gt; 트랜잭션 A가 &quot;VIP 등급인 회원 목록&quot;을 조회하니 5명이 조회됨.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,1,0&quot;&gt;문제 발생:&lt;/b&gt; 그사이 트랜잭션 B가 새로운 VIP 회원 1명을 **삽입(INSERT)**하고 Commit 함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,2,0&quot;&gt;치명적 결과:&lt;/b&gt; 트랜잭션 A가 다시 &quot;VIP 등급인 회원 목록&quot;을 조회하면 6명이 조회됨. 값 자체가 바뀌는 Non-Repeatable Read와 달리, 결과의 '건수(집합)' 자체가 달라지는 현상.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션의 격리수준 단계&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;격리 수준&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;Dirty Read&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;Non-Repeatable Read&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;Phantom Read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;READ UNCOMMITTED &lt;br /&gt;(커밋되지 않은 읽기)&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;READ COMMITTED &lt;br /&gt;(커밋된 읽기)&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;REPEATABLE READ&lt;br /&gt;(반복 가능한 읽기)&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;SERIALIZABLE &lt;br /&gt;(직렬화 가능)&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;방지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,0,0&quot;&gt;READ UNCOMMITTED:&lt;/b&gt; 남이 작업 중인 데이터도 다 읽을 수 있음. (정합성 문제가 너무 커서 실무에서는 사용하지 않음.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,1,0&quot;&gt;READ COMMITTED:&lt;/b&gt; 커밋이 완료된 데이터만 읽을 수 있음. Dirty Read는 막을 수 있어 가장 널리 사용되는 기본 수준. (대부분의 DBMS는 이 레벨의 격리수준을 사용함.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,2,0&quot;&gt;REPEATABLE READ:&lt;/b&gt; 트랜잭션이 시작되기 전 상태의 데이터만 일관되게 보여줌. Non-Repeatable Read까지 막아줍니다. (MySQL InnoDB는 갭 락, 넥스트 키 락 등의 메커니즘을 통해 이 레벨에서도 팬텀 리드까지 거의 방어해냄.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,3,0&quot;&gt;SERIALIZABLE:&lt;/b&gt; 모든 트랜잭션을 순서대로 하나씩 실행하는 것으로 완벽한 격리를 보장하지만, 성능이 극단적으로 떨어져 (데이터의 정합성이 매우 중요한 경우에만 사용됨.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/18</guid>
      <comments>https://memo50984.tistory.com/18#entry18comment</comments>
      <pubDate>Tue, 24 Mar 2026 09:08:44 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][초급프로젝트][Findex] 프로젝트 개인 개발 리포트</title>
      <link>https://memo50984.tistory.com/17</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 프로젝트 개요&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 목적: &lt;span style=&quot;background-color: #ffffff; color: #4a494f; text-align: start;&quot;&gt;가볍고 빠른 외부 API 연동 금융 분석 도구&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;프로젝트 핵심 기능: 금융위원회 OpenAPI 연동, 지수 정보 및 데이터 제공, 시각화 대시보드, 자동 연동 스케줄링&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 담당한 작업&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- R&amp;amp;R: &lt;/b&gt;지수 데이터 관련 담당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 공통 작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스키마 및 ERD 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 개인 작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 지수 데이터 CRUD 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 지수 데이터 다운로드(CSV) 구현&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 기술적 성과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 기술스택&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Framework&lt;/b&gt;: Spring Boot&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Database &amp;amp; ORM&lt;/b&gt;: PostgreSQL, Spring Data JPA&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Documentation&lt;/b&gt;: springdoc-openapi (Swagger)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Scheduling&lt;/b&gt;: Spring Scheduler&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Utility&lt;/b&gt;: MapStruct, QueryDSL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 및 협업&lt;/b&gt;: &lt;a href=&quot;http://Railway.io&quot;&gt;Railway.io&lt;/a&gt; / Git &amp;amp; GitHub / Discord&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 구현한 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지수 데이터 엔티티 캡슐화 및 무결성 확보&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캡슐화:&lt;/b&gt; 생성자 접근 제어를 통해 외부에서 객체를 무분별하게 생성하는 것을 차단하고, Setter를 제거하고 정적 팩토리 메서드와 명확하게 수정 가능한 값만 update하는 메서드를 통해 캡슐화했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무결성 확보:&lt;/b&gt; 정적 팩토리 메서드와 업데이트 메서드에서 유효값 검증 로직을 추가해서 엔티티가 자체적으로 유효하지 않은 상태로 존재할 수 없도록 하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지수 데이터 Export&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0,0&quot;&gt;메모리 오버헤드 방지:&lt;/b&gt; 대량의 데이터를 CSV 문자열로 변환할 때 StringBuilder를 활용하여, 불필요한 String 객체 생성을 막고 메모리 사용을 최적화했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,1,0&quot;&gt;외부 라이브러리 의존도 최소화:&lt;/b&gt; OpenCSV 등의 외부 프레임워크 없이 자체 포맷팅 로직을 구현하여 프로젝트의 무게를 줄였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,2,0&quot;&gt;인터페이스 추상화 활용:&lt;/b&gt; 파일 다운로드 처리에 스프링의 Resource 인터페이스를 활용하여, 향후 추출 방식(메모리/파일/스트림)이 변경되어도 API 스펙이 유지되도록 유연하게 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지수 데이터 목록 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커서 기반 페이지네이션 적용: &lt;/b&gt;오프셋 기반 페이지네이션이 아닌 커서 기반 페이지네이션을 적용함으로써 데이터 개수가 많아져도 성능 저하가 없도록 하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 최적화: &lt;/b&gt;QueryDSL을 적용하여 중복되는 코드를 최소화하고 타입 안정성을 보장하며 쿼리 성능 및 가독성을 향상시켰습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.&amp;nbsp;문제점&amp;nbsp;및&amp;nbsp;해결&amp;nbsp;과정&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;상황 (Situation):&lt;/b&gt; 사용자가 지수 데이터를 조회할 때, 대량의 시계열 데이터를 효율적으로 불러오고 다양한 조건(기간, 지수 식별자, 정렬 방식 등)으로 필터링할 수 있는 API가 필요했습니다&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;과제 (Task):&lt;/b&gt; 일반적인 오프셋(Offset) 기반 페이징은 페이지 번호가 뒤로 갈수록 DB에서 앞선 데이터를 모두 읽어야 하므로 성능 저하가 우려되었습니다. 또한, 다양한 검색 조건을 Spring Data JPA의 기본 메서드 이름이나 @Query로 처리하기에는 조건 조합의 수가 너무 많아 유지보수가 어려웠습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;행동 (Action):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능 개선을 위해 **커서 기반 페이징(Cursor-based Pagination)**을 도입했습니다.&lt;/li&gt;
&lt;li&gt;복잡한 동적 쿼리를 깔끔하게 처리하기 위해 &lt;b data-index-in-node=&quot;24&quot; data-path-to-node=&quot;11,2,1,1,0&quot;&gt;QueryDSL&lt;/b&gt;을 프로젝트에 적용했습니다. IndexDataQueryCondition이라는 하나의 DTO로 요청을 받고, indexInfoIdEq, dateRange 등 BooleanExpression을 반환하는 메서드들을 분리하여 조건 유무에 따라 동적으로 WHERE 절이 구성되도록 했습니다.&lt;/li&gt;
&lt;li&gt;정렬 기준 필드의 값이 동일할 때 발생할 수 있는 페이징 누락(데이터 건너뜀) 현상을 방지하기 위해 orderBy 절에 고유 식별자(indexData.id)를 2차 정렬 조건으로 추가했습니다.&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,3,0&quot;&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,3,0&quot;&gt;결과 (Result):&lt;/b&gt; 데이터량의 증가와 관계없이 전체 데이터를 스캔하지 않고 인덱스를 활용하여 일정한 응답 속도를 보장하는 커서 기반 페이징을 구현했습니다. 또한, 검색 조건을 QueryDSL의 BooleanExpression으로 모듈화하여, 새로운 조건이 추가되더라도 where 절에 단 한 줄의 코드만 추가하면 되는 높은 확장성과 가독성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.&amp;nbsp;협업&amp;nbsp;및&amp;nbsp;피드백&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;소통을 통한 충돌 관리 전략 수립:&lt;/b&gt; 협업 과정에서 Git을 통한 버전 관리의 중요성을 실감했습니다. 특히 병합(Merge) 시 발생하는 충돌을 경험하며, 기술적인 해결보다 '수정 전 소통'이 가장 저비용으로 충돌을 방지하는 전략임을 배웠습니다. 작업 시작 전 수정 범위와 영향도를 공유하는 문화를 통해 충돌 발생 자체를 최소화하는 협업 체계를 구축했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 리뷰를 통한 관점의 확장:&lt;/b&gt; 팀원들과의 코드 리뷰를 통해 제가 놓치고 있던 다양한 시각을 접할 수 있었습니다. 동료의 코드를 리뷰하며 전체적인 프로그램의 흐름(Flow)을 파악하는 문해력을 길렀고, '좋은 코드'에 대한 기준을 동료들과 맞추어 나가며 가독성 높은 코드를 작성하는 계기가 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;6.&amp;nbsp;코드&amp;nbsp;품질&amp;nbsp;및&amp;nbsp;최적화&lt;/b&gt; &lt;br /&gt;프로젝트&amp;nbsp;중&amp;nbsp;코드의&amp;nbsp;가독성과&amp;nbsp;유지보수성을&amp;nbsp;어떻게&amp;nbsp;고려했는지,&amp;nbsp;성능&amp;nbsp;최적화를&amp;nbsp;위한&amp;nbsp;작업을&amp;nbsp;설명해&amp;nbsp;주세요.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;객체지향적 설계를 통한 유지보수성 향상:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;Enum을 활용한 다형성(Strategy) 적용:&lt;/b&gt; 동적 정렬 및 커서 페이징 로직을 구현할 때, QueryDSL 내부에 복잡한 switch나 if-else 분기를 두지 않고 &lt;b data-index-in-node=&quot;97&quot; data-path-to-node=&quot;5,0,0&quot;&gt;IndexDataSortField Enum에 로직을 위임&lt;/b&gt;(cursorCondition(condition))했습니다. 이를 통해 새로운 정렬 기준이 추가되어도 기존 Repository 코드를 수정할 필요가 없는 **개방-폐쇄 원칙(OCP)**을 완벽히 준수했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;관심사 분리 (MapStruct 도입):&lt;/b&gt; 서비스 레이어(Service)가 비즈니스 로직에만 집중할 수 있도록, DTO와 Entity 간의 데이터 변환 책임을 **IndexDataMapper로 완전히 분리(SRP)**하여 코드의 응집도를 높이고 결합도를 낮추었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;다형성을 활용한 확장성 설계:&lt;/b&gt; Export 기능 구현 시 반환 타입을 Resource 인터페이스로 추상화하여, 향후 대용량 데이터 추출 시 스트리밍 방식(InputStreamResource) 등으로 구현을 변경하더라도 Controller의 API 스펙을 수정할 필요가 없는 유연한 구조를 마련했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,3,0&quot;&gt;캡슐화 및 데이터 무결성 강화:&lt;/b&gt; 엔티티 설계 시 @NoArgsConstructor(access = AccessLevel.PROTECTED)와 정적 팩토리 메서드(create)를 사용하여 무분별한 객체 생성을 막고, 무상태 변경을 방지하기 위해 Setter를 철저히 배제했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 최적화를 위한 쿼리 전략:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;DTO 직접 조회(Projections)를 통한 N+1 원천 차단:&lt;/b&gt; 일반적으로 JPA에서 Entity를 조회한 후 DTO로 변환하는 과정에서 연관관계 객체(IndexInfo)를 참조할 경우 &lt;b data-index-in-node=&quot;106&quot; data-path-to-node=&quot;7,0,0&quot;&gt;N+1 쿼리 문제&lt;/b&gt;가 발생할 수 있습니다. 이를 완벽하게 방지하기 위해, QueryDSL의 **Projections.constructor**를 사용하여 DB 쿼리 단계에서부터 꼭 필요한 컬럼(indexInfo.id 포함)만 선택하여 &lt;b data-index-in-node=&quot;235&quot; data-path-to-node=&quot;7,0,0&quot;&gt;DTO로 바로 매핑(Select Projection)&lt;/b&gt; 하도록 설계했습니다. 그 결과 N+1 문제가 전혀 발생하지 않으며 불필요한 엔티티 영속화로 인한 메모리 낭비도 최적화했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;최적화된 쿼리 실행 제어:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,0,0&quot;&gt;Export API:&lt;/b&gt; 대량의 데이터를 추출할 때 불필요한 연관관계 조회를 배제하고, findAllForExport 메서드를 통해 단 &lt;b data-index-in-node=&quot;74&quot; data-path-to-node=&quot;7,1,1,0,0&quot;&gt;1회의 쿼리&lt;/b&gt;로 모든 필수 데이터를 가져오도록 제어했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,1,0&quot;&gt;페이징 API:&lt;/b&gt; 목록 조회 쿼리 1회와 조건부 count 쿼리 1회, 총 &lt;b data-index-in-node=&quot;41&quot; data-path-to-node=&quot;7,1,1,1,0&quot;&gt;2회의 예측 가능한 쿼리&lt;/b&gt;만 발생하도록 최적화하여 데이터베이스 부하를 최소화했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 가독성 향상을 위한 전략:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;표준 컨벤션 기반의 일관된 코드 스타일 유지:&lt;/b&gt; Google Java Style Guide를 적용하여 팀원 간의 코드 포맷팅을 통일했습니다. 이를 통해 불필요한 포맷팅 관련 코드 리뷰 시간을 줄이고, 로직의 흐름을 읽는 데 집중할 수 있는 환경을 만들었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;API 명세 인터페이스 분리 (Controller 경량화):&lt;/b&gt; Swagger(@Operation, @ApiResponse 등) 및 유효성 검사 어노테이션이 Controller의 본질적인 로직을 가려 가독성을 떨어뜨리는 문제를 해결하기 위해, &lt;b data-index-in-node=&quot;134&quot; data-path-to-node=&quot;9,1,0&quot;&gt;IndexDataApi 인터페이스를 별도로 추출&lt;/b&gt;했습니다. 문서화에 대한 책임은 인터페이스에 위임하고, 구현체인 Controller는 순수하게 HTTP 요청 매핑과 비즈니스 로직 호출에만 집중하게 하여 코드의 가독성을 압도적으로 높였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7. 향후 개선 사항 및 제안&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;Export 시 대용량 데이터 처리 아키텍처 고도화&lt;/b&gt;: 현재는 수만 건 수준의 데이터를 메모리 내에서 처리하고 있으나, 향후 수백만 건 이상의 데이터 폭증을 대비하여 StreamingResponseBody를 도입한 실시간 스트리밍 Export로 리팩토링이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;비동기 처리 도입&lt;/b&gt;: OpenAPI 연동이나 무거운 파일 생성 등이 메인 서버 스레드를 점유하지 않도록, 메시지 큐를 활용하여 비동기 백그라운드로 분리하고 완료 시 알림을 보내는 방식의 아키텍처 개선이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;Export 포맷 다변화&lt;/b&gt;: 현재 CSV 형식만 지원하는 기능을 확장하여 Excel(XLSX), JSON 등 사용자의 니즈에 맞는 다양한 파일 포맷 선택 옵션을 제공할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/17</guid>
      <comments>https://memo50984.tistory.com/17#entry17comment</comments>
      <pubDate>Thu, 19 Mar 2026 08:57:06 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][8주차] SQL문과 역정규화</title>
      <link>https://memo50984.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;위클리페이저 8주차 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SQL에서 DDL과 DML의 차이점을 설명하고, 각각의 대표적인 명령어들의 용도를 설명하세요.&lt;/li&gt;
&lt;li&gt;역정규화가 필요한 상황과 적용 시 고려해야 할 사항, 그리고 역정규화를 적용할 때의 장단점을 설명해주세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 둘다 RDBMS와 연관되어 있는 주제이기에 한 포스팅에 담으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL (Structured Query Language)은 한글로 번역하면 구조적 질의어 라는 뜻으로, 관계형 데이터베이스에서 구조화된 데이터를 저장하고 처리하는 등 관리하는데 사용하는 언어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 SQL은 4가지로 분류할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DDL(데이터 정의어): CREATE, ALTER, DROP, TRUNCATE, RENAME&lt;/li&gt;
&lt;li&gt;DML(데이터 조작어): SELECT, INSERT, UPDATE, DELETE&lt;/li&gt;
&lt;li&gt;DCL(데이터 제어어): GRANT, REVOKE&lt;/li&gt;
&lt;li&gt;TCL(트랜잭션 제어어): COMMIT, ROLLBACK, SAVEPOINT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 SQL문을 조금 더 구체적으로 살펴보자. 먼저 DDL이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DDL&lt;/b&gt;은 데이터 정의어라는 뜻으로, 데이터베이스의 구조를 정의(생성/수정/삭제)할 때 사용하는 문장이다. 구조에는 스키마, 테이블, 뷰, 인덱스 등 많은 것이 있다. DDL의 특징으로는 자동 커밋이 있으며 이 때문에 롤백이 어려우므로 DDL을 사용할 때는 신중하게 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- PostgreSQL은 메타데이터(테이블 구조를 가지고 있는 시스템 카탈로그)도 MVCC로 관리하므로 롤백이 가능한 예외가 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DML&lt;/b&gt;은 데이터 조작어라는 뜻으로, 구조 내 데이터를 조작(조회/입력/수정/삭제)할 때 사용하는 문장이다. 특징으로는 트랜잭션의 영향을 받는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DCL&lt;/b&gt;은 데이터 제어어라는 뜻으로, 데이터베이스에 대한 접근 권한과 보안 정책을 제어할 때 사용하는 문장이다. 어떤 사용자가 특정 테이블에 접근 할 수 있는지, 접근 가능하다면 어떠한 DML을 실행할 수 있는지 등의 권한 설정이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCL&lt;/b&gt;은 트랜잭션 제어어라는 뜻으로, 하나의 작업 단위인 트랜잭션을 확정하거나 취소할 때 사용하는 문장이다. 여러 SQL 작업을 하나의 논리적인 단위로 묶어서 한 작업만 실패해도 묶인 모든 작업이 실패하도록 묶는 것이 트랜잭션이다. 대표적인 예가 계좌 송금이 있는데 내 계좌에서 돈이 빠져나가는 것, 상대 계좌에 돈이 들어오는 것, 이 2개의 작업이 트랜잭션으로 묶여야 한다. 만약 내 계좌에서 돈이 빠져나가는데 성공했지만 상대 계좌에 돈이 들어오는 것이 실패한다면? 난 입금했는데 상대방은 돈을 못 받았다고 화를 낼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 SQL문에 대해서 알아보았으니 위클리페이퍼 주제에 대해서 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL과 DML의 차이점은? DDL은 DB상의 어떤 구조를 만드는 것이고 DML은 DDL로 만들어진 구조 내에 데이터를 조작하는 것이라는 차이점이 있다. 또한 일부를 제외하면 DDL은 트랜잭션이 불가능하고, DML은 트랜잭션의 영향을 받는다는 것이 가장 큰 차이점이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 역정규화에 대해서 이야기 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;325&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTsKJp/dJMcabwy8pz/izQH32XzjpryIHZL1kE2JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTsKJp/dJMcabwy8pz/izQH32XzjpryIHZL1kE2JK/img.png&quot; data-alt=&quot;ERDCloud에 있는 OKKY 커뮤니티 ERD&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTsKJp/dJMcabwy8pz/izQH32XzjpryIHZL1kE2JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTsKJp%2FdJMcabwy8pz%2FizQH32XzjpryIHZL1kE2JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;325&quot; height=&quot;469&quot; data-origin-width=&quot;325&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ERDCloud에 있는 OKKY 커뮤니티 ERD&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 사진은 ERDCloud 사이트에 있는 개발자 커뮤니티인 OKKY의 ERD 중 article 테이블이다. 여기서 주목해야 할 칼럼은 note_count, scrap_count, view_count, vote_count이다. 이처럼 커뮤니티/피드형 웹서비스에서는 이러한 집계형 칼럼을 역정규화 하는 것이 일반적이다. 게시글에는 조회수, 댓글수 등이 항상 표시되어야 하는데 각 게시글마다 매번 조인해서 count 집계함수로 센다면 조회 비용이 너무 비싸므로 게시글 자체에 집계형 칼럼을 가지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 역정규화가 필요한 예시를 보았으니 역정규화를 적용 시 고려해야 할 사항을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 정규화를 잘 지키고 있는 테이블이라면 데이터 중복을 줄이고 정합성과 갱신 안정성을 높이지만, 조회 패턴이 강한 서비스에서는 테이블 구조에 따라서 JOIN으로 인한 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 무작정 조회를 많이 한다고 역정규화를 하면 되는가? 그건 절대 아니다. 역정규화를 하면 갱신 이상이 발생할 수 있기 때문에 정합성 관리 비용이 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 우리가 조회수 테이블의 숫자와 view_count가 네트워크 장애로 인해 달라진다면? 주기적으로 원본 데이터에서 값을 동기화해서 정합성을 관리해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 데이터가 자주 수정된다면? 원본 데이터가 수정될 때마다 그에 따른 역정규화 데이터도 수정해야해서 수정 비용이 비싸진다. 이처럼 역정규화를 적용하기 전에는 정말로 적용해야 하는가 충분히 고려해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 역정규화를 적용할 때 어떠한 장점이 있고 어떠한 단점이 생길지 알아보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1697&quot; data-start=&quot;1685&quot; data-section-id=&quot;1enpyy8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1699&quot; data-section-id=&quot;1hrqpoz&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1928&quot; data-start=&quot;1706&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1762&quot; data-start=&quot;1706&quot; data-section-id=&quot;fdjddp&quot;&gt;&lt;b&gt;조회 성능 향상&lt;/b&gt;: 목록 화면에서 JOIN/집계 없이 O(1)로 수치를 가져올 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1813&quot; data-start=&quot;1763&quot; data-section-id=&quot;xt7s4k&quot;&gt;&lt;b&gt;정렬/랭킹 최적화&lt;/b&gt;: 추천순/댓글순 등 집계값 기반 정렬이 빠르고 단순해진다.&lt;/li&gt;
&lt;li data-end=&quot;1874&quot; data-start=&quot;1814&quot; data-section-id=&quot;zd61cx&quot;&gt;&lt;b&gt;DB 부하 감소&lt;/b&gt;: 대량 집계 쿼리(GROUP BY)가 줄어들어 전체적인 DB 자원이 절약된다.&lt;/li&gt;
&lt;li data-end=&quot;1928&quot; data-start=&quot;1875&quot; data-section-id=&quot;wukee3&quot;&gt;&lt;b&gt;캐시 친화적&lt;/b&gt;: 게시글 row만 읽으면 되므로 애플리케이션 캐시/DB 캐시에 유리하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1936&quot; data-start=&quot;1930&quot; data-section-id=&quot;1hrgjzy&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2154&quot; data-start=&quot;1937&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1994&quot; data-start=&quot;1937&quot; data-section-id=&quot;1lpvhjk&quot;&gt;&lt;b&gt;정합성 관리 비용 증가&lt;/b&gt;: 원본 데이터 변경 시 카운터도 함께 갱신해야 한다(갱신 이상).&lt;/li&gt;
&lt;li data-end=&quot;2046&quot; data-start=&quot;1995&quot; data-section-id=&quot;n997ir&quot;&gt;&lt;b&gt;동시성/락 경합 가능성&lt;/b&gt;: 인기 글에 업데이트가 몰리면 성능이 떨어질 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2104&quot; data-start=&quot;2047&quot; data-section-id=&quot;y45f2g&quot;&gt;&lt;b&gt;장애/롤백 시 불일치 위험&lt;/b&gt;: 트랜잭션 경계가 어긋나면 원본과 카운터가 불일치할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2154&quot; data-start=&quot;2105&quot; data-section-id=&quot;1x4qh43&quot;&gt;&lt;b&gt;운영 복잡도 증가&lt;/b&gt;: 재계산 배치, 감사/검증 로직 등 운영 도구가 필요해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론이다. 이론적으로 데이터 모델링의 기본은 정규화를 통해 중복을 제거하고 정합성과 갱신 안정성을 확보하는 것이다. 다만 실무에서는 &amp;ldquo;읽기 트래픽이 압도적으로 많은&amp;rdquo; 서비스에서 정규화된 구조가 조회 시 JOIN/집계 비용을 크게 만들 수 있으며, 이는 곧 응답 지연과 DB 부하로 이어진다. 따라서 커뮤니티/피드형 서비스처럼 조회가 핵심인 도메인에서는 조회 성능을 확보하기 위해 카운터 캐시와 같은 역정규화를 선택적으로 적용한다. 단, 역정규화는 원본 데이터와의 불일치(정합성) 가능성을 수반하므로 동시성 제어, 증감 로직, 재계산(리빌드) 전략까지 함께 설계되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/16</guid>
      <comments>https://memo50984.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 3 Mar 2026 10:03:44 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][7주차] @Restcontroller와 HTTPMessageConverter</title>
      <link>https://memo50984.tistory.com/13</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 7주차 위클리페이퍼 두 번째 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Spring&amp;nbsp;Boot에서&amp;nbsp;@RestController로&amp;nbsp;들어온&amp;nbsp;HTTP&amp;nbsp;요청이&amp;nbsp;처리되어&amp;nbsp;응답으로&amp;nbsp;변환되는&amp;nbsp;전체&amp;nbsp;과정을&amp;nbsp;설명하세요.&amp;nbsp;특히&amp;nbsp;HTTP&amp;nbsp;메시지&amp;nbsp;컨버터가&amp;nbsp;동작하는&amp;nbsp;시점과&amp;nbsp;역할을&amp;nbsp;포함해서&amp;nbsp;설명하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트로 웹 API를 만들때 컨트롤러에 @RestController를 사용한다. 왜 우리는 @Controller 대신 @RestController를 사용할까? 그 이유는 웹 아키텍처의 패러다임의 변화이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 전통적인 웹 개발 환경에서는 서버가 화면까지 모두 반환하는 서버 사이드 렌더링 방식이 주를 이뤘다. JSP나 Thymeleaf와 같은 템플릿 엔진을 사용해 핸들러 메서드의 반환값을 Model에 담아 View와 결합했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현대로 넘어오면서 모바일 기기와 웹 애플리케이션의 복잡성으로 인해 SSR만으로는 UX를 개선하기가 어려워진다. 이를 해결하기 위해 클라이언트 사이드 렌더링(클라이언트가 페이지를 렌더링) 개념이 등장하고 이로 인해 프론트엔드와 백엔드의 분리가 시작되었다. 그 과정에서 백엔드, 즉 서버는 더 이상 화면을 반환하지 않고 HTTP 통신을 통해 데이터(주로 JSON) 를 주고받는 역할에 집중하게 되었다. 이러한 변화 속에서 등장한 것이 바로 @ResponseBody이다. 이 어노테이션은 명시적으로 View 대신 HTTP 응답 바디에 핸들러 메서드가 반환하는 값을 매핑한다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 얘기 할 주제인 @RestController가 @Controller에 @ResponseBody가 결합된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 @RestController로 들어온 HTTP 요청이 응답으로 변환되는 과정을 한번 흐름도로 살펴보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;8191&quot; data-origin-height=&quot;4212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ISAU/dJMcaca78Je/etNTkJpwmwAodrmkPTMeOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ISAU/dJMcaca78Je/etNTkJpwmwAodrmkPTMeOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ISAU/dJMcaca78Je/etNTkJpwmwAodrmkPTMeOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ISAU%2FdJMcaca78Je%2FetNTkJpwmwAodrmkPTMeOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;8191&quot; height=&quot;4212&quot; data-origin-width=&quot;8191&quot; data-origin-height=&quot;4212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 클라이언트가 HTTP 요청을 서버에 전송하면 Tomcat 서블릿 컨테이너가 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 톰캣 내부적으로 &lt;b&gt;DispatcherServlet&lt;/b&gt;이 이 요청을 처리할 수 있는 컨트롤러가 있는지 &lt;b&gt;HandlerMapping&lt;/b&gt;을 통해서 찾는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 실행 가능한 컨트롤러가 있다면 &lt;b&gt;HandlerAdapter&lt;/b&gt;를 불러서 실행을 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 컨트롤러 메서드의 파라미터에 따라서(@RequestBody, @PathVariable 등) &lt;b&gt;ArgumentResolver&lt;/b&gt;를 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 이때 @RequestBody가 붙어있다면 &lt;b&gt;RequestResponseBodyMethodProcessor&lt;/b&gt;를 Resolver로 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 이 리졸버는&lt;b&gt; HttpMessageConverter&lt;/b&gt;를 호출해서 JSON 데이터를 Java 객체로 역직렬화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 생성된 자바 객체를 가지고 핸들러 메서드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 핸들러 메서드는 &lt;b&gt;HandlerMethodReturnValueHandler&lt;/b&gt;을 통해서 리턴값을 누가 처리할 수 있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. @ResponseBody가 있다면 &lt;b&gt;RequestResponseBodyMethodProcessor&lt;/b&gt;를 ReturnValueHandler로 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. 이 ReturnValueHandler는 &lt;b&gt;HttpMessageConverter&lt;/b&gt;를 호출해서 Java 객체를 JSON 데이터로 직렬화하고 HttpServletResponse에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10. 그리고 mavContainer.setRequestHandled(true)를 호출하여 응답 처리가 끝났음을 명시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11. HandlerAdapter는 null을 반환하고 DisaptcherServlet은 반환값이 null이고 requestHandled가 true임을 확인하여 ViewResolver를 스킵하고 JSON 데이터를 클라이언트에게 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 포인트는 &lt;b&gt;RequestResponseBodyMethodProcessor&lt;/b&gt;가 AugumemtResolver와 ReturnValueHandler를 둘다 수행한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 알 수 있는 HttpMessageConverter의 역할은 JSON 데이터 -&amp;gt; Java객체 역직렬화, Java 객체 -&amp;gt; JSON직렬화 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 httpMessageConverter는 무엇이고 위의 상황에서 사용된 컨버터는 어떤 컨버터인지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpMessageConverter는 HTTP 요청 바디와 응답 바디를 자바 객체로 직렬화/역직렬화 하는 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 메서드는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;canRead(clazz, mediaType): 해당 자바 타입과 HTTP 요청의 Content-Type을 지원하는지 확인&lt;/li&gt;
&lt;li&gt;canWrite(clazz, mediaType): 반환할 자바 타입과 요청의 Accept 헤더를 지원하는지 확인&lt;/li&gt;
&lt;li&gt;read() / write(): 실제로 변환 로직을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이를 구현하는 여러 컨버터가 있으며 루프를 돌면서 우선순위가 높은 것 중 can~메서드의 조건에 맞는 것을 선택한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 89px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 13.1395%; text-align: center; height: 21px;&quot;&gt;우선순위&lt;/td&gt;
&lt;td style=&quot;width: 34.8837%; text-align: center; height: 21px;&quot;&gt;컨버터 클래스명&lt;/td&gt;
&lt;td style=&quot;width: 21.628%; text-align: center; height: 21px;&quot;&gt;지원하는 데이터 형식&lt;/td&gt;
&lt;td style=&quot;width: 30.3488%; text-align: center; height: 21px;&quot;&gt;비고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 34.8837%; height: 17px;&quot;&gt;ByteArrayHttpMessageConverter&lt;/td&gt;
&lt;td style=&quot;width: 21.628%; height: 17px;&quot;&gt;byte[]&lt;/td&gt;
&lt;td style=&quot;width: 30.3488%; height: 17px;&quot;&gt;이미지, 바이너리 파일 처리 시 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 34.8837%; height: 17px;&quot;&gt;StringHttpMessageConverter&lt;/td&gt;
&lt;td style=&quot;width: 21.628%; height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;12,2,1,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;12,2,2,0&quot;&gt;String&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.3488%; height: 17px;&quot;&gt;단순 텍스트 평문을 처리할 때 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 34.8837%; height: 17px;&quot;&gt;MappingJackson2HttpMessageConverter&lt;/td&gt;
&lt;td style=&quot;width: 21.628%; height: 17px;&quot;&gt;application/json&lt;/td&gt;
&lt;td style=&quot;width: 30.3488%; height: 17px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,3,3,0&quot;&gt;REST API 핵심 / &lt;/b&gt;Jackson 라이브러리를 사용해 JSON 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 34.8837%; height: 17px;&quot;&gt;Jaxb2RootElementHttpMessageConverter&lt;/td&gt;
&lt;td style=&quot;width: 21.628%; height: 17px;&quot;&gt;application/xml&lt;/td&gt;
&lt;td style=&quot;width: 30.3488%; height: 17px;&quot;&gt;XML 데이터를 처리할 때 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 x-www-form, octet-stream 등 더 많은 컨버터가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/13</guid>
      <comments>https://memo50984.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 23 Feb 2026 09:10:02 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][7주차] SOAP에서 REST로의 전환</title>
      <link>https://memo50984.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 7주차 위클리페이퍼 첫 번째 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;웹 API의 발전 과정에서 SOAP에서 REST로의 전환이 일어난 이유와 그 장단점에 대해 설명하세요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 웹 API란, 웹(네트워크)을 통해 서로 다른 애플리케이션이 데이터를 주고받을 수 있도록 정의된 약속이다. 우리가 흔히 접하는 예로는 프론트엔드(클라이언트)가 백엔드(서버)의 API를 호출하여 데이터를 주고받는 구조가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; 1. 과거의 웹 API: 엔터프라이즈 시스템 통합 &lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;486&quot; data-start=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;그러나 과거에는 지금처럼 브라우저나 모바일 기기와 서버 간 통신이 주요 목적이 아니었다. 웹 API의 목적은 이기종 엔터프라이즈급 시스템을 연결하는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;542&quot; data-start=&quot;496&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;511&quot; data-start=&quot;496&quot;&gt;은행사 A는 Java&lt;/li&gt;
&lt;li data-end=&quot;527&quot; data-start=&quot;512&quot;&gt;보험사 B는 .NET&lt;/li&gt;
&lt;li data-end=&quot;542&quot; data-start=&quot;528&quot;&gt;카드사 C는 C++&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;576&quot; data-start=&quot;544&quot; data-ke-size=&quot;size16&quot;&gt;로 작성된 엔터프라이즈 애플리케이션을 사용한다고 가정하자.&lt;/p&gt;
&lt;p data-end=&quot;678&quot; data-start=&quot;578&quot; data-ke-size=&quot;size16&quot;&gt;이때 서로 다른 언어와 메모리 구조를 가진 시스템들이 데이터를 교환해야 하는 상황이 발생한다. 그러나 각 플랫폼의 차이와 기업 방화벽 정책으로 인해 직접적인 통신은 매우 어려웠다.&lt;/p&gt;
&lt;p data-end=&quot;721&quot; data-start=&quot;680&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 등장한 웹 API가 바로 &lt;b&gt;SOAP&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;721&quot; data-start=&quot;680&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;746&quot; data-start=&quot;728&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. SOAP의 등장과 특징&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;846&quot; data-start=&quot;748&quot; data-ke-size=&quot;size16&quot;&gt;SOAP은 모든 방화벽에 기본적으로 열려 있는 HTTP(80번 포트)를 통신 수단으로 사용하고, 데이터를 XML이라는 텍스트 기반 구조로 통일하여 플랫폼 간 표준화를 이루었다.&lt;/p&gt;
&lt;p data-end=&quot;892&quot; data-start=&quot;848&quot; data-ke-size=&quot;size16&quot;&gt;SOAP은 단순한 웹 API를 넘어, 표준 메시지 &amp;ldquo;프로토콜&amp;rdquo;로 설계되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;994&quot; data-start=&quot;894&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;911&quot; data-start=&quot;894&quot;&gt;XML 기반 메시지 작성&lt;/li&gt;
&lt;li data-end=&quot;968&quot; data-start=&quot;912&quot;&gt;WSDL(Web Services Description Language)을 통한 인터페이스 정의&lt;/li&gt;
&lt;li data-end=&quot;994&quot; data-start=&quot;969&quot;&gt;WS-*라는 기능 확장 표준 스펙 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1051&quot; data-start=&quot;996&quot; data-ke-size=&quot;size16&quot;&gt;특히 WS-*는 엔터프라이즈급 시스템에서 필요한 보안성과 신뢰성을 제공하기 위해 만들어진 규격이다.&lt;/p&gt;
&lt;p data-end=&quot;1075&quot; data-start=&quot;1053&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 표준이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1157&quot; data-start=&quot;1077&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1095&quot; data-start=&quot;1077&quot;&gt;WS-Security (보안)&lt;/li&gt;
&lt;li data-end=&quot;1124&quot; data-start=&quot;1096&quot;&gt;WS-ReliableMessaging (신뢰성)&lt;/li&gt;
&lt;li data-end=&quot;1157&quot; data-start=&quot;1125&quot;&gt;WS-AtomicTransaction (분산 트랜잭션)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1245&quot; data-start=&quot;1159&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 SOAP은 보안, 신뢰성, 트랜잭션을 프로토콜 수준에서 지원하였다. 그러나 이러한 기능들은 동시에 SOAP의 복잡성과 오버헤드의 원인이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1275&quot; data-start=&quot;1252&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. SOAP과 MSA의 구조적 충돌&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1352&quot; data-start=&quot;1277&quot; data-ke-size=&quot;size16&quot;&gt;시간이 지나면서 엔터프라이즈 중심 환경에서 벗어나 MSA(마이크로서비스 아키텍처)와 모바일&amp;middot;개인화 기기가 중심이 되는 시대가 도래했다.&lt;/p&gt;
&lt;p data-end=&quot;1412&quot; data-start=&quot;1354&quot; data-ke-size=&quot;size16&quot;&gt;SOAP은 이기종 플랫폼 통합을 목표로 설계되었기 때문에 MSA와 구조적으로 맞지 않는 부분이 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;1505&quot; data-start=&quot;1414&quot; data-ke-size=&quot;size16&quot;&gt;WS-AtomicTransaction은 여러 시스템을 하나의 트랜잭션으로 묶어 강한 일관성을 유지해서 하나가 실패하면 전체가 롤백된다.&lt;/p&gt;
&lt;p data-end=&quot;1626&quot; data-start=&quot;1507&quot; data-ke-size=&quot;size16&quot;&gt;반면 MSA는 작은 서비스들이 독립적으로 동작하며, 부분 실패를 허용하고 재시도를 통해 **최종 일관성(Eventual Consistency)**을 달성하는 것이 더 유리하다. 이를 위해 Saga 패턴이 사용된다. WSDL 기반의 강한 계약 구조는 서비스 간 강한 결합을 유발하여 독립 배포와 확장성을 저해할 수 있다. 복잡한 XML 파싱과 WS-* 스펙 처리 과정은 대규모 엔터프라이즈 시스템에서는 문제가 되지 않았지만, 경량 클라이언트 환경에서는 부담이 될 수 있다. 이러한 배경 속에서 등장한 것이&lt;b&gt; REST&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1835&quot; data-start=&quot;1814&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. REST의 등장과 핵심 특징&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1897&quot; data-start=&quot;1837&quot; data-ke-size=&quot;size16&quot;&gt;REST는 SOAP처럼 하나의 표준 메시지 프로토콜이 아니라, HTTP를 기반으로 하는 아키텍처 스타일이다.&lt;/p&gt;
&lt;p data-end=&quot;1916&quot; data-start=&quot;1899&quot; data-ke-size=&quot;size16&quot;&gt;REST의 핵심은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2007&quot; data-start=&quot;1918&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1941&quot; data-start=&quot;1918&quot;&gt;자원(Resource)을 URI로 표현&lt;/li&gt;
&lt;li data-end=&quot;1989&quot; data-start=&quot;1942&quot;&gt;HTTP 메서드(GET, POST, PUT, DELETE 등)의 의미를 적극 활용&lt;/li&gt;
&lt;li data-end=&quot;2007&quot; data-start=&quot;1990&quot;&gt;Stateless 구조 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2083&quot; data-start=&quot;2009&quot; data-ke-size=&quot;size16&quot;&gt;Stateless 구조는 서버가 클라이언트의 상태를 저장하지 않는 것을 의미하며, 수평 확장과 오토 스케일링 환경에서 매우 유리하다.&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2083&quot; data-start=&quot;2009&quot; data-ke-size=&quot;size16&quot;&gt;다음으로는 REST의 6가지 제약 조건이다.&lt;/p&gt;
&lt;p data-end=&quot;2083&quot; data-start=&quot;2009&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2111&quot; data-start=&quot;2090&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;5. REST의 6가지 제약 조건&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2544&quot; data-start=&quot;2113&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2182&quot; data-start=&quot;2113&quot;&gt;&lt;b&gt;Client-Server&lt;/b&gt;&lt;br /&gt;클라이언트는 UI/UX를 담당하고, 서버는 데이터와 비즈니스 로직을 담당한다.&lt;/li&gt;
&lt;li data-end=&quot;2253&quot; data-start=&quot;2184&quot;&gt;&lt;b&gt;Stateless&lt;/b&gt;&lt;br /&gt;서버는 클라이언트 상태를 저장하지 않으며, 모든 요청은 필요한 정보를 포함해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;2310&quot; data-start=&quot;2255&quot;&gt;&lt;b&gt;Cacheable&lt;/b&gt;&lt;br /&gt;HTTP의 캐시 메커니즘을 활용하여 응답을 캐시할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2384&quot; data-start=&quot;2312&quot;&gt;&lt;b&gt;Uniform Interface&lt;/b&gt;&lt;br /&gt;모든 자원은 URI로 식별되며, HTTP 메서드의 의미를 일관되게 사용한다.&lt;/li&gt;
&lt;li data-end=&quot;2482&quot; data-start=&quot;2386&quot;&gt;&lt;b&gt;Layered System&lt;/b&gt;&lt;br /&gt;클라이언트는 자신이 최종 서버와 직접 통신하는지, 중간 계층(API Gateway, 프록시 등)을 거치는지 알 필요가 없다.&lt;/li&gt;
&lt;li data-end=&quot;2544&quot; data-start=&quot;2484&quot;&gt;&lt;b&gt;Code-On-Demand (선택)&lt;/b&gt;&lt;br /&gt;필요 시 클라이언트에 실행 코드를 전달할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 이러한 제약 조건을 통해 현대의 웹 환경에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 REST의 장단점은 무엇이 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2565&quot; data-start=&quot;2551&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;6. REST의 장점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2588&quot; data-start=&quot;2567&quot; data-ke-size=&quot;size16&quot;&gt;REST는 다음과 같은 장점을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2792&quot; data-start=&quot;2590&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2657&quot; data-start=&quot;2590&quot;&gt;&lt;b&gt;확장성에 유리하다.&lt;/b&gt;&lt;br /&gt;Stateless 구조 덕분에 로드 밸런싱과 오토 스케일링이 자연스럽게 이루어진다.&lt;/li&gt;
&lt;li data-end=&quot;2728&quot; data-start=&quot;2659&quot;&gt;&lt;b&gt;웹 인프라와 자연스럽게 통합된다.&lt;/b&gt;&lt;br /&gt;HTTP 캐시, CDN, 프록시, API Gateway와 쉽게 연동된다.&lt;/li&gt;
&lt;li data-end=&quot;2792&quot; data-start=&quot;2730&quot;&gt;&lt;b&gt;구현이 비교적 단순하고 가볍다.&lt;/b&gt;&lt;br /&gt;JSON을 사용하여 메시지 구조가 간결하고 파싱 비용이 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2813&quot; data-start=&quot;2799&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;7. REST의 단점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3045&quot; data-start=&quot;2815&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2918&quot; data-start=&quot;2815&quot;&gt;&lt;b&gt;통합된 표준 스택이 존재하지 않는다.&lt;/b&gt;&lt;br /&gt;SOAP은 WS-*로 기능을 통합했지만, REST는 TLS, OAuth, 메시지 브로커, Saga 패턴 등으로 분리하여 해결한다.&lt;/li&gt;
&lt;li data-end=&quot;2973&quot; data-start=&quot;2920&quot;&gt;&lt;b&gt;강한 분산 ACID 트랜잭션을 지원하지 않는다.&lt;/b&gt;&lt;br /&gt;대신 최종 일관성을 채택한다.&lt;/li&gt;
&lt;li data-end=&quot;3045&quot; data-start=&quot;2975&quot;&gt;&lt;b&gt;설계 일관성이 깨질 위험이 있다.&lt;/b&gt;&lt;br /&gt;아키텍처 스타일이므로 원칙을 지키지 않으면 RPC 형태로 변질될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3060&quot; data-start=&quot;3052&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;8. 결론&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3195&quot; data-start=&quot;3062&quot; data-ke-size=&quot;size16&quot;&gt;SOAP은 이기종 엔터프라이즈 시스템을 강한 계약과 표준화된 스펙으로 통합하기 위해 등장한 기술이다. 그러나 클라우드와 마이크로서비스 중심의 환경에서는 확장성과 독립 배포가 더욱 중요해졌고, 이에 따라 REST가 주류로 자리 잡게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;3295&quot; data-start=&quot;3197&quot; data-ke-size=&quot;size16&quot;&gt;EJB가 사라지고 Spring이 등장한 것과 달리, SOAP은 기술적으로 도태된 것이 아니다. 강한 일관성과 신뢰성이 중요한 금융권이나 레거시 환경에서는 여전히 사용되고 있다.&lt;/p&gt;
&lt;p data-end=&quot;3361&quot; data-start=&quot;3297&quot; data-ke-size=&quot;size16&quot;&gt;결국 SOAP에서 REST로의 전환은 단순한 기술 교체가 아니라, &lt;b&gt;아키텍처 패러다임의 변화&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/12</guid>
      <comments>https://memo50984.tistory.com/12#entry12comment</comments>
      <pubDate>Mon, 23 Feb 2026 09:09:17 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][6주차] Spring AOP의 필요성과 실제 사례</title>
      <link>https://memo50984.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;6주차 위클리페이퍼의 첫 번째 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Springo| AOP(Aspect Oriented Programming)가 필요한 이유와 이를 활용한 실제 애플리케이션 개발 사례에 대해 설명하세요.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 AOP에 대해서 기본 개념을 설명하자면 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라는 뜻으로, 공통 관심사 (Cross Cutting Concern)를 비즈니스 로직과 분리하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP가 필요한 이유는 핵심 비즈니스 로직 이외에 공통으로 필요한 기능(공통 관심사)를 한 곳으로 모아서 적용하므로 다음과 같은 장점이 있기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 중복 방지: 공통적으로 반복되는 기능을 한 곳으로 모아서 중복 제거&lt;/li&gt;
&lt;li&gt;일관성 유지 및 유지보수성 향상: 공통 기능이 다르게 작동하지 않고 일관되게 작동할 수 있도록 함으로써 유지보수성 향상&lt;/li&gt;
&lt;li&gt;관심사의 분리: 개발자가 핵심 비즈니스 로직에만 집중할 수 있도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 공통 관심사는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 로깅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 권한 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 성능 측정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 예외 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 캐싱&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 비동기 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 AOP와 관련된 어노테이션이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@Aspect&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;이 클래스가 AOP 횡단 관심사(공통 기능)를 정의한 클래스임을 스프링에 알립니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@Pointcut&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;AOP 기능이 적용될 지점(메서드, 패키지 등)을 정의합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@Before&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;대상 메서드가 실행되기 직전에 동작합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@After&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;대상 메서드 실행 후 동작합니다. (정상 종료, 예외 발생 상관없이 무조건 실행되는 finally 블록 같은 역할)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@AfterReturning&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;대상 메서드가 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;5,2,1,2,0&quot;&gt;정상적으로 종료&lt;/b&gt;되었을 때만 동작하며, 반환값을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@AfterThrowing&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;대상 메서드에서 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;5,2,1,3,0&quot;&gt;예외가 발생&lt;/b&gt;했을 때만 동작합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.1861%;&quot;&gt;@Around&lt;/td&gt;
&lt;td style=&quot;width: 80.8139%;&quot;&gt;가장 강력한 어드바이스로, 메서드 실행 전후의 제어권을 모두 가집니다.(@Before + @After)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 @Transaction, @Async 어노테이션은 AOP로 작동된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유의 해야할 점으로 AOP는 현재 스프링에서 CGlib 방식이며 상속을 기반으로 작동하기에 public(또는 protected)에만 사용할 수 있으며 self invocation이 발생할 수 있다.(동일 클래스 내에서 AOP 작동 안됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 애플리케이션 개발에서는 어떻게 AOP를 적용하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 RestController에 적용하는 로깅 AOP의 형태이다.&lt;/p&gt;
&lt;pre id=&quot;code_1771803290376&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
@Slf4j
public class LoggingAspect {

    @Pointcut(&quot;@within(org.springframework.web.bind.annotation.RestController)&quot;)
    public void restController() {}

    @Around(&quot;restController()&quot;)
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();

        log.info(&quot;▶ {}.{} start&quot;, className, methodName);

        try {
            Object result = joinPoint.proceed();
            long time = System.currentTimeMillis() - start;

            log.info(&quot;✔ {}.{} end ({}ms)&quot;, className, methodName, time);
            return result;

        } catch (Exception e) {
            log.error(&quot;✖ {}.{} exception: {}&quot;, className, methodName, e.getMessage());
            throw e;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 컨트롤러 코드가 다음과 같다고 가정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771803408891&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {

    @GetMapping(&quot;/{id}&quot;)
    public String findUser(@PathVariable Long id) {
        return &quot;User &quot; + id;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 GET /users/1 요청을 할 때 호출 순서는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client &lt;br /&gt;&amp;nbsp;&amp;rarr;&amp;nbsp;DispatcherServlet &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;CGLIB&amp;nbsp;Proxy(UserController) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;LoggingAspect&amp;nbsp;(@Around) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;UserController.findUser()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 로그는 이렇게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶&amp;nbsp;com.example.UserController.findUser&amp;nbsp;start &lt;br /&gt;▶ com.example.UserController.findUser end&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/10</guid>
      <comments>https://memo50984.tistory.com/10#entry10comment</comments>
      <pubDate>Mon, 9 Feb 2026 16:02:22 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][5주차] Web Server와 Web Application Server</title>
      <link>https://memo50984.tistory.com/8</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;5주차 위클리페이퍼 주제이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li style=&quot;list-style-type: circle;&quot;&gt;웹&amp;nbsp;서버(Web&amp;nbsp;Server)와&amp;nbsp;WAS(Web&amp;nbsp;Application&amp;nbsp;Server)의&amp;nbsp;차이를&amp;nbsp;설명하고,&amp;nbsp;Spring&amp;nbsp;Boot의&amp;nbsp;내장&amp;nbsp;톰캣이&amp;nbsp;이&amp;nbsp;둘&amp;nbsp;중&amp;nbsp;어디에&amp;nbsp;해당하는지&amp;nbsp;설명해주세요.&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle;&quot;&gt;Spring&amp;nbsp;Boot에서&amp;nbsp;사용되는&amp;nbsp;다양한&amp;nbsp;Bean&amp;nbsp;등록&amp;nbsp;방법들에&amp;nbsp;대해&amp;nbsp;설명하고,&amp;nbsp;각각의&amp;nbsp;장단점을&amp;nbsp;비교하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 포스팅은 첫 번째 주제에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버란 HTTP 프로토콜을 통해 인터넷을 사용하는 사용자에게 요청을 받아 응답(웹 서비스)을 제공하기 위한 서버이다. 우리가 인터넷을 사용하면 가장 먼저 하는 일이 무엇인가? 바로 주소를 입력하는 것이다. 바로가기 아이콘을 눌러서든 직접 URL을 입력하든 주소창에 우리가 이용할 웹사이트의 주소를 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 네이버를 생각해보자. &lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com &lt;/a&gt;또는 &lt;a href=&quot;http://m.naver.com&quot;&gt;m.naver.com &lt;/a&gt;이라는 주소를 입력하면 우리는 네이버의 홈페이지를 볼 수 있다. 이 과정을 살펴보면 주소창에 위의 주소를 입력하는 것이 네이버 회사에 웹 서비스 제공을 요청한 것이다.이 요청을 받은 네이버는 웹 서버를 통해서 HTML, CSS, 이미지 등으로 이루어진 홈화면을 우리의 브라우저에 응답(전달) 해 주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 좀 더 이해하기 쉽게 식당에 비유해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 식당에 손님이 들어와서 메뉴판을 본다. 메뉴판에 있는 메뉴는 주소창에 입력할 수 있는 URL이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손님이 메뉴판에 있는 치즈버거를 보고 &quot;저기요, 치즈버거 하나 주세요&quot;라고 주문을 하면 이 행위가 주소창에 URL을 입력해 HTTP 요청을 한 것이다. 그러면 주문을 받은 점원이 주방에 전달하고, 주방은 만들어진 치즈버거를 우리에게 갖다주는데 점원이 웹 서버고, 치즈버거가 바로 요청한 웹페이지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 만약에 치즈버거가 편의점 햄버거라고 가정해보자. 우리는 버거에 토핑을 추가하거나 뺼 수 없다. 그래서 패티가 2개 있는 햄버거가 먹고싶으면 아예 다른 햄버거를 사야한다. 만약 맥도날드였다면? 우리는 그냥 키오스크에서 주문할 때 패티 추가 또는 토마토 제외 버튼을 누르면 동일한 버거에서 내용물을 바꿔 끼울 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서 이미 만들어진 편의점 버거가 정적 콘텐츠이고, 맥도날드 버거가 동적 콘텐츠이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트의 일부가 수정될 때(가령, 마이페이지에서 사용자의 닉네임만 다르게 표시해야될 때) 정적인 웹 페이지로 관리해야한다면 모든 사용자의 개수만큼 정적 콘텐츠를 가지고 있어야한다. 이때, 만약 위에서 언급한 맥도날드 버거처럼 내용물을 갈아끼울 수 있다면? 하나의 마이페이지에서 닉네임만 갈아끼우면 되는 것이다. 이것이 바로 동적 컨텐츠의 필요성이다. 이러한 동적 컨텐츠를 사용할 수 있는 것이 바로 WAS(Web Application Server)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 WS와 WAS를 어느정도 알았으니 각 특징을 한눈에 비교해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Web&amp;nbsp;Server&amp;nbsp;(WS)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 역할: 정적(Static) 콘텐츠 처리 (HTML, CSS, 이미지 등)&lt;/li&gt;
&lt;li&gt;특징: 비즈니스 로직 수행 없이 파일 시스템에 있는 파일을 그대로 클라이언트에게 전달합니다.&lt;/li&gt;
&lt;li&gt;대표 주자: Nginx, Apache HTTP Server&lt;/li&gt;
&lt;li&gt;키워드: Event-Driven, Zero Copy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; WAS (Web Application Server)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 역할: 동적(Dynamic) 콘텐츠 처리 (DB 조회, 비즈니스 로직 수행)&lt;/li&gt;
&lt;li&gt;특징: 사용자 요청에 따라 데이터를 가공하고, 코드를 실행하여 결과를 만들어냅니다.&lt;/li&gt;
&lt;li&gt;대표 주자: Apache Tomcat, Jetty&lt;/li&gt;
&lt;li&gt;키워드: Servlet Container, Thread Pool&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 Spring Boot의 내장 톰캣이 이중에서 어디에 해당하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 톰캣이 서블릿 컨테이너라는 것을 알면 바로 정답을 알 수 있다. 서블릿 컨테이너는 자바에서 서블릿을 이용하여 동적 웹 페이지를 생성할 때 사용하는 프로그램이기 때문이다. 즉 Spring Boot의 톰캣은 자바의 서블릿 컴포넌트를 사용해서 동적 콘텐츠를 처리할 수 있는 서블릿 컨테이너이며 일종의 WAS인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 한가지 재밌는 사실이 있다. 톰캣은 정적인 컨텐츠를 서빙할 수 있는 DefaultServlet이 있고 동적인 컨텐츠를 서빙할 수 있는 DispatcherServlet이 있다. 그렇다면 WAS 단독으로 정적 컨텐츠, 동적 컨텐츠 모두 처리할 수 있는데 실제로는 WAS 앞에 WS를 사용한다. 그 이유가 뭘까?&lt;br /&gt;&lt;br /&gt;전통적은 서블릿 기반(Spring MVC 등)의 블로킹 I/O 모델에서는 WAS는 어떠한 요청이든 요청 1개에 하나의 스레드를 배정한다. 그래서 정적 컨텐츠든 동적 컨텐츠이든 요청이 들어오면 무조건 스레드 하나를 점유하게 된다. 여기서 문제가 발생한다. 만약 용량이 큰 파일을 다운로드 받거나, 네트워크가 느린 사용자가 다운로드를 하면 그 사용자의 다운로드가 전부 끝날때까지 그 스레드는 아무런 연산을 하지 않은 채, 연결을 유지하며 Blocking 상태로 대기하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이러한 상황이 반복되어 Thread Pool의 모든 스레드가 대기 상태에 빠지게&amp;nbsp; 되면 정작 비즈니스 로직이나 DB조회 등의 요청을 WAS가 응답할 수 없게 된다. 이것이 스레드 기아이며 이 상태에서는 요청이 큐에서 대기하거나, 최대치에 도달하면 요청이 거부될 수 있다. 반면 Nginx같은 WS는 Event-driven 방식을 사용하기 때문에 적은 수의 worker process가&amp;nbsp; event loop 기반으로 다수의 연결을 비동기로 유지할 수 있으며 정적 파일을 전송할 때 Zero Copy기술로 데이터 복사에 필요한 CPU 사용과 컨텍스트 스위칭을 최소화 한채 파일을 전송할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 식당에 비유하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS의 스레드는 요리사이다. 음식을 만드느라(비즈니스 로직 처리) 굉장히 바쁘다. 이때 만약 서빙하는 직원이 없다면(Nginx와 같은 WS) 주문을 받으러 가는 것도 요리사이고 서빙을 해야 하는 것도 요리사이다. 이런 상황에서 말을 굉장히 느리게 주문을 하는 사람이 있고, 또는 주문 자체가 굉장히 많다면? 아니면 서빙을 하기에 굉장히 무거워서 천천히 옮겨야 하는 음식이 있다면? 요리사는 주문과 서빙을 하느라 정작 요리를 못하게 될 것이다. 그래서 서빙하는 직원(WS)가 필요한 것이다.&lt;/p&gt;</description>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/8</guid>
      <comments>https://memo50984.tistory.com/8#entry8comment</comments>
      <pubDate>Tue, 3 Feb 2026 10:24:36 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][4주차] 프레임워크와 라이브러리의 차이점</title>
      <link>https://memo50984.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;아래는 4주차 위클리페이퍼 주제이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Spring Framework가 탄생하게 된 배경과 이를 통해 해결하고자 했던 문제점에 대해 설명하세요.&lt;/li&gt;
&lt;li&gt;프레임워크와 라이브러리의 차이점을 제어 흐름의 주체와 사용 방식을 중심으로 설명하고, Spring Framework와 일반 Java 라이브러리를 예시로 들어 설명하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;이번 포스팅에서는 두 번째 주제에 대해서 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 코드를 작성할 때 이미 작성된 코드를 다른 곳에서 재사용하려면 복사 붙여넣기를 통해 사용할 수 있다. 그러나 개발자라면 복사 붙여넣기가 아닌 함수를 통해 반복되는 작업을 줄일 것이다. 더 나아가, 내가 만든 함수뿐만이 아니라 남이 만들어 놓은 코드를 사용할 수도 있다. 이러한 것을 보고 '라이브러리' 또는 '프레임워크'를 사용한다고 한다. 둘다 코드를 '재사용한다' 라는 목적은 같지만, 서로 다른 이름으로 부르는 것에는 이유가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘을 구분하는 가장 큰 기준은 이전에 스프링에 대해서 포스팅 할때 언급한 &lt;b&gt;제어의 역전(IoC, Inversion if Control)&lt;/b&gt;의 유무이다. 즉, 프로그램의 흐름제어를 개발자가 가지고 있는지 외부에서 불러온 코드에 있는지 여부이다. 이를 통해 객체의 생명주기를 관리하는 주체가 개발자인지 아닌지가 갈리는데 이 개념을 통해 프레임워크와 라이브러리를 구분할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브러리: 제어의 주체가 개발자에게 있다. 개발자가 필요할 때 라이브러리를 호출해서 사용한다. 이를 통해 객체의 생명주기는 개발자가 제어한다.&lt;/li&gt;
&lt;li&gt;프레임워크: 제어의 주체가 프레임워크에게 있다. 프레임워크가 정한 규칙에 따라 개발자가 코드를 작성하면, 프레임워크가 필요할 때 개발자가 작성한 코드를 불러서 사용한다. 이를 통해 프레임워크가 객체의 생명주기를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 제어의 주체가 넘어가는 사례를 쉽게 확인할 수 있다. 아래 코드를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1770309128806&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5);


// 라이브러리 스타일
for (Integer n : numbers) { // 루프의 시작과 끝을 개발자가 직접 제어
    if (n % 2 == 0) {       // 조건 검사 시점을 개발자가 결정
        System.out.println(n); // 출력 시점도 개발자가 결정
    }
}

// 프레임워크 스타일
numbers.stream()
       .filter(n -&amp;gt; n % 2 == 0) // &quot;짝수만 골라줘&quot;라고 규칙만 전달
       .forEach(System.out::println); // &quot;출력해줘&quot;라고 결과만 선언&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;for문은 개발자가 루프의 범위를 조정할 수 있고, for문 내에 작성하는 순서에 따라 동작의 실행 순서를 정할 수 있다. 즉, 개발자가 직접 루프를 제어하는 외부 반복자 방식이다. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;하지만 stream API를 사용하면 제어권이 스트림 내부로 넘어간다. 개발자는 무엇을 할지만 알려줄 뿐, 실제 루프를 언제 돌릴지나 필터링을 어떤 순서로 처리할지는 스트림이 판단한다. 이를 내부 반복자라고 하며 루프의 제어 흐름이 스트림에게 있기에 일부 제어의 역전(IoC)이 일어난 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;다만 Stream은 여전히 스트림을 생성하고 종료하는 생명주기 관리는 개발자가 하고 있기에 라이브러리이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;그렇다면 실제로 Spring Framework는 일반 Java 라이브러리와 어떤 차이점이 있을까? 아래 코드를 보자&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770309782726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Math, Wrapper, Scanner -&amp;gt; 자바 표준 라이브러리의 클래스
public void myLogic() {
    // 필요할 때 직접 메서드를 호출함
    int maxVal = Math.max(10, 20); 
    
    // 문자열을 숫자로 바꾸는 시점도 내가 결정함
    int num = Integer.parseInt(&quot;123&quot;);
    
    // Scanner처럼 객체를 직접 생성해서 쓰기도 함
    Scanner sc = new Scanner(System.in);
}

// Spring Framework
@RestController
public class MyController {

    // &quot;이 메서드는 /hello 요청이 오면 실행해줘&quot;라고 규칙만 정함
    @GetMapping(&quot;/hello&quot;)
    public String welcome() {
        return &quot;Hello, Spring!&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;자바 표준 라이브러리의 클래스는 개발자가 명확하게 실행 시점을 제어한다. Math.max()나 Integer.parseInt()는 언제 호출할지, 몇 번 호출할지를 전적으로 개발자가 결정하며, 객체의 생성과 소멸 또한 개발자의 책임이다. 즉, 애플리케이션의 흐름은 끝까지 개발자가 주도한다.&lt;/p&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;396&quot; data-ke-size=&quot;size16&quot;&gt;반면 @GetMapping(&quot;/hello&quot;)이 붙은 welcome() 메서드는 개발자가 직접 호출하지 않는다. 웹 서버가 요청을 받아들이고, 요청을 어떤 컨트롤러로 매핑할지 판단한 뒤, 적절한 시점에 Spring 컨테이너가 해당 메서드를 호출한다. 개발자는 &lt;b&gt;언제 실행할지&lt;/b&gt;가 아니라 &lt;b&gt;어떤 조건에서 실행되어야 하는지&lt;/b&gt;만 선언할 뿐이다. 실행 시점과 호출 흐름의 제어권은 Spring Framework가 가진다.&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;396&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;829&quot; data-start=&quot;636&quot; data-ke-size=&quot;size16&quot;&gt;이 차이는 단순히 메서드 호출 방식의 차이가 아니다. Spring은 객체의 생성, 의존성 주입, 초기화와 소멸, 그리고 실행 시점까지 애플리케이션의 &lt;b&gt;전체 생명주기와 흐름을 프레임워크가 관리&lt;/b&gt;한다. 이처럼 프로그램의 주도권이 개발자가 아닌 외부 컨테이너로 넘어가는 구조를&lt;b&gt; 제어의 역전(IoC, Inversion of Control)&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-end=&quot;829&quot; data-start=&quot;636&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;984&quot; data-start=&quot;831&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면, 라이브러리는 개발자가 흐름을 주도하며 필요할 때마다 꺼내 쓰는 &amp;lsquo;도구&amp;rsquo;라면, 프레임워크는 이미 정해진 실행 흐름 속에 개발자의 코드를 끼워 넣어 실행시키는 &amp;lsquo;공장&amp;rsquo;이다. 개발자는 프레임워크를 호출하지 않고, 프레임워크가 개발자의 코드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IT/코드잇</category>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/7</guid>
      <comments>https://memo50984.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 26 Jan 2026 09:40:12 +0900</pubDate>
    </item>
    <item>
      <title>[코드잇][위클리페이퍼][4주차]  Spring Framework의 탄생 배경</title>
      <link>https://memo50984.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;아래는 4주차 위클리페이퍼 주제이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Spring&amp;nbsp;Framework가&amp;nbsp;탄생하게&amp;nbsp;된&amp;nbsp;배경과&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;해결하고자&amp;nbsp;했던&amp;nbsp;문제점에&amp;nbsp;대해&amp;nbsp;설명하세요.&lt;/li&gt;
&lt;li&gt;프레임워크와&amp;nbsp;라이브러리의&amp;nbsp;차이점을&amp;nbsp;제어&amp;nbsp;흐름의&amp;nbsp;주체와&amp;nbsp;사용&amp;nbsp;방식을&amp;nbsp;중심으로&amp;nbsp;설명하고,&amp;nbsp;Spring&amp;nbsp;Framework와&amp;nbsp;일반&amp;nbsp;Java&amp;nbsp;라이브러리를&amp;nbsp;예시로&amp;nbsp;들어&amp;nbsp;설명하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 첫 번째 주제에 대해서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주제는 스토리텔링 방식으로 서사를 나열하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 혹독한 겨울을 지나, 봄(Spring)이 오다 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크라는 이름에는 단순히 '봄'이라는 계절 이상의 의미가 담겨있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 EJB(Enterprise JavaBean)이라는 혹독한 겨울에서의 해방이라는 의미를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도대체 EJB가 무엇이고 어떤 문제점이 있길래 혹독한 겨울이라고 불렸을까? EJB가 등장하게 된 배경과 그 한계를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔터프라이즈 개발의 난관과 EJB의 등장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1990년대 후반, 자바 진영에 서블릿과 JSP가 등장했다. 이 기술들은 정적인 HTML만 보여주던 웹을 동적으로 바꾸는 혁명이었고 많은 개발자들이 자바로 웹을 만들기 시작했다. 하지만 이 기술을 은행 등 규모가 큰 엔터프라이즈 시스템에 적용하여 개발하려고 하니 큰 문제가 발생하였다. 트랜잭션, 보안 처리, 분산 처리와 페일 오버와 같은 비기능적 요구사항을 구현하다보니 스파게티 코드가 되어 유지보수가 매우 어렵고, 비즈니스 로직에 집중할 수 없었다. 이 상황을 해결하기 위해 썬 마이크로시스템즈에서 주도하여 IBM과 BEA 등의 벤더 기업과 함께 인프라(트랜잭션, 보안 등)을 알아서 해주는 컨테이너를 만들었고 이게 바로 EJB(Enterprise JavaBeans)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EJB가 가져온 또 다른 겨울 (문제점) &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EJB는 이론적으로는 완벽했지만 또다른 문제를 불러일으켰다. 가장 큰 문제점은 침투성이었는데 프레임워크가 코드의 주인이 되는 것으로, 간단한 비즈니스 로직을 작성하려고 해도 EJB가 강요하는 상속과 인터페이스를 붙여야했다. 이는 자바의 최대 장점인 객체지향을 활용할 수 없게 하였고 유지보수가 어려운 코드가 되게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, EJB는 매우 무거운 WAS(WebLogic, WebSphere) 위에서만 돌아갔고, 이는 코드를 한 줄만 수정해도 엄청난 로딩을 기다린 후에 결과를 확인할 수 있는 문제가 있었다. 그리고 EJB 코드는 EJB 컨테이너(위에서 언급한 WAS가 가지고 있음)에서만 작동했기에 단위 테스트는 불가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링(Spring)의 탄생과 해결책 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 위의 상황이 스프링이 탄생하게 된 배경이다. 위에서 언급한 문제점을 해결하기 위해 등장한게 바로 Spring이며 그 시초는 2002년에 &lt;b&gt;로드존슨&lt;/b&gt; 이 발매한 &lt;b&gt;&amp;lt;Expert One-on-One J2EE Design and Development&amp;gt;&lt;/b&gt; 책에 있는 3만 줄의 예제 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제 코드가 발전하여 현재의 Spring Framework가 되었으며, 스프링은 다음과 같은 전략으로 EJB의 문제점을 해결했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POJO (Plain Old Java Object) : 특정 기술에 종속되지 않는 순수한 자바 객체를 사용해서 객체지향 특징 살림.&lt;/li&gt;
&lt;li&gt;DI (Dependency Injection) / IoC (Inversion of Control): 객체의 생성과 관계 설정을 개발자가 직접 하는 게 아니라 컨테이너에게 제어권을 넘겨서(IoC), 컨테이너가 의존성을 주입(DI)해 주게 만듦. 이로써 객체 간 결합도를 낮춤.&lt;/li&gt;
&lt;li&gt;AOP (Aspect Oriented Programming): 트랜잭션이나 로깅 같은 부가 기능을 핵심 로직에서 분리하여 코드를 간결하게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;10,3&quot; data-ke-size=&quot;size16&quot;&gt;같은 문제의식 속에서 &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;10,3&quot;&gt;개빈 킹&lt;/b&gt;은 EJB의 가장 큰 난제였던 엔티티 빈을 대체하기 위해 &lt;b data-index-in-node=&quot;43&quot; data-path-to-node=&quot;10,3&quot;&gt;Hibernate&lt;/b&gt;를 만들었다. 하이버네이트는 POJO 기반으로 데이터베이스를 다룰 수 있게 해주는 ORM 기술의 선구자가 되었으며, 이는 훗날 자바 표준인 &lt;b data-index-in-node=&quot;130&quot; data-path-to-node=&quot;10,3&quot;&gt;JPA&lt;/b&gt;의 모태가 된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10,4&quot; data-ke-size=&quot;size16&quot;&gt;결국 스프링은 하이버네이트와 같은 혁신적인 기술들을 유연하게 수용했고, 덕분에 개발자들은 수억 원짜리 무거운 WAS(WebLogic, WebSphere) 대신 &lt;b data-index-in-node=&quot;89&quot; data-path-to-node=&quot;10,4&quot;&gt;톰캣(Tomcat)&lt;/b&gt; 같은 가볍고 단순한 WAS(서블릿 컨테이너)만으로도 엔터프라이즈급 시스템의 요구사항을 충족할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 마무리 &lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;결국 스프링의 탄생 배경은 EJB가 해결하고자 했던 '개발자가 인프라가 아닌 비즈니스 로직에만 집중하게 하겠다'는 이상을 진정으로 실현하기 위함이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 Spring이 제공하는 기능이 왜 필요한지 간접적으로 느낄 수 있었고, 어떠한 이유로 스프링을 사용하는 표준 형태가 등장하는지 알 것 같다. 앞으로 Spring을 사용할 때 단순히 돌아가는 코드에 만족하지 않고, 객체지향적 특징을 살려고 가독성과 유지보수가 좋은 코드를 만들도록 노력하겠다.&lt;/p&gt;</description>
      <category>IT/코드잇</category>
      <author>PSG-00</author>
      <guid isPermaLink="true">https://memo50984.tistory.com/6</guid>
      <comments>https://memo50984.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 26 Jan 2026 09:39:53 +0900</pubDate>
    </item>
  </channel>
</rss>