<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>비비빅을 좋아하는 개발자</title>
    <link>https://bbbicsan.tistory.com/</link>
    <description>열심히 할겁니다...
그리고 돈을 많이 벌겁니다...
그리고 행복할겁니다...</description>
    <language>ko</language>
    <pubDate>Wed, 9 Sep 2026 08:35:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>bbbicsan</managingEditor>
    <image>
      <title>비비빅을 좋아하는 개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/8104200/attach/d7d248c3602e418e81b7aaf785598309</url>
      <link>https://bbbicsan.tistory.com</link>
    </image>
    <item>
      <title>[LG CNS 6기] 29일차 TIL : Spring Boot JPA Entity&amp;middot;연관관계&amp;middot;CRUD 테스트와 LAZY Loading</title>
      <link>https://bbbicsan.tistory.com/57</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 29일차 TIL : Spring Boot JPA Entity&amp;middot;연관관계&amp;middot;CRUD 테스트와 LAZY Loading&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AwhvQ/dJMcabFvSja/P7CaIEak4BmPW8kRtjiBI0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AwhvQ/dJMcabFvSja/P7CaIEak4BmPW8kRtjiBI0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AwhvQ/dJMcabFvSja/P7CaIEak4BmPW8kRtjiBI0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAwhvQ%2FdJMcabFvSja%2FP7CaIEak4BmPW8kRtjiBI0%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;Spring Boot JPA를 중심으로 &lt;code&gt;Entity &amp;rarr; DTO&lt;/code&gt; 변환, Spring Data JPA Repository 기반 CRUD, MariaDB 외부 DB 연동, &lt;code&gt;User &amp;rarr; Blog &amp;rarr; Comment&lt;/code&gt; 연관관계 설정을 실습했다. 특히 &lt;code&gt;FetchType.LAZY&lt;/code&gt;로 인해 발생한 &lt;code&gt;LazyInitializationException&lt;/code&gt;을 통해 &lt;b&gt;Transaction과 Persistence Context의 생명주기&lt;/b&gt;를 이해하고, Validation과 인증&amp;middot;인가 흐름까지 함께 연결했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot JPA 환경 구성
        &amp;darr;
MariaDB 외부 DB 연동
        &amp;darr;
Entity Mapping
@Entity
@Table
@Id
@Column
@GeneratedValue
        &amp;darr;
User 1 : N Blog
Blog 1 : N Comment
        &amp;darr;
@ManyToOne
@OneToMany
@JoinColumn
mappedBy
fetch
orphanRemoval
cascade
        &amp;darr;
Spring Data JPA Repository
        &amp;darr;
SimpleJpaRepository 자동 구현
        &amp;darr;
JUnit CRUD 테스트
        &amp;darr;
UserRequestDTO
&amp;rarr; UserEntity
&amp;rarr; Repository
&amp;rarr; DB
&amp;rarr; UserEntity
&amp;rarr; UserResponseDTO
        &amp;darr;
Entity &amp;rarr; DTO Factory Method
        &amp;darr;
LAZY Loading
        &amp;darr;
LazyInitializationException
        &amp;darr;
Transaction / Session 경계
        &amp;darr;
JPA 기반 Service 전환
        &amp;darr;
Validation
        &amp;darr;
JWT
        &amp;darr;
Authentication / Authorization&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;Hibernate&lt;/li&gt;
&lt;li&gt;MariaDB&lt;/li&gt;
&lt;li&gt;Entity&lt;/li&gt;
&lt;li&gt;DTO&lt;/li&gt;
&lt;li&gt;Factory Method&lt;/li&gt;
&lt;li&gt;Repository&lt;/li&gt;
&lt;li&gt;SimpleJpaRepository&lt;/li&gt;
&lt;li&gt;JUnit&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Entity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Table&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Column&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@GeneratedValue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GenerationType.IDENTITY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@OneToMany&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ManyToOne&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@JoinColumn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mappedBy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cascade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orphanRemoval&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FetchType.LAZY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Transaction&lt;/li&gt;
&lt;li&gt;Persistence Context&lt;/li&gt;
&lt;li&gt;Proxy&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LazyInitializationException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. JPA 프로젝트 의존성 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 프로젝트에는 JPA를 중심으로 여러 기능을 함께 사용할 수 있도록 의존성을 구성했다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'

runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능별 역할은 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;의존성&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JPA&lt;/td&gt;
&lt;td&gt;Entity와 Repository 기반 ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Web&lt;/td&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MariaDB Driver&lt;/td&gt;
&lt;td&gt;외부 MariaDB 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;DTO 요청값 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Springdoc&lt;/td&gt;
&lt;td&gt;Swagger / OpenAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JJWT&lt;/td&gt;
&lt;td&gt;JWT 처리 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lombok&lt;/td&gt;
&lt;td&gt;Builder, Getter 등 Boilerplate 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트에는 MyBatis도 존재했지만, 오늘 실습은 &lt;b&gt;JPA/Hibernate 중심으로 전환&lt;/b&gt;해서 진행했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. application-dev.yml에서 MariaDB 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 프로필에서는 MariaDB DataSource를 설정했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  config:
    activate:
      on-profile: dev

  datasource:
    driver-class-name: ${DB_DRIVER}
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 접속정보는 코드에 직접 넣기보다 외부 설정에서 가져오는 방향으로 구성했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;.env
  &amp;darr;
Spring Config
  &amp;darr;
DB_DRIVER
DB_URL
DB_USER
DB_PASSWORD
  &amp;darr;
DataSource
  &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 DB 비밀번호와 같은 민감정보는 블로그에 기록하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Hibernate DDL 자동 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 설정에서는 다음 옵션을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  jpa:
    hibernate:
      ddl-auto: create&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서 함께 언급된 주요 옵션은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;create
&amp;rarr; Entity 기준으로 테이블 생성

create-drop
&amp;rarr; 생성 후 Application 종료 시 삭제

update
&amp;rarr; 기존 Schema를 기준으로 변경

none
&amp;rarr; 자동 DDL 처리 사용하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;code&gt;create&lt;/code&gt;를 사용했기 때문에 애플리케이션 실행 시 기존 테이블을 정리하고 Entity 정의를 기준으로 다시 생성하는 Hibernate SQL을 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Hibernate SQL 확인&lt;/h1&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  jpa:
    show-sql: true

    properties:
      hibernate:
        format_sql: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;show-sql&lt;/code&gt;을 사용하면 JPA가 내부적으로 어떤 SQL을 실행하는지 직접 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 저장 과정에서:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Hibernate:
    select
        ...
    from
        spring_jpa_user_tbl
    where
        email = ?

Hibernate:
    insert
    into
        spring_jpa_user_tbl
        (name, password, role, email)
    values
        (?, ?, ?, ?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태의 SQL이 실제로 출력됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용한다고 SQL이 사라지는 것이 아니라 &lt;b&gt;Hibernate가 SQL을 생성해서 DB와 통신한다&lt;/b&gt;는 것을 다시 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Hibernate Dialect 설정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB 환경에서는:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MariaDBDialect&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2 환경에서는 수업 중 비교 예제로:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;org.hibernate.dialect.H2Dialect&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 언급됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dialect는 사용하는 DBMS에 맞는 SQL을 Hibernate가 생성할 수 있도록 도와주는 역할을 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. Physical Naming Strategy&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate:
        naming:
          physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적 Table / Column 이름 변환 정책을 명시적으로 설정하는 방법도 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. 일반 Java 객체를 JPA Entity로 변경&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 &lt;code&gt;UserEntity&lt;/code&gt;는 단순한 Java 클래스였다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Builder
@Getter
@ToString
public class UserEntity {

    private String email;
    private String password;
    private String name;
    private String role;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서는 JPA가 해당 객체를 영속성 객체로 관리하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. @Entity&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Entity
public class UserEntity {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Entity&lt;/code&gt;를 적용하면 이 클래스는 JPA가 관리하는 Entity가 된다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;일반 Java Object
      &amp;darr;
@Entity
      &amp;darr;
JPA Persistence Object&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. @Table&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;SPRING_JPA_USER_TBL&quot;)
public class UserEntity {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Table&lt;/code&gt;을 통해 Entity가 실제 어떤 DB Table과 연결되는지 지정했다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UserEntity
      &amp;darr;
@Table
      &amp;darr;
SPRING_JPA_USER_TBL&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. JPA Entity의 식별자&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Entity에는 반드시 식별자가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 &lt;code&gt;UserEntity&lt;/code&gt;에서는 email을 Primary Key로 사용했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Id
@Column(
    name = &quot;email&quot;,
    length = 50,
    nullable = false
)
private String email;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;@Id
&amp;rarr; Primary Key

name = &quot;email&quot;
&amp;rarr; 실제 Column 이름

length = 50
&amp;rarr; VARCHAR(50)

nullable = false
&amp;rarr; NOT NULL&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. Hibernate가 실제 Table 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ddl-auto: create&lt;/code&gt; 설정으로 Hibernate가 다음과 같은 구조를 생성하는 것을 로그에서 확인했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE SPRING_JPA_USER_TBL (
    email VARCHAR(50) NOT NULL,
    name VARCHAR(255),
    password VARCHAR(255),
    role VARCHAR(255),
    PRIMARY KEY (email)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Java Entity Metadata
        &amp;darr;
Hibernate
        &amp;darr;
DDL 생성
        &amp;darr;
MariaDB Table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름이 실제로 동작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. BlogEntity 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 역시 JPA Entity로 구성했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;SPRING_JPA_BLOG_TBL&quot;)
@Builder
@Getter
@ToString
public class BlogEntity {

    @Id
    @GeneratedValue(
        strategy = GenerationType.IDENTITY
    )
    private Integer blogId;

    private String title;

    private String content;

    private String category;

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. GenerationType.IDENTITY&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog ID는:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GeneratedValue(
    strategy = GenerationType.IDENTITY
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서는 실제로:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기반 PK가 생성됐다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;INSERT
  &amp;darr;
DB가 PK 생성
  &amp;darr;
blogId&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. category에 DB Constraint 적용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog의 category에는 DB 레벨의 제약을 지정하는 예제도 사용했다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;@Column(
    columnDefinition =
        &quot;VARCHAR(20) DEFAULT '전체' &quot; +
        &quot;CHECK(CATEGORY IN ('전체','개발','생활','취미','일상'))&quot;
)
private String category;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 SQL 수업에서 직접 작성했던:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DEFAULT
CHECK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 이제 JPA Entity에 선언하고 Hibernate가 DDL로 생성하게 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. User : Blog = 1 : N&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 한 명은 여러 개의 Blog를 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User
  1
  │
  │
  N
Blog&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 양쪽 Entity에 관계를 표현했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. UserEntity의 @OneToMany&lt;/h1&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@OneToMany
private List&amp;lt;BlogEntity&amp;gt; blogs =
    new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User는 Blog 목록을 가진다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;UserEntity
└─ List&amp;lt;BlogEntity&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. BlogEntity의 @ManyToOne&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 FK를 가진 Blog 쪽에서는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ManyToOne(
    fetch = FetchType.LAZY,
    optional = false
)
@JoinColumn(name = &quot;email&quot;)
private UserEntity author;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. @ManyToOne의 의미&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ManyToOne
&amp;rarr; 여러 Blog가 하나의 User를 참조

@JoinColumn(name = &quot;email&quot;)
&amp;rarr; Blog Table에서 FK Column 생성

optional = false
&amp;rarr; 작성자 User가 반드시 존재해야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 MariaDB에서도 &lt;code&gt;spring_jpa_blog_tbl&lt;/code&gt;에 &lt;code&gt;email&lt;/code&gt; FK가 생성된 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. FetchType.LAZY&lt;/h1&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;fetch = FetchType.LAZY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 연관된 Entity를 처음부터 모두 가져오지 않고 &lt;b&gt;실제로 필요한 순간에 조회하는 지연 로딩 전략&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Blog SELECT
   &amp;darr;
User 정보까지 당장 필요 없음
   &amp;darr;
Proxy 유지
   &amp;darr;
author를 실제 사용
   &amp;darr;
User SELECT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이 LAZY Loading이 이후 트러블슈팅의 핵심이 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. Blog : Comment = 1 : N&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 하나에는 여러 Comment가 존재할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Blog
  1
  │
  │
  N
Comment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 쪽에는:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@OneToMany(
    mappedBy = &quot;blog&quot;,
    orphanRemoval = false
)
private List&amp;lt;CommentEntity&amp;gt; comments =
    new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계를 설정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. mappedBy&lt;/h1&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;mappedBy = &quot;blog&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 반대편 &lt;code&gt;CommentEntity&lt;/code&gt;의 &lt;code&gt;blog&lt;/code&gt; 필드가 관계를 관리한다는 의미다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;BlogEntity.comments
&amp;rarr; 관계의 반대편

CommentEntity.blog
&amp;rarr; FK를 가진 관계의 주인&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. orphanRemoval&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 코드에서는:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;orphanRemoval = false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 컬렉션에서 Comment 객체를 제거했다고 해서 해당 Comment Row를 자동으로 삭제하지 않는 설정이다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;orphanRemoval = false
&amp;rarr; 연관 Collection 제거
&amp;ne;
자동 DB DELETE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 주제에 포함된 &lt;code&gt;cascade&lt;/code&gt;, &lt;code&gt;orphanRemoval&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;는 JPA 연관 객체의 생명주기와 조회 시점을 제어하기 위한 중요한 옵션이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. CommentEntity 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Comment도 JPA Entity로 구성했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;SPRING_JPA_COMMENT_TBL&quot;)
@Builder
@Getter
@ToString
public class CommentEntity {

    @Id
    @GeneratedValue(
        strategy = GenerationType.IDENTITY
    )
    private Integer commentId;

    private String comment;

    private String email;

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. Comment : Blog = N : 1&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ManyToOne(
    fetch = FetchType.LAZY,
    optional = false
)
@JoinColumn(name = &quot;blogId&quot;)
private BlogEntity blog;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Comment Table에는 &lt;code&gt;blogId&lt;/code&gt; FK가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 관계는:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User
  1
  │
  N
Blog
  1
  │
  N
Comment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. Spring Data JPA Repository&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Repository 계층을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository
        extends JpaRepository&amp;lt;UserEntity, String&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 구현 클래스를 직접 만들지 않는다는 점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. SimpleJpaRepository&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit 실행 로그에서:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;org.springframework.data.jpa.repository.support.SimpleJpaRepository@...&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UserRepository Interface
        &amp;darr;
Spring Data JPA
        &amp;darr;
Proxy / 구현체 자동 생성
        &amp;darr;
SimpleJpaRepository
        &amp;darr;
Spring Bean&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 동작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. MyBatis와 JPA Repository 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 MyBatis에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Mapper Interface
+
Mapper XML
+
SQL 직접 작성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 기본 CRUD의 경우:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Repository Interface
+
JpaRepository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만으로도 많은 기능을 제공받는다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;MyBatis
&amp;rarr; SQL 중심

JPA
&amp;rarr; Entity 중심&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 차이도 체감할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. JUnit으로 Repository Bean 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서는:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private UserRepository userRepository;&lt;/code&gt;&lt;/pre&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;pre class=&quot;pf&quot;&gt;&lt;code&gt;System.out.println(
    &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; signUp repository : &quot;
    + userRepository
);&lt;/code&gt;&lt;/pre&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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;SimpleJpaRepository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체가 출력되어 Repository Bean이 정상적으로 주입된 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. 회원가입 JUnit 테스트 데이터&lt;/h1&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;UserRequestDTO request =
    UserRequestDTO.builder()
        .email(&quot;admin@naver.com&quot;)
        .password(&quot;123456789&quot;)
        .name(&quot;임정섭&quot;)
        .role(&quot;admin&quot;)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 테스트 데이터를 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;null&lt;/code&gt; 값을 넣어 구조를 만들고 이후 실제 테스트 값으로 변경하면서 테스트를 확장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. JPA 회원 저장 과정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그에서는:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
FROM spring_jpa_user_tbl
WHERE email = ?&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT
INTO spring_jpa_user_tbl
(name, password, role, email)
VALUES (?, ?, ?, ?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 실행됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Repository 수준의 Java 코드가 Hibernate에 의해 실제 SQL로 변환됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. Entity &amp;rarr; DTO Factory Method&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 Entity를 외부 응답 DTO로 변환할 때:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UserResponseDTO.fromEntity(entity)&lt;/code&gt;&lt;/pre&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;pre class=&quot;nginx&quot;&gt;&lt;code&gt;xxxEntity
   &amp;darr;
xxxDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환을 별도의 Factory Method로 분리한 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. Entity와 DTO를 분리하는 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 흐름에서는:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Database
    ↕
Entity

Controller / Response
    ↕
DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 역할을 구분했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository는 Entity를 반환하고, API 응답에 필요한 데이터는 DTO로 변환한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Repository
&amp;rarr; UserEntity

Factory Method
&amp;rarr; UserResponseDTO.fromEntity()

Controller
&amp;rarr; UserResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. 회원가입 테스트 결과&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그에서:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;entity :
UserEntity(
    email=admin@naver.com,
    ...
    role=admin,
    blogs=[]
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response :
UserResponseDTO(
    email=admin@naver.com,
    ...
    role=admin
)&lt;/code&gt;&lt;/pre&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;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;RequestDTO
   &amp;darr;
Entity
   &amp;darr;
JPA Repository
   &amp;darr;
MariaDB
   &amp;darr;
Entity
   &amp;darr;
ResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름이 동작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. 로그인 JUnit 테스트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에서는 Email과 Password를 준비했다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;UserRequestDTO request =
    UserRequestDTO.builder()
        .email(&quot;admin@naver.com&quot;)
        .password(&quot;123456789&quot;)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Repository의 Query Method를 호출했다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Optional&amp;lt;UserEntity&amp;gt; optional =
    userRepository
        .findByEmailAndPassword(
            request.getEmail(),
            request.getPassword()
        );&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. Spring Data JPA Query Method&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 이름:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;findByEmailAndPassword&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT *
FROM USER
WHERE EMAIL = ?
  AND PASSWORD = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 조회가 수행된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. Optional&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 사용자가 존재하지 않을 수도 있으므로:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;Optional&amp;lt;UserEntity&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 결과를 받았다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;사용자 존재
&amp;rarr; Optional&amp;lt;UserEntity&amp;gt;

사용자 없음
&amp;rarr; Optional.empty()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. 로그인 Entity &amp;rarr; DTO&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 후:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;UserResponseDTO.fromEntity(
    optional.get()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 Entity를 Response DTO로 변환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 Entity &amp;rarr; DTO Factory Method가 회원가입뿐 아니라 로그인 흐름에서도 사용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 1. LazyInitializationException&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 조회 테스트 중 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;org.hibernate.LazyInitializationException

failed to lazily initialize a collection of role:
UserEntity.blogs

could not initialize proxy - no Session&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 JPA 수업에서 가장 중요한 트러블슈팅이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. 왜 User 조회인데 blogs에서 오류가 발생했을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserEntity&lt;/code&gt;에는 Blog 목록이 존재한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private List&amp;lt;BlogEntity&amp;gt; blogs;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계 데이터는 LAZY 방식으로 처리될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 조회했다고 해서 Blog까지 즉시 조회하는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User SELECT
      &amp;darr;
UserEntity 생성

blogs
      &amp;darr;
아직 조회 안 함
      &amp;darr;
Lazy Proxy&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. LAZY Proxy&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY Loading에서는 실제 연관 데이터를 즉시 조회하지 않고 Proxy를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;UserEntity
├─ email
├─ password
├─ name
├─ role
└─ blogs
    &amp;darr;
    Proxy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 &lt;code&gt;blogs&lt;/code&gt; 값이 필요한 순간 Hibernate가 추가 SELECT를 실행한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. Transaction과 Session 경계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 이 과정을 시간 순서로 설명했다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;1. Repository 호출

2. Transaction Start

3. User SELECT

4. Repository Method 종료

5. Transaction End

6. Session / Persistence Context 종료

7. 이후 blogs 접근

8. 추가 SELECT 필요

9. Session 없음

10. LazyInitializationException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;LAZY 자체가 오류가 아니라 LAZY 데이터를 접근하는 시점&lt;/b&gt;이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. toString()도 LAZY Loading을 발생시킬 수 있다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서는 단순히:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;System.out.println(
    optional.get()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 Entity를 출력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;code&gt;UserEntity&lt;/code&gt;에 Lombok:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@ToString&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 붙어 있다면 내부적으로 &lt;code&gt;blogs&lt;/code&gt;까지 접근할 가능성이 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;println(entity)
      &amp;darr;
entity.toString()
      &amp;darr;
blogs 접근
      &amp;darr;
LAZY 초기화 시도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;단순 로그 출력도 연관 Entity 조회를 발생시킬 수 있다는 점&lt;/b&gt;을 배웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. 실제 추가 SELECT 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과에서는 실제로:&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;SELECT
...
FROM spring_jpa_blog_tbl
WHERE email = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태의 Blog 추가 조회도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;User SELECT
      &amp;darr;
blogs 필요
      &amp;darr;
Blog SELECT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실제 SQL로 발생한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. JPA에서 Transaction이 중요한 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY Loading은 Persistence Context가 살아 있는 동안 연관 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;@Transactional
      &amp;darr;
Persistence Context 유지
      &amp;darr;
Entity 조회
      &amp;darr;
LAZY 연관 Entity 접근
      &amp;darr;
추가 SELECT 가능
      &amp;darr;
Transaction 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JPA에서는 Transaction이 단순한 Commit/Rollback 개념뿐 아니라 &lt;b&gt;Entity와 Persistence Context의 생명주기&lt;/b&gt;와도 매우 밀접하게 연결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. 연관관계와 fetch 전략&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 &lt;code&gt;fetch&lt;/code&gt; 옵션을 실제 문제를 통해 이해할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;LAZY
&amp;rarr; 필요할 때 조회

즉시 모든 연관 객체를 조회하지 않음
&amp;rarr; 불필요한 SQL 감소 가능

하지만
&amp;rarr; Session 종료 후 접근하면 문제 발생 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 단순히 &lt;code&gt;LAZY가 좋다&lt;/code&gt;, &lt;code&gt;EAGER가 좋다&lt;/code&gt;의 문제가 아니라 &lt;b&gt;업무 흐름과 Transaction 범위를 고려해서 선택해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;45. 외부 설정 파일을 Spring Config로 읽기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 메인 클래스에서 Dotenv를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Dotenv env =
    Dotenv.configure()
        .ignoreIfMissing()
        .load();

env.entries()
    .forEach(entry -&amp;gt; {
        System.setProperty(
            entry.getKey(),
            entry.getValue()
        );
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Spring Config가 &lt;code&gt;.env&lt;/code&gt;를 직접 읽도록 구성하는 방법도 실습했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;46. spring.config.import&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  config:
    import:
      - optional:file:.env[.properties]&lt;/code&gt;&lt;/pre&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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;.env
 &amp;darr;
spring.config.import
 &amp;darr;
Spring Environment
 &amp;darr;
${DB_*}
 &amp;darr;
DataSource&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;47. Environment로 설정값 테스트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit에서는 Spring의 &lt;code&gt;Environment&lt;/code&gt;를 주입했다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private Environment env;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고:&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;env.getProperty(&quot;DB_DRIVER&quot;);
env.getProperty(&quot;DB_URL&quot;);
env.getProperty(&quot;DB_USER&quot;);
env.getProperty(&quot;DB_PASSWORD&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 실제 값이 들어왔는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그에서 MariaDB Driver와 URL 등이 출력되어 설정이 정상 로딩된 것을 확인했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Password는 블로그에서는 마스킹한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 2. Failed to load ApplicationContext&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 설정 파일을 import하는 과정에서:&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Failed to load ApplicationContext&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류도 발생했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spring.config.import&lt;/code&gt;에 지정한:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;파일 이름
파일 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 실제 &lt;code&gt;.env&lt;/code&gt; 위치와 맞지 않으면 Spring Context 자체가 시작되지 못할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;JUnit 실행
      &amp;darr;
Spring ApplicationContext 시작
      &amp;darr;
Config Import
      &amp;darr;
파일 찾지 못함 / 설정 문제
      &amp;darr;
ApplicationContext 실패&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 설정 파일 경로를 올바르게 맞춘 후:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Environment
&amp;rarr; DB_DRIVER
&amp;rarr; DB_URL
&amp;rarr; DB_USER
&amp;rarr; DB_PASSWORD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 정상적으로 읽는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;48. JPA 기반 UserService로 전환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트는 MyBatis 기반이었다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;UserService
 &amp;darr;
UserMapper
 &amp;darr;
Mapper XML&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이를 JPA Repository 기반 구조로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;UserService
 &amp;darr;
UserRepository
 &amp;darr;
Spring Data JPA
 &amp;darr;
Hibernate&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;49. Service에 @Transactional&lt;/h1&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public UserResponseDTO signUp(
        UserRequestDTO request
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에도:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Transactional
public Map&amp;lt;String, Object&amp;gt; signIn(
        UserRequestDTO request
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Service의 업무 단위를 Transaction 안에서 처리하는 구조다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;50. 로그인 Repository 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 평문 Password 버전은:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;UserEntity entity =
    userRepository
        .findByEmailAndPassword(
            request.getEmail(),
            request.getPassword()
        )
        .orElseThrow(
            () -&amp;gt; new LoginFailException(
                &quot;SignIn Fail!!&quot;
            )
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;51. 향후 Password Hashing 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 코드에는 향후 Password Hashing 방향도 주석으로 남겼다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;현재

findByEmailAndPassword(
    email,
    password
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후에는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;findById(email)
      &amp;darr;
UserEntity 조회
      &amp;darr;
DB에 저장된 Hash Password
      vs
입력 Password
      &amp;darr;
비교&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 방향으로 발전할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Password를 평문 그대로 조회 조건으로 사용하는 현재 방식은 학습용 단계이고, 인증 기능이 확장되면서 변경될 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;52. Entity 기반 Token 발급&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 후에는 요청 DTO 값보다 실제 DB에서 조회한 Entity 정보를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String at =
    jwtProvider.createAT(
        entity.getEmail()
    );

String rt =
    jwtProvider.createRT(
        entity.getEmail()
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;로그인 요청
      &amp;darr;
Repository
      &amp;darr;
실제 UserEntity
      &amp;darr;
Authentication 성공
      &amp;darr;
Entity Email
      &amp;darr;
AT / RT 발급&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;53. 로그인 Service 반환값&lt;/h1&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; map =
    new HashMap&amp;lt;&amp;gt;();

map.put(&quot;response&quot;, response);
map.put(&quot;at&quot;, at);
map.put(&quot;rt&quot;, rt);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서 필요한:&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;사용자 정보
Access Token
Refresh Token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 한 번에 전달한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;54. JPA로 변경되어도 GlobalExceptionHandler는 재사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 접근 기술이:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;MyBatis
&amp;rarr; JPA&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 변경됐지만 예외 처리 계층은 그대로 사용할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UserRepository
      &amp;darr;
조회 실패
      &amp;darr;
LoginFailException
      &amp;darr;
GlobalExceptionHandler
      &amp;darr;
401 Unauthorized&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층별 책임이 분리되어 있기 때문에 Persistence 기술을 변경해도 상위 API 구조를 대부분 유지할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;55. User Controller 로그인 응답&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는 Service 결과에서 Token을 꺼낸다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; map =
    userService.signIn(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Header를 구성한다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;HttpHeaders headers =
    new HttpHeaders();

headers.add(
    &quot;Authorization&quot;,
    (String) map.get(&quot;at&quot;)
);

headers.add(
    &quot;Refresh-Token&quot;,
    (String) map.get(&quot;rt&quot;)
);

headers.add(
    &quot;Access-Control-Expose-Headers&quot;,
    &quot;Authorization, Refresh-Token&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;56. 최종 로그인 ResponseEntity&lt;/h1&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(HttpStatus.OK)
        .headers(headers)
        .body(
            (UserResponseDTO)
                map.get(&quot;response&quot;)
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;200 OK

Headers
├─ Authorization
└─ Refresh-Token

Body
└─ UserResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;57. 회원가입 Validation 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 요청에서는 DTO가 정상적으로 Controller에 바인딩되고 Validation 결과도 로그로 확인했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;UserRequestDTO(
    email=user@naver.com,
    password=*********,
    name=임섭순,
    role=user
)

bindingResult
&amp;rarr; 0 errors&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client Request
      &amp;darr;
DTO Binding
      &amp;darr;
Validation
      &amp;darr;
0 errors
      &amp;darr;
Service
      &amp;darr;
JPA Repository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름이 정상적으로 이어졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;58. Swagger 로그인 테스트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger에서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /users/signIn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 직접 실행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;email
password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request URL은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/users/signIn
?email=admin@naver.com
&amp;amp;password=*********&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 생성됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 기존 API의 Query Parameter 방식이 JPA 전환 이후에도 그대로 유지됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;59. Authentication과 Authorization&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업에서는 인증과 인가 개념도 함께 다뤘다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Authentication
&amp;rarr; &quot;누구인가?&quot;

Authorization
&amp;rarr; &quot;무엇을 할 수 있는가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Email / Password 로그인
      &amp;darr;
사용자 확인
      &amp;darr;
Authentication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;role = user
role = admin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 권한을 기준으로 접근 가능한 기능을 결정하는 과정이 Authorization이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;60. User의 role&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;UserEntity&lt;/code&gt;에는:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;private String role;&lt;/code&gt;&lt;/pre&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;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;user
admin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 고려하는 방향이 언급됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후 Spring Security와 연결되면:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Authentication
&amp;rarr; Login / Token

Authorization
&amp;rarr; Role / Permission&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 발전할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 전체 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습한 내용을 하나로 연결하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Client
  &amp;darr;
UserRequestDTO
  &amp;darr;
Controller
  &amp;darr;
Validation
  &amp;darr;
Service
  &amp;darr;
@Transactional
  &amp;darr;
Spring Data JPA Repository
  &amp;darr;
Hibernate
  &amp;darr;
MariaDB
  &amp;darr;
Entity
  &amp;darr;
Factory Method
  &amp;darr;
ResponseDTO
  &amp;darr;
JWT
  &amp;darr;
ResponseEntity&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터 관계는:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UserEntity
    1
    │
    │
    N
BlogEntity
    1
    │
    │
    N
CommentEntity&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 연결됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 핵심 트러블슈팅 정리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. LAZY 연관 Collection 접근 시 LazyInitializationException&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;failed to lazily initialize a collection
could not initialize proxy - no Session&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User 조회
&amp;rarr; Transaction 종료
&amp;rarr; Session 종료
&amp;rarr; blogs LAZY Proxy 접근
&amp;rarr; 추가 SELECT 불가능&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 Entity 조회뿐 아니라 &lt;b&gt;연관 Entity를 언제 사용할지와 Transaction 경계&lt;/b&gt;까지 함께 설계해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 단순 로그 출력이 LAZY Query를 발생시킴&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;System.out.println(entity);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 단순 출력처럼 보여도:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;@ToString
&amp;rarr; blogs 접근
&amp;rarr; LAZY 초기화
&amp;rarr; SQL 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity의 &lt;code&gt;toString()&lt;/code&gt;이나 JSON 직렬화 시 연관관계 필드를 조심해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. .env를 JUnit이 읽지 못해 ApplicationContext 실패&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Failed to load ApplicationContext&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Test Context가 읽어야 할 외부 설정 파일 경로 문제.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  config:
    import:
      - optional:file:.env[.properties]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 경로를 맞추고 &lt;code&gt;Environment&lt;/code&gt;를 통해 값이 로딩됐는지 직접 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 크게 느낀 부분은 &lt;b&gt;JPA는 단순히 SQL을 안 쓰게 해주는 기술이 아니라 객체와 DB의 관계를 관리하는 기술&lt;/b&gt;이라는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에서는:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;SQL
&amp;rarr; Mapper
&amp;rarr; DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름에 집중했다면 JPA에서는:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Entity
&amp;rarr; Persistence Context
&amp;rarr; Association
&amp;rarr; Fetch Strategy
&amp;rarr; Transaction&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 함께 생각해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;FetchType.LAZY&lt;/code&gt; 문제를 직접 경험하면서 Entity가 단순히 SELECT 결과를 담는 객체가 아니라 &lt;b&gt;Hibernate가 관리하는 상태와 Proxy를 가진 객체&lt;/b&gt;라는 점을 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Entity와 DTO를 분리하며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UserResponseDTO.fromEntity(entity)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 Entity를 DTO로 변환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Entity와 DTO의 필드가 거의 같기 때문에 굳이 나눠야 하나 싶을 수도 있지만 역할을 생각하면 차이가 명확했다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Entity
&amp;rarr; DB Persistence Model

DTO
&amp;rarr; API / Layer 간 데이터 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity의 연관관계와 LAZY Proxy를 그대로 외부 응답에 노출하면 의도하지 않은 SQL이나 직렬화 문제가 발생할 수 있기 때문에 DTO 변환이 더 중요하게 느껴졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Spring Data JPA Repository&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository Interface만 정의했는데:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;SimpleJpaRepository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 객체가 자동으로 생성되는 것도 인상적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 MyBatis에서는 SQL과 Mapper XML을 직접 작성했지만 JPA에서는:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;findByEmailAndPassword(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Query Method를 통해 조회 조건을 표현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 SQL을 작성하지 않는다고 해서 SQL을 몰라도 되는 것은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 &lt;code&gt;show-sql&lt;/code&gt;로 Hibernate가 생성한 SQL을 보면서 결국 &lt;b&gt;DB에서 어떤 Query가 실행되는지를 이해해야 JPA도 제대로 사용할 수 있다&lt;/b&gt;는 점을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Transaction의 의미&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 Transaction을 주로:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;COMMIT
ROLLBACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관점에서 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JPA에서는:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Transaction
        &amp;darr;
Persistence Context
        &amp;darr;
Entity 관리
        &amp;darr;
LAZY Loading 가능 범위&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 연결돼 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 발생한 &lt;code&gt;LazyInitializationException&lt;/code&gt; 덕분에 왜 Service 계층에서 &lt;code&gt;@Transactional&lt;/code&gt;을 중요하게 사용하는지 더 구체적으로 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘까지는 JPA Entity와 관계 설정, Repository 기반 CRUD, DTO 변환, LAZY Loading을 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 다음 내용을 더 확인해봐야겠다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;cascade 옵션별 차이

orphanRemoval=true / false 비교

LAZY / EAGER 비교

N+1 문제

Fetch Join

EntityGraph

JPA 변경 감지
Dirty Checking

JPQL

Repository Query Method

JPA Transaction

H2 In-Memory Test

JUnit Repository Test

Password Hashing

JWT 실제 Claim / Signature

Spring Security

Authentication

Authorization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘 발생한 LAZY Loading 문제를 기반으로 &lt;b&gt;Fetch Join과 N+1 문제까지 연결해서 학습하면 JPA 조회 전략을 이해하는 데 도움이 될 것 같다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>DTO</category>
      <category>JPA</category>
      <category>JWT</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mariadb</category>
      <category>springboot</category>
      <category>til</category>
      <category>validation</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/57</guid>
      <comments>https://bbbicsan.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 8 Sep 2026 17:07:11 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #6] 금융 업무와 AI 계산을 분리한 Spring Boot&amp;middot;FastAPI Rule 분석 연동</title>
      <link>https://bbbicsan.tistory.com/56</link>
      <description>&lt;div lang=&quot;ko&quot; style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all; overflow-wrap: anywhere;&quot;&gt;&lt;!-- FinGuardOps #6: Tistory HTML body. 게시글 제목은 티스토리 제목 입력란에 별도로 입력합니다. --&gt;
&lt;div style=&quot;display: inline-block; margin: 0 0 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold;&quot;&gt;FinGuardOps Project Series #6&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 외부에서 들어오는 거래와 행동 이벤트를 어떤 기준으로 검증하고, 중복 요청과 충돌을 어떻게 구분할지 정리했습니다. 이번에는 그다음 단계인 &lt;b&gt;Spring Boot와 FastAPI 사이의 Rule 분석 연동&lt;/b&gt;을 다룹니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;다른 서비스의 API를 호출해 JSON 응답을 받는 것 자체는 연동의 시작일 뿐입니다. 분석 중 행동 이벤트나 Rule이 바뀌면 어떤 기준으로 계산한 결과일까요? HTTP 상태가 200이어도 다른 거래의 결과가 들어오면 어떻게 해야 할까요? 분석 결과를 저장하다 실패하면 근거와 거래 상태가 따로 남지는 않을까요?&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;저는 이 문제를 &lt;b&gt;입력 고정 &amp;rarr; 분석 계산 &amp;rarr; 응답 검증 &amp;rarr; 결과 저장&amp;middot;채택&lt;/b&gt;으로 나누었습니다. 특히 외부 HTTP 호출을 DB 트랜잭션 안에 오래 붙잡아 두지 않으면서, 분석 시작과 결과 반영에는 각각 명확한 정합성 경계를 두는 데 집중했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;FastAPI가 분석 결과를 반환했다는 사실만으로 금융 업무 처리가 완료되는 것은 아닙니다.&lt;br /&gt;&lt;b&gt;Spring Boot는 분석 입력과 Rule 버전을 먼저 고정하고, 응답이 그 입력에 대응하는지 검증한 뒤 탐지 결과와 근거를 하나의 트랜잭션으로 저장&amp;middot;채택합니다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_06_rule_analysis_flow.png&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z3bYa/dJMcagtkVe0/5LuBKzyT2nW1IOlMoXWcEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z3bYa/dJMcagtkVe0/5LuBKzyT2nW1IOlMoXWcEK/img.png&quot; data-alt=&quot;분석 입력을 고정하고 계산&amp;amp;middot;검증&amp;amp;middot;영속화 책임을 분리한 Spring Boot&amp;amp;middot;FastAPI Rule 분석 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z3bYa/dJMcagtkVe0/5LuBKzyT2nW1IOlMoXWcEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ3bYa%2FdJMcagtkVe0%2F5LuBKzyT2nW1IOlMoXWcEK%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;2200&quot; height=&quot;1060&quot; data-filename=&quot;finguardops_06_rule_analysis_flow.png&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1060&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분석 입력을 고정하고 계산&amp;middot;검증&amp;middot;영속화 책임을 분리한 Spring Boot&amp;middot;FastAPI Rule 분석 흐름&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;그림의 PostgreSQL 저장&amp;middot;채택 단계는 Spring Boot가 수행하는 DB 쓰기입니다. FastAPI가 업무 DB에 직접 연결하거나 거래 상태를 변경한다는 뜻은 아닙니다. 또한 이 글의 AI Service는 &lt;b&gt;Rule v1 계산 경로&lt;/b&gt;를 가리키며, 생성형 AI나 LLM은 이 분석에 참여하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 36px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background: #fffafb;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 16px; color: #7a1f3d; font-size: 23px;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 24px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-responsibilities&quot;&gt;금융 업무 처리와 Rule 계산을 분리한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-analysis-scope&quot;&gt;거래 접수 범위와 Rule 분석 범위를 구분하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-input-snapshot&quot;&gt;분석 입력을 Snapshot으로 고정하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-version-boundary&quot;&gt;HTTP API 버전과 Rule 엔진 버전 구분하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-fastapi-execution&quot;&gt;FastAPI에서 Rule&amp;middot;점수&amp;middot;Evidence를 계산하는 흐름&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-transaction-boundaries&quot;&gt;DB 트랜잭션과 HTTP 호출을 분리한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-response-validation&quot;&gt;HTTP 200 응답을 그대로 채택하지 않기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-result-adoption&quot;&gt;탐지 결과와 Evidence를 함께 저장&amp;middot;채택하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-failure-handling&quot;&gt;실패를 정상 분석 결과로 바꾸지 않기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-body-read-review&quot;&gt;본문 읽기 오류를 구분한 구현&amp;middot;리뷰 사례&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-test-evidence&quot;&gt;테스트로 확인하는 경계와 검증의 한계&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin: 5px 0;&quot;&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#fg6-operations&quot;&gt;운영 관점의 의미와 다음 글&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-responsibilities&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 금융 업무 처리와 Rule 계산을 분리한 이유&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;금융 거래의 업무 상태를 관리하는 코드와 분석을 계산하는 코드를 분리하되, 두 서비스가 같은 데이터를 제각각 확정하지 않도록 책임을 나눴습니다. Spring Boot가 거래와 분석 시도의 상태, 결과 채택과 영속화를 소유하고, FastAPI는 전달받은 Snapshot으로 Rule 결과를 계산합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background: #fff8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #7a1f3d; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;☕ Spring Boot &amp;mdash; 업무 상태와 결과 채택&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;분석 대상 거래를 확인하고, 필요한 거래&amp;middot;행동 이벤트&amp;middot;RuleVersion을 고정합니다. 분석 시도와 버전을 기록한 뒤 FastAPI를 호출하고, 응답을 검증하여 탐지 결과와 Evidence를 저장합니다. 어떤 결과를 거래의 현재 분석 결과로 채택할지도 Spring Boot가 결정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  FastAPI &amp;mdash; Rule 분석 계산&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;요청 DTO를 내부 도메인 입력으로 변환하고, 실행 가능한 RuleVersion인지 재검증합니다. 실행 계획에 따라 Rule을 평가한 뒤 점수&amp;middot;위험 등급&amp;middot;Evidence를 계산해 반환합니다. 업무 DB 조회&amp;middot;수정, 거래 대응 확정, 사건 최종 판정은 수행하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조에서 Spring Boot는 금융 업무의 소유권을 유지하고, FastAPI는 분석 계산의 경계를 가집니다. Python의 분석 경로를 수정할 때도 거래&amp;middot;사건의 저장 책임이 함께 이동하지 않도록 한 선택입니다. 다만 서비스를 분리한 만큼 통신 실패, 계약 호환성, 배포 버전 차이를 명시적으로 다뤄야 한다는 비용도 받아들였습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;계산 책임을 분리하는 것과 업무 상태의 최종 책임을 분산하는 것은 다른 문제입니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-analysis-scope&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  2. 거래 접수 범위와 Rule 분석 범위를 구분하기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 API의 입력 계약과 분석 엔진의 처리 범위는 같은 목록이 아닙니다. 거래 접수 모델은 계좌이체, 오픈뱅킹 이체, ATM 인출, 대출 실행 Mock을 표현하지만, 이 글에서 다루는 Rule v1은 두 이체 유형만 평가합니다. 입력 검증을 통과할 수 있다는 사실을 모든 유형의 분석&amp;middot;최종 처리까지 지원한다는 뜻으로 해석하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Rule v1 분석 대상
ACCOUNT_TRANSFER
OPEN_BANKING_TRANSFER

Rule v1 분석 대상이 아닌 유형
ATM_WITHDRAWAL
LOAN_DISBURSED&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트도 접수 API의 9개 유형 전체를 분석 요청에 그대로 복제하지 않았습니다. Rule v1에 필요한 다음 네 유형 중, 실제 실행할 Rule이 요구하는 이벤트를 조회해 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-2&quot;&gt;[2]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DEVICE_REGISTERED
PASSWORD_CHANGED
TRANSFER_LIMIT_CHANGED
BENEFICIARY_REGISTERED&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #edddc4; border-radius: 14px; background: #fffaf1; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #80551f; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  지원하지 않는 입력은 정상 미적중이 아닙니다&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule v1이 지원하지 않는 거래 유형을 임의의 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;0점&amp;middot;LOW&lt;/code&gt;로 통과시키지 않습니다. &amp;ldquo;분석했지만 적중한 Rule이 없음&amp;rdquo;과 &amp;ldquo;이 엔진으로 분석할 수 없음&amp;rdquo;은 구분해야 하기 때문입니다. 새 거래 유형 지원에는 별도의 Rule 계약과 검증이 필요합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-input-snapshot&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  3. 분석 입력을 Snapshot으로 고정하기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;분석이 시작된 뒤에도 DB를 계속 조회하면, 거래를 읽은 시점과 행동 이벤트를 읽은 시점, Rule 버전을 읽은 시점이 달라질 수 있습니다. FinGuardOps에서는 한 번의 분석이 사용할 데이터를 먼저 고정하고, 이후의 HTTP 호출은 DB Entity가 아닌 독립된 요청 객체로 수행하도록 구성했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;3.1 평가 기준 시각을 하나로 고정하기&lt;/h3&gt;
&lt;pre class=&quot;excel&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;T = FinancialTransaction.occurredAt
evaluationCutoffAt = T

행동 이벤트 시간 범위
T - 24시간 &amp;le; event.occurredAt &amp;le; T&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;평가 기준은 서버의 현재 시각이나 FastAPI 호출 시각이 아니라 저장된 거래의 발생 시각입니다. 예를 들어 거래가 10시에 발생하고 분석 요청이 10시 2분에 시작되더라도, 이 실행의 기준 시각은 10시로 유지합니다. 거래 시각, 행동 이벤트 조회 범위, Rule 적용 기간과 응답의 cutoff 검증이 같은 기준을 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;최근 24시간 시간창은 양 끝을 포함합니다. 정확히 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;T - 24시간&lt;/code&gt; 또는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;T&lt;/code&gt;에 발생한 이벤트는 포함하고, 범위를 벗어난 이벤트는 제외합니다. 분석 시작 후 늦게 들어온 이벤트가 발생 시각상 범위 안에 있더라도, 이미 고정한 이번 요청의 Snapshot에는 다시 끼워 넣지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;3.2 필요한 행동 이벤트만, 정해진 순서로 가져오기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;같은 고객의 이벤트
+ 실행할 Rule에 필요한 이벤트 유형
+ 포함 범위 안의 occurredAt

ORDER BY occurredAt DESC, eventId ASC
최대 1,000건&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;시각이 같은 이벤트에도 업무 식별자인 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;eventId&lt;/code&gt;를 보조 정렬로 사용합니다. 같은 시각의 여러 이벤트 중 하나를 근거로 선택할 때 입력 순서가 임의로 달라지지 않도록 하기 위해서입니다. Snapshot 상한은 요청 크기와 분석 입력을 제한하는 경계이며, 적격 이벤트가 1,000건을 초과하면 더 오래된 이벤트는 이번 분석에 포함하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-2&quot;&gt;[2]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  Snapshot의 범위가 곧 이번 분석의 범위&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;1,000건 제한을 두었다고 해서 고객의 전체 이력을 분석했다고 말할 수는 없습니다. 이 글의 분석은 &lt;b&gt;해당 실행에서 고정한 입력&lt;/b&gt;을 기준으로 합니다. 상한과 늦게 도착한 이벤트 처리 방식은 탐지 품질과 연결되는 제약으로 남겨 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;3.3 실행 가능한 RuleVersion 집합 고정하기&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FraudRule.lifecycleStatus = ACTIVE
RuleVersion.status = PUBLISHED

effectiveFrom &amp;le; T &amp;lt; effectiveTo
※ effectiveTo가 null이면 상한 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실행 가능한 버전 전체를 한 번에 고정하고, 같은 Rule의 복수 버전이나 지원하지 않는 설정&amp;middot;의존성을 정상 결과로 보정하지 않습니다. 실행 집합이 비어 있는 경우도 &amp;ldquo;모두 미적중&amp;rdquo;이 아니라 실행 계획 오류로 구분합니다. Spring Boot는 이 집합으로 예상 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;ruleSetVersion&lt;/code&gt;을 계산해 분석 시도에 기록합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Snapshot을 고정한다는 말은 요청용 불변 값을 구성한다는 뜻입니다. 거래&amp;middot;행동&amp;middot;외부 Provider의 전체 원문을 모두 하나의 JSON 컬럼에 영구 보관한다는 의미는 아닙니다. 특히 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;ExternalRiskSnapshot&lt;/code&gt;은 이 HTTP 분석 입력을 위한 인메모리 값이며, 원문 복제 저장과는 구분합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-version-boundary&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  4. HTTP API 버전과 Rule 엔진 버전 구분하기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule 분석에는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;/api/v1/rule-analysis&lt;/code&gt;와 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;/api/v2/rule-analysis&lt;/code&gt;가 있습니다. 여기서 v2는 Rule 알고리즘을 Rule v2로 바꿨다는 뜻이 아니라, 필수 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;externalRisk&lt;/code&gt;를 추가한 &lt;b&gt;HTTP 요청 계약의 버전&lt;/b&gt;입니다. 계산 엔진은 Rule v1 R001~R004를 유지합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/v2/rule-analysis
Content-Type: application/json
X-Trace-Id: trace_rule_analysis_demo_01&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Rule Analysis 요청 v1
├─ evaluationCutoffAt
├─ transaction
├─ behaviorEvents
└─ ruleVersions

Rule Analysis 요청 v2
├─ evaluationCutoffAt
├─ transaction
├─ behaviorEvents
├─ ruleVersions
└─ externalRisk  &amp;larr; 필수&amp;middot;non-null&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;기존 v1에 optional 필드를 덧붙이거나, 외부 조회가 빠졌을 때 기본 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;UNMATCHED&lt;/code&gt;를 넣어 호환되는 척하지 않았습니다. 상위 거래 처리 흐름이 External Risk를 먼저 조회하고 성공한 Snapshot을 고정한 뒤 v2 입력에 전달합니다. Provider 조회&amp;middot;실패 분류의 상세는 다음 편에서 다룹니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;동시에 &lt;b&gt;정보를 전달한다는 것과 점수에 사용한다는 것도 구분&lt;/b&gt;했습니다. v2는 외부 위험정보의 입력 계약을 검증하지만 기존 Rule v1 점수 정책에 외부 위험 점수를 더하지 않습니다. 관련 통합 테스트도 Snapshot 전달과 기존 점수 유지 조건을 함께 확인하도록 작성되어 있습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;같은 &amp;ldquo;버전&amp;rdquo;이라는 이름이어도 소유하는 의미가 다릅니다&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP API version
&amp;rarr; 서비스 간 요청&amp;middot;응답 형식

ruleSetVersion
&amp;rarr; 어떤 불변 RuleVersion 집합을 어떤 순서로 실행했는가

detectionResultVersion
&amp;rarr; 같은 거래에서 몇 번째 분석 시도인가

scoringPolicyVersion
&amp;rarr; 어떤 점수 합산&amp;middot;그룹 상한&amp;middot;등급 정책인가&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 값을 하나로 합치지 않으면 통신 형식을 바꾸는 작업과 탐지 정책을 바꾸는 작업을 구분할 수 있습니다. 과거 결과가 참조한 버전을 최신 정책 이름으로 덮어쓰지 않는 것도 같은 이유입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-fastapi-execution&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;⚙️ 5. FastAPI에서 Rule&amp;middot;점수&amp;middot;Evidence를 계산하는 흐름&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;FastAPI Endpoint는 HTTP 요청을 받고 응답 DTO로 변환하는 역할에 집중합니다. 분석 서비스는 요청 매핑, 실행 계획, Rule 평가, 점수 계산, Evidence 변환을 순서대로 조합합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청 DTO
&amp;rarr; RuleAnalysisRequestMapper
&amp;rarr; RuleExecutionPlanBuilder
&amp;rarr; RuleExecutionPlanRunner
&amp;rarr; RuleScoringCalculator
&amp;rarr; RuleEvidenceTransformer
&amp;rarr; RuleAnalysisResponseMapper&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; ai-service의 RuleAnalysisService.analyze()&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def analyze(self, request: RuleAnalysisRequest) -&amp;gt; RuleAnalysisResult:
    execution_input = RuleAnalysisRequestMapper.to_domain(request)
    plan = self.plan_builder.build(
        execution_input.evaluation_cutoff_at,
        execution_input.rule_versions,
    )
    planned_results = self.plan_runner.execute(plan, execution_input.rule_input)
    scoring_result = RuleScoringCalculator.calculate(plan, planned_results)
    return RuleEvidenceTransformer.transform(plan, planned_results, scoring_result)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;계획을 만드는 단계는 RuleVersion의 상태&amp;middot;기간&amp;middot;의존성을 확인하고, Runner는 그 계획에 따라 evaluator를 실행합니다. Scoring은 이미 나온 적중 결과를 합산하며, Evidence 변환은 평가에 사용한 근거를 응답 계약에 맞춰 구성합니다. 점수를 계산하는 단계에서 행동 이벤트를 다시 선택하거나 Rule 적중 여부를 보정하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;5.1 네 Rule이 실제로 판단하는 것&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;R001 &amp;mdash; 절대 고액 이체.&lt;/b&gt; 대상 이체에서 KRW 1,000만 원 이상인지 확인합니다. 고객의 평균 금액이나 과거 거래 합계와 비교하는 Rule은 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;R002 &amp;mdash; 최근 기기 등록 이벤트가 있는 고액 이체.&lt;/b&gt; R001 적중을 전제로, 같은 고객&amp;middot;같은 기기의 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;DEVICE_REGISTERED&lt;/code&gt; 이벤트가 최근 24시간 안에 있는지 확인합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;R003 &amp;mdash; 최근 보안정보 변경 시퀀스가 있는 고액 이체.&lt;/b&gt; R001 적중을 전제로, 같은 고객의 비밀번호 변경과 해당 출금 계좌의 한도 변경이 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;비밀번호 변경 &amp;le; 한도 변경 &amp;le; 거래 시각&lt;/code&gt; 순서로 존재하는지 확인합니다. 두 이벤트 모두 시간창 안에 있어야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;R004 &amp;mdash; 최근 등록 수취인 이체.&lt;/b&gt; 같은 고객&amp;middot;출금 계좌&amp;middot;수취인에 대응하는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;BENEFICIARY_REGISTERED&lt;/code&gt; 이벤트가 최근 24시간 안에 있는지 확인합니다. 이 Rule에는 고액 조건이 없습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  관측한 신호보다 강한 의미를 붙이지 않기&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;최근 기기 등록 이벤트가 있다는 사실만으로 &amp;ldquo;실제로 처음 사용하는 기기&amp;rdquo;를 완전히 증명하지는 못합니다. 한도 변경 이벤트도 변경 전후 값이 없으므로 실제 한도 상향 여부를 뜻하지 않습니다. 최근 수취인 등록 역시 과거 거래 관계가 전혀 없었음을 보장하지 않습니다. Rule의 설명은 입력으로 확인할 수 있는 사실에 맞췄습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;5.2 Evidence 합계와 최종 위험 점수가 다를 수 있는 이유&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule v1은 개별 Rule의 원래 기여도와 그룹 상한 적용 결과를 따로 유지합니다. R001은 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;amount&lt;/code&gt; 그룹, R002~R004는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;security&lt;/code&gt; 그룹으로 묶습니다. 아래는 네 Rule이 모두 적중했다고 가정한 &lt;b&gt;정책 계산 예시&lt;/b&gt;이며, 실제 금융 데이터로 성능을 측정한 결과가 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;원래 Rule 기여도
R001 = 15
R002 = 20
R003 = 40
R004 = 10
합계 = 85

amount 그룹   : min(15, 15) = 15
security 그룹 : min(60, 70) = 60

최종 riskScore = min(100, 15 + 60) = 75
scoring-policy-v1의 riskLevel = HIGH&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Evidence의 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;scoreContribution&lt;/code&gt;은 그룹 상한 적용 전 값입니다. 따라서 Evidence 합계가 85이고 최종 점수가 75인 것은 오류가 아닙니다. 그룹에서 차감한 10점을 개별 Evidence에 임의로 나눠 적지 않고, 그룹 summary로 설명합니다. 이 가중치와 위험 등급 경계는 구현&amp;middot;검증을 위한 초기 실험 정책이며, 운영 데이터로 최적화된 탐지 성능을 의미하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-transaction-boundaries&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  6. DB 트랜잭션과 HTTP 호출을 분리한 이유&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;외부 서비스가 응답할 때까지 하나의 DB 트랜잭션을 유지하면, 분석 지연을 기다리는 동안 업무 잠금과 트랜잭션 자원을 오래 붙잡을 수 있습니다. FinGuardOps에서는 분석 시작과 결과 채택을 각각의 트랜잭션으로 처리하고, 그 사이에 HTTP 호출을 배치했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_06_transaction_boundaries.png&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZeOfC/dJMcadXImUE/4ZJBrcpO9FNwv1wyk4ZiA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZeOfC/dJMcadXImUE/4ZJBrcpO9FNwv1wyk4ZiA0/img.png&quot; data-alt=&quot;분석 시작과 결과 채택의 DB 트랜잭션 사이에 외부 HTTP 호출을 분리한 처리 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZeOfC/dJMcadXImUE/4ZJBrcpO9FNwv1wyk4ZiA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZeOfC%2FdJMcadXImUE%2F4ZJBrcpO9FNwv1wyk4ZiA0%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;2200&quot; height=&quot;1180&quot; data-filename=&quot;finguardops_06_transaction_boundaries.png&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;1180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분석 시작과 결과 채택의 DB 트랜잭션 사이에 외부 HTTP 호출을 분리한 처리 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;6.1 분석 시작 &amp;mdash; 입력과 분석 시도를 함께 확정&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;startAnalysisV2()&lt;/code&gt;는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;REQUIRES_NEW&lt;/code&gt;와 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;REPEATABLE_READ&lt;/code&gt;를 사용합니다. 거래를 먼저 잠그고 시작 가능한 상태인지 확인한 뒤, Snapshot을 조립하고 External Risk를 v2 요청으로 매핑합니다. 매핑이 성공한 다음에만 새 분석 버전을 할당하고 분석 시도와 거래 상태를 저장합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; RuleAnalysisPersistenceService.startAnalysisV2()&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.REPEATABLE_READ
)
public StartedRuleAnalysisV2Execution startAnalysisV2(
        UUID transactionId,
        ExternalRiskSnapshot externalRiskSnapshot,
        String scoringPolicyVersion,
        String featureVersion,
        String modelVersion,
        String analysisTraceId,
        Instant startedAt
) {
    PreparedRuleAnalysisStart prepared = prepareStart(transactionId);
    RuleAnalysisRequestV2 request = requestV2Mapper.map(
            prepared.snapshot().request(),
            externalRiskSnapshot
    );
    StartedRuleAnalysis started = persistStart(
            prepared,
            scoringPolicyVersion,
            featureVersion,
            modelVersion,
            analysisTraceId,
            startedAt
    );
    return new StartedRuleAnalysisV2Execution(started, request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이때 거래는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;RECEIVED &amp;rarr; ANALYZING&lt;/code&gt;, 탐지 결과는 같은 시작 트랜잭션 안에서 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;PENDING &amp;rarr; IN_PROGRESS&lt;/code&gt;로 전이합니다. HTTP 호출은 이 시작 경계의 Commit 이후에만 실행됩니다. 입력 조립이나 v2 매핑이 실패하면 생성하지 못한 분석 시도를 실패 결과로 억지로 만들지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;6.2 HTTP 호출 &amp;mdash; 활성 DB 트랜잭션 없이 실행&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;오케스트레이터에 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;@Transactional&lt;/code&gt;을 붙이지 않는 것에서 끝내지 않았습니다. 상위 호출자가 이미 트랜잭션을 시작했을 가능성도 있으므로, 진입 시점과 HTTP 호출 전 활성 트랜잭션이 없는지 검사합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; RuleAnalysisOrchestrationService&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void requireNoActiveTransaction() {
    if (TransactionSynchronizationManager.isActualTransactionActive()) {
        throw new IllegalStateException(
                &quot;Rule analysis orchestration requires no active transaction&quot;
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  REQUIRES_NEW만으로 외부 트랜잭션 문제가 없어지지는 않습니다&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Spring 공식 문서의 전파 의미를 함께 보면, &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;REQUIRES_NEW&lt;/code&gt;는 독립된 물리 트랜잭션을 사용하지만 기존 외부 트랜잭션의 자원은 유지된 채 내부 트랜잭션이 별도 연결을 얻을 수 있습니다. 즉 &amp;ldquo;내부 메서드를 REQUIRES_NEW로 만들었으니 외부 호출 동안 DB 자원도 모두 해제된다&amp;rdquo;는 해석은 맞지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 활성 외부 트랜잭션이 있는 오케스트레이션 호출 자체를 거부하고, DB 쓰기 경계를 분리한 뒤 HTTP 호출로 넘어갑니다. Connection Pool 고갈 개선률을 측정했다는 주장이 아니라, 트랜잭션 수명을 통제하기 위한 구조입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;6.3 결과 채택 &amp;mdash; 다시 짧은 쓰기 경계로 진입&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증된 응답을 받은 뒤에는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;completeAndAdopt()&lt;/code&gt;의 별도 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;REQUIRES_NEW&lt;/code&gt; 트랜잭션으로 들어갑니다. 이 메서드에는 시작 경계와 달리 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;REPEATABLE_READ&lt;/code&gt;가 명시되어 있지 않습니다. 거래와 탐지 결과를 잠그고 상태&amp;middot;소유 관계를 다시 확인한 뒤 결과를 반영합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;동기 HTTP 요청 하나 안에서 처리하더라도 전체 흐름이 단일 DB 트랜잭션인 것은 아닙니다. 분석 시작, HTTP 호출, 결과 채택의 경계를 분리한 만큼 중간 상태와 실패 기록을 명시적으로 관리해야 한다는 Trade-off가 생깁니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-response-validation&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 7. HTTP 200 응답을 그대로 채택하지 않기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;FastAPI가 200을 반환했다는 사실은 통신 수준의 성공 신호입니다. 다른 거래의 결과, 요청과 다른 Rule 집합, 맞지 않는 점수와 Evidence까지 그대로 업무 결과로 받아들이지는 않습니다. Client의 HTTP 검증과 별도 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;RuleAnalysisResponseValidator&lt;/code&gt;를 통해 응답을 검증합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;7.1 통신 형식과 Trace 확인&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청 X-Trace-Id
= 응답 X-Trace-Id
= 응답 body.traceId

응답 Content-Type: application/json
알 수 없는 필드&amp;middot;필수값 누락&amp;middot;타입 강제 변환 거부
잘못된 JSON&amp;middot;후행 JSON 토큰 거부&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Client 전용 strict ObjectMapper를 사용해 분석 응답 계약을 검증합니다. Trace 헤더가 누락&amp;middot;중복되거나 body의 Trace와 다르면 정상 응답으로 채택하지 않습니다. 내부 서비스라고 해서 요청과 응답의 연결을 확인하지 않아도 된다고 가정하지 않았습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;7.2 분석 대상과 Rule 집합의 정확한 일치&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;response.transactionId == request.transaction.transactionId
response.analysis.evaluationCutoffAt == request.evaluationCutoffAt
response.analysis.ruleSetVersion == expectedRuleSetVersion&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;위 표현은 검사 항목을 요약한 것입니다. &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;ruleSetVersion&lt;/code&gt;은 64자리 해시 형식만 맞으면 통과하는 값이 아닙니다. 요청의 RuleVersion 집합으로 계산한 예상 값과 정확히 같은지 비교합니다. 형식이 올바르지만 다른 집합의 해시라면 거부합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;7.3 점수와 Evidence의 일관성&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;응답의 Rule contribution 개수&amp;middot;순서&amp;middot;중복, 적중 여부와 원래 기여도, 그룹별 상한과 최종 점수, 위험 등급의 조합을 검사합니다. Evidence 역시 적중 Rule에 대응해야 하며, Reason Code별 허용 필드와 요청 Snapshot의 행동 이벤트 ID&amp;middot;시각&amp;middot;관측값이 맞는지 확인합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;Rule 재판정과 결과 정합성 검증을 구분&lt;/b&gt;했습니다. Spring Boot가 Python evaluator를 별도로 실행해 적중 여부를 다시 만드는 것은 아닙니다. 반환된 contribution의 합계와 그룹 상한, 근거의 대응 관계는 검산하되, 틀린 응답을 재정렬하거나 누락 항목을 보충해 성공으로 바꾸지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;내부 서비스의 결과도 검증 대상입니다. 검증 실패는 응답을 고쳐 쓸 계기가 아니라, 그 결과를 업무 데이터로 채택하지 않을 근거입니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-result-adoption&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  8. 탐지 결과와 Evidence를 함께 저장&amp;middot;채택하기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증을 마친 결과를 저장할 때에는 탐지 결과와 근거가 서로 분리되어 남지 않도록 했습니다. &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;completeAndAdopt()&lt;/code&gt;는 거래와 탐지 결과를 순서대로 잠그고, 거래가 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;ANALYZING&lt;/code&gt;, 결과가 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;IN_PROGRESS&lt;/code&gt;인지 확인합니다. 잠긴 거래의 분석 결과인지, 시작 당시의 식별자&amp;middot;버전 정보와 맞는지도 검증합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; completeAndAdopt()의 검증 이후 저장 부분&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;evidenceRepository.saveAllAndFlush(evidence);

result.complete(riskScore, riskLevel, completedAt);
detectionResultRepository.saveAndFlush(result);

transaction.adoptDetectionResult(result);
transactionRepository.saveAndFlush(transaction);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 별도 Commit 세 번이 아니라 &lt;b&gt;같은 메서드의 한 트랜잭션 안에서 수행되는 flush 순서&lt;/b&gt;입니다. 뒤 단계에서 실패해 이 트랜잭션이 Rollback되면 앞서 flush한 Evidence도 함께 되돌아가야 합니다. 관련 통합 테스트는 이 저장 경계를 검증하도록 구성되어 있습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;근거의 표시 정보도 소유권을 나눴습니다&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;FastAPI가 반환하는 Evidence에는 Rule&amp;middot;Reason Code와 관측 근거가 포함됩니다. 내부 DB 식별자, &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;evidenceId&lt;/code&gt;, 표시 설명과 영속 정렬 순서는 Spring Boot의 매핑 계층에서 다룹니다. 특히 FastAPI의 1부터 시작하는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;executionOrder&lt;/code&gt;와, 적중 Evidence에 대해 0부터 연속으로 부여하는 영속 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;sortOrder&lt;/code&gt;를 같은 값으로 취급하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #edddc4; border-radius: 14px; background: #fffaf1; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #80551f; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  분석 완료와 최종 거래 성공은 다릅니다&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DetectionResult = COMPLETED
Transaction = ANALYZED

&amp;rarr; 분석 결과를 저장&amp;middot;채택한 상태
&amp;rarr; 위험 대응&amp;middot;고위험 사건 연결&amp;middot;최종 멱등 응답 확정과는 구분&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule 분석 경계가 성공한 뒤에도 상위 흐름의 위험 대응과 사건 연결이 남아 있습니다. 따라서 FastAPI의 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;200 OK&lt;/code&gt;나 거래의 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;ANALYZED&lt;/code&gt;를 공개 거래 API의 최종 성공과 같은 의미로 쓰지 않습니다. 최종 성공과 Snapshot 완료 간극은 별도 계약으로 관리합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-failure-handling&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  9. 실패를 정상 분석 결과로 바꾸지 않기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실패를 다룰 때는 오류 이름뿐 아니라 &lt;b&gt;분석 시도가 생성되기 전인지, 생성된 뒤인지, 결과를 채택하던 중인지&lt;/b&gt;를 구분했습니다. 같은 예외 처리 블록에서 모두 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;LOW&lt;/code&gt;로 변환하면 정상적인 미적중 결과와 분석 불능 상태를 구분할 수 없기 때문입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;9.1 시작 단계의 실패&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Snapshot 조립이나 v2 매핑이 실패하면 시작 트랜잭션은 Rollback되고 HTTP 호출은 실행하지 않습니다. 시작 트랜잭션이 정상적으로 Rollback된 경계에서는 거래가 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;RECEIVED&lt;/code&gt;에 남고 새 DetectionResult&amp;middot;Evidence가 생성되지 않습니다. 존재하지 않는 분석 결과에 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;FAILED&lt;/code&gt;를 기록하려고 하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;9.2 시작 이후 호출&amp;middot;변환&amp;middot;채택 단계의 실패&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;시작이 Commit된 뒤 Client 호출이 실패하면 Client 오류 범주를 사용하고, 일반 호출 오류&amp;middot;응답 매핑 오류&amp;middot;채택 오류도 별도로 구분합니다. 결과 채택이 실패해 Rollback되면 별도의 실패 기록 트랜잭션을 호출합니다. 이 실패 경계도 거래&amp;middot;탐지 결과의 소유 관계와 처리 중 상태를 확인한 후 변경합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP 호출 일반 실패
&amp;rarr; RULE_ANALYSIS_HTTP_CALL_FAILED

응답 매핑 실패
&amp;rarr; RULE_ANALYSIS_RESPONSE_MAPPING_FAILED

결과 채택 실패
&amp;rarr; RULE_ANALYSIS_ADOPTION_FAILED&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 문자열은 내부 분석 단계의 실패 분류입니다. 외부 REST API가 같은 원문을 그대로 공개한다는 뜻은 아닙니다. 공개 오류와 내부 진단 정보를 분리하는 것은 #5에서 다룬 오류 계약과 같은 원칙입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;9.3 실패 기록도 실패할 수 있습니다&lt;/h3&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; RuleAnalysisOrchestrationService.recordFailure() 본문&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    persistenceService.failAnalysis(
            started,
            failureCode,
            clock.instant()
    );
} catch (RuntimeException recordingFailure) {
    if (recordingFailure != original) {
        original.addSuppressed(recordingFailure);
    }
}
return original;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실패 기록 과정의 예외가 최초 장애를 덮어쓰지 않도록 원래 예외를 유지하고 보조 예외로 연결합니다. 이는 실패 상태가 항상 저장된다는 보장이 아닙니다. 프로세스 종료나 저장소 장애로 확정 여부를 확인할 수 없는 중간 상태는 자동 재분석이나 임의의 성공 변환으로 해결하지 않고, 별도의 복구 계약에서 다뤄야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background: #fff8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #7a1f3d; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  정상 미적중과 분석 실패를 구분합니다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;유효한 Rule 실행 결과가 미적중
&amp;rarr; 0점&amp;middot;LOW&amp;middot;빈 Evidence 가능

HTTP&amp;middot;입력 계약&amp;middot;응답&amp;middot;영속화 실패
&amp;rarr; 0점&amp;middot;LOW&amp;middot;빈 Evidence로 대체하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;분석 Client에는 자동 retry와 fallback을 두지 않았습니다. 호출 구간에 도달한 분석 시도는 HTTP 요청을 한 번 수행합니다. 시작 전 실패는 0회, 정상 호출 경로는 1회라는 의미이며, 시스템 전체의 모든 장애 상황에서 &amp;ldquo;정확히 한 번 실행&amp;rdquo;을 보장한다는 주장은 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;생성형 AI 리포트의 템플릿 fallback은 별도 기능입니다. Rule 분석 실패를 정상 탐지 결과로 바꾸는 fallback과 혼동하지 않습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-body-read-review&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  10. 본문 읽기 오류를 구분한 구현&amp;middot;리뷰 사례&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP Client 구현&amp;middot;리뷰에서 보강한 사례는 &lt;b&gt;응답 헤더를 받은 뒤 본문을 읽는 동안 발생하는 오류의 분류&lt;/b&gt;였습니다. JSON 역직렬화 중 예외가 났다고 해서 원인이 모두 잘못된 JSON인 것은 아닙니다. 정상 응답을 전송받던 중 연결이 끊기거나 읽기가 지연된 경우도 구분해야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-8&quot;&gt;[8]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  검토한 문제&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;본문 읽기 예외를 전부 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;AI_SERVICE_INVALID_RESPONSE&lt;/code&gt;로 묶으면, 전송 장애와 응답 계약 오류가 같은 실패로 보입니다. 반대로 Jackson 예외를 모두 Timeout으로 취급하면 잘못된 응답 형식이 네트워크 장애처럼 보입니다. 따라서 &amp;ldquo;어느 라이브러리에서 예외가 나왔는가&amp;rdquo;뿐 아니라 cause chain에 실제 전송 I/O 오류가 있는지를 확인했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-8&quot;&gt;[8]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;분류기는 먼저 Jackson 자체가 아닌 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;IOException&lt;/code&gt;이 있는지 확인합니다. 이 조건이 없으면 본문 transport 오류 분류를 적용하지 않고, Client가 잘못된 응답으로 처리하게 둡니다. 조건이 있으면 요청의 monotonic timeout budget 경과 여부로 응답 Timeout과 가용성 실패를 구분합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 발췌 &amp;middot; classifyBodyReadFailure()의 분류 부분&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!hasNonJacksonIOException(exception)) {
    return Optional.empty();
}
return Optional.of(responseTimeoutExpired.getAsBoolean()
        ? RuleAnalysisClientErrorCategory.AI_SERVICE_RESPONSE_TIMEOUT
        : RuleAnalysisClientErrorCategory.AI_SERVICE_UNAVAILABLE);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Jackson 자체의 형식&amp;middot;매핑 오류만 존재
&amp;rarr; AI_SERVICE_INVALID_RESPONSE

본문 읽기 cause chain에 non-Jackson I/O 존재
├─ response timeout budget 경과 &amp;rarr; AI_SERVICE_RESPONSE_TIMEOUT
└─ budget 경과 전               &amp;rarr; AI_SERVICE_UNAVAILABLE&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;예외 메시지에서 &amp;ldquo;timeout&amp;rdquo; 같은 문자열을 검색해 판단하지 않습니다. 같은 오류가 다른 메시지로 감싸져도 같은 범주로 분류되도록 하고, cause chain이 순환하는 경우에도 탐색이 끝나도록 identity 기반 방문 집합을 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;회귀 테스트에 남긴 조건&lt;/h3&gt;
&lt;pre class=&quot;elm&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;doesNotClassifyJacksonOnlyFailuresAsBodyTransportErrors
&amp;rarr; 순수 Jackson 실패는 transport 오류로 분류하지 않음

classifiesWrappedBodyTransportIoWithoutReadingExceptionMessages
&amp;rarr; 감싸진 I/O 오류를 메시지 문자열과 무관하게 분류

stopsBodyReadCauseTraversalWhenTheCauseChainContainsACycle
&amp;rarr; 원인 체인이 순환해도 탐색 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 사례는 운영 환경에서 장애가 발생했다고 꾸민 이야기가 아니라, HTTP Client 구현&amp;middot;리뷰와 회귀 테스트에 남아 있는 예외 경계 보강입니다. 네트워크 장애와 계약 오류를 구분해야 로그&amp;middot;메트릭을 읽을 때도 원인 후보를 다르게 좁힐 수 있다는 점을 보여줍니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-8&quot;&gt;[8]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-test-evidence&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  11. 테스트로 확인하는 경계와 검증의 한계&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;테스트는 통과 개수뿐 아니라 &lt;b&gt;어떤 경계를 실제로 실행하고 무엇을 대체했는지&lt;/b&gt;까지 함께 봐야 합니다. Rule 계산, HTTP Client, PostgreSQL 영속 경계와 실제 서비스 간 E2E는 서로 다른 검증입니다. 이 글에서는 테스트 코드의 기대 조건과 구현 당시 PR에 남긴 실행 결과를 분리해 설명합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;11.1 고정 입력&amp;middot;Rule 계약&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;시간창 양 끝, 동일 시각의 이벤트 선택 순서, R001 선행 조건, R003의 시퀀스, 그룹 상한과 Evidence 대응이 검증 기준입니다. 정해진 입력과 버전에서 같은 계산 결과를 얻어야 하며, 실행 계획 오류를 부분 성공으로 보정하지 않는지도 확인 대상으로 둡니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;11.2 HTTP 호출 시점의 Commit과 트랜잭션 상태&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;RuleAnalysisOrchestrationIntegrationTest&lt;/code&gt;는 PostgreSQL을 사용하는 영속 경계에서 Client 호출 시점의 거래&amp;middot;분석 상태를 조회하도록 구성되어 있습니다. 아래는 v2 테스트에 정의된 assertion 일부입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;실제 테스트 assertion 발췌 &amp;middot; v2CallsOnlyV2ClientAfterCommitAndDoesNotScoreExternalRisk()&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertThat(transactionActive).isFalse();
assertThat(committedState.get())
        .containsEntry(&quot;processing_status&quot;, &quot;ANALYZING&quot;)
        .containsEntry(&quot;analysis_status&quot;, &quot;IN_PROGRESS&quot;);
assertCompletedState(completed, 15, RiskLevel.MEDIUM, 1);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 테스트에서 확인하려는 것은 &amp;ldquo;응답이 나왔다&amp;rdquo;가 아니라, &lt;b&gt;HTTP Client를 호출하기 전에 시작 상태가 Commit되었고 그 호출 시점에는 활성 트랜잭션이 없다는 조건&lt;/b&gt;입니다. v2 Client만 한 번 호출하고 v1 Client는 호출하지 않는 조건, 전달한 External Risk Snapshot 때문에 기존 Rule 점수가 바뀌지 않는 조건도 포함합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;11.3 Rollback&amp;middot;무호출&amp;middot;정상 미적중&lt;/h3&gt;
&lt;pre class=&quot;sql&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;v2MapperFailureRollsBackBeforeHttpAndFailurePersistence
&amp;rarr; 매핑 실패 시 새 분석 결과&amp;middot;Evidence 없음, HTTP 무호출

rollsBackFlushedEvidenceBeforeRecordingAdoptionFailure
&amp;rarr; 채택 실패 시 앞서 flush한 Evidence도 Rollback

rejectsAnOuterTransactionBeforeCreatingAnAnalysisAttempt
&amp;rarr; 외부 트랜잭션이 있으면 분석 시도 생성 전에 거부

adoptsValidatedZeroLowResultWithEmptyEvidence
&amp;rarr; 정상적인 0점&amp;middot;LOW&amp;middot;빈 Evidence는 채택 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드명과 조건은 저장소의 통합 테스트에 정의되어 있습니다. 단, 이 테스트는 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;RuleAnalysisHttpClient&lt;/code&gt;를 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;@MockitoBean&lt;/code&gt;으로 대체합니다. 따라서 PostgreSQL 쓰기&amp;middot;Rollback 경계를 검증하는 테스트이지, 실제 FastAPI 프로세스와 DNS&amp;middot;TLS까지 연결한 E2E 테스트는 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;11.4 구현 당시 기록한 실행 결과&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP Client 구현 당시 PR에는 아래 두 테스트 클래스를 대상으로 실행한 결과가 기록되어 있습니다. &lt;b&gt;해당 변경을 검증한 시점의 기록&lt;/b&gt;이며, 이후 추가된 v2&amp;middot;전체 오케스트레이션 테스트까지 같은 수치로 검증했다는 의미는 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 6px; color: #6b7280; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP Client 구현 PR에 기록된 명령&amp;middot;결과 발췌&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.\gradlew.bat test --tests *RuleAnalysisHttpClientTest --tests *RuleAnalysisTransportErrorClassifierTest --console=plain

2 suites
33 tests
33 success
0 failures
0 errors
0 skipped
BUILD SUCCESSFUL&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 OS 네트워크에서의 connect timeout은 당시 검증 범위에서 제외했고, 설정값 적용과 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;HttpConnectTimeoutException&lt;/code&gt; 우선 분류 등을 결정적으로 검사했다고 기록했습니다. 이를 실제 네트워크 전체의 장애 복구 검증으로 확대해 해석하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e9df; border-radius: 14px; background: #f7fcf9; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #2f684d; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  재실행할 수 있는 PostgreSQL 경계 테스트&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;margin: 14px 0 24px; padding: 20px; border: 1px solid #343741; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal; overflow-wrap: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.\gradlew.bat integrationTest --tests &quot;*RuleAnalysisOrchestrationIntegrationTest&quot; --rerun-tasks --console=plain&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;위 명령은 &lt;code style=&quot;font-family: Consolas,monospace; font-size: .9em; padding: 2px 5px; border-radius: 4px; background: #f4eef1; color: #7a1f3d; overflow-wrap: anywhere;&quot;&gt;backend&lt;/code&gt; 디렉터리에서 Java 17과 Docker/Testcontainers 실행 환경을 준비한 뒤 사용하는 재검증 명령입니다. 실행 결과는 해당 코드 버전과 환경에서 나온 테스트 리포트로 확인해야 합니다. 위의 33개 성공 기록과 이 명령의 실행 결과를 합쳐 적지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-9&quot;&gt;[9]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;fg6-operations&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;☁️ 12. 운영 관점의 의미와 다음 글&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이번 연동에서 중요하게 본 것은 기술 스택 사이에 화살표를 하나 더 그리는 일이 아니라, &lt;b&gt;어떤 입력을 신뢰하고 어떤 결과를 언제 업무 상태로 확정할 것인지&lt;/b&gt;였습니다. Spring Boot와 FastAPI를 분리한 뒤에도 거래 상태와 검증된 결과의 소유권은 분명하게 유지해야 했습니다.&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;운영에서 읽을 수 있는 정보로 남기기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;분석 입력의 cutoff와 버전은 어떤 기준으로 계산했는지를 설명하고, Trace는 요청과 응답을 연결합니다. HTTP 호출&amp;middot;매핑&amp;middot;채택 실패를 구분하면 같은 &amp;ldquo;분석 실패&amp;rdquo;라도 어느 단계부터 확인할지 정할 수 있습니다. 오케스트레이터의 메트릭 기록은 업무 결과를 덮어쓰지 않도록 분리되어 있으며, 상세 대시보드와 수집 환경은 Observability 편에서 다룹니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-6&quot;&gt;[6]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조만으로 모든 장애가 해결되었다고 말하지는 않습니다. 서비스 분리에는 통신 계약 관리가 필요하고, 엄격한 응답 검증은 버전 불일치에 대한 명시적 대응을 요구합니다. 자동 재시도를 제외한 대신 일시적 실패도 사용자 오류 응답과 운영 절차로 이어질 수 있으며, 입력 상한을 두면 그 범위 밖의 근거는 분석에 포함되지 않습니다. 이러한 제한을 감추지 않는 것이 이후 확장 기준이 됩니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-2&quot;&gt;[2]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#fg6-ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 30px 0 13px; color: #64182f; font-size: 21px; line-height: 1.55;&quot; data-ke-size=&quot;size23&quot;&gt;이번 글에서 확정한 경계&lt;/h3&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;분석 입력과 버전을 고정하고, 외부 HTTP 호출은 DB 트랜잭션 밖에서 수행합니다. 반환된 결과는 요청과의 대응을 검증한 뒤 한 쓰기 경계에서 저장&amp;middot;채택하며, 실패를 정상 미적중 결과로 바꾸지 않습니다.&lt;/blockquote&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 설계는 애플리케이션을 여러 인스턴스로 배포할 때도 유지해야 할 기본 조건입니다. 다만 이번 글의 코드&amp;middot;테스트만으로 Kubernetes 확장, 클라우드 네트워크 장애나 운영 수준의 복구 성능까지 검증했다고 주장하지 않습니다. 실제 배포와 장애 실험은 해당 환경의 증거로 따로 기록합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 Rule 분석보다 먼저 실행되는 &lt;b&gt;External Risk 조회&lt;/b&gt;를 다룹니다. 조회 실패를 &amp;ldquo;위험정보 없음&amp;rdquo;으로 취급하지 않기 위해 Timeout&amp;middot;가용성 장애&amp;middot;잘못된 응답을 어떻게 구분하고, 어떤 성공 Snapshot만 다음 단계로 넘기는지 정리하겠습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  관련 계약과 코드&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;본문의 번호는 설명에 사용한 프로젝트 계약&amp;middot;구현&amp;middot;테스트를 가리킵니다. 일반적인 트랜잭션 전파 의미는 Spring 공식 문서로 별도 연결했습니다. 테스트의 기대 조건과 PR에 기록된 과거 실행 결과는 같은 종류의 증거가 아닙니다.&lt;/p&gt;
&lt;p id=&quot;fg6-ref-1&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[1]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/01-requirements/spring-rule-analysis-orchestration-contract.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Rule 분석 오케스트레이션&amp;middot;결과 채택 계약&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/07-decisions/ADR-006-final-transaction-success-and-idempotency-recovery.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;최종 거래 성공&amp;middot;멱등 복구 계약&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-2&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[2]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/detection/service/RuleAnalysisSnapshotAssembler.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisSnapshotAssembler&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-3&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/03-api/rule-v1-analysis-api.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Rule v1 내부 분석 API&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/01-requirements/external-risk-rule-analysis-input-contract.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;External Risk&amp;middot;Rule 분석 입력 계약&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-4&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[4]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/01-requirements/rule-v1-detection-contract.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Rule v1 탐지 계약&amp;middot;점수&amp;middot;Evidence 정책&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-5&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[5]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/ai-service/src/finguardops_ai/services/rule_analysis.py&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FastAPI RuleAnalysisService&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/ai-service/src/finguardops_ai/api/routers/rule_analysis.py&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FastAPI Rule 분석 Endpoint&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-6&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[6]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/detection/service/RuleAnalysisOrchestrationService.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisOrchestrationService&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/detection/service/RuleAnalysisPersistenceService.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisPersistenceService&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-7&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[7]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/rule/client/RuleAnalysisHttpClient.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisHttpClient&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/rule/client/RuleAnalysisResponseValidator.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisResponseValidator&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-8&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[8]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/rule/client/RuleAnalysisTransportErrorClassifier.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;본문 transport 오류 분류기&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/test/java/com/aifds/backend/rule/client/RuleAnalysisTransportErrorClassifierTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;분류기 회귀 테스트&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-9&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[9]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/integrationTest/java/com/aifds/backend/persistence/RuleAnalysisOrchestrationIntegrationTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RuleAnalysisOrchestrationIntegrationTest&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-10&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[10]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/pull/137&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;HTTP Client 구현&amp;middot;리뷰&amp;middot;테스트 기록&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-11&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[11]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://docs.spring.io/spring-framework/reference/6.2/data-access/transaction/declarative/tx-propagation.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Framework 6.2 공식 문서: Transaction Propagation&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;fg6-ref-12&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[12]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/build.gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Gradle 테스트 작업&amp;middot;Java Toolchain 정의&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 22px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[FinGuardOps #7] 외부 API 장애를 정상 결과로 오인하지 않는 External Risk 장애 경계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;외부 위험정보 조회의 성공&amp;middot;실패를 구분하고, Timeout과 잘못된 응답을 정상적인 미적중으로 바꾸지 않는 경계를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 38px 0 14px; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; max-width: 100%; box-sizing: border-box; padding: 13px 20px; border-radius: 10px; background: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36,41,47,.18);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; flex-shrink: 0; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;&quot; width=&quot;20&quot; height=&quot;20&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>FinGuardOps/Project</category>
      <category>API연동</category>
      <category>FastAPI</category>
      <category>FinGuardOps</category>
      <category>PostgreSQL</category>
      <category>ruleengine</category>
      <category>springboot</category>
      <category>금융IT</category>
      <category>장애격리</category>
      <category>트랜잭션</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/56</guid>
      <comments>https://bbbicsan.tistory.com/56#entry56comment</comments>
      <pubDate>Mon, 7 Sep 2026 17:03:45 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #5] 중복과 잘못된 입력을 막는 거래&amp;middot;행동 이벤트 API와 Validation 설계</title>
      <link>https://bbbicsan.tistory.com/55</link>
      <description>&lt;div lang=&quot;ko&quot; style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all; overflow-wrap: anywhere;&quot;&gt;&lt;!-- FinGuardOps #5: Tistory HTML body --&gt;
&lt;div style=&quot;display: inline-block; margin: 0 0 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold;&quot;&gt;FinGuardOps Project Series #5&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 거래&amp;middot;사건 상태와 멱등성 규칙을 나누고, 이를 PostgreSQL의 관계와 제약조건으로 표현하는 기준을 정리했습니다. 이번에는 그 모델의 바깥쪽, &lt;b&gt;외부 요청이 처음 들어오는 REST API 경계&lt;/b&gt;를 살펴보겠습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;계좌이체인데 수취 계좌가 없거나, ATM 인출인데 이체 채널이 지정되거나, 같은 이벤트 식별자에 서로 다른 내용이 전달된다면 어떻게 처리해야 할까요? JSON을 읽을 수 있다는 사실만으로 업무적으로 유효한 요청이라고 볼 수는 없습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;저는 이 문제를 &lt;b&gt;형식 검증, 업무 규칙 검증, 요청 동일성 확인, 저장 시 경쟁 제어&lt;/b&gt;로 나누었습니다. 특히 거래 생성과 행동 이벤트 수집은 모두 중복을 다루지만, 같은 멱등성 구조를 그대로 적용하지 않았습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;Validation은 DTO에 어노테이션을 붙이는 작업에서 끝나지 않습니다. 잘못된 입력이 금융 업무 상태로 들어오는 것을 막고, 재전송과 충돌을 구분하는 첫 번째 정합성 경계입니다.&lt;/blockquote&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 &lt;code&gt;POST /api/v1/transactions&lt;/code&gt;와 &lt;code&gt;POST /api/v1/behavior-events&lt;/code&gt;의 계약 및 구현 구조를 중심으로 설명합니다. Rule 계산, 외부 위험정보 장애 처리의 상세와 사건 최종화는 다음 글에서 이어갑니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops 거래 api validation과 멱등 처리 흐름.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAOr9f/dJMcahsjXya/fj06ksH5VGdKpK7kpB0Jn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAOr9f/dJMcahsjXya/fj06ksH5VGdKpK7kpB0Jn1/img.png&quot; data-alt=&quot;Client 요청부터 후속 분석 연결까지의 거래 API 입력 검증 흐름과 오류 분류&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAOr9f/dJMcahsjXya/fj06ksH5VGdKpK7kpB0Jn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAOr9f%2FdJMcahsjXya%2Ffj06ksH5VGdKpK7kpB0Jn1%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;1448&quot; height=&quot;1086&quot; data-filename=&quot;finguardops 거래 api validation과 멱등 처리 흐름.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Client 요청부터 후속 분석 연결까지의 거래 API 입력 검증 흐름과 오류 분류&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;margin: 16px 0 28px; padding: 17px 20px; border-left: 4px solid #7a1f3d; border-radius: 0 12px 12px 0; background: #fff7fa; color: #444; font-size: 14px; line-height: 1.8;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-weight: bold; color: #7a1f3d;&quot; data-ke-size=&quot;size16&quot;&gt;그림 보충: 재생은 충돌 응답과 구분합니다&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;그림의 &amp;lsquo;중복 / 충돌&amp;rsquo; 카드에 있는 &lt;b&gt;&amp;lsquo;같은 Key + 같은 Fingerprint &amp;rarr; 기존 결과 재생&amp;rsquo;은 처리가 완료된 경우&lt;/b&gt;입니다. 재생 자체가 &lt;code&gt;409&lt;/code&gt;라는 뜻은 아닙니다. 같은 요청이 &lt;code&gt;IN_PROGRESS&lt;/code&gt;이면 &lt;code&gt;409 IDEMPOTENCY_REQUEST_IN_PROGRESS&lt;/code&gt;, &lt;code&gt;COMPLETED&lt;/code&gt;이면 저장된 성공 응답, 확정 실패이면 저장된 실패를 반환합니다. 위 가로 흐름은 신규 요청이 진행하는 경로이며, 재생 분기는 거래 저장과 분석을 다시 실행하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;또한 마지막 카드의 &lt;code&gt;500&lt;/code&gt; 예시에 표시된 &amp;lsquo;데이터베이스 연결 실패&amp;rsquo;를 모두 &lt;code&gt;500&lt;/code&gt;으로 분류하지는 않습니다. 본문에서 설명하는 행동 이벤트 저장소 오류 경계는 &lt;b&gt;확인된 Timeout&amp;middot;일시적 가용성 실패는 &lt;code&gt;503&lt;/code&gt;, 그 밖의 내부 오류는 &lt;code&gt;500&lt;/code&gt;&lt;/b&gt;으로 구분합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;인증&amp;middot;인가 거절은 401&amp;middot;403으로 별도 구분합니다. 거래 접수 구현의 필수 Provider 가용성 점검은 멱등 선점보다 앞에 있으며, 실제 External Risk 조회는 신규 업무 처리 경로에서 수행됩니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-8&quot;&gt;[8]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 38px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background: #fffafb;&quot;&gt;
&lt;h2 style=&quot;margin: 0 0 16px; color: #7a1f3d; font-size: 23px;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 24px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#validation-boundary&quot;&gt;  유효하지 않은 요청을 거래 상태로 만들지 않기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#api-responsibilities&quot;&gt;  거래 API와 행동 이벤트 API의 책임 구분&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#common-contract&quot;&gt;  금액&amp;middot;시간&amp;middot;식별자를 명확하게 표현하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#transaction-api&quot;&gt;  거래 생성 요청과 최종 성공 응답&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#validation-implementation&quot;&gt;⚙️ JSON 파싱&amp;middot;필드 검증&amp;middot;교차 필드 검증 나누기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#behavior-api&quot;&gt;  행동 이벤트 API와 조건부 필드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#related-transaction&quot;&gt;  거래가 존재한다는 사실만으로 연결을 허용하지 않기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#idempotency-comparison&quot;&gt;  같은 중복 방지에 서로 다른 전략을 적용한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#concurrent-insert&quot;&gt;  동시 INSERT와 실패 트랜잭션의 재조회 경계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#error-contract&quot;&gt;  오류 응답도 클라이언트와의 계약으로 다루기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#test-evidence&quot;&gt;  성공 응답뿐 아니라 거절&amp;middot;중복&amp;middot;무저장을 확인하는 테스트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#operations-and-tradeoffs&quot;&gt;☁️ 운영 관점의 의미와 남겨 둔 경계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333; text-decoration: none;&quot; href=&quot;#closing&quot;&gt;  마치며: 검증된 데이터를 분석 경계로 넘기기&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;validation-boundary&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 유효하지 않은 요청을 거래 상태로 만들지 않기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 상태는 시스템이 접수한 유효한 금융 업무를 표현해야 합니다. 요청 형식부터 잘못된 데이터를 저장하기 위해 임의의 거래를 생성하면, 정상 접수 건수와 입력 거절 건수가 같은 데이터에 섞이게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;형식 또는 도메인 Validation 거절
&amp;rarr; 새 Transaction / IdempotencyRecord / BehaviorEvent를 저장하지 않음
&amp;rarr; External Risk&amp;middot;FastAPI 분석을 시작하지 않음
&amp;rarr; 오류 응답과 현재 요청의 traceId로 원인을 구분&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &amp;ldquo;저장하지 않는다&amp;rdquo;는 해당 요청으로 신규 업무 행을 생성하지 않는다는 뜻입니다. 이미 존재하던 거래나 멱등 기록을 삭제한다는 의미는 아닙니다. 잘못된 요청의 관측에는 오류 응답&amp;middot;로그&amp;middot;메트릭을 사용하고, 거래 상태에 &lt;code&gt;VALIDATION_FAILED&lt;/code&gt;를 추가하지 않았습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증을 통과한 거래는 먼저 &lt;code&gt;RECEIVED&lt;/code&gt;로 저장되지만, 접수되었다는 사실과 분석&amp;middot;위험 대응까지 성공했다는 사실도 구분합니다. 최종 성공 응답의 시점은 뒤의 거래 API 응답에서 다시 설명하겠습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;api-responsibilities&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  2. 거래 API와 행동 이벤트 API의 책임 구분&lt;/h2&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background: #fff8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #7a1f3d; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  금융 거래 생성&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/v1/transactions&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;계좌이체&amp;middot;오픈뱅킹 이체&amp;middot;ATM 인출&amp;middot;대출 실행 Mock을 접수합니다. 금액&amp;middot;기준 계좌&amp;middot;채널을 검증한 뒤 금융 업무 처리로 연결합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  행동 이벤트 수집&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/v1/behavior-events&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;로그인&amp;middot;기기 등록&amp;middot;비밀번호 변경 등 관측된 행동을 저장합니다. 필요한 경우 기존 거래에 연결하지만, 이벤트 접수 자체가 새 금융 거래나 분석 실행을 뜻하지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;대출 실행 사실을 나타내는 &lt;code&gt;LOAN_DISBURSED&lt;/code&gt;는 행동 이벤트가 아니라 금융 거래 유형으로 두었습니다. 반대로 &lt;code&gt;PASSWORD_CHANGED&lt;/code&gt;는 거래 금액이 없어도 독립적으로 발생하는 행동입니다. 두 입력을 하나의 범용 JSON API로 합치지 않은 이유입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  호출 권한과 입력 검증은 서로 다른 검사&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;거래 접수: SERVICE / TRANSACTION_INGESTOR / transaction:intake
행동 수집: SERVICE / BEHAVIOR_INGESTOR / behavior-event:intake
거래 조회: USER / transaction:read&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;수집 API는 승인된 Mock 금융&amp;middot;인증 시스템이나 수집 어댑터의 호출을 대상으로 합니다. 호출할 권한이 있어도 본문 검증을 통과해야 하며, 필드가 올바르다고 호출 권한이 생기는 것도 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;common-contract&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  3. 금액&amp;middot;시간&amp;middot;식별자를 명확하게 표현하기&lt;/h2&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;금액은 양의 10진 정수 문자열로 받기&lt;/h3&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;amount&quot;: &quot;1250000&quot;,
  &quot;currencyCode&quot;: &quot;KRW&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;금액은 문자열로 받아 허용한 형식을 검사하고 &lt;code&gt;BigDecimal&lt;/code&gt;로 변환합니다. JSON 숫자를 어떤 언어와 중간 타입으로 처리하는지에 의존하기보다, API 경계에서 10진 표현을 명시하려는 선택입니다. 문자열이라는 이유만으로 모든 값이 유효해지는 것은 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;허용: &quot;1250000&quot;
거절: 1250000       &amp;larr; 문자열이 아닌 JSON number
거절: &quot;1250000.00&quot;  &amp;larr; 소수점 표현
거절: &quot;1.25e6&quot;      &amp;larr; 지수 표현
거절: &quot;-1000&quot;, &quot;0&quot; &amp;larr; 음수 또는 0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 Validator는 &lt;code&gt;1&lt;/code&gt;부터 &lt;code&gt;999999999999999&lt;/code&gt;까지의 값을 허용합니다. DB의 &lt;code&gt;NUMERIC(19,4)&lt;/code&gt; 저장 범위에 맞춘 상한이며, 통화는 &lt;code&gt;KRW&lt;/code&gt;로 제한합니다. 반올림이나 자동 절삭으로 잘못된 입력을 교정하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;발생 시각과 저장 시각을 분리하기&lt;/h3&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;시각 필드의 의미를 설명하기 위한 예시&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;occurredAt&quot;: &quot;2026-07-23T01:15:30Z&quot;,
  &quot;createdAt&quot;: &quot;2026-07-23T01:15:31Z&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;occurredAt&lt;/code&gt;은 업무가 발생한 시각이고, &lt;code&gt;createdAt&lt;/code&gt;은 서버에서 저장한 시각입니다. 입력은 UTC ISO-8601의 &lt;code&gt;Z&lt;/code&gt; 표기로 제한합니다. 같은 순간을 나타내더라도 &lt;code&gt;+09:00&lt;/code&gt; 같은 offset 표현을 자동 수용하지 않는 것은 이 프로젝트의 입력 계약입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;검증 기준: occurredAt &amp;le; validationClock + 5분
정확히 +5분: 허용
+5분 초과: 거절&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증 시각은 주입한 &lt;code&gt;Clock&lt;/code&gt;에서 얻습니다. 시간 경계 테스트가 실제 현재 시각에 흔들리지 않게 기준 시각을 고정할 수 있는 구조입니다. 다만 초과 요청의 HTTP 분류는 거래와 행동 이벤트 코드가 다르므로, 10절에서 따로 짚겠습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;업무 ID와 참조값의 의미 유지하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;transactionId&lt;/code&gt;와 &lt;code&gt;eventId&lt;/code&gt;는 호출자가 전달하는 UUID v4 업무 식별자입니다. DB 내부 PK를 받지 않으며, 요청 추적용 &lt;code&gt;traceId&lt;/code&gt;와도 대체하지 않습니다. 고객&amp;middot;계좌&amp;middot;기기에는 실제 원문 대신 제한된 외부 참조값을 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;참조값은 제공될 때 1~128자이며 빈 값과 앞뒤 공백을 거절합니다. 서버가 임의로 &lt;code&gt;trim()&lt;/code&gt;하거나 대소문자를 바꾸지는 않습니다. 반면 검증된 UUID&amp;middot;시각&amp;middot;금액은 Fingerprint를 만들 때 정해진 형태로 직렬화합니다. &lt;b&gt;잘못된 입력을 교정하는 것과, 유효한 입력을 정규화하는 것은 다른 단계&lt;/b&gt;입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;transaction-api&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  4. 거래 생성 요청과 최종 성공 응답&lt;/h2&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;토큰은 설명용 자리표시자이며 실제 자격증명이 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/v1/transactions
Content-Type: application/json
Authorization: Bearer &amp;lt;TRANSACTION_INGESTOR_TOKEN&amp;gt;
Idempotency-Key: tx-create-demo-001&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;계약에 맞춘 요청 예시. 실제 고객&amp;middot;계좌 원문은 포함하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;transactionId&quot;: &quot;2f4c0a4e-8a9d-4c2f-9a1b-7d6e5f430001&quot;,
  &quot;transactionType&quot;: &quot;ACCOUNT_TRANSFER&quot;,
  &quot;amount&quot;: &quot;1250000&quot;,
  &quot;currencyCode&quot;: &quot;KRW&quot;,
  &quot;occurredAt&quot;: &quot;2026-07-23T01:15:30Z&quot;,
  &quot;externalCustomerRef&quot;: &quot;cust_ref_demo_a7f2&quot;,
  &quot;senderAccountRef&quot;: &quot;acct_ref_demo_s91c&quot;,
  &quot;recipientAccountRef&quot;: &quot;acct_ref_demo_r44d&quot;,
  &quot;channel&quot;: &quot;MOBILE_BANKING&quot;,
  &quot;deviceRef&quot;: &quot;device_ref_demo_18b3&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;입력은 위의 10개 업무 필드로 제한했습니다. 클라이언트가 &lt;code&gt;riskLevel&lt;/code&gt;, &lt;code&gt;processingStatus&lt;/code&gt;, &lt;code&gt;caseId&lt;/code&gt;를 보내 결과를 지정하도록 하지 않습니다. 결과는 서버의 분석과 위험 대응 정책이 결정합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background: #fff8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #7a1f3d; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;계좌이체&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ACCOUNT_TRANSFER&lt;/code&gt;&lt;br /&gt;수취 계좌 필수&lt;br /&gt;&lt;code&gt;channel = MOBILE_BANKING&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;오픈뱅킹 이체&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OPEN_BANKING_TRANSFER&lt;/code&gt;&lt;br /&gt;수취 계좌 필수&lt;br /&gt;&lt;code&gt;channel = OPEN_BANKING&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ded8ee; border-radius: 14px; background: #fbf9ff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #5d4381; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;ATM 인출&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ATM_WITHDRAWAL&lt;/code&gt;&lt;br /&gt;수취 계좌는 null&lt;br /&gt;&lt;code&gt;channel = ATM&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #dbe8dc; border-radius: 14px; background: #f9fff9; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #356a3c; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;대출 실행 Mock&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LOAN_DISBURSED&lt;/code&gt;&lt;br /&gt;수취 계좌는 null&lt;br /&gt;&lt;code&gt;channel = CORE_BANKING&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;senderAccountRef&lt;/code&gt;는 네 유형에서 모두 기준 계좌를 뜻합니다. 특히 대출 실행 Mock에서는 대출금이 입금되는 대상 계좌를 이 필드로 표현합니다. 일반적인 송금의 발신 계좌라는 의미로만 읽지 않도록 계약에 명시했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;201 Created가 의미하는 완료 범위&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;신규 거래의 성공 응답은 단순 INSERT 완료를 뜻하지 않습니다. 분석 결과의 저장&amp;middot;채택, 위험 대응, 필요한 사건 연결과 감사 기록을 확정하고, 성공 응답 Snapshot을 저장한 뒤 반환합니다. &lt;code&gt;RECEIVED&lt;/code&gt;나 &lt;code&gt;ANALYZED&lt;/code&gt;는 이 응답의 최종 성공 상태가 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;http&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 201 Created
Content-Type: application/json
X-Trace-Id: trace_blog_tx_0001&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;LOW로 분석&amp;middot;최종화된 경우의 응답 구조 예시입니다. 실제 실행 로그나 특정 입력의 고정된 분석 결과를 뜻하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;transactionId&quot;: &quot;2f4c0a4e-8a9d-4c2f-9a1b-7d6e5f430001&quot;,
  &quot;processingStatus&quot;: &quot;APPROVED&quot;,
  &quot;riskLevel&quot;: &quot;LOW&quot;,
  &quot;riskResponseOutcome&quot;: &quot;APPROVED&quot;,
  &quot;adoptedDetectionResultId&quot;: &quot;7f4c0a4e-8a9d-4c2f-9a1b-7d6e5f430101&quot;,
  &quot;caseId&quot;: null,
  &quot;createdAt&quot;: &quot;2026-07-23T01:15:31Z&quot;,
  &quot;traceId&quot;: &quot;trace_blog_tx_0001&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;LOW&amp;middot;MEDIUM은 &lt;code&gt;caseId: null&lt;/code&gt;을 명시하고, HIGH&amp;middot;CRITICAL은 사건 ID를 포함합니다. 상세 상태 조합은 #3의 계약을 따릅니다. 이 글에서는 결과를 만들어 내는 내부 분석 알고리즘보다, 클라이언트가 받는 응답의 의미에 집중합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;validation-implementation&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;⚙️ 5. JSON 파싱&amp;middot;필드 검증&amp;middot;교차 필드 검증 나누기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;필수 필드에는 &lt;code&gt;@NotNull&lt;/code&gt;을 사용하지만, 그것만으로 전체 계약을 표현하지는 않았습니다. DTO는 요청의 모양을 드러내고, 역직렬화기는 허용한 JSON 구조를 검사하며, 전용 Validator는 형식과 업무 조합을 판단하도록 나누었습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-2&quot;&gt;[2]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;저장소 TransactionCreateRequest 발췌. package와 import는 생략했습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JsonDeserialize(using = TransactionCreateRequestDeserializer.class)
public record TransactionCreateRequest(
        @NotNull String transactionId,
        @NotNull String transactionType,
        @NotNull String amount,
        @NotNull String currencyCode,
        @NotNull String occurredAt,
        @NotNull String externalCustomerRef,
        @NotNull String senderAccountRef,
        String recipientAccountRef,
        @NotNull String channel,
        String deviceRef
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;허용하지 않은 JSON을 자동으로 흡수하지 않기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;역직렬화기는 최상위 객체, 허용 필드 목록, 중복 키 여부와 각 값의 JSON 토큰을 확인합니다. 문자열&amp;middot;null 이외의 숫자&amp;middot;boolean&amp;middot;배열&amp;middot;객체를 문자열 필드에 자동 변환해 넣지 않습니다. null을 읽을 수 있다는 사실은 필수 필드의 null을 허용한다는 뜻이 아니며, 필수 검사는 다음 단계에서 수행합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;거절해야 할 입력 조각: 같은 키를 두 번 보내 앞선 값을 덮어쓰게 하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;amount&quot;: &quot;1250000&quot;,
  &quot;amount&quot;: &quot;1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 정책은 잘못된 필드명이나 예상하지 않은 데이터를 빨리 발견하는 데 유리하지만, 클라이언트가 새 필드를 먼저 보내는 변경에는 엄격합니다. 따라서 필드 확장은 서버&amp;middot;클라이언트 계약을 함께 검토해야 합니다. 엄격한 검증이 호환성 검토를 대신하는 것은 아닙니다.&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;각 필드가 유효해도 조합은 잘못될 수 있다&lt;/h3&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;교차 필드 검증을 설명하기 위한 요청 일부&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;transactionType&quot;: &quot;ATM_WITHDRAWAL&quot;,
  &quot;recipientAccountRef&quot;: &quot;acct_ref_demo_recipient&quot;,
  &quot;channel&quot;: &quot;MOBILE_BANKING&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 거래 유형과 채널은 각각 정의된 Enum에 속할 수 있습니다. 하지만 ATM 인출에 이체 채널과 수취 계좌를 함께 지정한 조합은 허용하지 않습니다. 이런 검사는 공통 필드 형식 검사와 구분한 도메인 검증에서 수행합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TransactionCreateRequest
&amp;rarr; 엄격한 JSON 해석
&amp;rarr; @NotNull 및 UUID&amp;middot;금액&amp;middot;시간&amp;middot;참조 형식 검사
&amp;rarr; 거래 유형별 수취 계좌&amp;middot;채널 조합 검사
&amp;rarr; ValidatedTransactionCommand
&amp;rarr; 멱등 처리와 업무 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증을 통과한 값은 &lt;code&gt;ValidatedTransactionCommand&lt;/code&gt;로 전달합니다. 문자열 상태의 외부 입력과 파싱된 업무 입력을 구분해, 후속 처리에서 같은 형식 해석을 반복하지 않도록 구성했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;behavior-api&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  6. 행동 이벤트 API와 조건부 필드&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/v1/behavior-events
Content-Type: application/json
Authorization: Bearer &amp;lt;BEHAVIOR_INGESTOR_TOKEN&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;신규 수취인 등록 행동을 수집하는 요청 예시&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;eventId&quot;: &quot;e54cbf7e-d857-4ca0-bff3-8d4321b7722a&quot;,
  &quot;eventType&quot;: &quot;BENEFICIARY_REGISTERED&quot;,
  &quot;occurredAt&quot;: &quot;2026-07-29T04:10:00Z&quot;,
  &quot;externalCustomerRef&quot;: &quot;cust_ref_demo_a7f2&quot;,
  &quot;accountRef&quot;: &quot;acct_ref_demo_s91c&quot;,
  &quot;deviceRef&quot;: &quot;device_ref_demo_18b3&quot;,
  &quot;transactionId&quot;: null,
  &quot;beneficiaryRef&quot;: &quot;acct_ref_demo_r82a&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트 요청은 위 8개 필드만 받습니다. &lt;code&gt;accountRef&lt;/code&gt;는 수취인을 등록한 고객 측 기준 계좌이고, &lt;code&gt;beneficiaryRef&lt;/code&gt;는 새로 등록된 수취인입니다. 둘을 하나의 계좌 필드로 합치면 Rule이 비교할 대상의 의미가 달라집니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LOGIN
LOGIN_FAILED
DEVICE_REGISTERED
PASSWORD_CHANGED
OTP_REISSUED
BENEFICIARY_REGISTERED
TRANSFER_LIMIT_CHANGED
TRANSFER_REQUESTED
ATM_WITHDRAWAL_REQUESTED&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;모든 이벤트에는 &lt;code&gt;eventId&lt;/code&gt;, &lt;code&gt;eventType&lt;/code&gt;, &lt;code&gt;occurredAt&lt;/code&gt;, &lt;code&gt;externalCustomerRef&lt;/code&gt;가 필요합니다. 나머지 필드는 이벤트 유형에 따라 필수&amp;middot;선택&amp;middot;금지로 나뉩니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;기기 식별이 필요한 행동&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LOGIN&lt;/code&gt;과 &lt;code&gt;DEVICE_REGISTERED&lt;/code&gt;에는 &lt;code&gt;deviceRef&lt;/code&gt;가 필요합니다. 계좌와 거래 연결은 선택 사항입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ded8ee; border-radius: 14px; background: #fbf9ff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #5d4381; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;계좌 문맥이 필요한 행동&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BENEFICIARY_REGISTERED&lt;/code&gt;는 &lt;code&gt;accountRef&lt;/code&gt;와 &lt;code&gt;beneficiaryRef&lt;/code&gt;가 모두 필요합니다. &lt;code&gt;TRANSFER_LIMIT_CHANGED&lt;/code&gt;에는 &lt;code&gt;accountRef&lt;/code&gt;가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background: #fff8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #7a1f3d; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;거래 연결이 필요한 행동&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TRANSFER_REQUESTED&lt;/code&gt;와 &lt;code&gt;ATM_WITHDRAWAL_REQUESTED&lt;/code&gt;에는 &lt;code&gt;accountRef&lt;/code&gt;와 &lt;code&gt;transactionId&lt;/code&gt;가 모두 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;추가 필드가 선택인 행동&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LOGIN_FAILED&lt;/code&gt;, &lt;code&gt;PASSWORD_CHANGED&lt;/code&gt;, &lt;code&gt;OTP_REISSUED&lt;/code&gt;의 계좌&amp;middot;기기&amp;middot;거래 참조는 선택입니다. 제공한 값에는 형식과 연결 검증이 적용됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;beneficiaryRef&lt;/code&gt;는 &lt;code&gt;BENEFICIARY_REGISTERED&lt;/code&gt;에서만 허용합니다. 다른 이벤트에서 보내면 무시하지 않고 거절합니다. 또한 자유 형식 &lt;code&gt;eventDetails&lt;/code&gt;, &lt;code&gt;observedSignals&lt;/code&gt;, 위험 점수 같은 필드를 받지 않아 수집 데이터와 서버 판단의 책임을 섞지 않았습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;수집 응답은 필요한 정보만 반환하기&lt;/h3&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;최초 저장 시 201 Created로 반환하는 응답 구조 예시&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;eventId&quot;: &quot;e54cbf7e-d857-4ca0-bff3-8d4321b7722a&quot;,
  &quot;eventType&quot;: &quot;BENEFICIARY_REGISTERED&quot;,
  &quot;transactionId&quot;: null,
  &quot;occurredAt&quot;: &quot;2026-07-29T04:10:00Z&quot;,
  &quot;createdAt&quot;: &quot;2026-07-29T04:10:01Z&quot;,
  &quot;traceId&quot;: &quot;trace_blog_event_0001&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;응답은 이벤트 ID&amp;middot;유형&amp;middot;선택적 거래 ID&amp;middot;발생 시각&amp;middot;저장 시각&amp;middot;현재 Trace의 6개 필드입니다. 내부 PK와 Fingerprint, 고객&amp;middot;계좌&amp;middot;기기 참조값을 그대로 되돌려 주지 않습니다. 거래가 없는 경우에도 &lt;code&gt;transactionId: null&lt;/code&gt;을 명시합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;related-transaction&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  7. 거래가 존재한다는 사실만으로 연결을 허용하지 않기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트에 &lt;code&gt;transactionId&lt;/code&gt;가 있으면 거래 존재 여부뿐 아니라 업무상 연결 가능성도 검증합니다. FK가 보조하는 참조 무결성과, 어떤 고객&amp;middot;계좌의 어떤 거래에 연결할 수 있는지는 구분해야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;참조 거래가 없음
&amp;rarr; 404 RESOURCE_NOT_FOUND

거래는 있지만 고객&amp;middot;계좌&amp;middot;거래 유형이 맞지 않음
&amp;rarr; 422 VALIDATION_ERROR&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;우선 &lt;code&gt;externalCustomerRef&lt;/code&gt;는 거래의 고객 참조와 정확히 같아야 합니다. 일반 이벤트에서 &lt;code&gt;accountRef&lt;/code&gt;를 제공했다면 거래의 기준 계좌 또는 수취 계좌에 해당하는지 확인합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 요청 행동에는 더 좁은 조건을 적용합니다. &lt;code&gt;TRANSFER_REQUESTED&lt;/code&gt;는 계좌이체 또는 오픈뱅킹 이체를 참조하고, &lt;code&gt;accountRef&lt;/code&gt;가 반드시 &lt;code&gt;senderAccountRef&lt;/code&gt;와 같아야 합니다. &lt;code&gt;ATM_WITHDRAWAL_REQUESTED&lt;/code&gt;는 ATM 인출 거래와 출금 기준 계좌를 요구합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;&amp;ldquo;존재하는 거래인가?&amp;rdquo;와 &amp;ldquo;이 이벤트가 그 거래에 속할 수 있는가?&amp;rdquo;는 서로 다른 질문입니다.&lt;/blockquote&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 연결이 선택적인 이벤트까지 모두 거래를 요구하지는 않습니다. 비밀번호 변경이나 신규 기기 등록처럼 거래 이전에 발생하는 행동을 독립적으로 수집할 수 있어야 하기 때문입니다. 단, 거래 연결을 명시한 신규 이벤트에는 위 검증을 적용합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;idempotency-comparison&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  8. 같은 중복 방지에 서로 다른 전략을 적용한 이유&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;두 API 모두 업무 ID가 있지만 처리의 수명과 반환할 결과가 다릅니다. 거래는 여러 단계를 거치는 명령의 진행 상태와 최초 응답을 보존해야 합니다. 반면 행동 이벤트 접수는 저장한 불변 이벤트에서 반환할 결과를 다시 구성할 수 있습니다. 그래서 거래에는 별도 멱등 레코드를, 행동 이벤트에는 &lt;code&gt;eventId&lt;/code&gt;와 Fingerprint 기반 자연 멱등성을 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;거래와 행동 이벤트의 중복 처리 전략 비교.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAHYHr/dJMcaiYQXDY/1XuVIZZmi4QAKPdN9VaJIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAHYHr/dJMcaiYQXDY/1XuVIZZmi4QAKPdN9VaJIK/img.png&quot; data-alt=&quot;거래 생성 명령과 불변 행동 이벤트에 서로 다른 중복 처리 전략을 적용한 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAHYHr/dJMcaiYQXDY/1XuVIZZmi4QAKPdN9VaJIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAHYHr%2FdJMcaiYQXDY%2F1XuVIZZmi4QAKPdN9VaJIK%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;1672&quot; height=&quot;941&quot; data-filename=&quot;거래와 행동 이벤트의 중복 처리 전략 비교.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;거래 생성 명령과 불변 행동 이벤트에 서로 다른 중복 처리 전략을 적용한 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;margin: 16px 0 28px; padding: 17px 20px; border-left: 4px solid #7a1f3d; border-radius: 0 12px 12px 0; background: #fff7fa; color: #444; font-size: 14px; line-height: 1.8;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;왼쪽의 &lt;b&gt;&amp;lsquo;같은 Key + 같은 Fingerprint &amp;rarr; 기존 결과 재생&amp;rsquo;은 완료된 요청을 기준으로 단순화한 표현&lt;/b&gt;입니다. 처리 중이면 &lt;code&gt;409&lt;/code&gt;로 구분하며, 확정 실패는 저장된 실패를 반환합니다. 반면 오른쪽 행동 이벤트는 같은 &lt;code&gt;eventId&lt;/code&gt;와 같은 정규화 요청이면 기존 불변 행을 바탕으로 &lt;code&gt;200 OK&lt;/code&gt;를 반환합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;거래: 자원 ID와 명령 ID를 구분하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Idempotency-Key&lt;/code&gt;는 거래 생성 명령과 재시도를 식별하고, &lt;code&gt;transactionId&lt;/code&gt;는 거래 자원을 식별합니다. 키는 8~128자의 영문&amp;middot;숫자&amp;middot;&lt;code&gt;._:-&lt;/code&gt; 조합이며, 작업 범위 &lt;code&gt;POST:/api/v1/transactions&lt;/code&gt;와 함께 Unique로 관리합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;동일 키라고 모두 재생하지는 않습니다. 입력 Fingerprint를 먼저 비교하고, 같을 때도 &lt;code&gt;IN_PROGRESS&lt;/code&gt;라면 다시 실행하지 않고 409를 반환합니다. &lt;code&gt;COMPLETED&lt;/code&gt;일 때 저장된 결과를 재생하며, 확정된 &lt;code&gt;FAILED&lt;/code&gt;도 자동 재실행 대신 저장한 실패 분류 또는 실패 Snapshot을 사용합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #dbe8dc; border-radius: 14px; background: #f9fff9; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #356a3c; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  재생 응답이 항상 200인 것은 아니다&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;최종 성공 Snapshot의 완료 재요청은 저장된 &lt;b&gt;201 Created&lt;/b&gt;를 재생합니다. 과거 무버전 legacy Snapshot은 200을 사용하는 별도 호환 규칙이 있습니다. 응답의 업무 값은 저장 결과를 유지하고 &lt;code&gt;traceId&lt;/code&gt;만 이번 요청 값으로 결합합니다. 재생은 최신 거래 조회나 새 분석이 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 분기는 인증&amp;middot;인가와 입력 검증, 필수 Provider 가용성 점검을 통과해 멱등 판별에 도달한 요청을 기준으로 합니다. 접수 Service는 Provider 구성이 사용 불가하면 멱등 재생보다 먼저 가용성 오류를 반환하는 경계를 갖습니다. 원격 조회를 재실행한다는 의미와는 구분해야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;행동 이벤트: 같은 사실의 재수신을 식별하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 &lt;code&gt;eventId&lt;/code&gt;와 같은 Fingerprint면 기존 이벤트를 반환하고, 같은 ID에 다른 내용이 오면 기존 행을 수정하지 않고 409로 거절합니다. 비슷한 시각의 같은 행동이어도 &lt;code&gt;eventId&lt;/code&gt;가 다르면 별도 사실로 저장합니다. 패턴이 유사한지 분석하는 일과 수신 중복을 판정하는 일은 다른 책임입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트는 별도 멱등 응답 JSON을 영속화하지 않고 기존 행에서 응답을 구성합니다. 코드에 등장하는 &lt;code&gt;BehaviorEventIntakeSnapshot&lt;/code&gt;은 응답 구성에 사용하는 값 객체이며, 거래의 영속 Response Snapshot과 같은 저장 전략이라는 뜻은 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;Fingerprint는 해석이 끝난 업무 입력으로 계산하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래는 10개, 행동 이벤트는 8개의 지정 필드를 고정 순서 JSON으로 직렬화해 SHA-256을 계산합니다. 원문 JSON의 공백이나 키 순서가 아니라 검증&amp;middot;정규화된 업무 내용이 비교 대상입니다. 선택 필드의 누락과 명시적 null은 같은 null로 표현하고, Trace&amp;middot;내부 PK&amp;middot;서버 생성 시각은 제외합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;concurrent-insert&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  9. 동시 INSERT와 실패 트랜잭션의 재조회 경계&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;저장 전에 같은 이벤트가 있는지 SELECT하는 것만으로 동시 요청을 막을 수는 없습니다. 두 요청 모두 아직 없는 행을 확인한 뒤 INSERT를 시도할 수 있기 때문입니다. 행동 이벤트 경계에서는 사전 조회와 별개로 DB의 &lt;code&gt;UNIQUE(event_id)&lt;/code&gt;가 경쟁 쓰기를 제한합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청 A: eventId 조회 &amp;rarr; 없음 &amp;rarr; INSERT 성공
요청 B: eventId 조회 &amp;rarr; 없음 &amp;rarr; 같은 eventId INSERT 충돌

B의 저장 트랜잭션 종료&amp;middot;rollback
&amp;rarr; 별도 읽기 트랜잭션에서 기존 행 조회
&amp;rarr; Fingerprint 같음: 기존 결과 반환
&amp;rarr; Fingerprint 다름: 충돌 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;구현은 저장 역할의 &lt;code&gt;BehaviorEventIntakeWriter&lt;/code&gt;와 재조회 역할의 &lt;code&gt;BehaviorEventReplayReader&lt;/code&gt;를 별도 Bean으로 나누고, 두 메서드에 독립 트랜잭션 경계를 둡니다. 충돌은 Writer 밖에서 처리하고, 실패한 저장 트랜잭션 안에서 다시 조회하는 구조를 피했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;두 클래스의 메서드 어노테이션만 발췌한 비교입니다. 독립 실행 가능한 클래스 전체는 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BehaviorEventIntakeWriter.create()
@Transactional(propagation = Propagation.REQUIRES_NEW)

// BehaviorEventReplayReader.findByEventId()
@Transactional(
        readOnly = true,
        propagation = Propagation.REQUIRES_NEW
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;또한 모든 &lt;code&gt;DataIntegrityViolationException&lt;/code&gt;을 이벤트 중복으로 해석하지 않습니다. 구현은 SQLSTATE &lt;code&gt;23505&lt;/code&gt;와 &lt;code&gt;uq_behavior_event_event_id&lt;/code&gt; 제약 이름을 함께 확인한 경우에만 기존 행 재조회 분기로 들어갑니다. 다른 무결성 오류를 정상적인 중복 재생으로 숨기지 않기 위한 구분입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 사례는 실제 장애가 발생했다는 회고가 아니라, 저장 Service와 통합 테스트에 나타나는 경쟁 처리 설계입니다. 동시 요청에서 기대하는 결과는 뒤의 테스트 코드로 확인할 수 있습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;error-contract&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  10. 오류 응답도 클라이언트와의 계약으로 다루기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;오류는 모두 같은 400이나 500으로 합치지 않았습니다. 클라이언트가 입력을 고쳐야 하는지, 기존 요청이 처리 중인지, 의존성을 사용할 수 없는지를 구분하도록 HTTP 상태와 공개 오류 코드를 함께 제공합니다. 아래 분류는 FinGuardOps의 계약이며 모든 API에 강제되는 유일한 분류 방식이라는 뜻은 아닙니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #eed5db; border-radius: 14px; background: #fff8f9; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #922e3b; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;400 &amp;middot; 형식 검증 실패&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;잘못된 JSON, 알 수 없는 필드, 중복 JSON 키, 허용하지 않은 타입, UUID&amp;middot;금액&amp;middot;필수 헤더의 형식 위반을 &lt;code&gt;VALIDATION_ERROR&lt;/code&gt;로 반환합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #e8dcb9; border-radius: 14px; background: #fffcf5; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #855b17; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;422 &amp;middot; 도메인 검증 실패&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 유형별 필드 조합, 이벤트 조건부 필드, 연결된 거래의 고객&amp;middot;계좌&amp;middot;유형 불일치 등을 구분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;404 &amp;middot; 참조 거래 없음&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트가 지정한 거래가 없으면 &lt;code&gt;RESOURCE_NOT_FOUND&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; min-width: 0; padding: 20px 22px; border: 1px solid #ded8ee; border-radius: 14px; background: #fbf9ff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #5d4381; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;409 &amp;middot; 처리 중 또는 충돌&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;키 충돌, 처리 중인 동일 명령, 거래 ID 중복, 이벤트 ID의 내용 충돌을 각각 공개 오류 코드로 구분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;503과 500을 구분하는 경계&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트 저장 경계는 확인된 Query Timeout을 &lt;code&gt;DEPENDENCY_TIMEOUT&lt;/code&gt;, 분류된 일시적 저장소 가용성 실패를 &lt;code&gt;DEPENDENCY_UNAVAILABLE&lt;/code&gt;로 매핑합니다. 그 밖의 DataAccess 오류나 모순된 내부 데이터까지 모두 503으로 바꾸지는 않습니다. 거래의 External Risk&amp;middot;분석 실패는 해당 단계의 별도 계약을 따릅니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;형식과 도메인의 경계가 실제 코드에서도 같은지 확인하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 대조하면서 주의한 부분은 미래 발생 시각의 분류입니다. 두 API 모두 5분 초과 입력을 거절하지만, 거래 Validator는 &lt;code&gt;FORMAT&lt;/code&gt;, 행동 이벤트 Validator는 &lt;code&gt;DOMAIN&lt;/code&gt; 예외를 만듭니다. 공통 예외 처리기는 이를 각각 400과 422로 변환합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-3&quot;&gt;[3]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;동일한 조건: occurredAt &amp;gt; validationClock + 5분

TransactionRequestValidator
&amp;rarr; FORMAT &amp;rarr; 400 VALIDATION_ERROR

BehaviorEventRequestValidator
&amp;rarr; DOMAIN &amp;rarr; 422 VALIDATION_ERROR&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 &amp;ldquo;미래 5분 초과는 두 API 모두 422&amp;rdquo;라고 일반화하지 않았습니다. &lt;b&gt;허용 범위가 같다는 것과 오류 분류까지 같다는 것은 다릅니다.&lt;/b&gt; 이 차이를 유지할지 통일할지는 API 계약과 테스트를 함께 검토할 항목이며, 설명을 위해 구현 결과를 임의로 바꾸지는 않습니다.&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;필드 오류와 내부 원문을 분리하기&lt;/h3&gt;
&lt;pre class=&quot;http&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 400 Bad Request
Content-Type: application/json&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;amount가 &quot;1.25e6&quot;인 요청에 대한 Validator&amp;middot;예외 매핑 구조 예시. 실제 호출을 캡처한 로그는 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;code&quot;: &quot;VALIDATION_ERROR&quot;,
  &quot;message&quot;: &quot;요청 필드를 확인해 주세요.&quot;,
  &quot;traceId&quot;: &quot;trace_blog_amount_0001&quot;,
  &quot;fieldErrors&quot;: [
    {
      &quot;field&quot;: &quot;amount&quot;,
      &quot;code&quot;: &quot;INVALID_AMOUNT_FORMAT&quot;,
      &quot;reason&quot;: &quot;amount must be an unsigned decimal integer string&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;필드 오류는 &lt;code&gt;field&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;으로 구성됩니다. 화면은 필드에 오류를 연결할 수 있고, 공개 오류 코드는 입력 원문을 응답에 복사하지 않고도 원인을 분류하는 데 사용할 수 있습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;응답에 SQL, DB 제약 이름, 스택 트레이스, 원본 토큰, Fingerprint나 실제 계좌 원문을 넣지 않습니다. 내부 진단에 사용하는 상세와 외부에 제공할 안전한 설명을 분리합니다. 인증 실패와 권한 부족 역시 Validation과 혼합하지 않고 401&amp;middot;403 경계에서 처리합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-7&quot;&gt;[7]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;test-evidence&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  11. 성공 응답뿐 아니라 거절&amp;middot;중복&amp;middot;무저장을 확인하는 테스트&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;입력 검증 테스트는 &amp;ldquo;예외가 발생한다&amp;rdquo;는 사실만 확인해서는 부족하다고 보았습니다. 어느 필드가 어떤 분류로 거절되는지, 오류 응답 이후 신규 업무 행이 남지 않는지, 중복 요청에 새 결과가 생기지 않는지까지 검증 조건으로 두었습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드와 실행 결과의 구분&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 저장소에 정의된 테스트와 assertion을 설명한 것입니다. 새로 실행해 얻은 성공 건수&amp;middot;실패 로그&amp;middot;성능 측정값은 제시하지 않습니다. 특히 여러 스레드의 DB 경쟁 테스트를 실제 Kubernetes 다중 Pod 부하 실험과 동일하게 표현하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;① JSON 경계: 중복 키가 값을 덮어쓰기 전에 거절되는가&lt;/h3&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;TransactionCreateRequestJsonTest의 테스트 메서드 발췌. fixture&amp;middot;assertion helper는 원본 파일에 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void rejectsDuplicateJsonKeyBeforeAnyValueCanBeOverwritten() {
    String json = completeJson().replace(
            &quot;\&quot;amount\&quot;: \&quot;0001\&quot;&quot;,
            &quot;\&quot;amount\&quot;: \&quot;0001\&quot;,\n  \&quot;amount\&quot;: \&quot;2\&quot;&quot;
    );

    assertFormatFailure(json, &quot;amount&quot;, DUPLICATE_JSON_FIELD);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 테스트 클래스는 객체가 아닌 JSON root, 알 수 없는 필드, 숫자&amp;middot;boolean&amp;middot;배열&amp;middot;객체 토큰도 검사합니다. 선택 필드의 누락과 명시적 null은 같은 null로 다루되, 숫자 토큰을 문자열로 바꾸는 입력 교정은 허용하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;② HTTP와 DB 경계: 거절 뒤에 업무 행이 남지 않는가&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TransactionIntakeControllerIntegrationTest&lt;/code&gt;에는 짧은 멱등성 키를 보낸 요청이 400으로 거절되고, 거래와 멱등 레코드의 개수가 모두 0인지 확인하는 테스트가 있습니다. Provider가 없는 환경에서도 요청 Validation이 먼저 적용되는지 함께 확인합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;requestValidationStillReturns400BeforeAvailability()의 assertion 부분 발췌&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.andExpect(status().isBadRequest())
.andExpect(jsonPath(&quot;$.code&quot;).value(&quot;VALIDATION_ERROR&quot;));

assertThat(transactionRepository.count()).isZero();
assertThat(idempotencyRepository.count()).isZero();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;③ 동시성: 여러 요청에서 최초 생성과 재생이 구분되는가&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BehaviorEventIntakeIntegrationTest&lt;/code&gt;는 8개의 작업을 latch로 대기시킨 후 같은 요청을 동시에 전달하도록 작성되어 있습니다. 검증 조건은 Created 하나, 나머지 Replay, 그리고 DB 행 하나입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 18px 0 8px; font-size: 13px; color: #666;&quot; data-ke-size=&quot;size14&quot;&gt;동시 요청 통합 테스트의 assertion 발췌. taskCount는 8입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertThat(results.stream().filter(
        BehaviorEventIntakeResult.Created.class::isInstance
)).hasSize(1);
assertThat(results.stream().filter(
        BehaviorEventIntakeResult.Replay.class::isInstance
)).hasSize(taskCount - 1);
assertThat(rowCount()).isEqualTo(1);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 테스트는 중복 방지를 메모리 플래그에만 의존하지 않고 PostgreSQL 저장 경쟁과 결과 재생으로 검증한다는 점에서 의미가 있습니다. 배포 환경에서의 네트워크 단절&amp;middot;프로세스 종료&amp;middot;여러 Pod 경쟁은 별도의 조건으로 추가 검증해야 합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;동일 검증을 다시 실행할 수 있는 명령&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Windows PowerShell에서 저장소 루트 기준으로 아래 명령을 사용할 수 있습니다. Java 17을 사용할 수 있어야 하며, PostgreSQL Testcontainers 통합 테스트에는 Docker 실행 환경이 필요합니다. 단위&amp;middot;웹 테스트와 PostgreSQL 통합 테스트는 Gradle 작업을 분리했습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;margin: 18px 0 24px; padding: 20px; border-radius: 12px; overflow-x: auto; background: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.75; white-space: pre; word-break: normal;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd backend

# JSON 파싱&amp;middot;형식 검증
.\gradlew.bat test --tests &quot;com.aifds.backend.transaction.dto.TransactionCreateRequestJsonTest&quot; --rerun-tasks

# 거래 / 행동 이벤트 Validator
.\gradlew.bat test --tests &quot;com.aifds.backend.transaction.validation.TransactionRequestValidatorTest&quot; --tests &quot;com.aifds.backend.behavior.validation.BehaviorEventRequestValidatorTest&quot; --rerun-tasks

# PostgreSQL 저장&amp;middot;중복&amp;middot;동시 요청
.\gradlew.bat integrationTest --tests &quot;com.aifds.backend.persistence.BehaviorEventIntakeIntegrationTest&quot; --tests &quot;com.aifds.backend.persistence.TransactionIntakeControllerIntegrationTest&quot; --rerun-tasks

# 전체 회귀 검증
.\gradlew.bat test integrationTest&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실행 뒤에는 &lt;code&gt;backend/build/reports/tests/test/index.html&lt;/code&gt;과 &lt;code&gt;backend/build/reports/tests/integrationTest/index.html&lt;/code&gt;에서 결과를 확인합니다. 개별 수정의 통과 여부뿐 아니라 전체 회귀 테스트와 해당 테스트가 실제로 실행되었는지도 함께 확인하는 기준으로 삼습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 실패 로그와 수정 전후 기록이 있는 문제는 별도 Troubleshooting 글에서 다룹니다. 이 글의 경쟁 요청 예시나 경계값 테스트를 과거 운영 장애가 발생했던 것처럼 바꾸어 설명하지는 않습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;operations-and-tradeoffs&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;☁️ 12. 운영 관점의 의미와 남겨 둔 경계&lt;/h2&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;재전송을 정상적인 입력 조건으로 보기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;응답을 받지 못한 호출자는 같은 요청을 다시 보낼 수 있습니다. 이때 키를 새로 만드는 것과 기존 키로 재전송하는 것은 다른 의미입니다. 거래는 동일 명령의 키와 내용을 유지해야 저장된 결과를 재현할 수 있으며, 입력 내용이 바뀌면 같은 명령으로 취급하지 않습니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;행동 이벤트 역시 재전송 시 같은 사실의 &lt;code&gt;eventId&lt;/code&gt;를 유지해야 합니다. 유사한 이벤트에 매번 새 ID를 부여한 뒤 서버가 알아서 중복으로 묶어 주기를 기대하지 않습니다. 호출자와 서버가 동일성 기준을 함께 지켜야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;공유 DB 설계와 다중 인스턴스 실험을 구분하기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;공유 PostgreSQL의 Unique와 영속 레코드를 사용하는 구조는 JVM 하나의 메모리만을 기준으로 중복을 판단하지 않습니다. 다만 이것만으로 모든 배포 장애에서 외부 호출까지 전역적으로 정확히 한 번 실행된다고 주장하지 않습니다. 이미 확정한 결과의 재생, 처리 중 요청의 재실행 제한, 불확실한 상태의 복구는 각 계약으로 다뤄야 합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;관측 정보에 금융 원문을 실어 보내지 않기&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;API의 &lt;code&gt;traceId&lt;/code&gt;는 오류 응답과 서버 진단을 연결하는 값입니다. 이를 업무 ID나 금융 데이터로 대체하지 않습니다. 멱등 키&amp;middot;Fingerprint&amp;middot;고객&amp;middot;계좌 참조값을 메트릭 label로 무분별하게 넣지 않고, 오류 분류와 처리 결과 중심으로 관측합니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서 설명한 Trace 필드는 API의 추적 계약입니다. 필드가 있다는 사실만으로 분산 Trace 수집 환경까지 모두 구축되었다는 의미는 아닙니다. 상세 메트릭&amp;middot;로그&amp;middot;트레이싱 연결은 Observability 편에서 다룹니다.&lt;/p&gt;
&lt;h3 style=&quot;margin: 32px 0 14px; color: #64182f; font-size: 21px; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size23&quot;&gt;계약을 엄격하게 만들 때의 비용&lt;/h3&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;Z 시각, 정수 문자열 금액, 허용 필드 목록과 고정 채널 조합은 입력을 명확하게 만드는 대신 호출자의 자유도를 제한합니다. 새 통화&amp;middot;채널&amp;middot;필드를 추가하려면 Validator뿐 아니라 Fingerprint, 응답 호환성, DB 제약과 테스트의 영향도 함께 확인해야 합니다. 편리하게 받아 주는 API보다 해석이 달라지지 않는 API를 우선한 선택입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;또한 기록된 만료 시각이 있다는 사실과 키 재사용 정책이 실행된다는 사실은 구분합니다. 거래의 &lt;code&gt;expiresAt&lt;/code&gt; 값만 보고 &amp;ldquo;24시간 뒤 같은 키를 다시 사용할 수 있다&amp;rdquo;고 설명하지 않습니다. 만료 판정과 정리 정책은 저장 필드와 별개의 동작 계약입니다.&lt;sup style=&quot;font-size: 12px; white-space: nowrap;&quot;&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-1&quot;&gt;[1]&lt;/a&gt; &lt;a style=&quot;color: #7a1f3d; text-decoration: none;&quot; href=&quot;#ref-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr style=&quot;margin: 42px 0; border: 0; border-top: 1px solid #eee;&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;closing&quot; style=&quot;margin: 0 0 22px; padding: 0 0 12px; border-bottom: 3px solid #7a1f3d; color: #222; font-size: 26px; line-height: 1.5; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;  13. 마치며: 검증된 데이터를 분석 경계로 넘기기&lt;/h2&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서 거래&amp;middot;행동 이벤트 API를 설계하며 중요하게 본 것은 입력 필드의 개수보다 &lt;b&gt;어떤 요청을 업무로 받아들이고, 어느 지점에서 거절하며, 반복 요청에 무엇을 돌려줄 것인가&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.85;&quot; data-ke-style=&quot;style1&quot;&gt;형식 검증으로 해석을 고정하고, 도메인 검증으로 잘못된 조합을 막으며, 업무의 성격에 맞는 멱등성과 DB 제약으로 중복 실행을 통제합니다.&lt;/blockquote&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래는 멱등성 키와 최초 응답 기록을 중심으로, 행동 이벤트는 불변 이벤트 ID와 저장된 사실을 중심으로 처리합니다. 같은 중복 방지라도 모든 요청에 같은 테이블과 같은 상태 모델이 필요한 것은 아니었습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 입력 경계 위에서 다음으로 필요한 것은 검증된 거래&amp;middot;행동&amp;middot;RuleVersion을 분석 Snapshot으로 묶고, FastAPI 응답을 다시 검증하는 과정입니다. 다음 글에서는 Spring Boot와 FastAPI 사이의 요청&amp;middot;응답 계약과 분석 결과 채택 경계를 정리하겠습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #dde1e6; border-radius: 14px; background: #f7f8fa; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #505764; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  관련 계약과 코드&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;본문의 번호는 아래 저장소 문서와 구현 파일을 가리킵니다. API 규칙, 구현에 나타나는 오류 분류, 테스트의 검증 조건을 구분해 확인할 수 있습니다.&lt;/p&gt;
&lt;p id=&quot;ref-1&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[1]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/03-api/transaction-detection-api.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;거래&amp;middot;행동&amp;middot;탐지 API 계약&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/03-api/api-conventions.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;API 공통 규칙&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-2&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[2]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/transaction/dto/TransactionCreateRequest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionCreateRequest&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/transaction/dto/TransactionCreateRequestDeserializer.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionCreateRequestDeserializer&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-3&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/transaction/validation/TransactionRequestValidator.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionRequestValidator&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/behavior/validation/BehaviorEventRequestValidator.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BehaviorEventRequestValidator&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-4&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[4]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/transaction/service/TransactionIntakeService.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionIntakeService&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/04-database/transaction-intake-schema.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;거래 접수 물리 계약&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-5&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[5]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/04-database/behavior-event-intake-schema.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;행동 이벤트 접수 물리 계약&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/behavior/service/BehaviorEventIntakeService.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BehaviorEventIntakeService&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-6&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[6]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/behavior/service/BehaviorEventIntakeWriter.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BehaviorEventIntakeWriter&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/behavior/service/BehaviorEventReplayReader.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BehaviorEventReplayReader&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-7&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[7]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/common/error/GlobalExceptionHandler.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GlobalExceptionHandler&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/main/java/com/aifds/backend/common/error/FieldErrorResponse.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FieldErrorResponse&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-8&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[8]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/test/java/com/aifds/backend/transaction/dto/TransactionCreateRequestJsonTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionCreateRequestJsonTest&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/integrationTest/java/com/aifds/backend/persistence/BehaviorEventIntakeIntegrationTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BehaviorEventIntakeIntegrationTest&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/src/integrationTest/java/com/aifds/backend/persistence/TransactionIntakeControllerIntegrationTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TransactionIntakeControllerIntegrationTest&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;ref-9&quot; style=&quot;margin: 12px 0; font-size: 13px; line-height: 1.8; scroll-margin-top: 24px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[9]&lt;/b&gt; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/backend/build.gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Gradle 테스트 작업 정의&lt;/a&gt; &amp;middot; &lt;a style=&quot;color: #7a1f3d; overflow-wrap: anywhere;&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps/blob/main/docs/02-architecture/security-architecture.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;보안 아키텍처&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px 22px; border: 1px solid #d8e4f5; border-radius: 14px; background: #f8fbff; box-sizing: border-box;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; color: #234f88; font-size: 18px; font-weight: bold; line-height: 1.5; overflow-wrap: anywhere;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[FinGuardOps #6] 금융 업무와 AI 계산을 분리한 Spring Boot&amp;middot;FastAPI Rule 분석 연동&lt;/b&gt;&lt;br /&gt;거래&amp;middot;행동&amp;middot;RuleVersion Snapshot을 전달하고, 분석 응답을 검증해 DetectionResult와 Evidence로 연결하는 과정을 다룹니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 38px 0 14px; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; max-width: 100%; box-sizing: border-box; padding: 13px 20px; border-radius: 10px; background: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36,41,47,.18);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; flex-shrink: 0; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;&quot; width=&quot;20&quot; height=&quot;20&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>FinGuardOps/Project</category>
      <category>FinGuardOps</category>
      <category>PostgreSQL</category>
      <category>RestAPI</category>
      <category>springboot</category>
      <category>validation</category>
      <category>금융IT</category>
      <category>동시성</category>
      <category>멱등성</category>
      <category>이상거래탐지</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/55</guid>
      <comments>https://bbbicsan.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 5 Sep 2026 23:35:02 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 28일차 TIL : React 댓글 부분 리렌더링&amp;middot;Spring Validation&amp;middot;전역 예외 처리</title>
      <link>https://bbbicsan.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 28일차 TIL : React 댓글 부분 리렌더링&amp;middot;Spring Validation&amp;middot;전역 예외 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coyHSK/dJMcadDbaV6/pB6JCEDJe7g3pYqh9ZbLv1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coyHSK/dJMcadDbaV6/pB6JCEDJe7g3pYqh9ZbLv1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coyHSK/dJMcadDbaV6/pB6JCEDJe7g3pYqh9ZbLv1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoyHSK%2FdJMcadDbaV6%2FpB6JCEDJe7g3pYqh9ZbLv1%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;React의 댓글 컴포넌트를 &lt;code&gt;List &amp;rarr; Item&lt;/code&gt; 구조로 분리하고, 댓글 &lt;b&gt;추가&amp;middot;삭제&amp;middot;수정 시 전체 데이터를 다시 조회하지 않고 필요한 부분만 상태 갱신하여 리렌더링&lt;/b&gt;했다. Spring Boot에서는 DELETE/PATCH API를 MyBatis와 연결하고, &lt;code&gt;@Valid&lt;/code&gt; 기반 DTO 검증과 사용자 정의 예외&amp;middot;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;를 이용한 전역 예외 처리까지 구현했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Spring MVC 요청/응답 흐름 복습
        &amp;darr;
DispatcherServlet
        &amp;darr;
Controller Mapping
        &amp;darr;
@RequestParam
@PathVariable
@RequestBody
@RequestHeader
        &amp;darr;
Service / Repository
        &amp;darr;
ResponseEntity
Status + Header + Body
        &amp;darr;
React Blog 상세 화면
        &amp;darr;
BlogCommentList
        &amp;darr;
BlogCommentItem
        &amp;darr;
댓글 추가
&amp;rarr; 배열 append
        &amp;darr;
댓글 삭제
&amp;rarr; filter()
        &amp;darr;
댓글 수정
&amp;rarr; map()
        &amp;darr;
필요한 댓글 영역만 부분 리렌더링
        &amp;darr;
Spring Boot Comment API
        &amp;darr;
DELETE /comments/delete/{id}
PATCH /comments/update/{id}/{comment}
        &amp;darr;
CommentController
        &amp;darr;
CommentService
        &amp;darr;
CommentMapper
        &amp;darr;
MyBatis
        &amp;darr;
Validation
@Valid
@Email
@NotBlank
@Size
        &amp;darr;
Custom Exception
LoginFailException
        &amp;darr;
@RestControllerAdvice
        &amp;darr;
GlobalExceptionHandler
        &amp;darr;
401 Unauthorized
        &amp;darr;
React Validation Error UI&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Spring MVC&lt;/li&gt;
&lt;li&gt;DispatcherServlet&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PathVariable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestHeader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;Partial Rendering&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spread Operator&lt;/li&gt;
&lt;li&gt;Props&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@DeleteMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PatchMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Valid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Jakarta Validation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@NotBlank&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Custom Exception&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ExceptionHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;204 No Content&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DTO&lt;/li&gt;
&lt;li&gt;Builder&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Spring MVC 전체 요청 흐름 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업 초반에는 지금까지 학습했던 Spring MVC의 요청 처리 흐름을 다시 한 번 전체 구조로 정리했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Browser / React
      &amp;darr; HTTP Request
WAS
Tomcat / WebLogic
      &amp;darr;
DispatcherServlet
      &amp;darr;
Controller
      &amp;darr;
@GetMapping
@PostMapping
@DeleteMapping
@PatchMapping
      &amp;darr;
Parameter Binding
      &amp;darr;
Service
      &amp;darr;
Repository / Mapper
      &amp;darr;
Database
      &amp;darr;
Controller
      &amp;darr;
ResponseEntity
      &amp;darr;
HTTP Response
      &amp;darr;
Browser / React&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 모든 요청이 Controller 메서드로 바로 들어가는 것이 아니라, &lt;code&gt;DispatcherServlet&lt;/code&gt;이 Front Controller 역할을 담당한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Controller Parameter Binding&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 사용했던 주요 요청값 바인딩 방식도 함께 복습했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequestParam
&amp;rarr; Query String

@PathVariable
&amp;rarr; URL Path 값

@RequestBody
&amp;rarr; JSON Body &amp;rarr; DTO

@RequestHeader
&amp;rarr; HTTP Header&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;GET /blogs/read/1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서는:&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;@PathVariable(&quot;id&quot;) Integer id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용할 수 있고,&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Authorization: Bearer ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는:&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;@RequestHeader(&quot;Authorization&quot;) String at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 받을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. ResponseEntity로 HTTP Response 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는 &lt;code&gt;ResponseEntity&amp;lt;?&amp;gt;&lt;/code&gt;를 통해 HTTP 응답을 직접 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;ResponseEntity
├─ Status
├─ Header
└─ Body&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Status Code는 크게:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;2xx
&amp;rarr; 성공

3xx
&amp;rarr; Redirect

4xx
&amp;rarr; Client Error

5xx
&amp;rarr; Server Error&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 댓글 수정&amp;middot;삭제와 로그인 실패 처리에서도 이 상태코드를 직접 활용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. React 댓글 컴포넌트 분리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 상세 페이지에서 댓글 렌더링 책임을 별도의 컴포넌트로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReadPage
      &amp;darr;
BlogCommentList
      &amp;darr;
BlogCommentItem&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogReadPage&lt;/code&gt;에서는 댓글 데이터와 이벤트 핸들러를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;BlogCommentList
    comments={comments || []}
    handler={commentDeleteHandler}
    updateHandler={commentUpdateHandler}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. BlogCommentList&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogCommentList&lt;/code&gt;에서는 댓글 배열을 순회한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;comments.map((comment, idx) =&amp;gt; {
    return (
        &amp;lt;BlogCommentItem
            key={idx}
            comment={comment}
            handler={handler}
            updateHandler={updateHandler}
        /&amp;gt;
    );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 부모 컴포넌트로부터 전달받은 데이터와 함수가:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReadPage
      &amp;darr; props
BlogCommentList
      &amp;darr; props
BlogCommentItem&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 내려간다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. BlogCommentItem의 수정 상태 관리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 댓글 Item은 자신의 수정 상태를 관리한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [isEdit, setIsEdit] =
    useState(false);

const [mention, setMention] =
    useState('');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;isEdit
&amp;rarr; 현재 댓글이 수정 모드인지 여부

mention
&amp;rarr; 수정 중인 댓글 내용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. 기존 댓글을 수정 입력값으로 설정&lt;/h1&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    setMention(comment.comment);
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 렌더링 시 기존 댓글 내용을 &lt;code&gt;mention&lt;/code&gt; 상태에 복사한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;comment.comment
      &amp;darr;
setMention()
      &amp;darr;
수정 Input 초기값&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. 작성자만 수정/삭제 버튼 표시&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 사용자 Email을 가져온다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user =
    localStorage.getItem('user');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 댓글 작성자와 비교한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;user === comment.email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 true일 때만 수정/삭제 UI를 보여준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;로그인 사용자
      &amp;darr;
localStorage user

댓글 작성자
      &amp;darr;
comment.email

같음
&amp;rarr; 수정 / 삭제 버튼 표시

다름
&amp;rarr; 버튼 숨김&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. 수정모드 전환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼은 &lt;code&gt;isEdit&lt;/code&gt; 상태에 따라 동작이 달라진다.&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;&amp;lt;Button
    title={
        isEdit
            ? '수정완료'
            : '수정'
    }
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;isEdit = false
&amp;rarr; &quot;수정&quot;

클릭
&amp;rarr; 수정모드 ON

isEdit = true
&amp;rarr; &quot;수정완료&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 UI가 변한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 댓글 추가 후 부분 리렌더링&lt;/h1&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;pre class=&quot;arcade&quot;&gt;&lt;code&gt;setComments(ary =&amp;gt; {
    return [
        ...ary,
        response.data
    ];
});

setComment('');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 댓글을 다시 조회하지 않고 새 댓글만 추가한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;기존 comments[]
      +
새 Comment
      &amp;darr;
새 comments[]
      &amp;darr;
setComments()
      &amp;darr;
댓글 영역만 리렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 댓글 추가에서는 Spread Operator&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 추가의 핵심은:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[...ary, response.data]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;...ary
&amp;rarr; 기존 댓글 모두 유지

response.data
&amp;rarr; 새로 등록된 댓글 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 새로운 배열을 만들어 React 상태를 갱신한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 댓글 삭제 Handler&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제에서는:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const commentDeleteHandler =
    async (e, id) =&amp;gt; {

    console.log(
        `debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; comment id ${id}`
    );

};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 삭제 대상의 댓글 ID를 전달받는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. 기존 json-server 댓글 삭제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 json-server에서는:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;await api.delete(
    `/comments/${id}`
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. Spring Boot 댓글 삭제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 연동 후에는 endpoint를 변경했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;await api.delete(
    `/comments/delete/${id}`,
    {
        headers: {
            Authorization:
                at ? at : &quot;&quot;
        }
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 구조:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DELETE /comments/delete/{id}

Header
Authorization: Access Token&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. CommentController DELETE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서는 댓글 ID를 PathVariable로 받는다.&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;@DeleteMapping(&quot;/delete/{id}&quot;)
public ResponseEntity&amp;lt;?&amp;gt; delete(
        @PathVariable(&quot;id&quot;)
        Integer id
) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; comment controller delete&quot;
    );

    int flag =
        commentService.delete(id);

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/comments/delete/3
          &amp;darr;
@PathVariable
          &amp;darr;
id = 3&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. CommentMapper DELETE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper Interface에는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public int delete(Integer id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis XML:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;delete id=&quot;delete&quot;
        parameterType=&quot;java.lang.Integer&quot;&amp;gt;

    DELETE FROM
        SPRING_COMMENT_TBL
    WHERE
        ID = #{id}

&amp;lt;/delete&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. 댓글 삭제 전체 Backend 흐름&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CommentController
      &amp;darr;
CommentService.delete(id)
      &amp;darr;
CommentMapper.delete(id)
      &amp;darr;
CommentMapper.xml
      &amp;darr;
DELETE
FROM SPRING_COMMENT_TBL
WHERE ID = #{id}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. 삭제 후 React에서 전체 재조회하지 않기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 성공 후:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;setComments(
    comments.filter(
        c =&amp;gt; c.id !== id
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;filter()&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;기존 comments
      &amp;darr;
filter()
      &amp;darr;
삭제 ID와 다른 댓글만 유지
      &amp;darr;
새 comments
      &amp;darr;
setComments()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. 댓글 삭제와 filter()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;comments

[
  id=1,
  id=2,
  id=3
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 &lt;code&gt;id=2&lt;/code&gt;를 삭제하면:&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;comments.filter(
    c =&amp;gt; c.id !== 2
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[
  id=1,
  id=3
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 새로운 배열로 상태가 변경되기 때문에 화면에서도 해당 댓글이 제거된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. 댓글 삭제 전체 흐름&lt;/h1&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;삭제 버튼 클릭
      &amp;darr;
commentDeleteHandler(id)
      &amp;darr;
Axios DELETE
      &amp;darr;
Spring CommentController
      &amp;darr;
CommentService
      &amp;darr;
MyBatis DELETE
      &amp;darr;
Database
      &amp;darr;
HTTP Response
      &amp;darr;
React filter()
      &amp;darr;
setComments()
      &amp;darr;
삭제된 댓글만 UI에서 제거&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. 댓글 수정 Handler&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 수정 기능은:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const commentUpdateHandler =
    async (id, mention) =&amp;gt; {

    console.log(
        `debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; id ${id},
         mention ${mention}`
    );

};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 수정 대상 ID와 수정 내용을 받는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. 기존 json-server 수정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await api.patch(
    `/comments/${id}`,
    {
        comment: mention
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식을 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Spring Boot 댓글 수정 요청&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 버전에서는:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await api.patch(
    `/comments/update/${id}/${encodeURIComponent(mention)}`,
    null,
    {
        headers: {
            Authorization:
                at ? at : &quot;&quot;
        }
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 변경했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. encodeURIComponent&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 댓글 내용은 URL Path에 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글에는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;공백
한글
특수문자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 포함될 수 있기 때문에:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;encodeURIComponent(mention)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;사용자가 입력한 댓글
      &amp;darr;
encodeURIComponent()
      &amp;darr;
URL-safe 문자열
      &amp;darr;
PathVariable&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. CommentController PATCH&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PatchMapping(
    &quot;/update/{id}/{comment}&quot;
)
public ResponseEntity&amp;lt;?&amp;gt; update(

        @PathVariable(&quot;id&quot;)
        Integer id,

        @PathVariable(&quot;comment&quot;)
        String comment
) {

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;PATCH
/comments/update/3/수정내용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 각각:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;id
&amp;rarr; 3

comment
&amp;rarr; 수정내용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 바인딩한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. Controller에서 Map 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값을 Service로 전달하기 위해:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; map =
    new HashMap&amp;lt;&amp;gt;();

map.put(&quot;id&quot;, id);
map.put(&quot;comment&quot;, comment);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 Map을 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;int flag =
    commentService.update(map);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 호출한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. MyBatis parameterType = Map&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서 Map을 전달하기 때문에 Mapper XML에서는:&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;&amp;lt;update id=&quot;update&quot;
        parameterType=&quot;java.util.Map&quot;&amp;gt;

&amp;lt;/update&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML에서는:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;#{id}
#{comment}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 DTO Property가 아니라 &lt;b&gt;Map Key&lt;/b&gt;와 연결된다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;map.put(&quot;id&quot;, id)
&amp;rarr; #{id}

map.put(&quot;comment&quot;, comment)
&amp;rarr; #{comment}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. 댓글 수정 HTTP Status&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 수정 결과는:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;return flag != 0
    ? ResponseEntity
        .status(
            HttpStatus.NO_CONTENT
        )
        .build()

    : ResponseEntity
        .status(
            HttpStatus.NOT_FOUND
        )
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 반환했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE 성공
&amp;rarr; 204 No Content

수정할 댓글 없음
&amp;rarr; 404 Not Found&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. React 댓글 수정에서는 map()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 수정 성공 후에는:&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;setComments(ary =&amp;gt; {

    return ary.map(comment =&amp;gt; {

        return comment.id === id
            ? {
                ...comment,
                comment: mention
            }
            : comment;

    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 상태를 갱신한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. 댓글 수정과 map()&lt;/h1&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;comments[]
      &amp;darr;
map()
      &amp;darr;
수정 대상 ID인가?
   ├─ YES
   │   &amp;rarr; 기존 객체 복사
   │   &amp;rarr; comment 값만 변경
   │
   └─ NO
       &amp;rarr; 기존 객체 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 전체 데이터를 다시 받아오지 않고 수정된 객체 하나만 교체한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. 댓글 추가&amp;middot;삭제&amp;middot;수정 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 React 부분 리렌더링에서 가장 중요했던 부분이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;React 처리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;댓글 추가&lt;/td&gt;
&lt;td&gt;Spread + append&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;댓글 삭제&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;댓글 수정&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE
&amp;rarr; [...comments, newComment]

DELETE
&amp;rarr; comments.filter(...)

UPDATE
&amp;rarr; comments.map(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 &lt;b&gt;기존 상세 데이터를 서버에서 다시 조회하지 않고 상태만 변경&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. 부분 리렌더링의 의미&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 방식이라면 댓글 하나를 변경할 때마다:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;댓글 변경
      &amp;darr;
Blog 상세 API 다시 요청
      &amp;darr;
Blog 다시 조회
      &amp;darr;
Comment 다시 조회
      &amp;darr;
전체 상태 재설정&lt;/code&gt;&lt;/pre&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;pre class=&quot;&quot;&gt;&lt;code&gt;댓글 변경 API
      &amp;darr;
성공 응답
      &amp;darr;
해당 comments State만 변경
      &amp;darr;
댓글 UI 리렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 API 호출과 DB 조회를 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. Spring Boot Custom Exception&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후에는 Spring의 예외 처리 구조도 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 실패를 처리하기 위해 사용자 정의 예외:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;LoginFailException&lt;/code&gt;&lt;/pre&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;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;.orElseThrow(
    () -&amp;gt; new RuntimeException(
        &quot;로그인 실패&quot;
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. LoginFailException으로 구체화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;.orElseThrow(
    () -&amp;gt; new LoginFailException(
        &quot;로그인 실패&quot;
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;RuntimeException
        &amp;darr;
LoginFailException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 예외 의미가 명확해졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. Service에서 업무 예외 발생&lt;/h1&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;UserResponseDTO response =
    userMapper
        .signIn(request)
        .orElseThrow(
            () -&amp;gt;
                new LoginFailException(
                    &quot;로그인 실패&quot;
                )
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 로그인 사용자를 찾지 못하면 Service가 직접 HTTP 응답을 만드는 것이 아니라 &lt;b&gt;업무 예외를 발생시킨다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. GlobalExceptionHandler&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 예외 Handler를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;는 여러 REST Controller에서 발생한 예외를 한 곳에서 처리할 수 있도록 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. @ExceptionHandler&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@ExceptionHandler(
    LoginFailException.class
)
public ResponseEntity&amp;lt;?&amp;gt; handleLoginFail(
        LoginFailException e
) {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 예외가 발생하면 해당 Handler Method가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;LoginFailException
      &amp;darr;
@ExceptionHandler
      &amp;darr;
handleLoginFail()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. ErrorResponse&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 메시지를 공통 응답 객체로 생성했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;ErrorResponse error =
    new ErrorResponse(
        e.getMessage()
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 응답:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(
            HttpStatus.UNAUTHORIZED
        )
        .body(error);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. 로그인 실패 응답&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger에서 잘못된 로그인 정보를 전송했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Body:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;message&quot;: &quot;로그인 실패&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 출력됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. Global Exception 처리 전체 흐름&lt;/h1&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;GET /users/signIn
      &amp;darr;
UserController
      &amp;darr;
UserService
      &amp;darr;
UserMapper
      &amp;darr;
사용자 없음
      &amp;darr;
LoginFailException
      &amp;darr;
GlobalExceptionHandler
      &amp;darr;
@ExceptionHandler
      &amp;darr;
ErrorResponse
      &amp;darr;
401 Unauthorized&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. 전역 예외 처리의 장점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller마다:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;try {

} catch (...) {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 반복할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
&amp;rarr; 정상 요청 처리에 집중

Service
&amp;rarr; Business Exception 발생

GlobalExceptionHandler
&amp;rarr; HTTP Error Response 담당&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 책임을 분리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. 회원가입 DTO Validation&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 요청에는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Valid
@RequestBody
UserRequestDTO request&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 적용했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;JSON Request
      &amp;darr;
@RequestBody
      &amp;darr;
UserRequestDTO
      &amp;darr;
@Valid
      &amp;darr;
Validation&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. @Email&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Email(
    message =
        &quot;이메일 형식을 지켜주세요!!&quot;
)
private String email;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 Email 형태인지 검사한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. @NotBlank&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@NotBlank(
    message =
        &quot;비밀번호는 필수입력 항목입니다.&quot;
)
private String password;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@NotBlank(
    message =
        &quot;이름은 필수입력 항목입니다.&quot;
)
private String name;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 문자열이나 공백만 입력한 요청을 허용하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;45. @Size&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Password에는:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Size(
    min = 4,
    max = 20
)
private String password;&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Password 최소 길이
&amp;rarr; 4

Password 최대 길이
&amp;rarr; 20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 제한한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;46. Validation Annotation&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 대표적으로 다음 Annotation들을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@NotNull

@NotEmpty

@NotBlank

@Pattern

@Email

@Size&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 데이터 특성에 맞춰 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;47. Password @Pattern&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 정규식을 이용한:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Pattern(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제도 코드에 주석으로 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실습 코드에서는 &lt;code&gt;@NotBlank + @Size&lt;/code&gt;가 실제 적용된 상태이고, &lt;code&gt;@Pattern&lt;/code&gt;은 추가적인 Password 정책을 적용할 수 있는 예제로 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;48. role 필드 준비&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserRequestDTO&lt;/code&gt;에:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;private String role;&lt;/code&gt;&lt;/pre&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;pre class=&quot;lisp&quot;&gt;&lt;code&gt;optional
(spring security role)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 표시되어 있었기 때문에 향후 Spring Security에서:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;USER
ADMIN
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 Role과 연결하기 위한 준비 단계로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 수업에서는 아직 실제 인증/인가 로직까지 연결되지는 않았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;49. DTO와 Lombok&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserRequestDTO&lt;/code&gt;에는 다음 Lombok Annotation이 적용됐다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Builder
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 커리큘럼에 있었던:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Builder
NoArgsConstructor
AllArgsConstructor
DTO 직렬화 이슈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 연결되는 부분이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;50. DTO 생성과 역직렬화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 JSON 요청을:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;JSON
 &amp;darr;
@RequestBody
 &amp;darr;
DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 변환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO를 어떻게 구성하느냐는 Spring/Jackson의 객체 생성 및 직렬화&amp;middot;역직렬화 과정과 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Builder
&amp;rarr; 개발자가 DTO를 편리하게 생성

@NoArgsConstructor
&amp;rarr; 기본 생성자

@AllArgsConstructor
&amp;rarr; 전체 필드를 받는 생성자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 사용하는 구조를 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;51. React Validation Error UI 준비&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서도 Validation 실패 메시지를 사용자에게 표시하기 위한 컴포넌트를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const ErrorMessage = styled.p`
    color: red;
    font-size: 13px;
    margin: 6px 0 0 2px;
`;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;52. Backend Validation &amp;rarr; React UI&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 목표하는 흐름은:&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;SignUpPage
      &amp;darr;
POST /users/signUp
      &amp;darr;
Spring @Valid
      &amp;darr;
Validation 실패
      &amp;darr;
Error Response
      &amp;darr;
Axios catch
      &amp;darr;
Error State
      &amp;darr;
&amp;lt;ErrorMessage&amp;gt;
      &amp;darr;
사용자에게 검증 메시지 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 화면에서는 프론트에 &lt;code&gt;ErrorMessage&lt;/code&gt; 스타일 컴포넌트를 추가하고 Axios Validation Error가 들어오는 흐름까지 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;53. Blog 화면 정상 동작 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Blog 화면도 계속 정상 동작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서는:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;로그인 사용자 환영 문구
글 작성하기
로그아웃
기상예보
OpenAPI

전체 / 개발 / 생활 / 취미 / 일상

첫번째 글&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정상적으로 렌더링됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Console에서도 Blog API 요청이 &lt;code&gt;200&lt;/code&gt;으로 처리되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 전체 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용을 한 번에 정리하면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;React
│
├─ Comment Create
│   &amp;darr;
│   [...comments, newComment]
│
├─ Comment Delete
│   &amp;darr;
│   filter()
│
└─ Comment Update
    &amp;darr;
    map()
        │
        &amp;darr;
Axios
        &amp;darr;
Spring Controller
        &amp;darr;
Service
        &amp;darr;
Mapper
        &amp;darr;
MyBatis
        &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 요청 검증과 오류 처리는:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;React Request
      &amp;darr;
Controller
      &amp;darr;
@Valid
      &amp;darr;
DTO Validation
      &amp;darr;
Service
      &amp;darr;
Custom Exception
      &amp;darr;
@RestControllerAdvice
      &amp;darr;
ErrorResponse
      &amp;darr;
HTTP Error
      &amp;darr;
React Error UI&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 연결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 댓글 하나를 변경할 때 Blog 전체를 다시 조회해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 댓글이 변경될 때마다 상세 데이터를 다시 요청할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;댓글 변경
&amp;rarr; GET Blog 상세
&amp;rarr; Blog SELECT
&amp;rarr; Comment SELECT
&amp;rarr; 전체 상태 재설정&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;React State만 변경했다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;추가
&amp;rarr; Spread

삭제
&amp;rarr; filter()

수정
&amp;rarr; map()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 댓글 영역만 변경되고 React가 다시 렌더링했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 댓글 삭제 후 화면에서도 바로 제거하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Backend&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;DELETE /comments/delete/{id}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Frontend&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;setComments(
    comments.filter(
        c =&amp;gt; c.id !== id
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 삭제된 ID를 React State에서도 제거하면서 서버와 화면의 상태를 맞췄다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 댓글 수정 후 전체 데이터를 다시 받아오지 않기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Backend&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;PATCH /comments/update/{id}/{comment}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Frontend&lt;/h3&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;setComments(ary =&amp;gt;
    ary.map(comment =&amp;gt;
        comment.id === id
            ? {
                ...comment,
                comment: mention
            }
            : comment
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 대상 하나만 새로운 객체로 교체했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. URL에 한글 댓글을 그대로 넣어도 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 수정 내용이 PathVariable에 포함됐다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;/comments/update/{id}/{comment}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글, 공백, 특수문자 때문에 URL 문제가 생길 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;encodeURIComponent(mention)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 로그인 실패 처리 코드가 Service에 섞임&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는:&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;new RuntimeException(&quot;로그인 실패&quot;)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;어떤 종류의 RuntimeException인지 의미가 명확하지 않고, HTTP 응답 처리까지 각 Controller에 분산될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;LoginFailException
+
GlobalExceptionHandler&lt;/code&gt;&lt;/pre&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;pre class=&quot;1c&quot;&gt;&lt;code&gt;LoginFailException
&amp;rarr; 401 Unauthorized
&amp;rarr; {&quot;message&quot;:&quot;로그인 실패&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 통일된 API 오류 응답을 만들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 회원가입 Request 값이 잘못되었을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 Email이나 빈 Password 등이 그대로 Service까지 들어가면 이후 계층에서 불필요한 처리가 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Valid
@RequestBody
UserRequestDTO request&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 DTO Validation을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Email
@NotBlank
@Size&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 통해 Controller 진입 시점에 요청 데이터의 유효성을 검사할 수 있게 됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요했던 부분은 &lt;b&gt;React의 상태와 서버의 데이터를 어떻게 일치시키는가&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 하나를 추가하거나 삭제할 때마다:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서버 전체 재조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 하지 않고:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;추가
&amp;rarr; 배열에 추가

삭제
&amp;rarr; 배열에서 제거

수정
&amp;rarr; 배열의 특정 객체 교체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식으로 React 상태를 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 React의 선언적 UI와 상태 기반 렌더링이 왜 중요한지 더 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  React 부분 리렌더링을 학습하며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 댓글 CRUD를 비교해보니 JavaScript 배열 메서드가 React 상태 관리에서 각각 명확한 역할을 하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Create
&amp;rarr; Spread

Delete
&amp;rarr; filter

Update
&amp;rarr; map&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 기존 배열을 직접 수정하는 것이 아니라 &lt;b&gt;새로운 배열/객체를 생성해 상태를 변경하는 것&lt;/b&gt;이 중요했다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;{
    ...comment,
    comment: mention
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 Spread Operator를 통해 기존 값을 유지하면서 필요한 필드만 수정하는 방식도 반복해서 사용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Spring 계층 분리를 학습하며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 삭제 하나만 해도:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;React
&amp;rarr; Axios
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; Mapper
&amp;rarr; MyBatis
&amp;rarr; DB&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
&amp;rarr; HTTP 책임

Service
&amp;rarr; 업무 처리

Mapper
&amp;rarr; DB 접근

React
&amp;rarr; 화면 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 책임이 명확했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  전역 예외 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;를 사용하면서 예외 처리도 계층화할 수 있다는 점을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Controller마다 실패 응답을 직접 만들었다면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Service
&amp;rarr; 의미 있는 Exception 발생

GlobalExceptionHandler
&amp;rarr; HTTP Error 변환&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;LoginFailException
&amp;rarr; 401 Unauthorized&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 도메인/업무 예외와 HTTP 상태를 명시적으로 연결하는 흐름이 인상적이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Validation과 DTO&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO는 단순한 데이터 저장 객체가 아니라:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Request Data 구조
Validation 정책
JSON 직렬화/역직렬화
계층 사이 데이터 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 담당한다는 점을 다시 확인했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Email
@NotBlank
@Size&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Validation을 DTO 가까이에 선언함으로써 API가 어떤 요청 데이터를 허용하는지 코드로 표현할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 안내에서는 JPA 관계 설정, H2, JUnit까지 예정되어 있었지만 오늘 공유된 실습에서는 &lt;b&gt;React 댓글 부분 리렌더링과 Spring Validation&amp;middot;예외 처리&lt;/b&gt;를 중심으로 진행됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 수업은 Docker를 사용할 예정이라고 들었기 때문에, 지금까지 만든 구조가 컨테이너 환경에서 어떻게 실행되는지 연결해서 보면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복습할 내용:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;React
&amp;rarr; npm 기반 실행환경

Spring Boot
&amp;rarr; Java / JAR 실행환경

MariaDB
&amp;rarr; DB 실행환경

Docker
&amp;rarr; 각각의 실행환경을 Container로 구성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이후에는 다음 주제들도 계속 확인해야겠다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;JPA Entity

@OneToMany
@ManyToOne

User - Blog - Comment 관계

H2 In-Memory DB

JUnit

Transaction Rollback

JWT 실제 검증

Spring Security

Global Exception Handling 확장

Validation ErrorResponse 통일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 React/Spring 연동 구조를 Docker와 JPA에서도 그대로 연결해서 보는 것이 다음 학습의 핵심이 될 것 같다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>exceptionhandling</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mariadb</category>
      <category>mybatis</category>
      <category>react</category>
      <category>restControllerAdvice</category>
      <category>springboot</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/54</guid>
      <comments>https://bbbicsan.tistory.com/54#entry54comment</comments>
      <pubDate>Fri, 4 Sep 2026 18:05:29 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 27일차 TIL : React&amp;middot;Spring Boot 연동과 MyBatis 1:N&amp;middot;Transaction&amp;middot;CORS</title>
      <link>https://bbbicsan.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 27일차 TIL : React&amp;middot;Spring Boot 연동과 MyBatis 1:N&amp;middot;Transaction&amp;middot;CORS&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ndjZn/dJMcaixWnEx/kGhiCnjsHtHaHzEvUvAzVk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ndjZn/dJMcaixWnEx/kGhiCnjsHtHaHzEvUvAzVk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ndjZn/dJMcaixWnEx/kGhiCnjsHtHaHzEvUvAzVk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FndjZn%2FdJMcaixWnEx%2FkGhiCnjsHtHaHzEvUvAzVk%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;React의 Blog&amp;middot;Comment 화면을 Spring Boot API와 실제로 연결하고, MyBatis를 이용해 &lt;b&gt;Blog 1 : N Comment 관계를 조회&amp;middot;조립&lt;/b&gt;했다. 또한 &lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;로 여러 조회를 하나의 논리적 작업 단위로 묶고, Access Token 전달, CORS, Builder 기반 DTO 조립, 댓글 등록 후 React 상태 갱신까지 연결했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Session vs JWT
        &amp;darr;
SPA에서 사용자 상태 유지
        &amp;darr;
React localStorage
user / AT / RT
        &amp;darr;
Blog DB Schema
SPRING_BLOG_TBL
        &amp;darr;
User 1 : N Blog
        &amp;darr;
Blog DTO / Mapper / Service / Controller
        &amp;darr;
React GET /blogs
        &amp;darr;
Blog 목록 조회
        &amp;darr;
React POST /blogs/insert
        &amp;darr;
Blog 등록
        &amp;darr;
Blog 상세조회
GET /blogs/read/{id}
        &amp;darr;
@PathVariable
@RequestHeader Authorization
        &amp;darr;
@Transactional(readOnly = true)
        &amp;darr;
Blog 1건 SELECT
+
Comment N건 SELECT
        &amp;darr;
BlogResponseDTO + comments[]
        &amp;darr;
Builder / toBuilder()
        &amp;darr;
React BlogReadPage
        &amp;darr;
Comment 등록
POST /comments/insert
        &amp;darr;
201 Created
        &amp;darr;
React comments 상태만 부분 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Axios&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;1:N&lt;/li&gt;
&lt;li&gt;Blog&lt;/li&gt;
&lt;li&gt;Comment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Mapper&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PathVariable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestHeader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpStatus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readOnly&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spring Proxy&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;li&gt;Session&lt;/li&gt;
&lt;li&gt;Access Token&lt;/li&gt;
&lt;li&gt;Refresh Token&lt;/li&gt;
&lt;li&gt;localStorage&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Builder&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toBuilder()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DTO&lt;/li&gt;
&lt;li&gt;Sequence&lt;/li&gt;
&lt;li&gt;Foreign Key&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Session과 JWT를 이용한 상태 유지&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업 초반에는 클라이언트의 이전 상태정보를 유지하는 방법으로 &lt;code&gt;Session&lt;/code&gt;과 &lt;code&gt;JWT&lt;/code&gt;를 다시 살펴봤다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client 상태 유지
      &amp;darr;
Session
또는
JWT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session 방식은 서버가 사용자의 상태를 관리하는 방식이고, JWT는 클라이언트가 발급받은 Token을 이후 요청에 함께 전달하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Session
&amp;rarr; 서버가 사용자 상태를 보관
&amp;rarr; Session ID를 기준으로 사용자 식별

JWT
&amp;rarr; Client가 Token 보관
&amp;rarr; 이후 요청 Header에 Token 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 React나 Vue 같은 SPA 환경에서는 로그인 이후 페이지 이동과 API 요청이 계속 이어지기 때문에 Token을 이용한 인증 흐름을 함께 이해할 필요가 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 로그인 후 React에서 상태정보 유지&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 수업에서 로그인 성공 시 다음 값을 &lt;code&gt;localStorage&lt;/code&gt;에 저장했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;localStorage.setItem(
    'user',
    response.data.email
);

localStorage.setItem(
    'at',
    response.headers.get('Authorization')
);

localStorage.setItem(
    'rt',
    response.headers.get('Refresh-Token')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장되는 값은:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;user
&amp;rarr; 로그인 사용자 Email

at
&amp;rarr; Access Token

rt
&amp;rarr; Refresh Token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 Blog 기능에서는 이 값들이 실제로 사용되기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. BlogIndexPage에서 로그인 사용자 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Blog 화면에서는 로그인 사용자 정보를 다음처럼 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user =
    localStorage.getItem('user');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 로그인 화면과 Blog 화면 사이의 사용자 상태를 &lt;code&gt;localStorage&lt;/code&gt;를 이용해 유지한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SignInPage
   &amp;darr;
localStorage.setItem('user', email)
   &amp;darr;
BlogIndexPage
   &amp;darr;
localStorage.getItem('user')&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Blog 카테고리 정의&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 화면에서는 다음 카테고리를 관리했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const CATEGORIES = [
    &quot;전체&quot;,
    &quot;개발&quot;,
    &quot;생활&quot;,
    &quot;취미&quot;,
    &quot;일상&quot;
];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 이후 DB의 &lt;code&gt;CATEGORY&lt;/code&gt; 제약조건과도 동일하게 맞춰졌다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;React Category
        ↕

Database CHECK Constraint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Front와 DB 양쪽에서 같은 도메인 값을 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Blog Table 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에 Blog 테이블을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE SEQUENCE BLOG_SEQ;

CREATE TABLE SPRING_BLOG_TBL (
    ID       INT PRIMARY KEY,
    TITLE    VARCHAR(50) NOT NULL,
    CONTENT  VARCHAR(200) NOT NULL,

    CATEGORY VARCHAR(50)
             DEFAULT '전체'
             CHECK (
                 CATEGORY IN (
                     '전체',
                     '개발',
                     '생활',
                     '취미',
                     '일상'
                 )
             ),

    EMAIL VARCHAR(50)
          REFERENCES SPRING_USER_TBL(EMAIL)
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. Blog Table 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컬럼의 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ID
&amp;rarr; Blog 식별자
&amp;rarr; Sequence 사용

TITLE
&amp;rarr; 제목

CONTENT
&amp;rarr; 본문

CATEGORY
&amp;rarr; Blog 분류
&amp;rarr; DEFAULT '전체'
&amp;rarr; CHECK Constraint

EMAIL
&amp;rarr; 작성 사용자
&amp;rarr; SPRING_USER_TBL.EMAIL 참조&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. User 1 : N Blog 관계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SPRING_BLOG_TBL.EMAIL&lt;/code&gt;은 회원 테이블의 Email을 참조한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SPRING_USER_TBL
EMAIL (PK)
      1
      &amp;darr;
      N
SPRING_BLOG_TBL
EMAIL (FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;회원 한 명
&amp;rarr; Blog 여러 개 작성 가능

Blog 한 개
&amp;rarr; 한 명의 작성자와 연결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;되는 구조다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. Spring Boot Blog 기능 Package&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 기능도 기능 단위로 계층을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;features
└─ blogs
   ├─ ctrl
   │  └─ BlogController
   │
   ├─ domain
   │  └─ dto
   │     ├─ BlogRequestDTO
   │     └─ BlogResponseDTO
   │
   ├─ repository
   │  └─ BlogMapper
   │
   └─ service
      └─ BlogService&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 MyBatis XML은:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;resources
└─ mappers
   └─ blogMapper.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 위치한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. BlogMapper - 전체 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper Interface에는 전체 Blog 조회 메서드를 정의했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Mapper
public interface BlogMapper {

    public List&amp;lt;BlogResponseDTO&amp;gt; findByAll();

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogMapper.findByAll()
        &amp;darr;
blogMapper.xml
        &amp;darr;
SELECT
        &amp;darr;
List&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. BlogService - 목록 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서는 Mapper를 생성자 주입받는다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BlogService {

    private final BlogMapper blogMapper;

    public List&amp;lt;BlogResponseDTO&amp;gt; list() {

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog service list&quot;
        );

        return blogMapper.findByAll();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다시:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
+
final BlogMapper&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용한 생성자 주입을 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. Blog 목록 전체 요청 흐름&lt;/h1&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;React BlogIndexPage
      &amp;darr;
GET /blogs
      &amp;darr;
BlogController
      &amp;darr;
BlogService.list()
      &amp;darr;
BlogMapper.findByAll()
      &amp;darr;
blogMapper.xml
      &amp;darr;
SPRING_BLOG_TBL
      &amp;darr;
List&amp;lt;BlogResponseDTO&amp;gt;
      &amp;darr;
JSON
      &amp;darr;
React setBlogs()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. React에서 Blog 목록 요청&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 기존 json-server 코드와 동일하게 &lt;code&gt;/blogs&lt;/code&gt;를 호출했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await api.get('/blogs')
    .then(response =&amp;gt; {

        console.log(
            'debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; axios request success',
            response
        );

        if (response.status === 200) {
            setBlogs(response.data);
        }
    })
    .catch(error =&amp;gt; {

        console.log(
            'debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; axios request error',
            error
        );
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 데이터는 React 상태에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;response.data
      &amp;darr;
setBlogs()
      &amp;darr;
React Rendering&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. Blog 데이터가 없을 때 화면 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Blog 테이블에는 데이터가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 화면에서도:&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;등록된 글이 없습니다!&lt;/code&gt;&lt;/pre&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;pre class=&quot;haskell&quot;&gt;&lt;code&gt;GET /blogs
&amp;rarr; 요청 성공

HTTP 200
&amp;rarr; 정상

response.data
&amp;rarr; []

UI
&amp;rarr; 등록된 글이 없습니다!&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. Blog 목록 ResponseEntity&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는 목록을 다음과 같이 반환했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(HttpStatus.OK)
        .body(list);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 중에는:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;데이터 없음
&amp;rarr; 204 NO_CONTENT

데이터 있음
&amp;rarr; 200 OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 분기하는 방식도 살펴봤지만, 현재 구현에서는 빈 List라도 &lt;code&gt;200 OK&lt;/code&gt;와 함께 반환하는 형태를 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. BlogMapper 등록 기능 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 등록 기능도 Mapper에 추가했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Mapper
public interface BlogMapper {

    public List&amp;lt;BlogResponseDTO&amp;gt; findByAll();

    public int save(
        BlogRequestDTO request
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회와 등록의 반환형 차이는:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;findByAll()
&amp;rarr; SELECT
&amp;rarr; List&amp;lt;BlogResponseDTO&amp;gt;

save()
&amp;rarr; INSERT
&amp;rarr; 영향받은 Row 수
&amp;rarr; int&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. Blog 등록 React 요청&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogWritePage&lt;/code&gt;에서는 다음 데이터를 Spring Boot에 전송했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;await api.post('/blogs/insert', {
    title,
    content,
    category,
    email: user
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 JSON은:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;title&quot;: &quot;첫번째 글&quot;,
  &quot;content&quot;: &quot;우와~ 성공히팅!!ㅎㅎ&quot;,
  &quot;category&quot;: &quot;일상&quot;,
  &quot;email&quot;: &quot;jslim9413@gmail.com&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. Blog 작성자 Email&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;email&lt;/code&gt;은 직접 입력받지 않고 로그인 사용자를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user =
    localStorage.getItem('user');&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;email: user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;로그인
&amp;rarr; user Email localStorage 저장
        &amp;darr;
BlogWritePage
        &amp;darr;
Blog 작성자 Email로 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. BlogRequestDTO 바인딩&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Spring Boot 로그에서도 React가 보낸 데이터가 DTO로 들어오는 것을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;BlogRequestDTO(
    title=첫번째 글,
    content=우와~ 성공히팅!!ㅎㅎ,
    category=일상,
    email=jslim9413@gmail.com
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;React JSON
      &amp;darr;
@RequestBody
      &amp;darr;
BlogRequestDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 정상 동작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. BlogController 등록 API&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@PostMapping(&quot;/insert&quot;)
public ResponseEntity&amp;lt;?&amp;gt; insert(
        @RequestBody
        BlogRequestDTO request
) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog controller insert&quot;
    );

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog controller insert params : &quot;
        + request
    );

    int flag =
        blogService.insert(request);

    return flag != 0
        ? ResponseEntity
            .status(HttpStatus.CREATED)
            .build()

        : ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. Blog 등록 Response&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;INSERT 성공
&amp;rarr; flag != 0
&amp;rarr; 201 CREATED

INSERT 실패
&amp;rarr; flag == 0
&amp;rarr; 400 BAD_REQUEST&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API의 실행 결과를 HTTP Status로 표현하는 구조를 다시 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. Blog 등록 성공 후 React 이동&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;if (response.status === 201) {
    moveUrl('/blogs/index');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 작성 성공 후 목록 화면으로 이동한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;POST /blogs/insert
      &amp;darr;
201 CREATED
      &amp;darr;
/blogs/index&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. Sequence 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog ID를 만들기 위해 Sequence도 다시 복습했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE SEQUENCE SEQ_TEST;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 값:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT NEXTVAL(SEQ_TEST);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 값:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SELECT LASTVAL(SEQ_TEST);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 옵션:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE SEQUENCE SEQ_TEST
START WITH 1000
INCREMENT BY 2
MAXVALUE 1005;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 다시 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Blog 상세보기와 1:N 관계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 핵심 주제는 Blog 상세조회였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 개의 Blog에는 여러 Comment가 존재할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Blog
  1
  &amp;darr;
  N
Comment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 상세 페이지에서는:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Blog 1건
+
Comment N건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 함께 내려줘야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. 기존 json-server의 1:N 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json-server에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;await api.get(
    `/blogs/${id}?_embed=comments`
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 &lt;code&gt;_embed=comments&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답은 개념적으로:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;...&quot;,
  &quot;comments&quot;: [
    {},
    {},
    {}
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 Blog 객체 안에 comments 배열이 포함된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. json-server 응답을 React State에 저장&lt;/h1&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;if (response.status === 200) {

    setBlog(response.data);

    setComments(
        response.data.comments
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;response.data
&amp;rarr; Blog State

response.data.comments
&amp;rarr; Comments State&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 나눈다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. Spring Boot 상세조회 API로 변경&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 json-server 요청을 Spring Boot endpoint로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;await api.get(
    `/blogs/read/${id}`,
    {
        headers: {
            Authorization:
                at ? at : &quot;&quot;
        }
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. Access Token을 후속 요청에 사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인할 때 저장했던 Access Token:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;localStorage.setItem(
    'at',
    response.headers.get('Authorization')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 Blog 상세조회 요청에 넣는다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Login
   &amp;darr;
AT 발급
   &amp;darr;
localStorage
   &amp;darr;
BlogReadPage
   &amp;darr;
Authorization Header
   &amp;darr;
Spring Boot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 저장용 데이터가 아니라 이후 인증 요청에 사용되는 값이라는 점을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. BlogController 상세조회 Endpoint&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/read/{id}&quot;)
public ResponseEntity&amp;lt;?&amp;gt; read(

        @PathVariable(&quot;id&quot;)
        Integer id,

        @RequestHeader(&quot;Authorization&quot;)
        String at
) {

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 파라미터 바인딩이 동시에 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/blogs/read/1
       &amp;darr;
@PathVariable
       &amp;darr;
id = 1&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Authorization: Bearer ...
       &amp;darr;
@RequestHeader
       &amp;darr;
at&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. Blog 단건 MyBatis 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;blogMapper.xml&lt;/code&gt;에는 다음 쿼리가 추가됐다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findById&quot;
        parameterType=&quot;java.lang.Integer&quot;
        resultType=&quot;com.example.inspire_mybatis.features.blogs.domain.dto.BlogResponseDTO&quot;&amp;gt;

    SELECT
        ID,
        TITLE,
        CONTENT,
        CATEGORY,
        EMAIL
    FROM
        SPRING_BLOG_TBL
    WHERE
        ID = #{id}

&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. CommentMapper - Blog별 댓글 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 Mapper에서는 Blog ID를 기준으로 댓글 목록을 조회한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findByBlogId&quot;
        parameterType=&quot;java.lang.Integer&quot;
        resultType=&quot;com.example.inspire_mybatis.features.comments.domain.dto.CommentResponseDTO&quot;&amp;gt;

    SELECT
        ID,
        COMMENT,
        EMAIL,
        BLOGID
    FROM
        SPRING_COMMENT_TBL
    WHERE
        BLOGID = #{blogId}
    ORDER BY
        ID DESC

&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Blog ID = 1
       &amp;darr;
SPRING_COMMENT_TBL
       &amp;darr;
BLOGID = 1
       &amp;darr;
Comment 여러 건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 가져온다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. MyBatis 1:N 관계 조립&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 하나의 JOIN 결과를 자동 관계 매핑하는 방식이 아니라:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SELECT Blog 1건
+
SELECT Comments N건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 각각 실행한 뒤 Service에서 하나의 DTO로 조립했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;BlogMapper.findById()
      &amp;darr;
BlogResponseDTO
      &amp;darr;

CommentMapper.findByBlogId()
      &amp;darr;
List&amp;lt;CommentResponseDTO&amp;gt;
      &amp;darr;

BlogResponseDTO.comments에 조립&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. @Transactional - 논리적인 작업의 단위&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세조회 Service에는:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 적용했다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public BlogResponseDTO read(
        Integer id
) {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 상세조회 내부에서는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Blog SELECT
+
Comment SELECT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번의 DB 작업이 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용자의 관점에서는:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;게시글 상세보기&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 하나의 업무다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 전체를 하나의 &lt;b&gt;논리적인 작업 단위(Transaction)&lt;/b&gt; 로 묶는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. Transaction을 논리적 업무로 이해하기&lt;/h1&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;게시글 상세조회 시작
        &amp;darr;
Blog SELECT
        &amp;darr;
Comment SELECT
        &amp;darr;
DTO 조립
        &amp;darr;
상세조회 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 SQL이 존재해도 하나의 Service 업무라면 하나의 Transaction으로 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 SQL 수업에서 배운 단순한 &lt;code&gt;COMMIT / ROLLBACK&lt;/code&gt; 개념이 Spring의 Service 계층으로 연결되기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. readOnly = true&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 현재 메서드가 조회 중심 Transaction이라는 것을 명시한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;readOnly = true
&amp;rarr; 조회 작업

INSERT
UPDATE
DELETE
&amp;rarr; 이 Transaction의 목적과 맞지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서도 이 영역에서 Update 같은 쓰기 작업을 수행하는 상황과 구분해서 설명했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. Spring Transaction과 Proxy&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 커리큘럼에서 Transaction은 &lt;code&gt;Proxy Transactional&lt;/code&gt;이라는 관점으로도 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 &lt;code&gt;@Transactional&lt;/code&gt;이 붙은 메서드를 기준으로 트랜잭션 경계를 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client / Controller
       &amp;darr;
Spring Proxy
       &amp;darr;
Transaction Start
       &amp;darr;
Service Method
       &amp;darr;
DB 작업
       &amp;darr;
Transaction 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습에서는 &lt;code&gt;BlogService.read()&lt;/code&gt;라는 &lt;b&gt;Service의 논리적 작업 단위&lt;/b&gt;를 Transaction 범위로 잡았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. Blog 상세조회 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서는 먼저 Blog를 찾는다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;BlogResponseDTO blog =
    blogMapper
        .findById(id)
        .orElseThrow(
            () -&amp;gt; new RuntimeException(
                id + &quot; BLOG NOT FOUND&quot;
            )
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Blog가 존재하지 않으면 예외를 발생시킨다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. 댓글 목록 조립 - 첫 번째 방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blog.setComments(
    commentMapper.findByBlogId(
        blog.getId()
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 기존 DTO의 상태를 직접 변경했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;BlogResponseDTO
       &amp;darr;
setComments()
       &amp;darr;
기존 객체 변경&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. Builder를 이용한 조립 방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다음 방식으로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return blog.toBuilder()
        .comments(
            commentMapper.findByBlogId(
                blog.getId()
            )
        )
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;기존 BlogResponseDTO
        &amp;darr;
toBuilder()
        &amp;darr;
comments 추가
        &amp;darr;
build()
        &amp;darr;
새 BlogResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 객체를 직접 수정하지 않고 &lt;b&gt;새로운 응답 DTO를 조립하는 방식&lt;/b&gt;으로 바뀌었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. Builder와 DTO 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 커리큘럼에는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Builder
@NoArgsConstructor
@AllArgsConstructor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 DTO 생성 관련 내용도 포함됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습에서는 특히 &lt;code&gt;toBuilder()&lt;/code&gt;를 이용해 기존 Blog 데이터를 유지하면서 &lt;code&gt;comments&lt;/code&gt;만 추가하는 방식이 실제로 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Blog 정보 유지
+
Comments 추가
        &amp;darr;
새 DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO를 어떻게 생성하고 조립하는지가 JSON 응답 구조와 직접 연결되기 때문에 DTO 생성 방식의 중요성을 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. Blog 상세조회 최종 Service&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 로직은 다음 흐름이었다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public BlogResponseDTO read(
        Integer id
) {

    BlogResponseDTO blog =
        blogMapper
            .findById(id)
            .orElseThrow(
                () -&amp;gt; new RuntimeException(
                    id + &quot; BLOG NOT FOUND&quot;
                )
            );

    return blog.toBuilder()
            .comments(
                commentMapper.findByBlogId(
                    blog.getId()
                )
            )
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. 1:N Response DTO&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 JSON은 개념적으로 다음 구조가 된다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;첫번째 글&quot;,
  &quot;content&quot;: &quot;우와~ 성공이당 ㅎㅎ&quot;,
  &quot;category&quot;: &quot;일상&quot;,
  &quot;email&quot;: &quot;jslim9413@gmail.com&quot;,
  &quot;comments&quot;: [
    {
      &quot;id&quot;: 2,
      &quot;comment&quot;: &quot;마무리&quot;,
      &quot;email&quot;: &quot;jslim9413@gmail.com&quot;,
      &quot;blogId&quot;: 1
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogResponseDTO
└─ comments : List&amp;lt;CommentResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 1:N 응답 구조가 완성된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. Blog 상세조회 실행 결과&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그에서도:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;blog controller read
blog controller read access token : Bearer ...
blog controller read params : 1

blog service read
blog service read params : 1&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;BlogResponseDTO(
    id=1,
    title=첫번째 글,
    ...
    comments=[]
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 확인됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 댓글 데이터가 존재하면 &lt;code&gt;comments&lt;/code&gt; 배열에 댓글이 포함됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. 상세조회 HTTP 상태 분기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;return response != null
        ? ResponseEntity
            .status(HttpStatus.OK)
            .body(response)

        : ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 반환했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Blog 존재
&amp;rarr; 200 OK

Blog 없음
&amp;rarr; 404 NOT_FOUND&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. Comment 기능 Package&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 기능도 Blog와 같은 계층 구조를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;features
└─ comments
   ├─ ctrl
   │  └─ CommentController
   │
   ├─ domain
   │  └─ dto
   │     ├─ CommentRequestDTO
   │     └─ CommentResponseDTO
   │
   ├─ repository
   │  └─ CommentMapper
   │
   └─ service
      └─ CommentService&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;45. CommentController 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 골격부터 작성했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/comments&quot;)
public class CommentController {

    @PostMapping(&quot;/insert&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; insert() {
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 DTO와 Service가 연결됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;46. React 댓글 등록&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 json-server에서는:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await api.post('/comments', {
    blogId: Number(blogId),
    comment,
    email
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 연동에서는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;await api.post(
    '/comments/insert',
    {
        blogId: Number(blogId),
        comment,
        email
    },
    {
        headers: {
            Authorization:
                at ? at : &quot;&quot;
        }
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 변경했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;47. 댓글 등록 요청 구조&lt;/h1&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;POST /comments/insert

Headers
Authorization: Bearer ...

Body
{
    blogId,
    comment,
    email
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Blog 상세조회뿐 아니라 댓글 작성에도 Access Token을 같이 보내기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;48. CommentService&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service도 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 골격:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Service
public class CommentService {

    public int insert(
            CommentRequestDTO request
    ) {

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; comment service insert&quot;
        );

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; comment service insert params : &quot;
            + request
        );

        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;를 적용하며 Mapper 주입 구조로 확장됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;49. 댓글 등록 Controller &amp;rarr; Service&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;int id =
    commentService.insert(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 댓글 INSERT 결과로 단순 영향 행 수가 아니라 &lt;b&gt;새로 생성된 댓글 ID를 받아 사용하는 방향&lt;/b&gt;으로 구현했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;50. Comment Insert 성공 응답&lt;/h1&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;return id != 0
    ? ResponseEntity
        .status(HttpStatus.CREATED)
        .body(
            CommentRequestDTO
                .builder()
                .blogId(
                    request.getBlogId()
                )
                .email(
                    request.getEmail()
                )
                .comment(
                    request.getComment()
                )
                .id(id)
                .build()
        )

    : ResponseEntity
        .status(
            HttpStatus.BAD_REQUEST
        )
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;51. 새 댓글 ID를 Response에 포함&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 등록 성공 시:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;201 CREATED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐 아니라 생성된 댓글의 ID까지 포함한 객체를 Body로 반환한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: 2,
  &quot;blogId&quot;: 1,
  &quot;email&quot;: &quot;jslim9413@gmail.com&quot;,
  &quot;comment&quot;: &quot;마무리&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 React가 추가 요청 없이 바로 새 댓글 객체를 상태에 넣을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;52. MyBatis selectKey&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 INSERT에서는 MyBatis의 &lt;code&gt;&amp;lt;selectKey&amp;gt;&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;insert id=&quot;save&quot;
        parameterType=&quot;com.example.inspire_mybatis.features.comments.domain.dto.CommentRequestDTO&quot;&amp;gt;

    &amp;lt;selectKey
        keyProperty=&quot;id&quot;
        resultType=&quot;int&quot;
        order=&quot;BEFORE&quot;&amp;gt;

        SELECT NEXTVAL(COMMENT_SEQ)

    &amp;lt;/selectKey&amp;gt;

    INSERT INTO SPRING_COMMENT_TBL
    VALUES(
        #{id},
        #{comment},
        #{email},
        #{blogId}
    )

&amp;lt;/insert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;53. selectKey 동작 흐름&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Comment Insert 요청
        &amp;darr;
SELECT NEXTVAL(COMMENT_SEQ)
        &amp;darr;
새 ID 생성
        &amp;darr;
request.id에 저장
        &amp;darr;
INSERT
        &amp;darr;
생성된 ID를 Java에서도 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Sequence로 생성한 PK 값을 INSERT 전 Java DTO에 다시 넣을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;54. order=&quot;BEFORE&quot;&lt;/h1&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;order=&quot;BEFORE&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 INSERT보다 먼저 Sequence를 조회한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. NEXTVAL
2. DTO.id 설정
3. INSERT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;55. React 댓글 상태 부분 갱신&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 등록 성공 후:&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;if (response.status === 201) {

    setComments(ary =&amp;gt; {
        return [
            ...ary,
            response.data
        ];
    });

    setComment('');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;56. 전체 재조회 대신 부분 업데이트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 하나를 추가한 뒤 Blog 전체를 다시 조회하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;기존 comments[]
       +
새 response.data
       &amp;darr;
새 comments[]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[...ary, response.data]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 새 댓글만 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 상태 변화에 따라 해당 영역만 다시 렌더링된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;57. 댓글 입력 초기화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 성공 후:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;setComment('');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 입력창을 비운다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;댓글 작성
&amp;rarr; POST 성공
&amp;rarr; comments 상태 추가
&amp;rarr; input 초기화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 UI 흐름도 자연스럽게 연결됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;58. 실제 댓글 등록 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 화면에서 댓글 &lt;code&gt;&quot;마무리&quot;&lt;/code&gt;를 작성한 뒤 실제로 댓글 목록에 추가되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome DevTools의 Axios 응답에서도:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;status: 201&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 함께 새 Comment 데이터가 반환되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;React
&amp;rarr; Spring Controller
&amp;rarr; Comment Service
&amp;rarr; Mapper
&amp;rarr; DB INSERT
&amp;rarr; Response
&amp;rarr; React State
&amp;rarr; 화면 리렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름이 정상 작동했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;59. WebConfig를 이용한 CORS&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React와 Spring Boot는 각각:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;React
localhost:3000

Spring Boot
localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Port가 다르므로 다른 Origin이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기존 &lt;code&gt;CorsConfig&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig
        implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(
            CorsRegistry registry
    ) {

        registry
            .addMapping(&quot;/**&quot;)
            .allowedHeaders(&quot;*&quot;)
            .allowedOriginPatterns(
                &quot;http://localhost:3000&quot;
            )
            .allowedMethods(
                &quot;GET&quot;,
                &quot;POST&quot;,
                &quot;DELETE&quot;,
                &quot;PUT&quot;,
                &quot;PATCH&quot;,
                &quot;OPTIONS&quot;
            );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog와 Comment API에도 같은 설정이 적용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 완성한 전체 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 구현한 내용을 하나로 연결하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;React
│
├─ BlogIndexPage
│   └─ GET /blogs
│
├─ BlogWritePage
│   └─ POST /blogs/insert
│
└─ BlogReadPage
    ├─ GET /blogs/read/{id}
    │      + Authorization
    │
    └─ POST /comments/insert
           + Authorization
           &amp;darr;
Spring Boot
           &amp;darr;
Controller
           &amp;darr;
Service
           &amp;darr;
@Transactional
           &amp;darr;
Mapper
           &amp;darr;
MyBatis XML
           &amp;darr;
MariaDB
           &amp;darr;
Blog 1 : N Comment
           &amp;darr;
Response DTO
           &amp;darr;
JSON
           &amp;darr;
React State&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. json-server의 &lt;code&gt;_embed&lt;/code&gt;를 Spring Boot로 어떻게 바꿀까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /blogs/1?_embed=comments&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json-server가 자동으로 Blog와 Comment를 묶어줬다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot에서는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;BlogMapper.findById()
+
CommentMapper.findByBlogId()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행하고 Service에서 조립했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;BlogResponseDTO blog =
    blogMapper.findById(id)
        .orElseThrow(...);

return blog.toBuilder()
        .comments(
            commentMapper.findByBlogId(
                blog.getId()
            )
        )
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring/MyBatis에서는 &lt;b&gt;관계 데이터의 조회와 응답 DTO 조립 책임을 직접 설계해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 여러 SELECT는 어디까지 하나의 Transaction인가?&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;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Blog SELECT
Comment SELECT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번의 DB 요청이 필요했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 Service 메서드에 적용했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transaction은 SQL 한 줄이 아니라 &lt;b&gt;업무 관점의 논리적인 작업 단위&lt;/b&gt;로 보는 것이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;게시글 상세보기
=
Blog 조회
+
Comment 조회
+
응답 조립&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기존 DTO를 직접 수정할 것인가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식&lt;/h3&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;blog.setComments(...);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blog.toBuilder()
    .comments(...)
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 DTO를 직접 변경하기보다 Builder로 새로운 응답 객체를 만들어 반환할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 학습 주제였던 Builder와 DTO 구성 문제가 실제 1:N 응답 조립에서 사용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 로그인 후 Token은 어디에 사용하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 로그인 성공 후 AT를 저장하기만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;headers: {
    Authorization: at ? at : &quot;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 Blog 상세조회와 Comment 작성 요청에 실제로 Token을 전달했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Login
&amp;rarr; Token 발급
&amp;rarr; localStorage
&amp;rarr; 이후 API Authorization Header&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JWT가 실제 후속 요청과 연결되기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 댓글 등록 후 Blog 전체를 다시 조회할 필요가 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 Blog 상세를 다시 요청할 수도 있지만 오늘은:&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;setComments(ary =&amp;gt; [
    ...ary,
    response.data
]);&lt;/code&gt;&lt;/pre&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;pre class=&quot;excel&quot;&gt;&lt;code&gt;DB INSERT
&amp;rarr; 새 댓글 Response
&amp;rarr; 기존 comments 배열에 추가
&amp;rarr; 부분 리렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 전체 재조회 없이 UI 상태를 갱신할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 지금까지 따로 배웠던 개념들이 실제 Blog 기능 안에서 하나로 연결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Database Relationship
MyBatis
Transaction
DTO
JWT
CORS
React State&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 하나의 Blog 상세화면 안에서 모두 사용됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog와 Comment의 1:N 관계를 보면서 DB의 관계형 모델이 단순히 테이블 설계에서 끝나는 것이 아니라:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;SQL 조회
&amp;rarr; Java DTO
&amp;rarr; JSON
&amp;rarr; React State&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 계속 영향을 준다는 점이 명확해졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요한 개념은 &lt;b&gt;&amp;ldquo;하나의 사용자 기능은 여러 계층과 여러 DB 작업의 조합으로 완성된다&amp;rdquo;&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;하지만 내부에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Blog ID 전달
      &amp;darr;
Authorization Token 전달
      &amp;darr;
Controller
      &amp;darr;
Service
      &amp;darr;
Blog 조회
      &amp;darr;
Comment 조회
      &amp;darr;
DTO 조립
      &amp;darr;
JSON 직렬화
      &amp;darr;
React 상태 저장
      &amp;darr;
화면 렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 많은 과정이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;@Transactional&lt;/code&gt;에서 말하는 &lt;b&gt;논리적인 작업 단위&lt;/b&gt;가 왜 필요한지도 이전보다 훨씬 이해하기 쉬웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  MyBatis 1:N을 구현하며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json-server에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;?_embed=comments&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄이면 관계 데이터가 자동으로 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Boot에서는 직접:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;BlogMapper
+
CommentMapper
+
Service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 Blog와 댓글을 조립해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 코드가 더 많아 보이지만, 오히려 어떤 데이터가 어디에서 만들어지고 어떤 책임을 갖는지가 더 명확해졌다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Mapper
&amp;rarr; DB 조회

Service
&amp;rarr; 업무 흐름 및 관계 데이터 조립

Controller
&amp;rarr; HTTP 요청/응답

React
&amp;rarr; 화면 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 각 계층의 역할이 분명했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  DTO와 Builder&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 &lt;code&gt;toBuilder()&lt;/code&gt; 방식이 특히 인상적이었다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return blog.toBuilder()
        .comments(...)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 객체에 Setter로 값을 추가하는 것과 달리, 기존 값을 기반으로 새로운 응답 객체를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO는 단순히 데이터를 담는 클래스라고만 생각했지만, 오늘은 &lt;b&gt;API가 어떤 JSON 구조를 만들어낼지를 결정하는 중요한 계약 객체&lt;/b&gt;라는 점을 다시 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 커리큘럼에 포함된 &lt;code&gt;Builder&lt;/code&gt;, &lt;code&gt;NoArgsConstructor&lt;/code&gt;, &lt;code&gt;AllArgsConstructor&lt;/code&gt;, DTO 직렬화 이슈도 결국 Spring이 DTO를 생성하고 JSON으로 변환하는 과정과 연결해서 복습해야겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Blog와 Comment는 조회와 작성까지 실제 React와 연결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 다음 흐름을 집중해서 살펴보면 좋을 것 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Blog Update / Delete

Comment Update / Delete

@Transactional
&amp;rarr; 쓰기 Transaction
&amp;rarr; Rollback

Exception Handling

JWT 실제 검증

Access Token 만료

Refresh Token 재발급

Spring Security

작성자 권한 검사

DTO Validation

Serialization / Deserialization

1:N MyBatis
&amp;rarr; ResultMap / Collection Mapping 방식 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘 사용한:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Blog SELECT
+
Comment SELECT
+
Service에서 조립&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식과 MyBatis의 &lt;code&gt;resultMap&lt;/code&gt;, &lt;code&gt;collection&lt;/code&gt;을 이용한 관계 매핑 방식의 차이도 이후 비교해보면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>axios</category>
      <category>CORS</category>
      <category>JWT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mariadb</category>
      <category>mybatis</category>
      <category>react</category>
      <category>springboot</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/53</guid>
      <comments>https://bbbicsan.tistory.com/53#entry53comment</comments>
      <pubDate>Thu, 3 Sep 2026 17:32:51 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 26일차 TIL : Spring Boot&amp;middot;MyBatis&amp;middot;Swagger&amp;middot;JWT&amp;middot;React 연동</title>
      <link>https://bbbicsan.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 26일차 TIL : Spring Boot&amp;middot;MyBatis&amp;middot;Swagger&amp;middot;JWT&amp;middot;React 연동&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnizbq/dJMcabrXQMf/krivxYwspkAyt4V63vycFk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnizbq/dJMcabrXQMf/krivxYwspkAyt4V63vycFk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnizbq/dJMcabrXQMf/krivxYwspkAyt4V63vycFk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnizbq%2FdJMcabrXQMf%2FkrivxYwspkAyt4V63vycFk%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;기존 Spring Boot + MyBatis 회원 기능을 새로운 프로젝트 구조로 옮기고, &lt;code&gt;.env&lt;/code&gt; 기반 환경설정, Swagger/OpenAPI 문서화, 회원가입/로그인 API, Service/Mapper 계층, JWT 형태의 토큰 응답, CORS 설정, React Axios 연동까지 연결하며 &lt;b&gt;프론트엔드와 백엔드가 실제로 통신하는 전체 흐름&lt;/b&gt;을 구현했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot + MyBatis 복습
        &amp;darr;
Mapper namespace / id
        &amp;darr;
Browser &amp;rarr; Backend &amp;rarr; DB &amp;rarr; JSON
        &amp;darr;
Spring DI
plain / encryption Bean 교체
        &amp;darr;
로그인 성공/실패 ResponseEntity
        &amp;darr;
Optional 로그인 테스트
        &amp;darr;
새 inspire_mybatis 프로젝트 생성
        &amp;darr;
.env 환경변수 로딩
        &amp;darr;
MyBatis XML package 경로 오류
        &amp;darr;
패키지 정합성 수정
        &amp;darr;
Spring Boot 정상 기동
        &amp;darr;
Swagger / OpenAPI
        &amp;darr;
@Tag
@Operation
@ApiResponses
@RequestBody / Schema
        &amp;darr;
회원가입 계층 구현
Controller &amp;rarr; Service &amp;rarr; Mapper
        &amp;darr;
JWT Provider
Access Token / Refresh Token
        &amp;darr;
회원가입 중복 PK 오류
        &amp;darr;
로그인 Header 구성
Authorization / Refresh-Token
        &amp;darr;
@RequestParam QueryString 로그인
        &amp;darr;
React SignUp / SignIn
        &amp;darr;
Axios baseURL + .env
        &amp;darr;
CORS 설정
        &amp;darr;
React &amp;rarr; Spring Boot 실제 요청
        &amp;darr;
로그인 성공
        &amp;darr;
localStorage에 user / AT / RT 저장&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;Mapper XML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;namespace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parameterType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resultType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spring DI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Resource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Optional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;dotenv&lt;/li&gt;
&lt;li&gt;Swagger&lt;/li&gt;
&lt;li&gt;OpenAPI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Tag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Operation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ApiResponse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpHeaders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;li&gt;Access Token&lt;/li&gt;
&lt;li&gt;Refresh Token&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;Axios&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;localStorage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. MyBatis Mapper 연결 구조 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 먼저 기존 MyBatis XML과 Java Mapper의 연결 구조를 다시 확인했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;com.example.testcase.features.users.repository.UserMapper&quot;&amp;gt;

    &amp;lt;insert id=&quot;save&quot;
            parameterType=&quot;com.example.testcase.features.users.domain.dto.UserRequestDTO&quot;&amp;gt;

        INSERT INTO SPRING_USER_TBL(
            EMAIL,
            PASSWORD,
            NAME
        )
        VALUES(
            #{email},
            #{password},
            #{name}
        )

    &amp;lt;/insert&amp;gt;

&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 연결은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Java Mapper Interface
UserMapper
       ↕
XML namespace

Java Method
save()
       ↕
XML statement id=&quot;save&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 MyBatis에서는 Java Mapper 메서드명과 XML의 &lt;code&gt;id&lt;/code&gt;가 일치해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. MyBatis의 parameter binding&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;VALUES(
    #{email},
    #{password},
    #{name}
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값들은 DTO의 property와 연결된다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;#{email}
&amp;rarr; getEmail()

#{password}
&amp;rarr; getPassword()

#{name}
&amp;rarr; getName()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 JDBC에서 직접 작성했던:&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;pstmt.setString(...);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부분을 MyBatis가 대신 처리해주는 구조다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. 요청 처리 전체 흐름 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 프론트와 백엔드, DB의 관계도 다시 연결했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser / React
      &amp;darr;
HTTP Request
      &amp;darr;
Spring Boot Backend
      &amp;darr;
Controller
      &amp;darr;
Service
      &amp;darr;
Mapper
      &amp;darr;
MyBatis
      &amp;darr;
Database
      &amp;darr;
DTO
      &amp;darr;
JSON Response
      &amp;darr;
Browser / React&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각각의 개념을 따로 보는 것이 아니라 하나의 요청 사슬로 보기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. 기존 Java 생성자 주입과 Spring DI 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Java 콘솔 Blog 프로젝트에서는 직접 생성자를 통해 의존성을 전달했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class DeleteController {

    private BlogReactService service;

    public DeleteController(BlogReactService service) {
        this.service = service;
    }

    public int delete(int blogId) {
        return service.delete(blogId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;객체를 직접 생성
      &amp;darr;
생성자로 Dependency 전달
      &amp;darr;
this.service = service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 이 역할을 Container가 대신 수행한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Java 직접 연결
&amp;rarr; 개발자가 객체 생성/전달

Spring DI
&amp;rarr; Spring Container가 Bean 생성/주입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 배운 객체지향의 &lt;b&gt;느슨한 결합, 인터페이스, 다형성&lt;/b&gt;이 Spring DI에서 실제로 활용된다는 점을 다시 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Bean 이름으로 Service 구현체 교체&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Resource(name = &quot;plain&quot;)
private UserService userService;&lt;/code&gt;&lt;/pre&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;pre class=&quot;css&quot;&gt;&lt;code&gt;@Resource(name = &quot;encryption&quot;)
private UserService userService;&lt;/code&gt;&lt;/pre&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;pre class=&quot;nginx&quot;&gt;&lt;code&gt;UserService
├─ plain
└─ encryption&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 여전히:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;userService.signIn(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구체 구현체를 직접 참조하지 않고 Bean 이름만 바꿔 실행 구현을 교체할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Resource(name=&quot;plain&quot;)
&amp;rarr; Plain 구현체

@Resource(name=&quot;encryption&quot;)
&amp;rarr; Encryption 구현체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 인터페이스 기반 다형성과 DI의 장점이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 로그인 성공/실패 ResponseEntity 분기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 Service 호출 결과에 따라 HTTP 응답을 다르게 반환하는 구조도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;UserResponseDTO response = userService.signIn(request);

if (response != null) {

    return ResponseEntity
            .status(HttpStatus.OK)
            .header(&quot;access-token&quot;, &quot;12345&quot;)
            .body(response);

} else {

    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;로그인 성공
&amp;rarr; response != null
&amp;rarr; 200 OK
&amp;rarr; Header
&amp;rarr; UserResponseDTO Body

로그인 실패
&amp;rarr; response == null
&amp;rarr; 404 NOT_FOUND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 코드는 더 발전했지만, 이 단계에서 &lt;b&gt;Service 결과에 따라 Controller가 HTTP 응답을 설계&lt;/b&gt;한다는 점을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. Optional을 이용한 로그인 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 Mapper의 반환형은:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;Optional&amp;lt;UserResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 변경됐다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;Optional&amp;lt;UserResponseDTO&amp;gt; response =
        userMapper.login(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;조회 결과 있음
&amp;rarr; Optional&amp;lt;UserResponseDTO&amp;gt;

조회 결과 없음
&amp;rarr; Optional.empty()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. Optional 로그인 테스트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 Given-When-Then 구조를 유지했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Test
public void signIn() {

    // given
    UserRequestDTO request =
        UserRequestDTO.builder()
            .email(&quot;jslim@gmail.com&quot;)
            .password(&quot;1234&quot;)
            .build();

    // when
    Optional&amp;lt;UserResponseDTO&amp;gt; response =
        userMapper.login(request);

    // then
    Assertions.assertNotNull(response.get());

    Assertions.assertEquals(
        &quot;jslim9413@gmail.com&quot;,
        response.get().getEmail()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 다음 방식도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;response.orElseThrow(
    () -&amp;gt; new RuntimeException(&quot;이메일 또는 패스워드가 불일치&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Optional 값 있음
&amp;rarr; 정상 사용자 반환

Optional.empty()
&amp;rarr; RuntimeException&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. 테스트 데이터와 실제 DB 값 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서 회원 데이터를 직접 확인했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;EMAIL                    PASSWORD   NAME
jslim9413@gmail.com      1234       ...
jslim9413@naver.com      1234       ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 작성 시:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Given의 입력값
vs
DB에 실제 저장된 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정확히 일치해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이메일처럼 &lt;code&gt;WHERE&lt;/code&gt; 조건으로 사용되는 값은 오타 하나만 있어도 로그인 조회가 실패하기 때문에 테스트 데이터 정합성이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. Controller &amp;rarr; Encryption Service 실제 실행 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그에서는 다음 흐름까지 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user controller signIn get mapping

user controller signIn params :
UserRequestDTO(...)

user controller signIn service binding :
UserEncryptionServiceImpl...

user encryption service signIn

params :
UserRequestDTO(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HTTP Request
      &amp;darr;
Controller
      &amp;darr;
@Resource(name=&quot;encryption&quot;)
      &amp;darr;
UserEncryptionServiceImpl
      &amp;darr;
signIn()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실제 런타임에서도 정상 연결되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 브라우저에서 로그인 API 직접 호출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 실제 요청을 실행했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;http://localhost:8000/users/signIn
?email=jslim9413@gmail.com
&amp;amp;password=1234&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답은 JSON으로 정상 반환됐다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;email&quot;: &quot;jslim9413@gmail.com&quot;,
  &quot;name&quot;: &quot;임정섭&quot;,
  &quot;password&quot;: &quot;1234&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 전체 흐름이 실제로 연결됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser
   &amp;darr;
GET /users/signIn
   &amp;darr;
UserRequestDTO
   &amp;darr;
Controller
   &amp;darr;
Service
   &amp;darr;
Mapper
   &amp;darr;
MariaDB
   &amp;darr;
UserResponseDTO
   &amp;darr;
JSON&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. ResponseEntity로 로그인 응답 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(HttpStatus.OK)
        .header(&quot;access-token&quot;, &quot;12345&quot;)
        .body(response);&lt;/code&gt;&lt;/pre&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;pre class=&quot;ada&quot;&gt;&lt;code&gt;Status
&amp;rarr; 200 OK

Header
&amp;rarr; access-token

Body
&amp;rarr; UserResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실제 JWT 형태의 Access/Refresh Token 응답으로 발전했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. 새로운 inspire_mybatis 프로젝트 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트와 별도로:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;inspire_mybatis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 새로 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 클래스:&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class InspireMybatisApplication {

    public static void main(String[] args) {

        SpringApplication.run(
            InspireMybatisApplication.class,
            args
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트로 기존 코드를 옮기면서 &lt;b&gt;패키지 경로와 환경설정을 다시 맞추는 작업&lt;/b&gt;이 필요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. .env 환경변수 로딩&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 정보를 코드에 직접 넣지 않기 위해 &lt;code&gt;.env&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Dotenv env = Dotenv.configure()
        .ignoreIfMissing()
        .load();

env.entries().forEach(entry -&amp;gt; {

    System.setProperty(
        entry.getKey(),
        entry.getValue()
    );

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; env : &quot;
        + entry.getKey()
        + &quot;\t&quot;
        + entry.getValue()
    );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.env
 &amp;darr;
dotenv-java
 &amp;darr;
key / value 읽기
 &amp;darr;
System.setProperty()
 &amp;darr;
Spring 설정에서 참조&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. .env를 사용하는 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 비밀번호처럼 민감한 값은 코드에 직접 넣거나 GitHub에 올리지 않는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;DB PASSWORD
API KEY
TOKEN
SECRET&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일로 분리한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;코드
&amp;rarr; 공개 가능

민감정보
&amp;rarr; .env
&amp;rarr; Git 제외&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 TIL에서는 실제 비밀번호나 토큰 값은 기록하지 않고 구조만 남겼다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 1. 새 프로젝트에서 MyBatis ClassNotFoundException&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트 실행 중 MyBatis Mapper XML을 읽으면서 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;BuilderException
Error resolving class

TypeException
Could not resolve type alias

ClassNotFoundException
Cannot find class:
com.example.testcase.features.users.domain.dto.UserRequestDTO&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot가 시작되는 과정에서 MyBatis Mapper를 생성하지 못했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트의 package는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;com.example.inspire_mybatis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인데 Mapper XML에서는 이전 프로젝트의:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;com.example.testcase&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 그대로 사용하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;현재 Java Package
com.example.inspire_mybatis...

XML
com.example.testcase...
        &amp;darr;
불일치&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확인할 부분&lt;/h2&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;...&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;parameterType=&quot;...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;resultType=&quot;...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 곳 모두 Java의 실제 package와 맞아야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper XML의 package를 새 프로젝트 기준으로 수정했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;com.example.testcase...
        &amp;darr;
com.example.inspire_mybatis...&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 재실행하자:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;MariaDB Connection
HikariPool Initialization
Hibernate ORM Initialization
Started InspireMybatisApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 출력됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Spring Boot가 정상 기동됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. Swagger UI 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Springdoc을 추가한 뒤 Swagger UI에 접속했다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;http://localhost:8000/swagger-ui/index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;POST /users/signUp
GET  /users/signIn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 API가 자동으로 문서화된 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. Swagger/OpenAPI 의존성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;에는 다음 의존성이 추가됐다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt;용 라이브러리도 추가했다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'io.github.cdimascio:dotenv-java:3.0.0'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;springdoc-openapi
&amp;rarr; Swagger UI

dotenv-java
&amp;rarr; .env 로딩&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. @Tag&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에 API 그룹 정보를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Tag(
    name = &quot;User API&quot;,
    description = &quot;사용자 생성과 로그인 관련 API 명세서&quot;
)
@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI에서 실제로:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User API
사용자 생성과 로그인 관련 API 명세서&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 표시됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. @Operation&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 API에 설명을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Operation(
    summary = &quot;회원가입&quot;,
    description = &quot;신규가입(email, password, name)&quot;
)
@PostMapping(&quot;/signUp&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI에서는 endpoint 옆에:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;회원가입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 의미 있는 설명이 표시됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. @ApiResponses&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 API 응답을 문서화했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ApiResponses({
    @ApiResponse(
        responseCode = &quot;201&quot;,
        description = &quot;가입성공&quot;
    ),
    @ApiResponse(
        responseCode = &quot;400&quot;,
        description = &quot;유효성검사실패&quot;
    ),
    @ApiResponse(
        responseCode = &quot;500&quot;,
        description = &quot;가입실패&quot;
    )
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 API 명세에서:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;201
&amp;rarr; 가입 성공

400
&amp;rarr; 유효성 검증 실패

500
&amp;rarr; 가입 처리 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 명확하게 표현했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. Swagger RequestBody와 Spring RequestBody&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 API에서는 같은 이름의 두 &lt;code&gt;RequestBody&lt;/code&gt;를 함께 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 문서용:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@io.swagger.v3.oas.annotations.parameters.RequestBody(
    description = &quot;사용자 정보를 담는 DTO&quot;,
    required = true,
    content = @Content(
        schema = @Schema(
            implementation = UserRequestDTO.class
        )
    )
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Spring 바인딩용:&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;@org.springframework.web.bind.annotation.RequestBody
UserRequestDTO request&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. 두 RequestBody의 역할 차이&lt;/h1&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Swagger @RequestBody
&amp;rarr; API 문서 명세

Spring @RequestBody
&amp;rarr; 실제 JSON &amp;rarr; Java DTO 바인딩&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Swagger
&quot;이 API는 이런 JSON을 받습니다&quot;

Spring
&quot;실제로 들어온 JSON을 DTO로 변환합니다&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 차이가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. 회원가입 요청값 실제 바인딩 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 회원가입 요청 로그에서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user controller signup

user controller signup params :
UserRequestDTO(
    email=jslim9413@naver.com,
    password=1234,
    name=임정섭
)&lt;/code&gt;&lt;/pre&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;HTTP JSON Body
     &amp;darr;
@RequestBody
     &amp;darr;
UserRequestDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 정상 동작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. 회원가입 계층 구현 시작&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 기능을 Controller에서 Service/Mapper 구조로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;UserController
     &amp;darr;
UserService
     &amp;darr;
UserMapper
     &amp;darr;
MyBatis XML
     &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UserService&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class UserService {

    public int signUp(UserRequestDTO request) {

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user service signUp&quot;
        );

        return 0;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 아직 골격 코드였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. UserMapper 기능 정의&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper는 이후 다음 형태로 확장됐다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Mapper
public interface UserMapper {

    public int save(UserRequestDTO request);

    public Optional&amp;lt;UserResponseDTO&amp;gt; signIn(
        UserRequestDTO request
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;save()
&amp;rarr; 회원가입 INSERT
&amp;rarr; int

signIn()
&amp;rarr; 로그인 SELECT
&amp;rarr; Optional&amp;lt;UserResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. 회원가입 Controller &amp;rarr; Service 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int signUpFlag =
        userService.signUp(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 호출하게 됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (signUpFlag != 0) {

    return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;POST /users/signUp
      &amp;darr;
Controller
      &amp;darr;
Service
      &amp;darr;
Mapper.save()
      &amp;darr;
INSERT
      &amp;darr;
1 반환
      &amp;darr;
201 Created&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. @RequiredArgsConstructor 기반 생성자 주입&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 다음 구조로 변경됐다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
    private final JwtProvider jwtProvider;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;final&lt;/code&gt; 필드를 선언하면 Lombok이 생성자를 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;final UserMapper
final JwtProvider
       &amp;darr;
@RequiredArgsConstructor
       &amp;darr;
Constructor 자동 생성
       &amp;darr;
Spring DI&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. Service에서 Mapper 호출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 로직은 최종적으로:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public int signUp(UserRequestDTO request) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user service signUp&quot;
    );

    return userMapper.save(request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Service가 직접 SQL을 처리하는 것이 아니라 Repository/Mapper에 위임한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. JwtProvider Bean&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 token 기능을 담당하는 클래스를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Component
public class JwtProvider {

    public String createAT(String email) {
        return &quot;Bearer XXXXXXX&quot;;
    }

    public String createRT(String email) {
        return &quot;XXXXXXXXXXXXXX&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 실제 JWT 서명 알고리즘 구현이 아니라 &lt;b&gt;토큰 발급 흐름을 만들기 위한 골격 코드&lt;/b&gt;였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. 로그인 실패 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;userMapper
    .signIn(request)
    .orElseThrow(
        () -&amp;gt; new RuntimeException(&quot;로그인 실패&quot;)
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 로그인 실패를 처리했다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;DB 사용자 존재
&amp;rarr; 로그인 성공

DB 사용자 없음
&amp;rarr; Optional.empty
&amp;rarr; orElseThrow
&amp;rarr; RuntimeException&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. 로그인 성공 후 Token 발급&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 시:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String at =
    jwtProvider.createAT(request.getEmail());

String rt =
    jwtProvider.createRT(request.getEmail());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 통해 토큰을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;로그인 성공
      &amp;darr;
JwtProvider
      &amp;darr;
Access Token
Refresh Token&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. 향후 Token 저장 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 코드 주석에서는:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;inMemory DB
&amp;rarr; Redis
&amp;rarr; H2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등에 AT/RT를 저장해 관리하는 방향도 언급됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 현재는 토큰 생성까지만 구현하고, 실제 저장/만료/재발급 등의 인증 인프라는 이후 단계로 남아 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 2. 회원가입 중복 이메일&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 실행 중 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Duplicate entry 'inspire@lgcns.com'
for key 'PRIMARY'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;DuplicateKeyException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 변환되어 올라왔다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 테이블의:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EMAIL VARCHAR(50) PRIMARY KEY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 EMAIL을 다시 INSERT하면 PK 중복 오류가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;동일 Email 존재
      &amp;darr;
회원가입 재요청
      &amp;darr;
INSERT
      &amp;darr;
PRIMARY KEY 중복
      &amp;darr;
DuplicateKeyException&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 제약조건은 Spring/MyBatis를 사용하더라도 그대로 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;중복 이메일 사전 조회

또는

DuplicateKeyException 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 예외 처리 전략이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. 로그인 Service 결과를 Map으로 반환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 정보와 토큰을 모두 Controller에 전달하기 위해 Service의 반환형을:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt;&lt;/pre&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;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();

map.put(&quot;at&quot;, at);
map.put(&quot;rt&quot;, rt);
map.put(&quot;response&quot;, response);

return map;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. Controller에서 HttpHeaders 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; map =
        userService.signIn(request);

HttpHeaders headers =
        new HttpHeaders();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 Service의 로그인 결과를 받았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. Access Token / Refresh Token Header&lt;/h1&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;headers.add(
    &quot;Authorization&quot;,
    (String) map.get(&quot;at&quot;)
);

headers.add(
    &quot;Refresh-Token&quot;,
    (String) map.get(&quot;rt&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 Header는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Authorization
&amp;rarr; Access Token

Refresh-Token
&amp;rarr; Refresh Token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. Access-Control-Expose-Headers&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 JavaScript에서 커스텀 Response Header를 읽을 수 있도록:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;headers.add(
    &quot;Access-Control-Expose-Headers&quot;,
    &quot;Authorization, Refresh-Token&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Spring Response
      &amp;darr;
Authorization
Refresh-Token
      &amp;darr;
Access-Control-Expose-Headers
      &amp;darr;
React Axios에서 Header 접근 가능&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. 로그인 최종 ResponseEntity&lt;/h1&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(HttpStatus.OK)
        .headers(headers)
        .body(
            (UserResponseDTO)
                map.get(&quot;response&quot;)
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;HTTP 200 OK

Headers
Authorization: AT
Refresh-Token: RT

Body
UserResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태의 응답이 완성됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. Swagger 로그인 응답 명세&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 API에도 응답 시나리오를 문서화했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;200
&amp;rarr; 로그인 성공

400
&amp;rarr; 로그인 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 문서가 실제 Controller 구현과 함께 발전하는 흐름을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. 로그인 Query String 방식으로 변경&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 API는 GET 요청이므로 Query String 방식으로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/signIn&quot;)
public ResponseEntity&amp;lt;?&amp;gt; signIn(

    @RequestParam(&quot;email&quot;)
    String email,

    @RequestParam(&quot;password&quot;)
    String password
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/users/signIn
?email=...
&amp;amp;password=...&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. @RequestParam&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Query String

email=aaa
password=1234
      &amp;darr;
@RequestParam
      &amp;darr;
String email
String password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입의 &lt;code&gt;@RequestBody&lt;/code&gt;와 비교하면:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;회원가입
POST JSON
&amp;rarr; @RequestBody DTO

로그인
GET QueryString
&amp;rarr; @RequestParam&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 구분됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. React 환경별 .env&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트에서도:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.env
.env.development
.env.production&lt;/code&gt;&lt;/pre&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;pre class=&quot;applescript&quot;&gt;&lt;code&gt;application-dev.yml
application-prod.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 목적이다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Development
&amp;rarr; 개발 API URL

Production
&amp;rarr; 운영 API URL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 분리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. Axios 공통 Instance&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/api/axios.js&lt;/code&gt;에서 Axios 인스턴스를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import axios from 'axios';

const endPoint =
    process.env.REACT_APP_BACKEND_ENDPOINT;

const api = axios.create({
    baseURL: endPoint
});

export default api;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.env
 &amp;darr;
REACT_APP_BACKEND_ENDPOINT
 &amp;darr;
process.env
 &amp;darr;
axios.create
 &amp;darr;
baseURL&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. URL 하드코딩 제거&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 React 각 컴포넌트에서:&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;api.post('/users/signUp', data);&lt;/code&gt;&lt;/pre&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;pre class=&quot;arcade&quot;&gt;&lt;code&gt;REACT_APP_BACKEND_ENDPOINT
+
/users/signUp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경별 endpoint 변경이 훨씬 쉬워졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. React form과 Backend DTO 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 화면의 form 상태는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const [form, setForm] = useState({
    name: '',
    email: '',
    password: ''
});&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UserRequestDTO
├─ name
├─ email
└─ password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 대응된다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;React form
     &amp;darr; JSON
UserRequestDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프론트와 백엔드 간 필드명 정합성이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;45. 공통 Input Handler&lt;/h1&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;const keyHandler = (e) =&amp;gt; {

    const { name, value } = e.target;

    setForm({
        ...form,
        [name]: value
    });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;...form
&amp;rarr; 기존 값 유지

[name]
&amp;rarr; 현재 Input의 name을 key로 사용

value
&amp;rarr; 입력된 새로운 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 handler로 여러 input을 처리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;46. React 회원가입 API 호출&lt;/h1&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await api.post(
    '/users/signUp',
    data
)
.then(response =&amp;gt; {

    console.log(
        'debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; axios request success',
        response
    );

    if (response.status === 201) {
        moveUrl('/users/signIn');
    }
})
.catch(error =&amp;gt; {

    console.log(
        'debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; axios request error',
        error
    );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SignUpPage
      &amp;darr;
form
      &amp;darr;
Axios POST
      &amp;darr;
Spring /users/signUp
      &amp;darr;
201 Created
      &amp;darr;
SignIn Page 이동&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;47. CORS 문제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin은:&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;scheme + host + port&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 구분되므로:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;localhost:3000
&amp;ne;
localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 브라우저 CORS 정책을 통과시키기 위한 설정이 필요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;48. CorsConfig&lt;/h1&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig
        implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(
            CorsRegistry registry
    ) {

        registry
            .addMapping(&quot;/**&quot;)
            .allowedHeaders(&quot;*&quot;)
            .allowedOriginPatterns(
                &quot;http://localhost:3000&quot;
            )
            .allowedMethods(
                &quot;GET&quot;,
                &quot;POST&quot;,
                &quot;DELETE&quot;,
                &quot;PUT&quot;,
                &quot;PATCH&quot;,
                &quot;OPTIONS&quot;
            );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;49. CORS 설정 의미&lt;/h1&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;addMapping(&quot;/**&quot;)
&amp;rarr; 모든 Backend API에 적용

allowedOriginPatterns(...)
&amp;rarr; React Origin 허용

allowedHeaders(&quot;*&quot;)
&amp;rarr; 모든 요청 Header 허용

allowedMethods(...)
&amp;rarr; 허용 HTTP Method&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OPTIONS&lt;/code&gt;는 Preflight Request와 연결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;50. Preflight Request&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 조건에 따라 실제 요청 전에:&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;OPTIONS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 먼저 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Browser
   &amp;darr;
OPTIONS
&quot;이 요청 보내도 됩니까?&quot;
   &amp;darr;
Spring CORS 정책
   &amp;darr;
허용
   &amp;darr;
POST / GET 실제 요청&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;51. React &amp;rarr; Spring 회원가입 실제 요청 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 회원가입 요청을 보낸 뒤 Spring 로그에:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user controller signup

UserRequestDTO(
    email=...,
    password=1234,
    name=...
)

user service signUp&lt;/code&gt;&lt;/pre&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;pre class=&quot;nginx&quot;&gt;&lt;code&gt;React
 &amp;darr;
Axios
 &amp;darr;
CORS
 &amp;darr;
UserController
 &amp;darr;
UserService&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 실제 통신이 성공했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;52. React 로그인 API 호출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 화면에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;await api.get(
    `/users/signIn?email=${form.email}&amp;amp;password=${form.password}`
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식으로 요청했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Axios에서는 다음 방식도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;await api.get('/users/signIn', {
    params: {
        email: form.email,
        password: form.password
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;53. Query String 직접 조립 vs params&lt;/h1&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;직접 조립

/users/signIn?email=${...}&amp;amp;password=${...}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;Axios params

api.get('/users/signIn', {
    params: {
        email,
        password
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 모두 동일한 Query String 요청을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;params&lt;/code&gt; 방식을 사용하면 URL 문자열을 직접 조립하지 않아도 된다는 차이가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;54. Axios Response 확인&lt;/h1&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;.then(response =&amp;gt; {

    console.log(
        'debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; axios request success',
        response
    );

})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome DevTools에서 실제 response 객체를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;status: 200

data:
&amp;rarr; 사용자 정보

headers:
&amp;rarr; authorization
&amp;rarr; refresh-token&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;55. Authorization / Refresh-Token 브라우저 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DevTools에서 실제로:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;authorization:
Bearer ...

refresh-token:
...&lt;/code&gt;&lt;/pre&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;pre class=&quot;ada&quot;&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 실제로 동작하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Spring
&amp;rarr; JWT Header 반환

CORS expose
&amp;rarr; Header 공개

Axios
&amp;rarr; response.headers 접근 성공&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;56. 로그인 Response Data 구조 수정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프론트 코드에는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.data[0].email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 Spring 로그인 응답은 배열이 아니라 단건 DTO 객체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.data.email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 수정했다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;기존 가정
Array Response

현재 Backend
Single Object Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트와 백엔드 응답 구조의 정합성을 맞춘 사례였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;57. localStorage에 사용자 정보 저장&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 후:&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;localStorage.setItem(
    'user',
    response.data.email
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 로그인 사용자의 email을 저장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;58. Access Token 저장&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;localStorage.setItem(
    'at',
    response.headers.get('Authorization')
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;59. Refresh Token 저장&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;localStorage.setItem(
    'rt',
    response.headers.get('Refresh-Token')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 로그인 성공 후:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;localStorage

user
&amp;rarr; 로그인 email

at
&amp;rarr; Access Token

rt
&amp;rarr; Refresh Token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 저장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;60. 로그인 성공 후 전체 흐름&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SignInPage
      &amp;darr;
GET /users/signIn
      &amp;darr;
@RequestParam
      &amp;darr;
Controller
      &amp;darr;
Service
      &amp;darr;
Mapper
      &amp;darr;
MariaDB
      &amp;darr;
JwtProvider
      &amp;darr;
Access Token
Refresh Token
      &amp;darr;
ResponseEntity
      &amp;darr;
Axios Response
      &amp;darr;
localStorage
├─ user
├─ at
└─ rt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;/blogs/index&lt;/code&gt;로 이동하는 코드는 주석 처리되어 있어, 우선 토큰 저장까지 검증한 상태였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. MyBatis Mapper package 경로 불일치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ClassNotFoundException
Could not resolve type alias&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트로 코드를 복사했지만 XML에는 기존 &lt;code&gt;com.example.testcase&lt;/code&gt; package가 남아 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;namespace
parameterType
resultType&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 새 &lt;code&gt;com.example.inspire_mybatis&lt;/code&gt; 기준으로 수정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 정상 기동.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 회원가입 중복 이메일&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;Duplicate entry ... for key PRIMARY
DuplicateKeyException&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EMAIL이 PRIMARY KEY인데 이미 존재하는 이메일로 INSERT했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring/MyBatis를 사용해도 DB 제약조건은 그대로 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후 중복 확인 또는 예외 응답 처리가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. React &amp;harr; Spring CORS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상/문제&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;React : localhost:3000
Spring: localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트가 달라 다른 Origin이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;CorsConfig
&amp;rarr; allowedOriginPatterns(&quot;http://localhost:3000&quot;)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;React 회원가입 요청이 Controller와 Service 로그까지 정상 도달했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Response 구조 불일치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 코드&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.data[0].email&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 Backend Response&lt;/h3&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
  &quot;email&quot;: &quot;...&quot;,
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.data.email&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;Front와 Backend 간에는 단순 endpoint뿐 아니라 &lt;b&gt;JSON 구조까지 계약이 맞아야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. JWT Header 접근&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 토큰을 Header로 내려줬지만 브라우저 JavaScript에서 읽으려면 커스텀 Header 노출 설정이 필요했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Access-Control-Expose-Headers
&amp;rarr; Authorization, Refresh-Token&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome DevTools Axios response에서 두 Header를 실제로 확인했고 &lt;code&gt;localStorage&lt;/code&gt; 저장 코드까지 연결했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 지금까지 배웠던 Spring Boot 개념들이 실제 프론트엔드와 연결되면서 하나의 애플리케이션 흐름으로 완성되기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
Service
Mapper
Swagger
React
JWT
CORS&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;React Form
    &amp;darr;
Axios
    &amp;darr;
CORS
    &amp;darr;
Spring Controller
    &amp;darr;
Service
    &amp;darr;
Mapper
    &amp;darr;
MyBatis
    &amp;darr;
MariaDB
    &amp;darr;
JWT Provider
    &amp;darr;
ResponseEntity
    &amp;darr;
Axios Response
    &amp;darr;
localStorage&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 백엔드 코드만 잘 작성한다고 끝나는 것이 아니라 &lt;b&gt;프론트가 어떤 방식으로 요청하고 어떤 응답 구조를 기대하는지까지 맞춰야 API가 실제로 동작한다&lt;/b&gt;는 점을 직접 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&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;회원가입 하나만 보더라도:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
&amp;rarr; HTTP 요청 해석

Service
&amp;rarr; 회원가입 로직

Mapper
&amp;rarr; SQL 실행

DB
&amp;rarr; 데이터 저장&lt;/code&gt;&lt;/pre&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JwtProvider
&amp;rarr; 토큰 발급&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 추가 책임이 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Controller가 모든 일을 직접 하지 않기 때문에 각 객체의 역할이 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 이전 Java 수업에서 배운:&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;Interface
Polymorphism
Loose Coupling
Factory
DI&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념들이 실제 Spring 프로젝트에 적용된 결과라는 점도 다시 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Swagger를 사용해보며 느낀 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger를 사용하면서 API가 단순히 코드에 존재하는 메서드가 아니라 &lt;b&gt;프론트엔드와 공유해야 하는 계약&lt;/b&gt;이라는 점도 명확해졌다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;HTTP Method
Endpoint
Request Body
Request Parameter
Response Status
Response Body&lt;/code&gt;&lt;/pre&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;pre class=&quot;less&quot;&gt;&lt;code&gt;@Tag
@Operation
@ApiResponses
@Schema&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 통해 코드와 API 문서를 함께 관리하는 방식이 실무적인 느낌이 강했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  환경변수의 중요성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드와 프론트 모두 환경별 설정을 분리했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Backend
.env
application-dev.yml
application-prod.yml

Frontend
.env
.env.development
.env.production&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 통해:&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;Local
Development
Production&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경마다 URL이나 DB 설정을 다르게 가져갈 수 있다는 점을 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 비밀번호나 Secret 같은 값을 GitHub에 직접 올리지 않도록 &lt;code&gt;.env&lt;/code&gt;를 사용하는 이유도 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;회원가입
POST /users/signUp
&amp;rarr; JSON
&amp;rarr; @RequestBody
&amp;rarr; Service
&amp;rarr; Mapper
&amp;rarr; DB
&amp;rarr; 201

로그인
GET /users/signIn
&amp;rarr; QueryString
&amp;rarr; @RequestParam
&amp;rarr; Service
&amp;rarr; Mapper
&amp;rarr; JWT
&amp;rarr; Header + Body
&amp;rarr; React
&amp;rarr; localStorage&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 다음 개념을 집중해서 확인해야겠다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;JWT 실제 생성/검증

Access Token 만료

Refresh Token 재발급

Authorization Header 사용

Axios Interceptor

Spring Security

로그인 실패 Exception 처리

회원 중복 가입 처리

Validation

Redis 기반 Token 관리

React 로그인 상태 유지

보호된 API 요청&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 &lt;code&gt;JwtProvider&lt;/code&gt;가 임시 문자열을 반환하는 수준이므로, 실제 JWT 서명과 인증/인가가 적용되면 오늘 만든 흐름과 비교하면서 학습하면 이해하기 좋을 것 같다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>CORS</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mybatis</category>
      <category>OpenAPI</category>
      <category>react</category>
      <category>springboot</category>
      <category>swagger</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/52</guid>
      <comments>https://bbbicsan.tistory.com/52#entry52comment</comments>
      <pubDate>Wed, 2 Sep 2026 17:54:10 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 25일차 TIL : Spring MVC Controller&amp;middot;REST API&amp;middot;IoC/DI&amp;middot;MyBatis&amp;middot;TDD</title>
      <link>https://bbbicsan.tistory.com/51</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 25일차 TIL : Spring MVC Controller&amp;middot;REST API&amp;middot;IoC/DI&amp;middot;MyBatis&amp;middot;TDD&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHPyOa/dJMcadJZvEa/bebdWGKqffgyFeKcq3HH21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHPyOa/dJMcadJZvEa/bebdWGKqffgyFeKcq3HH21/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHPyOa/dJMcadJZvEa/bebdWGKqffgyFeKcq3HH21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHPyOa%2FdJMcadJZvEa%2FbebdWGKqffgyFeKcq3HH21%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;Spring Boot 서버를 직접 기동한 뒤 &lt;code&gt;Controller &amp;rarr; Service &amp;rarr; Mapper &amp;rarr; MyBatis &amp;rarr; MariaDB&lt;/code&gt;로 이어지는 요청 처리 흐름을 구현했고, &lt;code&gt;@Controller&lt;/code&gt;와 &lt;code&gt;@RestController&lt;/code&gt;, &lt;code&gt;RequestMapping&lt;/code&gt;, DTO 바인딩, &lt;code&gt;ResponseEntity&lt;/code&gt;, IoC/DI, MyBatis XML Mapper, JUnit 기반 Given-When-Then 테스트까지 연결했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot Server 실행
        &amp;darr;
Embedded Tomcat
DispatcherServlet
        &amp;darr;
localhost:8000 접속
        &amp;darr;
Whitelabel 404 확인
        &amp;darr;
@Controller
@RequestMapping
@GetMapping
        &amp;darr;
JSP View Rendering
WEB-INF/view/index.jsp
        &amp;darr;
@RestController
        &amp;darr;
DTO &amp;rarr; JSON
        &amp;darr;
ResponseEntity
Status / Header / Body
        &amp;darr;
기능 단위 Package 구조
users
├─ ctrl
├─ domain
├─ service
└─ repository
        &amp;darr;
Spring IoC / Bean / DI
        &amp;darr;
@Autowired
@Resource
@Qualifier
@RequiredArgsConstructor
        &amp;darr;
MyBatis
Mapper Interface
Mapper XML
        &amp;darr;
SpringBootTest
Given - When - Then
        &amp;darr;
BindingException 발생
        &amp;darr;
mapper-locations 수정
        &amp;darr;
INSERT 성공
assertEquals()
        &amp;darr;
findByAll()
login()
        &amp;darr;
Controller &amp;rarr; Service DI
        &amp;darr;
@Resource(name = &quot;plain&quot;)
        &amp;darr;
UserPlainServiceImpl Bean 주입 확인&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Spring MVC&lt;/li&gt;
&lt;li&gt;Embedded Tomcat&lt;/li&gt;
&lt;li&gt;DispatcherServlet&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@GetMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DTO Binding&lt;/li&gt;
&lt;li&gt;JSP&lt;/li&gt;
&lt;li&gt;ViewResolver&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpStatus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spring Bean&lt;/li&gt;
&lt;li&gt;IoC&lt;/li&gt;
&lt;li&gt;DI&lt;/li&gt;
&lt;li&gt;Dependency Lookup&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Resource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Mapper&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mapper XML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parameterType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resultType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapper-locations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@SpringBootTest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JUnit&lt;/li&gt;
&lt;li&gt;TDD&lt;/li&gt;
&lt;li&gt;Given / When / Then&lt;/li&gt;
&lt;li&gt;Assertion&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Spring Boot 서버 기동&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업은 전날 생성한 Spring Boot 프로젝트를 실제로 실행하는 것부터 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 로그에서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Tomcat initialized with port 8000 (http)
Tomcat started on port 8000 (http)
Started DemoApplication&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;DemoApplication 실행
        &amp;darr;
SpringApplication.run()
        &amp;darr;
Spring Boot Context 생성
        &amp;darr;
Embedded Tomcat 시작
        &amp;darr;
8000 Port에서 요청 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. DispatcherServlet 초기화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 서버에 접속하자 DispatcherServlet도 초기화됐다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Initializing Spring DispatcherServlet
Completed initialization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 요청 흐름에서 DispatcherServlet은 &lt;b&gt;모든 요청의 Front Controller 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Browser
   &amp;darr;
Tomcat
   &amp;darr;
DispatcherServlet
   &amp;darr;
Controller 탐색
   &amp;darr;
Controller Method 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 Java MVC 구조를 배울 때 만들었던 Front Controller 개념이 Spring MVC 내부에서 실제로 사용되고 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. localhost:8000 접속과 Whitelabel Error Page&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 접속했더니 다음 화면이 나왔다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Whitelabel Error Page

status=404
No static resource for request '/'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 서버 실행 실패처럼 보일 수 있지만, 실제로는 반대였다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Spring Boot 실행 성공
        &amp;darr;
Tomcat 실행 성공
        &amp;darr;
브라우저 요청 도착 성공
        &amp;darr;
DispatcherServlet 실행 성공
        &amp;darr;
하지만 &quot;/&quot;에 연결된 Controller / Resource 없음
        &amp;darr;
404&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버 자체는 정상 동작하고 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 1. 404가 나왔는데 서버가 실패한 걸까?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Whitelabel Error Page
404 Not Found
No static resource for request '/'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 로그에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Tomcat started on port 8000
Started DemoApplication
DispatcherServlet initialized&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모두 정상적으로 출력됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt; 경로에:&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;Controller Mapping
또는
static/index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 존재하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;404
&amp;ne; 서버 기동 실패

404
= 요청 경로에 대응하는 자원이 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 점을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Spring Controller와 Bean 등록&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기본 Controller를 생성했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Controller
public class DemoController {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 객체가 필요할 때 직접:&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;new DemoController();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 호출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 &lt;code&gt;@Controller&lt;/code&gt; 같은 애너테이션을 통해 Spring이 객체를 생성하고 관리한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DemoController Class
       &amp;darr;
@Controller
       &amp;darr;
Component Scan
       &amp;darr;
Spring Container
       &amp;darr;
Bean 등록&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Spring에서 사용하는 역할별 Bean&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 다음 애너테이션들이 함께 등장했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
├─ @Controller
├─ @RestController
├─ @Service
└─ @Repository

MyBatis
└─ @Mapper&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MVC 요청 처리 + View 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;REST 요청 처리 + Data/JSON 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business Logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DB Access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Mapper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MyBatis Mapper Interface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. @Controller와 @RestController 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님이 특히 강조한 부분이었다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Controller
&amp;rarr; 액션 수행
&amp;rarr; Page / View 반환

@RestController
&amp;rarr; 액션 수행
&amp;rarr; Data / JSON 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser + JSP
&amp;rarr; @Controller

React / API Client
&amp;rarr; @RestController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 생각하면 이해하기 쉬웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. Endpoint와 Controller Method 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 사용자의 endpoint가 Controller의 action method와 연결된다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;User Endpoint
     &amp;darr;
Controller
     &amp;darr;
Action(Method)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequestMapping
@GetMapping
@PostMapping
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. @RequestMapping + @GetMapping&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/demo&quot;)
public class DemoController {

    @GetMapping(&quot;/index&quot;)
    public String getMethodName() {
        return &quot;index&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 요청 주소는:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;/demo + /index
=
/demo/index&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;GET /demo/index
      &amp;darr;
DemoController
      &amp;darr;
getMethodName()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 연결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. Query Parameter를 @RequestParam으로 받기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드에서:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/index&quot;)
public String getMethodName(
        @RequestParam String param
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 선언하면 다음 요청의 값을 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/demo/index?param=hello&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;param
&amp;rarr; &quot;hello&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Query String
      &amp;darr;
@RequestParam
      &amp;darr;
Java Method Parameter&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바인딩된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. JSP View Rendering&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Controller&lt;/code&gt;에서는 JSP 페이지를 반환하는 흐름을 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 다음 경로를 생성했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;src
└─ main
   └─ webapp
      └─ WEB-INF
         └─ view
            └─ index.jsp&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. JSP 관련 Dependency 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 JSP를 사용하기 위해 관련 라이브러리를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;sml&quot;&gt;&lt;code&gt;providedRuntime 'org.apache.tomcat.embed:tomcat-embed-jasper'

implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;tomcat-embed-jasper
&amp;rarr; JSP 컴파일 / 렌더링

JSTL API
&amp;rarr; JSP 표준 태그 API

JSTL 구현체
&amp;rarr; 실제 JSTL 동작&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. ViewResolver 설정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application-dev.yml&lt;/code&gt;에 View 경로를 설정했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;server:
  port: 8000

spring:
  mvc:
    view:
      prefix: /WEB-INF/view/
      suffix: .jsp

  config:
    activate:
      on-profile: dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Controller에서:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;return &quot;index&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 반환하면:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;prefix
/WEB-INF/view/

+
index

+
.jsp

=
/WEB-INF/view/index.jsp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. Controller &amp;rarr; JSP 전체 흐름&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Browser
   &amp;darr;
GET /demo/index
   &amp;darr;
DispatcherServlet
   &amp;darr;
DemoController
   &amp;darr;
@GetMapping(&quot;/index&quot;)
   &amp;darr;
return &quot;index&quot;
   &amp;darr;
ViewResolver
   &amp;darr;
/WEB-INF/view/index.jsp
   &amp;darr;
HTML Rendering
   &amp;darr;
Browser Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user endPoint : /index&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그도 출력되어 실제 Controller 메서드가 호출됐음을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. @RestController 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API를 위한 별도 Controller도 만들었다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/rest&quot;)
public class ReactController {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@RestController&lt;/code&gt;는 JSP View가 아니라 데이터를 반환한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. DTO를 JSON으로 반환&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/index&quot;)
public MessageDTO getMethodName() {

    return MessageDTO.builder()
            .message(&quot;테스트&quot;)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller가 Java DTO를 반환하면 Spring이 이를 JSON으로 변환한다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;MessageDTO
      &amp;darr;
Spring Message Converter
      &amp;darr;
JSON&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적인 응답:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;message&quot;: &quot;테스트&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. ResponseEntity&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 직접 반환에서 한 단계 더 나아가 &lt;code&gt;ResponseEntity&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@GetMapping(&quot;/index&quot;)
public ResponseEntity&amp;lt;?&amp;gt; getMethodName() {

    return ResponseEntity
            .status(HttpStatus.OK)
            .body(
                MessageDTO.builder()
                        .message(&quot;테스트&quot;)
                        .build()
            );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ResponseEntity&lt;/code&gt;를 사용하면 HTTP Response를 직접 제어할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;HTTP Response
├─ Status
├─ Header
└─ Body&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. Status + Header + Body 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다음 형태까지 확장했다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;return ResponseEntity
        .status(HttpStatus.CREATED)
        .header(&quot;access-token&quot;, &quot;12345&quot;)
        .body(
            MessageDTO.builder()
                    .message(&quot;테스트&quot;)
                    .build()
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 구조:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Status
&amp;rarr; 201 Created

Header
&amp;rarr; access-token: 12345

Body
&amp;rarr; {
     &quot;message&quot;: &quot;테스트&quot;
   }&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. ResponseEntity를 사용하는 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO를 직접 반환하면 Spring이 기본 응답을 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;DTO
&amp;rarr; 기본적으로 200 OK
&amp;rarr; JSON&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;ResponseEntity&lt;/code&gt;는:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Status
Header
Body&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 모두 직접 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 REST API의 응답 명세를 좀 더 명확하게 표현할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. 기능 중심 Package 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트에서는 기능 단위 패키지 구조도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;features
├─ blogs
├─ test
└─ users
   ├─ ctrl
   ├─ domain
   │  └─ dto
   ├─ repository
   └─ service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존처럼:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;controller
service
repository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 프로젝트 전체에 하나씩 만드는 것이 아니라:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;users
blogs
orders
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 기능별로 먼저 분리한 뒤 내부에서 계층을 나누는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. User 기능의 계층&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;users
├─ ctrl
│  &amp;rarr; UserController
│
├─ domain
│  └─ dto
│     &amp;rarr; UserRequestDTO
│     &amp;rarr; UserResponseDTO
│
├─ service
│  &amp;rarr; UserService
│  &amp;rarr; UserPlainServiceImpl
│  &amp;rarr; UserEncryptionServiceImpl
│
└─ repository
   &amp;rarr; UserMapper&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 흐름은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Controller
  &amp;darr;
Service
  &amp;darr;
Repository / Mapper
  &amp;darr;
DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. IoC - Inversion of Control&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework 핵심 개념으로 IoC를 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Java에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UserService service = new UserService();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 객체의 생성 책임이 개발자에게 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;객체 생성
객체 보관
객체 연결
객체 생명주기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 Spring Container가 관리한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Developer
   &amp;darr;
Class 작성

Spring Container
   &amp;darr;
Object 생성
   &amp;darr;
Bean 관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제어권이 개발자에서 Framework로 넘어갔기 때문에 &lt;b&gt;제어의 역전(IoC)&lt;/b&gt; 이라고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. Spring Bean&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean은 &lt;b&gt;Spring Container가 생성하고 관리하는 객체&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Controller
@Service
@Repository
@Component
@Mapper&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등으로 등록된 객체들이 Bean이 된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Spring Container
├─ UserController Bean
├─ UserService Bean
├─ UserMapper Bean
└─ ...&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Dependency Injection&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean끼리 필요한 관계를 연결하는 것이 DI이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
   &amp;darr; 필요
Service
   &amp;darr; 필요
Repository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 필요한 객체를 주입해준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. Dependency Lookup&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI와 함께 Dependency Lookup도 배웠다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Dependency Injection
&amp;rarr; Spring이 알아서 객체를 넣어줌

Dependency Lookup
&amp;rarr; 직접 Container에서 Bean 조회
&amp;rarr; getBean()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;applicationContext.getBean(UserService.class);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. DI 관련 방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 다음 방식들이 나왔다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequiredArgsConstructor + final
@Autowired
@Inject
@Resource
@Qualifier&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. 생성자 주입&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;final&lt;/code&gt; 필드에 대해 Lombok이 생성자를 만들어주고, Spring이 해당 생성자를 이용해 Bean을 주입한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;final Field
      &amp;darr;
@RequiredArgsConstructor
      &amp;darr;
Constructor 생성
      &amp;darr;
Spring DI&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. @Autowired를 이용한 Bean 주입&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private UserMapper userMapper;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;UserMapper 타입 Bean 탐색
       &amp;darr;
Spring Container
       &amp;darr;
주입&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. MyBatis Mapper Interface&lt;/h1&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Mapper
public interface UserMapper {

    public int save(UserRequestDTO request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Mapper&lt;/code&gt;를 붙이면 MyBatis가 해당 Interface의 구현 객체를 만들어 Spring Bean으로 등록한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;UserMapper Interface
       &amp;darr;
@Mapper
       &amp;darr;
MyBatis Proxy 생성
       &amp;darr;
Spring Bean&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. 회원 테이블 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에 다음 테이블을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE SPRING_USER_TBL (
    EMAIL    VARCHAR(50) PRIMARY KEY,
    PASSWORD VARCHAR(50) NOT NULL,
    NAME     VARCHAR(50) NOT NULL
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. MyBatis XML Mapper&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/main/resources/mappers/userMapper.xml&lt;/code&gt;을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;com.example.testcase.features.users.repository.UserMapper&quot;&amp;gt;

    &amp;lt;insert id=&quot;save&quot;
            parameterType=&quot;com.example.testcase.features.users.domain.dto.UserRequestDTO&quot;&amp;gt;

        INSERT INTO SPRING_USER_TBL(
            EMAIL,
            PASSWORD,
            NAME
        )
        VALUES(
            #{email},
            #{password},
            #{name}
        )

    &amp;lt;/insert&amp;gt;

&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. Mapper Interface와 XML 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 규칙이 매우 중요했다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Java Interface 전체 경로
      ↕
XML namespace

Java Method
save(...)
      ↕
XML id=&quot;save&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;UserMapper.save()
        &amp;darr;
userMapper.xml
        &amp;darr;
&amp;lt;insert id=&quot;save&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. parameterType&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT에서는:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;parameterType=&quot;...UserRequestDTO&quot;&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UserRequestDTO
      &amp;darr;
SQL에 전달할 Parameter Object&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. #{property} Parameter Binding&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;VALUES(
    #{email},
    #{password},
    #{name}
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 이 값이 DTO getter와 연결된다고 설명했다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;#{email}
&amp;rarr; getEmail()

#{password}
&amp;rarr; getPassword()

#{name}
&amp;rarr; getName()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JDBC에서 직접:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;pstmt.setString(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 하던 작업을 MyBatis가 대신 처리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. Spring DataSource 설정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application-dev.yml&lt;/code&gt;에 DB 설정을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  config:
    activate:
      on-profile: dev

  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/lgcns
    username: root
    password: ********&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Spring Boot
   &amp;darr;
DataSource
   &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 연결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. MyBatis Mapper XML 경로 설정&lt;/h1&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;mybatis:
  mapper-locations: classpath:/mappers/**/*Mapper.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정으로:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resources/mappers
        &amp;darr;
하위 디렉터리 탐색
        &amp;darr;
*Mapper.xml 로딩&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하도록 설정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. @SpringBootTest&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis 기능을 실제 Spring Context에서 테스트했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@SpringBootTest
public class UserApplicationTests {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@SpringBootTest&lt;/code&gt;는 Spring Application Context를 실제로 실행한 상태에서 테스트할 수 있게 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. TDD - Given / When / Then&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 다음 구조로 작성했다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;Given
&amp;rarr; 데이터 준비

When
&amp;rarr; 기능 실행

Then
&amp;rarr; 결과 검증&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. signUp 테스트&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void signUp() {

    // given
    UserRequestDTO request =
        UserRequestDTO.builder()
            .email(&quot;jslim9413@naver.com&quot;)
            .password(&quot;1234&quot;)
            .name(&quot;임섭춘&quot;)
            .build();

    // when
    int flag = userMapper.save(request);

    // then
    System.out.println(&quot;result : &quot; + flag);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 2. MyBatis Invalid bound statement&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 처음 실행했을 때:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;org.apache.ibatis.binding.BindingException

Invalid bound statement (not found: ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;userMapper.save(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행했지만 MyBatis가 SQL을 찾지 못했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 연결을 확인해야 했다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;UserMapper
↕
namespace

save()
↕
id=&quot;save&quot;

Mapper XML 위치
↕
mapper-locations&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interface Bean 자체는 생성됐지만 XML Statement가 등록되지 않은 상태였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application-dev.yml&lt;/code&gt;에 Mapper XML 위치를 명시했다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;mybatis:
  mapper-locations: classpath:/mappers/**/*Mapper.xml&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 다시 실행하자:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;result : 1&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT 성공
&amp;rarr; 영향받은 Row 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. Assertion으로 테스트 자동 검증&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;System.out.println(&quot;result : &quot; + flag);&lt;/code&gt;&lt;/pre&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Assertions.assertEquals(1, flag);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;expected = 1
actual   = flag&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 같으면 테스트 성공, 다르면 실패한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. 출력 확인에서 자동 검증으로&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Before

System.out.println()
&amp;rarr; 사람이 결과 확인


After

Assertions.assertEquals()
&amp;rarr; 테스트 코드가 자동으로 성공/실패 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 확인하면서 테스트의 목적을 좀 더 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. findByAll()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 전체 조회 기능도 추가했다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public List&amp;lt;UserResponseDTO&amp;gt; findByAll();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findByAll&quot;
        resultType=&quot;com.example.testcase.features.users.domain.dto.UserResponseDTO&quot;&amp;gt;

    SELECT
        EMAIL,
        PASSWORD,
        NAME
    FROM
        SPRING_USER_TBL

&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. parameterType vs resultType&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 MyBatis에서 중요한 차이였다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;parameterType
&amp;rarr; Java &amp;rarr; SQL
&amp;rarr; SQL에 넣어줄 객체 타입

resultType
&amp;rarr; SQL &amp;rarr; Java
&amp;rarr; SELECT 결과를 만들 객체 타입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;INSERT
UserRequestDTO
&amp;rarr; parameterType

SELECT
UserResponseDTO
&amp;rarr; resultType&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. ResultSet 매핑도 MyBatis가 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 JDBC에서는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;while(rset.next()) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 직접 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에서는:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;resultType=&quot;UserResponseDTO&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 지정하면 SELECT 결과를 DTO로 매핑한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;JDBC

ResultSet
&amp;rarr; getString()
&amp;rarr; DTO Builder
&amp;rarr; List


MyBatis

SELECT
&amp;rarr; resultType
&amp;rarr; DTO/List 자동 매핑&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. Login Mapper 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 기능도 추가하기 시작했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;login&quot;
        parameterType=&quot;...&quot;
        resultType=&quot;...&quot;&amp;gt;

    SELECT
        EMAIL,
        PASSWORD,
        NAME
    FROM
        SPRING_USER_TBL
    WHERE
        EMAIL = #{email}
        AND PASSWORD = #{password}

&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;email
password
   &amp;darr;
UserRequestDTO
   &amp;darr;
UserMapper.login()
   &amp;darr;
MyBatis
   &amp;darr;
SPRING_USER_TBL
   &amp;darr;
UserResponseDTO&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;45. 로그인 테스트 골격&lt;/h1&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Test
public void singIn() {
    System.out.println(&quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; 사용자 로그인 검증&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 테스트 메서드의 시작 단계까지 작성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;46. Service Interface와 여러 구현체&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 계층에서는 하나의 인터페이스에 여러 구현체가 등장했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;UserService
├─ UserPlainServiceImpl
└─ UserEncryptionServiceImpl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 배운 인터페이스와 다형성이 Spring Service 구조에 그대로 적용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;47. UserEncryptionServiceImpl&lt;/h1&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class UserEncryptionServiceImpl implements UserService {

    @Override
    public UserResponseDTO signIn(UserRequestDTO request) {

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user encryption service signIn&quot;
        );

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; params : &quot; + request
        );

        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 화면에서는 실제 암호화 기능을 구현하기 전의 골격 단계였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;48. UserController 로그인 Endpoint&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {

    @GetMapping(&quot;/signIn&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; signIn(
            UserRequestDTO request
    ) {

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user controller signIn get mapping&quot;
        );

        System.out.println(
            &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; params &quot; + request
        );

        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;endpoint는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /users/signIn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;49. Query Parameter &amp;rarr; DTO 자동 바인딩&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 예:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/users/signIn
?email=jslim9413@gmail.com
&amp;amp;password=1234&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;signIn(UserRequestDTO request)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 선언했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 Query Parameter를 DTO 필드에 자동으로 바인딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 로그:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;UserRequestDTO(
    email=jslim9413@gmail.com,
    password=1234,
    name=null
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;50. Controller &amp;rarr; Service DI&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private UserService userService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 타입 기반 DI를 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 &lt;code&gt;UserService&lt;/code&gt; 구현체는:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;UserPlainServiceImpl
UserEncryptionServiceImpl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개가 존재한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;UserService Bean 필요
      &amp;darr;
후보 2개
      &amp;darr;
어느 Bean을 주입할까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 문제가 생길 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;51. @Resource(name = &quot;plain&quot;)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 Bean 이름을 지정하는 방식으로 해결했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Resource(name = &quot;plain&quot;)
private UserService userService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Spring은 &lt;code&gt;&quot;plain&quot;&lt;/code&gt;이라는 이름의 Bean을 찾아 주입한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;UserService
      &amp;darr;
@Resource(name=&quot;plain&quot;)
      &amp;darr;
plain Bean
      &amp;darr;
UserPlainServiceImpl&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;52. 실제 Bean 주입 검증&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;System.out.println(
    &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; user controller signIn service binding &quot;
    + userService
);&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UserPlainServiceImpl@...&lt;/code&gt;&lt;/pre&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;pre class=&quot;css&quot;&gt;&lt;code&gt;@Resource(name=&quot;plain&quot;)
        &amp;darr;
UserPlainServiceImpl Bean
        &amp;darr;
실제 주입 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;53. Bean + DI + 계층 구조 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용을 전체적으로 합치면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Spring Container
      &amp;darr;
Bean 생성

UserController
      &amp;darr; DI
UserService
      &amp;darr;
UserPlainServiceImpl
      &amp;darr;
UserMapper
      &amp;darr;
MyBatis XML
      &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 각 객체를 직접 &lt;code&gt;new&lt;/code&gt;로 생성하지 않고 Spring이 객체 생성과 연결을 담당한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 전체 요청 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 구현한 구조를 하나로 연결하면:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;React / Browser
        &amp;darr;
HTTP Request

/users/signIn
?email=...
&amp;amp;password=...
        &amp;darr;
Embedded Tomcat
        &amp;darr;
DispatcherServlet
        &amp;darr;
UserController Bean
        &amp;darr;
UserRequestDTO Binding
        &amp;darr;
UserService Bean
        &amp;darr;
UserPlainServiceImpl
        &amp;darr;
UserMapper
        &amp;darr;
MyBatis Mapper XML
        &amp;darr;
MariaDB
        &amp;darr;
UserResponseDTO
        &amp;darr;
ResponseEntity
        &amp;darr;
Status + Header + JSON Body
        &amp;darr;
Client&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. localhost:8000에서 404 발생&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Whitelabel Error Page
404&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat과 DispatcherServlet은 정상적으로 동작했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt; endpoint가 구현되지 않았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;404
&amp;rarr; Server Down이 아니라
&amp;rarr; Resource Not Found일 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MyBatis BindingException&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Invalid bound statement&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper Interface는 Bean으로 등록됐지만 MyBatis가 대응되는 XML Statement를 로딩하지 못했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인한 부분&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;namespace
id
Mapper XML 경로
mapper-locations&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;mybatis:
  mapper-locations: classpath:/mappers/**/*Mapper.xml&lt;/code&gt;&lt;/pre&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;result : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 출력되면서 INSERT가 정상 동작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 같은 Service Interface 구현체가 여러 개인 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;UserService
├─ UserPlainServiceImpl
└─ UserEncryptionServiceImpl&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입만으로 DI하면 어떤 Bean을 사용할지 결정하기 어렵다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Resource(name = &quot;plain&quot;)
private UserService userService;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UserPlainServiceImpl@...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 통해 실제 Bean을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Spring Boot에서 하나의 요청이 어떤 과정을 거쳐 처리되는지 실제 코드로 연결하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
Service
Repository&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 단어가 단순한 폴더 구조처럼 느껴졌는데, 오늘 수업을 통해 각 객체가 &lt;b&gt;Spring Bean으로 등록되고 DI를 통해 실제로 연결되는 구조&lt;/b&gt;라는 점이 보이기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;@RestController
&amp;rarr; 요청 진입점

Service
&amp;rarr; Business Logic

Mapper
&amp;rarr; SQL 연결

MyBatis XML
&amp;rarr; 실제 SQL

MariaDB
&amp;rarr; 데이터 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 역할이 분리되는 흐름이 인상적이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&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;Java에서는 객체가 필요하면:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;new Object();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 직접 생성하는 것이 익숙했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring에서는:&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;Class
&amp;rarr; Annotation
&amp;rarr; Bean
&amp;rarr; Spring Container
&amp;rarr; DI&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 구조가 중심이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 Controller가 Service 구현체를 직접 생성하지 않고:&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;private UserService userService;&lt;/code&gt;&lt;/pre&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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Plain
Encryption&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 교체할 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이전에 Java OOP에서 배웠던:&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;Interface
Polymorphism
Loose Coupling&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Spring DI 구조에서 실제로 사용되고 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  JDBC와 MyBatis 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 수업에서 JDBC를 직접 구현했을 때는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Connection
PreparedStatement
executeQuery
ResultSet
DTO Mapping
close&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 전부 직접 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 MyBatis에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Mapper Interface
+
Mapper XML
+
parameterType
+
resultType&lt;/code&gt;&lt;/pre&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;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;JDBC
pstmt.setString()

&amp;darr;

MyBatis
#{email}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;JDBC
ResultSet &amp;rarr; DTO 직접 변환

&amp;darr;

MyBatis
resultType &amp;rarr; DTO 자동 매핑&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 연결해서 보니 MyBatis를 사용하는 이유가 이해되기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;code&gt;Controller &amp;rarr; Service &amp;rarr; Mapper&lt;/code&gt; 구조를 만들기 시작했고 로그인 흐름의 골격까지 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 학습에서는 다음 부분을 집중해서 확인해야겠다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;1. UserController에서 Service Method 실제 호출

2. UserService Interface와
   Plain / Encryption 구현체 차이

3. Service에서 UserMapper 호출

4. login Mapper 실제 결과 반환

5. UserResponseDTO 구성

6. ResponseEntity를 이용한
   Login 성공 / 실패 응답 처리

7. @Resource / @Qualifier / @Primary 차이

8. Controller Parameter Binding
   @RequestParam
   @PathVariable
   DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 오늘 수업 목표에 있었던 Swagger/OpenAPI:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Tag
@Operation
@Parameter
@ApiResponse&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 이후 Controller API와 연결해서 학습하게 될 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>ioc</category>
      <category>java</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mariadb</category>
      <category>RestController</category>
      <category>springboot</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/51</guid>
      <comments>https://bbbicsan.tistory.com/51#entry51comment</comments>
      <pubDate>Tue, 1 Sep 2026 20:21:10 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 24일차 TIL : DML 실습&amp;middot;쇼핑몰 DB&amp;middot;JDBC&amp;middot;Spring Boot 시작</title>
      <link>https://bbbicsan.tistory.com/50</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 24일차 TIL : DML 실습&amp;middot;쇼핑몰 DB&amp;middot;JDBC&amp;middot;Spring Boot 시작&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b06sU5/dJMcagzZSFB/Vth1OSxM33t44ESIyraewk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b06sU5/dJMcagzZSFB/Vth1OSxM33t44ESIyraewk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b06sU5/dJMcagzZSFB/Vth1OSxM33t44ESIyraewk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb06sU5%2FdJMcagzZSFB%2FVth1OSxM33t44ESIyraewk%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;DML 문제와 쇼핑몰 ERD 기반 종합 SQL 실습을 통해 &lt;code&gt;INSERT / UPDATE / DELETE / JOIN / Subquery&lt;/code&gt;를 복습하고, Java JDBC로 MariaDB의 조회&amp;middot;DTO 매핑까지 직접 구현한 뒤 Spring Initializr를 이용해 &lt;b&gt;Spring Boot + Spring MVC 백엔드 개발 환경&lt;/b&gt;으로 넘어갔다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DML Workshop Q1 ~ Q8
        &amp;darr;
INSERT / UPDATE / DELETE
        &amp;darr;
CREATE TABLE ... SELECT
        &amp;darr;
UPDATE + JOIN
UPDATE + Subquery
        &amp;darr;
쇼핑몰 ERD DDL/DML 실습
        &amp;darr;
customers
orders
products
orderdetail
        &amp;darr;
주문 INSERT
주문상세 INSERT
재고 UPDATE
        &amp;darr;
4개 테이블 JOIN
        &amp;darr;
GROUP BY + SUM
일별 매출 집계
        &amp;darr;
Java JDBC
        &amp;darr;
MariaDB JDBC Driver
Connection
PreparedStatement
ResultSet
DTO Mapping
        &amp;darr;
JDBC UPDATE 준비
PreparedStatement ?
RequestDTO
        &amp;darr;
Spring Boot &amp;amp; Spring MVC
        &amp;darr;
Spring Initializr
        &amp;darr;
Gradle / Java 17
Spring Web MVC
JPA
MyBatis
MariaDB
Lombok
        &amp;darr;
Spring Boot 프로젝트 구조
        &amp;darr;
application.yml
dev / prod Profile
        &amp;darr;
@SpringBootApplication
SpringApplication.run()
        &amp;darr;
Embedded Tomcat
        &amp;darr;
Controller
Service
Repository&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE TABLE ... SELECT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Subquery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JOIN UPDATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ERD&lt;/li&gt;
&lt;li&gt;PK / FK&lt;/li&gt;
&lt;li&gt;Composite Primary Key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JDBC&lt;/li&gt;
&lt;li&gt;MariaDB Driver&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreparedStatement&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResultSet&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DTO&lt;/li&gt;
&lt;li&gt;Builder Pattern&lt;/li&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Spring MVC&lt;/li&gt;
&lt;li&gt;Spring Initializr&lt;/li&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;MariaDB Driver&lt;/li&gt;
&lt;li&gt;Lombok&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spring Profile&lt;/li&gt;
&lt;li&gt;Embedded Tomcat&lt;/li&gt;
&lt;li&gt;Controller / Service / Repository&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. DML 실습 Q1 ~ Q8&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 첫 수업은 이전에 배운 DML을 실제 문제로 다시 구현하는 것으로 시작했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;INSERT
&amp;rarr; 새로운 데이터 추가

UPDATE
&amp;rarr; 기존 데이터 수정

DELETE
&amp;rarr; 기존 데이터 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 문법 문제뿐 아니라 &lt;code&gt;Subquery&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, 문자열 함수 등을 함께 사용하는 문제들이 포함됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Q1 - TB_CLASS_TYPE 데이터 입력&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;TB_CLASS_TYPE&lt;/code&gt;에 여러 행을 한 번에 INSERT했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;INSERT INTO TB_CLASS_TYPE
VALUES
('01', '전공필수'),
('02', '전공선택'),
('03', '교양필수'),
('04', '교양선택'),
('05', '논문지도');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번의 &lt;code&gt;INSERT&lt;/code&gt; 문으로 여러 Row를 저장할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;INSERT INTO
      &amp;darr;
VALUES (...),
       (...),
       (...)
      &amp;darr;
다중행 INSERT&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNEdVl/dJMcagNEAJp/GVJCqoiCjoXWJXpLW97yP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNEdVl/dJMcagNEAJp/GVJCqoiCjoXWJXpLW97yP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNEdVl/dJMcagNEAJp/GVJCqoiCjoXWJXpLW97yP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNEdVl%2FdJMcagNEAJp%2FGVJCqoiCjoXWJXpLW97yP0%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;1109&quot; height=&quot;466&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 1 - Table doesn't exist&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행 과정에서는 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SQL 오류 (1146)

Table 'lgcns.tb_calls_type' doesn't exist&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT 대상 테이블을 찾지 못했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 테이블은:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;TB_CLASS_TYPE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인데 잘못된 테이블명이 사용된 것이 원인이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 존재하는 테이블명을 확인한 후:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;INSERT INTO TB_CLASS_TYPE ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 수정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 오류가 발생하면 가장 먼저:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Database 선택
Table 이름
Column 이름
Alias&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부터 확인하는 습관이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Q2 - 기존 데이터를 이용해 새로운 테이블 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tb_student&lt;/code&gt;의 데이터를 이용해 새로운 학생 일반정보 테이블을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_학생일반정보
SELECT S.STUDENT_NO      AS '학번',
       S.STUDENT_NAME    AS '학생이름',
       S.STUDENT_ADDRESS AS '주소'
FROM tb_student S;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE TABLE
+
SELECT&lt;/code&gt;&lt;/pre&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;pre class=&quot;gams&quot;&gt;&lt;code&gt;기존 테이블 SELECT
        &amp;darr;
조회 결과 구조 생성
        &amp;darr;
새로운 TABLE 생성
        &amp;darr;
조회 데이터까지 함께 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후 &lt;code&gt;588행&lt;/code&gt;의 데이터가 생성된 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ty5Bf/dJMcah6ReP1/D4I1OKDfZTrq8CY8sFXnIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ty5Bf/dJMcah6ReP1/D4I1OKDfZTrq8CY8sFXnIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ty5Bf/dJMcah6ReP1/D4I1OKDfZTrq8CY8sFXnIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fty5Bf%2FdJMcah6ReP1%2FD4I1OKDfZTrq8CY8sFXnIk%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;1360&quot; height=&quot;696&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 2 - Unknown Column&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 과정에서:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 (1054)
Unknown column 'S.STUDENT_NO' in 'SELECT'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류도 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;code&gt;CREATE TABLE ... SELECT&lt;/code&gt;에서도 일반 SELECT와 마찬가지로 &lt;b&gt;테이블 별칭과 컬럼이 올바르게 연결되어 있는지&lt;/b&gt; 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM tb_student S&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;S.STUDENT_NO
S.STUDENT_NAME
S.STUDENT_ADDRESS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 관계를 올바르게 맞춰 정상 생성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Q3 - JOIN 결과로 새로운 테이블 생성&lt;/h1&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_국어국문학과
SELECT S.STUDENT_NO AS '학번',
       S.STUDENT_NAME AS '학생이름',
       CONCAT('19', SUBSTRING(S.STUDENT_SSN, 1, 2)) AS '출생년도',
       P.PROFESSOR_NAME AS '교수이름'
FROM tb_student S
JOIN tb_department D
  ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
JOIN tb_professor P
  ON S.COACH_PROFESSOR_NO = P.PROFESSOR_NO
WHERE D.DEPARTMENT_NAME = '국어국문학과';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개념이 함께 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;학생
+
학과
+
교수
        &amp;darr;
JOIN
        &amp;darr;
국어국문학과만 WHERE
        &amp;darr;
SUBSTRING
        &amp;darr;
출생년도 가공
        &amp;darr;
새 TABLE 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhX2Yq/dJMcahyV3Hy/xhsdftKtLxlYeA0Xvy9ZCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhX2Yq/dJMcahyV3Hy/xhsdftKtLxlYeA0Xvy9ZCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhX2Yq/dJMcahyV3Hy/xhsdftKtLxlYeA0Xvy9ZCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhX2Yq%2FdJMcahyV3Hy%2FxhsdftKtLxlYeA0Xvy9ZCk%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;658&quot; height=&quot;224&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Q4 - 학과 정원 10% 증가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 학과의 정원을 10% 증가시켰다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE tb_department
SET CAPACITY = ROUND(CAPACITY * 1.1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;WHERE&lt;/code&gt;가 없다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE 없음
&amp;rarr; 특정 행이 아니라 전체 행 UPDATE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실행 전에는 항상:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM tb_department;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 대상 범위를 확인하는 것이 안전하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mtKg5/dJMb998G2qI/gXutK2GtrA0aqaiJEAtj00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mtKg5/dJMb998G2qI/gXutK2GtrA0aqaiJEAtj00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mtKg5/dJMb998G2qI/gXutK2GtrA0aqaiJEAtj00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmtKg5%2FdJMb998G2qI%2FgXutK2GtrA0aqaiJEAtj00%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;658&quot; height=&quot;436&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. Q5 - 특정 학생의 주소 수정&lt;/h1&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE tb_student
SET STUDENT_ADDRESS = '서울시 종로구 숭인동 181-21'
WHERE STUDENT_NO = 'A413042';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;code&gt;WHERE&lt;/code&gt;를 이용해 하나의 학생만 정확하게 지정했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;UPDATE
&amp;rarr; 무엇을 수정할 것인가?

SET
&amp;rarr; 어떤 값으로 수정할 것인가?

WHERE
&amp;rarr; 누구를 수정할 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XJN1v/dJMcajcm5CS/RA061VcjtZy0IhCAa2K4OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XJN1v/dJMcajcm5CS/RA061VcjtZy0IhCAa2K4OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XJN1v/dJMcajcm5CS/RA061VcjtZy0IhCAa2K4OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXJN1v%2FdJMcajcm5CS%2FRA061VcjtZy0IhCAa2K4OK%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;559&quot; height=&quot;76&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. Q6 - 전체 학생 주민번호 앞 6자리만 유지&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE tb_student
SET STUDENT_SSN = SUBSTRING(STUDENT_SSN, 1, 6);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SUBSTRING()&lt;/code&gt;을 이용해 주민번호의 앞 6자리만 남겼다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전체 주민번호
      &amp;darr;
SUBSTRING(..., 1, 6)
      &amp;darr;
생년월일 6자리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 &lt;code&gt;WHERE&lt;/code&gt;가 없기 때문에 &lt;b&gt;전체 학생 데이터가 수정&lt;/b&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A34xZ/dJMcad4itCr/gFH0okrikjddh6eaqO7EV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A34xZ/dJMcad4itCr/gFH0okrikjddh6eaqO7EV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A34xZ/dJMcad4itCr/gFH0okrikjddh6eaqO7EV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA34xZ%2FdJMcad4itCr%2FgFH0okrikjddh6eaqO7EV0%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;925&quot; height=&quot;349&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. Q7 - 특정 학생의 특정 과목 평점 수정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건은 여러 테이블에 흩어져 있었다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;학생 이름
&amp;rarr; tb_student

학생 소속 학과
&amp;rarr; tb_department

과목 이름
&amp;rarr; tb_class

평점 / 학기
&amp;rarr; tb_grade&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 두 가지 방식으로 풀었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CASE 01. JOIN UPDATE&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE tb_grade G
JOIN tb_student S
  ON G.STUDENT_NO = S.STUDENT_NO
JOIN tb_department D
  ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
JOIN tb_class C
  ON G.CLASS_NO = C.CLASS_NO
SET G.POINT = 3.5
WHERE S.STUDENT_NAME = '김영훈'
  AND D.DEPARTMENT_NAME = '의학과'
  AND C.CLASS_NAME = '피부생리학'
  AND G.TERM_NO = '200501';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;성적
&amp;rarr; 학생 JOIN
&amp;rarr; 학과 JOIN
&amp;rarr; 과목 JOIN
        &amp;darr;
대상 행 특정
        &amp;darr;
POINT = 3.5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be2yub/dJMcad4itD3/3HfpnYuVkYDx1el9QenLNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be2yub/dJMcad4itD3/3HfpnYuVkYDx1el9QenLNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be2yub/dJMcad4itD3/3HfpnYuVkYDx1el9QenLNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe2yub%2FdJMcad4itD3%2F3HfpnYuVkYDx1el9QenLNK%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;717&quot; height=&quot;230&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CASE 02. Subquery UPDATE&lt;/h2&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE tb_grade
SET POINT = 3.5
WHERE STUDENT_NO IN (
    SELECT STUDENT_NO
    FROM TB_STUDENT
    WHERE STUDENT_NAME = '김영훈'
)
AND CLASS_NO = (
    SELECT CLASS_NO
    FROM TB_CLASS
    WHERE CLASS_NAME = '피부생리학'
)
AND TERM_NO = '200501';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문제를:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JOIN으로 대상 탐색
vs
Subquery로 필요한 Key 탐색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식으로 구현할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn7S6G/dJMcadb6YkU/CHCxFO0OEFapqOkHlAtUl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn7S6G/dJMcadb6YkU/CHCxFO0OEFapqOkHlAtUl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn7S6G/dJMcadb6YkU/CHCxFO0OEFapqOkHlAtUl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn7S6G%2FdJMcadb6YkU%2FCHCxFO0OEFapqOkHlAtUl1%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;630&quot; height=&quot;269&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. Q8 - 휴학생의 성적 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴학생의 성적 데이터를 처리하는 문제에서는 두 방법을 비교했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 1. POINT만 NULL 처리&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE tb_grade
SET POINT = NULL
WHERE STUDENT_NO IN (
    SELECT STUDENT_NO
    FROM tb_student
    WHERE ABSENCE_YN = 'Y'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;성적 Row
&amp;rarr; 유지

POINT
&amp;rarr; NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgIL37/dJMcag0Zsg9/kLb0Afk1Vimv444IYDefGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgIL37/dJMcag0Zsg9/kLb0Afk1Vimv444IYDefGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgIL37/dJMcag0Zsg9/kLb0Afk1Vimv444IYDefGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgIL37%2FdJMcag0Zsg9%2FkLb0Afk1Vimv444IYDefGk%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;466&quot; height=&quot;137&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 2. 성적 Row 삭제&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DELETE FROM tb_grade
WHERE STUDENT_NO IN (
    SELECT S.STUDENT_NO
    FROM tb_student S
    WHERE S.ABSENCE_YN = 'Y'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;성적 Row 자체
&amp;rarr; 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식의 차이를 명확하게 볼 수 있었다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE POINT = NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;행 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;행 자체 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XHuqx/dJMcagNEA5Q/V5kjWd0fkaGHGIzVKbofoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XHuqx/dJMcagNEA5Q/V5kjWd0fkaGHGIzVKbofoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XHuqx/dJMcagNEA5Q/V5kjWd0fkaGHGIzVKbofoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXHuqx%2FdJMcagNEA5Q%2FV5kjWd0fkaGHGIzVKbofoK%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;441&quot; height=&quot;124&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. DML에서 WHERE의 중요성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습 중 여러 번 확인한 핵심이었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE + WHERE
&amp;rarr; 특정 Row 수정

UPDATE without WHERE
&amp;rarr; 전체 Row 수정

DELETE + WHERE
&amp;rarr; 특정 Row 삭제

DELETE without WHERE
&amp;rarr; 전체 Row 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실제 데이터 변경 전에는:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SELECT ...
WHERE ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 먼저 대상 행을 확인한 후 같은 조건을 &lt;code&gt;UPDATE / DELETE&lt;/code&gt;에 적용하는 것이 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 쇼핑몰 ERD 기반 DDL/DML 종합 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 지난 시간 마지막에 문제만 제시됐던 &lt;b&gt;쇼핑몰 ERD 실습을 실제로 구현&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CUSTOMERS
   1
   &amp;darr;
   N
ORDERS
   1
   &amp;darr;
   N
ORDERDETAIL
   N
   &amp;uarr;
   1
PRODUCTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ORDERS&lt;/code&gt;와 &lt;code&gt;PRODUCTS&lt;/code&gt;의 N:M 관계를 &lt;code&gt;ORDERDETAIL&lt;/code&gt;이라는 중간 테이블을 통해 해소한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. ORDERDETAIL과 복합 Primary Key&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE orderdetail (
    orderno INT,
    pno INT,
    qty INT DEFAULT 0,
    cost INT DEFAULT 0,

    PRIMARY KEY (orderno, pno),

    FOREIGN KEY (orderno)
        REFERENCES orders(orderno),

    FOREIGN KEY (pno)
        REFERENCES products(pno)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;PRIMARY KEY (orderno, pno)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;ORDERNO 하나만으로는
&amp;rarr; 어떤 상품인지 식별 불가

PNO 하나만으로는
&amp;rarr; 어떤 주문인지 식별 불가

ORDERNO + PNO
&amp;rarr; 하나의 주문 상세 항목 식별&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WWTqg/dJMcaa0QW3c/KIpSQLkr0jGUPa3Av4PK10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WWTqg/dJMcaa0QW3c/KIpSQLkr0jGUPa3Av4PK10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WWTqg/dJMcaa0QW3c/KIpSQLkr0jGUPa3Av4PK10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWWTqg%2FdJMcaa0QW3c%2FKIpSQLkr0jGUPa3Av4PK10%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;438&quot; height=&quot;179&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. Customers 데이터 입력&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;INSERT INTO customers(cno, cname, address, email, phone)
VALUES
(101, '김철수', '서울 강남구', 'cskim@naver.com', '899-6666'),
(102, '이영희', '부산 서면', 'yhlee@empal.com', '355-8882'),
(103, '최진국', '제주 동광양', 'jkchoi@gmail.com', '852-5764'),
(104, '강준호', '강릉 홍제동', 'jhkang@hanmail.com', '559-7777'),
(105, '민병국', '대전 전민동', 'bgmin@hotmail.com', '559-8741'),
(106, '오민수', '광주 북구', 'msoh@microsoft.com', '542-9988');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후 6명의 고객 데이터가 정상적으로 저장된 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy9m7S/dJMb99Vaou5/v8y7yUQ1MBQvzWe14TgpJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy9m7S/dJMb99Vaou5/v8y7yUQ1MBQvzWe14TgpJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy9m7S/dJMb99Vaou5/v8y7yUQ1MBQvzWe14TgpJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy9m7S%2FdJMb99Vaou5%2Fv8y7yUQ1MBQvzWe14TgpJK%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;759&quot; height=&quot;490&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. 주문 생성 시 고객번호를 Subquery로 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김철수의 주문을 입력했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO orders(
    orderno,
    orderdate,
    address,
    phone,
    status,
    cno
)
VALUES(
    1,
    DATE_SUB(CURDATE(), INTERVAL 3 DAY),
    '서울 강남구',
    '899-6666',
    '결제완료',
    (
        SELECT CNO
        FROM customers
        WHERE CNAME = '김철수'
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CNO = 101&lt;/code&gt;을 직접 넣지 않고:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;김철수
 &amp;darr;
Subquery
 &amp;darr;
CNO = 101
 &amp;darr;
orders INSERT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 구현했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br9qlE/dJMcaaGFa7w/KySLUSs9SrzYqs75mmGKY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br9qlE/dJMcaaGFa7w/KySLUSs9SrzYqs75mmGKY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br9qlE/dJMcaaGFa7w/KySLUSs9SrzYqs75mmGKY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr9qlE%2FdJMcaaGFa7w%2FKySLUSs9SrzYqs75mmGKY0%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;590&quot; height=&quot;399&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. 상품 재고 차감&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김철수가 삼양라면 50개를 주문한 후:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE products
SET STOCK = STOCK - 50
WHERE PNO = 1001;&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;삼양라면
200 &amp;rarr; 150&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SET STOCK = 150&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 변경하기보다:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SET STOCK = STOCK - 주문수량&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 처리했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/064wU/dJMcabyEHo6/O1Zega5N44WUu2sm47IVQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/064wU/dJMcabyEHo6/O1Zega5N44WUu2sm47IVQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/064wU/dJMcabyEHo6/O1Zega5N44WUu2sm47IVQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F064wU%2FdJMcabyEHo6%2FO1Zega5N44WUu2sm47IVQK%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;522&quot; height=&quot;349&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. 한 주문에 여러 상품 입력&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이영희는 한 주문에서:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;새우깡 100개
월드콘 150개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 주문했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orders&lt;/code&gt;에는 주문 자체가 1건 들어간다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO orders(
    orderno,
    orderdate,
    address,
    phone,
    status,
    cno
)
VALUES(
    2,
    DATE_SUB(CURDATE(), INTERVAL 2 DAY),
    '부산 수영구',
    '337-5000',
    '결제완료',
    (
        SELECT CNO
        FROM customers
        WHERE CNAME = '이영희'
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zai62/dJMcagteSr6/QtxzHEwuO0XDgw6TiR3a8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zai62/dJMcagteSr6/QtxzHEwuO0XDgw6TiR3a8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zai62/dJMcagteSr6/QtxzHEwuO0XDgw6TiR3a8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZai62%2FdJMcagteSr6%2FQtxzHEwuO0XDgw6TiR3a8k%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;580&quot; height=&quot;352&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;orderdetail&lt;/code&gt;에는 상품별로 2건을 입력한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO orderdetail(orderno, pno, qty, cost)
VALUES
(
    2,
    (
        SELECT PNO
        FROM products
        WHERE PNAME = '새우깡'
    ),
    100,
    1500
),
(
    2,
    (
        SELECT PNO
        FROM products
        WHERE PNAME = '월드콘'
    ),
    150,
    2000
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;orders
&amp;rarr; 주문 1건

orderdetail
&amp;rarr; 주문 상품 N건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 관계가 실제 SQL에서 확인됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dx4I5/dJMcadpIaEH/TT0h3NepYeWIgOnWNyMs90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dx4I5/dJMcadpIaEH/TT0h3NepYeWIgOnWNyMs90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dx4I5/dJMcadpIaEH/TT0h3NepYeWIgOnWNyMs90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDx4I5%2FdJMcadpIaEH%2FTT0h3NepYeWIgOnWNyMs90%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;497&quot; height=&quot;422&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. JOIN UPDATE를 이용한 재고 차감&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새우깡과 월드콘의 주문수량을 &lt;code&gt;orderdetail&lt;/code&gt;에서 가져와 재고를 차감했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE products P
JOIN orderdetail D USING(pno)
SET stock = P.stock - D.qty
WHERE pno = (
    SELECT PNO
    FROM products
    WHERE PNAME = '새우깡'
);&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;새우깡
500 &amp;rarr; 400

월드콘
350 &amp;rarr; 200&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8CBhc/dJMcaaGFbgg/VklHR5upPB3kzzZPB0knH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8CBhc/dJMcaaGFbgg/VklHR5upPB3kzzZPB0knH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8CBhc/dJMcaaGFbgg/VklHR5upPB3kzzZPB0knH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8CBhc%2FdJMcaaGFbgg%2FVklHR5upPB3kzzZPB0knH0%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;560&quot; height=&quot;491&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. 주문 단위로 여러 상품 재고 일괄 차감&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오민수의 3번 주문에는:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;빼빼로 100개
코카콜라 50개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 상품을 각각 UPDATE하지 않고 주문번호를 이용해 한 번에 처리했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UPDATE products P
JOIN orderdetail OD
  ON P.pno = OD.pno
SET P.stock = P.stock - OD.qty
WHERE OD.orderno = 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;빼빼로
700 &amp;rarr; 600

코카콜라
550 &amp;rarr; 500&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 주문 상세정보에 있는 상품 수만큼 재고를 한 번에 변경할 수 있다는 점이 인상적이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIqANl/dJMcagzZTQQ/vkmHg8B4L1Sp9ZJMSx3O9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIqANl/dJMcagzZTQQ/vkmHg8B4L1Sp9ZJMSx3O9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIqANl/dJMcagzZTQQ/vkmHg8B4L1Sp9ZJMSx3O9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIqANl%2FdJMcagzZTQQ%2FvkmHg8B4L1Sp9ZJMSx3O9k%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;815&quot; height=&quot;668&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. 전체 주문 목록 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 주문 정보를 조회하기 위해 4개 테이블을 JOIN했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SELECT O.orderdate,
       C.cname,
       O.address,
       O.phone,
       O.status,
       P.pname,
       D.cost,
       D.qty,
       D.cost * D.qty AS 'cost*qty'
FROM orders O
JOIN orderdetail D
  ON O.orderno = D.orderno
JOIN products P
  ON D.pno = P.pno
JOIN customers C
  ON O.cno = C.cno;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;CUSTOMERS
    &amp;darr;
ORDERS
    &amp;darr;
ORDERDETAIL
    &amp;darr;
PRODUCTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;김철수 | 삼양라면 |  50,000

이영희 | 새우깡   | 150,000
이영희 | 월드콘   | 300,000

오민수 | 빼빼로   | 200,000
오민수 | 코카콜라 |  90,000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BvTjW/dJMcaa0QXkE/0ZuDzT3jId55K8jVvXJmmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BvTjW/dJMcaa0QXkE/0ZuDzT3jId55K8jVvXJmmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BvTjW/dJMcaa0QXkE/0ZuDzT3jId55K8jVvXJmmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBvTjW%2FdJMcaa0QXkE%2F0ZuDzT3jId55K8jVvXJmmk%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;698&quot; height=&quot;331&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. 일별 매출 집계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 주문정보에서 날짜별 매출을 계산했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SELECT O.orderdate,
       SUM(D.cost * D.qty) AS 'sum(cost*qty)'
FROM orders O
JOIN orderdetail D
  ON O.orderno = D.orderno
JOIN products P
  ON D.pno = P.pno
JOIN customers C
  ON O.cno = C.cno
GROUP BY O.orderdate;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2026-08-28 &amp;rarr;  50,000
2026-08-29 &amp;rarr; 450,000
2026-08-30 &amp;rarr; 290,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 8월 29일 매출은:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;새우깡
1,500 &amp;times; 100
= 150,000

월드콘
2,000 &amp;times; 150
= 300,000

합계
= 450,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 계산된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTjTTG/dJMcah6Rfgp/lzYzLSA7hWQTfAvNANHRKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTjTTG/dJMcah6Rfgp/lzYzLSA7hWQTfAvNANHRKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTjTTG/dJMcah6Rfgp/lzYzLSA7hWQTfAvNANHRKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTjTTG%2FdJMcah6Rfgp%2FlzYzLSA7hWQTfAvNANHRKk%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;470&quot; height=&quot;348&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. 신규 상품 추가&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO products
VALUES(
    1007,
    '목캔디',
    3000,
    500
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후 상품 테이블은 7건이 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deEboH/dJMcafA7qy2/YgXHh9TkSleFPU8aFikcNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deEboH/dJMcafA7qy2/YgXHh9TkSleFPU8aFikcNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deEboH/dJMcafA7qy2/YgXHh9TkSleFPU8aFikcNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeEboH%2FdJMcafA7qy2%2FYgXHh9TkSleFPU8aFikcNK%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;1022&quot; height=&quot;633&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. 최진국의 목캔디 주문&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 주문까지 추가했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO orders(
    orderno,
    orderdate,
    address,
    phone,
    status,
    cno
)
VALUES(
    4,
    CURDATE(),
    '제주 동광양',
    '352-4657',
    '결제완료',
    (
        SELECT cno
        FROM customers
        WHERE cname = '최진국'
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 상세:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;INSERT INTO orderdetail
VALUES(
    4,
    1007,
    200,
    3000
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재고 차감:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE products
SET stock = stock - 200
WHERE pno = 1007;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;목캔디
500 &amp;rarr; 300&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSH9MZ/dJMcagmrgrV/3a4hGCkhjXCMkPoraoymK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSH9MZ/dJMcagmrgrV/3a4hGCkhjXCMkPoraoymK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSH9MZ/dJMcagmrgrV/3a4hGCkhjXCMkPoraoymK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSH9MZ%2FdJMcagmrgrV%2F3a4hGCkhjXCMkPoraoymK0%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;700&quot; height=&quot;492&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. 쇼핑몰 DML 실습의 전체 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습을 실제 서비스 흐름처럼 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;고객 확인
     &amp;darr;
orders INSERT
     &amp;darr;
orderdetail INSERT
     &amp;darr;
products STOCK UPDATE
     &amp;darr;
JOIN으로 주문결과 확인
     &amp;darr;
GROUP BY로 매출 집계&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 생성
주문 상세 생성
재고 차감&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 논리적으로 하나의 작업 단위라는 점도 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Transaction을 사용할 때 이 작업들을 하나로 묶는 이유와도 연결될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. Java JDBC&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후에는 SQL에서 Java 애플리케이션으로 넘어가 &lt;b&gt;JDBC를 이용해 MariaDB에 직접 접근&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 정리한 JDBC 절차는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. Driver Loading
2. Connection
3. Statement / PreparedStatement
4. execute
5. ResultSet
6. close&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. JDBC Driver Loading&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MariadbDao&lt;/code&gt;에서 MariaDB JDBC Driver를 로딩했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public MariadbDao() {
    try {
        Class.forName(&quot;org.mariadb.jdbc.Driver&quot;);
        System.out.println(&quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; driver loading ok!!&quot;);
    } catch(Exception e) {
        e.printStackTrace();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 3 - ClassNotFoundException&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행했을 때:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java.lang.ClassNotFoundException:
org.mariadb.jdbc.Driver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 코드에서는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Class.forName(&quot;org.mariadb.jdbc.Driver&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 호출했지만 프로젝트의 classpath에 MariaDB JDBC Driver가 존재하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB JDBC Driver 라이브러리를 추가했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 실행하자:&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; driver loading ok!!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 출력됐다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Driver Class 없음
        &amp;darr;
ClassNotFoundException
        &amp;darr;
MariaDB JDBC Driver 추가
        &amp;darr;
Driver Loading 성공&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. JDBC Connection&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연결 정보를 상수로 정의하는 구조를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;private static final String DRIVER   = &quot;...&quot;;
private static final String USER     = &quot;...&quot;;
private static final String PASSWORD = &quot;...&quot;;
private static final String URL      = &quot;...&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제 Connection을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;conn = DriverManager.getConnection(
    URL,
    USER,
    PASSWORD
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;URL
USER
PASSWORD
   &amp;darr;
DriverManager
   &amp;darr;
Connection&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. DAO에서 부서 목록 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;departments()&lt;/code&gt; 메서드를 구현했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public List&amp;lt;DepartmentResponseDTO&amp;gt; departments() {

    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rset = null;

    String sql = &quot;&quot;&quot;
        select *
        from department
        &quot;&quot;&quot;;

    List&amp;lt;DepartmentResponseDTO&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. PreparedStatement와 executeQuery()&lt;/h1&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;pstmt = conn.prepareStatement(sql);
rset = pstmt.executeQuery();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Connection
&amp;rarr; DB 연결

PreparedStatement
&amp;rarr; SQL 준비

executeQuery()
&amp;rarr; SELECT 실행

ResultSet
&amp;rarr; 결과 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. ResultSet &amp;rarr; DTO Mapping&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while(rset.next()) {
    list.add(
        DepartmentResponseDTO.builder()
            .dept_id(rset.getString(1))
            .dept_name(rset.getString(2))
            .loc_id(rset.getString(3))
            .build()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ResultSet Row
      &amp;darr;
getString()
      &amp;darr;
DepartmentResponseDTO
      &amp;darr;
ArrayList&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 Java 수업에서 학습한:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;DTO
Builder
List
Stream&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 JDBC와 연결되는 것도 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. DTO List 출력&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JdbcApp&lt;/code&gt;에서는 DAO가 반환한 리스트를 출력했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MariadbDao dao = new MariadbDao();

List&amp;lt;DepartmentResponseDTO&amp;gt; list = dao.departments();

list.stream()
    .forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서는 실제 DTO가 출력됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DepartmentResponseDTO(
    dept_id=50,
    dept_name=해외영업1팀,
    loc_id=U1
)

DepartmentResponseDTO(
    dept_id=60,
    dept_name=기술지원팀,
    loc_id=OT
)

DepartmentResponseDTO(
    dept_id=80,
    dept_name=해외영업2팀,
    loc_id=A2
)

DepartmentResponseDTO(
    dept_id=90,
    dept_name=전략기획팀,
    loc_id=A3
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 최종 데이터 흐름은:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;MariaDB
   &amp;darr;
JDBC SELECT
   &amp;darr;
ResultSet
   &amp;darr;
DTO
   &amp;darr;
List&amp;lt;DTO&amp;gt;
   &amp;darr;
Stream
   &amp;darr;
Console&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 이어졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. JDBC DML - UPDATE 준비&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 이후에는 JDBC로 &lt;code&gt;UPDATE&lt;/code&gt;를 실행하는 메서드를 만들기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL:&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;String sql = &quot;&quot;&quot;
    update department
    set dept_name = ?
    where dept_id = ?
    &quot;&quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;?&lt;/code&gt;는 실제 값을 나중에 넣기 위한 Placeholder이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;첫 번째 ?
&amp;rarr; 변경할 DEPT_NAME

두 번째 ?
&amp;rarr; 대상 DEPT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. Request DTO로 수정값 전달&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JdbcApp&lt;/code&gt;에서 수정 요청 DTO를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DepartmentRequestDTO request =
    DepartmentRequestDTO.builder()
        .dept_id(&quot;90&quot;)
        .dept_name(&quot;교육팀&quot;)
        .build();

int flag = dao.update(request);

System.out.println(&quot;result flag : &quot; + flag);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;RequestDTO
dept_id = 90
dept_name = 교육팀
       &amp;darr;
DAO
       &amp;darr;
PreparedStatement
       &amp;darr;
UPDATE
       &amp;darr;
영향받은 행 수 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT와 DML 실행 차이도 연결됐다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
&amp;rarr; executeQuery()
&amp;rarr; ResultSet

INSERT / UPDATE / DELETE
&amp;rarr; executeUpdate()
&amp;rarr; int&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 수업 화면에서는 UPDATE 메서드를 기존 SELECT 코드를 기반으로 수정하는 중간 단계까지 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. Spring Boot &amp;amp; Spring MVC 시작&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 직접 구현한 뒤 수업은 &lt;b&gt;Spring Boot &amp;amp; Spring MVC&lt;/b&gt;로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 생성은:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;https://start.spring.io/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 VS Code에서:&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;Ctrl + Shift + P
&amp;rarr; Spring Initializr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 진행할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. Spring Initializr&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code Command Palette에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Spring Initializr: Create a Gradle Project...
Spring Initializr: Add Starters...
Spring Initializr: Create a Gradle Project with Kotlin DSL...
Spring Initializr: Create a Maven Project...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 기능을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;35. Spring Boot Dependency&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서 선택한 의존성은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot DevTools
Spring Configuration Processor
Spring Web
Spring Data JPA
MyBatis Framework
MariaDB Driver
Lombok&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 역할:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dependency&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot DevTools&lt;/td&gt;
&lt;td&gt;개발 편의 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration Processor&lt;/td&gt;
&lt;td&gt;설정 프로퍼티 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Web MVC&lt;/td&gt;
&lt;td&gt;Web / REST / MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JPA&lt;/td&gt;
&lt;td&gt;ORM 기반 DB 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyBatis Framework&lt;/td&gt;
&lt;td&gt;SQL Mapper 기반 DB 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MariaDB Driver&lt;/td&gt;
&lt;td&gt;MariaDB 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lombok&lt;/td&gt;
&lt;td&gt;반복 Java 코드 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;36. build.gradle&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 프로젝트는 실제로 다음 환경을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Spring Boot 4.0.8
Java 17
Gradle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 설정:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.8'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example.demo'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 의존성:&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:4.0.1'

runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

developmentOnly 'org.springframework.boot:spring-boot-devtools'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Initializr에서 선택했던 Starter가 실제 &lt;code&gt;build.gradle&lt;/code&gt; 의존성으로 들어가는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNOIIS/dJMcadpIaRU/whch8jxPrGU3aWtod7zVsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNOIIS/dJMcadpIaRU/whch8jxPrGU3aWtod7zVsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNOIIS/dJMcadpIaRU/whch8jxPrGU3aWtod7zVsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNOIIS%2FdJMcadpIaRU%2Fwhch8jxPrGU3aWtod7zVsk%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;881&quot; height=&quot;826&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;37. Spring Boot 프로젝트 구조&lt;/h1&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;src
├─ main
│  ├─ java
│  │  └─ Spring Java Code
│  │
│  └─ resources
│     ├─ static
│     ├─ templates
│     ├─ application.yml
│     ├─ application-dev.yml
│     └─ application-prod.yml
│
└─ test

build.gradle
gradlew
gradlew.bat
settings.gradle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 역할:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;src/main/java
&amp;rarr; Spring Application Code

resources/static
&amp;rarr; 정적 리소스
&amp;rarr; HTML / CSS / JS / Image

application.yml
&amp;rarr; Spring 설정

build.gradle
&amp;rarr; Build + Dependency 관리&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;38. Spring Profile&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경과 운영 환경의 설정을 분리했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;application.yml
&amp;rarr; 공통 설정

application-dev.yml
&amp;rarr; 개발 환경

application-prod.yml
&amp;rarr; 운영 환경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application.yml&lt;/code&gt;에서는:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;spring:
  profiles:
    default: dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 기본 환경을 &lt;code&gt;dev&lt;/code&gt;로 설정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;39. application-prod.yml&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  config:
    activate:
      on-profile: prod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;prod Profile 활성화
        &amp;darr;
application-prod.yml 적용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;공통 환경설정
+
개발 환경설정
+
운영 환경설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 분리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;40. Spring Boot Application 진입점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 시작점은 다음 클래스였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(
            DemoApplication.class,
            args
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;main()
   &amp;darr;
SpringApplication.run()
   &amp;darr;
Spring Boot Application Context
   &amp;darr;
Bean / Configuration Loading
   &amp;darr;
Embedded Tomcat
   &amp;darr;
Web Server Start&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 실행 로그에서도:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Spring Boot (v4.0.8)
Starting DemoApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 출력되며 정상적으로 애플리케이션이 기동되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;41. Embedded Tomcat&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 중요한 특징 중 하나로 내장 Tomcat을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방식은:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Application
     &amp;darr;
WAR
     &amp;darr;
외부 Tomcat / WebLogic
     &amp;darr;
Deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조라면 Spring Boot는:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot Application
+
Embedded Tomcat
     &amp;darr;
Executable JAR
     &amp;darr;
직접 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;42. Spring Boot Build와 배포&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 주석에서:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;React
&amp;rarr; npm start

Spring Boot Build
&amp;rarr; ./gradlew bootJar

Build Result
&amp;rarr; xxxx.jar

Deploy
&amp;rarr; JAR 실행
&amp;rarr; Embedded Tomcat Start&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Gradle의 &lt;code&gt;bootJar&lt;/code&gt;를 통해 &lt;b&gt;실행 가능한 Spring Boot JAR&lt;/b&gt;를 만들 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;43. Spring MVC 기본 계층&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트에서는 앞으로 다음과 같이 역할을 나누는 구조를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
React / Browser
       &amp;darr; HTTP
Controller
       &amp;darr;
Service
       &amp;darr;
Repository
       &amp;darr;
Database&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 역할:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
&amp;rarr; HTTP Request / Response

Service
&amp;rarr; Business Logic

Repository
&amp;rarr; Database Access&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 아직 각 계층을 실제 코드로 구현하기보다 &lt;b&gt;Spring Boot 프로젝트의 기본 구조와 역할을 이해하는 단계&lt;/b&gt;까지 진행했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;44. JDBC에서 Spring Boot로 넘어가는 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업에서 가장 자연스럽게 연결된 부분이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 JDBC에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Driver Loading
Connection
PreparedStatement
executeQuery
ResultSet
DTO Mapping
close&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 모두 개발자가 직접 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Boot 프로젝트에는:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Spring Data JPA
MyBatis
MariaDB Driver&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;직접 JDBC
        &amp;darr;
Spring Repository
        &amp;darr;
JPA / MyBatis
        &amp;darr;
DB Access 추상화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 발전하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 먼저 직접 구현해봤기 때문에 Spring이 어떤 반복 작업을 대신 처리해주는지 더 명확하게 비교할 수 있을 것 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 잘못된 테이블명으로 INSERT&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SQL 오류 1146
Table doesn't exist&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 &lt;code&gt;TB_CLASS_TYPE&lt;/code&gt; 테이블명을 확인하여 수정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. CREATE TABLE SELECT에서 컬럼 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Unknown column 'S.STUDENT_NO'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 과정&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;FROM 절 확인
&amp;rarr; Alias 확인
&amp;rarr; SELECT 컬럼 확인
&amp;rarr; CTAS 재실행
&amp;rarr; 588행 생성&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. WHERE가 없는 UPDATE&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE tb_department
SET CAPACITY = ROUND(CAPACITY * 1.1);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE tb_student
SET STUDENT_SSN = SUBSTRING(STUDENT_SSN, 1, 6);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 쿼리 모두 WHERE가 없기 때문에 전체 행이 변경된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE / DELETE 실행 전
&amp;rarr; 같은 WHERE를 적용한 SELECT로
   대상 Row를 먼저 확인하는 습관이 중요하다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 같은 UPDATE를 JOIN과 Subquery로 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 성적을 수정하는 문제를:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;JOIN UPDATE
vs
Subquery UPDATE&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;SQL은 같은 결과를 여러 구조로 만들 수 있으며 문제의 관계와 가독성에 따라 적절한 방식을 선택할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. JDBC Driver ClassNotFoundException&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ClassNotFoundException:
org.mariadb.jdbc.Driver&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB JDBC Driver가 Java classpath에 없었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC Driver를 추가했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증&lt;/h3&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; driver loading ok!!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 SQL에서 Spring Boot까지 범위가 크게 확장된 날이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오전에는 기존에 배운:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DDL
DML
JOIN
Subquery
GROUP BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실제 쇼핑몰 ERD와 연결해 종합적으로 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 주문이라는 하나의 기능도 데이터베이스 내부에서는:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;orders
orderdetail
products
customers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 테이블의 관계로 이루어진다는 점을 실제 INSERT와 UPDATE를 통해 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Java JDBC에서는 SQL에서 조회한 데이터를 Java 객체로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Database Row
&amp;rarr; ResultSet
&amp;rarr; DTO
&amp;rarr; List
&amp;rarr; Stream&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 흐름을 직접 구현하면서 DB 데이터가 Java 애플리케이션으로 들어오는 과정을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에는 Spring Boot 프로젝트를 생성하면서 기존 JDBC 방식에서 한 단계 더 높은 수준의 백엔드 개발 환경으로 넘어갔다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 크게 연결된 부분은 &lt;b&gt;DB &amp;rarr; JDBC &amp;rarr; Spring Boot&lt;/b&gt;의 흐름이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 SQL만 공부할 때는 DB 내부에서 쿼리가 실행되는 것만 생각했지만 JDBC를 통해:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Java
&amp;rarr; SQL 전달
&amp;rarr; MariaDB
&amp;rarr; ResultSet
&amp;rarr; Java DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 실제 애플리케이션과 DB가 어떻게 통신하는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JDBC에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Connection
PreparedStatement
ResultSet
Exception Handling
Resource Close&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 직접 작성해야 한다는 점도 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Spring Boot 프로젝트에:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;JPA
MyBatis
MariaDB Driver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 추가하는 것을 보면서 앞으로 Spring이 이러한 DB 접근 코드를 어떤 방식으로 추상화할지 연결해서 공부할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Spring Boot 자체도 단순 라이브러리가 아니라:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Configuration
Profile
Embedded Tomcat
Gradle Build
Executable JAR
MVC Layer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 백엔드 애플리케이션 실행 전반을 구성한다는 것을 처음 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 수업에서는 현재 생성한 Spring Boot 프로젝트를 기준으로 실제 Spring MVC 구조를 구현할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복습할 흐름은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser / React
        &amp;darr;
HTTP Request
        &amp;darr;
Embedded Tomcat
        &amp;darr;
Controller
        &amp;darr;
Service
        &amp;darr;
Repository
        &amp;darr;
JPA / MyBatis
        &amp;darr;
MariaDB&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;@SpringBootApplication

Controller
Service
Repository

Dependency Injection

application.yml
Profile

Spring Web MVC

Spring Data JPA
MyBatis

MariaDB Connection

JDBC 직접 구현
vs
Spring DB 접근&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 직접 만든 JDBC 코드와 앞으로 구현할 Spring Repository 코드를 비교하면서 &lt;b&gt;Spring이 어떤 반복 작업을 대신 처리해주는지&lt;/b&gt; 이해하는 것이 다음 학습의 핵심이 될 것 같다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>DML</category>
      <category>jdbc</category>
      <category>JPA</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>springboot</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/50</guid>
      <comments>https://bbbicsan.tistory.com/50#entry50comment</comments>
      <pubDate>Mon, 31 Aug 2026 18:21:58 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #4] 금융 업무 정합성을 지키는 ERD&amp;middot;PostgreSQL&amp;middot;Flyway 설계</title>
      <link>https://bbbicsan.tistory.com/49</link>
      <description>&lt;div style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all;&quot;&gt;&lt;!-- 시리즈 배지 --&gt;
&lt;div style=&quot;display: inline-block; margin-bottom: 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background-color: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold; letter-spacing: 0.2px;&quot;&gt;FinGuardOps Project Series #4&lt;/div&gt;
&lt;!-- 도입부 --&gt;
&lt;p style=&quot;font-size: 18px; line-height: 1.9;&quot; data-ke-size=&quot;size16&quot;&gt;지난 글에서는 거래 처리 상태, 위험 등급, 위험 대응 결과, 사건 진행 상태와 최종 판정, 멱등 처리 상태를 서로 분리해 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 애플리케이션의 Enum과 Service 검증만으로는 금융 업무 데이터의 정합성을 끝까지 보장하기 어렵습니다. 여러 Pod가 동시에 같은 요청을 처리하거나, 잘못된 SQL과 운영 실수가 애플리케이션 검증을 우회할 수도 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 &lt;code&gt;transactionId&lt;/code&gt;가 두 번 저장되거나, 하나의 거래에 같은 분석 버전이 중복 생성되거나, 최종 판정이 없는 사건이 &lt;code&gt;CLOSED&lt;/code&gt; 상태로 저장된다면 애플리케이션의 상태 모델과 실제 데이터가 서로 다른 이야기를 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FinGuardOps에서는 ERD를 단순한 테이블 목록이 아니라 &lt;b&gt;업무 규칙과 데이터 소유권을 관계형 모델로 표현하는 설계도&lt;/b&gt;로 정의했습니다. Spring Boot의 검증과 PostgreSQL의 제약을 중첩하고, Flyway로 배포 중에도 호환 가능한 스키마 확장 경로를 관리하는 것이 핵심입니다.&lt;/p&gt;
&lt;!-- 핵심 설계 원칙 --&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;FinGuardOps에서 PostgreSQL은 단순 저장소가 아니라, 거래&amp;middot;탐지&amp;middot;사건의 관계와 허용 가능한 상태 조합을 보조 검증하는 &lt;b&gt;마지막 정합성 방어선&lt;/b&gt;입니다.&lt;/blockquote&gt;
&lt;!--
  이미지 1 삽입 위치

  파일명:
  finguardops_핵심_도메인_ERD.png

  대체 텍스트:
  거래·행동·멱등성, 탐지·정책, 사건·감사, AI 운영의 네 영역으로 구성된 FinGuardOps 핵심 도메인 ERD
--&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_핵심_도메인_ERD.png&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CVmB2/dJMcah6QHXq/ZFKp4e8RbiI0geLkcmV1b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CVmB2/dJMcah6QHXq/ZFKp4e8RbiI0geLkcmV1b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CVmB2/dJMcah6QHXq/ZFKp4e8RbiI0geLkcmV1b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCVmB2%2FdJMcah6QHXq%2FZFKp4e8RbiI0geLkcmV1b0%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;1800&quot; height=&quot;1200&quot; data-filename=&quot;finguardops_핵심_도메인_ERD.png&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;margin-top: 8px; color: #777777; font-size: 14px; line-height: 1.6; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;거래 접수부터 탐지&amp;middot;사건&amp;middot;감사&amp;middot;AI 운영까지의 데이터 관계를 네 영역으로 분리한 FinGuardOps 핵심 도메인 ERD&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 18px 21px; border-left: 5px solid #666666; border-radius: 0 12px 12px 0; background-color: #f7f7f7; color: #444444;&quot; data-ke-style=&quot;style1&quot;&gt;이번 글은 특정 날짜의 테이블 구현 현황을 나열하는 글이 아니라, 상태 모델을 PostgreSQL에 어떻게 옮기고 Cloud Native 배포 환경에서 스키마를 어떻게 안전하게 확장할 것인지 설명합니다.&lt;/blockquote&gt;
&lt;!-- 목차 --&gt;
&lt;div style=&quot;margin: 38px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fffafb; box-shadow: 0 4px 14px rgba(122, 31, 61, 0.06);&quot;&gt;
&lt;h2 id=&quot;table-of-contents&quot; style=&quot;margin: 0 0 16px 0; color: #7a1f3d; font-size: 23px; border: none;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#database-questions&quot;&gt;  상태 모델을 데이터베이스로 옮길 때의 설계 질문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#modeling-principles&quot;&gt;  FinGuardOps의 데이터 모델링 원칙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#domain-erd&quot;&gt; ️ 전체 도메인 ERD와 데이터 영역&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#transaction-behavior-idempotency&quot;&gt;  거래&amp;middot;행동 이벤트&amp;middot;멱등성 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#detection-rule&quot;&gt;  탐지 결과&amp;middot;Evidence&amp;middot;RuleVersion 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#case-audit&quot;&gt; ️ 사건&amp;middot;조사 메모&amp;middot;감사 로그 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#ai-operations-data&quot;&gt;  AI 리포트&amp;middot;실행&amp;middot;Provider 사용량 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#postgres-defense&quot;&gt; ️ PostgreSQL을 정합성 방어선으로 사용한 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#query-index&quot;&gt;⚡ 조회 패턴을 기준으로 설계한 Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#locking-version&quot;&gt;  동시성&amp;middot;잠금&amp;middot;버전 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#flyway-evolution&quot;&gt;  Flyway와 Rolling Update를 고려한 스키마 확장&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#cloud-native-db&quot;&gt;☁️ Cloud Native 환경의 DB 운영&amp;middot;검증 기준&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#closing&quot;&gt;  마치며: 데이터 모델 위에 API 계약을 올리기&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 1 --&gt;
&lt;h2 id=&quot;database-questions&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 상태 모델을 데이터베이스로 옮길 때의 설계 질문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전이 규칙을 Java 코드에 정의했다고 해서 실제 데이터베이스의 모든 쓰기 경로가 그 규칙을 자동으로 따르는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 아닌 직접 SQL, 배치 작업, 운영 복구 도구 또는 경쟁 요청이 애플리케이션이 예상하지 못한 데이터 조합을 만들 가능성도 고려해야 합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 중복 식별자&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 &lt;code&gt;transactionId&lt;/code&gt;, &lt;code&gt;eventId&lt;/code&gt; 또는 동일한 멱등 Key가 동시에 저장되는 것을 어디에서 막을 것인가?&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 잘못된 상태 조합&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CLOSED&lt;/code&gt; 사건에 최종 판정이 없거나, 완료된 탐지 결과에 점수와 등급이 없는 상태를 어떻게 거부할 것인가?&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 잘못된 관계&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다른 거래의 탐지 결과를 현재 거래가 채택하거나, 존재하지 않는 거래에 행동 이벤트가 연결되는 것을 어떻게 막을 것인가?&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 14px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 과거 근거의 변경&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;게시된 Rule 조건과 과거 Evidence가 이후 정책 변경으로 이력 없이 바뀌지 않도록 어떻게 보존할 것인가?&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 동시 요청&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여러 Pod가 같은 거래의 다음 분석 버전 또는 사건을 동시에 만들 때 단 하나의 결과만 확정하려면 무엇이 필요한가?&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #e8dcb9; border-radius: 14px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ 배포 중 스키마 변경&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;구버전 Pod와 신버전 Pod가 함께 실행되는 동안에도 하나의 PostgreSQL Schema를 안전하게 사용할 수 있는가?&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 이 질문을 애플리케이션 코드 한 곳에만 맡기지 않습니다. API 검증, 도메인 검증, JPA 불변식과 PostgreSQL 제약을 계층별로 배치합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb; text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 16px; font-weight: bold; line-height: 2;&quot;&gt;
    API Validation
    &amp;rarr; Domain Validation
    &amp;rarr; JPA 불변식&amp;middot;동시성 제어
    &amp;rarr; PostgreSQL Constraint&amp;middot;Trigger
  &lt;/code&gt;&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 2 --&gt;
&lt;h2 id=&quot;modeling-principles&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  2. FinGuardOps의 데이터 모델링 원칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 나누는 기준은 기술적인 편의보다 각 데이터가 대답해야 하는 업무 질문과 변경 이유입니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;1. 상태와 결과 분리&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래 처리 단계, 위험 등급과 위험 대응 결과를 서로 다른 속성으로 관리하고, 사건 진행 상태와 최종 판정도 분리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;2. 현재값과 이력 분리&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;목록 조회에 필요한 현재값은 주요 Entity에 유지하고, 분석 버전과 주요 변경 이력은 별도 행으로 누적합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;3. 내부 PK와 업무 ID 분리&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;관계와 Join에는 &lt;code&gt;BIGINT Identity&lt;/code&gt;를 사용하고, API&amp;middot;로그&amp;middot;Trace에는 UUID v4 업무 식별자를 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;4. 불변 버전 보존&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;과거 분석에 사용한 Rule&amp;middot;Feature&amp;middot;모델 조건을 덮어쓰지 않고, 변경된 실행 정의는 새로운 버전으로 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;5. 업무 원본과 보조 데이터 분리&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL을 업무 원본으로 사용하고, Redis 캐시와 Observability 데이터가 원본을 대신하지 않도록 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;6. 민감정보 최소화&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 고객&amp;middot;계좌&amp;middot;기기&amp;middot;IP 원문을 반복 저장하지 않고, 연결용 참조값과 제한된 탐지 요약만 보존합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  내부 PK와 외부 업무 식별자를 나눈 이유&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;financial_transaction.id
&amp;rarr; DB 내부 관계용 BIGINT

financial_transaction.transaction_id
&amp;rarr; API&amp;middot;로그&amp;middot;추적용 UUID v4

fraud_case.id
&amp;rarr; DB 내부 관계용 BIGINT

fraud_case.case_id
&amp;rarr; 화면&amp;middot;API&amp;middot;감사용 UUID v4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 PK를 API에 노출하지 않으면 데이터베이스 구현을 바꾸더라도 외부 계약의 업무 식별자를 안정적으로 유지할 수 있습니다. 동시에 관계형 Join에서는 compact한 내부 키를 사용할 수 있습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 3 --&gt;
&lt;h2 id=&quot;domain-erd&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 3. 전체 도메인 ERD와 데이터 영역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 핵심 데이터는 네 개의 업무 영역으로 나눌 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 15px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 290px; padding: 23px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  거래&amp;middot;행동&amp;middot;멱등성&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Transaction&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BehaviorEvent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdempotencyRecord&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 290px; padding: 23px; border: 1px solid #dbe8dc; border-radius: 16px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  탐지&amp;middot;정책&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;DetectionResult&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DetectionEvidence&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FraudRule&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RuleVersion&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 290px; padding: 23px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 사건&amp;middot;감사&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;FraudCase&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CaseTransaction&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CaseNote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditLog&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 290px; padding: 23px; border: 1px solid #ded8ee; border-radius: 16px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  AI 운영&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;AiReportRequest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AiReportExecution&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ProviderCallAttempt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AiReport&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  핵심 Cardinality&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Transaction 1 : N DetectionResult
DetectionResult 1 : N DetectionEvidence
FraudRule 1 : N RuleVersion
FraudCase N : M Transaction
FraudCase 1 : N CaseNote
FraudCase 1 : N AiReportRequest
AiReportExecution 1 : N ProviderCallAttempt
AiReportExecution 1 : 0..1 AiReport&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 위험정보 조회 결과는 모든 원문을 별도 Entity로 영속화하는 대신, 분석 요청을 구성하는 동안 필요한 최소 Snapshot으로 취급합니다. 저장 필요성과 감사 정책이 확정되기 전에는 업무 원본 테이블을 무분별하게 늘리지 않습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;FastAPI와 LLM Provider가 반환한 값 자체는 FinGuardOps의 업무 원본이 아닙니다. Spring Boot가 요청&amp;middot;버전&amp;middot;정합성을 검증하고 PostgreSQL에 연결해 저장한 결과만 감사 가능한 업무 기록으로 취급합니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 4 --&gt;
&lt;h2 id=&quot;transaction-behavior-idempotency&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  4. 거래&amp;middot;행동 이벤트&amp;middot;멱등성 모델&lt;/h2&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;4.1 Transaction &amp;mdash; 거래의 현재 업무 상태&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Transaction&lt;/code&gt;은 FinGuardOps가 접수한 금융 거래의 식별 정보와 현재 처리 상태, 채택한 탐지 결과와 적용된 대응 결과를 소유합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;주요 속성&lt;/p&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;margin-bottom: 0; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;transactionId
transactionType
amount
currencyCode
occurredAt
externalCustomerRef
senderAccountRef
recipientAccountRef
channel
deviceRef
processingStatus
riskLevel
riskResponseOutcome
adoptedDetectionResultId
version
createdAt / updatedAt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금액은 API에서 문자열로 받아 정밀도 손실 없이 파싱하고, PostgreSQL에서는 &lt;code&gt;NUMERIC(19,4)&lt;/code&gt;를 사용하되 0보다 큰 정수만 허용하는 Check를 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;recipientAccountRef&lt;/code&gt;와 &lt;code&gt;channel&lt;/code&gt;은 거래 유형에 따라 필수 또는 금지되는 조합이 다르므로 애플리케이션과 DB가 같은 규칙을 검증합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;ACCOUNT_TRANSFER&lt;/b&gt;&lt;br /&gt;수취 계좌 필수&lt;br /&gt;MOBILE_BANKING&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;OPEN_BANKING_TRANSFER&lt;/b&gt;&lt;br /&gt;수취 계좌 필수&lt;br /&gt;OPEN_BANKING&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #ead9df; border-radius: 13px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;ATM_WITHDRAWAL&lt;/b&gt;&lt;br /&gt;수취 계좌 금지&lt;br /&gt;ATM&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #ead9df; border-radius: 13px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;LOAN_DISBURSED&lt;/b&gt;&lt;br /&gt;수취 계좌 금지&lt;br /&gt;CORE_BANKING&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;4.2 BehaviorEvent &amp;mdash; 거래 전후 행동 타임라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행동 이벤트는 거래 없이 먼저 발생할 수 있으므로 거래 참조를 선택적으로 둡니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Transaction 1
&amp;rarr; BehaviorEvent 0..N

BehaviorEvent
&amp;rarr; Transaction 0..1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행동 이벤트는 별도 &lt;code&gt;Idempotency-Key&lt;/code&gt;를 요구하지 않고, 호출자가 생성한 &lt;code&gt;eventId&lt;/code&gt;와 정규화한 Request Fingerprint로 자연 멱등성을 구성합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 &lt;code&gt;eventId&lt;/code&gt; &amp;rarr; 새로운 행동 이벤트 저장&lt;/li&gt;
&lt;li&gt;같은 &lt;code&gt;eventId&lt;/code&gt; + 같은 Fingerprint &amp;rarr; 기존 이벤트 반환&lt;/li&gt;
&lt;li&gt;같은 &lt;code&gt;eventId&lt;/code&gt; + 다른 Fingerprint &amp;rarr; 충돌로 거부&lt;/li&gt;
&lt;li&gt;동시 Insert &amp;rarr; &lt;code&gt;event_id&lt;/code&gt; Unique가 최종 단일 승자를 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 유형별로 필요한 계좌&amp;middot;기기&amp;middot;수취인&amp;middot;거래 참조가 다르므로 유형별 null 조합은 Check Constraint로 방어합니다. 고객&amp;middot;계좌&amp;middot;거래 유형처럼 여러 행을 비교해야 하는 규칙은 Service에서 검증합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;4.3 IdempotencyRecord &amp;mdash; 거래 상태와 명령 처리 상태 분리&lt;/h3&gt;
&lt;pre class=&quot;awk&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;operationScope
+ idempotencyKey
&amp;rarr; Unique

requestFingerprint
processingStatus
financialTransactionId
responseSnapshot
failureCode
expiresAt
createdAt / updatedAt / finishedAt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등 상태를 거래 행에 직접 넣지 않고 별도 Entity로 두면 처리 중 요청, 완료 결과 재생, 실패 결과와 보존 기간을 거래 업무 상태와 독립적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;거래 상태는 금융 업무가 어디까지 진행됐는지를 표현하고, IdempotencyRecord는 동일 명령의 실행권과 최초 응답 재생을 관리합니다. 두 상태를 하나의 컬럼으로 합치지 않습니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 5 --&gt;
&lt;h2 id=&quot;detection-rule&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  5. 탐지 결과&amp;middot;Evidence&amp;middot;RuleVersion 모델&lt;/h2&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;5.1 DetectionResult &amp;mdash; 거래별 분석 버전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 거래도 Rule&amp;middot;Feature&amp;middot;모델 조건이 바뀌면 다시 분석할 수 있습니다. 기존 결과를 덮어쓰지 않고 새로운 버전으로 보존합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Transaction 1
&amp;rarr; DetectionResult 0..N

UNIQUE(
  financial_transaction_id,
  detection_result_version
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패한 분석도 실행 사실을 보존하기 위해 하나의 버전을 소비합니다. 늦게 도착한 중복 응답이 같은 버전을 다시 저장하거나 기존 결과를 바꾸지 못하도록 합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;PENDING&lt;/b&gt;&lt;br /&gt;실행 조건과 버전 고정&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #e8dcb9; border-radius: 13px; background-color: #fffcf5;&quot;&gt;&lt;b&gt;IN_PROGRESS&lt;/b&gt;&lt;br /&gt;분석 시작 시각 필수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;COMPLETED&lt;/b&gt;&lt;br /&gt;점수&amp;middot;등급&amp;middot;완료 시각 필수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #eed5d5; border-radius: 13px; background-color: #fff9f9;&quot;&gt;&lt;b&gt;FAILED&lt;/b&gt;&lt;br /&gt;실패 코드&amp;middot;완료 시각 필수&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;5.2 DetectionEvidence &amp;mdash; 설명 가능한 개별 근거&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;DetectionResult 1
&amp;rarr; DetectionEvidence 0..N

Evidence Type
&amp;rarr; RULE
&amp;rarr; ML
&amp;rarr; EXTERNAL_RISK
&amp;rarr; BEHAVIOR_PATTERN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Evidence는 단순 설명 문자열이 아니라 안정적인 &lt;code&gt;reasonCode&lt;/code&gt;, 점수 기여도, 사용한 RuleVersion과 제한된 관측 요약을 보존합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #5d4381; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;observationSummary 저장 원칙&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reason Code별 허용 Key만 저장&lt;/li&gt;
&lt;li&gt;JSON Object만 허용하고 배열&amp;middot;중첩 객체 금지&lt;/li&gt;
&lt;li&gt;원문 행동 로그와 전체 Feature Vector 저장 금지&lt;/li&gt;
&lt;li&gt;실제 계좌&amp;middot;기기&amp;middot;IP 원문 저장 금지&lt;/li&gt;
&lt;li&gt;화면과 재현에 필요한 최소 관측값만 보존&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;5.3 FraudRule과 RuleVersion &amp;mdash; 논리 정책과 불변 실행 정의&lt;/h3&gt;
&lt;pre class=&quot;properties&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;FraudRule 1
&amp;rarr; RuleVersion N

DRAFT &amp;rarr; PUBLISHED
DRAFT &amp;rarr; WITHDRAWN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FraudRule&lt;/code&gt;은 논리적인 Rule의 정체성을 소유하고, &lt;code&gt;RuleVersion&lt;/code&gt;은 실제 실행 조건&amp;middot;가중치&amp;middot;Reason Code&amp;middot;적용 기간을 소유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시된 버전의 실행 정의를 수정하지 않습니다. 정책을 바꿔야 하면 기존 행을 업데이트하는 대신 새로운 &lt;code&gt;versionNumber&lt;/code&gt;를 생성합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb; text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 16px; font-weight: bold; line-height: 2;&quot;&gt;
    PUBLISHED 적용 기간
    = [effectiveFrom, effectiveTo)
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 시각은 포함하고 종료 시각은 제외하는 반개방 구간을 사용합니다. 동일 Rule의 게시 기간이 겹치지 않도록 PostgreSQL Exclusion Constraint를 사용하고, 인접한 두 기간은 허용합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 6 --&gt;
&lt;h2 id=&quot;case-audit&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 6. 사건&amp;middot;조사 메모&amp;middot;감사 로그 모델&lt;/h2&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;6.1 FraudCase와 CaseTransaction&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 사건은 여러 거래를 조사할 수 있고, 하나의 거래도 시간에 따라 여러 종료 사건 이력을 가질 수 있으므로 중간 Entity로 다대다 관계를 표현합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;FraudCase 1
&amp;rarr; CaseTransaction N
&amp;rarr; Transaction 1

UNIQUE(
  fraud_case_id,
  financial_transaction_id
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 사건과 거래의 중복 연결은 Unique로 막습니다. 다만 &amp;ldquo;한 거래에 활성 사건은 최대 하나&amp;rdquo;라는 규칙은 다른 테이블의 사건 상태까지 확인해야 하므로, 거래를 먼저 잠근 뒤 활성 사건을 조회&amp;middot;검증하는 Service 경계에서 보장합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;활성 사건&lt;/b&gt;&lt;br /&gt;OPEN&lt;br /&gt;IN_REVIEW&lt;br /&gt;ADDITIONAL_INFORMATION_REQUIRED&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;종료 사건&lt;/b&gt;&lt;br /&gt;CLOSED&lt;br /&gt;finalDisposition 필수&lt;br /&gt;closedAt 필수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;검토 중 사건&lt;/b&gt;&lt;br /&gt;IN_REVIEW&lt;br /&gt;assigneeRef 필수&lt;br /&gt;finalDisposition = null&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;6.2 CaseNote &amp;mdash; 조사 메모의 정정 이력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조사 메모는 과거 기록을 직접 덮어쓰기보다 append-only를 기본 방향으로 둡니다.&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;기존 메모 직접 수정&amp;middot;삭제
&amp;rarr; 지양

정정 필요
&amp;rarr; correctionOfNoteId로 원 메모 참조
&amp;rarr; 새로운 정정 메모 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모에는 불필요한 고객&amp;middot;계좌 원문이나 인증정보를 기록하지 않고, 담당자의 조사 근거에 필요한 최소 정보만 남깁니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;6.3 AuditLog &amp;mdash; 현재 상태가 아닌 변경 이력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사 로그는 거래나 사건의 현재 상태를 대신하는 테이블이 아닙니다. 누가, 언제, 어떤 이유로 무엇을 변경했는지 별도로 기록합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;pre class=&quot;properties&quot; style=&quot;margin: 0; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;actorType / actorId
action / reasonCode
targetType / targetId
beforeValueSummary
afterValueSummary
metadata
transactionId / caseId / traceId
changedAt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유 형식 JSON을 무제한 저장하지 않고, Action별 허용 Key&amp;middot;Scalar Type&amp;middot;크기를 제한합니다. Prompt, LLM 입출력, 내부 예외와 금융 원문을 감사 로그에 그대로 복제하지 않습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;Application 방어&lt;/b&gt;&lt;br /&gt;INSERT 전용 Repository&lt;br /&gt;수정&amp;middot;삭제 메서드 없음&lt;br /&gt;Typed JSON 계약&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;PostgreSQL 방어&lt;/b&gt;&lt;br /&gt;UPDATE&amp;middot;DELETE 차단 Trigger&lt;br /&gt;ON DELETE RESTRICT&lt;br /&gt;Action 조합 Check&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;사건 생성&amp;middot;거래 최종화&amp;middot;AuditLog append가 하나의 업무 변경이라면 감사 기록 실패를 무시하지 않고 같은 트랜잭션 전체를 Rollback합니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 7 --&gt;
&lt;h2 id=&quot;ai-operations-data&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  7. AI 리포트&amp;middot;실행&amp;middot;Provider 사용량 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 리포트 영역을 하나의 테이블로 합치면 외부 요청, 논리 실행, 실제 Provider 호출과 최종 결과의 의미가 섞입니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;AiReportRequest&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 리포트를 요청한 사실, 멱등성, 요청자와 최종 해결 결과를 보존합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;AiReportExecution&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;정확 일치 조건에 대해 실제로 공유되는 하나의 논리 실행을 관리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;ProviderCallAttempt&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 Provider 호출별 모델, 토큰, 지연시간, 비용과 실패를 기록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;AiReport&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;검증을 통과한 LLM 결과 또는 템플릿 fallback 결과의 본문을 보존합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;FraudCase 1
&amp;rarr; AiReportRequest N

AiReportExecution 1
&amp;rarr; ProviderCallAttempt N

AiReportExecution 1
&amp;rarr; AiReport 0..1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;♻️ 정확 일치 캐시를 데이터로 표현하는 방법&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;caseId
+ detectionResultVersion
+ promptVersion
+ modelVersion&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료된 결과를 정확 일치 캐시로 재사용할 때도 새로운 외부 요청 이력은 남길 수 있습니다. 하지만 새 실행, 새 Provider 호출, 가상 토큰과 가상 비용을 만들지 않습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 적중 요청 &amp;rarr; 기존 &lt;code&gt;AiReport&lt;/code&gt; 참조&lt;/li&gt;
&lt;li&gt;새 &lt;code&gt;AiReportExecution&lt;/code&gt; 생성 안 함&lt;/li&gt;
&lt;li&gt;새 &lt;code&gt;ProviderCallAttempt&lt;/code&gt; 생성 안 함&lt;/li&gt;
&lt;li&gt;캐시 자체를 &lt;code&gt;reportSource&lt;/code&gt;로 기록하지 않음&lt;/li&gt;
&lt;li&gt;측정하지 못한 토큰&amp;middot;비용을 0으로 단정하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 실행에서 Timeout으로 재시도하거나 모델을 라우팅하면 실제 Provider 호출이 여러 번 발생할 수 있습니다. 그래서 사용량과 비용은 논리 실행이 아니라 &lt;code&gt;ProviderCallAttempt&lt;/code&gt; 단위로 기록합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 8 --&gt;
&lt;h2 id=&quot;postgres-defense&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 8. PostgreSQL을 정합성 방어선으로 사용한 방법&lt;/h2&gt;
&lt;!--
  이미지 2 삽입 위치

  파일명:
  finguardops_정합성_방어와_스키마_진화.png

  대체 텍스트:
  API Validation부터 PostgreSQL 제약과 Flyway Expand-Migrate-Contract까지의 FinGuardOps 데이터 정합성 방어 구조
--&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_정합성_방어와_스키마_진화.png&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NDwin/dJMcacLe4Jy/t2dYKkMUVwAYqFecaR6tc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NDwin/dJMcacLe4Jy/t2dYKkMUVwAYqFecaR6tc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NDwin/dJMcacLe4Jy/t2dYKkMUVwAYqFecaR6tc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNDwin%2FdJMcacLe4Jy%2Ft2dYKkMUVwAYqFecaR6tc1%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;1800&quot; height=&quot;1200&quot; data-filename=&quot;finguardops_정합성_방어와_스키마_진화.png&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;margin-top: 8px; color: #777777; font-size: 14px; line-height: 1.6; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션 검증과 PostgreSQL 제약을 중첩하고, Flyway로 호환 가능한 스키마 확장을 관리하는 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 정합성은 한 계층의 검증에만 의존하지 않습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 2px solid #7a1f3d; border-radius: 15px; background-color: #fff8fa; text-align: center;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7a1f3d; font-size: 19px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;API 검증 &amp;rarr; 도메인 검증 &amp;rarr; JPA 불변식 &amp;rarr; PostgreSQL 최종 방어&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  Foreign Key&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재하지 않는 거래 참조 방지&lt;/li&gt;
&lt;li&gt;RuleVersion과 Evidence 관계 보장&lt;/li&gt;
&lt;li&gt;거래&amp;middot;사건&amp;middot;감사 데이터 삭제 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  Unique&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;transactionId&amp;middot;eventId 중복 방지&lt;/li&gt;
&lt;li&gt;멱등 Scope&amp;middot;Key 단일 승자&lt;/li&gt;
&lt;li&gt;거래별 분석 버전 중복 방지&lt;/li&gt;
&lt;li&gt;사건&amp;ndash;거래 연결 중복 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ Check&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;허용 Enum과 UUID v4&lt;/li&gt;
&lt;li&gt;금액&amp;middot;시간&amp;middot;문자열 범위&lt;/li&gt;
&lt;li&gt;상태별 nullable 조합&lt;/li&gt;
&lt;li&gt;거래&amp;middot;이벤트 유형별 필드 조합&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  Trigger&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AuditLog UPDATE&amp;middot;DELETE 차단&lt;/li&gt;
&lt;li&gt;PUBLISHED RuleVersion 불변성&lt;/li&gt;
&lt;li&gt;Evidence Snapshot 일치 검증&lt;/li&gt;
&lt;li&gt;직접 SQL 우회 변경 방어&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  Exclusion Constraint&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 FraudRule의 PUBLISHED 적용 기간이 겹치는 것을 막고, 인접한 버전 기간은 허용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  Typed JSONB&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;자유 형식 문서 저장소처럼 사용하지 않고, 허용 Key&amp;middot;Scalar Type&amp;middot;크기를 제한합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;애플리케이션 검증은 사용자에게 이해 가능한 오류를 제공하고, PostgreSQL 제약은 경쟁 쓰기와 우회 저장을 막는 최종 방어선으로 사용합니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 9 --&gt;
&lt;h2 id=&quot;query-index&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;⚡ 9. 조회 패턴을 기준으로 설계한 Index&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index는 Entity나 FK가 존재한다는 이유만으로 무조건 추가하지 않습니다. 실제 업무 조회의 Filter, 정렬과 Pagination 순서를 기준으로 설계합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;행동 이벤트 Rule 조회&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;externalCustomerRef
eventType
occurredAt DESC
eventId ASC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;사건 목록 조회&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;caseStatus
lastChangedAt
id&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;감사 이력 조회&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;caseId
changedAt DESC
id DESC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;탐지 장애 조회&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;analysisStatus
updatedAt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 22px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;장기 IN_PROGRESS 복구 후보&lt;/h3&gt;
&lt;pre class=&quot;fortran&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;operationScope
updatedAt
id
WHERE status = IN_PROGRESS&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚖️ Index도 비용이 있는 설계 선택&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;얻는 것&lt;/b&gt;&lt;br /&gt;조회 범위 축소&lt;br /&gt;정렬 비용 감소&lt;br /&gt;운영 검색 속도 향상&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9;&quot;&gt;&lt;b&gt;지불하는 것&lt;/b&gt;&lt;br /&gt;INSERT&amp;middot;UPDATE 비용&lt;br /&gt;추가 저장 공간&lt;br /&gt;Migration 시간과 잠금&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실제 Query와 실행계획 없이 계좌&amp;middot;기기&amp;middot;Reason Code별 Index를 선제적으로 계속 추가하지 않습니다. 느린 조회가 확인되면 Filter 선택도, 정렬과 Pagination을 함께 분석합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 10 --&gt;
&lt;h2 id=&quot;locking-version&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  10. 동시성&amp;middot;잠금&amp;middot;버전 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 동시성 문제를 하나의 잠금 방식으로 해결하지 않습니다. 사용자 업무의 수정 충돌과 짧은 단일 승자 경쟁은 성격이 다릅니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;낙관적 잠금&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 읽고 나중에 수정하는 비교적 긴 업무 흐름에서 &lt;code&gt;@Version&lt;/code&gt;으로 선행 변경을 감지합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FraudCase 상태 변경&lt;/li&gt;
&lt;li&gt;FraudRule 메타데이터 변경&lt;/li&gt;
&lt;li&gt;RuleVersion DRAFT 수정&lt;/li&gt;
&lt;li&gt;동시 편집 충돌 검출&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;비관적 잠금&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 거래를 기준으로 단 하나의 생성&amp;middot;버전 할당&amp;middot;최종화가 필요한 짧은 경쟁 구간에서 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래별 DetectionResult version 할당&lt;/li&gt;
&lt;li&gt;HIGH&amp;middot;CRITICAL 사건 생성&lt;/li&gt;
&lt;li&gt;활성 사건 중복 방지&lt;/li&gt;
&lt;li&gt;멱등 완료&amp;middot;복구 단일 승자&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  잠금 순서를 고정한 이유&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb; text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 16px; font-weight: bold; line-height: 2;&quot;&gt;
    Transaction
    &amp;rarr; FraudCase
    &amp;rarr; CaseTransaction
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 데이터 집합을 요청마다 다른 순서로 잠그면 Deadlock 가능성이 커집니다. 거래를 먼저 잠근 뒤 활성 사건과 연결을 일관된 순서로 잠그도록 설계합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;☁️ 여러 Pod가 공유해야 하는 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 Pod가 여러 개 실행되면 JVM 메모리 Lock은 다른 Pod와 공유되지 않습니다. 따라서 단일 승자는 PostgreSQL의 Unique, Row Lock과 Version을 기준으로 결정합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Pod A ─┐
       ├&amp;rarr; PostgreSQL Row / Unique Constraint
Pod B ─┘

&amp;rarr; 하나의 거래 버전
&amp;rarr; 하나의 활성 사건
&amp;rarr; 하나의 멱등 완료 결과&lt;/code&gt;&lt;/pre&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 11 --&gt;
&lt;h2 id=&quot;flyway-evolution&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  11. Flyway와 Rolling Update를 고려한 스키마 확장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 모델은 한 번 만들고 끝나는 구조가 아닙니다. 거래&amp;middot;탐지&amp;middot;사건&amp;middot;AI 운영 기능이 추가되면서 Schema도 계속 확장됩니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;11.1 적용된 Migration은 수정하지 않는다&lt;/h3&gt;
&lt;pre class=&quot;&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;이미 공유 환경에 적용된 Migration
&amp;rarr; 내용 수정 금지

새로운 스키마 변경
&amp;rarr; 다음 버전 Migration으로 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Migration을 수정하면 개발자마다 동일한 버전 번호가 서로 다른 Schema를 의미하게 됩니다. Flyway Checksum과 재현 가능성을 유지하기 위해 변경 이력을 앞으로만 추가합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;11.2 Additive Migration 우선&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;새 테이블 추가&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;새 nullable 컬럼 추가&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;새 Index 추가&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #dbe8dc; border-radius: 13px; background-color: #f9fff9;&quot;&gt;호환 가능한 제약 추가&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구버전 애플리케이션이 모르는 필드를 즉시 필수로 만들거나 기존 컬럼을 바로 Rename&amp;middot;Drop하면 Rolling Update 중 오류가 발생할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;11.3 Expand&amp;ndash;Migrate&amp;ndash;Contract&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;1. Expand&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;신&amp;middot;구 버전이 함께 사용할 수 있도록 새 컬럼&amp;middot;테이블을 호환 가능한 형태로 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #e8dcb9; border-radius: 16px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;2. Migrate&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션을 배포하고 필요한 데이터 이관과 이중 읽기&amp;middot;쓰기를 검증합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;3. Contract&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;구버전 사용 중단과 데이터 이관 완료를 확인한 뒤 오래된 컬럼&amp;middot;계약을 제거합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;11.4 Rolling Update 호환성&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;구버전 Pod
+ 신버전 Pod
+ 하나의 PostgreSQL Schema&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;새 컬럼을 처음부터 NOT NULL로 추가하지 않기&lt;/li&gt;
&lt;li&gt;구버전이 모르는 Enum 값을 즉시 저장하지 않기&lt;/li&gt;
&lt;li&gt;컬럼 Rename보다 새 컬럼 추가 후 단계적 전환&lt;/li&gt;
&lt;li&gt;삭제&amp;middot;축소는 모든 Pod 전환 이후 수행&lt;/li&gt;
&lt;li&gt;Application 배포 전에 Schema 호환성 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  AI Agent가 DB 계약을 임의로 바꾸지 못하게 한 이유&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0;&quot; data-ke-size=&quot;size16&quot;&gt;AI Agent는 ERD, Migration SQL과 테스트 초안을 만들 수 있지만 다음 변경을 독자적으로 확정할 수 없습니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적용된 Migration 수정&lt;/li&gt;
&lt;li&gt;컬럼&amp;middot;테이블 삭제&lt;/li&gt;
&lt;li&gt;대규모 Backfill과 데이터 변환&lt;/li&gt;
&lt;li&gt;새 상태값&amp;middot;제약조건 확정&lt;/li&gt;
&lt;li&gt;Production Migration 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;ERD 영향 검토
&amp;rarr; API 호환성 검토
&amp;rarr; Migration Review
&amp;rarr; 구&amp;middot;신버전 공존 확인
&amp;rarr; PostgreSQL 통합 테스트
&amp;rarr; 사용자 최종 승인&lt;/code&gt;&lt;/pre&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 12 --&gt;
&lt;h2 id=&quot;cloud-native-db&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;☁️ 12. Cloud Native 환경의 DB 운영&amp;middot;검증 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 설계는 DDL이 적용되는 것으로 끝나지 않습니다. 여러 인스턴스, Connection Pool, Migration과 장애 상황에서 같은 정합성 규칙이 유지되는지 검증해야 합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Connection Pool과 트랜잭션 보유 시간&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 호출 중 DB Connection과 Row Lock을 장시간 보유하지 않기&lt;/li&gt;
&lt;li&gt;짧은 상태 확정 트랜잭션과 외부 호출 경계 분리&lt;/li&gt;
&lt;li&gt;Pool 사용량과 Connection 획득 대기시간 관측&lt;/li&gt;
&lt;li&gt;Transaction Timeout과 Lock 대기시간 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb; text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 15px; font-weight: bold; line-height: 2;&quot;&gt;
    짧은 DB Commit
    &amp;rarr; Connection 반환
    &amp;rarr; External Risk&amp;middot;FastAPI 호출
    &amp;rarr; 새 트랜잭션에서 결과 확정
  &lt;/code&gt;&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Migration Gate&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Flyway Validate
&amp;rarr; Migration 적용
&amp;rarr; Hibernate Schema Validation
&amp;rarr; 애플리케이션 기동
&amp;rarr; Health Check
&amp;rarr; 핵심 업무 Smoke Test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Migration Checksum이 다르거나 JPA Mapping과 실제 Schema가 일치하지 않으면 애플리케이션이 조용히 동작하도록 두지 않고 배포를 중단합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  운영에서 관측할 항목&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;DB Connection Pool 사용량&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Connection 획득 대기시간&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Query 지연시간&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Lock 대기와 Deadlock&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Transaction Rollback&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Migration 성공&amp;middot;실패&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;Table&amp;middot;Index 크기&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff;&quot;&gt;장기 IN_PROGRESS 수&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Backup과 복구 원칙&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파괴적 Migration 전에 Backup과 복구 경로 확인&lt;/li&gt;
&lt;li&gt;Rollback SQL 하나에만 의존하지 않기&lt;/li&gt;
&lt;li&gt;복구 이후 FK&amp;middot;상태&amp;middot;사건&amp;middot;감사 정합성 재검증&lt;/li&gt;
&lt;li&gt;복구 작업의 실행 주체와 결과 감사&lt;/li&gt;
&lt;li&gt;보존&amp;middot;파기 정책과 복구 목표를 별도 운영 문서로 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  검증 기준&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ol style=&quot;margin: 0; padding-left: 23px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;빈 PostgreSQL에 모든 Flyway Migration이 순서대로 적용되는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;기존 데이터가 있는 Schema에도 Additive Migration이 적용되는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;JPA Mapping과 실제 Schema가 일치하는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;Unique&amp;middot;FK&amp;middot;Check&amp;middot;Trigger가 우회 SQL도 거부하는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;동시 Insert에서 단 하나의 요청만 성공하는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;구버전과 신버전이 공존해도 Schema를 함께 사용할 수 있는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;AuditLog와 PUBLISHED RuleVersion의 직접 변경이 거부되는가&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 9px;&quot;&gt;업무 Query가 예상한 Index와 정렬 전략을 사용하는가&lt;/li&gt;
&lt;li&gt;DB Pool 고갈&amp;middot;Lock 대기&amp;middot;Migration 실패를 지표와 로그로 확인할 수 있는가&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 13 --&gt;
&lt;h2 id=&quot;closing&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  13. 마치며: 데이터 모델 위에 API 계약을 올리기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 ERD는 거래&amp;middot;탐지&amp;middot;사건 데이터를 단순히 연결한 그림이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태와 결과, 현재값과 이력, 외부 요청과 실제 AI 실행, 업무 원본과 캐시를 분리하고, PostgreSQL 제약을 통해 허용 가능한 관계와 상태 조합을 보조 검증하는 설계입니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%); text-align: center;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 10px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;FinGuardOps 데이터 설계의 핵심&lt;/p&gt;
&lt;code style=&quot;color: #333333; font-size: 15px; font-weight: bold; line-height: 2.1;&quot;&gt;
    업무 관계
    &amp;rarr; ERD

    중복&amp;middot;상태 조합
    &amp;rarr; Unique&amp;middot;Check&amp;middot;FK

    불변 이력
    &amp;rarr; 버전 Entity&amp;middot;Append-only

    동시 쓰기
    &amp;rarr; Lock&amp;middot;Version&amp;middot;DB Constraint

    배포 중 스키마 변경
    &amp;rarr; Flyway&amp;middot;Expand&amp;ndash;Migrate&amp;ndash;Contract

    운영 검증
    &amp;rarr; Testcontainers&amp;middot;Metrics&amp;middot;Backup&amp;middot;Recovery
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 관계와 제약을 정의한 다음에는 외부 요청이 이 모델에 어떤 형식으로 들어오고, Validation 실패와 업무 충돌을 어떤 HTTP 계약으로 반환할지 결정해야 합니다.&lt;/p&gt;
&lt;!-- 다음 편 예고 --&gt;
&lt;div style=&quot;margin: 32px 0; padding: 22px 24px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 8px; color: #234f88; font-size: 17px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[FinGuardOps #5] 중복과&amp;nbsp;잘못된&amp;nbsp;입력을&amp;nbsp;막는&amp;nbsp;거래&amp;middot;행동&amp;nbsp;이벤트&amp;nbsp;API와&amp;nbsp;Validation&amp;nbsp;설계&lt;/b&gt;&lt;br /&gt;금융 거래와 9개 행동 이벤트의 Request&amp;middot;Response, Validation, 자연 멱등성, 오류 계약, Trace ID와 Rolling Update 중 API 호환성을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- GitHub 저장소 버튼 --&gt;
&lt;div style=&quot;margin: 40px 0 12px 0; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 13px 20px; border-radius: 10px; background-color: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36, 41, 47, 0.2);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;GitHub&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt; &lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>FinGuardOps/Project</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/49</guid>
      <comments>https://bbbicsan.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 31 Aug 2026 12:17:00 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #3] 재시도와 장애에도 안전한 거래&amp;middot;사건 상태 전이와 멱등성 설계</title>
      <link>https://bbbicsan.tistory.com/48</link>
      <description>&lt;div style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all;&quot;&gt;&lt;!-- 시리즈 배지 --&gt;
&lt;div style=&quot;display: inline-block; margin-bottom: 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background-color: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold; letter-spacing: 0.2px;&quot;&gt;FinGuardOps Project Series #3&lt;/div&gt;
&lt;!-- 도입부 --&gt;
&lt;p style=&quot;font-size: 18px; line-height: 1.9;&quot; data-ke-size=&quot;size16&quot;&gt;금융 거래 요청을 처리할 때 가장 위험한 상황 중 하나는 서버가 업무를 완료했지만 클라이언트가 응답을 받지 못하는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 요청이 실패했다고 판단해 같은 거래를 다시 전송할 수 있습니다. 별도의 통제가 없다면 거래뿐 아니라 탐지 결과, 외부 위험정보 조회, 고위험 사건과 감사 이력까지 중복으로 생성될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;거래 요청 전송
&amp;rarr; 서버의 금융 업무 처리 완료
&amp;rarr; 응답 전송 중 네트워크 연결 종료
&amp;rarr; 사용자는 실패로 판단
&amp;rarr; 같은 요청 재전송
&amp;rarr; 중복 거래&amp;middot;탐지&amp;middot;사건&amp;middot;외부 호출 위험&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버라면 메모리 Lock으로 해결할 수 있어 보이지만, 여러 Pod가 동시에 요청을 처리하거나 애플리케이션이 재시작되는 Cloud Native 환경에서는 메모리 상태만으로 안전성을 보장할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FinGuardOps에서는 &lt;b&gt;상태 전이&lt;/b&gt;로 업무가 어디까지 확정되었는지 표현하고, &lt;b&gt;멱등성&lt;/b&gt;으로 동일한 명령의 중복 실행을 통제하도록 설계했습니다.&lt;/p&gt;
&lt;!-- 핵심 설계 원칙 --&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;멱등성은 중복 요청을 단순히 무시하는 기능이 아니라, 동일한 명령에 대해 &lt;b&gt;최초로 확정한 업무 결과를 다시 재현하는 계약&lt;/b&gt;입니다.&lt;/blockquote&gt;
&lt;!--
  이미지 1 삽입 위치

  파일명:
  finguardops_상태_전이_설계_다이어그램.png

  대체 텍스트:
  거래 처리 상태와 위험 대응, 사건 상태와 최종 판정을 분리한 FinGuardOps 상태 전이 설계
--&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;거래&amp;amp;middot;사건 상태 전이와 개념 분리.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clpv0Y/dJMcaheF7JT/RkZcxvEMT4JMJppNc7foD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clpv0Y/dJMcaheF7JT/RkZcxvEMT4JMJppNc7foD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clpv0Y/dJMcaheF7JT/RkZcxvEMT4JMJppNc7foD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclpv0Y%2FdJMcaheF7JT%2FRkZcxvEMT4JMJppNc7foD0%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;1448&quot; height=&quot;1086&quot; data-filename=&quot;거래&amp;middot;사건 상태 전이와 개념 분리.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;margin-top: 8px; color: #777777; font-size: 14px; line-height: 1.6; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;거래 처리 단계, 위험 대응, 사건 진행 상태와 최종 판정을 서로 다른 개념으로 분리한 FinGuardOps 상태 모델&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 18px 21px; border-left: 5px solid #666666; border-radius: 0 12px 12px 0; background-color: #f7f7f7; color: #444444;&quot; data-ke-style=&quot;style1&quot;&gt;이번 글은 특정 시점의 구현 현황보다 재시도&amp;middot;동시성&amp;middot;재시작&amp;middot;Rolling Update 환경에서 금융 업무 정합성을 유지하기 위해 어떤 상태와 멱등 계약을 설계했는지 설명합니다.&lt;/blockquote&gt;
&lt;!-- 목차 --&gt;
&lt;div style=&quot;margin: 38px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fffafb; box-shadow: 0 4px 14px rgba(122, 31, 61, 0.06);&quot;&gt;
&lt;h2 id=&quot;table-of-contents&quot; style=&quot;margin: 0 0 16px 0; color: #7a1f3d; font-size: 23px; border: none;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#why-state-idempotency&quot;&gt;  왜 상태 전이와 멱등성이 필요한가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#six-concepts&quot;&gt;  하나의 status로 합치지 않은 여섯 가지 개념&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#transaction-state&quot;&gt;  거래 처리 상태 전이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#risk-response&quot;&gt; ️ 위험 등급과 위험 대응 결과&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#case-state&quot;&gt; ️ 사건 상태와 최종 판정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#concurrency-audit&quot;&gt;  상태 변경의 동시성&amp;middot;감사 원칙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#key-fingerprint&quot;&gt;  Idempotency-Key와 Request Fingerprint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#idempotency-decision&quot;&gt;  멱등 처리 상태와 중복 요청 판단&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#response-snapshot&quot;&gt;  Response Snapshot과 최초 결과 재생&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#completion-gap&quot;&gt; ️ 최종 성공 경계와 완료 간극 복구&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#cloud-native-validation&quot;&gt;☁️ Cloud Native 환경의 운영&amp;middot;검증 기준&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#closing&quot;&gt;  마치며: 상태 모델을 데이터베이스로 옮기기&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 1 --&gt;
&lt;h2 id=&quot;why-state-idempotency&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 왜 상태 전이와 멱등성이 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전이와 멱등성은 비슷해 보이지만 서로 다른 문제를 해결합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  상태 전이&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래와 사건이 업무 흐름의 어느 단계에 있으며, 현재 상태에서 어떤 다음 상태로 이동할 수 있는지를 표현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  멱등성&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;동일한 금융 명령이 반복되더라도 실제 업무를 다시 실행하지 않고 최초 결과를 재현하도록 통제합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 거래가 &lt;code&gt;ANALYZING&lt;/code&gt;이라는 사실은 분석 중이라는 업무 단계만 설명합니다. 같은 HTTP 요청을 다시 실행해도 되는지는 멱등 레코드와 요청 동일성 검증을 통해 따로 판단해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 멱등 레코드가 &lt;code&gt;IN_PROGRESS&lt;/code&gt;라고 해서 거래가 접수 단계인지, 분석 단계인지, 최종 업무 Commit 이후 응답만 누락된 상태인지까지 모두 설명할 수 있는 것은 아닙니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb; text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 16px; font-weight: bold; line-height: 2;&quot;&gt;
    거래 업무 상태
    + 사건 업무 상태
    + 멱등 명령 처리 상태
    &amp;rarr; 서로 분리해서 관리
  &lt;/code&gt;&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;☁️ Cloud Native 환경에서 문제가 더 커지는 이유&lt;/h3&gt;
&lt;pre class=&quot;properties&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Pod A
&amp;rarr; 최초 거래 요청 수신

Pod B
&amp;rarr; 같은 요청의 네트워크 재시도 수신

Rolling Update
&amp;rarr; 구버전과 신버전이 동시에 요청 처리

Pod 재시작
&amp;rarr; 프로세스 메모리의 Lock과 처리 정보 소멸&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 특정 인스턴스의 메모리가 아니라 모든 애플리케이션 인스턴스가 공유하는 영속 저장소를 기준으로 단일 처리 승자와 결과 재생 여부를 판단해야 합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 2 --&gt;
&lt;h2 id=&quot;six-concepts&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  2. 하나의 status로 합치지 않은 여섯 가지 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서는 거래와 사건을 하나의 &lt;code&gt;status&lt;/code&gt; 필드로 표현하지 않습니다. 각각의 값이 답해야 하는 질문이 다르기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;1. TransactionProcessingStatus&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래가 접수&amp;middot;분석&amp;middot;최종 대응 중 어느 단계에 있는가&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;2. RiskLevel&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule&amp;middot;ML 분석 결과의 위험 수준은 무엇인가&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;3. RiskResponseOutcome&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;위험 등급에 따라 어떤 Mock 대응을 적용했는가&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;4. CaseStatus&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;사건 조사가 어느 업무 단계에 있는가&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;5. FinalDisposition&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;담당자의 최종 조사 결과는 무엇인가&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d7dfef; border-radius: 15px; background-color: #f8faff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #2f4774; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;6. IdempotencyProcessingStatus&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;동일 명령이 처리 중인지, 완료됐는지, 실패가 확정됐는가&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  개념을 분리해야 하는 이유&lt;/h3&gt;
&lt;div style=&quot;margin: 25px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;p style=&quot;margin-top: 0; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;MEDIUM 거래&lt;/p&gt;
&lt;pre class=&quot;fix&quot; style=&quot;margin-bottom: 0; padding: 17px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;거래 처리 상태 = APPROVED
위험 등급 = MEDIUM
위험 대응 결과 = APPROVED_WITH_MONITORING
사건 = 생성하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 25px 0; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;조사 중인 사건&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;margin-bottom: 0; padding: 17px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;사건 상태 = IN_REVIEW
최종 판정 = null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 25px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;업무는 완료됐지만 응답 Snapshot 저장이 누락된 경우&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin-bottom: 0; padding: 17px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;거래 상태 = APPROVED
             또는 ADDITIONAL_AUTH_REQUIRED
             또는 HELD

멱등 상태 = IN_PROGRESS&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;code&gt;Transaction FAILED&lt;/code&gt; &amp;ne; &lt;code&gt;Idempotency FAILED&lt;/code&gt; &amp;ne; &lt;code&gt;AI Report FAILED&lt;/code&gt;&lt;br /&gt;각 실패는 원인, 영향 범위와 복구 방식이 서로 다릅니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 3 --&gt;
&lt;h2 id=&quot;transaction-state&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  3. 거래 처리 상태 전이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 상태는 요청 검증, 분석, 위험 대응과 최종 처리 결과를 구분합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;margin: 26px 0; padding: 22px; border-radius: 13px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;요청 형식 또는 도메인 Validation 실패
&amp;rarr; 거래 미생성
&amp;rarr; 400 또는 422 오류 응답
&amp;rarr; 로그&amp;middot;Trace&amp;middot;메트릭으로 관측

Validation 성공
&amp;rarr; RECEIVED
&amp;rarr; ANALYZING
   ├─ 분석 결과 확정 &amp;rarr; ANALYZED
   └─ 분석 처리 실패 &amp;rarr; FAILED

ANALYZED
├─ LOW      &amp;rarr; APPROVED
├─ MEDIUM   &amp;rarr; APPROVED
├─ HIGH     &amp;rarr; ADDITIONAL_AUTH_REQUIRED
└─ CRITICAL &amp;rarr; HELD&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  RECEIVED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;요청 검증을 통과해 거래가 최초 저장된 상태입니다. 접수 성공은 분석 성공이나 거래 승인을 의미하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  ANALYZING&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;거래&amp;middot;행동 이벤트&amp;middot;RuleVersion과 외부 위험정보를 바탕으로 이상거래 분석이 진행 중인 상태입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  ANALYZED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;탐지 결과와 Evidence가 저장되고 위험 등급이 정해진 상태입니다. 위험 대응과 사건 연결까지 끝난 최종 성공 상태는 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ APPROVED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;LOW 또는 MEDIUM 거래의 Mock 승인이 완료된 상태입니다. MEDIUM의 모니터링 여부는 별도 대응 결과로 관리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  ADDITIONAL_AUTH_REQUIRED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;HIGH 위험 거래에 Mock 추가 인증이 필요하다는 최종 처리 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;⏸️ HELD&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;CRITICAL 위험 거래를 Mock 보류한 상태입니다. 실제 고객 자금 동결이나 금융기관 거래 차단을 의미하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;❌ FAILED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Rule 분석이나 분석 오케스트레이션을 정상적으로 완료하지 못한 상태입니다. AI 리포트 생성 실패는 거래 FAILED의 원인이 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 2px solid #7a1f3d; border-radius: 15px; background-color: #fff8fa; text-align: center;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;ANALYZED = 분석 완료 &amp;ne; 전체 금융 업무 성공&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 4 --&gt;
&lt;h2 id=&quot;risk-response&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 4. 위험 등급과 위험 대응 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위험 등급과 최종 상태는 서로 독립적인 값을 아무렇게나 조합하지 않습니다. 승인된 정책이 허용하는 조합만 최종 결과로 사용합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 21px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;LOW&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;status = APPROVED
outcome = APPROVED
caseId = null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 21px; border: 1px solid #d7e6e5; border-radius: 15px; background-color: #f7ffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #22706e; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;MEDIUM&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;status = APPROVED
outcome = APPROVED_WITH_MONITORING
caseId = null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 21px; border: 1px solid #e8dcb9; border-radius: 15px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;HIGH&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;status = ADDITIONAL_AUTH_REQUIRED
outcome = ADDITIONAL_AUTH_REQUIRED
caseId = 필수&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 21px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;CRITICAL&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; style=&quot;margin-bottom: 0; padding: 15px; border-radius: 9px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 13px; line-height: 1.7;&quot;&gt;&lt;code&gt;status = HELD
outcome = HELD
caseId = 필수&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  MONITORING을 거래 상태로 두지 않은 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MEDIUM 거래는 승인 자체는 완료된 상태입니다. 따라서 거래 처리 상태는 &lt;code&gt;APPROVED&lt;/code&gt;로 유지하고, 승인 후 모니터링한다는 정책만 &lt;code&gt;APPROVED_WITH_MONITORING&lt;/code&gt;으로 분리합니다.&lt;/p&gt;
&lt;pre class=&quot;fix&quot; style=&quot;margin: 24px 0; padding: 19px; border-radius: 11px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;거래 상태 = APPROVED
위험 등급 = MEDIUM
대응 결과 = APPROVED_WITH_MONITORING&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⏸️ BLOCKED 대신 HELD를 사용한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 실제 금융기관의 거래 차단이나 고객 제재를 수행하지 않습니다. 따라서 CRITICAL 대응은 실제 차단을 의미하는 &lt;code&gt;BLOCKED&lt;/code&gt;보다 프로젝트 내부의 Mock 보류를 뜻하는 &lt;code&gt;HELD&lt;/code&gt;로 표현합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 5 --&gt;
&lt;h2 id=&quot;case-state&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 5. 사건 상태와 최종 판정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사건은 거래 처리와 별개의 조사 Workflow입니다. 사건이 열려 있다는 사실과 이상거래로 최종 확정됐다는 사실을 같은 값으로 표현하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot; style=&quot;margin: 26px 0; padding: 22px; border-radius: 13px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;OPEN
&amp;rarr; IN_REVIEW
&amp;rarr; ADDITIONAL_INFORMATION_REQUIRED
&amp;rarr; IN_REVIEW
&amp;rarr; CLOSED&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  OPEN&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;고위험 거래로 사건이 생성되어 검토를 기다리는 상태입니다. 최초에는 담당자가 없을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  IN_REVIEW&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;담당자가 거래, Evidence, 행동 타임라인과 연관 정보를 검토하는 상태입니다. 이 상태에는 담당자가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;❓ ADDITIONAL_INFORMATION_REQUIRED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;현재 정보만으로 판정하기 어려워 추가 자료가 필요한 상태입니다. 이 상태 자체가 이상거래 확정을 의미하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ CLOSED&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;조사와 필요한 기록이 완료된 상태입니다. 사건을 종료하려면 최종 판정이 반드시 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚖️ 최종 판정&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9; text-align: center;&quot;&gt;&lt;b&gt;NORMAL&lt;/b&gt;&lt;br /&gt;정상 거래로 확인&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff; text-align: center;&quot;&gt;&lt;b&gt;FALSE_POSITIVE&lt;/b&gt;&lt;br /&gt;오탐으로 확인&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9; text-align: center;&quot;&gt;&lt;b&gt;CONFIRMED_FRAUD&lt;/b&gt;&lt;br /&gt;이상거래로 확정&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;조사 진행 중
caseStatus = OPEN
             또는 IN_REVIEW
             또는 ADDITIONAL_INFORMATION_REQUIRED
finalDisposition = null

조사 종료
caseStatus = CLOSED
finalDisposition = NORMAL
                   또는 FALSE_POSITIVE
                   또는 CONFIRMED_FRAUD&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  초기 범위에서 허용하지 않는 전이&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;OPEN &amp;rarr; CLOSED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADDITIONAL_INFORMATION_REQUIRED &amp;rarr; CLOSED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLOSED &amp;rarr; IN_REVIEW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;최종 판정 없이 &lt;code&gt;CLOSED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;생성형 AI가 사건 상태 또는 최종 판정을 확정하는 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 6 --&gt;
&lt;h2 id=&quot;concurrency-audit&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  6. 상태 변경의 동시성&amp;middot;감사 원칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전이는 DB 문자열 하나를 바꾸는 작업이 아닙니다. 요청 당시의 현재 상태, 허용 전이, 변경 주체와 동시 수정 여부를 함께 검증해야 합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 모든 상태 변경에서 확인할 조건&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 상태가 요청자가 예상한 상태인가&lt;/li&gt;
&lt;li&gt;요청한 다음 상태가 허용되는가&lt;/li&gt;
&lt;li&gt;다른 요청이 먼저 값을 변경하지 않았는가&lt;/li&gt;
&lt;li&gt;변경 주체에게 필요한 권한이 있는가&lt;/li&gt;
&lt;li&gt;필수 사유와 연관 거래&amp;middot;사건 정보가 존재하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 두 분석 담당자가 같은 사건을 동시에 종료하려고 할 때, 나중 요청이 먼저 저장된 최종 판정을 조용히 덮어써서는 안 됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;현재 상태&amp;middot;동시성 버전 확인
&amp;rarr; 허용 전이 검증
&amp;rarr; 상태와 관련 데이터 변경
&amp;rarr; 감사 로그 생성
&amp;rarr; 하나의 정합성 경계에서 Commit&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  감사 로그에 남길 정보&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 12px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;변경 주체&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;변경 시각&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;이전 상태&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;변경 후 상태&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;변경 사유&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;transactionId&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;caseId&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;traceId&amp;middot;실행 식별자&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 상태로 다시 변경하는 요청도 무조건 성공시키지 않습니다. 실제 변경이 없는 정상 요청인지, 중복 명령인지, 오래된 화면에서 전송한 충돌 요청인지 구분할 수 있도록 명시적으로 검증합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 7 --&gt;
&lt;h2 id=&quot;key-fingerprint&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  7. Idempotency-Key와 Request Fingerprint&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Idempotency-Key&lt;/code&gt;만 비교해서는 동일한 금융 요청인지 안전하게 판단할 수 없습니다. 같은 Key가 전혀 다른 거래 내용에 재사용될 수도 있기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Idempotency-Key&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 업무 명령과 그 명령의 재시도를 식별합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Request Fingerprint&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Key와 함께 전달된 금융 업무 내용이 실제로 동일한지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!--
  이미지 2 삽입 위치

  파일명:
  finguardops_멱등_처리_흐름도.png

  대체 텍스트:
  Kubernetes 여러 Pod에서 공유 PostgreSQL 멱등 레코드로 중복 요청을 통제하고 Snapshot을 재생하는 흐름
--&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Cloud Native 멱등 요청 판단과 결과 재생.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJ6Rr/dJMcab6x1jf/feBPF2f6pCKKiR94DctW8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJ6Rr/dJMcab6x1jf/feBPF2f6pCKKiR94DctW8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJ6Rr/dJMcab6x1jf/feBPF2f6pCKKiR94DctW8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJ6Rr%2FdJMcab6x1jf%2FfeBPF2f6pCKKiR94DctW8K%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;1448&quot; height=&quot;1086&quot; data-filename=&quot;Cloud Native 멱등 요청 판단과 결과 재생.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;margin-top: 8px; color: #777777; font-size: 14px; line-height: 1.6; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;여러 Pod와 재시작 환경에서도 동일 요청의 중복 실행을 막고 최초로 확정한 결과를 재생하는 FinGuardOps 멱등 처리 흐름&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Fingerprint에 포함하는 업무 필드&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;transactionId
transactionType
amount
currencyCode
occurredAt
externalCustomerRef
senderAccountRef
recipientAccountRef
channel
deviceRef&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 JSON 문자열을 그대로 해시하지 않고, Validation과 정규화를 마친 업무 값을 고정된 순서로 직렬화한 뒤 SHA-256 Fingerprint를 계산합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 25px 0; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID &amp;rarr; canonical 문자열&lt;/li&gt;
&lt;li&gt;금액 &amp;rarr; canonical 10진 문자열&lt;/li&gt;
&lt;li&gt;시간 &amp;rarr; UTC ISO-8601 Instant&lt;/li&gt;
&lt;li&gt;Nullable 값 &amp;rarr; JSON null&lt;/li&gt;
&lt;li&gt;필드 순서 &amp;rarr; 고정&lt;/li&gt;
&lt;li&gt;Hash Algorithm &amp;rarr; SHA-256&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Fingerprint에서 제외하는 값&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;traceId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Idempotency-Key&lt;/code&gt; 자체&lt;/li&gt;
&lt;li&gt;서버 생성 시각&lt;/li&gt;
&lt;li&gt;내부 DB Primary Key&lt;/li&gt;
&lt;li&gt;현재 처리 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Idempotency-Key와 transactionId의 차이&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;Idempotency-Key
= 하나의 명령과 재시도를 식별

transactionId
= 생성되거나 처리되는 금융 거래 자원을 식별&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 식별자는 목적이 다르므로 서로 대체하지 않습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 8 --&gt;
&lt;h2 id=&quot;idempotency-decision&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  8. 멱등 처리 상태와 중복 요청 판단&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등 레코드는 동일한 업무 명령이 어느 상태인지 다음 세 값으로 표현합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff; text-align: center;&quot;&gt;&lt;b&gt;IN_PROGRESS&lt;/b&gt;&lt;br /&gt;처리 중 또는 결과 확정 여부가 불분명&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9; text-align: center;&quot;&gt;&lt;b&gt;COMPLETED&lt;/b&gt;&lt;br /&gt;최초 업무 결과와 Snapshot 확정&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9; text-align: center;&quot;&gt;&lt;b&gt;FAILED&lt;/b&gt;&lt;br /&gt;재생 가능한 업무 실패가 확정&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  새로운 Key&lt;/h3&gt;
&lt;pre class=&quot;&quot; style=&quot;margin: 20px 0; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;새 IN_PROGRESS Claim 생성
&amp;rarr; 실제 업무 처리 시작&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  같은 Key + 같은 Fingerprint&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IN_PROGRESS&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;&lt;b&gt;COMPLETED&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;External Risk&amp;middot;FastAPI&amp;middot;위험 대응&amp;middot;사건 생성을 다시 실행하지 않고 저장된 Response Snapshot을 재생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FAILED&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;자동 재처리하지 않고 저장된 안전한 실패 분류를 재생합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚠️ 같은 Key + 다른 Fingerprint&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9; text-align: center;&quot;&gt;&lt;code style=&quot;color: #8b2c2c; font-size: 17px; font-weight: bold;&quot;&gt;
    IDEMPOTENCY_KEY_CONFLICT
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 멱등 상태가 무엇이든 같은 Key를 다른 거래 내용에 재사용할 수 없도록 거부합니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;처리 완료 여부가 불확실한 요청을 자동 재실행하지 않습니다. 불확실 상태에서의 자동 재실행은 중복 거래&amp;middot;사건&amp;middot;외부 호출을 만들 수 있기 때문입니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 9 --&gt;
&lt;h2 id=&quot;response-snapshot&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  9. Response Snapshot과 최초 결과 재생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 거래 생성 명령이 다시 들어왔을 때 PostgreSQL에서 거래와 사건의 최신 상태를 조회해 반환하지 않습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 2px solid #7a1f3d; border-radius: 15px; background-color: #fff8fa; text-align: center;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;멱등 재생 = 최초 명령 결과 재현&lt;br /&gt;현재 상태 조회 = 별도의 조회 API 책임&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 생성 이후 사건 조사가 진행되거나 사건 상태가 변경되더라도, 같은 거래 생성 명령의 재시도에는 당시 최초로 확정한 응답을 반환합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Snapshot Envelope&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;responseBody
httpStatus
responseSchemaVersion
codecVersion
finalizedAt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  업무 응답에 포함할 값&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;transactionId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processingStatus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;riskLevel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;riskResponseOutcome&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adoptedDetectionResultId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caseId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createdAt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Snapshot에 저장하지 않을 값&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Idempotency-Key&lt;/li&gt;
&lt;li&gt;Request Fingerprint&lt;/li&gt;
&lt;li&gt;내부 DB Primary Key&lt;/li&gt;
&lt;li&gt;Credential과 Secret&lt;/li&gt;
&lt;li&gt;민감한 금융 참조값&lt;/li&gt;
&lt;li&gt;traceId&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;traceId&lt;/code&gt;는 최초 요청과 재요청마다 달라질 수 있으므로 Snapshot에는 저장하지 않고, 저장된 업무 결과와 현재 요청의 Trace를 결합합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Schema와 Codec에 버전을 두는 이유&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;구버전 Pod
&amp;rarr; Snapshot v1 생성

신버전 Pod
&amp;rarr; 기존 v1을 읽을 수 있음
&amp;rarr; 신규 요청에는 v2 생성

Rolling Update
&amp;rarr; v1&amp;middot;v2가 일정 시간 함께 존재&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 Schema와 Codec 버전을 명시하면 과거 요청 결과를 최신 구조로 잘못 해석하거나 소급 변경하는 문제를 방지할 수 있습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 10 --&gt;
&lt;h2 id=&quot;completion-gap&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 10. 최종 성공 경계와 완료 간극 복구&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 거래 상태에서는 성공 Response Snapshot을 확정하지 않습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 200px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff; text-align: center;&quot;&gt;&lt;b&gt;RECEIVED&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 200px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff; text-align: center;&quot;&gt;&lt;b&gt;ANALYZING&lt;/b&gt;&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 200px; padding: 19px; border: 1px solid #d8e4f5; border-radius: 13px; background-color: #f8fbff; text-align: center;&quot;&gt;&lt;b&gt;ANALYZED&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 성공은 분석 결과가 존재한다는 사실만으로 결정하지 않습니다. 다음 업무가 모두 확정되어야 합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ol style=&quot;margin: 0; padding-left: 23px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Rule 분석 결과 저장&lt;/li&gt;
&lt;li&gt;DetectionResult 완료&lt;/li&gt;
&lt;li&gt;Evidence 저장&lt;/li&gt;
&lt;li&gt;거래가 탐지 결과 채택&lt;/li&gt;
&lt;li&gt;위험 등급 확정&lt;/li&gt;
&lt;li&gt;위험 대응 결과 확정&lt;/li&gt;
&lt;li&gt;최종 거래 상태 전이&lt;/li&gt;
&lt;li&gt;HIGH&amp;middot;CRITICAL이면 사건 생성 또는 기존 사건 연결&lt;/li&gt;
&lt;li&gt;감사 로그와 필수 업무 Commit 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚠️ 완료 간극&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;거래&amp;middot;탐지&amp;middot;위험 대응&amp;middot;사건 Commit 성공
&amp;rarr; Response Snapshot 저장 또는 COMPLETED 전이 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 금융 업무 결과가 Commit되었다면 응답 저장 실패를 이유로 거래나 사건을 되돌리거나 업무를 다시 실행해서는 안 됩니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;유지할 것&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최종 거래 상태&lt;/li&gt;
&lt;li&gt;탐지 결과와 Evidence&lt;/li&gt;
&lt;li&gt;위험 대응 결과&lt;/li&gt;
&lt;li&gt;사건과 감사 기록&lt;/li&gt;
&lt;li&gt;멱등 상태 IN_PROGRESS&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;금지할 것&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;External Risk 재호출&lt;/li&gt;
&lt;li&gt;FastAPI 재호출&lt;/li&gt;
&lt;li&gt;새 DetectionResult 생성&lt;/li&gt;
&lt;li&gt;위험 대응 재실행&lt;/li&gt;
&lt;li&gt;새 사건 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  복구는 새로운 업무 실행이 아니다&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;PostgreSQL의 확정 상태 검증
&amp;rarr; 위험 등급&amp;middot;최종 상태&amp;middot;사건 연결 조합 확인
&amp;rarr; 동일한 Response Snapshot 결정적 재구성
&amp;rarr; 기존 멱등 Record를 COMPLETED로 전이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료 간극 복구는 거래를 재분석하거나 새 사건을 만드는 작업이 아니라, 이미 확정된 결과를 확인해 누락된 응답 기록만 복원하는 운영 작업입니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 11 --&gt;
&lt;h2 id=&quot;cloud-native-validation&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;☁️ 11. Cloud Native 환경의 운영&amp;middot;검증 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성은 코드 한 곳에 &lt;code&gt;if&lt;/code&gt; 문을 추가하는 것으로 끝나지 않습니다. 여러 인스턴스, 재시작, 배포 전환과 장애 상황에서 같은 계약이 유지되는지 검증해야 합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  여러 Pod의 동시 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod A와 Pod B가 같은 요청을 동시에 받더라도 애플리케이션 메모리가 아니라 PostgreSQL의 공유 멱등 Record와 Unique Constraint를 기준으로 실제 처리 승자를 하나만 정합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  애플리케이션 재시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스가 종료되더라도 다음 정보는 영속 저장소에 남아야 합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request Fingerprint&lt;/li&gt;
&lt;li&gt;멱등 처리 상태&lt;/li&gt;
&lt;li&gt;연결된 transactionId&lt;/li&gt;
&lt;li&gt;Response Snapshot&lt;/li&gt;
&lt;li&gt;안전하게 공개할 실패 분류&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Rolling Update&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신버전이 구버전 Snapshot을 읽을 수 있는가&lt;/li&gt;
&lt;li&gt;새 응답 Schema가 기존 Snapshot을 손상하지 않는가&lt;/li&gt;
&lt;li&gt;상태 Enum 추가가 구버전과 충돌하지 않는가&lt;/li&gt;
&lt;li&gt;DB Migration이 구버전&amp;middot;신버전의 공존을 허용하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  관측할 지표&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 12px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;동일 요청 재생 횟수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;Idempotency-Key 충돌 수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;IN_PROGRESS 체류 시간&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;장기 IN_PROGRESS 수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;완료 간극 발생 수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;복구 성공&amp;middot;거부&amp;middot;실패 수&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;거래 최종 처리시간&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 210px; padding: 18px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;금지된 상태 전이 요청 수&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 운영 복구 원칙&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불확실 상태의 자동 재분석 금지&lt;/li&gt;
&lt;li&gt;Scheduler&amp;middot;Batch로 무조건 반복 복구하지 않기&lt;/li&gt;
&lt;li&gt;승인된 내부 Record 하나만 대상으로 처리&lt;/li&gt;
&lt;li&gt;복구 과정에서 외부 서비스 재호출 금지&lt;/li&gt;
&lt;li&gt;실행 주체&amp;middot;시각&amp;middot;판정&amp;middot;결과를 감사 가능하게 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 36px; color: #356a3c; font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;  검증할 Acceptance Criteria&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ol style=&quot;margin: 0; padding-left: 23px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;같은 Key&amp;middot;같은 요청을 동시에 두 번 보내도 거래는 하나만 생성되는가&lt;/li&gt;
&lt;li&gt;같은 Key에 다른 거래 내용을 보내면 충돌하는가&lt;/li&gt;
&lt;li&gt;COMPLETED 재요청에서 External Risk와 FastAPI가 다시 호출되지 않는가&lt;/li&gt;
&lt;li&gt;애플리케이션 재시작 뒤에도 Snapshot을 재생할 수 있는가&lt;/li&gt;
&lt;li&gt;Rolling Update 중 구버전 Snapshot을 신버전이 해석할 수 있는가&lt;/li&gt;
&lt;li&gt;업무 Commit 뒤 Snapshot 저장이 실패해도 거래와 사건을 다시 실행하지 않는가&lt;/li&gt;
&lt;li&gt;허용되지 않은 거래&amp;middot;사건 상태 전이가 거부되는가&lt;/li&gt;
&lt;li&gt;동시 사건 종료 요청이 서로의 판정을 조용히 덮어쓰지 않는가&lt;/li&gt;
&lt;li&gt;AI 리포트 실패가 거래 상태와 사건 최종 판정을 변경하지 않는가&lt;/li&gt;
&lt;li&gt;복구 실행의 주체&amp;middot;판정&amp;middot;결과가 감사 가능하게 남는가&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 36px; color: #64182f; font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;  상태 계약을 AI Agent가 임의로 바꾸지 못하게 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 상태, 사건 상태, 허용 전이, Fingerprint 구성과 Snapshot 버전은 API&amp;middot;DB&amp;middot;테스트&amp;middot;운영 복구 절차에 동시에 영향을 주는 핵심 계약입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent는 구현&amp;middot;테스트&amp;middot;문서 초안을 만들 수 있지만, 새로운 상태 추가, 허용 전이 변경, Fingerprint 필드 변경, Snapshot 버전 전환과 DB Migration을 임의로 확정할 수 없도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;ADR 승인
&amp;rarr; API 영향 검토
&amp;rarr; DB Migration 검토
&amp;rarr; 구버전 호환성 확인
&amp;rarr; 단위&amp;middot;통합&amp;middot;동시성 테스트
&amp;rarr; 사용자 최종 승인
&amp;rarr; Pull Request merge&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;생성형 AI와 AI Agent는 상태 계약을 구현하는 도구이며, 금융 업무 규칙과 호환성&amp;middot;배포 승인 권한을 대신하지 않습니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 12 --&gt;
&lt;h2 id=&quot;closing&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  12. 마치며: 상태 모델을 데이터베이스로 옮기기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서는 거래 처리 단계, 위험 분석 결과, 대응 정책, 사건 조사 단계, 최종 판정과 멱등 명령 상태를 서로 분리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 하나의 &lt;code&gt;status&lt;/code&gt;에 여러 의미가 섞여 모순된 상태 조합이 만들어지는 문제를 줄이고, 각 상태의 변경 조건과 책임자를 명확하게 정의할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%); text-align: center;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 10px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 정합성 설계&lt;/p&gt;
&lt;code style=&quot;color: #333333; font-size: 15px; font-weight: bold; line-height: 2.1;&quot;&gt;
    상태 전이
    &amp;rarr; 업무가 어디까지 확정됐는지 표현

    멱등성
    &amp;rarr; 같은 명령의 중복 실행 통제

    Response Snapshot
    &amp;rarr; 최초 확정 결과 재현

    완료 간극 복구
    &amp;rarr; 업무를 재실행하지 않고 누락된 응답만 복원

    영속 멱등 Record
    &amp;rarr; 여러 Pod&amp;middot;재시작&amp;middot;Rolling Update에서도 계약 유지
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태와 멱등성 규칙을 정의한 다음에는 이 규칙을 애플리케이션 코드뿐 아니라 데이터베이스 제약조건으로도 강제해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 거래, 행동 이벤트, 탐지 결과, RuleVersion, 사건, 감사 로그와 멱등성 Record를 어떤 관계로 설계했는지 PostgreSQL ERD와 Flyway Migration 기준을 정리하겠습니다.&lt;/p&gt;
&lt;!-- 다음 편 예고 --&gt;
&lt;div style=&quot;margin: 32px 0; padding: 22px 24px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 8px; color: #234f88; font-size: 17px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[FinGuardOps #4] 금융&amp;nbsp;업무&amp;nbsp;정합성을&amp;nbsp;지키는&amp;nbsp;ERD&amp;middot;PostgreSQL&amp;middot;Flyway&amp;nbsp;설계&lt;/b&gt;&lt;br /&gt;거래&amp;middot;행동 이벤트&amp;middot;탐지 결과&amp;middot;사건&amp;middot;감사 로그&amp;middot;멱등성 Record의 관계와 Foreign Key, Unique, Check Constraint, Index와 Migration 전략을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- GitHub 저장소 버튼 --&gt;
&lt;div style=&quot;margin: 40px 0 12px 0; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 13px 20px; border-radius: 10px; background-color: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36, 41, 47, 0.2);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;GitHub&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt; &lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>FinGuardOps/Project</category>
      <category>CloudApplicationModernization</category>
      <category>CloudNative</category>
      <category>FDS</category>
      <category>FinGuardOps</category>
      <category>FraudOps</category>
      <category>개인프로젝트</category>
      <category>멱등성</category>
      <category>상태전이</category>
      <category>이상거래탐지</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/48</guid>
      <comments>https://bbbicsan.tistory.com/48#entry48comment</comments>
      <pubDate>Mon, 31 Aug 2026 11:14:51 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #2] 전체 시스템 구성과 기술별 역할을 어떻게 나눴는가</title>
      <link>https://bbbicsan.tistory.com/47</link>
      <description>&lt;div style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all;&quot;&gt;&lt;!-- 시리즈 배지 --&gt;
&lt;div style=&quot;display: inline-block; margin-bottom: 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background-color: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold; letter-spacing: 0.2px;&quot;&gt;FinGuardOps Project Series #2&lt;/div&gt;
&lt;!-- 도입부 --&gt;
&lt;p style=&quot;font-size: 18px; line-height: 1.9;&quot; data-ke-size=&quot;size16&quot;&gt;지난 글에서는 금융 이상거래 탐지에서 끝나지 않고 위험 대응, 사건 처리와 AI 운영까지 연결하는 &lt;b&gt;FinGuardOps를 기획한 이유&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;금융 업무 상태와 AI 계산을 하나의 애플리케이션에 모두 넣을지, 처음부터 전체 시스템을 마이크로서비스로 분리할지, PostgreSQL&amp;middot;Redis&amp;middot;Kafka는 각각 어떤 역할을 가져야 할지 고민했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 기술을 많이 사용하는 것보다 &lt;b&gt; 금융 업무 정합성, 계산 책임, 데이터 소유권과 장애 경계를 명확히 나누는 것 &lt;/b&gt; 을 우선했습니다.&lt;/p&gt;
&lt;!-- 핵심 설계 원칙 --&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;금융 업무 상태의 최종 책임은 &lt;b&gt;Spring Boot&lt;/b&gt;가 가지고, &lt;b&gt;FastAPI&lt;/b&gt;는 분석 결과 계산을 담당하며, &lt;b&gt;PostgreSQL&lt;/b&gt;은 영속 업무 데이터의 원본이 되도록 전체 구조를 설계했습니다.&lt;/blockquote&gt;
&lt;!--
  이 위치에 이미지를 삽입합니다.

  파일명:
  finguardops_전체_시스템_구성.png

  대체 텍스트:
  Spring Boot, FastAPI, PostgreSQL, Redis, Kafka, Docker Compose,
  Kubernetes, AWS와 Observability로 구성한 FinGuardOps 전체 시스템 구조
--&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_전체_시스템_구성.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyakhL/dJMcafVsGJO/dGRVfr0EEuGNfjuf2K7LXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyakhL/dJMcafVsGJO/dGRVfr0EEuGNfjuf2K7LXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyakhL/dJMcafVsGJO/dGRVfr0EEuGNfjuf2K7LXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyakhL%2FdJMcafVsGJO%2FdGRVfr0EEuGNfjuf2K7LXK%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;1448&quot; height=&quot;1086&quot; data-filename=&quot;finguardops_전체_시스템_구성.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;margin-top: 8px; color: #777777; font-size: 14px; line-height: 1.6; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그림 1. 금융 업무, AI 분석, 데이터, 인프라와 Observability의 책임을 계층별로 구분한 FinGuardOps의 목표 시스템 구성&lt;/p&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 18px 21px; border-left: 5px solid #666666; border-radius: 0 12px 12px 0; background-color: #f7f7f7; color: #444444;&quot; data-ke-style=&quot;style1&quot;&gt;이 글은 특정 날짜의 구현 현황을 정리하는 글이 아니라, FinGuardOps를 기획하면서 정한 &lt;b&gt;목표 아키텍처, 기술별 책임과 Cloud Application Modernization 전략&lt;/b&gt;을 소개하는 글입니다. 실제 운영 중인 레거시 시스템을 직접 이전했다고 주장하기보다, 금융 업무 애플리케이션을 컨테이너&amp;middot;CI/CD&amp;middot;Observability&amp;middot;Kubernetes&amp;middot;AWS 기반으로 단계적으로 현대화하는 기준을 설명합니다.&lt;/blockquote&gt;
&lt;!-- 목차 --&gt;
&lt;div style=&quot;margin: 38px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fffafb; box-shadow: 0 4px 14px rgba(122, 31, 61, 0.06);&quot;&gt;
&lt;h2 id=&quot;table-of-contents&quot; style=&quot;margin: 0 0 16px 0; color: #7a1f3d; font-size: 23px; border: none;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#architecture-principles&quot;&gt;   전체 구조를 설계하며 세운 기준 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#system-flow&quot;&gt;  ️ FinGuardOps의 전체 시스템 흐름 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#spring-backend&quot;&gt; ☕ Backend &amp;mdash; Spring Boot Modular Monolith &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#fastapi-service&quot;&gt;   AI Service &amp;mdash; FastAPI &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#frontend&quot;&gt;  ️ Frontend &amp;mdash; React와 TypeScript &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#data-external&quot;&gt;  ️ Data와 External Service &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#infra-observability&quot;&gt; ☁️ Infra&amp;middot;DevOps&amp;middot;Cloud와 Observability &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#responsibility-boundary&quot;&gt;   시스템 사이의 책임과 장애 경계 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#why-not-msa&quot;&gt;   처음부터 전체 MSA로 구성하지 않은 이유 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#development-roadmap&quot;&gt;  ️ 단계별 개발 로드맵과 검증 방향 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#closing&quot;&gt;   마치며: 다음은 상태 전이와 멱등성 &lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 1 --&gt;
&lt;h2 id=&quot;architecture-principles&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 전체 구조를 설계하며 세운 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 아키텍처는 사용하고 싶은 기술을 먼저 정한 뒤 기능을 끼워 맞추는 방식으로 설계하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 금융 이상거래 탐지와 사건 처리 과정에서 반드시 지켜야 할 원칙을 정하고, 각 원칙을 실현하는 데 적합한 구성요소와 기술을 선택했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 21px 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;☁️ Modernization은 단순 Cloud 이전이 아니다&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션을 클라우드에 올리는 것만으로 Modernization이 완성된다고 보지 않았습니다. 실행 환경 표준화, 자동화된 검증과 배포, 관측 가능성, 장애 격리, 서비스별 확장, 데이터 소유권과 운영 비용까지 함께 바꾸는 것을 목표로 구조를 설계했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  금융 업무 정합성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래, 탐지 결과, 위험 대응, 사건과 감사 로그가 중복되거나 일부만 반영되어서는 안 됩니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 거래와 중복 사건 방지&lt;/li&gt;
&lt;li&gt;허용된 상태 전이만 수행&lt;/li&gt;
&lt;li&gt;연관 업무 데이터의 함께 Commit 또는 Rollback&lt;/li&gt;
&lt;li&gt;최종 결과와 감사 이력의 일관성 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  계산과 업무 결정의 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rule&amp;middot;ML과 생성형 AI는 분석 결과를 계산하지만 금융 업무 상태를 직접 확정하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FastAPI는 위험 점수와 탐지 근거 계산&lt;/li&gt;
&lt;li&gt;Spring Boot는 분석 결과 계약 검증&lt;/li&gt;
&lt;li&gt;승인된 위험 대응 정책 적용&lt;/li&gt;
&lt;li&gt;거래&amp;middot;사건 상태의 최종 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 명확한 데이터 소유권&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 저장 기술의 역할을 분리하고, 업무 데이터의 원본이 여러 곳에 흩어지지 않도록 설계했습니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostgreSQL은 영속 업무 데이터 원본&lt;/li&gt;
&lt;li&gt;Redis는 재생성 가능한 캐시와 집계&lt;/li&gt;
&lt;li&gt;Kafka는 비동기 이벤트 전달&lt;/li&gt;
&lt;li&gt;Observability는 기술 상태 관측&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 20px;&quot; data-ke-size=&quot;size23&quot;&gt;  장애 범위의 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성마다 시스템에 미치는 영향이 다르므로 모든 장애를 하나의 실패로 처리하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 장애와 분석 서비스 장애 구분&lt;/li&gt;
&lt;li&gt;캐시 장애와 업무 데이터 유실 구분&lt;/li&gt;
&lt;li&gt;LLM 실패와 Rule&amp;middot;ML 판단 실패 구분&lt;/li&gt;
&lt;li&gt;관측 시스템 실패와 금융 업무 실패 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  핵심 업무부터 단계적으로 확장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker, Kafka, Kubernetes와 AWS를 사용하는 사실보다 각 기술이 어떤 문제를 해결하기 위해 필요한지를 설명할 수 있어야 한다고 판단했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 25px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%); text-align: center;&quot;&gt;&lt;code style=&quot;color: #4a2632; font-size: 15px; font-weight: bold; line-height: 2.1;&quot;&gt;
    핵심 거래&amp;middot;탐지&amp;middot;사건 흐름
    &amp;rarr; PostgreSQL 기반 업무 정합성
    &amp;rarr; Docker Compose 기반 통합 실행
    &amp;rarr; Observability
    &amp;rarr; Redis&amp;middot;Kafka 기반 확장
    &amp;rarr; Kubernetes 운영
    &amp;rarr; AWS 클라우드 배포
  &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 모든 기술을 동시에 적용하면 금융 업무 로직보다 인프라 설정과 분산 시스템 문제가 프로젝트의 중심이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 핵심 거래&amp;middot;탐지&amp;middot;사건 흐름을 먼저 설계하고, 그 위에 로컬 통합 환경, 관측, 비동기 처리, 컨테이너 오케스트레이션과 클라우드 운영을 단계적으로 확장하는 방향으로 구상했습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 2 --&gt;
&lt;h2 id=&quot;system-flow&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 2. FinGuardOps의 전체 시스템 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 FDS 분석 담당자와 플랫폼&amp;middot;클라우드 운영자가 React 관리자 화면을 통해 금융 업무와 플랫폼 운영 정보를 확인하는 구조로 구상했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 금융 업무 처리의 중심에서 PostgreSQL, External Risk와 FastAPI AI Service를 조정합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;margin: 26px 0; padding: 22px; border-radius: 13px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;FDS 분석 담당자 / 플랫폼&amp;middot;클라우드 운영자
                    &amp;darr;
            React + TypeScript
                    &amp;darr; REST API
       Spring Boot Modular Monolith
          ├─ PostgreSQL
          ├─ External Risk Service
          ├─ Redis
          ├─ Kafka
          └─ FastAPI AI Service
                 ├─ Rule Engine
                 ├─ ML Inference
                 └─ AI Report&amp;middot;Fallback

Spring Boot / FastAPI / Data / Messaging
                    &amp;darr;
          Metrics &amp;middot; Logs &amp;middot; Traces
                    &amp;darr;
Prometheus &amp;middot; Grafana &amp;middot; Loki &amp;middot; OpenTelemetry

서비스 실행&amp;middot;배포 환경
&amp;rarr; Docker Compose &amp;middot; Kubernetes &amp;middot; AWS &amp;middot; GitHub Actions&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  거래 요청의 기본 처리 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 거래 처리 흐름은 다음과 같이 설계했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb;&quot;&gt;
&lt;ol style=&quot;margin: 0; padding-left: 23px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;거래 요청과 Idempotency-Key를 검증합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;동일 요청을 식별하기 위한 Request Fingerprint를 계산합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;동일 멱등 요청 중 하나만 실제 업무 처리를 수행하도록 제어합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;금융 거래를 PostgreSQL에 저장하고 초기 상태를 확정합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;External Risk Service에서 외부 위험정보를 조회합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;거래&amp;middot;행동 이벤트&amp;middot;RuleVersion을 분석 Snapshot으로 구성합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;FastAPI에 Rule&amp;middot;ML 분석을 요청합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;Spring Boot가 분석 결과의 버전과 완전성을 검증합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 10px;&quot;&gt;위험도별 대응을 적용하고 필요한 사건을 생성합니다.&lt;/li&gt;
&lt;li&gt;최종 결과와 감사 이력을 저장하고 중복 요청에는 동일한 응답을 재생합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;External Risk와 FastAPI 호출은 긴 DB 트랜잭션 안에서 실행하지 않는 방향으로 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 응답을 기다리는 동안 거래 잠금과 DB Connection을 계속 점유하지 않도록 짧은 영속화 트랜잭션과 외부 호출 경계를 분리합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 3 --&gt;
&lt;h2 id=&quot;spring-backend&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;☕ 3. Backend &amp;mdash; Spring Boot Modular Monolith&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 FinGuardOps에서 요청을 전달하기만 하는 단순 API Gateway가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 거래&amp;middot;탐지&amp;middot;위험 대응&amp;middot;사건&amp;middot;감사로 이어지는 금융 업무의 최종 책임 &lt;/b&gt; 을 담당하도록 설계했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 12px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;☕ Spring Boot가 담당할 책임&lt;/p&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래&amp;middot;행동 이벤트 요청 검증&lt;/li&gt;
&lt;li&gt;거래 요청 멱등성 처리&lt;/li&gt;
&lt;li&gt;거래와 탐지 결과 상태 전이&lt;/li&gt;
&lt;li&gt;External Risk 조회 오케스트레이션&lt;/li&gt;
&lt;li&gt;FastAPI 분석 요청과 Trace 전달&lt;/li&gt;
&lt;li&gt;분석 응답의 요청 연결&amp;middot;버전&amp;middot;완전성 검증&lt;/li&gt;
&lt;li&gt;위험 등급별 대응 정책 적용&lt;/li&gt;
&lt;li&gt;HIGH&amp;middot;CRITICAL 거래의 사건 생성과 연결&lt;/li&gt;
&lt;li&gt;상태 변경과 의사결정의 감사 로그 기록&lt;/li&gt;
&lt;li&gt;최종 응답 Snapshot 저장과 멱등 재생&lt;/li&gt;
&lt;li&gt;AI 리포트 상태와 사용량&amp;middot;토큰&amp;middot;비용 영속화&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  왜 Modular Monolith로 시작하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래, 탐지 결과, 위험 대응, 사건과 감사 로그는 서로 완전히 독립적인 데이터가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고위험 거래를 추가 인증 상태로 전환하면서 사건을 생성하고 감사 로그를 남겨야 한다면, 이러한 변경은 함께 성공하거나 함께 Rollback되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 처음부터 여러 서비스로 분리하면 다음 문제를 핵심 업무 구현과 동시에 해결해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 트랜잭션과 최종 일관성&lt;/li&gt;
&lt;li&gt;중복 이벤트와 Consumer 멱등성&lt;/li&gt;
&lt;li&gt;서비스 간 네트워크 실패&lt;/li&gt;
&lt;li&gt;부분 성공에 대한 보상 처리&lt;/li&gt;
&lt;li&gt;독립 배포와 장애 대응 체계&lt;/li&gt;
&lt;li&gt;서비스별 데이터 소유권 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 구조에서는 이 복잡성보다 금융 업무 흐름의 완성도를 우선하기 위해 하나의 Spring Boot 애플리케이션 안에서 논리적인 모듈 경계를 유지하는 Modular Monolith를 선택했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 12px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;transaction&lt;/b&gt;&lt;br /&gt;거래 접수&amp;middot;검증&amp;middot;멱등성&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;behavior&lt;/b&gt;&lt;br /&gt;사용자 행동 이벤트&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;detection&lt;/b&gt;&lt;br /&gt;분석 요청과 결과 관리&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;risk response&lt;/b&gt;&lt;br /&gt;위험도별 대응 정책&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;case management&lt;/b&gt;&lt;br /&gt;사건 생성&amp;middot;조사&amp;middot;판정&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;audit&lt;/b&gt;&lt;br /&gt;주요 변경 이력&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;rule management&lt;/b&gt;&lt;br /&gt;Rule&amp;middot;정책 버전 관리&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 180px; padding: 17px; border: 1px solid #e5e5e5; border-radius: 12px; background-color: #ffffff;&quot;&gt;&lt;b&gt;AI operations&lt;/b&gt;&lt;br /&gt;리포트&amp;middot;사용량&amp;middot;비용&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;Spring Boot는 FastAPI가 반환한 위험 점수를 그대로 거래 상태에 적용하지 않습니다. 거래 처리 상태, 분석 결과 버전, 탐지 근거와 승인된 대응 정책을 검증한 뒤 금융 업무 상태를 변경합니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 4 --&gt;
&lt;h2 id=&quot;fastapi-service&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  4. AI Service &amp;mdash; FastAPI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI AI Service는 금융 업무 상태를 직접 변경하는 서비스가 아니라 &lt;b&gt;분석 결과를 계산하는 독립 서비스&lt;/b&gt;로 구성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python의 데이터 처리와 AI 생태계를 활용하면서도 AI 코드 변경이 금융 업무 상태를 직접 변경하지 못하도록 Spring Boot와 별도 프로세스로 분리했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #dbe8dc; border-radius: 16px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ FastAPI의 책임&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Feature 계산&lt;/li&gt;
&lt;li&gt;Rule 실행&lt;/li&gt;
&lt;li&gt;ML 추론&lt;/li&gt;
&lt;li&gt;위험 점수와 위험 등급 계산&lt;/li&gt;
&lt;li&gt;Reason Code와 Evidence 생성&lt;/li&gt;
&lt;li&gt;모델 라우팅&lt;/li&gt;
&lt;li&gt;AI 사건 리포트 생성&lt;/li&gt;
&lt;li&gt;Rule&amp;middot;ML 기반 템플릿 fallback&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #eed5d5; border-radius: 16px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  FastAPI가 하지 않는 일&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래 상태 직접 변경&lt;/li&gt;
&lt;li&gt;사건 직접 생성&lt;/li&gt;
&lt;li&gt;사건 상태 직접 변경&lt;/li&gt;
&lt;li&gt;최종 이상거래 판정&lt;/li&gt;
&lt;li&gt;거래 승인&amp;middot;보류&amp;middot;추가 인증 결정&lt;/li&gt;
&lt;li&gt;Spring Boot 업무 데이터 직접 영속화&lt;/li&gt;
&lt;li&gt;Rule 활성 상태와 버전 원본 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚙️ 초기 Rule 분석 범위&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고액 거래와 주요 행동 이벤트를 분석하는 초기 Rule 집합&lt;/li&gt;
&lt;li&gt;RuleVersion 기반 실행 계획&lt;/li&gt;
&lt;li&gt;같은 입력에 같은 결과를 반환하는 결정적 Rule 평가&lt;/li&gt;
&lt;li&gt;가중치와 그룹 상한을 이용한 위험 점수 계산&lt;/li&gt;
&lt;li&gt;Reason Code와 설명 가능한 Evidence 생성&lt;/li&gt;
&lt;li&gt;Spring Boot와 통신하기 위한 내부 분석 API&lt;/li&gt;
&lt;li&gt;Trace와 요청&amp;middot;응답 계약 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  확장할 AI 분석 범위&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #ded8ee; border-radius: 15px; background-color: #fbf9ff;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복합 패턴을 보완하는 ML 모델 추론과 평가&lt;/li&gt;
&lt;li&gt;HIGH&amp;middot;CRITICAL 사건 중심 생성형 AI 리포트&lt;/li&gt;
&lt;li&gt;LLM Provider 연동&lt;/li&gt;
&lt;li&gt;사건 복잡도 기반 모델 라우팅&lt;/li&gt;
&lt;li&gt;LLM 실패 시 Rule&amp;middot;ML 기반 템플릿 fallback&lt;/li&gt;
&lt;li&gt;입력&amp;middot;출력 토큰과 호출 비용 측정&lt;/li&gt;
&lt;li&gt;리포트 형식과 품질 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Service는 설명 가능하고 테스트하기 쉬운 Rule 분석을 첫 단계로 두고, 이후 ML과 생성형 AI 리포트를 추가하는 방향으로 구성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성형 AI는 위험 판단을 대신하는 것이 아니라 고위험 사건을 검토하는 담당자의 이해를 돕는 보조 수단으로 사용합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 5 --&gt;
&lt;h2 id=&quot;frontend&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 5. Frontend &amp;mdash; React와 TypeScript&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frontend는 FinGuardOps의 두 사용자 역할에 맞춰 &lt;b&gt;FDS 업무 화면&lt;/b&gt;과 &lt;b&gt;플랫폼 운영 화면&lt;/b&gt;으로 구분합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ FDS 분석 담당자 화면&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래 모니터링과 위험 거래 필터링&lt;/li&gt;
&lt;li&gt;위험 점수와 위험 등급 확인&lt;/li&gt;
&lt;li&gt;Rule&amp;middot;ML 탐지 Evidence&lt;/li&gt;
&lt;li&gt;사용자 행동 타임라인&lt;/li&gt;
&lt;li&gt;사건 대기열과 사건 상세&lt;/li&gt;
&lt;li&gt;조사 메모와 담당자 관리&lt;/li&gt;
&lt;li&gt;정상&amp;middot;오탐&amp;middot;이상거래 판정&lt;/li&gt;
&lt;li&gt;감사 이력과 AI 사건 리포트&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 플랫폼&amp;middot;클라우드 운영자 화면&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 Health와 배포 버전&lt;/li&gt;
&lt;li&gt;오류율&amp;middot;처리량과 지연시간 요약&lt;/li&gt;
&lt;li&gt;External Risk&amp;middot;Rule&amp;middot;ML&amp;middot;LLM 상태&lt;/li&gt;
&lt;li&gt;AI 모델별 호출량과 비용&lt;/li&gt;
&lt;li&gt;캐시 적중률과 fallback 비율&lt;/li&gt;
&lt;li&gt;장애&amp;middot;대응&amp;middot;복구 이력&lt;/li&gt;
&lt;li&gt;배포 이력과 업무 영향 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  React와 Grafana의 역할 구분&lt;/h3&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt; ️ React 관리자 화면&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래와 사건 업무 상태&lt;/li&gt;
&lt;li&gt;조사 메모와 최종 판정&lt;/li&gt;
&lt;li&gt;Rule과 운영 정책&lt;/li&gt;
&lt;li&gt;AI 비용&amp;middot;장애&amp;middot;배포 이력 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 21px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  Grafana&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스별 응답시간&lt;/li&gt;
&lt;li&gt;오류율과 처리량&lt;/li&gt;
&lt;li&gt;DB Connection Pool&lt;/li&gt;
&lt;li&gt;Kafka Consumer Lag&lt;/li&gt;
&lt;li&gt;런타임&amp;middot;인프라 시계열 지표&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 18px 21px; border-left: 5px solid #7a1f3d; border-radius: 0 12px 12px 0; background-color: #fff7fa; color: #444444;&quot; data-ke-style=&quot;style1&quot;&gt;React는 금융 업무와 운영 이력을 처리하는 제품 화면을 담당하고, Grafana는 기술 지표를 시계열로 분석하는 관측 화면을 담당합니다. 동일한 대시보드를 두 영역에 중복 구현하지 않는 것이 원칙입니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 6 --&gt;
&lt;h2 id=&quot;data-external&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 6. Data와 External Service&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서는 PostgreSQL, Redis와 Kafka를 모두 같은 종류의 데이터 저장소로 취급하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속 업무 데이터의 원본, 재생성 가능한 캐시, 비동기 이벤트 전달 책임을 명확하게 구분했습니다.&lt;/p&gt;
&lt;!-- PostgreSQL --&gt;
&lt;div style=&quot;margin: 28px 0; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  PostgreSQL &amp;mdash; 영속 업무 데이터의 원본&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 거래&amp;middot;탐지&amp;middot;사건과 감사 데이터를 보관하는 FinGuardOps의 정합성 기준으로 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;금융 거래와 행동 이벤트&lt;/li&gt;
&lt;li&gt;멱등성 Record와 응답 Snapshot&lt;/li&gt;
&lt;li&gt;탐지 결과와 Evidence&lt;/li&gt;
&lt;li&gt;Rule 정의와 RuleVersion&lt;/li&gt;
&lt;li&gt;사건과 거래 연결&lt;/li&gt;
&lt;li&gt;조사 메모와 Append-only 감사 로그&lt;/li&gt;
&lt;li&gt;AI 리포트와 사용량&amp;middot;토큰&amp;middot;비용&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;!-- External Risk --&gt;
&lt;div style=&quot;margin: 28px 0; padding: 24px; border: 1px solid #dbe8dc; border-radius: 16px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  External Risk &amp;mdash; 외부 위험정보 경계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 금융권 위험정보 API와 데이터를 사용할 수 없는 제약을 보완하기 위해 위험계좌&amp;middot;위험 기기&amp;middot;위험 IP 등의 정보를 제공하는 외부 의존성을 별도 경계로 둡니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬&amp;middot;테스트용 결정적 Mock&lt;/li&gt;
&lt;li&gt;실제 연동 구조를 재현하는 HTTP Provider&lt;/li&gt;
&lt;li&gt;Port와 Adapter 기반 외부 연동 분리&lt;/li&gt;
&lt;li&gt;Timeout&amp;middot;Unavailable&amp;middot;Invalid Response 시나리오&lt;/li&gt;
&lt;li&gt;외부 위험정보 실패와 Rule 분석 실패의 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;!-- Redis --&gt;
&lt;div style=&quot;margin: 28px 0; padding: 24px; border: 1px solid #eed5d5; border-radius: 16px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Redis &amp;mdash; 재생성 가능한 캐시와 집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 거래와 사건의 원본 저장소로 사용하지 않습니다. PostgreSQL의 원본에서 다시 계산하거나 조회할 수 있는 데이터만 보관합니다.&lt;/p&gt;
&lt;ul style=&quot;padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 사건&amp;middot;동일 분석 조건의 정확 일치 AI 리포트 캐시&lt;/li&gt;
&lt;li&gt;재생성 가능한 운영 집계&lt;/li&gt;
&lt;li&gt;필요성과 장애 정책이 확정되면 External Risk 캐시 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;margin-bottom: 0; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;caseId
+ detectionResultVersion
+ promptVersion
+ modelVersion&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Reason Code가 같거나 내용이 유사하다는 이유만으로 다른 사건의 AI 리포트를 재사용하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Kafka --&gt;
&lt;div style=&quot;margin: 28px 0; padding: 24px; border: 1px solid #ded8ee; border-radius: 16px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚫ Kafka &amp;mdash; 비동기 이벤트 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 핵심 동기 금융 판단을 대신하는 것이 아니라, 사용자 응답 경로에서 분리해야 하는 작업을 비동기로 전달하기 위해 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 사건 리포트 생성 요청&lt;/li&gt;
&lt;li&gt;사건 관련 도메인 이벤트&lt;/li&gt;
&lt;li&gt;운영 통계 집계 이벤트&lt;/li&gt;
&lt;li&gt;장애와 배포 관련 운영 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka를 도입할 때는 Producer와 Consumer만 구현하는 데서 끝내지 않고 Consumer 멱등성, 중복 이벤트, 순서, Consumer Lag, 재처리와 DLQ를 함께 검증합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;PostgreSQL은 업무 데이터의 원본이고, Redis는 재생성 가능한 캐시이며, Kafka는 비동기 이벤트 전달 수단입니다. 세 기술은 서로 대체 관계가 아닙니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 7 --&gt;
&lt;h2 id=&quot;infra-observability&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;☁️ 7. Infra&amp;middot;DevOps&amp;middot;Cloud와 Observability&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서 인프라는 애플리케이션을 마지막에 배포하기 위한 부가 요소가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 복구, 반복 가능한 실행 환경, 배포 안정성, 서비스 확장과 운영 상태 관측을 검증하기 위한 프로젝트의 핵심 영역으로 포함했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 2px solid #234f88; border-radius: 15px; background-color: #f8fbff; text-align: center;&quot;&gt;
&lt;p style=&quot;margin: 0px; color: #234f88; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 실행 &amp;rarr; 컨테이너 통합 &amp;rarr; 자동화된 검증 &amp;rarr; 오케스트레이션 &amp;rarr; 클라우드 운영 &amp;rarr; 장애&amp;middot;성능&amp;middot;비용 검증&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 270px; padding: 23px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  Docker Compose&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot, FastAPI, PostgreSQL, Redis, Kafka와 Observability 구성요소를 동일한 로컬 환경에서 반복 실행합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발&amp;middot;테스트 환경 재현&lt;/li&gt;
&lt;li&gt;서비스 간 네트워크 구성&lt;/li&gt;
&lt;li&gt;환경 변수와 Volume 관리&lt;/li&gt;
&lt;li&gt;Health Check와 시작 순서 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; padding: 23px; border: 1px solid #ded8ee; border-radius: 16px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;☸️ Kubernetes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 기반 구성을 서비스 단위로 배포하고 복구&amp;middot;확장&amp;middot;Rolling Update를 검증합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Deployment와 Service 구성&lt;/li&gt;
&lt;li&gt;Replica와 장애 복구&lt;/li&gt;
&lt;li&gt;Rolling Update와 Rollback&lt;/li&gt;
&lt;li&gt;Config&amp;middot;Secret 분리&lt;/li&gt;
&lt;li&gt;Resource Request&amp;middot;Limit&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; padding: 23px; border: 1px solid #e8dcb9; border-radius: 16px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;☁️ AWS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬과 Kubernetes 환경에서 검증한 구조를 클라우드 운영 환경으로 확장합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션과 데이터 계층 배포&lt;/li&gt;
&lt;li&gt;네트워크&amp;middot;보안&amp;middot;접근 경계&lt;/li&gt;
&lt;li&gt;관리형 데이터베이스와 저장소 검토&lt;/li&gt;
&lt;li&gt;클라우드 로그&amp;middot;메트릭 연계&lt;/li&gt;
&lt;li&gt;배포 전후 운영 상태 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 270px; padding: 23px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  GitHub Actions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue와 Pull Request 흐름에 자동 테스트, 이미지 생성과 배포 검증을 연결합니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Backend 단위&amp;middot;통합 테스트&lt;/li&gt;
&lt;li&gt;AI Service lint&amp;middot;format&amp;middot;pytest&lt;/li&gt;
&lt;li&gt;Container Image Build와 보안 검사&lt;/li&gt;
&lt;li&gt;Image Registry Push&lt;/li&gt;
&lt;li&gt;Kubernetes 배포와 이력 관리&lt;/li&gt;
&lt;li&gt;승인된 Issue를 기준으로 한 AI Agent 작업&lt;/li&gt;
&lt;li&gt;테스트&amp;middot;리뷰&amp;middot;사용자 승인 Human Gate&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 36px; color: #64182f; font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;  Observability&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 요청이 어느 단계에서 지연되거나 실패했는지 확인하기 위해 메트릭, 로그와 분산 추적을 서로 다른 관측 신호로 사용합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 26px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9;&quot;&gt;&lt;b&gt;  Prometheus&lt;/b&gt;&lt;br /&gt;API&amp;middot;Rule&amp;middot;ML&amp;middot;LLM 지연시간, 오류율, 처리량과 비용 메트릭 수집&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #e8dcb9; border-radius: 14px; background-color: #fffcf5;&quot;&gt;&lt;b&gt;  Grafana&lt;/b&gt;&lt;br /&gt;기술 시계열 대시보드, 장애 분석과 배포 전후 비교&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;  Loki&lt;/b&gt;&lt;br /&gt;Spring Boot&amp;middot;FastAPI&amp;middot;인프라의 구조화 로그 수집과 검색&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 220px; padding: 20px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;  OpenTelemetry&lt;/b&gt;&lt;br /&gt;거래 요청이 여러 구성요소를 거치는 흐름과 지연 구간 추적&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라와 Observability는 단순히 서비스가 실행된다는 사실을 보여주기 위한 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timeout, DB Pool 고갈, Kafka Consumer 중단, LLM 장애와 비용 급증 같은 시나리오를 주입하고, 어떤 지표로 탐지했는지와 어떻게 복구했는지를 검증하기 위해 사용합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 8 --&gt;
&lt;h2 id=&quot;responsibility-boundary&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  8. 시스템 사이의 책임과 장애 경계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서 가장 중요하게 정한 원칙은 계산, 업무 결정, 원본 저장, 캐시와 관측 책임을 섞지 않는 것입니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;기술별 책임과 데이터 소유권.png&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPROTF/dJMcafA6HuJ/AoiN1kTRdm8JiiqWxfpPxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPROTF/dJMcafA6HuJ/AoiN1kTRdm8JiiqWxfpPxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPROTF/dJMcafA6HuJ/AoiN1kTRdm8JiiqWxfpPxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPROTF%2FdJMcafA6HuJ%2FAoiN1kTRdm8JiiqWxfpPxK%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;1535&quot; height=&quot;1024&quot; data-filename=&quot;기술별 책임과 데이터 소유권.png&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 2px solid #7a1f3d; border-radius: 15px; background-color: #fff8fa; text-align: center;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7a1f3d; font-size: 19px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;계산 책임 &amp;ne; 업무 결정 책임 &amp;ne; 데이터 원본 &amp;ne; 캐시 &amp;ne; 기술 관측&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 13px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fff8fa;&quot;&gt;&lt;b&gt;☕ Spring Boot&lt;/b&gt;&lt;br /&gt;검증&amp;middot;상태 전이&amp;middot;위험 대응&amp;middot;사건&amp;middot;감사&amp;middot;영속화&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #e8dcb9; border-radius: 14px; background-color: #fffcf5;&quot;&gt;&lt;b&gt;  FastAPI&lt;/b&gt;&lt;br /&gt;Rule&amp;middot;ML&amp;middot;점수&amp;middot;Reason Code&amp;middot;Evidence&amp;middot;AI 리포트 계산&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;  PostgreSQL&lt;/b&gt;&lt;br /&gt;거래&amp;middot;탐지&amp;middot;사건&amp;middot;감사 업무 데이터 원본&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #eed5d5; border-radius: 14px; background-color: #fff9f9;&quot;&gt;&lt;b&gt;  Redis&lt;/b&gt;&lt;br /&gt;정확 일치 캐시와 재생성 가능한 집계&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #ded8ee; border-radius: 14px; background-color: #fbf9ff;&quot;&gt;&lt;b&gt;⚫ Kafka&lt;/b&gt;&lt;br /&gt;비동기 사건&amp;middot;리포트&amp;middot;통계&amp;middot;운영 이벤트&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #d8e4f5; border-radius: 14px; background-color: #f8fbff;&quot;&gt;&lt;b&gt;⚛️ React&lt;/b&gt;&lt;br /&gt;분석 담당자와 운영자를 위한 업무 화면&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #e8dcb9; border-radius: 14px; background-color: #fffcf5;&quot;&gt;&lt;b&gt;  Observability&lt;/b&gt;&lt;br /&gt;지연시간&amp;middot;오류율&amp;middot;처리량&amp;middot;로그&amp;middot;Trace&amp;middot;비용 관측&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 230px; padding: 20px; border: 1px solid #dbe8dc; border-radius: 14px; background-color: #f9fff9;&quot;&gt;&lt;b&gt;☁️ Infra&amp;middot;Cloud&lt;/b&gt;&lt;br /&gt;실행&amp;middot;배포&amp;middot;복구&amp;middot;확장과 운영 환경 제공&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  장애를 정상 결과로 바꾸지 않는 원칙&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;External Risk 실패 &amp;ne; 위험정보 없음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FastAPI 실패 &amp;ne; 정상 거래&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis 실패 &amp;ne; 업무 데이터 유실&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LLM 실패 &amp;ne; Rule&amp;middot;ML 판단 실패&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메트릭 기록 실패 &amp;ne; 금융 업무 실패&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;External Risk와 FastAPI가 실패하면 이를 정상 또는 저위험 결과로 바꾸지 않습니다. 오류가 발생한 단계와 종류를 보존하고 해당 업무 경계에 맞는 실패 상태로 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Redis와 LLM은 핵심 금융 업무 데이터의 원본이 아닙니다. 캐시나 AI 리포트가 실패해도 이미 계산된 Rule&amp;middot;ML 판단과 확정된 거래&amp;middot;사건 결과를 변경하지 않습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 9 --&gt;
&lt;h2 id=&quot;why-not-msa&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  9. 처음부터 전체 MSA로 구성하지 않은 이유&lt;/h2&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;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #eed5d5; border-radius: 16px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;⚠️ 처음부터 전체 MSA일 때 생기는 문제&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 트랜잭션&lt;/li&gt;
&lt;li&gt;서비스 간 부분 실패&lt;/li&gt;
&lt;li&gt;이벤트 중복과 순서&lt;/li&gt;
&lt;li&gt;보상 트랜잭션&lt;/li&gt;
&lt;li&gt;서비스별 배포&amp;middot;관측 환경&lt;/li&gt;
&lt;li&gt;데이터 소유권 조정&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #dbe8dc; border-radius: 16px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ 초기 아키텍처&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot Modular Monolith&lt;/li&gt;
&lt;li&gt;독립 FastAPI AI Service&lt;/li&gt;
&lt;li&gt;PostgreSQL 업무 데이터 원본&lt;/li&gt;
&lt;li&gt;External Risk Mock&amp;middot;HTTP 경계&lt;/li&gt;
&lt;li&gt;로컬 트랜잭션 기반 정합성 관리&lt;/li&gt;
&lt;li&gt;명확하게 구분된 장애 경계&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 34px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  서비스를 분리할 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 조건이 실제로 발생했을 때 특정 논리 모듈을 별도 서비스로 분리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;독립 배포 요구가 반복적으로 발생하는가&lt;/li&gt;
&lt;li&gt;특정 모듈만 독립적으로 확장해야 하는가&lt;/li&gt;
&lt;li&gt;서비스 분리로 얻는 장애 격리 효과가 명확한가&lt;/li&gt;
&lt;li&gt;데이터 소유권을 독립적으로 분리할 수 있는가&lt;/li&gt;
&lt;li&gt;독립 팀이 해당 영역을 운영하는가&lt;/li&gt;
&lt;li&gt;분산 트랜잭션과 운영 비용을 감수할 근거가 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;MSA를 고려하지 않은 것이 아니라, 핵심 금융 업무 정합성을 먼저 검증한 뒤 분리가 필요한 영역만 단계적으로 확장하기로 한 것입니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 10 --&gt;
&lt;h2 id=&quot;development-roadmap&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 10. Cloud Application Modernization 로드맵과 검증 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 전체 목표 구조를 한 번에 구현하거나 단순히 클라우드로 옮기는 방식보다, 각 단계에서 해결해야 할 애플리케이션과 운영 문제를 정의하고 검증한 뒤 다음 영역으로 확장하는 방식으로 진행합니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 15px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;1단계. 금융 업무 기반&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래와 행동 이벤트&lt;/li&gt;
&lt;li&gt;상태 전이와 멱등성&lt;/li&gt;
&lt;li&gt;Rule 기반 탐지&lt;/li&gt;
&lt;li&gt;위험도별 대응&lt;/li&gt;
&lt;li&gt;사건 생성과 감사 로그&lt;/li&gt;
&lt;li&gt;PostgreSQL 트랜잭션 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #dbe8dc; border-radius: 16px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;2단계. 서비스 연동&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot와 FastAPI 연동&lt;/li&gt;
&lt;li&gt;External Risk Service&lt;/li&gt;
&lt;li&gt;RuleVersion과 Evidence&lt;/li&gt;
&lt;li&gt;Timeout과 오류 계약&lt;/li&gt;
&lt;li&gt;서비스 간 Trace 전달&lt;/li&gt;
&lt;li&gt;장애 전파와 복구 경계&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;3단계. 로컬 운영 환경&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker와 Docker Compose&lt;/li&gt;
&lt;li&gt;서비스 Health Check&lt;/li&gt;
&lt;li&gt;Prometheus와 Grafana&lt;/li&gt;
&lt;li&gt;Loki와 OpenTelemetry&lt;/li&gt;
&lt;li&gt;React 관리자 화면&lt;/li&gt;
&lt;li&gt;CI 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #ded8ee; border-radius: 16px; background-color: #fbf9ff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #5d4381; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;4단계. 비동기&amp;middot;AI 운영&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 정확 일치 캐시&lt;/li&gt;
&lt;li&gt;Kafka 비동기 이벤트&lt;/li&gt;
&lt;li&gt;ML 복합 패턴 분석&lt;/li&gt;
&lt;li&gt;생성형 AI 사건 리포트&lt;/li&gt;
&lt;li&gt;모델 라우팅과 fallback&lt;/li&gt;
&lt;li&gt;AI 토큰&amp;middot;비용&amp;middot;품질 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #e8dcb9; border-radius: 16px; background-color: #fffcf5;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8a5a00; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;5단계. Cloud Native 확장&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Container Image Pipeline&lt;/li&gt;
&lt;li&gt;Kubernetes 배포와 복구&lt;/li&gt;
&lt;li&gt;AWS 클라우드 환경&lt;/li&gt;
&lt;li&gt;Rolling Update와 Rollback&lt;/li&gt;
&lt;li&gt;Config&amp;middot;Secret&amp;middot;리소스 관리&lt;/li&gt;
&lt;li&gt;클라우드 E2E 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 23px; border: 1px solid #eed5d5; border-radius: 16px; background-color: #fff9f9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;6단계. 장애&amp;middot;비용 실험&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;External Service Timeout&lt;/li&gt;
&lt;li&gt;Redis&amp;middot;Kafka&amp;middot;DB 장애&lt;/li&gt;
&lt;li&gt;Consumer Lag과 재처리&lt;/li&gt;
&lt;li&gt;LLM 오류와 fallback&lt;/li&gt;
&lt;li&gt;배포 전후 성능 비교&lt;/li&gt;
&lt;li&gt;AI FinOps 실험과 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 30px 0; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  전 단계에 공통으로 적용하는 AI-assisted Delivery&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;margin-bottom: 16px; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;Sprint Goal과 GitHub Issue
&amp;rarr; ADR&amp;middot;API&amp;middot;DB 계약 확인
&amp;rarr; AI Agent 구현&amp;middot;테스트&amp;middot;문서 초안
&amp;rarr; 자동 테스트와 정적 검사
&amp;rarr; 독립 Review
&amp;rarr; 사용자 승인
&amp;rarr; Pull Request merge
&amp;rarr; 배포와 운영 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;AI Agent는 승인된 작업 범위 안에서 초안을 만들고 반복 작업을 지원하지만, API&amp;middot;DB&amp;middot;아키텍처 변경, Secret 접근, Production 배포와 최종 Merge를 임의로 수행하지 못하도록 Human-in-the-loop Gate를 유지합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 36px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  각 단계에서 확인할 기준&lt;/h3&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fffafb;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상 흐름뿐 아니라 경계값과 실패 흐름을 테스트했는가&lt;/li&gt;
&lt;li&gt;중복 요청과 동시성 충돌을 검증했는가&lt;/li&gt;
&lt;li&gt;외부 장애가 어느 업무까지 영향을 주는지 구분했는가&lt;/li&gt;
&lt;li&gt;로그&amp;middot;메트릭&amp;middot;트레이스로 장애 위치를 확인할 수 있는가&lt;/li&gt;
&lt;li&gt;배포 후 Health와 핵심 업무 흐름을 다시 검증했는가&lt;/li&gt;
&lt;li&gt;AI의 호출량&amp;middot;토큰&amp;middot;지연시간&amp;middot;비용을 실제로 측정했는가&lt;/li&gt;
&lt;li&gt;측정하지 않은 향상이나 절감률을 성과로 표현하지 않았는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&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;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 11 --&gt;
&lt;h2 id=&quot;closing&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  11. 마치며: 다음은 상태 전이와 멱등성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 전체 구조는 다양한 기술을 많이 사용하는 것을 목표로 설계한 구조가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 업무 정합성, 사용자 화면, 분석 계산, 데이터 원본, 캐시, 비동기 처리, 실행&amp;middot;배포 환경과 기술 관측의 책임을 나누고 각 책임에 적합한 기술을 배치한 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Java&amp;middot;Spring Boot와 React 기반의 업무 애플리케이션, FastAPI 기반 AI Service, Cloud Native 운영 환경, 그리고 생성형 AI&amp;middot;AI Agent를 통제 가능한 개발 프로세스에 적용하는 방식을 하나의 Modernization 흐름으로 연결하고자 했습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%); text-align: center;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 10px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 핵심 책임 분리&lt;/p&gt;
&lt;code style=&quot;color: #333333; font-size: 15px; font-weight: bold; line-height: 2.1;&quot;&gt;
    Spring Boot
    &amp;rarr; 금융 업무 상태와 영속화

    FastAPI
    &amp;rarr; Rule&amp;middot;ML&amp;middot;AI 분석 계산

    PostgreSQL
    &amp;rarr; 영속 업무 데이터 원본

    Redis
    &amp;rarr; 재생성 가능한 캐시

    Kafka
    &amp;rarr; 비동기 이벤트 전달

    Docker&amp;middot;Kubernetes&amp;middot;AWS
    &amp;rarr; 실행&amp;middot;배포&amp;middot;복구&amp;middot;확장 환경

    Observability
    &amp;rarr; 장애&amp;middot;성능&amp;middot;비용 관측
  &lt;/code&gt;&lt;/div&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;!-- 다음 편 예고 --&gt;
&lt;div style=&quot;margin: 32px 0; padding: 22px 24px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 8px; color: #234f88; font-size: 17px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[FinGuardOps #3] 재시도와&amp;nbsp;장애에도&amp;nbsp;안전한&amp;nbsp;거래&amp;middot;사건&amp;nbsp;상태&amp;nbsp;전이와&amp;nbsp;멱등성&amp;nbsp;설계&lt;/b&gt;&lt;br /&gt;거래와 사건의 상태를 어떻게 분리하고, Idempotency-Key&amp;middot;Request Fingerprint&amp;middot;Response Snapshot으로 중복 요청을 어떻게 통제할 것인지 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- GitHub 저장소 버튼 --&gt;
&lt;div style=&quot;margin: 40px 0 12px 0; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 13px 20px; border-radius: 10px; background-color: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36, 41, 47, 0.2);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;GitHub&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt; &lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>FinGuardOps/Project</category>
      <category>aiagent</category>
      <category>AWS</category>
      <category>Cloud</category>
      <category>CloudApplicationModernization</category>
      <category>CloudNative</category>
      <category>FastAPI</category>
      <category>FinGuardOps</category>
      <category>react</category>
      <category>SystemArchitecture</category>
      <category>개인프로젝트</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/47</guid>
      <comments>https://bbbicsan.tistory.com/47#entry47comment</comments>
      <pubDate>Sun, 30 Aug 2026 19:14:25 +0900</pubDate>
    </item>
    <item>
      <title>[FinGuardOps #1] 금융 이상거래 탐지를 넘어 FraudOps 플랫폼을 기획한 이유</title>
      <link>https://bbbicsan.tistory.com/46</link>
      <description>&lt;div style=&quot;font-size: 16px; line-height: 1.85; color: #222222; word-break: keep-all;&quot;&gt;&lt;!-- 시리즈 배지 --&gt;
&lt;div style=&quot;display: inline-block; margin-bottom: 18px; padding: 7px 14px; border: 1px solid #7a1f3d; border-radius: 999px; background-color: #fff7fa; color: #7a1f3d; font-size: 14px; font-weight: bold; letter-spacing: 0.2px;&quot;&gt;FinGuardOps Project Series #1&lt;/div&gt;
&lt;!-- 도입부 --&gt;
&lt;p style=&quot;font-size: 18px; line-height: 1.9;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 금융 거래 데이터를 분석해 &lt;b&gt;이상거래를 탐지하는 프로젝트&lt;/b&gt;를 만들려고 했습니다. 거래 정보를 입력하면 Rule이나 모델이 위험 점수를 계산하고, 그 결과를 화면에서 확인할 수 있는 형태를 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트의 범위를 구체화할수록 &lt;b&gt; 위험 점수를 계산하는 것만으로 실제 FDS 업무가 완성되는 것은 아니다 &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;AI와 외부 서비스를 시스템에 연결하면 또 다른 문제도 생깁니다. 외부 위험정보 조회가 지연되거나 AI 서비스가 응답하지 않을 수 있고, 생성형 AI를 사용한다면 호출량과 토큰 비용도 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 분석 결과만 보여주는 것에서 끝나지 않고 &lt;b&gt;장애, 복구, 성능과 비용까지 운영할 수 있는 구조&lt;/b&gt;가 필요했습니다. 이러한 문제를 하나의 흐름으로 구현하기 위해 단순 금융 이상거래 탐지 시스템에서 범위를 확장하여 &lt;b&gt;FinGuardOps&lt;/b&gt;를 기획했습니다.&lt;/p&gt;
&lt;!-- 공식 프로젝트 설명 --&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;FinGuardOps&lt;/b&gt;는 금융거래와 사용자 행동을 기반으로 이상거래를 탐지하고, 위험 대응과 사건 처리를 지원하며, AI 서비스의 장애&amp;middot;성능&amp;middot;비용을 통합 관리하는 Cloud Native 금융 AI FraudOps 플랫폼입니다.&lt;/blockquote&gt;
&lt;!--
    이 위치에 생성한 이미지를 삽입합니다.

    파일명:
    finguardops_서비스_흐름과_비교도.png

    대체 텍스트:
    FinGuardOps 서비스 흐름과 단순 FDS 프로젝트 비교
  --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;finguardops_서비스_흐름과_비교도.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUkoQc/dJMcaf8PkVD/DxkFgXDveYEkweANV9ZX5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUkoQc/dJMcaf8PkVD/DxkFgXDveYEkweANV9ZX5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUkoQc/dJMcaf8PkVD/DxkFgXDveYEkweANV9ZX5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUkoQc%2FdJMcaf8PkVD%2FDxkFgXDveYEkweANV9ZX5k%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;1448&quot; height=&quot;1086&quot; data-filename=&quot;finguardops_서비스_흐름과_비교도.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;!-- 목차 --&gt;
&lt;div style=&quot;margin: 38px 0; padding: 24px 26px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fffafb; box-shadow: 0 4px 14px rgba(122, 31, 61, 0.06);&quot;&gt;
&lt;h2 id=&quot;table-of-contents&quot; style=&quot;margin: 0 0 16px 0; color: #7a1f3d; font-size: 23px; border: none;&quot; data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ol style=&quot;margin: 0; padding-left: 22px; line-height: 2;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#project-background&quot;&gt;   프로젝트를 시작하게 된 배경 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#why-financial-fds&quot;&gt;   왜 금융 이상거래 탐지를 선택했는가 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#fds-limitations&quot;&gt;   단순 이상거래 탐지 프로젝트에서 느낀 한계 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#from-fds-to-fraudops&quot;&gt;  ️ FDS에서 FraudOps 플랫폼으로 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#problems-to-solve&quot;&gt;   FinGuardOps가 해결하려는 문제 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#main-users&quot;&gt;   프로젝트의 주요 사용자 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#goals-and-non-goals&quot;&gt;   프로젝트의 목표와 제외 범위 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#ai-collaboration&quot;&gt;   생성형 AI를 개발에 활용하는 방식 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #333333; text-decoration: none;&quot; href=&quot;#closing&quot;&gt;   마치며: 이제 전체 구조를 설계할 차례 &lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 1 --&gt;
&lt;h2 id=&quot;project-background&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  1. 프로젝트를 시작하게 된 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드와 클라우드를 공부하면서 각 기술을 개별 실습으로 끝내지 않고 하나의 서비스 안에서 연결해 보고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 CRUD API를 만드는 것을 넘어 트랜잭션, 상태 전이, 동시성, 외부 서비스 연동과 장애 복구 같은 문제를 직접 다뤄 보고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 정상적으로 동작하는 모습만 구현하는 것도 중요하지만, 실제 운영 환경에서는 동일한 요청이 반복되거나 외부 서비스가 제한 시간 안에 응답하지 않는 상황도 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에는 일부 데이터만 저장되었는데 클라이언트에는 실패 응답이 전달될 수도 있고, 처리가 끝났지만 응답을 받지 못한 사용자가 같은 요청을 다시 보낼 수도 있습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 20px 22px; border: 1px solid #ead9df; border-radius: 14px; background-color: #fffafb;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 12px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  프로젝트 주제를 정할 때 세운 기준&lt;/p&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 정합성과 상태 관리가 중요한 도메인&lt;/li&gt;
&lt;li&gt;중복 요청과 동시성 문제를 자연스럽게 다룰 수 있는 도메인&lt;/li&gt;
&lt;li&gt;Java 백엔드와 Python 분석 서비스를 연결할 수 있는 도메인&lt;/li&gt;
&lt;li&gt;정상 흐름뿐 아니라 장애와 복구 과정까지 검증할 수 있는 도메인&lt;/li&gt;
&lt;li&gt;로그&amp;middot;메트릭&amp;middot;트레이싱과 클라우드 운영으로 확장할 수 있는 도메인&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&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;금융 거래는 한 번의 중복 처리나 잘못된 상태 변경도 중요한 문제로 이어질 수 있습니다. 또한 Rule과 ML을 이용한 분석 결과를 실제 업무 흐름과 연결하기에도 적합하다고 판단했습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 2 --&gt;
&lt;h2 id=&quot;why-financial-fds&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  2. 왜 금융 이상거래 탐지를 선택했는가&lt;/h2&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  2.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;클라이언트가 같은 거래 요청을 두 번 보냈다고 해서 거래가 두 건 생성되거나 고위험 사건이 중복으로 만들어져서는 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 하나의 요청이 여러 처리 단계를 거치는 동안 각 상태가 일관성 있게 변경되어야 합니다. 거래 분석은 실패했는데 거래 상태가 정상 완료로 남거나, 사건은 생성되었지만 해당 거래와 연결되지 않는 상황도 방지해야 합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px; border-radius: 14px; background-color: #f7f7f8; text-align: center;&quot;&gt;&lt;code style=&quot;color: #5e1830; font-size: 16px; font-weight: bold; line-height: 2;&quot;&gt;
      트랜잭션
      &amp;rarr; 멱등성
      &amp;rarr; 상태 전이
      &amp;rarr; 동시성 제어
      &amp;rarr; 중복 방지
      &amp;rarr; 감사 로그
      &amp;rarr; 장애 복구
    &lt;/code&gt;&lt;/div&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  2.2 위험 점수만큼 탐지 근거가 중요한 도메인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상거래 탐지 결과가 단순히 &lt;code&gt;위험 확률 87%&lt;/code&gt; 또는 &lt;code&gt;위험 점수 70점&lt;/code&gt;으로만 제공된다면, 담당자는 해당 거래가 왜 위험한지 판단하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 검토를 지원하려면 다음과 같은 근거를 함께 제공해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평소보다 큰 금액이 거래되었는가&lt;/li&gt;
&lt;li&gt;최근 등록된 기기에서 발생한 거래인가&lt;/li&gt;
&lt;li&gt;비밀번호나 이체 한도가 변경된 직후인가&lt;/li&gt;
&lt;li&gt;최근 등록된 수취인에게 송금했는가&lt;/li&gt;
&lt;li&gt;외부 위험계좌&amp;middot;IP&amp;middot;기기 정보와 일치하는가&lt;/li&gt;
&lt;li&gt;짧은 시간 안에 유사한 거래가 반복되었는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 FinGuardOps에서는 위험 점수만 반환하는 것이 아니라 &lt;b&gt;Rule&amp;middot;ML 판단 근거와 행동 타임라인을 함께 제공하는 구조&lt;/b&gt;를 목표로 정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐지 결과는 담당자의 판단을 지원하는 정보이며, 최종 이상거래 판정을 자동으로 대신하지 않습니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #64182f; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  2.3 백엔드&amp;middot;AI&amp;middot;클라우드를 연결할 수 있는 도메인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 이상거래 탐지는 하나의 기술만으로 완성되는 기능이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래를 접수하고 상태를 관리하는 백엔드, Rule과 ML을 실행하는 분석 서비스, 업무 데이터를 저장하는 데이터베이스, 외부 위험정보와 운영 인프라가 함께 연결되어야 합니다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0; padding: 20px; border: 1px solid #e3e3e3; border-radius: 14px; background-color: #fafafa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  하나의 업무 흐름에서 연결하고자 한 영역&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;margin-bottom: 0; padding: 18px; border-radius: 10px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.8;&quot;&gt;&lt;code&gt;Java&amp;middot;Spring Boot 백엔드
&amp;rarr; Python&amp;middot;FastAPI 분석 서비스
&amp;rarr; PostgreSQL 기반 업무 데이터
&amp;rarr; 외부 위험정보 연동
&amp;rarr; 로그&amp;middot;메트릭&amp;middot;트레이싱
&amp;rarr; Docker&amp;middot;Kafka&amp;middot;Kubernetes&amp;middot;AWS의 단계적 도입&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 26px 0; padding: 21px 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;☁️ Cloud Application Modernization 관점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 금융 FDS 기능을 구현하는 것뿐 아니라, 하나의 업무 애플리케이션을 &lt;b&gt;컨테이너 표준화, CI/CD, Observability, 비동기 처리, Kubernetes와 AWS&lt;/b&gt;로 단계적으로 확장하는 과정을 함께 다루기 위해 기획했습니다.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;실제 운영 중인 레거시 금융 시스템을 이전했다고 주장하는 프로젝트는 아닙니다. 대신 금융 업무 정합성을 먼저 확보한 뒤 실행 환경과 배포 방식, 관측 체계와 복구 전략을 점진적으로 현대화하는 &lt;b&gt;Cloud Application Modernization 전략&lt;/b&gt;을 설계하고 검증하는 것을 목표로 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 3 --&gt;
&lt;h2 id=&quot;fds-limitations&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  3. 단순 이상거래 탐지 프로젝트에서 느낀 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 구상했던 이상거래 탐지 프로젝트의 흐름은 비교적 단순했습니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot; style=&quot;margin: 24px 0; padding: 20px; border-radius: 12px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 15px; line-height: 1.9;&quot;&gt;&lt;code&gt;거래 데이터 입력
&amp;rarr; Rule 또는 ML 모델 실행
&amp;rarr; 위험 점수 출력
&amp;rarr; 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조만으로도 Rule이나 모델이 거래를 어떻게 분류하는지는 보여줄 수 있습니다. 하지만 실제 FDS 업무를 생각하면 탐지 이후에 많은 질문이 남습니다.&lt;/p&gt;
&lt;div style=&quot;margin: 26px 0; padding: 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❓ 탐지 결과 이후에 남는 질문&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위험하다고 판단된 거래에는 어떤 대응을 적용해야 하는가&lt;/li&gt;
&lt;li&gt;담당자는 어떤 탐지 근거와 행동 이력을 확인해야 하는가&lt;/li&gt;
&lt;li&gt;고위험 거래를 조사 대상 사건으로 어떻게 연결해야 하는가&lt;/li&gt;
&lt;li&gt;동일 요청이나 이벤트가 반복되었을 때 중복 처리를 어떻게 막아야 하는가&lt;/li&gt;
&lt;li&gt;누가 언제 거래 또는 사건 상태를 변경했는지 어떻게 추적해야 하는가&lt;/li&gt;
&lt;li&gt;외부 위험정보나 AI 서비스가 실패하면 거래 상태를 어떻게 관리해야 하는가&lt;/li&gt;
&lt;li&gt;처리 도중 장애가 발생했을 때 어디까지 저장하고 어떻게 복구해야 하는가&lt;/li&gt;
&lt;li&gt;AI 호출량, 지연시간과 비용을 어떻게 측정하고 통제해야 하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&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;그래서 프로젝트의 중심을 모델 정확도 자체보다 &lt;b&gt; 거래 접수부터 탐지, 위험 대응, 사건 처리와 운영까지 이어지는 전체 흐름 &lt;/b&gt; 으로 옮겼습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 4 --&gt;
&lt;h2 id=&quot;from-fds-to-fraudops&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 4. FDS에서 FraudOps 플랫폼으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 프로젝트의 목표는 금융 거래와 사용자 행동을 분석해 이상거래를 탐지하고 분석 결과를 제공하는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 탐지 이후의 업무 처리와 AI 서비스 운영 문제까지 포함하면서 프로젝트를 다음과 같이 다시 정의했습니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 26px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;이상거래를 탐지하는 데서 끝나지 않고, 탐지 결과를 위험 대응과 사건 처리로 연결하며, AI 서비스의 장애&amp;middot;성능&amp;middot;비용까지 통합 관리하는 FraudOps 플랫폼&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 이를 크게 &lt;b&gt;금융 FDS, AI 운영, Cloud Native 운영&lt;/b&gt;의 세 영역으로 나누어 설명합니다.&lt;/p&gt;
&lt;!-- 3개 핵심 영역 카드 --&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  금융 FDS&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;금융 거래와 행동 이벤트 처리&lt;/li&gt;
&lt;li&gt;Rule&amp;middot;ML 기반 위험도 계산&lt;/li&gt;
&lt;li&gt;설명 가능한 탐지 근거 제공&lt;/li&gt;
&lt;li&gt;위험도별 거래 대응&lt;/li&gt;
&lt;li&gt;사건 생성과 담당자 검토&lt;/li&gt;
&lt;li&gt;감사 이력 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  AI 운영&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고위험 사건 AI 리포트&lt;/li&gt;
&lt;li&gt;탐지 근거와 행동 타임라인 요약&lt;/li&gt;
&lt;li&gt;모델 라우팅&lt;/li&gt;
&lt;li&gt;템플릿 fallback&lt;/li&gt;
&lt;li&gt;입력&amp;middot;출력 토큰 기록&lt;/li&gt;
&lt;li&gt;모델별 비용과 품질 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 260px; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;☁️ Cloud Native 운영&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 Health와 배포 상태&lt;/li&gt;
&lt;li&gt;로그&amp;middot;메트릭&amp;middot;트레이싱&lt;/li&gt;
&lt;li&gt;오류율과 처리량 관측&lt;/li&gt;
&lt;li&gt;장애 격리와 복구&lt;/li&gt;
&lt;li&gt;안전한 배포&lt;/li&gt;
&lt;li&gt;비용과 운영 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 30px 0; padding: 18px 21px; border-left: 5px solid #666666; border-radius: 0 12px 12px 0; background-color: #f7f7f7; color: #444444;&quot; data-ke-style=&quot;style1&quot;&gt;이 글에서는 특정 시점의 구현 현황보다 FinGuardOps가 지향하는 &lt;b&gt;전체 제품 범위와 현대화 방향&lt;/b&gt;을 소개합니다. 금융 FDS, AI 운영과 Cloud Native 운영을 하나의 목표 구조로 정의하고, 이후 글에서는 각 문제를 어떤 설계와 검증 방식으로 해결하는지 단계별로 기록합니다.&lt;/blockquote&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 5 --&gt;
&lt;h2 id=&quot;problems-to-solve&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  5. FinGuardOps가 해결하려는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 특정 기술을 사용하는 것을 목표로 시작한 프로젝트가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 해결해야 할 문제를 정의하고, 그 문제를 해결하는 데 필요한 기술을 단계적으로 선택했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;  탐지와 설명&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 거래를 위험하다고 판단할 것인가&lt;/li&gt;
&lt;li&gt;Rule과 ML의 역할을 어떻게 구분할 것인가&lt;/li&gt;
&lt;li&gt;위험 점수의 근거를 어떻게 설명할 것인가&lt;/li&gt;
&lt;li&gt;재현 가능한 분석 결과를 어떻게 만들 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 위험 대응과 사건 처리&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위험도별 대응을 어떻게 구분할 것인가&lt;/li&gt;
&lt;li&gt;고위험 거래를 사건으로 어떻게 연결할 것인가&lt;/li&gt;
&lt;li&gt;사건 상태와 최종 판정을 어떻게 분리할 것인가&lt;/li&gt;
&lt;li&gt;담당자의 조사 결과를 어떻게 기록할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;  정합성과 중복 방지&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 거래 요청을 어떻게 식별할 것인가&lt;/li&gt;
&lt;li&gt;중복 이벤트를 어떻게 처리할 것인가&lt;/li&gt;
&lt;li&gt;중복 사건 생성을 어떻게 방지할 것인가&lt;/li&gt;
&lt;li&gt;일부 데이터만 반영되는 상황을 어떻게 막을 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;  장애와 복구&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;External Risk와 FastAPI 실패를 어떻게 구분할 것인가&lt;/li&gt;
&lt;li&gt;Timeout을 정상 결과로 처리하지 않으려면 어떻게 할 것인가&lt;/li&gt;
&lt;li&gt;처리 완료와 응답 저장 사이의 간극을 어떻게 복구할 것인가&lt;/li&gt;
&lt;li&gt;AI 리포트 실패를 금융 판단과 어떻게 격리할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;  Observability와 FinOps&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청이 거친 처리 단계를 어떻게 추적할 것인가&lt;/li&gt;
&lt;li&gt;서비스별 지연시간과 오류율을 어떻게 측정할 것인가&lt;/li&gt;
&lt;li&gt;LLM 호출량과 토큰을 어떻게 기록할 것인가&lt;/li&gt;
&lt;li&gt;비용 급증과 fallback 증가를 어떻게 확인할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 280px; padding: 20px; border: 1px solid #e5e5e5; border-radius: 14px; background-color: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 19px;&quot; data-ke-size=&quot;size23&quot;&gt;☁️ Application Modernization&lt;/h3&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 실행 환경을 컨테이너로 어떻게 표준화할 것인가&lt;/li&gt;
&lt;li&gt;검증되지 않은 변경이 배포되지 않도록 어떤 Gate를 둘 것인가&lt;/li&gt;
&lt;li&gt;서비스별 확장과 복구를 어떻게 분리할 것인가&lt;/li&gt;
&lt;li&gt;로컬 구성을 Kubernetes와 AWS로 어떻게 이전할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문들은 이후 거래 상태 전이, 멱등성, ERD, API, External Risk, CI/CD, Observability, Kubernetes, AWS와 AI FinOps를 설계하는 기준이 되었습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 6 --&gt;
&lt;h2 id=&quot;main-users&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  6. 프로젝트의 주요 사용자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 사용자는 &lt;b&gt;FDS 분석 담당자&lt;/b&gt;와 &lt;b&gt;플랫폼&amp;middot;클라우드 운영자&lt;/b&gt;로 구분했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 사용자는 확인하는 정보와 수행하는 업무가 다르기 때문에 화면과 API의 책임도 구분할 계획입니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 16px; margin: 28px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #ead9df; border-radius: 16px; background-color: #fff8fa;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #7a1f3d; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ FDS 분석 담당자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위험 거래와 사건을 조회하고 탐지 결과를 검토하는 사용자입니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위험 점수와 위험 등급 확인&lt;/li&gt;
&lt;li&gt;Rule&amp;middot;ML 탐지 근거 확인&lt;/li&gt;
&lt;li&gt;사용자 행동 타임라인과 연관 거래 확인&lt;/li&gt;
&lt;li&gt;외부 위험정보 확인&lt;/li&gt;
&lt;li&gt;AI 사건 리포트 검토&lt;/li&gt;
&lt;li&gt;조사 메모와 담당자 관리&lt;/li&gt;
&lt;li&gt;정상&amp;middot;오탐&amp;middot;이상거래 최종 판정&lt;/li&gt;
&lt;li&gt;사건 상태와 감사 이력 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; padding: 24px; border: 1px solid #d8e4f5; border-radius: 16px; background-color: #f8fbff;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; color: #234f88; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt; ️ 플랫폼&amp;middot;클라우드 운영자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps를 구성하는 서비스와 인프라의 상태를 확인하는 사용자입니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot&amp;middot;FastAPI Health 확인&lt;/li&gt;
&lt;li&gt;PostgreSQL과 향후 Redis&amp;middot;Kafka 상태 확인&lt;/li&gt;
&lt;li&gt;API 응답시간&amp;middot;오류율&amp;middot;처리량 확인&lt;/li&gt;
&lt;li&gt;External Risk&amp;middot;Rule&amp;middot;ML&amp;middot;LLM 처리시간 확인&lt;/li&gt;
&lt;li&gt;DB Connection Pool과 Consumer Lag 확인&lt;/li&gt;
&lt;li&gt;AI 호출량&amp;middot;토큰&amp;middot;비용 확인&lt;/li&gt;
&lt;li&gt;캐시 적중률과 fallback 비율 확인&lt;/li&gt;
&lt;li&gt;장애와 배포 이력 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FDS 분석 담당자 화면에서는 거래&amp;middot;사건&amp;middot;조사와 같은 업무 상태를 다루고, 플랫폼 운영 화면에서는 서비스 상태와 비용 요약을 다루도록 구분했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 시계열 기술 지표는 별도의 Observability 도구에서 확인하는 방향으로 계획했습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 7 --&gt;
&lt;h2 id=&quot;goals-and-non-goals&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  7. 프로젝트의 목표와 제외 범위&lt;/h2&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #356a3c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;✅ 7.1 프로젝트 목표&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #dbe8dc; border-radius: 15px; background-color: #f9fff9;&quot;&gt;
&lt;ol style=&quot;margin: 0; padding-left: 23px;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;margin-bottom: 12px;&quot;&gt;금융 이상거래 탐지와 사건 처리를 트랜잭션&amp;middot;멱등성&amp;middot;상태 전이&amp;middot;감사 로그를 갖춘 백엔드 시스템으로 구현합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 12px;&quot;&gt;Spring Boot, FastAPI와 React를 연결하여 업무 애플리케이션의 전체 사용자 흐름을 구성합니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 12px;&quot;&gt;Docker, CI/CD, Observability, Kafka, Kubernetes와 AWS를 단계적으로 적용해 애플리케이션을 Cloud Native 구조로 확장합니다.&lt;/li&gt;
&lt;li&gt;생성형 AI와 AI Agent를 활용하되 장애&amp;middot;비용&amp;middot;품질&amp;middot;권한과 최종 승인 경계를 사람이 통제합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 기술은 목적이 아니라 금융 거래의 정합성, 탐지 결과의 설명 가능성, 외부 의존성 장애와 운영 문제를 해결하기 위한 수단으로 사용합니다.&lt;/p&gt;
&lt;h3 style=&quot;margin-top: 32px; color: #8b2c2c; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;  7.2 명확하게 제외한 범위&lt;/h3&gt;
&lt;div style=&quot;margin: 24px 0; padding: 22px; border: 1px solid #eed5d5; border-radius: 15px; background-color: #fff9f9;&quot;&gt;
&lt;ul style=&quot;margin: 0; padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 금융기관 계좌와 실제 고객 거래 사용&lt;/li&gt;
&lt;li&gt;실제 거래 승인&amp;middot;인증&amp;middot;차단&amp;middot;고객 제재&lt;/li&gt;
&lt;li&gt;실제 금융기관 수준의 대규모 인프라&lt;/li&gt;
&lt;li&gt;실제 운영 중인 레거시 금융 시스템을 직접 마이그레이션했다는 주장&lt;/li&gt;
&lt;li&gt;처음부터 전체 시스템을 마이크로서비스로 분리하는 구성&lt;/li&gt;
&lt;li&gt;핵심 기능보다 앞선 Kafka&amp;middot;Kubernetes 도입&lt;/li&gt;
&lt;li&gt;생성형 AI를 통한 위험 점수 계산&lt;/li&gt;
&lt;li&gt;생성형 AI를 통한 최종 이상거래 판정&lt;/li&gt;
&lt;li&gt;생성형 AI를 통한 사건 상태 자동 확정&lt;/li&gt;
&lt;li&gt;Reason Code만으로 다른 사건의 AI 리포트를 재사용하는 방식&lt;/li&gt;
&lt;li&gt;시맨틱 캐시와 범용 FDS 챗봇&lt;/li&gt;
&lt;li&gt;측정하지 않은 성능 향상이나 비용 절감률 주장&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 거래 승인&amp;middot;인증&amp;middot;차단과 고객 제재는 Mock으로 처리하고, 생성형 AI는 담당자의 검토를 지원하는 보조 도구로만 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 업무 상태와 판단은 정해진 정책과 담당자의 검토를 통해 결정하도록 했습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 8 --&gt;
&lt;h2 id=&quot;ai-collaboration&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  8. 생성형 AI를 개발에 활용하는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps에서는 생성형 AI를 단순 코드 자동 생성기가 아니라 &lt;b&gt;역할이 구분된 개발 도구&lt;/b&gt;로 활용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM&amp;middot;Tech Lead, Backend, Frontend, AI Service와 Review 역할에 맞게 요구사항 분석, 구현 초안, 테스트 초안과 코드 리뷰를 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 AI가 제안한 내용을 그대로 프로젝트에 반영하지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 범위와 우선순위를 정하고, API&amp;middot;DB&amp;middot;아키텍처의 기준을 검토하며, 테스트 결과와 변경 파일을 확인한 뒤 최종 반영 여부를 직접 결정합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;margin: 26px 0; padding: 22px; border-radius: 13px; overflow-x: auto; background-color: #24262b; color: #f4f4f4; font-size: 14px; line-height: 1.9;&quot;&gt;&lt;code&gt;문제와 요구사항 정의
&amp;rarr; GitHub Issue 작성
&amp;rarr; 역할별 AI가 구현&amp;middot;문서화 초안 작성
&amp;rarr; API&amp;middot;DB&amp;middot;아키텍처 정합성 검토
&amp;rarr; 단위&amp;middot;통합 테스트 실행
&amp;rarr; 독립 코드 리뷰
&amp;rarr; 수정과 재검증
&amp;rarr; 사용자 최종 승인과 Pull Request merge&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;margin: 28px 0; padding: 21px 22px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #234f88; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  AI Agent와 Human-in-the-loop&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;승인된 GitHub Issue, ADR, API&amp;middot;DB 계약과 변경 허용 파일을 AI Agent의 작업 경계로 사용합니다. Agent는 구현&amp;middot;테스트&amp;middot;문서 초안을 만들 수 있지만, 요구사항과 계약을 임의로 바꾸거나 운영 배포를 확정할 수 없습니다.&lt;/p&gt;
&lt;ul style=&quot;margin-bottom: 0; padding-left: 21px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 직접 Commit과 자동 Merge 금지&lt;/li&gt;
&lt;li&gt;API 계약과 DB Migration의 임의 확정 금지&lt;/li&gt;
&lt;li&gt;Secret과 운영 Credential 접근 금지&lt;/li&gt;
&lt;li&gt;Production 자동 배포 금지&lt;/li&gt;
&lt;li&gt;테스트&amp;middot;리뷰&amp;middot;사용자 최종 승인 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 28px 0; padding: 21px 22px; border: 1px solid #ead9df; border-radius: 15px; background-color: #fff8fa;&quot;&gt;
&lt;p style=&quot;margin-top: 0; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  프로젝트에서 직접 담당하는 역할&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;PM, Backend Lead, DevOps Engineer, Architecture Owner, Reviewer, Tester와 Final Approver 역할을 담당합니다. 생성형 AI는 구현과 문서화 생산성을 높이기 위한 도구이며, 요구사항과 기술 결정의 책임을 대신하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;margin: 28px 0; padding: 20px 22px; border-left: 5px solid #7a1f3d; border-radius: 0 14px 14px 0; background-color: #fff7fa; color: #3b2029; font-size: 17px; line-height: 1.9;&quot; data-ke-style=&quot;style1&quot;&gt;생성형 AI를 역할별 개발 도구로 활용했으며, 요구사항 정의, API&amp;middot;DB 설계, 아키텍처 결정, 코드 검토, 테스트, DevOps와 최종 검증은 직접 수행했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 GitHub Issue, 브랜치, Commit, Pull Request, 테스트 결과와 리뷰 기록으로 남겨 AI를 활용한 개발 과정도 재현하고 검토할 수 있도록 운영하고 있습니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 44px 0; border: 0; border-top: 1px solid #eeeeee;&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 9 --&gt;
&lt;h2 id=&quot;closing&quot; style=&quot;margin-top: 48px; padding-bottom: 11px; border-bottom: 3px solid #7a1f3d; color: #222222; font-size: 27px;&quot; data-ke-size=&quot;size26&quot;&gt;  9. 마치며: 이제 전체 구조를 설계할 차례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinGuardOps는 단순히 이상거래 위험 점수를 보여주는 프로젝트가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 거래가 접수되고, 거래와 사용자 행동이 분석되며, 위험도에 따른 대응과 사건 처리가 이루어지고, 그 과정의 상태와 장애를 추적할 수 있는 전체 흐름을 구현하는 것이 목표입니다.&lt;/p&gt;
&lt;div style=&quot;margin: 28px 0; padding: 22px; border: 1px solid #ead9df; border-radius: 15px; background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%); text-align: center;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 10px; color: #7a1f3d; font-size: 18px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;FinGuardOps의 전체 흐름&lt;/p&gt;
&lt;code style=&quot;color: #333333; font-size: 15px; font-weight: bold; line-height: 2;&quot;&gt;
      금융거래&amp;middot;행동 이벤트
      &amp;rarr; Rule&amp;middot;ML 기반 이상거래 탐지
      &amp;rarr; 위험 등급별 대응
      &amp;rarr; 사건 생성과 담당자 검토
      &amp;rarr; 생성형 AI 사건 리포트
      &amp;rarr; 장애&amp;middot;성능&amp;middot;비용&amp;middot;배포 상태 운영
    &lt;/code&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 통해 금융 이상거래 탐지와 사건 처리를 백엔드 시스템으로 구현하는 역량, Spring Boot&amp;middot;FastAPI&amp;middot;React를 연결하는 애플리케이션 설계 역량, Docker&amp;middot;CI/CD&amp;middot;Observability&amp;middot;Kubernetes&amp;middot;AWS 기반의 Cloud Application Modernization 역량, 그리고 생성형 AI와 AI Agent의 장애&amp;middot;비용&amp;middot;권한을 통제하는 운영 역량을 함께 검증하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 목표와 범위를 정한 다음에는 금융 업무 상태와 AI 계산 책임을 어떻게 분리할지 결정해야 했습니다. 또한 프론트엔드, 데이터베이스와 인프라가 각각 어떤 정보를 소유하고 어떤 역할을 담당할지도 정해야 했습니다.&lt;/p&gt;
&lt;!-- 다음 편 예고 --&gt;
&lt;div style=&quot;margin: 32px 0; padding: 22px 24px; border: 1px solid #d8e4f5; border-radius: 15px; background-color: #f8fbff;&quot;&gt;
&lt;p style=&quot;margin-top: 0; margin-bottom: 8px; color: #234f88; font-size: 17px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;  다음 글&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; [FinGuardOps #2] 전체 시스템 구성과 기술별 역할을 어떻게 나눴는가&lt;/b&gt;&lt;br /&gt;Spring Boot, FastAPI, React, PostgreSQL과 인프라 영역을 어떤 기준으로 나누었는지 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- GitHub 저장소 버튼 --&gt;
&lt;div style=&quot;margin: 40px 0 12px 0; text-align: center;&quot;&gt;&lt;a style=&quot;display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 13px 20px; border-radius: 10px; background-color: #24292f; color: #ffffff; font-size: 15px; font-weight: bold; text-decoration: none; box-shadow: 0 4px 12px rgba(36, 41, 47, 0.2);&quot; href=&quot;https://github.com/Ahnjisan/FinGuardOps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; aria-label=&quot;FinGuardOps GitHub Repository&quot;&gt; &lt;img style=&quot;width: 20px; height: 20px; filter: brightness(0) invert(1); vertical-align: middle;&quot; src=&quot;https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg&quot; alt=&quot;GitHub&quot; /&gt; &lt;span&gt;GitHub에서 FinGuardOps 저장소 보기&lt;/span&gt; &lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>FinGuardOps/Project</category>
      <category>Ai</category>
      <category>Backend</category>
      <category>Cloud</category>
      <category>CloudNative</category>
      <category>FDS</category>
      <category>FinGuardOps</category>
      <category>FraudOps</category>
      <category>개인프로젝트</category>
      <category>금융IT</category>
      <category>이상거래탐지</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/46</guid>
      <comments>https://bbbicsan.tistory.com/46#entry46comment</comments>
      <pubDate>Sun, 30 Aug 2026 14:20:11 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 23일차 TIL : DDL&amp;middot;DML&amp;middot;제약조건&amp;middot;VIEW&amp;middot;Sequence와 참조 무결성</title>
      <link>https://bbbicsan.tistory.com/45</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 23일차 TIL : DDL&amp;middot;DML&amp;middot;제약조건&amp;middot;VIEW&amp;middot;Sequence와 참조 무결성&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAKov/dJMb998E8Sz/fLYhkKOkixPGFJs4XnJ7rk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAKov/dJMb998E8Sz/fLYhkKOkixPGFJs4XnJ7rk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAKov/dJMb998E8Sz/fLYhkKOkixPGFJs4XnJ7rk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAKov%2FdJMb998E8Sz%2FfLYhkKOkixPGFJs4XnJ7rk%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;DDL과 DML을 중심으로 &lt;code&gt;FOREIGN KEY&lt;/code&gt;, &lt;code&gt;CASCADE&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;DEFAULT&lt;/code&gt;, &lt;code&gt;ALTER TABLE&lt;/code&gt;, &lt;code&gt;VIEW&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;Sequence&lt;/code&gt;, &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;를 실습하며 &lt;b&gt;데이터베이스 구조를 정의하고 실제 데이터의 무결성을 유지하면서 변경하는 과정&lt;/b&gt;을 익혔다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SQL Day05 - DDL &amp;amp; DML
        &amp;darr;
Foreign Key
        &amp;darr;
참조 무결성
ON DELETE CASCADE
ON UPDATE CASCADE
NO ACTION
        &amp;darr;
PK / FK와 INDEX
SHOW INDEX
BTREE
        &amp;darr;
INSERT
DEFAULT / NULL
        &amp;darr;
Constraint
NOT NULL / CHECK
        &amp;darr;
ALTER TABLE
ADD / MODIFY / CHANGE / DROP
        &amp;darr;
쇼핑몰 ERD
1:N / N:M 관계
        &amp;darr;
VIEW
CREATE OR REPLACE VIEW
        &amp;darr;
DDL Workshop Q1 ~ Q15
강사님 풀이 확인
        &amp;darr;
DML
INSERT / UPDATE / DELETE
        &amp;darr;
UPDATE + Subquery
        &amp;darr;
Foreign Key 오류
참조 무결성 검증
        &amp;darr;
Sequence
NEXTVAL / LASTVAL
START WITH / INCREMENT BY / MAXVALUE
        &amp;darr;
Sequence + PK
        &amp;darr;
AUTO_INCREMENT
        &amp;darr;
쇼핑몰 ERD 기반 실습 과제
※ 문제만 제시, 수업 중 풀이는 진행하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DDL&lt;/li&gt;
&lt;li&gt;DML&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE&lt;/code&gt;&lt;code&gt;ALTER&lt;/code&gt;&lt;code&gt;DROP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COMMIT&lt;/code&gt;&lt;code&gt;ROLLBACK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Foreign Key&lt;/li&gt;
&lt;li&gt;Referential Integrity&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ON DELETE CASCADE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ON UPDATE CASCADE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NO ACTION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SHOW INDEX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;BTREE&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MODIFY COLUMN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CHANGE COLUMN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VIEW&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE OR REPLACE VIEW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WITH CHECK OPTION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sequence&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NEXTVAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LASTVAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;START WITH&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INCREMENT BY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAXVALUE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Foreign Key와 참조 무결성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 첫 번째 주제는 Foreign Key와 &lt;b&gt;참조 무결성(Referential Integrity)&lt;/b&gt; 이었다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부모 테이블
PK
 &amp;darr;
자식 테이블
FK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Foreign Key는 자식 테이블의 값이 부모 테이블에 존재하는 값을 참조하도록 제한한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 자식 데이터는 기본적으로:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;부모 테이블에 존재하는 키 값
또는
NULL이 허용된 경우 NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 가질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Foreign Key 옵션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키를 선언할 때 부모 데이터가 변경되거나 삭제되는 경우 자식 데이터를 어떻게 처리할지 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ON DELETE CASCADE
&amp;rarr; 부모 데이터 삭제
&amp;rarr; 자식 데이터도 연쇄 삭제

ON UPDATE CASCADE
&amp;rarr; 부모 키 변경
&amp;rarr; 자식 FK도 함께 변경

NO ACTION
&amp;rarr; 참조 관계가 존재하면
   무결성을 깨는 삭제/수정을 허용하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;TB_DEPT
DEPT_ID = 10
     &amp;darr;
TB_EMP
DEPT_ID = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인 상황에서 부모 &lt;code&gt;DEPT_ID = 10&lt;/code&gt;을 삭제하고 &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;가 설정되어 있다면 해당 부서를 참조하던 사원도 함께 삭제된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. PK / FK와 INDEX 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서 테이블을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;CREATE TABLE TB_DEPT (
    DEPT_ID    VARCHAR(10),
    DEPT_TITLE VARCHAR(50),
    PRIMARY KEY(DEPT_ID)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW INDEX FROM TB_DEPT;&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;Key_name   &amp;rarr; PRIMARY
Column_name &amp;rarr; DEPT_ID
Index_type &amp;rarr; BTREE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 확인됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. FK + CASCADE가 적용된 사원 테이블&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_EMP (
    EMP_ID    VARCHAR(10) PRIMARY KEY,
    EMP_NAME  VARCHAR(50) NOT NULL,
    HIRE_DATE DATE DEFAULT SYSDATE(),
    DEPT_ID   VARCHAR(10) NOT NULL,
    JOB_ID    VARCHAR(10) NOT NULL,

    FOREIGN KEY(DEPT_ID)
        REFERENCES TB_DEPT(DEPT_ID)
        ON DELETE CASCADE,

    FOREIGN KEY(JOB_ID)
        REFERENCES TB_JOB(JOB_ID)
        ON UPDATE CASCADE
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;TB_DEPT
DEPT_ID(PK)
     &amp;darr;
TB_EMP
DEPT_ID(FK)

TB_JOB
JOB_ID(PK)
     &amp;darr;
TB_EMP
JOB_ID(FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW INDEX FROM TB_EMP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행했더니:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;PRIMARY
DEPT_ID
JOB_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 인덱스를 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. INSERT와 DEFAULT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HIRE_DATE&lt;/code&gt;에는 다음과 같이 기본값이 선언되어 있었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HIRE_DATE DATE DEFAULT SYSDATE()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 컬럼을 생략하고 INSERT하면:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_EMP(
    EMP_ID,
    EMP_NAME,
    DEPT_ID,
    JOB_ID
)
VALUES(
    '100',
    'JSLIM',
    '10',
    'J1'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HIRE_DATE&lt;/code&gt;에는 자동으로 기본값이 들어간다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HIRE_DATE 생략
        &amp;darr;
DEFAULT SYSDATE()
        &amp;darr;
현재 날짜 저장&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. DEFAULT를 명시적으로 사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT할 때 &lt;code&gt;DEFAULT&lt;/code&gt; 키워드를 직접 사용할 수도 있었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_EMP(
    EMP_ID,
    EMP_NAME,
    HIRE_DATE,
    DEPT_ID,
    JOB_ID
)
VALUES(
    '200',
    'JSLIM',
    DEFAULT,
    '10',
    'J1'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우도 &lt;code&gt;HIRE_DATE&lt;/code&gt;에 선언된 기본값이 적용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. DEFAULT와 NULL의 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서 중요했던 차이였다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_EMP(
    EMP_ID,
    EMP_NAME,
    HIRE_DATE,
    DEPT_ID,
    JOB_ID
)
VALUES(
    '300',
    'JSLIM',
    NULL,
    '10',
    'J1'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NULL&lt;/code&gt;을 명시적으로 넣으면 &lt;code&gt;DEFAULT&lt;/code&gt;가 실행되는 것이 아니라 실제 &lt;code&gt;NULL&lt;/code&gt;이 저장된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;컬럼 생략
&amp;rarr; DEFAULT

DEFAULT 명시
&amp;rarr; DEFAULT

NULL 명시
&amp;rarr; NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;DEFAULT&lt;/code&gt;와 &lt;code&gt;NULL&lt;/code&gt;은 같은 의미가 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. NOT NULL과 CHECK&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 테이블을 만들면서 제약조건을 다시 실습했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_EMPLOYEE (
    EMP_ID    VARCHAR(10),
    EMP_NAME  VARCHAR(50) NOT NULL,
    SALARY    INT CHECK(SALARY &amp;gt; 0),
    GENDER    CHAR(1) CHECK(GENDER IN ('F', 'M')),
    HIRE_DATE DATE DEFAULT SYSDATE(),
    JOB_ID    VARCHAR(10),
    DEPT_ID   VARCHAR(10)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 제약조건의 의미:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EMP_NAME NOT NULL
&amp;rarr; 반드시 이름을 입력

SALARY CHECK(SALARY &amp;gt; 0)
&amp;rarr; 0 이하 급여 입력 방지

GENDER CHECK(GENDER IN ('F','M'))
&amp;rarr; F 또는 M만 허용

HIRE_DATE DEFAULT SYSDATE()
&amp;rarr; 값이 생략되면 현재 날짜&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. ALTER TABLE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 테이블 구조를 변경할 때 &lt;code&gt;ALTER TABLE&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 정리한 주요 형태:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ADD
&amp;rarr; 제약조건 등 추가

ADD COLUMN
&amp;rarr; 컬럼 추가

DROP CONSTRAINT
&amp;rarr; 제약조건 제거

CHANGE COLUMN
&amp;rarr; 컬럼 이름과 타입 변경

MODIFY COLUMN
&amp;rarr; 컬럼 이름은 유지하고 타입/속성 변경

DROP COLUMN
&amp;rarr; 컬럼 삭제

RENAME
&amp;rarr; 이름 변경&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. ALTER TABLE로 PK 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 &lt;code&gt;EMP_ID&lt;/code&gt;에 PK가 없던 테이블에 나중에 PK를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_EMPLOYEE
ADD CONSTRAINT PRIMARY KEY(EMP_ID);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;CREATE TABLE
&amp;rarr; 처음부터 구조 정의

ALTER TABLE
&amp;rarr; 만들어진 구조를 나중에 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 차이를 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. MODIFY와 CHANGE 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Workshop에서도 반복해서 사용했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;MODIFY COLUMN
&amp;rarr; 기존 컬럼명 유지
&amp;rarr; 타입 / NOT NULL 등의 속성 변경

CHANGE COLUMN
&amp;rarr; 컬럼명 변경 가능
&amp;rarr; 데이터 타입도 함께 다시 지정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_CLASS_TYPE
MODIFY COLUMN NAME VARCHAR(10) NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_CLASS_TYPE
CHANGE COLUMN NO CLASS_TYPE_NO VARCHAR(10);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 쇼핑몰 ERD&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL 실습 중 쇼핑몰 ERD를 살펴봤다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CUSTOMERS
   1
   &amp;darr;
   N
ORDERS
   1
   &amp;darr;
   N
ORDERDETAIL
   N
   &amp;uarr;
   1
PRODUCTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;ORDERS&lt;/code&gt;와 &lt;code&gt;PRODUCTS&lt;/code&gt;의 관계였다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;한 주문
&amp;rarr; 여러 상품 포함 가능

한 상품
&amp;rarr; 여러 주문에 포함 가능

ORDERS N : M PRODUCTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 N:M 관계를 &lt;code&gt;ORDERDETAIL&lt;/code&gt;이라는 중간 테이블로 해소한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;ORDERS 1 : N ORDERDETAIL
PRODUCTS 1 : N ORDERDETAIL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ORDERDETAIL&lt;/code&gt;에는:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ORDERNO
PNO
QTY
COST&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 주문과 상품의 관계 자체에 필요한 데이터를 저장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. VIEW&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW는 SELECT 결과를 논리적인 테이블처럼 사용할 수 있도록 만든 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW VIEW_NAME
AS
SELECT ...;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW EMP_VIEW
AS
SELECT E.EMP_NAME,
       D.DEPT_NAME
FROM DEPARTMENT D
JOIN EMPLOYEE E
  ON D.DEPT_ID = E.DEPT_ID
WHERE E.DEPT_ID = '90';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM EMP_VIEW;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만으로 기존 JOIN + WHERE 쿼리의 결과를 다시 사용할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;복잡한 SELECT
       &amp;darr;
VIEW
       &amp;darr;
간단한 SELECT&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. TABLE과 VIEW 차이&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;TABLE
&amp;rarr; 실제 데이터를 저장

VIEW
&amp;rarr; SELECT 결과를 논리적인 테이블처럼 제공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW를 통해 복잡한 쿼리를 재사용할 수 있다는 점이 장점이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  DDL Workshop 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 전날의 DDL Workshop 15문제를 강사님 풀이와 함께 일부 다시 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q1. TB_CATEGORY 생성&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE TB_CATEGORY (
    NAME   VARCHAR(10),
    USE_YN CHAR(1) DEFAULT 'Y'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WFHFk/dJMcahFB8nQ/oPX1RRbT5DaMpbS7I3AUW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WFHFk/dJMcahFB8nQ/oPX1RRbT5DaMpbS7I3AUW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WFHFk/dJMcahFB8nQ/oPX1RRbT5DaMpbS7I3AUW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWFHFk%2FdJMcahFB8nQ%2FoPX1RRbT5DaMpbS7I3AUW0%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;382&quot; height=&quot;111&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q2. TB_CLASS_TYPE 생성&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_CLASS_TYPE (
    NO   VARCHAR(5) PRIMARY KEY,
    NAME VARCHAR(10)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4I1Oy/dJMcafHVJIM/qYIfBw5kE4K2iAMFxDYmpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4I1Oy/dJMcafHVJIM/qYIfBw5kE4K2iAMFxDYmpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4I1Oy/dJMcafHVJIM/qYIfBw5kE4K2iAMFxDYmpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4I1Oy%2FdJMcafHVJIM%2FqYIfBw5kE4K2iAMFxDYmpk%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;440&quot; height=&quot;184&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q3. TB_CATEGORY에 PK 추가&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_CATEGORY
ADD PRIMARY KEY (NAME);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caLGa2/dJMcaaT3m2q/8elY6zwzKNTBpfKqkwqtI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caLGa2/dJMcaaT3m2q/8elY6zwzKNTBpfKqkwqtI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caLGa2/dJMcaaT3m2q/8elY6zwzKNTBpfKqkwqtI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaLGa2%2FdJMcaaT3m2q%2F8elY6zwzKNTBpfKqkwqtI0%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;372&quot; height=&quot;125&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q4. NAME 컬럼에 NOT NULL 추가&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_CLASS_TYPE
MODIFY COLUMN NAME VARCHAR(10) NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ziME1/dJMcagzX23z/803r40ptb1D3sKkAJyngU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ziME1/dJMcagzX23z/803r40ptb1D3sKkAJyngU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ziME1/dJMcagzX23z/803r40ptb1D3sKkAJyngU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FziME1%2FdJMcagzX23z%2F803r40ptb1D3sKkAJyngU1%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;541&quot; height=&quot;115&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;115&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q5. 컬럼 수정&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE tb_category
MODIFY NAME VARCHAR(20);

ALTER TABLE TB_CLASS_TYPE
CHANGE COLUMN NO CLASS_TYPE_NO VARCHAR(10);

ALTER TABLE TB_CLASS_TYPE
CHANGE COLUMN NAME CLASS_TYPE_NAME VARCHAR(10);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rxNcm/dJMcafVry5o/GqUugwNbT8HgBrd1jpnyoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rxNcm/dJMcafVry5o/GqUugwNbT8HgBrd1jpnyoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rxNcm/dJMcafVry5o/GqUugwNbT8HgBrd1jpnyoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrxNcm%2FdJMcafVry5o%2FGqUugwNbT8HgBrd1jpnyoK%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;503&quot; height=&quot;209&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q6. 컬럼명 변경&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE tb_category
CHANGE NAME CATEGORY_NAME VARCHAR(20);

ALTER TABLE tb_class_type
CHANGE NO CLASS_TYPE_NO VARCHAR(10),
CHANGE NAME CLASS_TYPE_NAME VARCHAR(20);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CHANGE&lt;/code&gt;를 사용해 이름과 타입을 동시에 지정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2BRez/dJMb998E86z/nkdZCH4iWjJ4aK3Epq3mYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2BRez/dJMb998E86z/nkdZCH4iWjJ4aK3Epq3mYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2BRez/dJMb998E86z/nkdZCH4iWjJ4aK3Epq3mYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2BRez%2FdJMb998E86z%2FnkdZCH4iWjJ4aK3Epq3mYk%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;580&quot; height=&quot;209&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q7. PRIMARY KEY 재정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 PK를 제거한 뒤 새 컬럼 기준으로 다시 PK를 지정하는 흐름이었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE tb_category
ADD CONSTRAINT PK_CATEGORY_NAME
PRIMARY KEY (CATEGORY_NAME);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE tb_class_type
ADD CONSTRAINT PK_CLASS_TYPE_NO
PRIMARY KEY (CLASS_TYPE_NO);&lt;/code&gt;&lt;/pre&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;677&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3PUP3/dJMb998E866/YR9P4LrRUYNMgm0iLCCYGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3PUP3/dJMb998E866/YR9P4LrRUYNMgm0iLCCYGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3PUP3/dJMb998E866/YR9P4LrRUYNMgm0iLCCYGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3PUP3%2FdJMb998E866%2FYR9P4LrRUYNMgm0iLCCYGk%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;677&quot; height=&quot;245&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q8. 다중행 INSERT&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;INSERT INTO tb_category
VALUES
('공학', 'Y'),
('자연과학', 'Y'),
('의학', 'Y'),
('예체능', 'Y'),
('인문사회', 'Y');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번의 INSERT로 여러 Row를 넣었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boRfmo/dJMcadb49jX/fl3SSiKxu15hLURQhSExsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boRfmo/dJMcadb49jX/fl3SSiKxu15hLURQhSExsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boRfmo/dJMcadb49jX/fl3SSiKxu15hLURQhSExsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboRfmo%2FdJMcadb49jX%2Ffl3SSiKxu15hLURQhSExsk%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;808&quot; height=&quot;340&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q9. 기존 테이블에 FK 추가&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE TB_DEPARTMENT
ADD CONSTRAINT FK_DEPARTMENT_CATEGORY
FOREIGN KEY(CATEGORY)
REFERENCES TB_CATEGORY(CATEGORY_NAME);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;TB_CATEGORY.CATEGORY_NAME
          &amp;darr;
TB_DEPARTMENT.CATEGORY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계를 &lt;code&gt;ALTER TABLE&lt;/code&gt;로 사후 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y1DeR/dJMcadb49kp/7HKCnEK8E1iu88fEs4ZAh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y1DeR/dJMcadb49kp/7HKCnEK8E1iu88fEs4ZAh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y1DeR/dJMcadb49kp/7HKCnEK8E1iu88fEs4ZAh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy1DeR%2FdJMcadb49kp%2F7HKCnEK8E1iu88fEs4ZAh0%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;734&quot; height=&quot;78&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q10. 학생 일반 정보 VIEW 생성&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW
VW_학생일반정보(학번, 학생이름, 주소)
AS
SELECT S.STUDENT_NO,
       S.STUDENT_NAME,
       S.STUDENT_ADDRESS
FROM tb_student S;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW 선언부에서도 결과 컬럼 이름을 지정할 수 있다는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcVC1t/dJMcafA5zVn/zXQgezE7FAm7oPG1yb0jVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcVC1t/dJMcafA5zVn/zXQgezE7FAm7oPG1yb0jVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcVC1t/dJMcafA5zVn/zXQgezE7FAm7oPG1yb0jVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcVC1t%2FdJMcafA5zVn%2FzXQgezE7FAm7oPG1yb0jVk%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;441&quot; height=&quot;162&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q11. 지도면담 VIEW&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW
VW_지도면담(학생이름, 학과이름, 지도교수이름)
AS
SELECT S.STUDENT_NAME,
       D.DEPARTMENT_NAME,
       P.PROFESSOR_NAME
FROM tb_student S
JOIN tb_department D
  ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
LEFT JOIN tb_professor P
  ON S.COACH_PROFESSOR_NO = P.PROFESSOR_NO
ORDER BY D.DEPARTMENT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도교수가 없는 학생도 남겨야 하므로 &lt;code&gt;LEFT JOIN&lt;/code&gt;을 사용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KO4x7/dJMcac5pbb9/9z7KA0UeKLmYakQkoEi6k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KO4x7/dJMcac5pbb9/9z7KA0UeKLmYakQkoEi6k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KO4x7/dJMcac5pbb9/9z7KA0UeKLmYakQkoEi6k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKO4x7%2FdJMcac5pbb9%2F9z7KA0UeKLmYakQkoEi6k1%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;703&quot; height=&quot;225&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q12. 학과별 학생 수 VIEW&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW
VW_학과별학생수(DEPARTMENT_NAME, STUDENT_COUNT)
AS
SELECT D.DEPARTMENT_NAME,
       COUNT(S.DEPARTMENT_NO)
FROM tb_student S
JOIN tb_department D
  ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
GROUP BY D.DEPARTMENT_NO;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLqSkd/dJMcajpOD5O/BkqkiDTE1PN1paA2NM5yR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLqSkd/dJMcajpOD5O/BkqkiDTE1PN1paA2NM5yR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLqSkd/dJMcajpOD5O/BkqkiDTE1PN1paA2NM5yR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLqSkd%2FdJMcajpOD5O%2FBkqkiDTE1PN1paA2NM5yR1%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;673&quot; height=&quot;235&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q14. WITH CHECK OPTION&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW
VW_학생일반정보(학번, 학생이름, 주소)
AS
SELECT S.STUDENT_NO,
       S.STUDENT_NAME,
       S.STUDENT_ADDRESS
FROM tb_student S
WHERE ...
WITH CHECK OPTION;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WITH CHECK OPTION&lt;/code&gt;은 VIEW를 통해 데이터를 변경할 때 &lt;b&gt;변경된 데이터도 VIEW의 WHERE 조건을 만족하도록 제한&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;VIEW 조건
   &amp;darr;
INSERT / UPDATE
   &amp;darr;
변경 후에도 조건 만족?
   &amp;darr;
YES &amp;rarr; 허용
NO  &amp;rarr; 거부&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCvZrh/dJMcaafwgiO/PlAfdgC7GLBW4gOlgHMMbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCvZrh/dJMcaafwgiO/PlAfdgC7GLBW4gOlgHMMbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCvZrh/dJMcaafwgiO/PlAfdgC7GLBW4gOlgHMMbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCvZrh%2FdJMcaafwgiO%2FPlAfdgC7GLBW4gOlgHMMbk%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;488&quot; height=&quot;239&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q15. 최근 3년 수강생 수가 많은 3과목&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT C.CLASS_NO AS 과목번호,
       C.CLASS_NAME AS 과목이름,
       COUNT(*) AS '누적수강생수(명)'
FROM TB_CLASS C
JOIN TB_GRADE G
  ON C.CLASS_NO = G.CLASS_NO
WHERE SUBSTRING(G.TERM_NO, 1, 4)
      BETWEEN '2007' AND '2009'
GROUP BY C.CLASS_NO,
         C.CLASS_NAME
ORDER BY COUNT(*) DESC,
         C.CLASS_NO
LIMIT 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;과목 + 성적 JOIN
      &amp;darr;
2007 ~ 2009 필터링
      &amp;darr;
과목별 GROUP BY
      &amp;darr;
COUNT(*)
      &amp;darr;
누적 수강생 수 계산
      &amp;darr;
내림차순 정렬
      &amp;darr;
LIMIT 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbfkBX/dJMcaafwgjq/Gnd1xpSXoNvByyuQtcPKcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbfkBX/dJMcaafwgjq/Gnd1xpSXoNvByyuQtcPKcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbfkBX/dJMcaafwgjq/Gnd1xpSXoNvByyuQtcPKcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbfkBX%2FdJMcaafwgjq%2FGnd1xpSXoNvByyuQtcPKcK%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;645&quot; height=&quot;254&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. DML&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후반부에는 DML로 넘어갔다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;DML
&amp;rarr; Data Manipulation Language
&amp;rarr; 테이블의 실제 데이터 조작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 명령은:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;INSERT
UPDATE
DELETE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이고 트랜잭션 관련 명령으로:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;COMMIT
ROLLBACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 함께 언급됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. INSERT / UPDATE / DELETE 비교&lt;/h1&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;행 개수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새로운 행 추가&lt;/td&gt;
&lt;td&gt;증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기존 행의 값 수정&lt;/td&gt;
&lt;td&gt;일반적으로 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기존 행 삭제&lt;/td&gt;
&lt;td&gt;감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT
&amp;rarr; Row 추가

UPDATE
&amp;rarr; Column 값 변경

DELETE
&amp;rarr; Row 제거&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. UPDATE 기본 문법&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE TABLE_NAME
SET COLUMN_NAME = VALUE,
    COLUMN_NAME = VALUE
WHERE CONDITION;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;SET&lt;/code&gt;에 직접 값뿐 아니라 서브쿼리 결과를 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE TABLE_NAME
SET COLUMN_NAME = (
    SELECT ...
)
WHERE ...;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. UPDATE + Subquery&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;심하균의 직급과 부서를
성해교 사원과 같은 직급과 부서로 수정한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE employee
SET JOB_ID = (
        SELECT JOB_ID
        FROM employee
        WHERE EMP_NAME = '성해교'
    ),
    DEPT_ID = (
        SELECT DEPT_ID
        FROM employee
        WHERE EMP_NAME = '성해교'
    )
WHERE EMP_NAME = '심하균';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;성해교의 JOB_ID
&amp;rarr; 심하균 JOB_ID

성해교의 DEPT_ID
&amp;rarr; 심하균 DEPT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. 해외영업2팀 BONUS_PCT 변경&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;해외영업2팀 사원의 BONUS_PCT를
0.3으로 수정한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE employee
SET BONUS_PCT = 0.3
WHERE DEPT_ID = (
    SELECT DEPT_ID
    FROM DEPARTMENT
    WHERE DEPT_NAME = '해외영업2팀'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;해외영업2팀
       &amp;darr;
DEPT_ID 서브쿼리
       &amp;darr;
해당 사원들 조회
       &amp;darr;
BONUS_PCT = 0.3&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. MariaDB CLI에서 UPDATE 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 데이터를 조회했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       MARRIAGE
FROM EMPLOYEE
WHERE EMP_ID = '143';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;나승원 | Y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE EMPLOYEE
SET MARRIAGE = 'N'
WHERE EMP_ID = '143';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Query OK, 1 row affected

Rows matched: 1
Changed: 1
Warnings: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Rows matched: 1
&amp;rarr; WHERE 조건에 해당하는 Row 1개 발견

Changed: 1
&amp;rarr; 실제 값이 변경됨

Warnings: 0
&amp;rarr; 경고 없음&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 1. 존재하지 않는 FK로 UPDATE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE employee
SET DEPT_ID = '65'
WHERE EMP_ID = '100';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SQL 오류 (1452)

Cannot add or update a child row:
a foreign key constraint fails&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DEPT_ID = '65'&lt;/code&gt;로 수정할 수 없었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;483&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QsYm5/dJMcagfFcqb/WO53sIHKNMHgoNAFV8joBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QsYm5/dJMcagfFcqb/WO53sIHKNMHgoNAFV8joBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QsYm5/dJMcagfFcqb/WO53sIHKNMHgoNAFV8joBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQsYm5%2FdJMcagfFcqb%2FWO53sIHKNMHgoNAFV8joBK%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;483&quot; height=&quot;160&quot; data-origin-width=&quot;483&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXvOF/dJMcaiEBaBJ/afJRJr95HsP7MFZQLkMI11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXvOF/dJMcaiEBaBJ/afJRJr95HsP7MFZQLkMI11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXvOF/dJMcaiEBaBJ/afJRJr95HsP7MFZQLkMI11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXvOF%2FdJMcaiEBaBJ%2FafJRJr95HsP7MFZQLkMI11%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;484&quot; height=&quot;223&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;employee.DEPT_ID&lt;/code&gt;는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;department.DEPT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 참조하는 Foreign Key이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 부모 테이블에 &lt;code&gt;65&lt;/code&gt;라는 부서번호가 존재하지 않았다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;department
DEPT_ID = 65
&amp;rarr; 없음

employee
DEPT_ID = 65
&amp;rarr; 존재하지 않는 부모를 참조

&amp;rarr; 참조 무결성 위반&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 부모 테이블에 존재하는 유효한 부서번호를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       DEPT_NAME
FROM department;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤 실제 존재하는 값으로 UPDATE한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Foreign Key는 INSERT뿐 아니라 &lt;b&gt;UPDATE에서도 참조 무결성을 강제한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. FK와 NULL&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE employee
SET DEPT_ID = NULL
WHERE EMP_ID = '100';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FK 컬럼이 NULL을 허용한다면:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;존재하지 않는 임의값
&amp;rarr; FK 위반

NULL
&amp;rarr; 특정 부모를 참조하지 않음
&amp;rarr; 허용 가능&lt;/code&gt;&lt;/pre&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;FK 컬럼
&amp;rarr; 부모에 존재하는 값
또는
&amp;rarr; NULL 허용 시 NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 가질 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3SUZ/dJMcabFnZMa/GBpmvJhzfOs5WPItji2H5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3SUZ/dJMcabFnZMa/GBpmvJhzfOs5WPItji2H5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3SUZ/dJMcabFnZMa/GBpmvJhzfOs5WPItji2H5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3SUZ%2FdJMcabFnZMa%2FGBpmvJhzfOs5WPItji2H5k%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;324&quot; height=&quot;128&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. INSERT 기본 문법&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT INTO TABLE_NAME ([COLUMN_NAME])
VALUES (VALUES);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT에서 특히 확인해야 할 것은:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;데이터 타입
컬럼 순서
값의 개수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. DELETE 기본 문법&lt;/h1&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;DELETE FROM TABLE_NAME
WHERE CONDITION;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DELETE&lt;/code&gt;는 Row 자체를 삭제하기 때문에 실행 후 전체 행 수가 변경된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;WHERE&lt;/code&gt;를 생략하면 전체 데이터가 삭제될 수 있으므로 주의해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. Sequence&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 자동 번호 생성을 위한 Sequence를 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 생성:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE SEQUENCE SEQ_TEST;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 값:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT NEXTVAL(SEQ_TEST);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 사용 값:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SELECT LASTVAL(SEQ_TEST);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Sequence 삭제:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DROP SEQUENCE SEQ_TEST;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. Sequence 옵션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequence를 다시 생성했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE SEQUENCE SEQ_TEST
START WITH 1000
INCREMENT BY 2
MAXVALUE 1005;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;START WITH 1000
&amp;rarr; 시작값

INCREMENT BY 2
&amp;rarr; 2씩 증가

MAXVALUE 1005
&amp;rarr; 최대 허용값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1000
 &amp;darr;
1002
 &amp;darr;
1004
 &amp;darr;
1006 ❌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;1006&lt;/code&gt;에서 오류가 발생한 이유는:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;MAXVALUE = 1005&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 초과했기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정 2. Sequence가 1006에서 실패&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NEXTVAL()&lt;/code&gt;을 계속 호출하자:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1000
1002
1004&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 정상적으로 생성됐지만 다음 호출에서 오류가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDHmVI/dJMcaiEBaGe/v4sg2GKX7FfAkfOKBK7dy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDHmVI/dJMcaiEBaGe/v4sg2GKX7FfAkfOKBK7dy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDHmVI/dJMcaiEBaGe/v4sg2GKX7FfAkfOKBK7dy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDHmVI%2FdJMcaiEBaGe%2Fv4sg2GKX7FfAkfOKBK7dy1%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;496&quot; height=&quot;436&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증가폭이 &lt;code&gt;2&lt;/code&gt;이므로 다음 값은 &lt;code&gt;1006&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1006 &amp;gt; MAXVALUE 1005&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 더 이상 값을 생성할 수 없었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MAXVALUE 1005&lt;/code&gt;는 반드시 &lt;code&gt;1005&lt;/code&gt;가 반환된다는 의미가 아니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;START WITH
+
INCREMENT BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 따라 실제 생성되지 않는 값일 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. Sequence를 PK 자동값으로 사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequence를 테이블의 기본값과 연결했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE TB_TEMP (
    ORDER_ID INT
             DEFAULT (NEXTVAL(SEQ_TEST))
             PRIMARY KEY,
    CUSTOMER_NAME VARCHAR(50)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_TEMP(CUSTOMER_NAME)
VALUES('JSLIM');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ORDER_ID&lt;/code&gt;를 입력하지 않아도 Sequence 값이 자동으로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;ORDER_ID | CUSTOMER_NAME
1000     | JSLIM
1002     | JSLIM
1004     | JSLIM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Sequence
       &amp;darr;
NEXTVAL
       &amp;darr;
ORDER_ID DEFAULT
       &amp;darr;
INSERT에서 PK 생략
       &amp;darr;
자동 식별자 생성&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. AUTO_INCREMENT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서는 자동 증가 식별자를 더 간단하게 만들 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_AUTO_TEMP (
    ORDER_ID INT AUTO_INCREMENT PRIMARY KEY,
    CUSTOMER_NAME VARCHAR(50)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_AUTO_TEMP(CUSTOMER_NAME)
VALUES('JSLIM');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1
2
3
4
5
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 PK가 자동 생성된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. Sequence vs AUTO_INCREMENT&lt;/h1&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Sequence&lt;/th&gt;
&lt;th&gt;AUTO_INCREMENT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;별도 객체&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;필요 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시작값&lt;/td&gt;
&lt;td&gt;직접 설정 가능&lt;/td&gt;
&lt;td&gt;설정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;증가폭&lt;/td&gt;
&lt;td&gt;자유롭게 설정&lt;/td&gt;
&lt;td&gt;일반적으로 자동 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 방식&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NEXTVAL()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컬럼 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;활용&lt;/td&gt;
&lt;td&gt;여러 곳에서 독립적으로 사용 가능&lt;/td&gt;
&lt;td&gt;특정 컬럼과 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Sequence
&amp;rarr; 별도의 번호 생성 객체

AUTO_INCREMENT
&amp;rarr; 컬럼 자체의 자동 증가 기능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 목적은 &lt;b&gt;식별자 자동 생성&lt;/b&gt;이라는 점에서는 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 배운 개념 연결하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 학습한 내용을 하나의 흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ERD
&amp;rarr; 테이블 관계 설계
        &amp;darr;
DDL
&amp;rarr; 실제 테이블 구조 생성
        &amp;darr;
PK / FK / CHECK / NOT NULL
&amp;rarr; 데이터 무결성 규칙
        &amp;darr;
ALTER
&amp;rarr; 기존 구조 변경
        &amp;darr;
VIEW
&amp;rarr; 복잡한 조회 구조 재사용
        &amp;darr;
DML
&amp;rarr; 실제 데이터 입력/수정/삭제
        &amp;darr;
Foreign Key
&amp;rarr; 잘못된 관계 데이터 차단
        &amp;darr;
Sequence / AUTO_INCREMENT
&amp;rarr; 식별자 자동 생성&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습에서는 제약조건이 실제 데이터 변경을 어떻게 제한하는지를 여러 상황에서 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 존재하지 않는 부모 키를 참조한 UPDATE&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE employee
SET DEPT_ID = '65'
WHERE EMP_ID = '100';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 1452
Cannot add or update a child row&lt;/code&gt;&lt;/pre&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;pre class=&quot;sas&quot;&gt;&lt;code&gt;employee.DEPT_ID
&amp;rarr; Foreign Key

department.DEPT_ID = 65
&amp;rarr; 존재하지 않음&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;부모 테이블의 실제 데이터를 먼저 확인하고 존재하는 값을 사용해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 포인트&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;FK
&amp;rarr; INSERT뿐만 아니라 UPDATE에서도 검증됨&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. FK에는 왜 NULL이 가능한가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE employee
SET DEPT_ID = NULL
WHERE EMP_ID = '100';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이해&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;65
&amp;rarr; 존재하지 않는 부모를 참조하는 값

NULL
&amp;rarr; 아무 부모도 참조하지 않는 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 컬럼이 NULL을 허용하면 FK에도 NULL을 넣을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Foreign Key
&amp;ne; 무조건 값이 있어야 함

NOT NULL Foreign Key
&amp;rarr; 반드시 부모 참조 필요

Nullable Foreign Key
&amp;rarr; 관계가 없음을 NULL로 표현 가능&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. DEFAULT와 NULL을 같은 것으로 생각하면 안 됨&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;컬럼 생략
&amp;rarr; DEFAULT

DEFAULT 명시
&amp;rarr; DEFAULT

NULL 명시
&amp;rarr; NULL&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEFAULT는 값이 없을 때 자동으로 들어갈 &lt;b&gt;대체 값&lt;/b&gt;이고, NULL은 &lt;b&gt;값이 존재하지 않는 상태&lt;/b&gt; 자체이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Sequence 최대값 초과&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1000
1002
1004
1006 &amp;rarr; 오류&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;MAXVALUE 1005&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 1006을 생성할 수 없었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequence의 실제 값은:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;START WITH
INCREMENT BY
MAXVALUE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 옵션의 조합으로 결정된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 DDL과 DML을 연속해서 배우면서 &lt;b&gt;테이블을 만드는 것과 데이터를 사용하는 것이 별개의 단계가 아니라 서로 연결된 과정&lt;/b&gt;이라는 점을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL에서는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;테이블 구조
PK
FK
CHECK
DEFAULT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 통해 어떤 데이터를 저장할 수 있는지를 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 DML에서는 그 규칙 안에서:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;INSERT
UPDATE
DELETE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Foreign Key 실습이 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 FK가 단순히 테이블을 JOIN하기 위한 연결 컬럼이라고 생각하기 쉬웠지만, 실제로 존재하지 않는 &lt;code&gt;DEPT_ID = '65'&lt;/code&gt;를 UPDATE하려고 했을 때 DB가 직접 오류를 발생시키는 것을 보면서 &lt;b&gt;FK의 핵심 목적은 단순 연결이 아니라 참조 무결성 보장&lt;/b&gt;이라는 점을 체감할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Sequence와 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;를 통해 PK를 사람이 직접 관리하지 않고 DB가 자동으로 생성할 수 있다는 것도 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이전까지 배운 ERD, PK/FK, JOIN이 DDL과 DML을 통해 실제 DB 동작으로 연결된 날이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쇼핑몰 ERD에서는:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;CUSTOMERS
&amp;rarr; ORDERS
&amp;rarr; ORDERDETAIL
&amp;larr; PRODUCTS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계를 보면서 N:M 관계가 왜 중간 테이블을 필요로 하는지 다시 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 구조를 실제 테이블로 만들려면:&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;CREATE TABLE
PRIMARY KEY
FOREIGN KEY
CHECK
DEFAULT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 DDL 문법이 필요하고, 만들어진 구조에 데이터를 넣고 수정하려면 DML이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘은 오류가 발생한 이유를 직접 확인하는 과정이 좋았다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;존재하지 않는 FK 값
&amp;rarr; 참조 무결성 오류

NULL
&amp;rarr; 관계 없음으로 허용 가능

Sequence MAXVALUE 초과
&amp;rarr; 자동번호 생성 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 DB가 단순히 데이터를 저장하는 공간이 아니라, &lt;b&gt;정해진 규칙을 기반으로 잘못된 데이터를 차단하는 시스템&lt;/b&gt;이라는 점이 더 명확하게 느껴졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 마지막 실습 과제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 마지막에는 &lt;b&gt;쇼핑몰 ERD 기반 DDL/DML 실습 과제&lt;/b&gt;가 제시됐다.&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;실습 구조는:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;CUSTOMERS
ORDERS
PRODUCTS
ORDERDETAIL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개 테이블을 기반으로 구성되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 요구사항은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;테이블 생성
      &amp;darr;
상품 / 회원 데이터 INSERT
      &amp;darr;
주문 데이터 INSERT
      &amp;darr;
주문 상세 데이터 INSERT
      &amp;darr;
상품 재고 UPDATE
      &amp;darr;
전체 주문 목록 JOIN
      &amp;darr;
일별 매출 GROUP BY + SUM&lt;/code&gt;&lt;/pre&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;ORDERS INSERT
+
ORDERDETAIL INSERT
+
PRODUCTS STOCK UPDATE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 함께 수행되어야 한다는 점이 눈에 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 문제만 확인한 상태이고, 다음 학습에서는 직접 쿼리를 작성하면서 테이블 관계와 DML 흐름을 구현해볼 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 오늘 마지막으로 제시된 쇼핑몰 실습을 직접 구현하면서 다음 부분을 집중적으로 확인해야겠다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. ERD에서 PK / FK 관계 먼저 확인

2. 부모 테이블부터 INSERT

3. 자식 테이블은 부모 PK가 존재하는지 확인

4. 주문 1건과 주문상세 N건의 관계 이해

5. 주문 시 재고 감소 UPDATE

6. 여러 테이블 JOIN으로 주문내역 조회

7. GROUP BY + SUM으로 매출 집계&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 오늘 DML에서 &lt;code&gt;COMMIT&lt;/code&gt;, &lt;code&gt;ROLLBACK&lt;/code&gt;이 언급됐기 때문에, 주문 처리처럼 여러 SQL이 하나의 작업 단위로 묶이는 경우:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;주문 INSERT
&amp;rarr; 주문상세 INSERT
&amp;rarr; 재고 UPDATE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중 하나라도 실패했을 때 전체 작업을 어떻게 되돌릴 것인지 &lt;b&gt;Transaction 관점&lt;/b&gt;으로도 연결해서 복습해야겠다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>constraint</category>
      <category>database</category>
      <category>DDL</category>
      <category>DML</category>
      <category>foreignkey</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>SQL</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/45</guid>
      <comments>https://bbbicsan.tistory.com/45#entry45comment</comments>
      <pubDate>Fri, 28 Aug 2026 17:19:32 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 22일차 TIL : Subquery &amp;amp; DDL</title>
      <link>https://bbbicsan.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 22일차 TIL : Subquery &amp;amp; DDL&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVoZA8/dJMcaheDFGu/RlHwGFprwTswQSTOJA1HOK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVoZA8/dJMcaheDFGu/RlHwGFprwTswQSTOJA1HOK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVoZA8/dJMcaheDFGu/RlHwGFprwTswQSTOJA1HOK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVoZA8%2FdJMcaheDFGu%2FRlHwGFprwTswQSTOJA1HOK%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;단일행&amp;middot;다중행&amp;middot;다중열 서브쿼리와 &lt;code&gt;IN / ANY / ALL&lt;/code&gt;, Inline View, 상호연관 서브쿼리까지 실습하고, 후반부에는 &lt;code&gt;CREATE / DROP / ALTER&lt;/code&gt;과 &lt;code&gt;PRIMARY KEY / FOREIGN KEY / NOT NULL / UNIQUE / CHECK&lt;/code&gt; 제약조건을 통해 테이블 구조를 직접 정의했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Subquery
   &amp;darr;
단일행 서브쿼리
   &amp;darr;
다중행 서브쿼리
   &amp;darr;
IN / ANY / ALL
   &amp;darr;
단일컬럼 / 다중컬럼
   &amp;darr;
Nested / Correlated Subquery
   &amp;darr;
Scalar Subquery
Inline View
WHERE Subquery
   &amp;darr;
서브쿼리 성능 고려
   &amp;darr;
Inline View + JOIN으로 개선
   &amp;darr;
Additional SELECT - Option 실습
   &amp;darr;
DDL
   &amp;darr;
CREATE / DROP / ALTER
   &amp;darr;
CREATE TABLE
   &amp;darr;
Constraint
PRIMARY KEY
FOREIGN KEY
NOT NULL
UNIQUE
CHECK
DEFAULT
   &amp;darr;
DDL + DML 제약조건 검증&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Subquery&lt;/li&gt;
&lt;li&gt;Single Row Subquery&lt;/li&gt;
&lt;li&gt;Multi Row Subquery&lt;/li&gt;
&lt;li&gt;Single Column Subquery&lt;/li&gt;
&lt;li&gt;Multi Column Subquery&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IN&lt;/code&gt;&lt;code&gt;ANY&lt;/code&gt;&lt;code&gt;ALL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Scalar Subquery&lt;/li&gt;
&lt;li&gt;Inline View&lt;/li&gt;
&lt;li&gt;Correlated Subquery&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;code&gt;LIMIT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OFFSET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRUNCATE()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DDL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE&lt;/code&gt;&lt;code&gt;DROP&lt;/code&gt;&lt;code&gt;ALTER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Constraint&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Subquery란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 SQL Day04의 첫 번째 주제는 &lt;b&gt;Subquery&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리는 하나의 SQL 안에 또 다른 SQL이 포함되는 구조이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Main Query
    &amp;darr;
Subquery 실행
    &amp;darr;
Subquery 결과 반환
    &amp;darr;
Main Query가 결과를 이용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리는 반환되는 데이터의 형태에 따라 다음처럼 구분할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;단일컬럼(Single Column)
다중컬럼(Multi Column)

단일행(Single Row)
다중행(Multi Row)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기술되는 위치에 따라:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT 절
&amp;rarr; Scalar Subquery

FROM 절
&amp;rarr; Inline View

WHERE 절
&amp;rarr; Subquery&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;Nested Subquery
Correlated Subquery&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 나눌 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 단일행 서브쿼리&lt;/h1&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;대표적으로:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;=
&amp;gt;
&amp;lt;
&amp;gt;=
&amp;lt;=
&amp;lt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 일반 비교 연산자를 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;pre class=&quot;&quot;&gt;&lt;code&gt;나승원 사원과 같은 직급이고
나승원 사원보다 많은 급여를 받는
사원의 정보를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 나승원의 기준값을 조회했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT JOB_ID,
       SALARY
FROM employee
WHERE EMP_NAME = '나승원';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;JOB_ID &amp;rarr; J5
SALARY &amp;rarr; 2,300,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 문제는 실제로:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;JOB_ID = J5
AND
SALARY &amp;gt; 2,300,000&lt;/code&gt;&lt;/pre&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE JOB_ID = (
        SELECT JOB_ID
        FROM employee
        WHERE EMP_NAME = '나승원'
      )
  AND SALARY &amp;gt; (
        SELECT SALARY
        FROM employee
        WHERE EMP_NAME = '나승원'
      );&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이 흐름&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;나승원의 JOB_ID 조회
        &amp;darr;
같은 직급 필터

나승원의 SALARY 조회
        &amp;darr;
더 높은 급여 필터&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 쿼리를 먼저 따로 실행해 &lt;b&gt;몇 행이 반환되는지 확인한 뒤 외부 쿼리와 결합하는 과정&lt;/b&gt;이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. MIN()을 이용한 단일행 서브쿼리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최저 급여를 받는 사원을 조회했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE SALARY = (
    SELECT MIN(SALARY)
    FROM employee
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SELECT MIN(SALARY)
        &amp;darr;
1,500,000
        &amp;darr;
SALARY = 1,500,000
        &amp;darr;
해당 사원 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 3명이었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;정지현 &amp;rarr; 1,500,000
고승우 &amp;rarr; 1,500,000
염정하 &amp;rarr; 1,500,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서브쿼리 결과
&amp;rarr; 값 1개

외부 쿼리 결과
&amp;rarr; 여러 행 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 점이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Inline View&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FROM&lt;/code&gt; 절에 서브쿼리를 넣으면 그 결과를 &lt;b&gt;가상의 테이블처럼 사용&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;code&gt;Inline View&lt;/code&gt;라고 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT V.DEPT_NAME,
       V.TOTAL_SALARY
FROM (
    SELECT D.DEPT_NAME,
           SUM(E.SALARY) AS TOTAL_SALARY
    FROM department D
    JOIN employee E
      ON D.DEPT_ID = E.DEPT_ID
    GROUP BY D.DEPT_NAME
) V;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;내부 SELECT
&amp;rarr; 집계 결과 생성
        &amp;darr;
가상 테이블 V
        &amp;darr;
외부 SELECT
&amp;rarr; V를 실제 테이블처럼 사용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. 부서별 급여총합이 가장 높은 부서 찾기&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서별 급여총합이 가장 높은 부서의
부서명과 급여총합을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Inline View를 이용해 최대값을 구하는 흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 단순히:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;SELECT V.DEPT_NAME,
       MAX(V.SUM)&lt;/code&gt;&lt;/pre&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;이후 더 간단하게:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT D.DEPT_NAME,
       SUM(E.SALARY) AS S
FROM employee E
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
GROUP BY E.DEPT_ID
ORDER BY S DESC
LIMIT 1;&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;해외영업3팀 &amp;rarr; 18,100,000&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;부서별 GROUP BY
      &amp;darr;
SUM(SALARY)
      &amp;darr;
ORDER BY DESC
      &amp;darr;
LIMIT 1
      &amp;darr;
최대 급여총합 부서&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. LIMIT과 OFFSET&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LIMIT&lt;/code&gt;은 조회할 행 개수를 제한하고, &lt;code&gt;OFFSET&lt;/code&gt;은 앞에서 몇 개의 행을 건너뛸지 지정한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT D.DEPT_NAME,
       SUM(E.SALARY) AS S
FROM employee E
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
GROUP BY E.DEPT_ID
ORDER BY S DESC
LIMIT 2 OFFSET 1;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;OFFSET 1
&amp;rarr; 첫 번째 행 건너뜀

LIMIT 2
&amp;rarr; 그 다음 2행 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1위 &amp;rarr; 해외영업3팀
2위 &amp;rarr; 해외영업1팀
3위 &amp;rarr; 기술지원팀&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;LIMIT 2 OFFSET 1
&amp;rarr; 2위, 3위 조회&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1페이지 &amp;rarr; LIMIT 10 OFFSET 0
2페이지 &amp;rarr; LIMIT 10 OFFSET 10
3페이지 &amp;rarr; LIMIT 10 OFFSET 20&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. 다중행 서브쿼리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중행 서브쿼리는 내부 쿼리가 여러 행을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 일반적인 &lt;code&gt;=&lt;/code&gt; 비교 대신:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;IN
ANY
ALL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. ANY / ALL&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 중요한 비교 연산자였다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&amp;gt; ANY
&amp;rarr; 결과 중 하나보다만 크면 TRUE
&amp;rarr; 최솟값보다 크면 TRUE

&amp;gt; ALL
&amp;rarr; 모든 값보다 커야 TRUE
&amp;rarr; 최댓값보다 커야 TRUE

&amp;lt; ANY
&amp;rarr; 결과 중 하나보다만 작으면 TRUE
&amp;rarr; 최댓값보다 작으면 TRUE

&amp;lt; ALL
&amp;rarr; 모든 값보다 작아야 TRUE
&amp;rarr; 최솟값보다 작으면 TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 2. 과장보다 많은 급여를 받는 대리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 과장과 대리의 급여를 따로 조회했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       E.SALARY
FROM employee E
JOIN job J
  ON E.JOB_ID = J.JOB_ID
WHERE J.JOB_TITLE = '과장';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       E.SALARY
FROM employee E
JOIN job J
  ON E.JOB_ID = J.JOB_ID
WHERE J.JOB_TITLE = '대리';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;김예수 &amp;rarr; 2,100,000
나승원 &amp;rarr; 2,300,000
박하일 &amp;rarr; 2,600,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이에서는 &lt;code&gt;&amp;gt; ANY&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       E.SALARY
FROM employee E
JOIN job J
  ON E.JOB_ID = J.JOB_ID
WHERE J.JOB_TITLE = '대리'
  AND SALARY &amp;gt; ANY (
      SELECT E.SALARY
      FROM employee E
      JOIN job J
        ON E.JOB_ID = J.JOB_ID
      WHERE J.JOB_TITLE = '과장'
  );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;&amp;gt; ANY
&amp;rarr; 과장 중 한 명의 급여보다라도 높으면 통과
&amp;rarr; 과장 급여의 최솟값보다 높으면 TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 3. 모든 과장보다 적은 급여를 받는 대리&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       E.SALARY
FROM employee E
JOIN job J
  ON E.JOB_ID = J.JOB_ID
WHERE J.JOB_TITLE = '대리'
  AND SALARY &amp;lt; ALL (
      SELECT E.SALARY
      FROM employee E
      JOIN job J
        ON E.JOB_ID = J.JOB_ID
      WHERE J.JOB_TITLE = '과장'
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전우성 &amp;rarr; 2,090,000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;&amp;lt; ALL
&amp;rarr; 모든 과장 급여보다 작아야 함
&amp;rarr; 결국 과장 급여 중 최솟값보다도 작아야 함&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. 다중열 서브쿼리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중열 서브쿼리는 하나의 컬럼이 아니라 &lt;b&gt;여러 컬럼을 하나의 묶음으로 비교&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;단일열
SALARY IN (...)

다중열
(DEPT_ID, SALARY) IN (...)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 4. 부서별 최소급여를 받는 사원&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서별 최소급여를 받는 사원의
이름, 부서명, 급여를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       DEPT_ID,
       SALARY
FROM employee
WHERE SALARY IN (
    SELECT MIN(SALARY)
    FROM employee
    GROUP BY DEPT_ID
);&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;이 쿼리는:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;자기 부서의 최소급여인가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;어느 부서든 최소급여 값 중 하나와 같은가?&quot;&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DEPT_ID&lt;/code&gt;와 &lt;code&gt;SALARY&lt;/code&gt;를 한 쌍으로 비교했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       DEPT_ID,
       SALARY
FROM employee E
WHERE (DEPT_ID, SALARY) IN (
    SELECT DEPT_ID,
           MIN(SALARY)
    FROM employee
    GROUP BY DEPT_ID
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서명까지 출력하면:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME,
       E.SALARY
FROM employee E
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
WHERE (E.DEPT_ID, E.SALARY) IN (
    SELECT DEPT_ID,
           MIN(SALARY)
    FROM employee
    GROUP BY DEPT_ID
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;고승우 | 본사 인사팀  | 1,500,000
김술오 | 회계팀       | 2,500,000
감우섭 | 회계팀       | 2,500,000
이중기 | 회계팀       | 2,500,000
정지현 | 해외영업1팀 | 1,500,000
정도연 | 기술지원팀   | 2,600,000
전우성 | 해외영업2팀 | 2,090,000
최만식 | 해외영업3팀 | 3,600,000&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 중첩 서브쿼리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국어국문학과에서 평균 평점이 가장 높은 학생을 찾는 문제에서는 여러 단계의 서브쿼리를 중첩했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT S.STUDENT_NO,
       S.STUDENT_NAME
FROM tb_student S
JOIN tb_grade G
  ON S.STUDENT_NO = G.STUDENT_NO
WHERE S.DEPARTMENT_NO = (
    SELECT DEPARTMENT_NO
    FROM tb_department
    WHERE DEPARTMENT_NAME = '국어국문학과'
)
GROUP BY S.STUDENT_NO,
         S.STUDENT_NAME
HAVING AVG(G.POINT) = (
    SELECT MAX(AVG_POINT)
    FROM (
        SELECT AVG(G2.POINT) AS AVG_POINT
        FROM tb_student S2
        JOIN tb_grade G2
          ON S2.STUDENT_NO = G2.STUDENT_NO
        WHERE S2.DEPARTMENT_NO = (
            SELECT DEPARTMENT_NO
            FROM tb_department
            WHERE DEPARTMENT_NAME = '국어국문학과'
        )
        GROUP BY S2.STUDENT_NO
    ) T
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;국어국문학과
     &amp;darr;
DEPARTMENT_NO 조회
     &amp;darr;
학생별 AVG(POINT)
     &amp;darr;
Inline View
     &amp;darr;
MAX(AVG_POINT)
     &amp;darr;
외부 HAVING에서 비교
     &amp;darr;
최고 평균 학생 조회&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 같은 문제를 RANK()로 해결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 문제를 Window Function으로도 풀었다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT V.STUDENT_NO,
       V.STUDENT_NAME
FROM (
    SELECT S.STUDENT_NO,
           S.STUDENT_NAME,
           RANK() OVER(
               ORDER BY AVG(G.POINT) DESC
           ) AS `rank`
    FROM TB_GRADE G
    JOIN TB_STUDENT S
      ON G.STUDENT_NO = S.STUDENT_NO
    JOIN TB_DEPARTMENT D
      ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
    WHERE D.DEPARTMENT_NAME = '국어국문학과'
    GROUP BY S.STUDENT_NO,
             S.STUDENT_NAME
    ORDER BY `rank`
) V
LIMIT 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;WHERE V.`rank` = 1&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;LIMIT 1
&amp;rarr; 첫 1명만 반환

WHERE rank = 1
&amp;rarr; 공동 1등이 여러 명이면 모두 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 상호연관(Correlated) 서브쿼리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상호연관 서브쿼리는 &lt;b&gt;내부 서브쿼리가 외부 쿼리의 컬럼을 참조하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Outer Query
   &amp;darr;
현재 행의 값 전달
   &amp;darr;
Inner Query
   &amp;darr;
결과 반환
   &amp;darr;
현재 행과 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME,
       E.SALARY
FROM employee E
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
WHERE E.SALARY = (
    SELECT MIN(E2.SALARY)
    FROM employee E2
    WHERE E2.DEPT_ID = E.DEPT_ID
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 쿼리에서:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;E2.DEPT_ID = E.DEPT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 외부 &lt;code&gt;E&lt;/code&gt;를 참조하기 때문에 상호연관 서브쿼리이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. 상호연관 서브쿼리의 성능 고려사항&lt;/h1&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;예를 들어 사원이 8만 명이고 각 사원에 대해:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;자신의 부서 평균연봉보다 많이 받는가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 검사한다면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사원 1명
&amp;rarr; 부서 평균 계산

사원 1명
&amp;rarr; 부서 평균 계산

...

최대 8만 번 반복&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서별 평균을 미리 한 번 계산하고 JOIN한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;부서별 AVG(연봉)
      &amp;darr;
Inline View
      &amp;darr;
사원 테이블과 JOIN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT A.사원명,
       A.사원번호,
       A.연봉,
       B.부서평균연봉
FROM 사원 A,
     (
         SELECT 부서번호,
                AVG(연봉) AS 부서평균연봉
         FROM 사원
         GROUP BY 부서번호
     ) B
WHERE A.부서번호 = B.부서번호;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;반복 서브쿼리
&amp;rarr; 동일 계산 반복 가능

Inline View + JOIN
&amp;rarr; 계산 결과를 먼저 만들고 재사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. Scalar Subquery 성능 고려사항&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT List에서 사용하는 Scalar Subquery도 결과 행마다 반복 실행될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT A.사원명,
       A.사원번호,
       A.연봉,
       (
           SELECT AVG(연봉)
           FROM 사원
           WHERE 부서번호 = A.부서번호
       ) AS 부서평균연봉
FROM 사원 A
WHERE A.부서구분 = '본부부서';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 1,000명이면 같은 부서 평균을 최대 1,000번 반복 계산할 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;부서별 평균을 먼저 계산
        &amp;darr;
Inline View
        &amp;darr;
JOIN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 방식으로 개선할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. Nested Loop Join 실행 계획&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리와 JOIN을 설명하는 과정에서 실행 계획도 살펴봤다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT STATEMENT
   &amp;darr;
NESTED LOOPS
   &amp;darr;
TABLE ACCESS BY INDEX ROWID
   &amp;darr;
INDEX RANGE SCAN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nested Loop Join은 한쪽 데이터를 기준으로 다른 테이블을 반복 탐색하는 방식으로 설명됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힌트 예:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;/*+ USE_NL(A B) */&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;USE_NL
&amp;rarr; Nested Loop Join 유도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 실행 계획 자체를 깊게 분석하기보다 &lt;b&gt;SQL 작성 방식에 따라 실제 DB 내부 접근 방식과 성능이 달라질 수 있다는 점&lt;/b&gt;을 함께 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. 직급별 평균급여와 같은 급여를 받는 사원&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;자기 직급의 평균급여를 받는 직원의
이름, 직급명, 급여를 검색한다면?

조건)
평균급여는 정수 5자리까지 절삭&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 무려 세 가지 방식으로 풀이했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CASE 01. 다중열 IN 서브쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 작성 과정에서 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;WHERE (JOB_ID, SALARY) IN (...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 (1052)
Column 'JOB_ID' in IN/ALL/ANY is ambiguous&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1697&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhxHOV/dJMcajpNGu5/V1uetxgbmjj3UFUeKP1iy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhxHOV/dJMcajpNGu5/V1uetxgbmjj3UFUeKP1iy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhxHOV/dJMcajpNGu5/V1uetxgbmjj3UFUeKP1iy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhxHOV%2FdJMcajpNGu5%2FV1uetxgbmjj3UFUeKP1iy0%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;1697&quot; height=&quot;652&quot; data-origin-width=&quot;1697&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;job J
employee E&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 모두 존재했고 두 테이블에 &lt;code&gt;JOB_ID&lt;/code&gt;가 모두 있기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 입장에서는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;JOB_ID
&amp;rarr; J.JOB_ID?
&amp;rarr; E.JOB_ID?&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;테이블 별칭을 명확하게 지정했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       E.SALARY
FROM job J
JOIN employee E
  ON J.JOB_ID = E.JOB_ID
WHERE (J.JOB_ID, E.SALARY) IN (
    SELECT JOB_ID,
           TRUNCATE(AVG(SALARY), -5)
    FROM employee
    GROUP BY JOB_ID
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;한선기 | 대표이사 | 9,000,000
안석규 | 부장     | 3,500,000
김술오 | 차장     | 2,500,000
이중기 | 차장     | 2,500,000
감우섭 | 차장     | 2,500,000
나승원 | 과장     | 2,300,000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjXJLQ/dJMb99OhKxU/Pks9EA0na7JcUYqnVIsKBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjXJLQ/dJMb99OhKxU/Pks9EA0na7JcUYqnVIsKBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjXJLQ/dJMb99OhKxU/Pks9EA0na7JcUYqnVIsKBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjXJLQ%2FdJMb99OhKxU%2FPks9EA0na7JcUYqnVIsKBK%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;1320&quot; height=&quot;541&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CASE 02. Inline View + JOIN&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       E.SALARY
FROM (
    SELECT JOB_ID,
           TRUNCATE(AVG(SALARY), -5) AS `JOBAVG`
    FROM employee
    GROUP BY JOB_ID
) V
JOIN employee E
  ON V.JOB_ID = E.JOB_ID
 AND V.JOBAVG = E.SALARY
JOIN job J
  ON E.JOB_ID = J.JOB_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;직급별 평균급여 계산
       &amp;darr;
Inline View V
       &amp;darr;
JOB_ID + 평균급여로 employee JOIN
       &amp;darr;
job JOIN
       &amp;darr;
직급명 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1715&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czv4oY/dJMcahsbRVl/ip7KmbcLMnNKP2e5WpFSUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czv4oY/dJMcahsbRVl/ip7KmbcLMnNKP2e5WpFSUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czv4oY/dJMcahsbRVl/ip7KmbcLMnNKP2e5WpFSUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fczv4oY%2FdJMcahsbRVl%2Fip7KmbcLMnNKP2e5WpFSUK%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;1715&quot; height=&quot;944&quot; data-origin-width=&quot;1715&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CASE 03. Correlated Subquery&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       E.SALARY
FROM job J
JOIN employee E
  ON J.JOB_ID = E.JOB_ID
WHERE E.SALARY = (
    SELECT TRUNCATE(AVG(SALARY), -5)
    FROM employee EE
    WHERE EE.JOB_ID = E.JOB_ID
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 쿼리가:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;EE.JOB_ID = E.JOB_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 외부 행의 직급번호를 참조한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTFUa/dJMcafVqxYC/vnDF6YIinOd4mHkp7AKNTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTFUa/dJMcafVqxYC/vnDF6YIinOd4mHkp7AKNTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTFUa/dJMcafVqxYC/vnDF6YIinOd4mHkp7AKNTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTFUa%2FdJMcafVqxYC%2FvnDF6YIinOd4mHkp7AKNTK%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;813&quot; height=&quot;592&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 풀이 비교&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CASE 01
다중열 IN
&amp;rarr; (JOB_ID, SALARY) 쌍 비교

CASE 02
Inline View + JOIN
&amp;rarr; 직급별 평균을 먼저 만들어 JOIN

CASE 03
Correlated Subquery
&amp;rarr; 현재 사원의 JOB_ID를 내부 쿼리가 참조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 결과를 여러 방식으로 구현해보면서 &lt;b&gt;SQL은 결과만 같다고 끝나는 것이 아니라 데이터량과 실행 방식까지 고려해야 한다&lt;/b&gt;는 점을 알 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Additional SELECT - Option 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 강사님은 기존 과제 범위를 확장해서:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;[Additional SELECT - Option]
Q1 ~ Q19&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q9. 인문사회 계열 과목과 담당 교수&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT C2.CLASS_NAME,
       P.PROFESSOR_NAME
FROM TB_CLASS_PROFESSOR C1
JOIN TB_CLASS C2 USING(CLASS_NO)
JOIN TB_PROFESSOR P USING(PROFESSOR_NO)
JOIN TB_DEPARTMENT D
  ON P.DEPARTMENT_NO = D.DEPARTMENT_NO
WHERE D.CATEGORY = '인문사회';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;TB_CLASS_PROFESSOR
      &amp;darr;
과목 연결
      &amp;darr;
교수 연결
      &amp;darr;
교수의 소속 학과 연결
      &amp;darr;
CATEGORY = 인문사회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 화면 기준 &lt;code&gt;197행&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l8wNf/dJMcafgNHYt/hep2BWo0hCkQbUAKXwo6Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l8wNf/dJMcafgNHYt/hep2BWo0hCkQbUAKXwo6Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l8wNf/dJMcafgNHYt/hep2BWo0hCkQbUAKXwo6Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl8wNf%2FdJMcafgNHYt%2Fhep2BWo0hCkQbUAKXwo6Mk%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;684&quot; height=&quot;325&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q10. 음악학과 학생 전체 평점&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT S.STUDENT_NO AS '학번',
       S.STUDENT_NAME AS '학생 이름',
       ROUND(AVG(G.POINT), 1) AS '전체 평점'
FROM tb_grade G
JOIN tb_student S
  ON G.STUDENT_NO = S.STUDENT_NO
JOIN tb_department D
  ON S.DEPARTMENT_NO = D.DEPARTMENT_NO
WHERE D.DEPARTMENT_NAME = '음악학과'
GROUP BY S.STUDENT_NO,
         S.STUDENT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;학생 + 성적 + 학과 JOIN
        &amp;darr;
음악학과 필터
        &amp;darr;
학생별 GROUP BY
        &amp;darr;
AVG(POINT)
        &amp;darr;
ROUND(..., 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 8명이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byDA0a/dJMcabSV3gq/zTYjGthADK7HqGN7kxn6b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byDA0a/dJMcabSV3gq/zTYjGthADK7HqGN7kxn6b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byDA0a/dJMcabSV3gq/zTYjGthADK7HqGN7kxn6b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyDA0a%2FdJMcabSV3gq%2FzTYjGthADK7HqGN7kxn6b1%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;657&quot; height=&quot;523&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q17. 최경희 학생과 같은 과 학생 조회&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT S.STUDENT_NAME,
       S.STUDENT_ADDRESS
FROM tb_student S
WHERE DEPARTMENT_NO = (
    SELECT S.DEPARTMENT_NO
    FROM tb_student S
    WHERE S.STUDENT_NAME = '최경희'
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;최경희
  &amp;darr;
DEPARTMENT_NO 조회
  &amp;darr;
같은 DEPARTMENT_NO 학생 조회
  &amp;darr;
이름 + 주소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 &lt;code&gt;17행&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR1E2f/dJMcagUiklp/pUyINRLoIlF3S3T8k9KZb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR1E2f/dJMcagUiklp/pUyINRLoIlF3S3T8k9KZb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR1E2f/dJMcagUiklp/pUyINRLoIlF3S3T8k9KZb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR1E2f%2FdJMcagUiklp%2FpUyINRLoIlF3S3T8k9KZb1%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;883&quot; height=&quot;559&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q18. 국어국문학과 최고 평점 학생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 배운 중첩 서브쿼리와 &lt;code&gt;RANK()&lt;/code&gt; 풀이를 적용한 문제였다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;국어국문학과 찾기
      &amp;darr;
학생별 평균 계산
      &amp;darr;
최고 평균 계산
      &amp;darr;
최고 평점 학생 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 오늘 서브쿼리 단원의 종합 예제에 가까웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q19. 환경조경학과와 같은 계열 학과의 전공평점&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT D.DEPARTMENT_NAME AS '계열학과명',
       ROUND(AVG(G.POINT), 1) AS '전공평점'
FROM tb_student S
JOIN tb_department D
  ON D.DEPARTMENT_NO = S.DEPARTMENT_NO
JOIN tb_grade G
  ON S.STUDENT_NO = G.STUDENT_NO
JOIN tb_class C
  ON D.DEPARTMENT_NO = C.DEPARTMENT_NO
WHERE D.CATEGORY = (
    SELECT CATEGORY
    FROM tb_department
    WHERE DEPARTMENT_NAME = '환경조경학과'
)
  AND C.CLASS_TYPE LIKE '%전공%'
GROUP BY D.DEPARTMENT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;환경조경학과
      &amp;darr;
CATEGORY 조회
      &amp;darr;
같은 계열 학과 조회
      &amp;darr;
전공 과목 조건
      &amp;darr;
학과별 GROUP BY
      &amp;darr;
AVG + ROUND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 화면 기준 &lt;code&gt;20행&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wTu5l/dJMcag0WLW1/8vhUgRqdl4j4jM4hY3TAdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wTu5l/dJMcag0WLW1/8vhUgRqdl4j4jM4hY3TAdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wTu5l/dJMcag0WLW1/8vhUgRqdl4j4jM4hY3TAdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwTu5l%2FdJMcag0WLW1%2F8vhUgRqdl4j4jM4hY3TAdK%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;760&quot; height=&quot;487&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. DDL&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리 실습 이후에는 DDL로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL은 데이터 자체를 조회&amp;middot;수정하는 것이 아니라 &lt;b&gt;데이터를 저장할 구조를 정의하는 SQL&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;DDL
&amp;rarr; Data Definition Language&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표 명령어:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE
&amp;rarr; 생성

ALTER
&amp;rarr; 수정

DROP
&amp;rarr; 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL을 통해:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Database
Table
View
Constraint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 구조를 정의한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. CREATE TABLE 기본 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 확인한 기본 형태:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE TABLE TABLE_NAME (
    COLUMN_NAME DATATYPE(SIZE) [DEFAULT] [CONSTRAINT TYPE],
    [CONSTRAINT TYPE]
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;컬럼명
+
데이터 타입
+
기본값
+
제약조건&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. 제약조건&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 주요 제약조건은 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;제약조건&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;행을 유일하게 식별&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른 테이블의 키 참조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL 입력 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;중복값 입력 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;지정 조건을 만족하는 값만 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;값이 생략될 때 기본값 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. EMPLOYEE 테이블 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 &lt;code&gt;EMPLOYEE&lt;/code&gt; 테이블을 생성하며 다양한 제약조건을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE EMPLOYEE (
    EMP_ID     CHAR(3) PRIMARY KEY,
    EMP_NAME   VARCHAR(20) NOT NULL,
    EMP_NO     CHAR(14) NOT NULL,
    EMAIL      VARCHAR(25),
    PHONE      VARCHAR(12),
    HIRE_DATE  DATE DEFAULT NOW(),
    JOB_ID     CHAR(2),
    SALARY     INT,
    BONUS_PCT  INT,
    MARRIAGE   CHAR(1) DEFAULT 'N',
    MGR_ID     CHAR(3),
    DEPT_ID    CHAR(2) REFERENCES DEPARTMENT(DEPT_ID),

    FOREIGN KEY(JOB_ID) REFERENCES JOB(JOB_ID),
    CHECK (MARRIAGE IN ('Y', 'N')),
    UNIQUE(EMP_NO)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EMP_ID
&amp;rarr; PRIMARY KEY

EMP_NAME
EMP_NO
&amp;rarr; NOT NULL

HIRE_DATE
&amp;rarr; DEFAULT NOW()

MARRIAGE
&amp;rarr; DEFAULT 'N'
&amp;rarr; CHECK ('Y', 'N')

JOB_ID
&amp;rarr; JOB 테이블 FOREIGN KEY

DEPT_ID
&amp;rarr; DEPARTMENT 테이블 참조

EMP_NO
&amp;rarr; UNIQUE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. Column Constraint vs Table Constraint&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약조건은 컬럼을 선언하면서 작성할 수도 있고, 테이블 선언 마지막에 별도로 작성할 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Column Constraint&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;BLOG_ID VARCHAR(50) PRIMARY KEY&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Table Constraint&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;PRIMARY KEY (BLOG_ID)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Column Constraint
&amp;rarr; 컬럼 옆에서 바로 선언

Table Constraint
&amp;rarr; 컬럼 정의 후 별도로 선언&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. DATABASE와 TABLE 생성 실습&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DROP DATABASE IF EXISTS TABLEDB;
CREATE DATABASE TABLEDB;

USE TABLEDB;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;DROP DATABASE IF EXISTS
&amp;rarr; DB가 존재하면 삭제

CREATE DATABASE
&amp;rarr; 데이터베이스 생성

USE
&amp;rarr; 사용할 데이터베이스 선택&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;TB_BLOG&lt;/code&gt; 테이블을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DROP TABLE IF EXISTS TB_BLOG;

CREATE TABLE TB_BLOG (
    BLOG_ID   VARCHAR(50),
    TITLE     VARCHAR(100),
    CONTENT   VARCHAR(100),
    EMAIL     VARCHAR(100),
    CREATE_AT DATE,

    PRIMARY KEY (BLOG_ID)
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. PRIMARY KEY 중복 오류&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TB_BLOG&lt;/code&gt;에 데이터를 INSERT하면서 PK 제약조건을 직접 확인했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_BLOG(
    BLOG_ID,
    TITLE,
    CONTENT,
    EMAIL,
    CREATE_AT
)
VALUES(
    '1',
    '휴',
    '하루 마무리',
    'jslim9413@naver.com',
    SYSDATE()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &lt;code&gt;BLOG_ID = '1'&lt;/code&gt;을 다시 입력하자:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 (1062)
Duplicate entry '1' for key 'PRIMARY'&lt;/code&gt;&lt;/pre&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;pre class=&quot;sas&quot;&gt;&lt;code&gt;BLOG_ID
&amp;rarr; PRIMARY KEY
&amp;rarr; 중복 불가&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. PRIMARY KEY NULL 오류&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_BLOG(BLOG_ID, TITLE, CONTENT, EMAIL)
VALUES(
    NULL,
    '휴',
    '하루 마무리',
    'jslim9413@naver.com'
);&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 (1048)
Column 'BLOG_ID' cannot be null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;는:&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;UNIQUE
+
NOT NULL&lt;/code&gt;&lt;/pre&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;중복값 ❌
NULL ❌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. DEFAULT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 다시 생성하면서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE_AT DATE DEFAULT SYSDATE()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 지정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 INSERT에서 &lt;code&gt;CREATE_AT&lt;/code&gt;을 생략해도:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_BLOG(BLOG_ID, TITLE, CONTENT, EMAIL)
VALUES(
    '1',
    '휴',
    '하루 마무리',
    'jslim9413@naver.com'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 날짜가 자동으로 들어간다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;INSERT에서 CREATE_AT 생략
        &amp;darr;
DEFAULT SYSDATE()
        &amp;darr;
현재 날짜 자동 입력&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. CHECK 제약조건&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CATEGORY&lt;/code&gt; 컬럼을 추가하고 입력 가능한 값을 제한했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE TB_BLOG (
    BLOG_ID   VARCHAR(50),
    TITLE     VARCHAR(100),
    CONTENT   VARCHAR(100),
    EMAIL     VARCHAR(100),
    CATEGORY  VARCHAR(100)
              CHECK(CATEGORY IN ('전체', '취미', '일상', '코딩')),
    CREATE_AT DATE DEFAULT SYSDATE(),

    PRIMARY KEY (BLOG_ID)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허용되는 값:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;전체
취미
일상
코딩&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO TB_BLOG(BLOG_ID, CATEGORY)
VALUES('1', '전체');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;BLOG_ID   &amp;rarr; 1
TITLE     &amp;rarr; NULL
CONTENT   &amp;rarr; NULL
EMAIL     &amp;rarr; NULL
CATEGORY  &amp;rarr; 전체
CREATE_AT &amp;rarr; 2026-08-27&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 확인한 것은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CHECK
&amp;rarr; CATEGORY 값 제한

DEFAULT
&amp;rarr; CREATE_AT 자동 입력

NOT NULL이 없는 컬럼
&amp;rarr; 값 생략 시 NULL 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 서브쿼리와 DDL 모두에서 &lt;b&gt;오류가 왜 발생했는지를 직접 확인하고 쿼리를 수정하는 과정&lt;/b&gt;이 많았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 부서별 최소급여를 SALARY만 비교했더니 잘못된 사원이 조회된 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;WHERE SALARY IN (
    SELECT MIN(SALARY)
    FROM employee
    GROUP BY DEPT_ID
)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;서브쿼리는:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;부서별 최소급여 &quot;값&quot;&lt;/code&gt;&lt;/pre&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;pre class=&quot;&quot;&gt;&lt;code&gt;다른 부서의 최소급여와 같은 급여&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;WHERE (DEPT_ID, SALARY) IN (
    SELECT DEPT_ID,
           MIN(SALARY)
    FROM employee
    GROUP BY DEPT_ID
)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DEPT_ID + SALARY&lt;/code&gt;가 같은 쌍만 비교되면서 각 부서의 실제 최소급여자만 조회됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;여러 값의 관계가 중요한 문제
&amp;rarr; 단일컬럼 비교보다 다중컬럼 비교 고려&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JOB_ID가 ambiguous하다는 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 오류 (1052)
Column 'JOB_ID' in IN/ALL/ANY is ambiguous&lt;/code&gt;&lt;/pre&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;pre class=&quot;properties&quot;&gt;&lt;code&gt;employee E
job J&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 있고 둘 다 &lt;code&gt;JOB_ID&lt;/code&gt;를 가지고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상태에서:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;WHERE (JOB_ID, SALARY) IN (...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 작성하면 어느 테이블의 &lt;code&gt;JOB_ID&lt;/code&gt;인지 DB가 알 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;WHERE (J.JOB_ID, E.SALARY) IN (...)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;JOIN 후 동일한 컬럼명이 여러 테이블에 존재하면:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;컬럼명만 사용 ❌
테이블별칭.컬럼명 ⭕&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 작성하는 것이 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Inline View에서 부서명과 MAX 값이 잘못 붙은 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서별 급여총합의 최대값을 구하면서:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;SELECT V.DEPT_NAME,
       MAX(V.SUM)&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;집계 함수의 최대값은 구했지만:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;그 MAX 값을 가진 행&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ORDER BY S DESC
LIMIT 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 최대값을 가진 행 자체를 조회했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT D.DEPT_NAME,
       SUM(E.SALARY) AS S
FROM employee E
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
GROUP BY E.DEPT_ID
ORDER BY S DESC
LIMIT 1;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;MAX(value)
&amp;rarr; 최대값

최대값을 가진 행
&amp;rarr; 별도의 조건/정렬 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 차이를 구분해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 상관 서브쿼리의 반복 실행 문제&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사원 8만 명
&amp;rarr; 내부 평균 계산 최대 8만 번&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP BY로 평균을 먼저 한 번 계산
        &amp;darr;
Inline View
        &amp;darr;
JOIN&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;SQL은:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;정답이 나오는가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐 아니라:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;같은 계산을 얼마나 반복하는가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. PRIMARY KEY 중복 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Duplicate entry '1' for key 'PRIMARY'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 &lt;code&gt;BLOG_ID = '1'&lt;/code&gt;을 다시 INSERT했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 PK를 사용하거나 기존 데이터를 삭제해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;PRIMARY KEY
&amp;rarr; 중복 불가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 규칙이 실제 INSERT 시점에서 DB에 의해 검증된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. PRIMARY KEY NULL 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Column 'BLOG_ID' cannot be null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK 컬럼에 &lt;code&gt;NULL&lt;/code&gt;을 넣었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;PRIMARY KEY
&amp;rarr; UNIQUE
&amp;rarr; NOT NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 특성을 동시에 가진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 서브쿼리의 종류를 단순히 외우는 것보다 &lt;b&gt;어떤 값을 몇 개 반환하는지에 따라 어떤 연산자를 선택해야 하는지&lt;/b&gt;가 중요했다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;단일행
&amp;rarr; 일반 비교 연산자

다중행
&amp;rarr; IN / ANY / ALL

다중컬럼
&amp;rarr; (A, B) 형태로 값의 관계까지 함께 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;ANY / ALL&lt;/code&gt;은 처음에는 문장만 보면 헷갈렸지만:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;&amp;gt; ANY
&amp;rarr; 최솟값보다 크면 됨

&amp;gt; ALL
&amp;rarr; 최댓값보다 커야 함

&amp;lt; ANY
&amp;rarr; 최댓값보다 작으면 됨

&amp;lt; ALL
&amp;rarr; 최솟값보다 작아야 함&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;다중열 IN
Inline View + JOIN
Correlated Subquery&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 방법으로 구현해보면서 SQL에는 하나의 정답만 있는 것이 아니라는 점도 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&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;처음에는:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;WHERE 안에 SELECT를 넣는다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로만 생각했는데, 오늘 수업을 통해:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서브쿼리가 몇 행을 반환하는가?
몇 컬럼을 반환하는가?
어디에 위치하는가?
외부 쿼리를 참조하는가?
몇 번 반복 실행될 수 있는가?&lt;/code&gt;&lt;/pre&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;pre class=&quot;excel&quot;&gt;&lt;code&gt;코드는 간단함
        &amp;darr;
하지만 외부 Row마다 반복 실행 가능
        &amp;darr;
데이터가 많으면 성능 문제 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 흐름이 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Inline View에서 먼저 집계한 뒤 JOIN하는 방식과 비교하면서 &lt;b&gt;쿼리 구조가 실행 비용에도 영향을 준다&lt;/b&gt;는 점을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL 파트에서는 이전까지 이미 만들어진 테이블을 조회하는 입장이었다면 오늘은 직접:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Database 생성
Table 생성
Column 정의
Constraint 정의&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 하면서 DB 구조 자체를 만드는 쪽으로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;PRIMARY KEY 선언
        &amp;darr;
중복 INSERT
        &amp;darr;
오류

PRIMARY KEY 선언
        &amp;darr;
NULL INSERT
        &amp;darr;
오류&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 직접 확인하면서 제약조건이 단순한 문법이 아니라 &lt;b&gt;잘못된 데이터가 DB에 들어가는 것을 실제로 차단하는 규칙&lt;/b&gt;이라는 점이 명확해졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 마지막 기준 과제 범위는:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;[Additional SELECT - Option]
Q1 ~ Q19&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 서브쿼리가 몇 개의 행을 반환하는지 먼저 판단
        &amp;darr;
2. 단일행이면 일반 비교 연산자
   다중행이면 IN / ANY / ALL 고려
        &amp;darr;
3. 컬럼 간 관계까지 비교해야 한다면 다중컬럼 서브쿼리 고려
        &amp;darr;
4. GROUP BY 결과 조건이면 HAVING
        &amp;darr;
5. 같은 집계를 반복한다면 Inline View + JOIN 검토
        &amp;darr;
6. 외부 컬럼을 내부 쿼리가 참조한다면 Correlated Subquery
        &amp;darr;
7. 결과만 맞는지보다 실행 구조도 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL에서는 다음 내용을 계속 복습할 예정이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE
DROP
ALTER

PRIMARY KEY
FOREIGN KEY
NOT NULL
UNIQUE
CHECK
DEFAULT

Column Constraint
vs
Table Constraint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 수업에서는 오늘 생성한 테이블을 기준으로 &lt;code&gt;ALTER&lt;/code&gt;를 이용한 구조 변경이나 다른 제약조건 실습이 더 이어질 것 같다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>constraint</category>
      <category>DDL</category>
      <category>inlineview</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>SQL</category>
      <category>subquery</category>
      <category>til</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/44</guid>
      <comments>https://bbbicsan.tistory.com/44#entry44comment</comments>
      <pubDate>Thu, 27 Aug 2026 18:00:13 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 21일차 TIL : GROUP BY&amp;middot;HAVING&amp;middot;Window Function&amp;middot;정규화&amp;middot;JOIN</title>
      <link>https://bbbicsan.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 21일차 TIL : GROUP BY&amp;middot;HAVING&amp;middot;Window Function&amp;middot;정규화&amp;middot;JOIN&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K8pGj/dJMcaiEyJew/r2zeKkCPqK6u1zeaswedwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K8pGj/dJMcaiEyJew/r2zeKkCPqK6u1zeaswedwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K8pGj/dJMcaiEyJew/r2zeKkCPqK6u1zeaswedwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK8pGj%2FdJMcaiEyJew%2Fr2zeKkCPqK6u1zeaswedwk%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;&lt;code&gt;GROUP BY&lt;/code&gt;와 &lt;code&gt;HAVING&lt;/code&gt;을 이용한 그룹 집계부터 &lt;code&gt;ROLLUP&lt;/code&gt;, Window Function, 정규화, Mapping Rule, 다양한 JOIN 방식까지 학습하며 &lt;b&gt;분리된 관계형 데이터를 집계하고 다시 연결하는 전체 흐름&lt;/b&gt;을 이해했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP BY
   &amp;darr;
그룹 함수
COUNT / SUM / AVG / MIN / MAX
   &amp;darr;
WHERE vs HAVING
   &amp;darr;
CASE + GROUP BY
   &amp;darr;
WITH ROLLUP
소계 / 전체 합계
   &amp;darr;
Window Function
OVER / PARTITION BY
   &amp;darr;
RANK()
전체 순위 / 부서별 순위
   &amp;darr;
Additional SELECT - 함수 실습
Q10 / Q12 / Q13 / Q14 / Q15
   &amp;darr;
정규화
1NF / 2NF / 3NF
   &amp;darr;
Mapping Rule
ERD &amp;rarr; Relational Schema
   &amp;darr;
JOIN
Cartesian Product
INNER JOIN
USING / ON
Non-Equi Join
LEFT JOIN
Self Join
EXISTS
   &amp;darr;
Additional SELECT - Option
Q1 ~ Q14 과제&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COUNT()&lt;/code&gt;&lt;code&gt;SUM()&lt;/code&gt;&lt;code&gt;AVG()&lt;/code&gt;&lt;code&gt;MAX()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CASE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WITH ROLLUP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Window Function&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OVER()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PARTITION BY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Normalization&lt;/li&gt;
&lt;li&gt;1NF / 2NF / 3NF&lt;/li&gt;
&lt;li&gt;Mapping Rule&lt;/li&gt;
&lt;li&gt;PK / FK&lt;/li&gt;
&lt;li&gt;JOIN&lt;/li&gt;
&lt;li&gt;Cartesian Product&lt;/li&gt;
&lt;li&gt;INNER JOIN&lt;/li&gt;
&lt;li&gt;LEFT JOIN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USING&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Non-Equi Join&lt;/li&gt;
&lt;li&gt;Self Join&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXISTS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. GROUP BY&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 SQL Day03의 첫 번째 주제는 &lt;code&gt;GROUP BY&lt;/code&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GROUP BY&lt;/code&gt;는 특정 컬럼에서 &lt;b&gt;동일한 값을 가지는 행들을 하나의 그룹으로 묶어서 통계 또는 집계 처리&lt;/b&gt;할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;여러 개의 Row
      &amp;darr;
같은 값을 기준으로 묶기
      &amp;darr;
GROUP BY
      &amp;darr;
COUNT / SUM / AVG / MIN / MAX
      &amp;darr;
그룹별 집계 결과&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GROUP BY column
GROUP BY expr
GROUP BY position&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 부서별 최고 급여를 구한다면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       MAX(SALARY)
FROM employee
GROUP BY DEPT_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;DEPT_ID
   &amp;darr;
같은 부서끼리 그룹화
   &amp;darr;
MAX(SALARY)
   &amp;darr;
부서별 최고 급여&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DEPT_ID&lt;/code&gt;가 &lt;code&gt;NULL&lt;/code&gt;인 사원들도 하나의 &lt;code&gt;NULL&lt;/code&gt; 그룹으로 묶여 집계되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. WHERE와 HAVING&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요했던 개념 중 하나는 &lt;code&gt;WHERE&lt;/code&gt;와 &lt;code&gt;HAVING&lt;/code&gt;의 차이였다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       SUM(SALARY)
FROM employee
WHERE SALARY &amp;gt;= 3000000
GROUP BY DEPT_ID
HAVING SUM(SALARY) &amp;gt;= 4000000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;employee
    &amp;darr;
WHERE SALARY &amp;gt;= 3000000
    &amp;darr;
급여 300만원 이상인 개별 사원만 필터링
    &amp;darr;
GROUP BY DEPT_ID
    &amp;darr;
부서별 그룹 생성
    &amp;darr;
SUM(SALARY)
    &amp;darr;
부서별 급여 합계
    &amp;darr;
HAVING SUM(SALARY) &amp;gt;= 4000000
    &amp;darr;
집계 결과가 조건을 만족하는 부서만 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;DEPT_ID | SUM(SALARY)
60      | 7,300,000
90      | 18,100,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;적용 시점&lt;/th&gt;
&lt;th&gt;조건 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY&lt;/code&gt; 이전&lt;/td&gt;
&lt;td&gt;개별 Row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY&lt;/code&gt; 이후&lt;/td&gt;
&lt;td&gt;Group / 집계 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; &quot;어떤 행을 그룹화할 것인가?&quot;

HAVING
&amp;rarr; &quot;만들어진 그룹 중 어떤 그룹을 남길 것인가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. CASE + GROUP BY&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CASE&lt;/code&gt;로 새로운 분류값을 만든 뒤 해당 값을 기준으로 그룹화할 수도 있었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CASE
           WHEN SALARY &amp;lt;= 3000000 THEN '초급'
           WHEN SALARY &amp;lt;= 4000000 THEN '중급'
           ELSE '고급'
       END AS GRADE,
       COUNT(*) AS COUNT
FROM employee
GROUP BY GRADE
ORDER BY COUNT DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;초급 &amp;rarr; 15명
중급 &amp;rarr; 5명
고급 &amp;rarr; 2명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SALARY
   &amp;darr;
CASE
   &amp;darr;
초급 / 중급 / 고급
   &amp;darr;
GROUP BY GRADE
   &amp;darr;
COUNT(*)
   &amp;darr;
등급별 인원수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GRADE&lt;/code&gt;는 실제 테이블에 존재하는 컬럼이 아니라 &lt;code&gt;CASE&lt;/code&gt;로 만들어진 &lt;b&gt;파생 값&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서는 이렇게 가공한 결과를 다시 그룹화에 활용할 수 있다는 점이 인상적이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. WITH ROLLUP&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WITH ROLLUP&lt;/code&gt;은 &lt;code&gt;GROUP BY&lt;/code&gt; 결과에 &lt;b&gt;소계와 전체 합계&lt;/b&gt;를 함께 추가한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT GROUPNAME,
       NUM,
       SUM(PRICE * AMOUNT)
FROM buyTbl
GROUP BY GROUPNAME, NUM WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;GROUPNAME + NUM
&amp;rarr; 상세 집계

GROUPNAME + NULL
&amp;rarr; 해당 GROUPNAME의 소계

NULL + NULL
&amp;rarr; 전체 합계&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;의류 | 5    | 150
의류 | 9    | 50
의류 | NULL | 200&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;의류 | NULL | 200&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 &lt;code&gt;ROLLUP&lt;/code&gt;이 추가한 의류 그룹의 소계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 원본 데이터에도 &lt;code&gt;NULL&lt;/code&gt;이 존재할 수 있기 때문에 &lt;b&gt;실제 NULL과 ROLLUP이 생성한 NULL의 의미를 구분하여 해석해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Window Function&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GROUP BY&lt;/code&gt; 이후에는 Window Function, 즉 분석 함수를 학습했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       SALARY,
       DEPT_ID,
       AVG(SALARY) OVER(PARTITION BY DEPT_ID) AS 'DAVG'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;기존 행을 유지하면서 집계 결과를 추가한다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;employee 각 사원
      &amp;darr;
PARTITION BY DEPT_ID
      &amp;darr;
부서별 논리적 파티션 생성
      &amp;darr;
AVG(SALARY)
      &amp;darr;
부서 평균 계산
      &amp;darr;
기존 사원 행 옆에 평균값 추가&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. GROUP BY vs Window Function&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식의 차이를 비교하면 이해하기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;사원 A ┐
사원 B ├─ 부서 10
사원 C ┘
   &amp;darr;
GROUP BY
   &amp;darr;
부서10 | 평균급여&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 행이 &lt;b&gt;그룹별 한 행으로 축약&lt;/b&gt;된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Window Function&lt;/h2&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;사원 A | 부서10 | 급여 | 부서평균
사원 B | 부서10 | 급여 | 부서평균
사원 C | 부서10 | 급여 | 부서평균&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 행은 그대로 유지되고 분석 결과만 추가된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP BY
&amp;rarr; 결과 행 수 감소 가능

Window Function
&amp;rarr; 기존 행 유지
&amp;rarr; 분석 컬럼 추가&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. RANK()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급여를 기준으로 전체 사원 순위를 매겼다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       SALARY,
       RANK() OVER(ORDER BY SALARY DESC) AS 'RANK'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;RANK()&lt;/code&gt;의 중요한 특징은 동점 처리이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2,600,000 &amp;rarr; 9위
2,600,000 &amp;rarr; 9위
2,500,000 &amp;rarr; 11위&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;RANK()
&amp;rarr; 1, 2, 2, 4

DENSE_RANK()
&amp;rarr; 1, 2, 2, 3

ROW_NUMBER()
&amp;rarr; 1, 2, 3, 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;RANK()&lt;/code&gt;는 동점자에게 같은 순위를 부여하고 다음 순위를 건너뛴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. PARTITION BY를 이용한 부서별 순위&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서별 급여 순위를 검색한다면?
부서마다 따로 순위를 정하고 싶다면?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       DEPT_ID,
       SALARY,
       RANK() OVER(
           PARTITION BY DEPT_ID
           ORDER BY SALARY DESC
       ) AS 'RANK'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;PARTITION BY DEPT_ID
&amp;rarr; 부서마다 순위 계산 영역 분리

ORDER BY SALARY DESC
&amp;rarr; 부서 내부에서 급여 내림차순 정렬

RANK()
&amp;rarr; 순위 부여&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 부서가 변경되면 다시 &lt;code&gt;1위&lt;/code&gt;부터 시작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 실습 문제 풀이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 개념을 배운 뒤 강사님이 바로 문제를 제시하고 SQL로 구현하는 실습이 많이 진행됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 1. 부서별 급여 총액 조건 검색&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;부서별 급여 총액이
9,000,000 이상인 부서만 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음 시도&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       SUM(SALARY)
FROM employee
WHERE SUM(SALARY) &amp;gt;= 9000000
GROUP BY DEPT_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SQL 오류 (1111)
Invalid use of group function&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JmicW/dJMcahS3UTQ/Ki0XSTpCQf54k0BdHm2zAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JmicW/dJMcahS3UTQ/Ki0XSTpCQf54k0BdHm2zAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JmicW/dJMcahS3UTQ/Ki0XSTpCQf54k0BdHm2zAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJmicW%2FdJMcahS3UTQ%2FKi0XSTpCQf54k0BdHm2zAk%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;398&quot; height=&quot;125&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 풀이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       SUM(SALARY)
FROM employee
GROUP BY DEPT_ID
HAVING SUM(SALARY) &amp;gt;= 9000000;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmE52l/dJMcacEpQMF/pcODiXyhknF8sF5xwXmBJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmE52l/dJMcacEpQMF/pcODiXyhknF8sF5xwXmBJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmE52l/dJMcacEpQMF/pcODiXyhknF8sF5xwXmBJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmE52l%2FdJMcacEpQMF%2FpcODiXyhknF8sF5xwXmBJ1%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;693&quot; height=&quot;186&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SUM(SALARY)
&amp;rarr; GROUP BY 이후 만들어지는 값

따라서
WHERE ❌
HAVING ⭕&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 통해 &lt;code&gt;WHERE&lt;/code&gt;와 &lt;code&gt;HAVING&lt;/code&gt;의 차이를 가장 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 2. 사용자별 총 구매액&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자별 구매총액을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT USERID,
       SUM(PRICE * AMOUNT)
FROM buyTbl
GROUP BY USERID;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;BBK &amp;rarr; 1,920
EJW &amp;rarr; 95
JYP &amp;rarr; 200
KBS &amp;rarr; 1,210
SSK &amp;rarr; 75&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/datHGl/dJMcagGN4jP/FGP03gFF1Et9Vtd0SdPqs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/datHGl/dJMcagGN4jP/FGP03gFF1Et9Vtd0SdPqs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/datHGl/dJMcagGN4jP/FGP03gFF1Et9Vtd0SdPqs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdatHGl%2FdJMcagGN4jP%2FFGP03gFF1Et9Vtd0SdPqs1%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;439&quot; height=&quot;252&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이 흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PRICE * AMOUNT
&amp;rarr; 한 구매 건의 비용

GROUP BY USERID
&amp;rarr; 같은 사용자끼리 그룹

SUM(...)
&amp;rarr; 사용자별 전체 구매액&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 3. 사용자별 평균 구매 개수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자별 평균 구매 개수를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT USERID,
       ROUND(AVG(AMOUNT))
FROM buyTbl
GROUP BY USERID;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;BBK &amp;rarr; 5
EJW &amp;rarr; 1
JYP &amp;rarr; 1
KBS &amp;rarr; 2
SSK &amp;rarr; 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPMCVQ/dJMcahMl5DU/Dcoe998RNjMKnifqRpUchk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPMCVQ/dJMcahMl5DU/Dcoe998RNjMKnifqRpUchk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPMCVQ/dJMcahMl5DU/Dcoe998RNjMKnifqRpUchk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPMCVQ%2FdJMcahMl5DU%2FDcoe998RNjMKnifqRpUchk%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;484&quot; height=&quot;257&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;AVG()
&amp;rarr; 그룹별 평균

ROUND()
&amp;rarr; 평균 결과 반올림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복수행 함수의 결과를 다시 단일행 함수로 처리한 실습이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 4. 특정 부서 + 부서 미배정 사원 조회&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;부서번호가 50번이거나
부서가 없는 사원의 이름, 주민번호, 급여를 출력하되
급여가 많은 사원부터 조회한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       EMP_NO,
       SALARY
FROM employee
WHERE DEPT_ID = '50'
   OR DEPT_ID IS NULL
ORDER BY SALARY DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DEPT_ID = '50'
OR
DEPT_ID IS NULL

&amp;rarr; 두 조건 중 하나를 만족하는 사원

ORDER BY SALARY DESC
&amp;rarr; 급여 높은 순&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 8명의 사원이 조회됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnQTSQ/dJMcadwkrdW/xHbokqxwgANkvv7CkC24bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnQTSQ/dJMcadwkrdW/xHbokqxwgANkvv7CkC24bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnQTSQ/dJMcadwkrdW/xHbokqxwgANkvv7CkC24bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnQTSQ%2FdJMcadwkrdW%2FxHbokqxwgANkvv7CkC24bK%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;604&quot; height=&quot;337&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 5. 성별 평균 급여&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;위 결과를 바탕으로
성별에 따른 평균 급여를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CASE
           WHEN SUBSTRING(EMP_NO, 8, 1) IN ('1', '3') THEN '남자'
           WHEN SUBSTRING(EMP_NO, 8, 1) IN ('2', '4') THEN '여자'
       END AS GENDER,
       AVG(SALARY)
FROM employee
WHERE DEPT_ID = '50'
   OR DEPT_ID IS NULL
GROUP BY GENDER
ORDER BY 2 DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;남자 &amp;rarr; 2,400,000
여자 &amp;rarr; 2,080,000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sSUm2/dJMcafVo5un/yLcL7huNqSHUoK2nWcSQk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sSUm2/dJMcafVo5un/yLcL7huNqSHUoK2nWcSQk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sSUm2/dJMcafVo5un/yLcL7huNqSHUoK2nWcSQk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsSUm2%2FdJMcafVo5un%2FyLcL7huNqSHUoK2nWcSQk1%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;817&quot; height=&quot;311&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이 흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; 대상 사원 선택

SUBSTRING()
&amp;rarr; 주민번호 뒷자리 첫 숫자 추출

CASE
&amp;rarr; 남자 / 여자 분류

GROUP BY
&amp;rarr; 성별 그룹화

AVG()
&amp;rarr; 성별 평균 급여&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 6. 사용자별 구매액이 일정 금액 이상인 사용자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용자별 총 구매액이
100 이상인 사용자만 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT USERID AS '아이디',
       SUM(PRICE * AMOUNT) AS '총 구매액'
FROM buyTbl
GROUP BY USERID
HAVING SUM(PRICE * AMOUNT) &amp;gt;= 100
ORDER BY 2 DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;BBK &amp;rarr; 1,920
KBS &amp;rarr; 1,210
JYP &amp;rarr; 200&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DBKdg/dJMcafVo5wO/g1SroTqm1ZvDJsncOMBwX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DBKdg/dJMcafVo5wO/g1SroTqm1ZvDJsncOMBwX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DBKdg/dJMcafVo5wO/g1SroTqm1ZvDJsncOMBwX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDBKdg%2FdJMcafVo5wO%2Fg1SroTqm1ZvDJsncOMBwX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;263&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;GROUP BY
&amp;rarr; 사용자별 그룹 생성

SUM
&amp;rarr; 총 구매액 계산

HAVING
&amp;rarr; 계산된 총 구매액 조건

ORDER BY
&amp;rarr; 집계 결과 정렬&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 7. 그룹별 구매비용 + ROLLUP&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;구매한 목록 중
그룹이름별 구매비용을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GROUP BY&lt;/code&gt;에서 더 나아가 &lt;code&gt;WITH ROLLUP&lt;/code&gt;까지 실습했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT GROUPNAME,
       NUM,
       SUM(PRICE * AMOUNT)
FROM buyTbl
GROUP BY GROUPNAME, NUM WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyTcW/dJMcaiLetI3/voKkS5jr4AP0cG0ioxJAbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyTcW/dJMcaiLetI3/voKkS5jr4AP0cG0ioxJAbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyTcW/dJMcaiLetI3/voKkS5jr4AP0cG0ioxJAbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtyTcW%2FdJMcaiLetI3%2FvoKkS5jr4AP0cG0ioxJAbk%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;671&quot; height=&quot;454&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;상품별 구매비용
      &amp;darr;
GROUPNAME별 집계
      &amp;darr;
WITH ROLLUP
      &amp;darr;
상세
+ 그룹별 소계
+ 전체 합계&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 8. Window Function으로 부서 평균 표시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;각 사원의 정보는 유지하면서
부서별 평균 급여를 함께 출력한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       SALARY,
       DEPT_ID,
       AVG(SALARY) OVER(
           PARTITION BY DEPT_ID
       ) AS 'DAVG'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/51Za3/dJMcadC4esp/N7cbUIM0zyoeYm6Z1uCKe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/51Za3/dJMcadC4esp/N7cbUIM0zyoeYm6Z1uCKe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/51Za3/dJMcadC4esp/N7cbUIM0zyoeYm6Z1uCKe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F51Za3%2FdJMcadC4esp%2FN7cbUIM0zyoeYm6Z1uCKe0%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;569&quot; height=&quot;443&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;GROUP BY
&amp;rarr; 행 축약

OVER(PARTITION BY ...)
&amp;rarr; 행 유지
&amp;rarr; 부서 평균 컬럼 추가&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 9. 전체 급여 순위&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;급여 순위를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       SALARY,
       RANK() OVER(
           ORDER BY SALARY DESC
       ) AS 'RANK'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQQAkO/dJMb99OgPpn/ED7r7UDwsm6VOXuDwoP891/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQQAkO/dJMb99OgPpn/ED7r7UDwsm6VOXuDwoP891/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQQAkO/dJMb99OgPpn/ED7r7UDwsm6VOXuDwoP891/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQQAkO%2FdJMb99OgPpn%2FED7r7UDwsm6VOXuDwoP891%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;556&quot; height=&quot;374&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;RANK()&lt;/code&gt;는 동점자는 같은 순위를 가지고 다음 순위는 건너뛴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 10. 부서별 급여 순위&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서별 급여 순위를 검색한다면?
부서마다 따로 순위를 정하고 싶다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       DEPT_ID,
       SALARY,
       RANK() OVER(
           PARTITION BY DEPT_ID
           ORDER BY SALARY DESC
       ) AS 'RANK'
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yxR5s/dJMcadC4et6/eIVr2PGB1jxPSvNPHzZhq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yxR5s/dJMcadC4et6/eIVr2PGB1jxPSvNPHzZhq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yxR5s/dJMcadC4et6/eIVr2PGB1jxPSvNPHzZhq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyxR5s%2FdJMcadC4et6%2FeIVr2PGB1jxPSvNPHzZhq1%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;703&quot; height=&quot;389&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;PARTITION BY
&amp;rarr; 순위를 다시 시작할 범위

ORDER BY
&amp;rarr; 해당 범위 안의 순위 기준&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Additional SELECT - 함수 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전날 스킵했던 문제:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;7 / 10 / 12 / 13 / 14 / 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중 &lt;code&gt;7번&lt;/code&gt;을 제외한 문제를 오늘 구현했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q10. 학과별 학생 수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학과별 학생 수를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPARTMENT_NO AS '학과번호',
       COUNT(STUDENT_NO) AS '학생수(명)'
FROM TB_STUDENT
GROUP BY DEPARTMENT_NO;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과 일부&lt;/h3&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;001 &amp;rarr; 14
002 &amp;rarr; 3
003 &amp;rarr; 8
004 &amp;rarr; 12
005 &amp;rarr; 10
006 &amp;rarr; 5
007 &amp;rarr; 8
008 &amp;rarr; 9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ag6h6/dJMcadwkrjo/AbdrlLbmyQv2rmfAKdUB4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ag6h6/dJMcadwkrjo/AbdrlLbmyQv2rmfAKdUB4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ag6h6/dJMcadwkrjo/AbdrlLbmyQv2rmfAKdUB4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAg6h6%2FdJMcadwkrjo%2FAbdrlLbmyQv2rmfAKdUB4K%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;535&quot; height=&quot;314&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근 과정&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;학생 수를 학과별로 알고 싶다.
      &amp;darr;
학과번호가 같은 학생끼리 그룹화
      &amp;darr;
GROUP BY DEPARTMENT_NO
      &amp;darr;
COUNT(STUDENT_NO)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q12. 학생의 년도별 평점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;A112113 학생의 년도별 평점을 구한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이 1&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT SUBSTRING(TERM_NO, 1, 4) AS '년도',
       ROUND(AVG(POINT), 1) AS '년도 별 평점'
FROM tb_grade
WHERE STUDENT_NO = 'A112113'
GROUP BY 1
ORDER BY 1;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;2001 &amp;rarr; 2.8
2002 &amp;rarr; 2.2
2003 &amp;rarr; 4.0
2004 &amp;rarr; 3.5&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 풀이&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT LEFT(TERM_NO, 4) AS '년도',
       ROUND(AVG(POINT), 1) AS '년도 별 평점'
FROM tb_grade
WHERE STUDENT_NO = 'A112113'
GROUP BY LEFT(TERM_NO, 4);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUoWbw/dJMcabSUIsG/XjMKJvrveHwaKJ7dBHWxZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUoWbw/dJMcabSUIsG/XjMKJvrveHwaKJ7dBHWxZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUoWbw/dJMcabSUIsG/XjMKJvrveHwaKJ7dBHWxZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUoWbw%2FdJMcabSUIsG%2FXjMKJvrveHwaKJ7dBHWxZ0%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;800&quot; height=&quot;469&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 해결 포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;TERM_NO&lt;/code&gt;가 학기까지 포함하고 있기 때문에 그대로 그룹화하면 년도별 평균을 구할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;TERM_NO
&amp;rarr; 200101
&amp;rarr; 200102
&amp;rarr; 200201
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SUBSTRING 또는 LEFT
&amp;rarr; 앞 4자리만 추출
&amp;rarr; 2001 / 2002 / 2003 / 2004
&amp;rarr; 년도별 GROUP BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 해결했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q13. 학과별 휴학생 수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;학과별 휴학생 수를 검색한다면?
휴학생이 0명인 학과도 표시한다.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT b.DEPARTMENT_NO,
       COUNT(a.STUDENT_NO) AS 휴학생수
FROM tb_department b
LEFT JOIN tb_student a
       ON b.DEPARTMENT_NO = a.DEPARTMENT_NO
      AND a.ABSENCE_YN = 'Y'
GROUP BY b.DEPARTMENT_NO;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과 일부&lt;/h3&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;001 &amp;rarr; 2
002 &amp;rarr; 0
003 &amp;rarr; 1
004 &amp;rarr; 1
005 &amp;rarr; 0
006 &amp;rarr; 3
007 &amp;rarr; 1
008 &amp;rarr; 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Oo6s/dJMcabSUIuQ/jh9iBN3CuyGanYJx6cRJR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Oo6s/dJMcabSUIuQ/jh9iBN3CuyGanYJx6cRJR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Oo6s/dJMcabSUIuQ/jh9iBN3CuyGanYJx6cRJR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Oo6s%2FdJMcabSUIuQ%2Fjh9iBN3CuyGanYJx6cRJR0%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;626&quot; height=&quot;410&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE ABSENCE_YN = 'Y'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 사용해서 학생 테이블을 그룹화하면 휴학생이 없는 학과는 애초에 결과에 나타나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기준을 &lt;code&gt;tb_department&lt;/code&gt;로 잡았다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;모든 학과
      &amp;darr;
LEFT JOIN
      &amp;darr;
휴학생이 있으면 매칭
휴학생이 없어도 학과는 유지
      &amp;darr;
COUNT(a.STUDENT_NO)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 조건을 &lt;code&gt;WHERE&lt;/code&gt;가 아니라 &lt;code&gt;ON&lt;/code&gt;에 작성했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;AND a.ABSENCE_YN = 'Y'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 LEFT JOIN으로 유지해야 하는 학과가 사라지는 것을 방지했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q14. 동명이인 학생 찾기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학생들 중 동명이인의 이름과
동명인 수를 출력한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;SELECT STUDENT_NAME AS '동명이인',
       COUNT(*) AS '동명인 수'
FROM tb_student
GROUP BY 1
HAVING COUNT(*) &amp;gt; 1
ORDER BY 1;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과 일부&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;김경민 &amp;rarr; 2
김명철 &amp;rarr; 2
김영호 &amp;rarr; 2
김보람 &amp;rarr; 3
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIBCf2/dJMcadwkrkk/ku8b1a5BsslAIgKt6BzUJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIBCf2/dJMcadwkrkk/ku8b1a5BsslAIgKt6BzUJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIBCf2/dJMcadwkrkk/ku8b1a5BsslAIgKt6BzUJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIBCf2%2FdJMcadwkrkk%2Fku8b1a5BsslAIgKt6BzUJ0%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;554&quot; height=&quot;441&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 해결 포인트&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;같은 이름끼리 그룹화
      &amp;darr;
COUNT(*)
      &amp;darr;
같은 이름이 몇 명인지 계산
      &amp;darr;
2명 이상만 필요
      &amp;darr;
HAVING COUNT(*) &amp;gt; 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;COUNT(*) &amp;gt; 1&lt;/code&gt;은 그룹화 후 알 수 있는 결과이므로 &lt;code&gt;WHERE&lt;/code&gt;가 아니라 &lt;code&gt;HAVING&lt;/code&gt;을 사용해야 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q15. 년도&amp;middot;학기별 평점 + 년도별 평점 + 총평점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;A112113 학생의

년도별
학기별
평점
년도별 누적 평점
총평점

을 출력한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT SUBSTRING(TERM_NO, 1, 4) AS '년도',
       SUBSTRING(TERM_NO, 5, 2) AS '학기',
       ROUND(AVG(POINT), 1) AS '평점'
FROM tb_grade
WHERE STUDENT_NO = 'A112113'
GROUP BY SUBSTRING(TERM_NO, 1, 4),
         SUBSTRING(TERM_NO, 5, 2)
WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과 구조&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;2003 | 01   | 3.5
2003 | 02   | 4.5
2003 | 03   | 4.0
2003 | NULL | 4.0  &amp;larr; 2003년 평균

2004 | 01   | 4.0
2004 | 02   | 3.0
2004 | NULL | 3.5  &amp;larr; 2004년 평균

NULL | NULL | 3.2  &amp;larr; 전체 평점&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba4Qa3/dJMcaas08s3/fICKuh4ogYo8yYMIkStciK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba4Qa3/dJMcaas08s3/fICKuh4ogYo8yYMIkStciK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba4Qa3/dJMcaas08s3/fICKuh4ogYo8yYMIkStciK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba4Qa3%2FdJMcaas08s3%2FfICKuh4ogYo8yYMIkStciK%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;759&quot; height=&quot;437&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 해결 포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 &lt;code&gt;GROUP BY&lt;/code&gt;만으로는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;년도 + 학기&lt;/code&gt;&lt;/pre&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;pre class=&quot;markdown&quot;&gt;&lt;code&gt;학기별 평균
+
년도별 평균
+
전체 평균&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모두 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;WITH ROLLUP&lt;/code&gt;을 이용했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;년도 + 학기
&amp;rarr; 세부 평균

년도 + NULL
&amp;rarr; 년도별 평균

NULL + NULL
&amp;rarr; 전체 평균&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. 정규화(Normalization)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 집계 실습 이후에는 데이터베이스 설계 개념인 정규화를 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화의 목적은:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;데이터 중복 방지
      &amp;darr;
효율적인 데이터 저장
      &amp;darr;
불필요한 NULL 감소
      &amp;darr;
각 속성을 적절한 테이블에 배치
      &amp;darr;
논리적으로 작은 단위의 테이블로 분리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 제1정규화(1NF)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제1정규화의 핵심은 &lt;b&gt;반복되는 속성 또는 반복 그룹을 제거하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;과정코드
과정명
교재1
교재2
교재3
교재4
교재5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 &lt;code&gt;교재&lt;/code&gt;라는 동일한 의미의 컬럼이 반복된다면 별도의 테이블로 분리한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;과정
- 과정코드 PK
- 과정명
- 교육내용
- 교육기간

        1 : N

교재
- 교재번호 PK
- 교재명
- 과정코드 FK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원과 자격증 관계 역시 같은 예였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;회원
회원번호 PK
   │
   │ 1
   │
   └──── N
        자격증
        자격증ID PK
        회원번호 FK&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 제2정규화(2NF)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제2정규화는 &lt;b&gt;복합키의 일부에만 의존하는 부분 함수 종속을 제거하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;동아리등록

PK = 동아리코드 + 학번

등록일자
등록사유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 구조에서는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;동아리코드 + 학번
        &amp;darr;
등록일자
등록사유&lt;/code&gt;&lt;/pre&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;pre class=&quot;&quot;&gt;&lt;code&gt;학번 &amp;rarr; 전화번호
학번 &amp;rarr; 주소
학번 &amp;rarr; 우편번호&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 복합키 일부에만 의존하는 속성이 있다면 별도 테이블로 분리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 제3정규화(3NF)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제3정규화는 &lt;b&gt;키가 아닌 컬럼이 다른 키가 아닌 컬럼에 의존하는 이행적 종속을 제거&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;사원번호(PK)
      &amp;darr;
우편번호
      &amp;darr;
시도명
구군명
읍면동명
나머지 번지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사원번호
&amp;rarr; 우편번호
&amp;rarr; 주소정보&lt;/code&gt;&lt;/pre&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;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;사원
- 사원번호 PK
- 이름
- 주민번호
- 우편번호 FK
- 입사일

주소
- 우편번호 PK
- 시도명
- 구군명
- 읍면동명
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 구조가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. 1NF / 2NF / 3NF 한 번에 정리&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1NF
반복되는 속성이 있는가?
&amp;rarr; 반복 속성 제거

2NF
복합키 일부에만 의존하는 속성이 있는가?
&amp;rarr; 부분 함수 종속 제거

3NF
비키 컬럼이 다른 비키 컬럼을 결정하는가?
&amp;rarr; 이행적 종속 제거&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. Mapping Rule&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ER Diagram에서 설계한 내용을 실제 관계형 데이터베이스 구조로 변환하는 과정을 &lt;code&gt;Mapping Rule&lt;/code&gt;이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Conceptual Model
      &amp;darr;
ER Diagram
      &amp;darr;
Mapping Rule
      &amp;darr;
Relational Schema&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대응 관계:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ER Model&lt;/th&gt;
&lt;th&gt;Relational Database&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entity&lt;/td&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attribute&lt;/td&gt;
&lt;td&gt;Column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identifier&lt;/td&gt;
&lt;td&gt;Primary Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationship&lt;/td&gt;
&lt;td&gt;Foreign Key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Entity
&amp;rarr; Table

Attribute
&amp;rarr; Column

Identifier
&amp;rarr; PK

Relationship
&amp;rarr; FK&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. JOIN&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화를 통해 여러 테이블로 나누어진 데이터를 다시 하나의 결과로 조회하기 위해 &lt;code&gt;JOIN&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;정규화
&amp;rarr; 테이블 분리

PK / FK
&amp;rarr; 테이블 간 관계 표현

JOIN
&amp;rarr; 관계를 이용하여 필요한 데이터를 결합&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스에서는:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;1 : 1
1 : N
N : M&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 관계를 기준으로 데이터를 연결한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  JOIN 실습 문제&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 11. JOIN 조건이 없으면 어떻게 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 다음 SQL을 실행했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME
FROM employee E,
     department D;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;154행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 154행인지 계산하면:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;employee 22행
&amp;times;
department 7행
=
154행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 모든 사원과 모든 부서가 한 번씩 조합됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;code&gt;Cartesian Product&lt;/code&gt;라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbB28b/dJMcacEpRvm/ApxksdjvFM3YdtHS4xDPKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbB28b/dJMcacEpRvm/ApxksdjvFM3YdtHS4xDPKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbB28b/dJMcacEpRvm/ApxksdjvFM3YdtHS4xDPKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbB28b%2FdJMcacEpRvm%2FApxksdjvFM3YdtHS4xDPKK%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;741&quot; height=&quot;691&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 12. JOIN 조건 추가&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME
FROM employee E,
     department D
WHERE E.DEPT_ID = D.DEPT_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인 조건을 추가하자 결과가:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;154행
&amp;darr;
20행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 줄었다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;E.DEPT_ID
=
D.DEPT_ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인 실제 관계가 존재하는 데이터만 남았기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xNjUj/dJMcafA3quq/p8almEAEvfaU6L6K3cBw31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xNjUj/dJMcafA3quq/p8almEAEvfaU6L6K3cBw31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xNjUj/dJMcafA3quq/p8almEAEvfaU6L6K3cBw31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxNjUj%2FdJMcafA3quq%2Fp8almEAEvfaU6L6K3cBw31%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;581&quot; height=&quot;476&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 13. USING()으로 여러 테이블 JOIN&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       JOB_TITLE,
       DEPT_NAME
FROM job J
JOIN employee E USING(JOB_ID)
JOIN department D USING(DEPT_ID);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JOB
 &amp;darr; JOB_ID
EMPLOYEE
 &amp;darr; DEPT_ID
DEPARTMENT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블의 조인 컬럼 이름이 동일할 때 &lt;code&gt;USING()&lt;/code&gt;을 사용하면 SQL을 더 간결하게 작성할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbkP0f/dJMb991VVdC/uV6jlBSAJBII3yyibZV7M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbkP0f/dJMb991VVdC/uV6jlBSAJBII3yyibZV7M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbkP0f/dJMb991VVdC/uV6jlBSAJBII3yyibZV7M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbkP0f%2FdJMb991VVdC%2FuV6jlBSAJBII3yyibZV7M0%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;442&quot; height=&quot;330&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 14. ON으로 4개 테이블 JOIN&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       D.DEPT_NAME,
       L.LOC_DESCRIBE
FROM job J
JOIN employee E
  ON J.JOB_ID = E.JOB_ID
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
JOIN location L
  ON L.LOCATION_ID = D.LOC_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계:&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;JOB
 &amp;darr;
EMPLOYEE
 &amp;darr;
DEPARTMENT
 &amp;darr;
LOCATION&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡해 보이지만 실제로는 &lt;b&gt;테이블을 한 개씩 관계를 따라 연결하는 과정&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs4E2X/dJMcaf13eyj/ElbZR7lMSXKyVZ05SLVn20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs4E2X/dJMcaf13eyj/ElbZR7lMSXKyVZ05SLVn20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs4E2X/dJMcaf13eyj/ElbZR7lMSXKyVZ05SLVn20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs4E2X%2FdJMcaf13eyj%2FElbZR7lMSXKyVZ05SLVn20%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;667&quot; height=&quot;494&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 15. 5개 테이블 JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가 정보까지 추가했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       D.DEPT_NAME,
       L.LOC_DESCRIBE,
       C.COUNTRY_NAME
FROM job J
JOIN employee E
  ON J.JOB_ID = E.JOB_ID
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
JOIN location L
  ON L.LOCATION_ID = D.LOC_ID
JOIN country C
  ON L.COUNTRY_ID = C.COUNTRY_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;JOB
 &amp;darr;
EMPLOYEE
 &amp;darr;
DEPARTMENT
 &amp;darr;
LOCATION
 &amp;darr;
COUNTRY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 사원의:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이름
직급
부서
지역
국가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보를 하나의 결과로 조회할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmUtMs/dJMcaf13eyR/AezWd3pAdZBD0yucHVWCdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmUtMs/dJMcaf13eyR/AezWd3pAdZBD0yucHVWCdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmUtMs/dJMcaf13eyR/AezWd3pAdZBD0yucHVWCdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmUtMs%2FdJMcaf13eyR%2FAezWd3pAdZBD0yucHVWCdk%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;744&quot; height=&quot;499&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 16. Non-Equi Join&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;code&gt;SAL_GRADE&lt;/code&gt; 테이블까지 추가했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       J.JOB_TITLE,
       D.DEPT_NAME,
       L.LOC_DESCRIBE,
       C.COUNTRY_NAME,
       S.SLEVEL
FROM job J
JOIN employee E
  ON J.JOB_ID = E.JOB_ID
JOIN department D
  ON E.DEPT_ID = D.DEPT_ID
JOIN location L
  ON L.LOCATION_ID = D.LOC_ID
JOIN country C
  ON L.COUNTRY_ID = C.COUNTRY_ID
JOIN sal_grade S
  ON E.SALARY BETWEEN S.LOWEST AND S.HIGHEST
WHERE DEPT_NAME LIKE '해외%'
  AND LOC_DESCRIBE LIKE '아시아%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JOIN은 대부분:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;A.ID = B.ID&lt;/code&gt;&lt;/pre&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;pre class=&quot;css&quot;&gt;&lt;code&gt;E.SALARY BETWEEN S.LOWEST AND S.HIGHEST&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 값이 특정 범위 안에 있는지 확인해야 했다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;Equi Join
&amp;rarr; =

Non-Equi Join
&amp;rarr; BETWEEN / &amp;gt; / &amp;lt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVmedR/dJMcabyANIU/oAttLZfuUmkL8nxHdtXCkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVmedR/dJMcabyANIU/oAttLZfuUmkL8nxHdtXCkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVmedR/dJMcabyANIU/oAttLZfuUmkL8nxHdtXCkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVmedR%2FdJMcabyANIU%2FoAttLZfuUmkL8nxHdtXCkK%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;908&quot; height=&quot;600&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 17. JYP 사용자의 구매상품&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;사용자 아이디가 JYP인 유저의
이름과 구매상품을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT U.NAME,
       B.PRODNAME
FROM buyTbl B
JOIN userTbl U
  ON B.USERID = U.USERID
WHERE U.USERID = 'JYP';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;조용필 | 모니터&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YaMTW/dJMcag0VQV5/ZXikvXO5OVpLFDevmz6ZSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YaMTW/dJMcag0VQV5/ZXikvXO5OVpLFDevmz6ZSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YaMTW/dJMcag0VQV5/ZXikvXO5OVpLFDevmz6ZSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYaMTW%2FdJMcag0VQV5%2FZXikvXO5OVpLFDevmz6ZSk%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;827&quot; height=&quot;305&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 18. 회원과 구매정보 JOIN&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자의 아이디,
이름,
구매상품,
연락처를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT U.USERID,
       U.NAME,
       B.PRODNAME,
       CONCAT(U.MOBILE1, U.MOBILE2) AS '연락처'
FROM userTbl U
JOIN buyTbl B USING(USERID);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;USING(USERID)
&amp;rarr; 회원과 구매정보 연결

CONCAT()
&amp;rarr; 연락처 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbwzJ0/dJMcabyANJL/oqpEdnBXe3dK6jF47mGON1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbwzJ0/dJMcabyANJL/oqpEdnBXe3dK6jF47mGON1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbwzJ0/dJMcabyANJL/oqpEdnBXe3dK6jF47mGON1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbwzJ0%2FdJMcabyANJL%2FoqpEdnBXe3dK6jF47mGON1%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;807&quot; height=&quot;345&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 19. 구매이력이 있는 회원만 검색&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;위 요구사항에서
구매이력이 있는 회원만 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT DISTINCT U.USERID,
                U.NAME,
                B.PRODNAME,
                U.ADDR,
                CONCAT(U.MOBILE1, '-', U.MOBILE2)
FROM userTbl U
JOIN buyTbl B
  ON U.USERID = B.USERID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNER JOIN은 두 테이블 모두에 관계가 존재하는 행만 반환하므로 구매이력이 있는 회원만 조회된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DISTINCT&lt;/code&gt;를 사용했지만 &lt;code&gt;PRODNAME&lt;/code&gt;이 SELECT 목록에 포함되어 있기 때문에:&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;KBS | 김범수 | 운동화
KBS | 김범수 | 노트북
KBS | 김범수 | 청바지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 같은 회원이라도 구매상품이 다르면 서로 다른 행으로 유지된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLyWqX/dJMcab6upKS/KfMBRCok7FEwKIJRZbJ3ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLyWqX/dJMcab6upKS/KfMBRCok7FEwKIJRZbJ3ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLyWqX/dJMcab6upKS/KfMBRCok7FEwKIJRZbJ3ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLyWqX%2FdJMcab6upKS%2FKfMBRCok7FEwKIJRZbJ3ak%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;583&quot; height=&quot;436&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 20. EXISTS로 구매이력 확인&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT U.USERID,
       U.NAME,
       U.ADDR,
       CONCAT(U.MOBILE1, '-', U.MOBILE2)
FROM userTbl U
WHERE EXISTS (
    SELECT *
    FROM buyTbl B
    WHERE U.USERID = B.USERID
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;EXISTS&lt;/code&gt;는 값을 가져오는 것이 아니라 &lt;b&gt;조건을 만족하는 행이 존재하는지 여부&lt;/b&gt;만 확인한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;userTbl 회원
      &amp;darr;
buyTbl에 같은 USERID가 있는가?
      &amp;darr;
YES
      &amp;darr;
조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN과 비교하면:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INNER JOIN
&amp;rarr; 구매 건마다 회원 데이터가 반복될 수 있음

EXISTS
&amp;rarr; 구매이력이 존재하는지만 검사
&amp;rarr; 회원 기준 결과 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cmr3G/dJMcafHTb3q/qoOZKfK23DEPhWHgHkej91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cmr3G/dJMcafHTb3q/qoOZKfK23DEPhWHgHkej91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cmr3G/dJMcafHTb3q/qoOZKfK23DEPhWHgHkej91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmr3G%2FdJMcafHTb3q%2FqoOZKfK23DEPhWHgHkej91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;351&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 21. LEFT JOIN&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME
FROM employee E
LEFT JOIN department D
       ON E.DEPT_ID = D.DEPT_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LEFT JOIN은 왼쪽 &lt;code&gt;employee&lt;/code&gt; 테이블의 행을 모두 유지한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;부서 매칭 성공
&amp;rarr; 부서명 출력

부서 매칭 실패
&amp;rarr; DEPT_NAME = NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과에서도 &lt;code&gt;employee&lt;/code&gt; 전체 &lt;code&gt;22행&lt;/code&gt;이 유지됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 22. Self Join&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q.&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사원이름과 사수의 이름을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &lt;code&gt;employee&lt;/code&gt; 테이블을 서로 다른 역할로 다시 JOIN했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       M.EMP_NAME,
       S.EMP_NAME
FROM employee E
LEFT JOIN employee M
       ON E.MGR_ID = M.EMP_ID
LEFT JOIN employee S
       ON M.MGR_ID = S.EMP_ID;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 alias의 역할:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;E
&amp;rarr; 현재 사원

M
&amp;rarr; 현재 사원의 사수

S
&amp;rarr; 사수의 사수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사원 E
 &amp;darr; MGR_ID
사수 M
 &amp;darr; MGR_ID
상위 사수 S&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과 예:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;전우성 &amp;rarr; 한선기 &amp;rarr; NULL
엄정하 &amp;rarr; 전우성 &amp;rarr; 한선기
심하균 &amp;rarr; NULL   &amp;rarr; NULL
박하일 &amp;rarr; NULL   &amp;rarr; NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self Join은 같은 테이블을 여러 번 사용하는 것이 아니라 &lt;b&gt;같은 테이블에 서로 다른 역할을 부여하여 관계를 추적하는 것&lt;/b&gt;이라고 이해하면 쉬웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 문제 해결 과정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업에서는 단순히 정답 SQL만 작성한 것이 아니라, 잘못된 결과나 오류를 확인하고 왜 그런 결과가 발생했는지를 다시 분석하는 과정이 많았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHERE에서 SUM()을 사용했더니 오류 발생&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;WHERE SUM(SALARY) &amp;gt;= 9000000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 실행하자:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SQL 오류 (1111)
Invalid use of group function&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;SQL의 논리적인 처리 흐름을 보면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;FROM
 &amp;darr;
WHERE
 &amp;darr;
GROUP BY
 &amp;darr;
HAVING
 &amp;darr;
SELECT
 &amp;darr;
ORDER BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WHERE&lt;/code&gt; 시점에서는 아직 &lt;code&gt;GROUP BY&lt;/code&gt;와 &lt;code&gt;SUM()&lt;/code&gt;이 수행되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 존재하지 않는 집계 결과를 &lt;code&gt;WHERE&lt;/code&gt;에서 사용하려고 한 것이 원인이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;HAVING SUM(SALARY) &amp;gt;= 9000000&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DEPT_ID,
       SUM(SALARY)
FROM employee
GROUP BY DEPT_ID
HAVING SUM(SALARY) &amp;gt;= 9000000;&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; 원본 Row 조건

HAVING
&amp;rarr; 그룹 함수 결과 조건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 구분하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JOIN했는데 결과가 154개나 나온 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT E.EMP_NAME,
       D.DEPT_NAME
FROM employee E,
     department D;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 실행했더니 예상보다 훨씬 많은 &lt;code&gt;154행&lt;/code&gt;이 조회됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN 조건이 없었다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;employee 22행
department 7행

22 &amp;times; 7
= 154&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 모든 가능한 조합이 생성되는 Cartesian Product가 발생했다.&lt;/p&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;pre class=&quot;fix&quot;&gt;&lt;code&gt;WHERE E.DEPT_ID = D.DEPT_ID&lt;/code&gt;&lt;/pre&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;154행
&amp;darr;
20행&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;JOIN 문제를 풀 때는 SQL을 먼저 작성하기보다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 어떤 테이블을 연결하는가?
2. 두 테이블은 어떤 컬럼으로 관계가 있는가?
3. PK / FK가 무엇인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 먼저 확인해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 휴학생이 0명인 학과가 사라지는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학과별 휴학생 수를 구하는 Q13에서는 단순히 휴학생만 &lt;code&gt;WHERE&lt;/code&gt;로 필터링하면 문제가 생길 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;학과별 휴학생 수 출력
+
휴학생이 0명인 학과도 출력&lt;/code&gt;&lt;/pre&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;pre class=&quot;&quot;&gt;&lt;code&gt;휴학생이 있는 학과
&amp;rarr; 존재

휴학생이 없는 학과
&amp;rarr; 행 자체가 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 되어 &lt;code&gt;0명&lt;/code&gt;이라는 결과를 표현할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 학과가 존재하는 &lt;code&gt;tb_department&lt;/code&gt;를 기준 테이블로 선택했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;FROM tb_department b
LEFT JOIN tb_student a
       ON b.DEPARTMENT_NO = a.DEPARTMENT_NO
      AND a.ABSENCE_YN = 'Y'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;COUNT(a.STUDENT_NO)&lt;/code&gt;&lt;/pre&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;pre class=&quot;basic&quot;&gt;&lt;code&gt;001 &amp;rarr; 2
002 &amp;rarr; 0
003 &amp;rarr; 1
004 &amp;rarr; 1
005 &amp;rarr; 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 휴학생이 없는 학과도 &lt;code&gt;0&lt;/code&gt;으로 표시됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;없는 데이터도 보여야 하는가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 먼저 확인하면 &lt;code&gt;INNER JOIN&lt;/code&gt;과 &lt;code&gt;LEFT JOIN&lt;/code&gt; 중 무엇이 필요한지 판단하기 쉬워진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 년도별 평점을 구하려는데 TERM_NO가 학기까지 포함된 문제&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;code&gt;TERM_NO&lt;/code&gt;는 다음처럼 저장되어 있었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;200101
200102
200201
200202
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;년도별 평균이 필요한데 &lt;code&gt;TERM_NO&lt;/code&gt; 전체를 그룹화하면 학기별 그룹이 만들어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SUBSTRING(TERM_NO, 1, 4)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;LEFT(TERM_NO, 4)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 년도만 추출했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;200101
 &amp;darr;
2001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;GROUP BY 년도&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;DB에 저장된 원본 값이 문제에서 요구하는 단위와 다르다면 &lt;b&gt;문자열 함수 등을 이용해 새로운 그룹 기준을 만들어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. GROUP BY만으로 년도별 소계와 전체 평균을 동시에 구하기 어려웠던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q15에서는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학기별 평점
년도별 평점
전체 평점&lt;/code&gt;&lt;/pre&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;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;GROUP BY 년도, 학기&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WITH ROLLUP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;년도 + 학기
&amp;rarr; 학기별 평균

년도 + NULL
&amp;rarr; 년도별 평균

NULL + NULL
&amp;rarr; 전체 평균&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ROLLUP&lt;/code&gt;은 단순 합계뿐 아니라 &lt;b&gt;계층적으로 그룹을 묶은 결과의 소계와 전체 집계를 함께 표현할 때 유용하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 다중 JOIN이 복잡하게 느껴졌던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블이:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;JOB
EMPLOYEE
DEPARTMENT
LOCATION
COUNTRY
SAL_GRADE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 늘어나자 처음에는 하나의 SQL 안에서 모든 관계를 동시에 생각해야 하는 것처럼 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 관계를 하나씩 보면:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;JOB
 &amp;darr; JOB_ID
EMPLOYEE
 &amp;darr; DEPT_ID
DEPARTMENT
 &amp;darr; LOC_ID
LOCATION
 &amp;darr; COUNTRY_ID
COUNTRY&lt;/code&gt;&lt;/pre&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;pre class=&quot;css&quot;&gt;&lt;code&gt;EMPLOYEE.SALARY
 &amp;darr;
SAL_GRADE.LOWEST ~ HIGHEST&lt;/code&gt;&lt;/pre&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-ke-size=&quot;size16&quot;&gt;다중 JOIN은:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;테이블 6개를 한 번에 JOIN&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 생각하기보다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;앞의 결과
+
다음 테이블 1개
+
다음 테이블 1개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 관계를 한 단계씩 따라가는 것이 훨씬 이해하기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 같은 employee 테이블을 다시 JOIN해야 했던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self Join 문제에서는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사원
사수
사수의 사수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 모두 &lt;code&gt;employee&lt;/code&gt; 테이블에 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 같은 테이블을 여러 번 JOIN하는 것이 어색했지만 alias를 역할로 생각하니 쉬워졌다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;E = Employee
M = Manager
S = Superior&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;E.MGR_ID = M.EMP_ID
M.MGR_ID = S.EMP_ID&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self Join은:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;같은 테이블을 복사한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;동일한 테이블의 데이터를
서로 다른 역할로 해석한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업은 &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, Window Function, 정규화, JOIN까지 범위가 상당히 넓었다.&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;오전에는:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP BY
HAVING
ROLLUP
Window Function&lt;/code&gt;&lt;/pre&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;특히:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; Row를 필터링

HAVING
&amp;rarr; Group을 필터링&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP BY
&amp;rarr; 행을 줄이며 집계

Window Function
&amp;rarr; 행을 유지하며 분석&lt;/code&gt;&lt;/pre&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;정규화
&amp;rarr; Mapping Rule
&amp;rarr; PK / FK
&amp;rarr; JOIN&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 크게 느낀 점은 &lt;b&gt;SQL 문제는 문법을 많이 외운다고 바로 풀리는 것이 아니라 데이터의 구조와 문제의 요구사항을 먼저 분석해야 한다는 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&quot;부서별&quot;
&amp;rarr; GROUP BY 필요

&quot;총액이 900만원 이상인 부서&quot;
&amp;rarr; HAVING 필요

&quot;휴학생이 0명인 학과도 출력&quot;
&amp;rarr; LEFT JOIN 필요

&quot;년도별 + 전체 평균&quot;
&amp;rarr; ROLLUP 고려

&quot;부서별 순위&quot;
&amp;rarr; PARTITION BY 필요

&quot;사원과 사수&quot;
&amp;rarr; Self Join 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 문제 문장 안에 어떤 SQL 문법이 필요한지 판단할 수 있는 단서가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘은 잘못 작성한 SQL의 결과를 확인하고 다시 수정하는 과정이 많았다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE에서 SUM 사용
&amp;rarr; 오류 발생
&amp;rarr; HAVING으로 수정

JOIN 조건 누락
&amp;rarr; 154개 행 생성
&amp;rarr; 관계 조건 추가

휴학생만 먼저 필터링
&amp;rarr; 0명 학과 표현 불가
&amp;rarr; LEFT JOIN으로 기준 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 과정이 단순히 정답 SQL을 보는 것보다 더 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 정규화와 JOIN이 연결되는 과정도 인상적이었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;정규화
&amp;rarr; 데이터 중복을 줄이기 위해 테이블 분리

Mapping Rule
&amp;rarr; ERD를 실제 PK/FK 구조로 변환

JOIN
&amp;rarr; 분리된 데이터를 관계를 따라 다시 결합&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스를 설계하고 사용하는 과정이 하나의 흐름으로 연결되기 시작한 느낌이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업 마지막에는 강사님이:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;[Additional SELECT - Option]
Q1 ~ Q14&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 문제에서 최종적으로 필요한 컬럼 찾기
        &amp;darr;
2. 해당 컬럼이 어느 테이블에 있는지 확인
        &amp;darr;
3. 여러 테이블이 필요하다면 PK / FK 관계 확인
        &amp;darr;
4. INNER JOIN / LEFT JOIN 판단
        &amp;darr;
5. 일반 행 조건은 WHERE
        &amp;darr;
6. 그룹 기준이 있다면 GROUP BY
        &amp;darr;
7. 집계 결과 조건은 HAVING
        &amp;darr;
8. 정렬 조건은 ORDER BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 내용을 집중적으로 복습할 예정이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE vs HAVING

GROUP BY vs Window Function

INNER JOIN vs LEFT JOIN

ON vs USING

Equi Join vs Non-Equi Join

JOIN vs EXISTS

Self Join

WITH ROLLUP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 수업에서 강사님 또는 다른 수강생의 풀이와 내가 작성한 SQL을 비교하면서 &lt;b&gt;결과만 같은 SQL이 아니라 왜 그 방법을 선택했는지 설명할 수 있는 수준&lt;/b&gt;까지 이해해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>GROUPBY</category>
      <category>having</category>
      <category>join</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>mariadb</category>
      <category>Normalization</category>
      <category>SQL</category>
      <category>til</category>
      <category>WindowFunction</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/43</guid>
      <comments>https://bbbicsan.tistory.com/43#entry43comment</comments>
      <pubDate>Wed, 26 Aug 2026 17:41:13 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 20일차 TIL : SQL 함수(Function)와 그룹 함수, Additional SELECT 실습</title>
      <link>https://bbbicsan.tistory.com/42</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 20일차 TIL : SQL 함수(Function)와 그룹 함수, Additional SELECT 실습&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuiyjq/dJMcajchP9u/o8KUzMdG0lyvyBHmxowvI0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuiyjq/dJMcajchP9u/o8KUzMdG0lyvyBHmxowvI0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuiyjq/dJMcajchP9u/o8KUzMdG0lyvyBHmxowvI0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcuiyjq%2FdJMcajchP9u%2Fo8KUzMdG0lyvyBHmxowvI0%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;MariaDB에서 문자&amp;middot;숫자&amp;middot;날짜&amp;middot;조건 함수를 실습하고, 단일행 함수와 복수행 함수를 구분한 뒤 여러 함수를 중첩하여 실제 데이터를 가공하는 SQL 문제를 풀었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SQL Function
        &amp;darr;
단일행 함수
        &amp;darr;
문자 함수
LENGTH / CHAR_LENGTH
UPPER / LOWER
LPAD / RPAD
SUBSTRING / LEFT / RIGHT
INSTR / REPEAT / REPLACE
        &amp;darr;
함수 중첩
INSTR + LEFT
SUBSTRING + CAST
        &amp;darr;
숫자 함수
ABS / CEILING / FLOOR / ROUND
        &amp;darr;
날짜 함수
NOW / SYSDATE
CURDATE / CURTIME
ADDDATE / DATEDIFF
        &amp;darr;
조건 함수
IF / IFNULL / CASE
        &amp;darr;
복수행(그룹) 함수
COUNT / MIN / MAX / AVG / SUM
        &amp;darr;
Additional SELECT - 함수
Q1 ~ Q11 실습&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL Function&lt;/li&gt;
&lt;li&gt;단일행 함수&lt;/li&gt;
&lt;li&gt;복수행 함수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LENGTH()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CHAR_LENGTH()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPPER()&lt;/code&gt;&lt;code&gt;LOWER()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LPAD()&lt;/code&gt;&lt;code&gt;RPAD()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUBSTRING()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEFT()&lt;/code&gt;&lt;code&gt;RIGHT()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSTR()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUBSTRING_INDEX()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REPEAT()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REPLACE()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CAST()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CONCAT()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ABS()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CEILING()&lt;/code&gt;&lt;code&gt;FLOOR()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ROUND()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NOW()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SYSDATE()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CURDATE()&lt;/code&gt;&lt;code&gt;CURTIME()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADDDATE()&lt;/code&gt;&lt;code&gt;DATEDIFF()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IF()&lt;/code&gt;&lt;code&gt;IFNULL()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CASE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COUNT()&lt;/code&gt;&lt;code&gt;MIN()&lt;/code&gt;&lt;code&gt;MAX()&lt;/code&gt;&lt;code&gt;AVG()&lt;/code&gt;&lt;code&gt;SUM()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;함수 중첩&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. SQL 함수(Function)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 반복적으로 사용하는 기능을 하나의 작은 기능 단위로 만들어두고 필요할 때 호출하여 결과를 반환하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 함수도 크게 두 종류로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;SQL Function
│
├─ 단일행 함수
│   └─ 한 행마다 함수 실행
│
└─ 복수행 함수
    └─ 여러 행을 하나의 그룹으로 계산&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일행 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Row마다 함수가 실행되고 각 Row마다 결과가 반환된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROW 1 &amp;rarr; 함수 &amp;rarr; 결과 1
ROW 2 &amp;rarr; 함수 &amp;rarr; 결과 2
ROW 3 &amp;rarr; 함수 &amp;rarr; 결과 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자, 숫자, 날짜, 변환 함수 등이 여기에 해당한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복수행 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Row를 입력받아 하나의 집계 결과를 계산한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROW 1
ROW 2 ──&amp;rarr; AVG() ──&amp;rarr; 평균 1개
ROW 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;COUNT
MIN
MAX
AVG
SUM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 문자 함수 - LENGTH()와 CHAR_LENGTH()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 실습에서는 사원의 이름 길이를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       LENGTH(EMP_NAME)
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과 한글 이름이 3글자인데 결과가 &lt;code&gt;9&lt;/code&gt;로 나왔다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;한선기 &amp;rarr; 9
강중훈 &amp;rarr; 9
최만식 &amp;rarr; 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이름이 3글자인데 왜 9가 나오는지 의문이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서 &lt;code&gt;LENGTH()&lt;/code&gt;는 &lt;b&gt;문자 개수&lt;/b&gt;가 아니라 &lt;b&gt;문자열의 바이트 길이&lt;/b&gt;를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UTF-8 계열에서 한글 한 글자는 일반적으로 3Byte이므로:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;임정섭

3글자 &amp;times; 3Byte
= 9Byte&lt;/code&gt;&lt;/pre&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT LENGTH('임정섭'),
       LENGTH('JSLIM'),
       CHAR_LENGTH('임정섭'),
       UPPER('lgcns'),
       LOWER('LGCNS');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;LENGTH('임정섭')      &amp;rarr; 9
LENGTH('JSLIM')       &amp;rarr; 5
CHAR_LENGTH('임정섭') &amp;rarr; 3
UPPER('lgcns')        &amp;rarr; LGCNS
LOWER('LGCNS')        &amp;rarr; lgcns&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LENGTH()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문자열의 Byte 길이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHAR_LENGTH()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 문자 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPPER()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;대문자로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOWER()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소문자로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. LPAD(), RPAD() - 문자열 채우기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열의 부족한 길이를 특정 문자로 채우는 함수이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT LPAD('5', 3, 'A');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;AA5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;LPAD(문자열, 최종길이, 채울문자)
RPAD(문자열, 최종길이, 채울문자)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT LPAD('5', 3, 'A'),
       RPAD('5', 3, 'A');&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;LPAD &amp;rarr; AA5
RPAD &amp;rarr; 5AA&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. SUBSTRING(), LEFT(), RIGHT() - 부분 문자열 추출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열에서 원하는 부분만 잘라내는 함수이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT SUBSTRING('ABCDEF', 1, 2),
       LEFT('ABCDEF', 2),
       RIGHT('ABCDEF', 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SUBSTRING('ABCDEF', 1, 2) &amp;rarr; AB
LEFT('ABCDEF', 2)         &amp;rarr; AB
RIGHT('ABCDEF', 2)        &amp;rarr; EF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SUBSTRING()
&amp;rarr; 시작 위치와 길이를 직접 지정

LEFT()
&amp;rarr; 왼쪽부터 자름

RIGHT()
&amp;rarr; 오른쪽부터 자름&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. INSTR() - 문자열 위치 찾기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;INSTR()&lt;/code&gt;는 문자열에서 특정 문자열이 시작되는 위치를 반환한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT INSTR('LGCNS CAMP', 'CAMP');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;7&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 위치는 1부터 시작한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;L G C N S   C A M P
1 2 3 4 5 6 7 8 9 10
            &amp;uarr;
          CAMP&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 함수 중첩 - 이메일 ID 추출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 단순히 하나의 함수만 사용하는 것이 아니라 &lt;b&gt;함수의 반환값을 다른 함수의 인자로 전달&lt;/b&gt;했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMAIL,
       INSTR(EMAIL, 'c.'),
       LEFT(EMAIL, INSTR(EMAIL, '@') - 1)
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;LEFT(EMAIL, INSTR(EMAIL, '@') - 1)&lt;/code&gt;&lt;/pre&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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;sg_ahn@vcc.com
      &amp;darr;
INSTR(EMAIL, '@')
      &amp;darr;
@ 위치 검색
      &amp;darr;
@ 위치 - 1
      &amp;darr;
LEFT()
      &amp;darr;
sg_ahn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sg_ahn@vcc.com  &amp;rarr; sg_ahn
jh_park@vcc.com &amp;rarr; jh_park
ms_choi@vcc.com &amp;rarr; ms_choi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습을 통해 함수는 단독으로 사용하는 것보다 &lt;b&gt;여러 함수를 조합했을 때 훨씬 다양한 데이터 가공이 가능하다&lt;/b&gt;는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. REPEAT(), REPLACE()&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REPEAT()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 원하는 횟수만큼 반복한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT REPEAT('LGCNS', 3);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;LGCNSLGCNSLGCNS&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REPLACE()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열의 특정 부분을 다른 문자열로 변경한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT REPLACE(
    '오늘은 즐거운 함수 수업',
    '함수',
    '메서드'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;오늘은 즐거운 메서드 수업&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. SUBSTRING()으로 입사년도 추출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입사년도만 추출한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문제를 두 가지 &lt;code&gt;SUBSTRING()&lt;/code&gt; 문법으로 작성했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT SUBSTRING(E.HIRE_DATE FROM 1 FOR 4) AS '입사년도',
       SUBSTRING(E.HIRE_DATE, 1, 4) AS '입사년도'
FROM employee E;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1990
2004
1995
1997
1998
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서는 다음 두 문법을 모두 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SUBSTRING(str FROM pos FOR len)

SUBSTRING(str, pos, len)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. CAST() - 데이터 타입 변환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 실습 데이터베이스 &lt;code&gt;sqlDB&lt;/code&gt;도 생성했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sqlDB
│
├─ userTbl
│   └─ 회원 정보
│
└─ buyTbl
    └─ 구매 정보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계는:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;userTbl.userID (PK)
        &amp;darr;
buyTbl.userID (FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이며 &lt;code&gt;buyTbl.num&lt;/code&gt;에는 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;가 적용되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;구매번호,
총 금액(PRICE * AMOUNT =),
구매액을 출력한다면?

조건: CAST() 활용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT NUM AS '구매번호',
       CONCAT(
           CAST(PRICE AS VARCHAR(10)),
           ' * ',
           CAST(AMOUNT AS VARCHAR(10)),
           ' = '
       ) AS '총 금액',
       PRICE * AMOUNT AS '구매액'
FROM buyTbl;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;구매번호 | 총 금액      | 구매액
1       | 30 * 2 =     | 60
2       | 1000 * 1 =   | 1000
3       | 200 * 1 =    | 200
4       | 200 * 5 =    | 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;PRICE / AMOUNT
      &amp;darr;
CAST()
      &amp;darr;
문자열 변환
      &amp;darr;
CONCAT()
      &amp;darr;
&quot;30 * 2 =&quot; 생성

PRICE * AMOUNT
      &amp;darr;
실제 구매액 계산&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 숫자 함수&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 처리하는 여러 함수를 실습했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SELECT ABS(-100),
       CEILING(4.7),
       CEILING(4.1),
       FLOOR(4.7),
       FLOOR(4.1),
       ROUND(4153.415354, 2),
       ROUND(4153.415354, -2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ABS(-100)             &amp;rarr; 100
CEILING(4.7)          &amp;rarr; 5
CEILING(4.1)          &amp;rarr; 5
FLOOR(4.7)            &amp;rarr; 4
FLOOR(4.1)            &amp;rarr; 4
ROUND(4153.415354, 2) &amp;rarr; 4153.42
ROUND(4153.415354,-2) &amp;rarr; 4200&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ABS()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;절댓값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CEILING()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;올림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FLOOR()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROUND()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;반올림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;ROUND()&lt;/code&gt;의 두 번째 인자에 음수가 들어갈 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ROUND(4153.415354, 2)
&amp;rarr; 소수점 둘째 자리

ROUND(4153.415354, -2)
&amp;rarr; 백의 자리 단위 반올림&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 날짜 함수&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 날짜와 시간을 확인하는 함수를 실습했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT NOW(),
       SYSDATE();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 당시:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2026-08-25 11:09:09&lt;/code&gt;&lt;/pre&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;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOW()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 날짜 + 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SYSDATE()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 날짜 + 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CURDATE()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 날짜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CURTIME()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADDDATE()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;날짜에 기간 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATEDIFF()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;두 날짜의 차이를 일 단위로 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT ADDDATE(CURDATE(), INTERVAL 30 YEAR);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재 날짜
&amp;rarr; 30년 추가
&amp;rarr; 30년 뒤 날짜&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. DATEDIFF() - 근속년수 계산&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;오늘 날짜를 기준으로
근속년수가 30년 이상인 사원의
이름, 입사일, 근속년수를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       HIRE_DATE,
       ROUND(DATEDIFF(CURDATE(), HIRE_DATE) / 365) AS '근속년수'
FROM employee
WHERE DATEDIFF(CURDATE(), HIRE_DATE) / 365 &amp;gt;= 30;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;CURDATE()
        &amp;darr;
오늘 날짜

DATEDIFF(CURDATE(), HIRE_DATE)
        &amp;darr;
근무한 총 일수

/ 365
        &amp;darr;
연 단위로 변환

ROUND()
        &amp;darr;
근속년수 출력

WHERE &amp;gt;= 30
        &amp;darr;
30년 이상만 필터링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;한선기 | 1990-04-01 | 36
최만식 | 1995-12-30 | 31&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;날짜 함수 DATEDIFF()
+
숫자 함수 ROUND()
+
WHERE 조건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 함께 사용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. IF() - 주민번호로 성별 판별&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;부서번호가 50번인 사원의
이름, 주민번호, 성별을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       EMP_NO,
       IF(
           SUBSTRING(EMP_NO, 8, 1) IN ('1', '3'),
           '남자',
           '여자'
       ) AS 'GENDER'
FROM employee
WHERE DEPT_ID = '50';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SUBSTRING(EMP_NO, 8, 1)
        &amp;darr;
주민번호 뒷자리 첫 숫자

1 또는 3
&amp;rarr; 남자

그 외
&amp;rarr; 여자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SUBSTRING()
+
IN
+
IF()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 중첩하여 새로운 파생 컬럼을 만들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. WHERE 조건으로 특정 성별만 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 성별 정보라도 조건 함수를 사용하지 않고 WHERE에서 직접 필터링할 수도 있었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       EMP_NO,
       '남자' AS GENDER
FROM employee
WHERE SUBSTRING(EMP_NO, 8, 1) = '1';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우:&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; 남자 데이터만 조회

'남자' AS GENDER
&amp;rarr; 조회 결과에 고정 문자열 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 &lt;code&gt;IF()&lt;/code&gt; 방식과 차이는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IF()
&amp;rarr; 전체 행을 남/여로 분기

WHERE
&amp;rarr; 특정 조건의 행 자체만 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. IF()와 IFNULL() - 빈 문자열과 NULL의 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JOB_ID가 J4인 사원의
이름, 사번, 사수번호를 조회

사수번호가 없는 경우
'관리자' 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       EMP_ID,
       IF(MGR_ID = '', '관리자', MGR_ID) AS '사수번호'
FROM employee
WHERE JOB_ID = 'J4';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 생각할 수 있는 방법은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IFNULL(MGR_ID, '관리자')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이지만 현재 데이터에서는 일부 사수번호가 &lt;code&gt;NULL&lt;/code&gt;이 아니라 &lt;b&gt;빈 문자열 &lt;code&gt;''&lt;/code&gt;&lt;/b&gt; 로 저장되어 있었다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;NULL
&amp;ne;
''&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IFNULL()
&amp;rarr; NULL만 처리

IF(MGR_ID = '', ...)
&amp;rarr; 빈 문자열 조건 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 차이가 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. CASE - 여러 조건 분기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;급여등급을 구분한다면?

300 이하 &amp;rarr; 초급
400 이하 &amp;rarr; 중급
그 이상 &amp;rarr; 고급&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 조건을 분기해야 할 때는 &lt;code&gt;CASE&lt;/code&gt; 표현식을 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;CASE
    WHEN 조건1 THEN 결과1
    WHEN 조건2 THEN 결과2
    ELSE 결과3
END&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CASE&lt;/code&gt;는 위의 조건부터 순서대로 검사한다는 점이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. 복수행 함수(Group Function)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 여러 행을 대상으로 계산하는 그룹 함수로 넘어갔다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT COUNT(*),
       COUNT(BONUS_PCT),
       COUNT(IFNULL(BONUS_PCT, 0)),
       MIN(SALARY),
       MAX(SALARY),
       AVG(SALARY),
       SUM(SALARY)
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;COUNT()&lt;/h2&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;COUNT(*)
&amp;rarr; 전체 행 개수

COUNT(BONUS_PCT)
&amp;rarr; BONUS_PCT가 NULL이 아닌 행 개수

COUNT(IFNULL(BONUS_PCT, 0))
&amp;rarr; NULL을 0으로 변환
&amp;rarr; 모든 행이 COUNT 대상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 그룹 함수에서는 &lt;code&gt;NULL&lt;/code&gt; 처리 여부가 결과에 영향을 줄 수 있다는 점이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MIN / MAX / AVG / SUM&lt;/h2&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;MIN(SALARY)
&amp;rarr; 최소 급여

MAX(SALARY)
&amp;rarr; 최대 급여

AVG(SALARY)
&amp;rarr; 평균 급여

SUM(SALARY)
&amp;rarr; 급여 총합&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. 일반 컬럼과 그룹 함수&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 함수를 배우면서 이후 &lt;code&gt;GROUP BY&lt;/code&gt;와 연결되는 중요한 특징도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       COUNT(*)
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 일반 컬럼과 집계 함수를 아무 기준 없이 함께 조회하면 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;EMP_NAME
&amp;rarr; 개별 Row 정보

COUNT(*)
&amp;rarr; 여러 Row의 집계 결과&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;일반 컬럼 + 그룹 함수
        &amp;darr;
GROUP BY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념으로 연결될 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Additional SELECT - 함수 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Workbook의 Additional SELECT 함수 문제도 직접 풀어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킵 문항:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;7, 10, 12, 13, 14, 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 풀이 대상:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1, 2, 3, 4, 5, 6, 8, 9&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. Q1 - 영어영문학과 학생 조회&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;영어영문학과(002) 학생의
학번, 이름, 입학년도를
입학년도가 빠른 순으로 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이 1:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT a.STUDENT_NO   학번,
       a.STUDENT_NAME 이름,
       a.ENTRANCE_DATE 입학년도
FROM tb_student a
WHERE 1 = 1
  AND a.DEPARTMENT_NO = '002'
ORDER BY 3 ASC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 표현:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT STUDENT_NO   AS '학번',
       STUDENT_NAME AS '이름',
       ENTRANCE_DATE AS '입학년도'
FROM tb_student
WHERE DEPARTMENT_NO = '002'
ORDER BY ENTRANCE_DATE ASC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 SQL은 같은 결과를 만든다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;ORDER BY 3
=
SELECT의 세 번째 컬럼
=
ENTRANCE_DATE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;AS&lt;/code&gt;는 컬럼 별칭에서 생략할 수도 있다는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDM4g7/dJMcaheBgpb/d5CuSufUyxaTA8wETHhXw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDM4g7/dJMcaheBgpb/d5CuSufUyxaTA8wETHhXw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDM4g7/dJMcaheBgpb/d5CuSufUyxaTA8wETHhXw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDM4g7%2FdJMcaheBgpb%2Fd5CuSufUyxaTA8wETHhXw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;416&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. Q2 - 이름이 세 글자가 아닌 교수 찾기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT PROFESSOR_NAME,
       PROFESSOR_SSN
FROM tb_professor
WHERE CHAR_LENGTH(PROFESSOR_NAME) &amp;lt;&amp;gt; 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT a.PROFESSOR_NAME,
       a.PROFESSOR_SSN
FROM tb_professor a
WHERE 1 = 1
  AND a.PROFESSOR_NAME NOT LIKE '___';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LIKE&lt;/code&gt;에서 &lt;code&gt;_&lt;/code&gt;는 정확히 한 글자를 의미한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;_   &amp;rarr; 1글자
__  &amp;rarr; 2글자
___ &amp;rarr; 3글자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;NOT LIKE '___'
&amp;rarr; 정확히 3글자가 아닌 문자열&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;강혁
박강아름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 문제를:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;CHAR_LENGTH()
&amp;rarr; 문자 개수를 계산하는 방식

NOT LIKE
&amp;rarr; 패턴으로 비교하는 방식&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 방법으로 해결했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjUy5A/dJMb998Ci1k/oeuVWxMkzv4DkAfadaMtKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjUy5A/dJMb998Ci1k/oeuVWxMkzv4DkAfadaMtKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjUy5A/dJMb998Ci1k/oeuVWxMkzv4DkAfadaMtKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjUy5A%2FdJMb998Ci1k%2FoeuVWxMkzv4DkAfadaMtKK%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;541&quot; height=&quot;360&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. Q3 - 남자 교수의 이름과 나이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT PROFESSOR_NAME AS '교수이름',
       126 - CAST(
           SUBSTRING(PROFESSOR_SSN, 1, 2)
           AS INT
       ) AS '나이'
FROM tb_professor
WHERE LEFT(
          SUBSTRING_INDEX(PROFESSOR_SSN, '-', -1),
          1
      ) = '1'
ORDER BY '나이';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주민번호 앞 두 자리를 추출한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;601004-1100528
 &amp;darr;
60&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 값을 &lt;code&gt;CAST()&lt;/code&gt;로 숫자로 변환하고 2026년을 기준으로 나이를 계산했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남자 여부는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;601004-1100528
       &amp;darr;
SUBSTRING_INDEX(..., '-', -1)
       &amp;darr;
1100528
       &amp;darr;
LEFT(..., 1)
       &amp;darr;
1&lt;/code&gt;&lt;/pre&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;pre class=&quot;markdown&quot;&gt;&lt;code&gt;SUBSTRING
+
CAST
+
SUBSTRING_INDEX
+
LEFT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 여러 문자&amp;middot;변환 함수를 중첩한 실습이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MTfin/dJMb991U2J4/0rxLxd1Pny2WIXMi6T0Wok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MTfin/dJMb991U2J4/0rxLxd1Pny2WIXMi6T0Wok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MTfin/dJMb991U2J4/0rxLxd1Pny2WIXMi6T0Wok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMTfin%2FdJMb991U2J4%2F0rxLxd1Pny2WIXMi6T0Wok%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;876&quot; height=&quot;354&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 &lt;code&gt;126 - 출생년도 두 자리&lt;/code&gt; 방식으로 계산했다. 정확한 만 나이를 구하려면 실제 생일이 지났는지까지 고려해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. Q4 - 교수 이름에서 성 제거&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT RIGHT(
           PROFESSOR_NAME,
           CHAR_LENGTH(PROFESSOR_NAME) - 1
       ) AS '이름'
FROM tb_professor;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;김진영
 &amp;darr;
CHAR_LENGTH() = 3
 &amp;darr;
3 - 1 = 2
 &amp;darr;
RIGHT(..., 2)
 &amp;darr;
진영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;김진영   &amp;rarr; 진영
강혁     &amp;rarr; 혁
박강아름 &amp;rarr; 강아름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 조건에서 성이 두 글자인 교수는 없다고 가정했기 때문에 가능한 방식이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxGnHT/dJMcahS26S1/TGF3GhlaL88mAY9KhcZLc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxGnHT/dJMcahS26S1/TGF3GhlaL88mAY9KhcZLc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxGnHT/dJMcahS26S1/TGF3GhlaL88mAY9KhcZLc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxGnHT%2FdJMcahS26S1%2FTGF3GhlaL88mAY9KhcZLc1%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;671&quot; height=&quot;336&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Q5 - 재수생 입학자 찾기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서는 &lt;b&gt;19살에 입학하면 재수를 하지 않은 것&lt;/b&gt;으로 간주했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT a.STUDENT_NO,
       a.STUDENT_NAME
FROM tb_student a
WHERE LEFT(a.ENTRANCE_DATE, 4)
      - CONCAT('19', LEFT(a.STUDENT_SSN, 2)) &amp;gt; 19;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;LEFT(ENTRANCE_DATE, 4)
&amp;rarr; 입학년도

LEFT(STUDENT_SSN, 2)
&amp;rarr; 출생년도 두 자리

CONCAT('19', ...)
&amp;rarr; 4자리 출생년도

입학년도 - 출생년도
&amp;rarr; 입학 당시 나이 계산

&amp;gt; 19
&amp;rarr; 재수 이상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는 문자열 함수로 데이터를 추출한 후 &lt;b&gt;산술 연산에 연결&lt;/b&gt;했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nS1JY/dJMcah6LQf1/1mxe4OC7SYj8k04YqyPwok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nS1JY/dJMcah6LQf1/1mxe4OC7SYj8k04YqyPwok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nS1JY/dJMcah6LQf1/1mxe4OC7SYj8k04YqyPwok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnS1JY%2FdJMcah6LQf1%2F1mxe4OC7SYj8k04YqyPwok%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;718&quot; height=&quot;323&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. Q6 - 2020년 크리스마스 요일&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DAYNAME('2020-12-25');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Friday&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 2020년 크리스마스는 금요일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 날짜 함수도 함께 확인했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;WEEKDAY()
&amp;rarr; 월요일을 0으로 시작

DAYOFWEEK()
&amp;rarr; 일요일을 1로 시작

DAYNAME()
&amp;rarr; 요일 이름 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FdsUb/dJMcacRKJyU/cVxwaIiQLfpwzcQuFjg95K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FdsUb/dJMcacRKJyU/cVxwaIiQLfpwzcQuFjg95K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FdsUb/dJMcacRKJyU/cVxwaIiQLfpwzcQuFjg95K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFdsUb%2FdJMcacRKJyU%2FcVxwaIiQLfpwzcQuFjg95K%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;478&quot; height=&quot;204&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. Q8 - 2000년도 이전 학번 학생 찾기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방법:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT STUDENT_NO AS '학번',
       STUDENT_NAME AS '이름'
FROM tb_student
WHERE STUDENT_NO NOT LIKE 'A%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방법:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT STUDENT_NO,
       STUDENT_NAME
FROM tb_student
WHERE SUBSTRING(STUDENT_NO, 1, 1) != 'A';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 2000년도 이후 학번은 &lt;code&gt;A&lt;/code&gt;로 시작한다고 주어졌기 때문에:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;A로 시작
&amp;rarr; 2000년도 이후

A로 시작하지 않음
&amp;rarr; 2000년도 이전&lt;/code&gt;&lt;/pre&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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;NOT LIKE
vs
SUBSTRING&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방법으로 구현할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1gxyL/dJMcabSTOHP/HOYpKkokATDnWuVkrbUAI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1gxyL/dJMcabSTOHP/HOYpKkokATDnWuVkrbUAI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1gxyL/dJMcabSTOHP/HOYpKkokATDnWuVkrbUAI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1gxyL%2FdJMcabSTOHP%2FHOYpKkokATDnWuVkrbUAI1%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;473&quot; height=&quot;394&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. Q9 - 학생의 총 평점 계산&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학번 A517178 학생의
학점 총 평점을 계산

소수점 첫째 자리까지 반올림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 풀이:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT ROUND(AVG(POINT), 1) AS '평점'
FROM tb_grade
WHERE STUDENT_NO = 'A517178';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3.6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;AVG()
&amp;rarr; 여러 학점의 평균

ROUND(..., 1)
&amp;rarr; 소수점 첫째 자리까지 반올림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 &lt;b&gt;복수행 함수와 단일행 숫자 함수를 중첩&lt;/b&gt;했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;성적 여러 건
    &amp;darr;
AVG()
    &amp;darr;
평균값
    &amp;darr;
ROUND()
    &amp;darr;
3.6&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pCshQ/dJMcajwHAgV/ws6G1t72TiKwOehDM5t6fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pCshQ/dJMcajwHAgV/ws6G1t72TiKwOehDM5t6fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pCshQ/dJMcajwHAgV/ws6G1t72TiKwOehDM5t6fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpCshQ%2FdJMcajwHAgV%2Fws6G1t72TiKwOehDM5t6fk%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;506&quot; height=&quot;267&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 한글 이름이 3글자인데 LENGTH() 결과가 9가 나온 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT EMP_NAME,
       LENGTH(EMP_NAME)
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;한선기 &amp;rarr; 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 세 글자인데 예상했던 &lt;code&gt;3&lt;/code&gt;이 아니라 &lt;code&gt;9&lt;/code&gt;가 나왔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB의 &lt;code&gt;LENGTH()&lt;/code&gt;는 문자의 개수가 아니라 &lt;b&gt;Byte 길이&lt;/b&gt;를 반환했다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;한글 3글자
&amp;times;
UTF-8 기준 일반적으로 3Byte
=
9Byte&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 개수를 알고 싶은 경우 &lt;code&gt;CHAR_LENGTH()&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CHAR_LENGTH('임정섭');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3&lt;/code&gt;&lt;/pre&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;함수가 문자 개수를 반환하는지 Byte 수를 반환하는지 정확한 반환 기준을 확인해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IFNULL()을 사용했는데 '관리자'가 출력되지 않는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사수번호가 없는 사원에게 &lt;code&gt;'관리자'&lt;/code&gt;를 출력하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 생각할 수 있는 방법은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IFNULL(MGR_ID, '관리자')&lt;/code&gt;&lt;/pre&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아니라:&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;''&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 문자열이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;IFNULL()&lt;/code&gt;만으로는 해결되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 수업에서는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IF(MGR_ID = '', '관리자', MGR_ID)&lt;/code&gt;&lt;/pre&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NULL
&amp;rarr; 값 자체가 없음

''
&amp;rarr; 길이가 0인 문자열&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값은 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실습을 통해 SQL에서 데이터의 &lt;b&gt;실제 저장 상태를 먼저 확인하는 것이 중요&lt;/b&gt;하다는 것을 알게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 같은 문제를 여러 방법으로 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Workbook 문제를 풀면서 하나의 요구사항을 여러 SQL로 표현할 수 있다는 점도 반복해서 확인했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이름이 세 글자가 아닌 교수&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;CHAR_LENGTH(PROFESSOR_NAME) &amp;lt;&amp;gt; 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;PROFESSOR_NAME NOT LIKE '___'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2000년도 이전 학번&lt;/h3&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;STUDENT_NO NOT LIKE 'A%'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SUBSTRING(STUDENT_NO, 1, 1) != 'A'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q1 정렬&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;ORDER BY 3 ASC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;ORDER BY ENTRANCE_DATE ASC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 특정 문법을 하나 외우는 것이 아니라:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;요구사항 분석
        &amp;darr;
필요한 데이터 확인
        &amp;darr;
사용 가능한 함수 선택
        &amp;darr;
함수 조합
        &amp;darr;
SQL 실행
        &amp;darr;
결과 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 과정을 스스로 만들 수 있는 능력이라는 것을 느꼈다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제까지는 &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;LIKE&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt; 등을 이용해 &lt;b&gt;어떤 행을 조회할 것인지&lt;/b&gt;에 집중했다면, 오늘은 조회한 값을 &lt;b&gt;어떻게 가공할 것인지&lt;/b&gt;를 집중적으로 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;문자열에서 위치 찾기
&amp;rarr; INSTR()

위치 기준으로 문자열 자르기
&amp;rarr; LEFT() / SUBSTRING()

문자열을 숫자로 변환
&amp;rarr; CAST()

평균 계산
&amp;rarr; AVG()

계산 결과 반올림
&amp;rarr; ROUND()&lt;/code&gt;&lt;/pre&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;이전에는:&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 저장된 데이터를 그대로 가져왔다면 오늘은:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;LEFT(EMAIL, INSTR(EMAIL, '@') - 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 실제 테이블에는 존재하지 않는 정보를 SQL에서 만들어낼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 &lt;code&gt;AVG()&lt;/code&gt;나 &lt;code&gt;COUNT()&lt;/code&gt;를 배우면서 SQL이 단순 조회 언어가 아니라 &lt;b&gt;여러 Row의 데이터를 집계하고 분석하는 도구&lt;/b&gt;라는 것도 체감할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 SQL 함수의 종류가 많아서 처음에는 각각의 함수를 모두 따로 외워야 하나 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실습을 진행하면서 함수 자체를 암기하는 것보다:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;문자열 길이가 필요한가?
&amp;rarr; LENGTH / CHAR_LENGTH

문자 일부가 필요한가?
&amp;rarr; SUBSTRING / LEFT / RIGHT

문자의 위치가 필요한가?
&amp;rarr; INSTR

자료형을 바꿔야 하는가?
&amp;rarr; CAST

조건에 따라 출력해야 하는가?
&amp;rarr; IF / CASE

여러 행을 계산해야 하는가?
&amp;rarr; COUNT / AVG / SUM&lt;/code&gt;&lt;/pre&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;특히 Workbook 문제를 풀 때는 하나의 함수만 알고 있어서는 해결하기 어려운 문제가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q3에서는:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SUBSTRING
&amp;rarr; CAST
&amp;rarr; SUBSTRING_INDEX
&amp;rarr; LEFT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 사용했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q9에서는:&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;AVG
&amp;rarr; ROUND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 중첩해서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 SQL에서도 Java에서 메서드를 조합했던 것처럼 &lt;b&gt;작은 기능들을 조합해서 하나의 요구사항을 해결한다는 점은 동일&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;LENGTH()&lt;/code&gt;가 한글 이름에서 9를 반환한 문제나 &lt;code&gt;IFNULL()&lt;/code&gt;이 빈 문자열을 처리하지 못했던 사례처럼, SQL 문법이 맞는 것만으로는 충분하지 않고 &lt;b&gt;데이터 타입과 실제 저장된 값까지 확인해야 정확한 결과를 얻을 수 있다는 점&lt;/b&gt;도 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 많은 함수를 새로 배웠지만, 실제 Workbook 문제를 직접 풀면서 바로 사용해본 덕분에 각 함수가 어떤 상황에서 필요한지 연결해서 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 마지막에 복수행 함수까지 학습했기 때문에 다음에는 그룹별로 데이터를 집계하는 방법을 더 집중적으로 볼 것 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;COUNT / SUM / AVG / MIN / MAX
        &amp;darr;
GROUP BY
        &amp;darr;
그룹별 집계
        &amp;darr;
HAVING
        &amp;darr;
그룹 결과 조건 검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 내용을 중점적으로 복습해야겠다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 단일행 함수와 복수행 함수의 차이

2. LENGTH와 CHAR_LENGTH의 차이

3. SUBSTRING / LEFT / RIGHT 구분

4. 함수 중첩 순서 읽는 연습

5. NULL과 빈 문자열의 차이

6. COUNT(*)와 COUNT(column)의 차이

7. 일반 컬럼과 그룹 함수를 함께 사용할 때
   GROUP BY가 필요한 이유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;까지 배우면 단순히 한 명의 데이터를 가공하는 것에서 나아가 &lt;b&gt;학과별 학생 수, 직급별 평균 급여처럼 그룹 단위로 데이터를 분석하는 SQL&lt;/b&gt;까지 연결해서 이해해봐야겠다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>GroupFunction</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>LGCNS6기TIL</category>
      <category>mariadb</category>
      <category>SQL</category>
      <category>SQLFunction</category>
      <category>til</category>
      <category>내일배움카드</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/42</guid>
      <comments>https://bbbicsan.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 25 Aug 2026 18:00:22 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 19일차 TIL : 데이터베이스 기초와 RDBMS&amp;middot;SELECT 조건 검색 실습</title>
      <link>https://bbbicsan.tistory.com/41</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 19일차 TIL : 데이터베이스 기초와 RDBMS&amp;middot;SELECT 조건 검색 실습&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0hZbp/dJMcahS2jRN/qNcLGGNK6IFqd7fkxfFe2k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0hZbp/dJMcahS2jRN/qNcLGGNK6IFqd7fkxfFe2k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0hZbp/dJMcahS2jRN/qNcLGGNK6IFqd7fkxfFe2k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0hZbp%2FdJMcahS2jRN%2FqNcLGGNK6IFqd7fkxfFe2k%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;데이터베이스와 RDBMS의 기본 개념부터 무결성&amp;middot;관계&amp;middot;PK/FK&amp;middot;정규화까지 학습하고, MariaDB 실습 환경에서 &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;LIKE&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;, &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;DISTINCT&lt;/code&gt; 등을 활용해 실제 데이터를 조건별로 조회했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Database 기본 개념
        &amp;darr;
DBMS / RDBMS 이해
        &amp;darr;
Oracle &amp;middot; MySQL &amp;middot; MariaDB &amp;middot; NoSQL
        &amp;darr;
데이터와 정보의 차이
        &amp;darr;
데이터베이스 무결성
        &amp;darr;
관계형 데이터베이스
        &amp;darr;
Entity / Relationship
        &amp;darr;
PK / FK
        &amp;darr;
정규화
        &amp;darr;
참조 무결성
        &amp;darr;
MariaDB 실습 환경 구성
        &amp;darr;
SELECT 기본 문법
        &amp;darr;
WHERE 조건 검색
        &amp;darr;
AND / OR / NOT
        &amp;darr;
BETWEEN / IN
        &amp;darr;
LIKE / NOT LIKE
        &amp;darr;
IS NULL / IS NOT NULL
        &amp;darr;
DISTINCT
        &amp;darr;
Workbook Basic SELECT 실습&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;DBMS&lt;/li&gt;
&lt;li&gt;RDBMS&lt;/li&gt;
&lt;li&gt;Oracle&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;MariaDB&lt;/li&gt;
&lt;li&gt;NoSQL&lt;/li&gt;
&lt;li&gt;DBeaver&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;DDL / DML / DCL&lt;/li&gt;
&lt;li&gt;Entity&lt;/li&gt;
&lt;li&gt;Relationship&lt;/li&gt;
&lt;li&gt;Primary Key&lt;/li&gt;
&lt;li&gt;Foreign Key&lt;/li&gt;
&lt;li&gt;Referential Integrity&lt;/li&gt;
&lt;li&gt;Normalization&lt;/li&gt;
&lt;li&gt;Index&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. 데이터베이스(Database)란?&lt;/h1&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;강의에서는 특히 다음 세 가지가 중요하게 강조되었다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;공유
+
통합
+
중복 최소화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 조직에서 업무상 필요한 데이터를 각 프로그램마다 따로 저장하는 것이 아니라 하나의 데이터베이스에 모아 관리하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 데이터(Data)와 정보(Information)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터와 정보는 비슷해 보이지만 의미가 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;관찰이나 측정을 통해 얻어진 사실 또는 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Information&lt;/td&gt;
&lt;td&gt;데이터를 가공&amp;middot;해석하여 의미를 부여한 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;학생 점수
90, 80, 70
        &amp;darr;
       Data
        &amp;darr;
평균 계산
        &amp;darr;
평균 점수 80점
        &amp;darr;
 Information&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;데이터가 원재료라면 정보는 데이터를 처리해서 얻은 의미 있는 결과&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. 데이터베이스의 특징&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 데이터베이스의 주요 특징을 다음과 같이 정리했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;데이터 독립성
&amp;rarr; Data Independence

데이터 무결성
&amp;rarr; Integrity

데이터 보안성
&amp;rarr; Security

데이터 불일치 배제
&amp;rarr; Inconsistency 방지

데이터 중복 감소
&amp;rarr; Redundancy 감소

데이터 공유
&amp;rarr; Share&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 오늘 수업에서는 특히 &lt;b&gt;무결성(Integrity)&lt;/b&gt; 이 이후 관계형 데이터베이스 설계와 연결되는 중요한 개념이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. DBMS(Database Management System)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBMS는 데이터베이스를 효율적으로 관리하기 위한 시스템이다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;사용자
  &amp;darr;
Application
  &amp;darr;
DBMS
  &amp;darr;
Database&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자나 애플리케이션이 저장된 데이터를 직접 다루는 것이 아니라 DBMS가 중간에서 데이터의 저장&amp;middot;조회&amp;middot;수정&amp;middot;삭제 등을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Database
&amp;rarr; 실제 데이터의 집합

DBMS
&amp;rarr; Database를 관리하는 소프트웨어&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBMS는 데이터 모델에 따라 다음처럼 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;계층형 DBMS
망형 DBMS
관계형 DBMS
객체지향형 DBMS
객체관계형 DBMS&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. RDBMS와 관계형 데이터베이스&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스(Relational Database)는 데이터를 &lt;b&gt;Table 형태&lt;/b&gt;로 관리하고 테이블 사이에 관계를 맺는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1969년 IBM 연구원이었던 E. F. Codd가 관계형 데이터베이스 모델을 제안했다고 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스의 핵심 구조는:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Table
 &amp;darr;
Column
 &amp;darr;
Row&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여러 테이블을:&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Primary Key
+
Foreign Key&lt;/code&gt;&lt;/pre&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;pre class=&quot;less&quot;&gt;&lt;code&gt;DEPARTMENT
DEPT_ID (PK)
     &amp;darr;
EMPLOYEE
DEPT_ID (FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 부모 테이블의 키를 자식 테이블에서 참조할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 관계형 데이터베이스의 핵심 - PK와 FK&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Primary Key&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 각 행을 유일하게 식별할 수 있는 키이다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Primary Key
&amp;rarr; 중복 X
&amp;rarr; NULL X
&amp;rarr; 하나의 행을 식별&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Foreign Key&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 테이블의 Primary Key를 참조하는 컬럼이다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;부모 Table
     &amp;darr;
Primary Key
     &amp;darr;
자식 Table
     &amp;darr;
Foreign Key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 서로 분리된 테이블을 연결할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. 데이터베이스 설계에서 중요한 무결성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 강의에서는 데이터베이스 설계 시 &lt;b&gt;참조 무결성(Referential Integrity)&lt;/b&gt; 이 중요하다고 강조했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;CUSTOMER
customer_id (PK)
        &amp;darr;
CAR
customer_id (FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 자동차 테이블에서 고객을 참조하고 있다면 &lt;code&gt;CAR.customer_id&lt;/code&gt;가 가리키는 고객은 실제 &lt;code&gt;CUSTOMER&lt;/code&gt; 테이블에 존재해야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;자식 FK
&amp;rarr; 존재하지 않는 부모 PK를 참조하면 안 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 이미 자식 테이블에서 참조 중인 부모 키를 임의로 수정하거나 삭제하면 관계가 깨질 수 있으므로 제약조건이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. 정규화가 필요한 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 고객정보와 자동차정보가 하나의 테이블에 들어 있는 예제를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;고객정보
- 이름
- 주민번호
- 생년월일
- 전화번호
- 주소

자동차정보
- 차종
- 차명
- 년식
- 구입일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 자동차를 가지고 있지 않은 고객이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;자동차 없음

차종   &amp;rarr; NULL
차명   &amp;rarr; NULL
년식   &amp;rarr; NULL
구입일 &amp;rarr; NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 주제의 데이터를 하나의 테이블에 넣으면 불필요한 &lt;code&gt;NULL&lt;/code&gt;과 중복이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 테이블을 분리한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;개인신상
- 고객정보

자동차
- 차량정보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 두 테이블 사이에 관계를 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;CUSTOMER
    &amp;darr;
PK &amp;rarr; FK
    &amp;darr;
CAR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;하나의 테이블이 하나의 명확한 주제를 갖도록 데이터를 분리하는 과정&lt;/b&gt;이 정규화의 중요한 목적 중 하나이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. Relationship - 테이블 사이의 관계&lt;/h1&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;예를 들면:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서 &amp;harr; 사원
고객 &amp;harr; 자동차
고객 &amp;harr; 주문&lt;/code&gt;&lt;/pre&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;pre class=&quot;markdown&quot;&gt;&lt;code&gt;부모 테이블
+
자식 테이블&lt;/code&gt;&lt;/pre&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;pre class=&quot;&quot;&gt;&lt;code&gt;부모 테이블 PK
        &amp;darr;
자식 테이블 FK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 키가 전달된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 존재형 관계와 행위형 관계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계는 크게 두 가지 관점으로 살펴봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;존재의 유형&lt;/h2&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;부서
 &amp;darr;
사원

&quot;부서에는 사원이 존재한다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;행위의 유형&lt;/h2&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;고객
 &amp;darr;
주문

&quot;고객이 주문한다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모와 자식을 판단하기 어려운 경우에는 관계를 &lt;b&gt;문장으로 서술&lt;/b&gt;해보는 방법도 배웠다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;각 고객은 자동차를 소유한다.
각 자동차는 고객에 의해 소유된다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 주체는 고객이므로:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;고객
&amp;rarr; 부모

자동차
&amp;rarr; 자식&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 판단할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. 학사관리 ERD를 통해 관계 이해하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학사관리 시스템 ERD에서는 다음 Entity들이 등장했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;TB_DEPARTMENT
TB_STUDENT
TB_PROFESSOR
TB_CLASS
TB_CLASS_PROFESSOR
TB_GRADE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학과는 학생, 교수, 과목과 관계를 가진다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;TB_DEPARTMENT
DEPARTMENT_NO (PK)
        &amp;darr;
        ├─ TB_STUDENT.DEPARTMENT_NO
        ├─ TB_PROFESSOR.DEPARTMENT_NO
        └─ TB_CLASS.DEPARTMENT_NO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;학과 : 학생 = 1 : N
학과 : 교수 = 1 : N
학과 : 과목 = 1 : N&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. N:M 관계와 연결 테이블&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TB_CLASS&lt;/code&gt;와 &lt;code&gt;TB_PROFESSOR&lt;/code&gt; 사이에는 &lt;code&gt;TB_CLASS_PROFESSOR&lt;/code&gt;가 존재한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;TB_CLASS
     &amp;darr;
TB_CLASS_PROFESSOR
     &amp;uarr;
TB_PROFESSOR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TB_CLASS_PROFESSOR&lt;/code&gt;에는:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;CLASS_NO (FK)
PROFESSOR_NO (FK)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;다대다(N:M) 관계를 중간 연결 테이블을 이용해 표현한 구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TB_GRADE&lt;/code&gt;도 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학생
 &amp;darr;
TB_GRADE
 &amp;uarr;
과목&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;TB_GRADE
- TERM_NO
- CLASS_NO
- STUDENT_NO
- POINT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 학생과 수업을 연결하는 것뿐만 아니라 관계 자체가 &lt;code&gt;POINT&lt;/code&gt;라는 속성을 가지고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. MariaDB 실습 환경 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 SQL 실습을 위해 MariaDB를 설치했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;MariaDB 설치
        &amp;darr;
lgcns Database 생성
        &amp;darr;
DB Client 연결
        &amp;darr;
SQL 실습&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MariaDB&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB는 관계형 데이터베이스 관리 시스템(RDBMS)이다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;MariaDB
&amp;rarr; 오픈소스 RDBMS
&amp;rarr; MySQL과 높은 호환성
&amp;rarr; Table 기반 데이터 저장
&amp;rarr; SQL을 이용한 데이터 관리&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HeidiSQL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 화면에서는 HeidiSQL을 통해 MariaDB에 접속했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HeidiSQL
&amp;rarr; MariaDB / MySQL 등을 관리하는 GUI Client
&amp;rarr; Database / Table 확인
&amp;rarr; SQL 작성 및 실행
&amp;rarr; 데이터 조회 및 관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;MariaDB
&amp;rarr; 실제 데이터를 저장&amp;middot;관리하는 DBMS

HeidiSQL / DBeaver
&amp;rarr; DBMS에 접속하여 SQL을 실행하는 Client Tool&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 구분할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. SQL의 종류&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 SQL을 다음과 같이 구분했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;DDL
&amp;rarr; 정의

DML
&amp;rarr; 조작

DCL
&amp;rarr; 제어

SELECT
&amp;rarr; 데이터 검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그중에서도 데이터를 조회하는 &lt;b&gt;SELECT&lt;/b&gt;를 집중적으로 실습했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. SELECT 기본 문법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT [DISTINCT] 컬럼명 | * | 표현식 및 함수 [AS] 별칭
FROM 테이블명
[WHERE 행의 조건]
[GROUP BY 그룹 기준]
[HAVING 그룹 조건]
[ORDER BY 정렬 기준 ASC | DESC];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 절의 역할은:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구문&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;조회할 컬럼 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DISTINCT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;중복 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;조회 대상 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;행 조건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그룹화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그룹 조건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;결과 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. SELECT 기본 조회 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 컬럼을 조회할 때는 &lt;code&gt;*&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 컬럼만 선택할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT emp_id,
       emp_name
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT *
&amp;rarr; 모든 Column

SELECT emp_id, emp_name
&amp;rarr; 필요한 Column만&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. WHERE - 원하는 행만 검색하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 부서번호가 90인 사원을 검색했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE dept_id = '90';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;employee 전체 데이터
        &amp;darr;
DEPT_ID = '90'
        &amp;darr;
90번 부서 사원만 조회&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. AND - 여러 조건을 모두 만족&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;부서번호가 90번이고
급여가 4,000,000 이상인 사원의
모든 정보를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE DEPT_ID = '90'
  AND SALARY &amp;gt;= 4000000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AND&lt;/code&gt;는 두 조건을 &lt;b&gt;모두 만족해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;조건 A
 AND
조건 B
 &amp;darr;
둘 다 TRUE인 행&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. OR - 여러 조건 중 하나라도 만족&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;부서번호가 60번이거나
90번인 사원을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE DEPT_ID = '60'
   OR DEPT_ID = '90';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;A OR B
&amp;rarr; 둘 중 하나라도 TRUE이면 조회&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. BETWEEN - 범위 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;급여가 3,500,000 이상
5,500,000 이하인 사원의
이름, 급여, 직급을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.EMP_NAME,
       e.SALARY,
       j.JOB_TITLE
FROM employee e,
     job j
WHERE e.JOB_ID = j.JOB_ID
  AND e.SALARY BETWEEN 3500000 AND 5500000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BETWEEN&lt;/code&gt;은 양 끝값을 포함한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;BETWEEN 3500000 AND 5500000

3500000 &amp;lt;= SALARY &amp;lt;= 5500000&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. CONCAT() - 문자열 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CONCAT()&lt;/code&gt;을 이용해 여러 문자열을 하나로 연결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CONCAT(
    EMP_NAME,
    '님의 급여는 ',
    SALARY,
    '(원)입니다.'
) AS 급여정보
FROM employee;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같은 문장 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;한선기님의 급여는 9000000(원)입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;AS&lt;/code&gt;를 사용해 결과 컬럼의 별칭을 지정할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. LIKE / NOT LIKE - 패턴 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 패턴 검색에는 &lt;code&gt;LIKE&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;%
&amp;rarr; 0개 이상의 문자열

_
&amp;rarr; 정확히 한 글자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;김씨 성을 가지는 사원의
모든 정보를 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE EMP_NAME LIKE '김%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 김씨가 아닌 사원은:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE EMP_NAME NOT LIKE '김%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. LIKE에서 실제 &lt;code&gt;_&lt;/code&gt; 문자 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;메일 ID에서
_ 앞의 문자가 정확히 3자리인
사원을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;_&lt;/code&gt; 자체가 LIKE에서는 한 글자를 의미하는 와일드카드이므로 실제 &lt;code&gt;_&lt;/code&gt;를 검색하려면 Escape가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE EMAIL LIKE '___!_%'
ESCAPE '!';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 해석하면:&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;___
&amp;rarr; 정확히 3글자

!_
&amp;rarr; 실제 &quot;_&quot; 문자

%
&amp;rarr; 이후 문자열&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. NULL 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서 &lt;code&gt;NULL&lt;/code&gt;은 일반 값과 다르기 때문에:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;= NULL      ❌
IS NULL     ✅

!= NULL     ❌
IS NOT NULL ✅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 Q)&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부서 배치를 받지 않은 사원은?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employee
WHERE DEPT_ID IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. Index를 이용한 데이터 접근&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index를 이용한 데이터 접근 방식도 개념적으로 확인했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQL 조건
   &amp;darr;
INDEX KEY 검색
   &amp;darr;
ROWID 확인
   &amp;darr;
실제 TABLE Row 접근
   &amp;darr;
결과 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 전체 Table을 처음부터 확인하는 것이 아니라 Index를 통해 필요한 데이터의 위치를 찾고 해당 Row에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Index의 세부 구조보다는 &lt;b&gt;Index가 데이터 접근 경로를 제공한다는 개념&lt;/b&gt;을 중심으로 이해했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  Workbook Basic SELECT 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 춘 대학교 교육시스템의 테이블을 이용해 Basic SELECT 문제도 직접 풀었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q1. 학과 이름과 계열 출력&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DEPARTMENT_NAME AS '학과 명',
       CATEGORY AS '계열'
FROM tb_department;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 포인트&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;AS
&amp;rarr; 조회 결과 Column의 별칭 설정&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q2. 학과별 정원 문장으로 출력&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CONCAT(
    DEPARTMENT_NAME,
    '의 정원은 ',
    CAPACITY,
    '명 입니다.'
) AS '학과별 정원'
FROM tb_department;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;국어국문학과의 정원은 20명 입니다.
영어영문학과의 정원은 36명 입니다.
사학과의 정원은 28명 입니다.
...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 포인트&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Column
+
문자열
+
Column
        &amp;darr;
CONCAT()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q3. 국어국문학과 여학생 중 현재 휴학생 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 학과번호를 조회했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DEPARTMENT_NO
FROM TB_DEPARTMENT
WHERE DEPARTMENT_NAME = '국어국문학과';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 학생 테이블에서 조건을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT a.STUDENT_NAME
FROM tb_student a
WHERE 1 = 1
  AND a.ABSENCE_YN = 'Y'
  AND a.DEPARTMENT_NO = '001'
  AND (
       a.STUDENT_SSN LIKE '______-2%'
    OR a.STUDENT_SSN LIKE '______-4%'
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는 여러 개의 조건이 동시에 사용되었다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;국어국문학과
+
휴학생
+
여학생&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. WHERE 1=1&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q3 실습 중 &lt;code&gt;WHERE 1=1&lt;/code&gt; 패턴도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WHERE 1 = 1
  AND 조건1
  AND 조건2
  AND 조건3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;1=1&lt;/code&gt;은 항상 TRUE이므로 결과에는 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 조건을 계속 추가할 때:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;AND 조건
AND 조건
AND 조건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 작성하기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 동적 SQL을 구성할 때 사용할 수 있는 패턴이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. AND와 OR의 우선순위 주의&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 작성하면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WHERE 조건1
  AND 조건2
  OR 조건3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도와 다르게 처리될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 하나의 조건 그룹으로 처리해야 할 &lt;code&gt;OR&lt;/code&gt;들은 괄호로 묶는 것이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WHERE 1 = 1
  AND a.ABSENCE_YN = 'Y'
  AND (
       조건1
    OR 조건2
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Q3에서 조건식을 작성하면서 &lt;b&gt;SQL에서도 논리 연산자 우선순위를 고려해야 한다는 점&lt;/b&gt;을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q4. 지정된 학번의 학생 조회&lt;/h2&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;SELECT STUDENT_NAME
FROM tb_student
WHERE STUDENT_NO IN (
    'A513079',
    'A513090',
    'A513091',
    'A513110',
    'A513119'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 5명의 학생이 조회되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 포인트&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;IN
&amp;rarr; 여러 후보 중 하나와 일치하는지 검사&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 여러 &lt;code&gt;OR&lt;/code&gt;를 작성하는 것보다 간결하다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;STUDENT_NO = 'A513079'
OR STUDENT_NO = 'A513090'
OR ...&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q5. 정원이 20명 이상 30명 이하인 학과&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DEPARTMENT_NAME,
       CATEGORY
FROM tb_department
WHERE CAPACITY BETWEEN 20 AND 30;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BETWEEN
&amp;rarr; 범위 검색
&amp;rarr; 시작값과 끝값 포함&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q6. 소속 학과가 없는 교수 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총장을 제외한 모든 교수가 학과에 소속되어 있다는 조건을 이용했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT PROFESSOR_NAME
FROM tb_professor
WHERE DEPARTMENT_NO IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;임해정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 소속 학과가 없는 교수가 총장임을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q7. 학과가 지정되지 않은 학생 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 조회가 아니라 몇 명인지 확인하기 위해 &lt;code&gt;COUNT(*)&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT COUNT(*) AS '학생 수'
FROM TB_STUDENT st
WHERE st.DEPARTMENT_NO IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;학생 수
0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 데이터에는 학과가 지정되지 않은 학생이 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에서는 다음 함수도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ISNULL(DEPARTMENT_NO)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;IS NULL
&amp;rarr; SQL 조건식

ISNULL()
&amp;rarr; MariaDB/MySQL 함수&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q8. 선수과목이 존재하는 과목 조회&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT CLASS_NO
FROM tb_class
WHERE PREATTENDING_CLASS_NO IS NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;C0405500
C0409000
C0432500
C3051900
C3221500
C3745400&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 6개의 과목이 조회되었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;IS NULL
&amp;rarr; 값 없음

IS NOT NULL
&amp;rarr; 값 있음&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q9. 대학에 존재하는 계열 조회&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DISTINCT CATEGORY
FROM tb_department;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;인문사회
자연과학
공학
의학
예체능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DISTINCT&lt;/code&gt;를 사용하여 중복을 제거한 결과 5개의 계열이 조회되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q10. 02학번 전주 거주 재학생 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;휴학생 제외
+
전주 거주
+
2002년도 입학&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 SQL:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT a.STUDENT_NO,
       a.STUDENT_NAME,
       a.STUDENT_SSN
FROM tb_student a
WHERE 1 = 1
  AND a.ABSENCE_YN = 'N'
  AND a.STUDENT_ADDRESS LIKE '%전주%'
  AND a.ENTRANCE_DATE LIKE '2002-%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과 총 &lt;b&gt;11건&lt;/b&gt;이 조회되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 다음과 같은 &lt;code&gt;ORDER BY&lt;/code&gt; 표현도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;ORDER BY 2 ASC, 1, 3 DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 결과의 컬럼 순번을 기준으로 보면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2 ASC
&amp;rarr; STUDENT_NAME 오름차순

1
&amp;rarr; STUDENT_NO 오름차순

3 DESC
&amp;rarr; STUDENT_SSN 내림차순&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 정렬할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 SQL 실습 핵심 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 사용한 조회 조건을 한 번에 정리하면:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
&amp;rarr; 데이터 조회

AS
&amp;rarr; 출력 컬럼 별칭

CONCAT()
&amp;rarr; 문자열 연결

WHERE
&amp;rarr; 행 조건

AND
&amp;rarr; 모든 조건 만족

OR
&amp;rarr; 하나 이상의 조건 만족

BETWEEN
&amp;rarr; 범위 검색

IN
&amp;rarr; 여러 값 중 하나와 일치

LIKE
&amp;rarr; 문자열 패턴 검색

NOT LIKE
&amp;rarr; 패턴과 일치하지 않는 데이터

%
&amp;rarr; 0개 이상의 문자열

_
&amp;rarr; 정확히 한 글자

IS NULL
&amp;rarr; 값이 없는 데이터

IS NOT NULL
&amp;rarr; 값이 존재하는 데이터

DISTINCT
&amp;rarr; 중복 제거

COUNT(*)
&amp;rarr; 행의 개수 계산&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Q3 - 한 가지 조건이 아닌 여러 조건을 조합해 조회하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;국어국문학과
+
여학생
+
휴학생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 세 가지 조건을 동시에 만족하는 학생을 찾아야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 하나의 조건만 생각하면 간단했지만, 실제 문제에서는 여러 Column의 조건을 조합해야 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT DEPARTMENT_NO
FROM TB_DEPARTMENT
WHERE DEPARTMENT_NAME = '국어국문학과';&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과를 이용해 학생 조회 조건을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE 1 = 1
AND ABSENCE_YN = 'Y'
AND DEPARTMENT_NO = '001'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 주민번호 패턴을 이용한 조건을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;AND (
     STUDENT_SSN LIKE '______-2%'
  OR STUDENT_SSN LIKE '______-4%'
)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 문장을 바로 SQL로 옮기려고 하기보다 요구사항을:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학과 조건
휴학 조건
성별 조건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 작은 단위로 나누고 하나씩 &lt;code&gt;WHERE&lt;/code&gt;에 추가하면 훨씬 이해하기 쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 &lt;code&gt;AND&lt;/code&gt;와 &lt;code&gt;OR&lt;/code&gt;를 함께 사용할 때는 괄호를 이용해 &lt;b&gt;조건의 논리적 범위&lt;/b&gt;를 명확하게 만들어야 한다는 점도 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 여러 값을 비교할 때 OR 대신 IN 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장기 연체자 5명을 찾는 문제에서는:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;STUDENT_NO = 'A513079'
OR STUDENT_NO = 'A513090'
OR ...&lt;/code&gt;&lt;/pre&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;pre class=&quot;gams&quot;&gt;&lt;code&gt;WHERE STUDENT_NO IN (...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 바꾸니 훨씬 읽기 쉬운 SQL이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 SQL에서도 단순히 실행되는 코드를 만드는 것뿐만 아니라 &lt;b&gt;조건을 어떤 형태로 표현해야 더 읽기 좋은지&lt;/b&gt;도 중요하다는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 내용 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업은 단순히 &lt;code&gt;SELECT&lt;/code&gt; 문법만 배우는 시간이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;Database
&amp;rarr; DBMS
&amp;rarr; RDBMS
&amp;rarr; Table
&amp;rarr; Entity
&amp;rarr; Relationship
&amp;rarr; PK / FK
&amp;rarr; Referential Integrity
&amp;rarr; Normalization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 데이터베이스의 전체적인 구조를 먼저 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실제 MariaDB에 데이터를 구성하고 SQL을 작성하면서 이론을 데이터 조회와 연결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 관계형 데이터베이스에서 테이블을 무작정 하나로 합치는 것이 아니라:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;하나의 명확한 주제
&amp;rarr; 하나의 Table

관련된 다른 데이터
&amp;rarr; 다른 Table

Table 연결
&amp;rarr; PK / FK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 설계 방향이 중요하다는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서는 처음에는:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM table;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 단순한 조회에서 시작했지만 점차:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE
&amp;rarr; 조건

AND / OR
&amp;rarr; 조건 조합

BETWEEN
&amp;rarr; 범위

IN
&amp;rarr; 여러 값

LIKE
&amp;rarr; 패턴

NULL
&amp;rarr; 값의 존재 여부

DISTINCT
&amp;rarr; 중복 제거&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 조건을 구체적으로 표현할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Workbook 문제를 직접 풀어보면서 각각의 연산자를 따로 외우는 것보다 &lt;b&gt;&amp;ldquo;어떤 데이터를 찾고 싶은가?&amp;rdquo;에 따라 적절한 조건식을 선택하는 것이 중요하다&lt;/b&gt;는 점을 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘부터 본격적으로 데이터베이스 수업이 시작됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 하면서 PostgreSQL이나 JPA를 사용해 본 경험은 있었지만, 오늘처럼 데이터베이스 자체의 개념부터 관계와 무결성, 정규화까지 순서대로 다시 학습하니 이전에 사용했던 기술들이 왜 그런 구조를 가지고 있었는지 조금 더 명확하게 연결됐다.&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;처음 고객과 자동차 정보를 하나의 테이블에 넣었을 때는 단순히 컬럼이 많아지는 정도의 문제로 보였지만, 자동차가 없는 고객 때문에 여러 &lt;code&gt;NULL&lt;/code&gt; 값이 발생하는 예제를 보면서 서로 다른 주제의 데이터가 한 테이블에 섞였을 때 어떤 문제가 생기는지 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;고객
+
자동차&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블로 분리하고:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;부모 PK
&amp;rarr; 자식 FK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 관계를 만드는 과정에서 이전에 ERD를 그릴 때 사용했던 PK와 FK의 의미도 다시 정리할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 실습에서는 &lt;code&gt;WHERE&lt;/code&gt; 조건을 작성하는 것이 생각보다 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히:&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE DEPARTMENT_NO = '001'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 하나의 조건만 작성하는 것은 어렵지 않았지만, Workbook Q3처럼:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;국어국문학과
AND
휴학생
AND
(
    여성 조건1
    OR
    여성 조건2
)&lt;/code&gt;&lt;/pre&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;또 Workbook Basic SELECT Q1~Q10을 직접 실행하고 결과까지 확인하면서:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;문제에서 조건 추출
        &amp;darr;
필요한 Table 확인
        &amp;darr;
조회할 Column 결정
        &amp;darr;
WHERE 조건 작성
        &amp;darr;
SQL 실행
        &amp;darr;
결과 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 문제 해결 흐름을 반복할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 JOIN이나 GROUP BY, Subquery 등을 배우면 지금은 여러 단계로 조회했던 데이터도 하나의 SQL로 처리하게 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 우선 &lt;code&gt;SELECT&lt;/code&gt;의 기본기를 확실하게 잡고, 특히 &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;LIKE&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;, &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;DISTINCT&lt;/code&gt;를 문제 상황에 맞게 선택할 수 있도록 복습해야겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  다음 학습 방향&lt;/h1&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 기본 조회 복습
        &amp;darr;
WHERE 조건식 익숙해지기
        &amp;darr;
LIKE / IN / BETWEEN 정확히 구분
        &amp;darr;
ORDER BY
        &amp;darr;
SQL 함수
        &amp;darr;
GROUP BY / HAVING
        &amp;darr;
JOIN
        &amp;darr;
Subquery
        &amp;darr;
DDL / DML&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 수업부터 여러 테이블을 함께 조회하게 된다면 오늘 배운:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Entity
Relationship
PK
FK
참조 무결성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념을 SQL의 &lt;code&gt;JOIN&lt;/code&gt;과 연결해서 이해하는 것을 목표로 해야겠다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>database</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>LGCNS6기TIL</category>
      <category>mariadb</category>
      <category>SQL</category>
      <category>til</category>
      <category>내일배움카드</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/41</guid>
      <comments>https://bbbicsan.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 24 Aug 2026 18:02:53 +0900</pubDate>
    </item>
    <item>
      <title>[ADsP] 26년도 50회 ADsP 합격 후기   (Feat. 5일 벼락치기) &amp;amp; 공부법&amp;middot;교재&amp;middot;꿀팁</title>
      <link>https://bbbicsan.tistory.com/40</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;2026년 8월 8일, &lt;b&gt;제50회 ADsP 데이터분석 준전문가 시험&lt;/b&gt;에 응시했고 최종 &lt;b&gt;68점으로 합격&lt;/b&gt;했다.  &lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfFUg9/dJMcacEmnVq/fFv77mxcz6KwYGBYTubvL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfFUg9/dJMcacEmnVq/fFv77mxcz6KwYGBYTubvL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfFUg9/dJMcacEmnVq/fFv77mxcz6KwYGBYTubvL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfFUg9%2FdJMcacEmnVq%2FfFv77mxcz6KwYGBYTubvL0%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;2252&quot; height=&quot;4000&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 : 16 / 20
2과목 : 16 / 20
3과목 : 36 / 60

총점  : 68 / 100
결과  : 합격 ✅&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIoh2T/dJMcabk8duj/UOhUUmPArxRRHV5ftIuo2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIoh2T/dJMcabk8duj/UOhUUmPArxRRHV5ftIuo2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIoh2T/dJMcabk8duj/UOhUUmPArxRRHV5ftIuo2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIoh2T%2FdJMcabk8duj%2FUOhUUmPArxRRHV5ftIuo2K%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;1080&quot; height=&quot;1058&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수만 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;생각보다 안정적으로 합격했네?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;  &amp;ldquo;이거 진짜 간당간당하게 붙거나 떨어지겠는데?&amp;rdquo;&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50회 ADsP를 직접 응시한 입장에서 느낀 체감 난이도는 &lt;b&gt;정말 높았다.&lt;/b&gt;&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;개인적으로는 기존의&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;ADsP는 대충 준비해도 합격할 수 있는 시험&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 인식에서 조금 벗어나려는 시도가 느껴진 회차였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 &lt;b&gt;전공자 기준 총 5일&lt;/b&gt;을 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기출과 모의고사를 풀 때는 못 봐도 70점대, 대부분 &lt;b&gt;80점 이상&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;시험 이틀 정도 뒤 인터넷에 올라온 복원문제로 보수적으로 가채점했을 때는 약 &lt;b&gt;64점 정도&lt;/b&gt;가 나왔고, 결과 발표 전까지는 생각보다 꽤 불안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 최종적으로는 &lt;b&gt;68점으로 합격&lt;/b&gt;했고, 이번 글에서는 내가 실제로 했던&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;아답터 요약강의
        &amp;darr;
미어캣책 개념 1회독
        &amp;darr;
기출문제
        &amp;darr;
모의고사
        &amp;darr;
오답 + 개념 리마인드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부법과 함께 &lt;b&gt;전공자 기준 5일 벼락치기 공부법 + 실제 시험 후기 + 나만의 ADsP 꿀팁&lt;/b&gt;을 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  목차&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 내용을 누르면 해당 위치로 바로 이동할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#difficulty&quot;&gt;  50회 ADsP, 체감 난이도는?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#background&quot;&gt; &amp;zwj;  먼저, 내 공부 베이스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#materials&quot;&gt;  사용한 공부 자료&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#five-day-plan&quot;&gt;  내가 실제로 한 5일 공부법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#step1-adapter&quot;&gt;  STEP 1. 아답터 요약강의로 전체 흐름 잡기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#step2-meerkat&quot;&gt;  STEP 2. 미어캣책으로 개념 1회독&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#step3-past&quot;&gt;  STEP 3. 기출&amp;middot;모의고사 + 오답 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#exam-review&quot;&gt; ️ 실제 50회 시험 후기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#result&quot;&gt;  실제 시험 결과&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tips&quot;&gt;  직접 준비하면서 느낀 ADsP 꿀팁 4가지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#summary&quot;&gt;✅ 최종 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;difficulty&quot;&gt;  50회 ADsP, 체감 난이도는?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50회 ADsP 시험을 응시한 분들이나 이후 시험을 준비하면서 50회 후기를 찾아본 분들이라면 어느 정도 공감할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 체감으로는,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  50회 시험은 정말 어려웠다.&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 나중에 복원된 문제를 다시 차분하게 풀어보면 시험장에서 느꼈던 것만큼 어렵지 않다고 생각할 수도 있다.&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;특히 크게 느낀 점은 세 가지였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① 기존 기출에서 자주 보지 못했던 지엽적인 내용

② 처음 보는 유형의 문제

③ 끝까지 헷갈리게 만드는 선지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 ADsP에 대해 찾아보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;기출 몇 번 돌리면 된다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;며칠이면 충분하다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;생각보다 쉬운 자격증이다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&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;그런데 50회에서는 단순히 기존 기출의 정답이나 자주 나오는 개념만 외우는 방식으로는 당황할 만한 문제가 꽤 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 이번 50회는 기존의 &lt;b&gt;&amp;ldquo;ADsP는 대충 준비해도 합격한다&amp;rdquo;&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;그래서 이번 시험을 통해 느낀 건,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞으로 ADsP를 준비한다면 빈출 개념은 확실하게 잡되, 기본서의 세부 내용도 최소한 한 번은 읽어보는 것이 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;background&quot;&gt; &amp;zwj;  먼저, 내 공부 베이스&lt;/h1&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;내 기준은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style7&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;전공 여부&lt;/td&gt;
&lt;td&gt;전공자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;준비기간&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5일&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시험&lt;/td&gt;
&lt;td&gt;제50회 ADsP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기출&amp;middot;모의고사&lt;/td&gt;
&lt;td&gt;못 보면 70점대, 대부분 &lt;b&gt;80점 이상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 시험&lt;/td&gt;
&lt;td&gt;체감 난이도 매우 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복원 가채점&lt;/td&gt;
&lt;td&gt;보수적으로 약 &lt;b&gt;64점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 결과&lt;/td&gt;
&lt;td&gt;&lt;b&gt;68점 합격&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험을 준비하면서 기출과 모의고사에서는 점수가 꽤 안정적으로 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;못 봤다고 생각한 시험도 70점 이상, 대부분은 &lt;b&gt;80점 이상&lt;/b&gt;을 유지했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 솔직히 시험을 보기 전에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;설마 떨어지겠어?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 생각이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 시험이 끝난 직후에는 정반대였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;와... 이거 진짜 간당간당하게 붙거나 떨어질 수도 있겠다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;기출&amp;middot;모의고사와 실제 50회 시험의 체감 차이가 컸다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;materials&quot;&gt;  사용한 공부 자료&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시험에서는 크게 두 가지 자료를 사용했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;① 2026 이지패스 ADsP 데이터분석 준전문가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 &lt;b&gt;미어캣책&lt;/b&gt;이라고 부르는 교재다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;② 유튜버 아답터 ADsP 요약강의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 둘 다 추천한다.&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 자료의 역할이 서로 다르기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;아답터 요약강의
        &amp;darr;
전체 흐름 + 빈출 개념 빠르게 파악

미어캣책
        &amp;darr;
세부 개념 + 빈출도 + 기출 포인트 보완

기출 / 모의고사
        &amp;darr;
실제 문제 유형 적응 + 부족한 개념 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 준비기간이 5일밖에 없었기 때문에 처음부터 기본서만 정독하기보다는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약강의로 전체 구조를 먼저 파악한 뒤 기본서를 읽는 방식&lt;/b&gt;이 상당히 효율적이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;five-day-plan&quot;&gt;  내가 실제로 한 5일 공부법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 공부했던 5일을 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[1일차]

아답터 요약강의 전체 수강
        &amp;darr;
미어캣책 1과목 개념 1회독


[2일차]

미어캣책 2과목
        &amp;darr;
3과목 약 1/3까지 개념 1회독


[3일차]

미어캣책 3과목
나머지 약 2/3 개념 1회독


[4일차]

기출문제 약 3회분
        &amp;darr;
오답 정리
        &amp;darr;
문제를 통해 개념 리마인드


[5일차]

제공 모의고사 약 3회분
        &amp;darr;
오답 정리
        &amp;darr;
부족한 개념 최종 복습


[시험 당일]

아침 + 시험장 이동
        &amp;darr;
아답터 요약강의 재청취
        &amp;darr;
특히 1&amp;middot;2과목 집중 복습&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3일 개념 + 2일 기출&amp;middot;모의고사&lt;/b&gt;&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5일이라는 시간이 짧았기 때문에 모든 개념을 완벽하게 암기하려고 하기보다는,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전체 흐름 파악
        &amp;darr;
기본서로 빈틈 채우기
        &amp;darr;
문제를 풀며 자주 나오는 개념 확인
        &amp;darr;
오답으로 다시 개념 복습&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사이클을 만드는 데 집중했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;step1-adapter&quot;&gt;  STEP 1. 아답터 요약강의로 전체 흐름 잡기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50회 시험이 어렵게 나온 이후, 아답터 요약강의에 대해&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이제 아답터 요약강의 의미 없어진 거 아니야?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;50회에서는 강의에서 안 나온 게 너무 많았다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&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;실제로 이번 50회는 상당히 지엽적으로 출제됐기 때문에 &lt;b&gt;아답터 요약강의에서 다루지 않은 내용도 분명히 있었다.&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;오히려 반대로,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 짧은 시간 안에 가장 자주 출제되는 개념들을 중심으로 전체 범위를 한 바퀴 돌 수 있다는 점&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서는 여전히 상당히 좋은 자료라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 준비기간이 짧다면 더더욱 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 강의를 들을 때의 목표는 모든 내용을 외우는 것이 아니었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;ldquo;1과목에서는 이런 내용을 배우는구나.&amp;rdquo;

&amp;ldquo;2과목에서는 이런 개념들이 중요하구나.&amp;rdquo;

&amp;ldquo;3과목은 이런 분석 방법들이 나오는구나.&amp;rdquo;&lt;/code&gt;&lt;/pre&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;그리고 개인적으로는 아답터 요약강의의 핵심 내용을 제대로 숙지한다면 &lt;b&gt;1&amp;middot;2과목에서 기본 점수를 확보하는 데 상당한 도움이 된다&lt;/b&gt;고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 1&amp;middot;2과목은 핵심 개념을 묻는 문제가 많기 때문에 시험 직전 복습 자료로 활용하기에도 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 시험 당일 아침과 시험장 이동 중에도 &lt;b&gt;1&amp;middot;2과목 위주로 요약강의를 한 번 더 들었다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  짧은 시간에 전체 흐름과 빈출 개념을 잡는 용도로는 여전히 강력하게 추천한다.&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;step2-meerkat&quot;&gt;  STEP 2. 미어캣책으로 개념 1회독&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약강의로 전체 흐름을 잡은 뒤에는 &lt;b&gt;미어캣책으로 개념을 1회독&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADsP를 준비하는 사람들이 많이 사용하는 이유를 알 수 있을 정도로 개인적으로 만족도가 높은 교재였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 좋았던 점은,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;개념 설명
+
중요 개념
+
빈출 포인트
+
기존 기출 관련 내용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 함께 볼 수 있다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 50회처럼 지엽적인 시험을 경험하고 나니 더욱 강조하고 싶은 부분이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기본서 구석에 있는 내용도 가능하면 한 번씩 읽어보자.&lt;/h2&gt;
&lt;/blockquote&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;MapReduce&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;이번 50회에서 MapReduce와 관련된 문제가 출제됐는데, 기존 기출에서는 상대적으로 자주 접했던 주제가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 미어캣책에서 해당 내용을 읽었던 기억이 남아 있었고, 시험장에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;잠깐, 이거 책에서 봤던 내용인데?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 기억을 활용해 결국 문제를 맞힐 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험 때문에 ADsP도 앞으로는 단순히 빈출 개념만 반복하기보다 &lt;b&gt;기본서의 세부 내용을 최소 한 번은 눈에 익히는 것&lt;/b&gt;을 추천하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 정의를 전부 외우지 못하더라도,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;본 적이 있다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 기억 자체가 시험에서 생각보다 큰 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 50회처럼 지엽적인 문제가 섞이는 시험에서는 이 차이가 더 크게 느껴졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;step3-past&quot;&gt;  STEP 3. 기출&amp;middot;모의고사 + 오답 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념 공부를 마친 뒤에는 바로 기출문제로 넘어갔다.&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;ADsP 역시 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기출을 반복하다 보면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;어떤 개념이 자주 나오는지

어떤 방식으로 질문하는지

어떤 선지가 자주 헷갈리게 나오는지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 조금씩 보이기 시작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  4일차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기출문제 약 3회분 + 오답 정리&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  5일차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제공 모의고사 약 3회분 + 오답 정리&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;여기서 중요한 것은 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;오늘 82점 나왔다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하고 점수만 확인하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 문제를 틀렸다면 해당 문제의 개념을 다시 확인하면서 &lt;b&gt;문제를 통해 개념을 리마인드하는 방식&lt;/b&gt;으로 공부했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;문제 풀이
   &amp;darr;
오답 확인
   &amp;darr;
왜 틀렸는지 확인
   &amp;darr;
관련 개념 복습
   &amp;darr;
다음 문제에 적용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기출문제는 단순한 실전 연습이 아니라,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내 개념 공부에서 비어 있는 부분을 찾아내는 도구&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;기출문제를 통해 공부 범위를 다시 압축하는 과정&lt;/b&gt;도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 기본서를 볼 때는 모든 내용이 중요해 보이지만, 기출을 반복하다 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;아, 이 부분은 이런 식으로 자주 물어보는구나.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 감이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때부터 공부 효율이 훨씬 좋아졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;exam-review&quot;&gt; ️ 실제 50회 시험 후기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 전까지만 해도 나는 ADsP 불합격을 크게 걱정하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기출과 모의고사에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;못 보면 70점대, 대부분 80점 이상&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 꾸준하게 나왔기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 50회 시험은 예상과 완전히 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 풀면서 계속 느낀 건,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;ldquo;이런 것도 나와?&amp;rdquo;

&amp;ldquo;이건 기출에서 거의 못 본 것 같은데?&amp;rdquo;

&amp;ldquo;두 선지가 왜 이렇게 둘 다 맞는 것 같지?&amp;rdquo;&lt;/code&gt;&lt;/pre&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &amp;ldquo;이거 진짜 간당간당하게 붙거나 떨어지겠다.&amp;rdquo;&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 후 약 이틀 정도 뒤 인터넷에 올라온 복원문제를 바탕으로 가채점도 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 최대한 &lt;b&gt;보수적으로 채점&lt;/b&gt;했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;내가 어떤 답을 골랐는지 기억이 안 남
        &amp;darr;
오답 처리

복원 정답에 대한 의견이 갈림
        &amp;darr;
오답 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 애매한 문제는 전부 틀린 것으로 계산했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 약&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;64점 정도&lt;/b&gt;&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 나왔던 것으로 기억한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격선은 넘었지만 워낙 시험 체감이 좋지 않았고 복원문제의 정답도 완전히 확정된 상태가 아니었기 때문에 결과 발표 전까지는 조금 불안했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;result&quot;&gt;  실제 시험 결과&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 최종 결과는,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 : 16 / 20
2과목 : 16 / 20
3과목 : 36 / 60

──────────────

총점 : 68점

✅ 합격&lt;/code&gt;&lt;/pre&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;68점으로 안정적으로 합격&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 내가 시험 직전에 집중했던 &lt;b&gt;1&amp;middot;2과목에서 각각 16점씩 확보한 것&lt;/b&gt;이 전체 점수를 만드는 데 꽤 큰 역할을 했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 16점
+
2과목 16점
=
32점 확보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50회 시험을 보고 가채점까지 했을 때는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;내 공부 방법이 잘못된 건가?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요약강의
   &amp;darr;
기본서
   &amp;darr;
기출
   &amp;darr;
모의고사
   &amp;darr;
오답 정리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부법 자체는 충분히 유효했다고 생각한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;tips&quot;&gt;&lt;b&gt;  직접 준비하면서 느낀 ADsP 꿀팁 4가지&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터는 이번 시험을 준비하고 직접 응시하면서 느낀 &lt;b&gt;개인적인 ADsP 공략법&lt;/b&gt;을 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  1. 과목 과락을 지나치게 걱정하지 말자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADsP는 각 과목에서 &lt;b&gt;40% 미만을 받으면 과락&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;특히 가장 많이 걱정하는 것이 &lt;b&gt;3과목&lt;/b&gt;이다.&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;하지만 직접 공부하고 시험까지 본 입장에서는,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;과락 자체를 너무 무서워할 필요는 없다고 생각한다.&lt;/b&gt;&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 1&amp;middot;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;3과목은 확실히 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 3과목은 &lt;b&gt;30문제&lt;/b&gt;로 문제 수가 가장 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과락을 피하려면 40% 이상이 필요하므로 최소 &lt;b&gt;12문제 이상&lt;/b&gt;을 맞혀야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3과목 총 문제 : 30문제

과락 기준
&amp;rarr; 40% 이상

필요 정답 수
&amp;rarr; 최소 12문제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 12문제가 많아 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 3과목에도 개념만 알고 있으면 풀 수 있는 문제, 어렵지 않은 문제, 데이터 해석 문제 등 &lt;b&gt;충분히 점수를 확보할 수 있는 유형&lt;/b&gt;이 섞여 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 문제 수가 30문제로 많다는 것도 오히려 장점이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모르는 문제가 조금 있더라도 나머지 문제에서 점수를 확보하거나, 완전히 모르는 문제에서도 선지를 줄여 찍어서 맞힐 가능성이 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결론은,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  3과목에서 고득점을 노리기보다 우선 과락을 피하는 전략으로 접근하자.&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과락을 지나치게 걱정하면서 3과목 하나에 모든 시간을 사용하는 것보다 &lt;b&gt;전체 점수 전략을 짜는 것이 더 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  2. 1&amp;middot;2과목에서 최대한 점수를 확보하자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 팁과 바로 이어지는 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADsP에서 가장 안정적인 합격 전략 중 하나는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1&amp;middot;2과목에서 최대한 점수를 확보하고 3과목에서 과락을 피하는 것&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;공부해보면 1&amp;middot;2과목은 3과목과 비교했을 때 확실히 접근하기 쉽다.&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;실제로 내 점수도&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 : 16 / 20
2과목 : 16 / 20

&amp;rarr; 두 과목에서 총 32점 확보&lt;/code&gt;&lt;/pre&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;예를 들어 1&amp;middot;2과목에서 각각 8문제씩 맞혔다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 : 8문제 정답 &amp;rarr; 16점
2과목 : 8문제 정답 &amp;rarr; 16점

총 32점 확보&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격에 필요한 점수가 60점이라면, 남은 3과목에서 &lt;b&gt;28점&lt;/b&gt;만 추가로 확보하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3과목은 한 문제당 2점이므로,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;28점 &amp;divide; 2점
= 14문제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 30문제 중 &lt;b&gt;14문제&lt;/b&gt;를 맞히면 전체 60점을 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1과목 8문제
+
2과목 8문제
+
3과목 14문제

= 60점&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 계산해보면 생각보다 합격 전략이 명확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 시험 당일에도 3과목 전체를 다시 보는 것보다 &lt;b&gt;아답터 요약강의의 1&amp;middot;2과목을 우선적으로 복습&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 1&amp;middot;2과목에서는 아직도 아답터 요약강의의 효과가 상당히 크다고 느꼈다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1&amp;middot;2과목은 최대한 확실하게, 3과목은 과락을 피하면서 추가 점수를 확보하자.&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단기 준비에서는 특히 추천하고 싶은 전략이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;  3. 가장 중요! 전부 이해하려 하지 말고 키워드를 중심으로 암기하자&lt;/b&gt;  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 정보처리기사 필기를 준비할 때도 느꼈던 부분인데, ADsP에도 그대로 적용됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 목표를 다시 생각해보면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  우선은 ADsP 자격증 취득이다.&lt;/b&gt;&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 개념을 제대로 이해하면 가장 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 자격증 시험을 준비하면서 &lt;b&gt;모든 개념을 100% 이해할 때까지 붙잡고 있을 필요는 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 ADsP는 비슷하게 느껴지는 용어도 많고, 통계&amp;middot;분석 관련 개념을 처음 접한다면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;이게 도대체 무슨 말이지?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싶은 부분도 분명히 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 역시 전공자이지만 모든 개념을 완벽하게 이해하면서 공부한 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 완벽하게 이해하려고 시간을 지나치게 사용하기보다 &lt;b&gt;문제에서 반복되는 핵심 키워드를 개념과 연결해서 암기하는 방식&lt;/b&gt;도 적극적으로 활용하는 것을 추천한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;특정 단어 / 표현
        &amp;darr;
관련 개념 연결
        &amp;darr;
선지 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 개념에 특정 단어나 특징이 반복해서 등장한다면, 그 단어를 보는 순간 관련 개념이 떠오르도록 연결해두는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법의 장점은 크게 세 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 공부시간을 크게 줄일 수 있다&lt;/b&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;특히 5일처럼 준비기간이 짧다면 이 차이가 정말 크다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 문제를 빠르게 풀 수 있다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 키워드를 보는 순간 관련 개념을 바로 떠올릴 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 모르는 문제에서도 선지를 줄일 수 있다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답을 정확하게 모르더라도&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;ldquo;이 키워드는 이 개념이랑 관련 있었는데?&amp;rdquo;

        &amp;darr;

관련 없는 선지 제거

        &amp;darr;

남은 선지에서 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 4개 중 하나를 찍는 것과 2개까지 선지를 줄인 뒤 선택하는 것은 확률 자체가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;비전공자나 단기간에 ADsP를 준비하는 사람에게 상당히 유용한 방법&lt;/b&gt;이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 무조건 암기만 하라는 뜻은 아니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이해할 수 있는 내용은 이해하고, 단기간에 점수를 만들어야 하는 부분에서는 키워드 암기를 적극적으로 활용하자.&lt;/b&gt;&lt;/h4&gt;
&lt;/blockquote&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  4. R&amp;middot;데이터 해석 문제는 생각보다 점수 챙기기 좋다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3과목을 처음 공부하면 가장 부담스러운 부분 중 하나가 &lt;b&gt;R과 데이터 해석 문제&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 코드나 출력 결과가 길게 나오기 때문에&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;zwj;  &amp;ldquo;이걸 내가 어떻게 풀어...?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;/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;중요한 건&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;R을 처음부터 끝까지 전부 공부할 필요가 없다는 것&lt;/b&gt;&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 해석 문제에서 자주 확인해야 하는 값들은 어느 정도 정해져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;p-value

계수의 부호
&amp;rarr; 양수 / 음수

R-squared

Adjusted R-squared

유의성 여부

출력 결과의 의미&lt;/code&gt;&lt;/pre&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;처음에는 표나 R 출력 결과 전체를 모두 이해해야 할 것 같지만, 기출을 반복해서 풀다 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;아, 이 유형에서는 여기만 보면 되는구나.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 부분이 보이기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 데이터 분석 내용을 지나치게 깊게 문제로 만들면 시험 난이도뿐 아니라 필요한 공부량도 크게 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ADsP에서는 복잡한 코드를 직접 작성하는 것보다는 &lt;b&gt;주어진 결과에서 필요한 값을 찾아 해석하는 유형&lt;/b&gt;에 익숙해지는 것이 중요하다고 느꼈다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;기출문제 반복
        &amp;darr;
어떤 값을 확인하는지 파악
        &amp;darr;
해석 방법 익히기
        &amp;darr;
비슷한 문제 반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 생김새만 보고 포기하면 오히려 아까운 점수를 버리는 셈이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  처음 생김새는 무섭지만, 유형에 익숙해지면 의외로 점수를 챙기기 좋은 파트다.&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 3과목 과락이 걱정된다면 &lt;b&gt;R&amp;middot;데이터 해석 문제를 처음부터 포기하지 않는 것&lt;/b&gt;을 추천하고 싶다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;summary&quot;&gt;✅ 최종 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 공부 과정과 결과를 다시 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;시험       : 제50회 ADsP
시험일     : 2026.08.08
준비기간   : 5일
베이스     : 전공자

사용 자료
&amp;rarr; 아답터 ADsP 요약강의
&amp;rarr; 2026 이지패스 ADsP 미어캣책

공부 방법
&amp;rarr; 요약강의 전체 1회독
&amp;rarr; 미어캣책 개념 1회독
&amp;rarr; 기출 3회분
&amp;rarr; 모의고사 3회분
&amp;rarr; 오답 + 개념 리마인드

기출&amp;middot;모의고사
&amp;rarr; 보통 80점 이상
&amp;rarr; 낮게 나오면 70점대

복원 가채점
&amp;rarr; 보수적으로 약 64점

실제 결과
&amp;rarr; 1과목 16 / 20
&amp;rarr; 2과목 16 / 20
&amp;rarr; 3과목 36 / 60

최종 점수
&amp;rarr; 68점

결과
&amp;rarr; ✅ 합격&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 50회 시험은 개인적으로 &lt;b&gt;기존 기출이나 모의고사를 풀 때보다 체감 난이도가 훨씬 높은 시험&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험이 끝난 직후에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;진짜 떨어질 수도 있겠다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;68점으로 합격&lt;/b&gt;할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시험을 통해 가장 크게 느낀 것은 단순히 기출 답만 반복하는 것보다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요약강의로 전체 흐름 파악
        &amp;darr;
기본서로 세부 개념까지 확인
        &amp;darr;
기출로 빈출 유형 분석
        &amp;darr;
오답으로 부족한 개념 보완&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정을 만드는 것이 중요하다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 개인적으로 ADsP 단기 합격에서 가장 중요하다고 생각하는 전략을 다시 정리하면,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① 3과목 과락을 지나치게 두려워하지 않기

② 1&amp;middot;2과목에서 최대한 점수 확보하기

③ 모든 내용을 100% 이해하려 하지 말고
   핵심 키워드 중심으로 암기하기

④ R&amp;middot;데이터 해석 문제를 처음부터 포기하지 않기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 50회처럼 지엽적으로 출제되는 시험까지 생각한다면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  아답터 요약강의로 큰 틀을 잡고, 미어캣책의 세부 내용까지 최소 한 번은 읽어보는 것을 추천한다.&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5일이라는 짧은 기간으로도 합격할 수 있었지만, 시험장에서 처음 보는 문제를 마주했을 때 결국 도움이 됐던 것은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;이거 책에서 한 번 봤는데?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;ADsP를 준비하고 있다면 기출 점수가 잘 나온다고 너무 방심하지 말고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;빈출 개념은 확실하게, 세부 개념은 최소한 눈에 익히고, 1&amp;middot;2과목에서 점수를 최대한 확보하는 전략&lt;/b&gt;으로 준비해보는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>My life/자격증</category>
      <category>ADsP</category>
      <category>ADsP50회</category>
      <category>ADsP5일</category>
      <category>adsp공부법</category>
      <category>ADsP교재</category>
      <category>ADSP기출</category>
      <category>ADsP꿀팁</category>
      <category>adsp벼락치기</category>
      <category>ADsP합격후기</category>
      <category>데이터분석준전문가</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/40</guid>
      <comments>https://bbbicsan.tistory.com/40#entry40comment</comments>
      <pubDate>Fri, 21 Aug 2026 20:25:40 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 17일차 TIL : Spring Boot 아키텍처 이해와 Stream API&amp;middot;Optional&amp;middot;CRUD 실습</title>
      <link>https://bbbicsan.tistory.com/39</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 17일차 TIL : Spring Boot 아키텍처 이해와 Stream API&amp;middot;Optional&amp;middot;CRUD 실습&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vV9nS/dJMcabrO3zS/gaxvlFmqA6uf4tht89IHj0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vV9nS/dJMcabrO3zS/gaxvlFmqA6uf4tht89IHj0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vV9nS/dJMcabrO3zS/gaxvlFmqA6uf4tht89IHj0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvV9nS%2FdJMcabrO3zS%2FgaxvlFmqA6uf4tht89IHj0%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&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;어제 직접 구현했던 &lt;code&gt;FrontController &amp;middot; BeanFactory &amp;middot; Controller &amp;middot; Service &amp;middot; DAO&lt;/code&gt; 구조를 Spring Boot 아키텍처와 연결해 이해하고, Java Stream API와 Optional을 다양한 데이터 처리 문제에 적용한 뒤 &lt;b&gt;전체조회 &amp;rarr; 상세조회 &amp;rarr; 검색 &amp;rarr; 등록&lt;/b&gt;으로 Blog CRUD 구조를 확장했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Spring Boot 아키텍처
        &amp;darr;
요청 처리 흐름
        &amp;darr;
Session Facade Pattern
(Front Controller)
        &amp;darr;
BeanFactory
(Controller 인스턴스 관리)
        &amp;darr;
Post CRUD Controller
        &amp;darr;
Stream API 메서드 실습
        &amp;darr;
조건 불만족 시 빈 리스트 반환
        &amp;darr;
Optional
        &amp;darr;
else 처리 개선&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 어제 직접 구현했던 Blog Console 프로그램의 구조를 다시 살펴보면서 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제는 직접:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; BeanFactory
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 만들었다면, 오늘은 이를 실제 Spring Boot 기반 웹 애플리케이션의 요청 처리 구조와 연결해서 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Java 공식 API 문서를 통해 Stream의 메서드 시그니처를 확인하고, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;collect&lt;/code&gt;, &lt;code&gt;groupingBy&lt;/code&gt;, &lt;code&gt;average&lt;/code&gt;, &lt;code&gt;distinct&lt;/code&gt;, &lt;code&gt;anyMatch&lt;/code&gt; 등 다양한 메서드를 Blog 데이터에 직접 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후에는 오전에 학습한 Stream과 Optional을 다시 Blog CRUD 구조에 적용하여 &lt;b&gt;상세조회와 검색 기능을 직접 구현하고, Insert 기능까지 확장&lt;/b&gt;했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot Architecture&lt;/li&gt;
&lt;li&gt;DispatcherServlet&lt;/li&gt;
&lt;li&gt;Front Controller / Session Facade Pattern&lt;/li&gt;
&lt;li&gt;BeanFactory&lt;/li&gt;
&lt;li&gt;Singleton Pattern&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Stream API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Predicate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;collect()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Collectors.toList()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Collectors.groupingBy()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapToInt()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;average()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;distinct()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sorted()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;anyMatch()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allMatch()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noneMatch()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Optional&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ifPresentOrElse()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orElse()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orElseThrow()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findAny()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CRUD&lt;/li&gt;
&lt;li&gt;Constructor Injection&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. 직접 구현한 구조와 Spring Boot 아키텍처 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업은 어제 만든 Blog Console 구조를 다시 확인하면서 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 직접 만든 구조는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;BlogReactView
        &amp;darr;
BlogFrontController
        &amp;darr;
BlogBeanFactory
        &amp;darr;
Controller
        &amp;darr;
Service
        &amp;darr;
DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이 구조를 실제 웹 애플리케이션 관점에서 다시 연결했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client / React
        &amp;darr;
HTTP Request / URL
        &amp;darr;
DispatcherServlet
        &amp;darr;
Controller
        &amp;darr;
Service
        &amp;darr;
Repository / DAO
        &amp;darr;
DB
        &amp;darr;
JSON Response
        &amp;darr;
React Rendering&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 어제 직접 구현했던 &lt;code&gt;BlogFrontController&lt;/code&gt;의 역할이 Spring MVC에서는 &lt;b&gt;DispatcherServlet과 같은 Front Controller 구조&lt;/b&gt;로 연결된다는 점이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 레이어별 책임&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 웹 애플리케이션을 역할에 따라 계층으로 나누어 설명했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Presentation Layer
        &amp;darr;
Controller

Business Layer
        &amp;darr;
Service

Persistence Layer
        &amp;darr;
Repository / DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층의 역할을 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
&amp;rarr; 사용자 요청을 받음
&amp;rarr; 요청 데이터 전달
&amp;rarr; 응답 반환

Service
&amp;rarr; 비즈니스 로직 처리

Repository / DAO
&amp;rarr; 데이터 저장 및 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 만든 콘솔 프로그램에서도 이미 비슷한 구조를 직접 구현했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. BeanFactory와 객체 생성 구조 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogBeanFactory&lt;/code&gt;에서는 객체를 직접 생성하고 서로 필요한 의존성을 연결했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dao = new BlogReactDao();

service =
    new BlogReactServiceImpl(
        dao
    );

map.put(
    &quot;list.inspire&quot;,
    new ListController(
        service
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 흐름은:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactDao
        &amp;darr;
BlogReactServiceImpl
        &amp;darr;
ListController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Factory 생성자를 &lt;code&gt;private&lt;/code&gt;으로 선언하고:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public static BlogBeanFactory getInstance() {

    if(instance == null) {

        instance =
            new BlogBeanFactory();
    }

    return instance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용하면서 Singleton 구조도 다시 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 어제는 Spring 없이 직접:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;객체 생성
+
객체 관리
+
Dependency Injection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 구현했고, 오늘은 이런 작업이 이후 Spring의 객체 관리 구조와 연결된다는 흐름으로 이해했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Java 공식 문서에서 Stream API 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream API를 단순히 예제 코드로만 사용하는 것이 아니라 Oracle Java 17 공식 API 문서를 확인하면서 실제 메서드 시그니처를 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;filter()&lt;/code&gt;는:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;filter(
    Predicate&amp;lt;? super T&amp;gt; predicate
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;filter()&lt;/code&gt;는 &lt;code&gt;Predicate&lt;/code&gt;를 전달받는다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt;
        &amp;darr;
조건 검사
        &amp;darr;
boolean
        &amp;darr;
true인 요소만 Stream에 유지&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. filter() - 조건에 맞는 데이터 추출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 데이터 중 조회수가 30 이상인 게시글만 출력했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getViewCnt() &amp;gt;= 30
    )
    .forEach(
        blog -&amp;gt;
            System.out.println(blog)
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 참조를 사용하면 다음처럼 줄일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getViewCnt() &amp;gt;= 30
    )
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
stream()
        &amp;darr;
filter(viewCnt &amp;gt;= 30)
        &amp;darr;
조건에 맞는 객체만 통과
        &amp;darr;
forEach()
        &amp;darr;
출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. map() - 데이터와 타입 변환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 API에서 &lt;code&gt;map()&lt;/code&gt;은 다음 형태였다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;map(
    Function&amp;lt;? super T, ? extends R&amp;gt; mapper
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;map()&lt;/code&gt;은 &lt;code&gt;Function&lt;/code&gt;을 전달받아 하나의 값을 다른 값으로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blog 객체에서 email만 추출해보았다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getViewCnt() &amp;gt;= 30
    )
    .map(
        BlogResponseDTO::getEmail
    )
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 Stream 요소의 타입 자체가 변경된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
filter()
        &amp;darr;
Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
map(BlogResponseDTO::getEmail)
        &amp;darr;
Stream&amp;lt;String&amp;gt;
        &amp;darr;
forEach()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;filter()
&amp;rarr; 필요한 요소 선택

map()
&amp;rarr; 필요한 값으로 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 차이를 다시 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. Stream의 중간 연산과 최종 연산&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 API를 살펴보면서 &lt;code&gt;limit()&lt;/code&gt;도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;limit(long maxSize)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;limit()&lt;/code&gt;은 Stream에서 처리할 요소의 최대 개수를 제한하는 중간 연산이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 확인한 메서드를 기준으로 보면:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;중간 연산
│
├─ filter()
├─ map()
├─ limit()
├─ distinct()
└─ sorted()

최종 연산
│
├─ collect()
├─ forEach()
├─ average()
├─ anyMatch()
├─ allMatch()
└─ noneMatch()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. collect() - Stream 결과를 다시 List로 반환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님께서 &lt;code&gt;collect()&lt;/code&gt;를 &lt;b&gt;자주 사용하는 최종 연산 메서드&lt;/b&gt;라고 강조하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 API의 형태는:&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;&amp;lt;R, A&amp;gt; R collect(
    Collector&amp;lt;? super T, A, R&amp;gt; collector
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 email이 &lt;code&gt;&quot;lim&quot;&lt;/code&gt;인 게시글만 추출해 List로 반환했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt; result =
    blogs.stream()
        .filter(
            blog -&amp;gt;
                blog.getEmail()
                    .equals(&quot;lim&quot;)
        )
        .collect(
            Collectors.toList()
        );

result.forEach(
    System.out::println
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
stream()
        &amp;darr;
filter(email == &quot;lim&quot;)
        &amp;darr;
Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
collect(Collectors.toList())
        &amp;darr;
List&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;filter()&lt;/code&gt;는 타입을 변경하지 않고 조건에 맞는 데이터만 유지하고, &lt;code&gt;collect()&lt;/code&gt;에서 Stream의 결과를 실제 Collection으로 다시 반환한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. Collectors.groupingBy() - 작성자별 그룹화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 email을 기준으로 게시글을 그룹화했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Map&amp;lt;String, List&amp;lt;BlogResponseDTO&amp;gt;&amp;gt; map =
    blogs.stream()
        .collect(
            Collectors.groupingBy(
                BlogResponseDTO::getEmail
            )
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 타입은:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, List&amp;lt;BlogResponseDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&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;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;lim&quot;
&amp;rarr; [
    react 게시글,
    msa 게시글
  ]

&quot;kim&quot;
&amp;rarr; [
    java 게시글
  ]

&quot;lee&quot;
&amp;rarr; [
    spring 게시글
  ]

&quot;park&quot;
&amp;rarr; [
    docker 게시글
  ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 작성자만 다시 가져올 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;map.get(&quot;lim&quot;)
    .stream()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과 &lt;code&gt;&quot;lim&quot;&lt;/code&gt;이 작성한 두 게시글이 함께 출력되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 조회수 평균 계산하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 강사님 실습 문제는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;Q) 조회수의 평균을 확인하고 싶다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 객체 Stream을 기본형 Stream으로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;double avg =
    blogs.stream()
        .mapToInt(
            BlogResponseDTO::getViewCnt
        )
        .average()
        .orElse(0);

System.out.println(avg);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
mapToInt(getViewCnt)
        &amp;darr;
IntStream
        &amp;darr;
average()
        &amp;darr;
OptionalDouble
        &amp;darr;
orElse(0)
        &amp;darr;
double&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;average()&lt;/code&gt;는 바로 &lt;code&gt;double&lt;/code&gt;을 반환하는 것이 아니라 &lt;code&gt;OptionalDouble&lt;/code&gt;을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 존재하지 않을 수도 있기 때문에:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;.orElse(0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 값이 없을 때 사용할 기본값을 지정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. Primitive Stream&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 계산을 하면서 객체 Stream과 기본형 Stream의 차이도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 기본형 Stream은:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;IntStream
LongStream
DoubleStream&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 API에서는:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;asDoubleStream()
asLongStream()
average()
boxed()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 메서드도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;mapToInt()&lt;/code&gt;를 사용하면:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
IntStream&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 이용한 합계나 평균 같은 계산에서는 기본형 Stream을 활용할 수 있다는 점을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. distinct() - 중복 이메일 제거&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문제는 email만 추출하되 중복된 데이터는 한 번만 출력하는 것이었다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blogs.stream()
    .map(
        BlogResponseDTO::getEmail
    )
    .distinct()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Stream&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
map(getEmail)
        &amp;darr;
Stream&amp;lt;String&amp;gt;
        &amp;darr;
distinct()
        &amp;darr;
중복 제거
        &amp;darr;
forEach()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;&quot;lim&quot;&lt;/code&gt;이라는 email이 여러 게시글에 존재하더라도 출력 결과에서는 한 번만 나타난다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. sorted() - 정렬&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream의 정렬 기능도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blogs.stream()
    .sorted()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sorted()&lt;/code&gt;는 Stream 요소의 정렬을 담당하는 중간 연산이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;code&gt;distinct()&lt;/code&gt;와 함께 데이터의 중복 제거 및 정렬 관련 Stream 메서드까지 범위를 확장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. anyMatch / allMatch / noneMatch&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Stream 안의 데이터가 특정 조건을 만족하는지 &lt;code&gt;boolean&lt;/code&gt;으로 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;anyMatch()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나라도 조건을 만족하는 데이터가 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;boolean isEmail =
    blogs.stream()
        .anyMatch(
            blog -&amp;gt;
                blog.getEmail()
                    .equals(&quot;jslim&quot;)
        );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;하나라도 만족
&amp;rarr; true&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;allMatch()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터가 조건을 만족하는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;boolean isExists =
    blogs.stream()
        .allMatch(
            blog -&amp;gt;
                blog.getViewCnt() &amp;gt;= 20
        );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;모든 데이터 만족
&amp;rarr; true&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;noneMatch()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 만족하는 데이터가 하나도 없는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;boolean isUnder =
    blogs.stream()
        .noneMatch(
            blog -&amp;gt;
                blog.getViewCnt() &amp;lt; 10
        );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;조건을 만족하는 데이터가 하나도 없음
&amp;rarr; true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 메서드를 비교하면:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;anyMatch()
&amp;rarr; 하나라도 만족?

allMatch()
&amp;rarr; 전부 만족?

noneMatch()
&amp;rarr; 하나도 만족하지 않음?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 정리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. Optional이 필요한 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream 실습 이후 &lt;code&gt;Optional&lt;/code&gt;을 본격적으로 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional은 주로 &lt;b&gt;메서드의 반환값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 표현할 때 사용&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;메서드 호출
    &amp;darr;
반환값 존재 여부 불확실
    &amp;darr;
Optional&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 객체가 없을 때 &lt;code&gt;null&lt;/code&gt;을 직접 반환하면:&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;result.method();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같이 사용할 때 &lt;code&gt;NullPointerException&lt;/code&gt;이 발생할 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional을 이용하면 값의 존재 여부를 명시적으로 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 Optional을 모든 곳에 사용하는 것이 아니라 &lt;b&gt;용도에 맞게 사용해야 하며, 특히 메서드 반환값에서 활용하는 방향&lt;/b&gt;을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. Optional.of()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 존재하는 Optional은 다음과 같이 만들었다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;Optional&amp;lt;String&amp;gt; optional =
    Optional.of(&quot;lgcns&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식으로는:&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;if(optional.isPresent()) {

    System.out.println(
        optional.get()
    );

} else {

    // 값이 없는 경우
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 값을 확인할 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. ifPresentOrElse()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 9 이후 사용할 수 있는 &lt;code&gt;ifPresentOrElse()&lt;/code&gt;도 실습했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;optional.ifPresentOrElse(

    value -&amp;gt;
        System.out.println(
            value
        ),

    () -&amp;gt;
        System.out.println(
            &quot;값이 없습니다.&quot;
        )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Optional
    &amp;darr;
값 있음?
│
├─ YES
│   &amp;darr;
│ value -&amp;gt; ...
│
└─ NO
    &amp;darr;
  () -&amp;gt; ...&lt;/code&gt;&lt;/pre&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;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;if
&amp;rarr; 값 존재

else
&amp;rarr; 값 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 Optional API 내부에서 처리할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. Optional.empty()와 orElseThrow()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 없는 Optional도 직접 만들었다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;optional =
    Optional.empty();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;String err =
    optional.orElseThrow(
        () -&amp;gt;
            new RuntimeException(
                &quot;값이 없습니다.&quot;
            )
    );&lt;/code&gt;&lt;/pre&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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Exception in thread &quot;main&quot;
java.lang.RuntimeException:
값이 없습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stack Trace에서도:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Optional.orElseThrow(...)
        &amp;darr;
BlogStreamApp.main(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 예외가 발생한 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional 관련 메서드를 정리하면:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Optional.of(value)
&amp;rarr; 값이 있는 Optional

Optional.empty()
&amp;rarr; 빈 Optional

ifPresentOrElse()
&amp;rarr; 값 있음 / 없음 분기

orElse(defaultValue)
&amp;rarr; 없으면 기본값 반환

orElseThrow()
&amp;rarr; 없으면 Exception 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. Stream API 실습 전체 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 실습한 Stream API를 목적별로 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;조건 검색
&amp;rarr; filter()

값 변환
&amp;rarr; map()

개수 제한
&amp;rarr; limit()

Collection 반환
&amp;rarr; collect()

그룹화
&amp;rarr; groupingBy()

숫자 Stream 변환
&amp;rarr; mapToInt()

평균
&amp;rarr; average()

기본값 처리
&amp;rarr; orElse()

중복 제거
&amp;rarr; distinct()

정렬
&amp;rarr; sorted()

하나라도 만족
&amp;rarr; anyMatch()

모두 만족
&amp;rarr; allMatch()

아무것도 만족하지 않음
&amp;rarr; noneMatch()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메서드를 따로 외우는 것보다:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;조회
&amp;rarr; 조건 검색
&amp;rarr; 변환
&amp;rarr; 수집
&amp;rarr; 집계
&amp;rarr; 조건 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 데이터 처리 과정으로 이해하는 것이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. Blog CRUD - 상세조회 기능 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후에는 오전에 배운 Stream과 Optional을 기존 Blog 프로젝트에 다시 적용했다.&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;View에서는 사용자의 &lt;code&gt;blogId&lt;/code&gt;를 입력받았다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public void read() {

    System.out.println();
    System.out.println(
        &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; 상세페이지 정보&quot;
    );

    String endPoint =
        &quot;read.inspire&quot;;

    System.out.print(
        &quot;키(blogId) 입력 : &quot;
    );

    int blogId =
        Integer.parseInt(
            scan.nextLine()
        );

    BlogResponseDTO response =
        front.read(
            endPoint,
            blogId
        );

    System.out.println(
        (response != null)
            ? response
            : &quot;정보없음&quot;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;사용자
   &amp;darr;
blogId 입력
   &amp;darr;
BlogReactView.read()
   &amp;darr;
BlogFrontController.read()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. ReadController와 Service 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ReadController&lt;/code&gt;는 &lt;code&gt;BlogReactService&lt;/code&gt;를 생성자 주입받는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;private BlogReactService service;

public ReadController(
        BlogReactService service) {

    this.service =
        service;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는 실제 데이터 조회를 직접 하지 않고 Service에 위임했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public BlogResponseDTO read(
        int blogId) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; read controller read params : &quot;
        + blogId
    );

    return service.read(
        blogId
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;ReadController
     &amp;darr;
Service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 책임을 분리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. DAO에서 Stream으로 단건 검색&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogReactDao.findById()&lt;/code&gt;에서는 오늘 배운 Stream API를 실제 상세조회에 활용했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;public Optional&amp;lt;BlogResponseDTO&amp;gt;
        findById(int blogId) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog dao findById() params : &quot;
        + blogId
    );

    return blogs.stream()
        .filter(
            blog -&amp;gt;
                blog.getBlogId()
                    == blogId
        )
        .findAny();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt;
        &amp;darr;
stream()
        &amp;darr;
filter(blogId 일치)
        &amp;darr;
findAny()
        &amp;darr;
Optional&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오전에 따로 학습했던 &lt;code&gt;filter()&lt;/code&gt;와 &lt;code&gt;Optional&lt;/code&gt;이 실제 DAO의 단건 조회 로직으로 연결되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Optional 처리 방식 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO의 &lt;code&gt;findById()&lt;/code&gt;는 &lt;code&gt;Optional&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서는 이를 여러 방식으로 처리해봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Case 01 - orElse(null)&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;return dao.findById(blogId)
    .orElse(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;값 있음
&amp;rarr; BlogResponseDTO

값 없음
&amp;rarr; null&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Case 02 - isPresent() + get()&lt;/h2&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Optional&amp;lt;BlogResponseDTO&amp;gt; result =
    dao.findById(blogId);

if(result.isPresent()) {

    return result.get();
}

return null;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 동작을 직접 조건문으로 구현한 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Case 03 - orElseThrow()&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;return dao.findById(blogId)
    .orElseThrow(
        () -&amp;gt;
            new RuntimeException(
                blogId
                + &quot; 정보를 찾을 수 없습니다.&quot;
            )
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 데이터가 없을 때 &lt;code&gt;null&lt;/code&gt; 대신 Exception을 발생시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;orElse(null)
&amp;rarr; 없으면 null

isPresent() + get()
&amp;rarr; 존재 여부를 직접 확인

orElseThrow()
&amp;rarr; 없으면 Exception&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional을 단순 예제로 끝내지 않고 &lt;b&gt;실제 단건 조회 결과 처리에 적용&lt;/b&gt;했다는 점이 중요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. 상세조회 전체 요청 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세조회 기능의 전체 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactView
      &amp;darr;
blogId 입력
      &amp;darr;
BlogFrontController
      &amp;darr;
BlogBeanFactory
      &amp;darr;
ReadController
      &amp;darr;
BlogReactService
      &amp;darr;
BlogReactServiceImpl
      &amp;darr;
BlogReactDao.findById()
      &amp;darr;
Stream.filter()
      &amp;darr;
findAny()
      &amp;darr;
Optional&amp;lt;BlogResponseDTO&amp;gt;
      &amp;darr;
Service에서 Optional 처리
      &amp;darr;
응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 전체조회 기능에서 구현한 계층 구조가 상세조회 기능까지 확장됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. 강사님 실습 문제 - Keyword 검색 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 강사님께서 직접 구현해보라고 다음 문제를 주셨다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Q)
blog content 내용 중 해당 키워드를 포함하는 것들을
검색해서 가져와 본다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현해야 할 단계도 함께 정리했다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. FrontController search() 정의

2. SearchController 클래스 정의

3. BeanFactory에
   &quot;search.inspire&quot; 등록

4. BlogReactServiceImpl에
   search() 정의

5. Service에서
   dao.findByKeyword() 호출

6. BlogReactDao에
   findByKeyword() 구현&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 참고하면서 각 계층을 직접 연결하는 실습이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;26. View에서 검색 Keyword 입력&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View에서는 검색 endpoint를 지정하고 사용자의 검색어를 입력받았다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public void search() {

    System.out.println();
    System.out.println(
        &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; 검색페이지 정보&quot;
    );

    String endPoint =
        &quot;search.inspire&quot;;

    System.out.print(
        &quot;키워드 입력 : &quot;
    );

    String keyword =
        scan.nextLine();

    List&amp;lt;BlogResponseDTO&amp;gt; response =
        front.search(
            endPoint,
            keyword
        );

    response.stream()
        .forEach(
            System.out::println
        );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;사용자 keyword 입력
        &amp;darr;
&quot;search.inspire&quot;
        &amp;darr;
FrontController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 요청을 전달했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;27. SearchController 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SearchController&lt;/code&gt;에서도 Service를 생성자 주입받았다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;private BlogReactService service;

public SearchController(
        BlogReactService service) {

    this.service =
        service;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View에서 전달된 &lt;code&gt;String keyword&lt;/code&gt;는 Service에 바로 전달하지 않고 &lt;code&gt;BlogRequestDTO&lt;/code&gt;로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public List&amp;lt;BlogResponseDTO&amp;gt; search(
        String keyword) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; search controller search params : &quot;
        + keyword
    );

    return service.search(
        BlogRequestDTO.builder()
            .keyword(
                keyword
            )
            .build()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 흐름은:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; String keyword

Controller
&amp;rarr; BlogRequestDTO

Service
&amp;rarr; BlogRequestDTO

DAO
&amp;rarr; request.getKeyword()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;28. Factory에 SearchController 등록&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Factory에도 새로운 endpoint를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;map.put(
    &quot;list.inspire&quot;,
    new ListController(
        service
    )
);

map.put(
    &quot;read.inspire&quot;,
    new ReadController(
        service
    )
);

map.put(
    &quot;search.inspire&quot;,
    new SearchController(
        service
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;endpoint와 Controller의 관계는:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;list.inspire
&amp;rarr; ListController

read.inspire
&amp;rarr; ReadController

search.inspire
&amp;rarr; SearchController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 확장됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;29. Stream filter()를 이용한 실제 검색 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO에서는 입력받은 keyword를 이용해 데이터를 필터링했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;public List&amp;lt;BlogResponseDTO&amp;gt;
        findByKeyword(
            BlogRequestDTO request) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog dao findByKeyword params : &quot;
        + request
    );

    return blogs.stream()
        .filter(
            blog -&amp;gt;
                blog.getTitle()
                    .contains(
                        request.getKeyword()
                    )
                ||
                blog.getContent()
                    .contains(
                        request.getKeyword()
                    )
        )
        .toList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 문제에서는 &lt;code&gt;content&lt;/code&gt; 검색을 중심으로 제시됐지만, 실제 구현에서는 조건을 확장해:&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;title에 keyword 포함
OR
content에 keyword 포함&lt;/code&gt;&lt;/pre&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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;filter(Predicate)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 실제 DAO에서:&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;title.contains(keyword)
        OR
content.contains(keyword)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 검색 조건으로 사용됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;30. 검색 기능 실행 결과&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogFrontController.search()&lt;/code&gt;의 최종 구현은 다음과 같이 Controller에 요청을 위임했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public List&amp;lt;BlogResponseDTO&amp;gt; search(
        String endPoint,
        String keyword) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; front controller endPoint : &quot;
        + endPoint
        + &quot;, keyword : &quot;
        + keyword
    );

    Object controller =
        factory.getBean(
            endPoint
        );

    return (
        (SearchController) controller
    ).search(keyword);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 검색 키워드로:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;oop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 입력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Console에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;front controller
        &amp;darr;
search controller
        &amp;darr;
blog service search
        &amp;darr;
blog dao findByKeyword&lt;/code&gt;&lt;/pre&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;pre class=&quot;ini&quot;&gt;&lt;code&gt;title   = java
content = oop
email   = kim
viewCnt = 20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 직접 구현한 검색 기능이 전체 계층을 거쳐 정상적으로 동작하는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;31. Insert 기능 구현 시작&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 기능 이후에는 게시글 등록 기능으로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;InsertController&lt;/code&gt; 역시 Service를 생성자 주입받았다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;private BlogReactService service;

public InsertController(
        BlogReactService service) {

    this.service =
        service;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록할 때는 여러 값이 필요했다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;title
content
email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 각각 Service에 전달하기보다 하나의 RequestDTO로 바인딩했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;public int insert(
        String title,
        String content,
        String email) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; insert controller insert params : &quot;
        + title
        + &quot;\t&quot;
        + content
        + &quot;\t&quot;
        + email
    );

    return service.insert(
        BlogRequestDTO.builder()
            .title(title)
            .content(content)
            .email(email)
            .build()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 이 부분에서:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;여러 파라미터
        &amp;darr;
하나의 객체로 Binding
        &amp;darr;
RequestDTO
        &amp;darr;
유효성 검사&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 필요성을 강조했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;32. Service에서 Insert 요청 DAO로 위임&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서는 Controller에서 전달받은 RequestDTO를 DAO로 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
public int insert(
        BlogRequestDTO request) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog service insert params : &quot;
        + request
    );

    return dao.save(
        request
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 기존 기능들과 동일하다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;InsertController
        &amp;darr;
BlogReactServiceImpl
        &amp;darr;
BlogReactDao.save()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 화면에서는 &lt;code&gt;dao.save()&lt;/code&gt;까지 연결하는 단계가 진행됐으며, 실제 저장 메서드의 최종 구현과 실행 결과까지는 확인하지 않았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;33. FrontController에 Insert 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrontController에도 Insert 요청을 처리하는 메서드를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;public int insert(
        String endPoint,
        String title,
        String content,
        String email) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; front controller endPoint : &quot;
        + endPoint
    );

    Object controller =
        factory.getBean(
            endPoint
        );

    return (
        (InsertController) controller
    ).insert(
        title,
        content,
        email
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;list()
read()
search()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;insert()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 Controller 구조가 확장되기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;34. RequestDTO 변환 메서드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogRequestDTO&lt;/code&gt;에는 현재 다음과 같은 데이터가 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;title
content
email
keyword&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 객체 변환용 정적 메서드도 작성했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public static BlogResponseDTO toEntity(
        BlogRequestDTO request) {

    return BlogResponseDTO.builder()
        .title(
            request.getTitle()
        )
        .content(
            request.getContent()
        )
        .email(
            request.getEmail()
        )
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 JPA 환경에서 DTO를 Entity로 변환할 때 &lt;b&gt;정적 메서드를 이용한 변환 패턴&lt;/b&gt;을 사용할 수 있다는 내용과 연결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 콘솔 실습 코드에서는 메서드 이름이 &lt;code&gt;toEntity()&lt;/code&gt;이지만 실제 반환 타입은 &lt;code&gt;BlogResponseDTO&lt;/code&gt;이므로, 실제 JPA Entity 변환과는 구분해서 이해했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  실습 기록&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Stream filter 조건 검색&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getViewCnt() &amp;gt;= 30
    )
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Stream
&amp;rarr; 조회수 30 이상 필터링
&amp;rarr; 해당 DTO만 출력&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. map()으로 email만 추출&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getViewCnt() &amp;gt;= 30
    )
    .map(
        BlogResponseDTO::getEmail
    )
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;BlogResponseDTO
&amp;rarr; email
&amp;rarr; String&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 Stream의 타입이 &lt;code&gt;String&lt;/code&gt; Stream으로 변경되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. email이 lim인 데이터 List 반환&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt; result =
    blogs.stream()
        .filter(
            blog -&amp;gt;
                blog.getEmail()
                    .equals(&quot;lim&quot;)
        )
        .collect(
            Collectors.toList()
        );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;filter()
&amp;rarr; 조건 검색

collect()
&amp;rarr; List 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 작성자별 groupingBy()&lt;/h2&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Map&amp;lt;String, List&amp;lt;BlogResponseDTO&amp;gt;&amp;gt; map =
    blogs.stream()
        .collect(
            Collectors.groupingBy(
                BlogResponseDTO::getEmail
            )
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lim&lt;/code&gt; 작성자의 게시글 두 개가 동일 그룹으로 묶이는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 조회수 평균 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님이 제시한:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;Q) 조회수의 평균을 확인하고 싶다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 다음과 같이 해결했다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;double avg =
    blogs.stream()
        .mapToInt(
            BlogResponseDTO::getViewCnt
        )
        .average()
        .orElse(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 Stream을 &lt;code&gt;IntStream&lt;/code&gt;으로 변환한 뒤 평균을 구하고, 데이터가 없을 경우 &lt;code&gt;0&lt;/code&gt;을 반환하도록 처리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. email 중복 제거&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blogs.stream()
    .map(
        BlogResponseDTO::getEmail
    )
    .distinct()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복된 &lt;code&gt;&quot;lim&quot;&lt;/code&gt; email이 한 번만 출력되는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Stream Match 실습&lt;/h2&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;anyMatch()
&amp;rarr; 하나라도 조건 만족?

allMatch()
&amp;rarr; 모두 조건 만족?

noneMatch()
&amp;rarr; 하나도 조건 만족하지 않음?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건 검사의 결과가 모두 &lt;code&gt;boolean&lt;/code&gt;으로 반환된다는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Optional ifPresentOrElse()&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;optional.ifPresentOrElse(

    value -&amp;gt;
        System.out.println(value),

    () -&amp;gt;
        System.out.println(
            &quot;값이 없습니다.&quot;
        )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional 값의 존재 여부에 따른 분기를 &lt;code&gt;if / else&lt;/code&gt; 없이 처리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Optional.empty() + orElseThrow()&lt;/h2&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;optional =
    Optional.empty();

String err =
    optional.orElseThrow(
        () -&amp;gt;
            new RuntimeException(
                &quot;값이 없습니다.&quot;
            )
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java.lang.RuntimeException:
값이 없습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 발생하는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. DAO 상세조회에 Stream + Optional 적용&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;return blogs.stream()
    .filter(
        blog -&amp;gt;
            blog.getBlogId()
                == blogId
    )
    .findAny();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 초반 별도 예제로 학습했던:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;filter()
+
Optional&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실제 CRUD 조회 기능으로 연결됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 강사님 검색 기능 구현 문제&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Q)
blog content 내용 중
해당 키워드를 포함하는 게시글을
검색해서 가져와 본다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; SearchController
&amp;rarr; Service
&amp;rarr; DAO
&amp;rarr; Stream.filter()
&amp;rarr; List 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 &lt;code&gt;oop&lt;/code&gt;를 검색하여 &lt;code&gt;content=oop&lt;/code&gt;인 게시글이 반환되는 것까지 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1. Search FrontController 구현 중 ReadController 코드가 남아있음&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;code&gt;BlogFrontController.search()&lt;/code&gt;를 추가하는 과정에서 상세조회 메서드의 코드를 복사한 흔적이 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 코드에는:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;return ((ReadController) controller)
    .read(blogId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 남아 있었고, 검색 메서드에는 존재하지 않는 &lt;code&gt;blogId&lt;/code&gt;가 사용되면서 Error가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK1paw/dJMcab6p19b/5OpI7vVI0RlNdm95xrR9q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK1paw/dJMcab6p19b/5OpI7vVI0RlNdm95xrR9q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK1paw/dJMcab6p19b/5OpI7vVI0RlNdm95xrR9q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK1paw%2FdJMcab6p19b%2F5OpI7vVI0RlNdm95xrR9q1%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;764&quot; height=&quot;58&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세조회 &lt;code&gt;read()&lt;/code&gt; 구조를 기준으로 검색 메서드를 작성하면서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ReadController
blogId
read()&lt;/code&gt;&lt;/pre&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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;SearchController
keyword
search()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhtokz/dJMcafgIa0T/LpOSUdPmdnmsqvGGQcfPgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhtokz/dJMcafgIa0T/LpOSUdPmdnmsqvGGQcfPgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhtokz/dJMcafgIa0T/LpOSUdPmdnmsqvGGQcfPgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhtokz%2FdJMcafgIa0T%2FLpOSUdPmdnmsqvGGQcfPgk%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;771&quot; height=&quot;136&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 endpoint에서 반환된 Controller를 &lt;code&gt;SearchController&lt;/code&gt;로 Casting하고 &lt;code&gt;keyword&lt;/code&gt;를 전달하도록 수정했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public List&amp;lt;BlogResponseDTO&amp;gt; search(
        String endPoint,
        String keyword) {

    Object controller =
        factory.getBean(
            endPoint
        );

    return (
        (SearchController) controller
    ).search(keyword);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DbpLA/dJMcaiLaaOm/xA5xJtQ1Kdef0bRar22040/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DbpLA/dJMcaiLaaOm/xA5xJtQ1Kdef0bRar22040/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DbpLA/dJMcaiLaaOm/xA5xJtQ1Kdef0bRar22040/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDbpLA%2FdJMcaiLaaOm%2FxA5xJtQ1Kdef0bRar22040%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;810&quot; height=&quot;290&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;oop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 입력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Console에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;front controller
&amp;rarr; search controller
&amp;rarr; blog service search
&amp;rarr; blog dao findByKeyword&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 호출 로그가 정상 출력됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0sYBg/dJMcahMhUj2/Dd42yEYklkMkrmfpkKMuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0sYBg/dJMcahMhUj2/Dd42yEYklkMkrmfpkKMuk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0sYBg/dJMcahMhUj2/Dd42yEYklkMkrmfpkKMuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0sYBg%2FdJMcahMhUj2%2FDd42yEYklkMkrmfpkKMuk1%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;853&quot; height=&quot;223&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 결과:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;title=java
content=oop
email=kim
viewCnt=20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 반환되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 컴파일 오류만 수정한 것이 아니라 &lt;b&gt;View &amp;rarr; Controller &amp;rarr; Service &amp;rarr; DAO 전체 흐름이 정상적으로 연결됐는지 실행 결과까지 검증&lt;/b&gt;했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75uCl/dJMcaji58Vd/kQXdyurr6KVmzWxKMzdgNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75uCl/dJMcaji58Vd/kQXdyurr6KVmzWxKMzdgNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75uCl/dJMcaji58Vd/kQXdyurr6KVmzWxKMzdgNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75uCl%2FdJMcaji58Vd%2FkQXdyurr6KVmzWxKMzdgNK%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;782&quot; height=&quot;594&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u4LNM/dJMcagfyL50/qxpGO0O2kkxvGtwhw1EK1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u4LNM/dJMcagfyL50/qxpGO0O2kkxvGtwhw1EK1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u4LNM/dJMcagfyL50/qxpGO0O2kkxvGtwhw1EK1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu4LNM%2FdJMcagfyL50%2FqxpGO0O2kkxvGtwhw1EK1K%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;1152&quot; height=&quot;694&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 내용 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요했던 것은 Stream과 Optional의 메서드를 많이 외운 것이 아니라, &lt;b&gt;각 문법이 실제 애플리케이션의 데이터 처리 코드에서 어떻게 사용되는지 연결해서 확인한 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오전에는 다음과 같이 각각의 Stream API를 직접 실습했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;filter()
&amp;rarr; 조건 검색

map()
&amp;rarr; 데이터 변환

collect()
&amp;rarr; Collection 반환

groupingBy()
&amp;rarr; 데이터 그룹화

mapToInt()
&amp;rarr; 숫자 Stream 변환

average()
&amp;rarr; 평균

distinct()
&amp;rarr; 중복 제거

anyMatch()
&amp;rarr; 하나라도 만족

allMatch()
&amp;rarr; 모두 만족

noneMatch()
&amp;rarr; 모두 불만족 여부 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 오후에는 이 메서드들이 실제 DAO에서 그대로 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;BlogReactDao.findById()
        &amp;darr;
stream()
        &amp;darr;
filter()
        &amp;darr;
findAny()
        &amp;darr;
Optional&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색에서도:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;BlogReactDao.findByKeyword()
        &amp;darr;
stream()
        &amp;darr;
filter()
        &amp;darr;
contains(keyword)
        &amp;darr;
toList()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 사용됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 오전에 학습한 Stream API가 단순 문법 실습에서 끝난 것이 아니라 &lt;b&gt;Repository / DAO에서 실제 데이터를 조회하는 코드로 그대로 연결&lt;/b&gt;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optional도 마찬가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Optional&amp;lt;String&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 가지고:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ifPresentOrElse()
orElse()
orElseThrow()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 연습했지만, 이후에는 실제 상세조회에서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;findById(blogId)
&amp;rarr; Optional&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태로 사용됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 단건 조회는 데이터가 반드시 존재한다고 보장할 수 없기 때문에 Optional을 사용하는 이유도 더 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 어제 직접 만든:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; BeanFactory
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조가 오늘:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DispatcherServlet
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; Repository&lt;/code&gt;&lt;/pre&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이 객체 생성이나 의존성 주입을 자동으로 처리하기 전에 직접 Factory와 Constructor Injection을 구현해본 덕분에 각 객체가 어떤 관계를 가지는지도 조금 더 명확하게 볼 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Stream API와 Optional을 집중적으로 학습했지만, 가장 의미 있었던 부분은 각각의 문법을 실제 CRUD 코드까지 연결해본 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 &lt;code&gt;filter()&lt;/code&gt;, &lt;code&gt;map()&lt;/code&gt;, &lt;code&gt;collect()&lt;/code&gt;를 배울 때는 단순히 Collection 데이터를 편리하게 처리하는 문법처럼 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Blog 프로젝트의 DAO에서:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(...)
    .findAny();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 실제 게시글을 조회하고,&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;blogs.stream()
    .filter(...)
    .toList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 검색 결과를 반환하면서 Stream이 실제 데이터 접근 로직에서도 자연스럽게 사용될 수 있다는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오전에 학습한 Optional이 오후 상세조회에서 바로 연결된 것이 기억에 남았다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;findById()
&amp;rarr; 데이터가 존재할 수도 있음
&amp;rarr; 존재하지 않을 수도 있음
&amp;rarr; Optional&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 흐름을 직접 구현해보니 Optional을 왜 사용하는지 단순 정의보다 훨씬 이해하기 쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orElse(null)&lt;/code&gt;, &lt;code&gt;isPresent() + get()&lt;/code&gt;, &lt;code&gt;orElseThrow()&lt;/code&gt; 세 가지 방식을 비교한 것도 도움이 됐다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;null로 반환할 것인가?

직접 존재 여부를 확인할 것인가?

데이터가 없다면 Exception으로 처리할 것인가?&lt;/code&gt;&lt;/pre&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;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Q)
blog content에 keyword가 포함된
게시글을 검색한다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 요구사항에서 시작해서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; SearchController
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층에 필요한 코드를 직접 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 중에는 &lt;code&gt;FrontController.search()&lt;/code&gt;에 기존 &lt;code&gt;ReadController&lt;/code&gt; 코드가 남아 있어 오류도 발생했지만, 검색 요청에 맞게 &lt;code&gt;SearchController&lt;/code&gt;와 &lt;code&gt;keyword&lt;/code&gt;로 수정하고 실제 실행 로그를 확인하면서 해결할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;front controller
&amp;rarr; search controller
&amp;rarr; blog service
&amp;rarr; blog dao&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 순서대로 출력되고 &lt;code&gt;content=oop&lt;/code&gt;인 데이터가 실제 조회되는 것을 확인하면서, 단순히 코드를 작성한 것보다 &lt;b&gt;요청이 각 레이어를 제대로 통과하는지 검증하는 과정&lt;/b&gt;이 중요하다는 것을 다시 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 기능에서는 여러 개의 파라미터를:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;title
content
email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 계속 전달하는 대신:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;BlogRequestDTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나로 묶어 Service에 전달하는 구조도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 Spring Boot를 본격적으로 사용하게 되면 오늘 직접 구현했던:&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;FrontController
BeanFactory
Constructor Injection
RequestDTO
Controller
Service
DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조가 실제 Spring에서는 어떤 Annotation과 기능으로 대체되는지 비교해서 학습하면 이해가 더 잘 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 복습할 부분은 &lt;b&gt;Stream의 중간 연산과 최종 연산 구분&lt;/b&gt;, &lt;code&gt;map()&lt;/code&gt; 사용 전후의 Stream 타입 변화, &lt;code&gt;Optional&lt;/code&gt;의 각 처리 방식, 그리고 &lt;code&gt;Controller &amp;rarr; Service &amp;rarr; DAO&lt;/code&gt; 계층에서 데이터와 책임이 어디까지 전달되는지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘처럼 새로운 기능을 구현할 때 단순히 메서드 하나만 만드는 것이 아니라:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;요청은 어디서 시작되는가?
        &amp;darr;
어떤 Controller가 받아야 하는가?
        &amp;darr;
Service에는 어떤 형태로 전달할 것인가?
        &amp;darr;
DAO에서는 어떤 Stream 연산이 필요한가?
        &amp;darr;
결과는 어떤 타입으로 반환할 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 전체 흐름을 먼저 생각하는 습관을 들여야겠다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>CRUD</category>
      <category>java</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>LGCNS6기TIL</category>
      <category>Spring Boot</category>
      <category>til</category>
      <category>내일배움카드</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/39</guid>
      <comments>https://bbbicsan.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 20 Aug 2026 19:04:06 +0900</pubDate>
    </item>
    <item>
      <title>[LG CNS 6기] 16일차 TIL : Java 예외 처리와 MVC&amp;middot;Factory&amp;middot;DI 실습</title>
      <link>https://bbbicsan.tistory.com/38</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[LG CNS 6기] 16일차 TIL : Java 예외 처리와 MVC&amp;middot;Factory&amp;middot;DI 실습&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FnyJM/dJMcahr4SpM/kY1t9tEZa8oxHGwcGJvBKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FnyJM/dJMcahr4SpM/kY1t9tEZa8oxHGwcGJvBKK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FnyJM/dJMcahr4SpM/kY1t9tEZa8oxHGwcGJvBKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFnyJM%2FdJMcahr4SpM%2FkY1t9tEZa8oxHGwcGJvBKK%2Fimg.jpg&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;1203&quot; height=&quot;958&quot; data-filename=&quot;am inspire camp 사진.jpg&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&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;Java의 예외 처리 구조와 &lt;code&gt;throw / throws&lt;/code&gt;, 사용자 정의 Exception을 학습하고, 콘솔 Blog 프로그램을 직접 구현하며 &lt;b&gt;View &amp;rarr; FrontController &amp;rarr; Controller &amp;rarr; Service &amp;rarr; DAO&lt;/b&gt;로 이어지는 계층 구조와 Singleton&amp;middot;Factory&amp;middot;Dependency Injection의 흐름을 연결했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘 학습한 커리큘럼&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;전날 내용 복습
│
├─ Collection API
│  ├─ List
│  ├─ Set
│  └─ Map
│
├─ Generics
│  └─ &amp;lt;? extends PersonDTO&amp;gt;
│
├─ Stream API
│  ├─ filter()
│  ├─ map()
│  └─ forEach()
│
▼
Exception Handling
│
├─ Runtime Exception
├─ Checked Exception
├─ try - catch - finally
├─ throw / throws
├─ 사용자 정의 Exception
└─ BufferedReader / IOException
│
▼
웹 애플리케이션 구조
│
├─ Servlet
├─ MVC
├─ Controller
├─ Service
├─ DAO / Repository
└─ ORM &amp;rarr; DB
│
▼
Blog Console 실습
│
├─ View
├─ FrontController
├─ BeanFactory
├─ Controller
├─ Service
├─ DAO
├─ Singleton Pattern
├─ Factory Pattern
└─ Dependency Injection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 전날 배운 Collection&amp;middot;Generics&amp;middot;Stream API를 간단히 복습한 뒤 &lt;b&gt;Exception Handling&lt;/b&gt;을 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 지금까지 배웠던 객체지향, 인터페이스, 다형성, Collection, Stream API를 하나의 프로그램 안에서 활용하기 위해 콘솔 기반 Blog 프로그램을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 클래스를 여러 개 만드는 것이 아니라, 최종적으로는 사용자 요청이&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 전달되고 다시 조회 결과가 View까지 반환되는 흐름을 직접 구현하고 실행 로그로 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 학습 키워드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java Exception Handling&lt;/li&gt;
&lt;li&gt;Runtime Exception&lt;/li&gt;
&lt;li&gt;Checked Exception&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try - catch - finally&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;printStackTrace()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BufferedReader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InputStreamReader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IOException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;사용자 정의 Exception&lt;/li&gt;
&lt;li&gt;&lt;code&gt;throw&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;throws&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MVC&lt;/li&gt;
&lt;li&gt;Front Controller&lt;/li&gt;
&lt;li&gt;Singleton Pattern&lt;/li&gt;
&lt;li&gt;Factory Pattern&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Constructor Injection&lt;/li&gt;
&lt;li&gt;Service Interface / Implementation&lt;/li&gt;
&lt;li&gt;DAO / Repository&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Stream API와 메서드 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Collection &amp;middot; Generics &amp;middot; Stream API 복습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 초반에는 전날 학습했던 Collection API와 Generics, Stream API를 다시 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection의 대표적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Collection API
│
├─ List
│  ├─ 중복 허용
│  ├─ 순서 존재
│  └─ index를 이용한 접근
│
├─ Set
│  ├─ 중복 허용 X
│  └─ 순서 보장 X
│
└─ Map
   └─ { key : value }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generics의 타입 문자도 다시 정리했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;T &amp;rarr; Type
E &amp;rarr; Element
K &amp;rarr; Key
V &amp;rarr; Value
N &amp;rarr; Number&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generics를 사용하면 Collection의 요소 타입을 컴파일 시점부터 제한할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();

list.add(&quot;String&quot;);

// generic 문법에 의한 error
// list.add(10);
// list.add(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Generics
&amp;rarr; 컴파일 시점 타입 안정성
&amp;rarr; 잘못된 타입 저장 방지
&amp;rarr; 불필요한 Casting 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 장점이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Wildcard를 이용한 여러 하위 타입 관리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StudentDTO&lt;/code&gt;, &lt;code&gt;TeacherDTO&lt;/code&gt;, &lt;code&gt;ManagerDTO&lt;/code&gt;처럼 &lt;code&gt;PersonDTO&lt;/code&gt;를 상속한 객체들을 각각 List로 만들고 하나의 Map에 저장했다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;List&amp;lt;StudentDTO&amp;gt; studentList =
    new ArrayList&amp;lt;StudentDTO&amp;gt;();

List&amp;lt;TeacherDTO&amp;gt; teacherList =
    new ArrayList&amp;lt;TeacherDTO&amp;gt;();

List&amp;lt;ManagerDTO&amp;gt; managerList =
    new ArrayList&amp;lt;ManagerDTO&amp;gt;();

Map&amp;lt;String, List&amp;lt;? extends PersonDTO&amp;gt;&amp;gt; map =
    new HashMap&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map에는 다음처럼 저장했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;map.put(&quot;student&quot;, studentList);
map.put(&quot;teacher&quot;, teacherList);
map.put(&quot;manager&quot;, managerList);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Value 타입을&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;List&amp;lt;? extends PersonDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 선언했기 때문에 &lt;code&gt;PersonDTO&lt;/code&gt;의 하위 타입을 담고 있는 여러 List를 하나의 Map에서 관리할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 이전에 학습한 &lt;b&gt;상속 + Generics&lt;/b&gt;가 실제 자료구조와 연결되는 예제였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Collection에서 Stream까지 연결하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전날 사용했던 Stream API도 다시 확인했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; filteringList =
    personList.stream()
        .filter(
            s -&amp;gt; s.getName()
                  .startsWith(&quot;j&quot;)
        )
        .map(
            s -&amp;gt; s.getName()
                  .toUpperCase()
        )
        .collect(
            Collectors.toList()
        );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령형 코드에서는 반복문과 조건문을 직접 작성했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;for(int idx = 0;
    idx &amp;lt; personList.size();
    idx++) {

    PersonDTO person =
        personList.get(idx);

    if(
        person.getName()
              .startsWith(&quot;j&quot;)
    ) {

        filteringList.add(
            person.getName()
                  .toUpperCase()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream에서는 이를 다음처럼 연결할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Collection
    &amp;darr;
stream()
    &amp;darr;
filter()
    &amp;darr;
map()
    &amp;darr;
collect()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 오후 Blog 실습에서도 조회 결과를 출력할 때 다시 Stream API를 사용하게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Exception Handling&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 오전의 핵심 내용은 &lt;b&gt;Exception Handling&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;수업에서는 크게 다음과 같이 구분해서 확인했다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;Exception
│
├─ Runtime
│  └─ 실행 중 발생
│
└─ Compile 시점에 처리를 요구하는 예외
   └─ IOException 등&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 처리의 기본 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;try {

    // 예외가 발생할 수 있는 코드

} catch(Exception e) {

    // 예외가 발생했을 때 처리

} finally {

    // 예외 발생 여부와 관계없이 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Runtime Exception 직접 발생시키기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에 존재하지 않는 인덱스까지 접근하도록 코드를 작성하여 Runtime Exception을 직접 확인했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;String[] strAry = {
    &quot;jslim&quot;,
    &quot;inspire&quot;,
    &quot;lgcns&quot;
};

try {

    for(int idx = 0;
        idx &amp;lt;= strAry.length;
        idx++) {

        System.out.println(
            strAry[idx]
        );
    }

} catch(ArrayIndexOutOfBoundsException e) {

    e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 길이는 &lt;code&gt;3&lt;/code&gt;이지만 사용할 수 있는 index는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0
1
2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 반복문 조건을:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;idx &amp;lt;= strAry.length&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 만들었기 때문에 마지막에는 다음 접근이 발생한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;strAry[3]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 발생한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. try - catch - finally의 실행 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;catch&lt;/code&gt;는 Exception이 실제로 발생했을 때 수행되고, &lt;code&gt;finally&lt;/code&gt;는 Exception 발생 여부와 관계없이 수행된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;try {

    // 예외 발생 가능 코드

} catch(Exception e) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; catch 예외발생시에만 수행&quot;
    );

    e.printStackTrace();

} finally {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; finally 예외발생 여부와 상관없이 수행&quot;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;try 시작
   &amp;darr;
Exception 발생
   &amp;darr;
catch 실행
   &amp;darr;
finally 실행
   &amp;darr;
다음 코드 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception이 발생하지 않아도:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;try
 &amp;darr;
정상 수행
 &amp;darr;
finally&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 JDBC Connection과도 연결해서 설명했다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Connection
     &amp;darr;
try
     &amp;darr;
DB 작업
     &amp;darr;
catch
     &amp;darr;
예외 처리
     &amp;darr;
finally
     &amp;darr;
close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 반드시 정리해야 하는 자원을 처리할 때 &lt;code&gt;finally&lt;/code&gt;가 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. printStackTrace()로 예외 추적하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생했을 때 다음 코드를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;e.printStackTrace();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;printStackTrace()&lt;/code&gt;를 통해 단순히 Exception의 이름만 확인하는 것이 아니라 &lt;b&gt;어떤 메서드를 거쳐 어느 코드에서 Exception이 발생했는지&lt;/b&gt; 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;main()
   &amp;darr;
메서드 호출
   &amp;darr;
다른 메서드 호출
   &amp;darr;
Exception 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 오후 Blog 실습에서 &lt;code&gt;NumberFormatException&lt;/code&gt;이 발생했을 때도 Stack Trace를 통해:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Integer.parseInt(...)
        &amp;darr;
BlogReactView.landingPage(...)
        &amp;darr;
BlogReactApp.main(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 호출 흐름을 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. InputStream과 BufferedReader&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 처리에서는 &lt;code&gt;BufferedReader&lt;/code&gt;도 사용했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;BufferedReader br =
    new BufferedReader(
        new InputStreamReader(
            System.in
        )
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 흐름을 정리하면:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;System.in
    &amp;darr;
InputStreamReader
    &amp;darr;
BufferedReader
    &amp;darr;
readLine()
    &amp;darr;
String&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;System.in&lt;/code&gt;은 표준 입력 Stream이고, &lt;code&gt;InputStreamReader&lt;/code&gt;를 거쳐 문자 단위로 처리할 수 있도록 만든 뒤 &lt;code&gt;BufferedReader&lt;/code&gt;로 입력을 받았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. IOException 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BufferedReader.readLine()&lt;/code&gt;에서는 &lt;code&gt;IOException&lt;/code&gt;을 처리해야 했다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;String line = null;

try {

    System.out.print(
        &quot;메시지를 입력하세요 : &quot;
    );

    line =
        br.readLine();

} catch(IOException e) {

    e.printStackTrace();
}

System.out.println(line);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runtime Exception 예제와 달리 &lt;code&gt;IOException&lt;/code&gt;은 컴파일 단계에서 예외 처리 여부를 확인한다는 차이가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 통해 &lt;b&gt;예외의 종류에 따라 처리 방식이 다를 수 있다는 점&lt;/b&gt;을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 사용자 정의 Exception 만들기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java가 기본적으로 제공하는 Exception 외에 직접 예외 클래스를 만들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;code&gt;InspireException&lt;/code&gt;을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class InspireException
        extends Exception {

    public InspireException() {
    }

    public InspireException(
            String msg) {

        super(msg);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Exception&lt;/code&gt; 클래스를 상속받고, 메시지를 전달받는 생성자에서는:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;super(msg);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 부모 Exception에 메시지를 전달했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. throw와 throws의 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 정의 Exception을 직접 발생시키면서 &lt;code&gt;throw&lt;/code&gt;와 &lt;code&gt;throws&lt;/code&gt;의 차이를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public void first(int x)
        throws InspireException {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; first method start&quot;
    );

    if(x &amp;lt; 0) {

        throw new InspireException(
            &quot;양의 정수만 가능합니다!!&quot;
        );
    }

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; first method end&quot;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;throw&lt;/code&gt;는 실제 예외 객체를 발생시킨다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;throw new InspireException(
    &quot;양의 정수만 가능합니다!!&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;throws&lt;/code&gt;는 해당 메서드에서 발생할 수 있는 Exception의 처리 책임을 호출한 쪽으로 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public void first(int x)
        throws InspireException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;throw
&amp;rarr; Exception 발생

throws
&amp;rarr; Exception 처리 책임 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 사용자 정의 Exception 호출 흐름&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출하는 쪽에서는 다시 &lt;code&gt;try-catch&lt;/code&gt;를 이용해 Exception을 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;ExceptionDemo demo =
    new ExceptionDemo();

try {

    demo.first(10);

} catch(InspireException e) {

    e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;demo.first()
     &amp;darr;
조건 확인
     &amp;darr;
throw
     &amp;darr;
InspireException 발생
     &amp;darr;
throws
     &amp;darr;
호출부로 전달
     &amp;darr;
catch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;13. 웹 애플리케이션의 계층 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception 실습 이후에는 앞으로 배우게 될 웹 백엔드의 전체 구조를 먼저 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 설명한 흐름은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser
   &amp;darr;
React
   &amp;darr;
Axios
   &amp;darr;
HTTP
   &amp;darr;
Servlet / Spring Boot
   &amp;darr;
Controller
   &amp;darr;
Service
   &amp;darr;
DAO / Repository
   &amp;darr;
ORM
   &amp;darr;
DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층으로 보면:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Presentation Layer
        &amp;darr;
Business Layer
        &amp;darr;
Persistence Layer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 나누어 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 단순히 그림으로 보는 데서 끝나지 않고, 이 구조를 이해하기 위한 &lt;b&gt;Blog Console 프로그램&lt;/b&gt;을 직접 만들기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;14. BlogReactView로 View 역할 분리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 사용자에게 메뉴를 출력하는 &lt;code&gt;BlogReactView&lt;/code&gt;를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public class BlogReactView {

    public void landingPage() {

        System.out.println();

        System.out.println(
            &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Inspire Camp Blog Ver(1.0) &amp;lt;&amp;lt;&amp;lt;&amp;lt;&quot;
        );

        System.out.println(&quot;1. 전체검색&quot;);
        System.out.println(&quot;2. 게시글 상세보기&quot;);
        System.out.println(&quot;3. 입력화면&quot;);
        System.out.println(&quot;4. 수정화면&quot;);
        System.out.println(&quot;5. 게시글 삭제&quot;);
        System.out.println(&quot;6. 검색&quot;);
        System.out.println(&quot;99. 프로그램 종료&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 클래스에서는 View 객체를 만들고 &lt;code&gt;landingPage()&lt;/code&gt;를 호출했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BlogReactApp {

    public static void main(
            String[] args) {

        BlogReactView view =
            new BlogReactView();

        view.landingPage();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;main()&lt;/code&gt;에서 직접 모든 코드를 처리할 수도 있지만, 화면 출력 책임을 View로 따로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactApp
      &amp;darr;
BlogReactView
      &amp;darr;
화면 출력&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;15. Scanner를 이용한 사용자 입력&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View에 사용자 입력을 받을 수 있도록 &lt;code&gt;Scanner&lt;/code&gt;를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;private Scanner scan;

public BlogReactView() {

    scan =
        new Scanner(
            System.in
        );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;System.in&lt;/code&gt;에서 들어오는 입력을 &lt;code&gt;Scanner&lt;/code&gt;를 이용해 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 메뉴를 한 번 출력하고 끝내는 것이 아니라 계속 사용할 수 있도록:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;while(true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public void landingPage() {

    while(true) {

        // 메뉴 출력
        // 사용자 입력
        // 기능 실행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메뉴 출력
   &amp;darr;
사용자 입력
   &amp;darr;
기능 실행
   &amp;darr;
다시 메뉴 출력&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;16. 메뉴 입력과 switch 분기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 메뉴 번호를 문자열로 받은 뒤 정수로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;System.out.print(
    &quot;원하시는 메뉴 번호를 입력하세요 : &quot;
);

Integer number =
    Integer.parseInt(
        scan.nextLine()
    );

switch(number) {

    case 99:

        exit();
        break;

    default:

        break;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료 기능도 별도의 메서드로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;public void exit() {

    System.out.println();

    System.out.println(
        &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; 시스템을 종료하겠습니다.&quot;
    );

    System.exit(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;17. FrontController 만들기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View가 각 기능별 Controller를 직접 찾아 호출하지 않도록 중앙 요청 처리 역할의 &lt;code&gt;BlogFrontController&lt;/code&gt;를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private BlogFrontController front;

public BlogReactView() {

    scan =
        new Scanner(System.in);

    front =
        new BlogFrontController();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 흐름은:&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;사용자
   &amp;darr;
View
   &amp;darr;
FrontController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrontController는 사용자가 요청한 endpoint를 기준으로 실제 Controller를 찾아주는 역할을 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;18. Singleton Pattern으로 BeanFactory 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 객체들을 관리하기 위해 &lt;code&gt;BlogBeanFactory&lt;/code&gt;를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class BlogBeanFactory {

    private Map&amp;lt;String, Object&amp;gt; map;

    private static BlogBeanFactory instance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자를 &lt;code&gt;private&lt;/code&gt;으로 선언했다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;private BlogBeanFactory() {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서는 직접:&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;new BlogBeanFactory();&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public static BlogBeanFactory getInstance() {

    if(instance == null) {

        instance =
            new BlogBeanFactory();
    }

    return instance;
}&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;BlogBeanFactory.getInstance()
        &amp;darr;
instance == null ?
        &amp;darr;
new BlogBeanFactory()
        &amp;darr;
instance 저장
        &amp;darr;
동일 객체 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;19. Factory Pattern과 endpoint 매핑&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogBeanFactory&lt;/code&gt;에서는 &lt;code&gt;Map&lt;/code&gt;을 사용해 endpoint와 Controller를 연결했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;map.put(
    &quot;list.inspire&quot;,
    new ListController(service)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrontController는 endpoint를 전달해서 해당 Controller를 얻는다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public Object getBean(
        String endPoint) {

    return map.get(endPoint);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&quot;list.inspire&quot;
       &amp;darr;
BlogBeanFactory
       &amp;darr;
Map
       &amp;darr;
ListController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 흐름이 만들어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;20. FrontController에서 실제 Controller 호출&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogFrontController&lt;/code&gt;에서는 Singleton Factory를 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private BlogBeanFactory factory;

public BlogFrontController() {

    factory =
        BlogBeanFactory.getInstance();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체검색 기능에서는:&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;public List&amp;lt;BlogResponseDTO&amp;gt; list(
        String endPoint) {

    System.out.println(
        &quot;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; front controller endPoint : &quot;
        + endPoint
    );

    Object controller =
        factory.getBean(
            endPoint
        );

    return (
        (ListController) controller
    ).list();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 endpoint에 맞는 Controller를 찾고 실제 기능을 위임했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FrontController
      &amp;darr;
Factory
      &amp;darr;
Controller 찾기
      &amp;darr;
Controller.list()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;21. Service Interface 정의&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직을 담당하는 Service 계층도 인터페이스와 구현 클래스로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public interface BlogReactService {

    public List&amp;lt;BlogResponseDTO&amp;gt;
        list();

    public BlogResponseDTO
        read(int blogId);

    public int
        insert(BlogRequestDTO request);

    public int
        update(BlogRequestDTO request);

    public int
        delete(int blogId);

    public List&amp;lt;BlogResponseDTO&amp;gt;
        search(BlogRequestDTO request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 계약은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;list()
&amp;rarr; 전체조회

read()
&amp;rarr; 상세조회

insert()
&amp;rarr; 등록

update()
&amp;rarr; 수정

delete()
&amp;rarr; 삭제

search()
&amp;rarr; 검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 로직은 &lt;code&gt;BlogReactServiceImpl&lt;/code&gt;이 담당하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class BlogReactServiceImpl
        implements BlogReactService {&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;22. DAO에 인메모리 데이터 만들기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 실제 DB는 연결하지 않았기 때문에 &lt;code&gt;BlogReactDao&lt;/code&gt; 내부에 List 형태의 테스트 데이터를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class BlogReactDao {

    private List&amp;lt;BlogResponseDTO&amp;gt; blogs;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서 게시글 5개를 초기화했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;blogs = new ArrayList&amp;lt;&amp;gt;(List.of(

    BlogResponseDTO.builder()
        .blogId(1)
        .title(&quot;react&quot;)
        .content(&quot;state&quot;)
        .email(&quot;lim&quot;)
        .viewCnt(10)
        .build(),

    BlogResponseDTO.builder()
        .blogId(2)
        .title(&quot;java&quot;)
        .content(&quot;oop&quot;)
        .email(&quot;kim&quot;)
        .viewCnt(20)
        .build(),

    BlogResponseDTO.builder()
        .blogId(3)
        .title(&quot;spring&quot;)
        .content(&quot;mybatis&quot;)
        .email(&quot;lee&quot;)
        .viewCnt(30)
        .build(),

    BlogResponseDTO.builder()
        .blogId(4)
        .title(&quot;docker&quot;)
        .content(&quot;devops&quot;)
        .email(&quot;park&quot;)
        .viewCnt(40)
        .build(),

    BlogResponseDTO.builder()
        .blogId(5)
        .title(&quot;msa&quot;)
        .content(&quot;kafka&quot;)
        .email(&quot;lim&quot;)
        .viewCnt(50)
        .build()
));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 실제 DB 대신:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReactDao
      &amp;darr;
List&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용해 Repository / DAO의 역할을 연습했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;23. Dependency Injection 직접 구현하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 Blog 실습에서 가장 중요한 부분 중 하나였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Factory에서 필요한 객체들을 생성한 뒤 하위 객체에 전달했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dao =
    new BlogReactDao();

service =
    new BlogReactServiceImpl(
        dao
    );

map.put(
    &quot;list.inspire&quot;,
    new ListController(
        service
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 의존관계는:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactDao
     &amp;darr;
BlogReactServiceImpl
     &amp;darr;
ListController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogReactServiceImpl&lt;/code&gt;에서는 DAO를 생성자로 전달받았다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;private BlogReactDao dao;

public BlogReactServiceImpl(
        BlogReactDao dao) {

    this.dao = dao;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 내부에서 직접:&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;new BlogReactDao();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 호출하지 않고 외부에서 생성한 객체를 주입받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 오늘은 Spring 없이 직접 &lt;b&gt;Constructor Injection 기반의 DI 구조&lt;/b&gt;를 만들어본 셈이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;24. 전체검색 요청을 DAO까지 연결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 메뉴에서:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 전체검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 선택했을 때 요청이 실제 DAO까지 전달되도록 연결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 로그는 다음 순서로 출력됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; front controller endPoint : list.inspire

debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; list controller list()

debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog service list

debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog dao findByAll()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로그를 통해 다음 호출 체인을 실제로 확인했다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReactView
      &amp;darr;
BlogFrontController
      &amp;darr;
BlogBeanFactory
      &amp;darr;
ListController
      &amp;darr;
BlogReactServiceImpl
      &amp;darr;
BlogReactDao&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 그림으로만 보던 계층 구조가 실제 코드에서는 어떤 순서로 호출되는지 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;25. DAO 결과를 다시 View까지 반환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO에서 조회한 데이터는 다시 반대 방향으로 전달된다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReactDao
      &amp;darr;
BlogReactServiceImpl
      &amp;darr;
ListController
      &amp;darr;
BlogFrontController
      &amp;darr;
BlogReactView&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View에서는 다음처럼 데이터를 받았다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public void list() {

    System.out.println();

    System.out.println(
        &quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; 데이터 출력&quot;
    );

    String endPoint =
        &quot;list.inspire&quot;;

    List&amp;lt;BlogResponseDTO&amp;gt; response =
        front.list(
            endPoint
        );

    response.stream()
        .forEach(
            System.out::println
        );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 초반 복습했던 Stream API와 메서드 참조가 실제 Blog 조회 기능에서 다시 사용됐다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.stream()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리가 끝난 뒤에는 다시 메뉴가 출력되며 다음 사용자 입력을 기다렸다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  실습 기록&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Runtime Exception 발생 확인&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;String 배열 생성
&amp;rarr; 반복문 실행
&amp;rarr; 잘못된 index 접근
&amp;rarr; ArrayIndexOutOfBoundsException
&amp;rarr; catch
&amp;rarr; printStackTrace()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 범위를 벗어난 접근을 통해 Runtime Exception이 실제로 발생하는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BufferedReader 입력 처리&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.in
&amp;rarr; InputStreamReader
&amp;rarr; BufferedReader
&amp;rarr; readLine()
&amp;rarr; String&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IOException&lt;/code&gt;을 &lt;code&gt;try-catch&lt;/code&gt;로 처리하면서 입력 Stream 구조를 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 사용자 정의 Exception&lt;/h2&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;InspireException
&amp;rarr; Exception 상속

first(x)
&amp;rarr; x &amp;lt; 0 검사
&amp;rarr; throw new InspireException(...)
&amp;rarr; throws InspireException
&amp;rarr; 호출부 catch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;throw&lt;/code&gt;와 &lt;code&gt;throws&lt;/code&gt;가 실제 코드에서 어떻게 연결되는지 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Blog 메뉴 View 구현&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactApp
&amp;rarr; BlogReactView
&amp;rarr; landingPage()
&amp;rarr; 메뉴 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 메뉴가 정상적으로 출력되는 것을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Inspire Camp Blog Ver(1.0) &amp;lt;&amp;lt;&amp;lt;&amp;lt;
1. 전체검색
2. 게시글 상세보기
3. 입력화면
4. 수정화면
5. 게시글 삭제
6. 검색
99. 프로그램 종료&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Scanner 기반 메뉴 입력&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Scanner(System.in)
&amp;rarr; scan.nextLine()
&amp;rarr; Integer.parseInt()
&amp;rarr; switch(number)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 메뉴 번호를 입력하면 해당 기능으로 분기하도록 구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. FrontController + BeanFactory 연결&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; endpoint
&amp;rarr; BlogBeanFactory
&amp;rarr; Controller 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;list.inspire&quot;&lt;/code&gt; endpoint를 기준으로 &lt;code&gt;ListController&lt;/code&gt;를 조회하도록 만들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Dependency Injection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Factory에서:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;BlogReactDao 생성
        &amp;darr;
BlogReactServiceImpl(dao)
        &amp;darr;
ListController(service)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 객체를 생성하고 의존성을 전달했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 DI를 사용하기 전에 객체가 실제로 어떤 의존관계를 가지는지 직접 코드로 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 전체검색 End-to-End 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 메뉴 &lt;code&gt;1&lt;/code&gt;을 입력했을 때:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;원하시는 메뉴 번호를 입력하세요 : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다음 로그가 출력됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; front controller endPoint : list.inspire
debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; list controller list()
debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog service list
debug &amp;gt;&amp;gt;&amp;gt;&amp;gt; blog dao findByAll()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름이 실제로 연결되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 조회 결과 출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO에서 반환된 &lt;code&gt;List&amp;lt;BlogResponseDTO&amp;gt;&lt;/code&gt;를 View에서 Stream API로 출력했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;response.stream()
    .forEach(
        System.out::println
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 게시글 DTO들이 Console에 출력되는 것을 확인했고, 이후 다시 메뉴로 돌아와 다음 입력을 기다렸다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  문제 해결 과정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1. 메뉴에 문자열을 입력하면 NumberFormatException 발생&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;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;원하시는 메뉴 번호를 입력하세요 : jslim&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Exception in thread &quot;main&quot;
java.lang.NumberFormatException:
For input string: &quot;jslim&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 발생했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Integer number =
    Integer.parseInt(
        scan.nextLine()
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Integer.parseInt()&lt;/code&gt;는 숫자 형태의 문자열만 정수로 변환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;&quot;1&quot;
&amp;rarr; 정상

&quot;99&quot;
&amp;rarr; 정상

&quot;jslim&quot;
&amp;rarr; Integer 변환 불가능
&amp;rarr; NumberFormatException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Stack Trace를 통해:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Integer.parseInt(...)
        &amp;darr;
BlogReactView.landingPage(...)
        &amp;darr;
BlogReactApp.main(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 예외 발생 위치를 추적할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서는 사용자가 실수하거나 악의적으로 잘못된 데이터를 입력할 수 있기 때문에 Exception을 그대로 프로그램 종료로 이어지게 만들어서는 안 된다고 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 처리 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자 입력
   &amp;darr;
숫자가 아닌 문자열
   &amp;darr;
NumberFormatException
   &amp;darr;
예외 처리
   &amp;darr;
안내 메시지 출력
   &amp;darr;
다시 메뉴 입력&lt;/code&gt;&lt;/pre&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;완성된 재입력 처리 코드까지 구현한 것은 아니기 때문에 실제 구현 여부를 과장해서 기록하지 않았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✨ 오늘 배운 내용 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요했던 부분은 Exception 문법이나 MVC 구조를 각각 따로 외우는 것이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 학습했던 여러 Java 개념들이 하나의 프로그램 안에서 연결되는 것을 직접 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;상속 / 다형성
        &amp;darr;
Interface
        &amp;darr;
Collection / Generics
        &amp;darr;
Stream
        &amp;darr;
Exception
        &amp;darr;
MVC 계층 구조
        &amp;darr;
Factory / Singleton
        &amp;darr;
Dependency Injection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Blog 프로그램의 전체검색 기능에서는 다음 구조가 실제로 완성되었다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;                 사용자
                   │
                   &amp;darr;
            BlogReactView
                   │
                   &amp;darr;
         BlogFrontController
                   │
                   &amp;darr;
          BlogBeanFactory
                   │
          &quot;list.inspire&quot;
                   │
                   &amp;darr;
            ListController
                   │
                   &amp;darr;
          BlogReactService
                   │
                   &amp;darr;
       BlogReactServiceImpl
                   │
                   &amp;darr;
            BlogReactDao
                   │
                   &amp;darr;
       List&amp;lt;BlogResponseDTO&amp;gt;
                   │
                   &amp;darr;
            다시 View로 반환
                   │
                   &amp;darr;
       Stream API로 데이터 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 생성 관계는 Factory에서 다음과 같이 구성했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BlogReactDao
      │
      &amp;darr;
BlogReactServiceImpl
      │
      &amp;darr;
ListController&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 코드로 보면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dao =
    new BlogReactDao();

service =
    new BlogReactServiceImpl(
        dao
    );

map.put(
    &quot;list.inspire&quot;,
    new ListController(
        service
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 직접 구현하면서 &lt;b&gt;DI란 객체가 필요한 의존성을 직접 생성하는 것이 아니라 외부에서 전달받는 방식&lt;/b&gt;이라는 점을 더 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 오전에 학습한 Exception Handling이 오후의 Blog 실습에서 바로 실제 문제로 나타났다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;사용자 입력
&amp;rarr; &quot;jslim&quot;
&amp;rarr; Integer.parseInt()
&amp;rarr; NumberFormatException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Exception 처리는 단순한 문법이 아니라 &lt;b&gt;예상하지 못한 사용자 입력에서도 프로그램의 흐름을 유지하기 위한 설계 요소&lt;/b&gt;라는 것을 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  오늘의 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 지금까지 학습했던 Java 문법들이 실제 백엔드 구조 안에서 처음으로 크게 연결된 수업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오전에는 &lt;code&gt;try-catch-finally&lt;/code&gt;, &lt;code&gt;throw&lt;/code&gt;, &lt;code&gt;throws&lt;/code&gt;, 사용자 정의 Exception을 각각 코드로 실습했다. 배열 범위를 넘어가면서 발생한 &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;이나 &lt;code&gt;BufferedReader&lt;/code&gt;의 &lt;code&gt;IOException&lt;/code&gt;을 직접 처리해보면서 Exception이 발생했을 때 프로그램의 흐름이 어떻게 바뀌는지 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;printStackTrace()&lt;/code&gt;를 단순히 에러 메시지를 출력하는 메서드로 보는 것이 아니라 &lt;b&gt;호출 Stack을 따라 실제 예외가 발생한 지점을 추적하는 도구&lt;/b&gt;라는 점이 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후 Blog 실습에서는 이전까지 그림으로만 봤던 MVC 구조를 직접 클래스 단위로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;BlogReactApp
&amp;rarr; BlogReactView&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도였지만, 수업이 진행되면서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; BeanFactory
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;front controller
&amp;rarr; list controller
&amp;rarr; blog service
&amp;rarr; blog dao&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 실제 출력되는 것을 보면서 Controller, Service, DAO를 왜 나누는지 조금 더 명확하게 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 &lt;code&gt;Controller &amp;rarr; Service &amp;rarr; Repository&lt;/code&gt;라는 구조를 외우는 느낌이 강했다면, 오늘은 &lt;b&gt;사용자의 요청이 각 계층을 거쳐 내려갔다가 다시 데이터를 가지고 올라오는 흐름&lt;/b&gt;으로 이해하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BlogBeanFactory&lt;/code&gt;에서 객체를 직접 조립한 부분도 중요했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dao =
    new BlogReactDao();

service =
    new BlogReactServiceImpl(
        dao
    );

new ListController(
    service
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 직접 작성해보니 Service가 DAO를 필요로 하고, Controller가 Service를 필요로 한다는 객체 간 의존관계가 코드상으로 확실히 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Spring을 사용하지 않고 직접 객체를 생성하고 주입하고 있지만, 오히려 이렇게 먼저 구현해보니 앞으로 Spring의 IoC Container와 DI를 배울 때 &lt;b&gt;Spring이 어떤 작업을 대신 해주는지 비교하기 쉬울 것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 메뉴에 &lt;code&gt;&quot;jslim&quot;&lt;/code&gt;을 입력하면서 발생한 &lt;code&gt;NumberFormatException&lt;/code&gt;도 오전에 배운 Exception Handling과 바로 연결됐다.&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;pre class=&quot;&quot;&gt;&lt;code&gt;기능이 정상적으로 동작하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;잘못된 입력이 들어와도
프로그램의 흐름을 유지할 수 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 생각하면서 코드를 작성해야겠다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 복습할 부분은 &lt;b&gt;&lt;code&gt;throw&lt;/code&gt;와 &lt;code&gt;throws&lt;/code&gt;의 역할 차이&lt;/b&gt;, &lt;code&gt;FrontController &amp;rarr; Factory &amp;rarr; Controller&lt;/code&gt; 요청 분배 흐름, 그리고 &lt;b&gt;Factory에서 직접 구현한 생성자 기반 DI 구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 오늘 직접 만든:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;View
&amp;rarr; FrontController
&amp;rarr; Controller
&amp;rarr; Service
&amp;rarr; DAO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 코드 없이도 설명할 수 있을 정도로 다시 정리한 뒤, 이후 Spring에서 동일한 구조가 어떻게 변경되는지 비교해보려고 한다.&lt;/p&gt;</description>
      <category>LG CNS AM INSPIRE 6기/Backend</category>
      <category>Backend</category>
      <category>Dependencyinjection</category>
      <category>java</category>
      <category>K-DT</category>
      <category>LGCNS</category>
      <category>LGCNS6기</category>
      <category>LGCNS6기TIL</category>
      <category>MVC</category>
      <category>til</category>
      <category>내일배움카드</category>
      <author>bbbicsan</author>
      <guid isPermaLink="true">https://bbbicsan.tistory.com/38</guid>
      <comments>https://bbbicsan.tistory.com/38#entry38comment</comments>
      <pubDate>Wed, 19 Aug 2026 18:13:04 +0900</pubDate>
    </item>
  </channel>
</rss>