<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>taecostudy76 님의 블로그</title>
    <link>https://taecostudy76.tistory.com/</link>
    <description>taecostudy76 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 23 Jul 2026 23:22:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>taecostudy76</managingEditor>
    <item>
      <title>  [Spring Boot] IoT 헬스케어 시스템의 실시간 데이터 파이프라인 구축기 (SSE + 데이터 샘플링)</title>
      <link>https://taecostudy76.tistory.com/27</link>
      <description>&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;  1. 들어가며: 쏟아지는 센서 데이터, 어떻게 감당할 것인가?&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;현재 라즈베리파이를 활용한 '실시간 피보호자 헬스케어 모니터링 시스템'의 백엔드를 개발 중이다. 센서로부터 심박수, 호흡수, 낙상 여부 등의 생체 데이터가 1초 단위로 서버에 전송되고 있는데, 여기서 큰 고민에 빠졌다.&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;6&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;6,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0&quot;&gt;&quot;1초마다 들어오는 데이터를 그대로 DB에 save() 하면, 유저가 늘어날수록 DB I/O 부하가 폭발하지 않을까?&quot;&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;하지만 보호자 앱에서는 &lt;b data-index-in-node=&quot;13&quot; data-path-to-node=&quot;7&quot;&gt;1. 1초 단위의 실시간 그래프&lt;/b&gt;를 봐야 하고, &lt;b data-index-in-node=&quot;39&quot; data-path-to-node=&quot;7&quot;&gt;2. 긴급 상황 발생 시 즉시 알림&lt;/b&gt;이 가야 한다. 이 딜레마를 해결하기 위해 데이터의 목적에 따라 처리 방식을 나누는 '3-Track 데이터 파이프라인'을 설계하고 적용한 과정을 기록해 본다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size26&quot;&gt;  2. 아키텍처 설계: 3-Track 파이프라인&lt;/h2&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;단순히 Controller -&amp;gt; Service -&amp;gt; DB 저장으로 이어지던 일차원적인 로직을, 중간에 VitalService를 '총괄 매니저'로 두어 3갈래로 데이터를 분배하도록 개선했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;Track A (실시간 모니터링):&lt;/b&gt; 1초마다 들어오는 데이터는 DB를 거치지 않고, 가공(DTO 변환) 후 곧바로 SseService를 통해 프론트엔드로 전송한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;Track B (블랙박스/긴급 상황):&lt;/b&gt; 심박수 비정상이나 낙상이 감지되면, 증거 보존을 위해 즉시 EmergencyEvent 테이블에 저장하고 알림을 발생시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;Track C (일반 데이터 샘플링):&lt;/b&gt; 가장 부하가 심한 일반 생체 데이터(Vital 테이블)는 &lt;b data-index-in-node=&quot;54&quot; data-path-to-node=&quot;11,2,0&quot;&gt;10초 주기로 1개씩만 솎아내어(Sampling)&lt;/b&gt; 저장한다. 이를 통해 DB 용량과 부하를 1/10 수준으로 최적화했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-path-to-node=&quot;12&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size26&quot;&gt;  3. 핵심 구현: 동시성을 고려한 10초 샘플링 로직&lt;/h2&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;가장 핵심이 되는 '10초 샘플링' 로직을 구현할 때, 단순히 서버에 long lastTime 변수 하나를 두면 여러 기기에서 동시에 데이터가 들어올 때 초시계가 꼬이는 문제(동시성 문제)가 발생한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 ConcurrentHashMap을 사용하여 '기기별 독립적인 초시계 장부'를 만들었다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;  트래픽 최적화를 적용한 코드 (VitalService.java)&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjx5Ynl1auUAxUAAAAAHQAAAAAQpwE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class VitalService {

    // ... (의존성 주입 생략)

    //   기기별로 마지막 저장 시간을 기록하는 스레드 안전한 Map 장부
    private final Map&amp;lt;String, Long&amp;gt; lastSavedTimeMap = new ConcurrentHashMap&amp;lt;&amp;gt;();

    @Transactional
    public void getVitalData(VitalRequestDto vitalDto) {
        // 1. 유저 및 기기 확인
        Device device = deviceRepository.findBySerialNum(vitalDto.serialNum()).orElseThrow(...);
        Users user = device.getUser();

        // 2. 긴급 상황 검사 (Track B)
        boolean isFallEvent = emergencyEventService.checkFallEvent(vitalDto, user, device);
        boolean isHREvent = emergencyEventService.checkHeartBreathEvent(vitalDto, user, device);
        boolean isEmergency = isFallEvent || isHREvent; // 하나라도 발생하면 긴급!

        // 3. 실시간 SSE 전송 (Track A)
        String statusStr = isEmergency ? &quot;위험&quot; : &quot;정상&quot;;
        ResponseUserVital sseData = new ResponseUserVital(user.getId(), vitalDto.heartRate(), vitalDto.breathRate(), statusStr);
        sseService.sendVitalDataToClient(user.getId(), sseData);

        // 4. 10초 샘플링 DB 저장 (Track C)
        long currentTime = System.currentTimeMillis();
        long lastSavedTime = lastSavedTimeMap.getOrDefault(vitalDto.serialNum(), 0L);

        // 마지막 저장 시점으로부터 10초(10,000ms)가 지났을 때만 DB에 Insert
        if (currentTime - lastSavedTime &amp;gt;= 10000) {
            Vital newVital = Vital.builder()
                .user(user)
                .device(device)
                .heartRate(vitalDto.heartRate())
                .breathRate(vitalDto.breathRate())
                .isFallDetected(isEmergency)
                .isPresent(vitalDto.isPresent())
                .build();
            
            vitalRepository.save(newVital);
            
            //   [주의] 저장 후 반드시 장부의 시간을 현재 시간으로 갱신해야 함!
            lastSavedTimeMap.put(vitalDto.serialNum(), currentTime);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size26&quot;&gt;  4. 오늘 마주친 트러블슈팅 &amp;amp; 배운 점&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size23&quot;&gt;  1. DTO와 Entity의 엄격한 분리&lt;/h3&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;초기에는 라즈베리파이에서 받은 데이터 형식과 프론트로 보내는 형식이 혼재될 뻔했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;22&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,0,0&quot;&gt;외부 통신(택배 상자):&lt;/b&gt; VitalRequestDto, ResponseUserVital (DTO 패키지)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,1,0&quot;&gt;내부 DB(창고):&lt;/b&gt; Vital, EmergencyEvent (Entity 패키지) 역할을 명확히 분리하니, 서비스 레이어(VitalService)가 단순한 비즈니스 로직이 아니라 데이터를 가공하고 라우팅하는 훌륭한 &lt;b data-index-in-node=&quot;120&quot; data-path-to-node=&quot;22,1,0&quot;&gt;파이프라인 매니저&lt;/b&gt; 역할을 수행하게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;  2. 상태(State) 갱신의 중요성&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;초기 코드 작성 시 10초마다 save()는 작성해 놓고, 정작 lastSavedTimeMap.put()으로 타이머를 초기화하는 코드를 빼먹는 실수를 했다. 이 한 줄이 빠지면 첫 10초 이후에는 서버가 무조건 10초가 넘었다고 판단하여 다시 1초마다 DB에 저장하는 폭주 기관차가 되어버린다. &lt;b data-index-in-node=&quot;165&quot; data-path-to-node=&quot;24&quot;&gt;조건문을 통과하여 동작을 수행했다면, 반드시 기준이 되는 상태값을 업데이트해야 함&lt;/b&gt;을 뼈저리게 느꼈다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;25&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size26&quot;&gt;  5. Next Step  &amp;zwj;♂️&lt;/h2&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;1초 단위의 거대한 데이터 흐름을 제어하는 뼈대는 완성되었다. 다음 작업으로는 아래 두 가지를 진행할 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,0,0&quot;&gt;데이터 안정화:&lt;/b&gt; 센서 데이터가 일시적으로 튀는 노이즈 현상을 막기 위해 이동 평균(Moving Average) 알고리즘 구현하기&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,1,0&quot;&gt;배치 처리(Batch):&lt;/b&gt; 매일 새벽, 하루 동안 쌓인 샘플링 데이터를 분석해 평균/최소/최대 수치를 계산하고 HealthStatus 테이블에 요약 저장하는 스케줄러 개발하기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/백엔드</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/27</guid>
      <comments>https://taecostudy76.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 10 May 2026 10:14:46 +0900</pubDate>
    </item>
    <item>
      <title>[백엔드 개발기] 실시간 IoT 헬스케어 생체 데이터 파이프라인 구축 및 아키텍처 설계</title>
      <link>https://taecostudy76.tistory.com/26</link>
      <description>&lt;h2 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;1. 도입: 기획 화면을 서버 아키텍처로 번역하다&lt;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 시작은 팀원이 그려온 기획 화면(사진)들이었다. 스마트워치(Edge 기기)에서 수집된 피보호자의 생체 데이터(심박수, 호흡수, 낙상 여부)를 실시간으로 모니터링하고, 비정상 수치 발생 시 보호자에게 즉각적인 알림을 보내는 시스템을 구축해야 했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;단순한 CRUD 게시판이 아니라 &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;7&quot;&gt;1초마다 쏟아지는 센서 데이터를 어떻게 안정적으로 처리하고 화면에 뿌려줄 것인가?&lt;/b&gt; 이 질문에서부터 아키텍처 고민이 시작되었다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size26&quot;&gt;2. 데이터 성격에 따른 처리 주체 분리 (Edge vs Server)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 고민한 것은 &quot;긴급 상황을 누가 판단할 것인가?&quot; 였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;낙상 감지 (Edge Computing):&lt;/b&gt; 초당 수십 번씩 변하는 자이로스코프의 원시 데이터(Raw Data)를 서버가 다 받을 수는 없다. 따라서 기기가 직접 연산하여 isFallDetected = true라는 '결과값'만 던져주면, 백엔드는 이를 100% 신뢰하고 즉시 알림을 생성하도록 설계했다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;심박수/호흡수 이상 (Server-side Rule Engine):&lt;/b&gt; 반면, 심박/호흡은 병원 정책이나 환자 상태에 따라 '정상 기준'이 수시로 바뀔 수 있다. 따라서 기기는 '수치'만 던지고, 백엔드에서 60~120이라는 비즈니스 룰을 적용해 이상 여부를 판단하도록 역할을 명확히 분리했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size26&quot;&gt;3. 객체 지향(OOP)을 활용한 클린 코드: &quot;룰은 엔티티가 스스로 알게 하라&quot;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;기능을 구현하다 보니 EmergencyEventService(알림 생성)와 UserProfileService(화면 조회) 양쪽에서 '심박수 60~120 비교 로직'이 중복해서 사용되는 것을 발견했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;DRY(Don't Repeat Yourself) 원칙을 지키기 위해, 판단 로직을 서비스 클래스에서 빼내어 데이터의 주인인 &lt;b data-index-in-node=&quot;69&quot; data-path-to-node=&quot;13&quot;&gt;Vital 엔티티 내부의 메서드(isHeartBreathNormal())로 캡슐화&lt;/b&gt;했다. 마찬가지로 &quot;FALL = 낙상이 감지되었습니다&quot; 같은 문자열 변환 로직도 EmergencyEvent 엔티티로 응집시켜, 불필요한 서비스 간의 의존성(순환 참조 위험)을 제거하고 유지보수성을 극대화했다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size26&quot;&gt;4. 실시간 데이터 파이프라인과 Two-Track 저장 전략&lt;/h2&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;센서에서 데이터가 들어올 때의 플로우를 다음과 같이 컨베이어 벨트처럼 4단계 파이프라인으로 설계하고, 이를 지휘하는 VitalProcessingService(총괄 매니저)를 도입했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,0&quot;&gt;안정화 (Filter):&lt;/b&gt; 날것의 센서 데이터에서 튀는 값(Noise)을 깎아내는 알고리즘 적용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0&quot;&gt;실시간 전송 (SSE):&lt;/b&gt; 프론트엔드 그래프 렌더링을 위해 가공된 데이터를 지연 없이 스트리밍&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,2,0&quot;&gt;긴급 검사:&lt;/b&gt; 수신 즉시 룰을 체크하여 긴급 상황이면 DB에 알림(EmergencyEvent) 생성&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,3,0&quot;&gt;최종 저장:&lt;/b&gt; 생체 데이터(Vital) DB 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17&quot;&gt;  트래픽 최적화 포인트 (Two-Track 전략):&lt;/b&gt; 1초마다 들어오는 모든 정상 생체 데이터를 그대로 DB에 INSERT 하면 부하가 엄청나다. 따라서 &lt;b data-index-in-node=&quot;87&quot; data-path-to-node=&quot;17&quot;&gt;긴급 상황은 1초마다 실시간으로 검사 및 알림&lt;/b&gt;을 보내되, &lt;b data-index-in-node=&quot;119&quot; data-path-to-node=&quot;17&quot;&gt;정상 데이터는 메모리(Buffer)에 들고 있다가 Spring @Scheduled를 활용해 10초 주기로 한 번에 DB에 밀어 넣는(Batch Insert)&lt;/b&gt; 투트랙 아키텍처를 도입해 DB I/O를 획기적으로 줄였다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size26&quot;&gt;5. 트러블슈팅: 404 에러와 DB 페이징의 함정&lt;/h2&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;개발 중 겪었던 두 가지 재미있는(?) 삽질과 해결 과정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0,0&quot;&gt;API 404 Not Found의 비밀:&lt;/b&gt; Postman으로 긴급 알림 상세 조회 테스트 중 권한(401/403)이나 서버 에러(500)가 아닌 404 에러를 마주했다. 원인은 컨트롤러의 클래스 레벨 @RequestMapping과 메서드 레벨 @GetMapping이 합쳐지는 과정에서 주소 경로 맨 뒤의 /detail을 호출 시 누락했던 단순 오타였다. (덤으로 @GetMapping 내부 문자열 시작 시 안전하게 /를 붙이는 컨벤션도 맞추었다.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,1,0&quot;&gt;사라진 최신 데이터 미스터리:&lt;/b&gt; 분명 서버 코드는 OrderByRecordedAtDesc로 최신 데이터를 가져오게 짰는데, DB GUI 툴(Supabase)에서는 최신 데이터가 안 보였다. 알고 보니 서버 버그가 아니라, 브라우저 렌더링 부하를 막기 위해 &lt;b data-index-in-node=&quot;142&quot; data-path-to-node=&quot;20,1,0&quot;&gt;GUI 툴 자체가 100건 단위로 페이징(Pagination) 처리&lt;/b&gt;를 해서 보여주고 있었기 때문이었다. 이는 역으로 나중에 프론트엔드에게 통계 리스트를 내려줄 때 왜 페이징 처리가 필수적인지 뼈저리게 깨닫는 계기가 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size26&quot;&gt;6. 결론: 협업을 위한 읽기/쓰기(Read/Write) 분리&lt;/h2&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;팀원과 업무를 나눌 때, 생체 데이터 처리는 내가 맡고 화면 통계 API는 팀원에게 위임했다. 이것이 가능했던 이유는 데이터를 가공해서 밀어 넣는 파이프라인(Write)과 저장된 데이터를 꺼내보는 통계(Read)의 역할을 완벽하게 분리했기 때문이다. 팀원에게는 &quot;데이터를 자바 메모리로 다 가져오지 말고, 무조건 DB 단에서 JPQL로 AVG, MAX, GROUP BY 계산을 끝내서 응답해라!&quot;라는 성능 최적화 가이드라인도 함께 전달했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;이제 뼈대는 완벽하게 세워졌다. 다음 목표는 실시간 센서 데이터의 노이즈를 효과적으로 제거할 '데이터 안정화 알고리즘'을 구현하여 파이프라인의 첫 단추를 완성하는 것이다!&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/26</guid>
      <comments>https://taecostudy76.tistory.com/26#entry26comment</comments>
      <pubDate>Fri, 8 May 2026 23:52:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] IoT 실시간 모니터링: SSE 통신 원리와 Optional 리팩토링 경험기</title>
      <link>https://taecostudy76.tistory.com/25</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;최근 라즈베리 파이(IoT)에서 수집한 활력 징후(심박수, 호흡수 등) 데이터를 프론트엔드로 실시간 전달하는 백엔드 서버를 개발하고 있습니다. 이 과정에서 &lt;b data-index-in-node=&quot;87&quot; data-path-to-node=&quot;4&quot;&gt;SSE(Server-Sent Events)를 활용한 실시간 통신 원리&lt;/b&gt;를 깊게 이해하게 되었고, &lt;b data-index-in-node=&quot;140&quot; data-path-to-node=&quot;4&quot;&gt;JPA를 사용할 때 Optional을 처리하는 로직&lt;/b&gt;을 제 방식대로 깔끔하게 리팩토링한 경험을 공유하고자 합니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;1. SSE(Server-Sent Events) 통신: 헷갈렸던 데이터의 흐름&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;초기에는 라즈베리 파이에서 서버로 데이터를 쏘는 부분(POST /api/vitals)에 프론트엔드로 데이터를 전달하는 로직이 섞여 있어서 통신 방식에 대한 혼란이 있었습니다.&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;7&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;7,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0&quot;&gt;&quot;클라이언트가 조회를 하려면 무조건 GET 방식을 써야 하는데, 왜 POST 요청 안에서 클라이언트에게 데이터를 쏴주고 있지?&quot;&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;이 혼란은 SSE의 본질인 &quot;단방향 Push&quot;와 &quot;스트림(Stream)&quot;의 개념을 깨달으면서 완벽하게 해결되었습니다. 서버에는 총 3개의 문(API)이 분리되어 있어야 했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;데이터 수신 (POST):&lt;/b&gt; 라즈베리 파이가 측정한 데이터를 서버로 던지는 문.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;실시간 파이프 연결 (GET):&lt;/b&gt; 프론트엔드가 처음 접속해 EventSource로 &quot;나 계속 데이터 받을게!&quot; 하고 수도관(SSE)을 열어두는 문.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;과거 기록 조회 (GET):&lt;/b&gt; 파이프 연결 전의 과거 데이터를 한 번에 가져가는 일반적인 문.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;즉, 라즈베리 파이가 POST로 데이터를 던지자마자, 서버는 DB에 이를 저장함과 동시에 미리 열려있던 프론트엔드의 GET 파이프(SSE)**를 통해 데이터를 물이 흘러가듯(Stream) 강제로 밀어 넣는 것이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;단순히 API를 만드는 것을 넘어, ConcurrentHashMap을 이용해 여러 유저의 SseEmitter(수도관)를 스레드 안전하게 관리하고, 연결이 끊기면 장부에서 삭제하는 디테일한 로직까지 고민해 볼 수 있는 좋은 경험이었습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size23&quot;&gt;2. 논리적 모순 해결: Optional과 orElseThrow의 올바른 사용법&lt;/h3&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;기기를 조회하는 로직을 작성하던 중, 기존 로직의 불필요한 점을 발견하고 구조를 개선했습니다. 기존 코드는 아래와 같이 작성되어 있었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14&quot;&gt;[수정 전 코드: 논리적 모순]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwj19fPa2fGTAxUAAAAAHQAAAAAQSw&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;public Optional&amp;lt;DeviceDto.ResponseDevice&amp;gt; getDeviceStatus(Long userId) {

    Device device = deviceRepository.findByUserId(userId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;연결 된 디바이스가 없습니다.&quot;));

    boolean isConnected = checkConnection(device.getLastConnectedAt());

    return new DeviceDto.ResponseDevice(...); // 타입 에러 발생!
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 두 가지 문제가 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;orElseThrow()를 사용해 예외 처리를 마쳤음에도, 메서드의 반환 타입을 Optional로 선언했습니다.&lt;/li&gt;
&lt;li&gt;예외가 터지지 않고 맨 밑의 return까지 도달했다는 것은 **&quot;기기가 무조건 100% 존재한다&quot;**는 뜻인데, 굳이 &quot;없을 수도 있다&quot;는 의미의 Optional로 다시 포장할 필요가 없었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18&quot;&gt;[수정 후 코드: 깔끔한 단일 객체 반환]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwj19fPa2fGTAxUAAAAAHQAAAAAQTA&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public DeviceDto.ResponseDevice getDeviceStatus(Long userId) {

    // 1. 없으면 바로 예외를 던지고 종료!
    Device device = deviceRepository.findByUserId(userId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;연결 된 디바이스가 없습니다.&quot;));

    // 2. 예외가 안 터졌다면 무조건 객체가 있으므로 안전하게 로직 실행
    boolean isConnected = checkConnection(device.getLastConnectedAt());

    // 3. Optional 껍질 없이 DTO 알맹이만 깔끔하게 반환
    return new DeviceDto.ResponseDevice(
        device.getId(),
        isConnected,
        device.getSerialNum()
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20&quot;&gt;[리팩토링의 결과]&lt;/b&gt; &quot;기기가 1대만 존재한다&quot;는 비즈니스 로직에 맞춰 반환값을 단일 객체로 변경했고, 중간에 orElseThrow()로 방어벽을 세웠으니 반환 타입에서 과감하게 Optional을 제거했습니다. 덕분에 코드가 위에서 아래로 물 흐르듯 자연스러워졌고, 이 메서드를 호출하는 컨트롤러 입장에서도 껍질을 깔 필요 없이 바로 DTO를 사용할 수 있게 되었습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size23&quot;&gt;  마무리하며&lt;/h3&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 &quot;돌아가는 코드&quot;를 짜는 것을 넘어, &lt;b data-index-in-node=&quot;31&quot; data-path-to-node=&quot;22&quot;&gt;&quot;왜 이 통신 방식을 사용하는가?&quot;&lt;/b&gt;, &quot;이 타입(Optional)을 반환하는 것이 논리적으로 타당한가?&quot;를 치열하게 고민하게 되었습니다. 앞으로도 프론트엔드와 유연하게 소통하며, 불필요한 리소스 낭비가 없는 단단한 백엔드 서버를 구축해 나가고 싶습니다.&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <category>PM</category>
      <category>개발블로그</category>
      <category>백엔드개발자</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/25</guid>
      <comments>https://taecostudy76.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 16 Apr 2026 15:34:39 +0900</pubDate>
    </item>
    <item>
      <title>[JPA/Spring] 엔티티 설계의 정석: @NoArgsConstructor(access = AccessLevel.PROTECTED)와 @Builder의 조화</title>
      <link>https://taecostudy76.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트를 진행하며 JPA 엔티티를 설계하는 과정에서 왜 특정 어노테이션을 조합해 사용하는지, 그리고 객체 지향적인 설계란 무엇인지 깊게 파고들어 보았습니다. 공부한 내용을 잊지 않기 위해 정리합니다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;4&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;1. @NoArgsConstructor(access = AccessLevel.PROTECTED)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;JPA 엔티티를 만들 때 가장 먼저 마주친 의문은 &quot;왜 기본 생성자를 굳이 protected로 막는가?&quot;였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;JPA의 요구사항:&lt;/b&gt; JPA는 프록시 객체를 생성하기 위해 기본 생성자가 반드시 필요합니다. 하지만 접근 제어자가 private이면 접근할 수 없으므로 최소 protected 이상이어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;객체 오염 방지:&lt;/b&gt; 개발자가 new Member()와 같이 빈 객체를 생성하는 것을 막기 위함입니다. 필수 데이터가 없는 불완전한 객체가 시스템을 돌아다니는 것을 원천 차단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;결론:&lt;/b&gt; JPA는 일하게 하되, 개발자의 실수는 막는 절묘한 방어막입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;2. @Builder와 생성자의 조합&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;빈 객체 생성을 막았다면, 객체는 어떻게 만들어야 할까요? 정답은 &lt;b data-index-in-node=&quot;37&quot; data-path-to-node=&quot;10&quot;&gt;빌더 패턴&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;가독성:&lt;/b&gt; new Member(&quot;홍길동&quot;, 20, ...) 처럼 순서에 의존하는 대신 .name(&quot;홍길동&quot;).age(20)처럼 명확한 코드를 작성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;안전성:&lt;/b&gt; 클래스 레벨이 아닌 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;11,1,0&quot;&gt;생성자 레벨에 @Builder&lt;/b&gt;를 붙여, 내가 허용하는 필드만 빌더에 포함하는 정교한 설계가 가능해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;12&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;3. @Embeddable &amp;amp; @Embedded (데이터의 부품화)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;모든 필드를 하나의 클래스에 다 넣으면 클래스가 너무 비대해집니다. 이때 사용하는 것이 임베디드 타입입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot;&gt;@Embeddable:&lt;/b&gt; 독립적인 테이블이 아닌, 다른 엔티티의 부품이 될 클래스에 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;@AttributeOverrides:&lt;/b&gt; 같은 부품을 한 테이블에서 다른 컬럼명으로 쓰고 싶을 때 재정의하여 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,2,0&quot;&gt;이점:&lt;/b&gt; 코드의 재사용성이 높아지고, 실력 점수 계산 같은 특정 로직을 해당 클래스에 응집시킬 수 있어 객체 지향적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;16&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size23&quot;&gt;4. 양방향 연관관계와 데이터 일치&lt;/h3&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;MatchRecord(게임 기록)와 MatchParticipant(참가자)의 관계를 공부하며 가장 헷갈렸던 부분입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt;@ManyToOne vs @OneToMany:&lt;/b&gt; '다(Many)' 쪽이 외래 키(FK)를 가지는 주인입니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt;양방향 데이터 일치의 필요성:&lt;/b&gt; DB는 FK 하나로 양쪽을 알지만, &lt;b data-index-in-node=&quot;37&quot; data-path-to-node=&quot;19,1,0&quot;&gt;자바 객체는 그렇지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,2,0&quot;&gt;연관관계 편의 메서드:&lt;/b&gt;&amp;nbsp;matchRecord.getParticipants().add(this);&lt;/li&gt;
&lt;li&gt;생성자에서 이 코드를 호출함으로써, 내 수첩뿐만 아니라 상대방의 리스트에도 나를 추가해줘야 메모리상에서 데이터 불일치가 일어나지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;20&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size23&quot;&gt;  마치며&lt;/h3&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;단순히 코드를 작동시키는 것을 넘어, &quot;왜 이 어노테이션을 쓰는가?&quot;와 &quot;어떻게 하면 더 안전한 객체를 만들 것인가?&quot;를 고민해 본 시간이었습니다. JPA는 알수록 객체 지향 프로그래밍에 대한 깊은 이해를 요구하는 것 같습니다.&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <category>javaspring</category>
      <category>백엔드개발자</category>
      <category>코딩</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/24</guid>
      <comments>https://taecostudy76.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 11 Mar 2026 15:51:14 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] 풀스택 개발자 수강 최종 후기</title>
      <link>https://taecostudy76.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 멀티캠퍼스 [현대이지웰] 풀스택 개발자 과정을 수료하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 시작 했을 때가 7월 초였는데 이제 6개월이 지나 12월이 되었고 크리스마스 얼마 남지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 정말 빠른 것 같습니다. ㅜㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 후기에 대해서 작성을 해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.45.06@2x.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbmmTq/dJMcadHkSCg/neD9UKd1Kvc2dKuocULl2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbmmTq/dJMcadHkSCg/neD9UKd1Kvc2dKuocULl2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbmmTq/dJMcadHkSCg/neD9UKd1Kvc2dKuocULl2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbmmTq%2FdJMcadHkSCg%2FneD9UKd1Kvc2dKuocULl2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;366&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.45.06@2x.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 이지웰 과정은 총 6개월입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다른 과정들도 6개월이라서 큰 차이는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 6회 차를 수강했는데 이번 과정에 약 30명 정도 하는 인원이 듣기 때문에 메인 강사님과 보조 강사님 두 분이 같이 진행하셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 강사님은 모든 과정 수업을 진행하시고 보조 강사님은 질문 같은 것들을 답변해주시면서 진도에 차질 없이 수업을 진행할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.47.35@2x.png&quot; data-origin-width=&quot;2390&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pKTNQ/dJMcah34wcq/fMkAfH71PckTyoKUge9671/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pKTNQ/dJMcah34wcq/fMkAfH71PckTyoKUge9671/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pKTNQ/dJMcah34wcq/fMkAfH71PckTyoKUge9671/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpKTNQ%2FdJMcah34wcq%2FfMkAfH71PckTyoKUge9671%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;2390&quot; height=&quot;230&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.47.35@2x.png&quot; data-origin-width=&quot;2390&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 이지웰 과정은 기본 HTML, CSS, JavaScript 등 프론트 과정과 Java, Spring Boot 등 백엔드 과정으로 되어 있습니다.&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;처음에 이 과정을 봤을 때 MyBatis만 배우고 JPA는 안배우는 줄 알았지만 JPA도 배워서 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 내용을 전부 설명할 수는 없지만 웹 개발 생태계가 시대에 따라 변화하는 과정을 함께 언급하시며 수업을 받았기 때문에 유익했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.51.24@2x.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPw0OJ/dJMb99LJGIf/oM5nr2glrP5xL7Oxcr4Ad1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPw0OJ/dJMb99LJGIf/oM5nr2glrP5xL7Oxcr4Ad1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPw0OJ/dJMb99LJGIf/oM5nr2glrP5xL7Oxcr4Ad1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPw0OJ%2FdJMb99LJGIf%2FoM5nr2glrP5xL7Oxcr4Ad1%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;1776&quot; height=&quot;850&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.51.24@2x.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 전담 취업 컨설턴트님도 있어서 취업 관련 고민 상담이나 진로 선택에 있어서 많은 도움을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 수업 과정 중간에 취업 관련 멘토링도 있어서 자소서나 포트폴리오를 어떻게 작성해야 하는지에 대한 부분도 굉장히 유익했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 아쉬웠던 점은 초반에는 취업 컨설턴트님과 접점이 있었지만 후반으로 갈 수록 접점이 거의 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.53.59@2x.png&quot; data-origin-width=&quot;2804&quot; data-origin-height=&quot;1682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgLj1w/dJMcagc2ra5/tQHQzHANFqSKTciMttd8p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgLj1w/dJMcagc2ra5/tQHQzHANFqSKTciMttd8p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgLj1w/dJMcagc2ra5/tQHQzHANFqSKTciMttd8p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgLj1w%2FdJMcagc2ra5%2FtQHQzHANFqSKTciMttd8p0%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;2804&quot; height=&quot;1682&quot; data-filename=&quot;CleanShot 2025-12-22 at 14.53.59@2x.png&quot; data-origin-width=&quot;2804&quot; data-origin-height=&quot;1682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공지사항이나 관련 정보등 MLP라는 사이트를 통해서 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 점은 비대면으로 수업하면서 좋았다고 생각합니다. Slack를 사용해서 매니저님이 공지를 해주시기도 했지만, MLP로 직접 들어가서&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;수업은 매주 월요일 부터 금요일까지 아침 9시부터 오후 6시까지 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜 시간 앉아서 수업을 듣기 때문에 지루할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이론 보다는 직접 생각하면서 코드를 짜는 것이 더 재밌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 초반에 이론 중심으로 수업을 들을 때는 굉장히 지루했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중간에 실습하는 과제를 주시는데 그때 정말 재미있게 과제를 풀어나갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이론과 실습을 적절히 분배해서 수업을 하는 것 같아서 만족했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.09.42@2x.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjmE6/dJMcaaYbz2o/hRwNuBr0BCOauHNkFjkGLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjmE6/dJMcaaYbz2o/hRwNuBr0BCOauHNkFjkGLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjmE6/dJMcaaYbz2o/hRwNuBr0BCOauHNkFjkGLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjmE6%2FdJMcaaYbz2o%2FhRwNuBr0BCOauHNkFjkGLk%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;2592&quot; height=&quot;1656&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.09.42@2x.png&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 수업이 다 끝나면 수업을 바탕으로 프로젝트를 진행합니다.&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;최종 프로젝트 개발 기간은 대략 8주 정도 주어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 기간 동안에는 멘토링 시간도 있어서 더욱 알찬 시간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업 때와 똑같이 아침 9시부터 오후 6시까지 개발을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때가 저는 가장 재미있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 배운 내용을 가지고 개발을 진행하고 어려움이 있으면 고민도 하고 구글링도 하면서 실력이 늘어나는 것을 몸소 체감하는 시간이었습니다.&lt;/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;그리고 거의 끝날 때쯤에는 배포 과정 설명을 따로 pdf 파일로 주셔서 그걸로 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번도 직접 배포한 경험이 없었기 때문에 굉장히 어려웠지만 하면서 배우는 것들이 있어서 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로 자세한 설명이 있었던 것은 아니지만 직접 이론도 찾아가며 배포하는 경험이 유익했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.14.32@2x.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VAtFv/dJMcabQjYXn/Sq0Kb2wYE7fqWeUuWS53IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VAtFv/dJMcabQjYXn/Sq0Kb2wYE7fqWeUuWS53IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VAtFv/dJMcabQjYXn/Sq0Kb2wYE7fqWeUuWS53IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVAtFv%2FdJMcabQjYXn%2FSq0Kb2wYE7fqWeUuWS53IK%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;2334&quot; height=&quot;1020&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.14.32@2x.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포는 AWS의 EC2를 사용해서 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD는 Jenkins를 사용해서 자동 빌드 및 배포하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 취업 조건에 CI/CD를 사용한 배포 경험이라는 것을 많이 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하게 무엇인지 몰랐지만 이번 프로젝트를 통해서 경험을 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.17.22@2x.png&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1pfNv/dJMcahCZRfE/Qoah4nf6haEklTGVs2vyvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1pfNv/dJMcahCZRfE/Qoah4nf6haEklTGVs2vyvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1pfNv/dJMcahCZRfE/Qoah4nf6haEklTGVs2vyvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1pfNv%2FdJMcahCZRfE%2FQoah4nf6haEklTGVs2vyvK%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;1698&quot; height=&quot;1702&quot; data-filename=&quot;CleanShot 2025-12-22 at 15.17.22@2x.png&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 TIL 챌린지를 참여하면서 수업을 참여했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 챌린지 일주일에 한 번만 자신의 블로그에 배운 내용 등을 작성해서 올리는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 달에 총 4번을 작성하게 되면 네이버 페이를 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 정말 좋습니다! 내 포트폴리오를 쌓을 주으면서, 6번의 네이버 페이를 받을 수 있습니다. 이게 제법 쏠쏠합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 이 과정을 하신다면 이건 꼭 하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월 동안 힘들지 않았다고 하면 거짓말이겠죠?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힘들긴 했지만 그만큼 얻어 가는 것도 많았다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교에서는 취업을 위한 수업이 아니라 학문을 위한 수업을 위주로 하기 때문에 실무에서 사용한 기술이나 프로젝트 진행 방식을 경험하기에는 한계가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 과정을 수강했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 개발자나 IT 업계로 취업을 하시고 싶고, 개발에 대한 지식이 하나도 없다면 이 과정을 추천드립니다!&lt;/p&gt;</description>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/23</guid>
      <comments>https://taecostudy76.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 22 Dec 2025 15:22:49 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] 외부 파일에 이미지 저장 및 불러오기</title>
      <link>https://taecostudy76.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 하면서 사용자로부터 받은 이미지를 내 컴퓨터에 로컬에 저장을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰를 작성하면서 이미지를 업로드하면 로컬 폴더에 저장이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 배포를 하면서 외부 파일을 사용해야 했고, FileZilla를 통해서 aws에 열었던 IP를 사용해 연결을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 project 파일에 configure.properties를 넣었습니다. 이 파일은 application.properties에 있던 중요 정보를 옮겨 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 projectImage 폴더를 넣었고 그 안에 리뷰 사진들과 상품 사진들 등 사진들이 저장되는 것을 확인했습니다.&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;이제 이거를 코드를 사용해서 CRUD를 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.20.23@2x.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5dUxz/dJMcaajxaZr/ua9C7nZKj0PcLMMfE0M0Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5dUxz/dJMcaajxaZr/ua9C7nZKj0PcLMMfE0M0Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5dUxz/dJMcaajxaZr/ua9C7nZKj0PcLMMfE0M0Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5dUxz%2FdJMcaajxaZr%2Fua9C7nZKj0PcLMMfE0M0Gk%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;1108&quot; height=&quot;264&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.20.23@2x.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 interface를 만들어 틀을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.21.15@2x.png&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NAgZb/dJMcah31URp/mEp79ZbxZcZ42JcrQMkGpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NAgZb/dJMcah31URp/mEp79ZbxZcZ42JcrQMkGpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NAgZb/dJMcah31URp/mEp79ZbxZcZ42JcrQMkGpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNAgZb%2FdJMcah31URp%2FmEp79ZbxZcZ42JcrQMkGpK%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;1302&quot; height=&quot;1124&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.21.15@2x.png&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서비스 로직을 구현 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Value 어노테이션은 properties에 있는 이미지 파일 저장 경로를 자동으로 불러와 upLoadDir에 넣어 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;upload 메서드는 MultipartFile 객체로 여러 개의 파일을 받아 옵니다.&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;MultipartFile은 스프링에서 HTTP 요청으로 업로드된 파일을 표현하는 인터페이스입니다.&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;받아 온 데이터를 if 문을 통해서 확인하고 이름을 만들어 줍니다. UUID를 사용해서 랜덤 문자열을 만들고 기존 파일의 이미지 명과 uuid를 합쳐서 고유한 이미지 파일 이름을 만듭니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 경로를 Paths.get 메서드를 사용해서 가져오고 그 자리에 파일을 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.29.43@2x.png&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQilk/dJMcagxhBjk/gNjkZxAM6ipM9Kl6CKuGLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQilk/dJMcagxhBjk/gNjkZxAM6ipM9Kl6CKuGLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQilk/dJMcagxhBjk/gNjkZxAM6ipM9Kl6CKuGLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQilk%2FdJMcagxhBjk%2FgNjkZxAM6ipM9Kl6CKuGLk%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;1218&quot; height=&quot;702&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.29.43@2x.png&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delete 메서드는 파일 이름을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름이 /images/abc.jpg 이런 식이라면 뒤에 실제 파일 이름만을 남겨 놓습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Path 객체를 사용해서 실제 파일 경로를 찾고 삭제를 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.32.45@2x.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AqBtv/dJMcagYlYqj/srQ0QYfremXSi6RgiBfwbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AqBtv/dJMcagYlYqj/srQ0QYfremXSi6RgiBfwbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AqBtv/dJMcagYlYqj/srQ0QYfremXSi6RgiBfwbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAqBtv%2FdJMcagYlYqj%2FsrQ0QYfremXSi6RgiBfwbk%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;1458&quot; height=&quot;222&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.32.45@2x.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 서비스 클래스에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 받아 올 때 List 객체를 MultipartFile 타입으로 받아 옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.34.15@2x.png&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RMsnP/dJMcaiV72Cx/RhAwSl4WU6CF2Lcbm5AOjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RMsnP/dJMcaiV72Cx/RhAwSl4WU6CF2Lcbm5AOjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RMsnP/dJMcaiV72Cx/RhAwSl4WU6CF2Lcbm5AOjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRMsnP%2FdJMcaiV72Cx%2FRhAwSl4WU6CF2Lcbm5AOjK%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;1476&quot; height=&quot;636&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.34.15@2x.png&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미지 파일 여부를 따진 후에 imageUrl과 review를 entity 타입으로 바꿔 이미지를 저장합니다.&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-filename=&quot;CleanShot 2025-12-15 at 16.36.45@2x.png&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjUrY1/dJMcabW230z/cIsOVnh9BZzMrbTAr9BLH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjUrY1/dJMcabW230z/cIsOVnh9BZzMrbTAr9BLH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjUrY1/dJMcabW230z/cIsOVnh9BZzMrbTAr9BLH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjUrY1%2FdJMcabW230z%2FcIsOVnh9BZzMrbTAr9BLH1%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;1352&quot; height=&quot;432&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.36.45@2x.png&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 삭제할 때는 DB에서 이미지 이름을 그대로 가져옵니다. 그리고 url로 외부 파일에 있는 이미지를 삭제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.39.30@2x.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n79Hi/dJMcagxhBJQ/pc8qRXnEvV7yb0AHVpZayK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n79Hi/dJMcagxhBJQ/pc8qRXnEvV7yb0AHVpZayK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n79Hi/dJMcagxhBJQ/pc8qRXnEvV7yb0AHVpZayK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn79Hi%2FdJMcagxhBJQ%2Fpc8qRXnEvV7yb0AHVpZayK%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;1362&quot; height=&quot;344&quot; data-filename=&quot;CleanShot 2025-12-15 at 16.39.30@2x.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 이미지 파일을 가져올 때는 위 사진처럼 @RequestPart 어노테이션을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청의 multipart/form-data 중에서 이름이 &quot;files&quot;인 파트를 이 파라미터에 매핑한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;required = false는 이 값이 아무 값도 안 들어와도 에러를 내지 않는다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null이나 빈 리스트로 값이 들어오는 것을 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Java</category>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/22</guid>
      <comments>https://taecostudy76.tistory.com/22#entry22comment</comments>
      <pubDate>Mon, 15 Dec 2025 16:42:35 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] AWS 활용한 배포 War, tomcat(버전 맞추기 필수!)</title>
      <link>https://taecostudy76.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에 지금까지 했던 프로젝트를 배포하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에 포트를 활성화 했고, jenkins를 사용해서 빌드를 하려고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 빌드는 성공했고, tomcat도 잘 올라와 있는데 404 에러가 떴습니다.&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;프로젝트 build.gradle 보니 war이 아니라 jar이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 독립적인 Java 애플리케이션을 패키징할 수 있고, 실행 가능한 JAR 파일로 만들어서 개발자가 쉽게 어플리케이션을 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Java 애플리케이션의 진입점인 main 메서드를 포함하고 있어 JVM에서 직접 실행하여 별도의 서버가 필요하지 않습니다.&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;WAR 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Java 웹 애플리케이션을 패키징하는 데 최적화 되어 있고, 웹 구성 요소인 JSP, 서블릿, 리스너 등과 웹 어플리케이션을 실행하기 위한&lt;br /&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;원래 프로젝트의 application.properties에 가보면 jar로 되어 있습니다. 이제 배포를 하기 위해서 외부 톰캣을 사용할 것이기 때문에&lt;br /&gt;war로 바꾸는 작업을 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 17.54.38@2x.png&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctIqQd/dJMb99SqSdr/AEj5g4OCVHprnfaivMKmLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctIqQd/dJMb99SqSdr/AEj5g4OCVHprnfaivMKmLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctIqQd/dJMb99SqSdr/AEj5g4OCVHprnfaivMKmLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctIqQd%2FdJMb99SqSdr%2FAEj5g4OCVHprnfaivMKmLK%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;1676&quot; height=&quot;1074&quot; data-filename=&quot;CleanShot 2025-12-08 at 17.54.38@2x.png&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;war는 활성화하고 jar는 비활성화를 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 17.58.35@2x.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdQWAw/dJMcafkN3RQ/XwF0zKMWK5NVG0iYObfRUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdQWAw/dJMcafkN3RQ/XwF0zKMWK5NVG0iYObfRUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdQWAw/dJMcafkN3RQ/XwF0zKMWK5NVG0iYObfRUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdQWAw%2FdJMcafkN3RQ%2FXwF0zKMWK5NVG0iYObfRUk%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;1528&quot; height=&quot;606&quot; data-filename=&quot;CleanShot 2025-12-08 at 17.58.35@2x.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 외부 WAS에 WAR 파일을 배포할 때 Spring Boot 애플리케이션을 초기화하고 SpringApplicationBuilder를 사용해 ShoppingmallApplication 클래스를 메인 소스로 지정하여 이를 통해 웹 컨테이너가 Spring 콘텍스트를 자동으로 로드하고 실행하는 configure 메서드를 오버라이드 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.00.58@2x.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZUiob/dJMcah3ZwqJ/tC0YK2zF0wlml7cKIQeq90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZUiob/dJMcah3ZwqJ/tC0YK2zF0wlml7cKIQeq90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZUiob/dJMcah3ZwqJ/tC0YK2zF0wlml7cKIQeq90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZUiob%2FdJMcah3ZwqJ%2FtC0YK2zF0wlml7cKIQeq90%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;806&quot; height=&quot;960&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.00.58@2x.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 후에 빌드를 해보면 war 파일이 생성된 것을 확인할 수 있습니다. jenkins에 올리기 전에 외부 톰캣을 활용해서 실행을 시켜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 진행을 했는데 또 404가 떴습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.02.43@2x.png&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LvIZ8/dJMcajtViaM/EB81yaFzRQFlSIiQNSaWeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LvIZ8/dJMcajtViaM/EB81yaFzRQFlSIiQNSaWeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LvIZ8/dJMcajtViaM/EB81yaFzRQFlSIiQNSaWeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLvIZ8%2FdJMcajtViaM%2FEB81yaFzRQFlSIiQNSaWeK%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;1764&quot; height=&quot;1298&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.02.43@2x.png&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;war를 선택을 해서 서버를 실행시켜야 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.03.39@2x.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOXIgY/dJMcafZpgKI/zt2TBWuMkvPiC9W38stfv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOXIgY/dJMcafZpgKI/zt2TBWuMkvPiC9W38stfv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOXIgY/dJMcafZpgKI/zt2TBWuMkvPiC9W38stfv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOXIgY%2FdJMcafZpgKI%2Fzt2TBWuMkvPiC9W38stfv1%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;1474&quot; height=&quot;552&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.03.39@2x.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 컴퓨터에 설치한 외부 톰캣 webapps에 자신의 프로젝트에서 생성된 war 파일을 붙여 넣으면 톰캣이 실행되면서 자동으로 파일 압축을 풉니다.&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;그리고 실행시켰는데 또 404가 뜹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 것들은 다 했고 WAR도 서버에 잘 올라가는데 404 에러 계속 떴습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 계속한 결과 스프링부트 버전이 높아서 tomcat도 버전을 맞춰서 올려야 한다는 블로그를 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-tomcat/3.5.7&quot;&gt;https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-tomcat/3.5.7&lt;/a&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;저희 프로젝트 spring boot 버전이 높아서 10 버전을 사용해야 했고 저는 9 버전의 외부 톰캣을 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 404 에러가 떴고 10 버전을 다운 받아서 실행해 보니 잘 동작을 했습니다.&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;jenkins에 빌드를 전에 했을 때 외부 톰캣 10 버전으로 했는데 404가 떴습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 거: 외부 톰캣 버전을 10.1.xx 버전으로 해서 했더니 잘 동작했다. 근데 우리 jenkins에서도 톰캣버전이 10 버전인데 왜 이전에는 안되었을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jenkins 실행 성공이 되어야지 80 포트도 실행되는 게 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; war 생성 후 git에 올려서 jenkin에 올려 실행시켜야 하는데 전에는 war를 생성하지 않은 버전을 git에 올렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 톰캣 버전이 10 이어도 404가 떴었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.10.59@2x.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/udGBw/dJMcagqtMr9/LWE7TKgoLn6zxVaKtr16SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/udGBw/dJMcagqtMr9/LWE7TKgoLn6zxVaKtr16SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/udGBw/dJMcagqtMr9/LWE7TKgoLn6zxVaKtr16SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FudGBw%2FdJMcagqtMr9%2FLWE7TKgoLn6zxVaKtr16SK%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;2112&quot; height=&quot;940&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.10.59@2x.png&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle 썼기 때문에 꼭 구성에서 gradle 버전을 설치해줘야 합니다.&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;Jenkins를 사용하여 CI/CD 사용법을 익힐 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI는 직속적 통합으로 코드 변경 시 자동 빌드와 테스트 진행으로 충돌 조기 발견을 하고 Git 같은 저장소에 주기적 병합으로 팀 협업을 강화할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD는 지속적 제공 및 배포로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Continuous Delivery: 테스트 완료 후 수동 배포 승인과&lt;br /&gt;Continuous Deployment: 모든 테스트 통과 시 자동 프로덕션 배포를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmVJsy/dJMcadAtf6q/fvdkQnUPEDfQ0qyDRh5k2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmVJsy/dJMcadAtf6q/fvdkQnUPEDfQ0qyDRh5k2K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;1524&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.15.15@2x.png&quot; style=&quot;width: 19.9491%; margin-right: 10px;&quot; data-widthpercent=&quot;20.18&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmVJsy/dJMcadAtf6q/fvdkQnUPEDfQ0qyDRh5k2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmVJsy%2FdJMcadAtf6q%2FfvdkQnUPEDfQ0qyDRh5k2K%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;652&quot; height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY5tMW/dJMcaacI5bi/CKqikMKqa3aqAuV6u07Ym1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY5tMW/dJMcaacI5bi/CKqikMKqa3aqAuV6u07Ym1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;610&quot; data-filename=&quot;CleanShot 2025-12-08 at 18.16.08@2x.png&quot; style=&quot;width: 78.8881%;&quot; data-widthpercent=&quot;79.82&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY5tMW/dJMcaacI5bi/CKqikMKqa3aqAuV6u07Ym1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY5tMW%2FdJMcaacI5bi%2FCKqikMKqa3aqAuV6u07Ym1%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;1032&quot; height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 빌드시 충돌을 확인할 수 있어 매우 간편합니다. 만약 충돌 로그를 확인하고 싶다면 오른쪽 사진처럼 Console Output를 눌러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인하면 됩니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/21</guid>
      <comments>https://taecostudy76.tistory.com/21#entry21comment</comments>
      <pubDate>Mon, 8 Dec 2025 18:18:28 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] Spring Boot 활용하여 기본 CRUD 기능 구현</title>
      <link>https://taecostudy76.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot를 활용해서 리뷰의 기본 CRUD 기능을 구현했습니다.&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;기본 아키텍처는 Controller 와 Service, Repository, Entity, DTO로 구성되어 있습니다.&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;Repository는 JPA를 활용하여 DB와 직접 소통하는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity를 사용해서 직접 소통하며, JPA는 기본 CRUD를 제공하기 때문에 직접 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;만약에 성능 또는 커스텀이 필요하다면 @Query 어노테이션을 사용하여 JPQL 문을 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.25.37@2x.png&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oKzOZ/dJMcahv60JH/0EfKNNNlyxhiA9v1blSJUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oKzOZ/dJMcahv60JH/0EfKNNNlyxhiA9v1blSJUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oKzOZ/dJMcahv60JH/0EfKNNNlyxhiA9v1blSJUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoKzOZ%2FdJMcahv60JH%2F0EfKNNNlyxhiA9v1blSJUK%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;1566&quot; height=&quot;492&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.25.37@2x.png&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepository를 상속받아 사용하고, 타입은 &amp;lt;Entity, pk타입&amp;gt;을 입력해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 메서드 작성을 안 해도 되지만 주석을 사용하여 어떤 메서드를 사용했는지 남겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.27.28@2x.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJirm/dJMcaaqdOKP/cygnxIabjIPYzt7KbMJjm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJirm/dJMcaaqdOKP/cygnxIabjIPYzt7KbMJjm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJirm/dJMcaaqdOKP/cygnxIabjIPYzt7KbMJjm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAJirm%2FdJMcaaqdOKP%2FcygnxIabjIPYzt7KbMJjm1%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;1770&quot; height=&quot;930&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.27.28@2x.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 @Query 어노테이션을 사용하여 직접 원하는 데이터 값을 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository를 모두 구현했다면 이제 Service를 클래스를 사용하여 원하는 기능을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfwt3G/dJMcai9zPHU/oBLxmTu8MPHVTuQd27BUAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfwt3G/dJMcai9zPHU/oBLxmTu8MPHVTuQd27BUAK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1056&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.29.38@2x.png&quot; style=&quot;width: 48.1261%; margin-right: 10px;&quot; data-widthpercent=&quot;48.69&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfwt3G/dJMcai9zPHU/oBLxmTu8MPHVTuQd27BUAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfwt3G%2FdJMcai9zPHU%2FoBLxmTu8MPHVTuQd27BUAK%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;1570&quot; height=&quot;1056&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DqoeI/dJMcaacGohW/PMU9If7bf006Stjo3mTxC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DqoeI/dJMcaacGohW/PMU9If7bf006Stjo3mTxC0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;1006&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.38.37@2x.png&quot; style=&quot;width: 50.7111%;&quot; data-widthpercent=&quot;51.31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DqoeI/dJMcaacGohW/PMU9If7bf006Stjo3mTxC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDqoeI%2FdJMcaacGohW%2FPMU9If7bf006Stjo3mTxC0%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;1576&quot; height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 등록 기능을 구현했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 기능을 구현할 때 예외 상황을 먼저 생각을 하고 예외 상황이 발생했을 때 어떻게 해야 할 건지를 코드 작성 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 ReviewDTO 데이터 값과 이미지 파일, 리뷰를 작성한 사용자의 memberNo를 받아 옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 예외 상황을 미리 예상해 보면, 1. 구매 내역이 없는 사용자 2. 주문한 사용자와 로그인한 사용자 일치 확인 등등 을 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 받아온 ReviewDTO를 DBMS와 소통해야 하기 때문에 Entity로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미지와 태그를 따로 테이블에 저장을 한 후 reviewNo를 다시 반환해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.39.36@2x.png&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I0YOz/dJMcagRvdAF/7aUOvVt8OmS5bDMRlbWD2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I0YOz/dJMcagRvdAF/7aUOvVt8OmS5bDMRlbWD2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I0YOz/dJMcagRvdAF/7aUOvVt8OmS5bDMRlbWD2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI0YOz%2FdJMcagRvdAF%2F7aUOvVt8OmS5bDMRlbWD2K%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;1650&quot; height=&quot;892&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.39.36@2x.png&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 조회는 수정 페이지에서 정보를 불러오는 것과 상품 페이지에서 리뷰 목록을 불러오는 것 이렇게 2 기능을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 reviewNo를 받으면 예외가 생길 경우 &quot;리뷰 조회 에러&quot;를 띄웁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReviewDTO 클래스 안에 있는 toDto() 메서드를 활용해서 DB에서 가져온 데이터를 (Entity타입) DTO로 변환하고 클라이언트에 보냅니다.&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;리뷰 목록도 같은 맥락입니다. 리뷰 목록에서는 stream를 사용해서 작업을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.45.30@2x.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYxH12/dJMcaajr8ay/K2gEmiWO2kSy9vKJ1N757k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYxH12/dJMcaajr8ay/K2gEmiWO2kSy9vKJ1N757k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYxH12/dJMcaajr8ay/K2gEmiWO2kSy9vKJ1N757k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYxH12%2FdJMcaajr8ay%2FK2gEmiWO2kSy9vKJ1N757k%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;1638&quot; height=&quot;1510&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.45.30@2x.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 삭제는 간단합니다. 해당 reviewNo를 클라이언트에서 받아온 뒤에 .deleteById(reviewNo) 메서드를 사용해서 삭제하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 메서드도 마찬가지로 예외 상황을 고려하여 대비해주고, 리뷰 정보에 같이 있는 태그와 이미지도 같이 삭제해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.48.22@2x.png&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU0zp6/dJMcadtFEE8/m6uai4APXsQJL7uBcMFTFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU0zp6/dJMcadtFEE8/m6uai4APXsQJL7uBcMFTFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU0zp6/dJMcadtFEE8/m6uai4APXsQJL7uBcMFTFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU0zp6%2FdJMcadtFEE8%2Fm6uai4APXsQJL7uBcMFTFK%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;1580&quot; height=&quot;1006&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.48.22@2x.png&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 업데이트는 등록과 비슷합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 상황 대비하여 코드를 작성하여 클라이언트에서 받은 데이터를 .update()메소드를 통해 DB에 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 클래스에 원하는 기능을 다 만들었다면 이제 클라이언트와 소통하는 Controller를 구성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버는 API를 통해 소통을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.50.01@2x.png&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6CoDt/dJMcabimMLQ/TiVwaZbXZLJWB5Vl8l4HO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6CoDt/dJMcabimMLQ/TiVwaZbXZLJWB5Vl8l4HO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6CoDt/dJMcabimMLQ/TiVwaZbXZLJWB5Vl8l4HO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6CoDt%2FdJMcabimMLQ%2FTiVwaZbXZLJWB5Vl8l4HO1%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;1568&quot; height=&quot;772&quot; data-filename=&quot;CleanShot 2025-12-01 at 10.50.01@2x.png&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에 PostMapping(&quot;/reviews&quot;) 이렇게 작성했는데 &quot;/reviews&quot;는 진짜 URL이 아니라 클라이언트와 소통하는 경로입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestPart 어노테이션은 여러 파일을 받기 위한 매핑 어노테이션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value = &quot;files&quot; name이 files인 List 형태의 파일들을 받고, required = false 이므로 파일이 없어도 에러가 생기지 않고 서버에 빈 상태 그대로 저장합니다.&amp;nbsp; 그리고 Spring JWT로 로그인이 구현되어 있어 token을 받아 사용하기 때문에 위 사진처럼 예외 상황을 대비하여 코드를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/20</guid>
      <comments>https://taecostudy76.tistory.com/20#entry20comment</comments>
      <pubDate>Mon, 1 Dec 2025 11:15:00 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] 인터페이스 기반 프로젝션이란?</title>
      <link>https://taecostudy76.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터 페이스 기반 프로젝선(Interface-Based Projecttion)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서 엔티티의 전체가 아닌 일부 필드만 선택적으로 조회할 수 있게 해주는 기능입니다.&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;br /&gt;다른 사용자가 상품구매를 하기 전 판단을 할 수 있게 하는 기능을 만들고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tag에 모든 엔티티를 조회하지 않고 필요한 필드만 조회할 수 있는 인터페이스 기반 프로젝션을 사용하고자 했습니다.&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;p data-ke-size=&quot;size16&quot;&gt;- 일반적으로 JPA Repository 쿼리는 엔티티 전체를 조회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하지만 인터페이스 기반 프로젝션을 사용하게 되면, 원하는 필드만 SELECT 하도록 JPA가 쿼리를 최적하 하여 필요 없는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 제외하고 경량 DTO 역할을 하게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 18.54.53@2x.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p99dg/dJMb99Y6OuZ/SCkyDd3dGEFakpNYDF57H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p99dg/dJMb99Y6OuZ/SCkyDd3dGEFakpNYDF57H0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p99dg/dJMb99Y6OuZ/SCkyDd3dGEFakpNYDF57H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp99dg%2FdJMb99Y6OuZ%2FSCkyDd3dGEFakpNYDF57H0%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;846&quot; height=&quot;428&quot; data-filename=&quot;CleanShot 2025-11-24 at 18.54.53@2x.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Interface는 Repository 안에서 선언 한 것이며 따로 만든 Tag 관련 Entity나 DTO 하고는 전혀 상관없는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝션 인터페이스는 JPA가 쿼리 결과를 매핑해주는 임시 DTO 역할(조회 전용, 일시적 데이터 받기)만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 18.58.22@2x.png&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwdvqJ/dJMcahCP4pV/FzM8K8FWfpXav2mfEbXUL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwdvqJ/dJMcahCP4pV/FzM8K8FWfpXav2mfEbXUL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwdvqJ/dJMcahCP4pV/FzM8K8FWfpXav2mfEbXUL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwdvqJ%2FdJMcahCP4pV%2FFzM8K8FWfpXav2mfEbXUL0%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;1584&quot; height=&quot;720&quot; data-filename=&quot;CleanShot 2025-11-24 at 18.58.22@2x.png&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Query 인터페이스를 사용하여 JPA가 기본으로 제공하는 쿼리문이 아닌 직접 쿼리문을 작성할 수 있는 어노테이션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 상품과 특정 피부타입인 리뷰들의 태그별 개수를 구하는 쿼리문을 작성항 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리문을 작성할 때 주의해야 하는 점은 인터페이스 프로젝션을 미리 만들어 놨기 때문에 필드 이름과 SELECT 별칭은 일치해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 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;/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;위 코드에서 보면 TagStatSimple 타입으로 프록시가 생성이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TagStatSimple은 직접 구현한 실체 클래스가 아니라 동적으로 getter만 존재하는 임시 익명 프록시 객체를 만들어 각 row의 값을 할당합니다.&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;개발자는 .getTagName, getCount(), getTagStatus를 부르면 프록시 객체가 내부에 저장된 값을 그대로 돌려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.29.23@2x.png&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg2fE0/dJMcahpjs92/KsD8VCRmO8XAKRtvN9iH1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg2fE0/dJMcahpjs92/KsD8VCRmO8XAKRtvN9iH1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg2fE0/dJMcahpjs92/KsD8VCRmO8XAKRtvN9iH1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg2fE0%2FdJMcahpjs92%2FKsD8VCRmO8XAKRtvN9iH1K%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;1564&quot; height=&quot;470&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.29.23@2x.png&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 사용자가 쓴&amp;nbsp; 총 리뷰 수를 구해서 백분율을 계산을 할 때 분모를 사용하고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 개수를 조회하는데 있어서 혹시 모를 중복 방지를 위해 DISTINCT를 걸었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN을 통해서 원하는 테이블까지 경로를 만들고 oi.product.prdNo = :prdNo 를 통해서 리뷰가 달린 주문 항목의 상품 번호가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상품 번호와 같은 경우 와 s.skinType = :skinType 회원이 설정한 skinType이 조건과 동일한 경우를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Where 절로 하여 분모를 정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.47.39@2x.png&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv9zVo/dJMcagcRZpE/fJtvlarkprZHa6yqAf5V4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv9zVo/dJMcagcRZpE/fJtvlarkprZHa6yqAf5V4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv9zVo/dJMcagcRZpE/fJtvlarkprZHa6yqAf5V4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv9zVo%2FdJMcagcRZpE%2FfJtvlarkprZHa6yqAf5V4k%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;1566&quot; height=&quot;878&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.47.39@2x.png&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 로그인한 사용자의 피부 타입을 가져오기 위해서 userSkin에 로그인한 사용자를 넣어주고, 그 사용자의 피부타입을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository에서 만든 총 리뷰 수를 구하는 메서드를 불러 값을 넣어주고, 만약 리뷰수가 없다면 빈 값을 넣어 주는 작업을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.47.59@2x.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RUcBA/dJMcabo5sI1/qwK2ygGCaw84k4evHBfln0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RUcBA/dJMcabo5sI1/qwK2ygGCaw84k4evHBfln0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RUcBA/dJMcabo5sI1/qwK2ygGCaw84k4evHBfln0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRUcBA%2FdJMcabo5sI1%2FqwK2ygGCaw84k4evHBfln0%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;1858&quot; height=&quot;278&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.47.59@2x.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분자 계산에서는 상위 3개의 태그를 가져올 예정이기 때문에 Pageable를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pageable이란 페이징을 위한 설정 정보를 담는 인터페이스 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PageRequest.of() 메서드를 사용해서 첫 번째부터 3개 데이터 만들 가져오도록 Pageable를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.48.12@2x.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IvosD/dJMcacVPPP7/74Kb5K3nhiGn8ta5BTugXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IvosD/dJMcacVPPP7/74Kb5K3nhiGn8ta5BTugXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IvosD/dJMcacVPPP7/74Kb5K3nhiGn8ta5BTugXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvosD%2FdJMcacVPPP7%2F74Kb5K3nhiGn8ta5BTugXk%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;1572&quot; height=&quot;530&quot; data-filename=&quot;CleanShot 2025-11-24 at 19.48.12@2x.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Proejction을 매핑하여 TagStat DTO로 변환하는 작업을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;percantage에 각 태그마다의 빈도수를 백분율로 변환하고 dto 객체를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-24 at 20.02.02@2x.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/slDsl/dJMcaiItrkR/SJpiuYjvC8S4habgMkDuk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/slDsl/dJMcaiItrkR/SJpiuYjvC8S4habgMkDuk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/slDsl/dJMcaiItrkR/SJpiuYjvC8S4habgMkDuk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FslDsl%2FdJMcaiItrkR%2FSJpiuYjvC8S4habgMkDuk0%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;2304&quot; height=&quot;1228&quot; data-filename=&quot;CleanShot 2025-11-24 at 20.02.02@2x.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 데이터를 사용해서 원하는 기능을 완성할 수 있었습니다.&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/19</guid>
      <comments>https://taecostudy76.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 24 Nov 2025 20:03:34 +0900</pubDate>
    </item>
    <item>
      <title>멀티캠퍼스 [현대 이지웰] Spring 어노테이션 기록하기</title>
      <link>https://taecostudy76.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 프레임워크를 이용한 백엔드 개발을 진행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 제가 진행한 내용을 기록하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 dto와 Entity를 먼저 설계하고 Reposiroty와 Service를 구현하고 Controller를 사용해서 클라이언트와 API 통신을 할 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 20.49.18@2x.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nzwWp/dJMcacaqwm0/A7ofngPXkxlKlwzNZFvmwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nzwWp/dJMcacaqwm0/A7ofngPXkxlKlwzNZFvmwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nzwWp/dJMcacaqwm0/A7ofngPXkxlKlwzNZFvmwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnzwWp%2FdJMcacaqwm0%2FA7ofngPXkxlKlwzNZFvmwk%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;1060&quot; height=&quot;884&quot; data-filename=&quot;CleanShot 2025-11-18 at 20.49.18@2x.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 ReviewDto를 살펴 보면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어노테이션들이 여러 개를 설정했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Getter와 @Setter는 각 필드의 get 메서드와 set 메서드를 Spring이 자동으로 만들어 주는 기능을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 따로 제가 코드를 작성할 필요가 없어 간편하고 길게 코드를 쓰지 않아도 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NoArgsConstructor는 매개변수 없는 기본 생성자를 자동으로 생성해 주는 어노테이션입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@AllArgsConstructors는 클래스 내 모든 필드를 매개변수로 받는 생성자를 자동으로 생성하는 어노테이션입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Builder는 빌더 패턴을 쉽게 구현할 수 있도록 도와주는 어노테이션입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;필드를 체인 방식으로 설정하며 객체를 생성할 수 있는 빌더클래스를 자동 생성합니다. 가독성과 유지보수성을 향상하는데 유용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 20.54.22@2x.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baq7ST/dJMcahCNVoM/aNuluDR0kuawLdIdXh9kv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baq7ST/dJMcahCNVoM/aNuluDR0kuawLdIdXh9kv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baq7ST/dJMcahCNVoM/aNuluDR0kuawLdIdXh9kv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbaq7ST%2FdJMcahCNVoM%2FaNuluDR0kuawLdIdXh9kv1%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;1338&quot; height=&quot;808&quot; data-filename=&quot;CleanShot 2025-11-18 at 20.54.22@2x.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 Dto 클래스 안에 Entity를 Dto로 변환하는 메서드를 만들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;장점 태그와 단점 태그 두 개가 따로 필요해서 나눴고, Review테이블과 Tag테이블을 연결해 주는&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테이블인 ReviewTagMaps 테이블 Entity에서 pk를 찾고 있으면 빌더 패턴을 사용해서 각 태그 변수에 넣어 줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Stream를 사용했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java의 Stream API는 선언형 프로그래밍 방식으로 기존의 반복문 대신 필터링, 매핑, 정렬 등을 연속된 연산 체인으로&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간결하게 표현할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. map()&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Stream 내의 각 요소를 내가 원하는 형태로 변환하는 중간 연산입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- ex) .map(ReviewTagMap::getTag) // ReviewTagMap 객체에서 getTag()로 Tag 객체만 추출&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. filter()&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 원하는 조건(Boolean)에 따라 Stream의 요소를 선별해서 남기는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- ex) .filter(tag&amp;nbsp; -&amp;gt; tag.getTagStatus().equals(&quot;장점&quot;)) // 추출한 Tag 객체에서 getTagStatus()가 &quot;장점&quot;인 것만 남기고 나머지는 버립니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. collect()&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 가공한 Stream의 요소들을 다시 List, Set등의 컬렉션으로 수집하는 최종연산입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- ex) .collect(Collectors.toList()) // Stream에 남아있는 요소들을 모두 List로 변환해서 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.10.31@2x.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FhiDs/dJMcachb24U/Ufs5ienKNXn1ddR5ZOGF8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FhiDs/dJMcachb24U/Ufs5ienKNXn1ddR5ZOGF8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FhiDs/dJMcachb24U/Ufs5ienKNXn1ddR5ZOGF8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFhiDs%2FdJMcachb24U%2FUfs5ienKNXn1ddR5ZOGF8K%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;1498&quot; height=&quot;652&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.10.31@2x.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미지 관련 dto 또한 Stream를 사용해서 toDTO 메서드를 꺼내오고 List로 만들어 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 빌더 패턴을 사용해서 Entity를 dto로 변환해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.23.13@2x.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dctTIR/dJMcaap83mF/kfqSwj120ZVsXEJZNHtOIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dctTIR/dJMcaap83mF/kfqSwj120ZVsXEJZNHtOIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dctTIR/dJMcaap83mF/kfqSwj120ZVsXEJZNHtOIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdctTIR%2FdJMcaap83mF%2FkfqSwj120ZVsXEJZNHtOIk%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;1462&quot; height=&quot;996&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.23.13@2x.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Entity는 JPA 엔티티 클래스임을 명시합니다. 데이터베이스 테이블과 1대 1로 매핑이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Table(name = &quot;review&quot;) 클래스가 매핑될 DB 테이블 이름을 지정하고 없으면 클래스명이 테이블명으로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EntityListeners(AuditingEntityListener.class)는 Auditing 기능(생성일, 수정일) 자동 입력을 활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Id 테이블의 기본키와 매핑될 필드입니다. 반드시 있어야 합니다.&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;@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;review_seq_gen&quot;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어노테이션은 PK 값을 자동 생성 전략을 지정합니다. 시퀀스를 사용하여 PK 값을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SequenceGenerator는 시퀀스 객체의 이름과 실제 DB 시퀀스를 필요로 하고 설정을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Column 매핑되는 DB 컬럼 설정. 이름 길이 등 여러 속성을 상세히 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Lob Large Object 타입에 사용하며 긴 텍스트나 대용량 데이터를 다룰 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ManyToone(fetch = FetchType.LAZY) 다대일 관계 매핑으로 여기서는 Review가 OrderOtem 하나만 참조한다는 의미입니다.&lt;br /&gt;LAZY는 실제 값이 필요할 때만 DB에서 데이터를 불러온다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JoinColumn(name = &quot;orderItemNo&quot;) FK 외래키 컬럼의 이름 지정을 합니다. 이 컬럼이 연결되는 관계의 데이터베이스 컬럼이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.23.26@2x.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efGHnU/dJMcaaXY0SC/NkVbd2TI38K6iW2k7C6A40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efGHnU/dJMcaaXY0SC/NkVbd2TI38K6iW2k7C6A40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efGHnU/dJMcaaXY0SC/NkVbd2TI38K6iW2k7C6A40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefGHnU%2FdJMcaaXY0SC%2FNkVbd2TI38K6iW2k7C6A40%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;1482&quot; height=&quot;852&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.23.26@2x.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@OneToMany(mappedBy = &quot;review&quot;, cascade = CascadeType.REMOVE) 일대다 관계 매핑으로 Review가 여러 ReviewTagMap과 ReviewImage를 가질 수 있고, cascade는 Review가 삭제될 때 관련 객체도 함께 삭제한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@CreatedDate, @LastModifiedDate는 JPA Auditing에서 사용하며 각각 생성일, 수정일 자동 저장에 쓰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.40.14@2x.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by0Rxi/dJMcafdU2nt/sxiUn46HuF51kEoLGdiKM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by0Rxi/dJMcafdU2nt/sxiUn46HuF51kEoLGdiKM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by0Rxi/dJMcafdU2nt/sxiUn46HuF51kEoLGdiKM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby0Rxi%2FdJMcafdU2nt%2FsxiUn46HuF51kEoLGdiKM0%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;512&quot; height=&quot;368&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.40.14@2x.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 repository를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.40.48@2x.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8nnMq/dJMcah3SmR6/O9LdIus8FXE4l451Xj5os1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8nnMq/dJMcah3SmR6/O9LdIus8FXE4l451Xj5os1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8nnMq/dJMcah3SmR6/O9LdIus8FXE4l451Xj5os1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8nnMq%2FdJMcah3SmR6%2FO9LdIus8FXE4l451Xj5os1%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;1230&quot; height=&quot;702&quot; data-filename=&quot;CleanShot 2025-11-18 at 21.40.48@2x.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepository를 상속받아 사용하여 JPA를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속을 받으면 기본 CRUD 메서드를 가져다가 쓸 수 있고, 직접 선언을 안해도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 나중에 보면 확인하고 싶어서 주석 처리로 써놨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기본 제공 이외에 다른 메소드를 사용하고 싶으면 위 사진처럼 작성해서 사용을 하면 JPA가 자동으로 쿼리문을 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 직접 쿼리문을 작성해서 하는 방법도 있습니다.&lt;/p&gt;</description>
      <category>개발/백엔드</category>
      <category>[현대이지웰]java풀스택</category>
      <category>멀티캠퍼스it부트캠프</category>
      <category>부트캠프후기</category>
      <category>풀스택부트캠프</category>
      <author>taecostudy76</author>
      <guid isPermaLink="true">https://taecostudy76.tistory.com/18</guid>
      <comments>https://taecostudy76.tistory.com/18#entry18comment</comments>
      <pubDate>Tue, 18 Nov 2025 21:44:17 +0900</pubDate>
    </item>
  </channel>
</rss>