<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>leve68</title>
    <link>https://leve68.tistory.com/</link>
    <description>leve68 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 16 Jun 2026 07:11:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>leve68</managingEditor>
    <image>
      <title>leve68</title>
      <url>https://tistory1.daumcdn.net/tistory/7541848/attach/8fb798c7b37240df9767aa0528d2fe49</url>
      <link>https://leve68.tistory.com</link>
    </image>
    <item>
      <title>WebRTC로 시작하는 실시간 웹 통신</title>
      <link>https://leve68.tistory.com/entry/WebRTC%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%B9-%ED%86%B5%EC%8B%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 개발을 하다 보면 실시간 화상 통화나 음성 채팅 기능을 구현해야 하는 순간이 찾아옵니다. 기존에 REST API로 JSON 데이터만 주고받던 개발자에게 실시간 스트리밍은 완전히 새로운 영역입니다. WebRTC는 이러한 복잡한 실시간 통신을 웹 브라우저에서 간단하게 구현할 수 있게 해주는 기술입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실시간 통신이 어려운 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹의 태생적 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹은 원래 문서를 공유하기 위해 설계되었습니다. HTTP는 요청하면 응답하는 단순한 구조로, 지속적인 연결이나 실시간 데이터 교환에는 적합하지 않았습니다. 실시간 화상 통화를 구현하려면 근본적으로 다른 접근이 필요했습니다.&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;과거에는 Flash나 Silverlight 같은 플러그인으로 이 문제를 해결했지만, 보안 취약점과 성능 문제로 인해 결국 웹에서 퇴출되었습니다. 순수 웹 기술만으로 실시간 통신을 구현할 수 있는 새로운 해답이 필요했고, 바로 그 해답이 WebRTC입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebRTC의 해결책: P2P 직접 연결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중앙 서버의 부담 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC가 제시한 핵심 아이디어는 단순하지만 혁신적입니다. &lt;b&gt;서버를 거치지 않고 사용자끼리 직접 연결하자&lt;/b&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;WebRTC는 W3C와 IETF 표준으로 채택되어 모든 주요 브라우저에서 네이티브 지원됩니다. 별도 플러그인 없이 JavaScript API만으로 카메라 접근부터 P2P 전송까지 모든 것이 가능합니다. 이는 웹 플랫폼에서 네이티브 앱 수준의 실시간 통신을 실현한 기술적 성취입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NAT라는 장벽&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;P2P 연결의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로는 완벽한 P2P 연결이지만, 현실은 그렇게 단순하지 않습니다. 가장 큰 장벽은 바로 &lt;b&gt;NAT(Network Address Translation)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 사용자는 공유기 뒤에 숨어있습니다. 이는 IPv4 주소 부족 문제를 해결하기 위한 불가피한 선택이었지만, P2P 연결에는 큰 걸림돌이 됩니다. 내부에서는 192.168.1.100 같은 사설 IP만 알 수 있고, 상대방이 어떤 공인 IP와 포트로 접근해야 하는지 알기 어렵습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NAT의 다양한 유형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NAT에도 여러 종류가 있고, 각각 다른 특성을 보입니다. &lt;b&gt;Full Cone NAT&lt;/b&gt;는 비교적 관대해서 한 번 매핑이 생성되면 외부에서 자유롭게 접근할 수 있지만, &lt;b&gt;Symmetric NAT&lt;/b&gt;는 매우 까다로워서 통신 대상마다 다른 매핑을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업 환경에서는 방화벽이라는 추가 장벽도 있습니다. 특정 포트만 허용하고, 트래픽을 면밀히 검사하는 Deep Packet Inspection까지 동원됩니다. 이런 환경에서 P2P 연결을 수립하는 것은 마치 미로에서 출구를 찾는 것과 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NAT를 극복하는 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Signaling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P2P 연결을 위해서는 먼저 상대방을 찾아야 합니다. 마치 전화를 걸기 위해 전화번호를 알아야 하는 것처럼 말입니다. 이 역할을 하는 것이 &lt;b&gt;Signaling&lt;/b&gt; 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Signaling은 고정 IP를 가진 중개 서버를 통해 이루어집니다. 양쪽 사용자가 이 서버에 접속해서 서로의 연결 정보를 교환합니다. WebRTC 표준에서는 Signaling 방법을 명시하지 않았기 때문에 WebSocket, Socket.IO, 심지어 이메일로도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Signaling 서버는 연결 정보만 중개할 뿐, 실제 미디어 데이터는 전혀 거치지 않는다는 것입니다. 마치 소개팅에서 중매인이 처음 만남만 주선하고 나중에는 빠지는 것과 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STUN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NAT 뒤에 있는 사용자가 자신의 외부 주소를 알아내는 방법이 바로 &lt;b&gt;STUN(Session Traversal Utilities for NAT)&lt;/b&gt; 서버입니다. 마치 거울을 보고 자신의 모습을 확인하는 것처럼, STUN 서버는 &quot;당신이 외부에서 보이는 주소는 203.142.167.89:62000입니다&quot;라고 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STUN 서버는 비교적 단순하고 가벼워서 Google같은 회사에서 무료로 제공하기도 합니다. 하지만 모든 NAT 환경에서 동작하는 것은 아닙니다. 특히 Symmetric NAT 환경에서는 STUN만으로는 해결되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TURN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STUN으로도 연결이 안 될 때 등장하는 것이 &lt;b&gt;TURN(Traversal Using Relays around NAT)&lt;/b&gt; 서버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TURN 서버는 클라이언트를 대신해서 트래픽을 중계하므로 높은 대역폭과 처리 능력이 필요합니다. 그래서 대부분 유료 서비스이고, 운영 비용도 상당합니다. 하지만 가장 까다로운 네트워크 환경에서도 연결을 보장할 수 있는 역할을 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 연결이 이루어지는 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SDP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결할 수 있는 주소를 알았다고 해서 바로 통화가 가능한 것은 아닙니다. 서로 어떤 방식으로 통신할지 합의해야 합니다. 이때 사용하는 것이 &lt;b&gt;SDP(Session Description Protocol)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SDP는 마치 이력서와 같습니다. &quot;저는 H.264와 VP8 코덱을 지원하고, 최대 1080p 해상도까지 가능하며, Opus 오디오 코덱을 사용합니다&quot;라는 식으로 자신의 능력을 상세히 기술합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결을 요청하는 쪽(Caller)이 먼저 자신의 SDP를 &lt;b&gt;Offer&lt;/b&gt;로 제시하고, 응답하는 쪽(Callee)이 이를 검토해서 자신이 지원하는 부분만 골라 &lt;b&gt;Answer&lt;/b&gt;로 응답합니다. 이 과정을 통해 양쪽이 모두 지원하는 최적의 통신 방식이 결정됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ICE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ICE(Interactive Connectivity Establishment)&lt;/b&gt;는 가능한 모든 연결 경로를 탐색하고 그 중 가장 좋은 것을 선택하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ICE는 세 가지 유형의 후보를 수집합니다. &lt;b&gt;Host Candidate&lt;/b&gt;는 로컬 네트워크의 직접 주소이고, &lt;b&gt;Server Reflexive Candidate&lt;/b&gt;는 STUN을 통해 얻은 외부 주소이며, &lt;b&gt;Relay Candidate&lt;/b&gt;는 TURN 서버 주소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 후보는 우선순위가 있어서 일반적으로 Host &amp;gt; Server Reflexive &amp;gt; Relay 순서로 시도됩니다. 가장 빠르고 효율적인 경로를 우선 시도하고, 안 되면 차선책을 사용하는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Connectivity Check&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ICE는 수집된 후보(로컬 및 원격)를 조합해 여러 가능한 연결 경로를 구성하고, 이 경로들 중 실제로 사용할 수 있는지를 테스트합니다. 이 과정을 &lt;b&gt;Connectivity Check&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 로컬 후보가 3개, 원격 후보가 3개라면 총 9개의 조합이 만들어질 수 있습니다. ICE는 이 후보 쌍들을 우선순위에 따라 정렬하고, 각 조합에 대해 실제로 패킷을 전송해 응답이 오는지를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트는 후보가 생성되는 즉시 시작되며(Trickle ICE), 가장 먼저 성공적으로 연결된 경로가 &lt;b&gt;최종 경로&lt;/b&gt;로 선택됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안과 품질 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 암호화로 안전한 통신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 모든 통신을 자동으로 암호화합니다. &lt;b&gt;DTLS(Datagram Transport Layer Security)&lt;/b&gt;로 시그널링 단계에서 키를 교환하고, &lt;b&gt;SRTP(Secure Real-time Transport Protocol)&lt;/b&gt;로 실제 미디어 스트림을 암호화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 사용자가 별도로 설정할 필요 없이 자동으로 이루어집니다. 마치 HTTPS가 웹 브라우징을 자동으로 암호화하는 것처럼, WebRTC도 실시간 통신을 기본적으로 보호합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 상황에 맞춘 품질 조절&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 네트워크 상황을 실시간으로 모니터링하면서 품질을 자동 조절합니다. 패킷 손실이 늘어나면 비트레이트를 낮추고, 네트워크가 좋아지면 다시 품질을 높입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;b&gt;Google Congestion Control&lt;/b&gt;이라는 정교한 알고리즘이 동작합니다. 단순히 패킷 손실만 보는 것이 아니라 지연 시간, 지터, 대역폭 등을 종합적으로 분석해서 최적의 전송률을 계산합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 WebRTC API 활용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getUserMedia로 미디어 접근&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC의 첫 번째 단계는 사용자의 카메라와 마이크에 접근하는 것입니다. 이를 위해 getUserMedia API를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
}).then(stream =&amp;gt; {
    // 비디오 요소에 스트림 연결
    document.getElementById('localVideo').srcObject = stream;
}).catch(error =&amp;gt; {
    console.error('미디어 접근 실패:', error);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTCPeerConnection으로 연결 수립&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 P2P 연결은 RTCPeerConnection 객체를 통해 이루어집니다. 이 객체가 앞서 설명한 모든 복잡한 과정을 자동화해줍니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const pc = new RTCPeerConnection({
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
    ]
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순수 WebSocket으로 Signaling 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 단순한 WebSocket 중개자 역할을 담당합니다. 복잡한 STOMP나 SockJS 없이도 충분히 동작합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프론트엔드 (WebSocket 연결)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// WebSocket 연결 설정 (Spring Boot 서버와 연결)
const socket = new WebSocket('ws://localhost:8080/signaling');

// 메시지 수신 처리
socket.onmessage = async (event) =&amp;gt; {
    const message = JSON.parse(event.data);

    switch (message.type) {
        case 'offer':
            await handleOffer(message.offer);
            break;
        case 'answer':
            await handleAnswer(message.answer);
            break;
        case 'ice-candidate':
            await handleIceCandidate(message.candidate);
            break;
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Offer/Answer 교환 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 연결 과정에서는 SDP Offer와 Answer를 교환해야 합니다. 여기서 Spring Boot가 단순한 중개자 역할을 담당합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Caller 측 Offer 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 통화를 시작하는 측에서 Offer 생성
async function startCall() {
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    // Spring Boot 서버를 통해 상대방에게 전송
    socket.send(JSON.stringify({
        type: 'offer',
        offer: offer
    }));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Callee 측 Answer 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// Offer를 받은 측에서 Answer 생성
async function handleOffer(offer) {
    await pc.setRemoteDescription(offer);

    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);

    // Spring Boot 서버를 통해 응답 전송
    socket.send(JSON.stringify({
        type: 'answer',
        answer: answer
    }));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot의 간단한 중개 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 받은 메시지를 단순히 다른 클라이언트들에게 전달하는 역할만 수행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebSocket 설정&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SignalingHandler(), &quot;/signaling&quot;)
                .setAllowedOrigins(&quot;*&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 중개 Handler&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class SignalingHandler extends TextWebSocketHandler {

    private final Set&amp;lt;WebSocketSession&amp;gt; sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 발신자를 제외한 모든 클라이언트에게 메시지 전달
        for (WebSocketSession s : sessions) {
            if (!s.equals(session) &amp;amp;&amp;amp; s.isOpen()) {
                s.sendMessage(message);
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ICE Candidate 교환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결 경로를 찾기 위해 ICE Candidate 정보도 교환해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ICE Candidate 이벤트 처리 (발견한 클라이언트에서 발신)
pc.onicecandidate = (event) =&amp;gt; {
    if (event.candidate) {
        socket.send(JSON.stringify({
            type: 'ice-candidate',
            candidate: event.candidate
        }));
    }
};

// ICE Candidate 수신 처리 (응답이 요구되지 않음)
async function handleIceCandidate(candidate) {
    await pc.addIceCandidate(candidate);
}
&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 Boot는 단순히 메시지만 중개하고, 실제 미디어 데이터는 클라이언트끼리 직접 P2P로 전송됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.webp&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxi0ru/btsPjo4MIz8/TFMkZqOMMfGGuTzF2DMqEK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxi0ru/btsPjo4MIz8/TFMkZqOMMfGGuTzF2DMqEK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxi0ru/btsPjo4MIz8/TFMkZqOMMfGGuTzF2DMqEK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxi0ru%2FbtsPjo4MIz8%2FTFMkZqOMMfGGuTzF2DMqEK%2Fimg.webp&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;526&quot; height=&quot;694&quot; data-filename=&quot;image.webp&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1338&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;WebRTC를 공부하며 네트워크의 한계를 어떻게 극복했는지, 보안과 성능을 어떻게 양립시켰는지, 복잡한 협상 과정을 어떻게 자동화했는지를 알 수 있었습니다.&lt;/p&gt;</description>
      <category>Project/Web</category>
      <category>WebRTC</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/34</guid>
      <comments>https://leve68.tistory.com/entry/WebRTC%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%B9-%ED%86%B5%EC%8B%A0#entry34comment</comments>
      <pubDate>Tue, 15 Jul 2025 20:14:40 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스 모델링</title>
      <link>https://leve68.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%AA%A8%EB%8D%B8%EB%A7%81</link>
      <description>&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;h3 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;b&gt;데이터 무결성 유지&lt;/b&gt;는 가장 기본적이면서도 중요한 목적입니다. 잘못된 데이터가 저장되는 것을 방지하고, 데이터 간의 일관성과 정확성을 보장합니다. 예를 들어, 존재하지 않는 부서에 직원이 배정되는 상황을 미연에 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 최소화&lt;/b&gt;를 통해 저장 공간을 절약하고 데이터 관리의 복잡성을 줄입니다. 동일한 정보가 여러 곳에 중복 저장되면 데이터 불일치 문제가 발생할 수 있으며, 이를 체계적으로 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율적인 데이터 검색&lt;/b&gt;이 가능하도록 최적화된 구조를 설계합니다. 자주 사용되는 쿼리 패턴을 고려하여 인덱스 설계나 테이블 구조를 결정함으로써 성능을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 용이성&lt;/b&gt;을 확보하여 향후 요구사항 변경이나 기능 확장에 유연하게 대응할 수 있도록 합니다. 잘 설계된 데이터베이스는 비즈니스 로직의 변화에도 큰 수정 없이 적응할 수 있습니다.&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;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;&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;h3 data-ke-size=&quot;size23&quot;&gt;2단계. 개념적 데이터 모델링&lt;/h3&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;이 단계에서 가장 많이 사용되는 것이 개체-관계 모델입니다. 개체(엔티티) 추출, 속성 추출, 관계 추출을 통해 업무와 데이터 간의 관계를 구상하고, 핵심 Entity와 관계 도출을 수행합니다.&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;주요 산출물은 개념 ERD(Entity Relationship Diagram)입니다. 개념 ERD는 피터 첸 표기법을 사용하여 엔티티, 관계, 속성을 시각적으로 표현합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계. 논리적 데이터 모델링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적 데이터 모델링의 결과인 ER모델을 구체화된 업무 중심의 관계형 데이터 모델로 만드는 단계입니다. 개념 모델을 구체화하여 데이터의 논리적 구조를 정의합니다.&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;이 단계에서는 정규화(Normalization) 과정이 핵심입니다. 논리 ERD를 작성하고, 릴레이션 스키마를 생성합니다. E-R모델을 관계형 데이터 모델로 변환하는 과정에서 엔티티는 릴레이션(Relation)으로, 속성은 속성으로, 후보 키는 기본 키와 대체 키로, 관계는 외래키(1:1관계, 1:N관계)나 릴레이션(N:M관계)으로 해소됩니다.&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;특히 N:M 관계를 해소하는 과정이 중요합니다. N:M 관계는 데이터 중복이 발생할 수 밖에 없으므로, 관계를 릴레이션으로 도출해서 1:N의 관계로 풀어내야 합니다.&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;논리 모델링 결과를 실제 데이터베이스 환경에서 구현 가능한 형태로 변환하는 과정입니다. DBMS의 특성과 성능을 고려하여 데이터 타입 및 크기, 인덱스, 파티셔닝, 저장 구조 등을 결정합니다.&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;성능, 무결성, 보안 등을 고려하여 최적의 데이터베이스 구조를 설계하며, 역정규화(Denormalization) 과정을 통해 정규화된 데이터를 성능 향상을 위해 일부 중복을 허용하거나 다시 합치는 과정이 포함됩니다.&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;최종 산출물은 물리 ERD입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개념적 데이터 모델링의 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적 데이터 모델링에서 가장 중요한 개념은 개체(Entity)입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개체(Entity)의 정의와 특성&lt;/h3&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;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유형 엔티티 | &lt;/b&gt;물리적 형태가 있는 엔티티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개념 엔티티 &lt;b&gt;|&lt;/b&gt; &lt;/b&gt;물리적 형태는 없지만 관리해야 할 개념적 정보를 나타내는 엔티티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사건 엔티티 &lt;b&gt;|&lt;/b&gt; &lt;/b&gt;업무를 수행함에 따라 발생되는 엔티티&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발생 시점에 따른 분류&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 엔티티 | &lt;/b&gt;원래 존재하는 정보로 타 엔티티에 의존하지 않고 독립적으로 생성이 가능한 엔티티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중심 엔티티 | &lt;/b&gt;기본 엔티티로부터 발생하는 엔티티로 업무에서 중심적인 역할을 하는 엔티티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행위 엔티티 | &lt;/b&gt;중심 엔티티로부터 보다 더 세부적인 정보를 위해 파생된 엔티티&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개체 식별 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개체를 정확히 식별하는 것은 데이터베이스 모델링의 성공을 좌우하는 핵심 과정입니다. 다음 다섯 가지 기준을 순차적으로 적용하면 효과적으로 개체를 식별할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;요구사항 문서를 분석할 때는 명사에 주목해야 합니다. 개체와 속성의 후보는 대부분 명사 형태로 나타나기 때문입니다. 예를 들어 &quot;고객이 상품을 주문한다&quot;라는 문장에서 '고객', '상품', '주문'이 개체 후보가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 시스템 범위 내 명사만 선별&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;같은 의미를 다르게 표현한 단어들을 통일해야 합니다. '고객'과 '회원', '상품'과 '제품'처럼 비슷한 의미로 사용되는 용어들 중 하나로 표준화합니다. 이는 혼란을 방지하고 일관성을 유지하기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 복수 속성 보유 여부 확인&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;5. 집합 개념 여부 판단&lt;/b&gt;&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;정규화(Normalization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규화는 데이터베이스에서 중복을 최소화하고 데이터 무결성 유지를 위해 새로운 엔티티를 도출하는 과정입니다. 속성 간에 존재하는 함수적 종속성을 기반으로 유연한 구조로 정제하는 과정이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 1정규화부터 제 5정규화에 이르는 단계가 있지만, 보통 제 3정규화까지면 충분한 경우가 대부분입니다&lt;b&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;제 1정규화(First Normal Form, 1NF)&lt;/b&gt;는 모든 속성이 원자값(Atomic Value)을 가져야 한다는 조건입니다. 컬럼에 다중 값을 저장하면 안 되며, 다치 속성을 갖는 릴레이션이 대상이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제 2정규화(Second Normal Form, 2NF)&lt;/b&gt;는 1NF를 만족해야 하고, 부분적 함수 종속을 제거해야 합니다. 기본 키가 복합키 형태이며 기본 키의 일부에만 종속성을 갖는 속성을 지닌 릴레이션이 대상입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제 3정규화(Third Normal Form, 3NF)&lt;/b&gt;는 2NF를 만족해야 하고, 이행적(Transitive) 함수 종속을 제거해야 합니다. 기본 키가 아닌 속성이 다른 속성의 결정자 역할을 하는 릴레이션이 대상입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관계 해소 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적 모델링에서 식별된 관계들을 관계형 데이터베이스에서 구현 가능한 형태로 변환하는 과정도 중요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1:1 관계&lt;/b&gt;는 한 엔티티의 한 개의 레코드가 다른 엔티티의 한 개의 레코드와만 연결되는 관계입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1:N 관계&lt;/b&gt;는 가장 많이 보이는 관계 유형으로, 한 엔티티의 한 개의 레코드가 다른 엔티티의 여러 개의 레코드와 연결될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N:M 관계&lt;/b&gt;는 한 엔티티의 한 개의 레코드가 다른 엔티티의 여러 개의 레코드와 연결될 수 있으며, 관계(연결) 엔티티로 해결 필요합니다. N:M 관계는 데이터 중복이 발생할 수 밖에 없으므로, 관계를 릴레이션으로 도출해서 1:N의 관계로 풀어냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;물리적 데이터 모델링과 역정규화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적 데이터 모델링에서는 논리 모델링 결과를 실제 데이터베이스 환경에서 구현 가능한 형태로 변환합니다. 이 과정에서 DBMS의 특성과 성능을 고려해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역정규화(Denormalization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역정규화는 정규화된 데이터를 성능 향상을 위해 일부 중복을 허용하거나 다시 합치는 과정입니다. 테이블을 병합(merge) 하거나, 조회 속도를 높이기 위해 중복 데이터를 허용하는 작업이 포함됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컬럼 역정규화&lt;/b&gt;는 잦은 조인이 유발되는 컬럼 추가, 조인 패스가 긴 컬럼 추가, 파생 컬럼 추가가 포함됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테이블 역정규화&lt;/b&gt;는 집계 테이블 추가, 잦은 조인이 유발되는 테이블과 통합이 있습니다.&lt;/li&gt;
&lt;/ul&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;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/33</guid>
      <comments>https://leve68.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%AA%A8%EB%8D%B8%EB%A7%81#entry33comment</comments>
      <pubDate>Tue, 8 Jul 2025 15:23:03 +0900</pubDate>
    </item>
    <item>
      <title>CI/CD 기본 개념</title>
      <link>https://leve68.tistory.com/entry/CICD-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현대 소프트웨어 개발에서 코드를 작성하고 배포하는 과정은 매우 복잡하고 반복적입니다. 개발자가 코드를 작성한 후 테스트하고, 다른 개발자의 코드와 통합하고, 최종적으로 실제 서버에 배포하는 모든 과정을 수동으로 진행한다면 많은 시간과 노력이 소모됩니다. CI/CD는 이러한 반복적인 과정을 자동화하여 개발 효율성을 크게 향상시키는 핵심 개념입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CI/CD란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD는 &lt;b&gt;Continuous Integration(지속적 통합)&lt;/b&gt;과 &lt;b&gt;Continuous Deployment(지속적 배포)&lt;/b&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;CI/CD가 제공하는 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD를 도입하면 이러한 문제들을 체계적으로 해결할 수 있습니다. 코드 변경사항이 발생할 때마다 자동으로 빌드와 테스트가 실행되어 문제를 조기에 발견할 수 있고, 테스트를 통과한 코드만 자동으로 배포되어 안정성이 보장됩니다. 또한 반복적인 작업이 자동화되어 개발자는 핵심 비즈니스 로직 개발에 더 집중할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반적인 CI/CD 파이프라인 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 파이프라인은 일반적으로 다음과 같은 순서로 진행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GelnH/btsO789ddUx/Yl7fFumoanfgubZgiEn9P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GelnH/btsO789ddUx/Yl7fFumoanfgubZgiEn9P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GelnH/btsO789ddUx/Yl7fFumoanfgubZgiEn9P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGelnH%2FbtsO789ddUx%2FYl7fFumoanfgubZgiEn9P1%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;1560&quot; height=&quot;422&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 코드 작성 및 커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 새로운 기능을 개발하거나 버그를 수정한 후 코드를 커밋합니다. 이때 커밋은 CI/CD 파이프라인의 시작점이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 자동 빌드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋이 감지되면 자동으로 빌드 프로세스가 시작됩니다. 소스 코드가 컴파일되고 실행 가능한 형태로 변환됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 자동 테스트&lt;/h3&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;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CI/CD 도구의 선택지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD를 구현할 수 있는 다양한 도구들이 있으며, 각각 고유한 특징과 장단점을 가지고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub이 제공하는 CI/CD 플랫폼으로, GitHub 저장소와 완벽하게 통합되어 있습니다. 가장 큰 장점은 별도의 서버 구축 없이 무료로 사용할 수 있다는 점입니다. GitHub에 코드를 호스팅하고 있다면 추가 설정 없이 바로 CI/CD를 구축할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Jenkins&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 자동화 서버로 매우 유연하고 확장 가능한 플랫폼입니다. 다양한 플러그인을 통해 거의 모든 개발 환경에 적용할 수 있습니다. 하지만 별도의 서버에 Jenkins를 설치하고 관리해야 한다는 단점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타 도구들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circle CI, Travis CI 등도 널리 사용되는 CI/CD 도구들입니다. 각각 고유한 특징을 가지고 있으며, 프로젝트의 요구사항에 따라 적절한 도구를 선택할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Actions를 통한 CI/CD 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 현재 가장 접근하기 쉬운 CI/CD 도구 중 하나입니다. GitHub Actions를 일종의 클라우드 컴퓨터라고 생각하면 이해하기 쉽습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions 동작 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions를 활용한 CI/CD의 전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zRiMB/btsO6w4iTxr/18oehhpLd4VO89JD6kTSa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zRiMB/btsO6w4iTxr/18oehhpLd4VO89JD6kTSa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zRiMB/btsO6w4iTxr/18oehhpLd4VO89JD6kTSa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzRiMB%2FbtsO6w4iTxr%2F18oehhpLd4VO89JD6kTSa0%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;1272&quot; height=&quot;248&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발자가 코드를 작성하고 로컬에서 커밋을 생성&lt;/li&gt;
&lt;li&gt;커밋을 GitHub 저장소에 푸시&lt;/li&gt;
&lt;li&gt;푸시 이벤트를 GitHub Actions가 감지하여 미리 정의된 워크플로우를 실행&lt;/li&gt;
&lt;li&gt;워크플로우에 정의된 빌드, 테스트, 배포 작업이 순차적으로 진행&lt;/li&gt;
&lt;li&gt;최종적으로 서버에 새로운 코드가 배포되고 서비스 재시작&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Actions 기본 문법과 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 YAML 형식의 설정 파일을 통해 워크플로우를 정의합니다. 이 파일은 프로젝트 루트의 .github/workflows/ 디렉토리에 위치해야 합니다.&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;프로젝트명/
├── .github/
│   └── workflows/
│       └── deploy.yml
└── (기타 프로젝트 파일들)&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;deploy.yml 파일에는 다음과 같은 구조로 워크플로우를 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 워크플로우의 이름을 지정합니다
name: Github Actions 실행시켜보기

# 언제 이 워크플로우가 실행될지 이벤트를 정의합니다
on:
  push:
    branches:
      - main

# 실행할 작업들을 정의합니다
jobs: 
  My-Deploy-Job: 
    # 어떤 운영체제에서 실행할지 지정합니다
    runs-on: ubuntu-latest
    
    # 순차적으로 실행될 단계들을 정의합니다
    steps: 
      - name: Hello World 출력하기
        run: echo &quot;Hello World&quot;
        
      - name: 여러 명령어 실행하기
        run: |
          echo &quot;Good&quot;
          echo &quot;Morning&quot;
          
      - name: GitHub Actions 기본 변수 사용하기
        run: |
          echo $GITHUB_SHA
          echo $GITHUB_REPOSITORY

      - name: Secret 변수 사용하기
        run: |
          echo ${{ secrets.MY_NAME }}
          echo ${{ secrets.MY_HOBBY }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 구성 요소 이해하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Workflow&lt;/b&gt;는 하나의 YAML 파일에 정의된 전체 자동화 과정을 의미합니다. 하나의 저장소에 여러 개의 워크플로우를 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event&lt;/b&gt;는 워크플로우가 언제 실행될지를 결정하는 트리거입니다. 푸시, 풀 리퀘스트, 스케줄 등 다양한 이벤트를 설정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Job&lt;/b&gt;은 워크플로우의 구성 단위로, 하나의 워크플로우는 여러 개의 Job으로 구성될 수 있습니다. 기본적으로 Job들은 병렬로 실행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Step&lt;/b&gt;은 Job 내에서 순차적으로 실행되는 개별 작업 단위입니다. 각 Step은 명령어를 실행하거나 미리 만들어진 Action을 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저장소에 코드 업로드하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우 파일을 작성한 후 GitHub 저장소에 코드를 업로드하면 자동으로 CI/CD가 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;git init
git add .
git commit -m &quot;GitHub Actions 워크플로우 추가&quot;
git branch -M main
git remote add origin {저장소 주소}
git push -u origin main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;푸시가 완료되면 GitHub 저장소의 Actions 탭에서 워크플로우 실행 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경 변수와 시크릿 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서는 API 키, 데이터베이스 접속 정보 등 민감한 정보를 안전하게 관리해야 합니다. GitHub Actions는 이를 위해 다양한 변수 시스템을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 환경 변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 워크플로우 실행 중 사용할 수 있는 다양한 기본 환경 변수를 제공합니다. $GITHUB_SHA는 현재 커밋의 해시값을, $GITHUB_REPOSITORY는 저장소 이름을 담고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시크릿 변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감한 정보는 저장소 설정의 Secrets 섹션에서 안전하게 관리할 수 있습니다. 워크플로우에서는 ${{ secrets.변수명 }} 형태로 사용할 수 있으며, 실행 로그에서는 값이 마스킹되어 보안이 유지됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로&amp;nbsp;CI/CD에&amp;nbsp;대해&amp;nbsp;학습하면서,&amp;nbsp;관련&amp;nbsp;내용들을&amp;nbsp;차례대로&amp;nbsp;정리해&amp;nbsp;나갈&amp;nbsp;예정입니다.&amp;nbsp;이번&amp;nbsp;글을&amp;nbsp;포함해&amp;nbsp;CI/CD&amp;nbsp;카테고리의&amp;nbsp;포스트들은&amp;nbsp;다음&amp;nbsp;강의를&amp;nbsp;수강하며&amp;nbsp;학습한&amp;nbsp;내용을&amp;nbsp;정리한&amp;nbsp;것입니다.&lt;/p&gt;
&lt;figure id=&quot;og_1751874199741&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;비전공자도 이해할 수 있는 CI/CD 입문&amp;middot;실전 강의 | JSCODE 박재성 - 인프런&quot; data-og-description=&quot;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 CI/CD 입문 강의를 만들어봤습니다!,   에라이, 못 해먹겠네!비전공자로 개발을 시작해 여러 회사에서 CTO로&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-ci-cd-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84&quot; data-og-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-ci-cd-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bJrO5j/hyZfunGSm5/Kdiq0KL1KDFW2Row2vzsu1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/BAVPK/hyZgbItOo3/MJlcc4VrjfR0eXPlowUtok/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/b4uaaf/hyZfuOLoCN/cKoVCCtfksqvJa0GO0a971/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-ci-cd-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-ci-cd-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bJrO5j/hyZfunGSm5/Kdiq0KL1KDFW2Row2vzsu1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/BAVPK/hyZgbItOo3/MJlcc4VrjfR0eXPlowUtok/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/b4uaaf/hyZfuOLoCN/cKoVCCtfksqvJa0GO0a971/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;비전공자도 이해할 수 있는 CI/CD 입문&amp;middot;실전 강의 | JSCODE 박재성 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 CI/CD 입문 강의를 만들어봤습니다!,   에라이, 못 해먹겠네!비전공자로 개발을 시작해 여러 회사에서 CTO로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Infra/CI CD</category>
      <category>CI/CD</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/32</guid>
      <comments>https://leve68.tistory.com/entry/CICD-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90#entry32comment</comments>
      <pubDate>Mon, 7 Jul 2025 16:47:59 +0900</pubDate>
    </item>
    <item>
      <title>VIEW와 INDEX로 데이터베이스 최적화</title>
      <link>https://leve68.tistory.com/entry/VIEW%EC%99%80-INDEX%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%B5%9C%EC%A0%81%ED%99%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이글에서는 복잡한 쿼리를 단순화하는 VIEW와 성능을 최적화하는 INDEX에 대해 알아보겠습니다. 이 두 기능을 잘 활용하면 데이터베이스의 효율성을 크게 향상시킬 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VIEW - 복잡한 쿼리를 위한 가상 테이블&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW는 하나 이상의 테이블을 기반으로 한 논리적인 가상 테이블입니다. 실제 데이터를 저장하지는 않지만, 마치 실제 테이블처럼 사용할 수 있어 복잡한 쿼리를 단순화하고 코드의 재사용성을 높여줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIEW의 개념과 핵심 가치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW를 이해하는 가장 좋은 방법은 'Stored Query'라고 생각하는 것입니다. 자주 사용하는 복잡한 SELECT 문을 저장해두고, 필요할 때마다 간단히 호출할 수 있는 기능입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 복잡한 조인 쿼리를 매번 작성하는 대신
CREATE VIEW employee_details AS
SELECT 
    e.empno,
    e.ename,
    e.job,
    e.sal,
    d.dname,
    d.loc
FROM emp e
JOIN dept d ON e.deptno = d.deptno;

-- 간단하게 사용
SELECT * FROM employee_details WHERE sal &amp;gt; 3000;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 복잡한 JOIN 로직을 반복해서 작성할 필요가 없어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIEW의 장점과 실무 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW가 제공하는 핵심 가치들을 살펴보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡성 감소:&lt;/b&gt;&amp;nbsp;여러 테이블을 조인하거나 복잡한 계산이 포함된 쿼리를 VIEW로 저장하면, 다른 개발자들도 쉽게 데이터에 접근할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 강화:&lt;/b&gt; 전체 테이블이 아닌 특정 컬럼이나 조건에 맞는 데이터만 노출할 수 있어 보안을 강화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 추상화:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;테이블 구조가 변경되어도 VIEW의 인터페이스는 동일하게 유지할 수 있어, 애플리케이션 코드의 수정을 최소화할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751464011810&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 급여 정보는 제외하고 기본 정보만 노출
CREATE VIEW public_employee_info AS
SELECT empno, ename, job, hiredate, deptno
FROM emp;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIEW의 유형과 특성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW는 기반 테이블의 수와 데이터 가공 여부에 따라 분류할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순 뷰 (Simple VIEW)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 테이블에서만 데이터를 조회&lt;/li&gt;
&lt;li&gt;데이터를 가공하지 않은 raw data&lt;/li&gt;
&lt;li&gt;데이터 조작(INSERT, UPDATE, DELETE) 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751464033275&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE VIEW active_employees AS
SELECT * FROM emp WHERE job != 'INACTIVE';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복합 뷰 (Complex VIEW)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 개 이상의 테이블에서 데이터를 조회&lt;/li&gt;
&lt;li&gt;집계 함수나 GROUP BY 등으로 데이터를 가공&lt;/li&gt;
&lt;li&gt;데이터 조작에 제한이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751464043992&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE VIEW dept_summary AS
SELECT 
    d.deptno,
    d.dname,
    COUNT(e.empno) as emp_count,
    AVG(e.sal) as avg_salary
FROM dept d
LEFT JOIN emp e ON d.deptno = e.deptno
GROUP BY d.deptno, d.dname;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIEW와 데이터 조작의 제한사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIEW를 통한 데이터 조작은 특정 조건을 만족할 때만 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 조작이 가능한 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가공되지 않은 raw data&lt;/li&gt;
&lt;li&gt;단일 테이블 기반&lt;/li&gt;
&lt;li&gt;DISTINCT, 집계 함수, GROUP BY 등을 사용하지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 조작이 불가능한 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DISTINCT 처리된 데이터&lt;/li&gt;
&lt;li&gt;집계 함수 (COUNT, SUM, AVG 등) 사용&lt;/li&gt;
&lt;li&gt;GROUP BY, HAVING 절 사용&lt;/li&gt;
&lt;li&gt;연산이나 함수로 가공된 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 VIEW 활용 - 피벗 테이블 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 분석 쿼리를 VIEW로 구현하는 실제 사례를 살펴보겠습니다. 부서별 직무별 급여 분석을 위한 피벗 테이블을 만들어보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STEP 1: 기본 피벗 뷰 생성&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE VIEW v_emp_dept_job_sal AS
SELECT  
    deptno,
    IF(job='CLERK', sal, NULL) AS CLERK,
    IF(job='MANAGER', sal, NULL) AS MANAGER,
    IF(job='PRESIDENT', sal, NULL) AS PRESIDENT,
    IF(job='ANALYST', sal, NULL) AS ANALYST,
    IF(job='SALESMAN', sal, NULL) AS SALESMAN
FROM emp;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 VIEW는 각 직무별로 급여 데이터를 분리하여 저장합니다. 해당 직무가 아닌 경우 NULL 값을 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STEP 2: 부서별 직무별 사원 수 분석&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT  
    deptno,
    COUNT(CLERK) AS CLERK,
    COUNT(MANAGER) AS MANAGER,
    COUNT(PRESIDENT) AS PRESIDENT,
    COUNT(ANALYST) AS ANALYST,
    COUNT(SALESMAN) AS SALESMAN
FROM v_emp_dept_job_sal
GROUP BY deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STEP 3: 부서별 직무별 평균 급여 분석&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT  
    deptno,
    ROUND(AVG(CLERK), 2) AS CLERK_AVG,
    ROUND(AVG(MANAGER), 2) AS MANAGER_AVG,
    ROUND(AVG(PRESIDENT), 2) AS PRESIDENT_AVG,
    ROUND(AVG(ANALYST), 2) AS ANALYST_AVG,
    ROUND(AVG(SALESMAN), 2) AS SALESMAN_AVG
FROM v_emp_dept_job_sal
GROUP BY deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 번 만들어둔 VIEW를 다양한 방식으로 활용하여 여러 관점의 분석이 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIEW 관리 명령어&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- VIEW 생성 (기존 VIEW가 있으면 교체)
CREATE OR REPLACE VIEW view_name AS
SELECT ...;

-- VIEW 수정
ALTER VIEW view_name AS
SELECT ...;

-- VIEW 삭제
DROP VIEW view_name;

-- WITH CHECK OPTION 사용
CREATE VIEW high_salary_emp AS
SELECT * FROM emp WHERE sal &amp;gt; 3000
WITH CHECK OPTION;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WITH CHECK OPTION을 사용하면 VIEW의 조건을 위반하는 데이터 조작을 방지할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;INDEX - 데이터 검색 성능의 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INDEX는 데이터베이스에서 데이터 검색 속도를 획기적으로 향상시키는 핵심 기술입니다. 책의 목차나 색인과 같은 역할을 하여, 대량의 데이터에서 원하는 정보를 빠르게 찾을 수 있게 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX의 개념과 작동 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INDEX는 테이블의 컬럼을 기준으로 정렬된 별도의 데이터 구조를 생성합니다. 이 구조를 통해 전체 테이블을 스캔하지 않고도 원하는 데이터를 빠르게 찾을 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 인덱스가 없는 경우의 검색
SELECT * FROM employees WHERE last_name = 'Smith';
-- 전체 테이블을 순차적으로 스캔 (Table Scan)

-- 인덱스가 있는 경우의 검색
CREATE INDEX idx_last_name ON employees(last_name);
SELECT * FROM employees WHERE last_name = 'Smith';
-- 인덱스를 통한 빠른 검색 (Index Seek)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX의 자동 생성과 수동 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 특정 제약 조건에 대해 인덱스가 자동으로 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동 생성되는 인덱스&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PRIMARY KEY: 클러스터 인덱스 생성&lt;/li&gt;
&lt;li&gt;UNIQUE: 유니크 인덱스 생성&lt;/li&gt;
&lt;li&gt;FOREIGN KEY: 일반 인덱스 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수동으로 생성하는 인덱스&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 단일 컬럼 인덱스
CREATE INDEX idx_salary ON employees(salary);

-- 복합 컬럼 인덱스
CREATE INDEX idx_dept_job ON employees(department_id, job_title);

-- 유니크 인덱스
CREATE UNIQUE INDEX idx_email ON employees(email);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX 설계 전략&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절에 자주 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;JOIN 조건에 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;ORDER BY 절에 사용되는 컬럼&lt;/li&gt;
&lt;li&gt;GROUP BY 절에 사용되는 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택도(Selectivity) 고려&lt;/b&gt; 선택도가 높은 컬럼(중복값이 적은 컬럼)일수록 인덱스 효과가 큽니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 선택도가 높은 컬럼 (좋은 인덱스 후보)
CREATE INDEX idx_employee_id ON employees(employee_id);     -- 유일값
CREATE INDEX idx_email ON employees(email);                -- 거의 유일

-- 선택도가 낮은 컬럼 (인덱스 효과 제한적)
CREATE INDEX idx_gender ON employees(gender);              -- 'M', 'F' 두 값만&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복합 인덱스의 컬럼 순서&lt;/b&gt; 복합 인덱스에서는 선택도가 높은 컬럼을 앞에 배치하는 것이 효과적입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 효과적인 복합 인덱스
CREATE INDEX idx_dept_job_salary ON employees(department_id, job_title, salary);

-- 다음 쿼리들이 모두 이 인덱스를 활용 가능
SELECT * FROM employees WHERE department_id = 10;
SELECT * FROM employees WHERE department_id = 10 AND job_title = 'Manager';
SELECT * FROM employees WHERE department_id = 10 AND job_title = 'Manager' AND salary &amp;gt; 50000;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX의 장단점과 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT 성능 향상&lt;/li&gt;
&lt;li&gt;ORDER BY, GROUP BY 처리 속도 향상&lt;/li&gt;
&lt;li&gt;유니크 제약 조건 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가 저장 공간 필요&lt;/li&gt;
&lt;li&gt;INSERT, UPDATE, DELETE 성능 저하&lt;/li&gt;
&lt;li&gt;인덱스 유지보수 오버헤드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 과도한 인덱스는 오히려 성능 저하
-- 테이블에 10개 이상의 인덱스는 DML 성능에 심각한 영향
-- 사용되지 않는 인덱스는 정기적으로 제거
-- 인덱스 사용 현황 확인
SHOW INDEX FROM employees;
-- 사용되지 않는 인덱스 식별을 위한 쿼리
SELECT
    s.table_name,
    s.index_name,
    s.cardinality,
    s.sub_part,
    s.nullable
FROM information_schema.statistics s
WHERE s.table_schema = 'your_database'
ORDER BY s.table_name, s.seq_in_index;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; 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; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 자주 실행되는 쿼리
SELECT employee_id, last_name, department_id 
FROM employees 
WHERE department_id = 10;

-- 커버링 인덱스 생성
CREATE INDEX idx_covering ON employees(department_id, employee_id, last_name);&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;VIEW와 INDEX는 데이터베이스 최적화의 핵심 도구입니다. VIEW를 통해 복잡한 비즈니스 로직을 캡슐화하고 코드의 재사용성을 높일 수 있으며, INDEX를 통해 대용량 데이터에서도 빠른 검색 성능을 보장할 수 있습니다. 정리하면 다음과 같습니다.&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;VIEW 활용의 핵심&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 JOIN과 집계 쿼리의 단순화&lt;/li&gt;
&lt;li&gt;데이터 보안과 접근 제어&lt;/li&gt;
&lt;li&gt;피벗 테이블과 같은 고급 데이터 분석 구조 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX 설계의 핵심&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 패턴 분석을 통한 적절한 인덱스 선정&lt;/li&gt;
&lt;li&gt;선택도와 복합 인덱스 순서 고려&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/31</guid>
      <comments>https://leve68.tistory.com/entry/VIEW%EC%99%80-INDEX%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%B5%9C%EC%A0%81%ED%99%94#entry31comment</comments>
      <pubDate>Wed, 2 Jul 2025 22:51:15 +0900</pubDate>
    </item>
    <item>
      <title>DDL과 데이터 타입</title>
      <link>https://leve68.tistory.com/entry/DDL%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 효과적으로 활용하려면 먼저 견고한 구조를 설계해야 합니다. 이전 글에서 DML과 트랜잭션을 다뤘다면, 이번에는 데이터베이스의 골격을 만드는 DDL과 적절한 데이터 타입 선택에 대해 체계적으로 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DDL(Data Definition Language)의 역할과 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL은 데이터베이스 객체들을 생성, 삭제, 변경하기 위한 명령어입니다. 테이블, 인덱스, 뷰 등과 같은 데이터베이스 객체들의 구조를 정의하는 핵심 도구로, 마치 건물의 설계도를 그리는 것과 같은 역할을 합니다.&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE DATABASE ssafydb
DEFAULT CHARACTER SET utf8mb4 
COLLATE utf8mb4_0900_ai_ci;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 utf8mb4는 MySQL에서 완전한 UTF-8 지원을 위한 문자셋이고, utf8mb4_0900_ai_ci는 대소문자를 구분하지 않는 정렬 규칙입니다. 이러한 설정은 나중에 한글 데이터나 이모지를 저장할 때 문제를 방지해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 설정을 확인하거나 변경하는 것도 중요합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 현재 설정 확인
SHOW VARIABLES LIKE 'character_set_database';
SHOW VARIABLES LIKE 'collation_database';

-- 설정 변경
ALTER DATABASE ssafydb
DEFAULT CHARACTER SET utf8mb4 
COLLATE utf8mb4_0900_ai_ci;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;h3 data-ke-size=&quot;size23&quot;&gt;숫자 데이터 타입의 전략적 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 다양한 크기의 숫자 타입을 제공하므로, 저장할 데이터의 범위에 맞는 최적의 타입을 선택해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;데이터 타입&lt;/td&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;표현 범위&lt;/td&gt;
&lt;td&gt;활용 사례&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TINYINT&lt;/td&gt;
&lt;td&gt;1바이트&lt;/td&gt;
&lt;td&gt;-128 ~ 127&lt;/td&gt;
&lt;td&gt;나이, 상태 코드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMALLINT&lt;/td&gt;
&lt;td&gt;2바이트&lt;/td&gt;
&lt;td&gt;-32,768 ~ 32,767&lt;/td&gt;
&lt;td&gt;년도, 작은 카운터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT/INTEGER&lt;/td&gt;
&lt;td&gt;4바이트&lt;/td&gt;
&lt;td&gt;-2,147,483,648 ~ 2,147,483,647&lt;/td&gt;
&lt;td&gt;일반적인 ID, 수량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIGINT&lt;/td&gt;
&lt;td&gt;8바이트&lt;/td&gt;
&lt;td&gt;-9E+18 ~ 9E+18&lt;/td&gt;
&lt;td&gt;대용량 ID, 타임스탬프&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 소수점 처리가 중요한 금융 데이터
salary DECIMAL(10,2)  -- 99,999,999.99까지 정확하게 저장

-- 과학 계산용 근사치
temperature FLOAT    -- 빠른 연산, 근사치 허용
coordinates DOUBLE   -- 높은 정밀도 필요&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 용도의 BIT 타입도 유용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 플래그나 상태를 비트로 관리
user_permissions BIT(8)  -- 8개의 권한을 비트로 관리
is_active BIT(1)         -- 단순 true/false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 고정 길이 - 항상 동일한 길이일 때
country_code CHAR(2)     -- 'KR', 'US' 등
phone_number CHAR(11)    -- '01012345678'

-- 가변 길이 - 길이가 다양할 때
user_name VARCHAR(50)    -- 실제 사용하는 길이만큼만 저장
email VARCHAR(255)       -- 이메일 주소&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 텍스트나 바이너리 데이터를 위한 타입들도 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 텍스트 데이터
article_content TEXT           -- 64KB까지의 글 내용
book_content LONGTEXT          -- 4GB까지의 대용량 텍스트

-- 바이너리 데이터 (실제로는 파일 경로 저장을 권장)
profile_image BLOB             -- 이미지 파일
video_file LONGBLOB            -- 동영상 파일&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한된 선택지를 다루는 ENUM과 SET 타입은 데이터 무결성을 보장하는 데 유용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 단일 선택
status ENUM('active', 'inactive', 'pending')

-- 다중 선택 (취미, 관심사 등)
interests SET('sports', 'music', 'reading', 'travel')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENUM은&amp;nbsp;정의된&amp;nbsp;값&amp;nbsp;중&amp;nbsp;하나만&amp;nbsp;선택,&amp;nbsp;SET은&amp;nbsp;정의된&amp;nbsp;값&amp;nbsp;중&amp;nbsp;여러&amp;nbsp;개를&amp;nbsp;조합하여&amp;nbsp;선택&amp;nbsp;가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;데이터 타입&lt;/td&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;형식&lt;/td&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;3바이트&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD&lt;/td&gt;
&lt;td&gt;날짜만 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIME&lt;/td&gt;
&lt;td&gt;3바이트&lt;/td&gt;
&lt;td&gt;HH:MM:SS&lt;/td&gt;
&lt;td&gt;시간만 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATETIME&lt;/td&gt;
&lt;td&gt;8바이트&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD HH:MM:SS&lt;/td&gt;
&lt;td&gt;고정된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMESTAMP&lt;/td&gt;
&lt;td&gt;4바이트&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD HH:MM:SS&lt;/td&gt;
&lt;td&gt;UTC 기준, 시간대 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 생년월일은 날짜만 필요
birth_date DATE

-- 근무 시간은 시간만 필요
work_start_time TIME

-- 게시글 작성일시는 고정된 시간
created_at DATETIME DEFAULT CURRENT_TIMESTAMP

-- 로그 기록은 시간대 변환이 필요할 수 있음
logged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TIMESTAMP는 협정 세계시(UTC)를 기준으로 동작합니다. 1970년 1월 1일 자정을 0으로 설정한 유닉스 타임스탬프 개념을 사용하여, 전 세계 어디서든 동일한 기준으로 시간을 관리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테이블 생성과 설계 원칙&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE user_profiles
(
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    birth_date DATE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 이름은 명확하고 일관성 있게 정해야 합니다. 문자로 시작하고, 30자 이내로 제한하며, 의미 있는 이름을 사용하는 것이 좋습니다.&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,           -- 기본키
    user_id INT NOT NULL,                              -- NOT NULL
    product_code VARCHAR(20) UNIQUE,                   -- 유니크
    order_date DATE DEFAULT (CURRENT_DATE),            -- 기본값
    quantity INT CHECK (quantity &amp;gt; 0),                 -- 체크 제약
    status ENUM('pending', 'confirmed', 'shipped', 'delivered') DEFAULT 'pending',
    
    -- 외래키 제약조건
    FOREIGN KEY (user_id) REFERENCES users(user_id) 
        ON DELETE CASCADE ON UPDATE CASCADE
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;b&gt;NOT NULL&lt;/b&gt;: 필수 입력 필드 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UNIQUE&lt;/b&gt;: 중복 방지 (이메일, 전화번호 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PRIMARY KEY&lt;/b&gt;: 테이블의 주 식별자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FOREIGN KEY&lt;/b&gt;: 테이블 간 관계 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CHECK&lt;/b&gt;: 값의 유효성 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DEFAULT&lt;/b&gt;: 기본값 설정으로 입력 편의성 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다양한 테이블 생성 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 테이블을 활용한 테이블 생성 방법들도 실무에서 자주 사용됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 구조만 복사
CREATE TABLE users_backup LIKE users;

-- 구조와 데이터 모두 복사
CREATE TABLE active_users AS
SELECT * FROM users WHERE status = 'active';

-- 특정 컬럼만 선택해서 새 테이블 생성
CREATE TABLE user_summary AS
SELECT user_id, username, email, created_at
FROM users
WHERE created_at &amp;gt;= '2024-01-01';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;h3 data-ke-size=&quot;size23&quot;&gt;ALTER TABLE로 유연한 스키마 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALTER&amp;nbsp;TABLE은&amp;nbsp;이미&amp;nbsp;생성된&amp;nbsp;테이블의&amp;nbsp;구조를&amp;nbsp;변경하는&amp;nbsp;SQL&amp;nbsp;명령어입니다.&amp;nbsp;테이블을&amp;nbsp;삭제하고&amp;nbsp;다시&amp;nbsp;만들지&amp;nbsp;않고도&amp;nbsp;컬럼을&amp;nbsp;추가/삭제/수정할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;데이터를&amp;nbsp;보존하면서&amp;nbsp;스키마를&amp;nbsp;변경할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 과정에서 테이블 구조를 변경해야 하는 경우가 빈번합니다. ALTER TABLE 명령어를 통해 안전하게 스키마를 변경할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 컬럼 추가
ALTER TABLE users 
ADD COLUMN phone VARCHAR(20) AFTER email;

-- 컬럼 속성 변경
ALTER TABLE users 
MODIFY COLUMN username VARCHAR(100) NOT NULL;

-- 컬럼 이름과 속성 동시 변경
ALTER TABLE users 
CHANGE COLUMN phone phone_number VARCHAR(15);

-- 컬럼 삭제
ALTER TABLE users 
DROP COLUMN phone_number;

-- 테이블 이름 변경
ALTER TABLE users 
RENAME TO user_accounts;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 데이터만 삭제 (구조 유지, 빠른 실행)
TRUNCATE TABLE temp_logs;

-- 테이블 완전 삭제
DROP TABLE old_backup_table;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TRUNCATE는 DELETE보다 훨씬 빠르지만, WHERE 조건을 사용할 수 없고 롤백이 불가능하므로 주의해서 사용해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDL과 데이터 타입은 데이터베이스 설계의 근본이 되는 요소들입니다. 적절한 데이터 타입 선택은 성능과 저장 공간 효율성을 좌우하고, 올바른 제약 조건 설정은 데이터의 품질을 보장합니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장할 데이터의 범위와 특성을 정확히 파악&lt;/li&gt;
&lt;li&gt;성능과 저장 공간의 균형점 고려&lt;/li&gt;
&lt;li&gt;미래 확장 가능성도 함께 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테이블 설계 시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확하고 일관성 있는 명명 규칙 적용&lt;/li&gt;
&lt;li&gt;적절한 제약 조건으로 데이터 무결성 보장&lt;/li&gt;
&lt;li&gt;인덱스 계획까지 함께 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스키마 관리 시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ALTER TABLE을 활용한 안전한 변경&lt;/li&gt;
&lt;li&gt;정기적인 테이블 정리와 최적화&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/30</guid>
      <comments>https://leve68.tistory.com/entry/DDL%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85#entry30comment</comments>
      <pubDate>Mon, 30 Jun 2025 17:33:33 +0900</pubDate>
    </item>
    <item>
      <title>DML과 트랜잭션</title>
      <link>https://leve68.tistory.com/entry/DML%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 데이터베이스의 핵심 개념 중 DML과 트랜잭션에 대해 포괄적으로 다루어보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DML&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Manipulation Language(DML)는 테이블의 데이터를 조작하기 위한 명령어입니다. INSERT, UPDATE, DELETE 세 가지 핵심 명령어로 구성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INSERT - 데이터 삽입&lt;/h3&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;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO emp (empno, ename, job, mgr, sal, deptno)
VALUES (9997, '이름', '직무', NULL, (SELECT MAX(hisal) FROM salgrade), 20);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT 명령어를 사용할 때 주의할 점들이 있습니다. INTO절의 컬럼 리스트에 명시한 컬럼에만 VALUES절에서 대응되는 컬럼 값을 입력해야 합니다. INTO절의 컬럼 리스트를 명시하지 않으면 테이블 생성 시 정의한 모든 컬럼을 순서와 동일한 순서로 입력해야 합니다.&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO emp(empno, ename, sal)
VALUES(9998, 'ㅇㅇ', '2000'),
      (9999, 'ㄹㄹ', '3000');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT 명령문의 VALUES절 대신 서브 쿼리에서 검색된 결과를 한꺼번에 입력할 수도 있습니다. 서브 쿼리의 결과 집합은 INSERT 명령문에 지정된 컬럼 개수와 데이터 타입이 일치해야 합니다.&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;NULL과 기본값 처리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NULL 입력: INTO절에서 해당 컬럼 이름 생략 또는 VALUES절에서 NULL 키워드 사용&lt;/li&gt;
&lt;li&gt;기본값 처리: INTO절에서 해당 컬럼 이름 생략 또는 VALUES절에서 DEFAULT 키워드 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UPDATE - 기존 데이터 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터를 수정할 때는 UPDATE 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;UPDATE emp
SET deptno = 30, sal = 3000
WHERE empno = 9997;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE 사용 시 중요한 주의사항이 있습니다. 수정 값은 직접 명시하거나 서브 쿼리의 결과를 이용할 수 있지만, 서브 쿼리를 이용할 경우 단일 행 서브 쿼리만 가능합니다.&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; MySQL에서는 같은 테이블로 서브쿼리를 통해 업데이트를 시도하면 실패합니다. 읽고 쓰는 순서가 명확하지 않기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 오류 발생
update emp
set deptno = (select deptno from emp where empno=7934),
    job = (select job from emp where empno=7934)
where empno=9997;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안 1: FROM절 우회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;update emp
set deptno = (select deptno from (select deptno from emp where empno=7902) e),
    job = (select job from (select job from emp where empno=7902) e)
where empno=9997;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안 2: CROSS JOIN 우회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;update emp e1
cross join (select deptno, job from emp where empno=7902) e2
set e1.deptno = e2.deptno,
    e1.job = e2.job
where e1.empno=9997;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DELETE - 데이터 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터를 삭제할 때는 DELETE 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;DELETE FROM emp
WHERE empno &amp;gt;= 8000;
&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;ON&amp;nbsp;DUPLICATE&amp;nbsp;KEY&amp;nbsp;UPDATE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 기존 데이터가 있으면 수정하고 없으면 삽입하는 편리한 기능을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO emp(empno, ename, sal)
VALUES (8003, 'micky', 3500)
ON DUPLICATE KEY UPDATE job='clerk', deptno=30;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션과 동시성 제어&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;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;All or Nothing&lt;/b&gt; 개념입니다. 모든 작업이 성공적으로 완료되면 커밋(COMMIT)하고, 하나라도 실패하면 롤백(ROLLBACK)을 수행하여 데이터의 무결성을 보장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ACID 특성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 올바르게 처리되기 위해서는 ACID 특성을 만족해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Atomicity(원자성)&lt;/b&gt;: 트랜잭션 내의 모든 연산이 성공하거나, 하나라도 실패하면 전체가 취소됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consistency(일관성)&lt;/b&gt;: 트랜잭션이 실행되기 전과 후의 데이터베이스 상태가 일관성을 유지해야 함, 데이터의 무결성(Integrity) 및 비즈니스 규칙(Business Rules)이 항상 유지되는 상태를 의미&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Isolation(고립성)&lt;/b&gt;: 하나의 트랜잭션이 완료될 때까지 다른 트랜잭션이 접근하지 못함, 하나의 특정 트랜잭션이 완료될때까지, 다른 트랜잭션이 특정 트랜잭션의 결과를 참조할 수 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Durability(지속성)&lt;/b&gt;: 트랜잭션이 성공적으로 완료되면 그 결과가 영구적으로 반영됨&lt;/li&gt;
&lt;/ul&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;READ UNCOMMITTED&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;커밋되지 않은 데이터를 읽을 수 있음&lt;/td&gt;
&lt;td&gt;Dirty Read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;READ COMMITTED&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;커밋된 데이터만 읽을 수 있음&lt;/td&gt;
&lt;td&gt;Non-Repeatable Read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;REPEATABLE READ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;트랜잭션 내에서 같은 데이터를 조회하면 항상 같은 데이터 조회 보장&lt;/td&gt;
&lt;td&gt;Phantom Read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SERIALIZABLE&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;MySQL의 경우 기본적으로 REPEATABLE READ를 지원하며, 반복 읽기를 보장합니다.&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;MySQL에서 격리 수준을 확인하고 변경하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 현재 세션의 트랜잭션 격리 수준 확인 (SQL)
SELECT @@transaction_isolation;

-- 세션의 트랜잭션 격리 수준 변경 (SQL)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;&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;MySQL의 격리 수준별 차이점을 실습을 통해 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REPEATABLE READ (MySQL 기본값)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAgqsQ/btsOVg8z2ok/IfE8eZW5a46S8dQfTkVywk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAgqsQ/btsOVg8z2ok/IfE8eZW5a46S8dQfTkVywk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAgqsQ/btsOVg8z2ok/IfE8eZW5a46S8dQfTkVywk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAgqsQ%2FbtsOVg8z2ok%2FIfE8eZW5a46S8dQfTkVywk%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;2048&quot; height=&quot;570&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 왼쪽 세션: 트랜잭션 시작 후 전체 조회
START TRANSACTION;
SELECT * FROM users;  -- 기존 데이터만 존재

-- 오른쪽 세션: 트랜잭션 없이 자동 커밋으로 데이터 추가
INSERT INTO users VALUES ('ssafy2');
COMMIT;

-- 왼쪽 세션: 동일한 조회 다시 실행
SELECT * FROM users;  -- 여전히 기존 데이터만 보임 (반복 읽기 지원)
COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배타적 잠금(X-Lock) 동작&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tSdwA/btsOWJacF8T/YoHBiWFe7JMir0Iitrxst0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tSdwA/btsOWJacF8T/YoHBiWFe7JMir0Iitrxst0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tSdwA/btsOWJacF8T/YoHBiWFe7JMir0Iitrxst0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtSdwA%2FbtsOWJacF8T%2FYoHBiWFe7JMir0Iitrxst0%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;2048&quot; height=&quot;521&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 왼쪽 세션: 특정 레코드 수정 시작
START TRANSACTION;
UPDATE accounts SET balance = 1000 WHERE id = 100;
-- 아직 COMMIT 하지 않음

-- 오른쪽 세션: 동일한 레코드 수정 시도
UPDATE accounts SET balance = 2000 WHERE id = 100;
-- 왼쪽 트랜잭션이 COMMIT 될 때까지 대기함&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FOR UPDATE를 통한 조회 잠금&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;-- 일관성 유지를 위해 조회 시에도 잠금 사용
SELECT * FROM accounts WHERE id = 100 FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;READ UNCOMMITTED 실습&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7ocS2/btsOVGyiMuo/Kg4N117lDkKX8QWAgeDkQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7ocS2/btsOVGyiMuo/Kg4N117lDkKX8QWAgeDkQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7ocS2/btsOVGyiMuo/Kg4N117lDkKX8QWAgeDkQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7ocS2%2FbtsOVGyiMuo%2FKg4N117lDkKX8QWAgeDkQ0%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;2048&quot; height=&quot;538&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 왼쪽 세션: 격리 수준 변경 후 트랜잭션 시작
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM users;  -- ssafy2 존재

-- 오른쪽 세션: 데이터 추가 후 아직 커밋하지 않음
START TRANSACTION;
INSERT INTO users VALUES ('ssafy3');
-- COMMIT 하지 않음

-- 왼쪽 세션: 다시 조회
SELECT * FROM users;  -- ssafy3까지 보임 (Dirty Read 발생)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;READ COMMITTED 실습&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDIAgs/btsOWIvB1Qc/tjdLNvOuwvgAu0qX8enSmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDIAgs/btsOWIvB1Qc/tjdLNvOuwvgAu0qX8enSmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDIAgs/btsOWIvB1Qc/tjdLNvOuwvgAu0qX8enSmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDIAgs%2FbtsOWIvB1Qc%2FtjdLNvOuwvgAu0qX8enSmK%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;2048&quot; height=&quot;555&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 왼쪽 세션: 격리 수준 변경 후 트랜잭션 시작
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM users;  -- ssafy2 존재

-- 오른쪽 세션: 데이터 추가
START TRANSACTION;
INSERT INTO users VALUES ('ssafy3');
-- 아직 COMMIT 하지 않음

-- 왼쪽 세션: 조회 시도
SELECT * FROM users;  -- ssafy3 보이지 않음 (커밋되지 않았으므로)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 범위를 최소화하여 잠금 유지 시간을 줄임&lt;/li&gt;
&lt;li&gt;적절한 격리 수준 선택하여 동시성 개선&lt;/li&gt;
&lt;li&gt;인덱스를 활용하여 성능 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCL(Transaction Control Language)&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;b&gt;START TRANSACTION&lt;/b&gt;: 명시적인 트랜잭션 시작, 현 트랜잭션 처리를 위한 암묵적 자동 커밋 해제(session에 적용되는 것이 아님)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COMMIT&lt;/b&gt;: 트랜잭션을 성공적으로 완료하고 변경 사항을 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ROLLBACK [ TO SAVEPOINT명 ]&lt;/b&gt;: 트랜잭션을 취소하고 변경 사항을 되돌림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SAVEPOINT SAVEPOINT명&lt;/b&gt;: 트랜잭션 내에서 특정 지점을 저장하여 부분 롤백을 가능하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;START TRANSACTION;
    DML 작업들...
    SAVEPOINT P1;
    더 많은 DML 작업들...
ROLLBACK TO P1;  -- 또는 ROLLBACK; 또는 COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVCC와 잠금 메커니즘&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현대 데이터베이스에서는 동시성 제어를 위해 MVCC(Multi-Version Concurrency Control)를 사용합니다. MySQL InnoDB 엔진에서는 이를 통해 읽기와 쓰기 작업이 서로 차단되지 않도록 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;비잠금 읽기와 잠금 읽기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비잠금 읽기 (Non-Locking Read)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;일반적인 SELECT 문은 다른 트랜잭션의 쓰기 작업을 차단하지 않습니다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;-- 다른 트랜잭션이 데이터를 수정해도 읽기 가능
SELECT * FROM accounts WHERE account_id = 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잠금 읽기 (Locking Read)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;특정 상황에서는 데이터 일관성을 위해 잠금이 필요합니다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;-- 공유 잠금: 다른 트랜잭션의 읽기는 허용, 쓰기는 대기
SELECT * FROM accounts WHERE account_id = 100 FOR SHARE;

-- 배타적 잠금: 다른 트랜잭션의 읽기, 쓰기 모두 대기
SELECT * FROM accounts WHERE account_id = 100 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잠금 읽기의 실제 활용 사례&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FOR SHARE 사용 사례&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;-- 주문 처리 시 재고 확인
START TRANSACTION;

-- 재고를 읽기 잠금으로 확인 (다른 주문이 동시에 확인 가능)
SELECT stock_quantity 
FROM products 
WHERE product_id = 123 
FOR SHARE;

-- 재고가 충분한 경우 주문 생성
INSERT INTO orders (product_id, quantity) VALUES (123, 2);

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FOR UPDATE 사용 사례&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;-- 계좌 이체 처리
START TRANSACTION;

-- 출금 계좌 배타적 잠금 (다른 트랜잭션의 접근 완전 차단)
SELECT balance 
FROM accounts 
WHERE account_id = 100 
FOR UPDATE;

-- 잔액 확인 후 이체 처리
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 100;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 200;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SERIALIZABLE 격리 수준&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 높은 격리 수준인 SERIALIZABLE에서는 모든 SELECT가 자동으로 FOR SHARE로 처리됩니다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;-- 트랜잭션 격리 수준 설정
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 이제 모든 SELECT가 공유 잠금으로 동작
SELECT * FROM employees WHERE department_id = 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 격리 수준에서는 Phantom Read 현상도 완전히 방지되지만, 동시성은 크게 떨어집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 데이터베이스의 핵심 개념인 DML과 트랜잭션에 대해 자세히 살펴보았습니다. INSERT, UPDATE, DELETE 명령어를 통한 데이터 조작 방법과 트랜잭션의 ACID 특성, 격리 수준 등 동시성 제어의 핵심 개념들을 다루었습니다.&lt;/p&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/29</guid>
      <comments>https://leve68.tistory.com/entry/DML%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98#entry29comment</comments>
      <pubDate>Sat, 28 Jun 2025 17:59:15 +0900</pubDate>
    </item>
    <item>
      <title>SQL 서브쿼리와 집합연산</title>
      <link>https://leve68.tistory.com/entry/SQL-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC%EC%99%80-%EC%A7%91%ED%95%A9%EC%97%B0%EC%82%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 학습하면서 기본적인 SELECT 문을 넘어서면 반드시 마주치게 되는 개념이 바로 서브쿼리입니다. 이번 글에서는 서브쿼리의 개념부터 실전 활용까지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서브쿼리의 기본 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브쿼리란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리는 하나의 쿼리가 다른 SQL 문에 포함되는 구조를 말합니다. 하나의 쿼리 결과를 다른 SQL 문에 전달하기 위해 두 개 이상의 SQL 문을 하나로 처리하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 예시로 살펴보겠습니다. &quot;SMITH 사원과 같은 부서에 근무하는 사원들을 조회하려면&quot; 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방법이라면 다음과 같이 두 단계로 나누어 처리해야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 SMITH 사원의 부서 번호를 조회&lt;/li&gt;
&lt;li&gt;그 부서 번호를 사용하여 해당 부서의 모든 사원 조회&lt;/li&gt;
&lt;/ol&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 ename, deptno
FROM emp
WHERE deptno = (
    SELECT deptno
    FROM emp
    WHERE ename = 'SMITH'
);
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;괄호로 감싸기:&lt;/b&gt; 서브쿼리는 반드시 괄호로 감싸서 메인 쿼리와 구분해야 합니다. 특히 FROM 절에서 사용할 때는 필수입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세미콜론 제외:&lt;/b&gt; 서브쿼리 끝에는 세미콜론을 붙이지 않습니다. 세미콜론은 전체 SQL 문의 끝에만 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ORDER BY 절 제한:&lt;/b&gt; 서브쿼리에서 ORDER BY 절 사용은 제한적이거나 의미가 없는 경우가 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서브쿼리의 유형별 분류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반환 결과에 따른 분류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 행 서브쿼리&lt;/b&gt;는 서브쿼리에서 하나의 행만을 조회하여 메인 쿼리에 반환하는 형태입니다. 비교 조건에서 단일 행 비교 연산자(=, &amp;gt;, &amp;gt;=, &amp;lt;, &amp;lt;=, &amp;lt;&amp;gt;)만 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 행 서브쿼리&lt;/b&gt;는 서브쿼리에서 여러 행을 검색하여 메인 쿼리에 반환하는 형태입니다. 비교 조건에서 다중 행 비교 연산자(IN, ANY, ALL, EXISTS)를 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 방식에 따른 분류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상호 연관 서브쿼리&lt;/b&gt;는 메인 쿼리와 별도로 독립적인 실행이 불가능한 서브쿼리입니다. 메인 쿼리가 먼저 실행되어 조회된 값을 서브쿼리에서 참조하는 연관 관계가 있는 서브쿼리입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립 서브쿼리&lt;/b&gt;는 메인 쿼리와 별도로 독립적으로 실행이 가능한 서브쿼리입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위치에 따른 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리는 SQL 문의 다양한 위치에서 사용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스칼라 서브쿼리&lt;/b&gt;: 단 하나의 값만을 조회하는 서브쿼리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인라인 뷰&lt;/b&gt;: FROM절에 작성하는 서브쿼리로 테이블 형태로 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 열 서브쿼리&lt;/b&gt;: 여러 열을 조회하는 서브쿼리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 행 서브쿼리&lt;/b&gt;: 여러 행을 조회하는 서브쿼리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다중 행 서브쿼리 연산자 완전 정복&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IN 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IN 연산자는 메인 쿼리의 비교 조건이 서브쿼리 결과 중에서 하나라도 만족하면 참이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT empno, ename
FROM emp
WHERE empno IN (
    SELECT DISTINCT mgr
    FROM emp
    WHERE mgr IS NOT NULL
);
&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;ANY 연산자의 다양한 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANY 연산자는 서브쿼리 결과 중 어느 하나라도 만족하면 참이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;= ANY:&lt;/b&gt; IN 연산자와 동일한 기능을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;gt; ANY:&lt;/b&gt; 서브쿼리 결과 중 가장 작은 값보다 크면 참입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;lt; ANY:&lt;/b&gt; 서브쿼리 결과 중 가장 큰 값보다 작으면 참입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT ename, sal
FROM emp
WHERE sal &amp;gt; ANY (
    SELECT sal
    FROM emp
    WHERE job = 'SALESMAN'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 쿼리는 SALESMAN의 최저 급여보다 급여가 많은 사원을 조회합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALL 연산자의 엄격한 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALL 연산자는 서브쿼리 결과 중 모든 값을 만족해야 참이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&amp;gt; ALL:&lt;/b&gt; 서브쿼리 결과 중 가장 큰 값보다 크면 참입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;lt; ALL:&lt;/b&gt; 서브쿼리 결과 중 가장 작은 값보다 작으면 참입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT ename, sal
FROM emp
WHERE sal &amp;gt; ALL (
    SELECT sal
    FROM emp
    WHERE job = 'SALESMAN'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 SALESMAN의 최대 급여보다도 급여가 많은 사원을 조회합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EXISTS 연산자와 Semi Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXISTS 연산자는 서브쿼리의 실행 결과가 하나라도 존재하면 메인 쿼리 조건절이 참이 됩니다. 조건에 맞는 행이 있는지의 판단 결과만 이용하고, SELECT절에서 따로 검증데이터를 가져오지 않는 형태로 대부분 작성됩니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT m.empno, m.ename
FROM emp m
WHERE EXISTS (
    SELECT 1
    FROM emp e
    WHERE m.empno = e.mgr
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 관리자인 사원들을 조회합니다. EXISTS는 단지 조건을 만족하는 행의 존재 여부만 확인하므로 SELECT 절에는 보통 상수값 1을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXISTS 연산자는 &lt;b&gt;Semi Join&lt;/b&gt;이라고도 불리며, 만족하는 행이 존재하면 바로 참을 반환하는 특징이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서브쿼리 실전 활용 패턴&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;n1ql&quot;&gt;&lt;code&gt;SELECT 
    ename, sal, deptno,
    (SELECT ROUND(AVG(sal), 2) FROM emp d WHERE d.deptno = e.deptno) 부서평균급여,
    (SELECT ROUND(AVG(sal), 2) FROM emp) 사원평균급여
FROM emp e
ORDER BY deptno;
&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;인라인 뷰는 FROM절에 위치하는 서브쿼리로, 서브쿼리의 실행 결과 집합을 마치 테이블처럼 사용하기 위한 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT d.deptno, d.max_sal, e.ename
FROM (
    SELECT deptno, MAX(sal) max_sal
    FROM emp
    GROUP BY deptno
) d
JOIN emp e ON e.deptno = d.deptno AND e.sal = d.max_sal;
&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;routeros&quot;&gt;&lt;code&gt;SELECT ename, sal, deptno
FROM emp
WHERE (IFNULL(deptno, ' '), sal) IN (
    SELECT IFNULL(deptno, ' '), MAX(sal)
    FROM emp
    GROUP BY deptno
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 각 부서별로 최대 급여를 받는 사원들을 조회합니다. 부서가 NULL인 경우도 고려하여 IFNULL 함수를 사용했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;집합 연산으로 데이터 결합하기&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;h3 data-ke-size=&quot;size23&quot;&gt;UNION과 UNION ALL의 차이&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UNION ALL&lt;/b&gt;은 두 결과 집합을 합한 결과를 반환하며, 중복 행도 포함합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UNION&lt;/b&gt;은 두 결과 집합을 합한 결과에서 중복 행을 제거합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 주문을 했거나 이벤트를 신청한 고객번호와 주문/이벤트 타입 조회
SELECT member_id, 'Order' AS activity_type
FROM orders
UNION ALL
SELECT member_id, 'Event Registration' AS activity_type
FROM event_registrations;

-- 주문을 했거나 이벤트를 신청한 고객번호 중복 없이 조회
SELECT member_id
FROM orders
UNION
SELECT member_id
FROM event_registrations;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INTERSECT와 EXCEPT 연산&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;INTERSECT&lt;/b&gt;는 두 결과 집합의 공통 행을 추출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EXCEPT&lt;/b&gt;는 첫 번째 집합에서 두 번째 집합에 있는 행을 제외한 결과를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 주문도 하고 이벤트도 신청한 고객번호 조회
SELECT member_id
FROM orders
INTERSECT
SELECT member_id
FROM event_registrations;

-- 주문은 했으나 이벤트는 한번도 신청하지 않은 고객번호 조회
SELECT member_id
FROM orders
EXCEPT
SELECT member_id
FROM event_registrations;
&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;서브쿼리와 집합 연산은 SQL에서 가장 강력한 기능 중 하나입니다. 단일 행 서브쿼리부터 복잡한 다중 행 서브쿼리, 그리고 다양한 집합 연산까지 공부하면 거의 모든 데이터 조회 요구사항을 해결할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/28</guid>
      <comments>https://leve68.tistory.com/entry/SQL-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC%EC%99%80-%EC%A7%91%ED%95%A9%EC%97%B0%EC%82%B0#entry28comment</comments>
      <pubDate>Thu, 26 Jun 2025 23:32:26 +0900</pubDate>
    </item>
    <item>
      <title>CCW를 활용한 선분 교차 판정 알고리즘</title>
      <link>https://leve68.tistory.com/entry/CCW%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%A0%EB%B6%84-%EA%B5%90%EC%B0%A8-%ED%8C%90%EC%A0%95-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;두 선분이 서로 만나는지를 어떻게 컴퓨터가 판단할 수 있을까요? 백준의 &lt;a title=&quot;선분 교차 2&quot; href=&quot;http://boj.kr/17387&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;선분 교차 2&lt;/a&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;두 선분이 교차한다는 것은 무엇을 의미할까요? 직관적으로 생각해보면, 두 선분이 서로를 &quot;가로막고&quot; 있어야 합니다. 이를 좀 더 수학적으로 표현하면, 각 선분의 양 끝점이 다른 선분을 기준으로 서로 반대편에 위치해야 한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW 알고리즘은 바로 이 &quot;서로 반대편에 있는가?&quot;를 판단하는 도구입니다. 세 점의 방향성을 통해 점들의 위치 관계를 파악할 수 있기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CCW 알고리즘의 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW 알고리즘은 세 점의 위치 관계를 통해 방향성을 판단하는 기법입니다. 세 점 A, B, C가 주어졌을 때, 점 C가 벡터 AB를 기준으로 반시계방향에 있는지 시계방향에 있는지를 판단할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CCW 함수의 수학적 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 점 A(x₁, y₁), B(x₂, y₂), C(x₃, y₃)에 대한 CCW 값은 다음과 같이 계산됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;CCW(A, B, C) = (x₂ - x₁) &amp;times; (y₃ - y₁) - (y₂ - y₁) &amp;times; (x₃ - x₁)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;nbsp;&lt;/p&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;1026&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRXjmH/btsORFr8KOJ/1zDT1qslpBLXNKtoGOHP51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRXjmH/btsORFr8KOJ/1zDT1qslpBLXNKtoGOHP51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRXjmH/btsORFr8KOJ/1zDT1qslpBLXNKtoGOHP51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRXjmH%2FbtsORFr8KOJ%2F1zDT1qslpBLXNKtoGOHP51%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;400&quot; height=&quot;376&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;1024&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wCblb/btsORVn4G1Q/cqkFf5OHfKSy0F1KfRHv4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wCblb/btsORVn4G1Q/cqkFf5OHfKSy0F1KfRHv4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wCblb/btsORVn4G1Q/cqkFf5OHfKSy0F1KfRHv4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwCblb%2FbtsORVn4G1Q%2FcqkFf5OHfKSy0F1KfRHv4k%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;400&quot; height=&quot;371&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;0&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;1020&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QSrpQ/btsOQJ3urjO/IEnxat7atoohFvv0xDpct1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QSrpQ/btsOQJ3urjO/IEnxat7atoohFvv0xDpct1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QSrpQ/btsOQJ3urjO/IEnxat7atoohFvv0xDpct1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQSrpQ%2FbtsOQJ3urjO%2FIEnxat7atoohFvv0xDpct1%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;400&quot; height=&quot;370&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caK2mH/btsOQTxX3GX/T3Q39uBZjvNPfWtKTJDyHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caK2mH/btsOQTxX3GX/T3Q39uBZjvNPfWtKTJDyHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caK2mH/btsOQTxX3GX/T3Q39uBZjvNPfWtKTJDyHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaK2mH%2FbtsOQTxX3GX%2FT3Q39uBZjvNPfWtKTJDyHk%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;400&quot; height=&quot;388&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CCW를 활용한 교차 판정 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선분 AB와 선분 CD가 교차하는지 확인하려면 두 가지 조건을 검사해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 조건: 선분 AB를 기준으로 한 검사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점 C와 D가 선분 AB를 기준으로 서로 반대편에 있는지 확인합니다. 이는 CCW(A, B, C)와 CCW(A, B, D)의 부호를 비교하여 판단할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 두 CCW 값의 부호가 다르다면, 점 C와 D는 직선 AB를 기준으로 서로 다른 편에 위치한다는 의미입니다. 이는 선분 CD가 선분 AB를 &quot;가로지르고&quot; 있다는 것을 나타냅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 조건: 선분 CD를 기준으로 한 검사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 점 A와 B가 선분 CD를 기준으로 서로 반대편에 있는지 확인합니다. CCW(C, D, A)와 CCW(C, D, B)의 부호를 비교하여 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건이 만족되면 선분 AB가 선분 CD를 &quot;가로지르고&quot; 있다는 의미입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교차 판정의 결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 조건이 모두 만족되면, 즉 서로가 서로를 가로지르고 있다면 두 선분은 반드시 교차합니다. 이것이 CCW를 이용한 선분 교차 판정의 핵심 원리입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 계산 과정 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 예시를 통해 이 과정을 살펴보겠습니다. 선분 (0,0)-(4,4)와 선분 (0,4)-(4,0)이 교차하는지 확인해보겠습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 선분: A(0,0), B(4,4)&lt;/li&gt;
&lt;li&gt;두 번째 선분: C(0,4), D(4,0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 선분 AB와 선분 CD가 교차하는지 확인해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 검사: 선분 AB를 기준으로 한 방향성 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점 C와 D가 선분 AB를 기준으로 서로 반대편에 있는지 확인해야 합니다.&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;CCW(A, B, C) 계산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW((0,0), (4,4), (0,4)) = (4-0) &amp;times; (4-0) - (4-0) &amp;times; (4-0) = 4 &amp;times; 4 - 4 &amp;times; 0 = 16 - 0 = 16 (양수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 점 C가 벡터 AB를 기준으로 반시계방향에 위치함을 의미합니다.&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;CCW(A, B, D) 계산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW((0,0), (4,4), (4,0)) = (4-0) &amp;times; (0-0) - (4-0) &amp;times; (4-0) = 4 &amp;times; 0 - 4 &amp;times; 4 = 0 - 16 = -16 (음수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 점 D가 벡터 AB를 기준으로 시계방향에 위치함을 의미합니다.&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;CCW(A, B, C) = 16 &amp;gt; 0이고 CCW(A, B, D) = -16 &amp;lt; 0이므로, 두 값의 부호가 다릅니다. 따라서 점 C와 D는 선분 AB를 기준으로 서로 반대편에 위치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 검사: 선분 CD를 기준으로 한 방향성 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 점 A와 B가 선분 CD를 기준으로 서로 반대편에 있는지 확인합니다.&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;CCW(C, D, A) 계산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW((0,4), (4,0), (0,0)) = (4-0) &amp;times; (0-4) - (0-4) &amp;times; (0-0) = 4 &amp;times; (-4) - (-4) &amp;times; 0 = -16 - 0 = -16 (음수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 점 A가 벡터 CD를 기준으로 시계방향에 위치함을 의미합니다.&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;CCW(C, D, B) 계산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW((0,4), (4,0), (4,4)) = (4-0) &amp;times; (4-4) - (0-4) &amp;times; (4-0) = 4 &amp;times; 0 - (-4) &amp;times; 4 = 0 - (-16) = 16 (양수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 점 B가 벡터 CD를 기준으로 반시계방향에 위치함을 의미합니다.&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;CCW(C, D, A) = -16 &amp;lt; 0이고 CCW(C, D, B) = 16 &amp;gt; 0이므로, 두 값의 부호가 다릅니다. 따라서 점 A와 B는 선분 CD를 기준으로 서로 반대편에 위치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 판정 과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfOs8F/btsOQ7CN8DN/E5m9udUN1BuMsCS7TOHT80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfOs8F/btsOQ7CN8DN/E5m9udUN1BuMsCS7TOHT80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfOs8F/btsOQ7CN8DN/E5m9udUN1BuMsCS7TOHT80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfOs8F%2FbtsOQ7CN8DN%2FE5m9udUN1BuMsCS7TOHT80%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;400&quot; height=&quot;402&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 조건을 모두 검토한 결과, 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;점 C와 D가 선분 AB를 기준으로 서로 반대편에 위치 ✓&lt;/li&gt;
&lt;li&gt;점 A와 B가 선분 CD를 기준으로 서로 반대편에 위치 ✓&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 조건이 모두 만족되므로 선분 AB와 선분 CD는 교차한다고 판단할 수 있습니다. 실제로 이 두 선분은 (2,2) 지점에서 만납니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특수한 경우들과 예외 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현에서는 CCW 값이 0이 나오는 특수한 상황을 추가로 고려해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일직선상 배치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCW 값이 0이 나오는 경우는 세 점이 일직선상에 위치한다는 의미입니다. 이때는 일반적인 교차 판정 공식 CCW1 &amp;times; CCW2 &amp;lt; 0으로는 판별할 수 없으므로 별도의 처리가 필요합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GWFxR/btsORlU90oX/8qkgyVdRtwk7BNZPiIEYek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GWFxR/btsORlU90oX/8qkgyVdRtwk7BNZPiIEYek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GWFxR/btsORlU90oX/8qkgyVdRtwk7BNZPiIEYek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGWFxR%2FbtsORlU90oX%2F8qkgyVdRtwk7BNZPiIEYek%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;400&quot; height=&quot;403&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 선분의 끝점이 다른 선분 위에 정확히 위치하는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p3tJd/btsOQxotO75/9eJuA2zd6gYkOSz69jj4J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p3tJd/btsOQxotO75/9eJuA2zd6gYkOSz69jj4J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p3tJd/btsOQxotO75/9eJuA2zd6gYkOSz69jj4J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp3tJd%2FbtsOQxotO75%2F9eJuA2zd6gYkOSz69jj4J0%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;400&quot; height=&quot;350&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연장선상에 있지만 실제로는 만나지 않는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMFOq1/btsORkvduZX/PnygBtvW1kgKMiXaDEZKh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMFOq1/btsORkvduZX/PnygBtvW1kgKMiXaDEZKh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMFOq1/btsORkvduZX/PnygBtvW1kgKMiXaDEZKh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMFOq1%2FbtsORkvduZX%2FPnygBtvW1kgKMiXaDEZKh1%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;400&quot; height=&quot;357&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;CCW를 활용한 선분 교차 판정은 계산 기하학의 기초이면서 &quot;서로 반대편에 있는가?&quot;라는 직관적인 아이디어를 수학적으로 정확하게 구현한 알고리즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 선분 교차 판정 문제들을 풀 수 있는 알고리즘 공부가 되었습니다.&lt;/p&gt;</description>
      <category>CS/Algorithm</category>
      <category>Algorithm</category>
      <category>CCW</category>
      <category>선분 교차 판정</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/27</guid>
      <comments>https://leve68.tistory.com/entry/CCW%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%A0%EB%B6%84-%EA%B5%90%EC%B0%A8-%ED%8C%90%EC%A0%95-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98#entry27comment</comments>
      <pubDate>Wed, 25 Jun 2025 17:01:41 +0900</pubDate>
    </item>
    <item>
      <title>SQL 집계 함수와 JOIN</title>
      <link>https://leve68.tistory.com/entry/SQL-%EC%A7%91%EA%B3%84-%ED%95%A8%EC%88%98%EC%99%80-JOIN</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 학습하면서 단순한 SELECT 문을 넘어 본격적으로 데이터를 분석하고 활용하려면 반드시 익혀야 할 개념들이 있습니다. 바로 집계 함수와 JOIN입니다. 이번 글에서는 SQL의 집계 함수와 JOIN에 대해 정리하면서, 실제로 어떻게 활용할 수 있는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;집계 함수가 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 함수들은 하나의 입력에 대해 하나의 출력을 만드는 단일 행 함수지만, 실제 업무에서는 여러 데이터를 종합해서 하나의 결과를 얻어야 하는 상황이 훨씬 많습니다. &quot;이번 달 총 매출이 얼마인지&quot;, &quot;부서별 평균 급여는 얼마인지&quot; 같은 질문에 답하려면 집계 함수가 필요합니다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;COUNT 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT 함수는 가장 기본적이면서도 주의할 점이 있는 함수입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전체 레코드 수 (NULL 포함)
SELECT COUNT(*) FROM emp;

-- 특정 컬럼의 NULL이 아닌 값의 개수
SELECT COUNT(comm) FROM emp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT(*)는 전체 레코드 수를 반환하지만, COUNT(컬럼명)은 해당 컬럼의 NULL이 아닌 값만 셉니다. 이 차이를 모르면 예상과 다른 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수치 계산 함수들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUM, AVG, MAX, MIN 함수들은 모두 NULL 값을 집계하지 않는다는 공통점이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 부서별 급여 통계
SELECT 
    SUM(sal) as 총급여,
    AVG(sal) as 평균급여,
    MAX(sal) as 최고급여,
    MIN(sal) as 최저급여
FROM emp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL값을 집계하지 않음에 주의해야 합니다. COUNT(*) 함수만 예외적으로 전체 레코드 수를 반환합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY로 데이터 그룹화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집계 함수가 진정한 위력을 발휘하는 순간은 GROUP BY절과 만날 때입니다. GROUP BY는 결과 집합의 하위 데이터 그룹을 만들어 각 그룹별로 집계 함수를 적용할 수 있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 부서별 사원 수와 평균 급여
SELECT deptno, COUNT(*), AVG(sal)
FROM emp
GROUP BY deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY절에 기술한 컬럼이나 표현식을 기준으로 결과 집합 전체 행을 그룹별로 나누게 됩니다. 기준 1로 그룹을 나누고 다음 기준을 적용하여 기준 1로 형성된 그룹별로 다시 서브 그룹을 나눕니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUP BY 사용 시 핵심 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY를 사용할 때 반드시 기억해야 할 규칙이 있습니다. &lt;b&gt;SELECT절의 모든 요소는 GROUP BY절의 표현식, 집계 함수를 포함하는 표현식 또는 상수만 가능합니다&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 잘못된 예시 - ename은 GROUP BY에 없음
SELECT deptno, ename, COUNT(*)  -- 오류!
FROM emp
GROUP BY deptno;

-- 올바른 예시
SELECT deptno, COUNT(*)
FROM emp
GROUP BY deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 중요한 규칙은 &lt;b&gt;WHERE절에는 집계 함수를 사용할 수 없다&lt;/b&gt;는 것입니다. Grouping 이전에 WHERE절을 사용하여 그룹 대상 집합을 먼저 선택해야 하기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HAVING절로 그룹 조건 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY절에 의해 생성된 그룹을 대상으로 조건을 적용하려면 HAVING절을 사용합니다. 이는 집계 관련 조건절로 이해하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 사원이 3명 이상인 부서만 조회
SELECT deptno, COUNT(*), AVG(sal)
FROM emp
GROUP BY deptno
HAVING COUNT(*) &amp;gt;= 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HAVING절은 GROUP BY절에 의해 생성된 그룹을 대상으로 조건을 적용하여 그룹을 필터링하기 위해 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WITH ROLLUP으로 소계와 총계 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 항목 종계나 각 그룹별 소계가 필요한 경우 WITH ROLLUP을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT deptno, job, SUM(sal)
FROM emp
GROUP BY deptno, job WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 부서별 소계와 전체 총계를 동시에 구할 수 있습니다. ROLLUP에 의해 생성된 NULL값을 구분하기 위해 GROUPING 함수를 활용할 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUPING 함수의 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회되는 컬럼의 NULL값이 실제 NULL값인지 ROLLUP에 의해 생성된 NULL값인지 판단하기 위해 GROUPING 함수를 사용합니다. 실제 NULL값 또는 NULL값이 아닌 값은 0 리턴, ROLLUP에 의한 NULL값은 1 리턴됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750743479737&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
    deptno,
    CASE GROUPING(deptno)
        WHEN 1 THEN '총계'
        ELSE CAST(deptno AS CHAR)
    END AS 부서구분,
    SUM(sal) AS 급여합계
FROM emp
GROUP BY deptno WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서 GROUPING(deptno)가 1을 반환하면 ROLLUP에 의한 총계 행이므로 '총계'로 표시하고, 0을 반환하면 실제 부서 번호이므로 그대로 표시합니다. 이렇게 하면 상사가 없어서 NULL인 경우와 ROLLUP 처리에 의한 NULL을 명확히 구분할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JOIN의 개념과 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN은 하나의 SQL 명령문으로 여러 테이블에 저장된 데이터를 한번에 조회할 수 있는 기능입니다. 두 개 이상의 테이블을 '결합'한다는 의미로, 주로 Primary Key와 Foreign Key의 관계를 가진 컬럼을 소유하고 있는 테이블들 통한 검색 시 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스에서는 데이터의 중복을 최소화하기 위해 정규화를 수행하는데, 이로 인해 데이터가 여러 테이블에 분산 저장됩니다. JOIN을 통해 이러한 분산된 데이터를 의미 있는 결과로 조합할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N개의 테이블 조인 시 N-1개 이상의 조인 조건이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JOIN의 다양한 유형&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건에 따른 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EQUI JOIN&lt;/b&gt;은 조인 조건의 컬럼을 '='(equal) 비교를 통해 같은 값을 가지는 행을 연결하여 결과를 생성하는 조인 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NON-EQUI JOIN&lt;/b&gt;은 '&amp;lt;', 'BETWEEN a AND b'와 같이 '=' 조건이 아닌 연산자를 사용하여 조인하는 방법입니다.&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;b&gt;INNER JOIN&lt;/b&gt;은 조인 조건에 부합하는 행에 대해서만 연결하여 결과를 생성하는 조인 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OUTER JOIN&lt;/b&gt;은 조인 조건에 부합하지 않는 기준 테이블의 행들도 모두 포함하여 결과를 생성하는 조인 방법입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EQUI JOIN 문법 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EQUI JOIN은 가장 많이 사용하는 조인 조건의 형태로, 여러 가지 문법으로 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- USING 문법
SELECT ename, dname
FROM emp
JOIN dept USING(deptno);

-- ON 문법
SELECT ename, dname
FROM emp
JOIN dept ON(emp.deptno = dept.deptno);

-- WHERE 문법
SELECT ename, dname
FROM emp, dept
WHERE emp.deptno = dept.deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;USING절을 사용하면 테이블명, 테이블 별칭 사용 불가하므로 주의해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;INNER JOIN과 OUTER JOIN 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INNER JOIN의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNER JOIN은 조인 조건에 부합하는 행에 대해서만 연결하는 조인 방식입니다. 양쪽 테이블에 모두 존재하는 데이터만 조회됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 부서 배치를 받지 못한 사원은 조인 결과에서 누락
SELECT ename, dname
FROM emp
JOIN dept USING(deptno);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OUTER JOIN으로 누락 데이터 포함하기&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;b&gt;LEFT OUTER JOIN&lt;/b&gt;: 기준 테이블을 먼저 기술&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RIGHT OUTER JOIN&lt;/b&gt;: 기준 테이블을 나중에 기술&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FULL OUTER JOIN&lt;/b&gt;: 양쪽 테이블 모두 기준 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 급여 범위를 벗어나는 사원 정보를 포함하여 조회
SELECT e.ename, e.sal, s.grade
FROM emp e
LEFT JOIN salgrade s ON(e.sal BETWEEN s.losal AND s.hisal);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NATURAL JOIN과 CROSS JOIN&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NATURAL JOIN&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 ename, dname
FROM emp
NATURAL JOIN dept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 두 테이블에서 의미가 서로 다른 컬럼의 이름이 같을 경우 의도하지 않은 조인 결과가 생성되므로 주의가 필요합니다. 공통 컬럼이 없을 경우 CROSS JOIN과 결과가 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CROSS JOIN&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 ename, dname
FROM emp
CROSS JOIN dept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블의 레코드가 많을 경우 너무 많은 결과 생성에 유의해야 합니다. 테이블1의 행 개수 * 테이블2의 행 개수의 결과만큼 생성됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SELF JOIN으로 계층 관계 표현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELF JOIN은 주로 계층형 테이블에서 나타나는 조인의 형태로, 하나의 테이블을 마치 두 개의 테이블처럼 조인하는 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 모든 사원과 자신의 상사 이름 조회
SELECT e.ename AS 사원이름, m.ename AS 상사이름
FROM emp e
JOIN emp m ON(e.mgr = m.empno);&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;집계 함수와 JOIN은 SQL에서 가장 강력한 기능 중 하나입니다. 집계 함수를 통해 대량의 데이터를 의미 있는 정보로 요약할 수 있고, JOIN을 통해 정규화된 테이블들을 연결하여 완전한 정보를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 GROUP BY와 HAVING절을 활용한 데이터 그룹화, 다양한 JOIN 기법을 통한 테이블 연결은 잘 알아두어야 할 것 같습니다.&lt;/p&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/26</guid>
      <comments>https://leve68.tistory.com/entry/SQL-%EC%A7%91%EA%B3%84-%ED%95%A8%EC%88%98%EC%99%80-JOIN#entry26comment</comments>
      <pubDate>Tue, 24 Jun 2025 14:47:16 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2에서 Docker를 활용한 배포</title>
      <link>https://leve68.tistory.com/entry/AWS-EC2%EC%97%90%EC%84%9C-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%B0%ED%8F%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS EC2와 Docker를 조합하여 Spring Boot 애플리케이션을 효율적으로 배포하는 방법을 단계별로 알아보겠습니다. 특히 AWS ECR(Elastic Container Registry)을 활용한 이미지 관리부터 Docker Compose를 통한 멀티 컨테이너 환경 구축까지 다룰 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EC2 환경 준비하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker와 Docker Compose 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Ubuntu EC2 인스턴스에 Docker 환경을 구축해야 합니다. 다음 명령어로 한 번에 설치할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; \
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common &amp;amp;&amp;amp; \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - &amp;amp;&amp;amp; \
sudo apt-key fingerprint 0EBFCD88 &amp;amp;&amp;amp; \
sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot; &amp;amp;&amp;amp; \
sudo apt-get update &amp;amp;&amp;amp; \
sudo apt-get install -y docker-ce &amp;amp;&amp;amp; \
sudo usermod -aG docker ubuntu &amp;amp;&amp;amp; \
newgrp docker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 Docker Compose도 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo curl -L &quot;https://github.com/docker/compose/releases/download/2.27.1/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose &amp;amp;&amp;amp; \
sudo chmod +x /usr/local/bin/docker-compose &amp;amp;&amp;amp; \
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose&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;docker -v
docker compose version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EC2에 AWS CLI 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 서비스와의 연동을 위해 AWS CLI를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo apt install unzip
curl &quot;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip&quot; -o &quot;awscliv2.zip&quot;
unzip awscliv2.zip
sudo ./aws/install
aws --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS ECR을 활용한 이미지 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ECR이 필요한 이유&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJfxl/btsOLDCgxSR/HZPnQGrmn5mnKTpP7rD341/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJfxl/btsOLDCgxSR/HZPnQGrmn5mnKTpP7rD341/img.png&quot; data-alt=&quot;전통적인 배포 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJfxl/btsOLDCgxSR/HZPnQGrmn5mnKTpP7rD341/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJfxl%2FbtsOLDCgxSR%2FHZPnQGrmn5mnKTpP7rD341%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;538&quot; height=&quot;162&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전통적인 배포 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 배포 방식에서는 GitHub에 코드를 푸시하고 EC2에서 전체 프로젝트를 클론받아 실행했습니다. 이 방식은 몇 가지 문제점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 소스코드가 서버에 노출됨&lt;/li&gt;
&lt;li&gt;실행 환경 구성이 복잡함&lt;/li&gt;
&lt;li&gt;의존성 관리의 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker와 ECR을 활용하면 이러한 문제를 해결할 수 있습니다. 필요한 코드와 환경만 이미지로 패키징하여 저장하고, 배포 시에는 해당 이미지만 다운로드하여 실행하는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GIgnV/btsOLCiX8Qz/GRKfz2IuG68OKjoCBvNFYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GIgnV/btsOLCiX8Qz/GRKfz2IuG68OKjoCBvNFYk/img.png&quot; data-alt=&quot;ECR을 활용한 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GIgnV/btsOLCiX8Qz/GRKfz2IuG68OKjoCBvNFYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGIgnV%2FbtsOLCiX8Qz%2FGRKfz2IuG68OKjoCBvNFYk%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;539&quot; height=&quot;153&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ECR을 활용한 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS ECR은 Docker Hub와 같은 역할을 하는 AWS의 컨테이너 레지스트리 서비스입니다. AWS 생태계와의 연동이 원활하고 보안성이 뛰어나다는 장점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IAM 사용자 생성 및 권한 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR을 사용하기 위해서는 먼저 IAM 사용자를 생성하고 적절한 권한을 부여해야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS 콘솔에서 IAM 서비스로 이동&lt;/li&gt;
&lt;li&gt;사용자 생성 시 &quot;ECR-User&quot;와 같은 명확한 이름 설정&lt;/li&gt;
&lt;li&gt;권한 정책에서 &quot;AmazonEC2ContainerRegistryFullAccess&quot; 정책 연결&lt;/li&gt;
&lt;li&gt;사용자 생성 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o9YKE/btsOL4sNvRI/CdMBkoP0iWazUCAyrCnMe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o9YKE/btsOL4sNvRI/CdMBkoP0iWazUCAyrCnMe0/img.png&quot; data-alt=&quot;권한 정책 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o9YKE/btsOL4sNvRI/CdMBkoP0iWazUCAyrCnMe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo9YKE%2FbtsOL4sNvRI%2FCdMBkoP0iWazUCAyrCnMe0%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;542&quot; height=&quot;138&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;권한 정책 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 사용자의 Access Key를 발급받아야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;생성된 사용자 선택 후 &quot;보안 자격 증명&quot; 탭 이동&lt;/li&gt;
&lt;li&gt;&quot;액세스 키 만들기&quot; 선택&lt;/li&gt;
&lt;li&gt;&quot;외부에서 실행되는 애플리케이션&quot; 선택&lt;/li&gt;
&lt;li&gt;액세스 키와 시크릿 액세스 키 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tb4fz/btsOKDbInJt/x5QcZYcM7a1203lgWo8fQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tb4fz/btsOKDbInJt/x5QcZYcM7a1203lgWo8fQk/img.png&quot; data-alt=&quot;외부에서 실행되는 애플리케이션으로 엑세스 키 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tb4fz/btsOKDbInJt/x5QcZYcM7a1203lgWo8fQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftb4fz%2FbtsOKDbInJt%2Fx5QcZYcM7a1203lgWo8fQk%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;532&quot; height=&quot;428&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1646&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;외부에서 실행되는 애플리케이션으로 엑세스 키 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS CLI 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급받은 액세스 키로 AWS CLI를 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;aws configure&lt;/code&gt;&lt;/pre&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;AWS Access Key ID: 발급받은 액세스 키&lt;/li&gt;
&lt;li&gt;AWS Secret Access Key: 발급받은 시크릿 액세스 키&lt;/li&gt;
&lt;li&gt;Default region name: ap-northeast-2 (서울 리전)&lt;/li&gt;
&lt;li&gt;Default output format: (엔터로 스킵)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cedhwW/btsOMQ8kyge/SlS5fImzkxpI18IpjZKve1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cedhwW/btsOMQ8kyge/SlS5fImzkxpI18IpjZKve1/img.png&quot; data-alt=&quot;발급 받은 엑세스 키 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cedhwW/btsOMQ8kyge/SlS5fImzkxpI18IpjZKve1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcedhwW%2FbtsOMQ8kyge%2FSlS5fImzkxpI18IpjZKve1%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;533&quot; height=&quot;298&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;발급 받은 엑세스 키 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ECR 레포지토리 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 콘솔에서 ECR 서비스로 이동하여 새 레포지토리를 생성합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;레포지토리 생성&quot; 클릭&lt;/li&gt;
&lt;li&gt;가시성 설정을 &quot;프라이빗&quot;으로 선택 (보안상 권장)&lt;/li&gt;
&lt;li&gt;레포지토리 이름 입력 (예: &quot;my-spring-app&quot;)&lt;/li&gt;
&lt;li&gt;레포지토리 생성 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnYmQX/btsOK99eAx5/AvMdzu1Kxw1G9r0KURieaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnYmQX/btsOK99eAx5/AvMdzu1Kxw1G9r0KURieaK/img.png&quot; data-alt=&quot;프라이빗으로 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnYmQX/btsOK99eAx5/AvMdzu1Kxw1G9r0KURieaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnYmQX%2FbtsOK99eAx5%2FAvMdzu1Kxw1G9r0KURieaK%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;520&quot; height=&quot;340&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프라이빗으로 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot 애플리케이션 배포&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 배포 방식&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Dockerfile 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 Dockerfile을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

COPY build/libs/*SNAPSHOT.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이미지 빌드 및 ECR 업로드&lt;/h4&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;./gradlew clean build
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR에 로그인하고 이미지를 빌드한 후 업로드합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wx2Tk/btsOLQBeHYq/4JQiGuAIIWnkZzG9Q27aA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wx2Tk/btsOLQBeHYq/4JQiGuAIIWnkZzG9Q27aA1/img.png&quot; data-alt=&quot;푸시 명령 보기에서 가이드 따라가기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wx2Tk/btsOLQBeHYq/4JQiGuAIIWnkZzG9Q27aA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwx2Tk%2FbtsOLQBeHYq%2F4JQiGuAIIWnkZzG9Q27aA1%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;536&quot; height=&quot;206&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;푸시 명령 보기에서 가이드 따라가기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin [ECR URI]

# EC2 아키텍처에 맞게 이미지 빌드
docker build --platform linux/amd64 -t my-spring-app .

# 이미지 태깅
docker tag my-spring-app:latest [ECR URI]/my-spring-app:latest

# ECR에 이미지 푸시
docker push [ECR URI]/my-spring-app:latest&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EC2에서 컨테이너 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스에 접속하여 이미지를 다운로드하고 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dg1lf5/btsOLc52tSh/J40kBA9psIsXOWZPIimAw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dg1lf5/btsOLc52tSh/J40kBA9psIsXOWZPIimAw0/img.png&quot; data-alt=&quot;ECR URL 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dg1lf5/btsOLc52tSh/J40kBA9psIsXOWZPIimAw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdg1lf5%2FbtsOLc52tSh%2FJ40kBA9psIsXOWZPIimAw0%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;537&quot; height=&quot;224&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;855&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ECR URL 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin [ECR URI]

# 이미지 다운로드
docker pull [ECR URI]/my-spring-app

# 컨테이너 실행
docker run -d -p 8080:8080 [ECR URI]/my-spring-app&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Compose를 활용한 배포&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;compose.yml 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 컨테이너 배포도 Docker Compose로 관리하면 더 체계적입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  my-spring-app:
    image: [ECR URI]/my-spring-app:latest
    ports:
      - 8080:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Compose 실행&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose up --build -d&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;애플리케이션 업데이트&lt;/h4&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;# 로컬에서 새 버전 빌드 및 푸시
./gradlew clean build
docker build --platform linux/amd64 -t my-spring-app .
docker tag my-spring-app:latest [ECR URI]/my-spring-app:latest
docker push [ECR URI]/my-spring-app:latest

# EC2에서 새 이미지 적용
docker compose pull
docker compose up --build -d&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;실제 운영 환경에서는 애플리케이션 서버뿐만 아니라 데이터베이스, 캐시 서버 등 여러 컨테이너가 함께 동작해야 합니다. Docker Compose를 활용하면 이러한 복잡한 환경을 쉽게 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot + MySQL + Redis 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 compose.yml을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;services:
  my-spring-app:
    image: [ECR URI]/my-spring-app:latest
    ports:
      - 8080:8080
    depends_on:
      mysql-db:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql-db:3306/mydb
      - SPRING_REDIS_HOST=redis-cache

  mysql-db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
      MYSQL_DATABASE: mydb
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;]
      interval: 5s
      retries: 10

  redis-cache:
    image: redis:alpine
    ports:
      - 6379:6379
    healthcheck:
      test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
      interval: 5s
      retries: 10
&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;위 설정에서 주목할 점은 depends_on과 healthcheck 설정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;healthcheck는 각 서비스가 정상적으로 시작되었는지 확인하는 방법을 정의합니다. MySQL의 경우 mysqladmin ping 명령으로, Redis의 경우 redis-cli ping 명령으로 상태를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;depends_on의 condition: service_healthy 설정은 의존하는 서비스의 헬스체크가 성공한 후에 해당 서비스를 시작하도록 보장합니다. 이를 통해 데이터베이스가 완전히 준비된 후에 애플리케이션이 시작되도록 할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;볼륨을 통한 데이터 영속화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서비스에는 볼륨 설정이 포함되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;volumes:
  - ./mysql_data:/var/lib/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 통해 컨테이너가 재시작되어도 데이터베이스 데이터가 보존됩니다. EC2 인스턴스의 ./mysql_data 디렉토리에 실제 데이터가 저장되므로, 컨테이너 관리와 독립적으로 데이터를 유지할 수 있습니다.&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose up -d --build&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;# 모든 컨테이너 상태 확인
docker ps

# Compose 서비스별 상태 확인
docker compose ps

# 전체 로그 확인
docker compose logs

# 특정 서비스 로그 확인
docker compose logs my-spring-app
&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;AWS EC2와 Docker를 활용한 배포는 현대적인 애플리케이션 운영의 표준이 되었습니다. ECR을 통한 이미지 관리부터 Docker Compose를 활용한 멀티 컨테이너 환경 구축까지, 이러한 기술들을 조합하면 확장 가능하고 유지보수가 용이한 배포 시스템을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>AWS</category>
      <category>Deployment</category>
      <category>docker</category>
      <category>EC2</category>
      <category>ECR</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/25</guid>
      <comments>https://leve68.tistory.com/entry/AWS-EC2%EC%97%90%EC%84%9C-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%B0%ED%8F%AC#entry25comment</comments>
      <pubDate>Sun, 22 Jun 2025 21:49:43 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스와 SQL 기초</title>
      <link>https://leve68.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%99%80-SQL-%EA%B8%B0%EC%B4%88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로서 반드시 알아야 할 기본 소양 중 하나가 바로 데이터베이스입니다. 아무리 완벽한 애플리케이션을 만들어도 데이터를 저장하고 관리할 수 없다면 의미가 없겠죠. 이 글에서는 데이터베이스의 기본 개념부터 SQL 활용법까지 간단하게 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스란 무엇인가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스의 정의와 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 다수의 사용자들이 공유할 수 있도록 통합하고 저장한 데이터의 집합입니다. DBMS(Database Management System)에 의해 관리되며, 우리가 일상적으로 사용하는 모든 애플리케이션의 뒤에서 중요한 역할을 담당합니다.&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;b&gt;동시 공유&lt;/b&gt;가 가능해 여러 사용자가 동시에 데이터를 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 독립성&lt;/b&gt;도 중요한 특징입니다. 애플리케이션과 데이터 저장 구조가 독립적으로 운영되어, 한쪽의 변경이 다른 쪽에 영향을 주지 않습니다. &lt;b&gt;무결성&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;중복 최소화&lt;/b&gt;는 정규화를 통해 달성되며, 이는 저장 공간을 효율적으로 사용하고 데이터 일관성을 유지하는 데 도움이 됩니다. 마지막으로 &lt;b&gt;보안&lt;/b&gt; 기능을 통해 권한이 있는 사용자만 데이터에 접근할 수 있도록 제어합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL과 관계형 데이터베이스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL 서버 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 현재 가장 널리 사용되는 오픈소스 관계형 데이터베이스 중 하나입니다. MySQL 서버는 크게 MySQL 엔진과 Storage 엔진으로 구성됩니다.&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;MySQL 엔진&lt;/b&gt;은 SQL 인터페이스, 파서, 옵티마이저, 캐시 등을 포함하며, 클라이언트의 요청을 처리하는 핵심 역할을 담당합니다. 쿼리를 분석하고 최적화하는 것이 주된 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Storage 엔진&lt;/b&gt;은 실제 데이터를 저장하고 관리하는 부분입니다. MySQL 5.5 이후 버전에서는 &lt;b&gt;InnoDB&lt;/b&gt;가 기본 엔진으로 설정되어 있으며, 현재 대부분의 프로덕션 환경에서 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;InnoDB의 주요 특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 지원으로 데이터 일관성 보장&lt;/li&gt;
&lt;li&gt;외래키 제약조건 지원&lt;/li&gt;
&lt;li&gt;행 수준 잠금으로 동시성 향상&lt;/li&gt;
&lt;li&gt;장애 복구 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관계형 데이터베이스의 구성 요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스는 세 가지 핵심 요소로 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개체(Entity)&lt;/b&gt;는 현실 세계의 유형 또는 무형의 실체를 나타내며, 데이터베이스에서는 테이블로 구현됩니다. 예를 들어 회원, 상품, 주문 같은 것들이 개체가 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관계(Relationship)&lt;/b&gt;는 2개 이상의 개체 간의 상호 작용이나 연결을 정의하며, 데이터베이스에서는 외래키로 구현됩니다. 1:1관계, 1:N관계, N:M관계 등 다양한 형태가 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속성(Attribute)&lt;/b&gt;은 개체가 포함하는 하나하나의 성질을 나타내며, 데이터베이스에서는 열(Column)로 구현됩니다. 회원 개체라면 이름, 이메일, 전화번호 등이 속성이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL 기초와 SELECT 문&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL의 정의와 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL(Structured Query Language)은 관계형 데이터베이스 관리 시스템의 데이터를 관리하기 위해 설계된 프로그래밍 언어입니다. ANSI SQL을 사용하면 이 표준을 따른 모든 DBMS에서 사용할 수 있어 이식성이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL은 크게 네 가지로 분류됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DML(Data Manipulation Language)&lt;/b&gt;: 데이터 조작어 - INSERT, UPDATE, DELETE, SELECT&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DDL(Data Definition Language)&lt;/b&gt;: 데이터 정의어 - CREATE, ALTER, DROP, RENAME&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCL(Transaction Control Language)&lt;/b&gt;: 트랜잭션 제어어 - COMMIT, ROLLBACK, SAVEPOINT&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DCL(Data Control Language)&lt;/b&gt;: 데이터 제어어 - GRANT, REVOKE&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SELECT 문의 기본 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 문은 테이블에 저장된 데이터를 조회하는 가장 기본적이면서도 중요한 SQL 문입니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * | {[DISTINCT] 컬럼명 | 표현식} {[AS] [별칭]}, ...
FROM 테이블명
WHERE 조건식 [{AND|OR}, 조건식];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SELECT절&lt;/b&gt;에서는 조회하려는 컬럼명 또는 표현식을 나열하고, 별칭을 사용할 수 있습니다. &lt;b&gt;FROM절&lt;/b&gt;에서는 조회하려는 데이터가 저장된 테이블을 명시하며, &lt;b&gt;WHERE절&lt;/b&gt;에서는 결과 집합을 filtering하기 위한 조건을 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;열 별칭 활용(AS)&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 name, salary * 12 AS '연봉'
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복 행 제거(DISTINCT)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회하는 모든 컬럼의 조합이 일치하는 행을 제거하고 싶을 때 DISTINCT 키워드를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT DISTINCT department
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조건부 데이터 조회와 WHERE 절&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WHERE 절의 기본 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절은 결과 집합을 filtering하기 위한 핵심 구문입니다. 테이블에 저장된 데이터 중에서 원하는 데이터만 선택적으로 검색하는 기능을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name, salary
FROM employees
WHERE salary &amp;gt; 50000;
&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;기본 비교 연산자&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=: 같다&lt;/li&gt;
&lt;li&gt;!=, &amp;lt;&amp;gt;: 같지 않다&lt;/li&gt;
&lt;li&gt;&amp;lt;, &amp;lt;=, &amp;gt;, &amp;gt;=: 작다, 작거나 같다, 크다, 크거나 같다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NULL 비교&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IS NULL&lt;/b&gt;: NULL인지 비교
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NULL 값은 컬럼에 값이 지정(할당)되지 않은 상태&lt;/li&gt;
&lt;li&gt;0(zero)이나 공백과는 다른 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name FROM employees WHERE commission IS NULL;
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BETWEEN a AND b&lt;/b&gt;: a이상 b이하의 범위 비교(경계값 포함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IN (value1, value2, ...)&lt;/b&gt;: 목록 안에 일치하는 값이 있는지 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name FROM employees WHERE salary BETWEEN 30000 AND 50000;
SELECT name FROM employees WHERE department IN ('개발팀', '기획팀');
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;LIKE 패턴&lt;/b&gt;: 특정 패턴을 만족하는지 비교
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;%: 0개 이상의 임의의 문자&lt;/li&gt;
&lt;li&gt;_: 1개의 임의의 문자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name FROM employees WHERE name LIKE '김%';  -- 김으로 시작하는 이름
SELECT name FROM employees WHERE name LIKE '김_수';  -- 김?수 형태의 이름
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;논리 연산자와 NULL 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;논리 연산자&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AND, &amp;amp;&amp;amp;&lt;/b&gt;: 논리 곱&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OR, ||&lt;/b&gt;: 논리 합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;XOR&lt;/b&gt;: 배타적 논리 합 (두 논리식의 결과가 서로 다를 때만 참)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NOT, !&lt;/b&gt;: 논리 부정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NULL과 논리 연산의 특별한 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL과 논리 연산에서는 특별한 주의가 필요합니다. NULL과의 연산 결과는 기본적으로 NULL이 되며, WHERE 절에서 NULL 결과는 FALSE로 처리되어 해당 행이 결과에서 제외됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- column2가 실제로 NULL이라면, 이 조건은 참이 되어 결과에 포함
WHERE column1 = 10 AND column2 IS NULL

-- column2가 NULL이라면 결과는 NULL이 되어 해당 행은 제외
WHERE column1 = column2&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정렬과 결과 제한&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ORDER BY절&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT구문 실행 결과를 특정 컬럼 값 기준으로 정렬할 때 사용합니다. 컬럼이름이나 컬럼 별칭, 컬럼 기술 순서(1,2..)로 표현 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name, salary
FROM employees
ORDER BY salary DESC, name ASC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 조건은 &lt;b&gt;ASC(오름차순, 기본값)&lt;/b&gt;과 &lt;b&gt;DESC(내림차순)&lt;/b&gt;가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LIMIT절을 통한 결과 제한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행 결과 제한을 위해 LIMIT절을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;LIMIT 개수 [OFFSET n]
-- 또는
LIMIT n, 개수
&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;sql&quot;&gt;&lt;code&gt;SELECT * FROM employees LIMIT 5;                    -- 처음 5개 행
SELECT * FROM employees LIMIT 5 OFFSET 10;          -- 11번째부터 5개 행
SELECT * FROM employees LIMIT 10, 5;                -- 위와 동일 (구 문법)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내장 함수 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자 관련 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 수학 연산&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ABS(숫자)&lt;/b&gt;: 절댓값을 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIGN(숫자)&lt;/b&gt;: 부호를 반환 (음수(-1), 0(0), 양수(+1))&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MOD(분자, 분모)&lt;/b&gt;: 분자를 분모로 나눈 나머지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반올림 및 올림/내림&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ROUND(숫자, 자릿수=0)&lt;/b&gt;: 숫자를 지정된 자릿수로 반올림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TRUNCATE(숫자, 자릿수)&lt;/b&gt;: 숫자를 지정된 자릿수로 버림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CEILING(숫자)&lt;/b&gt;: 값보다 큰 정수 중 가장 작은 수 (올림)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FLOOR(숫자)&lt;/b&gt;: 값보다 작은 정수 중 가장 큰 수 (내림)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT ROUND(3.14159, 2);     -- 3.14
SELECT CEILING(3.14);         -- 4
SELECT FLOOR(3.14);           -- 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;기본 문자 처리 함수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CONCAT('문자열1', '문자열2', ...)&lt;/b&gt;: 문자열들을 결합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LENGTH('문자열')&lt;/b&gt;: 문자열의 바이트 크기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CHAR_LENGTH('문자열')&lt;/b&gt;: 문자열의 길이(글자수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문자열 추출&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SUBSTRING('문자열', 시작위치, 개수)&lt;/b&gt;: 지정된 위치부터 개수만큼 추출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LEFT('문자열', 개수)&lt;/b&gt;: 왼쪽에서 개수만큼 추출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RIGHT('문자열', 개수)&lt;/b&gt;: 오른쪽에서 개수만큼 추출&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CONCAT('안녕', '하세요');          -- '안녕하세요'
SELECT SUBSTRING('Hello World', 7, 5);   -- 'World'
SELECT LEFT('Hello', 2);                 -- 'He'
&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;현재 날짜/시간 함수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;NOW()&lt;/b&gt;: 현재 날짜와 시간&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CURDATE()&lt;/b&gt;: 현재 날짜&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CURTIME()&lt;/b&gt;: 현재 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;날짜 연산&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DATE_ADD(날짜, INTERVAL n UNIT)&lt;/b&gt;: 날짜에서 n UNIT만큼 더함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DATE_SUB(날짜, INTERVAL n UNIT)&lt;/b&gt;: 날짜에서 n UNIT만큼 뺌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT NOW();                                    -- 2024-01-15 14:30:25
SELECT DATE_ADD('2024-01-15', INTERVAL 7 DAY);  -- 2024-01-22&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;IF(논리식, 값1, 값2)&lt;/b&gt;를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name, IF(salary &amp;gt; 50000, '고액연봉', '일반연봉') AS 연봉구분
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL 처리를 위해서는 &lt;b&gt;IFNULL(값, 대체값)&lt;/b&gt;을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT name, IFNULL(commission, 0) AS 수당
FROM employees;
&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;데이터베이스와 SQL은 현대 소프트웨어 개발에서 빼놓을 수 없는 핵심 기술입니다. 기본적인 개념부터 시작해서 실제 데이터 조회까지, 복잡한 데이터 처리도 해낼 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Database</category>
      <category>DATABASE</category>
      <category>MySQL</category>
      <category>sql</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/24</guid>
      <comments>https://leve68.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%99%80-SQL-%EA%B8%B0%EC%B4%88#entry24comment</comments>
      <pubDate>Fri, 20 Jun 2025 15:31:38 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2에 서버 배포</title>
      <link>https://leve68.tistory.com/entry/AWS-EC2%EC%97%90-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 완성한 웹 애플리케이션을 실제 사용자들이 접근할 수 있도록 만드는 것, 바로 배포입니다. 도커를 활용한 배포를 학습하기 전, 이번 글에서는 AWS EC2를 활용하여 서버를 배포하는 전 과정을 단계별로 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포의 본질과 필요성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포란 무엇인가&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M24sK/btsOFOKOtR6/270QNdZD0jHSImI2EiJgak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M24sK/btsOFOKOtR6/270QNdZD0jHSImI2EiJgak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M24sK/btsOFOKOtR6/270QNdZD0jHSImI2EiJgak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM24sK%2FbtsOFOKOtR6%2F270QNdZD0jHSImI2EiJgak%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;578&quot; height=&quot;355&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포(Deployment)는 우리가 개발한 애플리케이션을 다른 사용자들이 인터넷을 통해 접근할 수 있도록 만드는 과정입니다. 로컬 환경에서만 동작하던 서버를 외부에 공개하여 전 세계 어디서든 접속 가능하게 만드는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 완료되면 애플리케이션은 고유한 주소를 부여받게 됩니다. IP 주소(예: 124.16.2.1)나 도메인(예: www.example.com) 형태로 제공되는 이 주소를 통해 사용자들은 우리의 서비스에 접근할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개인 컴퓨터 배포의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로는 개인 컴퓨터에서도 서버를 배포할 수 있습니다. 하지만 실제로는 여러 제약사항이 있습니다. 24시간 컴퓨터를 켜두어야 하는 전력 비용 문제, 개인 네트워크의 보안 위험성, 그리고 불안정한 인터넷 연결 등이 주요 걸림돌입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들을 해결하기 위해 클라우드 서비스가 등장했고, 그 중에서도 AWS EC2는 가장 널리 사용되는 서비스 중 하나입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS EC2 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EC2의 기본 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2(Elastic Compute Cloud)는 아마존에서 제공하는 클라우드 컴퓨팅 서비스입니다. 간단히 말해 인터넷을 통해 컴퓨터를 빌려 사용하는 서비스라고 생각하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2의 핵심은 물리적인 하드웨어 없이도 강력한 컴퓨팅 리소스를 활용할 수 있다는 점입니다. 필요에 따라 성능을 조절할 수 있고, 사용한 만큼만 비용을 지불하면 되며, 전 세계 어디서든 안정적인 서비스를 제공할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리전의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS는 전 세계에 여러 데이터 센터를 운영하고 있으며, 이를 리전(Region)이라고 부릅니다. 리전은 단순히 컴퓨터가 물리적으로 위치한 지역을 의미하지만, 서비스 성능에 큰 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리전 선택의 핵심 기준은 주요 사용자층의 지리적 위치입니다. 한국 사용자를 대상으로 하는 서비스라면 서울 리전을 선택하는 것이 가장 적절합니다. 물리적 거리가 가까울수록 네트워크 지연시간이 줄어들어 더 빠른 서비스를 제공할 수 있기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EC2 인스턴스 생성과 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 설정 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스 생성 과정에서 가장 먼저 결정해야 할 것은 운영 체제입니다. 여러 선택지가 있지만, 서버 운영에는 Ubuntu 22.04 LTS를 주로 사용한다고 합니다. 안정성과 성능, 그리고 풍부한 커뮤니티 지원이 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 유형은 EC2에서 빌리는 컴퓨터의 사양을 의미합니다. 처음 시작할 때는 프리 티어로 제공되는 t2.micro나 t3.micro를 선택하면 충분합니다. 나중에 필요에 따라 성능을 업그레이드할 수 있으니 부담 없이 시작해도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안 설정의 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 페어는 EC2 인스턴스에 접근할 때 사용하는 인증 수단입니다. 전통적인 아이디/패스워드 대신 공개키 암호화 방식을 사용하여 더 안전한 접근을 보장합니다. RSA와 .pem 형식을 선택하여 키 페어를 생성하면 되며, 생성된 키 파일은 안전하게 보관해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 설정에서는 보안 그룹을 구성해야 합니다. 최소한 두 개의 포트를 열어야 하는데, 22번 포트는 SSH 접속을 위한 것이고, 80번 포트는 웹 서버 운영을 위한 것입니다. 소스 유형을 '위치 무관'으로 설정하면 전 세계 어디서든 접근할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 그룹&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안 그룹의 역할&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcyIRE/btsOGW83LiC/YGIeFdnkJ998lu5hVJKkNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcyIRE/btsOGW83LiC/YGIeFdnkJ998lu5hVJKkNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcyIRE/btsOGW83LiC/YGIeFdnkJ998lu5hVJKkNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcyIRE%2FbtsOGW83LiC%2FYGIeFdnkJ998lu5hVJKkNk%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;678&quot; height=&quot;164&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 그룹은 AWS 클라우드에서 네트워크 보안을 담당하는 핵심 구성 요소입니다. EC2 인스턴스를 집이라고 생각한다면, 보안 그룹은 집 바깥쪽에 설치된 울타리와 대문 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방화벽은 두 가지 트래픽을 제어합니다. 인바운드 트래픽은 외부에서 EC2 인스턴스로 들어오는 데이터를, 아웃바운드 트래픽은 EC2 인스턴스에서 외부로 나가는 데이터를 의미합니다. 각각에 대해 허용할 IP 범위와 포트를 세밀하게 설정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적절한 보안 정책 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 그룹 설정에서는 최소 권한 원칙을 적용하는 것이 중요합니다. 필요한 포트만 열고, 가능하다면 접근 IP 범위도 제한하는 것이 좋습니다. 하지만 웹 서비스의 경우 전 세계 사용자의 접근을 허용해야 하므로, 80번이나 443번 포트는 모든 IP에 대해 열어두는 것이 일반적입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EC2 인스턴스 관리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인스턴스 정보 파악하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFdkRn/btsOHgFXP3b/9xoX4l5Wi133bap98R7o31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFdkRn/btsOHgFXP3b/9xoX4l5Wi133bap98R7o31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFdkRn/btsOHgFXP3b/9xoX4l5Wi133bap98R7o31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFdkRn%2FbtsOHgFXP3b%2F9xoX4l5Wi133bap98R7o31%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;571&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스가 생성되면 다양한 정보를 확인할 수 있습니다. 가장 중요한 것은 퍼블릭 IPv4 주소입니다. 이것이 바로 외부에서 우리의 서버에 접근할 때 사용하는 주소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 상태는 현재 컴퓨터가 켜져 있는지 꺼져 있는지를 나타냅니다. '실행 중' 상태여야 외부에서 접근할 수 있습니다. 모니터링 탭에서는 CPU 사용률, 네트워크 트래픽 등 다양한 성능 지표를 확인할 수 있어 서버 운영에 큰 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탄력적 IP의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스를 중지했다가 다시 시작하면 IP 주소가 변경됩니다. 이는 서비스 운영에 큰 문제가 될 수 있습니다. 사용자들이 기존 주소로 접근하려 해도 연결되지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탄력적 IP(Elastic IP)는 이 문제를 해결해주는 기능입니다. 고정 IP를 할당받아 인스턴스를 재시작해도 동일한 주소를 유지할 수 있습니다. 실제 서비스 운영에서는 거의 필수적인 기능이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탄력적 IP 페이지에서 IP를 할당받아 EC2 인스턴스와 연결해주면 EC2 인스턴스의 IP를 고정할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 애플리케이션 배포하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Express 서버 배포 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 기반의 Express 서버를 배포하는 과정을 살펴보겠습니다. 먼저 Ubuntu에 Node.js를 설치해야 합니다. 최신 LTS 버전을 설치하는 것을 권장합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Node.js 설치
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 파일을 EC2에 업로드하는 방법은 여러 가지가 있습니다. Git을 사용하거나, SCP로 파일을 전송하거나, 직접 파일을 복사하는 방법 등이 있습니다. 환경 변수가 필요한 경우 .env 파일을 별도로 생성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 안정적으로 운영하기 위해서는 PM2(Process Manager 2)를 사용하는 것이 좋습니다. PM2는 Node.js 애플리케이션을 백그라운드에서 실행하고, 오류 발생 시 자동으로 재시작해주는 프로세스 매니저입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# PM2 설치 및 서버 실행
npm install -g pm2
pm2 start app.js --name &quot;my-server&quot;
pm2 save
pm2 startup
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot 서버 배포 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션 배포는 Java 기반이므로 JDK 설치부터 시작합니다. OpenJDK를 사용하는 것이 일반적입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# OpenJDK 설치 (예: Java 17)
sudo apt update
sudo apt install openjdk-17-jdk
java -version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 EC2에 업로드한 후, 포트 설정을 확인해야 합니다. 일반적으로 웹 서버는 80번 포트를 사용하므로 application.yml 파일에 다음과 같이 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;server:
  port: 80
&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;# 프로젝트 빌드
./gradlew clean build

# JAR 파일 실행
cd build/libs
sudo java -jar your-application-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 80번 포트에서 실행하려면 관리자 권한이 필요하므로 sudo 명령어를 사용해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포 후 관리와 모니터링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 상태 점검&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 성공적으로 배포되었다면, 웹 브라우저에서 EC2 인스턴스의 퍼블릭 IP 주소로 접근해볼 수 있습니다. 정상적으로 페이지가 로드된다면 배포가 완료된 것입니다.&lt;/p&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;AWS EC2를 활용한 서버 배포는 현대 웹 개발에서 필수적인 기술입니다. 처음에는 복잡해 보일 수 있지만, 직접 해보면서 기본 개념을 이해하고 단계별로 학습하면 좋을 것 같습니다. 다음 글에서는 이 지식을 바탕으로 docker를 활용해서 배포를 진행해 보겠습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>AWS</category>
      <category>Deployment</category>
      <category>docker</category>
      <category>EC2</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/23</guid>
      <comments>https://leve68.tistory.com/entry/AWS-EC2%EC%97%90-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC#entry23comment</comments>
      <pubDate>Wed, 18 Jun 2025 17:22:50 +0900</pubDate>
    </item>
    <item>
      <title>Docker Compose 2</title>
      <link>https://leve68.tistory.com/entry/Docker-Compose-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현대 애플리케이션은 단일 서비스로만 구성되는 경우가 드뭅니다. 대부분 웹 서버, 데이터베이스, 캐시 서버 등 여러 컴포넌트가 함께 동작하여 완전한 시스템을 구성합니다. 개발 과정에서 이런 복잡한 환경을 매번 수동으로 설정하는 것은 매우 비효율적입니다. Docker Compose는 이런 문제를 해결해주는 도구로, 여러 컨테이너를 하나의 설정 파일로 관리할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Compose의 필요성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 컨테이너 관리의 복잡성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 웹 애플리케이션을 개발할 때를 생각해보겠습니다. Spring Boot로 개발한 백엔드 서버가 있고, MySQL 데이터베이스가 필요하며, 성능 향상을 위해 Redis 캐시 서버도 함께 사용한다고 가정해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 없이 이런 환경을 구성한다면 각각의 서비스를 개별적으로 설치하고 설정해야 합니다. Docker를 사용하더라도 각 컨테이너를 따로 실행하고 네트워크를 연결하는 것은 여전히 번거로운 작업입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Compose가 제공하는 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 YAML 파일 하나로 여러 컨테이너의 설정을 정의하고, 단일 명령어로 전체 시스템을 실행할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 다중 컨테이너 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL과 Redis 동시 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 예제부터 시작해보겠습니다. MySQL 데이터베이스와 Redis 캐시 서버를 동시에 실행하는 설정을 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 compose.yml 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306

  my-cache-server:
    image: redis
    ports:
      - 6379:6379&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정 파일은 두 개의 서비스를 정의합니다. my-db는 MySQL 컨테이너를, my-cache-server는 Redis 컨테이너를 나타냅니다. 각 서비스는 독립적인 설정을 가지며, Docker Compose가 이들을 하나의 네트워크로 연결해줍니다.&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;docker compose up -d
&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;docker compose ps
&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;docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot 애플리케이션과 데이터베이스 연동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 파일 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 애플리케이션을 포함한 더 복잡한 시나리오를 다뤄보겠습니다. Spring Boot 애플리케이션이 MySQL 데이터베이스와 연동하는 환경을 구성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Spring Boot 프로젝트의 application.yml 파일을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://my-db:3306/mydb
    username: root
    password: pwd1234
    driver-class-name: com.mysql.cj.jdbc.Driver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 URL에서 localhost 대신 my-db를 사용한다는 것입니다. 이는 Docker Compose의 서비스명을 통해 컨테이너 간 통신하는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 컨테이너화하기 위한 Dockerfile을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

COPY build/libs/*SNAPSHOT.jar /app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Compose 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Spring Boot와 MySQL을 함께 실행하는 compose.yml 파일을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  my-server:
    build: .
    ports:
      - 8080:8080
    depends_on:
      my-db:
        condition: service_healthy
        
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
      MYSQL_DATABASE: mydb
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;]
      interval: 5s
      retries: 10
&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;depends_on과 healthcheck&lt;/b&gt;는 서비스 간의 의존성과 실행 순서를 관리합니다. MySQL이 완전히 초기화되기 전에 Spring Boot가 실행되면 데이터베이스 연결 오류가 발생할 수 있습니다. healthcheck를 통해 MySQL이 준비된 상태인지 확인하고, depends_on으로 이 조건이 만족될 때까지 Spring Boot 컨테이너의 실행을 지연시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MYSQL_DATABASE 환경변수&lt;/b&gt;는 MySQL 컨테이너 초기 실행 시 자동으로 mydb 데이터베이스를 생성해줍니다.&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;armasm&quot;&gt;&lt;code&gt;./gradlew clean build
docker compose up -d --build
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--build 옵션은 Docker 이미지를 새로 빌드하도록 강제하는 옵션으로, 코드가 변경되었을 때 유용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 네트워킹의 이해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;localhost의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션을 컨테이너로 실행할 때 가장 자주 발생하는 문제는 네트워킹 관련 이슈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 개발 환경에서는 모든 서비스가 같은 컴퓨터에서 실행되므로 localhost로 서로 통신할 수 있었습니다. 하지만 컨테이너 환경에서는 각 컨테이너가 자신만의 네트워크망과 IP 주소를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 컨테이너 입장에서 localhost:3306은 Spring Boot 컨테이너 내부의 3306번 포트를 의미합니다. MySQL은 별도의 컨테이너에서 실행되고 있으므로, 이 주소로는 MySQL에 접근할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OfD1o/btsOFEtnynb/IvJ27ScaOd6kDD37Dw5e80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OfD1o/btsOFEtnynb/IvJ27ScaOd6kDD37Dw5e80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OfD1o/btsOFEtnynb/IvJ27ScaOd6kDD37Dw5e80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOfD1o%2FbtsOFEtnynb%2FIvJ27ScaOd6kDD37Dw5e80%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;743&quot; height=&quot;338&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;884&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;Docker Compose는 이 문제를 해결하기 위해 서비스명을 DNS로 사용할 수 있게 해줍니다. compose.yml에서 정의한 서비스명 my-db가 곧 해당 컨테이너의 네트워크 주소가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 application.yml에서 데이터베이스 URL을 다음과 같이 설정해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://my-db:3306/mydb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Spring Boot는 Docker Compose 네트워크를 통해 MySQL 컨테이너에 정확히 연결할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다중 서비스 애플리케이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 캐시 서버 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Spring Boot, MySQL, Redis가 모두 함께 동작하는 완전한 시스템을 구축해보겠습니다. 먼저 Spring Boot 프로젝트에 Redis 의존성을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    // 기타 의존성들...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 연결을 위한 설정 클래스를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 컨트롤러도 추가하여 Redis 연결을 테스트해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
public class AppController {

    @Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @GetMapping(&quot;/&quot;)
    public String home() {
        redisTemplate.opsForValue().set(&quot;test-key&quot;, &quot;Hello Redis!&quot;);
        return &quot;Application is running with Redis cache!&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서비스에 대한 설정을 포함한 application.yml을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://my-db:3306/mydb
    username: root
    password: pwd1234
    driver-class-name: com.mysql.cj.jdbc.Driver
  data:
    redis:
      host: my-cache-server
      port: 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 Redis 호스트를 localhost가 아닌 my-cache-server로 설정했습니다. 이는 앞서 설명한 컨테이너 네트워킹 원리와 동일합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Compose 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개의 서비스가 모두 포함된 완전한 compose.yml 파일입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  my-server:
    build: .
    ports:
      - 8080:8080
    depends_on:
      my-db:
        condition: service_healthy
      my-cache-server:
        condition: service_healthy
        
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
      MYSQL_DATABASE: mydb
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;]
      interval: 5s
      retries: 10
      
  my-cache-server:
    image: redis
    ports:
      - 6379:6379
    healthcheck:
      test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
      interval: 5s
      retries: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 서비스가 MySQL과 Redis 모두가 준비될 때까지 기다리도록 depends_on 설정을 확장했습니다. Redis에도 healthcheck를 추가하여 서비스가 완전히 준비된 상태에서만 다음 단계로 진행하도록 했습니다.&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;armasm&quot;&gt;&lt;code&gt;./gradlew clean build
docker compose down
docker compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서비스가 정상적으로 실행되면 브라우저에서 http://localhost:8080에 접속하여 애플리케이션이 정상 동작하는지 확인할 수 있습니다.&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;docker compose ps
docker logs [컨테이너_ID]
&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;2048&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blq9C6/btsODWvkPk2/QzjtwaCa3BvBxZXZjAfe3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blq9C6/btsODWvkPk2/QzjtwaCa3BvBxZXZjAfe3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blq9C6/btsODWvkPk2/QzjtwaCa3BvBxZXZjAfe3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblq9C6%2FbtsODWvkPk2%2FQzjtwaCa3BvBxZXZjAfe3K%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;757&quot; height=&quot;264&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;715&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;Docker Compose는 복잡한 다중 컨테이너 환경을 단순하게 관리할 수 있게 해주는 필수 도구입니다. 서비스 간의 의존성 관리, 네트워크 설정, 헬스체크 등의 기능을 통해 안정적인 개발 환경을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>compose</category>
      <category>docker</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/22</guid>
      <comments>https://leve68.tistory.com/entry/Docker-Compose-2#entry22comment</comments>
      <pubDate>Tue, 17 Jun 2025 16:54:16 +0900</pubDate>
    </item>
    <item>
      <title>Docker Compose 1</title>
      <link>https://leve68.tistory.com/entry/Docker-Compose-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하다 보면 필연적으로 마주치게 되는 문제가 있습니다. 단일 컨테이너만으로는 실제 서비스를 구성하기 어렵다는 점입니다. 웹 애플리케이션을 운영하려면 프론트엔드 서버, 백엔드 API 서버, 데이터베이스, 캐시 서버 등 여러 컨테이너가 함께 동작해야 합니다. 각각의 컨테이너를 개별적으로 관리하는 것은 복잡하고 비효율적입니다. Docker Compose는 이러한 문제를 해결하여 여러 컨테이너를 하나의 서비스로 정의하고 통합 관리할 수 있게 해주는 도구입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Compose의 핵심 가치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡성 제거와 표준화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose의 가장 큰 장점은 복잡한 Docker CLI 명령어들을 YAML 파일 하나로 대체할 수 있다는 점입니다. 긴 옵션들과 매개변수들로 가득한 docker run 명령어 대신, 직관적이고 읽기 쉬운 설정 파일로 컨테이너를 정의할 수 있습니다.&lt;/p&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;docker run -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 \
  -v /Users/seungyeon/Documents/Develop/docker-mysql/mysql_data:/var/lib/mysql \
  -d mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 YAML 파일로 동일한 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750049322263&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - /Users/seungyeon/Documents/Develop/docker-mysql/mysql_data:/var/lib/mysql
    ports:
      - 3306:3306&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;Docker Compose에서는 각각의 컨테이너를 &lt;b&gt;서비스(service)&lt;/b&gt;라는 개념으로 다룹니다. 이는 단순히 컨테이너를 실행하는 것을 넘어서, 전체 시스템을 구성하는 하나의 구성 요소로 바라보는 관점입니다. 이러한 접근 방식은 마이크로서비스 아키텍처와도 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Compose 파일 작성하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 구조 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose 파일의 가장 기본적인 형태부터 살펴보겠습니다. compose.yml 파일을 생성하고 다음 내용을 작성해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-web-server:
    container_name: webserver
    image: nginx
    ports: 
      - 80:80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일의 구조를 분석해보면 다음과 같습니다. YAML 파일은 들여쓰기로 계층을 구분하므로 정확한 들여쓰기가 중요합니다. 반드시 탭이 아닌 스페이스를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;services 섹션 하위에 정의된 my-web-server는 서비스 이름입니다. 이는 Docker Compose 내에서 이 컨테이너를 식별하는 고유한 이름으로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;container_name은 실제 생성될 컨테이너의 이름을 지정하며, CLI에서 --name 옵션과 동일한 역할을 합니다. image는 사용할 Docker 이미지를 명시하고, ports는 포트 매핑을 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Compose 파일 실행과 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 compose 파일을 실행하는 것은 매우 간단합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-d 옵션을 사용하면 백그라운드에서 실행되어 터미널을 계속 사용할 수 있습니다. 실행 현황은 다음 명령어로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 compose.yml에 정의된 컨테이너 중 실행 중인 것들만 보여줍니다. 모든 컨테이너를 확인하고 싶다면 -a 옵션을 추가하면 됩니다.&lt;/p&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;docker compose down&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 compose로 생성된 모든 컨테이너를 중지하고 삭제합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 활용 사례들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 캐시 서버 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 캐싱을 위한 Redis 서버를 Docker Compose로 구성해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-cache-server:
    image: redis
    ports:
      - 6379:6379&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 기본 포트인 6379를 호스트의 동일한 포트로 매핑했습니다. 컨테이너가 실행되면 Redis 클라이언트로 접속하여 정상 동작을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너 접속
docker exec -it [컨테이너명] bash

# Redis CLI 실행 후 테스트
redis-cli
set mykey &quot;Hello Compose&quot;
get mykey&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL 데이터베이스와 볼륨 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서는 데이터 영속성이 중요하므로 볼륨과 함께 MySQL을 설정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pwd1234
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 environment 섹션입니다. 이는 컨테이너 실행 시 환경변수를 설정하는 기능으로, CLI의 -e 옵션과 동일합니다. volumes 섹션을 통해 현재 디렉토리의 mysql_data 폴더와 컨테이너의 데이터 디렉토리를 연결하여 데이터를 영속적으로 보존할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 애플리케이션 배포하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot 애플리케이션 컨테이너화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 개발한 Spring Boot 애플리케이션을 Docker Compose로 배포하는 과정을 살펴보겠습니다. 먼저 Dockerfile을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

COPY build/libs/*SNAPSHOT.jar /app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 빌드한 후 compose.yml 파일을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;./gradlew clean build&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-server:
    build: .
    ports:
      - 8080:8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 build: .는 현재 디렉토리의 Dockerfile을 사용하여 이미지를 빌드하라는 의미입니다. 기존에 만들어진 이미지를 사용하는 image 옵션과는 다른 접근 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시에는 --build 옵션을 추가하여 이미지를 새로 빌드하도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose up -d --build&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node.js 기반 애플리케이션들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest.js나 Next.js 같은 Node.js 기반 애플리케이션도 유사한 방식으로 배포할 수 있습니다. Nest.js의 경우 다음과 같은 Dockerfile을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM node

WORKDIR /app

COPY . .

RUN npm install

RUN npm run build

EXPOSE 3000

ENTRYPOINT [ &quot;node&quot;, &quot;dist/main.js&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.dockerignore 파일로 불필요한 파일들을 제외합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;node_modules&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose.yml 파일은 다음과 같이 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-server:
    build: .
    ports:
      - 3000:3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js의 경우에는 프로덕션 모드로 실행하기 위해 Dockerfile을 약간 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM node:20-alpine

WORKDIR /app

COPY . .

RUN npm install

RUN npm run build

EXPOSE 3000

ENTRYPOINT [ &quot;npm&quot;, &quot;run&quot;, &quot;start&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose.yml에서는 호스트의 80번 포트로 접근할 수 있도록 매핑을 변경할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-web-server:
    build: .
    ports:
      - 80:3000&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;HTML, CSS로 구성된 정적 웹사이트는 Nginx를 사용하여 매우 간단하게 배포할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM nginx 
COPY ./ /usr/share/nginx/html&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  my-web-server:
    build: .
    ports:
      - 80:80&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고급 Docker Compose 기능들&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 compose.yml에 정의된 모든 컨테이너의 로그를 시간순으로 정렬하여 보여줍니다. 특정 서비스의 로그만 확인하고 싶다면 서비스 이름을 추가하면 됩니다.&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 이미지 다운로드 및 업데이트
docker compose pull

# 강제 재빌드와 함께 실행
docker compose up --build -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pull 명령어는 compose.yml에 정의된 외부 이미지들을 최신 버전으로 업데이트하고, --build 옵션은 로컬에서 빌드하는 이미지들을 강제로 재빌드합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 컨테이너 오케스트레이션을 위한 최적의 도구입니다. 복잡한 CLI 명령어들을 YAML 파일로 추상화하여 관리를 용이하게 하고, 여러 컨테이너를 하나의 서비스 단위로 묶어서 관리할 수 있게 해줍니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>compose</category>
      <category>docker</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/21</guid>
      <comments>https://leve68.tistory.com/entry/Docker-Compose-1#entry21comment</comments>
      <pubDate>Mon, 16 Jun 2025 13:55:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 핵심 컴포넌트</title>
      <link>https://leve68.tistory.com/entry/Spring-Security-%ED%95%B5%EC%8B%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 Spring Security의 흐름을 간단하게 알아보았습니다. Spring Security를 사용해 단순해 보이는 로그인 기능 하나 구현하려다 보면 Filter, SecurityContext, Authentication, AuthenticationManager 등 생소한 개념들이 복잡하게 얽혀있는 구조를 마주하게 됩니다. 하지만 이 복잡성에는 분명한 이유가 있습니다. 현대 웹 애플리케이션의 보안 요구사항이 결코 단순하지 않기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹 보안의 본질적 복잡성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 개의 근본적 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 보안의 복잡성을 이해하려면 모든 보안 시스템이 답해야 하는 두 가지 근본적 질문을 살펴봐야 합니다. &lt;b&gt;&quot;당신은 누구인가?&quot;(Authentication)&lt;/b&gt;와 &lt;b&gt;&quot;당신은 무엇을 할 수 있는가?&quot;(Authorization)&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 다양한 방식으로 접근할 수 있습니다. 브라우저를 통한 폼 로그인, 모바일 앱을 통한 토큰 기반 인증, API 클라이언트를 통한 JWT 토큰 등 각각은 서로 다른 인증 메커니즘을 요구합니다. 게다가 현대의 웹 애플리케이션은 수백, 수천 개의 엔드포인트를 가지고 있고, 각각에 세밀한 권한 제어가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Security의 핵심 설계 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 이런 복잡성을 해결하기 위해 세 가지 핵심 원칙을 따릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;관심사의 분리&lt;/b&gt;를 통해 보안적인 이슈와 비즈니스 로직을 완전히 분리하여 개발자가 핵심 기능 구현에 집중할 수 있게 해줍니다. 이는 AOP(Aspect Oriented Programming)의 철학과 일맥상통하며, 횡단 관심사인 보안을 별도로 처리함으로써 코드의 가독성과 유지보수성을 크게 향상시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨테이너 무관성&lt;/b&gt;을 보장하여 특정 서블릿 컨테이너에 종속되지 않고 어떤 환경에서도 동작할 수 있도록 설계되었습니다. 서블릿 애플리케이션에서는 Filter 기반으로 동작하지만, 이는 표준 서블릿 API를 활용한 것이므로 Tomcat, Jetty, Undertow 등 어떤 컨테이너를 사용하든 일관된 보안 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 확장성&lt;/b&gt;을 통해 다양한 인증 방법과 사용자 정보 저장 방식을 지원하여 프로젝트의 특성에 맞게 선택적으로 구성할 수 있습니다. 폼 기반 인증, JWT 토큰, OAuth2, LDAP 등 다양한 인증 메커니즘을 동시에 지원할 수 있으며, 사용자 정보를 메모리, 데이터베이스, LDAP 등 다양한 저장소에서 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 개념 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 제대로 이해하기 위해서는 핵심 용어들을 정확히 파악하는 것이 중요합니다. 이 용어들이 전체 아키텍처를 관통하는 개념들이기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Principal&lt;/b&gt;은 인증된 사용자 또는 이를 식별하는 주체를 의미합니다. 사용자 계정뿐만 아니라 디바이스나 시스템도 Principal이 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Secured Resource&lt;/b&gt;는 보안이 적용된 리소스로, URL이나 API 엔드포인트, 파일 등을 포함합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Authentication(인증)&lt;/b&gt;은 Principal이 믿을 수 있는지 파악하는 과정으로, 일반적으로 ID와 비밀번호를 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Authorization(권한 부여)&lt;/b&gt;은 인증이 완료된 Principal이 어떤 행위를 할 권한이 있는지 확인하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처의 핵심 - 두 세계의 만남&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서블릿과 Spring 컨테이너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 복잡한 구조를 이해하려면 웹 애플리케이션에 존재하는 두 개의 서로 다른 컨테이너를 이해해야 합니다. 서블릿 컨테이너는 Java EE 표준에 따라 Filter와 Servlet을 관리하며 웹 요청의 진입점 역할을 합니다. 반면 Spring 컨테이너는 Spring 빈들의 생명주기를 관리하고 의존성 주입, AOP, 트랜잭션 관리 등을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 두 컨테이너가 서로 다른 시점에 초기화되고, 서로 다른 방식으로 객체를 관리한다는 점입니다. 웹 애플리케이션이 시작될 때 먼저 서블릿 컨테이너가 시작되어 web.xml의 Filter들을 초기화하고, 그 다음에 Spring 컨테이너가 시작되어 Spring 빈들을 초기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter는 Spring의 제공 기술이 아니기 때문에 기본적으로는 Spring의 의존성 주입이나 AOP 같은 기능을 활용할 수 없습니다. 하지만 보안 처리를 위해서는 Spring이 제공하는 다양한 기능들이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DelegatingFilterProxy의 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 이 문제를 DelegatingFilterProxy라는 브릿지를 통해 해결합니다. 이 컴포넌트는 서블릿 컨테이너에는 일반적인 Filter로 보이지만, 실제 작업은 Spring 컨테이너의 빈에게 위임합니다.&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;입니다. 만약 Filter 초기화 시점에 바로 Spring 빈을 찾으려고 하면 문제가 됩니다. 아직 Spring 컨테이너가 준비되지 않았을 가능성이 높기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DelegatingFilterProxy는 실제 첫 번째 HTTP 요청이 들어올 때까지 Spring 빈 조회를 지연시킵니다. 첫 번째 요청이 들어오는 시점에는 Spring 컨테이너의 초기화가 완료되어 있을 가능성이 높기 때문입니다. 이런 지연 로딩 전략을 통해 서블릿 표준을 준수하면서도 Spring의 모든 기능을 안전하게 활용할 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902231487&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Spring Boot에서는 자동으로 설정됨
@EnableWebSecurity
public class SecurityConfig {
    // DelegatingFilterProxy가 자동으로 등록되어
    // 실제 보안 처리는 Spring의 FilterChainProxy에게 위임
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SecurityFilterChain의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FilterChainProxy는 DelegatingFilterProxy로부터 작업을 위임받은 Filter로, SecurityFilterChain을 목록으로 관리하며 Security 관련 작업을 위임합니다. 이 컴포넌트가 Spring Security의 실질적인 진입점이 됩니다.&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;SecurityFilterChain은 Security와 관련된 Filter들의 체인으로 구성됩니다. 여기에는 CSRF 방어를 위한 CsrfFilter, 사용자 로그아웃을 처리하는 LogoutFilter, 사용자명과 비밀번호 기반 인증을 담당하는 UsernamePasswordAuthenticationFilter, 자동 로그인 기능을 제공하는 RememberMeAuthenticationFilter, 그리고 권한 검사를 수행하는 AuthorizationFilter 등이 포함됩니다.&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;FilterChainProxy의 가장 강력한 기능 중 하나는 경로 기반으로 다양한 SecurityFilterChain을 보유할 수 있다는 점입니다. 요청을 경로 기반으로 매칭하면서 최초로 적용되는 체인에서 처리합니다. 이를 통해 하나의 애플리케이션 내에서도 API 엔드포인트와 웹 페이지에 서로 다른 보안 정책을 적용할 수 있습니다. 예를 들어, /api/** 경로에는 JWT 기반의 무상태 인증을, /admin/** 경로에는 세션 기반의 폼 인증을 적용하는 것이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902247103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    return http
        .requestMatchers(&quot;/api/**&quot;)
        .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2ResourceServer(oauth2 -&amp;gt; oauth2.jwt(withDefaults()))
        .build();
}

@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
    return http
        .formLogin(withDefaults())
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증(Authentication) 처리 메커니즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authentication 객체의 이중 생명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication 객체는 Spring Security에서 흥미로운 이중 생명을 가집니다. 인증 전에는 사용자가 인증을 위해 AuthenticationManager에게 제공하는 정보로서 Principal과 Credentials로 구성됩니다. 하지만 인증 후에는 현재 인증된 사용자의 정보로서 Principal과 Authorities로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 사용자가 로그인 폼에서 제출한 원시 정보만을 담고 있습니다. 사용자명과 비밀번호 2개만 가지며 이 상태에서는 isAuthenticated()가 false를 반환합니다. 아직 검증되지 않은 상태이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인증이 완료되면 완전히 다른 모습으로 변화합니다. Principal에는 UserDetails 객체가, Authorities에는 권한 목록이 저장됩니다. 그리고 보안상의 이유로 Credentials(비밀번호)는 제거됩니다. 인증이 완료된 후에는 비밀번호를 메모리에 보관할 이유가 없기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902633498&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/login&quot;)
public ResponseEntity&amp;lt;?&amp;gt; login(@RequestBody LoginRequest request) {
    // 인증 전: 사용자가 입력한 username, password
    Authentication authRequest = new UsernamePasswordAuthenticationToken(
        request.getUsername(), 
        request.getPassword()
    );
    
    // Spring Security가 인증 처리
    Authentication authResult = authenticationManager.authenticate(authRequest);
    
    // 인증 후: UserDetails 객체, 권한 목록, password는 제거됨
    SecurityContextHolder.getContext().setAuthentication(authResult);
    
    return ResponseEntity.ok(&quot;Login successful&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthenticationManager와 책임의 연쇄&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationManager는 &quot;누가 이 인증을 처리할 수 있는가?&quot;라는 질문에 답하기 위해 책임 연쇄 패턴을 사용합니다. 기본 구현체인 ProviderManager는 여러 AuthenticationProvider를 순차적으로 확인하여 현재 Authentication을 처리할 수 있는 Provider를 찾습니다.&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;왜 여러 Provider가 필요할까요? 하나의 애플리케이션에서 Form 로그인, JWT 토큰, OAuth2, LDAP 등 다양한 인증 방식을 동시에 지원해야 하는 경우가 많기 때문입니다. 각각은 서로 다른 인증 메커니즘을 담당하며, 이런 방식으로 유연한 확장이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DaoAuthenticationProvider는 UserDetailsService를 이용해서 인증 처리를 수행합니다. 이는 데이터베이스나 다른 저장소에서 사용자 정보를 조회하여 인증하는 가장 일반적인 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SecurityContext와 ThreadLocal의 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증된 사용자 정보를 어디에 저장할 것인가는 웹 애플리케이션 설계에서 정말 중요한 결정입니다. 몇 가지 선택지가 있지만, 각각은 고유한 한계를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 변수를 사용하면 간단하지만, 멀티스레드 환경에서 사용자 정보가 뒤섞이는 심각한 문제가 발생합니다. HTTP 세션을 사용하는 전통적인 방식도 있지만, 세션 관리의 복잡성과 메모리 사용량 증가라는 단점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security가 선택한 답은 ThreadLocal입니다. 웹 요청당 하나의 스레드가 할당되고, 스레드별로 독립적인 저장 공간을 제공하여 요청 간 완전한 격리를 보장합니다. 이를 통해 자동적인 생명주기 관리도 가능해지고&amp;nbsp;어디서든 현재 사용자 정보에 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityContextHolder는 이런 ThreadLocal 저장 방식을 전략 패턴으로 구현했습니다. 기본값인 MODE_THREADLOCAL 외에도, 자식 스레드에 전파하는 MODE_INHERITABLETHREADLOCAL, 단일 사용자용 MODE_GLOBAL 등을 제공합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902296903&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class UserService {
    
    public String getCurrentUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : &quot;anonymous&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserDetailsService의 단순함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService 인터페이스를 보면 정말 단순합니다. 단 하나의 메서드만 정의되어 있습니다. 하지만 이 단순함 속에 중요한 설계 철학이 담겨 있습니다.&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;바로 단일 책임 원칙입니다. UserDetailsService는 오직 사용자 조회만을 담당합니다. 인증 로직이나 비밀번호 검증은 다른 컴포넌트가 처리하죠. 이를 통해 테스트 가능성과 유연성이 크게 증대됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 비밀번호 검증을 하지 않을까요? UserDetailsService는 사용자 정보만 반환하고, 실제 비밀번호 검증은 DaoAuthenticationProvider가 담당합니다. 이렇게 하면 다양한 저장소 지원이 가능하고, 비밀번호 암호화 방식도 독립적으로 관리할 수 있습니다.&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 Security는 인증 실패를 단순히 &quot;실패&quot;로 처리하지 않고 구체적인 원인을 분류합니다. 존재하지 않는 사용자인지, 잘못된 비밀번호인지, 만료된 계정인지, 잠긴 계정인지 등을 구분하여 처리합니다.&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;AuthenticationEntryPoint는 인증이 필요한 리소스에 인증되지 않은 사용자가 접근할 때 인증을 요청하는 역할을 수행합니다. 이는 웹 애플리케이션에서는 로그인 페이지로의 리다이렉트를, API에서는 401 Unauthorized 응답을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationFailureHandler는 로그인을 시도했으나 어떠한 이유로 인증에 실패했을 때 동작합니다. 이를 통해 실패 원인에 따른 차별화된 처리가 가능하며, 사용자에게 적절한 피드백을 제공할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;권한부여(Authorization)의 체계적 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 검사의 기본 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 완료되면 이제 권한 검사 차례입니다. Authorization은 Authentication 객체에 포함된 GrantedAuthority 목록을 사용하여 수행되는 프로세스입니다. GrantedAuthority는 사용자에게 부여된 권한으로 특정 작업을 수행할 수 있는지에 대한 정보를 담고 있으며, 권한은 일반적으로 ROLE_ 접두사로 관리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthorizationManager는 GrantedAuthority를 이용해 Secured Resource에 대한 접근을 제어합니다. 이는 메서드 실행 전 권한 검사를 위한 @PreAuthorize와 메서드 실행 후 결과 반환 가능 여부를 결정하는 @PostAuthorize 등을 통해 구현됩니다.&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 Security에서 권한은 GrantedAuthority 인터페이스로 표현됩니다. 여기서 중요한 것은 역할(Role)과 권한(Authority)의 구분입니다. 역할은 사용자의 전반적 지위를 나타내고, 권한은 특정 작업을 수행할 수 있는 능력을 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의해야 할 함정이 있습니다. hasRole(&quot;USER&quot;)를 사용하면 내부적으로 &quot;ROLE_USER&quot; 권한을 확인하는데, 실수로 hasRole(&quot;ROLE_USER&quot;)라고 작성하면 실제로는 &quot;ROLE_ROLE_USER&quot;를 찾게 되어 의도한 대로 동작하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902355435&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&quot;)        // 올바름: ROLE_ADMIN 검사
@PreAuthorize(&quot;hasRole('ROLE_ADMIN')&quot;)   // 잘못됨: ROLE_ROLE_ADMIN 검사
@PreAuthorize(&quot;hasAuthority('ROLE_ADMIN')&quot;) // 올바름: 직접 권한 검사&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;실제 조직에서는 권한이 계층적입니다. 관리자는 일반 사용자의 모든 권한을 자동으로 가져야 하는 것이 자연스럽죠. RoleHierarchy는 이런 현실적인 조직 구조를 반영하여 권한 관리의 복잡성을 크게 감소시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 role이 다른 role을 자동으로 포함해야 하는 경우, RoleHierarchy를 통해 ADMIN이 STAFF를, STAFF가 USER를 자동으로 포함하도록 설정할 수 있습니다. 내부적으로는 사용자의 직접적인 권한뿐만 아니라 계층 구조를 통해 암시되는 모든 권한을 포함한 확장된 권한 목록을 생성합니다.&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 Security는 다양한 요청 매칭 전략을 제공하여 세밀한 권한 제어를 가능하게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 경로를 지정할 때는 /secured와 같이 명시하면 /secured, /secured/, /secured.html에 모두 대응됩니다. 패턴을 활용한 그룹 지정에서는 /**를 사용하여 모든 하위 경로를, /*를 사용하여 바로 아래 한 단계만 매칭할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 복잡한 패턴도 가능합니다. &lt;span style=&quot;color: #4078f2;&quot;&gt;/&lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span style=&quot;color: #4078f2;&quot;&gt;/&lt;/span&gt;&lt;span&gt;v1&lt;/span&gt;&lt;span style=&quot;color: #a0a1a7;&quot;&gt;/**/&lt;/span&gt;&lt;span style=&quot;color: #4078f2;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;처럼 특정 확장자를 가진 파일만 매칭하거나, /public/?.html처럼 한 글자 이름의 파일만 매칭하는 것도 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902393433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.authorizeHttpRequests(auth -&amp;gt; auth
    .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
    .requestMatchers(&quot;/api/v1/**&quot;).hasRole(&quot;API_USER&quot;)
    .requestMatchers(HttpMethod.DELETE, &quot;/api/**&quot;).hasRole(&quot;ADMIN&quot;)
    .anyRequest().authenticated()
)&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 Security는 위에서부터 순서대로 규칙을 확인하고, 첫 번째로 매치되는 규칙을 적용합니다. 따라서 구체적인 규칙을 먼저, 일반적인 규칙을 나중에 배치해야 합니다.&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;중요한 규칙들을 정리하면, 세부적인 경로를 먼저, 일반적인 경로를 나중에 배치하고, anyRequest()는 반드시 마지막에 위치시켜야 합니다. 이후에는 다른 규칙을 추가할 수 없기 때문입니다. 또한 권한이 높은 것부터 낮은 것 순으로 ADMIN &amp;rarr; USER &amp;rarr; 인증만 필요 &amp;rarr; 모든 접근 허용 순서로 배치하는 것이 좋습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749902414917&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 올바른 순서
.requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
.requestMatchers(&quot;/user/**&quot;).hasRole(&quot;USER&quot;)
.anyRequest().authenticated()&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;하나의 애플리케이션에서 여러 보안 정책을 운영해야 하는 경우가 많습니다. API 엔드포인트는 JWT 기반 무상태 인증을, 웹 페이지는 세션 기반 폼 인증을 사용하는 것처럼 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FilterChainProxy는 @Order 순서대로 SecurityFilterChain을 확인하고, RequestMatcher를 통한 요청 매칭 여부를 확인한 다음, 첫 번째로 매칭되는 체인을 선택합니다. 여기서 순서가 중요한 이유는 구체적인 패턴이 일반적인 패턴보다 먼저 와야 의도한 대로 동작하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요청에 대해서는 URL 패턴 매칭, HTTP 메서드 확인, 필요한 권한과 사용자 권한 비교, 계층적 권한 적용 등의 체계적인 검사가 이루어집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 각 컴포넌트들이 어떤 설계 원리에 따라 만들어졌는지, 그리고 왜 그런 구조를 가지게 되었는지 이해하는 것이 중요합니다. 단순히 설정 방법만 알아서는 복잡한 요구사항을 해결하기 어렵기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿과 Spring의 두 세계를 연결하는 DelegatingFilterProxy의 정교한 지연 로딩 전략, ThreadLocal을 통한 스레드 안전한 보안 컨텍스트 관리, 그리고 Authentication과 Authorization의 체계적인 처리 과정을 이해하기 위해 작성해 보았습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/20</guid>
      <comments>https://leve68.tistory.com/entry/Spring-Security-%ED%95%B5%EC%8B%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8#entry20comment</comments>
      <pubDate>Sat, 14 Jun 2025 21:09:24 +0900</pubDate>
    </item>
    <item>
      <title>Dockerfile</title>
      <link>https://leve68.tistory.com/entry/Dockerfile</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하다 보면 기존에 제공되는 이미지만으로는 우리의 요구사항을 충족하기 어려운 경우가 많습니다. 특정 애플리케이션을 실행하거나, 개발 환경에 맞는 커스텀 설정이 필요할 때가 그런 상황입니다. Dockerfile은 이러한 문제를 해결해주는 강력한 도구로, 우리만의 맞춤형 Docker 이미지를 생성할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dockerfile의 개념과 필요성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 이미지 커스터마이징의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub에서 제공하는 기본 이미지들은 범용적으로 설계되어 있어 특정 프로젝트의 요구사항을 완벽하게 만족하기 어렵습니다. 예를 들어, Node.js 이미지를 다운로드받아도 우리의 애플리케이션 코드는 포함되어 있지 않고, 필요한 의존성 패키지도 설치되어 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 컨테이너를 실행할 때마다 수동으로 파일을 복사하고 패키지를 설치하는 것은 비효율적일 뿐만 아니라 휴먼 에러가 발생할 가능성도 높습니다. 이런 반복적인 작업을 자동화하고 표준화하는 것이 Dockerfile의 핵심 가치입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile이 제공하는 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile은 Docker 이미지를 만들게 해주는 스크립트 파일입니다. 텍스트 파일에 일련의 명령어들을 작성하면, Docker가 이를 순차적으로 실행하여 우리가 원하는 환경이 구성된 이미지를 자동으로 생성해줍니다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;FROM 명령어의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Dockerfile은 FROM 명령어로 시작됩니다. 이 명령어는 우리가 만들고자 하는 이미지의 기반이 될 베이스 이미지를 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄만으로도 OpenJDK 17이 설치된 Linux 환경을 얻을 수 있습니다. FROM 명령어는 특정 초기 이미지를 기반으로 추가적인 설정을 할 수 있게 해주며, 이때 '특정 초기 이미지'가 바로 베이스 이미지입니다.&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그를 생략하면 자동으로 latest 태그가 적용되는데, 이는 예상치 못한 버전 변경으로 인한 호환성 문제를 야기할 수 있습니다. alpine 태그는 필수 기능만을 가지는 경량화된 버전으로, 이미지 크기를 줄이고 보안을 강화하는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 이미지 빌드해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 Dockerfile을 작성해서 이미지를 빌드해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

# 컨테이너가 바로 종료되지 않도록 하는 임시 명령어
ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 빌드는 docker build 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker build -t my-jdk17-server .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 -t 옵션은 태그를 지정하는 것이고, 마지막의 .은 Dockerfile이 존재하는 디렉토리 경로를 의미합니다. 현재 디렉토리에 Dockerfile이 있으므로 .을 사용했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 복사와 애플리케이션 배포&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;COPY 명령어로 파일 전달하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애플리케이션을 배포하려면 호스트 컴퓨터에 있는 소스 코드나 빌드 파일을 컨테이너로 복사해야 합니다. 이때 사용하는 것이 COPY 명령어입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;COPY [호스트 컴퓨터의 파일 경로] [컨테이너 내부 경로]&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;FROM ubuntu

# my-app 디렉토리의 모든 내용을 컨테이너의 /my-app/ 디렉토리에 복사
COPY my-app /my-app/

ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시(/)를 명시하는 것은 &quot;디렉토리로 복사하겠다&quot;는 의도를 명확히 표현하는 방법입니다.&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM ubuntu

# 모든 텍스트 파일을 복사
COPY *.txt /text-files/

ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.dockerignore로 불필요한 파일 제외하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 파일을 복사할 때 특정 파일이나 폴더를 제외하고 싶다면 .dockerignore 파일을 활용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;readme.txt
node_modules
.git&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면 COPY 명령어 실행 시 해당 파일들은 무시됩니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM ubuntu

# 전체 복사 (단, .dockerignore에 명시된 파일들은 제외)
COPY ./ /

ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 실행 명령어 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ENTRYPOINT의 역할과 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENTRYPOINT는 컨테이너가 생성되고 최초로 실행할 때 수행되는 명령어를 지정합니다. 이는 컨테이너의 주 프로세스가 되며, 이 프로세스가 종료되면 컨테이너도 함께 종료됩니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&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;자주 마주치는 문제 중 하나는 컨테이너가 예상보다 빨리 종료되는 현상입니다. 이는 ENTRYPOINT로 지정된 프로세스가 완료되면 컨테이너가 자동으로 종료되기 때문입니다.&lt;/p&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;FROM ubuntu

# hello 출력 후 바로 종료 (logs로 확인 가능)
ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;echo hello&quot;]&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;FROM ubuntu

# 500초 동안 컨테이너 유지
ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 빌드 과정에서의 명령 실행&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RUN 명령어의 특성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RUN 명령어는 이미지 생성 과정에서 필요한 명령어를 실행할 때 사용합니다. ENTRYPOINT와의 가장 큰 차이점은 실행 시점입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM ubuntu

# 이미지 빌드 시점에 git 설치
RUN apt update &amp;amp;&amp;amp; apt install -y git

ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RUN과 ENTRYPOINT의 실행 시점 비교&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;b&gt;RUN과 COPY&lt;/b&gt;: 이미지 생성(빌드) 시 실행되며, 결과는 이미지에 저장됩니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ENTRYPOINT와 CMD&lt;/b&gt;: 이미지를 기반으로 컨테이너를 실행할 때 실행됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 패키지 설치나 환경 설정은 RUN으로, 애플리케이션 실행은 ENTRYPOINT로 처리하는 것이 적절합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;작업 디렉토리 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WORKDIR의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 내부에서 작업할 때 기존 시스템 파일들과 우리의 애플리케이션 파일들이 뒤섞이면 관리가 어려워집니다. WORKDIR 명령어를 사용하면 명확한 작업 공간을 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM ubuntu

# /my-dir을 작업 디렉토리로 설정
WORKDIR /my-dir

# 현재 디렉토리의 모든 파일이 /my-dir 내부로 복사됨
COPY . .

ENTRYPOINT [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;sleep 500&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WORKDIR로 작업 디렉토리를 전환하면 그 이후에 등장하는 모든 RUN, CMD, ENTRYPOINT, COPY, ADD 명령문은 해당 디렉토리를 기준으로 실행됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 문서화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPOSE 명령어는 컨테이너 내부에서 어떤 포트에 프로그램이 실행되는지를 문서화합니다. 실제 포트 매핑은 컨테이너 실행 시점에 결정되지만, EXPOSE를 통해 개발자들에게 어떤 포트를 사용하는지 알려줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너가 3000번 포트를 사용함을 명시
EXPOSE 3000&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 애플리케이션 배포 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot 애플리케이션 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 Docker로 배포하는 과정을 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

# 빌드된 JAR 파일을 컨테이너로 복사
COPY build/libs/*SNAPSHOT.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&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;# Spring Boot 애플리케이션 빌드
./gradlew clean build

# Docker 이미지 빌드
docker build -t hello-server .

# 컨테이너 실행
docker run -d -p 8080:8080 hello-server&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node.js 애플리케이션 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest.js나 Next.js 같은 Node.js 기반 애플리케이션도 유사한 방식으로 배포할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM node:20-alpine

WORKDIR /app

# 소스 코드 복사
COPY . .

# 이미지 빌드 시점에 의존성 설치
RUN npm install

# 애플리케이션 빌드
RUN npm run build

# 포트 정보 문서화
EXPOSE 3000

# 애플리케이션 실행
ENTRYPOINT [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.dockerignore 파일로 불필요한 파일들을 제외합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;node_modules
.git
.next&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;Nginx를 사용한 정적 웹사이트 배포는 매우 간단합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM nginx

# HTML, CSS 파일들을 Nginx 웹 루트로 복사
COPY ./ /usr/share/nginx/html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 파일을 준비하고 이미지를 빌드합니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;My Web Page&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 이미지 빌드
docker build -t my-web-server .

# 컨테이너 실행 (80번 포트로 매핑)
docker run -d -p 80:80 my-web-server&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;Dockerfile을 마스터하면 개발 환경의 일관성을 보장하고, 배포 과정을 자동화할 수 있습니다. FROM으로 시작해서 각종 설정과 파일 복사를 거쳐 ENTRYPOINT로 마무리하는 일련의 과정을 통해, 어떤 종류의 애플리케이션이든 Docker 이미지로 패키징할 수 있습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>docker</category>
      <category>dockerfile</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/19</guid>
      <comments>https://leve68.tistory.com/entry/Dockerfile#entry19comment</comments>
      <pubDate>Fri, 13 Jun 2025 15:02:46 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 개요</title>
      <link>https://leve68.tistory.com/entry/Spring-Security-%EA%B0%9C%EC%9A%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 Java 기반 애플리케이션에서 인증과 권한 부여를 처리하는 강력한 보안 프레임워크입니다. 복잡해 보이는 보안 처리 과정을 체계적으로 관리하여 개발자가 비즈니스 로직에 집중할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Security가 필요한 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안 처리의 복잡성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 웹 애플리케이션에서 보안은 필수 요소입니다. 사용자 인증부터 권한 관리, 각종 보안 공격 방어까지 고려해야 할 요소들이 매우 많습니다. Spring Security는 이러한 복잡한 보안 요구사항을 체계적으로 해결해주는 포괄적인 보안 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 장점은 비즈니스 로직과 보안 로직의 완전한 분리입니다. 개발자는 핵심 비즈니스 기능 구현에 집중할 수 있고, 보안은 Spring Security가 투명하게 처리해줍니다. 또한 다양한 인증 방식과 저장소를 지원하여 프로젝트 요구사항에 맞게 유연하게 구성할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름의 핵심 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (2).jpg&quot; data-origin-width=&quot;4306&quot; data-origin-height=&quot;2700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDSNb/btsOxiYNOSt/K0LbkB8uJKzkMoFwFTs6p1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDSNb/btsOxiYNOSt/K0LbkB8uJKzkMoFwFTs6p1/img.jpg&quot; data-alt=&quot;간단한 Spring Security 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDSNb/btsOxiYNOSt/K0LbkB8uJKzkMoFwFTs6p1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDSNb%2FbtsOxiYNOSt%2FK0LbkB8uJKzkMoFwFTs6p1%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;764&quot; height=&quot;479&quot; data-filename=&quot;Untitled (2).jpg&quot; data-origin-width=&quot;4306&quot; data-origin-height=&quot;2700&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;간단한 Spring Security 흐름&lt;/figcaption&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;Spring Security의 동작은 크게 두 개의 필터 체인으로 구성됩니다. &lt;b&gt;Servlet Filter Chain&lt;/b&gt;은 서블릿 컨테이너 레벨에서 동작하며, 기본적인 요청 전처리와 Spring Security로의 연결을 담당합니다. &lt;b&gt;Security Filter Chain&lt;/b&gt;은 Spring 컨테이너 내에서 동작하며, 실제 인증과 권한 부여 로직을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이중 구조는 서블릿 표준과 Spring 생태계를 효과적으로 연결하면서도, 각각의 장점을 최대한 활용할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: Servlet Filter Chain - 요청의 첫 번째 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter 0: 기본 전처리 작업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 HTTP 요청이 웹 애플리케이션에 도달하면 가장 먼저 &lt;b&gt;Filter 0&lt;/b&gt;을 거치게 됩니다. 이 필터는 요청에 대한 기본적인 전처리 작업을 수행합니다. 요청 인코딩 설정, 로깅을 위한 요청 정보 기록, CORS 헤더 처리 등이 이 단계에서 이루어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DelegatingFilterProxy: 서블릿과 Spring 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 보듯이 &lt;b&gt;DelegatingFilterProxy&lt;/b&gt;는 매우 중요한 위치에 있습니다. 이 컴포넌트는 서블릿 컨테이너의 필터 체인과 Spring Security의 보안 필터 체인을 연결하는 핵심 역할을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 필터는 서블릿 컨테이너에 의해 관리되지만, Spring Security의 보안 로직은 Spring 컨테이너에서 관리되는 빈들을 사용합니다. DelegatingFilterProxy는 이 두 세계를 연결하여 Spring의 모든 기능을 보안 필터에서도 활용할 수 있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@EnableWebSecurity
@Configuration
public class SecurityConfig {
    // Spring Security 설정이 자동으로 DelegatingFilterProxy에 연결됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter 2와 최종 리소스 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingFilterProxy를 거친 요청은 &lt;b&gt;Filter 2&lt;/b&gt; 등의 추가 필터들을 거쳐 최종적으로 &lt;b&gt;Secured Resource&lt;/b&gt;에 도달합니다. 하지만 그 전에 중요한 보안 검사 단계를 거쳐야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: Security Filter Chain - 보안 처리 시작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Security Filter 0: 보안 컨텍스트 초기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingFilterProxy로부터 작업을 위임받은 후, &lt;b&gt;Security Filter 0&lt;/b&gt;에서 보안 처리를 위한 기본적인 준비 작업이 시작됩니다. SecurityContext 초기화, 요청에 대한 보안 관련 메타데이터 설정 등이 이루어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authentication Filter: 인증 정보 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 &lt;b&gt;Authentication Filter&lt;/b&gt;는 사용자 인증의 시작점입니다. 이 필터는 요청에서 인증 정보를 추출하여 Authentication 객체를 생성합니다. 예를 들어, 폼 기반 로그인에서 UsernamePasswordAuthenticationFilter가 사용자가 입력한 아이디와 비밀번호를 추출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 이 필터가 직접 인증을 수행하지 않는다는 것입니다. 대신 생성한 Authentication 객체를 다음 단계로 전달합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authorization Filter: 권한 검사 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Authorization Filter&lt;/b&gt;는 인증이 완료된 후 사용자의 권한을 검사하는 필터입니다. 이 필터는 인증된 사용자가 요청한 리소스에 접근할 권한이 있는지 확인하는 중요한 단계입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: 인증 처리 과정 - Authentication 객체의 검증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authentication Manager: 인증 처리의 중심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림의 오른쪽 부분을 보면, Authentication Filter에서 생성된 Authentication 객체가 &lt;b&gt;Authentication Manager&lt;/b&gt;로 전달되는 것을 볼 수 있습니다. Authentication Manager는 인증 처리의 중심 역할을 하며, 실제 인증 작업을 적절한 구성 요소에게 위임합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Provider Manager: 인증 Provider 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication Manager의 구현체인 &lt;b&gt;Provider Manager&lt;/b&gt;는 여러 AuthenticationProvider들을 관리합니다. 그림에서 보듯이 Provider Manager는 적절한 &lt;b&gt;Authentication Provider&lt;/b&gt;를 선택하여 인증 작업을 위임합니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DaoAuthentication Provider: 실제 인증 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 &lt;b&gt;DaoAuthentication Provider&lt;/b&gt;는 가장 일반적인 인증 방식인 username/password 기반 인증을 처리합니다. 이 Provider는 데이터베이스나 메모리에 저장된 사용자 정보를 이용한 인증을 담당합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 사용자 정보 조회 및 검증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserDetailService: 사용자 정보 조회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 &lt;b&gt;UserDetailService&lt;/b&gt;는 &lt;b&gt;User Database&lt;/b&gt;와 연결되어 있습니다. 이는 실제 사용자 정보를 조회하는 역할을 담당하는 핵심 컴포넌트입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(user.getRoles())
            .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;User Database: 사용자 정보 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;User Database&lt;/b&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;b&gt;인증&lt;/b&gt; 다이아몬드 박스는 인증 처리의 결과를 나타냅니다. 사용자 존재 여부, 비밀번호 일치 여부, 계정 상태 등을 종합적으로 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fail 경로&lt;/b&gt;: 인증에 실패하면 &lt;b&gt;AuthenticationException&lt;/b&gt;이 발생합니다. 존재하지 않는 사용자, 잘못된 비밀번호, 계정 잠김 등의 상황에서 이 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Success 경로&lt;/b&gt;: 모든 검증이 성공하면 새로운 &lt;b&gt;Authentication&lt;/b&gt; 객체가 생성되어 SecurityContext에 저장됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5단계: 권한 검사 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authorization Manager: 권한 검사 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 인증 정보 확인 후 &lt;b&gt;Authorization Filter&lt;/b&gt;에서 &lt;b&gt;Authorization Manager&lt;/b&gt;로 요청이 전달되는 것을 볼 수 있습니다. Authorization Manager는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 단계입니다.&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;b&gt;권한&lt;/b&gt; 다이아몬드 박스에서 최종 접근 가능 여부가 결정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Yes 경로&lt;/b&gt;: 사용자가 필요한 권한을 가지고 있으면 &lt;b&gt;Security Filter n&lt;/b&gt;으로 요청 처리가 계속 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;No 경로&lt;/b&gt;: 권한이 부족하면 &lt;b&gt;AccessDeniedException&lt;/b&gt;이 발생하여 일반적으로 403 Forbidden 응답이 반환됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(authorize -&amp;gt; authorize
            .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
            .requestMatchers(&quot;/user/**&quot;).hasAnyRole(&quot;USER&quot;, &quot;ADMIN&quot;)
            .requestMatchers(&quot;/public/**&quot;).permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .build();
}&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;Spring Security의 인증과 권한 부여 흐름은 각 단계가 명확한 역할을 가지고 유기적으로 연결되어 있습니다. &lt;b&gt;Servlet Filter Chain&lt;/b&gt;에서 &lt;b&gt;Security Filter Chain&lt;/b&gt;으로 이어지는 흐름, 그리고 &lt;b&gt;Authentication Manager&lt;/b&gt;를 중심으로 한 인증 처리 과정, 마지막으로 &lt;b&gt;Authorization Manager&lt;/b&gt;를 통한 권한 검사까지의 전 과정이 체계적으로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컴포넌트가 인터페이스 기반으로 설계되어 있어 프로젝트 요구사항에 맞게 유연하게 커스터마이징할 수 있으며, 복잡한 보안 로직을 직접 구현할 필요 없이 설정을 통해 강력한 보안 기능을 쉽게 적용할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/18</guid>
      <comments>https://leve68.tistory.com/entry/Spring-Security-%EA%B0%9C%EC%9A%94#entry18comment</comments>
      <pubDate>Wed, 11 Jun 2025 18:23:24 +0900</pubDate>
    </item>
    <item>
      <title>REST API</title>
      <link>https://leve68.tistory.com/entry/REST-API</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션 개발에서 프론트엔드와 백엔드를 분리하여 개발하는 것이 일반적인 패턴이 되었습니다. 이러한 구조에서 두 영역을 연결하는 핵심적인 역할을 하는 것이 바로 REST API입니다. Spring Framework를 사용하여 REST API를 구축하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST API가 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 하나의 애플리케이션에서 모든 것을 처리하는 모놀리식 구조가 일반적이었습니다. 하지만 현대 웹 개발에서는 다양한 플랫폼과 클라이언트가 동일한 데이터를 필요로 합니다. 멀티 플랫폼과 멀티 디바이스의 시대에 접어들면서 플랫폼 종속적인 개발 방식의 한계가 드러났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 환경에서 REST API는 플랫폼 독립적인 데이터 통신을 가능하게 합니다. 모든 클라이언트가 HTTP 프로토콜을 통해 동일한 방식으로 서버의 자원에 접근할 수 있게 되어, 개발 효율성과 유지보수성이 크게 향상됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST API의 핵심 개념과 설계 원칙&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST의 구성 요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST(Representational State Transfer)는 2000년 로이 필딩(Roy Fielding)이 제안한 아키텍처 스타일입니다. 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하는 설계 원칙을 가지고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Representational&lt;/b&gt;은 자원의 표현을 의미합니다. 자원의 표현으로 JSON, XML 등의 형식으로 표현되며, 현재는 JSON이 가장 널리 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;State&lt;/b&gt;는 애플리케이션의 상태를 나타냅니다. 클라이언트는 서버의 자원 상태를 요청하고, 서버는 현재 상태를 응답으로 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transfer&lt;/b&gt;는 네트워크를 통해 상태를 전송함을 의미합니다. HTTP 프로토콜을 기반으로 클라이언트와 서버 간에 자원의 상태 정보를 주고받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST API의 주요 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API는 다음과 같은 핵심 특징을 가지고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자원 중심(resource-oriented)&lt;/b&gt;으로 설계됩니다. 모든 것은 자원으로 표현되며 각 자원은 고유한 URI를 갖습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt; 활용을 통해 요청을 위해 HTTP의 GET/POST/PUT/DELETE 등을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자원의 표현&lt;/b&gt;에서 자원은 JSON, XML을 사용합니다. 현재는 JSON이 표준처럼 사용되고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무상태(Stateless)&lt;/b&gt;를 유지하여 각 요청은 이전 요청과 독립적이며 서버는 클라이언트의 상태를 저장하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식과 REST 방식의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 웹 애플리케이션과 REST API의 가장 큰 차이점을 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;URL 설계 방식&lt;/b&gt;에서 기존에는 행위 중심으로 설계했습니다. 예를 들어 /getUserInfo?id=123과 같이 동작을 나타내는 단어를 포함했습니다. 반면 REST에서는 자원 중심으로 /users/123과 같이 명사 형태로 작성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드 사용&lt;/b&gt;에서 기존에는 주로 GET과 POST만 사용했지만, REST에서는 GET, POST, PUT, DELETE 등 HTTP 메서드를 의미에 맞게 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 관리&lt;/b&gt;에서 기존에는 서버에 세션 상태 유지 경향이 있었지만, REST에서는 Stateless(무상태)로 각 요청은 독립적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 형식&lt;/b&gt;에서 기존에는 HTML이 주를 이뤘지만, REST에서는 주로 JSON, XML 등 표준화된 형식을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST 서비스를 위한 URL 작성 권장 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API를 설계할 때는 일관성 있고 직관적인 URL 구조를 만드는 것이 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자원 중심의 URL 설계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자원 중심의 URL&lt;/b&gt; 사용에서 URL은 자원을 나타내며 명사 형태로 작성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt; 사용에서 URL 자체는 자원의 위치를 나타내며 HTTP 메서드로 작업의 의미를 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계층 구조&lt;/b&gt; 반영에서 자원의 관계를 URL에 반영하여 계층 구조를 표현합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목록과 개별 자원&lt;/b&gt;의 구분에서 /members는 전체 목록을, /members/{mno}는 개별 자원을 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 관리&lt;/b&gt;에서 API 버전을 URL에 포함하여 /v1/members, /v2/members와 같이 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소문자 &lt;/b&gt;사용에서 기본적으로 소문자를 사용하고 단어간 구분이 필요할 때는 하이픈(-)을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 스트링&lt;/b&gt; 활용에서 필터링, 정렬 등 추가적인 정보는 쿼리 스트링으로 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST API URL 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 REST API URL의 구체적인 예제입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GET&lt;/b&gt; /api/members - 전체 회원 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET&lt;/b&gt; /api/members?key=email&amp;amp;word=01&amp;amp;page=3 - 필터링된 회원 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET&lt;/b&gt; /api/members/123 - 특정 회원 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;POST&lt;/b&gt; /api/members - 새로운 회원 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT&lt;/b&gt; /api/members/123 - 회원 정보 전체 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PATCH&lt;/b&gt; /api/members/123 - 회원 정보 부분 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE&lt;/b&gt; /api/members/123 - 회원 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring에서 REST API 구현하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@RestController를 활용한 기본 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 REST API를 구현하는 가장 기본적인 방법은 @RestController 어노테이션을 사용하는 것입니다. 이 어노테이션은 @Controller와 @ResponseBody를 결합한 것으로, 모든 메서드의 반환값이 HTTP 응답 본문으로 직접 전송됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/members&quot;)
@RequiredArgsConstructor
public class MemberRestController {
    
    private final BasicMemberService memberService;
    
    @PostMapping
    public Map&amp;lt;String, Object&amp;gt; registMember(@RequestBody Member member) {
        try {
            memberService.registMember(member);
            return Map.of(&quot;status&quot;, &quot;SUCCESS&quot;, &quot;member&quot;, member);
        } catch (DataAccessException e) {
            return Map.of(&quot;status&quot;, &quot;FAIL&quot;, &quot;error&quot;, e.getMessage());
        }
    }
}
&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;REST API에서 클라이언트로부터 데이터를 받는 방법은 크게 세 가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@PathVariable&lt;/b&gt;은 URL 경로의 일부를 변수로 처리할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;/{id}&quot;)
public Member getMember(@PathVariable Long id) {
    return memberService.findById(id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@RequestParam&lt;/b&gt;은 쿼리 파라미터를 처리할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping
public List&amp;lt;Member&amp;gt; getMembers(@RequestParam(required = false) String name) {
    return memberService.findByName(name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@RequestBody&lt;/b&gt;는 HTTP 요청 본문의 JSON 데이터를 자바 객체로 변환할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping
public Member createMember(@RequestBody Member member) {
    return memberService.save(member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ResponseEntity를 활용한 세밀한 응답 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 데이터만 반환하는 것이 아니라 HTTP 상태 코드와 헤더를 함께 제어해야 할 때는 ResponseEntity를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface RestControllerHelper {
    
    default ResponseEntity&amp;lt;?&amp;gt; handleSuccess(Object data) {
        Map&amp;lt;String, Object&amp;gt; response = Map.of(&quot;status&quot;, &quot;SUCCESS&quot;, &quot;data&quot;, data);
        return ResponseEntity.ok(response);
    }
    
    default ResponseEntity&amp;lt;?&amp;gt; handleFail(Exception e) {
        Map&amp;lt;String, Object&amp;gt; response = Map.of(&quot;status&quot;, &quot;FAIL&quot;, &quot;error&quot;, e.getMessage());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}&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;REST API 테스트와 문서화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MockMvc를 활용한 단위 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API의 품질을 보장하기 위해서는 철저한 테스트가 필요합니다. Spring에서는 MockMvc를 사용하여 실제 서버를 구동하지 않고도 컨트롤러를 테스트할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@WebMvcTest(controllers = { MemberRestController.class })
public class MemberRestControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private BasicMemberService memberService;
    
    @Test
    public void registMemberTest() throws Exception {
        Member member = Member.builder().name(&quot;test&quot;).email(&quot;test@email.com&quot;).build();
        
        when(memberService.registMember(member)).thenReturn(1);
        
        mockMvc.perform(post(&quot;/api/v1/members&quot;)
                .contentType(MediaType.APPLICATION_JSON)
                .content(new ObjectMapper().writeValueAsString(member)))
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.status&quot;).value(&quot;SUCCESS&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 형태로 요청을 전송할 때는 서버에서 @RequestBody로 변경해야 테스트가 정상적으로 동작합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swagger를 활용한 API 문서화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API의 사용법을 문서화하는 것은 매우 중요합니다. Swagger는 자동으로 API 문서를 생성하고 관리할 수 있는 강력한 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 의존성을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springdoc&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;springdoc-openapi-starter-webmvc-ui&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.8.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 클래스를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class SwaggerConfig {
    
    @Bean
    public GroupedOpenApi memberOpenApi() {
        String[] paths = { &quot;/api/v1/members/**&quot; };
        return GroupedOpenApi.builder()
                .group(&quot;Member 관련 API&quot;)
                .pathsToMatch(paths)
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에 문서화 정보를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Tag(name = &quot;회원 관리&quot;, description = &quot;회원 관리를 위한 기능 제공&quot;)
@RestController
public class MemberController {
    
    @Operation(summary = &quot;회원 목록 조회&quot;, description = &quot;모든 회원의 정보를 반환합니다&quot;)
    @ApiResponses({
        @ApiResponse(responseCode = &quot;200&quot;, description = &quot;조회 성공&quot;),
        @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 오류&quot;)
    })
    @GetMapping(&quot;/members&quot;)
    public List&amp;lt;Member&amp;gt; getMembers() {
        return memberService.findAll();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RestTemplate을 활용한 클라이언트 프로그래밍&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 다른 REST API를 호출해야 하는 경우가 자주 있습니다. Spring에서는 RestTemplate을 제공하여 HTTP 클라이언트 기능을 쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RestTemplate 설정과 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 호출하는 컨트롤러 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class ExternalApiController {
    
    private final RestTemplate restTemplate;
    
    @Value(&quot;${api.key}&quot;)
    private String apiKey;
    
    @GetMapping(&quot;/external-data&quot;)
    public ResponseEntity getExternalData() {
        try {
            String url = &quot;https://api.example.com/data?key=&quot; + apiKey;
            Map&amp;lt;string, object=&quot;&quot;&amp;gt; result = restTemplate.getForObject(url, Map.class);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of(&quot;error&quot;, e.getMessage()));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CORS 문제 해결하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CORS가 발생하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저의 동일 출처 정책(Same Origin Policy)에 의해, JavaScript에서 다른 도메인의 리소스에 접근할 때 제한이 발생합니다. 예를 들어 localhost:8080에서 실행되는 웹 애플리케이션이 127.0.0.1:8080의 API를 호출하려고 하면 CORS 오류가 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring에서 CORS 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 문제를 해결하는 방법은 크게 두 가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@CrossOrigin 어노테이션&lt;/b&gt;을 사용하여 컨트롤러별로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@CrossOrigin(origins = &quot;http://localhost:3000&quot;)
@RestController
public class MemberController {
    // 컨트롤러 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebMvcConfigurer를 통한 전역 설정&lt;/b&gt;을 사용하면 애플리케이션 전체에 CORS 정책을 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/api/**&quot;)
                .allowedOrigins(&quot;http://localhost:3000&quot;)
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)
                .allowedHeaders(&quot;*&quot;);
    }
}&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;REST API는 현대 웹 애플리케이션 개발에서 없어서는 안 될 핵심 기술입니다. Spring Framework의 강력한 기능들을 활용하면 견고하고 유지보수하기 쉬운 REST API를 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>REST</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/17</guid>
      <comments>https://leve68.tistory.com/entry/REST-API#entry17comment</comments>
      <pubDate>Tue, 10 Jun 2025 12:20:36 +0900</pubDate>
    </item>
    <item>
      <title>Docker Volume</title>
      <link>https://leve68.tistory.com/entry/Docker-Volume</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하여 개발하다 보면 반드시 마주치게 되는 문제가 있습니다. 바로 컨테이너가 삭제되면 내부 데이터도 함께 사라진다는 점입니다. 개발 환경에서 데이터베이스 컨테이너를 재시작할 때마다 기존 데이터가 모두 사라진다면, 매번 초기 설정을 다시 해야 하는 번거로움이 발생합니다. Docker Volume은 이러한 문제를 해결하여 컨테이너의 생명주기와 독립적으로 데이터를 보존할 수 있게 해주는 기능입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 데이터의 근본적 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일시적 데이터 저장소로서의 컨테이너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 컨테이너는 태생적으로 임시적인 특성을 가지고 있습니다. 컨테이너가 실행되는 동안 생성되는 모든 데이터는 컨테이너의 내부 파일 시스템에 저장되는데, 이는 컨테이너와 함께 생성되고 소멸됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 특성은 무상태(stateless) 애플리케이션에서는 장점이 될 수 있지만, 데이터베이스나 파일 저장이 필요한 애플리케이션에서는 큰 제약이 됩니다. 개발 과정에서 컨테이너를 업데이트하거나 재시작해야 할 때마다 데이터가 사라지는 것은 매우 비효율적입니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;Docker Volume의 개념과 작동 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume의 기본 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Volume은 컨테이너에서 데이터를 영속적으로 저장하기 위한 메커니즘입니다. 핵심 아이디어는 컨테이너 자체의 저장 공간을 사용하지 않고, 호스트 시스템의 저장 공간을 공유하여 사용하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 마치 USB 드라이브와 같은 개념으로 이해할 수 있습니다. 컴퓨터가 꺼져도 USB 드라이브의 데이터는 보존되는 것처럼, 컨테이너가 삭제되어도 볼륨에 저장된 데이터는 호스트에 그대로 남아있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume 마운트 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Volume을 사용하기 위한 기본 문법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -v [호스트의 디렉토리 절대경로]:[컨테이너의 디렉토리 절대경로] [이미지명]:[태그명]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문법에서 -v 옵션이 볼륨 마운트를 의미하며, 콜론(:)을 기준으로 왼쪽은 호스트의 디렉토리, 오른쪽은 컨테이너 내부의 디렉토리를 나타냅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume의 동작 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Volume의 동작 방식은 호스트 디렉토리의 존재 여부에 따라 달라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TmCh7/btsOuhroGiq/Tk70UWAqT5xthOiKIkzOCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TmCh7/btsOuhroGiq/Tk70UWAqT5xthOiKIkzOCk/img.png&quot; data-alt=&quot;호스트 디렉토리가 이미 존재하는 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TmCh7/btsOuhroGiq/Tk70UWAqT5xthOiKIkzOCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTmCh7%2FbtsOuhroGiq%2FTk70UWAqT5xthOiKIkzOCk%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;612&quot; height=&quot;344&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호스트 디렉토리가 이미 존재하는 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;1502&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oe96G/btsOsxCuvtk/ej10AjjUTvnpFB0jFvPiSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oe96G/btsOsxCuvtk/ej10AjjUTvnpFB0jFvPiSK/img.png&quot; data-alt=&quot;호스트 디렉토리가 존재하지 않는 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oe96G/btsOsxCuvtk/ej10AjjUTvnpFB0jFvPiSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foe96G%2FbtsOsxCuvtk%2Fej10AjjUTvnpFB0jFvPiSK%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;622&quot; height=&quot;351&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호스트 디렉토리가 존재하지 않는 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;호스트 디렉토리가 존재하지 않는 경우&lt;/b&gt;, Docker가 자동으로 해당 경로에 디렉토리를 생성하고, 컨테이너 내부의 데이터를 호스트로 복사합니다. 이는 새로운 볼륨을 초기화할 때 발생하는 동작입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 활용: MySQL 컨테이너 운영&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume 없이 MySQL 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 일반적인 MySQL 컨테이너 실행 방식을 살펴보겠습니다. MySQL 컨테이너는 루트 패스워드를 환경변수로 설정해야 정상적으로 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 -e 옵션은 컨테이너의 환경변수를 설정하는 데 사용됩니다. MySQL이 정상적으로 실행되면 컨테이너에 접속하여 데이터베이스를 생성해볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# MySQL 컨테이너 접속
docker exec -it [MySQL 컨테이너 ID] bash

# MySQL 클라이언트 실행
mysql -u root -p

# 데이터베이스 생성 및 확인
mysql&amp;gt; create database mydb;
mysql&amp;gt; show databases;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성한 데이터베이스는 컨테이너가 실행되는 동안에만 존재합니다. 컨테이너를 삭제하고 새로 생성하면 이전에 만든 mydb 데이터베이스는 완전히 사라집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/owcuL/btsOttTEoH7/cNCp1xZ1rbKgOUguLmRnKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/owcuL/btsOttTEoH7/cNCp1xZ1rbKgOUguLmRnKK/img.png&quot; data-alt=&quot;MySQL 컨테이너&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/owcuL/btsOttTEoH7/cNCp1xZ1rbKgOUguLmRnKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FowcuL%2FbtsOttTEoH7%2FcNCp1xZ1rbKgOUguLmRnKK%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;555&quot; height=&quot;306&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL 컨테이너&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume을 활용한 데이터 영속화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Volume을 사용하여 MySQL 데이터를 영속적으로 보존하는 방법을 알아보겠습니다. 먼저 호스트에서 데이터를 저장할 디렉토리를 준비합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 볼륨 저장공간으로 사용할 폴더 생성
cd /Users/seungyeon/Documents/Develop
mkdir docker-mysql

# 현재 위치의 절대 경로 확인
pwd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 컨테이너의 데이터 디렉토리는 /var/lib/mysql입니다. 이 경로를 호스트의 디렉토리와 연결하여 데이터를 보존할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 \
  -v /Users/seungyeon/Documents/Develop/docker-mysql/mysql_data:/var/lib/mysql \
  -d mysql&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;# 데이터베이스 생성 후 컨테이너 종료
docker stop [MySQL 컨테이너 ID]
docker rm [MySQL 컨테이너 ID]

# 동일한 볼륨 설정으로 새 컨테이너 생성
docker run -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 \
  -v /Users/seungyeon/Documents/Develop/docker-mysql/mysql_data:/var/lib/mysql \
  -d mysql

# 데이터베이스 확인
mysql&amp;gt; show databases;  # 이전에 생성한 mydb가 그대로 존재&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;1634&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQKx3/btsOtLzL5yx/VKNBNLi5H5kKYObdc2Wkjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQKx3/btsOtLzL5yx/VKNBNLi5H5kKYObdc2Wkjk/img.png&quot; data-alt=&quot;Volume을 활용한 MySQL 컨테이너&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQKx3/btsOtLzL5yx/VKNBNLi5H5kKYObdc2Wkjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQKx3%2FbtsOtLzL5yx%2FVKNBNLi5H5kKYObdc2Wkjk%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;564&quot; height=&quot;307&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Volume을 활용한 MySQL 컨테이너&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다양한 데이터베이스 적용 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL 볼륨 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 역시 동일한 방식으로 볼륨을 설정할 수 있습니다. PostgreSQL의 데이터 디렉토리는 /var/lib/postgresql/data입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cd /Users/seungyeon/Documents/Develop
mkdir docker-postgresql

docker run -e POSTGRES_PASSWORD=root -p 5432:5432 \
  -v /Users/seungyeon/Documents/Develop/docker-postgresql/postgresql_data:/var/lib/postgresql/data \
  -d postgres&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MongoDB 볼륨 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 좀 더 복잡한 환경변수 설정이 필요하지만, 볼륨 사용법은 동일합니다. MongoDB의 데이터 디렉토리는 /data/db입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cd /Users/seungyeon/Documents/Develop
mkdir docker-mongodb

docker run -e MONGO_INITDB_ROOT_USERNAME=root \
  -e MONGO_INITDB_ROOT_PASSWORD=root \
  -p 27017:27017 \
  -v /Users/seungyeon/Documents/Develop/docker-mongodb/mongodb_data:/data/db \
  -d mongo&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Volume 사용 시 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 디렉토리 존재 시 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Volume 사용 시 가장 주의해야 할 점은 호스트의 디렉토리가 이미 존재할 경우입니다. 특히 MySQL에서 이미 mysql_data 폴더가 존재하는 상태에서 새로운 컨테이너를 생성하려고 하면 예기치 못한 오류가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Docker Volume의 동작 원리 때문입니다. 호스트의 디렉토리가 이미 존재할 경우, 해당 디렉토리의 내용이 컨테이너 디렉토리를 덮어씌우게 됩니다. 만약 기존 폴더에 호환되지 않는 데이터가 들어있다면 컨테이너가 정상적으로 시작되지 않을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경변수 변경의 제한사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 MYSQL_ROOT_PASSWORD 값을 변경해서 새로 컨테이너를 실행해도 실제 비밀번호는 변경되지 않습니다. 이는 볼륨 생성 시점에 모든 사용자 정보와 인증 데이터가 저장되기 때문입니다.&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;Volume 설정 시 절대 경로를 정확히 입력하는 것이 중요합니다. 상대 경로를 사용하거나 경로가 잘못되면 의도하지 않은 위치에 데이터가 저장되거나, 볼륨이 제대로 마운트되지 않을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Volume은 컨테이너 기반 개발에서 데이터 영속성을 보장하는 핵심 기술입니다. 특히 데이터베이스와 같이 상태를 유지해야 하는 서비스를 컨테이너로 운영할 때는 필수적인 기능입니다. Volume을 올바르게 활용하면 개발 환경의 일관성을 유지하면서도 데이터 손실 없이 안전하게 컨테이너를 관리할 수 있습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>docker</category>
      <category>Volume</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/16</guid>
      <comments>https://leve68.tistory.com/entry/Docker-Volume#entry16comment</comments>
      <pubDate>Mon, 9 Jun 2025 13:44:28 +0900</pubDate>
    </item>
    <item>
      <title>자주 사용되는 Docker CLI</title>
      <link>https://leve68.tistory.com/entry/%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-Docker-CLI</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 처음 접하는 개발자들이 가장 어려워하는 부분 중 하나가 바로 명령어 사용법입니다. 이번 글에서는 자주 사용되는 Docker 명령어들을 체계적으로 정리하고, 실제 개발 상황에서 어떻게 활용하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker CLI 기본 개념 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker CLI를 효과적으로 사용하려면 먼저 이미지와 컨테이너의 관계를 명확히 이해해야 합니다. 이미지는 애플리케이션 실행에 필요한 모든 것이 패키징된 템플릿이고, 컨테이너는 이 이미지를 기반으로 실제 실행되는 인스턴스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 마치 클래스와 객체의 관계와 비슷합니다. 하나의 이미지로부터 여러 개의 컨테이너를 생성할 수 있으며, 각 컨테이너는 독립적으로 동작합니다. 이러한 구조를 이해하면 Docker 명령어들의 동작 방식을 더 쉽게 파악할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 관리의 핵심 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 다운로드와 버전 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub와 같은 레지스트리에서 이미지를 다운로드할 때는 docker pull 명령어를 사용합니다. 가장 기본적인 형태는 이미지명만 지정하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker pull nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실무에서는 특정 버전을 지정하는 것이 중요합니다. 태그를 명시하지 않으면 자동으로 latest 태그가 적용되는데, 이는 예상치 못한 버전 변경으로 인한 문제를 야기할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker pull nginx:stable-perl
&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;다운로드된 이미지들을 확인하려면 docker image ls 명령어를 사용합니다. 이 명령어의 출력에는 여러 가지 중요한 정보가 포함되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;docker image ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과에서 주목해야 할 항목들은 다음과 같습니다. &lt;b&gt;REPOSITORY&lt;/b&gt;는 이미지의 이름을, &lt;b&gt;TAG&lt;/b&gt;는 버전 정보를 나타냅니다. &lt;b&gt;IMAGE ID&lt;/b&gt;는 해당 이미지의 고유 식별자이며, &lt;b&gt;CREATED&lt;/b&gt;는 이미지가 생성된 날짜를 보여줍니다. 이때 주의할 점은 CREATED가 이미지를 다운로드한 날짜가 아니라 이미지가 빌드된 날짜라는 것입니다.&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;arduino&quot;&gt;&lt;code&gt;docker image rm nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 이미지 이름이나 ID를 지정하여 개별 삭제가 가능합니다. 이미지 ID의 경우 전체를 입력할 필요 없이 앞의 몇 글자만 입력해도 됩니다. 다만 컨테이너에서 사용 중인 이미지는 삭제되지 않으므로, 필요한 경우 강제 삭제 옵션을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;docker image rm -f nginx
&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;docker image rm $(docker images -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 사용하지 않는 모든 이미지를 삭제하며, 강제 삭제가 필요한 경우 -f 옵션을 추가할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 생명주기 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 생성과 실행의 차이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker에서 컨테이너를 다루는 방법은 크게 두 가지로 나뉩니다. 첫 번째는 생성과 실행을 분리하는 방식이고, 두 번째는 한 번에 처리하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성과 실행을 분리하는 경우, 먼저 docker create 명령어로 컨테이너를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;docker create nginx
docker start [컨테이너명 또는 ID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 컨테이너 설정을 미리 준비해두고 필요할 때 실행하고 싶은 경우에 유용합니다. 생성된 모든 컨테이너는 docker ps -a 명령어로 확인할 수 있으며, 현재 실행 중인 컨테이너만 보고 싶다면 docker ps를 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원스텝 실행과 백그라운드 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 일반적으로 사용되는 방식은 docker run 명령어를 통한 원스텝 실행입니다. 이 명령어는 이미지 다운로드, 컨테이너 생성, 실행을 한 번에 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker run nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 docker run은 포그라운드 모드로 실행됩니다. 이는 터미널이 해당 프로세스에 점유되어 다른 작업을 할 수 없다는 의미입니다. 대부분의 서버 애플리케이션은 백그라운드에서 실행되어야 하므로 -d 옵션을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker run -d nginx
&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;실무에서는 컨테이너를 구분하기 쉽게 이름을 지정하는 것이 중요합니다. Docker가 자동으로 생성하는 랜덤한 이름보다는 의미 있는 이름을 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker run -d --name my-web-server nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션의 경우 외부에서 접근할 수 있도록 포트 매핑이 필요합니다. nginx는 기본적으로 80번 포트를 사용하는데, 호스트의 다른 포트로 연결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker run -d -p 4000:80 nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면 호스트의 4000번 포트로 접근할 때 컨테이너의 80번 포트로 연결됩니다. 여러 nginx 컨테이너를 실행할 때 각각 다른 호스트 포트를 사용하면 포트 충돌을 피할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YxDuR/btsOsQOvild/PlujZuR765CBkVXtkVAPK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YxDuR/btsOsQOvild/PlujZuR765CBkVXtkVAPK1/img.png&quot; data-alt=&quot;포트 매핑 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YxDuR/btsOsQOvild/PlujZuR765CBkVXtkVAPK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYxDuR%2FbtsOsQOvild%2FPlujZuR765CBkVXtkVAPK1%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;352&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;포트 매핑 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 모니터링과 디버깅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 확인과 실시간 모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 컨테이너의 상태를 파악하기 위해서는 로그 확인이 필수적입니다. docker logs 명령어는 다양한 옵션을 제공하여 효과적인 디버깅을 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;docker logs [컨테이너 ID 또는 이름]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 로그 확인 외에도 특정 줄 수만 확인하거나 실시간으로 로그를 모니터링할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;docker logs --tail 10 [컨테이너 ID]
docker logs -f [컨테이너 ID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 -f 옵션은 실시간으로 생성되는 로그를 확인할 수 있어 문제 상황을 즉시 파악할 때 유용합니다. 기존 로그는 제외하고 새로 생성되는 로그만 보고 싶다면 --tail 0 -f 옵션을 조합해서 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 내부 접속과 탐색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 컨테이너 내부에 직접 접속하여 파일을 확인하거나 명령어를 실행해야 할 때가 있습니다. docker exec 명령어를 사용하면 실행 중인 컨테이너에 새로운 프로세스를 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker exec -it [컨테이너 ID] bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-i 옵션은 interactive 모드를, -t 옵션은 pseudo-TTY를 할당하여 마치 일반적인 터미널처럼 사용할 수 있게 해줍니다. 컨테이너 내부에서는 일반적인 리눅스 명령어들을 사용할 수 있으며, exit 명령어로 호스트로 돌아올 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너 생명주기 마무리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 종료와 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발이 완료되거나 테스트가 끝나면 컨테이너와 이미지를 정리하는 것이 좋습니다. 컨테이너 종료는 docker stop 명령어를 사용하며, 이는 컨테이너에 종료 신호를 보내고 일정 시간 기다린 후 종료시킵니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;docker stop [컨테이너 ID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급한 상황에서는 docker kill 명령어로 강제 종료할 수 있지만, 데이터 손실의 위험이 있으므로 권장되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료된 컨테이너는 docker rm 명령어로 삭제할 수 있습니다. 실행 중인 컨테이너를 삭제하려면 -f 옵션을 사용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker rm [컨테이너 ID]
docker rm -f [컨테이너 ID]  # 실행 중인 컨테이너 강제 삭제
&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;docker rm $(docker ps -qa)      # 중지된 모든 컨테이너 삭제
docker rm -f $(docker ps -qa)   # 모든 컨테이너 강제 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 활용 사례: Redis 컨테이너 운영&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적인 명령어 학습보다는 실제 서비스를 통해 Docker CLI를 익히는 것이 더 효과적입니다. Redis를 예로 들어 전체 과정을 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker run -d -p 6379:6379 redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 기본적으로 6379번 포트를 사용하므로 호스트의 동일한 포트로 매핑했습니다. 컨테이너가 정상적으로 실행되었다면 Redis 클라이언트로 접속해볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker exec -it [컨테이너 ID] bash
redis-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 내부에서 간단한 명령어를 실행해보면 정상 동작을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; set mykey &quot;Hello Docker&quot;
OK
127.0.0.1:6379&amp;gt; get mykey
&quot;Hello Docker&quot;
&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;Docker CLI는 처음에는 복잡해 보일 수 있지만, 기본적인 명령어들의 패턴을 이해하면 쉽게 익힐 수 있습니다. 이미지 관리, 컨테이너 생명주기, 모니터링과 디버깅으로 나누어 체계적으로 접근하면 Docker를 활용할 수 있습니다.&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>docker</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/15</guid>
      <comments>https://leve68.tistory.com/entry/%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-Docker-CLI#entry15comment</comments>
      <pubDate>Sat, 7 Jun 2025 20:48:36 +0900</pubDate>
    </item>
    <item>
      <title>모던 JavaScript의 핵심 기능</title>
      <link>https://leve68.tistory.com/entry/%EB%AA%A8%EB%8D%98-JavaScript%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript는 ES2015(ES6)를 기점으로 현대적인 프로그래밍 언어로 대폭 발전했습니다. 매년 새로운 기능들이 추가되면서 개발자들의 생산성과 코드 품질을 크게 향상시켰습니다. 이번 글에서는 ES2015부터 ES2020까지의 주요 기능들을 간단한 예제와 함께 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ES2015의 혁신적인 변화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 변수 선언 키워드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES2015는 기존의 var 키워드 외에 let과 const를 도입했습니다. 가장 큰 차이점은 블록 스코프와 호이스팅 방지입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// let: 블록 스코프, 재할당 가능
if (true) {
    let blockVar = &quot;블록 변수&quot;;
    console.log(blockVar); // &quot;블록 변수&quot;
}
console.log(blockVar); // ReferenceError

// const: 블록 스코프, 재할당 불가
const config = { apiUrl: &quot;/api/v1&quot; };
config.apiUrl = &quot;/api/v2&quot;; // 속성 변경은 가능
config = {}; // TypeError: 재할당 불가
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Arrow Function과 this 바인딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arrow Function은 간결한 문법과 함께 lexical this 바인딩을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 기존 함수
const add = function(a, b) { return a + b; };

// 화살표 함수
const addArrow = (a, b) =&amp;gt; a + b;

// this 바인딩 차이점
const person = {
    name: &quot;Alice&quot;,
    
    // 기존 함수 방식
    sayHelloOld: function() {
        setTimeout(function() {
            console.log(this.name); // undefined - this가 전역객체를 가리킴
        }, 100);
    },
    
    // 화살표 함수 방식
    sayHello: function() {
        setTimeout(() =&amp;gt; {
            console.log(this.name); // &quot;Alice&quot; - 상위 스코프의 this 사용
        }, 100);
    }
};

person.sayHelloOld(); // undefined 출력
person.sayHello();    // &quot;Alice&quot; 출력&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;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const language = &quot;javascript&quot;;

const project = {
    language, // language: language와 동일
    sayLang() { // sayLang: function() {}과 동일
        console.log(`사용 언어: ${this.language}`);
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spread Operator의 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열과 객체를 복사하고 합치는 작업이 간단해졌습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 배열 합치기
const frontend = [&quot;React&quot;, &quot;Vue&quot;];
const backend = [&quot;Node&quot;, &quot;Spring&quot;];
const fullstack = [...frontend, ...backend];

// 객체 복사 및 확장
const user = { name: &quot;홍길동&quot;, age: 30 };
const newUser = { ...user, role: &quot;admin&quot; };

// 함수 매개변수
function sum(...numbers) {
    return numbers.reduce((a, b) =&amp;gt; a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Destructuring으로 값 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 데이터에서 필요한 값만 간단히 추출할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 객체 구조분해
const user = { name: &quot;김철수&quot;, job: &quot;개발자&quot;, city: &quot;서울&quot; };
const { name, city } = user;

// 배열 구조분해
const colors = [&quot;red&quot;, &quot;green&quot;, &quot;blue&quot;];
const [primary, secondary] = colors;

// 함수 매개변수에서 활용
function greet({ name, age = 20 }) {
    console.log(`안녕하세요, ${name}님! ${age}살이시군요.`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Template Literal과 기본 매개변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 템플릿과 함수 기본값 설정이 가능해졌습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Template Literal
const name = &quot;홍길동&quot;;
const greeting = `안녕하세요, ${name}님!
오늘은 ${new Date().getFullYear()}년입니다.`;

// 기본 매개변수
function createUser(name, age = 20, role = 'user') {
    return { name, age, role };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Computed&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Property&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;동적으로 객체의 속성 이름을 생성할 수 있게 되었습니다&lt;/span&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749200181077&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 동적 속성 이름 생성
const key = &quot;userName&quot;;
const user = {
    [key]: &quot;홍길동&quot;,
    age: 30
};

console.log(user.userName);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Class 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 프로그래밍을 위한 클래스 문법이 도입되었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    sayHello() {
        return `안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`;
    }
    
    static create(name, age) {
        return new Person(name, age);
    }
}

const person = new Person(&quot;홍길동&quot;, 30);
console.log(person.sayHello());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Promise로 비동기 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 지옥을 해결하는 Promise가 도입되었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Promise 생성
function fetchData() {
    return new Promise((resolve, reject) =&amp;gt; {
        setTimeout(() =&amp;gt; {
            resolve(&quot;데이터 로드 완료&quot;);
        }, 1000);
    });
}

// Promise 사용
fetchData()
    .then(data =&amp;gt; {
        console.log(data); // &quot;데이터 로드 완료&quot;
        return &quot;처리 완료&quot;;
    })
    .then(result =&amp;gt; console.log(result))
    .catch(error =&amp;gt; console.error(error));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Proxy로 메타프로그래밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 기본 동작을 가로채고 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const target = { message: &quot;hello&quot; };

const handler = {
    get(target, prop) {
        return target[prop] + &quot; world&quot;;
    },
    set(target, prop, value) {
        target[prop] = value.toUpperCase();
    }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); // &quot;hello world&quot;
proxy.message = &quot;hi&quot;;
console.log(target.message); // &quot;HI&quot;
&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;JavaScript에 표준 모듈 시스템이 도입되었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// utils.js
export const formatDate = (date) =&amp;gt; date.toLocaleDateString();
export default function logger(message) {
    console.log(`[LOG] ${message}`);
}

// main.js
import logger, { formatDate } from './utils.js';
logger(&quot;애플리케이션 시작&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이후 버전들의 주요 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES2016: 지수 연산자&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// 기존: Math.pow(2, 3)
console.log(2 ** 3); // 8

// 배열 includes 메서드
const fruits = [&quot;apple&quot;, &quot;banana&quot;];
console.log(fruits.includes(&quot;apple&quot;)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES2017: async/await&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise를 더 직관적으로 사용할 수 있게 되었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Promise 방식
function fetchData() {
    return fetch('/api/data')
        .then(response =&amp;gt; response.json())
        .catch(error =&amp;gt; console.error(error));
}

// async/await 방식
async function fetchDataAsync() {
    try {
        const response = await fetch('/api/data');
        return await response.json();
    } catch (error) {
        console.error(error);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES2020: Optional Chaining과 Null Coalescing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩 객체와 null 처리가 훨씬 안전해졌습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const user = { name: &quot;김철수&quot;, email: { domain: &quot;gmail.com&quot; } };

// Optional Chaining
console.log(user.email?.id); // undefined (에러 없음)
console.log(user.email?.domain); // &quot;gmail.com&quot;

// Null Coalescing Operator
const age = 0;
console.log(age || &quot;기본값&quot;); // &quot;기본값&quot; (0도 falsy로 처리)
console.log(age ?? &quot;기본값&quot;); // 0 (null/undefined만 처리)
&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;ES2015부터 ES2020까지의 JavaScript 발전은 개발자의 생산성을 크게 향상시켰습니다. 특히 클래스 문법, Promise, Proxy와 같은 고급 기능들과 함께 비동기 처리, 모듈화, 안전한 데이터 접근 방식들이 언어 차원에서 지원되면서 더 견고한 코드를 작성할 수 있게 되었습니다.&lt;/p&gt;</description>
      <category>FrontEnd/JavaScript</category>
      <category>javascript</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/14</guid>
      <comments>https://leve68.tistory.com/entry/%EB%AA%A8%EB%8D%98-JavaScript%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5#entry14comment</comments>
      <pubDate>Fri, 6 Jun 2025 17:57:28 +0900</pubDate>
    </item>
    <item>
      <title>Docker 기본 개념</title>
      <link>https://leve68.tistory.com/entry/Docker-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현대 소프트웨어 개발에서 애플리케이션을 배포하고 관리하는 것은 복잡한 과제 중 하나입니다. 서로 다른 환경에서 동일한 애플리케이션을 실행하려면 운영체제, 라이브러리, 의존성 등 다양한 요소들을 고려해야 하기 때문입니다. Docker는 이러한 문제를 해결하기 위해 등장한 컨테이너 기반 가상화 기술로, 애플리케이션의 이식성을 크게 향상시켜주는 혁신적인 도구입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker가 필요한 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이식성의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker의 가장 핵심적인 가치는 &lt;b&gt;이식성&lt;/b&gt;에 있습니다. 이식성이란 특정 프로그램을 다른 환경으로 쉽게 옮겨서 설치하고 실행할 수 있는 특성을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 개발 환경에서는 개발자의 로컬 환경, 테스트 서버, 운영 서버가 각각 다른 설정을 가지고 있어 &quot;내 컴퓨터에서는 잘 되는데요&quot;라는 문제가 자주 발생했습니다. Docker는 이러한 환경 차이로 인한 문제를 근본적으로 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 기술을 통해 애플리케이션과 그 실행 환경을 하나의 패키지로 묶어서 어디서든 동일하게 실행할 수 있게 해줍니다. 이는 마치 USB 메모리에 프로그램을 담아서 어느 컴퓨터에서든 동일하게 실행할 수 있는 것과 비슷한 개념입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 기초 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 이해하기 위해서는 먼저 네트워크의 기본 개념인 IP와 Port에 대해 알아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP와 Port의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP 주소&lt;/b&gt;는 네트워크 상에서 특정 컴퓨터를 가리키는 고유한 주소입니다. 이는 현실 세계에서 집의 주소와 같은 역할을 합니다. 인터넷에 연결된 모든 컴퓨터는 고유한 IP 주소를 가지고 있어서 다른 컴퓨터들이 이를 통해 통신할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Port 번호&lt;/b&gt;는 한 컴퓨터 내에서 실행되고 있는 특정 프로그램의 주소를 나타냅니다. 하나의 컴퓨터에서 여러 개의 서비스가 동시에 실행될 수 있는데, 각 서비스는 서로 다른 포트 번호를 사용하여 구분됩니다. 이는 아파트에서 동일한 주소 내에 여러 호수가 있는 것과 비슷합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Well-known Port&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 포트 번호들은 특정 용도로 예약되어 있어 &lt;b&gt;Well-known Port&lt;/b&gt;라고 불립니다. 이러한 표준화를 통해 전 세계의 컴퓨터들이 일관된 방식으로 통신할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예로는 SSH 원격 접속을 위한 22번 포트, 웹 브라우징을 위한 HTTP의 80번 포트, 그리고 보안 웹 통신을 위한 HTTPS의 443번 포트가 있습니다. 브라우저에서 웹사이트에 접속할 때 포트 번호를 별도로 입력하지 않아도 되는 이유는 브라우저가 기본적으로 80번 포트를 사용하도록 설정되어 있기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker와 Container의 핵심 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker의 정의와 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 컨테이너를 사용하여 각각의 프로그램을 분리된 환경에서 실행하고 관리할 수 있는 플랫폼입니다. 이를 통해 개발자들은 애플리케이션의 실행 환경을 표준화하고, 배포 과정을 간소화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Container의 본질&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;b&gt;호스트 컴퓨터&lt;/b&gt;는 컨테이너들을 포함하고 있는 물리적인 컴퓨터를 의미하고, &lt;b&gt;컨테이너&lt;/b&gt;는 그 안에서 실행되는 독립적인 가상 환경을 의미합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Container의 독립성&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;b&gt;디스크 독립성&lt;/b&gt;을 통해 각 컨테이너는 고유의 파일 시스템을 가집니다. 한 컨테이너에서 파일을 생성하거나 수정해도 다른 컨테이너나 호스트 시스템에 영향을 주지 않습니다. 이는 애플리케이션 간의 충돌을 방지하고 보안을 강화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크 독립성&lt;/b&gt;을 통해 각 컨테이너는 자체 네트워크 인터페이스를 가집니다. 컨테이너마다 고유한 IP 주소와 포트 공간을 할당받아, 네트워크 레벨에서도 완전히 분리된 환경을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Image의 개념과 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker &lt;b&gt;Image&lt;/b&gt;는 컨테이너를 생성하기 위한 템플릿 역할을 합니다. 이는 닌텐도 게임기의 카트리지와 같은 개념으로 이해할 수 있습니다. 게임 카트리지에 게임 실행에 필요한 모든 데이터가 들어있듯이, Docker 이미지에는 애플리케이션 실행에 필요한 모든 것이 포함되어 있습니다.&lt;/p&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;Docker는 현대 소프트웨어 개발과 배포에 혁신을 가져온 기술입니다. 컨테이너를 통한 애플리케이션의 완전한 독립성과 이식성 보장은 개발자들이 환경 차이로 인한 문제에서 벗어날 수 있게 해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 Docker에 대해 학습하면서, 관련 내용들을 차례대로 정리해 나갈 예정입니다. 이번 글을 포함해 Docker 카테고리의 포스트들은 다음 강의를 수강하며 학습한 내용을 정리한 것입니다.&lt;/p&gt;
&lt;figure id=&quot;og_1749116487550&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;비전공자도 이해할 수 있는 Docker 입문/실전 강의 | JSCODE 박재성 - 인프런&quot; data-og-description=&quot;JSCODE 박재성 | ,   에라이, 못 해먹겠네!비전공자로 개발을 시작해 여러 회사에서 CTO로 활동하다가, 현재는 교육자로 활동하고 있는 박재성이라고 합니다. 저도 비전공자로 개발을 시작해 서버&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/o6kMk/hyY37rxoZw/0SkNIEK6bP712bRkLNBsK1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bRXthS/hyY32X4okQ/ush1v4VN7qv0QWj5nhxj91/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cy57jW/hyY1k6ZfQK/G2i17CteEcu1sGUgbagHXk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/o6kMk/hyY37rxoZw/0SkNIEK6bP712bRkLNBsK1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bRXthS/hyY32X4okQ/ush1v4VN7qv0QWj5nhxj91/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cy57jW/hyY1k6ZfQK/G2i17CteEcu1sGUgbagHXk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;비전공자도 이해할 수 있는 Docker 입문/실전 강의 | JSCODE 박재성 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSCODE 박재성 | ,   에라이, 못 해먹겠네!비전공자로 개발을 시작해 여러 회사에서 CTO로 활동하다가, 현재는 교육자로 활동하고 있는 박재성이라고 합니다. 저도 비전공자로 개발을 시작해 서버&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/Docker</category>
      <category>docker</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/13</guid>
      <comments>https://leve68.tistory.com/entry/Docker-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90#entry13comment</comments>
      <pubDate>Thu, 5 Jun 2025 18:47:00 +0900</pubDate>
    </item>
    <item>
      <title>Spring의 트랜잭션 관리</title>
      <link>https://leve68.tistory.com/entry/Spring%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework에서 제공하는 트랜잭션 관리 기능, 특히 @Transactional 어노테이션에 대해 살펴보겠습니다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;업무 로직 분리&lt;/b&gt;: 비즈니스 로직을 DAO와 분리하여 관심사를 명확히 구분&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 통합 처리&lt;/b&gt;: 여러 DAO의 작업을 하나의 논리적 단위로 묶어 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 일관성 보장&lt;/b&gt;: 트랜잭션을 통해 여러 데이터 변경 작업이 모두 성공하거나 모두 실패하도록 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 재사용성 향상&lt;/b&gt;: 공통 비즈니스 로직을 여러 컨트롤러에서 재사용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 JDBC 기반 트랜잭션 관리는 다음과 같은 문제가 있었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public int registMember(Member member) throws SQLException {
    Connection con = util.getConnection();
    try {
        con.setAutoCommit(false);
        int result = mDao.insert(con, member);
        con.commit();
        return result;
    } catch (SQLException e) {
        con.rollback();
        throw e;
    } finally {
        util.close(con);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring의 트랜잭션 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 이러한 문제를 해결하기 위해 트랜잭션 관리를 추상화하는 PlatformTransactionManager 인터페이스를 제공합니다. 이를 통해 다양한 트랜잭션 API(JDBC, JPA 등)를 일관된 방식으로 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 프로젝트에 포함된 의존성을 기반으로 적절한 트랜잭션 매니저를 자동으로 구성해줍니다. 예를 들어, MyBatis 스타터를 사용하면 DataSourceTransactionManager가 자동으로 등록됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선언적 트랜잭션 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 가장 큰 특징 중 하나는 선언적 트랜잭션 관리입니다. @Transactional 어노테이션을 사용하면 AOP를 기반으로 트랜잭션 관리 코드가 자동으로 주입됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
    
    private final MemberDao memberDao;
    private final AddressDao addressDao;
    
    @Transactional
    @Override
    public void registerMemberWithAddress(Member member, Address address) {
        memberDao.insert(member);
        address.setMno(member.getMno());
        addressDao.insert(address);
        // 예외 발생 시 자동으로 롤백됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 이전 JDBC 기반 코드와 비교해 매우 간결합니다. 트랜잭션 관리 코드는 모두 제거되고 비즈니스 로직만 남습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional 어노테이션의 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션은 AOP를 기반으로 동작합니다. 어노테이션이 적용된 메서드가 호출되면 다음 절차를 따릅니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스프링의 AOP 프록시가 이 호출을 가로챕니다.&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;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional의 주요 속성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. propagation (전파 속성)&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;b&gt;REQUIRED (기본값)&lt;/b&gt;: 현재 트랜잭션이 있으면 참여하고, 없으면 새로 시작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;: 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션은 일시 중단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NESTED&lt;/b&gt;: 현재 트랜잭션이 있으면 중첩 트랜잭션을 시작, 없으면 REQUIRED와 동일하게 동작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SUPPORTS&lt;/b&gt;: 현재 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NOT_SUPPORTED&lt;/b&gt;: 트랜잭션 없이 실행하고, 현재 트랜잭션이 있으면 일시 중단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NEVER&lt;/b&gt;: 트랜잭션 없이 실행하고, 현재 트랜잭션이 있으면 예외를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MANDATORY&lt;/b&gt;: 현재 트랜잭션이 있어야 실행하고, 없으면 예외를 발생시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Payment payment) {
    // 항상 새로운 트랜잭션에서 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. isolation (격리 수준)&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;b&gt;DEFAULT&lt;/b&gt;: 데이터베이스 기본 격리 수준 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;READ_UNCOMMITTED&lt;/b&gt;: 커밋되지 않은 데이터도 읽을 수 있음 (Dirty Read)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;READ_COMMITTED&lt;/b&gt;: 커밋된 데이터만 읽을 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REPEATABLE_READ&lt;/b&gt;: 같은 데이터를 여러 번 읽어도 동일한 결과 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SERIALIZABLE&lt;/b&gt;: 가장 높은 격리 수준, 완전한 데이터 일관성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(isolation = Isolation.READ_COMMITTED)
public Member getMember(Long id) {
    return memberRepository.findById(id).orElse(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. timeout&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 최대 실행 시간을 초 단위로 설정합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Transactional(timeout = 30) // 30초
public void longRunningOperation() {
    // 시간이 오래 걸리는 작업
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. readOnly&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 읽기 전용으로 설정합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public List&amp;lt;Member&amp;gt; getAllMembers() {
    return memberRepository.findAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 전용 트랜잭션은 데이터베이스 최적화의 기회를 제공하며, 특히 조회 작업에 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. rollbackFor와 noRollbackFor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring은 런타임 예외만 롤백하고 체크 예외는 롤백하지 않습니다. 이 설정을 변경할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = {SQLException.class, IOException.class})
public void methodWithCustomRollback() throws SQLException, IOException {
    // 지정된 체크 예외가 발생해도 롤백됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 관리의 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 자기 호출 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 클래스 내에서 @Transactional 메서드를 호출할 경우 트랜잭션이 적용되지 않습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class SelfInvocationService {
    
    public void outerMethod() {
        // 트랜잭션 없음
        this.innerMethod(); // 자기 호출 - 트랜잭션 적용 안됨
    }
    
    @Transactional
    public void innerMethod() {
        // 트랜잭션이 시작되지 않음
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책: 별도의 빈으로 분리하거나, 자기 자신을 주입받아 프록시를 통해 호출&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. private 메서드에 적용 불가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional은 public 메서드에만 적용됩니다. private, protected, package-private 메서드에는 적용되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 롤백 규칙 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 런타임 예외만 롤백되고 체크 예외는 롤백되지 않습니다. 필요에 따라 rollbackFor 속성을 사용해야 합니다.&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class ProgrammaticTxService {
    
    private final TransactionTemplate transactionTemplate;
    private final MemberRepository memberRepository;
    
    public ProgrammaticTxService(PlatformTransactionManager transactionManager, MemberRepository memberRepository) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.memberRepository = memberRepository;
    }
    
    public void updateMemberWithCustomTx(Member member) {
        transactionTemplate.execute(status -&amp;gt; {
            memberRepository.update(member);
            return null; // 성공 시 반환값
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 트랜잭션 관리 기능은 개발자가 비즈니스 로직에 집중할 수 있도록 많은 보일러플레이트 코드를 제거해줍니다. @Transactional 어노테이션을 사용한 선언적 트랜잭션 관리는 코드를 간결하게 유지하면서도 강력한 트랜잭션 기능을 제공합니다.&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;다양한 트랜잭션 API에 대한 일관된 프로그래밍 모델&lt;/li&gt;
&lt;li&gt;선언적 트랜잭션 관리로 코드 간결화&lt;/li&gt;
&lt;li&gt;AOP를 통한 비즈니스 로직과 트랜잭션 관리 로직 분리&lt;/li&gt;
&lt;li&gt;다양한 속성을 통한 세밀한 트랜잭션 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring과 MyBatis를 함께 사용할 때 @Transactional을 활용하면, 데이터 일관성을 유지하면서도 개발 생산성을 크게 향상시킬 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <category>transactional</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/12</guid>
      <comments>https://leve68.tistory.com/entry/Spring%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EB%A6%AC#entry12comment</comments>
      <pubDate>Fri, 30 May 2025 11:53:02 +0900</pubDate>
    </item>
    <item>
      <title>MyBatis의 동적 SQL</title>
      <link>https://leve68.tistory.com/entry/MyBatis%EC%9D%98-%EB%8F%99%EC%A0%81-SQL</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis의 가장 강력한 기능 중 하나인 동적 SQL에 대해 자세히 살펴보겠습니다. 동적 SQL을 사용하면 조건에 따라 다양하게 변하는 쿼리를 효과적으로 작성할 수 있어, 복잡한 검색 기능이나 조건부 데이터 처리에 큰 도움이 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동적 SQL의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애플리케이션 개발에서는 단순한 CRUD 쿼리만으로는 충분하지 않은 경우가 많습니다. 사용자의 검색 조건에 따라 WHERE 절이 달라지거나, 선택적으로 JOIN이 필요하거나, 테이블 이름이나 정렬 조건이 동적으로 변해야 하는 경우가 자주 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 전통적인 방법으로는 문자열 연결이나 조건문을 통해 SQL을 동적으로 구성해야 했습니다. MyBatis에서도 기본적으로 두 가지 방식으로 파라미터를 처리할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;#{}&lt;/b&gt;: PreparedStatement의 파라미터('?')를 대체하는 안전한 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;${}&lt;/b&gt;: 문자열 자체를 치환하는 방식 (SQL 인젝션에 취약할 수 있음)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이러한 간단한 치환만으로는 복잡한 동적 쿼리를 구성하기 어렵습니다. 이 문제를 해결하기 위해 MyBatis는 XML 기반의 다양한 동적 SQL 태그를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동적 SQL 주요 태그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 조건에 따라 SQL을 구성할 수 있는 다양한 태그를 제공합니다. 각 태그의 사용법과 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. if 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 조건 처리 태그로, 조건이 참일 때만 포함할 SQL 조각을 지정합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findAddresses&quot; resultMap=&quot;addressMap&quot;&amp;gt;
    SELECT * FROM address
    &amp;lt;if test=&quot;title != null&quot;&amp;gt;
        WHERE title = #{title}
    &amp;lt;/if&amp;gt;
    &amp;lt;if test=&quot;mno != null&quot;&amp;gt;
        AND mno = #{mno}
    &amp;lt;/if&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;title이 null이 아니면 &quot;WHERE title = ?&quot; 조건이 추가됩니다.&lt;/li&gt;
&lt;li&gt;mno가 null이 아니면 &quot;AND mno = ?&quot; 조건이 추가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 코드에는 문제가 있습니다. title이 null이면 첫 번째 if 블록이 생략되므로, 두 번째 조건은 &quot;AND&quot;로 시작하게 되어 SQL 문법 오류가 발생할 수 있습니다. 또한 두 조건이 모두 null이면 WHERE 절 자체가 없어져야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. where 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 if 태그의 문제를 해결하기 위해 MyBatis는 where 태그를 제공합니다. where 태그는 다음과 같은 기능을 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내부 조건이 하나라도 충족되면 WHERE 절을 추가합니다.&lt;/li&gt;
&lt;li&gt;조건의 시작 부분에 AND나 OR가 있으면 자동으로 제거합니다.&lt;/li&gt;
&lt;li&gt;조건이 하나도 없으면 WHERE 절 자체를 추가하지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findAddresses&quot; resultMap=&quot;addressMap&quot;&amp;gt;
    SELECT * FROM address
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;title != null&quot;&amp;gt;
            AND title = #{title}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;mno != null&quot;&amp;gt;
            AND mno = #{mno}
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두 조건이 모두 null이면 WHERE 절이 없는 쿼리가 생성되고, 둘 중 하나만 만족해도 적절한 WHERE 절이 생성됩니다. 첫 번째 조건의 AND는 자동으로 제거됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. choose, when, otherwise 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 조건 중 하나만 선택해야 할 때는 choose, when, otherwise 태그를 사용합니다. 이는 Java의 switch 문과 유사하게 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findAddresses&quot; resultMap=&quot;addressMap&quot;&amp;gt;
    SELECT * FROM address
    &amp;lt;where&amp;gt;
        &amp;lt;choose&amp;gt;
            &amp;lt;when test=&quot;title != null&quot;&amp;gt;
                title = #{title}
            &amp;lt;/when&amp;gt;
            &amp;lt;when test=&quot;mno != null&quot;&amp;gt;
                mno = #{mno}
            &amp;lt;/when&amp;gt;
            &amp;lt;otherwise&amp;gt;
                is_default = true
            &amp;lt;/otherwise&amp;gt;
        &amp;lt;/choose&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;title이 null이 아니면 &quot;WHERE title = ?&quot; 조건을 사용합니다.&lt;/li&gt;
&lt;li&gt;그렇지 않고 mno가 null이 아니면 &quot;WHERE mno = ?&quot; 조건을 사용합니다.&lt;/li&gt;
&lt;li&gt;두 조건이 모두 null이면 &quot;WHERE is_default = true&quot; 조건을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. trim 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 태그보다 더 세밀한 제어가 필요한 경우 trim 태그를 사용할 수 있습니다. trim 태그는 내부 내용의 앞뒤에 특정 문자열을 추가하거나 제거할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findAddresses&quot; resultMap=&quot;addressMap&quot;&amp;gt;
    SELECT * FROM address
    &amp;lt;trim prefix=&quot;WHERE&quot; prefixOverrides=&quot;AND|OR&quot;&amp;gt;
        &amp;lt;if test=&quot;title != null&quot;&amp;gt;
            AND title = #{title}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;mno != null&quot;&amp;gt;
            AND mno = #{mno}
        &amp;lt;/if&amp;gt;
    &amp;lt;/trim&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trim 태그의 주요 속성은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;prefix&lt;/b&gt;: 내용이 있을 경우 앞에 추가할 문자열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;prefixOverrides&lt;/b&gt;: 내용 맨 앞에 있는 지정된 문자열 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;suffix&lt;/b&gt;: 내용이 있을 경우 뒤에 추가할 문자열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;suffixOverrides&lt;/b&gt;: 내용 맨 뒤에 있는 지정된 문자열 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. foreach 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션을 반복 처리할 때는 foreach 태그를 사용합니다. 주로 IN 절을 구성할 때 유용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findAddressesByTitles&quot; resultMap=&quot;addressMap&quot;&amp;gt;
    SELECT * FROM address
    &amp;lt;where&amp;gt;
        &amp;lt;foreach collection=&quot;titles&quot; item=&quot;title&quot; open=&quot;title IN (&quot; separator=&quot;, &quot; close=&quot;)&quot;&amp;gt;
            #{title}
        &amp;lt;/foreach&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 제목 목록(titles)을 받아 해당 제목들 중 하나와 일치하는 주소를 모두 찾습니다. foreach 태그의 주요 속성은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;collection&lt;/b&gt;: 반복할 항목들의 컬렉션(배열, 리스트, 맵 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;item&lt;/b&gt;: 각 반복에서 현재 항목을 참조할 변수명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;index&lt;/b&gt;: 현재 항목의 인덱스를 저장할 변수명 (선택 사항)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;open&lt;/b&gt;: 반복 시작 전에 추가할 문자열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;close&lt;/b&gt;: 반복 종료 후에 추가할 문자열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;separator&lt;/b&gt;: 항목 사이에 추가할 구분자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;foreach는 배열뿐만 아니라 맵을 반복 처리할 때도 사용할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;insert id=&quot;insertMembers&quot;&amp;gt;
    INSERT INTO member (name, email, password) VALUES
    &amp;lt;foreach collection=&quot;members&quot; item=&quot;member&quot; separator=&quot;,&quot;&amp;gt;
        (#{member.name}, #{member.email}, #{member.password})
    &amp;lt;/foreach&amp;gt;
&amp;lt;/insert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 여러 회원을 한 번에 추가하는 배치 삽입 쿼리를 생성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. set 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE 문에서 SET 절을 동적으로 구성할 때는 set 태그를 사용합니다. set 태그는 내부 내용이 있을 경우 SET 절을 추가하고, 각 업데이트 구문 뒤의 쉼표를 적절히 처리합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;update id=&quot;updateAddress&quot;&amp;gt;
    UPDATE address
    &amp;lt;set&amp;gt;
        &amp;lt;if test=&quot;mno != null&quot;&amp;gt;mno = #{mno},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;title != null&quot;&amp;gt;title = #{title},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;address != null&quot;&amp;gt;address = #{address},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;detailAddress != null&quot;&amp;gt;detail_address = #{detailAddress},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;x != null&quot;&amp;gt;x = #{x},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;y != null&quot;&amp;gt;y = #{y},&amp;lt;/if&amp;gt;
    &amp;lt;/set&amp;gt;
    WHERE ano = #{ano}
&amp;lt;/update&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 null이 아닌 필드만 업데이트하고, 마지막 필드 뒤의 쉼표를 자동으로 제거합니다. 모든 필드가 null이면 SET 절 자체가 생성되지 않아 SQL 오류가 발생할 수 있으므로, 실제로는 적어도 하나의 필드가 업데이트되도록 검증 로직이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OGNL 표현식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis의 동적 SQL 태그 내에서는 OGNL(Object Graph Navigation Language)이라는 표현식 언어를 사용합니다. OGNL을 통해 객체의 속성에 접근하고 다양한 연산을 수행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;속성 접근&lt;/b&gt;: person.name, employee.department.name&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컬렉션/맵 요소 접근&lt;/b&gt;: list[0], map['key'] 또는 map.key&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드 호출&lt;/b&gt;: person.getName(), list.size()&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연산자&lt;/b&gt;: +, -, *, /, %, ==, !=, &amp;lt;, &amp;gt;, &amp;lt;=, &amp;gt;=, &amp;amp;&amp;amp;, ||, !&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기타 표현식&lt;/b&gt;: 3 + 4, 'hello' + ' world', list.isEmpty() ? 'empty' : 'not empty'&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OGNL 사용 예제&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;!-- 문자열 비교 --&amp;gt;
&amp;lt;if test=&quot;status != null and status.equals('ACTIVE')&quot;&amp;gt;
    AND status = 'ACTIVE'
&amp;lt;/if&amp;gt;

&amp;lt;!-- 숫자 비교 --&amp;gt;
&amp;lt;if test=&quot;age != null and age &amp;gt;= 18&quot;&amp;gt;
    AND age &amp;gt;= 18
&amp;lt;/if&amp;gt;

&amp;lt;!-- 컬렉션 관련 --&amp;gt;
&amp;lt;if test=&quot;roles != null and !roles.isEmpty()&quot;&amp;gt;
    AND role IN
    &amp;lt;foreach collection=&quot;roles&quot; item=&quot;role&quot; open=&quot;(&quot; separator=&quot;,&quot; close=&quot;)&quot;&amp;gt;
        #{role}
    &amp;lt;/foreach&amp;gt;
&amp;lt;/if&amp;gt;

&amp;lt;!-- 삼항 연산자 --&amp;gt;
&amp;lt;if test=&quot;sortField != null&quot;&amp;gt;
    ORDER BY ${sortDirection != null ? sortField + ' ' + sortDirection : sortField}
&amp;lt;/if&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 다중 조건 검색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 검색 기능에서 이름, 이메일, 역할, 가입 날짜 등 여러 조건으로 검색할 수 있는 기능을 구현해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;searchMembers&quot; parameterType=&quot;MemberSearchCondition&quot; resultType=&quot;Member&quot;&amp;gt;
    SELECT * FROM member
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;name != null and name != ''&quot;&amp;gt;
            AND name LIKE CONCAT('%', #{name}, '%')
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;email != null and email != ''&quot;&amp;gt;
            AND email LIKE CONCAT('%', #{email}, '%')
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;role != null&quot;&amp;gt;
            AND role = #{role}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;startDate != null&quot;&amp;gt;
            AND register_date &amp;gt;= #{startDate}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;endDate != null&quot;&amp;gt;
            AND register_date &amp;lt;= #{endDate}
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
    &amp;lt;if test=&quot;sortField != null&quot;&amp;gt;
        ORDER BY ${sortField} ${sortDirection == null ? 'ASC' : sortDirection}
    &amp;lt;/if&amp;gt;
    &amp;lt;if test=&quot;limit != null and offset != null&quot;&amp;gt;
        LIMIT #{limit} OFFSET #{offset}
    &amp;lt;/if&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 MemberSearchCondition 객체를 통해 여러 검색 조건을 받아들이고, 조건이 있는 경우에만 해당 WHERE 절을 추가합니다. 또한 정렬 조건과 페이징 처리도 동적으로 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO 인터페이스는 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public interface MemberDao {
    List&amp;lt;Member&amp;gt; searchMembers(MemberSearchCondition condition);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberSearchCondition 클래스:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;lasso&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Data
public class MemberSearchCondition {
    private String name;
    private String email;
    private String role;
    private LocalDate startDate;
    private LocalDate endDate;
    private String sortField;
    private String sortDirection;
    private Integer limit;
    private Integer offset;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 동적 UPDATE 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보 수정 기능에서 사용자가 변경한 필드만 업데이트하는 기능을 구현해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;update id=&quot;updateMember&quot; parameterType=&quot;Member&quot;&amp;gt;
    UPDATE member
    &amp;lt;set&amp;gt;
        &amp;lt;if test=&quot;name != null&quot;&amp;gt;name = #{name},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;email != null&quot;&amp;gt;email = #{email},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;password != null&quot;&amp;gt;password = #{password},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;role != null&quot;&amp;gt;role = #{role},&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;status != null&quot;&amp;gt;status = #{status},&amp;lt;/if&amp;gt;
        updated_at = NOW()
    &amp;lt;/set&amp;gt;
    WHERE mno = #{mno}
&amp;lt;/update&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 null이 아닌 필드만 업데이트하고, 항상 updated_at 필드는 현재 시간으로 업데이트합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 동적 테이블 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경우에 따라 테이블 이름을 동적으로 변경해야 할 때는 ${} 문법을 사용할 수 있습니다. 하지만 SQL 인젝션 위험이 있으므로 주의해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findFromTable&quot; resultType=&quot;map&quot;&amp;gt;
    SELECT * FROM ${tableName}
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;id != null&quot;&amp;gt;
            id = #{id}
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 테이블 이름을 파라미터로 받아 동적으로 쿼리를 생성합니다. 이런 방식은 반드시 안전한 값만 전달되도록 검증이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. IN 절을 이용한 다중 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 선택한 여러 항목에 대해 IN 절을 사용하여 조회하는 기능을 구현해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findMembersByIds&quot; resultType=&quot;Member&quot;&amp;gt;
    SELECT * FROM member
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;ids != null and !ids.isEmpty()&quot;&amp;gt;
            mno IN
            &amp;lt;foreach collection=&quot;ids&quot; item=&quot;id&quot; open=&quot;(&quot; separator=&quot;,&quot; close=&quot;)&quot;&amp;gt;
                #{id}
            &amp;lt;/foreach&amp;gt;
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 회원 ID 목록을 받아 해당 ID를 가진 모든 회원을 조회합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 동적 JOIN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요에 따라 JOIN을 추가하거나 제외하는 쿼리를 작성해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;findMembersWithDetails&quot; resultMap=&quot;memberDetailMap&quot;&amp;gt;
    SELECT m.*, 
        &amp;lt;if test=&quot;includeProfile&quot;&amp;gt;p.profile_id, p.picture_url, p.bio,&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;includeAddresses&quot;&amp;gt;a.ano, a.title, a.address, a.detail_address,&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;includeOrders&quot;&amp;gt;o.order_id, o.order_date, o.total_amount&amp;lt;/if&amp;gt;
    FROM member m
    &amp;lt;if test=&quot;includeProfile&quot;&amp;gt;
        LEFT JOIN profile p ON m.mno = p.mno
    &amp;lt;/if&amp;gt;
    &amp;lt;if test=&quot;includeAddresses&quot;&amp;gt;
        LEFT JOIN address a ON m.mno = a.mno
    &amp;lt;/if&amp;gt;
    &amp;lt;if test=&quot;includeOrders&quot;&amp;gt;
        LEFT JOIN orders o ON m.mno = o.mno
    &amp;lt;/if&amp;gt;
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;name != null&quot;&amp;gt;
            AND m.name LIKE CONCAT('%', #{name}, '%')
        &amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 필요에 따라 회원 정보와 함께 프로필, 주소, 주문 정보를 함께 조회할 수 있습니다. 파라미터로 전달된 플래그에 따라 JOIN이 동적으로 추가됩니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1. SQL 인젝션 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#{}를 사용하여 파라미터를 바인딩하면 PreparedStatement의 파라미터로 처리되므로 SQL 인젝션이 방지됩니다. 반면 ${}는 문자열 그대로 치환되어 SQL 인젝션에 취약할 수 있습니다. 따라서 동적 테이블 이름, 컬럼 이름, ORDER BY 절 등에만 제한적으로 사용하고, 사용자 입력에는 절대 사용하지 않아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 가독성과 유지보수성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 동적 SQL은 가독성이 떨어질 수 있습니다. 다음과 같은 방법으로 개선할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 조각을 재사용 가능한 &amp;lt;sql&amp;gt; 태그로 분리&lt;/li&gt;
&lt;li&gt;복잡한 조건은 Java 코드에서 미리 처리&lt;/li&gt;
&lt;li&gt;주석을 통해 의도 명확하게 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &amp;lt;sql&amp;gt; 태그를 활용한 재사용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;sql id=&quot;baseSelectClause&quot;&amp;gt;
    SELECT mno, name, email, role, status, created_at, updated_at
    FROM member
&amp;lt;/sql&amp;gt;

&amp;lt;sql id=&quot;whereClause&quot;&amp;gt;
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;name != null&quot;&amp;gt;AND name LIKE CONCAT('%', #{name}, '%')&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;email != null&quot;&amp;gt;AND email = #{email}&amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;status != null&quot;&amp;gt;AND status = #{status}&amp;lt;/if&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/sql&amp;gt;

&amp;lt;select id=&quot;searchMembers&quot; resultType=&quot;Member&quot;&amp;gt;
    &amp;lt;include refid=&quot;baseSelectClause&quot; /&amp;gt;
    &amp;lt;include refid=&quot;whereClause&quot; /&amp;gt;
    ORDER BY mno DESC
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 공통 SQL 조각을 &amp;lt;sql&amp;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;동적 SQL은 다양한 조합으로 실행될 수 있어 모든 경우를 테스트하기 어렵습니다. 따라서 주요 케이스에 대한 단위 테스트와 통합 테스트를 작성하는 것이 중요합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Test
void testSearchMembersWithVariousConditions() {
    // 이름만 지정
    MemberSearchCondition condition1 = new MemberSearchCondition();
    condition1.setName(&quot;홍&quot;);
    List&amp;lt;Member&amp;gt; result1 = memberDao.searchMembers(condition1);
    assertThat(result1).extracting(&quot;name&quot;).allMatch(name -&amp;gt; name.toString().contains(&quot;홍&quot;));
    
    // 이름과 역할 지정
    MemberSearchCondition condition2 = new MemberSearchCondition();
    condition2.setName(&quot;홍&quot;);
    condition2.setRole(&quot;ADMIN&quot;);
    List&amp;lt;Member&amp;gt; result2 = memberDao.searchMembers(condition2);
    assertThat(result2).extracting(&quot;name&quot;, &quot;role&quot;)
                      .allMatch(tuple -&amp;gt; tuple.toList().get(0).toString().contains(&quot;홍&quot;) 
                                     &amp;amp;&amp;amp; &quot;ADMIN&quot;.equals(tuple.toList().get(1)));
    
    // 조건 없음 (전체 조회)
    MemberSearchCondition condition3 = new MemberSearchCondition();
    List&amp;lt;Member&amp;gt; result3 = memberDao.searchMembers(condition3);
    assertThat(result3).isNotEmpty();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 성능 고려사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 SQL은 편리하지만 복잡한 조건이 많을 경우 성능에 영향을 줄 수 있습니다. 다음 사항을 고려해야합니다.&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;인덱스 활용을 고려한 WHERE 절 작성&lt;/li&gt;
&lt;li&gt;대용량 데이터 처리 시 페이징 적용&lt;/li&gt;
&lt;li&gt;실행 계획 분석을 통한 쿼리 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 MyBatis의 동적 SQL 기능에 대해 자세히 알아보았습니다. if, where, choose, trim, foreach, set 등의 태그를 활용하면 복잡한 조건부 쿼리를 깔끔하게 작성할 수 있습니다. 또한 OGNL 표현식을 통해 파라미터 객체의 속성에 쉽게 접근하고 다양한 연산을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 SQL은 다양한 검색 조건, 선택적 업데이트, 다중 항목 처리 등 실제 비즈니스 요구사항을 구현하는 데 매우 유용합니다. 하지만 SQL 인젝션 방지, 가독성 유지, 성능 최적화 등의 주의사항을 항상 염두에 두어야 합니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>MyBatis</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/11</guid>
      <comments>https://leve68.tistory.com/entry/MyBatis%EC%9D%98-%EB%8F%99%EC%A0%81-SQL#entry11comment</comments>
      <pubDate>Fri, 23 May 2025 23:08:21 +0900</pubDate>
    </item>
    <item>
      <title>MyBatis</title>
      <link>https://leve68.tistory.com/entry/My-Batis</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework와 함께 데이터 접근 계층을 구현하는 데 널리 사용되는 MyBatis에 대해 알아보겠습니다. 데이터베이스와의 상호작용은 거의 모든 웹 애플리케이션에서 필수적인데, Spring과 MyBatis를 함께 사용하면 이 작업을 훨씬 효율적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전통적인 JDBC의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 전통적인 JDBC 접근 방식의 문제점을 살펴보겠습니다. 다음은 회원 등록을 처리하는 일반적인 JDBC 코드입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public int registMember(Member member) throws SQLException {
    Connection con = util.getConnection();
    try {
        con.setAutoCommit(false);
        int result = mDao.insert(con, member); // 실제 비즈니스 로직
        con.commit();
        return result;
    } catch (SQLException e) {
        con.rollback();
        throw e;
    } finally {
        util.close(con);
    }
}

@Override
public int insert(Connection con, Member member) throws SQLException {
    String sql = &quot;insert into member (name, email, password) values(?,?,?)&quot;;
    int result = -1;
    try (PreparedStatement pstmt = con.prepareStatement(sql)) {
        pstmt.setString(1, member.getName());
        pstmt.setString(2, member.getEmail());
        pstmt.setString(3, member.getPassword());
        result = pstmt.executeUpdate(); // 실제 쿼리 실행
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에는 다음과 같은 문제점이 있습니다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;기술 종속적인 코드&lt;/b&gt;: 비즈니스 로직보다 기술적인 코드(연결 관리, 트랜잭션 처리 등)가 더 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복적인 작업&lt;/b&gt;: 모든 데이터베이스 작업마다 유사한 패턴의 코드를 반복해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 관리&lt;/b&gt;: 연결, 문장, 결과셋 등의 리소스를 직접 관리해야 하는 부담이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 처리 복잡성&lt;/b&gt;: 체크 예외 처리를 위한 try-catch 블록이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL과 Java 코드의 혼재&lt;/b&gt;: SQL 쿼리가 Java 코드 안에 문자열로 존재하여 유지보수가 어렵습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MyBatis란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 이러한 문제를 해결하기 위한 퍼시스턴스 프레임워크입니다. 주요 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL과 Java 코드의 분리&lt;/b&gt;: SQL 쿼리를 XML 파일로 분리하여 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 객체 매핑&lt;/b&gt;: ResultSet 결과를 Java 객체에 자동으로 매핑합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;간소화된 JDBC 코드&lt;/b&gt;: 연결 관리, 파라미터 설정, 결과 매핑 등을 자동화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동적 SQL 지원&lt;/b&gt;: 조건에 따라 SQL을 동적으로 구성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring과의 통합&lt;/b&gt;: Spring과 쉽게 통합되어 트랜잭션 관리 등을 Spring에 위임할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot에서 MyBatis 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 환경에서 MyBatis를 사용하기 위한 설정을 단계별로 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven pom.xml에 다음 의존성을 추가합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.2.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!-- 데이터베이스 드라이버 (MySQL 예시) --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. application.properties 설정&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# 데이터소스 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password

# 커넥션 풀 설정
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.minimum-idle=3
spring.datasource.hikari.maximum-pool-size=5
spring.datasource.hikari.connection-timeout=600000

# MyBatis 설정
mybatis.type-aliases-package=com.example.domain
mybatis.mapper-locations=classpath:/mappers/**/*.xml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;b&gt;type-aliases-package&lt;/b&gt;: DTO 클래스가 위치한 패키지를 지정합니다. 이렇게 하면 XML 매퍼에서 전체 경로 대신 클래스명만으로 참조할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mapper-locations&lt;/b&gt;: SQL 매퍼 XML 파일의 위치를 지정합니다. 위 설정은 classpath의 mappers 디렉토리와 그 하위 모든 디렉토리에서 XML 파일을 찾습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DAO(Data Access Object) 인터페이스 작성&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MemberDao {
    // 회원 등록
    int insert(Member member);
    
    // 이메일로 회원 조회
    Member select(String email);
    
    // 모든 회원 조회
    List&amp;lt;Member&amp;gt; search();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis를 사용하면 DAO 인터페이스만 정의하고 구현 클래스는 작성할 필요가 없습니다. MyBatis가 런타임에 구현체를 동적으로 생성해 줍니다. 또한 전통적인 JDBC에서와 달리 Connection 객체를 메서드 인자로 전달할 필요가 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. XML 매퍼 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 SQL 쿼리를 정의할 XML 매퍼 파일을 작성합니다. 이 파일은 src/main/resources/mappers 디렉토리에 생성합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;com.example.dao.MemberDao&quot;&amp;gt;
    &amp;lt;!-- 회원 등록 --&amp;gt;
    &amp;lt;insert id=&quot;insert&quot; parameterType=&quot;Member&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;mno&quot;&amp;gt;
        INSERT INTO member (name, email, password) 
        VALUES (#{name}, #{email}, #{password})
    &amp;lt;/insert&amp;gt;
    
    &amp;lt;!-- 이메일로 회원 조회 --&amp;gt;
    &amp;lt;select id=&quot;select&quot; parameterType=&quot;String&quot; resultType=&quot;Member&quot;&amp;gt;
        SELECT mno, name, email, password 
        FROM member 
        WHERE email = #{email}
    &amp;lt;/select&amp;gt;
    
    &amp;lt;!-- 모든 회원 조회 --&amp;gt;
    &amp;lt;select id=&quot;search&quot; resultType=&quot;Member&quot;&amp;gt;
        SELECT mno, name, email, password 
        FROM member
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 매퍼 파일의 주요 요소를 살펴보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;namespace&lt;/b&gt;: DAO 인터페이스의 전체 경로와 일치시켜야 합니다. MyBatis는 이를 통해 인터페이스와 XML을 매핑합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;: DAO 인터페이스의 메서드명과 일치시켜야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;parameterType&lt;/b&gt;: 파라미터의 타입을 지정합니다. type-aliases 설정을 통해 패키지 없이 클래스명만으로 참조할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;resultType&lt;/b&gt;: 조회 결과를 매핑할 객체의 타입을 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useGeneratedKeys&lt;/b&gt;, &lt;b&gt;keyProperty&lt;/b&gt;: 자동 생성 키(auto increment)를 사용할 때 지정합니다. 생성된 키 값이 keyProperty에 지정된 객체 속성에 자동으로 할당됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 SQL 쿼리에서 파라미터를 참조할 때는 #{속성명} 형식을 사용합니다. 이는 PreparedStatement의 '?'로 치환되어 SQL 인젝션을 방지합니다.&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;마지막으로 Spring Boot 애플리케이션 클래스에 MyBatis 매퍼를 스캔하도록 설정합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
@MapperScan(&quot;com.example.dao&quot;)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@MapperScan 어노테이션은 지정된 패키지에서 MyBatis 매퍼 인터페이스를 찾아 자동으로 등록합니다. 이로써 DAO 인터페이스에 @Mapper 어노테이션을 일일이 붙이지 않아도 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 계층 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis와 Spring을 함께 사용할 때 서비스 계층은 다음과 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
    
    private final MemberDao memberDao;
    
    @Transactional
    @Override
    public int registerMember(Member member) {
        return memberDao.insert(member);
    }
    
    @Override
    public Member getMember(String email) {
        return memberDao.select(email);
    }
    
    @Override
    public List&amp;lt;Member&amp;gt; getAllMembers() {
        return memberDao.search();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;생성자 주입&lt;/b&gt;: @RequiredArgsConstructor와 final 필드를 통해 의존성을 주입받습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 관리&lt;/b&gt;: @Transactional 어노테이션을 통해 선언적으로 트랜잭션을 관리합니다. 이제 트랜잭션 시작, 커밋, 롤백을 직접 코드로 작성할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 처리 단순화&lt;/b&gt;: MyBatis와 Spring은 JDBC 예외를 Spring의 DataAccessException으로 변환하여 체크 예외를 처리해야 하는 번거로움을 줄여줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ResultMap을 이용한 고급 매핑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 데이터베이스 컬럼명과 Java 객체의 속성명이 다르거나, 객체 간의 관계를 매핑해야 할 필요가 있습니다. 이런 경우 ResultMap을 사용할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;memberMap&quot; type=&quot;Member&quot;&amp;gt;
    &amp;lt;id property=&quot;mno&quot; column=&quot;mno&quot;/&amp;gt;
    &amp;lt;result property=&quot;name&quot; column=&quot;member_name&quot;/&amp;gt;
    &amp;lt;result property=&quot;email&quot; column=&quot;member_email&quot;/&amp;gt;
    &amp;lt;result property=&quot;password&quot; column=&quot;member_password&quot;/&amp;gt;
&amp;lt;/resultMap&amp;gt;

&amp;lt;select id=&quot;select&quot; resultMap=&quot;memberMap&quot;&amp;gt;
    SELECT mno, name AS member_name, email AS member_email, password AS member_password 
    FROM member 
    WHERE email = #{email}
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResultMap의 주요 요소는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;: 매핑을 식별하는 고유 이름&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type&lt;/b&gt;: 결과를 매핑할 객체 타입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;id 태그&lt;/b&gt;: 기본 키 컬럼을 매핑&lt;/li&gt;
&lt;li&gt;&lt;b&gt;result 태그&lt;/b&gt;: 일반 컬럼을 매핑
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;property: Java 객체의 속성명&lt;/li&gt;
&lt;li&gt;column: 데이터베이스 컬럼명&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체 간 관계 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 객체 간의 일대일(has-one), 일대다(has-many) 관계도 매핑할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일대일 관계 매핑 (has-one)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원과 프로필처럼 일대일 관계를 매핑하려면 association 태그를 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;memberWithProfileMap&quot; type=&quot;Member&quot;&amp;gt;
    &amp;lt;id property=&quot;mno&quot; column=&quot;mno&quot;/&amp;gt;
    &amp;lt;result property=&quot;name&quot; column=&quot;name&quot;/&amp;gt;
    &amp;lt;result property=&quot;email&quot; column=&quot;email&quot;/&amp;gt;
    &amp;lt;association property=&quot;profile&quot; javaType=&quot;Profile&quot;&amp;gt;
        &amp;lt;id property=&quot;profileId&quot; column=&quot;profile_id&quot;/&amp;gt;
        &amp;lt;result property=&quot;pictureUrl&quot; column=&quot;picture_url&quot;/&amp;gt;
        &amp;lt;result property=&quot;bio&quot; column=&quot;bio&quot;/&amp;gt;
    &amp;lt;/association&amp;gt;
&amp;lt;/resultMap&amp;gt;

&amp;lt;select id=&quot;selectWithProfile&quot; resultMap=&quot;memberWithProfileMap&quot;&amp;gt;
    SELECT m.mno, m.name, m.email, p.profile_id, p.picture_url, p.bio
    FROM member m
    LEFT JOIN profile p ON m.mno = p.mno
    WHERE m.email = #{email}
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일대다 관계 매핑 (has-many)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원과 주소처럼 일대다 관계를 매핑하려면 collection 태그를 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;memberWithAddressesMap&quot; type=&quot;Member&quot;&amp;gt;
    &amp;lt;id property=&quot;mno&quot; column=&quot;mno&quot;/&amp;gt;
    &amp;lt;result property=&quot;name&quot; column=&quot;name&quot;/&amp;gt;
    &amp;lt;result property=&quot;email&quot; column=&quot;email&quot;/&amp;gt;
    &amp;lt;collection property=&quot;addresses&quot; ofType=&quot;Address&quot;&amp;gt;
        &amp;lt;id property=&quot;ano&quot; column=&quot;ano&quot;/&amp;gt;
        &amp;lt;result property=&quot;title&quot; column=&quot;title&quot;/&amp;gt;
        &amp;lt;result property=&quot;address&quot; column=&quot;address&quot;/&amp;gt;
        &amp;lt;result property=&quot;detailAddress&quot; column=&quot;detail_address&quot;/&amp;gt;
    &amp;lt;/collection&amp;gt;
&amp;lt;/resultMap&amp;gt;

&amp;lt;select id=&quot;selectWithAddresses&quot; resultMap=&quot;memberWithAddressesMap&quot;&amp;gt;
    SELECT m.mno, m.name, m.email, a.ano, a.title, a.address, a.detail_address
    FROM member m
    LEFT JOIN address a ON m.mno = a.mno
    WHERE m.email = #{email}
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 다양한 매핑 방법이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;중첩 Select&lt;/b&gt;: 주 쿼리 실행 후 추가 쿼리를 실행하여 관련 객체를 로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중첩 ResultMap&lt;/b&gt;: 다른 ResultMap을 참조하여 재사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컬럼 접두사&lt;/b&gt;: 컬럼명이 중복될 때 접두사를 사용하여 구분&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring과 MyBatis 통합의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring과 MyBatis를 함께 사용할 때의 주요 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;선언적 트랜잭션 관리&lt;/b&gt;: @Transactional 어노테이션으로 간편하게 트랜잭션 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합된 예외 처리&lt;/b&gt;: MyBatis 예외가 Spring의 DataAccessException으로 변환되어 일관된 예외 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 주입&lt;/b&gt;: Spring의 IoC 컨테이너를 통해 DAO에 대한 의존성 주입 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성&lt;/b&gt;: 단위 테스트와 통합 테스트가 더 쉬워짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AOP 활용&lt;/b&gt;: 로깅, 성능 측정 등을 위한 관점 지향 프로그래밍 적용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Spring Framework와 MyBatis를 통합하여 사용하는 방법을 알아보았습니다. MyBatis는 SQL과 Java 객체 간의 매핑을 쉽게 해주며, Spring과 함께 사용하면 많은 보일러플레이트 코드를 제거할 수 있습니다. SQL을 직접 작성할 수 있어 복잡한 쿼리에 대한 제어력을 유지하면서도, 많은 반복 코드를 제거해주어 개발 생산성을 높여줍니다. 또한 Spring과의 통합을 통해 트랜잭션 관리, 예외 처리 등 다양한 기능을 손쉽게 활용할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>MyBatis</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/10</guid>
      <comments>https://leve68.tistory.com/entry/My-Batis#entry10comment</comments>
      <pubDate>Sat, 3 May 2025 23:01:00 +0900</pubDate>
    </item>
    <item>
      <title>Interceptor | ErrorPage | FileUpload</title>
      <link>https://leve68.tistory.com/entry/Interceptor-ErrorPage-FileUpload</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크를 사용하면서 웹 애플리케이션 개발 시 자주 마주치게 되는 세 가지 중요한 기능에 대해 알아보겠습니다. 인터셉터, 에러 페이지 처리, 그리고 파일 업로드는 꼭 필요한 기능들인데요, 이번 글에서는 이 기능들의 개념부터 구현 방법까지 자세히 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 인터셉터 (Interceptor)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터셉터란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터는 DispatcherServlet이 컨트롤러를 호출하기 전과 후에 요청과 응답을 가로채서 처리하는 스프링 MVC의 기능입니다. 서블릿 필터와 유사하지만, 스프링 MVC의 컨텍스트 내에서 동작하기 때문에 스프링의 모든 기능을 활용할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터셉터의 주요 메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터를 구현하기 위해서는 HandlerInterceptor 인터페이스를 구현해야 합니다. 이 인터페이스는 다음 세 가지 핵심 메서드를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class MyInterceptor implements HandlerInterceptor {
    
    // 컨트롤러 실행 전
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 요청 처리 전 로직
        // true 반환 시 계속 진행, false 반환 시 요청 처리 중단
        return true;
    }
    
    // 컨트롤러 실행 후, 뷰 렌더링 전
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 
            ModelAndView modelAndView) throws Exception {
        // 컨트롤러 로직 실행 후, 뷰 렌더링 전 로직
    }
    
    // 요청 처리 완료 후 (뷰 렌더링 후)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, 
            Exception ex) throws Exception {
        // 요청 처리 완료 후 로직 (예외 처리 포함)
    }
}
&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;인터셉터를 구현한 후에는 스프링 MVC 설정에 등록해야 합니다. 이는 WebMvcConfigurer 인터페이스를 구현하여 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private MyInterceptor myInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor)
                .addPathPatterns(&quot;/secure/**&quot;)     // 인터셉터를 적용할 경로 패턴
                .excludePathPatterns(&quot;/public/**&quot;, &quot;/resources/**&quot;);  // 제외할 경로 패턴
    }
}
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;로그인 체크&lt;/b&gt;: 로그인이 필요한 페이지에 대한 접근 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한 검사&lt;/b&gt;: 특정 리소스에 접근할 권한이 있는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로깅&lt;/b&gt;: 모든 요청에 대한 로그 기록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공통 데이터 설정&lt;/b&gt;: 요청 처리에 필요한 공통 데이터 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 측정&lt;/b&gt;: 요청 처리 시간 측정&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 인터셉터 구현 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 로그인 체크를 위한 인터셉터 구현 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        log.debug(&quot;AuthInterceptor preHandle 실행&quot;);
        
        // 세션에서 로그인 정보 확인
        HttpSession session = request.getSession();
        UserDto loginUser = (UserDto) session.getAttribute(&quot;loginUser&quot;);
        
        // 로그인되지 않은 경우
        if (loginUser == null) {
            log.debug(&quot;인증되지 않은 사용자 접근&quot;);
            
            // 로그인 페이지로 리다이렉트
            response.sendRedirect(request.getContextPath() + &quot;/login&quot;);
            return false;  // 요청 처리 중단
        }
        
        return true;  // 요청 처리 계속 진행
    }
}
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;preHandle: 등록한 순서대로 실행됩니다.&lt;/li&gt;
&lt;li&gt;postHandle 및 afterCompletion: 등록한 순서의 &lt;b&gt;역순으로&lt;/b&gt; 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 인터셉터 A, B, C 순으로 등록했다면 실행 순서는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;A.preHandle &amp;rarr; B.preHandle &amp;rarr; C.preHandle&lt;/li&gt;
&lt;li&gt;컨트롤러 실행&lt;/li&gt;
&lt;li&gt;C.postHandle &amp;rarr; B.postHandle &amp;rarr; A.postHandle&lt;/li&gt;
&lt;li&gt;뷰 렌더링&lt;/li&gt;
&lt;li&gt;C.afterCompletion &amp;rarr; B.afterCompletion &amp;rarr; A.afterCompletion&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;세 가지 기능의 비교&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;필터, AOP, 인터셉터의 차이점&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션에서 공통 관심사를 처리하는 방법으로 필터, AOP, 인터셉터가 있습니다. 이들의 주요 차이점은 다음과 같습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. Servlet Filter&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring과 무관하게 작동&lt;/li&gt;
&lt;li&gt;DispatcherServlet 호출 이전에 동작&lt;/li&gt;
&lt;li&gt;ServletRequest와 ServletResponse에 대한 전처리와 후처리만 가능&lt;/li&gt;
&lt;li&gt;Spring 컨텍스트와의 통합이 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. Handler Interceptor&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 컨텍스트 내에서 동작&lt;/li&gt;
&lt;li&gt;DispatcherServlet이 요청을 처리한 후, 컨트롤러 호출 전/후에 작동&lt;/li&gt;
&lt;li&gt;Spring의 모든 빈과 기능에 접근 가능&lt;/li&gt;
&lt;li&gt;HTTP 요청과 응답에 관련된 작업에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. AOP (Aspect-Oriented Programming)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring의 핵심 기능으로, 메소드 호출 시점에 작동&lt;/li&gt;
&lt;li&gt;HTTP 요청 처리와 직접적인 관련이 적음&lt;/li&gt;
&lt;li&gt;주로 서비스, DAO 등 비즈니스 계층에 적용&lt;/li&gt;
&lt;li&gt;트랜잭션 관리, 로깅, 보안 등 애플리케이션 전반적인 관심사 처리에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 페이지 처리 (Error Page)&lt;/h2&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오류 발생 시 /error 경로로 요청이 내부적으로 전달됩니다.&lt;/li&gt;
&lt;li&gt;BasicErrorController가 이 요청을 처리합니다.&lt;/li&gt;
&lt;li&gt;해당 오류 코드에 맞는 에러 페이지를 찾아 표시합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/error/{status}.html 형식의 페이지를 찾습니다. (예: 404.html, 500.html)&lt;/li&gt;
&lt;li&gt;없으면 기본 에러 페이지(Whitelabel Error Page)를 표시합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 에러 페이지 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 에러 페이지를 정의하는 방법은 크게 두 가지가 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 정적 HTML 파일 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/main/resources/static/error/ 또는 src/main/resources/templates/error/ 디렉토리에 에러 상태 코드와 일치하는 이름의 파일을 생성합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;404.html: 404 에러(페이지를 찾을 수 없음)용 페이지&lt;/li&gt;
&lt;li&gt;500.html: 500 에러(서버 내부 오류)용 페이지&lt;/li&gt;
&lt;li&gt;error.html: 그 외 모든 에러를 위한 기본 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 컨트롤러에서 @ExceptionHandler 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 내에서 발생하는 특정 예외를 처리하기 위해 @ExceptionHandler를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
public class ProductController {
    
    // 이 컨트롤러 내에서 발생하는 ProductNotFoundException 예외를 처리
    @ExceptionHandler(ProductNotFoundException.class)
    public String handleProductNotFound(ProductNotFoundException ex, Model model) {
        model.addAttribute(&quot;errorMessage&quot;, ex.getMessage());
        return &quot;error/product-not-found&quot;;
    }
    
    // 다른 컨트롤러 메서드들...
}&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;애플리케이션 전체에서 발생하는 예외를 처리하려면 @ControllerAdvice 또는 @RestControllerAdvice를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex, Model model) {
        logger.error(&quot;예외 발생: {}&quot;, ex.getMessage(), ex);
        model.addAttribute(&quot;errorMessage&quot;, &quot;서비스 처리 중 오류가 발생했습니다.&quot;);
        return &quot;error/general-error&quot;;
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public String handleResourceNotFound(ResourceNotFoundException ex, Model model) {
        model.addAttribute(&quot;errorMessage&quot;, ex.getMessage());
        return &quot;error/not-found&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;더 세밀한 제어를 위해 ErrorAttributes를 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
    
    @Override
    public Map&amp;lt;String, Object&amp;gt; getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map&amp;lt;String, Object&amp;gt; errorAttributes = super.getErrorAttributes(webRequest, options);
        
        // 추가 정보 포함
        errorAttributes.put(&quot;company&quot;, &quot;My Company&quot;);
        errorAttributes.put(&quot;contact&quot;, &quot;support@mycompany.com&quot;);
        
        return errorAttributes;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파일 업로드 (File Upload)&lt;/h2&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;스프링 부트에서 파일 업로드를 위한 기본 설정은 application.properties 또는 application.yml 파일에서 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 최대 파일 크기
spring.servlet.multipart.max-file-size=10MB
# 최대 요청 크기
spring.servlet.multipart.max-request-size=10MB
# multipart 요청 활성화 (기본값은 true)
spring.servlet.multipart.enabled=true
# 임시 파일 저장 위치
spring.servlet.multipart.location=/tmp&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;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/files&quot;)
public class FileUploadController {
    
    private final String uploadDir = &quot;uploads&quot;;
    
    @PostMapping(&quot;/upload&quot;)
    public String uploadFile(@RequestParam(&quot;file&quot;) MultipartFile file, Model model) {
        if (file.isEmpty()) {
            model.addAttribute(&quot;message&quot;, &quot;파일을 선택해주세요.&quot;);
            return &quot;upload-form&quot;;
        }
        
        try {
            // 업로드 디렉토리 생성
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            
            // 원본 파일명 추출
            String originalFilename = file.getOriginalFilename();
            // 파일명 충돌 방지를 위한 고유 파일명 생성
            String uniqueFilename = UUID.randomUUID().toString() + &quot;_&quot; + originalFilename;
            
            // 파일 저장
            Path filePath = uploadPath.resolve(uniqueFilename);
            Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
            
            model.addAttribute(&quot;message&quot;, &quot;파일 업로드 성공: &quot; + originalFilename);
            model.addAttribute(&quot;filePath&quot;, filePath.toString());
            
            return &quot;upload-success&quot;;
            
        } catch (IOException e) {
            model.addAttribute(&quot;message&quot;, &quot;파일 업로드 실패: &quot; + e.getMessage());
            return &quot;upload-form&quot;;
        }
    }
    
    @GetMapping(&quot;/form&quot;)
    public String showUploadForm() {
        return &quot;upload-form&quot;;
    }
}&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;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;/upload-multiple&quot;)
public String uploadMultipleFiles(@RequestParam(&quot;files&quot;) MultipartFile[] files, Model model) {
    List&amp;lt;String&amp;gt; uploadedFiles = new ArrayList&amp;lt;&amp;gt;();
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            try {
                // 파일 저장 로직 (위와 유사)
                String filename = saveFile(file);
                uploadedFiles.add(filename);
            } catch (IOException e) {
                model.addAttribute(&quot;message&quot;, &quot;파일 업로드 실패: &quot; + e.getMessage());
                return &quot;upload-form&quot;;
            }
        }
    }
    
    model.addAttribute(&quot;message&quot;, &quot;파일 업로드 성공: &quot; + uploadedFiles.size() + &quot;개 파일&quot;);
    model.addAttribute(&quot;files&quot;, uploadedFiles);
    
    return &quot;upload-success&quot;;
}

private String saveFile(MultipartFile file) throws IOException {
    // 실제 파일 저장 로직
    // ...
}&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 타입 검증&lt;/b&gt;: 허용된 파일 형식만 업로드할 수 있도록 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 크기 제한&lt;/b&gt;: 서버 리소스를 보호하기 위해 파일 크기 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;악성 파일 검사&lt;/b&gt;: 바이러스 검사 등을 통해 악성 파일 필터링&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전한 저장 경로&lt;/b&gt;: 웹 접근이 불가능한 안전한 경로에 파일 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고유 파일명 사용&lt;/b&gt;: 파일명 충돌 및 경로 순회 공격 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 파일 타입 검증 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private boolean isImageFile(MultipartFile file) {
    String contentType = file.getContentType();
    return contentType != null &amp;amp;&amp;amp; contentType.startsWith(&quot;image/&quot;);
}

@PostMapping(&quot;/upload-image&quot;)
public String uploadImage(@RequestParam(&quot;file&quot;) MultipartFile file, Model model) {
    if (!isImageFile(file)) {
        model.addAttribute(&quot;message&quot;, &quot;이미지 파일만 업로드 가능합니다.&quot;);
        return &quot;upload-form&quot;;
    }
    
    // 파일 저장 로직
    // ...
}
&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;이번 글에서는 스프링 웹 애플리케이션 개발에서 중요한 세 가지 기능인 인터셉터, 에러 페이지 처리, 파일 업로드에 대해 살펴보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터는 특히 인증, 로깅, 성능 측정과 같은 공통 관심사를 처리하는 데 유용하며, 에러 페이지 처리는 사용자 경험을 향상시키는 데 중요한 역할을 합니다. 마지막으로, 파일 업로드 기능은 웹 애플리케이션에서 자주 필요한 기능으로, 스프링에서 제공하는 간편한 API를 통해 안전하게 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/9</guid>
      <comments>https://leve68.tistory.com/entry/Interceptor-ErrorPage-FileUpload#entry9comment</comments>
      <pubDate>Wed, 30 Apr 2025 20:47:09 +0900</pubDate>
    </item>
    <item>
      <title>Spring MVC</title>
      <link>https://leve68.tistory.com/entry/Spring-MVC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 개발하다 보면 많은 요청과 응답을 처리해야 합니다. 이런 복잡한 과정을 효율적으로 관리하기 위해 스프링 프레임워크는 MVC 패턴을 기반으로 한 모듈을 제공합니다. 스프링 MVC의 기본 구조와 동작 원리, 그리고 실제 개발에서 어떻게 활용하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 MVC란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 Model-View-Controller(Model2) 아키텍처를 기반으로 하는 스프링 프레임워크의 하위 모듈입니다. 이 구조는 애플리케이션의 관심사를 효과적으로 분리하여 유지보수성과 확장성을 높여줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 구성 요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 크게 세 가지 영역으로 구성됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DispatcherServlet&lt;/b&gt;: 스프링 MVC의 핵심 컴포넌트로, 모든 클라이언트 요청을 가장 먼저 받아 적절한 컨트롤러로 전달하는 프론트 컨트롤러 역할을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스프링 MVC 인프라 컴포넌트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HandlerMapping&lt;/b&gt;: 요청 URL을 기반으로 적절한 컨트롤러를 찾아주는 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HandlerAdapter&lt;/b&gt;: 컨트롤러의 메서드를 호출하여 요청을 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ViewResolver&lt;/b&gt;: 컨트롤러가 반환한 뷰 이름을 실제 뷰 객체로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발자가 작성하는 영역&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모델(Model)&lt;/b&gt;: 데이터와 비즈니스 로직을 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뷰(View)&lt;/b&gt;: 사용자에게 보여지는 화면 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핸들러(Handler/Controller)&lt;/b&gt;: 클라이언트의 요청을 처리하는 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 MVC의 동작 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC가 요청을 처리하는 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 DispatcherServlet으로 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 HandlerMapping에게 요청을 처리할 적합한 컨트롤러가 무엇인지 문의합니다.&lt;/li&gt;
&lt;li&gt;HandlerMapping은 URL 패턴을 분석하여 해당 요청을 처리할 컨트롤러를 결정하고 반환합니다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 선택된 컨트롤러를 실행하기 위해 HandlerAdapter에게 요청을 전달합니다.&lt;/li&gt;
&lt;li&gt;HandlerAdapter는 컨트롤러(@Controller)에게 요청 처리를 위임합니다.&lt;/li&gt;
&lt;li&gt;컨트롤러는 비즈니스 로직 수행을 위해 서비스 계층을 호출합니다.&lt;/li&gt;
&lt;li&gt;컨트롤러는 처리 결과를 모델 객체에 저장합니다.&lt;/li&gt;
&lt;li&gt;컨트롤러는 뷰 이름을 반환하고, HandlerAdapter는 이를 DispatcherServlet에게 전달합니다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 ViewResolver에게 뷰 이름을 전달하여 실제 뷰 객체를 요청합니다.&lt;/li&gt;
&lt;li&gt;ViewResolver는 뷰 이름을 분석하여 적합한 뷰 객체를 찾아 반환합니다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 뷰 객체에게 렌더링을 요청합니다.&lt;/li&gt;
&lt;li&gt;뷰는 모델 데이터를 사용하여 응답을 생성하고 클라이언트에게 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 마치 오케스트라의 지휘자와 같이 DispatcherServlet이 요청 처리의 전체 과정을 조율하는 구조입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트롤러 작성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서 컨트롤러는 @Controller 어노테이션을 사용하여 정의합니다. 컨트롤러는 클라이언트의 요청을 받아들이고 적절한 응답을 제공하는 역할을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@RequestMapping 어노테이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestMapping은 요청 URL과 컨트롤러 메서드를 연결하는 어노테이션입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 레벨 매핑&lt;/b&gt;: 컨트롤러 클래스의 모든 메서드에 공통으로 적용되는 기본 경로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드 레벨 매핑&lt;/b&gt;: 특정 메서드에만 적용되는 경로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드 지정&lt;/b&gt;: GET, POST 등의 HTTP 메서드를 지정할 수 있으며, @GetMapping, @PostMapping과 같은 축약형 어노테이션도 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/simple&quot;)
@RequiredArgsConstructor
public class SimpleController {
    private final SimpleService sService;
    
    @GetMapping(&quot;/forward&quot;)
    public String forward(Model model) {
        String value = sService.helloMVC();
        model.addAttribute(&quot;data&quot;, value);
        return &quot;mvc/simple&quot;;
    }
    
    @GetMapping(&quot;/redirect&quot;)
    public String redirect() {
        return &quot;redirect:/simple/forward&quot;;
    }
    
    @GetMapping(&quot;/json&quot;)
    @ResponseBody
    public Map&amp;lt;String, Object&amp;gt; json() {
        return Map.of(&quot;name&quot;, &quot;hong&quot;, &quot;age&quot;, 30);
    }
}&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청 분석&lt;/b&gt;: 클라이언트의 요청 데이터(파라미터, 헤더, 쿠키 등)를 분석합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직 수행&lt;/b&gt;: 서비스 계층을 호출하여 필요한 비즈니스 로직을 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 저장&lt;/b&gt;: 처리 결과를 Model 객체에 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뷰 이름 반환&lt;/b&gt;: 결과를 표시할 뷰의 논리적 이름을 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;뷰 리졸버 (ViewResolver)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 리졸버는 컨트롤러가 반환한 논리적인 뷰 이름을 실제 뷰 파일의 위치로 변환하는 역할을 합니다. 스프링 부트에서는 application.properties 파일에서 다음과 같이 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 컨트롤러가 &quot;home&quot;이라는 뷰 이름을 반환하면, &quot;/WEB-INF/views/home.jsp&quot; 파일을 찾아 렌더링하도록 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트롤러 단위 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC 컨트롤러는 MockMvc를 사용하여 효과적으로 테스트할 수 있습니다. MockMvc는 실제 서버 환경 없이도 스프링 MVC의 동작을 시뮬레이션할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@WebMvcTest(SimpleController.class)
public class SimpleControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private SimpleService simpleService;
    
    @Test
    public void testForward() throws Exception {
        // 서비스 동작 모의 설정
        when(simpleService.helloMVC()).thenReturn(&quot;Hello, MVC!&quot;);
        
        // 요청 수행 및 검증
        mockMvc.perform(get(&quot;/simple/forward&quot;))
               .andExpect(status().isOk())
               .andExpect(view().name(&quot;mvc/simple&quot;))
               .andExpect(model().attributeExists(&quot;data&quot;))
               .andExpect(model().attribute(&quot;data&quot;, &quot;Hello, MVC!&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 과정은 일반적으로 다음 단계로 진행됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청 만들기 (MockMvcRequestBuilders 사용)&lt;/li&gt;
&lt;li&gt;실행 (perform 메서드)&lt;/li&gt;
&lt;li&gt;검증 (andExpect 메서드로 상태, 뷰, 모델 등 확인)&lt;/li&gt;
&lt;li&gt;결과 확인 (필요한 경우 andDo 메서드로 추가 작업 수행)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핸들러 메서드의 파라미터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC의 컨트롤러 메서드는 다양한 타입의 파라미터를 지원합니다. 파라미터의 순서에 관계없이 필요한 객체를 주입받을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@RequestParam&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestParam은 HTTP 요청의 파라미터를 메서드 파라미터로 바인딩합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@GetMapping(&quot;/calc&quot;)
@ResponseBody
public Map&amp;lt;String, Integer&amp;gt; calc(
    @RequestParam Integer num1,
    @RequestParam Integer num2,
    @RequestParam String oper
) {
    int result = switch(oper) {
        case &quot;+&quot; -&amp;gt; num1 + num2;
        default -&amp;gt; num1 * num2;
    };
    return Map.of(&quot;result&quot;, result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 /calc?num1=10&amp;amp;num2=20&amp;amp;oper=+와 같은 요청을 처리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ModelAttribute&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ModelAttribute는 요청 파라미터를 객체의 프로퍼티에 바인딩합니다. 객체의 프로퍼티 이름과 요청 파라미터의 이름이 일치하면 자동으로 값이 설정됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(&quot;/regist&quot;)
@ResponseBody
public Map&amp;lt;String, Object&amp;gt; regist(@ModelAttribute Member member, Model model) {
    log.debug(&quot;member: {}&quot;, member);
    model.addAttribute(&quot;member&quot;, member);
    return Map.of(&quot;result&quot;, member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 /regist?name=hong&amp;amp;email=hong@example.com&amp;amp;password=1234와 같은 요청을 처리하여 Member 객체의 각 프로퍼티에 값을 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@CookieValue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@CookieValue는 HTTP 요청의 쿠키 값을 메서드 파라미터로 바인딩합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/welcome&quot;)
public String welcome(@CookieValue(value = &quot;userId&quot;, required = false) String userId, Model model) {
    if (userId != null) {
        model.addAttribute(&quot;message&quot;, &quot;Welcome back, &quot; + userId);
    } else {
        model.addAttribute(&quot;message&quot;, &quot;Welcome new user&quot;);
    }
    return &quot;welcome&quot;;
}
&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;스프링 MVC에서는 리다이렉션을 통해 다른 URL로 요청을 전달할 수 있습니다. 리다이렉션 시 데이터를 전달하는 방법에는 여러 가지가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리다이렉션 방법&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/redirect&quot;)
public String redirect() {
    return &quot;redirect:/simple/forward&quot;;
}
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;URL 쿼리 파라미터&lt;/b&gt;: URL 뒤에 쿼리 스트링을 추가하여 데이터 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래시 스코프&lt;/b&gt;: 리다이렉션이 완료될 때까지만 유지되는 일회성 세션&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RedirectAttributes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedirectAttributes는 리다이렉션 시 데이터 전달을 위한 특별한 객체입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/process&quot;)
public String processForm(RedirectAttributes redirectAttributes) {
    // 처리 로직
    redirectAttributes.addAttribute(&quot;status&quot;, &quot;success&quot;); // URL 쿼리 파라미터로 추가
    redirectAttributes.addFlashAttribute(&quot;message&quot;, &quot;처리가 완료되었습니다.&quot;); // 플래시 속성으로 추가
    
    return &quot;redirect:/result&quot;;
}
&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;스프링 MVC는 웹 애플리케이션 개발에 필요한 강력한 구조와 기능을 제공합니다. DispatcherServlet을 중심으로 한 구조는 요청 처리의 흐름을 체계적으로 관리하며, 다양한 어노테이션과 컴포넌트를 통해 개발자가 핵심 비즈니스 로직에 집중할 수 있게 도와줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 개발부터 뷰 처리, 파라미터 바인딩, 리다이렉션 처리까지, 스프링 MVC는 웹 개발의 다양한 측면을 포괄적으로 지원합니다. 이러한 특성들로 인해 스프링 MVC는 현대 자바 웹 애플리케이션 개발의 표준으로 자리 잡았습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>MVC</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/8</guid>
      <comments>https://leve68.tistory.com/entry/Spring-MVC#entry8comment</comments>
      <pubDate>Sun, 27 Apr 2025 00:03:23 +0900</pubDate>
    </item>
    <item>
      <title>Spring AOP</title>
      <link>https://leve68.tistory.com/entry/Spring-AOP</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 코드의 중복을 획기적으로 줄이고 핵심 비즈니스 로직에 집중할 수 있게 해주는 강력한 기술입니다. 제가 이해한 AOP의 개념과 실제 활용법에 대해 공유하려 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP가 왜 필요할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 서비스 클래스의 메서드를 살펴보면, 크게 두 부분으로 구성됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직&lt;/b&gt; - 실제로 우리가 수행하려는 핵심 작업 (데이터 생성, 조회 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부가 기능&lt;/b&gt; - 핵심 작업을 수행하기 위해 필요한 보조 작업 (로깅, DB 접속, 트랜잭션 처리, 예외 처리 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이러한 부가 기능들이 여러 메서드에서 계속 반복된다는 점입니다. 예를 들어, 모든 DB 작업 전후로 로깅을 해야 한다면, 각 메서드마다 로깅 코드를 중복해서 작성해야 하죠. 이런 중복 코드는 유지보수를 어렵게 만들고, 핵심 비즈니스 로직을 파악하기 어렵게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 이러한 '횡단 관심사(Cross-cutting Concerns)'를 분리하여 모듈화하는 프로그래밍 패러다임입니다. 즉, 여러 곳에 흩어진 부가 기능을 한 곳으로 모아 관리할 수 있게 해주는 것이죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP의 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP를 이해하기 위해서는 몇 가지 핵심 용어를 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Target&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AOP가 적용될 객체입니다. 핵심 비즈니스 로직을 포함한 Bean 객체가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Aspect&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;횡단 관심사를 모듈화한 단위입니다. 하나 이상의 Advice를 포함합니다.&lt;/li&gt;
&lt;li&gt;Spring에서는 @Aspect 애노테이션을 사용하여 클래스를 Aspect로 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Advice&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 Join Point에서 실행되는 코드입니다. 실제 부가 기능을 정의합니다.&lt;/li&gt;
&lt;li&gt;Spring에서는 @Before, @After, @Around 등의 애노테이션으로 Advice를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Join Point&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Advice가 적용될 수 있는 지점입니다.&lt;/li&gt;
&lt;li&gt;Spring AOP에서는 메서드 실행 지점으로 제한됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Pointcut&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Advice를 적용할 대상을 선택하는 표현식입니다.&lt;/li&gt;
&lt;li&gt;예: &quot;com.example.service 패키지의 모든 메서드&quot; 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. Weaving&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Aspect 코드를 비즈니스 로직 코드에 적용하는 과정입니다.&lt;/li&gt;
&lt;li&gt;Spring에서는 런타임에 Proxy를 통해 Weaving이 이루어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring AOP의 동작 원리 Proxy 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 Proxy 패턴을 사용하여 구현됩니다. 간단히 말해, 대상 객체를 감싸는 프록시 객체를 생성하고, 이 프록시가 요청을 가로채서 부가 기능을 수행한 뒤, 대상 객체에 요청을 전달하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring에서는 기본적으로 CGLIB(Code Generation Library)를 사용하여 클래스 기반 프록시를 생성합니다. 이 프록시는 원본 클래스를 상속받아 생성되므로, 원본 클래스의 모든 메서드를 오버라이드할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring AOP 프록시 생성 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;빈 등록 및 초기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 컨테이너가 빈 객체를 생성하고 초기화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BeanPostProcessor 작동&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AOP를 위한 AbstractAutoProxyCreator(BeanPostProcessor의 구현체)가 동작합니다.&lt;/li&gt;
&lt;li&gt;이 단계에서 프록시 적용 대상인지 판단합니다(어노테이션, 설정 등 확인).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프록시 객체 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 빈이 인터페이스를 구현했다면 &amp;rarr; JDK 동적 프록시 사용&lt;/li&gt;
&lt;li&gt;인터페이스가 없는 클래스라면 &amp;rarr; CGLIB 프록시(상속 기반) 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빈 교체&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본 빈 대신 생성된 프록시 객체가 Spring 컨테이너에 등록됩니다.&lt;/li&gt;
&lt;li&gt;다른 빈에서 의존성 주입을 요청하면 프록시 객체가 제공됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프록시 객체 내부 구조&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;타겟 참조&lt;/b&gt;: 프록시는 원본 빈(타겟)에 대한 참조를 내부적으로 가지고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Advice 참조&lt;/b&gt;: 공통 기능을 담당하는 Advice 객체에 대한 참조도 가지고 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중요: Advice 객체는 싱글톤으로 관리되어 여러 프록시가 공유합니다(로직 중복 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;런타임 실행 흐름&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; 프록시 객체 &amp;rarr; [전처리 Advice] &amp;rarr; 원본 객체 &amp;rarr; [후처리 Advice] &amp;rarr; 프록시 객체 &amp;rarr; 클라이언트&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 예시(트랜잭션)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 서비스 메서드 호출 (실제로는 프록시를 호출)&lt;/li&gt;
&lt;li&gt;프록시는 트랜잭션 시작 (begin transaction)&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;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 AOP 코드 작성해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제로 AOP를 적용해보겠습니다. DAO 메서드 호출 시 로깅을 자동으로 수행하는 AOP를 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Spring Boot 프로젝트에 AOP 의존성을 추가해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로깅을 담당할 Aspect 클래스를 작성합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component // 스프링 빈으로 등록
@Aspect    // Aspect로 선언
@Slf4j     // Lombok의 로깅 기능 사용
public class LoggingAspect {
    
    // DAO 패키지의 모든 메서드 실행 전에 로깅
    @Before(&quot;execution(* com.example.dao..*(..))&quot;)
    public void logBeforeDao(JoinPoint jp) {
        log.info(&quot;DAO 메서드 호출: {} 파라미터: {}&quot;, 
                 jp.getSignature().toShortString(), 
                 Arrays.toString(jp.getArgs()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 com.example.dao 패키지의 모든 메서드가 실행되기 전에 로그가 자동으로 출력됩니다. 비즈니스 로직에는 로깅 코드가 없어도 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pointcut 표현식 작성법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pointcut은 Advice를 적용할 대상을 선택하는 표현식입니다. Spring AOP에서는 다양한 Pointcut 지정자를 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 Pointcut 지정자&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;execution&lt;/b&gt;: 메서드 시그니처를 기반으로 매칭
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;execution(반환타입 패키지.클래스.메서드(파라미터))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;within&lt;/b&gt;: 특정 타입에 속한 모든 메서드 매칭
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;within(com.example.service.*)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bean&lt;/b&gt;: 특정 이름의 빈에 매칭
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;bean(userService)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@annotation&lt;/b&gt;: 특정 애노테이션이 적용된 메서드 매칭
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@annotation(org.springframework.transaction.annotation.Transactional)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;execution 표현식 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execution 표현식은 가장 많이 사용되는 Pointcut 지정자입니다. 몇 가지 예제를 통해 알아보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;execution(* com.example.service.*.*(..)): service 패키지의 모든 클래스의 모든 메서드&lt;/li&gt;
&lt;li&gt;execution(* com.example.service..*(..)): service 패키지 및 하위 패키지의 모든 메서드&lt;/li&gt;
&lt;li&gt;execution(* com.example.service.UserService.find*(..)): UserService의 find로 시작하는 모든 메서드&lt;/li&gt;
&lt;li&gt;execution(* *..*Service.*(..)): 이름이 Service로 끝나는 모든 클래스의 모든 메서드&lt;/li&gt;
&lt;li&gt;execution(void com.example.*.set*(..)): 리턴 타입이 void이고 이름이 set으로 시작하는 모든 메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pointcut 표현식을 정교하게 작성하면 원하는 대상에만 정확히 AOP를 적용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Advice 유형별 활용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 메서드 실행 시점에 따라 다양한 Advice 유형을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Before&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 메서드 실행 전에 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Before(&quot;execution(* com.example.service.UserService.saveUser(..)) &amp;amp;&amp;amp; args(user)&quot;)
public void beforeSaveUser(JoinPoint jp, User user) {
    // 사용자 저장 전 비밀번호 암호화
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    log.info(&quot;사용자 저장 전 비밀번호 암호화 완료&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@AfterReturning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 메서드가 정상적으로 결과를 반환한 후 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@AfterReturning(
    value = &quot;execution(* com.example.service.UserService.getUser(*))&quot;,
    returning = &quot;result&quot;
)
public void afterReturnGetUser(JoinPoint jp, User result) {
    // 개인정보 마스킹 처리
    if (result != null) {
        result.setPhoneNumber(maskPhoneNumber(result.getPhoneNumber()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@AfterThrowing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 메서드가 예외를 던질 때 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@AfterThrowing(
    value = &quot;execution(* com.example.service.*.*(..))&quot;,
    throwing = &quot;ex&quot;
)
public void handleException(JoinPoint jp, Exception ex) {
    log.error(&quot;메서드 {} 실행 중 예외 발생: {}&quot;, 
              jp.getSignature().toShortString(), 
              ex.getMessage());
    // 관리자에게 알림 발송 등의 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@After&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 발생 여부와 상관없이 타겟 메서드 종료 후 항상 동작합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@After(&quot;execution(* com.example.service.ResourceService.*(..))&quot;)
public void releaseResources(JoinPoint jp) {
    log.info(&quot;리소스 정리 작업 수행&quot;);
    // 리소스 정리 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Around&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 강력한 Advice 유형으로, 타겟 메서드 호출을 완전히 제어할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Around(&quot;execution(* com.example.service.ProductService.*(..))&quot;)
public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    long startTime = System.currentTimeMillis();
    
    // 타겟 메서드 호출
    Object result;
    try {
        result = pjp.proceed();
    } catch (Exception e) {
        log.error(&quot;메서드 실행 중 예외 발생&quot;, e);
        throw e;
    }
    
    long endTime = System.currentTimeMillis();
    log.info(&quot;메서드 {} 실행 시간: {}ms&quot;, 
             pjp.getSignature().toShortString(), 
             (endTime - startTime));
    
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Around는 다른 모든 Advice 유형을 대체할 수 있는 가장 유연한 방식이지만, 복잡도가 높아질 수 있으므로 필요한 경우에만 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 활용 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 다양한 실제 상황에서 활용할 수 있습니다. 몇 가지 대표적인 사례를 소개합니다.&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Aspect
@Component
@Slf4j
public class PerformanceAspect {
    
    @Around(&quot;execution(* com.example.service.*.*(..))&quot;)
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long executionTime = System.currentTimeMillis() - start;
        log.info(&quot;{} 실행 시간: {}ms&quot;, joinPoint.getSignature(), executionTime);
        
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 요청/응답 로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API의 요청과 응답을 로깅하여 디버깅에 활용할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Aspect
@Component
@Slf4j
public class ApiLoggingAspect {
    
    @Around(&quot;@within(org.springframework.web.bind.annotation.RestController)&quot;)
    public Object logApiCalls(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        
        log.info(&quot;API 요청: {} {}&quot;, request.getMethod(), request.getRequestURI());
        log.info(&quot;요청 파라미터: {}&quot;, Arrays.toString(joinPoint.getArgs()));
        
        Object result = joinPoint.proceed();
        
        log.info(&quot;API 응답: {}&quot;, result);
        
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 권한 검사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 메서드 호출 시 사용자 권한을 검사하는 로직을 AOP로 구현할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Aspect
@Component
public class SecurityAspect {
    
    @Autowired
    private SecurityService securityService;
    
    @Before(&quot;@annotation(com.example.annotation.RequireAdmin)&quot;)
    public void checkAdminAccess(JoinPoint joinPoint) {
        if (!securityService.isCurrentUserAdmin()) {
            throw new AccessDeniedException(&quot;관리자 권한이 필요합니다.&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 @RequireAdmin 애노테이션을 정의하고, 관리자 권한이 필요한 메서드에 적용하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring 내부의 AOP 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크 자체도 내부적으로 AOP를 활용하여 다양한 기능을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 @Configuration 애노테이션은 하나 이상의 Bean을 포함하고 Bean의 싱글톤을 보장합니다. 이는 CGLIB 기반의 프록시로 처리되기 때문에, @Bean 메서드가 호출될 때 이미 생성된 Bean이 있다면 그것을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤을 보장하는 방법: @Component와의 비교&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@Configuration&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CGLIB 프록시를 사용하여 Bean 메서드를 감싸고 관리합니다.&lt;/li&gt;
&lt;li&gt;Bean 메서드가 여러 번 호출되더라도 Spring 컨테이너에서 이미 생성된 동일한 인스턴스를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Component&lt;/b&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;Bean 메서드가 호출될 때마다 실제 메서드 로직이 그대로 실행되어 새로운 인스턴스가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// @Configuration 사용 시
@Configuration
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(serviceB()); // 여기서 serviceB()는 이미 생성된 빈을 반환
    }
    
    @Bean
    public ServiceB serviceB() {
        System.out.println(&quot;Creating a new ServiceB instance&quot;);
        return new ServiceB();
    }
}

// @Component 사용 시
@Component
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(serviceB()); // 여기서 serviceB()는 새로운 인스턴스 생성
    }
    
    @Bean
    public ServiceB serviceB() {
        System.out.println(&quot;Creating a new ServiceB instance&quot;);
        return new ServiceB();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Transactional&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 예로, @Transactional 애노테이션은 AOP를 통해 트랜잭션 관리를 자동화합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class OrderService {
    
    @Transactional
    public void placeOrder(Order order) {
        // 주문 처리 로직
        // 이 메서드 실행 전후로 트랜잭션 시작/종료가 자동으로 처리됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Async&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드 실행도 AOP를 통해 구현됩니다. 별도의 Thread나 ThreadPool을 직접 생성하지 않고도 비동기 처리가 가능합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class EmailService {
    
    @Async
    public void sendNotificationEmail(String email, String message) {
        // 이메일 전송 로직
        // 이 메서드는 별도의 스레드에서 비동기적으로 실행됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Cacheable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 결과를 캐싱하는 기능도 AOP로 구현되어 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class ProductService {
    
    @Cacheable(&quot;products&quot;)
    public Product getProductById(Long id) {
        // 상품 조회 로직 (DB 조회 등)
        // 결과가 자동으로 캐시되어 동일한 id로 요청 시 DB 조회를 생략
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 개념을 이해하고 나면 코드의 중복을 획기적으로 줄이고 비즈니스 로직의 가독성을 높일 수 있는 강력한 도구입니다. 특히 로깅, 보안, 성능 측정과 같은 공통 관심사를 처리할 때 AOP의 진가가 발휘됩니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>AOP</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/7</guid>
      <comments>https://leve68.tistory.com/entry/Spring-AOP#entry7comment</comments>
      <pubDate>Sat, 26 Apr 2025 12:08:00 +0900</pubDate>
    </item>
    <item>
      <title>SpringBoot</title>
      <link>https://leve68.tistory.com/entry/SpringBoot</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework의 확장인 SpringBoot는 Spring 애플리케이션을 빠르고 쉽게 구성할 수 있도록 도와주는 도구입니다. 복잡한 설정을 간소화하고 개발자가 비즈니스 로직에 집중할 수 있게 해주는 SpringBoot의 주요 특징과 사용법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpringBoot 프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot 애플리케이션은 일관된 디렉토리 구조를 가지고 있어 프로젝트 관리와 개발을 용이하게 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;src/main/java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 자바 코드가 위치하는 디렉토리&lt;/li&gt;
&lt;li&gt;컨트롤러, 서비스, 모델 등 애플리케이션의 핵심 로직이 이곳에 위치&lt;/li&gt;
&lt;li&gt;패키지 구조를 통해 관심사를 분리하고 코드를 체계적으로 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;src/main/resources&lt;/b&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;정적 웹 리소스 (static): CSS, JavaScript, 이미지 등 정적 파일&lt;/li&gt;
&lt;li&gt;동적 웹 리소스 (templates): Thymeleaf, JSP 등 템플릿 파일&lt;/li&gt;
&lt;li&gt;애플리케이션 설정 정보: application.properties 또는 application.yml 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;src/test/java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스팅 관련 소스 코드 및 리소스&lt;/li&gt;
&lt;li&gt;단위 테스트, 통합 테스트를 위한 코드&lt;/li&gt;
&lt;li&gt;테스트 환경 설정 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 Maven 또는 Gradle 빌드 시스템의 표준 레이아웃을 따르며, SpringBoot는 이 구조를 기반으로 자동 설정 기능을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Application 설정 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot에서는 application.properties 또는 application.yml 파일을 통해 애플리케이션의 설정 정보를 관리합니다. 두 형식 모두 같은 기능을 제공하지만, YAML 형식은 계층적 구조를 더 직관적으로 표현할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Properties 파일 작성법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;property와 value는 = 로 구분&lt;/li&gt;
&lt;li&gt;계층 구조는 .(점)을 사용하여 표현&lt;/li&gt;
&lt;li&gt;리스트 값은 쉼표로 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 값 활용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Value(&quot;${property_name}&quot;) 애노테이션을 사용해 자바 코드에서 설정 값 참조&lt;/li&gt;
&lt;li&gt;설정된 스칼라 값을 자동으로 적절한 타입으로 변환 지원&lt;/li&gt;
&lt;li&gt;리스트, 맵 등의 복합 데이터 타입도 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Built-in Properties&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot는 기본 설정 값을 제공하며, 필요에 따라 재정의할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;server.port: 서버 포트 설정 (기본값: 8080)&lt;/li&gt;
&lt;li&gt;spring.datasource.*: 데이터베이스 연결 설정&lt;/li&gt;
&lt;li&gt;logging.level.*: 패키지별 로깅 레벨 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: logging.level.org.springframework=DEBUG&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 예제&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@SpringBootTest
class Fw03ApplicationTests {
	
	private final Logger log = LoggerFactory.getLogger(Fw03ApplicationTests.class);

	@Value(&quot;${service.manager.name}&quot;)
	private String managerName;
	
	@Value(&quot;${service.manager.age}&quot;)
	private int managerAge;
	
	@Value(&quot;${service.roles}&quot;)
	private List&amp;lt;String&amp;gt; roles;
	
	@Test
	void contextLoads() {
		Assertions.assertEquals(managerAge, 30);
		Assertions.assertEquals(managerName, &quot;hong&quot;);
		log.debug(&quot;debug: {}&quot;, roles);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에 대응하는 application.properties&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.application.name=FW_03
service.manager.name=hong
service.manager.age=30
service.roles=admin, manager, guest

logging.level.com.ssafy=trace
logging.pattern.console=%msg%n
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pom.xml&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven 기반 SpringBoot 프로젝트의 의존성 관리는 pom.xml 파일을 통해 이루어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBoot 의존성 관리 시스템의 가장 큰 특징은 &lt;b&gt;Spring에서 관리되는 라이브러리는 버전이 생략&lt;/b&gt;된다는 점입니다.&lt;/li&gt;
&lt;li&gt;spring-boot-starter-parent를 상속받음으로써 적절한 버전의 라이브러리 자동 선택&lt;/li&gt;
&lt;li&gt;필요한 기능별로 스타터 패키지를 추가하여 관련 의존성을 한번에 가져올 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring-boot-starter-web: 웹 애플리케이션 개발 시 필요한 의존성&lt;/li&gt;
&lt;li&gt;spring-boot-starter-data-jpa: JPA를 이용한 데이터 접근 시 필요한 의존성&lt;/li&gt;
&lt;li&gt;spring-boot-starter-security: 보안 관련 기능 구현 시 필요한 의존성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스타터 의존성의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 라이브러리들의 호환되는 버전을 자동으로 관리&lt;/li&gt;
&lt;li&gt;빌드 설정 간소화&lt;/li&gt;
&lt;li&gt;검증된 라이브러리 조합으로 안정성 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@SpringBootApplication&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SpringBootApplication은 SpringBoot 애플리케이션의 시작점인 클래스에 선언하는 애노테이션으로, 내부적으로 세 가지 중요한 애노테이션을 포함하고 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@SpringBootConfiguration&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부적으로 @Configuration을 가짐&lt;/li&gt;
&lt;li&gt;자바 기반 설정 클래스임을 나타냄&lt;/li&gt;
&lt;li&gt;빈 등록을 처리할 수 있는 근거가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@EnableAutoConfiguration&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBoot의 핵심 기능인 자동 설정 활성화&lt;/li&gt;
&lt;li&gt;의존성에 근거하여 필요한 빈들을 자동으로 구성&lt;/li&gt;
&lt;li&gt;예: spring-boot-starter-web 의존성이 있으면 Tomcat과 Spring MVC 관련 설정 자동 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ComponentScan&lt;/b&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;@Component, @Service, @Repository, @Controller 등이 붙은 클래스를 찾아 빈으로 등록&lt;/li&gt;
&lt;li&gt;@SpringBootApplication이 선언된 클래스의 하위 패키지를 자동으로 스캔&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단한 SpringBoot 애플리케이션 예제&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 간단한 코드만으로도 완전한 웹 애플리케이션을 구동할 수 있습니다. 내부적으로 SpringBoot는 다음과 같은 작업을 수행합니다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클래스패스에 있는 라이브러리를 기반으로 필요한 빈을 자동 구성&lt;/li&gt;
&lt;li&gt;내장 서버(기본값: Tomcat) 실행&lt;/li&gt;
&lt;li&gt;애플리케이션 컨텍스트 초기화&lt;/li&gt;
&lt;li&gt;컴포넌트 스캔을 통한 빈 등록&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@SpringBootTest&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SpringBootTest는 스프링 애플리케이션의 통합 테스트를 위한 애노테이션입니다. 테스트 시 전체 애플리케이션 컨텍스트를 로드하여 실제 환경과 유사한 조건에서 테스트를 수행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 애플리케이션 컨텍스트 로드 지원&lt;/li&gt;
&lt;li&gt;실제 애플리케이션과 동일한 환경에서 테스트 가능&lt;/li&gt;
&lt;li&gt;테스트를 위한 별도의 설정 제공 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slice Test 권장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 애플리케이션 컨텍스트를 로드하는 것은 테스트 시간이 오래 걸릴 수 있으므로, 필요한 모듈만 로딩하는 Slice Test를 권장합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@WebMvcTest: 컨트롤러 레이어 테스트&lt;/li&gt;
&lt;li&gt;@DataJpaTest: JPA 관련 컴포넌트 테스트&lt;/li&gt;
&lt;li&gt;@JsonTest: JSON 직렬화/역직렬화 테스트&lt;/li&gt;
&lt;li&gt;@RestClientTest: REST 클라이언트 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 슬라이스 테스트를 활용하면 테스트 실행 속도를 높이고 특정 레이어에 집중하여 테스트할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring HRM (HTTP Request Management)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot는 HTTP 요청을 처리하기 위한 다양한 기능을 제공합니다. 이를 통해 웹 애플리케이션의 요청-응답 흐름을 효율적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bean의 Scope&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너가 빈 객체를 어떻게 생성하고 언제까지 관리할 것인지 결정하는 것을 Bean의 Scope라고 합니다. 적절한 스코프 선택은 애플리케이션의 성능과 메모리 사용에 영향을 미칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Singleton Scope&lt;/b&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;상태를 유지하지 않는(Stateless) 서비스나 DAO에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Prototype Scope&lt;/b&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;상태를 가지는(Stateful) 객체에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Request Scope &amp;amp; Session Scope&lt;/b&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;Request Scope: HTTP 요청이 유지되는 동안 존재하는 빈&lt;/li&gt;
&lt;li&gt;Session Scope: HTTP 세션이 유지되는 동안 존재하는 빈&lt;/li&gt;
&lt;li&gt;웹 애플리케이션에서 사용자별 데이터를 관리할 때 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프 지정은 @Scope 애노테이션을 통해 할 수 있습니다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Scope(&quot;prototype&quot;)
public class PrototypeBeanExample {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lombok&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lombok은 자바 개발 시 반복적인 코드(getter, setter, 생성자 등)를 애노테이션을 통해 자동 생성해주는 라이브러리입니다. SpringBoot와 함께 사용하면 보일러플레이트 코드를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DTO 자동 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@Data&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 모두 포함&lt;/li&gt;
&lt;li&gt;많은 코드를 한번에 생성 &amp;rarr; 생각지 못한 문제 발생 가능성 있음&lt;/li&gt;
&lt;li&gt;@ToString에서 순환 참조 문제가 발생할 수 있으므로 주의 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Builder&lt;/b&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;기본값을 유지하기 위해 @Builder.Default 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lombok 예제&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
@Builder
public class Member {
    private int mno;
    private @NonNull String name;
    private @NonNull String email;
    private @NonNull String password;
    private String role;
    
    @ToString.Exclude  // toString() 메서드에서 제외
    @EqualsAndHashCode.Exclude  // equals(), hashCode() 메서드에서 제외
    @Builder.Default   // 빌더 패턴 사용 시 기본값 유지
    private List&amp;lt;Address&amp;gt; addresses = new ArrayList&amp;lt;&amp;gt;();
}
&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;jboss-cli&quot;&gt;&lt;code&gt;Member member = Member.builder()
                     .mno(mno)
                     .name(name)
                     .email(email)
                     .password(pass)
                     .role(role)
                     .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC 패턴 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot에서는 MVC(Model-View-Controller) 패턴을 구현하기 위한 다양한 컴포넌트를 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAO (Data Access Object)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO는 데이터 접근 로직을 캡슐화하는 객체로, 데이터베이스 연산을 담당합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Stereotype Annotation 적용&lt;/b&gt;: @Repository&lt;/li&gt;
&lt;li&gt;JPA를 사용할 경우 JpaRepository 인터페이스를 상속받아 간편하게 구현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    List&amp;lt;User&amp;gt; findByLastName(String lastName);
    Optional&amp;lt;User&amp;gt; findByEmail(String email);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Service&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층은 비즈니스 로직을 담당하며, DAO를 통해 데이터에 접근합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Stereotype Annotation 적용&lt;/b&gt;: @Service&lt;/li&gt;
&lt;li&gt;의존성 주입을 통해 필요한 컴포넌트 사용&lt;/li&gt;
&lt;li&gt;@RequiredArgsConstructor 활용으로 final 필드 기반 생성자 자동 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BasicAddressService implements AddressService {

    private final AddressDao dao;
    private final DBUtil util;
    
    @Override
    public List&amp;lt;Address&amp;gt; findByUserId(long userId) {
        // 비즈니스 로직 구현
        return dao.findByUserId(userId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller&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;@RequiredArgsConstructor를 사용하여 코드 간소화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@WebServlet(urlPatterns = &quot;/auth&quot;)
@SuppressWarnings(&quot;serial&quot;)
@RequiredArgsConstructor
public class AuthController extends HttpServlet implements ControllerHelper {

    private final MemberService mService;
    private final AddressService aService;
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 요청 처리 로직
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API 개발 시에는 @RestController 애노테이션을 사용하는 것이 더 일반적입니다&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/users&quot;)
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    @GetMapping
    public List&amp;lt;UserDto&amp;gt; getAllUsers() {
        return userService.findAll();
    }
    
    @PostMapping
    public ResponseEntity&amp;lt;UserDto&amp;gt; createUser(@RequestBody UserDto userDto) {
        UserDto created = userService.createUser(userDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}
&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;SpringBoot는 Spring Framework의 복잡성을 크게 줄이고, 개발자가 비즈니스 로직에 집중할 수 있도록 도와줍니다. 자동 설정, 내장 서버, 간편한 의존성 관리 등의 기능을 통해 애플리케이션 개발 생산성을 크게 향상시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Lombok과 같은 도구를 함께 사용하면 반복적인 코드를 최소화하고, MVC 패턴을 효율적으로 구현할 수 있습니다. SpringBoot의 이러한 특징들은 현대적인 Java 웹 애플리케이션 개발에 있어 필수적인 요소로 자리 잡았습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/6</guid>
      <comments>https://leve68.tistory.com/entry/SpringBoot#entry6comment</comments>
      <pubDate>Thu, 24 Apr 2025 20:33:00 +0900</pubDate>
    </item>
    <item>
      <title>SLF4J와 JUnit</title>
      <link>https://leve68.tistory.com/entry/SLF4J%EC%99%80-JUnit</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SLF4J &amp;amp; 로깅(Logging)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅은 디버깅의 한계를 극복하고 장기간 동작에 대한 기록을 남기는 데 사용됩니다. 디버깅만으로는 파악하기 어려운 문제나 실행 중인 애플리케이션의 상태를 모니터링하는 데 큰 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SLF4J (Simple Logging Facade for Java)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SLF4J는 Java에서 로깅을 위한 파사드(facade) 패턴을 구현한 라이브러리입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파사드 패턴 | &lt;/b&gt;라이브러리의 복잡한 구조를 단순화해 인터페이스로 제공하는 패턴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SLF4J 구현체 | &lt;/b&gt;log4j, logback 등이 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SLF4J의 주요 특징&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;심각도에 따른 로그 레벨&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;trace &amp;lt; debug &amp;lt; info &amp;lt; warn &amp;lt; error&lt;/b&gt; (낮은 순에서 높은 순)&lt;/li&gt;
&lt;li&gt;사용자 설정에 따른 로그 레벨 결정&lt;/li&gt;
&lt;li&gt;개발 과정과 운영 과정을 분리할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그 레이아웃 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Log Layout&lt;/b&gt;: 로그 메시지가 출력되는 형식&lt;/li&gt;
&lt;li&gt;시간, 스레드, 클래스명, 라인 번호 등 다양한 정보를 포함할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다양한 Appender 제공&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Appender&lt;/b&gt;: 로그 메시지를 어디에 기록할지 결정&lt;/li&gt;
&lt;li&gt;콘솔, 파일, 데이터베이스 등 다양한 출력 대상 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Logback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 logback을 SLF4J의 기본 구현체로 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 로깅 코드 예시&lt;/h4&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;private static final Logger log = LoggerFactory.getLogger(LoggerTest.class);

public class LoggerTest {
    log.trace(&quot;trace: {}&quot;, &quot;trace level&quot;);
    log.debug(&quot;debug: {}&quot;, &quot;debug level&quot;);
    log.info(&quot;info: {}&quot;, &quot;info level&quot;);
    log.warn(&quot;warn: {}&quot;, &quot;warn level&quot;);
    log.error(&quot;error: {}&quot;, &quot;error level&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일반적인 출력 결과&lt;/h4&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;21:44:48.342 [main] DEBUG com.ssafy.live.LoggerTest -- debug: debug level
21:44:48.343 [main] INFO com.ssafy.live.LoggerTest -- info: info level
21:44:48.343 [main] WARN com.ssafy.live.LoggerTest -- warn: warn level
21:44:48.343 [main] ERROR com.ssafy.live.LoggerTest -- error: error level
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 출력에서 trace 레벨 로그가 보이지 않는 이유는 기본 로그 레벨 설정이 debug 이상으로 되어 있기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Logback 설정 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 콘솔 로깅 기본 설정&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration&amp;gt;

&amp;lt;configuration&amp;gt;
    &amp;lt;import class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;/&amp;gt;
    &amp;lt;import class=&quot;ch.qos.logback.core.ConsoleAppender&quot;/&amp;gt;
    
    &amp;lt;appender name=&quot;STDOUT&quot; class=&quot;ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder class=&quot;PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;pattern&amp;gt;%d{HH:mm:ss} [%thread] [%-5level] %C{1}.%M.%L - %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;
    
    &amp;lt;root level=&quot;trace&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;STDOUT&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&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;루트 로거의 레벨을 trace로 설정하여 모든 로그 출력&lt;/li&gt;
&lt;li&gt;콘솔에 로그를 출력하는 STDOUT 애펜더 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 파일 로깅 추가 설정&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;import class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot; /&amp;gt;
&amp;lt;import class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot; /&amp;gt;

&amp;lt;appender name=&quot;FILE&quot; class=&quot;RollingFileAppender&quot;&amp;gt;
    &amp;lt;file&amp;gt;logFile.log&amp;lt;/file&amp;gt;
    
    &amp;lt;rollingPolicy class=&quot;TimeBasedRollingPolicy&quot;&amp;gt;
        &amp;lt;fileNamePattern&amp;gt;logFile.%d{yyyy-MM-dd}.log&amp;lt;/fileNamePattern&amp;gt;
        &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
        &amp;lt;totalSizeCap&amp;gt;3GB&amp;lt;/totalSizeCap&amp;gt;
    &amp;lt;/rollingPolicy&amp;gt;
    
    &amp;lt;encoder class=&quot;PatternLayoutEncoder&quot;&amp;gt;
        &amp;lt;pattern&amp;gt;%d{HH:mm:ss.SSS} %-4relative [%thread] %-5level %logger{35} -%kvp- %msg%n&amp;lt;/pattern&amp;gt;
    &amp;lt;/encoder&amp;gt;
&amp;lt;/appender&amp;gt;

&amp;lt;root level=&quot;trace&quot;&amp;gt;
    &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;appender-ref ref=&quot;FILE&quot; /&amp;gt;
&amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&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;기본 로그 파일명 지정 (logFile.log)&lt;/li&gt;
&lt;li&gt;일자별 로그 파일 생성 (logFile.yyyy-MM-dd.log)&lt;/li&gt;
&lt;li&gt;최대 30일치 로그 파일 보관&lt;/li&gt;
&lt;li&gt;전체 로그 파일 용량 제한 (3GB)&lt;/li&gt;
&lt;li&gt;콘솔과 파일 모두에 로그 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그 레벨 설정 후 출력 결과&lt;/h4&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;21:44:48.341 [main] TRACE com.ssafy.live.LoggerTest -- trace: trace level
21:44:48.342 [main] DEBUG com.ssafy.live.LoggerTest -- debug: debug level
21:44:48.343 [main] INFO com.ssafy.live.LoggerTest -- info: info level
21:44:48.343 [main] WARN com.ssafy.live.LoggerTest -- warn: warn level
21:44:48.343 [main] ERROR com.ssafy.live.LoggerTest -- error: error level
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 레벨을 trace로 설정한 후에는 모든 레벨의 로그가 정상적으로 출력됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JUnit - 자바 테스트 프레임워크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit은 Java 코드의 단위 테스트 자동화를 위한 프레임워크입니다. 코드의 품질을 보장하고 리팩토링 과정에서 발생할 수 있는 문제를 사전에 발견하는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 테스트 코드 예시&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CalculatorJUnitTest {
    private Calculator calc;
    
    @BeforeEach
    public void setup() {
        log.debug(&quot;@BeforeEach - 개별 테스트에서 필요한 리소스 초기화&quot;);
        calc = new Calculator();
    }
    
    // @BeforeAll, @AfterAll은 static이어야 함
    @BeforeAll
    public static void setupAll() {
        log.debug(&quot;@BeforeAll - 모든 테스트에서 공통적으로 사용할 리소스 초기화&quot;);
    }
    
    @Test
    @DisplayName(&quot;두 수를 더해서 나오는 합을 검증: ex) 1 + 2 = 3&quot;)
    public void 두_수를_더해서_나오는_합을_검증() {
        int result = calc.add(10, 20);
        Assertions.assertEquals(30, result);
    }
}&lt;/code&gt;&lt;/pre&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;@BeforeEach: 각 테스트 메소드 실행 전에 호출되어 리소스를 초기화&lt;/li&gt;
&lt;li&gt;@BeforeAll: 모든 테스트 실행 전에 한 번만 호출 (static 메소드여야 함)&lt;/li&gt;
&lt;li&gt;@Test: 실제 테스트를 수행할 메소드 지정&lt;/li&gt;
&lt;li&gt;@DisplayName: 테스트에 표시될 이름 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Assertion (단정문)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단정문은 메서드 호출 결과가 예상한 값과 동일한지 판단하는 문장입니다. JUnit에서는 테스트 검증을 위한 다양한 Assertion 메서드를 제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Assertion 메서드의 주요 유형&lt;/h4&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;JUnit은 기본적인 값 비교를 위한 다양한 메서드를 제공합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;assertEquals(expected, actual): 두 값이 같은지 검증합니다&lt;/li&gt;
&lt;li&gt;assertNotEquals(unexpected, actual): 두 값이 다른지 검증합니다&lt;/li&gt;
&lt;li&gt;assertSame(expected, actual): 두 객체의 참조가 동일한지 검증합니다&lt;/li&gt;
&lt;li&gt;assertNotSame(unexpected, actual): 두 객체의 참조가 다른지 검증합니다&lt;/li&gt;
&lt;li&gt;assertTrue(condition): 조건이 참인지 검증합니다&lt;/li&gt;
&lt;li&gt;assertFalse(condition): 조건이 거짓인지 검증합니다&lt;/li&gt;
&lt;li&gt;assertNull(actual): 객체가 null인지 검증합니다&lt;/li&gt;
&lt;li&gt;assertNotNull(actual): 객체가 null이 아닌지 검증합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 배열 및 컬렉션 관련 메서드&lt;/b&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;assertArrayEquals(expectedArray, actualArray): 두 배열의 내용이 같은지 검증합니다&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;assertThrows(expectedType, executable): 특정 타입의 예외가 발생하는지 검증합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 그룹 검증 메서드&lt;/b&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;assertAll(executables...): 여러 검증을 그룹으로 실행하고 모든 실패를 한 번에 보고합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Assertion 메서드 활용 예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit의 Assertion 메서드는 다양한 테스트 시나리오에 활용할 수 있습니다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void testMultipleAssertions() {
    // 예외 검증
    Exception exception = assertThrows(ArithmeticException.class, () -&amp;gt; {
        int result = 10 / 0;
    });
    
    // 여러 검증을 그룹으로 묶기
    assertAll(&quot;사용자 정보 검증&quot;,
        () -&amp;gt; assertEquals(&quot;John&quot;, user.getFirstName()),
        () -&amp;gt; assertEquals(&quot;Doe&quot;, user.getLastName()),
        () -&amp;gt; assertEquals(30, user.getAge())
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assertion 메서드를 적절히 활용하면 테스트 의도를 명확히 표현하고, 실패 시 유용한 피드백을 얻을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Given-When-Then 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDD(Behavior Driven Development)에서 권장되는 패턴으로, 테스트 코드의 가독성을 높여줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Given | &lt;/b&gt;테스트를 위해 필요한 상황 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;When | &lt;/b&gt;테스트하는 메서드 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Then&amp;nbsp; | &lt;/b&gt;테스트 결과 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void userRegistrationTest() {
    // Given: 테스트에 필요한 조건 설정
    String username = &quot;testuser&quot;;
    String email = &quot;test@example.com&quot;;
    String password = &quot;password123&quot;;
    UserService userService = new UserService();
    
    // When: 테스트하려는 기능 실행
    User createdUser = userService.registerUser(username, email, password);
    
    // Then: 결과 검증
    assertNotNull(createdUser);
    assertEquals(username, createdUser.getUsername());
    assertEquals(email, createdUser.getEmail());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FIRST 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드 작성 시 따라야 할 FIRST 원칙은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Fast&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 빠르게 동작해야 합니다&lt;/li&gt;
&lt;li&gt;목적을 단순화하고 외부 환경과 무관하게 작성해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Independent&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 서로 독립적이어야 합니다&lt;/li&gt;
&lt;li&gt;이전 테스트의 상태에 의존하면 안 됩니다&lt;/li&gt;
&lt;li&gt;어떤 순서로 테스트하더라도 언제나 성공해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Repeatable&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 환경에 상관없이 반복 실행 가능해야 합니다&lt;/li&gt;
&lt;li&gt;반복해서 테스트를 진행하더라도 동일하게 동작해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Self-Validating&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 스스로 결과를 검증할 수 있어야 합니다&lt;/li&gt;
&lt;li&gt;테스트 자체만으로 검증이 완료되어야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Timely&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TDD의 경우 실제 코드 이전에 테스트 코드가 작성되어야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;SLF4J와 JUnit은 Java 개발, 특히 Spring 애플리케이션 개발에서 필수적인 도구입니다. 로깅을 통해 애플리케이션의 동작을 모니터링하고, 테스트를 통해 코드의 안정성을 보장할 수 있습니다. 이 두 도구를 효과적으로 활용하면 더 안정적이고 유지보수하기 쉬운 애플리케이션을 개발할 수 있습니다.&lt;/p&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/5</guid>
      <comments>https://leve68.tistory.com/entry/SLF4J%EC%99%80-JUnit#entry5comment</comments>
      <pubDate>Thu, 24 Apr 2025 00:12:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring Framework 개요</title>
      <link>https://leve68.tistory.com/entry/Spring-Framework-%EA%B0%9C%EC%9A%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프레임워크란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 &lt;b&gt;Frame을 가지고 하는 작업 또는 구조물의 뼈대&lt;/b&gt;입니다. 소프트웨어 측면에서는 비즈니스 로직이 빠진 뼈대만 갖춰진 반제품 형태의 애플리케이션이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 이 뼈대 위에 필요한 기능을 구현하기만 하면 되므로, 기본적인 구조를 매번 처음부터 만들 필요가 없습니다. 이는 마치 집을 지을 때 기초 공사와 골조가 이미 완성된 상태에서 내부 인테리어만 진행하는 것과 비슷합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이브러리 vs 프레임워크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이브러리&lt;/b&gt;는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재사용 가능한 코드(함수, 클래스 등)의 모음&lt;/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;예를 들어, Java의 Apache Commons나 JavaScript의 jQuery는 개발자가 직접 불러와서 사용하는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;프레임워크&lt;/b&gt;는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적을 가진 라이브러리들이 유기적으로 연결된 인프라&lt;/li&gt;
&lt;li&gt;애플리케이션의 기본 구조와 규칙을 제공&lt;/li&gt;
&lt;li&gt;제어 흐름이 프레임워크에 있음 (프레임워크가 개발자의 코드를 호출)&lt;/li&gt;
&lt;li&gt;&quot;제어의 역전(IoC)&quot; 개념이 적용됨&lt;/li&gt;
&lt;li&gt;특정 문제 도메인을 해결하기 위한 전체적인 아키텍처 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이점은 '누가 누구를 호출하는가'입니다. 라이브러리에서는 개발자의 코드가 라이브러리를 호출하지만, 프레임워크에서는 프레임워크가 개발자의 코드를 호출합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Framework&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Framework&lt;/b&gt;는 자바 애플리케이션 개발을 위한 경량 프레임워크입니다. 다음과 같은 특징이 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링은 개발자를 위한 도구입니다&lt;/li&gt;
&lt;li&gt;처음 접근할 때는 원리보다는 사용법 위주로 배우는 것이 좋습니다&lt;/li&gt;
&lt;li&gt;기능들이 모듈화되어 있어 필요한 의존성만 가져올 수 있습니다&lt;/li&gt;
&lt;li&gt;다른 프레임워크와 연동 지원이 잘 되어 교체가 쉽습니다&lt;/li&gt;
&lt;li&gt;그 자체로 실행 가능한 Product가 아닙니다 (마치 밀키트와 같습니다)&lt;/li&gt;
&lt;/ul&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;b&gt;Spring Core&lt;/b&gt;: 기본 IoC 컨테이너와 DI 기능 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring MVC&lt;/b&gt;: 웹 애플리케이션 개발을 위한 모델-뷰-컨트롤러 아키텍처 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Data&lt;/b&gt;: 데이터 액세스 계층 추상화 및 단순화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Security&lt;/b&gt;: 인증과 권한 부여 기능 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring AOP&lt;/b&gt;: 관점 지향 프로그래밍 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Boot&lt;/b&gt;는 스프링 애플리케이션을 개발하는 일종의 템플릿입니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 자동화를 통한 개발 속도 향상&lt;/li&gt;
&lt;li&gt;단위 테스트 강화로 프로젝트 안정성 강화&lt;/li&gt;
&lt;li&gt;Spring 프레임워크의 보일러플레이트 코드를 제거함(노출하지 않음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 &quot;약속된 규칙(Convention over Configuration)&quot;을 기반으로 합니다. 개발자가 명시적으로 설정하지 않아도 기본값으로 적절한 설정을 제공합니다. 예를 들어, 웹 애플리케이션을 개발할 때 Tomcat 서버가 자동으로 내장되어 별도의 서버 설정이 필요 없습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 스프링 부트 애플리케이션 예시
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 몇 줄의 코드만으로 웹 애플리케이션을 실행할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Framework의 핵심 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 &lt;b&gt;POJO&lt;/b&gt; 기반의 &lt;b&gt;DI&lt;/b&gt;, &lt;b&gt;AOP&lt;/b&gt;, &lt;b&gt;PSA&lt;/b&gt;를 핵심 특징으로 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DI (Dependency Injection : 의존성 주입)&lt;/h3&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;b&gt;코드의 유지 보수성 향상&lt;/b&gt;에 큰 도움이 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 주입은 객체 간의 결합도를 낮추어 유연하고 테스트 가능한 코드를 작성할 수 있게 합니다. 스프링에서는 주로 생성자 주입, 필드 주입, 세터 주입의 세 가지 방식이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 의존성 주입을 사용하면 다음과 같이 코드를 작성할 수 있습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// DI 사용 전
public class UserService {
    private UserRepository userRepository = new UserRepositoryImpl();
}

// DI 사용 후 (생성자 주입)
@Service
public class UserService {
    private final UserRepository userRepository;
    
    @Autowired // 스프링 4.3부터는 단일 생성자의 경우 생략 가능
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AOP (Aspect Oriented Programming : 관점 지향 프로그래밍)&lt;/h3&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;b&gt;핵심 관심사에 집중&lt;/b&gt;할 수 있게 해줍니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅, 트랜잭션 관리, 보안과 같은 공통 기능들은 여러 클래스에 걸쳐 반복적으로 나타납니다. AOP는 이러한 횡단 관심사(cross-cutting concerns)를 분리하여 코드 중복을 줄이고 모듈성을 향상시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP의 실제 예시:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// AOP를 사용하여 메소드 실행 시간 측정
@Aspect
@Component
public class PerformanceAspect {
    
    @Around(&quot;execution(* com.example.service.*.*(..))&quot;)
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long executionTime = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + &quot; 실행 시간: &quot; + executionTime + &quot;ms&quot;);
        
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PSA (Portable Service Abstraction : 이식 가능한 서비스 추상화)&lt;/h3&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;b&gt;툴에 종속되지 않는&lt;/b&gt; 장점이 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PSA는 기술 구현체에 상관없이 일관된 방식으로 서비스를 이용할 수 있게 해줍니다. 예를 들어, 데이터베이스 접근 방식이 JDBC에서 JPA로 변경되더라도 비즈니스 로직은 수정할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PSA의 대표적인 예:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 트랜잭션 관리를 위한 PSA
@Service
public class PaymentService {
    
    @Transactional
    public void processPayment(Order order) {
        // 트랜잭션 로직...
        // 내부적으로 어떤 트랜잭션 관리자가 사용되는지 몰라도 됩니다
        // JPA, JDBC, Hibernate 등 어떤 기술을 사용하든 동일한 방식으로 작성
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;POJO (Plain Old Java Object : 평범한 옛날 자바 객체)&lt;/h3&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;b&gt;가독성, 유지보수성이 뛰어나고&lt;/b&gt; &lt;b&gt;단위 테스트가 용이&lt;/b&gt;합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POJO 기반 개발은 과거 EJB(Enterprise JavaBeans)와 같은 무거운 프레임워크의 복잡성에 대한 반발로 등장했습니다. 스프링은 POJO 프로그래밍 모델을 통해 비즈니스 로직에 집중할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POJO 예시:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 단순한 POJO 클래스
public class User {
    private String username;
    private String email;
    
    // 기본 생성자
    public User() {}
    
    // 게터와 세터
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 애플리케이션&lt;/b&gt;: Spring MVC와 Spring Boot를 사용한 RESTful API 및 웹 서비스 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로서비스&lt;/b&gt;: 독립적으로 배포 가능한 작은 서비스들로 구성된 아키텍처 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 처리&lt;/b&gt;: Spring Data를 활용한 다양한 데이터 저장소(RDB, NoSQL 등) 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 처리&lt;/b&gt;: Spring Batch를 이용한 대용량 데이터 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라우드 애플리케이션&lt;/b&gt;: Spring Cloud를 활용한 클라우드 네이티브 서비스 개발&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>BackEnd/Spring Framework</category>
      <category>Spring</category>
      <category>SSAFY</category>
      <author>leve68</author>
      <guid isPermaLink="true">https://leve68.tistory.com/4</guid>
      <comments>https://leve68.tistory.com/entry/Spring-Framework-%EA%B0%9C%EC%9A%94#entry4comment</comments>
      <pubDate>Wed, 23 Apr 2025 20:51:31 +0900</pubDate>
    </item>
  </channel>
</rss>