<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩하는 코끼리</title>
    <link>https://blu-blu.tistory.com/</link>
    <description>코양이의 스탠딩 코딩쇼</description>
    <language>ko</language>
    <pubDate>Fri, 17 Apr 2026 04:44:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코양이 </managingEditor>
    <image>
      <title>코딩하는 코끼리</title>
      <url>https://tistory1.daumcdn.net/tistory/6956624/attach/6bdb04b8282b4687a7223a3d19437f73</url>
      <link>https://blu-blu.tistory.com</link>
    </image>
    <item>
      <title>[백준] 2941 크로아티아 알파벳 | 구현, 문자열 | 실버 Ⅴ | JAVA</title>
      <link>https://blu-blu.tistory.com/180</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  정답 코드&lt;/h2&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.io.*;

public class Main{
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String input = br.readLine();

        int count = 0;

        int i = 0;

        while (i &amp;lt; input.length()) {
            // dz= 패턴 먼저 찾기
            if (i+2 &amp;lt; input.length() &amp;amp;&amp;amp; input.substring(i, i+3).equals(&quot;dz=&quot;)){
                count++;
                i += 3;
            }
            // 마지막 한글자 이전까지 두글자 체크
            else if (i+1 &amp;lt; input.length()) {
                String two = input.substring(i, i+2);
                if (two.equals(&quot;c=&quot;) || two.equals(&quot;c-&quot;) ||
                        two.equals(&quot;d-&quot;) || two.equals(&quot;lj&quot;) ||
                        two.equals(&quot;nj&quot;) || two.equals(&quot;s=&quot;) ||
                        two.equals(&quot;z=&quot;)) {
                    count++;
                    i += 2;
                }
                else {
                    count++;
                    i++;
                }
            }
            // 마지막 한 글자
            else {
                count++;
                i++;
            }

        }

        System.out.println(count);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  조건문 흐름 설명 &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건 분기 설명 조건&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 90.1163%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.9535%;&quot;&gt;조건 분기&lt;/td&gt;
&lt;td style=&quot;width: 35.6977%;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;width: 35.3488%;&quot;&gt;조건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.9535%;&quot;&gt;if&lt;/td&gt;
&lt;td style=&quot;width: 35.6977%;&quot;&gt;dz=가 있는 경우 (3글자)&lt;/td&gt;
&lt;td style=&quot;width: 35.3488%;&quot;&gt;i+2 &amp;lt; length &amp;amp;&amp;amp; substring(i, i+3) == &quot;dz=&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.9535%;&quot;&gt;else if 내부 if&lt;/td&gt;
&lt;td style=&quot;width: 35.6977%;&quot;&gt;나머지 크로아티아 알파벳 (2글자)&lt;/td&gt;
&lt;td style=&quot;width: 35.3488%;&quot;&gt;&quot;c=&quot;, &quot;c-&quot;, &quot;d-&quot;, &quot;lj&quot;, &quot;nj&quot;, &quot;s=&quot;, &quot;z=&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.9535%;&quot;&gt;else if 내부 else&lt;/td&gt;
&lt;td style=&quot;width: 35.6977%;&quot;&gt;그냥 한 글자 처리&lt;/td&gt;
&lt;td style=&quot;width: 35.3488%;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.9535%;&quot;&gt;else&lt;/td&gt;
&lt;td style=&quot;width: 35.6977%;&quot;&gt;마지막 남은 한 글자&lt;/td&gt;
&lt;td style=&quot;width: 35.3488%;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  예시 문자열: &quot;ljes=njak&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 현재 인덱스 (i) 현재 문자 / 패턴 어떤 조건에 해당? 증가한 i 누적 count&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 68.4884%; height: 297px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;i&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;현재 문자&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;어떤 조건인지&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;증가한 i&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;lj&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else if 내부 if&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+2 &amp;rarr; 2&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;e&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else if 내부 else (매칭 없음)&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+1 &amp;rarr; 3&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;s=&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else if 내부 if&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+2 &amp;rarr; 5&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;nj&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else if 내부 if&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+2 &amp;rarr; 7&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;a&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else if 내부 else&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+1 &amp;rarr; 8&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.32558%;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 12.9907%;&quot;&gt;k&lt;/td&gt;
&lt;td style=&quot;width: 25.1488%;&quot;&gt;else (마지막 글자)&lt;/td&gt;
&lt;td style=&quot;width: 20.3482%;&quot;&gt;+1 &amp;rarr; 9&lt;/td&gt;
&lt;td style=&quot;width: 10.084%;&quot;&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  substring() 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;262&quot; data-start=&quot;202&quot;&gt;substring(i, j)는 &lt;b&gt;i번 인덱스부터 j-1번 인덱스까지&lt;/b&gt; 잘라낸 문자열을 반환합니다.&lt;/li&gt;
&lt;li data-end=&quot;327&quot; data-start=&quot;263&quot;&gt;즉, 문자열 &quot;ljes=njak&quot;에서 input.substring(0, 2)은 &quot;lj&quot;를 반환합니다.&lt;/li&gt;
&lt;li data-end=&quot;408&quot; data-start=&quot;328&quot;&gt;자바에서 substring(a, b)는 &lt;b&gt;a &amp;lt;= index &amp;lt; b 범위의 문자열&lt;/b&gt;을 의미하므로, 끝 인덱스는 포함되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩 테스트 일지  </category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/180</guid>
      <comments>https://blu-blu.tistory.com/180#entry180comment</comments>
      <pubDate>Tue, 10 Jun 2025 19:39:15 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 다대일 채팅(like 버블), 채팅방 개설 및 입장, 채팅방 목록 조회 기능 구현 방법. 처음부터 끝까지 정리.</title>
      <link>https://blu-blu.tistory.com/179</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  다대일 채팅이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 시작하기에 앞서 다대일 채팅이라는 것에 낯선 독자분들을 위해 다대일 채팅 기능에 대해 간단히 설명드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다대일 채팅 기능&lt;/b&gt;이란, 한쪽(1쪽)에 위치한 사용자는 다수(N쪽)에 있는 사용자들의 채팅을 모두 확인할 수 있고, 자신의 메시지가 N쪽의 모든 사용자에게 전달됩니다. 반면, N쪽의 사용자는 다른 N쪽 사용자들의 채팅을 볼 수 없으며, 오직 1쪽 사용자의 메시지만 확인할 수 있습니다. 또한, N쪽 사용자가 보낸 메시지는 다른 N쪽 사용자에게는 공유되지 않고, 1쪽 사용자에게만 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 그림으로 다음과 같이 표현해봤습니다!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceUGJn/btsLurYLRXp/4fCRk4U3SlfvtD8tfWEDE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceUGJn/btsLurYLRXp/4fCRk4U3SlfvtD8tfWEDE1/img.png&quot; data-alt=&quot;왼 : 1쪽 사용자 시점 / 오 : N쪽 사용자 시점&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceUGJn/btsLurYLRXp/4fCRk4U3SlfvtD8tfWEDE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceUGJn%2FbtsLurYLRXp%2F4fCRk4U3SlfvtD8tfWEDE1%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;716&quot; height=&quot;354&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;왼 : 1쪽 사용자 시점 / 오 : N쪽 사용자 시점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1쪽 사용자 시점&lt;/b&gt;: 단체 채팅방에서 모든 참여자와 소통하는 느낌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N쪽 사용자 시점&lt;/b&gt;: 1쪽 사용자와 1:1 채팅을 하는 느낌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 형태는 대표적으로 연예인과 팬들이 소통하는 애플리케이션인 버블과 유사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱을 보고&amp;nbsp;다대일 채팅은 1쪽 사용자가 방장 또는 주인 역할을 하며, 다수의 사용자와 효율적으로 소통하고 정보를 전달할 수 있어 매우 유용한 기능이라고 생각했습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;⭐ ERD 구성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brgZ8P/btsLuJdPhWX/xv5JnOBsxtdd6YLo1dqqs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brgZ8P/btsLuJdPhWX/xv5JnOBsxtdd6YLo1dqqs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brgZ8P/btsLuJdPhWX/xv5JnOBsxtdd6YLo1dqqs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrgZ8P%2FbtsLuJdPhWX%2Fxv5JnOBsxtdd6YLo1dqqs1%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;685&quot; height=&quot;270&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다대일 채팅 시스템에서 주요 엔티티는 &lt;b&gt;User&lt;/b&gt;, &lt;b&gt;ChatRoom&lt;/b&gt;, &lt;b&gt;ChatRoomMember&lt;/b&gt;, 그리고 &lt;b&gt;Message&lt;/b&gt;입니다. 각각의 엔티티는 아래와 같은 역할을 담당합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;User&lt;/b&gt;: 시스템의 사용자 정보와 소셜 로그인 관련 데이터를 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChatRoom&lt;/b&gt;: 채팅방의 기본 정보와 생성자 정보를 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChatRoomMember&lt;/b&gt;: 사용자가 특정 채팅방에 속해 있는 관계를 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Message&lt;/b&gt;: 채팅방 내에서 주고받은 메시지를 기록.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ User 엔티티&lt;/h3&gt;
&lt;pre id=&quot;code_1734966595921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;users&quot;)
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // AUTO_INCREMENT 적용

    @Column(name = &quot;social_id&quot;, nullable = false, unique = true)
    private String socialId; // 소셜 로그인에서 받은 사용자 ID (예: Google ID, Kakao ID)

    @Column(name = &quot;provider&quot;, nullable = false)
    private String provider; // 소셜 로그인 제공자 (Google, Kakao, Naver 등)

    @Column(name = &quot;access_token&quot;, nullable = true)
    private String accessToken; // 액세스 토큰 (소셜 로그인 인증 후 받은 토큰)

    @Column(name = &quot;name&quot;, nullable = true)
    private String name; // 사용자 이름 (옵션)

    @Column(name = &quot;created_at&quot;)
    private Timestamp createdAt;

    @Column(name = &quot;updated_at&quot;)
    private Timestamp updatedAt;

    @OneToMany(mappedBy = &quot;user&quot;)
    private List&amp;lt;ChatRoomMember&amp;gt; chatRoomMembers; // User가 참여한 ChatRooms에 대한 관계

    @OneToMany(mappedBy = &quot;sender&quot;)
    private List&amp;lt;Message&amp;gt; messages; // 사용자가 보낸 메시지들에 대한 관계

    @PrePersist
    public void prePersist() {
        // 엔티티가 처음 저장될 때 createdAt을 현재 시간으로 설정
        this.createdAt = new Timestamp(System.currentTimeMillis());
    }

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return List.of();
    }

    @Override
    public String getPassword() {
        return &quot;&quot;;
    }

    @Override
    public String getUsername() {
        return &quot;&quot;;
    }

    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return UserDetails.super.isEnabled();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UserDetails 상속&lt;/b&gt;: Spring Security와 통합을 위한 인터페이스. 인증/인가와 관련된 메서드들을 제공하지만, 이 프로젝트에서는 비활성화된 기본 구현을 유지함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@OneToMany 관계&lt;/b&gt;: 사용자와 관련된 채팅방 및 메시지 데이터를 연결함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ ChatRoom 엔티티&lt;/h3&gt;
&lt;pre id=&quot;code_1734966643212&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;chat_rooms&quot;)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatRoom {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // AUTO_INCREMENT 적용

    @ManyToOne
    @JoinColumn(name = &quot;created_by&quot;, nullable = false)
    private User createdBy; // `Users` 테이블의 `id`를 참조하는 외래 키

    @Column(name = &quot;name&quot;, nullable = false)
    private String name;

    @Column(name = &quot;password&quot;)
    private String password;

    @Column(name = &quot;created_at&quot;)
    private Timestamp createdAt;

    @Column(name = &quot;updated_at&quot;)
    private Timestamp updatedAt;

    @OneToMany(mappedBy = &quot;chatRoom&quot;)
    private List&amp;lt;ChatRoomMember&amp;gt; members; // ChatRoom에 속한 멤버들

    @PrePersist
    public void prePersist() {
        // 엔티티가 처음 저장될 때 createdAt과 updatedAt을 현재 시간으로 설정
        this.createdAt = new Timestamp(System.currentTimeMillis());
        this.updatedAt = this.createdAt;
    }

    @PreUpdate
    public void preUpdate() {
        // 엔티티가 수정될 때 updatedAt을 현재 시간으로 설정
        this.updatedAt = new Timestamp(System.currentTimeMillis());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;createdBy 필드&lt;/b&gt;: 채팅방을 생성한 사용자를 나타내며, User 엔티티와 @ManyToOne 관계를 맺음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타임스탬프 관리&lt;/b&gt;: @PrePersist와 @PreUpdate를 통해 createdAt과 updatedAt 필드를 자동 갱신.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ ChatRoomMember 엔티티&lt;/h3&gt;
&lt;pre id=&quot;code_1734966681644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;chat_room_members&quot;)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatRoomMember {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // AUTO_INCREMENT 적용

    @ManyToOne
    @JoinColumn(name = &quot;chatroom_id&quot;, nullable = false)
    private ChatRoom chatRoom; // `ChatRooms` 테이블의 `id`를 참조하는 외래 키

    @ManyToOne
    @JoinColumn(name = &quot;user_id&quot;, nullable = false)
    private User user; // `Users` 테이블의 `id`를 참조하는 외래 키

    @Column(name = &quot;joined_at&quot;)
    private Timestamp joinedAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;N:N 관계 매핑&lt;/b&gt;: 다대일(사용자와 채팅방)을 통해 N:N 관계를 구현.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;joinedAt 필드&lt;/b&gt;: 사용자가 채팅방에 참여한 시점을 기록.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ Message 엔티티&lt;/h3&gt;
&lt;pre id=&quot;code_1734966704298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;messages&quot;)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // AUTO_INCREMENT 적용

    @ManyToOne
    @JoinColumn(name = &quot;chatroom_id&quot;, nullable = false)
    private ChatRoom chatRoom; // `ChatRooms` 테이블의 `id`를 참조하는 외래 키

    @ManyToOne
    @JoinColumn(name = &quot;sender_id&quot;, nullable = false)
    private User sender; // `Users` 테이블의 `id`를 참조하는 외래 키

    @Column(name = &quot;content&quot;, nullable = false)
    private String content; // 메시지 내용

    @Column(name = &quot;sent_at&quot;)
    private Timestamp sentAt; // 메시지 전송 시각
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 저장&lt;/b&gt;: 각 메시지를 DB에 저장. 데이터 보존성은 높지만, 성능에 영향을 줄 가능성이 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개선 방안&lt;/b&gt;: Redis와 같은 메모리 캐시를 활용하여 자주 사용되는 메시지를 캐싱하고, 주기적으로 배치 작업을 통해 DB에 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 다대일 채팅 기능 구현 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ WebSocketConfig 설정&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // /queue는 클라이언트로 브로드캐스트할 메시지에 대한 경로
        config.enableSimpleBroker(&quot;/queue&quot;);

        // /app은 클라이언트가 서버로 보낼 메시지에 대한 경로
        config.setApplicationDestinationPrefixes(&quot;/app&quot;);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 클라이언트가 연결할 수 있는 웹소켓 엔드포인트를 설정
        registry.addEndpoint(&quot;/ws&quot;)  // /ws 경로로 연결을 설정
                .setAllowedOriginPatterns(&quot;*&quot;)  // 모든 출처에서 요청을 허용
                .withSockJS();  // SockJS를 사용하여 연결 안정성을 높임
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;클래스는&amp;nbsp;Spring&amp;nbsp;WebSocket&amp;nbsp;메시지&amp;nbsp;브로커를&amp;nbsp;구성하는&amp;nbsp;설정&amp;nbsp;파일로,&amp;nbsp;프론트엔드에서&amp;nbsp;WebSocket&amp;nbsp;연결을&amp;nbsp;설정할&amp;nbsp;때&amp;nbsp;중요한&amp;nbsp;역할을&amp;nbsp;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주요 기능:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;configureMessageBroker: 메시지 브로커 경로를 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/queue: 클라이언트로 브로드캐스트될 메시지의 대상 경로입니다.&lt;/li&gt;
&lt;li&gt;/app: 클라이언트가 서버로 메시지를 보낼 때 사용하는 경로입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;registerStompEndpoints: 클라이언트가 /ws 경로로 연결할 수 있도록 엔드포인트를 설정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setAllowedOriginPatterns(&quot;*&quot;):&amp;nbsp;모든&amp;nbsp;출처에서의&amp;nbsp;연결을&amp;nbsp;허용합니다.&lt;/li&gt;
&lt;li&gt;withSockJS:&amp;nbsp;SockJS를&amp;nbsp;통해&amp;nbsp;WebSocket을&amp;nbsp;지원하지&amp;nbsp;않는&amp;nbsp;환경에서도&amp;nbsp;연결&amp;nbsp;안정성을&amp;nbsp;보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 이유&lt;/b&gt;&lt;br /&gt;이 클래스는 백엔드의 다른 메서드에서 직접 호출되지 않더라도 WebSocket 연결을 설정하기 위해 필수적입니다. 클라이언트는 /ws 엔드포인트로 연결을 시도하고, 설정된 경로 규칙에 따라 메시지를 주고받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 메시지 전송 로직&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1️⃣&lt;span&gt; DTO 파일&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1734967236626&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class MessageRequest {
    private Long chatRoomId;
    private String content;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트로부터 메시지 전송 요청을 받을 때 사용하는 데이터 전송 객체입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chatRoomId: 메시지가 전송될 채팅방 ID.&lt;/li&gt;
&lt;li&gt;content: 전송할 메시지 내용.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2️⃣ Controller 파일&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/messages&quot;)
@RequiredArgsConstructor
@Tag(name = &quot;채팅&quot;, description = &quot;채팅 API를 제공합니다.&quot;)
public class MessageController {

    private final MessageService messageService;

    // 메시지 전송
    @Operation(summary = &quot;메시지 전송&quot;, description = &quot;특정 채팅방에 메시지를 전송합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;메시지 전송 완료&quot;),
            @ApiResponse(responseCode = &quot;400&quot;, description = &quot;잘못된 요청&quot;)
    })
    @PostMapping
    public ResponseEntity&amp;lt;String&amp;gt; sendMessage(
            @RequestBody MessageRequest request,
            @AuthenticationPrincipal User user) {
        messageService.sendMessage(request, user);
        return ResponseEntity.ok(&quot;메시지 전송 완료&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API의 컨트롤러로, 메시지 전송 요청을 처리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 메서드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sendMessage:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기능&lt;/b&gt;: 특정 채팅방에 메시지를 전송합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작동 방식&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MessageService를 호출하여 메시지를 저장하고 전달합니다.&lt;/li&gt;
&lt;li&gt;결과적으로 클라이언트가 메시지 전송 결과를 확인할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3️⃣ Service 파일&lt;/h4&gt;
&lt;pre id=&quot;code_1734967299333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MessageService {

    private final MessageRepository messageRepository;
    private final ChatRoomMemberRepository chatRoomMemberRepository;
    private final SimpMessagingTemplate messagingTemplate;
    private final ChatRoomRepository chatRoomRepository;

    @Transactional
    public void sendMessage(MessageRequest request, User user) {
        Optional&amp;lt;ChatRoom&amp;gt; optionalChatRoom = chatRoomRepository.findById(request.getChatRoomId());
        ChatRoom chatRoom = optionalChatRoom.orElseThrow(() -&amp;gt; new RuntimeException(&quot;ChatRoom not found&quot;));

        // 메시지 저장
        Message message = Message.builder()
                .chatRoom(chatRoom)
                .sender(user)
                .content(request.getContent())
                .sentAt(new Timestamp(System.currentTimeMillis()))
                .build();
        messageRepository.save(message);

        if (chatRoom.getCreatedBy().getId().equals(user.getId())) {
            System.out.println(&quot;나(교수: &quot; + user.getProvider() + user.getName() + &quot;) -&amp;gt; &quot;);
        } else {
            System.out.println(&quot;나(학생: &quot; + user.getProvider() + user.getName() + &quot;) -&amp;gt; &quot;);
        }

        // 메시지 전송 로직
        if (chatRoom.getCreatedBy().getId().equals(user.getId())) {
            // 1쪽 User -&amp;gt; 모든 N쪽 User로 메시지 브로드캐스팅
            chatRoomMemberRepository.findAllByChatRoom(chatRoom).forEach(member -&amp;gt; {
                System.out.println(&quot;\t 학생에게 전송 (학생: &quot; +
                        member.getUser().getProvider() +
                        member.getUser().getName() + &quot;)&quot;);
                // 메시지 전송
                messagingTemplate.convertAndSendToUser(
                        member.getUser().getSocialId(),
                        &quot;/queue/messages&quot;,
                        message.getContent()
                );
            });
        } else {
            // N쪽 User -&amp;gt; 1쪽 User로 메시지 전송
            System.out.println(&quot;\t 교수에게 전송 (교수: &quot; +
                    chatRoom.getCreatedBy().getProvider() +
                    chatRoom.getCreatedBy().getName() + &quot;)&quot;);
            // 메시지 전송
            messagingTemplate.convertAndSendToUser(
                    chatRoom.getCreatedBy().getSocialId(),
                    &quot;/queue/messages&quot;,
                    message.getContent()
            );
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스는 메시지를 저장하고 WebSocket 브로커를 통해 메시지를 전달하는 역할을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 메서드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sendMessage:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 저장&lt;/b&gt;: MessageRepository를 통해 메시지를 DB에 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebSocket 메시지 전송&lt;/b&gt;: SimpMessagingTemplate을 사용하여 메시지를 특정 사용자에게 전송합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;convertAndSendToUser&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용 목적&lt;/b&gt;: WebSocket을 통해 특정 사용자에게 메시지를 전송.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파라미터&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;destination: 전송 대상 경로 (/queue/messages).&lt;/li&gt;
&lt;li&gt;payload: 전달할 메시지 내용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 로직&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;교수 -&amp;gt; 학생&lt;/b&gt;: 채팅방의 모든 학생들에게 메시지를 브로드캐스팅.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;학생 -&amp;gt; 교수&lt;/b&gt;: 교수가 생성한 채팅방에서 교수에게 메시지 전송.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpMessagingTemplate의 역할&lt;br /&gt;WebSocket 통신에서 서버가 클라이언트로 실시간 메시지를 전송할 때 사용하는 Spring 프레임워크의 도구입니다. 이를 통해 사용자에게 메시지를 안정적으로 전달할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4️⃣ Repository 파일&lt;/h4&gt;
&lt;pre id=&quot;code_1734967366526&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface ChatRoomMemberRepository extends JpaRepository&amp;lt;ChatRoomMember, Long&amp;gt; {

    // 주어진 채팅방에 속한 모든 멤버를 조회
    List&amp;lt;ChatRoomMember&amp;gt; findAllByChatRoom(ChatRoom chatRoom);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 레포지토리는 특정 채팅방에 속한 멤버 목록을 조회하는 데 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주요 메서드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findAllByChatRoom: 주어진 채팅방의 모든 멤버를 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용&lt;/b&gt;: 메시지를 브로드캐스트하거나 특정 사용자에게 메시지를 전송할 때 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  다대일 채팅 작동 방식 요약&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 /ws 엔드포인트를 통해 WebSocket 연결을 설정.&lt;/li&gt;
&lt;li&gt;메시지 전송 요청이 컨트롤러를 통해 서비스로 전달.&lt;/li&gt;
&lt;li&gt;서비스는 메시지를 데이터베이스에 저장하고 SimpMessagingTemplate을 통해 실시간으로 사용자에게 전달.&lt;/li&gt;
&lt;li&gt;ChatRoomMemberRepository를 사용하여 대상 사용자 목록을 조회하고 메시지를 보냄.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 채팅방 개설 및 입장, 채팅방 조회 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ DTO 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1734967971444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class ChatRoomCreateRequest {
    private String name;
    private String password;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 생성 요청 시 필요한 데이터를 담는 DTO입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name: 채팅방 이름 (필수 입력).&lt;/li&gt;
&lt;li&gt;password: 채팅방 비밀번호 (필수 입력).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734968002744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class ChatRoomJoinRequest {
    private String password;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 입장 요청 시 필요한 데이터를 담는 DTO입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;password: 채팅방 입장 시 입력해야 하는 비밀번호.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734968021558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRoomResponse {
    private Long id;
    private String name;
    private String createdByName; // 생성자 이름
    private Timestamp createdAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 정보를 반환할 때 사용하는 DTO입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id: 채팅방 ID.&lt;/li&gt;
&lt;li&gt;name: 채팅방 이름.&lt;/li&gt;
&lt;li&gt;createdByName: 채팅방 생성자 이름.&lt;/li&gt;
&lt;li&gt;createdAt: 채팅방 생성 시간.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ Controller 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1734968057808&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/chatrooms&quot;)
@RequiredArgsConstructor
@Tag(name = &quot;채팅방&quot;, description = &quot;채팅방 개설 및 입장, 조회 API를 제공합니다.&quot;)
public class ChatRoomController {

    private final ChatRoomService chatRoomService;

    // 채팅방 생성
    @Operation(summary = &quot;채팅방 생성&quot;, description = &quot;새로운 채팅방을 생성합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;채팅방 생성 성공&quot;),
            @ApiResponse(responseCode = &quot;400&quot;, description = &quot;잘못된 요청&quot;)
    })
    @PostMapping
    public ResponseEntity&amp;lt;ChatRoomResponse&amp;gt; createChatRoom(
            @RequestBody ChatRoomCreateRequest request,
            @AuthenticationPrincipal User user) {

        if (user == null) {
            throw new IllegalArgumentException(&quot;인증된 사용자가 아닙니다.&quot;);
        }

        if (request.getName() == null || request.getName().isEmpty()) {
            throw new IllegalArgumentException(&quot;채팅방 이름은 필수입니다.&quot;);
        }
        if (request.getPassword() == null || request.getPassword().isEmpty()) {
            throw new IllegalArgumentException(&quot;비밀번호는 필수입니다.&quot;);
        }

        ChatRoomResponse chatRoomResponse = chatRoomService.createChatRoom(request, user);

        if (chatRoomResponse == null) {
            throw new IllegalStateException(&quot;채팅방 생성에 실패했습니다.&quot;);
        }

        return ResponseEntity.ok(chatRoomResponse);
    }


    // 채팅방 참여
    @Operation(summary = &quot;채팅방 참여&quot;, description = &quot;주어진 비밀번호로 채팅방에 참여합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;참여 성공&quot;),
            @ApiResponse(responseCode = &quot;401&quot;, description = &quot;비밀번호가 틀림&quot;)
    })
    @PostMapping(&quot;/{chatRoomId}/join&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; joinChatRoom(
            @PathVariable Long chatRoomId,
            @RequestBody ChatRoomJoinRequest request,
            @AuthenticationPrincipal User user) {

        if (user == null) {
            throw new IllegalArgumentException(&quot;인증된 사용자가 아닙니다.&quot;);
        }

        boolean success = chatRoomService.joinChatRoom(chatRoomId, request, user);
        if (success) {
            return ResponseEntity.ok(&quot;참여 성공&quot;);
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(&quot;비밀번호가 틀렸습니다.&quot;);
        }
    }


    // 로그인한 사용자가 생성한 채팅방 목록 조회
    @Operation(summary = &quot;내가 만든 채팅방 리스트&quot;, description = &quot;로그인한 사용자가 생성한 채팅방 목록을 조회합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;채팅방 리스트 조회 성공&quot;),
            @ApiResponse(responseCode = &quot;401&quot;, description = &quot;인증되지 않은 사용자&quot;)
    })
    @GetMapping(&quot;/mylist&quot;)
    public ResponseEntity&amp;lt;List&amp;lt;ChatRoomResponse&amp;gt;&amp;gt; getMyChatRooms(
            @AuthenticationPrincipal User user) {

        if (user == null) {
            throw new IllegalArgumentException(&quot;인증된 사용자가 아닙니다.&quot;);
        }

        List&amp;lt;ChatRoomResponse&amp;gt; chatRooms = chatRoomService.getMyChatRooms(user);
        return ResponseEntity.ok(chatRooms);
    }


    // 내가 참여한 채팅방 리스트 조회
    @Operation(summary = &quot;내가 참여한 채팅방 리스트&quot;, description = &quot;로그인한 사용자가 참여한 채팅방 목록을 조회합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;채팅방 리스트 조회 성공&quot;),
            @ApiResponse(responseCode = &quot;401&quot;, description = &quot;인증되지 않은 사용자&quot;)
    })
    @GetMapping(&quot;/list&quot;)
    public ResponseEntity&amp;lt;List&amp;lt;ChatRoomResponse&amp;gt;&amp;gt; getUserChatRooms(
            @AuthenticationPrincipal User user) {

        if (user == null) {
            throw new IllegalArgumentException(&quot;인증된 사용자가 아닙니다.&quot;);
        }

        // 사용자가 참여한 채팅방 목록을 서비스에서 가져옴
        List&amp;lt;ChatRoomResponse&amp;gt; chatRooms = chatRoomService.getChatRooms(user);
        return ResponseEntity.ok(chatRooms);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 요청을 처리하는 컨트롤러입니다. 네 가지 주요 엔드포인트를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;채팅방 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;POST /chatrooms&lt;/b&gt;: 새로운 채팅방을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;: 요청한 사용자와 입력된 채팅방 이름 및 비밀번호가 유효한지 확인.&lt;/li&gt;
&lt;li&gt;서비스 계층을 호출하여 ChatRoomResponse 객체를 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;채팅방 입장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;POST /chatrooms/{chatRoomId}/join&lt;/b&gt;: 지정된 ID의 채팅방에 입장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;: 인증된 사용자와 비밀번호 일치 여부 확인.&lt;/li&gt;
&lt;li&gt;성공 시 &quot;참여 성공&quot; 메시지 반환, 실패 시 401 상태 코드와 함께 &quot;비밀번호가 틀렸습니다.&quot; 메시지 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 만든 채팅방 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GET /chatrooms/mylist&lt;/b&gt;: 사용자가 생성한 채팅방 목록을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;: 인증된 사용자 여부 확인.&lt;/li&gt;
&lt;li&gt;서비스 계층에서 데이터 조회 후 ChatRoomResponse 리스트 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 참여한 채팅방 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GET /chatrooms/list&lt;/b&gt;: 사용자가 참여한 채팅방 목록을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;: 인증된 사용자 여부 확인.&lt;/li&gt;
&lt;li&gt;서비스 계층에서 데이터 조회 후 ChatRoomResponse 리스트 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ Repository 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1734968089496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface ChatRoomRepository extends JpaRepository&amp;lt;ChatRoom, Long&amp;gt; {
    // createdBy가 주어진 사용자와 일치하는 채팅방 리스트를 조회
    List&amp;lt;ChatRoom&amp;gt; findByCreatedBy(User user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 관련 데이터 조작을 담당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;findByCreatedBy(User user)&lt;/b&gt;: 특정 사용자가 생성한 채팅방 목록을 조회.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734968104381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface ChatRoomMemberRepository extends JpaRepository&amp;lt;ChatRoomMember, Long&amp;gt; {
    // 로그인한 사용자가 참여한 채팅방 목록 조회
    List&amp;lt;ChatRoomMember&amp;gt; findByUser(User user);

    // 주어진 채팅방에 속한 모든 멤버를 조회
    List&amp;lt;ChatRoomMember&amp;gt; findAllByChatRoom(ChatRoom chatRoom);

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 참여자 관련 데이터를 다룹니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;findByUser(User user)&lt;/b&gt;: 특정 사용자가 참여한 채팅방 멤버 조회.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;findAllByChatRoom(ChatRoom chatRoom)&lt;/b&gt;: 특정 채팅방의 모든 멤버 조회.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ Service 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1734968149270&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ChatRoomService {

    private final ChatRoomRepository chatRoomRepository;
    private final ChatRoomMemberRepository chatRoomMemberRepository;

    @Transactional
    public ChatRoomResponse createChatRoom(ChatRoomCreateRequest request, User currentUser) {
        ChatRoom chatRoom = ChatRoom.builder()
                .name(request.getName())
                .password(request.getPassword())
                .createdBy(currentUser)
                .createdAt(new Timestamp(System.currentTimeMillis()))
                .build();
        ChatRoom savedChatRoom = chatRoomRepository.save(chatRoom);

        return new ChatRoomResponse(
                savedChatRoom.getId(),
                savedChatRoom.getName(),
                savedChatRoom.getCreatedBy().getName(),
                savedChatRoom.getCreatedAt()
        );
    }

    @Transactional
    public boolean joinChatRoom(Long chatRoomId, ChatRoomJoinRequest request, User user) {
        ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;ChatRoom not found&quot;));

        if (!chatRoom.getPassword().equals(request.getPassword())) {
            return false; // 비밀번호가 틀릴 경우 참여 불가
        }

        ChatRoomMember member = ChatRoomMember.builder()
                .chatRoom(chatRoom)
                .user(user)
                .joinedAt(new Timestamp(System.currentTimeMillis()))
                .build();
        chatRoomMemberRepository.save(member);
        return true;
    }

    // 로그인한 사용자가 생성한 채팅방 목록 조회
    public List&amp;lt;ChatRoomResponse&amp;gt; getMyChatRooms(User user) {
        // 로그인한 사용자가 생성한 채팅방만 가져오기
        List&amp;lt;ChatRoom&amp;gt; chatRooms = chatRoomRepository.findByCreatedBy(user);

        // ChatRoom -&amp;gt; ChatRoomResponse 변환
        return chatRooms.stream()
                .map(chatRoom -&amp;gt; new ChatRoomResponse(chatRoom.getId(), chatRoom.getName(), user.getName(),chatRoom.getCreatedAt()))
                .collect(Collectors.toList());
    }

    // 로그인한 사용자가 참여한 채팅방 목록 조회
    public List&amp;lt;ChatRoomResponse&amp;gt; getChatRooms(User user) {
        // 사용자가 참여한 채팅방 멤버들 조회
        List&amp;lt;ChatRoomMember&amp;gt; chatRoomMembers = chatRoomMemberRepository.findByUser(user);

        // 채팅방 리스트 추출
        return chatRoomMembers.stream()
                .map(chatRoomMember -&amp;gt; new ChatRoomResponse(
                        chatRoomMember.getChatRoom().getId(),
                        chatRoomMember.getChatRoom().getName(),
                        chatRoomMember.getChatRoom().getCreatedBy().getName(),
                        chatRoomMember.getChatRoom().getCreatedAt()))
                .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직을 처리하며, Controller와 Repository 사이를 연결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;채팅방 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createChatRoom 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력된 요청 데이터를 바탕으로 ChatRoom 엔티티를 생성하고 저장.&lt;/li&gt;
&lt;li&gt;생성된 채팅방 데이터를 ChatRoomResponse로 변환하여 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;채팅방 입장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;joinChatRoom 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅방 ID로 ChatRoom 조회 후 비밀번호 검증.&lt;/li&gt;
&lt;li&gt;비밀번호가 일치하면 ChatRoomMember 엔티티 생성 및 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 만든 채팅방 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getMyChatRooms 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Repository에서 해당 사용자가 생성한 채팅방을 조회.&lt;/li&gt;
&lt;li&gt;ChatRoomResponse 리스트로 변환하여 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 참여한 채팅방 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getChatRooms 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Repository에서 해당 사용자가 참여한 채팅방 멤버를 조회.&lt;/li&gt;
&lt;li&gt;ChatRoomResponse 리스트로 변환하여 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  깃허브&lt;/h2&gt;
&lt;figure id=&quot;og_1734968496287&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Suanna01/CocO: [개인프로젝트] 교수-학생 다대일 채팅앱&quot; data-og-description=&quot;[개인프로젝트] 교수-학생 다대일 채팅앱. Contribute to Suanna01/CocO development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Suanna01/Coco&quot; data-og-url=&quot;https://github.com/Suanna01/CocO&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rrW6k/hyXSvfHpjw/12f0pyhaITcoCUfFArU6Dk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m13TY/hyXOm5MPXI/3RL9OKEoWrMl1vk6F9hBMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Suanna01/Coco&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Suanna01/Coco&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rrW6k/hyXSvfHpjw/12f0pyhaITcoCUfFArU6Dk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m13TY/hyXOm5MPXI/3RL9OKEoWrMl1vk6F9hBMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Suanna01/CocO: [개인프로젝트] 교수-학생 다대일 채팅앱&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[개인프로젝트] 교수-학생 다대일 채팅앱. Contribute to Suanna01/CocO development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  다대일 채팅 백엔드 구현 마침표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글을 통해 다대일 채팅 기능의 백엔드 구현 과정을 정리해 보았습니다.&lt;/p&gt;
&lt;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;프론트엔드는 잘 다뤄보지 않았고, 노트북 사양도 감당하지 못하는 거 같아 좀 어려울 거 같지만, 함께 고민하고 성장해 나가길 기대하며, 앞으로도 좋은 기술을 함께 나누는 글로 찾아오겠습니다!&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;challenge&quot; data-emoticon-name=&quot;005&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/challenge/large/005.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/challenge/large/005.png&quot; width=&quot;150&quot; /&gt;&lt;/figure&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>프로젝트  /CocO  </category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/179</guid>
      <comments>https://blu-blu.tistory.com/179#entry179comment</comments>
      <pubDate>Tue, 24 Dec 2024 00:39:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 구글, 카카오, 네이버 소셜 로그인 완전 기초부터 구현까지 이 글 하나로 끝내기</title>
      <link>https://blu-blu.tistory.com/178</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ OAuth 2.0 인증 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 안전하게 사용자 정보를 다른 수 있도록 설계된 오픈 표준 인증 프로토콜입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 주요 목적은 애플리케이션(클라이언트)이 사용자의 비밀번호를 직접 받지 않고, 소셜 플랫폼이 제공하는 권한을 받아 사용자의 데이터에 접근할 수 있도록 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 자세한 정보는 아래 글에서 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1734702053242&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[CocO] Spring Security &amp;amp; OAuth2.0 &amp;amp; JWT Token 소셜로그인 동작 방식과 개념 정리  &quot; data-og-description=&quot; CocO 깃허브 보러가기&amp;nbsp;GitHub - Suanna01/CocO: [개인프로젝트] 교수-학생 다대일 채팅앱[개인프로젝트] 교수-학생 다대일 채팅앱. Contribute to Suanna01/CocO development by creating an account on GitHub.github.com⭐Spr&quot; data-og-host=&quot;blu-blu.tistory.com&quot; data-og-source-url=&quot;https://blu-blu.tistory.com/97&quot; data-og-url=&quot;https://blu-blu.tistory.com/97&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKRgcR/hyXOj8xFfh/C1niVtVyP7oS8Ae6kkEph1/img.png?width=800&amp;amp;height=679&amp;amp;face=0_0_800_679,https://scrap.kakaocdn.net/dn/b69yk8/hyXOlZCvgc/r43FoIUzcghjaNBKrzssl1/img.png?width=800&amp;amp;height=679&amp;amp;face=0_0_800_679,https://scrap.kakaocdn.net/dn/c88A4E/hyXOqGBe3U/oKxMdk4c8AVd6X1lOomqRk/img.png?width=1392&amp;amp;height=1182&amp;amp;face=0_0_1392_1182&quot;&gt;&lt;a href=&quot;https://blu-blu.tistory.com/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blu-blu.tistory.com/97&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKRgcR/hyXOj8xFfh/C1niVtVyP7oS8Ae6kkEph1/img.png?width=800&amp;amp;height=679&amp;amp;face=0_0_800_679,https://scrap.kakaocdn.net/dn/b69yk8/hyXOlZCvgc/r43FoIUzcghjaNBKrzssl1/img.png?width=800&amp;amp;height=679&amp;amp;face=0_0_800_679,https://scrap.kakaocdn.net/dn/c88A4E/hyXOqGBe3U/oKxMdk4c8AVd6X1lOomqRk/img.png?width=1392&amp;amp;height=1182&amp;amp;face=0_0_1392_1182');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CocO] Spring Security &amp;amp; OAuth2.0 &amp;amp; JWT Token 소셜로그인 동작 방식과 개념 정리  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt; CocO 깃허브 보러가기&amp;nbsp;GitHub - Suanna01/CocO: [개인프로젝트] 교수-학생 다대일 채팅앱[개인프로젝트] 교수-학생 다대일 채팅앱. Contribute to Suanna01/CocO development by creating an account on GitHub.github.com⭐Spr&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blu-blu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ 소셜 로그인 동작 원리 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인의 기본 흐름은 OAuth 2.0 인증 방식을 따릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 사용자가 로그인 버튼 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 권한 요청&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 사용자를 소셜 플랫폼의 인증 서버로 리다이렉트하여 권한 승인을 요청합니다.&amp;nbsp;이 요청에는 애플리케이션의 Client ID, 리다이렉트될 URL(Redirect URI), 필요한 권한 범위 등이 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. Authorization Code 발급&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 권한 승인을 완료하면, 소셜 플랫폼의 인증 서버는 애플리케이션(클라이언트)으로 Authorization Code를 보냅니다.&amp;nbsp;이때 Authorization Code는 임시로 발급되는 코드로, 클라이언트가 소셜 플랫폼에서 Access Token을 발급받을 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. Access Token 교환&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 받은 Authorization Code와 함께 소셜 플랫폼의 토큰 서버에 요청을 보내고, Access Token을 발급받습니다.&lt;br /&gt;Access Token은 사용자의 데이터를 안전하게 요청하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. 사용자 정보 조회&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 Access Token을 이용해 소셜 플랫폼의 리소스 서버에서 사용자 정보를 가져옵니다.&lt;br /&gt;예를 들어, Google의 경우 https://www.googleapis.com/userinfo에서 정보를 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. 애플리케이션 로그인 처리&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; 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;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ 각 소셜 플랫폼의 개발자 센터에서 ID, Secret 발급받는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 스크린 샷이 함께 있으면 편한데, 전 따로 캡쳐해두지 않아서 다른 글을 읽으면 더 쉽게 이해하실 수 있을 거 같습니다.&amp;nbsp;&lt;br /&gt;이 부분은 어렵지 않고, 엄청 많은 글에서 다루고 있기 때문에 쉽게 따라하시 수 있을 것입니다.&lt;br /&gt;그래도 간단히 각 소셜 플랫폼의 특징을 정리해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구글
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Google Cloud Console에서 프로젝트 생성 후, OAuth 2.0 Client ID와 Secret을 발급받습니다.&lt;/li&gt;
&lt;li&gt;Redirect URI를 설정합니다. 사용자가 권한 승인을 완료한 후 리다이렉션될 URL을 입력해야 합니다. 이는 프로젝트가 실행되는 URL에 따라 다를 수 있지만, 뒤에 /auth/google/callback을 붙이는 게 통상적입니다.&lt;/li&gt;
&lt;li&gt;Client ID, Client Secret를 발급받을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;카카오
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kakao Developers에서 애플리케이션 생성 후, REST API 키와 Redirect URI를 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;Redirect URI를 설정합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt; REST API 키를 발급받을 수 있습니다. 이는 구글, 네이버의 Client ID처럼 사용됩니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;추가로 보안을 강화하기 위해 Client Secret을 발급받고 싶다면, &lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot;&gt;[내 애플리케이션] &amp;gt; [카카오 로그인] &amp;gt; [보안]에서 [코드 생성]를 눌러 발급받을 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;네이버
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Naver Cloud Platform에서 애플리케이션 등록 후, Client ID와 Client Secret을 발급받습니다.&lt;/li&gt;
&lt;li&gt;Callback URL을 지정합니다.&lt;/li&gt;
&lt;li&gt;Client ID, Client Secret을 발급받을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ application.yaml에 인증 정보 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감한 정보를 외부 설정 파일로 분리하고, 이 파일은 소스 코드와 별도로 관리해야 합니다. &lt;br /&gt;application.yaml 파일은 .gitignore에 추가하여 외부에 노출되지 않도록 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 애플리케이션의 인증 관련 URL 및 키 값을 별도로 관리하면 코드가 간결해지고 유지보수성이 좋아집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1734706197891&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sns:
  google:
    url: https://accounts.google.com/o/oauth2/v2/auth # 고정값
    client:
      id: &amp;lt;발급받은 ID&amp;gt; # 구글 개발자 콘솔에서 발급받은 Client ID
      secret: &amp;lt;발급받은 Secret&amp;gt; # 구글 개발자 콘솔에서 발급받은 Client Secret
    callback:
      url: http://localhost:8080/auth/google/callback # 유저 설정에 따라 변경 가능 (Redirect URI)
    token:
      url: https://oauth2.googleapis.com/token # 고정값

  kakao:
    url: https://kauth.kakao.com/oauth/authorize # 고정값
    client:
      id: &amp;lt;발급받은 ID&amp;gt; # 카카오 개발자 센터에서 발급받은 REST API Key
      secret: &amp;lt;발급받은 Secret&amp;gt; # 카카오 개발자 센터에서 발급받은 Client Secret (선택 사항)
    callback:
      url: http://localhost:8080/auth/kakao/callback # 유저 설정에 따라 변경 가능 (Redirect URI)
    token:
      url: https://kauth.kakao.com/oauth/token # 고정값

  naver:
    url: https://nid.naver.com/oauth2.0/authorize # 고정값
    client:
      id: &amp;lt;발급받은 ID&amp;gt; # 네이버 개발자 센터에서 발급받은 Client ID
      secret: &amp;lt;발급받은 Secret&amp;gt; # 네이버 개발자 센터에서 발급받은 Client Secret
    callback:
      url: http://localhost:8080/auth/naver/callback # 유저 설정에 따라 변경 가능 (Redirect URI)
    token:
      url: https://nid.naver.com/oauth2.0/token # 고정값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;1) 고정값&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization URL (url):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 소셜 플랫폼에서 제공하는 인증 URL로, 해당 값은 플랫폼별로 고정되어 있습니다.&lt;/li&gt;
&lt;li&gt;예:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;구글: https://accounts.google.com/o/oauth2/v2/auth&lt;/li&gt;
&lt;li&gt;카카오: https://kauth.kakao.com/oauth/authorize&lt;/li&gt;
&lt;li&gt;네이버: https://nid.naver.com/oauth2.0/authorize&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Token URL (token.url):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 코드로 Access Token을 교환하기 위한 URL입니다.&lt;/li&gt;
&lt;li&gt;예:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;구글:&lt;span&gt;&amp;nbsp;&lt;/span&gt;https://oauth2.googleapis.com/token&lt;/li&gt;
&lt;li&gt;카카오:&lt;span&gt;&amp;nbsp;&lt;/span&gt;https://kauth.kakao.com/oauth/token&lt;/li&gt;
&lt;li&gt;네이버:&lt;span&gt;&amp;nbsp;&lt;/span&gt;https://nid.naver.com/oauth2.0/token&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 유저별 값 (발급받은 값과 설정 값)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client ID (client.id):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인 API를 사용할 애플리케이션의 고유 식별자입니다.&lt;/li&gt;
&lt;li&gt;각 소셜 플랫폼의 개발자 콘솔에서 애플리케이션 등록 후 발급받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Client Secret (client.secret):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 요청 시 애플리케이션의 신뢰성을 보증하는 비밀 키입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Redirect URI (callback.url):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 소셜 로그인 승인을 완료한 뒤, 인증 결과를 전달받을 애플리케이션의 경로입니다.&lt;/li&gt;
&lt;li&gt;개발자 콘솔에서 사전에 등록한 값과 요청 시 사용하는 값이 정확히 일치해야 합니다.&lt;/li&gt;
&lt;li&gt;예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 개발 환경: http://localhost:8080/auth/{provider}/callback&lt;/li&gt;
&lt;li&gt;프로덕션 환경: https://example.com/auth/&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;{provider}/callback&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 엔티티 파일&lt;/h2&gt;
&lt;pre id=&quot;code_1734708033789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;users&quot;)
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // AUTO_INCREMENT 적용

    @Column(name = &quot;social_id&quot;, nullable = false, unique = true)
    private String socialId; // 소셜 로그인에서 받은 사용자 ID (예: Google ID, Kakao ID)

    @Column(name = &quot;provider&quot;, nullable = false)
    private String provider; // 소셜 로그인 제공자 (Google, Kakao, Naver 등)

    @Column(name = &quot;access_token&quot;, nullable = true)
    private String accessToken; // 액세스 토큰 (소셜 로그인 인증 후 받은 토큰)

    @Column(name = &quot;name&quot;, nullable = true)
    private String name; // 사용자 이름 (옵션)

    @Column(name = &quot;created_at&quot;)
    private Timestamp createdAt;

    @Column(name = &quot;updated_at&quot;)
    private Timestamp updatedAt;

    @OneToMany(mappedBy = &quot;user&quot;)
    private List&amp;lt;ChatRoomMember&amp;gt; chatRoomMembers; // User가 참여한 ChatRooms에 대한 관계

    @OneToMany(mappedBy = &quot;sender&quot;)
    private List&amp;lt;Message&amp;gt; messages; // 사용자가 보낸 메시지들에 대한 관계

    @PrePersist
    public void prePersist() {
        // 엔티티가 처음 저장될 때 createdAt을 현재 시간으로 설정
        this.createdAt = new Timestamp(System.currentTimeMillis());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;이 User 엔티티는 소셜 로그인에서 받은 사용자 정보를 데이터베이스에 저장하고 관리하기 위해 사용됩니다. 주요 필드와 어노테이션 설명은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;@Entity: 이 클래스는 데이터베이스 테이블과 매핑된 엔티티임을 나타냅니다.&lt;/li&gt;
&lt;li&gt;@Id와 @GeneratedValue: id 필드는 기본 키로, 자동으로 값이 생성됩니다.&lt;/li&gt;
&lt;li&gt;@Column: 각 필드와 데이터베이스 컬럼을 매핑합니다. 예를 들어, socialId는 소셜 로그인에서 받은 사용자 고유 ID입니다.&lt;/li&gt;
&lt;li&gt;@PrePersist: 엔티티가 DB에 저장되기 전 createdAt 필드를 자동으로 현재 시간으로 설정합니다.&lt;/li&gt;
&lt;li&gt;@OneToMany: 사용자와 채팅방, 메시지 간의 관계를 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 엔티티는 소셜 로그인 사용자 정보를 저장하고, 관련된 데이터를 관리하는 데 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 소셜 타입 구분 Enum 파일&lt;/h2&gt;
&lt;pre id=&quot;code_1734708183397&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum SocialLoginType { //소셜 로그인 타입을 구분할 enum 클래스
    GOOGLE,
    KAKAO,
    NAVER
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인 제공자(Google, Kakao, Naver 등)를 구분하기 위해 enum을 사용합니다. 이렇게 하면 소셜 로그인 타입을 코드 내에서 쉽게 관리하고 오류를 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 대소문자 변환 파일&lt;/h2&gt;
&lt;pre id=&quot;code_1734708257421&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SocialLoginTypeConverter implements Converter&amp;lt;String, SocialLoginType&amp;gt; { //대문자 값을 소문자로 mapping
    @Override
    public SocialLoginType convert(String s) {
        return SocialLoginType.valueOf(s.toUpperCase());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 입력하는 값(소문자)과 SocialLoginType enum을 매핑하기 위해 Converter를 사용합니다. 대소문자 구분 없이 값을 처리할 수 있게 합니다.&amp;nbsp;예를 들어, &quot;google&quot;을 SocialLoginType.GOOGLE로 변환합니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;0 0 []&quot;&gt;✅ 소셜 공통 클래스&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SocialOauth 인터페이스는 소셜 로그인에서 공통적으로 필요한 메서드를 정의하는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 소셜 로그인 타입별로 해당 인터페이스를 구현하여, 다양한 소셜 로그인 서비스에 대해 일관된 방법으로 접근할 수 있습니다. 이 인터페이스는 세 가지 주요 메서드를 제공합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1734715393771&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface SocialOauth { //소셜 로그인 타입별로 공통적으로 사용될 interface
    /**
     * 각 Social Login 페이지로 Redirect 처리할 URL Build
     * 사용자로부터 로그인 요청을 받아 Social Login Server 인증용 code 요청
     */
    String getOauthRedirectURL();

    /**
     * API Server로부터 받은 code를 활용하여 사용자 인증 정보 요청
     * @param code API Server 에서 받아온 code
     * @return API 서버로 부터 응답받은 Json 형태의 결과를 string으로 반환
     */
    String requestAccessToken(String code);

    default SocialLoginType type() {
        if (this instanceof GoogleOauth) {
            return SocialLoginType.GOOGLE;
        } else if (this instanceof NaverOauth) {
            return SocialLoginType.NAVER;
        } else if (this instanceof KakaoOauth) {
            return SocialLoginType.KAKAO;
        } else {
            return null;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getOauthRedirectURL():
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 소셜 로그인 페이지로 리디렉션될 URL을 반환합니다. 이 URL은 사용자로부터 인증을 받기 위해 소셜 로그인 서버로 요청을 보내는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;requestAccessToken(String code):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인 서버로부터 받은 인증 코드를 사용하여 액세스 토큰을 요청합니다. 이 메서드는 해당 토큰을 반환하여, 이후 사용자의 정보를 요청할 때 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;type()&lt;span&gt;&amp;nbsp;&lt;/span&gt;(default 메서드):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 SocialOauth 객체가 어떤 소셜 로그인 서비스(Google, Naver, Kakao)에 속하는지 구분하는 메서드입니다. GoogleOauth, NaverOauth, KakaoOauth 클래스가 각각 구현하여 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size26&quot;&gt;✅ 소셜별 OAuth 인증 처리 클래스 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 구글 OAuth 인증 처리 클래스&lt;/h3&gt;
&lt;pre id=&quot;code_1734708817217&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class GoogleOauth implements SocialOauth {
    @Value(&quot;${sns.google.url}&quot;)
    private String GOOGLE_SNS_BASE_URL;
    @Value(&quot;${sns.google.client.id}&quot;)
    private String GOOGLE_SNS_CLIENT_ID;
    @Value(&quot;${sns.google.callback.url}&quot;)
    private String GOOGLE_SNS_CALLBACK_URL;
    @Value(&quot;${sns.google.client.secret}&quot;)
    private String GOOGLE_SNS_CLIENT_SECRET;
    @Value(&quot;${sns.google.token.url}&quot;)
    private String GOOGLE_SNS_TOKEN_BASE_URL;

    @Override
    public String getOauthRedirectURL() {
        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
        params.put(&quot;scope&quot;, &quot;profile&quot;);
        params.put(&quot;response_type&quot;, &quot;code&quot;);
        params.put(&quot;client_id&quot;, GOOGLE_SNS_CLIENT_ID);
        params.put(&quot;redirect_uri&quot;, GOOGLE_SNS_CALLBACK_URL);

        String parameterString = params.entrySet().stream()
                .map(x -&amp;gt; x.getKey() + &quot;=&quot; + x.getValue())
                .collect(Collectors.joining(&quot;&amp;amp;&quot;));

        return GOOGLE_SNS_BASE_URL + &quot;?&quot; + parameterString;
    }

    @Override
    public String requestAccessToken(String code) {
        RestTemplate restTemplate = new RestTemplate();

        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
        params.put(&quot;code&quot;, code);
        params.put(&quot;client_id&quot;, GOOGLE_SNS_CLIENT_ID);
        params.put(&quot;client_secret&quot;, GOOGLE_SNS_CLIENT_SECRET);
        params.put(&quot;redirect_uri&quot;, GOOGLE_SNS_CALLBACK_URL);
        params.put(&quot;grant_type&quot;, &quot;authorization_code&quot;);

        ResponseEntity&amp;lt;String&amp;gt; responseEntity =
                restTemplate.postForEntity(GOOGLE_SNS_TOKEN_BASE_URL, params, String.class);

        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            return responseEntity.getBody();
        }
        return &quot;구글 로그인 요청 처리 실패&quot;;
    }


}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 카카오 OAuth 인증 처리 클래스&lt;/h3&gt;
&lt;pre id=&quot;code_1734709231763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class KakaoOauth implements SocialOauth {

    @Value(&quot;${sns.kakao.url}&quot;)
    private String KAKAO_SNS_BASE_URL;
    @Value(&quot;${sns.kakao.client.id}&quot;)
    private String KAKAO_SNS_CLIENT_ID;
    @Value(&quot;${sns.kakao.callback.url}&quot;)
    private String KAKAO_SNS_CALLBACK_URL;
    @Value(&quot;${sns.kakao.client.secret}&quot;)
    private String KAKAO_SNS_CLIENT_SECRET;
    @Value(&quot;${sns.kakao.token.url}&quot;)
    private String KAKAO_SNS_TOKEN_BASE_URL;

    @Override
    public String getOauthRedirectURL() {
        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
        params.put(&quot;response_type&quot;, &quot;code&quot;);
        params.put(&quot;client_id&quot;, KAKAO_SNS_CLIENT_ID);
        params.put(&quot;redirect_uri&quot;, KAKAO_SNS_CALLBACK_URL);

        String parameterString = params.entrySet().stream()
                .map(x -&amp;gt; x.getKey() + &quot;=&quot; + x.getValue())
                .collect(Collectors.joining(&quot;&amp;amp;&quot;));

        return KAKAO_SNS_BASE_URL + &quot;?&quot; + parameterString;
    }

    @Override
    public String requestAccessToken(String code) {
        RestTemplate restTemplate = new RestTemplate();

        // 1. HTTP 헤더 설정 (Content-Type을 application/x-www-form-urlencoded로 설정)
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // 2. 요청 파라미터를 문자열로 인코딩
        MultiValueMap&amp;lt;String, String&amp;gt; params = new LinkedMultiValueMap&amp;lt;&amp;gt;();
        params.add(&quot;code&quot;, code);
        params.add(&quot;client_id&quot;, KAKAO_SNS_CLIENT_ID);
        params.add(&quot;client_secret&quot;, KAKAO_SNS_CLIENT_SECRET);
        params.add(&quot;redirect_uri&quot;, KAKAO_SNS_CALLBACK_URL);
        params.add(&quot;grant_type&quot;, &quot;authorization_code&quot;);

        // 3. HttpEntity 생성 (헤더와 파라미터 포함)
        HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; requestEntity = new HttpEntity&amp;lt;&amp;gt;(params, headers);

        // 4. POST 요청 보내기
        ResponseEntity&amp;lt;String&amp;gt; responseEntity =
                restTemplate.postForEntity(KAKAO_SNS_TOKEN_BASE_URL, requestEntity, String.class);

        // 5. 응답 확인 및 반환
        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            return responseEntity.getBody();
        }
        return &quot;카카오 로그인 요청 처리 실패&quot;;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ 네이버 OAuth 인증 처리 클래스&lt;/h3&gt;
&lt;pre id=&quot;code_1734709270598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NaverOauth implements SocialOauth {
    @Value(&quot;${sns.naver.url}&quot;)
    private String NAVER_SNS_BASE_URL;
    @Value(&quot;${sns.naver.client.id}&quot;)
    private String NAVER_SNS_CLIENT_ID;
    @Value(&quot;${sns.naver.callback.url}&quot;)
    private String NAVER_SNS_CALLBACK_URL;
    @Value(&quot;${sns.naver.client.secret}&quot;)
    private String NAVER_SNS_CLIENT_SECRET;
    @Value(&quot;${sns.naver.token.url}&quot;)
    private String NAVER_SNS_TOKEN_BASE_URL;

    @Override
    public String getOauthRedirectURL() {
        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
        params.put(&quot;response_type&quot;, &quot;code&quot;);
        params.put(&quot;client_id&quot;, NAVER_SNS_CLIENT_ID);
        params.put(&quot;redirect_uri&quot;, NAVER_SNS_CALLBACK_URL);
        params.put(&quot;state&quot;, &quot;random_state_value&quot;); // CSRF 방지를 위한 state 파라미터 추가

        String parameterString = params.entrySet().stream()
                .map(x -&amp;gt; x.getKey() + &quot;=&quot; + x.getValue())
                .collect(Collectors.joining(&quot;&amp;amp;&quot;));

        return NAVER_SNS_BASE_URL + &quot;?&quot; + parameterString;
    }

    @Override
    public String requestAccessToken(String code) {
        RestTemplate restTemplate = new RestTemplate();

        // Header 설정
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // 파라미터 설정 (application/x-www-form-urlencoded)
        MultiValueMap&amp;lt;String, String&amp;gt; params = new LinkedMultiValueMap&amp;lt;&amp;gt;();
        params.add(&quot;code&quot;, code);
        params.add(&quot;client_id&quot;, NAVER_SNS_CLIENT_ID);
        params.add(&quot;client_secret&quot;, NAVER_SNS_CLIENT_SECRET);
        params.add(&quot;redirect_uri&quot;, NAVER_SNS_CALLBACK_URL);
        params.add(&quot;grant_type&quot;, &quot;authorization_code&quot;);
        params.add(&quot;state&quot;, &quot;random_state_value&quot;);

        // HTTP Entity 생성
        HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; requestEntity = new HttpEntity&amp;lt;&amp;gt;(params, headers);

        // 요청 전송
        ResponseEntity&amp;lt;String&amp;gt; responseEntity =
                restTemplate.postForEntity(NAVER_SNS_TOKEN_BASE_URL, requestEntity, String.class);

        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            return responseEntity.getBody();
        }
        return &quot;네이버 로그인 요청 처리 실패&quot;;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Value 어노테이션을 사용하여 application.yaml에서 설정해둔 각 소셜 로그인에 필요한 URL, 클라이언트 ID, 클라이언트 비밀번호, 리디렉션 URL 등을 프로퍼티 파일에서 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;getOauthRedirectURL():
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 Google 소셜 로그인 페이지로 리디렉션되도록 하는 URL을 생성합니다. 파라미터로 scope, response_type, client_id, redirect_uri 등을 설정하여 OAuth 인증을 시작하는 URL을 반환합니다.&lt;/li&gt;
&lt;li&gt;이때 생성되는 URL은 각 소셜 서비스마다 다른 형식으로 요구하기 때문에 각각 따로 클래스를 구현해야합니다.&lt;/li&gt;
&lt;li&gt;각 소셜 서비스의 인증 URL 구조 차이:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구글 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터: client_id, redirect_uri, scope, response_type&lt;/li&gt;
&lt;li&gt;Google은 인증을 요청할 때 scope (예: &quot;profile&quot;)을 명시하고, response_type은 code로 설정합니다. 이 URL을 통해 Google 로그인 페이지로 리디렉션됩니다.&lt;/li&gt;
&lt;li&gt;GOOGLE_SNS_BASE_URL은 Google의 인증 URL이며, 위 코드에서는 이 URL에 파라미터를 추가해 인증 요청 URL을 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카카오 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터: client_id, redirect_uri, response_type&lt;/li&gt;
&lt;li&gt;Kakao의 경우 인증 요청 URL은 Google과 유사하지만, Kakao는 기본적으로 scope 파라미터가 없을 수 있습니다. 대신 Kakao에 필요한 다른 파라미터가 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;KAKAO_SNS_BASE_URL을 사용하여 Kakao 인증 요청 URL을 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네이버 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터: client_id, redirect_uri, response_type, state&lt;/li&gt;
&lt;li&gt;Naver는 CSRF 공격을 방지하기 위해 state 파라미터를 요구합니다. 다른 파라미터들과 함께 Naver의 인증 URL을 생성합니다.&lt;/li&gt;
&lt;li&gt;NAVER_SNS_BASE_URL을 사용하여 Naver 인증 요청 URL을 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;requestAccessToken(String code):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 인증 후 받은 인증 코드를 이용하여 액세스 토큰을 요청합니다. RestTemplate을 사용해 Google의 토큰 발급 API에 POST 요청을 보내고, 성공적인 응답을 반환합니다. 실패 시 오류 메시지를 반환합니다.&lt;/li&gt;
&lt;li&gt;각 소셜 서비스의 토큰 요청 방식 차이
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구글 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터 : code, client_id, client_secret, redirect_uri, grant_type&lt;/li&gt;
&lt;li&gt;RestTemplate의 postForEntity 메서드를 사용하여 파라미터를 Map으로 전송.&lt;/li&gt;
&lt;li&gt;Content-Type 설정이 자동으로 처리되며, 추가적인 헤더나 파라미터 인코딩 없이 요청이 보냄.&lt;/li&gt;
&lt;li&gt;state 파라미터가 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카카오 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터 : code, client_id, client_secret, redirect_uri, grant_type&lt;/li&gt;
&lt;li&gt;Content-Type을 application/x-www-form-urlencoded로 명시적으로 설정. (application/x-www-form-urlencoded는 웹 폼 데이터를 서버로 전송할 때 일반적으로 사용되는 인코딩 형식이기 때문에 오류가 나서 Content-Type을 설정했습니다.)&lt;/li&gt;
&lt;li&gt;HttpEntity를 사용하여 파라미터와 헤더를 함께 전송.&lt;/li&gt;
&lt;li&gt;state 파라미터가 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네이버 OAuth:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필수 파라미터 : code, client_id, client_secret, redirect_uri, grant_type, state&lt;/li&gt;
&lt;li&gt;Content-Type을 application/x-www-form-urlencoded로 명시적으로 설정.&lt;/li&gt;
&lt;li&gt;HttpEntity를 사용하여 파라미터와 헤더를 함께 전송.&lt;/li&gt;
&lt;li&gt;state 파라미터가 추가되어 있음 (보안 및 CSRF 공격 방지를 위한 랜덤 값).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 62.4419%; height: 337px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 18.2558%; height: 22px; text-align: center;&quot;&gt;소셜 서비스&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%; height: 22px; text-align: center;&quot;&gt;인증 URL 필수 파라미터&lt;/td&gt;
&lt;td style=&quot;width: 29.0698%; height: 22px; text-align: center;&quot;&gt;토큰 요청 파라미터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 18.2558%; height: 22px; text-align: center;&quot;&gt;구글 OAuth&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%; height: 22px; text-align: left;&quot;&gt;- client_id&lt;br /&gt;- redirect_uri&lt;br /&gt;- scope&lt;br /&gt;- response_type&lt;/td&gt;
&lt;td style=&quot;width: 29.0698%; height: 22px; text-align: left;&quot;&gt;- code&lt;br /&gt;- client_id&lt;br /&gt;- client_secret&lt;br /&gt;- redirect_uri&lt;br /&gt;- grant_type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 18.2558%; height: 22px; text-align: center;&quot;&gt;카카오 OAuth&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%; height: 22px; text-align: left;&quot;&gt;- client_id&lt;br /&gt;- redirect_uri&lt;br /&gt;- response_type&lt;/td&gt;
&lt;td style=&quot;width: 29.0698%; height: 22px; text-align: left;&quot;&gt;- code&lt;br /&gt;- client_id&lt;br /&gt;- client_secret&lt;br /&gt;- redirect_uri&lt;br /&gt;- grant_type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 18.2558%; height: 22px; text-align: center;&quot;&gt;네이버 OAuth&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%; height: 22px; text-align: left;&quot;&gt;- client_id&lt;br /&gt;- redirect_uri&lt;br /&gt;- response_type&lt;br /&gt;- state&lt;/td&gt;
&lt;td style=&quot;width: 29.0698%; height: 22px; text-align: left;&quot;&gt;- code&lt;br /&gt;- client_id&lt;br /&gt;- client_secret&lt;br /&gt;- redirect_uri&lt;br /&gt;- grant_type &lt;br /&gt;- state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size26&quot;&gt;✅ 레파지토리 구현&lt;/h2&gt;
&lt;pre id=&quot;code_1734716315632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    // 소셜 로그인 ID로 사용자 찾기
    Optional&amp;lt;User&amp;gt; findBySocialId(String socialId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size26&quot;&gt;✅ 서비스 클래스 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스 클래스는 소셜 로그인(OAuth)을 처리하는 핵심적인 역할을 합니다. 사용자가 소셜 로그인을 할 때, 소셜 로그인 타입에 맞는 인증 절차를 관리하고, 사용자 정보를 받아와 저장하는 기능을 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1734708451409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OauthService {
    private final List&amp;lt;SocialOauth&amp;gt; socialOauthList; // 다양한 소셜 로그인 OAuth 처리 객체
    private final UserRepository userRepository; // 사용자 정보를 저장하는 레포지토리
    private static final Logger logger = LoggerFactory.getLogger(OauthService.class);

    // 소셜 로그인 요청 URL을 반환하는 메서드
    public String request(SocialLoginType socialLoginType) {
        SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType); // 주어진 소셜 로그인 타입에 맞는 OAuth 객체 찾기
        return socialOauth.getOauthRedirectURL(); // 해당 OAuth 객체의 리디렉션 URL 반환
    }

    // 인증 코드로 액세스 토큰을 요청하는 메서드
    public String requestAccessToken(SocialLoginType socialLoginType, String code) {
        SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType); // 주어진 소셜 로그인 타입에 맞는 OAuth 객체 찾기
        return socialOauth.requestAccessToken(code); // 액세스 토큰 요청
    }


    // JSON에서 액세스 토큰만 추출하는 메서드
    private String extractAccessTokenFromJson(String accessTokenJson) {
        // JSON을 파싱하여 액세스 토큰만 추출
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode jsonNode = objectMapper.readTree(accessTokenJson);
            // 여기서 필요한 토큰만 반환
            return jsonNode.get(&quot;access_token&quot;) != null ? jsonNode.get(&quot;access_token&quot;).asText() : null;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null; // 실패할 경우 null 반환
        }
    }

    // 액세스 토큰을 사용하여 사용자 정보를 가져오고, 사용자 정보가 있으면 저장하는 메서드
    public User requestAccessTokenAndSaveUser(SocialLoginType socialLoginType, String code) {
        // 1. 액세스 토큰을 포함한 JSON 응답을 요청
        String accessTokenJson = this.requestAccessToken(socialLoginType, code);

        // 2. JSON에서 액세스 토큰만 추출
        String accessToken = extractAccessTokenFromJson(accessTokenJson);

        if (accessToken == null) {
            // 액세스 토큰이 없으면 예외 처리
            throw new RuntimeException(&quot;Failed to extract access token.&quot;);
        }

        // 3. 액세스 토큰을 사용해 사용자 정보 요청
        String userInfo = getUserInfo(socialLoginType, accessToken);

        // 4. 사용자 정보를 파싱하여 User 객체 생성
        User user = parseUserInfo(userInfo, socialLoginType, accessToken);

        // 5. 기존 사용자 확인 후 처리
        Optional&amp;lt;User&amp;gt; existingUser = userRepository.findBySocialId(user.getSocialId());
        if (existingUser.isPresent()) {
            // 이미 존재하는 사용자라면 로그인 처리 (액세스 토큰 갱신 등)
            User existing = existingUser.get();
            existing.setAccessToken(user.getAccessToken()); // 토큰 갱신
            existing.setUpdatedAt(new Timestamp(System.currentTimeMillis())); // 수정 시간 갱신
            return userRepository.save(existing); // 수정된 사용자 정보 저장
        } else {
            // 새 사용자라면 저장
            user.setCreatedAt(new Timestamp(System.currentTimeMillis())); // 생성 시간 설정
            return userRepository.save(user); // 새 사용자 정보 저장
        }
    }

    // 실제 소셜 로그인 API에서 사용자 정보를 받아오는 메서드 (Google, Kakao, Naver 등)
    private String getUserInfo(SocialLoginType socialLoginType, String accessToken) {
        switch (socialLoginType) {
            case GOOGLE:
                return googleApiCall(accessToken);  // Google API 호출 메서드
            case KAKAO:
                return kakaoApiCall(accessToken);  // Kakao API 호출 메서드
            case NAVER:
                return naverApiCall(accessToken);  // Naver API 호출 메서드
            default:
                throw new IllegalArgumentException(&quot;지원되지 않는 소셜 로그인 타입입니다.&quot;); // 지원되지 않는 로그인 타입 오류
        }
    }

    // 각 소셜 로그인 제공자별 API 호출 (실제 API 호출 방식은 각 로그인 서비스의 문서를 참조해야 함)
    // 구글 API 호출 시 응답 상태 코드와 메시지 출력
    public String googleApiCall(String accessToken) {
        try {
            // accessToken을 URL 인코딩
            String encodedAccessToken = URLEncoder.encode(accessToken, &quot;UTF-8&quot;);
            logger.debug(&quot;Encoded access token: {}&quot;, encodedAccessToken);

            String url = &quot;https://www.googleapis.com/oauth2/v3/userinfo?access_token=&quot; + encodedAccessToken;
            logger.debug(&quot;Google API URL: {}&quot;, url);

            URL obj = new URL(url);
            HttpURLConnection con = (HttpURLConnection) obj.openConnection();
            con.setRequestMethod(&quot;GET&quot;);
            con.setRequestProperty(&quot;Content-Type&quot;, &quot;application/json&quot;);

            int responseCode = con.getResponseCode();
            logger.info(&quot;Google API response code: {}&quot;, responseCode);

            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();
                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();
                logger.info(&quot;Successfully received response from Google API.&quot;);
                return response.toString();
            } else {
                // 실패 시 에러 메시지와 상태 코드 출력
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getErrorStream()));
                String inputLine;
                StringBuffer errorResponse = new StringBuffer();
                while ((inputLine = in.readLine()) != null) {
                    errorResponse.append(inputLine);
                }
                in.close();
                logger.error(&quot;Google API call failed with response code: {}, error: {}&quot;, responseCode, errorResponse.toString());
                throw new RuntimeException(&quot;Google API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: &quot; + responseCode + &quot;, 에러 메시지: &quot; + errorResponse.toString());
            }
        } catch (IOException e) {
            logger.error(&quot;Google API 호출 중 오류 발생: {}&quot;, e.getMessage(), e);
            throw new RuntimeException(&quot;Google API 호출 중 오류 발생&quot;, e);
        }
    }


    private String kakaoApiCall(String accessToken) {
        try {
            String url = &quot;https://kapi.kakao.com/v2/user/me&quot;;
            URL obj = new URL(url);
            HttpURLConnection con = (HttpURLConnection) obj.openConnection();
            con.setRequestMethod(&quot;GET&quot;);
            // Kakao의 경우 Authorization 헤더에 &quot;Bearer&quot; 토큰을 설정해야 합니다.
            con.setRequestProperty(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken);

            int responseCode = con.getResponseCode();
            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();
                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();
                return response.toString();
            } else {
                throw new RuntimeException(&quot;Kakao API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: &quot; + responseCode); // 오류 메시지 한국어로 수정
            }
        } catch (IOException e) {
            throw new RuntimeException(&quot;Kakao API 호출 중 오류 발생&quot;, e); // 오류 메시지 한국어로 수정
        }
    }

    private String naverApiCall(String accessToken) {
        try {
            String url = &quot;https://openapi.naver.com/v1/nid/me&quot;;
            URL obj = new URL(url);
            HttpURLConnection con = (HttpURLConnection) obj.openConnection();
            con.setRequestMethod(&quot;GET&quot;);
            // Naver의 경우 Authorization 헤더에 &quot;Bearer&quot; 토큰을 설정해야 합니다.
            con.setRequestProperty(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken);

            int responseCode = con.getResponseCode();
            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();
                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();
                return response.toString();
            } else {
                throw new RuntimeException(&quot;Naver API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: &quot; + responseCode); // 오류 메시지 한국어로 수정
            }
        } catch (IOException e) {
            throw new RuntimeException(&quot;Naver API 호출 중 오류 발생&quot;, e); // 오류 메시지 한국어로 수정
        }
    }


    // 사용자 정보를 파싱하여 User 객체 생성
    private User parseUserInfo(String userInfo, SocialLoginType socialLoginType, String accessToken) {
        JsonObject jsonObject = JsonParser.parseString(userInfo).getAsJsonObject();

        // socialId와 name을 소셜 로그인 타입별로 분리
        String socialId = &quot;&quot;;
        String name = &quot;&quot;;

        if (socialLoginType == SocialLoginType.GOOGLE) {
            socialId = jsonObject.get(&quot;sub&quot;).getAsString(); // Google은 &quot;sub&quot;를 ID로 사용
            name = jsonObject.get(&quot;name&quot;).getAsString();    // Google에서 제공하는 이름
        } else if (socialLoginType == SocialLoginType.KAKAO) {
            socialId = jsonObject.get(&quot;id&quot;).getAsString(); // Kakao는 &quot;id&quot;를 사용자 ID로 사용
            name = jsonObject.getAsJsonObject(&quot;properties&quot;).get(&quot;nickname&quot;).getAsString(); // Kakao에서 제공하는 nickname
        } else if (socialLoginType == SocialLoginType.NAVER) {
            JsonObject response = jsonObject.getAsJsonObject(&quot;response&quot;); // Naver의 데이터는 response 필드 안에 존재
            socialId = response.get(&quot;id&quot;).getAsString();   // Naver는 &quot;id&quot;를 사용자 ID로 사용
            name = response.get(&quot;name&quot;).getAsString();     // Naver에서 제공하는 이름
        }

        // User 객체에 정보 세팅
        User user = new User();
        user.setSocialId(socialId);             // 소셜 ID 설정
        user.setName(name);                     // 사용자의 이름 설정
        user.setProvider(socialLoginType.name()); // 로그인 제공자 설정
        user.setAccessToken(accessToken);       // 액세스 토큰 설정
        return user;
    }


    // 주어진 소셜 로그인 타입에 맞는 OAuth 객체를 찾는 메서드
    private SocialOauth findSocialOauthByType(SocialLoginType socialLoginType) {
        return socialOauthList.stream()
                .filter(x -&amp;gt; x.type() == socialLoginType)
                .findFirst()
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;알 수 없는 SocialLoginType 입니다.&quot;)); // 지원되지 않는 소셜 로그인 타입 오류
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  핵심 흐름&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 소셜 로그인 요청을 하면 request() 메서드에서 소셜 로그인 URL을 반환합니다.&lt;/li&gt;
&lt;li&gt;사용자가 로그인 후 인증 코드를 받으면, requestAccessToken() 메서드를 통해 액세스 토큰을 요청합니다.&lt;/li&gt;
&lt;li&gt;액세스 토큰을 이용해 getUserInfo() 메서드로 사용자 정보를 받아오고, parseUserInfo()에서 이를 처리하여 User 객체로 변환합니다.&lt;/li&gt;
&lt;li&gt;기존 사용자가 있으면 정보가 갱신되고, 새 사용자는 데이터베이스에 저장됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  각 메서드의 역할과 흐름&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;r&lt;/b&gt;equest(SocialLoginType socialLoginType)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인 요청 URL을 반환합니다.&lt;/li&gt;
&lt;li&gt;주어진 소셜 로그인 타입에 맞는 OAuth 객체를 찾아 리디렉션 URL을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;requestAccessToken(SocialLoginType socialLoginType, String code)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 인증 코드로 액세스 토큰을 요청하는 메서드입니다.&lt;/li&gt;
&lt;li&gt;소셜 로그인 타입에 맞는 OAuth 객체를 사용하여 액세스 토큰을 요청합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;extractAccessTokenFromJson(String accessTokenJson)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON 형식의 응답에서 액세스 토큰만 추출하는 메서드입니다.&lt;/li&gt;
&lt;li&gt;ObjectMapper를 사용하여 JSON을 파싱하고, 액세스 토큰을 추출하여 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;requestAccessTokenAndSaveUser(SocialLoginType socialLoginType, String code)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액세스 토큰을 요청하고, 이를 이용해 사용자 정보를 가져와 사용자 데이터를 저장하는 메서드입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액세스 토큰을 요청하고,&lt;/li&gt;
&lt;li&gt;액세스 토큰을 사용해 사용자 정보를 가져와 파싱한 후 User 객체를 생성하고,&lt;/li&gt;
&lt;li&gt;기존 사용자가 있으면 토큰을 갱신하고, 없다면 새 사용자로 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;getUserInfo(SocialLoginType socialLoginType, String accessToken)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인 제공자(구글, 카카오, 네이버)에 따라 사용자 정보를 가져오는 메서드입니다.&lt;/li&gt;
&lt;li&gt;각 로그인 서비스에 맞는 API 호출 메서드로 분기합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;googleApiCall(String accessToken)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구글 API를 호출하여 사용자 정보를 가져오는 메서드입니다. 액세스 토큰을 이용해 구글의 사용자 정보 API에 요청을 보내고, 응답을 반환합니다.&lt;/li&gt;
&lt;li&gt;URL 인코딩: 먼저 전달된 accessToken을 URL 인코딩하여 Google API 호출에 사용할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;API 호출: https://www.googleapis.com/oauth2/v3/userinfo?access_token=encodedAccessToken URL을 사용하여 GET 요청을 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kakaoApiCall(String accessToken)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카카오 API를 호출하여 사용자 정보를 가져오는 메서드입니다. 카카오의 사용자 정보 API에 액세스 토큰을 사용해 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;Authorization 헤더 설정: 카카오는 Authorization 헤더에 Bearer 타입의 accessToken을 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;API 호출: https://kapi.kakao.com/v2/user/me URL에 GET 요청을 보내 사용자 정보를 조회합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;naverApiCall(String accessToken)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 API를 호출하여 사용자 정보를 가져오는 메서드입니다. 네이버의 사용자 정보 API에 액세스 토큰을 사용해 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;Authorization 헤더 설정: 네이버 API 역시 카카오와 마찬가지로 Authorization 헤더에 Bearer 타입의 accessToken을 사용해야 합니다.&lt;/li&gt;
&lt;li&gt;API 호출: https://openapi.naver.com/v1/nid/me URL로 GET 요청을 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;parseUserInfo(String userInfo, SocialLoginType socialLoginType, String accessToken)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인 서비스의 응답 데이터를 파싱하여 User 객체를 생성하는 메서드입니다.&lt;/li&gt;
&lt;li&gt;각 소셜 로그인 제공자의 응답 형식에 맞게 데이터를 추출하여 User 객체를 생성하고 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;findSocialOauthByType(SocialLoginType socialLoginType)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 소셜 로그인 타입에 맞는 OAuth 객체를 찾아 반환하는 메서드입니다. 제공된 소셜 로그인 타입에 맞는 SocialOauth 객체를 리스트에서 찾아 반환합니다. 만약 해당 타입이 없으면 예외를 던집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ Response DTO 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 소셜 로그인 후 서버에서 반환할 사용자 정보를 캡슐화하는 역할을 하며, API 응답으로 사용자 이름, 액세스 토큰, 로그인 제공자를 포함한 정보를 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1734716255389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class UserResponse {
    private String name;
    private String accessToken;
    private String provider;

    public UserResponse(String name, String accessToken, String provider) {
        this.name = name;
        this.accessToken = accessToken;
        this.provider = provider;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 컨트롤러 구현&lt;/h2&gt;
&lt;pre id=&quot;code_1734716196337&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(value = &quot;/auth&quot;)
@Slf4j
@Tag(name = &quot;OAuth&quot;, description = &quot;소셜 로그인 인증을 시작하고 콜백을 처리하는 API를 제공합니다.&quot;)
public class OauthController {

    private final OauthService oauthService;

    @Operation(
            summary = &quot;소셜 로그인 프로세스 시작&quot;,
            description = &quot;이 API는 사용자를 소셜 로그인 페이지로 리다이렉트하여 인증 절차를 시작합니다.&quot;,
            operationId = &quot;startSocialLogin&quot;,
            parameters = {
                    @Parameter(name = &quot;socialLoginType&quot;, description = &quot;소셜 로그인 유형 (예: Kakao, Google 등).&quot;, required = true)
            })
    @GetMapping(value = &quot;/{socialLoginType}&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; socialLoginType(
            @PathVariable(name = &quot;socialLoginType&quot;) SocialLoginType socialLoginType) {
        log.info(&quot;&amp;gt;&amp;gt; 사용자로부터 SNS 로그인 요청을 받음 :: {} Social Login&quot;, socialLoginType);
        String redirectURL = oauthService.request(socialLoginType);
        return ResponseEntity.ok(redirectURL);  // 리다이렉션 URL을 응답으로 반환
    }

    @Operation(
            summary = &quot;소셜 로그인 콜백 처리 (백엔드에서 사용 X)&quot;,
            description = &quot;사용자가 소셜 로그인 후 콜백 URL로 받은 코드를 통해 액세스 토큰을 요청합니다.&quot;,
            operationId = &quot;handleSocialLoginCallback&quot;,
            parameters = {
                    @Parameter(name = &quot;socialLoginType&quot;, description = &quot;소셜 로그인 유형 (예: Kakao, Google 등).&quot;, required = true),
                    @Parameter(name = &quot;code&quot;, description = &quot;소셜 로그인 API 서버로부터 받은 인증 코드.&quot;, required = true)
            })
    @GetMapping(value = &quot;/{socialLoginType}/callback&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; callback(
            @PathVariable(name = &quot;socialLoginType&quot;) SocialLoginType socialLoginType,
            @RequestParam(name = &quot;code&quot;) String code,
            HttpSession session) {

        log.info(&quot;&amp;gt;&amp;gt; 소셜 로그인 API 서버로부터 받은 code :: {}&quot;, code);

        // 액세스 토큰을 통해 사용자 정보를 받아온 후 저장
        User user = oauthService.requestAccessTokenAndSaveUser(socialLoginType, code);

        if (user != null) {
            log.info(&quot;&amp;gt;&amp;gt; 사용자 정보 DB 저장 완료 :: {}&quot;, user.getName());

            // 세션에 사용자 정보 저장
            session.setAttribute(&quot;loginUser&quot;, user);

            // 로그인한 유저 정보를 response body로 반환
            return ResponseEntity.ok(new UserResponse(user.getName(), user.getAccessToken(), user.getProvider()));
        } else {
            log.error(&quot;&amp;gt;&amp;gt; 사용자 정보 저장 실패&quot;);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(&quot;사용자 정보 저장 실패&quot;);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;socialLoginType 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: 사용자가 소셜 로그인 유형을 선택하면, 해당 소셜 로그인 페이지로 리다이렉트하는 URL을 반환합니다.&lt;/li&gt;
&lt;li&gt;설명: socialLoginType을 경로 변수로 받아 해당 소셜 로그인 페이지로 리다이렉트할 URL을 생성하고 반환합니다.&lt;/li&gt;
&lt;li&gt;HTTP 메서드: GET&lt;/li&gt;
&lt;li&gt;리턴: 소셜 로그인 페이지로 리다이렉트할 URL을 응답 본문에 담아 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;callback 메서드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적: 사용자가 소셜 로그인 후 콜백 URL로 받은 인증 코드를 처리하고, 액세스 토큰을 요청하여 사용자 정보를 저장합니다.&lt;/li&gt;
&lt;li&gt;설명:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;code 파라미터로 받은 인증 코드를 통해 액세스 토큰을 요청합니다.&lt;/li&gt;
&lt;li&gt;액세스 토큰을 통해 사용자 정보를 받아오고, 이를 데이터베이스에 저장한 후 세션에 사용자 정보를 저장합니다.&lt;/li&gt;
&lt;li&gt;이후, 로그인한 사용자 정보를 응답 본문으로 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTP 메서드: GET&lt;/li&gt;
&lt;li&gt;리턴: 사용자 정보가 정상적으로 저장되면 로그인한 사용자 정보를 담은 UserResponse 객체를 반환하고, 실패 시 500 에러와 함께 실패 메시지를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 구현 결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JZuNm/btsLqEx1kQ4/ERodxk0CkcATRbM55iia30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JZuNm/btsLqEx1kQ4/ERodxk0CkcATRbM55iia30/img.png&quot; data-alt=&quot;구글 로그인 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JZuNm/btsLqEx1kQ4/ERodxk0CkcATRbM55iia30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJZuNm%2FbtsLqEx1kQ4%2FERodxk0CkcATRbM55iia30%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;662&quot; height=&quot;120&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구글 로그인 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDdaT/btsLq0tYeRd/lnW9laUDz87KzzQa06aXEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDdaT/btsLq0tYeRd/lnW9laUDz87KzzQa06aXEK/img.png&quot; data-alt=&quot;카카오 로그인 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDdaT/btsLq0tYeRd/lnW9laUDz87KzzQa06aXEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDdaT%2FbtsLq0tYeRd%2FlnW9laUDz87KzzQa06aXEK%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;629&quot; height=&quot;122&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카카오 로그인 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/70koN/btsLrgwt8UV/6SwKzOOQXC4OKIcIpUsexk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/70koN/btsLrgwt8UV/6SwKzOOQXC4OKIcIpUsexk/img.png&quot; data-alt=&quot;네이버 로그인 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/70koN/btsLrgwt8UV/6SwKzOOQXC4OKIcIpUsexk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F70koN%2FbtsLrgwt8UV%2F6SwKzOOQXC4OKIcIpUsexk%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;674&quot; height=&quot;94&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네이버 로그인 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 소셜 로그인 모두 제대로 응답되는 것을 확인할 수 있었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고&lt;/h2&gt;
&lt;figure id=&quot;og_1734705331021&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;REST API 사용 &amp;nbsp;|&amp;nbsp; Identity Platform Documentation &amp;nbsp;|&amp;nbsp; Google Cloud&quot; data-og-description=&quot;의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. REST API 사용 이 문서에서는 Identity Platform REST API를 사용하여 사용자 로그인 및 토큰 작업 등의 일&quot; data-og-host=&quot;cloud.google.com&quot; data-og-source-url=&quot;https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko&quot; data-og-url=&quot;https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHPQZS/hyXOp11QfG/sN4JZBQqAHCRdzmKLnFl5k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHPQZS/hyXOp11QfG/sN4JZBQqAHCRdzmKLnFl5k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;REST API 사용 &amp;nbsp;|&amp;nbsp; Identity Platform Documentation &amp;nbsp;|&amp;nbsp; Google Cloud&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. REST API 사용 이 문서에서는 Identity Platform REST API를 사용하여 사용자 로그인 및 토큰 작업 등의 일&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloud.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1734705235546&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#security-client-secret&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/N90BK/hyXOjtVVHQ/cxkGEikBzespknK7uLqbq0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cUdka6/hyXOlrMc3W/3uYwwngn7LEmJWz1coM3Z1/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/I111u/hyXOiaKzC9/xrqQEmjjw9n0M9SwbcEjIk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#security-client-secret&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#security-client-secret&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/N90BK/hyXOjtVVHQ/cxkGEikBzespknK7uLqbq0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cUdka6/hyXOlrMc3W/3uYwwngn7LEmJWz1coM3Z1/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/I111u/hyXOiaKzC9/xrqQEmjjw9n0M9SwbcEjIk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1734705827156&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네이버 로그인 개발가이드 - LOGIN&quot; data-og-description=&quot;네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디 &quot; data-og-host=&quot;developers.naver.com&quot; data-og-source-url=&quot;https://developers.naver.com/docs/login/devguide/devguide.md&quot; data-og-url=&quot;https://developers.naver.com/docs/login/devguide/devguide.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Nin5f/hyXOcVTM3g/VTLj1K5ykKz32BfnrHmlI1/img.png?width=880&amp;amp;height=1604&amp;amp;face=0_0_880_1604,https://scrap.kakaocdn.net/dn/ccP5iK/hyXOmqC3yc/gHW7vuvgfKXLOdlFJvXmzk/img.png?width=880&amp;amp;height=591&amp;amp;face=0_0_880_591,https://scrap.kakaocdn.net/dn/cmy8KD/hyXOpAXLZP/QESx5BoZGmqPERDAa8VKgk/img.png?width=880&amp;amp;height=474&amp;amp;face=0_0_880_474&quot;&gt;&lt;a href=&quot;https://developers.naver.com/docs/login/devguide/devguide.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.naver.com/docs/login/devguide/devguide.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Nin5f/hyXOcVTM3g/VTLj1K5ykKz32BfnrHmlI1/img.png?width=880&amp;amp;height=1604&amp;amp;face=0_0_880_1604,https://scrap.kakaocdn.net/dn/ccP5iK/hyXOmqC3yc/gHW7vuvgfKXLOdlFJvXmzk/img.png?width=880&amp;amp;height=591&amp;amp;face=0_0_880_591,https://scrap.kakaocdn.net/dn/cmy8KD/hyXOpAXLZP/QESx5BoZGmqPERDAa8VKgk/img.png?width=880&amp;amp;height=474&amp;amp;face=0_0_880_474');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네이버 로그인 개발가이드 - LOGIN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트  /CocO  </category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/178</guid>
      <comments>https://blu-blu.tistory.com/178#entry178comment</comments>
      <pubDate>Sat, 21 Dec 2024 02:44:17 +0900</pubDate>
    </item>
    <item>
      <title>[SQLD 55회] 컴퓨터공학과 3일 공부 후 합격한 후기</title>
      <link>https://blu-blu.tistory.com/177</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DlNd2/btsK9gcUrlE/dTIgs4fEB5tz04AJkHZgs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DlNd2/btsK9gcUrlE/dTIgs4fEB5tz04AJkHZgs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DlNd2/btsK9gcUrlE/dTIgs4fEB5tz04AJkHZgs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDlNd2%2FbtsK9gcUrlE%2FdTIgs4fEB5tz04AJkHZgs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;366&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 개발과 자격증 공부가 겹치다 보니 시간을 많이 못 낼 것 같아서 책은 패스하고, 유튜브만 열심히 파봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브 링크 찾아와주고 같이 공부해주고 &lt;b&gt;긍정적인 얘기&lt;/b&gt;만 해준 승햄아~~ 정말 고맙다! 너 없었으면 합격 못했을거야  &lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(지금이 연예대상 시즌이라 따라해봄)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 시험 후기를 말해보자면, 사실 지금까지 자격증 시험 볼 때 시간 부족하다는 느낌은 단 1도 없었거든요? 그런데 이번엔 살짝 빡빡하더라구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소엔 OMR 카드도 &quot;이건 진짜 100% 답이다!&quot; 싶은 것만 체크하는데, 이번엔 50문제 중 40문제 풀었을 때 이미 시간이 30분밖에 안 남은 거예요. 이때부터는 '아 몰라, 내 선택이 정답이다!' 하고 바로바로 OMR에 체크했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게 풀고 나니까, 몇 문제는 확실히 맞췄는지 못 세겠더라구요. 평소엔 몇 개는 확실히 맞고, 몇 개는 운빨로 걸쳐서 합격 여부를 대충 가늠했었는데, 이번엔 그게 안 돼서 시험 점수 발표날까지 진짜 두근두근...  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론: 합격했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLqHVb/btsK8ZoWsdZ/5FbXsQy2loo0BbKQpTYPbK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLqHVb/btsK8ZoWsdZ/5FbXsQy2loo0BbKQpTYPbK/img.jpg&quot; data-alt=&quot;멋있어 너.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLqHVb/btsK8ZoWsdZ/5FbXsQy2loo0BbKQpTYPbK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLqHVb%2FbtsK8ZoWsdZ%2F5FbXsQy2loo0BbKQpTYPbK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;226&quot; height=&quot;223&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;멋있어 너.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;br /&gt;코테랑 개발 공부도 틈틈이 챙겨야겠져? ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공부방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&amp;amp;si=PLsohJhvxGr0EYnv&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&amp;amp;si=PLsohJhvxGr0EYnv&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733469015355&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SQLD_NEW&quot; data-og-description=&quot;2024년 3,4회차 시험 대비하는 분들인 이 재생목록 영상을 시청하시면 됩니다 (기존 영상 및 자료에서 누락된 부분 및 오탈자 수정하여 재편집된 영상입니다)&quot; data-og-host=&quot;www.youtube.com&quot; data-og-source-url=&quot;https://youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&amp;amp;si=PLsohJhvxGr0EYnv&quot; data-og-url=&quot;http://www.youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bWMqHj/hyXKxECWqj/8SPZbeEZo7Kxjfp66SmxzK/img.jpg?width=480&amp;amp;height=270&amp;amp;face=87_67_124_107,https://scrap.kakaocdn.net/dn/bJnSuR/hyXKsQRoBx/HsnMBrD1CVmJhHxcK9cp11/img.jpg?width=480&amp;amp;height=270&amp;amp;face=87_67_124_107&quot;&gt;&lt;a href=&quot;https://youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&amp;amp;si=PLsohJhvxGr0EYnv&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://youtube.com/playlist?list=PLbflMVhwy2jOoDIm7kVgt1_sYDJm_oBaD&amp;amp;si=PLsohJhvxGr0EYnv&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bWMqHj/hyXKxECWqj/8SPZbeEZo7Kxjfp66SmxzK/img.jpg?width=480&amp;amp;height=270&amp;amp;face=87_67_124_107,https://scrap.kakaocdn.net/dn/bJnSuR/hyXKsQRoBx/HsnMBrD1CVmJhHxcK9cp11/img.jpg?width=480&amp;amp;height=270&amp;amp;face=87_67_124_107');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SQLD_NEW&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2024년 3,4회차 시험 대비하는 분들인 이 재생목록 영상을 시청하시면 됩니다 (기존 영상 및 자료에서 누락된 부분 및 오탈자 수정하여 재편집된 영상입니다)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.youtube.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;그리고 위 유튜브에서 제공하는 pdf도 별도로 친구와 함께 구매해서 pdf로 필기해가며 들었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째날, 1강 듣기&lt;br /&gt;둘째날, 2강 듣기&lt;br /&gt;셋째날, 3,4강 듣기&lt;br /&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;아무래도 전공자여서 아는 내용이 꽤 있어서 이해하긴 어렵지 않았지만, 생각보다 외울게 꽤 많더라구요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 시험보기 직전까지 외우고 시험봤습니다.&amp;nbsp;&lt;br /&gt;시험 직전까지 최대한 생소한 개념 위주로 표시해두고 이 위주로 외웠습니다.&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;저는 노랭이 책을 풀진 않았지만,, 그냥 저처럼 책 사지 않고 유튜브 강의로 개념 숙지하는 것도 좋을 거 같아요! 위 유튜브에 기출문제 풀이도 있긴해서 기출까지도 대비할 수 있을 거 같아요. 물론 전 보지 않았지만,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 비전공자는 좀 듣기 어려운 유튜브 강의 같아요! 전공자 위주로 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자격증  /SQLD</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/177</guid>
      <comments>https://blu-blu.tistory.com/177#entry177comment</comments>
      <pubDate>Fri, 6 Dec 2024 16:51:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Request Body가 계속 Null일 때. Import를 제대로 했나에 대하여.</title>
      <link>https://blu-blu.tistory.com/176</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8FcV9/btsK8n3JKNj/gAKIcE0Q5FFkWGBo6kZfl0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8FcV9/btsK8n3JKNj/gAKIcE0Q5FFkWGBo6kZfl0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8FcV9/btsK8n3JKNj/gAKIcE0Q5FFkWGBo6kZfl0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8FcV9%2FbtsK8n3JKNj%2FgAKIcE0Q5FFkWGBo6kZfl0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;224&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;안녕하세요.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;제가 import를 잘 못해서 Request Body를 null로만 받아온 사람으로 보이시나요?&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;맞긴합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;지피티가 제가 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;JSON 데이터와 DTO 매칭 잘 했는지 의심을 계속해서 좀 속상했는데요. 코드를 잘 살펴보니 import를 잘 못했더라구요?&amp;nbsp;&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;지피티도 못 찾을만한 실수에 대하여 글을 써봅니다.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐&amp;nbsp;RequestBody를&amp;nbsp;사용할&amp;nbsp;때&amp;nbsp;발생한&amp;nbsp;문제와&amp;nbsp;해결&amp;nbsp;방법&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Spring Boot에서 REST API를 개발할 때 @RequestBody는 클라이언트로부터 JSON 데이터를 객체로 변환하는 데 자주 사용됩니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;e60a4c4b-152c-4542-847a-abdeb2b9a077&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 어노테이션을 사용할 때 올바르지 않은 라이브러리를 import하면 의도치 않은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;br /&gt;import org.springframework.web.bind.annotation.RequestBody와 &lt;br /&gt;import io.swagger.v3.oas.annotations.parameters.RequestBody를 &lt;br /&gt;혼동하여 발생한 문제와 이를 해결한 과정을 정리해 보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST 컨트롤러에서 @RequestBody를 사용해 클라이언트의 요청 데이터를 처리하려 했지만, 계속해서 null 값이 반환되는 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 가정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733388527733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1&quot;)
public class UserController {

    @PostMapping(&quot;/users&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; createUser(@RequestBody UserDto userDto) {
        return ResponseEntity.ok(&quot;User created: &quot; + userDto.getName());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 클라이언트가 JSON 데이터를 POST로 전송하면 이를 UserDto 객체로 매핑해야 합니다. 그러나 실행 중 userDto가 항상 null로 반환되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;❗해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 디버깅하는 과정에서 다음과 같은 사실을 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestBody 어노테이션을 import할 때 잘못된 패키지를 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733388681824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import io.swagger.v3.oas.annotations.parameters.RequestBody; // 잘못된 import&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 관련 어노테이션은 API 문서를 위한 메타데이터를 추가하는 역할을 하며, Spring MVC의 @RequestBody와는 전혀 다른 기능을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733389122197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.web.bind.annotation.RequestBody; // 올바른 import&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC의 @RequestBody는 다음과 같이 올바른 패키지를 import해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  문제 원인 분석 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 import가 제대로 되었다면 아래 사항들을 확인해보시길 바랍니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아래 사항들을 모두 잘 했는데 아무리 해도 null이어서 확인해보니 import 오류였습니다.. &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣&amp;nbsp;JSON 데이터와 DTO 매칭 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON의 키(name, age)는 DTO 클래스의 필드명과 정확히 일치해야 합니다.&lt;br /&gt;불일치가 있을 경우, 매핑에 실패하거나 일부 필드가 null로 설정됩니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2️⃣ JSON 매칭을 위한 어노테이션 추가&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 클래스에 @JsonProperty 또는 @JsonNaming을 사용하여 JSON 데이터와 DTO 필드 간 매칭을 명시적으로 지정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733388986641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.annotation.JsonProperty;

public class UserDto {
    @JsonProperty(&quot;full_name&quot;)
    private String name;
    private int age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아래와 같이 필드명이 name이어도 full_name이라는 키로 JSON을 전달할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733389029780&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;full_name&quot;: &quot;코양이&quot;,
  &quot;age&quot;: 30
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3️⃣ build.gradle 설정 확인&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 매핑은 Jackson 라이브러리를 통해 이루어집니다. Jackson 관련 디펜던시가 누락되었는지 확인하세요.&lt;/p&gt;
&lt;pre id=&quot;code_1733389089379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' // 예시 버전
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' // Java 8 Date/Time 지원&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  교훈과 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1) 어노테이션의 역할 명확히 이해하기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring MVC의 @RequestBody: 요청 본문 데이터를 객체로 변환.&lt;/li&gt;
&lt;li&gt;Swagger의 @RequestBody: API 문서 생성용 메타데이터 추가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 어노테이션의 역할이 완전히 다르므로 용도에 맞는 패키지를 선택해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2) IDE의 자동 완성 기능 주의하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE에서 자동 완성 기능을 사용할 때 잘못된 패키지가 선택될 수 있습니다. 따라서 import문을 항상 확인하는 습관을 들여야 할 거 같습니다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot와 Swagger를 함께 사용하면서 @RequestBody를 잘못 import해 발생한 문제는 흔히 겪는 실수입니다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술 지식 쌓아가기  /Backend  </category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/176</guid>
      <comments>https://blu-blu.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 5 Dec 2024 18:03:19 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 16953 A &amp;rarr; B | DFS, BFS, 그래프 | 실버 Ⅱ | JAVA</title>
      <link>https://blu-blu.tistory.com/175</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/16953&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/16953&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 풀이 과정&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;문제 정의&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BFS 탐색에서 큐를 이용해 현재 숫자와 연산 횟수를 저장합니다.&lt;/li&gt;
&lt;li&gt;큐에서 숫자를 꺼내 가능한 연산(2를 곱하거나, 1을 추가)을 수행한 결과를 큐에 삽입합니다.&lt;/li&gt;
&lt;li&gt;연산이 끝난 후 목표 값 B에 도달하면 최소 연산 횟수를 출력합니다.&lt;/li&gt;
&lt;li&gt;BFS를 종료했는데도 B에 도달하지 못한 경우 -1을 출력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제약 조건&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자가 B를 초과하면 더 이상 탐색할 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 정답 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1732710259944&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        long A = sc.nextLong();
        long B = sc.nextLong();
        sc.close();

        System.out.println(bfs(A, B));
    }

    private static int bfs(long A, long B) {
        Queue&amp;lt;long[]&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        queue.offer(new long[]{A, 1}); // 현재 값과 연산 횟수 저장

        while (!queue.isEmpty()) {
            long[] current = queue.poll();
            long currentValue = current[0];
            int steps = (int) current[1];

            // B에 도달한 경우 연산 횟수 반환
            if (currentValue == B) {
                return steps;
            }

            // 다음 가능한 연산 수행
            long next1 = currentValue * 2;
            long next2 = currentValue * 10 + 1;

            // 연산 결과가 B 이하일 때만 큐에 추가
            if (next1 &amp;lt;= B) {
                queue.offer(new long[]{next1, steps + 1});
            }
            if (next2 &amp;lt;= B) {
                queue.offer(new long[]{next2, steps + 1});
            }
        }

        // B에 도달할 수 없는 경우
        return -1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 코드 설명&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;입력 처리&lt;/b&gt;: A와 B를 입력받습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BFS 탐색&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐에 초기 값 (A, 1)을 추가합니다. 여기서 1은 첫 번째 연산입니다.&lt;/li&gt;
&lt;li&gt;큐에서 값을 꺼내 현재 값이 B인지 확인합니다.&lt;/li&gt;
&lt;li&gt;현재 값에서 가능한 연산 (2를 곱하거나, 1을 추가)을 수행하고 결과를 큐에 추가합니다.&lt;/li&gt;
&lt;li&gt;연산 결과가 B를 초과하면 더 이상 탐색하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 반환&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BFS를 통해 B에 도달하면 연산 횟수를 반환합니다.&lt;/li&gt;
&lt;li&gt;BFS 탐색이 끝난 후에도 도달하지 못하면 -1을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>코딩 테스트 일지  </category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/175</guid>
      <comments>https://blu-blu.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 27 Nov 2024 21:25:02 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 10026 적록색약 | DFS, BFS, 그래프 | 실버 Ⅱ | JAVA</title>
      <link>https://blu-blu.tistory.com/174</link>
      <description>&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;정답 코드&lt;/h3&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static int N;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static char[][] grid;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static boolean[][] visitedNormal, visitedColorBlind;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static int[] dx = {0, 0, -1, 1};
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static int[] dy = {-1, 1, 0, 0};

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static void main(String[] args) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Scanner sc = new Scanner(System.in);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;N = sc.nextInt();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;grid = new char[N][N];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;visitedNormal = new boolean[N][N];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;visitedColorBlind = new boolean[N][N];

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (int i = 0; i &amp;lt; N; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;grid[i] = sc.next().toCharArray();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int normalCount = 0;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int colorBlindCount = 0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 적록색약이 아닌 경우
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (int i = 0; i &amp;lt; N; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (int j = 0; j &amp;lt; N; j++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!visitedNormal[i][j]) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dfs(i, j, grid[i][j], false);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;normalCount++;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!visitedColorBlind[i][j]) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dfs(i, j, grid[i][j], true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;colorBlindCount++;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(normalCount + &quot; &quot; + colorBlindCount);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// DFS 탐색
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static void dfs(int x, int y, char color, boolean isColorBlind) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isColorBlind) visitedColorBlind[x][y] = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else visitedNormal[x][y] = true;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (int i = 0; i &amp;lt; 4; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int nx = x + dx[i];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int ny = y + dy[i];

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (nx &amp;lt; 0 || ny &amp;lt; 0 || nx &amp;gt;= N || ny &amp;gt;= N) continue;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isColorBlind &amp;amp;&amp;amp; !visitedColorBlind[nx][ny] &amp;amp;&amp;amp; isSameColor(color, grid[nx][ny], true)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dfs(nx, ny, color, true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (!isColorBlind &amp;amp;&amp;amp; !visitedNormal[nx][ny] &amp;amp;&amp;amp; isSameColor(color, grid[nx][ny], false)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dfs(nx, ny, color, false);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 색상 비교 (적록색약 여부에 따라 다름)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static boolean isSameColor(char color1, char color2, boolean isColorBlind) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isColorBlind) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (color1 == 'R' || color1 == 'G') return color2 == 'R' || color2 == 'G';
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return color1 == color2;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;코드 설명&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;1. 입력 처리&lt;br&gt;&lt;br&gt;N과 그림 데이터를 입력받습니다.&lt;br&gt;&lt;br&gt;각각의 visitedNormal과 visitedColorBlind 배열로 적록색약 여부에 따라 방문 여부를 관리합니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;2. DFS 탐색&lt;br&gt;&lt;br&gt;시작점에서 인접한 칸들을 재귀적으로 탐색하며 같은 구역으로 판단합니다.&lt;br&gt;&lt;br&gt;적록색약의 경우 R과 G를 동일한 색으로 간주합니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;3. 구역 개수 계산&lt;br&gt;&lt;br&gt;방문하지 않은 시작점을 기준으로 DFS를 호출하여 구역 개수를 증가시킵니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;4. 색상 비교 로직&lt;br&gt;&lt;br&gt;isSameColor 함수로 적록색약 여부에 따른 색상 비교를 수행합니다&lt;/p&gt;</description>
      <category>코딩 테스트 일지  </category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/174</guid>
      <comments>https://blu-blu.tistory.com/174#entry174comment</comments>
      <pubDate>Tue, 26 Nov 2024 20:02:40 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 11725 트리의 부모 찾기 | DFS, BFS, 그래프 | 실버 Ⅱ | JAVA</title>
      <link>https://blu-blu.tistory.com/173</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11725&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11725&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐ 문제 해결을 위한 접근 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 루트 없는 트리에서 각 노드의 부모를 찾는 문제입니다. 트리의 구조를 BFS(너비 우선 탐색) 또는 DFS(깊이 우선 탐색)로 탐색하여 각 노드의 부모를 기록하면 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;알고리즘 설계&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력 파싱: 주어진 입력을 읽어 그래프를 인접 리스트로 표현합니다.&lt;/li&gt;
&lt;li&gt;탐색:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루트 노드(1번 노드)를 기준으로 BFS를 수행합니다.&lt;/li&gt;
&lt;li&gt;탐색 중 각 노드의 부모를 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;출력: 2번 노드부터 N번 노드까지의 부모를 출력합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 고려 사항&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 크기가 최대 100,000으로 탐색 알고리즘은 &lt;span&gt;&lt;span&gt;O(N)O(N)&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 복잡도를 가지는 방식(BFS/DFS)을 사용해야 효율적으로 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;양방향 그래프로 주어지기 때문에 방문 처리를 통해 중복 탐색을 방지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 정답 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1732543651951&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        
        // 그래프 인접 리스트 초기화
        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt;= n; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }

        // 간선 정보 입력
        for (int i = 0; i &amp;lt; n - 1; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            graph.get(a).add(b);
            graph.get(b).add(a);
        }

        // 부모를 기록할 배열
        int[] parent = new int[n + 1];
        boolean[] visited = new boolean[n + 1];

        // BFS를 이용해 부모 찾기
        Queue&amp;lt;Integer&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        queue.offer(1);
        visited[1] = true;

        while (!queue.isEmpty()) {
            int current = queue.poll();

            for (int next : graph.get(current)) {
                if (!visited[next]) {
                    visited[next] = true;
                    parent[next] = current;
                    queue.offer(next);
                }
            }
        }

        // 결과 출력
        StringBuilder sb = new StringBuilder();
        for (int i = 2; i &amp;lt;= n; i++) {
            sb.append(parent[i]).append(&quot;\n&quot;);
        }
        System.out.print(sb);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 코드 풀이&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 문제 이해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 트리의 구조를 활용하여 루트 노드(1번 노드)를 기준으로 각 노드의 부모를 찾는 문제입니다. 트리는 사이클이 없는 연결 그래프이므로 BFS 또는 DFS를 사용해 탐색을 진행하며, 각 노드의 부모를 기록할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 입력 및 그래프 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력된 트리의 간선 정보를 인접 리스트로 변환합니다. 이 과정에서 노드 &lt;span&gt;&lt;span&gt;aa&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;a&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와 &lt;span&gt;&lt;span&gt;bb&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;b&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 간의 양방향 간선을 저장합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. BFS를 이용한 탐색&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 노드부터 BFS를 수행하며 각 노드의 부모를 parent 배열에 기록합니다. 방문 배열을 활용해 탐색 중복을 방지합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 결과 출력&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모를 기록한 parent 배열을 사용하여 2번 노드부터 &lt;span&gt;&lt;span&gt;NN&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 노드까지의 부모를 출력합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 시간 및 공간 복잡도&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 복잡도: &lt;span&gt;&lt;span&gt;O(N)O(N)&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; (그래프의 모든 노드를 한 번씩 방문)&lt;/li&gt;
&lt;li&gt;공간 복잡도: &lt;span&gt;&lt;span&gt;O(N)O(N)&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; (그래프 인접 리스트와 부모 배열 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 코드 설명&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래프 초기화: 각 노드에 연결된 다른 노드를 인접 리스트로 저장합니다.&lt;/li&gt;
&lt;li&gt;BFS 탐색: 큐를 이용해 탐색하며 부모 노드를 기록합니다.&lt;/li&gt;
&lt;li&gt;출력: 탐색 완료 후 2번 노드부터 부모 노드를 출력합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>코딩 테스트 일지  </category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/173</guid>
      <comments>https://blu-blu.tistory.com/173#entry173comment</comments>
      <pubDate>Mon, 25 Nov 2024 23:08:07 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 11724 연결 요소의 개수 | DFS, BFS, 그래프 | 실버 Ⅱ | JAVA</title>
      <link>https://blu-blu.tistory.com/172</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11724&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11724&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⭐&amp;nbsp;해결 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래프를 인접 리스트로 표현.&lt;/li&gt;
&lt;li&gt;DFS(깊이 우선 탐색) 또는 BFS(너비 우선 탐색)로 모든 정점을 방문.&lt;/li&gt;
&lt;li&gt;방문한 정점과 방문하지 않은 정점을 기준으로 연결 요소를 구분.&lt;/li&gt;
&lt;li&gt;그래프 탐색을 통해 모든 정점을 방문하고, 방문하지 않은 새로운 정점 발견 시 연결 요소 개수 증가.&lt;/li&gt;
&lt;li&gt;탐색 알고리즘으로 DFS를 선택.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 정답 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1732442889122&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class ConnectedComponents {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        
        // 입력 받기
        int n = sc.nextInt(); // 정점의 개수
        int m = sc.nextInt(); // 간선의 개수
        
        // 그래프 초기화
        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt;= n; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }
        
        // 간선 입력 받기
        for (int i = 0; i &amp;lt; m; i++) {
            int u = sc.nextInt();
            int v = sc.nextInt();
            graph.get(u).add(v);
            graph.get(v).add(u); // 무방향 그래프이므로 양쪽 추가
        }
        
        // 방문 여부 배열
        boolean[] visited = new boolean[n + 1];
        int components = 0;
        
        // DFS로 연결 요소 탐색
        for (int i = 1; i &amp;lt;= n; i++) {
            if (!visited[i]) {
                dfs(i, graph, visited);
                components++; // 새로운 연결 요소 발견
            }
        }
        
        // 결과 출력
        System.out.println(components);
    }
    
    private static void dfs(int node, List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; graph, boolean[] visited) {
        visited[node] = true;
        for (int neighbor : graph.get(node)) {
            if (!visited[neighbor]) {
                dfs(neighbor, graph, visited);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 코드 설명&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;그래프 입력 및 초기화&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력으로 주어진 정점 &lt;span&gt;&lt;span&gt;NN&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;과 간선 &lt;span&gt;&lt;span&gt;MM&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;M&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;을 사용하여 인접 리스트로 그래프를 구성합니다.&lt;/li&gt;
&lt;li&gt;무방향 그래프이므로 양쪽 정점 간 연결을 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DFS 탐색&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방문 여부를 기록하는 visited 배열 사용.&lt;/li&gt;
&lt;li&gt;정점 &lt;span&gt;&lt;span&gt;ii&lt;/span&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;에서 탐색을 시작하고 연결된 모든 정점을 재귀적으로 방문.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연결 요소 개수&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방문하지 않은 정점 발견 시, 새로운 연결 요소를 찾았으므로 components 값을 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출력&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최종적으로 연결 요소 개수를 출력.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>코딩 테스트 일지  </category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/172</guid>
      <comments>https://blu-blu.tistory.com/172#entry172comment</comments>
      <pubDate>Sun, 24 Nov 2024 19:09:19 +0900</pubDate>
    </item>
    <item>
      <title>[IT 뉴스] 오픈AI, 구글 왕국에 도전장 &amp;ndash; 웹브라우저와 AI로 생태계 흔들기</title>
      <link>https://blu-blu.tistory.com/171</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://naver.me/xgNBtKzR&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://naver.me/xgNBtKzR&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[AI한입뉴스]오픈AI, 크롬 자리까지 노리나…구글 판 흔든다&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;오픈AI가 구글의 텃밭인 검색 엔진을 넘어 웹브라우저를 넘보고 있습니다. 구글 안드로이드 진영의 핵심 파트너인 삼성전자와는 새로운 동맹을 추진할 조짐인데요. 구글이 크롬 매각 위기에 처&quot; data-og-host=&quot;n.news.naver.com&quot; data-og-source-url=&quot;https://naver.me/xgNBtKzR&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lZ1KC/hyXzOBH68s/AgD9zkkINubF4L5sJiDKkk/img.jpg?width=745&amp;amp;height=876&amp;amp;face=0_0_745_876&quot; data-og-url=&quot;https://n.news.naver.com/mnews/article/277/0005505084&quot;&gt;&lt;a href=&quot;https://n.news.naver.com/mnews/article/277/0005505084&quot; target=&quot;_blank&quot; data-source-url=&quot;https://naver.me/xgNBtKzR&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lZ1KC/hyXzOBH68s/AgD9zkkINubF4L5sJiDKkk/img.jpg?width=745&amp;amp;height=876&amp;amp;face=0_0_745_876')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;[AI한입뉴스]오픈AI, 크롬 자리까지 노리나…구글 판 흔든다&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;오픈AI가 구글의 텃밭인 검색 엔진을 넘어 웹브라우저를 넘보고 있습니다. 구글 안드로이드 진영의 핵심 파트너인 삼성전자와는 새로운 동맹을 추진할 조짐인데요. 구글이 크롬 매각 위기에 처&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;n.news.naver.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;본 글은 위 뉴스를 읽고 적는 글입니다!&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;✅️ 핵심 요약&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;오픈AI가 구글의 핵심 영역인 웹브라우저와 모바일 생태계를 정조준하며 도전장을 내밀었습니다. 크롬 개발자 영입과 브라우저 프로토타입 개발, 삼성전자와의 협업 논의 등을 통해 구글의 독점적인 검색 및 광고 시장에 균열을 시도하고 있습니다. 이는 검색, 브라우저, AI 생태계를 재편하려는 야심으로, 구글과의 정면 승부를 예고합니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  느낀 점과 배울 점&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;1. 도전 정신의 중요성&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;오픈AI는 구글이라는 거대 기업에 정면으로 도전하며 기존의 질서를 흔들고 있습니다. 이는 혁신을 이루기 위해 기존 틀에 얽매이지 않고 새로운 영역에 도전하는 자세가 얼마나 중요한지 보여줍니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;2. 기술과 비즈니스의 융합 필요성&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;단순히 뛰어난 기술을 개발하는 것만으로는 충분하지 않습니다. 오픈AI는 AI 기술을 브라우저, 검색, 모바일 생태계 등 다양한 비즈니스 모델에 통합하며 실질적인 가치를 창출하고 있습니다. 기술력과 사업 전략의 조화가 성공의 핵심임을 느꼈습니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;3. 생태계 경쟁의 치열함&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;기술 전쟁은 단일 제품이나 서비스의 경쟁을 넘어, 생태계를 둘러싼 다각적 전투로 진화하고 있습니다. 기업뿐만 아니라 사용자와 파트너 모두를 끌어들이는 생태계 전략의 중요성을 깨닫게 됩니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;⭐️ 결론&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;오픈AI와 구글 간의 경쟁은 AI와 기술 산업의 미래를 가늠하는 중요한 사건입니다. 이를 통해 변화에 유연하게 대처하고, 기존의 한계를 넘어서는 혁신과 협력을 지속해야 한다는 교훈을 얻을 수 있었습니다&lt;br&gt;&lt;br&gt;저는 이러한 흐름을 보며 스스로 변화에 민감하게 반응하고, 끊임없이 학습하며, 새로운 도전에 나설 준비를 해야 한다고 생각했습니다. &lt;br&gt;&lt;br&gt;오픈AI처럼 용기 있게 기존 강자에 도전하는 자세와 구글처럼 위협 속에서도 생태계를 강화하려는 지속적 혁신은 개인과 조직 모두에게 중요한 메시지를 전달합니다. 결국, 승자는 변화와 경쟁을 두려워하지 않는 자라는 것을 알게 되었습니다.&lt;br&gt;&lt;br&gt;굉장히 흥미로운 뉴스 기사였습니다!&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;</description>
      <category>기술 지식 쌓아가기  /양대기 기자  ️</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>코양이 </author>
      <guid isPermaLink="true">https://blu-blu.tistory.com/171</guid>
      <comments>https://blu-blu.tistory.com/171#entry171comment</comments>
      <pubDate>Sat, 23 Nov 2024 17:18:07 +0900</pubDate>
    </item>
  </channel>
</rss>